Files
ark-stronghold-protocol/docs/DESIGN.md
T

499 KiB
Raw Blame History

卫戍协议:盟约 — 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=CODE link, 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 dependency ws@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, global PIXI), 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-core driving the system Chrome (dev dependency, optional; puppeteer-core 25 is ESM-only and needs Node ≥ 22.12, so the suites load it with await 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 S at (9,10) and (12,10) (R1–3 only the lower one); protection objective E at (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.
  • Tile semantics come from data/stages.json legend: height (LOW/HIGH), buildable (ALL/MELEE/RANGED/NONE — the effective deploy type: the 深水区 tile_deepsea refuses deployment, NONE with the level's buildableType ALL 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, legend b: 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 — except a branch whose own trait reads "可以放置于远程位" (钩索师 歌蕾蒂娅 / 崖心, 推击手 见行者: chess.json placement 'all'), which may use the ranged tiles too (the remake's reading, §22.6); ranged chess on ALL/RANGED and on LOW ALL/MELEE tiles ("所有行动内远程干员可部署在近战位"). Tokens follow their own position; a summon marked ownerRange (狼群, 流形: "只能部署在召唤者攻击范围内") 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|LEFT chosen with the 4-direction deploy wheel after dropping it on a tile (also re-orientable in place); default RIGHT; 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); rangeExtend applies 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 col c→20−c with 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 (data hitArea: 巨型单位, 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 Battle receives 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_SCALE with MOVE_SCALE = 0.5 (in sim/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's maxPlayTime; also modes[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 passes 2 × value game 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 (bossOvertimeAfter 150 / bossOvertimeDrainPerSec 1 = official bossTurnHpReduceTime) 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.paused the running battle's field clock, the HUD deadline / overtimeAt and 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 as deadline (ms epoch) + serverNow for 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 (the model form it is in — an enemy's mode, §21.4, a 傀儡师's 替身 'doll', §22.11 — UnitInfo form).

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 for maxHp, def; the 卫戍 systems' "+X%" ATK / DEF / max HP (盟约, 策略, 装备, 机变 cards, per-layer 特质) are 直接乘算 and go into Σpct with the skills' "+X%" (sim/constants.js DIRECT_BONUS_STACKING 'add', content/support directMods; 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_MIN 20: PRTS 数值范围 "ATTACK_SPEED 默认下限 20", §20.3).
  • interval = bat × (1 + ΣbatPct) × 100 / aspd (blackboard base_attack_time is treated as batPct, e.g. −0.3 → 70%).
  • Changing maxHp keeps 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[] }

  • mods keys (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, and so do an enemy's area effects and buff auras unless the 隐匿 ally blocks that enemy, targeting.js areaSelectable/auraSelectable, §22.12; an enemy's 隐匿 returns only 3 s after a block ends, or after its PRTS page's "(解除阻挡N秒后恢复)" — stealthOffbuffs,targeting.js enemyStealthed, §22.8; 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 — and an enemy's splash and other area effects still hit it, ba.camou "无法躲避溅射类攻击", §22.12), 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 from others: 禁疗 summons and 孤立 units; an HP-regen attribute still applies — PRTS 安洁莉娜 备注), healFree (禁疗 that stops the unit's own heals too, shown as the status 'healFree': 史尔特尔's 余烬 sets it with noHeal — only an HP-regen attribute and her S3's "无视禁疗" start heal pass, §22.7), 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/neural element gauges (see damage), stealth, camou (迷彩, its own keys: 忍冬 S3 vulpis:camou until her next cast, 寒芒克洛丝 S1), reveal.
  • Immunities from enemy data (stunImmune, silenceImmune, sleepImmune, frozenImmune, levitateImmune) are honoured by applyStatus; a unit both invulnerable and untargetable (a 重生, a 永久无敌 leader part) takes no status from the other side at all (PRTS 无敌 "无法被不同阵营选中", §21.19). Flag selfBound = 自缚 (守墓石像's 转换模式, the 自缚 leaders) beside its noMove: 余 S2 never teleports such a unit (束缚 sets noMove only).
  • 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; the two 复活 revive in place and open a new deployment for the lock (§21.21); a running 坚固 window's hook sits at PRIO_UNDYING_HELD −99, before a 傀儡师's switch to its 替身 (−100, ahead of the lock — a 本体 holding 不死 does not switch, §22.11)
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 卫戍协议/帮助 "按从上到下>从左到右的顺序部署。优先部署干员,随后为召唤物"; aggroSeq ranks 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 (deploy with initial: true). Then operators carried into 联防 knocked out (carryState.down) are forced out (FORCED_EXIT, HP 0, §19.3). Then battleStart, 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 (and, since 0.1.2, one forced out by its own effects, §22.15) stays down on the tile it fell on — its own home when it fell on another board piece's home (that piece on the field or not) 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.snap down, §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 uses blockCnt capacity (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 (dmgType heal, 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 interval s if a valid target exists; targets = up to maxTargets (default 1) from unitsInGrid(rangeGrid) (a rangeAoe profile — 阵法术师, 轰击术师 — 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, trait text parsed by build-data into targetPriority), (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 — or within 3 s of a block's end (§22.8). 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.json abnormal, noHeal; §20.3; 史尔特尔 during 余烬, §22.7), 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 through e.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), and an enemy's area effects — splash, blasts, area skills, zones, jumps — and buff auras skip it unless it blocks that enemy, while 迷彩 does not protect from splash and the other area effects (targeting.js areaSelectable / auraSelectable, §22.12); 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); unblocked, they stand for each attack's clip (enemies.json attackAnim: its wind-up before the strike and the rest of the clip after it, shortened when the attacks come quicker than the clip; ATTACK_PAUSE = 0.35 s after the strike when no clip is known; a 「不停止移动」 attacker never stops; §22.14). 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); arts max(A × (1 − R'/100), 0.05·A) with R' = 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, not dmgTakenMul (脆弱 = "受到的物理、法术、真实伤害提升"). 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 with ceil(final) ≥ 300000 is cancelled — no HP or pool loss, no credit, no damage number, no damaged / kill (shared/constants.js BOSS_HIT_LIMIT, damage.js leaderHitCancelled, after every multiplier and the hit-step blocks, before HP shields; loseHp too — a part's 传递, a drone's death; the client's MAX_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), 损伤抵抗 = data epResistance (PRTS 元素; the 5 % floor of DMG_e, PRTS 游戏数据基础). An element fill on a target with no HP left is refused (damage.js hasHp: 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.js ELEMENT; 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 (sourcePlayerId for unite). LP accounting is done by the match, not the battle.
  • Results: damageDealt / a unit's dmg count only HP removed from the other side — a unit's own drain or 流失 and friendly damage are the target's taken only (§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 by the official rule, 14 / 28 in the data with 余 S2's deviation, §22.10), TAKE_DAMAGE (on taking damage — any damage, 无来源 included, never a 流失; every MANUAL 重装 skill, §21.12 — but the six of the deliberate deviation, §21.29: 深巡 / 雷蛇 S2, 号角 S2 / S3 and 灰毫 S1 / S2 are DEFAULT, rawRule keeping the official TAKE_DAMAGE; and 余 S2, SKILL_RANGE on its x-1 (§22.10)), 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_DECK bonds also count the hand; 绝技 counts elites on board; 调和 adds +1 to each core bond count that has a real member on the board (the views mark such an entry harmony: 1, §21.26), etc. (research 02).
  • 变形同构体: an operator wearing it together with an item that has a giveBondId is 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).
  • tier from thresholds; active = tier ≥ 1.
  • layers[bondId] persist all match; gains come from garrisons/bands/items/choices/bond effects; IN_BATTLE gains are collected from BattleResult.layerGains at SETTLE (disabled in unite/boss/hidden).
  • A bond holds at most BOND_LAYER_CAP = 999 layers (the client's MAX_GARRISON_STACK, §20.12): every writer — PlayerState.addLayers, the SETTLE merge, Battle.addLayers, the dev tools — adds layerGainRoom(before, n) = min(n, 999 − before); a gain at the cap adds 0 and dispatches nothing (no onLayers, no layerGain hook, 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 (every [holder, item] pair, taken before the first item runs) — 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; never one built around a bond the mode switches off — gd.bandBondIds, §21.26); 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; a 钩索师 / 推击手 on the ground tiles, as a blocker — §22.6; 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 onBattleStart event carries routes and side) 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_TRANSFER 1, 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 carry descRaw; 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.kick {seat, playerId} (host, before the match: another human leaves the room and gets room.closed {reason:'kicked'}; may join again; playerId = the player the host confirmed, a seat that changed hands meanwhile → BAD_TARGET) · 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'|'kicked'|'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.

Spectator seats (a remake feature — the official room has none; community report #26, owner's decision 2026-10-04; the spectator-seat subsection of §23): room.spectate {code} takes one of a co-op room's MAX_SPECTATORS (2) spectator seats, in its lobby or during its match; room.removeSpectator {playerId} (host) frees one (room.closed {reason:'kicked'} to it); room.state.spectators: [{playerId, name, connected}]. A spectator is never a player (not in seats, not counted for the start, never host, keeps no room alive — room.closed {empty} when the last human leaves), receives the room's broadcasts and watches like an eliminated player (Match.addSpectator: b.start watch / m.field, never m.private); it may only g.watch, leave, and store a room.loadout — anything else answers error SPECTATOR. Its seat is kept and given back on reconnect like a player's.

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,harmony? /* 调和's +1 is in count, §21.26 */}], 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.ev is carried as gt (the frame's t is 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, state 0 counting | 1 timer done, waiting for DP | 2 timer done, its tile taken — a safeguard, no ally deploys there (sim/constants.js DOWN_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 unit's model form from then on (an enemy's mode, a 傀儡师's 替身 §22.11) — 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 to g.unitStats (seq echoed): 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.js unitStatsEntry; 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 = the unit's current model form (an enemy's mode §21.4, a 傀儡师's 替身 §22.11; 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, harmony? /* 调和's +1 is in count, §21.26 */, 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, shop })  // animates camera; shop: false = the
                                             //   folded shop's prep camera (official shop-collapsed framing, §21.27)
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 — a failed or timed-out model is loaded again (bounded), and a view re-resolves its model and avatar when the asset manifest arrives late or the tab is shown again (§21.27). 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; 查看禁用盟约与干员 under the order list opens the briefing's bond rows, legend and banned operators again in a read-only dialog — one component, ui/matchInfo.js, §21.28), 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; its 本局信息 tab lists the briefing's disabled bonds and banned operators, §21.28); 机变 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); a strategy built around one carries a 本局禁用 tag and note in the strategy draft and stays selectable (bands.json bondIds, §21.26).

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; the heavy 2-player boss field (18 ops, ~130 enemies alive, test/sim/robustness.test.js) < 0.5 ms per tick on a development machine, the best of three runs of the same battle — 1.0 ms when process.env.CI is set (GitHub's shared windows runners measured 0.51–0.58 ms and failed the 0.5 ms bar on PR #10, #12, #14 and master's own push; a real regression still shows locally).

