feedback6 R21F: enemy animation states, the client part of PR #275 by @xcdoge re-implemented — the resolver gives an enemy every numbered skill clip (anims.skills; the caller's first index stays the primary, operators unchanged, 宴's PREFAB_SPINE_ROLES fix kept), the stun spellings Stun_1 and Dizzy_Begin/Loop/End, a separate run role (Run_Begin/Loop/End); the renderer walks an enemy faster than moveSpeed 1 on its Run cycle (猎狗pro, 深池侦察犬 ×2; [ASSUMED] threshold), swaps an enemy's skill clip on a cast event (UnitView.setSkillSlot; a form in force is kept), and plays the 重生 of 巨大的丑东西 (Revive, then the fleeing 大祭司 Idle_2 / Move_2 / Stun_2) and 自在 (Revive_01); data/assets.json regenerated offline: 18 enemies gain anims.skills, 6 anims.run, 3 a stun clip (巨大的丑东西, 斩胄之剑, 破胄之锤), nothing else; the sim's cast slot (a new event) is not part of it; golden: unchanged

This commit is contained in:
sganggs committed 2026-10-07 17:59:11 +08:00
1 parent 14df10a5a3
commit 76a2db7dd0
11 files changed
+315 -19

No files matched your search

+2
View File
@@ -1388,6 +1388,8 @@ export async function createFieldView(host, options = {}) {
}
case 'heal': { const v = views.get(e[1]); if (v) fx.heal(v, Number(e[2]) || 0); break; }
case 'skill': { const v = views.get(e[1]); if (v) { v.setSkill?.(!!e[2]); fx.skill(v, !!e[2]); } break; }
// the skill slot an enemy ability casts (PR #275): the view swaps to that slot's clip (a multi-skill boss's Skill_01..04)
case 'cast': { const v = views.get(e[1]); if (v) v.setSkillSlot?.(e[2] | 0); break; }
case 'die': {
const v = views.get(e[1]);
const used = consumedIds.delete(e[1]);
+29 -5
View File
@@ -107,6 +107,9 @@ export class SpineActor {
this.endClip = null; // setForm's closing clip, played as a change clip once the clock reaches endAt,
this.endAt = 0; // landing in endRoles (the next form's) when given
this.endRoles = null;
this.skillIndex = null; // the skill slot whose clip `skill` plays (setSkillIndex), null = the manifest's primary
this.runMode = false; // move on the model's own Run cycle (setRunMode)
this.formRoles = null; // the clip set of the form in force (setForm), over the unit's own roles
this.skillOn = false;
this.skillClipOnce = false; // skillBegin: the skill's own clip plays once (a clip with no Begin / own Idle)
this.skillEndPending = false; // … and the skill turned off inside it: play its End clip / base when it ends
@@ -145,12 +148,32 @@ export class SpineActor {
* @param {number|undefined} index 0-based skill index
*/
setSkillIndex(index) {
const anims = this.entry?.anims || {};
const clip = Number.isInteger(index) && anims.skills ? anims.skills[String(index)] : null;
this.roles = this.baseRoles = clip ? { ...anims, skill: clip } : anims;
this.skillIndex = Number.isInteger(index) ? index : null;
this._applyRoles();
}
/** The unit's own roles: the manifest's with its equipped skill's clip (setSkillIndex) — what a form ends in. */
/**
* A fast mover walks on its model's own Run cycle (`anims.run`; PR #275 by @xcdoge): 猎狗pro (moveSpeed 1.9, 行动速度很快)
* ships Move_Loop 0.80 s next to Run_Loop 0.53 s. Composes with setSkillIndex; a model without a Run cycle is unchanged.
*/
setRunMode(on) {
this.runMode = !!on;
this._applyRoles();
}
/** The unit's own roles (the manifest's, its skill slot's clip and the Run cycle applied) under the form in force. */
_applyRoles() {
const anims = this.entry?.anims || {};
const clip = Number.isInteger(this.skillIndex) && anims.skills ? anims.skills[String(this.skillIndex)] : null;
const run = this.runMode && anims.run ? anims.run : null;
const base = clip || run ? { ...anims } : anims;
if (clip) base.skill = clip;
if (run) base.move = run;
this.baseRoles = base;
this.roles = this.formRoles ? { ...base, ...this.formRoles } : base;
}
/** The unit's own roles: the manifest's with its skill slot's clip (setSkillIndex) and Run cycle — what a form ends in. */
_baseRoles() { return this.baseRoles || this.entry?.anims || {}; }
/**
@@ -163,6 +186,7 @@ export class SpineActor {
*/
setForm(roles, change = null, end = null) {
const anims = this._baseRoles();
this.formRoles = roles || null; // kept over a later skill slot (_applyRoles)
this.roles = roles ? { ...anims, ...roles } : anims;
this.endClip = null;
if (this.dead) return;
@@ -464,7 +488,7 @@ export class SpineActor {
if (this.endClip && this.clock >= this.endAt) {
const clip = this.endClip;
this.endClip = null;
if (this.endRoles) this.roles = { ...this._baseRoles(), ...this.endRoles };
if (this.endRoles) { this.formRoles = this.endRoles; this.roles = { ...this._baseRoles(), ...this.endRoles }; }
if (!this.dead) this._change(clip);
}
if (this.windUntil != null && this.clock >= this.windUntil) {
+31 -1
View File
@@ -289,6 +289,18 @@ export const FORMS = Object.freeze({
enemy_1525_blkswb: rebirth('Revive1', 'Revive2', 'Revive3', clipSet('B_Idle', 'B_Move', 'B_Die', 'B_Attack')),
enemy_1535_wlfmster: rebirth('A_revive_1', 'A_revive_2', 'A_revive_3', clipSet('B_Idle', 'B_Move', 'B_Die', 'B_Attack')),
enemy_1539_reid: rebirth('Revive_Begin', 'Revive_Loop', 'Revive_End'),
// 重生 on the skeleton's own Revive clip (PR #275 by @xcdoge; content/enemies/leaders.js kitUglyThing / kitXi): 巨大的丑东西
// — Revive 8.67 s of the 10 s 重生 (its self-destruct 2.17 s in), then the fleeing 大祭司 (Idle_2 / Move_2 / Stun_2;
// 不进行攻击) holding its idle while the sim still holds the 重生 (reported as a stun); 自在 — Revive_01 5.33 s for the
// 5 s 重生, then the same model, stronger (reborn.atk)
enemy_1512_mcmstr: Object.freeze({
reborn: Object.freeze({ change: 'Revive', next: 'form2', roles: Object.freeze({ idle: 'Idle_2', deploy: 'Idle_2', move: loop('Idle_2'), stun: loop('Idle_2'), attack: null, skill: null }) }),
form2: Object.freeze({ change: null, roles: Object.freeze({ ...clipSet('Idle_2', 'Move_2', 'Die'), stun: loop('Stun_2') }) }),
}),
enemy_1517_xi: Object.freeze({
reborn: Object.freeze({ change: 'Revive_01', next: 'form2', roles: Object.freeze({}) }),
form2: Object.freeze({ change: null, roles: Object.freeze({}) }),
}),
enemy_1516_jakill: Object.freeze({
reborn: Object.freeze({ change: 'C1_Die', roles: JAKILL2 }),
form2: Object.freeze({ change: null, roles: JAKILL2 }),
@@ -355,6 +367,9 @@ export class UnitView {
this.info = { ...info };
this.id = info.id;
this.uid = info.uid ?? null;
// the skill slot whose Spine clip the unit shows (DESIGN §16): an ally's equipped skill; an enemy's cast slot when the
// sim reports one (`cast` event, setSkillSlot — a multi-skill boss's Skill_01..04, PR #275)
this.skillIndex = Number.isInteger(info.skillIndex) ? info.skillIndex : null;
this.prep = !!opts.prep;
this.lodIdle = opts.lod === 'idle';
this.culled = false; // outside the viewport this frame (not animated, not drawn)
@@ -365,6 +380,9 @@ export class UnitView {
// enemies: the official prefab's size factor (1 for operators, summons and enemies at the standard size)
const def = this.isEnemy && ctx.lookupDef ? ctx.lookupDef(info) : null;
this.modelK = this.isEnemy ? enemyModelScale(def) : 1;
// a fast enemy walks on its model's own Run cycle when it has one (PR #275 by @xcdoge: 猎狗pro, moveSpeed 1.9, Run_Loop
// 0.53 s next to Move_Loop 0.80 s; also 深池侦察犬 1.7). [ASSUMED] the threshold: faster than the standard 1
this.moveFast = this.isEnemy && Number(def?.stats?.moveSpeed) > 1;
// the official's own model quirks (enemies.json, read from its battle prefabs — tools/local-extract/enemy_model_offsets.py,
// PR #211): a vertical stretch (its Graphic scale's sy / sx, the two 帝国炮火先兆者 at 1.263) and a mirrored X scale
// (the Graphic's sx is negative, so the official draws the authored model flipped: 木制瑞印)
@@ -546,7 +564,8 @@ export class UnitView {
let actor = null;
try {
actor = new SpineActor(data, entry);
actor.setSkillIndex(this.info.skillIndex);
actor.setSkillIndex(this.skillIndex ?? undefined);
actor.setRunMode(this.moveFast);
// enemies play their attack clip once per attack, then walk on (GitHub #58: the sim stands them for that clip)
actor.clipPerAttack = this.isEnemy;
} catch (err) {
@@ -849,6 +868,17 @@ export class UnitView {
if (this.actor) this.actor.setSkill(on);
}
/**
* The skill slot an enemy casts (the sim's `cast` event; PR #275 by @xcdoge): its clip set swaps to that slot's skill
* clip (`anims.skills`, a multi-skill boss's Skill_01..04), which the SKILL flag right after plays. The same slot again
* or a non-slot changes nothing.
*/
setSkillSlot(index) {
if (!Number.isInteger(index) || index < 0 || index === this.skillIndex) return;
this.skillIndex = index;
if (this.actor) this.actor.setSkillIndex(index);
}
onDeploy() {
this.fadeIn = 0;
if (!this.alive) this.revive();