320 行
13 KiB
JavaScript
320 行
13 KiB
JavaScript
|
|
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();
|