With the bench rule the gained operator starts the next prep on the bench while its carrier's deployment is free. The buy loop's pre-emptive bench shed sells the weakest bench single by piece value while the bench is crowded, before the placement step runs: in 20 solo 绝境 matches on 昆图斯 it sold 44 of 222 gained operators that way. - bot.js: botPrepEndSteps remembers the chess owned when the prep ends (rememberOwned); the shed's keepFresh now also spares a piece gained since then (a transformation's operator, battle-result / round-start grants), so the lineup step decides about it. No memory yet (a first prep, a seat just put under AI 托管) changes nothing. - botbench, decisions only: 昆图斯 (seeds 201-220) 绝境 7 -> 11 / 20 wins with the fix (8 on the WA build), 险境 16 -> 17 / 20 (15); the bots' own strategies unchanged within noise (solo 绝境 8 / 12 both, solo 险境 19 / 24 both, co-op 绝境 4 AI 9 -> 8 / 36). - Test: a real solo 绝境 昆图斯 bot match (seed 9 sold its R7 operator in the R8 prep before the fix): no gained operator shed in its first prep, none waiting on the bench beside a free deployment. - DESIGN §21.1 / §21.6 / §6.6, META and the CHANGELOG line say so.
360 KiB
卫戍协议:盟约 — Web Remake · Architecture & Contracts (DESIGN.md)
This is the single source of truth for every implementer. Research lives in docs/research/ (start with 00-INDEX.md; where a research file's body and its "Addendum (critic)" disagree, the addendum wins). When this document and research disagree, this document wins; when this document is silent, follow research; when both are silent, choose the simplest faithful behaviour and write it down in the module's header comment.
Language: all player-facing text is Simplified Chinese (names/descriptions come from official data). Code, comments and identifiers are English.
Versions: the first public release was 0.1.0 (2026-10-02, the state of §0–§20.15); 0.1.1 (package.json, shared/constants.js APP_VERSION; CHANGELOG.md) adds the player feedback after it — §21 — and is the state described by this document. The labels v1 / v2 / v2.1–v2.5.2 in §0, §14–§20 and in the BALANCE / SIM comparisons name the design generations and the private playtest builds that came before it; they are kept as history.
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.
Out of scope v1: matchmaking queue, training/tutorial, DIY (甄选) slots (the 4 DIY chess are removed from the pool), 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 HTTP static server (gzip for .skel/.atlas/.json/.js/.css), WebSocket upgrade at /ws, boot
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
PlayerState.js per-player economy/shop/hand/board/items/bonds/LP state + all prep-intent handlers
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
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/tier1.js … tier6.js operator kits (skill + talents) keyed by base chessId
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
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, 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
render/ app.js, projection.js, tiles.js, units.js, spine.js, 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 (this), DATA.md, SIM.md, META.md, ASSETS.md, BALANCE.md, DEPLOY.md, PLAYING.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.
3. Coordinates, fields and geometry
- Every stage is a 19-row × 21-col grid (
data/stages.json). Row 0 is the bottom row, col 0 is the left. Tile(r,c)has world centre(x=c, y=r); 1 tile = 1 world unit. Unit positions are floats in this space. - Areas (research 05 §2.2):
- Normal field (own board): rows 9–12, cols 2–10. Enemy gates
Sat (9,10) and (12,10) (R1–3 only the lower one); protection objectiveEat (9,2). Col 9 lane is not deployable (tile type decides). - Hand (整备区): row 7, cols 0–9 (10 regular slots, index = col). Temp slots: row 8, cols 4–8 (5 slots). Hand/temp tiles are never part of a battle.
- Partner display cols 11–18 of rows 9–12 (the unite field uses them).
- Boss field: rows 1–5; left half cols 2–10, right half cols 10–18.
- Enemy preview pen rows 14–18 × cols 7–13 (row 16 empty): during prep the next round's enemies idle there (upper-gate enemies rows 17–18, lower-gate rows 14–15, ≤ 50 models, spawn-time order) — research 08 §4 / 09 §2.
- Normal field (own board): rows 9–12, cols 2–10. Enemy gates
- Tile semantics come from
data/stages.jsonlegend: height (LOW/HIGH), buildable (ALL/MELEE/RANGED/NONE — the effective deploy type: the 深水区tile_deepsearefuses deployment, NONE with the level'sbuildableTypeALL kept, PRTS 深水区 地形信息 "拒绝部署(待补充)"; an active 特制水上平台 makes its tile deployable in the prep deploy map only, §21.3), ground-passable, fly-only, special terrain (mire, smog, deepsea, infection). The fenced tiles (围墙tile_fence_bound/ 围栏tile_fence, legendb: LOW, buildable ALL, passable to flyers only) take units, which attack normally from there but block no ground enemy (PRTS 地形机制, §21.23). Melee chess may stand only on LOW tiles with buildable ALL/MELEE; ranged chess on ALL/RANGED and on LOW ALL/MELEE tiles ("所有行动内远程干员可部署在近战位"). Tokens follow their ownposition; a summon markedownerRange(狼群, 流形: "只能部署在召唤者攻击范围内") goes only on a tile of its owner's attack range — the loadout grid rotated by the owner's facing around its tile (PlayerState.summonRange, §21.3). - Facing (corrected, see research 09 §1.2): like standard Arknights, every board piece has a direction
dir ∈ UP|RIGHT|DOWN|LEFTchosen with the 4-direction deploy wheel after dropping it on a tile (also re-orientable in place); defaultRIGHT; persists across rounds. Range grid entries are[dRow, dCol]relative to facing RIGHT and are rotated: RIGHT(dr,dc), UP(dc,−dr), LEFT(−dr,−dc), DOWN(−dc,dr)(row 0 = bottom);rangeExtendapplies along +dCol before rotating — to a running skill's range too, unless that skill ignores 攻击距离 (targeting.noRangeExtend, 信仰搅拌机 S3, §21.16). The right-side Final Assault player is mirrored colc→20−cwith RIGHT↔LEFT (UP/DOWN unchanged). - Range test: an enemy is inside a grid range if the tile containing it (
round(y), round(x)) is one of the range tiles; a huge enemy (datahitArea: 巨型单位, a 4.95 × 2.95 rectangle moved 1.0 up) if any tile its hit rectangle overlaps is one (PRTS 作战机制 "巨型BOSS单位的每一个占据的格子都可以让其本身通过格子判定";sim/body.js, §19.4). Radius ranges (enemies, auras, AoE) use Euclidean distance in tiles — to a huge enemy's rectangle ASSUMED — except splash around a struck target, a 中点判定 on positions (PRTS 作战机制 "中点判定…案例:阻挡,酒神1天赋的1.3溅射半径"). A ranged enemy's normal attack takes an ally whose collider (radius 0.25, PRTS 作战机制 §碰撞体积 "我方干员碰撞体积基本均为以0.25格为半径的圆形") touches its range circle: centre distance ≤rangeRadius+ 0.25 (constants.js ALLY_COLLIDER_RADIUS,ai.js enemyAttack; §20.4). - Fields at runtime: a
Battlereceives a field rect (rows/cols subset) and the full stage grid. Normal battles use rows 9–12 × cols 0–10; unite uses rows 9–12 × cols 0–20; boss uses rows 0–5 × cols 0–20 (solo boss uses the left half only). Pathfinding never leaves the rect. - Speed:
tilesPerSecond = moveSpeed × MOVE_SCALEwithMOVE_SCALE = 0.5(insim/constants.js, tunable).
4. Time model
- Simulation time is game seconds. Fixed step
TICK = 1/30 s. - Combat runs at 2× real time (forced, like the original): 60 ticks per real second — in the browser's runner (§14) or, for server-run fields, the server's accumulator (2 ticks every real 1/30 s, never more than 8 ticks per interval to avoid spirals).
- Combat time limits come from
data/config.json → modes[m].rounds[r].combatTimeLimit(= the level'smaxPlayTime; alsomodes[m].combatTimeLimit[r]) and are real seconds of the forced-2× battle (verified by the balance pass: spawn schedules exceed the limit when read as game seconds). The server passes2 × valuegame seconds to the Battle (server/match/gamedata.js → combatTimeLimit;config.combatTimeScale, default 2); the countdown players see equals the data value. When time runs out, living non-boss enemies count as leaked. 联防 uses the round's limit. - Final Assault / Hidden Core have no hard stop and their clocks are real seconds too: the boss level's
levelMaxPlayTime(120 real s) is only the HUD countdown (m.public.deadline; the battle goes on past it), and the overtime drain (bossOvertimeAfter150 /bossOvertimeDrainPerSec1 = officialbossTurnHpReduceTime) takes 1 team LP per whole real second from 150 real s (= 300 game s on the 2× field clock; first point at 151 s —gamedata.js bossOvertimeDue).m.public.overtimeAt= the ms epoch when the drain starts; both are placed on the field clock. - Solo pause (§14): while
m.public.pausedthe running battle's field clock, the HUDdeadline/overtimeAtand every server deadline of the battle stand still; on resume they move on by the paused time. Co-op battles never pause. - Prep/draft timers are real seconds, stored as absolute server deadlines (
Date.now()-based) and sent to clients asdeadline(ms epoch) +serverNowfor clock-offset correction. - Snapshots to clients: every 3 ticks (= 20 Hz real) per watched field.
5. Simulation (server/sim) — engine contract
5.1 Battle API (implemented by the sim-core owner; consumed by match & content)
import { Battle } from './sim/Battle.js'
const b = new Battle({
seed, // uint32
kind, // 'normal' | 'unite' | 'boss' | 'hidden'
modeId, round,
stage, // data/stages.json entry (full 19×21 grid)
rect: { r0, r1, c0, c1 }, // inclusive field bounds
timeLimit, // game seconds; boss/hidden: Infinity (overtime handled by match)
players: [ PlayerBattleInput ], // 1 (normal), 1–2 (unite, boss)
spawns: [ SpawnSpec ], // enemy schedule for this field
routes: [ RouteSpec ], // from the wave template, resolved to this field
sharedBoss: SharedBossPool | null,// { hp, maxHp, damage(playerId, amount) } for boss rounds
flags: { layerGainsEnabled: bool, dpInit: 10, dpPerSec: 1, dpMax: 99 },
})
b.step() // advance exactly one TICK
b.finished // true when: all enemies dead & no spawns pending, or time limit reached, or all players' LP pools say stop (boss: match decides via b.forceEnd())
b.forceEnd(reason)
b.time // game seconds elapsed
b.result() // BattleResult (below), valid once finished
b.snapshot() // compact full snapshot (§8.4) of this field
b.drainEvents() // array of client-facing events since last drain (§8.4)
b.on(event, fn, { priority=0, owner } ) / b.off(...) // hook bus (§5.4)
PlayerBattleInput:
{ playerId, seat, side: 'L'|'R',
colOffset, // 0 for own field; +8 for the right half in unite/boss
units: [ { uid, kind: 'chess'|'token', chessId /* golden id if elite */, tokenId?, row, col,
items: [itemId...], // equipped items (golden ids when merged)
carryState?: { hpPct, sp, skillActive } | { down: true } } ], // unite: helper units keep end-of-combat state;
// { down: true } = knocked out at its end: deployed, then forced out (§19.3)
bonds: { [bondId]: { count, active, tier, layers } }, // snapshot from bondsMeta at combat start
bandId, playerEffects: [ EffectRef ], // active band/choice/team effects with battle parts
lpForBoss?: number }
SpawnSpec: { time, enemyKey, routeIndex, count=1, interval=0, mods: { hpMul, atkMul, defMul, speedMul }, sourcePlayerId?, bounty?: { coins, ownerPlayerId } , tag?: 'escort'|'boss'|'bounty' }
BattleResult:
{ time, reason: 'cleared'|'timeout'|'forced',
perPlayer: { [playerId]: {
killed, total, // enemies attributed to this player's field/half
leaked: [ { enemyKey, mods, lpr, sourcePlayerId } ], // for unite aggregation & LP
perfect: bool, // no leaks
layerGains: { [bondId]: n }, // IN_BATTLE gains (0 if flags.layerGainsEnabled false); what was added — capped at
// the bond's room under BOND_LAYER_CAP 999 (§6.3, §20.12)
coins: n, // bounty coins earned
damageDealt: n, bossDamage: n, healingDone: n, deaths: n,
unitsEnd: [ { uid, hpPct, sp, alive } ] } },
bossHpLeft? }
5.2 Units & stats
Unit fields (minimum): id (int, unique per battle), side ('ally'|'enemy'), kind ('op'|'token'|'enemy'|'device'), defId, ownerId (playerId; enemies: the field owner / source), x, y, tileR, tileC (ops), dir (allies: UP|RIGHT|DOWN|LEFT deploy direction, §3; sim/dir.js), facing (derived horizontal sign +1 / −1: LEFT ⇒ −1), hp, alive, sp, buffs[], statusFlags, blocking[] (ops), blockedBy (enemies), liveRangeGrid (allies: the 攻击范围 the detail card shows — the running skill's grid, else the unit's own, plus rangeExtend; never a kit's target-selection grid, §21.16), form (enemies: the model form it is in — UnitInfo form, §21.4).
Base stats (from data): maxHp, atk, def, res (0–100), aspd (100 base), bat (base attack time, s), blockCnt, rangeGrid (ops) / rangeRadius (enemies, 0 = melee), moveSpeed, massLevel, weight, lpr (enemies), dmgType ('phys'|'arts'|'heal'|'true'), motion ('WALK'|'FLY'), cost, respawnTime, spRecovery.
Aggregation (units.js, recomputed lazily when buffs change):
ATK = (base + Σflat) × (1 + Σpct) × Πmul— same shape formaxHp,def; the 卫戍 systems' "+X%" ATK / DEF / max HP (盟约, 策略, 装备, 机变 cards, per-layer 特质) are 直接乘算 and go intoΣpctwith the skills' "+X%" (sim/constants.js DIRECT_BONUS_STACKING 'add', content/supportdirectMods; PRTS 盟约记录 / 游戏数据基础, §20.10) — "提升至X%" effects (炎佑 ×1.5) and the char_attribute_mul 特质 stay inΠmul;res = clamp((base + Σflat) × Πmul, 0, 100);aspd = clamp(100 + Σaspd + base-100, 20, 600)(constants.js ASPD_MIN20: PRTS 数值范围 "ATTACK_SPEED 默认下限 20", §20.3).interval = bat × (1 + ΣbatPct) × 100 / aspd(blackboardbase_attack_timeis treated asbatPct, e.g. −0.3 → 70%).- Changing
maxHpkeeps the HP ratio. - Stats are rounded only for display; the sim keeps floats.
Elite (精锐) chess have their own stats in data (golden chess id). The module bonus is already folded into data stats by build-data.
5.3 Buffs & statuses
Buff = { key, source, duration /*s, Infinity*/, stacks=1, maxStacks=1, refresh: 'replace'|'extend'|'stack', mods, flags, onTick?, onExpire?, tags[] }
modskeys (all optional, additive within a key):atkFlat, atkPct, atkMul, defFlat, defPct, defMul, hpFlat, hpPct, hpMul, resFlat, resMul, aspd, batPct, blockCnt, rangeExtend (tiles), moveMul, dmgDealtMul (multiplicative, e.g. 1.2), dmgTakenMul, physTakenMul, artsTakenMul, trueTakenMul, defIgnoreFlat, defIgnorePct, resIgnoreFlat, resIgnorePct, dodgePhys, dodgeArts, healingDealtMul, healingTakenMul, spRecoveryFlat, spRecoveryMul, maxTargets (+n), shield (absorbs damage; consumed).flags:stun, freeze, sleep, silence (no skills), disarm (no attacks), stealth (untargetable by enemies/ops unless blocked or revealed — an operator's radius area damage skips it too,Battle.foesInRadius, PRTS 作战机制 §AOE伤害判定, §21.19; an enemy's b.snap stealth bit is set only while its 隐匿 is on, §21.4), camou (an ally's 迷彩: like stealth for enemy targeting and on screen, but not 隐匿 for 隐匿 conditions — 叙拉古, 家族徽章; §20.2), liftoff (an ally's 起飞, 蒂比's skills: it blocks flyers only and has 对地规避 — no ground enemy selects it, so no selected damage or status of one lands, while what selects nobody still does:ignoreSelect/ 无来源 — 无视无法选择 abilities, direct picks, flying units' blasts, a debuff's ticks; it stays a ground unit on its tile;targeting.js evadesGround, §21.22), invulnerable, unblockable, levitate, fear (enemy: cannot attack, unblockable, runs to random tiles of the ±45° fan away from the source; no source or itself as source = inside its own tile —fear.js, §20.4), taunt (+aggro), cold, noHeal (no heals: 禁疗 summons and 孤立 units; an HP-regen attribute still applies — PRTS 安洁莉娜 备注), isolated (孤立: no friendly selector picks it — no heal, buff, aura or talent of an ally,Battle.allySelectable; §20.3).- Status catalogue (
buffs.js, keys used by content):stun,freeze(stun + res −15),cold(aspd −30; a second cold while cold ⇒ freeze 3 s unless immune),sleep(stun; ends on damage? no: in AK sleep = untargetable & unable to act; keep AK: untargetable & inactive),slow(moveMul),bind(moveMul 0),fragile(dmgTakenMul),artsFragile(artsTakenMul),silence,fear(the 恐惧 walk,fear.js),burnGauge/necrosis/neuralelement gauges (see damage),stealth,camou(迷彩, its own keys: 忍冬 S3vulpis:camouuntil her next cast, 寒芒克洛丝 S1),reveal. - Immunities from enemy data (
stunImmune,silenceImmune,sleepImmune,frozenImmune,levitateImmune) are honoured byapplyStatus; a unit bothinvulnerableanduntargetable(a 重生, a 永久无敌 leader part) takes no status from the other side at all (PRTS 无敌 "无法被不同阵营选中", §21.19). FlagselfBound= 自缚 (守墓石像's 转换模式, the 自缚 leaders) beside itsnoMove: 余 S2 never teleports such a unit (束缚 setsnoMoveonly). - Same-named debuffs on an enemy (奥术's arts damage taken, 灵知 坚冰, the 莱恩哈特 / 缄默德克萨斯 RES cuts): one instance per target whoever applies it, the strongest wins (
Battle.applyStrongest; PRTS 作战机制 "同名buff…只能表现出一个" — strongest rather than latest ASSUMED, §20.10). PRTS 盟约记录's current 奥术 note fits this under the default overlap; an equal re-hit refreshes the instance (an earlier PRTS wording forbade re-applying while one's own lasts) [ASSUMED, slightly stronger] (§21.7).
5.4 Hook bus (the only way content plugs in)
battle.on(name, handler, { priority, owner }); handlers run in descending priority, then registration order. Handlers receive a mutable ctx. Events:
| name | ctx | notes |
|---|---|---|
battleStart |
{} |
after initial deployment |
deploy |
{ unit, initial } |
ops/tokens/enemies spawn |
tick |
{ dt } |
every tick after movement & attacks |
beforeAttack |
{ attacker, targets } |
may modify targets |
attack |
{ attacker, targets, isSkill } |
an attack was performed |
hit |
{ source, target, dmg } |
before mitigation; mutate dmg (DamageInfo) |
damaged |
{ source, target, amount, type, dmg } |
after application |
heal |
{ source, target, amount } |
before application (mutable amount) |
fatal |
{ unit, source, credit, dmg, amount, prevented } |
HP would reach 0 — fired by every HP loss of a unit without a boss pool (hits of any type, element bursts, 无来源 damage, loseHp); set prevented. Order: kits' own savers (10 … −60) → items' 不死 (坚固维式重锤, once per deployment) PRIO_REVIVE −100 → items' 复活 (M3茧甲) PRIO_RESPAWN −101 → 埃芒加德 −110 (§21.21) |
kill |
{ killer, victim } |
victim hp reached 0; a killed ability that keeps the enemy alive (a knock-out into a 重生 / form change, §21.4) hides it from every later handler, the kill count, credit and the bounty |
death |
{ unit } |
unit removed (ops may redeploy later); the die event and the death hooks fire only on removal |
skillStart / skillEnd |
{ unit, skill } |
|
ammoUsed |
{ unit, left } |
per ammo consumed |
spGain |
{ unit, amount } |
mutable |
statusApplied |
{ source, target, status, duration } |
after immunity check |
blocked |
{ blocker, enemy } |
enemy became blocked |
enemySpawn / enemyLeak |
{ enemy } |
|
battleEnd |
{ result } |
may add layer gains / coins |
DamageInfo: { amount (pre-mitigation), type: 'phys'|'arts'|'true'|'element', element?: 'burn'|'necrosis'|'neural'|'apoptosis', atkScale, defIgnoreFlat, defIgnorePct, resIgnoreFlat, resIgnorePct, mul (multiplier), canDodge, isSkill, isSplash, ignoreSelect (no selection — a 无视无法选择 ability, a direct pick, a debuff's tick: it reaches an airborne 起飞 ally, §21.22), tags[] }.
Engine helpers (content must use these, never touch internals): battle.dealDamage(source, target, dmgInfo), battle.heal(source, target, amount, {overheal=false}), battle.applyStatus(target, key, { duration, source, value }), battle.addBuff(unit, buff), battle.removeBuff(unit, key), battle.spawnToken(ownerUnit|playerId, tokenId, row, col, opts), battle.spawnEnemy(enemyKey, { routeIndex, pos, mods, tag }), battle.addProjectile({ from, to|target, speed, onHit, visual }), battle.push(e, force, { from, dir, fixed, fixedAngle, inward, effect }) / battle.pull(e, force, { to, center, stop }) / battle.pullToFront(e, unit, force) / battle.forceLevel(e, force) / battle.pushDistance(e, force, { effect }) (the official 力度 − 重量 tables, §20.3; a 静态刚体 — data staticBody — is never moved, §21.9; battle.displace(e, dir, distance) is the raw mover), battle.allySelectable(a, by) / battle.alliesFor(by, ownerId) (friendly selection, skipping 孤立 units, §20.3), battle.unitsInGrid(unit, grid), battle.enemiesInRadius(x, y, r) (every enemy: enemy-side auras / heals, collisions), battle.foesInRadius(x, y, r) (the enemies an operator's area effect can select — no untargetable, no unblocked unrevealed 隐匿 enemy; PRTS 作战机制 §AOE伤害判定, §21.19), battle.alliesInRadius(x, y, r, ownerId?), battle.addLayers(playerId, bondId, n, reason) (no-op when gains disabled; at most the room left under BOND_LAYER_CAP 999 — shared/constants.js layerGainRoom, §20.12; records into result), battle.loseHp(target, amount, { source, from, sourceless }) (流失 — it skips the damage events: no 受击回复, TAKE_DAMAGE, shield or counter, damage.js isHpLoss; only for what the official calls 流失 — "N 真实伤害 per second" effects such as 源石溶剂 are periodicDamage damage, §21.12; a 物理流失 is first reduced by the target's DEF, 阿戈尔's devour, §21.19; sourceless = 无来源, source keeps the credit — leader parts pass damage on 1:1 with it, §20.10), battle.addCoins(playerId, n), battle.fx(kind, params) (client VFX event), battle.rng() (never Math.random), battle.getPlayer(playerId) (bonds/band/effects view), battle.redeploy(unit) (on its rest tile: where a knocked-out operator lies, else its home; battle.isReservedTile(r, c) = a tile no automatic placement takes, battle.downOn(r, c) = the knocked-out operator lying there, whose tile no ally deploys or moves onto — §21.24), battle.after(seconds, fn) (scheduled callbacks), battle.every(seconds, fn).
5.5 Combat rules (engine)
- Initial deployment at t=0, free: operators row-major top→bottom then left→right (right-side boss player: right→left), then the summons (PRTS 卫戍协议/帮助 "按从上到下>从左到右的顺序部署。优先部署干员,随后为召唤物";
aggroSeqranks every start-of-battle summon after all operators) — a skill's placed summon (赫默 医疗探机, 巫恋 诅咒娃娃) included, once and free ("所有手动部署的召唤物,无视所属干员的持有状态…作战开始时立即部署一次"; afterwards it re-appears on its tile each time the owner's skill gives one, §20.1); on a shared field (联防, boss) the players' i-th operators deploy together ASSUMED. Deploy effects fire (deploywithinitial: true). Then operators carried into 联防 knocked out (carryState.down) are forced out (FORCED_EXIT, HP 0, §19.3). ThenbattleStart, after which the forced-out operators' timers are re-read (redeploy-time effects that start with the battle, e.g. 机变 征召, cover them too). - DP: per player, start 10, +1/s, cap 99; used only for auto-redeploy (dead op: after
respawnTime, if the tile it lies on is free and DP ≥ cost ⇒ redeploy there with full HP, SP = initSp). A knocked-out operator stays down on the tile it fell on — its own home when it fell on another board piece's home and its home is free — until it redeploys there, and no ally deploys or moves onto that tile meanwhile (PRTS 卫戍协议/帮助 §作战阶段 单位部署;Battle.downOn/restTile/_layBody, §21.24; b.snapdown, §18.3: counting / waiting for DP / its tile taken, a safeguard) — also one that entered 联防 knocked out (FORCED_EXIT, its full redeploy timer — confirmed by the user — with the battle-start redeploy effects, §19.3); summons, devices and enemies leave at once. - Blocking (contact radius, PRTS 游戏数据基础 §阻挡半径 / 作战机制 "中点判定…案例:阻挡";
constants.js BLOCK_RADIUS,Battle._checkBlock, §19.2): an operator with free capacity blocks an unblocked, blockable enemy whose position lies within its block radius of its centre — ground 0.7071 (squared 0.49999037), 0.8944 for blockFly units vs flyers (起飞), devices 0.4472 (阻隔工事 / 障碍物); air units (FLY, 近地悬浮, 浮空) only by blockFly units. A ground enemy is never blocked by a unit standing on a tile ground units cannot pass (围墙 / 围栏: PRTS 地形机制 "部署在其中的单位,若当前阻挡类型为'地面阻挡'则无法阻挡敌人" — an enemy pushed or pulled against the fence walks on; air blocking there stays ASSUMED, §21.23) nor by an airborne (起飞) unit, which blocks flyers only (§21.22). The nearest such operator wins (ties: scan order ASSUMED). It is re-checked every tick, moving or not, so another operator in contact with room takes the enemy over when its blocker dies, is withdrawn or is stunned, and an operator whose capacity frees up grabs an overlapping enemy; one that finds no room walks on. A head-on enemy stops at contact (≈ 0.71 tile), outside the blocker's tile. Enemy usesblockCntcapacity (default 1). Blocked enemies stop moving and attack their blocker if melee (or anyone in range if ranged, blocker first). Every blocker — melee units (重装要塞 / 领主 / 哨戒铁卫 included), summons and a ranged operator on a melee tile alike — whose attack hits enemies may always target the enemies it blocks, in range or not, whatever its facing, and targets them first ("可以选择且优先选择阻挡单位", PRTS 选择器; the user's rule after playtest #6, "阻挡了就一定要能打到": officially the collision pushes a blocked enemy to its blocker's front;Battle.blockedTargets,targeting.js sortEnemyTargets; §20.3 — it replaces the playtest #5 QA's melee-only restriction of §19.2). A healer (dmgTypeheal, or a skill attack turned into a heal) selects injured allies only and keeps healing while it blocks (PRTS 卫戍协议/帮助 "对于医疗干员(咒愈师分支除外),攻击目标为需要治疗的单位"; §21.17); 咒愈师 follow the blocker rule. The huge leaders are 自缚 + unblockable (§7). - Operator attacks: every
intervals if a valid target exists; targets = up tomaxTargets(default 1) fromunitsInGrid(rangeGrid)(arangeAoeprofile — 阵法术师, 轰击术师 — takes every selectable enemy in range, §21.18), priority: (1) the enemies it blocks (every blocker, in range or not, §20.3), (2) profession/trait priorities (e.g. fly-first snipers,traittext parsed by build-data intotargetPriority), (3) enemy with the least remaining path distance to its goal, ties → earliest spawned. Melee (no ranged trait) and ground-only attacks cannot hit air units (Unit.isFlying: FLY, or a 近地悬浮 / 浮空 enemy — PRTS "算作空中单位", §19.1). Stealthed enemies can't be targeted unless blocked or revealed. Ranged ops fire projectiles (speed ~12 tiles/s, instant for melee/arts-beam classes as data indicates); damage applies on impact; a 锁定攻击范围 AoE (rangeAoe) strikes every selectable enemy on its range at once with instant hits (§21.18). - Healers (
dmgType heal): target the ally in range with the lowest HP% below 100%, never a 禁疗 / 孤立 unit (tokens.jsonabnormal,noHeal; §20.3), and never the enemies they block — also while blocking (§21.17); do nothing otherwise (SP still regenerates). - Enemy AI: follow the route (checkpoints; WALK paths recomputed on the grid when devices change); when blocked stop & fight (
dmgType 'none'enemies never attack unless content arms them throughe.profile— 转译基底's armed forms, §21.4); ranged enemies (rangeRadius > 0) attack ops whose 0.25 collider touches their radius (centre distance ≤rangeRadius+ 0.25, §3, §20.4): the enemy's own target rule filters the candidates first (profile.canTarget: 只攻击地面单位 …), then priority blocker → special priority → highest taunt → latest deployed (aggroSeq) — PRTS 索敌 "阻挡→特殊优先级→仇恨值→最早出现"; a stealthed ally is attacked only by the enemy it blocks (§19.1); an airborne (起飞) ally is never a ground enemy's selection — picks, area damage, zones and statuses skip it, while 无视无法选择 abilities, direct picks, debuff ticks and flying units still reach it (§21.22); they pause movement forATTACK_PAUSE = 0.35 sper attack. FLY enemies ignore ground obstacles (only blockFly units block them) and follow their checkpoint route. A feared enemy runs between random 恐惧可达地块 away from the source (PRTS 诱发移动;fear.js); 恐惧 outranks 诱导; the 恐惧 / 诱导 walks re-plan from where the enemy stands after a push or an obstacle change, and the route re-plans when they end (§20.4). - Damage: phys
max(A − max(0, D×(1−defIgnorePct) − defIgnoreFlat), 0.05·A); artsmax(A × (1 − R'/100), 0.05·A)withR' = max(0, R×(1−resIgnorePct) − resIgnoreFlat); true = A; 元素伤害 ('elemental')max(A × (1 − 元素抗性/100), 0.05·A). Then ×dmgDealtMul(source) ×dmgTakenMul& type-specific taken multipliers (target) ×dmg.mul— 元素伤害 takes 元素脆弱 (elementalTakenMul) alone, notdmgTakenMul(脆弱 = "受到的物理、法术、真实伤害提升"). Then 限伤: on a leader (isBoss— the 10 bossInfoDict enemyIds and their mirrored copies; never parts, drones or escorts) in a'boss'/'hidden'battle a hit withceil(final) ≥ 300000is cancelled — no HP or pool loss, no credit, no damage number, nodamaged/ kill (shared/constants.js BOSS_HIT_LIMIT,damage.js leaderHitCancelled, after every multiplier and the hit-step blocks, before HP shields;loseHptoo — a part's 传递, a drone's death; the client'sMAX_BATTLE_DAMAGE, §20.12). Then shields absorb; dodge checks for phys/arts. Element damage fills a gauge (1000; enemy leaders 2000) ×elemTakenMul× max(0.05, 1 − 损伤抵抗/100), 损伤抵抗 = dataepResistance(PRTS 元素; the 5 % floor of DMG_e, PRTS 游戏数据基础). An element fill on a target with no HP left is refused (damage.jshasHp: no burst on a corpse). A full gauge bursts with the side-specific official effects, as 无来源 damage (no damage-dealt multiplier or penetration of the unit that filled the gauge, which keeps the kill and the stats; §19.5) (sim/constants.jsELEMENT; operators: 灼燃 1200 arts + RES −20, 神经 stun 10 s then 1000 true, 凋亡 15 s of 阻回 (noSp: no SP gain of any kind) + 静默 with −1 SP/s and 100 arts/s, 侵蚀 DEF −100 then 800 phys; enemies: the "·我方" terms). Every burst starts a 爆发冷却 = its duration (灼燃 / 神经 / 侵蚀 10 s — enemies' 侵蚀 8 s — 凋亡 15 s) during which NO element of the unit fills or can be recovered; when it ends every gauge of the unit resets (§18.3). - Leak: an enemy reaching its goal is removed, counted as leaked for its field owner (
sourcePlayerIdfor unite). LP accounting is done by the match, not the battle. - Results:
damageDealt/ a unit'sdmgcount only HP removed from the other side — a unit's own drain or 流失 and friendly damage are the target'stakenonly (§21.12). - End: all spawns done and no enemies alive ⇒
cleared;time ≥ timeLimit⇒timeout(remaining enemies = leaked). Boss battles end when the shared boss pool reaches 0 (victory) or the match forces an end (team LP 0). A pool holding less than 1 HP is empty (constants.js BOSS_POOL_MIN_HP: the hit, or the per-player credit, that would leave less takes the rest; §20.5).
5.6 Skills (skills.js + kits)
SP: spType INCREASE_WITH_TIME (+spRecovery/s), INCREASE_WHEN_ATTACK (+1 per attack), INCREASE_WHEN_TAKEN_DAMAGE (+1 per hit taken); starts at initSp; no SP gain while a duration skill is active. maxChargeTime > 1 ⇒ charges.
Auto-trigger rules (skillTrigger.rule in data — the official 技能策略, PRTS 卫戍协议/帮助 §作战阶段 技能操作; §20.2): DEFAULT (SP ready AND about to attack/heal AND an enemy/injured ally inside the initial range, or an enemy the unit blocks; with trigger.allies + hpAtMost an ally of the skill grid at or below that HP ratio — 塞雷娅 S1 急救, whose cast replaces that attack and is withdrawn with its charge if the ally recovers first), SKILL_RANGE (a MANUAL skill with a 技能范围 of its own, not an attack-range change: any living enemy, stealthed ones included, on the skill range, checked every tick — "仅在技能范围内存在敌人(无视其不可选中)时释放技能"; 13 skills / 26 records), TAKE_DAMAGE (on taking damage — any damage, 无来源 included, never a 流失; every MANUAL 重装 skill, §21.12), SP_FULL (immediately — 执旗手 / 战术家 / 吟游者 MANUAL skills, and AUTO DP skills such as 伺夜 S1, 焰尾 S1, 凛御银灰 S1), CUSTOM_RANGE (enemy inside the custom grid), SEARCH (an enemy inside the initial range, checked every tick — no attack needed; 解放者 / 阵法术师 MANUAL skills), GDGLOW_SKILL_2 (a valid target anywhere on the field: 荒芜拉普兰德, 纯烬艾雅法拉 S3), special ids. The class rows cover every MANUAL skill of the class and never an AUTO skill (AUTO skills keep their own rule — an inference: PRTS names whole classes; the data's skillIndex 0 / -1 sentinel argues the other way, §20.2). Operation cooldown: the engine auto-casts a MANUAL skill no sooner than AUTO_OP_COOLDOWN (3 s) after its previous cast and after the unit's battle-start deployment ("自动操作具有3s冷却,在完成一次操作或作战开始时部署的单位将进入冷却"; Battle._deploy initial, battle.flags.startOpCooldown); kits with an automatic cast of their own check skill.opCooling; a pending "next attack" never takes a second charge. PASSIVE skills are always on.
SkillSpec (authored per chess in sim/content/kits/tierN.js; values come from the chess's skill blackboard at its level — normal Lv4, elite Lv7 — so a spec is a function of the blackboard):
// kits map: baseChessId -> (bb, chess) => Kit
export default {
chess_char_1_01: (bb, chess) => ({
skill: {
kind: 'duration' | 'ammo' | 'instant' | 'charges' | 'passive' | 'toggle',
duration: bb.duration, // duration kind
ammo: bb['attack@trigger_time'], // ammo kind
mods: { atkPct: bb.atk, batPct: bb.base_attack_time }, // applied while active
targeting: { maxTargets, rangeGrid, priority: 'fly'|'ranged'|'lowestHp'|'highestHp'|..., noRangeExtend, showOwnRange }, // noRangeExtend: the range ignores 攻击距离; showOwnRange: the grid only selects targets (the card keeps the unit's own, §21.16)
attack: { dmgType, atkScale, splashRadius, hits, projectile: 'arrow'|'bolt'|'none'|'beam', onHit(ctx) },
onStart(ctx) {}, onEnd(ctx) {}, // bursts, summons, heals, statuses (use engine helpers only)
},
talents: [ { install(battle, unit) {} } ], // passive/conditional talents via hooks
trait: { ... } // optional overrides of profession trait behaviour
}),
}
The kit loader passes bb = flattened blackboard { key: value } of the chess's skill (normal or elite), and chess = data record (for talents' blackboards). Missing kit ⇒ a generic kit derived from blackboard keys (atk/attack_speed/def/max_hp/base_attack_time/atk_scale/max_target/duration/ammo) so every chess always works.
Profession traits (default behaviours implemented once in the engine, keyed by subProfessionId): e.g. fastshot (fly priority), aoesniper/splashcaster (splash 1.1 tiles around the target; 格雷伊 1.0), bombarder (ground splash 0.9 + aftershock), chain caster (bounce ×3, −15%, 1.7-tile jumps — PRTS 溅射半径一览, §21.19), phalanx/blastcaster (rangeAoe: every selectable enemy in range at once, instant; §21.18), funnel, physician (single heal), ringhealer (aoe heal), bard (aura heal, no attack), slower (slow on hit), loopshooter (回环射手: a boomerang out to the target and back, attacks again only once it is caught — §17.4), underminer, instructor, lord (ranged attack ×0.8 when not melee), centurion/fighter/musha/swordmaster/reaper etc., protector/guardian/fortress/duelist, pioneer/charger/tactician/bearer/agent, executor, merchant, hookmaster, pusher, stalker, geek, dollkeeper, traper, craftsman, summoner … (build-data provides the list of subprofessions present in the pool; the sim-core owner implements all present ones with simplified faithful behaviour).
6. Match & meta (server/match)
6.1 State machine (Match.js)
LOBBY(room) → INFO_CHECK (co-op 25 s; solo and any single-human match untimed (§18.2); all ready ⇒ skip; the operator loadout (§16) locks when it ends) → BAND_DRAFT (co-op: random order, ONE countdown — `BAND_TURN_SECONDS` 30 s per turn = m.public.deadline, AI seats pick at once, 1 skip each, a strategy a teammate already took is refused (队友已选), timeout ⇒ the strategy the player highlights (`g.bandFocus`) while free, else band_bldsk, else the first free one; solo: free pick, no timer, no skip; a single human: untimed) → BATTLE_CHECK (3 s)
→ loop r = 1..lastRound:
ROUND_START (income, upgrade price −1 (floor 0), temp NOT wiped — what overflowed after the last prep's deadline is shown in this
prep (§6.2 temp overflow), unfrozen shop slots rerolled, frozen kept, <进入休整期时> effects)
[SP_DRAFT] (if r ∈ spRounds: 机变 draft; co-op 30 s first picker / 16 s others; solo 3 cards; solo / single human untimed;
a card is picked with two taps, §18.2)
PREP (co-op: timer from config, ends when all alive humans+bots ready; solo / single human: untimed, ends on ready)
PREP_END (<休整期结束时> effects; unfrozen shop cleared; temp overflow: Ready is blocked while temp is non-empty; at the deadline
the temp pieces due at this prep are resolved (chess sold back, items destroyed, summon stacks removed — they come back at the next round start, §20.1) — pieces that arrived
after Ready or during the <休整期结束时> effects wait for the next prep (§6.2 tempDue))
COMBAT (one Battle per alive player, all in parallel, 2×; watchers get snapshots; solo: pausable, §14 Solo pause)
UNITE (co-op only, if ≥1 leaked & ≥1 perfect: ≤2 perfect helpers in `unite.js helperOrder` order (research 08 §5 / PRTS 帮助:
most units on the field > an active bond > most standing units > seat; the pair ordered by units > active bond >
Σ active layers > standing > seat, the first one on the right-hand field) fight the union of leaks; no layer gains;
a helper's operator knocked out at the end of its own combat enters down — carryState { down: true },
PRTS "上一阶段为退场状态的干员强制退场", its full redeploy timer — confirmed by the user (§19.3))
SETTLE (LP −min(leaks,10) per player (unite survivors to their source); eliminations; layer gains applied; coins; broadcast)
R14 FINAL_ASSAULT (merged team LP; pairs by seat; shared boss pool; 120 real s countdown, no hard stop; overtime −1 team LP per
real s after 150 real s (§4, m.public.overtimeAt); no layer gains; team LP 0 ⇒ defeat at once (PRTS 直接失败),
pool 0 ⇒ victory — the first end the server registers decides, §20.5)
R15 HIDDEN_CORE (if difficulty ≥ NORMAL and Σ activated layers > 350 solo / 1200 co-op and team LP > 1)
→ RESULT (per-player stats, titles, rounds passed, victory/defeat)
Solo FUNNY has 9 rounds (boss at R9); everything is read from data/config.json → modes[modeId].
6.2 Player state (PlayerState.js) — authoritative per player
{ playerId, seat, name, isBot, connected,
alive, lp, bandId,
funds, shop: { level, upgradePrice, slots: [ Slot ], frozen: bool, freeRefreshes: n, rewardOffer: null | { tier, source:'merge'|'special', label, queued, slots:[Slot] } }, // a special offer (凯瑟琳's 定向投放 …) by its source's label; queued = offers behind the shown one (§21.2)
hand: [ Piece|null ×10 ], temp: [ Piece|null ×5 ],
board: Map<'r,c', Piece>, // chess & token pieces on the normal field
bonds: { [bondId]: { count, active, tier, layers } }, // layers persist all match
effects: [ EffectRef ], // band/choice/team/garrison persistent effects & counters
counters: { refreshes, buys, sells, … },
stats: { … for results } }
Slot = { kind:'chess'|'item', id, price, sold:false, frozen }
Piece = { uid, kind:'chess'|'item'|'token', id /* chessId (golden id if elite) | itemId | tokenId */,
items:[itemPiece...] (chess only, max 2), count (token stacks), ownerUid (tokens), boughtRound,
meta: { round?: { r, n: { key: count } } } } // per-piece counters of the current round, module-prefixed keys (拉普兰德's first refresh, §21.1)
Intent handlers (validate → mutate → recompute bonds → push private view): buy(slot), refresh(), freeze(), levelUp(), sell(uid), move(uid, to), equip(itemUid, targetUid, replaceUid?), useArt(itemUid, row, col), destroy(uid), pickReward(idx), pickChoice(idx), setReady(bool). Every handler rejects when the phase forbids it (only PREP allows board/shop mutations; SP_DRAFT allows pickChoice only; reward pick allowed in PREP). Invalid intents return error with a code from shared/constants.js → ERR — the server never throws on bad input.
Summons (§20.1): an operator on the board sends its manually deployable summons to the hand — deploy-limit copies in one stack, no deploy slot (tokens.json placeable, gamedata.placeableTokens); moving or swapping it sends its placed summons back to their stack, withdrawing or selling it removes them. A range-bound summon (tokens.json ownerRange: 狼群, 流形) goes only on a tile of its owner's attack range (PlayerState._legal / summonRange); in a swap each piece is checked from the tile it ends on; an in-place re-orientation or promotion that leaves it outside sends it back to its stack with a toast (one still inside stays ASSUMED, user-confirmed 2026-10-03), and a re-orientation whose lifted summon would have no room is refused with HAND_FULL; on the other paths it leaves the board and returns at the next round start (§21.3).
Equipment (research 04 §2 + addendum, 09 §1.2): 2 slots per chess; a third item on a full carrier replaces the equipped item replaceUid names (the replace dialog's pick, official UseEquipUp.unloadInstId; absent ⇒ the oldest; a uid not equipped on the target ⇒ BAD_TARGET, nothing changes) and the replaced item is destroyed. Equipped items are otherwise locked: destroy (销毁) accepts hand / temp items only (BAD_TARGET 'equipped items are locked'); equipment goes back to the hand only when its carrier merges, is sold, or is destroyed by 突变细胞's transformation (the cell itself returns too; PRTS 帮助 "在失去该干员(干员出售、销毁、合并等)…时自动卸除", §21.1) — a sale is refused with HAND_FULL when there is no room for it; returned equipment is stowed before the operator a transformation gains and before a merge elite's summon cards, and every path that hands the player an item — returned equipment included — ends with the auto-merge, so no player holds two identical mergeable normal items (§21.7).
Economy/shop/pool/merge/hand rules: exactly as research 00-INDEX §3–§4 (income min(3+r,12); prices by tier; refresh 1; freeze toggles all; sell +1; items not sellable; upgrade prices per mode with −1/round; shared pool copies 12/14/18/16/8/5 (缪尔赛思 4) across all players; elite = 3 copies; odds = copy-weighted draw over unbanned visible chess with tier ≤ level; merge 3 (风丸 2) → elite onto the consumed deployed copy's tile, else to the hand (§20.11) + reward offer of 3 different free chess of tier min(level+1, 6) (copy-weighted; a tier short of different chess tops up from the tier below ASSUMED, §20.6); purchase completing a merge allowed with full hand; full hand blocks buy; overflow to temp; temp blocks ready).
Temp overflow (临时整备区, row 8; user playtest #3 item 3). Passive gains that find the hand full (merge elites, grants — the operator a 突变细胞 transformation gains included, §21.1 —, battle-result rewards, equipment returned by a merged / sold / transformed carrier) overflow into the 5 temp slots, and Ready is refused while any is occupied (official "直到溢出情况排除才可开始进行作战"). A temp piece is resolved — a chess sold back to the pool with its summons removed, items destroyed, a summon stack removed (it comes back at the next round start, PRTS "干员所属召唤物会于下一回合返还", §20.1) — only at the deadline of the first prep in which its player could act on it (PlayerState.tempDue(piece) vs prepsEnded; every write into a temp slot goes through _putTemp, which records it): a piece that arrived during a prep before Ready is due at that prep's end; one that arrived after Ready or during the <休整期结束时> effects, or outside PREP (COMBAT, 联防, SETTLE — battle-result grants, a SETTLE merge's elite, returned equipment —, the next ROUND_START, 机变) is kept, visible and usable (move to a free hand slot, place, equip, use, destroy, sell) through the next prep. Un-readying (setReady(false)) makes whatever arrived while ready due at the current prep. ROUND_START no longer wipes temp (it used to destroy what SETTLE had put there before the player ever saw it). A merge's elite whose merge consumed a deployed copy takes that copy's tile and facing (§20.11); otherwise it goes to the hand, overflowing into temp (also outside PREP). server/match/audit.js (tools/matchrun.mjs --check) tracks the arrivals itself and flags any temp piece that outlives its due prep. The UI frames the row on the board with the rule (§17.5).
6.3 Bonds & layers (bondsMeta.js)
count= number of distinct base chess on the board carrying the bond (BOARD);BOARD_AND_DECKbonds also count the hand; 绝技 counts elites on board; 调和 adds +1 to each core bond count, etc. (research 02).- 变形同构体: an operator wearing it together with an item that has a
giveBondIdis a member of that bond — counted like any member (BOARD: on the board only; once per operator), in the battle, and in the client's member list (tagged 同构) and bond chips (§21.11). tierfrom thresholds;active = tier ≥ 1.layers[bondId]persist all match; gains come from garrisons/bands/items/choices/bond effects; IN_BATTLE gains are collected fromBattleResult.layerGainsat SETTLE (disabled in unite/boss/hidden).- A bond holds at most
BOND_LAYER_CAP= 999 layers (the client'sMAX_GARRISON_STACK, §20.12): every writer —PlayerState.addLayers, the SETTLE merge,Battle.addLayers, the dev tools — addslayerGainRoom(before, n)= min(n, 999 − before); a gain at the cap adds 0 and dispatches nothing (no onLayers, nolayerGainhook, no 'layer' event), so the per-N milestones stop with the count. - Σ activated layers (hidden-core check) = Σ layers over bonds that are active at the moment of the check (end of R14 prep, per research).
6.4 Prep-phase effect dispatcher (effectsMeta.js)
A registry mapping effect keys → handler objects { onRoundStart, onPrepEnd, onGain(piece), onSold(piece), onRefresh, onPrice(slot), onBuy, onMerge, onLevelUp, onBattleResult }. Sources: garrison SERVER_* keys (per chess on board or hand as the key requires), band effects, item meta effects, bond meta effects (助力, 远见, 奇迹, 投资人…), choice/team cards. Content owners register handlers; the dispatcher owns ordering and the 投资人 "获得时 ×2/×3" multiplier. onBuy = a shop purchase (g.buy) only: a free pick or a grant never counts as 购买 (休露丝, 玛恩纳, 升华, 整备; §21.21). Equipped items run from a snapshot — the owned chess and each holder's items as that step begins — each only while it is still equipped on its still-owned holder (handlers move and destroy pieces: 突变细胞, 博士投影; §21.25).
6.5 Waves (waves.js)
Per match: pick stage (weighted among the mode's config.modes[m].stages, weight 50 each: 标准 FUNNY = 战场#01 act1autochess_m01 only, 险境 NORMAL = 8 stages, 绝境 / 终极 = 7 without 战场#01 — waves.js setupMatchWaves, test/match/stagepick.test.js), 3 of 6 typed enemy factions, disabled bond set (3 core + 4 add-on for NORMAL+, per research), boss (weighted), hidden boss. Per round: wave template (by mode & round) → replace placeholders per research 05 §3.2 algorithm → apply stat multipliers (research 00-INDEX §5 atk ×base×1.1^k, hp ×base×1.2^k, ABYSS speed ×1.15 from R3) → SpawnSpec[] + routes resolved on the stage. (Superseded by the official generator — §14 "Corrections from research 08/09" and the waves.js header.) A player's normal battle (and the bot rehearsal) runs withBounties(gd, round, wave, bounties, playerId): the bounty units inserted among their host action's own units, which are re-timed around them (client order); previewOf keeps one entry per action (wave.spawns + bountySpawns).
6.6 AI players (bot.js)
Deterministic-ish heuristic player used for (a) AI teammates, (b) auto-play for disconnected humans (only auto-ready + auto-picks, not shopping, unless "AI 托管" is toggled), (c) tests. Strategy (§21.6): pick a band (weighted by starting LP); each prep: take offers, sell dead bench weight, fill the board, complete affordable merges, level on the curve, then buy only merge progress / lineup upgrades worth more than the refreshes their price would buy (shop odds of completing held pairs), freeze for an unaffordable third copy (坎诺特 keeps 5 funds back); build around a committed focus core bond (a teammate's main bond on its strip counts against it; bonds the mode switches off never) + a second bond; value dealers by DPS vs the round's DEF / RES (an attack on every enemy in range counts double — 阵法术师 / 轰击术师), with the range the unit is deployed with; place by the exposure model + rehearsal on the server's deploy map (no 深水区; a tactician's 援军 within PlayerState.summonRange; g.move judges legality); items by effect (META §1.5: 突变细胞 on the least valuable normal single below 6阶, a cell left in temp gets a hand slot made for it, the operator its transformation gains is deployed by the next prep's lineup step like any owned unit — AI 托管 too; the buy loop's bench shed never sells a piece gained since the bot's last prep, §21.1); 机变 bounties by expected payout − expected LP loss over the offered cards; resolve temp — on a human seat under AI 托管 never by destroying an item while a bench operator can be sold for the slot —; ready. A bot's whole prep — shop decisions and layout planning (botPrepBeginSteps, 25–70 ms late in a 4-bot match), the layout rehearsal and botPrepEndSteps — runs in wall-clock slices of botSliceMs (8 ms) per scheduler callback on a real host (step generators that yield between whole actions, never with a transient board; the same actions in the same order, so decisions are unchanged; at once in virtual time).
7. Content modules (server/sim/content + match/effectsMeta handlers)
Each domain file server/sim/content/{tokens,bonds,garrisons,items,bands,enemies,bosses,devices,choices}.js exports install(battle) (battle side; called once per Battle by content/index.js → installContent) and registerMeta(registry) (prep side; called once at server boot by match/effectsMeta.js, API documented in docs/META.md). Kits live in content/kits/tier1..6.js (export default { [baseChessId]: (bb, chess) => Kit }). Content must be data-driven from blackboards (numbers never hard-coded when a blackboard key exists). Every effect gets at least one unit test in test/content/*.test.js using the test/helpers/battleHarness.js harness (provided by sim-core).
- kits: all 112 visible chess + hidden ones used by effects (盟约·辅助干员), default skill + talents + trait specifics; tokens.
- bonds: 23 (battle + meta), exact per-layer formulas from research 02.
- garrisons: 43 effect keys (IN_BATTLE via hooks; SERVER_* via meta registry).
- items: 56 equipment (+golden) + 3 Arts; 变形同构体 bond grant; VI-tier set/combos.
- bands: 40 strategies (meta + battle). 鸭爵 “神秘顾客” swaps 0–2 ground enemies (not the leader, its parts or bounties; on a pair boss field only the player's half — the boss / hidden
onBattleStartevent carriesroutesandside) in every teammate's normal / Final Assault / Hidden Core battle from R5 into the act2 鸭爵 / 高普尼克 / 流泪小子 / 圆仔 (lpr 1, 1 fund to the killer, 联防 included) (§21.5). - enemies: special-type behaviours (FLY, TIMES, ELEMENT, DOT, INVISIBLE, REFLECTION, SPECIAL) and notable individual abilities from
data/enemies.json; bosses 1–10 scripted — the huge leaders (假想敌:胄 ×2, 假想敌:管 ×2, 盐风主教昆图斯, 阿利斯泰尔, “萨米的意志”) are 自缚 + 无法被阻挡 (PRTS 天赋;content/bosses.js SELF_BOUND: a persistent noMove + unblockable buff from spawn) with their hit area (§19.4); 圆仔 and 鸭爵 are 无法被阻挡 (PRTS 天赋, §21.5); the knock-out forms follow PRTS — 深池逐火 余烬 (5 / 10 hits, 隐匿, blockable, back after 10 s), 转译基底·α (damage cancelled until its 2 s change into 寻仇者 / 特战术师 / 幽灵), the leaders' 重生, 守墓石像's statue and flight — and every 重生 clears what operators put on the enemy and restarts its skills from their initial cooldown (§21.4, §21.19); the mode's air units are 静态刚体, never moved (§21.9); leader parts (斩胄之剑 / 破胄之锤 / 碎铳之簧) pass every damage they take to their leader 1:1 as 无来源 HP loss (PART_TRANSFER1, PRTS "等量"), a 假想敌:胄 drone that dies costs it 2 % of the pool (DRONE_LINK_BASE 'pool') (§20.10, §20.13); devices/terrain. - choices: 机变 families (悬赏决策 bounties, 道具补给, 机密商店, 战术决策 team/personal buffs). 悬赏决策 = the official structure of its round (choices.json
bountyDrafts, 86 drafted cards: R3 a fixed set of six 两场 cards, R9 6 of a boss group's list, R11 6 of a seen 7-card list; §21.2); 机密商店 (R11 the official composition) and 战术决策 can offer the same card twice (shopDraft/tacticDraft); bounty cards carrydescRaw; no draft offers a multi-round card, and 教鞭's lasts two battles (choices.js MULTI_ROUND_BOUNTY_BATTLES, §20.6). 教鞭 = a random 战术特训 card (3 drawn, 1 taken).
8. Network protocol (shared/protocol.js is normative)
Transport: one WebSocket per tab at /ws, JSON text frames { t: '<type>', ...payload }. Client requests may carry rid (request id); the server's direct reply echoes rid (ok or error). Server pushes have no rid. Max frame 64 KB inbound; rate limit 40 msgs/s per connection (excess dropped with error RATE).
8.1 Session & lobby
C→S: hello {name, token?, version} · room.create {mode:'solo'|'coop', difficulty} · room.join {code} · room.leave · room.ready {ready} · room.setDifficulty {difficulty} (host) · room.addBot / room.removeBot {seat} (host) · room.start (host) · room.loadout {entries} (§16: any time in the lobby / room and during INFO_CHECK, then WRONG_PHASE; heavy rate-limit bucket; the match exposes it as m.private.loadout) · ping {c}
S→C: welcome {playerId, token, name, serverNow} · room.state {code, hostId, mode, difficulty, seats:[{seat, playerId, name, isBot, ready, connected}|null ×4]} · room.closed {reason} · pong {c, s} · error {rid?, code, msg} · ok {rid}
Platform additions (implemented): welcome.{resumed, version}, room.state.inMatch, error.detail; room.closed.reason ∈ 'timeout'|'shutdown'; WebSocket close codes 4001 (session taken over by another socket — client must not auto-reconnect), 4002 (no hello in time), 1008 (flooding), 1001 (shutdown). The host's room.start counts as the host being ready. After any (re)hello the server pushes room.state and, if a match runs, the match resends m.public/m.private (+m.field, or the field's b.start under client-side combat). Reconnect window: a dropped session stays resumable for 10 min (net.js NET_DEFAULTS.reconnectWindowMs; co-op seats keep playing their last lineup meanwhile, §6.6); a session that drops while its solo room's match runs stays resumable for the official config.constants.singleReconnectTime (86 400 s = 24 h; lobby option soloReconnectWindowMs; research 01 §1 "24小时内随时返回") — the untimed solo run simply waits. Only when the window expires does the drop become match.onLeave (co-op: 中途退出 = elimination; solo: the run ends 'abandoned'). The client keeps its token in sessionStorage plus a recent-token list in localStorage, so reopening the tab resumes. Everything lives in server memory: a server restart ends every room and run. The lobby⇄match interface (constructor opts, start/handle/onDisconnect/onReconnect/onLeave/dispose) is documented verbatim at the top of server/match/Match.js and is normative. server/data.js provides getData() + index getters (getChess, getBond, getGarrison, getItem, getBand, getEffect, getEnemy, getWave, getStage, getBoss, getToken, getConfig, getMode). Note: the skill trigger rule lives at chess.skill.trigger.rule in data.
8.2 Match
C→S (all carry rid): g.infoReady · g.band {bandId} · g.bandSkip · g.bandFocus {bandId?} (the strategy highlighted in the draft screen — what a timed-out turn takes; absent / null clears it; WRONG_PHASE outside BAND_DRAFT, ALREADY after the pick, BAD_TARGET for a band not allowed; §18.2) · g.buy {slot} · g.refresh · g.freeze · g.levelUp · g.sell {uid} · g.move {uid, to:{area:'board', row, col}|{area:'hand', idx}, dir?} · g.equip {itemUid, targetUid, replaceUid?} (replaceUid: which equipped item a third one replaces, §6.2) · g.art {itemUid, row, col, dir?} (dir ∈ UP|RIGHT|DOWN|LEFT from the deploy wheel, §3; absent ⇒ to.dir, then RIGHT; g.move onto the piece's own tile re-orients it; board pieces in m.private / m.field {prep:true} carry dir) · g.destroy {uid} (hand / temp items only; equipped items are locked, §6.2) · g.reward {idx} · g.choice {idx} · g.ready {ready} · g.emote {id} · g.watch {fieldId} · g.autoplay {on} · g.pause {on} (solo battles only, §14 Solo pause) · g.unitStats {seq?} (ROUND_START / SP_DRAFT / PREP, else WRONG_PHASE; eliminated: ELIMINATED) → push m.unitStats (§18.5) · g.leave
S→C:
m.public— full public state on every change (throttled ≤ 10/s):{ phase, round, lastRound, deadline, serverNow, modeId, difficulty, stageId, factions, disabledBonds, bannedChess, bossId, hiddenBossId?, teamLp?, bossHp?:{hp,max}, overtimeAt? /* FA / HC: ms epoch the overtime drain starts, §4 */, paused /* solo pause, §14 */, draft?:{order,turn,picks,skipsLeft,turnDeadline,turnSeconds /* one turn's length, 0 untimed; deadline = turnDeadline (§18.2) */,untimed}, sp?:{family,cards /* bounty cards carry descRaw?, §20.6 */,turn,picks}, players:[{playerId, seat, name, isBot, connected, alive, lp, bandId, shopLevel, boardCount, ready, bonds:[{bondId,count,active,tier,layers}], fieldId, status, pendingLp? /* COMBAT / 联防 of a normal round: the LP the player's own battle will cost at settlement so far, min(lpCapPerRound, counted leaks) — in 联防 min(lpCapPerRound, uniteLeft) — omitted when 0 and in every other phase (§17.5 live LP) */, uniteLeft? /* 联防: the leaker's enemies still standing — not spawned yet + alive + counted leaks, uncapped, live (§20.6) */}] , fields:[{fieldId, kind, players:[playerId...]}] }m.private— the recipient's full PlayerState view (§6.2 minus server-only data) on every change.m.field— field meta when watching starts:{ fieldId, kind, rect, stageId, units: [UnitInfo] }(the units on the field, knocked-out operators waiting to redeploy included —Battle.fieldMeta, §18.3)- NOTE: game time in
b.snap/b.evis carried asgt(the frame'stis the message type). b.snap—{ fieldId, gt, units: [[id, x, y, hp, maxHp, sp, spMax, flags, anim]] , dp, killed, total, down?, elem? }20 Hz. Optional, only when non-empty (§18.3):down: [[id, respawnAt (game s), respawnTime (s), state, row, col]]— knocked-out operators waiting to redeploy,state0 counting | 1 timer done, waiting for DP | 2 timer done, its tile taken — a safeguard, no ally deploys there (sim/constants.jsDOWN_STATE);row, col= the tile it lies on and comes back on (§21.24);elem: [[id, element, fill, cooldownEnd (game s), cooldown (s)]]— the one gauge a unit shows: the fullest (ties by the official element id 神经 < 侵蚀 < 灼燃 < 凋亡), fill with 2 decimals rounded DOWN and kept at 0.01–0.99 outside a burst (§20.8); during a 爆发冷却 the bursting element with fill 1 and the cooldown numbers (else 0, 0)b.ev—{ fieldId, ev: [ ... ] }event tuples:['spawn', UnitInfo],['atk', srcId, tgtId, projKind],['dmg', tgtId, amount, type],['heal', tgtId, amount],['skill', id, on:0|1],['die', id, reason](reason'killed','retreat','expired'…; only'killed'plays an operator's knock-down sound),['status', id, key, on],['fx', kind, x, y, extra]('hitCap' {id, n}= a cancelled 限伤 hit, drawn as nothing, §20.12;extra.form= the enemy's model form from then on —shared/protocol.js fxForm, state like a spawn: never dropped by the client, §21.4),['layer', playerId, bondId, n],['bounty', playerId, coins],['deploy', id]m.toast {kind, text}·m.ticker {text, id, type, priority, playerId}(the strip plays the highest priority first, §21.10) ·m.emote {playerId, id}·m.result {...}m.unitStats {seq, round, units: [unitStatsEntry]}— the answer tog.unitStats(seqechoed): every own board operator / summon by uid with{id, uid, defId, hp, alive, maxHp, atk, def, res, interval, blockCnt, moveSpeed, range?, base: {…same}}(shared/protocol.jsunitStatsEntry;range= an ally's live range grid, facing RIGHT, never a target-selection grid, §21.16) as its next battle starts (§18.5)
UnitInfo = { id, kind, side, ownerId, defId, name, tier, golden, spine, avatar, x, y, facing, dir, maxHp, skillIndex?, moduleId?, items?, form? } (dir = the ally's deploy direction; the renderer picks the model (Back for UP, mirrored for LEFT) and the ground wedge from it; skillIndex = the ally's equipped skill (§16): the renderer plays that skill's Spine clip (anims.skills[i]) and the audio its own ON_SKILL_START sound; moduleId = an elite ally's equipped module (uniEquipId | 'none'); items = an ally operator's equipped item ids (absent without any; never on enemies or summons): the bond popup lists a 变形同构体 wearer from them and the detail card shows its 同构 chip and 装备 (§21.11); all three also on the prep-scouting m.field units (Match.prepFieldMeta), so a teammate's unit shows its owner's loadout in the detail card; form = an enemy's current model form (§21.4, a view built mid-battle starts in it)).
8.3 Exact view shapes (normative for match ⇄ UI)
m.private:
{ playerId, seat, alive, lp, funds, bandId, ready, canReady /* false while temp non-empty */,
shop: { level, maxLevel: 6, upgradePrice, refreshPrice /* effective (0 when a free refresh is available) */, freeRefreshes,
frozen, slots: [ { kind: 'chess'|'item', id, price, basePrice, sold } | null ],
rewardOffer: null | { tier, source: 'merge'|'special', label, queued, slots: [ { kind: 'chess'|'item', id, price: 0, sold } ] } }, // §21.2
hand: [ Piece | null ], // length 10, index = hand slot
temp: [ Piece | null ], // length 5
board: [ Piece & { row, col, dir } ], // dir ∈ UP|RIGHT|DOWN|LEFT (§3; bench pieces carry none)
deployCap, deployCount,
bonds: [ { bondId, count, active, tier, layers, thresholds, countsHand } ], // sorted: active first, then layers desc
effects: [ { id, name, desc, iconKind: 'band'|'choice'|'team'|'item'|'garrison', iconId, counter?, counterText? /* a bounty: 还剩 N 场作战, §20.6 */ } ],
nextEnemies: [ { enemyKey, count, tag } ], // preview of the upcoming round's wave (after waves.js generation)
stats: { dmgDealt, kills, leaks, gold, refreshes, merges } }
Piece = { uid, kind: 'chess'|'item'|'token', id, golden: bool, tier, items: [ { uid, id } ] /* chess only */, count /* token stacks */, ownerUid /* token */ }
m.public.players[].status ∈ 'acting' | 'ready' | 'deciding' | 'combat' | 'done' | 'helping' | 'left' | 'dead'.
m.public.fields[]: { fieldId, kind: 'normal'|'unite'|'boss'|'hidden', players: [playerId…], live: bool } — fieldId is 'n:<playerId>' for normal fields, 'u' for the unite field, 'b1'/'b2' for boss fields. Clients g.watch a fieldId; default = own field (or the unite/boss field they are in).
flags bitmask: 1 blocked, 2 stunned, 4 frozen, 8 stealth (an ally's 隐匿 or 迷彩 — §20.2; an enemy's only while its 隐匿 is on: not blocked, not revealed — drawn solid otherwise, §21.4), 16 skillActive, 32 shielded, 64 invulnerable, 128 cold, 256 sleep, 512 flying.
anim small int: 0 idle, 1 move, 2 attack, 3 skill, 4 die, 5 stun, 6 deploy.
9. Client rendering contract (public/js/render)
import { createFieldView } from './render/app.js'
const view = await createFieldView(canvasHost /* HTMLElement */, { data, assets, audio })
view.setStage(stage) // draw tiles (procedural), devices
view.setCamera(kind /* 'prep'|'normal'|'unite'|'boss' */, { rect, side }) // animates camera
view.setPrep(privateState, { editable }) // render hand/temp/board pieces as chibis (spine or fallback); editable enables drag;
// a merge's new elite on the board plays fx.promote instead of fx.deploy (render/promote.js, §20.11),
// also after a battle (a merge between two preps: the last prep state is kept, §20.14)
view.enterBattle(fieldMeta) // switch to battle rendering for m.field
view.pushSnapshot(snap) ; view.pushEvents(ev) // interpolated rendering (100 ms buffer)
view.highlightTiles(tiles, style) // legal/illegal placement, range preview
view.on('pieceDragStart'|'pieceDrop'|'pieceDragEnd'|'pieceClick'|'pieceHover'|'tileHover', fn)
// pieceDrop: { uid, target: {area:'board',row,col}|{area:'hand',idx}|{area:'outside', clientX, clientY} }
// area 'outside' lets the DOM UI decide: released off the grid or over DOM covering the canvas (the shop bar), mouse
// and touch alike — there is no drag-to-sell, the piece goes back
// the drop target, its highlight and tileHover while dragging = the tile under the POINTER; an item dropped on a
// tile equips the unit standing on it (§18.1)
view.pieceScreenRect(uid) // the drawn body's rect { left, top, right, bottom, width, height, x, y } (tooltips / overlays)
view.tileScreen(row, col) // { x, y, s, poly } client px of a tile top (direction wheel geometry)
view.holdPiece(uid, {row,col}|null) // keep a dropped piece on a tile while its direction is chosen
view.setPieceDir(uid, dir) // show a prep piece facing UP|RIGHT|DOWN|LEFT (wheel preview / stored dir)
view.resize() ; view.destroy()
Visuals: oblique perspective board (tilted ~30°, trapezoid tiles), raised highland blocks with side faces, road/floor/forbidden styles per research 07 §7, pulsing red gates, blue objective, dashed hand pads. Units: Spine (Front/Back rule, feet anchored, shadow sprite), elite gold outline glow, tier chip, HP bar (green ally / red enemy), SP bar (skill ready glow), status icons, the element gauge row under the bars — icon + white bar of the remaining 元素值, element-coloured refill during a 爆发冷却 (§18.3, §20.8); enemies drawn × their official prefab factor (enemies.json modelScale, §20.4); an enemy's mode — the form of a sim fx (shared/protocol.js fxForm), or UnitInfo form for a view built later through render/app.js renderInfo — switches clip sets (render/units.js FORMS: 掠海漂移体 crawl, 暴鸰 bombed, 转译基底's forms, the 逐火 embers, the leaders' 重生, 守墓石像; closing clips timed from the fx dur), a one-off cast (PROJ[kind].once) plays once, and a local-only model's web alias is tinted (ALIAS_TINT: 灼热 / 炽焰源石虫, §21.14); damage numbers (phys orange-white, arts purple, true white, heal green); projectiles drawn per kind, hit sparks and melee slashes, skill activation bursts and auras (§17.3); death fade for enemies, summons and devices — a knocked-out operator stays on its tile in its held Die pose under a redeploy ring (§18.3); leak flash at the objective. Fallback when Spine missing/failed: avatar in a rarity-coloured diamond with bob/lunge tweens. 60 fps target with ≤ 120 units. Picking (which unit a press / hover / dropped item is on) and the drag target follow one rule, render/pick.js — the tile under the pointer (§18.1); the Spine store never hands out a skeleton whose unload is in flight (§17.1).
10. Client UI (screens & components)
Look & feel: research 06 §11–§12 (near-black green-grey panels, mint #4ed8af accents, gold prices, bracket frames, hexagon badges, bond discs, 7-segment countdown). Fonts: Noto Sans SC (Google Fonts with local fallback), Bender/Novecento self-hosted for numerals and Latin micro-labels.
Screens: Title (season art backdrop, 开始 → nickname), Lobby (独立模拟 / 同盟模拟 → create or join; recent rooms), Room (4 seats, invite code + copy link, difficulty picker (host), add/remove AI, ready, start), Briefing (INFO_CHECK: stage, factions, disabled bonds, banned chess, boss silhouette, ready x/4), Band draft (grid of 40 bands with icon/name/LP/effect; turn order list; skip), Game (Pixi field + DOM HUD: top bar with round/phase capsule/LP (live during the own battle, §17.5)/timer/ready; bond strip + popups; left team panel with LP/status/emote bubbles, click to watch; bottom shop bar (level card, 3–5 chess cards, item card, funds, refresh, freeze, collapse); detail panel on click/hover (header with bonds, then 特质 — the operator's own effect — right under it, 特性, stats (live: the battle's own numbers / the start-of-battle ones in prep, §18.5), skill, module, equipped items, talents: §17.5); enemy preview drawer (next round enemies with counts, factions); 机变 overlay (the official card: framed icon + name, the effect text — data descRaw — under it, the taker's round avatar at the top-right; two taps, §18.2, §20.7); merge reward overlay (the promotion reward, or a special offer — 凯瑟琳's 定向投放 … — as item / chess cards under its source's label, with the count of offers queued behind it, §21.2); phase banners; ticker (by broadcast priority, highest first; a leader-damage line plays only in its round and a player's newer one replaces the older, §21.10); toasts; emote wheel; settings (volume, quality, show damage numbers)), Result (victory/defeat, rounds, per-player lineup, stats, titles, back to room).
A bond the mode never activates (标准: official inactiveBondIdList) reads 本局禁用 on shop / reward cards (struck through), detail chips and the popup — no 在场 count, no 当前效果 (§21.7).
Interactions (research 09 §1.2 / §5, which supersede the first draft): shop card, upgrade and 机变 card are two-tap (select → confirm, §18.2) — a tap anywhere on the card, its confirm strip included, is the card's tap (shop cards have no ⓘ corner; the armed bar sits above the detail card; a shop card that cannot be bought right now opens its detail on tap, §20.7); a press / drop goes by the tile under the pointer (§18.1); drag bench↔board — a legal board drop (also onto the piece's own tile) opens the 4-direction deploy wheel (swipe UP/RIGHT/DOWN/LEFT with the rotated range striped orange; release in the centre, ✕ 点击取消 or Esc cancels); tap a unit's tile ⇒ underframe with 出售 +N (and 撤退 on the board; items/Arts in the hand / temp: 销毁 — equipped items are locked, §6.2) plus its rotated range; no drag-to-sell; drag an item onto a unit's tile ⇒ equip that unit (both slots used ⇒ the replace dialog ui/equipReplace.js shows the incoming and the two equipped items; the player picks the one to replace — it is destroyed — g.equip replaceUid; cancel changes nothing); Arts are dropped on a tile and get the direction step; right-click or long-press ⇒ detail (or a tap on a shop card while the bar is locked); R refresh, F freeze, D level-up, Space ready (in a solo battle: pause / resume, §14 Solo pause). All actions optimistic-free: UI waits for server state (latency is small).
Audio: autochess BGM per phase, UI SFX (buy/sell/refresh/level/merge/ready/timer), per-unit attack/hit SFX (throttled; an impact plays for a hostile attack's phys / arts / true damage only, never an operator's skill-mode file for a normal attack, ≤ 2 copies of one battle sound — §18.4), boss music in R14/15.
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.
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.
13. Local-client art (optional, host-side)
tools/local-extract/extract.py (Python: UnityPy + lz4 + Pillow; decodes Arknights' custom LZ4AK bundle compression via aklz4.py) extracts art that no web source has from a locally installed client (CrossOver Windows build or PlayCover iOS build) into public/assets/local/** and writes data/local-assets.json ({ groups: { '<subdir>': { name: { path, w, h, kind } } } }). Everything here is optional: clients must render correctly when the file or any entry is missing (fall back to the web assets / procedural art).
map/autochess/TX_autochessi_*— the real autochess board atlas (_Ddiffuse 2048² with floor tiles, gold-framed high-ground plates, REINFORCEMENTS bench pads, EQUIPMENTS pads, crates, gates;_N/_M/_Enormal/mask/emission;common_Ddevice icons;BGbackdrop).map/autochesssand/*— the sand-theme variant.ui/{common,battle,outer}/*— every sprite of the three official autochess UI bundles (tier chips I–VI, bond disc rings, frames, banners, HUD pieces, badges, backgrounds, mode art).emoticon/{basic,basic_2}/pic_<id>_battle.png— the 12 official battle emotes (shared/constants.js → EMOTES,emoteArtPath).guide/autochess_{home,shop,handbook}_N.png— the official tutorial pages; stored squashed to 1024² — display at 16:9.projectiles/*— battle projectile sprites.spine/enemy/<id>/— enemy models no web dump carries (灼热 / 炽焰源石虫,extract.py ENEMY_SPINES): manifestenemies[id].spineLocal(metadata from the committedtools/assets/local-enemy-spines.json); drawn instead of the tinted web alias when every file is listed (§21.14).module/<TYPE>— the official module (uniequip) TYPE icons (white glyphs; keys are the client's mixed-case file names, e.g.PRI-X,mar-x,isw-a), matched case-insensitively against a ModuleRecord'stypeNamebyassetUrls.moduleTypeIconUrl(local, typeName)(Greek type letters map to the file's Latin letter:ISW-α→isw-a) — shown on the 干员调配 module cards / module info, the detail panel's 模组 row and the shop card's module tag; lettered tiles / type text without them.
14. Client-side combat (v2 architecture — supersedes the combat-streaming parts of §4, §8.2, §9)
Why: like the official client, each player's browser simulates its own battle locally and uploads the result. The server no longer paces battles in real time nor streams b.snap/b.ev for combat, so a low-power host (e.g. a Windows mini PC) is enough, and combat rendering has no network jitter. Measured (4 human seats, HARD, real sim, in-process): server CPU per combat round ≈ 170–360 ms → ≈ 1 ms; server→client traffic ≈ 2–3.3 MB → ≈ 0.25 MB per round. Browser cost ≈ 0.05–0.1 ms per tick (≈ 3–6 ms per real second at 2×) on the main thread (no worker needed).
Modes. Match option clientCombat (default on; env SP_COMBAT=server → the legacy server-run + streaming mode, kept as a fallback). m.public.combatMode = 'client' | 'server' tells the UI which one runs.
Shared deterministic sim. server/sim/** is pure ESM runnable in both Node and browsers. Node-only code (reading data/*.json from disk, research fallbacks) lives in server/sim/nodeData.js, imported dynamically by simdata.js under Node only; the browser injects its own frozen copies of /data/{chess,enemies,tokens,stages,waves,bonds,items,garrisons,bands,effects}.json via setSimData(data) (getSimData() serves them). The HTTP server serves server/sim/** read-only at /sim/ (.js only, no listings, nodeData.js excluded under any spelling — the on-disk name decides, for case-insensitive hosts) and a generated /data.js stand-in of server/data.js (the content support module imports ../../../data.js; in the browser it returns getSimData()). A battle is fully described by a JSON BattleSpec (server/sim/spec.js buildBattleSpec: { v, battleId, fieldId, kind, seed, modeId, round, stageId, rect, timeLimit|null, players:[PlayerBattleInput], spawns, routes, flags, enemyOverrides, waveId, bossId, content, boss?: { poolHp, poolMax } } — the JSON round trip of the Battle options; ±Infinity → ±1e308, spawns at time Infinity dropped) and constructed with createBattleFromSpec(spec, dataSource, { sharedBoss? }) identically on both sides; the server builds its own battles from the spec too, so both run exactly the same input. No wall-clock or Math.random inside the sim. Verified: spec-built battles give identical result digests (resultDigest) in any construction order; whole client-combat matches are outcome-identical to server-run matches (same seed).
Authority per field.
- Normal field: the owning human's client is authoritative (if connected).
- Unite field (联防) and boss/hidden pair fields: the lowest-seat connected human in that field is authoritative; every other human in (or watching) that field receives the same spec and simulates it locally for display only.
- Bots, departed/disconnected players, fields with no connected human: the server simulates. Normal and 联防 fields headlessly (
fields.js HeadlessJob, with a[gt, killed, total(, left)]progress timeline —left: the 联防 leakers' enemies still standing, §20.6) in wall-clock slices ofheadlessSliceMs(8 ms) per callback on a real host — never one blocking run per field (3 bot fields ≈ 0.2–0.5 s of CPU at COMBAT start here, seconds on a mini PC); at once in virtual time; the result is released at the battle's natural end on the field clock (so round pacing and the teammates' progress look live). Boss fields in real time without snapshots (HeadlessPacer); when no field of a boss round has a connected human the whole round runs server-side as before (FieldRunner with streaming off). - Takeover: an authority that disconnects or leaves, or misses its deadline (
timeLimit / speedreal s + 15 s grace, normal/联防) loses the field: normal/联防 → the server re-simulates the spec from t = 0 (deterministic: the same result); boss → the partner's replica is promoted (b.start authoritative: true), else the server runs it, fast-forwarded to the field clock through aCreditPoolthat only credits damage beyond what the client already reported (the fast-forward is spread over the pacing intervals, ≤ 240 extra ticks each). A boss authority silent for 12 s is handed over the same way. The former authority getsb.end { reason: 'takeover' }and keeps displaying.
Protocol (additions; shared/protocol.js is normative).
battleId=<prefix>.<round>.<seq>.<fieldId>(Match._ccField;<prefix>= the match seed in base 36 plus-<matchNo>, the room's match number the lobby passes asopts.matchNo;<seq>counts every battle of the match; the informational.<fieldId>is dropped when the id would exceed the protocol's 64 chars): unique for the whole match although field ids repeat (n:<pid>every round,u,b1/b2in both boss rounds), and across the matches of one room. The client runner keys its local battles by it (the same id again = the same battle, no rebuild) and the server matches reports by it, so a lateb.progress/b.resultof a finished battle — or one that crossed into the room's next match on the same socket — is ignored instead of being credited to another battle.- S→C
b.start { battleId, fieldId, kind, spec, authoritative, startAt, serverNow, elapsed, speed, watch, done }— sent at COMBAT/UNITE/FINAL_ASSAULT/HIDDEN start to every human in the field (联防: to every human; COMBAT and boss rounds: eliminated humans get the first field as a display replica — research 09 "keep-watching auto-observes the first available field"), ong.watch, and on reconnect/resync.elapsed= game seconds on the field clock (a finished field: its end time); the client fast-forwards to it.speed= game seconds per real second (2; tests/tools may speed up). - C→S
b.progress { battleId, gt, killed, total, leaks?, left?, bossDmg?, by?, done? }— authoritative clients, ~1 Hz (boss fields 4 Hz). Normal/联防:leaks= counted leaks so far; 联防:left= per leaker its enemies still standing (spec.js uniteLeft, clamped by the server tofields.js uniteBillBounds; §20.6). Boss:bossDmg= cumulative damage of this field to the shared pool,by= the same per player,leaks= the field's cumulative LP cost (leaks ×lpr+ leader "扣除目标生命" effects;spec.js attachLpMeter). The server applies deltas (never twice). Stale reports (unknown battle, not the authority, after the match ended) are ignored, never answered with an error; the platform never answers a rid-lessb.progresswith an error frame (it would surface as a toast). - C→S
b.result { battleId, result }— authoritative client at battle end;result=compactResult(BattleResult)(the fields the match consumes; ≤ 4 players, ≤ 400 leaks, ≤ 64 unitsEnd, ≤ 160 unitStats, ≤ 40 layer gains;fitResultkeeps the frame ≤ 60 KB: unit statistics, then leak mods, then — boss only — leak entries go first). Structural checks inisBattleResult;server/net.jsroutesb.*frames to the match (lobbyrouteGame). - S→C
b.pool { hp, max, teamLp, acked: { [fieldId]: cumulative damage counted } }(boss rounds, ≤ 4 Hz; exact numbers, never rounded) — clients reconcile theirLocalBossPool: displayed pool = server hp − (local cumulative damage − acked). Pools never hold less than 1 HP (BOSS_POOL_MIN_HP:SharedBossPoolandLocalBossPool; a browser pool reads any finite rest below 1 as 0 — §20.5); the 'cleared'-result dust rule (a boss field whose client result is'cleared'while < 1 HP is left on the server empties the pool) only catches noise at that boundary. - S→C
b.end { battleId, fieldId, reason }—'cleared'(pool 0) /'forced'(team LP 0): the client callsbattle.forceEndand reports;'takeover': stop reporting, keep displaying. m.public.fields[]gainsprogress: { killed, total, done };m.public.combatMode;m.public.paused: boolean(always present; solo pause below).b.snap/b.evare not sent for combat in client mode.g.watchrules: see Spectating.
Result validation (server, fields.js validateClientResult). A client result is accepted only if structurally valid and plausible for its spec: every spec player reported and nobody else; finite numbers; killed ≤ total ≤ 4·spawns + 100; every leaked enemyKey is in the spec's spawns (multiset bound) or a key the spawned enemies' data mentions (content summons/splits, bounded by the total) — boss fields excepted (their leaks cost team LP through b.progress); counted: false only for enemies that never count (data notCountInTotal, schedule countInTotal: false, boss/part entries, content spawns); 联防: leaks + unspawned per (enemy, leaker) ≤ what that leaker sent in (nobody's survivors are billed to a teammate); per-bond layerGains ≤ min(60 + 4·round, 999 − the bond's starting layers) (§20.12), none when the spec disables gains, bonds must exist and be named by the player's bond snapshot, band, effects, units or items; coins ≤ Σ bounty coins of the spawns; perfect consistent with the counted leaks; unit states only for the player's own board uids with hpPct ∈ [0, 1]. The accepted result is rebuilt from whitelisted fields (leak mods/tags/sources from the spec — a content-spawned enemy (split, summon) keeps the mods of the schedule entry they equal, i.e. its parent's round multipliers and bounty id, so its 联防 re-entry and bounty match the server run; unit names from server data). Otherwise (or on any exception) the server re-simulates (normal/联防); a boss field is handed over instead (below). SP_VERIFY=all|sample|off (default off): all re-simulates every accepted normal/联防 result before accepting it (the server result wins on a digest mismatch); sample checks ~1 battle in 8 in a later callback and logs mismatches. Match.verifyStats = { checked, mismatches, rejected, takeovers }. Desync between display-only clients and the authority is tolerated: the authoritative outcome is what counts and is broadcast at settlement.
Boss rounds. The shared pool (SharedBossPool, co-op size = bloodPoint[difficulty], see corrections) lives on the server and is fed by the authorities' b.progress damage deltas (attributed per player through by; BOSS_HIT tickers from each player's damage to that round's pool, §21.10) and by server-run fields; a server clock (250 ms) applies the overtime drain (−bossOvertimeDrainPerSec team LP per whole real second past bossOvertimeAfter real s, measured on the field clock — gamedata.js bossOvertimeDue, §4), the silence watchdog and the end checks: pool 0 → b.end cleared (victory), team LP 0 → b.end forced (defeat) — the first one the server registers decides the verdict (Match._finalEnding) and after a forced end no report (an in-flight b.progress, the final b.result) credits the pool (§20.5); results are awaited ≤ 6 s (× timerScale) after a forced end. b.pool is the live channel for the pool and the team LP: a boss b.progress (4 Hz per field) and the team-LP losses never re-mark the whole public state — m.public (bossHp, teamLp, fields[].progress) and the players' merged-LP shares (_syncTeamLp) refresh at ~1 Hz (Match._bossPublic, BOSS_PUBLIC_MS), and at once when the round ends. A boss field's b.result ends it only when the shared pool is empty or after that b.end — except a 'cleared' result whose cumulative report covers what the pool still holds (within its 1 HP floor) while the plausibility budget (one field credits ≤ the pool per BOSS_MIN_CLEAR_GS 5 game s) has not caught up: it is held (f.heldResult; 999-layer boards kill a leader in 2–4 game s, §20.14) — the boss clock keeps crediting the report at the budget's rate, the silence watchdog and handovers skip the field, a re-sent copy is ignored, and the pool's end (_endFinal) completes the field with it; a forged report gains nothing a b.progress could not already get. Any other result — 'forced' / 'timeout' at t = 0, 'cleared' while the pool still holds more than the report covers — and any result failing validation would stop the pair's fight (and, with every field done, end the Final Assault as a defeat), so the field is handed over (Match._bossHandover) to the partner's display replica (promoted with b.start authoritative: true) or, without one, to the server (fast-forwarded to the field clock); the sender gets b.end takeover and is never that field's authority again (f.demoted). There is no synthetic zero result for a running boss field: a stats-only stand-in (fields.js syntheticResult, carrying the damage already credited, never charging LP) only fills a result that cannot be had otherwise — the Final Assault already ending (no result, or an invalid one, within the grace) or a server-run battle whose result() threw.
Solo pause (official PauseUp / ResumeUp; Match.setPause). C→S g.pause { on }: solo matches only (co-op → WRONG_PHASE 'co-op battles never pause'), and on: true only while a battle runs (COMBAT / FINAL_ASSAULT / HIDDEN_CORE with a live field, not once the Final Assault is ending — else WRONG_PHASE 'no battle running'); on: false is always accepted. While m.public.paused is true everything of the battle is frozen: the field clock (b.start elapsed, the boss plausibility budgets and the overtime drain), the authority deadline and the server-run release timers, the boss clock, the HUD deadline / overtimeAt (shifted by the paused time on resume) and the server-run pacers (FieldRunner / HeadlessPacer skip their intervals); the browser's runner stops its local clock. In solo battles the top bar shows a 暂停 / 继续作战 button (ui/hud.js PauseButton, ui/matchStatus.js pauseAvailable; Space toggles it), a paused overlay offers 继续作战 / 放弃模拟, and the HUD clocks freeze at the pause moment. A disconnect or leave resumes (the server takes the field over — nobody is left to resume it); the battle phase ending drops the pause.
Spectating (research 09 §3.1 / §6.3). No live spectating while your own normal battle runs (g.watch → WRONG_PHASE 'own battle running'; the UI toasts "作战中无法查看队友"). After your own battle ends you see "⌛ 作战结束,等待队友完成作战" with each teammate's live progress (m.public.fields[].progress); tapping a teammate's avatar expands 前往查看 → g.watch → b.start (display replica fast-forwarded to the field clock); the own row shows 返回战场. Prep: the same flow shows the teammate's board (m.field with prep: true, unchanged). 联防: every human receives the unite spec (helpers + observers). Final Assault: each human sees its own pair field; the other pair → BAD_TARGET 'other group hidden'. In 联防/FA fields the ‹ › pill switches the camera LEFT half / 全景 / RIGHT half ("你自己" / "👁 name" / "全景" / "无人在家"); there is no view switcher during normal combat. Eliminated players start each combat phase on the first field (auto-observe) and may watch any field.
Client runner. public/js/battle/runner.js owns the local Battles: fixed-step clock at the battle speed (60 ticks per real second at 2×, at most max(8, 4·speed) ticks per frame, bounded fast-forward of ≤ 240 ticks per frame when far behind; a new battle is caught up silently before it is shown), feeds the existing renderer through the same snapshot()/drainEvents() wire formats every frame (view.setLocalFeed shrinks the interpolation buffer to ~2 frames), publishes the field meta (m.field shape + local, battleId, players, sides, speed) into store.match.field and its state into store.match.battle (with leaks: { [fieldId]: counted leaks so far } of every normal field it simulates and, while the 联防 field is on screen, uniteLeft per leaker (§20.6), authoritative or display replica — the settle rule's count, /sim/spec.js battleProgress; published in the frame a count changes: the live LP of §17.5), sends b.progress/b.result when authoritative (results retried once on timeout; a result lost with the socket — the request failed DISCONNECTED / OFFLINE or timed out twice — is kept and sent again when the session is back online and whenever a b.start still names that finished battle authoritative: true; the server answers a duplicate ok and changes nothing; a server refusal is final), follows m.public.paused (solo pause: every local clock stands still, resume shifts it by the paused time), handles b.pool/b.end, fast-forwards on reconnect/observe, and survives tab backgrounding (a 250 ms interval pump keeps authoritative battles on their clock while hidden; the server deadline + takeover cover extreme cases). Catch-up frames keep the state events and every enemy form fx (keepsState); while hidden, the battle on screen holds them for the first frame back (hold; past HELD_MAX the superseded status / skill toggles are dropped, compactHeld, and only a backlog still past it re-enters the view from the field meta), and a battle that ended while hidden delivers them when the tab is shown; a catch-up frame or a hidden step drains its events every EV_SLICE (15) ticks, each batch stamped with its own game time (gt), so a late form fx reaches the render engine as late as it is; the game screen's pre-entry buffer (keepEarly, also while a re-sent field meta re-enters the field on screen) and the render engine's event queue (render/interp.js isCosmeticEvent) never drop a form fx either (§21.4). Sim content errors are logged as console warnings (the server logs its own). The sim loader fetches the ten data files (one retry each) and fails when any is missing — a battle on partial data would report a plausible wrong result — so the next b.start retries and the server's deadline covers the field meanwhile. public/js/battle/observe.js holds the observing rules and labels.
Corrections from research 08/09 (normative):
- Waves: official generator (research 08 §2): 3 of 6 types, each owns exactly 3 of the 15 round slots, SPECIAL the rest (shuffled per match); per round one type + one weighted special entry (half by round); each template SPAWN action becomes
clamp(roundHalfEven(n·P(t)/P(new)), 1, 5)units over the same window; placeholder actions of the other movement class are skipped. Stage per match from the mode's list (标准 = 战场#01 only, §6.5). No custom balance multipliers (data/tuning.jsonkeeps only titles); co-op boss pool =bloodPoint[difficulty]unscaled. - Ground pathing: official SPFA-style 4-direction search from the goal with crates as cost-1000 tiles, then straight-line smoothing (research 08 §3.4). act1 m02 crates #001–#004 inactive in 下半. Remake refinement (user playtest #2, narrowed by §21.15): route lengths and routes stay the official ones unless 0.1.0's road-over-floor preference route crosses strictly fewer non-blockable (floor / gate / 深水区) tiles — a corner touch is not a crossing; ties keep the official route. E.g. 战场#01's lower-gate enemies take the col-8 road, while 战场#04's keep the official diagonal from row 9 into row 10 (server/sim/grid.js header; test/sim/pathing-official.test.js, pathing-blockable.test.js).
- Observing: no watching while your own normal battle runs; after it ends (and in prep) tap a teammate → 前往查看 (local replica, fast-forwarded). ‹ › only inside 联防/FA fields; FA: own pair only; eliminated: free.
- Prep interactions: tap a unit → underframe with 出售 +1 (and 撤退 on board); items/Arts → 销毁; shop and upgrade are two-tap (select → confirm; the remake's 机变 cards too, §18.2); strategy draft forbids a strategy already taken by a teammate (队友已选).
- Emotes: 6 themes × 6 battle emotes (research 09 §4.2), image-only bubbles beside the sender avatar, 1 s cooldown, 3 s bubble;
g.emote {id}with the official emoji id.
15. Official 3D board scene (v2 visual target)
Findings (2026-09-28): 卫戍协议 levels have no scene bundle (mapId null; the official hot-update list contains none). The client builds the board at runtime from the stage grid using the map theme arts/maps/map_autochess (materials MT_autochess (albedo TX_autochessi_D 2048², normal _N, metallic/gloss _M, emission _E), MT_autochess_common (common_D/_E: device/gate icons), MT_autochess_Transparent (Transparent_D/N/M/E), MT_autochessi_BG (TX_autochessi_BG)), the background meshes arts/maps/map_autochess/bkg_mesh.ab (S_Background_common, S_Background_shadow) and arts/maps/common/meshes/s_background_common.ab, device meshes (s_common_box_01 crate, s_wind_device blower, …), plus the standard gate/objective effects. The level's options.configBlackBoard carries the official camera framing (default_pos_x/y, default_size, enemy_pos_x/y, …).
Target: a real 3D board layer rendered with three.js (vendored ESM, no CDN) underneath the existing Pixi layer:
- Geometry generated from
data/stages.jsonlike the official builder: LOW tiles = top quads; HIGH tiles = raised blocks with side walls; forbidden/void, fences, gates (red), objective (blue), hand/temp pads, special terrain; UVs mapped onto the official atlas regions (reuse/extendpublic/assets/local/map/autochess/tiles.json); normal + emission + metallic maps; lighting tuned to match official screenshots (key directional light, ambient/hemisphere, soft contact shadows); official background meshes & texture; device meshes (OBJ exported bytools/local-extract) with their textures; animated gate/objective effects. - One camera model drives both layers:
public/js/render/projection.jskeeps its public API (worldToScreen,screenToWorld/tile picking, presets, transitions) but derives it from a three.jsPerspectiveCamerainitialised from the official configBlackBoard framing, so Spine units, FX, drag/drop and hit-testing stay aligned with the 3D board. - Must degrade gracefully: when local-client art or WebGL2 features are missing, fall back to the current 2D atlas board; keep 60 fps (static geometry merged into few draw calls; textures mipmapped; DPR-aware).
- Implemented (
public/js/render/board3d/*,render/app.js): three.js is downloaded only when the local-art manifest lists the board atlas; the built 3D area, the drawn 2D rows and the lit rect followviewKind(kind, {rect})(the field the camera actually shows: a 'prep' camera on rows ≤ 6 =bossPrep); the background plane is extended with mirrored copies and the clear colour matches the fog (no visible edge at any aspect); battle crates live in their own group (survive scene rebuilds) and follow the board mode (switchableBox: 3D mesh ⇄ 2D box on a mode switch or a lost WebGL context); quality 'low' drops shadows;?board=2d|3doverrides. The direction wheel's range stripes sit between the board canvas and the unit canvas. - Done: Final Assault / Hidden Core prep takes place on your own half of the boss field (research 09 §1.2): pieces shown at boss-field positions (row − 7; right-hand player mirrored col → 20 − col, RIGHT ↔ LEFT), drop targets and wheel directions mapped back to board coordinates (
prepCamera→bossPrepin public/js/ui/gameLogic.js; covered by the playtest2 browser tests).
16. Operator loadout: skill & module selection (v2.1, user playtest #1)
Official rule (bwiki 盟约, 更新公告 5114): before a match the player cannot change an operator's tier, but can change the equipped skill and module of each chess; some non-default choices are stronger. Scope in the remake (no accounts ⇒ everything the chess's status unlocks is selectable):
- Skills: any skill unlocked at the chess's status (
evolvePhase: E1 ⇒ S1–S2, E2 ⇒ S1–S3) at the chess's skill level (normal Lv4, elite Lv7). Default =defaultSkillIndex. - Modules: only matter for the elite (golden) chess (
equipLevel1, or 3 at T6): any module of that character (or none); default =defaultUniEquipId. Normal chess have no module (equipLevel 0).
Data (data/chess.json, built by tools/build-data.mjs): every chess keeps skill (the default, unchanged shape) and gains skills: [SkillRecord…] (index, skillId, iconId, name, desc/descRaw, skillType, durationType, duration, spType, spCost, initSp, maxChargeTime, bb (at the chess's skill level), bbStr, rangeGrid, trigger {rule, customRangeGrid} resolved for THAT skill index, isDefault). Golden chess gain modules: [ModuleRecord…] = { uniEquipId, name, typeName, icon, isDefault, attr: {maxHp, atk, def, res, aspd, cost, blockCnt, respawnTime…} (flat adds at the chess's equipLevel), traitOverride: {desc, descRaw, bb} | null, talentChanges: [{talentIndex, name, desc, bb}] } plus statsBase (stats WITHOUT any module); stats stays = stats with the default module (back-compat).
Loadout wire & state: the client keeps a per-browser loadout { [baseChessId]: { skill: index, module: uniEquipId | 'none' } } (localStorage) and sends it with C2S room.loadout { entries } (validated: known visible chess, legal skill index for that chess's normal AND elite status, legal module id). The lobby stores it on the seat; at match start Match receives seats[].loadout; PlayerState keeps it immutable for the match; board pieces are resolved to {chessId, skillIndex, moduleId} when building PlayerBattleInput.units[] (fields skillIndex, moduleId; BattleSpec carries them). Bots use defaults. m.private exposes the effective loadout so the UI (shop cards, detail panel) shows the chosen skill/module.
Sim: simdata resolves a unit def for (chessId, skillIndex, moduleId) (cached): skill = the selected SkillRecord; stats = statsBase + module attr (elite) ; trait/talents = base with the selected module's overrides. Kit contract (backward compatible): a kit function (bb, chess) => { skill?, skills?: { [skillId]: SkillSpec }, talents, trait } receives chess.skill = the selected skill and bb = its blackboard; the loader uses skills[selectedSkillId] if present, else skill only when the selected skill is the chess's default, else the generic skill spec — talents/trait from the kit always apply. Every visible chess must get hand-authored specs for all of its selectable skills (tracked by a coverage test).
UI: a 干员调配 screen reachable from the lobby, room and briefing (INFO_CHECK): filter by tier/bond/class, per chess choose skill (icon, name, SP info, desc at Lv4 and Lv7) and elite module (official type icon from the local-client art when extracted (§13), else a lettered tile; name/type, stat bonus, trait/talent changes), 恢复默认, persisted; the chosen skill/module shown on shop cards and in the detail panel (a teammate's unit — 前往查看 in prep, 联防 / 最终攻势 fields, an observed battle — shows its owner's skill/module from its UnitInfo skillIndex / moduleId, gameLogic.js unitLoadout) — which also shows the stats, 特性, talents and attack range the unit fights with (shared/loadoutRecord.js loadoutRecord / attackRangeGrid, the same composition the sim's getChess(id, loadout) uses; the deploy wheel's range preview too) — the range it is deployed with: a selected passive range skill (引星棘刺 S3 3-9) and the 特性's permanent 攻击距离 (信仰搅拌机 SPT-Y +1) included, over all 1284 loadouts equal to the battle's range at deployment; in battle and prep the card draws the live range (§21.16). In battle the renderer plays the equipped skill's own Spine clip and ON_SKILL_START sound (UnitInfo.skillIndex; data/assets.json lists icons, clips and sounds for every skill index).
17. User playtest #3 (v2.2): models, picking, FX, rules, HUD
Nine reports from a real co-op session. Where each is handled: #1 lost operator models → §17.1; #2 the LP did not move while enemies entered the own blue gate → §17.5; #3 equipment pushed into the second bench row vanished after the battle → §6.2 temp overflow (server) + §17.5 (UI); #4 蕾缪安 S3 fired every shell at once → §17.4; #5 回环射手 attacked like a fast shooter → §17.4; #6 bullets / skill effects hardly visible → §17.3; #7 a click selected the unit one row lower (equipment too) → §17.2 (superseded by §18.1); #8 the operator's own effect (特质) sat at the bottom of the detail card → §17.5; #9 spinners next to texts → §17.5.
17.1 Lost operator models (#1) — the Spine store (public/js/assets.js RefLru)
Cause: PIXI.Assets.unload is asynchronous (the loader cache entry is dropped and the textures destroyed a microtask later), so a skeleton evicted and re-acquired in the same task came back as the doomed object and turned invisible for good (a tier chip without a body; the dead data stayed cached, so every later copy of that operator was invisible too). It happened at every battle → prep switch: enterPrepMode → clearViews made the reference count 0 for an instant, the LRU applied its "no scene on screen" budget (0 bytes) at that instant and dropped the bench models idle since the battle started, then setPrep took them back at once. Rules now: an unload may return a promise and a load of a key whose unload is still in flight waits for it (unloadSpineData returns one; stats().spine.unloading); the spine store runs eviction in one pass SPINE_EVICT_DELAY_MS (1 s) after a release (a scene switch takes its models back first); the quiet budget applies only after SPINE_QUIET_DELAY_MS (3 s) with nothing referenced (lobby / room). The count-cap path (60 skeletons) waits the same way. Tests: test/render/assets.test.js (fake PIXI.Assets with the same async unload), test/render/models.browser.test.js (models alive after long battles, drag / wheel stress).
17.2 Picking and the drag target (#7) — superseded by §18.1
v2.2 answered "a click selected the unit one row lower" with body picking: every path took the drawn chibi (a head / body shape per unit, measured from its posed Spine model) and, where two bodies were close, a 1-px render probe of the pixel under the pointer; the dragged ghost stood on the pointer (on touch 0.6 tile above the finger) and an item went to the operator whose sprite was under the plate. User playtest #4 item 1 rejected it ("感觉你想复杂了,地上都画好了一个一个方格,点击对应方格就选中那个方格的人物就行"; the model moved up away from the finger): picking is by the tile under the pointer and the dragged model is held under the pointer (§18.1). The shapes, the probe, the touch lift, pieceDragOver and the DOM-side pickPieceAt / equipRetarget are gone.
17.3 Battle FX (#6) — render/fx.js, style.js PROJ, textures.js
- Projectiles per kind (
PROJ[kind].look), sized in tiles × the camera's px per tile (the same on the 3D board and the 2D fallback, 1280×720 to 1920×1080):arrowa bright tracer with a glowing head, a muzzle flash aimed at the target and a hit flare;bolt/orb/enemya glowing ball with a particle trail (arts purple with a burst + shockwave, heal green with a sparkle, enemy red);bomb/loba shell on a visible arc with a ground shadow, smoke and embers, exploding on landing; 阵法术师 / 轰击术师 strikes are instantbeamlines to every victim (§21.18);droneBomb(暴鸰) a falling shell at 5 tiles/s (§21.14);dronea small cyan dart;boomerang(回环射手) a spinning boomerang with afterimages that flies out to the moving target atPROJECTILE_SPEEDS.boomerangand back to the thrower's current position atBOOMERANG_RETURN_SPEED(the sim emits no event for the return: the renderer models it; dropped when the thrower's view is gone or dead). A shot lingers 0.08 s in its target so fast shots still read. Flight times use the sim's own speeds, loaded at run time from/sim/constants.js(style.js PROJkeeps a test-guarded copy:test/render/fxproj.test.jsfails when they differ), so a shot lands with its damage number. - Hits and skills: a melee blow draws a thick crescent slash swept along the blow (attacker → victim on screen) in the damage-type colour with a white core, plus sparks; skill activation = gold light pillar with a white-hot core, flash, shockwave and hex ring on the ground, rising motes; an active skill keeps a slowly turning hex and a soft glow under the unit. Blasts share one explosion (fireball, flash, shockwave, coloured ring, sparks, smoke;
heavykinds — bombard, airstrike, meltdown — add debris and a scorch mark). Beams and chain lightning: glow + body + white core, flare at the target. - Anchoring: an fx whose
extra.idnames a unit is drawn on that unit (following its rendered position) only when the event's (x, y) lies within 0.75 tile of it (or has no position); otherwise at (x, y) — the sim puts the caster inidof many area effects.FX_KINDSentries flaggedpt(bombard, bombardShell) always happen at (x, y) (theiridis the shooter; 蕾缪安's bombard used to explode on herself). - 蕾缪安 S3 contract:
'lock' {x, y, id: enemy, src: shooter}→ a reticle following that enemy (its view byid), at the spot it left once it is gone;'bombardShell' {x, y, id: shooter, r, t: flight game s, i}→ a launch streak leaving the shooter and a shell dropping onto (x, y) aftert(a flight ≥ 0.45 real s climbs out of her first), with a closing warning ring and the blast radius throbbing; it takes the shooter's nearest free lock;'bombard' {x, y, id: shooter, r}at the same spot → the heavy explosion and the end of that lock. A lock also ends on the S2'crit'from the same shooter, when the shooter is gone (locks no shell took yet), orLOCK_T= 5 game s after that shooter's last lock / shell / bombard (an idle timeout, not an age: ammo grants and stuns stretch an S3 far past 5 s); ≤ 48 reticles. - Cost: particle, projectile and lock records are pooled (the per-frame emitters allocate nothing); a freed particle is a zero-size transparent quad (Pixi's ParticleRenderer ignores
visible); cosmetic extras (trails, muzzle flashes, afterimages, debris, scorch marks, motes) are off at quality 'low' and at adaptive load level ≥ 2, and trails stop at 75 % of the particle cap. Measured: the 120-unit stress scene and the crowded boss-m02 replay stay at 60 fps with no measurable per-frame increase. The FX atlas is 1024×512 with non-overlapping frames (the old light pillar carried two status icons at its foot). The FX are procedural: the local client's per-weapon projectile sprites are not used (the sim'sarrowalso covers gun snipers, and friends may not have the local art).
17.4 回环射手 (#5) and 蕾缪安 S3 (#4) — the sim (official PRTS notes; numbers from the data)
- 回环射手 boomerang (
professions.js loopshooter,ai.js throwBoomerang; PRTS 跃跃 "投射物飞行速度15,返回时飞行速度3.75", "必须回收全部回旋投掷物才可以进行下一次攻击"): projectile'boomerang'(resolveProfilekeeps it over the data's generic'arrow') flies out atPROJECTILE_SPEEDS.boomerang= 15 tiles/s and hits on arrival, then flies back to the thrower's current position atBOOMERANG_RETURN_SPEED= 3.75 tiles/s without damage; the thrower attacks only while it holds its boomerang (every one it threw caught) and its attack cooldown is ready, so the real interval is the longer of the two. A target that died mid-flight is not hit (the boomerang still flies to its last position and back); a thrower knocked out / withdrawn loses it and a redeployed one holds a fresh one. 跃跃 S2's extra boomerangs share the one flight (cnt hits, the talent rolls per hit). Event contract unchanged:['atk', thrower, target, 'boomerang']. - 蕾缪安 S3 礼炮·强制追思 (
kits/tier6.js lemuen; PRTS S3 note — the timing is not in the blackboard): one lock every 0.5 s while an enemy is in range (unlocked targets first, then lowest DEF), each costing a round; the lock mark follows its enemy and stays where it left (death / leak). After the skill ends ONE shell every 0.3 s in lock order (PRTS "以0.3s为间隔"), aimed at a random point of the 0.4-side square around its mark (±attack@emit_offset0.2, drawn with the battle rng in her facing frame so a turned board plays alike), landing 0.3 s after it is fired (flight ASSUMED: the data and PRTS give none; the PRTS count of ≤ 33 fits a first impact one interval after the end); each shell hits every enemy withindist_2once —proj_atk_scale_1withindist_1, elseproj_atk_scale_2— with the ATK cached when the skill ended; at most 10 s of shells (≤ 33). Knocked out / withdrawn after the end: shells in the air still land, the rest are dropped; knocked out while locking: no bombardment. fx:'lock'per lock,'bombardShell'when a shell is fired,'bombard'at its impact (§17.3). - Tests:
test/sim/professions.test.js,test/content/kits_t1t2.test.js(跃跃),test/content/kits_t6.test.js(shell sequence, spacing, cap, knock-out, rotation invariance).
17.5 Game HUD (#2, #3, #8, #9) — ui/hud.js, teamPanel.js, underframe.js, detailPanel.js, data.js
- Live LP (#2): in a normal round the top bar's LP tower and the own team row show
lp − min(lpCapPerRound, counted leaks)as soon as the own battle's enemies enter the blue gate — the number in red followed by a −N plate that pops again on every leak, clamped at 0, with a tooltip (ui/hud.js liveLp/pendingLoss/pendingTip). The count isownLeaks(local, server)= the larger of the local runner'sstate().leaks[own field](§14) andm.public players[].pendingLp(§8.2; the authority'sb.progressleaks, the field's result, the recorded result in 联防) — both only grow within a round, so a paused display replica never freezes the number. The pending part is dropped as soon as the settledm.privatelands (itslporstats.leakschanged;Match.flushsends it before the SETTLEm.public), the phase leaves COMBAT / 联防 or the round changes — never subtracted twice. 联防 (revised by §20.6, user playtest #6 item 7): a leaker's count is its enemies still standing — the local replica's while the 联防 field is on screen, else m.publicuniteLeft— shown uncapped as ×N in the capsule and in the leakers' team rows, and the LP tower shows lp − min(10, N), tagged 联防中. Teammates' rows use theirpendingLp(teamPanel.js rowLp); server-run fields (bots, taken-over fields) report none before their result is released. Boss rounds are unchanged (the team LP moves live throughb.pool). - Temp row (#3, UI): while
m.private tempholds pieces during the own prep, a dashed red frame surrounds the 临时整备区 row on the board and a label says how many are waiting, what to do and when they go (underframe.js TempRowNotice/tempRowFrame, placed throughview.tileScreen— it follows the camera, the mirrored Final Assault prep and the DOM fallback; no pointer events; the label hides while dragging or placing). Its wording follows the server rule (§6.2,tempRowRule(ready)): not ready — "放入整备区或战场、配发或使用后才能准备;休整期结束时仍在此处的将被销毁"; ready in PREP (so whatever lies there arrived after Ready) — "已准备就绪后进入的单位保留到下个休整期,届时仍在此处的将被销毁(取消准备则在本休整期结束时销毁)". The 准备就绪 button shows "临时整备区 N 个单位待处理" under it (not only on hover; the effects column moves down a line meanwhile,.gm.has-readywhy) and Space toasts the reason (hud.js TEMP_RULE/tempReadyReason). - Detail card order (#8) (
detailPanel.js CHESS_SECTIONS): header (portrait, name, tier, class, bonds) → 特质 (the operator's own effect — garrison trigger chip such as 休整期结束时 + description, a compact gold block, visible without scrolling at 1920×1080, 1280×720, iPhone 14 and 640×360) → 特性 (class trait) → stats (a compact 4×2 grid) + attack range → 技能 → 模组 → 装备 → 天赋. On phones ≤ 460 px tall the portrait is shorter and the romanised name is hidden. - Busy indicators and texts (#9): every text of the game (operators, skills, bonds, items, enemies, 特质 …) is static data,
/data/*.jsonloaded once per page and never fetched during a match; the "spinners" the player saw were busy indicators of requests in flight next to texts — and the 特质 chip's type icon: it was picked by the trigger, so every <休整期开始时 / 结束时> 特质 showed the spoked 特异化 glyph (it reads like a loading spinner) before its text; the chip now shows the garrison's own officialeventTypeIcon(detailPanel.js garrisonTypeIconKey: crossed swords 作战能力, gold 整备能力, chain link 持续 / 单次叠加, spokes only for 特异化). Now: nocursor: wait / progressanywhere; a loadingButtonkeeps its icon and shows a thin sweeping bar along its bottom edge after 250 ms (no spinner ring); a 机变 card in flight shows a 选择中 strip until the pick appears inm.public(choiceOverlay.js pickBusy); the 作战结束,等待队友完成作战 hourglass is still; the hexagonSpinneris kept for genuine loading screens only (entering the match, the field view, connecting, guide images, the first 干员调配 load, which says the data loads once per page).main.js warmGameDatadownloads every in-match file (gameComponents GAME_FILES) in the background once the player is in a room, and the match screen waits for them;data.jsretries a transient failure (network error, HTTP 5xx / 408 / 429) after 600 ms and 2 s (RETRY_DELAYS_MS) — a 404 or invalid JSON is final. - Tests:
test/ui/playtest3.test.js,test/match/runner.test.js(live leak counts),test/match/temp.test.js(§6.2),test/docs-consistency.test.js.
18. User playtest #4 (v2.3): picking by tile, confirms, timers, battle state, content
Thirteen reports after playing v2.2. Where each is handled: #1 picking should simply follow the tiles and the dragged model moved away from the finger → §18.1; #2 a confirm step for 机变 picks → §18.2; #3 a countdown although playing alone → §18.2; #4 the strategy draft's 10 s turn out of step with the top-right countdown → §18.2; #5 the special 维式重锤 and 突变细胞 were sold in the shop → §18.6; #6 纯烬艾雅法拉's skill sound rang outside her skill → §18.4; #7 the operator card showed fixed numbers → §18.5; #8 no official element gauges → §18.3; #9 knocked-out operators vanished instead of lying on their tile with a redeploy ring → §18.3; #10 the enemy drones' behaviour → §18.6; #11 赫默's drone deployed at the start instead of by her skill → §18.6; #12 the 炎佑 dragon returned to a fixed point and its flame was no continuous jet → §18.6; #13 once in a while a huge piece of UI covered the screen for a second or two → §18.7.
18.1 Picking by tile, the model under the pointer (#1) — render/pick.js, render/app.js, render/drag.js
- One rule: the ground tile under the pointer decides (
render/projection.js pickTile, raised tops first). Prep presses / clicks / right-click / long-press / drag starts / hover (app.js pieceAt) and the enemy preview pen (penUnitAt) take the unit standing on that tile (board, bench row 7, temp row 8;pick.js pickOnTile). An empty tile selects nothing; a press on a unit's head drawn over the tile behind it is a press on that tile; a raised tile in front hides the near 0.17–0.24 tile of the one behind it, and a press there is on the raised tile — what is drawn. Several units on one tile (the pen's clusters, a piece held for a moment): the nearest to the pointer's point on the tile, ties → the front-most. - Battle (
app.js battleUnitAt→pickBattle): an ally on the tile under the pointer — a knocked-out operator lying on its tile (§18.3) included. An enemy withinENEMY_REACH0.6 tile ASSUMED, using whichever is nearer: its ground position from the pointer's point on the ground, or its drawn body on screen (the upright line from its feet up to its head, in its px per tile) — so a press on a tall enemy's or the boss's torso or head picks it; a flying enemy uses its drawn body only. When an ally tile and an enemy both qualify the nearer (in tiles) wins; ties → the ally, then the front-most. Dead and dying units are not picked (a knocked-out operator waiting on its tile is). - Drag: the dragged unit is held under the pointer, mouse and touch alike — its drawn (lifted) feet
DRAG_HOLD_TILES= 0.45 tile (in its own px per tile) below the pointer [ASSUMED: the v2.1 offset], so the pointer rests on the chibi; an item plate is centred on the pointer. The drop target, its highlight,tileHoverand the direction wheel's tile are the tile under the pointer. An item dropped on a tile equips the unit on that tile (ui/gameLogic.js canPlace/dropIntent; an empty tile says 请将装备拖拽至干员身上). A release over DOM covering the canvas (the shop bar) is'outside': the piece goes back. - Touch: the field canvas cancels
touchend(app.js onTouchEnd, non-passive). A tap's compatibility mouse events and click came aftertouchend, hit-tested at the finger again — on the underframe the tap had just opened over the unit's tile (clamped under the top bar for rows 11–12 at 844×390), which pressed 撤退 / 出售. - Notched phones: the shop bar keeps its place at the bottom edge instead of rising by the bottom safe-area inset (
css/devices.css .gm__hud > .shopbar): raised by it the bar covered ~3/4 of bench pads 2–9 at 844×390 with the iPhone insets, and bench pieces are pressed and dropped on by their tile. (The v2.3 figures — 23 % of those tiles free before, 71 % after — are superseded by §19.8: the prep camera now clears the HUD, every bench pad 100 % free on phones.) - Removed: the pixel probe (
drawnAt,hoverProbeAt), body shapes (pickShape,PROBE_MARGIN,BODY_H),TOUCH_LIFT_TILES, the dragdropPointhook,pieceDragOver,ui/gameLogic.js pickPieceAt/equipRetarget.UnitView.bounds()is a plain body rectangle again (tooltips andpieceScreenRectonly). - Tests:
test/render/pick.test.js,drag.test.js,unitview.test.js,test/ui/gameLogic.test.js,test/ui/playtest4.test.js(shop bar); browser:test/render/models.browser.test.js(every tile of every board, high ground, bench and temp; battle allies and enemies incl. torso and head; the model under the pointer at every drag step; the wheel on the pointer's tile; equipment by tile; phone touch incl. the ghost click and the shop-bar release),test/render/downelem.browser.test.js(a knocked-out operator picked on its tile),test/ui/playtest2.e2e.test.js(the top-row tile under the bond strip; the notched phone's bench).
18.2 Confirms and timers (#2, #3, #4) — ui/choiceOverlay.js, server/match/Match.js, screens/bandDraft.js
- 机变 cards take two taps (like the shop's cards): the stateful
ChoiceOverlaywraps the pureChoiceView. A first tap on a pickable card (bounty = extra enemies, 道具补给 / 机密商店 items, tactics) only selects it — gold frame, a 确认选择 · 再次点击 strip, the other cards dimmed, 确认选择 in the header; a second tap on it, or 确认选择, sendsg.choice. Another card moves the selection; Esc or a press elsewhere drops it; so do the card being taken, the turn moving on and a pick in flight. Cards stay buttons (Tab / Enter). Each card shows its full effect text (§20.7). The other picks were already two-step (晋升奖励 in the shop bar, the strategy draft's 确认选择, 确认替换). - A single human is untimed (
Match.loneHuman: exactly one human seat at the start →Match.soloUntimed): 独立模拟 and a 同盟 room started alone or with AI teammates only publish no INFO_CHECK / BAND_DRAFT / SP_DRAFT / PREP deadline, and BATTLE_CHECK / ROUND_START / SETTLE run silently — the timers only ever made humans wait on each other. The co-op rules stay (shuffled draft order and skip, 6 机变 cards, 联防); pause stays solo-only; a disconnected lone human's match waits (the co-op reconnect window then ends it as abandoned).Countdown(ui/components.js) renders nothing without a deadline (it used to draw a--COUNTDOWN placeholder that only a CSS rule hid). - The strategy draft has ONE countdown: every turn lasts
Match.BAND_TURN_SECONDS= 30 s ASSUMED (× timerScale;data/config.json timers.bandTurnsays the same — the official data only has the whole BAND_CHECK step, 50 s, now informational) andm.public.deadlineIS the current turn's end (=draft.turnDeadline;draft.turnSeconds= the turn length, 0 untimed), so the step header and the picker's row show the same number. No step cap: the turns bound the step (≤ (seats + skips) × 30 s). AI seats pick at once. The draft screen reports its highlighted strategy (g.bandFocus); a turn that runs out takes it while it is allowed and free (Match.timeoutBand), else 华法琳 (band_bldsk), else the first free strategy (defaultBand, 队友已选 never). The selection starts on that default and the tip always names what a timeout gives ("每位博士有 30 秒,超时将自动选择当前选中的「X」"; untimed: "本局不限时").server/match/audit.jschecks it (deadline = the turn's, 30 s; nothing timed for a single human). - Tests:
test/ui/playtest4.test.js,test/ui/bandDraft.test.js,test/match/draft.test.js,test/ui/gameLogic.test.js(phaseTotalSeconds= the turn),test/match/hardening.test.js(the audit),test/ui/mock.e2e.test.js(two taps). Browser tests that need a timed co-op briefing now seat two humans (test/ui/loadout.e2e.test.js); a single human's 机变 is picked by the test itself (test/ui/playtest2.real.e2e.test.js), and the real-server drafts wait for BAND_DRAFT before picking (a client left in the briefing let its 30 s turns run out).
18.3 Knocked-out operators and element gauges (#9, #8) — sim/Battle.js, sim/damage.js, render/interp.js, render/units.js
- Knocked out, not gone:
Battle.snapshot()listsdown: [[id, respawnAt, respawnTime, state, row, col]](§8.2; the tile since §21.24) for operators knocked out (reason'killed', not withdrawn — orFORCED_EXIT, entering 联防 knocked out, §19.3), deployed at least once and with a finite respawn timer;state= sim/constants.jsDOWN_STATE(0 counting, 1 timer done but DP < cost, 2 timer done but its tile is taken — the official auto-redeploy rule, research 01 §4.3; a safeguard since §21.24, no ally may take the tile a knocked-out operator lies on).Battle.isDown(u);fieldMeta()lists them too, so a client joining mid-battle (reconnect, observer, replica) shows them. The renderer (interp.js downAt,app.js syncBattle,units.js setDown) keeps such an operator's view on the tile it lies on (the entry's row, col): its Spine Die clip plays once and holds its last frame (a collapsed / kneeling pose), lightly greyed (tint 0xb4b4b4, alpha 0.92), under a redeploy ring above the head — a dark disc with a mint arc filling as the timer runs and the seconds left (game s); timer done: a full pulsing amber ring "DP" (waiting for DP) or red "!" (its tile taken). Thedeployevent plays the Start clip and restores the normal look; a view created for an operator already down starts on the held pose; leaving the list without a redeploy fades it out. Summons, devices and enemies vanish as before. ASSUMED look: the official knocked-down / ring art is in no extracted autochess bundle. - Element gauges, official rules (PRTS 元素 / 术语释义;
damage.js): gain ×elemTakenMul× (1 − 损伤抵抗 / 100), 损伤抵抗 = dataepResistance(EP_RESISTANCE; it used to readepDamageResistanceas a fraction). A burst's 爆发冷却 (= its duration: 灼燃 / 神经 / 侵蚀 10 s — enemies' 侵蚀 8 s — 凋亡 15 s; the operators' 侵蚀 had none) locks EVERY element of the unit — nothing fills or recovers (reduceElement) — and its end resets EVERY gauge (burstLocked(unit)takes no element). 神经 applies the stun (operators) / 3 麻痹 (enemies) before its damage, 侵蚀 its DEF cut before its damage; the operators' 凋亡 burst holds 阻回 (noSp:skills.js gainSprefuses SP of every kind — time, attack, hurt, granted) + 静默, −1 SP/s and 100 arts/s. - The gauge on screen (official display: one icon, the "当前损伤元素"):
damage.js elementView(u, now)→ b.snapelem(§8.2): the fullest gauge, ties by the official element id (constants.js ELEMENT_ORDER: 神经 < 侵蚀 < 灼燃 < 凋亡); during a 爆发冷却 the bursting element, refilling over the cooldown.interp.jsappends it to the unit's tuple;units.jsdraws it — superseded by §20.8: a row under the unit's own HP / SP bars, the element disc (0.15 tile, 8–15 px; operators with the glyph, enemies ×0.8 plain colour) and a white bar of the remaining 元素值 (1 − fill), refilling in the element's colour with a pulsing disc during the 爆发冷却 (v2.3 drew a white ring right of the bars; the procedural atlastextures.js hudRingskeeps the discs and the redeploy ring), sprites made on first use and hidden when idle. The burst's lock status (burnBurst,neuralBurst…) is not repeated in the status row while the element icon shows it (only a feed withoutelem, e.g. an older recording, shows it there). ASSUMED placement (the official draws it under the model, the remake's bars are above the head) and look (§20.8). - Recordings (
tools/record-battle.mjs) carry both keys, so the render demo shows them. Tests:test/sim/downelem.test.js,test/sim/combat.test.js,test/render/downring.test.js,test/render/interp.test.js,test/render/downelem.browser.test.js.
18.4 Battle sounds (#6) — public/js/audio.js, tools/assets/audio.mjs
- Cause: every
'atk'— heals included — made its source the target's "last attacker", and any later damage on that target (enemy projectiles, element fills, DoTs) played that unit'shit; 纯烬艾雅法拉 (who only heals) had her S3 impact (p_imp_gtshpbrnch_s, bank ON_ABILITY_HIT.attack.2) ashitin the manifest, so it rang all the time. - Now only a hostile
'atk'(from the other side) authors the target's next impact: it plays once, within 2.5 s real ASSUMED, and only for phys / arts / true damage; heals and chain bounces never author one. An operator's skill-mode file (official names_d/_h/_s; the normal attack's end in_n) never plays for a normal attack (normalAttackSfx). At most 2 overlapping copies of one battle sound (SfxLimiter; official banksmaxSoundAllowed2 — a third copy is skipped, not the oldest stopped ASSUMED). - Manifest:
tools/assets/audio.mjs pickUnitSfx(banks, { operator, projectile })picks normal-mode banks for operators — the plainattack/combatability first, never a bank with a skill-mode file — with the operator's own projectile banks (ON_PROJECTILE_BORN/_HIT.projectile_chr_<name>,tools/assets/plan.mjs) as fallbacks; enemies and tokens are unchanged. 56 operators' picks changed (e.g. agoat2hit→p_imp_gtshpbrnch_n, nearl2attack→p_atk_goldspear_n, whitw2 →p_atk_whtolfdrksl_n). Loop-bank skill sounds still play once (open). - Tests:
test/ui/audio.test.js,test/assets.test.js.
18.5 Live operator stats (#7) — ui/detailPanel.js, battle/runner.js, Match.unitStats
- Battle:
battleRunner.unitStats(id, fieldId)reads the local sim's last computed stats (unit._s, never the lazyunit.s, so looking cannot shift the sim) and the detail card re-reads it 4× a second: current HP / max HP, ATK, DEF, RES, attack interval, block (enemies also move speed) and the range grid (range: the live 攻击范围 — a running skill's grid included; 全场 for a whole-field range, smaller cells beyond the box, §21.16).battleRunner.unitIdOf(uid, ownerId, fieldId?)maps an own board piece to its battle unit, so a card opened in prep and left open into the battle turns live. - Prep: C→S
g.unitStats {seq?}→m.unitStats {seq, round, units}(§8.2):Match.unitStatsbuilds a Battle from the player's battle input after theonBattleStartmeta (dispatched withev.preview: trueand nospawns— handlers must not change the match then, docs/META.md), with the flags of the battle it previews (a normal round gains IN_BATTLE layers from its start; the Final Assault is previewed as a normal field without them), starts it (initial deployment + battleStart hooks), reads it and drops it — never stepped, so skills and timed effects are not in it [ASSUMED display] (a passive range skill and a permanent 攻击距离 are part of the start range, §21.16); cached per input (≈ 0.3–0.7 ms a build).screens/game.jsasks 120 ms after anym.private/ phase change while an own board unit's card is open and keeps the newest answer only. - The card colours each value against the unit's base — green when it helps (a shorter interval is up), red when it hurts — with the difference beside it, the base in the tooltip and a 实时 / 开战时 tag. The shape is
shared/protocol.js unitStatsEntry. Tests:test/match/unitStats.test.js,test/match/runner.test.js,test/ui/playtest4.test.js.
18.6 Shop-only items, drones, summons, 炎佑 (#5, #10, #11, #12) — tools/build-data.mjs, sim/simdata.js, sim/content/*
- #5 not sold in the shop: the official
trapShopChessDatashashideInShopfalse for all 56 normal items, so an explicit sourced list (tools/build-data.mjs SHOP_EXCLUDED_ITEMS→ items.jsonshopExcluded/shopExcludedBy) marks the effect-only items: 战栗 / 坚固 / 加速 / 灼燃维式重锤 (the 维多利亚 bond every 25 layers, 洛洛's 定制品) and 突变细胞 (the 昆图斯 strategy 不稳定要素; user playtest #4 first-hand).sim/simdata.js isShopItemis the one predicate for every shop-item draw — the shop's item slot, 道具补给 / 机密商店 cards,pool_equip_normal/_shop_1/_kathe/_narant,Match.rollItemId— so 51 normal items are sold (the plain 维式重锤 still is). 洛洛's 定制品 (pool_equip_rockr) = the same 4 special hammers aspool_equip_vict, uniform ASSUMED. The item card says 调度中心不出售 · 获取途径:… (detailPanel.js ItemDetail). Other strategy-granted items stay on sale (no source says otherwise; 变形同构体 / 骑士储蓄罐 / 盟约之币 appear in an official 机密商店 screenshot). - #10 drones (stats and ranges = enemy_database level 0, re-derived by
test/data.test.js; behaviour per PRTS): 暴鸰 makes no normal attack — it holds still and drops one bomb (released 8 ticks into its Attack clip, homing at 5 tiles/s) that hits its target and the 8 tiles around on arrival (100 % ATK, splash ignores camouflage); a stun before the release interrupts it; when the cast ends (landed, ≥ 0.667 s after the release) it flies on at ×2 speed (§21.14); 帝国炮火先兆者 / 中枢先兆者 fire a shell per attack that lands 3 s later at the target's spot and hits every ally within 1.2 for 100 % of the ATK at launch, with no source (content/enemies.js kitShell; newai.js enemyAttackhooksprofile.deferHit— content resolves the hit in itsattackhandler — andprofile.shot, the'atk'kind:'mortar'draws no projectile; fxbombardShell); 假想敌:黑云 grabs withinrange_radius× 2.5 (3.75) at most 3 flyers (nearest first ASSUMED, 0.5 s delay ASSUMED), both held 4 s, ammo capped by enemies.jsonsp.maxSp(3; newspfrom enemy_database spData), 全弹发射 = one 130 % hit per ammo on random allies anywhere on the field; “萨科塔之眼” never attacks flying allies; 萨科塔 self-fear speed final ×1.5, while it flutters inside its tile (§20.4). Unchanged (already right): 妖怪 / 妖怪MKII / 威龙 / 寒霜 / 法术大师A1·A2 / 御4 / 护障 / 护障·P / 远眺 (威龙's drawn size was fixed in §20.4). - #11 skill summons are not prep pieces — superseded by §20.1: v2.3 limited tokens.json
placeabletotokenDisplayTypeDEFAULT summons made by a talent (海嗣, 狼群, 流形), so 赫默's 医疗探机 and 巫恋's 诅咒娃娃 appeared only when the skill fired. Since user playtest #6 every manually deployable summon is a hand piece again, and its placed piece deploys once at the battle start (PRTS), then with each cast. - #12 炎佑 (PRTS “炎佑” 级别0(卫戍协议),
content/tokens.js yanyouKit; 孤立 — never healed or selected by allies, §20.3): it flies after the highest-aggro targetable enemy of the whole field (operator order) and hovers 0.25 tile ASSUMED from it (a second 炎佑 of 9 炎 0.8 row beside the first); with no enemy on the field it stays where it is (user playtest #4; PRTS says it then tracks a random tile). 祛恶之焰 (模式乙): fires like a normal attack when its 15 s cooldown is full, locks that attack's first target and channels up to 20 s — each second (the first at once) 60 % ATK arts to every enemy within 1.0 of the target, no normal attacks meanwhile — ending when the target is gone or 炎佑 is silenced (inferred); the cooldown then runs again ASSUMED. Every damage it deals adds burn = 20 % ATK (flame ticks included, official 下半 notice); 元素脆弱 aura of radius 1.5 around itself; it ignores element damage; ATK / HP = the template 600 / 12000 plus 30 % of the 炎 operators' sums (PRTS 最终加算; the 9-炎 ×1.5 on the whole ATK ASSUMED). The flame is drawn as one continuous jet (render/fx.js _flame): the sim emits fxyanyouFlame {x, y, id: dragon, target, r, n, dur: 1}once per game second, and each tick re-aims and extends the dragon's jet — a stream of fire from the dragon onto its locked target with a burning disc of radiusrfollowing the target — which burnsdur+ 0.2 s and goes out when no tick comes or the dragon is gone. - Tests:
test/content/playtest4_summons.test.js,enemies_bosses.test.js,tokens_devices.test.js,bonds_core.test.js,choices.test.js,test/data.test.js,test/match/pool.test.js,test/render/fxflame.test.js,test/ui/playtest4.test.js(item card).
18.7 The occasional giant UI flash (#13) — render/fx.js pop
The first in-battle layer gain of each bond (b.ev 'layer') pops that bond's icon over the field (FxSystem.pop). The icon is PIXI.Texture.from(url) — a 1×1 placeholder until its image has loaded — and the pop sized the sprite from it (46 / 1): the bond icon was drawn ~5000 px across the whole screen for the pop's 1.4 s, once per bond per page (73 official garrisons gain layers in battle, e.g. 送葬人's). Now it is sized (46 px along its longer side) only once its texture is valid, hidden until then. Found by sampling long real-server sessions every 80 ms (DOM boxes > 25 % of the viewport, images drawn > 300 px, Pixi objects > 25 % or showing a whole atlas page, CSS scale ≥ 2); every other large object seen is a designed full-screen layer. Tests: test/render/fxpop.test.js, test/render/flash.browser.test.js.
19. User playtest #5 (v2.4): hovering enemies, target order, blocking, 联防, elements, huge bosses, maps, phone HUD
Ten reports after playing v2.3 (绝境 on desktop, 标准 on an Android phone in landscape). Where each is handled: #1 operators that only attack ground units hit 近地悬浮 enemies → §19.1; #2 an operator lying knocked out (濒死) vanished when 联防 began instead of counting down and redeploying → §19.3; #3 element damage against the 海嗣 felt weak, audit every operator's and enemy's element amounts and gauges → §19.5 (enemies, the gauge pipeline) and §19.6 (the player side); #4 an enemy held between two operators walked off when the rear one died instead of the front one taking it over → §19.2; #5 the enemies' target order differed from the official → §19.1; #6 act2 m01's normal field had its two wind lanes but no blower machines, which showed in the boss round instead → §19.7; #7 the tiles framed by the 1×3 pipes on the boss map are deployable officially, check every map → §19.7; #8 the bottom-left settings icon was deformed → §19.8; #9 on a long phone screen the shop bar covered the bench → §19.8; #10 the huge bosses could only be hit around one tile → §19.4. A follow-up message added #11 至简 should cost 1 (his 特质 says 购买价格为1) → §19.9. Normative lines rewritten with it: §3 range test, §5.1 carryState, §5.5 (initial deployment, DP / down, blocking, operator attacks, enemy AI, damage), §6.1 UNITE, §7 bosses; §18.3's isDown now includes FORCED_EXIT, and §18.1's notched-phone figures are superseded by §19.8. The integration also applied the workstreams' cross-checks (hasHp in the pipeline, 脆弱 vs 元素伤害, element healing, 淤困, 塑心 S2, splash as a 中点判定); they are listed in the sections they belong to. Three QA passes over the integrated branch then left residuals, fixed in their sections: 掠海漂移体's model crawls after its drop (§19.1); blocked-first and out-of-range selection are melee-only (§19.2 — superseded by §20.3); a forced-out operator's timer takes the battle-start redeploy effects (§19.3); the gauge intake's 5 % floor (§19.5); the fallback board draws the boss half in the boss prep (§19.7). The user's follow-up then settled two ASSUMED choices: the boss field's row-6 blowers do stand under the bench in the normal views (§19.7), and a forced-out operator's redeploy timer does restart in full (§19.3).
19.1 Hovering enemies and the enemies' target order (#1, #5) — sim/units.js, targeting.js, ai.js, Battle.js, content/enemies.js, bosses.js, kits, match/bot.js, render/units.js FORMS
- #1 cause:
float()only cancelled hits from non-'ranged'profiles and otherwise left the floater a ground unit, so 迷迭香 (攻击对小范围的地面敌人) shot and splashed it (5.2k damage in a sim run) and terrain, traps and 浮空 treated it as ground. Official (ba.float "无法被阻挡或近战攻击"; PRTS 术语释义 / 行动方式): the unit is unblocked and counts as a real air unit (算作空中单位) that keeps ground pathing. - Now
Unit.isFlyingis the one air-unit test:motionFLY, or an enemy flagfloat(近地悬浮) orlevitate(浮空); pathing, displacement tiles and 诱导 readmotion. canHitFly / groundOnly, splash, chain, the kits' "地面敌人" filters, ground terrain and traps skip air units; 失衡免疫 is the flagnoDisplace; 静态刚体 (every air unit of the mode except “炎佑”, plus 昆图斯) is datastaticBodyand never displaced; 守墓石像 (statue and flight) and 喷气人 (flight) are 失衡免疫 (§21.9); 浮空 is refused only on data flyers and already-levitated units (PRTS 行动方式), so 阿罗玛 can lift a floater. Blocking follows (§19.2): only blockFly units block air units. - Enemy kits: 掠海漂移体 — 晕眩 / 冻结 / 沉睡 → 爬行模式 for good (0.5 s stun), then it only attacks its blocker; 不会攻击飞行单位. Its model follows (integration QA: melee operators hitting an enemy that still looked airborne): the sim's fx
formcrawlswitches the view to the skeleton's crawl clips (*_02, after itsChangeclip) —render/units.js FORMS,spine.js setForm, kept on the unit info for a view built later (a view built mid-battle starts crawling: UnitInfoform, §21.4). 吉兆飞鳞 — 晕眩 / 沉睡 → 8 s 晕眩模式 (it stays down while still asleep), a freeze grounds it when the freeze ends, its sprint is the final ×3. Of the 32 official floaters only these two hover for good in this mode; 喷气人 hovers for its 7 s 飞行模式 (§21.9) (the bot'sHOVER_KEYS;fieldModelcounts them as air routes on ground paths). - Skill effects (per PRTS 备注; the official per-ability targetMotion is not in the data): ground-only — 隐德来希 S2 血镰, 归溟幽灵鲨 拥抱自我, 琳琅诗怀雅 S3, 乌尔比安 S1 [ASSUMED: a 捕网 like 雪雉's]; PRTS 可对空 — 德克萨斯 S2, 忍冬 S2, 焰尾 S2, 缄默德克萨斯 S3, 凛御银灰 S2, 玛恩纳 S3 (his melee skill attacks:
targeting.canHitFly; §21.8), 锏 S3 归于宁静 (added by §20.3); no 对空 note, ASSUMED to hit air — 风丸 折纸生花, 乌尔比安 S3, 缄默德克萨斯 S2, 余 S2, 见行者 S2, 耀骑士临光 不畏苦暗 and the token appear bursts (“耀阳”, 沙之碑, 迷迭香的战术装备's stun, 纸偶). A stun / freeze / sleep from any of them drops a hovering 掠海漂移体. - #5 official: PRTS 索敌 (敌方) "阻挡→特殊优先级→仇恨值(更容易被攻击→…→最后部署的目标→不容易被攻击)→最早出现"; PRTS 卫戍协议/帮助 "按从上到下>从左到右的顺序部署。优先部署干员,随后为召唤物", the right-hand boss player mirrored.
- #5 changes:
Battle.startdeploys operators row-major, then summons;aggroSeqranks every start-of-battle summon after all operators (deploySeqstays the deployment identity). An enemy's own rulee.profile.canTargetfilters its candidates before the sort (萨卡兹枯朽战车 no longer idles when its preferred target stands on high ground; it, 掠海漂移体 and “萨科塔之眼” ignore flying allies). Special priorities break ties by taunt, then latest deployed (targeting.js aggroCmp): 铳 / 昆图斯 highest DEF, 胄 highest / lowest ATK, the 碎铳之簧 bounce, 唱沙 / 沙狱 max HP, “自在” nearest. Kept (PRTS 作战机制 §隐匿 "我方干员并不会因为阻挡而解除隐匿"; 索敌的概念): a stealthed ally (隐匿 / 迷彩 / 排气格栅) is attacked only by the enemy it blocks (迷彩 is its own flagcamou, not 隐匿 for 隐匿 conditions, §20.2). ASSUMED "从上到下>从左到右" = row-major; on a shared field (联防, boss) the players' i-th operators deploy together, then the summons (it used to be player after player, which made the leader prefer the later player's pieces). - Tests:
test/sim/playtest5-targeting.test.js(18; 13 fail on 731a01c),test/content/enemies_bosses.test.js(吉兆飞鳞),test/match/bot.test.js(a hovering wave counts as air),test/render/forms.test.js(the crawl clips); browser (opt-in)test/render/forms.browser.test.js(the real sim feeding the field view).
19.2 Blocking by contact radius, and the hand-over (#4) — sim/constants.js BLOCK_RADIUS, Battle._checkBlock / blockedTargets, ai.js
- Cause: an operator blocked only an enemy standing on its own tile (§5.5's old "|dx|,|dy| ≤ 0.5"). An enemy that passed a full front operator was blocked by the rear one, on the rear tile; when the rear operator died, the front one — which the enemy still overlapped — never took it.
- Official (PRTS 游戏数据基础 §阻挡半径; 作战机制 "中点判定…案例:阻挡"): contact = the enemy's position within the blocker's radius of its centre, compared as squared distances — ground 0.70709997 (² 0.49999037), air (起飞 / blockFly vs flyers) 0.8944, devices (阻隔工事 / 障碍物) 0.4472.
- Now (§5.5 Blocking): every tick, moving or not, an unblocked blockable enemy is blocked by the nearest operator in contact that has free capacity for its weight — the front operator takes over when the blocker dies, is withdrawn or is stunned (
stunreleases its enemies); an operator whose capacity frees up grabs an overlapping enemy; an enemy that finds no room walks on. A head-on enemy stops at contact, ≈ 0.71 tile from the blocker's centre, outside its tile (the small official deceleration is not modelled). A knocked-out or undeployed operator never blocks. - Consequences, both official: "可以选择且优先选择阻挡单位" (PRTS 选择器: the selector of units whose range is smaller than their block radius) — a melee blocker (data
positionMELEE: 要塞 / 领主 / 哨戒铁卫 included; every blocker since §20.3) may always target the enemies it blocks, in range or not, and targets them first (Battle.blockedTargets→ai.js acquireTargets,targeting.js sortEnemyTargets, the DEFAULT skill trigger — PRTS 卫戍协议/帮助 "敌人被近战干员自身阻挡"); 瑕光 S2 also sleeps the enemy she blocks (PRTS 备注). A rear operator whose range ends at the blocker's tile no longer reaches the enemy held in front of it (a regression sweep of 143 blocker × skill pairs: own-tile defenders 0.96–1.05×). - Ranged operators on melee tiles (卫戍协议 "所有行动内远程干员可部署在近战位") — superseded by §20.3: the integration QA had made them target by their range alone (PRTS 索敌的概念 "我方索敌优先级:阻挡(近战限定)"; "远程位干员通常而言不会优先攻击自己阻挡的目标;甚至如果这个目标被阻挡在该干员的身后…无法攻击到这个敌人"): no blocked-first priority, no out-of-range target, no skill cast for the enemy they held. The user's rule after playtest #6 — "阻挡了就一定要能打到" — makes every blocker hit what it blocks.
- 灰毫 S2 专注轰击 — superseded by §20.2: v2.4 read the 下半 重装 special strategy "受到伤害时释放技能" as skill 1 only (BWIKI "重装职业干员 技能1,受到伤害后自动释放"; the data row has
skillIndex 0, with-1as the all-skills sentinel — research 03 C1) and left S2 DEFAULT. The class rows now cover every MANUAL skill of the class, so 灰毫 S2 is TAKE_DAMAGE (the counter-evidence stays recorded in §20.2). - ASSUMED the nearest blocker wins, ties by scan order.
- Tests:
test/sim/playtest5_blocking.test.js(16, incl. the user's case on act2 m01 with real operators, the ranged blocker and 银灰 as a melee-position ranged attacker), adjusted geometry inkits_alt_t5/engine-requests/enemies_bosses.
| Contact | Official (PRTS) | v2.3 | Now |
|---|---|---|---|
| ground operator | d² < 0.49999037 (r 0.7071) | the enemy's tile = the operator's tile | d² < 0.49999037 |
| air (blockFly / 起飞) vs flyers | r 0.8944 | same tile | d² < 0.79995137 |
| device (阻隔工事 / 障碍物) | r 0.4472 | same tile | d² < 0.2 |
| blocker gone / capacity freed | contact re-checked continuously | only on the enemy's own tile | every tick, nearest free blocker |
19.3 Knocked-out operators in 联防 (#2) — server/match/unite.js, sim/Battle.js, sim/constants.js FORCED_EXIT, render/app.js, render/units.js
- Cause:
unite.js uniteBattleOptsdropped every operator thatunitsEndreportedalive: false— and its summons — from the 联防 input (an old ASSUMED rule). Every client and the server build the unite field from that input, so the operator was missing for the authority, the partners, observers and theSP_COMBAT=serverfallback alike. - Official (PRTS 卫戍协议/帮助 §联防阶段): "部署完成后,将对应单位的生命比例、技力修改至与上一阶段结束时相同(召唤物仅修改技力,上一阶段为退场状态的干员强制退场)".
- Now: the operator is fielded with
carryState: { down: true }(§5.1); its summons are fielded as the board has them.Battle.startdeploys everyone (the initialdeployhook fires), then beforebattleStartsets its HP to 0 (the end-of-phase ratio, askill()) and withdraws it with removal reasonFORCED_EXIT='forcedExit'.Battle.isDownaccepts that reason, so it is in b.snapdownandfieldMetawith its ring, shows 0 HP on the live card, and redeploys by §5.5 (timer done, own tile free, DP ≥ cost) at full HP. The forced exit is no new knock-out: nokillhook, nodeathwith reason'killed', nodeathscount — 深海 / 不屈 revives, 崇高牺牲 layers and selfdead garrisons fired in the own combat. The client's'die'with that reason goes straight to the held knocked-down pose (UnitView.die(true): no fall, no burst, no sound). - The timer restarts at the operator's full redeploy time (70 s for most chess): the official setup carries only
hp/techper operator (research 09 §3HelpBattleInfo), and the user confirmed it after the v2.4 follow-up ("进联防复活时间确实是重新算" — a request to carry the remainder was withdrawn). A short 联防 may therefore end before it comes back (a real-sim reproduction ended at 28 s; in the integration QA's real matches 43 % of 1,564 knocked-out operators were still counting when their 联防 ended). Also ASSUMED: the forced exit happens beforebattleStart; the timer is re-read after it, so a redeploy-time effect that starts with the battle covers it (机变 征召 "所有干员的再部署时间-50%": 70 → 35 s, as for a later knock-out — it read 70 s before the integration QA), while 征召's own row check ("场上至少有一行存在3名干员") does not count it ASSUMED. Not covered: 召唤物仅修改技力 (summons enter 联防 fresh;unitsEndlists operators only — carrying their SP would extend the client-reported result,spec.jsdigests andfields.jsvalidation). - Tests:
test/match/unitedown.test.js(both combat modes, both helper halves, the real sim),test/sim/downelem.test.js(incl. 征召),test/render/downring.test.js,test/match/combat.test.js; browser (opt-in)test/render/unitedown.browser.test.js.
19.4 Huge bosses: hit area, 自缚 (#10) — tools/build-data.mjs HIT_AREAS, sim/body.js, content/bosses.js SELF_BOUND, render/pick.js
- Cause: every range test took the enemy's position tile, and the data had no body size, so a boss at (3,10) was hit only by ranges covering (3,10).
- Official (PRTS 天赋): "巨型单位:受击判定区域为长4.95、宽2.95的长方形,向上偏移1.0" (管·隐秘核心 also 向右偏移1.0; PRTS盟约记录 gives the season's 阿利斯泰尔 and “萨米的意志” the same area); PRTS 作战机制 "巨型BOSS单位的每一个占据的格子都可以让其本身通过格子判定". The same talent lines list 自缚 (PRTS 异常效果: 无法移动, not recognised as 束缚) and 无法被阻挡. 铳, 卢西恩 and the boss parts are regular units.
- Now: enemies.json
hitArea {w, h, dx, dy}(by prefabKey);sim/body.jsis the one helper behind every range test on enemies (the engine tile index,enemiesInKeys,unitsInGrid,enemiesInRadius, nearest / farthest priority, kits, tokens, bonds, professions,support.onKeys). A huge enemy occupies every tile its rectangle overlaps — 5 × 3, rows 3–5 × cols 8–12 around (3,10): the whole fence-framed pipe block, so an operator on the 1×3 fence tiles (§19.7) hits it. Radius rules measure to the rectangle (0 inside) [ASSUMED: PRTS calls collider tests the most common]; splash around a struck target is a 中点判定 (PRTS 作战机制 "中点判定…案例:阻挡,酒神1天赋的1.3溅射半径"):enemiesInRadius(x, y, r, true)counts every enemy by its position — the engine's profession splash and aftershocks and the kits' target-centred splash (焰影苇草's 灼痕 burst, 新约能天使 火力电台, 仇白 S1) — so a splash on an escort 0.53 tiles from the rectangle but 1.41 from the boss's centre no longer reaches the boss. Its position stays its 判定中心 (projectiles, its own attacks, splash centred on it, distance-scaled numbers). Integration pass: the reaperrange front line, 凛御银灰 S3's lateral line and the 香槟炸弹 tile trigger are body-aware too; movement, pathing, terrain, tile-entry tracking, placement heuristics and range keys taken from blocked enemies stay on the position (a huge enemy never moves and is never blocked). - 自缚: the co-op 昆图斯 walked its official route (3,10) → (2,2) off the block and leaked at ≈ 280 game s.
SELF_BOUND(exactly the 7 hitArea keys) gives them a persistentnoMove+unblockablebuff at spawn (the mirrored co-op copy too). - Picking (
pick.js AREA_PICK): a press on the hit area or inside the drawn body box selects the boss, counted as 1 tile away, so an ally on the pressed tile or a nearer regular enemy wins. - ASSUMED 胄·隐秘核心 keeps the upward offset although its PRTS section omits it: enemy_database gives
enemy_9013_acstmk_2the prefabKeyenemy_9013_acstmk— one prefab, one collider — while the 管 copy that PRTS lists with a different offset has a prefab of its own (enemy_9021_acduml_2); ask at the next playtest if the hidden round feels off. The rectangle is fixed in map space (not mirrored). - Tests:
test/sim/playtest5_bossbody.test.js(12, incl. the centre-point splash),test/render/pick.test.js, the Final Assault leak regression intest/sim/battle.test.js; browser (opt-in)test/render/bossbody.browser.test.js.
| Boss | Hit area (PRTS) | Tiles at its spawn | Talent |
|---|---|---|---|
| 假想敌:胄 (+ 隐秘核心 copy) | 4.95 × 2.95, up 1.0 (copy: ASSUMED same) | (3,10) → rows 3–5 × cols 8–12 | 自缚、不可阻挡 |
| 假想敌:管 / 管·隐秘核心 | 4.95 × 2.95, up 1.0 / + right 1.0 | (3,10) / (3,9) → rows 3–5 × cols 8–12 | 自缚,无法被阻挡 |
| 盐风主教昆图斯 | 4.95 × 2.95, up 1.0 | (3,10) → rows 3–5 × cols 8–12 | 自缚,无法被阻挡 (walked and leaked before) |
| 阿利斯泰尔 / “萨米的意志” (卫戍 versions) | 4.95 × 2.95, up 1.0 | (3,10) → rows 3–5 × cols 8–12 | 自缚,无法被阻挡 |
| 假想敌:铳, 卢西恩, the parts | none (point) | its tile | 铳 不可阻挡 only; 卢西恩 blockable |
19.5 Element gauges and enemy element damage (#3, enemy side and pipeline) — sim/damage.js, content/enemies.js, content/bosses.js
- Verdict: official behaviour, not a remake bug. Every 海嗣 has 损伤抵抗 (
epResistance) 0 and 元素抗性 (epDamageResistance) 0 (in data/enemies.json only 转译基底·α has 损伤抵抗 10); gauges are 1000 (leaders 2000) and never decay; no 海嗣 talent changes element intake; every enemy element attacker deals ATK × its official ratio per hit × the round's ATK multiplier (47 checked). Boss levels override some values, which the runtime applies: 卢西恩 ATK 700 (solo 600) and 不祥幻影 200, × 0.12 per normal hit (skill 0.2) in boss_5 (level_act2autochess_h07_05(_s)); 盐风主教昆图斯 480 (solo 380) in h07_04. A blocked 底海滑动者 at 险境 R5 bursts 神经 on its 25th hit, ≈ 50 game s. "Weak" also has a player-side part — §19.6. - Official rules (PRTS 元素 / 游戏数据基础 / 伤害分类; gamedata_const termDescriptionDict): gauge gain = 损伤值 × max(5 %, 1 − 损伤抵抗 %) (PRTS 游戏数据基础: DMG_e "目标受到元素损伤时也可以使用该公式计算,只需要将 D 值改为目标的损伤抵抗即可"), then the 元素损伤倍率 effects; 元素伤害 = max(A(1 − 元素抗性 %), 5 %A); 元素脆弱 ("受到的元素伤害提升") raises 元素伤害 only; 脆弱 ("受到的物理、法术、真实伤害提升") never touches 元素伤害; bursts are 无来源 ("元素爆发通常造成无来源的伤害"; 无来源 = "无法被追溯伤害来源", yet the kill is traced).
- Fixed (W3b): 元素脆弱 is the mod
elementalTakenMul(元素伤害 only; it used to share the gauge rateelemTakenMul, so 炎佑's 20 % sped up fills instead of raising bursts); 元素抗性 reduces'elemental'with the 5 % floor (expectedFinalmirrors it); bursts carry DamageInfosourceless— no damage-dealt multiplier or penetration of the filler, and thehit/damaged/fatalhooks seesource: nullpluscredit(stats, kill and boss-pool share keep the filler; a real 深巡 bursting a 海嗣 dealt 10500 instead of 7000, 精准狙击镜 made a 侵蚀 burst 6500 instead of 5000); target-side reactions with no source condition readsource || credit(sharing: 圣杯, 盲信之誓, 余音; 抵挡: 星熊 战术装甲, 拉普兰德 日晷). The enemies' 凋亡 虚弱 is 50 % × remaining ÷ 15 s each tick (it ran 1 s ahead); 麻痹免疫 is honoured (假想敌:铳, the 胄 parts); 淤困's host burst spreads 1000 (was 780); 灼藤's first attack covers the 3×3; 节日爵士乐手 channels on one locked target (20 % arts + 10 % burn every 0.5 s, 21 ticks, ends on 沉默, no re-cast mid-channel). Side effect: 隐德来希's 心烛 cancels bursts on a candle ("只受隐德来希攻击的影响"). - Integration pass:
damage.js hasHp(alive, HP > 0; a boss: its pool) is the one predicate —applyElementrefuses a target with no HP left, so no rider in a killing blow'sdamagedhook can burst a corpse (the content copies in tier5 / tier6 / items / tokens now import it);'elemental'damage skipsdmgTakenMul(脆弱 and the other "受到的伤害±" effects [ASSUMED: they share that multiplier]) — only 元素脆弱 scales it; element healing (reduceElement) lowers every element type by the amount on its own (PRTS 菲莱 备注: 清除元素损伤 = "一次等同于自身最大元素值的全类型元素损伤治疗"; it healed the amount across all types in total); 淤困's "受到的元素损伤提高至130%" is anelementHitmultiplier at priority 20 like every operator-side "受到的元素损伤±", before a 损伤屏障 (it was theelemTakenMulmod, applied after the barrier). - Verified unchanged: the burst effects on both sides; cooldowns 10 / 10 / 15 s (enemies' 侵蚀 8 s), locking then resetting every gauge; leaders take no 侵蚀 in the Final Assault (PRTS 盟约记录); 炎佑's aura = 20 % 元素脆弱 within 1.5. The gauge's look is the official icon + white bar since §20.8 (it differed in v2.4). ASSUMED 爵士乐手's first tick one interval after the cast. The gauge intake's 5 % floor (
elementIntake, integration QA) is unreachable with the data (损伤抵抗 ≤ 10). - Tests:
test/sim/playtest5_elements.test.js(18),test/content/playtest5_followups.test.js(pipeline guard, 脆弱, element healing),combat.test.js,test/content/{kits_t5,tokens_devices,playtest4_summons,enemies_bosses}.
| 海嗣 (user's case) | Official ratio | Per hit / s at base ATK | Hits to a 1000 gauge | Time |
|---|---|---|---|---|
| 底海滑动者 (ATK 280, BAT 2.0) | 15 % ATK 神经 | 42 | 24 | 48 game s |
| 富营养的滑动者 (360) | 15 % 神经 | 54 | 19 | 38 game s |
| 骨海漂流体 (250) | 20 % 侵蚀 | 50 | 20 | 40 game s |
| 掠海漂移体 (500, BAT 4) | 50 % 侵蚀 | 250 | 4 | 16 game s |
| 元核孽生者 (400, season) | 5 %/s 神经 in r 2.5 (season override; PRTS's base page: ATK 500, 10 %) | 20 / s | — | 50 s |
| 深溟巢涌者 (140) | 5 % ATK 神经 per damage output | ≈ 7 / s | — | ≈ 143 s |
19.6 Element damage of the player side (#3, operator side) — sim/content/kits/tier{2,3,5,6}.js, tokens.js, items/battle.js
- Audit: only these deal 元素损伤 from the player side in 下半 — 盟约·辅助干员 (band 优等生), 菲莱, 烛煌, 妮芙 (T5 / hidden T6), 余, 塑心, 炎佑 (炎 ×6), 灼燃维式重锤 (+ 蒸汽之心); no bond, band, garrison, 机变 card or device deals any. Every number matched its blackboard (character_table at the chess status, battle_equip_table modules); the discrepancies were form, trigger and scope. "N%攻击力的…损伤" = N × ATK, "伤害N%的…" = N × the damage just dealt.
- The fix that matters most: 盟约·辅助干员's 迭代元素 used to pick the "first element not bursting" — a burst locks every gauge, so she only ever filled 神经. Now every damage she deals (HP > 0; PRTS corrects the timing to "造成伤害时") attaches 18 % ATK of 神经 → 灼燃 → 凋亡 (
PITHST_ELEMENTS); the element applied first bursts, so alone she bursts 神经 and in a team completes the 灼燃 / 凋亡 gauges 余 / 塑心 build — in the reviewer's runs the team's first 灼燃 / 凋亡 burst came ≈ 40 % sooner (9.0 s vs 16.0 s; 5.0 s vs 8.2 s). Teams without her are unchanged. ASSUMED the three apply together (the reading of the text). - Killing blows: element riders inside
damagedhooks run beforebattle.kill, while the target isaliveat 0 HP; a lethal hit used to burst the corpse (a second fatal, 烛煌 熔点引爆's heal and 350 % hit, 妮芙's stack). The riders check HP left and, since the integration,applyElementrefuses it (§19.5). - 塑心 S2 (integration): PRTS "受影响的干员即将造成伤害时,因该效果造成的凋亡损伤生效于当次触发的伤害之前,该造成的凋亡损伤的来源始终为塑心" — the rider is a late
hithandler, so the 凋亡 lands (and may burst) before the damage, a killing blow included; a hit dodged or absorbed after that still carried it ASSUMED. 纯烬 氤氲 is keyed per source (agoat2:mist:<id>): two 纯烬 keep their own stacks, ATK and heal source. - Tests:
test/content/playtest5_elements.test.js(9),test/content/playtest5_followups.test.js(塑心 S2 order, 氤氲 per source),kits_t1t2.test.js(1_15, 哈洛德 with 侵蚀),kits_t3.test.js(菲莱).
| Source (normal / elite) | Official | v2.3 | Now |
|---|---|---|---|
| 盟约·辅助干员 迭代元素 | 18 % ATK each of 神经(优先)/ 灼燃 / 凋亡 per damage dealt; RIT-X 21.24 % vs 精英 / 领袖, talent only | per attack hit (dodged too), 神经 only | fixed: all three per damage > 0 |
| 菲莱 S2 冥河诅咒 | when attacked (cd 2 s): 110/140 % ATK arts + 25 % ATK 凋亡 to ground enemies in x-4 (3×3) | radius 1.5 | fixed (3×3, body-aware, 近地悬浮 = air) |
| 菲莱 PRP-X | "阻挡敌人时,自身造成的元素损伤提升15%" on every fill she deals | S2 blast only | fixed (all fills, a hammer too) |
| 受到的元素损伤降低: 菲莱 神河谕使 / 哈洛德 我即军营 / 纯烬 火山灰疗愈 | −10 % (+2 SP on 凋亡 even for 0) / −12/15 % for allies over half / −12/14 % (×talent_scale in S3); before the 损伤屏障 | the 元素脆弱 multiplier (also cut 元素伤害) | fixed: elementHit at priority 20 |
| 哈洛德 trait / T1 | "元素损伤最严重" and the over-half checks | 侵蚀 ignored | fixed (侵蚀 counts) |
| 纯烬 氤氲 | per stack / s: HP 10 % ATK, element 10 % × trait ratio (5/6 % ATK), cached ATK | element 10 % ATK (2×) | fixed; per source (integration) |
| 塑心 S2 安魂的弥撒 | self + top-ATK ally: each damage + 15/20 % of her ATK 凋亡, before the damage | after the damage, skipped on a killing blow | fixed (integration) |
| 塑心 S1 / T1 / T2 / RIT-X / RIT-Y | 85/95 % ATK 凋亡; 10 % ATK/s; 精神逆构 ×1.2 (elite ×1.33 + 5 % 元素脆弱 in the burst); ×1.18 vs 精英 / 领袖; RIT-Y 音乐家的旅程: field-wide ×1.2 + 150 元素伤害/s during the burst, 10 % 元素脆弱 in range | same | OK |
| 余 T1 / S3 / S1 / T2 / PRP-X | 40 % ATK arts + 12 % ATK 灼燃/s on blocked enemies; 火墙 6/7 %; 30/40 % ATK on the attacker; T2 1.5 % max HP of HP and element per s with ≥ 4 operators on the field (elite ≥ 2; elite ≥ 4 also +14 % arts vs 灼燃 bursts); ×1.15 while blocking | same | OK |
| 烛煌 S1 / S2 / S3 / T1 / PRI-X | 30 % of the damage as 灼燃; +60/70 % ATK 元素伤害 on burst targets; 熔点引爆 350 % + 12 % heal; ×1.1 vs burst targets | same | OK |
| 妮芙 S2 / T1 / PRI-Y | 18/22 % of her arts damage as 凋亡; 失魂 40 % ATK 元素伤害/s (PRI-Y 烦恼问诊匣: every 0.7 s, SP +0.2/s) | same | OK |
| 炎佑 / 灼燃维式重锤 | 20 % ATK 灼燃 per damage, 20 % 元素脆弱 aura r 1.5 / 10 % of the arts damage (蒸汽之心: own hammer ×2) | same | OK (元素脆弱 now raises bursts, §19.5) |
| any rider on a killing blow | a dead unit takes no element | burst the corpse | fixed (hasHp + pipeline guard) |
19.7 Maps: blower machines, boss-field deployment (#6, #7) — render/board3d/layout.js, render/tiles.js, render/app.js, server/match/board.js, Match.startRound / bossGroupOf / deployFieldOf, ui/gameLogic.js, ui/fallbackField.js
- #6 cause: the 3D board builds only the current view's area, and for the normal / 联防 views it stopped at row 12. act2 m01's own blowers (#001 / #002, the partner's #101 / #102) stand on the separator row 13 facing DOWN into rows 12–10 (level_act2autochess_m01 predefines: (13, 5/9/13/17) and (6, 5/9/11/15), all DOWN, none hidden), so they were never built; the Pixi airflow streaks and the sim were right. Now the normal / 联防 field rects are rows 6–13 and one device rule serves both boards: every active device standing on a built tile is drawn (3D
stageDevices, 2D_stageDevicesminus its faded margin). Normal / prep: (13,5) and (13,9) above the lanes, and the boss field's (6,5) / (6,9) on the wall under the bench; 联防 adds (13,13) and (13,17); 最终攻势 / 隐秘核心: (6,5), (6,9), (6,11), (6,15). - #6, the boss field's machines under the bench: the integration QA hid the row-6 blowers (6,5) / (6,9) in the normal / 联防 views (reading the official shop camera as putting that wall under the shop bar); the user's follow-up, first-hand — "吹风机原版道中也该有" — puts them back: the official normal rounds show them, so every view draws every device on its built tiles again (the rule above, as the workstream had it). Their airflow covers drawn tiles only, so it shows in the Final Assault view. Unchanged (cosmetic, pre-existing): the official normal view also shows the partner's half beside the own field (its row-13 machine (13,13) at the top right); the remake builds the own half only.
- #7 cause: every legality check of a boss round's prep read the normal field (board (10–12, 8) = tile_forbidden) —
board.js buildDeployMap, the client'scanPlace/ highlights, the bot and the invariants — while the pieces stand on the player's half of the boss field, where (3–5, 8) and (3–5, 12) are tile_fence_bound (low ground, buildable ALL, fly-only passable). NowbuildDeployMap(stage, { field })takes'normal' | 'bossL' | 'bossR'(board row = boss-field row +BOARD_ROWS_ABOVE_BOSS7; the right-hand player mirrored col c → 20 − c, research 09 §1.2 ConvertChessPositionInfoToBossMap);Match.bossGroupOfis the one pairing lookup (deployFieldOf,nextEnemiesFor, the bot);Match.startRoundplans the round's enemies beforePlayerState.startRound, so R14 → R15 re-checks boss-half pieces on the boss field (they used to be withdrawn with 地形变化; rngWaves is used only there, no random stream changes); a quit before the boss fight re-pairs and re-checks at once; the client'sgameLogic.deployFieldOffeedsplacementContext({ field }). Operators on the fence tiles hit the huge boss (§19.4) but block no ground enemy there (§21.23); ground enemies cannot walk onto those tiles. - Audit (all 11 stages × normal / 联防 / boss L / boss R, server = client = builder deployTiles = the raw level files, except the 深水区 refusal and the 特制水上平台 exception of §21.3): the only wrong tiles were act2 m01's six boss tiles; on every stage normal-legal ⊆ boss-legal and boss L = boss R, also under every map card, so R13 → R14 and R14 → R15 move nothing. act2 m01 now deploys 13 melee + 3 ranged-only tiles on the normal board and 16 + 3 on each boss half. Devices: 3D = 2D for every stage × view; act1 m05's 特制水上平台 are ×64 (16 per normal half, 32 on the boss field). The DOM fallback board (
ui/fallbackField.js, no WebGL /?render=fallback) draws the player's boss half in the boss prep too —screens/game.jsnow hands it thebossPrepcamera, which it maps throughgameLogic.fieldTileon the own board's layout (integration QA: it drew the normal field's walls under legal highlights). Limitations:Match.unitStatspreviews on the normal field (stats only); 射击台 stays ranged-only ASSUMED. - Tests:
test/match/playtest5-deploy.test.js(incl. boss → boss R14 → R15 and the fallback board),test/render/playtest5-maps.test.js(every device on a built tile in every view, the row-6 machines under the bench included),test/ui/playtest2.test.js; browser (opt-in)test/render/board3d.browser.test.js(area key '6,13,0,10').
19.8 Settings gear; the shop bar over the bench on phones (#8, #9) — ui/gameComponents.js, render/projection.js, ui/fieldHost.js, render/app.js, css/devices.css
- #8: the ⚙ was a hand-written SVG path with uneven teeth on a rim that was not round (the extracted autochess UI has no gear sprite; 交流 is the official
emoji_btn, 📖 / ⛶ were clean).gearPath()now generates a regular 8-tooth gear (tip r 10, root r 7.4, hole r 3.3); on phones the corner glyphs aremax(.28rem, 17px), half their 34 px buttons (were 11 px). - #9, the user's viewport: the screenshot is 2772×1272 px with the page right of a 141 px cutout band; the page's own markers give DPR ≈ 3.48, so 756×366 CSS px (≈ 2.07:1) — the mock rendered there puts the exit and 准备就绪 buttons on the screenshot's pixels within ±1 px.
- Cause: the official shop camera (
left_shop_camera_param, boss*_boss_shop_*) fits the prep area exactly at 16:9 (bench near edge 811 px vs the shop bar at 813 px at 1920×1080), while the remake's HUD is in rem with a 40 px floor (css/theme.css), so below 432 CSS px of height it is relatively taller: v2.3 left the worst bench pad 54 % free at 756×366 (Final Assault prep 49 %), 71 % at 844×390, 49 % at 800×360. - Now: the view receives the HUD bands,
hudBands(kind, size)(top = the HUD layer's safe-area top + 2.16rem, the bond strip; bottom = 2.64rem + 3 px, the shop bar; the rem values copy the CSS, which a test checks), andpresetCamera('prep' | 'bossPrep', …, { hud })runsclearHudon the band from the bench's near edge to the back row: the official camera when it fits (0.5 px tolerance), else the smallest pan, else a zoom-out about the centre — a 2D pan / zoom of the image, so picking, the three.js camera and flights stay consistent. Every desktop prep camera is unchanged; the bar stays on the bottom edge (§18.1); the collapsed shop keeps the camera (nothing jumps) — the official client moves to its closerleft_prepare_camera_paramthere, so on a phone the remake's collapsed prep board stays at the clearing zoom (×0.844 at 756×366; integration QA, an optional follow-up:shop: !collapsedfor the own prep camera with a 0 bottom band). Accepted: an armed shop card rises 4 px above the bar on a phone and covers ≈ 3 px of the pads' near corners (≈ 11 px under the official camera at 1920×1080). ASSUMED the bond strip's bottom (2.16rem, measured 2.14–2.15rem); the zoom anchor. - Tests:
test/render/projection.test.js(HUD clearance suite),test/ui/playtest5-ui.test.js; browser (opt-in,SP_E2E=1)test/ui/playtest5-ui.e2e.test.js(per-tile coverage at 16 viewports, the armed card, the gear).
| Viewport (CSS px) | Worst bench pad free, v2.3 → now (prep / Final Assault prep) | Camera now | Tile px |
|---|---|---|---|
| 756×366 (the user's Android) | 54 → 100 % / 49 → 100 % | ×0.844 | 41.5 → 35 |
| 798×366 + 41 px inset (same phone over the cutout) | 53 → 100 % / 49 → 100 % | ×0.844 | 41.5 → 35 |
| 844×390 (19.5:9, with or without notch) | 71 → 100 % / 66 → 100 % | ×0.905 | 44.2 → 40 |
| 800×360 (20:9) | 49 → 100 % / 45 → 100 % | ×0.828 | 40.8 → 33.8 |
| 915×412 / 914×411 / 960×411 | 83 → 100 % / 83 → 100 % | ×0.95 | 46.7 → 44.5 |
| 1920×1080, 1680×1050, 1560×1040, 2560×1080 | 100 % | official | unchanged |
| 1280×720 | 100 % / 96 → 100 % | official (Final Assault prep: pan up ≤ 2.8 px) | unchanged |
19.9 至简's price (#11) — sim/content/garrisons/meta.js SERVER_CHESS_PRICE, server/match/effectsMeta.js dispatch
- What the user saw: 至简 (Ⅲ) cost 2 资金; the user remembered 1. His 特质 garrison_13 reads 购买价格为1, but
bb.priceis 2, and the handler set the price tobb.price("blackboards are authoritative"). - Official: two chess carry this 特质 (
effectTypeSERVER_CHESS_PRICE) and both texts say 购买价格为1 — 至简 (Ⅲ, tier price 3,price2) and 红豆 (Ⅰ, tier price 2,price1, hidden in 下半). Only "bb.priceis the discount off the tier price" gives the text's 1 for both (3 − 2, 2 − 1). - Change: the handler lowers the price by
bb.price(ctx.modifyPrice(-p)), andEffectDispatcher.dispatchruns the priced chess's own 特质 first ononPrice(before globals / band / bonds), so 远见's discount (−1, never below 1) and strategy caps act on the lowered price: 至简 stays at 1 with 远见 150 instead of dropping to 0. The shop card keepsbasePrice3, so the price shows in the discount tone. docs/META.md §2.2 / §2.3 say the same. - Tests:
test/content/garrisons_meta.test.js(every SERVER_CHESS_PRICE owner: tier price −bb.price= the text's price =priceOf; buying 至简 costs 1; a global modifier already sees 1; 远见 150 leaves 至简 at 1).
20. User playtest #6 (v2.5): summons, skill triggers, blocking, push force, enemies, boss pool, bounties, 机变 cards, element gauge; leader HP, elite to the board, official limits
Nineteen reports after playing v2.4 (#1–#3 from a friend of the user). Where each is handled: #1 凯瑟琳's device could not be placed by hand → §20.1; #2 赫默's drone could not be placed by hand → §20.1; #3 忍冬's 叙拉古 stealth never ended → §20.2; #4 the bounty enemies kept coming instead of stopping after two battles → §20.6; #5 the Final Assault leader at 0 HP kept fighting until the team LP ran out, and the result still said 胜利 → §20.5; #6 the 道具补给 cards showed no description → §20.7; #7 in 联防 the leaker's count should fall live as teammates kill (it seemed capped at 10) → §20.6; #8 新约能天使 S2's shield on teammates seemed not to work → §20.2; #9 威龙 was drawn far too large → §20.4; #10 on the phone the shop's confirm strip did not buy (only the middle of the card worked) → §20.7; #11 the element gauge seemed to fill at the wrong rate → §20.8; #12 萨卡兹枯朽战车 hit too softly → §20.4; #13 “萨科塔之翼”'s 战栗 (small random flight) was missing → §20.4; #14 enemy weight vs push force (野鬃 pushed too many enemies) → §20.3; #15 skills whose range is wider than the attack range did not fire when an enemy entered it → §20.2; #16 古米's ally heal fired only after she engaged an enemy → §20.2; #17 the front operator that took over an enemy held between two operators never attacked it → §20.3; #18 炎佑 was healed by medics → §20.3; #19 the 3-choose-1 promotion reward often offered two identical operators → §20.6.
Eight workstreams were merged into playtest6 (WH → WG → WD → WC → WB → WA → WF → WE); the reviews' last rounds and the cross-workstream notes were applied on top: a stocked skill summon deploys as soon as its owner is back (§20.1), the AUTO DP skills 焰尾 S1 / 凛御银灰 S1 fire on full SP (§20.2), 锏 S3's slashes give 无敌 and 晕眩 / 冻结免疫 (§20.3), the AUTO_OP_COOLDOWN comment and the SKILL_RANGE count (13 skills / 26 records) were corrected. The user then answered the open questions (2026-10-01); the answers are settled decisions, recorded in their sections and in §20.9, each with the one-line change that flips it: the placed skill summons deploy once at the battle start and 凯瑟琳's device stays a hand card (§20.1), every blocker hits what it blocks (§20.3), 萨卡兹枯朽战车 follows the re-research — ranged enemies reach an operator by its 0.25 collider (§20.4) — and multi-round bounties last two battles (§20.6). Normative lines rewritten with it: §2 (fear.js), §3 (the enemies' reach), §5.2 (ASPD floor 20), §5.3 (camou, fear, noHeal, isolated), §5.4 (push / pull / ally-selection helpers), §5.5 (initial deployment, blocking, operator attacks, healers, enemy AI, end), §5.6 (skill triggers, the operation cooldown), §6.1 / §6.2 (the Final Assault verdict, summon stacks, summons in the hand, the promotion reward), §7 (bounty draft, 教鞭), §8.2 / §8.3 (uniteLeft, descRaw, counterText, the elem fill, the stealth bit), §9 (gauge row, model scale), §10 (机变 card, the card tap), §14 (left, the pool floor, the verdict, uniteLeft); superseded or revised: §17.5 (the 联防 count), §18.3 (the gauge look), §18.6 #11 (skill summons), §19.2 (the melee-only blocked-first rule; 灰毫 S2), §19.5 (the gauge look).
Follow-up (playtest6b, 2026-10-02). Four more workstreams were merged into playtest6b in this order: the elite-to-board rule (§20.11), the 假想敌:胄 kit audit (§20.13), the Final Assault leader HP report (§20.10) and the official limits read from the client binary (§20.12). Reports: co-op 终极 / 绝境 leaders died within seconds although the official leader never dies that fast with such layers (§20.10); "官方就是合成精锐时,如果消耗了场上的干员,精锐会出现在场上那个位置" (§20.11, user-confirmed). The boss-HP and the limits workstreams both capped bond layers at 999 — one implementation remains (shared/constants.js BOND_LAYER_CAP / layerGainRoom, used by every writer), and the boss-HP workstream's reading of 限伤 as an engine fixed-point overflow "not modelled" is superseded by the binary evidence (MAX_BATTLE_DAMAGE 300000, the hit is cancelled). Normative lines rewritten with it: §2 (promote.js), §5.1 (layerGains capped), §5.2 (直接乘算 in Σpct), §5.3 (same-named debuffs), §5.4 (addLayers, loseHp), §5.5 (限伤 between the multipliers and the shields), §6.2 (where a merge's elite goes), §6.3 (the layer cap), §7 (leader parts, drones), §8.2 (hitCap), §9 (the promotion cue), §14 (the layer bound of client results); §20.9 records the decisions. Two QA verifiers then checked the integrated build; their residual issues are handled in §20.14. A later report after v2.5.1 — the bond strip should follow the watched teammate in every watch situation — → §20.15. The player reports after the public 0.1.0 release → §21 (v0.1.1).
20.1 Summons placed by hand (#1, #2) — tools/build-data.mjs, match/gamedata.js placeableTokens, PlayerState, match/bot.js, sim/Battle.js, content/tokens.js, kits tier2/3/4, ui/detailPanel.js, shared/constants.js SKILL_SUMMON_START_DEPLOY
- Seen (the friend): 凯瑟琳's device and 赫默's drone could not be placed by hand. Cause: v2.3 (§18.6 #11) limited hand pieces to talent summons after the user's playtest #4 report on the drone, which dropped 医疗探机 and 诅咒娃娃; 爬行号·防护单元 has no shop-state entry, so 凯瑟琳's kit placed it automatically.
- Official (PRTS 卫戍协议/帮助): §战斗部署 "如果部署的干员拥有可手动部署的附属召唤物,则该召唤物会立刻加入手牌区" — deploy-limit copies ("非初始持有量"), one stack per operator, no deploy slot (token pages: 部署占用数 0); withdrawing the operator removes its summons, moving it "其所属召唤物全部退场并重置至手牌区". §手牌区: summons left in temp "会于下一回合返还". §作战阶段: placed summons deploy after the operators; "所有手动部署的召唤物,无视所属干员的持有状态…作战开始时立即部署一次(例如…赫默的医疗探机)"; one that leaves "将在满足条件后立即原地再部署1个". Token 备注: 诅咒娃娃 is withdrawn when 巫恋 leaves; a deployed 医疗探机 stays when 赫默 leaves.
shopStateTokenDict(55 entries): 医疗探机 / 诅咒娃娃 / 狼群 / 海嗣 / 流形 DEFAULT, 投递坐标 and the battle-only tokens HIDDEN, andtoken_10041_cathy_catsldthe only pool summon with no entry. - Now (data / prep): tokens.json
placeable= not HIDDEN and made by an owner's talent or skill — 医疗探机 (赫默 S2), 诅咒娃娃 (巫恋 S2), 爬行号·防护单元, 海嗣, 狼群, 流形.placeableTokens(chessId, loadout)follows the equipped skill (赫默 S1: no card) and gives the deploy limit (凯瑟琳 2). A moved or swapped operator sends its placed summons back to their stack (PlayerState._liftTokensOf; a summon dragged onto an operator stays where it was dropped when it stays legal — a range-bound summon inside its owner's range from the owner's new tile, §21.3);startRoundtops the board owners' stacks back up, so a stack removed from temp returns. - Now (battle): talent pieces (狼群, 海嗣, 流形, devices) deploy with the board, after the operators. A skill summon's piece also deploys once with the board, free (settled below), then is docked on its tile (
dockSkillSummons, the tile stays reserved):releaseSkillSummonadds one to the owner's stock per cast (≤ 1) and the piece takes its tile once its 5 s redeploy time has passed, free, never while its owner is off the field — a stocked one deploys as soon as the owner is back (review round 3: adeployhook on the owner; it used to wait for the next cast). The drone lasts 10 s and stays when 赫默 leaves; the doll lasts 15 s and leaves with 巫恋. Not placed: 医疗探机 / 诅咒娃娃 / devices / 海嗣 never appear; 狼群 / 流形 still come as 援军 on a tactical point (inside the tactician's range, never water, §21.3). The 爬行号·防护单元 kit shields the operator on the tile it faces (range 1-1): the full shield on each new target, invulnerable, keycathy:shield. Bots place the cards (a device next to the best operator no device faces yet, facing it); every bot layout first lifts the placed summons and places them again, so a device never stays beside a tile its operator left (residual QA: 3.5–8.6 % of the bots' devices faced nothing). The summon card says how it takes the field (summonDeployHint, reading the switch) and shows the token's talent / skill with the owner's variant (golden 巫恋's doll −30 %, golden 赫默's drone ATK 114). - Settled by the user: (1) the placed skill summon deploys once at the battle start, the PRTS rule —
SKILL_SUMMON_START_DEPLOY = truedrives the sim and the card hint (PLAYING §4 and SIM.md follow;test/ui/playtest6_summons.test.jschecks). The playtest #4 report ("是赫默开技能释放一次,不是开局直接就部署了") had been made against v2.2, which already deployed the drone the PRTS way. Flip:SKILL_SUMMON_START_DEPLOY = false(only with the skill). (2) 凯瑟琳's device stays a manually placed hand card (no shop-state entry read as shown). Flip: droppingtoken_10041_cathy_catsldfromplaceableintools/build-data.mjsonly hides the card — the v2.4 automatic placement in her kit would have to come back with it. - ASSUMED: skill-summon redeploys cost no DP; no deploy while the owner is off the field; a swap lifts both operators' summons; the device is invulnerable.
- Tests:
test/match/playtest6_summons.test.js,test/content/playtest6_summons.test.js(start deploy, each cast, the owner's redeploy),test/ui/playtest6_summons.test.js,test/content/{playtest4_summons,tokens_devices,kits_t3,summon_loadout_conflicts},test/sim/{loadout-summons,docs-examples}; opt-intest/ui/playtest6-summons.e2e.test.js(SP_E2E=1).
20.2 Skill triggers and buffs (#15, #16, #8, #3) — tools/build-data.mjs resolveTrigger, sim/skills.js, sim/Battle.js, sim/targeting.js, sim/snapshot.js, kits
- #15 seen: a skill whose range is wider than the attack range did not fire when an enemy entered it (德克萨斯 剑雨 13 tiles vs 2; 凛御银灰 S2; 锏 S3). Cause: every MANUAL skill used the basic rule, and research 03 C1 read the class rows (
skillIndex 0) as "skill 1 only" (BWIKI). - Official (PRTS 卫戍协议/帮助 §作战阶段 技能操作): basic rule "技能就绪,且即将进行普通攻击/治疗"; a skill with its own 技能范围 (not an attack-range increase) "不通过普通攻击/治疗触发技能,仅在技能范围内存在敌人(无视其不可选中)时释放技能"; class strategies — 重装 "不受技能范围影响,受到伤害时释放技能" (下半, 2026/3/14), 战术家 / 执旗手 / 吟游者 when ready, 解放者 / 阵法术师 an enemy in the initial range, 澄闪 / 纯烬 / 荒芜 S3 "全场存在可选目标时"; "自动操作具有3s冷却,在完成一次操作或作战开始时部署的单位将进入冷却".
- Now (§5.6): the class rows cover every MANUAL skill of the class and never an AUTO one — an inference (PRTS names whole classes; under the old reading the 阵法术师 row could never cover 薄绿's default S2, a phalanx never attacks with its skill off); counter-evidence kept: BWIKI's "技能1" and the data's
skillIndex 0/-1sentinel.SKILL_RANGE: 13 skills (26 records with the elites), any living enemy on the skill range, checked every tick (attack-range changes keep the basic rule).GDGLOW_SKILL_2: a valid target anywhere. The 3 s operation cooldown after each automatic cast and after the battle-start deployment (flags.startOpCooldown; the test harness sets it to 0); kit-driven casts of MANUAL skills (波登可 S1, 雪猎 bullets, the 流形 copy) checkskill.opCooling; AUTO skills are exempt; a pending "next attack" never takes a second charge. Players notice: every 重装 manual skill (号角 S3, 瑕光 S3, 灰毫 S2 — reversing §19.2 —, 古米 S2 …) now waits for the operator to be hit. - Integration follow-up (WE review round 2): the AUTO DP skills 焰尾 S1 迅敏直觉 and 凛御银灰 S1 周旋的谋略 ("立即获得…部署费用") are
SP_FULLlike 伺夜 S1 and 德克萨斯's 冲锋号令·γ型 — an AUTO skill takes no 技能策略 (they used to wait 12–16 s for the next attack). - #16: 古米 备用军粮 (AUTO) had picked up the 重装 row. PRTS 备注 "存在生命值不满的可治疗角色时可触发;…切换至治疗模式…直至古米完成一次普通攻击的治疗": it fires on an injured ally of its x-4 area and her next attack heals the most injured ally there (
heal_scale× ATK); S2 食粮烹制 is MANUAL, so the 重装 rule. Audit: 塞雷娅 S1 急救 — PRTS 备注 "仅在周围有符合血量条件的友方单位时可触发,触发时会替换当次攻击", 修正 "血量小于等于一半" — is checked at her attack (DEFAULT +allies+hpAtMost0.5) and that attack becomes the heal; a cast whose ally recovered first is withdrawn with its charge. - #8: 新约能天使 S2 gave the shield to the fastest attacker; official (PRTS 备注) "攻击范围内仇恨值最高的我方干员" — now
aggroCmp(taunt, then the latest deployed), throughbattle.alliesInGrid(孤立 units skipped). The amounts were already right: 180 % (精锐 200 %) of each holder's own max HP, decaying by initial / 30 per s. - #3 (friend): the bond 隐匿 already ended on time (
bondeffect_siracusa32 + 0.4 × layers s). 忍冬 S3 "进入迷彩状态,直至下一次开启技能" was the genericstealthunder the keystealth, which made 伪装服's timed 隐匿 permanent (or stripped it) and counted as 隐匿 for 叙拉古 / 家族徽章. Now a separatecamouflag and status (vulpis:camou; 寒芒克洛丝 S1 too) that works like 隐匿 for targeting and on screen — only the enemy it blocks attacks it (PRTS 异常效果 "※迷彩与'阻挡时解除'没有直接关系"; §19.1 kept). What stays see-through after a kill is that official 迷彩, until her next cast. Declined (optional review UX item): a distinct 迷彩 look — the base game draws both see-through, and the detail card has no status row. - ASSUMED: enemy skills and splash selectors skip 迷彩 like target selection; every initial deployment gets the full 3 s (the remake deploys everyone at 0 s); a token skill without a
skillTypecounts as MANUAL. - Tests:
test/sim/playtest6-skills.test.js, kit tests (incl.kits_alt_t4/kits_alt_t5for the AUTO DP skills),test/data.test.js,test/sim/docs-examples.test.js.
| Skill (examples) | type | before → after | why |
|---|---|---|---|
| 德克萨斯 S2 剑雨, 凛御银灰 S2, 锏 S2 / S3, 异客 S3, 焰尾 S2, 忍冬 S2, 诗怀雅 S1, 山 S3, 小满 S2, 归溟幽灵鲨 S1, 崖心 S2, 见行者 S2 | MANUAL | DEFAULT (kit CUSTOM_RANGE) → SKILL_RANGE | own 技能范围 |
| 深巡 S2, 古米 S2, 折桠 S2, 瑕光 S2 / S3, 信仰搅拌机 S2 / S3, 泥岩 S3, 塞雷娅 S3, 余 S2, 角峰 / 雷蛇 / 泡泡 / 菲莱 / 蛇屠箱 S2, 灰毫 S2, 星熊 S3, 号角 S2 / S3 | MANUAL | DEFAULT → TAKE_DAMAGE | 重装 row |
| 薄绿 S2, 蜜蜡 S2, 卡涅利安 S2 / S3, 玛恩纳 S2 | MANUAL | DEFAULT → SEARCH | 阵法术师 / 解放者 row |
| 伺夜 S3, 魔王 S2 / S3, 浊心斯卡蒂 S3 | MANUAL | DEFAULT → SP_FULL | 战术家 / 吟游者 row |
| 古米 S1, 瑕光 S1, 塞雷娅 S1 | AUTO | TAKE_DAMAGE → own rule (古米: injured ally; 塞雷娅: at her attack, ally ≤ 50 %) | AUTO: no 技能策略 |
| 焰尾 S1, 凛御银灰 S1 | AUTO | DEFAULT → SP_FULL | AUTO DP skill (integration) |
| 银灰 S3, 深靛 S1, 野鬃 S2, 蒂比 S1, 断崖 S2, 忍冬 S3 … | MANUAL | DEFAULT (unchanged) | attack-range change ("非攻击距离增加") |
20.3 Combat rules: blocking, ASPD floor, push / pull force vs weight, 炎佑 / 孤立 (#17, #14, #18) — targeting.js sortEnemyTargets, Battle.blockedTargets / push / pull / pullToFront / allySelectable / alliesFor, constants.js ASPD_MIN / PUSH_TILES(_EFFECT), kits, tools/build-data.mjs TOKEN_ABNORMAL
- #17 seen: an enemy held between two operators; after the rear one died the front one took it over (§19.2), but only the enemy attacked. Workstream finding: every melee hand-over hit the enemy (3360 immediate hand-overs, 215 late ones — the enemy 0.52–0.69 tile behind the front operator's centre); the cases that did not were a ranged operator on a melee tile with the enemy behind it out of range, the playtest #5 QA rule (§19.2, PRTS 索敌的概念 "我方索敌优先级:阻挡(近战限定)"; "远程位干员…甚至如果这个目标被阻挡在该干员的身后…无法攻击到这个敌人") — the likeliest reading of the report.
- Settled by the user — "阻挡了就一定要能打到": officially the collision pushes a blocked enemy to its blocker's front, so whatever blocks an enemy can hit it. Now every blocker — melee units, summons and a ranged operator on a melee tile alike — may always target the enemies it blocks, in range or not, whatever its facing, and targets them first (
Battle.blockedTargetswithout the melee gate;sortEnemyTargetsblocked-first for every attacker; the skills' DEFAULT trigger follows);targeting.js meleeUnitis gone. 深靛's kit (canAttack, the bound-target retarget) looked at her range only and now includes her blocked enemies; so do the skills that pick their own targets from the range (residual QA): 迷迭香 S3 (blocked enemies only — her own included), 荒芜拉普兰德 S2's drone locks, 佩佩 S2's random pick, 灵知 S3's unfrozen-first order and 远牙 S3's own-range filter. Two skill texts still win over the rule: 普罗旺斯 S2 "普通攻击不再以生命值高于80%的敌人作为目标" and 寒檀 S2's icicles on random tiles of her range. Healers (they keep healing while they block, §21.17) and 停止攻击 skills (泡泡 S2, 蛇屠箱 S2, 古米 S2, 凯瑟琳 S2, 菲莱 S2, 宴 S1, 铃兰 S3, 信仰搅拌机 S3) still never hit. Flip: gateBattle.blockedTargetsandsortEnemyTargets' blocked-first on a melee position again (the v2.4meleeUnit). - ASPD floor: 沼泽 −50 + 寒霜 −50 took ASPD to 10 (塞雷娅 swung every 12 s); officially "ATTACK_SPEED 默认下限 20" (PRTS 数值范围; 游戏数据基础 "实际被限制了下限为20") —
ASPD_MIN20, also in the bot's DPS estimate. - #14 seen: 野鬃 S2 pushed too many enemies. Cause: every push / pull moved 0.5 + 0.5·力度 tiles, cut 25 % per missing weight level. Official (PRTS 游戏数据基础 §重量公式, 推与拉): 受力等级 = 力度 (微小力 −1, 小力 0, 中力 1, 较大力 2, 大力 3) − the current 重量等级 (失重 counts). Push, 弹道 column: ≤ −3 → 0, −2 → 0.12, −1 → 0.44, 0 → 1.7, 1 → 2.14, 2 → 2.96, ≥ 3 → 3.53 tiles; 特效 column (见行者 S1 / S2, named 特效类): 0.085 / 0.374 / 1.562 / 1.987 / 2.773 / 3.331. Pushes follow the client buff template (§21.13):
knockback[dir]= directional (推击手, 野鬃 S2), turning radial at −2 when the target is > 45° off or < 0.25 tile away;knockback[relative]= radial (琳琅诗怀雅 S3, 山 S3, 莫斯提马 S3); 圣聆初雪 S1 朝部署方向 = KnockBackWithCharacterDirection (no correction); 见行者 S2 waives the angle only. Pull: ≥ 0 all the way (拉力起点 0.5 tile ahead, 急停 0.6708), −1 → 35 %, −2 → 0.03, ≤ −3 → 0; 薄绿 S2's 拖拽 is a push towards her. Now:Battle.push / pull / pullToFront / forceLevel / pushDistance,displacethe raw mover; every source migrated. 野鬃 S2 on weight 0 / 1 / 2 / 3 / ≥ 4: 2.14 / 1.7 / 0.44 / 0.12 / 0 tiles (light enemies now move further — official). Enemy weights equal enemy_database for all 249 enemies. - 锏 S3 (PRTS 备注 "※可对空…※多段斩击期间,自身获得无敌,晕眩免疫,冻结免疫"): its slashes reach air units (※可对空, added to §19.1's list) — the mode's air units are 静态刚体: hit, never moved (a non-static flyer would be pulled, §21.9) —, and during the slashes she is invulnerable (buff
blkkgt:slashes) and refuses stun / freeze (integration follow-up of the WF note). - #18: PRTS “炎佑” 天赋 "{{特殊机制|我方单位}},{{异常效果|孤立}}"; 孤立 = ALLY_TARGET_FREE — PRTS 选择器 可选判定: a friendly selector never picks it, and selectors decide heal, buff and talent targets. 禁疗 (“小自在”, “耀阳”, 斯卡蒂的海嗣, 沙之碑, 流形, 狼群, 迷迭香的战术装备, 黄金盟誓, 保护目标(冻结状态)) blocks heals only; a 生命回复速度 attribute still applies (PRTS 安洁莉娜 备注). Now: tokens.json
abnormal(TOKEN_ABNORMAL) givesnoHeal, 孤立 alsoisolated;Battle.allySelectable / alliesForback every ally selection (alliesInGrid(Of), auras, 全场 talents, zone / grid buffs, heal picks — a heal pick skips 禁疗 units instead of wasting the heal); enemies still target 炎佑; a summon aimed at injured allies (findSummonTile 'ally') skips them too. - ASSUMED: the 失衡 slide is instantaneous; a 静态刚体's 0.1 s 失衡硬直 is not modelled (§21.9); unclassified pushes use the 弹道 column; a puller never moves the enemy it blocks; 薄绿's inward push stops at the 急停 radius; 溯光星源's pull stops 0.3 tile from the target; 全场 talents select through a selector (skip 孤立); no unit-specific ASPD floor.
- Tests:
test/sim/playtest6-combat.test.js(the user's layout on act2 m01 × 4 facings; the late hand-over and a plain block for every blocking chess × 4 facings, ranged blockers and out-of-range enemies included; the skills that pick their own targets, 普罗旺斯 S2 the exception; ASPD; push / pull tables; 孤立 / 禁疗),test/sim/playtest5_blocking.test.js(the ranged blocker hits and casts for the enemy behind it),kits_t6(锏 S3), expectations moved in core, engine-requests, facing and the kit suites.
| Displacement source | Official 力度 / kind | v2.4.1 | Now |
|---|---|---|---|
| 野鬃 S2 | 中力 directional (knockback[dir], §21.13); > 45° off / < 0.25 tile → radial −2 | 1.0 tile, −25 % per weight above 2 | 弹道 along her facing: w0 2.14, w1 1.7, w2 0.44, w3 0.12, ≥ 4 0 |
| 山 S3 (2nd hit) | 中力 radial (knockback[relative]) | 1.0 tile, −25 % per weight above 2 | 弹道: w0 2.14, w1 1.7, w2 0.44, w3 0.12, ≥ 4 0 |
| 莫斯提马 S3 | 小力 radial | 0.5 tile | w0 1.7, w1 0.44, w2 0.12, ≥ 3 0 |
| 琳琅诗怀雅 S3 | radial (knockback[relative], PRTS 备注), ground only (§21.13) | 0.5 tile along the facing, flyers too | push table, radial from her centre |
| 圣聆初雪 S1 | 中力 directional, fixed (KnockBackWithCharacterDirection), ground only | 1.0 tile along the facing, flyers too | push table along the facing |
| 见行者 S1 / S2 (hidden) | 中力 推击手, 特效类 | 1.0 / 1.5 tiles | 特效 column (w0 1.987 … w3 0.085) |
| 崖心 S1 / S2, 歌蕾蒂娅 S1–S3, 百炼嘉维尔 S2, 乌尔比安 S1 | pulls 至面前 | ≤ 1 tile or all the way, scaled | pull table (w ≤ 1 all the way, w2 35 %, w3 0.03, ≥ 4 0) |
| 锏 S3 | p_force 1 / e_force 2, 可对空 | ≤ 1 / 1.5 tiles, ground only | pull table, 急停 0.6708; air units hit, a 静态刚体 not moved (§21.9) |
| Air units of the mode | 静态刚体 (PRTS 特殊机制) | moved like ground ones | 0 tiles from every source, still hit (§21.9) |
| 薄绿 S2 | 小力 拖拽 = push towards her | pull ≤ 0.5 tile | push table towards her |
| ASPD floor | 20 | 10 | 20 |
20.4 Enemies: 萨卡兹枯朽战车, the “萨科塔” 恐惧, 威龙's size (#12, #13, #9) — sim/fear.js, ai.js, grid.js straightClear, Battle.applyStatus, content/enemies.js (kitTank, pollution), constants.js ALLY_COLLIDER_RADIUS, tools/build-data.mjs MODEL_SCALES, render/units.js
- #13 seen: “萨科塔之翼” below half HP should fly about at random in a small area (the user wrote 战栗) but hung still. Cause: every feared enemy was frozen. Official: the bird fears itself — PRTS "生命值首次低于50%时,对自身施加持续5s的恐惧,在5s内移动速度最终提升至150%" (之眼 / 昂首 the same); 恐惧 = "无法被阻挡并四散逃跑" (ba.fear), 战栗 = "被阻挡后无法进行普通攻击" (ba.tremble), so the behaviour described is 恐惧 — report and data agree, only the name differs. PRTS 诱发移动 §恐惧: 恐惧可达地块 = tile centres within 10 of the hit point, in the ±45° fan away from the source, passable, able to reach the goal, not the objective; next stop a random reachable tile within 5 path steps (offset in a 0.5 square), else its own tile; no source / itself as source ⇒ no reachable tile; 恐惧 outranks 诱导.
- Now:
fear.js; the birds flutter within ±0.25 of their tile for 5 s at ×1.5, then fly on. Operator fears (叙拉古 6, 妮芙 S2, 荒芜拉普兰德 S3) make enemies flee into the fan instead of freezing (a visible change). The 恐惧 and 诱导 walks re-plan from where the enemy stands after a push or an obstacle change (grid.version; PRTS "恐惧效果结束时立刻清除【恐惧移动】"); a checkpoint on an obstacle tile counts as unreachable; an off-centre ground start steps back to its tile centre when the straight line would clip a wall (grid.js straightClear, shared). They used to walk through walls after a push (4–7 % of random pushes on m01–m04). - #12 seen: 萨卡兹枯朽战车's damage felt low. Already right: stats (lv0 HP 12000 / 15000, ATK 750 / 850, DEF 750, RES 10, BAT 4.5, range 2.2, weight 4; no season override, no wave
overwrittenData), the round multipliers, the target filter (只攻击位于低地的我方单位,且不会攻击飞行单位) and the 200 % blocked melee attack. Fixed (WG): 秽蚀轰击 timing (initSp 2 = maxSp 2 ⇒ attacks 1, 4, 7 …, not 3, 6 …); the 【污染秽蚀】 radius 1.7 (was 1 ASSUMED); its damage "每秒受到50/25点真实普通伤害…同名效果不叠加" — true damage throughdealDamage, one tick per second per unit (also 萨卡兹枯朽战士's death zone, r 2, 8 s; zones are now absorbed by shields, scaled by damage-taken modifiers and fire TAKE_DAMAGE skills). The skill hit is 100 % (PRTS skill text), also against a blocker [ASSUMED that it fires there]. Measured: roughly unchanged overall (first 20 s +4…+42 %, a long blocked fight −7…−9 %). - #12 re-researched (the user's follow-up: "远程攻击射程挺远、伤害很高"): enemy_database (
enemy_1272_nhtank/_2: rangeRadius 2.2,PollutedRangedAtkrange_radius 2.2, atk_scale implicit 100 %,Empty.attack@chuang_atk_scale2, spData 攻击回复 2 / 2), the PRTS pages (攻击方式 近战 远程; "仅半径2.2范围内存在位于低地的非飞行我方单位时可触发:对目标造成100%物理普通伤害"), activity_table act2autochess (DOT special, no stat override; the range-4 tank is 斗智场's 改装排污车, another mode) and the act1 / act2 autochess levels: there is no higher scale and no longer data range. The hard hits are the round multiplier (table below: 绝境 R7 998, 尖端 R13 1656 per ranged hit before DEF). What the data does support is a longer effective reach: PRTS 作战机制 §碰撞体积与位置识别 "我方干员碰撞体积基本均为以0.25格为半径的圆形…关于碰撞体积与远程索敌,碰撞箱碰撞是为最常用的方式,索敌抬手等均使用碰撞箱" — the remake measured enemy range from the operator's centre. Settled: every ranged enemy's normal attack takes an ally whose 0.25 collider touches its range circle (ALLY_COLLIDER_RADIUS,ai.js enemyAttack: centre distance ≤ rangeRadius + 0.25; content skills / zones keep their own point radius ASSUMED). 枯朽战车: reach 2.2 → 2.45 (13 → 21 tiles around it); a lone operator beside its lane takes 4 hits instead of 3 (绝境 R6, DEF 300: 2095 → 2793 attack damage, 2595 → 3593 with the zone); dense lineups unchanged (its 4.5 s interval is the limit). The user's memory of a long range is stronger than the data supports — said plainly in the user notes. Flip:ALLY_COLLIDER_RADIUS = 0. - #9 seen: 威龙 drawn far too large. Official: the client scales each enemy Spine model in its battle prefab — 0.27 for 1454 of 2147 prefabs; 威龙 0.16, 妖怪 0.20, 妖怪MKII 0.22, the 岁 relics 0.4–0.6. Now:
MODEL_SCALES(122 prefabs) → enemies.jsonmodelScale= product / 0.27 (125 keys);render/units.jsscales the skeleton and the bar height (enemies without setup-pose bounds: chibi headroom ×modelScale). 威龙 is 1.28 tiles wide instead of 2.15 — 1.08 × a 妖怪, as in the game. 85 enemy skeletons still have no bounds in data/assets.json (asset pipeline, not addressed). - Tests:
test/sim/playtest6-enemies.test.js(flutter, determinism, operator fear, 恐惧 > 诱导, push / obstacle re-planning on m01–m04, the tank incl. its reach),test/render/playtest6-enemies.test.js,test/local-extract.test.js(enemy_scales),combat.test.js; opt-intest/render/playtest6-enemies.browser.test.js.
| Round | Tank | 险境 ATK · ranged hit · blocked | 绝境 | 终极 |
|---|---|---|---|---|
| R1 | 萨卡兹枯朽战车 | 600 · 600 · 1200 | 825 · 825 · 1650 | 825 · 825 · 1650 |
| R4–R7 | 萨卡兹枯朽战车 | 726 · 726 · 1452 | 998 · 998 · 1997 | 998–1208 · … · 1997–2416 |
| R8 | 尖端萨卡兹枯朽战车 | — (banned) | 1131 · 1131 · 2263 | 1369 · 1369 · 2738 |
| R13 | 尖端萨卡兹枯朽战车 | — | 1656 · 1656 · 3313 | 1656 · 1656 · 3313 |
20.5 Final Assault leader at 0 HP that would not die (#5) — sim/constants.js BOSS_POOL_MIN_HP, match/finalAssault.js SharedBossPool, sim/spec.js LocalBossPool, match/Match.js _creditBoss / _onResult / _finishFinal, ui/hud.js PhaseCapsule, ui/gameLogic.js bossPctText
- Seen: 2 humans (AI teammates eliminated), 绝境, act2 m01, client-side combat — the boss bar read "0.0%" with 95 s left, the leader fought on until the overtime drain took the team LP, and the result said 胜利. Reproduced: real 2-human client-combat matches (pool scaled to 5 %): on v2.4.1 11 of 40 fights stalled, every boss family; about 11 minutes of drain followed, ending 胜利 or 失败.
- Cause: float dust in the shared pool, not a boss script, hit area or part.
_creditBosssubtracts each report per player and the pieces add up only in exact arithmetic (40000 − 12345.6 − 7654.3 − 20000.1 = 3.6e-12); the browser'sLocalBossPool(server hp − (cum − acked)) absorbed every later hit (less than half an ulp ofcum), so the leader never died, nothing new was reported, and m.public rounded the server's dust to "0 HP". When the team LP hit 0, the verdict was read from the pool after the finalb.result, whose per-player damage isMath.rounded — rounding up emptied the dust (胜利). - Official: PRTS 卫戍协议/帮助 §领袖作战阶段 "…计时结束后战斗仍然会继续,但每超时1秒扣除1目标生命值"; 卫戍协议:盟约 下半 "…使目标生命值扣除至0,则无视倒计时直接失败" (research 01 §10). The victory after the drain contradicted it; the user was right.
- Now: a shared pool never holds less than
BOSS_POOL_MIN_HP(1) — the hit, or the per-player credit, that would leave less takes the rest (SharedBossPool, LocalBossPool), and a browser pool reads any finite rest below 1 as 0, so a pool shown at 0 is a dead leader on every field (Battle._bossSync/_checkEnd) andb.end clearedgoes out at once. The first end the server registers decides (Match._finalEnding: pool 0 → victory, team LP 0 → defeat); after a forced end no report credits the pool. The boss bar prefers the live pool of the battle on screen over the ~1 Hzm.public.bossHpand never shows "0.0%" for a living leader ("<0.1%"). Checked and ruled out: the kill check reads the pool whatever the unit's own hp or invulnerability; mirrored and hidden-core copies share it; parts only pass damage; the Hidden Core, 4 humans and server-run mode end promptly. - ASSUMED: the 1-HP floor (official HP is a float shown rounded — PRTS 作战机制 "红字在显示时会就近取整"); a client emptying the pool within its latency after the server registered team LP 0 still loses; the HUD prefers the local snapshot pool.
- Tests:
test/sim/playtest6_bosspool.test.js,test/match/playtest6-bosspool.test.js,test/ui/playtest6-bosshud.test.js.
20.6 Match flow: the bounty draft, 教鞭, the 联防 leak counter, the promotion reward (#4, #7, #19) — tools/build-data.mjs bountyDraftExclusion, match/choices.js (MULTI_ROUND_BOUNTY_BATTLES), content/items/meta.js, Match.addBounty, PlayerState.pushRewardOffer / effectsView, sim/spec.js uniteLeft, Match._uniteLeft, fields.js uniteBillBounds, ui/hud.js MissTag, ui/teamPanel.js rowLp
- #4 seen: "悬赏决策的敌人…正常只会在选择之后出现两回合,现在是一直出". Cause: a 1–2-battle card already stopped after its battles, but the draft drew from all 129 ENEMY_GAIN cards (战术特训 and the hidden 鸭爵 set included) and every card was plain text (the server's stripped
descbeat the rich text), so a 每场 multi-round card looked like a 下场 one. - Official: PRTS 卫戍协议:盟约 下半/PRTS盟约记录 §机变阶段 "敌人轮选" lists the 1- and 2-battle bounties, 源石虫·特训 and the 7 multi-round cards and comments out the 鸭爵 set; 战术特训 sits under "※以下悬赏任务仅由法术教鞭生成"; §法术 教鞭 "于3个战术特训的悬赏任务中选择一项"; 杜宾 加练! "若自身战斗完美作战可获得资金". The card text colours the duration (
<@ba.vup>下场作战</>/两场作战,<@ba.vdown>每场</>). - Now: cards.bounty
draft= that table (105 cards: 56 next-battle incl. 源石虫·特训, 42 two-battle, 7 multi-round), drawn uniformly, no multi-round cap — superseded by §21.2 (the official draft structure of each round); bounty cards carrydescRaw; the effects column says 还剩 N 场作战 (counterText). 教鞭 draws 3 战术特训 cards and takes one at random (it used to draw the band bounties, so 战术特训 was unreachable); “神秘顾客” (granted by nothing in act2) keeps the band bounties ASSUMED. Not modelled: the one official draft screenshot hints at one series per draft. - Settled by the user — "我不记得有过多轮悬赏": until confirmed otherwise every official multi-round card (山海众头目·多轮悬赏, 多轮悬赏·假想敌 ×6, and 法术大师A2·多轮战术特训 from 教鞭) lasts two battles exactly like the "接下来两场作战" cards (no draft offers one since §21.2; 教鞭's card still lasts two battles):
MULTI_ROUND_BOUNTY_BATTLES = 2(bountyBattles;bountyTextrewrites "之后的 / 后续<@ba.vdown>每场</>作战" to "接下来<@ba.vup>两场作战</>" on the card and in the effects column, 还剩 N 场作战); choices.json keeps the officialrounds99 /multiRound. Flip:MULTI_ROUND_BOUNTY_BATTLES = nullrestores the official red 每场 everywhere (no data rebuild). - #7 seen: the leaker's number never fell during 联防 and seemed capped at 10. Official: PRTS 帮助 "防卫失败的玩家可通过上方信息栏确认自身所属敌人的剩余数量"; the capsule shows an orange runner tag ×N (research 09
tag_miss, drawn with ui/battle bg_miss_enemy ASSUMED); the 10-LP cap per round is unchanged. Now (revising §17.5):uniteLeftper leaker = not spawned yet + alive + counted leaks — it falls on kills and rises on splits / summons (children inheritsourcePlayerId); the server takes it fromb.progress left, the headless timeline[gt, killed, total, left]or the exactuniteSurvivors, clamps live values touniteBillBounds; m.public players[]uniteLeftis uncapped andpendingLp= min(cap, uniteLeft). ×N in the capsule and in the leakers' team rows; a client with the 联防 field on screen shows its local replica's counts there; a teammate's row tooltip names the teammate. - #19: PRTS 帮助 "临时刷新3名高于调度中心等级1阶(最多6阶)的干员"; the official 玩法介绍 image shows three different operators at price 0. v2.4.1 rolled three times independently (14–21 % of offers had a duplicate). Now three different chess, copy-weighted; a tier short of different chess tops up from the tier below ASSUMED; the offer reserves no copies. The integration QA found the same repeat in the other free pick-one chess offers — 信标 (same tier), 寻呼模块 (same bond ≤ shop level, content/items/meta.js) and the engine default
builtinMeta use_equip_reward_special_goods_char_chess— they now draw different operators too (信标 tops up from the tier below, 寻呼模块 shows fewer cards when the pool has no other one), andpushRewardOfferdrops a repeated id from any list it is given (test/content/items.test.js). - Tests:
test/match/playtest6-matchflow.test.js(the draft, two battles for a multi-round card end to end, 联防 counts, rewards),test/match/followups.test.js,test/ui/playtest6-unite.test.js,test/ui/playtest6-bounty.test.js,test/content/items.test.js(教鞭).
20.7 机变 card text; the shop's confirming tap on phones (#6, #10) — ui/choiceOverlay.js, css/screens/game-panels.css, ui/shopBar.js, css/screens/game-shop.css, css/devices.css
- #6 seen (screenshot 2, Android 756×366 CSS px, 终极 co-op): the 道具补给 cards showed only icon, name and tier. Cause: the card was a centred column sized for 16:9; with the root font clamped at 40 px a 3×2 card was 105 px tall and showed 0.2 lines of text (217 of 223 texts cut), in all four families; at 1920×1080 nine long texts were cut at 4 lines. Official (research 06 §11.5; screenshots Mb2mtd9 / Wrkhtxv / SEbySVl): a framed icon at the top-left with the bold name beside it, the description below, numbers in blue, the tier chip on the icon's corner, the taker's round avatar alone at the top-right. Now: that layout; the text from
cardText(the card's owndescRaw, else the datadescRawwhile its plain form equals the server'sdesc, else the server text), clamped to 7 lines (8 at line height 1.3 on phones under 700 px wide: with Noto Sans SC — the page's web font, metrics of Android's CJK font — 海沟实验体 wraps to 8 lines at 640×360, residual QA); short screens get a tighter overlay header (no CONTINGENCY micro line) and .19rem text; the taker badge is the avatar alone (name in its tooltip and the card's aria-label) and the taken card's head reserves its width. All 223 texts (51 items, 129 bounty, 43 tactic) fit in full, taken or not, from 640×360 to 2560×1080, measured with Noto Sans SC. - #10 seen: on the phone the second tap on the 确认购买 strip did nothing; only the middle of the card worked. Cause: the card's invisible ⓘ corner had the generic 44 px touch area (its 30 px override lost on specificity:
:is()scores 0,3,1 against 0,2,1), covering the bottom-right quarter of the card and the right half of the strip (120 / 144 grid points); below 768 px the left-docked detail card opened by the first tap also reached over the bar's first cards. Official (research 09 §5): first tap = detail + 确认购买, second tap buys; the card has no info corner. Now: the ⓘ is removed (a tap anywhere on a card is the card's tap),.shopbar.has-armed { z-index: 10 }keeps the armed bar above the detail card (z 9; below.uframe12 and.spov20), and a card that cannot be bought right now opens its detail (not aria-disabled, help cursor). 144 / 144 points on touch at 640×360 … 932×430 and with the mouse at 1920×1080; the strip centre, its right end and the bottom-right corner buy. - ASSUMED: phone text 7.6 px; the avatar inside the card's corner; the gold strip may cover the last line of the longest texts while a card is selected; the micro line hidden on phones; the armed bar hides the detail card's corner on < 768 px phones; solo 3-card sizes.
- Tests:
test/ui/playtest6-ui.test.js; opt-intest/ui/playtest6-ui.e2e.test.js(SP_E2E=1: every text at 9 viewports, each card taken once, the hit grid by tap target and real taps); mock.e2e (detail + Esc through a card tap).
20.8 Element gauge on screen (#11) — sim/damage.js elementView, render/units.js, render/textures.js
- Seen: correcting playtest #5, the user said the 海嗣's element damage is right after all, but the gauge "fills at the wrong rate: far from used up when the burst already hit". Measured (Node sim → interp → UnitView, and headless Chrome on the real view; 14 element enemies vs 古米, the reviewer with 5 operators against a mixed 海嗣 wave): the drawn gauge follows the sim within one frame. Steady attackers burst off a 1–5 % sliver; big hits legitimately burst far up the bar (掠海漂移体 does 50 % of its ATK × the round's ATK per hit — 25 % of the gauge at base, ≈ 44 % at 绝境 R11; 萨卡兹王庭军精锐术师 死亡之眼; 昆图斯's skills).
- Display faults, all fixed:
elementViewrounded the fill to the nearest 1 % (a gauge 1–4 元素值 short showed empty); the v2.3 ring right of the bars sat on the next operator's tier chip (read as the neighbour's gauge, and a shrinking ring reads like a filling one); during the 爆发冷却 the refill was the same white as the 元素值 left while the burst's stun / damage hit the unit. - Official (PRTS 元素): "当单位存在一项不为最大值的元素时,模型下部会显示对应的元素图标,并以白条显示剩余的元素值"; enemies get a small colour-only icon; during the 爆发冷却 "单位的元素条显示缓慢恢复至上限(动画效果)" (no colour given).
- Now: fill rounded DOWN to 1 %, kept at 0.01–0.99 outside a burst (1 during one) — the bar only runs out when the gauge bursts; the gauge is a row under the unit's own HP / SP bars inside their span: the element disc (0.15 tile, 8–15 px; enemies ×0.8, plain colour; light rim) and a white bar of the remaining 元素值 (SP-bar height, dark track); during the 爆发冷却 the bar refills in the element's colour (
ELEMENT_RING[el].tint) with the disc pulsing, white again when it ends. - ASSUMED: the refill colour and pulse; the geometry; placement under the above-head bars (official: under the model); the disc rim. The render-demo recordings carry fills made with the old rounding (display only).
- Tests:
test/sim/playtest6_elemgauge.test.js,test/render/playtest6_elembar.test.js,test/render/downring.test.js; opt-intest/render/elembar.browser.test.js,downelem.browser.test.js.
20.9 Settled decisions and what stays ASSUMED
The user's answers of 2026-10-01 (playtest #6 follow-up):
| Decision | Settled rule | One-line flip |
|---|---|---|
| A placed skill summon at the battle start (§20.1) | deploys once, free, then with each cast (PRTS §作战阶段) | shared/constants.js SKILL_SUMMON_START_DEPLOY = false |
| 凯瑟琳's 爬行号·防护单元 (§20.1) | a manually placed hand card | drop token_10041_cathy_catsld from placeable (tools/build-data.mjs) — and restore the v2.4 kit placement, or the device never appears |
| Multi-round bounty cards (§20.6) | last two battles like "接下来两场作战", blue text — 教鞭's card; no draft offers one (§21.2) | server/match/choices.js MULTI_ROUND_BOUNTY_BATTLES = null (official 每场) |
| 萨卡兹枯朽战车's reach (§20.4) | data range 2.2 + the operator's 0.25 collider for every ranged enemy; scales per data (100 % ranged, 200 % blocked melee, skill 100 %) | server/sim/constants.js ALLY_COLLIDER_RADIUS = 0 |
| Blocking (§20.3) | every blocker hits the enemies it blocks, first, whatever its range or facing | gate Battle.blockedTargets / sortEnemyTargets blocked-first on a melee position again |
| A merge's elite (§20.11) | takes the consumed deployed copy's tile and facing, else the hand (PRTS 帮助 "若消耗已部署至作战区的干员,则发送至作战区对应位置"; the user confirmed it, 2026-10-01) | none — official; restoring the v2.5 hand-first rule means reverting PlayerState._mergeChess (merge 5f3842f) |
Official since the follow-up — read from the client binary (research 11, §20.12) or the official texts (§20.10, §20.13):
| Rule | Now | One-line flip |
|---|---|---|
| Bond layer cap (§20.12) | 999 per bond (MAX_GARRISON_STACK; AddBondCount stores min(L + n, 999)) — no longer ASSUMED |
shared/constants.js BOND_LAYER_CAP = 0 |
| 限伤 (§20.12) | a leader hit with ceil(d) ≥ 300000 in boss / hidden battles is cancelled (MAX_BATTLE_DAMAGE), not an overflow |
shared/constants.js BOSS_HIT_LIMIT = 0 |
| 直接乘算 (§20.10) | bond / strategy / item / 机变 / 特质 "+X%" are summed with each other and the skills' | server/sim/constants.js DIRECT_BONUS_STACKING = 'multiply' |
| Ground routes (§21.15) | the official flow field unless the playtest #2 route crosses strictly fewer floor tiles (a corner touch is no crossing) | server/sim/grid.js _buildField always use = o (reverses 战场#01's col-8 road) |
| Leader parts (§20.10, §20.13) | every damage a 剑 / 锤 / 碎铳之簧 takes is lost by its leader 1:1, 无来源 | server/sim/content/bosses.js PART_TRANSFER = 0.5 (v2.5's share; v2.5 also passed a 剑 / 锤's damage only while grounded) |
Settled by the user ("保持固定血量"): the co-op leader pool does not shrink when players are eliminated — config bossHpScale.aliveScaling is false (pool = bloodPoint whatever the number of alive players; one community note, 巴哈姆特 12294, said otherwise) (§20.10); flip: aliveScaling: true (× alive / 4).
Settled by the user (2026-10-02, after v2.5.2: "共享阵地按你说的规则就行"): on a shared field (联防, Final Assault / Hidden Core) the bond strip follows the player of the half the ‹ › pill points at; on 全景 or an empty half it shows your own bonds when you fight there, else the teammate picked with 前往查看 or the field's first player — never your own (§20.15); rule: public/js/ui/watchBonds.js bondOwnerId.
Still ASSUMED (each in its section): the class rows covering every MANUAL skill (§20.2); skill-summon redeploys free and never while the owner is away (§20.1); the 1-HP pool floor and the server-order verdict (§20.5); a short promotion tier topping up from below and “神秘顾客” keeping the band bounties (§20.6); the 弹道 column for unclassified pushes and the other displacement details (§20.3); 枯朽战车's skill firing on its blocker at 100 % and content enemy skills keeping a point radius (§20.4); the gauge's refill colour and placement (§20.8); the 机变 card's phone sizes (§20.7); the elite's tile among several deployed copies (the first in deploy order) and the elite's fresh summon stack (§20.11; a transformed carrier's tile no longer counts, §21.1); "strongest" rather than "latest" for same-named debuffs, which "最大生命值" the 胄 drone link reads (the pool), the 剑 / 锤 dive hits passing on, the springs' split and the solo pool × 0.25 (§20.10); the scene server clamping prep gains like the client, a cancelled hit showing nothing, a passed-on loss being one hit and the order blocks → 限伤 → HP shields (§20.12); 胄's unread max_hp 0.5, the untargetable 初始模式 and a replacement copy inheriting only the HP (§20.13); talent debuffs keyed per source unit (two copies of one operator each apply theirs, §20.14).
20.10 Final Assault leader HP and damage (user report after playtest #6, 2026-10-01) — server/sim/constants.js DIRECT_BONUS_STACKING, content/support directMods, sim/Battle.js applyStrongest / loseHp sourceless, content bonds / bands / items / choices / garrisons / kits tier4, content/bosses.js PART_TRANSFER / DRONE_LINK_BASE, match/gamedata.js bossPoolHp / bossPoolShare, match/finalAssault.js, data/config.json bossHpScale
- Seen: co-op 终极 / 绝境 leaders died within seconds; with the user's layer counts the official leader never dies that fast.
- HP and pool — official, unchanged:
bloodPoint[difficulty]= activity_table act2autochess bossInfoDictbloodPoint/bloodPointNormal/bloodPointHard/bloodPointAbyssof the current data (the cache equals upstream ArknightsGameData master as of 2026-10-01;test/match/bosshp.test.jspins all 40 values — e.g. 胄 247 500 / 675 000 / 1 800 000 / 3 600 000, hidden 胄 750 000 / 937 500 / 3 600 000 / 7 200 000). PRTS 盟约记录's leader table is an older (11月18日) revision: 铳 险境 and 胄 / 铳 / 萨米 绝境 differ, no 终极 column, no 卢西恩 row. The data has no per-player or per-field factor; leader HP ignoresenemyScale("领袖单位于服务器的生命值加成不受上述加成影响"). One pool for every boss field ("最终攻势中,所有人将一起对敌方领袖造成伤害"); notice 5114's "敌方领袖的总生命值不变" means the two mirrored copies of a pair field share it. - Pool size: co-op = bloodPoint whatever the number of alive players —
bossHpScale.aliveScaling(× alive / 4, one community note: 巴哈姆特 12294) stays off — the user chose the fixed pool (§20.9); solo × 0.25 ASSUMED. - Fixed — why leaders fell too fast:
- 直接乘算: "+X%" ATK / DEF / max HP from bonds, strategies, equipment, 机变 cards and per-layer 特质 are summed with each other and with the skills' "+X%" (PRTS 盟约记录 "…属性加成均为直接乘算"; 游戏数据基础 D_t = Σtᵢ, A = (A₀ + D_p)(1 + D_t)). v2.5 compounded them (×74 instead of ×12 at 600 精准 layers with four more bonuses).
DIRECT_BONUS_STACKING 'add', content/supportdirectMods; 炎佑 ×1.5 and the char_attribute_mul 特质 stay multipliers (§5.2). - Same-named enemy debuffs: one instance per target, the strongest (PRTS 作战机制 "同名buff…只能表现出一个"; 巴哈姆特 12316 "共享型buff會跟對面搶") — 奥术, 灵知 坚冰, the 莱恩哈特 / 缄默德克萨斯 RES cuts (
Battle.applyStrongest, §5.3). v2.5 kept one 奥术 per player, so a pair field multiplied it (×40 arts damage taken at +400 layers; ×7.6 now). "Strongest" rather than "latest" is ASSUMED. - Leader parts: 剑 / 锤 / 碎铳之簧 pass every damage they take 1:1 as 无来源 HP loss, credited to the attacker (PRTS "受到等量的无来源生命流失";
PART_TRANSFER1,loseHp … sourceless); v2.5 passed ½, a 剑 / 锤 only while grounded. The dive hits counting (PRTS talent "受到伤害时" vs the handbook "被击落时") and the springs' split to the other springs are ASSUMED (§20.13 for the 剑 / 锤 kit). - 坚守 thorns: dealt 无来源 by the member who was hit and credited to that member (statistics only; up to 19 % of a co-op pool was nobody's).
- Bond layers: uncapped in v2.5; now 999 per bond — official, §20.12 (this workstream first added the cap as ASSUMED from the community; the merge kept the limits workstream's single implementation).
- 直接乘算: "+X%" ATK / DEF / max HP from bonds, strategies, equipment, 机变 cards and per-layer 特质 are summed with each other and with the skills' "+X%" (PRTS 盟约记录 "…属性加成均为直接乘算"; 游戏数据基础 D_t = Σtᵢ, A = (A₀ + D_p)(1 + D_t)). v2.5 compounded them (×74 instead of ×12 at 600 精准 layers with four more bonuses).
- Kept: a 胄 drone costs 2 % of the leader's shown max HP = the pool (
DRONE_LINK_BASE 'pool', = v2.5; which "最大生命值" the official client reads is ASSUMED — the review tried the unit's data 600 000, which made drones 19–78 % of a solo 标准 fight, and went back). Under 限伤 it is one hit: 144 000 at most today (hidden 胄 终极), cancelled only above a 14 999 950 pool (§20.12). - Ruled out by measurement: double crediting (client / server / replica), multi-hits through the 5×3 hit area, element bursts, %-max-HP damage on leaders (only the drone link).
- Official, not a bug: per-layer bonuses have no cap below 999 (谢拉格 vs cold ×(1.35 + 0.01 L) = ×11.34, 奥术 at low HP ×15.7, 精准 +1208.8 % ATK at 999 layers), so very high layers still kill fast — 绝境 with +400 on every active bond: median 8.6 s, fastest 3.6 s; 终极 under 20 s needs ≈ +400 on every bond (≈ 2 800 active layers per player; median 16.8 s); with two bonds per player at 999 (≈ 2 000 layers) 绝境 kills median 10 s, 7 of 28 under 5 s; with every active bond at 999 终极 median 8.3 s, 绝境 3.7 s — and there 限伤 (§20.12) cancels 6–77 hits per 12–28 fights. The community reports the same (巴哈姆特 12522, 999 谢拉格 "boss一秒死").
- Measured (real seconds — the fights run at 2×, so half the game seconds of the QA tables). Integrated build (QA 6b on
cca11e6, after the elite-to-board merge): the R14 boards of real 4-AI co-op matches, seeds 7 / 8 / 9 / 12 × boss_1…7 = 28 Final Assaults per row, both pair fields on one pool, the same boards replayed on v2.5 → now: 终极 own layers median 206 → 258 s, +200 on every active bond 24.5 → 42.1 s (fastest 8.2 → 18.9 s), +400 11.3 → 16.8 s; 绝境 own 79 → 99 s, +200 11.6 → 14.8 s (fastest 1.9 → 7.3 s), +400 6.1 → 8.6 s; each player's top bond requested at 1500 (now held at 999): 终极 53 → 95 s, 绝境 17.5 → 33 s. On each version's own bot boards the medians moved only ×1.0–1.2 (终极 own 256 → 258 s, +200 47 → 54 s; 绝境 own 81 → 99 s, +200 13 → 15 s): the elite-to-board rule (§20.11) gives the bots slightly stronger R14 boards (终极 2.12 → 2.26 elites per player, 绝境 2.14 → 2.51; 绝境 6.0 → 6.9 active bonds), so a player whose board did not change feels the same-board numbers. No landed leader hit reached 300 000 (v2.5 landed up to 18.6 M); the largest at +400 is 144 000 (a hidden 胄 drone; 114 000 from an operator), at 999 layers 299 600. The boss-HP workstream's own measurement (42 pre-merge boards, seeds 7–9: 终极 +200 30.9 → 56.0 s, …) is kept in BALANCE §3, labelled as such; BALANCE §3 has both tables. - Tests:
test/match/bosshp.test.js(real Final Assault / Hidden Core matches, the 40 bloodPoint values; the 奥术 pair test runs seed 12 since the elite-to-board merge changed the bots' boards),test/sim/bosshp_stacking.test.js,test/match/layercap.test.js,test/sim/battle.test.js(Battle.addLayers cap),test/content/bonds_addon.test.js,kits_t4.test.js,kits_alt_t4.test.js,enemies_bosses.test.js,items.test.js,garrisons_battle.test.js,choices.test.js,test/match/unitStats.test.js,finalAssault.test.js,balance.test.js,playtest6-bosspool.test.js; opt-inSIM_E2E=1 node --test test/sim/bossfield.browser.test.js(the real boss / hidden specs of a 999-layer bot match give the same digest, pool and fx in headless Chrome as in Node; covers §20.12 and §20.13 too).
20.11 A merge's elite takes the deployed copy's tile (user-confirmed) — match/PlayerState.js _mergeChess / transformChess, match/board.js mergeTile, match/audit.js, ui/gameLogic.js mergeProgress / mergeTarget, ui/shopBar.js armedSlotOf / onArm, screens/game.js MERGE_HL, render/promote.js, render/fx.js promote, render/app.js setPrep
- Seen (the user, first-hand, after playtest #6): "官方就是合成精锐时,如果消耗了场上的干员,精锐会出现在场上那个位置".
- Cause: research 01 A1 row 7 read PRTS 帮助 as "1 elite goes to the hand, not to a board tile" (marked VERIFIED, repeated in
01-core-data.jsonpromotion.eliteGoesTo), so_mergeChessput every elite in the hand and used a consumed copy's tile only when the hand and temp were both full. - Official (PRTS 卫戍协议/帮助): §干员的获得与精锐化 "…发送1名【精锐】状态的该干员至手牌区(若消耗已部署至作战区的干员,则发送至作战区对应位置)"; tip "干员晋级后已配发装备会回收至整备区,需要重新配置"; §战斗部署 "如果部署的干员拥有可手动部署的附属召唤物,则该召唤物会立刻加入手牌区", deploy order "从上到下>从左到右". The text does not say which tile wins among several deployed copies.
- Now (server): when a consumed copy stood on the board the elite takes that tile and its facing, otherwise it goes to the hand, overflowing into temp (outside PREP too). Several deployed copies: the one that deploys first — row desc, then col asc, the mirrored right boss half included —
board.js mergeTile(tiles, legal)ASSUMED. A 突变细胞 transformation destroys its carrier before the gain, so the carrier's freed tile is no copy's (§21.1 — official footage; the 0.1.1 WA rule that counted it,transformChessfromKey/fromDirASSUMED, is withdrawn). A tile the elite may not use is skipped; with the hand and temp also full the elite stays on the first deployed copy's tile rather than being lost (as_evictIllegal). The deploy count never grows (no BOARD_FULL path). Equipment returns to the hand, overflow temp; with both full it stays on the elite up to itsequipPerChess(2) slots, any further item destroyed with a log warning (unchanged). The consumed copies' summons are removed; an elite on the board gets its own fresh stack (grantTokensFor, after the items) following the §16 loadout [ASSUMED: placed summons are not carried over]. Every path (g.buy, g.reward,ctx.grantChess, 画卷, transformations), every phase (a SETTLE / Final Assault merge places the elite at once, its reward offer waits for the next prep) and the boss-field prep (legality on the player's half, §19.7). Reward offer, ticker andstats.mergesunchanged;onMergecarriesarea('board' | 'hand' | 'temp'). Bots plan the elite like any owned unit. - Now (client):
gameLogic.mergeTarget(priv, chessId)mirrorsmergeTile; a shop or reward card armed for a merge-completing buy lights the target tile in gold (ShopBar onArm→screens/game.jshighlight groupmergeTile); the 可晋升 tag says 精锐干员将出现在作战区原位置 or 精锐干员将进入整备区; an elite card never merges (completesChessMergerefusesisGolden,mergeProgressreports 0 copies: no pips, tag or tile);render/promote.js promotionsOfrecognises a merge in setPrep and the elite playsfx.promote(gold pillar, shockwave, hex ring, streaks from the other copies) instead offx.deploy(view.debug.promotions). - Checks:
audit.jschecks every merge — the tile ismergeTileof the deployed copies legal for the elite, with that copy's facing; the deploy count did not grow; otherwise the elite is not on the board, except the no-room fallback.tools/matchrun --checksweeps are clean. - Normative lines rewritten: §2 (
promote.js), §6.2 (the economy line and the temp-overflow paragraph), §9 (the promotion cue). - Tests:
test/match/playtest6-elite-to-board.test.js,test/match/merge.test.js,hardening.test.js,facing.test.js,test/ui/playtest6-elite.test.js,test/render/playtest6-promote.test.js,test/docs-consistency.test.js; opt-inSP_E2E=1 node --test test/ui/playtest6-elite.e2e.test.js(real server and prep: the armed card lights the deployed copy's tile, the elite stands there after the buy, 剩余可放置角色 unchanged).
20.12 Official limits: 999 layers per bond, 限伤 300000 on leaders — shared/constants.js BOND_LAYER_CAP / BOSS_HIT_LIMIT / layerGainRoom, match/PlayerState.addLayers, Match SETTLE, sim/Battle.addLayers / loseHp, sim/damage.js leaderHitCancelled, match/fields.js validateClientResult, match/invariants.js, render/fx.js, tools/matchrun.mjs, tools/balance.mjs
- Asked: the co-op leaders as hard to kill as in the official game. Research 11 (
docs/research/11-limits-official.md) found two official caps in the client (2.7.71 il2cpp) that the remake lacked. - Official [DATA]:
AutoChessBattleConst.MAX_GARRISON_STACK = 999—AddBondCountstoresmin(L + n, 999), per bond.MAX_BATTLE_DAMAGE = 300000— in step mode (every boss battle outside training)AutoChessStepModeManager._OnBossEnemyTakeDamagerunsmodifier.Cancel()whenceil(value) ≥ 300000on anIsBossEnemyunit;IsBossEnemy(disassembled, research 11 §2.3) = enemyId ∈autoChessData.bossInfoDict[*].enemyId= data/bosses.jsonenemyKey. [COMM] bonds stay at 999 while fed; 「一次超过30w会被卡掉」: players avoid extra multipliers at 999 layers. 限伤 is a deliberate constant, not an engine fixed-point overflow — the boss-HP workstream's earlier reading (2³¹, "not modelled", §20.10) is superseded. - Now — layers: the single implementation is
shared/constants.js BOND_LAYER_CAP+layerGainRoom(before, n)= min(n, 999 − before), never negative (the boss-HP workstream's duplicatesim/constants.js BOND_LAYER_CAP / layerRoomwas removed in the merge). Every writer uses it:PlayerState.addLayers(特质, items, bands, 机变 cards, bonds), the SETTLE ofBattleResult.layerGains,Battle.addLayers(the live copy, clamped after thelayerGainhook, like the client), and the dev tools' direct writes (matchrun --layers N,balance.mjs applyBoard). A gain at the cap adds 0: no onLayers, hook or 'layer' event, so the per-N milestones (远见, 奇迹, 维多利亚 …) stop with the count. Client results: per-bondlayerGains ≤ min(60 + 4·round, 999 − starting layers).invariants.jsflags a bond above 999; the UI shows the server's capped count. - Now — 限伤: kinds
'boss'/'hidden'only; a leader isisBoss(the tag-'boss' units: those 10 keys and their mirrored copies — parts, 刺胄之弹, drones and escorts never count). A hit withceil(final) ≥ 300000is cancelled: checked indealDamageafter DEF/RES and every multiplier, before HP shields, and inloseHp(a part's 传递, a drone's death). A cancelled hit causes no HP or pool loss, no credit or stats, no damage number, nodamaged/fatal/ kill; the attack, its SP, thehithook and separate element 损伤 hits stay. Element bursts and DoT ticks are checked, gauge fills are not. The sim emits fxhitCap {id, n}, drawn as nothing (deterministic in Node and browsers; SP_VERIFY digests agree). - The line vs the pool: 【死亡集群】 drone deaths cost 0.02 × the pool max (§20.10, §20.13) — at most 144 000 today (boss_8 终极 7.2M), so it lands; a pool above 14 999 950 would cancel every drone death. An alive-scaled pool (§20.9, open) can only shrink, but any larger pool must re-check it; a real-kit test pins the line.
- ASSUMED: the scene server clamps prep gains the same way; a cancelled hit shows nothing; a loss passed on to a leader is one hit; the order hit-step blocks → 限伤 → HP shields — 阿利斯泰尔's 莫非王土 vest (
boss:vest, 5000): a hit of 300000–304999 is cancelled and the barrier stays; 假想敌:再生's blocks on 假想敌:管 (hidden h08_03) negate a hit past the line before the limit sees it. - Measured: the competent and strong balance boards never reach the line (biggest leader hits 28k–72k), so BALANCE §3's model numbers stand (BALANCE §7); the co-op replays up to +400 per bond and a top bond at 999 cancel nothing either. Two bonds per player at 999 or every active bond at 999 do reach it: 6–18 cancelled hits per 28 Final Assaults and 14–77 per 12 Hidden Cores (QA 6b), and those kills take longer than with v2.5's uncapped hits (§20.10).
- Flip:
BOND_LAYER_CAP = 0and / orBOSS_HIT_LIMIT = 0(or Infinity). - Tests:
test/sim/playtest6_limits.test.js(including the real-kit vest, 再生 and 【死亡集群】 cases),test/match/playtest6-limits.test.js(a 999-layer board through a real Final Assault + Hidden Core, both tools at the cap),test/render/playtest6_hitcap.test.js,test/sim/playtest6_bosspool.test.js(adapted), and the boss-HP workstream'stest/match/layercap.test.js/test/sim/battle.test.jscap tests on the single API.
20.13 假想敌:胄 kit audit (PRTS) — content/bosses.js kitHelm / kitBlade
- Audit (a separate session, 2026-10-01), against the PRTS 技能 / 天赋 / 能力修正 texts and the skill blackboards:
- 灭顶之灾 fires one 刺胄之弹 below 20 % too (PRTS 能力修正 原因 6 "描述与游戏实际表现不符合": no extra shell; no blackboard holds a second one), at the highest-ATK operator within attack range for both copies ("触发索敌和普通攻击相同"; boss_1 used the whole field).
- 死亡集群: any drone death costs the leader 2 % max HP (= the pool, §20.10), credited to the killing operator if one killed it; a leak costs nothing (能力修正 "该无人机单位死亡时"). Drone HP:
summon.hp_ratio(隐秘核心) scales it; skill 2'smax_hp0.5 is not read ASSUMED — no source defines it. - 斩胄之剑 / 破胄之锤: 初始模式 hovers invulnerable (untargetable ASSUMED) and attacks everything in range (锤 150 % ATK, unblockable). 掷 → 出击模式: blink to the start of the level's
<hand>_blinkroute, fly at the lowest-ATK operator, no normal attack. 15 hits → 【瘫痪】: stun 10 s (its end ends 瘫痪, not the 20 s), ground unit, damage taken ×1.3. Landing or 瘫痪 ending: a new 初始模式 copy with the old HP is summoned on the<hand>_originroute and the old one is killed (a part: uncounted, no bounty) [ASSUMED: only the HP is inherited, so 掷 comes back after its initial 25 / 55 s instead of the 80 s cooldown]. - Every damage a 剑 / 锤 takes passes 100 % to 胄 (PRTS "等量", also during the dive) — merged with §20.10 into the one
PART_TRANSFER1 (BLADE_TRANSFERis its alias), 无来源 and credited to the attacker.
- Measured (same-seed
balance.mjs --boss-only, n = 3–5 per cell): boss_1 unchanged (≤ 0.5 s); boss_8 独立 绝境 44.7 → 45.2 s, 独立 终极 win 2/3 → 3/3, 同盟 绝境 23.3 → 23.7 s, 同盟 终极 72.0 → 64.2 s (BALANCE §3). - Tests:
test/content/enemies_bosses.test.js(shell count and range, drone deaths and leaks, the pool-based link, the sortie / 瘫痪 / replacement cycle with the 1:1 无来源 transfer and its credit).
20.14 QA residuals of the integrated build (2026-10-02) — match/Match.js _onResult / heldResult, render/app.js promoBase, ui/shopBar.js mergeHint, ui/detailPanel.js, screens/game.js dSide
Two QA verifiers ran the integrated playtest6b (real 4-AI and 2-human co-op matches for every leader, Node vs headless Chrome digests, the merge UI on the user's phone profile and on desktop). Every check passed; the residual issues and what was done:
- Fast boss kills handed over (fixed). Under client-side combat a pair that empties the pool faster than the plausibility budget (one field credits ≤ the pool per
BOSS_MIN_CLEAR_GS5 game s, §14) had its'cleared'b.resultrejected as implausible, the client demoted and the field handed to the partner or the server — seen at 绝境 with every bond at 999 (kills at 2.0–4.0 game s; official 999-layer play reports "boss一秒死"). Now a'cleared'result whose cumulative report covers what the pool holds (1 HP slack, the pool floor) is held (f.heldResult): the boss clock credits the report at the budget's rate, the silence watchdog and handovers skip the field, a re-sent copy is ignored, and the pool's end completes the field with it (_endFinal); a team-LP defeat first still credits nothing more. The budget itself is unchanged, so a forged report gains nothing ab.progresscould not already get. The verdict waits at most until the budget covers the report (≤ 5 game s from the field's start). Re-run of the QA cases (绝境 seed 7 boss_1 / boss_6 with every bond pushed to 999, seed 21 with 990 early layers, 3 more at 999 / 终极): 0 rejections, 0 takeovers, every boss field kept by its client, Σ credited boss damage = the cleared pools; in headless Chrome (2 humans + 2 AI, a scratch server with a 12× slower budget so the path runs) both boss rounds held the clearing client's result and went on to the Hidden Core and the result with no takeover and no console error. - No promotion cue for a merge outside the prep (fixed). A merge completed between two preps (a band / 机变 grant at ROUND_START, a SETTLE merge) placed the elite silently:
enterBattleemptied the prep state the cue compares with.render/app.jskeeps the last prep pieces across a battle or scouting board (promoBase), so the nextsetPrepplaysfx.promoteon the elite (streaks from the consumed copies' slots). Re-run of the QA case (松桐 grants the third 古米 at R4 ROUND_START):view.debug.promotionsrecords the elite on the copy's tile, copies 2. - The 可晋升 destination on touch (fixed). The tag's text was a native
title, never shown on a phone. The detail card the first tap opens now says it under the header — 「可晋升:精锐干员将出现在作战区原位置」 / 「…将进入整备区」 (shopBar.js mergeHint, gold.dhint--merge; shop and reward cards). - The detail card covered the gold tile (fixed). At 1920×1080 the left card covered half of a target tile in the leftmost legal column; the card now docks on the side away from the armed merge tile (
screens/game.js dSide, the underframe'spanelSiderule), on phones too. - Measurements re-taken (docs). §20.10 / §20.12 and BALANCE §3 / §7 now give the integrated build's numbers (real seconds; the earlier boss-HP numbers are labelled as pre-merge boards) and the 999-layer profiles where 限伤 cancels hits.
- Not changed — per-source talent debuffs ASSUMED. Some talents key their debuff with the source unit (
etlchi:dot:<id>,ines:atkSteal:<id>,ines:sentry:<id>, and similar slows / DoTs in other kits), so two copies of one operator (a pair field where both players own it) each apply theirs. PRTS 作战机制 says only that same-named buffs show one instance under the default overlap strategy; whether these talents use it across copies is not in the data the project has, and the effect on leader kill times is small (an arts DoT, an ATK steal, a slow). The rule stays applied to the debuffs that move leader kills (奥术, 灵知 坚冰, the RES cuts — §20.10). - Not changed — temp items behind a full hand. With the hand full, items overflowing into the 临时整备区 (row 8) stand behind the bench row's operator models and are hard to see; the row's red frame, the 「临时整备区 N 件道具待处理」 notice and the refused Ready still show them. A layout change of the temp row is left for a later pass.
- Tests:
test/match/clientCombat.test.js(the held result; a 'cleared' result not covering the pool is still handed over),test/render/playtest6-promote.test.js, opt-inRENDER_E2E=1 node --test test/render/browser.test.js(a merge between two preps is cued),test/ui/playtest6-elite.test.js(the detail hint), opt-inSP_E2E=1 node --test test/ui/playtest6-elite.e2e.test.js(the hint in the real detail card).
20.15 The bond strip follows the watched player (user report after playtest #6, 2026-10-02) — ui/watchBonds.js, ui/bondStrip.js, ui/combatHud.js, screens/game.js (watchWho, bondOpen), battle/runner.js bondLayers / ownerOps, battle/observe.js resumedWatch, match/bondsMeta.js bondsWithGains, PlayerState.pendingLayerGains / bondsView, Match.publicView / _finishCombat / settle
User: 「观看队友时(不仅是休整时候,还有自己战斗结束时,联防时,最终boss战)盟约栏应该变成当前队友的盟约以及他的层数」.
Before this change:
- Only 前往查看 of a teammate's normal field switched the strip, and it showed the start-of-round layers.
- An eliminated player's auto-observed field, the 联防 field and the Final Assault pair field kept the viewer's own bonds.
- A teammate's popup listed the viewer's pieces as its members.
Whose bonds (watchBonds.screenFieldId / bondOwnerId / screenStrip): the player whose field or half the camera shows.
The field on screen is, in order of preference:
- the watched field (
g.watch); - else the battle the view shows. Under client-side combat this is the runner's battle on screen, which is also an eliminated player's auto-observed field; under server-run combat it is the field the view entered;
- else home.
In SETTLE it is the field the last battle left on screen. In prep without watching, it is the own board.
-
The own board / own battle → your bonds (
m.private bonds). -
A normal field
'n:<pid>'→ its owner's bonds (m.public players[].bonds). This covers:- a teammate's board in prep (前往查看). Research 09 §3.1: "their board, read-only, including their pen and bonds". Official clients receive every board's bonds (
ChangePositionDn.boardStatus.updatedBonds); - a teammate's battle watched after the own one;
- the field an eliminated player auto-observes;
- a field picked in the legacy view switcher.
- a teammate's board in prep (前往查看). Research 09 §3.1: "their board, read-only, including their pen and bonds". Official clients receive every board's bonds (
-
A shared field (联防
'u', boss / hidden'b1'/'b2') — settled by the user (2026-10-02: "共享阵地按你说的规则就行"; no official source shows the strip there):- If the ‹ › pill points at a half that a player holds → that player. The pill's "你自己" means yours; "👁 name" means theirs.
- Else (全景, an empty half "无人在家", or no pill), if you fight on that field → yours.
- Else (you are not on the field: a 联防 leaker, an eliminated spectator) → never your own. The strip shows the teammate you picked with 前往查看 if they are on that field, else the field's first listed player (helper 1 / the pair's first seat).
Basis: official observing targets a player (
ObserveUp { obIndex },DeadAutoObDn { obIndex },m_leftBattleObIdx / m_rightBattleObIdx), and the official non-helper pill reads "‹ 👁 helper# ›" (research 09 §3.1). -
The picked teammate: the game screen remembers
watchWho = { fieldId, playerId }when a team row is tapped (requestWatch(fid, playerId)). Theg.watchrequest still names only the field.watchWhocounts only while that field is the watched one (watchedPlayer). It is cleared by 返回战场, the own row and a phase change, and restored if the server refuses the watch. -
Camera: picking a teammate on a two-half shared field moves the camera to their half (
playerLayer), once per pick and per field entered. The ‹ › pill is also offered on a shared field watched with 前往查看, so the pill and the strip tag always name the same player.- On such a field the ‹ › pill replaces the observing pill and carries 返回战场 (absent for an eliminated player).
- On a normal field the observing pill "👁 name" takes the strip owner's name.
- The default camera on entering a field is still 全景.
-
返回战场 without an own field (client-side combat: a 联防 leaker, an eliminated player): no
g.watchis sent, because it would be refused withBAD_TARGET 'no such field'(an error toast). The screen keeps the field it shows, and the strip falls back to the rules above. -
A reload / reconnect while watching a teammate's battle after the own one (client-side combat): the server already resends the watched field (
Match._resendBattle,b.start watch: true). But a fresh screen starts with no watched field, so before this the HUD showed 「作战结束,等待队友完成作战」 with no observing pill and no 返回战场, and the own row did nothing.- The screen now adopts that field as the watched one, once per battle (
observe.resumedWatch, decided at the first sight of its battleId). - It does so only for a teammate's normal battle in COMBAT, shown to a living player who watches nothing.
- A battle first seen while a watch is already set is only marked as seen. So a 返回战场 tapped while the own
b.startis still on its way never brings the teammate back. - A loading battle waits until it runs (its state carries no
watchyet). - 联防 leakers and eliminated spectators keep their own rules.
- The screen now adopts that field as the watched one, once per battle (
Labelling:
- A teammate's strip leads with an amber "👁 name" pill (the observing pill's spelling) and gets amber disc rings,
aria-label"name 的盟约" anddata-owner; slots carrydata-bond. - The pill is horizontal at every width, with a pixel floor: 10 px on desktop and tablet, 9 px below 768 CSS px (1rem is clamped at 40 px there).
- An empty teammate strip reads "name 尚未激活盟约".
Detail card chips and the popup (whose bond a popup shows):
- A unit card's bond chips use the bonds of
watchBonds.detailBondOwner:- a unit on the field → its owner, when that owner is a known player (yours or a teammate's: a shared field shows both halves' units, whoever the strip follows);
- your own piece (board / bench) → yours;
- a bond-member card opened from a popup → that popup's player;
- anything else (shop / reward / drawer cards, a unit without a known owner) → the strip's player.
- A bond popup carries the player it was opened for:
bondOpen = { id, ownerId, from: 'strip' | 'detail' }(toggleBond). From the strip, that is the strip's player; from a chip, it is the same player the chips show. The same bond of the same player closes it. popupViewbuilds the popup from that player, never simply from the strip:- the entry with the live layers (as the strip and the chips show it);
- "👁 name 的盟约" for a teammate;
- the member list: your pieces, or that teammate's operators on the field on screen (
ownerBoard: the prep scouting board's or a server-run battle field's units). Under client-side combat it also uses the battle on screen's operators (battleRunner.ownerOps(ownerId, fieldId)), because the field meta the runner publishes is taken before they deploy. - Their hand is never sent, so a teammate's member counts as owned only when it is on the field.
- The strip marks a disc as open only for a popup of its own player.
- A popup opened from the strip closes when the strip changes hands: another teammate, a ‹ › half, or back to your own. A popup opened from a chip stays open, labelled with its own player.
Layers:
- In battle: the battle runner reports
state().bondLayers = { [playerId]: { [bondId]: n } }. These are absolute live counts of every bond that grew in a battle it simulates (the own battle, a teammate's display replica), capped atBOND_LAYER_CAP(999). They are published when one grows and kept through the round. - The client lays them over the view's counts with
withLiveLayers: the higher value wins (a count only grows) and never shows above 999. They apply in the battle phases and SETTLE only. - 联防 and boss battles disable gains, so they report none.
- After COMBAT (
_finishCombat): until SETTLE, bothm.private bondsandm.public players[].bondsadd the finished normal battle's IN_BATTLE gains (PlayerState.pendingLayerGains= the result'slayerGains;bondsMeta.bondsWithGains: floored, at most up to the cap, likesettle()). The strip of a player, and of a teammate watching him in the 联防, therefore keeps the layers his battle reached. - This is views only:
ps.bonds/ps.layersare untouched. SETTLE clears the pending gains as it adds them (once), and the next round start clears them too.
Data:
m.public players[].bonds = ps.alive ? bondList(gd, ps.bondsView()) : []: every bond with members, layers or an active tier. It is the same list and order as the player's ownm.private bonds, minusthresholds/countsHand(the client reads those from bonds.json).- An eliminated player's list is
[], for these reasons:- nobody can watch them:
g.watchrefuses them and they have no field; - the result screen reads
m.result's own bonds; - their remaining layers would otherwise cost every
m.publicfor the rest of the match.
- nobody can watch them:
- Measured in a 4-AI NORMAL harness match (seed 11, round 13, 3 players eliminated):
players[].bondsis 771 B, against 2424 B without this rule. The review measured 705 B on v2.5.1; the difference is the living player's layers-only entries, which the strip needs. - There is no new message and no new field.
m.publicstays throttled and deduplicated, so the extra traffic is the living players' layers-only entries plus one change at the end of COMBAT. - Nothing is exposed that scouting (research 09 §3.1) or watching the battle does not already show.
21. Player feedback after 0.1.0 (v0.1.1)
Thirty-two reports after the public 0.1.0 release (2026-10-02), relayed by the user in six batches; most were not tested by the user. Verdicts: real (fixed), partly (the rule was right, what a player sees was wrong or a related part was wrong — fixed), not real (not reproduced; tests lock the rule). Where each is handled: batch 1 — #1 拉普兰德's "获得该干员后该回合的首次刷新" should stack 叙拉古 → §21.1 (real); #2 late bounty enemies offered in early bounty drafts → §21.2 (real); #3 the water pool map: operators deployable in the water, 突袭 operators land in it → §21.3 (real); #4 昆图斯's 突变细胞 gone after one use → §21.1 (real: not consumed; its new operator joins the bench, not the carrier's tile — corrected after the merge, pointed out in PR #2); #5 转译基底 does not transform → §21.4 (real); #6 凯瑟琳's three upgrade items shown wrong → §21.2 (real); #7 鸭爵's 圆仔 blockable, audit the other 鸭爵 enemies → §21.5 (real); #8 深池逐火战士's 怨恨的余烬 cannot be killed → §21.4 (real); #9 tacticians (伺夜) can place the tactical point outside their attack range → §21.3 (real); #10 bots too dumb, smarter without more CPU → §21.6 (real). Batch 2 — B1 奥术 not working → §21.7 (partly: the bond works, 标准 switches it off); B2 源石溶剂 white + white → gold, then an extra white → §21.7 (not real); B3 维娜·维多利亚 S3 summons one 黄金盟誓 → §21.8 (real); B4 玛恩纳's skill deals no damage → §21.8 (real, on flying waves); B5 薄绿 can pull flying enemies → §21.9 (real). Batch 3 — C1 the hidden leader's "造成50%伤害" broadcast before the fight → §21.10 (real); C2 变形同构体 + class item: the bond member count excludes the wearer → §21.11 (partly: display only). Batch 4 — D1 信仰搅拌机: 源石溶剂's HP drain should trigger S3 → §21.12 (real); D2 野鬃's skill sometimes pushes the wrong way → §21.13 (real); D3 special 源石虫 all drawn as the plain one → §21.14 (partly); D4 暴鸰's bombs do not drop properly → §21.14 (partly); D5 the originium-floor map's lower route should cut diagonally from row 4 to row 3 → §21.15 (real). Batch 5 — E1 烛煌 S3's attack range does not change → §21.16 (partly: the card); E2 medics that block a roaming leader stop healing and attack it → §21.17 (not real); E3 卡涅利安's attack is not a true AoE → §21.18 (real). Batch 6 — F1 坚固维式重锤's lock does not work → §21.21 (partly: the lock works; it was once per battle, now once per deployment as the user settled, and an M3茧甲 equipped first took the first lethal hit); F2 a free 谢拉格 uses up 休露丝's discount → §21.21 (not real); F3 蒂比 still hit after taking off → §21.22 (real); F4 深巡 on a fenced tile does not attack → §21.23 (not real; a fence blocking fault found on the way is fixed); F5 突袭 operators land on a knocked-out operator's body → §21.24 (real). Two hardening fixes are credited to pull requests (PR #2, PR #7) → §21.25.
Eighteen workstreams (WA–WR, each on its own branch from master 7f13473) were merged into feedback1 in the order WB, WA, WE, WD, WC, WI, WM, WN, WO, WL, WG, WH, WJ, WK, WP, WR, WQ, WF, then WB's follow-up from 66 screenshots of 22 matches (bounty lists, the 机密商店 and 战术决策 with repeats) after QA, then batch 6's four in the order WS, WU, WT (on WI's branch), WV (on WC's); data/*.json was rebuilt by tools/build-data.mjs --offline after every builder merge, never hand-merged. The integration fixed a bug already in 0.1.0 (a 联防 battle timed out during 隐德来希 S3 reported its 心烛 as leaks, so the server refused an honest client result — candles carry mem.noLeak), the last review of the transform workstream (form fx were still dropped by the render engine), every cross-workstream note worth doing with a primary source, and reconciled the bot with the merged rules — §21.19. §21.20 lists the settled decisions and what stays ASSUMED; batch 6 and the pull-request fixes follow it (§21.21–§21.25). Normative lines rewritten with it: §2 (grid.js), §3 (the effective buildable, the 深水区, range-bound summons, 攻击距离 vs skill ranges), §5.2 (liveRangeGrid, form), §5.3 (stealth vs area damage, the 奥术 refresh), §5.4 (kill / death, foesInRadius, loseHp vs damage, 静态刚体), §5.5 (healers that block, rangeAoe, armed enemies, results), §5.6 (TAKE_DAMAGE, noRangeExtend / showOwnRange, the profession splash radii), §6.2 (rewardOffer, piece meta.round, summons in range, returned equipment), §6.3 (变形同构体), §6.6 (the bot), §7 (bounty drafts, 鸭爵, knock-out forms), §8.2 / §8.3 (fx form, m.ticker, range, UnitInfo items / form, the stealth bit, rewardOffer), §9 (forms, alias tint), §10 (offers, ticker, 本局禁用), §13 (spine/enemy), §14 (ground pathing, boss tickers, the runner's form fx), §16, §17.3, §18.5, §18.6 #10, §19.1, §19.7, §20.1, §20.3, §20.6, §20.9; with batch 6: §3 (fenced tiles), §5.3 (liftoff), §5.4 (fatal, ignoreSelect, redeploy), §5.5 (bodies and redeploys, blocking, enemy AI), §6.4 (onBuy, the item snapshot), §8.2 (down row, col), §18.3, §19.7.
21.1 拉普兰德's first refresh; 突变细胞 is not consumed and its new operator joins the bench (#1, #4) — sim/content/garrisons/meta.js SERVER_GAIN_BOND_LAYER_BY_REFRESH_CNT, match/PlayerState.js pieceRoundCount / bumpPieceRoundCount / _mergeChess / transformChess / acquireChess, match/effectsMeta.js ctx.pieceCounter / incPieceCounter / transform, match/builtinMeta.js char_chess_transformation_equip, match/audit.js, match/bot.js cellTarget / makeHandRoom, tools/build-data.mjs ITEM_RULES, ui/detailPanel.js ItemDetail
- #1 seen: with 普罗旺斯 + 德克萨斯 deployed — refresh, buy 拉普兰德, deploy her (3 叙拉古), refresh again: no 叙拉古 layers. Players: officially "获得该干员后该回合的首次刷新" also stacks. Cause: the handler compared the PLAYER's round refresh count with
refresh_cnt1. - #1 official: garrison_123_a/_b = SERVER_REFRESH_SHOP / SERVER_GAIN_BOND_LAYER_BY_REFRESH_CNT, bb
refresh_cnt1, layer 4 / 8, "若为本回合首次主动刷新…此干员在整备区时也有效" (activity_table act2autochess). Prep effects run on the scene server (research 11 §1.2); PRTS has no timing note. Text and report agree when the count belongs to the trait instance; the "本回合每刷新过1次" multipliers (阿罗玛 122, 安洁莉娜 147) fire on other events and keep the player's count. - #1 now: each copy counts the manual refreshes it sees this round, on the board or in the hand (
piece.meta.round, module-prefixed keygarrison:SERVER_GAIN_BOND_LAYER_BY_REFRESH_CNT:refreshes), and fires on therefresh_cnt-th. A new piece starts at 0; a move or an in-place promotion keeps the count; a re-triggered trait neither fires nor counts; temp pieces do not count. - #4 seen: "昆图斯策略给的突变细胞用完就没了". Cause: research 04's ASSUMED "item consumed"; the built-in and a content override both destroyed the cell.
- #4 official: the text "战斗结束后,装备者替换为高一阶的随机干员" never says 销毁 (every consumable's text does; data EQUIP, upgradeNum 100); PRTS 下半/PRTS盟约记录 备注 "生效时,原干员销毁,获得一名高一阶的随机初始干员(最高六阶)"; PRTS 卫戍协议/帮助 "佩戴的装备无法手动卸除,在失去该干员(干员出售、销毁、合并等)或装备合并为进阶品质时自动卸除"; community guides re-inject it every round (bilibili cv47000418, cv48003106; cg.163.com "这个道具可以无限使用").
- #4 where the new operator goes (corrected after the WA merge; pointed out in the closed PR #2, whose playtest claim the footage confirms): official gameplay footage — bilibili BV1vzyVBuEN9 (上半, ≈ 8:24) and BV1Qkw1zMEoR (下半, ≈ 7:25) — shows the next prep with the carrier's tile empty, 剩余可放置角色 one higher and the 整备区 holding the returned equipment and the new tier+1 operator, which the player then deploys by hand. It reads the PRTS 备注 as written — a destroy followed by a gain ("原干员销毁,获得一名…") — and PRTS 帮助 sends whatever a player recruits or gains to the 手牌区 (= 整备区; "被发送至手牌区的物资优先从右到左填充空位"). WA had kept the new operator on the carrier's tile when legal ASSUMED and counted that tile for a merge the transformation completed ASSUMED; both are withdrawn (PR #2 itself still let the merge use the carrier's tile — not adopted).
- #4 now: after every battle the carrier is destroyed wherever it stands (board, hand or temp — a board tile is freed and the deploy count drops; its summons are removed, its copies return to the pool). Its equipment, the cell included, returns to the hand first (overflow temp; auto-merging like any gain). Then a random NORMAL operator one tier higher (max 6; an elite carrier too) is gained like any gained operator (
PlayerState.transformChess→acquireChess, onGain source'transform'): the hand (right → left), overflow temp, and with both full the no-room rule of every gain (back to the pool, toast 整备区已满,获得的干员已返还; an item that found no slot takes one the gain freed, else stays on the gained operator up to 2 slots, else it is destroyed with a log warning, as in a merge). Like any operator on the bench it brings no summon card until it is deployed. A gain that completes a merge merges like any gained copy (§20.11): the elite on a consumed deployed copy's tile, else the hand — the carrier's freed tile is no copy's (_mergeChesslost itsfromKey/fromDiroptions,audit.jsits exception for them); the returned cell is stowed before the elite's summon card. Per-piece state stays with the returned items (商业包装方案's sell count, keyed by the item). One implementation (the built-in); the item card states the rule (items.jsonnote). Bots inject their least valuable single normal operator below 6阶 (cellTarget: never an elite, a merge pair or a carrier of a cell), make a hand slot for a cell left in temp (makeHandRoom), and — AI 托管 too — deploy the gained operator in the next prep's lineup step like any owned unit (a real co-op round in the tests: SETTLE → the next prep). The buy loop's pre-emptive bench shed, which sells the weakest bench single by piece value while the bench is crowded, no longer sells a piece gained since the bot's last prep ended (bot.js rememberOwned): on the new rule it had sold 44 of 222 gained operators in 20 solo 绝境 matches on 昆图斯 before any placement step saw them (now none; a gained operator stays off the board only when the board is full). Measured (tools/botbench.mjs, decisions only): strategy 昆图斯 (--band band_quintus, seeds 201–220) 绝境 8 / 20 wins on the WA build → 7 with the bench rule alone → 11 with the shed fix (LP left 16.8 → 13.3 → 17.1), 险境 15 → 16 → 17 (16.3 → 16.2 → 17.6); the bots' own strategies, bench rule alone → with the fix: solo 绝境 (seeds 1–12) 8 → 8 / 12, solo 险境 (seeds 13–36) 19 → 19 / 24, co-op 绝境 4 AI (seeds 1–36) 9 → 8 / 36 (LP left within 0.6) — noise; the shed's cost is a set of the owned uids per prep. The client needs no special case:render/app.js setPrepdrops the carrier's view and draws the new piece on its bench slot (checked in headless Chrome: the tile empty, the operator and the cell on the bench, no promotion cue, no console error). - ASSUMED: an elite merged this round keeps its copies' highest count (no second firing that round); a copy re-bought after a sale fires on its own first refresh ("获得该干员后"; each extra +4 costs 3 + 1 − 1 = 3 funds); a bench carrier (hand or temp) transforms too (the footage shows deployed carriers); a 6阶 carrier becomes another 6阶; with the hand and temp both full the gained operator and an item without a slot follow the rules of every gain (no source shows that case).
- Tests:
test/match/feedback1-meta.test.js(a deployed and a bench carrier, the hand full → temp, the hand and temp full, a merge with and without a deployed copy, 商业包装方案's count, bots and AI 托管 redeploying, a real solo 绝境 昆图斯 bot match: no gained operator shed in its first prep, none waiting on the bench beside a free deployment),test/ui/feedback1-cell.test.js;items.test.js,merge.test.js,facing.test.js,playtest6-elite-to-board.test.jsupdated.
21.2 Bounty drafts by the official round structures; special offers shown as what they hold (#2, #6) — tools/build-data.mjs BOUNTY_INITIAL_SETS / BOUNTY_BOSS_GROUPS / BOUNTY_HUNTER_GROUPS / SHOP_DRAFT / TACTIC_DRAFT, match/choices.js bountyDraftCards / shopDraftCards / tacticDraftCards, choices.json bountyDrafts / shopDraft / tacticDraft / draftPool / schedule bountyDraft, test/fixtures/official-bounty-drafts.json; match/PlayerState.js pushRewardOffer / pushItemOffer, match/effectsMeta.js offerLabel, ui/gameLogic.js offerHeader, ui/shopBar.js RewardCards, ui/rewardOverlay.js
- #2 seen: "本来应该后期出的悬赏的怪物在前期的悬赏就出现了,导致选了打不过". Cause: every draft drew uniformly from 105 cards (§20.6), so R3 could offer the R9 bosses and the R11 giants.
- #2 official (66 screenshots of 22 co-op 绝境 / 终极 matches of this season at R3 / R9 / R11, collected by the user 2026-10-03; readings in the fixture, no player names): every card is an act2autochess (下半) effect whose enemy is the effect's
enemy_id(the title only names category and tier: 悬赏·损伤I = 底海滑动者 e12_4 / 临时收音师 e18_1 / 心虚设计师 e16_11); each 悬赏决策 event is a fixed card list and a draft shows 6 different cards of it in varying positions.- R3 (22 / 22): six 接下来两场作战 cards, I I I II II III (10 coins), one of 10 fixed sets (enemy_initial_1..10), all six shown; 9 sets seen — series 17 / 18 / 19 whole, and hand-made mixed sets (one holds two series-20 cards, one holds 自制投石机 e17_6), so no simple rule describes them.
- R9 (22 / 22): boss bounties X·悬赏 + 源石虫·特训, 下场作战 — one of 6 groups (one per bossInitial event) of 9, 9, 9, 9, 8 and 6 cards; 庞贝 and 鼠王 never share a draft; named bosses do not always come together (match 19 = 自在 + 杰斯顿 + 陷落雪祀, match 12 = 杰斯顿 alone). The 鼠王 group came in 14 of 22 matches, every dark-grey-board match among them.
- R11: 悬赏决策 14, 机密商店 4, 战术决策 4, 道具补给 0 of 22. A bounty draft is a list of 7 下场战斗 cards — one 特异III giant, one card per faction series — of which the draft leaves out one (match 15 leaves out the giant; matches 1 / 6 / 21 / 22 leave out four different cards of one list); 7 lists seen.
- 机密商店 (matches 2, 4, 5, 8): six free items with repeats (盟约之币 ×2, 变形同构体 ×2); each shop exactly two tier VI, at least one V and one 盟约之币, no tier I / II. 战术决策 (matches 7, 9, 18, 20): six ally cards each (no 排斥 / 责罚 / 裁决, no 模拟战场演变), 补给 twice in match 7.
- Never in a draft: the 7 multi-round cards (the 假想敌 question is answered), enemyeffect_3_*, 战术特训, the 鸭爵 set.
- #2 now: cards.bounty
draftPoolinitial 42, boss 20, hunter 24 (86 drafted);draftExcluded'unseen' (19: multi-round, e3_*, 凋零骑士 / 遗弃者 / 锏 / 扎罗 / 迷路的巨像). A draft = one group, then 6 different cards of it, each drawn with weight 1 + the official drafts of its group it showed in. R3 = one of 10 slots: a seen set, or for the unseen 10th a rule set (tiers I I I II II III, ≤ 2 per series, the III 法术大师A1 e10_6 or 鼎沸 e20_6 — the two-battle cards no draft showed); R9 = a seen group, picked by the matches it came in; R11 = one of the 7 seen lists, uniform (the 4 lists seen with 6 cards completed by the list shape, so 12_8 / 16_2 / 16_6 come in about 1 R11 bounty draft in 15); 绝境 / 终极 R11 families: bounty 14 / shop 4 / tactic 4 (the official counts; was supply 50 / shop 50). The R11 机密商店 (shopDraft): six slots drawn with replacement — VI, VI, V, 盟约之币 and twice V / IV / III / 盟约之币 — items weighted by the official cards they showed on; the earlier 标准 / 险境 shops keep any tier I–VI per card, also with replacement. The 战术决策 (tacticDraft): every card drawn on its own with replacement; at R11 only the ally cards, weighted 1 + the official cards each showed on; other rounds every card, uniform. Identical cards are separate cards (picks go by index; two picks of a team card stack).MULTI_ROUND_BOUNTY_BATTLES = 2stays (it now only affects 教鞭's 法术大师A2·多轮战术特训). The mode's inactive enemy list no longer removes bounty cards (PRTS 11/18 note "不影响悬赏决策出场"). Flip: add a newly seen set, group or list to the build-data constants and rebuild. On the 22 official bounty drafts the generator reaches every one (60 000 seeds per round,mode_multi_hardandmode_multi_abyss). - #6 seen: 凯瑟琳's three items appeared as blank operator cards with raw ids under 晋升奖励. Now: a special-refresh offer is drawn as what it holds — free item cards (icon, name, tier, description, FREE) or chess cards under its source's label (凯瑟琳 定向投放, 娜仁图亚 见者有份, 寻呼模块, 信标, 松果) + SPECIAL + 免费选择 1 件 / 名; m.private
shop.rewardOffer = { tier, source: 'merge'|'special', label, queued, slots }; offers waiting behind the shown one show as "之后还有 N 项" and a +N on the reminder pill; a special offer plays the refresh sound. - ASSUMED: the card weights inside a group; the unseen 10th R3 set (rule) and 险境 R6 drafting like R3; R9 groups picked by the matches they came in (what of the match decides — map, leader or difficulty — is open) and the three single-draft groups completed only with the base cards they lack; R11 = one of the 7 seen lists (which of the 15 events R11 fires is open: by the blocks of effectChoiceInfoDict it would be bounty_hunter_8..15, and with 8 events an unseen list would come in about 1 R11 bounty draft in 8); the R11 family weights = the official counts; the 机密商店 slot split and item weights, and the earlier 标准 / 险境 shops; the 战术决策 weights, R11 without terrain cards, independent draws (open: all 4 official drafts hold a 驰援, a 盟誓 and two of 列装 / 财富 / 补给 / 整备 / 升华), twin battle passives stacking; solo 绝境 / 终极 R11 = the co-op weights (no solo screenshot; the data has 15 solo
bounty_hunter_*_sevents); solo shows 3 of 6. - Tests:
test/match/feedback1-bounty.test.js,test/match/feedback1-secret-shop.test.js,test/match/feedback1-tactic.test.js,test/ui/feedback1-offers.test.js,test/ui/feedback1-secret-shop.test.js,test/ui/feedback1-tactic.test.js,test/match/playtest6-matchflow.test.js,test/ui/playtest6-bounty.test.js,test/content/choices.test.js; opt-intest/ui/feedback1-offers.e2e.test.js,feedback1-secret-shop.e2e.test.js,feedback1-tactic.e2e.test.js(SP_E2E=1).
21.3 No deployment in the water; tactical points inside the tactician's range (#3, #9) — sim/grid.js DEPLOY_REFUSED_TILES / canStand, tools/build-data.mjs effectiveBuildable / ownerRange, match/board.js ownerRangeKeys, match/PlayerState.js _legal / summonRange / _liftOutOfRange / _reorient, ui/gameLogic.js summonRange, match/bot.js, match/invariants.js, content/kits/tier3.js tacticalPoint
- #3 seen: "有水池的那张图干员可以错误的被部署到水里,突袭分队干员也会自动落到水里" — the 深水区 (
tile_deepsea) of 战场#08(下半) 涨潮控制 (act2 m04): board (10–12, 6), partner (10–12, 14), boss (3–5, 6) / (3–5, 14). Cause: the level gives it buildableType ALL and the legend copied it; the deploy maps (server, client, bots) andgrid.canStand(the 突袭 landing tile, tactical points, summon tiles) treated it as road, and a tactical point even preferred it (it lies on the enemy path). - #3 official: PRTS 深水区, 地形信息 tile_deepsea: 部署类型 全部位, 地形机制 "拒绝部署(待补充)"; PRTS 作战机制: the 地形标记 AdvancedBuildableMask (values include 深水) "限制玩家仅能部署匹配的单位于其上";
trap_040_canoe特制水上平台 "在水上建立可以部署任意单位的平台" (season 1's 战场#05 puts one on each water tile). There is no PRTS page "地形". - #3 now:
DEPLOY_REFUSED_TILESmakes the effective buildable NONE: stages.jsontiles.d.buildableNONE (buildableTypeALL kept),deployTileswithout the water,canStandrefuses it (and melee units on a hard-blocked 射击台 / mound); every automatic placement and the bots follow. An active 特制水上平台 makes its tile deployable in the prep deploy map only (act1 m05, weight 0; the sim models no canoe, its Grid keeps those tiles NONE ASSUMED). Gate routes are unchanged; the water counts as non-blockable in the path tie-break, so a few off-route smoothed steps of the m04 boss field (rows 4–5) go around it. 维娜·维多利亚's lions skip it too (§21.8). - #9 seen: "干员伺夜这样的战术家的战术点能被错误的被放到攻击范围之外". Official: 狼群 / 流形 token text "只能部署在召唤者攻击范围内" (character_table; PRTS 狼群 特性); the tactician trait "可以在攻击范围内选择一次战术点来召唤援军"; PRTS 卫戍协议/帮助 "移动干员时,其所属召唤物全部退场并重置至手牌区".
- #9 now: tokens.json
ownerRange; such a piece stands only onownerRangeKeys(loadout attack grid, owner tile, owner dir)— enforced by the server (_legal/summonRange), the client (drag highlights; toast "只能部署在召唤者攻击范围内"), the bot and the invariants. Moving the owner sends its summons back (unchanged); in swaps each piece is checked from the tile it ends on; an in-place re-orientation or promotion that leaves the summon outside sends it back to its stack with a toast — one still inside stays (user-confirmed, §21.20); with nowhere to go (hand and temp full) the re-orientation is refused withHAND_FULL, on the other paths the summon leaves the board and comes back at the next round start (grantTokensFor). No out-of-range placement reaches the battle. - ASSUMED: the range is the loadout's grid (
attackRangeGrid, §21.16; items and bonds do not widen it); automatic placements never put a melee unit on a 射击台; the canoe in the deploy map only. - Tests:
test/match/feedback1-placement.test.js,test/sim/feedback1-water.test.js; board, playtest5-deploy, playtest6_summons, ui/gameLogic, pathing-blockable / -crosscheck updated; opt-intest/ui/feedback1-placement.e2e.test.js(SP_E2E=1; fastServerSP_STAGE).
21.4 Enemies that change form when knocked out (#5 转译基底·α, #8 深池逐火) — content/enemies.js (husk, kitTranslator, reborn, statue, setForm, rebirthCleanse), sim/ai.js enemyAttack, sim/snapshot.js, shared/protocol.js fxForm, battle/runner.js keepsState / hold / compactHeld, screens/game.js keepEarly, render/interp.js isCosmeticEvent, render/app.js renderInfo, render/units.js FORMS, render/spine.js
- #8 seen: the 怨恨的余烬 could not be killed. Cause: the ember stood still, 隐匿 AND unblockable, so no operator could ever target it (a real R3 round on 战场#01: 0 of 7 killed, 7 leaked).
- #8 official (PRTS 深池逐火战士 / 护卫 天赋;
Revive[Trigger]5 / 10,interval10; PRTS 特殊机制 §重生 / §特殊生命值机制; 作战机制 §隐匿): every knock-out → a 1 s 重生 (immobile, 无敌 + 无法阻挡 + 失衡免疫); then the ember — max HP 5 (护卫's 火灰 10), 1 per damage instance, no attack, 隐匿 + 缴械; not killed within 10 s → the warrior with full HP. A blocked enemy loses 隐匿. - #8 now: the ember walks on; blocked, it is targetable and drawn solid (the stealth bit only while its 隐匿 is on). The same round: 7 / 7. An operator's radius area damage reaches it only while blocked or revealed (§21.19).
- #5 seen: 转译基底 played a "transform" clip and vanished. Causes: no clip sets for its forms (it stayed on A_* and died on B_Die, the 寻仇者's death clip); its first form took damage; and the client dropped the change fx in catch-up frames, hidden tabs, the pre-entry buffer and the render engine's stale-event drop (b.snap carries no form).
- #5 official (PRTS 转译基底·α 天赋; talents Passive / Mode_*_Passive / Mode_Fuchou_Anger): no attack; every damage instance cancelled; 失衡免疫 + control immunities; the 4th physical hit → 寻仇者, the 4th arts hit → 特战术师 (2 targets), a block → 幽灵 (unblockable, no attack — PRTS 幽灵 "不进行普通攻击,无法被阻挡"); once, 2 s.
- #5 now: A_Die_B / C / D, then B* / D* / C_*; no HP lost before the change (an HP loss stops at 1); the armed forms attack through
ai.js enemyAttack(e.profile). - Kill accounting: a survived knock-out gives no kill, credit, bounty, die event or death hook; wave completion and 联防 counters see the enemy standing; a bounty coin is paid once, at the final death.
- 重生 (PRTS §重生 "清空自身身上除白名单外所有Buff"):
rebirthCleanseremoves ally-sourced buffs and source-less statuses, keepingpersisttalents, enemy-given buffs (锏's 抵抗) and source-less field state; its skills restart from their initial cooldown when it ends (§21.19). - Forms on the client:
setFormkeepse.form(UnitInfoform) and putsformon its fx (fxForm, the 'phase' fx too, so barrier / charge phases are no forms). No client stage drops a form fx: runner catch-up frames (keepsState), the hidden-tab backlog of the battle on screen (hold; pastHELD_MAXsuperseded status / skill toggles are compacted away,compactHeld, and only a backlog still past it re-enters the view from the field meta), a battle that ended while hidden (delivered when the tab is shown; every batch keeps the game time it was drained at,EV_SLICE), the game screen's pre-entry buffer (keepEarly, also while a re-sent field meta re-enters the field on screen) and the render engine's queue (render/interp.js isCosmeticEvent: a form fx is never dropped as stale nor shed; one handed out late switches the model without its telegraph or a change clip that would already have ended, its closing clip shortened — the last three since the integration, §21.19). Late views:renderInfokeepsform. Closing clips are timed from the fxdurand keep the current form's death clip until the next form's fx. - Audit: 锏 / 扎罗 had #5's look (fixed: their 重生 clips); 杰斯顿 → C2_*; 守墓石像 per PRTS (statue, then flight, §21.9); 再生: 1 s 重生, then shields within 1.8. No unit stays untargetable. “余音” stays unmapped (no source says which form is A).
- ASSUMED: the translator is immobile during its change and stays damage-cancelled; the HP-loss floor 1; form letters B / C / D; the 特战术师 form also hits flyers (the standalone PRTS 特战术师 "不会攻击飞行单位" may carry over — one line,
canTarget: (u) => !u.isFlying); the ember's 10 s counts from the end of the 重生; 再生's 傀儡 walks; the statue does not attack (clip Sleep); 隐匿 returns at once after a block (PRTS: 3 s); the 重生 whitelist. - Tests:
test/sim/feedback1-transform.test.js,test/match/feedback1-runner-forms.test.js(stalls, hidden tabs, a battle ending while hidden, compaction, the runner feeding the render buffer),test/render/interp.test.js,test/render/forms.test.js,test/content/enemies_bosses.test.js; opt-intest/render/feedback1-forms.browser.test.js(RENDER_E2E=1; blocking 1 s long tasks).
21.5 The 鸭爵 strategy's enemies (#7) — content/enemies.js (runWhenHit, 圆仔 kits), content/bands/meta.js duckReplace / duckGlobal, Match.startFinalAssault (event routes / side), tools/build-data.mjs bandSwapEnemyKeys
- Seen: "鸭爵策略带来的怪物圆仔错误的能被阻挡"; the user asked for an audit of the other three. Cause: 圆仔's kit had only the front guard (its unblockability is in PRTS's talent text, not in the handbook ability list). The audit found more: 鸭爵's run multiplied the speed by
run.attack@move_speedinstead of adding it; the swap ran only in normal battles and only on NORMAL-rank enemies; the four kept the database's roguelikelifePointReduce0; on a pair boss field a player's swaps came from both halves. - Official: PRTS 圆仔 天赋 "无法攻击/被阻挡;受到来源于正面的物理和法术伤害-80%;自身始终朝向我方干员数量最多的方向"; PRTS 鸭爵 天赋 "受到伤害时开始奔跑,移动速度+400%"; PRTS 卫戍协议:盟约 下半/PRTS盟约记录 §策略 鸭爵 备注: "每回合将有0~2名敌人被替换为上述敌人之一,且上述敌人为特殊版本,相较于原版更弱" (popup: 鸭爵 6000 HP, +300 % speed when hurt; 高普尼克 11000 / 1600 ATK; 流泪小子 4500, 3.5 s stun; 圆仔 13000), "但进入保护目标点将减少1点目标生命值,且在最终回合和隐秘核心回合中仍然生效". Data: the band's enemylist names the
_2versions (blackboard min 0, max 2, round 5, minweight 0.6, maxweight 0.99). How the swap is drawn is in no source. - Now: 圆仔 / 圆仔_2 unblockable + front guard; 鸭爵 / 鸭爵_2 run ×(1 + v) once (+400 % / +300 %). The swap runs in normal, Final Assault and Hidden Core battles (联防 adds none); candidates: any ground wave enemy, normal or elite, except the leader, its parts, bounties, earlier swaps and uncounted units — each swap takes the replaced unit's time and route; on a pair boss field each player's 0–2 come from the enemies whose route ends on that player's half (
ev.side+ev.routes, the bounties' half rule), the 60–99 % share taken over that half. The four_2enemies havelpr1 (builder): the Final Assault / Hidden Core LP and the card's 目标价值 read it. - ASSUMED: the uniform 0–2 count and the uniform pick; minweight / maxweight = the 60–99 % part of the player's enemies in time order; ground only; elites eligible (about two thirds of the leader rounds' escorts in that share are elites; NORMAL-only would leave ≈ 80 % of R14 pair halves without a candidate vs ≈ 36 %); per-half swaps; decided at battle start (not in the prep preview); 圆仔's backward pose not rendered.
- Tests:
test/match/feedback1-ducklord.test.js(a real R6 wave; solo Final Assault + LP; co-op Final Assault → Hidden Core pair fields on the real match path),test/content/bands.test.js,test/content/enemies_bosses.test.js,test/data.test.js.
21.6 AI players (#10 "人机有点太笨了") — server/match/bot.js, tools/botbench.mjs
- Measured on the 0.1.0 bot (
tools/botbench.mjs, same seeds on two builds): 0–2 refreshes a prep and side-grade singles sold again for 1 (≈ 2 merges a match, ≈ 2 elites at R13); a level-up could spend the funds a third copy needed; the bounty score ignored the enemy (co-op 绝境: 155 of ≈ 310 bounty enemies leaked); 信标 (destroys its carrier) went on the best operator; DPS ignored DEF / RES; the focus bond flip-flopped. - Now (decisions only, no rule or balance change): refresh vs buy by the shop odds of completing held pairs (a full board buys only merge progress or a lineup upgrade worth more than the refreshes its price would pay for; an elite's value includes the merge's free pick); ≤ 4 pairs on the bench, merges before level-ups, a freeze for an unaffordable third copy; one committed focus core bond + a second bond, a teammate's main core bond read from its public bond strip (humans too) counting against it; dealers valued by DPS against the round's DEF / RES; 坎诺特 keeps its interest capital (5) back; bounty picks by expected payout − expected leaks × (2 + 20 / LP) with the kill chance from the exposure model — an expected-value pick (co-op leaks are bound by the drafts, §21.2); items by effect (信标 on a bench single, 拟态物质 on a pair, both 博士投影 on the strongest normal operator, 突变细胞 per §21.1, bond items on a focus member, survival items on blockers); 教鞭 / “神秘顾客” used after a perfect battle and kept otherwise — a bot drops a kept one only for a hand slot, a human's seat under AI 托管 never; a tactician's 援军 inside
PlayerState.summonRange. - Result (seeds 1–40, rehearsal 3, on the WF branch): 独立 绝境 19 → 26 / 40 wins (LP left 14.3 → 16.6), 同盟 绝境 (4 AI) 5 → 12 / 40 (5.1 → 12.7), 标准 unchanged; merges per bot ≈ 2 → 3–4; forced strategies (solo 绝境, seeds 201–220): 坎诺特 9 → 14, 昆图斯 9 → 11, 杜宾 8 → 9. Decision time unchanged: heuristics CPU p50 / p95 31 / 74 → 28 / 70 ms solo and 17 / 39 → 17 / 38 ms co-op; the whole prep with its rehearsal 89 / 205 and 83 / 162 ms wall.
- Reconciled with the merged rules (§21.19): the server's legality (deploy map without the 深水区,
summonRange), WA's cell rule (and its destroy-then-gain correction: the gained operator is deployed by the lineup step, the bench shed spares a piece gained since the last prep — §21.1), WB's draft structures (it scores whatever is offered), 本局禁用 bonds (never planned), the range a unit is deployed with, and the AoE of §21.18 in its DPS value. On the merged build, seeds 1–40: solo 绝境 27 → 27 wins, co-op 绝境 9 → 11 (within the noise); decision time stays (heuristics CPU 24 / 57 ms solo, 16 / 36 ms co-op; the whole prep 78 / 153 and 73 / 151 ms wall; BALANCE §5). After the QA: WB's 22-match bounty lists (a giant in every R11 bounty list, 14 of 22 R11s a 悬赏决策) take the same bot to 22 / 40 and 9 / 40 — a rule change; the bench-shed fix (never sell a piece of this prep, §21.19) then 24 / 40 and 10 / 40. The 0.1.1 build against 0.1.0 (19 / 40, 5 / 40): 24 / 40 solo 绝境, 10 / 40 co-op 绝境 (4 AI) — part of it is the rules: 0.1.0's bot on the 0.1.1 rules wins 23 / 40 and 7 / 40 (LP left 14.6 / 5.2 against 17.4 / 13.1, merges per bot 2.2 / 2.0 against 3.8 / 3.1); same-prep buy → sell in co-op 绝境 (2 AI 托管 + 2 bots, seeds 21–26) 29 → 3; decision time unchanged (one thread, seeds 1–3: the whole prep 81 / 187 → 81 / 184 ms wall solo, 71 / 153 → 71 / 129 ms co-op; heuristics CPU 36 / 88 → 35 / 86 and 16 / 40 → 16 / 40 ms). - ASSUMED (tuned; the official AI is undocumented): MERGE_HIT 36, MAX_PAIRS 4, BENCH_BUDGET 6, FOCUS_MEMBER 4, ARMOR_WEIGHT 10, BOUNTY_KILL 2, the LP value 2 + 20 / LP, the teammate penalty 6, the 坎诺特 reserve = the band's interest capital,
CROWD(×2 / ×1.3 / ×1.4). - Tests:
test/match/bot-smarter.test.js(bounty picks, item carriers, Arts kept / AI 托管 safe, tactician range, pair via buy or freeze, 坎诺特, the bond strip, the merged rules, a CPU cost guard < 300 ms),test/match/feedback1-meta.test.js(AI 托管 never destroys an item to clear temp).
21.7 奥术 works; 本局禁用 bonds; the 源石溶剂 merge (B1, B2) — ui/gameLogic.js modeOffBonds, ui/detailPanel.js BondChips, ui/shopBar.js ChessCard, ui/bondStrip.js BondPopup, screens/game.js, match/PlayerState.js _mergeChess, match/effectsMeta.js destroyPiece, content/bonds/addon/battle.js
- B1 measured — the bond works (a real 险境 battle through
Match._normalOpts, and the same battle from its BattleSpec, the browser path — identical): each member's arts hit debuffs its target ×(1.2 + 0.01·L) arts damage taken for 3 s and later arts hits land multiplied (深靛 + 洛洛, R5, seed 7: 41 / 54 arts hits debuffed at L0, 0 without the bond); 3 different members: below 50 % HP ×1.4·(…) (seed 7: 9 hits at ×1.68 and, after 海霓's live +3 layers, 19 at ×1.722); all 10 members × S1–S3 × normal / elite apply it, summons do not (not 干员); leaders too (Final Assault 绝境 R14, 999 谢拉格: 329 819 → 426 911 leader damage in 700 s at L0). - What a player saw — fixed: 标准模拟 switches 奥术 off (official modeDataDict inactiveBondIdList: 拉特兰 阿戈尔 卡西米尔 灵巧 奥术 奇迹 投资人 突袭 独行 绝技), while 深靛 洛洛 阿罗玛 夕 圣聆初雪 stay in the pool through their other bond; their cards showed 奥术 normally, the chip read 「0/2 未激活」 with three deployed and the popup 「在场 0/2 未激活」. Now such a bond is struck through on shop / reward cards, the chip reads 本局禁用, the popup reads 本局禁用 with a note and no 在场 count or 当前效果 block (its numbers would promise an effect) [ASSUMED look].
- Official, unchanged: 限伤 cancels a leader hit ≥ 300 000 in boss / hidden battles (§20.12), so very large multiplier stacks can make huge hits vanish (巴哈姆特 12316); our measured boards stay far below it (largest 24 813).
- The stacking rule, reconciled — §20.10 stands: PRTS 盟约记录's current note 「※同一单位仅可对同一目标同时施加1个该盟约法术伤害提升效果」, read with the engine default (PRTS 常见同名状态) and 巴哈姆特 12316, gives one effective instance per target (applyStrongest). Known deviation ASSUMED: an earlier revision (oldid 386936) said a unit cannot re-apply while its own instance lasts; an equal re-hit refreshes it here (slightly stronger). Strongest rather than earliest stays ASSUMED.
- B2 — not reproduced: every PlayerState path ends with exactly one golden and no white (buy next to a white in the hand or equipped, two buys, equip on another or a full carrier, a reward pick, 道具补给, 列装, 整备 + buy, 画卷, the carrier's sale); random intents over all 50 mergeable items conserve W + 2·G and never leave a pair (upgradeNum 2 → _b; PRTS 帮助 「已拥有2件同一初始装备时…自动合并」). A latent gap closed: equipment returned by a promotion (
_mergeChess) or byctx.destroyPiece(突变细胞, the round-start promotion item) now runscheckItemMerges(unreachable by a player before). - ASSUMED: 整备 bought next to an owned white merges the two first and keeps the effect for the next purchase (the official text may mean the bought item turns golden); the 标准 popup lists members the mode keeps out of the pool dimmed as not owned.
- Tests:
test/sim/feedback1b-arcane.test.js,test/ui/feedback1b-bonds.test.js,test/match/feedback1b-items.test.js.
21.8 维娜·维多利亚's lions; 玛恩纳 S3 hits air units (B3, B4) — kits/tier6.js siege2, tokens.js SKILL_SUMMON_UNCAPPED, kits/tier5.js 玛恩纳
- B3 seen: her S3 (default, defaultSkillIndex 2) summoned one 黄金盟誓. Real: on the real match path (战场#05 下半, R8, (10,5)) every cast placed 1 lion beside 3 free melee tiles (the reviewer's sweep of m01–m04: 74 casts, all 1 lion). The kit picked one tile, and the per-owner deploy limit (the token's phase maxDeployCount 1, a hand count) would have withdrawn a second.
- B3 official: skill_table skchr_siege2_3 "立即在天赋一生效范围内可部署地面召唤“黄金盟誓”"; EN client "Summons Golden Vows on deployable tiles within Talent 1's range"; wiki.gg "only be spawned on open low ground tiles"; PRTS 备注 "于技能结束时/维娜退场时自动退场".
- B3 now: one lion on every free tile of her 3×3 talent area where a melee piece could deploy (LOW, ALL / MELEE, unoccupied, not a reserved home tile;
grid.canStand, so never the 深水区), at most 8, for the skill's duration;SKILL_SUMMON_UNCAPPEDkeeps the deploy limit off them; each counts for 诸王的叹息 (546 × (1 + 1.45 + 0.30) with 6 lions). 1 → 3 lions at (10,5) on 战场#05, 6 at (11,4) on 战场#01. - B4 seen: 玛恩纳 "开技能没伤害". Real, on flying waves: his default S3 fires (CUSTOM_RANGE 3-18) on any enemy, flyers included, but his attacks were ground-only melee — a flyer-only cast hit nothing for 26 s, then the trait ramp reset (4 of 19 real casts; seed 21 R2 dealt 0).
- B4 official (PRTS S3 备注): "※可对空"; "每击倒一名符合条件的敌人,特性提供的攻击力加成-10%,特性最低降低至+0%" — only "自身普通攻击(与该次攻击附带的伤害)" kills count, "于当次攻击后统一结算", not kills by 第二天赋 (reflection), by a mark another unit's attack set off, or sourceless ones; S2's "不重置" follows the same rule.
- B4 now: S3
targeting.canHitFly(flyer-only casts 0–10k → 14k–93k damage); bonus = max(0, ramp × trait_up − 0.1 × own kills), settled after the attack; an own kill = an enemy his current attack (or the mark it set off) damaged — the set is cleared after each attack, so carried damage counts and reflection / teammates' marks do not. His trait sets block 0 only while the skill is off: during S2 / S3 he blocks up to 3, and the 卡西米尔 bond's true damage rides his attacks only while he blocks nothing. - ASSUMED: fence tiles (low, deployable, not walkable) count as 可部署地面; "−10 %" = absolute points of the bonus.
- Tests:
test/content/feedback1b_kits.test.js,kits_t6(every lion off the reserved tile),test/sim/feedback1-integrate.test.js(no lion in 战场#08's 深水区).
21.9 Air units are 静态刚体 (B5) — tools/build-data.mjs STATIC_BODIES, enemies.json staticBody, sim/Battle.js _displaceable, content/bosses.js (胄 parts), content/enemies.js (statue, 喷气人)
- Seen (relayed: "飞机可以被薄绿的技能拉走(难道不能拉动吗?…你去复核下)"). Real:
_displaceablechecked only 失衡免疫 and leaders, so every flyer moved by the 力度 − 重量 tables — 薄绿 S2 dragged 妖怪 1.33 tiles per hit and an act2 m01 drone reached the goal 6.6 s late; with client combat drones were dragged 43–55 times per match; 崖心 S1 moved a 刺胄之弹 0.87 tiles; 守墓石像 moved 0.03–0.44 tiles (1.4–1.7 under 失重). - Official: PRTS 特殊机制 静态刚体 "可以进入失衡状态…但物理层面上无法产生任何速度或移动…※与失衡免疫不同…※是否为静态刚体与单位的行动方式无关"; PRTS 失衡位移机制 (air units count, "但会因下述的判断而被取消位移"); every air unit of the mode except “炎佑” (weight 10, 我方单位) lists {{特殊机制|静态刚体}} in its PRTS 天赋, and so does 盐风主教昆图斯 — 28 keys (all 249 enemy pages checked). 失衡免疫 also: 刺胄之弹 / 斩胄之剑 / 破胄之锤 (their talent also lists 不可阻挡, so a grounded 剑 / 锤 stays unblockable); 守墓石像 转换模式 "无法被阻挡,自缚,失衡免疫,免疫浮空" and 飞行模式 "变为飞行单位,失衡免疫"; 喷气人 飞行模式 "近地悬浮,不可阻挡,失衡免疫,移动速度+50%", −90 % for 1.5 s after take-off and 1.333 s unblockable at −90 % after landing.
- Now: a static body moves 0 tiles from every push or pull; skills keep their reach and still hit it (薄绿 S2, 锏 S3, the hookmasters, 莫斯提马 S3 …; distance effects come to nothing). 守墓石像's statue is unblockable, rooted,
noDisplaceand refuses 浮空, its flight keeps a persistentnoDisplace; 喷气人's 7 s flight is an air unit withnoDisplace, then the brake and landing windows. Ground enemies keep §20.3's tables; 吉兆飞鳞 / 掠海漂移体 are 失衡免疫 while hovering and movable once grounded. - ASSUMED: a static body's 0.1 s 失衡硬直 is not modelled; a 浮空 that 守墓石像 carries at its knock-out runs out; 见行者 S2 / 歌蕾蒂娅 S3 reach air units, 乌尔比安 S1 does not.
- Tests:
test/sim/feedback1b-displacement.test.js(the data guard, 薄绿 vs drones, the act2 m01 flight, the 16 × 5 audit, coins / 流形 / links, the 胄 shell, 喷气人, 守墓石像).
21.10 Damage broadcasts in the Hidden Core; the strip by priority (C1) — match/Match.js startFinalAssault / tickerText FLOW_TICKER_PRIORITY, match/finalAssault.js BOSS_HIT_STEPS / SharedBossPool.byPlayer, ui/ticker.js, main.js (m.ticker)
- Seen: "隐藏boss还没打就出了造成50%伤害播报". Reproduced in real bot matches into the 隐秘核心: solo 绝境 seeds 3–5, the hidden leader's first hit (seed 3: 2034 of 900000) announced "超过50%"; co-op 2 seats 险境 seed 3: "超过80%" at 36 % dealt.
- Cause 1 (server): share =
stats.bossDamage / pool.maxHp, andstats.bossDamage(the result's 领袖伤害) sums both boss rounds. Cause 2 (client): the strip played its queue first in, first out, 5.2 s a line; a Final Assault line played 2–8 s into the Hidden Core over its fresh leader. - Official [DATA]: activity_table
autoChessData.broadcastListcomment_boss_hit_1..3 "{0}博士对敌方领袖造成的伤害超过20% / 50% / 80%!" (BOSS_HIT, paramList 0.2 / 0.5 / 0.8, priority 30); constDatabroadcastBeginDelay1; client 2.7.71 keepsbossDamage[localPlayer]per boss battle (research 11 §2.1); research 06 §9.2 "The highest priority wins": BOSS_HIT 30 > CHAR_DAMAGE 20 > SHOP_LEVEL 11 > (bond 10) > GOLDEN_CHAR 2 > CHAR_GIFT 1. - Now (server): share = the player's damage to that round's
SharedBossPool(byPlayer, fed by every credit path) ÷ itsmaxHp; each threshold once per player per round, a hit passing several announces the highest;stats.bossDamageunchanged (both rounds). - Now (client): lines keep
type,playerId,priorityand the round they arrived in; a BOSS_HIT line from another round leaves the queue and the strip; a player's newer BOSS_HIT line of the round replaces their older one, queued or on screen; and (integration, §21.19) the queue is ordered by priority, first in first out among equals, the line on screen never cut short, the lowest priority's oldest dropped past 4 queued — the remake's match-flow notices (隐秘核心已解锁, 联防阶段, a player out or gone) atFLOW_TICKER_PRIORITY25 ASSUMED, contentticker()lines 0. - ASSUMED: the threshold test ≥ ("超过"); the per-battle reset of
bossDamage(inferred); a dropped line is not replayed; the 1 s begin delay is not modelled. - Tests:
test/match/bosshit-ticker.test.js(solo 绝境 server-run, 2 and 4 seats client combat, SP_COMBAT=server, a takeover),test/ui/bosshit-ticker.test.js(the queue helpers across a 14 → 15 round change, the replacement rule, the priority order, timeline replays).
21.11 变形同构体 wearers in the member list and on the card (C2) — ui/gameLogic.js grantedBonds / pieceBondIds / bondMembers, ui/bondStrip.js BondPopup, ui/detailPanel.js BondChips / ChessDetail / resolveDetail, ui/watchBonds.js ownerBoard, battle/runner.js ownerOps, sim/snapshot.js unitInfo, match/Match.js prepFieldMeta
- Seen: with 变形同构体 (缪尔赛思's 特质 garrison_76 / band 变形者集群) and the matching item the wearer gets the bond's effect, but "那个盟约的人口数量没有把转职之后的干员算进去". Partly real: the count that activates the bond already included the wearer everywhere (
bondsMeta.pieceBondsfor the prep count, m.private / m.public, the battle input;support.unitBondsin the battle — checked on real matches for all 36 pairings). The client did not: the popup's member list and the wearer's card read only the records' bonds (在场 3/6 next to 成员 2/10, no chip). - Official: band 变形者集群 "与特定装备一同装备时装备者将视为特定盟约成员"; eff_acarm073 "携带者获得额外盟约(由另一件携带装备而定)"; the item's talent lists the 14 pairings; a member counts on the board only (BOARD) and once per operator.
- Now:
gameLogic.grantedBonds= the server's rule (a test pins the three together); the popup lists wearers tagged 同构, the card shows the granted bond as a dashed 同构 chip; a 同构 row opens the wearer's card with its item ids; a card without an own piece lists the carried items read-only under 装备. UnitInfoitems?(simunitInfo,Match.prepFieldMeta) carries an ally operator's item ids, so teammates' popups and cards show wearers too. - ASSUMED, unchanged: operator talents that test "【拉特兰】/【卡西米尔】/【叙拉古】… 干员" read the record's bonds (kits tier1 内线 ammo pick, tier5 inFaction, tier6 hasBond, tokens.js 卡西米尔 sword) — no source says whether a converted operator counts for them; 调和's +1 raises 在场 for core bonds without a member row.
- Tests:
test/ui/morph-bonds.test.js,test/ui/watch-bonds.test.js, server-side locks in the match suite.
21.12 源石溶剂 is damage; 信仰搅拌机 S3 counters any damage (D1) — sim/damage.js periodicDamage / isHpLoss / applyHpLoss, items/battle.js periodic_damage, kits tier1 / 2 / 4 / 5, content/enemies.js, bonds/addon/battle.js, choices.js
- Seen (community): officially the 源石溶剂 drain sets off 信仰搅拌机 S3, and next to 莫斯提马 it stacks 拉特兰 on its own; the remake never cast S3. Reproduced on the real match path (R3; default S3 + 源石溶剂, 莫斯提马 above her, 隐现): v0.1.0 never cast S3 (SP full at 23 s); without enemies she drained to death with 0 layers.
- Cause: the drain was
loseHp— a 流失: no 受击回复 SP, no TAKE_DAMAGE, ignored by shields — and the counter needed an enemy attack and a target. - Official: PRTS 盟约记录 源石溶剂 "受到60真实伤害" (修正 "并非流失"; 备注 "造成无来源真实持续环境伤害" and "携带后,全场范围内的所有敌人类我方单位也会获得此装备的…效果(该效果的付与为我方阵营索敌,可对空,无视目标可选性)"); PRTS 作战机制 (生命流失 "不会触发反伤、受击回复"; "同名buff的默认叠加策略buff只能表现出一个"); PRTS 伤害分类 (BUFF damage includes "自残类型", "弧光锋卫失衡状态下的自残伤害", "深水区/涨潮水蚀"); PRTS 帮助 重装 "受到伤害时释放技能"; PRTS 信仰搅拌机 S3 备注 "反击受到任何伤害后均可触发,无需目标,视为普通攻击"; garrison_22 (莫斯提马): every 6 bullets spent on her 4 tiles → 【拉特兰】 +1 / +2.
- Now:
periodicDamage(n)= 无来源 true damage (canDodge false, tags dot / periodic) with the old credit — shields, 脆弱,hiteffects, 受击回复 SP and TAKE_DAMAGE all see it, the kill credit is unchanged. S3 counters every damage but a 流失 or an element 损伤, spending a bullet even with no enemy in range (measured without enemies: +5 拉特兰 per cast, elite 2 casts +22; with the R3 wave +7; the browser sim gives the same digests). The drain also ticks on every 敌人类我方单位 of the field (炎佑, a partner's too), credited to nobody, once per second however many carriers, while a carrier is on the field. Real damage instead of 流失 also for 狂暴宿主组长 (悬赏·特异III), 码头水手's drowning ("每秒受到1000点无来源真实伤害"), 弧光锋卫's 失衡 bleed and the 孽罪奇美拉 aura (now radius 1.2, a tick every 0.5 s, one tick per unit — "同类效果取最高"). One rule both ways (isHpLoss): 雷蛇 战术防御 and 蒂比 S2 react to real damage; 乌尔比安, 伪装服, 远牙, 录武官's guard, 坚守 thorns and 机变 自愈 skip a 流失; operator 流失 (宴, 风丸, 水月, 华法琳, 烛煌, 史尔特尔, 号角 …), 传递 and the drone link stay 流失. Damage dealt (unitdmg, the results' 造成伤害) never counts the source's own side. - ASSUMED: the drain gives 受击回复 SP; the chimera aura is 无来源; a target-less counter is one bullet with no hit; the 炎佑 tick ends with the carriers; drowning keeps its 0.25 s cadence and the 失衡 bleed stays per distance moved (the amounts are official); 心烛 and 扎罗's transfers stay 流失.
- Tests:
test/content/feedback1d-solvent.test.js,items.test.js,kits_t1t2.test.js(蒂比).
21.13 野鬃's push direction (D2) — kits/tier1.js (野鬃), kits/tier3.js (琳琅诗怀雅 S3), content/generic.js, sim/Battle.js push, constants.js
- Seen (community): "干员野鬃的技能有时会把怪物往攻击方向相反方向推". Real: S2 夹枪冲锋 pushed radially at full force (§20.3's table), so a target behind her centre — an enemy that walked through her tile while her single block was taken, grabbed by the §19.2 hand-over — was thrown 1.7–2.14 tiles towards the objective. Measured on act2 m01–m04 (R3 / R5 / R7 official waves, every melee tile × 4 facings): 2689 of 15915 pushes (16.9 %) went more than 0.5 tile against her facing (LEFT 47 %).
- Official (client data): her S2 attack ability (charpack char_496_wildmn, anim Skill_2) carries buff
wildmn_s_2[force]of templateknockback[dir]= Knockback {_useSourceDirectiontrue,_decreaseForceLevelWhenNotInDirection2} (battle/buff_template_data.json): a 方向力 along her deploy direction with the PRTS 推与拉 特殊修正 (> 45° off or < 0.25 tile → radial at 受力等级 −2). Radial =knockback[relative]: 琳琅诗怀雅 S3 (swire2_s_3[knockback]; PRTS 备注 "推开效果为径向推动"), 山 S3, 莫斯提马 S3. 圣聆初雪 S1 = KnockBackWithCharacterDirection (no correction). Cross-check: 莱伊 S1ray_s_1decreases by 1, matching its PRTS 备注. - Now: 野鬃 S2 =
Battle.pushwithdir=unit.fwd(within 45° the full 中力 table along it: w0 2.14, w1 1.7, w2 0.44, w3 0.12; behind, beside or < 0.25 tile radial at −2: w0 0.44, w1 0.12, ≥ w2 0) — 0 pushes over 0.5 tile against her facing in the same sample; 琳琅诗怀雅 S3's coins are radial from her centre at full force (they went along her facing). The generic kit's text keywords (now also 往攻击方向) are a fallback only. §20.3's table is rewritten accordingly; nothing goes into ASSUMED. - Tests:
test/sim/feedback1d-push.test.js(4 facings × ahead / ≤ 45° / behind / beside / < 0.25 tile, the mirrored boss half, a real act2 m01 R5 hand-over, 琳琅 S3).
21.14 Special Originium slugs and 暴鸰's bomb (D3, D4) — tools/local-extract/extract.py ENEMY_SPINES, tools/assets/local-enemy-spines.json, plan.mjs localEnemySpines, fetch-assets --local-spines, assets.js spineEntry, render/units.js ALIAS_TINT / FORMS, content/enemies.js kitBombd, snapshot.js form, style.js PROJ.droneBomb
- D3 ("所有特殊源石虫的模型全表现为普通源石虫"), partly real: 高能 / 冰爆 / 简饲源石虫 and “庞贝” always had their own models; 灼热 / 炽焰源石虫 (enemy_1305_mhslim / _2, the ELEMENT slugs, up to 10 a round) were the plain 源石虫 — no web dump has their models (the asset plan aliases them); the official skeletons exist only in the client (refs/arts/enm_art_12.ab).
- D3 now: an optional overlay — the manifest keeps the alias as the web model and
enemies[id].spineLocalnames the files of the data/local-assets.json groupspine/enemy/<id>with the metadata of the committedtools/assets/local-enemy-spines.json(data/assets.json is byte-identical with or without the extraction). The client draws the official model when the local manifest lists every file, else (or on a load failure) the web alias tinted toward the slug's own lava colours (ALIAS_TINT: 灼热 orange, 炽焰 red-orange, research 07 §5.6) [ASSUMED look]. The 0.1.1 release bundle carries the models (extracted withnode tools/setup.mjs --local, thespine/enemyjob). Audit (249 enemies vs the client's prefabs): no other enemy is drawn with another's model; 伊利昂的木驮兽 starts on skinFullin the game and is drawn withdefault(unchanged).fetch-assets --prunenever deletespublic/assets/localany more. - D4 ("暴鸰的炸弹无法正常投放"), partly real: the bomb hit in the trigger tick, with no projectile, and stayed under the drone. Official (prefab enemy_1040_bombd / projectile_bombd; PRTS 暴鸰): Boomb plays Attack, the bomb leaves on OnAttack (frame 8) and flies homing at 5 tiles/s; the cast ends when the bomb is gone, ≥ 0.667 s after the release; buff bomb_s: the bomb-less mode S1 (*_2 clips) + speed ×2 ("技能结束后");
_immuneStunWhenAffecting0. - D4 now: the drone holds still at the trigger; 8 ticks later (8/30 s) the release ('atk' droneBomb + the form 'bombed' — UnitInfo
form); on landing the target 100 % ATK + the 8 tiles around 100 % splash (a target gone mid-flight: the bomb lands where it was; the drone's death after the release does not stop it); at max(landing, release + 0.667) ×2 speed and it flies on. A stun / freeze / sleep before the release interrupts the cast (no drop; the skill re-arms with its 1 s cooldown). - ASSUMED: no drop by a dead drone; the ATK at the release; holding still through the cast; the bomb-less look from the release; an interrupted cast keeps its one trigger; a stun after the release does not stop the cast end; the trigger radius 2 (§20.4).
- Tests:
test/feedback1d-models.test.js,test/sim/feedback1d-bombd.test.js(a real wave),test/render/feedback1d-bombd.test.js,test/static-local-art.test.js.
21.15 Enemy ground routes: 战场#04's official diagonal (D5) — server/sim/grid.js (_buildField, _spfa, smoothChains, crossTiles / segmentTiles), data/stages.json groundPaths*, test/sim/pathing-official / -blockable / -crosscheck / pathing
- Seen (real): on 战场#04 活性源石 (act1autochess_m04, the only stage with tile_infection) the lower-gate enemies walked (9,10) → (9,8) along row 9, then straight up into (10,8).
- Official: every autochess route has
allowDiagonalMovetrue andvisitEveryTileCenter/visitEveryNodeCenterfalse; normal WALK routes have no checkpoints; the client SPFA flow field + Bresenham smoothing (research 08 §3.1; PRTS 作战机制 "所有敌人始终选择最短路径行动…") gives (9,10) → (10,7) → (10,4) → (9,4) → E — a diagonal that touches the floor (10,9) only at a corner. - Cause: playtest #2's road-over-floor smoothing (§14 corrections) counted a diagonal step's corner tiles as floor and bent official diagonals into L shapes even where they cross no floor.
- Now: each flow field keeps two pointers per tile with the official distances — the official one, and 0.1.0's playtest #2 preference one restored exactly;
nextis the preference pointer only if its route crosses strictly fewer non-blockable tiles (exact geometry: a segment crosses the tiles whose interior it passes through, a corner touch is not a crossing), ties keep the official pointer (unless the preference one only skips a collinear official waypoint). Kept (playtest #2): 战场#01's lower-gate enemies take the col-8 road; on 战场#02's Final Assault both halves leave the exit via (2,9) / (2,11), so a blocker beside the exit holds them. - Audit (11 stages × 14 gate / field legs = 154 routes): 140 official as merged by the workstream (0.1.0: 109); with the 深水区 refusal (§21.3) 7 routes of the inactive 战场#05(上半), whose canoe water now counts as non-blockable, differ too — 21 of 154 (pathing-official
DEVIATIONS), each crossing strictly less floor. Visible: the boss exits of 战场#04, #05(下半) and #07(下半) are official diagonals — at #07(下半) the enemy passes (2,9) / (2,11) at √0.5 ≈ 0.70711 > the block radius 0.70709997 (§19.2), so an operator there no longer blocks them, as with the official route. - Unchanged: movement tile centre to tile centre along
next[][ASSUMED simplification];findPath/groundPathTilesreturn Bresenham tiles. One-line flip: alwaysuse = oin_buildField(pure official routes; reverses 战场#01's col-8 road).
21.16 The live attack range on the card; 烛煌 S3 (E1) — sim/Battle.js _refreshRange, sim/skills.js changesRange, shared/loadoutRecord.js attackRangeGrid / traitRangeExtend / extendedGrid, shared/protocol.js unitStatsEntry, ui/detailPanel.js, kits tier4 (信仰搅拌机) / tier5 (烛煌) / tier6 (荒芜拉普兰德)
- Seen: "干员烛煌开启3技能时候攻击范围不会变". Partly real: the sim was right (real 绝境 matches: 80 of 152 S3 hits on tiles only 4-11 reaches, none on 4-11 missed; the cast keeps the initial 3-1 per §20.2), but the battle card's 攻击范围 mini-map stayed 3-1 while the live ATK / interval beside it changed — the battle board has no range overlay.
- Official: skill_table skchr_blaze2_3 rangeId 4-11; PRTS 烛煌 S3 备注 "攻击溅射半径1.7;于攻击造成伤害前判定元素爆发并造成元素伤害", "补充后的弹药数量无法超过上限"; PRTS 信仰搅拌机 S3 备注 "此技能的攻击范围不受“攻击距离”属性影响".
- Now (card):
unit.liveRangeGrid(the running skill's grid, else the unit's own, plusrangeExtend) →unitStatsEntry.range(allies only) → drawn in battle 4× a second and in the prep preview; 400+ tiles read 全场, a grid beyond 6 × 7 draws smaller cells in the same box. - Now (record range):
attackRangeGrid= the range a unit is deployed with — a selected "被动效果:攻击范围扩大" skill (引星棘刺 S3 3-9), else a module's 攻击范围扩大 grid, else its own, plus the 特性's permanent 攻击距离 (traitRangeExtend: 信仰搅拌机 SPT-Y +1; 空弦 ISW-A works in 集成战略 only) — used by the card without a live entry, the board overlay, the deploy wheel, the server'ssummonRangeand the bot; over all 1284 loadouts it equals the battle's range at deployment. - Now (kits): 烛煌 S3 = one target on 4-11 + a 1.7 splash (中点判定; it used to hit every enemy inside 4-11); the 灼燃 burst bonus is dealt before the hit, on main and splash victims, from the attack as fired (her last shot lands after
end('ammo')and keeps it); each landed shot emits fx 'splash'; a burst refills ammo only up to 18 / 21. 信仰搅拌机 精锐 + SPT-Y + S3 was 11 tiles, now exactly 3-13 (targeting.noRangeExtend). 荒芜拉普兰德 S1's target grid only selects (targeting.showOwnRange: the card keeps 3-1). - ASSUMED: 技能范围 areas (古米, 泥岩, 山 S3, 锏) show while their skill runs; content extra tiles (蕾缪安, 维娜 S3) are not drawn; the 全场 / 6 × 7 thresholds (远牙's line is 2.1 px per cell on an 844 × 390 phone); the bonus also lands on a hit that is then dodged.
- Tests:
test/sim/feedback1e-skillrange.test.js,test/ui/feedback1e-range.test.js,test/render/feedback1e-splash.test.js,test/ui/playtest2.test.js.
21.17 Healers that block keep healing — not reproduced (E2) — sim/ai.js acquireTargets, sim/skills.js _defaultCondition, Battle.blockedTargets, test/sim/feedback1e-healers.test.js
- Seen (relayed): "走炎盟约的时候,如果是那种会到处跑的BOSS,被医疗阻挡之后,医疗不奶人了,会输出BOSS而且如果是那种强力击类型的技能也会在BOSS身上打出正确数值的伤害"; the user asked to check every healer under every bond.
- Official: PRTS 卫戍协议/帮助 (技能策略) "对于医疗干员(咒愈师分支除外),攻击目标为需要治疗的单位"; PRTS 仇恨: a medic's heal uses HP_RATIO_NOT_FULL_ASC (full-HP units excluded, itself counted); PRTS 选择器: blocked units are added only if the selector's side can pick them; 咒愈师 "攻击造成法术伤害".
- Verified, not reproduced (real Battle, skills made ready while blocking): the 炎 lineup (9 炎, tier 3, 2 炎佑) — 录武官 normal / elite × S1 / S2 holds 卢西恩 / 不祥幻影, casts while holding, deals 0 damage and keeps healing; every pure healer (医师 / 群愈师 / 疗养师 / 链愈师 / 行医: 26 records, 56 loadouts, each skill cast while blocking) × 23 bonds as a member, and 112 items: 0 damage; the 20 boss / hidden fields: 0 healer damage (healers block 卢西恩 / 不祥幻影 and a 碎铳之簧 with its shield down; the other leaders and parts cannot be blocked by a block-1 unit). 古米 S1 / S2, 塞雷娅 S1, 波登可 S1 heal allies while blocking; 瑕光 S1 / S3 strike + heal by text; 咒愈师 hit what they block (§20.3); 吟游者 make no normal attack.
- Likely what players saw (all official): 卢西恩 evades only while unblocked (in the 炎 run 11 % of hits dodged and 1,162 dmg/s while it roams, 0 dodged and 6,751 dmg/s while the medic holds it — the numbers come from the others and 炎佑); a blocking medic heals itself first when it is the most injured; 神经 bursts stun it for 10 s; operators do not turn, so its heal animation faces the boss.
- Now: no behaviour change; the rule is written in §5.5, SIM.md, the ai.js header,
Battle.blockedTargets, targeting.js and PLAYING, and locked by tests. Not checked: whether 华法琳 S2's official 流失 can kill an ally (the remake's can; PRTS gives no non-lethal note).
21.18 阵法术师 / 轰击术师 strike every enemy in range (E3) — sim/professions.js (SUB.phalanx / blastcaster, resolveProfile rangeAoe), kits/tier4.js 卡涅利安, the 流形 copy (content/tokens.js, kits/tier6.js), render/fx.js
- Seen: "干员卡涅利安的攻击不是真群攻". Real: the trait "技能开启时攻击造成群体法术伤害" was one bolt at the priority target + a 1.1 splash — spread enemies took 1 hit of 5 per attack; on act2 m01 R10 the elite 卡涅利安 hit 2.3 of 12.4 enemies in range per attack, 薄绿 2.0 of 12.6; on a real Match every 阵法术师 and 轰击术师 hit exactly 1 target while 2.7–4.7 stood in range.
- Official: a 阵法术师 / 轰击术师 attack strikes every selectable enemy on its range at once, the same damage near and far — primary for the 轰击术师: PRTS 作战机制 §AOE伤害判定, a 锁定攻击范围 AoE "在攻击前摇结束时选取范围内的全体目标,同时造成伤害" (its example, 伊芙利特's 炎爆, is her next-attack skill "下次攻击造成…"); for the 阵法术师 Terra Wiki Phalanx Caster (secondary) and PRTS 林 S3 备注 (one normal attack can kill several); supporting only: PRTS 溅射半径一览 has no radius for either (it omits 撼地者 too). 扩散术师 keep a 1.1 splash around the target (格雷伊 1.0, §21.19).
- Now: phalanx and blastcaster are
rangeAoeprofiles —resolveProfileapplies the flag after every override (kit trait included):allInRange+ instant'beam'hits drawn as a line to each victim (no splash ring, no shells). Blocked enemies are struck too (§20.3); a stealthed one only when revealed or blocked. Per-hit kit effects run on every enemy struck (薄绿 S2 拖拽 — a 静态刚体 is not moved, §21.9 —, 卡涅利安 S2 停顿 / 束缚, 阿罗玛 浮空, whose first-attack mark is shared between 阿罗玛 since §21.19). 卡涅利安 (PRTS 备注): a charged S1 keeps the skill-off trait — the guard AND 不攻击 for the whole skill (players will notice); S3's ATK climbs in 1 s steps to +140 % / +200 % at 20 s; the charged mark stacks before the damage, onebillro:markper enemy (another 卡涅利安 only adds stacks), ending with its setter's skill. A 流形 copy of a 'beam' becomes a bolt. - ASSUMED: no target cap (no source states one); damage at the attack instant (no wind-up modelled); the beam look.
- Tests:
test/sim/feedback1e-aoe.test.js(incl. a real wave counting the damaged enemies per attack id); opt-intest/render/phalanx-aoe.browser.test.js(RENDER_E2E=1).
| Branch (pool) | Official | 0.1.0 | Now |
|---|---|---|---|
| 阵法术师 (薄绿, 蜜蜡, 卡涅利安, 圣聆初雪) | every enemy in range | bolt + 1.1 splash | all in range, instant |
| 轰击术师 (协律, 阿罗玛) | every enemy on the line | bolt + 1.1 splash | all on the line, instant |
| 扩散术师 (格雷伊, 莫斯提马, 莱恩哈特, 夕, 寒檀) | 1.1 splash (格雷伊 1.0) | 1.1 | 1.1, 格雷伊 1.0 (§21.19) |
21.19 Integration follow-ups (feedback1) — render/interp.js, render/app.js, battle/runner.js, screens/game.js, sim/Battle.js foesInRadius, sim/professions.js, sim/constants.js CHAIN_RADIUS, kits tier1–6, content/{enemies,tokens,generic}.js, content/items/battle.js, content/bonds/core.js, ui/ticker.js, match/Match.js, match/bot.js
The last review of §21.4's client fix, the cross-workstream notes and the last review rounds were checked on the merged branch; the worthwhile items with a primary source are done here (the rest: §21.20):
- Form fx in the render engine (review of the final §21.4 fix, major):
render/interp.js isCosmeticEventcounted every 'fx' as cosmetic, soprocessEventsdropped one stamped > 1.5 game s behind the render clock and a full queue shed it — after a main-thread stall over ≈ 0.78 s real at 2×, on a watched replica after a hidden tab (catch-up frames of 8 game s), after Chrome's intensive throttling, and for the game screen's replayed early buffer (stamped 0). Reproduced in headless Chrome: 转译基底·α endeddead:null:B_Die— report #5's look. Now a form fx is state for the queue too; one handed out late switches the model without its telegraph, with its closing clip shortened by the lateness and without a change clip that would already have ended. The runner compacts an overflowing hidden-tab backlog (compactHeld) instead of re-entering the view, flushes the backlog and a last snapshot of a battle that ended while the tab was hidden, and the game screen buffers the frames of a re-sent field meta for the field on screen until it re-enters it. The browser test stalls the main thread (blocking 1 s long tasks; the oldsleepleft the renderer's ticker running). - Area damage and 隐匿 (PRTS 作战机制 §AOE伤害判定 "AOE的判定是对攻击范围内的每个可以被选中的敌人进行判定"; 隐匿 "隐匿状态下的单位一般无法被敌方的索敌机制和Buff选择器选中为目标"): an operator's radius area effects — profession splash, aftershocks, skill circles and zones, counting conditions — use
Battle.foesInRadius(no untargetable, no unblocked unrevealed 隐匿 enemy); enemy-side auras / heals and physical collisions keepenemiesInRadius. The 逐火 embers and every 隐匿 enemy take less splash than in 0.1.0 (officially they took none). - Splash radii (PRTS 溅射半径一览, 特性 / 特殊): 投掷手 0.9 (was 1.0), 链术师 jumps 1.7 (1.8;
CHAIN_RADIUS), 格雷伊 1.0 (1.1); 迷迭香 S2 末梢阻断 1.5 (the table's skill row; was ×1.3 ASSUMED). - 阿罗玛's 非首次标记 (PRTS 阿罗玛 天赋 备注 "…自身离场时移除自身已施加的标记,不同阿罗玛之间的非首次标记通用"): one mark per enemy (
aroma:bubbled, the setter as source) shared by every 阿罗玛, removed when its setter leaves (two used to trigger on one enemy; E3 made it show). - 奥术法阵 on the field (PRTS 盟约记录 备注, the same as 源石溶剂's "…敌人类我方单位也会获得此装备的“造成伤害时使目标失去特殊能力5秒”效果"): 炎佑's damage silences its target for 5 s while a carrier is on the field [ASSUMED: it ends with its carriers].
- 重生 cooldowns (PRTS 特殊机制 §重生 "重生结束时,重置自身的通用技能与当前形态的技能冷却为初始冷却"): every ability with a cooldown restarts from its initial cooldown when a 重生 ends (
rebirthCooldowns:reborn(), the husk's 1 s 重生, the statue's instant one). - 高准度伦蒂尼姆城防自行炮 (悬赏 target; PRTS 技能 "以攻击范围内生命上限最高的我方单位为中心,生成9格的炮击区域,6s内每0.5s对炮击区域中生命比例最高的我方单位进行攻击…(最多进行10次攻击)", 备注 失衡 / 晕眩 / 冻结 / 浮空 / 沉睡免疫 during the animation): the lock is in its range (it was any ally), the area the 3×3 tiles, 10 shots 0.5 s apart (were 3, 1 s apart ASSUMED), 失衡免疫 + those immunities over the bombardment [ASSUMED: the animation's length].
- 琳琅诗怀雅 S3's coins (PRTS 备注 "前方范围2-4内的地面敌方单位与自身阻挡的所有单位"): range_table 2-4 in front plus the units she blocks (her 1-1 attack range before).
- 阿戈尔's devour (PRTS 盟约记录 "造成5000点物理流失", 修正 "【吞噬】的物理流失来源为被付与目标自身"; PRTS 作战机制: a 物理流失 "会受到目标当前防御力…影响而相应衰减"): 5000 less the target's DEF, still a 流失 (no shields, dodge or multipliers), the kill to the marker (DEF-free ASSUMED in 0.1.0).
- The ticker by priority (research 06 §9.2, WJ's note): §21.10.
- AI 托管 and temp (the promise "你开 AI 托管时,它不会销毁你的道具"): on a human seat under AI 托管 an item left in temp with a full hand gets a hand slot (a bench operator sold) instead of being destroyed; only a hand of nothing but items still drops it, as the temp deadline would.
- The bot (§21.6): reconciled with the merged rules as listed there.
- 0.1.0 bug found in the merge: a 联防 battle timed out during 隐德来希 S3 reported its 心烛 as leaks, so the server refused the honest client's result (fullmatch-coop3 seed 116) — candles carry
mem.noLeakandBattle._timeoutskips them. - Checked, no change: 斩胄之剑 / 破胄之锤 stay unblockable while grounded (their PRTS 天赋 lists 不可阻挡); the database's lpr-0 enemies (简饲源石虫, 节日气球, leader parts, tokens) still cost 0 LP in a leader round (the official
lifePointReduce); 鸭爵 swaps skip bounty-tagged spawns; 转译基底's bounty coin is paid once. - After the independent QA of the merged branch (residuals, each checked against its source first):
- WB's 22-match follow-up merged (QA major): on the 11-match model 16 of the 58 official bounty drafts of the 22 matches could not be generated (two series-20 cards in an R3 set, named bosses apart at R9, an R11 draft without a giant); with the follow-up every one is reachable — §21.2.
- A status of the knocking-out hit (§21.4): 妮芙 S2's fear (and a 叙拉古 6 proc, 铃兰's slow…) landed after the 重生's cleanse — the same hit runs
if (target.alive) applyStatusafter the kill, and the husk is still alive — so an ember walked unblockable + 隐匿 for ≈ 3 s more. A unit both 无敌 and 无法选中 now takes no status from the other side (PRTS 无敌 "无法被不同阵营选中";Battle.applyStatus). - 余 S2 and 自缚 (in 0.1.0 already): 厚礼上宾 teleported a 守墓石像 in its 转换模式 onto 余's tile. PRTS 余 S2 备注 "处于消失状态的/持有自缚的单位不视为可达目标"; the statue's PRTS 天赋 lists 自缚. Flag
selfBound(the statue, the 自缚 leaders, the 胄 parts' anchor) besidenoMove— notnoMoveitself, which 束缚 sets too. - 信仰搅拌机 S2 (in 0.1.0 already, ASSUMED then): with fewer than 30 bullets the lethal hit went through. PRTS 备注 "弹药量不足时仍可抵挡致命伤害,此时将消耗所有剩余弹药并退出技能状态" and "自身持有不死时此效果不会生效": it blocks, spends what is left and ends; while 坚固维式重锤's 不死 window holds her (
unit.mem.undyingUntil) it steps aside (骑士戒律's earlier 不死 already did). - Late form fx after a hidden tab or a catch-up (§21.4's client half): the runner sent the hidden-tab backlog and a catch-up frame's events stamped with the frame's game time, so the render engine saw a 16 s old form fx as on time and replayed the 2 s change clip on a walking 转译基底·α (the final form was right). Such a step now drains its events every
EV_SLICE(15) ticks and each batch keeps its own game time (battle/runner.js stepEntry / drainSlices / heldAt); render/app.js then skips the change clip as for a stall. - The bot's bench shed (§21.6): the buy loop's pre-emptive shed (one hand slot left) sold the single it had just bought for 3–5 back for 1 — the buy picks by lineup gain, the shed by piece value (co-op 绝境, 2 AI 托管 + 2 bots, seeds 21–26: 29 same-prep buy → sell, all from that line, vs 7 in 0.1.0). It now never sells a piece that came this prep (
sellWeakestHand keepFresh); 3 remain, from other paths (a reward pick's room,arrange,sellJunk). - Checked, not changed: the QA's other notes are on the user-facing list (wording) or the release packaging (the 灼热 / 炽焰源石虫 models come from the gitignored local extraction:
public/assets/local/spine/enemy/enemy_1305_mhslim{,_2}+data/local-assets.json).
- Tests:
test/sim/feedback1-followups.test.js,test/sim/feedback1-integrate.test.js,test/sim/feedback1-residuals.test.js,test/sim/feedback1-transform.test.js,test/match/feedback1-runner-forms.test.js,test/render/interp.test.js,test/ui/bosshit-ticker.test.js,test/match/feedback1-meta.test.js,test/match/bot-smarter.test.js,test/content/{enemies_bosses,bonds_core,kits_alt_t4,kits_alt_t6}.test.js.
21.20 Settled decisions and what stays ASSUMED
Settled by the user:
| Decision | Settled rule | One-line flip |
|---|---|---|
| A tactician re-oriented in place (§21.3) | a range-bound summon still inside the new range stays; one outside goes back to its stack (user-confirmed 2026-10-03; PRTS only says a MOVED owner resets every summon) | PlayerState._liftOutOfRange lifting every range-bound summon of a re-oriented owner |
| Bounty drafts (§21.2) | the official structure of each round from the user's 66 screenshots of 22 matches (R3 fixed sets, R9 boss groups, R11 seen 7-card lists; no multi-round card in a draft) | add a newly seen set / group / list to tools/build-data.mjs BOUNTY_INITIAL_SETS / BOUNTY_BOSS_GROUPS / BOUNTY_HUNTER_GROUPS and rebuild |
| 机密商店 / 战术决策 repeats (§21.2) | the same card can be offered twice, as officially (the user, 2026-10-03: "机密商店按官方改成可以重复吧", "战术决策也按官方改成可以重复吧"); R11 机密商店 = two VI, ≥ 1 V, ≥ 1 盟约之币 | choices.json shopDraft / tacticDraft removed (choices.js falls back to the previous draws, still with replacement) |
| 坚固维式重锤's scope (§21.21) | one 不死 lock per DEPLOYMENT: the carrier's own redeploy (after a knock-out, a 突袭 retreat + redeploy) re-arms it and ends a window still running (the user, 2026-10-03: "每次部署一次"; the text says only 首次) | content/items/battle.js hammerAcquire: drop its deploy handler (one lock per battle) |
| Multi-round bounties (§20.6, unchanged) | last two battles — now only 教鞭's card | server/match/choices.js MULTI_ROUND_BOUNTY_BATTLES = null |
| §20.9's decisions | unchanged (start-deploy summons, 凯瑟琳's hand card, every blocker hits, the collider reach, the elite to the board, the fixed leader pool, the watched bond strip) | as §20.9 |
Official since this release (each with its source in its section): 突变细胞 not consumed, its carrier destroyed (tile freed) and the new operator gained into the 整备区 — official footage, pointed out in PR #2 (§21.1); the 深水区 refuses deployment and a tactical point stays in range (§21.3); the knock-out forms and kill accounting (§21.4); 圆仔 / 鸭爵 unblockable, +300 % speed, swaps in leader rounds, lpr 1 (§21.5); 标准's 本局禁用 bonds (§21.7); 维娜's lions on every free tile, 玛恩纳 S3 可对空 and his per-kill rule (§21.8); 静态刚体 (§21.9); the per-round BOSS_HIT share and the priority strip (§21.10, §21.19); the 变形同构体 member (§21.11); 源石溶剂 as damage, 信仰搅拌机's counters, the BUFF-damage enemies (§21.12); 野鬃's directional push and 琳琅's radial coins (§21.13); 暴鸰's drop (§21.14); diagonal routes (§21.15); 烛煌 S3's splash, 攻击距离 vs 信仰搅拌机 S3 (§21.16); medics heal while blocking (§21.17); 阵法术师 / 轰击术师 strike all in range (§21.18); area damage vs 隐匿, the splash radii, 阿罗玛's shared mark, 奥术法阵 on 炎佑, 重生 cooldowns, the 自行炮, 琳琅's coin range, 阿戈尔's 物理流失, no status on a 无敌 + 无法选中 unit, 余 S2 vs 自缚, 信仰搅拌机 S2 with too few bullets (§21.19); the R11 机密商店 composition and repeats in the 机密商店 and the 战术决策 (§21.2); M3茧甲's revive after every 不死 (§21.21); 起飞 = 对地规避 (§21.22); no ground blocking from a fenced tile (§21.23); a knocked-out operator keeps its tile and redeploys there, rule 3 included (§21.24).
Still ASSUMED (each in its section):
- §21.1: an elite merged this round keeps its copies' refresh count; a 拉普兰德 re-bought after a sale fires again; a bench 突变细胞 carrier transforms too; a 6阶 carrier becomes another 6阶; with the hand and temp both full the gained operator and an item without a slot follow the rules of every gain.
- §21.2: the weights inside a group; the unseen 10th R3 set and 险境 R6 drafting like R3; R9 groups by the matches they came in; R11 one of the 7 seen lists and the family weights (solo = co-op); the 机密商店 slots / weights and the earlier shops; the 战术决策 weights, independent draws and twin passives stacking; solo shows 3 of 6. Open for the user: each match's 确认本局信息 page (what decides the R9 group), more R11 悬赏决策 / 机密商店 / 战术决策, solo 绝境 / 终极 R3 / R9 / R11.
- §21.3: the range is the loadout grid (items and bonds do not widen it); the canoe in the deploy map only.
- §21.4: the translator immobile and damage-cancelled through its change; the 特战术师 form hits flyers (open for the user); the ember's 10 s after the 重生; 隐匿 back at once after a block; the 重生 whitelist; “余音” unmapped.
- §21.5: the 鸭爵 swap's 0–2 uniform count and pick, the 60–99 % window, ground only, elites eligible, per-half on pair fields, decided at battle start.
- §21.6: the bot's tuning constants and
CROWD. - §21.7: strongest rather than earliest for 奥术, an equal re-hit refreshing it; the 本局禁用 look; 整备 next to an owned white.
- §21.8: fence tiles as 可部署地面 for 黄金盟誓; the absolute −10 % per kill of 玛恩纳 S3.
- §21.9: a 静态刚体's 0.1 s 失衡硬直 not modelled; 守墓石像's carried 浮空 runs out; which pushes reach air units without a note.
- §21.10: ≥ for "超过"; the per-battle
bossDamagereset; a dropped line not replayed; the flow notices at priority 25; no begin delay. - §21.11: operator talents reading the record's bonds for "【X】干员" (a 变形同构体 wearer does not count there).
- §21.12: the drain's 受击回复 SP; the chimera aura 无来源; a target-less counter = one bullet; the 炎佑 tick ending with the carriers; the drowning / bleed cadences; 心烛 and 扎罗 as 流失.
- §21.14: the alias tint; 暴鸰's cast details.
- §21.15: tile-centre movement along
next[]. - §21.16: the 技能范围 areas on the card, extra content tiles not drawn, the 全场 / 6 × 7 thresholds, the bonus on a dodged hit.
- §21.18: no target cap; damage at the attack instant; the beam look.
- §21.19: the 奥术法阵 rider ending with its carriers; the 自行炮's immunity window = its bombardment.
- §21.21: a 突袭 retreat + redeploy is a deployment (it re-arms the lock); the 阿戈尔 devour and the carrier's own 源石溶剂 spend the lock; kits' own savers before the item's 不死; M3茧甲 before 埃芒加德; a running window ends with its deployment; a free pick or grant is no 购买; a 谢拉格 already priced ≤ 1 still counts as the round's first.
- §21.22: a ground enemy's shot in flight when 蒂比 takes off lands on nothing; ground-enemy auras still apply; 流失 DoTs keep ticking.
- §21.23: air blocking (blockFly) on a fenced tile stays; the gate reads ground passability.
- §21.24: the body goes home once, at the knock-out, only when its home is free; 初始位置 = the board pieces of every player on the field; 歌利亚头盔 ignores bodies; a withdrawn operator leaves no body.
Not done (noted, no primary source or out of scope): whether 华法琳 S2's 流失 can kill an ally (§21.17); 伊利昂的木驮兽's Full skin (§21.14); the render-demo recording public/dev/recordings/normal-a1m04.json still shows 战场#04's old L-shaped lower route (display only); M3茧甲's "同名装备的效果不可叠加" (two copies give two revives, a + b three) and two own 坚固维式重锤 giving a 16 s lock; the M3茧甲 / 埃芒加德 revive in PRTS's form (退场, then a 0-time / 0-cost redeploy) and a status icon for the 不死 window (§21.21); 深巡 S2's nearest-first order (§21.23); 【盲信之誓】 hurting once per chain (PRTS "多条锁链的伤害不叠加") and the “帝国的甲胄” barrage's timing (§21.22's audit).
21.21 坚固维式重锤's lock, once per deployment; 休露丝's discount (F1, F2) — content/items/battle.js hammerAcquire / PRIO_REVIVE / PRIO_RESPAWN, content/bands/battle.js PRIO_BAND_REVIVE, content/bands/meta.js (休露丝), match/PlayerState.js buy / pickReward
- F1 seen: "维多利亚坚固锤子的锁血没生效". Official (act2autochess eff_acarm043 / eff_acgarm043): "首次受到致命伤害时生命值不低于1,持续8秒",
undeadable_duration8 (the latest upstream data agrees; PRTS still prints the act1 15 s); 异常效果 不死 "重设常规生命值时不会使其低于1". - Measured on 0.1.0, the lock works: every HP loss of a unit without a boss pool fires
fatal(hits of every type, element bursts, 无来源 damage,loseHp流失) — on every visible chess against scaled real enemies, in forced-hammer bot matches and in the browser's local battle. - Why it looked missing: it was once per battle — the hammer state lives on the unit, which a redeploy reuses, so a carrier knocked out after its lock came back with none (forced-hammer co-op matches, 3 绝境 + 4 困难: 27 of 382 carrier knock-outs); the 阿戈尔 battle-start devour (a lethal hit on the operator in front: 10 of 179 locks on 绝境) and the carrier's own 源石溶剂 spend it; with an M3茧甲 equipped first, its revive (same priority, registered first) took the first lethal hit.
- Now: once per deployment — the user's first-hand memory of the official mode (2026-10-03, "每次部署一次"; the text says only 首次 where M3茧甲 says 一场战斗 and 拉特兰桥夹 每次部署): the carrier's own non-initial
deploy(the redeploy after a knock-out, a 突袭 retreat + redeploy) re-arms the lock and ends a window still running. Thefatalorder is kits' own savers (10 … −60) → items' 不死PRIO_REVIVE−100 → items' 复活 (M3茧甲)PRIO_RESPAWN−101 → 埃芒加德 −110: PRTS (M3茧甲 / 埃芒加德 / 阿戈尔 备注) "“复活”的实现方式为:受益者因移动之外的原因退场时下次部署的再部署时间和费用归零" — a revive acts on a knock-out, which a 不死 prevents. No status icon shows the 8-s window (the client has none for 不死; a one-shot fx marks the lock). - F2 seen: a 谢拉格 taken from the promotion reward used up 休露丝's "每回合购买的首名<谢拉格>干员消耗资金为1". Not real: only
g.buydispatchesonBuy, where every 购买 counter lives (休露丝, 玛恩纳, 升华, 整备); the promotion reward, 寻呼模块's pick and the 拟态物质 / 紧急调度券 / 简易通讯机 grants never touch it (pinned through real intents; METAonBuy). Buying the copy that completes a merge uses it, as it should. - Tests:
test/content/feedback1f-hammer.test.js(per deployment, a retreat + redeploy, the devour, the M3茧甲 order),test/match/feedback1f-sciurus.test.js.
21.22 蒂比's 起飞 is 对地规避 (F3) — sim/targeting.js evadesGround / canTargetAlly, sim/damage.js liftoffEvades (DamageInfo ignoreSelect), Battle.applyStatus / _blockerFor, buffs.js (liftoff), kits/tier2.js LIFTOFF_FLAGS, kits/tier5.js (隐德来希), content/enemies.js, content/bosses.js
- Seen: after 蒂比 took off, enemies still hit her. Real: 起飞 was
unit.ground = false, which freed her from melee enemies only; ranged ground enemies kept shooting her and their areas and statuses kept landing (46 of 241 enemy types hit her while airborne; the client replica the same). - Official: ba.liftoff "不阻挡地面敌人且不会被地面敌人攻击,可以阻挡飞行敌人"; PRTS 术语释义: 起飞 includes 对地规避, a 无法选择 effect only selectors whose 行动方式 is ground respect, and she keeps a ground 行动方式; PRTS 异常效果: direct picks and abilities that "无视无法选择" are not stopped (these effects act "仅在选择时"); PRTS 作战机制: an AoE judges only selectable units.
- Now: 起飞 is the skill flag
liftoff(S1 and S2, normal and elite). No ground enemy (notUnit.isFlying) selects her — normal attacks, ability picks, 周围四格 additions, chains, bounces, shells, the “帝国的甲胄” barrage, 鼠王's 沙狱, 烹泉's blast and steam, ground zones (集团军重型火炮's 燃烧区域 is "不可对空", 迷彩 alone exempted) and their statuses; the pipeline refuses a ground enemy's damage on her before thehithook, so a refused hit neither sets off her S2 nor restarts 片场工作指南. She blocks flyers only; flyers, 近地悬浮 and 浮空 enemies still attack her with no 对空 check; she stays a ground unit (污染秽蚀 at the low-ground 50/s; 隐德来希 S2's 血镰 on her cuts air). - Still reaches her (no selection — DamageInfo /
applyStatusignoreSelect, or 无来源): the "无视无法选择" abilities (【污染秽蚀】, 【盲信之誓】, 萨卡兹悖谬暴虐兵长's 暴击 splash — her 片场工作指南 may dodge it); 碎铳之簧's counter on its attacker; the 刺胄之弹 / 剑 / 锤 blasts (flying units); the ticks of a debuff a ground enemy put on her before take-off (出血, 沙狱, burning, 淤困, 【自然涌动】 — their mods stay); sourceless damage (毒雾, the chimera aura, 源石溶剂). - ASSUMED: §21.20. Audit: 蒂比 is the mode's only 起飞 / 对地规避 operator; outside F3, ranged enemies still pick invulnerable allies (锏 S3, 泥岩 S3) and enemy splash still hits 隐匿 allies (§19.1).
- Tests:
test/sim/feedback1f-liftoff.test.js(13), opt-intest/render/feedback1f-liftoff.browser.test.js(RENDER_E2E=1),kits_t1t2/kits_alt_t2.
21.23 深巡 on a fenced tile; fenced units block no ground enemy (F4) — sim/Battle.js _blockerFor
- Seen: "干员深巡放在围栏围着的地块里不会主动攻击". Official: the fenced tiles are 围墙 (
tile_fence_bound, "可放置近战单位,敌方无法进入") and 围栏 (tile_fence, "不可以通行") — LOW, buildable ALL, passable to flyers only; PRTS 围墙 / 围栏 地形机制 "部署在其中的单位,若当前阻挡类型为'地面阻挡'则无法阻挡敌人"; 深巡 is a 哨戒铁卫 ("能够阻挡三个敌人,可以进行远程攻击", range 2-2); defender skills fire "受到伤害时" (PRTS 卫戍协议/帮助). - Not reproduced: from a fenced tile she attacks every targetable enemy on her range tiles — every deployable fenced tile of the 7 stages that have them, every direction facing walkable ground, normal and elite; the Final Assault halves and 联防; a real Match; the browser's battle (7 attacks, 1977 damage). Targeting reads range tiles only (no path or line-of-sight condition).
- Likely what players saw (official): in some directions her 3-tile line faces only fence or forbidden tiles; 阻隔工事 crates hold some fenced tiles from the start (act1 m01: 3 of each 6-tile block; act1 m04, act2 m03); nothing hits her there, so her skill rarely fires; an own-tile melee unit never attacks from a fenced tile.
- Fixed (found while checking F4): a push or pull stops an enemy at the fence edge, 0.5 from the fenced unit — inside the 0.7071 ground radius — and 0.1.0 let that unit block it (薄绿 S2 on a fenced tile held the enemies she dragged).
_blockerFornow refuses ground blocking from a tile ground units cannot pass (on the stages exactly thebtiles). - ASSUMED: §21.20. Tests:
test/sim/feedback1f-fence.test.js; PLAYING §10 tip.
21.24 Knocked-out operators keep their tile (F5) — sim/Battle.js downOn / restTile / _layBody / isReservedTile / redeploy / _checkRedeploys / _downState / snapshot, sim/units.js body, content/bonds/addon/battle.js (突袭, 不屈), content/bonds/core.js (阿戈尔), kits tier1 / 3 / 4 free-tile helpers, render/interp.js, render/units.js setDown
- Seen: "突袭盟约的干员会移动到濒死干员的尸体上". Real: a knocked-out operator left the occupancy map at once and only its home tile stayed reserved — a 突袭 member that fell after its jump lay on its landing tile, which the next jump picked; a timed redeploy came back on its home, where a teammate that had jumped there lay (96 real battles: 6 placements on a body; the shared boss field mirrored: 38).
- Official (PRTS 卫戍协议/帮助 §作战阶段 单位部署): "干员退场后…原地留下一个“倒地干员”…满足再部署条件时,移除场上的该倒地干员并自动部署至该位置"; "倒地干员所在地块视为可部署,但所有我方单位在此处的部署行为将被阻止"; "若干员被击倒的位置为其他干员或召唤物的初始位置,则在被击倒后,尝试返回其自身的初始位置". The 突袭 text "再部署至一名地面敌人周围" adds no tile rule.
- Now: a knocked-out operator lies on
body— where it fell, or its own home when it fell on another board piece's home and its home is free (x / y / tile keep where it fell for the 被击倒时 handlers). Every redeploy comes back there (timed, 不屈, 阿戈尔); a withdrawn operator still comes back home.downOn:_deploy(redeploys, the 突袭 landing, summons),spawnDeviceandrelocaterefuse the body tile for every ally;isReservedTilereports it, so the 突袭 landing, tactical points, summon / device pickers and 维娜's lions skip it. b.snapdowncarries the tile (§8.2) and the client draws the body there; WAIT_TILE matches the_checkRedeployshold-back and is a safeguard. After the fix: 0 placements on a body in 526 jumps. Superseded: "later redeploys use the board tile" — an operator moved off its board tile (a 突袭 jump, 乌尔比安's anchor) now redeploys where it fell; research 01 §4.3 carries the PRTS quotes. - ASSUMED: §21.20. Tests:
test/sim/feedback1f-downed-tile.test.js,test/render/feedback1f-down-tile.test.js, opt-intest/render/feedback1f-down-tile.browser.test.js;downelem,bonds_addon,engine-requests,robustnessfollow the rule.
21.25 Hardening credited to pull requests (PR #2, PR #7) — match/effectsMeta.js dispatch / stillEquipped, tools/fetch-assets.mjs shrinkGuard, tools/assets/manifest.mjs droppedEntries
- The prep dispatcher's equipped items (pointed out in PR #2, whose 突变细胞 bench rule §21.1 now follows — official footage confirmed it —, its merge path excepted): step 5 iterated
holder.itemslive while handlers move and destroy pieces — normal 博士投影 destroys itself at the round start (the splice skipped the next item on that holder), 突变细胞 transforms its holder (the next item ran with a holder that was gone). It now walks a snapshot of the owned chess and of each holder's items and runs an item only while it is still equipped on its still-owned holder; a piece gained or equipped meanwhile waits for the next dispatch (effectsMeta header, META §2.2). No shipped item has a second hook on those events, so play is unchanged; a synthetic spy handler pins the rule (test/match/feedback1-dispatch-snapshot.test.js). - fetch-assets never shrinks the committed manifest by accident (pointed out in PR #7, whose
data/assets.json, regenerated on a machine where some downloads failed, was 42 audio entries short — all 42 still resolve upstream): a rebuilt manifest leaves out every entry whose files are missing on that machine. When it would lose an entry of the currentdata/assets.json, the run keeps the current file, prints the entries it would drop (also the report'sdroppedEntries/manifestWritten) and exits 1;--allow-shrink(or--prune) writes it (ASSETS.md;test/fetch-assets-shrink.test.js).