Merge upstream 0.2.2 and resolve reroll conflicts (#408)

This commit is contained in:
Siornya committed 2026-10-10 00:46:46 +08:00
commit be095d8638
447 files changed
+33376 -8843

No files matched your search

+16 -5
View File
@@ -12,14 +12,21 @@
// opts.mode 'solo' | 'coop'
// opts.difficulty 'FUNNY'|'NORMAL'|'HARD'|'ABYSS'
// opts.modeId string modeIdFor(mode, difficulty), e.g. 'mode_multi_hard'
// opts.aiPicksLast boolean (optional) the co-op room option 「AI 队友最后选择」 (room.setAiPicksLast, GitHub #338):
// the strategy and 机变 drafts put every human seat before every AI seat
// (MatchPhases.humansFirst); only `true` turns it on; ignored in solo
// opts.seats Array<{ seat: 0..3, playerId: string, name: string, isBot: boolean, connected: boolean,
// loadout?: { [baseChessId]: { skill: index, module: uniEquipId|'none'|null } } | null,
// ops?: { [charId]: { potential: 1–6, cultivate: 0–3 } } | null,
// notOwned?: string[] | null,
// diy?: { [slotBaseId]: { charId, skillIndex, uniEquipId } } | null }>
// sorted by seat, 1–4 entries, ≥ 1 human; solo ⇒ exactly 1 human and no bots.
// Bot playerIds start with 'ai_'. Seat indexes may have gaps (e.g. seats 0 and 2).
// `loadout` (DESIGN §16, optional): the human's operator loadout, already checked by the lobby
// (shared/protocol.js checkLoadout); PlayerState re-checks it against opts.data and ignores it for bots.
// `ops` (0.2.2, optional): the human's per-operator 潜能 / 练度 (checkLoadoutOps; missing = 潜能 6, 精英2
// Lv.60 — the owner's decision of 2026-10-08), re-checked and changed like the loadout (INFO_CHECK);
// bots fight with the defaults.
// `notOwned` (0.2.0 补位, optional): the base chess ids the human marked as not owned (干员持有) — fixed
// for the match; those chess fight as their stand-ins (PlayerState setNotOwned; bots own everything).
// `diy` (0.2.0 自选编队, optional): the human's 自选 picks (shared/protocol.js checkDiyPicks) — fixed for
@@ -53,9 +60,10 @@
// (a resync request). May be called without a preceding onDisconnect. Resend full
// state: m.public, m.private and, if a battle is on (client-side combat), the
// b.start of the field the player is on / watching (server-run mode: m.field + b.snap).
// setLoadout(playerId, loadout) → { ok } | { error, detail? } (DESIGN §16; optional for the platform) a new checked
// operator loadout from room.loadout. Accepted only during INFO_CHECK (the briefing's
// 干员调配 entry); afterwards the match's loadout is locked (WRONG_PHASE).
// setLoadout(playerId, loadout, ops?) → { ok } | { error, detail? } (DESIGN §16; optional for the platform) a new
// checked operator loadout (and its 潜能 / 练度 `ops`, 0.2.2) from room.loadout. Accepted only
// during INFO_CHECK (the briefing's 干员调配 entry); afterwards the match's loadout is locked
// (WRONG_PHASE).
// onLeave(playerId) The human quit permanently (g.leave, room.leave, or the 10-minute reconnect
// window expired). They will never return under this playerId in this match;
// treat as quit (AI takes over / eliminated per DESIGN). No onDisconnect follows.
@@ -106,7 +114,7 @@
// (g.bandFocus → timeoutBand); g.unitStats answers m.unitStats: the stats the board's units start their next battle with.
// opts.clientCombat default true (env SP_COMBAT=server → false: the legacy server-run + snapshot streaming mode)
// opts.verify 'off' | 'sample' | 'all' (env SP_VERIFY, default 'off'): re-simulate accepted client results
// ('sample': ~1 in 8, in a later callback, mismatches logged; 'all': before accepting — the
// ('sample': ~1 in 8, in later slices, mismatches logged; 'all': before accepting — the
// server's result wins on a mismatch)
//
// Engine-only extra options (tests / tools; the lobby never passes them):
@@ -121,7 +129,7 @@
// opts.botSliceMs wall-clock ms of rehearsal per scheduler callback (default 8 with a real scheduler, unbounded
// with a virtual one); the rest runs in later callbacks (scheduleBotPrep)
// opts.headlessSliceMs wall-clock ms per callback of a server-run normal / 联防 field (client-side combat: bots,
// takeovers; default 8 with a real scheduler, at once with a virtual one)
// takeovers, result verification; default 8 with a real scheduler, at once with a virtual one)
// Seats may be all bots (tools/matchrun.mjs); the lobby always has ≥ 1 human.
//
// Diagnostics: m.errors / m.errorCount (engine), m.dispatcher.errors / .errorsByKey (meta handlers), m.simErrors
@@ -151,6 +159,7 @@
// intents.js the g.* / b.* router, emotes, AI 托管, g.unitStats
// pause.js the solo pause
// phases.js INFO_CHECK, BAND_DRAFT (the strategy draft), BATTLE_CHECK, ROUND_START
// setupVote.js unanimous INFO_CHECK setup reroll, suspended confirmation, preserved player settings
// spDraft.js SP_DRAFT (机变) and the rolls the meta effects ask for (bounties, item ids, pools)
// prep.js PREP: its start, the AI seats' sliced preps, Ready, the deadline, its end
// combat.js battle construction, COMBAT (both modes), the per-field results, then 联防 or SETTLE
@@ -237,6 +246,8 @@ export class Match {
this.gd = new GameData(this.data, this.modeId);
if (!this.difficulty) this.difficulty = this.gd.difficulty;
this.isSolo = this.mode === 'solo' || this.gd.isSolo;
/** 「AI 队友最后选择」 (opts.aiPicksLast, GitHub #338): humans draft before AI seats (MatchPhases.humansFirst) */
this.aiPicksLast = !this.isSolo && opts.aiPicksLast === true;
this.ownsScheduler = !opts.scheduler;
this.sched = opts.scheduler || new RealScheduler({ now: opts.now || Date.now, onError: (e) => this.reportError('timer', e) });
this.registry = opts.registry || getDefaultRegistry();