15 KiB
DESIGN §0, §1, §2, §11, §12 — Scope, stack, repository layout, quality bar
Part of DESIGN.md (the index; section numbers are global).
0. Product scope (v1)
A faithful, polished, online co-op browser remake of Arknights' seasonal auto-chess TD mode 「卫戍协议:盟约」(下半, act2autochess).
In scope:
- Title → lobby (nickname, create room / join by 4-letter code or
?room=CODElink, host picks difficulty, up to 4 players, AI teammates can fill seats, ready/start). Solo (独立模拟) and co-op (同盟模拟, 1–4 humans + optional AI). - Difficulties: 标准 FUNNY, 险境 NORMAL, 绝境 HARD, 终极 ABYSS (solo and multi variants from
modeDataDict). Training/tutorial mode is out of scope. - Pre-game: INFO_CHECK briefing (disabled bonds, banned operators, enemy factions, stage) → BAND_CHECK strategy draft (40 bands, random order, 1 skip in co-op) → BATTLE_CHECK.
- Rounds 1–14 (+ hidden 15): prep (shop, hand, board, items, freeze, refresh, level-up, sell, merge + reward pick, ready), 机变 draft rounds, simultaneous auto-combat on each player's own field, 联防 unite phase, LP loss/elimination, Final Assault boss round with merged LP and shared boss HP pool, Hidden Core, settlement with titles.
- Full content: 112 visible chess (+ elites), their default skills and talents, 23 bonds with layers, all 43 特质 (garrison) effect keys, 56 equipment + Arts, 40 bands, 机变 cards, enemy factions/special enemies, 10 bosses, stage devices/terrain (crates, blowers, mire, smog, deep sea, infection).
- Rendering with the real Spine battle chibis (PixiJS 7 + pixi-spine 4), procedural tiles, VFX, damage numbers, real BGM/SFX, emotes, broadcast ticker.
- Reconnect, AI take-over of disconnected players, robust validation of every client intent.
New features require explicit maintainer approval before implementation. The 2026-10-10 review approved retaining the existing PWA installation, unanimous opening reroll, teammate emotes and cooperative purchasing heuristic (DESIGN §28.15, §28.16, §28.19, §28.21); this is not blanket approval for future additions.
Out of scope v1: matchmaking queue, training/tutorial, DIY (甄选) slots (the 4 DIY chess are removed from the shared pool — 0.2.0 plays them as 自选编队: the picks of the 干员调配 overlay's 自选编队 tab, each sold only in its player's shop; DATA.md §18, shared/diy.js, server/match/player/diy.js), trophies/progression persistence, reporting.
1. Tech stack
- Node.js ≥ 22 (CI: 22 and 24), ESM (
"type": "module"), single dependencyws@8. No bundler, no TypeScript. JSDoc types where helpful. - Server-authoritative simulation. Clients send intents; the server validates, mutates state and pushes state/snapshots.
- Client: static files, native ES modules. Vendored libs in
public/vendor/:pixi.min.js(PixiJS 7.4.2 UMD, globalPIXI),pixi-spine.js(4.0.6 UMD,PIXI.spine),preact.module.js+hooks.module.js+htm.module.js(Preact 10 + htm, no build step). No CDN at runtime (LAN play must work offline). - Shared code in
shared/is imported by both server and browser (pure ESM, no Node APIs). - Tests:
node --test(test/**/*.test.js). Browser E2E:puppeteer-coredriving the system Chrome (dev dependency, optional; puppeteer-core 25 is ESM-only and needs Node ≥ 22.12, so the suites load it withawait import).
Run: npm install && npm run assets && npm start → http://localhost:3000. Friends on LAN use http://<host-ip>:3000. Internet play: a tunnel (e.g. cloudflared tunnel --url http://localhost:3000) or any Node host (Dockerfile provided).
2. Repository layout & ownership
server/
index.js process entry: startServer() wires server/http/ (plain node:http + ws); boots when run directly
http/
config.js ROOT, served dirs, env (PORT, HOST, TRUST_PROXY, DEBUG), options handed to net.js / lobby.js
websocket.js session wiring (SessionRegistry → Lobby → Network), WebSocket upgrade at /ws (64 KB frames,
permessage-deflate from 1 KB)
static.js mounts / → public, /data/, /shared/, /sim/ (.js only), the /data.js browser stand-in, path guards,
the ~<hash> segment of /assets/ /fonts/ /media/ URLs stripped (immutable when it names the file)
media.js /media/… extension-less audio → public/assets/audio
files.js one file → response: MIME, brotli q5 / gzip (.skel/.atlas/.json/.js/.css …), ETag (content hash when known) / 304,
cache policy, ranges; serveBuffer for the served manifests
buildTag.js build tag of the served browser runtime (/healthz build, public/js/ui/buildGuard.js)
routes.js request listener: 414 / 400 / 405 guards, GET /healthz (?traffic=1: the counters), then static files
traffic.js traffic counters: HTTP body bytes by category (assets / audio / code / data), WebSocket wire / payload
assetIndex.js content hashes (SHA-256, 16 hex) of public/assets + public/fonts, background build, .cache/asset-hashes.json
assetViews.js the served /data/assets.json + /data/local-assets.json: art URLs as /assets/~<hash>/… (immutable when it matches)
common.js security headers, URL split, error page, JSON replies, bare 400 for unparseable requests
boot.js boot banner (Local / LAN / tunnel URLs), port-in-use hint, graceful SIGINT / SIGTERM
net.js session registry, send helpers, per-connection rate limit, message validation (uses shared/protocol.js)
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 — 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
effectsMeta.js registry + dispatcher of prep-phase ("SERVER_*") effects: garrisons, bands, items, bonds
choices.js 机变 draft: card generation per family, pick order, timeouts, application
waves.js match enemy factions, per-round spawn list generation, stat scaling, bounties
unite.js 联防 helper selection, leaked-enemy aggregation, LP attribution
finalAssault.js R14/R15 pairing, merged LP, shared boss HP pool, overtime drain
results.js settlement stats, titles (评语)
bot.js AI player (plays prep phases; also auto-play for disconnected humans)
sim/
Battle.js one field simulation (normal / unite / boss); public API §5 — the class: constructor + method install
battle/ Battle's methods by concern (players, lifecycle, hooks, spawns, deploy, blocking, status, combat,
queries, summons, tiles, displacement, economy, events), installed on Battle.prototype
constants.js TICK, conversions, tuning knobs
rng.js seeded PRNG (mulberry32) + helpers
grid.js field grid, tile queries, passability, official 4-direction SPFA flow field + line-of-sight smoothing, road-over-floor preference only where it crosses fewer floor tiles (§21.15); DEPLOY_REFUSED_TILES (深水区, §21.3)
units.js Unit / Operator / Enemy / Token classes, stat aggregation
buffs.js Buff model, status catalogue (stun, freeze, cold, sleep, slow, fragile, stealth, …)
damage.js damage & heal pipeline, element gauges
targeting.js target selection helpers, range tests, priorities
body.js hit areas of huge enemies: the one helper behind every range test on enemies (§3, §19.4)
projectiles.js projectile flight/impact
skills.js skill runtime: SP, triggers, durations, ammo, charges; interprets SkillSpec
ai.js enemy movement/attack AI, operator attack loop
fear.js 恐惧 movement of enemies (reachable-tile fan, random checkpoints, self-fear flutter; §20.4)
snapshot.js compact serialization for clients
content/
index.js installs all content into a Battle (kits, bonds, garrisons, items, bands, enemies, devices)
kits/index.js operator kit registry keyed by base chessId (guide: kits/README.md)
kits/ops/*.js one operator kit (skills + talents) per file, <chessId>-<codename>.js
kits/shared/*.js helpers the kits share (shared/tier1.js: the general ones)
tokens.js summon/token definitions
bonds.js battle side of the 23 bonds
garrisons.js battle side (IN_BATTLE) garrison effect keys
items.js battle side of equipment + Arts
bands.js battle side of band (strategy) effects
enemies.js enemy ability specs + generic special-type behaviours: dispatch hooks, KITS, STATS_ONLY
enemies/ helpers.js, archetypes.js, one kit file per special type (invisible, times, element, dot,
reflection, fly, special) and leaders.js — each with its part of KITS
bosses.js boss scripts (boss_1 … boss_10)
devices.js stage devices & special terrain
shared/
constants.js enums, phases, areas, error codes, UI-relevant constants
protocol.js message catalogue + validators (§8)
format.js rich-text description → HTML/plain; number formatting
rules.js pure helpers shared by UI & server (e.g. price display, sell value, bond tier from count)
data/ generated by tools/build-data.mjs — committed; see docs/DATA.md
public/
index.html single page; loads /js/main.js as module
css/ theme.css (tokens, fonts), components.css, screens/*.css
js/
main.js boot, router between screens, global store
net.js WebSocket client, reconnect, request/response helpers
store.js tiny observable store (state from server + local UI state)
data.js fetches /data/*.json, same indexes as server/data.js
audio.js BGM/SFX manager (Web Audio), volume settings
assets.js asset URLs, image cache, Spine loader with LRU + fallback
screens/ title.js, lobby.js, room.js, briefing.js, bandDraft.js, game.js (entry; game/*.js plates, marks, early events), result.js
ui/ hud.js, bondStrip.js, bondPopup.js, shopBar.js, teamPanel.js, detailPanel.js,
tooltip.js, choiceOverlay.js, rewardOverlay.js, ticker.js, emotes.js, settings.js, toasts.js,
gameLogic.js (barrel; gameLogic/*.js placement, bonds, phases, shop, loadout)
render/ app.js (field view; app/*.js helpers), projection.js, tiles.js, units.js, spine.js, fx.js (effects; fx/*.js), interp.js, drag.js, pick.js, promote.js (API §9)
vendor/ pixi, pixi-spine, preact, hooks, htm (copied from node_modules by tools/vendor.mjs)
assets/ downloaded art/audio (git-ignored), see docs/ASSETS.md
fonts/ self-hosted fonts
tools/
build-data.mjs official data + research JSON → data/*.json
fetch-assets.mjs downloads/optimizes assets → public/assets + data/assets.json
vendor.mjs copies vendor libs from node_modules → public/vendor
test/ node:test suites; test/e2e/ browser + bot tests
docs/ DESIGN.md (the index of this document: the rules in design/, the per-release revisions in
history/), ARCHITECTURE.md, DATA.md, SIM.md, META.md, I18N.md, ASSETS.md, BALANCE.md,
DEPLOY.md, PLAYING.md, WINDOWS.md, research/
(the wire protocol is normative in shared/protocol.js itself)
LICENSE GPL-3.0-or-later (the project's code); NOTICE.md: scope, non-commercial game assets, the Spine
Runtimes linking permission; THIRD-PARTY-NOTICES.md: libraries, fonts, data sources
Ownership rule for parallel agents: only edit files assigned to you; if you need a change in someone else's file, write it in your final report instead (the integrator applies it). Shared files (shared/*, docs/DESIGN.md) are owned by the architect.
11. Quality bar & testing
- No crash paths: every server handler guarded; a thrown error inside a Battle tick for one field must not kill the match (log, force-end that battle as timeout, continue). Invariants asserted in tests: no NaN/Infinity in any unit field; hp ∈ [0, maxHp]; positions inside rect; battles terminate within
timeLimit + 1 s(boss: terminate by pool/force); pool copy counts never negative and never exceed caps; funds never negative; hand/temp sizes respected. - Unit tests per module; content tests per effect; full-match bot tests: 1/2/4-player matches of every difficulty with fixed seeds run to RESULT headlessly (fast-forward: no real-time pacing) with zero errors; protocol fuzz (random/invalid intents) never crashes and never corrupts state.
- Browser E2E (puppeteer-core + system Chrome): open 2 tabs, create/join room, add AI, play through prep/combat of several rounds, zero console errors, screenshots saved to
test/e2e/out/for visual review. - Performance: a normal battle tick with 60 enemies + 10 ops < 0.5 ms; 4 parallel fields at 2× real time < 15% CPU of one core; the heavy 2-player boss field (18 ops, ~130 enemies alive,
test/sim/robustness.test.js) < 0.5 ms per tick on a development machine, the best of three runs of the same battle — 1.0 ms whenprocess.env.CIis set (GitHub's shared windows runners measured 0.51–0.58 ms and failed the 0.5 ms bar on PR #10, #12, #14 and master's own push; a real regression still shows locally).
12. Implementation phases
- Foundation: build-data, fetch-assets/vendor, server platform (http/ws/lobby), client shell (screens up to room).
- Core: sim engine + harness; match/meta; render engine; game UI.
- Content: kits, bonds, garrisons, items, bands, enemies/bosses/devices, choices.
- Integration & QA: wire everything, bots, E2E, balance pass, bug-hunt workflows until dry, visual polish.