The owner's decisions of 2026-10-08: 「D12 [PR #301] 可以做,具体布局怎么优化你来决定,他那样确实方便,但是三个竖排有点乱影响 观感」 and 「D7D8调配干员里自己设置吧,默认满潜满加成」. The quick choices are PR #301's idea (by @farridge); the layout is new: one list instead of three columns of cards. - screens/loadout.js: RosterHead + RosterRow — every row the same four columns under a sticky head (干员 | 技能 | 模组 精锐 | 潜能 · 练度): portrait / name / bonds (selects, opens the detail), three skill slots (an empty one for a two-skill chess; the label says slot, name, SP, duration — quickSkillTags), the elite's modules (不装备 first), the 潜能 / 练度 selects; a quick choice goes through setChoice / setEntries like the detail and keeps a phone on the list. The detail's 普通 / 精锐 skill toggle is one preview for the skills, the 特质 and the rows. The detail opens with the 潜能与练度 section; 恢复默认 / 全部恢复默认 reset the settings too (全部: the roster's operators — a 自选 pick's stay with the 自选编队 tab); 导入 reports a settings-only import; ←/→ stay inside a focused row's choices. - screens/cultivation.js (new): CultivationSelects (two compact selects; quiet at the defaults, mint when changed) and CultivationSection (潜能 1–6 and the four official tiers — effects.json names and effect texts — the rule in one line, the stand-in note for a chess marked not owned). - screens/diy.js: an owned 自选 pick's slot card carries its operator's selects; a prototype says it has neither. - css: the head and the rows share one grid (`--cols`), 32 px quick targets (40 px on a coarse pointer); the detail slides over the list below 1000 px (was 760 px); below 700 px the row's bonds give way to the name. Headless check at 1920 × 1080, 1366 × 768, 844 × 390 and 667 × 375: no sideways overflow, the head's columns equal every row's. - The 局内数值 caption says 含潜能与练度 when the numbers carry both (effects.json loaded); the overlay's note says the skills, modules, 潜能 and 练度 can be set (the operator's in-match level cannot). - i18n: 21 strings in the four packs (the tier names, short tier labels, the notes, the reset confirm, the import toast). - Docs: design/match.md (the list, the 潜能与练度 section, 导出's `ops`), ARCHITECTURE.md (screens/cultivation.js). - Tests: test/ui/loadout-rows.test.js (new, 7): a row's pick / skill slots / modules / aria-pressed / callbacks and the shared 精锐 preview, its selects (values, options, a change, the changed mark, a stand-in), the head, the detail's section (tiers from effects.json, the effect text, the stand-in note), a 自选 owned pick vs a prototype, 局内数值 at 潜能1 / 未精英化 and at the defaults with its caption, the css grid and breakpoints. e2e (not run here; their steps replayed with a headless page): the row pick selector, the phone test (one list, 40 px targets, the detail slides over, back to the list), the 局内数值 expectations ×1.1, a new 潜能 / 练度 flow (row select → stored, 部署费用 17 / 攻击 454; the detail's 未精英化 → 413; the row follows; 恢复默认).
20 KiB
ARCHITECTURE.md — a map of the code
For a newcomer who wants to find their way before changing something. This file only maps the code. The contracts are
in the design document — DESIGN.md is its index, the current rules are in docs/design/ (§5 the battle
engine: design/engine.md; §6 the match: design/match.md; §8 the protocol and §14
client-side combat: design/network.md; §9 rendering: design/client.md), the
per-release revisions and their evidence in docs/history/;
the details in SIM.md (battle engine, hooks, SkillSpec), META.md (match engine, prep-phase
effects), DATA.md (generated data), ASSETS.md (art and audio) and I18N.md
(languages). How to set up, test and send a change: CONTRIBUTING.md.
1. What runs where
browser (public/) one Node process (server/index.js, Node ≥ 22)
┌──────────────────────────────────────┐ ┌─────────────────────────────────────────────────────┐
│ screens/, ui/ Preact + htm │─ HTTP ▶│ server/http/ static files: / /data/ /shared/ │
│ render/ PixiJS, pixi-spine, │ │ /sim/ /media/, GET /healthz │
│ three.js │ │ │
│ battle/runner.js runs the sim │◀ /sim/ │ server/sim/ the deterministic battle sim │
│ net.js the socket │◀ /ws ▶ │ server/net.js → lobby.js → match/Match.js │
│ │ │ (rooms) (rounds, economy, views) │
└──────────────────────────────────────┘ └─────────────────────────────────────────────────────┘
- The server is one Node process: plain
node:httpplusws, no framework (server/index.jswiresserver/http/). It serves the static client, the generated data and the sim's source files, and runs every room and match in memory — rounds, shop, economy, bots, validation. Nothing is written to disk. - The browser loads native ES modules with no bundler and no build step; the libraries are vendored into
public/vendor/bytools/vendor.mjsonnpm install. - The battle simulation (
server/sim/) is pure ESM without any Node API, served read-only at/sim/. Both sides run it from the same JSON battle description (server/sim/spec.js, the BattleSpec): each player's browser simulates its own battle (public/js/battle/runner.js) and reports progress and the result; the server checks the result against the spec (server/match/fields.jsvalidateClientResult), runs the battles no connected human owns (bots, a dropped player, an authority that missed its deadline) and can re-simulate results (SP_VERIFY).SP_COMBAT=serveris the older mode in which the server runs every battle and streams snapshots. DESIGN §14. - Determinism is what makes this work: fixed 1/30 s ticks, a seeded PRNG (
server/sim/rng.js), no wall clock, noMath.randomand only correctly rounded arithmetic in the sim, so the same spec and data give the same battle on every machine, bit for bit. ECMA-262 leavesMath.hypot,sin,cos,atan2,pow(and**) implementation- approximated and the engines differ in their last bits (V8, SpiderMonkey and JavaScriptCore each computehypotdifferently; Chrome's and Node'ssindiffer), so the sim takes them fromserver/sim/detmath.js, built only from- − × ÷ and
Math.sqrt; ESLint andtest/sim/detmath.test.jsrefuse the Math ones inserver/sim/. The golden results (§5) rely on it.
- − × ÷ and
2. The WebSocket protocol
shared/protocol.js is normative: C2S holds one validator per client message and S2C lists the server messages.
Every frame is JSON text, { t, rid?, …fields }.
| direction | messages | handled in |
|---|---|---|
| client → server | hello (name, reconnect token) → welcome; ping → pong |
server/net.js |
room.*: create, join, ready, difficulty, AI seats, kick, start, the 干员调配 loadout, 干员持有 ownership, 自选编队 picks, spectating |
server/lobby.js |
|
g.*: match intents — buy, refresh, freeze, level up, sell, move, equip, Arts, rewards, 机变 choices, ready, emotes, watching, pause … |
server/match/match/intents.js → server/match/player/ |
|
b.progress, b.result: the battle reports of the authoritative browser |
server/match/match/reports.js |
|
| server → client | room.state, room.closed |
server/lobby.js |
m.public (what every player sees), m.private (one player's shop, hand, funds …), m.field, m.toast, m.ticker, m.emote, m.unitStats, m.result |
server/match/match/views.js, server/match/player/views.js |
|
b.start (a BattleSpec), b.pool (the shared leader HP), b.end; b.snap / b.ev only in the server-run mode |
server/match/match/clientCombat.js |
- A request that carries
ridis answered withokorerrorechoing it.server/net.jsrate-limits each socket, validates every message againstC2Sand refuses anything unknown; the handlers never trust the client. - Messages meant for people (
m.toast,m.ticker) carry a message id and its parameters, so each client shows them in its own language (shared/i18n.jswireMessage). - The exact view shapes: DESIGN §8.3 and META §5.
3. The directory layout
0.2.0 split the big files without changing behaviour. Three classes keep their constructor in one file and their
methods in a folder, installed on the prototype in a fixed order (a method name defined twice throws):
server/sim/Battle.js with server/sim/battle/, server/match/Match.js with server/match/match/, and
server/match/PlayerState.js with server/match/player/. Two client modules became barrels that re-export a folder,
so old imports keep working: public/js/ui/gameLogic.js (public/js/ui/gameLogic/) and public/js/render/fx.js
(public/js/render/fx/). DESIGN §2 lists every file.
Server and shared code
| path | what |
|---|---|
server/index.js |
the process entry (npm start); startServer() wires server/http/ |
server/http/ |
config.js (environment), websocket.js (sessions, /ws), static.js (the mounts), media.js, files.js (MIME, gzip, ETag, ranges), buildTag.js, routes.js (/healthz), common.js, boot.js (a pending update package first, banner, shutdown) |
server/net.js |
sessions and reconnect tokens, rate limits, message validation |
server/lobby.js |
rooms, seats, AI seats, spectators; starts a Match |
server/data.js |
loads data/*.json once (frozen) |
server/packs.js |
the content packs (PACKS.md): finds the language packs of public/i18n/ and the pack folders of packs/, validates them, answers /packs/index.json and which pack files may be served; re-reads the folders when they change |
server/update.js |
the update package on the player's machine (DEPLOY.md §1.5): before the server starts, an extracted UPDATE.json is finished — the install verified against MANIFEST.json, the files the new version dropped deleted, or the start refused when the install is another version; doctor's MANIFEST.json check |
server/match/Match.js |
one match: the phase machine, timers, the round loop, co-op, the views; its methods are in server/match/match/ (phases.js, prep.js, combat.js, clientCombat.js, reports.js, unitePhase.js, bossRounds.js, settle.js, views.js, intents.js …) |
server/match/PlayerState.js |
one player's shop, hand, board, items, bonds and LP, and every prep intent; its methods are in server/match/player/ (economy.js, acquire.js, placement.js, items.js, pieces.js, prep.js, round.js, diy.js, views.js …) |
server/match/ (the rest) |
pool.js (the shared chess pool), board.js (placement), bondsMeta.js, effectsMeta.js and builtinMeta.js (prep-phase effects), choices.js (机变), waves.js, unite.js (联防), finalAssault.js, results.js, bot.js (AI players and AI 托管), fields.js (the battles of a combat phase), scheduler.js, gamedata.js |
server/sim/Battle.js |
one battle field; its methods are in server/sim/battle/ (lifecycle.js, spawns.js, deploy.js, blocking.js, combat.js, status.js, summons.js, displacement.js, events.js …) |
server/sim/ (the rest) |
skills.js, ai.js, damage.js, buffs.js, targeting.js, grid.js (pathing), units.js, professions.js, snapshot.js, spec.js (the BattleSpec), simdata.js (data records → engine defs), nodeData.js (the Node-only data loader, never served) |
server/sim/content/ |
everything game-specific, installed into a battle by server/sim/content/index.js |
server/sim/content/kits/ |
the operator kits: ops/ holds one file per chess (<chessId>-<codename>.js, 129), per 补位 stand-in (standin-<codename>.js, 9) and per 自选 operator (op-<codename>.js, 71); shared/ the helpers several kits use; index.js the registry; README.md the guide |
server/sim/content/enemies/ |
the enemy kits by special type (invisible.js, times.js, element.js, dot.js, reflection.js, fly.js, special.js) and leaders.js; server/sim/content/enemies.js dispatches them, server/sim/content/bosses.js scripts the leaders |
server/sim/content/garrisons/, items/, bands/ |
特质, equipment and strategies: battle.js is the battle side, meta.js the prep side (registerMeta, META §2) |
server/sim/content/bonds/ |
the 23 bonds: core.js the 8 core bonds (both sides), server/sim/content/bonds/addon/ the 15 add-on bonds (battle.js, meta.js) |
server/sim/content/ (the rest) |
tokens.js (summons), devices.js (terrain and stage devices), generic.js (the kit built from a skill's data when a chess has none), choices.js (机变 cards in battle) |
shared/ |
imported by the server and the browser: protocol.js, constants.js, i18n.js, i18nData.js and i18nPacks.js (languages), packs.js (the content-pack format), standIn.js (补位), diy.js (自选编队), highGround.js, loadoutRecord.js |
Client (public/)
| path | what |
|---|---|
public/index.html, public/js/main.js |
the page and its entry: boot, the router (title → lobby → room → game) |
public/js/net.js, public/js/store.js, public/js/data.js |
the socket client, the observable store, the data loader (/data/*.json, with the English overlay) |
public/js/battle/ |
runner.js (the local battle: loads /sim/, steps it, reports), observe.js (who may watch which field) |
public/js/screens/ |
title.js, lobby.js, room.js, loadout.js (干员调配), cultivation.js (its 潜能 / 练度 controls), ownership.js (干员持有), diy.js (自选编队), briefing.js, bandDraft.js, game.js with public/js/screens/game/, result.js |
public/js/ui/ |
the HUD components (hud.js, shopBar.js, detailPanel.js, bondStrip.js, teamPanel.js …); public/js/ui/gameLogic/ the pure in-match logic, unit-tested in Node |
public/js/render/ |
the field view: app.js with public/js/render/app/, units.js and spine.js (models), tiles.js, projection.js, interp.js, pick.js, drag.js, public/js/render/fx/ (effects; kinds.js maps the fx kinds), public/js/render/board3d/ (the official 3D board) |
public/css/, public/i18n/<code>.json |
the styles; the UI strings of each language pack (English ships) |
Data, tools, tests
| path | what |
|---|---|
data/*.json |
generated by tools/build-data.mjs and committed — never edited by hand (DATA.md); data/backups.json holds the 补位 and 自选 data, data/i18n/<code>.json the game texts of a language pack (English), data/assets.json the art manifest |
packs/ |
content packs installed as folders, packs/<id>/pack.json + files (PACKS.md); empty in the repository but for its readme |
tools/ |
build-data.mjs, build-i18n.mjs, i18n.mjs (UI strings, language packs), packs.mjs (content packs), setup.mjs / fetch-assets.mjs / tools/assets/ (art and audio), tools/local-extract/ (art from a local game client), vendor.mjs, golden.mjs, check-imports.mjs, package.mjs / package-update.mjs (the release zips, the update package), doctor.mjs; sweeps: matchrun.mjs, simrun.mjs, botbench.mjs, balance.mjs |
test/ |
node:test suites by area: test/content/ (kits, enemies, bonds, items), test/sim/ (the engine), test/match/ (match engine, bots), test/ui/, test/render/, test/golden/ (the stored digests), test/helpers/ (battleHarness.js; designDocs.js, the design document in § order for the doc tests), test/e2e/ (browser and bot runs) |
types/ |
JSDoc typedefs of the type-checked slice (types/README.md) |
docs/ |
the documents; docs/DESIGN.md the index of the design document, docs/design/ its current rules, docs/history/ its per-release revisions; docs/research/ the research on the official mode |
4. Data flow
official zh_CN tables (.cache/gamedata) ─┐
docs/research/*.json ────────────────────┴─▶ tools/build-data.mjs ─▶ data/*.json
official EN tables ────────────────────────▶ tools/build-i18n.mjs ─▶ data/i18n/en.json
public mirrors, a local client ────────────▶ tools/setup.mjs ──────▶ public/assets/ + data/assets.json
data/*.json ──────────┬─▶ server/data.js ─────────────▶ the match, the server's battles
├─▶ public/js/data.js ──────────▶ the UI
└─▶ public/js/battle/runner.js ─▶ the browser's battles (setSimData)
data/i18n/en.json ──────▶ public/js/data.js ──────────▶ the English game texts
public/assets/ ─────────▶ public/js/render/, public/js/audio.js (URLs listed in data/assets.json)
- Game data.
tools/build-data.mjsdownloads the official zh_CN tables into.cache/gamedata/(or reads them there with--offline), joins them with the research JSON and writesdata/*.jsondeterministically; DATA.md documents every field. A data fix goes into the builder: rebuild withnode tools/build-data.mjs --offlineand compare the JSON to see that only the intended data changed. - On the server,
server/data.jsloads the files once; the match reads them throughserver/match/gamedata.js, the sim throughserver/sim/simdata.js, which turns every record into a frozen engine def. - In the browser,
public/js/data.jsfetches the same files once per page for the UI, and the battle runner loads its own frozen copies for the sim (loadBrowserSim→setSimData), so both sides simulate from the same data. - Languages. Chinese is the source language; every other language is a pack —
public/i18n/<code>.json(UI strings) and optionallydata/i18n/<code>.json(game texts), listed by the server at/packs/index.json(server/packs.js; PACKS.md). The English game texts come from the official EN client (tools/build-i18n.mjs→data/i18n/en.json, applied bypublic/js/data.js); UI strings are wrapped int('…')and translated in the packs (tools/i18n.mjslists what is missing); server messages travel as message ids. I18N.md. - Art and audio are never committed.
npm run setupdownloads them from public mirrors (tools/fetch-assets.mjs, planned bytools/assets/plan.mjs) intopublic/assets/and writes the manifestdata/assets.json;tools/local-extract/can add art from a local game client. The client only requests URLs listed in the manifest. ASSETS.md.
5. Safety nets
- Golden results (
tools/golden.mjs,test/golden/,test/golden.test.js): a fixed corpus of seeded scenarios — every chess with every skill and module, every bond, every leader and 联防 field, whole bot matches, every 补位 stand-in and every 自选 operator — reduced to digests.npm testchecks a fast subset;npm run goldenorGOLDEN_FULL=1 node --test test/golden.test.jschecks everything. A refactor must never change them; a gameplay change runsnpm run golden:updateand commits the new digests with the change, naming the scenarios that moved and why (test/golden/README.md). - Import boundaries (
tools/check-imports.mjs,npm run check:imports):server/sim/is served to browsers, so it may not import the match, the lobby, the server entry, the net layer (ws,node:http…),public/or any Node builtin;server/match/may not importpublic/or the net layer. The one listed exception isserver/sim/nodeData.js, the Node-only data loader the browser never loads, andtest/check-imports.test.jspins that list. The kit layout test (test/content/kits_layout.test.js) also refuses Node-only imports, clocks andMath.randomin kits. - Lint and types.
npm run lintis a correctness-only ESLint (eslint.config.js: warnings are allowed, errors are not);npm run typecheckchecks a JSDoc slice (jsconfig.json;types/README.mdsays how to widen it). CI runs both once, and the tests on Ubuntu and Windows with Node 22 and 24 (.github/workflows/ci.yml). - Tests.
node --testruns every suite that needs no browser; the browser suites are opt-in (CONTRIBUTING.md §2). The battle harnesstest/helpers/battleHarness.js(SIM.md §10) sets up a battle in a few lines.
6. Where to start
| to change … | start at | then |
|---|---|---|
| an operator's skill, talent or module | its kit in server/sim/content/kits/ops/ (search for the chess id or the Chinese name) |
the fidelity checklist of server/sim/content/kits/README.md, a test in test/content/, golden |
| add a 自选 operator | server/sim/content/kits/README.md, "How to add an operator (自选)" |
op-<codename>.js, OPERATOR_KIT_FILES, test/content/op_<codename>.test.js |
| a rule of the battle engine (blocking, damage, statuses, skills) | server/sim/battle/, server/sim/damage.js, server/sim/skills.js, server/sim/targeting.js, server/sim/ai.js |
SIM.md, DESIGN §5 |
| an enemy or a leader | server/sim/content/enemies/, server/sim/content/bosses.js |
test/content/enemies_bosses.test.js |
| a bond, 特质, item or strategy | server/sim/content/bonds/, server/sim/content/garrisons/, server/sim/content/items/, server/sim/content/bands/ (the battle side and the prep side apart) |
META.md §2 |
| the shop, economy, hand or placement | server/match/player/, server/match/pool.js, server/match/board.js |
test/match/ |
| phases, timers, 联防, the Final Assault | server/match/match/phases.js, server/match/match/unitePhase.js, server/match/match/bossRounds.js, server/match/unite.js, server/match/finalAssault.js |
META.md §1, §4 |
| the bots | server/match/bot.js |
tools/botbench.mjs |
| a new client message | shared/protocol.js (C2S), then server/match/match/intents.js (or server/lobby.js for room.*) |
DESIGN §8 |
| data from the official tables | tools/build-data.mjs |
rebuild --offline, JSON compare, DATA.md, golden |
| a screen or a HUD panel | public/js/screens/, public/js/ui/; pure logic in public/js/ui/gameLogic/ |
test/ui/ |
| battle visuals | public/js/render/fx/kinds.js, public/js/render/units.js, public/js/render/spine.js |
test/render/ |
| a UI text | t('…') in the code, the English in public/i18n/en.json |
I18N.md |
| a language | public/i18n/<code>.json (node tools/i18n.mjs template <code>), no code |
I18N.md "Adding a language", PACKS.md |
| HTTP, headers, static routes | server/http/ |
test/version.test.js, test/client-static.test.js |
Before changing a rule, read the DESIGN section that owns it (module headers cite their sections; the table in
DESIGN.md names the file that holds each) and the official source; a rule no source settles is marked [ASSUMED]
(CONTRIBUTING.md §3).