Playtest #5 fixes: hovering enemies, target order, blocking hand-over, 联防 knocked-out, elements, huge-boss hit area, maps, gear icon, phone HUD, 至简 price
- #1 近地悬浮 enemies count as air units (Unit.isFlying): ground-only attacks, splash, terrain and traps skip them; PRTS 不可对空 skill effects made ground-only; 掠海漂移体 / 吉兆飞鳞 drop modes; the bot plans them as air - #2 an operator knocked out before 联防 is fielded and force-exited (PRTS 强制退场): it lies on its tile with the redeploy ring and redeploys (full timer [ASSUMED]); partners, observers and the server fallback included - #3 element audit of every operator, item and enemy: 海嗣 numbers are official; bursts are 无来源, 元素脆弱 only raises 元素伤害, 元素抗性 applied, 盟约·辅助干员 fills all three gauges, 菲莱 / 哈洛德 / 纯烬 / enemy kits fixed - #4 blocking by the official contact radius (0.7071): another operator in contact takes over at once when the blocker dies, withdraws or is stunned; melee blockers attack what they block - #5 enemy target order per PRTS 索敌: blocker, special priority, taunt, last deployed; initial deploy 从上到下>从左到右, operators before summons; special priorities break ties by taunt then deploy order - #6 act2 m01's normal-field blower machines (row 13) are drawn in the normal / 联防 views - #7 Final Assault prep legality reads the player's half of the boss field (fence tiles deployable); all 11 stages audited against the official level files - #8 regular settings gear; #9 the prep camera keeps the bench and temp rows clear of the shop bar on phones - #10 huge bosses have the official 4.95 × 2.95 hit area (sim/body.js) and are 自缚 / unblockable - #11 至简 costs 1: 购买价格为N's bb.price is a discount off the tier price, applied before other price effects - DESIGN §19 and the normative §3 / §5 / §6 / §7 lines, SIM / META / DATA / PLAYING updated; 2595 unit tests and every browser suite green; dev recordings regenerated
This commit is contained in:
1 parent
d4f868cd19
commit
15291c2293
104 files changed
+5539
-662
No files matched your search
+3
-2
@@ -329,7 +329,7 @@ blackboards (transitively). Level = `randomEnemyAttributeDict[key].level` (0 for
|
||||
|---|---|---|
|
||||
| `key`, `name`, `level`, `rank`, `handbookIndex` | …, `"萨卡兹枯朽前锋"`, `0`, `"NORMAL"`, `"NAC18"` | `rank` NORMAL / ELITE / BOSS |
|
||||
| `desc`, `descRaw`, `applyWay` | …, `"MELEE"` | `applyWay` MELEE / RANGED / ALL / NONE |
|
||||
| `stats` | `{"maxHp":4700,"atk":300,"def":100,"res":40,"moveSpeed":0.8,"bat":3,"aspd":100,"rangeRadius":0,"rawRangeRadius":-1,"blockCnt":1,"massLevel":1,"lpr":1,"hpRecoveryPerSec":0,"elementRes":0,"elementDmgRes":0,"hitRatePhys":0,"hitRateArts":0,"dmgType":"phys","dmgTypes":["phys","arts"],"motion":"WALK","immunities":{…},"otherImmunities":[],"tauntLevel":0}` | level-0 (unscaled). **`rangeRadius` ≥ 0 is the normal-attack radius under the DESIGN §5.5 contract: `> 0` ⇔ the enemy attacks units it is not blocked by; always 0 for `applyWay` MELEE** (melee enemies only hit their blocker; 粉碎攻坚手/宿主士兵/深池方阵步兵 carry an official 1.2–2.5 used by abilities) and negative sentinels become 0. `rawRangeRadius` = the official value. `blockCnt` = block slots it consumes; `lpr` = lifePointReduce; `dmgType` = first handbook damage type (`phys`/`arts`/`heal`/`none`/…), full list in `dmgTypes`. Undefined database fields (`m_defined:false`) holding the zero value fall back to the defaults `lpr` 1, `bat` 1, `aspd` 100, `moveSpeed` 1 (see §15) |
|
||||
| `stats` | `{"maxHp":4700,"atk":300,"def":100,"res":40,"moveSpeed":0.8,"bat":3,"aspd":100,"rangeRadius":0,"rawRangeRadius":-1,"blockCnt":1,"massLevel":1,"lpr":1,"hpRecoveryPerSec":0,"elementRes":0,"elementDmgRes":0,"hitRatePhys":0,"hitRateArts":0,"dmgType":"phys","dmgTypes":["phys","arts"],"motion":"WALK","immunities":{…},"otherImmunities":[],"tauntLevel":0}` | level-0 (unscaled). **`rangeRadius` ≥ 0 is the normal-attack radius under the DESIGN §5.5 contract: `> 0` ⇔ the enemy attacks units it is not blocked by; always 0 for `applyWay` MELEE** (melee enemies only hit their blocker; 粉碎攻坚手/宿主士兵/深池方阵步兵 carry an official 1.2–2.5 used by abilities) and negative sentinels become 0. `rawRangeRadius` = the official value. `blockCnt` = block slots it consumes; `lpr` = lifePointReduce; `dmgType` = first handbook damage type (`phys`/`arts`/`heal`/`none`/…), full list in `dmgTypes`. `elementRes` = 损伤抵抗 (official `epResistance` = EP_RESISTANCE, % off every element gauge fill; 10 on 转译基底·α, 0 on every other enemy here) and `elementDmgRes` = 元素抗性 (`epDamageResistance`, % off 元素伤害 — the bursts' HP damage; 0 on every enemy here — newer event enemies of the full database reach 10–15 on both) — PRTS 元素 / 游戏数据基础 and the enemy pages' 损伤抵抗 / 元素抗性 rows. Undefined database fields (`m_defined:false`) holding the zero value fall back to the defaults `lpr` 1, `bat` 1, `aspd` 100, `moveSpeed` 1 (see §15) |
|
||||
| `abilities[]` | `{"text":"位于源石污染区内时,攻击造成法术伤害","textRaw":"…","format":"NORMAL"}` | handbook ability list |
|
||||
| `talents` | `{"bb":{"1.hp_ratio":0.2,"1.damage_scale":0.5},"bbStr":{}}` | talentBlackboard |
|
||||
| `skills[]` | `{"prefabKey":"2","priority":0,"cooldown":50,"initCooldown":0,"spCost":0,"bb":{"hp_ratio":0.02},"bbStr":{"branch_id":"boss_summon_enemy","enemy_key":"enemy_1005_yokai"}}` | `branch_id` → `waves[t].branches` |
|
||||
@@ -341,6 +341,7 @@ blackboards (transitively). Level = `randomEnemyAttributeDict[key].level` (0 for
|
||||
| `templateSlot` | `"N"` | placeholder slot when the key is a template key |
|
||||
| `summons[]`, `inactiveIn[]`, `seasonOverride` | | spawned enemies; modes banning it; overridden fields |
|
||||
| `iconId`, `spine` | `"enemy_1422_lrsldr"` | asset ids |
|
||||
| `hitArea` | `{"w":4.95,"h":2.95,"dx":0,"dy":1}` (`enemy_9013_acstmk`) | **huge units only** (巨型单位, 7 keys: 假想敌:胄 ×2, 假想敌:管 ×2 — the 隐秘核心 one `dx` 1 —, 盐风主教昆图斯, 阿利斯泰尔,帝国余晖, “萨米的意志”): the hit rectangle, `w` tiles along the columns × `h` along the rows, centred on the unit's position moved `dx` columns right / `dy` rows up (sim/body.js; user playtest #5). Not in the game tables (the collider lives in the prefab): `tools/build-data.mjs HIT_AREAS` by `prefabKey`, from PRTS "巨型单位:受击判定区域为长4.95、宽2.95的长方形,向上偏移1.0" and PRTS盟约记录 (the season's 阿利斯泰尔 / “萨米的意志” versions); absent = a point. All 7 are also 自缚 + 无法被阻挡 (PRTS 天赋 — not a data field either: content/bosses.js `SELF_BOUND`) |
|
||||
|
||||
## 10. `factions.json` — special enemies (特训敌人)
|
||||
|
||||
@@ -400,7 +401,7 @@ exactly (the builder warns if a multi-fragment template ever appears).
|
||||
| `mapChars[]` | `{"key":"char_605_cmedic","alias":"char_605_cmedic#1","pos":[10,2],…}` | band_amedic map characters (→ `tokens.json`) |
|
||||
| `special` | `{"mire":{"intervalSec":3,"aspdPerStack":-0.05,"moveMulPerStack":-0.05,"maxStacks":10,…}}`, `{"infection":{"bb":{"damage":70,"atk":0.2,"attack_speed":20,"duration":300}}}`, `{"deepsea":{"skillId":"sktok_tidectrl_3","bb":{…}}}`, `{"smog":{"rule":"operatorsNotTargetableByEnemyRanged"}}`, `{"blower":{…}}` | special-terrain parameters |
|
||||
| `runes[]`, `globalBuffs[]` | `{"key":"map_tile_blackb_assign","bb":{…},"bbStr":{…}}` | level runes (act2 m03 reed rune has no reed tiles — leftover) |
|
||||
| `deployTiles` | `{"normal":{"melee":[[9,3],…],"rangedOnly":[[10,4],…],"changedByDevices":[…]},"bossLeft":{…},"bossRight":{…}}` | deployable tiles at match start: `melee` = LOW buildable ALL/MELEE (ranged may use them too), `rangedOnly` = high ground / active platforms; tiles under active crates/mounds excluded |
|
||||
| `deployTiles` | `{"normal":{"melee":[[9,3],…],"rangedOnly":[[10,4],…],"changedByDevices":[…]},"bossLeft":{…},"bossRight":{…}}` | deployable tiles at match start: `melee` = LOW buildable ALL/MELEE (ranged may use them too), `rangedOnly` = high ground / active platforms; tiles under active crates/mounds excluded. `bossLeft` / `bossRight` (field coordinates, boss rows 1–5) are what a player may deploy on in a boss round's prep: board (r, c) = boss (r − 7, c) / the right half mirrored (r − 7, 20 − c) (server/match/board.js `field`, user playtest #5 item 7) |
|
||||
| `groundPaths` / `groundPathsWithDevices` | `{"12,10->9,2":[[12,10],[12,9],…,[12,3],[11,3],[10,3],[9,3],[9,2]]}` | the ground route of the sim's own `server/sim/grid.js` flow field (research 08 §3.4: 4-direction SPFA from the goal, crates cost 1000, Bresenham line-of-sight smoothing — official route lengths — plus the remake's road-over-floor preference: among equal-length routes the fewest non-blockable floor / gate tiles, and smoothing never cuts across floor the grid route does not walk, e.g. 战场#01's lower gate takes the col-8 road, not the col-9 floor lane; DESIGN §14 corrections) as the tiles an enemy crosses, over the whole map, without / with the match-start devices (crates cost 1000, platforms / mounds blocking [ASSUMED]); the battle re-reads its flow fields live |
|
||||
| `options`, `config` | `{"characterLimit":8,"moveMultiplier":0.5}`, `{…configBlackBoard strings…}` | |
|
||||
|
||||
|
||||
+151
-12
@@ -65,6 +65,7 @@ server/
|
||||
buffs.js Buff model, status catalogue (stun, freeze, cold, sleep, slow, fragile, stealth, …)
|
||||
damage.js damage & heal pipeline, element gauges
|
||||
targeting.js target selection helpers, range tests, priorities
|
||||
body.js hit areas of huge enemies: the one helper behind every range test on enemies (§3, §19.4)
|
||||
projectiles.js projectile flight/impact
|
||||
skills.js skill runtime: SP, triggers, durations, ammo, charges; interprets SkillSpec
|
||||
ai.js enemy movement/attack AI, operator attack loop
|
||||
@@ -127,7 +128,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. Radius ranges (enemies, splash, auras) use Euclidean distance in tiles.
|
||||
- **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溅射半径").
|
||||
- **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).
|
||||
|
||||
@@ -180,7 +181,8 @@ b.on(event, fn, { priority=0, owner } ) / b.off(...) // hook bus (§5.4)
|
||||
colOffset, // 0 for own field; +8 for the right half in unite/boss
|
||||
units: [ { uid, kind: 'chess'|'token', chessId /* golden id if elite */, tokenId?, row, col,
|
||||
items: [itemId...], // equipped items (golden ids when merged)
|
||||
carryState?: { hpPct, sp, skillActive } } ], // unite: helper units keep end-of-combat state
|
||||
carryState?: { hpPct, sp, skillActive } | { down: true } } ], // unite: helper units keep end-of-combat state;
|
||||
// { down: true } = knocked out at its end: deployed, then forced out (§19.3)
|
||||
bonds: { [bondId]: { count, active, tier, layers } }, // snapshot from bondsMeta at combat start
|
||||
bandId, playerEffects: [ EffectRef ], // active band/choice/team effects with battle parts
|
||||
lpForBoss?: number }
|
||||
@@ -255,13 +257,13 @@ Engine helpers (content must use these, never touch internals): `battle.dealDama
|
||||
|
||||
### 5.5 Combat rules (engine)
|
||||
|
||||
- **Initial deployment** at t=0, free, order top→bottom then left→right (right-side boss player: right→left). Deploy effects fire (`deploy` with `initial: true`). Then `battleStart`.
|
||||
- **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); summons, devices and enemies leave at once.
|
||||
- **Blocking**: a ground op with free capacity blocks a ground, non-unblockable enemy whose position enters its tile (|dx|,|dy| ≤ 0.5). Enemy uses `blockCnt` capacity (default 1). Blocked enemies stop moving and attack their blocker if melee (or anyone in range if ranged, blocker first). Blocker death frees its enemies.
|
||||
- **Operator attacks**: every `interval` s if a valid target exists; targets = up to `maxTargets` (default 1) from `unitsInGrid(rangeGrid)`, priority: (1) units it blocks (melee), (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) cannot hit FLY enemies. 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.
|
||||
- **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).
|
||||
- **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 [ASSUMED] 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: priority blocker → highest taunt → **latest deployed**; they pause movement for `ATTACK_PAUSE = 0.35 s` per attack. FLY enemies ignore blocking and ground obstacles, follow their checkpoint route.
|
||||
- **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. Then × `dmgDealtMul` (source) × `dmgTakenMul` & type-specific taken multipliers (target) × `dmg.mul`; shields absorb; dodge checks for phys/arts. Element damage fills a gauge (1000; enemy leaders 2000) × `elemTakenMul` × (1 − 损伤抵抗/100), 损伤抵抗 = data `epResistance` (PRTS 元素). A full gauge bursts with the side-specific official effects (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).
|
||||
- **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.
|
||||
- **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).
|
||||
|
||||
@@ -314,7 +316,9 @@ LOBBY(room) → INFO_CHECK (co-op 25 s; solo and any single-human match untimed
|
||||
COMBAT (one Battle per alive player, all in parallel, 2×; watchers get snapshots; solo: pausable, §14 Solo pause)
|
||||
UNITE (co-op only, if ≥1 leaked & ≥1 perfect: ≤2 perfect helpers in `unite.js helperOrder` order (research 08 §5 / PRTS 帮助:
|
||||
most units on the field > an active bond > most standing units > seat; the pair ordered by units > active bond >
|
||||
Σ active layers > standing > seat, the first one on the right-hand field) fight the union of leaks; no layer gains)
|
||||
Σ active layers > standing > seat, the first one on the right-hand field) fight the union of leaks; no layer gains;
|
||||
a helper's operator knocked out at the end of its own combat enters down — carryState { down: true },
|
||||
PRTS "上一阶段为退场状态的干员强制退场", its full redeploy timer [ASSUMED] (§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)
|
||||
@@ -378,7 +382,7 @@ Each domain file `server/sim/content/{tokens,bonds,garrisons,items,bands,enemies
|
||||
- garrisons: 43 effect keys (IN_BATTLE via hooks; SERVER_* via meta registry).
|
||||
- items: 56 equipment (+golden) + 3 Arts; 变形同构体 bond grant; VI-tier set/combos.
|
||||
- bands: 40 strategies (meta + battle).
|
||||
- enemies: special-type behaviours (FLY, TIMES, ELEMENT, DOT, INVISIBLE, REFLECTION, SPECIAL) and notable individual abilities from `data/enemies.json`; bosses 1–10 scripted; devices/terrain.
|
||||
- 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).
|
||||
|
||||
---
|
||||
@@ -620,7 +624,7 @@ Thirteen reports after playing v2.2. Where each is handled: #1 picking should si
|
||||
- **Battle** (`app.js battleUnitAt` → `pickBattle`): an ally on the tile under the pointer — a knocked-out operator lying on its tile (§18.3) included. An enemy within `ENEMY_REACH` 0.6 tile [ASSUMED], using whichever is nearer: its ground position from the pointer's point on the ground, or its drawn body on screen (the upright line from its feet up to its head, in its px per tile) — so a press on a tall enemy's or the boss's torso or head picks it; a flying enemy uses its drawn body only. When an ally tile and an enemy both qualify the nearer (in tiles) wins; ties → the ally, then the front-most. Dead and dying units are not picked (a knocked-out operator waiting on its tile is).
|
||||
- **Drag**: the dragged unit is held under the pointer, mouse and touch alike — its drawn (lifted) feet `DRAG_HOLD_TILES` = 0.45 tile (in its own px per tile) below the pointer [ASSUMED: the v2.1 offset], so the pointer rests on the chibi; an item plate is centred on the pointer. The drop target, its highlight, `tileHover` and the direction wheel's tile are the tile under the pointer. An item dropped on a tile equips the unit on that tile (`ui/gameLogic.js canPlace` / `dropIntent`; an empty tile says 请将装备拖拽至干员身上). A release over DOM covering the canvas (the shop bar) is `'outside'`: the piece goes back.
|
||||
- **Touch**: the field canvas cancels `touchend` (`app.js onTouchEnd`, non-passive). A tap's compatibility mouse events and click came after `touchend`, hit-tested at the finger again — on the underframe the tap had just opened over the unit's tile (clamped under the top bar for rows 11–12 at 844×390), which pressed 撤退 / 出售.
|
||||
- **Notched phones**: the shop bar keeps its place at the bottom edge instead of rising by the bottom safe-area inset (`css/devices.css .gm__hud > .shopbar`): raised by it the bar covered ~3/4 of bench pads 2–9 at 844×390 with the iPhone insets (23 % of those tiles left free, 71 % now — as without insets), and bench pieces are pressed and dropped on by their tile.
|
||||
- **Notched phones**: the shop bar keeps its place at the bottom edge instead of rising by the bottom safe-area inset (`css/devices.css .gm__hud > .shopbar`): raised by it the bar covered ~3/4 of bench pads 2–9 at 844×390 with the iPhone insets, and bench pieces are pressed and dropped on by their tile. (The v2.3 figures — 23 % of those tiles free before, 71 % after — are superseded by §19.8: the prep camera now clears the HUD, every bench pad 100 % free on phones.)
|
||||
- Removed: the pixel probe (`drawnAt`, `hoverProbeAt`), body shapes (`pickShape`, `PROBE_MARGIN`, `BODY_H`), `TOUCH_LIFT_TILES`, the drag `dropPoint` hook, `pieceDragOver`, `ui/gameLogic.js pickPieceAt` / `equipRetarget`. `UnitView.bounds()` is a plain body rectangle again (tooltips and `pieceScreenRect` only).
|
||||
- Tests: `test/render/pick.test.js`, `drag.test.js`, `unitview.test.js`, `test/ui/gameLogic.test.js`, `test/ui/playtest4.test.js` (shop bar); browser: `test/render/models.browser.test.js` (every tile of every board, high ground, bench and temp; battle allies and enemies incl. torso and head; the model under the pointer at every drag step; the wheel on the pointer's tile; equipment by tile; phone touch incl. the ghost click and the shop-bar release), `test/render/downelem.browser.test.js` (a knocked-out operator picked on its tile), `test/ui/playtest2.e2e.test.js` (the top-row tile under the bond strip; the notched phone's bench).
|
||||
|
||||
@@ -633,7 +637,7 @@ Thirteen reports after playing v2.2. Where each is handled: #1 picking should si
|
||||
|
||||
### 18.3 Knocked-out operators and element gauges (#9, #8) — `sim/Battle.js`, `sim/damage.js`, `render/interp.js`, `render/units.js`
|
||||
|
||||
- **Knocked out, not gone**: `Battle.snapshot()` lists `down: [[id, respawnAt, respawnTime, state]]` (§8.2) for operators knocked out (reason `'killed'`, not withdrawn), 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.
|
||||
- **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).
|
||||
- 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`.
|
||||
@@ -662,3 +666,138 @@ Thirteen reports after playing v2.2. Where each is handled: #1 picking should si
|
||||
### 18.7 The occasional giant UI flash (#13) — `render/fx.js pop`
|
||||
|
||||
The first in-battle layer gain of each bond (b.ev `'layer'`) pops that bond's icon over the field (`FxSystem.pop`). The icon is `PIXI.Texture.from(url)` — a 1×1 placeholder until its image has loaded — and the pop sized the sprite from it (46 / 1): the bond icon was drawn ~5000 px across the whole screen for the pop's 1.4 s, once per bond per page (73 official garrisons gain layers in battle, e.g. 送葬人's). Now it is sized (46 px along its longer side) only once its texture is valid, hidden until then. Found by sampling long real-server sessions every 80 ms (DOM boxes > 25 % of the viewport, images drawn > 300 px, Pixi objects > 25 % or showing a whole atlas page, CSS scale ≥ 2); every other large object seen is a designed full-screen layer. Tests: `test/render/fxpop.test.js`, `test/render/flash.browser.test.js`.
|
||||
|
||||
---
|
||||
|
||||
## 19. User playtest #5 (v2.4): hovering enemies, target order, blocking, 联防, elements, huge bosses, maps, phone HUD
|
||||
|
||||
Ten reports after playing v2.3 (绝境 on desktop, 标准 on an Android phone in landscape). Where each is handled: #1 operators that only attack ground units hit 近地悬浮 enemies → §19.1; #2 an operator lying knocked out (濒死) vanished when 联防 began instead of counting down and redeploying → §19.3; #3 element damage against the 海嗣 felt weak, audit every operator's and enemy's element amounts and gauges → §19.5 (enemies, the gauge pipeline) and §19.6 (the player side); #4 an enemy held between two operators walked off when the rear one died instead of the front one taking it over → §19.2; #5 the enemies' target order differed from the official → §19.1; #6 act2 m01's normal field had its two wind lanes but no blower machines, which showed in the boss round instead → §19.7; #7 the tiles framed by the 1×3 pipes on the boss map are deployable officially, check every map → §19.7; #8 the bottom-left settings icon was deformed → §19.8; #9 on a long phone screen the shop bar covered the bench → §19.8; #10 the huge bosses could only be hit around one tile → §19.4. A follow-up message added #11 至简 should cost 1 (his 特质 says 购买价格为1) → §19.9. Normative lines rewritten with it: §3 range test, §5.1 `carryState`, §5.5 (initial deployment, DP / down, blocking, operator attacks, enemy AI, damage), §6.1 UNITE, §7 bosses; §18.3's `isDown` now includes `FORCED_EXIT`, and §18.1's notched-phone figures are superseded by §19.8. The integration also applied the workstreams' cross-checks (hasHp in the pipeline, 脆弱 vs 元素伤害, element healing, 淤困, 塑心 S2, splash as a 中点判定); they are listed in the sections they belong to. Three QA passes over the integrated branch then left residuals, fixed in their sections: 掠海漂移体's model crawls after its drop (§19.1); blocked-first and out-of-range selection are melee-only (§19.2); a forced-out operator's timer takes the battle-start redeploy effects (§19.3); the gauge intake's 5 % floor (§19.5); the boss field's machines no longer stand under the bench in the normal views, and the fallback board draws the boss half in the boss prep (§19.7).
|
||||
|
||||
### 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 掠海漂移体.
|
||||
- **#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).
|
||||
- Tests: `test/sim/playtest5-targeting.test.js` (18; 13 fail on 731a01c), `test/content/enemies_bosses.test.js` (吉兆飞鳞), `test/match/bot.test.js` (a hovering wave counts as air), `test/render/forms.test.js` (the crawl clips); browser (opt-in) `test/render/forms.browser.test.js` (the real sim feeding the field view).
|
||||
|
||||
### 19.2 Blocking by contact radius, and the hand-over (#4) — `sim/constants.js BLOCK_RADIUS`, `Battle._checkBlock` / `blockedTargets`, `ai.js`
|
||||
|
||||
- **Cause**: an operator blocked only an enemy standing on its own tile (§5.5's old "|dx|,|dy| ≤ 0.5"). An enemy that passed a full front operator was blocked by the rear one, on the rear tile; when the rear operator died, the front one — which the enemy still overlapped — never took it.
|
||||
- **Official** (PRTS 游戏数据基础 §阻挡半径; 作战机制 "中点判定…案例:阻挡"): contact = the enemy's position within the blocker's radius of its centre, compared as squared distances — ground 0.70709997 (² 0.49999037), air (起飞 / blockFly vs flyers) 0.8944, devices (阻隔工事 / 障碍物) 0.4472.
|
||||
- **Now** (§5.5 Blocking): every tick, moving or not, an unblocked blockable enemy is blocked by the nearest operator in contact that has free capacity for its weight — the front operator takes over when the blocker dies, is withdrawn or is stunned (`stun` releases its enemies); an operator whose capacity frees up grabs an overlapping enemy; an enemy that finds no room walks on. A head-on enemy stops at contact, ≈ 0.71 tile from the blocker's centre, outside its tile (the small official deceleration is not modelled). A knocked-out or undeployed operator never blocks.
|
||||
- **Consequences, both official**: "可以选择且优先选择阻挡单位" (PRTS 选择器: the selector of units whose range is smaller than their block radius) — a **melee** blocker (data `position` MELEE: 要塞 / 领主 / 哨戒铁卫 included) 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.
|
||||
- [ASSUMED] the nearest blocker wins, ties by scan order.
|
||||
- Tests: `test/sim/playtest5_blocking.test.js` (16, incl. the user's case on act2 m01 with real operators, the ranged blocker and 银灰 as a melee-position ranged attacker), adjusted geometry in `kits_alt_t5` / `engine-requests` / `enemies_bosses`.
|
||||
|
||||
| Contact | Official (PRTS) | v2.3 | Now |
|
||||
|---|---|---|---|
|
||||
| ground operator | d² < 0.49999037 (r 0.7071) | the enemy's tile = the operator's tile | d² < 0.49999037 |
|
||||
| air (blockFly / 起飞) vs flyers | r 0.8944 | same tile | d² < 0.79995137 |
|
||||
| device (阻隔工事 / 障碍物) | r 0.4472 | same tile | d² < 0.2 |
|
||||
| blocker gone / capacity freed | contact re-checked continuously | only on the enemy's own tile | every tick, nearest free blocker |
|
||||
|
||||
### 19.3 Knocked-out operators in 联防 (#2) — `server/match/unite.js`, `sim/Battle.js`, `sim/constants.js FORCED_EXIT`, `render/app.js`, `render/units.js`
|
||||
|
||||
- **Cause**: `unite.js uniteBattleOpts` dropped every operator that `unitsEnd` reported `alive: false` — and its summons — from the 联防 input (an old [ASSUMED] rule). Every client and the server build the unite field from that input, so the operator was missing for the authority, the partners, observers and the `SP_COMBAT=server` fallback alike.
|
||||
- **Official** (PRTS 卫戍协议/帮助 §联防阶段): "部署完成后,将对应单位的生命比例、技力修改至与上一阶段结束时相同(召唤物仅修改技力,上一阶段为退场状态的干员强制退场)".
|
||||
- **Now**: the operator is fielded with `carryState: { down: true }` (§5.1); its summons are fielded as the board has them. `Battle.start` deploys everyone (the initial `deploy` hook fires), then before `battleStart` sets its HP to 0 (the end-of-phase ratio, as `kill()`) and withdraws it with removal reason `FORCED_EXIT` = `'forcedExit'`. `Battle.isDown` accepts that reason, so it is in b.snap `down` and `fieldMeta` with its ring, shows 0 HP on the live card, and redeploys by §5.5 (timer done, own tile free, DP ≥ cost) at full HP. The forced exit is no new knock-out: no `kill` hook, no `death` with reason `'killed'`, no `deaths` count — 深海 / 不屈 revives, 崇高牺牲 layers and selfdead garrisons fired in the own combat. The client's `'die'` with that reason goes straight to the held knocked-down pose (`UnitView.die(true)`: no fall, no burst, no sound).
|
||||
- [ASSUMED] the timer restarts at the operator's **full** redeploy time (70 s for most chess): the official setup carries only `hp` / `tech` per operator (research 09 §3 `HelpBattleInfo`). A short 联防 may therefore end before it comes back (a real-sim reproduction ended at 28 s; in the integration QA's real matches 43 % of 1,564 knocked-out operators were still counting when their 联防 ended). If the user wants the own combat's remainder continued ("继续转再起倒计时"), the fallback is small: carry `respawnLeft` in `unitsEnd` / `carryState`. Also [ASSUMED]: the forced exit happens before `battleStart`; the timer is re-read after it, so a redeploy-time effect that starts with the battle covers it (机变 征召 "所有干员的再部署时间-50%": 70 → 35 s, as for a later knock-out — it read 70 s before the integration QA), while 征召's own row check ("场上至少有一行存在3名干员") does not count it [ASSUMED]. Not covered: 召唤物仅修改技力 (summons enter 联防 fresh; `unitsEnd` lists operators only — carrying their SP would extend the client-reported result, `spec.js` digests and `fields.js` validation).
|
||||
- Tests: `test/match/unitedown.test.js` (both combat modes, both helper halves, the real sim), `test/sim/downelem.test.js` (incl. 征召), `test/render/downring.test.js`, `test/match/combat.test.js`; browser (opt-in) `test/render/unitedown.browser.test.js`.
|
||||
|
||||
### 19.4 Huge bosses: hit area, 自缚 (#10) — `tools/build-data.mjs HIT_AREAS`, `sim/body.js`, `content/bosses.js SELF_BOUND`, `render/pick.js`
|
||||
|
||||
- **Cause**: every range test took the enemy's position tile, and the data had no body size, so a boss at (3,10) was hit only by ranges covering (3,10).
|
||||
- **Official** (PRTS 天赋): "巨型单位:受击判定区域为长4.95、宽2.95的长方形,向上偏移1.0" (管·隐秘核心 also 向右偏移1.0; PRTS盟约记录 gives the season's 阿利斯泰尔 and “萨米的意志” the same area); PRTS 作战机制 "巨型BOSS单位的每一个占据的格子都可以让其本身通过格子判定". The same talent lines list 自缚 (PRTS 异常效果: 无法移动, not recognised as 束缚) and 无法被阻挡. 铳, 卢西恩 and the boss parts are regular units.
|
||||
- **Now**: enemies.json `hitArea {w, h, dx, dy}` (by prefabKey); `sim/body.js` is the one helper behind every range test on enemies (the engine tile index, `enemiesInKeys`, `unitsInGrid`, `enemiesInRadius`, nearest / farthest priority, kits, tokens, bonds, professions, `support.onKeys`). A huge enemy occupies every tile its rectangle overlaps — 5 × 3, rows 3–5 × cols 8–12 around (3,10): the whole fence-framed pipe block, so an operator on the 1×3 fence tiles (§19.7) hits it. Radius rules measure to the rectangle (0 inside) [ASSUMED: PRTS calls collider tests the most common]; **splash around a struck target is a 中点判定** (PRTS 作战机制 "中点判定…案例:阻挡,酒神1天赋的1.3溅射半径"): `enemiesInRadius(x, y, r, true)` counts every enemy by its position — the engine's profession splash and aftershocks and the kits' target-centred splash (焰影苇草's 灼痕 burst, 新约能天使 火力电台, 仇白 S1) — so a splash on an escort 0.53 tiles from the rectangle but 1.41 from the boss's centre no longer reaches the boss. Its position stays its 判定中心 (projectiles, its own attacks, splash centred on it, distance-scaled numbers). Integration pass: the reaperrange front line, 凛御银灰 S3's lateral line and the 香槟炸弹 tile trigger are body-aware too; movement, pathing, terrain, tile-entry tracking, placement heuristics and range keys taken from blocked enemies stay on the position (a huge enemy never moves and is never blocked).
|
||||
- **自缚**: the co-op 昆图斯 walked its official route (3,10) → (2,2) off the block and leaked at ≈ 280 game s. `SELF_BOUND` (exactly the 7 hitArea keys) gives them a persistent `noMove` + `unblockable` buff at spawn (the mirrored co-op copy too).
|
||||
- **Picking** (`pick.js AREA_PICK`): a press on the hit area or inside the drawn body box selects the boss, counted as 1 tile away, so an ally on the pressed tile or a nearer regular enemy wins.
|
||||
- [ASSUMED] 胄·隐秘核心 keeps the upward offset although its PRTS section omits it: enemy_database gives `enemy_9013_acstmk_2` the prefabKey `enemy_9013_acstmk` — one prefab, one collider — while the 管 copy that PRTS lists with a different offset has a prefab of its own (`enemy_9021_acduml_2`); ask at the next playtest if the hidden round feels off. The rectangle is fixed in map space (not mirrored).
|
||||
- Tests: `test/sim/playtest5_bossbody.test.js` (12, incl. the centre-point splash), `test/render/pick.test.js`, the Final Assault leak regression in `test/sim/battle.test.js`; browser (opt-in) `test/render/bossbody.browser.test.js`.
|
||||
|
||||
| Boss | Hit area (PRTS) | Tiles at its spawn | Talent |
|
||||
|---|---|---|---|
|
||||
| 假想敌:胄 (+ 隐秘核心 copy) | 4.95 × 2.95, up 1.0 (copy: [ASSUMED] same) | (3,10) → rows 3–5 × cols 8–12 | 自缚、不可阻挡 |
|
||||
| 假想敌:管 / 管·隐秘核心 | 4.95 × 2.95, up 1.0 / + right 1.0 | (3,10) / (3,9) → rows 3–5 × cols 8–12 | 自缚,无法被阻挡 |
|
||||
| 盐风主教昆图斯 | 4.95 × 2.95, up 1.0 | (3,10) → rows 3–5 × cols 8–12 | 自缚,无法被阻挡 (walked and leaked before) |
|
||||
| 阿利斯泰尔 / “萨米的意志” (卫戍 versions) | 4.95 × 2.95, up 1.0 | (3,10) → rows 3–5 × cols 8–12 | 自缚,无法被阻挡 |
|
||||
| 假想敌:铳, 卢西恩, the parts | none (point) | its tile | 铳 不可阻挡 only; 卢西恩 blockable |
|
||||
|
||||
### 19.5 Element gauges and enemy element damage (#3, enemy side and pipeline) — `sim/damage.js`, `content/enemies.js`, `content/bosses.js`
|
||||
|
||||
- **Verdict: official behaviour, not a remake bug.** Every 海嗣 has 损伤抵抗 (`epResistance`) 0 and 元素抗性 (`epDamageResistance`) 0 (in data/enemies.json only 转译基底·α has 损伤抵抗 10); gauges are 1000 (leaders 2000) and never decay; no 海嗣 talent changes element intake; every enemy element attacker deals ATK × its official ratio per hit × the round's ATK multiplier (47 checked). Boss levels override some values, which the runtime applies: 卢西恩 ATK 700 (solo 600) and 不祥幻影 200, × 0.12 per normal hit (skill 0.2) in boss_5 (level_act2autochess_h07_05(_s)); 盐风主教昆图斯 480 (solo 380) in h07_04. A blocked 底海滑动者 at 险境 R5 bursts 神经 on its 25th hit, ≈ 50 game s. "Weak" also has a player-side part — §19.6.
|
||||
- **Official rules** (PRTS 元素 / 游戏数据基础 / 伤害分类; gamedata_const termDescriptionDict): gauge gain = 损伤值 × max(5 %, 1 − 损伤抵抗 %) (PRTS 游戏数据基础: DMG_e "目标受到元素损伤时也可以使用该公式计算,只需要将 D 值改为目标的损伤抵抗即可"), then the 元素损伤倍率 effects; 元素伤害 = max(A(1 − 元素抗性 %), 5 %A); 元素脆弱 ("受到的元素伤害提升") raises 元素伤害 only; 脆弱 ("受到的物理、法术、真实伤害提升") never touches 元素伤害; bursts are 无来源 ("元素爆发通常造成无来源的伤害"; 无来源 = "无法被追溯伤害来源", yet the kill is traced).
|
||||
- **Fixed (W3b)**: 元素脆弱 is the mod `elementalTakenMul` (元素伤害 only; it used to share the gauge rate `elemTakenMul`, so 炎佑's 20 % sped up fills instead of raising bursts); 元素抗性 reduces `'elemental'` with the 5 % floor (`expectedFinal` mirrors it); bursts carry DamageInfo `sourceless` — no damage-dealt multiplier or penetration of the filler, and the `hit` / `damaged` / `fatal` hooks see `source: null` plus `credit` (stats, kill and boss-pool share keep the filler; a real 深巡 bursting a 海嗣 dealt 10500 instead of 7000, 精准狙击镜 made a 侵蚀 burst 6500 instead of 5000); target-side reactions with no source condition read `source || credit` (sharing: 圣杯, 盲信之誓, 余音; 抵挡: 星熊 战术装甲, 拉普兰德 日晷). The enemies' 凋亡 虚弱 is 50 % × remaining ÷ 15 s each tick (it ran 1 s ahead); 麻痹免疫 is honoured (假想敌:铳, the 胄 parts); 淤困's host burst spreads 1000 (was 780); 灼藤's first attack covers the 3×3; 节日爵士乐手 channels on one locked target (20 % arts + 10 % burn every 0.5 s, 21 ticks, ends on 沉默, no re-cast mid-channel). Side effect: 隐德来希's 心烛 cancels bursts on a candle ("只受隐德来希攻击的影响").
|
||||
- **Integration pass**: `damage.js hasHp` (alive, HP > 0; a boss: its pool) is the one predicate — `applyElement` refuses a target with no HP left, so no rider in a killing blow's `damaged` hook can burst a corpse (the content copies in tier5 / tier6 / items / tokens now import it); `'elemental'` damage skips `dmgTakenMul` (脆弱 and the other "受到的伤害±" effects [ASSUMED: they share that multiplier]) — only 元素脆弱 scales it; element healing (`reduceElement`) lowers **every** element type by the amount on its own (PRTS 菲莱 备注: 清除元素损伤 = "一次等同于自身最大元素值的全类型元素损伤治疗"; it healed the amount across all types in total); 淤困's "受到的元素损伤提高至130%" is an `elementHit` multiplier at priority 20 like every operator-side "受到的元素损伤±", before a 损伤屏障 (it was the `elemTakenMul` mod, applied after the barrier).
|
||||
- **Verified unchanged**: the burst effects on both sides; cooldowns 10 / 10 / 15 s (enemies' 侵蚀 8 s), locking then resetting every gauge; leaders take no 侵蚀 in the Final Assault (PRTS 盟约记录); 炎佑's aura = 20 % 元素脆弱 within 1.5. **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).
|
||||
- Tests: `test/sim/playtest5_elements.test.js` (18), `test/content/playtest5_followups.test.js` (pipeline guard, 脆弱, element healing), `combat.test.js`, `test/content/{kits_t5,tokens_devices,playtest4_summons,enemies_bosses}`.
|
||||
|
||||
| 海嗣 (user's case) | Official ratio | Per hit / s at base ATK | Hits to a 1000 gauge | Time |
|
||||
|---|---|---|---|---|
|
||||
| 底海滑动者 (ATK 280, BAT 2.0) | 15 % ATK 神经 | 42 | 24 | 48 game s |
|
||||
| 富营养的滑动者 (360) | 15 % 神经 | 54 | 19 | 38 game s |
|
||||
| 骨海漂流体 (250) | 20 % 侵蚀 | 50 | 20 | 40 game s |
|
||||
| 掠海漂移体 (500, BAT 4) | 50 % 侵蚀 | 250 | 4 | 16 game s |
|
||||
| 元核孽生者 (400, season) | 5 %/s 神经 in r 2.5 (season override; PRTS's base page: ATK 500, 10 %) | 20 / s | — | 50 s |
|
||||
| 深溟巢涌者 (140) | 5 % ATK 神经 per damage output | ≈ 7 / s | — | ≈ 143 s |
|
||||
|
||||
### 19.6 Element damage of the player side (#3, operator side) — `sim/content/kits/tier{2,3,5,6}.js`, `tokens.js`, `items/battle.js`
|
||||
|
||||
- **Audit**: only these deal 元素损伤 from the player side in 下半 — 盟约·辅助干员 (band 优等生), 菲莱, 烛煌, 妮芙 (T5 / hidden T6), 余, 塑心, 炎佑 (炎 ×6), 灼燃维式重锤 (+ 蒸汽之心); no bond, band, garrison, 机变 card or device deals any. Every number matched its blackboard (character_table at the chess status, battle_equip_table modules); the discrepancies were form, trigger and scope. "N%攻击力的…损伤" = N × ATK, "伤害N%的…" = N × the damage just dealt.
|
||||
- **The fix that matters most**: 盟约·辅助干员's 迭代元素 used to pick the "first element not bursting" — a burst locks every gauge, so she only ever filled 神经. Now every damage she deals (HP > 0; PRTS corrects the timing to "造成伤害时") attaches 18 % ATK of 神经 → 灼燃 → 凋亡 (`PITHST_ELEMENTS`); the element applied first bursts, so alone she bursts 神经 and in a team completes the 灼燃 / 凋亡 gauges 余 / 塑心 build — in the reviewer's runs the team's first 灼燃 / 凋亡 burst came ≈ 40 % sooner (9.0 s vs 16.0 s; 5.0 s vs 8.2 s). Teams without her are unchanged. [ASSUMED] the three apply together (the reading of the text).
|
||||
- **Killing blows**: element riders inside `damaged` hooks run before `battle.kill`, while the target is `alive` at 0 HP; a lethal hit used to burst the corpse (a second fatal, 烛煌 熔点引爆's heal and 350 % hit, 妮芙's stack). The riders check HP left and, since the integration, `applyElement` refuses it (§19.5).
|
||||
- **塑心 S2 (integration)**: PRTS "受影响的干员即将造成伤害时,因该效果造成的凋亡损伤生效于当次触发的伤害之前,该造成的凋亡损伤的来源始终为塑心" — the rider is a late `hit` handler, so the 凋亡 lands (and may burst) before the damage, a killing blow included; a hit dodged or absorbed after that still carried it [ASSUMED]. **纯烬 氤氲** is keyed per source (`agoat2:mist:<id>`): two 纯烬 keep their own stacks, ATK and heal source.
|
||||
- Tests: `test/content/playtest5_elements.test.js` (9), `test/content/playtest5_followups.test.js` (塑心 S2 order, 氤氲 per source), `kits_t1t2.test.js` (1_15, 哈洛德 with 侵蚀), `kits_t3.test.js` (菲莱).
|
||||
|
||||
| Source (normal / elite) | Official | v2.3 | Now |
|
||||
|---|---|---|---|
|
||||
| 盟约·辅助干员 迭代元素 | 18 % ATK each of 神经(优先)/ 灼燃 / 凋亡 per damage dealt; RIT-X 21.24 % vs 精英 / 领袖, talent only | per attack hit (dodged too), 神经 only | **fixed**: all three per damage > 0 |
|
||||
| 菲莱 S2 冥河诅咒 | when attacked (cd 2 s): 110/140 % ATK arts + 25 % ATK 凋亡 to ground enemies in x-4 (3×3) | radius 1.5 | **fixed** (3×3, body-aware, 近地悬浮 = air) |
|
||||
| 菲莱 PRP-X | "阻挡敌人时,自身造成的元素损伤提升15%" on every fill she deals | S2 blast only | **fixed** (all fills, a hammer too) |
|
||||
| 受到的元素损伤降低: 菲莱 神河谕使 / 哈洛德 我即军营 / 纯烬 火山灰疗愈 | −10 % (+2 SP on 凋亡 even for 0) / −12/15 % for allies over half / −12/14 % (×talent_scale in S3); before the 损伤屏障 | the 元素脆弱 multiplier (also cut 元素伤害) | **fixed**: `elementHit` at priority 20 |
|
||||
| 哈洛德 trait / T1 | "元素损伤最严重" and the over-half checks | 侵蚀 ignored | **fixed** (侵蚀 counts) |
|
||||
| 纯烬 氤氲 | per stack / s: HP 10 % ATK, element 10 % × trait ratio (5/6 % ATK), cached ATK | element 10 % ATK (2×) | **fixed**; per source (integration) |
|
||||
| 塑心 S2 安魂的弥撒 | self + top-ATK ally: each damage + 15/20 % of her ATK 凋亡, before the damage | after the damage, skipped on a killing blow | **fixed** (integration) |
|
||||
| 塑心 S1 / T1 / T2 / RIT-X / RIT-Y | 85/95 % ATK 凋亡; 10 % ATK/s; 精神逆构 ×1.2 (elite ×1.33 + 5 % 元素脆弱 in the burst); ×1.18 vs 精英 / 领袖; RIT-Y 音乐家的旅程: field-wide ×1.2 + 150 元素伤害/s during the burst, 10 % 元素脆弱 in range | same | OK |
|
||||
| 余 T1 / S3 / S1 / T2 / PRP-X | 40 % ATK arts + 12 % ATK 灼燃/s on blocked enemies; 火墙 6/7 %; 30/40 % ATK on the attacker; T2 1.5 % max HP of HP and element per s with ≥ 4 operators on the field (elite ≥ 2; elite ≥ 4 also +14 % arts vs 灼燃 bursts); ×1.15 while blocking | same | OK |
|
||||
| 烛煌 S1 / S2 / S3 / T1 / PRI-X | 30 % of the damage as 灼燃; +60/70 % ATK 元素伤害 on burst targets; 熔点引爆 350 % + 12 % heal; ×1.1 vs burst targets | same | OK |
|
||||
| 妮芙 S2 / T1 / PRI-Y | 18/22 % of her arts damage as 凋亡; 失魂 40 % ATK 元素伤害/s (PRI-Y 烦恼问诊匣: every 0.7 s, SP +0.2/s) | same | OK |
|
||||
| 炎佑 / 灼燃维式重锤 | 20 % ATK 灼燃 per damage, 20 % 元素脆弱 aura r 1.5 / 10 % of the arts damage (蒸汽之心: own hammer ×2) | same | OK (元素脆弱 now raises bursts, §19.5) |
|
||||
| any rider on a killing blow | a dead unit takes no element | burst the corpse | **fixed** (hasHp + pipeline guard) |
|
||||
|
||||
### 19.7 Maps: blower machines, boss-field deployment (#6, #7) — `render/board3d/layout.js`, `render/tiles.js`, `render/app.js`, `server/match/board.js`, `Match.startRound` / `bossGroupOf` / `deployFieldOf`, `ui/gameLogic.js`, `ui/fallbackField.js`
|
||||
|
||||
- **#6 cause**: the 3D board builds only the current view's area, and for the normal / 联防 views it stopped at row 12. act2 m01's own blowers (#001 / #002, the partner's #101 / #102) stand on the separator row 13 facing DOWN into rows 12–10 (level_act2autochess_m01 predefines: (13, 5/9/13/17) and (6, 5/9/11/15), all DOWN, none hidden), so they were never built; the Pixi airflow streaks and the sim were right. **Now** the normal / 联防 field rects are rows 6–13 and one device rule serves both boards: every active device standing on a built tile is drawn (3D `stageDevices`, 2D `_stageDevices` minus its faded margin), the boss field's (rows ≤ 6, `BOSS_WALL_ROW`) only in the views that build the boss field. Normal / prep: (13,5) and (13,9) above the lanes; 联防 adds (13,13) and (13,17); 最终攻势 / 隐秘核心: (6,5), (6,9), (6,11), (6,15).
|
||||
- **#6, the boss field's machines under the bench** (integration QA): the boss field's row-6 blowers (6,5) / (6,9) stood on the wall under the bench in the normal / 联防 views (v2.3 and the first integration), visible in the user's R7 screenshot between the bench and the shop bar. An official player never sees that wall in a normal round: the official shop camera (`left_shop_camera_param`, = the remake's prep camera at 16:9) puts row 6 at ≈ 818–941 px of 1080, under the shop bar (top 813 px); the normal battle camera shows nothing below row 8; and the shop-collapsed prep view (`left_prepare_camera_param`; PRTS's in-game capture of act2 m01 at 1280×720) ends at row 6's far edge — the remake's shop bar leaves the left part of that band open, and its prep camera stays when the shop collapses. So the boss field's devices are drawn with the boss field only; whichever way "反而在关底吹风机的贴图又出现了" is read (the boss round shows its machines — still true — or they showed under the bench — no more), the normal views now match the official. Row 6 itself stays built (the wall under the bench). Unchanged (cosmetic, pre-existing): the official normal view also shows the partner's half beside the own field (its row-13 machine (13,13) at the top right); the remake builds the own half only.
|
||||
- **#7 cause**: every legality check of a boss round's prep read the normal field (board (10–12, 8) = tile_forbidden) — `board.js buildDeployMap`, the client's `canPlace` / highlights, the bot and the invariants — while the pieces stand on the player's half of the boss field, where (3–5, 8) and (3–5, 12) are tile_fence_bound (low ground, buildable ALL, fly-only passable). **Now** `buildDeployMap(stage, { field })` takes `'normal' | 'bossL' | 'bossR'` (board row = boss-field row + `BOARD_ROWS_ABOVE_BOSS` 7; the right-hand player mirrored col c → 20 − c, research 09 §1.2 ConvertChessPositionInfoToBossMap); `Match.bossGroupOf` is the one pairing lookup (`deployFieldOf`, `nextEnemiesFor`, the bot); `Match.startRound` plans the round's enemies before `PlayerState.startRound`, so R14 → R15 re-checks boss-half pieces on the boss field (they used to be withdrawn with 地形变化; rngWaves is used only there, no random stream changes); a quit before the boss fight re-pairs and re-checks at once; the client's `gameLogic.deployFieldOf` feeds `placementContext({ field })`. Operators on the fence tiles hit the huge boss (§19.4); ground enemies cannot walk onto those tiles.
|
||||
- **Audit** (all 11 stages × normal / 联防 / boss L / boss R, server = client = builder deployTiles = the raw level files): the only wrong tiles were act2 m01's six boss tiles; on every stage normal-legal ⊆ boss-legal and boss L = boss R, also under every map card, so R13 → R14 and R14 → R15 move nothing. act2 m01 now deploys 13 melee + 3 ranged-only tiles on the normal board and 16 + 3 on each boss half. Devices: 3D = 2D for every stage × view; act1 m05's 特制水上平台 are ×64 (16 per normal half, 32 on the boss field). The DOM fallback board (`ui/fallbackField.js`, no WebGL / `?render=fallback`) draws the player's boss half in the boss prep too — `screens/game.js` now hands it the `bossPrep` camera, which it maps through `gameLogic.fieldTile` on the own board's layout (integration QA: it drew the normal field's walls under legal highlights). Limitations: `Match.unitStats` previews on the normal field (stats only); 射击台 stays ranged-only [ASSUMED].
|
||||
- Tests: `test/match/playtest5-deploy.test.js` (incl. boss → boss R14 → R15 and the fallback board), `test/render/playtest5-maps.test.js` (incl. the boss field's devices only in the boss views), `test/ui/playtest2.test.js`; browser (opt-in) `test/render/board3d.browser.test.js` (area key '6,13,0,10').
|
||||
|
||||
### 19.8 Settings gear; the shop bar over the bench on phones (#8, #9) — `ui/gameComponents.js`, `render/projection.js`, `ui/fieldHost.js`, `render/app.js`, `css/devices.css`
|
||||
|
||||
- **#8**: the ⚙ was a hand-written SVG path with uneven teeth on a rim that was not round (the extracted autochess UI has no gear sprite; 交流 is the official `emoji_btn`, 📖 / ⛶ were clean). `gearPath()` now generates a regular 8-tooth gear (tip r 10, root r 7.4, hole r 3.3); on phones the corner glyphs are `max(.28rem, 17px)`, half their 34 px buttons (were 11 px).
|
||||
- **#9, the user's viewport**: the screenshot is 2772×1272 px with the page right of a 141 px cutout band; the page's own markers give DPR ≈ 3.48, so **756×366 CSS px (≈ 2.07:1)** — the mock rendered there puts the exit and 准备就绪 buttons on the screenshot's pixels within ±1 px.
|
||||
- **Cause**: the official shop camera (`left_shop_camera_param`, boss `*_boss_shop_*`) fits the prep area exactly at 16:9 (bench near edge 811 px vs the shop bar at 813 px at 1920×1080), while the remake's HUD is in rem with a 40 px floor (css/theme.css), so below 432 CSS px of height it is relatively taller: v2.3 left the worst bench pad 54 % free at 756×366 (Final Assault prep 49 %), 71 % at 844×390, 49 % at 800×360.
|
||||
- **Now**: the view receives the HUD bands, `hudBands(kind, size)` (top = the HUD layer's safe-area top + 2.16rem, the bond strip; bottom = 2.64rem + 3 px, the shop bar; the rem values copy the CSS, which a test checks), and `presetCamera('prep' | 'bossPrep', …, { hud })` runs `clearHud` on the band from the bench's near edge to the back row: the official camera when it fits (0.5 px tolerance), else the smallest pan, else a zoom-out about the centre — a 2D pan / zoom of the image, so picking, the three.js camera and flights stay consistent. Every desktop prep camera is unchanged; the bar stays on the bottom edge (§18.1); the collapsed shop keeps the camera (nothing jumps) — the official client moves to its closer `left_prepare_camera_param` there, so on a phone the remake's collapsed prep board stays at the clearing zoom (×0.844 at 756×366; integration QA, an optional follow-up: `shop: !collapsed` for the own prep camera with a 0 bottom band). Accepted: an armed shop card rises 4 px above the bar on a phone and covers ≈ 3 px of the pads' near corners (≈ 11 px under the official camera at 1920×1080). [ASSUMED] the bond strip's bottom (2.16rem, measured 2.14–2.15rem); the zoom anchor.
|
||||
- Tests: `test/render/projection.test.js` (HUD clearance suite), `test/ui/playtest5-ui.test.js`; browser (opt-in, `SP_E2E=1`) `test/ui/playtest5-ui.e2e.test.js` (per-tile coverage at 16 viewports, the armed card, the gear).
|
||||
|
||||
| Viewport (CSS px) | Worst bench pad free, v2.3 → now (prep / Final Assault prep) | Camera now | Tile px |
|
||||
|---|---|---|---|
|
||||
| **756×366 (the user's Android)** | 54 → 100 % / 49 → 100 % | ×0.844 | 41.5 → 35 |
|
||||
| 798×366 + 41 px inset (same phone over the cutout) | 53 → 100 % / 49 → 100 % | ×0.844 | 41.5 → 35 |
|
||||
| 844×390 (19.5:9, with or without notch) | 71 → 100 % / 66 → 100 % | ×0.905 | 44.2 → 40 |
|
||||
| 800×360 (20:9) | 49 → 100 % / 45 → 100 % | ×0.828 | 40.8 → 33.8 |
|
||||
| 915×412 / 914×411 / 960×411 | 83 → 100 % / 83 → 100 % | ×0.95 | 46.7 → 44.5 |
|
||||
| 1920×1080, 1680×1050, 1560×1040, 2560×1080 | 100 % | official | unchanged |
|
||||
| 1280×720 | 100 % / 96 → 100 % | official (Final Assault prep: pan up ≤ 2.8 px) | unchanged |
|
||||
|
||||
### 19.9 至简's price (#11) — `sim/content/garrisons/meta.js SERVER_CHESS_PRICE`, `server/match/effectsMeta.js dispatch`
|
||||
|
||||
- **What the user saw**: 至简 (Ⅲ) cost 2 资金; the user remembered 1. His 特质 garrison_13 reads 购买价格为1, but `bb.price` is 2, and the handler set the price to `bb.price` ("blackboards are authoritative").
|
||||
- **Official**: two chess carry this 特质 (`effectType` SERVER_CHESS_PRICE) and both texts say 购买价格为1 — 至简 (Ⅲ, tier price 3, `price` 2) and 红豆 (Ⅰ, tier price 2, `price` 1, hidden in 下半). Only "`bb.price` is the discount off the tier price" gives the text's 1 for both (3 − 2, 2 − 1).
|
||||
- **Change**: the handler lowers the price by `bb.price` (`ctx.modifyPrice(-p)`), and `EffectDispatcher.dispatch` runs the priced chess's own 特质 first on `onPrice` (before globals / band / bonds), so 远见's discount (−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).
|
||||
+30
-8
@@ -11,7 +11,8 @@ server/match/
|
||||
gamedata.js typed, defaulted view of data/*.json (config tunables with research defaults) + the balance layer
|
||||
(data/tuning.json, §3.1)
|
||||
pool.js SharedPool (copies per base chess, across players), per-match bans, copy-weighted rolls
|
||||
board.js placement legality from the stage legend, slot helpers, deployment order
|
||||
board.js placement legality from the stage legend on the deploy field (own board / boss half), slot helpers,
|
||||
deployment order
|
||||
bondsMeta.js bond counting modes, tiers, 调和 / 独行 / 助力 / 绝技, layers
|
||||
effectsMeta.js MetaRegistry + EffectDispatcher + the handler ctx (this document, §2)
|
||||
builtinMeta.js engine built-ins (consume-on-equip items, Arts, EffectRefs used by 机变 defaults)
|
||||
@@ -51,6 +52,20 @@ LOBBY → INFO_CHECK (co-op 25 s; solo and single-human matches untimed; all hum
|
||||
→ RESULT (m.result to every human, then onEnd once)
|
||||
```
|
||||
|
||||
**Deploy field (user playtest #5 item 7).** From the ROUND_START of a boss round (最终攻势 / 隐秘核心) a player deploys on
|
||||
its half of the boss field (`Match.deployFieldOf` → `'bossL'`, or `'bossR'` for the second player of a seat pair — the
|
||||
same pairing as the fields, `bossWaves` / `Match.bossGroupOf`, planned in `startRound` BEFORE the players' round start
|
||||
so R14 → R15 never re-checks a boss-half board against the normal field, and re-paired — with an immediate re-check —
|
||||
when a teammate quits before the fight): `board.js buildDeployMap`
|
||||
reads the stage tile (r − 7, c) / mirrored (r − 7, 20 − c) under every board tile (r, c) (official
|
||||
ConvertChessPositionInfoToBossMap, player_map_ud_offset 7) and that half's devices under the player's overrides. Board
|
||||
coordinates never change (g.move, the battle input). A change of the deploy field re-checks the board like a terrain
|
||||
change (`PlayerState.deployMap` → `_evictIllegal`; the read-only checker `invariants.js` uses the pure
|
||||
`Match.deployMapFor`); on the data's 11 stages everything legal on the normal board is legal on both boss halves (and
|
||||
the two halves have the same classes), and the boss halves add act2 m01's fenced tiles (board (10–12, 8)), which stay
|
||||
legal from R14 into R15. The client mirrors it
|
||||
(`ui/gameLogic.js deployFieldOf` / `placementContext({ field })`); the bot plans on the same map.
|
||||
|
||||
| Timer (real s, × `opts.timerScale`) | Value |
|
||||
|---|---|
|
||||
| INFO_CHECK | `config.timers.infoCheck` 25 |
|
||||
@@ -215,8 +230,9 @@ Every handler method is `(ctx, ev)`; `ev` is shared by all handlers of one dispa
|
||||
| `onLayers` | bond layers were added (prep or battle gains) | `{ bondId, from, to, reason }` (milestones: 维多利亚 25, 远见 10, 奇迹 100 …) |
|
||||
|
||||
Dispatch order per player: `global` → `band` → `bond` (data order) → garrisons (board in deployment order, then hand)
|
||||
→ equipped items → EffectRefs (insertion order). Every call is isolated with try/catch (the error is logged once and
|
||||
counted in `match.dispatcher.errors`); nested dispatches are capped at depth 6.
|
||||
→ equipped items → EffectRefs (insertion order). `onPrice` runs the priced chess's own 特质 first (购买价格为N sets the
|
||||
price that 远见's discount and the strategies' caps then act on). Every call is isolated with try/catch (the error is
|
||||
logged once and counted in `match.dispatcher.errors`); nested dispatches are capped at depth 6.
|
||||
|
||||
### 2.3 Garrisons (特质)
|
||||
The dispatcher calls `handler[hook] ?? handler.run` only on the hook of the garrison's `eventType` (a handler may widen
|
||||
@@ -229,7 +245,7 @@ that per garrison with `garrisonHooks(garrison) → hook[]`, e.g. "<进入休整
|
||||
| `SERVER_PREP_FIN` 休整期结束时 | `onPrepEnd` | same |
|
||||
| `SERVER_REFRESH_SHOP` 刷新时 | `onRefresh` | same |
|
||||
| `SERVER_CHESS_SOLD` 售出时 | `onSold` | the sold piece |
|
||||
| `SERVER_PRICE` 购买价格 | `onPrice` | the chess in the priced slot (`ctx.source.where === 'shop'`) |
|
||||
| `SERVER_PRICE` 购买价格 | `onPrice` (first) | the chess in the priced slot (`ctx.source.where === 'shop'`); SERVER_CHESS_PRICE `bb.price` is the discount off the tier price (至简 3 − 2 = 1, 红豆 2 − 1 = 1: both texts say 购买价格为1) |
|
||||
| `IN_BATTLE` | — | battle side (server/sim/content/garrisons.js `install`) |
|
||||
|
||||
```js
|
||||
@@ -423,9 +439,12 @@ chosen by most units on the field (downed included) > an active bond > most stan
|
||||
units > active bond > Σ active layers > standing > seat (LP plays no part), the first one on the right-hand field
|
||||
(colOffset +8, where escaped_multi enters), the other colOffset 0; the escaped template of that size routes the leaked
|
||||
enemies by slot class; helpers' operators carry
|
||||
`{ hpPct, sp, skillActive }` from `unitsEnd` ("阵地以其当前状态"); an operator dead at the end of the helper's own combat
|
||||
stays out of the 联防 battle with its summons [ASSUMED: the sim has no "start undeployed with a running redeploy timer"
|
||||
input]; `flags.layerGainsEnabled = false`; time limit = the round's combat limit.
|
||||
`{ hpPct, sp, skillActive }` from `unitsEnd` ("阵地以其当前状态"); an operator knocked out at the end of the helper's own
|
||||
combat carries `{ down: true }` (PRTS 卫戍协议/帮助: "部署完成后…上一阶段为退场状态的干员强制退场"): deployed, then forced out
|
||||
at once, it lies on its tile with the redeploy ring and redeploys like after any knock-out (docs/SIM.md §1.1; user
|
||||
playtest #5 item 2 — it used to stay out and vanish); its timer is its full redeploy time [ASSUMED: the official setup
|
||||
carries only hp / tech per operator], with the redeploy-time effects that start with the battle (机变 征召); summons are
|
||||
fielded as the board has them; `flags.layerGainsEnabled = false`; time limit = the round's combat limit.
|
||||
Every enemy still alive at the end (leaked again, or never spawned before the limit) costs its **source** player 1 LP.
|
||||
A client-run 联防 result may bill a survivor only to a leaker who sent that enemy in — a split / summon only to a leaker
|
||||
who sent in its parent, ≤ the parents' data offspring count (磨砻 2, 烹泉 4 …; fields.js offspringPerParent).
|
||||
@@ -517,6 +536,9 @@ receiver only), plus CUSTOM texts (eliminations, 联防, hidden core).
|
||||
* 中途退出 = elimination at once (research); a quitter's operators already fighting in a shared field (联防, boss
|
||||
field) finish that battle, its LP already merged into the team LP stays there.
|
||||
* After the LP merge (Final Assault) the per-player LP shown is a share of the team LP ∝ the LP each player brought in.
|
||||
* A 联防 helper's operators dead at the end of its own combat do not take part in the 联防 battle.
|
||||
* A 联防 helper's operator knocked out at the end of its own combat is deployed and forced out at once (PRTS 强制退场,
|
||||
§4) with HP 0, and redeploys after its full redeploy time: no timer carry (the official setup carries only hp / tech
|
||||
per operator), forced out before `battleStart` (its timer re-read after it, so 征召's −50 % applies; 征召's row check
|
||||
does not count it), no knock-out hooks (`kill`, 'killed' deaths) a second time.
|
||||
* A merge completed after the prep (SETTLE effects) keeps its reward offer for the next prep; its elite goes to the hand,
|
||||
overflowing into temp like a prep merge's (temp pieces that arrive after the prep wait through the next prep).
|
||||
+7
-4
@@ -18,7 +18,7 @@
|
||||
| 选择策略 | 40 名策略(分队长)决定**初始目标生命值(20–45)**和一条永久规则。同盟:随机顺序轮流选,**每人 30 秒**,右上角和「决策中」显示同一个倒计时;AI 队友立即选完;队友已选的不能重复,每人可**跳过一次**(排到最后);超时自动选你**当前选中**的策略(已被队友选走则选华法琳)。独立:自由选择、不限时;同盟模拟只有你一名玩家时也不限时。 |
|
||||
| 机变 | 部分回合开始前出现:同盟共 6 张卡轮流各取 1 张(首位 30 秒,其余 16 秒;只有你一名玩家时不限时),独立 3 选 1。卡片包括资金、干员、盟约层数、战术特训(悬赏敌人)等。选卡要**点两次**:第一次只选中(金框 +「确认选择 · 再次点击」),再点同一张或点右上的「确认选择」才生效;点别的卡换选,按 Esc 或点空白处取消。 |
|
||||
| 休整期 | 购买、摆阵、配装备、冻结 / 刷新、升级调度中心;同盟有倒计时(65–215 秒,随回合增加);独立模拟、以及只有你一名玩家(只带 AI 队友)的同盟模拟不限时。所有人「准备就绪」即提前开战,全员就绪前可取消。 |
|
||||
| 作战 | 每人在自己的战场各自作战(战斗在你的浏览器里模拟,2 倍速)。干员按**从上到下、从左到右**的顺序自动部署并消耗部署费用(初始 10,+1/秒)。时限 45–115 秒(实际秒数,即屏幕上的倒计时),时间到仍存活的敌人算漏怪。有敌人进入蓝门时,顶栏的目标生命值会**立即**显示扣除后的数值(红字 + 「−N」,每回合至多 10 点),结算时确定;联防阶段标注「联防中」,队友拦下的部分最后不扣。左侧队伍栏同样显示队友的待扣生命值。被击倒的干员**留在原地**(倒地姿势、略微变灰),头上的圆圈是再部署倒计时:绿色圆环 + 秒数;变成黄色「DP」表示部署费用不够,红色「!」表示原位被占。血条右侧的圆形图标是**元素损伤**:干员为带图标的彩色圆盘,敌人为小号纯色圆盘,白色圆环是剩余元素值;爆条后进入爆发冷却(灼燃 / 神经 / 侵蚀 10 秒,敌方侵蚀 8 秒,凋亡 15 秒),期间所有元素都不会累积也无法被治疗,结束后清零。 |
|
||||
| 作战 | 每人在自己的战场各自作战(战斗在你的浏览器里模拟,2 倍速)。干员按**从上到下、从左到右**的顺序自动部署并消耗部署费用(初始 10,+1/秒)。时限 45–115 秒(实际秒数,即屏幕上的倒计时),时间到仍存活的敌人算漏怪。有敌人进入蓝门时,顶栏的目标生命值会**立即**显示扣除后的数值(红字 + 「−N」,每回合至多 10 点),结算时确定;联防阶段标注「联防中」,队友拦下的部分最后不扣。左侧队伍栏同样显示队友的待扣生命值。被击倒的干员**留在原地**(倒地姿势、略微变灰),头上的圆圈是再部署倒计时:绿色圆环 + 秒数;变成黄色「DP」表示部署费用不够,红色「!」表示原位被占。血条右侧的圆形图标是**元素损伤**:干员为带图标的彩色圆盘,敌人为小号纯色圆盘,白色圆环是剩余元素值;爆条后进入爆发冷却(灼燃 / 神经 / 侵蚀 10 秒,敌方侵蚀 8 秒,凋亡 15 秒),期间所有元素都不会累积也无法被治疗,结束后清零。元素爆发造成的是**无来源伤害**,不吃干员的增伤、穿透和装备加成(例如深巡打海嗣时灼燃爆发固定 7000),击杀仍记在爆条的干员名下;元素脆弱只提高元素伤害,不加快元素条;脆弱不影响元素伤害。已被击杀的敌人不会再被附加元素损伤。 |
|
||||
| 联防 | 见第 5 节。 |
|
||||
| 结算 | 扣 LP、发放悬赏资金、结算层数;LP ≤ 0 的玩家淘汰(其干员回到公共卡池)。 |
|
||||
|
||||
@@ -66,12 +66,13 @@
|
||||
- 拖动时模型一直在指针 / 手指下(脚在指针下方约半格,鼠标和触屏一样),**指针所在的格子**就是落点并高亮;装备拖到哪个格子,就配发给那个格子上的干员;在商店栏上松手则放回原位。
|
||||
- 点击干员显示攻击范围和底部按钮(「撤退」回到整备区、「出售 +1」);右键 / 长按查看详细信息:头像旁是盟约,下面第一项就是干员的**特质**(如「休整期结束时 …」),之后是特性、属性、攻击范围、技能、模组、装备、天赋。属性是**实时数值**:作战中每秒刷新 4 次(当前生命、攻击、防御、法抗、攻击间隔、阻挡),休整期显示棋盘上的干员**开战时**的数值(已算入装备、盟约层数、特质、策略与机变效果);高于基础值为绿色、低于为红色并标出差值(攻击间隔变短算增益),鼠标悬停可看基础值。
|
||||
- 能在休整期摆放的召唤物只有天赋召唤物(海嗣、狼群、流形);赫默的医疗无人机、巫恋的诅咒娃娃这类技能召唤物只在技能发动时出现。
|
||||
- 部署顺序是**从上到下、从左到右**,最后部署的干员嘲讽等级最高;部署费用不够时后面的干员会晚一点上场。
|
||||
- **阻挡**看接触:干员阻挡中心距离约 0.7 格以内、与它接触的敌人(地面干员挡不住飞行和近地悬浮的敌人)。敌人卡在两名干员之间时,阻挡它的干员倒下、撤退或被晕眩后,另一名仍与它接触、还有空余阻挡数的干员会**立刻接替阻挡**;都没有空位时敌人才会走掉。迎面走来的敌人会停在干员格子前面。**近战干员**总能攻击自己阻挡的敌人(即使它不在攻击范围内),并优先攻击它;站在近战位的远程干员也能阻挡,但只打攻击范围内的敌人、不会优先打自己阻挡的敌人(挡在身后、范围外的敌人打不到)。
|
||||
- 开战时干员按**从上到下、从左到右**的顺序部署,召唤物在所有干员之后。被阻挡的敌人先打阻挡它的干员;没被阻挡的远程敌人先打嘲讽等级高的,同级时打最后部署的(召唤物、靠下靠右的干员和刚再部署的干员更容易挨打)。隐匿、迷彩的干员只会被它阻挡的敌人攻击。
|
||||
- 右上角 🔍▶▶ 可以查看本回合即将来袭的敌人(预览区),左上角 🔍 查看本局信息(禁用盟约、策略)。
|
||||
|
||||
## 5. 联防(同盟)
|
||||
|
||||
某回合**有人漏怪、又有人完美作战**(全歼且无漏怪)时触发:最多 2 名完美作战的队友带着自己的阵容(保持作战结束时的生命与技能状态,已倒下的干员不参加),迎击其他玩家漏掉的全部敌人。
|
||||
某回合**有人漏怪、又有人完美作战**(全歼且无漏怪)时触发:最多 2 名完美作战的队友带着自己的阵容(保持作战结束时的生命与技能状态;作战结束时已被击倒的干员在原位倒地,按官方的「强制退场」以完整的再部署时间重新倒计时,时间到且部署费用足够后照常再部署——联防较短时可能来不及再部署;机变「征召」这类开战时生效的再部署时间减免同样计入),迎击其他玩家漏掉的全部敌人。
|
||||
|
||||
- 参加者按官方规则挑选:场上单位最多 > 有已激活的盟约 > 未倒下的单位最多 > 座位靠前(与 LP 无关);两人参加时,排在前面的一位(再比较激活盟约的层数总和)在右半场「率先迎敌」。
|
||||
|
||||
@@ -94,6 +95,8 @@
|
||||
## 7. 最终攻势与隐秘核心
|
||||
|
||||
- **最终攻势**(第 14 回合;独立标准为第 9 回合):按座位两两配对,每对共享一个战场,一人守左半边、一人守右半边(右侧玩家的棋盘左右镜像)。所有人**共同削减同一条领袖血条**,看不到另一组的战场。全队 LP 合并成一个池,不再有每回合 10 点上限。顶部的 120 秒倒计时结束后战斗**继续**(不会超时判负);从开战第 150 秒起(实际秒数)全队每秒扣 1 点 LP。领袖血量归零即胜利,LP 归零即失败。此阶段盟约不叠层。
|
||||
- 最终攻势 / 隐秘核心的休整期按你在**关底战场上的半场**判定可部署地块(右侧玩家左右镜像),拖动时高亮的就是可放置的格子:例如「源石流发生装置」关底图中被 1×3 橙色管子围起的地块可以部署。在最终攻势放在这些格子上的干员进入隐秘核心时会留在原位。
|
||||
- **巨型领袖**(假想敌:胄 / 管、盐风主教昆图斯、阿利斯泰尔、“萨米的意志”)的受击判定是一片约 5×3 格的区域(整个管道区块):攻击范围碰到其中任意一格就能打到它;它们**自缚、无法被阻挡**,不会离开原地。溅射按敌人的中心判定,打在旁边小怪身上的溅射碰不到巨型领袖。
|
||||
- **隐秘核心**(第 15 回合,仅险境及以上):打倒领袖后,若全队激活的盟约层数总和 **> 1200**(独立 > 350)且 LP > 1,进入隐秘核心,挑战更强的领袖。失败不影响本局的通关。
|
||||
|
||||
## 8. 表情与观战
|
||||
@@ -111,7 +114,7 @@
|
||||
- 前期优先凑出一个能激活的盟约,比零散买高阶干员更稳;同名 3 张会自动晋升并带来一次免费的高阶招募。
|
||||
- 资金不能留到下回合:没用完就刷新或升级;想要的卡没钱买时先**冻结**。
|
||||
- 升级价格每回合自动 −1,不急的话可以等一两回合再升。
|
||||
- 休整期看看右上角的敌人预览,有飞行单位就留好对空(远程)干员。
|
||||
- 休整期看看右上角的敌人预览,有飞行单位就留好对空(远程)干员;**近地悬浮**的敌人(掠海漂移体、吉兆飞鳞)也算空中单位,近战和只打地面的干员(如迷迭香)打不到,把它晕眩、冻结或睡眠后才会掉下来(掠海漂移体掉下来后一直爬行,模型也换成爬行姿态)。
|
||||
- 同盟模拟里别和队友抢同一系干员(卡池共用),分工凑不同的盟约更容易。
|
||||
- LP 只剩 10 左右时(一回合最多扣 10)要优先保完美作战。
|
||||
- 联防只让完美作战的队友上场,所以稳定清怪的阵容对全队都有价值。
|
||||
|
||||
+144
-33
@@ -15,6 +15,7 @@ server/sim/
|
||||
buffs.js Buff model, mod keys, status catalogue
|
||||
damage.js damage / heal pipeline, shields, dodge, element gauges
|
||||
targeting.js range grids (rotated by the unit direction), target filters, priorities
|
||||
body.js where a unit can be hit: huge units' hit rectangles — the helper of every range test on enemies
|
||||
dir.js the 4 deploy directions: vectors, rotation, mirror (shared with server/match and browsers)
|
||||
projectiles.js projectile flight & impact
|
||||
skills.js SkillRuntime (SP, charges, triggers, kinds, SkillSpec)
|
||||
@@ -57,9 +58,14 @@ b.fieldMeta(); // { fieldId, kind, rect, stageId, units: Uni
|
||||
```
|
||||
|
||||
Construction creates every ally unit (not yet deployed) and installs content (kits, then domain modules). The first
|
||||
`step()` (or `b.start()`) spawns stage crates, deploys all board units for free — per player, **top→bottom then
|
||||
left→right** (right boss side: right→left in field columns) — firing `deploy {initial:true}` for each, then
|
||||
`battleStart`. Register hooks before the first step (e.g. `opts.setup(battle)` or right after `new Battle`).
|
||||
`step()` (or `b.start()`) spawns stage crates, deploys all board units for free — per player the **operators top→bottom
|
||||
then left→right** (right boss side: right→left in field columns), **then the summon pieces** the same way (PRTS
|
||||
卫戍协议/帮助 "按从上到下>从左到右的顺序部署。优先部署干员,随后为召唤物") — firing `deploy {initial:true}` for each, forces
|
||||
out the operators that enter knocked out (`carryState.down`, 联防 — §1.1), then fires `battleStart`. Every summon that came in during the initial deployment (also one an operator's deploy brought along)
|
||||
then ranks after all the operators in the aggro order (`unit.aggroSeq`, §1.2). On a shared field (联防, the boss field)
|
||||
the players' fields deploy side by side: the i-th operators of all players come in together (in `players` order), then
|
||||
the summons the same way, and the summons rank after the operators of all players **[ASSUMED]** (PRTS describes one
|
||||
field — units deploy one after another with a fixed delay from the battle start — and "两处阵地将前后拼接为一处阵地"). Register hooks before the first step (e.g. `opts.setup(battle)` or right after `new Battle`).
|
||||
|
||||
**Tick order:** scheduled callbacks → spawns → DP → buffs (+HP regen) → enemies (attack, move, block) →
|
||||
enemy tile index → allies (skill tick, attack) → projectiles → auto-redeploys → boss pool sync → `tick` hook →
|
||||
@@ -98,6 +104,13 @@ directions on an open field). 余 S3's fire wall runs through his tile perpendic
|
||||
RIGHT / LEFT, his row facing UP / DOWN; fx `firewall.axis` = `'col'|'row'`).
|
||||
`carryState: { hpPct, sp, skillActive }` restores unite helpers (HP ratio, SP; `skillActive` restarts a timed skill for a
|
||||
fresh duration/ammo **without spending a charge** — `unitsEnd` reports `sp: 0` while a skill runs, so pass it through as is).
|
||||
`carryState: { down: true }` = an operator knocked out at the end of the helper's own combat (PRTS 卫戍协议/帮助 "上一阶段为
|
||||
退场状态的干员强制退场", user playtest #5 item 2): `start()` deploys it with everyone (initial `deploy` fires), then — before
|
||||
`battleStart` — withdraws it with reason `FORCED_EXIT` (constants.js `'forcedExit'`) and HP 0 (the end-of-phase HP ratio, as after
|
||||
`kill`): it is down on its tile (`isDown`, b.snap `down`) with its full redeploy timer and redeploys by the normal rule; no `kill` hook, no `'killed'` death (knock-out
|
||||
effects such as 深海 / 不屈 revives or 崇高牺牲 layers fired in its own combat), no `deaths` count. Right after `battleStart`
|
||||
its timer is re-read (`deathAt + respawnTime × persist.redeployMul × s.redeployMul`), so a redeploy-time effect switched on
|
||||
by a `battleStart` handler covers it like a later knock-out (机变 征召 −50 %; 征召's row check does not count it [ASSUMED]).
|
||||
|
||||
### 1.2 Enemies, routes, ownership
|
||||
|
||||
@@ -119,9 +132,33 @@ the decorative crate on it — act1 m03 (11,5); test/sim/pathing.test.js). If cr
|
||||
and destroys it. FLY legs fly straight between checkpoints. `disappear` hides
|
||||
the enemy (untargetable, not in snapshots), `wait` pauses, `appear` teleports.
|
||||
|
||||
**Blocking** (`Battle._checkBlock`, official contact rule — PRTS 游戏数据基础 §阻挡半径, 作战机制 "中点判定 … 案例: 阻挡";
|
||||
user playtest #5 item 4): an unblocked, blockable enemy is blocked by an ally (or device) whose centre is within its
|
||||
block radius of the enemy's position — `constants.js BLOCK_RADIUS`: ground 0.7071 (compared as d² < 0.49999037, the
|
||||
tile's circumscribed circle), air 0.8944 (blockFly units against flyers), devices 0.4472 — while that blocker has free
|
||||
capacity for the enemy's `blockWeight` (data `blockCnt`). It is checked every tick for every unblocked enemy, moving or
|
||||
not: an enemy that overlaps an operator when its blocker dies / is withdrawn / is stunned, or when the operator's
|
||||
blocked enemy dies, is taken over at once; an enemy that finds no room walks on (pass-through). Several blockers in
|
||||
contact → the nearest [ASSUMED]. A head-on enemy therefore stops at contact, ~0.71 tile from the blocker's centre, on the
|
||||
tile in front of it (PRTS 作战机制: a blocked enemy's collider does not enter the blocker's tile; the official few
|
||||
hundredths of a tile of deceleration are not modelled), and a **melee** blocker (data `position` MELEE, `targeting.js
|
||||
meleeUnit`: 要塞 / 领主 / 哨戒铁卫 included) may always target the enemies it blocks, in range or not, and targets them first
|
||||
("可以选择且优先选择阻挡单位", PRTS 选择器; 索敌的概念 "我方索敌优先级:阻挡(近战限定)" — `Battle.blockedTargets`, used by
|
||||
`ai.js acquireTargets` and the skills' DEFAULT trigger, and `sortEnemyTargets`; "自身这格内" rules name the blocked enemies
|
||||
separately — 瑕光 S2, PRTS 备注). A ranged operator standing on a melee tile blocks, but targets by its range alone: no
|
||||
blocked-first priority, nothing out of range, no DEFAULT cast for the enemy it holds (PRTS 索敌的概念 "远程位干员通常而言不会
|
||||
优先攻击自己阻挡的目标;甚至如果这个目标被阻挡在该干员的身后…无法攻击到这个敌人").
|
||||
|
||||
**Enemy attacks:** blocked melee enemies hit their blocker; enemies with `rangeRadius > 0` **and `applyWay` ≠ `MELEE`**
|
||||
attack allies within the radius (a MELEE enemy only ever hits its blocker — data/enemies.json already zeroes their
|
||||
`rangeRadius`, the engine enforces it for any source; content may set `enemy.profile.melee = false`) (blocker → highest taunt → latest deployed; stealthed and `untargetable` allies are skipped for ranged shots)
|
||||
`rangeRadius`, the engine enforces it for any source; content may set `enemy.profile.melee = false`). Target order
|
||||
(targeting.js `sortAllyTargets`, PRTS 作战机制 索敌 "阻挡→特殊优先级→仇恨值(更容易被攻击→…→最后部署的目标→不容易被攻击)"):
|
||||
its blocker → highest taunt level → latest deployed (`aggroSeq` = the deploy order: a redeploy or a mid-battle summon is the
|
||||
latest). `enemy.profile.canTarget(ally)` (content: 萨卡兹枯朽战车 "只攻击位于低地的我方单位,且不会攻击飞行单位", 掠海漂移体 / “萨科塔之眼” 不会攻击飞行单位 …) filters the candidates before the order;
|
||||
a special priority (假想敌:铳 / 昆图斯 highest DEF, 假想敌:胄 highest / lowest ATK, “自在” nearest …) sorts by its key and
|
||||
breaks ties by taunt, then latest deployed (`aggroCmp`); `untargetable` / sleeping allies and devices are never targets;
|
||||
a stealthed ally (隐匿 / 迷彩, 排气格栅) only for the enemy it blocks — our operators keep 隐匿 while blocking (PRTS 作战机制
|
||||
§隐匿; 索敌的概念: a blocked enemy "强行无视对方可选性" attacks its blocker)
|
||||
and pause `ATTACK_PAUSE` (0.35 s) after each unblocked attack; `fear`/`disarm` stop attacks; `dmgType 'none'` enemies
|
||||
never attack; `dmgType 'heal'` enemies heal the lowest-HP% enemy in their radius instead. Content can take over an
|
||||
enemy's attack: `enemy.profile.deferHit` = the engine makes the attack (target, timing, the `'atk'` event) but deals no
|
||||
@@ -204,13 +241,35 @@ extraRangeKeys (content extra targets, `battle.setExtraRange`), blocking[] (alli
|
||||
('WALK'|'FLY' enemies), profile, skill (SkillRuntime), kit, items (itemIds), lpr/mods/tag/bounty/sourcePlayerId (enemies),
|
||||
stats {dmg,kills,heal,taken,attacks}, mem {} and trait {} (free scratch space), persist {redeployMul, …}.`
|
||||
Getters: `s` (aggregated stats), `maxHp`, `atk`, `hpRatio`, `sp`, `spMax`, `canAct`, `isFlying`, `statusFlags` (UF bits),
|
||||
`dmgType`, `weight`.
|
||||
`dmgType`, `weight`. `isFlying` = an **air unit** for every targeting / ground-only rule: `motion` FLY, or an enemy with
|
||||
flag `float` (近地悬浮, PRTS 术语释义 "算作空中单位") or `levitate` (浮空 "变为空中单位"); movement and pathing read `motion`
|
||||
(a hovering enemy keeps walking the ground path). `deploySeq` counts deployments (and identifies one: `seq === u.deploySeq`);
|
||||
`aggroSeq` is the aggro order (= deploySeq, except the summons of the initial deployment, §1).
|
||||
|
||||
**Hit areas (`body.js`, user playtest #5 item 10).** A regular enemy is a point: in a grid range when the tile of its
|
||||
position (`round(y)`, `round(x)`) is a range tile, in a radius when its position is (DESIGN §3). A huge enemy (巨型单位:
|
||||
`enemy.hitArea` from data/enemies.json `hitArea` — 假想敌:胄 / 管 / 盐风主教昆图斯 / 阿利斯泰尔 / “萨米的意志”, PRTS
|
||||
"受击判定区域为长4.95、宽2.95的长方形,向上偏移1.0") is hit anywhere on a rectangle `w` × `h` tiles centred on its position
|
||||
moved `dx` columns right / `dy` rows up: it occupies every tile the rectangle overlaps (`bodyKeys` — 5 × 3 tiles; PRTS
|
||||
作战机制 "巨型BOSS单位的每一个占据的格子都可以让其本身通过格子判定"), and radius rules measure to the rectangle (`bodyDist`, 0
|
||||
inside) [ASSUMED: PRTS 作战机制 calls collider tests the most common one] — except **splash around a struck / marked
|
||||
target**, a 中点判定 (PRTS 作战机制 "中点判定…案例:阻挡,酒神1天赋的1.3溅射半径"): `enemiesInRadius(x, y, r, true)` counts
|
||||
every enemy, a huge one too, by its position (the engine's profession splash and 炮手 aftershocks, and the kits' splash
|
||||
centred on an enemy — 焰影苇草 S3's 灼痕 burst, 新约能天使 火力电台, 仇白 S1 留羽). Its position stays its 判定中心 (projectiles fly to it; its own
|
||||
attacks, and splash centred on it, measure from it); body size plays no part in blocking or movement: every huge leader is 自缚 + 无法被阻挡 (PRTS 天赋; content/bosses.js
|
||||
`SELF_BOUND` gives it a persistent `noMove` + `unblockable` buff at spawn — it stands where it spawns, its route is never
|
||||
walked, so the rectangle stays on the pipe block). Content tests enemies only through the engine
|
||||
queries above or `body.js` (`bodyInKeys`, `bodyOnTile`, `bodyInRadius`, `bodyDist`, `bodyTileReach`; wave-B modules via
|
||||
`support/index.js` `onKeys` / `inRange`), never `rangeKeySet.has(tileKey(enemy))`; distance-scaled numbers (farther
|
||||
targets take more damage, pull strength), movement, pathing, terrain under the enemy, tile-entry tracking, placement
|
||||
heuristics and the range keys taken from blocked enemies (a huge enemy is never blocked) keep the position.
|
||||
|
||||
`unit.s` (lazy, recomputed after any buff change): `maxHp, atk, def, res, aspd, bat, interval, blockCnt, moveSpeed,
|
||||
rangeExtend, baseRangeExtend (its permanent part: persist + never-expiring buffs), massLevel (base + ΣmassFlat, ≥ 0 —
|
||||
`unit.weight`), maxTargets (+n), taunt, dodgePhys, dodgeArts, defIgnoreFlat/Pct, resIgnoreFlat/Pct, dmgDealtMul,
|
||||
physDealtMul, artsDealtMul, dmgTakenMul, physTakenMul, artsTakenMul, trueTakenMul, elemTakenMul, healingDealtMul,
|
||||
healingTakenMul, atkScaleMul, spRecovery, spCostFlat, redeployMul, hpRegen, shield, flags{…}`.
|
||||
physDealtMul, artsDealtMul, dmgTakenMul, physTakenMul, artsTakenMul, trueTakenMul, elemTakenMul (元素损伤倍率: gauge fills),
|
||||
elementalTakenMul (元素脆弱: 元素伤害), healingDealtMul, healingTakenMul, atkScaleMul, spRecovery, spCostFlat, redeployMul,
|
||||
hpRegen, shield, flags{…}`.
|
||||
|
||||
Aggregation: `ATK/DEF/maxHp = (base + Σflat) × (1 + Σpct) × Πmul`; `res = clamp((base + ΣresFlat) × ΠresMul, 0, 100)`;
|
||||
`aspd = clamp(base + Σaspd, 10, 600)`; `interval = bat × (1 + ΣbatPct) × 100 / aspd`; `moveSpeed = (base + ΣmoveFlat) × ΠmoveMul`;
|
||||
@@ -233,10 +292,11 @@ mods, flags, onTick(ctx), interval, onExpire(ctx), onRemove(ctx), tags, shield,
|
||||
defIgnoreFlat defIgnorePct resIgnoreFlat resIgnorePct dodgePhys dodgeArts spRecoveryFlat maxTargets taunt hpRegen
|
||||
hpRegenRatio spCostFlat moveFlat massFlat` (重量 levels: 失重 = `massFlat: −1`; never edit `base.massLevel`);
|
||||
multiplicative: `atkMul defMul hpMul resMul moveMul dmgDealtMul dmgTakenMul physTakenMul artsTakenMul trueTakenMul
|
||||
elemTakenMul healingDealtMul healingTakenMul spRecoveryMul redeployMul atkScaleMul physDealtMul artsDealtMul`.
|
||||
elemTakenMul elementalTakenMul healingDealtMul healingTakenMul spRecoveryMul redeployMul atkScaleMul physDealtMul artsDealtMul`.
|
||||
A `rangeExtend` on a `persist` never-expiring buff is **permanent**: it also widens the initial range (§7.1).
|
||||
**Flags:** `stun freeze sleep silence disarm stealth invulnerable unblockable levitate fear cold reveal bind noHeal
|
||||
untargetable blockFly noMove noSp burstLock hidden attract`. `taunt: true` as a flag counts as +1 taunt level (DESIGN §5.3).
|
||||
untargetable blockFly noMove noSp burstLock hidden attract float noDisplace` (`float` = 近地悬浮 (an air unit,
|
||||
`Unit.isFlying`), `noDisplace` = 失衡免疫 (`displace()` moves nothing)). `taunt: true` as a flag counts as +1 taunt level (DESIGN §5.3).
|
||||
|
||||
**Statuses** — `battle.applyStatus(target, key, { duration, source, value, force, refresh, point })` (returns true if
|
||||
applied); honours enemy/op immunities (`stun`, `silence`, `sleep`, `frozen`, `levitate`, `feared`) unless `force`; fires
|
||||
@@ -249,22 +309,22 @@ if it outlasts it, resumes when the strong one expires (pass `refresh` to opt ou
|
||||
|
||||
| key | effect | value |
|
||||
|---|---|---|
|
||||
| `stun` | cannot act / move; **a stunned operator blocks nothing** (its blocked enemies walk on) | – |
|
||||
| `stun` | cannot act / move; **a stunned operator blocks nothing** (its blocked enemies are released: taken over by another operator in contact with room, else they walk on — §1.2 Blocking) | – |
|
||||
| `freeze` | stun; **enemies** also RES −15 | – |
|
||||
| `cold` | ASPD −30; a 2nd cold while cold ⇒ `freeze` 3 s (unless frozen-immune) | – |
|
||||
| `sleep` | 无敌且无法行动: inactive, untargetable, **takes no damage** (unless the attacker profile has `hitSleep` or the damage `ignoreSleep`), blocks nothing | – |
|
||||
| `slow` | moveMul 1 − value (*strongest*) | default 0.5 |
|
||||
| `sluggish` (停顿) | moveMul 0.2 | – |
|
||||
| `bind` (束缚) | cannot move | – |
|
||||
| `fragile` / `artsFragile` / `physFragile` / `elemFragile` | taken ×(1+value) (*strongest*) | 0.3 / 0.3 / 0.3 / 0.2 |
|
||||
| `fragile` / `artsFragile` / `physFragile` / `elemFragile` | taken ×(1+value) (*strongest*); `elemFragile` = 元素脆弱 ("受到的元素伤害提升"): `elementalTakenMul`, 元素伤害 only — never the gauge | 0.3 / 0.3 / 0.3 / 0.2 |
|
||||
| `silence` | no skill activation | – |
|
||||
| `fear` (恐惧) | 无法被阻挡并四散逃跑: enemy cannot attack, is unblockable (released) and does not advance | – |
|
||||
| `tremble` (战栗) | 被阻挡后无法进行普通攻击: no normal attack **while blocked** (abilities still fire) | – |
|
||||
| `palsy` (麻痹) | each stack cancels one enemy normal attack (max 3, lasts until consumed) | stacks, default 1 |
|
||||
| `palsy` (麻痹) | each stack cancels one enemy normal attack (max 3, lasts until consumed); refused by 麻痹免疫 (data `palsyImmune`) | stacks, default 1 |
|
||||
| `disarm` | no normal attacks | – |
|
||||
| `stealth` / `reveal` | untargetable unless blocked / cancels stealth | – |
|
||||
| `stealth` / `reveal` | untargetable unless blocked (an ally: only the enemy it blocks attacks it) / cancels stealth | – |
|
||||
| `invulnerable` | ignores damage | – |
|
||||
| `levitate` (浮空) | stun + unblockable (unblocks enemies); **half duration on units with (current) massLevel > 3** | – |
|
||||
| `levitate` (浮空) | stun + unblockable (unblocks enemies) + 失衡免疫 (`noDisplace`); an air unit meanwhile (`isFlying`: melee cannot hit it); **half duration on units with (current) massLevel > 3**; refused on data flyers (`motion` FLY) and units already levitated (PRTS 异常效果 "若单位数据上为飞行单位…或是持有浮空异常则Buff取消") — a 近地悬浮 enemy is WALK in its data, so it can be levitated; 浮空 is not one of the 近地悬浮 enemies' drop triggers | – |
|
||||
| `attract` (诱导) | 无法被阻挡并向目标位置移动: unblockable (released); the engine walks it (own speed, grid path re-planned on obstacle changes; flyers straight) to `opts.point` (`[r, c]` or `{x, y}`, default the source's tile, clamped to the rect) and keeps it there; stun/bind/sleep stop it; its route re-plans from where it stands when the status ends. A new application moves the point | point |
|
||||
| `resist` (抵抗) | the control statuses of `RESIST_STATUSES` (晕眩 冻结 寒冷 沉睡 恐惧 战栗 诱导 浮空 束缚 沉默 缴械 停顿 减速) applied to the unit last ×(1 − value); a resisting unit loses one 麻痹 stack every 5 s. **同名效果不叠加** — `battle.resistOf(u)` = the strongest `status: 'resist'` buff (never a product): several sources (灵知, 流明, 寒檀, enemy talents) never compound. *strongest*; a permanent one that survives death is a `persist` buff with `status: 'resist', data: { value }` | 0.5 (≤ 0.95) |
|
||||
| `taunt` | taunt level +value | 1 |
|
||||
@@ -277,14 +337,24 @@ a custom buff with `flags.sleep` also blocks nothing and is untargetable/invulne
|
||||
|
||||
**Element gauges** (`unit.elem = {burn, neural, apoptosis, erosion, necrosis}`; capacity `unit.gaugeMax` = 1000, enemy
|
||||
leaders (rank BOSS / boss units) 2000): deal `{ type:'element', element, amount }` (fires `elementHit` first; the gauge
|
||||
gain is × `elemTakenMul` × (1 − 损伤抵抗 / 100), 损伤抵抗 = the target's data `epResistance` — PRTS 元素 "受到的元素损伤 =
|
||||
损伤值 × (1 − 损伤抵抗 × 0.01)"). A full gauge bursts with the official effects, which depend on the side hit
|
||||
(constants.js `ELEMENT`):
|
||||
gain is amount × `dmg.mul` × `elemTakenMul` (元素损伤倍率: "受到的元素损伤提高/降低…") × max(5 %, 1 − 损伤抵抗 / 100),
|
||||
损伤抵抗 = the target's data `epResistance` — PRTS 元素 "受到的元素损伤 = 损伤值 × (1 − 损伤抵抗 × 0.01),
|
||||
后续可应用元素损伤倍率", with the 5 % floor of DMG_e (PRTS 游戏数据基础 "目标受到元素损伤时也可以使用该公式计算,只需要将 D 值
|
||||
改为目标的损伤抵抗即可"); `elementIntake(unit)` in damage.js returns that factor, applied after `elementHit`). 元素脆弱 never
|
||||
scales it, no gauge decays (EP_RECOVERY_PER_SEC 0), and every enemy in data/enemies.json has 损伤抵抗 0 except 转译基底·α
|
||||
(10); operators have none.
|
||||
Enemies deal ATK × their talent's `ep_damage_ratio` per hit (content/enemies.js `ep`). A full gauge bursts with the
|
||||
official effects, which depend on the side hit (constants.js `ELEMENT`); burst damage is **无来源** (DamageInfo
|
||||
`sourceless`, PRTS 伤害分类 "无法被追溯伤害来源": no damage-dealt multiplier or penetration of the unit that filled the
|
||||
gauge, and the `hit` / `damaged` / `fatal` hooks see `source: null` — no attacker-keyed content applies — while the ctx's
|
||||
`credit`, the stats and the kill keep that unit; target-side reactions with no source condition read `source || credit`:
|
||||
damage sharing — 圣杯, 盲信之誓, 余音 — and 抵挡 — 星熊 战术装甲, 拉普兰德 日晷, whose "enemy damage" guard is otherwise
|
||||
the source's side):
|
||||
|
||||
| element | operator hit by enemies (ba.dt.*) | enemy hit by operators ("·我方" ba.dt.*2) |
|
||||
|---|---|---|
|
||||
| `burn` 灼燃 | 1200 arts + RES −20, 10 s lock | 7000 元素伤害 + RES −20, 10 s lock |
|
||||
| `neural` 神经 | stun 10 s, then 1000 true (10 s lock) | 3 `palsy`, then 6000 元素伤害, 10 s lock |
|
||||
| `neural` 神经 | stun 10 s, then 1000 true (10 s lock) | 3 `palsy` (none with 麻痹免疫), then 6000 元素伤害, 10 s lock |
|
||||
| `apoptosis` 凋亡 | 15 s: 阻回 (`noSp`: no SP gain of any kind, skills.js) + 静默 (no skill activation), −1 SP/s, 100 arts/s | 15 s: 50 % weaken recovering over the burst, 800 元素伤害/s |
|
||||
| `erosion` 侵蚀 | permanent DEF −100 (stacking `erosionDown`) then 800 phys, 10 s lock | permanent DEF −120 then 5000 元素伤害, 8 s lock |
|
||||
| `necrosis` (legacy spare gauge) | 12 s: 100 true/s, ATK −20 % | same |
|
||||
@@ -298,7 +368,14 @@ neighbours (淤困 parasite) cannot bounce it back into a second burst. `element
|
||||
a unit shows (b.snap `elem`, §9): the fullest one, ties by the official element id (`constants.js ELEMENT_ORDER`: 神经,
|
||||
侵蚀, 灼燃, 凋亡, then the legacy `necrosis`); during a 爆发冷却 the bursting element with fill 1 and the cooldown's end.
|
||||
**元素伤害** (element HP damage, e.g. "每秒受到…元素伤害") is the DamageInfo type `'elemental'` (+ optional `element` for the
|
||||
client colour): no DEF/RES/dodge, × source `dmgDealtMul` × target `dmgTakenMul` × `elemTakenMul`, shields absorb it.
|
||||
client colour): no DEF/RES/dodge — 元素抗性 instead (the target's data `epDamageResistance`, `max(A × (1 − D/100), 5 %A)`,
|
||||
PRTS 游戏数据基础 DMG_e; 0 on every enemy in data/enemies.json) —, × source `dmgDealtMul` × target `elementalTakenMul`
|
||||
(元素脆弱) only — the target's `dmgTakenMul` (脆弱, ba.fragile "受到的物理、法术、真实伤害提升", and the other "受到的伤害±"
|
||||
effects [ASSUMED: they share that multiplier]) does not scale it —, shields absorb it. An element fill on a target with no
|
||||
HP left is refused (damage.js `hasHp`: a lethal hit's `damaged` hook runs before `battle.kill`, while the target is still
|
||||
`alive` at 0 HP — no burst on the corpse). **Element healing** (`battle.reduceElement(unit, amount, el?)`) lowers element
|
||||
`el`, or every element type, each by `amount` on its own (PRTS 菲莱 备注: 清除元素损伤 = "一次等同于自身最大元素值的全类型元素
|
||||
损伤治疗").
|
||||
|
||||
---
|
||||
|
||||
@@ -306,15 +383,19 @@ client colour): no DEF/RES/dodge, × source `dmgDealtMul` × target `dmgTakenMul
|
||||
|
||||
`battle.dealDamage(source, target, dmg)` → HP removed. `DamageInfo = { amount, type:'phys'|'arts'|'true'|'elemental'|'element',
|
||||
element?, defIgnoreFlat, defIgnorePct, resIgnoreFlat, resIgnorePct, mul=1, canDodge (phys/arts), isSkill, isSplash,
|
||||
isAttack, attackId, ignoreSleep, tags[], cancel }` (`battle.makeDamage(d)` normalises). `attackId` is the same for every
|
||||
isAttack, attackId, ignoreSleep, sourceless, tags[], cancel }` (`sourceless`: 无来源 damage — the source's stats add
|
||||
nothing and the hooks get `source: null` plus `credit` = the source, which keeps the stats and the kill; a `loseHp` whose
|
||||
`from` is 无来源 is 无来源 too) (`battle.makeDamage(d)` normalises). `attackId` is the same for every
|
||||
damage instance of one normal attack (all targets, splash, chain, projectile impacts; 0 for non-attack damage) — use it
|
||||
for "本次攻击" procs that must roll once per attack. **Dodge** from several buffs rolls independently: the unit's
|
||||
`s.dodgePhys` = 1 − Π(1 − pᵢ) (a single source keeps its exact value).
|
||||
Order: invulnerable / asleep? → **`hit`** (mutate `dmg`, set `dmg.cancel`) → dodge (`rng()`) → mitigation (phys
|
||||
`max(A − max(0, D×(1−defIgnorePct) − defIgnoreFlat), 5 %A)`, arts `max(A×(1 − R′/100), 5 %A)`, true = A; source ignore
|
||||
mods are added) → × source `dmgDealtMul` (× phys/artsDealtMul) × target `dmgTakenMul` × type-taken mul × `dmg.mul` →
|
||||
shields → HP loss (boss units: routed to `sharedBoss.damage(playerId, amount)`) → if HP ≤ 0: **`fatal`**
|
||||
(`ctx.prevented = true` keeps the unit at ≥ 1 HP) → **`damaged`** → SP-on-hurt / TAKE_DAMAGE → `kill` + `death`.
|
||||
`max(A − max(0, D×(1−defIgnorePct) − defIgnoreFlat), 5 %A)`, arts `max(A×(1 − R′/100), 5 %A)`, elemental
|
||||
`max(A×(1 − 元素抗性/100), 5 %A)`, true = A; source ignore mods are added) → × source `dmgDealtMul` (× phys/artsDealtMul)
|
||||
× target `dmgTakenMul` (not for elemental) × type-taken mul × `dmg.mul` (a `sourceless` hit skips every source term) →
|
||||
shields → HP loss
|
||||
(boss units: routed to `sharedBoss.damage(playerId, amount)`) → if HP ≤ 0: **`fatal`** (`ctx.prevented = true` keeps the
|
||||
unit at ≥ 1 HP) → **`damaged`** → SP-on-hurt / TAKE_DAMAGE → `kill` + `death`.
|
||||
|
||||
`battle.heal(source, target, amount, { overheal=false, self, silent })`: no-op on `noHeal` targets (unless self);
|
||||
× source `healingDealtMul` × target `healingTakenMul`; **`heal`** hook (mutable amount); capped at max HP; `overheal`
|
||||
@@ -335,13 +416,13 @@ registration order. `battle.off(handle)` / `battle.off(name, fn)` / `battle.offO
|
||||
| `tick` | `{ dt }` | end of every tick |
|
||||
| `beforeAttack` | `{ attacker, targets, isSkill, profile }` | allies **and** enemies; replace/filter `ctx.targets` |
|
||||
| `attack` | `{ attacker, targets, isSkill }` | an attack/heal was performed (projectiles may still be in flight) |
|
||||
| `hit` | `{ source, target, dmg }` | before mitigation; mutate `dmg` (not fired for gauge fills — see `elementHit`). `source` may be null (terrain, bursts) |
|
||||
| `hit` | `{ source, target, dmg, credit }` | before mitigation; mutate `dmg` (not fired for gauge fills — see `elementHit`). `source` may be null (terrain; 无来源 `dmg.sourceless` bursts, whose `credit` names the unit credited) |
|
||||
| `elementHit` | `{ source, target, dmg }` | before a gauge fill (`dmg.type === 'element'`); mutate `dmg.amount`/`dmg.mul`, set `dmg.cancel` |
|
||||
| `damaged` | `{ source, target, amount, type, dmg }` | after application (`amount` may be 0 when shielded); element fills too |
|
||||
| `damaged` | `{ source, target, amount, type, dmg, credit }` | after application (`amount` may be 0 when shielded); element fills too (with their source); 无来源: `source` null, `credit` set |
|
||||
| `heal` | `{ source, target, amount, opts }` | mutable `amount` |
|
||||
| `fatal` | `{ unit, source, dmg, amount, prevented }` | HP would reach 0 — set `prevented` (substitutes, 不屈, 复活 items) |
|
||||
| `fatal` | `{ unit, source, credit, dmg, amount, prevented }` | HP would reach 0 — set `prevented` (substitutes, 不屈, 复活 items) |
|
||||
| `kill` | `{ killer, victim }` | victim HP reached 0 (a handler may revive by restoring HP) |
|
||||
| `death` | `{ unit, reason:'killed'|'leak'|'retreat'|'merchant'|'expired', killer }` | unit removed |
|
||||
| `death` | `{ unit, reason:'killed'|'leak'|'retreat'|'merchant'|'expired'|'forcedExit', killer }` | unit removed (`'forcedExit'`: an operator entering 联防 knocked out, §1.1) |
|
||||
| `skillStart` / `skillEnd` | `{ unit, skill, reason }` | mutate `skill.ammoLeft` / `skill.timeLeft` in skillStart |
|
||||
| `ammoUsed` | `{ unit, left, skill }` | per ammo consumed |
|
||||
| `spGain` | `{ unit, amount, reason:'time'|'attack'|'hurt'|'init'|…, skill }` | mutable `amount` (time gains fire every tick) |
|
||||
@@ -377,7 +458,8 @@ or guard with a per-unit flag while dealing it. When the guard trips, the logged
|
||||
| `spawnDevice(key, row, col, { hp, obstacle, blockCnt, name, def, res, atk, bat, aspd })`, `setObstacle(r, c, on)` | obstacles re-path enemies; spawnDevice returns null outside the rect or on a living unit |
|
||||
| `isReservedTile(r, c)` | true when a living unit stands there or it is the home tile of an ally piece that has not deployed yet / waits to redeploy — summon tile pickers must skip these (`findTacticalPoint` does) |
|
||||
| `addProjectile({ from, target | to:{x,y}, speed, onHit(ctx), visual, source, hitDead })` | homing; fizzles if the target dies unless `hitDead` |
|
||||
| `unitsInGrid(unit, grid, {side, extend})`, `alliesInGrid(unit)`, `enemiesInRadius(x, y, r)`, `alliesInRadius(x, y, r, ownerId?)` | grid offsets are relative to facing RIGHT, rotated by `unit.dir` |
|
||||
| `unitsInGrid(unit, grid, {side, extend})`, `alliesInGrid(unit)`, `enemiesInRadius(x, y, r, centre?)`, `alliesInRadius(x, y, r, ownerId?)` | grid offsets are relative to facing RIGHT, rotated by `unit.dir`; enemies by their body (§2 hit areas: a huge enemy on every tile it occupies / within `r` of its rectangle; `centre` = splash around a target, a 中点判定 by position) |
|
||||
| `enemiesInKeys(keys, attacker, profile)`, `blockedTargets(unit, profile)` | targetable enemies whose body is on the tiles (a huge one listed once); the enemies a melee unit blocks — always selectable by it (none for a ranged operator on a melee tile, §1.2 Blocking) |
|
||||
| `allies(ownerId?)`, `aliveEnemies()`, `unitAt(r, c)`, `unitById(id)`, `tileInfo(r, c)`, `lowestHpAllyInRange(unit)` | |
|
||||
| `addLayers(playerId, bondId, n, reason, {source})`, `addCoins(playerId, n)` | layers are a no-op when `flags.layerGainsEnabled` is false (unite/boss) |
|
||||
| `getPlayer(playerId)` | `{ playerId, seat, side, colOffset, mirror, dir (default unit direction: RIGHT, mirrored side LEFT), facing (its sign), bonds (live copy, layers updated by addLayers), bandId, playerEffects, lpForBoss, dp, units }` |
|
||||
@@ -385,7 +467,7 @@ or guard with a per-unit flag while dealing it. When the guard trips, the logged
|
||||
| `addDp(playerId, n)`, `retreat(unit, {reason, permanent})`, `relocate(unit, r, c)` | |
|
||||
| `redeploy(unit, { free=true, tile, keepSp })` | immediate (re)deployment of a dead/retreated ally (full HP, `deploy {initial:false}`); `free: false` pays `base.cost` DP (refused without it); `tile: [r, c]` lands on that tile once (home unchanged — later redeploys use the board tile; refused when off-rect or occupied, no fallback); `keepSp` keeps SP/charges (保留技力), restored before `deploy` fires — 突袭 raids, 阿戈尔 revive in place |
|
||||
| `refreshRange(unit)`, `setExtraRange(unit, keys)`, `rangeChanged(unit)` | rebuild the ranges after changing `unit.rangeGrid` (流形 copies); extra targetable tiles (absolute keys; merged into every later rebuild until set again; `null` clears; never in `baseRangeKeys`): 蕾缪安 wanted, 维娜 S3 |
|
||||
| `displace(enemy, {x, y}, tiles, {force})` | push/pull along passable tiles; heavier (massLevel) enemies move less; unblocks + re-paths |
|
||||
| `displace(enemy, {x, y}, tiles, {force})` | push/pull along passable tiles (by `motion`: FLY in the rect, else ground-passable); heavier (massLevel) enemies move less; nothing for 失衡免疫 (`noDisplace`: 近地悬浮, 浮空) and leaders; unblocks + re-paths |
|
||||
| `after(seconds, fn, {owner})`, `every(seconds, fn, {owner, immediate})` | return `{cancel()}`; fn(battle, sched) |
|
||||
| `fx(kind, params)`, `rng()` (+ `rng.int/range/chance/pick/shuffle/weighted`) | never use Math.random |
|
||||
| `forceAttack(unit, targets?, { noAmmo })` | an immediate attack with the current profile (hooks, attack SP, a running ammo skill's bullet); `noAmmo: true` = spends no bullet and emits no `ammoUsed` (圣约送葬人 extra attack). Returns true when it attacked |
|
||||
@@ -457,6 +539,28 @@ has the generic spec. Summons: `battle.tokenDef(tokenId, unit)` / `battle.spawnT
|
||||
(token skill of that slot: `variants[owner].bySkill[i]`, module attributes: `.byModule[id]`). Coverage of the selectable
|
||||
skills: `node tools/kit-coverage.mjs --missing [--tier N] [--strict]`.
|
||||
|
||||
Element conventions of the kits (user playtest #5 #3; official term dictionary: 元素损伤 = the gauge, 元素伤害 = HP damage):
|
||||
- **元素损伤 dealt** = `{ type: 'element', element, amount }` with the official base: "N%攻击力的…损伤" ⇒ `N × ATK` at that
|
||||
moment, "伤害N%的…损伤" / "相当于法术伤害N%" ⇒ `N ×` the HP damage just dealt (post-mitigation, `damaged` ctx.amount).
|
||||
No damage buff or 脆弱 scales it (the attached-to damage already carries them).
|
||||
- **"自身造成的元素损伤提升N%"** (菲莱 / 余 PRP-X while blocking, 塑心 RIT-X vs elite / leader) multiplies EVERY element fill
|
||||
of the unit — kit talents, a carried 灼燃维式重锤 — in an `elementHit` handler (`ctx.source === unit` ⇒ `ctx.dmg.mul *=`);
|
||||
a talent-ratio upgrade (盟约·辅助干员 RIT-X `ep_damage_ratio_boss`) touches its talent only (PRTS 备注).
|
||||
- **"受到的元素损伤±N%"** is also an `elementHit` multiplier (PRTS 元素 "…后续可应用元素损伤倍率提升/降低等效果") — not the
|
||||
mod `elemTakenMul` (the gauge's generic 元素损伤倍率 in `elementIntake`, applied after every `elementHit` handler, so
|
||||
after the 损伤屏障; 元素脆弱 is `elementalTakenMul`). On operators (菲莱 神河谕使, 哈洛德 我即军营, 纯烬艾雅法拉 火山灰疗愈,
|
||||
and the host of 假想敌:淤困, "受到的元素损伤提高至130%") it runs at priority 20, before the 损伤屏障 (菲莱 S1, 纯烬 S2 at −50:
|
||||
"损伤屏障于伤害计算前、第一天赋后处理"); the enemy-side amplifier 塑心 精神逆构 (×1.2 凋亡 in her range) has no ordering
|
||||
constraint (priority 0 — enemies carry no 损伤屏障).
|
||||
- **Element attached to a damage** (a `damaged` hook: 迭代元素, 灼燃维式重锤, 炎佑, 妮芙 S2, 余 S3 火墙): the hook runs before
|
||||
`battle.kill`, so a killing blow still sees the target `alive` at 0 HP — these riders check HP left (damage.js `hasHp`,
|
||||
boss: pool HP; tier6 `elementDmg`) and applyElement itself refuses such a target, so nothing bursts on the corpse.
|
||||
塑心 S2 (安魂的弥撒) is the exception: PRTS "…凋亡损伤生效于当次触发的伤害之前,该造成的凋亡损伤的来源始终为塑心" — a late
|
||||
`hit` handler (priority −1000, after any cancel), so its 凋亡 lands (and may burst) before the damage; a hit dodged or
|
||||
absorbed after that still carried it [ASSUMED].
|
||||
- **盟约·辅助干员 迭代元素** (tier6.js `pithst`): every damage she deals (HP damage > 0; "造成伤害时") attaches `ep_damage_ratio
|
||||
× ATK` of 神经, then 灼燃, then 凋亡 (`PITHST_ELEMENTS`); the element applied first bursts, so alone she bursts 神经.
|
||||
|
||||
### 7.3 SkillSpec schema
|
||||
|
||||
```js
|
||||
@@ -655,7 +759,12 @@ const bombard = (battle, unit, locks) => {
|
||||
Profile resolution: `PROFESSION_DEFAULTS[profession] → SUB[subProfessionId] → trait tunables (trait text + data
|
||||
trait.bb, elites include module upgrades) → data fields (dmgType / attackKind / projectile / canHitFly / targetPriority)
|
||||
→ kit.trait`. Defaults: SNIPER ranged phys arrow · CASTER ranged arts bolt · MEDIC ranged heal · SUPPORT ranged arts ·
|
||||
TANK/WARRIOR/PIONEER/SPECIAL melee phys (melee cannot hit FLY). Ranged attacks fly as projectiles
|
||||
TANK/WARRIOR/PIONEER/SPECIAL melee phys (melee cannot hit air units: FLY, 近地悬浮, 浮空 — `Unit.isFlying`). Skill and
|
||||
talent effects that pick their own victims follow the skill's text and PRTS 备注 (the official per-ability `targetMotion`,
|
||||
PRTS 选择器, is not in the data): "地面敌人" / "不可对空" ⇒ `!e.isFlying` (e.g. 隐德来希 S2 血镰, 归溟幽灵鲨 拥抱自我,
|
||||
琳琅诗怀雅 S3 cash-out, 乌尔比安 S1 捕网 [ASSUMED like 雪雉's]); "可对空" or no note ⇒ air units too (no note = [ASSUMED]:
|
||||
风丸 折纸生花, 乌尔比安 S3, 缄默德克萨斯 S2, 余 S2, 见行者 S2, 耀骑士临光 不畏苦暗 and the token appear bursts — “耀阳”,
|
||||
沙之碑, 迷迭香的战术装备's stun, 纸偶). A stun / freeze / sleep from any of them drops a hovering 掠海漂移体 for good. Ranged attacks fly as projectiles
|
||||
(`constants.js PROJECTILE_SPEEDS`: arrow 14, bolt 11, bomb/lob 8, orb 10, drone 16, enemy 10 tiles/s; boomerang 15 out,
|
||||
3.75 back = `BOOMERANG_RETURN_SPEED`, PRTS 跃跃); melee/`none` hits are instant. Kit-settable profile flags beyond the
|
||||
table: `hitSleep` (targets and damages sleeping enemies — "可以攻击沉睡的敌人"), `onEachHit(b, u, victim, hctx)`, `dmgMul`,
|
||||
@@ -716,7 +825,8 @@ Unknown subprofessions fall back to the profession default (test `professions.te
|
||||
- `snapshot()` → `{ fieldId, t, units: [[id, x, y, hp, maxHp, sp, spMax, flags, anim]], dp, killed, total, dps?, boss?, down?, elem? }`.
|
||||
`sp/spMax` show remaining duration/ammo as a draining bar while a timed skill is active. Units in DIE state stay 0.8 s.
|
||||
`down: [[id, respawnAt, respawnTime, state]]` (only when non-empty) = knocked-out operators waiting to redeploy
|
||||
(`Battle.isDown(u)`: reason `'killed'`, deployed at least once, a finite respawn timer; `state` = constants.js
|
||||
(`Battle.isDown(u)`: reason `'killed'` or `FORCED_EXIT` (§1.1 carryState `down`), deployed at least once, a finite
|
||||
respawn timer; `state` = constants.js
|
||||
`DOWN_STATE`: 0 counting, 1 timer done / DP short, 2 timer done / own tile taken); `elem: [[id, element, fill,
|
||||
cooldownEnd, cooldown]]` (only when non-empty) = `elementView` of every unit with a gauge or a running 爆发冷却 (§3).
|
||||
`fieldMeta()` lists the knocked-out operators too (a client joining mid-battle shows them; DESIGN §18.3).
|
||||
@@ -797,7 +907,8 @@ bat, blockCnt, cost, respawnTime, spRecovery, tauntLevel, massLevel, hpRecoveryP
|
||||
skillType, durationType, duration, spType, spCost, initSp, maxCharges, rangeGrid, trigger {rule, grid}, bb, description}`,
|
||||
`talents [{name, description, bb, bbStr, rangeGrid, tokenKey}]`, `raw`. Enemy: `maxHp, atk, def, res, aspd, bat,
|
||||
rangeRadius, moveSpeed, massLevel, lpr, motion, applyWay, dmgType, immune, tauntLevel, blockCnt, epResistance,
|
||||
epDamageResistance, hpRecoveryPerSec, notCountInTotal, tags, abilities, skills, talent, raw`. Tokens use
|
||||
epDamageResistance, hpRecoveryPerSec, notCountInTotal, hitArea (huge units only, §2; else null), tags, abilities, skills,
|
||||
talent, raw`. Tokens use
|
||||
`variants[ownerChessId]` (owner-level stats). `skcom_withdraw` token skills are ignored.
|
||||
Operator loadouts (DESIGN §16, DATA.md §2.2 / §14): `getChess(id, { skillIndex, moduleId })` = the def with the selected
|
||||
skill (`def.skill`, its bb / SP / trigger) and, for elites, the module's stats / trait / talents; `def.loadout` = the
|
||||
|
||||
Reference in new issue
Block a user