feedback2 WA: a knocked-out operator facing UP falls and lies with its Front model, the Back model holds still meanwhile (GitHub issue #25)
131 of the 135 Back skeletons have no Die clip, so SpineActor.die() changed nothing and the attack / skill / idle loop went on under the redeploy ring. UnitView._wantsBack() now wants the Back model only while the unit stands (or when its Back skeleton has a Die clip of its own); die() / revive() mark the view and its update swaps the model in place after the frame's events (a death and a redeploy in one frame load nothing). The Front model replays the fall (held end for a unit already down), gets the knocked-down grey, and the redeploy brings the Back model back with the deploy clip carried over. A skeleton without a Die clip holds its idle's first frame.
This commit is contained in:
1 parent
2a03467897
commit
072e1231ae
3 files changed
+396
-12
No files matched your search
@@ -12,7 +12,8 @@
|
||||
// clip plays once at its own speed, then base
|
||||
// setSkill(on) skill begin→loop while active (skill idle replaces idle), end on stop
|
||||
// deploy() 'Start' once, then base
|
||||
// die() die clip once (callers fade out afterwards)
|
||||
// die() die clip once (callers fade out afterwards); a skeleton without one holds its
|
||||
// idle clip's first frame (GitHub issue #25: the attack loop went on)
|
||||
// stunned (setBase('stun')) stun clip, or the current track frozen at timeScale 0
|
||||
// setForm(roles, change, end) another clip set of the skeleton (an enemy's mode), after a change clip
|
||||
// (no attack cuts the change clip short); `end` = { clip, in, roles? }: a
|
||||
@@ -341,24 +342,45 @@ export class SpineActor {
|
||||
if (this.has(d) && d !== this.roles.idle) {
|
||||
this.mode = 'deploy';
|
||||
this._play(d, false, { mix: 0 });
|
||||
this.deployAt = this.clock;
|
||||
this.deployUntil = this.clock + this.dur(d);
|
||||
}
|
||||
}
|
||||
|
||||
/** Play the death clip; returns its duration (0 when there is none). */
|
||||
/** Seconds into the deploy clip while it plays, else null (a model swapped mid-deploy carries it over: units.js). */
|
||||
deployElapsed() {
|
||||
return this.mode === 'deploy' ? Math.max(0, this.clock - (this.deployAt || 0)) : null;
|
||||
}
|
||||
|
||||
/** The skeleton's death clip, or null. */
|
||||
dieClip() {
|
||||
const d = this.roles.die || (this.has('Die') ? 'Die' : null);
|
||||
return d && this.has(d) ? d : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Play the death clip; returns its duration (0 when there is none). A skeleton without one (131 of the 135 Back
|
||||
* models, GitHub issue #25; a few idle-only summons and enemies) stops whatever looped — an attack, a skill or the idle —
|
||||
* and holds the first frame of its idle clip (frozen when it has none either): a dead unit never goes on attacking. A
|
||||
* knocked-out operator shows its fall with the Front model instead (render/units.js _wantsBack).
|
||||
*/
|
||||
die() {
|
||||
if (this.dead) return 0;
|
||||
this.dead = true;
|
||||
this.frozen = false;
|
||||
this.mode = 'die';
|
||||
const d = this.roles.die || (this.has('Die') ? 'Die' : null);
|
||||
if (d && this.has(d)) { this._play(d, false, { mix: 0.05 }); return this.dur(d); }
|
||||
const d = this.dieClip();
|
||||
if (d) { this._play(d, false, { mix: 0.05 }); return this.dur(d); }
|
||||
const idle = this.has(this.roles.idle) ? this.roles.idle : this.has('Idle') ? 'Idle' : null;
|
||||
if (idle) this._play(idle, false, { mix: 0.1, timeScale: 0 });
|
||||
else this.frozen = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** Revive (redeploy after death). */
|
||||
revive() {
|
||||
this.dead = false;
|
||||
this.frozen = false;
|
||||
this.mode = 'base';
|
||||
this.skillOn = false;
|
||||
this._play(this._baseName(), true);
|
||||
|
||||
@@ -19,12 +19,15 @@
|
||||
// the legacy `facing` ±1): the Back model for UP (when one exists, research 07 §5.5), the Front model for RIGHT and
|
||||
// DOWN, mirrored for LEFT; the orange ground wedge "›" of prep board pieces points along `dir` (rotated on the ground
|
||||
// plane). `setDir(dir)` re-orients a live view (swapping Front ⇄ Back without a fallback flash). Enemies flip by the
|
||||
// sign of their horizontal velocity (with hysteresis).
|
||||
// sign of their horizontal velocity (with hysteresis). A dead or knocked-out operator facing UP falls and lies with its
|
||||
// Front model — 131 of the 135 Back skeletons have no Die clip (GitHub issue #25: the Back model went on with its
|
||||
// attack loop under the redeploy ring) — and stands up again with the Back model (`_wantsBack`, `_syncModel`).
|
||||
//
|
||||
// Knocked-out operators (user playtest #4 item 9, b.snap `down`): `setDown([id, respawnAt, respawnTime, state, row,
|
||||
// col])` keeps a dead operator on the tile it lies on (row / col: where it fell, or its home — sim Battle._layBody,
|
||||
// player report F5 after 0.1.0) in its knocked-down pose — the Spine Die clip played once and held on its last
|
||||
// frame (the collapsed / kneeling pose with closed eyes), slightly greyed — with a redeploy ring above its head:
|
||||
// frame (the collapsed / kneeling pose with closed eyes; the Front model's for one facing UP, see above), slightly
|
||||
// greyed — with a redeploy ring above its head:
|
||||
// a dark disc, a mint arc filling as the respawn timer runs and the seconds left; once the timer is done and it still
|
||||
// waits, a full amber ring with "DP" (not enough DP) or a red ring with "!" (its tile is taken). `onDeploy` (the
|
||||
// redeploy) plays the deploy clip and restores the normal look; `setDown(null)` on a dead view lets it fade out.
|
||||
@@ -81,6 +84,17 @@ export function enemyModelScale(rec) {
|
||||
const k = Number(rec && rec.modelScale);
|
||||
return Number.isFinite(k) && k > 0.05 && k < 20 ? k : 1;
|
||||
}
|
||||
/**
|
||||
* Seconds of the death clip of a manifest Spine entry (`anims.die`, else a 'Die' clip, as SpineActor.dieClip; its
|
||||
* `animations` duration), 0 when it has none — 131 of the 135 operator Back models (GitHub issue #25).
|
||||
*/
|
||||
export function dieClipDur(entry) {
|
||||
const durs = entry && entry.animations && typeof entry.animations === 'object' ? entry.animations : null;
|
||||
if (!durs) return 0;
|
||||
const name = (entry.anims && typeof entry.anims.die === 'string' && entry.anims.die) || 'Die';
|
||||
const d = Object.hasOwn(durs, name) ? Number(durs[name]) : NaN;
|
||||
return Number.isFinite(d) && d > 0 ? d : 0;
|
||||
}
|
||||
/** World step (x = col, y = row) of a direction. */
|
||||
export const DIR_STEP = Object.freeze({ UP: [0, 1], RIGHT: [1, 0], DOWN: [0, -1], LEFT: [-1, 0] });
|
||||
const nowMs = () => (globalThis.performance ? globalThis.performance.now() : Date.now());
|
||||
@@ -325,6 +339,7 @@ export class UnitView {
|
||||
this.body.addChild(this.fallback);
|
||||
this.actor = null;
|
||||
this.spineReady = false;
|
||||
this._modelDirty = false; // died / stood up since the last frame: update() checks Front ⇄ Back (_syncModel)
|
||||
this._spineBusy = false; // a Spine load of this view is in flight
|
||||
this._spineTries = 0; // failed loads since the last model (SPINE_RETRY_MS)
|
||||
this._retryAt = 0; // when the next retry is due (ms, performance clock; 0 = none)
|
||||
@@ -381,7 +396,8 @@ export class UnitView {
|
||||
const a = this.ctx.assets;
|
||||
if (!a || !a.spineEntry || !a.spine) return;
|
||||
const id = this.info.spine || this.info.defId;
|
||||
// Front/Back rule (research 07 §5.5 / 09 §1.2): Front facing right/down (mirrored for left), Back facing up.
|
||||
// Front/Back rule (research 07 §5.5 / 09 §1.2): Front facing right/down (mirrored for left), Back facing up — while
|
||||
// standing (a knocked-out operator lies with the model that has a fall: _wantsBack).
|
||||
const back = this._wantsBack();
|
||||
const entry = id ? a.spineEntry(id, { back }) : null;
|
||||
if (!entry || this.ctx.settings?.quality === 'low' && this.isEnemy && !this.isBoss && this.ctx.crowded?.()) return;
|
||||
@@ -427,8 +443,11 @@ export class UnitView {
|
||||
this._releaseEntry(entry);
|
||||
return;
|
||||
}
|
||||
// a Front ⇄ Back swap (setDir) replaces the previous model in place: no fallback diamond in between
|
||||
// a Front ⇄ Back swap (setDir, or knocked out / standing again: _syncModel) replaces the previous model in place:
|
||||
// no fallback diamond in between; a deploy clip it was playing goes on on the new model (an operator facing UP
|
||||
// redeployed: its Back model takes over from the Front model it lay down with)
|
||||
const swap = !!this.actor;
|
||||
const deployed = swap && this.alive ? this.actor.deployElapsed() : null;
|
||||
if (swap) this._dropActor();
|
||||
this.actor = actor;
|
||||
this._actorEntry = entry;
|
||||
@@ -448,6 +467,7 @@ export class UnitView {
|
||||
} else {
|
||||
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); }
|
||||
}
|
||||
}, () => {
|
||||
if (req === this._spineReq) this._spineBusy = false;
|
||||
@@ -602,11 +622,27 @@ export class UnitView {
|
||||
else if (this.dir === 'LEFT') this.setDir('RIGHT');
|
||||
}
|
||||
|
||||
/** Whether this unit should show its Back model (facing UP and a Back model exists). */
|
||||
/**
|
||||
* Whether this unit should show its Back model: facing UP and a Back model exists — while it stands. A dead or
|
||||
* knocked-out one keeps the Back model only when that skeleton has a fall of its own (4 of the 135 Back models); the
|
||||
* others have no Die clip, so the Front model shows the fall and the held knocked-down pose (GitHub issue #25: the
|
||||
* Back model went on with its attack loop under the redeploy ring) [ASSUMED: no source says which model the official
|
||||
* client lies down with; a Back skeleton without a Die clip cannot show a fall].
|
||||
*/
|
||||
_wantsBack() {
|
||||
const a = this.ctx.assets;
|
||||
const id = this.info.spine || this.info.defId;
|
||||
return !this.isEnemy && this.dir === 'UP' && !!id && !!a && typeof a.hasBack === 'function' && !!a.hasBack(id);
|
||||
if (this.isEnemy || this.dir !== 'UP' || !id || !a || typeof a.hasBack !== 'function' || !a.hasBack(id)) return false;
|
||||
return this.alive || dieClipDur(typeof a.spineEntry === 'function' ? a.spineEntry(id, { back: true }) : null) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the model the unit wants when it is not the one shown or loading (Front ⇄ Back: its direction, or it went down /
|
||||
* stood up again). A swap replaces the model in place (_acquireSpine: no diamond in between; a fall or a deploy clip
|
||||
* goes on where it is). Nothing before the first model was asked for (no manifest entry yet).
|
||||
*/
|
||||
_syncModel() {
|
||||
if (this.entry && this._wantsBack() !== !!this.entryBack) this._loadSpine();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -623,7 +659,7 @@ export class UnitView {
|
||||
this.facing = d === 'LEFT' ? -1 : 1;
|
||||
this.visFacing = this.facing;
|
||||
if (this.imp) this.imp.dirty = true;
|
||||
if (this.entry && this._wantsBack() !== !!this.entryBack) this._loadSpine();
|
||||
this._syncModel();
|
||||
}
|
||||
|
||||
_releaseEntry(entry) {
|
||||
@@ -637,6 +673,7 @@ export class UnitView {
|
||||
this.imp = null;
|
||||
}
|
||||
this._box = null;
|
||||
this._tint = undefined; // the next model gets the current tint on its first frame (a knocked-down one its grey)
|
||||
const old = this.actor;
|
||||
this.actor = null;
|
||||
this.spineReady = false;
|
||||
@@ -705,11 +742,17 @@ export class UnitView {
|
||||
/**
|
||||
* The unit died: its Die clip plays, then it fades (unless it stays down, setDown). `instant`: it starts on the held
|
||||
* end of the clip — a unit that is already down (a field joined mid-battle, an operator entering 联防 knocked out).
|
||||
* An operator facing UP whose Back model has no Die clip (GitHub issue #25) falls with its Front model: the swap is
|
||||
* made by this frame's update (`_modelDirty` → _syncModel, after every event and snapshot of the frame, so a death
|
||||
* and a redeploy in one frame — a 突袭 jump, a 不屈 revive, a backlog after a hidden tab — load nothing); meanwhile the
|
||||
* Back model holds still (SpineActor.die), and the Front model's Die clip times the fade.
|
||||
*/
|
||||
die(instant = false) {
|
||||
if (!this.alive) return;
|
||||
this.alive = false;
|
||||
const d = this.actor ? this.actor.die() : 0;
|
||||
this._modelDirty = true;
|
||||
let d = this.actor ? this.actor.die() : 0;
|
||||
if (this.actor && !d) d = this._fallDur();
|
||||
const rate = this.ctx.animRate?.() || 1;
|
||||
this.dieT = 0;
|
||||
this.dying = clamp(d / rate, 0.35, 1.6) + 0.55;
|
||||
@@ -717,8 +760,17 @@ export class UnitView {
|
||||
if (instant) { this.dieT = 30; if (this.actor) this.actor.update(30); }
|
||||
}
|
||||
|
||||
/** Seconds of the fall of the model the unit wants now (its manifest entry's Die clip), 0 without one. */
|
||||
_fallDur() {
|
||||
const a = this.ctx.assets;
|
||||
const id = this.info.spine || this.info.defId;
|
||||
return a && typeof a.spineEntry === 'function' && id ? dieClipDur(a.spineEntry(id, { back: this._wantsBack() })) : 0;
|
||||
}
|
||||
|
||||
/** Standing again (a redeploy, a revive): an operator facing UP gets its Back model back (this frame's update). */
|
||||
revive() {
|
||||
this.alive = true;
|
||||
this._modelDirty = true;
|
||||
this.dying = 0; this.remove = false; this.alpha = 1;
|
||||
this.down = null;
|
||||
this.hp = this.maxHp; this.ghostHp = this.hp;
|
||||
@@ -761,6 +813,8 @@ export class UnitView {
|
||||
update(dt, cam, t) {
|
||||
if (this.destroyed) return;
|
||||
const P = this.P;
|
||||
// the model for the state the frame's events and snapshot left (Front ⇄ Back when it went down / stood up: die, revive)
|
||||
if (this._modelDirty) { this._modelDirty = false; this._syncModel(); }
|
||||
// a failed / timed-out model load is tried again once its wait is over (SPINE_RETRY_MS; frames only: never hidden)
|
||||
if (this._retryAt && nowMs() >= this._retryAt) { this._retryAt = 0; if (!this.actor && !this._spineBusy) this._loadSpine(true); }
|
||||
if (this.zTarget != null && this.z !== this.zTarget) {
|
||||
|
||||
Reference in new issue
Block a user