feedback1 WB: bounty drafts offer the cards of their half (late bounty enemies no longer in the R3 draft)
Player report #2: boss bounties, second-half specials and their upgraded copies showed up in the first 悬赏决策 draft. A draft up to R7 (the official wave generator's first half) now offers the 接下来两场作战 cards, a draft from R8 the 下场作战 and multi-round cards, with no tier window (the official late draft screenshot shows boss bounties worth 1 to 6 together). choices.json cards.bounty draftHalf, schedule bountyHalf; the mode's inactive enemy list no longer thins the bounty draft (PRTS 11/18 note).
This commit is contained in:
1 parent
7f134737ce
commit
5e6fe19a70
11 files changed
+264
-39
No files matched your search
+1
-1
@@ -1 +1 @@
|
||||
{"events":{"artifact_paid_1":{"id":"artifact_paid_1","choiceType":"EQUIP_FREE","effectType":"EQUIP","name":"机密商店","desc":"无需消耗资金,获得装备补给。","descRaw":"无需消耗资金,获得装备补给。","color":"#35d8b4","family":"shop","solo":false,"training":false},"artifact_paid_1_s":{"id":"artifact_paid_1_s","choiceType":"EQUIP_FREE","effectType":"EQUIP","name":"机密商店","desc":"无需消耗资金,获得装备补给。","descRaw":"无需消耗资金,获得装备补给。","color":"#f2bd3e","family":"shop","solo":true,"training":false},"artifact_paid_2":{"id":"artifact_paid_2","choiceType":"EQUIP_FREE","effectType":"EQUIP","name":"机密商店","desc":"无需消耗资金,获得装备补给。","descRaw":"无需消耗资金,获得装备补给。","color":"#35d8b4","family":"shop","solo":false,"training":false},"artifact_paid_2_s":{"id":"artifact_paid_2_s","choiceType":"EQUIP_FREE","effectType":"EQUIP","name":"机密商店","desc":"无需消耗资金,获得装备补给。","descRaw":"无需消耗资金,获得装备补给。","color":"#f2bd3e","family":"shop","solo":true,"training":false},"artifact_paid_3":{"id":"artifact_paid_3","choiceType":"EQUIP_FREE","effectType":"EQUIP","name":"机密商店","desc":"无需消耗资金,获得装备补给。","descRaw":"无需消耗资金,获得装备补给。","color":"#35d8b4","family":"shop","solo":false,"training":false},"artifact_paid_3_s":{"id":"artifact_paid_3_s","choiceType":"EQUIP_FREE","effectType":"EQUIP","name":"机密商店","desc":"无需消耗资金,获得装备补给。","descRaw":"无需消耗资金,获得装备补给。","color":"#f2bd3e","family":"shop","solo":true,"training":false},"artifact_paid_4":{"id":"artifact_paid_4","choiceType":"EQUIP_FREE","effectType":"EQUIP","name":"机密商店","desc":"无需消耗资金,获得装备补给。","descRaw":"无需消耗资金,获得装备补给。","color":"#35d8b4","family":"shop","solo":false,"training":false},"artifact_paid_4_s":{"id":"artifact_paid_4_s","choiceType":"EQUIP_FREE","effectType":"EQUIP","name":"机密商店","desc":"无需消耗资金,获得装备补给。","descRaw":"无需消耗资金,获得装备补给。","color":"#f2bd3e","family":"shop","solo":true,"training":false},"artifact_paid_5":{"id":"artifact_paid_5","choiceType":"EQUIP_FREE","effectType":"EQUIP","name":"机密商店","desc":"无需消耗资金,获得装备补给。","descRaw":"无需消耗资金,获得装备补给。","color":"#35d8b4","family":"shop","solo":false,"training":false},"artifact_paid_5_s":{"id":"artifact_paid_5_s","choiceType":"EQUIP_FREE","effectType":"EQUIP","name":"机密商店","desc":"无需消耗资金,获得装备补给。","descRaw":"无需消耗资金,获得装备补给。","color":"#f2bd3e","family":"shop","solo":true,"training":false},"bossInitial_1":{"id":"bossInitial_1","choiceType":"BOUNTY_HUNT","effectType":"ENEMY_GAIN","name":"悬赏决策","desc":"选定悬赏目标,获取额外奖励。","descRaw":"选定悬赏目标,获取额外奖励。","color":"#35d8b4","family":"bounty","solo":false,"training":false},"bossInitial_1_s":{"id":"bossInitial_1_s","choiceType":"BOUNTY_HUNT","effectType":"ENEMY_GAIN","name":"悬赏决策","desc":"选定悬赏目标,获取额外奖励。","descRaw":"选定悬赏目标,获取额外奖励。","color":"#35d8b4","family":"bounty","solo":true,"training":false},"bossInitial_2":{"id":"bossInitial_2","choiceType":"BOUNTY_HUNT","effectType":"ENEMY_GAIN","name":"悬赏决策","desc":"选定悬赏目标,获取额外奖励。","descRaw":"选定悬赏目标,获取额外奖励。","color":"#35d8b4","family":"bounty","solo":false,"training":false},"bossInitial_2_s":{"id":"bossInitial_2_s","choiceType":"BOUNTY_HUNT","effectType":"ENEMY_GAIN","name":"悬赏决策","desc":"选定悬赏目标,获取额外奖励。","descRaw":"选定悬赏目标,获取额外奖励。","color":"#35d8b4","family":"bounty","solo":true,"training":false},"bossInitial_3":{"id":"bossInitial_3","choiceType":"BOUNTY_HUNT","effectType":"ENEMY_GAIN","name":"悬赏决策","desc":"选定悬赏目标,获取额外奖励。","descRaw":"选定悬赏目标,获取额外奖励。","color":"#35d8b4","family":"bounty","solo":false,"training":false},"bossInitial_3_s":{"id":"bossInitial_3_s","choiceType":"BOUNTY_HUNT","effectType":"ENEMY_GAIN","name":"悬赏决策","desc":"选定悬赏目标,获取额外奖励。","descRaw":"选定悬赏目标,获取额外奖励。","color":"#35d8b4","family":"bounty","solo":true,"training":false},"bossInitial_4":{"id":"bossInitial_4","choiceType":"BOUNTY_HUNT","effectType":"ENEMY_GAIN","name":"悬赏决策","desc":"选定悬赏目标,获取额外奖励。","descRaw":"选定悬赏目标,获取额外奖励。","color":"#35d8b4","family":"bounty","solo":false,"training":false},"bossInitial_4_s":{"id":"bossInitial_4_s","choiceType":"BOUNTY_HUNT","effectType":"ENEMY_GAIN","name":"悬赏决策","desc":"Line truncated
|
||||
{"events":{"artifact_paid_1":{"id":"artifact_paid_1","choiceType":"EQUIP_FREE","effectType":"EQUIP","name":"机密商店","desc":"无需消耗资金,获得装备补给。","descRaw":"无需消耗资金,获得装备补给。","color":"#35d8b4","family":"shop","solo":false,"training":false},"artifact_paid_1_s":{"id":"artifact_paid_1_s","choiceType":"EQUIP_FREE","effectType":"EQUIP","name":"机密商店","desc":"无需消耗资金,获得装备补给。","descRaw":"无需消耗资金,获得装备补给。","color":"#f2bd3e","family":"shop","solo":true,"training":false},"artifact_paid_2":{"id":"artifact_paid_2","choiceType":"EQUIP_FREE","effectType":"EQUIP","name":"机密商店","desc":"无需消耗资金,获得装备补给。","descRaw":"无需消耗资金,获得装备补给。","color":"#35d8b4","family":"shop","solo":false,"training":false},"artifact_paid_2_s":{"id":"artifact_paid_2_s","choiceType":"EQUIP_FREE","effectType":"EQUIP","name":"机密商店","desc":"无需消耗资金,获得装备补给。","descRaw":"无需消耗资金,获得装备补给。","color":"#f2bd3e","family":"shop","solo":true,"training":false},"artifact_paid_3":{"id":"artifact_paid_3","choiceType":"EQUIP_FREE","effectType":"EQUIP","name":"机密商店","desc":"无需消耗资金,获得装备补给。","descRaw":"无需消耗资金,获得装备补给。","color":"#35d8b4","family":"shop","solo":false,"training":false},"artifact_paid_3_s":{"id":"artifact_paid_3_s","choiceType":"EQUIP_FREE","effectType":"EQUIP","name":"机密商店","desc":"无需消耗资金,获得装备补给。","descRaw":"无需消耗资金,获得装备补给。","color":"#f2bd3e","family":"shop","solo":true,"training":false},"artifact_paid_4":{"id":"artifact_paid_4","choiceType":"EQUIP_FREE","effectType":"EQUIP","name":"机密商店","desc":"无需消耗资金,获得装备补给。","descRaw":"无需消耗资金,获得装备补给。","color":"#35d8b4","family":"shop","solo":false,"training":false},"artifact_paid_4_s":{"id":"artifact_paid_4_s","choiceType":"EQUIP_FREE","effectType":"EQUIP","name":"机密商店","desc":"无需消耗资金,获得装备补给。","descRaw":"无需消耗资金,获得装备补给。","color":"#f2bd3e","family":"shop","solo":true,"training":false},"artifact_paid_5":{"id":"artifact_paid_5","choiceType":"EQUIP_FREE","effectType":"EQUIP","name":"机密商店","desc":"无需消耗资金,获得装备补给。","descRaw":"无需消耗资金,获得装备补给。","color":"#35d8b4","family":"shop","solo":false,"training":false},"artifact_paid_5_s":{"id":"artifact_paid_5_s","choiceType":"EQUIP_FREE","effectType":"EQUIP","name":"机密商店","desc":"无需消耗资金,获得装备补给。","descRaw":"无需消耗资金,获得装备补给。","color":"#f2bd3e","family":"shop","solo":true,"training":false},"bossInitial_1":{"id":"bossInitial_1","choiceType":"BOUNTY_HUNT","effectType":"ENEMY_GAIN","name":"悬赏决策","desc":"选定悬赏目标,获取额外奖励。","descRaw":"选定悬赏目标,获取额外奖励。","color":"#35d8b4","family":"bounty","solo":false,"training":false},"bossInitial_1_s":{"id":"bossInitial_1_s","choiceType":"BOUNTY_HUNT","effectType":"ENEMY_GAIN","name":"悬赏决策","desc":"选定悬赏目标,获取额外奖励。","descRaw":"选定悬赏目标,获取额外奖励。","color":"#35d8b4","family":"bounty","solo":true,"training":false},"bossInitial_2":{"id":"bossInitial_2","choiceType":"BOUNTY_HUNT","effectType":"ENEMY_GAIN","name":"悬赏决策","desc":"选定悬赏目标,获取额外奖励。","descRaw":"选定悬赏目标,获取额外奖励。","color":"#35d8b4","family":"bounty","solo":false,"training":false},"bossInitial_2_s":{"id":"bossInitial_2_s","choiceType":"BOUNTY_HUNT","effectType":"ENEMY_GAIN","name":"悬赏决策","desc":"选定悬赏目标,获取额外奖励。","descRaw":"选定悬赏目标,获取额外奖励。","color":"#35d8b4","family":"bounty","solo":true,"training":false},"bossInitial_3":{"id":"bossInitial_3","choiceType":"BOUNTY_HUNT","effectType":"ENEMY_GAIN","name":"悬赏决策","desc":"选定悬赏目标,获取额外奖励。","descRaw":"选定悬赏目标,获取额外奖励。","color":"#35d8b4","family":"bounty","solo":false,"training":false},"bossInitial_3_s":{"id":"bossInitial_3_s","choiceType":"BOUNTY_HUNT","effectType":"ENEMY_GAIN","name":"悬赏决策","desc":"选定悬赏目标,获取额外奖励。","descRaw":"选定悬赏目标,获取额外奖励。","color":"#35d8b4","family":"bounty","solo":true,"training":false},"bossInitial_4":{"id":"bossInitial_4","choiceType":"BOUNTY_HUNT","effectType":"ENEMY_GAIN","name":"悬赏决策","desc":"选定悬赏目标,获取额外奖励。","descRaw":"选定悬赏目标,获取额外奖励。","color":"#35d8b4","family":"bounty","solo":false,"training":false},"bossInitial_4_s":{"id":"bossInitial_4_s","choiceType":"BOUNTY_HUNT","effectType":"ENEMY_GAIN","name":"悬赏决策","desc":"Line truncated
|
||||
+2
-2
@@ -319,9 +319,9 @@ Types: `BAND_INITIAL` 41, `BOND` 23, `EQUIP` 115, `ENEMY_GAIN` 129 (悬赏), `BU
|
||||
| `events[id]` | `{"id":"enemy_initial_1","choiceType":"BOUNTY_HUNT","effectType":"ENEMY_GAIN","name":"悬赏决策","desc":"…","color":"#35d8b4","family":"bounty","solo":false,"training":false}` | 109 official choice events; `_s` = solo copies; `usedBy` on events opened by items (`hunter_band_1` ← 教鞭 / “神秘顾客”) |
|
||||
| `families` | `{"bounty":{"name":"悬赏决策",…},"supply":{…},"shop":{…},"tactic":{…}}` | 悬赏决策 / 道具补给 / 机密商店 / 战术决策 |
|
||||
| `format` | `{"multi":{"cards":6,"pickOrder":"random","firstPickSec":30,"otherPickSec":16,"onTimeout":"autoPickRandom","eachPlayerPicks":1},"solo":{"cards":3,"timer":null},"opensAfterIncome":true}` | |
|
||||
| `cards.bounty[]` | `{"effectId":"enemyeffect_10_4","name":"悬赏·飞行I","tier":1,"coin":1,"payout":"kill","rounds":2,"multiRound":false,"enemyKey":"enemy_1005_yokai","count":1,"adds":[…],"draft":true,"draftExcluded":null}` | `payout` `kill` (killer gets coins, 联防 helpers too) / `perfect` (chooser's own phase perfect); `rounds` = battles affected (99 = all remaining); `tier` from the I/II/III suffix (else coin value); `draft` = offered by the 悬赏决策 draft — the PRTS 下半 记录 §机变阶段 "敌人轮选" table (105 cards: 1- and 2-battle kill bounties, 源石虫·特训, the 7 multi-round cards); else `draftExcluded` `perfect` (战术特训: "仅由法术教鞭生成" — the 教鞭 Art's cards; 20) / `hidden` (鸭爵 / 高普尼克 / 流泪小子 / 圆仔, commented out of the table; 4) — build-data `bountyDraftExclusion`, `server/match/choices.js draftBounty`, user playtest #6 item 4 |
|
||||
| `cards.bounty[]` | `{"effectId":"enemyeffect_10_4","name":"悬赏·飞行I","tier":1,"coin":1,"payout":"kill","rounds":2,"multiRound":false,"enemyKey":"enemy_1005_yokai","count":1,"adds":[…],"draft":true,"draftExcluded":null}` | `payout` `kill` (killer gets coins, 联防 helpers too) / `perfect` (chooser's own phase perfect); `rounds` = battles affected (99 = all remaining); `tier` from the I/II/III suffix (else coin value); `draft` = offered by the 悬赏决策 draft — the PRTS 下半 记录 §机变阶段 "敌人轮选" table (105 cards: 1- and 2-battle kill bounties, 源石虫·特训, the 7 multi-round cards); else `draftExcluded` `perfect` (战术特训: "仅由法术教鞭生成" — the 教鞭 Art's cards; 20) / `hidden` (鸭爵 / 高普尼克 / 流泪小子 / 圆仔, commented out of the table; 4) — build-data `bountyDraftExclusion`, `server/match/choices.js draftBounty`, user playtest #6 item 4; `draftHalf` = the half whose drafts offer it: 1 (rounds ≤ 7) for the 42 "接下来两场作战" cards, 2 (R8 on) for the "下场作战" and multi-round cards (63), null when not drafted — build-data `bountyDraftHalf`, player feedback after 0.1.0 report #2 |
|
||||
| `cards.tactic[]` | `{"effectId":"map_m01_1","name":"模拟战场演变·模式一","kind":"terrain","stageId":"act1autochess_m01","team":false}` | `kind` `ally` / `enemyDebuff` / `terrain` (only for the match stage); `team` = also given to teammates |
|
||||
| `schedule[modeId]` | `{"spRounds":[3,6,9],"rounds":{"3":{"families":[{"family":"bounty","weight":50},…],"cards":6,"supplyTiers":[1,4],"bountyTiers":[1,2],"events":{"bounty":[…],"supply":[…],"shop":[…],"tactic":[…]},"assumed":true}}}` | research 01 A4 defaults: pick a family by weight; `supplyTiers` = item tier window for 道具补给; 机密商店 draws any tier; `events` = official event ids usable as the phase header |
|
||||
| `schedule[modeId]` | `{"spRounds":[3,6,9],"rounds":{"3":{"families":[{"family":"bounty","weight":50},…],"cards":6,"supplyTiers":[1,4],"bountyHalf":1,"events":{"bounty":[…],"supply":[…],"shop":[…],"tactic":[…]},"assumed":true}}}` | research 01 A4 defaults: pick a family by weight; `supplyTiers` = item tier window for 道具补给; 机密商店 draws any tier; `bountyHalf` = the `draftHalf` of the round's 悬赏决策 cards (1 up to `factions.generation.firstHalfMaxRound` 7, else 2); `events` = official event ids usable as the phase header (`enemy_initial_*` in the first half, `bounty_hunter_*` / `bossInitial_*` from R8) |
|
||||
| `pools[poolId]` | `{"kind":"equip","items":[…]}` / `{"kind":"chess","tier":2,"rule":"shopEligible"}` | server-side reward pools referenced by effects (`pool_equip_*`, `pool_chess_shop_N_reward`, `pool_char_later`, …), all [ASSUMED]; `rule:"shopEligible"` = visible/non-hidden entries, `maxTier:"shopLevel"` = ≤ current shop level, `weighted` = `[[id, weight]]`. `pool_equip_vict` (维多利亚, every 25 layers: "获得一件带有随机特殊效果的维式重锤") and `pool_equip_rockr` (洛洛's 定制品; user playtest #4 — the research 04 §8 guess 有限加速器 / 激光发射器 / 护盾无人机 / 双模机械臂 / 蜂鸣器 is superseded) = the 4 special 维式重锤, uniform |
|
||||
|
||||
## 9. `enemies.json` — `{ [enemyKey]: Enemy }` (249 keys that can appear)
|
||||
|
||||
+7
-1
@@ -111,7 +111,13 @@ text "无需消耗资金"). 悬赏决策 offers only choices.json cards with `dr
|
||||
item 4): the PRTS table 卫戍协议:盟约 下半/PRTS盟约记录 §机变阶段 "敌人轮选" — kill bounties for the next battle ("下场作战")
|
||||
or the next two ("接下来两场作战"), 源石虫·特训, and the 7 multi-round cards ("之后 / 后续的每场作战"), but never 战术特训
|
||||
(listed under "※以下悬赏任务仅由法术教鞭生成") nor the 鸭爵 / 高普尼克 / 流泪小子 / 圆仔 cards (commented out of the table) —
|
||||
105 cards, drawn uniformly. Every bounty card carries the effect's official rich text `descRaw` (the battles in blue
|
||||
105 cards. **Each draft offers the cards of its half** (`choices.js bountyDraftHalf`, choices.json `draftHalf`; player
|
||||
feedback after 0.1.0, report #2 — late bounty enemies in the early drafts): a draft up to R7 (the official wave
|
||||
generator's first half: 绝境 / 终极 R3, 险境 co-op R3 / R6) draws the 42 "接下来两场作战" cards, a draft from R8 on (R9)
|
||||
the 56 "下场作战" cards (boss bounties, 特异 giants, the faction _7 / _8 cards, 源石虫·特训) and the 7 multi-round ones —
|
||||
uniformly, with no tier window (the official late-draft screenshot shows boss bounties worth 1 to 6 together; the
|
||||
evidence is in `tools/build-data.mjs bountyDraftHalf`). The mode's inactive enemy list does not thin the draft (PRTS
|
||||
11/18 note "不影响悬赏决策出场"). Every bounty card carries the effect's official rich text `descRaw` (the battles in blue
|
||||
"下场作战" / "两场作战"; the overlay and the effects column render it; the effects column also says "还剩 N 场作战").
|
||||
**Multi-round cards last two battles** (`choices.js MULTI_ROUND_BOUNTY_BATTLES = 2`, `bountyBattles` / `bountyText`): the
|
||||
user does not remember any multi-round bounty (playtest #6 answer, "我不记得有过多轮悬赏"), so until that is confirmed
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
|---|---|
|
||||
| 确认本局信息 | 本局地图(标准模拟固定为「战场#01」;险境从 8 张地图中随机,绝境 / 终极从不含战场#01 的 7 张中随机)、敌方派系、**被禁用的盟约及其干员**、领袖预告。所有人确认后继续;同盟模拟最多 25 秒;只有你一名玩家时(独立模拟,或同盟模拟只带 AI 队友)不限时。 |
|
||||
| 选择策略 | 40 名策略(分队长)决定**初始目标生命值(20–45)**和一条永久规则。同盟:随机顺序轮流选,**每人 30 秒**,右上角和「决策中」显示同一个倒计时;AI 队友立即选完;队友已选的不能重复,每人可**跳过一次**(排到最后);超时自动选你**当前选中**的策略(已被队友选走则选华法琳)。独立:自由选择、不限时;同盟模拟只有你一名玩家时也不限时。 |
|
||||
| 机变 | 部分回合开始前出现:同盟共 6 张卡轮流各取 1 张(首位 30 秒,其余 16 秒;只有你一名玩家时不限时),独立 3 选 1。卡片包括装备、资金、干员、盟约层数、悬赏等;**悬赏决策**为你的作战添加悬赏敌人,卡上用蓝字标出持续多久:「下场作战」/「接下来两场作战」的只出现一场或两场,之后不再出现;官方的「多轮悬赏」(原文写「之后的每场作战」)在这里也按「接下来两场作战」处理,只出现两场。击倒它的人(联防中的队友也算)获得资金;右侧效果栏会写「还剩 N 场作战」。每张卡左上是图标和名称,下面写着完整效果(与详情卡相同的官方描述,手机上也显示);已被选走的卡变暗,右上角显示选走它的玩家头像(悬停可看名字)。选卡要**点两次**:第一次只选中(金框 +「确认选择 · 再次点击」),再点同一张或点右上的「确认选择」才生效;点别的卡换选,按 Esc 或点空白处取消。 |
|
||||
| 机变 | 部分回合开始前出现:同盟共 6 张卡轮流各取 1 张(首位 30 秒,其余 16 秒;只有你一名玩家时不限时),独立 3 选 1。卡片包括装备、资金、干员、盟约层数、悬赏等;**悬赏决策**为你的作战添加悬赏敌人,卡上用蓝字标出持续多久:「下场作战」/「接下来两场作战」的只出现一场或两场,之后不再出现;官方的「多轮悬赏」(原文写「之后的每场作战」)在这里也按「接下来两场作战」处理,只出现两场。前半程(第 7 回合及以前,如第 3 回合)的悬赏决策只提供「接下来两场作战」的悬赏;「下场作战」的强敌悬赏(各首领、特异巨兽等)和多轮悬赏从第 8 回合以后(第 9 回合)才出现。击倒它的人(联防中的队友也算)获得资金;右侧效果栏会写「还剩 N 场作战」。每张卡左上是图标和名称,下面写着完整效果(与详情卡相同的官方描述,手机上也显示);已被选走的卡变暗,右上角显示选走它的玩家头像(悬停可看名字)。选卡要**点两次**:第一次只选中(金框 +「确认选择 · 再次点击」),再点同一张或点右上的「确认选择」才生效;点别的卡换选,按 Esc 或点空白处取消。 |
|
||||
| 休整期 | 购买、摆阵、配装备、冻结 / 刷新、升级调度中心;同盟有倒计时(65–215 秒,随回合增加);独立模拟、以及只有你一名玩家(只带 AI 队友)的同盟模拟不限时。所有人「准备就绪」即提前开战,全员就绪前可取消。 |
|
||||
| 作战 | 每人在自己的战场各自作战(战斗在你的浏览器里模拟,2 倍速)。干员按**从上到下、从左到右**的顺序自动部署并消耗部署费用(初始 10,+1/秒)。时限 45–115 秒(实际秒数,即屏幕上的倒计时),时间到仍存活的敌人算漏怪。有敌人进入蓝门时,顶栏的目标生命值会**立即**显示扣除后的数值(红字 + 「−N」,每回合至多 10 点),结算时确定;联防阶段标注「联防中」,队友拦下的部分最后不扣。左侧队伍栏同样显示队友的待扣生命值。被击倒的干员**留在原地**(倒地姿势、略微变灰),头上的圆圈是再部署倒计时:绿色圆环 + 秒数;变成黄色「DP」表示部署费用不够,红色「!」表示原位被占。血条下方的一行是**元素损伤**(与原版一致:元素图标 + 白条):干员为带图标的彩色圆盘,敌人为小号纯色圆盘,右侧白条是剩余元素值,像血条一样从右往左减少,白条耗尽的那一下就是爆条(被大额元素损伤一次打掉一截时白条也会一次少一截,例如掠海漂移体每击约为 25%–50%);爆条后进入爆发冷却(灼燃 / 神经 / 侵蚀 10 秒,敌方侵蚀 8 秒,凋亡 15 秒),这时白条变成**元素颜色**(神经青绿、侵蚀蓝、灼燃橙红、凋亡灰紫)并在冷却期间慢慢回满、图标闪烁——彩色条表示已经爆条、正在承受爆发效果,不是还剩的元素值;期间所有元素都不会累积也无法被治疗,结束后清零。元素爆发造成的是**无来源伤害**,不吃干员的增伤、穿透和装备加成(例如深巡打海嗣时灼燃爆发固定 7000),击杀仍记在爆条的干员名下;元素脆弱只提高元素伤害,不加快元素条;脆弱不影响元素伤害。已被击杀的敌人不会再被附加元素损伤。 |
|
||||
| 联防 | 见第 5 节。 |
|
||||
|
||||
+30
-13
@@ -3,11 +3,17 @@
|
||||
//
|
||||
// Generation (generateDraft): the family is a weighted pick from choices.schedule[modeId].rounds[r].families; the card
|
||||
// count is `cards` (co-op 6, solo 3):
|
||||
// bounty 悬赏决策 distinct cards.bounty entries with tier ∈ bountyTiers whose enemy exists and is active in the mode,
|
||||
// draft cards only (`draftBounty`, choices.json `draft`: the PRTS 下半 记录 §机变阶段 "敌人轮选" table —
|
||||
// never 战术特训, which only 法术教鞭 creates, nor the hidden 鸭爵 set; the 7 multi-round "之后 / 后续
|
||||
// 的每场作战" cards stay in, as the table lists them); each card carries its official rich text
|
||||
// `descRaw` (the battles in blue "下场作战" / "两场作战"); a multi-round card lasts
|
||||
// bounty 悬赏决策 distinct cards.bounty entries whose enemy exists, draft cards only (`draftBounty`, choices.json
|
||||
// `draft`: the PRTS 下半 记录 §机变阶段 "敌人轮选" table — never 战术特训, which only 法术教鞭 creates,
|
||||
// nor the hidden 鸭爵 set; the 7 multi-round "之后 / 后续的每场作战" cards stay in, as the table lists
|
||||
// them) of the draft's half (`bountyDraftHalf`, choices.json `draftHalf`; player feedback after
|
||||
// 0.1.0, report #2 — late bounty enemies in the early drafts): rounds ≤ 7 (the official wave
|
||||
// generator's first half: R3, 险境 R6) offer the "接下来两场作战" cards, R8 on (R9) the "下场作战" cards
|
||||
// (boss bounties, 特异 giants, the faction _7 / _8 cards …) and the multi-round ones (tools/build-data.mjs
|
||||
// bountyDraftHalf has the evidence); drawn uniformly (no tier window: the official late draft shows
|
||||
// boss bounties worth 1 to 6 together); the mode's inactive enemy list does not apply (PRTS
|
||||
// 卫戍协议:盟约 11/18 note "以上调整仅针对战术特训敌人,不影响悬赏决策出场"); each card carries its official
|
||||
// rich text `descRaw` (the battles in blue "下场作战" / "两场作战"); a multi-round card lasts
|
||||
// MULTI_ROUND_BOUNTY_BATTLES battles and says so (`bountyBattles` / `bountyText`: the user's call
|
||||
// after playtest #6 — "我不记得有过多轮悬赏")
|
||||
// supply 道具补给 random normal EQUIP shop items with tier in supplyTiers [lo, hi] (duplicates allowed)
|
||||
@@ -34,7 +40,7 @@
|
||||
// anything else (env_gbuff…, enemy_attribute_mul…) a battle EffectRef for the sim content (playerEffects)
|
||||
// cards with `team: true` apply to the picker AND every alive teammate ("若存在其他队友则他们也获得").
|
||||
|
||||
import { weightedPick } from './waves.js';
|
||||
import { weightedPick, firstHalfMax } from './waves.js';
|
||||
|
||||
export const FAMILY_NAMES = { bounty: '悬赏决策', supply: '道具补给', shop: '机密商店', tactic: '战术决策' };
|
||||
|
||||
@@ -75,7 +81,16 @@ function scheduleFor(gd, round) {
|
||||
const r = sch && sch.rounds && sch.rounds[String(round)];
|
||||
if (r && typeof r === 'object') return r;
|
||||
// fallback: a supply draft
|
||||
return { families: [{ family: 'supply', weight: 1 }], cards: gd.isSolo ? 3 : 6, supplyTiers: [1, Math.min(6, 1 + Math.floor(round / 3))], bountyTiers: [1, 2] };
|
||||
return { families: [{ family: 'supply', weight: 1 }], cards: gd.isSolo ? 3 : 6, supplyTiers: [1, Math.min(6, 1 + Math.floor(round / 3))] };
|
||||
}
|
||||
|
||||
/**
|
||||
* The half of the match a 悬赏决策 draft of `round` belongs to: 1 up to the official wave generator's first-half end
|
||||
* (waves.js firstHalfMax, 7), else 2. Its cards are the cards.bounty entries with that `draftHalf` (player feedback
|
||||
* after 0.1.0, report #2).
|
||||
*/
|
||||
export function bountyDraftHalf(round, gd = null) {
|
||||
return Number(round) <= firstHalfMax(gd) ? 1 : 2;
|
||||
}
|
||||
|
||||
function formatCount(gd) {
|
||||
@@ -105,7 +120,7 @@ export function generateDraft(gd, rng, round, { stageId = null, bondAvailable =
|
||||
const fams = Array.isArray(sch.families) && sch.families.length ? sch.families.map((f) => [f.family, f.weight]) : [['supply', 1]];
|
||||
let family = weightedPick(rng, fams) || 'supply';
|
||||
const n = Number.isInteger(sch.cards) && sch.cards > 0 ? Math.min(sch.cards, 6) : formatCount(gd);
|
||||
const opts = { stageId, bondAvailable };
|
||||
const opts = { stageId, bondAvailable, round };
|
||||
let cards = buildCards(gd, rng, family, n, sch, opts);
|
||||
if (!cards.length && family !== 'supply') { family = 'supply'; cards = buildCards(gd, rng, family, n, sch, opts); }
|
||||
if (!cards.length) return null;
|
||||
@@ -145,19 +160,21 @@ export function cardTargetBonds(gd, effectId) {
|
||||
/**
|
||||
* A bounty card the 悬赏决策 draft may offer (choices.json cards.bounty `draft`, tools/build-data.mjs
|
||||
* bountyDraftExclusion: the PRTS "敌人轮选" table — no 战术特训, no 鸭爵 set; user playtest #6 item 4). Data without the
|
||||
* flag (older builds, test fixtures) falls back to the kill bounties.
|
||||
* flag (older builds, test fixtures) falls back to the kill bounties. With `half` (1 | 2, bountyDraftHalf) the card must
|
||||
* also belong to that half (choices.json `draftHalf`; a card without one — test fixtures — fits both).
|
||||
*/
|
||||
export function draftBounty(c) {
|
||||
export function draftBounty(c, half = null) {
|
||||
if (!c) return false;
|
||||
if (half != null && Number.isInteger(c.draftHalf) && c.draftHalf !== half) return false;
|
||||
if (typeof c.draft === 'boolean') return c.draft;
|
||||
return c.payout !== 'perfect';
|
||||
}
|
||||
|
||||
function buildCards(gd, rng, family, n, sch, { stageId = null, bondAvailable = null } = {}) {
|
||||
function buildCards(gd, rng, family, n, sch, { stageId = null, bondAvailable = null, round = 1 } = {}) {
|
||||
const cardsData = gd.choices.cards || {};
|
||||
if (family === 'bounty') {
|
||||
const tiers = Array.isArray(sch.bountyTiers) && sch.bountyTiers.length ? sch.bountyTiers : [1, 2];
|
||||
const pool = (Array.isArray(cardsData.bounty) ? cardsData.bounty : []).filter((c) => c && draftBounty(c) && tiers.includes(c.tier) && gd.enemy(c.enemyKey) && !gd.inactiveEnemies.has(c.enemyKey));
|
||||
const half = sch.bountyHalf === 1 || sch.bountyHalf === 2 ? sch.bountyHalf : bountyDraftHalf(round, gd);
|
||||
const pool = (Array.isArray(cardsData.bounty) ? cardsData.bounty : []).filter((c) => c && draftBounty(c, half) && gd.enemy(c.enemyKey));
|
||||
const pick = pool.slice();
|
||||
rng.shuffle(pick);
|
||||
return pick.slice(0, n).map((c) => {
|
||||
|
||||
@@ -122,7 +122,9 @@ function placeholderMap(gd) {
|
||||
return out;
|
||||
}
|
||||
|
||||
const firstHalfMax = (gd) => intOr(gen(gd).firstHalfMaxRound, Math.floor(intOr(gen(gd).maxLevelCnt, 15) / 2));
|
||||
/** Last round of the official generator's first half (factions.generation.firstHalfMaxRound: maxLevelCnt / 2 = 7); also
|
||||
* the half of the 悬赏决策 drafts (choices.js bountyDraftHalf). */
|
||||
export const firstHalfMax = (gd) => intOr(gd && gd.factions ? gen(gd).firstHalfMaxRound : null, Math.floor(intOr(gd && gd.factions ? gen(gd).maxLevelCnt : null, 15) / 2));
|
||||
|
||||
/** The special entry list (factions.json entries) sorted by key. */
|
||||
function entryList(gd) {
|
||||
|
||||
@@ -27,8 +27,9 @@ function dataWith(modeId, round, family, { tactic = null, bounty = null, cards =
|
||||
const out = { ...DATA, choices: { ...ch, schedule: { ...ch.schedule, [modeId]: { ...sch, rounds: { ...sch.rounds, [String(round)]: r } } }, cards: { ...ch.cards } } };
|
||||
if (tactic) out.choices.cards.tactic = ch.cards.tactic.filter((c) => tactic.includes(c.effectId));
|
||||
// the listed bounty cards are offered by this draft even when the real one would not (战术特训 comes from 法术教鞭
|
||||
// only — choices.json `draft`, user playtest #6 item 4): these tests drive the payouts through the draft
|
||||
if (bounty) out.choices.cards.bounty = ch.cards.bounty.filter((c) => bounty.includes(c.effectId)).map((c) => ({ ...c, draft: true }));
|
||||
// only — choices.json `draft`, user playtest #6 item 4; a "下场作战" card belongs to the drafts from R8 on —
|
||||
// `draftHalf`, player feedback #2): these tests drive the payouts through the draft
|
||||
if (bounty) out.choices.cards.bounty = ch.cards.bounty.filter((c) => bounty.includes(c.effectId)).map((c) => ({ ...c, draft: true, draftHalf: null }));
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
// Player feedback after 0.1.0, report #2 (workstream WB): "本来应该后期出的悬赏的怪物在前期的悬赏就出现了,导致选了打不过".
|
||||
// The 悬赏决策 draft of the first half (co-op / solo 绝境 · 终极 R3, 险境 co-op R3 / R6) offered boss bounties (碎骨, W,
|
||||
// 弑君者, 萨卡兹百夫长, 鼠王, 庞贝, 大鲍勃), second-half specials (尖端萨卡兹枯朽战车, 假想敌:蚀裂 / 淤困 …, 法术大师A2,
|
||||
// 灼藤, 重弩突袭者) and their upgraded variants (新硎, 家族暗影灭迹人, 异光体孽生者 …): the card tier read from the I/II/III
|
||||
// suffix, the boss bounties' small coin values and the multi-round cards' fixed tier 2 all passed the R3 tier window
|
||||
// [1, 2]. Official evidence (tools/build-data.mjs bountyDraftHalf): the one official co-op draft screenshot shows six boss
|
||||
// bounties with coins 1–6 together at team LP 6 / 10 / 9 / 15 (a late draft), and in every faction series of the data
|
||||
// the "接下来两场作战" cards use first-half enemies, the "下场作战" ones second-half enemies. Now a first-half draft
|
||||
// (R ≤ 7, the official wave generator's half) offers the two-battle cards, a draft from R8 on the next-battle and
|
||||
// multi-round cards. Real data, real draft code, the real match path for the players' scenario.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync, existsSync } from 'node:fs';
|
||||
import { PHASE } from '../../shared/constants.js';
|
||||
import { DATA, makeMatch } from './harness.js';
|
||||
import { FakeBattle } from './fakeBattle.js';
|
||||
import { GameData } from '../../server/match/gamedata.js';
|
||||
import { generateDraft, bountyDraftHalf, MULTI_ROUND_BOUNTY_BATTLES } from '../../server/match/choices.js';
|
||||
import { createRng } from '../../server/sim/rng.js';
|
||||
|
||||
const BOUNTY_MODES = ['mode_multi_normal', 'mode_multi_hard', 'mode_multi_abyss', 'mode_single_hard', 'mode_single_abyss'];
|
||||
const CARD = new Map(DATA.choices.cards.bounty.map((c) => [c.effectId, c]));
|
||||
const isBoss = (key) => DATA.enemies[key] && DATA.enemies[key].rank === 'BOSS';
|
||||
|
||||
/** The official wave generator's halves (activity_table specialEnemyInfoDict isInFirstHalf), when the cache has it. */
|
||||
function officialHalves() {
|
||||
const p = new URL('../../.cache/gamedata/excel/activity_table.json', import.meta.url);
|
||||
if (!existsSync(p)) return null;
|
||||
const act = JSON.parse(readFileSync(p, 'utf8')).activity.AUTOCHESS_SEASON.act2autochess;
|
||||
const first = new Set(), second = new Set();
|
||||
for (const v of Object.values(act.specialEnemyInfoDict)) (v.isInFirstHalf ? first : second).add(v.specialEnemyKey);
|
||||
return { first, second, secondOnly: new Set([...second].filter((k) => !first.has(k))) };
|
||||
}
|
||||
|
||||
/** Every bounty draft of every bounty mode and SP round over `seeds` seeds: [{ modeId, round, cards }]. */
|
||||
function drafts(seeds = 30) {
|
||||
const out = [];
|
||||
for (const modeId of BOUNTY_MODES) {
|
||||
const gd = new GameData(DATA, modeId);
|
||||
for (const r of DATA.choices.schedule[modeId].spRounds) {
|
||||
for (let seed = 1; seed <= seeds; seed++) {
|
||||
const d = generateDraft(gd, createRng(seed * 7919 + r * 31), r, { stageId: 'act2autochess_m01' });
|
||||
if (d && d.family === 'bounty') out.push({ modeId, round: r, cards: d.cards });
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
test('#2 data: every drafted bounty card names its draft half — the "接下来两场作战" cards the first, the "下场作战" and multi-round cards the second', () => {
|
||||
let first = 0, second = 0;
|
||||
for (const c of DATA.choices.cards.bounty) {
|
||||
if (!c.draft) { assert.equal(c.draftHalf, null, `${c.effectId}: not drafted, no half`); continue; }
|
||||
assert.equal(c.draftHalf, c.rounds === 2 ? 1 : 2, `${c.effectId} ${c.name} (${c.rounds} battles): half ${c.draftHalf}`);
|
||||
if (c.draftHalf === 1) first++; else second++;
|
||||
}
|
||||
assert.equal(first, 42, '42 two-battle cards (6 faction series × I–III, 4 sets of 6)');
|
||||
assert.equal(second, 63, '56 next-battle cards (incl. 源石虫·特训, 24 boss bounties) + 7 multi-round cards');
|
||||
assert.equal(bountyDraftHalf(3), 1);
|
||||
assert.equal(bountyDraftHalf(7), 1);
|
||||
assert.equal(bountyDraftHalf(8), 2);
|
||||
assert.equal(bountyDraftHalf(9), 2);
|
||||
});
|
||||
|
||||
test('#2 data cross-check (official tables): no first-half card brings a boss or a second-half-only special; every such card is a second-half card', (t) => {
|
||||
const halves = officialHalves();
|
||||
if (!halves) { t.skip('no .cache/gamedata (run tools/build-data.mjs once)'); return; }
|
||||
let late = 0;
|
||||
for (const c of DATA.choices.cards.bounty) {
|
||||
if (!c.draft) continue;
|
||||
const lateEnemy = isBoss(c.enemyKey) || halves.secondOnly.has(c.enemyKey);
|
||||
if (c.draftHalf === 1) assert.ok(!lateEnemy, `${c.effectId} ${c.name}: ${DATA.enemies[c.enemyKey].name} is a boss / second-half special`);
|
||||
if (lateEnemy) { assert.equal(c.draftHalf, 2, `${c.effectId} ${c.name}`); late++; }
|
||||
}
|
||||
assert.ok(late >= 30, `${late} cards with a boss or a second-half special`);
|
||||
// the faction series pair the halves: _4–_6 (two battles) with first-half enemies, _7 / _8 (next battle) with later ones
|
||||
for (const s of [10, 11, 12, 13, 14, 15]) {
|
||||
for (const i of [4, 5, 6]) assert.equal(CARD.get(`enemyeffect_${s}_${i}`).draftHalf, 1, `enemyeffect_${s}_${i}`);
|
||||
for (const i of [7, 8]) assert.equal(CARD.get(`enemyeffect_${s}_${i}`).draftHalf, 2, `enemyeffect_${s}_${i}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('#2 the first-half drafts (R3, 险境 R6) offer only two-battle cards: no boss, no second-half special, no multi-round card', () => {
|
||||
const halves = officialHalves();
|
||||
let early = 0, cards = 0;
|
||||
for (const d of drafts()) {
|
||||
if (d.round > 7) continue;
|
||||
early++;
|
||||
for (const c of d.cards) {
|
||||
cards++;
|
||||
const card = CARD.get(c.id);
|
||||
assert.equal(card.rounds, 2, `${d.modeId} R${d.round}: ${c.id} ${c.name} lasts ${card.rounds} battle(s) — a later draft's card`);
|
||||
assert.ok(!isBoss(c.enemyKey), `${d.modeId} R${d.round}: ${c.name} — a boss bounty in the first half`);
|
||||
if (halves) assert.ok(!halves.secondOnly.has(c.enemyKey), `${d.modeId} R${d.round}: ${c.name} — a second-half special`);
|
||||
assert.ok(!card.multiRound, `${d.modeId} R${d.round}: ${c.name} — a multi-round card`);
|
||||
}
|
||||
}
|
||||
assert.ok(early >= 100 && cards >= 400, `${early} first-half bounty drafts, ${cards} cards`);
|
||||
});
|
||||
|
||||
test('#2 the second-half drafts (R9) offer the next-battle and multi-round cards — boss bounties of every coin value among them (the official screenshot: 碎骨 1 … “复仇者” 6)', () => {
|
||||
const coins = new Set();
|
||||
let late = 0, multi = 0;
|
||||
for (const d of drafts(60)) {
|
||||
if (d.round < 8) continue;
|
||||
late++;
|
||||
for (const c of d.cards) {
|
||||
const card = CARD.get(c.id);
|
||||
assert.notEqual(card.rounds, 2, `${d.modeId} R${d.round}: ${c.id} ${c.name} — a first-half (two-battle) card`);
|
||||
if (isBoss(c.enemyKey)) coins.add(c.coin);
|
||||
if (card.multiRound) {
|
||||
multi++;
|
||||
assert.equal(c.rounds, MULTI_ROUND_BOUNTY_BATTLES, 'a multi-round card still lasts two battles (settled, DESIGN §20.9)');
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.ok(late >= 100, `${late} second-half drafts`);
|
||||
for (const n of [1, 2, 3, 4, 5, 6]) assert.ok(coins.has(n), `a boss bounty worth ${n} was offered (seen: ${[...coins].sort().join(', ')})`);
|
||||
assert.ok(multi > 0, 'multi-round cards are offered in the second half');
|
||||
});
|
||||
|
||||
test('#2 the mode\'s inactive enemy list does not thin the bounty draft (PRTS 卫戍协议:盟约 11/18 note "不影响悬赏决策出场")', () => {
|
||||
const gd = new GameData(DATA, 'mode_multi_normal');
|
||||
const dropped = ['enemy_1272_nhtank_2', 'enemy_10067_ftsjc', 'enemy_9008_acbunn'];
|
||||
for (const k of dropped) assert.ok(gd.inactiveEnemies.has(k), `${k} is inactive in 险境 waves`);
|
||||
const seen = new Set();
|
||||
for (let seed = 1; seed <= 400 && seen.size < dropped.length; seed++) {
|
||||
const d = generateDraft(gd, createRng(seed), 9, { stageId: 'act2autochess_m01' });
|
||||
if (d && d.family === 'bounty') for (const c of d.cards) if (dropped.includes(c.enemyKey)) seen.add(c.enemyKey);
|
||||
}
|
||||
assert.deepEqual([...seen].sort(), dropped.slice().sort(), 'the three bounties stay in 险境\'s second-half drafts');
|
||||
});
|
||||
|
||||
test('#2 E2E (co-op 绝境, the players\' case): the real R3 draft offers no late bounty, and the picked enemy comes for R3 and R4 at their round scale', () => {
|
||||
for (let seed = 1; seed <= 6; seed++) {
|
||||
const h = makeMatch({ mode: 'coop', difficulty: 'HARD', humans: 1, bots: 1, seed: 910 + seed, fake: true }).start();
|
||||
const m = h.m;
|
||||
assert.ok(h.drive(() => m.phase === PHASE.SP_DRAFT && m.round === 3, { ready: true }), 'the R3 draft');
|
||||
assert.equal(m.sp.family, 'bounty', 'R3 of 绝境 is always a bounty draft');
|
||||
const names = m.sp.cards.map((c) => `${c.name}(${CARD.get(c.id).rounds})`).join(', ');
|
||||
for (const c of m.sp.cards) {
|
||||
assert.equal(CARD.get(c.id).draftHalf, 1, `seed ${seed}: ${names}`);
|
||||
assert.ok(!isBoss(c.enemyKey), `seed ${seed}: ${c.name} is a boss bounty`);
|
||||
}
|
||||
assert.equal(m.publicView().sp.cards.length, m.sp.cards.length);
|
||||
// the human takes a card when its turn comes; the bot picks by itself
|
||||
let picked = null;
|
||||
for (let guard = 0; guard < 200 && m.phase === PHASE.SP_DRAFT; guard++) {
|
||||
if (m.spTurn() === 'p_0') {
|
||||
picked = m.sp.cards.find((c) => m.sp.taken[c.idx] == null);
|
||||
assert.deepEqual(m.handle('p_0', { t: 'g.choice', idx: picked.idx }), { ok: true });
|
||||
} else h.sched.runNext();
|
||||
}
|
||||
assert.ok(picked, 'the human picked');
|
||||
const b = h.ps('p_0').bounties.find((x) => x.card.effectId === picked.id);
|
||||
assert.ok(b, 'the picker holds the bounty');
|
||||
h.drive(() => m.phase === PHASE.ROUND_START && m.round === 6, { ready: true });
|
||||
const rounds = [];
|
||||
for (const f of FakeBattle.instances) {
|
||||
if (f.kind !== 'normal' || !f.players.includes('p_0')) continue;
|
||||
const spec = f.spawns.find((s) => s.tag === 'bounty' && s.mods && s.mods.bountyId === b.id);
|
||||
if (!spec) continue;
|
||||
rounds.push(f.round);
|
||||
const scale = m.gd.enemyScale(f.round);
|
||||
assert.equal(spec.mods.hpMul, scale.hpMul, `R${f.round}: the bounty enemy takes the round's HP scale`);
|
||||
assert.equal(spec.mods.atkMul, scale.atkMul, `R${f.round}: the bounty enemy takes the round's ATK scale`);
|
||||
}
|
||||
assert.deepEqual(rounds, [3, 4], `seed ${seed}: ${picked.name} (两场作战) spawned in R${rounds.join(', R')}`);
|
||||
m.dispose();
|
||||
}
|
||||
});
|
||||
@@ -173,12 +173,13 @@ test('#4 data: cards.bounty marks the draft pool after the PRTS table — 战术
|
||||
assert.equal(list.find((c) => c.effectId === 'enemyeffect_5_1').draft, true, '源石虫·特训 is in the table too');
|
||||
});
|
||||
|
||||
/** Drive a co-op 绝境 match to its R3 bounty draft; the human takes the first free card `want(card, free)` accepts. */
|
||||
function pickAtR3(seed, want) {
|
||||
/** Drive a co-op 绝境 match to its R3 (or R`round`) bounty draft; the human takes the first free card `want(card, free)`
|
||||
* accepts. */
|
||||
function pickAtR3(seed, want, round = 3) {
|
||||
const h = makeMatch({ mode: 'coop', difficulty: 'HARD', humans: 1, bots: 1, seed, fake: true }).start();
|
||||
const m = h.m;
|
||||
const ok = h.drive(() => m.phase === PHASE.SP_DRAFT && m.round === 3, { ready: true });
|
||||
assert.ok(ok && m.sp.family === 'bounty', `seed ${seed}: the R3 bounty draft`);
|
||||
const ok = h.drive(() => m.phase === PHASE.SP_DRAFT && m.round === round, { ready: true });
|
||||
assert.ok(ok && m.sp.family === 'bounty', `seed ${seed}: the R${round} bounty draft`);
|
||||
let picked = null;
|
||||
for (let guard = 0; guard < 200 && m.phase === PHASE.SP_DRAFT; guard++) {
|
||||
if (m.spTurn() === 'p_0') {
|
||||
@@ -191,7 +192,7 @@ function pickAtR3(seed, want) {
|
||||
return { h, m, ps: h.ps('p_0'), picked };
|
||||
}
|
||||
|
||||
/** Rounds (≤ R8) whose normal battle of p_0 spawned bounty `bountyId`. */
|
||||
/** Rounds whose normal battle of p_0 spawned bounty `bountyId`. */
|
||||
function bountyRounds(bountyId) {
|
||||
const out = [];
|
||||
for (const f of FakeBattle.instances) {
|
||||
@@ -221,11 +222,12 @@ test('#4 E2E (co-op 绝境, the real R3 draft): a 1–2-battle bounty\'s enemies
|
||||
});
|
||||
|
||||
test('#4 E2E: a multi-round card lasts two battles like the "两场作战" cards — blue text on the card and in the effects column (the user\'s call)', () => {
|
||||
// "我不记得有过多轮悬赏" (user, after playtest #6): choices.js MULTI_ROUND_BOUNTY_BATTLES = 2; null restores 每场
|
||||
// "我不记得有过多轮悬赏" (user, after playtest #6): choices.js MULTI_ROUND_BOUNTY_BATTLES = 2; null restores 每场. The
|
||||
// multi-round cards are second-half cards (the R9 draft; player feedback #2, choices.js bountyDraftHalf)
|
||||
assert.equal(MULTI_ROUND_BOUNTY_BATTLES, 2);
|
||||
let done = false;
|
||||
for (let seed = 1; seed <= 40 && !done; seed++) {
|
||||
const { h, m, ps, picked } = pickAtR3(700 + seed, (c) => MULTI.has(c.id));
|
||||
const { h, m, ps, picked } = pickAtR3(700 + seed, (c) => MULTI.has(c.id), 9);
|
||||
if (!picked) { m.dispose(); continue; }
|
||||
done = true;
|
||||
assert.equal(picked.rounds, 2);
|
||||
@@ -236,12 +238,12 @@ test('#4 E2E: a multi-round card lasts two battles like the "两场作战" cards
|
||||
const e = ps.privateView().effects.find((x) => x.id === b.id);
|
||||
assert.equal(e.counterText, '还剩 2 场作战');
|
||||
assert.match(e.desc, /<@ba\.vup>两场作战<\/>/, 'the effects tooltip says the same');
|
||||
h.drive(() => m.phase === PHASE.ROUND_START && m.round === 9, { ready: true });
|
||||
assert.deepEqual(bountyRounds(b.id), [3, 4], 'its enemy comes for two battles, then never again');
|
||||
h.drive(() => m.phase === PHASE.ROUND_START && m.round === 13, { ready: true });
|
||||
assert.deepEqual(bountyRounds(b.id), [9, 10], 'its enemy comes for two battles, then never again');
|
||||
assert.ok(!ps.bounties.some((x) => x.id === b.id), 'and the bounty is gone');
|
||||
m.dispose();
|
||||
}
|
||||
assert.ok(done, 'some R3 draft offered a multi-round card');
|
||||
assert.ok(done, 'some R9 draft offered a multi-round card');
|
||||
});
|
||||
|
||||
test('#4 the active bounty says how many battles it has left, with its card text in the official colours (effects column)', () => {
|
||||
|
||||
@@ -37,9 +37,11 @@ test('#4 the draft overlay shows each bounty card\'s battles in the official col
|
||||
// the official multi-round cards: two battles, in the two-battle cards' blue (the user's call after playtest #6)
|
||||
const MULTI = new Set(DATA.choices.cards.bounty.filter((c) => c.multiRound).map((c) => c.effectId));
|
||||
const seen = new Set();
|
||||
for (let seed = 1; seed <= 40 && seen.size < 3; seed++) {
|
||||
const d = generateDraft(gd, createRng(seed * 977 + 3), 3, { stageId: 'act2autochess_m02' });
|
||||
assert.equal(d.family, 'bounty', 'co-op 绝境 R3 is a bounty draft');
|
||||
// R3 offers the two-battle cards, R9 the next-battle and multi-round ones (player feedback #2, choices.js bountyDraftHalf)
|
||||
for (let seed = 1; seed <= 80 && seen.size < 3; seed++) {
|
||||
const round = seed % 2 ? 3 : 9;
|
||||
const d = generateDraft(gd, createRng(seed * 977 + 3), round, { stageId: 'act2autochess_m02' });
|
||||
assert.equal(d.family, 'bounty', `co-op 绝境 R${round} is a bounty draft`);
|
||||
const sp = normalizeSp({ family: d.family, cards: d.cards.map(cardView), order: ['p_0'], turn: 'p_0', picks: {}, taken: {} }, [{ playerId: 'p_0' }]);
|
||||
for (const card of sp.cards) {
|
||||
const view = resolveSpCard(card, sp.family);
|
||||
|
||||
+29
-5
@@ -2334,6 +2334,27 @@ function bountyDraftExclusion(e, main) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The half of the match whose 悬赏决策 drafts offer a drafted bounty card (player feedback after 0.1.0, report #2: "本来应该
|
||||
* 后期出的悬赏的怪物在前期的悬赏就出现了"): 1 = the drafts of rounds ≤ firstHalfMaxRound (7: co-op / solo 绝境 · 终极 R3,
|
||||
* 险境 co-op R3 / R6), 2 = the drafts from R8 on (R9). The official card list of each draft event (enemy_initial_1..10,
|
||||
* bounty_hunter_1..15, bossInitial_1..6) is server-side; what the official side shows:
|
||||
* - the one official co-op 悬赏决策 screenshot (Bahamut 12294, research 06 §15 "Wrkhtxv") holds six boss bounties —
|
||||
* “复仇者” 6, “庞贝” 2, W 1, 萨卡兹百夫长 2, “邪魔的利刃” 3, 碎骨 1 — with the team at LP 6 / 10 / 9 / 15: a late
|
||||
* draft, coin values 1–6 together;
|
||||
* - the data's six faction series pair the battles with the wave generator's halves (specialEnemyInfoDict
|
||||
* isInFirstHalf): "接下来两场作战" (_4–_6) bring first-half enemies or their attached keys (妖怪, 磨砻, 逐腐兽, 萨卡兹
|
||||
* 枯朽战车, 山海众头目, 深池伙友卫队 …), "下场作战" (_7 / _8) second-half specials or their upgraded copies (法术大师A2,
|
||||
* 假想敌:蚀裂, 尖端萨卡兹枯朽战车, 新硎, 家族暗影灭迹人, 异光体孽生者 …).
|
||||
* Rule: a "接下来两场作战" card (data `round` 2) is a first-half card; a "下场作战" card (the boss bounties, the 特异 giants,
|
||||
* the faction _7 / _8 cards, 源石虫·特训) and a multi-round card (the 假想敌, a second-half special each) a second-half
|
||||
* one [ASSUMED beyond the evidence above]. Not a drafted card ⇒ null.
|
||||
*/
|
||||
function bountyDraftHalf(main, draftExcluded) {
|
||||
if (draftExcluded) return null;
|
||||
return main.rounds === 2 ? 1 : 2;
|
||||
}
|
||||
|
||||
/** Classify a 机变 choice event id into a family. */
|
||||
function choiceFamily(ev) {
|
||||
if (ev.choiceType === 'BOUNTY_HUNT' || ev.choiceType === 'PERSONAL_CHOOSE') return 'bounty';
|
||||
@@ -2387,7 +2408,7 @@ function buildChoices(ctx, effects, items, chess) {
|
||||
bounty.push({
|
||||
effectId: e.effectId, name: e.name, desc: e.desc, tier, coin,
|
||||
payout: main.payout, rounds: main.rounds, multiRound, enemyKey: main.enemyKey, count: main.count, adds,
|
||||
draft: !draftExcluded, draftExcluded,
|
||||
draft: !draftExcluded, draftExcluded, draftHalf: bountyDraftHalf(main, draftExcluded),
|
||||
});
|
||||
}
|
||||
bounty.sort((a, b) => naturalCmp(a.effectId, b.effectId));
|
||||
@@ -2412,7 +2433,9 @@ function buildChoices(ctx, effects, items, chess) {
|
||||
// Per-mode schedule of families (research 01 A4).
|
||||
const schedule = {};
|
||||
const supplyWindow = { 3: [1, 4], 6: [2, 5], 9: [3, 6], 11: [4, 6] };
|
||||
const bountyTiers = { 3: [1, 2], 6: [1, 2, 3], 9: [2, 3], 11: [2, 3] };
|
||||
// the 悬赏决策 draft of a round offers the cards of its half (cards.bounty `draftHalf`, bountyDraftHalf): the official
|
||||
// wave generator's first half ends at maxLevelCnt / 2 (factions.generation.firstHalfMaxRound)
|
||||
const firstHalfMaxRound = Math.floor((ctx.ac.constData.maxLevelCnt ?? 15) / 2);
|
||||
for (const [modeId, rounds] of Object.entries(act.battleDataDict)) {
|
||||
const mode = act.modeDataDict[modeId];
|
||||
const solo = mode.modeType === 'SINGLE';
|
||||
@@ -2429,10 +2452,11 @@ function buildChoices(ctx, effects, items, chess) {
|
||||
? [{ family: r === 9 ? 'tactic' : 'supply', weight: 100 }]
|
||||
: [{ family: 'bounty', weight: 50 }, { family: 'supply', weight: 25 }, { family: 'shop', weight: 10 }, { family: 'tactic', weight: 15 }];
|
||||
} else families = [{ family: 'supply', weight: 45 }, { family: 'tactic', weight: 35 }, { family: 'shop', weight: 20 }];
|
||||
const bountyEvents = r <= 3 ? eventsOf('bounty', (e) => /^enemy_initial/.test(e.id) && e.solo === solo) : eventsOf('bounty', (e) => /^(bounty_hunter|bossInitial)/.test(e.id) && e.solo === solo);
|
||||
const bountyHalf = r <= firstHalfMaxRound ? 1 : 2;
|
||||
const bountyEvents = bountyHalf === 1 ? eventsOf('bounty', (e) => /^enemy_initial/.test(e.id) && e.solo === solo) : eventsOf('bounty', (e) => /^(bounty_hunter|bossInitial)/.test(e.id) && e.solo === solo);
|
||||
m[r] = {
|
||||
families, cards: solo ? 3 : 6,
|
||||
supplyTiers: supplyWindow[r] || [1, 6], bountyTiers: bountyTiers[r] || [1, 3],
|
||||
supplyTiers: supplyWindow[r] || [1, 6], bountyHalf,
|
||||
events: {
|
||||
bounty: bountyEvents,
|
||||
supply: diff === 'TRAINING' ? eventsOf('supply', (e) => e.training) : eventsOf('supply', (e) => !e.training && e.solo === solo),
|
||||
@@ -2448,7 +2472,7 @@ function buildChoices(ctx, effects, items, chess) {
|
||||
return {
|
||||
events,
|
||||
families: {
|
||||
bounty: { name: '悬赏决策', desc: '选定悬赏目标,获取额外奖励。', cards: 'cards.bounty entries with draft: true (PRTS 敌人轮选: no 战术特训 — the 教鞭 Art offers those — and no 鸭爵 set)' },
|
||||
bounty: { name: '悬赏决策', desc: '选定悬赏目标,获取额外奖励。', cards: 'cards.bounty entries with draft: true (PRTS 敌人轮选: no 战术特训 — the 教鞭 Art offers those — and no 鸭爵 set) and draftHalf = schedule[*].bountyHalf (1: the "接下来两场作战" cards, rounds ≤ firstHalfMaxRound; 2: the "下场作战" and multi-round cards)' },
|
||||
supply: { name: '道具补给', desc: '无需消耗资金,获得装备补给。', cards: 'random normal EQUIP items in schedule[*].supplyTiers (duplicates allowed)' },
|
||||
shop: { name: '机密商店', desc: '无需消耗资金,获得装备补给。', cards: 'random normal EQUIP items of any tier I–VI (duplicates allowed)' },
|
||||
tactic: { name: '战术决策', desc: '选择战术增益。', cards: 'cards.tactic (terrain cards only for the match stage)' },
|
||||
|
||||
Reference in new issue
Block a user