feat: 实现游戏核心功能与前端界面
- 添加游戏核心系统:战斗、任务、日志、随机数生成等 - 实现前端主界面、HUD、底部导航和各类卡片组件 - 添加用户认证系统与游戏存档持久化 - 配置项目基础架构与开发环境 - 补充文档说明与Docker部署支持
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
import type { GameAction, GameView } from '@tinywaste/game-core';
|
||||
import { edgeDestination, getActiveQuestCount, getCurrentPlace, getRiskDots, getRouteRiskTier } from '../hudModel';
|
||||
import { getPlaceThumbStyle } from '../uiAssets';
|
||||
import { PanelHeader } from '../../shared/components/PanelHeader';
|
||||
|
||||
export function RoutePanel({
|
||||
isWorking,
|
||||
onSendAction,
|
||||
view,
|
||||
}: {
|
||||
isWorking: boolean;
|
||||
onSendAction: (action: GameAction) => void;
|
||||
view: GameView;
|
||||
}) {
|
||||
const currentPlace = getCurrentPlace(view);
|
||||
const activeQuestCount = getActiveQuestCount(view);
|
||||
|
||||
return (
|
||||
<section className="panel panel-route">
|
||||
<PanelHeader title="Current Location" subtitle="路线、风险、耗时全部固定在当前视口内" />
|
||||
|
||||
<article className="route-summary-card">
|
||||
<span className="intel-kicker">CURRENT SECTOR</span>
|
||||
<h2>{currentPlace?.name ?? view.header.placeName}</h2>
|
||||
<p>{view.header.placeDesc}</p>
|
||||
<div className="route-summary-meta">
|
||||
<div>
|
||||
<small>风险</small>
|
||||
<strong>{view.header.riskLabel}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<small>服务</small>
|
||||
<strong>{view.place.services.length || 1} 项</strong>
|
||||
</div>
|
||||
<div>
|
||||
<small>任务</small>
|
||||
<strong>{activeQuestCount}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div className="intel-meta">
|
||||
{(currentPlace?.tags ?? []).map((tag) => (
|
||||
<span key={tag} className="hud-chip muted">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<div className="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 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>
|
||||
|
||||
<section className="world-map-panel">
|
||||
<PanelHeader title="World Map" subtitle="节点式世界图用于压缩表达地理推进" compact />
|
||||
<div className="map-node-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>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user