feedback1 gaps: bands.json bondIds is built once and read by the bot
The band -> bond derivation moves out of GameData into shared/bandBonds.js, which tools/build-data.mjs runs per band to write bands.json bondIds (regenerated with --offline; 9 bands are tied, the build checks the ids). GameData.bandBondIds reads the field, so the server and the client share one source. DATA.md documents the field; the test checks the helper on synthetic bands and that the data matches it.
This commit is contained in:
1 parent
3bd855982e
commit
1f29b8a3d0
7 files changed
+86
-46
No files matched your search
+1
-1
@@ -1 +1 @@
|
||||
{"band_bldsk":{"bandId":"band_bldsk","sortId":1,"name":"华法琳","iconId":"icon_bldsk","modeTypeList":["LOCAL","SINGLE","MULTI"],"totalHp":28,"effectId":"aceffect_band_21","effectName":"重点监护","desc":"【重点监护】开始作战时,使场上每个不同等阶的1个随机我方干员的所属盟约层数+2","descRaw":"【重点监护】开始作战时,使场上每个不同等阶的1个随机我方干员的所属盟约层数+2","buffs":[{"key":"prep_finish_char_bond_add_layer","bb":{"layer":2},"bbStr":{}}],"params":{"layer":2},"victorCount":3,"rewardModulus":1,"unlockDesc":null},"band_amiya":{"bandId":"band_amiya","sortId":2,"name":"阿米娅","iconId":"icon_amiya","modeTypeList":["LOCAL","SINGLE","MULTI"],"totalHp":25,"effectId":"aceffect_band_22","effectName":"众志合一","desc":"【众志合一】场上存在3、4或5种以上激活盟约时,所有干员攻击力、生命值分别+20/30/40%","descRaw":"【众志合一】场上存在3、4或5种以上激活盟约时,所有干员攻击力、生命值分别+20/30/40%","buffs":[{"key":"env_gbuff_new_with_verify","bb":{"atk_1":0.2,"atk_2":0.3,"atk_3":0.4,"max_hp_1":0.2,"max_hp_2":0.3,"max_hp_3":0.4,"value_1":3,"value_2":4,"value_3":5},"bbStr":{"key":"act1autochess_band2_buff"}}],"params":{"atk_1":0.2,"atk_2":0.3,"atk_3":0.4,"max_hp_1":0.2,"max_hp_2":0.3,"max_hp_3":0.4,"value_1":3,"value_2":4,"value_3":5,"key":"act1autochess_band2_buff"},"victorCount":3,"rewardModulus":1,"unlockDesc":null},"band_duyaoy":{"bandId":"band_duyaoy","sortId":3,"name":"杜遥夜","iconId":"icon_duyaoy","modeTypeList":["LOCAL","SINGLE","MULTI"],"totalHp":29,"effectId":"aceffect_band_28","effectName":"广交豪杰","desc":"【广交豪杰】每回合前2次主动刷新为特殊刷新:优先刷新出1名<炎>干员\n在<炎>部分干员缺席时体验可能不完整","descRaw":"【广交豪杰】每回合前2次主动刷新为特殊刷新:优先刷新出1名<炎>干员\n<@ba.vdown>在<炎>部分干员缺席时体验可能不完整</>","buffs":[{"key":"band_first_self_refresh_present_char","bb":{"count":1},"bbStr":{"bond":"yanShip"}}],"params":{"count":1,"bond":"yanShip"},"victorCount":3,"rewardModulus":1,"unlockDesc":null},"band_sarkazb":{"bandId":"band_sarkazb","sortId":4,"name":"歌利亚","iconId":"icon_sarkazb","modeTypeList":["LOCAL","SINGLE","MULTI"],"totalHp":45,"effectId":"aceffect_band_44","effectName":"坚不可摧","desc":"【坚不可摧】初始目标生命值为45点","descRaw":"【坚不可摧】初始目标生命值为45点","buffs":[],"params":{},"victorCount":3,"rewardModulus":1,"unlockDesc":null},"band_orchid":{"bandId":"band_orchid","sortId":5,"name":"梓兰","iconId":"icon_orchid","modeTypeList":["SINGLE","MULTI"],"totalHp":30,"effectId":"aceffect_band_23","effectName":"猎头顾问","desc":"【猎头顾问】每次主动刷新均变为特殊刷新:必定刷新2名同名干员,并冻结其中一名;第10回合获得1个<寻呼模块>\n<寻呼模块>:装备时销毁,立即在调度中心中刷新三名与该干员盟约相同的干员,免费获得其中一名!(不超过当前调度中心等级)","descRaw":"【猎头顾问】每次主动刷新均变为特殊刷新:必定刷新2名同名干员,并冻结其中一名;第10回合获得1个<寻呼模块>\n<@ba.acrem><寻呼模块>:装备时销毁,立即在调度中心中刷新三名与该干员盟约相同的干员,免费获得其中一名!(不超过当前调度中心等级)</>","buffs":[{"key":"band_shop_refresh_copy_max_lv_char","bb":{"count":1},"bbStr":{}},{"key":"preparation_start_gain_chess_from_round","bb":{"round":10,"count":1},"bbStr":{"type":"equip","chess":"chess_item_4_01_e_a"}}],"params":{"count":1,"round":10,"type":"equip","chess":"chess_item_4_01_e_a"},"victorCount":3,"rewardModulus":1,"unlockDesc":"在【标准模拟】中通过第9回合后解锁"},"band_justin":{"bandId":"band_justin","sortId":6,"name":"小贾斯汀","iconId":"icon_justin","modeTypeList":["SINGLE","MULTI"],"totalHp":23,"effectId":"aceffect_band_47","effectName":"私募基金","desc":"【私募基金】在第1、4、7、10回合进入休整期时获得1个<骑士储蓄罐>\n<骑士储蓄罐>:装备时随机获得1~6资金并销毁","descRaw":"【私募基金】在第1、4、7、10回合进入休整期时获得1个<骑士储蓄罐>\n<@ba.acrem><骑士储蓄罐>:装备时随机获得1~6资金并销毁</>","buffs":[{"key":"preparation_start_gain_chess_from_round","bb":{"round":1,"count":1},"bbStr":{"type":"equip","chess":"chess_item_3_12_e_a"}},{"key":"preparation_start_gain_chess_from_round","bb":{"round":4,"count":1},"bbStr":{"type":"equip","chess":"chess_item_3_12_e_a"}},{"key":"preparation_start_gain_chess_from_round","bb":{"round":7,"count":1},"bbStr":{"type":"equip","chess":"chess_item_3_12_e_a"}},{"key":"preparation_start_gain_chess_from_round","bb":{"round":10,"count":1},"bbStr":{"type":"equip","chess":"chess_item_3_12_e_a"}}],"params":{"round":1,"count":1,"type":"equip","chLine truncated
|
||||
{"band_bldsk":{"bandId":"band_bldsk","sortId":1,"name":"华法琳","iconId":"icon_bldsk","modeTypeList":["LOCAL","SINGLE","MULTI"],"totalHp":28,"effectId":"aceffect_band_21","effectName":"重点监护","desc":"【重点监护】开始作战时,使场上每个不同等阶的1个随机我方干员的所属盟约层数+2","descRaw":"【重点监护】开始作战时,使场上每个不同等阶的1个随机我方干员的所属盟约层数+2","buffs":[{"key":"prep_finish_char_bond_add_layer","bb":{"layer":2},"bbStr":{}}],"params":{"layer":2},"victorCount":3,"rewardModulus":1,"unlockDesc":null,"bondIds":[]},"band_amiya":{"bandId":"band_amiya","sortId":2,"name":"阿米娅","iconId":"icon_amiya","modeTypeList":["LOCAL","SINGLE","MULTI"],"totalHp":25,"effectId":"aceffect_band_22","effectName":"众志合一","desc":"【众志合一】场上存在3、4或5种以上激活盟约时,所有干员攻击力、生命值分别+20/30/40%","descRaw":"【众志合一】场上存在3、4或5种以上激活盟约时,所有干员攻击力、生命值分别+20/30/40%","buffs":[{"key":"env_gbuff_new_with_verify","bb":{"atk_1":0.2,"atk_2":0.3,"atk_3":0.4,"max_hp_1":0.2,"max_hp_2":0.3,"max_hp_3":0.4,"value_1":3,"value_2":4,"value_3":5},"bbStr":{"key":"act1autochess_band2_buff"}}],"params":{"atk_1":0.2,"atk_2":0.3,"atk_3":0.4,"max_hp_1":0.2,"max_hp_2":0.3,"max_hp_3":0.4,"value_1":3,"value_2":4,"value_3":5,"key":"act1autochess_band2_buff"},"victorCount":3,"rewardModulus":1,"unlockDesc":null,"bondIds":[]},"band_duyaoy":{"bandId":"band_duyaoy","sortId":3,"name":"杜遥夜","iconId":"icon_duyaoy","modeTypeList":["LOCAL","SINGLE","MULTI"],"totalHp":29,"effectId":"aceffect_band_28","effectName":"广交豪杰","desc":"【广交豪杰】每回合前2次主动刷新为特殊刷新:优先刷新出1名<炎>干员\n在<炎>部分干员缺席时体验可能不完整","descRaw":"【广交豪杰】每回合前2次主动刷新为特殊刷新:优先刷新出1名<炎>干员\n<@ba.vdown>在<炎>部分干员缺席时体验可能不完整</>","buffs":[{"key":"band_first_self_refresh_present_char","bb":{"count":1},"bbStr":{"bond":"yanShip"}}],"params":{"count":1,"bond":"yanShip"},"victorCount":3,"rewardModulus":1,"unlockDesc":null,"bondIds":["yanShip"]},"band_sarkazb":{"bandId":"band_sarkazb","sortId":4,"name":"歌利亚","iconId":"icon_sarkazb","modeTypeList":["LOCAL","SINGLE","MULTI"],"totalHp":45,"effectId":"aceffect_band_44","effectName":"坚不可摧","desc":"【坚不可摧】初始目标生命值为45点","descRaw":"【坚不可摧】初始目标生命值为45点","buffs":[],"params":{},"victorCount":3,"rewardModulus":1,"unlockDesc":null,"bondIds":[]},"band_orchid":{"bandId":"band_orchid","sortId":5,"name":"梓兰","iconId":"icon_orchid","modeTypeList":["SINGLE","MULTI"],"totalHp":30,"effectId":"aceffect_band_23","effectName":"猎头顾问","desc":"【猎头顾问】每次主动刷新均变为特殊刷新:必定刷新2名同名干员,并冻结其中一名;第10回合获得1个<寻呼模块>\n<寻呼模块>:装备时销毁,立即在调度中心中刷新三名与该干员盟约相同的干员,免费获得其中一名!(不超过当前调度中心等级)","descRaw":"【猎头顾问】每次主动刷新均变为特殊刷新:必定刷新2名同名干员,并冻结其中一名;第10回合获得1个<寻呼模块>\n<@ba.acrem><寻呼模块>:装备时销毁,立即在调度中心中刷新三名与该干员盟约相同的干员,免费获得其中一名!(不超过当前调度中心等级)</>","buffs":[{"key":"band_shop_refresh_copy_max_lv_char","bb":{"count":1},"bbStr":{}},{"key":"preparation_start_gain_chess_from_round","bb":{"round":10,"count":1},"bbStr":{"type":"equip","chess":"chess_item_4_01_e_a"}}],"params":{"count":1,"round":10,"type":"equip","chess":"chess_item_4_01_e_a"},"victorCount":3,"rewardModulus":1,"unlockDesc":"在【标准模拟】中通过第9回合后解锁","bondIds":[]},"band_justin":{"bandId":"band_justin","sortId":6,"name":"小贾斯汀","iconId":"icon_justin","modeTypeList":["SINGLE","MULTI"],"totalHp":23,"effectId":"aceffect_band_47","effectName":"私募基金","desc":"【私募基金】在第1、4、7、10回合进入休整期时获得1个<骑士储蓄罐>\n<骑士储蓄罐>:装备时随机获得1~6资金并销毁","descRaw":"【私募基金】在第1、4、7、10回合进入休整期时获得1个<骑士储蓄罐>\n<@ba.acrem><骑士储蓄罐>:装备时随机获得1~6资金并销毁</>","buffs":[{"key":"preparation_start_gain_chess_from_round","bb":{"round":1,"count":1},"bbStr":{"type":"equip","chess":"chess_item_3_12_e_a"}},{"key":"preparation_start_gain_chess_from_round","bb":{"round":4,"count":1},"bbStr":{"type":"equip","chess":"chess_item_3_12_e_a"}},{"key":"preparation_start_gain_chess_from_round","bb":{"round":7,"count":1},"bbStr":{"type":"equip","chess":"chess_item_3_12_e_a"}},{"key":"preparation_start_gain_chess_from_round","bb":{"round":10,"count":1},"bbStr":{"type":"equip","chess"Line truncated
|
||||
@@ -302,6 +302,7 @@ everything needed to resolve a unit for `(chessId, skillIndex, moduleId)` (`simd
|
||||
| `effectId`, `effectName`, `desc`, `descRaw` | `"aceffect_band_21"`, `"重点监护"`, `"【重点监护】开始作战时…"` | |
|
||||
| `buffs[]`, `params` | `[{"key":"prep_finish_char_bond_add_layer","bb":{"layer":2},"bbStr":{}}]` | |
|
||||
| `victorCount`, `rewardModulus`, `unlockDesc` | `3`, `1`, `null` | |
|
||||
| `bondIds` | `["lateranoShip"]` (潘格尼尼) / `[]` | the bonds the strategy is built around (`shared/bandBonds.js`: the bond names in <…> of its text — the official note "在<X>部分干员缺席时体验可能不完整" among them — and the bond ids / bond pools of its blackboards, `pool` → choices.json `pools[pool].bond`); 9 bands are tied. The bot never picks, and the strategy draft marks 本局禁用, a band whose bond the mode switches off (DESIGN §21.26) |
|
||||
|
||||
## 7. `effects.json` — `{ [effectId]: Effect }` (361: every effect of the season)
|
||||
|
||||
|
||||
+2
-2
@@ -60,8 +60,8 @@
|
||||
// for it, makeHandRoom, instead of being destroyed), keep one hand slot free (freeHandSlot: a kept bounty Art goes
|
||||
// before a chess on a bot's own seat, never on a human's seat under AI 托管), then Ready.
|
||||
// Strategy (botPickBand): weighted by starting LP among the offered bands; never one whose mechanic rides on a bond the
|
||||
// mode switches off (gd.bandBondIds: the bond its text names in <…> or its effect's blackboards name — 标准's 潘格尼尼,
|
||||
// 克莱门莎, 玛恩纳; DESIGN §21.26); alone, 老鲤's withheld first-round funds only rarely (× 0.02).
|
||||
// mode switches off (gd.bandBondIds = bands.json bondIds: the bond its text names in <…> or its blackboards name — 标准's
|
||||
// 潘格尼尼, 克莱门莎, 玛恩纳; DESIGN §21.26); alone, 老鲤's withheld first-round funds only rarely (× 0.02).
|
||||
// 机变 (botPickCard): a bounty by its expected payout minus the expected LP lost — bountyKillChance runs the exposure
|
||||
// model for that one enemy against the own board; a card the board is unlikely to beat wins only when nothing better
|
||||
// is offered or it pays much more —; tactic cards by what they act on (a 盟誓 / 驰援 card on the own bonds, 升华 …);
|
||||
|
||||
@@ -90,8 +90,7 @@ export class GameData {
|
||||
for (const k of Object.keys(this.shopItemsByTier)) this.shopItemsByTier[k].sort();
|
||||
this.bondIds = Object.keys(this._bonds).sort((a, b) => (numOr(this._bonds[a].identifier, 99) - numOr(this._bonds[b].identifier, 99)) || (a < b ? -1 : 1));
|
||||
this.modeInactiveBonds = new Set(Array.isArray(this.mode.inactiveBondIds) ? this.mode.inactiveBondIds : []);
|
||||
/** bond name → id (bandBondIds) and its memo */
|
||||
this._bondByName = new Map(Object.entries(this._bonds).filter(([, r]) => r && typeof r.name === 'string').map(([id, r]) => [r.name, id]));
|
||||
/** bandBondIds memo */
|
||||
this._bandBonds = new Map();
|
||||
this.inactiveEnemies = new Set(Array.isArray(this.mode.inactiveEnemyKeys) ? this.mode.inactiveEnemyKeys : []);
|
||||
/** data/tuning.json (titles only, see the header) */
|
||||
@@ -462,41 +461,19 @@ export class GameData {
|
||||
}
|
||||
|
||||
/**
|
||||
* The bonds a strategy's mechanic is built around, read from its own data (DESIGN §21.26): the bond names its text puts
|
||||
* in <…> — the official note "在<X>部分干员缺席时体验可能不完整" names that bond too — and the bond ids and bond pools its
|
||||
* effect's blackboards name (bb / bbStr values: 克莱门莎's `bond_id` egirShip, 玛恩纳's `bond` kazimierzShip, 潘格尼尼's
|
||||
* `pool` pool_char_later → choices.json pools[pool].bond lateranoShip). Names map to ids through bonds.json; a bracketed
|
||||
* name that is no bond (<寻呼模块>, <画卷>, <鸭爵> …) and a pool without a bond add nothing. Bond ids in data order,
|
||||
* [] for an unknown band or one tied to no bond (华法琳, 阿米娅 …). The bot never picks a strategy tied to a bond the mode
|
||||
* switches off (bot.js botPickBand: 标准's 潘格尼尼 / 克莱门莎 / 玛恩纳).
|
||||
* The bonds a strategy's mechanic is built around (DESIGN §21.26): bands.json `bondIds`, written at build time by
|
||||
* shared/bandBonds.js from the band's own text and blackboards (潘格尼尼 → 拉特兰, 克莱门莎 → 阿戈尔, 玛恩纳 → 卡西米尔 …) —
|
||||
* the field the strategy draft's 本局禁用 mark reads too. Known bond ids in data order; [] for an unknown band, one tied to
|
||||
* no bond (华法琳, 阿米娅 …) or data without the field. The bot never picks a strategy tied to a bond the mode switches
|
||||
* off (bot.js botPickBand).
|
||||
* @param {string} bandId
|
||||
* @returns {string[]}
|
||||
*/
|
||||
bandBondIds(bandId) {
|
||||
if (this._bandBonds.has(bandId)) return this._bandBonds.get(bandId);
|
||||
const b = this.band(bandId);
|
||||
const found = new Set();
|
||||
if (b) {
|
||||
for (const [, name] of String(b.desc || '').matchAll(/<([^<>]+)>/g)) {
|
||||
const id = this._bondByName.get(name.trim());
|
||||
if (id) found.add(id);
|
||||
}
|
||||
const pools = this.choices.pools && typeof this.choices.pools === 'object' ? this.choices.pools : {};
|
||||
for (const buff of Array.isArray(b.buffs) ? b.buffs : []) {
|
||||
for (const board of [buff && buff.bb, buff && buff.bbStr]) {
|
||||
if (!board || typeof board !== 'object') continue;
|
||||
for (const v of Object.values(board)) {
|
||||
if (typeof v !== 'string') continue;
|
||||
for (const part of v.split(',').map((s) => s.trim())) {
|
||||
if (this.bond(part)) found.add(part);
|
||||
const pool = own(pools, part);
|
||||
if (pool && typeof pool.bond === 'string' && this.bond(pool.bond)) found.add(pool.bond);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const out = Object.freeze(this.bondIds.filter((id) => found.has(id)));
|
||||
const listed = this.band(bandId)?.bondIds;
|
||||
const set = new Set(Array.isArray(listed) ? listed : []);
|
||||
const out = Object.freeze(this.bondIds.filter((id) => set.has(id)));
|
||||
this._bandBonds.set(bandId, out);
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
// shared/bandBonds.js — the bonds a strategy (band) is built around, read from the band's own data (DESIGN §21.26).
|
||||
// Pure (no Node APIs). tools/build-data.mjs runs it once per band and writes the result as data/bands.json `bondIds` —
|
||||
// the one field both sides read: the server (match/gamedata.js GameData.bandBondIds → bot.js botPickBand never takes a
|
||||
// band whose bond the mode switches off) and the client (screens/bandDraft.js marks such a band 本局禁用, gameLogic
|
||||
// bandOffBonds). A band is tied to:
|
||||
// * the bonds its text names in <…> — the official note "在<X>部分干员缺席时体验可能不完整" names that bond too; a
|
||||
// bracketed name that is no bond (<寻呼模块>, <画卷>, <鸭爵> …) adds nothing;
|
||||
// * the bond ids and bond pools its effect's blackboards name (bb / bbStr string values, comma lists included):
|
||||
// 克莱门莎's `bond_id` egirShip, 玛恩纳's `bond` kazimierzShip, 潘格尼尼's `pool` pool_char_later → choices.json
|
||||
// `pools[pool].bond` lateranoShip; a pool without a bond adds nothing.
|
||||
|
||||
const isObj = (v) => !!v && typeof v === 'object';
|
||||
|
||||
/**
|
||||
* Bond ids a band is tied to, in the order of `bonds`.
|
||||
* @param {{ desc?: string, buffs?: Array<{ bb?: object, bbStr?: object }> }|null|undefined} band bands.json record
|
||||
* @param {{ bonds: Record<string, { name?: string }>|Array<{ bondId: string, name?: string }>,
|
||||
* pools?: Record<string, { bond?: string }>|null }} ctx bonds.json (map or list) and choices.json `pools`
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export function bandBondIds(band, { bonds, pools = null } = {}) {
|
||||
const list = Array.isArray(bonds) ? bonds.filter(isObj).map((b) => [b.bondId, b]) : Object.entries(isObj(bonds) ? bonds : {});
|
||||
const ids = list.map(([id]) => id).filter((id) => typeof id === 'string');
|
||||
if (!isObj(band) || !ids.length) return [];
|
||||
const known = new Set(ids);
|
||||
const byName = new Map(list.filter(([, b]) => isObj(b) && typeof b.name === 'string').map(([id, b]) => [b.name, id]));
|
||||
const found = new Set();
|
||||
for (const [, name] of String(band.desc || '').matchAll(/<([^<>]+)>/g)) {
|
||||
const id = byName.get(name.trim());
|
||||
if (id) found.add(id);
|
||||
}
|
||||
const poolOf = (k) => (isObj(pools) && Object.hasOwn(pools, k) && isObj(pools[k]) ? pools[k] : null);
|
||||
for (const buff of Array.isArray(band.buffs) ? band.buffs : []) {
|
||||
for (const board of [buff && buff.bb, buff && buff.bbStr]) {
|
||||
if (!isObj(board)) continue;
|
||||
for (const v of Object.values(board)) {
|
||||
if (typeof v !== 'string') continue;
|
||||
for (const part of v.split(',').map((s) => s.trim())) {
|
||||
if (known.has(part)) found.add(part);
|
||||
const pool = poolOf(part);
|
||||
if (pool && typeof pool.bond === 'string' && known.has(pool.bond)) found.add(pool.bond);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ids.filter((id) => found.has(id));
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import { computeBonds, bondList, bondSnapshot, bondsWithGains, HARMONY_BOND } fr
|
||||
import { botPickBand } from '../../server/match/bot.js';
|
||||
import { tileKey } from '../../server/match/board.js';
|
||||
import { validateClientResult } from '../../server/match/fields.js';
|
||||
import { bandBondIds as bandBondIdsOf } from '../../shared/bandBonds.js';
|
||||
import { DATA, makeMatch, give, legalTileFor, checkInvariants } from './harness.js';
|
||||
|
||||
const MLYSS = 'chess_char_6_11_a'; // 缪尔赛思 (调和)
|
||||
@@ -115,16 +116,21 @@ describe('§21.26 3 — strategies tied to a bond the mode switches off', () =>
|
||||
assert.deepEqual(funny.bandBondIds('nope'), []);
|
||||
});
|
||||
|
||||
test('a band tied only through a pool or a blackboard bond id is caught too (潘格尼尼 without its text)', () => {
|
||||
const data = { ...DATA, bands: {
|
||||
band_pool: { ...DATA.bands.band_paganini, bandId: 'band_pool', desc: '【定制铳械】累计花费55资金后,获得1名精锐干员' },
|
||||
band_bb: { ...DATA.bands.band_clementia, bandId: 'band_bb', desc: '【崇高牺牲】' },
|
||||
band_free: { ...DATA.bands.band_bldsk, bandId: 'band_free' },
|
||||
} };
|
||||
const gd = new GameData(data, 'mode_single_funny');
|
||||
assert.deepEqual(gd.bandBondIds('band_pool'), ['lateranoShip'], 'choices.json pools.pool_char_later.bond');
|
||||
assert.deepEqual(gd.bandBondIds('band_bb'), ['egirShip'], 'bbStr bond_id');
|
||||
assert.deepEqual(gd.bandBondIds('band_free'), []);
|
||||
test('the derivation (shared/bandBonds.js, run by the data build) catches a tie through a pool or a blackboard bond id alone; GameData reads the field', () => {
|
||||
const ctx = { bonds: DATA.bonds, pools: DATA.choices.pools };
|
||||
const pool = { ...DATA.bands.band_paganini, desc: '【定制铳械】累计花费55资金后,获得1名精锐干员' };
|
||||
const bb = { ...DATA.bands.band_clementia, desc: '【崇高牺牲】' };
|
||||
assert.deepEqual(bandBondIdsOf(pool, ctx), ['lateranoShip'], 'choices.json pools.pool_char_later.bond');
|
||||
assert.deepEqual(bandBondIdsOf(bb, ctx), ['egirShip'], 'bbStr bond_id');
|
||||
assert.deepEqual(bandBondIdsOf({ desc: '在<阿戈尔>部分干员缺席时体验可能不完整;<寻呼模块>', buffs: [{ bb: { bond: 'kazimierzShip,nope' } }] }, ctx), ['egirShip', 'kazimierzShip'], 'the note, comma lists; an item in <…> is no bond');
|
||||
assert.deepEqual(bandBondIdsOf(DATA.bands.band_bldsk, ctx), []);
|
||||
assert.deepEqual(bandBondIdsOf(null, ctx), []);
|
||||
// the build wrote exactly this for every band (data/bands.json is not stale against the helper)
|
||||
for (const [id, b] of Object.entries(DATA.bands)) assert.deepEqual(b.bondIds, bandBondIdsOf(b, ctx), id);
|
||||
// GameData reads the field (known bonds, data order) — never re-derives
|
||||
const gd = new GameData({ ...DATA, bands: { band_x: { ...DATA.bands.band_bldsk, bandId: 'band_x', bondIds: ['kazimierzShip', 'nope', 'egirShip'] }, band_old: { ...DATA.bands.band_paganini, bondIds: undefined } } }, 'mode_single_funny');
|
||||
assert.deepEqual(gd.bandBondIds('band_x'), ['egirShip', 'kazimierzShip']);
|
||||
assert.deepEqual(gd.bandBondIds('band_old'), [], 'data without the field: no tie');
|
||||
});
|
||||
|
||||
/** Bands the bots took over seeds 1–200: solo (one AI's pick) and co-op (the real draft, 4 AI seats). */
|
||||
@@ -255,6 +261,7 @@ test('§21.26 docs: DESIGN (the subsection and the normative lines), META, PLAYI
|
||||
assert.ok(at > 0, 'the subsection');
|
||||
const s = DESIGN.slice(at);
|
||||
for (const k of ['morphPairings', 'harmonyMembers', '`harmony: 1`', 'bandBondIds', 'botPickBand', '0 of 200 and 0 of 800', '5 of 200 solo and 23 of 800 co-op']) assert.ok(s.includes(k), k);
|
||||
assert.match(doc('docs/DATA.md'), /\| `bondIds` \| `\["lateranoShip"\]` \(潘格尼尼\) \/ `\[\]` \|/, 'DATA.md bands.json bondIds');
|
||||
assert.match(DESIGN, /bonds:\[\{bondId,count,active,tier,layers,harmony\? \/\* 调和's \+1 is in count, §21\.26 \*\/\}\]/, '§8.2');
|
||||
assert.match(DESIGN, /bonds: \[ \{ bondId, count, active, tier, layers, harmony\? \/\* 调和's \+1 is in count, §21\.26 \*\/, thresholds, countsHand \} \]/, '§8.3');
|
||||
assert.match(DESIGN, /Strategy \(§21\.6\): pick a band \(weighted by starting LP; never one built around a bond the mode switches off — `gd\.bandBondIds`, §21\.26\)/, '§6.6');
|
||||
|
||||
@@ -35,6 +35,7 @@ import { existsSync } from 'node:fs';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { Grid, DEPLOY_REFUSED_TILES } from '../server/sim/grid.js';
|
||||
import { bandBondIds } from '../shared/bandBonds.js';
|
||||
|
||||
// ===== CLI & IO ==================================================================================
|
||||
|
||||
@@ -1433,7 +1434,10 @@ function buildItems(ctx, effects) {
|
||||
|
||||
// ===== bands (策略) ===============================================================================
|
||||
|
||||
/** Build data/bands.json: the 40 season strategies. */
|
||||
/**
|
||||
* Build data/bands.json: the 40 season strategies. `bondIds` (the bonds a strategy is built around: shared/bandBonds.js
|
||||
* over its text and blackboards, needing bonds.json and choices.json pools) is added in main() once those are built.
|
||||
*/
|
||||
function buildBands(ctx, effects) {
|
||||
const { act, ac } = ctx;
|
||||
const out = {};
|
||||
@@ -3072,6 +3076,7 @@ function validateAll(f) {
|
||||
if (it.requiresBondId && !bonds[it.requiresBondId]) err(`item ${it.id}: requiresBond ${it.requiresBondId} missing`);
|
||||
}
|
||||
for (const b of Object.values(bands)) if (!effects[b.effectId]) err(`band ${b.bandId}: effect missing`);
|
||||
for (const b of Object.values(bands)) for (const id of b.bondIds || []) if (!bonds[id]) err(`band ${b.bandId}: bond ${id} missing`);
|
||||
for (const w of Object.values(waves)) {
|
||||
for (const sp of w.spawns) {
|
||||
if (sp.action) continue;
|
||||
@@ -3145,6 +3150,9 @@ async function main() {
|
||||
const factions = buildFactions(ctx, enemies);
|
||||
const bosses = buildBosses(ctx, enemies, waves);
|
||||
const choices = buildChoices(ctx, effects, items, chess);
|
||||
// the bonds each strategy is built around (DESIGN §21.26): the bot skips, and the strategy draft marks 本局禁用, a band
|
||||
// whose bond the mode switches off
|
||||
for (const b of Object.values(bands)) b.bondIds = bandBondIds(b, { bonds, pools: choices.pools });
|
||||
const config = buildConfig(ctx, waves, stages, bands);
|
||||
const files = { config, chess, bonds, garrisons, items, bands, effects, choices, enemies, factions, waves, stages, bosses, tokens };
|
||||
|
||||
|
||||
Reference in new issue
Block a user