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, }; };