feedback6: merge R21F — enemy flyers hover 1.3 tiles above the road instead of climbing each raised tile (GitHub #277, @FrogThai); enemy animation states from PR #275 by @xcdoge, client part: numbered skill clips, the Stun_1 / Dizzy_* spellings, run cycles (猎狗pro, 深池侦察犬), the rebirths of 巨大的丑东西 and 自在 (data/assets.json regenerated offline: enemies only); golden unchanged
This commit is contained in:
commit
1e58843fb4
13 files changed
+396
-30
No files matched your search
@@ -28,17 +28,21 @@
|
||||
// loop = P_Loop → P_Attack… → P → P_Idle → P_Begin; begin = P_Begin|P_Start; end = P_End;
|
||||
// idle = P_Idle (replaces idle while the skill is active);
|
||||
// else the same over directional-only clips (Skill_Right_Loop, Skill_Loop_Up; Right before Up);
|
||||
// else attack (via attack)
|
||||
// else attack (via attack); with `numberedSkills` (enemies) every numbered skill clip of the skeleton
|
||||
// (Skill_01, Skill_2_Loop …) adds its index after the caller's, giving `skills` (a multi-skill boss)
|
||||
// die : Die → <form>Die → first /^Die/ → first /_Die$/ → null (renderer: Front model's Die, else fade)
|
||||
// move : Move_Begin|Move_Start + (Move_Loop → Move) + Move_End → <form>Move → Run_Begin/Loop/End
|
||||
// → Run → first /^Move/ → null
|
||||
// stun : Stun_Begin + Stun (+ Stun_End) → Stun → null (renderer: freeze the track)
|
||||
// run : Run_Begin/Loop/End → Run → <form>Run → absent (only a model with its own Run cycle has the role)
|
||||
// stun : (Stun_Begin | Dizzy_Begin) + (Stun → Stun_1 → Dizzy_Loop) (+ Stun_End | Dizzy_End) → Stun_Begin alone
|
||||
// → null (renderer: freeze the track)
|
||||
|
||||
/**
|
||||
* @typedef {{ begin: string|null, loop: string, end: string|null, via?: string }} Clip
|
||||
* @typedef {Clip & { index: number, idle: string|null }} SkillClip
|
||||
* @typedef {{ idle: string|null, deploy: string|null, attack: Clip|null, attackDown: Clip|null,
|
||||
* skill: SkillClip|null, skills?: Record<string, SkillClip>, die: string|null, move: Clip|null, stun: Clip|null }} Roles
|
||||
* skill: SkillClip|null, skills?: Record<string, SkillClip>, die: string|null, move: Clip|null, run?: Clip,
|
||||
* stun: Clip|null }} Roles
|
||||
*/
|
||||
|
||||
function makeFinder(names) {
|
||||
@@ -221,10 +225,27 @@ function resolveMove(names, find, form) {
|
||||
return any ? clip(null, any, null) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The model's own run cycle, when it has one (猎狗pro: Move_Loop 0.80 s next to Run_Loop 0.53 s): a role of its own, not
|
||||
* the move choice — the renderer walks a fast enemy on it (render/units.js `moveFast`; PR #275 by @xcdoge).
|
||||
*/
|
||||
function resolveRun(find, form) {
|
||||
const t = triple(find, 'Run');
|
||||
if (t) return t;
|
||||
const r = find('Run') ?? form('Run');
|
||||
return r ? clip(null, r, null) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The stun family is spelled two ways and sometimes numbered (PR #275 by @xcdoge): Stun / Stun_Begin / Stun_End
|
||||
* (吉兆飞鳞, 乌顶巨角卢鲁), Stun_1 / Stun_2 (巨大的丑东西: its first and its second form — render/units.js FORMS gives the
|
||||
* second form Stun_2), Dizzy_Begin / Dizzy_Loop / Dizzy_End (斩胄之剑 / 破胄之锤, stun-immune in this mode). Without a role
|
||||
* a stunned enemy freezes its current clip.
|
||||
*/
|
||||
function resolveStun(find) {
|
||||
const stun = find('Stun');
|
||||
const begin = find('Stun_Begin');
|
||||
if (stun) return clip(begin, stun, find('Stun_End'));
|
||||
const stun = find('Stun') ?? find('Stun_1') ?? find('Dizzy_Loop');
|
||||
const begin = find('Stun_Begin') ?? find('Dizzy_Begin');
|
||||
if (stun) return clip(begin, stun, find('Stun_End') ?? find('Dizzy_End'));
|
||||
if (begin) return clip(null, begin, null);
|
||||
return null;
|
||||
}
|
||||
@@ -241,6 +262,8 @@ function resolveDie(names, find, form) {
|
||||
* @param {string[]} animationNames all animation names in the skeleton
|
||||
* @param {object} [opts]
|
||||
* @param {number[]} [opts.skillIndices] 0-based skill indices to resolve (first = primary); default [0]
|
||||
* @param {boolean} [opts.numberedSkills] also resolve every numbered skill clip of the skeleton (Skill_01, Skill_2_Loop …)
|
||||
* after the caller's indices: an enemy's per-skill clips (a multi-skill boss such as 盐风主教昆图斯, Skill_01..04)
|
||||
* @param {Record<string, number>} [opts.durations] animation durations in seconds (prefers animated idles)
|
||||
* @returns {Roles}
|
||||
*/
|
||||
@@ -256,6 +279,16 @@ export function resolveRoles(animationNames, opts = {}) {
|
||||
const indices = [...new Set((opts.skillIndices?.length ? opts.skillIndices : [0])
|
||||
.filter((i) => Number.isInteger(i) && i >= 0 && i < 10))];
|
||||
if (!indices.length) indices.push(0);
|
||||
// an enemy's numbered skill clips (PR #275 by @xcdoge: its manifest only ever had index 0, so a multi-skill boss could
|
||||
// show one cast clip): appended after the caller's indices, whose first stays the primary `skill`
|
||||
if (opts.numberedSkills) {
|
||||
const found = new Set();
|
||||
for (const n of names) {
|
||||
const m = /^Skill_?0*(\d+)(?:$|_)/i.exec(n);
|
||||
if (m && Number(m[1]) >= 1 && Number(m[1]) <= 10) found.add(Number(m[1]) - 1);
|
||||
}
|
||||
for (const i of [...found].sort((a, b) => a - b)) if (!indices.includes(i)) indices.push(i);
|
||||
}
|
||||
/** @type {Record<string, SkillClip>} */
|
||||
const skills = {};
|
||||
for (const i of indices) {
|
||||
@@ -272,6 +305,8 @@ export function resolveRoles(animationNames, opts = {}) {
|
||||
move: resolveMove(names, find, form),
|
||||
stun: resolveStun(find),
|
||||
};
|
||||
const run = resolveRun(find, form);
|
||||
if (run) roles.run = run;
|
||||
if (indices.length > 1) roles.skills = skills;
|
||||
return roles;
|
||||
}
|
||||
@@ -289,7 +324,7 @@ export function roleAnimationNames(roles) {
|
||||
};
|
||||
if (!roles) return [];
|
||||
for (const k of ['idle', 'deploy', 'die']) if (typeof roles[k] === 'string') out.add(roles[k]);
|
||||
for (const k of ['attack', 'attackDown', 'skill', 'move', 'stun']) addClip(roles[k]);
|
||||
for (const k of ['attack', 'attackDown', 'skill', 'move', 'run', 'stun']) addClip(roles[k]);
|
||||
if (roles.skills) for (const c of Object.values(roles.skills)) addClip(c);
|
||||
return [...out];
|
||||
}
|
||||
@@ -166,7 +166,8 @@ export function applyRoleFix(roles, fix, durations) {
|
||||
/**
|
||||
* Spine metadata of extracted models (findLocalEnemyModels / findLocalTokenModels), parsed like processModels does — but
|
||||
* read only: the atlas is sized / pma-normalized in memory (extract.py already writes it so), nothing on disk changes.
|
||||
* Roles are resolved for skill index 0, like the web enemy and token models, then LOCAL_SPINE_ROLES applied.
|
||||
* Roles are resolved for skill index 0 (an enemy also for its numbered skill clips), like the web enemy and token models,
|
||||
* then LOCAL_SPINE_ROLES applied.
|
||||
* @param {string} root absolute public/assets directory
|
||||
* @param {Record<string, { dir: string, skel: string, atlas: string, pngs: string[] }>} found
|
||||
* @returns {Promise<{ meta: Record<string, LocalSpineMeta>, problems: string[] }>}
|
||||
@@ -192,7 +193,7 @@ export async function localSpineMeta(root, found) {
|
||||
const sk = parseSkel(await readFile(join(root, m.skel)), info.regions);
|
||||
if (!sk.animations.length) throw new Error('skeleton has no animations');
|
||||
if (sk.missingRegions?.length) problems.push(`${id}: ${sk.missingRegions.length} attachment(s) not in atlas (e.g. ${sk.missingRegions[0]})`);
|
||||
const fixed = applyRoleFix(resolveRoles(sk.animations, { skillIndices: [0], durations: sk.durations }), LOCAL_SPINE_ROLES[id], sk.durations);
|
||||
const fixed = applyRoleFix(resolveRoles(sk.animations, { skillIndices: [0], numberedSkills: id.startsWith('enemy_'), durations: sk.durations }), LOCAL_SPINE_ROLES[id], sk.durations);
|
||||
if (fixed.missing.length) problems.push(`${id}: role fix not applied (clip missing) for ${fixed.missing.join(', ')}`);
|
||||
meta[id] = {
|
||||
skel: base(m.skel), atlas: base(m.atlas), textures: info.pages, pma: true,
|
||||
@@ -330,7 +331,9 @@ export async function processModels(models, { root, dl, cachePath, download = tr
|
||||
if (!sk.animations.length) { problems.push(`${m.key}: skeleton has no animations`); continue; }
|
||||
const key = String(m.key);
|
||||
const id = key.startsWith('enemy:') ? key.slice('enemy:'.length) : key.startsWith('op:') ? key.split(':')[1] : null; // op:<char>:front|back
|
||||
const fixed = applyRoleFix(resolveRoles(sk.animations, { skillIndices: m.skillIndices, durations: sk.durations }), id ? PREFAB_SPINE_ROLES[id] : undefined, sk.durations);
|
||||
// an enemy's numbered skill clips are resolved too (anims.skills: the clip of each skill slot it casts, PR #275);
|
||||
// operators keep the pool's indices (their equipped skill picks among them)
|
||||
const fixed = applyRoleFix(resolveRoles(sk.animations, { skillIndices: m.skillIndices, numberedSkills: key.startsWith('enemy:'), durations: sk.durations }), id ? PREFAB_SPINE_ROLES[id] : undefined, sk.durations);
|
||||
if (fixed.missing.length) problems.push(`${m.key}: role fix not applied (clip missing) for ${fixed.missing.join(', ')}`);
|
||||
const anims = fixed.roles;
|
||||
entries.set(m.key, {
|
||||
|
||||
Reference in new issue
Block a user