12. Implementation phases

  1. Foundation: build-data, fetch-assets/vendor, server platform (http/ws/lobby), client shell (screens up to room).
  2. Core: sim engine + harness; match/meta; render engine; game UI.
  3. Content: kits, bonds, garrisons, items, bands, enemies/bosses/devices, choices.
  4. 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 the art the web download does not include (some of it, most official HUD sprites among it, is on the public mirror too — §22.5) 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). The emotes and the guide pages below are on the public mirror too (§22.5): fetch-assets downloads them (data/assets.json ui['emoticon/<dir>/<picId>'], ui['guide/<key>']) and the client shows the local copy first, then that one (data.js artUrls); docs/DEPLOY.md §6 lists what still needs the client.

  • map/autochess/TX_autochessi_* — the real autochess board atlas (_D diffuse 2048² with floor tiles, gold-framed high-ground plates, REINFORCEMENTS bench pads, EQUIPMENTS pads, crates, gates; _N/_M/_E normal/mask/emission; common_D device icons; BG backdrop). 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/<dir>/<picId>.png — the 36 official battle emotes, 6 themes (shared/constants.js → EMOTE_THEMES / EMOTES, emoteArtPath; data/emotes.json); also downloaded from the mirror (§22.5).
  • guide/autochess_{home,shop,handbook}_N.png — the 19 official tutorial pages; stored squashed to 1024² — display at 16:9 (the mirror copies too, §22.5).
  • projectiles/* — battle projectile sprites.
  • spine/enemy/<id>/ — enemy models no web dump carries (灼热 / 炽焰源石虫, extract.py ENEMY_SPINES): manifest enemies[id].spineLocal (metadata from the committed tools/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's typeName by assetUrls.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 of headlessSliceMs (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 / speed real 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 a CreditPool that 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 gets b.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 as opts.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/b2 in 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 late b.progress / b.result of 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"), on g.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 to fields.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-less b.progress with 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; fitResult keeps the frame ≤ 60 KB: unit statistics, then leak mods, then — boss only — leak entries go first). Structural checks in isBattleResult; server/net.js routes b.* frames to the match (lobby routeGame).
  • S→C b.pool { hp, max, teamLp, acked: { [fieldId]: cumulative damage counted } } (boss rounds, ≤ 4 Hz; exact numbers, never rounded) — clients reconcile their LocalBossPool: displayed pool = server hp − (local cumulative damage − acked). Pools never hold less than 1 HP (BOSS_POOL_MIN_HP: SharedBossPool and LocalBossPool; 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 calls battle.forceEnd and reports; 'takeover': stop reporting, keep displaying.
  • m.public.fields[] gains progress: { killed, total, done }; m.public.combatMode; m.public.paused: boolean (always present; solo pause below).
  • b.snap / b.ev are not sent for combat in client mode. g.watch rules: 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) — the flat part raised by what the player's layer 特质 can add to that bond (their per-battle caps; an uncapped one leaves only the 999 room, §21.26) —, 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.json keeps 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.json like 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/extend public/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 by tools/local-extract) with their textures; animated gate/objective effects.
  • One camera model drives both layers: public/js/render/projection.js keeps its public API (worldToScreen, screenToWorld/tile picking, presets, transitions) but derives it from a three.js PerspectiveCamera initialised 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 follow viewKind(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|3d overrides. 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 → bossPrep in 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 (equipLevel 1, 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; a 局内数值 section between the skills and the modules (普通 / 精锐 toggle, 精锐 first) draws the detail card's own stats block, 特性 and 天赋 for the chosen skill / module (§22.16); 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).

导出 / 导入 a loadout: 导出 hands the current loadout out as a versioned envelope { kind: 'stronghold.loadout', v, exportedAt, count, entries } — copied through ui/clipboard.js copyText or downloaded as a file. 导入 accepts that envelope, the stored { v, entries } form and a bare { [chessId]: { skill, module } } map (serialised text too), and refuses another tool's kind, junk, an empty result and a newer v (parseImport — a newer payload is never mis-read). Both go through loadoutSync.applyLoadoutEntries → sanitizeEntries, so a preset from another build never sends the server an entry it would refuse, an import that keeps nothing changes nothing, and the result is reported as N imported / M not (ui/loadoutModel.js, screens/loadout.js).


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) and no scene holding the cache (a field view in battle mode holds it, §21.27). 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): arrow a bright tracer with a glowing head, a muzzle flash aimed at the target and a hit flare; bolt / orb / enemy a glowing ball with a particle trail (arts purple with a burst + shockwave, heal green with a sparkle, enemy red); bomb / lob a shell on a visible arc with a ground shadow, smoke and embers, exploding on landing; 阵法术师 / 轰击术师 strikes are instant beam lines to every victim (§21.18); droneBomb (暴鸰) a falling shell at 5 tiles/s (§21.14); drone a small cyan dart; boomerang (回环射手) a spinning boomerang with afterimages that flies out to the moving target at PROJECTILE_SPEEDS.boomerang and back to the thrower's current position at BOOMERANG_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 PROJ keeps a test-guarded copy: test/render/fxproj.test.js fails 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; heavy kinds — 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.id names 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 in id of many area effects. FX_KINDS entries flagged pt (bombard, bombardShell) always happen at (x, y) (their id is 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 by id), 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) after t (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), or LOCK_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's arrow also 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' (resolveProfile keeps it over the data's generic 'arrow') flies out at PROJECTILE_SPEEDS.boomerang = 15 tiles/s and hits on arrival, then flies back to the thrower's current position at BOOMERANG_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_offset 0.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 within dist_2 once — proj_atk_scale_1 within dist_1, else proj_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 is ownLeaks(local, server) = the larger of the local runner's state().leaks[own field] (§14) and m.public players[].pendingLp (§8.2; the authority's b.progress leaks, 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 settled m.private lands (its lp or stats.leaks changed; Match.flush sends it before the SETTLE m.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.public uniteLeft — 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 their pendingLp (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 through b.pool).
  • Temp row (#3, UI): while m.private temp holds 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 through view.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/*.json loaded 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 official eventTypeIcon (detailPanel.js garrisonTypeIconKey: crossed swords 作战能力, gold 整备能力, chain link 持续 / 单次叠加, spokes only for 特异化). Now: no cursor: wait / progress anywhere; a loading Button keeps 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 in m.public (choiceOverlay.js pickBusy); the 作战结束,等待队友完成作战 hourglass is still; the hexagon Spinner is 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 warmGameData downloads 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.js retries 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 within ENEMY_REACH 0.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, tileHover and 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 after touchend, 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 drag dropPoint hook, pieceDragOver, ui/gameLogic.js pickPieceAt / equipRetarget. UnitView.bounds() is a plain body rectangle again (tooltips and pieceScreenRect only).
  • 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 ChoiceOverlay wraps the pure ChoiceView. 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 确认选择, sends g.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.bandTurn says the same — the official data only has the whole BAND_CHECK step, 50 s, now informational) and m.public.deadline IS 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.js checks 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() lists down: [[id, respawnAt, respawnTime, state, row, col]] (§8.2; the tile since §21.24) for operators knocked out (reason 'killed', or FORCED_EXIT, entering 联防 knocked out, §19.3) or forced out by their own effects (余烬, 耀骑士临光 S2, 骑士戒律 + 竞技旗, 伊内丝 S3, a 商人 that cannot pay — every 退场 but the 突袭 retreat, §22.15), deployed at least once and with a finite respawn timer; state = sim/constants.js DOWN_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). The deploy event 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), 损伤抵抗 = data epResistance (EP_RESISTANCE; it used to read epDamageResistance as 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 gainSp refuses 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.snap elem (§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.js appends it to the unit's tuple; units.js draws 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 atlas textures.js hudRings keeps 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 without elem, 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's hit; 纯烬艾雅法拉 (who only heals) had her S3 impact (p_imp_gtshpbrnch_s, bank ON_ABILITY_HIT.attack.2) as hit in 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 banks maxSoundAllowed 2 — 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 plain attack / combat ability 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. agoat2 hit → p_imp_gtshpbrnch_n, nearl2 attack → 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 lazy unit.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.unitStats builds a Battle from the player's battle input after the onBattleStart meta (dispatched with ev.preview: true and no spawns — 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.js asks 120 ms after any m.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 trapShopChessDatas has hideInShop false for all 56 normal items, so an explicit sourced list (tools/build-data.mjs SHOP_EXCLUDED_ITEMS → items.json shopExcluded / shopExcludedBy) marks the effect-only items: 战栗 / 坚固 / 加速 / 灼燃维式重锤 (the 维多利亚 bond every 25 layers, 洛洛's 定制品) and 突变细胞 (the 昆图斯 strategy 不稳定要素; user playtest #4 first-hand). sim/simdata.js isShopItem is 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 as pool_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; new ai.js enemyAttack hooks profile.deferHit — content resolves the hit in its attack handler — and profile.shot, the 'atk' kind: 'mortar' draws no projectile; fx bombardShell); 假想敌:黑云 grabs within range_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.json sp.maxSp (3; new sp from 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 placeable to tokenDisplayType DEFAULT 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 fx yanyouFlame {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 radius r following the target — which burns dur + 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.isFlying is the one air-unit test: motion FLY, or an enemy flag float (近地悬浮) or levitate (浮空); pathing, displacement tiles and 诱导 read motion. canHitFly / groundOnly, splash, chain, the kits' "地面敌人" filters, ground terrain and traps skip air units; 失衡免疫 is the flag noDisplace; 静态刚体 (every air unit of the mode except “炎佑”, plus 昆图斯) is data staticBody and 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 form crawl switches the view to the skeleton's crawl clips (*_02, after its Change clip) — render/units.js FORMS, spine.js setForm, kept on the unit info for a view built later (a view built mid-battle starts crawling: UnitInfo form, §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's HOVER_KEYS; fieldModel counts 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.start deploys operators row-major, then summons; aggroSeq ranks every start-of-battle summon after all operators (deploySeq stays the deployment identity). An enemy's own rule e.profile.canTarget filters 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 flag camou, 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 (stun releases 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 position MELEE: 要塞 / 领主 / 哨戒铁卫 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 -1 as 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) — and DEFAULT again since §21.29, a deliberate deviation the owner decided (with 灰毫 S1).
  • 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 in kits_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 uniteBattleOpts dropped every operator that unitsEnd reported alive: 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 the SP_COMBAT=server fallback alike.
  • Official (PRTS 卫戍协议/帮助 §联防阶段): "部署完成后,将对应单位的生命比例、技力修改至与上一阶段结束时相同(召唤物仅修改技力,上一阶段为退场状态的干员强制退场)".
  • Now: the operator is fielded with carryState: { down: true } (§5.1); its summons are fielded as the board has them. Battle.start deploys everyone (the initial deploy hook fires), then before battleStart sets its HP to 0 (the end-of-phase ratio, as kill()) and withdraws it with removal reason FORCED_EXIT = 'forcedExit'. Battle.isDown accepts that reason, so it is in b.snap down and fieldMeta with 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: no kill hook, no death with reason 'killed', no deaths count — 深海 / 不屈 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 / tech per operator (research 09 §3 HelpBattleInfo), 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 before battleStart; 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; unitsEnd lists operators only — carrying their SP would extend the client-reported result, spec.js digests and fields.js validation).
  • 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.js is 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 persistent noMove + unblockable buff 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_2 the prefabKey enemy_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 in test/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 rate elemTakenMul, so 炎佑's 20 % sped up fills instead of raising bursts); 元素抗性 reduces 'elemental' with the 5 % floor (expectedFinal mirrors it); bursts carry DamageInfo sourceless — no damage-dealt multiplier or penetration of the filler, and the hit / damaged / fatal hooks see source: null plus credit (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 read source || 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 — applyElement refuses a target with no HP left, so no rider in a killing blow's damaged hook can burst a corpse (the content copies in tier5 / tier6 / items / tokens now import it); 'elemental' damage skips dmgTakenMul (脆弱 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 an elementHit multiplier at priority 20 like every operator-side "受到的元素损伤±", before a 损伤屏障 (it was the elemTakenMul mod, 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 damaged hooks run before battle.kill, while the target is alive at 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, applyElement refuses it (§19.5).
  • 塑心 S2 (integration): PRTS "受影响的干员即将造成伤害时,因该效果造成的凋亡损伤生效于当次触发的伤害之前,该造成的凋亡损伤的来源始终为塑心" — the rider is a late hit handler, 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 _stageDevices minus 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's canPlace / 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). Now buildDeployMap(stage, { field }) takes 'normal' | 'bossL' | 'bossR' (board row = boss-field row + BOARD_ROWS_ABOVE_BOSS 7; the right-hand player mirrored col c → 20 − c, research 09 §1.2 ConvertChessPositionInfoToBossMap); Match.bossGroupOf is the one pairing lookup (deployFieldOf, nextEnemiesFor, the bot); Match.startRound plans the round's enemies before PlayerState.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's gameLogic.deployFieldOf feeds placementContext({ 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.js now hands it the bossPrep camera, which it maps through gameLogic.fieldTile on the own board's layout (integration QA: it drew the normal field's walls under legal highlights). Limitations: Match.unitStats previews 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 are max(.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), and presetCamera('prep' | 'bossPrep', …, { hud }) runs clearHud on 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 kept the camera — superseded by §21.27 (public issue #5): folding the shop moves the own prep board to the official shop-collapsed camera (left_prepare_camera_param, presetCamera shop: false) clear of the folded shop's band (hudBands(kind, size, { shop: false }): the tab and the corner buttons' hit areas), and unfolding back to the shop camera. 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.price is 2, and the handler set the price to bb.price ("blackboards are authoritative").
  • Official: two chess carry this 特质 (effectType SERVER_CHESS_PRICE) and both texts say 购买价格为1 — 至简 (Ⅲ, tier price 3, price 2) and 红豆 (Ⅰ, tier price 2, price 1, hidden in 下半). Only "bb.price is 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)), and EffectDispatcher.dispatch runs the priced chess's own 特质 first on onPrice (before globals / band / bonds), so 远见's discount and strategy caps act on the lowered price: 至简 stayed at 1 with 远见 150 (a floor of 1 then assumed for 远见; since 0.1.3 the −1 has no floor but 0, so 至简 drops to 0 — owner's decision 2026-10-04). The shop card keeps basePrice 3, 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 takes 至简 to 0 since 0.1.3).

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, and token_10041_cathy_catsld the 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); startRound tops 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): releaseSkillSummon adds 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: a deploy hook 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, key cathy: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 = true drives the sim and the card hint (PLAYING §4 and SIM.md follow; test/ui/playtest6_summons.test.js checks). 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: dropping token_10041_cathy_catsld from placeable in tools/build-data.mjs only 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-in test/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 / -1 sentinel. SKILL_RANGE: 13 skills (26 records with the elites; 14 / 28 in the data since 余 S2's deviation, §22.10), 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) check skill.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 — partly reverted by §21.29: by the owner's deliberate deviation (2026-10-03, community feedback) 深巡 / 雷蛇 S2, 号角 S2 / S3 and 灰毫 S1 / S2 cast with an enemy in range (DEFAULT) again.
  • Integration follow-up (WE review round 2): the AUTO DP skills 焰尾 S1 迅敏直觉 and 凛御银灰 S1 周旋的谋略 ("立即获得…部署费用") are SP_FULL like 伺夜 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 + hpAtMost 0.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), through battle.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_siracusa 32 + 0.4 × layers s). 忍冬 S3 "进入迷彩状态,直至下一次开启技能" was the generic stealth under the key stealth, which made 伪装服's timed 隐匿 permanent (or stripped it) and counted as 隐匿 for 叙拉古 / 家族徽章. Now a separate camou flag 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 skillType counts as MANUAL.
  • Tests: test/sim/playtest6-skills.test.js, kit tests (incl. kits_alt_t4 / kits_alt_t5 for 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 (深巡 / 雷蛇 S2, 号角 S2 / S3, 灰毫 S1 / S2 back to DEFAULT, §21.29; 余 S2 now SKILL_RANGE on its x-1, §22.10) 重装 row (§21.29: a deliberate deviation for six skills)
薄绿 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.blockedTargets without the melee gate; sortEnemyTargets blocked-first for every attacker; the skills' DEFAULT trigger follows); targeting.js meleeUnit is 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: gate Battle.blockedTargets and sortEnemyTargets' blocked-first on a melee position again (the v2.4 meleeUnit).
  • ASPD floor: 沼泽 −50 + 寒霜 −50 took ASPD to 10 (塞雷娅 swung every 12 s); officially "ATTACK_SPEED 默认下限 20" (PRTS 数值范围; 游戏数据基础 "实际被限制了下限为20") — ASPD_MIN 20, 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, displace the 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) gives noHeal, 孤立 also isolated; Battle.allySelectable / alliesFor back 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 through dealDamage, 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, PollutedRangedAtk range_radius 2.2, atk_scale implicit 100 %, Empty.attack@chuang_atk_scale 2, 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.json modelScale = product / 0.27 (125 keys); render/units.js scales 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-in test/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. _creditBoss subtracts 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's LocalBossPool (server hp − (cum − acked)) absorbed every later hit (less than half an ulp of cum), 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 final b.result, whose per-player damage is Math.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) and b.end cleared goes 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 Hz m.public.bossHp and 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 desc beat 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 carry descRaw; 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; bountyText rewrites "之后的 / 后续<@ba.vdown>每场</>作战" to "接下来<@ba.vup>两场作战</>" on the card and in the effects column, 还剩 N 场作战); choices.json keeps the official rounds 99 / multiRound. Flip: MULTI_ROUND_BOUNTY_BATTLES = null restores 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): uniteLeft per leaker = not spawned yet + alive + counted leaks — it falls on kills and rises on splits / summons (children inherit sourcePlayerId); the server takes it from b.progress left, the headless timeline [gt, killed, total, left] or the exact uniteSurvivors, clamps live values to uniteBillBounds; m.public players[] uniteLeft is uncapped and pendingLp = 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), and pushRewardOffer drops 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 own descRaw, else the data descRaw while its plain form equals the server's desc, 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 .uframe 12 and .spov 20), 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-in test/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: elementView rounded 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-in test/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).

  • 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 bossInfoDict bloodPoint / bloodPointNormal / bloodPointHard / bloodPointAbyss of the current data (the cache equals upstream ArknightsGameData master as of 2026-10-01; test/match/bosshp.test.js pins 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 ignores enemyScale ("领袖单位于服务器的生命值加成不受上述加成影响"). 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:
    1. 直接乘算: "+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/support directMods; 炎佑 ×1.5 and the char_attribute_mul 特质 stay multipliers (§5.2).
    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.
    3. Leader parts: 剑 / 锤 / 碎铳之簧 pass every damage they take 1:1 as 无来源 HP loss, credited to the attacker (PRTS "受到等量的无来源生命流失"; PART_TRANSFER 1, 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).
    4. 坚守 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).
    5. 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).
  • 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-in SIM_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.json promotion.eliteGoesTo), so _mergeChess put 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, transformChess fromKey / fromDir ASSUMED, 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 its equipPerChess (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 and stats.merges unchanged; onMerge carries area ('board' | 'hand' | 'temp'). Bots plan the elite like any owned unit.
  • Now (client): gameLogic.mergeTarget(priv, chessId) mirrors mergeTile; a shop or reward card armed for a merge-completing buy lights the target tile in gold (ShopBar onArm → screens/game.js highlight group mergeTile); the 可晋升 tag says 精锐干员将出现在作战区原位置 or 精锐干员将进入整备区; an elite card never merges (completesChessMerge refuses isGolden, mergeProgress reports 0 copies: no pips, tag or tile); render/promote.js promotionsOf recognises a merge in setPrep and the elite plays fx.promote (gold pillar, shockwave, hex ring, streaks from the other copies) instead of fx.deploy (view.debug.promotions).
  • Checks: audit.js checks every merge — the tile is mergeTile of 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 --check sweeps 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-in SP_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 — AddBondCount stores min(L + n, 999), per bond. MAX_BATTLE_DAMAGE = 300000 — in step mode (every boss battle outside training) AutoChessStepModeManager._OnBossEnemyTakeDamage runs modifier.Cancel() when ceil(value) ≥ 300000 on an IsBossEnemy unit; IsBossEnemy (disassembled, research 11 §2.3) = enemyId ∈ autoChessData.bossInfoDict[*].enemyId = data/bosses.json enemyKey. [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 duplicate sim/constants.js BOND_LAYER_CAP / layerRoom was removed in the merge). Every writer uses it: PlayerState.addLayers (特质, items, bands, 机变 cards, bonds), the SETTLE of BattleResult.layerGains, Battle.addLayers (the live copy, clamped after the layerGain hook, 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-bond layerGains ≤ min(60 + 4·round, 999 − starting layers) (+ the lineup's layer 特质, §21.26). invariants.js flags a bond above 999; the UI shows the server's capped count.
  • Now — 限伤: kinds 'boss' / 'hidden' only; a leader is isBoss (the tag-'boss' units: those 10 keys and their mirrored copies — parts, 刺胄之弹, drones and escorts never count). A hit with ceil(final) ≥ 300000 is cancelled: checked in dealDamage after DEF/RES and every multiplier, before HP shields, and in loseHp (a part's 传递, a drone's death). A cancelled hit causes no HP or pool loss, no credit or stats, no damage number, no damaged / fatal / kill; the attack, its SP, the hit hook and separate element 损伤 hits stay. Element bursts and DoT ticks are checked, gauge fills are not. The sim emits fx hitCap {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 = 0 and / or BOSS_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's test/match/layercap.test.js / test/sim/battle.test.js cap 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's max_hp 0.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>_blink route, 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>_origin route 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_TRANSFER 1 (BLADE_TRANSFER is 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_GS 5 game s, §14) had its 'cleared' b.result rejected 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 a b.progress could 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: enterBattle emptied the prep state the cue compares with. render/app.js keeps the last prep pieces across a battle or scouting board (promoBase), so the next setPrep plays fx.promote on the elite (streaks from the consumed copies' slots). Re-run of the QA case (松桐 grants the third 古米 at R4 ROUND_START): view.debug.promotions records 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's panelSide rule), 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-in RENDER_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-in SP_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 shared field (联防 'u', boss / hidden 'b1' / 'b2') — settled by the user (2026-10-02: "共享阵地按你说的规则就行"; no official source shows the strip there):

    1. If the ‹ › pill points at a half that a player holds → that player. The pill's "你自己" means yours; "👁 name" means theirs.
    2. Else (全景, an empty half "无人在家", or no pill), if you fight on that field → yours.
    3. 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)). The g.watch request still names only the field. watchWho counts 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.watch is sent, because it would be refused with BAD_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.start is still on its way never brings the teammate back.
    • A loading battle waits until it runs (its state carries no watch yet).
    • 联防 leakers and eliminated spectators keep their own rules.

Labelling:

  • A teammate's strip leads with an amber "👁 name" pill (the observing pill's spelling) and gets amber disc rings, aria-label "name 的盟约" and data-owner; slots carry data-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.
  • popupView builds 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 at BOND_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, both m.private bonds and m.public players[].bonds add the finished normal battle's IN_BATTLE gains (PlayerState.pendingLayerGains = the result's layerGains; bondsMeta.bondsWithGains: floored, at most up to the cap, like settle()). 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.layers are 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 own m.private bonds, minus thresholds / countsHand (the client reads those from bonds.json).
  • An eliminated player's list is [], for these reasons:
    • nobody can watch them: g.watch refuses them and they have no field;
    • the result screen reads m.result's own bonds;
    • their remaining layers would otherwise cost every m.public for the rest of the match.
  • Measured in a 4-AI NORMAL harness match (seed 11, round 13, 3 players eliminated): players[].bonds is 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.public stays 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). Hardening credited to pull requests (PR #2, PR #7, PR #10, PR #14) → §21.25. Closing additions from the public GitHub issues and pull requests: the gaps found while triaging issues #1 / #8 → §21.26; the client fixes for issue #5 (the folded shop's camera) and #8 item 5 (operators left as placeholders) → §21.27; the strategy draft's match info (#8 item 1) → §21.28; six 重装 skills cast with an enemy in range, the owner's deliberate deviation (issue #4, PR #12) → §21.29.

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), then the four closing branches (the gaps, the 突变细胞 bench rule, the issue #5 / #8 client fixes, the draft's match info — each adding its own subsection, numbered §21.26–§21.28 in that merge order) and GitHub's master (PR #10, PR #12); 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), then the closing additions (§21.26–§21.29), with what QA of that integration found and the integration fixed: a lent 坚固维式重锤 and in-place revives against the once-per-deployment lock (§21.21), 卢西恩 / 锏 casting for an airborne 蒂比 alone (§21.22), rule 3 for a summon that is gone (§21.24) and when the dispatcher takes its item snapshot (§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; with the closing additions: §5.6 (TAKE_DAMAGE), §6.2 (the 突变细胞 gain), §6.3 (harmony), §6.6 (the band pick, the gained operator), §8.2 / §8.3 (harmony?), §9 (setCamera shop, the model fallback), §10 (the draft's match info), §11 (the boss-field speed bar), §14 / §20.12 (the layer bound), §17.1, §19.2, §19.8, §20.2, §20.9, §20.11.

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_cnt 1.
  • #1 official: garrison_123_a/_b = SERVER_REFRESH_SHOP / SERVER_GAIN_BOND_LAYER_BY_REFRESH_CNT, bb refresh_cnt 1, 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 key garrison:SERVER_GAIN_BOND_LAYER_BY_REFRESH_CNT:refreshes), and fires on the refresh_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 (_mergeChess lost its fromKey / fromDir options, audit.js its 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.json note). 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 setPrep drops 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.js updated.

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 draftPool initial 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 = 2 stays (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_hard and mode_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_*_s events); 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-in test/ui/feedback1-offers.e2e.test.js, feedback1-secret-shop.e2e.test.js, feedback1-tactic.e2e.test.js (SP_E2E=1).
  • #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) and grid.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_TILES makes the effective buildable NONE: stages.json tiles.d.buildable NONE (buildableType ALL kept), deployTiles without the water, canStand refuses 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 on ownerRangeKeys(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 with HAND_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-in test/ui/feedback1-placement.e2e.test.js (SP_E2E=1; fastServer SP_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, interval 10; 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) — and, since 0.1.2, for 3 s after a block ends (§22.8).
  • #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"): rebirthCleanse removes ally-sourced buffs and source-less statuses, keeping persist talents, 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: setForm keeps e.form (UnitInfo form) and puts form on 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; past HELD_MAX superseded 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: renderInfo keeps form. Closing clips are timed from the fx dur and 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); the 重生 whitelist. (隐匿 returned at once after a block until 0.1.2 — now 3 s, the knock-out's own release included: §22.8.)
  • 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-in test/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_speed instead of adding it; the swap ran only in normal battles and only on NORMAL-rank enemies; the four kept the database's roguelike lifePointReduce 0; 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 _2 versions (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 _2 enemies have lpr 1 (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 by ctx.destroyPiece (突变细胞, the round-start promotion item) now runs checkItemMerges (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_UNCAPPED keeps 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: _displaceable checked 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, noDisplace and refuses 浮空, its flight keeps a persistent noDisplace; 喷气人's 7 s flight is an air unit with noDisplace, 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, and stats.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.broadcastList comment_boss_hit_1..3 "{0}博士对敌方领袖造成的伤害超过20% / 50% / 80%!" (BOSS_HIT, paramList 0.2 / 0.5 / 0.8, priority 30); constData broadcastBeginDelay 1; client 2.7.71 keeps bossDamage[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) ÷ its maxHp; each threshold once per player per round, a hit passing several announces the highest; stats.bossDamage unchanged (both rounds).
  • Now (client): lines keep type, playerId, priority and 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) at FLOW_TICKER_PRIORITY 25 ASSUMED, content ticker() 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.pieceBonds for the prep count, m.private / m.public, the battle input; support.unitBonds in 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 装备. UnitInfo items? (sim unitInfo, 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 (explained since §21.26: 含调和 +1 and a 调和 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, 脆弱, hit effects, 受击回复 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 (unit dmg, 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 template knockback[dir] = Knockback {_useSourceDirection true, _decreaseForceLevelWhenNotInDirection 2} (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: 莱伊 S1 ray_s_1 decreases by 1, matching its PRTS 备注.
  • Now: 野鬃 S2 = Battle.push with dir = 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].spineLocal names the files of the data/local-assets.json group spine/enemy/<id> with the metadata of the committed tools/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 with node tools/setup.mjs --local, the spine/enemy job). Audit (249 enemies vs the client's prefabs): no other enemy is drawn with another's model; 伊利昂的木驮兽 starts on skin Full in the game and is drawn with default (unchanged). fetch-assets --prune never deletes public/assets/local any 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 ("技能结束后"); _immuneStunWhenAffecting 0.
  • 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 allowDiagonalMove true and visitEveryTileCenter / visitEveryNodeCenter false; 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; next is 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 / groundPathTiles return Bresenham tiles. One-line flip: always use = o in _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, plus rangeExtend) → 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's summonRange and 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 rangeAoe profiles — resolveProfile applies 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, one billro:mark per 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-in test/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 isCosmeticEvent counted every 'fx' as cosmetic, so processEvents dropped 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: 转译基底·α ended dead: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 old sleep left 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 keep enemiesInRadius. 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.noLeak and Battle._timeout skips 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) applyStatus after 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) beside noMove — not noMove itself, 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 (items/battle.js holdsUndying: this deployment's window) 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 首次); the lock belongs to the deployment, so a borrowed hammer (萨尔贡 × 娜仁图亚) follows the same rule content/items/battle.js deploymentOf returning one key for the whole battle (one lock per battle)
Six 重装 skills' trigger (§21.29) 深巡 S2, 雷蛇 S2, 号角 S2 / S3 and 灰毫 S1 / S2 cast with an enemy in range (the basic strategy, DEFAULT) instead of the official 下半 TANK row's TAKE_DAMAGE — a deliberate, documented deviation (the owner, 2026-10-03: "反馈的人太多了"; GitHub issue #4, PR #12); every other MANUAL 重装 skill, 深巡 S1 included, waits for a hit (余 S2 excepted since 2026-10-04: the owner extended the deviation to 余 S2, which casts with an enemy on its x-1 — SKILL_RANGE, §22.10) remove the entry from tools/build-data.mjs TRIGGER_DEVIATIONS (for 深巡 / 雷蛇 also PR #12's trigger: 'DEFAULT' kit line) and rebuild; for 余 S2 remove its TRIGGER_DEVIATIONS entry (chess_char_6_03_a) and rebuild
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 3 s after a block ends (§22.8; it was "at once" until 0.1.1); 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 bossDamage reset; 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); an in-place 复活 (M3茧甲, 埃芒加德) is a new deployment too (PRTS's 复活 is 退场 + a 0-time / 0-cost redeploy); 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 and outlasts the lend that started it; 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; an area skill cast because allies are near counts only those it can hurt (卢西恩's 【aoe】, 锏's CircleAttack).
  • §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 homes of the board pieces of every player on the field, on the field or not (a summon that has expired or been killed included); 歌利亚头盔 ignores bodies; every 退场 leaves a body (§22.15), except the 突袭 retreat, which redeploys at once.

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: the SP reset and the other per-deployment effects — only 坚固维式重锤's lock treats the in-place revive as a new deployment) 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 / deploymentOf / holdsUndying / revivedInPlace / PRIO_REVIVE / PRIO_RESPAWN, content/bands/battle.js PRIO_BAND_REVIVE, kits/tier4.js (信仰搅拌机 S2), content/bands/meta.js (休露丝), match/PlayerState.js buy / pickReward

  • F1 seen: "维多利亚坚固锤子的锁血没生效". Official (act2autochess eff_acarm043 / eff_acgarm043): "首次受到致命伤害时生命值不低于1,持续8秒", undeadable_duration 8 (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 拉特兰桥夹 每次部署): a new deployment of the carrier (every deploy — the redeploy after a knock-out, a 突袭 retreat + redeploy, 阿戈尔's 立刻复活 — and an in-place 复活, below) re-arms the lock and ends a window still running. The fatal order 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).
  • QA after the integration, fixed: (1) the lock lived in the hooks of the carrier's hammer grants — a hammer lent by 萨尔贡 × 娜仁图亚 (60 s) took its re-arm hook along when the lend ran out, so a borrower knocked out while lent came back with its lock spent when lent again (real Match, 萨尔贡 6: no lock in the second deployment), and a window started through the lend ended with it (knocked out 4 s into its 8 s). The lock now belongs to the deployment — deploymentOf (the deploy counter plus the in-place revives) marks the deployment whose lock is spent — and one battle-level hook holds every running window (holdsUndying, also what 信仰搅拌机 S2 steps aside for): a borrower is re-armed in its next deployment like an owner, a lend refreshed in the same deployment re-arms nothing, and a window outlasts the lend [ASSUMED: the 异常效果 不死 outlasts its source]. (2) M3茧甲 and 埃芒加德 revive in place, which kept the lock spent, while 阿戈尔 5's 立刻复活 — a real redeploy — re-armed it; PRTS describes all three the same way ("“复活”的实现方式为:受益者因移动之外的原因退场时下次部署的再部署时间和费用归零"), so under the per-deployment rule a revive is a new deployment: both in-place revives now call revivedInPlace and the revived carrier locks again on its next lethal hit ASSUMED.
  • F2 seen: a 谢拉格 taken from the promotion reward used up 休露丝's "每回合购买的首名<谢拉格>干员消耗资金为1". Not real: only g.buy dispatches onBuy, where every 购买 counter lives (休露丝, 玛恩纳, 升华, 整备); the promotion reward, 寻呼模块's pick and the 拟态物质 / 紧急调度券 / 简易通讯机 grants never touch it (pinned through real intents; META onBuy). 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, the M3茧甲 / 埃芒加德 revives, a lent hammer), test/sim/feedback1-residuals.test.js (信仰搅拌机 S2 and the window of an earlier deployment), 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 (not Unit.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 the hit hook, 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 / applyStatus ignoreSelect, or 无来源): the "无视无法选择" abilities (【污染秽蚀】, 【盲信之誓】, 萨卡兹悖谬暴虐兵长's 暴击 splash — her 片场工作指南 may dodge it); 碎铳之簧's counter and 重装侦察兵's 暴露 on their 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, 源石溶剂).
  • QA after the integration, fixed: 卢西恩,“猩红血钻”'s 【aoe】 (radius 2) and 锏's CircleAttack (first form, radius 1.5) decided to cast by counting every ally in their radius, so an airborne 蒂比 alone in range set them off and the blast skipped her (6 casts in 60 s, 0 hits on her); they count only the allies they can hurt (!evadesGround) [ASSUMED: no PRTS line states the trigger]. The player text (PLAYING §10, CHANGELOG) now says that ground-enemy auras / field effects and counters on her as an attacker still reach her.
  • 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 (14), opt-in test/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 fired (since §21.29 her default S2 casts with an enemy in range, a deliberate deviation; S1 still waits for a hit); 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). _blockerFor now refuses ground blocking from a tile ground units cannot pass (on the stages exactly the b tiles).
  • 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, 不屈, 阿戈尔), also for an operator forced out by its own effects (余烬, 耀骑士临光 S2, 骑士戒律 + 竞技旗, 伊内丝 S3, a 商人 that cannot pay; §22.15); only the 突袭 retreat leaves no body. downOn: _deploy (redeploys, the 突袭 landing, summons), spawnDevice and relocate refuse the body tile for every ally; isReservedTile reports it, so the 突袭 landing, tactical points, summon / device pickers and 维娜's lions skip it. b.snap down carries the tile (§8.2) and the client draws the body there; WAIT_TILE matches the _checkRedeploys hold-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.
  • QA after the integration, fixed: rule 3 counted only the board pieces that were not removed, but a summon leaves its home free only once it has expired or been killed (a docked one keeps its tile reserved), so the 召唤物 clause never applied — a 突袭 member that landed on the home of 浊心斯卡蒂's expired 海嗣 and fell there lay on it and kept the 海嗣 (远古血亲) from coming back. Every board piece's home counts now, on the field or not. Cost: the body bookkeeping added about 2.5 % per tick on 519 real specs (8 % on boss fields; whole matches and the perf budgets unchanged); _checkRedeploys now checks the DP before the tile scans, so an operator waiting for DP costs no ally scan — on the same 519 specs, against the integration head, the same tick counts and boss fields 11.9 → 10.9 µs per tick (a tile → body map is left for later).
  • ASSUMED: §21.20. Tests: test/sim/feedback1f-downed-tile.test.js (rule 3 for a summon that has expired or been killed), test/render/feedback1f-down-tile.test.js, opt-in test/render/feedback1f-down-tile.browser.test.js; downelem, bonds_addon, engine-requests, robustness follow the rule.

21.25 Hardening credited to pull requests (PR #2, PR #7, PR #10, PR #14) — match/effectsMeta.js dispatch / stillEquipped, tools/fetch-assets.mjs shrinkGuard, tools/assets/manifest.mjs droppedEntries, assets.js loadImageElement, ui/guide.js preload, css/screens/loadout.css

  • 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.items live 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). QA after the integration: the items were read per holder as the walk reached it, so an item equipped meanwhile onto a holder not yet walked ran in the same dispatch and an item moved to a later holder ran twice (latent: no shipped item equips or moves items on those hooks); every [holder, item] pair is now taken before the first item runs, and two more synthetic cases pin it.
  • 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 current data/assets.json, the run keeps the current file, prints the entries it would drop (also the report's droppedEntries / manifestWritten) and exits 1; --allow-shrink (or --prune) writes it (ASSETS.md; test/fetch-assets-shrink.test.js).
  • crossOrigin on the two new Image() loads (PR #10, merged on GitHub): with /assets/ served from another origin (a CDN or object storage) an image loaded without CORS approval taints the canvas and breaks the WebGL textures (an empty map, a white 3D board). assets.js loadImageElement — the asset store's only image path, §21.27's seeded store included — and ui/guide.js preload set crossOrigin = 'anonymous'; a same-origin page is unaffected.
  • The loadout screen's background layer (pointed out in PR #14): css/screens/loadout.css .lo > * { position: relative } — one class, a later rule — overrode .lo__bg { position: absolute }, so the mint glow and grid layer was a 0-px flex item and never drew. Now .lo > :where(:not(.lo__bg)): the same one-class specificity for every other layer, never the background. Checked in headless Chrome at 1920×1080 and 844×390 (touch): the layer covers the overlay and every other box of the screen is where it was (to 0.1 px). Tests: test/ui/loadout.test.js (the cascade), opt-in test/ui/loadout.e2e.test.js (SP_E2E=1: the layer's box and stacking at both sizes).

21.26 Gaps found while triaging GitHub issues #1 / #8 (v0.1.1)

Where: ui/gameLogic.js morphPairings / HARMONY_BOND / harmonyMembers, ui/detailPanel.js MorphPairings / MorphGrantLine / ItemDetail / ChessDetail / BondChips, ui/bondStrip.js BondPopup / BondStrip, match/bondsMeta.js computeBonds / bondList, shared/bandBonds.js (→ bands.json bondIds), match/gamedata.js bandBondIds, match/bot.js botPickBand, screens/bandDraft.js BandOffTag / BandOffNote, ui/gameLogic.js bandOffBonds, tools/botbench.mjs (--band), match/fields.js layerAllowanceOf, public/dev/game-mock.js (variants funny / morph / harmony). Three gaps the user decided to close in 0.1.1 (no rule changes), and a false rejection of honest client results found on the way.

  • 变形同构体 shows its pairings (issue #1: players read the "转职球" as doing nothing). Its text says "具体对应关系可在模拟中查看本装备天赋栏", and no screen showed that 天赋栏. Now the item card (shop, reward, hand / temp: ItemDetail) has a 天赋 section: "搭配以下装备时,携带者视为对应盟约的成员:", then one line per bond in bonds.json order — 【bond】 and the items whose giveBondId is it (one name per item family, by tier): the 14 pairings of the official talent (character_table trap_1073_acarm073, "“炎国短刀”→【炎】盟约 …"; a test checks the two against each other), its "“维式重锤”系列装备" as the 5 hammers — 18 items, built from the same giveBondId the server counts with (morphPairings; pieceBonds / grantedBonds). A bond the mode never activates is struck through with 本局禁用 (modeOffBonds, §21.7; 标准: 拉特兰 阿戈尔 卡西米尔 奥术 突袭). On a wearer's card (its 装备 rows; also a teammate's unit and the card a 同构 row opens) the pairing it wears is highlighted — 生效中, or 已搭配 when that bond is off — and a wearer without one reads 暂未生效. A bond item's card and row say "与变形同构体一同装备时,携带者视为【X】成员" (MorphGrantLine). The 机密商店 cards have no detail view and keep the official text. Phones: the list keeps ≥ 9 px (px floors in game-panels.css).
  • 调和's +1 explained (issue #1: "一些不考虑整备区的盟约计算人数时也会把整备区的干员算进去" — the +1 read as a miscount; §21.11 had left "调和's +1 raises 在场 for core bonds without a member row"). The rule is unchanged (§6.3; one +1 total, research 02 §2.1 ASSUMED). Now a bond state whose count holds the +1 carries harmony: 1 (computeBonds) and both views send it — m.private bonds[] and m.public players[].bonds[] entries { …, harmony? } (absent otherwise; not in the battle input, bondSnapshot) — so the client never re-derives the rule. The popup says 在场 n(含调和 +1) and a 调和 row heads its member list, naming the 调和 operators on that board (harmonyMembers: 缪尔赛思, Pith's 盟约·辅助干员; a tap opens the first one's card; with that board not on screen "调和已激活:核心盟约激活人数 +1") — for your own strip and a teammate's (§20.15: the entry from m.public, the operators from the field on screen). The strip disc's and the card chip's titles add (含调和 +1).
  • Bots skip strategies built around a switched-off bond: in 标准 the bot took 潘格尼尼 (【定制铳械】 an elite <拉特兰>), 克莱门莎 (<阿戈尔>) or 玛恩纳 (<卡西米尔>) in 5 of 200 solo and 23 of 800 co-op picks (seeds 1–200). bands.json bondIds holds a band's tie, read from its own data at build time (tools/build-data.mjs through shared/bandBonds.js; GameData.bandBondIds and the client read the field) — the bond names in <…> of its text (the official note "在部分干员缺席时体验可能不完整" among them; a bracketed item or enemy adds nothing) and the bond ids / bond pools of its blackboards (bond, bond_id; pool → choices.json pools[pool].bond, 潘格尼尼's pool_char_later) — nine bands are tied (杜遥夜 炎, 佩佩 / 娜仁图亚 萨尔贡, 哈洛德 维多利亚, 休露丝 谢拉格, 潘格尼尼 拉特兰, 克莱门莎 阿戈尔, 玛恩纳 卡西米尔, 贾维 叙拉古). botPickBand weighs a band tied to a bond of gd.modeInactiveBonds 0 — one rng draw per pick, the default band when every offered band is excluded: 0 of 200 and 0 of 800 in 标准, while the other modes keep exactly the earlier picks (绝境: 5 / 200, 23 / 800). tools/botbench.mjs --band still forces any band.
  • The strategy draft marks them 本局禁用 for human players (the owner's follow-up): a band whose bondIds meet the mode's inactive bonds (gameLogic.bandOffBonds, the same field and rule as the bot — a test pins the two in every mode) carries a 本局禁用 tag on its card and, in the detail pane, "本局禁用【拉特兰】盟约,此策略效果可能无法发挥" with the bond struck through (§21.7's look); it stays selectable — the official game lets you pick it.
  • Found on the way — the client-result layer bound (match/fields.js layerAllowanceOf / validateClientResult): with the bots' new picks the full-match suite's coop/FUNNY/107 (4 humans on AI 托管) reached a 6-谢拉格 freeze board at R12 (初雪 + 银灰: garrison_28, 50 % +1 per freeze in range, no per-battle cap in the data; 凛御银灰 hands out a 60 % +2) whose honest +112 谢拉格 layers passed the flat 60 + 4·12 = 108, so the server rejected the client and re-simulated (SP_VERIFY=all agrees with the client on all 57 battles of that match). Uncapped traits are common (菲莱 / 百炼嘉维尔 per skill, 斯卡蒂 per 2 kills, 莫斯提马 per 6 bullets …) and capped ones reach 100–200 (史尔特尔, 荒芜拉普兰德), so the flat bound now adds, per bond, what the player's IN_BATTLE layer 特质 can add to it — each trait of its units and each one their ADD_BOND traits hand out (once per operator), on the bonds it names, up to the cap the sim applies (max_add_count_per_battle, the handed-out 华法琳 trait's GRANTED_CAP_OVERRIDE); a trait without a cap — or a 魔王, whose +extra counts toward none — leaves its bonds bounded only by the 999 room. A forged gain on any other bond meets the flat bound as before.
  • Normative lines rewritten with it: §6.3 (harmony in the views), §6.6 (the band pick), §8.2 / §8.3 (bonds[] entries harmony?), §14 / §20.12 (the layer bound), §21.11 (its 调和 note).
  • Tests: test/ui/feedback1-gaps.test.js, test/match/feedback1-gaps.test.js; opt-in test/ui/feedback1-gaps.e2e.test.js (SP_E2E=1; 1920×1080, 844×390, 756×366; SP_E2E_OUT moves the screenshots).

21.27 Client fixes for GitHub issues #5 and #8 (v0.1.1) — ui/gameLogic.js prepCameraFor / foldCamera, screens/game.js, ui/fieldHost.js hudBands / cornerBand / seedAssets, render/app.js, css/devices.css, assets.js, render/units.js

Two public GitHub issues after 0.1.0, both client-only. Normative lines rewritten with them: §9 (setCamera shop, the model fallback), §17.1 (the quiet budget), §19.8 (the collapsed shop).

  • #5 seen: "准备阶段,收起商店界面时,界面并不会进行缩放(观战及作战时无此现象)". Real: folding the shop (收起) was UI state only — the own prep camera was requested once on entering prep, never with shop, so render/projection.js's official shop-collapsed camera (left_prepare_camera_param / *_boss_prepare_*, presetCamera shop: false) was never used, and hudBands always reserved the bar (measured: 110 px per tile at 1920×1080, 36 at 844×390, 31 at 756×366, the same folded).
  • Now: while the own prep board is on screen and the bar is shown folded, the game requests prepCameraFor(pub, myId, folded) — the prep camera with shop: !folded — whenever the fold changes, with the normal 0.75 s flight (foldCamera): never over the pen (it folds the bar itself and returns to the camera it left), on a teammate's board, in battle or for an eliminated player (no bar); deferred while a piece is dragged or its direction is chosen (the drop target and the wheel sit on tiles of the camera in use) and applied when that ends; unfolding returns to exactly the shop camera; the fold still resets at combat start. The view hands the request's flag to hud(kind, size, { shop }) (also on resize). The folded band is the higher of the tab (.shopbar-tab: .8rem + 3 px, above the bottom safe-area inset) and the corner buttons' hit areas (交流 / ⚙ / 📖 / ⛶, measured by cornerBand; on touch screens a 34 px button takes taps 5 px above itself, --tap-min 44 px): with the tab alone the corner covered bench pad 0 at 756×366 and the near edge of pads 0–2 at 844×390. Below 768 CSS px the corner wraps into two rows only because the open bar would cover it; with the bar folded it stays on one row (css/devices.css). Picking reads the camera of every frame. ASSUMED the folded band (the official collapsed HUD is not in the extracted bundles).
  • Measured (headless Chrome; px per tile at row 10; every bench pad fully pressable open and folded, also the Final Assault prep halves): 1920×1080 109.8 → 130.7 (the official camera at its own scale, panned up 46 px to clear the tab); 844×390 35.9 → 44.3; 756×366 (touch) 31.4 → 40.1. A real drag deploy and a tap on a piece work folded; a drag or the wheel defer the flight; scouting (the 'normal' camera), the battle camera and the next prep are unchanged.
  • #8 item 5 seen: "游览器切换页面后所有干员会变成方块+水滴的图标" — the image-less placeholder (render/textures.js) on every operator. A plain tab switch does not reproduce it; a reload does (phone browsers and Chrome's Memory Saver discard background tabs and reload them on return; the game resumes into the match): the asset store downloaded /data/assets.json a second time beside the game data's copy, createFieldView waited ≤ 4 s for it, every view looked its model and avatar up once in its constructor, and a failed download kept {} for the page — placeholders for good. Related without a reload: a battle that began in a hidden tab built no views (they are built in animation frames), nothing referenced a skeleton, and the quiet budget (§17.1) unloaded all of them ≈ 18 s later, so back in the tab every unit was an avatar diamond until its model downloaded again; a model that failed or timed out (20 s) was never retried.
  • Now: the field host seeds the asset store with the game data's copies (seedAssets → assets.seed / seedLocal; the match screen already waits for them, so no second download). Without one, ready() retries a transient failure after 600 ms and 2 s (like data.js), then resolves {} without keeping it: the next ready() and a bounded background backoff (5 / 15 / 30 / 60 s) fetch again, and onChange tells the field view when a manifest arrives late — every view re-resolves what it could not draw (UnitView.retryAssets; prep item icons through setPrep / ItemView.setIcon). A view whose model failed or timed out loads it again after 2 / 6 / 15 / 30 s (SPINE_RETRY_MS; frames only, so never while hidden), acquire(entry, { retry: true }) dropping the remembered failure; showing the tab again retries at once (also a missing manifest). Battle mode holds the Spine cache (spine.hold(), from enterBattle to the next setPrep or destroy): no quiet budget meanwhile, the 48 MB idle budget still applies, and the quiet budget keeps freeing everything in the lobby, room and result screen (the policy is in the assets.js header).
  • Verified (headless Chrome, no console errors): a reload with every assets.json request delayed 6 s, with only a second download delayed (the 0.1.0 race: none is made now) and with the first 4 requests failing — operators drawn as Spine models at once; the first 6 failing — placeholders until the store's first background try 5 s later, then Spine models; a battle started in a hidden tab, back after 25–30 s (still running, ended meanwhile, or paused with Spine downloads slowed to 4 s) — every unit a model within 0.3 s of the return, no diamonds (before: the cache was empty from ≈ 18 s); six solo rounds with two battles started hidden — the cache follows the roster (4 → 24 skeletons, 6.9 → 25.2 MB estimated, idle ≤ 11 MB, under the 48 MB budget), never a diamond or a placeholder.
  • Tests: test/ui/issue5-fold-camera.test.js, test/render/issue8-placeholders.test.js, test/ui/playtest5-ui.test.js (wiring), test/render/assets.test.js; opt-in SP_E2E=1 node --test test/ui/mock.e2e.test.js (fold / unfold at 1920×1080 and 844×390).

21.28 Match info in the strategy draft (GitHub issue #8 item 1, v0.1.1) — ui/matchInfo.js, screens/briefing.js, screens/bandDraft.js draftInfoStatus, ui/enemyDrawer.js InfoTab, css/screens/draft.css .minfo-dlg

  • Reported (public issue #8, item 1, "选策略时没法返回查看禁用的干员和盟约"): the disabled bonds and the banned operators were shown only in the INFO_CHECK briefing, but a strategy is chosen partly by them. The owner took it into 0.1.1 as a remake addition (no official screen is copied; the look is the briefing's).
  • One component: ui/matchInfo.js — matchInfoModel(pub, { bonds, chess, mode }) (pure: gameLogic disabledBondSets over config.modes[modeId].inactiveBondIds — the drawn set D "阵容不完整" and the mode's static inactive bonds "本局禁用", e.g. 标准's 10; every bond by bondOrder, then identifier, split 核心 / 附加; the banned operators of m.public bannedChess, known chess only, by tier; banned members per bond) and the hookless blocks MatchBondRow / MatchLegend / BannedOperators / MatchInfo (greyed disc with ✕, the red badge, the briefingBondTip tooltip, the legend, the 本局禁用干员 grid). The briefing's right column is MatchInfo; the in-game 本局信息 tab (left 🔍) reads the same model for its compact chips and avatars (its banned list is now by tier, like the briefing's).
  • The draft: 查看禁用盟约与干员 under the order list opens MatchInfoDialog — MatchInfo in the components.js Modal, read-only, closed by 关闭, a tap outside or Esc (a press on the backdrop closes it on mousedown, so the click never reaches what lies under it — e.g. 确认选择). The draft runs on underneath: the dialog's footer repeats the turn and its seconds (draftInfoStatus: 轮到你决策 / X 决策中 + the step header's number, orange at ≤ 10 s; 已选择「…」after the own pick); a turn change (a pick, a skip, a turn that runs out, an AI pick) or the own pick closes it; the end of the draft unmounts it with the screen; it never changes the highlighted band (g.bandFocus) or the buttons.
  • Sizes: on desktop the briefing's own sizes (1920×1080: no scrolling); every size in the dialog is max(design rem, px floor), so on phones (1rem = 40 px; 756×366, 844×390) the text stays ≥ 9–15 px, the dialog fits the safe area and its body scrolls under a fixed header and footer.
  • Tests: test/ui/match-info.test.js (the model, greying by state, badges, tips, the legend, banned by tier, real 标准 / 绝境 matches, draftInfoStatus, briefing / draft / drawer reading the one module); opt-in test/ui/match-info.e2e.test.js (SP_E2E=1: solo 标准 on desktop — dialog = briefing, the three ways to close, a confirm afterwards, the in-game tab; co-op 绝境 with a desktop and a phone — the live countdown, a turn change closing it, 844×390 / 756×366 layout and scrolling, the draft's end unmounting it).

21.29 Deliberate deviation: six 重装 skills cast with an enemy in range (GitHub issue #4, PR #12) — tools/build-data.mjs TRIGGER_DEVIATIONS / resolveTrigger / validateAll, data/chess.json skills[].trigger, kits/tier1.js (深巡, 雷蛇), kits/tier2.js (灰毫), kits/tier5.js (号角)

  • Reported: public issue #4 — 深巡 (哨戒铁卫, range 2-2) never cast her default S2 行动能力剥夺 until something hit her, in practice until she blocked; PR #12 (merged on GitHub) gave 深巡 S2 and 雷蛇 S2 trigger: 'DEFAULT' in their kits. Many players asked the same of other defenders; the owner (2026-10-03): "反馈的人太多了".
  • Official, unchanged: the 下半 (act2autochess, 2026-03) skillTriggerDataList row TANK | | | 0 | TAKE_DAMAGE and PRTS 卫戍协议/帮助's 重装 strategy "不受技能范围影响,受到伤害时释放技能" (下半, 2026/3/14) — a class row, covering every MANUAL 重装 skill whatever its index (§20.2); 上半 (act1autochess, 2025-11) had no TANK row, so 重装 skills used the basic strategy then (research 03 §1.4 keeps the history). PR #12's reasons — the row as "skill 1 only" and "offensive skills activate when an enemy is in their skill range" — rested on a community summary (arknights.wiki.gg), not on the CN client or PRTS; that quote is removed from research 03 (promised on PR #12).
  • Decision (the owner, 2026-10-03): a deliberate, documented deviation — six offensive 重装 skills take the basic strategy (DEFAULT: SP ready, about to attack, an enemy inside the initial range or one it blocks): 深巡 S2 行动能力剥夺, 雷蛇 S2 反击电弧, 号角 S2 暴风号令 and S3 终极防线, 灰毫 S1 攻击力强化·γ型 and S2 专注轰击. Every other MANUAL 重装 skill keeps TAKE_DAMAGE — 深巡 S1 侵袭破坏应对 included.
  • Where: in the data, so the server, the browser battle and the bots read one rule. TRIGGER_DEVIATIONS is keyed by the normal chess id and the skill id (the elite record follows through its baseId), never by the skill id alone: 灰毫 S1 is the generic skcom_atk_up[3], which 惊蛰, 幽灵鲨, 耶拉, 莫斯提马 and 莱恩哈特 carry too. resolveTrigger applies it last; the 12 records read trigger { rule: 'DEFAULT', rawRule: 'TAKE_DAMAGE' } — rawRule keeps the official row for traceability — and validateAll fails the build when an entry no longer meets a TAKE_DAMAGE row. PR #12's kit lines (trigger: 'DEFAULT' on 深巡 / 雷蛇 S2) stay, agreeing with the data (a test pins kit = data); their comments and the contributor's tests now give this section's reason.
  • Measured (a real battle: 战场#01(下半), one 萨卡兹枯朽前锋 walking the lane from the gate, the operator SP-full on (9,5) facing it): 深巡 / 雷蛇 S2 cast 6.2 s after the spawn, when the enemy steps onto the 2-2 line, and 号角 S2 / S3 and 灰毫 S1 / S2 within 1.3 s (their 要塞 range reaches the gate) — each before anything has hit it (the enemy reaches the defender after ≈ 24 s, if at all); each of the other 46 MANUAL 重装 records (normal + elite) casts on the first hit it takes, at 23.8 s.
  • Players notice: these six fire sooner and more often — from range, before anything reaches the defender; 号角 S3's 过载 HP drain therefore starts sooner; 灰毫 S2 (block 0) lets an enemy pass as soon as one comes into range, not after the first hit; on a fenced tile (§21.23) 深巡's S2 fires.
  • One-line flip: remove the entry from TRIGGER_DEVIATIONS (for 深巡 / 雷蛇 also the kit line) and rebuild.
  • Normative lines rewritten with it: §5.6 (TAKE_DAMAGE), §19.2 (灰毫 S2), §20.2 (players notice, the table), §21.20 (a settled row), §21.23 (深巡 on a fenced tile).
  • Tests: test/sim/feedback1-tank-triggers.test.js (the data, kit = data, the real battle for the six and for every other MANUAL 重装 record), test/data.test.js, kits_t1t2 / kits_alt_t1 (PR #12's), kits_alt_t2 (灰毫 S2), kits_t5 / kits_alt_t5 (号角), test/docs-consistency.test.js.
  • Extended 2026-10-04: 余 S2 (SKILL_RANGE on its x-1, §22.10); 14 records deviate now.

22. GitHub issues after 0.1.1 (v0.1.2)

Public GitHub issues filed after the 0.1.1 release (2026-10-03), each triaged against 0.1.1 first; the real ones were fixed for 0.1.2 in fix branches WA–WL, each independently reviewed before it was merged into feedback2. Where each is handled: #25 back-facing operators keep their attack pose when knocked out → §22.1; #51 突袭's idle redeploy → §22.2; #33 items 1–2 阿戈尔's devour and 乌尔比安 S3 → §22.3; #33 item 6 活性源石, with the 沼泽 and 深水区 terrain → §22.4; #42 emotes and guide pages without a local client, and the English title (#38) → §22.5; #32 item 4 钩索师 / 推击手 on 高台 → §22.6; #52 史尔特尔's 余烬 → §22.7; #43 an enemy's 隐匿 after a block, and the 深池逐火 ember window → §22.8; #32 item 7 荒芜拉普兰德 S3's drones → §22.9; #32 item 1 余 S2 → §22.10; #44 归溟幽灵鲨's 替身 → §22.11; #32 item 6 enemy splash against a 隐匿 operator → §22.12; #35 / #16 (PR #48) 要塞 against air units and 莫斯提马's slow icon → §22.13; #58 / #60 / #64 follow in §22.14–§22.16. Decisions of the owner, 2026-10-04: 荒芜拉普兰德's drones measure the distance to an enemy's position, a huge enemy's centre (§22.9); 余 S2 casts with an enemy on its x-1 — a deliberate deviation from the official TAKE_DAMAGE row, added to §21.29's six (§22.10); 乌尔比安 keeps the devour gains through his 【移动】 — the deviation stays (§22.3).

22.1 Back-facing operators play their fall (GitHub #25) — render/units.js _wantsBack / _syncModel / die / revive / _acquireSpine / _dropActor / dieClipDur, render/spine.js SpineActor.die / dieClip / deployElapsed

  • Seen (public issue #25, "部分干员被击倒后在部署状态时仍有攻击动作"; the screenshot: 跃跃 seen from behind under its redeploy ring, still in its attack pose, while 拉普兰德 beside it lies down; the reporter also saw it on 拉普兰德, 异德 (缄默德克萨斯), 斯卡蒂 and 德克萨斯, "并非稳定出现" — all five have a Back skeleton without a Die clip). Real in 0.1.0 and 0.1.1, drawing only (the sim had the operator down; it never attacked): an operator deployed facing UP is drawn with its Back model (§9, research 07 §5.5), and 131 of the mode's 135 Back skeletons (data/assets.json) have no Die clip — only 薄绿, 玛恩纳, 松果 and 预备干员-重装 have one, while every operator's Front skeleton has one. Not every time: only operators facing UP.
  • Official: PRTS 卫戍协议/帮助 says a knocked-out operator stays on its tile as a "倒地干员" (§21.24); which model the official client lies it down with is in no source. The project's asset plan already had the rule — research 07 §5.4 "die: Die (if the Back model lacks it, swap to the Front model for the death animation)", docs/ASSETS.md Roles die: null — but the renderer never implemented it. ASSUMED the official client does the same (a Back skeleton without a Die clip cannot show a fall), and the four Back skeletons with a Die clip fall with it.
  • Cause: UnitView._wantsBack() ignored whether the unit stands; SpineActor.die() found no clip, returned 0 and changed nothing, so the attack / skill / idle loop playing at the knock-out went on under the ring for the whole respawn timer.
  • Now:
    • _wantsBack(): facing UP with a Back model, while the unit stands; a dead or knocked-out one keeps the Back model only when that skeleton has a Die clip (dieClipDur of its manifest entry), so the other 131 fall and lie with the Front model (unmirrored, as facing UP).
    • die() and revive() mark the view, and its next update() — after every event and the snapshot of that frame (render/app.js) — runs _syncModel(): the model the state wants is loaded when it is neither shown nor loading, and swapped in place like a direction change (no avatar diamond in between). A death and a redeploy handed out in the same frame load nothing: a 突袭 jump (retreat + redeploy in one tick), a 不屈 revive (from the death hook), a backlog delivered after a hidden tab.
    • The Front model replays the state: the Die clip where it would be (dieT), a knocked-down one holding its end; die(true) and a view built for a unit already down start on the held end (the 联防 entry FORCED_EXIT, a field entered late, the re-entry from the field meta after an overflowed hidden-tab backlog). Its first frame gets the knocked-down grey (_dropActor drops the cached tint; a swapped-in model used to keep the previous model's tint state). A view that gets no down entry (summons and devices) fades like one facing RIGHT — a forced-out operator facing UP lies down like a knocked-out one (§22.15): the Front model's Die clip times the fade (_fallDur).
    • The redeploy (a deploy event: timed, 不屈, 阿戈尔 — every Battle._deploy — or a revive seen only in the snapshot) brings the Back model back; a deploy clip the Front model is playing goes on on the Back model where it is (SpineActor.deployElapsed).
    • Safety net: until the Front model is there, or when it fails to load, the Back model holds still — SpineActor.die() on a skeleton without a Die clip stops whatever looped and holds the first frame of its idle clip (frozen when it has none either), and nothing restarts an attack on a dead actor. The 20 summon / enemy skeletons without a Die clip (e.g. 浊心斯卡蒂's 海嗣) now fade holding still instead of looping [ASSUMED look].
    • Unchanged model choice: operators facing RIGHT / DOWN / LEFT, enemies and summons (no Back models in the manifest: _wantsBack is false for them; only the safety net above changes how their idle-only skeletons die), the redeploy ring, the sim. Nothing changes in render/interp.js or battle/runner.js: die / deploy are state events every client stage keeps (runner keepsState, game.js keepEarly, interp isCosmeticEvent), and b.snap down / fieldMeta rebuild the view of an operator already down; both now reach the model swap.
  • ASSUMED: the Front model for the fall and the knocked-down pose (above); the Back skeleton's own Die clip when it has one; the held first idle frame of a skeleton without a Die clip.
  • Tests: test/render/issue25-downback.test.js (fake PIXI with the real manifest and asset helpers: the manifest counts; the five operators of the report lie down with their Front model's Die; an UP knock-out mid-attack — the Back model holds still, then the Front model's Die is held under the ring, greyed, every acquire paired with a release; RIGHT unchanged; the redeploy back to the Back model with the deploy clip carried over; the snapshot's DIE anim and a snapshot revive; a death and a redeploy in one frame load nothing; a view built for an operator already down and the 联防 entry lie on the held end; a withdrawn one fades like a RIGHT one; 薄绿 keeps its Back Die clip; the SpineActor safety net; enemies and summons unchanged); downring, feedback1f-down-tile, unitview, forms, test/ui/unitFacing.test.js unchanged.

22.2 突袭's idle redeploy lands where it can attack (#51) — content/bonds/addon/battle.js raidPoll / raidTile / raidReach / raidTargets, research 02 §3.18

  • Seen (GitHub issue #51, on 0.1.0): a 突袭 member idle for 10 s was redeployed even when no landing tile let it attack an enemy, so it hopped every 10 s between useless tiles; and the idle trigger always went for the most advanced enemy, even when another ground enemy could have been reached. Real (reproduced on 0.1.1): with one speed-0 enemy that no landing tile reaches (it stands on plain floor, and a RIGHT-facing melee member reaches it only from that tile or the floor tile behind it), the member jumped at 10 / 20 / 30 / 40 s between (10,9) and (10,8) beside it and never attacked; a second, reachable enemy was ignored. On the five real runs of the F5 test (test/sim/feedback1f-downed-tile.test.js) 26 of 60 jumps landed with no enemy in range — e.g. three members jumping to one enemy whose two reachable tiles the first two took.
  • Official: the bond text (activity_table bondInfoDict / effectInfoDataDict; PRTS 盟约记录, no note under it) "【突袭】干员10秒内未进行攻击或技能就绪时,若范围内没有敌人,则保留技力立即再部署至一名地面敌人周围" names no tile rule; whether an official jump can land out of range is not known (the reporter: the text "按字面允许落在不能立即攻击的位置").
  • Cause: the covers-the-target requirement of raidPoll bound only the 技能就绪 trigger (if (!idleOk && !tile[2]) continue;), and raidTile scored covering tiles first but still returned a non-covering one when none covered — so the idle trigger jumped to its first candidate whatever the tile.
  • Now (the owner's decision — the reporter's proposal; stricter than the 「优先」 of our first reply on #51: a covering tile or no jump, never a fallback landing): both triggers jump only to a landing tile from which the member's attack range (its own grid + 攻击距离 rangeExtend, turned by its direction; a huge enemy counts on any tile it occupies) covers the chosen ground enemy. The poll walks the candidates in the existing order — its own field's ground enemies (another field's only while its own has none), the most advanced (least remaining path) first, at most the 8 most advanced — and takes the first one that has such a tile (the nearest within 2 tiles, the facing-RIGHT tie-break as before). When none has one the member stays where it is: its idle time keeps counting and the 0.25 s poll looks again, so it jumps as soon as an enemy can be reached. After a jump the idle time restarts from the landing (then from each attack). Unchanged: never onto a knocked-out operator's tile (§21.24), never into the 深水区 (§21.3, grid.canStand), SP kept, a real redeployment (部署时 effects), the ATK / HP bonus.
  • Cost: a search that finds nothing now repeats every 0.25 s, so it is cheap: raidTile looks only at the tiles a range offset leads back to from the enemy's body (before: a 25-tile scan building a range per tile), and a player's members share one sorted candidate list per poll (rebuilt after a jump). On the heavy 2-player boss field of test/sim/robustness.test.js (≈ 130 enemies, 72 on the ground) with 18 members that never reach anything — every poll searches — the bond costs +5.8 µs per tick, as much as 0.1.1's +5.5 µs with its jumps every 10 s (the requirement alone on the old scan: +29.6 µs). Whenever a covering tile existed the landing tile is the old one (200 000 random cases: grids, 攻击距离, directions, huge bodies).
  • Players notice: a member with nothing it can reach waits on its tile instead of hopping; it passes over an unreachable leading enemy for one it can hit; when several members go for one enemy and its reachable tiles are taken, the rest wait. The five F5 runs: 35 jumps instead of 60, none out of range.
  • ASSUMED: "never a useless landing" (the text says only 再部署至一名地面敌人周围; issue #51); the cap of 8 candidates (a member waits when the 8 most advanced ground enemies are all out of reach — the 技能就绪 path's bound since 0.1.0, kept for the cost); the target order and the 2-tile search (research 02 §3.18, now written as above). Issue #49 (a passive skill never counts as 技能就绪) is unchanged and separate: it decides when a member may jump, this section where it lands.
  • Tests: test/content/bonds_addon.test.js "突袭 #51" ×2 (no reachable landing: neither trigger jumps in 45 s, then a reachable enemy → both jump within one poll; an unreachable leading enemy is passed over for the second, which it attacks, and the next jump comes the idle time after its last attack); the other 突袭 tests (bonds_addon, feedback1-water, feedback1f-downed-tile, facing-invariance) unchanged.

22.3 阿戈尔's devour skips members it has already knocked out (#33 item 1) — content/bonds/core.js devour, content/items/battle.js deploymentOf (exported through content/items.js), kits/tier5.js (乌尔比安 S3, #33 item 2)

  • Seen (GitHub #33 item 1, "阿戈尔阵营复活机制看起来不会触发"): the 5-tier revive works on its own (5 distinct 阿戈尔, one lethal hit each: the first 3 come back on their tile at full HP, normal and 联防 battles alike), but the usual food chain used it up at t = 0. One row, all facing right, 浊心斯卡蒂 → 乌尔比安 → 幽灵鲨 → 海霓 → 深巡 → 隐现: the battle-start devour knocked out 幽灵鲨, 海霓 and 深巡, the revive brought each straight back (all 3 charges), and 乌尔比安's marks still pending on them knocked them out a second time — down until their 70 / 80 s redeploy, no revive left for the fight (联防 battles alike).
  • Official (PRTS 卫戍协议:盟约 下半/PRTS盟约记录, 阿戈尔 备注): "标记按付与顺序触发【吞噬】效果,目标首次被击倒后解除自身被付与但还未触发的【吞噬】效果"; the 5-tier "前3名【阿戈尔】干员首次被击倒时立刻复活" (bonds.json max_free_respawn_cnt 3), implemented as "受益者因移动之外的原因退场时下次部署的再部署时间和费用归零". So a devour knock-out is a first knock-out and spends a revive, and the revived member takes no further mark.
  • Cause: devour() skipped a mark only while its target or marker was dead (!t.alive || !m.alive); a member revived in the same pass was alive again, so its pending marks fired. 埃芒加德's revives went the same way (it revives in place: the next pending mark knocked the revived unit out again and spent a second charge), and so did 不屈's 立刻重新部署 (the redeployed unit was knocked out again in the same instant, where 不屈 does not fire twice, and stayed down).
  • Now: when the marks are placed, the pass records each target's deployment (items/battle.js deploymentOf, the deploy counter plus the in-place revives — exported for this). A mark is skipped once its target is off the field or in another deployment, i.e. was knocked out during the pass — whether the 5-tier revive or 不屈's 立刻重新部署 redeployed it or 埃芒加德 / M3茧甲 revived it in place. The marker side is unchanged since 0.1.0 (PRTS describes only the target): a marker off the field gives no further mark, one knocked out and back in the same pass still gives its marks. The chain above: 浊心斯卡蒂's five marks resolve, the three members fall once each and revive, every later mark on them is cancelled; all five 阿戈尔 stand and only 隐现 falls. The 3 revives are still spent at t = 0, as officially. When nothing brings a devoured unit back during the pass (no 5-tier revive, 不屈 redeploy, 埃芒加德 or M3茧甲), the devour resolves exactly as before.
  • 联防 (#33 item 3, not changed): the members knocked out twice at t = 0 were still down at the end of the 45–60 s own combat (70 s redeploy), so they entered every 联防 knocked out (forced out, §19.3) and the chain there formed without them. After this fix that is far rarer: with the 5-tier, the members the devour knocks out (up to 3) stand again and enter 联防 down only when the fight knocks them out once more (with 3–4 members no 阿戈尔 revive brings them back, as before). The order of the 联防 forced exit and the battle-start devour stays ASSUMED (§19.3: forced exit first); no source gives it.
  • 乌尔比安 S3 (#33 item 2) — seen: "乌尔比安3技能结束返回会清空阿戈尔阵营增益,转职突袭后疑似不会返回(未转职时三技能会在原地留下一个图标,转职后无图标)". Official (PRTS 乌尔比安 S3 备注; the chess skill is skchr_ulpia_3, skill range 6-1 = his own tile and the 6 ahead): the anchor's target is "自身所在地块(仅阻挡敌人时)>技能范围内距离自身最近的包含敌人的地块>技能范围内距离最远的地块"; the 【移动】 goes to the landing tile > the tile beyond it > his own tile, and "若目标地块不为当前地块,会在原地部署一个从不混淆的方向"; "【移动】后仅继承下列效果:技能进度、第二天赋叠加层数". Cause: the anchor search started one tile ahead, so while he blocked — typical right after a 突袭 landing next to the enemies — he threw ahead, walked off the enemy he blocked (the blast could miss it — with an enemy one tile ahead its 1.8 radius still reaches the one he blocks) and left the marker. Now: while he blocks an enemy the anchor lands on his own tile: the blast (radius 1.8) hits what he blocks, he stays put, no marker, nothing to return from. His own tile is a candidate only while he blocks ("仅阻挡敌人时"): without a blocked enemy the search starts one tile ahead, exactly as in 0.1.1, so a flyer over his tile does not keep him there. Considered and not taken: reading the own tile of the 6-1 range as distance 0 for the second rule. Measured with the triage's batch (8 real stages × 5 round templates × 2 seeds, 突袭 through 变形同构体 + 突袭手雷), before the 突袭 landing change (#51, §22.2) is merged: with 突袭, 56 of 84 casts used to move him off his landing tile, now none does — 82 of the 84 came while he blocked, the other 2 (act2 m03, tile (10,8)) have no standable tile ahead (pre-existing); without 突袭, 64 of 80 casts still move him, as before.
  • 乌尔比安 S3 — not reproduced (the triage, real data): no 阿戈尔 gain is cleared on the return. The devour's base ATK / block (bond:egir:devour, e.g. +399 ATK, +1 block), the 阿戈尔 HP and the 血脉的哺养 stacks are there before, during and after the S3 (Battle.relocate keeps every buff); what ends is the S3's own +40–60 % HP / +200–230 % ATK. With 突袭 he does return and the marker is drawn, both on the landing tile next to the enemies, not on his board tile (8 stages × 5 round templates × 2 seeds); with this fix a cast while he blocks there leaves no marker at all, as officially. Deviation, kept: officially the 【移动】 keeps only the skill progress and the second talent's stacks, so the official game drops the devour gains on the move; the remake keeps them through the 【移动】 and the return.
  • ASSUMED: (1) a marker revived during the pass keeps the base ATK / block it gained at marking (a persistent buff, as through every knock-out since 0.1.0). (2) With the anchor on his own tile, 乌尔比安's 【移动】 does not try "the tile one beyond the landing" (he would otherwise step one tile forward).
  • Tests: test/content/bonds_core.test.js (the chain in a normal and a 联防 battle: one knock-out and one revive each, all five standing, the next knock-out final; a marker revived by the 5-tier revive still gives its marks; a target 埃芒加德 revived in place takes none — one revive spent, not two; a target 不屈 redeployed takes none and stays standing), test/content/kits_t5.test.js (乌尔比安 S3 while he blocks: the anchor on his own tile, no move, no marker; a flyer over his tile, not blocked: the anchor ahead and the move, as in 0.1.1).

22.4 活性源石 stays on an enemy after it leaves the tile (#33 item 6) — server/sim/content/devices.js (touchInfection, enterTerrain, terrainFor, tickMire, deepWaterDamage), content/enemies.js (rebirthCleanse, kitDeepsea), tools/build-data.mjs (special.mire)

  • Seen (public issue #33, other part 3: 「红地板图敌方单位经过后,红地板附带的效果会消失」): on 战场#04 an enemy lost the red floor's effect the moment it walked off it. Measured on v0.1.1 (the real stage): an enemy on (10,6) from 15.67 s to 19.67 s lost 4 × 70 HP; 12 s later it still had 99720 / 100000 HP and no ATK / ASPD bonus.
  • Official: the red floor is 活性源石 tile_infection, the only one in the season's data — 战场#04 (act1autochess_m04) at (10,6), (11,6), (10,14), (11,14) and on the boss rows at (3,6), (4,6), (3,14), (4,14); level blackboard damage 70, atk 0.2, attack_speed 20, duration 300. PRTS 特殊地形 tile template (特殊地形/trapper/tilesformat.json): "部署于其上的我军和经过的敌军在{duration}s内每秒受到{damage}真实伤害,攻击力提升{atk×100}%,攻击速度增加{attack_speed}" (main-story stage pages: "在五分钟内"). The crisis-contract tag global_tile_infection_1 「目标:可控感染」 (PRTS 切尔诺伯格 6区废墟/历史合约): "踏过活性源石地块的敌人不再持续损失生命值" — without it an enemy that has walked over the tile goes on losing HP. Same-name effects do not stack (PRTS 作战机制: "同名buff的默认叠加策略buff只能表现出一个"). Its damage is 环境伤害 (PRTS 自然环境 lists 活性源石). A 重生 clears every buff but a whitelist (PRTS 特殊机制 §重生 "清空自身身上除白名单外所有Buff"; its 非首次标记 row: "如无特殊说明,也默认同常规Buff一样可被重生清除").
  • Cause: devices.js read duration as a lifetime of the tiles (terrainFor switched them off after 300 s of battle time) and tied the effect to standing on the tile (enterTerrain removed it as soon as the unit's tile changed).
  • Now: every tick a unit is on the tile (allies by their tile, ground enemies by their rounded position; flyers, hovering / levitated and hidden units never touch it, as before), touchInfection gives it the timed effect terrain:infection for duration s — 70 true damage per second, ATK +20 %, ASPD +20 — or, when it already carries it, gives it back its full time. Leaving the tile no longer removes it: an enemy that crossed it drains 70/s with ATK +20 % / ASPD +20 for 300 s from its last contact — longer than any own-field or 联防 battle lasts (≤ 230 s of battle time). All 12 ground legs of 战场#04 (own field, 联防 field, boss field) cross one of these tiles, so every ground enemy there carries it once past the tile. One effect per unit: a second pass or the next tile of a pair restarts its time and never adds a second effect (ATK +20 % once, 70/s, no extra tick). The tiles never switch off — the old lifetime only ever ran out on the boss / hidden fields (no time limit), at 300 s. An operator deployed on it is always in contact, so it drains until it is knocked out or withdrawn, past 300 s too (this follows from the ASSUMED refresh rule; v0.1.1 drained it the same way within the first 300 s). An operator moved off the tile (Battle.relocate: 乌尔比安's S3 move and return, 夕's 小自在) keeps the effect for its time — 乌尔比安 landing on (10,6) carries it home for 300 s (measured by the review); leaving the field (knock-out, withdrawal, 突袭's jump) drops it with every other buff. A 重生 (rebirthCleanse: 逐火 embers, 假想敌:再生's 傀儡, 锏, 守墓石像 …) clears it like any buff, and contact gives it again while the enemy stands on the tile. Kept through the 重生 (the first version of this fix), its 1 s ticks — each a hit of a husk's 特殊生命值 — killed a 深池逐火战士 ember that had crossed (11,6) at ~6 s instead of letting it stand up at 1 + 10 s (护卫's 火灰 at ~11 s, 傀儡 at ~16 s; found by the review). The damage itself is unchanged: true damage no unit deals (无来源), tagged terrain (环境伤害), a damage instance, not a 流失.
  • ASSUMED: the time counts from the last contact (it is refreshed every tick on the tile) and the per-second rhythm runs on through a refresh — hence an operator on the tile drains past 300 s; contact gives the effect again to an enemy reborn on the tile; not 持续伤害 (no dot tag: PRTS 伤害分类's list of BUFF damage names 深水区, 源石污染区 and 活性源石风暴 but not 活性源石, so 锡人's 凋敝魂灵 does not raise it).
  • Checked: no other terrain reads a duration — 沼泽 (layers, cleared on leaving), 深水区 (sea_drown[enemy] has none), 烟雾 and the blowers; 沼泽's interval and 深水区's damage kind were wrong, though (below). Nothing relied on the old lifetime: the client draws the tile glow from the stage map (it never went out), terrain effects have no status icon, the enemy abilities that care about the tile (萨卡兹枯朽前锋's arts attacks, “灵幛”, 孽罪奇美拉) read the tile they stand on (onTerrain), and 联防 carries no effects (HP, SP and a running skill only). 纠缠藤蔓 (enemy_2052_smgia, a 悬赏 enemy: "受到来自自然环境的伤害时,自身在短时间内获得高额脆弱"; content/enemies.js reads the terrain tag) that has crossed the tile stays ×2 fragile for 300 s and the tile's tick hits it for 140/s — official, PRTS 自然环境 listing 活性源石 as 环境伤害.
  • Other terrain found while fixing #33 item 6 (the review):
    • 沼泽, official (PRTS 沼泽控制 备注; skill_table sktok_mire): a unit in the mire "每秒将会触发一次" its 【陷入沼泽】 (the skill charges in maxChargeTime 1 s): an enemy gains 1 layer of ASPD −5 % and move speed −5 %, "若其重量大于等于3,改为获得2层"; any other unit 1 layer of ASPD −5 % only; at most 10 layers (max_stack_cnt); "上述减益于单位不再位于沼泽之中时解除". The skill's value 3 is that 重量. Cause: build-data and the devices.js fallback read value as the interval (3 s): 10 layers after 27 s for light and heavy enemies alike, and operators got the move part too. Now: special.mire has intervalSec 1 (the skill's charge time) and heavyWeight 3 (value); tickMire triggers on entering and then every second — 1 layer, 2 for an enemy of 重量 ≥ 3 (s.massLevel at the trigger), operators the ASPD part only, at most 10, cleared on leaving: 10 layers after 9 s (重量 ≥ 3: 4 s). ASSUMED the first trigger comes on entering (as before). A layer stays −5 ASPD (the remake's reading of the fraction, aspdOf).
    • 深水区, official (PRTS 涨潮控制 技能3 深水: "【水蚀】的敌方单位每秒受到40点无来源真实持续伤害(不属于环境伤害,不会触发受击回复)"; PRTS 自然环境: 涨潮's damage to enemies "并非环境伤害"; PRTS 伤害分类 lists 深水区/涨潮水蚀 as BUFF damage). Cause: the deep-water tick shared the terrain tag with 活性源石, so 纠缠藤蔓 turned ×2 fragile in deep water (360 HP lost in 5 s instead of 200) and 锡人's 凋敝魂灵 ("受到的持续伤害提高") never raised it. Now: deepWaterDamage — 无来源 true, canDodge: false, sourceless, noSp, tags dot / periodic / deepsea, no terrain. The 免疫水蚀 swimmers (潜水员) cancel only deepsea / drown damage, not 环境伤害.
  • Tests: test/content/tokens_devices.test.js — 活性源石 (the operator and the enemy standing on it), an enemy keeps the effect after leaving the tile (20 s later: 70/s, ATK, ASPD, the time left), one effect per unit (two passes give one effect that ends duration s after the last contact, 70/s while it lasted; an operator drains for 3 × duration, keeps the effect after Battle.relocate and loses it duration s later), a 重生 clears it (a 深池逐火战士 that crossed the tile stands up from its ember after 1 + 10 s), 沼泽 (the data; operators ASPD only; 重量 1 vs 3: 1 vs 2 layers a second, at most 10; cleared on leaving), 深水 (the tick's tags; 纠缠藤蔓 not fragile); test/content/enemies_bosses.test.js (潜水员: the deep-water tick is cancelled, 环境伤害 is not).

22.5 Emotes and guide pages without a local client; deployment docs (#42); the English title (#38) — tools/assets/plan.mjs UI_EXTRAS / GUIDE_PAGES, data/assets.json ui['emoticon/<dir>/<picId>'] / ui['guide/<key>'], data.js artUrls / nextArtUrl, ui/emotes.js emoteArtUrls / EmoteArt, ui/guide.js guidePages / GuideHost, tools/setup.mjs LOCAL_ART_FALLBACK / LOCAL_ART_COPY_HINT, tools/doctor.mjs, README, docs/DEPLOY.md §6, server/index.js banner, package.json name

  • #42 seen: "游玩时左下角的六页表情全部显示为默认图标" — a Linux server deployed from source (git clone, setup with the asset download, Node 22); copying public/assets/local/ and data/local-assets.json from the release bundle fixed it. Real: both are extracted from a locally installed client (§13; setup extracts only when it finds one), and the emote art and the 玩法说明 pages came from there only, so a source deploy without the client drew the neutral glyph for all 36 emotes and the official tips text instead of the 19 pages. README, DEPLOY.md, setup and doctor said the local art did not matter ("不影响游戏", "其他功能不受影响") — wrong: the 3D board, part of the official HUD art and two enemy models fall back too.
  • Source: the mirror the pipeline already uses (ArknightsAssets2 cn, assets/dyn) has all 55 files — the six theme bundles' ui/emoticon/theme/[uc]<themeId>/icon/<picId>.png (the 36 battle sprites, keyed by display_meta_table picId) and arts/guidebookpages/[pack]autochess/autochess_{home 1–9, shop 1–6, handbook 1–4}.png. Checked 2026-10-03: every URL answers 200 on raw.githubusercontent.com and on the jsDelivr fallback; the same pictures as the local extraction, each at the size of its local copy (all 55 compared; the pages 1024² squashed — still shown at 16:9), pixel differences only from the platforms' texture compression.
  • Now: the asset plan downloads them like any other UI sprite (UI_EXTRAS, built from shared/constants.js EMOTE_CATALOG and GUIDE_PAGES — a test keeps GUIDE_PAGES equal to guide.js GUIDE_CHAPTERS), keyed by the data/local-assets.json group and name: ui['emoticon/<dir>/<picId>'] → /assets/ui/emoticon/<dir>/<picId>.png, ui['guide/<key>'] → /assets/ui/guide/<key>.png (55 files, 21.3 MiB; the download ≈ 270 MB, was ≈ 250 MB). The committed data/assets.json lists them (regenerated by a real fetch-assets run: only these 55 entries, hash and stats changed), so npm run setup — which the start scripts run on every start — on an install made before them finds 55 files missing and runs fetch-assets; §21.25's conventions hold: ledger, retries, the jsDelivr fallback, and a run where they fail keeps the committed manifest (shrink guard), so the next setup tries again. The client looks both manifests up by the same names (data.js artUrls): the local-client picture first, then the mirror copy; EmoteArt and the guide viewer move to the next URL when an image fails to load (nextArtUrl: a copied data/local-assets.json without its files falls through to the mirror copy); when none is left they keep 0.1.1's fallback — the glyph, and for a 玩法说明 page the official tips text (guideStage), also when data/assets.json lists the downloaded pages but the files are not on disk (a git pull and restart without setup, an older asset folder, a failed download the shrink guard kept); the chapter tabs, dots and keys still reach the other pages. localAsset stays local-only.
  • Docs now: README, docs/DEPLOY.md (new §6 本地客户端素材, the 排错 rows), docs/PLAYING.md, docs/ASSETS.md and the setup / doctor messages say what still needs the client — the official 3D board (→ the 2D board with procedural tiles), part of the official HUD art (the emote button and panel frames, the pause plate, the equip-replace dialog, the loadout screen, the team status and miss tags, module type icons … → CSS look-alikes, icons or text) and 灼热 / 炽焰源石虫's models (→ the tinted web alias, §21.14) — and README, DEPLOY §6 and doctor that a server without the client can copy public/assets/local/ and data/local-assets.json from the release bundle of the same version (both are read per request: no restart); setup's row, which the launcher prints on every start, only points to DEPLOY §6. The Docker parts are unchanged (deferred by the owner).
  • Not done here: the mirror also has most sprites of the three autochess UI bundles that the local ui/battle / ui/common / ui/outer groups hold (by name 377 / 389, 137 / 160 and 659 / 659 under ui/autochess/[uc]autochess{battle,common,outer}/**; the emote panel frames, matte_pause and bg_miss_enemy checked at the local sizes), so the HUD row of DEPLOY §6 could shrink in a follow-up.
  • #38, the English title — official: EN client data, activity_table basicInfo.act2autochess.name = "Stronghold Protocol: Alliance" (CN 卫戍协议:盟约; ArknightsAssets/ArknightsGamedata en/gamedata/excel/activity_table.json, 2026-09-29 — Kengxxiao/ArknightsGameData_YoStar stops at EN client 31.5.80, 2025-11-13, before this season, with act1vautochess "Stronghold Protocol" only). Now: the README title and English summary, the server start-up banner and the package name (stronghold-protocol-alliance, package.json and the lockfile) say Alliance instead of "Covenant"; the Chinese title 卫戍协议:盟约 and the repository name stay. "Covenant" remains only in 新约能天使's official appellation ("Exusiai the New Covenant") and its buff key angel2:covenant. Only the title is settled here: #38 itself (a full English UI) stays open.
  • PR #27 follow-up: tools/doctor.mjs's comment now says directly that tun0 / tap0 are matched first by VPN_IF (substring tun\d / tap) and classed as vpn — it spoke of no longer listing ^tun\d, which master's VIRTUAL_IF never had; comment only.
  • Tests: test/assets.test.js (the 55 plan entries and URLs, a rebuilt manifest, the committed manifest lists them), test/ui/localArt.test.js (the lookup order, a server without the client, guideStage, GUIDE_PAGES = GUIDE_CHAPTERS), test/fetch-assets-shrink.test.js (a failed mirror run keeps the manifest), test/static-local-art.test.js (the docs and messages), test/version.test.js (the English title), test/doctor.test.js.

22.6 钩索师 and 推击手 on 高台 (#32 item 4) — tools/build-data.mjs PLACE_ON_RANGED_RE / branchTraitText / validateAll, chess.json placement, match/board.js positionClass / basePositionClass, ui/gameLogic.js piecePosition, match/bot.js isBlocker / planLayoutSteps, tools/balance.mjs

  • Seen: public issue #32 item 4 — "哥蕾蒂娅无法部署到高台". 0.1.1 refused every 高台 tile for her (server BAD_TILE; the client lit no 高台 and said 近战单位只能部署在地面), and the same for 崖心 (钩索师) and 见行者 (推击手), normal and elite.
  • Official: the branch trait of both 钩索师 chess (歌蕾蒂娅 chess_char_4_12, 崖心 chess_char_2_03) reads 「技能可以使敌人产生位移\n可以放置于远程位」, the 推击手 见行者's (chess_char_3_07) 「同时攻击阻挡的所有敌人\n可以放置于远程位」 — character_table description (their trait is null: the branch's own text), position MELEE; act2autochess charChessDataDict sets only status, bonds and garrisons for them. PRTS 新人入门 (branch table): 钩索师 and 推击手 "可部署在高台和地面". PRTS 卫戍协议/帮助 §战斗部署: "所有行动内远程干员可部署在近战位", and 部署效果 do not apply in this placement — "携带Y模组的教官仍无法部署至高台位". That 教官 ability is a module talent ("可以额外部署在远程位", battle_equip_table uniequip_003_pallas / uniequip_002_doberm / uniequip_002_rdoc, blackboard buildable_type 2), a deploy effect; the 钩索师 / 推击手 line is the branch trait itself — no talent, no blackboard, and every module of the three keeps it (HOK-X, HOK-Y, PUS-X). No Y-module 教官 is in the pool (诗怀雅 has INS-X). No source states the 钩索师 case for 卫戍协议 in so many words: the remake reads the branch trait as still applying ASSUMED; the old "melee never on ranged-only tiles" was itself an assumption (research 03 C2).
  • Cause: build-data copied position MELEE and nothing read 可以放置于远程位; board.js positionClass mapped MELEE to 'melee' (melee tiles only), so PlayerState._legal refused the 高台 (g.move → BAD_TILE), and the client mirror (gameLogic.js piecePosition / tileAllows) agreed.
  • Now: build-data sets placement: 'all' on a chess whose branch trait without module (branchTraitText: the trait candidate's text, else the description) contains 可以放置于远程位 — exactly 崖心, 见行者 and 歌蕾蒂娅, normal and elite (6 records, the only data change; validateAll allows it on a MELEE record only; module-only text never counts). positionClass returns 'all' for them, so the server takes them on every deployable tile — g.move, swaps, a merge's elite (mergeTile), the terrain re-check (_evictIllegal), invariants and audit —, and the client's piecePosition 'ALL' lights the 高台 while dragging; every other melee unit keeps the refusal and its text. The battle keeps position MELEE: on a ground tile they block as before; on a 高台 (unit.ground false) they block nothing and attack from there — the sim needed no change (act2 m01, one 萨卡兹枯朽前锋 on the lower lane, 40 s: 歌蕾蒂娅 on (10,4) facing it 2 hits, no block, on (9,5) 12 hits, blocking; 崖心 and 见行者 alike; no errors).
  • Bots read the record's own position (basePositionClass): they still count these units as blockers (isBlocker; with the widened class they would not, and on act2 m01 the planner put 歌蕾蒂娅 and 崖心 on the 高台 as non-blockers, leaving 见行者 the only blocker on the road) and plan them on the ground tiles, as in 0.1.1 ASSUMED: offered the 高台, the planner still chose road tiles in the cases measured, but the lineup's blocker count assumes they block and a greedy 高台 pick could take a ranged dealer's tile — not benchmarked. tools/balance.mjs uses the same blocker test. Tokens are unchanged; the sim's automatic placements (the 突袭 landing tile, summon / device tiles) still treat them as melee, ground tiles only ASSUMED.
  • Players notice: 歌蕾蒂娅 can be dragged onto a 高台 (PLAYING §4); there she attacks but blocks nothing. 崖心 and 见行者 get the same rule but are hidden chess this season (isHidden, never in the pool, the shop or a 突变细胞 roll), so only 歌蕾蒂娅 is reachable in play; 见行者's elite PUS-X refund (half the DP of a redeploy onto a ranged tile, kits/tier3.js !unit.ground) is therefore moot.
  • On a 高台 she is not a 地面干员 for the effects that read the tile (unit.ground: the 战栗维式重锤 proc, 休谟斯 回收利用, 不屈, the 只攻击地面单位 enemy, 污染秽蚀's 高地 rate), like a ranged operator up there; position-based effects keep her 近战 (精准 tier 2, 诗怀雅's talent) [ASSUMED: the official texts may mean the position class by 地面干员].
  • Normative lines rewritten with it: §3 (placement), §6.6 (the bot); research 03 C2; DATA §2 (placement).
  • Tests: test/match/feedback1-placement.test.js (#32-4: the data rule, cross-checked against character_table when the cache exists; g.move onto (10,4) / (11,4) / (12,4) of 战场#01(下半) for the six records; a 重装 refused with its text; ranged unchanged; client highlights = the server's tiles; swaps; the merge's elite on the 高台; the battle input and the sim on / off the 高台; bots), test/match/board.test.js (positionClass / basePositionClass), test/ui/gameLogic.test.js (the client mirror).

22.7 史尔特尔's 余烬 holds 禁疗 (GitHub #52) — kits/tier5.js (史尔特尔 余烬 / S3), sim/damage.js heal, sim/buffs.js (healFree), render/style.js STATUS_ICON, render/textures.js STATUS_KEYS / drawStatusIcon

  • Seen (GitHub #52 「史尔特尔进入锁血后仍能被治疗」): during the 8 s of 余烬 medics kept healing her. v0.1.1 measured: a 赫默 beside her healed her twice for 738 HP; with a 50 % HP 深巡 also in range, 赫默 spent 1,107 of its 1,845 HP of healing on her — on a unit that leaves anyway.
  • Official (checked on PRTS 2026-10-03): chess talent 余烬 (chess_char_5_07_a / _b) 「受到致命伤害时持续使生命值不低于1,8秒后强制退出战场」 (bb surtr_t_2[withdraw].interval 8); PRTS 史尔特尔 天赋备注 "持有不死的情况下不会触发此天赋" / "触发本天赋后,获得禁疗与不死" / "强制退出战场视为撤回干员"; PRTS 异常效果 禁疗 (HEAL_FREE) "无法成为治疗类能力的目标,且受到的治疗量变为0 ※…增减生命回复速度或生命回复速度(百分比)属性的效果不会被识别为治疗类能力"; PRTS 史尔特尔 技能3 备注 "生效后立刻治疗自身当前最大生命值100%的生命值(无视禁疗)".
  • Cause: the talent only prevented the blow and scheduled the retreat. Nothing marked her 禁疗, so every heal pick (Battle.injuredAlliesInKeys and the kits' own) chose her as the lowest HP ratio and heal() let the heal through. The sim's noHeal (the 禁疗 summons, 孤立, 武者 / 收割者 / 不屈者, 艾丝黛尔 S1, 烛煌's 倒地) stops only heals from other units (!self in damage.js heal), the "不能被其他…治疗" of those traits.
  • Now: the trigger adds surtr:ember = flags noHeal + healFree with the status 'healFree' (a green cross struck through in the unit's status row) until she leaves; the retreat drops it. healFree in damage.js heal: no heal reaches the unit, its own included (休眠子裔's heal on attack, …), except an HP-regen attribute tick (regen: the engine's natural regeneration from hpRegen / hpRegenRatio, e.g. 安洁莉娜's 兼职工作) and a heal flagged ignoreHealFree (her S3's start heal, "无视禁疗": S3 starting inside the window heals her to full). Every heal pick already skips noHeal units, so medics heal the next injured ally. 不死 (a later lethal blow inside the window changes nothing and does not restart the 8 s) and the retreat at 8 s are unchanged. Other noHeal users are unchanged: the summons' 禁疗 (tokens.json abnormal healFree) keeps noHeal only — no self-heal reaches them but 伺夜 S2's 恢复 of its 禁疗 狼群, which the skill text grants.
  • Measured (the same layout): 0 heals reach her (v0.1.1: 2 heals, 738 HP); 赫默 heals the 50 % 深巡 instead (1,107 HP in 12 s); she leaves 8.03 s after the blow.
  • ASSUMED: on the same first lethal blow 余烬 (fatal −60) runs before 坚固维式重锤's lock (PRIO_REVIVE −100), so the hammer never starts while 余烬 is unused (PRTS gives no order); the status lasts until the retreat rather than a fixed 8 s. Known deviations, outcome-neutral (she is 不死 and leaves at 8 s anyway): 余's 闲云隐市 and 天马之盔's 8 %/s use hp_recovery_per_sec_by_max_hp_ratio, the 生命回复速度(百分比) attribute that PRTS 禁疗 exempts, but they run as kit heals (battle.heal) here, so 禁疗 stops them on her (moving them to the regen attribute would also change the bleed removal and heal-bonus hooks of other units); the forced exit's 「返还部分部署费用」 DP refund is not modelled (no retreat in the sim refunds DP); a chain healer's bounces skip her (ai.js doHeal chain filter, review of #52).
  • Tests: test/sim/feedback2-surtr-stealth.test.js (the medic beside her, her own heals / regen / S3's start heal, the status events), kits_t5, items, playtest6-combat (the 禁疗 summons), render/tiles / boardart / fxproj (the status atlas).

22.8 An enemy's 隐匿 returns 3 s after a block; the 深池逐火 embers (#43 follow-up, players' "the stealth monster revives forever") — sim/Battle.js _stealthSwitch / _unblock / releaseBlocked / foesInRadius, sim/ai.js enforceBlockCapacity, sim/targeting.js enemyStealthed / stealthOffKey / canTargetEnemy, sim/snapshot.js flagsOf, sim/constants.js STEALTH_RESTORE, sim/buffs.js (stealthOff), content/enemies.js (stealth, STEALTH_RESTORE_BY_KEY, husk, kitInvisShield, kitShadowKiller, kitJazz, kitBoneSpike)

  • Seen: §21.4 left "隐匿 returns at once after a block (PRTS: 3 s)" ASSUMED; v0.1.1 hid an enemy again one tick after its block ended (a 潜伏者 whose blocker falls: untargetable 0.03 s later). Players reported (via the owner) that the 深池逐火 "stealth monster revives forever and can't be killed": a warrior knocked out while blocked releases its block through the 重生's 无法阻挡, its blocker takes the next warrior at once, and when the 重生 ends the ember is not blocked again — 隐匿, out of reach of ranged operators and operator splash — walks on and stands up with full HP 10 s later.
  • Official (checked on PRTS 2026-10-03): 作战机制 §隐匿 "对于绝大部分可隐匿的敌人而言,在被我方单位阻挡后会解除隐匿,不被阻挡的3秒后重新进入隐匿。这是敌方单位的隐匿所拥有的特殊机制,我方干员并不会因为阻挡而解除隐匿" and "少部分的隐匿表现上与我方干员相同…可能不具有这样的特殊机制"; §隐匿与Buff的关系 "在被阻挡时开关会被关掉从而失去隐匿,阻挡状态解除后3s开关重新被开启而恢复隐匿"; 异常效果 隐匿 "与'阻挡时解除'没有直接关系,该机制通常由其他效果实现". The enemy pages' 天赋 "隐匿(解除阻挡N秒后恢复)" (PRTS text search): 0 s on 21 enemies — here 业余竞演者, 节日爵士乐手, 假想敌:骨刺, 流泪小子 (both), 访问团强攻冠军, and the veil of 清明 / 堂皇 ("获得隐匿(解除阻挡0秒后恢复)"); 1 s on 家族灭迹人 / 家族暗影灭迹人; no page names another time. Every other stealthy enemy here has a plain 隐匿: 潜伏者, 隐形弩手 (+组长), 隐形术师, 山海众头目 / 秘使, 重弩突袭者, 架桥船工 / 扶桥老手, 深池伙友影刃精英, 潜水员 (in water), 锏's second form and the 深池逐火 embers (PRTS 深池逐火战士 / 精锐战士 / 护卫 "获得隐匿、缴械"). 山海众头目 / 秘使 天赋 "该隐匿每次生效后自身获得强击标记(不可叠加)", 技能 破隐一击 "仅持有强击标记且被阻挡时可触发…技能开始时消耗强击标记".
  • Cause: targeting.js canTargetEnemy, snapshot.js flagsOf and Battle.foesInRadius read blockedBy live, so the 隐匿 came back with the block's end.
  • Now: every block release — Battle._unblock, releaseBlocked, ai.js enforceBlockCapacity (a blocker's death, retreat or stun, a block-count drop, a push, 无法阻挡 / 浮空 / 恐惧, a teleport) — calls Battle._stealthSwitch(e): each 隐匿 source buff the enemy holds gets a stealthOff:<source key> buff for its restore time, constants.js STEALTH_RESTORE (3 s) or the source's data.stealthRestore (content/enemies.js STEALTH_RESTORE_BY_KEY: the 0 s / 1 s enemies above; the veil 0 s). targeting.js enemyStealthed(e) — 隐匿 flag, not revealed (反隐), not blocked, and some source not switched off — is the one test for ally targeting (canTargetEnemy), operator area damage (foesInRadius), the b.snap stealth bit (drawn solid meanwhile), 节日爵士乐手 (attacks only without its 隐匿), 假想敌:骨刺 (3 targets only in 隐匿) and 山海众头目 (the mark comes at the spawn and whenever its 隐匿 turns on again, the next blocked attack spends it: a new block inside the 3 s gives no second ×2 hit — until 0.1.2 every block did). A new block inside the window lifts the 隐匿 again and its end restarts the window; a 反隐 that ends outside a window hides it at once, as before; a veiled 潜伏者 hides as soon as its block ends (the veil) and, if the veil runs out inside its own 3 s, is revealed until they end. Our operators' 隐匿 / 迷彩 are untouched (allies get no switch).
  • The 深池逐火 embers (all three, husk() with stealthy): the knock-out releases the warrior's block at once (_unblock right after the 重生's 无法阻挡, as applyStatus does for a status) once the ember's 隐匿 is on, so the ember is revealed until 3 s after the knock-out — the 1 s 重生 stays 无敌 and untargetable, then about 2 s in which ranged operators and operator splash can hit it even though its blocker took the next warrior. An ember knocked out while not blocked is 隐匿 as soon as its 重生 ends (the switch only follows a block).
  • Measured (before = the v0.1.1 sim, after = this change; the flat stage with its row-9 lane, and real rounds on 战场#01 built like the match — an official wave with the TIMES pick, full content): the integrator's seven lineups (艾丝黛尔 / 角峰 / 隐现 / 幽灵鲨 against 1–4 战士 or 护卫): identical — 艾丝黛尔 alone against four 战士 2 s apart stays 0 / 4 because only one warrior is ever knocked out, and no lineup there knocks out a 护卫. 28 runs of a blocker + 2–3 ranged / splash operators against 4–6 战士 / 精锐战士 0.5–3 s apart: 81 → 84 of 126 killed, stand-ups 12 → 7 (23 runs unchanged, 4 one more kill, 1 one fewer — its blocker fell in the changed fight). Real rounds (R3 / R5 精锐战士 pick, R9 / R11 护卫 pick, three lineups up to the tier-4 four of §21.4's test): unchanged (R3 7 / 7, R5 5 / 16, R9 0 / 18, R11 0 / 28 for the tier-4 lineup; 0–1 for the weak ones). Why so little: in real rounds most knock-outs happen while the warrior is NOT blocked — ranged operators finish it on the way after it passed a full blocker (R5, tier-4 lineup: 10 of its 18 knock-outs, and 10 of the 13 embers that stood up again) — so its ember is 隐匿 at once, which is official; an ember freed by its own knock-out gets about 2 s after the 重生 and still needs 5 separate hits (火灰 10). What beats embers, officially: a free block slot where they walk (a second blocker behind the first), 反隐 (银灰 鹰眼视觉, 伊内丝 影哨, the SPT-X modules, 信仰搅拌机's module), or many fast hits.
  • ASSUMED: at the knock-out the 重生's cleanse comes first, then the ember's 隐匿 with the block-end switch, which outlasts the 重生 (PRTS documents neither the order nor whether the switch survives a 重生); 深池伙友影刃精英 and 锏's second form (PRTS writes their 隐匿 as the plain 异常效果 with no restore note) follow the general 3 s, not the "表现上与我方干员相同" kind; one switch per 隐匿 source — with several sources the enemy hides as soon as any is back on; a push or a teleport (DISAPPEAR) ends a block like any other release; a warrior released by its blocker less than 3 s before its knock-out leaves an ember that is 隐匿 straight after its 重生 (the window is opened by the knock-out's own release only — 4 of 55 knock-outs in the measured rounds, no outcome changed when prototyped).
  • Tests: test/sim/feedback2-surtr-stealth.test.js (the 3 s window and its consumers, a new block inside it, the release paths, the 0 s / 1 s enemies and the veil, our operators, 山海众头目's mark, the three embers' window, an ember knocked out unblocked), enemies_bosses, feedback1-transform, feedback1-followups, combat, playtest5-targeting.

22.9 荒芜拉普兰德 S3's drones attack (#32 item 7) — kits/tier6.js whitw2 (WHITW2_SPREAD / WHITW2_CHASE / WHITW2_REAPPEAR_SIDE / WHITW2_ORBIT, releaseDrones / pickTarget / flyDrone / circle)

  • Seen (GitHub #32, item 7): 「易拉三技能浮游单元无法造成伤害,只能造成三技能自带dot伤害」 — 荒芜拉普兰德's S3 终幕·浩劫 dealt only its once-per-second area damage.
  • Official: PRTS 荒芜拉普兰德 S3 备注, 技能流程: ① for 1.3 s after the cast or after a drone is added ("开启技能/补充浮游单元1.3s内") the drones spread evenly outward from her, one along her facing (初速度 0.1, 加速度 1.9, 最大速度 2.0); ② each then locks the target "距离自身最近>距离本体最近" anywhere on the field and flies at it (初速度 2.0, 加速度 1.0, 最大速度 4.0, 转向速度 1/6 per frame; a target that leaves or turns unselectable on the way → a new pick, the speed back to 2.0); ③ at the target it "锁定目标持续进行攻击,此状态下的攻击行为同正常浮游单元"; when that target leaves or turns unselectable it reappears at a random point of the 1.5-side square around it and picks again; with nothing selectable it circles (radius 0.9, 1.0 线速度, counter-clockwise, the circle on its left). Also: "该技能释放的浮游单元造成的伤害不属于普通攻击/技能直接伤害"; the area damage / slow has radius 0.9, does not stack and carries 头狼's 沉默. PRTS 分支特性信息 驭械术师: a drone starts at 20 % of the operator's ATK, +15 per hit on the target it hit last, up to 110 %, back to the start on a different target; drones released by a skill "不受缴械类效果制约" — the same note says they "造成技能直接伤害", which S3's own 备注 overrides for this skill. skill_table skchr_whitw2_3 at the chess levels (Lv4 / Lv7): atk +0.45 / +0.6, attack@cnt 2, attack@times 1.3 (PRTS ①'s 1.3 s), attack@move_speed −0.3 / −0.4, attack@magic_atk_scale 1, attack@fear 2, attack@range_radius 0.9, attack@projectile_turn_speed 5 (× 1/30 s = PRTS's 1/6 per frame); trait init 0.2 / 0.35 (elite: module 狼时), delta 0.15, max 1.1; 头狼 every 20 s: cap × 1.1 (PRTS 备注: 121 %), 2 s 沉默, 数量+1.
  • Cause: the S3 spec switched her attack off for the whole skill (attack: { noAttack: true }) and its tick only moved the drones (at attack@projectile_move_speed, 1 tile/s), feared a target the drone came within 0.9 of and dealt the area hit; no drone ever attacked. The kit's header said so ("she stops attacking while they roam"). The drones also preferred enemies no other drone held — an ASSUMED rule PRTS does not have.
  • Now: her own attack stays off — every drone is out (浮游单元 +attack@cnt with her own one; +1 from 头狼 stage 3, and a drone added when stage 3 comes during the skill spreads along her facing). The drones fly ① ② ③ with PRTS's numbers (spread for attack@times s; the distance covered at the mean of the old and new speed, exact under constant acceleration; the reappearance point is drawn in her facing frame, so a battle turned with her direction plays the same). A drone that reaches its target fears it (attack@fear s) and stays on it; it attacks every attack interval of hers (her live ASPD) for ATK × its own funnel ramp (trait init, +delta per hit on the same target, the cap — raised by 头狼 stage 1 —, back to init on a new target), arts. That damage is neither a normal attack nor skill damage: no attack hook, isAttack / isSkill false, tag droneAttack — the 叙拉古 6 assassin chance ("对敌人攻击时") and the on-attack items skip it, 头狼's 沉默 ("造成伤害时") applies; 缴械 does not stop it. The once-per-second area hit and the slow are unchanged (around every drone, one hit per enemy; tags skill + drone). A wolf knocked out or withdrawn in the middle of a tick (a kill hook, say) deals nothing more in that tick. fx: droneLock (the wolf-eye mark) when a drone picks its target, drone at a drone for each of its attacks and once per second for drones still roaming; the client draws them already.
  • Distance to a huge enemy ASSUMED — the owner's decision of 2026-10-04: a drone measures every enemy's distance to its position (a huge enemy's centre, its 判定中心), so leader rounds stay close to 0.1.1. Considered and set aside: the sim's general convention for operator-side distance picks, the hit rectangle (body.js bodyDist: targeting.js:135 sortEnemyTargets 'nearest' / 'farthest', kits/tier3.js:86 enemiesAround, kits/tier6.js:1108 异客's storm chain, kits/tier6.js:2525 / :2549 溯光星源's bounces and links, kits/tier4.js:526 / :995; only the profession chain bounce, ai.js:257, takes the nearest by position) — with it a huge leader (胄, 管) was the nearest enemy of every drone around it and drew them all for the whole skill (胄 h07_01: her damage on the leader 106 309, on the other enemies 3 036, 2 deaths). A drone on a huge enemy sits at its centre; it reaches it on touching the hit rectangle.
  • Players notice: once a drone reaches a target that stays alive, its hits outweigh the area damage (3 parked 重装防御者, RES 0, the full 40 s: drone hits 38 411 of 54 640); against streams of weak enemies the area hit still does most of it (the flat stage's two lanes, 10 + 10 walkers, 40 s: drones 11 049, area 50 188 of 61 236). All drones may lock the same enemy — each takes the one nearest to itself — so against a cluster the area hit covers fewer enemies than before. Measured, old → new: parked, first 12 s normal 12 901 → 12 089 (drone hits 7 512 + area 4 578), elite 17 658 → 18 085; the whole 40 s 47 857 → 54 640 and 65 504 → 77 369; the lanes 15 481 → 61 236 (kills 1 → 14), elite 49 213 → 72 224 (9 → 18). Leader rounds (one elite 荒芜拉普兰德 per side, S3 at once, kept alive, 60 s; her damage on the leader / on the other enemies / enemy deaths, 0.1.1 → now): 假想敌:胄 (act1 h07_01) 32 467 / 80 186 / 9 → 84 880 / 44 391 / 6; 假想敌:管 (h07_03) 19 936 / 62 176 / 5 → 37 520 / 69 927 / 5; 假想敌:铳 (h07_02) 31 260 / 49 982 / 4 → 62 794 / 30 221 / 0; 卢西恩 (act2 h07_05) 37 957 / 58 512 / 6 → 75 094 / 40 881 / 2 (both of normal size: a drone that picks the leader because it is nearest keeps the lock, and a leader never falls); the heavy two-player 胄 scene of robustness.test.js (the review bench: 4 wolves per side, 120 extra enemies at ×20 HP, 60 s): the wolves' damage 1 315 038 → 1 291 567, on the leader 50 170 → 42 907, enemies killed 12 → 13. With the hit-rectangle distance it would have been 胄 106 309 / 3 036 / 2, 管 57 202 / 36 427 / 2 and the heavy scene 406 075 / 125 271 / 2.
  • ASSUMED: the centre distance above (the owner's decision); reading ① "补充浮游单元" as: after an addition only the added drone spreads, along her facing, while the others carry on; the turn rate (转向速度) is not modelled — a chasing drone heads straight at its target; a drone's attack clock runs all the time and its first hit lands on arrival when the clock is ready; the drones keep flying and attacking while she is stunned, frozen or silenced (PRTS only names 缴械); a drone reaches a huge enemy when it touches the hit rectangle and then sits at its position; the fear's source is her (they flee from her, as before); the area hit keeps isSkill (a skill DoT; PRTS's line speaks of 直接伤害).
  • Tests: test/sim/feedback2-whitw2-yu.test.js (spread and chase numbers; per-drone hits, ramps and intervals, no normal attack, the damage flags, 缴械; nearest-target picks, the reappearance square and the ramp restart, the circle; 头狼 stage 3 mid-skill; a wolf knocked out mid-tick; facing UP / LEFT / DOWN = RIGHT with a reappearance), test/content/kits_t6.test.js (the S3 test now has one enemy: a nearer one would draw every drone).

22.10 余 S2: leaders teleported, no pull effect without a pull, cast with an enemy on its x-1 (#32 item 1) — kits/tier6.js teleportEnemy / yu (skchr_yu_2), tools/build-data.mjs TRIGGER_DEVIATIONS / resolveTrigger / validateAll, data/chess.json skills[].trigger

  • Seen (GitHub #32, item 1): 「余二技能有伤害和特效,但不能拉怪」.
  • Official: PRTS 余 S2 厚礼上宾 (技能范围 x-1): "立即对周围所有敌人造成相当于攻击力100%的法术伤害并将其中的地面可达目标传送至自身位置"; 备注: "可达目标" = a target that can reach his tile by ground pathing, not through impassable tiles — "如果余或目标所在位置已经是一个不可通行地块(例如围栏、高台等),传送效果会因此失效"; "处于消失状态的/持有自缚的单位不视为可达目标"; 免疫传送 / 传送免疫 units are not teleported; the teleport is judged 0.13 s after the damage, the target is unblocked first and lands at a random point near one of the four corners of his tile. PRTS 卢西恩,“猩红血钻” and 假想敌:铳 (both sections): 传送抗性 无. Trigger: the 下半 TANK class row TANK | | | 0 | TAKE_DAMAGE and PRTS 卫戍协议/帮助's 重装 strategy "不受技能范围影响,受到伤害时释放技能" (§20.2, §21.29).
  • Mostly not a bug (triage on the 0.1.1 sim): ordinary ground enemies inside x-1 are teleported — 余 in a lane of the flat stage pulled all 3 enemies inside x-1, on 战场#01(下半) all 3; on a 围墙 tile none (the official rule). The usual first hit came from a ranged enemy outside x-1 (a 深池暗影术师 2.7 tiles away): under TAKE_DAMAGE the skill fired with nobody to pull, so only its damage and effect showed.
  • Cause (two real faults): teleportEnemy skipped every leader (isBoss) — no PRTS basis; and S2 played its pull fx even when nobody was teleported (n = 0).
  • Now: a leader is teleported like any ground enemy unless it is 自缚 — the 7 huge leaders (SELF_BOUND), 守墓石像's 转换模式 and the 胄 parts' anchor keep selfBound (§21.19), so 卢西恩 and 假想敌:铳 are the leaders it can pull. A teleported leader re-plans from his tile: on the real rounds 假想敌:铳 (不可阻挡) walks on to its next patrol checkpoint and 卢西恩 is blocked by 余 (block +2), blinks past and resumes its loop. 静态刚体 is no exemption (the 备注 names 消失 / 自缚 / 免疫传送; PRTS 静态刚体 forbids physical movement) and every 静态刚体 of the mode flies or is 自缚 anyway (§21.9). The pull fx plays only when at least one enemy was teleported (each still gets its own teleport fx).
  • Deliberate deviation — S2 casts with an enemy on its x-1 (the owner's decision of 2026-10-04; it joins §21.29's six): players saw it fire from a ranged hit with nobody to pull, so instead of the official row's TAKE_DAMAGE it casts once its SP is ready and an enemy stands on its own 技能范围. Not the basic strategy (DEFAULT, §21.29's rule): his attack range is his own tile (0-1), so "about to attack with an enemy in the initial range, or one he blocks" fires only once an enemy is already on his tile — measured, an enemy parked two tiles away never set it off, under DEFAULT as under TAKE_DAMAGE. It takes SKILL_RANGE — the mode's official strategy for a MANUAL skill with a 技能范围 when no class row applies ("仅在技能范围内存在敌人(无视其不可选中)时释放技能", §20.2) — on x-1: TRIGGER_DEVIATIONS chess_char_6_03_a: { skchr_yu_2: 'SKILL_RANGE' } (per chess, the elite through its baseId); a SKILL_RANGE deviation carries the skill's own range as customRangeGrid, and validateAll checks both records. The two records read { rule: 'SKILL_RANGE', rawRule: 'TAKE_DAMAGE', customRangeGrid: <x-1> } (rebuilt with --offline: nothing else in data/ changed). His S1 keeps TAKE_DAMAGE, his S3 its official charId row (CUSTOM_RANGE).
  • Players notice: S2 never fires from a hit outside x-1 any more; it fires as soon as an enemy steps onto x-1 — any enemy, a flyer or a 隐匿 one too (a flyer alone: the burst hits it, nobody is pulled) — so in a stream it often pulls only the leading enemy where the hit used to come with several bunched up. Measured on the triage lanes (SP full from the start; cast / enemies pulled): flat stage beside the lane 5.90 s by the 深池暗影术师's hit, 0 → 6.33 s, 1; in the lane 8.13 s, 3 → 4.83 s, 1; 战场#01(下半) 11.87 s, 3 → 8.57 s, 1. One-line flip: remove the entry from TRIGGER_DEVIATIONS and rebuild.
  • Checked: no enemy of the mode is immune to the teleport — every enemy name of data/enemies.json was looked up on PRTS: all list 传送抗性 无 (the four 悬赏 bosses on their "(敌方)" pages, “余音” on its own) except “斩胄之剑”, “破胄之锤” and 刺胄之弹, whose value is empty — they fly (the 剑 / 锤 also stay 自缚-anchored when shot down), so they are never teleported anyway; 碎铳之簧's page has no such row and “炎佑” is an ally-side unit.
  • Not modelled: the 0.13 s between the damage and the teleport (same tick) and the corner landing points (the tile centre).
  • Tests: test/sim/feedback2-whitw2-yu.test.js (S2 fires with a ground enemy two tiles away and nobody hitting him, the enemy on his tile; a ranged hit from outside x-1 no longer casts it; 卢西恩 and 铳 teleported, “萨米的意志” (自缚) stays, one pull fx for two; a flyer alone on x-1 → no teleport / pull fx; the real 铳 and 卢西恩 rounds), test/sim/feedback1-tank-triggers.test.js (the data and the real battle for 余 S2 beside the six), test/data.test.js (14 deviated records), kits_alt_t6 (6_03 S2 now asserts the reason SKILL_RANGE); feedback1-residuals (守墓石像) and playtest6-skills (#15 audit, 余 S2 now among its SKILL_RANGE skills) pass unchanged.

22.11 归溟幽灵鲨's 替身 is drawn and sits the fight out (#44, 「归鲨死了没替身」) — professions.js installDollkeeper (DOLL_SWITCH, hook dollSwitch), kits/tier5.js (归溟幽灵鲨: trait dollNoAttack, S2 onEnd, 拥抱自我), content/items/battle.js (PRIO_UNDYING_HELD), render/units.js FORMS (doll, leave), UnitView (_dieForm), render/spine.js (baseRoles), render/style.js / textures.js (icon doll), render/fx.js (dollEnd)

  • Seen (GitHub #44): 「归鲨死掉后没有替身」. The owner's triage: in 0.1.1 the sim did switch her to the 替身, but the client kept drawing her 本体; the 替身 also attacked and could cast S2 生存的渴望, whose end then knocked her out.
  • Official: the trait (character_table / chess trait) 「受到致命伤时不撤退,切换成<替身>作战(替身阻挡数为0),持续20秒后自身再次替换<替身>」 (bb duration 20; atk / max_hp are the 替身's bonuses — 0 on both 傀儡师 but for the elite modules). PRTS 分支特性信息/data §傀儡师: "受到足以致命的伤害且未持有不死的情况下" → "一段切换动画", then the <替身> fights for 20 s holding 阻回; at its end a switch animation back; "切换动画开始时会清除自身一切Buff;切换途中重设自身生命至最大值,随后再次清除自身一切Buff"; during a switch animation 不死, 无敌, 阻回, 禁疗, 孤立, 强制缴械, 眩晕 / 冻结 / 睡眠 immunity; block ×0 "从切换至<替身>开始时持续至切换回非<替身>开始"; "部分机制也可以直接让干员切换为<替身>形态". PRTS 归溟幽灵鲨: 特性备注 「<替身>不进行普通攻击」 (and 天赋备注: 伤害与减速不可对空); S2's text corrected to 「技能结束后立刻切换为<替身>」 (原文 视为被击倒); S2 备注 「技能期间持有不死,因此技能期间不会因致命伤而切换为<替身>」. PRTS 风丸: 特性备注 「<替身>状态下可对空」 — her 替身 attacks; her 纸偶 leaves "持有者切换替身时". The 替身's HP: PRTS resets it "至最大值", the trait's 替身 HP bonus is 0, character_table has no 替身 of 归溟幽灵鲨 and 风丸's 纸偶 token (token_10022_kazema_shadow) has her own max HP at every level — so the operator's own max HP. Her skeleton (char_1023_ghost2.skel, read with the client's own parser): the *_B clips draw the 替身's own slots (*_T) and hide the 本体's — Start_B (1 s, fades in), Idle_B, Die_B (1 s, breaks apart and has faded out at its end), Die_B_2 (1 s, collapses and stays); Start_2 (1 s) brings the 本体 back; her Back skeleton has only Idle_B and Start_2. 风丸's: Start_B (its OnAttack at 0.267 s is 折纸生花), Idle_B, Attack_B, Die_B; her Back skeleton Start_B, Idle_B and Attack_B. Neither Back skeleton has the 替身's death clip.
  • Cause: the 替身 was only a buff (block −99, HP × 0.5): nothing kept it from attacking or casting; S2's end emitted 'knockout' and loseHp(hp + 1) — a lethal HP loss, so the 替身, or the knock-out when she already was one (a cast as the 替身 killed her at its end). On the client an operator had no form: the 'substitute' / 'swap' fx carried no form, UnitInfo none, FORMS had no entry for her skeleton, and 'trait:substitute' had no icon. Already in 0.1.1: a running 坚固维式重锤 不死 window did not stop the switch (its battle-level hook and the 傀儡师's sat at the same fatal priority, the 傀儡师's registered first).
  • Now (every 傀儡师): a lethal hit on the 本体 that nothing prevented before it — a skill's or talent's own 不死 (10 … −60) or a running 坚固 window (PRIO_UNDYING_HELD −99; the hammer's lock, a new window, stays at −100 after the switch, as in 0.1.1) — switches her: the running skill ends (reason substitute: S1 / S3, 风丸's S2 and its 纸偶) and her statuses are removed; for DOLL_SWITCH (1 s) she holds trait:dollSwitching (flag u.trait.dollSwitching) — 无敌, 阻回, 禁疗, 孤立, 缴械, 眩晕 / 冻结 / 睡眠 refused, a lethal 流失 stops at 1 HP; trait:substitute lasts the switch + 20 s: block 0 from the switch on, 阻回, max HP = her 替身 token's (风丸) or her own (归溟幽灵鲨, was 50 %), HP full; u.form = 'doll' and fx 'substitute' { form: 'doll', dur }. When it ends, the switch back: the same 1 s animation and clearing, block and HP back, fx 'swap' { form: null }. A lethal hit on the 替身 (after its switch) knocks her out; the form is then reset by fx 'dollEnd' { form: null } after the 'die' event (drawn as nothing), so she redeploys as the 本体. Content switches her at once with the hook dollSwitch { unit, reason, done }. 归溟幽灵鲨's kit sets dollNoAttack: her 替身 carries 缴械 and 沉默 — no normal attack and no cast (S2 / S3's basic strategy casts on an attack the 替身 never makes). Her S2 end emits dollSwitch (no lethal HP loss — so no 不死 / 复活 item takes it — and no 'knockout' fx, gone from the client's table); nothing when it ended with her, by her switch, or she already is the 替身. 拥抱自我 works while the 替身 fights (not during the switch). Client: FORMS char_1023_ghost2.doll — Start_B (change), Idle_B, Die_B as its closing clip over its last second (timed from the fx dur), Die_B_2 to die on, no attack or skill clip, and the new leave clip Start_2, played when a form ends (UnitView.setForm; not on a knocked-out view); char_4016_kazema.doll — Start_B, Idle_B / Attack_B, Die_B, leave Start. A form now ends in the unit's own roles (SpineActor.baseRoles: the equipped skill's clip — an operator came back on the manifest's default skill clip). 'trait:substitute' shows the puppet icon doll. The fx carry form, so no client stage drops them (keepsState / keepEarly / isCosmeticEvent) and a view built later starts from UnitInfo form. Facing UP, the Back model shows the 替身 and skips the clips it lacks; knocked out, she lies on the Front model like every operator facing UP (§22.1) — the view keeps the form it died in (_dieForm, set by die(), cleared when it stands up), so that model, built a frame after the 'dollEnd' reset, and any model rebuilt while she lies down show the 替身's Die_B_2 (风丸: Die_B); she stands up as the 本体 (facing UP: on the Back model, its deploy clip); a revive in the same frame as the knock-out (阿戈尔: die → deploy → 'dollEnd') brings the 本体 back on Start_2 and loads no model.
  • Players notice: she turns into her 替身 (a puppet of her, with an icon) for 21 s; it no longer attacks and her SP waits until she is back; S2's end no longer kills her — she becomes the 替身; the 替身 has her full HP; after each switch she cannot be hurt and does not act for 1 s; S1 / S3 running at a lethal hit end; knocked out as the 替身 she lies as the collapsed 替身, facing UP too. With a 坚固维式重锤 whose 8 s window still runs (its lock set off by the 替身's lethal hit), a lethal hit on the 本体 leaves her at 1 HP instead of switching her again. 风丸 likewise (her 替身 drawn, the 1 s switches, 阻回, S2 and its 纸偶 end with the switch). Measured (the report's repro: a harmless enemy parked in her range, lethal hits by hand): 0.1.1 — the 替身 at 1 328 / 2 657 HP made 17 of her 19 attacks and her SP kept filling; with 38 / 40 SP she cast S2 as the 替身 (at 2.4 s, 1.9 s after the hit) and was knocked out at its end (17.4 s); now — 2 657 / 2 657, no attack and the SP held 22 s; S2 cast as the 本体 at 24.9 s, alive.
  • ASSUMED: the switch animation lasts 1 s each way (the skeleton's Start_B / Start_2) and the 20 s run after it; the direct switch at S2's end (立刻切换) plays the same 1 s animation; "清除一切Buff" = the running skill and the catalogue statuses — every other buff survives, the remake's bond / item / talent effects (buffs too) and other units' timed buffs and shields alike, and element gauges and 爆发冷却 stay; 无敌 is modelled as damage immunity only, like every ally 无敌 in the code (PRTS also gives "无法被不同阵营选中": enemies may still target her during a switch); no S1 (技能范围) cast as her 替身 either; the 替身 can be healed (PRTS names 禁疗 for the switch only); 风丸's 替身 may still cast a skill that was ready at the switch (it attacks); Die_B over the 替身's last second is visual only; a view built from UnitInfo while she lies down (a field opened later: the sim has reset the form) shows the 本体's fall.
  • Open: 风丸's 替身 attack range (PRTS x-4) and 对空 are not modelled — it keeps her own range and ground-only attack.
  • Normative lines rewritten with it: §5.2 (form), §5.4 (fatal: PRIO_UNDYING_HELD), §8.2 (fx form, UnitInfo form).
  • Tests: test/sim/feedback2-doll.test.js (cases A–D of the repro: the 替身's form, HP, block, 阻回, no attack, 拥抱自我, the switch back; S2's 不死 and its end; a knock-out as the 替身, the event order, the redeploy; no cast as the 替身 for S2 at 38 / 40 SP and a ready S1; S3 ending with the switch; the hook; a running 坚固 window stopping the switch; 风丸's 替身 attacks), test/render/feedback2-doll.test.js (the clips in the manifest, Start_B → Idle_B → Die_B → Start_2 with her S3 clip after it, Die_B_2 held through 'dollEnd' and the redeploy, UnitInfo form, the Back model, a late fx; knocked out facing UP — the Front model's Die_B_2 with the reset in the same frame or the next, 风丸's Die_B —, a model rebuilt while down, the redeploy on the Back model, a same-frame revive on Start_2 without a model load, a view built while down; 风丸, sim → view, the skeleton's clip meanings), test/sim/professions.test.js (the switch windows), test/sim/fidelity.test.js (her own max HP).

22.12 Enemy splash spares a 隐匿 operator that does not block the enemy (#32 item 6) — sim/targeting.js areaSelectable / auraSelectable, content/enemies.js areaAllies / areaAlliesInTiles / fieldAllies / auraAllies / targetAndArea / zone / dmgZone, content/bosses.js, sim/Battle.js alliesInRadius

  • Seen: 「隐蔽单位仍会受到溅射伤害」. Real on the enemy side (the operator side was fixed in 0.1.1, §21.19): every enemy area effect took every ally in its area. Measured on v0.1.1: 碎骨 grenades 角峰 on (10,6); 古米 on (10,7) under a 999 s 隐匿, not blocking, took 4 splashes (2650 damage) and was knocked out. §21.22's audit had listed it ("enemy splash still hits 隐匿 allies").
  • Official:
    • PRTS 作战机制 §隐匿 "隐匿效果使得获得该效果的单位无法被任何敌方的能力索敌选中", "隐匿的我方干员阻挡敌人时,因为“阻挡优先级最高”的效果,敌人会无视一切可选性对该干员进行攻击"; §AOE伤害判定 "AOE的判定是对攻击范围内的每个可以被选中的敌人进行判定"; §隐匿与Buff的关系 "隐匿状态下的单位一般无法被敌方的索敌机制和Buff选择器选中为目标", "目前明日方舟中使用能选中隐匿状态单位的Buff效果一定是无视隐匿状态起作用的" — its one enemy example being 寒霜's 攻速下降 Debuff.
    • PRTS 异常效果 §无法选择: with 隐匿, 不可选中, 无敌, 塔不可选中 or 对地规避, "常见的、来自不同阵营的“选择”行为将无视这些单位进行(如同范围内不存在这个单位)"; the abilities PRTS marks "无视可选性" skip that check; "'直接选中'的能力不会进行具体的目标选择,故同样不受这些仅在选择时生效的异常效果制约".
    • gamedata_const ba.invisible 隐匿 "不阻挡时不成为敌方攻击的目标" vs ba.camou 迷彩 "不阻挡时不成为敌方普通攻击的目标(无法躲避溅射类攻击)"; PRTS 异常效果 迷彩 "所有光环类能力、以及涉及中点判定/格子判定的效果均不受迷彩制约,但存在主动排除受迷彩影响单位的情况(此时站内会特别标注)"; PRTS 选择器 "非弹道类型的溅射攻击会自动无视迷彩", "所有触发选择器通常不无视迷彩".
    • The exceptions are the notes on each PRTS enemy page ("无视无法选择", "无视(目标)可选性", "无视迷彩"); every area site below was checked against its page.
  • Cause: content/enemies.js and content/bosses.js picked their area victims with battle.alliesInRadius / alliesInTiles / allies(); §21.22 had added only a 对地规避 filter, site by site.
  • Now: one area selector, targeting.js areaSelectable(src, a) (helpers areaAllies, areaAlliesInTiles, fieldAllies): no 隐匿 ally unless it blocks src, no untargetable or sleeping (沉睡 = 无敌 + 无法行动) ally, no airborne 起飞 ally for a ground src (the damage pipeline's refusal, evadesGround; the per-site filters are gone). 迷彩 is not checked. A dead src (death blasts) blocks nobody; src null (no selecting enemy) keeps 隐匿 out but not 起飞. Buff auras use auraSelectable / auraAllies: the same without 对地规避. A target the ability locked beforehand — an attack's target, 死亡之眼's channel, a C4, an 爆炸箭 — is a direct pick (targetAndArea): a 隐匿 it gained meanwhile does not save it; only the units around it are an area selection. Per site:
    • Selects (a 隐匿 operator that does not block the enemy is spared):
      • splash — 碎骨's grenades (their DEF down too; its target keeps it), 自制投石机's stones, the 周围四格 of 控潮术师 / 领潮员 / 术师快艇 and 腐败骑士, 暴鸰's 8-tile bomb, 灼藤's first 3×3, the shells of 帝国炮火先兆者 / 中枢先兆者 (their damage stays 无来源);
      • death and self blasts — 高能源石虫 / 冰爆源石虫 / 卷心籽 (PRTS 死亡爆炸 "…无视迷彩,不可对空", "来源无效" included), 烹泉 / 沏虹 (blast and steam), “巨大的丑东西”'s self-destruct, “庞贝”'s blast while blocked;
      • area skills and statuses — W's C4, 鼠王's 唱沙 and 沙狱, 凋零骑士's 爆炸箭, 腐败骑士's 蓄力锤, 萨卡兹王庭军精锐术师's 死亡之眼 burst, “自在”'s 纬地经天 and 破桎而出, 锏's 速杀 (those on its way; its former blocker is still hit), 肆虐风雪 and 瞬息杀机, “萨科塔昂首”'s 祈祷邀约, “帝国的甲胄”'s barrage, the 自行炮's shots (each picks inside its 9 tiles — a 迷彩 operator can be shot now);
      • pulses — the 神经毒素 of 元核孽生者 / 异光体孽生者, 鼎沸, 深溟巢涌者 (its pulse rides on its engine attack, which needs a target: a 迷彩 operator alone in its range is never hit), 水遁忍者's spin; zones — 集团军重型火炮's 燃烧区域 and 假想敌:蚀裂's sourceless 毒雾 (src null: a 隐匿 operator is spared, an airborne 蒂比 still reached — §21.22; until 0.1.2 it took everyone inside); jumps — 陷落雪祀's chain, the 碎铳之簧 element bounce;
      • leaders and parts — the 刺胄之弹 / 斩胄之剑 / 破胄之锤 blasts (the flyer selects, so 对地规避 still does not apply), 铳's 最终之罚 and the springs' 末日布道 tramples, “余音”'s pulse and 合奏, 管's 裂管之奏, 弦's 断弦之奏, 昆图斯's 大潮 and tentacles, 卢西恩 / 不祥幻影's 【aoe】, 阿利斯泰尔's 王权号令 and 斥退, “萨米的意志”'s 冰凌 column and doom.
    • Normal attacks on every operator in range take the targets of a normal attack (canTargetAlly: no 隐匿 or 迷彩 operator unless it blocks the enemy — PRTS 选择器: a normal attack does not ignore 迷彩): 斩胄之剑 / 破胄之锤's hover attack ("普通攻击对攻击范围内的所有我方单位…") also took 隐匿 and 迷彩 operators until 0.1.2 (targetsNear … ranged: false); “灵幛” already did not.
    • Cast conditions of area skills count the targets of their trigger selection (targetsNear): 卢西恩 / 不祥幻影's 【aoe】 ("需要目标"), 锏's CircleAttack — a lone 隐匿 or 迷彩 operator no longer sets them off, as a lone airborne 蒂比 has not since §21.22.
    • Buff auras (auraAllies): 深池伙友卫队's field and 扎罗's 远古威慑 no longer slow a 隐匿 operator that does not block them (their PRTS pages carry no 无视 note); an airborne 蒂比 is still slowed [ASSUMED, §21.20 / §21.22's ground-enemy auras].
    • Still reach every ally there: 【污染秽蚀】, 【盲信之誓】, 萨卡兹悖谬暴虐兵长's 暴击, 假想敌:淤困's burst spread (PRTS "无视目标可选性" — now ignoreSelect, so it also reaches an airborne 蒂比, which the pipeline used to refuse), 远眺's death 暴露 and 圆仔's facing count ("无视其可选性"); 寒霜's aura (PRTS: "无视隐匿"); map, tile and terrain effects — “墓碑”'s halving (PRTS: "实际属于地图效果"), 【国度】, a chimera's 源石污染区 aura, 深水区 / 活性源石 / 源石污染区 tiles; the ticks of a debuff already on a unit.
    • 起飞: 锏's 【瞬息杀机】 no longer reaches an airborne 蒂比 — it used to set her SP to 0 directly, past the pipeline; its area selection now skips her (对地规避: 锏 walks).
    • The operators' 隐匿 sources all set the flag the selectors read (stealth): 伪装服 (applyStatus('stealth') on the first damage), 叙拉古 6, 伊内丝 S2 and the 排气格栅 tile (PRTS 特殊地形: "即隐匿"); 迷彩 (camou): 忍冬 S3, 寒芒克洛丝 S1.
  • ASSUMED:
    • A 隐匿 operator that blocks the enemy stays selectable for that enemy's area abilities and auras (PRTS states the forced selection for its attacks).
    • 迷彩 is hit where PRTS gives no basis — no splash type, no 中点判定 / 格子判定 or aura reading, no "无视迷彩" note — as since 0.1.1: 锏's 肆虐风雪, 瞬息杀机 and the bystanders of 速杀, 昆图斯's 大潮 (whole field) and tentacles, “萨米的意志”'s doom and 冰凌 column (a 格子判定 reading), “余音”'s pulse and 合奏, 铳's and the springs' tramples, “巨大的丑东西”'s self-destruct, W's C4 blast, “帝国的甲胄”'s barrage and the 自行炮's zone (格子判定 readings), 陷落雪祀's chain, the 碎铳之簧 bounce.
    • 【国度】 and the chimera's aura reach a 隐匿 operator (tile / terrain readings).
    • An airborne 蒂比 is still reached by ground-enemy buff auras and by 蚀裂's 毒雾 (§21.22).
    • An invulnerable (无敌) operator is still selectable by areas, as by attacks (§21.22's open deviation; its damage is 0).
  • Found, not changed (enemy-number gaps against PRTS, queued for a later round):
    • 碎骨's grenade: PRTS's page gives 3×3, 26 % ATK, DEF −50 % for 5 s; the code has radius 1, 100 % ATK, 3 s.
    • 扎罗: 远古威慑 radius 1.5 / ASPD −50 (code 2.5 / −30), the second form's range 1.25 (code 2.5), 溶血骇惧's loss curve (up to 30 % max HP/s at 40 s; code 5 %).
    • “复仇者”'s 冲锋 trigger counts 隐匿 / 迷彩 operators (targetsNear … ranged: false).
    • “庞贝”'s blast radius: PRTS 1.4, code 1.
    • “巨大的丑东西”: its unblocked ranged 3×3 splash attack is missing; self-destruct radius 3.0 (code 2.5).
    • “墓碑”, 烹泉 / 沏虹 and 集团军重型火炮: the splash of their normal attacks (3×3, radius 1.0, radius 1.0) is missing.
    • 碎铳之簧: bounce radius 1.7 and ×0.8 per bounce (code 2 / 0.85), chase trample radius 0.35 (code 0.5).
    • 锏's 速杀: PRTS hits only its former blocker, 0.3 s after the blink.
    • W's C4: PRTS installs it on the unit in a 3.2 s cast, 180 % on it (code: 1 s fuse, radius 1).
    • 管's 裂管之奏: the extra 0.8-radius 凋亡 hit "(无视其可选性)" is not modelled.
    • “萨科塔昂首”'s 祈祷邀约 "不可对空" does not filter flying allies.
  • Tests: test/sim/feedback2-stealth-splash.test.js (9 — the selectors; 碎骨 next to a 隐匿, a 迷彩 and a 伪装服 operator; “庞贝”'s blast on its 隐匿 blocker; 卢西恩's 【aoe】; the 无视无法选择 exceptions; 蚀裂's 毒雾 and the 深池伙友卫队 / 扎罗 auras vs 隐匿 and 起飞; 死亡之眼's locked target; the blade / hammer hover attack, the 自行炮 and 深溟巢涌者 vs 迷彩, 祈祷邀约 vs 隐匿 — every battle case fails on 0.1.1), test/content/enemies_bosses.test.js, test/sim/feedback1f-liftoff.test.js, test/docs-consistency.test.js (卢西恩's / 锏's cast conditions).

22.13 要塞 cannot hit air units; 莫斯提马's slow shows its icon (GitHub #35, #16; PR #48) — tools/build-data.mjs classifyAttack (fortress no longer in the can-hit-flying ranged set), data/chess.json canHitFly false for chess_char_2_18_a/_b (灰毫) and chess_char_5_08_a/_b (号角), sim/professions.js (fortress, resolveProfile), kits/tier4.js (莫斯提马's slow pulse status: 'slow', visible: true)

  • Seen: GitHub #35 — the 要塞 operators 号角 and 灰毫 shot at flying enemies and splashed them; #16 — "莫斯提马三技能没有减速".
  • Official: PRTS 新人入门 marks the 要塞 branch 不可对空; the 要塞 trait text never mentions air, and no 号角 or 灰毫 skill adds anti-air. 莫斯提马's slow was not broken: her talent's move_speed −0.15, ×3 during S3 (talent_scale), is 85 % / 55 % of the movement speed per PRTS, and it already applied in 0.1.1 — only no status icon was drawn on the slowed enemy, which is what the reporter saw.
  • Cause: classifyAttack gave canHitFly to every ranged attacker unless the trait says 地面敌人 (投掷手); a 要塞 attacks from the melee position with a ranged normal attack (MELEE_RANGED_SUBPROFS), so it counted as ranged and the fortress profile did not set groundOnly. 莫斯提马's pulse carried no status.
  • Now: canHitFly is false for the sub-profession fortress — the only data change is those 4 records of data/chess.json (normal + elite); the fortress profile reads canHitFly: false, groundOnly: true and resolveProfile forces both for any fortress or groundOnly profile (the data's generic ranged default cannot re-enable it), so the four records neither target nor splash a flying enemy (targeting.canTargetEnemy, the splash loop of ai.js resolveHit); ground enemies are still attacked and take the radius-1.0 splash. 莫斯提马's slow pulse passes { status: 'slow', visible: true }, so the existing slow status icon shows on every enemy in her range; the slow itself is unchanged. §21.29's skill-trigger deviation for the six 重装 skills (号角, 灰毫) is untouched.
  • Players notice: 号角 and 灰毫 (and their elites) ignore flying enemies completely — no shot, no splash — so flyers need another operator; against ground enemies they play as in 0.1.1. Enemies slowed by 莫斯提马 carry the slow icon.
  • Contributed by the PR #48 author (Paper-Yuan), squash-merged 2026-10-04. Normative lines rewritten with it: docs/SIM.md (the profession table: 要塞 ground only, by the PR), docs/DATA.md §2.1 (canHitFly).
  • Tests: the PR's — test/sim/professions.test.js (the four records read canHitFly false / groundOnly; a 要塞 beside a FLY enemy makes no attack and leaves it untouched, beside a ground enemy it attacks and splashes) and test/content/kits_t4.test.js (莫斯提马's slow buff carries the slow status).

22.14 Ranged enemies stand for each attack (#58) — sim/ai.js attackStand / enemyAttack / updateEnemy / advanceRoute, sim/simdata.js normalizeEnemy, sim/Battle.js spawnEnemy, tools/build-data.mjs enemyAttackAnim / attacksOnTheMove / loadManifest, enemies.json attackAnim / attackMoves, render/spine.js SpineActor.windUp / attack (clipPerAttack), render/units.js

  • Seen: 「1.1中敌方远程攻击单位在攻击时任会移动」. Real in the sim and the client: an unblocked ranged enemy stood only ATTACK_PAUSE = 0.35 s after each strike (no source for the number), while every ranged attacker's attack clip is longer (median 1.33 s, 0.37–3.5 s); the client stretched the clip over the whole attack interval and kept the attack pose for 1.4 intervals, so the clip looped while the enemy walked about 90 % of the time. Measured on feedback2 6b634fe (能天使 on (10,6), one enemy walking row 9): 隐形弩手 (interval 2.70 s, clip 1.0 s, strike frame 0.533 s) walked 0.24 tiles during the wind-up, stood 0.37 s and walked 2.33 s per attack; 深池暗影术师 (3.0 s, 1.2 s) stood 0.37 s and walked 2.67 s.
  • Official: no source gives the length of the stop. The owner's decision (2026-10-04, from first-hand memory of the official game): a ranged enemy stops for each attack's animation and walks on between attacks. The enemy handbook treats attacking on the move as a special ability — “十字路口”量产型 / 试做型 「不停止移动的四向攻击」 (not an enemy of this mode).
  • Cause: ai.js enemyAttack set pauseUntil = now + ATTACK_PAUSE after the strike and nothing before it; the data had no clip lengths (they lived only in the client's asset manifest); SpineActor.attack / windUp played the clip at clip / interval speed in a loop.
  • Now:
    • Data: tools/build-data.mjs enemyAttackAnim copies each enemy model's attack clip from the committed asset manifest (data/assets.json anims.attack.loop — the clip the client plays, not an Idle stand-in —, its length and first hits time) into enemies.json attackAnim { clip, dur, hit } (213 of 249 keys; the rest have no attack clip). An ability text with 「不停止移动」 gives attackMoves: true (no key of the mode). The sim reads both from its data, never from client files.
    • Sim (ai.js attackStand): the stand is the clip — hit of it before the strike and dur − hit after it, both divided by dur / interval when the attacks come quicker than the clip. Before the strike: enemyAttack reports the wind-up (cooldown ≤ the wind-up, an ally it may target within its reach) and updateEnemy holds the enemy; after the strike e.atkStandUntil holds it. Only the walking is held (advanceRoute(…, standing) holds a MOVE leg, and a 诱导 walk): a checkpoint's WAIT keeps running and DISAPPEAR / APPEAR legs still happen — leaving the field ends the stand — so a wait is not lengthened by the clips (data/waves.json has 122 WAIT and 99 DISAPPEAR checkpoints; review finding). It is drawn standing (moving false, so the client's base clip is Idle until it walks). The first attack of an enemy that walks into range with its cooldown long over still lands at once (no wind-up to stand through; strike times are unchanged). No clip known: ATTACK_PAUSE after the strike, as before; attackMoves (data, or content through e.profile.attackMoves): never stops. A stun / freeze / sleep ends the stand, and a 恐惧 is not held by it. Blocked and melee enemies are unaffected; content holds are separate (暴鸰 has no normal attack — noAttack — and hovers through its drop on its own pauseUntil).
    • Client (SpineActor.clipPerAttack, set for every enemy model by render/units.js): each attack plays the clip once, wound up so its strike frame meets the attack, at its own speed (faster only when the attack interval is shorter than the clip), then the resting clip — Move once the sim walks it again. An attack seen late starts on its strike frame. Operators keep the clip looping over their attack rhythm.
  • Measured:
    • Same layout as above (attackAnim in, this branch): 隐形弩手 stands 1.00 s per attack (0.50 s before the strike — one tick short of 0.533 — and 0.47 s after) and walks 1.70 s; 深池暗影术师 stands 1.23 s and walks 1.80 s; 萨卡兹百夫长 (5.0 s, clip 3.5 s) stands 3.53 s and walks 1.50 s (it used to stand 0.40 s). Over the 75 ranged attackers of the mode, the median share of an attack cycle spent walking went from 91 % to 63 %; three (clip ≥ interval: 深溟巢涌者, 富营养的巢涌者, 节日爵士乐手) no longer walk while they have a target.
    • Real rounds: four seeded 4-bot co-op matches (HARD seeds 1–2, ABYSS seeds 1–2, rounds 1–12 / 14) saved every normal field as its BattleSpec. The 58 fields with ranged enemies were replayed on 50c9b5b and on this branch. Leaks 750 → 764 (+1.9 %), target LP 753 → 767, kills 886 → 876. The 423 ranged enemies: leaked 222 → 223, mean time on the field 24.6 → 27.2 s (+11 %), their attacks 2725 → 2948 (+8 %), mean tiles walked 6.67 → 6.65. Per field the change goes both ways (HARD s1 R9 one field 0 → 6 leaks, ABYSS s1 R9 another 14 → 8). The same numbers after the route-leg fix (none of these fights has a ranged enemy on a WAIT / DISAPPEAR leg while it attacks). The ranged enemies cross the field more slowly, but they also stand in range of the operators longer and hit them more often, so the net effect on leaks is small.
  • ASSUMED: the stand lasts exactly the attack clip and is split at its strike frame; the clip's speed-up when the attack interval is shorter than the clip (the client's rule); no wind-up stand before an attack that lands as the target comes into range; a stun / freeze / sleep ends the stand, and 恐惧 is not held by it; an enemy in another form (掠海漂移体's crawl, 转译基底's forms, 杰斯顿's second form) stands for its base clip — known mismatches: 扎罗's second form stands 1.433 s (A_Attack) though its B_Attack lasts 2.0 s (2.5 s interval: about 0.4 s of walking during the recovery), and 转译基底's 特战术师 stands 2.0 s (B_Attack) though its D_Attack lasts 1.667 s; a per-form clip would need the client's FORMS mapping in shared data (later); an attack interrupted by 麻痹 keeps the old 0.35 s stand (PRTS 异常效果 麻痹: a 0.5 s 麻痹震颤 that also stops movement — not modelled); a model without an attack clip keeps ATTACK_PAUSE.
  • Normative lines rewritten with it: §5.5 Enemy AI (the stand replaces the fixed 0.35 s pause); docs/SIM.md, docs/DATA.md (attackAnim), docs/ASSETS.md, docs/PLAYING.md.
  • Tests: test/sim/feedback2-enemy-attack-stand.test.js (7 — attackAnim matches the manifest for every enemy; 隐形弩手 with an operator in range stands for its clip at each attack and walks between attacks; a 1 s attacker with a 2 s clip never walks while it has a target; an attackMoves enemy keeps walking while the same enemy without it stands; the fallback, and a stun ending the stand; on a 3 s WAIT checkpoint the wait ends at the same time with or without a target, and the enemy walks on once its current clip is over; DISAPPEAR / APPEAR legs proceed during the stands, and hiding ends the stand), test/render/feedback2-enemy-attack-clip.test.js (4, fake PIXI with the real manifest: wind-up, one clip per attack at its own speed, then Move; a quick attacker's clip at 2×, an attack seen late starts on its strike frame; an operator's loop unchanged; enemy views use a clip per attack, operator views do not); test/sim/combat.test.js keeps the 0.35 s fallback.

22.15 An operator forced off the field lies down and comes back there (#60) — sim/Battle.js isDown / retreat (with _remove, _layBody, restTile, downOn, _checkRedeploys, snapshot unchanged), content/kits/tier5.js (余烬)

  • Seen: 「42突袭死后会消失」 (42 = 史尔特尔, a community nickname). Real: her talent 余烬 keeps her at 1 HP after a lethal blow and forces her out 8 s later (battle.retreat, reason 'retreat'). Battle.isDown took only 'killed' and FORCED_EXIT, so she left no body: no b.snap down entry, so the client played Die and faded her out, and no redeploy ring. restTile sent her home, so 70 s later she came back on her board tile, not where she fell. Measured on feedback2 6b634fe: on (10,3), out at 9 s, back at 79 s on (10,3), with nothing on the field in between; after a 突袭 jump to (12,8), out at 21 s, back at 90 s on (10,3), then she jumped again. The same path was used by 耀骑士临光 S2 (tier6.js), the 骑士戒律 + 竞技旗 combo (items/battle.js), 伊内丝 S3's first deployment (tier4.js) and a 商人 that cannot pay (professions.js, tier3.js 琳琅诗怀雅).
  • Official: PRTS 卫戍协议/帮助 §作战阶段 单位部署: "干员退场后,将返回隐藏的待部署区,并原地留下一个“倒地干员”以供查看信息,满足再部署条件时,移除场上的该倒地干员并自动部署至该位置。" This covers every 退场, not only a knock-out (the owner's decision, 2026-10-04: every operator that leaves the field leaves a 倒地干员 on that tile and is redeployed there). PRTS 史尔特尔 天赋 余烬: 「受到致命伤害时持续使生命值不低于1,8秒后强制退出战场」. Her page's 备注 "强制退出战场视为撤回干员" describes the regular game, where a withdrawal refunds part of the cost; 卫戍协议 has no withdrawal in battle.
  • Cause: §21.20 / §21.24 ASSUMED "a withdrawn operator leaves no body": isDown listed two removal reasons.
  • Now: isDown takes every removal of an operator that may come back, except the 突袭 retreat ('raid', which redeploys at once on its landing tile): 'killed', FORCED_EXIT (联防), 'retreat' (余烬, 耀骑士临光 S2, 骑士戒律 + 竞技旗, 伊内丝 S3) and 'merchant'. The §21.24 rules then apply unchanged:
    • _remove lays the body where the operator stood (its own home when that tile is another board piece's home and its home is free).
    • b.snap down and the field meta list it, so the client draws the body and the ring. The client needed no change: it draws whatever down lists.
    • _checkRedeploys / redeploy() bring it back on that tile, and downOn / isReservedTile keep allies off it.
    • A forced exit is still no kill. Its death reason stays 'retreat' / 'merchant': no kill hook, no 被击倒 effects, no 不屈 or 阿戈尔 redeploy, no knock-down count (perPlayer.deaths) and no knock-down sound.
    • 联防 is unchanged: any operator not alive at the end of its own combat already entered 联防 down (carryState.down, FORCED_EXIT).
    • After the fix, the same layouts: down on (10,3) — down [1, 79, 70, 0, 10, 3] — and back there at 79 s; after the jump, down on (12,8) and back on (12,8) at 90 s; 耀骑士临光 S2 lies on her tile with her ×1.25 redeploy ring.
  • ASSUMED: 'merchant' counts as a 退场 too (the PRTS sentence and the owner's "every operator that leaves the field"; the triage had left it out); the regular game's partial refund for a 强制退出 (视为撤回干员) is not modelled; the 突袭 retreat leaves no body, since it is redeployed on its landing tile in the same tick (should both tiles refuse it, it waits for its timer and comes back home).
  • Normative lines rewritten with it: §5.4 DP / redeploy, §8.2 down, §21.20 (ASSUMED list), §21.24 (Now), §22.1 (the withdrawn-operator sentence); docs/SIM.md, docs/PLAYING.md.
  • Tests: test/sim/feedback2-forced-exit-down.test.js (4 — 史尔特尔 after 余烬: down on her tile, the field meta, no kill / knock-down count, back on that tile after 70 s; after a 突袭 jump: down on the landing tile, the tile reserved, back there with her home unchanged; 耀骑士临光 S2: down on her tile with the ×1.25 ring and back there; a real knock-out unchanged); test/sim/downelem.test.js (a forced exit is down; the 突袭 retreat, permanent removals, summons, devices and enemies never are) and test/sim/engine-requests.test.js (a retreated operator lies where it stood and comes back there) follow the rule.

22.16 干员调配 shows the stats, 攻击范围, 特性 and 天赋 the chosen skill / module fight with (#64) — screens/loadout.js statsPreview / LoadoutStats, ui/detailPanel.js chessStatsBlock / chessTalents / traitText, css/screens/loadout.css .lo-sec--stats

  • Seen (GitHub #64): the 干员调配 detail had only 技能 and 模组 — no 生命上限 / 攻击 / 防御 / 法术抗性 / 攻击间隔 / 阻挡数 / 部署费用 / 再部署, no 攻击范围, no 特性 or 天赋 of the operator (headless 1920 × 1080: block titles ["技能 SKILL", "模组 MODULE"], 0 .dstats, 0 .rgrid). A module card lists only its own overrides (ModuleInfo: 属性 +80 …), so a player could not tell what a module or a skill choice does to the operator without leaving for the board and opening its card.
  • Official: no rule to look up — the screen adds none. The rule it serves is unchanged (§16, "开始游戏前无法调整干员的等级,但可调整其所携带的技能和模组"); the numbers shown are the ones the sim fights with, from the same composition (shared/loadoutRecord.js, §16): an elite's stats = statsBase + the module's attr (不装备: statsBase), its 特性 = the module's traitOverride (不装备: traitBase), its talents = the base ones + the module's talentChanges, its range = attackRangeGrid (a selected "被动效果:攻击范围扩大" skill, a module's own grid, the 特性's permanent 攻击距离 — §21.16).
  • Cause: Detail rendered the two choice sections only; the card's stats block was inline in ChessDetail and its 特性 helper traitText private, so there was nothing to reuse.
  • Now: a 局内数值 section between 技能 and 模组 (next to both choices; the first .lo-seg stays the skill-level toggle). statsPreview(base, golden, entries, level, getChess) picks the variant — the 精锐 record when asked for and the chess has one, else the normal one — and resolves it with chessLoadout(variant, entries, getChess).record (gameLogic.js, the call the shop card and the detail card make); no stat arithmetic of its own. LoadoutStats then draws chessStatsBlock (the card's eight-stat grid and 攻击范围 mini-map — the card now calls the same function, blocks.stats, so the two cannot drift), a 特性 / 天赋 card in the module-info card style (traitText and chessTalents, the card's own rules: the 特性 line exists when the chess has one and follows the chosen module; named, not hidden talents) and a caption. A 普通 / 精锐 toggle has its own state (statLevel, default 精锐: the module exists only there, so a module click changes the numbers at once; it does not move the skill descriptions' toggle); 精锐 is disabled for a chess without an elite record. The numbers are the record's — no live entry, so none of the card's 实时 / 开战时 tag or green / red; the caption says what they leave out: "数值含所选模组;不含技能发动、装备、盟约等局内加成" (in 普通: "普通干员没有模组,所选模组在「精锐」中生效;…"). Measured over the data: 信仰搅拌机 SPT-Y 3 → 4 tiles, 莫斯提马 SPC-X 9 → 10, 引星棘刺 S3 13 → 19 (both variants); 隐现 elite 1,421 / 508 with MAR-X, 1,341 / 486 on 不装备, 1,123 / 399 as the normal chess; 异客's other module rewrites 机理分析 (80 % → 70 %).
  • Now (css): the card's .dstats / .dstat / .drange / .rgrid (game-panels.css, loaded on every page) at this screen's reading sizes — every text size keeps a px floor (labels 9, values 12, range cells 7 px at the phones' 1 rem = 40 px); the stats grid takes flex: 1 1 max(3.2rem, 190px) and the range box wraps under it when the column is narrower than about 260 px. Headless Chrome (test/ui/loadout.e2e.test.js, screenshots test/e2e/out/loadout-stats-*.png): 1920 × 1080 the grid with the range box beside it; 844 × 390 (a 240 px detail column) the range box under the numbers, no sideways overflow, no label cut off; 667 × 375 (the detail slid over the roster) beside again. Portrait phones get the app's "rotate device" screen, so they are no target.
  • ASSUMED: the section's place (the issue offers before 技能 or after 模组 — between them keeps both choices within a screen of the numbers on a 1080p display), 精锐 as the default variant, the caption wording, no difference colouring against 不装备, and that the 特质 (garrison) block is not repeated (the issue names 特性 and 天赋 only).
  • Tests: test/ui/loadout-stats.test.js (9: the variant and the loadout resolution, the sim's range changes, the section against ChessDetail for 11 chess / loadout / level cases, the eight values, 特性 / 天赋 following the module, the toggle, the section's place, the css floors), test/ui/feedback1e-range.test.js (the source contract now points at chessStatsBlock), test/ui/loadout.e2e.test.js (+2 opt-in browser tests: desktop and 844 × 390 touch).
  • Review (Grok, 2026-10-04): the 特性 line fell back to the class trait on a record cloned for a non-default skill or module (e.g. 隐现 on S1 showed 优先攻击空中单位 instead of MAR-X's 攻击空中单位时攻击力提升至110%; 信仰搅拌机 SPT-Y showed the class trait instead of 攻击距离+1) — the in-match card shared the bug since 0.1.1. traitText now returns the record's own module line whenever there is one (a 不装备 record carries none: checked on all 133 elite records); test in test/ui/loadout-stats.test.js.