feat: 实现游戏核心功能与前端界面
- 添加游戏核心系统:战斗、任务、日志、随机数生成等 - 实现前端主界面、HUD、底部导航和各类卡片组件 - 添加用户认证系统与游戏存档持久化 - 配置项目基础架构与开发环境 - 补充文档说明与Docker部署支持
This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
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,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user