Playtest #6 fixes: summons placed by hand, boss pool stall, bounties, 联防 leak counter, distinct offers, 机变 card text, phone buy tap, skill triggers, blocking, push force, 孤立, enemies, element bar
- #1 #2 skill / device summons (赫默's drone, 巫恋's doll, 凯瑟琳's devices) are hand cards placed by the player; placed summons deploy once at battle start, then a skill summon re-appears at its tile on each cast (the user's call) - #3 叙拉古 隐匿 ends on time (32 + 0.4 × layers s); 忍冬 S3 迷彩 lasts until her next cast (official), kept apart from 隐匿 - #4 multi-round bounty cards last two battles (the user's call; MULTI_ROUND_BOUNTY_BATTLES), blue / red card text - #5 the shared boss pool's float dust (≈3.6e-12 HP) stalled Final Assault leaders at 0 %; < 1 HP now clears the pool, and the team LP reaching 0 first is a defeat (PRTS) - #6 #10 机变 cards show their official effect text; an invisible 详情 button no longer swallows the confirming tap on phones - #7 live ×N counter of a leaker's enemies still on the 联防 field (LP loss stays capped at 10) - #8 #15 #16 skill triggers per 技能策略: skill-range triggers, injured-ally heals (古米, 塞雷娅), tank take-damage triggers with a 3 s cooldown; 新约能天使 S2 shields the highest-aggro operator in range - #9 official per-enemy model scales (威龙) - #11 element bar under the unit's HP bar (white = 元素值 left, element-coloured refill after a burst), floor rounding - #12 枯朽战车 kit per PRTS; every ranged enemy reaches its target's 0.25 collider - #13 恐惧 wander (萨科塔之翼 at half HP, operator fears make enemies flee) with re-planning after pushes - #14 push / pull distance by 力度 − 重量 (PRTS table); #17 every blocker attacks what it blocks; ASPD floor 20 - #18 炎佑 is 孤立: no heals or ally buffs - #19 every free pick-one chess offer (promotion reward, 信标, 寻呼模块) shows different operators - DESIGN §20 and the normative lines, SIM / META / DATA / PLAYING updated; 2747 unit tests and all 27 browser suites green; dev recordings regenerated
This commit is contained in:
1 parent
e6628f0cfe
commit
6464e4b46a
138 files changed
+7946
-1379
No files matched your search
+173
-42
@@ -69,6 +69,7 @@ server/
|
||||
projectiles.js projectile flight/impact
|
||||
skills.js skill runtime: SP, triggers, durations, ammo, charges; interprets SkillSpec
|
||||
ai.js enemy movement/attack AI, operator attack loop
|
||||
fear.js 恐惧 movement of enemies (reachable-tile fan, random checkpoints, self-fear flutter; §20.4)
|
||||
snapshot.js compact serialization for clients
|
||||
content/
|
||||
index.js installs all content into a Battle (kits, bonds, garrisons, items, bands, enemies, devices)
|
||||
@@ -128,7 +129,7 @@ Ownership rule for parallel agents: **only edit files assigned to you**; if you
|
||||
- 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).
|
||||
- **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溅射半径").
|
||||
- **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).
|
||||
|
||||
@@ -211,7 +212,7 @@ b.on(event, fn, { priority=0, owner } ) / b.off(...) // hook bus (§5.4)
|
||||
Base stats (from data): `maxHp, atk, def, res` (0–100), `aspd` (100 base), `bat` (base attack time, s), `blockCnt`, `rangeGrid` (ops) / `rangeRadius` (enemies, 0 = melee), `moveSpeed`, `massLevel`, `weight`, `lpr` (enemies), `dmgType` ('phys'|'arts'|'heal'|'true'), `motion` ('WALK'|'FLY'), `cost`, `respawnTime`, `spRecovery`.
|
||||
|
||||
**Aggregation** (`units.js`, recomputed lazily when buffs change):
|
||||
- `ATK = (base + Σflat) × (1 + Σpct) × Πmul` — same shape for `maxHp`, `def`; `res = clamp((base + Σflat) × Πmul, 0, 100)`; `aspd = clamp(100 + Σaspd + base-100, 10, 600)`.
|
||||
- `ATK = (base + Σflat) × (1 + Σpct) × Πmul` — same shape for `maxHp`, `def`; `res = clamp((base + Σflat) × Πmul, 0, 100)`; `aspd = clamp(100 + Σaspd + base-100, 20, 600)` (`constants.js ASPD_MIN` 20: PRTS 数值范围 "ATTACK_SPEED 默认下限 20", §20.3).
|
||||
- `interval = bat × (1 + ΣbatPct) × 100 / aspd` (blackboard `base_attack_time` is treated as `batPct`, e.g. −0.3 → 70%).
|
||||
- Changing `maxHp` keeps the HP **ratio**.
|
||||
- Stats are rounded only for display; the sim keeps floats.
|
||||
@@ -223,8 +224,8 @@ 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), invulnerable, unblockable, levitate, fear (enemy: cannot attack), taunt (+aggro), cold`.
|
||||
- **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`, `burnGauge`/`necrosis`/`neural` element gauges (see damage), `stealth`, `reveal`.
|
||||
- `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)`.
|
||||
- **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`.
|
||||
|
||||
### 5.4 Hook bus (the only way content plugs in)
|
||||
@@ -253,25 +254,25 @@ 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.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; records into result), `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; `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; records into result), `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); 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).
|
||||
- **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). A **melee** blocker (data `position` MELEE — 重装要塞 / 领主 / 哨戒铁卫 included) may always target the enemies it blocks, in range or not, and targets them first ("可以选择且优先选择阻挡单位", PRTS 选择器; 索敌的概念 "我方索敌优先级:阻挡(近战限定)"; `Battle.blockedTargets`, `targeting.js meleeUnit`); a ranged operator on a melee tile blocks but attacks only what its range holds ("远程位干员通常而言不会优先攻击自己阻挡的目标", §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 (melee units only, in range or not), (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%; 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 within radius: 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.
|
||||
- **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).
|
||||
- **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` (脆弱 = "受到的物理、法术、真实伤害提升"); 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.
|
||||
- **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).
|
||||
- **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): `DEFAULT` (SP ready AND about to attack/heal AND an enemy/injured ally inside the **initial** range), `TAKE_DAMAGE` (on taking damage), `SP_FULL` (immediately), `CUSTOM_RANGE` (enemy inside the custom grid), `SEARCH` (an enemy inside the initial range, checked every tick — no attack needed), special ids. 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 — 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.
|
||||
|
||||
**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
|
||||
@@ -311,7 +312,7 @@ LOBBY(room) → INFO_CHECK (co-op 25 s; solo and any single-human match untimed
|
||||
a card is picked with two taps, §18.2)
|
||||
PREP (co-op: timer from config, ends when all alive humans+bots ready; solo / single human: untimed, ends on ready)
|
||||
PREP_END (<休整期结束时> effects; unfrozen shop cleared; temp overflow: Ready is blocked while temp is non-empty; at the deadline
|
||||
the temp pieces due at this prep are resolved (chess sold back, items / summon stacks destroyed) — pieces that arrived
|
||||
the temp pieces due at this prep are resolved (chess sold back, items destroyed, summon stacks removed — they come back at the next round start, §20.1) — pieces that arrived
|
||||
after Ready or during the <休整期结束时> effects wait for the next prep (§6.2 tempDue))
|
||||
COMBAT (one Battle per alive player, all in parallel, 2×; watchers get snapshots; solo: pausable, §14 Solo pause)
|
||||
UNITE (co-op only, if ≥1 leaked & ≥1 perfect: ≤2 perfect helpers in `unite.js helperOrder` order (research 08 §5 / PRTS 帮助:
|
||||
@@ -321,7 +322,8 @@ LOBBY(room) → INFO_CHECK (co-op 25 s; solo and any single-human match untimed
|
||||
PRTS "上一阶段为退场状态的干员强制退场", its full redeploy timer — confirmed by the user (§19.3))
|
||||
SETTLE (LP −min(leaks,10) per player (unite survivors to their source); eliminations; layer gains applied; coins; broadcast)
|
||||
R14 FINAL_ASSAULT (merged team LP; pairs by seat; shared boss pool; 120 real s countdown, no hard stop; overtime −1 team LP per
|
||||
real s after 150 real s (§4, m.public.overtimeAt); no layer gains)
|
||||
real s after 150 real s (§4, m.public.overtimeAt); no layer gains; team LP 0 ⇒ defeat at once (PRTS 直接失败),
|
||||
pool 0 ⇒ victory — the first end the server registers decides, §20.5)
|
||||
R15 HIDDEN_CORE (if difficulty ≥ NORMAL and Σ activated layers > 350 solo / 1200 co-op and team LP > 1)
|
||||
→ RESULT (per-player stats, titles, rounds passed, victory/defeat)
|
||||
```
|
||||
@@ -346,11 +348,13 @@ Piece = { uid, kind:'chess'|'item'|'token', id /* chessId (golden id if elite) |
|
||||
|
||||
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.
|
||||
|
||||
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).
|
||||
|
||||
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 to hand + reward offer of 3 free chess of tier `min(level+1, 6)`; purchase completing a merge allowed with full hand; full hand blocks buy; overflow to temp; temp blocks ready).
|
||||
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 to hand + reward offer of 3 **different** free chess of tier `min(level+1, 6)` (copy-weighted; a tier short of different chess tops up from the tier below [ASSUMED], §20.6); purchase completing a merge allowed with full hand; full hand blocks buy; overflow to temp; temp blocks ready).
|
||||
|
||||
**Temp overflow (临时整备区, row 8; user playtest #3 item 3).** Passive gains that find the hand full (merge elites, grants, battle-result rewards, equipment returned by a merged / sold carrier) overflow into the 5 temp slots, and Ready is refused while any is occupied (official "直到溢出情况排除才可开始进行作战"). A temp piece is resolved — a chess sold back to the pool with its summons removed, items / summon stacks destroyed — only at the deadline of the **first prep in which its player could act on it** (`PlayerState.tempDue(piece)` vs `prepsEnded`; every write into a temp slot goes through `_putTemp`, which records it): a piece that arrived during a prep before Ready is due at that prep's end; one that arrived after Ready or during the `<休整期结束时>` effects, or outside PREP (COMBAT, 联防, SETTLE — battle-result grants, a SETTLE merge's elite, returned equipment —, the next ROUND_START, 机变) is kept, visible and usable (move to a free hand slot, place, equip, use, destroy, sell) through the **next** prep. Un-readying (`setReady(false)`) makes whatever arrived while ready due at the current prep. ROUND_START no longer wipes temp (it used to destroy what SETTLE had put there before the player ever saw it). A merge's elite always goes to the hand, overflowing into temp (also outside PREP); it takes a freed board tile of a consumed copy only when the hand and temp are both full. `server/match/audit.js` (`tools/matchrun.mjs --check`) tracks the arrivals itself and flags any temp piece that outlives its due prep. The UI frames the row on the board with the rule (§17.5).
|
||||
**Temp overflow (临时整备区, row 8; user playtest #3 item 3).** Passive gains that find the hand full (merge elites, grants, battle-result rewards, equipment returned by a merged / sold carrier) overflow into the 5 temp slots, and Ready is refused while any is occupied (official "直到溢出情况排除才可开始进行作战"). A temp piece is resolved — a chess sold back to the pool with its summons removed, items destroyed, a summon stack removed (it comes back at the next round start, PRTS "干员所属召唤物会于下一回合返还", §20.1) — only at the deadline of the **first prep in which its player could act on it** (`PlayerState.tempDue(piece)` vs `prepsEnded`; every write into a temp slot goes through `_putTemp`, which records it): a piece that arrived during a prep before Ready is due at that prep's end; one that arrived after Ready or during the `<休整期结束时>` effects, or outside PREP (COMBAT, 联防, SETTLE — battle-result grants, a SETTLE merge's elite, returned equipment —, the next ROUND_START, 机变) is kept, visible and usable (move to a free hand slot, place, equip, use, destroy, sell) through the **next** prep. Un-readying (`setReady(false)`) makes whatever arrived while ready due at the current prep. ROUND_START no longer wipes temp (it used to destroy what SETTLE had put there before the player ever saw it). A merge's elite always goes to the hand, overflowing into temp (also outside PREP); it takes a freed board tile of a consumed copy only when the hand and temp are both full. `server/match/audit.js` (`tools/matchrun.mjs --check`) tracks the arrivals itself and flags any temp piece that outlives its due prep. The UI frames the row on the board with the rule (§17.5).
|
||||
|
||||
### 6.3 Bonds & layers (bondsMeta.js)
|
||||
|
||||
@@ -383,7 +387,7 @@ Each domain file `server/sim/content/{tokens,bonds,garrisons,items,bands,enemies
|
||||
- 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); devices/terrain.
|
||||
- choices: 机变 families (悬赏决策 bounties, 道具补给, 机密商店, 战术决策 team/personal buffs).
|
||||
- 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).
|
||||
|
||||
---
|
||||
|
||||
@@ -400,11 +404,11 @@ Platform additions (implemented): `welcome.{resumed, version}`, `room.state.inMa
|
||||
### 8.2 Match
|
||||
C→S (all carry `rid`): `g.infoReady` · `g.band {bandId}` · `g.bandSkip` · `g.bandFocus {bandId?}` (the strategy highlighted in the draft screen — what a timed-out turn takes; absent / null clears it; `WRONG_PHASE` outside BAND_DRAFT, `ALREADY` after the pick, `BAD_TARGET` for a band not allowed; §18.2) · `g.buy {slot}` · `g.refresh` · `g.freeze` · `g.levelUp` · `g.sell {uid}` · `g.move {uid, to:{area:'board', row, col}|{area:'hand', idx}, dir?}` · `g.equip {itemUid, targetUid, replaceUid?}` (`replaceUid`: which equipped item a third one replaces, §6.2) · `g.art {itemUid, row, col, dir?}` (`dir` ∈ UP|RIGHT|DOWN|LEFT from the deploy wheel, §3; absent ⇒ `to.dir`, then RIGHT; `g.move` onto the piece's own tile re-orients it; board pieces in `m.private` / `m.field {prep:true}` carry `dir`) · `g.destroy {uid}` (hand / temp items only; equipped items are locked, §6.2) · `g.reward {idx}` · `g.choice {idx}` · `g.ready {ready}` · `g.emote {id}` · `g.watch {fieldId}` · `g.autoplay {on}` · `g.pause {on}` (solo battles only, §14 Solo pause) · `g.unitStats {seq?}` (ROUND_START / SP_DRAFT / PREP, else `WRONG_PHASE`; eliminated: `ELIMINATED`) → push `m.unitStats` (§18.5) · `g.leave`
|
||||
S→C:
|
||||
- `m.public` — full public state on every change (throttled ≤ 10/s): `{ phase, round, lastRound, deadline, serverNow, modeId, difficulty, stageId, factions, disabledBonds, bannedChess, bossId, hiddenBossId?, teamLp?, bossHp?:{hp,max}, overtimeAt? /* FA / HC: ms epoch the overtime drain starts, §4 */, paused /* solo pause, §14 */, draft?:{order,turn,picks,skipsLeft,turnDeadline,turnSeconds /* one turn's length, 0 untimed; deadline = turnDeadline (§18.2) */,untimed}, sp?:{family,cards,turn,picks}, players:[{playerId, seat, name, isBot, connected, alive, lp, bandId, shopLevel, boardCount, ready, bonds:[{bondId,count,active,tier,layers}], fieldId, status, pendingLp? /* COMBAT / 联防 of a normal round: the LP the player's own battle will cost at settlement so far, min(lpCapPerRound, counted leaks) — omitted when 0 and in every other phase (§17.5 live LP) */}] , fields:[{fieldId, kind, players:[playerId...]}] }`
|
||||
- `m.public` — full public state on every change (throttled ≤ 10/s): `{ phase, round, lastRound, deadline, serverNow, modeId, difficulty, stageId, factions, disabledBonds, bannedChess, bossId, hiddenBossId?, teamLp?, bossHp?:{hp,max}, overtimeAt? /* FA / HC: ms epoch the overtime drain starts, §4 */, paused /* solo pause, §14 */, draft?:{order,turn,picks,skipsLeft,turnDeadline,turnSeconds /* one turn's length, 0 untimed; deadline = turnDeadline (§18.2) */,untimed}, sp?:{family,cards /* bounty cards carry descRaw?, §20.6 */,turn,picks}, players:[{playerId, seat, name, isBot, connected, alive, lp, bandId, shopLevel, boardCount, ready, bonds:[{bondId,count,active,tier,layers}], fieldId, status, pendingLp? /* COMBAT / 联防 of a normal round: the LP the player's own battle will cost at settlement so far, min(lpCapPerRound, counted leaks) — in 联防 min(lpCapPerRound, uniteLeft) — omitted when 0 and in every other phase (§17.5 live LP) */, uniteLeft? /* 联防: the leaker's enemies still standing — not spawned yet + alive + counted leaks, uncapped, live (§20.6) */}] , fields:[{fieldId, kind, players:[playerId...]}] }`
|
||||
- `m.private` — the recipient's full PlayerState view (§6.2 minus server-only data) on every change.
|
||||
- `m.field` — field meta when watching starts: `{ fieldId, kind, rect, stageId, units: [UnitInfo] }` (the units on the field, knocked-out operators waiting to redeploy included — `Battle.fieldMeta`, §18.3)
|
||||
- NOTE: game time in `b.snap`/`b.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 0..1, cooldownEnd (game s), cooldown (s)]]` — the one gauge a unit shows: the fullest (ties by the official element id 神经 < 侵蚀 < 灼燃 < 凋亡), during a 爆发冷却 the bursting element with fill 1 and the cooldown numbers (else 0, 0)
|
||||
- `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]`, `['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)
|
||||
@@ -424,14 +428,14 @@ S→C:
|
||||
board: [ Piece & { row, col, dir } ], // dir ∈ UP|RIGHT|DOWN|LEFT (§3; bench pieces carry none)
|
||||
deployCap, deployCount,
|
||||
bonds: [ { bondId, count, active, tier, layers, thresholds, countsHand } ], // sorted: active first, then layers desc
|
||||
effects: [ { id, name, desc, iconKind: 'band'|'choice'|'team'|'item'|'garrison', iconId, counter? } ],
|
||||
effects: [ { id, name, desc, iconKind: 'band'|'choice'|'team'|'item'|'garrison', iconId, counter?, counterText? /* a bounty: 还剩 N 场作战, §20.6 */ } ],
|
||||
nextEnemies: [ { enemyKey, count, tag } ], // preview of the upcoming round's wave (after waves.js generation)
|
||||
stats: { dmgDealt, kills, leaks, gold, refreshes, merges } }
|
||||
Piece = { uid, kind: 'chess'|'item'|'token', id, golden: bool, tier, items: [ { uid, id } ] /* chess only */, count /* token stacks */, ownerUid /* token */ }
|
||||
```
|
||||
`m.public.players[].status` ∈ `'acting' | 'ready' | 'deciding' | 'combat' | 'done' | 'helping' | 'left' | 'dead'`.
|
||||
`m.public.fields[]`: `{ fieldId, kind: 'normal'|'unite'|'boss'|'hidden', players: [playerId…], live: bool }` — `fieldId` is `'n:<playerId>'` for normal fields, `'u'` for the unite field, `'b1'`/`'b2'` for boss fields. Clients `g.watch` a fieldId; default = own field (or the unite/boss field they are in).
|
||||
`flags` bitmask: 1 blocked, 2 stunned, 4 frozen, 8 stealth, 16 skillActive, 32 shielded, 64 invulnerable, 128 cold, 256 sleep, 512 flying.
|
||||
`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.
|
||||
`anim` small int: 0 idle, 1 move, 2 attack, 3 skill, 4 die, 5 stun, 6 deploy.
|
||||
|
||||
---
|
||||
@@ -460,7 +464,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 icon right of the bars (§18.3); 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); 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).
|
||||
|
||||
---
|
||||
|
||||
@@ -468,9 +472,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 (two taps, §18.2); 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; phase banners; ticker; toasts; emote wheel; settings (volume, quality, show damage numbers)), **Result** (victory/defeat, rounds, per-player lineup, stats, titles, back to room).
|
||||
|
||||
Interactions (research 09 §1.2 / §5, which supersede the first draft): shop card, upgrade and 机变 card are two-tap (select → confirm, §18.2); 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; `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).
|
||||
Interactions (research 09 §1.2 / §5, which supersede the first draft): shop card, upgrade and 机变 card are two-tap (select → confirm, §18.2) — a tap anywhere on the card, its confirm strip included, is the card's tap (shop cards have no ⓘ corner; the armed bar sits above the detail card; a shop card that cannot be bought right now opens its detail on tap, §20.7); a press / drop goes by the tile under the pointer (§18.1); drag bench↔board — a legal board drop (also onto the piece's own tile) opens the 4-direction deploy wheel (swipe UP/RIGHT/DOWN/LEFT with the rotated range striped orange; release in the centre, ✕ 点击取消 or Esc cancels); tap a unit's tile ⇒ underframe with **出售 +N** (and **撤退** on the board; items/Arts in the hand / temp: **销毁** — equipped items are locked, §6.2) plus its rotated range; no drag-to-sell; drag an item onto a unit's tile ⇒ equip that unit (both slots used ⇒ the replace dialog `ui/equipReplace.js` shows the incoming and the two equipped items; the player picks the one to replace — it is destroyed — `g.equip replaceUid`; cancel changes nothing); Arts are dropped on a tile and get the direction step; right-click or long-press ⇒ detail (or a tap on a shop card while the bar is locked); `R` refresh, `F` freeze, `D` level-up, `Space` ready (in a solo battle: pause / resume, §14 Solo pause). All actions optimistic-free: UI waits for server state (latency is small).
|
||||
|
||||
Audio: autochess BGM per phase, UI SFX (buy/sell/refresh/level/merge/ready/timer), per-unit attack/hit SFX (throttled; an impact plays for a hostile attack's phys / arts / true damage only, never an operator's skill-mode file for a normal attack, ≤ 2 copies of one battle sound — §18.4), boss music in R14/15.
|
||||
|
||||
@@ -517,28 +521,28 @@ Audio: autochess BGM per phase, UI SFX (buy/sell/refresh/level/merge/ready/timer
|
||||
**Authority per field.**
|
||||
- Normal field: the owning human's client is **authoritative** (if connected).
|
||||
- Unite field (联防) and boss/hidden pair fields: the lowest-seat **connected human** in that field is authoritative; every other human in (or watching) that field receives the same spec and simulates it locally **for display only**.
|
||||
- Bots, departed/disconnected players, fields with no connected human: the **server** simulates. Normal and 联防 fields headlessly (`fields.js HeadlessJob`, with a `[gt, killed, total]` progress timeline) in wall-clock slices of `headlessSliceMs` (8 ms) per callback on a real host — never one blocking run per field (3 bot fields ≈ 0.2–0.5 s of CPU at COMBAT start here, seconds on a mini PC); at once in virtual time; the result is released at the battle's natural end on the field clock (so round pacing and the teammates' progress look live). Boss fields in real time without snapshots (`HeadlessPacer`); when no field of a boss round has a connected human the whole round runs server-side as before (FieldRunner with streaming off).
|
||||
- Bots, departed/disconnected players, fields with no connected human: the **server** simulates. Normal and 联防 fields headlessly (`fields.js HeadlessJob`, with a `[gt, killed, total(, left)]` progress timeline — `left`: the 联防 leakers' enemies still standing, §20.6) in wall-clock slices of `headlessSliceMs` (8 ms) per callback on a real host — never one blocking run per field (3 bot fields ≈ 0.2–0.5 s of CPU at COMBAT start here, seconds on a mini PC); at once in virtual time; the result is released at the battle's natural end on the field clock (so round pacing and the teammates' progress look live). Boss fields in real time without snapshots (`HeadlessPacer`); when no field of a boss round has a connected human the whole round runs server-side as before (FieldRunner with streaming off).
|
||||
- Takeover: an authority that disconnects or leaves, or misses its deadline (`timeLimit / speed` real s + 15 s grace, normal/联防) loses the field: normal/联防 → the server re-simulates the spec from t = 0 (deterministic: the same result); boss → the partner's replica is promoted (`b.start authoritative: true`), else the server runs it, fast-forwarded to the field clock through a `CreditPool` that only credits damage beyond what the client already reported (the fast-forward is spread over the pacing intervals, ≤ 240 extra ticks each). A boss authority silent for 12 s is handed over the same way. The former authority gets `b.end { reason: 'takeover' }` and keeps displaying.
|
||||
|
||||
**Protocol (additions; `shared/protocol.js` is normative).**
|
||||
- `battleId` = `<prefix>.<round>.<seq>.<fieldId>` (`Match._ccField`; `<prefix>` = the match seed in base 36 plus `-<matchNo>`, the room's match number the lobby passes as `opts.matchNo`; `<seq>` counts every battle of the match; the informational `.<fieldId>` is dropped when the id would exceed the protocol's 64 chars): unique for the whole match although field ids repeat (`n:<pid>` every round, `u`, `b1`/`b2` in both boss rounds), and across the matches of one room. The client runner keys its local battles by it (the same id again = the same battle, no rebuild) and the server matches reports by it, so a late `b.progress` / `b.result` of a finished battle — or one that crossed into the room's next match on the same socket — is ignored instead of being credited to another battle.
|
||||
- S→C `b.start { battleId, fieldId, kind, spec, authoritative, startAt, serverNow, elapsed, speed, watch, done }` — sent at COMBAT/UNITE/FINAL_ASSAULT/HIDDEN start to every human in the field (联防: to every human; COMBAT and boss rounds: eliminated humans get the first field as a display replica — research 09 "keep-watching auto-observes the first available field"), on `g.watch`, and on reconnect/resync. `elapsed` = game seconds on the field clock (a finished field: its end time); the client fast-forwards to it. `speed` = game seconds per real second (2; tests/tools may speed up).
|
||||
- C→S `b.progress { battleId, gt, killed, total, leaks?, bossDmg?, by?, done? }` — authoritative clients, ~1 Hz (boss fields 4 Hz). Normal/联防: `leaks` = counted leaks so far. Boss: `bossDmg` = cumulative damage of this field to the shared pool, `by` = the same per player, `leaks` = the field's cumulative LP cost (leaks × `lpr` + leader "扣除目标生命" effects; `spec.js attachLpMeter`). The server applies deltas (never twice). Stale reports (unknown battle, not the authority, after the match ended) are ignored, never answered with an error; the platform never answers a rid-less `b.progress` with an error frame (it would surface as a toast).
|
||||
- C→S `b.progress { battleId, gt, killed, total, leaks?, left?, bossDmg?, by?, done? }` — authoritative clients, ~1 Hz (boss fields 4 Hz). Normal/联防: `leaks` = counted leaks so far; 联防: `left` = per leaker its enemies still standing (`spec.js uniteLeft`, clamped by the server to `fields.js uniteBillBounds`; §20.6). Boss: `bossDmg` = cumulative damage of this field to the shared pool, `by` = the same per player, `leaks` = the field's cumulative LP cost (leaks × `lpr` + leader "扣除目标生命" effects; `spec.js attachLpMeter`). The server applies deltas (never twice). Stale reports (unknown battle, not the authority, after the match ended) are ignored, never answered with an error; the platform never answers a rid-less `b.progress` with an error frame (it would surface as a toast).
|
||||
- C→S `b.result { battleId, result }` — authoritative client at battle end; `result` = `compactResult(BattleResult)` (the fields the match consumes; ≤ 4 players, ≤ 400 leaks, ≤ 64 unitsEnd, ≤ 160 unitStats, ≤ 40 layer gains; `fitResult` keeps the frame ≤ 60 KB: unit statistics, then leak mods, then — boss only — leak entries go first). Structural checks in `isBattleResult`; `server/net.js` routes `b.*` frames to the match (lobby `routeGame`).
|
||||
- S→C `b.pool { hp, max, teamLp, acked: { [fieldId]: cumulative damage counted } }` (boss rounds, ≤ 4 Hz; exact numbers, never rounded) — clients reconcile their `LocalBossPool`: displayed pool = server hp − (local cumulative damage − acked). A boss field whose client result is `'cleared'` while < 1 HP is left on the server (float dust of summing both fields) empties the pool.
|
||||
- S→C `b.pool { hp, max, teamLp, acked: { [fieldId]: cumulative damage counted } }` (boss rounds, ≤ 4 Hz; exact numbers, never rounded) — clients reconcile their `LocalBossPool`: displayed pool = server hp − (local cumulative damage − acked). Pools never hold less than 1 HP (`BOSS_POOL_MIN_HP`: `SharedBossPool` and `LocalBossPool`; a browser pool reads any finite rest below 1 as 0 — §20.5); the 'cleared'-result dust rule (a boss field whose client result is `'cleared'` while < 1 HP is left on the server empties the pool) only catches noise at that boundary.
|
||||
- S→C `b.end { battleId, fieldId, reason }` — `'cleared'` (pool 0) / `'forced'` (team LP 0): the client calls `battle.forceEnd` and reports; `'takeover'`: stop reporting, keep displaying.
|
||||
- `m.public.fields[]` gains `progress: { killed, total, done }`; `m.public.combatMode`; `m.public.paused: boolean` (always present; solo pause below).
|
||||
- `b.snap` / `b.ev` are **not sent for combat** in client mode. `g.watch` rules: see Spectating.
|
||||
|
||||
**Result validation (server, `fields.js validateClientResult`).** A client result is accepted only if structurally valid and plausible for its spec: every spec player reported and nobody else; finite numbers; `killed ≤ total ≤ 4·spawns + 100`; every leaked `enemyKey` is in the spec's spawns (multiset bound) or a key the spawned enemies' data mentions (content summons/splits, bounded by the total) — boss fields excepted (their leaks cost team LP through `b.progress`); `counted: false` only for enemies that never count (data `notCountInTotal`, schedule `countInTotal: false`, boss/part entries, content spawns); 联防: leaks + `unspawned` per (enemy, leaker) ≤ what that leaker sent in (nobody's survivors are billed to a teammate); per-bond `layerGains ≤ 60 + 4·round`, 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); 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`: any other result — `'forced'` / `'timeout'` at t = 0, `'cleared'` while the pool still holds — 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 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`: any other result — `'forced'` / `'timeout'` at t = 0, `'cleared'` while the pool still holds — 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, 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; 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.
|
||||
|
||||
**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.
|
||||
@@ -606,7 +610,7 @@ v2.2 answered "a click selected the unit one row lower" with body picking: every
|
||||
|
||||
### 17.5 Game HUD (#2, #3, #8, #9) — `ui/hud.js`, `teamPanel.js`, `underframe.js`, `detailPanel.js`, `data.js`
|
||||
|
||||
- **Live LP (#2)**: in a normal round the top bar's LP tower and the own team row show `lp − min(lpCapPerRound, counted leaks)` as soon as the own battle's enemies enter the blue gate — the number in red followed by a −N plate that pops again on every leak, clamped at 0, with a tooltip (`ui/hud.js liveLp` / `pendingLoss` / `pendingTip`). The count is `ownLeaks(local, server)` = the larger of the local runner's `state().leaks[own field]` (§14) and `m.public players[].pendingLp` (§8.2; the authority's `b.progress` leaks, the field's result, the recorded result in 联防) — both only grow within a round, so a paused display replica never freezes the number. The pending part is dropped as soon as the settled `m.private` lands (its `lp` or `stats.leaks` changed; `Match.flush` sends it before the SETTLE `m.public`), the phase leaves COMBAT / 联防 or the round changes — never subtracted twice. 联防: the own battle's count stays on show as an upper bound tagged 联防中 (teammates may still stop part of it; the settled loss can be lower). Teammates' rows use their `pendingLp` (`teamPanel.js rowLp`); server-run fields (bots, taken-over fields) report none before their result is released. Boss rounds are unchanged (the team LP moves live through `b.pool`).
|
||||
- **Live LP (#2)**: in a normal round the top bar's LP tower and the own team row show `lp − min(lpCapPerRound, counted leaks)` as soon as the own battle's enemies enter the blue gate — the number in red followed by a −N plate that pops again on every leak, clamped at 0, with a tooltip (`ui/hud.js liveLp` / `pendingLoss` / `pendingTip`). The count is `ownLeaks(local, server)` = the larger of the local runner's `state().leaks[own field]` (§14) and `m.public players[].pendingLp` (§8.2; the authority's `b.progress` leaks, the field's result, the recorded result in 联防) — both only grow within a round, so a paused display replica never freezes the number. The pending part is dropped as soon as the settled `m.private` lands (its `lp` or `stats.leaks` changed; `Match.flush` sends it before the SETTLE `m.public`), the phase leaves COMBAT / 联防 or the round changes — never subtracted twice. 联防 (**revised by §20.6**, user playtest #6 item 7): a leaker's count is its enemies still standing — the local replica's while the 联防 field is on screen, else m.public `uniteLeft` — shown uncapped as ×N in the capsule and in the leakers' team rows, and the LP tower shows lp − min(10, N), tagged 联防中. Teammates' rows use their `pendingLp` (`teamPanel.js rowLp`); server-run fields (bots, taken-over fields) report none before their result is released. Boss rounds are unchanged (the team LP moves live through `b.pool`).
|
||||
- **Temp row (#3, UI)**: while `m.private temp` holds pieces during the own prep, a dashed red frame surrounds the 临时整备区 row on the board and a label says how many are waiting, what to do and when they go (`underframe.js TempRowNotice` / `tempRowFrame`, placed through `view.tileScreen` — it follows the camera, the mirrored Final Assault prep and the DOM fallback; no pointer events; the label hides while dragging or placing). Its wording follows the server rule (§6.2, `tempRowRule(ready)`): not ready — "放入整备区或战场、配发或使用后才能准备;休整期结束时仍在此处的将被销毁"; ready in PREP (so whatever lies there arrived after Ready) — "已准备就绪后进入的单位保留到下个休整期,届时仍在此处的将被销毁(取消准备则在本休整期结束时销毁)". The 准备就绪 button shows "临时整备区 N 个单位待处理" under it (not only on hover; the effects column moves down a line meanwhile, `.gm.has-readywhy`) and Space toasts the reason (`hud.js TEMP_RULE` / `tempReadyReason`).
|
||||
- **Detail card order (#8)** (`detailPanel.js CHESS_SECTIONS`): header (portrait, name, tier, class, bonds) → **特质** (the operator's own effect — garrison trigger chip such as 休整期结束时 + description, a compact gold block, visible without scrolling at 1920×1080, 1280×720, iPhone 14 and 640×360) → 特性 (class trait) → stats (a compact 4×2 grid) + attack range → 技能 → 模组 → 装备 → 天赋. On phones ≤ 460 px tall the portrait is shorter and the romanised name is hidden.
|
||||
- **Busy indicators and texts (#9)**: every text of the game (operators, skills, bonds, items, enemies, 特质 …) is static data, `/data/*.json` loaded once per page and never fetched during a match; the "spinners" the player saw were busy indicators of requests in flight next to texts — and the 特质 chip's type icon: it was picked by the trigger, so every <休整期开始时 / 结束时> 特质 showed the spoked 特异化 glyph (it reads like a loading spinner) before its text; the chip now shows the garrison's own official `eventTypeIcon` (`detailPanel.js garrisonTypeIconKey`: crossed swords 作战能力, gold 整备能力, chain link 持续 / 单次叠加, spokes only for 特异化). Now: no `cursor: wait / progress` anywhere; a loading `Button` keeps its icon and shows a thin sweeping bar along its bottom edge after 250 ms (no spinner ring); a 机变 card in flight shows a 选择中 strip until the pick appears in `m.public` (`choiceOverlay.js pickBusy`); the 作战结束,等待队友完成作战 hourglass is still; the hexagon `Spinner` is kept for genuine loading screens only (entering the match, the field view, connecting, guide images, the first 干员调配 load, which says the data loads once per page). `main.js warmGameData` downloads every in-match file (`gameComponents GAME_FILES`) in the background once the player is in a room, and the match screen waits for them; `data.js` retries a transient failure (network error, HTTP 5xx / 408 / 429) after 600 ms and 2 s (`RETRY_DELAYS_MS`) — a 404 or invalid JSON is final.
|
||||
@@ -630,7 +634,7 @@ Thirteen reports after playing v2.2. Where each is handled: #1 picking should si
|
||||
|
||||
### 18.2 Confirms and timers (#2, #3, #4) — `ui/choiceOverlay.js`, `server/match/Match.js`, `screens/bandDraft.js`
|
||||
|
||||
- **机变 cards take two taps** (like the shop's cards): the stateful `ChoiceOverlay` wraps the pure `ChoiceView`. A first tap on a pickable card (bounty = extra enemies, 道具补给 / 机密商店 items, tactics) only selects it — gold frame, a 确认选择 · 再次点击 strip, the other cards dimmed, 确认选择 in the header; a second tap on it, or 确认选择, sends `g.choice`. Another card moves the selection; Esc or a press elsewhere drops it; so do the card being taken, the turn moving on and a pick in flight. Cards stay buttons (Tab / Enter). The other picks were already two-step (晋升奖励 in the shop bar, the strategy draft's 确认选择, 确认替换).
|
||||
- **机变 cards take two taps** (like the shop's cards): the stateful `ChoiceOverlay` wraps the pure `ChoiceView`. A first tap on a pickable card (bounty = extra enemies, 道具补给 / 机密商店 items, tactics) only selects it — gold frame, a 确认选择 · 再次点击 strip, the other cards dimmed, 确认选择 in the header; a second tap on it, or 确认选择, sends `g.choice`. Another card moves the selection; Esc or a press elsewhere drops it; so do the card being taken, the turn moving on and a pick in flight. Cards stay buttons (Tab / Enter). Each card shows its full effect text (§20.7). The other picks were already two-step (晋升奖励 in the shop bar, the strategy draft's 确认选择, 确认替换).
|
||||
- **A single human is untimed** (`Match.loneHuman`: exactly one human seat at the start → `Match.soloUntimed`): 独立模拟 and a 同盟 room started alone or with AI teammates only publish no INFO_CHECK / BAND_DRAFT / SP_DRAFT / PREP deadline, and BATTLE_CHECK / ROUND_START / SETTLE run silently — the timers only ever made humans wait on each other. The co-op rules stay (shuffled draft order and skip, 6 机变 cards, 联防); pause stays solo-only; a disconnected lone human's match waits (the co-op reconnect window then ends it as abandoned). `Countdown` (`ui/components.js`) renders nothing without a deadline (it used to draw a `--` COUNTDOWN placeholder that only a CSS rule hid).
|
||||
- **The strategy draft has ONE countdown**: every turn lasts `Match.BAND_TURN_SECONDS` = 30 s [ASSUMED] (× timerScale; `data/config.json timers.bandTurn` says the same — the official data only has the whole BAND_CHECK step, 50 s, now informational) and `m.public.deadline` IS the current turn's end (= `draft.turnDeadline`; `draft.turnSeconds` = the turn length, 0 untimed), so the step header and the picker's row show the same number. No step cap: the turns bound the step (≤ (seats + skips) × 30 s). AI seats pick at once. The draft screen reports its highlighted strategy (`g.bandFocus`); a turn that runs out takes it while it is allowed and free (`Match.timeoutBand`), else 华法琳 (`band_bldsk`), else the first free strategy (`defaultBand`, 队友已选 never). The selection starts on that default and the tip always names what a timeout gives ("每位博士有 30 秒,超时将自动选择当前选中的「X」"; untimed: "本局不限时"). `server/match/audit.js` checks it (deadline = the turn's, 30 s; nothing timed for a single human).
|
||||
- Tests: `test/ui/playtest4.test.js`, `test/ui/bandDraft.test.js`, `test/match/draft.test.js`, `test/ui/gameLogic.test.js` (`phaseTotalSeconds` = the turn), `test/match/hardening.test.js` (the audit), `test/ui/mock.e2e.test.js` (two taps). Browser tests that need a timed co-op briefing now seat two humans (`test/ui/loadout.e2e.test.js`); a single human's 机变 is picked by the test itself (`test/ui/playtest2.real.e2e.test.js`), and the real-server drafts wait for BAND_DRAFT before picking (a client left in the briefing let its 30 s turns run out).
|
||||
@@ -639,7 +643,7 @@ Thirteen reports after playing v2.2. Where each is handled: #1 picking should si
|
||||
|
||||
- **Knocked out, not gone**: `Battle.snapshot()` lists `down: [[id, respawnAt, respawnTime, state]]` (§8.2) for operators knocked out (reason `'killed'`, not withdrawn — or `FORCED_EXIT`, entering 联防 knocked out, §19.3), deployed at least once and with a finite respawn timer; `state` = sim/constants.js `DOWN_STATE` (0 counting, 1 timer done but DP < cost, 2 timer done but the home tile is taken — the official auto-redeploy rule, research 01 §4.3). `Battle.isDown(u)`; `fieldMeta()` lists them too, so a client joining mid-battle (reconnect, observer, replica) shows them. The renderer (`interp.js downAt`, `app.js syncBattle`, `units.js setDown`) keeps such an operator's view on its tile: its Spine Die clip plays once and holds its last frame (a collapsed / kneeling pose), lightly greyed (tint 0xb4b4b4, alpha 0.92), under a **redeploy ring** above the head — a dark disc with a mint arc filling as the timer runs and the seconds left (game s); timer done: a full pulsing amber ring "DP" (waiting for DP) or red "!" (its tile taken). The `deploy` event plays the Start clip and restores the normal look; a view created for an operator already down starts on the held pose; leaving the list without a redeploy fades it out. Summons, devices and enemies vanish as before. [ASSUMED] look: the official knocked-down / ring art is in no extracted autochess bundle.
|
||||
- **Element gauges, official rules** (PRTS 元素 / 术语释义; `damage.js`): gain × `elemTakenMul` × (1 − 损伤抵抗 / 100), 损伤抵抗 = data `epResistance` (EP_RESISTANCE; it used to read `epDamageResistance` as a fraction). A burst's **爆发冷却** (= its duration: 灼燃 / 神经 / 侵蚀 10 s — enemies' 侵蚀 8 s — 凋亡 15 s; the operators' 侵蚀 had none) locks EVERY element of the unit — nothing fills or recovers (`reduceElement`) — and its end resets EVERY gauge (`burstLocked(unit)` takes no element). 神经 applies the stun (operators) / 3 麻痹 (enemies) before its damage, 侵蚀 its DEF cut before its damage; the operators' 凋亡 burst holds 阻回 (`noSp`: `skills.js gainSp` refuses SP of every kind — time, attack, hurt, granted) + 静默, −1 SP/s and 100 arts/s.
|
||||
- **The gauge on screen** (official display: one icon, the "当前损伤元素"): `damage.js elementView(u, now)` → b.snap `elem` (§8.2): the fullest gauge, ties by the official element id (`constants.js ELEMENT_ORDER`: 神经 < 侵蚀 < 灼燃 < 凋亡); during a 爆发冷却 the bursting element, refilling over the cooldown. `interp.js` appends it to the unit's tuple; `units.js` draws it right of the HP / SP bars — operators: the element's disc with its glyph; enemies: a smaller plain disc of the element colour (official rule) — with a white ring for the remaining 元素值 (1 − fill). One procedural 512×576 atlas (`textures.js hudRings` / `ringArc`, 49 arc frames and the discs), sprites made on first use and hidden when idle. The burst's lock status (`burnBurst`, `neuralBurst` …) is not repeated in the status row while the element icon shows it (only a feed without `elem`, e.g. an older recording, shows it there). [ASSUMED] placement (the official draws it under the model, the remake's bars are above the head) and size (0.3 tile, enemies ×0.8).
|
||||
- **The gauge on screen** (official display: one icon, the "当前损伤元素"): `damage.js elementView(u, now)` → b.snap `elem` (§8.2): the fullest gauge, ties by the official element id (`constants.js ELEMENT_ORDER`: 神经 < 侵蚀 < 灼燃 < 凋亡); during a 爆发冷却 the bursting element, refilling over the cooldown. `interp.js` appends it to the unit's tuple; `units.js` draws it — **superseded by §20.8**: a row under the unit's own HP / SP bars, the element disc (0.15 tile, 8–15 px; operators with the glyph, enemies ×0.8 plain colour) and a white bar of the remaining 元素值 (1 − fill), refilling in the element's colour with a pulsing disc during the 爆发冷却 (v2.3 drew a white ring right of the bars; the procedural atlas `textures.js hudRings` keeps the discs and the redeploy ring), sprites made on first use and hidden when idle. The burst's lock status (`burnBurst`, `neuralBurst` …) is not repeated in the status row while the element icon shows it (only a feed without `elem`, e.g. an older recording, shows it there). [ASSUMED] placement (the official draws it under the model, the remake's bars are above the head) and look (§20.8).
|
||||
- Recordings (`tools/record-battle.mjs`) carry both keys, so the render demo shows them. Tests: `test/sim/downelem.test.js`, `test/sim/combat.test.js`, `test/render/downring.test.js`, `test/render/interp.test.js`, `test/render/downelem.browser.test.js`.
|
||||
|
||||
### 18.4 Battle sounds (#6) — `public/js/audio.js`, `tools/assets/audio.mjs`
|
||||
@@ -658,9 +662,9 @@ Thirteen reports after playing v2.2. Where each is handled: #1 picking should si
|
||||
### 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. Unchanged (already right): 妖怪 / 妖怪MKII / 威龙 / 寒霜 / 法术大师A1·A2 / 御4 / 护障 / 护障·P / 远眺.
|
||||
- **#11 skill summons are not prep pieces**: tokens.json `placeable` = `tokenDisplayType` DEFAULT **and** made by a talent (海嗣, 狼群, 流形). 赫默's 医疗探机 and 巫恋's 诅咒娃娃 appear only when the skill fires (their kits place them); `PlayerState.grantTokensFor` just grants the placeable ones.
|
||||
- **#12 炎佑** (PRTS “炎佑” 级别0(卫戍协议), `content/tokens.js yanyouKit`): 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.
|
||||
- **#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).
|
||||
- **#11 skill summons are not prep pieces** — **superseded by §20.1**: v2.3 limited tokens.json `placeable` to `tokenDisplayType` DEFAULT summons made by a talent (海嗣, 狼群, 流形), so 赫默's 医疗探机 and 巫恋's 诅咒娃娃 appeared only when the skill fired. Since user playtest #6 every manually deployable summon is a hand piece again, and its placed piece deploys once at the battle start (PRTS), then with each cast.
|
||||
- **#12 炎佑** (PRTS “炎佑” 级别0(卫戍协议), `content/tokens.js yanyouKit`; 孤立 — never healed or selected by allies, §20.3): it flies after the highest-aggro targetable enemy of the whole field (operator order) and hovers 0.25 tile [ASSUMED] from it (a second 炎佑 of 9 炎 0.8 row beside the first); **with no enemy on the field it stays where it is** (user playtest #4; PRTS says it then tracks a random tile). 祛恶之焰 (模式乙): fires like a normal attack when its 15 s cooldown is full, locks that attack's first target and channels up to 20 s — each second (the first at once) 60 % ATK arts to every enemy within 1.0 of the target, no normal attacks meanwhile — ending when the target is gone or 炎佑 is silenced (inferred); the cooldown then runs again [ASSUMED]. Every damage it deals adds burn = 20 % ATK (flame ticks included, official 下半 notice); 元素脆弱 aura of radius 1.5 around itself; it ignores element damage; ATK / HP = the template 600 / 12000 **plus** 30 % of the 炎 operators' sums (PRTS 最终加算; the 9-炎 ×1.5 on the whole ATK [ASSUMED]). The flame is drawn as one continuous jet (`render/fx.js _flame`): the sim emits fx `yanyouFlame {x, y, id: dragon, target, r, n, dur: 1}` once per game second, and each tick re-aims and extends the dragon's jet — a stream of fire from the dragon onto its locked target with a burning disc of radius `r` following the target — which burns `dur` + 0.2 s and goes out when no tick comes or the dragon is gone.
|
||||
- Tests: `test/content/playtest4_summons.test.js`, `enemies_bosses.test.js`, `tokens_devices.test.js`, `bonds_core.test.js`, `choices.test.js`, `test/data.test.js`, `test/match/pool.test.js`, `test/render/fxflame.test.js`, `test/ui/playtest4.test.js` (item card).
|
||||
|
||||
### 18.7 The occasional giant UI flash (#13) — `render/fx.js pop`
|
||||
@@ -671,16 +675,16 @@ The first in-battle layer gain of each bond (b.ev `'layer'`) pops that bond's ic
|
||||
|
||||
## 19. User playtest #5 (v2.4): hovering enemies, target order, blocking, 联防, elements, huge bosses, maps, phone HUD
|
||||
|
||||
Ten reports after playing v2.3 (绝境 on desktop, 标准 on an Android phone in landscape). Where each is handled: #1 operators that only attack ground units hit 近地悬浮 enemies → §19.1; #2 an operator lying knocked out (濒死) vanished when 联防 began instead of counting down and redeploying → §19.3; #3 element damage against the 海嗣 felt weak, audit every operator's and enemy's element amounts and gauges → §19.5 (enemies, the gauge pipeline) and §19.6 (the player side); #4 an enemy held between two operators walked off when the rear one died instead of the front one taking it over → §19.2; #5 the enemies' target order differed from the official → §19.1; #6 act2 m01's normal field had its two wind lanes but no blower machines, which showed in the boss round instead → §19.7; #7 the tiles framed by the 1×3 pipes on the boss map are deployable officially, check every map → §19.7; #8 the bottom-left settings icon was deformed → §19.8; #9 on a long phone screen the shop bar covered the bench → §19.8; #10 the huge bosses could only be hit around one tile → §19.4. A follow-up message added #11 至简 should cost 1 (his 特质 says 购买价格为1) → §19.9. Normative lines rewritten with it: §3 range test, §5.1 `carryState`, §5.5 (initial deployment, DP / down, blocking, operator attacks, enemy AI, damage), §6.1 UNITE, §7 bosses; §18.3's `isDown` now includes `FORCED_EXIT`, and §18.1's notched-phone figures are superseded by §19.8. The integration also applied the workstreams' cross-checks (hasHp in the pipeline, 脆弱 vs 元素伤害, element healing, 淤困, 塑心 S2, splash as a 中点判定); they are listed in the sections they belong to. Three QA passes over the integrated branch then left residuals, fixed in their sections: 掠海漂移体's model crawls after its drop (§19.1); blocked-first and out-of-range selection are melee-only (§19.2); a forced-out operator's timer takes the battle-start redeploy effects (§19.3); the gauge intake's 5 % floor (§19.5); the fallback board draws the boss half in the boss prep (§19.7). The user's follow-up then settled two [ASSUMED] choices: the boss field's row-6 blowers do stand under the bench in the normal views (§19.7), and a forced-out operator's redeploy timer does restart in full (§19.3).
|
||||
Ten reports after playing v2.3 (绝境 on desktop, 标准 on an Android phone in landscape). Where each is handled: #1 operators that only attack ground units hit 近地悬浮 enemies → §19.1; #2 an operator lying knocked out (濒死) vanished when 联防 began instead of counting down and redeploying → §19.3; #3 element damage against the 海嗣 felt weak, audit every operator's and enemy's element amounts and gauges → §19.5 (enemies, the gauge pipeline) and §19.6 (the player side); #4 an enemy held between two operators walked off when the rear one died instead of the front one taking it over → §19.2; #5 the enemies' target order differed from the official → §19.1; #6 act2 m01's normal field had its two wind lanes but no blower machines, which showed in the boss round instead → §19.7; #7 the tiles framed by the 1×3 pipes on the boss map are deployable officially, check every map → §19.7; #8 the bottom-left settings icon was deformed → §19.8; #9 on a long phone screen the shop bar covered the bench → §19.8; #10 the huge bosses could only be hit around one tile → §19.4. A follow-up message added #11 至简 should cost 1 (his 特质 says 购买价格为1) → §19.9. Normative lines rewritten with it: §3 range test, §5.1 `carryState`, §5.5 (initial deployment, DP / down, blocking, operator attacks, enemy AI, damage), §6.1 UNITE, §7 bosses; §18.3's `isDown` now includes `FORCED_EXIT`, and §18.1's notched-phone figures are superseded by §19.8. The integration also applied the workstreams' cross-checks (hasHp in the pipeline, 脆弱 vs 元素伤害, element healing, 淤困, 塑心 S2, splash as a 中点判定); they are listed in the sections they belong to. Three QA passes over the integrated branch then left residuals, fixed in their sections: 掠海漂移体's model crawls after its drop (§19.1); blocked-first and out-of-range selection are melee-only (§19.2 — superseded by §20.3); a forced-out operator's timer takes the battle-start redeploy effects (§19.3); the gauge intake's 5 % floor (§19.5); the fallback board draws the boss half in the boss prep (§19.7). The user's follow-up then settled two [ASSUMED] choices: the boss field's row-6 blowers do stand under the bench in the normal views (§19.7), and a forced-out operator's redeploy timer does restart in full (§19.3).
|
||||
|
||||
### 19.1 Hovering enemies and the enemies' target order (#1, #5) — `sim/units.js`, `targeting.js`, `ai.js`, `Battle.js`, `content/enemies.js`, `bosses.js`, kits, `match/bot.js`, `render/units.js FORMS`
|
||||
|
||||
- **#1 cause**: `float()` only cancelled hits from non-`'ranged'` profiles and otherwise left the floater a ground unit, so 迷迭香 (攻击对小范围的地面敌人) shot and splashed it (5.2k damage in a sim run) and terrain, traps and 浮空 treated it as ground. **Official** (ba.float "无法被阻挡或近战攻击"; PRTS 术语释义 / 行动方式): the unit is unblocked and counts as a real air unit (算作空中单位) that keeps ground pathing.
|
||||
- **Now** `Unit.isFlying` is the one air-unit test: `motion` FLY, or an enemy flag `float` (近地悬浮) or `levitate` (浮空); pathing, displacement tiles and 诱导 read `motion`. canHitFly / groundOnly, splash, chain, the kits' "地面敌人" filters, ground terrain and traps skip air units; 失衡免疫 is the flag `noDisplace`; 浮空 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; 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 掠海漂移体.
|
||||
- **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 掠海漂移体.
|
||||
- **#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. [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).
|
||||
- **#5 changes**: `Battle.start` deploys operators row-major, then summons; `aggroSeq` ranks every start-of-battle summon after all operators (`deploySeq` stays the deployment identity). An enemy's own rule `e.profile.canTarget` filters its candidates before the sort (萨卡兹枯朽战车 no longer idles when its preferred target stands on high ground; it, 掠海漂移体 and “萨科塔之眼” ignore flying allies). Special priorities break ties by taunt, then latest deployed (`targeting.js aggroCmp`): 铳 / 昆图斯 highest DEF, 胄 highest / lowest ATK, the 碎铳之簧 bounce, 唱沙 / 沙狱 max HP, “自在” nearest. Kept (PRTS 作战机制 §隐匿 "我方干员并不会因为阻挡而解除隐匿"; 索敌的概念): a stealthed ally (隐匿 / 迷彩 / 排气格栅) is attacked only by the enemy it blocks (迷彩 is its own flag `camou`, not 隐匿 for 隐匿 conditions, §20.2). [ASSUMED] "从上到下>从左到右" = row-major; on a shared field (联防, boss) the players' i-th operators deploy together, then the summons (it used to be player after player, which made the leader prefer the later player's pieces).
|
||||
- Tests: `test/sim/playtest5-targeting.test.js` (18; 13 fail on 731a01c), `test/content/enemies_bosses.test.js` (吉兆飞鳞), `test/match/bot.test.js` (a hovering wave counts as air), `test/render/forms.test.js` (the crawl clips); browser (opt-in) `test/render/forms.browser.test.js` (the real sim feeding the field view).
|
||||
|
||||
### 19.2 Blocking by contact radius, and the hand-over (#4) — `sim/constants.js BLOCK_RADIUS`, `Battle._checkBlock` / `blockedTargets`, `ai.js`
|
||||
@@ -688,9 +692,9 @@ Ten reports after playing v2.3 (绝境 on desktop, 标准 on an Android phone in
|
||||
- **Cause**: an operator blocked only an enemy standing on its own tile (§5.5's old "|dx|,|dy| ≤ 0.5"). An enemy that passed a full front operator was blocked by the rear one, on the rear tile; when the rear operator died, the front one — which the enemy still overlapped — never took it.
|
||||
- **Official** (PRTS 游戏数据基础 §阻挡半径; 作战机制 "中点判定…案例:阻挡"): contact = the enemy's position within the blocker's radius of its centre, compared as squared distances — ground 0.70709997 (² 0.49999037), air (起飞 / blockFly vs flyers) 0.8944, devices (阻隔工事 / 障碍物) 0.4472.
|
||||
- **Now** (§5.5 Blocking): every tick, moving or not, an unblocked blockable enemy is blocked by the nearest operator in contact that has free capacity for its weight — the front operator takes over when the blocker dies, is withdrawn or is stunned (`stun` releases its enemies); an operator whose capacity frees up grabs an overlapping enemy; an enemy that finds no room walks on. A head-on enemy stops at contact, ≈ 0.71 tile from the blocker's centre, outside its tile (the small official deceleration is not modelled). A knocked-out or undeployed operator never blocks.
|
||||
- **Consequences, both official**: "可以选择且优先选择阻挡单位" (PRTS 选择器: the selector of units whose range is smaller than their block radius) — a **melee** blocker (data `position` MELEE: 要塞 / 领主 / 哨戒铁卫 included) may always target the enemies it blocks, in range or not, and targets them first (`Battle.blockedTargets` → `ai.js acquireTargets`, `targeting.js sortEnemyTargets`, the DEFAULT skill trigger — PRTS 卫戍协议/帮助 "敌人被近战干员自身阻挡"); 瑕光 S2 also sleeps the enemy she blocks (PRTS 备注). A rear operator whose range ends at the blocker's tile no longer reaches the enemy held in front of it (a regression sweep of 143 blocker × skill pairs: own-tile defenders 0.96–1.05×).
|
||||
- **Ranged operators on melee tiles** (卫戍协议 "所有行动内远程干员可部署在近战位") block, but target by their range alone (PRTS 索敌的概念 "我方索敌优先级:阻挡(近战限定)"; "远程位干员通常而言不会优先攻击自己阻挡的目标;甚至如果这个目标被阻挡在该干员的身后…无法攻击到这个敌人"): no blocked-first priority, no out-of-range target, no skill cast for the enemy they hold (`targeting.js meleeUnit`). Found by the integration QA: 能天使 / 夕 facing away kept shooting the enemy held behind them (9264 / 5860 damage in 20 s).
|
||||
- 灰毫 S2 专注轰击 casting (block 0, ranged only) as soon as she blocks is the basic strategy: the skill has no skill range and "敌人被近战干员自身阻挡" satisfies the attack-target condition; the 下半 重装 special strategy "受到伤害时释放技能" covers skill 1 only (BWIKI "重装职业干员 技能1,受到伤害后自动释放"; the data row has `skillIndex 0`, with `-1` as the all-skills sentinel — research 03 C1; PRTS's strategy table does not name the skill), so her S1 is TAKE_DAMAGE and S2 stays DEFAULT.
|
||||
- **Consequences, both official**: "可以选择且优先选择阻挡单位" (PRTS 选择器: the selector of units whose range is smaller than their block radius) — a **melee** blocker (data `position` MELEE: 要塞 / 领主 / 哨戒铁卫 included; every blocker since §20.3) may always target the enemies it blocks, in range or not, and targets them first (`Battle.blockedTargets` → `ai.js acquireTargets`, `targeting.js sortEnemyTargets`, the DEFAULT skill trigger — PRTS 卫戍协议/帮助 "敌人被近战干员自身阻挡"); 瑕光 S2 also sleeps the enemy she blocks (PRTS 备注). A rear operator whose range ends at the blocker's tile no longer reaches the enemy held in front of it (a regression sweep of 143 blocker × skill pairs: own-tile defenders 0.96–1.05×).
|
||||
- **Ranged operators on melee tiles** (卫戍协议 "所有行动内远程干员可部署在近战位") — **superseded by §20.3**: the integration QA had made them target by their range alone (PRTS 索敌的概念 "我方索敌优先级:阻挡(近战限定)"; "远程位干员通常而言不会优先攻击自己阻挡的目标;甚至如果这个目标被阻挡在该干员的身后…无法攻击到这个敌人"): no blocked-first priority, no out-of-range target, no skill cast for the enemy they held. The user's rule after playtest #6 — "阻挡了就一定要能打到" — makes every blocker hit what it blocks.
|
||||
- 灰毫 S2 专注轰击 — **superseded by §20.2**: v2.4 read the 下半 重装 special strategy "受到伤害时释放技能" as skill 1 only (BWIKI "重装职业干员 技能1,受到伤害后自动释放"; the data row has `skillIndex 0`, with `-1` as the all-skills sentinel — research 03 C1) and left S2 DEFAULT. The class rows now cover every MANUAL skill of the class, so 灰毫 S2 is TAKE_DAMAGE (the counter-evidence stays recorded in §20.2).
|
||||
- [ASSUMED] the nearest blocker wins, ties by scan order.
|
||||
- Tests: `test/sim/playtest5_blocking.test.js` (16, incl. the user's case on act2 m01 with real operators, the ranged blocker and 银灰 as a melee-position ranged attacker), adjusted geometry in `kits_alt_t5` / `engine-requests` / `enemies_bosses`.
|
||||
|
||||
@@ -733,7 +737,7 @@ Ten reports after playing v2.3 (绝境 on desktop, 标准 on an Android phone in
|
||||
- **Official rules** (PRTS 元素 / 游戏数据基础 / 伤害分类; gamedata_const termDescriptionDict): gauge gain = 损伤值 × max(5 %, 1 − 损伤抵抗 %) (PRTS 游戏数据基础: DMG_e "目标受到元素损伤时也可以使用该公式计算,只需要将 D 值改为目标的损伤抵抗即可"), then the 元素损伤倍率 effects; 元素伤害 = max(A(1 − 元素抗性 %), 5 %A); 元素脆弱 ("受到的元素伤害提升") raises 元素伤害 only; 脆弱 ("受到的物理、法术、真实伤害提升") never touches 元素伤害; bursts are 无来源 ("元素爆发通常造成无来源的伤害"; 无来源 = "无法被追溯伤害来源", yet the kill is traced).
|
||||
- **Fixed (W3b)**: 元素脆弱 is the mod `elementalTakenMul` (元素伤害 only; it used to share the gauge rate `elemTakenMul`, so 炎佑's 20 % sped up fills instead of raising bursts); 元素抗性 reduces `'elemental'` with the 5 % floor (`expectedFinal` mirrors it); bursts carry DamageInfo `sourceless` — no damage-dealt multiplier or penetration of the filler, and the `hit` / `damaged` / `fatal` hooks see `source: null` plus `credit` (stats, kill and boss-pool share keep the filler; a real 深巡 bursting a 海嗣 dealt 10500 instead of 7000, 精准狙击镜 made a 侵蚀 burst 6500 instead of 5000); target-side reactions with no source condition read `source || credit` (sharing: 圣杯, 盲信之誓, 余音; 抵挡: 星熊 战术装甲, 拉普兰德 日晷). The enemies' 凋亡 虚弱 is 50 % × remaining ÷ 15 s each tick (it ran 1 s ahead); 麻痹免疫 is honoured (假想敌:铳, the 胄 parts); 淤困's host burst spreads 1000 (was 780); 灼藤's first attack covers the 3×3; 节日爵士乐手 channels on one locked target (20 % arts + 10 % burn every 0.5 s, 21 ticks, ends on 沉默, no re-cast mid-channel). Side effect: 隐德来希's 心烛 cancels bursts on a candle ("只受隐德来希攻击的影响").
|
||||
- **Integration pass**: `damage.js hasHp` (alive, HP > 0; a boss: its pool) is the one predicate — `applyElement` refuses a target with no HP left, so no rider in a killing blow's `damaged` hook can burst a corpse (the content copies in tier5 / tier6 / items / tokens now import it); `'elemental'` damage skips `dmgTakenMul` (脆弱 and the other "受到的伤害±" effects [ASSUMED: they share that multiplier]) — only 元素脆弱 scales it; element healing (`reduceElement`) lowers **every** element type by the amount on its own (PRTS 菲莱 备注: 清除元素损伤 = "一次等同于自身最大元素值的全类型元素损伤治疗"; it healed the amount across all types in total); 淤困's "受到的元素损伤提高至130%" is an `elementHit` multiplier at priority 20 like every operator-side "受到的元素损伤±", before a 损伤屏障 (it was the `elemTakenMul` mod, applied after the barrier).
|
||||
- **Verified unchanged**: the burst effects on both sides; cooldowns 10 / 10 / 15 s (enemies' 侵蚀 8 s), locking then resetting every gauge; leaders take no 侵蚀 in the Final Assault (PRTS 盟约记录); 炎佑's aura = 20 % 元素脆弱 within 1.5. **Differs, not changed**: the gauge's look (official: icon + white bar under the model; §18.3 [ASSUMED]). [ASSUMED] 爵士乐手's first tick one interval after the cast. The gauge intake's 5 % floor (`elementIntake`, integration QA) is unreachable with the data (损伤抵抗 ≤ 10).
|
||||
- **Verified unchanged**: the burst effects on both sides; cooldowns 10 / 10 / 15 s (enemies' 侵蚀 8 s), locking then resetting every gauge; leaders take no 侵蚀 in the Final Assault (PRTS 盟约记录); 炎佑's aura = 20 % 元素脆弱 within 1.5. The gauge's look is the official icon + white bar since §20.8 (it differed in v2.4). [ASSUMED] 爵士乐手's first tick one interval after the cast. The gauge intake's 5 % floor (`elementIntake`, integration QA) is unreachable with the data (损伤抵抗 ≤ 10).
|
||||
- Tests: `test/sim/playtest5_elements.test.js` (18), `test/content/playtest5_followups.test.js` (pipeline guard, 脆弱, element healing), `combat.test.js`, `test/content/{kits_t5,tokens_devices,playtest4_summons,enemies_bosses}`.
|
||||
|
||||
| 海嗣 (user's case) | Official ratio | Per hit / s at base ATK | Hits to a 1000 gauge | Time |
|
||||
@@ -801,3 +805,130 @@ Ten reports after playing v2.3 (绝境 on desktop, 标准 on an Android phone in
|
||||
- **Official**: two chess carry this 特质 (`effectType` SERVER_CHESS_PRICE) and both texts say 购买价格为1 — 至简 (Ⅲ, tier price 3, `price` 2) and 红豆 (Ⅰ, tier price 2, `price` 1, hidden in 下半). Only "`bb.price` is the discount off the tier price" gives the text's 1 for both (3 − 2, 2 − 1).
|
||||
- **Change**: the handler lowers the price by `bb.price` (`ctx.modifyPrice(-p)`), and `EffectDispatcher.dispatch` runs the priced chess's own 特质 first on `onPrice` (before globals / band / bonds), so 远见's discount (−1, never below 1) and strategy caps act on the lowered price: 至简 stays at 1 with 远见 150 instead of dropping to 0. The shop card keeps `basePrice` 3, so the price shows in the discount tone. docs/META.md §2.2 / §2.3 say the same.
|
||||
- Tests: `test/content/garrisons_meta.test.js` (every SERVER_CHESS_PRICE owner: tier price − `bb.price` = the text's price = `priceOf`; buying 至简 costs 1; a global modifier already sees 1; 远见 150 leaves 至简 at 1).
|
||||
|
||||
---
|
||||
|
||||
## 20. User playtest #6 (v2.5): summons, skill triggers, blocking, push force, enemies, boss pool, bounties, 机变 cards, element gauge
|
||||
|
||||
Nineteen reports after playing v2.4 (#1–#3 from a friend of the user). Where each is handled: #1 凯瑟琳's device could not be placed by hand → §20.1; #2 赫默's drone could not be placed by hand → §20.1; #3 忍冬's 叙拉古 stealth never ended → §20.2; #4 the bounty enemies kept coming instead of stopping after two battles → §20.6; #5 the Final Assault leader at 0 HP kept fighting until the team LP ran out, and the result still said 胜利 → §20.5; #6 the 道具补给 cards showed no description → §20.7; #7 in 联防 the leaker's count should fall live as teammates kill (it seemed capped at 10) → §20.6; #8 新约能天使 S2's shield on teammates seemed not to work → §20.2; #9 威龙 was drawn far too large → §20.4; #10 on the phone the shop's confirm strip did not buy (only the middle of the card worked) → §20.7; #11 the element gauge seemed to fill at the wrong rate → §20.8; #12 萨卡兹枯朽战车 hit too softly → §20.4; #13 “萨科塔之翼”'s 战栗 (small random flight) was missing → §20.4; #14 enemy weight vs push force (野鬃 pushed too many enemies) → §20.3; #15 skills whose range is wider than the attack range did not fire when an enemy entered it → §20.2; #16 古米's ally heal fired only after she engaged an enemy → §20.2; #17 the front operator that took over an enemy held between two operators never attacked it → §20.3; #18 炎佑 was healed by medics → §20.3; #19 the 3-choose-1 promotion reward often offered two identical operators → §20.6.
|
||||
|
||||
Eight workstreams were merged into `playtest6` (WH → WG → WD → WC → WB → WA → WF → WE); the reviews' last rounds and the cross-workstream notes were applied on top: a stocked skill summon deploys as soon as its owner is back (§20.1), the AUTO DP skills 焰尾 S1 / 凛御银灰 S1 fire on full SP (§20.2), 锏 S3's slashes give 无敌 and 晕眩 / 冻结免疫 (§20.3), the `AUTO_OP_COOLDOWN` comment and the SKILL_RANGE count (13 skills / 26 records) were corrected. The user then answered the open questions (2026-10-01); the answers are **settled decisions**, recorded in their sections and in §20.9, each with the one-line change that flips it: the placed skill summons deploy once at the battle start and 凯瑟琳's device stays a hand card (§20.1), every blocker hits what it blocks (§20.3), 萨卡兹枯朽战车 follows the re-research — ranged enemies reach an operator by its 0.25 collider (§20.4) — and multi-round bounties last two battles (§20.6). Normative lines rewritten with it: §2 (`fear.js`), §3 (the enemies' reach), §5.2 (ASPD floor 20), §5.3 (`camou`, `fear`, `noHeal`, `isolated`), §5.4 (push / pull / ally-selection helpers), §5.5 (initial deployment, blocking, operator attacks, healers, enemy AI, end), §5.6 (skill triggers, the operation cooldown), §6.1 / §6.2 (the Final Assault verdict, summon stacks, summons in the hand, the promotion reward), §7 (bounty draft, 教鞭), §8.2 / §8.3 (`uniteLeft`, `descRaw`, `counterText`, the `elem` fill, the stealth bit), §9 (gauge row, model scale), §10 (机变 card, the card tap), §14 (`left`, the pool floor, the verdict, `uniteLeft`); superseded or revised: §17.5 (the 联防 count), §18.3 (the gauge look), §18.6 #11 (skill summons), §19.2 (the melee-only blocked-first rule; 灰毫 S2), §19.5 (the gauge look).
|
||||
|
||||
### 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).
|
||||
- **Settled by the user**: (1) the placed skill summon **deploys once at the battle start**, the PRTS rule — `SKILL_SUMMON_START_DEPLOY = true` drives the sim and the card hint (PLAYING §4 and SIM.md follow; `test/ui/playtest6_summons.test.js` checks). The playtest #4 report ("是赫默开技能释放一次,不是开局直接就部署了") had been made against v2.2, which already deployed the drone the PRTS way. Flip: `SKILL_SUMMON_START_DEPLOY = false` (only with the skill). (2) 凯瑟琳's device **stays a manually placed hand card** (no shop-state entry read as shown). Flip: dropping `token_10041_cathy_catsld` from `placeable` in `tools/build-data.mjs` only hides the card — the v2.4 automatic placement in her kit would have to come back with it.
|
||||
- [ASSUMED]: skill-summon redeploys cost no DP; no deploy while the owner is off the field; a swap lifts both operators' summons; the device is invulnerable.
|
||||
- Tests: `test/match/playtest6_summons.test.js`, `test/content/playtest6_summons.test.js` (start deploy, each cast, the owner's redeploy), `test/ui/playtest6_summons.test.js`, `test/content/{playtest4_summons,tokens_devices,kits_t3,summon_loadout_conflicts}`, `test/sim/{loadout-summons,docs-examples}`; opt-in `test/ui/playtest6-summons.e2e.test.js` (`SP_E2E=1`).
|
||||
|
||||
### 20.2 Skill triggers and buffs (#15, #16, #8, #3) — `tools/build-data.mjs resolveTrigger`, `sim/skills.js`, `sim/Battle.js`, `sim/targeting.js`, `sim/snapshot.js`, kits
|
||||
|
||||
- **#15 seen**: a skill whose range is wider than the attack range did not fire when an enemy entered it (德克萨斯 剑雨 13 tiles vs 2; 凛御银灰 S2; 锏 S3). **Cause**: every MANUAL skill used the basic rule, and research 03 C1 read the class rows (`skillIndex 0`) as "skill 1 only" (BWIKI).
|
||||
- **Official** (PRTS 卫戍协议/帮助 §作战阶段 技能操作): basic rule "技能就绪,且即将进行普通攻击/治疗"; a skill with its own 技能范围 (not an attack-range increase) "不通过普通攻击/治疗触发技能,仅在技能范围内存在敌人(无视其不可选中)时释放技能"; class strategies — 重装 "不受技能范围影响,受到伤害时释放技能" (下半, 2026/3/14), 战术家 / 执旗手 / 吟游者 when ready, 解放者 / 阵法术师 an enemy in the initial range, 澄闪 / 纯烬 / 荒芜 S3 "全场存在可选目标时"; "自动操作具有3s冷却,在完成一次操作或作战开始时部署的单位将进入冷却".
|
||||
- **Now** (§5.6): the class rows cover **every MANUAL skill** of the class and never an AUTO one — an inference (PRTS names whole classes; under the old reading the 阵法术师 row could never cover 薄绿's default S2, a phalanx never attacks with its skill off); counter-evidence kept: BWIKI's "技能1" and the data's `skillIndex 0` / `-1` sentinel. `SKILL_RANGE`: 13 skills (26 records with the elites), any living enemy on the skill range, checked every tick (attack-range changes keep the basic rule). `GDGLOW_SKILL_2`: a valid target anywhere. The 3 s operation cooldown after each automatic cast and after the battle-start deployment (`flags.startOpCooldown`; the test harness sets it to 0); kit-driven casts of MANUAL skills (波登可 S1, 雪猎 bullets, the 流形 copy) check `skill.opCooling`; AUTO skills are exempt; a pending "next attack" never takes a second charge. Players notice: every 重装 manual skill (号角 S3, 瑕光 S3, 灰毫 S2 — reversing §19.2 —, 古米 S2 …) now waits for the operator to be hit.
|
||||
- **Integration follow-up (WE review round 2)**: the AUTO DP skills 焰尾 S1 迅敏直觉 and 凛御银灰 S1 周旋的谋略 ("立即获得…部署费用") are `SP_FULL` like 伺夜 S1 and 德克萨斯's 冲锋号令·γ型 — an AUTO skill takes no 技能策略 (they used to wait 12–16 s for the next attack).
|
||||
- **#16**: 古米 备用军粮 (AUTO) had picked up the 重装 row. PRTS 备注 "存在生命值不满的可治疗角色时可触发;…切换至治疗模式…直至古米完成一次普通攻击的治疗": it fires on an injured ally of its x-4 area and her next attack heals the most injured ally there (`heal_scale` × ATK); S2 食粮烹制 is MANUAL, so the 重装 rule. Audit: 塞雷娅 S1 急救 — PRTS 备注 "仅在周围有符合血量条件的友方单位时可触发,触发时会替换当次攻击", 修正 "血量小于等于一半" — is checked at her attack (DEFAULT + `allies` + `hpAtMost` 0.5) and that attack becomes the heal; a cast whose ally recovered first is withdrawn with its charge.
|
||||
- **#8**: 新约能天使 S2 gave the shield to the fastest attacker; official (PRTS 备注) "攻击范围内仇恨值最高的我方干员" — now `aggroCmp` (taunt, then the latest deployed), through `battle.alliesInGrid` (孤立 units skipped). The amounts were already right: 180 % (精锐 200 %) of each holder's own max HP, decaying by initial / 30 per s.
|
||||
- **#3 (friend)**: the bond 隐匿 already ended on time (`bondeffect_siracusa` 32 + 0.4 × layers s). 忍冬 S3 "进入迷彩状态,直至下一次开启技能" was the generic `stealth` under the key `stealth`, which made 伪装服's timed 隐匿 permanent (or stripped it) and counted as 隐匿 for 叙拉古 / 家族徽章. Now a separate `camou` flag and status (`vulpis:camou`; 寒芒克洛丝 S1 too) that works like 隐匿 for targeting and on screen — only the enemy it blocks attacks it (PRTS 异常效果 "※迷彩与'阻挡时解除'没有直接关系"; §19.1 kept). What stays see-through after a kill is that official 迷彩, until her next cast. Declined (optional review UX item): a distinct 迷彩 look — the base game draws both see-through, and the detail card has no status row.
|
||||
- [ASSUMED]: enemy skills and splash selectors skip 迷彩 like target selection; every initial deployment gets the full 3 s (the remake deploys everyone at 0 s); a token skill without a `skillType` counts as MANUAL.
|
||||
- Tests: `test/sim/playtest6-skills.test.js`, kit tests (incl. `kits_alt_t4` / `kits_alt_t5` for the AUTO DP skills), `test/data.test.js`, `test/sim/docs-examples.test.js`.
|
||||
|
||||
| Skill (examples) | type | before → after | why |
|
||||
|---|---|---|---|
|
||||
| 德克萨斯 S2 剑雨, 凛御银灰 S2, 锏 S2 / S3, 异客 S3, 焰尾 S2, 忍冬 S2, 诗怀雅 S1, 山 S3, 小满 S2, 归溟幽灵鲨 S1, 崖心 S2, 见行者 S2 | MANUAL | DEFAULT (kit CUSTOM_RANGE) → SKILL_RANGE | own 技能范围 |
|
||||
| 深巡 S2, 古米 S2, 折桠 S2, 瑕光 S2 / S3, 信仰搅拌机 S2 / S3, 泥岩 S3, 塞雷娅 S3, 余 S2, 角峰 / 雷蛇 / 泡泡 / 菲莱 / 蛇屠箱 S2, 灰毫 S2, 星熊 S3, 号角 S2 / S3 | MANUAL | DEFAULT → TAKE_DAMAGE | 重装 row |
|
||||
| 薄绿 S2, 蜜蜡 S2, 卡涅利安 S2 / S3, 玛恩纳 S2 | MANUAL | DEFAULT → SEARCH | 阵法术师 / 解放者 row |
|
||||
| 伺夜 S3, 魔王 S2 / S3, 浊心斯卡蒂 S3 | MANUAL | DEFAULT → SP_FULL | 战术家 / 吟游者 row |
|
||||
| 古米 S1, 瑕光 S1, 塞雷娅 S1 | AUTO | TAKE_DAMAGE → own rule (古米: injured ally; 塞雷娅: at her attack, ally ≤ 50 %) | AUTO: no 技能策略 |
|
||||
| 焰尾 S1, 凛御银灰 S1 | AUTO | DEFAULT → SP_FULL | AUTO DP skill (integration) |
|
||||
| 银灰 S3, 深靛 S1, 野鬃 S2, 蒂比 S1, 断崖 S2, 忍冬 S3 … | MANUAL | DEFAULT (unchanged) | attack-range change ("非攻击距离增加") |
|
||||
|
||||
### 20.3 Combat rules: blocking, ASPD floor, push / pull force vs weight, 炎佑 / 孤立 (#17, #14, #18) — `targeting.js sortEnemyTargets`, `Battle.blockedTargets / push / pull / pullToFront / allySelectable / alliesFor`, `constants.js ASPD_MIN / PUSH_TILES(_EFFECT)`, kits, `tools/build-data.mjs TOKEN_ABNORMAL`
|
||||
|
||||
- **#17 seen**: an enemy held between two operators; after the rear one died the front one took it over (§19.2), but only the enemy attacked. **Workstream finding**: every melee hand-over hit the enemy (3360 immediate hand-overs, 215 late ones — the enemy 0.52–0.69 tile behind the front operator's centre); the cases that did not were a ranged operator on a melee tile with the enemy behind it out of range, the playtest #5 QA rule (§19.2, PRTS 索敌的概念 "我方索敌优先级:阻挡(近战限定)"; "远程位干员…甚至如果这个目标被阻挡在该干员的身后…无法攻击到这个敌人") — the likeliest reading of the report.
|
||||
- **Settled by the user** — "阻挡了就一定要能打到": officially the collision pushes a blocked enemy to its blocker's front, so whatever blocks an enemy can hit it. **Now** every blocker — melee units, summons and a ranged operator on a melee tile alike — may always target the enemies it blocks, in range or not, whatever its facing, and targets them first (`Battle.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`).
|
||||
- **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).
|
||||
- **#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.
|
||||
- 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 |
|
||||
| 莫斯提马 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 |
|
||||
| 见行者 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 |
|
||||
| 薄绿 S2 | 小力 拖拽 = push towards her | pull ≤ 0.5 tile | push table towards her |
|
||||
| ASPD floor | 20 | 10 | 20 |
|
||||
|
||||
### 20.4 Enemies: 萨卡兹枯朽战车, the “萨科塔” 恐惧, 威龙's size (#12, #13, #9) — `sim/fear.js`, `ai.js`, `grid.js straightClear`, `Battle.applyStatus`, `content/enemies.js` (kitTank, pollution), `constants.js ALLY_COLLIDER_RADIUS`, `tools/build-data.mjs MODEL_SCALES`, `render/units.js`
|
||||
|
||||
- **#13 seen**: “萨科塔之翼” below half HP should fly about at random in a small area (the user wrote 战栗) but hung still. **Cause**: every feared enemy was frozen. **Official**: the bird fears itself — PRTS "生命值首次低于50%时,对自身施加持续5s的恐惧,在5s内移动速度最终提升至150%" (之眼 / 昂首 the same); 恐惧 = "无法被阻挡并四散逃跑" (ba.fear), 战栗 = "被阻挡后无法进行普通攻击" (ba.tremble), so the behaviour described is 恐惧 — report and data agree, only the name differs. PRTS 诱发移动 §恐惧: 恐惧可达地块 = tile centres within 10 of the hit point, in the ±45° fan away from the source, passable, able to reach the goal, not the objective; next stop a random reachable tile within 5 path steps (offset in a 0.5 square), else its own tile; no source / itself as source ⇒ no reachable tile; 恐惧 outranks 诱导.
|
||||
- **Now**: `fear.js`; the birds flutter within ±0.25 of their tile for 5 s at ×1.5, then fly on. Operator fears (叙拉古 6, 妮芙 S2, 荒芜拉普兰德 S3) make enemies flee into the fan instead of freezing (a visible change). The 恐惧 and 诱导 walks re-plan from where the enemy stands after a push or an obstacle change (`grid.version`; PRTS "恐惧效果结束时立刻清除【恐惧移动】"); a checkpoint on an obstacle tile counts as unreachable; an off-centre ground start steps back to its tile centre when the straight line would clip a wall (`grid.js straightClear`, shared). They used to walk through walls after a push (4–7 % of random pushes on m01–m04).
|
||||
- **#12 seen**: 萨卡兹枯朽战车's damage felt low. **Already right**: stats (lv0 HP 12000 / 15000, ATK 750 / 850, DEF 750, RES 10, BAT 4.5, range 2.2, weight 4; no season override, no wave `overwrittenData`), the round multipliers, the target filter (只攻击位于低地的我方单位,且不会攻击飞行单位) and the 200 % blocked melee attack. **Fixed (WG)**: 秽蚀轰击 timing (initSp 2 = maxSp 2 ⇒ attacks 1, 4, 7 …, not 3, 6 …); the 【污染秽蚀】 radius 1.7 (was 1 [ASSUMED]); its damage "每秒受到50/25点真实普通伤害…同名效果不叠加" — true damage through `dealDamage`, one tick per second per unit (also 萨卡兹枯朽战士's death zone, r 2, 8 s; zones are now absorbed by shields, scaled by damage-taken modifiers and fire TAKE_DAMAGE skills). The skill hit is 100 % (PRTS skill text), also against a blocker [ASSUMED that it fires there]. Measured: roughly unchanged overall (first 20 s +4…+42 %, a long blocked fight −7…−9 %).
|
||||
- **#12 re-researched (the user's follow-up: "远程攻击射程挺远、伤害很高")**: enemy_database (`enemy_1272_nhtank` / `_2`: rangeRadius 2.2, `PollutedRangedAtk` range_radius 2.2, atk_scale implicit 100 %, `Empty.attack@chuang_atk_scale` 2, spData 攻击回复 2 / 2), the PRTS pages (攻击方式 近战 远程; "仅半径2.2范围内存在位于低地的非飞行我方单位时可触发:对目标造成100%物理普通伤害"), activity_table act2autochess (DOT special, no stat override; the range-4 tank is 斗智场's 改装排污车, another mode) and the act1 / act2 autochess levels: **there is no higher scale and no longer data range**. The hard hits are the round multiplier (table below: 绝境 R7 998, 尖端 R13 1656 per ranged hit before DEF). What the data does support is a longer *effective* reach: PRTS 作战机制 §碰撞体积与位置识别 "我方干员碰撞体积基本均为以0.25格为半径的圆形…关于碰撞体积与远程索敌,碰撞箱碰撞是为最常用的方式,索敌抬手等均使用碰撞箱" — the remake measured enemy range from the operator's centre. **Settled**: every ranged enemy's normal attack takes an ally whose 0.25 collider touches its range circle (`ALLY_COLLIDER_RADIUS`, `ai.js enemyAttack`: centre distance ≤ rangeRadius + 0.25; content skills / zones keep their own point radius [ASSUMED]). 枯朽战车: reach 2.2 → 2.45 (13 → 21 tiles around it); a lone operator beside its lane takes 4 hits instead of 3 (绝境 R6, DEF 300: 2095 → 2793 attack damage, 2595 → 3593 with the zone); dense lineups unchanged (its 4.5 s interval is the limit). The user's memory of a *long* range is stronger than the data supports — said plainly in the user notes. Flip: `ALLY_COLLIDER_RADIUS = 0`.
|
||||
- **#9 seen**: 威龙 drawn far too large. **Official**: the client scales each enemy Spine model in its battle prefab — 0.27 for 1454 of 2147 prefabs; 威龙 0.16, 妖怪 0.20, 妖怪MKII 0.22, the 岁 relics 0.4–0.6. **Now**: `MODEL_SCALES` (122 prefabs) → enemies.json `modelScale` = product / 0.27 (125 keys); `render/units.js` scales the skeleton and the bar height (enemies without setup-pose bounds: chibi headroom × `modelScale`). 威龙 is 1.28 tiles wide instead of 2.15 — 1.08 × a 妖怪, as in the game. 85 enemy skeletons still have no bounds in data/assets.json (asset pipeline, not addressed).
|
||||
- Tests: `test/sim/playtest6-enemies.test.js` (flutter, determinism, operator fear, 恐惧 > 诱导, push / obstacle re-planning on m01–m04, the tank incl. its reach), `test/render/playtest6-enemies.test.js`, `test/local-extract.test.js` (`enemy_scales`), `combat.test.js`; opt-in `test/render/playtest6-enemies.browser.test.js`.
|
||||
|
||||
| Round | Tank | 险境 ATK · ranged hit · blocked | 绝境 | 终极 |
|
||||
|---|---|---|---|---|
|
||||
| R1 | 萨卡兹枯朽战车 | 600 · 600 · 1200 | 825 · 825 · 1650 | 825 · 825 · 1650 |
|
||||
| R4–R7 | 萨卡兹枯朽战车 | 726 · 726 · 1452 | 998 · 998 · 1997 | 998–1208 · … · 1997–2416 |
|
||||
| R8 | 尖端萨卡兹枯朽战车 | — (banned) | 1131 · 1131 · 2263 | 1369 · 1369 · 2738 |
|
||||
| R13 | 尖端萨卡兹枯朽战车 | — | 1656 · 1656 · 3313 | 1656 · 1656 · 3313 |
|
||||
|
||||
### 20.5 Final Assault leader at 0 HP that would not die (#5) — `sim/constants.js BOSS_POOL_MIN_HP`, `match/finalAssault.js SharedBossPool`, `sim/spec.js LocalBossPool`, `match/Match.js _creditBoss / _onResult / _finishFinal`, `ui/hud.js PhaseCapsule`, `ui/gameLogic.js bossPctText`
|
||||
|
||||
- **Seen**: 2 humans (AI teammates eliminated), 绝境, act2 m01, client-side combat — the boss bar read "0.0%" with 95 s left, the leader fought on until the overtime drain took the team LP, and the result said 胜利. **Reproduced**: real 2-human client-combat matches (pool scaled to 5 %): on v2.4.1 11 of 40 fights stalled, every boss family; about 11 minutes of drain followed, ending 胜利 or 失败.
|
||||
- **Cause**: float dust in the shared pool, not a boss script, hit area or part. `_creditBoss` subtracts each report per player and the pieces add up only in exact arithmetic (40000 − 12345.6 − 7654.3 − 20000.1 = 3.6e-12); the browser's `LocalBossPool` (server hp − (cum − acked)) absorbed every later hit (less than half an ulp of `cum`), so the leader never died, nothing new was reported, and m.public rounded the server's dust to "0 HP". When the team LP hit 0, the verdict was read from the pool after the final `b.result`, whose per-player damage is `Math.round`ed — rounding up emptied the dust (胜利).
|
||||
- **Official**: PRTS 卫戍协议/帮助 §领袖作战阶段 "…计时结束后战斗仍然会继续,但每超时1秒扣除1目标生命值"; 卫戍协议:盟约 下半 "…使目标生命值扣除至0,则无视倒计时直接失败" (research 01 §10). The victory after the drain contradicted it; the user was right.
|
||||
- **Now**: a shared pool never holds less than `BOSS_POOL_MIN_HP` (1) — the hit, or the per-player credit, that would leave less takes the rest (SharedBossPool, LocalBossPool), and a browser pool reads any finite rest below 1 as 0, so a pool shown at 0 is a dead leader on every field (`Battle._bossSync` / `_checkEnd`) and `b.end cleared` goes out at once. The first end the server registers decides (`Match._finalEnding`: pool 0 → victory, team LP 0 → defeat); after a forced end no report credits the pool. The boss bar prefers the live pool of the battle on screen over the ~1 Hz `m.public.bossHp` and never shows "0.0%" for a living leader ("<0.1%"). Checked and ruled out: the kill check reads the pool whatever the unit's own hp or invulnerability; mirrored and hidden-core copies share it; parts only pass damage; the Hidden Core, 4 humans and server-run mode end promptly.
|
||||
- [ASSUMED]: the 1-HP floor (official HP is a float shown rounded — PRTS 作战机制 "红字在显示时会就近取整"); a client emptying the pool within its latency after the server registered team LP 0 still loses; the HUD prefers the local snapshot pool.
|
||||
- Tests: `test/sim/playtest6_bosspool.test.js`, `test/match/playtest6-bosspool.test.js`, `test/ui/playtest6-bosshud.test.js`.
|
||||
|
||||
### 20.6 Match flow: the bounty draft, 教鞭, the 联防 leak counter, the promotion reward (#4, #7, #19) — `tools/build-data.mjs bountyDraftExclusion`, `match/choices.js` (`MULTI_ROUND_BOUNTY_BATTLES`), `content/items/meta.js`, `Match.addBounty`, `PlayerState.pushRewardOffer / effectsView`, `sim/spec.js uniteLeft`, `Match._uniteLeft`, `fields.js uniteBillBounds`, `ui/hud.js MissTag`, `ui/teamPanel.js rowLp`
|
||||
|
||||
- **#4 seen**: "悬赏决策的敌人…正常只会在选择之后出现两回合,现在是一直出". **Cause**: a 1–2-battle card already stopped after its battles, but the draft drew from all 129 ENEMY_GAIN cards (战术特训 and the hidden 鸭爵 set included) and every card was plain text (the server's stripped `desc` beat the rich text), so a 每场 multi-round card looked like a 下场 one.
|
||||
- **Official**: PRTS 卫戍协议:盟约 下半/PRTS盟约记录 §机变阶段 "敌人轮选" lists the 1- and 2-battle bounties, 源石虫·特训 and the 7 multi-round cards and comments out the 鸭爵 set; 战术特训 sits under "※以下悬赏任务仅由法术教鞭生成"; §法术 教鞭 "于3个战术特训的悬赏任务中选择一项"; 杜宾 加练! "若自身战斗完美作战可获得资金". The card text colours the duration (`<@ba.vup>下场作战</>` / `两场作战`, `<@ba.vdown>每场</>`).
|
||||
- **Now**: cards.bounty `draft` = that table (105 cards: 56 next-battle incl. 源石虫·特训, 42 two-battle, 7 multi-round), drawn uniformly, no multi-round cap; 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).
|
||||
- **#7 seen**: the leaker's number never fell during 联防 and seemed capped at 10. **Official**: PRTS 帮助 "防卫失败的玩家可通过上方信息栏确认自身所属敌人的剩余数量"; the capsule shows an orange runner tag ×N (research 09 `tag_miss`, drawn with ui/battle bg_miss_enemy [ASSUMED]); the 10-LP cap per round is unchanged. **Now** (revising §17.5): `uniteLeft` per leaker = not spawned yet + alive + counted leaks — it falls on kills and rises on splits / summons (children inherit `sourcePlayerId`); the server takes it from `b.progress left`, the headless timeline `[gt, killed, total, left]` or the exact `uniteSurvivors`, clamps live values to `uniteBillBounds`; m.public players[] `uniteLeft` is uncapped and `pendingLp` = min(cap, uniteLeft). ×N in the capsule and in the leakers' team rows; a client with the 联防 field on screen shows its local replica's counts there; a teammate's row tooltip names the teammate.
|
||||
- **#19**: PRTS 帮助 "临时刷新3名高于调度中心等级1阶(最多6阶)的干员"; the official 玩法介绍 image shows three different operators at price 0. v2.4.1 rolled three times independently (14–21 % of offers had a duplicate). **Now** three different chess, copy-weighted; a tier short of different chess tops up from the tier below [ASSUMED]; the offer reserves no copies. The integration QA found the same repeat in the other free pick-one chess offers — 信标 (same tier), 寻呼模块 (same bond ≤ shop level, content/items/meta.js) and the engine default `builtinMeta use_equip_reward_special_goods_char_chess` — they now draw different operators too (信标 tops up from the tier below, 寻呼模块 shows fewer cards when the pool has no other one), and `pushRewardOffer` drops a repeated id from any list it is given (`test/content/items.test.js`).
|
||||
- Tests: `test/match/playtest6-matchflow.test.js` (the draft, two battles for a multi-round card end to end, 联防 counts, rewards), `test/match/followups.test.js`, `test/ui/playtest6-unite.test.js`, `test/ui/playtest6-bounty.test.js`, `test/content/items.test.js` (教鞭).
|
||||
|
||||
### 20.7 机变 card text; the shop's confirming tap on phones (#6, #10) — `ui/choiceOverlay.js`, `css/screens/game-panels.css`, `ui/shopBar.js`, `css/screens/game-shop.css`, `css/devices.css`
|
||||
|
||||
- **#6 seen** (screenshot 2, Android 756×366 CSS px, 终极 co-op): the 道具补给 cards showed only icon, name and tier. **Cause**: the card was a centred column sized for 16:9; with the root font clamped at 40 px a 3×2 card was 105 px tall and showed 0.2 lines of text (217 of 223 texts cut), in all four families; at 1920×1080 nine long texts were cut at 4 lines. **Official** (research 06 §11.5; screenshots Mb2mtd9 / Wrkhtxv / SEbySVl): a framed icon at the top-left with the bold name beside it, the description below, numbers in blue, the tier chip on the icon's corner, the taker's round avatar alone at the top-right. **Now**: that layout; the text from `cardText` (the card's own `descRaw`, else the data `descRaw` while its plain form equals the server's `desc`, else the server text), clamped to 7 lines (8 at line height 1.3 on phones under 700 px wide: with Noto Sans SC — the page's web font, metrics of Android's CJK font — 海沟实验体 wraps to 8 lines at 640×360, residual QA); short screens get a tighter overlay header (no CONTINGENCY micro line) and .19rem text; the taker badge is the avatar alone (name in its tooltip and the card's aria-label) and the taken card's head reserves its width. All 223 texts (51 items, 129 bounty, 43 tactic) fit in full, taken or not, from 640×360 to 2560×1080, measured with Noto Sans SC.
|
||||
- **#10 seen**: on the phone the second tap on the 确认购买 strip did nothing; only the middle of the card worked. **Cause**: the card's invisible ⓘ corner had the generic 44 px touch area (its 30 px override lost on specificity: `:is()` scores 0,3,1 against 0,2,1), covering the bottom-right quarter of the card and the right half of the strip (120 / 144 grid points); below 768 px the left-docked detail card opened by the first tap also reached over the bar's first cards. **Official** (research 09 §5): first tap = detail + 确认购买, second tap buys; the card has no info corner. **Now**: the ⓘ is removed (a tap anywhere on a card is the card's tap), `.shopbar.has-armed { z-index: 10 }` keeps the armed bar above the detail card (z 9; below `.uframe` 12 and `.spov` 20), and a card that cannot be bought right now opens its detail (not aria-disabled, help cursor). 144 / 144 points on touch at 640×360 … 932×430 and with the mouse at 1920×1080; the strip centre, its right end and the bottom-right corner buy.
|
||||
- [ASSUMED]: phone text 7.6 px; the avatar inside the card's corner; the gold strip may cover the last line of the longest texts while a card is selected; the micro line hidden on phones; the armed bar hides the detail card's corner on < 768 px phones; solo 3-card sizes.
|
||||
- Tests: `test/ui/playtest6-ui.test.js`; opt-in `test/ui/playtest6-ui.e2e.test.js` (`SP_E2E=1`: every text at 9 viewports, each card taken once, the hit grid by tap target and real taps); mock.e2e (detail + Esc through a card tap).
|
||||
|
||||
### 20.8 Element gauge on screen (#11) — `sim/damage.js elementView`, `render/units.js`, `render/textures.js`
|
||||
|
||||
- **Seen**: correcting playtest #5, the user said the 海嗣's element damage is right after all, but the gauge "fills at the wrong rate: far from used up when the burst already hit". **Measured** (Node sim → interp → UnitView, and headless Chrome on the real view; 14 element enemies vs 古米, the reviewer with 5 operators against a mixed 海嗣 wave): the drawn gauge follows the sim within one frame. Steady attackers burst off a 1–5 % sliver; big hits legitimately burst far up the bar (掠海漂移体 does 50 % of its ATK × the round's ATK per hit — 25 % of the gauge at base, ≈ 44 % at 绝境 R11; 萨卡兹王庭军精锐术师 死亡之眼; 昆图斯's skills).
|
||||
- **Display faults, all fixed**: `elementView` rounded the fill to the nearest 1 % (a gauge 1–4 元素值 short showed empty); the v2.3 ring right of the bars sat on the next operator's tier chip (read as the neighbour's gauge, and a shrinking ring reads like a filling one); during the 爆发冷却 the refill was the same white as the 元素值 left while the burst's stun / damage hit the unit.
|
||||
- **Official** (PRTS 元素): "当单位存在一项不为最大值的元素时,模型下部会显示对应的元素图标,并以白条显示剩余的元素值"; enemies get a small colour-only icon; during the 爆发冷却 "单位的元素条显示缓慢恢复至上限(动画效果)" (no colour given).
|
||||
- **Now**: fill rounded DOWN to 1 %, kept at 0.01–0.99 outside a burst (1 during one) — the bar only runs out when the gauge bursts; the gauge is a row under the unit's own HP / SP bars inside their span: the element disc (0.15 tile, 8–15 px; enemies ×0.8, plain colour; light rim) and a white bar of the remaining 元素值 (SP-bar height, dark track); during the 爆发冷却 the bar refills in the element's colour (`ELEMENT_RING[el].tint`) with the disc pulsing, white again when it ends.
|
||||
- [ASSUMED]: the refill colour and pulse; the geometry; placement under the above-head bars (official: under the model); the disc rim. The render-demo recordings carry fills made with the old rounding (display only).
|
||||
- Tests: `test/sim/playtest6_elemgauge.test.js`, `test/render/playtest6_elembar.test.js`, `test/render/downring.test.js`; opt-in `test/render/elembar.browser.test.js`, `downelem.browser.test.js`.
|
||||
|
||||
### 20.9 Settled decisions and what stays [ASSUMED]
|
||||
|
||||
The user's answers of 2026-10-01 (playtest #6 follow-up):
|
||||
|
||||
| Decision | Settled rule | One-line flip |
|
||||
|---|---|---|
|
||||
| A placed skill summon at the battle start (§20.1) | deploys once, free, then with each cast (PRTS §作战阶段) | `shared/constants.js SKILL_SUMMON_START_DEPLOY = false` |
|
||||
| 凯瑟琳's 爬行号·防护单元 (§20.1) | a manually placed hand card | drop `token_10041_cathy_catsld` from `placeable` (`tools/build-data.mjs`) — and restore the v2.4 kit placement, or the device never appears |
|
||||
| Multi-round bounty cards (§20.6) | last two battles like "接下来两场作战", blue text, in the draft | `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 |
|
||||
|
||||
Still [ASSUMED] (each in its section): the class rows covering every MANUAL skill (§20.2); skill-summon redeploys free and never while the owner is away (§20.1); the 1-HP pool floor and the server-order verdict (§20.5); a short promotion tier topping up from below and “神秘顾客” keeping the band bounties (§20.6); the 弹道 column for unclassified pushes and the other displacement details (§20.3); 枯朽战车's skill firing on its blocker at 100 % and content enemy skills keeping a point radius (§20.4); the gauge's refill colour and placement (§20.8); the 机变 card's phone sizes (§20.7).
|
||||
Reference in new issue
Block a user