0.2.6-dev: port PR #416 by Cromemadnd — at every round start the own board's pieces replay their Start clip and landing sound one after another in deployment order (the maintainer's confirmation from official play, 2026-10-13), and a purchase or a move onto / across the board plays them too ([ASSUMED], the PR author's account); one sound per landing, idle prep pieces only, the round marked once the view took the sequence, own board only, timers cleared when prep mode ends and on destroy; DESIGN §31.8; golden: unchanged
This commit is contained in:
1 parent
281cf01ff0
commit
231da0efbc
16 files changed
+791
-22
No files matched your search
@@ -35,6 +35,13 @@
|
||||
// redeploy) plays the deploy clip and restores the normal look; `setDown(null)` on a dead view lets it fade out.
|
||||
// An operator that enters a battle already knocked out (联防, user playtest #5 item 2: sim 'die' reason 'forcedExit')
|
||||
// goes down with `die(true)`: straight to the held end of the clip, no fall.
|
||||
// Prep landings (PR #416 by @Cromemadnd): `land()` plays a prep piece's deploy clip (Start) again with its landing sound
|
||||
// — the round start's replay of the own board (render/app.js replayDeploys), a purchase, a move onto or across the board.
|
||||
// One landing, one sound: a piece whose Start still plays (on its model, owed to a model still loading, or cued less than
|
||||
// a clip ago) keeps it and sounds nothing more, and only an idle prep piece is ever pulled into it — never a battle unit
|
||||
// or one in an attack, skill, death or form clip. The sound is `ctx.audio.deployCue` (public/js/audio.js: the unit's own
|
||||
// battle deploy sound, through the SFX volume, mute and the battle limiter); `onDeploy` itself — the sim's 'deploy'
|
||||
// events — stays silent: audio.handleBattleEvents sounds those.
|
||||
// Enemy modes (the `form` of a sim fx — shared/protocol.js fxForm — → `setForm(form, fx)`): 掠海漂移体 dropping to 爬行模式
|
||||
// (user playtest #5 item 1) plays its skeleton's 'Change' clip once, then the crawl set (*_02); 暴鸰 flies on without its
|
||||
// bomb (*_2) after the drop (feedback D4); 转译基底's forms, the 逐火 embers, 再生's puppet, the leaders' 重生 and 守墓石像
|
||||
@@ -149,6 +156,11 @@ export const SPINE_RETRY_MS = Object.freeze([2000, 6000, 15000, 30000]);
|
||||
* [ASSUMED] the length: a healthy load finishes within a few seconds once the tab is visible.
|
||||
*/
|
||||
export const SPINE_STUCK_MS = 5000;
|
||||
/**
|
||||
* Seconds a prep landing (UnitView.land) counts as playing when the model's Start clip length is unknown (no manifest entry
|
||||
* yet): the operators' Start clips run 0.77–1.2 s, most of them 1 s (data/assets.json). [ASSUMED]
|
||||
*/
|
||||
export const START_CLIP_FALLBACK = 1;
|
||||
|
||||
/** Heights above this count as standing on a raised top (bench pads are the lowest raised tiles, 0.16). */
|
||||
const RAISED_Z = 0.12;
|
||||
@@ -447,9 +459,9 @@ function bar(P, parent, color, alpha = 1) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared context handed to every view by app.js.
|
||||
* Shared context handed to every view by app.js (`audio`: the audio manager of public/js/audio.js — prep landings, land()).
|
||||
* @typedef {{ P: any, layers: { shadow: any, units: any, bars: any, groundFx: any },
|
||||
* assets: any, cam: () => any, heightAt: (r:number,c:number)=>number, fx: any, time: () => number,
|
||||
* assets: any, audio?: any, cam: () => any, heightAt: (r:number,c:number)=>number, fx: any, time: () => number,
|
||||
* settings: { damageNumbers: boolean, quality: string } }} ViewCtx
|
||||
*/
|
||||
|
||||
@@ -565,6 +577,7 @@ export class UnitView {
|
||||
this.spineReady = false;
|
||||
this._modelDirty = false; // died / stood up since the last frame: update() checks Front ⇄ Back (_syncModel)
|
||||
this._pendingDeployElapsed = null; // elapsed game seconds until a late model can continue Start
|
||||
this._landUntil = 0; // a prep landing (land) owns the Start clip until then (ms, performance clock)
|
||||
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)
|
||||
@@ -1060,6 +1073,44 @@ export class UnitView {
|
||||
if (this.actor) this.actor.deploy();
|
||||
}
|
||||
|
||||
/**
|
||||
* A prep piece lands (PR #416 by @Cromemadnd; see the header): its Start clip again — a model still loading continues it
|
||||
* by the time gone (onDeploy, 0.2.3) — and its landing sound, once. Refused (false) for anything but an idle prep piece
|
||||
* (a battle unit, one dying, lifted by a drag, or in another clip) and for a piece whose Start already plays: a second
|
||||
* trigger of the same landing — a purchase then a move, the round start's replay over a piece just put down — changes
|
||||
* nothing and sounds nothing. Refused in a hidden tab too: no frame shows it there, and the clip and the suspended audio
|
||||
* would only play it, stale, once the tab is back.
|
||||
* @returns {boolean} whether the Start clip started and the landing sound was asked for
|
||||
*/
|
||||
land() {
|
||||
if (this.destroyed || !this.prep || this.isEnemy || !this.alive || this.dying > 0 || this.lift > 0) return false;
|
||||
if (globalThis.document?.hidden) return false;
|
||||
if (this.deploying()) return false;
|
||||
if (this.actor && this.actor.mode !== 'base') return false;
|
||||
this.onDeploy();
|
||||
this._landUntil = nowMs() + this._startDur() * 1000;
|
||||
try { this.ctx.audio?.deployCue?.({ def: this.info.spine || this.info.defId, defId: this.info.defId, kind: this.info.kind }, this.id); } catch { /* the sound never stops the landing */ }
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the Start clip plays now: on the model, owed to a model still loading (onDeploy), or a landing (land) cued less
|
||||
* than a clip ago — the last covers a model without a Start clip and a view without a model.
|
||||
*/
|
||||
deploying() {
|
||||
if (this.actor && this.actor.deployElapsed() != null) return true;
|
||||
if (!this.actor && this._pendingDeployElapsed != null && this._pendingDeployElapsed < this._startDur()) return true;
|
||||
return nowMs() < this._landUntil;
|
||||
}
|
||||
|
||||
/** Seconds of the Start clip of the model asked for (its manifest entry), START_CLIP_FALLBACK without one. */
|
||||
_startDur() {
|
||||
const e = this.entry;
|
||||
const name = e && e.anims && typeof e.anims.deploy === 'string' ? e.anims.deploy : null;
|
||||
const d = name && e.animations && Object.hasOwn(e.animations, name) ? Number(e.animations[name]) : NaN;
|
||||
return Number.isFinite(d) && d > 0 ? d : START_CLIP_FALLBACK;
|
||||
}
|
||||
|
||||
onStatus(key, on) {
|
||||
if (typeof key !== 'string') return;
|
||||
if (on) this.statuses.add(key); else this.statuses.delete(key);
|
||||
|
||||
Reference in new issue
Block a user