feat(HUD): 添加HUD界面资产与组件

- 新增HUD图标、面板和状态指示器资产
- 实现HUD图标组件和错误边界组件
- 重构顶部状态栏和底部导航栏
- 更新路线面板样式和交互
- 添加HUD资产清单和切片脚本
- 移除未使用的资产文件
- 调整API安全配置和生产环境设置
This commit is contained in:
2026-04-29 01:24:37 +08:00
parent 6234a50bdb
commit 5c41eb410b
71 changed files with 2492 additions and 1028 deletions
@@ -1,7 +1,16 @@
import type { GameAction, GameView } from '@tinywaste/game-core';
import { edgeDestination, getActiveQuestCount, getCurrentPlace, getExpeditionStatus, getRiskDots, getRouteRiskTier } from '../hudModel';
import { useState } from 'react';
import {
edgeDestination,
getCurrentPlace,
getExpeditionStatus,
getPlaceAtmosphere,
getRiskDots,
getRouteRiskTier,
getVisibleRoutes,
} from '../hudModel';
import { getPlaceThumbStyle } from '../uiAssets';
import { PanelHeader } from '../../shared/components/PanelHeader';
import { HudIcon } from './HudIcon';
export function RoutePanel({
isWorking,
@@ -12,100 +21,121 @@ export function RoutePanel({
onSendAction: (action: GameAction) => void;
view: GameView;
}) {
const [safeOnly, setSafeOnly] = useState(false);
const currentPlace = getCurrentPlace(view);
const activeQuestCount = getActiveQuestCount(view);
const placeId = currentPlace?.id ?? view.place.id;
const expedition = getExpeditionStatus(view);
const atmosphere = getPlaceAtmosphere(placeId);
const allRoutes = getVisibleRoutes(view, 10);
const routes = safeOnly
? allRoutes.filter((edge) => !edge.blockedReason && getRouteRiskTier(edge.risk).tone === 'safe')
: allRoutes.slice(0, 3);
const launchRoute = routes[0] ?? null;
return (
<section className="panel panel-route expedition-panel">
<header className="expedition-head">
<section className="panel panel-route expedition-panel hud-shell-panel">
<header className="expedition-panel-head">
<div>
<p className="eyebrow">Expedition rail</p>
<p className="eyebrow"></p>
<h2>{currentPlace?.name ?? view.header.placeName}</h2>
</div>
<span className={`hud-chip ${expedition.tone === 'good' ? 'safe' : expedition.tone}`}>
{view.header.riskLabel}
</span>
<span className={`hud-chip ${expedition.tone === 'good' ? 'safe' : expedition.tone}`}>{view.header.riskLabel}</span>
</header>
<article className="expedition-core">
<span className="intel-kicker">CURRENT SECTOR</span>
<p>{view.header.placeDesc}</p>
<div className="expedition-status-grid">
<article className="expedition-status-card">
<small></small>
<strong>{expedition.readiness}%</strong>
</article>
<article className="expedition-status-card">
<small></small>
<strong>{view.player.inventoryUsage}/{view.player.inventoryCapacity}</strong>
</article>
<article className="expedition-status-card">
<small></small>
<strong>{expedition.threatCount || activeQuestCount}</strong>
</article>
<section className="expedition-location-card">
<p className="eyebrow"></p>
<strong>{view.header.placeName}</strong>
<div className="expedition-weather-block">
<div className="expedition-weather-main">
<HudIcon name="weather" className="expedition-weather-icon" />
<span>{atmosphere.weather}</span>
</div>
<div className="expedition-weather-meta">
<span>{atmosphere.temperature}</span>
<span>{atmosphere.wind}</span>
</div>
</div>
<div className="intel-meta">
{(currentPlace?.tags ?? []).map((tag) => (
<span key={tag} className="hud-chip muted">
{tag}
</span>
))}
</div>
</article>
<section className="route-stack">
<PanelHeader title="Routes" subtitle="选择下一段远行目标" compact />
<div className="route-list expedition-route-list">
{view.map.edges.map((edge) => {
const destination = edgeDestination(view, edge);
if (!destination) return null;
const riskTier = getRouteRiskTier(edge.risk);
const riskDots = getRiskDots(edge.risk);
return (
<button
key={`${edge.from}-${edge.to}`}
className={`route-card expedition-route-card tone-${riskTier.tone}`}
disabled={Boolean(edge.blockedReason) || isWorking}
onClick={() => onSendAction({ type: 'travel', toPlaceId: edge.to })}
>
<div className="route-card-thumb" style={getPlaceThumbStyle(destination.id)} />
<div className="route-card-copy">
<strong>{destination.name}</strong>
<p>{destination.desc}</p>
<div className="route-card-detail">
<span>{edge.travelTimeMin} </span>
<span>{riskTier.label}</span>
</div>
<div className="risk-dots" aria-hidden="true">
{riskDots.map((active, index) => (
<span key={`${destination.id}-${index}`} className={active ? 'active' : ''} />
))}
</div>
</div>
<div className="route-card-state">
<span>{destination.visited ? '已探明' : '未知'}</span>
{edge.blockedReason ? <em>{edge.blockedReason}</em> : null}
</div>
</button>
);
})}
<div className="expedition-risk-line">
<span></span>
<strong>{view.header.riskLabel}</strong>
</div>
</section>
<section className="world-map-panel expedition-map-panel">
<PanelHeader title="Known sectors" subtitle="压缩后的节点情报" compact />
<div className="map-node-grid expedition-map-grid">
{view.map.places.map((place) => (
<article key={place.id} className={`map-node-card ${place.current ? 'current' : ''}`}>
<strong>{place.name}</strong>
<span>{place.visited ? '已探明' : '待深入'}</span>
<small>{place.tags.join(' · ') || 'unknown'}</small>
</article>
))}
<section className="expedition-routes-card">
<div className="expedition-section-head">
<span>线</span>
<button className={`expedition-toggle ${safeOnly ? 'active' : ''}`} type="button" onClick={() => setSafeOnly((prev) => !prev)}>
{safeOnly ? '显示全部' : '仅安全路线'}
</button>
</div>
<div className="expedition-route-stack">
{routes.map((edge, index) => {
const destination = edgeDestination(view, edge);
if (!destination) return null;
const riskTier = getRouteRiskTier(edge.risk);
const riskDots = getRiskDots(edge.risk);
return (
<button
key={`${edge.from}-${edge.to}`}
className={`expedition-route-card ${index === 0 ? 'active' : ''} tone-${riskTier.tone}`}
disabled={Boolean(edge.blockedReason) || isWorking}
onClick={() => onSendAction({ type: 'travel', toPlaceId: edge.to })}
type="button"
>
<div className="route-card-thumb" style={getPlaceThumbStyle(destination.id)} />
<div className="route-card-copy">
<div className="route-card-topline">
<strong>{destination.name}</strong>
<span>{destination.visited ? '已探明' : '未知'}</span>
</div>
<p>{destination.desc}</p>
<div className="route-card-detail">
<span>{edge.travelTimeMin} </span>
<span>{riskTier.label}</span>
</div>
<div className="risk-dots" aria-hidden="true">
{riskDots.map((active, dotIndex) => (
<span key={`${destination.id}-${dotIndex}`} className={active ? 'active' : ''} />
))}
</div>
</div>
</button>
);
})}
</div>
</section>
<section className="expedition-status-card">
<div className="expedition-section-head">
<span></span>
</div>
<div className="expedition-status-grid">
<article className="expedition-status-cell">
<small></small>
<strong>{expedition.readiness}%</strong>
</article>
<article className="expedition-status-cell">
<small></small>
<strong>{Math.round((view.player.stats.life / view.player.maxStats.life) * 100)}%</strong>
</article>
<article className="expedition-status-cell">
<small></small>
<strong>
{view.player.inventoryUsage}/{view.player.inventoryCapacity}
</strong>
</article>
</div>
<button
className="primary-button expedition-launch-button"
disabled={!launchRoute || isWorking}
onClick={() => launchRoute && onSendAction({ type: 'travel', toPlaceId: launchRoute.to })}
type="button"
>
</button>
</section>
</section>
);