fix: correct community-reported combat and strategy draft behavior
Guard lethal stuns against a replacement deployment; keep bard auras running through stun; require an active Aegir bond for Clementia's strategy; allow Compass SP to bypass recovery locks; and refresh Hidden Core gun mitigation against current gun/spring companions. Set multiplayer strategy picks to 50s and describe renderer fallback without diagnosing unsupported hardware. Golden moves (full 285-scenario regeneration): - roster-008: inactive Aegir no longer gains 18 strategy layers. - boss-boss_4-solo: bard regeneration continues through stun. - hidden-boss_9-pair: mitigation covers first hits/current companions. - standin-07: hammer stun cannot follow an immediate revival deployment. No ending-reason changes. Sources and report-by-report findings: DESIGN 28.25.
This commit is contained in:
1 parent
77cd29f07a
commit
708caf7d06
37 files changed
+277
-103
No files matched your search
+1
-1
@@ -1 +1 @@
|
||||
{"season":"act2autochess","seasonName":"卫戍协议:盟约","modes":{"mode_training_1":{"modeId":"mode_training_1","name":"入门协议","code":"AC-TR-1","sortId":1,"type":"LOCAL","difficulty":"TRAINING","inScope":false,"color":"#4ed8af","iconId":"mode_training_icon","backgroundId":"background_training","desc":"用以熟悉协议内容的模式","effectDescList":["·仅可使用预置的调配干员","·无奖励","·无法用于解锁策略"],"unlockText":null,"specialPhaseTime":150,"activeBondIds":["yanShip","sargonShip","victoriaShip","kjeragShip","lateranoShip","egirShip","siracusaShip","kazimierzShip","preciShip","swiftShip","skillfulShip","arcaneShip","steadShip","deputShip","visiShip","miraShip","investShip","raidShip","indomShip","maniShip","emptyShip","soloShip","suntShip"],"inactiveBondIds":[],"inactiveEnemyKeys":[],"lastRound":4,"bossRound":4,"hiddenRound":null,"rounds":{"1":{"template":"act1autochess_tr01","bossTemplates":null,"combatTimeLimit":40,"levelMaxPlayTime":40,"prepTime":999,"prepTimeData":999,"isSpPrepare":false,"isBoss":false,"isHidden":false,"bossOvertimeAfter":null},"2":{"template":"act1autochess_tr02","bossTemplates":null,"combatTimeLimit":45,"levelMaxPlayTime":45,"prepTime":999,"prepTimeData":999,"isSpPrepare":false,"isBoss":false,"isHidden":false,"bossOvertimeAfter":null},"3":{"template":"act1autochess_tr02","bossTemplates":null,"combatTimeLimit":45,"levelMaxPlayTime":45,"prepTime":999,"prepTimeData":999,"isSpPrepare":true,"isBoss":false,"isHidden":false,"bossOvertimeAfter":null},"4":{"template":null,"bossTemplates":{"boss_7":"act1autochess_tr04"},"combatTimeLimit":null,"levelMaxPlayTime":180,"prepTime":999,"prepTimeData":999,"isSpPrepare":false,"isBoss":true,"isHidden":false,"bossOvertimeAfter":150}},"spRounds":[3],"combatTimeLimit":{"1":40,"2":45,"3":45,"4":null},"enemyScale":{"1":{"atk":1,"hp":1,"speed":1,"assumed":true},"2":{"atk":1,"hp":1,"speed":1,"assumed":true},"3":{"atk":1,"hp":1,"speed":1,"assumed":true},"4":{"atk":1,"hp":1,"speed":1,"assumed":true}},"bossHpScale":{"bloodPointKey":null,"unaffectedByEnemyScale":true},"upgradePrices":[4,4,6,9,12],"maxShopLevel":6,"shopSlots":{"1":{"chess":3,"item":0},"2":{"chess":3,"item":0},"3":{"chess":3,"item":0},"4":{"chess":3,"item":0},"5":{"chess":3,"item":1},"6":{"chess":3,"item":1}},"levelTagColors":{"1":"#434343","2":"#626654","3":"#445760","4":"#615B74","5":"#6C5E41","6":"#5d341e"},"stages":["act1autochess_m01"],"bossWeights":{"boss_7":5},"hiddenBossWeights":{}},"mode_single_funny":{"modeId":"mode_single_funny","name":"标准模拟","code":"AC-1","sortId":2,"type":"SINGLE","difficulty":"FUNNY","inScope":true,"color":"#f6a329","iconId":"mode_funny_icon","backgroundId":"background_funny","desc":"时长较短的模拟训练","effectDescList":["·可以快速完成作战","·常规奖励"],"unlockText":"通关【入门协议】后解锁","specialPhaseTime":150,"activeBondIds":["yanShip","sargonShip","victoriaShip","kjeragShip","siracusaShip","preciShip","swiftShip","steadShip","deputShip","visiShip","indomShip","maniShip","emptyShip"],"inactiveBondIds":["lateranoShip","egirShip","kazimierzShip","skillfulShip","arcaneShip","miraShip","investShip","raidShip","soloShip","suntShip"],"inactiveEnemyKeys":["enemy_1112_emppnt","enemy_1207_sfji","enemy_1275_dwlock_2","enemy_1234_dsubrl_2","enemy_10122_uacann_2","enemy_1272_nhtank_2","enemy_9008_acbunn","enemy_1174_duholy_2","enemy_1355_mrfly_2","enemy_1197_sfshu_2","enemy_10067_ftsjc","enemy_1270_nhstlk_2","enemy_1283_sgkill","enemy_1175_dushdo_2","enemy_1254_lypa_2","enemy_1425_lrcmra_2","enemy_1329_cbshld_2","enemy_1072_dlancer"],"lastRound":9,"bossRound":9,"hiddenRound":null,"rounds":{"1":{"template":"act1autochess_01","bossTemplates":null,"combatTimeLimit":45,"levelMaxPlayTime":45,"prepTime":null,"prepTimeData":300,"isSpPrepare":false,"isBoss":false,"isHidden":false,"bossOvertimeAfter":null},"2":{"template":"act1autochess_02","bossTemplates":null,"combatTimeLimit":45,"levelMaxPlayTime":45,"prepTime":null,"prepTimeData":300,"isSpPrepare":false,"isBoss":false,"isHidden":false,"bossOvertimeAfter":null},"3":{"template":"act1autochess_03","bossTemplates":null,"combatTimeLimit":55,"levelMaxPlayTime":55,"prepTime":null,"prepTimeData":300,"isSpPrepare":false,"isBoss":false,"isHidden":false,"bossOvertimeAfter":null},"4":{"template":"act1autochess_04","bossTemplates":null,"combatTimeLimit":55,"levelMaxPlayTime":55,"prepTime":null,"prepTimeData":300,"isSpPrepare":false,"isBoss":false,"isHidden":false,"bossOvertimeAfter":null},"5":{"template":"act1autochess_05","bossTemplates":null,"combatTimeLimit":55,"levelMaxPlayTime":55,"prepTime":null,"prepTimeData":300,"isSpPrepare":false,"isBoss":false,"isHidden":false,"bossOvertimeAfter":null},"6":{"template":"act1autochess_06","bossTemplates":null,"combatTimeLimit":70,"levelMaxPlayTime":70,"prepTime":null,"prepTimeData":300,"isSpPrepare":false,"isBoss":false,"isHidden":false,"bossOvertimeAfter":null},"7":{"template":"act1autochess_07","bossTemplates":nuLine truncated
|
||||
{"season":"act2autochess","seasonName":"卫戍协议:盟约","modes":{"mode_training_1":{"modeId":"mode_training_1","name":"入门协议","code":"AC-TR-1","sortId":1,"type":"LOCAL","difficulty":"TRAINING","inScope":false,"color":"#4ed8af","iconId":"mode_training_icon","backgroundId":"background_training","desc":"用以熟悉协议内容的模式","effectDescList":["·仅可使用预置的调配干员","·无奖励","·无法用于解锁策略"],"unlockText":null,"specialPhaseTime":150,"activeBondIds":["yanShip","sargonShip","victoriaShip","kjeragShip","lateranoShip","egirShip","siracusaShip","kazimierzShip","preciShip","swiftShip","skillfulShip","arcaneShip","steadShip","deputShip","visiShip","miraShip","investShip","raidShip","indomShip","maniShip","emptyShip","soloShip","suntShip"],"inactiveBondIds":[],"inactiveEnemyKeys":[],"lastRound":4,"bossRound":4,"hiddenRound":null,"rounds":{"1":{"template":"act1autochess_tr01","bossTemplates":null,"combatTimeLimit":40,"levelMaxPlayTime":40,"prepTime":999,"prepTimeData":999,"isSpPrepare":false,"isBoss":false,"isHidden":false,"bossOvertimeAfter":null},"2":{"template":"act1autochess_tr02","bossTemplates":null,"combatTimeLimit":45,"levelMaxPlayTime":45,"prepTime":999,"prepTimeData":999,"isSpPrepare":false,"isBoss":false,"isHidden":false,"bossOvertimeAfter":null},"3":{"template":"act1autochess_tr02","bossTemplates":null,"combatTimeLimit":45,"levelMaxPlayTime":45,"prepTime":999,"prepTimeData":999,"isSpPrepare":true,"isBoss":false,"isHidden":false,"bossOvertimeAfter":null},"4":{"template":null,"bossTemplates":{"boss_7":"act1autochess_tr04"},"combatTimeLimit":null,"levelMaxPlayTime":180,"prepTime":999,"prepTimeData":999,"isSpPrepare":false,"isBoss":true,"isHidden":false,"bossOvertimeAfter":150}},"spRounds":[3],"combatTimeLimit":{"1":40,"2":45,"3":45,"4":null},"enemyScale":{"1":{"atk":1,"hp":1,"speed":1,"assumed":true},"2":{"atk":1,"hp":1,"speed":1,"assumed":true},"3":{"atk":1,"hp":1,"speed":1,"assumed":true},"4":{"atk":1,"hp":1,"speed":1,"assumed":true}},"bossHpScale":{"bloodPointKey":null,"unaffectedByEnemyScale":true},"upgradePrices":[4,4,6,9,12],"maxShopLevel":6,"shopSlots":{"1":{"chess":3,"item":0},"2":{"chess":3,"item":0},"3":{"chess":3,"item":0},"4":{"chess":3,"item":0},"5":{"chess":3,"item":1},"6":{"chess":3,"item":1}},"levelTagColors":{"1":"#434343","2":"#626654","3":"#445760","4":"#615B74","5":"#6C5E41","6":"#5d341e"},"stages":["act1autochess_m01"],"bossWeights":{"boss_7":5},"hiddenBossWeights":{}},"mode_single_funny":{"modeId":"mode_single_funny","name":"标准模拟","code":"AC-1","sortId":2,"type":"SINGLE","difficulty":"FUNNY","inScope":true,"color":"#f6a329","iconId":"mode_funny_icon","backgroundId":"background_funny","desc":"时长较短的模拟训练","effectDescList":["·可以快速完成作战","·常规奖励"],"unlockText":"通关【入门协议】后解锁","specialPhaseTime":150,"activeBondIds":["yanShip","sargonShip","victoriaShip","kjeragShip","siracusaShip","preciShip","swiftShip","steadShip","deputShip","visiShip","indomShip","maniShip","emptyShip"],"inactiveBondIds":["lateranoShip","egirShip","kazimierzShip","skillfulShip","arcaneShip","miraShip","investShip","raidShip","soloShip","suntShip"],"inactiveEnemyKeys":["enemy_1112_emppnt","enemy_1207_sfji","enemy_1275_dwlock_2","enemy_1234_dsubrl_2","enemy_10122_uacann_2","enemy_1272_nhtank_2","enemy_9008_acbunn","enemy_1174_duholy_2","enemy_1355_mrfly_2","enemy_1197_sfshu_2","enemy_10067_ftsjc","enemy_1270_nhstlk_2","enemy_1283_sgkill","enemy_1175_dushdo_2","enemy_1254_lypa_2","enemy_1425_lrcmra_2","enemy_1329_cbshld_2","enemy_1072_dlancer"],"lastRound":9,"bossRound":9,"hiddenRound":null,"rounds":{"1":{"template":"act1autochess_01","bossTemplates":null,"combatTimeLimit":45,"levelMaxPlayTime":45,"prepTime":null,"prepTimeData":300,"isSpPrepare":false,"isBoss":false,"isHidden":false,"bossOvertimeAfter":null},"2":{"template":"act1autochess_02","bossTemplates":null,"combatTimeLimit":45,"levelMaxPlayTime":45,"prepTime":null,"prepTimeData":300,"isSpPrepare":false,"isBoss":false,"isHidden":false,"bossOvertimeAfter":null},"3":{"template":"act1autochess_03","bossTemplates":null,"combatTimeLimit":55,"levelMaxPlayTime":55,"prepTime":null,"prepTimeData":300,"isSpPrepare":false,"isBoss":false,"isHidden":false,"bossOvertimeAfter":null},"4":{"template":"act1autochess_04","bossTemplates":null,"combatTimeLimit":55,"levelMaxPlayTime":55,"prepTime":null,"prepTimeData":300,"isSpPrepare":false,"isBoss":false,"isHidden":false,"bossOvertimeAfter":null},"5":{"template":"act1autochess_05","bossTemplates":null,"combatTimeLimit":55,"levelMaxPlayTime":55,"prepTime":null,"prepTimeData":300,"isSpPrepare":false,"isBoss":false,"isHidden":false,"bossOvertimeAfter":null},"6":{"template":"act1autochess_06","bossTemplates":null,"combatTimeLimit":70,"levelMaxPlayTime":70,"prepTime":null,"prepTimeData":300,"isSpPrepare":false,"isBoss":false,"isHidden":false,"bossOvertimeAfter":null},"7":{"template":"act1autochess_07","bossTemplates":nuLine truncated
|
||||
+2
-2
@@ -80,7 +80,7 @@ legal from R14 into R15. The client mirrors it
|
||||
| Timer (real s, × `opts.timerScale`) | Value |
|
||||
|---|---|
|
||||
| INFO_CHECK | `config.timers.infoCheck` 25 |
|
||||
| band draft turn | `Match.BAND_TURN_SECONDS` 30 [ASSUMED] (= `timers.bandTurn`), the step's only countdown: `m.public.deadline` = the current turn's end, no step cap (`timers.bandDraft` 50 = the official whole step, informational) (co-op; solo / single human untimed) |
|
||||
| band draft turn | `Match.BAND_TURN_SECONDS` 50 (owner official-play report, 2026-10-10) (= `timers.bandTurn`), the step's only countdown: `m.public.deadline` = the current turn's end, no step cap (`timers.bandDraft` 50 = the official whole step, informational) (co-op; solo / single human untimed) |
|
||||
| BATTLE_CHECK | `battleCheck` 3 |
|
||||
| 机变 first / other pickers | `spFirst` 30 / `spTurn` 16 (co-op; solo / single human untimed) |
|
||||
| PREP | `modes[m].rounds[r].prepTime` (co-op; solo / single human untimed) |
|
||||
@@ -98,7 +98,7 @@ band draft / 机变 / PREP deadline, and BATTLE_CHECK / ROUND_START / SETTLE run
|
||||
`m.public.overtimeAt` = when the overtime drain starts, 150 real s; both on the field clock).
|
||||
|
||||
### 1.1 Band draft
|
||||
Co-op: random order (all seats, bots included), one pick per turn, ONE countdown: `BAND_TURN_SECONDS` 30 s per turn,
|
||||
Co-op: random order (all seats, bots included), one pick per turn, ONE countdown: `BAND_TURN_SECONDS` 50 s per turn,
|
||||
published as `m.public.deadline` (= `draft.turnDeadline`; `draft.turnSeconds` its length) — no step cap; AI seats pick
|
||||
at once. A turn that runs out takes the strategy the player highlights in the draft screen (`g.bandFocus {bandId?}`,
|
||||
`Match.timeoutBand`) while it is allowed and no teammate holds it, else `bandDraft.timeoutBandId` 华法琳, else the first
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@
|
||||
| 阶段 | 说明 |
|
||||
|---|---|
|
||||
| 确认本局信息 | 本局地图(标准模拟固定为「战场#01」;险境从 8 张地图中随机,绝境 / 终极从不含战场#01 的 7 张中随机)、敌方派系(三种特训敌人类型,如折射、飞行:和官方一样,每种占全局 15 个回合位中的 3 个,第 14 回合领袖战和第 15 回合隐秘核心也各占一个位,所以险境 / 绝境 / 终极第 1–13 回合中有 7–9 波是这三种类型、每种最多 3 波,其余各波是「特异」类型;每波只有一种类型)、**被禁用的盟约及其干员**、领袖预告。所有人确认后继续;同盟模拟最多 25 秒;只有你一名玩家时(独立模拟,或同盟模拟只带 AI 队友)不限时。 |
|
||||
| 选择策略 | 40 名策略(分队长)决定**初始目标生命值(20–45)**和一条永久规则。同盟:随机顺序轮流选,**每人 30 秒**,右上角和「决策中」显示同一个倒计时;AI 队友立即选完;队友已选的不能重复,每人可**跳过一次**(排到最后);创建者可以在同盟等待室打开「**AI 队友最后选择**」(默认关闭):选择策略和机变轮选时所有博士都排在 AI 队友前面(博士之间、AI 之间仍是随机顺序,AI 托管或掉线的博士也算博士),跳过时排到其他还没选的博士之后、AI 之前(没有其他博士时排到最后);超时自动选你**当前选中**的策略(已被队友选走则选华法琳)。独立:自由选择、不限时;同盟模拟只有你一名玩家时也不限时。选的时候可以点左侧决策顺序下方的「查看禁用盟约与干员」,再看一遍「确认本局信息」里的盟约(置灰、禁用人数)和本局禁用干员:只能查看,倒计时照常走,底部也显示轮到谁和剩余秒数;点「关闭」、点窗口外面或按 Esc 关闭,轮到下一位时会自动关闭。 |
|
||||
| 选择策略 | 40 名策略(分队长)决定**初始目标生命值(20–45)**和一条永久规则。同盟:随机顺序轮流选,**每人 50 秒**,右上角和「决策中」显示同一个倒计时;AI 队友立即选完;队友已选的不能重复,每人可**跳过一次**(排到最后);创建者可以在同盟等待室打开「**AI 队友最后选择**」(默认关闭):选择策略和机变轮选时所有博士都排在 AI 队友前面(博士之间、AI 之间仍是随机顺序,AI 托管或掉线的博士也算博士),跳过时排到其他还没选的博士之后、AI 之前(没有其他博士时排到最后);超时自动选你**当前选中**的策略(已被队友选走则选华法琳)。独立:自由选择、不限时;同盟模拟只有你一名玩家时也不限时。选的时候可以点左侧决策顺序下方的「查看禁用盟约与干员」,再看一遍「确认本局信息」里的盟约(置灰、禁用人数)和本局禁用干员:只能查看,倒计时照常走,底部也显示轮到谁和剩余秒数;点「关闭」、点窗口外面或按 Esc 关闭,轮到下一位时会自动关闭。 |
|
||||
| 机变 | 部分回合开始前出现:同盟共 6 张卡轮流各取 1 张(随机顺序,开启「AI 队友最后选择」时博士先选;首位 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 队友)的同盟模拟不限时。所有人「准备就绪」即提前开战,全员就绪前可取消;点「准备就绪」时商店自动收起、棋盘放大到全场,取消准备时商店再展开(资金旁的「收起」和商店标签「展开商店」随时可以手动切换)。 |
|
||||
- **特殊地形点一下就有说明**:战场上的**活性源石**、**沼泽**、**排气格栅**、**深水区**,以及**红门 / 蓝门 / 传送点**,点一下就会弹出它自己的说明卡——名称、机制,以及**这一关自己的数值**(比如战场#04 的活性源石:每秒 70 点真实伤害、攻击力 +20%、攻击速度 +20、持续 300 秒)。休整期和作战期都能点。普通地板 / 道路 / 高台点了不会弹东西,所以点一下仍然只是「取消选中、关掉卡片」。
|
||||
|
||||
+5
-3
@@ -840,7 +840,7 @@ registration order. `battle.off(handle)` / `battle.off(name, fn)` / `battle.offO
|
||||
| `tick` | `{ dt }` | end of every tick |
|
||||
| `beforeAttack` | `{ attacker, targets, isSkill, profile }` | allies **and** enemies; replace/filter `ctx.targets` |
|
||||
| `enemyAttackStart` | `{ enemy, targets }` | an enemy starts a normal attack (it has targets), **before** 麻痹 may interrupt it (ai.js enemyAttack; the client's ON_BEFORE_ABILITY_SPELL_ON): a burst a handler causes interrupts that very attack (酒神 堕梦, PRTS 备注 "触发的元素爆发可打断当次普攻"); a handler that kills or stuns the enemy ends it |
|
||||
| `attack` | `{ attacker, targets, isSkill }` | an attack/heal was performed (projectiles may still be in flight) |
|
||||
| `attack` | `{ attacker, targets, isSkill, targetDeployments? }` | an attack/heal was performed (projectiles may still be in flight). Normal enemy attacks provide a `Map<Unit, deploySeq>` captured before their damage; post-attack control effects must not cross a target redeployment. |
|
||||
| `hit` | `{ source, target, dmg, credit }` | before mitigation; mutate `dmg` (not fired for gauge fills — see `elementHit`). `source` may be null (terrain; 无来源 `dmg.sourceless` bursts, whose `credit` names the unit credited) |
|
||||
| `elementHit` | `{ source, target, dmg }` | before a gauge fill (`dmg.type === 'element'`); mutate `dmg.amount`/`dmg.mul`, set `dmg.cancel` |
|
||||
| `damaged` | `{ source, target, amount, type, dmg, credit }` | after application (`amount` may be 0 when shielded); element fills too (with their source); 无来源: `source` null, `credit` set |
|
||||
@@ -996,8 +996,10 @@ flag alters movement or 失衡.
|
||||
attack speed paces it, and an SP refund that refills the bar at once (迅捷) no longer recasts it every tick (GitHub #298,
|
||||
引星棘刺 S1; [ASSUMED] one attack interval: PRTS prints no 前后摇 for such casts). The cast is no attack: the unit's
|
||||
attacks and heals keep their own rhythm, and its first cast comes as soon as it is ready (#124). `gainSp` is ignored while a duration/ammo/toggle skill
|
||||
runs (its bar shows the skill), whatever the reason, and — any reason but `'init'` — while the unit has the `noSp`
|
||||
flag (阻回: "停止并阻止任意形式的技力回复"; the operators' 凋亡 burst, §3).
|
||||
runs (its bar shows the skill), and while the unit has `noSp` (阻回; the operators' 凋亡 burst, §3), except for
|
||||
deployment initialization (`reason: 'init'`) and explicitly documented bypasses.
|
||||
`gainSp(n, reason, silent, { ignoreLock: true })` bypasses both locks while retaining the source reason, charge cap
|
||||
and `spGain` hook; used by 黄沙罗盘 (PRTS says all its SP ignores 阻回), including its caster's +3 SP.
|
||||
- Kinds: `duration` (mods for `duration` s), `ammo` (mods until `ammo` attacks were made, optional duration cap),
|
||||
`instant` (onStart + optional one-shot attack override applied to the next attack), `charges` (instant with charges),
|
||||
`passive` (always on from deployment, no SP), `toggle` (stays on until death once activated).
|
||||
|
||||
@@ -121,3 +121,7 @@ sound. A selected mode with no mix uses its own default volume/probability, not
|
||||
A model that arrives after an actual deployment continues Start at the elapsed game time; after the clip's duration it
|
||||
shows the current base pose. A subsequent attack/death supersedes that pending entrance. No extra deployment sound
|
||||
or per-phase animation schedule is added by this change.
|
||||
|
||||
The simplified-view notice describes a renderer load failure, rather than declaring the device incapable of 3D.
|
||||
The same fallback covers import failure, timeout and WebGL initialization errors; a reload can retry.
|
||||
It does not diagnose a particular user's GPU or persist a disabled renderer preference (§28.25).
|
||||
@@ -202,3 +202,17 @@ the existing per-tick behavior. The optional flag defaults to false for compatib
|
||||
0.2.3 echo/counter interaction (DESIGN §28.24): recursively earned “余音” pulses are drained FIFO by that echo in the
|
||||
same call, retaining the earned attack value and the lethal-hit pulse. [ASSUMED] This local order avoids exhausting
|
||||
the hook-depth guard on scaled hit-count HP; the guard remains unchanged for other recursive handlers.
|
||||
|
||||
### Community corrections (2026-10-10, §28.25)
|
||||
|
||||
- A normal enemy attack's post-hit status belongs to the target's deployment at the strike. If lethal damage
|
||||
immediately redeploys the target (不屈), that old attack cannot stun the new deployment.
|
||||
- Bard continuous regeneration, including 浊心斯卡蒂's replacement trait, continues through stun; leaving the field
|
||||
stops refreshing its aura. Stun still prevents attacks and skill use.
|
||||
- 黄沙罗盘 SP ignores both an active skill's recovery lock and `noSp`, including the caster and other 萨尔贡 recipients
|
||||
of the 浓茶 combination. Other SP gifts retain their existing restrictions.
|
||||
- 克莱门莎's 崇高牺牲 strategy requires 阿戈尔 to be active before a knock-out grants layers.
|
||||
- 隐秘核心 铳 takes 20% physical/arts damage while another gun or spring remains. The condition is checked on each hit
|
||||
as well as by its aura tick, including the first hit and immediately after the last companion leaves.
|
||||
|
||||
Sources and the feedback items requiring no simulation change are recorded in [§28.25](../history/0.2.3.md#2825-community-feedback-of-2026-10-10).
|
||||
@@ -7,7 +7,7 @@ Part of [DESIGN.md](../DESIGN.md) (the index; section numbers are global).
|
||||
### 6.1 State machine (Match.js)
|
||||
|
||||
```
|
||||
LOBBY(room) → INFO_CHECK (co-op 25 s; solo and any single-human match untimed (§18.2); all ready ⇒ skip; the operator loadout (§16) locks when it ends) → BAND_DRAFT (co-op: random order — the room option 「AI 队友最后选择」 (host, off by default; `room.setAiPicksLast`) puts every human seat before every AI seat, each group keeping the drawn order, and draws no extra random number; a skipping human goes behind the other humans still to pick and ahead of the AI seats, or to the very end when no other human remains [ASSUMED] —, ONE countdown — `BAND_TURN_SECONDS` 30 s per turn = m.public.deadline, AI seats pick at once, 1 skip each, a strategy a teammate already took is refused (队友已选), timeout ⇒ the strategy the player highlights (`g.bandFocus`) while free, else band_bldsk, else the first free one; solo: free pick, no timer, no skip; a single human: untimed) → BATTLE_CHECK (3 s)
|
||||
LOBBY(room) → INFO_CHECK (co-op 25 s; solo and any single-human match untimed (§18.2); all ready ⇒ skip; the operator loadout (§16) locks when it ends) → BAND_DRAFT (co-op: random order — the room option 「AI 队友最后选择」 (host, off by default; `room.setAiPicksLast`) puts every human seat before every AI seat, each group keeping the drawn order, and draws no extra random number; a skipping human goes behind the other humans still to pick and ahead of the AI seats, or to the very end when no other human remains [ASSUMED] —, ONE countdown — `BAND_TURN_SECONDS` 50 s per turn = m.public.deadline, AI seats pick at once, 1 skip each, a strategy a teammate already took is refused (队友已选), timeout ⇒ the strategy the player highlights (`g.bandFocus`) while free, else band_bldsk, else the first free one; solo: free pick, no timer, no skip; a single human: untimed) → BATTLE_CHECK (3 s)
|
||||
→ loop r = 1..lastRound:
|
||||
ROUND_START (income, upgrade price −1 (floor 0), temp NOT wiped — what overflowed after the last prep's deadline is shown in this
|
||||
prep (§6.2 temp overflow), unfrozen shop slots rerolled, frozen kept, <进入休整期时> effects)
|
||||
|
||||
@@ -348,3 +348,50 @@ and the queue clears. Both tests fail against the previous implementation. Toget
|
||||
刺玫 healing suites, 273 tests pass. The benchmark now writes actual diagnostic messages beside nonzero error counts,
|
||||
so failures cannot be mistaken for ordinary performance results. Recomputed golden and final benchmark results are
|
||||
recorded with the candidate; do not reuse the earlier, erroneous benchmark as a clean result.
|
||||
|
||||
### 28.25 Community feedback of 2026-10-10
|
||||
|
||||
The current development candidate was checked against thirteen reports, including feedback explicitly describing
|
||||
0.2.1. Sources: [PRTS mode strategies](https://prts.wiki/w/卫戍协议:盟约),
|
||||
[alliance/item notes](https://prts.wiki/w/卫戍协议:盟约_下半/PRTS盟约记录),
|
||||
[吟游者 branch notes](https://prts.wiki/w/分支特性信息/data),
|
||||
[假想敌:铳](https://prts.wiki/w/假想敌:铳), the cached official enemy and activity tables, and the owner's supplied
|
||||
克莱门莎 official showcase. The multiplayer 50-second setting is the owner's explicit official-play requirement.
|
||||
|
||||
| Report | Verified result / change |
|
||||
|---|---|
|
||||
| 1. Lethal hammer/sailor stun after 不屈 | Fixed. Enemy normal attacks snapshot target deployments before damage. The subsequent periodic stun cannot affect a replacement deployment created during that strike. A surviving target is still stunned normally. Covers both hammer variants (third strike) and both dock sailors (fourth strike). |
|
||||
| 2. Opening strategy time | Multiplayer picks now have 50 seconds each, including skips; the header and per-turn deadline remain one clock. Solo and single-human matches remain untimed. Regenerated `config.json` from the builder. |
|
||||
| 3. Fartooth + dollkeepers | Already fixed in §28.1. Existing actual Kazemaru and Specter alter swap/return tests pass, including active-bond gating, caps and repeated swaps. |
|
||||
| 4. Gavial S2 pulls final boss | Not reproduced. Both tier-4 and tier-5 S2 implementations use the shared pull API, which rejects boss-pool leaders. New actual-S2 tests hit a final boss without moving it and additionally reject exaggerated pull force. |
|
||||
| 5. Clementia strategy gains inactive Aegir | Fixed. PRTS explicitly requires activation; the former `requireActive: false` and its old test contradicted that note. An inactive bond now gains nothing; an active one gains the knocked-out member's tier, still disabled in unite/boss battles. This is the strategy, separate from the new playable operator. |
|
||||
| 6. Pith/Aegir ATK in 0.2.1 | No confirmed current defect. Devoured ATK is `atkFinal`, added after an operator's own ATK percentages, then scaled by independent multipliers; existing numeric tests check this order. The report lacks the full lineup, skill/equipment state and exact unrounded stats, so 5340 versus “about 6040” alone cannot establish a discrepancy. |
|
||||
| 7. Stunned bards stop regeneration | Fixed for the default trait and Skadi alter's custom continuous trait. Aura refresh requires being on-field, not being able to act. Stun still blocks attacks/skill use, and leaving stops the aura. |
|
||||
| 8. Stationary redeployment loses Raid | Current code grants the ATK/HP bonus to the deployment made by Raid's relocation. Ordinary redeployment clears it; the existing passive-skill revive tests do jump promptly when their conditions hold. PRTS's “期间” does not settle whether an ordinary stationary redeploy should retain it. Preserve this rule pending an official comparison of that exact case. |
|
||||
| 9. Gun boss “90% reduction absent” | The official value is damage ×0.2 (80% reduction), already implemented. Fixed an actual coverage gap: PRTS includes another gun as well as springs in the condition. Also refresh on hit so the first hit and immediate companion removal cannot see an outdated aura. |
|
||||
| 10. Compass/tea +3 bypasses SP lock | Fixed. PRTS says all 黄沙罗盘 SP bypasses 阻回. A documented `ignoreLock` option permits the first-end refund and combo gift during `noSp` or a running skill, including the caster. Ordinary SP gifts still obey both locks. |
|
||||
| 11. Dobermann whip random bounty | Already implemented as a private three-choice selection (§27). Existing choice and reconnect tests pass. A timer/bot can still resolve an unanswered choice automatically. |
|
||||
| 12. Lemuen ammo does not gain layers | Not reproduced with real S3 consumption, normal and elite. Each ten bullets grants +2/+4 to each active named bond when at least three own operators share the row. Two actual casts cross that threshold and gain the expected layers. The existing garrison tests also cover fewer than three in the row. |
|
||||
| 13. Sudden simplified view | The message is emitted for any renderer startup failure, including import timeout, not exclusively unsupported hardware. Reworded it to describe the failure and offer reload. The prior mount-cancellation/late-disposal fix remains; the report alone does not identify this user's browser failure. No forced persistent 2D preference is written. |
|
||||
|
||||
The new same-strike revival, bard and SP-lock regressions fail seven checks against the pre-change simulation and
|
||||
pass after these fixes. The additional band and boss tests exercise inactive activation and same-tick reduction.
|
||||
The validation record accompanying the candidate lists full CI, browser recovery and golden results.
|
||||
|
||||
The official showcase was reviewed in talent/S1, S2 and S3 segments. S2 shows a moving cabin then a stationary vortex,
|
||||
including the second battlefield demonstration. S3's repeated bombardments agree with the documented three-hit,
|
||||
one-second pulse interval. Crownslayer's supporting skill and smoke are visible in the later S3 segment and are not
|
||||
attributed to Clementia. Edited footage does not settle the absolute first-bomb offset, S2 splash radius, or all exile
|
||||
status rules; the existing marked assumptions remain. No visual resemblance is used as proof of extra immunity.
|
||||
|
||||
Full golden regeneration covers all 285 scenarios. Four digests intentionally move relative to the preceding candidate:
|
||||
|
||||
- `roster-008`: 崇高牺牲 no longer grants 18 阿戈尔 layers while inactive; combat damage and kills are unchanged.
|
||||
- `boss-boss_4-solo`: the bard's regeneration continues during stun, changing healing, subsequent attacks and snapshots.
|
||||
- `hidden-boss_9-pair`: 铳's reduction is checked for the first hit/current companions; boss HP left changes
|
||||
2,992,115 → 2,992,511.
|
||||
- `standin-07`: hammer stuns no longer cross the immediate revival deployment provided by M3茧甲; kills 9 → 10,
|
||||
counted leaks 71 → 70, with corresponding combat/RNG/snapshot differences.
|
||||
|
||||
No scenario's ending reason changes. The bonds, matches and DIY families are byte-identical to the preceding golden
|
||||
files. The 50-second setting and fallback-message change do not alter battle rules in those digests.
|
||||
+1
-1
@@ -239,7 +239,7 @@
|
||||
"继续作战": "Resume Battle",
|
||||
"正在载入模拟数据…": "Loading simulation data...",
|
||||
"正在进入模拟…": "Entering simulation...",
|
||||
"当前设备无法启用 3D / WebGL 渲染,已切换为简化视图(功能不受影响)": "3D / WebGL rendering is unavailable on this device; switched to a simplified view (all features still work)",
|
||||
"渲染器暂时未能加载,已切换为简化视图(功能不受影响,可刷新重试)": "The renderer could not load. Switched to the simplified view (all features remain available; reload to retry).",
|
||||
"已配发的装备无法销毁": "Equipped Equipment cannot be destroyed",
|
||||
"销毁道具": "Destroy Item",
|
||||
"道具": "Item",
|
||||
|
||||
+1
-1
@@ -245,7 +245,7 @@
|
||||
"继续作战": "戦闘を再開",
|
||||
"正在载入模拟数据…": "シミュレーションデータを読み込み中…",
|
||||
"正在进入模拟…": "シミュレーションに進入中…",
|
||||
"当前设备无法启用 3D / WebGL 渲染,已切换为简化视图(功能不受影响)": "この端末では3D / WebGLで描画できないため、簡易表示に切り替えました(機能に影響はありません)",
|
||||
"渲染器暂时未能加载,已切换为简化视图(功能不受影响,可刷新重试)": "描画エンジンを読み込めなかったため簡易表示に切り替えました(機能はそのまま利用できます。再読み込みで再試行)。",
|
||||
"已配发的装备无法销毁": "装備済みの装備品は破棄できません",
|
||||
"销毁道具": "アイテムを破棄",
|
||||
"道具": "アイテム",
|
||||
|
||||
+1
-1
@@ -241,7 +241,7 @@
|
||||
"继续作战": "전투 재개",
|
||||
"正在载入模拟数据…": "시뮬레이션 데이터를 불러오는 중…",
|
||||
"正在进入模拟…": "시뮬레이션에 진입하는 중…",
|
||||
"当前设备无法启用 3D / WebGL 渲染,已切换为简化视图(功能不受影响)": "이 기기에서는 3D / WebGL 렌더링을 사용할 수 없어 간략 보기로 전환했습니다. 기능에는 영향이 없습니다.",
|
||||
"渲染器暂时未能加载,已切换为简化视图(功能不受影响,可刷新重试)": "렌더러를 불러오지 못해 간소화 화면으로 전환했습니다(모든 기능을 사용할 수 있으며 새로고침하면 다시 시도합니다).",
|
||||
"已配发的装备无法销毁": "장착한 장비는 파기할 수 없습니다.",
|
||||
"销毁道具": "아이템 파기",
|
||||
"道具": "아이템",
|
||||
|
||||
@@ -243,7 +243,7 @@
|
||||
"继续作战": "繼續作戰",
|
||||
"正在载入模拟数据…": "正在載入模擬資料…",
|
||||
"正在进入模拟…": "正在進入模擬…",
|
||||
"当前设备无法启用 3D / WebGL 渲染,已切换为简化视图(功能不受影响)": "目前的裝置無法啟用 3D / WebGL 渲染,已切換為簡化檢視(功能不受影響)",
|
||||
"渲染器暂时未能加载,已切换为简化视图(功能不受影响,可刷新重试)": "繪圖引擎暫時未能載入,已切換為簡化檢視(功能不受影響,可重新整理再試)",
|
||||
"已配发的装备无法销毁": "已配發的裝備無法銷毀",
|
||||
"销毁道具": "銷毀道具",
|
||||
"道具": "道具",
|
||||
|
||||
@@ -624,7 +624,7 @@ function MatchScreen() {
|
||||
let asked = null;
|
||||
try { asked = new URLSearchParams(globalThis.location?.search || '').get('render'); } catch { asked = null; }
|
||||
if (asked === 'fallback' || globalThis.__SP_RENDER__ === 'fallback') return;
|
||||
toast(t('当前设备无法启用 3D / WebGL 渲染,已切换为简化视图(功能不受影响)'), 'info', { ttl: 5000 });
|
||||
toast(t('渲染器暂时未能加载,已切换为简化视图(功能不受影响,可刷新重试)'), 'info', { ttl: 5000 });
|
||||
}, [viewKind]);
|
||||
|
||||
// phase changes: banners, sounds, resets
|
||||
|
||||
@@ -568,7 +568,7 @@ const recJson = (rec) => { try { return rec ? JSON.stringify(rec) : ''; } catch
|
||||
/**
|
||||
* Bonds an IN_BATTLE layer gain of this player can name: its bond snapshot (every bond its lineup counts), plus bonds
|
||||
* its band, its effects (机变 cards, 驻守 …), its units and their items mention (content grants layers to those —
|
||||
* e.g. 克莱门莎's <阿戈尔>, requireActive: false). Anything else is a forged gain.
|
||||
* including referenced bonds outside the lineup snapshot). Anything else is a forged gain.
|
||||
*/
|
||||
function layerBondsOf(p, gd) {
|
||||
const out = new Set(Object.keys(p.bonds && typeof p.bonds === 'object' ? p.bonds : {}));
|
||||
|
||||
@@ -26,6 +26,6 @@ export const DELAYS = Object.freeze({
|
||||
* Seconds of one turn of the co-op strategy draft (user playtest #4 item 4: the old 12 s per turn — research 06 §724,
|
||||
* itself [ASSUMED] — inside the 50 s step was far too little and counted apart from the header's 50 s). [ASSUMED]: the
|
||||
* official data only gives the whole BAND_CHECK step (autoChessData.enterStepList: 50 s, hint 15 s); the turn clock is
|
||||
* the remake's. It is also the step's only countdown (m.public.deadline = draft.turnDeadline). × timerScale.
|
||||
* the remake's (50 s per turn per the owner's 2026-10-10 official-play report). It is also the step's only countdown (m.public.deadline = draft.turnDeadline). × timerScale.
|
||||
*/
|
||||
export const BAND_TURN_SECONDS = 30;
|
||||
export const BAND_TURN_SECONDS = 50;
|
||||
+3
-1
@@ -906,6 +906,8 @@ function enemyAttack(b, e, prevCd) {
|
||||
// — the 'atk' event (kind `e.profile.shot`, drawn by the content's own fx), cooldown, pause, the 'attack' hook — and
|
||||
// the content's 'attack' handler deals its damage
|
||||
const deferred = !!(e.profile && e.profile.deferHit);
|
||||
// A lethal strike may immediately redeploy its target. Post-attack effects still belong to the struck deployment.
|
||||
const targetDeployments = new Map(targets.map((t) => [t, t.deploySeq]));
|
||||
for (const t of targets) {
|
||||
b._ev(['atk', e.id, t.id, deferred ? (e.profile.shot || 'none') : rangedShot ? 'enemy' : 'none']);
|
||||
if (deferred) continue;
|
||||
@@ -919,7 +921,7 @@ function enemyAttack(b, e, prevCd) {
|
||||
b.addProjectile({ from: e, target: t, speed: PROJECTILE_SPEEDS.enemy, visual: 'enemy', source: e, onHit: (c) => hit(c.target, true) });
|
||||
} else hit(t);
|
||||
}
|
||||
if (b._hooks.attack) b.emit('attack', { attacker: e, targets, isSkill: false });
|
||||
if (b._hooks.attack) b.emit('attack', { attacker: e, targets, targetDeployments, isSkill: false });
|
||||
e.atkCd = e.s.interval;
|
||||
// stands for the rest of its attack clip (attackStand; the wind-up was stood before the strike) — every enemy, blocked
|
||||
// or not: PRTS 状态机, an enemy's ATTACK / COMBAT state "攻击结束后回退到MOVE状态" and checks only 异常状态 every frame
|
||||
|
||||
@@ -84,7 +84,7 @@ const BY_KEY = {
|
||||
}, { priority: PRIO_BAND_REVIVE });
|
||||
},
|
||||
|
||||
// 克莱门莎 崇高牺牲
|
||||
// 克莱门莎 崇高牺牲: PRTS requires the 阿戈尔 bond to be active.
|
||||
act1autochess_band13_buff(battle, ps, p, bandId) {
|
||||
const bond = typeof p.bond_id === 'string' && p.bond_id ? p.bond_id : null;
|
||||
if (!bond) return;
|
||||
@@ -93,7 +93,7 @@ const BY_KEY = {
|
||||
const u = c.unit;
|
||||
if (c.reason !== 'killed' || !isOp(u) || u.ownerId !== ps.playerId || !unitBonds(u).includes(bond)) return;
|
||||
const n = byLevel ? tierOf(u) : Math.max(1, Math.floor(num(p.value, 1)));
|
||||
if (gainLayers(battle, { playerId: ps.playerId, bonds: bond, n, requireActive: false, source: u, reason: 'band' }) > 0) {
|
||||
if (gainLayers(battle, { playerId: ps.playerId, bonds: bond, n, requireActive: true, source: u, reason: 'band' }) > 0) {
|
||||
fxOn(battle, 'layer', u, keyOf(bandId), bandId, { bond, n });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -621,13 +621,16 @@ function kitGun(ab, e, b) {
|
||||
},
|
||||
];
|
||||
if (hidden) {
|
||||
list.push({
|
||||
iv: 0.25,
|
||||
tick(b2, e2) { // 【未尽的告解】 physical / arts damage taken ×4.damage_scale while the 碎铳之簧 it came with remain
|
||||
const springs = b2.enemies.some((o) => o.alive && isSpring(o));
|
||||
if (springs) { const m = T(ab, '4.damage_scale') ?? 1; b2.addBuff(e2, { key: 'boss:confession', duration: 0.35, refresh: 'replace', mods: { physTakenMul: m, artsTakenMul: m } }); }
|
||||
},
|
||||
});
|
||||
// PRTS 未尽的告解: another gun OR spring keeps the reduction. Refresh on hit as well so the first hit and
|
||||
// a hit immediately after the last companion dies see the current condition, not a stale polling result.
|
||||
const confession = (b2, e2) => {
|
||||
const other = b2.enemies.some((o) => o !== e2 && o.alive && (isGun(o) || isSpring(o)));
|
||||
if (other) {
|
||||
const m = T(ab, '4.damage_scale') ?? 1;
|
||||
b2.addBuff(e2, { key: 'boss:confession', duration: 0.35, refresh: 'replace', mods: { physTakenMul: m, artsTakenMul: m } });
|
||||
} else b2.removeBuff(e2, 'boss:confession');
|
||||
};
|
||||
list.push({ iv: 0.25, tick: confession, hitIn(c, b2, e2) { confession(b2, e2); } });
|
||||
if (s3) list.push({
|
||||
iv: s3.bb.interval ?? 1,
|
||||
tick(b2, e2) { // 【盲信之誓】 links: phys per second on operators standing on a line ("无视无法选择、迷彩": 起飞 too)
|
||||
|
||||
@@ -138,7 +138,10 @@ const nthAttackStatus = (n, key, dur, sil = true) => ({
|
||||
attack(c, b, e, a) {
|
||||
a.n++;
|
||||
if (a.n % n) return;
|
||||
for (const t of c.targets) if (t.alive && dur > 0) b.applyStatus(t, key, { duration: dur, source: e });
|
||||
for (const t of c.targets) {
|
||||
if (!t.alive || !(dur > 0) || (c.targetDeployments && c.targetDeployments.get(t) !== t.deploySeq)) continue;
|
||||
b.applyStatus(t, key, { duration: dur, source: e });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -735,7 +735,7 @@ const BY_ITEM = {
|
||||
}
|
||||
});
|
||||
},
|
||||
// 黄沙罗盘: 初始技力 +init_sp each deploy; (萨尔贡) first skill end +sp; + 萨尔贡浓茶: each cast all 萨尔贡 +addition_sp
|
||||
// 黄沙罗盘 (PRTS: all its SP ignores 阻回): 初始技力 +init_sp each deploy; (萨尔贡) first skill end +sp; + 萨尔贡浓茶: each cast all 萨尔贡 +addition_sp
|
||||
chess_item_6_07_e(battle, u, rec, S) {
|
||||
const p = bp(rec, 'act1autochess_equip_acarm103_global_buff');
|
||||
if (!p) return;
|
||||
@@ -749,14 +749,14 @@ const BY_ITEM = {
|
||||
S.on('skillEnd', (c) => {
|
||||
if (c.unit !== u || firstDone || c.reason === 'death' || !memberOf(battle, u, 'sargonShip')) return;
|
||||
firstDone = true;
|
||||
if (u.skill && u.alive) u.skill.gainSp(num(p.sp), 'item');
|
||||
if (u.skill && u.alive) u.skill.gainSp(num(p.sp), 'item', false, { ignoreLock: true });
|
||||
});
|
||||
S.on('skillStart', (c) => {
|
||||
if (c.unit !== u || !partner || !memberOf(battle, u, 'sargonShip') || !carries(battle, u, partner)) return;
|
||||
const n = num(p.addition_sp);
|
||||
if (!(n > 0)) return;
|
||||
for (const a of battle.allyUnits) {
|
||||
if (a.kind === 'op' && a.ownerId === u.ownerId && onField(a) && a.skill && memberOf(battle, a, 'sargonShip')) a.skill.gainSp(n, 'item');
|
||||
if (a.kind === 'op' && a.ownerId === u.ownerId && onField(a) && a.skill && memberOf(battle, a, 'sargonShip')) a.skill.gainSp(n, 'item', false, { ignoreLock: true });
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -82,7 +82,7 @@ function skadi2(bb, chess, def) {
|
||||
if (unit.skill) unit.skill.addTriggerRange(() => seaborns(battle, unit));
|
||||
let n = 0;
|
||||
battle.every(0.5, () => {
|
||||
if (!unit.canAct) return;
|
||||
if (!live(unit)) return; // the continuous trait (including its skill conversion) survives stun
|
||||
for (let i = unit.buffs.length - 1; i >= 0; i--) if (isInspire(unit.buffs[i])) battle.removeBuff(unit, unit.buffs[i]);
|
||||
n++;
|
||||
const toks = seaborns(battle, unit);
|
||||
|
||||
@@ -19,7 +19,7 @@ const GROUND = Object.freeze({ canHitFly: false, groundOnly: true });
|
||||
const AIR = Object.freeze({ canHitFly: true });
|
||||
const SPEED = 2, DISTANCE = 3, CATCH_HALF = 0.55, VORTEX_RADIUS = 1.5;
|
||||
const SPLASH_RADIUS = 1.5; // [ASSUMED] PRTS has not published S2's normal attack splash radius.
|
||||
const FIRST_BOMB = 0.5; // [ASSUMED] PRTS lists the landing delay as unknown.
|
||||
const FIRST_BOMB = 0.5; // [ASSUMED] Neither PRTS nor the edited official demo settles the first landing offset.
|
||||
const CAST = 'clemnt:cast', CARRY = 'clemnt:carry:';
|
||||
const skillBb = (raw, id) => skillRec(raw, id)?.bb ?? {};
|
||||
const tile = (u) => [Math.round(u.y), Math.round(u.x)];
|
||||
|
||||
@@ -430,7 +430,8 @@ export function bardRegen(battle, bard, ally, value, duration = BARD_REGEN_DUR)
|
||||
|
||||
const installBard = (battle, unit) => {
|
||||
battle.every(BARD_REGEN_IV, () => {
|
||||
if (!unit.canAct) return;
|
||||
// Continuous regeneration is an aura, so stun does not stop it.
|
||||
if (!unit.alive || !unit.deployed || unit.hidden) return;
|
||||
const v = unit.s.atk * (unit.profile.auraRatio ?? 0.1);
|
||||
for (const ally of battle.alliesInGrid(unit)) bardRegen(battle, unit, ally, v);
|
||||
}, { owner: unit });
|
||||
|
||||
@@ -322,11 +322,12 @@ export class SkillRuntime {
|
||||
}
|
||||
|
||||
/** Add SP (fires the `spGain` hook). Ignored while a duration/ammo/toggle skill runs (its bar shows the skill). */
|
||||
gainSp(amount, reason = 'time', silent = false) {
|
||||
// ignoreLock is reserved for explicitly documented exceptions (黄沙罗盘), not ordinary SP gifts.
|
||||
gainSp(amount, reason = 'time', silent = false, { ignoreLock = false } = {}) {
|
||||
if (this.noSkill || this.kind === 'passive' || !(amount > 0)) return 0;
|
||||
if (this.active && this.isTimed && reason !== 'init') return 0;
|
||||
if (this.active && this.isTimed && reason !== 'init' && !ignoreLock) return 0;
|
||||
// 阻回 (the operators' 凋亡 burst, damage.js): "停止并阻止任意形式的技力回复" — no SP of any kind (time, attack, hurt, gifts)
|
||||
if (reason !== 'init' && this.unit.s.flags.noSp) return 0;
|
||||
if (reason !== 'init' && !ignoreLock && this.unit.s.flags.noSp) return 0;
|
||||
let cost = this.spCost;
|
||||
if (this.charges >= this.maxCharges && this.sp >= cost) return 0;
|
||||
let amt = amount;
|
||||
|
||||
@@ -572,18 +572,22 @@ test('埃芒加德 命结之秘: the first 3 knock-downs of the battle revive at
|
||||
cover('band_ermengard');
|
||||
});
|
||||
|
||||
test('克莱门莎 崇高牺牲: a <阿戈尔> operator knocked down ⇒ +its tier <阿戈尔> layers (no activation needed; none in 联防/boss)', () => {
|
||||
test('克莱门莎 崇高牺牲: a <阿戈尔> operator knocked down ⇒ +its tier <阿戈尔> layers (requires activation; none in 联防/boss)', () => {
|
||||
const ops = { t_eg: op('t_eg', { bonds: ['egirShip'], tier: 3 }), t_x: op('t_x', { bonds: ['yanShip'], tier: 5 }) };
|
||||
const h = fight({ band: 'band_clementia', ops, units: [{ chessId: 't_eg', row: 10, col: 4 }, { chessId: 't_x', row: 11, col: 4 }], foes: [[10, 9]] });
|
||||
const h = fight({ band: 'band_clementia', bonds: active('egirShip'), ops, units: [{ chessId: 't_eg', row: 10, col: 4 }, { chessId: 't_x', row: 11, col: 4 }], foes: [[10, 9]] });
|
||||
h.step(1);
|
||||
h.b.dealDamage(foe(h), h.unit('t_eg'), { amount: 1e6, type: 'true' });
|
||||
h.b.dealDamage(foe(h), h.unit('t_x'), { amount: 1e6, type: 'true' });
|
||||
h.b.retreat(h.unit('t_eg')); // not knocked down: nothing (already dead anyway)
|
||||
assert.deepEqual(h.b.result().perPlayer.p1.layerGains, { egirShip: 3 });
|
||||
const u = fight({ band: 'band_clementia', kind: 'unite', ops, units: [{ chessId: 't_eg', row: 10, col: 4 }], foes: [[10, 9]] });
|
||||
const u = fight({ band: 'band_clementia', bonds: active('egirShip'), kind: 'unite', ops, units: [{ chessId: 't_eg', row: 10, col: 4 }], foes: [[10, 9]] });
|
||||
u.step(1);
|
||||
u.b.dealDamage(foe(u), u.unit('t_eg'), { amount: 1e6, type: 'true' });
|
||||
assert.deepEqual(u.b.result().perPlayer.p1.layerGains, {});
|
||||
const inactive = fight({ band: 'band_clementia', ops, units: [{ chessId: 't_eg', row: 10, col: 4 }], foes: [[10, 9]] });
|
||||
inactive.step();
|
||||
inactive.b.dealDamage(foe(inactive), inactive.unit('t_eg'), { amount: 1e6, type: 'true' });
|
||||
assert.deepEqual(inactive.b.result().perPlayer.p1.layerGains, {}, 'an inactive 阿戈尔 bond gains no layers');
|
||||
cover('band_clementia');
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
// Community reports of 2026-10-10: actual attacks / skills, including death and redeployment in the same tick.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { makeBattle, chessRec, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
|
||||
|
||||
const bond = (layers = 0) => ({ count: 3, active: true, tier: 1, layers });
|
||||
const dummy = enemyRec({ key: 'dummy', hp: 1e9, atk: 0, speed: 0, mass: 0 });
|
||||
const wall = (id, opts = {}) => chessRec({ id, skill: null, stats: { maxHp: 1e7, atk: 0, def: 0, blockCnt: 3 }, ...opts });
|
||||
const finish = (h) => { checkInvariants(h.b); assert.deepEqual(h.b.errors, []); };
|
||||
const near = (actual, expected) => assert.ok(Math.abs(actual - expected) < 1e-6, `${actual} != ${expected}`);
|
||||
|
||||
for (const key of ['enemy_1045_hammer', 'enemy_1045_hammer_2', 'enemy_1160_hvyslr', 'enemy_1160_hvyslr_2']) {
|
||||
test(`${key}: a lethal stunning strike cannot stun the deployment created by 不屈`, () => {
|
||||
const h = makeBattle({ autoFinish: false, timeLimit: 100, defs: { chess: { wall: wall('wall') } },
|
||||
units: [{ chessId: 'wall', row: 10, col: 5 }], bonds: { indomShip: bond(300) } });
|
||||
const nth = key.includes('hvyslr') ? 4 : 3;
|
||||
h.step(); const u = h.unit('wall'); const e = h.spawn(key, { pos: [10, 5], mods: { speedMul: 0 } });
|
||||
assert.ok(h.runUntil(() => e.stats.attacks >= nth - 1, 40));
|
||||
const seq = u.deploySeq; u.hp = 1;
|
||||
assert.ok(h.runUntil(() => e.stats.attacks >= nth, 20));
|
||||
assert.ok(u.alive && u.deploySeq > seq, 'the lethal stunning strike immediately redeployed the operator');
|
||||
assert.ok(!u.s.flags.stun, 'the old attack cannot put stun on that new deployment');
|
||||
// The next stunning strike that does not kill must still stun normally.
|
||||
assert.ok(h.runUntil(() => e.stats.attacks >= nth * 2, 40));
|
||||
assert.ok(u.s.flags.stun); finish(h);
|
||||
});
|
||||
}
|
||||
|
||||
for (const skadi of [false, true]) {
|
||||
test(`${skadi ? '浊心斯卡蒂 custom' : 'default'} bard keeps its regeneration while stunned, stops after leaving`, () => {
|
||||
const bard = chessRec({ id: 'bard', profession: 'SUPPORT', subProfessionId: 'bard', attackKind: 'none', dmgType: 'heal',
|
||||
skill: null, rangeGrid: [[0, 0], [0, 1]], stats: { atk: 1000 } });
|
||||
const h = makeBattle({ autoFinish: false, defs: { chess: { bard, wall: wall('wall') } },
|
||||
units: [{ chessId: skadi ? 'chess_char_6_04_a' : 'bard', row: 10, col: 4, skillIndex: 1 }, { chessId: 'wall', row: 10, col: 5 }] });
|
||||
h.step(); const u = h.b.allyUnits[0], a = h.unit('wall'); a.hp = 100;
|
||||
if (skadi) { u.skill.gainSp(999, 'init'); assert.ok(u.skill.activate('test')); }
|
||||
h.run(1); const regen = a.s.hpRegen; assert.ok(regen > 0);
|
||||
h.b.applyStatus(u, 'stun', { duration: 10 }); h.run(2);
|
||||
near(a.s.hpRegen, regen); const hp = a.hp; h.run(1); assert.ok(a.hp > hp);
|
||||
h.b.kill(u, null); h.run(1); near(a.s.hpRegen, 0); finish(h);
|
||||
});
|
||||
}
|
||||
|
||||
test('黄沙罗盘 + 浓茶 bypass both the active skill lock and noSp, including the caster; ordinary SP still obeys them', () => {
|
||||
const op = (id, bonds) => chessRec({ id, bonds, skill: { spCost: 100, initSp: 0, duration: 20 } });
|
||||
const h = makeBattle({ autoFinish: false, defs: { chess: { a: op('a', ['sargonShip']), b: op('b', ['sargonShip']), c: op('c', []) } },
|
||||
units: [{ chessId: 'a', row: 10, col: 4, items: ['chess_item_6_07_e_a', 'chess_item_2_04_e_a'] },
|
||||
{ chessId: 'b', row: 11, col: 4 }, { chessId: 'c', row: 12, col: 4 }] });
|
||||
h.step(); const a = h.unit('a'), b = h.unit('b'), c = h.unit('c');
|
||||
for (const u of [a, b, c]) { u.skill.gainSp(100, 'init'); h.b.addBuff(u, { key: 'test:lock', flags: { noSp: true } }); }
|
||||
assert.ok(b.skill.activate('test')); assert.ok(c.skill.activate('test')); assert.ok(a.skill.activate('test'));
|
||||
near(a.skill.sp, 3); near(b.skill.sp, 3); near(c.skill.sp, 0);
|
||||
near(b.skill.gainSp(5, 'item'), 0); near(b.skill.sp, 3);
|
||||
a.skill.end('test'); near(a.skill.sp, 33); finish(h);
|
||||
});
|
||||
|
||||
for (const elite of [false, true]) {
|
||||
test(`蕾缪安 S3 real ammo consumption gains active bonds with three operators in the row (${elite ? 'elite' : 'normal'})`, () => {
|
||||
const id = `chess_char_6_01_${elite ? 'b' : 'a'}`;
|
||||
const h = makeBattle({ autoFinish: false, timeLimit: 120, hooks: ['ammoUsed'],
|
||||
defs: { chess: { a: wall('a'), b: wall('b') }, enemies: { dummy } },
|
||||
bonds: { lateranoShip: bond(), preciShip: bond() },
|
||||
units: [{ chessId: id, row: 10, col: 4 }, { chessId: 'a', row: 10, col: 2 }, { chessId: 'b', row: 10, col: 3 }] });
|
||||
h.step(); const u = h.unit(id); h.spawn('dummy', { pos: [10, 6] });
|
||||
for (let i = 0; i < 2; i++) { u.skill.gainSp(999, 'init'); assert.ok(u.skill.activate('test')); assert.ok(h.runUntil(() => !u.skill.active, 30)); }
|
||||
const spent = h.hooksOf('ammoUsed').filter((c) => c.unit === u).length;
|
||||
assert.ok(spent >= 10); const gains = h.result().perPlayer.p1.layerGains;
|
||||
for (const b of ['lateranoShip', 'preciShip']) assert.equal(gains[b], Math.floor(spent / 10) * (elite ? 4 : 2), b);
|
||||
finish(h);
|
||||
});
|
||||
}
|
||||
|
||||
for (const id of ['chess_char_4_23_a', 'chess_char_5_18_a']) {
|
||||
test(`${id} 百炼嘉维尔 S2 damages a final boss but cannot pull it`, () => {
|
||||
const sharedBoss = { hp: 1e9, maxHp: 1e9, damage(pid, n) { this.hp -= n; } };
|
||||
const h = makeBattle({ kind: 'boss', sharedBoss, autoFinish: false, hooks: ['damaged'], captureNoisy: true,
|
||||
units: [{ chessId: id, row: 10, col: 3, skillIndex: 1 }] });
|
||||
h.step(); const u = h.unit(id);
|
||||
const e = h.spawn('enemy_9017_achunt', { pos: [3, 5], tag: 'boss', mods: { speedMul: 0 } });
|
||||
e.profile.noAttack = true;
|
||||
u.skill.gainSp(999, 'init'); assert.ok(u.skill.activate('test')); h.run(3);
|
||||
assert.ok(h.hooksOf('damaged').some((c) => c.source === u && c.target === e), 'S2 actually hits the leader');
|
||||
near(e.x, 5); near(e.y, 3);
|
||||
// An excessive force isolates the leader exclusion from its ordinary high weight.
|
||||
near(h.b.pullToFront(e, u, 99), 0); near(e.x, 5); near(e.y, 3); finish(h);
|
||||
});
|
||||
}
|
||||
@@ -2713,6 +2713,23 @@ test('假想敌:铳 (h07_02 override) never uses 最终之罚 in the regular b
|
||||
assert.ok(!h.eventsOf('fx').some((f) => f[1] === 'charge'));
|
||||
});
|
||||
|
||||
test('隐秘核心 铳: another gun alone grants 80% reduction from the very first hit; no stale reduction after it leaves', () => {
|
||||
const h = bossArena({ units: [{ chessId: 't_wall', row: 10, col: 7 }] });
|
||||
h.step();
|
||||
const g = put(h, 'enemy_9017_achunt_2', [3, 10], { tag: 'boss' });
|
||||
const other = put(h, 'enemy_9017_achunt_2', [3, 4], { tag: 'boss' });
|
||||
const full = 1000 * (1 - g.s.res / 100);
|
||||
approx(h.b.dealDamage(null, g, { amount: 1000, type: 'arts' }), full * 0.2);
|
||||
// Leaving the field without killing the shared boss pool isolates the companion condition.
|
||||
other.alive = false; other.deployed = false;
|
||||
approx(h.b.dealDamage(null, g, { amount: 1000, type: 'arts' }), full);
|
||||
const spring = put(h, 'enemy_9020_actrpc', [3, 6], { tag: 'part' });
|
||||
approx(h.b.dealDamage(null, g, { amount: 1000, type: 'arts' }), full * 0.2);
|
||||
spring.alive = false; spring.deployed = false;
|
||||
approx(h.b.dealDamage(null, g, { amount: 1000, type: 'arts' }), full);
|
||||
checkInvariants(h.b); assert.deepEqual(h.b.errors, []);
|
||||
});
|
||||
|
||||
test('假想敌:铳 (隐秘核心): damage ×0.2 while springs live; 盲信之誓 lines hurt operators on them; 末日布道 dash', () => {
|
||||
const h = bossArena({ units: [{ chessId: 't_wall', row: 10, col: 7 }], setup(b) { b.enemyOverrides = W.act1autochess_h08_02.overrides; } });
|
||||
h.step();
|
||||
|
||||
@@ -617,7 +617,7 @@ test('黄沙罗盘: initial SP +30 / +50; (萨尔贡) first skill end +30 SP; +
|
||||
u.skill.sp = 0;
|
||||
u.skill.activate('test', { free: true });
|
||||
u.skill.end('test');
|
||||
close(u.skill.sp, 0, 'only the first skill end');
|
||||
close(u.skill.sp, 3, 'only the first skill end gives 30; this cast still gives its own 3 through the lock');
|
||||
cover(A('6_07'), B('6_07'), A('2_04'), B('2_04'));
|
||||
});
|
||||
|
||||
|
||||
@@ -323,11 +323,11 @@ test('user playtest #4 (DESIGN §18): picking by tile, timers, 机变 two taps,
|
||||
assert.match(META, /solo \/ single human untimed/);
|
||||
assert.match(PLAYING, /只有你一名玩家/);
|
||||
// #4 one countdown: BAND_TURN_SECONDS per turn (code = data = docs)
|
||||
assert.equal(BAND_TURN_SECONDS, 30);
|
||||
assert.equal(BAND_TURN_SECONDS, 50);
|
||||
assert.equal(DATA.config.timers.bandTurn, BAND_TURN_SECONDS);
|
||||
assert.match(DESIGN, /`BAND_TURN_SECONDS` 30 s per turn = m\.public\.deadline/);
|
||||
assert.match(META, /`Match\.BAND_TURN_SECONDS` 30/);
|
||||
assert.match(PLAYING, /\*\*每人 30 秒\*\*/);
|
||||
assert.match(DESIGN, /`BAND_TURN_SECONDS` 50 s per turn = m\.public\.deadline/);
|
||||
assert.match(META, /`Match\.BAND_TURN_SECONDS` 50/);
|
||||
assert.match(PLAYING, /\*\*每人 50 秒\*\*/);
|
||||
assert.ok(!/12 s\/turn/.test(DESIGN) && !/`bandTurn` 12/.test(META) && !/每人 12 秒/.test(PLAYING), 'the old 12 s turn is gone');
|
||||
assert.match(META, /`ev\.preview` true/, 'META: onBattleStart handlers must not change the match for the stats preview');
|
||||
// #2 机变 two taps
|
||||
|
||||
+14
-14
@@ -493,24 +493,24 @@
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":7,"total":7,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":851512,"bossDmg":507527,"heal":97350,"deaths":4,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_4_17_b",1,"uniequip_002_hsguma",3574.36,1,4,0,34947,15344,19358,35,0],
|
||||
["p1",1,"op","chess_char_4_17_b",1,"uniequip_002_hsguma",3574.36,1,4,0,34947,15344,18716,35,0],
|
||||
["p1",2,"op","chess_char_4_18_b",1,"uniequip_002_mudrok",6421.77,1,6,2,47087,20583,21153,15,7],
|
||||
["p1",3,"op","chess_char_4_23_b",2,"uniequip_002_gvial2",3092.1,1,21,4,222093,33954,10820,50,2],
|
||||
["p1",3,"op","chess_char_4_23_b",2,"uniequip_002_gvial2",3092.1,1,21,4,222093,33954,11556,50,2],
|
||||
["p1",4,"op","chess_char_4_19_b",2,"uniequip_002_flamtl",3727.68,1,9,0,14897,8313,0,32,2],
|
||||
["p1",5,"op","chess_char_4_22_b",2,"uniequip_002_svrash",2825.9,1,23,0,189827,5620,0,112,3],
|
||||
["p1",6,"op","chess_char_4_20_b",2,"uniequip_002_fartth",0,0,22,0,263410,3122,0,104,5],
|
||||
["p1",7,"op","chess_char_4_21_b",1,"uniequip_002_plosis",1742.4,1,11,0,0,4444,18833,22,2],
|
||||
["p1",8,"op","chess_char_4_24_b",1,"uniequip_002_billro",2395.8,1,12,1,79251,5783,825,91,6],
|
||||
["p1",9,"op","chess_char_4_25_b",2,"uniequip_002_cetsyr",1586.02,1,3,0,0,4444,5184,0,3],
|
||||
["p1",10,"op","chess_char_4_26_b",0,"uniequip_002_bldsk",1548.74,1,5,0,0,4444,21177,28,2],
|
||||
["p1",9,"op","chess_char_4_25_b",2,"uniequip_002_cetsyr",1586.02,1,3,0,0,4444,5277,0,3],
|
||||
["p1",10,"op","chess_char_4_26_b",0,"uniequip_002_bldsk",1548.74,1,5,0,0,4444,20990,27,2],
|
||||
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
|
||||
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null]
|
||||
],
|
||||
"enemies": {"enemy_1010_demon_2":[2,2,0,4541,107495],"enemy_1438_dspred":[5,5,0,35246,236490],"enemy_1521_dslily":[1,0,0,60130,507527]},
|
||||
"hooks": [1,23,6000,612,612,1020,1376,7619,11,11,32,31,0,13754,153,15,8,0,1,12,0,30,6],
|
||||
"events": {"atk":817,"deploy":13,"die":11,"dmg":1354,"engage":7,"fx":380,"heal":88,"skill":65,"spawn":20,"status":567},
|
||||
"hooks": [1,23,6000,611,611,1020,1376,7785,11,11,32,31,0,13754,153,15,8,0,1,12,0,30,6],
|
||||
"events": {"atk":816,"deploy":13,"die":11,"dmg":1354,"engage":7,"fx":380,"heal":86,"skill":65,"spawn":20,"status":567},
|
||||
"fx": {"appear":2,"bandage":2,"block":22,"burst":30,"camouflage":1,"disappear":2,"dodge":6,"featherArrow":5,"flame":2,"garrisonGrant":1,"grow":1,"healField":2,"jungleSoul":2,"mote":68,"redistribute":45,"reweave":3,"riposte":6,"rockfall":18,"rockslide":7,"sandChains":6,"shieldBreak":22,"slash":51,"spGift":6,"tentacle":6,"thorns":55,"tide":5,"truesilver":3,"undying":1},
|
||||
"snaps": "4357c1b6 34df6216 b3da85aa 3b7d368b 762f4ac8 daa91aef 832c4be2 95b21603 93538c1e b305aa21 42b77f2e f8296dc4 8fa97c9b 26ee953f b4ec0d70 46537e48 9cb7a666 1bbcbd3e 6cf0c396 02349ae3"
|
||||
"snaps": "4357c1b6 34df6216 b3da85aa 3b7d368b 762f4ac8 daa91aef 832c4be2 95b21603 93538c1e 6b29d2ca 77ccce4c cf06422e f2dccfed 789733a1 83ba65ee 81086826 06bf209c 7bdd56fd c413e565 757edd58"
|
||||
},
|
||||
"boss-boss_5-pair": {
|
||||
"about": "卢西恩,“猩红血钻” pair pool 1560000: chess_char_6_03_b chess_char_6_06_b chess_char_6_07_b chess_char_6_01_b chess_char_6_02_b chess_char_6_04_b chess_char_6_05_b chess_char_6_08_b chess_char_6_09_b chess_char_6_11_b token_10017_skadi2_dedant token_10030_mlyss_wtrman | chess_char_6_15_b chess_char_6_19_b chess_char_5_01_b chess_char_6_17_b chess_char_6_12_b chess_char_6_13_b chess_char_6_14_b chess_char_6_16_b chess_char_6_18_b chess_char_6_20_b",
|
||||
@@ -1076,12 +1076,12 @@
|
||||
"killed": 8,
|
||||
"total": 8,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": 2992115,
|
||||
"bossHpLeft": 2992511,
|
||||
"rngDraws": 1828,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {
|
||||
"p1": {"killed":8,"total":8,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":2840858,"bossDmg":1130306,"heal":26454,"deaths":3,"layerGains":{}},
|
||||
"p2": {"killed":0,"total":0,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":3891982,"bossDmg":1427731,"heal":133966,"deaths":7,"layerGains":{}}
|
||||
"p1": {"killed":8,"total":8,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":2840728,"bossDmg":1130176,"heal":26454,"deaths":3,"layerGains":{}},
|
||||
"p2": {"killed":0,"total":0,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":3891716,"bossDmg":1427465,"heal":133966,"deaths":7,"layerGains":{}}
|
||||
},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_6_03_b",2,"uniequip_002_yu",7326,1,15,0,3237,9163,15665,31,3],
|
||||
@@ -1093,7 +1093,7 @@
|
||||
["p1",7,"op","chess_char_6_05_b",2,"uniequip_002_pasngr",2696.54,1,13,1,71156,0,0,0,11],
|
||||
["p1",8,"op","chess_char_6_08_b",2,"uniequip_002_reed2",1834.8,1,19,0,55032,0,9419,73,2],
|
||||
["p1",9,"op","chess_char_6_09_b",0,"uniequip_002_cello",0,0,21,1,160062,14044,0,27,27],
|
||||
["p1",10,"op","chess_char_6_11_b",2,"uniequip_002_mlyss",2082.3,1,59,0,30214,11261,306,74,2],
|
||||
["p1",10,"op","chess_char_6_11_b",2,"uniequip_002_mlyss",2082.3,1,59,0,30083,11261,306,74,2],
|
||||
["p1",11,"token","token_10017_skadi2_dedant",null,null,138,0,28,0,0,0,0,0,null],
|
||||
["p1",12,"token","token_10030_mlyss_wtrman",null,null,0,0,24,0,0,1670,348,4,1],
|
||||
["p2",13,"op","chess_char_6_15_b",2,"uniequip_002_qiubai",903.1,1,68,4,817795,32754,0,248,4],
|
||||
@@ -1104,7 +1104,7 @@
|
||||
["p2",18,"op","chess_char_6_13_b",1,"uniequip_002_angel2",2436.5,1,14,0,526339,8805,4488,259,5],
|
||||
["p2",19,"op","chess_char_6_14_b",2,"uniequip_002_lumen",3342.24,1,20,0,0,0,19549,78,1],
|
||||
["p2",20,"op","chess_char_6_16_b",2,"uniequip_002_halo2",1582.9,1,10,0,103786,0,0,127,3],
|
||||
["p2",21,"op","chess_char_6_18_b",2,"uniequip_002_whitw2",1859.61,1,27,0,346326,5155,0,25,3],
|
||||
["p2",21,"op","chess_char_6_18_b",2,"uniequip_002_whitw2",1859.61,1,27,0,346061,5155,0,25,3],
|
||||
["p2",22,"op","chess_char_6_20_b",2,"uniequip_002_agoat2",1758.31,1,25,0,0,14000,103507,78,2],
|
||||
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
|
||||
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null],
|
||||
@@ -1128,7 +1128,7 @@
|
||||
"enemies": {
|
||||
"enemy_1267_nhpbr_2": [6,6,0,1900,141894],
|
||||
"enemy_9007_acelem": [2,2,0,14390,309587],
|
||||
"enemy_9017_achunt_2": [2,0,0,57663,2607885],
|
||||
"enemy_9017_achunt_2": [2,0,0,57663,2607489],
|
||||
"enemy_9018_actrpa": [1,0,0,53759,1092472],
|
||||
"enemy_9019_actrpb": [1,0,0,24773,1015854],
|
||||
"enemy_9020_actrpc": [1,0,0,22601,1668902]
|
||||
@@ -1175,7 +1175,7 @@
|
||||
"wanted": 6,
|
||||
"zone": 6
|
||||
},
|
||||
"snaps": "8b102142 7109c20e 6cb62905 16f4d84e db38e8d1 fb736fc0 d644e65e bf490768 454f169f 723043e7 883b6557 609aa7bb bfa05477 7d38be83 76b95dc1 8ac15f69 7c25d190 03595be2 40606ec4 e87eb589"
|
||||
"snaps": "8b102142 0561d2e7 b88fff99 8716a28b 1badf400 91b895ee 78b15f27 974adf64 c118d65e 831cdc6f 0c7385df d5ab8494 2e6d06b7 f460f583 e7a7bafc 6ab1c142 80a01218 1de18f0f 84c4afee 5fb9b9bc"
|
||||
},
|
||||
"hidden-boss_9-solo": {
|
||||
"about": "假想敌:铳 solo pool 2800000: chess_char_5_08_b chess_char_5_11_b chess_char_5_05_b chess_char_5_06_b chess_char_5_13_b chess_char_5_07_b chess_char_5_02_b chess_char_5_03_b chess_char_5_10_b chess_char_5_12_b",
|
||||
|
||||
+4
-16
@@ -454,19 +454,7 @@
|
||||
"rngDraws": 43,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {
|
||||
"p1": {
|
||||
"killed": 58,
|
||||
"total": 83,
|
||||
"perfect": false,
|
||||
"leaks": 29,
|
||||
"leaksUncounted": 1,
|
||||
"coins": 8,
|
||||
"dmg": 584652,
|
||||
"bossDmg": 0,
|
||||
"heal": 5484,
|
||||
"deaths": 18,
|
||||
"layerGains": {"egirShip":18,"raidShip":36}
|
||||
}
|
||||
"p1": {"killed":58,"total":83,"perfect":false,"leaks":29,"leaksUncounted":1,"coins":8,"dmg":584652,"bossDmg":0,"heal":5484,"deaths":18,"layerGains":{"raidShip":36}}
|
||||
},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_3_06_b",1,"uniequip_002_philae",0,0,6,3,8669,8443,0,11,1],
|
||||
@@ -524,9 +512,9 @@
|
||||
"enemy_2021_syfish": [60,46,14,2435,336653],
|
||||
"enemy_2025_syufo": [13,3,7,30859,107202]
|
||||
},
|
||||
"hooks": [1,136,5100,622,622,1146,1244,412,76,130,23,25,0,8861,90,55,88,26,1,81,15,12,0],
|
||||
"events": {"atk":700,"bounty":4,"deploy":48,"die":104,"dmg":1157,"engage":12,"fx":224,"heal":412,"layer":15,"leak":26,"skill":50,"spawn":126,"status":151},
|
||||
"fx": {"aoe":21,"blink":2,"buff":1,"burst":12,"coin":26,"displace":30,"explode":31,"garrison":9,"layer":6,"pull":30,"revive":5,"shield":1,"shieldBreak":1,"stone":1,"strike":11,"summon":27,"telegraph":1,"volley":9},
|
||||
"hooks": [1,136,5100,622,622,1146,1244,412,76,130,23,25,0,8861,90,55,88,26,1,81,9,12,0],
|
||||
"events": {"atk":700,"bounty":4,"deploy":48,"die":104,"dmg":1157,"engage":12,"fx":218,"heal":412,"layer":9,"leak":26,"skill":50,"spawn":126,"status":151},
|
||||
"fx": {"aoe":21,"blink":2,"buff":1,"burst":12,"coin":26,"displace":30,"explode":31,"garrison":9,"pull":30,"revive":5,"shield":1,"shieldBreak":1,"stone":1,"strike":11,"summon":27,"telegraph":1,"volley":9},
|
||||
"snaps": "b19807c9 91093618 e23e7617 b1bc3a57 24c17dc9 e4263c69 ce35e0e8 34019a9b d63a9b7b 8f9d73c2 d3acc943 cbc3410b 6b14e481 7d8bf5fe 9a5b67ba 72b72b18 90c03fa8"
|
||||
},
|
||||
"roster-009": {
|
||||
|
||||
+23
-23
@@ -359,21 +359,21 @@
|
||||
"time": 223.2,
|
||||
"ticks": 6696,
|
||||
"reason": "cleared",
|
||||
"killed": 9,
|
||||
"killed": 10,
|
||||
"total": 80,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 81,
|
||||
"rngDraws": 84,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {
|
||||
"p1": {
|
||||
"killed": 9,
|
||||
"killed": 10,
|
||||
"total": 80,
|
||||
"perfect": false,
|
||||
"leaks": 71,
|
||||
"leaks": 70,
|
||||
"leaksUncounted": 0,
|
||||
"coins": 4,
|
||||
"dmg": 146478,
|
||||
"dmg": 144421,
|
||||
"bossDmg": 0,
|
||||
"heal": 0,
|
||||
"deaths": 25,
|
||||
@@ -384,35 +384,35 @@
|
||||
["p1",1,"op","chess_char_4_17_b",1,"none",0,0,3,0,189,3211,0,1,1],
|
||||
["p1",2,"op","chess_char_4_18_b",1,"none",0,0,86,0,60,9306,0,2,1],
|
||||
["p1",3,"op","chess_char_5_05_b",2,"none",0,0,100,0,572,5816,0,1,1],
|
||||
["p1",4,"op","chess_char_4_19_b",1,"none",1847.63,1,110,0,230,3864,0,7,1],
|
||||
["p1",5,"op","chess_char_5_01_b",2,"none",1036.53,1,108,1,2433,3985,0,6,3],
|
||||
["p1",6,"op","chess_char_4_22_b",1,"none",3191,1,112,0,959,14040,0,6,1],
|
||||
["p1",4,"op","chess_char_4_19_b",1,"none",1847.63,1,110,1,466,3864,0,8,1],
|
||||
["p1",5,"op","chess_char_5_01_b",2,"none",1036.53,1,108,0,2197,3985,0,5,3],
|
||||
["p1",6,"op","chess_char_4_22_b",1,"none",3191,1,112,0,1090,14040,0,8,1],
|
||||
["p1",7,"op","chess_char_4_23_b",1,"none",0,0,71,0,3484,6865,0,8,1],
|
||||
["p1",8,"op","chess_char_5_06_b",2,"uniequip_002_acguad",0,0,10,0,0,3820,0,0,0],
|
||||
["p1",9,"op","chess_char_4_20_b",1,"none",1255.06,1,109,2,17210,4785,0,32,1],
|
||||
["p1",10,"op","chess_char_4_24_b",1,"none",1007.59,1,111,2,5402,3072,0,19,1],
|
||||
["p1",9,"op","chess_char_4_20_b",1,"none",1255.06,1,109,2,15866,4785,0,30,1],
|
||||
["p1",10,"op","chess_char_4_24_b",1,"none",1007.59,1,111,1,5309,3072,0,18,1],
|
||||
["p1",11,"op","chess_char_5_02_b",2,"none",0,0,105,1,8684,4080,0,19,1],
|
||||
["p1",12,"op","chess_char_5_03_b",2,"uniequip_002_accast",0,0,4,3,107254,1749,0,10,1],
|
||||
["p1",12,"op","chess_char_5_03_b",2,"uniequip_002_accast",0,0,4,5,106504,1749,0,10,1],
|
||||
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
|
||||
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null]
|
||||
],
|
||||
"enemies": {
|
||||
"enemy_1005_yokai_2": [18,3,15,3213,10312],
|
||||
"enemy_1041_lazerd_2": [24,3,21,43210,35712],
|
||||
"enemy_1042_frostd": [30,1,29,0,34310],
|
||||
"enemy_1045_hammer": [1,1,0,2029,15552],
|
||||
"enemy_1045_hammer_2": [1,0,1,4288,18162],
|
||||
"enemy_1046_agent": [1,1,0,72,3888],
|
||||
"enemy_1050_lslime": [1,0,1,9094,6601],
|
||||
"enemy_1005_yokai_2": [18,4,14,3323,11182],
|
||||
"enemy_1041_lazerd_2": [24,3,21,42917,34367],
|
||||
"enemy_1042_frostd": [30,1,29,0,33047],
|
||||
"enemy_1045_hammer": [1,1,0,2515,15552],
|
||||
"enemy_1045_hammer_2": [1,0,1,5062,16522],
|
||||
"enemy_1046_agent": [1,1,0,57,3888],
|
||||
"enemy_1050_lslime": [1,0,1,8323,7921],
|
||||
"enemy_1061_zomshd": [1,0,1,61,13892],
|
||||
"enemy_1062_rager_2": [1,0,1,2628,17050],
|
||||
"enemy_1062_rager_2": [1,0,1,2335,17050],
|
||||
"enemy_1067_snslime": [1,0,1,0,0],
|
||||
"enemy_1071_dftman": [1,0,1,0,0]
|
||||
},
|
||||
"hooks": [1,112,6696,299,299,578,571,566,34,105,13,13,0,9182,8,14,80,71,1,34,2,0,7],
|
||||
"events": {"atk":324,"bounty":2,"deploy":30,"die":34,"dmg":571,"engage":11,"fx":22,"layer":2,"leak":71,"skill":26,"spawn":94,"status":111},
|
||||
"fx": {"aoe":1,"dodge":7,"dp":6,"garrison":2,"garrisonGrant":1,"palsy":2,"revive":3},
|
||||
"snaps": "801e90ec c1cca6cb f2a5a38b 6dc62d98 cb792a41 23ec9de6 62a40e3b eb8791c8 5cf201e4 e0c50494 b5bd31db 490ef7be f2c93ce1 ee84f980 68266f8f d66f411d 6f00ab8c 98dfa057 343e0f1c 7a9470b3 c574bda9 8be7c32a dadff196"
|
||||
"hooks": [1,112,6696,302,302,560,551,566,35,105,13,13,0,9244,8,13,80,70,1,35,2,0,9],
|
||||
"events": {"atk":326,"bounty":2,"deploy":30,"die":35,"dmg":551,"engage":11,"fx":25,"layer":2,"leak":70,"skill":26,"spawn":94,"status":113},
|
||||
"fx": {"aoe":1,"dodge":9,"dp":7,"garrison":2,"garrisonGrant":1,"palsy":2,"revive":3},
|
||||
"snaps": "801e90ec c1cca6cb 81d33420 f867f133 edacd763 1628b012 6c63447d 731a52ce e4d1fee0 1aa30026 5afb0d3a 3292f917 f65a695f 7f9749da 215a493b 0a35d6fd 8300297d 71eb2c83 2ae65721 93a77bf5 f413d9ea 3b2f4b8c ef24d1bf"
|
||||
},
|
||||
"standin-08": {
|
||||
"about": "elite band 6 as stand-ins: chess_char_5_08_b→char_610_acfend chess_char_5_14_b→char_608_acpion chess_char_5_19_b→char_617_sharp2 chess_char_5_07_b→char_601_cguard chess_char_5_13_b→char_609_acguad chess_char_5_17_b→char_615_acspec chess_char_5_10_b→char_606_csuppo chess_char_5_11_b→char_613_acmedc chess_char_5_12_b→char_614_acsupo chess_char_5_15_b→char_603_csnipe chess_char_5_18_b→char_611_acnipe chess_char_5_20_b→char_614_acsupo",
|
||||
|
||||
@@ -113,6 +113,7 @@ test('band draft timers (user playtest #4 item 4): one countdown of BAND_TURN_SE
|
||||
assert.equal(new Set([...h2.m.players.values()].map((ps) => ps.bandId)).size, 4, 'the rest get distinct free strategies');
|
||||
h2.m.dispose();
|
||||
// data and code say the same: data/config.json timers.bandTurn (tools/build-data.mjs) = Match.BAND_TURN_SECONDS
|
||||
assert.equal(BAND_TURN_SECONDS, 50, 'owner official-play report: 50 seconds per multiplayer pick');
|
||||
assert.equal(DATA.config.timers.bandTurn, BAND_TURN_SECONDS);
|
||||
});
|
||||
|
||||
|
||||
@@ -164,7 +164,7 @@ describe('server: automatic assignments never duplicate a teammate\'s strategy',
|
||||
test('every human idle: the turn timeouts assign distinct strategies (no separate step cap)', () => {
|
||||
const h = draftOf({ humans: 4, seed: 9 });
|
||||
const m = h.m;
|
||||
h.run(() => m.phase !== PHASE.BAND_DRAFT, { maxTime: 120000 });
|
||||
h.run(() => m.phase !== PHASE.BAND_DRAFT, { maxTime: 4 * TURN_MS + 1 });
|
||||
assert.equal(m.phase, PHASE.BATTLE_CHECK);
|
||||
const ids = distinct(m);
|
||||
assert.ok(ids.includes('band_bldsk'), 'the first idle player still gets the official default');
|
||||
@@ -194,7 +194,7 @@ describe('server: automatic assignments never duplicate a teammate\'s strategy',
|
||||
m.dispose();
|
||||
for (const seed of [1, 2, 3, 4]) {
|
||||
const h2 = draftOf({ humans: 2, bots: 2, seed });
|
||||
h2.run(() => h2.m.phase !== PHASE.BAND_DRAFT, { maxTime: 120000 });
|
||||
h2.run(() => h2.m.phase !== PHASE.BAND_DRAFT, { maxTime: 4 * TURN_MS + 1 });
|
||||
distinct(h2.m);
|
||||
h2.m.dispose();
|
||||
}
|
||||
|
||||
@@ -365,7 +365,7 @@ describe('user playtest #2 item 10 — boss-round prep on the boss field (real s
|
||||
for (const c of [host, guest]) await c.waitFor((s) => s.phase === 'INFO_CHECK', 'briefing', 30000);
|
||||
for (const c of [host, guest]) await c.click('.brief__foot .btn--primary', '准备就绪');
|
||||
// the draft first (a client still in the briefing would count as done without picking: its turn then runs out —
|
||||
// Match.BAND_TURN_SECONDS, 30 s each since user playtest #4 item 4)
|
||||
// Match.BAND_TURN_SECONDS, 50 s each since the 2026-10-10 official-play report)
|
||||
for (const c of [host, guest]) await c.waitFor((s) => s.phase !== 'INFO_CHECK', 'band draft', 40000);
|
||||
// band draft: whoever's turn it is picks a free strategy
|
||||
const picked = new Set();
|
||||
|
||||
@@ -88,7 +88,7 @@ describe('DESIGN §16 — a teammate\'s unit shows its owner\'s loadout (real se
|
||||
assert.deepEqual(await loOf(host), {}, 'the host fights with the defaults');
|
||||
for (const c of [host, guest]) await c.click('.brief__foot .btn--primary', '准备就绪');
|
||||
// the draft first (a client still in the briefing would count as done without picking: its turn then runs out —
|
||||
// Match.BAND_TURN_SECONDS, 30 s each since user playtest #4 item 4)
|
||||
// Match.BAND_TURN_SECONDS, 50 s each since the 2026-10-10 official-play report)
|
||||
for (const c of [host, guest]) await c.waitFor((s) => s.phase !== 'INFO_CHECK', 'band draft', 40000);
|
||||
const picked = new Set();
|
||||
const t0 = Date.now();
|
||||
|
||||
@@ -3696,8 +3696,8 @@ function buildConfig(ctx, waves, stages, bands) {
|
||||
timers: {
|
||||
infoCheck: step('INFO_CHECK')?.time ?? 25, infoCheckHint: step('INFO_CHECK')?.hintTime ?? 5,
|
||||
// bandTurn: one turn of the co-op strategy draft = its only countdown (server/match/Match.js BAND_TURN_SECONDS,
|
||||
// [ASSUMED] — user playtest #4 item 4; the official data only has the whole step's 50 s, kept for reference)
|
||||
bandDraft: step('BAND_CHECK')?.time ?? 50, bandDraftHint: step('BAND_CHECK')?.hintTime ?? 15, bandTurn: 30,
|
||||
// owner official-play report 2026-10-10: 50 s per turn; the official whole-step value stays as reference)
|
||||
bandDraft: step('BAND_CHECK')?.time ?? 50, bandDraftHint: step('BAND_CHECK')?.hintTime ?? 15, bandTurn: 50,
|
||||
battleCheck: step('BATTLE_CHECK')?.time ?? 3,
|
||||
spFirst: 30, spTurn: act.modeDataDict.mode_multi_normal?.specialPhaseTime ?? 16,
|
||||
soloPrepTimeData: 300, soloSpTimeData: act.modeDataDict.mode_single_normal?.specialPhaseTime ?? 150,
|
||||
|
||||
Reference in new issue
Block a user