feat: 实现游戏核心功能与前端界面
- 添加游戏核心系统:战斗、任务、日志、随机数生成等 - 实现前端主界面、HUD、底部导航和各类卡片组件 - 添加用户认证系统与游戏存档持久化 - 配置项目基础架构与开发环境 - 补充文档说明与Docker部署支持
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
import type { EdgeView, GameView, InventoryViewEntry, QuestView } from '@tinywaste/game-core';
|
||||
import { getEquipmentArt, getItemArt } from './uiAssets';
|
||||
|
||||
export interface EquipmentPreviewEntry {
|
||||
id: string;
|
||||
label: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
art?: string;
|
||||
isEquipped: boolean;
|
||||
}
|
||||
|
||||
export function edgeDestination(view: GameView, edge: EdgeView) {
|
||||
return view.map.places.find((place) => place.id === edge.to);
|
||||
}
|
||||
|
||||
export function getTimeLabels(timeLabel: string) {
|
||||
const timeSegments = timeLabel.split('·').map((part) => part.trim());
|
||||
return {
|
||||
dayLabel: timeSegments[0] ?? 'Day 1',
|
||||
clockLabel: timeSegments[1] ?? timeLabel,
|
||||
};
|
||||
}
|
||||
|
||||
export function getActiveQuestCount(view: GameView) {
|
||||
return view.quests.filter((quest) => quest.state === 'active').length;
|
||||
}
|
||||
|
||||
export function getAlertCount(view: GameView) {
|
||||
return Number(Boolean(view.pendingEvent)) + Number(Boolean(view.combat));
|
||||
}
|
||||
|
||||
export function getCurrentPlace(view: GameView) {
|
||||
return view.map.places.find((place) => place.current) ?? null;
|
||||
}
|
||||
|
||||
export function getVisibleInventory(view: GameView, limit = 6) {
|
||||
return view.player.inventory.slice(0, limit);
|
||||
}
|
||||
|
||||
export function getObjectiveRows(view: GameView) {
|
||||
return view.quests
|
||||
.filter((quest) => quest.state === 'active' || quest.state === 'completed')
|
||||
.map((quest) => {
|
||||
const doneCount = quest.steps.filter((step) => step.done).length;
|
||||
const nextStep = quest.steps.find((step) => !step.done)?.text ?? quest.steps.at(-1)?.text ?? '待机';
|
||||
|
||||
return {
|
||||
id: quest.id,
|
||||
title: quest.title,
|
||||
detail: nextStep,
|
||||
progress: `${doneCount}/${quest.steps.length}`,
|
||||
done: quest.state === 'completed',
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function getAttributeRows(view: GameView) {
|
||||
const { attributes } = view.player;
|
||||
|
||||
return [
|
||||
{ label: 'STR', name: '力量', value: attributes.str },
|
||||
{ label: 'AGI', name: '敏捷', value: attributes.agi },
|
||||
{ label: 'INT', name: '智力', value: attributes.int },
|
||||
{ label: 'PER', name: '感知', value: attributes.per },
|
||||
{ label: 'LCK', name: '幸运', value: attributes.luck },
|
||||
];
|
||||
}
|
||||
|
||||
export function getStatusEffects(view: GameView) {
|
||||
const { stats, maxStats } = view.player;
|
||||
const effects: { id: string; label: string; detail: string; tone: 'good' | 'warn' | 'danger' }[] = [];
|
||||
|
||||
if (stats.life / maxStats.life <= 0.55) {
|
||||
effects.push({ id: 'life', label: '创伤', detail: '生命处于低位', tone: 'danger' });
|
||||
}
|
||||
if (stats.thirst / maxStats.thirst <= 0.5) {
|
||||
effects.push({ id: 'thirst', label: '脱水', detail: '口渴已压缩行动余量', tone: 'warn' });
|
||||
}
|
||||
if (stats.hunger / maxStats.hunger <= 0.5) {
|
||||
effects.push({ id: 'hunger', label: '饥饿', detail: '需要补充稳定热量', tone: 'warn' });
|
||||
}
|
||||
if (stats.energy / maxStats.energy <= 0.45) {
|
||||
effects.push({ id: 'energy', label: '疲劳', detail: '精力不足,适合休整', tone: 'warn' });
|
||||
}
|
||||
if (stats.sanity / maxStats.sanity <= 0.45) {
|
||||
effects.push({ id: 'sanity', label: '精神波动', detail: '理智偏低,事件风险提升', tone: 'danger' });
|
||||
}
|
||||
if (stats.radiation / maxStats.radiation >= 0.32) {
|
||||
effects.push({ id: 'radiation', label: '污染累积', detail: '辐射正在侵蚀身体', tone: 'danger' });
|
||||
}
|
||||
|
||||
if (!effects.length) {
|
||||
effects.push({ id: 'stable', label: '稳定', detail: '当前幸存者状态平稳', tone: 'good' });
|
||||
}
|
||||
|
||||
return effects.slice(0, 4);
|
||||
}
|
||||
|
||||
export function getSupplyCounts(view: GameView) {
|
||||
const counts = {
|
||||
water: 0,
|
||||
food: 0,
|
||||
medicine: 0,
|
||||
material: 0,
|
||||
};
|
||||
|
||||
for (const item of view.player.inventory) {
|
||||
if (item.type === 'water') counts.water += item.count;
|
||||
if (item.type === 'food') counts.food += item.count;
|
||||
if (item.type === 'medicine') counts.medicine += item.count;
|
||||
if (item.type === 'material') counts.material += item.count;
|
||||
}
|
||||
|
||||
return counts;
|
||||
}
|
||||
|
||||
export function getKitItem(view: GameView) {
|
||||
return view.player.inventory.find((item) => item.type === 'medicine' || item.type === 'water') ?? null;
|
||||
}
|
||||
|
||||
export function getLoadoutEntries(view: GameView) {
|
||||
const inventoryById = new Map(view.player.inventory.map((item) => [item.itemId, item]));
|
||||
const weaponItem = view.player.equipment.weapon ? inventoryById.get(view.player.equipment.weapon) : undefined;
|
||||
const bodyItem = view.player.equipment.body ? inventoryById.get(view.player.equipment.body) : undefined;
|
||||
const toolItem = view.player.equipment.tool ? inventoryById.get(view.player.equipment.tool) : undefined;
|
||||
const kitItem = getKitItem(view);
|
||||
|
||||
const left: EquipmentPreviewEntry[] = [
|
||||
{
|
||||
id: 'head',
|
||||
label: 'Head',
|
||||
title: '遮面兜帽',
|
||||
subtitle: '造型预置位',
|
||||
art: getEquipmentArt(undefined, 'head'),
|
||||
isEquipped: false,
|
||||
},
|
||||
{
|
||||
id: 'body',
|
||||
label: 'Body',
|
||||
title: bodyItem?.name ?? '未装备护甲',
|
||||
subtitle: bodyItem ? '已挂载在躯干槽' : '建议保留一件抗风装备',
|
||||
art: getEquipmentArt(bodyItem?.itemId, 'body'),
|
||||
isEquipped: Boolean(bodyItem),
|
||||
},
|
||||
{
|
||||
id: 'back',
|
||||
label: 'Back',
|
||||
title: '远行背包',
|
||||
subtitle: `容量 ${view.player.inventoryUsage}/${view.player.inventoryCapacity}`,
|
||||
art: getEquipmentArt(undefined, 'back'),
|
||||
isEquipped: true,
|
||||
},
|
||||
];
|
||||
|
||||
const right: EquipmentPreviewEntry[] = [
|
||||
{
|
||||
id: 'weapon',
|
||||
label: 'Weapon',
|
||||
title: weaponItem?.name ?? '未装备主武器',
|
||||
subtitle: weaponItem ? '武器槽在线' : '可通过蓝图制作远程装备',
|
||||
art: getEquipmentArt(weaponItem?.itemId, 'weapon'),
|
||||
isEquipped: Boolean(weaponItem),
|
||||
},
|
||||
{
|
||||
id: 'tool',
|
||||
label: 'Tool',
|
||||
title: toolItem?.name ?? '未挂载工具',
|
||||
subtitle: toolItem ? '工具槽在线' : '撬棍可作为早期万能工具',
|
||||
art: getEquipmentArt(toolItem?.itemId, 'tool'),
|
||||
isEquipped: Boolean(toolItem),
|
||||
},
|
||||
{
|
||||
id: 'kit',
|
||||
label: 'Kit',
|
||||
title: kitItem?.name ?? '未配置急救包',
|
||||
subtitle: kitItem ? `携带 ${kitItem.count} 件应急补给` : '建议随身携带水或药物',
|
||||
art: kitItem ? getItemArt(kitItem.itemId, kitItem.type) : getEquipmentArt(undefined, 'kit'),
|
||||
isEquipped: Boolean(kitItem),
|
||||
},
|
||||
];
|
||||
|
||||
return { left, right };
|
||||
}
|
||||
|
||||
export function getRouteRiskTier(risk: number) {
|
||||
if (risk <= 0.35) return { label: '低风险', tone: 'safe' as const };
|
||||
if (risk <= 0.65) return { label: '中风险', tone: 'warn' as const };
|
||||
return { label: '高风险', tone: 'danger' as const };
|
||||
}
|
||||
|
||||
export function getRiskDots(risk: number) {
|
||||
const lit = Math.max(1, Math.min(6, Math.round(risk * 6)));
|
||||
return Array.from({ length: 6 }, (_, index) => index < lit);
|
||||
}
|
||||
|
||||
export function getInventoryStateLabel(item: InventoryViewEntry) {
|
||||
if (item.equipped) return '已装备';
|
||||
if (item.canUse) return '可立即使用';
|
||||
if (item.equipSlot) return '可挂载';
|
||||
return '物资储备';
|
||||
}
|
||||
|
||||
export function getQuestProgress(quest: QuestView) {
|
||||
return `${quest.steps.filter((step) => step.done).length}/${quest.steps.length}`;
|
||||
}
|
||||
Reference in New Issue
Block a user