feedback2 WL: the detail card's stats block, talent list and 特性 text are shared functions (chessStatsBlock / chessTalents / traitText) the 干员调配 screen can draw too

This commit is contained in:
sganggs committed 2026-10-04 02:51:21 +08:00
1 parent 50c9b5b1b6
commit 41bfdb5f96
2 files changed
+50 -28

No files matched your search

+47 -27
View File
@@ -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 }) {
</section>`;
}
/** 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`
<div key="stats" class=${cx('dstats-wrap', live && 'is-live')} data-live=${live ? live.src || 'prep' : undefined}>
<div class="dstats">
<${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` : '—'} />
</div>
<div class="drange"><span class="dstat__k">攻击范围</span><${RangeGrid} grid=${cardRangeGrid(live, rec, chess)} /></div>
</div>`;
}
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
</div>`;
blocks.garrison = garrison ? html`<${GarrisonBlock} key="garrison" garrison=${garrison} m=${m} />` : null;
blocks.trait = c.trait?.desc ? html`<p key="trait" class="dtrait"><${Icon} name="info" /><${RichText} text=${traitText(c, golden, lo)} /></p>` : 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`
<div key="stats" class=${cx('dstats-wrap', live && 'is-live')} data-live=${live ? live.src || 'prep' : undefined}>
<div class="dstats">
<${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` : '—'} />
</div>
<div class="drange"><span class="dstat__k">攻击范围</span><${RangeGrid} grid=${cardRangeGrid(live, fr, c)} /></div>
</div>`;
blocks.stats = chessStatsBlock({ rec: fr, chess: c, live });
blocks.skill = sk ? html`<${Section} key="skill" title="技能" micro="SKILL" class="dsec--skill">
<div class="dskill" data-skill=${sk.skillId || ''}>
<${Img} src=${skIcon} class="dskill__icon" fallback=${html`<span class="dskill__icon dskill__icon--empty">${skSlot ? html`<b class="num">${skSlot}</b>` : null}</span>`} />
@@ -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`<div key=${i} class="dtalent"><b>${t.name}</b><${RichText} text=${t.descRaw || t.desc} class="dtext" /></div>`)}
const talents = chessTalents(fr);
blocks.talents = talents.length ? html`<${Section} key="talents" title="天赋" micro="TALENT" class="dsec--talent">
${talents.map((t, i) => html`<div key=${i} class="dtalent"><b>${t.name}</b><${RichText} text=${t.descRaw || t.desc} class="dtext" /></div>`)}
<//>` : null;
blocks.actions = piece && editable && piece.kind !== 'item' ? html`<div key="actions" class="dactions">
<${Button} variant="amber" icon="close" class="dpanel__sell" onClick=${() => onSell(piece, c)}>出售<span class="dsell num">+${sell}</span><//>
+3 -1
View File
@@ -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\)/);
});