import { build } from 'esbuild' const source = ` import { buildSpineJson } from './src/export/spineJsonExporter.ts' import { validateSpineJson } from './src/export/spineJsonValidator.ts' const frameTexture = { width: 8, height: 8 } const single = { id: 1, imageMode: 'single', sequenceFrames: [], textureName: 'star.png', textureFolder: '', anchorX: 0.5, anchorY: 0.5, blend: 'normal', independentAlpha: 'off', texture: frameTexture, } const sequence = { id: 2, imageMode: 'sequence', sequenceFrames: [ { name: 'seq_0.png', previewUrl: '', texture: frameTexture }, { name: 'seq_1.png', previewUrl: '', texture: frameTexture }, { name: 'seq_2.png', previewUrl: '', texture: frameTexture }, ], textureName: 'seq.png', textureFolder: '', anchorX: 0.5, anchorY: 0.5, blend: 'add', independentAlpha: 'off', texture: frameTexture, } const trailState = (x) => ({ bones: [0, 1, 2].map((index) => ({ boneName: 'p_0_trail_' + index, x: x - index * 5, y: 0, rotation: 0, worldX: x - index * 5, worldY: 0, worldRotation: 0, })), width: 20, alpha: 1, colorHex: 0xffffff, resourceId: 1, }) const particle = (boneName, spawnId, resourceId, imageFrameIndex, x, trail) => ({ active: true, boneName, spawnId, resourceId, imageFrameIndex, imageVisible: true, colorHex: 0xffffff, alpha: 1, x, y: 0, rotation: 0, scaleX: 1, scaleY: 1, worldX: x, worldY: 0, worldRotation: 0, worldScaleX: 1, worldScaleY: 1, trailState: trail, }) const frames = Array.from({ length: 6 }, (_, frame) => [ particle('p_0', 1, 1, 0, frame, trailState(frame)), particle('p_1', 2, 2, frame % 3, frame * 2), ]) const system = { id: 1, animation: 'animation', visible: true, frames, config: { name: 'lizi_1', mode: 'burst', lifeMode: 'fixed', lifeMin: 1, lifeMax: 1, delay: 0, imageResources: [single, sequence], trail: true, trailResources: [{ id: 1, texture: frameTexture, textureName: 'trail.png', blend: 'normal', boneCount: 3, lengthMode: 'fixed', length: 100, widthSizeMode: 'fixed', width: 100, widthMode: 'particle', }], }, } const settings = { imagesPath: './images/', spineVersion: '4.2', keyframeCurve: 'linear', fps: 30, omitFps: false, integerFrameAlignment: true, bonePool: true, excludeEmptyAnimations: false, fileName: 'naming.json', } const result = buildSpineJson({ systems: [system], timeline: { fps: 30, animations: ['animation'] }, settings }) validateSpineJson(result.text, '4.2') const json = JSON.parse(result.text) const failures = [] const bone = (name) => json.bones.find((item) => item.name === name) const slot = (name) => json.slots.find((item) => item.name === name) // 1. root 下的粒子根节点 = 系统名 if (!bone('lizi_1') || bone('lizi_1').parent !== 'root') failures.push('粒子根节点应为 lizi_1 且父级为 root') // 2. 粒子骨骼 = 系统名_序号(父级为根节点) for (const name of ['lizi_1_0', 'lizi_1_1']) { if (!bone(name) || bone(name).parent !== 'lizi_1') failures.push('粒子骨骼命名/父级错误:' + name) } // 3. 插槽 = 系统名_序号_颜色模式(挂在对应骨骼下) if (!slot('lizi_1_0_normal') || slot('lizi_1_0_normal').bone !== 'lizi_1_0') failures.push('插槽应为 lizi_1_0_normal 且挂在 lizi_1_0') if (!slot('lizi_1_1_add') || slot('lizi_1_1_add').bone !== 'lizi_1_1') failures.push('插槽应为 lizi_1_1_add 且挂在 lizi_1_1') // 4. 图片附件:单图为系统名;序列帧加序号尾缀 const skin = json.skins[0].attachments const singleAttachments = Object.keys(skin['lizi_1_0_normal'] || {}) if (JSON.stringify(singleAttachments) !== JSON.stringify(['lizi_1'])) failures.push('单图附件应为 lizi_1,实际 ' + JSON.stringify(singleAttachments)) const sequenceAttachments = Object.keys(skin['lizi_1_1_add'] || {}).sort() if (JSON.stringify(sequenceAttachments) !== JSON.stringify(['lizi_1_0', 'lizi_1_1', 'lizi_1_2'])) { failures.push('序列帧附件应为 lizi_1_0/1/2,实际 ' + JSON.stringify(sequenceAttachments)) } // 动画里的附件时间轴要引用同样的名字 const anim = json.animations.animation const usedNames = new Set((anim.slots['lizi_1_1_add']?.attachment || []).map((key) => key.name).filter(Boolean)) for (const name of ['lizi_1_0', 'lizi_1_1', 'lizi_1_2']) if (!usedNames.has(name)) failures.push('序列帧动画未引用附件 ' + name) if (!(anim.slots['lizi_1_0_normal']?.attachment || []).some((key) => key.name === 'lizi_1')) failures.push('单图动画未引用附件 lizi_1') // 拖尾骨骼同样遵循“系统名_序号”前缀,槽位挂在首个拖尾骨骼下。 for (let index = 0; index < 3; index++) { const name = 'lizi_1_0_trail_1_' + index if (!bone(name) || bone(name).parent !== 'lizi_1') failures.push('拖尾骨骼命名/父级错误:' + name) } if (!slot('lizi_1_0_trail_1') || slot('lizi_1_0_trail_1').bone !== 'lizi_1_0_trail_1_0') { failures.push('拖尾插槽应为 lizi_1_0_trail_1 且挂在 lizi_1_0_trail_1_0') } if (failures.length) { failures.forEach((message) => console.error('命名规则失败:' + message)); process.exitCode = 1 } else console.log('导出命名规则校验通过:lizi_1 / lizi_1_0 / lizi_1_0_normal / lizi_1(_序号)') ` const result = await build({ stdin: { contents: source, resolveDir: process.cwd(), sourcefile: 'verify-spine-naming.ts', loader: 'ts' }, bundle: true, platform: 'node', format: 'esm', write: false, logLevel: 'silent', }) const code = Buffer.from(result.outputFiles[0].contents).toString('base64') await import(`data:text/javascript;base64,${code}`)