From 41bfdb5f96b4dd6be823a1e03a74efb0c9e274e3 Mon Sep 17 00:00:00 2001 From: sganggs Date: Sun, 4 Oct 2026 02:51:21 +0800 Subject: [PATCH] =?UTF-8?q?feedback2=20WL:=20the=20detail=20card's=20stats?= =?UTF-8?q?=20block,=20talent=20list=20and=20=E7=89=B9=E6=80=A7=20text=20a?= =?UTF-8?q?re=20shared=20functions=20(chessStatsBlock=20/=20chessTalents?= =?UTF-8?q?=20/=20traitText)=20the=20=E5=B9=B2=E5=91=98=E8=B0=83=E9=85=8D?= =?UTF-8?q?=20screen=20can=20draw=20too?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/js/ui/detailPanel.js | 74 ++++++++++++++++++++------------ test/ui/feedback1e-range.test.js | 4 +- 2 files changed, 50 insertions(+), 28 deletions(-) diff --git a/public/js/ui/detailPanel.js b/public/js/ui/detailPanel.js index 1b70dd3f..3f2d457a 100644 --- a/public/js/ui/detailPanel.js +++ b/public/js/ui/detailPanel.js @@ -28,6 +28,8 @@ // mini-map follows the live entry's `range` too (cardRangeGrid: the grid the unit attacks with now — a running skill's // range such as 烛煌 S3's 4-11, rangeExtend included; community report E1 after 0.1.0, it used to stay the base grid); // a grid larger than the box (RANGE_FIT) draws smaller cells (rangeGridStyle), a whole-field one reads 全场. +// The stats block (chessStatsBlock), the 特性 text (traitText) and the talent list (chessTalents) are exported: the 干员调配 +// screen's 局内数值 section draws the same ones for the chosen skill / module, without a live entry (GitHub issue #64). import { html, Icon, TierChip, MicroLabel, Button, confirmDialog, useTicker } from './components.js'; import { Img, RichText, UnitThumb, BondGlyph, GIcon } from './gameComponents.js'; @@ -267,9 +269,9 @@ export function BondChips({ bondIds, bonds = [], onBond = null, off = null, gran /** * 特性 text of the record the unit fights with (DESIGN §16: `lo.record` = the chosen module's traitOverride, or the - * no-module traitBase for 不装备 — data `trait` is the default module's). + * no-module traitBase for 不装备 — data `trait` is the default module's). Also the 干员调配 screen's (screens/loadout.js). */ -function traitText(c, golden, lo) { +export function traitText(c, golden, lo) { const t = (lo?.record || c).trait || {}; const base = t.descRaw || t.desc || ''; if (!golden || lo?.record !== c) return base; @@ -310,6 +312,45 @@ function GarrisonBlock({ garrison, m }) { `; } +/** The talents the card lists (天赋): the loadout record's named, not hidden ones. */ +export function chessTalents(rec) { + return (Array.isArray(rec?.talents) ? rec.talents : []).filter((t) => t && t.name && !t.hidden); +} + +/** + * The card's stats block (block key "stats"): the eight stats and the 攻击范围 mini-map. `rec` = the loadout-resolved + * record the unit fights with (chessLoadout `.record`: the chosen module's stats, the chosen skill's passive range), + * `chess` the chess record (the range's fallback), `live` the live / start-of-battle entry (liveStat) or null — the + * record's own numbers, untagged. A plain function (its root node keeps the key), shared by the detail card and the + * 干员调配 screen (screens/loadout.js 局内数值, GitHub issue #64): one block, so the two never disagree. + * @param {{ rec: any, chess: any, live?: any }} p + */ +export function chessStatsBlock({ rec, chess, live = null }) { + const s = rec?.stats || {}; + const interval = attackInterval(s.bat, s.aspd); + // live (battle) / start-of-battle (prep) values against the base, else the record's (liveStat) + const st = { + maxHp: liveStat(live, 'maxHp', s.maxHp), atk: liveStat(live, 'atk', s.atk), def: liveStat(live, 'def', s.def), + res: liveStat(live, 'res', s.res ?? 0, fmtRes), interval: liveStat(live, 'interval', interval, fmtInterval), + blockCnt: liveStat(live, 'blockCnt', s.blockCnt, (v) => String(v)), + }; + return html` +
+
+ <${LiveTag} live=${live} /> + <${Stat} k="生命上限" ...${st.maxHp} /> + <${Stat} k="攻击" ...${st.atk} /> + <${Stat} k="防御" ...${st.def} /> + <${Stat} k="法术抗性" ...${st.res} /> + <${Stat} k="攻击间隔" ...${st.interval} /> + <${Stat} k="阻挡数" ...${st.blockCnt} /> + <${Stat} k="部署费用" v=${s.cost ?? '—'} /> + <${Stat} k="再部署" v=${s.respawnTime != null ? `${s.respawnTime}s` : '—'} /> +
+
攻击范围<${RangeGrid} grid=${cardRangeGrid(live, rec, chess)} />
+
`; +} + export function ChessDetail({ chess, piece, unit, snapHp, editable, onSell, bonds, offBonds = null, loadout, onBond, live = null, hint = null, unitItems = null }) { const m = data.get('assets'); const hp = hpOf(live, snapHp); @@ -317,9 +358,7 @@ export function ChessDetail({ chess, piece, unit, snapHp, editable, onSell, bond const c = chess; // stats / talents the unit fights with: the chosen module's (or none — statsBase) for an elite (DESIGN §16) const fr = lo?.record || c; - const s = fr.stats || {}; const golden = !!(c.isGolden || piece?.golden); - const interval = attackInterval(s.bat, s.aspd); const sk = lo?.skill || c.skill || null; // a chosen skill the manifest has no icon for (only the default skills' icons are fetched): its slot letter const skIcon = sk && lo && !lo.defaultSkill ? skillRecordIconUrl(m, sk, { empty: false }) : skillIconUrl(m, c); @@ -361,27 +400,7 @@ export function ChessDetail({ chess, piece, unit, snapHp, editable, onSell, bond `; blocks.garrison = garrison ? html`<${GarrisonBlock} key="garrison" garrison=${garrison} m=${m} />` : null; blocks.trait = c.trait?.desc ? html`

<${Icon} name="info" /><${RichText} text=${traitText(c, golden, lo)} />

` : null; - // live (battle) / start-of-battle (prep) values against the base, else the record's (liveStat) - const st = { - maxHp: liveStat(live, 'maxHp', s.maxHp), atk: liveStat(live, 'atk', s.atk), def: liveStat(live, 'def', s.def), - res: liveStat(live, 'res', s.res ?? 0, fmtRes), interval: liveStat(live, 'interval', interval, fmtInterval), - blockCnt: liveStat(live, 'blockCnt', s.blockCnt, (v) => String(v)), - }; - blocks.stats = html` -
-
- <${LiveTag} live=${live} /> - <${Stat} k="生命上限" ...${st.maxHp} /> - <${Stat} k="攻击" ...${st.atk} /> - <${Stat} k="防御" ...${st.def} /> - <${Stat} k="法术抗性" ...${st.res} /> - <${Stat} k="攻击间隔" ...${st.interval} /> - <${Stat} k="阻挡数" ...${st.blockCnt} /> - <${Stat} k="部署费用" v=${s.cost ?? '—'} /> - <${Stat} k="再部署" v=${s.respawnTime != null ? `${s.respawnTime}s` : '—'} /> -
-
攻击范围<${RangeGrid} grid=${cardRangeGrid(live, fr, c)} />
-
`; + blocks.stats = chessStatsBlock({ rec: fr, chess: c, live }); blocks.skill = sk ? html`<${Section} key="skill" title="技能" micro="SKILL" class="dsec--skill">
<${Img} src=${skIcon} class="dskill__icon" fallback=${html`${skSlot ? html`${skSlot}` : null}`} /> @@ -415,8 +434,9 @@ export function ChessDetail({ chess, piece, unit, snapHp, editable, onSell, bond : !piece && carried.length ? html`<${Section} key="equip" title="装备" micro=${`EQUIP ${carried.length}/2`} class="dsec--equip"> ${carried.map((id, i) => html`<${ItemRow} key=${`${i}:${id}`} itemId=${id} carried=${carried} off=${offBonds} />`)} ` : null; - blocks.talents = Array.isArray(fr.talents) && fr.talents.some((t) => t && t.name && !t.hidden) ? html`<${Section} key="talents" title="天赋" micro="TALENT" class="dsec--talent"> - ${fr.talents.filter((t) => t && t.name && !t.hidden).map((t, i) => html`
${t.name}<${RichText} text=${t.descRaw || t.desc} class="dtext" />
`)} + const talents = chessTalents(fr); + blocks.talents = talents.length ? html`<${Section} key="talents" title="天赋" micro="TALENT" class="dsec--talent"> + ${talents.map((t, i) => html`
${t.name}<${RichText} text=${t.descRaw || t.desc} class="dtext" />
`)} ` : null; blocks.actions = piece && editable && piece.kind !== 'item' ? html`
<${Button} variant="amber" icon="close" class="dpanel__sell" onClick=${() => onSell(piece, c)}>出售+${sell} diff --git a/test/ui/feedback1e-range.test.js b/test/ui/feedback1e-range.test.js index 853ca880..3629ca31 100644 --- a/test/ui/feedback1e-range.test.js +++ b/test/ui/feedback1e-range.test.js @@ -101,7 +101,9 @@ test('a whole-field skill (纯烬艾雅法拉 S3 "攻击范围扩大至整个战 test('the game screen hands the card the live entry it already reads (battle runner / m.unitStats)', () => { const src = readFileSync(path.join(ROOT, 'public/js/ui/detailPanel.js'), 'utf8'); - assert.match(src, /grid=\$\{cardRangeGrid\(live, fr, c\)\}/); + // (the stats block moved into the shared chessStatsBlock — also the 干员调配 screen's 局内数值, GitHub issue #64) + assert.match(src, /grid=\$\{cardRangeGrid\(live, rec, chess\)\}/); + assert.match(src, /blocks\.stats = chessStatsBlock\(\{ rec: fr, chess: c, live \}\)/, 'the card draws it through the shared block'); const game = readFileSync(path.join(ROOT, 'public/js/screens/game.js'), 'utf8'); assert.match(game, /battleRunner\.unitStats\(uid, fid\)/); });