HUD
React 游戏内 HUD:船体与护盾数值条、信用点与任务目标,以及快捷栏,栏中物品可实际使用,使用后进入冷却。
安装
# once per project: where @zzzui items come frompnpm dlx shadcn@latest registry add @zzzui=https://zzzui.catforest.dev/r/{name}.jsonpnpm dlx shadcn@latest add @zzzui/hud预览
Hull
Shield
12,480cr
Night shift
Current: Mark the drop zone
Current: Open the north gate
import { type ComponentType, type KeyboardEvent, useEffect, useState,} from "react";import { Counter } from "@/components/zero/counter";import { CutIn, CutInContent, CutInDescription, CutInEyebrow, CutInTitle,} from "@/components/zero/cut-in";import { Gauge, GaugeLabel, GaugeValue } from "@/components/zero/gauge";import { CellIcon } from "@/components/zero/icons/cell";import { FlareIcon } from "@/components/zero/icons/flare";import { KeyIcon } from "@/components/zero/icons/key";import { KitIcon } from "@/components/zero/icons/kit";import { Inventory, InventorySlot } from "@/components/zero/inventory";import { Objective, Objectives, ObjectivesHeader, ObjectiveTitle,} from "@/components/zero/objectives";/* * An in-game HUD over a scene: the hull and shield at the top left, credits and objectives at the * top right, a hotbar along the bottom. Using a slot (a click, or its number while the HUD has * focus) does what the item does and puts it on cooldown: the kit patches the hull, the cell * charges the shield, the flare and the key each finish an objective. Finishing them all pays out, * announced by a cut-in. The scene is a drawn backdrop; put your game behind it. */type Use = "kit" | "cell" | "flare" | "key";const bar: { id: Use; name: string; icon: ComponentType; rarity: "common" | "uncommon" | "legendary"; cooldown: number;}[] = [ { id: "kit", name: "Repair kit", icon: KitIcon, rarity: "uncommon", cooldown: 4000, }, { id: "cell", name: "Power cell", icon: CellIcon, rarity: "uncommon", cooldown: 3000, }, { id: "flare", name: "Signal flare", icon: FlareIcon, rarity: "common", cooldown: 2000, }, { id: "key", name: "Gate key", icon: KeyIcon, rarity: "legendary", cooldown: 0, },];/** A slot's key number in its corner, drawn from data-key so it is not part of its name. */const key = "before:absolute before:start-1.5 before:top-1 before:font-bold before:font-zero-text before:text-zero-text-3 before:text-zero-xs before:content-[attr(data-key)]";const goals = [ { id: "flare", title: "Mark the drop zone" }, { id: "key", title: "Open the north gate" },] as const;export function Hud() { const [hull, setHull] = useState(46); const [shield, setShield] = useState(1); const [stock, setStock] = useState({ kit: 3, cell: 6, flare: 12, key: 1 }); const [done, setDone] = useState<string[]>([]); const [credits, setCredits] = useState(12480); const [paid, setPaid] = useState(false); const [announce, setAnnounce] = useState(false); // when each slot's cooldown ends, and the clock that redraws the sweeps const [ready, setReady] = useState<Partial<Record<Use, number>>>({}); const [now, setNow] = useState(0); useEffect(() => { const waiting = Object.values(ready).some((at) => at && at > now); if (!waiting) return; const frame = requestAnimationFrame((t) => setNow(t)); return () => cancelAnimationFrame(frame); }, [ready, now]); const use = (slot: (typeof bar)[number]) => { if (stock[slot.id] === 0 || (ready[slot.id] ?? 0) > now) return; if (slot.id === "kit") setHull((h) => Math.min(120, h + 30)); if (slot.id === "cell") setShield((s) => Math.min(4, s + 1)); if (slot.id === "flare" || slot.id === "key") { const next = done.includes(slot.id) ? done : [...done, slot.id]; setDone(next); if (next.length === goals.length && !paid) { setPaid(true); setAnnounce(true); setCredits((c) => c + 3100); } } setStock((s) => ({ ...s, [slot.id]: s[slot.id] - 1 })); if (slot.cooldown) { const t = performance.now(); setNow(t); setReady((r) => ({ ...r, [slot.id]: t + slot.cooldown })); } }; const keys = (event: KeyboardEvent<HTMLElement>) => { const slot = bar[Number(event.key) - 1]; if (slot) use(slot); }; return ( <section aria-label="Heads-up display" onKeyDown={keys} className="grid min-h-[28rem] w-full max-w-6xl grid-rows-[auto_1fr_auto] gap-6 overflow-hidden border-(length:--zero-stroke) border-zero-ink p-4 sm:p-6" style={{ // the backdrop: a glow low in the accent, a floor grid, and a dark sky background: "radial-gradient(ellipse at 50% 115%, color-mix(in srgb, var(--color-zero-accent) 22%, transparent), transparent 60%), repeating-linear-gradient(90deg, color-mix(in srgb, var(--color-zero-text) 5%, transparent) 0 1px, transparent 1px 48px), linear-gradient(var(--color-zero-void), var(--color-zero-ground))", }} > <div className="flex flex-wrap items-start justify-between gap-6"> <div className="grid w-full max-w-64 gap-3 bg-[linear-gradient(to_right,rgb(0_0_0/0.72)_55%,transparent)] py-3 ps-4 pe-10"> <Gauge value={hull} max={120} low={0.25}> <GaugeLabel>Hull</GaugeLabel> <GaugeValue /> </Gauge> <Gauge value={shield} max={4} segments={4} tone="rare"> <GaugeLabel>Shield</GaugeLabel> <GaugeValue /> </Gauge> </div> <div className="grid justify-items-end gap-3"> <p className="flex items-baseline gap-1.5"> <Counter value={credits} delta className="text-zero-xl" /> <span className="font-black font-zero text-zero-accent italic"> cr </span> </p> <Objectives aria-label="Objectives" className="w-72"> <ObjectivesHeader>Night shift</ObjectivesHeader> {goals.map((goal) => ( <Objective key={goal.id} state={done.includes(goal.id) ? "done" : "active"} > <ObjectiveTitle>{goal.title}</ObjectiveTitle> </Objective> ))} </Objectives> </div> </div> <div /> <Inventory columns={6} aria-label="Hotbar" className="justify-self-center bg-[rgb(0_0_0/0.6)] p-2" > {bar.map((slot, index) => { const left = Math.max(0, (ready[slot.id] ?? 0) - now); return ( <InventorySlot key={slot.id} rarity={slot.rarity} count={stock[slot.id]} cooldown={ left && slot.cooldown ? left / slot.cooldown : undefined } disabled={stock[slot.id] === 0} aria-label={`${slot.name}, ${stock[slot.id]}`} aria-keyshortcuts={String(index + 1)} data-key={index + 1} onClick={() => use(slot)} className={key} > <slot.icon /> </InventorySlot> ); })} <InventorySlot aria-label="Empty" data-key={5} className={key} /> <InventorySlot aria-label="Empty" data-key={6} className={key} /> </Inventory> <CutIn open={announce} onOpenChange={setAnnounce}> <CutInContent> <CutInEyebrow>Run complete</CutInEyebrow> <CutInTitle>Night shift</CutInTitle> <CutInDescription>+3,100 cr</CutInDescription> </CutInContent> </CutIn> </section> );}行为
点击快捷栏中的物品格即可使用;HUD 获得焦点时,也可按对应的数字键。修理包修复船体,能量电池为护盾充能,信号弹与钥匙各完成一项目标;使用后,物品格上以扫过的遮罩显示冷却进度。两项目标全部完成后发放报酬,并以 Cut-in 宣告。
背景为代码绘制的占位(光晕与地面网格),实际使用时将游戏画面置于其后。在手机上,各面板上下堆叠。
由 Gauge、Counter、Objectives、Inventory、Cut-in 组成,安装此代码块时会一并安装这些组件。