// Result screen (research 06 §10.6): victory / defeat hero with rounds passed, boss medallions, and a
// card per player — band, final lineup avatars with elite marks, stats, title (评语) with its icon —
// plus 返回同盟.
//
// Expected m.result (free-form in DESIGN §8.2; fields read tolerantly, see gameLogic.normalizeResult):
// { victory, roundsPassed, lastRound?, hiddenCleared?, bossId?, hiddenBossId?, difficulty?, modeId?, durationMs?,
// players: [{ playerId, seat, name, isBot, alive, lp, bandId, roundsPassed?,
// title: 'comment_1' | { id, name? } | null,
// lineup: [{ id /* chessId, golden id if elite */, golden?, tier?, items?: [itemId] }],
// bonds?: [{ bondId, layers, active }],
// stats: { dmgDealt, kills, leaks, gold /* funds SPENT */, refreshes, merges, bossDamage?, itemsEquipped?,
// activatedLayers?, lpLost?, perfectRounds? } }] }
import { useEffect } from '../../vendor/hooks.module.js';
import { html, Button, Icon, MicroLabel, DifficultyTag } from '../ui/components.js';
import { useGameData, Img, UnitThumb, BandIcon, PlayerAvatar, BondGlyph, LpTower, Sprite } from '../ui/gameComponents.js';
import { normalizeResult, fmtNum } from '../ui/gameLogic.js';
import { enemyIconUrl, titleIconUrl, uiUrl } from '../ui/assetUrls.js';
import { store, useStore, emptyMatch } from '../store.js';
import { audio } from '../audio.js';
const cx = (...p) => p.flat().filter(Boolean).join(' ');
// The first seven present are shown (+ remaining LP = a 4×2 grid); the title (评语) stats come first.
// `gold` is the funds a player SPENT (server/match/PlayerState.js spend(); the 挥金如土 title stat).
const STAT_ROWS = [
['dmgDealt', '造成伤害'], ['kills', '击倒敌人'], ['bossDamage', '领袖伤害'], ['activatedLayers', '盟约层数'],
['merges', '晋升次数'], ['itemsEquipped', '配发装备'], ['gold', '消耗资金'], ['perfectRounds', '完美作战'],
['refreshes', '刷新次数'], ['leaks', '未击倒'], ['lpLost', '损失生命'],
];
function PlayerCard({ p, myId, titles, best, solo = false }) {
const gd = useGameData();
const titleRec = p.title ? titles.find((t) => t.id === p.title.id) || null : null;
const titleName = p.title?.name || titleRec?.name || null;
const stats = STAT_ROWS.filter(([k]) => Number.isFinite(p.stats[k])).slice(0, 7);
const band = p.bandId ? gd.band(p.bandId) : null;
const lineup = p.lineup.slice(0, 10);
const bonds = p.bonds.filter((b) => b.active || b.layers > 0).sort((a, b) => (b.layers || 0) - (a.layers || 0)).slice(0, 6);
return html`
<${PlayerAvatar} player=${p} self=${p.playerId === myId} />
${p.name}${p.isBot ? html`AI` : null}${p.playerId === myId ? html`你` : null}
${band ? html`<${BandIcon} bandId=${band.bandId} size="xs" />${band.name}` : '—'}
${lineup.length ? lineup.map((u, i) => html`<${UnitThumb} key=${i} kind=${u.kind === 'token' ? 'token' : 'chess'} id=${u.id} golden=${!!u.golden} tier=${u.tier} size="sm" />`)
: html`${p.alive === false ? '阵容已撤离' : 'NO INFO'}`}
${bonds.length ? html`
${bonds.map((b) => html`
<${BondGlyph} bondId=${b.bondId} />${b.layers ?? 0}`)}
` : null}
<${MicroLabel}>ROUNDS${MicroLabel}>${p.roundsPassed}
${p.trophies > 0 || p.reward > 0 ? html`${p.trophies > 0 ? html`<${Icon} name="crown" />+${p.trophies}` : null}${p.reward > 0 ? html`<${Icon} name="shield" />+${p.reward}` : null}` : null}
${titleName ? html`
<${Img} src=${titleIconUrl(gd.m, titleRec?.picId || p.title?.picId || String(p.title.id || '').replace('comment_', 'comment_icon_'))} class="rcard__ticon" fallback=${html`<${Icon} name="crown" />`} />
<${MicroLabel} tone="gold">评语${MicroLabel}>${titleName}
` : html``}
${stats.map(([k, label]) => html`
${label}${fmtNum(p.stats[k])}
`)}
${Number.isFinite(p.lp) ? html`
${p.lpShared && !solo ? '同盟剩余生命' : '剩余生命'}<${LpTower} value=${p.lp} size="sm" />
` : null}
`;
}
/** RESULT screen. */
export function ResultScreen() {
const res = useStore((s) => s.match.result);
const pub = useStore((s) => s.match.public);
const myId = useStore((s) => s.me.playerId);
const hasRoom = useStore((s) => !!s.room);
const gd = useGameData();
const r = normalizeResult(res, pub);
const titles = Array.isArray(gd.config?.titles) ? gd.config.titles : [];
const best = {};
for (const [k] of STAT_ROWS) {
let top = null;
for (const p of r.players) if (Number.isFinite(p.stats[k]) && p.stats[k] > 0 && (top == null || p.stats[k] > top.v)) top = { v: p.stats[k], id: p.playerId };
if (top && r.players.length > 1) best[k] = top.id;
}
useEffect(() => { audio.sfx(r.victory ? 'settlementSucceed' : 'settlementFail'); }, []);
const back = () => store.set({ match: emptyMatch() });
const boss = r.bossId ? gd.boss(r.bossId) : null;
// the Hidden Core medal (and its corrupted leader) only once R15 was actually fought
const hidden = r.hiddenBossId && r.hiddenReached ? gd.boss(r.hiddenBossId) : null;
const bg = uiUrl(gd.m, r.victory ? 'settle/settlemen_teamshow_success' : 'settle/settlemen_teamshow_fail');
const mins = r.durationMs ? Math.round(r.durationMs / 60000) : null;
return html`
<${Sprite} k="entry/season_logo_settle" class="result__logoimg" fallback=${html`<${MicroLabel} tone="mint">STRONGHOLD PROTOCOL${MicroLabel}>`} />
${r.difficulty ? html`<${DifficultyTag} difficulty=${r.difficulty} size="lg" />` : null}
${r.victory ? '模拟完成' : '模拟失败'}
${r.victory ? '成功卫戍 · 敌方领袖已被击败' : '防线已被突破'}
通过回合
${r.roundsPassed}
${r.roundsPassed <= r.lastRound ? html`/${r.lastRound}` : null}
${boss ? html`
<${Img} src=${enemyIconUrl(gd.m, boss.enemyKey)} />${r.victory ? html`<${Icon} name="check" />` : html`<${Icon} name="close" />`}
敌方领袖
` : null}
${hidden ? html`
<${Img} src=${enemyIconUrl(gd.m, hidden.enemyKey)} />${r.hiddenCleared ? html`<${Icon} name="check" />` : html`<${Icon} name="close" />`}
隐秘核心
` : null}
${mins ? html`本局耗时 ${mins} 分钟
` : null}
同盟成员<${MicroLabel}>ALLIANCE REPORT${MicroLabel}>
${r.players.length ? r.players.map((p) => html`<${PlayerCard} key=${p.playerId} p=${p} myId=${myId} titles=${titles} best=${best} solo=${r.players.length < 2} />`)
: html`暂无结算数据
`}
`;
}