feedback7 R22-GHSIM: 假想敌:骨刺 changes model with its 隐匿 at once — the three-headed snake (*_A) while hidden, its *_B set when blocked or revealed, back to *_A when hidden again (GitHub #296, PR #365 by @Sukvii, render part); golden: unchanged
The contributor's code (render/spine.js syncFormPose, the render/units.js wiring and FORMS entry, the headless test/render/forms.test.js block), applied from PR #365, so they are the author of this commit. Maintainer side: the hard-coded Spine id became a STEALTH_FORMS table (form while the stealth bit is off), comments and docs/ASSETS.md reworded. Not ported: the PR's sim targeting rewrite (content/enemies/invisible.js), its enemies_bosses tests, its roster.json and the Chrome suite (forms.browser.test.js, not run here). Seen (#296, FrogThai): a blocked 骨刺 kept the snake. The sim already sends UF.STEALTH only while its 隐匿 is effective (not blocked, not revealed — targeting.js enemyStealthed); the view now keys the FORMS 'revealed' clip set (Idle_B / Move_B / Die_B / Attack_B, all in the skeleton) off that bit in UnitView.sync — before a DIE of the same snapshot — and re-poses at once: a running attack continues in the other attack clip at the same point relative to its strike frame (Attack_A hit 0.533 s, Attack_B 0.733 s; a pending wind-up still strikes on time), a stun / idle restarts on the new set without a crossfade, and a frozen skeleton is posed with update(0); a model loaded later is posed the same way. Golden: unchanged (render only).
This commit is contained in:
1 parent
ca671f2fd0
commit
bfef34ffa3
4 files changed
+256
-1
No files matched your search
@@ -200,6 +200,41 @@ export class SpineActor {
|
||||
else if (this.mode === 'stun' && this.has(this.roles.stun?.loop)) this._play(this.roles.stun.loop, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-pose on the clip set in force right after an immediate form switch (render/units.js STEALTH_FORMS: 假想敌:骨刺's
|
||||
* stealth bit; PR #365): a running attack goes on in the new set's attack clip at the same point relative to its
|
||||
* strike frame (a pending wind-up still strikes at windUntil), a stun / the resting state restarts on the new set's
|
||||
* clip without a crossfade, and the skeleton is posed at once — also while a freeze holds it (`frozen`).
|
||||
*/
|
||||
syncFormPose() {
|
||||
if (this.dead) return;
|
||||
if (this.mode === 'attack') {
|
||||
const track = this.spine.state.tracks[0];
|
||||
const clip = this._attackClip();
|
||||
if (!track || !clip) return;
|
||||
const dur = this.dur(clip.loop);
|
||||
const oldHit = this._hitTime(this.current, this.dur(this.current));
|
||||
const hit = this._hitTime(clip.loop, dur);
|
||||
const start = clampN(track.trackTime + hit - oldHit, 0, dur);
|
||||
const lead = this.wound && this.windUntil != null
|
||||
? Math.max(0, this.windUntil - this.clock) : 0;
|
||||
const ts = lead > 0 ? Math.max(0, hit - start) / lead : track.timeScale;
|
||||
const tailTs = lead > 0 ? this.windTs : ts;
|
||||
this._play(clip.loop, track.loop, { start, timeScale: ts, mix: 0 });
|
||||
this.attackUntil = this.clock + lead +
|
||||
Math.max(0, dur - (lead > 0 ? hit : start)) / tailTs;
|
||||
} else if (this.mode === 'stun') {
|
||||
const name = this.has(this.roles.stun?.loop) ? this.roles.stun.loop : this._baseName();
|
||||
this._play(name, true, { mix: 0 });
|
||||
} else if (this.mode === 'base') {
|
||||
this._play(this._baseName(), true, { mix: 0 });
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
// Apply the new attachments even when normal updates are frozen.
|
||||
this.spine.update(0);
|
||||
}
|
||||
|
||||
/** Play a form's transition clip once; attacks and the resting state wait for it (mode 'change'). */
|
||||
_change(clip) {
|
||||
if (this.mode !== 'change') this.stunWanted = this.mode === 'stun';
|
||||
|
||||
@@ -190,6 +190,9 @@ export const EL_BAR = Object.freeze({ icon: 0.15, min: 8, max: 15, enemy: 0.8, g
|
||||
* → UnitView.setForm), per Spine id; the mode's roles override the manifest's (data/assets.json anims), `change` plays
|
||||
* once first:
|
||||
* - 掠海漂移体 (PRTS: 受晕眩/沉睡/冻结影响后进入爬行模式 — for good) crawls on its *_02 clips after 'Change';
|
||||
* - 假想敌:骨刺 (GitHub #296, PR #365): not an fx — its form follows the snapshot's stealth bit (STEALTH_FORMS): the
|
||||
* manifest's *_A (the three-headed snake) while its 隐匿 is on, 'revealed' *_B the moment it is blocked or revealed, *_A
|
||||
* again when the 隐匿 is back; no change clip, a running attack / stun pose carried over (SpineActor.syncFormPose);
|
||||
* - 暴鸰 flies on its bomb-less *_2 clips once its one bomb left (the official prefab's mode S1: Move→Move_2, Idle→Idle_2,
|
||||
* Die→Die_2; user feedback after 0.1.0, D4: the bomb used to stay under the drone — no change clip, the drop is its
|
||||
* Attack clip);
|
||||
@@ -224,6 +227,12 @@ export const EL_BAR = Object.freeze({ icon: 0.15, min: 8, max: 15, enemy: 0.8, g
|
||||
* built for the knock-out — and one rebuilt while it is down — the 替身's death clip; it stands up as the 本体.
|
||||
* A kind without a clip set of this skeleton (barriers, charges, …) changes nothing. 吉兆飞鳞's 晕眩模式 is its Stun clip.
|
||||
*/
|
||||
/**
|
||||
* Enemy forms that follow the snapshot's stealth bit (UF.STEALTH: the sim sends it only while the enemy's 隐匿 is on —
|
||||
* not blocked, not revealed) instead of a sim fx, per Spine id: the FORMS kind drawn while the bit is OFF; the manifest's
|
||||
* clips while it is on. Switched at once (UnitView.sync, and for a model loaded later the load callback).
|
||||
*/
|
||||
export const STEALTH_FORMS = Object.freeze({ enemy_9008_acbunn: 'revealed' });
|
||||
const loop = (name, via = null) => Object.freeze(via ? { begin: null, loop: name, end: null, via } : { begin: null, loop: name, end: null });
|
||||
const clipSet = (idle, move, die, attack = null) => Object.freeze({
|
||||
idle, deploy: idle, die, move: loop(move),
|
||||
@@ -274,6 +283,9 @@ export const FORMS = Object.freeze({
|
||||
enemy_2025_syufo: Object.freeze({
|
||||
crawl: Object.freeze({ change: 'Change', roles: clipSet('Idle_02', 'Move_02', 'Die_02', 'Attack_02') }),
|
||||
}),
|
||||
enemy_9008_acbunn: Object.freeze({
|
||||
revealed: Object.freeze({ change: null, roles: clipSet('Idle_B', 'Move_B', 'Die_B', 'Attack_B') }),
|
||||
}),
|
||||
enemy_10081_mpplai: Object.freeze({
|
||||
translator_fuchou: Object.freeze({ change: 'A_Die_B', roles: clipSet('B_Idle', 'B_Move', 'B_Die', 'B_Attack') }),
|
||||
translator_youling: Object.freeze({ change: 'A_Die_C', roles: clipSet('C_Idle', 'C_Move', 'C_Die') }),
|
||||
@@ -600,6 +612,8 @@ export class UnitView {
|
||||
if (this.flags & UF.SKILL) this.actor.setSkill(true);
|
||||
this.actor.setBase(this._baseFromAnim());
|
||||
if (deployed != null) { this.actor.deploy(); if (deployed > 0) this.actor.update(deployed); }
|
||||
// a stealth-driven form (STEALTH_FORMS): the pose of the clip set in force now, even under a frozen stun
|
||||
if (STEALTH_FORMS[id]) this.actor.syncFormPose();
|
||||
}
|
||||
}, () => {
|
||||
if (req === this._spineReq) { this._spineBusy = false; this._stuckAt = 0; }
|
||||
@@ -747,6 +761,17 @@ export class UnitView {
|
||||
const prevFlags = this.flags;
|
||||
this.flags = s.flags | 0;
|
||||
this.anim = s.anim | 0;
|
||||
// a stealth-driven form (STEALTH_FORMS: 假想敌:骨刺) follows this snapshot's stealth bit at once — before a DIE in the
|
||||
// same snapshot, so it dies in the form it had
|
||||
const stealthForm = this.isEnemy && this.alive ? STEALTH_FORMS[this.info.spine || this.info.defId] : null;
|
||||
if (stealthForm) {
|
||||
const form = this.flags & UF.STEALTH ? null : stealthForm;
|
||||
if (form !== this.form) {
|
||||
this.setForm(form);
|
||||
this.actor?.syncFormPose();
|
||||
if (this.imp) this.imp.dirty = true;
|
||||
}
|
||||
}
|
||||
if (this.isEnemy && Math.abs(s.vx) > 0.08) this.visFacing = s.vx < 0 ? -1 : 1;
|
||||
if ((prevFlags ^ this.flags) & UF.SKILL) this.setSkill(!!(this.flags & UF.SKILL));
|
||||
if (this.anim === ANIM.DIE && this.alive) this.die();
|
||||
|
||||
Reference in new issue
Block a user