19 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 and noMath.randomin the sim, so the same spec and data give the same battle on every machine, bit for bit. The golden results (§5) rely on it.
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 (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/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 and i18nData.js, 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 (干员调配), 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/en.json |
the styles; the English UI strings |
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/en.json the English game texts, data/assets.json the art manifest |
tools/ |
build-data.mjs, build-i18n.mjs, i18n.mjs (UI strings), 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 (the release zip), 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. 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 inpublic/i18n/en.json(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 |
| 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).