feedback7 R22-LOADOUT: 干员调配's module card reads the module's talent changes at the operator's 潜能, as 局内数值 and the battle do; golden: unchanged

Grok's review of the branch (low): ModuleInfo read talentChanges from the data's module record — the full-potential one —
so at 潜能1 余's 闲云隐市 still read 「生命上限2%(+0.5%)」 while 局内数值 and the battle use 1.5 % (异客 +10 %(+2 %) vs
+8 %). ModuleInfo takes `potential` and composes the elite at it first (shared/potential.js atPotential; the module's
traitOverride does not depend on potential); the detail passes the operator's setting.

- Test: test/ui/loadout-rows.test.js — 余's module card at none / 潜能6 / 潜能1, 不装备's base 特性, the detail's prop.
This commit is contained in:
sganggs committed 2026-10-08 22:20:30 +08:00
1 parent 4f8f5d2acf
commit 9f36db32e8
2 files changed
+31 -5

No files matched your search

+12 -4
View File
@@ -34,11 +34,12 @@ import { PHASE } from '../../../shared/constants.js';
import {
MODULE_NONE, PROF_ORDER, PROF_NAME, rosterOf, filterRoster, recordsOf, chessOptions, effectiveChoice, setChoice, resetChoice,
changedCount, skillLabel, moduleBadge, attrRows, skillTags, quickSkillTags, traitLines, serializeExport, parseImport, LOADOUT_IMPORT_MAX_BYTES,
opsOf, setOps, resetOps,
opsOf, setOps, resetOps, moduleRecord,
} from '../ui/loadoutModel.js';
import { loadoutStore, openLoadout, closeLoadout, setEntries, setOpsMap, applyLoadoutEntries, setNotOwned, applyOwnershipImport, setDiyPicks, applyDiyImport } from '../ui/loadoutSync.js';
import { CultivationSelects, CultivationSection } from './cultivation.js';
import { cultivationCharIds } from '../../../shared/protocol.js';
import { atPotential } from '../../../shared/potential.js';
import { setOwned, notOwnedCount, serializeOwnership, parseOwnershipImport, OWNERSHIP_IMPORT_MAX_BYTES } from '../ui/ownershipModel.js';
import { OwnershipPanel, useOwnershipRoster } from './ownership.js';
import { DiyPanel, diyData } from './diy.js';
@@ -263,9 +264,16 @@ function SkillOption({ m, opt, on, level, onPick }) {
</button>`;
}
export function ModuleInfo({ m, golden, opt }) {
/**
* The chosen module's card: its stat bonus, the 特性 it fights with, its talent changes — read at the operator's 潜能
* (`potential`, 0.2.2: the elite composed at it first, shared/potential.js atPotential, as 局内数值 and the battle do; the
* data record is the full-potential one — 余's 闲云隐市 「生命上限2%(+0.5%)」 is 「1.5%」 at 潜能1). null = full potential.
* @param {{ m: any, golden: any, opt: { id: string, rec: any, isDefault?: boolean }, potential?: number|null }} props
*/
export function ModuleInfo({ m, golden, opt, potential = null }) {
if (!golden) return null;
const rec = opt.id === MODULE_NONE ? null : opt.rec;
const at = atPotential(golden, potential);
const rec = opt.id === MODULE_NONE ? null : at === golden ? opt.rec : moduleRecord(at, opt.id) || opt.rec;
if (!rec) {
const traitBase = golden.traitBase || null;
return html`<div class="lo-minfo lo-minfo--none">
@@ -412,7 +420,7 @@ function Detail({ m, chess, golden, entries, ops = {}, onChange, onOps, onReset,
${mo.isDefault ? html`<span class="lo-badge lo-badge--def lo-mod__def">${t('默认')}</span>` : null}
</button>`)}
</div>
${modOpt ? html`<${ModuleInfo} m=${m} golden=${golden} opt=${modOpt} />` : null}
${modOpt ? html`<${ModuleInfo} m=${m} golden=${golden} opt=${modOpt} potential=${opsOf(ops, chess.charId).potential} />` : null}
</section>` : null}
${notOwned ? html`<p class="lo-locknote lo-locknote--standin" data-testid="loadout-standin-note"><${Icon} name="info" />${standInName(chess) ? t('干员持有中标记为未持有:此棋子由替补干员 {name} 上场,技能与模组固定(补位干员技能不可更改);这里的调配在改回「持有」后生效', { name: standInName(chess) })
: t('干员持有中标记为未持有:此棋子由替补干员上场,技能与模组固定(补位干员技能不可更改);这里的调配在改回「持有」后生效')}</p>` : null}
+19 -1
View File
@@ -15,7 +15,7 @@ globalThis.fetch = async (url) => {
const name = String(url).split('/').pop();
try { const body = readFileSync(path.join(ROOT, 'data', name), 'utf8'); return { ok: true, status: 200, json: async () => JSON.parse(body) }; } catch { return { ok: false, status: 404, json: async () => ({}) }; }
};
const { RosterRow, RosterHead, statsPreview, LoadoutStats } = await import('../../public/js/screens/loadout.js');
const { RosterRow, RosterHead, statsPreview, LoadoutStats, ModuleInfo } = await import('../../public/js/screens/loadout.js');
const { CultivationSelects, CultivationSection, cultivateName } = await import('../../public/js/screens/cultivation.js');
const { DiyPanelView } = await import('../../public/js/screens/diy.js');
const { data } = await import('../../public/js/data.js');
@@ -144,6 +144,24 @@ test('局内数值 at the operator\'s settings: 刺玫 at 潜能1 / 未精英化
assert.match(cap(LoadoutStats({ base: v, golden: g, entries: {}, level: 'elite', onLevel() {}, getChess: get, ops: {} })), /^数值含所选模组;含潜能与练度;不含技能发动/);
});
test('the module card reads its talent changes at the operator\'s 潜能, as 局内数值 and the battle do (余 闲云隐市: 2%(+0.5%) / 1.5%)', () => {
// (Grok review of the branch: the card printed the full-potential line whatever the setting)
const g = get('chess_char_6_03_b');
const mod = g.modules.find((x) => x.uniEquipId === 'uniequip_002_yu');
const opt = { id: mod.uniEquipId, rec: mod, isDefault: !!mod.isDefault };
const texts = (tree) => find(tree, (v) => typeof v.type === 'function' && v.type.name === 'RichText').map((v) => String(v.props.text));
const full = texts(ModuleInfo({ m: null, golden: g, opt }));
assert.ok(full.some((x) => x.includes('生命上限2%<@ba.talpu>(+0.5%)</>')), 'full potential (none given)');
assert.deepEqual(texts(ModuleInfo({ m: null, golden: g, opt, potential: 6 })), full);
const p1 = texts(ModuleInfo({ m: null, golden: g, opt, potential: 1 }));
assert.ok(p1.some((x) => x.includes('生命上限1.5%的生命')), '潜能1');
assert.ok(!p1.some((x) => x.includes('(+0.5%)')));
assert.deepEqual(texts(ModuleInfo({ m: null, golden: g, opt: { id: 'none', rec: null }, potential: 1 })), [g.traitBase.descRaw], '不装备: the base 特性');
// the detail hands the operator's 潜能 to it
const src = readFileSync(path.join(ROOT, 'public/js/screens/loadout.js'), 'utf8');
assert.match(src, /<\$\{ModuleInfo\} m=\$\{m\} golden=\$\{golden\} opt=\$\{modOpt\} potential=\$\{opsOf\(ops, chess\.charId\)\.potential\} \/>/);
});
test('css: the head and the rows share one grid (columns aligned), quick targets grow on a coarse pointer, the detail slides over below 1000 px', () => {
const css = readFileSync(path.join(ROOT, 'public/css/screens/loadout.css'), 'utf8');
assert.match(css, /\.lo-list__head, \.lo \.lo-card \{ display: grid; grid-template-columns: var\(--cols\);/);