e783e6e533
- 添加游戏核心系统:战斗、任务、日志、随机数生成等 - 实现前端主界面、HUD、底部导航和各类卡片组件 - 添加用户认证系统与游戏存档持久化 - 配置项目基础架构与开发环境 - 补充文档说明与Docker部署支持
247 lines
8.4 KiB
TypeScript
247 lines
8.4 KiB
TypeScript
import type {
|
|
ActionPreview,
|
|
EdgeView,
|
|
GameContent,
|
|
GameState,
|
|
GameView,
|
|
InventoryEntry,
|
|
InventoryViewEntry,
|
|
PendingEventView,
|
|
PlaceDefinition,
|
|
PlaceActionDefinition,
|
|
RecipeDefinition,
|
|
RecipeView,
|
|
TradeOfferView,
|
|
} from './types';
|
|
import { canAccessShelterStorage, buildSurvivalView } from './systems/survival-system';
|
|
import { copyStats, dangerLabel, formatTime } from './systems/shared';
|
|
import { getConditionFailure, getCurrentPlace, hasConditions, ensurePlaceRuntime } from './systems/place-system';
|
|
import { buildQuestView } from './systems/quest-system';
|
|
import { getContainerUsage, getInventoryUsage, getItemCount, getItemCountFromEntries, isItemEquipped } from './systems/inventory-system';
|
|
|
|
const buildInventoryEntries = (
|
|
entries: InventoryEntry[],
|
|
state: GameState,
|
|
content: GameContent,
|
|
): InventoryViewEntry[] =>
|
|
entries.map((entry) => {
|
|
const item = content.items[entry.itemId];
|
|
return {
|
|
...entry,
|
|
name: item.name,
|
|
type: item.type,
|
|
desc: item.desc,
|
|
volume: item.volume,
|
|
equipSlot: item.equipSlot,
|
|
canUse: Boolean(item.effects?.length),
|
|
equipped: isItemEquipped(state.player, entry.itemId),
|
|
};
|
|
});
|
|
|
|
const buildActionPreview = (
|
|
state: GameState,
|
|
content: GameContent,
|
|
action: PlaceActionDefinition,
|
|
): ActionPreview => {
|
|
const failed = action.requires?.find((rule) => !hasConditions(state, [rule]));
|
|
const runtime = ensurePlaceRuntime(state, content, state.player.placeId);
|
|
const remainingStock = action.stock ? runtime.stocks[action.id] ?? action.stock.initial : null;
|
|
|
|
let disabledReason = failed ? getConditionFailure(failed, content) : undefined;
|
|
if (!disabledReason && action.stock && remainingStock !== null && remainingStock <= 0) {
|
|
disabledReason = action.onEmptyText ?? '这里已经空了';
|
|
}
|
|
|
|
return {
|
|
id: action.id,
|
|
name: action.name,
|
|
kind: action.kind,
|
|
desc: action.desc,
|
|
timeCostMin: action.timeCostMin,
|
|
energyCost: action.energyCost,
|
|
risk: action.risk,
|
|
rewardHint: action.rewardHint,
|
|
disabledReason,
|
|
remainingStock,
|
|
};
|
|
};
|
|
|
|
const getRecipeOwnedCount = (state: GameState, recipeInputId: string) => {
|
|
const bagCount = getItemCount(state.player, recipeInputId);
|
|
if (!canAccessShelterStorage(state)) {
|
|
return bagCount;
|
|
}
|
|
|
|
return bagCount + getItemCountFromEntries(state.player.storage, recipeInputId);
|
|
};
|
|
|
|
const buildRecipeView = (state: GameState, content: GameContent, recipe: RecipeDefinition): RecipeView => {
|
|
const requirementFailure = recipe.requirements?.find((rule) => !hasConditions(state, [rule]));
|
|
const missing = recipe.inputs.find((input) => getRecipeOwnedCount(state, input.itemId) < input.count);
|
|
const craftable = state.player.knownRecipes.includes(recipe.id) && !requirementFailure && !missing;
|
|
|
|
return {
|
|
id: recipe.id,
|
|
name: recipe.name,
|
|
desc: recipe.desc,
|
|
timeCostMin: recipe.timeCostMin,
|
|
energyCost: recipe.energyCost,
|
|
craftable,
|
|
disabledReason:
|
|
!state.player.knownRecipes.includes(recipe.id)
|
|
? '尚未解锁'
|
|
: requirementFailure
|
|
? getConditionFailure(requirementFailure, content)
|
|
: missing
|
|
? `缺少 ${content.items[missing.itemId].name}`
|
|
: undefined,
|
|
inputs: recipe.inputs.map((input) => ({
|
|
itemId: input.itemId,
|
|
name: content.items[input.itemId].name,
|
|
count: input.count,
|
|
owned: getRecipeOwnedCount(state, input.itemId),
|
|
})),
|
|
outputs: recipe.outputs.map((output) => ({
|
|
itemId: output.itemId,
|
|
name: content.items[output.itemId].name,
|
|
count: output.count,
|
|
})),
|
|
};
|
|
};
|
|
|
|
const buildTradeView = (state: GameState, content: GameContent, place: PlaceDefinition): TradeOfferView[] =>
|
|
(place.tradeOffers ?? []).map((offer) => {
|
|
const missing = offer.costs.find((cost) => getItemCount(state.player, cost.itemId) < cost.count);
|
|
|
|
return {
|
|
id: offer.id,
|
|
name: offer.name,
|
|
desc: offer.desc,
|
|
available: !missing,
|
|
disabledReason: missing ? `缺少 ${content.items[missing.itemId].name}` : undefined,
|
|
gives: offer.gives.map((entry) => ({
|
|
itemId: entry.itemId,
|
|
name: content.items[entry.itemId].name,
|
|
count: entry.count,
|
|
})),
|
|
costs: offer.costs.map((entry) => ({
|
|
itemId: entry.itemId,
|
|
name: content.items[entry.itemId].name,
|
|
count: entry.count,
|
|
owned: getItemCount(state.player, entry.itemId),
|
|
})),
|
|
};
|
|
});
|
|
|
|
const buildPendingEventView = (state: GameState, content: GameContent): PendingEventView | null => {
|
|
if (!state.world.pendingEvent) return null;
|
|
const event = content.events[state.world.pendingEvent.eventId];
|
|
|
|
return {
|
|
id: event.id,
|
|
title: event.title,
|
|
text: event.text,
|
|
options: event.options.map((option) => {
|
|
const failed = option.conditions?.find((rule) => !hasConditions(state, [rule]));
|
|
return {
|
|
id: option.id,
|
|
text: option.text,
|
|
disabledReason: failed ? getConditionFailure(failed, content) : undefined,
|
|
};
|
|
}),
|
|
};
|
|
};
|
|
|
|
export const buildGameView = (state: GameState, content: GameContent): GameView => {
|
|
const place = getCurrentPlace(state, content);
|
|
const inventory = buildInventoryEntries(state.player.inventory, state, content);
|
|
const storage = buildInventoryEntries(state.player.storage, state, content).map((entry) => ({
|
|
...entry,
|
|
equipped: false,
|
|
}));
|
|
|
|
const edges: EdgeView[] = content.edges
|
|
.filter((edge) => edge.from === state.player.placeId)
|
|
.map((edge) => {
|
|
const failed = edge.conditions?.find((rule) => !hasConditions(state, [rule]));
|
|
return {
|
|
from: edge.from,
|
|
to: edge.to,
|
|
travelTimeMin: edge.travelTimeMin,
|
|
risk: edge.risk,
|
|
blockedReason: failed ? getConditionFailure(failed, content) : undefined,
|
|
};
|
|
});
|
|
|
|
return {
|
|
meta: state.meta,
|
|
header: {
|
|
placeName: place.name,
|
|
placeDesc: place.desc,
|
|
timeLabel: formatTime(state.world.time.totalMinutes),
|
|
riskLabel: dangerLabel(place.dangerLevel),
|
|
},
|
|
player: {
|
|
name: state.player.name,
|
|
stats: copyStats(state.player.stats),
|
|
maxStats: copyStats(state.player.maxStats),
|
|
attributes: { ...state.player.attributes },
|
|
equipment: { ...state.player.equipment },
|
|
inventory,
|
|
inventoryUsage: getInventoryUsage(state, content),
|
|
inventoryCapacity: content.inventoryCapacity,
|
|
storage,
|
|
storageUsage: getContainerUsage(state.player.storage, content),
|
|
storageCapacity: content.storageCapacity,
|
|
storageAccessible: canAccessShelterStorage(state),
|
|
},
|
|
map: {
|
|
places: Object.values(content.places).map((entry) => ({
|
|
id: entry.id,
|
|
name: entry.name,
|
|
desc: entry.desc,
|
|
dangerLevel: entry.dangerLevel,
|
|
tags: entry.tags,
|
|
visited: state.world.places[entry.id]?.visited ?? false,
|
|
current: entry.id === state.player.placeId,
|
|
})),
|
|
edges,
|
|
},
|
|
place: {
|
|
id: place.id,
|
|
name: place.name,
|
|
desc: place.desc,
|
|
services: place.services,
|
|
actions: place.actions.map((action) => buildActionPreview(state, content, action)),
|
|
tradeOffers: buildTradeView(state, content, place),
|
|
},
|
|
recipes: Object.values(content.recipes).map((recipe) => buildRecipeView(state, content, recipe)),
|
|
quests: Object.values(content.quests).map((quest) => buildQuestView(state, quest)),
|
|
survival: buildSurvivalView(state),
|
|
pendingEvent: buildPendingEventView(state, content),
|
|
combat: state.world.activeCombat
|
|
? {
|
|
enemyId: state.world.activeCombat.enemyId,
|
|
enemyName: content.enemies[state.world.activeCombat.enemyId].name,
|
|
enemyLife: state.world.activeCombat.enemyLife,
|
|
enemyMaxLife: content.enemies[state.world.activeCombat.enemyId].life,
|
|
distance: state.world.activeCombat.distance,
|
|
round: state.world.activeCombat.round,
|
|
log: state.world.activeCombat.log.slice(-8),
|
|
availableMoves: [
|
|
{ id: 'combat', move: 'attack', label: '攻击' },
|
|
{ id: 'combat', move: 'defend', label: '防御' },
|
|
{ id: 'combat', move: 'advance', label: '逼近' },
|
|
{ id: 'combat', move: 'retreat', label: '后撤' },
|
|
{ id: 'combat', move: 'escape', label: '脱离' },
|
|
],
|
|
}
|
|
: null,
|
|
logs: state.world.logs.slice(-40).reverse(),
|
|
flags: { ...state.world.flags },
|
|
gameOver: state.world.gameOver,
|
|
victory: state.world.victory,
|
|
deathReason: state.world.deathReason,
|
|
};
|
|
};
|