lawless-client/client/tools/generate-skeletons.js
2026-07-09 11:54:43 +08:00

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();