diff --git a/public/js/screens/loadout.js b/public/js/screens/loadout.js index 779b5ec9..7448fea3 100644 --- a/public/js/screens/loadout.js +++ b/public/js/screens/loadout.js @@ -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 }) { `; } -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`
<${Icon} name="info" />${standInName(chess) ? t('干员持有中标记为未持有:此棋子由替补干员 {name} 上场,技能与模组固定(补位干员技能不可更改);这里的调配在改回「持有」后生效', { name: standInName(chess) }) : t('干员持有中标记为未持有:此棋子由替补干员上场,技能与模组固定(补位干员技能不可更改);这里的调配在改回「持有」后生效')}
` : null} diff --git a/test/ui/loadout-rows.test.js b/test/ui/loadout-rows.test.js index ad39bede..8dd0e0f6 100644 --- a/test/ui/loadout-rows.test.js +++ b/test/ui/loadout-rows.test.js @@ -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\);/);