feedback5 R2: Match.js → server/match/match/*.js, a pure move — the 167 class members (166 methods and the soloUntimed getter), verbatim with their comments, go to 16 per-concern method containers (platform, infra, messaging, views, watch, intents, pause, phases, spDraft, prep, combat, clientCombat, reports, unitePhase, bossRounds, settle) that Match.js installs on Match.prototype in a fixed order with class-method descriptors (a name defined twice throws); the constants the modules share go to match/common.js (FLOW_TICKER_PRIORITY, DELAYS, BAND_TURN_SECONDS re-exported by Match.js; OK, fail, BOSS_CLOCK_MS), GAME_TYPES to platform.js, the boss-round budgets / rates to bossRounds.js; Match.js keeps the header (+ a code-layout paragraph), the constructor-only constants and the class with its constructor. Section-header comments are replaced by the module headers. docs-consistency reads the method modules (matchText) and pins layerGainRoom in match/settle.js

This commit is contained in:
sganggs committed 2026-10-05 17:10:38 +08:00
1 parent 8ab6d021da
commit 1ca4936f0a
19 files changed
+3105 -2923

No files matched your search

+139
View File
@@ -0,0 +1,139 @@
// server/match/match/prep.js — Match methods: the PREP phase — its start (deferred item merges, onPrepStart), the AI
// seats' sliced preps (scheduleBotPrep: economy + layout, rehearsal, Ready), Ready and the deadline, and its end
// (onPrepEnd, PlayerState.endPrep, then COMBAT or the Final Assault / Hidden Core).
// Installed on Match.prototype by server/match/Match.js (a method container: never instantiated; `this` is the match).
import { PHASE } from '../../../shared/constants.js';
import { botPrepBeginSteps, botPrepEndSteps } from '../bot.js';
import { DELAYS } from './common.js';
export class MatchPrep {
enterPrep() {
this.phase = PHASE.PREP;
this.sp = null;
const alive = this.alivePlayers();
for (const ps of alive) {
ps.ready = false;
// Items gained as the previous prep ended waited unmerged (acquireItem deferMerge). Merge them now, before
// this prep's onPrepStart grants and before the player acts — not in endPrep, which runs in the same prep
// that granted them and would take an equipped copy off for the fight about to start.
ps.checkItemMerges();
ps.recompute();
this.dispatch(ps, 'onPrepStart', { round: this.round });
ps.recompute();
}
// solo / single-human matches: untimed (soloUntimed); co-op: the round's prepTime
const secs = this.soloUntimed ? null : this.gd.prepTime(this.round);
this.setDeadline(secs, () => this.prepDeadline());
let i = 0;
for (const ps of alive) if (ps.botControlled) this.scheduleBotPrep(ps, i++);
this.markPublic();
this.maybeEndPrep();
}
/**
* The bot plays a prep in three stages, every one in slices of ≤ botSliceMs wall-clock ms (one scheduler callback
* each, so other rooms' battles and every player's requests keep flowing): economy + the default layout
* (bot.js botPrepBeginSteps — shop decisions and layout planning, 50–120 ms late in a 4-bot match), the layout
* rehearsal (whole simulated battles, 0.2–1 s of CPU per bot late in a match), then botPrepEndSteps (the rehearsed
* layout, temp, Ready). The step generators run the same actions in the same order as the one-shot routine (same
* rng draws, same decisions); in virtual time (botSliceMs unbounded) each stage runs at once. The prep ending first
* (deadline) or a newer schedule for the seat drops the job (a step never leaves a transient board behind).
*/
scheduleBotPrep(ps, i = 0) {
const round = this.round;
const token = (ps._botPrepToken = (ps._botPrepToken || 0) + 1);
const valid = () => this.phase === PHASE.PREP && this.round === round && ps.alive && !ps.ready && ps.botControlled && ps._botPrepToken === token;
const bounded = Number.isFinite(this.botSliceMs);
const now = () => (typeof performance !== 'undefined' ? performance.now() : Date.now());
/** Step a generator until done or the slice budget is used; `then(value)` once it is done (null on an error). */
const drive = (gen, label, then) => {
const t0 = now();
let r = null;
try {
do r = gen.next(); while (!r.done && !(bounded && now() - t0 >= this.botSliceMs));
} catch (e) {
this.reportError(`bot ${ps.playerId}${label}`, e);
then(null);
return;
}
if (r.done) { then(r.value); return; }
this.later(0, () => { if (valid()) drive(gen, label, then); });
};
const ready = () => {
if (!ps.ready) {
ps.resolveTemp();
ps.setReady(true);
}
};
const end = (job) => {
let gen = null;
try { gen = botPrepEndSteps(this, ps, job); } catch (e) { this.reportError(`bot ${ps.playerId}`, e); }
if (!gen) { ready(); return; }
drive(gen, '', ready);
};
this.later(this.scaled(DELAYS.BOT_ACTION + i * DELAYS.BOT_STAGGER), () => {
if (!valid()) return;
drive(botPrepBeginSteps(this, ps), '', (job) => {
if (!job) { end(null); return; }
const slice = () => {
if (!valid()) return;
let done = true;
try { done = job.run(this.botSliceMs); } catch (e) { this.reportError(`bot ${ps.playerId} rehearsal`, e); }
if (done) { if (bounded) this.later(0, () => { if (valid()) end(job); }); else end(job); }
else this.later(0, slice);
};
// bounded slices start in a callback of their own (the economy + default layout above already used this one)
if (bounded) this.later(0, slice);
else slice();
});
});
}
onReadyChanged(ps) {
this.markPublic();
void ps;
this.maybeEndPrep();
}
maybeEndPrep() {
if (this.phase !== PHASE.PREP || this._prepEndQueued) return;
const allReady = () => { const alive = this.alivePlayers(); return alive.length > 0 && alive.every((p) => p.ready); };
if (!allReady()) return;
const round = this.round;
this._prepEndQueued = true;
// the prep deadline stays armed until the phase really ends: a player may un-ready before this runs
this.later(0, () => {
this._prepEndQueued = false;
if (this.phase === PHASE.PREP && this.round === round && allReady()) this.endPrep();
});
}
prepDeadline() {
if (this.phase !== PHASE.PREP) return;
for (const ps of this.alivePlayers()) {
if (ps.ready) continue;
ps.resolveTemp();
ps.ready = true;
ps.dirty();
}
this.endPrep();
}
endPrep() {
if (this.phase !== PHASE.PREP) return;
this.setDeadline(0);
const alive = this.alivePlayers();
for (const ps of alive) this.dispatch(ps, 'onPrepEnd', { round: this.round });
for (const ps of alive) ps.endPrep();
const r = this.round;
if (r === this.gd.bossRound) {
this.hiddenLayerSum = alive.reduce((s, p) => s + p.activatedLayers(), 0);
this.startFinalAssault(false);
} else if (r === this.gd.hiddenRound) {
this.startFinalAssault(true);
} else {
this.startCombat();
}
}
}