feedback5 WE2: 自选 summon hand counts include the tokens' own deploy talents (#21)

tools/build-data.mjs tokenTalentDeckBonus adds a summon's talent max_deploy_count /
max_deck_stack_cnt (the hidden "TOKEN数" talent; a module token part of the same talent
replaces it, matched by prefabKey) to its variant deployLimit / deckStack. Regenerated
offline: only data/backups.json tokens of 麦哲伦 (drones), 令 (souls), 白铁 (devices) and
夜莺 (幻影) changed — deployLimit 3 / 4 with SUM-Y stage 3, 2, 3; deckStack 5 (8), 3 (4).
The hand count (player/diy.js placeableTokens) now matches the official one for every
自选 summoner (PRTS 卫戍协议/帮助 "根据召唤物部署数量上限", the owners' talents, PRTS 幻影 备注).
The summoner kits read deckStack / deployLimit as they are (same decks as before).
Golden: no scenario moved (the diy family places no summon pieces).
This commit is contained in:
sganggs committed 2026-10-06 15:01:46 +08:00
1 parent a009d77d71
commit 99d8807328
12 files changed
+165 -33

No files matched your search

+1 -1
View File
@@ -1 +1 @@
{"units":{"char_600_cpione":{"charId":"char_600_cpione","name":"预备干员-先锋","appellation":"Reserve Operator - Vanguard","rarity":4,"profession":"PIONEER","subProfessionId":"pioneer","subProfessionName":"尖兵","position":"MELEE","nationId":null,"isNotObtainable":true,"assets":{"avatar":"char_600_cpione","avatarGolden":"char_600_cpione","portrait":"char_600_cpione_1","portraitGolden":"char_600_cpione_1","spine":"char_600_cpione","subProfIcon":"sub_pioneer_icon"},"moduleNames":{},"standsIn":["chess_char_5_01_a"],"forms":{"2/1/4/0":{"status":{"phase":2,"level":1,"skillLevel":4,"equipLevel":0},"stats":{"maxHp":1225,"atk":320,"def":272,"res":0,"cost":11,"blockCnt":2,"bat":1.05,"aspd":100,"respawnTime":70,"spRecovery":1,"hpRecoveryPerSec":0,"moveSpeed":1,"tauntLevel":0,"massLevel":0,"deployLimit":1,"deckStack":0},"immunities":{"stun":false,"silence":false,"sleep":false,"frozen":false,"levitate":false},"rangeId":"1-1","rangeGrid":[[0,0],[0,1]],"dmgType":"phys","attackKind":"melee","projectile":"none","canHitFly":false,"targetPriority":null,"trait":{"desc":"能够阻挡两个敌人","descRaw":"能够阻挡两个敌人","bb":{},"bbStr":{},"rangeGrid":null},"skills":[{"skillId":"skcom_charge_cost[1]","iconId":"skcom_charge_cost[1]","name":"冲锋号令·α型","level":4,"desc":"立即获得6点部署费用","descRaw":"立即获得<@ba.vup>6</>点部署费用","skillType":"AUTO","durationType":"NONE","duration":0,"spType":"INCREASE_WITH_TIME","spCost":27,"initSp":3,"maxChargeTime":1,"increment":1,"bb":{"cost":6},"bbStr":{},"rangeId":null,"rangeGrid":null,"prefabId":"skcom_charge_cost","trigger":{"rule":"DEFAULT","rawRule":"DEFAULT","customRangeGrid":null},"index":0,"overrideTokenKey":null},{"skillId":"skcom_charge_cost[2]","iconId":"skcom_charge_cost[2]","name":"冲锋号令·β型","level":4,"desc":"立即获得9点部署费用","descRaw":"立即获得<@ba.vup>9</>点部署费用","skillType":"AUTO","durationType":"NONE","duration":0,"spType":"INCREASE_WITH_TIME","spCost":36,"initSp":9,"maxChargeTime":1,"increment":1,"bb":{"cost":9},"bbStr":{},"rangeId":null,"rangeGrid":null,"prefabId":"skcom_charge_cost","trigger":{"rule":"DEFAULT","rawRule":"DEFAULT","customRangeGrid":null},"index":1,"overrideTokenKey":null},{"skillId":"skcom_charge_cost[3]","iconId":"skcom_charge_cost[3]","name":"冲锋号令·γ型","level":4,"desc":"立即获得12点部署费用","descRaw":"立即获得<@ba.vup>12</>点部署费用","skillType":"AUTO","durationType":"NONE","duration":0,"spType":"INCREASE_WITH_TIME","spCost":41,"initSp":12,"maxChargeTime":1,"increment":1,"bb":{"cost":12},"bbStr":{},"rangeId":null,"rangeGrid":null,"prefabId":"skcom_charge_cost","trigger":{"rule":"DEFAULT","rawRule":"DEFAULT","customRangeGrid":null},"index":2,"overrideTokenKey":null}],"talents":[{"index":0,"name":"攻击提升","desc":"攻击力+8%","descRaw":"攻击力+8%","bb":{"atk":0.08},"bbStr":{},"rangeGrid":null,"tokenKey":null,"hidden":false,"fromModule":false}],"displayTokens":[],"tokens":[]},"2/60/7/1":{"status":{"phase":2,"level":60,"skillLevel":7,"equipLevel":1},"stats":{"maxHp":1674,"atk":380,"def":322,"res":0,"cost":11,"blockCnt":2,"bat":1.05,"aspd":100,"respawnTime":70,"spRecovery":1,"hpRecoveryPerSec":0,"moveSpeed":1,"tauntLevel":0,"massLevel":0,"deployLimit":1,"deckStack":0},"immunities":{"stun":false,"silence":false,"sleep":false,"frozen":false,"levitate":false},"rangeId":"1-1","rangeGrid":[[0,0],[0,1]],"dmgType":"phys","attackKind":"melee","projectile":"none","canHitFly":false,"targetPriority":null,"trait":{"desc":"能够阻挡两个敌人","descRaw":"能够阻挡两个敌人","bb":{},"bbStr":{},"rangeGrid":null},"skills":[{"skillId":"skcom_charge_cost[1]","iconId":"skcom_charge_cost[1]","name":"冲锋号令·α型","level":7,"desc":"立即获得6点部署费用","descRaw":"立即获得<@ba.vup>6</>点部署费用","skillType":"AUTO","durationType":"NONE","duration":0,"spType":"INCREASE_WITH_TIME","spCost":25,"initSp":6,"maxChargeTime":1,"increment":1,"bb":{"cost":6},"bbStr":{},"rangeId":null,"rangeGrid":null,"prefabId":"skcom_charge_cost","trigger":{"rule":"DEFAULT","rawRule":"DEFAULT","customRangeGrid":null},"index":0,"overrideTokenKey":null},{"skillId":"skcom_charge_cost[2]","iconId":"skcom_charge_cost[2]","name":"冲锋号令·β型","level":7,"desc":"立即获得9点部署费用","descRaw":"立即获得<@ba.vup>9</>点部署费用","skillType":"AUTO","durationType":"NONE","duration":0,"spType":"INCREASE_WITH_TIME","spCost":33,"initSp":10,"maxChargeTime":1,"increment":1,"bb":{"cost":9},"bbStr":{},"rangeId":null,"rangeGrid":null,"prefabId":"skcom_charge_cost","trigger":{"rule":"DEFAULT","rawRule":"DEFAULT","customRangeGrid":null},"index":1,"overrideTokenKey":null},{"skillId":"skcom_charge_cost[3]","iconId":"skcom_charge_cost[3]","name":"冲锋号令·γ型","level":7,"desc":"立即获得12点部署费用","descRaw":"立即获得<@ba.vup>12</>点部署费用","skillType":"AUTO","durationType":"NONE","duration":0,"spType":"INCREASE_WILine truncated {"units":{"char_600_cpione":{"charId":"char_600_cpione","name":"预备干员-先锋","appellation":"Reserve Operator - Vanguard","rarity":4,"profession":"PIONEER","subProfessionId":"pioneer","subProfessionName":"尖兵","position":"MELEE","nationId":null,"isNotObtainable":true,"assets":{"avatar":"char_600_cpione","avatarGolden":"char_600_cpione","portrait":"char_600_cpione_1","portraitGolden":"char_600_cpione_1","spine":"char_600_cpione","subProfIcon":"sub_pioneer_icon"},"moduleNames":{},"standsIn":["chess_char_5_01_a"],"forms":{"2/1/4/0":{"status":{"phase":2,"level":1,"skillLevel":4,"equipLevel":0},"stats":{"maxHp":1225,"atk":320,"def":272,"res":0,"cost":11,"blockCnt":2,"bat":1.05,"aspd":100,"respawnTime":70,"spRecovery":1,"hpRecoveryPerSec":0,"moveSpeed":1,"tauntLevel":0,"massLevel":0,"deployLimit":1,"deckStack":0},"immunities":{"stun":false,"silence":false,"sleep":false,"frozen":false,"levitate":false},"rangeId":"1-1","rangeGrid":[[0,0],[0,1]],"dmgType":"phys","attackKind":"melee","projectile":"none","canHitFly":false,"targetPriority":null,"trait":{"desc":"能够阻挡两个敌人","descRaw":"能够阻挡两个敌人","bb":{},"bbStr":{},"rangeGrid":null},"skills":[{"skillId":"skcom_charge_cost[1]","iconId":"skcom_charge_cost[1]","name":"冲锋号令·α型","level":4,"desc":"立即获得6点部署费用","descRaw":"立即获得<@ba.vup>6</>点部署费用","skillType":"AUTO","durationType":"NONE","duration":0,"spType":"INCREASE_WITH_TIME","spCost":27,"initSp":3,"maxChargeTime":1,"increment":1,"bb":{"cost":6},"bbStr":{},"rangeId":null,"rangeGrid":null,"prefabId":"skcom_charge_cost","trigger":{"rule":"DEFAULT","rawRule":"DEFAULT","customRangeGrid":null},"index":0,"overrideTokenKey":null},{"skillId":"skcom_charge_cost[2]","iconId":"skcom_charge_cost[2]","name":"冲锋号令·β型","level":4,"desc":"立即获得9点部署费用","descRaw":"立即获得<@ba.vup>9</>点部署费用","skillType":"AUTO","durationType":"NONE","duration":0,"spType":"INCREASE_WITH_TIME","spCost":36,"initSp":9,"maxChargeTime":1,"increment":1,"bb":{"cost":9},"bbStr":{},"rangeId":null,"rangeGrid":null,"prefabId":"skcom_charge_cost","trigger":{"rule":"DEFAULT","rawRule":"DEFAULT","customRangeGrid":null},"index":1,"overrideTokenKey":null},{"skillId":"skcom_charge_cost[3]","iconId":"skcom_charge_cost[3]","name":"冲锋号令·γ型","level":4,"desc":"立即获得12点部署费用","descRaw":"立即获得<@ba.vup>12</>点部署费用","skillType":"AUTO","durationType":"NONE","duration":0,"spType":"INCREASE_WITH_TIME","spCost":41,"initSp":12,"maxChargeTime":1,"increment":1,"bb":{"cost":12},"bbStr":{},"rangeId":null,"rangeGrid":null,"prefabId":"skcom_charge_cost","trigger":{"rule":"DEFAULT","rawRule":"DEFAULT","customRangeGrid":null},"index":2,"overrideTokenKey":null}],"talents":[{"index":0,"name":"攻击提升","desc":"攻击力+8%","descRaw":"攻击力+8%","bb":{"atk":0.08},"bbStr":{},"rangeGrid":null,"tokenKey":null,"hidden":false,"fromModule":false}],"displayTokens":[],"tokens":[]},"2/60/7/1":{"status":{"phase":2,"level":60,"skillLevel":7,"equipLevel":1},"stats":{"maxHp":1674,"atk":380,"def":322,"res":0,"cost":11,"blockCnt":2,"bat":1.05,"aspd":100,"respawnTime":70,"spRecovery":1,"hpRecoveryPerSec":0,"moveSpeed":1,"tauntLevel":0,"massLevel":0,"deployLimit":1,"deckStack":0},"immunities":{"stun":false,"silence":false,"sleep":false,"frozen":false,"levitate":false},"rangeId":"1-1","rangeGrid":[[0,0],[0,1]],"dmgType":"phys","attackKind":"melee","projectile":"none","canHitFly":false,"targetPriority":null,"trait":{"desc":"能够阻挡两个敌人","descRaw":"能够阻挡两个敌人","bb":{},"bbStr":{},"rangeGrid":null},"skills":[{"skillId":"skcom_charge_cost[1]","iconId":"skcom_charge_cost[1]","name":"冲锋号令·α型","level":7,"desc":"立即获得6点部署费用","descRaw":"立即获得<@ba.vup>6</>点部署费用","skillType":"AUTO","durationType":"NONE","duration":0,"spType":"INCREASE_WITH_TIME","spCost":25,"initSp":6,"maxChargeTime":1,"increment":1,"bb":{"cost":6},"bbStr":{},"rangeId":null,"rangeGrid":null,"prefabId":"skcom_charge_cost","trigger":{"rule":"DEFAULT","rawRule":"DEFAULT","customRangeGrid":null},"index":0,"overrideTokenKey":null},{"skillId":"skcom_charge_cost[2]","iconId":"skcom_charge_cost[2]","name":"冲锋号令·β型","level":7,"desc":"立即获得9点部署费用","descRaw":"立即获得<@ba.vup>9</>点部署费用","skillType":"AUTO","durationType":"NONE","duration":0,"spType":"INCREASE_WITH_TIME","spCost":33,"initSp":10,"maxChargeTime":1,"increment":1,"bb":{"cost":9},"bbStr":{},"rangeId":null,"rangeGrid":null,"prefabId":"skcom_charge_cost","trigger":{"rule":"DEFAULT","rawRule":"DEFAULT","customRangeGrid":null},"index":1,"overrideTokenKey":null},{"skillId":"skcom_charge_cost[3]","iconId":"skcom_charge_cost[3]","name":"冲锋号令·γ型","level":7,"desc":"立即获得12点部署费用","descRaw":"立即获得<@ba.vup>12</>点部署费用","skillType":"AUTO","durationType":"NONE","duration":0,"spType":"INCREASE_WILine truncated
+2 -2
View File
@@ -486,9 +486,9 @@ Glyph legend (`rows`):
| `owners[]` | `["chess_char_3_19_a","chess_char_3_19_b"]` | | | `owners[]` | `["chess_char_3_19_a","chess_char_3_19_b"]` | |
| `stats`, `rangeGrid`, `dmgType`, `attackKind`, `projectile`, `canHitFly` | first owner's values | defaults | | `stats`, `rangeGrid`, `dmgType`, `attackKind`, `projectile`, `canHitFly` | first owner's values | defaults |
| `skill` | `{"skillId":"sktok_vigil_wolf_3","bb":{…}}` | default token skill (same slot as the owner's skill) | | `skill` | `{"skillId":"sktok_vigil_wolf_3","bb":{…}}` | default token skill (same slot as the owner's skill) |
| `deployLimit`, `count` | `1`, `1` | `count` = copies sent to the hand / spawned (talent/skill `cnt`); `null` ⇒ use `deployLimit` | | `deployLimit`, `count` | `1`, `1` | `deployLimit` = the first owner's `stats.deployLimit`. `count` = copies sent to the hand / spawned (talent/skill `cnt`); `null` ⇒ use `deployLimit` |
| `abnormal[]` | `["healFree"]` | abnormal effects the summon holds from the start, no official table carries them — `tools/build-data.mjs TOKEN_ABNORMAL` from the PRTS summon pages (user playtest #6 item 18): `healFree` = 禁疗 (“小自在”, “耀阳”, 斯卡蒂的海嗣, 沙之碑, 流形, 狼群, 迷迭香的战术装备, 黄金盟誓, 保护目标(冻结状态)), `isolated` = 孤立 "无法被同阵营选中" (“炎佑”, 从不混淆的方向); `[]` otherwise. The sim sets `noHeal` / `isolated` (docs/SIM.md §3) | | `abnormal[]` | `["healFree"]` | abnormal effects the summon holds from the start, no official table carries them — `tools/build-data.mjs TOKEN_ABNORMAL` from the PRTS summon pages (user playtest #6 item 18): `healFree` = 禁疗 (“小自在”, “耀阳”, 斯卡蒂的海嗣, 沙之碑, 流形, 狼群, 迷迭香的战术装备, 黄金盟誓, 保护目标(冻结状态)), `isolated` = 孤立 "无法被同阵营选中" (“炎佑”, 从不混淆的方向); `[]` otherwise. The sim sets `noHeal` / `isolated` (docs/SIM.md §3) |
| `variants[ownerChessId]` | `{"phase":2,"level":1,"stats":{…},"immunities":{…},"rangeGrid":…,"trait":{…},"dmgType":…,"skill":{full skill record},"talents":[…],"count":1,"sources":["talent","display"]}` | stats at the owner's phase/level (clamped to the token's max level) + golden module `tokenAttributeBlackboard`; the owner's module parts flagged `isToken` upgrade the variant's `trait` (+`moduleDesc`) and `talents` (伺夜's wolves, 缪尔赛思's 流形 `scale` 1, 浊心斯卡蒂's 海嗣 30 s, “耀阳” `atk_scale` 1.15). `sources` ⊆ `talent`/`skill`/`display`: how the owner produces it (`display` only = listed on the character but unused by its default skill/talents, e.g. 迷迭香 S2, 凛御银灰 eagle1/3). `count` = copies from a talent `cnt` or the default skill's `cnt` when that skill overrides this token; `null` ⇒ use `deployLimit` | | `variants[ownerChessId]` | `{"phase":2,"level":1,"stats":{…},"immunities":{…},"rangeGrid":…,"trait":{…},"dmgType":…,"skill":{full skill record},"talents":[…],"count":1,"sources":["talent","display"]}` | stats at the owner's phase/level (clamped to the token's max level) + golden module `tokenAttributeBlackboard` + the token's own talent additions to `deployLimit` / `deckStack` (blackboard `max_deploy_count` / `max_deck_stack_cnt` — the hidden "TOKEN数" talent of 麦哲伦's / 令's summons and 白铁's devices, 夜莺's 幻影; a module token part of the same talent replaces it: `tools/build-data.mjs tokenTalentDeckBonus`, 0.2.0 — no tokens.json summon has one; PRTS 幻影 备注 "最大可部署数量为3", the owners' "最多同时部署3个"); the owner's module parts flagged `isToken` upgrade the variant's `trait` (+`moduleDesc`) and `talents` (伺夜's wolves, 缪尔赛思's 流形 `scale` 1, 浊心斯卡蒂's 海嗣 30 s, “耀阳” `atk_scale` 1.15). `sources` ⊆ `talent`/`skill`/`display`: how the owner produces it (`display` only = listed on the character but unused by its default skill/talents, e.g. 迷迭香 S2, 凛御银灰 eagle1/3). `count` = copies from a talent `cnt` or the default skill's `cnt` when that skill overrides this token; `null` ⇒ use `deployLimit` |
| `variants[o].bySkill[i]` | `{"skill":{…},"count":1,"sources":["talent","display"]}` | owner loadout with the non-default skill index `i` (one entry per other selectable owner skill): the token skill of that slot (伺夜's wolves, 缪尔赛思's 流形, 凛御银灰's eagles…), the count and how the chess then produces it (`sources` may be `[]`: 风丸 S1 makes no 纸偶; 赫默 / 巫恋 S1 only `display` ⇒ no hand piece). The sim resolves them for an owner loadout: `simdata getToken(id, ownerChessId, loadout)` → `def.sources` / `def.count` | | `variants[o].bySkill[i]` | `{"skill":{…},"count":1,"sources":["talent","display"]}` | owner loadout with the non-default skill index `i` (one entry per other selectable owner skill): the token skill of that slot (伺夜's wolves, 缪尔赛思's 流形, 凛御银灰's eagles…), the count and how the chess then produces it (`sources` may be `[]`: 风丸 S1 makes no 纸偶; 赫默 / 巫恋 S1 only `display` ⇒ no hand piece). The sim resolves them for an owner loadout: `simdata getToken(id, ownerChessId, loadout)` → `def.sources` / `def.count` |
| `variants[o].byModule[m]` | `{"stats":{…},"immunities":{…},"trait":{…},"talents":[…]}` | golden owner with another module `m` or `'none'`: the token as that module makes it (module `tokenAttributeBlackboard`, `isToken` trait/talent parts) | | `variants[o].byModule[m]` | `{"stats":{…},"immunities":{…},"trait":{…},"talents":[…]}` | golden owner with another module `m` or `'none'`: the token as that module makes it (module `tokenAttributeBlackboard`, `isToken` trait/talent parts) |
| `assets` | `{"avatar":"token_10028_vigil_wolf","spine":"token_10028_vigil_wolf"}` | | | `assets` | `{"avatar":"token_10028_vigil_wolf","spine":"token_10028_vigil_wolf"}` | |
+4 -2
View File
@@ -95,8 +95,10 @@ export function diyGameData(gd, records) {
token: { value: (id) => gd.token(id) || tokenOf(id) }, token: { value: (id) => gd.token(id) || tokenOf(id) },
/** /**
* GameData.placeableTokens for a slotted slot: the summons its record lists (the pick's skill and talents) that are * GameData.placeableTokens for a slotted slot: the summons its record lists (the pick's skill and talents) that are
* placeable, by the variant of the owner form (`bySkill[skillIndex]` sources) — the deploy limit as the count, the * placeable, by the variant of the owner form (`bySkill[skillIndex]` sources) — the deploy limit as the count (PRTS
* active module's own when its variant has one (`byModule`: 望's TRP-X "可同时部署的陷阱数量提升", 6 → 7 棋子). * 卫戍协议/帮助 "根据召唤物部署数量上限(非初始持有量)"), the active module's own when its variant has one (`byModule`:
* 望's TRP-X "可同时部署的陷阱数量提升", 6 → 7 棋子; SUM-Y stage 2+ 4 drones / summons). The data's deploy limit holds the
* token's own talent additions (tools/build-data.mjs tokenTalentDeckBonus, 0.2.0): 麦哲伦 / 令 / 电弧 3, 白铁 2, 夜莺 3 幻影.
*/ */
placeableTokens: { placeableTokens: {
value: (chessId, loadout = null) => { value: (chessId, loadout = null) => {
+3 -1
View File
@@ -185,7 +185,9 @@ both tiers, both forms and both module stages. A 自选 piece has no 特质 and
the kit's business. Summons: `battle.tokenDef(tokenId, unit)` / `battle.spawnToken(unit, …)` resolve the variant of the the kit's business. Summons: `battle.tokenDef(tokenId, unit)` / `battle.spawnToken(unit, …)` resolve the variant of the
pick. A **placeable** summon (data/backups.json `tokens[id].placeable`, made by the pick's skill or a talent — the pick. A **placeable** summon (data/backups.json `tokens[id].placeable`, made by the pick's skill or a talent — the
variant's `sources`) is a hand piece like any operator's: when the 自选 piece is deployed in prep its player gets one variant's `sources`) is a hand piece like any operator's: when the 自选 piece is deployed in prep its player gets one
stack of the variant's `deployLimit` — the picked module's own when its variant has one (望's TRP-X: 7 棋子) — stack of the variant's `deployLimit` — the picked module's own when its variant has one (望's TRP-X: 7 棋子); it holds
the token's own talent additions (max_deploy_count / max_deck_stack_cnt, tools/build-data.mjs `tokenTalentDeckBonus`:
麦哲伦 / 令 3, SUM-Y stage 2+ 4, 白铁 2, 夜莺 3), so a deck reads `deployLimit` / `deckStack` as they are —
(server/match/player/diy.js `placeableTokens`, PlayerState `grantTokensFor`), places (server/match/player/diy.js `placeableTokens`, PlayerState `grantTokensFor`), places
it on a legal tile (the token's position / `ownerRange`), loses it with its owner (sold, merged, moved back), and the it on a legal tile (the token's position / `ownerRange`), loses it with its owner (sold, merged, moved back), and the
match hands every placed piece to the battle as a PlayerBattleInput token unit `{ kind: 'token', tokenId, ownerUid }` — match hands every placed piece to the battle as a PlayerBattleInput token unit `{ kind: 'token', tokenId, ownerUid }` —
+2 -2
View File
@@ -68,8 +68,8 @@
// - S3 铁钳号·原型机 (MANUAL, DEFAULT): 30 s, ATK +atk, ASPD +attack_speed; +1 stock at the start. // - S3 铁钳号·原型机 (MANUAL, DEFAULT): 30 s, ATK +atk, ASPD +attack_speed; +1 stock at the start.
// [ASSUMED] the stock refilled by +cnt on each deployment, capped at cnt + the CRA-X +1; a RechargeToken beyond the cap is lost; // [ASSUMED] the stock refilled by +cnt on each deployment, capped at cnt + the CRA-X +1; a RechargeToken beyond the cap is lost;
// the crab charges only from the attacks that select it (an ally's area skill that picks enemies does not see it); summons // the crab charges only from the attacks that select it (an ally's area skill that picks enemies does not see it); summons
// attack it too. Open (data, report): the hand count of his devices is the data's deployLimit 1 — tools/build-data.mjs takes // attack it too. The hand count of his devices is the data's deployLimit 2 (the attribute frame's 1 + the token's hidden talent
// the attribute frame's maxDeployCount and leaves out the token's hidden talent max_deploy_count +1 (E2 "最多可部署2个"). // max_deploy_count 1 — E2 "最多可部署2个"; tools/build-data.mjs tokenTalentDeckBonus, 0.2.0).
import { num, talentBb, talentGrid, traitBb, skillRec, up, giveSp } from '../shared/tier1.js'; import { num, talentBb, talentGrid, traitBb, skillRec, up, giveSp } from '../shared/tier1.js';
import { absoluteRangeKeys, sortEnemyTargets } from '../../../targeting.js'; import { absoluteRangeKeys, sortEnemyTargets } from '../../../targeting.js';
+5 -8
View File
@@ -23,8 +23,9 @@
// target her; her summons leave the field with her (shared/summoner.js). // target her; her summons leave the field with her (shared/summoner.js).
// - T1 挑灯问梦 "可以使用5个召唤物(最多同时部署3个),功能随技能选择而改变": the summon of her pick's skill (“清平” / “逍遥” / // - T1 挑灯问梦 "可以使用5个召唤物(最多同时部署3个),功能随技能选择而改变": the summon of her pick's skill (“清平” / “逍遥” /
// “弦惊” — the skills' overrideTokenKey) is the hand piece the player places; the deck of ../shared/summoner.js with charge = // “弦惊” — the skills' overrideTokenKey) is the hand piece the player places; the deck of ../shared/summoner.js with charge =
// cnt (5), cap = the summon deck (max_deck_stack_cnt 5, +3 with SUM-Y), at most 1 + max_deploy_count (2 ⇒ 3; SUM-Y stage 3: // cnt (5), cap = the summon record's deckStack (5: its hidden talent's max_deck_stack_cnt, tools/build-data.mjs
// 4) standing. Every summon holds 禁疗 (PRTS; [ASSUMED: given here — the data's `abnormal` lacks it]) and fights with its // tokenTalentDeckBonus; +3 with SUM-Y), at most its deployLimit standing (3 = 1 + the talent's max_deploy_count 2; SUM-Y
// stage 2+: 4) — the hand count too. Every summon holds 禁疗 (PRTS; [ASSUMED: given here — the data's `abnormal` lacks it]) and fights with its
// own data: “清平” blocks 1 and strikes in melee (physical) on its 1-1, “逍遥” shoots arts on its 3-1 (air units too), “弦惊” // own data: “清平” blocks 1 and strikes in melee (physical) on its 1-1, “逍遥” shoots arts on its 3-1 (air units too), “弦惊”
// blocks 2 and strikes every enemy it blocks (physical; its trait "攻击阻挡的所有敌人"). // blocks 2 and strikes every enemy it blocks (physical; its trait "攻击阻挡的所有敌人").
// - T2 随付笺咏醉屠苏 "召唤物被击倒/吸收/回收时令额外获得3点技力、攻击力+3%(攻击力加成最多叠加5层)": a summon knocked out, // - T2 随付笺咏醉屠苏 "召唤物被击倒/吸收/回收时令额外获得3点技力、攻击力+3%(攻击力加成最多叠加5层)": a summon knocked out,
@@ -74,7 +75,6 @@ const TAG_S3 = 'ling:s3';
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {}; const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
const skillOn = (u, id) => !!(u && u.skill && u.skill.active && u.skill.id === id); const skillOn = (u, id) => !!(u && u.skill && u.skill.active && u.skill.id === id);
const tokTalent = (def, key) => (def?.talents ?? []).find((t) => t && t.bb && t.bb[key] != null) ?? null;
const tileOf = (u) => u.tileR * COLS + u.tileC; const tileOf = (u) => u.tileR * COLS + u.tileC;
/** Her S1 / S3 acting on a summon (held from each skill start / end and each summon deployment). */ /** Her S1 / S3 acting on a summon (held from each skill start / end and each summon deployment). */
@@ -217,16 +217,13 @@ export default {
talents: [ talents: [
{ install(battle, unit) { // 挑灯问梦: the deck of her summon pieces and their kit { install(battle, unit) { // 挑灯问梦: the deck of her summon pieces and their kit
const cnt = Math.max(0, Math.floor(num(t0.cnt))); const cnt = Math.max(0, Math.floor(num(t0.cnt)));
const tdef = tokenId ? battle.tokenDef(tokenId, unit) : null;
const deckBase = num(tokTalent(tdef, 'max_deck_stack_cnt')?.bb?.max_deck_stack_cnt, cnt);
const extra = num(tokTalent(tdef, 'max_deploy_count')?.bb?.max_deploy_count, 0);
const sp = num(t1.sp), atk = num(t1.atk), stacks = Math.max(1, Math.floor(num(t1.max_stack_cnt, 1))); const sp = num(t1.sp), atk = num(t1.atk), stacks = Math.max(1, Math.floor(num(t1.max_stack_cnt, 1)));
summonDeck(battle, unit, { summonDeck(battle, unit, {
tokenIds: SOULS, tokenIds: SOULS,
charge: cnt, charge: cnt,
start: sumY, start: sumY,
cap: deckBase + num(tokenId ? tokenStat(battle, unit, tokenId, 'deckStack') : 0, 0), cap: num(tokenId ? tokenStat(battle, unit, tokenId, 'deckStack') : cnt, cnt),
maxDeployed: num(tokenId ? tokenStat(battle, unit, tokenId, 'deployLimit') : 1, 1) + extra, maxDeployed: num(tokenId ? tokenStat(battle, unit, tokenId, 'deployLimit') : 1, 1),
kit: (t) => soulKit(t, unit, opts), kit: (t) => soulKit(t, unit, opts),
// 随付笺咏醉屠苏: knocked out / absorbed / recalled ⇒ +sp SP and a stack of ATK +atk (not when they leave with her) // 随付笺咏醉屠苏: knocked out / absorbed / recalled ⇒ +sp SP and a stack of ATK +atk (not when they leave with her)
onLeave(t, kind) { onLeave(t, kind) {
+5 -7
View File
@@ -23,8 +23,9 @@
// PRTS "可对空"), blocks 1, ground enemies target her. Her drones leave the field with her (shared/summoner.js). // PRTS "可对空"), blocks 1, ground enemies target her. Her drones leave the field with her (shared/summoner.js).
// - T1 支援无人机·龙腾 "可以使用5个不阻挡敌人的无人机(最多同时部署3个),功能随技能选择而改变": the drone of her pick's skill // - T1 支援无人机·龙腾 "可以使用5个不阻挡敌人的无人机(最多同时部署3个),功能随技能选择而改变": the drone of her pick's skill
// (龙腾.F / .L / .A — the skills' overrideTokenKey) is the hand piece the player places; the deck of ../shared/summoner.js // (龙腾.F / .L / .A — the skills' overrideTokenKey) is the hand piece the player places; the deck of ../shared/summoner.js
// with charge = cnt (5) at each of her deployments, cap = the drone deck (its hidden talent's max_deck_stack_cnt 5, +the // with charge = cnt (5) at each of her deployments, cap = the drone record's deckStack (5: its hidden talent's
// SUM-Y deckStack 3), at most 1 + its max_deploy_count (2 ⇒ 3; SUM-Y stage 3: 3 ⇒ 4) standing. Every drone holds 禁疗 // max_deck_stack_cnt, tools/build-data.mjs tokenTalentDeckBonus; SUM-Y +3), at most its deployLimit standing (3 = 1 + the
// talent's max_deploy_count 2; SUM-Y stage 2+: 4) — the hand count too. Every drone holds 禁疗
// (PRTS; the data's `abnormal` lacks it [ASSUMED: given here]), blocks nothing, has no skill of its own here (its 回收 is // (PRTS; the data's `abnormal` lacks it [ASSUMED: given here]), blocks nothing, has no skill of its own here (its 回收 is
// her skills' end, below). 龙腾.F (ATK 0) makes no attack (the engine's rule for a 0-ATK summon; a commented-out PRTS // her skills' end, below). 龙腾.F (ATK 0) makes no attack (the engine's rule for a 0-ATK summon; a commented-out PRTS
// note "缴械,普通攻击不造成伤害" agrees [ASSUMED]); 龙腾.L strikes one ground enemy on its own tile for arts damage; 龙腾.A // note "缴械,普通攻击不造成伤害" agrees [ASSUMED]); 龙腾.L strikes one ground enemy on its own tile for arts damage; 龙腾.A
@@ -135,15 +136,12 @@ export default {
talents: [ talents: [
{ install(battle, unit) { // 支援无人机·龙腾: the deck of her drone pieces and their kit { install(battle, unit) { // 支援无人机·龙腾: the deck of her drone pieces and their kit
const cnt = Math.max(0, Math.floor(num(t0.cnt))); const cnt = Math.max(0, Math.floor(num(t0.cnt)));
const tdef = tokenId ? battle.tokenDef(tokenId, unit) : null;
const deckBase = num(tokTalent(tdef, 'max_deck_stack_cnt')?.bb?.max_deck_stack_cnt, cnt);
const extra = num(tokTalent(tdef, 'max_deploy_count')?.bb?.max_deploy_count, 0);
summonDeck(battle, unit, { summonDeck(battle, unit, {
tokenIds: DRONES, tokenIds: DRONES,
charge: cnt, charge: cnt,
start: sumY, start: sumY,
cap: deckBase + num(tokenId ? tokenStat(battle, unit, tokenId, 'deckStack') : 0, 0), cap: num(tokenId ? tokenStat(battle, unit, tokenId, 'deckStack') : cnt, cnt),
maxDeployed: num(tokenId ? tokenStat(battle, unit, tokenId, 'deployLimit') : 1, 1) + extra, maxDeployed: num(tokenId ? tokenStat(battle, unit, tokenId, 'deployLimit') : 1, 1),
kit: (t) => droneKit(t, unit, opts), kit: (t) => droneKit(t, unit, opts),
}); });
// her skills on the standing drones: at each start / end (a drone deployed meanwhile syncs itself) // her skills on the standing drones: at each start / end (a drone deployed meanwhile syncs itself)
+2 -4
View File
@@ -231,8 +231,6 @@ export default {
talents: [ talents: [
{ install(battle, unit) { // 卡带里的灵感: the deck of her summon pieces and their kit { install(battle, unit) { // 卡带里的灵感: the deck of her summon pieces and their kit
const cnt = Math.max(0, Math.floor(num(t0.cnt))); const cnt = Math.max(0, Math.floor(num(t0.cnt)));
const tdef = tokenId ? battle.tokenDef(tokenId, unit) : null;
const extra = num(tokTalent(tdef, 'max_deploy_count')?.bb?.max_deploy_count, 0);
const ratios = { atk: num(t1.atk), def: num(t1.def), hp: num(t1.max_hp) }; const ratios = { atk: num(t1.atk), def: num(t1.def), hp: num(t1.max_hp) };
const inspireOne = (s) => { const inspireOne = (s) => {
if (!up(unit)) return; if (!up(unit)) return;
@@ -250,8 +248,8 @@ export default {
summonDeck(battle, unit, { summonDeck(battle, unit, {
tokenIds: TOWERS, tokenIds: TOWERS,
charge: cnt, charge: cnt,
cap: num(tokenId ? tokenStat(battle, unit, tokenId, 'deckStack') : cnt, cnt) + num(tokTalent(tdef, 'max_deck_stack_cnt')?.bb?.max_deck_stack_cnt, 0), cap: num(tokenId ? tokenStat(battle, unit, tokenId, 'deckStack') : cnt, cnt),
maxDeployed: num(tokenId ? tokenStat(battle, unit, tokenId, 'deployLimit') : 1, 1) + extra, maxDeployed: num(tokenId ? tokenStat(battle, unit, tokenId, 'deployLimit') : 1, 1),
kit: (t) => towerKit(t, unit, opts), kit: (t) => towerKit(t, unit, opts),
}); });
// her skills on the standing summons at each start / end (a summon deployed meanwhile syncs itself); 鼓舞 again // her skills on the standing summons at each start / end (a summon deployed meanwhile syncs itself); 鼓舞 again
+3 -2
View File
@@ -5,8 +5,9 @@
// Sources: the client's battle data (charpack char_248_mgllan / char_2023_ling / char_4195_radian: Talents/1 // Sources: the client's battle data (charpack char_248_mgllan / char_2023_ling / char_4195_radian: Talents/1
// charge_token[born] — RechargeToken `cnt` at every deployment of the owner; CommonAbilities die_to_kill_token — KillTokens // charge_token[born] — RechargeToken `cnt` at every deployment of the owner; CommonAbilities die_to_kill_token — KillTokens
// when the owner leaves; the SUM-Y equips mgllan_equip_2_* / ling_equip_1_*: trigger_charge_token — RechargeToken the // when the owner leaves; the SUM-Y equips mgllan_equip_2_* / ling_equip_1_*: trigger_charge_token — RechargeToken the
// trait's `cnt` once, on the undeployed card (`_attachPassiveBuffsOnDummy`); the token decks: max_deck_stack_cnt / // trait's `cnt` once, on the undeployed card (`_attachPassiveBuffsOnDummy`); the token decks: deckStack / deployLimit of
// max_deploy_count of the tokens' hidden talent, deckStack / deployLimit of the token records); PRTS 分支特性信息 召唤师 // the token records, which carry the tokens' hidden talent max_deck_stack_cnt / max_deploy_count — tools/build-data.mjs
// tokenTalentDeckBonus); PRTS 分支特性信息 召唤师
// ("干员离场后,附属的召唤物随之消失"); PRTS 卫戍协议/帮助 §作战阶段 (placed summons: "所有手动部署的召唤物,无视所属干员的持有 // ("干员离场后,附属的召唤物随之消失"); PRTS 卫戍协议/帮助 §作战阶段 (placed summons: "所有手动部署的召唤物,无视所属干员的持有
// 状态,不消耗持有数量,作战开始时立即部署一次", "若战场区初始部署有召唤物,若召唤物在战斗期间退场,将在满足条件后立即原地再部署1个"). // 状态,不消耗持有数量,作战开始时立即部署一次", "若战场区初始部署有召唤物,若召唤物在战斗期间退场,将在满足条件后立即原地再部署1个").
// //
@@ -0,0 +1,96 @@
// test/content/feedback5-we2-summon-hand.test.js — 0.2.0 WE2 #21: the hand count of every 自选 summoner's placeable summon
// is its deploy limit (PRTS 卫戍协议/帮助 §战斗部署 "根据召唤物部署数量上限(非初始持有量),发送等量召唤物至手牌区"), and the
// data's deploy limit / holding carry the tokens' own talent additions (tools/build-data.mjs tokenTalentDeckBonus: the
// hidden "TOKEN数" talent max_deploy_count / max_deck_stack_cnt on top of the attribute frame). Official counts: the owners'
// talents (麦哲伦 / 令 / 电弧 "最多同时部署3个", SUM-Y stage 2+ "最多同时部署4个", 白铁 / 娜斯提 "最多可部署2个", 望 "可以使用6枚
// 棋子", TRP-X +1), PRTS 幻影 备注 "最大可部署数量为3" (夜莺).
// Run: node --test test/content/feedback5-we2-summon-hand.test.js
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { getData } from '../../server/data.js';
import { GameData } from '../../server/match/gamedata.js';
import { diyGameData } from '../../server/match/player/diy.js';
import { diyRecord, diySlot } from '../../shared/diy.js';
const load = (f) => JSON.parse(readFileSync(new URL(`../../data/${f}.json`, import.meta.url), 'utf8'));
const CHESS = load('chess');
const BACKUPS = load('backups');
const DATA = { chess: CHESS, backups: BACKUPS };
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
const STAGE = { 5: 1, 6: 3 };
const statusOf = (tier, elite) => (elite ? `2/60/7/${STAGE[tier]}` : '2/1/4/0');
/** The official hand count of `charId`'s summon under a module at its stage (null module = none). */
function official(charId, mod, stage) {
switch (charId) {
case 'char_248_mgllan': return mod === 'uniequip_003_mgllan' && stage >= 2 ? 4 : 3; // SUM-Y stage 2+: 最多同时部署4个
case 'char_2023_ling': return mod === 'uniequip_002_ling' && stage >= 2 ? 4 : 3; // SUM-Y stage 2+
case 'char_4195_radian': return 3;
case 'char_4072_ironmn': return 2;
case 'char_4212_nasti': return 2;
case 'char_179_cgbird': return 3; // PRTS 幻影 备注
case 'char_2027_wang': return mod === 'uniequip_002_wang' ? 7 : 6; // TRP-X +1
case 'char_4048_doroth': return 9; // 10 / 13, the hand's 9 cap
default: return 1;
}
}
const gd = new GameData(getData({ log: { warn() {}, error() {}, info() {} } }), 'mode_multi_hard');
test('every 自选 summoner: the hand count of each placeable summon in every form, skill and module is the official one', () => {
const owners = new Map(); // charId → placeable token ids it makes
for (const [tid, t] of Object.entries(BACKUPS.tokens)) {
if (t.kind !== 'summon' || t.placeable !== true) continue;
for (const key of Object.keys(t.variants)) {
const cid = key.split('@')[0];
if (!owners.has(cid)) owners.set(cid, new Set());
owners.get(cid).add(tid);
}
}
assert.ok(['char_248_mgllan', 'char_2023_ling', 'char_4195_radian', 'char_4072_ironmn', 'char_179_cgbird', 'char_2027_wang'].every((c) => owners.has(c)));
let checked = 0;
for (const [charId] of owners) {
if (!BACKUPS.diy.ownedPool.includes(charId)) continue;
for (const tier of [5, 6]) {
for (const elite of [false, true]) {
const form = BACKUPS.units[charId].forms[statusOf(tier, elite)];
for (const mod of elite ? [null, ...(form.modules ?? []).map((m) => m.uniEquipId)] : [null]) {
for (const s of form.skills) {
const pick = { charId, skillIndex: s.index, uniEquipId: mod };
const slot = diySlot(SLOT[tier], DATA);
const id = elite ? slot.goldenId : slot.baseId;
const rec = diyRecord(SLOT[tier], pick, { elite, data: DATA });
assert.ok(rec, `${charId} T${tier} ${elite ? 'elite' : 'normal'} ${mod} S${s.index + 1}`);
const view = diyGameData(gd, new Map([[id, rec]]));
for (const { tokenId, count } of view.placeableTokens(id, { skillIndex: s.index, moduleId: mod })) {
assert.equal(count, official(charId, mod, elite ? STAGE[tier] : 0), `${charId} T${tier} ${elite ? 'elite' : 'normal'} ${mod ?? 'none'} S${s.index + 1}: ${tokenId}`);
checked++;
}
}
}
}
}
}
assert.ok(checked > 200, `${checked} hand counts checked`);
});
test('the data: deploy limit / holding = the attribute frame + the token talents\' max_deploy_count / max_deck_stack_cnt (a module token part of the same talent replaces it)', () => {
const v = (tid, owner) => BACKUPS.tokens[tid].variants[owner];
// 麦哲伦: 1 + 2 standing, 0 + 5 held; SUM-Y +3 held, stage 3 its token part max_deploy_count 3 ⇒ 4
const d1 = 'token_10005_mgllan_drone1';
assert.deepEqual([v(d1, 'char_248_mgllan@2/1/4/0').stats.deployLimit, v(d1, 'char_248_mgllan@2/1/4/0').stats.deckStack], [3, 5]);
assert.deepEqual([v(d1, 'char_248_mgllan@2/60/7/1').byModule.uniequip_003_mgllan.stats.deployLimit, v(d1, 'char_248_mgllan@2/60/7/1').byModule.uniequip_003_mgllan.stats.deckStack], [3, 8]);
assert.deepEqual([v(d1, 'char_248_mgllan@2/60/7/3').byModule.uniequip_003_mgllan.stats.deployLimit, v(d1, 'char_248_mgllan@2/60/7/3').byModule.uniequip_003_mgllan.stats.deckStack], [4, 8]);
// 夜莺's 幻影: its visible talent's max_deploy_count 2 (RIN-Y stage 3 restates the same talent: not counted twice)
const ph = 'token_10003_cgbird_bird';
assert.equal(v(ph, 'char_179_cgbird@2/1/4/0').stats.deployLimit, 3);
assert.equal(v(ph, 'char_179_cgbird@2/60/7/3').byModule.uniequip_003_cgbird.stats.deployLimit, 3);
// 望's 棋子: the rank-2 talent (+1) is a potential talent — potential 0 keeps 6
assert.equal(v('token_10064_wang_stone1', 'char_2027_wang@2/1/4/0').stats.deployLimit, 6);
// the chess summons of tokens.json have no such talent: 凯瑟琳's 爬行号 2 of 3, everyone else 1
const tokens = load('tokens');
assert.equal(tokens.token_10041_cathy_catsld.variants.chess_char_4_11_a.stats.deployLimit, 2);
assert.equal(tokens.token_10000_silent_healrb.variants.chess_char_2_02_a.stats.deployLimit, 1);
});
+4 -3
View File
@@ -105,12 +105,13 @@ test('白铁 in every 自选 form: his kit (all three skills authored), stats +
{ ok: true, picks: { [SLOT[6]]: { charId: IRON, skillIndex: 2, uniEquipId: Y } } }); { ok: true, picks: { [SLOT[6]]: { charId: IRON, skillIndex: 2, uniEquipId: Y } } });
}); });
test('<支援装置> hand pieces: the picked skill\'s device only (variant sources), deploy limit 1 in the data (official 1 + max_deploy_count 1 = 2: reported), the module costs / redeploy times', () => { test('<支援装置> hand pieces: the picked skill\'s device only (variant sources), deploy limit 2 in the data (1 + the hidden max_deploy_count 1: E2 "最多可部署2个"), the module costs / redeploy times', () => {
for (const [skill, id] of TOK.entries()) { for (const [skill, id] of TOK.entries()) {
for (const s of [0, 1, 2]) assert.equal(tokOf(id, 6, true, s, null).sources.includes('skill'), s === skill, `${id} under S${s + 1}`); for (const s of [0, 1, 2]) assert.equal(tokOf(id, 6, true, s, null).sources.includes('skill'), s === skill, `${id} under S${s + 1}`);
const v = tokOf(id, 6, true, skill, null); const v = tokOf(id, 6, true, skill, null);
assert.equal(v.stats.deployLimit, 1, `${id}: the data's deploy limit`); assert.equal(v.talents.find((t) => t.bb.max_deploy_count != null).bb.max_deploy_count, 1, `${id}: the hidden max_deploy_count`);
assert.equal(v.talents.find((t) => t.bb.max_deploy_count != null).bb.max_deploy_count, 1, `${id}: the hidden max_deploy_count (E2 "最多可部署2个")`); assert.equal(v.stats.deployLimit, 2, `${id}: the data's deploy limit (tools/build-data.mjs tokenTalentDeckBonus)`);
assert.deepEqual([v.stats.deckStack, tokOf(id, 6, true, skill, X).stats.deckStack], [3, 4], `${id}: the holding 3 (CRA-X +1)`);
} }
// CRA-X 部署费用 −1 / −1 / −4, CRA-Y 再部署时间 −5 / −5 / −10 (PRTS module page) // CRA-X 部署费用 −1 / −1 / −4, CRA-Y 再部署时间 −5 / −5 / −10 (PRTS module page)
const cost = (mod) => TOK.map((id, s) => tokOf(id, 6, true, s, mod).stats.cost); const cost = (mod) => TOK.map((id, s) => tokOf(id, 6, true, s, mod).stats.cost);
+38 -1
View File
@@ -1368,8 +1368,44 @@ function classifyToken(char, traitText) {
return c; return c;
} }
/** The blackboard keys of a summon's own talents that add to its deploy limit / holding (statsFrom: deployLimit / deckStack). */
const TOKEN_DECK_KEYS = Object.freeze(['max_deploy_count', 'max_deck_stack_cnt']);
/** /**
* Build one owner-specific variant of a token / map character at (phase, level, skill level). * A summon's talent additions to its deploy limit and holding: the blackboard keys `max_deploy_count` /
* `max_deck_stack_cnt` of the token's own talents — the hidden "TOKEN数+N" talent of 麦哲伦's drones and 令's summons
* (E2: max_deck_stack_cnt 5, max_deploy_count 2), of 白铁's devices (E2: 3, 1), the visible 转瞬即逝的幻影 of 夜莺's
* 幻影 (max_deploy_count 2) — which the client adds to the attribute frame's maxDeployCount / maxDeckStackCnt (statsFrom
* reads the frame only): PRTS 幻影 备注 "每次部署夜莺时获得2个可部署的幻影,最大可部署数量为3" (1 + 2); the owners' own
* talents "最多同时部署3个" (麦哲伦 / 令: 1 + 2), "最多可部署2个" (白铁: 1 + 1); SUM-Y stage 2+ "最多同时部署4个" — its token part
* (the same talent, max_deploy_count 3) replaces the talent's values. The candidates as everywhere: the token's phase /
* level at potential 0 (望's rank-2 棋子 +1 stays out); a module's token part (`moduleTokenParts`, unlocked at the owner's
* phase / level as moduleTalentChanges) replaces the base candidate of the same prefabKey key by key (mergeTalentChanges:
* what it does not restate stays) — matched by prefabKey, not talentIndex (夜莺's RIN-Y stage 3 part names talentIndex 0
* for her 幻影's talent 1). The hand count of a placeable summon is this deploy limit (PRTS 卫戍协议/帮助 "根据召唤物部署数量
* 上限(非初始持有量),发送等量召唤物至手牌区"; gamedata / player/diy.js placeableTokens).
* @returns {Record<string, number>} the non-zero additions by blackboard key
*/
function tokenTalentDeckBonus(char, phase, level, moduleTokenParts, modPhase, modLevel) {
const byKey = new Map();
const take = (c, merge) => {
if (!c) return;
const key = String(c.prefabKey ?? '');
const bb = {};
for (const b of c.blackboard || []) if (TOKEN_DECK_KEYS.includes(b.key) && typeof b.value === 'number') bb[b.key] = b.value;
byKey.set(key, merge ? { ...(byKey.get(key) || {}), ...bb } : bb);
};
for (const t of char.talents || []) take(bestCandidate(t?.candidates, phase, level), false);
for (const part of moduleTokenParts || []) take(bestCandidate(part?.addOrOverrideTalentDataBundle?.candidates, modPhase, modLevel), true);
const out = {};
for (const bb of byKey.values()) for (const [k, v] of Object.entries(bb)) out[k] = (out[k] || 0) + v;
for (const k of Object.keys(out)) if (!out[k]) delete out[k];
return out;
}
/**
* Build one owner-specific variant of a token / map character at (phase, level, skill level). Its stats add the module's
* token attributes and the summon's talent additions to its deploy limit / holding (tokenTalentDeckBonus).
* @returns {object} * @returns {object}
*/ */
function tokenVariant(ctx, tokenId, char, { phase, level, skillIndex, skillLevel, modulePhase, moduleTokenParts, label }) { function tokenVariant(ctx, tokenId, char, { phase, level, skillIndex, skillLevel, modulePhase, moduleTokenParts, label }) {
@@ -1378,6 +1414,7 @@ function tokenVariant(ctx, tokenId, char, { phase, level, skillIndex, skillLevel
const bonus = {}; const bonus = {};
const tokBonus = modulePhase?.tokenAttributeBlackboard?.[tokenId]; const tokBonus = modulePhase?.tokenAttributeBlackboard?.[tokenId];
for (const b of Array.isArray(tokBonus) ? tokBonus : []) bonus[b.key] = (bonus[b.key] || 0) + b.value; for (const b of Array.isArray(tokBonus) ? tokBonus : []) bonus[b.key] = (bonus[b.key] || 0) + b.value;
for (const [k, v] of Object.entries(tokenTalentDeckBonus(char, ph, lv, moduleTokenParts, phase, level))) bonus[k] = (bonus[k] || 0) + v;
const attrs = interpolateAttrs(char, ph, lv); const attrs = interpolateAttrs(char, ph, lv);
const tc = bestCandidate(char.trait?.candidates, ph, lv); const tc = bestCandidate(char.trait?.candidates, ph, lv);
const tbb = flattenBB(tc?.blackboard); const tbb = flattenBB(tc?.blackboard);