feedback1 integrate: DESIGN §21 (player feedback after 0.1.0) and the normative lines it changes
- one subsection per workstream (reports, verdict, cause, official rule and source, what changed, [ASSUMED], tests), the integration follow-ups and the settled decisions (tactician rotation keeps in-range summons, user-confirmed) - §2 §3 §5 §6 §7 §8 §9 §10 §13 §14 §16-§20 rewritten where the merged behaviour changed them - docs-consistency checks §21 against the code; PLAYING notes the player-visible follow-ups
This commit is contained in:
1 parent
ff4ff3f350
commit
c7f49b4895
3 files changed
+363
-60
No files matched your search
+305
-57
@@ -62,7 +62,7 @@ server/
|
||||
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, pathfinding (8-dir, no corner cutting)
|
||||
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
|
||||
@@ -131,8 +131,8 @@ Ownership rule for parallel agents: **only edit files assigned to you**; if you
|
||||
- **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), ground-passable, fly-only, special terrain (mire, smog, deepsea, infection). Melee chess may stand only on LOW tiles with buildable ALL/MELEE; ranged chess on ALL/RANGED **and** on LOW ALL/MELEE tiles ("所有行动内远程干员可部署在近战位"). Tokens follow their own `position`.
|
||||
- **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. The right-side Final Assault player is mirrored col `c→20−c` with RIGHT↔LEFT (UP/DOWN unchanged).
|
||||
- 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). Melee chess may stand only on LOW tiles with buildable ALL/MELEE; ranged chess on ALL/RANGED **and** on LOW ALL/MELEE tiles ("所有行动内远程干员可部署在近战位"). Tokens follow their 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).
|
||||
@@ -212,7 +212,7 @@ b.on(event, fn, { priority=0, owner } ) / b.off(...) // hook bus (§5.4)
|
||||
|
||||
### 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).
|
||||
`Unit` fields (minimum): `id` (int, unique per battle), `side` ('ally'|'enemy'), `kind` ('op'|'token'|'enemy'|'device'), `defId`, `ownerId` (playerId; enemies: the field owner / source), `x, y`, `tileR, tileC` (ops), `dir` (allies: UP|RIGHT|DOWN|LEFT deploy direction, §3; `sim/dir.js`), `facing` (derived horizontal sign +1 / −1: LEFT ⇒ −1), `hp`, `alive`, `sp`, `buffs[]`, `statusFlags`, `blocking[]` (ops), `blockedBy` (enemies), `liveRangeGrid` (allies: the 攻击范围 the detail card shows — the running skill's grid, else the unit's own, plus `rangeExtend`; never a kit's target-selection grid, §21.16), `form` (enemies: the model form it is in — UnitInfo `form`, §21.4).
|
||||
|
||||
Base stats (from data): `maxHp, atk, def, res` (0–100), `aspd` (100 base), `bat` (base attack time, s), `blockCnt`, `rangeGrid` (ops) / `rangeRadius` (enemies, 0 = melee), `moveSpeed`, `massLevel`, `weight`, `lpr` (enemies), `dmgType` ('phys'|'arts'|'heal'|'true'), `motion` ('WALK'|'FLY'), `cost`, `respawnTime`, `spRecovery`.
|
||||
|
||||
@@ -229,10 +229,10 @@ Base stats (from data): `maxHp, atk, def, res` (0–100), `aspd` (100 base), `ba
|
||||
`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), camou (an ally's 迷彩: like stealth for enemy targeting and on screen, but not 隐匿 for 隐匿 conditions — 叙拉古, 家族徽章; §20.2), invulnerable, unblockable, levitate, fear (enemy: cannot attack, unblockable, runs to random tiles of the ±45° fan away from the source; no source or itself as source = inside its own tile — `fear.js`, §20.4), taunt (+aggro), cold, noHeal (no heals: 禁疗 summons and 孤立 units; an HP-regen attribute still applies — PRTS 安洁莉娜 备注), isolated (孤立: no friendly selector picks it — no heal, buff, aura or talent of an ally, `Battle.allySelectable`; §20.3)`.
|
||||
- `flags`: `stun, freeze, sleep, silence (no skills), disarm (no attacks), stealth (untargetable by enemies/ops unless blocked or revealed — an operator's radius area damage skips it too, `Battle.foesInRadius`, PRTS 作战机制 §AOE伤害判定, §21.19; an enemy's b.snap stealth bit is set only while its 隐匿 is on, §21.4), camou (an ally's 迷彩: like stealth for enemy targeting and on screen, but not 隐匿 for 隐匿 conditions — 叙拉古, 家族徽章; §20.2), invulnerable, unblockable, levitate, fear (enemy: cannot attack, unblockable, runs to random tiles of the ±45° fan away from the source; no source or itself as source = inside its own tile — `fear.js`, §20.4), taunt (+aggro), cold, noHeal (no heals: 禁疗 summons and 孤立 units; an HP-regen attribute still applies — PRTS 安洁莉娜 备注), isolated (孤立: no friendly selector picks it — no heal, buff, aura or talent of an ally, `Battle.allySelectable`; §20.3)`.
|
||||
- **Status catalogue** (`buffs.js`, keys used by content): `stun`, `freeze` (stun + res −15), `cold` (aspd −30; a second cold while cold ⇒ freeze 3 s unless immune), `sleep` (stun; ends on damage? no: in AK sleep = untargetable & unable to act; keep AK: untargetable & inactive), `slow` (moveMul), `bind` (moveMul 0), `fragile` (dmgTakenMul), `artsFragile` (artsTakenMul), `silence`, `fear` (the 恐惧 walk, `fear.js`), `burnGauge`/`necrosis`/`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`.
|
||||
- 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).
|
||||
- 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)
|
||||
|
||||
@@ -248,8 +248,8 @@ Base stats (from data): `maxHp, atk, def, res` (0–100), `aspd` (100 base), `ba
|
||||
| `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) |
|
||||
| `kill` | `{ killer, victim }` | victim hp reached 0 |
|
||||
| `death` | `{ unit }` | unit removed (ops may redeploy later) |
|
||||
| `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 |
|
||||
@@ -260,25 +260,26 @@ Base stats (from data): `maxHp, atk, def, res` (0–100), `aspd` (100 base), `ba
|
||||
|
||||
DamageInfo: `{ amount (pre-mitigation), type: 'phys'|'arts'|'true'|'element', element?: 'burn'|'necrosis'|'neural'|'apoptosis', atkScale, defIgnoreFlat, defIgnorePct, resIgnoreFlat, resIgnorePct, mul (multiplier), canDodge, isSkill, isSplash, 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; `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)`, `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 })` (流失; `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)`, `battle.after(seconds, fn)` (scheduled callbacks), `battle.every(seconds, fn)`.
|
||||
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)`, `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 its tile is free and DP ≥ cost ⇒ redeploy with full HP, SP = initSp). A knocked-out operator stays **down** on its tile from the knock-out until it redeploys (b.snap `down`, §18.3: counting / waiting for DP / its tile taken) — 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. 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 — 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). 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)`, 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. Ranged ops fire projectiles (speed ~12 tiles/s, instant for melee/arts-beam classes as data indicates); damage applies on impact.
|
||||
- **Healers** (`dmgType heal`): target the ally in range with the lowest HP% below 100%, never a 禁疗 / 孤立 unit (tokens.json `abnormal`, `noHeal`; §20.3); 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; 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); they pause movement for `ATTACK_PAUSE = 0.35 s` per attack. FLY enemies ignore ground obstacles (only blockFly units block them) and follow their checkpoint route. A feared enemy runs between random 恐惧可达地块 away from the source (PRTS 诱发移动; `fear.js`); 恐惧 outranks 诱导; the 恐惧 / 诱导 walks re-plan from where the enemy stands after a push or an obstacle change, and the route re-plans when they end (§20.4).
|
||||
- **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. 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. 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), 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); they pause movement for `ATTACK_PAUSE = 0.35 s` per attack. FLY enemies ignore ground obstacles (only blockFly units block them) and follow their checkpoint route. A feared enemy runs between random 恐惧可达地块 away from the source (PRTS 诱发移动; `fear.js`); 恐惧 outranks 诱导; the 恐惧 / 诱导 walks re-plan from where the enemy stands after a push or an obstacle change, and the route re-plans when they end (§20.4).
|
||||
- **Damage**: phys `max(A − max(0, D×(1−defIgnorePct) − defIgnoreFlat), 0.05·A)`; 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), `TAKE_DAMAGE` (on taking damage — every MANUAL 重装 skill), `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.
|
||||
Auto-trigger rules (`skillTrigger.rule` in data — the official 技能策略, PRTS 卫戍协议/帮助 §作战阶段 技能操作; §20.2): `DEFAULT` (SP ready AND about to attack/heal AND an enemy/injured ally inside the **initial** range, or an enemy the unit blocks; with `trigger.allies` + `hpAtMost` an ally of the skill grid at or below that HP ratio — 塞雷娅 S1 急救, whose cast replaces that attack and is withdrawn with its charge if the ally recovers first), `SKILL_RANGE` (a MANUAL skill with a 技能范围 of its own, not an attack-range change: any living enemy, stealthed ones included, on the skill range, checked every tick — "仅在技能范围内存在敌人(无视其不可选中)时释放技能"; 13 skills / 26 records), `TAKE_DAMAGE` (on taking damage — any damage, 无来源 included, never a 流失; every MANUAL 重装 skill, §21.12), `SP_FULL` (immediately — 执旗手 / 战术家 / 吟游者 MANUAL skills, and AUTO DP skills such as 伺夜 S1, 焰尾 S1, 凛御银灰 S1), `CUSTOM_RANGE` (enemy inside the custom grid), `SEARCH` (an enemy inside the initial range, checked every tick — no attack needed; 解放者 / 阵法术师 MANUAL skills), `GDGLOW_SKILL_2` (a valid target anywhere on the field: 荒芜拉普兰德, 纯烬艾雅法拉 S3), special ids. The class rows cover **every MANUAL skill** of the class and never an AUTO skill (AUTO skills keep their own rule — an inference: PRTS names whole classes; the data's `skillIndex 0` / `-1` sentinel argues the other way, §20.2). **Operation cooldown**: the engine auto-casts a MANUAL skill no sooner than `AUTO_OP_COOLDOWN` (3 s) after its previous cast and after the unit's battle-start deployment ("自动操作具有3s冷却,在完成一次操作或作战开始时部署的单位将进入冷却"; `Battle._deploy` initial, `battle.flags.startOpCooldown`); kits with an automatic cast of their own check `skill.opCooling`; a pending "next attack" never takes a second charge. PASSIVE skills are always on.
|
||||
|
||||
**SkillSpec** (authored per chess in `sim/content/kits/tierN.js`; values come from the chess's skill blackboard at its level — normal Lv4, elite Lv7 — so a spec is a *function of the blackboard*):
|
||||
```js
|
||||
@@ -290,7 +291,7 @@ export default {
|
||||
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'|... },
|
||||
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)
|
||||
},
|
||||
@@ -301,7 +302,7 @@ export default {
|
||||
```
|
||||
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), `chain` caster (bounce ×3, −15%), `funnel`, `blastcaster`, `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).
|
||||
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).
|
||||
|
||||
---
|
||||
|
||||
@@ -340,7 +341,7 @@ Solo FUNNY has 9 rounds (boss at R9); everything is read from `data/config.json
|
||||
```js
|
||||
{ playerId, seat, name, isBot, connected,
|
||||
alive, lp, bandId,
|
||||
funds, shop: { level, upgradePrice, slots: [ Slot ], frozen: bool, freeRefreshes: n, rewardOffer: null | { slots:[Slot], tier } },
|
||||
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
|
||||
@@ -349,14 +350,15 @@ Solo FUNNY has 9 rounds (boss at R9); everything is read from `data/config.json
|
||||
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:{} }
|
||||
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.
|
||||
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 or is sold (a sale is refused with `HAND_FULL` when there is no room for it).
|
||||
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 new piece'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).
|
||||
|
||||
@@ -365,6 +367,7 @@ Economy/shop/pool/merge/hand rules: exactly as research 00-INDEX §3–§4 (inco
|
||||
### 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, 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.
|
||||
@@ -380,7 +383,7 @@ Per match: pick stage (weighted among the mode's `config.modes[m].stages`, weigh
|
||||
|
||||
### 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: pick a band; each prep: level up on schedule, buy chess that raise bond counts or complete merges, sell leftovers, place melee on road tiles nearest the enemy path, ranged on highland/others, equip items to highest-tier ops, 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).
|
||||
Deterministic-ish heuristic player used for (a) AI teammates, (b) auto-play for disconnected humans (only auto-ready + auto-picks, not shopping, unless "AI 托管" is toggled), (c) tests. Strategy (§21.6): pick a band (weighted by starting LP); each prep: take offers, sell dead bench weight, fill the board, complete affordable merges, level on the curve, then buy only merge progress / lineup upgrades worth more than the refreshes their price would buy (shop odds of completing held pairs), freeze for an unaffordable third copy (坎诺特 keeps 5 funds back); build around a committed focus core bond (a teammate's main bond on its strip counts against it; bonds the mode switches off never) + a second bond; value dealers by DPS vs the round's DEF / RES (an attack on every enemy in range counts double — 阵法术师 / 轰击术师), with the range the unit is deployed with; place by the exposure model + rehearsal on the server's deploy map (no 深水区; a tactician's 援军 within `PlayerState.summonRange`; `g.move` judges legality); items by effect (META §1.5: 突变细胞 on the least valuable normal single below 6阶, a cell left in temp gets a hand slot made for it); 机变 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).
|
||||
|
||||
---
|
||||
|
||||
@@ -392,9 +395,9 @@ Each domain file `server/sim/content/{tokens,bonds,garrisons,items,bands,enemies
|
||||
- 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).
|
||||
- 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); 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). 悬赏决策 = cards.bounty `draft` (the PRTS 敌人轮选 table: no 战术特训, no 鸭爵 set; 105 cards), drawn uniformly; bounty cards carry `descRaw`; a multi-round card lasts two battles (`choices.js MULTI_ROUND_BOUNTY_BATTLES`, §20.6). 教鞭 = a random 战术特训 card (3 drawn, 1 taken).
|
||||
- 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 a boss group + cheap fill, R11 a 特异III giant group + tier-I fill; §21.2); 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).
|
||||
|
||||
---
|
||||
|
||||
@@ -416,11 +419,11 @@ S→C:
|
||||
- `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]]` — knocked-out operators waiting to redeploy, `state` 0 counting | 1 timer done, waiting for DP | 2 timer done, its tile taken (sim/constants.js `DOWN_STATE`); `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), `['layer', playerId, bondId, n]`, `['bounty', playerId, coins]`, `['deploy', id]`
|
||||
- `m.toast {kind, text}` · `m.ticker {text}` · `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, base: {…same}}` (shared/protocol.js `unitStatsEntry`) as its next battle starts (§18.5)
|
||||
- `b.ev` — `{ fieldId, ev: [ ... ] }` event tuples: `['spawn', UnitInfo]`, `['atk', srcId, tgtId, projKind]`, `['dmg', tgtId, amount, type]`, `['heal', tgtId, amount]`, `['skill', id, on:0|1]`, `['die', id, reason]` (reason `'killed'`, `'retreat'`, `'expired'` …; only `'killed'` plays an operator's knock-down sound), `['status', id, key, on]`, `['fx', kind, x, y, extra]` (`'hitCap' {id, n}` = a cancelled 限伤 hit, drawn as nothing, §20.12; `extra.form` = the enemy's model form from then on — `shared/protocol.js fxForm`, state like a spawn: never dropped by the client, §21.4), `['layer', playerId, bondId, n]`, `['bounty', playerId, coins]`, `['deploy', id]`
|
||||
- `m.toast {kind, text}` · `m.ticker {text, id, type, priority, playerId}` (the strip plays the highest priority first, §21.10) · `m.emote {playerId, id}` · `m.result {...}`
|
||||
- `m.unitStats {seq, round, units: [unitStatsEntry]}` — the answer 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? }` (`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'); both also on the prep-scouting `m.field` units (`Match.prepFieldMeta`), so a teammate's unit shows its owner's loadout in the detail card).
|
||||
`UnitInfo = { id, kind, side, ownerId, defId, name, tier, golden, spine, avatar, x, y, facing, dir, maxHp, skillIndex?, moduleId?, items?, form? }` (`dir` = the ally's deploy direction; the renderer picks the model (Back for UP, mirrored for LEFT) and the ground wedge from it; `skillIndex` = the ally's equipped skill (§16): the renderer plays that skill's Spine clip (`anims.skills[i]`) and the audio its own ON_SKILL_START sound; `moduleId` = an elite ally's equipped module (uniEquipId | 'none'); `items` = an ally operator's equipped item ids (absent without any; never on enemies or summons): the bond popup lists a 变形同构体 wearer from them and the detail card shows its 同构 chip and 装备 (§21.11); all three also on the prep-scouting `m.field` units (`Match.prepFieldMeta`), so a teammate's unit shows its owner's loadout in the detail card; `form` = an enemy's current model form (§21.4, a view built mid-battle starts in it)).
|
||||
|
||||
### 8.3 Exact view shapes (normative for match ⇄ UI)
|
||||
|
||||
@@ -429,7 +432,7 @@ S→C:
|
||||
{ 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, slots: [ { kind: 'chess', id, price: 0, sold } ] } },
|
||||
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)
|
||||
@@ -442,7 +445,7 @@ Piece = { uid, kind: 'chess'|'item'|'token', id, golden: bool, tier, items: [ {
|
||||
```
|
||||
`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 (隐匿, or an ally's 迷彩 — §20.2), 16 skillActive, 32 shielded, 64 invulnerable, 128 cold, 256 sleep, 512 flying.
|
||||
`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.
|
||||
|
||||
---
|
||||
@@ -473,7 +476,7 @@ view.holdPiece(uid, {row,col}|null) // keep a dropped piece on a tile w
|
||||
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); damage numbers (phys orange-white, arts purple, true white, heal green); projectiles drawn per kind, hit sparks and melee slashes, skill activation bursts and auras (§17.3); death fade for enemies, summons and devices — a knocked-out operator stays on its tile in its held Die pose under a redeploy ring (§18.3); leak flash at the objective. Fallback when Spine missing/failed: avatar in a rarity-coloured diamond with bob/lunge tweens. 60 fps target with ≤ 120 units. Picking (which unit a press / hover / dropped item is on) and the drag target follow one rule, `render/pick.js` — the tile under the pointer (§18.1); the Spine store never hands out a skeleton whose unload is in flight (§17.1).
|
||||
Visuals: oblique perspective board (tilted ~30°, trapezoid tiles), raised highland blocks with side faces, road/floor/forbidden styles per research 07 §7, pulsing red gates, blue objective, dashed hand pads. Units: Spine (Front/Back rule, feet anchored, shadow sprite), elite gold outline glow, tier chip, HP bar (green ally / red enemy), SP bar (skill ready glow), status icons, the element gauge row under the bars — icon + white bar of the remaining 元素值, element-coloured refill during a 爆发冷却 (§18.3, §20.8); enemies drawn × their official prefab factor (enemies.json `modelScale`, §20.4); an enemy's mode — the `form` of a sim fx (`shared/protocol.js fxForm`), or UnitInfo `form` for a view built later through `render/app.js renderInfo` — switches clip sets (`render/units.js FORMS`: 掠海漂移体 crawl, 暴鸰 bombed, 转译基底's forms, the 逐火 embers, the leaders' 重生, 守墓石像; closing clips timed from the fx `dur`), a one-off cast (`PROJ[kind].once`) plays once, and a local-only model's web alias is tinted (`ALIAS_TINT`: 灼热 / 炽焰源石虫, §21.14); damage numbers (phys orange-white, arts purple, true white, heal green); projectiles drawn per kind, hit sparks and melee slashes, skill activation bursts and auras (§17.3); death fade for enemies, summons and devices — a knocked-out operator stays on its tile in its held Die pose under a redeploy ring (§18.3); leak flash at the objective. Fallback when Spine missing/failed: avatar in a rarity-coloured diamond with bob/lunge tweens. 60 fps target with ≤ 120 units. Picking (which unit a press / hover / dropped item is on) and the drag target follow one rule, `render/pick.js` — the tile under the pointer (§18.1); the Spine store never hands out a skeleton whose unload is in flight (§17.1).
|
||||
|
||||
---
|
||||
|
||||
@@ -481,7 +484,9 @@ Visuals: oblique perspective board (tilted ~30°, trapezoid tiles), raised highl
|
||||
|
||||
Look & feel: research 06 §11–§12 (near-black green-grey panels, mint `#4ed8af` accents, gold prices, bracket frames, hexagon badges, bond discs, 7-segment countdown). Fonts: Noto Sans SC (Google Fonts with local fallback), Bender/Novecento self-hosted for numerals and Latin micro-labels.
|
||||
|
||||
Screens: **Title** (season art backdrop, 开始 → nickname), **Lobby** (独立模拟 / 同盟模拟 → create or join; recent rooms), **Room** (4 seats, invite code + copy link, difficulty picker (host), add/remove AI, ready, start), **Briefing** (INFO_CHECK: stage, factions, disabled bonds, banned chess, boss silhouette, ready x/4), **Band draft** (grid of 40 bands with icon/name/LP/effect; turn order list; skip), **Game** (Pixi field + DOM HUD: top bar with round/phase capsule/LP (live during the own battle, §17.5)/timer/ready; bond strip + popups; left team panel with LP/status/emote bubbles, click to watch; bottom shop bar (level card, 3–5 chess cards, item card, funds, refresh, freeze, collapse); detail panel on click/hover (header with bonds, then 特质 — the operator's own effect — right under it, 特性, stats (live: the battle's own numbers / the start-of-battle ones in prep, §18.5), skill, module, equipped items, talents: §17.5); enemy preview drawer (next round enemies with counts, factions); 机变 overlay (the official card: framed icon + name, the effect text — data `descRaw` — under it, the taker's round avatar at the top-right; two taps, §18.2, §20.7); merge reward overlay; phase banners; ticker; toasts; emote wheel; settings (volume, quality, show damage numbers)), **Result** (victory/defeat, rounds, per-player lineup, stats, titles, back to room).
|
||||
Screens: **Title** (season art backdrop, 开始 → nickname), **Lobby** (独立模拟 / 同盟模拟 → create or join; recent rooms), **Room** (4 seats, invite code + copy link, difficulty picker (host), add/remove AI, ready, start), **Briefing** (INFO_CHECK: stage, factions, disabled bonds, banned chess, boss silhouette, ready x/4), **Band draft** (grid of 40 bands with icon/name/LP/effect; turn order list; skip), **Game** (Pixi field + DOM HUD: top bar with round/phase capsule/LP (live during the own battle, §17.5)/timer/ready; bond strip + popups; left team panel with LP/status/emote bubbles, click to watch; bottom shop bar (level card, 3–5 chess cards, item card, funds, refresh, freeze, collapse); detail panel on click/hover (header with bonds, then 特质 — the operator's own effect — right under it, 特性, stats (live: the battle's own numbers / the start-of-battle ones in prep, §18.5), skill, module, equipped items, talents: §17.5); enemy preview drawer (next round enemies with counts, factions); 机变 overlay (the official card: framed icon + name, the effect text — data `descRaw` — under it, the taker's round avatar at the top-right; two taps, §18.2, §20.7); merge reward overlay (the promotion reward, or a special offer — 凯瑟琳's 定向投放 … — as item / chess cards under its source's label, with the count of offers queued behind it, §21.2); phase banners; ticker (by broadcast priority, highest first; a leader-damage line plays only in its round and a player's newer one replaces the older, §21.10); toasts; emote wheel; settings (volume, quality, show damage numbers)), **Result** (victory/defeat, rounds, per-player lineup, stats, titles, back to room).
|
||||
|
||||
A bond the mode never activates (标准: official `inactiveBondIdList`) reads 本局禁用 on shop / reward cards (struck through), detail chips and the popup — no 在场 count, no 当前效果 (§21.7).
|
||||
|
||||
Interactions (research 09 §1.2 / §5, which supersede the first draft): shop card, upgrade and 机变 card are two-tap (select → confirm, §18.2) — a tap anywhere on the card, its confirm strip included, is the card's tap (shop cards have no ⓘ corner; the armed bar sits above the detail card; a shop card that cannot be bought right now opens its detail on tap, §20.7); a press / drop goes by the tile under the pointer (§18.1); drag bench↔board — a legal board drop (also onto the piece's own tile) opens the 4-direction deploy wheel (swipe UP/RIGHT/DOWN/LEFT with the rotated range striped orange; release in the centre, ✕ 点击取消 or Esc cancels); tap a unit's tile ⇒ underframe with **出售 +N** (and **撤退** on the board; items/Arts in the hand / temp: **销毁** — equipped items are locked, §6.2) plus its rotated range; no drag-to-sell; drag an item onto a unit's tile ⇒ equip that unit (both slots used ⇒ the replace dialog `ui/equipReplace.js` shows the incoming and the two equipped items; the player picks the one to replace — it is destroyed — `g.equip replaceUid`; cancel changes nothing); Arts are dropped on a tile and get the direction step; right-click or long-press ⇒ detail (or a tap on a shop card while the bar is locked); `R` refresh, `F` freeze, `D` level-up, `Space` ready (in a solo battle: pause / resume, §14 Solo pause). All actions optimistic-free: UI waits for server state (latency is small).
|
||||
|
||||
@@ -515,6 +520,7 @@ Audio: autochess BGM per phase, UI SFX (buy/sell/refresh/level/merge/ready/timer
|
||||
- `emoticon/{basic,basic_2}/pic_<id>_battle.png` — the 12 official battle emotes (`shared/constants.js → EMOTES`, `emoteArtPath`).
|
||||
- `guide/autochess_{home,shop,handbook}_N.png` — the official tutorial pages; stored squashed to 1024² — **display at 16:9**.
|
||||
- `projectiles/*` — battle projectile sprites.
|
||||
- `spine/enemy/<id>/` — enemy models no web dump carries (灼热 / 炽焰源石虫, `extract.py ENEMY_SPINES`): 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.
|
||||
|
||||
---
|
||||
@@ -545,17 +551,17 @@ Audio: autochess BGM per phase, UI SFX (buy/sell/refresh/level/merge/ready/timer
|
||||
|
||||
**Result validation (server, `fields.js validateClientResult`).** A client result is accepted only if structurally valid and plausible for its spec: every spec player reported and nobody else; finite numbers; `killed ≤ total ≤ 4·spawns + 100`; every leaked `enemyKey` is in the spec's spawns (multiset bound) or a key the spawned enemies' data mentions (content summons/splits, bounded by the total) — boss fields excepted (their leaks cost team LP through `b.progress`); `counted: false` only for enemies that never count (data `notCountInTotal`, schedule `countInTotal: false`, boss/part entries, content spawns); 联防: leaks + `unspawned` per (enemy, leaker) ≤ what that leaker sent in (nobody's survivors are billed to a teammate); per-bond `layerGains ≤ min(60 + 4·round, 999 − the bond's starting layers)` (§20.12), none when the spec disables gains, bonds must exist and be named by the player's bond snapshot, band, effects, units or items; `coins ≤ Σ bounty coins of the spawns`; `perfect` consistent with the counted leaks; unit states only for the player's own board uids with hpPct ∈ [0, 1]. The accepted result is **rebuilt** from whitelisted fields (leak mods/tags/sources from the spec — a content-spawned enemy (split, summon) keeps the mods of the schedule entry they equal, i.e. its parent's round multipliers and bounty id, so its 联防 re-entry and bounty match the server run; unit names from server data). Otherwise (or on any exception) the server re-simulates (normal/联防); a boss field is **handed over** instead (below). `SP_VERIFY=all|sample|off` (default `off`): `all` re-simulates every accepted normal/联防 result before accepting it (the server result wins on a digest mismatch); `sample` checks ~1 battle in 8 in a later callback and logs mismatches. `Match.verifyStats = { checked, mismatches, rejected, takeovers }`. Desync between display-only clients and the authority is tolerated: the authoritative outcome is what counts and is broadcast at settlement.
|
||||
|
||||
**Boss rounds.** The shared pool (`SharedBossPool`, co-op size = `bloodPoint[difficulty]`, see corrections) lives on the server and is fed by the authorities' `b.progress` damage deltas (attributed per player through `by`, BOSS_HIT tickers as before) 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.
|
||||
**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; catch-up when visible again; the server deadline + takeover cover extreme cases). 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.
|
||||
**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; 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): route lengths stay the official ones, but among equal-length routes the one with the fewest non-blockable (floor / gate) tiles wins and smoothing never cuts across floor the grid route does not walk — e.g. 战场#01's lower-gate enemies take the col-8 road, not the col-9 floor lane (server/sim/grid.js header; test/sim/pathing-blockable.test.js).
|
||||
- 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.
|
||||
@@ -587,7 +593,7 @@ Official rule (bwiki 盟约, 更新公告 5114): before a match the player canno
|
||||
|
||||
**Sim:** `simdata` resolves a unit def for `(chessId, skillIndex, moduleId)` (cached): `skill` = the selected SkillRecord; stats = `statsBase` + module attr (elite) ; trait/talents = base with the selected module's overrides. Kit contract (backward compatible): a kit function `(bb, chess) => { skill?, skills?: { [skillId]: SkillSpec }, talents, trait }` receives `chess.skill` = the **selected** skill and `bb` = its blackboard; the loader uses `skills[selectedSkillId]` if present, else `skill` only when the selected skill is the chess's default, else the **generic** skill spec — talents/trait from the kit always apply. Every visible chess must get hand-authored specs for all of its selectable skills (tracked by a coverage test).
|
||||
|
||||
**UI:** a 干员调配 screen reachable from the lobby, room and briefing (INFO_CHECK): filter by tier/bond/class, per chess choose skill (icon, name, SP info, desc at Lv4 and Lv7) and elite module (official type icon from the local-client art when extracted (§13), else a lettered tile; name/type, stat bonus, trait/talent changes), 恢复默认, persisted; the chosen skill/module shown on shop cards and in the detail panel (a teammate's unit — 前往查看 in prep, 联防 / 最终攻势 fields, an observed battle — shows **its owner's** skill/module from its UnitInfo `skillIndex` / `moduleId`, `gameLogic.js unitLoadout`) — which also shows the stats, 特性, talents and attack range the unit fights with (`shared/loadoutRecord.js` `loadoutRecord` / `attackRangeGrid`, the same composition the sim's `getChess(id, loadout)` uses; the deploy wheel's range preview too). 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).
|
||||
**UI:** a 干员调配 screen reachable from the lobby, room and briefing (INFO_CHECK): filter by tier/bond/class, per chess choose skill (icon, name, SP info, desc at Lv4 and Lv7) and elite module (official type icon from the local-client art when extracted (§13), else a lettered tile; name/type, stat bonus, trait/talent changes), 恢复默认, persisted; the chosen skill/module shown on shop cards and in the detail panel (a teammate's unit — 前往查看 in prep, 联防 / 最终攻势 fields, an observed battle — shows **its owner's** skill/module from its UnitInfo `skillIndex` / `moduleId`, `gameLogic.js unitLoadout`) — which also shows the stats, 特性, talents and attack range the unit fights with (`shared/loadoutRecord.js` `loadoutRecord` / `attackRangeGrid`, the same composition the sim's `getChess(id, loadout)` uses; the deploy wheel's range preview too) — the range it is deployed with: a selected passive range skill (引星棘刺 S3 3-9) and the 特性's permanent 攻击距离 (信仰搅拌机 SPT-Y +1) included, over all 1284 loadouts equal to the battle's range at deployment; in battle and prep the card draws the live `range` (§21.16). In battle the renderer plays the equipped skill's own Spine clip and ON_SKILL_START sound (`UnitInfo.skillIndex`; `data/assets.json` lists icons, clips and sounds for every skill index).
|
||||
|
||||
---
|
||||
|
||||
@@ -605,7 +611,7 @@ v2.2 answered "a click selected the unit one row lower" with body picking: every
|
||||
|
||||
### 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 (blastcaster shells purple); `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.
|
||||
- **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.
|
||||
@@ -664,14 +670,14 @@ Thirteen reports after playing v2.2. Where each is handled: #1 picking should si
|
||||
|
||||
### 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). `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]; 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.
|
||||
- **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 — one bomb on its target and the 8 tiles around it (100 % ATK, splash ignores camouflage), then its speed ×2 (final); 帝国炮火先兆者 / 中枢先兆者 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).
|
||||
- **#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).
|
||||
@@ -689,9 +695,9 @@ Ten reports after playing v2.3 (绝境 on desktop, 标准 on an Android phone in
|
||||
### 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`; 浮空 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 `phase` `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 reload mid-battle replays no events: such a view hovers until then). 吉兆飞鳞 — 晕眩 / 沉睡 → 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 appear in this mode (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, 锏 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 掠海漂移体.
|
||||
- **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).
|
||||
@@ -787,7 +793,7 @@ Ten reports after playing v2.3 (绝境 on desktop, 标准 on an Android phone in
|
||||
- **#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); ground enemies cannot walk onto those tiles.
|
||||
- **Audit** (all 11 stages × normal / 联防 / boss L / boss R, server = client = builder deployTiles = the raw level files): 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].
|
||||
- **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`
|
||||
@@ -824,14 +830,14 @@ Nineteen reports after playing v2.4 (#1–#3 from a friend of the user). Where e
|
||||
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.
|
||||
**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); `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. 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).
|
||||
- **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`).
|
||||
@@ -861,22 +867,25 @@ Eight workstreams were merged into `playtest6` (WH → WG → WD → WC → WB
|
||||
### 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 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`).
|
||||
- **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 are radial except 推击手 / 朝部署方向; a directional push turns radial at −2 when the target is > 45° off or < 0.25 tile away (圣聆初雪 S1 waives both, 见行者 S2 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 and pulls hit air units (added to §19.1's 可对空 list), and during the slashes she is invulnerable (buff `blkkgt:slashes`) and refuses stun / freeze (integration follow-up of the WF note).
|
||||
- **#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; 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.
|
||||
- [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, 山 S3 (2nd hit) | 中力 radial push | 1.0 tile, −25 % per weight above 2 | 弹道: w0 2.14, w1 1.7, w2 0.44, w3 0.12, ≥ 4 0 |
|
||||
| 野鬃 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, 圣聆初雪 S1 | 小力 / 中力 directional (圣聆初雪 fixed, ground only) | 0.5 / 1.0 tile along the facing, flyers too | push table along the facing |
|
||||
| 琳琅诗怀雅 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 too |
|
||||
| 锏 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 |
|
||||
|
||||
@@ -909,8 +918,8 @@ Eight workstreams were merged into `playtest6` (WH → WG → WD → WC → WB
|
||||
|
||||
- **#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; 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 and stays in the draft: `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).
|
||||
- **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` (教鞭).
|
||||
@@ -939,7 +948,7 @@ The user's answers of 2026-10-01 (playtest #6 follow-up):
|
||||
|---|---|---|
|
||||
| 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, in the draft | `server/match/choices.js MULTI_ROUND_BOUNTY_BATTLES = null` (official 每场) |
|
||||
| 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`) |
|
||||
@@ -951,6 +960,7 @@ Official since the follow-up — read from the client binary (research 11, §20.
|
||||
| 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).
|
||||
@@ -1101,3 +1111,241 @@ In SETTLE it is the field the last battle left on screen. In prep without watchi
|
||||
- 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)
|
||||
|
||||
Twenty-seven reports after the public 0.1.0 release (2026-10-02), relayed by the user in five 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); #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).
|
||||
|
||||
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; 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]. 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.
|
||||
|
||||
### 21.1 拉普兰德's first refresh; 突变细胞 is not consumed (#1, #4) — `sim/content/garrisons/meta.js SERVER_GAIN_BOND_LAYER_BY_REFRESH_CNT`, `match/PlayerState.js pieceRoundCount / bumpPieceRoundCount / _mergeChess / transformChess`, `match/effectsMeta.js ctx.pieceCounter / incPieceCounter / transform`, `match/builtinMeta.js char_chess_transformation_equip`, `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 now**: after every battle the carrier becomes a random NORMAL operator one tier higher (max 6; an elite carrier too) on its tile when legal; its equipment, the cell included, returns to the hand (overflow temp; with both full it stays on the new operator) — before the new operator's or elite's summon cards (as `_mergeChess` already does), so a 狼群 card never pushes the cell into temp. 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) and make a hand slot for a cell left in temp (`makeHandRoom`).
|
||||
- [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); the carrier transforms wherever it stands; a 6阶 carrier becomes another 6阶.
|
||||
- Tests: `test/match/feedback1-meta.test.js`, `test/ui/feedback1-cell.test.js`; `items.test.js`, `merge.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_TEMPLATES / BOUNTY_HUNTER_TEMPLATES / BOUNTY_HUNTER`, `match/choices.js bountyDraftCards`, choices.json `bountyDrafts` / `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** (33 screenshots of 11 co-op 绝境 / 终极 matches 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); a draft holds 6 different cards in varying positions.
|
||||
- **R3 (11 / 11)**: six 接下来两场作战 cards, I I I II II III (10 coins), one of 10 fixed sets (enemy_initial_1..10); seen: series 18 ×4, 19, 17 and 4 mixed sets of one card from each of 6 of the series 10–15 / 20.
|
||||
- **R9 (12 / 12)**: boss bounties X·悬赏 + 源石虫·特训, 下场作战 — a group of 0–3 named bosses that always come together, plus cheap fill (W / 碎骨 / 弑君者, 大鲍勃, 庞贝 or 鼠王, 源石虫·特训); 7 groups seen for 6 events (two game versions, or a group set per match — open).
|
||||
- **R11**: 悬赏决策 5, 机密商店 4, 战术决策 2, 道具补给 0 of 11; a bounty draft = a group (one 特异III giant + 1–4 tier-II cards, always together; matches 1 and 6 share the whole group) filled with tier-I cards, at most one per faction series 10–15; 4 groups seen for 15 events.
|
||||
- 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_*, 凋零骑士 / 遗弃者 / 锏 / 扎罗 / 迷路的巨像). R3 = a seen set, or the mixed rule for the 3 unseen slots; R9 = a seen group + its fill; R11 = a seen group (4 of 15 slots) or a rule group of the same shape, then tier-I fill; 绝境 / 终极 R11 families: bounty 45 / shop 35 / tactic 20 (was supply 50 / shop 50). `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 or group to the build-data constants and rebuild.
|
||||
- **#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 3 unseen R3 sets (mixed rule) and 险境 R6 drafting like R3; uniform among the seen R9 groups, unseen bosses not offered; the R11 group / fill split by tier, the 11 unseen R11 slots, uniform among the events; the R11 weights; solo 绝境 / 终极 R11 = the co-op weights (no solo screenshot; the data has 15 solo `bounty_hunter_*_s` events); solo shows 3 of 6. Not changed: 机密商店 and 战术决策 can repeat a card officially; the remake's never do.
|
||||
- Tests: `test/match/feedback1-bounty.test.js`, `test/ui/feedback1-offers.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` (`SP_E2E=1`).
|
||||
|
||||
### 21.3 No deployment in the water; tactical points inside the tactician's range (#3, #9) — `sim/grid.js DEPLOY_REFUSED_TILES / canStand`, `tools/build-data.mjs effectiveBuildable / ownerRange`, `match/board.js ownerRangeKeys`, `match/PlayerState.js _legal / summonRange / _liftOutOfRange / _reorient`, `ui/gameLogic.js summonRange`, `match/bot.js`, `match/invariants.js`, `content/kits/tier3.js tacticalPoint`
|
||||
|
||||
- **#3 seen**: "有水池的那张图干员可以错误的被部署到水里,突袭分队干员也会自动落到水里" — the 深水区 (`tile_deepsea`) of 战场#08(下半) 涨潮控制 (act2 m04): board (10–12, 6), partner (10–12, 14), boss (3–5, 6) / (3–5, 14). **Cause**: the level gives it buildableType ALL and the legend copied it; the deploy maps (server, client, bots) 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).
|
||||
- **#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), 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, 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); 隐匿 returns at once after a block (PRTS: 3 s); the 重生 whitelist.
|
||||
- Tests: `test/sim/feedback1-transform.test.js`, `test/match/feedback1-runner-forms.test.js` (stalls, hidden tabs, a battle ending while hidden, compaction, the runner feeding the render buffer), `test/render/interp.test.js`, `test/render/forms.test.js`, `test/content/enemies_bosses.test.js`; opt-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, 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).
|
||||
- [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.
|
||||
- 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 and with its closing clip shortened by the lateness. 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 four lpr-0 enemies of the database (简饲源石虫, 节日气球, 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.
|
||||
- Tests: `test/sim/feedback1-followups.test.js`, `test/sim/feedback1-integrate.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_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 33 screenshots of 11 matches (R3 fixed sets, R9 boss groups + fill, R11 giant groups + tier-I fill; no multi-round card in a draft) | add a newly seen set / group to `tools/build-data.mjs BOUNTY_INITIAL_SETS / BOUNTY_BOSS_TEMPLATES / BOUNTY_HUNTER_TEMPLATES` 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 (§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 物理流失 (§21.19).
|
||||
|
||||
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 6阶 突变细胞 carrier becomes another 6阶.
|
||||
- §21.2: the 3 unseen R3 sets and 险境 R6 drafting like R3; uniform among the seen R9 groups; the R11 group / fill split, the 11 unseen R11 slots and the R11 weights (solo = co-op); solo shows 3 of 6. Open for the user: all six cards of an R11 悬赏决策, an R9 screenshot with the match's 确认本局信息 page and date, solo 绝境 / 终极 R3 / R9 / R11.
|
||||
- §21.3: the range is the loadout grid (items and bonds do not widen it); the canoe in the deploy map only.
|
||||
- §21.4: the translator immobile and damage-cancelled through its change; the 特战术师 form hits flyers (open for the user); the ember's 10 s after the 重生; 隐匿 back at once after a block; the 重生 whitelist; “余音” unmapped.
|
||||
- §21.5: the 鸭爵 swap's 0–2 uniform count and pick, the 60–99 % window, ground only, elites eligible, per-half on pair fields, decided at battle start.
|
||||
- §21.6: the bot's tuning constants and `CROWD`.
|
||||
- §21.7: strongest rather than earliest for 奥术, an equal re-hit refreshing it; the 本局禁用 look; 整备 next to an owned white.
|
||||
- §21.8: fence tiles as 可部署地面 for 黄金盟誓; the absolute −10 % per kill of 玛恩纳 S3.
|
||||
- §21.9: a 静态刚体's 0.1 s 失衡硬直 not modelled; 守墓石像's carried 浮空 runs out; which pushes reach air units without a note.
|
||||
- §21.10: ≥ for "超过"; the per-battle `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.
|
||||
|
||||
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); 机密商店 / 战术决策 repeating a card as officially (§21.2).
|
||||
+3
-3
@@ -22,7 +22,7 @@
|
||||
| 联防 | 见第 5 节。 |
|
||||
| 结算 | 扣 LP、发放悬赏资金、结算层数;LP ≤ 0 的玩家淘汰(其干员回到公共卡池)。 |
|
||||
|
||||
**断线与离开**:同盟模拟断线后 10 分钟内重新打开页面即可回到原座位;期间你的阵容照常自动作战,机变和休整期到时自动选择 / 准备,但不会替你购买(想让 AI 代打,请在「离开模拟」里选「暂离(AI 托管)」)。独立模拟的休整期与机变不计时,断线或关掉页面后 **24 小时内**用同一个浏览器打开即可继续;独立模拟的作战(含最终攻势 / 隐秘核心)还可以点顶栏的「暂停」(或按 Space)随时暂停,倒计时和领袖战的加时扣血一起停下(暂停中断线会自动继续,由服务器接着打完这场作战)。同盟模拟的作战不能暂停。「放弃模拟」:独立模拟立即结束本局;同盟模拟中视为你被淘汰(干员回到公共卡池),队友继续。服务器重启会结束所有对局。
|
||||
**断线与离开**:同盟模拟断线后 10 分钟内重新打开页面即可回到原座位;期间你的阵容照常自动作战,机变和休整期到时自动选择 / 准备,但不会替你购买(想让 AI 代打,请在「离开模拟」里选「暂离(AI 托管)」;AI 托管不会为了腾地方销毁你的道具,整备区满时它会卖掉备战的干员来放道具)。独立模拟的休整期与机变不计时,断线或关掉页面后 **24 小时内**用同一个浏览器打开即可继续;独立模拟的作战(含最终攻势 / 隐秘核心)还可以点顶栏的「暂停」(或按 Space)随时暂停,倒计时和领袖战的加时扣血一起停下(暂停中断线会自动继续,由服务器接着打完这场作战)。同盟模拟的作战不能暂停。「放弃模拟」:独立模拟立即结束本局;同盟模拟中视为你被淘汰(干员回到公共卡池),队友继续。服务器重启会结束所有对局。
|
||||
|
||||
**回合数与机变回合**
|
||||
|
||||
@@ -72,9 +72,9 @@
|
||||
- **阻挡**看接触:干员阻挡中心距离约 0.7 格以内、与它接触的敌人(地面干员挡不住飞行和近地悬浮的敌人)。敌人卡在两名干员之间时,阻挡它的干员倒下、撤退或被晕眩后,另一名仍与它接触、还有空余阻挡数的干员会**立刻接替阻挡**;都没有空位时敌人才会走掉。迎面走来的敌人会停在干员格子前面。**阻挡了就一定能打到**:任何干员(包括站在近战位的远程干员和召唤物)总能攻击自己阻挡的敌人,不管它在不在攻击范围内、在干员哪一侧,并且优先攻击它(医疗干员除外:咒愈师以外的医疗挡住敌人时照常治疗队友,不会攻击它;「停止攻击」类技能期间也除外;技能原文另有限制的也按原文:普罗旺斯二技能开启时不以生命值高于 80% 的敌人为目标,寒檀二技能的冰凌随机落在攻击范围内的地块上)。
|
||||
- 开战时干员按**从上到下、从左到右**的顺序部署,召唤物在所有干员之后。被阻挡的敌人先打阻挡它的干员;没被阻挡的远程敌人先打嘲讽等级高的,同级时打最后部署的(召唤物、靠下靠右的干员和刚再部署的干员更容易挨打)。隐匿的干员只会被它阻挡的敌人攻击;**迷彩**(如忍冬三技能期间击倒敌人后、寒芒克洛丝一技能)同样只会被它阻挡的敌人攻击,也显示为半透明,但它不算隐匿(不触发叙拉古等「隐匿时」效果)。叙拉古的隐匿按时(32 秒 + 每层 0.4 秒)结束;忍冬的迷彩按官方持续到她下一次开技能。
|
||||
- **技能自动释放**按官方「技能策略」:一般的手动技能在技能就绪、即将攻击 / 治疗且初始攻击范围内有敌人(治疗技能:有受伤的队友)时释放;**带独立技能范围**的技能(如德克萨斯「剑雨」、凛御银灰「御敌的锋锐」、锏「归于宁静」,不含「攻击范围扩大」类)在**技能范围内出现敌人**时就释放,不必先攻击;**重装干员**的手动技能在受到伤害时释放;执旗手 / 战术家 / 吟游者技能就绪即释放,解放者 / 阵法术师在初始攻击范围内有敌人时释放。**自动触发**的技能按其本身规则,例如古米「备用军粮」在身边(技能范围内)有受伤的队友时就会发动,下一次攻击改为治疗该队友(不用先接敌;她的二技能「食粮烹制」是手动技能,按重装规则在受到伤害时释放);塞雷娅「急救」在她即将攻击时、身边有生命不高于一半的队友才发动,这次攻击直接改为治疗;新约能天使二技能的护盾按官方给攻击范围内**仇恨最高**的队友(嘲讽等级高的优先,同级时给最后部署的),不一定是站在最前面挨打的那个。手动技能的自动释放有 3 秒冷却:开战部署后先等 3 秒,之后同一干员两次自动释放至少间隔 3 秒(可充能的技能不会同时放完)。
|
||||
- **推开 / 拉拽**按官方「力度 − 重量」决定距离:例如野鬃二技能(中等力度)把重量 0 / 1 / 2 / 3 的敌人推开约 2.1 / 1.7 / 0.4 / 0.1 格,重量 4 以上推不动——轻型敌人反而推得更远。野鬃二技能沿她的朝向推开:正前方 45° 以内按上面的距离,身后、侧面或贴身的敌人只被轻推一下(约 0.4 格以内);琳琅诗怀雅三技能的金币则从她身边向外推开。攻击速度最低为 20(沼泽、寒霜等减速叠满时也不会更慢)。
|
||||
- **推开 / 拉拽**按官方「力度 − 重量」决定距离:例如野鬃二技能(中等力度)把重量 0 / 1 / 2 / 3 的敌人推开约 2.1 / 1.7 / 0.4 / 0.1 格,重量 4 以上推不动——轻型敌人反而推得更远。野鬃二技能沿她的朝向推开:正前方 45° 以内按上面的距离,身后、侧面或贴身的敌人只被轻推一下(约 0.4 格以内);琳琅诗怀雅三技能的金币则从她身边向外推开,落在她前方 2–4 格的范围(官方「范围2-4」)内的地面敌人和她阻挡的敌人身上。攻击速度最低为 20(沼泽、寒霜等减速叠满时也不会更慢)。
|
||||
- **元素损伤条**在单位自己的血条正下方:左边是元素图标,白条是剩余的元素值,只有真正爆发时才会耗尽;爆发后条变成元素的颜色、图标闪烁,在冷却期间慢慢回满(这段时间不会再积累元素损伤)。
|
||||
- **敌人**:远程敌人的射程按官方从干员的碰撞体积(半径 0.25 格)算起,比数据上的攻击范围半径远 0.25 格——例如萨卡兹枯朽战车(射程 2.2)能打到中心 2.45 格以内、站在低地的干员。它的第一下攻击就是「秽蚀轰击」(在目标处留下半径 1.7、持续 10 秒的污染区:低地每秒 50、高台 25 点真实伤害,同名不叠加),被阻挡时普通攻击伤害翻倍。“萨科塔之翼”等血量第一次低于一半时会在原地小范围乱飞 5 秒(官方叫「恐惧」);叙拉古、妮芙等干员造成的恐惧会让敌人朝远离干员的方向逃跑。隐匿的敌人只有被阻挡时才能被选为攻击目标:深池逐火战士每次被击倒后先重生 1 秒,然后变成继续往前走的隐匿「余烬」(需要 5 次伤害击倒,护卫的「火灰」10 次),一般要用干员挡住它才能选中(挡住时它不再半透明),10 秒内没打掉就满血站起来;敌人重生时身上的减速、恐惧等状态会被清空,被恐惧的逐火战士变成余烬后也能被挡住;转译基底·α 原始形态受到的伤害全部无效,受到第 4 次物理伤害、第 4 次法术伤害或被阻挡时花 2 秒变成寻仇者、特战术师或幽灵,之后才能被击倒。
|
||||
- **敌人**:远程敌人的射程按官方从干员的碰撞体积(半径 0.25 格)算起,比数据上的攻击范围半径远 0.25 格——例如萨卡兹枯朽战车(射程 2.2)能打到中心 2.45 格以内、站在低地的干员。它的第一下攻击就是「秽蚀轰击」(在目标处留下半径 1.7、持续 10 秒的污染区:低地每秒 50、高台 25 点真实伤害,同名不叠加),被阻挡时普通攻击伤害翻倍。“萨科塔之翼”等血量第一次低于一半时会在原地小范围乱飞 5 秒(官方叫「恐惧」);叙拉古、妮芙等干员造成的恐惧会让敌人朝远离干员的方向逃跑。隐匿的敌人只有被阻挡(或被揭示)时才能被选为攻击目标,溅射、技能的圆形范围伤害也打不到没被挡住的隐匿敌人(和官方一样):深池逐火战士每次被击倒后先重生 1 秒,然后变成继续往前走的隐匿「余烬」(需要 5 次伤害击倒,护卫的「火灰」10 次),一般要用干员挡住它才能选中(挡住时它不再半透明),10 秒内没打掉就满血站起来;敌人重生时身上的减速、恐惧等状态会被清空、技能冷却从头开始,被恐惧的逐火战士变成余烬后也能被挡住;转译基底·α 原始形态受到的伤害全部无效,受到第 4 次物理伤害、第 4 次法术伤害或被阻挡时花 2 秒变成寻仇者、特战术师或幽灵,之后才能被击倒。
|
||||
- **策略「鸭爵」(“神秘顾客”)**:从第 5 回合起,你和队友的每场作战(**最终攻势和隐秘核心也算**,联防不另外替换)有 0–2 名地面敌人(不含领袖及其部件、悬赏敌人)被替换为特殊版本的鸭爵、高普尼克、流泪小子或圆仔,出现在被替换敌人的时间和路线上(两人同场的领袖战里,各自只替换走向自己保护目标点的敌人);击倒者获得 1 资金(联防中的队友也算),漏进保护目标点照常扣 1 点目标生命值。鸭爵和圆仔**不攻击、无法被阻挡**:鸭爵受到伤害后移动速度 +300%;圆仔始终面向我方干员较多的一侧,来自正面的物理 / 法术伤害 −80%。流泪小子隐匿(被阻挡时才能被选中),攻击使目标晕眩 3.5 秒;高普尼克免疫晕眩、冻结和浮空。
|
||||
- 右上角 🔍▶▶ 可以查看本回合即将来袭的敌人(预览区),左上角 🔍 查看本局信息(禁用盟约、策略)。
|
||||
|
||||
|
||||
@@ -612,3 +612,58 @@ test('playtest6b QA residuals (DESIGN §20.14): the held boss result, the cue be
|
||||
assert.match(BALANCE, /\*\*2026-10-02 — the integrated build \(QA 6b/);
|
||||
assert.match(BALANCE, /measured on the boss-HP workstream's boards, \*\*before\*\* the elite-to-board merge/);
|
||||
});
|
||||
|
||||
test('player feedback after 0.1.0 (DESIGN §21, v0.1.1): every report mapped, the settled decisions, the normative lines — code and docs agree', async () => {
|
||||
const sec = (n) => DESIGN.slice(DESIGN.indexOf(`## ${n}.`), DESIGN.indexOf(`## ${n + 1}.`) > 0 ? DESIGN.indexOf(`## ${n + 1}.`) : undefined);
|
||||
const S21 = sec(21);
|
||||
assert.match(DESIGN, /## 21\. Player feedback after 0\.1\.0 \(v0\.1\.1\)/);
|
||||
for (let i = 1; i <= 20; i++) assert.match(S21, new RegExp(`### 21\\.${i} `), `§21.${i}`);
|
||||
const intro = S21.slice(0, S21.indexOf('### 21.1 '));
|
||||
for (let i = 1; i <= 10; i++) assert.match(intro, new RegExp(`#${i} `), `the intro maps report #${i}`);
|
||||
for (const r of ['B1', 'B2', 'B3', 'B4', 'B5', 'C1', 'C2', 'D1', 'D2', 'D3', 'D4', 'D5', 'E1', 'E2', 'E3']) assert.match(intro, new RegExp(`${r} `), `the intro maps report ${r}`);
|
||||
const sub = (n) => { const a = S21.indexOf(`### 21.${n} `); const b = S21.indexOf('\n### 21.', a + 5); return S21.slice(a, b > 0 ? b : undefined); };
|
||||
// settled: the tactician re-orientation (user-confirmed) and the screenshot-based bounty structures
|
||||
const s2120 = sub(20);
|
||||
assert.match(s2120, /A tactician re-oriented in place \(§21\.3\) \| a range-bound summon still inside the new range stays[^\n]*user-confirmed 2026-10-03/);
|
||||
assert.match(s2120, /Bounty drafts \(§21\.2\) \| the official structure of each round from the user's 33 screenshots/);
|
||||
assert.match(s2120, /MULTI_ROUND_BOUNTY_BATTLES = null/);
|
||||
// §21.2 = §7 = §20.6 superseded = the data
|
||||
assert.match(sub(2), /initial 42, boss 20, hunter 24 \(86 drafted\)/);
|
||||
assert.match(sec(7), /86 drafted cards: R3 a fixed set of six 两场 cards/);
|
||||
assert.match(sec(20), /drawn uniformly, no multi-round cap — \*\*superseded by §21\.2\*\*/);
|
||||
const pools = {};
|
||||
for (const c of Object.values(DATA.choices.cards.bounty)) if (c && c.draftPool) pools[c.draftPool] = (pools[c.draftPool] || 0) + 1;
|
||||
assert.deepEqual(pools, { initial: 42, boss: 20, hunter: 24 }, 'choices.json draftPool counts');
|
||||
// §5.4 foesInRadius / §5.3 stealth vs area damage = the engine; splash radii = §5.6 = SIM
|
||||
const { Battle } = await import('../server/sim/Battle.js');
|
||||
assert.equal(typeof Battle.prototype.foesInRadius, 'function');
|
||||
assert.match(sec(5), /`battle\.foesInRadius\(x, y, r\)` \(the enemies an operator's area effect can select/);
|
||||
assert.equal(SIM_CONST.CHAIN_RADIUS, 1.7);
|
||||
assert.equal(SUB.bombarder.splashRadius, 0.9);
|
||||
assert.match(sec(5), /`bombarder` \(ground splash 0\.9 \+ aftershock\), `chain` caster \(bounce ×3, −15%, 1\.7-tile jumps/);
|
||||
assert.match(SIM, /1\.7-tile jumps \(constants\.js CHAIN_RADIUS/);
|
||||
assert.ok(!/\[OPEN\] PRTS 溅射半径一览/.test(SIM), 'SIM: the splash radii are no longer open');
|
||||
// the form fx are state in every client stage (§14 = §21.4 = SIM)
|
||||
const { isCosmeticEvent } = await import('../public/js/render/interp.js');
|
||||
assert.equal(isCosmeticEvent(['fx', 'phase', 0, 0, { id: 1, form: 'husk' }]), false);
|
||||
assert.equal(isCosmeticEvent(['fx', 'burst', 0, 0, {}]), true);
|
||||
assert.match(sec(14), /the render engine's event queue \(`render\/interp\.js isCosmeticEvent`\) never drop a form fx/);
|
||||
assert.match(SIM, /render engine's event queue \(`render\/interp\.js isCosmeticEvent`/);
|
||||
// the ticker priority (§8.2 = §10 = §21.10 = META = the server constant)
|
||||
const { FLOW_TICKER_PRIORITY } = await import('../server/match/Match.js');
|
||||
assert.equal(FLOW_TICKER_PRIORITY, 25);
|
||||
assert.match(sec(8), /`m\.ticker \{text, id, type, priority, playerId\}`/);
|
||||
assert.match(META, /The strip queues by `priority`, highest first/);
|
||||
// UnitInfo / unitStats / rewardOffer shapes (§8)
|
||||
assert.match(sec(8), /skillIndex\?, moduleId\?, items\?, form\? \}/);
|
||||
assert.match(sec(8), /moveSpeed, range\?, base: \{…same\}\}/);
|
||||
assert.match(sec(8), /rewardOffer: null \| \{ tier, source: 'merge'\|'special', label, queued, slots/);
|
||||
// §6.6 describes the new bot; the old one-liner is gone
|
||||
assert.ok(!/buy chess that raise bond counts or complete merges, sell leftovers, place melee on road tiles nearest the enemy path/.test(DESIGN), '§6.6: the 0.1.0 bot description is gone');
|
||||
assert.match(sec(6), /Strategy \(§21\.6\): pick a band/);
|
||||
// §14 ground pathing and §2 grid.js follow §21.15; §20.3 pushes follow §21.13
|
||||
assert.ok(!/smoothing never cuts across floor the grid route does not walk/.test(DESIGN), '§14: the 0.1.0 pathing sentence is gone');
|
||||
assert.ok(!/pathfinding \(8-dir, no corner cutting\)/.test(DESIGN), '§2: grid.js line updated');
|
||||
assert.ok(!/Pushes are radial except 推击手/.test(DESIGN), '§20.3: the push rule follows the client templates');
|
||||
assert.match(sec(20), /\| 野鬃 S2 \| 中力 directional \(knockback\[dir\], §21\.13\)/);
|
||||
});
|
||||
Reference in new issue
Block a user