feat: 实现避难所仓储系统与生存状态HUD改进

- 新增避难所仓储功能,支持物品在随身背包与仓储间转移
- 添加生存状态图标资源与状态阈值告警系统
- 重构HUD界面,优化状态显示与操作流程
- 扩展游戏核心系统,增加仓储相关动作和校验
- 更新文档说明仓储规则与实现细节
This commit is contained in:
2026-04-28 22:58:49 +08:00
parent e783e6e533
commit 6234a50bdb
27 changed files with 1455 additions and 1024 deletions
+110 -23
View File
@@ -1,5 +1,5 @@
import type { EdgeView, GameView, InventoryViewEntry, QuestView } from '@tinywaste/game-core';
import { getEquipmentArt, getItemArt } from './uiAssets';
import { getEquipmentArt, getItemArt, getStatusArt } from './uiAssets';
export interface EquipmentPreviewEntry {
id: string;
@@ -30,14 +30,45 @@ export function getAlertCount(view: GameView) {
return Number(Boolean(view.pendingEvent)) + Number(Boolean(view.combat));
}
export function getDominantAlert(view: GameView) {
return view.survival.alerts[0] ?? null;
}
export function getCurrentPlace(view: GameView) {
return view.map.places.find((place) => place.current) ?? null;
}
export function getLatestLog(view: GameView) {
return view.logs[0] ?? null;
}
export function getVisibleInventory(view: GameView, limit = 6) {
return view.player.inventory.slice(0, limit);
}
export function getQuickUseItems(view: GameView, limit = 6) {
const priority = {
water: 0,
food: 1,
medicine: 2,
tool: 3,
material: 4,
weapon: 5,
armor: 6,
ammo: 7,
quest: 8,
} as const;
return [...view.player.inventory]
.filter((item) => item.canUse)
.sort((left, right) => {
const priorityDelta = priority[left.type] - priority[right.type];
if (priorityDelta !== 0) return priorityDelta;
return right.count - left.count;
})
.slice(0, limit);
}
export function getObjectiveRows(view: GameView) {
return view.quests
.filter((quest) => quest.state === 'active' || quest.state === 'completed')
@@ -55,6 +86,10 @@ export function getObjectiveRows(view: GameView) {
});
}
export function getPrimaryObjective(view: GameView) {
return getObjectiveRows(view)[0] ?? null;
}
export function getAttributeRows(view: GameView) {
const { attributes } = view.player;
@@ -68,35 +103,87 @@ export function getAttributeRows(view: GameView) {
}
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' });
}
const effects = view.survival.alerts.map((entry) => ({
id: entry.stat,
label: `${entry.label} · ${entry.summary}`,
detail: entry.impact,
recovery: entry.recovery,
art: getStatusArt(entry.stat),
tone:
entry.tier === 'strained' ? ('warn' as const) : entry.tier === 'safe' ? ('good' as const) : ('danger' as const),
}));
if (!effects.length) {
effects.push({ id: 'stable', label: '稳定', detail: '当前幸存者状态平稳', tone: 'good' });
return [
{
id: 'stable',
label: '稳定',
detail: '当前幸存者状态平稳,可以继续规划下一步行动。',
recovery: '保持补给循环与节奏即可。',
art: getStatusArt('life'),
tone: 'good' as const,
},
];
}
return effects.slice(0, 4);
}
export function getExpeditionStatus(view: GameView) {
const { stats, maxStats } = view.player;
const readiness = Math.round(
(stats.life / maxStats.life) * 34 +
(stats.energy / maxStats.energy) * 28 +
(stats.thirst / maxStats.thirst) * 20 +
(stats.hunger / maxStats.hunger) * 18,
);
const loadPercent = Math.round((view.player.inventoryUsage / view.player.inventoryCapacity) * 100);
const threatCount = view.survival.alerts.length;
return {
readiness,
loadPercent,
threatCount,
tone: readiness >= 74 && threatCount === 0 ? 'good' : readiness >= 48 ? 'warn' : 'danger',
};
}
export function getSurvivorMetrics(view: GameView) {
const protection = Math.min(
100,
Math.round(
(view.player.equipment.body ? 34 : 12) +
view.player.attributes.str * 5 +
(view.player.stats.life / view.player.maxStats.life) * 30,
),
);
const stealth = Math.min(
100,
Math.round(
view.player.attributes.agi * 8 +
view.player.attributes.per * 5 +
(view.player.stats.sanity / view.player.maxStats.sanity) * 20,
),
);
const mobility = Math.max(
0,
Math.min(
100,
Math.round(
view.player.attributes.agi * 7 +
(view.player.stats.energy / view.player.maxStats.energy) * 42 -
(view.player.inventoryUsage / view.player.inventoryCapacity) * 18,
),
),
);
return [
{ id: 'protection', label: '防护', value: protection },
{ id: 'stealth', label: '潜行', value: stealth },
{ id: 'mobility', label: '机动', value: mobility },
];
}
export function getSupplyCounts(view: GameView) {
const counts = {
water: 0,