feat(HUD): 添加HUD界面资产与组件
- 新增HUD图标、面板和状态指示器资产 - 实现HUD图标组件和错误边界组件 - 重构顶部状态栏和底部导航栏 - 更新路线面板样式和交互 - 添加HUD资产清单和切片脚本 - 移除未使用的资产文件 - 调整API安全配置和生产环境设置
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user