diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 3372a17c..76bdb3aa 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -11,3 +11,7 @@ b48a67caec5ba0699097e21f3db4d4dbb35a2aed 560328cacc863c26d07780096527e255ebf62bc4 # feedback5 R4: kits/tier1..6.js → kits/ops/ (one file per kit), kits/shared/, kits/index.js 1169102d46742474bf0f0189d467c791984d9e05 +# feedback5 R2: server/match/Match.js → server/match/match/*.js (the Match methods, verbatim; shared constants in common.js) +1ca4936f0a541cabda9acae5fe6f9213df4b5e6a +# feedback5 R2: server/match/PlayerState.js → server/match/player/*.js (the PlayerState methods, verbatim) +64443b4aab3126d6703509b8c47e9cfa58b75157 diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 16a9c3e7..d020a46c 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -56,8 +56,15 @@ server/ lobby.js rooms (4-letter codes), seats, host, AI seats, ready/start, reconnect tokens, room→Match wiring data.js loads data/*.json once, builds indexes (getChess, getBond, …); frozen objects match/ - Match.js match state machine, timers, round loop, co-op orchestration, broadcasting views - PlayerState.js per-player economy/shop/hand/board/items/bonds/LP state + all prep-intent handlers + Match.js match state machine, timers, round loop, co-op orchestration, broadcasting views — the class: + the lobby⇄match interface (header), constructor + method install + match/ Match's methods by concern (platform, infra, messaging, views, watch, intents, pause, phases, + spDraft, prep, combat, clientCombat, reports, unitePhase, bossRounds, settle; shared constants + in common.js), installed on Match.prototype + PlayerState.js per-player economy/shop/hand/board/items/bonds/LP state + all prep-intent handlers — the class: + constructor + method install + player/ PlayerState's methods by concern (basics, pieces, acquire, economy, placement, items, prep, + round, views; common.js), installed on PlayerState.prototype pool.js SharedPool (copies per base chess), banned/disabled bonds, odds & rolls board.js placement legality, deploy cap, hand/temp management, merge detection & execution bondsMeta.js bond member counting (BOARD / BOARD_AND_DECK / golden), activation tiers, persistent layers diff --git a/docs/META.md b/docs/META.md index ccf20097..3ff9df23 100644 --- a/docs/META.md +++ b/docs/META.md @@ -7,7 +7,14 @@ Normative contracts stay in DESIGN.md §6 and §8; this file documents the imple ``` server/match/ Match.js state machine, timers, round loop, co-op orchestration, views (the lobby⇄match interface is at the top) - PlayerState.js per-player economy / shop / hand / board / items / bonds / LP + every prep intent handler + — the class: constructor + method install + match/ Match's methods by concern: platform, infra, messaging, views, watch, intents, pause, phases, + spDraft (机变), prep, combat, clientCombat, reports, unitePhase (联防), bossRounds (Final Assault / + Hidden Core), settle; common.js (FLOW_TICKER_PRIORITY, DELAYS, BAND_TURN_SECONDS, re-exported by Match.js) + PlayerState.js per-player economy / shop / hand / board / items / bonds / LP + every prep intent handler — the class: + constructor + method install + player/ PlayerState's methods by concern: basics, pieces, acquire (gains, merges), economy (funds, layers, + the shop), placement (g.move), items, prep, round (lifecycle, battleInput), views; common.js gamedata.js typed, defaulted view of data/*.json (config tunables with research defaults) + the balance layer (data/tuning.json, §3.1) pool.js SharedPool (copies per base chess, across players), per-match bans, copy-weighted rolls diff --git a/docs/research/09-ux-official.md b/docs/research/09-ux-official.md index 5ed7a559..dbd810b0 100644 --- a/docs/research/09-ux-official.md +++ b/docs/research/09-ux-official.md @@ -318,7 +318,7 @@ The public mirror (ArknightsAssets2 `cn`, `ui/emoticon/theme/[uc]/icon/ - `g.move {uid, to:{area:'board',row,col}, dir}` with `dir ∈ 'UP'|'RIGHT'|'DOWN'|'LEFT'`; the server defaults to `'RIGHT'` when absent, for bots and old clients. - `g.art {itemUid,row,col,dir}`. - Broadcast `dir` in the board state (public and private views, snapshot `UnitInfo.dir`). -2. **`server/match/PlayerState.js`.** +2. **`server/match/PlayerState.js`** (the methods named here: `server/match/player/placement.js`, `items.js`). - Store `piece.dir`, preserved across rounds. - In `_moveChessToBoard` and `_moveTokenToBoard`, `if (occ === piece) return OK;` must become "update `piece.dir`, recompute, OK". This is the in-place re-orient. - Swaps keep each piece's own dir. @@ -363,7 +363,7 @@ The public mirror (ArknightsAssets2 `cn`, `ui/emoticon/theme/[uc]/icon/ 4. Prep: the same avatar → 前往查看 flow (read-only board + pen). 5. 联防 and FA fields: the ‹ › pill cycles LEFT half / 全景 / RIGHT half, with captions "你自己" / "👁 name#" / "全景". Non-helpers are auto-moved to the 联防 field as observers. In FA the other pair's field is not selectable. 6. Eliminated players: a dialog offers 继续观战 (auto-observe the first live field, free switching) or 退出. -7. `server/match/Match.js watch()` must enforce the same rules (prep: any teammate; combat: only if your own field finished; FA: own pair only; dead: anything). +7. `Match.watch()` (`server/match/match/watch.js`) must enforce the same rules (prep: any teammate; combat: only if your own field finished; FA: own pair only; dead: anything). ### 6.4 Combat computation model (user item 5, architecture) [proposal based on §3.2] diff --git a/docs/research/11-limits-official.md b/docs/research/11-limits-official.md index eb6101a4..873e8707 100644 --- a/docs/research/11-limits-official.md +++ b/docs/research/11-limits-official.md @@ -220,7 +220,7 @@ line is lost rather than clamped. - `shared/constants.js`: `BOND_LAYER_CAP = 999` and `BOSS_HIT_LIMIT = 300000` (0 / Infinity = off), plus `layerGainRoom(before, n)` = min(n, cap − before), never negative. - Layers: every writer clamps with it — `PlayerState.addLayers` (every prep-side gain: 特质, items, bands, 机变 cards, - bonds), the settle of the in-battle gains (`Match.js`), `Battle.addLayers` (the live in-battle copy, after the + bonds), the settle of the in-battle gains (`Match.settle`, `server/match/match/settle.js`), `Battle.addLayers` (the live in-battle copy, after the `layerGain` hook, as the client's `AddBondCount`); a gain at the cap adds 0 (no onLayers, no 'layer' event, no hook), so the per-N milestones (远见, 奇迹, 维多利亚 …) stop with the count; the client-result check (`fields.js`) bounds a reported gain by the room left from the bond's starting layers; `invariants.js` flags a bond above the cap. The dev diff --git a/public/dev/game-mock.js b/public/dev/game-mock.js index d4b3e72f..d8fb5e5f 100644 --- a/public/dev/game-mock.js +++ b/public/dev/game-mock.js @@ -681,7 +681,7 @@ async function mockRequest(t, f = {}) { const who = f.fieldId.replace(/^n:/, ''); if (who !== ME) { const units = shuffle(S.pool).slice(0, 6).map((c, i) => ({ id: 900 + i, kind: 'op', side: 'ally', ownerId: who, defId: c.chessId, name: c.name, tier: c.tier, golden: i === 0, spine: c.charId, avatar: c.assets.avatar, x: [3, 4, 5, 7, 8, 5][i], y: [9, 9, 10, 11, 12, 12][i], facing: 1, maxHp: c.stats.maxHp })); - // like server/match/Match.js prepFieldMeta: a read-only prep board with THEIR coming enemies (the pen shows them) + // like server/match/match/views.js prepFieldMeta: a read-only prep board with THEIR coming enemies (the pen shows them) const theirs = S.priv.nextEnemies.filter((e) => e.source !== 'bounty').slice(0, 3).map((e) => ({ ...e, gate: 'upper', count: e.count + 1 })); store.patch('match', { field: { fieldId: f.fieldId, kind: 'normal', rect: { ...GEO.NORMAL_RECT }, stageId: pub.stageId, units, prep: true, nextEnemies: theirs } }); } diff --git a/public/js/battle/observe.js b/public/js/battle/observe.js index e7fa93da..7b4112a8 100644 --- a/public/js/battle/observe.js +++ b/public/js/battle/observe.js @@ -1,5 +1,5 @@ // Observing rules and labels of client-side combat (research 09 §3.1 / §6.3, DESIGN §14 "Spectating") — pure helpers -// for the game screen, the team panel and the combat HUD (mirror of server/match/Match.js _watchClient): +// for the game screen, the team panel and the combat HUD (mirror of server/match/match/watch.js _watchClient): // * prep (休整期): tap a teammate → 前往查看 → their board (read-only); // * own normal battle running: no observing ("当前无法查看"); // * own battle over: "⌛ 作战结束,等待队友完成作战" + the teammates' progress; tap a teammate → 前往查看 → a local diff --git a/public/js/screens/bandDraft.js b/public/js/screens/bandDraft.js index e16e78a3..0bd162a4 100644 --- a/public/js/screens/bandDraft.js +++ b/public/js/screens/bandDraft.js @@ -58,7 +58,7 @@ export function allowedBands(bands, modeType) { export const DEFAULT_TIMEOUT_BAND = 'band_bldsk'; /** - * The strategy the server assigns me when my turn times out (server/match/Match.js defaultBand): the official default + * The strategy the server assigns me when my turn times out (server/match/match/phases.js defaultBand): the official default * 「华法琳」 while no teammate holds it, else the first free strategy in draft order (sortId) — never one a teammate * already picked (队友已选). * @param {any[]} bands allowedBands(...) (sortId order) @@ -218,7 +218,7 @@ export function BandDraftScreen() { act('g.bandFocus', { bandId: sel }, { sfx: false, quiet: true }); }, [sel, timed, myPick]); - // what a timeout gives me: the highlighted band while free, else the default (never 队友已选 — Match.js timeoutBand) + // what a timeout gives me: the highlighted band while free, else the default (never 队友已选 — match/phases.js timeoutBand) const autoId = autoPickBand(sel, { bands, taken, myPick, defaultId }); const autoName = (autoId && gd.band(autoId)?.name) || gd.band(defaultId)?.name || '华法琳'; diff --git a/public/js/screens/result.js b/public/js/screens/result.js index 86226ef2..8d9857c5 100644 --- a/public/js/screens/result.js +++ b/public/js/screens/result.js @@ -22,7 +22,7 @@ import { audio } from '../audio.js'; const cx = (...p) => p.flat().filter(Boolean).join(' '); // The first seven present are shown (+ remaining LP = a 4×2 grid); the title (评语) stats come first. -// `gold` is the funds a player SPENT (server/match/PlayerState.js spend(); the 挥金如土 title stat). +// `gold` is the funds a player SPENT (server/match/player/economy.js spend(); the 挥金如土 title stat). const STAT_ROWS = [ ['dmgDealt', '造成伤害'], ['kills', '击倒敌人'], ['bossDamage', '领袖伤害'], ['activatedLayers', '盟约层数'], ['merges', '晋升次数'], ['itemsEquipped', '配发装备'], ['gold', '消耗资金'], ['perfectRounds', '完美作战'], diff --git a/public/js/ui/hud.js b/public/js/ui/hud.js index 1e43eb69..b522d5bd 100644 --- a/public/js/ui/hud.js +++ b/public/js/ui/hud.js @@ -102,7 +102,7 @@ export function MissTag({ n, name = null }) { const LEAK_PHASES = new Set([PHASE.COMBAT, PHASE.UNITE]); /** - * LP a round's leaks cost at settlement — the server rule (server/match/Match.js settle): min(lpCapPerRound, counted + * LP a round's leaks cost at settlement — the server rule (server/match/match/settle.js): min(lpCapPerRound, counted * leaks), never negative. * @param {number} leaks counted leaks so far * @param {number} [cap] data/config.json lpCapPerRound diff --git a/public/js/ui/teamPanel.js b/public/js/ui/teamPanel.js index 1f44d10a..815bbbca 100644 --- a/public/js/ui/teamPanel.js +++ b/public/js/ui/teamPanel.js @@ -7,7 +7,7 @@ // watches that player's field at once. // Live LP (user playtest #3 item 2): during a normal round's battle each row's tower shows lp − the loss that player's // leaks so far will cost (red, −N): the own row the top bar's live value (`self`, ui/hud.js liveLp), a teammate's row -// m.public players[].pendingLp (server/match/Match.js, ~1 Hz). 联防 (user playtest #6 item 7): a leaker's row adds the +// m.public players[].pendingLp (server/match/match/views.js, ~1 Hz). 联防 (user playtest #6 item 7): a leaker's row adds the // runner tag ×N — its enemies still standing on the 联防 field, uncapped and live (falling as the helpers kill them, // rising when one splits) — next to lp − min(lpCapPerRound, N): the own row the top bar's value, a teammate's row the // local 联防 replica's count while it is on screen (`uniteLocal`: the battle runner's state().uniteLeft, the same battle diff --git a/public/js/ui/underframe.js b/public/js/ui/underframe.js index 95258c0b..9382a035 100644 --- a/public/js/ui/underframe.js +++ b/public/js/ui/underframe.js @@ -135,7 +135,7 @@ export function tempRowFrame(a, b, { labelW = 0, gap = 8, vw = Infinity } = {}) } /** - * What the temp row's label says will happen to its pieces (server/match/PlayerState.js tempDue): a piece is resolved + * What the temp row's label says will happen to its pieces (server/match/player/basics.js tempDue): a piece is resolved * at the end of the first prep in which the player can act on it. Not ready (or outside PREP): at the end of this / the * coming prep, and 准备就绪 waits for the row to be cleared. Ready in PREP: Ready is refused while the row holds pieces, * so whatever lies there arrived after it — kept through the NEXT prep (cancelling Ready makes it due at this one). diff --git a/shared/constants.js b/shared/constants.js index 8ae63403..deb1abaf 100644 --- a/shared/constants.js +++ b/shared/constants.js @@ -10,7 +10,7 @@ export const MAX_SEATS = 4; * Spectator seats of a co-op room (community report #26, owner's decision 2026-10-04) — a remake feature: the official * room has 1–4 players and no spectator seat (there only eliminated players and 联防 bystanders watch, research 09 §3.1). * A spectator never counts as a player, may not act, and watches like an eliminated player (server/lobby.js spectate, - * server/match/Match.js addSpectator). + * server/match/match/watch.js addSpectator). */ export const MAX_SPECTATORS = 2; export const ROOM_CODE_LEN = 4; @@ -79,8 +79,8 @@ export const SKILL_SUMMON_START_DEPLOY = true; * `Torappu.Battle.AutoChessBattleConst.MAX_GARRISON_STACK = 999`, and its bond counter (`AddBondCount`) stores * `min(L + n, 999)` — each bond stops at 999 on its own; the community reports bonds sitting at 999 while fed (巴哈姆特 * 12534 "每把都能999层", 12316 "999謝"; research 02 §layers). The only implementation of the cap (DESIGN §20.12). Every - * writer of a bond's layers goes through `layerGainRoom`: the prep-side gains (server/match/PlayerState.js addLayers — - * 特质, items, bands, 机变 cards, bonds), the settle of the in-battle gains (server/match/Match.js) and the live in-battle + * writer of a bond's layers goes through `layerGainRoom`: the prep-side gains (server/match/player/economy.js addLayers — + * 特质, items, bands, 机变 cards, bonds), the settle of the in-battle gains (server/match/match/settle.js) and the live in-battle * copy (server/sim/battle/economy.js addLayers, as the client's AddBondCount) — and the dev tools' direct writes (tools/matchrun.mjs * --layers, tools/balance.mjs applyBoard); a gain at the cap adds 0 (no onLayers, no 'layer' * event), and the client-result check (server/match/fields.js) bounds a reported gain by the room left. Milestones paid diff --git a/test/match/playtest6-bosspool.test.js b/test/match/playtest6-bosspool.test.js index dd5e6d60..4dafc88c 100644 --- a/test/match/playtest6-bosspool.test.js +++ b/test/match/playtest6-bosspool.test.js @@ -9,7 +9,7 @@ // the verdict then came from the pool after the final b.result, whose per-player damage is rounded: rounding up // emptied the dust → 胜利 (what the user saw), rounding down → 失败. // -// Now (sim/constants.js BOSS_POOL_MIN_HP, finalAssault.js SharedBossPool, sim/spec.js LocalBossPool, Match.js): a pool +// Now (sim/constants.js BOSS_POOL_MIN_HP, finalAssault.js SharedBossPool, sim/spec.js LocalBossPool, match/reports.js): a pool // never holds less than 1 HP (the hit that would leave less takes the rest; a browser pool reading below 1 is 0), so // the leader dies on every field as soon as the pool shows 0 and the fight ends; the first end condition the server // registers decides the verdict (pool 0 → victory, team LP 0 → defeat: PRTS 卫戍协议:盟约 下半 "…使目标生命值扣除至0, diff --git a/test/match/playtest6-elite-to-board.test.js b/test/match/playtest6-elite-to-board.test.js index 0f5b1daa..232eed47 100644 --- a/test/match/playtest6-elite-to-board.test.js +++ b/test/match/playtest6-elite-to-board.test.js @@ -1,7 +1,7 @@ // User playtest #6 follow-up — a merge's elite goes to the board when it consumed a deployed copy. The user (first-hand, // official game): "官方就是合成精锐时,如果消耗了场上的干员,精锐会出现在场上那个位置". PRTS 卫戍协议/帮助 §干员的获得与精锐化: // "不获得第3名干员,销毁已有的2名初始干员,发送1名【精锐】状态的该干员至手牌区(若消耗已部署至作战区的干员,则发送至作战区 -// 对应位置)". Remake (server/match/PlayerState.js _mergeChess, board.js mergeTile): the elite takes the tile and facing of the +// 对应位置)". Remake (server/match/player/acquire.js _mergeChess, board.js mergeTile): the elite takes the tile and facing of the // consumed copy that deploys first (row desc, col asc — [ASSUMED] when several stood on the board); a 突变细胞 carrier is // destroyed before its gain, so its tile never counts; with no deployed copy it goes to the hand (overflow temp). // Equipment returns to the hand ("干员晋级后已配发装备会回收至整备区"), the copies' summons are removed and the elite on diff --git a/test/match/unitStats.test.js b/test/match/unitStats.test.js index f5a3d159..6401c69a 100644 --- a/test/match/unitStats.test.js +++ b/test/match/unitStats.test.js @@ -1,7 +1,7 @@ // Live operator stats (user playtest #4 item 7 — the detail card showed the fixed record numbers): g.unitStats answers // m.unitStats { seq, round, units } with the stats every unit of the player's board starts its next battle with — the // real battle input (equipment, bonds / layers, 特质, band and 机变 effects, the onBattleStart meta) built into a Battle -// that is started and read, never stepped (server/match/Match.js unitStats); the shape is shared/protocol.js +// that is started and read, never stepped (server/match/match/intents.js unitStats); the shape is shared/protocol.js // unitStatsEntry (also the browser runner's live battle stats, test/match/runner.test.js). The preview changes nothing // of the match. import { test } from 'node:test'; diff --git a/test/ui/bandDraft.test.js b/test/ui/bandDraft.test.js index 05d7c380..6a0c5889 100644 --- a/test/ui/bandDraft.test.js +++ b/test/ui/bandDraft.test.js @@ -1,8 +1,8 @@ // Strategy draft 队友已选 (research 09 §5 / §7, DESIGN §14 corrections): a strategy a teammate already picked cannot be -// chosen again — the server refuses it (server/match/Match.js pickBand → BAD_TARGET '队友已选'), bots re-draw, and the +// chosen again — the server refuses it (server/match/match/phases.js pickBand → BAD_TARGET '队友已选'), bots re-draw, and the // UI marks it (screens/bandDraft.js teammateBands). Automatic assignments (a turn that runs out, a departing seat) give -// the official default 「华法琳」 only while no teammate holds it, else the first free strategy (Match.js defaultBand; the -// UI names it: bandDraft.js timeoutBand) — a timed-out turn takes the highlighted band first (g.bandFocus, Match.js +// the official default 「华法琳」 only while no teammate holds it, else the first free strategy (match/phases.js defaultBand; the +// UI names it: bandDraft.js timeoutBand) — a timed-out turn takes the highlighted band first (g.bandFocus, match/phases.js // timeoutBand; user playtest #4 item 4: one countdown, BAND_TURN_SECONDS per turn, no separate step cap). import { test, describe } from 'node:test'; diff --git a/test/ui/bosshit-ticker.test.js b/test/ui/bosshit-ticker.test.js index 8c90492a..5746d370 100644 --- a/test/ui/bosshit-ticker.test.js +++ b/test/ui/bosshit-ticker.test.js @@ -1,5 +1,5 @@ // Player report after 0.1.0, client part: "隐藏boss还没打就出了造成50%伤害播报". Besides the server's share (fixed in -// server/match/Match.js, test/match/bosshit-ticker.test.js) the ticker strip plays its queue in order, TICKER_MS (5.2 s) +// server/match/match/bossRounds.js, test/match/bosshit-ticker.test.js) the ticker strip plays its queue in order, TICKER_MS (5.2 s) // per line and up to QUEUE_MAX (4) lines behind: in a headless-Chrome run the Final Assault's "超过20%" / "超过80%" // lines played 2–8 s into the Hidden Core with its leader at 96 % / 53 %. A leader-damage line (BOSS_HIT) now plays // only during the round it came in, and a player's newer BOSS_HIT line of the round supersedes their older one, queued diff --git a/test/ui/playtest3.test.js b/test/ui/playtest3.test.js index 106d98a7..83e71346 100644 --- a/test/ui/playtest3.test.js +++ b/test/ui/playtest3.test.js @@ -2,7 +2,7 @@ // 2 the own LP drops live while the own battle's enemies enter the blue gate — pendingLoss / liveLp (the settle // rule min(lpCapPerRound, counted leaks), reset when the settled m.private lands, 联防中 during 联防), the team // panel rows (rowLp: own live value, teammates' m.public players[].pendingLp), the LP tower's −N tick, and the -// server's pendingLp (server/match/Match.js _pendingLpView) +// server's pendingLp (server/match/match/views.js _pendingLpView) // 3 the temp overflow row (临时整备区): the ready button's visible reason, the row's frame geometry (tempRowFrame) // 8 the operator's own effect (特质 / garrison) right under the detail card's header (CHESS_SECTIONS, rendered order) // 9 no spinning busy indicators next to texts: no wait / progress cursors, the button's busy bar instead of a