feedback3: merge WF — 阿戈尔 revives by position, 傀儡师 switches at full HP, 联防 uses the layers the combat reached, deployment is column-major
This commit is contained in:
commit
ee9fc83207
35 files changed
+392
-149
No files matched your search
+1
-1
@@ -1 +1 @@
|
||||
{"yanShip":{"bondId":"yanShip","name":"炎","identifier":1,"isCore":true,"bondType":"SEASON","bondOrder":2,"powerIdList":["yan","sui","lungmen","lee","lgd"],"iconId":"icon_yanShip","activeCount":3,"thresholds":[3,6,9],"maxCount":null,"thresholdTemplate":"count_threshold_upward","countMode":"BOARD","countsHand":false,"countsGoldenOnly":false,"activeType":"BATTLE","isActiveInDeck":false,"noStack":false,"weight":10,"maxInactiveBondCount":-1,"layerMilestones":[],"desc":"【炎】干员攻击力提升(受层数影响)\n<在场6名不同【炎】干员>战斗开始时召唤可自主行动的“炎佑”,其攻击力生命值分别为开战时【炎】干员攻击力生命值总和的30%,“炎佑”同时攻击3个目标,可使用“祛恶之焰”,攻击时附带攻击力一定比例的灼燃损伤且周围敌人受到一定比例的元素脆弱\n<在场9名不同【炎】干员>改为召唤2只“炎佑”,攻击力提升至1.5倍,受到的伤害-90%","descRaw":"【炎】干员<@ba.vup>攻击力</>提升<@autochess.gray>(受层数影响)</>\n<在场<@autochess.dgreen>6</>名不同【炎】干员>战斗开始时召唤可自主行动的<@ba.vup>“炎佑”</>,其攻击力生命值分别为开战时【炎】干员攻击力生命值总和的30%,“炎佑”同时攻击3个目标,可使用<@ba.vup>“祛恶之焰”</>,攻击时附带攻击力一定比例的灼燃损伤且周围敌人受到一定比例的元素脆弱\n<在场<@autochess.dgreen>9</>名不同【炎】干员>改为召唤2只<@ba.vup>“炎佑”</>,攻击力提升至<@ba.vup>1.5</>倍,受到的伤害<@ba.vup>-90%</>","effectId":"bondeffect_yan","effectName":"炎","effectDesc":"【炎】干员攻击力+{0:0%}(受层数影响)\n<在场6名不同【炎】干员>战斗开始时收集【炎】干员部分属性召唤“炎佑”(其攻击力和生命值为【炎】干员攻击力和生命值总和的30%);“炎佑”同时攻击3个目标,攻击附带20%攻击力的灼燃损伤,周围敌人受到20%的元素脆弱,周期性使用“祛恶之焰”\n<在场9名不同【炎】干员>改为召唤2只“炎佑”;“炎佑”攻击力提升至1.5倍,受到的伤害-90%","effectDescRaw":"【炎】干员攻击力<@ba.vup>+{0:0%}</><@ba.acrem>(受层数影响)</>\n<在场<@ba.vup>6</>名不同【炎】干员>战斗开始时收集【炎】干员部分属性召唤“炎佑”(其攻击力和生命值为【炎】干员攻击力和生命值总和的30%);“炎佑”同时攻击3个目标,攻击附带20%<@ba.vup>攻击力</>的<@ba.vup>灼燃损伤</>,周围敌人受到20%的<@ba.vup>元素脆弱</>,周期性使用“祛恶之焰”\n<在场<@ba.vup>9</>名不同【炎】干员>改为召唤2只“炎佑”;“炎佑”攻击力提升至1.5倍,受到的伤害-90%","effectDescParams":[{"index":0,"format":"0%","base":"base_atk","perStack":"atk_per_stack"}],"bb":{"base_atk":0.23,"atk_per_stack":0.009,"power_bond_char_cnt":6,"ex_bond_char_cnt":9,"damage_resistance":0.9,"atk":1.5},"bbStr":{"key":"act1autochess_bond_eff_yan"},"buffs":[{"key":"env_gbuff_new","bb":{"base_atk":0.23,"atk_per_stack":0.009,"power_bond_char_cnt":6,"ex_bond_char_cnt":9,"damage_resistance":0.9,"atk":1.5},"bbStr":{"key":"act1autochess_bond_eff_yan"}}],"baseParams":["base_atk"],"perStackParams":["atk_per_stack"],"members":["chess_char_1_03_a","chess_char_2_04_a","chess_char_3_03_a","chess_char_3_04_a","chess_char_4_15_a","chess_char_4_17_a","chess_char_5_03_a","chess_char_5_12_a","chess_char_5_23_a","chess_char_6_03_a","chess_char_6_15_a"],"visibleMembers":["chess_char_1_03_a","chess_char_2_04_a","chess_char_3_03_a","chess_char_3_04_a","chess_char_4_17_a","chess_char_5_03_a","chess_char_5_12_a","chess_char_5_23_a","chess_char_6_03_a","chess_char_6_15_a"],"spec":{"tiers":[{"need":"3 distinct 炎 on board","effect":"炎 members (+调和 members) ATK x(1 + 0.23 + 0.009*L)."},{"need":"6 distinct","effect":"At battle start spawn 1 \"炎佑\" (enemy_9012_acloon template, allied flying ranged unit, not blockable, moves on its own). ATK = 0.30 x sum(ATK of 炎 operators on board at battle start), maxHP = 0.30 x sum(maxHP of those operators). Template: DEF 200, RES 0, BAT 2.5 s, ASPD 100, attack range radius 2.0 tiles, magic damage, hits up to 3 targets; every hit adds 灼燃损伤 (burn elemental damage) = 20% of its ATK and applies 元素脆弱 (elemental damage taken x1.2) to enemies within 0.8 tiles of the target. Skill \"祛恶之焰\": cooldown 15 s (first cast 15 s after spawn), channels a flame on one enemy in range dealing 60% ATK magic damage per second to it and to enemies within 1.0 tile, for up to 20 s."},{"need":"9 distinct","effect":"Spawn 2 炎佑 instead; each has ATK x1.5 and takes 90% less damage (damage_resistance 0.9)."}],"formulas":{"atkMultiplier":"1 + 0.23 + 0.009*L"},"caps":"none","layerGain":"Economy-driven: 惊蛰 获得时 +shop level (no activation needed); 小满 休整期结束 +1 per operator gained this rouLine truncated
|
||||
{"yanShip":{"bondId":"yanShip","name":"炎","identifier":1,"isCore":true,"bondType":"SEASON","bondOrder":2,"powerIdList":["yan","sui","lungmen","lee","lgd"],"iconId":"icon_yanShip","activeCount":3,"thresholds":[3,6,9],"maxCount":null,"thresholdTemplate":"count_threshold_upward","countMode":"BOARD","countsHand":false,"countsGoldenOnly":false,"activeType":"BATTLE","isActiveInDeck":false,"noStack":false,"weight":10,"maxInactiveBondCount":-1,"layerMilestones":[],"desc":"【炎】干员攻击力提升(受层数影响)\n<在场6名不同【炎】干员>战斗开始时召唤可自主行动的“炎佑”,其攻击力生命值分别为开战时【炎】干员攻击力生命值总和的30%,“炎佑”同时攻击3个目标,可使用“祛恶之焰”,攻击时附带攻击力一定比例的灼燃损伤且周围敌人受到一定比例的元素脆弱\n<在场9名不同【炎】干员>改为召唤2只“炎佑”,攻击力提升至1.5倍,受到的伤害-90%","descRaw":"【炎】干员<@ba.vup>攻击力</>提升<@autochess.gray>(受层数影响)</>\n<在场<@autochess.dgreen>6</>名不同【炎】干员>战斗开始时召唤可自主行动的<@ba.vup>“炎佑”</>,其攻击力生命值分别为开战时【炎】干员攻击力生命值总和的30%,“炎佑”同时攻击3个目标,可使用<@ba.vup>“祛恶之焰”</>,攻击时附带攻击力一定比例的灼燃损伤且周围敌人受到一定比例的元素脆弱\n<在场<@autochess.dgreen>9</>名不同【炎】干员>改为召唤2只<@ba.vup>“炎佑”</>,攻击力提升至<@ba.vup>1.5</>倍,受到的伤害<@ba.vup>-90%</>","effectId":"bondeffect_yan","effectName":"炎","effectDesc":"【炎】干员攻击力+{0:0%}(受层数影响)\n<在场6名不同【炎】干员>战斗开始时收集【炎】干员部分属性召唤“炎佑”(其攻击力和生命值为【炎】干员攻击力和生命值总和的30%);“炎佑”同时攻击3个目标,攻击附带20%攻击力的灼燃损伤,周围敌人受到20%的元素脆弱,周期性使用“祛恶之焰”\n<在场9名不同【炎】干员>改为召唤2只“炎佑”;“炎佑”攻击力提升至1.5倍,受到的伤害-90%","effectDescRaw":"【炎】干员攻击力<@ba.vup>+{0:0%}</><@ba.acrem>(受层数影响)</>\n<在场<@ba.vup>6</>名不同【炎】干员>战斗开始时收集【炎】干员部分属性召唤“炎佑”(其攻击力和生命值为【炎】干员攻击力和生命值总和的30%);“炎佑”同时攻击3个目标,攻击附带20%<@ba.vup>攻击力</>的<@ba.vup>灼燃损伤</>,周围敌人受到20%的<@ba.vup>元素脆弱</>,周期性使用“祛恶之焰”\n<在场<@ba.vup>9</>名不同【炎】干员>改为召唤2只“炎佑”;“炎佑”攻击力提升至1.5倍,受到的伤害-90%","effectDescParams":[{"index":0,"format":"0%","base":"base_atk","perStack":"atk_per_stack"}],"bb":{"base_atk":0.23,"atk_per_stack":0.009,"power_bond_char_cnt":6,"ex_bond_char_cnt":9,"damage_resistance":0.9,"atk":1.5},"bbStr":{"key":"act1autochess_bond_eff_yan"},"buffs":[{"key":"env_gbuff_new","bb":{"base_atk":0.23,"atk_per_stack":0.009,"power_bond_char_cnt":6,"ex_bond_char_cnt":9,"damage_resistance":0.9,"atk":1.5},"bbStr":{"key":"act1autochess_bond_eff_yan"}}],"baseParams":["base_atk"],"perStackParams":["atk_per_stack"],"members":["chess_char_1_03_a","chess_char_2_04_a","chess_char_3_03_a","chess_char_3_04_a","chess_char_4_15_a","chess_char_4_17_a","chess_char_5_03_a","chess_char_5_12_a","chess_char_5_23_a","chess_char_6_03_a","chess_char_6_15_a"],"visibleMembers":["chess_char_1_03_a","chess_char_2_04_a","chess_char_3_03_a","chess_char_3_04_a","chess_char_4_17_a","chess_char_5_03_a","chess_char_5_12_a","chess_char_5_23_a","chess_char_6_03_a","chess_char_6_15_a"],"spec":{"tiers":[{"need":"3 distinct 炎 on board","effect":"炎 members (+调和 members) ATK x(1 + 0.23 + 0.009*L)."},{"need":"6 distinct","effect":"At battle start spawn 1 \"炎佑\" (enemy_9012_acloon template, allied flying ranged unit, not blockable, moves on its own). ATK = 0.30 x sum(ATK of 炎 operators on board at battle start), maxHP = 0.30 x sum(maxHP of those operators). Template: DEF 200, RES 0, BAT 2.5 s, ASPD 100, attack range radius 2.0 tiles, magic damage, hits up to 3 targets; every hit adds 灼燃损伤 (burn elemental damage) = 20% of its ATK and applies 元素脆弱 (elemental damage taken x1.2) to enemies within 0.8 tiles of the target. Skill \"祛恶之焰\": cooldown 15 s (first cast 15 s after spawn), channels a flame on one enemy in range dealing 60% ATK magic damage per second to it and to enemies within 1.0 tile, for up to 20 s."},{"need":"9 distinct","effect":"Spawn 2 炎佑 instead; each has ATK x1.5 and takes 90% less damage (damage_resistance 0.9)."}],"formulas":{"atkMultiplier":"1 + 0.23 + 0.009*L"},"caps":"none","layerGain":"Economy-driven: 惊蛰 获得时 +shop level (no activation needed); 小满 休整期结束 +1 per operator gained this rouLine truncated
|
||||
+9
-7
@@ -188,7 +188,8 @@ b.on(event, fn, { priority=0, owner } ) / b.off(...) // hook bus (§5.4)
|
||||
items: [itemId...], // equipped items (golden ids when merged)
|
||||
carryState?: { hpPct, sp } | { sp } | { down: true } } ], // unite: an operator keeps its end-of-combat HP ratio + SP, a summon its SP (no running skill);
|
||||
// { 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
|
||||
bonds: { [bondId]: { count, active, tier, layers } }, // snapshot from bondsMeta at combat start (联防: with the layers the
|
||||
// helper's own combat reached, PlayerState.bondsView — §20.15)
|
||||
bandId, playerEffects: [ EffectRef ], // active band/choice/team effects with battle parts
|
||||
lpForBoss?: number }
|
||||
```
|
||||
@@ -265,7 +266,7 @@ Engine helpers (content must use these, never touch internals): `battle.dealDama
|
||||
|
||||
### 5.5 Combat rules (engine)
|
||||
|
||||
- **Initial deployment** at t=0, free: operators row-major top→bottom then left→right (right-side boss player: right→left), then the summons (PRTS 卫戍协议/帮助 "按从上到下>从左到右的顺序部署。优先部署干员,随后为召唤物"; `aggroSeq` ranks every start-of-battle summon after all operators) — a skill's placed summon (赫默 医疗探机, 巫恋 诅咒娃娃) included, once and free ("所有手动部署的召唤物,无视所属干员的持有状态…作战开始时立即部署一次"; afterwards it re-appears on its tile each time the owner's skill gives one, §20.1); on a shared field (联防, boss) the players' i-th operators deploy together [ASSUMED]. Deploy effects fire (`deploy` with `initial: true`). Then operators carried into 联防 knocked out (`carryState.down`) are forced out (`FORCED_EXIT`, HP 0, §19.3). Then `battleStart`, after which the forced-out operators' timers are re-read (redeploy-time effects that start with the battle, e.g. 机变 征召, cover them too).
|
||||
- **Initial deployment** at t=0, free: operators by column — the left column first, top to bottom within a column (right-side boss player: from its own left, the highest field column) — then the summons (PRTS 卫戍协议/帮助 "按从上到下>从左到右的顺序部署。优先部署干员,随后为召唤物", read as a scan: down each column, the columns left to right — the order PRTS gives the 阿戈尔 devour, "更靠左和靠上"; row-major until 0.1.3, §23.f4; `aggroSeq` ranks every start-of-battle summon after all operators) — a skill's placed summon (赫默 医疗探机, 巫恋 诅咒娃娃) included, once and free ("所有手动部署的召唤物,无视所属干员的持有状态…作战开始时立即部署一次"; afterwards it re-appears on its tile each time the owner's skill gives one, §20.1); on a shared field (联防, boss) the players' i-th operators deploy together [ASSUMED]. Deploy effects fire (`deploy` with `initial: true`). Then operators carried into 联防 knocked out (`carryState.down`) are forced out (`FORCED_EXIT`, HP 0, §19.3). Then `battleStart`, after which the forced-out operators' timers are re-read (redeploy-time effects that start with the battle, e.g. 机变 征召, cover them too).
|
||||
- **DP**: per player, start 10, +1/s, cap 99; used only for auto-redeploy (dead op: after `respawnTime`, if the tile it lies on is free and DP ≥ cost ⇒ redeploy there with full HP, SP = initSp). A knocked-out operator (and, since 0.1.2, one forced out by its own effects, §22.15) stays **down** on the tile it fell on — its own home when it fell on another board piece's home (that piece on the field or not) and its home is free — until it redeploys there, and no ally deploys or moves onto that tile meanwhile (PRTS 卫戍协议/帮助 §作战阶段 单位部署; `Battle.downOn` / `restTile` / `_layBody`, §21.24; b.snap `down`, §18.3: counting / waiting for DP / its tile taken, a safeguard) — also one that entered 联防 knocked out (`FORCED_EXIT`, its full redeploy timer — confirmed by the user — with the battle-start redeploy effects, §19.3); summons, devices and enemies leave at once.
|
||||
- **Blocking** (contact radius, PRTS 游戏数据基础 §阻挡半径 / 作战机制 "中点判定…案例:阻挡"; `constants.js BLOCK_RADIUS`, `Battle._checkBlock`, §19.2): an operator with free capacity blocks an unblocked, blockable enemy whose position lies within its block radius of its centre — ground 0.7071 (squared 0.49999037), 0.8944 for blockFly units vs flyers (起飞), devices 0.4472 (阻隔工事 / 障碍物); air units (FLY, 近地悬浮, 浮空) only by blockFly units. A ground enemy is never blocked by a unit standing on a tile ground units cannot pass (围墙 / 围栏: PRTS 地形机制 "部署在其中的单位,若当前阻挡类型为'地面阻挡'则无法阻挡敌人" — an enemy pushed or pulled against the fence walks on; air blocking there stays [ASSUMED], §21.23) nor by an airborne (起飞) unit, which blocks flyers only (§21.22). The nearest such operator wins (ties: scan order [ASSUMED]). It is re-checked every tick, moving or not, so another operator in contact with room takes the enemy over when its blocker dies, is withdrawn or is stunned, and an operator whose capacity frees up grabs an overlapping enemy; one that finds no room walks on. A head-on enemy stops at contact (≈ 0.71 tile), outside the blocker's tile. Enemy uses `blockCnt` capacity (default 1). Blocked enemies stop moving and attack their blocker if melee (or anyone in range if ranged, blocker first). **Every blocker** — melee units (重装要塞 / 领主 / 哨戒铁卫 included), summons and a ranged operator on a melee tile alike — whose attack hits enemies may always target the enemies it blocks, in range or not, whatever its facing, and targets them first ("可以选择且优先选择阻挡单位", PRTS 选择器; the user's rule after playtest #6, "阻挡了就一定要能打到": officially the collision pushes a blocked enemy to its blocker's front; `Battle.blockedTargets`, `targeting.js sortEnemyTargets`; §20.3 — it replaces the playtest #5 QA's melee-only restriction of §19.2). A healer (dmgType `heal`, or a skill attack turned into a heal) selects injured allies only and keeps healing while it blocks (PRTS 卫戍协议/帮助 "对于医疗干员(咒愈师分支除外),攻击目标为需要治疗的单位"; §21.17); 咒愈师 follow the blocker rule. The huge leaders are 自缚 + unblockable (§7).
|
||||
- **Operator attacks**: every `interval` s if a valid target exists; targets = up to `maxTargets` (default 1) from `unitsInGrid(rangeGrid)` (a `rangeAoe` profile — 阵法术师, 轰击术师 — takes every selectable enemy in range, §21.18), priority: (1) the enemies it blocks (every blocker, in range or not, §20.3), (2) profession/trait priorities (e.g. fly-first snipers, `trait` text parsed by build-data into `targetPriority`), (3) enemy with the **least remaining path distance** to its goal, ties → earliest spawned. Melee (no ranged trait) and ground-only attacks cannot hit air units (`Unit.isFlying`: FLY, or a 近地悬浮 / 浮空 enemy — PRTS "算作空中单位", §19.1). Stealthed enemies can't be targeted unless blocked or revealed — or within 3 s of a block's end (§22.8). Ranged ops fire projectiles (speed ~12 tiles/s, instant for melee/arts-beam classes as data indicates); damage applies on impact; a 锁定攻击范围 AoE (`rangeAoe`) strikes every selectable enemy on its range at once with instant hits (§21.18).
|
||||
@@ -325,7 +326,8 @@ 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 — with
|
||||
the layers their own combat reached (the round's pending gains, PRTS "以其阵地当前的状态" [ASSUMED]);
|
||||
a helper's operator knocked out at the end of its own combat enters down — carryState { down: true },
|
||||
PRTS "上一阶段为退场状态的干员强制退场", its full redeploy timer — confirmed by the user (§19.3))
|
||||
SETTLE (LP −min(leaks,10) per player (unite survivors to their source); eliminations; layer gains applied; coins; broadcast)
|
||||
@@ -705,7 +707,7 @@ Ten reports after playing v2.3 (绝境 on desktop, 标准 on an Android phone in
|
||||
- **Enemy kits**: 掠海漂移体 — 晕眩 / 冻结 / 沉睡 → 爬行模式 for good (0.5 s stun), then it only attacks its blocker; 不会攻击飞行单位. Its model follows (integration QA: melee operators hitting an enemy that still looked airborne): the sim's fx `form` `crawl` switches the view to the skeleton's crawl clips (`*_02`, after its `Change` clip) — `render/units.js FORMS`, `spine.js setForm`, kept on the unit info for a view built later (a view built mid-battle starts crawling: UnitInfo `form`, §21.4). 吉兆飞鳞 — 晕眩 / 沉睡 → 8 s 晕眩模式 (it stays down while still asleep), a freeze grounds it when the freeze ends, its sprint is the final ×3. Of the 32 official floaters only these two hover for good in this mode; 喷气人 hovers for its 7 s 飞行模式 (§21.9) (the bot's `HOVER_KEYS`; `fieldModel` counts them as air routes on ground paths).
|
||||
- **Skill effects** (per PRTS 备注; the official per-ability targetMotion is not in the data): ground-only — 隐德来希 S2 血镰, 归溟幽灵鲨 拥抱自我, 琳琅诗怀雅 S3, 乌尔比安 S1 [ASSUMED: a 捕网 like 雪雉's]; PRTS 可对空 — 德克萨斯 S2, 忍冬 S2, 焰尾 S2, 缄默德克萨斯 S3, 凛御银灰 S2, 玛恩纳 S3 (his melee skill attacks: `targeting.canHitFly`; §21.8), 锏 S3 归于宁静 (added by §20.3); no 对空 note, [ASSUMED] to hit air — 风丸 折纸生花, 乌尔比安 S3, 缄默德克萨斯 S2, 余 S2, 见行者 S2, 耀骑士临光 不畏苦暗 and the token appear bursts (“耀阳”, 沙之碑, 迷迭香的战术装备's stun, 纸偶). A stun / freeze / sleep from any of them drops a hovering 掠海漂移体.
|
||||
- **#5 official**: PRTS 索敌 (敌方) "阻挡→特殊优先级→仇恨值(更容易被攻击→…→最后部署的目标→不容易被攻击)→最早出现"; PRTS 卫戍协议/帮助 "按从上到下>从左到右的顺序部署。优先部署干员,随后为召唤物", the right-hand boss player mirrored.
|
||||
- **#5 changes**: `Battle.start` deploys operators row-major, then summons; `aggroSeq` ranks every start-of-battle summon after all operators (`deploySeq` stays the deployment identity). An enemy's own rule `e.profile.canTarget` filters its candidates before the sort (萨卡兹枯朽战车 no longer idles when its preferred target stands on high ground; it, 掠海漂移体 and “萨科塔之眼” ignore flying allies). Special priorities break ties by taunt, then latest deployed (`targeting.js aggroCmp`): 铳 / 昆图斯 highest DEF, 胄 highest / lowest ATK, the 碎铳之簧 bounce, 唱沙 / 沙狱 max HP, “自在” nearest. Kept (PRTS 作战机制 §隐匿 "我方干员并不会因为阻挡而解除隐匿"; 索敌的概念): a stealthed ally (隐匿 / 迷彩 / 排气格栅) is attacked only by the enemy it blocks (迷彩 is its own flag `camou`, not 隐匿 for 隐匿 conditions, §20.2). [ASSUMED] "从上到下>从左到右" = row-major; on a shared field (联防, boss) the players' i-th operators deploy together, then the summons (it used to be player after player, which made the leader prefer the later player's pieces).
|
||||
- **#5 changes**: `Battle.start` deploys operators row-major (by column since 0.1.3, §23.f4), then summons; `aggroSeq` ranks every start-of-battle summon after all operators (`deploySeq` stays the deployment identity). An enemy's own rule `e.profile.canTarget` filters its candidates before the sort (萨卡兹枯朽战车 no longer idles when its preferred target stands on high ground; it, 掠海漂移体 and “萨科塔之眼” ignore flying allies). Special priorities break ties by taunt, then latest deployed (`targeting.js aggroCmp`): 铳 / 昆图斯 highest DEF, 胄 highest / lowest ATK, the 碎铳之簧 bounce, 唱沙 / 沙狱 max HP, “自在” nearest. Kept (PRTS 作战机制 §隐匿 "我方干员并不会因为阻挡而解除隐匿"; 索敌的概念): a stealthed ally (隐匿 / 迷彩 / 排气格栅) is attacked only by the enemy it blocks (迷彩 is its own flag `camou`, not 隐匿 for 隐匿 conditions, §20.2). [ASSUMED] "从上到下>从左到右" = row-major (superseded in 0.1.3: a column scan, §23.f4); 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`
|
||||
@@ -997,7 +999,7 @@ Still [ASSUMED] (each in its section): the class rows covering every MANUAL skil
|
||||
- **Seen** (the user, first-hand, after playtest #6): "官方就是合成精锐时,如果消耗了场上的干员,精锐会出现在场上那个位置".
|
||||
- **Cause**: research 01 A1 row 7 read PRTS 帮助 as "1 elite goes to the hand, not to a board tile" (marked VERIFIED, repeated in `01-core-data.json` promotion.eliteGoesTo), so `_mergeChess` put every elite in the hand and used a consumed copy's tile only when the hand and temp were both full.
|
||||
- **Official** (PRTS 卫戍协议/帮助): §干员的获得与精锐化 "…发送1名【精锐】状态的该干员至手牌区(若消耗已部署至作战区的干员,则发送至作战区对应位置)"; tip "干员晋级后已配发装备会回收至整备区,需要重新配置"; §战斗部署 "如果部署的干员拥有可手动部署的附属召唤物,则该召唤物会立刻加入手牌区", deploy order "从上到下>从左到右". The text does not say which tile wins among several deployed copies.
|
||||
- **Now (server)**: when a consumed copy stood on the board the elite takes that tile and its facing, otherwise it goes to the hand, overflowing into temp (outside PREP too). Several deployed copies: the one that deploys first — row desc, then col asc, the mirrored right boss half included — `board.js mergeTile(tiles, legal)` [ASSUMED]. A 突变细胞 transformation destroys its carrier before the gain, so the carrier's freed tile is no copy's (§21.1 — official footage; the 0.1.1 WA rule that counted it, `transformChess` `fromKey` / `fromDir` [ASSUMED], is withdrawn). A tile the elite may not use is skipped; with the hand and temp also full the elite stays on the first deployed copy's tile rather than being lost (as `_evictIllegal`). The deploy count never grows (no BOARD_FULL path). Equipment returns to the hand, overflow temp; with both full it stays on the elite up to its `equipPerChess` (2) slots, any further item destroyed with a log warning (unchanged). The consumed copies' summons are removed; an elite on the board gets its own fresh stack (`grantTokensFor`, after the items) following the §16 loadout [ASSUMED: placed summons are not carried over]. Every path (g.buy, g.reward, `ctx.grantChess`, 画卷, transformations), every phase (a SETTLE / Final Assault merge places the elite at once, its reward offer waits for the next prep) and the boss-field prep (legality on the player's half, §19.7). Reward offer, ticker and `stats.merges` unchanged; `onMerge` carries `area` ('board' | 'hand' | 'temp'). Bots plan the elite like any owned unit.
|
||||
- **Now (server)**: when a consumed copy stood on the board the elite takes that tile and its facing, otherwise it goes to the hand, overflowing into temp (outside PREP too). Several deployed copies: the one that deploys first — col asc, then row desc since 0.1.3 (§23.f4; row desc, then col asc before), the mirrored right boss half included — `board.js mergeTile(tiles, legal)` [ASSUMED]. A 突变细胞 transformation destroys its carrier before the gain, so the carrier's freed tile is no copy's (§21.1 — official footage; the 0.1.1 WA rule that counted it, `transformChess` `fromKey` / `fromDir` [ASSUMED], is withdrawn). A tile the elite may not use is skipped; with the hand and temp also full the elite stays on the first deployed copy's tile rather than being lost (as `_evictIllegal`). The deploy count never grows (no BOARD_FULL path). Equipment returns to the hand, overflow temp; with both full it stays on the elite up to its `equipPerChess` (2) slots, any further item destroyed with a log warning (unchanged). The consumed copies' summons are removed; an elite on the board gets its own fresh stack (`grantTokensFor`, after the items) following the §16 loadout [ASSUMED: placed summons are not carried over]. Every path (g.buy, g.reward, `ctx.grantChess`, 画卷, transformations), every phase (a SETTLE / Final Assault merge places the elite at once, its reward offer waits for the next prep) and the boss-field prep (legality on the player's half, §19.7). Reward offer, ticker and `stats.merges` unchanged; `onMerge` carries `area` ('board' | 'hand' | 'temp'). Bots plan the elite like any owned unit.
|
||||
- **Now (client)**: `gameLogic.mergeTarget(priv, chessId)` mirrors `mergeTile`; a shop or reward card armed for a merge-completing buy lights the target tile in gold (`ShopBar onArm` → `screens/game.js` highlight group `mergeTile`); the 可晋升 tag says 精锐干员将出现在作战区原位置 or 精锐干员将进入整备区; an elite card never merges (`completesChessMerge` refuses `isGolden`, `mergeProgress` reports 0 copies: no pips, tag or tile); `render/promote.js promotionsOf` recognises a merge in setPrep and the elite plays `fx.promote` (gold pillar, shockwave, hex ring, streaks from the other copies) instead of `fx.deploy` (`view.debug.promotions`).
|
||||
- **Checks**: `audit.js` checks every merge — the tile is `mergeTile` of the deployed copies legal for the elite, with that copy's facing; the deploy count did not grow; otherwise the elite is not on the board, except the no-room fallback. `tools/matchrun --check` sweeps are clean.
|
||||
- Normative lines rewritten: §2 (`promote.js`), §6.2 (the economy line and the temp-overflow paragraph), §9 (the promotion cue).
|
||||
@@ -1106,7 +1108,7 @@ In SETTLE it is the field the last battle left on screen. In prep without watchi
|
||||
- The client lays them over the view's counts with `withLiveLayers`: the higher value wins (a count only grows) and never shows above 999. They apply in the battle phases and SETTLE only.
|
||||
- 联防 and boss battles disable gains, so they report none.
|
||||
- **After COMBAT** (`_finishCombat`): until SETTLE, both `m.private bonds` and `m.public players[].bonds` add the finished normal battle's IN_BATTLE gains (`PlayerState.pendingLayerGains` = the result's `layerGains`; `bondsMeta.bondsWithGains`: floored, at most up to the cap, like `settle()`). The strip of a player, and of a teammate watching him in the 联防, therefore keeps the layers his battle reached.
|
||||
- This is views only: `ps.bonds` / `ps.layers` are untouched. SETTLE clears the pending gains as it adds them (once), and the next round start clears them too.
|
||||
- `ps.bonds` / `ps.layers` are untouched. SETTLE clears the pending gains as it adds them (once), and the next round start clears them too. Since 0.1.3 the 联防 field fights with the same counts: the helpers' battle input takes its bonds from `bondsView()` (`battleInput({ reached: true })`, PRTS §联防阶段 "将以其阵地当前的状态" [ASSUMED] — it used to carry the round-start layers while the strip showed the reached ones); no other rule reads them.
|
||||
|
||||
**Data:**
|
||||
- `m.public players[].bonds = ps.alive ? bondList(gd, ps.bondsView()) : []`: every bond with members, layers or an active tier. It is the same list and order as the player's own `m.private bonds`, minus `thresholds` / `countsHand` (the client reads those from bonds.json).
|
||||
@@ -1497,7 +1499,7 @@ Public GitHub issues filed after the 0.1.1 release (2026-10-03), each triaged ag
|
||||
- **Seen** (GitHub #33 item 1, "阿戈尔阵营复活机制看起来不会触发"): the 5-tier revive works on its own (5 distinct 阿戈尔, one lethal hit each: the first 3 come back on their tile at full HP, normal and 联防 battles alike), but the usual food chain used it up at t = 0. One row, all facing right, 浊心斯卡蒂 → 乌尔比安 → 幽灵鲨 → 海霓 → 深巡 → 隐现: the battle-start devour knocked out 幽灵鲨, 海霓 and 深巡, the revive brought each straight back (all 3 charges), and 乌尔比安's marks still pending on them knocked them out a second time — down until their 70 / 80 s redeploy, no revive left for the fight (联防 battles alike).
|
||||
- **Official** (PRTS 卫戍协议:盟约 下半/PRTS盟约记录, 阿戈尔 备注): "标记按付与顺序触发【吞噬】效果,目标首次被击倒后解除自身被付与但还未触发的【吞噬】效果"; the 5-tier "前3名【阿戈尔】干员首次被击倒时立刻复活" (bonds.json `max_free_respawn_cnt` 3), implemented as "受益者因移动之外的原因退场时下次部署的再部署时间和费用归零". So a devour knock-out is a first knock-out and spends a revive, and the revived member takes no further mark.
|
||||
- **Cause**: `devour()` skipped a mark only while its target or marker was dead (`!t.alive || !m.alive`); a member revived in the same pass was alive again, so its pending marks fired. 埃芒加德's revives went the same way (it revives in place: the next pending mark knocked the revived unit out again and spent a second charge), and so did 不屈's 立刻重新部署 (the redeployed unit was knocked out again in the same instant, where 不屈 does not fire twice, and stayed down).
|
||||
- **Now**: when the marks are placed, the pass records each target's deployment (`items/battle.js deploymentOf`, the deploy counter plus the in-place revives — exported for this). A mark is skipped once its target is off the field or in another deployment, i.e. was knocked out during the pass — whether the 5-tier revive or 不屈's 立刻重新部署 redeployed it or 埃芒加德 / M3茧甲 revived it in place. The marker side is unchanged since 0.1.0 (PRTS describes only the target): a marker off the field gives no further mark, one knocked out and back in the same pass still gives its marks. The chain above: 浊心斯卡蒂's five marks resolve, the three members fall once each and revive, every later mark on them is cancelled; all five 阿戈尔 stand and only 隐现 falls. The 3 revives are still spent at t = 0, as officially. When nothing brings a devoured unit back during the pass (no 5-tier revive, 不屈 redeploy, 埃芒加德 or M3茧甲), the devour resolves exactly as before.
|
||||
- **Now**: when the marks are placed, the pass records each target's deployment (`items/battle.js deploymentOf`, the deploy counter plus the in-place revives — exported for this). A mark is skipped once its target is off the field or in another deployment, i.e. was knocked out during the pass — whether the 5-tier revive or 不屈's 立刻重新部署 redeployed it or 埃芒加德 / M3茧甲 revived it in place. The marker side is unchanged since 0.1.0 (PRTS describes only the target): a marker off the field gives no further mark, one knocked out and back in the same pass still gives its marks. The chain above: 浊心斯卡蒂's five marks resolve, the three members fall once each, every later mark on them is cancelled. (0.1.2 revived all three, spending the 3 revives at t = 0; since 0.1.3 they belong to the first 3 members by position, not to the first knocked out — here 幽灵鲨 comes back and 浊心斯卡蒂 / 乌尔比安 keep theirs for the fight, §23.f1.) When nothing brings a devoured unit back during the pass (no 5-tier revive, 不屈 redeploy, 埃芒加德 or M3茧甲), the devour resolves exactly as before.
|
||||
- **联防 (#33 item 3, not changed)**: the members knocked out twice at t = 0 were still down at the end of the 45–60 s own combat (70 s redeploy), so they entered every 联防 knocked out (forced out, §19.3) and the chain there formed without them. After this fix that is far rarer: with the 5-tier, the members the devour knocks out (up to 3) stand again and enter 联防 down only when the fight knocks them out once more (with 3–4 members no 阿戈尔 revive brings them back, as before). The order of the 联防 forced exit and the battle-start devour stays [ASSUMED] (§19.3: forced exit first); no source gives it.
|
||||
- **乌尔比安 S3 (#33 item 2) — seen**: "乌尔比安3技能结束返回会清空阿戈尔阵营增益,转职突袭后疑似不会返回(未转职时三技能会在原地留下一个图标,转职后无图标)". **Official** (PRTS 乌尔比安 S3 备注; the chess skill is `skchr_ulpia_3`, skill range 6-1 = his own tile and the 6 ahead): the anchor's target is "自身所在地块(仅阻挡敌人时)>技能范围内距离自身最近的包含敌人的地块>技能范围内距离最远的地块"; the 【移动】 goes to the landing tile > the tile beyond it > his own tile, and "若目标地块不为当前地块,会在原地部署一个从不混淆的方向"; "【移动】后仅继承下列效果:技能进度、第二天赋叠加层数". **Cause**: the anchor search started one tile ahead, so while he blocked — typical right after a 突袭 landing next to the enemies — he threw ahead, walked off the enemy he blocked (the blast could miss it — with an enemy one tile ahead its 1.8 radius still reaches the one he blocks) and left the marker. **Now**: while he blocks an enemy the anchor lands on his own tile: the blast (radius 1.8) hits what he blocks, he stays put, no marker, nothing to return from. His own tile is a candidate only while he blocks ("仅阻挡敌人时"): without a blocked enemy the search starts one tile ahead, exactly as in 0.1.1, so a flyer over his tile does not keep him there. Considered and not taken: reading the own tile of the 6-1 range as distance 0 for the second rule. Measured with the triage's batch (8 real stages × 5 round templates × 2 seeds, 突袭 through 变形同构体 + 突袭手雷), before the 突袭 landing change (#51, §22.2) is merged: with 突袭, 56 of 84 casts used to move him off his landing tile, now none does — 82 of the 84 came while he blocked, the other 2 (act2 m03, tile (10,8)) have no standable tile ahead (pre-existing); without 突袭, 64 of 80 casts still move him, as before.
|
||||
- **乌尔比安 S3 — not reproduced** (the triage, real data): no 阿戈尔 gain is cleared on the return. The devour's base ATK / block (`bond:egir:devour`, e.g. +399 ATK, +1 block), the 阿戈尔 HP and the 血脉的哺养 stacks are there before, during and after the S3 (`Battle.relocate` keeps every buff); what ends is the S3's own +40–60 % HP / +200–230 % ATK. With 突袭 he does return and the marker is drawn, both on the landing tile next to the enemies, not on his board tile (8 stages × 5 round templates × 2 seeds); with this fix a cast while he blocks there leaves no marker at all, as officially. **Deviation, kept**: officially the 【移动】 keeps only the skill progress and the second talent's stacks, so the official game drops the devour gains on the move; the remake keeps them through the 【移动】 and the return.
|
||||
|
||||
+7
-6
@@ -12,7 +12,7 @@ server/match/
|
||||
(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 on the deploy field (own board / boss half), slot helpers,
|
||||
deployment order
|
||||
reading order (boardOrder), a merge's elite tile in deployment order (mergeTile)
|
||||
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)
|
||||
@@ -342,7 +342,7 @@ Every handler method is `(ctx, ev)`; `ev` is shared by all handlers of one dispa
|
||||
| `onDestroy` | an item was destroyed (player / replaced) | `{ item, holder, reason }` |
|
||||
| `onLayers` | bond layers were added (prep or battle gains) | `{ bondId, from, to, reason }` (milestones: 维多利亚 25, 远见 10, 奇迹 100 …); `to` ≤ 999 (`BOND_LAYER_CAP`) — a gain at the cap dispatches nothing, so the milestones stop with the count |
|
||||
|
||||
Dispatch order per player: `global` → `band` → `bond` (data order) → garrisons (board in deployment order, then hand)
|
||||
Dispatch order per player: `global` → `band` → `bond` (data order) → garrisons (board in reading order — top row first, `board.js boardOrder` —, then hand)
|
||||
→ 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.
|
||||
@@ -511,7 +511,7 @@ Any `choice:` handler whose EffectRef reuses its own key must guard like this (o
|
||||
* **Merge**: 3 normal copies (风丸 2) anywhere (board/hand/temp) → 1 elite (the incoming copy, then temp, hand, board
|
||||
copies are consumed). Where it goes (PRTS 卫戍协议/帮助 "发送1名【精锐】状态的该干员至手牌区(若消耗已部署至作战区的干员,
|
||||
则发送至作战区对应位置)", the user's playtest #6 follow-up): when a consumed copy stood on the board, onto that copy's
|
||||
tile with its facing — of several, the one that deploys first (row desc, then col asc; `board.js mergeTile`,
|
||||
tile with its facing — of several, the one that deploys first (col asc, then row desc; `board.js mergeTile`,
|
||||
[ASSUMED]); a 突变细胞 carrier is destroyed before its gain, so its freed tile is no copy's (`transformChess`, DESIGN
|
||||
§21.1); a tile the elite may not use (a stale terrain change) is skipped. It replaces a deployed copy, so the deploy count never
|
||||
grows (no BOARD_FULL), and as a deployment its manually deployable summons join the hand (`grantTokensFor`, the
|
||||
@@ -702,9 +702,10 @@ spawn tile `start`, where the boss-field prep shows it —, + its bounties).
|
||||
Bond layers in the views (DESIGN §20.15): from the end of COMBAT (`_finishCombat`, every normal result in) until SETTLE,
|
||||
`m.private bonds` and `m.public players[].bonds` add the finished battle's IN_BATTLE gains (`PlayerState.pendingLayerGains`
|
||||
= the result's `layerGains`; `bondsMeta.bondsWithGains`: floored, at most up to `BOND_LAYER_CAP`, like the settlement), so
|
||||
the strip keeps the layers the battle reached through the COMBAT_END pause and the 联防. Views only: `ps.bonds` /
|
||||
`ps.layers` (rules, the 联防 spec, `activatedLayers`) are untouched; SETTLE clears the pending gains as it adds them to
|
||||
`ps.layers` (once); the next round start clears them too.
|
||||
the strip keeps the layers the battle reached through the COMBAT_END pause and the 联防. The 联防 field fights with the
|
||||
same counts (its input's `bonds` come from `bondsView()`, PlayerState.battleInput `reached`; since 0.1.3). `ps.bonds` /
|
||||
`ps.layers` (rules, `activatedLayers`) are untouched; SETTLE clears the pending gains as it adds them to `ps.layers`
|
||||
(once); the next round start clears them too.
|
||||
|
||||
`m.field` = `{ fieldId, kind, rect, stageId, units, live }`; during prep `g.watch 'n:<pid>'` returns a one-shot board
|
||||
view with `prep: true` (scouting a teammate).
|
||||
|
||||
+4
-4
@@ -18,7 +18,7 @@
|
||||
| 选择策略 | 40 名策略(分队长)决定**初始目标生命值(20–45)**和一条永久规则。同盟:随机顺序轮流选,**每人 30 秒**,右上角和「决策中」显示同一个倒计时;AI 队友立即选完;队友已选的不能重复,每人可**跳过一次**(排到最后);超时自动选你**当前选中**的策略(已被队友选走则选华法琳)。独立:自由选择、不限时;同盟模拟只有你一名玩家时也不限时。选的时候可以点左侧决策顺序下方的「查看禁用盟约与干员」,再看一遍「确认本局信息」里的盟约(置灰、禁用人数)和本局禁用干员:只能查看,倒计时照常走,底部也显示轮到谁和剩余秒数;点「关闭」、点窗口外面或按 Esc 关闭,轮到下一位时会自动关闭。 |
|
||||
| 机变 | 部分回合开始前出现:同盟共 6 张卡轮流各取 1 张(首位 30 秒,其余 16 秒;只有你一名玩家时不限时),独立 3 选 1。卡片包括装备、资金、干员、盟约层数、悬赏等;**悬赏决策**为你的作战添加悬赏敌人,卡上用蓝字标出持续多久:「下场作战」/「接下来两场作战」的只出现一场或两场,之后不再出现;官方的「多轮悬赏」(原文写「之后的每场作战」)在这里也按「接下来两场作战」处理,只出现两场(如教鞭的法术大师A2·多轮战术特训)。各回合的悬赏决策按官方对局截图来出:第 3 回合是 6 张「接下来两场作战」的悬赏(3 张 1 资金、2 张 2 资金、1 张 3 资金);第 9 回合是各首领的「下场作战」悬赏和「源石虫·特训」;绝境 / 终极的第 11 回合多半是悬赏决策(也可能是机密商店或战术决策),其中的悬赏通常有 1 只特异巨兽,其余是各阵营的「下场战斗」悬赏。击倒悬赏敌人的人(联防中的队友也算)获得资金;右侧效果栏会写「还剩 N 场作战」。**机密商店**的 6 件装备都免费,和官方一样可能出现两件相同的装备(各算一张卡,可以分别被选走);第 11 回合(绝境 / 终极)的机密商店按官方:2 件 VI 阶、至少 1 件 V 阶和 1 枚盟约之币,标准 / 险境较早回合的机密商店各阶装备都可能出现。**战术决策**也和官方一样可能出现两张相同的卡(官方截图里出现过两张「补给」;各算一张卡,可以分别被选走,每张只生效一次,两人各选一张全队卡则效果叠加);第 11 回合(绝境 / 终极)的战术决策按官方截图只出盟誓、驰援、列装 / 财富 / 补给 / 整备 / 升华等增益卡,不出削弱敌人的卡和战场演变卡。每张卡左上是图标和名称,下面写着完整效果(与详情卡相同的官方描述,手机上也显示);已被选走的卡变暗,右上角显示选走它的玩家头像(悬停可看名字)。选卡要**点两次**:第一次只选中(金框 +「确认选择 · 再次点击」),再点同一张或点右上的「确认选择」才生效;点别的卡换选,按 Esc 或点空白处取消。 |
|
||||
| 休整期 | 购买、摆阵、配装备、冻结 / 刷新、升级调度中心;同盟有倒计时(65–215 秒,随回合增加);独立模拟、以及只有你一名玩家(只带 AI 队友)的同盟模拟不限时。所有人「准备就绪」即提前开战,全员就绪前可取消。 |
|
||||
| 作战 | 每人在自己的战场各自作战(战斗在你的浏览器里模拟,2 倍速)。干员按**从上到下、从左到右**的顺序自动部署并消耗部署费用(初始 10,+1/秒)。时限 45–115 秒(实际秒数,即屏幕上的倒计时),时间到仍存活的敌人算漏怪。有敌人进入蓝门时,顶栏的目标生命值会**立即**显示扣除后的数值(红字 + 「−N」,每回合至多 10 点),结算时确定;联防阶段标注「联防中」,队友拦下的部分最后不扣。左侧队伍栏同样显示队友的待扣生命值。被击倒的干员**留在倒下的格子上**(倒地姿势、略微变灰),之后也在这一格再部署(与官方一样;倒在其他干员或召唤物初始位置上的,会回到自己的初始位置躺下,召唤物已经消失也一样);因自身效果强制退场的干员(史尔特尔「余烬」结束、耀骑士临光 2 技能结束等)同样倒在原地、在原地再部署;倒地干员所在的格子**不会被占用**,突袭盟约的再部署、援军、召唤物都会避开它。头上的圆圈是再部署倒计时:绿色圆环 + 秒数;变成黄色「DP」表示部署费用不够。血条下方的一行是**元素损伤**(与原版一致:元素图标 + 白条):干员为带图标的彩色圆盘,敌人为小号纯色圆盘,右侧白条是剩余元素值,像血条一样从右往左减少,白条耗尽的那一下就是爆条(被大额元素损伤一次打掉一截时白条也会一次少一截,例如掠海漂移体每击约为 25%–50%);爆条后进入爆发冷却(灼燃 / 神经 / 侵蚀 10 秒,敌方侵蚀 8 秒,凋亡 15 秒),这时白条变成**元素颜色**(神经青绿、侵蚀蓝、灼燃橙红、凋亡灰紫)并在冷却期间慢慢回满、图标闪烁——彩色条表示已经爆条、正在承受爆发效果,不是还剩的元素值;期间所有元素都不会累积也无法被治疗,结束后清零。元素爆发造成的是**无来源伤害**,不吃干员的增伤、穿透和装备加成(例如深巡打海嗣时灼燃爆发固定 7000),击杀仍记在爆条的干员名下;元素脆弱只提高元素伤害,不加快元素条;脆弱不影响元素伤害。已被击杀的敌人不会再被附加元素损伤。 |
|
||||
| 作战 | 每人在自己的战场各自作战(战斗在你的浏览器里模拟,2 倍速)。干员按**逐列从左到右、每列从上到下**的顺序(官方「从上到下>从左到右」:最左一列最先)自动部署并消耗部署费用(初始 10,+1/秒)。时限 45–115 秒(实际秒数,即屏幕上的倒计时),时间到仍存活的敌人算漏怪。有敌人进入蓝门时,顶栏的目标生命值会**立即**显示扣除后的数值(红字 + 「−N」,每回合至多 10 点),结算时确定;联防阶段标注「联防中」,队友拦下的部分最后不扣。左侧队伍栏同样显示队友的待扣生命值。被击倒的干员**留在倒下的格子上**(倒地姿势、略微变灰),之后也在这一格再部署(与官方一样;倒在其他干员或召唤物初始位置上的,会回到自己的初始位置躺下,召唤物已经消失也一样);因自身效果强制退场的干员(史尔特尔「余烬」结束、耀骑士临光 2 技能结束等)同样倒在原地、在原地再部署;倒地干员所在的格子**不会被占用**,突袭盟约的再部署、援军、召唤物都会避开它。头上的圆圈是再部署倒计时:绿色圆环 + 秒数;变成黄色「DP」表示部署费用不够。血条下方的一行是**元素损伤**(与原版一致:元素图标 + 白条):干员为带图标的彩色圆盘,敌人为小号纯色圆盘,右侧白条是剩余元素值,像血条一样从右往左减少,白条耗尽的那一下就是爆条(被大额元素损伤一次打掉一截时白条也会一次少一截,例如掠海漂移体每击约为 25%–50%);爆条后进入爆发冷却(灼燃 / 神经 / 侵蚀 10 秒,敌方侵蚀 8 秒,凋亡 15 秒),这时白条变成**元素颜色**(神经青绿、侵蚀蓝、灼燃橙红、凋亡灰紫)并在冷却期间慢慢回满、图标闪烁——彩色条表示已经爆条、正在承受爆发效果,不是还剩的元素值;期间所有元素都不会累积也无法被治疗,结束后清零。元素爆发造成的是**无来源伤害**,不吃干员的增伤、穿透和装备加成(例如深巡打海嗣时灼燃爆发固定 7000),击杀仍记在爆条的干员名下;元素脆弱只提高元素伤害,不加快元素条;脆弱不影响元素伤害。已被击杀的敌人不会再被附加元素损伤。 |
|
||||
| 联防 | 见第 5 节。 |
|
||||
| 结算 | 扣 LP、发放悬赏资金、结算层数;LP ≤ 0 的玩家淘汰(其干员回到公共卡池)。 |
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
- **购买点两次**:第一次点商店卡片只选中(卡片抬起并打开详情,底部出现「确认购买 · 再次点击」),再点这张卡片的**任意位置**(包括金色的确认条)才购买;点别处或按 Esc 取消。升级调度中心、晋升奖励同样两次;策略或装备带来的免费挑选(如凯瑟琳【定向投放】升级后刷新的 3 件装备、娜仁图亚【见者有份】的 2 件装备、寻呼模块的 3 名干员)也一样出现在商店卡片的位置,左侧标题写着它的来源,装备显示为装备卡(图标、名称、说明、FREE),选 1 个免费获得;同时有多个免费挑选时依次出现,标题下和「稍后」后的提示按钮会写还有几项(如「+1」)。不能购买时(例如已准备就绪)点卡片只打开详情。
|
||||
- **公共卡池**(同盟):每名干员的副本数有限,I–VI 阶分别 12 / 14 / 18 / 16 / 8 / 5 份,所有玩家共用;持有的干员(棋盘 + 整备区)占用副本,商店里展示的不占用。一名干员被大家拿光后就刷不出来了;淘汰玩家的干员会还回卡池。
|
||||
- **位置上限**:棋盘最多部署 **8** 名;整备区 **10** 格;整备区满时新获得的单位(合成的精锐、奖励、退回的装备等)进入**临时整备区**(整备区前面的 5 格)。临时整备区有单位时**不能准备就绪**,棋盘上会用红色虚线框标出这一排并说明规则:把它们放进整备区或战场、配发或使用,或者出售 / 销毁。休整期中进入临时整备区的单位,若到**本休整期结束**时仍留在那里就会被自动处理(干员返还卡池、装备销毁);作战、结算期间或准备就绪之后才进入的(例如战斗奖励、结算时合成的精锐),会保留到**下一个休整期**结束,你一定有机会处理它们。
|
||||
- **晋升精锐**:3 名同名干员(无论是否部署)自动合成**精锐**(等级、技能、模组大幅提升;风丸只需 2 名),精锐不会再合成。如果被合成的干员里有**已经部署在战场上**的,精锐会直接出现在**那名干员的位置**(保持原来的朝向,不多占部署位;有两三名都在场上时,取最先部署的那名——从上到下、从左到右);否则精锐进入整备区(满了进入临时整备区)。原先配发的装备都退回整备区,需要重新配发;被合成干员的召唤物随之移除,场上的精锐会把自己的召唤物送进整备区。点选一张能合成的商店卡片时,精锐将出现的那一格会亮起金色边框,同时打开的详情卡里也会写明去向(「可晋升:精锐干员将出现在作战区原位置 / 将进入整备区」;详情卡会自动停靠在不遮挡这一格的一侧)。在回合开始或结算时因策略、奖励等完成的合成,精锐出现时同样会播放金色的晋升特效。
|
||||
- **晋升精锐**:3 名同名干员(无论是否部署)自动合成**精锐**(等级、技能、模组大幅提升;风丸只需 2 名),精锐不会再合成。如果被合成的干员里有**已经部署在战场上**的,精锐会直接出现在**那名干员的位置**(保持原来的朝向,不多占部署位;有两三名都在场上时,取最先部署的那名——最左一列优先,同一列从上到下);否则精锐进入整备区(满了进入临时整备区)。原先配发的装备都退回整备区,需要重新配发;被合成干员的召唤物随之移除,场上的精锐会把自己的召唤物送进整备区。点选一张能合成的商店卡片时,精锐将出现的那一格会亮起金色边框,同时打开的详情卡里也会写明去向(「可晋升:精锐干员将出现在作战区原位置 / 将进入整备区」;详情卡会自动停靠在不遮挡这一格的一侧)。在回合开始或结算时因策略、奖励等完成的合成,精锐出现时同样会播放金色的晋升特效。
|
||||
- **晋级奖励**:每次晋升后获得一次特殊招募:从 3 名 **高一阶**(最高 VI)、**互不相同**的随机干员中**免费**挑 1 名(该阶剩余干员不足 3 名时由低一阶补足)。它不能刷新、不能冻结、本回合结束失效。
|
||||
- 每回合商店会自动刷新未冻结的格子。
|
||||
- **「本回合首次主动刷新」按每名干员各自计算**(拉普兰德):她在本回合遇到的第一次主动刷新生效,在整备区里也算——先刷新、再买下拉普兰德,下一次刷新她仍会叠层;同一回合内用已经生效过的拉普兰德合成的精锐,本回合不再生效。
|
||||
@@ -70,7 +70,7 @@
|
||||
- **召唤物手动摆放**:把带可部署召唤物的干员放上棋盘后,它的召唤物会进入整备区(赫默的医疗探机、巫恋的诅咒娃娃、凯瑟琳的 2 个支援装置、浊心斯卡蒂的海嗣、伺夜的狼群、缪尔赛思的流形;同一干员的召唤物叠在一格,不占部署位),像干员一样拖到棋盘上并选方向。凯瑟琳的支援装置给**它面朝的那一格**上的干员加屏障,所以要放在干员旁边、朝向干员。**战术家的援军**(伺夜的狼群、缪尔赛思的流形,也就是选战术点)和官方一样**只能放在召唤者的攻击范围内**:拖动时只有这些格子会亮起;之后在原地给战术家换朝向时,不在新攻击范围里的援军会退回整备区(还在范围里的保持不动;整备区和临时整备区都满、援军无处可退时,这次换朝向不生效并提示「整备区已满」)。撤退或出售干员时它的召唤物一起移除;**移动干员到别的格子时(包括把召唤物拖到干员身上交换位置),它已摆放的召唤物会退回整备区**,需要重新摆放。整备区满时退回的召唤物进入临时整备区,休整期结束还没处理的会在下一回合重新回到整备区。
|
||||
- 作战中:海嗣、狼群、流形、支援装置开战时就在摆放的位置部署;医疗探机、诅咒娃娃开战时也在摆放的位置免费部署一次(持续 10 / 15 秒,和官方一样不需要先开技能),之后所属干员每次发动技能时再次在原位出现;所属干员被击倒、再部署回场时,技能给的那一个如果还没出现,会立刻出现。巫恋退场时诅咒娃娃随之撤退,赫默退场时已出现的医疗探机不受影响。医疗探机、诅咒娃娃、支援装置、海嗣没有摆放就不会出现;狼群、流形(战术家的援军)没有摆放时会在战术点出现。
|
||||
- **阻挡**看接触:干员阻挡中心距离约 0.7 格以内、与它接触的敌人(地面干员挡不住飞行和近地悬浮的敌人)。敌人卡在两名干员之间时,阻挡它的干员倒下、撤退或被晕眩后,另一名仍与它接触、还有空余阻挡数的干员会**立刻接替阻挡**;都没有空位时敌人才会走掉。迎面走来的敌人会停在干员格子前面。**阻挡了就一定能打到**:任何干员(包括站在近战位的远程干员和召唤物)总能攻击自己阻挡的敌人,不管它在不在攻击范围内、在干员哪一侧,并且优先攻击它(医疗干员除外:咒愈师以外的医疗挡住敌人时照常治疗队友,不会攻击它;「停止攻击」类技能期间也除外;技能原文另有限制的也按原文:普罗旺斯二技能开启时不以生命值高于 80% 的敌人为目标,寒檀二技能的冰凌随机落在攻击范围内的地块上)。
|
||||
- 开战时干员按**从上到下、从左到右**的顺序部署,召唤物在所有干员之后。被阻挡的敌人先打阻挡它的干员;没被阻挡的远程敌人先打嘲讽等级高的,同级时打最后部署的(召唤物、靠下靠右的干员和刚再部署的干员更容易挨打)。隐匿的干员只会被它阻挡的敌人攻击,没被它挡住的敌人的溅射、爆炸、范围技能、留下的伤害区域(包括假想敌:蚀裂被击倒后喷出的毒雾)以及深池伙友卫队、扎罗的减攻速光环也落不到它身上(和官方一样;写明「无视无法选择」的效果如污染秽蚀照常能打到,寒霜的减攻速光环和“墓碑”这类地图效果照常生效);**迷彩**(如忍冬三技能期间击倒敌人后、寒芒克洛丝一技能)同样只会被它阻挡的敌人攻击,也显示为半透明,但躲不开溅射、爆炸和范围伤害(同时打范围内所有单位的普通攻击——如“斩胄之剑”“破胄之锤”“灵幛”——打不到它),也不算隐匿(不触发叙拉古等「隐匿时」效果)。叙拉古的隐匿按时(32 秒 + 每层 0.4 秒)结束;忍冬的迷彩按官方持续到她下一次开技能。**蒂比起飞**期间只挡飞行敌人,地面敌人不会选她为目标(普通攻击、溅射、技能范围、燃烧区域和要选中目标的减益都落不到她身上);飞行和悬浮敌人、写明「无视无法选择」的效果(如污染秽蚀)照常能打到她;地面敌人的光环和全场效果(如“墓碑”让全场攻击力、防御力减半)、她攻击敌人时招来的反制(如重装侦察兵的暴露)照常生效,起飞前已挂上的持续伤害也照常跳。
|
||||
- 开战时干员按**逐列从左到右、每列从上到下**的顺序部署(最左一列最先,与阿戈尔吞噬「更靠左和靠上」的顺序相同),召唤物在所有干员之后。被阻挡的敌人先打阻挡它的干员;没被阻挡的远程敌人先打嘲讽等级高的,同级时打最后部署的(召唤物、最右一列靠下的干员和刚再部署的干员更容易挨打)。隐匿的干员只会被它阻挡的敌人攻击,没被它挡住的敌人的溅射、爆炸、范围技能、留下的伤害区域(包括假想敌:蚀裂被击倒后喷出的毒雾)以及深池伙友卫队、扎罗的减攻速光环也落不到它身上(和官方一样;写明「无视无法选择」的效果如污染秽蚀照常能打到,寒霜的减攻速光环和“墓碑”这类地图效果照常生效);**迷彩**(如忍冬三技能期间击倒敌人后、寒芒克洛丝一技能)同样只会被它阻挡的敌人攻击,也显示为半透明,但躲不开溅射、爆炸和范围伤害(同时打范围内所有单位的普通攻击——如“斩胄之剑”“破胄之锤”“灵幛”——打不到它),也不算隐匿(不触发叙拉古等「隐匿时」效果)。叙拉古的隐匿按时(32 秒 + 每层 0.4 秒)结束;忍冬的迷彩按官方持续到她下一次开技能。**蒂比起飞**期间只挡飞行敌人,地面敌人不会选她为目标(普通攻击、溅射、技能范围、燃烧区域和要选中目标的减益都落不到她身上);飞行和悬浮敌人、写明「无视无法选择」的效果(如污染秽蚀)照常能打到她;地面敌人的光环和全场效果(如“墓碑”让全场攻击力、防御力减半)、她攻击敌人时招来的反制(如重装侦察兵的暴露)照常生效,起飞前已挂上的持续伤害也照常跳。
|
||||
- **技能自动释放**按官方「技能策略」:一般的手动技能在技能就绪、即将攻击 / 治疗且初始攻击范围内有敌人(治疗技能:有受伤的队友)时释放;**带独立技能范围**的技能(如德克萨斯「剑雨」、凛御银灰「御敌的锋锐」、锏「归于宁静」,不含「攻击范围扩大」类)在**技能范围内出现敌人**时就释放,不必先攻击;**重装干员**的手动技能在受到伤害时释放(例外:深巡、雷蛇的二技能,号角的二、三技能,灰毫的一、二技能按玩家反馈改为攻击范围内有敌人时就释放,余的二技能「厚礼上宾」改为周围两格(技能范围)内有敌人时就释放,这是本作有意与官方不同的地方;深巡一技能仍要受到伤害);执旗手 / 战术家 / 吟游者技能就绪即释放,解放者 / 阵法术师在初始攻击范围内有敌人时释放。**自动触发**的技能按其本身规则,例如古米「备用军粮」在身边(技能范围内)有受伤的队友时就会发动,下一次攻击改为治疗该队友(不用先接敌;她的二技能「食粮烹制」是手动技能,按重装规则在受到伤害时释放);塞雷娅「急救」在她即将攻击时、身边有生命不高于一半的队友才发动,这次攻击直接改为治疗;新约能天使二技能的护盾按官方给攻击范围内**仇恨最高**的队友(嘲讽等级高的优先,同级时给最后部署的),不一定是站在最前面挨打的那个。手动技能的自动释放有 3 秒冷却:开战部署后先等 3 秒,之后同一干员两次自动释放至少间隔 3 秒(可充能的技能不会同时放完)。
|
||||
- **推开 / 拉拽**按官方「力度 − 重量」决定距离:例如野鬃二技能(中等力度)把重量 0 / 1 / 2 / 3 的敌人推开约 2.1 / 1.7 / 0.4 / 0.1 格,重量 4 以上推不动——轻型敌人反而推得更远。野鬃二技能沿她的朝向推开:正前方 45° 以内按上面的距离,身后、侧面或贴身的敌人只被轻推一下(约 0.4 格以内);琳琅诗怀雅三技能的金币则从她身边向外推开,落在她前方 2–4 格的范围(官方「范围2-4」)内的地面敌人和她阻挡的敌人身上。攻击速度最低为 20(沼泽、寒霜等减速叠满时也不会更慢)。
|
||||
- **元素损伤条**在单位自己的血条正下方:左边是元素图标,白条是剩余的元素值,只有真正爆发时才会耗尽;爆发后条变成元素的颜色、图标闪烁,在冷却期间慢慢回满(这段时间不会再积累元素损伤)。
|
||||
@@ -80,7 +80,7 @@
|
||||
|
||||
## 5. 联防(同盟)
|
||||
|
||||
某回合**有人漏怪、又有人完美作战**(全歼且无漏怪)时触发:最多 2 名完美作战的队友带着自己的阵容(与官方相同,只保留作战结束时的生命比例和技力:作战结束时还开着的技能在联防开场是关着的,技力是开技能后剩下的值,通常为 0;召唤物只保留技力;作战结束时已被击倒的干员在原位倒地(阿戈尔的开场吞噬仍把它当作站着的干员计算标记与基础攻击力、阻挡数,之后它照常退场),按官方的「强制退场」以完整的再部署时间重新倒计时,时间到且部署费用足够后照常再部署——联防较短时可能来不及再部署;机变「征召」这类开战时生效的再部署时间减免同样计入),迎击其他玩家漏掉的全部敌人。
|
||||
某回合**有人漏怪、又有人完美作战**(全歼且无漏怪)时触发:最多 2 名完美作战的队友带着自己的阵容(与官方相同,只保留作战结束时的生命比例和技力:作战结束时还开着的技能在联防开场是关着的,技力是开技能后剩下的值,通常为 0;召唤物只保留技力;作战结束时已被击倒的干员在原位倒地(阿戈尔的开场吞噬仍把它当作站着的干员计算标记与基础攻击力、阻挡数,之后它照常退场),按官方的「强制退场」以完整的再部署时间重新倒计时,时间到且部署费用足够后照常再部署——联防较短时可能来不及再部署;机变「征召」这类开战时生效的再部署时间减免同样计入),迎击其他玩家漏掉的全部敌人。盟约层数按本回合自己作战结束时达到的层数(即盟约栏显示的层数)计算,联防中不再叠加层数;这些层数在结算时只计入一次。
|
||||
|
||||
- 参加者按官方规则挑选:场上单位最多 > 有已激活的盟约 > 未倒下的单位最多 > 座位靠前(与 LP 无关);两人参加时,排在前面的一位(再比较激活盟约的层数总和)在右半场「率先迎敌」。
|
||||
|
||||
|
||||
+5
-4
@@ -59,9 +59,10 @@ 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 the **operators top→bottom
|
||||
then left→right** (right boss side: right→left in field columns), **then the summon pieces** the same way (PRTS
|
||||
卫戍协议/帮助 "按从上到下>从左到右的顺序部署。优先部署干员,随后为召唤物"; a skill's summon — 赫默's 医疗探机, 巫恋's
|
||||
`step()` (or `b.start()`) spawns stage crates, deploys all board units for free — per player the **operators by column, the
|
||||
left column first, top to bottom within a column** (right boss side: from its own left, the highest field column),
|
||||
**then the summon pieces** the same way (PRTS 卫戍协议/帮助 "按从上到下>从左到右的顺序部署。优先部署干员,随后为召唤物", a
|
||||
scan down each column — the 阿戈尔 devour's "更靠左和靠上"; row-major until 0.1.3; a skill's summon — 赫默's 医疗探机, 巫恋's
|
||||
诅咒娃娃 — included, once, free: content/tokens.js `dockSkillSummons` / `releaseSkillSummon`, user playtest #6) — except a
|
||||
summon piece content flags `deferDeploy` (one its owner's loadout does not make, e.g. 赫默 on S1; with
|
||||
`SKILL_SUMMON_START_DEPLOY` off also a skill's summon, which then waits on its reserved tile until the skill gives
|
||||
@@ -1143,7 +1144,7 @@ table: `hitSleep` (targets and damages sleeping enemies — "可以攻击沉睡
|
||||
| agent / hookmaster | can hit FLY (ranged reach); hook displacement comes from skills (generic: `force`) |
|
||||
| bearer | block 0 while the skill is active |
|
||||
| alchemist | ranged lob, can hit FLY |
|
||||
| dollkeeper | fatal damage (no 不死 — a running 坚固维式重锤 window comes first) ⇒ a 1 s switch animation [ASSUMED length, also on a direct switch] (无敌, 不死, 阻回, 禁疗, 孤立, 缴械, 眩晕 / 冻结 / 睡眠 immune; ends the running skill and removes the statuses), then the <替身> for 20 s (bb duration): block 0 from the switch on, 阻回, HP = its own 替身 token's (风丸 纸偶), else its own max HP (归溟幽灵鲨: PRTS resets it "至最大值", the trait's 替身 HP bonus `max_hp` is 0, 风丸's 纸偶 has her HP); then the switch back (the same animation) at full HP; dies if the 替身 dies. Kit flag `dollNoAttack` (归溟幽灵鲨): the 替身 makes no normal attack and casts no skill. Hook `dollSwitch` switches it at once (归溟幽灵鲨 S2's end). Model form `'doll'` (PRTS 分支特性信息 傀儡师, DESIGN §22.11) |
|
||||
| dollkeeper | fatal damage (no 不死 — a running 坚固维式重锤 window comes first) ⇒ a 1 s switch animation [ASSUMED length, also on a direct switch] (无敌, 不死, 阻回, 禁疗, 孤立, 缴械, 眩晕 / 冻结 / 睡眠 immune; ends the running skill and removes the statuses; the HP is set to the max at its start and again at its end — PRTS "切换途中重设自身生命至最大值" —, so a lethal 流失 inside it, held at 1 HP, never outlasts it), then the <替身> for 20 s (bb duration): block 0 from the switch on, 阻回, HP = its own 替身 token's (风丸 纸偶), else its own max HP (归溟幽灵鲨: PRTS resets it "至最大值", the trait's 替身 HP bonus `max_hp` is 0, 风丸's 纸偶 has her HP); then the switch back (the same animation) at full HP; dies if the 替身 dies. Kit flag `dollNoAttack` (归溟幽灵鲨): the 替身 makes no normal attack and casts no skill. Hook `dollSwitch` switches it at once (归溟幽灵鲨 S2's end). Model form `'doll'` (PRTS 分支特性信息 傀儡师, DESIGN §22.11) |
|
||||
| geek | loses 1–3 % max HP per second (bb hp_ratio), never lethal on its own |
|
||||
| merchant | −3 DP every 3 s (bb cost/interval); retreats when DP runs out |
|
||||
| skywalker | can block FLY enemies (蒂比's kit: only while airborne — 起飞, flag `liftoff`, which also releases the ground enemies she blocked) |
|
||||
|
||||
@@ -82,7 +82,7 @@ The visible chess per tier are 16/17/19/22/19/19. The item slot uses the same ti
|
||||
| Deploy cap | **8** (`characterLimit`); **9** with 人事部文档. Summons don't use a slot (PRTS token pages of the pool's hand summons: 部署占用数 0; user playtest #6). | DATA/VERIFIED |
|
||||
| Hand (整备区) | **10 regular + 5 temporary**, shared by operators, items and summon stacks. Full ⇒ no buy or withdraw. Passive overflow goes to temp; **temp blocks Ready**; temp is wiped at the next phase. Board↔hand swap is allowed when full. | VERIFIED |
|
||||
| Equipment | 2 per operator (board **or** hand). Locked; 3rd = replace, and the old one is destroyed. 2 identical normal items (equipped or not) merge into golden. | VERIFIED |
|
||||
| Deployment | Free, at t=0, order **top→bottom then left→right** (right-side boss player mirrored). Ranged units may use melee tiles. | VERIFIED |
|
||||
| Deployment | Free, at t=0, order **从上到下>从左到右** = down each column, the columns left to right (right-side boss player mirrored; research 01 §4.3). Ranged units may use melee tiles. | VERIFIED |
|
||||
| DP | Start **10**, **+1/s**, cap **99**. Used only for auto-redeploy (original tile, respawn timer done, DP ≥ `cost`). | DATA/VERIFIED |
|
||||
| Skill auto-cast | Default: SP ready + about to attack/heal + an enemy in the **initial** range. ~~`skillTriggerDataList` rows with `skillIndex` 0 = skill 1 only (TANK S1 → on damage: only 古米 and 灰毫 this season)~~ — Superseded by user playtest #6 (DESIGN §20): the class rows apply to **every MANUAL skill** of the class and never to an AUTO skill (PRTS 卫戍协议/帮助 names whole classes; the 阵法术师 row must cover 薄绿's default S2, a phalanx that never attacks with its skill off); a MANUAL skill with its own 技能范围 (not an attack-range change) uses SKILL_RANGE; automatic operations have a 3 s cooldown. charId rows per 03. The TANK row came with 下半 (上半 had none); 深巡 S2, 雷蛇 S2, 号角 S2 / S3 and 灰毫 S1 / S2 deliberately cast with an enemy in range instead (the owner's decision, DESIGN §21.29); 余 S2 too, on its x-1 (SKILL_RANGE, the owner, 2026-10-04, DESIGN §22.10). | VERIFIED (class-row reading superseded; six 重装 skills deviate on purpose) |
|
||||
| Speed | 2× forced, no manual control | VERIFIED |
|
||||
|
||||
@@ -9562,7 +9562,7 @@
|
||||
"promotion": {
|
||||
"eliteGoesTo": "hand; when a consumed copy was deployed, that copy's board position (PRTS 卫戍协议/帮助 '发送1名【精锐】状态的该干员至手牌区(若消耗已部署至作战区的干员,则发送至作战区对应位置)'; user-confirmed after playtest #6)",
|
||||
"eliteTileAmongSeveralDeployed": {
|
||||
"rule": "the consumed deployed copy that deploys first (board row desc, then col asc; mirrored on the right boss half), with its facing",
|
||||
"rule": "the consumed deployed copy that deploys first (board col asc, then row desc — the deployment order since 0.1.3; mirrored on the right boss half), with its facing",
|
||||
"assumed": true
|
||||
},
|
||||
"equipmentReturnsTo": "hand",
|
||||
|
||||
@@ -213,8 +213,8 @@ Boss HP scaling:
|
||||
Each player fights their own copy of the round level independently. There is no PvP.
|
||||
- **Deployment:** automatic, paying DP (start 10, +1/s). **Redeploy also costs DP** [COMM].
|
||||
- **Deploy order:**
|
||||
- 上半: left→right, bottom→top.
|
||||
- 下半: top→bottom, then left→right. The last-placed unit has the highest taunt.
|
||||
- 上半: left→right, bottom→top (PRTS "从左到右>从下到上": along each row, the rows from the bottom).
|
||||
- 下半: PRTS 卫戍协议/帮助 "按从上到下>从左到右的顺序部署" (from 2026-03-14): a scan down each column (top first), the columns from left to right — the order PRTS gives the 阿戈尔 devour ("从最先部署(更靠左和靠上的)的【阿戈尔】干员开始") and act-2 videos show (BV1rVwjziEQt, BV1pGAPzrEpZ). The remake read it as top row first until 0.1.3. The last-placed unit has the highest taunt.
|
||||
- **Skills:** fire automatically. Units with summons deploy the summon on first deployment regardless of SP [COMM].
|
||||
- **Perfect clear (完美作战):** every enemy killed before the time limit, none reached the objective.
|
||||
- **Bond stacking:**
|
||||
@@ -227,6 +227,7 @@ Each player fights their own copy of the round level independently. There is no
|
||||
- **Setup:** 1–2 players who *did* clear perfectly field their board as-is. They face the **union of all enemies the other players leaked**, using the escaped-battle template `level_act1autochess_escaped_multi` (char limit 20).
|
||||
- **Rules:**
|
||||
- Bond stacking is disabled ("该阶段不能叠加层数").
|
||||
- The helpers fight "以其阵地当前的状态" (PRTS 帮助): the remake reads that as including the layers their own combat reached that round (the count the strip shows) [ASSUMED]; the settlement adds those gains once.
|
||||
- Bounty kills here pay the killer.
|
||||
- Only perfect-clear teammates can defend (tip).
|
||||
- Community: in 下半 the result shown is the *defended* player's view.
|
||||
@@ -842,11 +843,11 @@ Local copies are in `scratchpad/gd/extra/critic/` (`bw_盟约.txt`, `bw_sweep.tx
|
||||
| new | – | At **combat start, all unfrozen shop contents are cleared** ("进入作战期后,栏上若未处于冻结状态则清空全部物资"). | VERIFIED |
|
||||
| 4.2 / 8 | bench 10 + temp area | **Hand (手牌区/整备区) = 10 regular + 5 temporary slots**, shared by operators, **items** and hand-deployable **summons**. Several summons of one owner stack into 1 slot; summons of different owners don't stack.<br>When the regular hand is full, every action that would add a card is refused: buy, and withdraw from the board. Exception: a purchase that completes a 3-merge nets −1 card, so allow it.<br>Passive gains (merge result, band/trait/item grants, items returned by promotion) overflow into the temp slots. **While temp is non-empty the player cannot start combat.** Temp is wiped when the next phase starts.<br>A board↔hand swap is allowed even with a full hand. Cards fill hand slots **right→left**. | VERIFIED |
|
||||
| 5.2 | – | **Items cannot be sold** ("多余的装备无法进行出售,可以销毁"). They can only be destroyed for 0 funds. Operators sell for 1 from board or hand. | VERIFIED |
|
||||
| 7 | promotion reward [COMM] | Confirmed (PRTS 帮助, BWIKI, official intro). On acquiring the 3rd copy:<br>1. The 3rd copy is not created, and the 2 owned copies are destroyed wherever they are.<br>2. **1 elite goes to the hand — or, when a consumed copy was deployed, to that copy's board position** (PRTS 帮助 "发送1名【精锐】状态的该干员至手牌区(若消耗已部署至作战区的干员,则发送至作战区对应位置)"; the user confirmed it first-hand after playtest #6 — this row used to read "not to a board tile", a misreading of the same sentence). Their equipment returns to the hand ("干员晋级后已配发装备会回收至整备区,需要重新配置"). [ASSUMED] where the text is silent: with two or three deployed copies the elite takes the tile of the one that deploys first (top→bottom, then left→right — the official deploy order) with its facing; the copies' summons leave and the elite's own summon card joins the hand (§战斗部署 "如果部署的干员拥有可手动部署的附属召唤物,则该召唤物会立刻加入手牌区"); the deploy count never grows. A 突变细胞 carrier is destroyed before its new operator is gained (official footage, DESIGN §21.1), so a merge that operator completes never uses the carrier's tile.<br>3. The shop then temporarily shows **3 operators of tier min(shopLevel+1, 6) at price 0**. The official intro shows "II 0 ×3" after a tier-I merge at L1 and "III 0 ×3" at L2.<br>4. The player takes **1**. The offer cannot be refreshed or frozen and expires at round end. | VERIFIED (elite to the copy's board position: PRTS + the user); the tile among several deployed copies ASSUMED |
|
||||
| 7 | promotion reward [COMM] | Confirmed (PRTS 帮助, BWIKI, official intro). On acquiring the 3rd copy:<br>1. The 3rd copy is not created, and the 2 owned copies are destroyed wherever they are.<br>2. **1 elite goes to the hand — or, when a consumed copy was deployed, to that copy's board position** (PRTS 帮助 "发送1名【精锐】状态的该干员至手牌区(若消耗已部署至作战区的干员,则发送至作战区对应位置)"; the user confirmed it first-hand after playtest #6 — this row used to read "not to a board tile", a misreading of the same sentence). Their equipment returns to the hand ("干员晋级后已配发装备会回收至整备区,需要重新配置"). [ASSUMED] where the text is silent: with two or three deployed copies the elite takes the tile of the one that deploys first (the left column first, top to bottom — the official deploy order, §4.3) with its facing; the copies' summons leave and the elite's own summon card joins the hand (§战斗部署 "如果部署的干员拥有可手动部署的附属召唤物,则该召唤物会立刻加入手牌区"); the deploy count never grows. A 突变细胞 carrier is destroyed before its new operator is gained (official footage, DESIGN §21.1), so a merge that operator completes never uses the carrier's tile.<br>3. The shop then temporarily shows **3 operators of tier min(shopLevel+1, 6) at price 0**. The official intro shows "II 0 ×3" after a tier-I merge at L1 and "III 0 ×3" at L2.<br>4. The player takes **1**. The offer cannot be refreshed or frozen and expires at round end. | VERIFIED (elite to the copy's board position: PRTS + the user); the tile among several deployed copies ASSUMED |
|
||||
| 2 | BAND_CHECK "3 random + reroll" [ASSUMED] | **All unlocked bands are shown.** In co-op, players pick **one at a time in a random order** shown on the left ("以随机顺序进行策略轮选"). Each co-op player may **skip once**. The step lasts 50 s; on timeout the band is 华法琳. **Duplicates are allowed:** the official illustration shows 2 teammates' picks still un-greyed in the grid. [ASSUMED] 12 s per turn inside the 50 s step. | VERIFIED except the per-turn timer |
|
||||
| 2 | banned bonds "1 core + 1 add-on" [ASSUMED] | Replaced by the verified rule in **A2**. | VERIFIED |
|
||||
| 1, 12 | one ×0.7/×0.8 multiplier per mode; `aceffect_enemy_*` mapping [ASSUMED] | Replaced by the **per-round table in A3**. | VERIFIED (PRTS, user-sourced) |
|
||||
| 4.3 | "automatic, paying DP" | At battle start **all board units deploy for free**, one by one in order: 下半 top→bottom, then left→right; right-side players in the boss round are mirrored.<br>**DP starts at 10, +1/s, cap 99.** These are the round-level `options`. `initialCost` 0 in `m0X` is the terrain file, not the round level.<br>DP is only spent on **automatic redeploy**: the unit returns to its original tile when its respawn timer is done, the tile is free, and DP ≥ `cost`.<br>Precisely (PRTS 卫戍协议/帮助 §作战阶段 单位部署, checked after player report F5): the unit comes back on the tile where its 倒地干员 lies ("原地留下一个“倒地干员”…满足再部署条件时,移除场上的该倒地干员并自动部署至该位置"). That is its original tile unless it was moved off it (a 突袭 jump, 乌尔比安's anchor). No ally may deploy on that tile ("倒地干员所在地块视为可部署,但所有我方单位在此处的部署行为将被阻止"). One knocked out on another piece's initial position goes back to its own ("若干员被击倒的位置为其他干员或召唤物的初始位置,则在被击倒后,尝试返回其自身的初始位置").<br>Deploy effects do not fire during prep placement. Battle speed is forced to **2×**. | VERIFIED |
|
||||
| 4.3 | "automatic, paying DP" | At battle start **all board units deploy for free**, one by one in order: 下半 "从上到下>从左到右" — down each column, the columns left to right (§4.3); right-side players in the boss round are mirrored.<br>**DP starts at 10, +1/s, cap 99.** These are the round-level `options`. `initialCost` 0 in `m0X` is the terrain file, not the round level.<br>DP is only spent on **automatic redeploy**: the unit returns to its original tile when its respawn timer is done, the tile is free, and DP ≥ `cost`.<br>Precisely (PRTS 卫戍协议/帮助 §作战阶段 单位部署, checked after player report F5): the unit comes back on the tile where its 倒地干员 lies ("原地留下一个“倒地干员”…满足再部署条件时,移除场上的该倒地干员并自动部署至该位置"). That is its original tile unless it was moved off it (a 突袭 jump, 乌尔比安's anchor). No ally may deploy on that tile ("倒地干员所在地块视为可部署,但所有我方单位在此处的部署行为将被阻止"). One knocked out on another piece's initial position goes back to its own ("若干员被击倒的位置为其他干员或召唤物的初始位置,则在被击倒后,尝试返回其自身的初始位置").<br>Deploy effects do not fire during prep placement. Battle speed is forced to **2×**. | VERIFIED |
|
||||
| 4.3 | leaked/timeout | When `maxPlayTime` ends, the battle stops. Every enemy not killed (still alive, or reached the goal) is "未击倒" and feeds 联防/LP. [ASSUMED] `maxPlayTime` is **game-time** seconds, so the client at 2× shows it ticking twice as fast. Last spawns land at 15/38/73 s against limits of 45/55/115 s, which leaves the time needed to kill walkers. | VERIFIED + ASSUMED unit |
|
||||
| 3 | solo prep 300 s | 下半 solo has **no timer** on prep or 机变 ("休整期及机变阶段没有时间限制…24小时内随时返回"). Treat `normalPhaseTime` 300 and `specialPhaseTime` 150 as an optional "timed solo" setting. | VERIFIED |
|
||||
| 4.4 | 联防 partner rule "fewest leaks" | Meaningless, because helpers are perfect by definition. There are **at most 2 helpers**: "所有防卫成功的玩家(最多2名)". Default choice: highest LP, then seat index [ASSUMED]. 1 helper uses `escaped_single` (8 units); 2 helpers use `escaped_multi` (20). | VERIFIED cap |
|
||||
|
||||
@@ -2727,7 +2727,7 @@
|
||||
},
|
||||
{
|
||||
"need": "5 distinct",
|
||||
"effect": "The first 3 阿戈尔 members to be knocked out (first time each) revive immediately (max_free_respawn_cnt 3)."
|
||||
"effect": "The first 3 阿戈尔 members by position (the devour order: leftmost first, then topmost; fixed at battle start) each revive immediately on their own first knock-out, whatever the order of knock-outs (max_free_respawn_cnt 3)."
|
||||
}
|
||||
],
|
||||
"algorithm": [
|
||||
|
||||
@@ -373,7 +373,7 @@ Members (11 chess, 10 in current shop pool; by tier in shop: {'1': 1, '2': 1, '3
|
||||
|
||||
- **[3 distinct]** 阿戈尔 members maxHP x(1 + 0.35 + 0.01*L).
|
||||
- **[3 distinct (battle start)]** Devour (吞噬): see algorithm.
|
||||
- **[5 distinct]** The first 3 阿戈尔 members to be knocked out (first time each) revive immediately (max_free_respawn_cnt 3). Devour knock-outs count: a chain that knocks out 3 members spends the 3 revives at battle start (and those members stay standing, step 4).
|
||||
- **[5 distinct]** The first 3 阿戈尔 members by position — the devour order below (step 1), i.e. the deployment order, fixed at battle start — each revive immediately on their own first knock-out, whatever the order of knock-outs (max_free_respawn_cnt 3; PRTS 阿戈尔 备注 "从最先部署(更靠左和靠上的)的【阿戈尔】干员开始", players' videos; until 0.1.3 the remake gave them to the first 3 members knocked out). A devour knock-out of one of them spends its revive (it stays standing, step 4); any other member the devour knocks out stays down.
|
||||
- Algorithm:
|
||||
1. Order: 阿戈尔 members sorted leftmost first, then topmost ("更靠左和靠上").
|
||||
2. Each 阿戈尔 in order marks the unit on the tile directly in front of it (its facing direction) and also the front-tile unit of every 阿戈尔 it has marked (chain). It never marks itself, a unit it already marked, or a unit that marked it.
|
||||
|
||||
@@ -1980,7 +1980,7 @@ Consequences for the current pool:
|
||||
- Withdrawing the owner removes its summons from the board.
|
||||
- Tokens marked `DEFAULT` in `shopStateTokenDict` are these placeable pieces: 医疗探机, 诅咒娃娃, 狼群, 斯卡蒂的海嗣, 流形. `HIDDEN` tokens exist only in battle.
|
||||
- [ASSUMED] Summons do **not** use one of the 8 deploy slots, as in the base game. Keep this as a config flag.
|
||||
- **Battle start:** every board unit deploys for free, in order **top→bottom, then left→right** (下半). In the boss round the order is mirrored for the right-side player. After that, knocked-out units redeploy automatically on their tile when the respawn timer is done and DP ≥ `cost` (DP starts at 10, +1/s, cap 99).
|
||||
- **Battle start:** every board unit deploys for free, in order **从上到下>从左到右** (下半): down each column, the columns left to right (research 01 §4.3). In the boss round the order is mirrored for the right-side player. After that, knocked-out units redeploy automatically on their tile when the respawn timer is done and DP ≥ `cost` (DP starts at 10, +1/s, cap 99).
|
||||
- **Ranged units may stand on melee tiles** ("所有行动内远程干员可部署在近战位"). Melee units may **not** use ranged-only tiles (HIGH `h`) [ASSUMED standard] — except the branches whose own trait reads 可以放置于远程位 (钩索师 歌蕾蒂娅 / 崖心, 推击手 见行者; PRTS 新人入门 "可部署在高台和地面"). That line is the branch trait, not a 部署效果 like the Y-module 教官's talent "可以额外部署在远程位" (the 帮助: "携带Y模组的教官仍无法部署至高台位"), so it still applies in prep — the remake's reading [ASSUMED], GitHub issue #32 item 4, DESIGN §22.6. Maps: see 05.
|
||||
|
||||
### C3. Pool membership per match
|
||||
|
||||
@@ -67,7 +67,7 @@ Evidence was extracted to the scratchpad `research2/`: guide pages, video frames
|
||||
| Moving a deployed unit | Drag the unit to any legal tile, **including its own tile**. The same diamond opens. The original stays visible until confirmed; cancelling leaves the unit and its facing unchanged. Re-orienting in place = drag the unit onto its own tile and swipe a new direction. | WIKI + ASSUMED (own-tile case) |
|
||||
| Tap a deployed unit | Opens the selection diamond (underframe) around the unit and shows its **attack range tiles** on the board (orange). Buttons on the diamond: **撤退** (red runner icon, upper-left; unit returns to the bench) and **出售 +1** (coin badge, upper-right). The left side shows the detail card: portrait, tier, LV, ATK/DEF/RES/block/cost, an "攻击范围" mini-grid, bond icons, and tabs 特质/技能/特性/天赋/装备. You cannot rotate from this menu. | DATA + WIKI |
|
||||
| Default facing | Every player-initiated placement goes through the wheel, so there is no silent default. Server-created board units (effects such as 外勤医疗 or 预备干员, bots, and back-compat) default to **RIGHT**, toward the gates. | ASSUMED |
|
||||
| Persistence | Facing is stored with the position and survives round changes. At combat start units auto-deploy on their tiles **with their facing**, in order **top→bottom, then left→right** (盟约下). | DATA (positions) + COMM (order) |
|
||||
| Persistence | Facing is stored with the position and survives round changes. At combat start units auto-deploy on their tiles **with their facing**, in order **从上到下>从左到右** (盟约下): down each column, the columns left to right (research 01 §4.3). | DATA (positions) + COMM (order) |
|
||||
| Melee, ranged, medic | All use the same wheel. The range preview uses the unit's own rangeGrid (heal range for medics). Blocking and "front" logic use the facing vector. | DATA |
|
||||
| Summons (tokens) | Each summon is its own bench card ("干员及其召唤物会自动加入整备区"), placed and moved freely with the same wheel. Retreating the owner also retreats its summons. | WIKI |
|
||||
| Equipment | Dragged onto a unit (board or bench). No direction step. A third item opens the equip-replace dialog (`UseEquipUp { charChessInstId, equipChessInstId, isChangeEquip, unloadInstId }`). | DATA |
|
||||
|
||||
@@ -676,7 +676,8 @@ export function mergeProgress(priv, chessId, getChess = () => null) {
|
||||
/**
|
||||
* Where the elite appears when gaining one more normal copy of `chessId` completes a merge now (PRTS 卫戍协议/帮助
|
||||
* "若消耗已部署至作战区的干员,则发送至作战区对应位置"; mirror of server board.js mergeTile / PlayerState._mergeChess): the
|
||||
* board tile of the deployed copy that deploys first (row desc, then col asc) — `{ row, col, dir }` — or null (no
|
||||
* board tile of the deployed copy that deploys first (col asc, then row desc: by column from the left, top to bottom —
|
||||
* Battle.start) — `{ row, col, dir }` — or null (no
|
||||
* merge — an elite card never merges, see mergeProgress — or no copy is deployed: the elite goes to the hand). The
|
||||
* copies stand on legal tiles, and the elite is the same operator, so the tile needs no legality check here.
|
||||
* @param {any} priv
|
||||
@@ -692,7 +693,7 @@ export function mergeTarget(priv, chessId, getChess = () => null) {
|
||||
const board = (Array.isArray(priv?.board) ? priv.board : []).filter((p) => p?.kind === 'chess' && !p.golden && Number.isInteger(p.row) && Number.isInteger(p.col)
|
||||
&& !String(p.id).endsWith('_b') && (p.id === base || getChess(p.id)?.baseId === base));
|
||||
if (!board.length) return null;
|
||||
board.sort((a, b) => b.row - a.row || a.col - b.col);
|
||||
board.sort((a, b) => a.col - b.col || b.row - a.row);
|
||||
return { row: board[0].row, col: board[0].col, dir: board[0].dir || 'RIGHT' };
|
||||
}
|
||||
|
||||
|
||||
@@ -321,7 +321,7 @@ export class PlayerState {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Every owned chess piece: board (deploy order) then hand then temp. */
|
||||
/** Every owned chess piece: board (reading order, board.js boardOrder) then hand then temp. */
|
||||
allChess() {
|
||||
const out = [];
|
||||
for (const { piece } of boardOrder(this.board)) if (piece.kind === 'chess') out.push(piece);
|
||||
@@ -509,8 +509,8 @@ export class PlayerState {
|
||||
* Merge `need` normal copies of `baseId` (the incoming, not yet stowed piece first, then temp, hand, board) into
|
||||
* the elite — PRTS 卫戍协议/帮助 §干员的获得与精锐化: "发送1名【精锐】状态的该干员至手牌区(若消耗已部署至作战区的干员,
|
||||
* 则发送至作战区对应位置)" (the user's playtest #6 follow-up confirms it). The tile (`mergeTile`): when a consumed copy
|
||||
* stood on the board the elite takes its tile and facing — of several, the one that deploys first (board reading
|
||||
* order: top → bottom, then left → right) [ASSUMED]. The incoming copy is never deployed (a 突变细胞 transformation
|
||||
* stood on the board the elite takes its tile and facing — of several, the one that deploys first (the left board
|
||||
* column first, top to bottom within a column — Battle.start's order) [ASSUMED]. The incoming copy is never deployed (a 突变细胞 transformation
|
||||
* destroyed its carrier before the gain: that tile is no copy's). It replaces a deployed copy, so the deploy count
|
||||
* never grows. Otherwise the elite goes to the hand, overflow temp — outside PREP too (a SETTLE merge's elite waits in
|
||||
* temp through the next prep, tempDue). The copies' equipment returns to the hand ("干员晋级后已配发装备会回收至整备区";
|
||||
@@ -1524,14 +1524,20 @@ export class PlayerState {
|
||||
|
||||
/**
|
||||
* The bond states the views show (m.private bonds, m.public players[].bonds): the computed states plus the pending
|
||||
* in-battle gains of this round's finished normal battle (bondsMeta.bondsWithGains). Never used by rules.
|
||||
* in-battle gains of this round's finished normal battle (bondsMeta.bondsWithGains). The 联防 field fights with them too
|
||||
* (battleInput `reached`); no other rule reads them.
|
||||
*/
|
||||
bondsView() { return bondsWithGains(this.bonds, this.pendingLayerGains); }
|
||||
|
||||
// =================================================================================================
|
||||
// battle input
|
||||
|
||||
battleInput({ side = 'L', colOffset = 0, carry = null } = {}) {
|
||||
/**
|
||||
* `reached`: the bonds carry the layers this round's own combat reached (bondsView: the pending in-battle gains, capped
|
||||
* like settle()) — the 联防 field (unite.js; PRTS 卫戍协议/帮助 §联防阶段 "将以其阵地当前的状态", [ASSUMED] the current
|
||||
* state includes those layers, as the strip shows them). The gains stay pending: settle() adds them once.
|
||||
*/
|
||||
battleInput({ side = 'L', colOffset = 0, carry = null, reached = false } = {}) {
|
||||
// a terrain change not yet followed by a recompute (a content hook at the prep end) never fields an illegal board
|
||||
this.deployMap();
|
||||
if (this._legalityStale) this.recompute();
|
||||
@@ -1557,7 +1563,7 @@ export class PlayerState {
|
||||
side,
|
||||
colOffset,
|
||||
units,
|
||||
bonds: bondSnapshot(this.bonds),
|
||||
bonds: bondSnapshot(reached ? this.bondsView() : this.bonds),
|
||||
bandId: this.bandId,
|
||||
playerEffects: this.effects.filter((e) => e.battle !== false).map((e) => ({
|
||||
id: e.id, key: e.key ?? null, source: e.iconKind ?? null, params: e.params ?? null, counter: e.counter ?? null, data: e.data ?? null,
|
||||
|
||||
+10
-5
@@ -188,7 +188,11 @@ export function legalTiles(map, pos) {
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Board pieces sorted in deployment order: top→bottom (row desc) then left→right (col asc). */
|
||||
/**
|
||||
* Board pieces in reading order: top row first (row desc), left to right within a row (col asc) — the battle input's order
|
||||
* (unit ids), the garrisons' dispatch order and the lists the views send. Not the deployment order: the battle deploys by
|
||||
* column (Battle.start; mergeTile follows that one).
|
||||
*/
|
||||
export function boardOrder(board) {
|
||||
return [...board.entries()]
|
||||
.map(([k, p]) => { const [r, c] = parseKey(k); return { r, c, piece: p }; })
|
||||
@@ -197,16 +201,17 @@ export function boardOrder(board) {
|
||||
|
||||
/**
|
||||
* Where a merge's elite stands (PRTS 卫戍协议/帮助 "若消耗已部署至作战区的干员,则发送至作战区对应位置"): of the board tiles
|
||||
* the consumed copies stood on (`[{ key, dir }]`), the first in deployment order (top→bottom, left→right — the copy the
|
||||
* battle deploys first; the right-hand boss half is mirrored on screen but deploys in the same board order) that
|
||||
* `legal(r, c)` accepts for the elite, or null (⇒ the hand). [ASSUMED] the order: the official text names one position.
|
||||
* the consumed copies stood on (`[{ key, dir }]`), the first in deployment order (the left board column first, top to
|
||||
* bottom within a column — the copy the battle deploys first, Battle.start; the right-hand boss half is mirrored on
|
||||
* screen but deploys in the same board order) that `legal(r, c)` accepts for the elite, or null (⇒ the hand). [ASSUMED]
|
||||
* the order: the official text names one position. Until 0.1.3 the deployment order, and so this one, was top row first.
|
||||
* public/js/ui/gameLogic.js mergeTarget mirrors it for the client.
|
||||
* @param {Array<{ key: string, dir?: string }>} tiles
|
||||
* @param {(r: number, c: number) => boolean} [legal]
|
||||
* @returns {{ key: string, dir?: string, r: number, c: number } | null}
|
||||
*/
|
||||
export function mergeTile(tiles, legal = () => true) {
|
||||
const sorted = (tiles || []).map((t) => { const [r, c] = parseKey(t.key); return { ...t, r, c }; }).sort((a, b) => b.r - a.r || a.c - b.c);
|
||||
const sorted = (tiles || []).map((t) => { const [r, c] = parseKey(t.key); return { ...t, r, c }; }).sort((a, b) => a.c - b.c || b.r - a.r);
|
||||
return sorted.find((t) => inField(t.r, t.c) && legal(t.r, t.c)) || null;
|
||||
}
|
||||
|
||||
|
||||
+15
-5
@@ -18,13 +18,16 @@
|
||||
// them beside the operators; one off the field at the end enters fresh [ASSUMED]). Enemies = the union of every leaker's
|
||||
// counted leaks (same stats: the SpawnSpec mods travel with the leak), routed on the escaped template (`escaped_single`
|
||||
// for 1 helper, `escaped_multi` for 2): walkers on its `lrsldr` action, flyers on `yokai`, tokens on `gopro_2` /
|
||||
// `lazerd` (waves.js buildUniteWave); kill bounties keep paying the killer (a helper). No IN_BATTLE layer gains. Time
|
||||
// limit = the round's combat limit.
|
||||
// `lazerd` (waves.js buildUniteWave); kill bounties keep paying the killer (a helper). No IN_BATTLE layer gains ("该阶段
|
||||
// 不能叠加层数"); the helpers' bonds carry the layers their own combat reached (PlayerState.battleInput `reached`: the
|
||||
// round's pending gains, capped like settle() — the strip's count; "以其阵地当前的状态" [ASSUMED] includes them; until 0.1.3
|
||||
// the round-start layers), and settle() still adds those gains once. Time limit = the round's combat limit.
|
||||
// LP: an enemy still alive at the end (leaked in the unite battle, or never spawned before the limit) costs its
|
||||
// SOURCE player 1 LP; each player's round loss = min(lpCap, survivors attributed to them + leaks that could not
|
||||
// re-enter) — the same 10 cap as a normal round.
|
||||
|
||||
import { buildUniteWave } from './waves.js';
|
||||
import { layerGainRoom } from '../../shared/constants.js';
|
||||
|
||||
/**
|
||||
* @param {import('./Match.js').Match} m
|
||||
@@ -67,16 +70,22 @@ export function planUnite(m, results) {
|
||||
|
||||
/**
|
||||
* Helper metrics of a perfect player: units on the field (board operators, downed included), whether a bond is
|
||||
* active, Σ layers of the active bonds, operators still standing at the end of the player's own combat.
|
||||
* active, Σ layers of the active bonds as the 联防 battle will fight them (the persistent layers plus this round's
|
||||
* pending gains, capped like bondsView / settle — PRTS 以其阵地当前的状态; the official "层数最高" tie-break is marked 存疑),
|
||||
* operators still standing at the end of the player's own combat.
|
||||
*/
|
||||
export function helperStats(m, ps, results) {
|
||||
const units = ps.deployCount;
|
||||
let active = false;
|
||||
let layers = 0;
|
||||
const pending = ps.pendingLayerGains;
|
||||
for (const [id, b] of Object.entries(ps.bonds || {})) {
|
||||
if (!b || !b.active) continue;
|
||||
active = true;
|
||||
layers += Number(ps.layers && ps.layers[id]) || Number(b.layers) || 0;
|
||||
const stored = Number(ps.layers && ps.layers[id]) || Number(b.layers) || 0;
|
||||
const gain = pending && Number(pending[id]);
|
||||
const add = Number.isFinite(gain) && gain > 0 ? layerGainRoom(stored, Math.floor(gain)) : 0;
|
||||
layers += stored + add;
|
||||
}
|
||||
const r = results && typeof results.get === 'function' ? results.get(ps.playerId) : null;
|
||||
const opUids = new Set();
|
||||
@@ -122,7 +131,8 @@ export function uniteBattleOpts(m, plan, timeLimit) {
|
||||
}
|
||||
// 2 helpers: the first one meets the enemies first on the right-hand field (escaped_multi enters at col 18)
|
||||
const colOffset = plan.helpers.length > 1 && i === 0 ? 8 : 0;
|
||||
const input = ps.battleInput({ side: 'L', colOffset, carry });
|
||||
// the layers its own combat reached (bondsView, the round's pending gains; PRTS "以其阵地当前的状态") — see header
|
||||
const input = ps.battleInput({ side: 'L', colOffset, carry, reached: true });
|
||||
const ev = { input, kind: 'unite', round: m.round, spawns: wave.spawns };
|
||||
m.dispatch(ps, 'onBattleStart', ev);
|
||||
return ev.input && typeof ev.input === 'object' ? ev.input : input;
|
||||
|
||||
+14
-10
@@ -5,11 +5,11 @@
|
||||
// b.step(); b.finished; b.forceEnd(reason); b.time; b.result(); b.snapshot(); b.drainEvents(); b.on/off(...)
|
||||
//
|
||||
// Construction creates every ally unit (undeployed) and installs content (kits + domain modules). The first
|
||||
// step() (or an explicit start()) deploys everything (operators top→bottom, left→right — mirrored for the right boss
|
||||
// side —, then the summon pieces the same way — except a piece content flags `deferDeploy`, which waits on its tile;
|
||||
// the players of a shared field side by side), fires `deploy` (initial) for each unit, forces out the operators that
|
||||
// enter already knocked out (`carryState.down`, 联防: constants.js FORCED_EXIT — down on their tile, redeploy timer
|
||||
// running) and then fires `battleStart`.
|
||||
// step() (or an explicit start()) deploys everything (operators by column from the left, top to bottom within a
|
||||
// column — mirrored for the right boss side —, then the summon pieces the same way — except a piece content flags
|
||||
// `deferDeploy`, which waits on its tile; the players of a shared field side by side), fires `deploy` (initial) for
|
||||
// each unit, forces out the operators that enter already knocked out (`carryState.down`, 联防: constants.js
|
||||
// FORCED_EXIT — down on their tile, redeploy timer running) and then fires `battleStart`.
|
||||
// An operator that left the field — knocked out, or forced out by its own effects (史尔特尔's 余烬 …, GitHub #60) — lies
|
||||
// on its `body` tile — where it fell, or its own home when it fell on another board piece's home — and redeploys there;
|
||||
// no ally deploys or moves onto that tile meanwhile (PRTS 卫戍协议/帮助 §作战阶段 单位部署; `isDown`, `_layBody`, `downOn`,
|
||||
@@ -350,14 +350,18 @@ export class Battle {
|
||||
if (this.started) return;
|
||||
this.started = true;
|
||||
this._safe(() => this._spawnStageDevices(), 'stageDevices');
|
||||
// PRTS 卫戍协议/帮助: "按从上到下>从左到右的顺序部署。优先部署干员,随后为召唤物(如果有)" — per player the operators
|
||||
// top row first, left to right within a row (the mirrored right boss side: right to left in field columns,
|
||||
// research 01 §4.3 "嘲諷優先鏡射"), then the summon pieces in the same order. [ASSUMED] On a shared field (联防, boss)
|
||||
// the players' fields deploy at the same time (PRTS: one unit after another with a fixed delay, from the battle
|
||||
// PRTS 卫戍协议/帮助 §作战阶段: "按从上到下>从左到右的顺序部署。优先部署干员,随后为召唤物(如果有)", noted "自卫戍协议:
|
||||
// 盟约 下半(2026/3/14)起,部署顺序由“从左到右>从下到上”更改为“从上到下>从左到右”" — a scanning order: down each column
|
||||
// (top first), the columns from left to right; act 1's along each row, the rows from the bottom. It is the order PRTS
|
||||
// gives the 阿戈尔 devour ("从最先部署(更靠左和靠上的)的【阿戈尔】干员开始", content/bonds/core.js egirOrder) and the one
|
||||
// act-2 videos show. Per player the operators, left column first, top to bottom within a column (the mirrored right
|
||||
// boss side from its own left, i.e. the highest field column — PRTS "部署顺序…左右镜像", research 01 §4.3), then the
|
||||
// summon pieces in the same order. Until 0.1.3 the top row came first (row-major). [ASSUMED] On a shared field (联防,
|
||||
// boss) the players' fields deploy at the same time (PRTS: one unit after another with a fixed delay, from the battle
|
||||
// start), so the i-th operators of all players come in together (in `players` order), then the summons likewise
|
||||
const seq0 = this._deploySeq;
|
||||
const lists = this.players.map((ps) => ps.units.filter((u) => u.kind === 'op' || u.kind === 'token').slice().sort((a, b) =>
|
||||
b.homeR - a.homeR || (ps.mirror ? b.homeC - a.homeC : a.homeC - b.homeC) || a.id - b.id));
|
||||
(ps.mirror ? b.homeC - a.homeC : a.homeC - b.homeC) || b.homeR - a.homeR || a.id - b.id));
|
||||
// a summon piece flagged `deferDeploy` by content (one its owner's loadout does not make — 赫默 on S1 with a drone
|
||||
// piece —, or a skill's summon when shared/constants.js SKILL_SUMMON_START_DEPLOY is off: content/tokens.js
|
||||
// dockSkillSummons) stays off the field, its tile reserved (isReservedTile), until content deploys it
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
// 拉特兰 lateranoShip member ammo skills start with floor(ammo × (1 + base_ammo_percent + ammo_percent_per_stack·L));
|
||||
// 6: every ammo used by a member → all members ATK +atk_per_consume (≤ max_atk_for_consume)
|
||||
// 阿戈尔 egirShip members max HP +(base_max_hp + max_hp_per_stack·L) (直接乘算); battle start devour (see devour());
|
||||
// 5: the first max_free_respawn_cnt members knocked out for the first time redeploy at once (free)
|
||||
// 5: the first max_free_respawn_cnt members by position (the devour's order, fixed at battle
|
||||
// start) each redeploy at once (free) on their first knock-out
|
||||
// 叙拉古 siracusaShip every member deployment: ASPD +(base + per·L) for (base_duration + per·L) s; 6: 隐匿 for the same
|
||||
// time, and while hidden / end_duration s after, attacks proc (PRD, nominal `prob`) base_damage +
|
||||
// damage_per_stack·L true damage + fear `fear` s
|
||||
@@ -330,6 +331,23 @@ function installLaterano(battle, pid, bb, members) {
|
||||
// =====================================================================================================================
|
||||
// 阿戈尔
|
||||
|
||||
// "更靠左和靠上": left first (on the player's own board: mirrored players count from the field's right), then top first —
|
||||
// row 0 is the BOTTOM row (DESIGN §3), so the top of the board is the highest row index. The order is a board position,
|
||||
// independent of the members' directions (only "身前" follows each member's `dir`). The devour marks in this order and the
|
||||
// 5-tier revive's beneficiaries are the first members in it (PRTS "从最先部署(更靠左和靠上的)的【阿戈尔】干员开始"; the
|
||||
// battle's initial deployment order, Battle.start).
|
||||
const boardCol = (u) => (u.player && u.player.mirror ? -u.tileC : u.tileC);
|
||||
const egirOrder = (a, b) => boardCol(a) - boardCol(b) || b.tileR - a.tileR || a.id - b.id;
|
||||
|
||||
/**
|
||||
* 联防: an operator forced out at the deployment (carry.down, Battle.start, before battleStart) still stands on its
|
||||
* deploy position. The devour and the 5-tier revive slots both count it there (PRTS 盟约记录 前3名 / 最先部署).
|
||||
*/
|
||||
function egirDownAtStart(battle, u) {
|
||||
return S.isOp(u) && !u.alive && u.removeReason === FORCED_EXIT && !!u.carry && u.carry.down === true
|
||||
&& battle.isDown(u);
|
||||
}
|
||||
|
||||
/**
|
||||
* Battle-start devour (research 02 §3.6 algorithm): members in order (further left on the player's own board — the
|
||||
* Final Assault right side is mirrored, so its board-left is the field's right — then higher on the board first) mark
|
||||
@@ -351,20 +369,14 @@ function installLaterano(battle, pid, bb, members) {
|
||||
*/
|
||||
function devour(battle, pid, bb, members) {
|
||||
const memberSet = new Set(members);
|
||||
// "更靠左和靠上": left first (on the player's own board: mirrored players count from the field's right), then top
|
||||
// first — row 0 is the BOTTOM row (DESIGN §3), so the top of the board is the highest row index. The order is a
|
||||
// board position, independent of the members' directions (only "身前" follows each member's `dir`).
|
||||
const boardCol = (u) => (u.player && u.player.mirror ? -u.tileC : u.tileC);
|
||||
// 联防: an operator down at the end of its own combat (carryState.down — Battle.start forced it out right before
|
||||
// battleStart, FORCED_EXIT) takes part in the devour as if it stood on its tile, then stays out: it marks in its turn,
|
||||
// it is "the unit in front" of another (the chain goes on through it when it is a member), its base ATK / block count
|
||||
// for its marker and its own marks resolve (their 物理流失 lands, credited to it as usual); only the marks ON it resolve
|
||||
// nothing — it is forced out, so it is never knocked out again, revived or devoured. Per players' reports (community
|
||||
// report #3, GitHub #33 item 3), owner's decision 2026-10-04; until 0.1.2 the forced exit came first and the chain broke.
|
||||
const downAtStart = (u) => S.isOp(u) && !u.alive && u.removeReason === FORCED_EXIT && !!u.carry && u.carry.down === true
|
||||
&& battle.isDown(u);
|
||||
const order = members.filter((u) => S.onField(u) || downAtStart(u)).sort((a, b) =>
|
||||
boardCol(a) - boardCol(b) || b.tileR - a.tileR || a.id - b.id);
|
||||
const downAtStart = (u) => egirDownAtStart(battle, u);
|
||||
const order = members.filter((u) => S.onField(u) || downAtStart(u)).sort(egirOrder);
|
||||
const opAt = (u) => {
|
||||
const [r, c] = S.frontTile(u);
|
||||
const a = S.allyAt(battle, r, c, pid);
|
||||
@@ -424,27 +436,40 @@ function installEgir(battle, pid, bb, members) {
|
||||
};
|
||||
apply();
|
||||
onLayers(battle, pid, 'egirShip', apply);
|
||||
battle.on('battleStart', () => devour(battle, pid, bb, members), { once: true });
|
||||
if (!reached(battle, pid, 'egirShip', bb.power_bond_char_cnt)) return;
|
||||
// 5: "前3名【阿戈尔】干员首次被击倒时立刻复活" — PRTS: the knocked-out unit's next deployment has 0 redeploy time and
|
||||
// 0 cost, i.e. it IS knocked out (被击倒 triggers, 克莱门莎, 幽灵鲨 … fire) and redeploys at once where it lies (the
|
||||
// engine's rest tile, Battle._layBody: the tile it was knocked out on — a raid-relocated member comes back where it
|
||||
// fell —, or its own home when it fell on another board piece's home; PRTS 卫戍协议/帮助 §作战阶段 单位部署) with full
|
||||
// HP, SP reset and `deploy` effects (卡西米尔 / 叙拉古). Death priority 11: before 不屈 (10),
|
||||
// whose redeploy "also consumes a 复活 charge" — with this order the charge is always the one used, same outcome.
|
||||
// A member the battle-start devour knocks out spends a charge like any other first knock-out; the marks still pending
|
||||
// on it are cancelled (devour), so it stays standing.
|
||||
const memberSet = new Set(members);
|
||||
const max = Math.max(0, Math.floor(num(bb.max_free_respawn_cnt, 0)));
|
||||
const st = { knocked: new Set(), revives: 0 };
|
||||
// 5: "前3名【阿戈尔】干员首次被击倒时立刻复活" — the 3 (bonds.json max_free_respawn_cnt) are the first 3 members BY POSITION,
|
||||
// in the devour's order above = the battle's deployment order (PRTS 盟约记录 阿戈尔 备注 "从最先部署(更靠左和靠上的)的
|
||||
// 【阿戈尔】干员开始"; players' videos: the revivers go by position, not by who is eaten or knocked out first). They are
|
||||
// fixed at battle start, before the devour; each revives once, on its own first knock-out ('killed'), whatever the order
|
||||
// of knock-outs, so a member the devour knocks out comes back only when it is one of them. Until 0.1.3 the first 3
|
||||
// members knocked out took the revives and the devour's food spent them at t = 0. A member entering 联防 down (forced
|
||||
// out before battleStart) keeps the slot its position earned: the forced exit is not a 击倒 (reason !== 'killed'), so
|
||||
// the charge waits until they are knocked out after standing back up. A 调和 member counts like any other.
|
||||
// [ASSUMED] a beneficiary 埃芒加德 / M3茧甲 save in place (items revivedInPlace, the stand-in for PRTS's 0-time redeploy)
|
||||
// has used its revive (PRTS 不屈 备注: a held 复活 is consumed even when another effect redeploys the unit at once).
|
||||
// PRTS: the knocked-out unit's next deployment has 0 redeploy time and 0 cost, i.e. it IS knocked out (被击倒 triggers,
|
||||
// 克莱门莎, 幽灵鲨 … fire) and redeploys at once where it lies (the engine's rest tile, Battle._layBody: the tile it was
|
||||
// knocked out on — a raid-relocated member comes back where it fell —, or its own home when it fell on another board
|
||||
// piece's home; PRTS 卫戍协议/帮助 §作战阶段 单位部署) with full HP, SP reset and `deploy` effects (卡西米尔 / 叙拉古).
|
||||
// Death priority 11: before 不屈 (10), whose redeploy "also consumes a 复活 charge" — with this order the charge is
|
||||
// always the one used, same outcome. The marks still pending on a revived member are cancelled (devour).
|
||||
const max = reached(battle, pid, 'egirShip', bb.power_bond_char_cnt) ? Math.max(0, Math.floor(num(bb.max_free_respawn_cnt, 0))) : 0;
|
||||
const holders = new Map(); // beneficiary → its in-place revives (mem.revives) at battle start
|
||||
battle.on('battleStart', () => {
|
||||
for (const u of members.filter((x) => S.isOp(x) && (S.onField(x) || egirDownAtStart(battle, x))).sort(egirOrder).slice(0, max)) holders.set(u, u.mem.revives | 0);
|
||||
devour(battle, pid, bb, members);
|
||||
}, { once: true });
|
||||
if (!(max > 0)) return;
|
||||
const used = new Set();
|
||||
let revives = 0;
|
||||
battle.on('death', (c) => {
|
||||
const u = c.unit;
|
||||
if (c.reason !== 'killed' || !memberSet.has(u) || st.knocked.has(u)) return;
|
||||
st.knocked.add(u);
|
||||
if (st.revives >= max || u.alive || u.removed) return;
|
||||
if (c.reason !== 'killed' || !holders.has(u) || used.has(u)) return;
|
||||
used.add(u);
|
||||
if ((u.mem.revives | 0) !== holders.get(u)) return; // saved in place since the battle start: the revive is gone
|
||||
if (u.alive || u.removed) return;
|
||||
if (!battle.redeploy(u, { free: true })) return;
|
||||
st.revives++;
|
||||
S.fxOn(battle, 'revive', u, 'bond:egirShip', 'respawn', { n: st.revives });
|
||||
revives++;
|
||||
S.fxOn(battle, 'revive', u, 'bond:egirShip', 'respawn', { n: revives });
|
||||
}, { priority: 11 });
|
||||
}
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ function textNum(text, re, fallback) {
|
||||
/**
|
||||
* A tile a summon may take: inside the field and not reserved (Battle.isReservedTile: nobody on it, no knocked-out
|
||||
* operator lying there, not the home tile of an ally that has not deployed yet / waits to redeploy — the initial
|
||||
* deployment runs top→bottom: a summon placed while it runs must not steal a later board unit's tile).
|
||||
* deployment runs one unit after another: a summon placed while it runs must not steal a later board unit's tile).
|
||||
*/
|
||||
function freeTile(battle, r, c) {
|
||||
return Number.isInteger(r) && Number.isInteger(c) && battle.grid.inRect(r, c) && !battle.isReservedTile(r, c);
|
||||
@@ -290,7 +290,7 @@ const KITS = {
|
||||
battle.addBuff(unit, { key: 'talent:angel_bless', mods: { ...bless }, persist: true, allowDead: true });
|
||||
battle.on('deploy', (ctx) => {
|
||||
if (ctx.unit !== unit) return;
|
||||
// after the whole board is deployed (initial deployment goes top→bottom)
|
||||
// after the whole board is deployed (the initial deployment goes one unit after another)
|
||||
battle.after(0, () => {
|
||||
if (!alive(unit)) return;
|
||||
const cands = battle.allies(unit.ownerId).filter((a) => a !== unit && a.kind === 'op' && !a.findBuff('talent:angel_bless_ally'));
|
||||
|
||||
@@ -348,7 +348,8 @@ function inRectTile(battle, r, c) { return Number.isInteger(r) && Number.isInteg
|
||||
|
||||
/**
|
||||
* A tile a summon may take: inside the field, nobody on it, and not the home tile of a board unit that has not
|
||||
* deployed yet / waits to redeploy (initial deployment runs top→bottom, a summon must not steal a later unit's tile).
|
||||
* deployed yet / waits to redeploy (the initial deployment runs one unit after another, a summon must not steal a later
|
||||
* unit's tile).
|
||||
*/
|
||||
export function tileFree(battle, r, c) {
|
||||
return inRectTile(battle, r, c) && !battle.isReservedTile(r, c);
|
||||
|
||||
@@ -128,16 +128,27 @@ const installDollkeeper = (battle, unit) => {
|
||||
};
|
||||
const at = (extra) => ({ x: unit.x, y: unit.y, id: unit.id, ...extra });
|
||||
// a switch animation: "切换动画开始时会清除自身一切Buff" (header: the running skill and the statuses), then 1 s of 不死
|
||||
// (the fatal hook below) 无敌 阻回 禁疗 孤立 强制缴械 and 眩晕 / 冻结 / 睡眠 immunity (beforeStatus below)
|
||||
// (the fatal hook below) 无敌 阻回 禁疗 孤立 强制缴械 and 眩晕 / 冻结 / 睡眠 immunity (beforeStatus below). "切换途中重设自身
|
||||
// 生命至最大值": the HP is set to the max at the start (enter / leave) and again when the switch ends, so she always
|
||||
// leaves it at full HP — a lethal 流失 inside the switch (无敌 does not stop it, 不死 holds it at 1 HP: a later 阿戈尔
|
||||
// devour mark on her) used to leave her the 替身's 20 s at 1 HP (community report #2)
|
||||
const startSwitch = () => {
|
||||
const sk = unit.skill;
|
||||
if (sk && sk.active && sk.kind !== 'passive') sk.end('substitute');
|
||||
for (const b of unit.buffs.slice()) if (b.status) battle.removeBuff(unit, b);
|
||||
unit.trait.dollSwitching = true;
|
||||
const done = () => { unit.trait.dollSwitching = false; };
|
||||
const ended = () => {
|
||||
done();
|
||||
if (!unit.alive || !unit.deployed) return;
|
||||
unit.markDirty();
|
||||
unit.hp = unit.s.maxHp;
|
||||
};
|
||||
battle.addBuff(unit, {
|
||||
key: 'trait:dollSwitching', duration: DOLL_SWITCH, onExpire: done, onRemove: done,
|
||||
flags: { invulnerable: true, noSp: true, noHeal: true, isolated: true, disarm: true },
|
||||
key: 'trait:dollSwitching', duration: DOLL_SWITCH, onExpire: ended, onRemove: done,
|
||||
// noHeal refuses another unit's heal; healFree is 禁疗 (damage.js: a self-heal is 0 too, regen excepted). The
|
||||
// window still ends at full HP — onExpire writes it, it does not heal.
|
||||
flags: { invulnerable: true, noSp: true, noHeal: true, healFree: true, isolated: true, disarm: true },
|
||||
});
|
||||
};
|
||||
const leave = () => {
|
||||
|
||||
+114
-30
@@ -374,37 +374,43 @@ test('阿戈尔 devour order: left first, then top first (row 0 is the bottom: t
|
||||
checkInvariants(h.b);
|
||||
});
|
||||
|
||||
test('阿戈尔 5: the first 3 members knocked out for the first time redeploy at once (death + deploy fire); a second knock-out is final', () => {
|
||||
test('阿戈尔 5: the first 3 members by position (left column first, top first) revive at once on their first knock-out, whatever the order of knock-outs; the others never; a second knock-out is final', () => {
|
||||
const list = [];
|
||||
for (let i = 0; i < 5; i++) list.push([`g${i}_a`, ['egirShip'], { skill: { spCost: 30, initSp: 5, duration: 10 } }]);
|
||||
// one per row pair so nobody devours anybody
|
||||
// one per row pair so nobody devours anybody: col 3 rows 9–12 (g0 bottom … g3 top), g4 at (9,5). PRTS 盟约记录 阿戈尔
|
||||
// "前3名【阿戈尔】干员首次被击倒时立刻复活", the first 3 being the first deployed — "更靠左和靠上": g3, g2, g1
|
||||
const units = list.map(([id], i) => ({ chessId: id, row: 9 + (i % 4), col: 3 + 2 * Math.floor(i / 4) }));
|
||||
const h = makeBattle({ defs: defsOf(list), units, bonds: { egirShip: bondOn(5, 0, null, [3, 5]) }, hooks: ['death', 'deploy'] });
|
||||
h.step(1);
|
||||
const us = list.map(([id]) => h.unit(id));
|
||||
const kill = (u) => h.b.dealDamage(null, u, { amount: 1e9, type: 'true' });
|
||||
us[0].skill.gainSp(20, 'test');
|
||||
const tiles = us.map((u) => [u.tileR, u.tileC]);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const redeploys = () => h.hooksOf('deploy').filter((c) => c.unit === us[i] && !c.initial).length;
|
||||
const before = redeploys();
|
||||
kill(us[i]);
|
||||
assert.ok(us[i].alive && us[i].deployed, `member ${i} back on the field`);
|
||||
close(us[i].hp, us[i].s.maxHp);
|
||||
assert.deepEqual([us[i].tileR, us[i].tileC], tiles[i], 'same tile');
|
||||
assert.ok(h.hooksOf('death').some((c) => c.unit === us[i] && c.reason === 'killed'), 'it was knocked out (被击倒 triggers fire)');
|
||||
assert.equal(redeploys(), before + 1, `member ${i} redeployed`);
|
||||
const redeploys = (u) => h.hooksOf('deploy').filter((c) => c.unit === u && !c.initial).length;
|
||||
// knocked out first, yet no beneficiary: g0 (bottom of the first column) and g4 (second column) stay down
|
||||
for (const u of [us[0], us[4]]) {
|
||||
kill(u);
|
||||
assert.ok(!u.alive, `${u.defId} is not among the first 3 by position: no revive`);
|
||||
assert.equal(redeploys(u), 0);
|
||||
}
|
||||
assert.equal(us[0].skill.sp, 5, 'a redeploy restarts from the initial SP');
|
||||
kill(us[3]);
|
||||
assert.ok(!us[3].alive, 'the 4th dies');
|
||||
kill(us[0]);
|
||||
assert.ok(!us[0].alive, 'second knock-out is final');
|
||||
us[1].skill.gainSp(20, 'test');
|
||||
const tiles = us.map((u) => [u.tileR, u.tileC]);
|
||||
for (const i of [1, 3, 2]) {
|
||||
const u = us[i];
|
||||
const before = redeploys(u);
|
||||
kill(u);
|
||||
assert.ok(u.alive && u.deployed, `member ${i} back on the field`);
|
||||
close(u.hp, u.s.maxHp);
|
||||
assert.deepEqual([u.tileR, u.tileC], tiles[i], 'same tile');
|
||||
assert.ok(h.hooksOf('death').some((c) => c.unit === u && c.reason === 'killed'), 'it was knocked out (被击倒 triggers fire)');
|
||||
assert.equal(redeploys(u), before + 1, `member ${i} redeployed`);
|
||||
}
|
||||
assert.equal(us[1].skill.sp, 5, 'a redeploy restarts from the initial SP');
|
||||
kill(us[1]);
|
||||
assert.ok(!us[1].alive, 'second knock-out is final');
|
||||
checkInvariants(h.b);
|
||||
// a member moved off its board tile (突袭 / relocation) revives where it was knocked out; later redeploys go home
|
||||
const hm = makeBattle({ defs: defsOf(list), units, bonds: { egirShip: bondOn(5, 0, null, [3, 5]) }, hooks: [] });
|
||||
hm.step(1);
|
||||
const m = hm.unit('g4_a');
|
||||
const m = hm.unit('g3_a');
|
||||
const home = [m.homeR, m.homeC];
|
||||
assert.ok(hm.b.relocate(m, 12, 8));
|
||||
hm.b.dealDamage(null, m, { amount: 1e9, type: 'true' });
|
||||
@@ -412,16 +418,17 @@ test('阿戈尔 5: the first 3 members knocked out for the first time redeploy a
|
||||
assert.deepEqual([m.homeR, m.homeC], home, 'home unchanged');
|
||||
checkInvariants(hm.b);
|
||||
// 3 members: no revive
|
||||
const h3 = makeBattle({ defs: defsOf(list), units: units.slice(0, 3), bonds: { egirShip: bondOn(3, 0, null, [3, 5]) } });
|
||||
const h3 = makeBattle({ defs: defsOf(list), units: units.slice(1, 4), bonds: { egirShip: bondOn(3, 0, null, [3, 5]) } });
|
||||
h3.step(1);
|
||||
h3.b.dealDamage(null, h3.unit('g0_a'), { amount: 1e9, type: 'true' });
|
||||
assert.ok(!h3.unit('g0_a').alive);
|
||||
h3.b.dealDamage(null, h3.unit('g3_a'), { amount: 1e9, type: 'true' });
|
||||
assert.ok(!h3.unit('g3_a').alive);
|
||||
});
|
||||
|
||||
test('阿戈尔 5 + the devour chain (GitHub #33): a member the devour knocks out revives, its pending marks are cancelled and it stays standing (normal and 联防)', () => {
|
||||
test('阿戈尔 5 + the devour chain (GitHub #33): the devour knocks out the food, only the members among the first 3 by position come back; the others keep theirs for the fight (normal and 联防)', () => {
|
||||
// one row, all facing right: 浊心斯卡蒂 → 乌尔比安 → 幽灵鲨 → 海霓 → 深巡 → 隐现. PRTS 盟约记录 阿戈尔 备注 "标记按付与顺序触发
|
||||
// 【吞噬】效果,目标首次被击倒后解除自身被付与但还未触发的【吞噬】效果"; until 0.1.1 the marks still pending on a revived member
|
||||
// knocked it out again, so all 3 revives went at t = 0 and the members stayed down
|
||||
// 【吞噬】效果,目标首次被击倒后解除自身被付与但还未触发的【吞噬】效果". The 3 revives belong to the first 3 by position
|
||||
// (浊心斯卡蒂, 乌尔比安, 幽灵鲨 — community reports 「阿戈尔盟约复活有bug,只能复活一个干员」); until 0.1.3 the first 3
|
||||
// knocked out took them, so the food (幽灵鲨, 海霓, 深巡) spent all 3 at t = 0 and nobody came back in the fight
|
||||
const ids = ['chess_char_6_04_a', 'chess_char_5_05_a', 'chess_char_2_07_a', 'chess_char_3_09_a', 'chess_char_1_04_a', 'chess_char_1_01_a'];
|
||||
for (const kind of ['normal', 'unite']) {
|
||||
const h = makeBattle({
|
||||
@@ -434,16 +441,93 @@ test('阿戈尔 5 + the devour chain (GitHub #33): a member the devour knocks ou
|
||||
// comes from one (幽灵鲨's, 海霓's, 深巡's)
|
||||
assert.deepEqual(tagged(h, 'bond:egir:devour').map((c) => [c.source.defId, c.target.defId]),
|
||||
[ulpia, ghost, hn, deep, fodder].map((t) => [skadi.defId, t.defId]), kind);
|
||||
const revived = (u) => h.hooksOf('deploy').filter((c) => c.unit === u && !c.initial).length;
|
||||
for (const u of [ghost, hn, deep]) {
|
||||
assert.equal(h.hooksOf('death').filter((c) => c.unit === u && c.reason === 'killed').length, 1, `${kind}: ${u.defId} knocked out once`);
|
||||
assert.equal(h.hooksOf('deploy').filter((c) => c.unit === u && !c.initial).length, 1, `${kind}: ${u.defId} revived at once`);
|
||||
}
|
||||
assert.equal(revived(ghost), 1, `${kind}: 幽灵鲨 (third by position) revived at once`);
|
||||
assert.equal(revived(hn) + revived(deep), 0, `${kind}: 海霓 and 深巡 (fourth, fifth) not revived`);
|
||||
h.run(3);
|
||||
for (const u of [skadi, ulpia, ghost, hn, deep]) assert.ok(u.alive && u.deployed && u.hp > 0, `${kind}: ${u.defId} standing`);
|
||||
assert.ok(!fodder.alive, `${kind}: the fodder (no member) stays down`);
|
||||
// the 3 revives went to the first knock-outs at t = 0, as officially: the next knock-out of a member is final
|
||||
for (const u of [skadi, ulpia, ghost]) assert.ok(u.alive && u.deployed && u.hp > 0, `${kind}: ${u.defId} standing`);
|
||||
for (const u of [hn, deep, fodder]) assert.ok(!u.alive, `${kind}: ${u.defId} down`);
|
||||
// 浊心斯卡蒂 and 乌尔比安 still hold theirs: their first knock-out in the fight is revived, 幽灵鲨's second is final
|
||||
for (const u of [skadi, ulpia]) {
|
||||
h.b.dealDamage(null, u, { amount: 1e9, type: 'true' });
|
||||
assert.ok(u.alive && revived(u) === 1, `${kind}: ${u.defId} revived in the fight`);
|
||||
}
|
||||
h.b.dealDamage(null, ghost, { amount: 1e9, type: 'true' });
|
||||
assert.ok(!ghost.alive, `${kind}: no revive left`);
|
||||
assert.ok(!ghost.alive, `${kind}: 幽灵鲨 has no revive left`);
|
||||
checkInvariants(h.b);
|
||||
}
|
||||
});
|
||||
|
||||
test('阿戈尔 5 beneficiaries: fixed at battle start — a 联防 member entering down keeps the slot, a 调和 member is one, the mirrored boss side counts from its own left', () => {
|
||||
const list = [['g0_a', ['egirShip']], ['g1_a', ['egirShip']], ['g2_a', ['egirShip']], ['g3_a', ['egirShip']], ['g4_a', ['egirShip']], ['mani_a', ['maniShip']]];
|
||||
// five members in five columns (one per column, nobody in front of anybody): g0 col 3 … g4 col 7
|
||||
const units = list.slice(0, 5).map(([chessId], i) => ({ chessId, row: 9 + (i % 2) * 3, col: 3 + i }));
|
||||
const revives = (h, id) => {
|
||||
const u = h.unit(id);
|
||||
h.b.dealDamage(null, u, { amount: 1e9, type: 'true' });
|
||||
return u.alive;
|
||||
};
|
||||
// 联防: g0 entered down (forced out before battleStart). The slots stay the first 3 by position — g0, g1, g2.
|
||||
// The forced exit is not a 击倒, so g0's charge waits until they stand back up. g3 is not promoted into it.
|
||||
const hu = makeBattle({
|
||||
kind: 'unite', defs: defsOf(list), autoFinish: false, timeLimit: 60, bonds: { egirShip: bondOn(5, 0, null, [3, 5]) },
|
||||
units: units.map((u, i) => (i === 0 ? { ...u, carryState: { down: true } } : u)),
|
||||
});
|
||||
hu.step(1);
|
||||
const g0 = hu.unit('g0_a');
|
||||
assert.ok(!g0.alive, 'g0 forced out');
|
||||
assert.deepEqual(['g4_a', 'g3_a', 'g1_a', 'g2_a'].map((id) => revives(hu, id)), [false, false, true, true]);
|
||||
assert.ok(hu.runUntil(() => g0.alive && g0.deployed, 25), 'g0 stands back up inside the redeploy time');
|
||||
assert.equal(revives(hu, 'g0_a'), true, 'g0 still held the first slot');
|
||||
hu.b.dealDamage(null, g0, { amount: 1e9, type: 'true' });
|
||||
assert.ok(!g0.alive, 'g0 has no second revive');
|
||||
checkInvariants(hu.b);
|
||||
// 调和 (an active 调和 member enjoys the core bonds) in the first column: one of the 3
|
||||
const hm = makeBattle({
|
||||
defs: defsOf(list), autoFinish: false, timeLimit: 60,
|
||||
units: [{ chessId: 'mani_a', row: 11, col: 2 }, ...units.slice(0, 4)],
|
||||
bonds: { egirShip: bondOn(5, 0, null, [3, 5]), maniShip: { count: 1, active: true, tier: 1, layers: 0 } },
|
||||
});
|
||||
hm.step(1);
|
||||
assert.deepEqual(['g2_a', 'mani_a', 'g0_a', 'g1_a'].map((id) => revives(hm, id)), [false, true, true, true]);
|
||||
// the right-hand boss player (mirrored): its board's left is the field's right — board cols 3, 4, 5 = field cols 17,
|
||||
// 16, 15 hold the revives (read in field columns from the left they would go to g4, g3, g2)
|
||||
const hb = makeBattle({
|
||||
kind: 'boss', defs: defsOf(list), autoFinish: false, timeLimit: 60,
|
||||
players: [{ playerId: 'R1', side: 'R', colOffset: 8, units: units.map((u, i) => ({ ...u, uid: i + 1 })), bonds: { egirShip: bondOn(5, 0, null, [3, 5]) } }],
|
||||
});
|
||||
hb.step(1);
|
||||
assert.deepEqual(['g4_a', 'g3_a', 'g0_a', 'g1_a', 'g2_a'].map((id) => [hb.unit(id).tileC, revives(hb, id)]),
|
||||
[[13, false], [14, false], [17, true], [16, true], [15, true]]);
|
||||
checkInvariants(hb.b);
|
||||
});
|
||||
|
||||
test('阿戈尔 5: a beneficiary 埃芒加德 or M3茧甲 save in place has used its 阿戈尔 revive [ASSUMED]; the others keep theirs', () => {
|
||||
const list = [['g0_a', ['egirShip']], ['g1_a', ['egirShip']], ['g2_a', ['egirShip']], ['g3_a', ['egirShip']], ['g4_a', ['egirShip']]];
|
||||
const units = (it = {}) => list.map(([chessId], i) => ({ chessId, row: 9 + (i % 2) * 3, col: 3 + i, items: it[i] }));
|
||||
const kill = (h, id) => { const u = h.unit(id); h.b.dealDamage(null, u, { amount: 1e9, type: 'true' }); return u; };
|
||||
for (const how of ['band', 'item']) {
|
||||
const h = makeBattle({
|
||||
defs: defsOf(list), autoFinish: false, timeLimit: 60, hooks: ['death', 'deploy'],
|
||||
units: units(how === 'item' ? { 0: ['chess_item_4_12_e_a'] } : {}), ...(how === 'band' ? { bandId: 'band_ermengard' } : {}),
|
||||
bonds: { egirShip: bondOn(5, 0, null, [3, 5]) },
|
||||
});
|
||||
h.step(1);
|
||||
// g0, g1, g2 (columns 3–5) hold the 阿戈尔 revives; g0 is saved in place first
|
||||
const g0 = kill(h, 'g0_a');
|
||||
assert.ok(g0.alive && h.hooksOf('death').filter((c) => c.unit === g0).length === 0, `${how}: saved in place (no knock-out)`);
|
||||
// 埃芒加德 saves the battle's first 3 knock-downs: its other 2 go to g3 and g4 (no 阿戈尔 beneficiaries)
|
||||
if (how === 'band') for (const id of ['g3_a', 'g4_a']) assert.ok(kill(h, id).alive, 'band revive');
|
||||
kill(h, 'g0_a');
|
||||
assert.ok(!g0.alive, `${how}: its 阿戈尔 revive went with the save — its first knock-out is final`);
|
||||
assert.equal(h.hooksOf('deploy').filter((c) => c.unit === g0 && !c.initial).length, 0);
|
||||
for (const id of ['g1_a', 'g2_a']) {
|
||||
const u = kill(h, id);
|
||||
assert.ok(u.alive && h.hooksOf('deploy').filter((c) => c.unit === u && !c.initial).length === 1, `${how}: ${id} revived by 阿戈尔`);
|
||||
}
|
||||
checkInvariants(h.b);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -321,7 +321,8 @@ for (const key of ['enemy_1203_sfhu', 'enemy_1203_sfhu_2']) {
|
||||
// 持续时间和效果独立计算"; "普通攻击对目标对及目标周围半径1.0范围内的所有我方单位造成法术普通伤害" (0.1.3: the blast took the attack
|
||||
// radius 2 and left a steam zone; the attack hit its target only)
|
||||
test(`${nm(key)}: death spawns 4 茶器; the blast (r 1.25, ATK arts) gives each unit hit one ${tb(key, 'DeadBoom.duration')} s layer of ASPD ${tb(key, 'DeadBoom.attack_speed')}; its attacks splash r 1.0`, () => {
|
||||
const h = arena({ units: [{ chessId: 't_gun', row: 10, col: 6 }, { chessId: 't_wall', row: 11, col: 8 }, { chessId: 't_wall2', row: 10, col: 5 }], hooks: ['damaged'], captureNoisy: true });
|
||||
// t_gun deploys last (column 6, below t_wall): the attack's target, t_wall2 and t_wall within 1.0 of it
|
||||
const h = arena({ units: [{ chessId: 't_gun', row: 10, col: 6 }, { chessId: 't_wall', row: 11, col: 6 }, { chessId: 't_wall2', row: 10, col: 5 }], hooks: ['damaged'], captureNoisy: true });
|
||||
h.step();
|
||||
const e = put(h, key, [10, 7]);
|
||||
h.runUntil(() => h.hooksOf('damaged').some((c) => c.source === e && c.dmg.isAttack), 10);
|
||||
@@ -1372,7 +1373,8 @@ test(`${nm('enemy_1050_lslime')}: 4 targets, burning DoT on hit, ASPD up below h
|
||||
// 目标造成物理普通伤害,对溅射目标造成物理溅射伤害") — community report #14: until 0.1.3 the target took a full attack and the 4
|
||||
// orthogonal neighbours 100 % ATK (no diagonal), DEF down for 3 s
|
||||
test(`${nm('enemy_1500_skulsr')}: an unblocked attack is a grenade — 26 % ATK on the target and on its 8 surrounding tiles, DEF −50 % for 5 s; blocked a plain hit; ATK up below half`, () => {
|
||||
const h = arena({ units: [{ chessId: 't_wall', row: 11, col: 3 }, { chessId: 't_wall2', row: 12, col: 6 }, { chessId: 't_wall3', row: 11, col: 5 }], captureNoisy: true, hooks: ['statusApplied', 'damaged'] });
|
||||
// t_wall3 is the one in range (√5): the target; t_wall2 on its diagonal, t_wall two tiles away
|
||||
const h = arena({ units: [{ chessId: 't_wall', row: 11, col: 3 }, { chessId: 't_wall2', row: 12, col: 4 }, { chessId: 't_wall3', row: 11, col: 5 }], captureNoisy: true, hooks: ['statusApplied', 'damaged'] });
|
||||
h.step();
|
||||
const e = put(h, 'enemy_1500_skulsr', [10, 7]);
|
||||
h.runUntil(() => e.stats.attacks >= 1, 10);
|
||||
|
||||
@@ -298,6 +298,10 @@ test('余: S3 fire wall adds burn to allied arts damage crossing it and clears c
|
||||
enemies: [{ key: 'enemy_dummy', pos: [10, 6] }, { key: 'enemy_shooter', pos: [11, 8] }],
|
||||
seed: 11,
|
||||
});
|
||||
// the shooter aims at 优等生 behind the wall: a taunt, since 余 (his column further right) now deploys last — the
|
||||
// latest deployed, the shooter's pick at equal taunt (deployment by column since 0.1.3)
|
||||
h.step();
|
||||
h.b.addBuff(h.unit('chess_char_1_15_a'), { key: 'test:taunt', mods: { taunt: 1 }, persist: true });
|
||||
const u = h.unit('chess_char_6_03_a');
|
||||
assert.ok(h.runUntil(() => u.skill.active, 10));
|
||||
h.run(15);
|
||||
|
||||
@@ -99,6 +99,19 @@ test('联防 helpers follow unite.helperOrder (units > active bond > standing >
|
||||
const c = player('c', 2, 2, 5, true); // most units + an active bond
|
||||
const order = helperOrder(m, [a, b, c], new Map()).map((p) => p.playerId);
|
||||
assert.deepEqual(order, ['c', 'b'], 'LP never decides; the first helper (right-hand field) is c');
|
||||
// the tie-break is the layers the 联防 will fight with: stored plus this round's pending gains, capped at 999
|
||||
const reached = (playerId, seat, stored, pending) => ({
|
||||
playerId, seat, lp: 10, deployCount: 4, board: new Map(),
|
||||
layers: { bond_x: stored },
|
||||
pendingLayerGains: pending,
|
||||
bonds: { bond_x: { active: true, layers: stored } },
|
||||
});
|
||||
const hi = reached('A', 0, 200, { bond_x: 50 }); // fights at 250
|
||||
const lo = reached('B', 1, 230, null); // fights at 230
|
||||
assert.deepEqual(helperOrder(m, [lo, hi], new Map()).map((p) => p.playerId), ['A', 'B']);
|
||||
const capped = reached('C', 2, 990, { bond_x: 50 }); // 999, not 1040
|
||||
const under = reached('D', 3, 995, null);
|
||||
assert.deepEqual(helperOrder(m, [under, capped], new Map()).map((p) => p.playerId), ['C', 'D']);
|
||||
assert.ok(!/highest LP, then seat/.test(DESIGN), 'DESIGN §6.1 no longer picks helpers by LP');
|
||||
assert.match(DESIGN, /helperOrder/);
|
||||
assert.match(META, /unite\.js helperOrder/);
|
||||
@@ -515,7 +528,8 @@ test('user playtest #6 follow-up: a merge consuming a deployed copy puts the eli
|
||||
assert.equal(promo.eliteTileAmongSeveralDeployed?.assumed, true);
|
||||
for (const [name, text] of [['research 01', R01], ['00-INDEX', INDEX], ['META', META], ['SIM', SIM], ['PlayerState', PS]]) assert.match(text.replace(/\s+|\/\/\s/g, ''), official, `${name} quotes PRTS`);
|
||||
assert.match(INDEX, /to \*\*that copy's board position\*\*/);
|
||||
assert.match(META, /of several, the one that deploys first \(row desc, then col asc; `board\.js mergeTile`/);
|
||||
// the deployment order is by column since 0.1.3 (Battle.start; DESIGN §23.f4), so is the merge's tile among several copies
|
||||
assert.match(META, /of several, the one that deploys first \(col asc, then row desc; `board\.js mergeTile`/);
|
||||
assert.ok(!/it takes a freed\s+board tile of a consumed copy only when the hand and temp are both full/.test(META), 'META: the old fallback-only wording is gone');
|
||||
assert.ok(!/only with the hand and temp both full does it take a/.test(PS), 'PlayerState header: the old fallback-only wording is gone');
|
||||
assert.match(PLAYING, /精锐会直接出现在\*\*那名干员的位置\*\*/);
|
||||
|
||||
@@ -88,7 +88,7 @@ test('merge with a full hand and a full temp: the elite takes its deployed copy\
|
||||
assert.deepEqual(m.handle('p_0', { t: 'g.buy', slot: 0 }), { ok: true }, 'a purchase that completes a merge is allowed with a full hand');
|
||||
const elite = [...ps.board.values()].find((p) => p.id === m.gd.goldenIdOf(id));
|
||||
assert.ok(elite, 'the elite stands on a consumed copy\'s tile');
|
||||
assert.equal(ps.find(elite.uid).key, `${t1[0]},${t1[1]}`, 'the copy that deploys first (legalTileFor walks the deploy order)');
|
||||
assert.equal(ps.find(elite.uid).key, `${t1[0]},${t1[1]}`, 'the copy that deploys first (legalTileFor walks the top row left to right: on one row, the deploy order)');
|
||||
assert.equal(elite.poolCopies, 3, 'it holds all three copies');
|
||||
assert.deepEqual(elite.items.map((x) => x.id).sort(), ['chess_item_1_01_e_a', 'chess_item_2_04_e_a'], 'returned items are kept on it');
|
||||
checkInvariants(m);
|
||||
|
||||
@@ -116,7 +116,8 @@ test('merge from two board copies with a full hand: the elite takes the copy tha
|
||||
stock(m, ps, id);
|
||||
assert.equal(ps.deployCount, 2);
|
||||
assert.deepEqual(m.handle('p_0', { t: 'g.buy', slot: 0 }), { ok: true });
|
||||
// deploy order = row desc, then col asc: (9,3) before (9,4) — [ASSUMED] the copy the battle deploys first
|
||||
// deploy order = by column from the left (col asc, then row desc; Battle.start): (9,3) before (9,4) — [ASSUMED] the copy
|
||||
// the battle deploys first
|
||||
const elite = ps.board.get('9,3');
|
||||
assert.ok(elite && elite.id === DATA.chess[id].goldenId, 'the elite stands on (9,3)');
|
||||
assert.equal(elite.dir, 'DOWN', 'with the facing of the copy that stood there');
|
||||
@@ -358,10 +359,10 @@ test('a merge completed during SETTLE (突变细胞) keeps its reward offer for
|
||||
if (holderTile) assert.ok(!ps.board.has(holderTile), `${label}: the carrier's tile is empty`);
|
||||
if (deployedTiles.length) {
|
||||
// PRTS 卫戍协议/帮助: a merge consuming a deployed copy sends the elite to that copy's tile — outside PREP too; of
|
||||
// several, the first in deploy order (row desc, col asc)
|
||||
// several, the first in deploy order (col asc, then row desc — by column from the left, Battle.start)
|
||||
const map = ps.deployMap();
|
||||
const pos = positionClass(m.gd.chess(elite.id));
|
||||
const legal = deployedTiles.map((k) => k.split(',').map(Number)).filter(([r, c]) => canPlace(map, pos, r, c)).sort((a, b) => b[0] - a[0] || a[1] - b[1]);
|
||||
const legal = deployedTiles.map((k) => k.split(',').map(Number)).filter(([r, c]) => canPlace(map, pos, r, c)).sort((a, b) => a[1] - b[1] || b[0] - a[0]);
|
||||
assert.ok(legal.length, `${label}: a legal deployed tile exists`);
|
||||
assert.equal(loc.area, 'board', `${label}: the elite stands on the board`);
|
||||
assert.equal(loc.key, `${legal[0][0]},${legal[0][1]}`, `${label}: on the first deployed tile in deploy order`);
|
||||
|
||||
@@ -86,11 +86,12 @@ test('buy: one deployed copy + one hand copy → the elite takes the deployed co
|
||||
m.dispose();
|
||||
});
|
||||
|
||||
test('buy: two deployed copies → the copy that deploys first (row desc, then col asc) gives its tile and facing, in either placement order', () => {
|
||||
test('buy: two deployed copies → the copy that deploys first (col asc, then row desc: by column from the left) gives its tile and facing, in either placement order', () => {
|
||||
for (const order of ['first-placed-first', 'first-placed-last']) {
|
||||
const { m, ps } = prep({ seed: 42 });
|
||||
const [id] = meleeTier1(m);
|
||||
// two legal tiles, the first of them in deploy order
|
||||
// two legal tiles, the first of them in deploy order (legalTileFor walks the top row left to right: on one row, the
|
||||
// deploy order)
|
||||
const t1 = legalTileFor(m, ps, id);
|
||||
const t2 = legalTileFor(m, ps, id, new Set([tileKey(...t1)]));
|
||||
const [early, late] = order === 'first-placed-first' ? [t1, t2] : [t2, t1];
|
||||
@@ -101,7 +102,7 @@ test('buy: two deployed copies → the copy that deploys first (row desc, then c
|
||||
stock(m, ps, id);
|
||||
assert.deepEqual(m.handle('p_0', { t: 'g.buy', slot: 0 }), { ok: true });
|
||||
const want = mergeTile([t1, t2].map((t) => ({ key: tileKey(...t) })));
|
||||
assert.equal(want.key, tileKey(...t1), 'legalTileFor walks the deploy order');
|
||||
assert.equal(want.key, tileKey(...t1), 'the first in deploy order');
|
||||
const elite = ps.board.get(want.key);
|
||||
assert.ok(elite && m.gd.isGolden(elite.id), `${order}: the elite on ${want.key}`);
|
||||
assert.equal(elite.dir, want.key === tileKey(...early) ? 'UP' : 'LEFT', `${order}: the facing of the copy that stood there`);
|
||||
|
||||
@@ -21,6 +21,7 @@ import { createStore, initialState } from '../../public/js/store.js';
|
||||
import * as specMod from '../../server/sim/spec.js';
|
||||
import { DataSource } from '../../server/sim/simdata.js';
|
||||
import { DATA, makeMatch, checkInvariants } from './harness.js';
|
||||
import { FakeBattle } from './fakeBattle.js';
|
||||
|
||||
const row = (h, pid) => h.m.publicView().players.find((p) => p.playerId === pid);
|
||||
const entry = (list, id) => (list || []).find((b) => b.bondId === id) || null;
|
||||
@@ -122,6 +123,36 @@ test('the 联防 phase keeps showing the gains (a teammate watching the helper s
|
||||
m.dispose();
|
||||
});
|
||||
|
||||
test('the 联防 field fights with the layers the helper\'s own combat reached (PRTS 联防阶段 "以其阵地当前的状态"), capped like the settlement, which still adds them once', () => {
|
||||
// p_0 perfect with gains, p_1 leaks: 联防 with p_0 as the helper. Until 0.1.3 the 联防 input carried the round-start
|
||||
// layers while the strip showed the reached ones (community report #24: 不屈 at 200+ layers "sometimes not triggering")
|
||||
const h = makeMatch({
|
||||
mode: 'coop', humans: 2, seed: 95, fake: true,
|
||||
script: (b) => (b.round === 1 && b.kind === 'normal' ? { layerGains: { p_0: { indomShip: 20, yanShip: 9 } }, leaks: { p_1: 2 } } : {}),
|
||||
}).start();
|
||||
const m = h.m;
|
||||
h.toPrep(1);
|
||||
const a = h.ps('p_0');
|
||||
a.layers.indomShip = 230;
|
||||
a.layers.yanShip = 995;
|
||||
a.recompute();
|
||||
const first = FakeBattle.instances.length;
|
||||
const input = (kind, round) => FakeBattle.instances.slice(first).find((x) => x.kind === kind && x.round === round)?.opts.players.find((p) => p.playerId === 'p_0');
|
||||
h.drive(() => m.phase === PHASE.UNITE || m.phase === PHASE.SETTLE);
|
||||
assert.equal(m.phase, PHASE.UNITE, 'p_1 leaked, p_0 was perfect: 联防');
|
||||
assert.deepEqual([input('normal', 1).bonds.indomShip.layers, input('normal', 1).bonds.yanShip.layers], [230, 995], 'the own combat: the round-start layers');
|
||||
const u = input('unite', 1);
|
||||
assert.equal(u.bonds.indomShip.layers, 250, '联防: 230 + the 20 reached in the own combat');
|
||||
assert.equal(u.bonds.yanShip.layers, BOND_LAYER_CAP, '995 + 9 is capped at 999, as the settlement caps it');
|
||||
assert.equal(u.bonds.indomShip.layers, entry(a.privateView().bonds, 'indomShip').layers, 'the count the strip shows');
|
||||
assert.deepEqual([a.layers.indomShip, a.bonds.indomShip.layers], [230, 230], 'nothing persistent yet');
|
||||
h.drive(() => m.phase === PHASE.COMBAT && m.round === 2);
|
||||
assert.deepEqual([a.layers.indomShip, a.layers.yanShip], [250, BOND_LAYER_CAP], 'settled once (not 270)');
|
||||
assert.equal(input('normal', 2).bonds.indomShip.layers, 250, 'the next combat starts from 250');
|
||||
checkInvariants(m);
|
||||
m.dispose();
|
||||
});
|
||||
|
||||
// ---- the browser runner's live layers ---------------------------------------------------------------------------------
|
||||
|
||||
const DS = new DataSource(DATA, null);
|
||||
|
||||
+10
-8
@@ -13,25 +13,27 @@ const approx = (a, b, eps = 1e-6) => assert.ok(Math.abs(a - b) <= eps, `${a} ≈
|
||||
const guard = (o = {}) => chessRec({ id: 't_guard', profession: 'WARRIOR', stats: { atk: 300, blockCnt: 2 }, skill: null, ...o });
|
||||
const walker = (o = {}) => enemyRec({ key: 'enemy_walker', hp: 1e6, speed: 1, ...o });
|
||||
|
||||
test('initial deployment order: top→bottom then left→right; right boss side right→left', () => {
|
||||
test('initial deployment order: by column from the left, top to bottom within a column (PRTS 从上到下>从左到右 as a scan); the right boss side from its own left', () => {
|
||||
// PRTS 卫戍协议/帮助 §作战阶段 "按从上到下>从左到右的顺序部署" (since 2026-03-14; act 1: "从左到右>从下到上") — down each column,
|
||||
// the columns from the left: the order PRTS gives the 阿戈尔 devour ("更靠左和靠上"). Until 0.1.3: the top row first
|
||||
const h = makeBattle({
|
||||
defs: { chess: { a: guard({ id: 'a' }), b: guard({ id: 'b' }), c: guard({ id: 'c' }), d: guard({ id: 'd' }) } },
|
||||
units: [{ chessId: 'a', row: 9, col: 5 }, { chessId: 'b', row: 12, col: 7 }, { chessId: 'c', row: 12, col: 3 }, { chessId: 'd', row: 10, col: 4 }],
|
||||
defs: { chess: { a: guard({ id: 'a' }), b: guard({ id: 'b' }), c: guard({ id: 'c' }), d: guard({ id: 'd' }), e: guard({ id: 'e' }) } },
|
||||
units: [{ chessId: 'a', row: 9, col: 5 }, { chessId: 'b', row: 12, col: 7 }, { chessId: 'c', row: 12, col: 3 }, { chessId: 'd', row: 10, col: 4 }, { chessId: 'e', row: 12, col: 5 }],
|
||||
content: 'none',
|
||||
});
|
||||
h.step();
|
||||
assert.deepEqual(h.hooksOf('deploy').filter((c) => c.initial).map((c) => c.unit.defId), ['c', 'b', 'd', 'a']);
|
||||
assert.deepEqual(h.hooksOf('deploy').filter((c) => c.initial).map((c) => c.unit.defId), ['c', 'd', 'e', 'a', 'b']);
|
||||
assert.equal(h.hooksOf('battleStart').length, 1);
|
||||
const hb = makeBattle({
|
||||
kind: 'boss',
|
||||
defs: { chess: { a: guard({ id: 'a' }), b: guard({ id: 'b' }) } },
|
||||
players: [{ playerId: 'R1', side: 'R', colOffset: 8, units: [{ uid: 1, chessId: 'a', row: 10, col: 3 }, { uid: 2, chessId: 'b', row: 10, col: 6 }] }],
|
||||
defs: { chess: { a: guard({ id: 'a' }), b: guard({ id: 'b' }), c: guard({ id: 'c' }) } },
|
||||
players: [{ playerId: 'R1', side: 'R', colOffset: 8, units: [{ uid: 1, chessId: 'a', row: 10, col: 3 }, { uid: 2, chessId: 'b', row: 10, col: 6 }, { uid: 3, chessId: 'c', row: 12, col: 6 }] }],
|
||||
content: 'none',
|
||||
});
|
||||
hb.step();
|
||||
const order = hb.hooksOf('deploy').filter((c) => c.initial).map((c) => [c.unit.defId, c.unit.tileR, c.unit.tileC, c.unit.facing]);
|
||||
// board row 10 → boss row 3; mirrored cols: 3 → 17, 6 → 14; right→left in field coords
|
||||
assert.deepEqual(order, [['a', 3, 17, -1], ['b', 3, 14, -1]]);
|
||||
// board rows 10 / 12 → boss rows 3 / 5; mirrored cols: 3 → 17, 6 → 14; its own left first = the highest field column
|
||||
assert.deepEqual(order, [['a', 3, 17, -1], ['c', 5, 14, -1], ['b', 3, 14, -1]]);
|
||||
});
|
||||
|
||||
test('DP: starts at 10, +1/s, cap 99; dead operator redeploys after respawnTime when DP ≥ cost', () => {
|
||||
|
||||
@@ -543,7 +543,7 @@ test('ranged enemies attack allies in radius: blocker → taunt → latest deplo
|
||||
chess: { t_a: guard({ id: 't_a', stats: { maxHp: 1e6, atk: 0 } }), t_b: guard({ id: 't_b', stats: { maxHp: 1e6, atk: 0, tauntLevel: taunt } }) },
|
||||
enemies: { enemy_caster: enemyRec({ key: 'enemy_caster', hp: 1e6, speed: 1, atk: 100, range: 2.5, dmgType: 'arts' }) },
|
||||
},
|
||||
// deploy order: top→bottom, so t_b (row 11) deploys before t_a (row 10) ⇒ t_a is the latest deployed
|
||||
// deploy order: one column, top to bottom, so t_b (row 11) deploys before t_a (row 10) ⇒ t_a is the latest deployed
|
||||
units: [{ chessId: 't_a', row: 10, col: 8 }, { chessId: 't_b', row: 11, col: 8 }],
|
||||
enemies: [{ key: 'enemy_caster', pos: [10, 10] }], content: 'none', autoFinish: false,
|
||||
});
|
||||
|
||||
@@ -158,6 +158,36 @@ test('S1 / S3 running at a lethal hit end with the switch (清除自身一切Buf
|
||||
assert.equal(s2.h.b.emit('dollSwitch', { unit: s2.g, reason: 'test', done: false }).done, false, 'already the 替身');
|
||||
});
|
||||
|
||||
test('E: a lethal 流失 inside the switch (a second 阿戈尔 devour mark) — she leaves it at full HP, both ways (PRTS "切换途中重设自身生命至最大值"; community report #2)', { skip }, () => {
|
||||
// 乌尔比安 → 斯卡蒂 → 归溟幽灵鲨 → 深巡 → 幽灵鲨 → 隐现 (5 阿戈尔): 乌尔比安's mark switches her, 斯卡蒂's lands inside
|
||||
// the 1 s switch (无敌 does not stop a 流失, 不死 holds it at 1 HP; the switch is no knock-out, so the mark is not
|
||||
// cancelled). Until 0.1.3 she then fought the 替身's 20 s at 1 HP and fell to the first hit
|
||||
const ids = ['chess_char_5_05_a', 'chess_char_3_05_a', G, 'chess_char_1_04_a', 'chess_char_2_07_a', 'chess_char_1_01_a'];
|
||||
const h = makeBattle({
|
||||
seed: 4, timeLimit: 300, autoFinish: false, captureNoisy: true, hooks: ['damaged', 'death'],
|
||||
units: ids.map((chessId, i) => ({ chessId, row: 10, col: 3 + i })),
|
||||
bonds: { egirShip: { count: 5, active: true, tier: 2, layers: 0 } },
|
||||
});
|
||||
h.step();
|
||||
const g = h.unit(G);
|
||||
const marks = h.hooksOf('damaged').filter((c) => c.target === g && c.dmg?.tags?.includes('bond:egir:devour'));
|
||||
assert.equal(marks.length, 2, 'two devour marks resolve on her');
|
||||
assert.ok(g.alive && g.trait.doll && g.trait.dollSwitching, 'the first switched her');
|
||||
assert.ok(g.hp < 2, `the second, inside the switch, left 1 HP (不死; a regen tick aside): ${g.hp}`);
|
||||
h.run(DOLL_SWITCH + 0.05);
|
||||
assert.ok(g.trait.doll && !g.trait.dollSwitching, 'the 替身 fights');
|
||||
approx(g.hp, g.s.maxHp, 'at full HP when the switch ends');
|
||||
// the switch back: a lethal 流失 inside it is held at 1 HP, then she is the 本体 at full HP
|
||||
assert.ok(h.runUntil(() => !g.trait.doll && g.trait.dollSwitching, 25), 'switching back');
|
||||
h.b.loseHp(g, 1e9, { source: null });
|
||||
assert.ok(g.alive && g.hp < 2, `not knocked out while switching: held at 1 HP (${g.hp})`);
|
||||
h.run(DOLL_SWITCH + 0.05);
|
||||
assert.ok(!g.trait.doll && !g.trait.dollSwitching && g.form === null, 'the 本体');
|
||||
approx(g.hp, g.s.maxHp, 'at full HP when the switch back ends');
|
||||
assert.equal(h.hooksOf('death').filter((c) => c.unit === g).length, 0, 'never knocked out');
|
||||
checkInvariants(h.b);
|
||||
});
|
||||
|
||||
test('a 本体 holding 不死 (a running 坚固维式重锤 window) does not switch: held at 1 HP (PRTS "受到足以致命的伤害且未持有不死的情况下")', { skip }, () => {
|
||||
const { h, g, lethal } = setup({ items: ['chess_item_3_09_e_a'] });
|
||||
lethal();
|
||||
|
||||
@@ -256,16 +256,16 @@ function lastTarget(h, enemy) {
|
||||
return ev ? h.b.unitById(ev[2]) : null;
|
||||
}
|
||||
|
||||
test('#5 unblocked ranged enemies: highest taunt, then the latest deployed — the bottom row, rightmost (从上到下 > 从左到右)', () => {
|
||||
const units = [[12, 5], [11, 3], [9, 4], [9, 7], [10, 8]].map(([row, col], i) => ({ chessId: 't_wall', row, col, uid: i + 1 }));
|
||||
test('#5 unblocked ranged enemies: highest taunt, then the latest deployed — the rightmost column, its bottom (从上到下 > 从左到右: down each column, the columns left to right)', () => {
|
||||
const units = [[12, 5], [11, 3], [9, 4], [9, 7], [10, 8], [12, 8]].map(([row, col], i) => ({ chessId: 't_wall', row, col, uid: i + 1 }));
|
||||
const h = arena({ units, enemies: [{ key: 'enemy_t_shooter', pos: [10.5, 6] }] });
|
||||
h.step();
|
||||
const order = h.hooksOf('deploy').filter((c) => c.initial).map((c) => [c.unit.tileR, c.unit.tileC]);
|
||||
assert.deepEqual(order, [[12, 5], [11, 3], [10, 8], [9, 4], [9, 7]]);
|
||||
assert.deepEqual(order, [[11, 3], [9, 4], [12, 5], [9, 7], [12, 8], [10, 8]]);
|
||||
h.run(1.2);
|
||||
const e = h.enemy('enemy_t_shooter');
|
||||
const t = lastTarget(h, e);
|
||||
assert.deepEqual([t.tileR, t.tileC], [9, 7], 'the last deployed');
|
||||
assert.deepEqual([t.tileR, t.tileC], [10, 8], 'the last deployed');
|
||||
// a taunt level beats deployment order
|
||||
const h2 = arena({ units, enemies: [{ key: 'enemy_t_shooter', pos: [10.5, 6] }] });
|
||||
h2.step();
|
||||
@@ -279,7 +279,7 @@ test('#5 the initial deployment puts every operator before the summons (PRTS 卫
|
||||
const h = arena({ units: [{ chessId: 't_wall', row: 12, col: 3, uid: 1 }, { chessId: 't_wall', row: 9, col: 8, uid: 2 }, tok], enemies: [{ key: 'enemy_t_shooter', pos: [10.5, 6] }] });
|
||||
h.step();
|
||||
const order = h.hooksOf('deploy').filter((c) => c.initial).map((c) => c.unit.uid);
|
||||
assert.deepEqual(order, [1, 2, 3], 'operators top→bottom, then the summon');
|
||||
assert.deepEqual(order, [1, 2, 3], 'the operators (by column from the left), then the summon');
|
||||
const t = h.unit(3);
|
||||
assert.ok(t.aggroSeq > h.unit(1).aggroSeq && t.aggroSeq > h.unit(2).aggroSeq);
|
||||
h.run(1.2);
|
||||
|
||||
@@ -175,6 +175,10 @@ test('dollkeeper: fatal damage ⇒ a 1 s switch, 20 s as the substitute (block 0
|
||||
assert.equal(h.b.applyStatus(u, 'stun', { duration: 5 }), false, '眩晕 immune');
|
||||
h.b.loseHp(u, 1e5);
|
||||
assert.ok(u.alive && u.trait.doll && u.hp >= 1, '不死');
|
||||
const low = u.hp;
|
||||
assert.ok(u.s.flags.noHeal && u.s.flags.healFree, '禁疗 during the switch');
|
||||
assert.equal(h.b.heal(u, u, 500), 0, 'a self-heal does not land');
|
||||
approx(u.hp, low);
|
||||
u.hp = 2000;
|
||||
h.run(1.05);
|
||||
assert.ok(u.trait.doll && !u.trait.dollSwitching, 'fighting as the substitute');
|
||||
|
||||
@@ -56,11 +56,13 @@ describe('gameLogic.mergeTarget (mirror of server board.js mergeTile)', () => {
|
||||
assert.deepEqual(mergeTarget({ board: [onBoard(A, 10, 4, 'UP')], hand: [inHand(A, 2)] }, A, getChess), { row: 10, col: 4, dir: 'UP' });
|
||||
assert.deepEqual(mergeTarget({ board: [onBoard(A, 10, 4)], temp: [inHand(A, 2)] }, A, getChess), { row: 10, col: 4, dir: 'RIGHT' }, 'temp copies count');
|
||||
});
|
||||
test('two deployed copies → the one that deploys first: row desc, then col asc', () => {
|
||||
test('two deployed copies → the one that deploys first: col asc, then row desc (by column from the left, Battle.start)', () => {
|
||||
const priv = { board: [onBoard(A, 9, 2, 'DOWN'), onBoard(A, 11, 7, 'LEFT')], hand: [] };
|
||||
assert.deepEqual(mergeTarget(priv, A, getChess), { row: 11, col: 7, dir: 'LEFT' });
|
||||
assert.deepEqual(mergeTarget(priv, A, getChess), { row: 9, col: 2, dir: 'DOWN' }, 'the left column, though lower (row-major until 0.1.3)');
|
||||
const same = { board: [onBoard(A, 10, 8, 'UP'), onBoard(A, 10, 3, 'DOWN')], hand: [] };
|
||||
assert.deepEqual(mergeTarget(same, A, getChess), { row: 10, col: 3, dir: 'DOWN' });
|
||||
const column = { board: [onBoard(A, 9, 4, 'UP'), onBoard(A, 12, 4, 'LEFT')], hand: [] };
|
||||
assert.deepEqual(mergeTarget(column, A, getChess), { row: 12, col: 4, dir: 'LEFT' }, 'one column: the top first');
|
||||
});
|
||||
test('elites and other operators are ignored; 风丸 merges with one deployed copy', () => {
|
||||
const priv = { board: [{ ...onBoard(golden(A), 12, 2), golden: true }, onBoard('chess_char_1_02_a', 12, 3), onBoard(A, 9, 9)], hand: [inHand(A, 5)] };
|
||||
|
||||
Reference in new issue
Block a user