Playtest #3 fixes: lost models, picking, live LP, temp overflow, 蕾缪安 S3, 回环射手, FX, detail card, spinners

- #1 lost operator models: the Spine store no longer hands out a skeleton whose unload is in flight
  (PIXI.Assets.unload is async); eviction runs 1 s after a release and the empty-scene budget only after 3 s
- #7 picking: one shared rule (render/pick.js, pixel probe where bodies overlap) for prep/battle clicks,
  hover, the enemy pen and equipment drops; the drag ghost stands on its drop tile; hover probes capped
  at one per frame
- #2 the top-bar LP drops live as the own battle's enemies enter the blue gate (−N, 联防中), teammates'
  pending LP in the team panel
- #3 temp overflow pieces that arrive after a prep deadline survive through the next prep (tempDue);
  temp-row warning and ready-button reason
- #4 蕾缪安 S3: locks every 0.5 s, then one shell every 0.3 s in lock order (PRTS notes), bombardShell fx
- #5 回环射手: real boomerang (out 15, back 3.75 tiles/s, attack only when every boomerang is caught)
- #6 visible projectiles, muzzle flashes, slashes, skill bursts and auras at unchanged frame cost
- #8 特质 right under the detail card header, with its official type icon
- #9 no spinning busy cursors / spinners next to texts; game data warmed before the match
- docs, recordings and tests updated (2414 unit tests, all browser suites green)
This commit is contained in:
sganggs committed 2026-09-29 23:42:55 +08:00
1 parent 01a44cd288
commit 0aba602ecf
72 files changed
+5132 -571

No files matched your search

+3 -2
View File
@@ -16,8 +16,9 @@
// --rehearsal N layouts each bot rehearses per prep with the real sim (default: the Match default, 3; 0 = off,
// faster sweeps with the heuristic placement only)
// --check audit every match: engine invariants at every phase change + rule checks (server/match/audit.js:
// income, upgrade price, freeze, shop odds/bans, drafts, 联防 helpers, settle LP, final assault
// pairing/pool, hidden core, titles, deadlines). Exit code 1 when anything is violated.
// income, upgrade price, freeze, shop odds/bans, temp overflow lifetime (no piece outlives the prep it is
// due at), drafts, 联防 helpers, settle LP, final assault pairing/pool, hidden core, titles, deadlines).
// Exit code 1 when anything is violated.
// --errors per-source error summary over all runs: sim/content errors grouped by content module + handler label
// (+ unit) with occurrence and battle counts, meta handler errors by registry key, match engine errors
// --odds shop tier distribution of rolled chess slots per shop level (all runs)
+6 -2
View File
@@ -15,6 +15,8 @@
// enemy-pen demo (render-demo ?pen=<name>). Also writes public/dev/recordings/index.json.
//
// Usage: node tools/record-battle.mjs [--only name,name] [--out dir] [--seed n] [--max seconds]
// (`--max` caps every scenario; without it a scenario's own `max` — else 90 game s — applies, so a plain run
// reproduces the committed recordings byte for byte.)
import fs from 'node:fs';
import path from 'node:path';
@@ -39,6 +41,8 @@ const OUT = path.resolve(ROOT, typeof opt.out === 'string' ? opt.out : 'public/d
const ONLY = typeof opt.only === 'string' ? new Set(opt.only.split(',')) : null;
const SEED = Number(opt.seed ?? 20260927) >>> 0;
const MAX_SECONDS = Number(opt.max ?? 90);
/** Game seconds recorded at most for scenario `sc`: `--max`, else its own `max`, else 90. */
const maxSecondsOf = (sc) => (opt.max != null ? MAX_SECONDS : Number(sc.max ?? MAX_SECONDS));
const quiet = { warn() {}, error() {}, info() {}, debug() {} };
const ds = getDefaultSource();
@@ -111,7 +115,7 @@ const SCENARIOS = [
},
{
name: 'late-m01-r13', title: '常规作战 · 源石流发生装置 · R13(官方波次)', kind: 'normal', stageId: 'act2autochess_m01', modeId: 'mode_multi_hard', round: 13,
wave: 'official', waveSeed: 5,
wave: 'official', waveSeed: 5, max: 110, // the whole round (55 real s = 110 game s)
players: () => [{ playerId: 'P1', seat: 0, side: 'L', colOffset: 0, units: lineup('act2autochess_m01', ['星熊*', '银灰*', '斯卡蒂*', '莫斯提马*', '白面鸮', '寒芒克洛丝*', '阿罗玛', '远牙*']) }],
},
{
@@ -169,7 +173,7 @@ function record(sc) {
const frames = [];
let field = null;
let ticks = 0;
const maxT = Math.min(MAX_SECONDS, sc.kind === 'boss' ? timeLimit : timeLimit + 1);
const maxT = Math.min(maxSecondsOf(sc), sc.kind === 'boss' ? timeLimit : timeLimit + 1);
while (!b.finished) {
b.step();
ticks++;