Files
ark-stronghold-protocol/public/js/ui/combatHud.js
T
xcdoge b4bc89ad4e feedback7 UIBACK: the top-bar capsule counts like the official — resolved = killed + leaked among the enemies the round scheduled (opening 0/3, a leak 1/3, a kill 2/3, the splitter 3/3); a split child, a summon and a boss summon are in neither number but still cost LP and break 完美作战 (PR #308 by @xcdoge, the capsule half of PR #157 — authored by them: the sim / protocol / HUD hunks applied as reviewed): inTotal / killedInTotal / leakedInTotal / resolved, b.snap / BattleResult / b.progress (optional field) / m.public progress carry resolved, killed may exceed total (validation bounds it by the content-spawn allowance); dropped the PR's stray design-doc line — it had replaced the S→C b.end bullet of docs/design/network.md, which is back; the 联防 live-counter change mark follows the enemy list now that a spawn no longer moves total (uniteLeftMark + test); golden: only total / players.*.total / snaps move, outcomes bit-identical — roster 23 (roster-002 003 007 008 009 010 011 012 013 015 017 018 020 023 024 025 026 027 037 045 047 048 049), bonds 2 (bond-maniShip-low, bond-soloShip-high), fields 11 (boss-boss_1 3 5 pair+solo, boss-boss_6-solo, hidden-boss_8 10 pair+solo), diy 11 (diy-036 040 056 062 068 073 083 091 095 114 115), matches 12 (solo-FUNNY-1 NORMAL-1 HARD-1 ABYSS-1 ABYSS-2, coop2-NORMAL-3 coop2-HARD-4 coop3-ABYSS-5 coop4-ABYSS-7 coop2-NORMAL-8-serverrun solo-HARD-9-boosted coop2-NORMAL-70-diy: the total column of 36 round rows), standins unchanged
2026-10-08 19:30:36 +08:00

96 lines
6.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Combat HUD (research 06 §11.3/§11.7, research 09 §3.1 / §6.3): DP counter at the right edge and the bottom-centre
// pills.
// Client-side combat (`client` prop, DESIGN §14 — the official behaviour):
// * own normal battle over: "⌛ 作战结束,等待队友完成作战" with each teammate's live progress (kills / total, ✓);
// * observing a teammate (team row → 前往查看): "👁 name" + 返回战场;
// * 联防 / 最终攻势: the ‹ › pill switches the camera LEFT half / 全景 / RIGHT half ("你自己" / name / "全景"); it
// replaces the observing pill on such a field watched with 前往查看 (a 联防 leaker, an eliminated spectator — the
// official non-helper pill "‹ 👁 helper# ›"), with 返回战场 beside the arrows (DESIGN §20.15);
// * there is NO view switcher during normal combat.
// Server-run combat (legacy streaming mode): the ‹ 自己 › switcher cycling the live fields of m.public.fields.
import { html, Icon } from './components.js';
import { DpCounter } from './hud.js';
import { GIcon } from './gameComponents.js';
import { switcherLabel, cycleField } from './gameLogic.js';
import { t } from '../../../shared/i18n.js';
const cx = (...p) => p.flat().filter(Boolean).join(' ');
/** Teammates' progress under the waiting pill (css/screens/game.css .chud__progress / .chud__prog). */
function ProgressList({ list }) {
if (!Array.isArray(list) || !list.length) return null;
return html`<div class="chud__progress" role="list" aria-label=${t('队友作战进度')}>
${list.map((p) => html`<span key=${p.playerId} role="listitem" class=${cx('chud__prog', p.done && 'is-done')}>
<b>${p.name}</b>
${p.done
? html`<span class="chud__prog__ok" aria-label=${t('作战结束')}>✓</span>`
: html`<span class="num">${(p.resolved ?? p.killed) != null && p.total != null ? `${p.resolved ?? p.killed}/${p.total}` : '•••'}</span>`}
</span>`)}
</div>`;
}
/**
* @param {{ pub:any, myId:string, watching:string|null, hud:any, myDone:boolean, onWatch:(fieldId:string)=>void, spectating?: boolean,
* client?: null | { progress?: any[]|null, observing?: { name: string }|null, onBack?: () => void,
* layers?: Array<{ key: string, label: string, self: boolean, watch: boolean }>, layer?: string, onLayer?: (k: string) => void } }} props
*/
export function CombatHud({ pub, myId, watching, hud, myDone, onWatch, spectating = false, spectator = false, client = null }) {
// `spectator`: a spectator seat (community report #26) watches like an eliminated player, under its own caption
if (client) return ClientHud({ hud, myDone, spectating, spectator, client });
const fields = (Array.isArray(pub?.fields) ? pub.fields : []).filter((f) => f && f.live !== false);
// the watched field may already have finished (not live): still name it
const label = switcherLabel(pub, watching, myId, spectating);
const canCycle = fields.length > 1;
return html`<div class="chud">
<${DpCounter} dp=${hud?.dp} />
<div class="chud__bottom">
${myDone ? html`<div class="chud__msg" role="status"><${Icon} name="hourglass" /><span>${t('作战结束,等待队友完成作战')}</span></div>` : null}
${spectating ? (spectator ? html`<div class="chud__msg" role="status"><${GIcon} name="eye" /><span>${t('观战中')}</span></div>`
: html`<div class="chud__msg chud__msg--dead" role="status"><${Icon} name="close" /><span>${t('你已被淘汰,正在观战')}</span></div>`) : null}
${fields.length ? html`<div class=${cx('vswitch', !canCycle && 'is-single')}>
<button type="button" class="vswitch__arrow" disabled=${!canCycle} aria-label=${t('上一个战场')}
onClick=${() => { const n = cycleField(fields, watching, -1); if (n && n !== watching) onWatch(n); }}><${Icon} name="chevronLeft" /></button>
<span class="vswitch__label">${label}</span>
<button type="button" class="vswitch__arrow" disabled=${!canCycle} aria-label=${t('下一个战场')}
onClick=${() => { const n = cycleField(fields, watching, 1); if (n && n !== watching) onWatch(n); }}><${Icon} name="chevronRight" /></button>
</div>` : null}
</div>
</div>`;
}
function ClientHud({ hud, myDone, spectating, spectator = false, client }) {
const layers = Array.isArray(client.layers) ? client.layers : [];
const idx = Math.max(0, layers.findIndex((l) => l.key === (client.layer || 'ALL')));
const cur = layers[idx] || null;
const step = (d) => {
if (!layers.length || !client.onLayer) return;
const n = Math.min(layers.length - 1, Math.max(0, idx + d));
if (n !== idx) client.onLayer(layers[n].key);
};
const observing = client.observing;
const halves = layers.length > 0;
return html`<div class="chud">
<${DpCounter} dp=${hud?.dp} />
<div class="chud__bottom">
${myDone && !observing ? html`<div class="chud__msg chud__wait" role="status"><${Icon} name="hourglass" /><span>${t('作战结束,等待队友完成作战')}</span></div>
<${ProgressList} list=${client.progress} />` : null}
${spectating && !observing ? (spectator ? html`<div class="chud__msg" role="status"><${GIcon} name="eye" /><span>${t('观战中,可点击左侧成员头像前往查看')}</span></div>`
: html`<div class="chud__msg chud__msg--dead" role="status"><${Icon} name="close" /><span>${t('你已被淘汰,可点击队友头像前往查看')}</span></div>`) : null}
${observing && !halves ? html`<div class="vswitch chud__observe is-single" role="status">
<span class="vswitch__label"><${GIcon} name="eye" /><span>${observing.name}</span></span>
${client.onBack ? html`<button type="button" class="btn btn--secondary btn--sm chud__back" onClick=${() => client.onBack()}>
<span class="btn__label">${t('返回战场')}</span></button>` : null}
</div>` : null}
${halves ? html`<div class=${cx('vswitch', 'chud__layers', observing && 'is-observing')}>
<button type="button" class="vswitch__arrow" disabled=${idx <= 0} aria-label=${t('左侧战场')} onClick=${() => step(-1)}><${Icon} name="chevronLeft" /></button>
<span class="vswitch__label">
${cur && cur.watch ? html`<${GIcon} name="eye" />` : null}<span>${cur ? cur.label : t('全景')}</span></span>
<button type="button" class="vswitch__arrow" disabled=${idx >= layers.length - 1} aria-label=${t('右侧战场')} onClick=${() => step(1)}><${Icon} name="chevronRight" /></button>
${observing && client.onBack ? html`<button type="button" class="btn btn--secondary btn--sm chud__back" onClick=${() => client.onBack()}>
<span class="btn__label">${t('返回战场')}</span></button>` : null}
</div>` : null}
</div>
</div>`;
}