const fs = require('fs'); const path = require('path'); const OUTPUT_DIR = path.resolve(__dirname, '../assets/resources/skeletons'); /** * Generate DragonBones 5.5 skeleton JSON files for each selectable * race / profession / gender combination. */ function ensureDir(dir) { if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } } function color(alpha, r, g, b) { return { alphaMultiplier: alpha, redMultiplier: r, greenMultiplier: g, blueMultiplier: b }; } function transform(x = 0, y = 0, scX = 1, scY = 1, rotate = 0) { const t = { x, y }; if (scX !== 1) t.scX = scX; if (scY !== 1) t.scY = scY; if (rotate !== 0) t.skX = rotate; return t; } function display(name, w, h, rotate = 0, x = 0, y = 0) { return { name, path: name, pivot: { x: 0.5, y: 0.5 }, transform: transform(x, y, w, h, rotate), }; } function frame(duration, values) { return { duration, ...values }; } function makeBone(name, parent, t) { return { name, parent, transform: t }; } function makeSlot(name, parent, colorValues, displayIndex = 0) { return { name, parent, displayIndex, color: colorValues }; } const THEMES = { human: { base: color(100, 100, 92, 82), torso: color(100, 100, 88, 70), head: color(100, 100, 92, 82), arm: color(100, 100, 78, 54), leg: color(100, 92, 68, 48), hair: color(100, 25, 20, 15), prop: color(100, 217, 178, 98), }, dandao: { torso: color(100, 120, 90, 55), prop: color(100, 200, 120, 60), }, rusheng: { torso: color(100, 100, 95, 85), prop: color(100, 220, 200, 160), }, ludao: { torso: color(100, 110, 100, 70), prop: color(100, 240, 220, 100), }, ziran: { torso: color(100, 70, 120, 70), prop: color(100, 80, 160, 80), }, shi: { torso: color(100, 120, 110, 70), prop: color(100, 240, 200, 80), }, zhenfa: { torso: color(100, 70, 110, 140), prop: color(100, 80, 180, 220), }, xiuzhen: { torso: color(100, 130, 80, 50), prop: color(100, 220, 220, 230), }, baigong: { torso: color(100, 120, 95, 60), prop: color(100, 160, 120, 70), }, niyuan: { base: color(100, 60, 80, 100), torso: color(100, 45, 75, 100), head: color(100, 60, 85, 100), arm: color(100, 40, 65, 95), leg: color(100, 35, 55, 90), hair: color(100, 30, 50, 85), prop: color(100, 50, 110, 140), }, heishanmushu: { base: color(100, 70, 90, 60), torso: color(100, 55, 85, 50), head: color(100, 70, 95, 60), arm: color(100, 50, 75, 45), leg: color(100, 45, 65, 40), hair: color(100, 40, 70, 35), prop: color(100, 90, 150, 70), }, huangyizhiwang: { base: color(100, 100, 100, 90), torso: color(100, 95, 95, 75), head: color(100, 110, 105, 90), arm: color(100, 90, 90, 75), leg: color(100, 85, 85, 70), hair: color(100, 80, 80, 65), prop: color(100, 220, 210, 140), }, }; function getTheme(professionId) { const theme = THEMES[professionId] || {}; const base = professionId.startsWith('niyuan') || professionId.startsWith('heishan') || professionId.startsWith('huangyi') ? (THEMES[professionId].base || THEMES.human.base) : THEMES.human.base; return { base, torso: theme.torso || base, head: theme.head || base, arm: theme.arm || base, leg: theme.leg || base, hair: theme.hair || THEMES.human.hair, prop: theme.prop || THEMES.human.prop, }; } function buildSkeleton(roleId, professionId, gender) { const isFemale = gender === 'female'; const theme = getTheme(professionId); // Gender scale factors const gs = isFemale ? { x: 0.9, y: 0.95, torsoW: 0.85, headS: 1.08, hairLen: 1.4, shoulder: 0.9 } : { x: 1, y: 1, torsoW: 1.05, headS: 1, hairLen: 1, shoulder: 1.05 }; const bone = [ makeBone('root', undefined, transform(0, -40)), makeBone('hips', 'root', transform(0, 40)), makeBone('torso', 'hips', transform(0, 18)), makeBone('head', 'torso', transform(0, 22)), makeBone('hairBack', 'head', transform(0, 7)), makeBone('hairL', 'head', transform(-7 * gs.shoulder, 6)), makeBone('hairR', 'head', transform(7 * gs.shoulder, 6)), makeBone('armL', 'torso', transform(-13 * gs.shoulder, 16)), makeBone('forearmL', 'armL', transform(0, -14)), makeBone('armR', 'torso', transform(13 * gs.shoulder, 16)), makeBone('forearmR', 'armR', transform(0, -14)), makeBone('legL', 'hips', transform(-7, 0)), makeBone('legR', 'hips', transform(7, 0)), makeBone('prop', 'forearmR', transform(0, -10)), ]; const torsoW = 18 * gs.torsoW; const torsoH = 36; const headS = 14 * gs.headS; const armW = 5; const armH = 15; const forearmW = 4.5; const forearmH = 14; const legW = 6; const legH = 20; const hairW = 5; const hairH = 14 * gs.hairLen; const slot = [ makeSlot('slot_torso', 'torso', theme.torso), makeSlot('slot_head', 'head', theme.head), makeSlot('slot_hair_back', 'hairBack', theme.hair), makeSlot('slot_hair_l', 'hairL', theme.hair), makeSlot('slot_hair_r', 'hairR', theme.hair), makeSlot('slot_arm_l', 'armL', theme.arm), makeSlot('slot_forearm_l', 'forearmL', theme.arm), makeSlot('slot_arm_r', 'armR', theme.arm), makeSlot('slot_forearm_r', 'forearmR', theme.arm), makeSlot('slot_leg_l', 'legL', theme.leg), makeSlot('slot_leg_r', 'legR', theme.leg), makeSlot('slot_prop', 'prop', theme.prop), ]; const skinSlotData = [ { name: 'slot_torso', display: [display('part', torsoW, torsoH)] }, { name: 'slot_head', display: [display('part', headS, headS)] }, { name: 'slot_hair_back', display: [display('part', hairW * 1.5, hairH * 1.2)] }, { name: 'slot_hair_l', display: [display('part', hairW, hairH, -10)] }, { name: 'slot_hair_r', display: [display('part', hairW, hairH, 10)] }, { name: 'slot_arm_l', display: [display('part', armW, armH)] }, { name: 'slot_forearm_l', display: [display('part', forearmW, forearmH)] }, { name: 'slot_arm_r', display: [display('part', armW, armH)] }, { name: 'slot_forearm_r', display: [display('part', forearmW, forearmH)] }, { name: 'slot_leg_l', display: [display('part', legW, legH)] }, { name: 'slot_leg_r', display: [display('part', legW, legH)] }, ]; // Profession-specific prop display const propDisplays = { xiuzhen: [display('part', 4, 26, -45, 0, 0)], // sword dandao: [display('part', 8, 12, 0, 0, 0), display('part', 3, 6, 0, 2, 6)], // gourd rusheng: [display('part', 10, 14, 0, 0, 0)], // scroll ludao: [display('part', 7, 12, 0, 0, 0)], // talisman ziran: [display('part', 3, 28, 0, 0, 0)], // staff shi: [display('part', 4, 10, 0, 0, 0), display('part', 4, 10, 0, 0, -4), display('part', 4, 10, 0, 0, 4)], // beads zhenfa: [display('part', 12, 12, 0, 0, 0)], // disk baigong: [display('part', 10, 10, 0, 0, 0), display('part', 3, 14, 0, 0, 0)], // hammer niyuan: [display('part', 4, 24, -30, 0, 0)], // tentacle heishanmushu: [display('part', 14, 8, 45, 0, 4), display('part', 3, 16, -20, 0, -2)], // horns/vines huangyizhiwang: [display('part', 10, 12, 0, 0, 0), display('part', 8, 5, 0, 0, 8)], // mask/cloth }; skinSlotData.push({ name: 'slot_prop', display: propDisplays[professionId] || [display('part', 6, 6)] }); const skin = [{ name: 'default', slot: skinSlotData }]; const amp = 1; const dur = 48; const half = dur / 2; const animation = [ { name: 'idle', duration: dur, playTimes: 0, bone: [ { name: 'torso', rotateFrame: [frame(half, { rotate: -1.5 * amp, tweenEasing: 0 }), frame(half, { rotate: 1.5 * amp, tweenEasing: 0 })], translateFrame: [frame(half, { x: 0, y: -0.8 * amp, tweenEasing: 0 }), frame(half, { x: 0, y: 0.8 * amp, tweenEasing: 0 })] }, { name: 'head', rotateFrame: [frame(half, { rotate: 0.8 * amp, tweenEasing: 0 }), frame(half, { rotate: -0.8 * amp, tweenEasing: 0 })] }, { name: 'hairBack', rotateFrame: [frame(half, { rotate: -3 * amp, tweenEasing: 0 }), frame(half, { rotate: 3 * amp, tweenEasing: 0 })] }, { name: 'hairL', rotateFrame: [frame(half, { rotate: -2 * amp, tweenEasing: 0 }), frame(half, { rotate: 2 * amp, tweenEasing: 0 })] }, { name: 'hairR', rotateFrame: [frame(half, { rotate: 2 * amp, tweenEasing: 0 }), frame(half, { rotate: -2 * amp, tweenEasing: 0 })] }, { name: 'armL', rotateFrame: [frame(half, { rotate: -3 * amp, tweenEasing: 0 }), frame(half, { rotate: -1 * amp, tweenEasing: 0 })] }, { name: 'forearmL', rotateFrame: [frame(half, { rotate: -2 * amp, tweenEasing: 0 }), frame(half, { rotate: 1 * amp, tweenEasing: 0 })] }, { name: 'armR', rotateFrame: [frame(half, { rotate: 1 * amp, tweenEasing: 0 }), frame(half, { rotate: 3 * amp, tweenEasing: 0 })] }, { name: 'forearmR', rotateFrame: [frame(half, { rotate: 1 * amp, tweenEasing: 0 }), frame(half, { rotate: -2 * amp, tweenEasing: 0 })] }, { name: 'legL', rotateFrame: [frame(half, { rotate: 1 * amp, tweenEasing: 0 }), frame(half, { rotate: -0.5 * amp, tweenEasing: 0 })] }, { name: 'legR', rotateFrame: [frame(half, { rotate: -0.5 * amp, tweenEasing: 0 }), frame(half, { rotate: 1 * amp, tweenEasing: 0 })] }, { name: 'prop', rotateFrame: [frame(half, { rotate: -2 * amp, tweenEasing: 0 }), frame(half, { rotate: 2 * amp, tweenEasing: 0 })], translateFrame: [frame(half, { x: 0, y: -0.5 * amp, tweenEasing: 0 }), frame(half, { x: 0, y: 0.5 * amp, tweenEasing: 0 })] }, ], }, { name: 'focus', duration: 18, playTimes: 1, bone: [ { name: 'torso', rotateFrame: [frame(6, { rotate: -4 * amp, tweenEasing: 0 }), frame(6, { rotate: 2 * amp, tweenEasing: 0 }), frame(6, { rotate: 0, tweenEasing: 0 })] }, { name: 'head', rotateFrame: [frame(6, { rotate: -2 * amp, tweenEasing: 0 }), frame(6, { rotate: 1 * amp, tweenEasing: 0 }), frame(6, { rotate: 0, tweenEasing: 0 })] }, { name: 'armR', rotateFrame: [frame(6, { rotate: -10 * amp, tweenEasing: 0 }), frame(6, { rotate: 18 * amp, tweenEasing: 0 }), frame(6, { rotate: 3 * amp, tweenEasing: 0 })] }, { name: 'forearmR', rotateFrame: [frame(6, { rotate: -15 * amp, tweenEasing: 0 }), frame(6, { rotate: 5 * amp, tweenEasing: 0 }), frame(6, { rotate: 0, tweenEasing: 0 })] }, { name: 'prop', rotateFrame: [frame(6, { rotate: -10 * amp, tweenEasing: 0 }), frame(6, { rotate: 25 * amp, tweenEasing: 0 }), frame(6, { rotate: 2 * amp, tweenEasing: 0 })] }, { name: 'armL', rotateFrame: [frame(6, { rotate: 4 * amp, tweenEasing: 0 }), frame(6, { rotate: -2 * amp, tweenEasing: 0 }), frame(6, { rotate: -1 * amp, tweenEasing: 0 })] }, ], }, ]; const json = { name: `hh_${roleId}_${professionId}_${gender}`, version: '5.5', compatibleVersion: '5.5', frameRate: 24, armature: [ { name: 'preview', type: 'Armature', frameRate: 24, aabb: { x: -80, y: -30, width: 160, height: 160 }, bone, slot, skin, animation, defaultActions: [{ gotoAndPlay: 'idle' }], }, ], textureAtlas: [ { name: `hh_atlas_${roleId}_${professionId}_${gender}`, imagePath: 'white', width: 1, height: 1, scale: 1, SubTexture: [{ name: 'part', x: 0, y: 0, width: 1, height: 1 }], }, ], }; return json; } function main() { ensureDir(OUTPUT_DIR); const combos = [ { role: 'human', profession: 'dandao' }, { role: 'human', profession: 'rusheng' }, { role: 'human', profession: 'ludao' }, { role: 'human', profession: 'ziran' }, { role: 'human', profession: 'shi' }, { role: 'human', profession: 'zhenfa' }, { role: 'human', profession: 'xiuzhen' }, { role: 'human', profession: 'baigong' }, { role: 'shenqianyi', profession: 'niyuan' }, { role: 'shenqianyi', profession: 'heishanmushu' }, { role: 'shenqianyi', profession: 'huangyizhiwang' }, ]; for (const combo of combos) { for (const gender of ['male', 'female']) { const json = buildSkeleton(combo.role, combo.profession, gender); const filename = `${combo.role}_${combo.profession}_${gender}.json`; fs.writeFileSync(path.join(OUTPUT_DIR, filename), JSON.stringify(json, null, 2)); console.log('Generated', filename); } } } main();