feedback5 WK: the 集成战略 (ISW-A) module rule is a roster rule — validateDiyPicks (so room.diy, PlayerState.setDiy and the picker) refuses it, checkDiyPick / diyRecordOf / the sim still compose any module of the form (the kits' tests and the golden diy family field them); shared/diy.js diyModuleOf; docs follow

Golden unchanged.
This commit is contained in:
sganggs committed 2026-10-06 00:23:54 +08:00
1 parent cde3e7305e
commit 853566129c
3 files changed
+33 -15

No files matched your search

+5 -4
View File
@@ -660,15 +660,16 @@ composes the same record (`public/js/ui/gameLogic/standIn.js standInOf`, the ren
A 自选 piece is composed by `shared/diy.js`: A 自选 piece is composed by `shared/diy.js`:
`checkDiyPick(slotId, pick, data)` checks one pick `{ charId, skillIndex?, uniEquipId? }` against a slot (a pick of the `checkDiyPick(slotId, pick, data)` checks one pick `{ charId, skillIndex?, uniEquipId? }` against a slot (a pick of the
slot's tier; a prototype takes its `diy.locked` selection, another skill is refused; an owned pick names one of its three slot's tier; a prototype takes its `diy.locked` selection, another skill is refused; an owned pick names one of its three
skills and optionally a module of its elite form at the slot's stage — never a 集成战略 one, `isDiyModule`: the ISW-A skills and optionally a module of its elite form at the slot's stage), `diySlot(id, data)` names a slot's tier, elite
modules of 凯尔希, 傀影, 菲亚梅塔, 提丰, 艾丽妮 and 霍尔海雅 [ASSUMED], the owner's decision of 2026-10-05), twin and `shopLevel`, `diyRecord(slotId, pick, { elite, data })` /
`diySlot(id, data)` names a slot's tier, elite twin and `shopLevel`, `diyRecord(slotId, pick, { elite, data })` /
`diyRecordOf(slot, pick, data)` give the record — the slot's identity (tier, price, merge, status; no 特质), the pick's `diyRecordOf(slot, pick, data)` give the record — the slot's identity (tier, price, merge, status; no 特质), the pick's
derived bonds, the operator's form at the slot's status with that skill and module (active on the elite only), plus derived bonds, the operator's form at the slot's status with that skill and module (active on the elite only), plus
`diyFor` = the slot's base id —, `diyPool(tier, { data, kitted })` the legal picks of a tier (prototypes, then the owned `diyFor` = the slot's base id —, `diyPool(tier, { data, kitted })` the legal picks of a tier (prototypes, then the owned
pool; with `kitted`, only operators with a kit: server/sim/content/kits/index.js `KITTED_CHARS`) and pool; with `kitted`, only operators with a kit: server/sim/content/kits/index.js `KITTED_CHARS`) and
`validateDiyPicks(picks, { data, kitted })` a roster (a prototype may fill a tier-5 and a tier-6 slot, an owned operator `validateDiyPicks(picks, { data, kitted })` a roster (a prototype may fill a tier-5 and a tier-6 slot, an owned operator
one slot, the picks of a tier differ). **In battle** a PlayerBattleInput entry of a DIY slot carries `diy` (the pick) and one slot, the picks of a tier differ, no 集成战略 module — `isDiyModule`: the ISW-A modules of 凯尔希, 傀影, 菲亚梅塔, 提丰,
艾丽妮 and 霍尔海雅 are never a player's choice [ASSUMED], the owner's decision of 2026-10-05, while the record and the sim
still compose them for the kits' tests). **In battle** a PlayerBattleInput entry of a DIY slot carries `diy` (the pick) and
is fielded as `getChess(slotId, { diy })` (docs/SIM.md §12); its kit is `KITS[charId]` (kits/README.md "How to add an is fielded as `getChess(slotId, { diy })` (docs/SIM.md §12); its kit is `KITS[charId]` (kits/README.md "How to add an
operator (自选)"). (`shared/standIn.js diyRecord(slot, charId, backups, sel)` is the older prototype-only form.) operator (自选)"). (`shared/standIn.js diyRecord(slot, charId, backups, sel)` is the older prototype-only form.)
+22 -7
View File
@@ -12,8 +12,10 @@
// - no 特质 (PRTS "甄选加入的干员不会拥有任何特质"); bonds from the operator's factions (`diy.operators[id].bonds`); // - no 特质 (PRTS "甄选加入的干员不会拥有任何特质"); bonds from the operator's factions (`diy.operators[id].bonds`);
// - the normal form is E2 Lv1, skill rank 4, no module; the elite E2 Lv60, rank 7, its module at stage 1 (tier 5) or 3 // - the normal form is E2 Lv1, skill rank 4, no module; the elite E2 Lv60, rank 7, its module at stage 1 (tier 5) or 3
// (tier 6) — the slot record's `status` (activity_table diyChessDict) picks the unit form; // (tier 6) — the slot record's `status` (activity_table diyChessDict) picks the unit form;
// - an owned pick chooses its skill (any of 3) and module (any of its modules but a 集成战略-only one — ISW-A, "在集成战略 // - an owned pick chooses its skill (any of 3) and module (any of its modules, or none — a roster never carries a
// 中…" [ASSUMED: not usable outside 集成战略, the owner's decision of 2026-10-05] — or none); a prototype carries the skill // 集成战略-only one: ISW-A, "在集成战略中…" [ASSUMED: not usable outside 集成战略, the owner's decision of 2026-10-05],
// validateDiyPicks / isDiyModule; the composed record and the sim still field any module, as the kits test them); a
// prototype carries the skill
// and module of its 补位 rows at that tier (`diy.locked`: "技能携带规则与系统补位时一致" — [ASSUMED] that reading, the // and module of its 补位 rows at that tier (`diy.locked`: "技能携带规则与系统补位时一致" — [ASSUMED] that reading, the
// owner's decision of 2026-10-05); // owner's decision of 2026-10-05);
// - potential 0 for everyone [ASSUMED: no account]. // - potential 0 for everyone [ASSUMED: no account].
@@ -26,8 +28,9 @@ import { composeUnitRecord, unitForm, statusKey } from './standIn.js';
export const DIY_TIERS = Object.freeze([5, 6]); export const DIY_TIERS = Object.freeze([5, 6]);
/** /**
* Module types a 自选 pick never carries: the 集成战略 modules (ISW-A — their own effect applies only "在集成战略中") * Module types a player's 自选 roster never carries: the 集成战略 modules (ISW-A — their own effect applies only "在集成
* [ASSUMED: not usable outside 集成战略, the owner's decision of 2026-10-05]. * 战略中") [ASSUMED: not usable outside 集成战略, the owner's decision of 2026-10-05]. A roster rule (validateDiyPicks,
* the client's picker), like the kit list: checkDiyPick / diyRecordOf still compose any module of the form.
*/ */
export const DIY_EXCLUDED_MODULE_TYPE = /^ISW-/; export const DIY_EXCLUDED_MODULE_TYPE = /^ISW-/;
@@ -179,7 +182,6 @@ export function checkDiyPick(slotId, pick, data) {
if (uniEquipId !== null && !mod) { if (uniEquipId !== null && !mod) {
return { error: `${charId}: no module ${uniEquipId} at stage ${slot.golden?.status?.equipLevel ?? '?'}` }; return { error: `${charId}: no module ${uniEquipId} at stage ${slot.golden?.status?.equipLevel ?? '?'}` };
} }
if (mod && !proto && !isDiyModule(mod)) return { error: `${charId}: ${uniEquipId} is a 集成战略 module (${mod.typeName})` };
return { ok: true, pick: { charId, skillIndex, uniEquipId } }; return { ok: true, pick: { charId, skillIndex, uniEquipId } };
} }
@@ -221,10 +223,21 @@ export function diyRecord(slotId, pick, { elite = false, data } = { data: null }
return diyRecordOf(elite || slot.elite ? slot.golden : slot.normal, pick, data); return diyRecordOf(elite || slot.elite ? slot.golden : slot.normal, pick, data);
} }
/**
* The module record `uniEquipId` of `charId`'s elite form at a slot's stage (data/backups.json form `modules[]`), or null.
* @param {string} slotId @param {string} charId @param {string} uniEquipId @param {DiyData} data
*/
export function diyModuleOf(slotId, charId, uniEquipId, data) {
const slot = diySlot(slotId, data);
const form = slot?.golden ? unitForm(backupsOf(data), charId, slot.golden.status) : null;
return (form?.modules ?? []).find((m) => m && m.uniEquipId === uniEquipId) ?? null;
}
/** /**
* Check a whole 自选 roster: `picks` maps slot base ids to a pick (absent / null = an empty slot). Strict: an unknown * Check a whole 自选 roster: `picks` maps slot base ids to a pick (absent / null = an empty slot). Strict: an unknown
* slot, an illegal pick (checkDiyPick), an operator without a kit (when `kitted` is given), the same operator twice in * slot, an illegal pick (checkDiyPick), a 集成战略 module (isDiyModule), an operator without a kit (when `kitted` is
* one tier, or an owned operator in two slots rejects the roster; a prototype may fill a tier-5 and a tier-6 slot. * given), the same operator twice in one tier, or an owned operator in two slots rejects the roster; a prototype may fill
* a tier-5 and a tier-6 slot.
* @param {any} picks * @param {any} picks
* @param {{ data: DiyData, kitted?: Kitted }} opts * @param {{ data: DiyData, kitted?: Kitted }} opts
* @returns {{ ok: true, picks: Record<string, DiyLoadout> } | { error: 'BAD_MSG'|'BAD_TARGET', detail: string }} * @returns {{ ok: true, picks: Record<string, DiyLoadout> } | { error: 'BAD_MSG'|'BAD_TARGET', detail: string }}
@@ -245,6 +258,8 @@ export function validateDiyPicks(picks, { data, kitted = null } = { data: null }
if (!('ok' in c)) return { error: 'BAD_TARGET', detail: c.error }; if (!('ok' in c)) return { error: 'BAD_TARGET', detail: c.error };
const { charId } = c.pick; const { charId } = c.pick;
if (!ok(charId)) return { error: 'BAD_TARGET', detail: `${charId} has no kit yet` }; if (!ok(charId)) return { error: 'BAD_TARGET', detail: `${charId} has no kit yet` };
const mod = c.pick.uniEquipId ? diyModuleOf(slotId, charId, c.pick.uniEquipId, data) : null;
if (mod && !isDiyModule(mod)) return { error: 'BAD_TARGET', detail: `${charId}: ${c.pick.uniEquipId} is a 集成战略 module (${mod.typeName})` };
const tier = diy.slots[slotId].tier; const tier = diy.slots[slotId].tier;
const inTier = byTier.get(tier) ?? new Set(); const inTier = byTier.get(tier) ?? new Set();
if (inTier.has(charId)) return { error: 'BAD_TARGET', detail: `${charId} fills two tier-${tier} slots` }; if (inTier.has(charId)) return { error: 'BAD_TARGET', detail: `${charId} fills two tier-${tier} slots` };
+6 -4
View File
@@ -103,15 +103,17 @@ test('checkDiyPick: an owned 6★ chooses any of its 3 skills and any module of
for (const [slot, pick, re] of bad) assert.match(checkDiyPick(slot, pick, DATA).error, re, JSON.stringify(pick)); for (const [slot, pick, re] of bad) assert.match(checkDiyPick(slot, pick, DATA).error, re, JSON.stringify(pick));
}); });
test('checkDiyPick: an owned pick never carries a 集成战略 module (ISW-A) [ASSUMED, the owner\'s decision of 2026-10-05]', () => { test('validateDiyPicks: a roster never carries a 集成战略 module (ISW-A) [ASSUMED, the owner\'s decision of 2026-10-05]; the record still composes it', () => {
const backups = DATA.backups; const backups = DATA.backups;
let checked = 0; let checked = 0;
for (const charId of backups.diy.ownedPool) { for (const charId of backups.diy.ownedPool) {
for (const [slot, key] of [[T5, '2/60/7/1'], [T6, '2/60/7/3']]) { for (const [slot, key] of [[T5, '2/60/7/1'], [T6, '2/60/7/3']]) {
for (const mod of backups.units[charId].forms[key]?.modules ?? []) { for (const mod of backups.units[charId].forms[key]?.modules ?? []) {
const c = checkDiyPick(slot, { charId, skillIndex: 0, uniEquipId: mod.uniEquipId }, DATA); const pick = { charId, skillIndex: 0, uniEquipId: mod.uniEquipId };
if (/^ISW-/.test(mod.typeName)) { assert.match(c.error || '', /集成战略 module/, `${charId} ${mod.uniEquipId}`); assert.equal(isDiyModule(mod), false); checked++; } assert.ok(checkDiyPick(slot, pick, DATA).ok, `${charId} ${mod.uniEquipId}: a legal record (the sim and the kits field it)`);
else assert.ok(c.ok, `${charId} ${mod.uniEquipId}: ${c.error}`); const v = validateDiyPicks({ [slot]: pick }, { data: DATA });
if (/^ISW-/.test(mod.typeName)) { assert.match(v.detail || '', /集成战略 module/, `${charId} ${mod.uniEquipId}`); assert.equal(isDiyModule(mod), false); checked++; }
else assert.ok(v.ok, `${charId} ${mod.uniEquipId}: ${v.detail}`);
} }
} }
} }