diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 00000000..a8f02f2c --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,29 @@ +{ + // First typecheck slice (npm run typecheck). strict stays off until a folder is clean. + // tsc does not load this file unless invoked with -p jsconfig.json. + // Widen one path at a time: see types/README.md. + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "checkJs": true, + "allowJs": true, + "noEmit": true, + "strict": false, + "skipLibCheck": true, + "types": [], + "lib": ["ES2022"], + "maxNodeModuleJsDepth": 0 + }, + "include": [ + "shared/**/*.js", + "server/sim/rng.js", + "server/sim/dir.js", + "server/sim/constants.js", + "types/**/*.js" + ], + "exclude": [ + "shared/protocol.js", + "shared/bandBonds.js" + ] +} diff --git a/package-lock.json b/package-lock.json index a5ef6302..78d2a72b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,8 @@ "@eslint/js": "^10.0.1", "eslint": "^10.12.0", "globals": "^17.13.0", - "puppeteer-core": "^25.12.0" + "puppeteer-core": "^25.12.0", + "typescript": "^5.9.2" }, "engines": { "node": ">=22" @@ -2334,6 +2335,20 @@ "engines": { "node": ">= 18" } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } } } } diff --git a/package.json b/package.json index 3a40f32e..cf814805 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,8 @@ "setup": "node tools/setup.mjs", "doctor": "node tools/doctor.mjs", "launch": "node scripts/launch.mjs", - "lint": "eslint ." + "lint": "eslint .", + "typecheck": "tsc --noEmit --checkJs -p jsconfig.json" }, "engines": { "node": ">=22" @@ -52,6 +53,7 @@ "@eslint/js": "^10.0.1", "eslint": "^10.12.0", "globals": "^17.13.0", - "puppeteer-core": "^25.12.0" + "puppeteer-core": "^25.12.0", + "typescript": "^5.9.2" } } diff --git a/types/README.md b/types/README.md new file mode 100644 index 00000000..3e409922 --- /dev/null +++ b/types/README.md @@ -0,0 +1,25 @@ +# Typecheck + +`npm run typecheck` runs `tsc --noEmit --checkJs -p jsconfig.json`. + +The first slice is the closed set that already typechecks: + +- `shared/` except `shared/protocol.js` and `shared/bandBonds.js` (their JSDoc does not match the code yet; see below) +- `server/sim/rng.js`, `server/sim/dir.js`, `server/sim/constants.js` (the last imports `GEO` from shared) +- `types/` — JSDoc for `Unit`, `DamageInfo`, the snapshot tuples, and the protocol message envelope + +`strict` is off. `tsc` does not read `jsconfig.json` on its own; the script passes `-p`. + +## Widen it + +Add one file or one folder to `jsconfig.json` `include`, then run `npm run typecheck`. Fix JSDoc that disagrees with the code in that folder. Leave `strict` off until the folder reports nothing. Do not add `@ts-nocheck`. + +Suggested order once the sim split settles: `server/sim/grid.js`, `server/sim/body.js`, `server/sim/buffs.js`, then `server/sim/damage.js` against the typedefs in `types/core.js`. The sim does not import `types/`. The typedefs are a reference, not a runtime dependency. + +`types/core.js` follows docs/DESIGN.md §5.2, §5.4, §8 and docs/SIM.md §4, §9. Where those two disagree on `DamageInfo.type`, the typedef uses the union `makeDamageInfo` actually builds (`'elemental'` as well as `'element'`). + +## Not in the first slice + +`shared/protocol.js` — the catalogue JSDoc is ahead of a few object literals (`$optional` sits on a `Record` of checkers; `unitStatsEntry` does not list `side` or `silenced`, and the code reads `side`). Fix the comments, then remove it from `exclude`. + +`shared/bandBonds.js` — the data-shape JSDoc rejects an empty fallback object and a tuple callback. Same treatment. diff --git a/types/core.js b/types/core.js new file mode 100644 index 00000000..effe5f3a --- /dev/null +++ b/types/core.js @@ -0,0 +1,119 @@ +/** + * Core shapes for the sim and the wire protocol. + * + * Taken from docs/DESIGN.md §5.2 (Unit), §5.4 (DamageInfo), §8 (envelope, b.snap) + * and docs/SIM.md §4 (DamageInfo as makeDamageInfo builds it) and §9 (snapshot tuples). + * The sim does not import this file. It is here so the shapes have one JSDoc home + * while server/sim is still being split. + */ + +/** + * A unit inside one battle. Minimum fields from DESIGN §5.2. + * Stats (`maxHp`, `atk`, …) are recomputed by the sim and are not all listed here. + * + * @typedef {Object} Unit + * @property {number} id unique per battle + * @property {'ally'|'enemy'} side + * @property {'op'|'token'|'enemy'|'device'} kind + * @property {string} defId + * @property {string|number} ownerId player id; enemies use the field owner or the source player + * @property {number} x + * @property {number} y + * @property {number} [tileR] operators + * @property {number} [tileC] operators + * @property {'UP'|'RIGHT'|'DOWN'|'LEFT'} [dir] allies: deploy direction. Enemies are not meaningful here. + * @property {number} facing horizontal sign, +1 or −1 (LEFT is −1) + * @property {number} hp + * @property {boolean} alive + * @property {number} sp + * @property {Object[]} [buffs] + * @property {number} [statusFlags] + * @property {number[]} [blocking] operators: ids of enemies they block + * @property {number|null} [blockedBy] enemies: blocker id + * @property {number[][]} [liveRangeGrid] the attack range the detail card shows + * @property {string} [form] model form (enemy mode, or a doll's `'doll'`) + */ + +/** + * One damage instance, after `makeDamageInfo` (server/sim/damage.js). + * DESIGN §5.4 lists `type` as `'phys'|'arts'|'true'|'element'`. + * SIM §4 and the normaliser also use `'elemental'` (HP damage from an element burst) + * and keep `'element'` for gauge fill, which removes no HP. + * + * @typedef {Object} DamageInfo + * @property {number} amount pre-mitigation + * @property {'phys'|'arts'|'true'|'elemental'|'element'} type + * @property {'burn'|'necrosis'|'neural'|'apoptosis'|string|null} [element] + * @property {number} [atkScale] + * @property {number} [defIgnoreFlat] + * @property {number} [defIgnorePct] + * @property {number} [resIgnoreFlat] + * @property {number} [resIgnorePct] + * @property {number} [mul] + * @property {boolean} [canDodge] + * @property {boolean} [isSkill] + * @property {boolean} [isSplash] + * @property {boolean} [isAttack] + * @property {boolean} [isProjectile] + * @property {string[]} [tags] + * @property {boolean} [cancel] set by a `hit` hook to drop the instance + * @property {boolean} [noSp] + * @property {boolean} [ignoreSleep] + * @property {boolean} [ignoreSelect] reaches an airborne ally; the hit selected nobody + * @property {boolean} [sourceless] 无来源: source stats add nothing + * @property {number} [attackId] shared by every instance of one normal attack; 0 otherwise + * @property {Unit|null} [traitAlly] + */ + +/** + * One living unit in a snapshot. + * `[id, x, y, hp, maxHp, sp, spMax, flags, anim]` (SIM §9, DESIGN §8.2). + * `flags` is the UF bitmask. `anim` is the ANIM code. + * + * @typedef {[number, number, number, number, number, number, number, number, number]} UnitSnap + */ + +/** + * A knocked-out operator waiting to redeploy. + * `[id, respawnAt, respawnTime, state, row, col]`. + * `state`: 0 counting, 1 timer done and DP short, 2 timer done and the tile is taken. + * + * @typedef {[number, number, number, number, number, number]} DownSnap + */ + +/** + * Element gauge or burst cooldown on one unit. + * `[id, element, fill, cooldownEnd, cooldown]`. + * + * @typedef {[number, string, number, number, number]} ElemSnap + */ + +/** + * `snapshot()` (SIM §9). On the wire this object is the payload of `b.snap`, + * and `t` is sent as `gt` because the frame's `t` is the message type (DESIGN §8.2). + * `dp` is one number. `down` and `elem` are omitted when empty. + * + * @typedef {Object} FieldSnapshot + * @property {string} fieldId + * @property {number} t game seconds + * @property {UnitSnap[]} units + * @property {number} dp + * @property {number} killed + * @property {number} total + * @property {Object} [dps] + * @property {Object} [boss] + * @property {DownSnap[]} [down] + * @property {ElemSnap[]} [elem] + */ + +/** + * One WebSocket JSON frame (DESIGN §8). Client requests may carry `rid`. + * The server's `ok` or `error` echoes it. Server pushes omit it. + * Other own fields are the payload. Inbound frames are capped at 64 KB. + * + * @typedef {Object} ProtocolMessage + * @property {string} t message type + * @property {number} [rid] request id + */ + +export {};