73 lines
3.5 KiB
JavaScript
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(); }
|
|
}
|