Files
ark-stronghold-protocol/server/match/match/pause.js
T

73 lines
3.5 KiB
JavaScript

// server/match/match/pause.js — Match methods: the solo pause (g.pause, DESIGN §14 "Solo pause") — freeze, resume
// (every clock and deadline shifted by the pause), the drop at the battle phase's end, and the field clocks' "now".
// Installed on Match.prototype by server/match/Match.js (a method container: never instantiated; `this` is the match).
import { PHASE, ERR } from '../../../shared/constants.js';
import { BOSS_CLOCK_MS, OK, fail } from './common.js';
export class MatchPause {
/**
* Solo pause (official PauseUp / ResumeUp; DESIGN §14 "Solo pause"): g.pause { on } freezes the running battle — the
* field clock (b.start `elapsed`, the boss budgets and overtime), the result deadline / server release timers, the
* boss clock, the HUD `deadline` / `overtimeAt` (shifted by the pause on resume) and the server-run pacers
* (FieldRunner / HeadlessPacer skip their intervals) — and the browser's runner stops its local clock while
* `m.public.paused` is true. Solo matches only (co-op battles never pause: WRONG_PHASE) and only while a battle runs;
* `{ on: false }` is always accepted. A disconnect / leave, or the battle phase ending, resumes.
*/
setPause(ps, on) {
void ps;
if (!this.isSolo) return fail(ERR.WRONG_PHASE, 'co-op battles never pause');
if (!on) { this._resume(); return OK; }
if (this.paused) return OK;
const battlePhase = this.phase === PHASE.COMBAT || this.phase === PHASE.FINAL_ASSAULT || this.phase === PHASE.HIDDEN_CORE;
if (!battlePhase || this._finalEnding || !this.fields.some((f) => f.live && !f.done)) return fail(ERR.WRONG_PHASE, 'no battle running');
this.paused = true;
this._pausedAt = this.sched.now();
for (const f of this.fields) {
if (!f.cc) continue;
if (f.deadlineTimer) { this.cancel(f.deadlineTimer); f.deadlineTimer = null; f.rearmDeadline = true; }
if (f.doneTimer) { this.cancel(f.doneTimer); f.doneTimer = null; f.rearmRelease = true; }
}
if (this._bossClock) { this.cancel(this._bossClock); this._bossClock = null; }
this.markPublic();
return OK;
}
/** End a solo pause: every clock and deadline moves on by the paused time (no-op when not paused). */
_resume() {
if (!this.paused) return;
const d = Math.max(0, this.sched.now() - this._pausedAt);
this.paused = false;
this._pausedAt = 0;
this.pausedMs += d;
if (this.deadline) this.deadline += d;
if (this.overtimeAt) this.overtimeAt += d;
if (this._bossStartAt != null) this._bossStartAt += d;
for (const f of this.fields) {
if (!f.cc || f.done) continue;
f.startAt += d;
f.lastProgressAt += d;
if (f.rearmDeadline && f.mode === 'client') this._armDeadline(f);
if (f.rearmRelease && f.mode === 'server') this._armRelease(f);
f.rearmDeadline = false;
f.rearmRelease = false;
}
if (this._bossClockOn && !this._bossClock && (this.phase === PHASE.FINAL_ASSAULT || this.phase === PHASE.HIDDEN_CORE)) {
this._bossClock = this.later(BOSS_CLOCK_MS, () => this._bossClockTick());
}
this.markPublic();
}
/** The battle phase is over: drop the pause without shifting anything (its timers are gone). */
_clearPause() {
if (!this.paused) return;
this.pausedMs += Math.max(0, this.sched.now() - this._pausedAt);
this.paused = false;
this._pausedAt = 0;
this.markPublic();
}
/** The field clocks' "now": frozen at the pause instant while paused. */
_clockNow() { return this.paused ? this._pausedAt : this.sched.now(); }
}