import { useEffect, useRef, useSyncExternalStore } from 'react' import type { Engine } from '../game/engine' import { GRID } from '../game/map' import type { HudState, Team } from '../game/types' const MINIMAP_PX = 4 // px per cell function fmtTime(t: number): string { const s = Math.max(0, Math.ceil(t)) return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}` } function Minimap({ engine }: { engine: Engine }) { const bgRef = useRef(null) const fgRef = useRef(null) useEffect(() => { const bg = bgRef.current! engine.map.drawMinimap(bg.getContext('2d')!, MINIMAP_PX) let raf = 0 const draw = () => { raf = requestAnimationFrame(draw) const fg = fgRef.current if (!fg) return const ctx = fg.getContext('2d')! const size = GRID * MINIMAP_PX ctx.clearRect(0, 0, size, size) const info = engine.minimapInfo() const toPx = (x: number, z: number): [number, number] => [ (x / 2.5 + GRID / 2) * MINIMAP_PX, (z / 2.5 + GRID / 2) * MINIMAP_PX, ] // bomb const bomb = info.bomb let bombPos = bomb.plantedPos ?? bomb.droppedPos if (!bombPos && bomb.carrierId !== null) { const carrier = info.soldiers[bomb.carrierId] if (carrier && (carrier.team === info.playerTeam || carrier.lastSeenBy[info.playerTeam] > info.time - 2)) { bombPos = carrier.pos } } if (bombPos && (bomb.plantedPos ? Math.sin(info.time * 10) > -0.4 : true)) { const [bx, bz] = toPx(bombPos.x, bombPos.z) ctx.fillStyle = '#ff7828' ctx.fillRect(bx - 4, bz - 4, 8, 8) ctx.fillStyle = '#000' ctx.font = 'bold 7px sans-serif' ctx.textAlign = 'center' ctx.fillText('C4', bx, bz + 2.5) } for (const s of info.soldiers) { if (!s.alive) continue const mine = s.team === info.playerTeam const seen = s.lastSeenBy[info.playerTeam] > info.time - 1.6 if (!mine && !seen) continue const [px, pz] = toPx(s.pos.x, s.pos.z) if (s.id === info.viewedId) { // white arrow with view direction ctx.save() ctx.translate(px, pz) ctx.rotate(-s.yaw) ctx.fillStyle = '#ffffff' ctx.beginPath() ctx.moveTo(0, -6) ctx.lineTo(4.4, 4.4) ctx.lineTo(0, 2) ctx.lineTo(-4.4, 4.4) ctx.closePath() ctx.fill() ctx.restore() } else { ctx.fillStyle = mine ? '#57c46a' : '#e84c3d' ctx.beginPath() ctx.arc(px, pz, 3.4, 0, Math.PI * 2) ctx.fill() } } } raf = requestAnimationFrame(draw) return () => cancelAnimationFrame(raf) }, [engine]) const size = GRID * MINIMAP_PX return (
) } function Crosshair({ st }: { st: HudState }) { if (st.me.scoped) return null if (!st.me.alive) return null if (st.me.weaponId === 'awp') { // unscoped AWP: big circle = inaccurate return
} const gap = Math.min(40, st.me.spreadPx) const len = 9 return (
) } function Scope() { return (
) } function TeamTag({ team }: { team: Team }) { return {team} } export default function HUD({ engine }: { engine: Engine }) { useSyncExternalStore(engine.hud.subscribe, engine.hud.getSnapshot) const st = engine.hud.state const now = engine.time if (st.phase === 'menu') { return (

DUST II

5v5 炸弹模式 · 三维第一人称 · 纯代码生成

WASD 移动空格 跳跃Shift 静步 鼠标左键 开火右键 AWP 开镜R 换弹 1/2/3 切枪E 安放 / 拆除B 购买阶段换 AWP
) } const dmgFlash = now - st.damageAt < 0.4 const hitAge = now - st.hitmarkerAt return (
{st.me.scoped && } {hitAge < 0.25 && (
)} {dmgFlash &&
} {/* top bar */}
{st.ctScore}
{st.bombPlanted ? 💣 {Math.ceil(st.bombTimeLeft ?? 0)} : {st.phase === 'freeze' ? fmtTime(st.freezeLeft) : fmtTime(st.timeLeft)}}
回合 {st.round}{st.pistolRound ? ' · 手枪局' : ''}
{st.tScore}
{/* killfeed */}
{st.killfeed.map(k => (
{k.attacker} {k.weapon}{k.headshot ? ' ◎' : ''} {k.victim}
))}
{/* bottom left: hp / armor */} {st.me.alive && (
+ {st.me.hp}
🛡 {st.me.armor}
{st.me.hasBomb &&
携带 C4
}
)} {/* bottom right: ammo */} {st.me.alive && (
{st.me.weaponName}{st.me.reloading ? '(换弹中…)' : ''}
{st.me.weaponId !== 'knife' && (
{st.me.mag} / {st.me.reserve}
)}
)} {/* center messages */}
{st.phase === 'freeze' && (
准备阶段 · {Math.ceil(st.freezeLeft)}s
{!st.pistolRound &&
按 B 切换主武器:当前 {st.loadout === 'awp' ? 'AWP' : '步枪'}
} {st.pistolRound &&
手枪局 — {st.playerTeam === 'T' ? 'Glock-18' : 'USP-S'},无护甲
}
)} {st.channel && (
{st.channel.label}
)} {st.centerHint && !st.channel &&
{st.centerHint}
} {st.bombPlanted && st.phase === 'live' &&
C4 已安放
} {st.phase === 'over' && st.winner && (
{st.winner} 获胜
{st.winReason}
)}
{/* spectate bar */} {st.spectating && st.phase === 'live' && (
{st.spectating.isSelf ? 你已阵亡 — 正在寻找存活队友… : <> 观战中:{st.spectating.name} ←/→ 或 左键 切换 · E 接管该队友 }
)} {/* pause overlay (pointer unlocked mid-game) */} {!st.locked && !st.auto && st.phase !== 'matchover' && (
engine.requestLock()}>

已暂停 / 鼠标未锁定

点击任意处继续(锁定鼠标)

)} {st.phase === 'matchover' && st.matchWinner && (

比赛结束 — {st.matchWinner} {st.matchWinner === st.playerTeam ? '胜利 🏆' : '获胜'}

CT {st.ctScore} : {st.tScore} T

)}
) }