diff --git a/package.json b/package.json index e456f56..69d435c 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ }, "private": true, "version": "1.6.0", - "buildDate": "2026/1/6 03:05:21", + "buildDate": "2026/1/6 07:55:12", "main": "dist-electron/main.js", "type": "module", "scripts": { diff --git a/src/App.vue b/src/App.vue index 323ed28..c9fc9d9 100644 --- a/src/App.vue +++ b/src/App.vue @@ -414,6 +414,8 @@ + + @@ -476,6 +478,12 @@ import { useTheme } from '@/composables/useTheme' import { useI18n } from '@/composables/useI18n' import { useGameConfig } from '@/composables/useGameConfig' + import { useGameLoop } from '@/composables/useGameLoop' + import { createGameEngine, type GameEngine } from '@/services/gameEngine' + import { createMissionEngine, type MissionEngine } from '@/services/missionEngine' + import { createNpcEngine, type NpcEngine } from '@/services/npcEngine' + import { createEconomyEngine, type EconomyEngine } from '@/services/economyEngine' + import { createProgressionEngine, type ProgressionEngine } from '@/services/progressionEngine' import { localeNames, detectBrowserLocale, type Locale } from '@/locales' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' @@ -518,9 +526,8 @@ import HintToast from '@/components/notifications/HintToast.vue' import BackToTop from '@/components/common/BackToTop.vue' import Sonner from '@/components/ui/sonner/Sonner.vue' - import { MissionType, BuildingType, TechnologyType, DiplomaticEventType, ShipType } from '@/types/game' - import type { FleetMission, NPC, MissileAttack } from '@/types/game' - import { DIPLOMATIC_CONFIG } from '@/config/gameConfig' + import DebugOverlay from '@/components/debug/DebugOverlay.vue' + import { BuildingType, TechnologyType } from '@/types/game' import type { VersionInfo } from '@/utils/versionCheck' import { formatNumber, getResourceColor } from '@/utils/format' import { scaleNumber, scaleResources } from '@/utils/speed' @@ -550,21 +557,15 @@ Crown, Scroll } from 'lucide-vue-next' - import * as gameLogic from '@/logic/gameLogic' import * as planetLogic from '@/logic/planetLogic' import * as officerLogic from '@/logic/officerLogic' import * as buildingValidation from '@/logic/buildingValidation' import * as resourceLogic from '@/logic/resourceLogic' import * as researchValidation from '@/logic/researchValidation' - import * as fleetLogic from '@/logic/fleetLogic' - import * as shipLogic from '@/logic/shipLogic' - import * as npcGrowthLogic from '@/logic/npcGrowthLogic' - import * as npcBehaviorLogic from '@/logic/npcBehaviorLogic' - import * as diplomaticLogic from '@/logic/diplomaticLogic' import * as publicLogic from '@/logic/publicLogic' import * as oreDepositLogic from '@/logic/oreDepositLogic' - import * as campaignLogic from '@/logic/campaignLogic' - import { generateNPCName, countOldFormatNPCs, updateNPCName } from '@/logic/npcNameGenerator' + import { generateRandomPosition, generatePositionKey, shouldInitializeGame, initializePlayer } from '@/logic/gameLogic' + import { countOldFormatNPCs, updateNPCName } from '@/logic/npcNameGenerator' import pkg from '../package.json' import { toast } from 'vue-sonner' import { migrateGameData } from '@/utils/migration' @@ -601,14 +602,14 @@ const sidebarOpen = ref(window.innerWidth >= 1024) // 移动端资源栏展开状态 const resourceBarExpanded = ref(false) - const npcUpdateCounter = ref(0) // 累计秒数 - const NPC_UPDATE_INTERVAL = 5 // 每1秒更新一次NPC,确保发育速度与玩家相当 - // NPC行为系统更新函数(侦查和攻击决策) - const npcBehaviorCounter = ref(0) - const NPC_BEHAVIOR_INTERVAL = 5 // 每5秒检查一次NPC行为 + // 游戏引擎 + let gameEngine: GameEngine | null = null + let missionEngine: MissionEngine | null = null + let npcEngine: NpcEngine | null = null + let economyEngine: EconomyEngine | null = null + let progressionEngine: ProgressionEngine | null = null // 游戏循环定时器 - const gameLoop = ref | null>(null) const pointsUpdateInterval = ref | null>(null) const konamiCleanup = ref<(() => void) | null>(null) const versionCheckInterval = ref | null>(null) // 重命名星球相关状态 @@ -818,7 +819,7 @@ } const initGame = async () => { - const shouldInit = gameLogic.shouldInitializeGame(gameStore.player.planets) + const shouldInit = shouldInitializeGame(gameStore.player.planets) if (!shouldInit) { const now = Date.now() // 迁移矿脉储量数据(为没有矿脉数据的星球初始化) @@ -858,7 +859,7 @@ return } - gameStore.player = gameLogic.initializePlayer(gameStore.player.id, t('common.playerName')) + gameStore.player = initializePlayer(gameStore.player.id, t('common.playerName')) const initialPlanet = planetLogic.createInitialPlanet(gameStore.player.id, t('planet.homePlanet')) gameStore.player.planets = [initialPlanet] gameStore.currentPlanetId = initialPlanet.id @@ -871,926 +872,14 @@ const generateNPCPlanets = () => { const npcCount = 200 for (let i = 0; i < npcCount; i++) { - const position = gameLogic.generateRandomPosition() - const key = gameLogic.generatePositionKey(position.galaxy, position.system, position.position) + const position = generateRandomPosition() + const key = generatePositionKey(position.galaxy, position.system, position.position) if (universeStore.planets[key]) continue const npcPlanet = planetLogic.createNPCPlanet(i, position, t('planet.planetPrefix')) universeStore.planets[key] = npcPlanet } } - const updateGame = async () => { - const now = Date.now() - if (gameStore.isPaused) return - gameStore.gameTime = now - // 检查军官过期 - gameLogic.checkOfficersExpiration(gameStore.player.officers, now) - // 处理游戏更新(建造队列、研究队列等) - const result = gameLogic.processGameUpdate(gameStore.player, now, gameStore.gameSpeed, handleNotification, handleUnlockNotification) - gameStore.player.researchQueue = result.updatedResearchQueue - // 处理舰队任务 - gameStore.player.fleetMissions.forEach(mission => { - if (mission.status === 'outbound' && now >= mission.arrivalTime) { - processMissionArrival(mission) - } else if (mission.status === 'returning' && mission.returnTime && now >= mission.returnTime) { - processMissionReturn(mission) - } - }) - - // 处理导弹攻击任务(使用反向循环以便安全删除) - for (let i = gameStore.player.missileAttacks.length - 1; i >= 0; i--) { - const missileAttack = gameStore.player.missileAttacks[i] - if (missileAttack && missileAttack.status === 'flying' && now >= missileAttack.arrivalTime) { - await processMissileAttackArrival(missileAttack) - // 导弹攻击是单程的,到达后直接从数组中移除 - gameStore.player.missileAttacks.splice(i, 1) - } - } - - // 处理NPC舰队任务 - npcStore.npcs.forEach(npc => { - if (npc.fleetMissions) { - npc.fleetMissions.forEach(mission => { - if (mission.status === 'outbound' && now >= mission.arrivalTime) { - processNPCMissionArrival(npc, mission) - } else if (mission.status === 'returning' && mission.returnTime && now >= mission.returnTime) { - processNPCMissionReturn(npc, mission) - } - }) - } - }) - - // NPC成长系统更新 - updateNPCGrowth(1) - - // NPC行为系统更新(侦查和攻击决策) - updateNPCBehavior(1) - - // 检查成就解锁 - checkAchievementUnlocks() - - // 检查战役任务进度 - if (gameStore.player.campaignProgress) { - campaignLogic.checkAllActiveQuestsProgress(gameStore.player, npcStore.npcs) - } - - // 检查并处理被消灭的NPC(所有星球都被摧毁的NPC) - const eliminatedNpcIds = diplomaticLogic.checkAndHandleEliminatedNPCs(npcStore.npcs, gameStore.player, gameStore.locale) - if (eliminatedNpcIds.length > 0) { - // 从universeStore中移除被消灭NPC的星球数据,并收集需要清理的任务ID - const missionIdsToRemove: string[] = [] - eliminatedNpcIds.forEach(npcId => { - const npc = npcStore.npcs.find(n => n.id === npcId) - if (npc) { - // 遍历NPC的所有星球,从universeStore中删除 - if (npc.planets) { - npc.planets.forEach(planet => { - const planetKey = gameLogic.generatePositionKey(planet.position.galaxy, planet.position.system, planet.position.position) - if (universeStore.planets[planetKey]) { - delete universeStore.planets[planetKey] - } - }) - } - // 收集该NPC所有任务的ID(用于清理玩家的警报) - if (npc.fleetMissions) { - npc.fleetMissions.forEach(m => missionIdsToRemove.push(m.id)) - } - } - }) - - // 清理玩家的即将到来舰队警报(移除已消灭NPC的任务警报) - if (gameStore.player.incomingFleetAlerts && missionIdsToRemove.length > 0) { - gameStore.player.incomingFleetAlerts = gameStore.player.incomingFleetAlerts.filter(alert => !missionIdsToRemove.includes(alert.id)) - } - - // 从NPC列表中移除被消灭的NPC - npcStore.npcs = npcStore.npcs.filter(npc => !eliminatedNpcIds.includes(npc.id)) - } - } - - const processMissionArrival = async (mission: FleetMission) => { - // 从宇宙星球地图中查找目标星球 - const targetKey = gameLogic.generatePositionKey( - mission.targetPosition.galaxy, - mission.targetPosition.system, - mission.targetPosition.position - ) - // 先从玩家星球中查找,再从宇宙地图中查找 - // 如果任务指定了targetIsMoon,需要精确匹配行星或月球 - const targetPlanet = - gameStore.player.planets.find(p => { - const positionMatch = - p.position.galaxy === mission.targetPosition.galaxy && - p.position.system === mission.targetPosition.system && - p.position.position === mission.targetPosition.position - // 如果任务明确指定目标类型,按类型匹配 - if (mission.targetIsMoon !== undefined) { - return positionMatch && p.isMoon === mission.targetIsMoon - } - // 兼容旧任务:默认优先匹配行星(非月球) - return positionMatch && !p.isMoon - }) || - // 如果没有匹配到指定类型,尝试匹配同位置的任何星球 - gameStore.player.planets.find( - p => - p.position.galaxy === mission.targetPosition.galaxy && - p.position.system === mission.targetPosition.system && - p.position.position === mission.targetPosition.position - ) || - universeStore.planets[targetKey] - - // 获取起始星球名称(用于报告) - const originPlanet = gameStore.player.planets.find(p => p.id === mission.originPlanetId) - const originPlanetName = originPlanet?.name || t('fleetView.unknownPlanet') - - if (mission.missionType === MissionType.Transport) { - // 在处理任务之前保存货物信息(因为processTransportArrival会清空cargo) - const transportedResources = { ...mission.cargo } - const isGiftMission = mission.isGift && mission.giftTargetNpcId - const result = fleetLogic.processTransportArrival(mission, targetPlanet, gameStore.player, npcStore.npcs) - - // 更新成就统计(仅在成功时追踪) - if (result.success) { - const totalTransported = - transportedResources.metal + transportedResources.crystal + transportedResources.deuterium + transportedResources.darkMatter - if (isGiftMission) { - // 送礼成功 - gameLogic.trackDiplomacyStats(gameStore.player, 'gift', { resourcesAmount: totalTransported }) - } else { - // 普通运输任务成功 - gameLogic.trackMissionStats(gameStore.player, 'transport', { resourcesAmount: totalTransported }) - } - } - - // 生成失败原因消息 - let transportFailMessage = t('missionReports.transportFailed') - if (!result.success && result.failReason) { - if (result.failReason === 'targetNotFound') { - transportFailMessage = t('missionReports.transportFailedTargetNotFound') - } else if (result.failReason === 'giftRejected') { - transportFailMessage = t('missionReports.transportFailedGiftRejected') - } - } - - // 生成运输任务报告 - if (!gameStore.player.missionReports) { - gameStore.player.missionReports = [] - } - gameStore.player.missionReports.push({ - id: `mission-report-${mission.id}`, - timestamp: Date.now(), - missionType: MissionType.Transport, - originPlanetId: mission.originPlanetId, - originPlanetName, - targetPosition: mission.targetPosition, - targetPlanetId: targetPlanet?.id, - targetPlanetName: - targetPlanet?.name || `[${mission.targetPosition.galaxy}:${mission.targetPosition.system}:${mission.targetPosition.position}]`, - success: result.success, - message: result.success ? t('missionReports.transportSuccess') : transportFailMessage, - details: { - transportedResources, - failReason: result.failReason - }, - read: false - }) - } else if (mission.missionType === MissionType.Attack) { - const attackResult = await fleetLogic.processAttackArrival(mission, targetPlanet, gameStore.player, null, gameStore.player.planets) - if (attackResult) { - gameStore.player.battleReports.push(attackResult.battleResult) - - // 更新成就统计 - 攻击 - const debrisValue = attackResult.debrisField - ? attackResult.debrisField.resources.metal + attackResult.debrisField.resources.crystal - : 0 - const won = attackResult.battleResult.winner === 'attacker' - gameLogic.trackAttackStats(gameStore.player, attackResult.battleResult, won, debrisValue) - - // 检查是否攻击了NPC星球,更新外交关系 - if (targetPlanet) { - const targetNpc = npcStore.npcs.find(npc => npc.planets.some(p => p.id === targetPlanet.id)) - if (targetNpc) { - diplomaticLogic.handleAttackReputation(gameStore.player, targetNpc, attackResult.battleResult, npcStore.npcs, gameStore.locale) - - // 同步战斗损失到NPC的实际星球数据 - const npcPlanet = targetNpc.planets.find(p => p.id === targetPlanet.id) - if (npcPlanet) { - // 同步舰队损失 - Object.entries(attackResult.battleResult.defenderLosses.fleet).forEach(([shipType, lost]) => { - npcPlanet.fleet[shipType as ShipType] = Math.max(0, (npcPlanet.fleet[shipType as ShipType] || 0) - lost) - }) - // 同步防御损失(修复后的数据已在targetPlanet中) - npcPlanet.defense = { ...targetPlanet.defense } - // 同步资源(被掠夺后的) - npcPlanet.resources = { ...targetPlanet.resources } - } - } - } - - if (attackResult.moon) { - gameStore.player.planets.push(attackResult.moon) - } - if (attackResult.debrisField) { - // 将残骸场添加到游戏状态 - universeStore.debrisFields[attackResult.debrisField.id] = attackResult.debrisField - } - } - } else if (mission.missionType === MissionType.Colonize) { - const colonizeResult = fleetLogic.processColonizeArrival(mission, targetPlanet, gameStore.player, t('planet.colonyPrefix')) - const newPlanet = colonizeResult.planet - - // 更新成就统计 - 殖民 - if (colonizeResult.success && newPlanet) { - gameLogic.trackMissionStats(gameStore.player, 'colonize') - } - - // 生成失败原因消息 - let failMessage = t('missionReports.colonizeFailed') - if (!colonizeResult.success && colonizeResult.failReason) { - if (colonizeResult.failReason === 'positionOccupied') { - failMessage = t('missionReports.colonizeFailedOccupied') - } else if (colonizeResult.failReason === 'maxColoniesReached') { - failMessage = t('missionReports.colonizeFailedMaxColonies') - } - } - - // 生成殖民任务报告 - if (!gameStore.player.missionReports) { - gameStore.player.missionReports = [] - } - gameStore.player.missionReports.push({ - id: `mission-report-${mission.id}`, - timestamp: Date.now(), - missionType: MissionType.Colonize, - originPlanetId: mission.originPlanetId, - originPlanetName, - targetPosition: mission.targetPosition, - targetPlanetId: newPlanet?.id, - targetPlanetName: newPlanet?.name, - success: colonizeResult.success, - message: colonizeResult.success ? t('missionReports.colonizeSuccess') : failMessage, - details: newPlanet - ? { - newPlanetId: newPlanet.id, - newPlanetName: newPlanet.name - } - : { failReason: colonizeResult.failReason }, - read: false - }) - if (newPlanet) { - gameStore.player.planets.push(newPlanet) - } - } else if (mission.missionType === MissionType.Spy) { - const spyResult = fleetLogic.processSpyArrival(mission, targetPlanet, gameStore.player, null, npcStore.npcs) - if (spyResult.success && spyResult.report) { - gameStore.player.spyReports.push(spyResult.report) - // 更新成就统计 - 侦查 - gameLogic.trackMissionStats(gameStore.player, 'spy') - } - - // 生成侦查任务报告(即使失败也生成) - if (!gameStore.player.missionReports) { - gameStore.player.missionReports = [] - } - - let spyFailMessage = t('missionReports.spyFailed') - if (!spyResult.success && spyResult.failReason) { - if (spyResult.failReason === 'targetNotFound') { - spyFailMessage = t('missionReports.spyFailedTargetNotFound') - } - } - - gameStore.player.missionReports.push({ - id: `mission-report-${mission.id}`, - timestamp: Date.now(), - missionType: MissionType.Spy, - originPlanetId: mission.originPlanetId, - originPlanetName, - targetPosition: mission.targetPosition, - targetPlanetId: targetPlanet?.id, - targetPlanetName: - targetPlanet?.name || `[${mission.targetPosition.galaxy}:${mission.targetPosition.system}:${mission.targetPosition.position}]`, - success: spyResult.success, - message: spyResult.success ? t('missionReports.spySuccess') : spyFailMessage, - details: spyResult.success ? { spyReportId: spyResult.report?.id } : { failReason: spyResult.failReason }, - read: false - }) - } else if (mission.missionType === MissionType.Deploy) { - const deployed = fleetLogic.processDeployArrival(mission, targetPlanet, gameStore.player.id, gameStore.player.technologies) - - // 更新成就统计 - 部署 - if (deployed.success) { - gameLogic.trackMissionStats(gameStore.player, 'deploy') - } - - // 生成失败原因消息 - let deployFailMessage = t('missionReports.deployFailed') - if (!deployed.success && deployed.failReason) { - if (deployed.failReason === 'targetNotFound') { - deployFailMessage = t('missionReports.deployFailedTargetNotFound') - } else if (deployed.failReason === 'notOwnPlanet') { - deployFailMessage = t('missionReports.deployFailedNotOwnPlanet') - } - } - - // 生成部署任务报告 - if (!gameStore.player.missionReports) { - gameStore.player.missionReports = [] - } - gameStore.player.missionReports.push({ - id: `mission-report-${mission.id}`, - timestamp: Date.now(), - missionType: MissionType.Deploy, - originPlanetId: mission.originPlanetId, - originPlanetName, - targetPosition: mission.targetPosition, - targetPlanetId: targetPlanet?.id, - targetPlanetName: - targetPlanet?.name || `[${mission.targetPosition.galaxy}:${mission.targetPosition.system}:${mission.targetPosition.position}]`, - success: deployed.success, - message: deployed.success ? t('missionReports.deploySuccess') : deployFailMessage, - details: { - deployedFleet: mission.fleet, - failReason: deployed.failReason - }, - read: false - }) - if (deployed.success && !deployed.overflow) { - const missionIndex = gameStore.player.fleetMissions.indexOf(mission) - if (missionIndex > -1) gameStore.player.fleetMissions.splice(missionIndex, 1) - return - } - } else if (mission.missionType === MissionType.Recycle) { - // 处理回收任务 - const debrisId = `debris_${mission.targetPosition.galaxy}_${mission.targetPosition.system}_${mission.targetPosition.position}` - const debrisField = universeStore.debrisFields[debrisId] - const recycleResult = fleetLogic.processRecycleArrival(mission, debrisField) - - // 更新成就统计 - 回收(无论是否有残骸都算飞行任务,但只有成功回收才计入回收资源量) - const totalRecycled = - recycleResult.success && recycleResult.collectedResources - ? recycleResult.collectedResources.metal + recycleResult.collectedResources.crystal - : 0 - gameLogic.trackMissionStats(gameStore.player, 'recycle', { resourcesAmount: totalRecycled }) - - // 生成失败原因消息 - let recycleFailMessage = t('missionReports.recycleFailed') - if (!recycleResult.success && recycleResult.failReason) { - if (recycleResult.failReason === 'noDebrisField') { - recycleFailMessage = t('missionReports.recycleFailedNoDebris') - } else if (recycleResult.failReason === 'debrisEmpty') { - recycleFailMessage = t('missionReports.recycleFailedDebrisEmpty') - } - } - - // 生成回收任务报告 - if (!gameStore.player.missionReports) { - gameStore.player.missionReports = [] - } - gameStore.player.missionReports.push({ - id: `mission-report-${mission.id}`, - timestamp: Date.now(), - missionType: MissionType.Recycle, - originPlanetId: mission.originPlanetId, - originPlanetName, - targetPosition: mission.targetPosition, - success: recycleResult.success, - message: recycleResult.success ? t('missionReports.recycleSuccess') : recycleFailMessage, - details: recycleResult.success - ? { - recycledResources: recycleResult.collectedResources, - remainingDebris: recycleResult.remainingDebris || undefined - } - : { failReason: recycleResult.failReason }, - read: false - }) - - if (recycleResult.success && recycleResult.collectedResources && debrisField) { - if (recycleResult.remainingDebris && (recycleResult.remainingDebris.metal > 0 || recycleResult.remainingDebris.crystal > 0)) { - // 更新残骸场 - universeStore.debrisFields[debrisId] = { - id: debrisField.id, - position: debrisField.position, - resources: recycleResult.remainingDebris, - createdAt: debrisField.createdAt, - expiresAt: debrisField.expiresAt - } - } else { - // 残骸场已被完全收集,删除 - delete universeStore.debrisFields[debrisId] - } - } - } else if (mission.missionType === MissionType.Destroy) { - // 处理行星毁灭任务(需要先战斗,再计算毁灭概率) - const destroyResult = await fleetLogic.processDestroyArrival(mission, targetPlanet, gameStore.player, null, gameStore.player.planets) - - // 处理战斗报告(如果发生了战斗) - if (destroyResult.battleResult) { - gameStore.player.battleReports.push(destroyResult.battleResult) - - // 处理战斗对NPC的影响 - if (targetPlanet) { - const targetNpc = npcStore.npcs.find(npc => npc.planets.some(p => p.id === targetPlanet.id)) - if (targetNpc) { - diplomaticLogic.handleAttackReputation(gameStore.player, targetNpc, destroyResult.battleResult, npcStore.npcs, gameStore.locale) - - // 同步战斗损失到NPC的实际星球数据 - const npcPlanet = targetNpc.planets.find(p => p.id === targetPlanet.id) - if (npcPlanet) { - Object.entries(destroyResult.battleResult.defenderLosses.fleet).forEach(([shipType, lost]) => { - npcPlanet.fleet[shipType as ShipType] = Math.max(0, (npcPlanet.fleet[shipType as ShipType] || 0) - lost) - }) - npcPlanet.defense = { ...targetPlanet.defense } - npcPlanet.resources = { ...targetPlanet.resources } - } - } - } - } - - // 处理新生成的月球 - if (destroyResult.moon) { - gameStore.player.planets.push(destroyResult.moon) - } - - // 处理残骸场 - if (destroyResult.debrisField) { - universeStore.debrisFields[destroyResult.debrisField.id] = destroyResult.debrisField - } - - // 更新成就统计 - 行星毁灭 - if (destroyResult.success) { - gameLogic.trackMissionStats(gameStore.player, 'destroy') - } - - // 生成失败原因消息 - let destroyFailMessage = t('missionReports.destroyFailed') - if (!destroyResult.success && destroyResult.failReason) { - if (destroyResult.failReason === 'targetNotFound') { - destroyFailMessage = t('missionReports.destroyFailedTargetNotFound') - } else if (destroyResult.failReason === 'ownPlanet') { - destroyFailMessage = t('missionReports.destroyFailedOwnPlanet') - } else if (destroyResult.failReason === 'noDeathstar') { - destroyFailMessage = t('missionReports.destroyFailedNoDeathstar') - } else if (destroyResult.failReason === 'chanceFailed') { - destroyFailMessage = t('missionReports.destroyFailedChance', { chance: destroyResult.destructionChance.toFixed(1) }) - } - } - - // 生成毁灭任务报告 - if (!gameStore.player.missionReports) { - gameStore.player.missionReports = [] - } - gameStore.player.missionReports.push({ - id: `mission-report-${mission.id}`, - timestamp: Date.now(), - missionType: MissionType.Destroy, - originPlanetId: mission.originPlanetId, - originPlanetName, - targetPosition: mission.targetPosition, - targetPlanetId: targetPlanet?.id, - targetPlanetName: targetPlanet?.name, - success: destroyResult.success, - message: destroyResult.success ? t('missionReports.destroySuccess') : destroyFailMessage, - details: destroyResult.success - ? { - destroyedPlanetName: - targetPlanet?.name || - `[${mission.targetPosition.galaxy}:${mission.targetPosition.system}:${mission.targetPosition.position}]`, - hadBattle: !!destroyResult.battleResult - } - : { - failReason: destroyResult.failReason, - destructionChance: destroyResult.destructionChance, - deathstarsLost: destroyResult.deathstarsLost, - hadBattle: !!destroyResult.battleResult - }, - read: false - }) - - if (destroyResult.success && destroyResult.planetId) { - // 星球被摧毁 - - // 处理外交关系(如果目标是NPC星球) - if (targetPlanet && targetPlanet.ownerId) { - const planetOwner = npcStore.npcs.find(npc => npc.id === targetPlanet.ownerId) - if (planetOwner) { - diplomaticLogic.handlePlanetDestructionReputation(gameStore.player, targetPlanet, planetOwner, npcStore.npcs, gameStore.locale) - - // 从NPC的星球列表中移除被摧毁的星球 - const npcPlanetIndex = planetOwner.planets.findIndex(p => p.id === destroyResult.planetId) - if (npcPlanetIndex > -1) { - planetOwner.planets.splice(npcPlanetIndex, 1) - } - - // 检查并处理被消灭的NPC(所有星球都被摧毁的NPC) - const eliminatedNpcIds = diplomaticLogic.checkAndHandleEliminatedNPCs(npcStore.npcs, gameStore.player, gameStore.locale) - - // 从npcStore中移除被消灭的NPC - if (eliminatedNpcIds.length > 0) { - npcStore.npcs = npcStore.npcs.filter(npc => !eliminatedNpcIds.includes(npc.id)) - } - } - } - - // 从玩家星球列表中移除(如果是玩家的星球) - const planetIndex = gameStore.player.planets.findIndex(p => p.id === destroyResult.planetId) - if (planetIndex > -1) { - gameStore.player.planets.splice(planetIndex, 1) - } else { - // 不是玩家星球,从宇宙地图中移除 - delete universeStore.planets[targetKey] - } - - // 取消所有前往该位置的NPC任务(回收、攻击、侦查等) - const destroyedDebrisId = `debris_${mission.targetPosition.galaxy}_${mission.targetPosition.system}_${mission.targetPosition.position}` - npcStore.npcs.forEach(npc => { - if (npc.fleetMissions) { - // 找到需要取消的任务(前往已摧毁星球位置的outbound任务) - const missionsToCancel = npc.fleetMissions.filter(m => { - if (m.status !== 'outbound') return false - // 检查回收任务的残骸场ID - if (m.missionType === MissionType.Recycle && m.debrisFieldId === destroyedDebrisId) { - return true - } - // 检查其他任务的目标星球ID - if (m.targetPlanetId === destroyResult.planetId) { - return true - } - return false - }) - - // 将这些任务的舰队返回给NPC - missionsToCancel.forEach(m => { - const npcOriginPlanet = npc.planets.find(p => p.id === m.originPlanetId) - if (npcOriginPlanet) { - shipLogic.addFleet(npcOriginPlanet.fleet, m.fleet) - } - }) - - // 从任务列表中移除这些任务 - npc.fleetMissions = npc.fleetMissions.filter(m => !missionsToCancel.includes(m)) - } - - // 清理关于被摧毁星球的侦查报告 - if (npc.playerSpyReports && destroyResult.planetId && destroyResult.planetId in npc.playerSpyReports) { - delete npc.playerSpyReports[destroyResult.planetId] - } - }) - - // 同时删除该位置的残骸场(星球被摧毁后残骸场也消失) - delete universeStore.debrisFields[destroyedDebrisId] - } - } else if (mission.missionType === MissionType.Expedition) { - // 处理探险任务 - const expeditionResult = fleetLogic.processExpeditionArrival(mission) - - // 确保返回时间正确设置(兼容旧版本任务数据) - // 如果 returnTime 不存在或已过期,重新计算 - const now = Date.now() - if (!mission.returnTime || mission.returnTime <= now) { - // 返回时间应该等于当前时间加上单程飞行时间 - const flightDuration = mission.arrivalTime - mission.departureTime - mission.returnTime = now + flightDuration - } - - // 更新成就统计 - 探险 - const isSuccessful = - expeditionResult.eventType === 'resources' || expeditionResult.eventType === 'darkMatter' || expeditionResult.eventType === 'fleet' - gameLogic.trackMissionStats(gameStore.player, 'expedition', { successful: isSuccessful }) - - // 生成探险任务报告 - if (!gameStore.player.missionReports) { - gameStore.player.missionReports = [] - } - - // 根据事件类型生成不同的报告消息 - let reportMessage = '' - let reportDetails: Record = { - // 保存探险区域信息 - expeditionZone: mission.expeditionZone - } - - switch (expeditionResult.eventType) { - case 'resources': - reportMessage = t('missionReports.expeditionResources') - reportDetails.foundResources = expeditionResult.resources - break - case 'darkMatter': - reportMessage = t('missionReports.expeditionDarkMatter') - reportDetails.foundResources = expeditionResult.resources - break - case 'fleet': - reportMessage = t('missionReports.expeditionFleet') - reportDetails.foundFleet = expeditionResult.fleet - break - case 'pirates': - reportMessage = expeditionResult.fleetLost - ? t('missionReports.expeditionPiratesAttack') - : t('missionReports.expeditionPiratesEscaped') - if (expeditionResult.fleetLost) reportDetails.fleetLost = expeditionResult.fleetLost - break - case 'aliens': - reportMessage = expeditionResult.fleetLost - ? t('missionReports.expeditionAliensAttack') - : t('missionReports.expeditionAliensEscaped') - if (expeditionResult.fleetLost) reportDetails.fleetLost = expeditionResult.fleetLost - break - default: - reportMessage = t('missionReports.expeditionNothing') - } - - gameStore.player.missionReports.push({ - id: `mission-report-${mission.id}`, - timestamp: Date.now(), - missionType: MissionType.Expedition, - originPlanetId: mission.originPlanetId, - originPlanetName, - targetPosition: mission.targetPosition, - success: expeditionResult.eventType !== 'nothing', - message: reportMessage, - details: reportDetails, - read: false - }) - } - } - - const processMissionReturn = (mission: FleetMission) => { - const originPlanet = gameStore.player.planets.find(p => p.id === mission.originPlanetId) - if (!originPlanet) return - shipLogic.addFleet(originPlanet.fleet, mission.fleet) - resourceLogic.addResources(originPlanet.resources, mission.cargo) - const missionIndex = gameStore.player.fleetMissions.indexOf(mission) - if (missionIndex > -1) gameStore.player.fleetMissions.splice(missionIndex, 1) - } - - // NPC任务处理 - const processNPCMissionArrival = (npc: NPC, mission: FleetMission) => { - if (mission.missionType === MissionType.Recycle) { - // NPC回收任务到达 - const debrisId = mission.debrisFieldId - if (!debrisId) { - console.warn('[NPC Mission] Recycle mission missing debrisFieldId') - mission.status = 'returning' - mission.returnTime = Date.now() + (mission.arrivalTime - mission.departureTime) - return - } - - const debrisField = universeStore.debrisFields[debrisId] - const recycleResult = fleetLogic.processRecycleArrival(mission, debrisField) - - if (recycleResult && debrisField && recycleResult.collectedResources) { - // 更新成就统计 - 被NPC回收残骸(如果残骸是玩家战斗产生的) - const totalRecycled = recycleResult.collectedResources.metal + recycleResult.collectedResources.crystal - if (totalRecycled > 0) { - gameLogic.trackDiplomacyStats(gameStore.player, 'debrisRecycledByNPC', { resourcesAmount: totalRecycled }) - } - - if (recycleResult.remainingDebris && (recycleResult.remainingDebris.metal > 0 || recycleResult.remainingDebris.crystal > 0)) { - // 更新残骸场 - universeStore.debrisFields[debrisId] = { - id: debrisField.id, - position: debrisField.position, - resources: recycleResult.remainingDebris, - createdAt: debrisField.createdAt - } - } else { - // 残骸已被完全回收,从宇宙中删除 - delete universeStore.debrisFields[debrisId] - } - } - - // 移除即将到来的警告(回收任务已到达) - removeIncomingFleetAlertById(mission.id) - - // 设置返回时间 - mission.returnTime = Date.now() + (mission.arrivalTime - mission.departureTime) - return - } - - // 找到目标星球 - const targetKey = gameLogic.generatePositionKey( - mission.targetPosition.galaxy, - mission.targetPosition.system, - mission.targetPosition.position - ) - const targetPlanet = - gameStore.player.planets.find( - p => - p.position.galaxy === mission.targetPosition.galaxy && - p.position.system === mission.targetPosition.system && - p.position.position === mission.targetPosition.position - ) || universeStore.planets[targetKey] - - if (!targetPlanet) { - console.warn('[NPC Mission] Target planet not found') - return - } - - if (mission.missionType === MissionType.Spy) { - // NPC侦查到达 - const { spiedNotification, spyReport } = npcBehaviorLogic.processNPCSpyArrival(npc, mission, targetPlanet, gameStore.player) - - // 更新成就统计 - 被NPC侦查 - gameLogic.trackDiplomacyStats(gameStore.player, 'spiedByNPC') - - // 保存侦查报告到NPC(用于后续攻击决策) - if (!npc.playerSpyReports) { - npc.playerSpyReports = {} - } - npc.playerSpyReports[targetPlanet.id] = spyReport - - // 添加被侦查通知给玩家 - if (!gameStore.player.spiedNotifications) { - gameStore.player.spiedNotifications = [] - } - gameStore.player.spiedNotifications.push(spiedNotification) - - // 移除即将到来的警告(侦查已到达) - removeIncomingFleetAlertById(mission.id) - } else if (mission.missionType === MissionType.Attack) { - // NPC攻击到达 - 使用专门的NPC攻击处理逻辑 - fleetLogic.processNPCAttackArrival(npc, mission, targetPlanet, gameStore.player, gameStore.player.planets).then(attackResult => { - if (attackResult) { - // 更新成就统计 - 被NPC攻击 + 防御统计 - gameLogic.trackDiplomacyStats(gameStore.player, 'attackedByNPC') - const debrisValue = attackResult.debrisField - ? attackResult.debrisField.resources.metal + attackResult.debrisField.resources.crystal - : 0 - const won = attackResult.battleResult.winner === 'defender' - gameLogic.trackDefenseStats(gameStore.player, attackResult.battleResult, won, debrisValue) - - // 添加战斗报告给玩家 - gameStore.player.battleReports.push(attackResult.battleResult) - - // 如果生成月球,添加到玩家星球列表 - if (attackResult.moon) { - gameStore.player.planets.push(attackResult.moon) - } - - // 如果生成残骸场,添加到宇宙残骸场列表 - if (attackResult.debrisField) { - const existingDebris = universeStore.debrisFields[attackResult.debrisField.id] - if (existingDebris) { - // 累加残骸资源 - universeStore.debrisFields[attackResult.debrisField.id] = { - ...existingDebris, - resources: { - metal: existingDebris.resources.metal + attackResult.debrisField.resources.metal, - crystal: existingDebris.resources.crystal + attackResult.debrisField.resources.crystal - } - } - } else { - // 新残骸场 - universeStore.debrisFields[attackResult.debrisField.id] = attackResult.debrisField - } - } - } - - // 移除即将到来的警告(攻击已到达) - removeIncomingFleetAlertById(mission.id) - }) - } - } - - const processNPCMissionReturn = (npc: NPC, mission: FleetMission) => { - // 找到NPC的起始星球 - const originPlanet = npc.planets.find(p => p.id === mission.originPlanetId) - if (!originPlanet) return - - // 返还舰队 - shipLogic.addFleet(originPlanet.fleet, mission.fleet) - - // 如果携带掠夺资源,给NPC添加资源 - if (mission.cargo) { - originPlanet.resources.metal += mission.cargo.metal - originPlanet.resources.crystal += mission.cargo.crystal - originPlanet.resources.deuterium += mission.cargo.deuterium - } - - // 从NPC任务列表中移除 - if (npc.fleetMissions) { - const missionIndex = npc.fleetMissions.indexOf(mission) - if (missionIndex > -1) { - npc.fleetMissions.splice(missionIndex, 1) - } - } - } - - // 处理导弹攻击到达 - const processMissileAttackArrival = async (missileAttack: MissileAttack) => { - // 动态导入导弹逻辑 - const missileLogic = await import('@/logic/missileLogic') - - // 找到目标星球 - const targetKey = gameLogic.generatePositionKey( - missileAttack.targetPosition.galaxy, - missileAttack.targetPosition.system, - missileAttack.targetPosition.position - ) - const targetPlanet = - gameStore.player.planets.find( - p => - p.position.galaxy === missileAttack.targetPosition.galaxy && - p.position.system === missileAttack.targetPosition.system && - p.position.position === missileAttack.targetPosition.position - ) || universeStore.planets[targetKey] - - // 如果目标星球不存在,导弹失败 - if (!targetPlanet) { - missileAttack.status = 'arrived' - // 生成失败报告 - if (!gameStore.player.missionReports) { - gameStore.player.missionReports = [] - } - gameStore.player.missionReports.push({ - id: `missile-report-${missileAttack.id}`, - timestamp: Date.now(), - missionType: MissionType.MissileAttack, - originPlanetId: missileAttack.originPlanetId, - originPlanetName: gameStore.player.planets.find(p => p.id === missileAttack.originPlanetId)?.name || t('fleetView.unknownPlanet'), - targetPosition: missileAttack.targetPosition, - targetPlanetId: undefined, - targetPlanetName: `[${missileAttack.targetPosition.galaxy}:${missileAttack.targetPosition.system}:${missileAttack.targetPosition.position}]`, - success: false, - message: t('missionReports.missileAttackFailed'), - details: { - missileCount: missileAttack.missileCount, - missileHits: 0, - missileIntercepted: 0, - defenseLosses: {} - }, - read: false - }) - return - } - - // 计算导弹攻击结果 - const impactResult = missileLogic.calculateMissileImpact(missileAttack.missileCount, targetPlanet) - - // 应用损失到目标星球 - missileLogic.applyMissileAttackResult(targetPlanet, impactResult.defenseLosses) - - // 如果目标是NPC的星球,同步损失到NPC实际数据并扣除外交好感度 - if (targetPlanet.ownerId && targetPlanet.ownerId !== gameStore.player.id) { - const targetNpc = npcStore.npcs.find(npc => npc.id === targetPlanet.ownerId) - if (targetNpc) { - // 同步防御损失到NPC的实际星球数据 - const npcPlanet = targetNpc.planets.find(p => p.id === targetPlanet.id) - if (npcPlanet) { - missileLogic.applyMissileAttackResult(npcPlanet, impactResult.defenseLosses) - } - - // 导弹攻击扣除好感度 - const { REPUTATION_CHANGES } = DIPLOMATIC_CONFIG - const reputationLoss = REPUTATION_CHANGES.ATTACK / 2 // 导弹攻击的好感度惩罚是普通攻击的一半 - - // 更新NPC对玩家的关系(统一使用 npc.relations 作为唯一数据源) - if (!targetNpc.relations) { - targetNpc.relations = {} - } - const npcRelation = diplomaticLogic.getOrCreateRelation(targetNpc.relations, targetNpc.id, gameStore.player.id) - targetNpc.relations[gameStore.player.id] = diplomaticLogic.updateReputation( - npcRelation, - reputationLoss, - DiplomaticEventType.Attack, - t('diplomacy.reports.wasAttackedByMissile') - ) - } - } - - // 标记导弹攻击为已到达 - missileAttack.status = 'arrived' - - // 生成导弹攻击报告 - if (!gameStore.player.missionReports) { - gameStore.player.missionReports = [] - } - const reportMessage = - impactResult.missileHits > 0 - ? `${t('missionReports.missileAttackSuccess')}: ${impactResult.missileHits} ${t('missionReports.hits')}` - : t('missionReports.missileAttackIntercepted') - - gameStore.player.missionReports.push({ - id: `missile-report-${missileAttack.id}`, - timestamp: Date.now(), - missionType: MissionType.MissileAttack, - originPlanetId: missileAttack.originPlanetId, - originPlanetName: gameStore.player.planets.find(p => p.id === missileAttack.originPlanetId)?.name || t('fleetView.unknownPlanet'), - targetPosition: missileAttack.targetPosition, - targetPlanetId: targetPlanet.id, - targetPlanetName: targetPlanet.name, - success: true, - message: reportMessage, - details: { - missileCount: missileAttack.missileCount, - missileHits: impactResult.missileHits, - missileIntercepted: impactResult.missileIntercepted, - defenseLosses: impactResult.defenseLosses - }, - read: false - }) - } - // 打开敌方警报面板 const openEnemyAlertPanel = () => { enemyAlertNotificationsRef.value?.open() @@ -1804,391 +893,116 @@ } } - /** - * 同步NPC星球数据到universeStore - * 解决npcStore和universeStore数据不同步的问题 - */ - const syncNPCPlanetToUniverse = (npc: any) => { - npc.planets.forEach((npcPlanet: any) => { - const planetKey = gameLogic.generatePositionKey(npcPlanet.position.galaxy, npcPlanet.position.system, npcPlanet.position.position) - const universePlanet = universeStore.planets[planetKey] - if (universePlanet) { - // 同步所有关键数据 - universePlanet.resources = { ...npcPlanet.resources } - universePlanet.buildings = { ...npcPlanet.buildings } - universePlanet.fleet = { ...npcPlanet.fleet } - universePlanet.defense = { ...npcPlanet.defense } - } - }) - } + // 创建任务引擎(处理舰队任务和导弹攻击) + missionEngine = createMissionEngine({ + removeIncomingFleetAlert: removeIncomingFleetAlertById + }) - const updateNPCGrowth = (deltaSeconds: number) => { - // 累积时间 - npcUpdateCounter.value += deltaSeconds + // 创建NPC引擎(处理NPC成长和行为,支持分片更新) + npcEngine = createNpcEngine({ + sliceSize: 20, // 每 tick 最多更新 20 个 NPC + growthInterval: 5, // 成长更新间隔 5 秒 + behaviorInterval: 5 // 行为更新间隔 5 秒 + }) - // 只在达到更新间隔时才执行 - if (npcUpdateCounter.value < NPC_UPDATE_INTERVAL) { - return - } + // 创建经济引擎(处理资源生产、建造/研究队列) + economyEngine = createEconomyEngine({ + onNotification: handleNotification, + onUnlock: handleUnlockNotification + }) - // 获取所有星球 - const allPlanets = Object.values(universeStore.planets) - - // 如果NPC store为空,从星球数据中初始化NPC - if (npcStore.npcs.length === 0) { - const npcMap = new Map() - - allPlanets.forEach(planet => { - // 跳过玩家的星球 - if (planet.ownerId === gameStore.player.id || !planet.ownerId) return - - // 这是NPC的星球 - if (!npcMap.has(planet.ownerId)) { - // 为每个NPC设置随机的初始冷却时间,避免所有NPC同时行动 - const now = Date.now() - const randomSpyOffset = Math.random() * 240 * 1000 // 0-4分钟的随机延迟 - const randomAttackOffset = Math.random() * 480 * 1000 // 0-8分钟的随机延迟 - - // 初始化NPC与玩家的中立关系 - const initialRelations: Record = {} - initialRelations[gameStore.player.id] = { - fromId: planet.ownerId, - toId: gameStore.player.id, - reputation: 0, - status: 'neutral' as const, - lastUpdated: now, - history: [] - } - - npcMap.set(planet.ownerId, { - id: planet.ownerId, - name: generateNPCName(planet.ownerId, gameStore.locale), - planets: [], - technologies: {}, // 初始化空科技树 - difficulty: 'medium' as const, // 默认中等难度 - relations: initialRelations, // 外交关系(默认与玩家中立) - allies: [], // 盟友列表 - enemies: [], // 敌人列表 - lastSpyTime: now - randomSpyOffset, // 设置随机的上次侦查时间 - lastAttackTime: now - randomAttackOffset, // 设置随机的上次攻击时间 - fleetMissions: [], // 舰队任务 - playerSpyReports: {} // 对玩家的侦查报告 - }) - } - - npcMap.get(planet.ownerId)!.planets.push(planet) - }) - - // 保存到store - npcStore.npcs = Array.from(npcMap.values()) - - // 如果有NPC,基于距离初始化NPC实力 - if (npcStore.npcs.length > 0) { - // 获取玩家母星(第一个非月球星球) - const homeworld = gameStore.player.planets.find(p => !p.isMoon) - - if (homeworld) { - npcStore.npcs.forEach(npc => { - // 基于距离初始化NPC实力 - npcGrowthLogic.initializeNPCByDistance(npc, homeworld.position) - // 同步NPC星球数据到universeStore - syncNPCPlanetToUniverse(npc) - }) - } - - // 初始化NPC之间的外交关系(盟友/敌人) - npcGrowthLogic.initializeNPCDiplomacy(npcStore.npcs) - } - } - - // 确保所有NPC都有间谍探测器(修复旧版本保存的数据) - if (npcStore.npcs.length > 0) { - npcGrowthLogic.ensureNPCSpyProbes(npcStore.npcs) - } - - // 确保所有NPC都有AI类型(修复旧版本保存的数据) - if (npcStore.npcs.length > 0) { - npcGrowthLogic.ensureAllNPCsAIType(npcStore.npcs) - } - - // 确保所有NPC都与玩家建立了关系(修复旧版本保存的数据) - if (npcStore.npcs.length > 0) { - const now = Date.now() - // 获取玩家母星(用于计算距离) - const homeworld = gameStore.player.planets.find(p => !p.isMoon) - - npcStore.npcs.forEach(npc => { - if (!npc.relations) { - npc.relations = {} - } - // 如果NPC没有与玩家的关系,建立中立关系 - if (!npc.relations[gameStore.player.id]) { - npc.relations[gameStore.player.id] = { - fromId: npc.id, - toId: gameStore.player.id, - reputation: 0, - status: 'neutral' as const, - lastUpdated: now, - history: [] - } - } - - // 迁移旧存档:如果NPC没有距离数据,计算并设置 - if (homeworld && npc.distanceToHomeworld === undefined) { - const npcPlanet = npc.planets[0] - if (npcPlanet) { - npc.distanceToHomeworld = npcGrowthLogic.calculateDistanceToHomeworld(npcPlanet.position, homeworld.position) - npc.difficultyLevel = npcGrowthLogic.calculateDifficultyLevel(npc.distanceToHomeworld) - // 重新初始化NPC实力以匹配新的距离难度系统 - npcGrowthLogic.initializeNPCByDistance(npc, homeworld.position) - // 同步NPC星球数据到universeStore - syncNPCPlanetToUniverse(npc) - } - } - }) - } - - // 如果没有NPC,直接返回 - if (npcStore.npcs.length === 0) { - npcUpdateCounter.value = 0 - return - } - - // 获取玩家母星用于距离计算 - const homeworldForGrowth = gameStore.player.planets.find(p => !p.isMoon) - - // 使用累积的时间更新每个NPC(基于距离的成长系统) - npcStore.npcs.forEach(npc => { - if (homeworldForGrowth) { - npcGrowthLogic.updateNPCGrowthByDistance(npc, homeworldForGrowth.position, npcUpdateCounter.value, gameStore.gameSpeed) - // 同步NPC星球数据到universeStore(确保侦查报告显示正确数据) - syncNPCPlanetToUniverse(npc) - } - }) - - // 重置计数器 - npcUpdateCounter.value = 0 - } - - const updateNPCBehavior = (deltaSeconds: number) => { - // 累积时间 - npcBehaviorCounter.value += deltaSeconds - - // 只在达到更新间隔时才执行 - if (npcBehaviorCounter.value < NPC_BEHAVIOR_INTERVAL) { - return - } - - // 如果没有NPC,直接返回 - if (npcStore.npcs.length === 0) { - npcBehaviorCounter.value = 0 - return - } - - const now = Date.now() - // 合并玩家星球和NPC星球到allPlanets(NPC需要能够侦查和攻击玩家星球) - const allPlanets = [...gameStore.player.planets, ...Object.values(universeStore.planets)] - - // 计算当前所有正在进行的侦查和攻击任务数量 - let activeSpyMissions = 0 - let activeAttackMissions = 0 - npcStore.npcs.forEach(npc => { - if (npc.fleetMissions) { - npc.fleetMissions.forEach(mission => { - if (mission.status === 'outbound') { - if (mission.missionType === 'spy') { - activeSpyMissions++ - } else if (mission.missionType === 'attack') { - activeAttackMissions++ - } - } - }) - } - }) - - // 获取并发限制配置 - const config = npcBehaviorLogic.calculateDynamicBehavior(gameStore.player.points) - - // 更新每个NPC的行为(随机顺序,避免总是优先处理同一批NPC) - const shuffledNpcs = [...npcStore.npcs].sort(() => Math.random() - 0.5) - shuffledNpcs.forEach(npc => { - // 在更新前检查当前并发数,如果已达上限则跳过该NPC - npcBehaviorLogic.updateNPCBehaviorWithLimit(npc, gameStore.player, allPlanets, universeStore.debrisFields, now, { - activeSpyMissions, - activeAttackMissions, - config - }) - - // 重新计算当前并发数(因为可能新增了任务) - activeSpyMissions = 0 - activeAttackMissions = 0 - npcStore.npcs.forEach(n => { - if (n.fleetMissions) { - n.fleetMissions.forEach(mission => { - if (mission.status === 'outbound') { - if (mission.missionType === 'spy') activeSpyMissions++ - else if (mission.missionType === 'attack') activeAttackMissions++ - } - }) - } - }) - - // 处理增强NPC行为(中立和友好NPC的特殊行为) - const relation = npc.relations?.[gameStore.player.id] - if (relation?.status === 'neutral') { - const neutralResult = npcBehaviorLogic.updateNeutralNPCBehavior(npc, npcStore.npcs, gameStore.player, now) - - // 处理贸易提议 - if (neutralResult.tradeOffer) { - if (!gameStore.player.tradeOffers) { - gameStore.player.tradeOffers = [] - } - gameStore.player.tradeOffers.push(neutralResult.tradeOffer) - toast.info(t('npcBehavior.tradeOfferReceived'), { - description: t('npcBehavior.tradeOfferDesc', { npcName: neutralResult.tradeOffer.npcName }) - }) - } - - // 处理态度摇摆 - if (neutralResult.swingDirection) { - if (!gameStore.player.attitudeChangeNotifications) { - gameStore.player.attitudeChangeNotifications = [] - } - gameStore.player.attitudeChangeNotifications.push({ - id: `attitude_${Date.now()}_${npc.id}`, - timestamp: now, - npcId: npc.id, - npcName: npc.name, - previousStatus: 'neutral', - newStatus: neutralResult.swingDirection, - reason: 'attitude_swing', - read: false - }) - const statusKey = neutralResult.swingDirection === 'friendly' ? 'npcBehavior.becameFriendly' : 'npcBehavior.becameHostile' - toast.info(t('npcBehavior.attitudeChanged'), { - description: t(statusKey, { npcName: npc.name }) - }) - } - } else if (relation?.status === 'friendly') { - const friendlyResult = npcBehaviorLogic.updateFriendlyNPCBehavior(npc, npcStore.npcs, gameStore.player, now) - - // 处理情报报告 - if (friendlyResult.intelReport) { - if (!gameStore.player.intelReports) { - gameStore.player.intelReports = [] - } - gameStore.player.intelReports.push(friendlyResult.intelReport) - toast.info(t('npcBehavior.intelReceived'), { - description: t('npcBehavior.intelReceivedDesc', { npcName: friendlyResult.intelReport.fromNpcName }) - }) - } - - // 处理联合攻击邀请 - if (friendlyResult.jointAttackInvite) { - if (!gameStore.player.jointAttackInvites) { - gameStore.player.jointAttackInvites = [] - } - gameStore.player.jointAttackInvites.push(friendlyResult.jointAttackInvite) - toast.info(t('npcBehavior.jointAttackInvite'), { - description: t('npcBehavior.jointAttackInviteDesc', { npcName: friendlyResult.jointAttackInvite.fromNpcName }) - }) - } - - // 处理资源援助 - if (friendlyResult.aidProvided) { - if (!gameStore.player.aidNotifications) { - gameStore.player.aidNotifications = [] - } - gameStore.player.aidNotifications.push({ - id: `aid_${Date.now()}_${npc.id}`, - timestamp: now, - npcId: npc.id, - npcName: npc.name, - aidResources: friendlyResult.aidProvided, - read: false - }) - const totalAid = friendlyResult.aidProvided.metal + friendlyResult.aidProvided.crystal + friendlyResult.aidProvided.deuterium - toast.success(t('npcBehavior.aidReceived'), { - description: t('npcBehavior.aidReceivedDesc', { npcName: npc.name, amount: totalAid.toLocaleString() }) - }) - } - } - }) - - npcBehaviorCounter.value = 0 - } - - // 更新NPC关系统计(友好/敌对数量) - const updateNPCRelationStats = () => { - let friendlyCount = 0 - let hostileCount = 0 - const playerId = gameStore.player.id - npcStore.npcs.forEach(npc => { - const relation = npc.relations?.[playerId] - if (relation) { - const status = diplomaticLogic.calculateRelationStatus(relation.reputation) - if (status === 'friendly') { - friendlyCount++ - } else if (status === 'hostile') { - hostileCount++ - } - } - }) - gameLogic.trackDiplomacyStats(gameStore.player, 'updateRelations', { friendlyCount, hostileCount }) - } - - // 检查成就解锁 - const achievementCheckCounter = ref(0) - const ACHIEVEMENT_CHECK_INTERVAL = 5 // 每5秒检查一次成就 - - const checkAchievementUnlocks = () => { - achievementCheckCounter.value += 1 - - // 只在达到更新间隔时才执行 - if (achievementCheckCounter.value < ACHIEVEMENT_CHECK_INTERVAL) { - return - } - - // 更新NPC关系统计 - updateNPCRelationStats() - - // 检查并解锁成就 - const unlocks = gameLogic.checkAndUnlockAchievements(gameStore.player) - - // 显示成就解锁通知(奖励已在 checkAndUnlockAchievements 中应用) - unlocks.forEach(unlock => { - // 显示 toast 通知 + // 创建进度引擎(处理成就检查、战役进度、外交清理等低频任务) + progressionEngine = createProgressionEngine({ + achievementInterval: 5000, // 每5秒检查成就 + campaignInterval: 5000, // 每5秒检查战役进度 + diplomacyCleanupInterval: 15000, // 每15秒清理外交数据 + onAchievementUnlock: unlock => { const tierName = t(`achievements.tiers.${unlock.tier}`) const achievementName = t(`achievements.names.${unlock.id}`) toast.success(t('achievements.unlocked'), { description: `${achievementName} (${tierName})` }) - }) - - achievementCheckCounter.value = 0 - } - - // 启动游戏循环 - const startGameLoop = () => { - if (gameStore.isPaused) return - // 清理旧的定时器 - if (gameLoop.value) { - clearInterval(gameLoop.value) } - // 游戏循环固定为1秒,避免高倍速时的卡顿 - // gameSpeed 只作用于资源产出和时间消耗的倍率 - const interval = 1000 - // 启动新的游戏循环 - gameLoop.value = setInterval(() => { - updateGame() - }, interval) - } + }) + + // 创建游戏引擎 + gameEngine = createGameEngine({ + t, + onTick: async ctx => { + const { profiler } = ctx + + // 初始化 ProgressionEngine(首次 tick 时) + progressionEngine?.init(ctx) + + // 使用 EconomyEngine 处理资源生产和队列完成 + profiler.start('tick:economy') + economyEngine?.tick(ctx) + profiler.end('tick:economy') + + // 使用 MissionEngine 处理任务(玩家任务到达/返回、NPC任务到达/返回、导弹攻击) + profiler.start('tick:mission') + await missionEngine?.tick(ctx) + profiler.end('tick:mission') + + // 使用 NpcEngine 处理NPC成长和行为(分片更新) + profiler.start('tick:npc') + npcEngine?.tick(ctx) + profiler.end('tick:npc') + + // 使用 ProgressionEngine 处理低频任务(成就、战役、外交清理) + profiler.start('tick:progression') + progressionEngine?.tick(ctx) + profiler.end('tick:progression') + }, + onPauseChange: paused => { + if (paused) { + stopLoop() + } else { + startLoop() + } + }, + notify: (message, type = 'info') => { + toast[type](message) + }, + notifyUnlock: handleUnlockNotification + }) + + // 游戏循环(使用 composable 管理) + // gameSpeed 只作用于资源产出和时间消耗的倍率,循环固定为1秒 + const { start: startLoop, stop: stopLoop } = useGameLoop((now, deltaMs) => { + gameEngine?.tick(now, deltaMs) + }, 1000) // 停止游戏循环 const stopGameLoop = () => { - if (gameLoop.value) { - clearInterval(gameLoop.value) - gameLoop.value = null + stopLoop() + } + + // 启动游戏循环(带暂停检查) + const startGameLoop = () => { + if (gameEngine?.isPaused()) return + startLoop() + } + + // 处理页面可见性变化(解决离线进度问题) + // 当页面隐藏时停止游戏循环,避免浏览器限流期间浪费离线时间 + // 当页面恢复可见时,立即处理离线进度并重启游戏循环 + const handleVisibilityChange = () => { + if (document.hidden) { + // 页面隐藏,停止游戏循环 + // 这样 lastUpdate 不会被更新,离线时间会被保留 + stopGameLoop() + if (pointsUpdateInterval.value) { + clearInterval(pointsUpdateInterval.value) + pointsUpdateInterval.value = null + } + } else { + // 页面恢复可见,立即处理离线进度 + if (!gameStore.isPaused) { + // 重新启动游戏循环(离线时间的资源累积会在下一次 tick 时自动处理) + startGameLoop() + startPointsUpdate() + } } } @@ -2365,6 +1179,8 @@ gameStore.locale = detectBrowserLocale() } await initGame() + // 初始化游戏引擎 + gameEngine?.init() // 启动游戏循环 startGameLoop() // 启动积分更新定时器 @@ -2376,6 +1192,9 @@ window.addEventListener('cancel-build', handleCancelBuildEvent as EventListener) window.addEventListener('cancel-research', handleCancelResearchEvent as EventListener) + // 添加页面可见性变化监听(解决离线进度问题) + document.addEventListener('visibilitychange', handleVisibilityChange) + // 首次检查版本(被动检测) const versionInfo = await checkLatestVersion(gameStore.player.lastVersionCheckTime || 0, (time: number) => { gameStore.player.lastVersionCheckTime = time @@ -2440,15 +1259,18 @@ } }) - // 清理定时器 + // 清理定时器和游戏引擎 onUnmounted(() => { - if (gameLoop.value) clearInterval(gameLoop.value) + stopGameLoop() + gameEngine?.dispose() if (pointsUpdateInterval.value) clearInterval(pointsUpdateInterval.value) if (konamiCleanup.value) konamiCleanup.value() if (versionCheckInterval.value) clearInterval(versionCheckInterval.value) // 移除队列取消事件监听 window.removeEventListener('cancel-build', handleCancelBuildEvent as EventListener) window.removeEventListener('cancel-research', handleCancelResearchEvent as EventListener) + // 移除页面可见性变化监听 + document.removeEventListener('visibilitychange', handleVisibilityChange) // 移除 Android 返回键监听 if (Capacitor.isNativePlatform()) { CapacitorApp.removeAllListeners() diff --git a/src/components/debug/DebugOverlay.vue b/src/components/debug/DebugOverlay.vue new file mode 100644 index 0000000..512959c --- /dev/null +++ b/src/components/debug/DebugOverlay.vue @@ -0,0 +1,144 @@ + + + + + + + + + Debug + {{ formatTime(totalTime) }} + + + + + + + + + + + + + + 平均耗时 (60帧) + + {{ item.label }} + + {{ formatTime(item.time) }} + + + + 等待数据... + + + + Ctrl+Shift+D 切换显示 + + + + diff --git a/src/composables/useGameLoop.ts b/src/composables/useGameLoop.ts new file mode 100644 index 0000000..ba15887 --- /dev/null +++ b/src/composables/useGameLoop.ts @@ -0,0 +1,162 @@ +import { ref, onUnmounted } from 'vue' +import { createTimeSource, type TimeSource, type TimeSourceConfig } from '@/services/timeSource' + +export type TickFn = (now: number, deltaMs: number) => void + +/** + * 追赶模式回调 + * @param pendingMs 待追赶的时间(毫秒) + * @param totalCaughtUp 本次追赶的总时间 + */ +export type CatchUpFn = (pendingMs: number, totalCaughtUp: number) => void + +/** + * 游戏循环配置 + */ +export interface GameLoopOptions { + /** tick 间隔(毫秒,默认 1000) */ + intervalMs?: number + /** 时间源配置 */ + timeSourceConfig?: Partial + /** 追赶模式回调 */ + onCatchUp?: CatchUpFn +} + +/** + * 游戏循环 composable + * 提供可测试、可暂停的定时器管理 + * 集成 TimeSource 处理离线/标签页不活跃场景 + */ +export const useGameLoop = (tick: TickFn, optionsOrInterval: GameLoopOptions | number = 1000) => { + // 兼容旧版 API(直接传入 intervalMs) + const options: GameLoopOptions = typeof optionsOrInterval === 'number' ? { intervalMs: optionsOrInterval } : optionsOrInterval + + const intervalMs = options.intervalMs ?? 1000 + + const timerId = ref | null>(null) + const catchUpTimerId = ref | null>(null) + const timeSource: TimeSource = createTimeSource(options.timeSourceConfig) + + // 追赶模式状态 + const isCatchingUp = ref(false) + const totalCaughtUp = ref(0) + + const isRunning = () => timerId.value !== null + + /** + * 处理追赶模式 + * 在追赶模式下,会以更快的频率处理积压的时间 + */ + const processCatchUp = () => { + const pending = timeSource.getPendingCatchUp() + if (pending <= 0) { + // 追赶完成 + if (catchUpTimerId.value) { + clearInterval(catchUpTimerId.value) + catchUpTimerId.value = null + } + isCatchingUp.value = false + totalCaughtUp.value = 0 + return + } + + // 获取追赶配置 + const chunkSize = options.timeSourceConfig?.catchUpChunkSize ?? 60000 // 默认 60 秒 + const catchUpAmount = Math.min(pending, chunkSize) + + // 消耗追赶时间 + timeSource.consumeCatchUp(catchUpAmount) + totalCaughtUp.value += catchUpAmount + + // 执行追赶 tick(使用当前时间,但 deltaMs 是追赶时间) + const now = timeSource.now() + tick(now, catchUpAmount) + + // 通知追赶进度 + options.onCatchUp?.(timeSource.getPendingCatchUp(), totalCaughtUp.value) + } + + /** + * 启动追赶模式 + */ + const startCatchUp = () => { + if (catchUpTimerId.value) return + isCatchingUp.value = true + totalCaughtUp.value = 0 + + // 追赶模式使用更快的间隔(100ms) + catchUpTimerId.value = setInterval(processCatchUp, 100) + } + + /** + * 主循环 tick + */ + const mainTick = () => { + const now = timeSource.now() + const deltaMs = timeSource.getDeltaMs(now) + + // 执行正常 tick + tick(now, deltaMs) + + // 检查是否需要追赶 + if (timeSource.needsCatchUp(now) && !isCatchingUp.value) { + startCatchUp() + } + } + + const start = () => { + if (timerId.value) { + clearInterval(timerId.value) + } + + // 初始化时间源 + timeSource.setLastTime(timeSource.now()) + timeSource.resetCatchUp() + + // 启动主循环 + timerId.value = setInterval(mainTick, intervalMs) + } + + const stop = () => { + // 停止主循环 + if (timerId.value) { + clearInterval(timerId.value) + timerId.value = null + } + // 停止追赶循环 + if (catchUpTimerId.value) { + clearInterval(catchUpTimerId.value) + catchUpTimerId.value = null + } + isCatchingUp.value = false + } + + /** + * 获取时间源(用于调试或高级用途) + */ + const getTimeSource = () => timeSource + + /** + * 检查是否正在追赶 + */ + const getIsCatchingUp = () => isCatchingUp.value + + /** + * 获取待追赶时间 + */ + const getPendingCatchUp = () => timeSource.getPendingCatchUp() + + // 组件卸载时自动清理 + onUnmounted(() => { + stop() + }) + + return { + start, + stop, + isRunning, + getTimeSource, + getIsCatchingUp, + getPendingCatchUp + } +} diff --git a/src/locales/de.ts b/src/locales/de.ts index 66cb61e..0c7cd6a 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -981,7 +981,8 @@ export default { gamePause: 'Spielpause', gamePauseDesc: 'Spielzeit und Ressourcenproduktion pausieren oder fortsetzen', battleMode: 'Bis zum Ende kämpfen', - battleModeDesc: 'Wenn aktiviert, dauern Kämpfe bis zu 100 Runden bis ein Sieger feststeht. Wenn deaktiviert, wird der klassische 6-Runden-Modus verwendet', + battleModeDesc: + 'Wenn aktiviert, dauern Kämpfe bis zu 100 Runden bis ein Sieger feststeht. Wenn deaktiviert, wird der klassische 6-Runden-Modus verwendet', pause: 'Pausieren', resume: 'Fortsetzen', gamePaused: 'Spiel pausiert', diff --git a/src/logic/dirtyFlags.ts b/src/logic/dirtyFlags.ts new file mode 100644 index 0000000..1b01dae --- /dev/null +++ b/src/logic/dirtyFlags.ts @@ -0,0 +1,82 @@ +/** + * 脏标志系统 + * 用于性能优化,追踪哪些子系统需要处理 + * 当标志为 false 时,对应的引擎可以跳过重计算 + */ + +/** + * 脏标志接口,用于追踪游戏子系统是否需要更新 + */ +export interface DirtyFlags { + /** 经济计算需要更新(资源生产、存储) */ + economyDirty: boolean + /** 舰队/任务需要处理(有活动任务) */ + fleetDirty: boolean + /** NPC行为需要更新 */ + npcDirty: boolean + /** 建造/研究队列需要处理 */ + queuesDirty: boolean +} + +/** + * 创建新的脏标志实例,所有标志设为 true(强制初始处理) + */ +export const createDirtyFlags = (): DirtyFlags => ({ + economyDirty: true, + fleetDirty: true, + npcDirty: true, + queuesDirty: true +}) + +/** + * 重置所有脏标志为 false(干净状态) + */ +export const resetDirtyFlags = (flags: DirtyFlags): void => { + flags.economyDirty = false + flags.fleetDirty = false + flags.npcDirty = false + flags.queuesDirty = false +} + +/** + * 标记经济为脏(需要重新计算) + * 调用时机:资源变化、建筑完成、星球状态变化 + */ +export const markEconomyDirty = (flags: DirtyFlags): void => { + flags.economyDirty = true +} + +/** + * 标记舰队为脏(需要处理) + * 调用时机:新任务发送、任务到达、舰队组成变化 + */ +export const markFleetDirty = (flags: DirtyFlags): void => { + flags.fleetDirty = true +} + +/** + * 标记NPC为脏(需要行为更新) + * 调用时机:NPC状态变化、玩家-NPC交互、基于时间的NPC事件 + */ +export const markNpcDirty = (flags: DirtyFlags): void => { + flags.npcDirty = true +} + +/** + * 标记队列为脏(需要处理) + * 调用时机:新项目入队、队列项目完成、队列取消 + */ +export const markQueuesDirty = (flags: DirtyFlags): void => { + flags.queuesDirty = true +} + +/** + * 标记所有标志为脏(强制完整处理) + * 调用时机:游戏加载、重大状态变化、时间跳跃 + */ +export const markAllDirty = (flags: DirtyFlags): void => { + flags.economyDirty = true + flags.fleetDirty = true + flags.npcDirty = true + flags.queuesDirty = true +} diff --git a/src/logic/missionResolverLogic.ts b/src/logic/missionResolverLogic.ts new file mode 100644 index 0000000..ae33fc7 --- /dev/null +++ b/src/logic/missionResolverLogic.ts @@ -0,0 +1,445 @@ +/** + * 任务解析纯函数 + * 所有函数都是纯函数:无副作用,相同输入总是产生相同输出 + * 便于单元测试和服务端复用 + */ + +import type { FleetMission, Resources, Fleet, Position, ExpeditionZone } from '@/types/game' +import { MissionType, ShipType } from '@/types/game' +import { SHIPS } from '@/config/gameConfig' + +/** + * 任务状态 + */ +export type MissionStatus = 'outbound' | 'returning' | 'arrived' | 'completed' + +/** + * 任务解析结果 + */ +export interface MissionResolution { + /** 新的任务状态 */ + newStatus: MissionStatus + /** 是否应该处理到达逻辑 */ + shouldProcessArrival: boolean + /** 是否应该处理返回逻辑 */ + shouldProcessReturn: boolean + /** 是否任务已完成(可以移除) */ + isCompleted: boolean +} + +/** + * 飞行时间计算结果 + */ +export interface FlightTimeResult { + /** 飞行时间(毫秒) */ + flightTimeMs: number + /** 到达时间戳 */ + arrivalTime: number + /** 返回时间戳(往返任务) */ + returnTime?: number +} + +/** + * 掠夺计算结果 + */ +export interface PlunderResult { + /** 可掠夺的资源 */ + plunderedResources: Resources + /** 实际装载的资源(受运载能力限制) */ + loadedResources: Resources + /** 剩余资源 */ + remainingResources: Resources +} + +/** + * 货舱容量计算结果 + */ +export interface CargoCapacityResult { + /** 总货舱容量 */ + totalCapacity: number + /** 已使用容量 */ + usedCapacity: number + /** 剩余容量 */ + remainingCapacity: number +} + +/** + * 解析任务当前状态(纯函数) + * 根据当前时间判断任务应该处于什么状态 + * @pure + */ +export const resolveMission = (mission: FleetMission, now: number): MissionResolution => { + const { status, arrivalTime, returnTime } = mission + + // 出发中 -> 检查是否到达 + if (status === 'outbound') { + if (now >= arrivalTime) { + return { + newStatus: 'returning', + shouldProcessArrival: true, + shouldProcessReturn: false, + isCompleted: false + } + } + return { + newStatus: 'outbound', + shouldProcessArrival: false, + shouldProcessReturn: false, + isCompleted: false + } + } + + // 返回中 -> 检查是否返回母星 + if (status === 'returning') { + if (returnTime && now >= returnTime) { + return { + newStatus: 'completed', + shouldProcessArrival: false, + shouldProcessReturn: true, + isCompleted: true + } + } + return { + newStatus: 'returning', + shouldProcessArrival: false, + shouldProcessReturn: false, + isCompleted: false + } + } + + // 已完成 + return { + newStatus: 'completed', + shouldProcessArrival: false, + shouldProcessReturn: false, + isCompleted: true + } +} + +/** + * 计算两点间距离(纯函数) + * @pure + */ +export const calcDistance = (from: Position, to: Position): number => { + // 同一星球 + if (from.galaxy === to.galaxy && from.system === to.system && from.position === to.position) { + return 5 + } + + // 同一星系 + if (from.galaxy === to.galaxy && from.system === to.system) { + return 1000 + 5 * Math.abs(from.position - to.position) + } + + // 同一银河 + if (from.galaxy === to.galaxy) { + return 2700 + 95 * Math.abs(from.system - to.system) + } + + // 不同银河 + return 20000 * Math.abs(from.galaxy - to.galaxy) +} + +/** + * 计算舰队最慢速度(纯函数) + * @pure + */ +export const calcFleetMinSpeed = (fleet: Partial): number => { + let minSpeed = Number.MAX_SAFE_INTEGER + + for (const [shipType, count] of Object.entries(fleet)) { + if (count && count > 0) { + const config = SHIPS[shipType as ShipType] + if (config && config.speed < minSpeed) { + minSpeed = config.speed + } + } + } + + return minSpeed === Number.MAX_SAFE_INTEGER ? 0 : minSpeed +} + +/** + * 计算飞行时间(纯函数) + * @pure + */ +export const calcFlightTime = (from: Position, to: Position, fleet: Partial, speedMultiplier: number = 1): number => { + const distance = calcDistance(from, to) + const minSpeed = calcFleetMinSpeed(fleet) + + if (minSpeed === 0) return 0 + + // 飞行时间公式:(10 + 35000 / speedMultiplier * sqrt(distance * 10 / minSpeed)) + const baseTime = 10 + (35000 / speedMultiplier) * Math.sqrt((distance * 10) / minSpeed) + + return Math.round(baseTime) +} + +/** + * 计算完整飞行时间结果(纯函数) + * @pure + */ +export const calcFlightTimeResult = ( + from: Position, + to: Position, + fleet: Partial, + departureTime: number, + speedMultiplier: number = 1, + isOneWay: boolean = false +): FlightTimeResult => { + const flightTimeSeconds = calcFlightTime(from, to, fleet, speedMultiplier) + const flightTimeMs = flightTimeSeconds * 1000 + const arrivalTime = departureTime + flightTimeMs + + return { + flightTimeMs, + arrivalTime, + returnTime: isOneWay ? undefined : arrivalTime + flightTimeMs + } +} + +/** + * 计算舰队货舱容量(纯函数) + * @pure + */ +export const calcCargoCapacity = (fleet: Partial, currentCargo?: Resources): CargoCapacityResult => { + let totalCapacity = 0 + + for (const [shipType, count] of Object.entries(fleet)) { + if (count && count > 0) { + const config = SHIPS[shipType as ShipType] + if (config) { + totalCapacity += config.cargoCapacity * count + } + } + } + + const usedCapacity = currentCargo ? currentCargo.metal + currentCargo.crystal + currentCargo.deuterium + currentCargo.darkMatter : 0 + + return { + totalCapacity, + usedCapacity, + remainingCapacity: Math.max(0, totalCapacity - usedCapacity) + } +} + +/** + * 计算掠夺资源(纯函数) + * @pure + */ +export const calcPlunder = (defenderResources: Resources, attackerFleet: Partial, plunderRatio: number = 0.5): PlunderResult => { + const { totalCapacity } = calcCargoCapacity(attackerFleet) + + // 可掠夺的资源(防守方资源的一定比例) + const availableMetal = Math.floor(defenderResources.metal * plunderRatio) + const availableCrystal = Math.floor(defenderResources.crystal * plunderRatio) + const availableDeuterium = Math.floor(defenderResources.deuterium * plunderRatio) + const availableDarkMatter = Math.floor(defenderResources.darkMatter * plunderRatio) + + const totalAvailable = availableMetal + availableCrystal + availableDeuterium + availableDarkMatter + + // 如果货舱容量足够,全部装载 + if (totalCapacity >= totalAvailable) { + const plunderedResources: Resources = { + metal: availableMetal, + crystal: availableCrystal, + deuterium: availableDeuterium, + darkMatter: availableDarkMatter, + energy: 0 + } + + return { + plunderedResources, + loadedResources: { ...plunderedResources }, + remainingResources: { + metal: defenderResources.metal - availableMetal, + crystal: defenderResources.crystal - availableCrystal, + deuterium: defenderResources.deuterium - availableDeuterium, + darkMatter: defenderResources.darkMatter - availableDarkMatter, + energy: defenderResources.energy + } + } + } + + // 按比例分配货舱容量 + const ratio = totalCapacity / totalAvailable + const loadedMetal = Math.floor(availableMetal * ratio) + const loadedCrystal = Math.floor(availableCrystal * ratio) + const loadedDeuterium = Math.floor(availableDeuterium * ratio) + const loadedDarkMatter = Math.floor(availableDarkMatter * ratio) + + return { + plunderedResources: { + metal: availableMetal, + crystal: availableCrystal, + deuterium: availableDeuterium, + darkMatter: availableDarkMatter, + energy: 0 + }, + loadedResources: { + metal: loadedMetal, + crystal: loadedCrystal, + deuterium: loadedDeuterium, + darkMatter: loadedDarkMatter, + energy: 0 + }, + remainingResources: { + metal: defenderResources.metal - loadedMetal, + crystal: defenderResources.crystal - loadedCrystal, + deuterium: defenderResources.deuterium - loadedDeuterium, + darkMatter: defenderResources.darkMatter - loadedDarkMatter, + energy: defenderResources.energy + } + } +} + +/** + * 计算燃料消耗(纯函数) + * @pure + */ +export const calcFuelConsumption = (fleet: Partial, distance: number, holdTime: number = 0): number => { + let totalConsumption = 0 + + for (const [shipType, count] of Object.entries(fleet)) { + if (count && count > 0) { + const config = SHIPS[shipType as ShipType] + if (config) { + // 基础燃料消耗 + const baseFuel = config.fuelConsumption || 0 + // 距离因子 + const distanceFactor = 1 + distance / 35000 + // 单艘船消耗 + const shipFuel = Math.ceil(baseFuel * distanceFactor * count) + totalConsumption += shipFuel + } + } + } + + // 驻留时间额外消耗 + if (holdTime > 0) { + totalConsumption += Math.ceil(totalConsumption * holdTime * 0.1) + } + + return totalConsumption +} + +/** + * 检查是否可以执行任务(纯函数) + * @pure + */ +export const canExecuteMission = ( + missionType: MissionType, + fleet: Partial, + resources: Resources, + fuelRequired: number +): { canExecute: boolean; reason?: string } => { + // 检查舰队是否为空 + const totalShips = Object.values(fleet).reduce((sum, count) => sum + (count || 0), 0) + if (totalShips === 0) { + return { canExecute: false, reason: 'noShips' } + } + + // 检查燃料是否足够 + if (resources.deuterium < fuelRequired) { + return { canExecute: false, reason: 'insufficientFuel' } + } + + // 殖民任务需要殖民船 + if (missionType === MissionType.Colonize) { + const colonyShipCount = fleet[ShipType.ColonyShip] ?? 0 + if (colonyShipCount < 1) { + return { canExecute: false, reason: 'noColonyShip' } + } + } + + // 侦查任务需要探测器 + if (missionType === MissionType.Spy) { + const probeCount = fleet[ShipType.EspionageProbe] ?? 0 + if (probeCount < 1) { + return { canExecute: false, reason: 'noSpyProbe' } + } + } + + // 回收任务需要回收船 + if (missionType === MissionType.Recycle) { + const recyclerCount = fleet[ShipType.Recycler] ?? 0 + if (recyclerCount < 1) { + return { canExecute: false, reason: 'noRecycler' } + } + } + + return { canExecute: true } +} + +/** + * 创建任务对象(纯函数) + * @pure + */ +export const createMission = ( + playerId: string, + originPlanetId: string, + targetPosition: Position, + missionType: MissionType, + fleet: Partial, + cargo: Resources, + departureTime: number, + flightTimeMs: number, + options?: { + targetPlanetId?: string + targetIsMoon?: boolean + isGift?: boolean + giftTargetNpcId?: string + expeditionZone?: ExpeditionZone + } +): FleetMission => { + const arrivalTime = departureTime + flightTimeMs + const returnTime = arrivalTime + flightTimeMs + + return { + id: `mission-${departureTime}-${Math.random().toString(36).substr(2, 9)}`, + playerId, + originPlanetId, + targetPosition, + targetPlanetId: options?.targetPlanetId, + targetIsMoon: options?.targetIsMoon, + missionType, + fleet: { ...fleet }, + cargo: { ...cargo }, + departureTime, + arrivalTime, + returnTime, + status: 'outbound', + isGift: options?.isGift, + giftTargetNpcId: options?.giftTargetNpcId, + expeditionZone: options?.expeditionZone + } +} + +/** + * 计算任务剩余时间(纯函数) + * @pure + */ +export const calcMissionRemainingTime = (mission: FleetMission, now: number): number => { + if (mission.status === 'outbound') { + return Math.max(0, mission.arrivalTime - now) + } + if (mission.status === 'returning' && mission.returnTime) { + return Math.max(0, mission.returnTime - now) + } + return 0 +} + +/** + * 计算召回任务的新返回时间(纯函数) + * @pure + */ +export const calcRecallReturnTime = (mission: FleetMission, now: number): number => { + if (mission.status !== 'outbound') { + return mission.returnTime || now + } + + // 计算已经飞行的时间 + const flownTime = now - mission.departureTime + // 返回时间 = 当前时间 + 已飞行时间 + return now + flownTime +} diff --git a/src/logic/productionLogic.ts b/src/logic/productionLogic.ts new file mode 100644 index 0000000..83b9bdd --- /dev/null +++ b/src/logic/productionLogic.ts @@ -0,0 +1,345 @@ +/** + * 生产计算纯函数 + * 所有函数都是纯函数:无副作用,相同输入总是产生相同输出 + * 便于单元测试和服务端复用 + */ + +import type { Planet, Resources } from '@/types/game' +import { BuildingType } from '@/types/game' + +/** + * 生产加成配置 + */ +export interface ProductionBonuses { + /** 能量生产加成百分比 */ + energyProductionBonus: number + /** 存储容量加成百分比 */ + storageCapacityBonus: number + /** 资源速度倍率 */ + resourceSpeed: number +} + +/** + * 科技加成配置 + */ +export interface TechBonuses { + /** 金属采矿研究等级 */ + mineralResearchLevel: number + /** 晶体采矿研究等级 */ + crystalResearchLevel: number + /** 燃料研究等级 */ + fuelResearchLevel: number +} + +/** + * 生产计算结果 + */ +export interface ProductionResult { + /** 资源生产量(每秒) */ + production: Resources + /** 能量生产量 */ + energyProduction: number + /** 能量消耗量 */ + energyConsumption: number + /** 能量比率(0-1) */ + energyRatio: number + /** 存储容量 */ + capacity: Resources +} + +// 基础配置常量 +const BASE_ENERGY_PRODUCTION = 0 +const BASE_METAL_PRODUCTION = 10 +const BASE_CRYSTAL_PRODUCTION = 5 +const BASE_DEUTERIUM_PRODUCTION = 0 +const BASE_STORAGE_CAPACITY = 10000 +const STORAGE_CAPACITY_FACTOR = 2 +const RESEARCH_PRODUCTION_BONUS_PER_LEVEL = 0.02 + +// 能量生产配置 +const SOLAR_PLANT_BASE = 50 +const SOLAR_PLANT_FACTOR = 1.1 +const FUSION_REACTOR_BASE = 150 +const FUSION_REACTOR_FACTOR = 1.15 + +// 能量消耗配置 +const METAL_MINE_ENERGY_BASE = 10 +const METAL_MINE_ENERGY_FACTOR = 1.1 +const CRYSTAL_MINE_ENERGY_BASE = 10 +const CRYSTAL_MINE_ENERGY_FACTOR = 1.1 +const DEUTERIUM_SYNTH_ENERGY_BASE = 15 +const DEUTERIUM_SYNTH_ENERGY_FACTOR = 1.1 + +// 资源生产配置(每小时) +const METAL_MINE_PRODUCTION_BASE = 30 +const METAL_MINE_PRODUCTION_FACTOR = 1.1 +const CRYSTAL_MINE_PRODUCTION_BASE = 20 +const CRYSTAL_MINE_PRODUCTION_FACTOR = 1.1 +const DEUTERIUM_SYNTH_PRODUCTION_BASE = 10 +const DEUTERIUM_SYNTH_PRODUCTION_FACTOR = 1.1 + +// 核聚变反应堆重氢消耗 +const FUSION_DEUTERIUM_BASE = 10 +const FUSION_DEUTERIUM_FACTOR = 1.1 + +/** + * 计算科技生产加成 + * @pure + */ +export const calcTechProductionBonus = ( + mineralResearchLevel: number = 0, + crystalResearchLevel: number = 0, + fuelResearchLevel: number = 0 +): { metalBonus: number; crystalBonus: number; deuteriumBonus: number } => { + return { + metalBonus: mineralResearchLevel * RESEARCH_PRODUCTION_BONUS_PER_LEVEL, + crystalBonus: crystalResearchLevel * RESEARCH_PRODUCTION_BONUS_PER_LEVEL, + deuteriumBonus: fuelResearchLevel * RESEARCH_PRODUCTION_BONUS_PER_LEVEL + } +} + +/** + * 计算能量生产(纯函数) + * @pure + */ +export const calcEnergyProduction = (planet: Planet, energyProductionBonus: number = 0): number => { + let totalEnergy = BASE_ENERGY_PRODUCTION + + // 太阳能电站 + const solarPlantLevel = planet.buildings[BuildingType.SolarPlant] || 0 + if (solarPlantLevel > 0) { + totalEnergy += Math.floor(SOLAR_PLANT_BASE * solarPlantLevel * Math.pow(SOLAR_PLANT_FACTOR, solarPlantLevel)) + } + + // 核聚变反应堆 + const fusionReactorLevel = planet.buildings[BuildingType.FusionReactor] || 0 + if (fusionReactorLevel > 0) { + totalEnergy += Math.floor(FUSION_REACTOR_BASE * fusionReactorLevel * Math.pow(FUSION_REACTOR_FACTOR, fusionReactorLevel)) + } + + // 应用加成 + return Math.floor(totalEnergy * (1 + energyProductionBonus)) +} + +/** + * 计算能量消耗(纯函数) + * @pure + */ +export const calcEnergyConsumption = (planet: Planet): number => { + let totalConsumption = 0 + + // 金属矿 + const metalMineLevel = planet.buildings[BuildingType.MetalMine] || 0 + if (metalMineLevel > 0) { + totalConsumption += Math.floor(METAL_MINE_ENERGY_BASE * metalMineLevel * Math.pow(METAL_MINE_ENERGY_FACTOR, metalMineLevel)) + } + + // 晶体矿 + const crystalMineLevel = planet.buildings[BuildingType.CrystalMine] || 0 + if (crystalMineLevel > 0) { + totalConsumption += Math.floor(CRYSTAL_MINE_ENERGY_BASE * crystalMineLevel * Math.pow(CRYSTAL_MINE_ENERGY_FACTOR, crystalMineLevel)) + } + + // 重氢合成器 + const deuteriumSynthesizerLevel = planet.buildings[BuildingType.DeuteriumSynthesizer] || 0 + if (deuteriumSynthesizerLevel > 0) { + totalConsumption += Math.floor( + DEUTERIUM_SYNTH_ENERGY_BASE * deuteriumSynthesizerLevel * Math.pow(DEUTERIUM_SYNTH_ENERGY_FACTOR, deuteriumSynthesizerLevel) + ) + } + + return totalConsumption +} + +/** + * 计算资源生产量(纯函数) + * @pure + * @param planet 星球状态 + * @param energyRatio 能量比率(0-1) + * @param techBonuses 科技加成 + * @returns 每秒资源生产量 + */ +export const calcResourceProduction = (planet: Planet, energyRatio: number, techBonuses?: TechBonuses): Resources => { + const effectiveRatio = Math.min(1, Math.max(0, energyRatio)) + const bonuses = techBonuses + ? calcTechProductionBonus(techBonuses.mineralResearchLevel, techBonuses.crystalResearchLevel, techBonuses.fuelResearchLevel) + : { metalBonus: 0, crystalBonus: 0, deuteriumBonus: 0 } + + // 金属生产 + let metalProduction = BASE_METAL_PRODUCTION + const metalMineLevel = planet.buildings[BuildingType.MetalMine] || 0 + if (metalMineLevel > 0) { + metalProduction += Math.floor( + METAL_MINE_PRODUCTION_BASE * metalMineLevel * Math.pow(METAL_MINE_PRODUCTION_FACTOR, metalMineLevel) * effectiveRatio + ) + } + metalProduction = Math.floor(metalProduction * (1 + bonuses.metalBonus)) + + // 晶体生产 + let crystalProduction = BASE_CRYSTAL_PRODUCTION + const crystalMineLevel = planet.buildings[BuildingType.CrystalMine] || 0 + if (crystalMineLevel > 0) { + crystalProduction += Math.floor( + CRYSTAL_MINE_PRODUCTION_BASE * crystalMineLevel * Math.pow(CRYSTAL_MINE_PRODUCTION_FACTOR, crystalMineLevel) * effectiveRatio + ) + } + crystalProduction = Math.floor(crystalProduction * (1 + bonuses.crystalBonus)) + + // 重氢生产 + let deuteriumProduction = BASE_DEUTERIUM_PRODUCTION + const deuteriumSynthesizerLevel = planet.buildings[BuildingType.DeuteriumSynthesizer] || 0 + if (deuteriumSynthesizerLevel > 0) { + // 重氢生产受温度影响 + const tempModifier = planet.temperature ? 1.28 - 0.002 * planet.temperature.max : 1 + deuteriumProduction += Math.floor( + DEUTERIUM_SYNTH_PRODUCTION_BASE * + deuteriumSynthesizerLevel * + Math.pow(DEUTERIUM_SYNTH_PRODUCTION_FACTOR, deuteriumSynthesizerLevel) * + effectiveRatio * + tempModifier + ) + } + + // 核聚变反应堆消耗重氢 + const fusionReactorLevel = planet.buildings[BuildingType.FusionReactor] || 0 + if (fusionReactorLevel > 0) { + deuteriumProduction -= Math.floor(FUSION_DEUTERIUM_BASE * fusionReactorLevel * Math.pow(FUSION_DEUTERIUM_FACTOR, fusionReactorLevel)) + } + + deuteriumProduction = Math.floor(deuteriumProduction * (1 + bonuses.deuteriumBonus)) + + return { + metal: metalProduction, + crystal: crystalProduction, + deuterium: deuteriumProduction, + darkMatter: 0, + energy: 0 + } +} + +/** + * 计算存储容量(纯函数) + * @pure + */ +export const calcStorageCapacity = (planet: Planet, storageCapacityBonus: number = 0): Resources => { + const baseCapacity = BASE_STORAGE_CAPACITY + const bonusMultiplier = 1 + storageCapacityBonus + + // 金属仓库 + const metalStorageLevel = planet.buildings[BuildingType.MetalStorage] || 0 + const metalCapacity = Math.floor(baseCapacity * Math.pow(STORAGE_CAPACITY_FACTOR, metalStorageLevel) * bonusMultiplier) + + // 晶体仓库 + const crystalStorageLevel = planet.buildings[BuildingType.CrystalStorage] || 0 + const crystalCapacity = Math.floor(baseCapacity * Math.pow(STORAGE_CAPACITY_FACTOR, crystalStorageLevel) * bonusMultiplier) + + // 重氢储罐 + const deuteriumTankLevel = planet.buildings[BuildingType.DeuteriumTank] || 0 + const deuteriumCapacity = Math.floor(baseCapacity * Math.pow(STORAGE_CAPACITY_FACTOR, deuteriumTankLevel) * bonusMultiplier) + + return { + metal: metalCapacity, + crystal: crystalCapacity, + deuterium: deuteriumCapacity, + darkMatter: Number.MAX_SAFE_INTEGER, + energy: 0 + } +} + +/** + * 计算完整的生产数据(纯函数) + * 这是主要的入口函数,聚合所有生产计算 + * @pure + */ +export const calcProduction = (planet: Planet, bonuses: ProductionBonuses, techBonuses?: TechBonuses): ProductionResult => { + const energyProduction = calcEnergyProduction(planet, bonuses.energyProductionBonus) + const energyConsumption = calcEnergyConsumption(planet) + const energyRatio = energyConsumption > 0 ? Math.min(1, energyProduction / energyConsumption) : 1 + const capacity = calcStorageCapacity(planet, bonuses.storageCapacityBonus) + + // 计算每秒生产量 + const productionPerSecond = calcResourceProduction(planet, energyRatio, techBonuses) + + // 应用资源速度倍率 + const production: Resources = { + metal: Math.floor(productionPerSecond.metal * bonuses.resourceSpeed), + crystal: Math.floor(productionPerSecond.crystal * bonuses.resourceSpeed), + deuterium: Math.floor(productionPerSecond.deuterium * bonuses.resourceSpeed), + darkMatter: 0, + energy: 0 + } + + return { + production, + energyProduction, + energyConsumption, + energyRatio, + capacity + } +} + +/** + * 计算时间段内的资源生产(纯函数) + * @pure + * @param planet 星球 + * @param deltaMs 时间间隔(毫秒) + * @param bonuses 加成配置 + * @param techBonuses 科技加成 + * @returns 时间段内生产的资源量 + */ +export const calcProductionForPeriod = ( + planet: Planet, + deltaMs: number, + bonuses: ProductionBonuses, + techBonuses?: TechBonuses +): Resources => { + const productionData = calcProduction(planet, bonuses, techBonuses) + const seconds = deltaMs / 1000 + + return { + metal: Math.floor(productionData.production.metal * seconds), + crystal: Math.floor(productionData.production.crystal * seconds), + deuterium: Math.floor(productionData.production.deuterium * seconds), + darkMatter: 0, + energy: 0 + } +} + +/** + * 应用资源生产到星球(纯函数,返回新的资源状态) + * @pure + * @param currentResources 当前资源 + * @param produced 生产的资源 + * @param capacity 存储容量 + * @returns 新的资源状态和溢出量 + */ +export const applyProduction = ( + currentResources: Resources, + produced: Resources, + capacity: Resources +): { newResources: Resources; overflow: Resources } => { + const newMetal = currentResources.metal + produced.metal + const newCrystal = currentResources.crystal + produced.crystal + const newDeuterium = currentResources.deuterium + produced.deuterium + + const clampedMetal = Math.min(newMetal, capacity.metal) + const clampedCrystal = Math.min(newCrystal, capacity.crystal) + const clampedDeuterium = Math.min(newDeuterium, capacity.deuterium) + + return { + newResources: { + metal: clampedMetal, + crystal: clampedCrystal, + deuterium: clampedDeuterium, + darkMatter: currentResources.darkMatter + produced.darkMatter, + energy: currentResources.energy + }, + overflow: { + metal: Math.max(0, newMetal - capacity.metal), + crystal: Math.max(0, newCrystal - capacity.crystal), + deuterium: Math.max(0, newDeuterium - capacity.deuterium), + darkMatter: 0, + energy: 0 + } + } +} diff --git a/src/logic/queueLogic.ts b/src/logic/queueLogic.ts new file mode 100644 index 0000000..d2521e9 --- /dev/null +++ b/src/logic/queueLogic.ts @@ -0,0 +1,332 @@ +/** + * 队列处理纯函数 + * 所有函数都是纯函数:无副作用,相同输入总是产生相同输出 + * 便于单元测试和服务端复用 + */ + +import type { BuildQueueItem, Fleet } from '@/types/game' +import { BuildingType, TechnologyType, ShipType, DefenseType } from '@/types/game' +import { BUILDINGS, TECHNOLOGIES, SHIPS, DEFENSES } from '@/config/gameConfig' + +/** + * 队列检查结果 + */ +export interface QueueCheckResult { + /** 已完成的项目 */ + completedItems: BuildQueueItem[] + /** 剩余的队列项目 */ + remainingItems: BuildQueueItem[] +} + +/** + * 建筑完成结果 + */ +export interface BuildingCompletionResult { + /** 新的建筑等级 */ + newBuildings: Record + /** 完成的建筑列表 */ + completed: Array<{ type: BuildingType; level: number }> +} + +/** + * 研究完成结果 + */ +export interface ResearchCompletionResult { + /** 新的科技等级 */ + newTechnologies: Record + /** 完成的研究列表 */ + completed: Array<{ type: TechnologyType; level: number }> +} + +/** + * 船厂完成结果 + */ +export interface ShipyardCompletionResult { + /** 新的舰队 */ + newFleet: Partial + /** 完成的船只列表 */ + completed: Array<{ type: ShipType; count: number }> +} + +/** + * 防御完成结果 + */ +export interface DefenseCompletionResult { + /** 新的防御设施 */ + newDefense: Partial> + /** 完成的防御列表 */ + completed: Array<{ type: DefenseType; count: number }> +} + +/** + * 检查队列中已完成的项目(纯函数) + * @pure + */ +export const checkQueueCompletion = (queue: BuildQueueItem[], now: number): QueueCheckResult => { + const completedItems: BuildQueueItem[] = [] + const remainingItems: BuildQueueItem[] = [] + + for (const item of queue) { + if (item.endTime && item.endTime <= now) { + completedItems.push(item) + } else { + remainingItems.push(item) + } + } + + return { completedItems, remainingItems } +} + +/** + * 应用建筑队列完成(纯函数) + * @pure + * @returns 新的建筑状态和完成列表 + */ +export const applyBuildingCompletion = ( + currentBuildings: Record, + completedItems: BuildQueueItem[] +): BuildingCompletionResult => { + const newBuildings = { ...currentBuildings } + const completed: Array<{ type: BuildingType; level: number }> = [] + + for (const item of completedItems) { + // 检查 type 是 'building' 或 'demolish' 且 itemType 是 BuildingType + if ((item.type === 'building' || item.type === 'demolish') && item.itemType in BuildingType) { + const buildingType = item.itemType as BuildingType + const currentLevel = newBuildings[buildingType] || 0 + + if (item.type === 'demolish') { + // 拆除:等级减1 + newBuildings[buildingType] = Math.max(0, currentLevel - 1) + } else { + // 建造:等级加1 或设置为目标等级 + newBuildings[buildingType] = item.targetLevel ?? currentLevel + 1 + } + completed.push({ type: buildingType, level: newBuildings[buildingType] }) + } + } + + return { newBuildings, completed } +} + +/** + * 应用研究队列完成(纯函数) + * @pure + * @returns 新的科技状态和完成列表 + */ +export const applyResearchCompletion = ( + currentTechnologies: Record, + completedItems: BuildQueueItem[] +): ResearchCompletionResult => { + const newTechnologies = { ...currentTechnologies } + const completed: Array<{ type: TechnologyType; level: number }> = [] + + for (const item of completedItems) { + if (item.type === 'technology' && item.itemType in TechnologyType) { + const techType = item.itemType as TechnologyType + const currentLevel = newTechnologies[techType] || 0 + newTechnologies[techType] = item.targetLevel ?? currentLevel + 1 + completed.push({ type: techType, level: newTechnologies[techType] }) + } + } + + return { newTechnologies, completed } +} + +/** + * 应用船厂队列完成(纯函数) + * @pure + * @returns 新的舰队状态和完成列表 + */ +export const applyShipyardCompletion = (currentFleet: Partial, completedItems: BuildQueueItem[]): ShipyardCompletionResult => { + const newFleet = { ...currentFleet } + const completed: Array<{ type: ShipType; count: number }> = [] + + for (const item of completedItems) { + if (item.type === 'ship' && item.itemType in ShipType) { + const shipType = item.itemType as ShipType + const currentCount = newFleet[shipType] || 0 + const addCount = item.quantity || 1 + newFleet[shipType] = currentCount + addCount + completed.push({ type: shipType, count: addCount }) + } + } + + return { newFleet, completed } +} + +/** + * 应用防御队列完成(纯函数) + * @pure + * @returns 新的防御状态和完成列表 + */ +export const applyDefenseCompletion = ( + currentDefense: Partial>, + completedItems: BuildQueueItem[] +): DefenseCompletionResult => { + const newDefense = { ...currentDefense } + const completed: Array<{ type: DefenseType; count: number }> = [] + + for (const item of completedItems) { + if (item.type === 'defense' && item.itemType in DefenseType) { + const defenseType = item.itemType as DefenseType + const currentCount = newDefense[defenseType] || 0 + const addCount = item.quantity || 1 + newDefense[defenseType] = currentCount + addCount + completed.push({ type: defenseType, count: addCount }) + } + } + + return { newDefense, completed } +} + +/** + * 计算建筑建造时间(纯函数) + * @pure + */ +export const calcBuildingTime = ( + buildingType: BuildingType, + level: number, + roboticsFactoryLevel: number = 0, + naniteFactoryLevel: number = 0, + gameSpeed: number = 1 +): number => { + const config = BUILDINGS[buildingType] + if (!config) return 0 + + const baseCost = config.baseCost + const metalCost = baseCost.metal * Math.pow(config.costMultiplier, level - 1) + const crystalCost = baseCost.crystal * Math.pow(config.costMultiplier, level - 1) + + // 基础建造时间(秒) + const baseTime = ((metalCost + crystalCost) / 2500) * 3600 + + // 机器人工厂加成 + const roboticsBonus = 1 + roboticsFactoryLevel + + // 纳米机器人工厂加成 + const naniteBonus = Math.pow(2, naniteFactoryLevel) + + // 最终时间 + return Math.max(1, Math.floor(baseTime / (roboticsBonus * naniteBonus * gameSpeed))) +} + +/** + * 计算研究时间(纯函数) + * @pure + */ +export const calcResearchTime = (techType: TechnologyType, level: number, researchLabLevel: number = 0, gameSpeed: number = 1): number => { + const config = TECHNOLOGIES[techType] + if (!config) return 0 + + const baseCost = config.baseCost + const metalCost = baseCost.metal * Math.pow(config.costMultiplier, level - 1) + const crystalCost = baseCost.crystal * Math.pow(config.costMultiplier, level - 1) + + // 基础研究时间(秒) + const baseTime = ((metalCost + crystalCost) / 1000) * 3600 + + // 研究实验室加成 + const labBonus = 1 + researchLabLevel + + // 最终时间 + return Math.max(1, Math.floor(baseTime / (labBonus * gameSpeed))) +} + +/** + * 计算船只建造时间(纯函数) + * @pure + */ +export const calcShipBuildTime = ( + shipType: ShipType, + count: number, + shipyardLevel: number = 0, + naniteFactoryLevel: number = 0, + gameSpeed: number = 1 +): number => { + const config = SHIPS[shipType] + if (!config) return 0 + + const { metal, crystal } = config.cost + + // 单艘船基础建造时间(秒) + const baseTime = ((metal + crystal) / 2500) * 3600 + + // 船厂加成 + const shipyardBonus = 1 + shipyardLevel + + // 纳米机器人工厂加成 + const naniteBonus = Math.pow(2, naniteFactoryLevel) + + // 单艘船最终时间 + const singleShipTime = Math.max(1, Math.floor(baseTime / (shipyardBonus * naniteBonus * gameSpeed))) + + return singleShipTime * count +} + +/** + * 计算防御建造时间(纯函数) + * @pure + */ +export const calcDefenseBuildTime = ( + defenseType: DefenseType, + count: number, + shipyardLevel: number = 0, + naniteFactoryLevel: number = 0, + gameSpeed: number = 1 +): number => { + const config = DEFENSES[defenseType] + if (!config) return 0 + + const { metal, crystal } = config.cost + + // 单个防御基础建造时间(秒) + const baseTime = ((metal + crystal) / 2500) * 3600 + + // 船厂加成 + const shipyardBonus = 1 + shipyardLevel + + // 纳米机器人工厂加成 + const naniteBonus = Math.pow(2, naniteFactoryLevel) + + // 单个防御最终时间 + const singleDefenseTime = Math.max(1, Math.floor(baseTime / (shipyardBonus * naniteBonus * gameSpeed))) + + return singleDefenseTime * count +} + +/** + * 检查是否可以开始下一个队列项(纯函数) + * @pure + */ +export const canStartNextQueueItem = (queue: BuildQueueItem[], now: number): boolean => { + if (queue.length === 0) return false + + const firstItem = queue[0] + if (!firstItem) return false + + // 如果第一个项目还没有开始时间,可以开始 + if (!firstItem.startTime) return true + // 如果第一个项目已经完成,可以开始下一个 + if (firstItem.endTime && firstItem.endTime <= now) return true + + return false +} + +/** + * 计算队列总剩余时间(纯函数) + * @pure + */ +export const calcQueueRemainingTime = (queue: BuildQueueItem[], now: number): number => { + if (queue.length === 0) return 0 + + let totalTime = 0 + for (const item of queue) { + if (item.endTime) { + const remaining = Math.max(0, item.endTime - now) + totalTime += remaining + } + } + + return totalTime +} diff --git a/src/services/economyEngine.ts b/src/services/economyEngine.ts new file mode 100644 index 0000000..a1132e0 --- /dev/null +++ b/src/services/economyEngine.ts @@ -0,0 +1,57 @@ +import type { TickContext } from './tickContext' +import type { UnlockedItem } from '@/logic/unlockLogic' +import * as gameLogic from '@/logic/gameLogic' +import { markEconomyDirty, markQueuesDirty } from '@/logic/dirtyFlags' + +export interface EconomyEngine { + /** 每 tick 调用,处理资源生产和队列完成 */ + tick(ctx: TickContext): void +} + +export interface EconomyEngineOptions { + /** 完成回调(建筑/科技/船舰/防御) */ + onNotification?: (type: string, itemType: string, level?: number) => void + /** 解锁回调 */ + onUnlock?: (unlockedItems: UnlockedItem[]) => void +} + +/** + * 创建经济引擎 + * 负责资源生产、建筑队列、研究队列、等待队列的处理 + */ +export const createEconomyEngine = (options: EconomyEngineOptions = {}): EconomyEngine => { + /** + * 主 tick 函数 + * 处理资源生产、队列完成 + */ + const tick = (ctx: TickContext) => { + const { gameStore, now, dirtyFlags } = ctx + + // 记录队列长度以检测完成 + const prevResearchQueueLen = gameStore.player.researchQueue.length + const prevBuildQueueLens = gameStore.player.planets.map(p => p.buildQueue.length) + + // 检查军官过期 + gameLogic.checkOfficersExpiration(gameStore.player.officers, now) + + // 处理游戏更新(资源生产、建造队列、研究队列、等待队列) + const result = gameLogic.processGameUpdate(gameStore.player, now, gameStore.gameSpeed, options.onNotification, options.onUnlock) + + // 更新研究队列(processGameUpdate 内部已处理,但需要同步到 store) + gameStore.player.researchQueue = result.updatedResearchQueue + + // 检测是否有队列完成(通过长度变化) + const researchCompleted = result.updatedResearchQueue.length < prevResearchQueueLen + const buildingCompleted = gameStore.player.planets.some((p, i) => p.buildQueue.length < (prevBuildQueueLens[i] ?? 0)) + + // 如果有队列完成,标记相关标志 + if (researchCompleted || buildingCompleted) { + markEconomyDirty(dirtyFlags) // 建筑/研究完成可能影响资源生产 + markQueuesDirty(dirtyFlags) // 队列状态已改变 + } + } + + return { + tick + } +} diff --git a/src/services/gameEngine.ts b/src/services/gameEngine.ts new file mode 100644 index 0000000..55717bc --- /dev/null +++ b/src/services/gameEngine.ts @@ -0,0 +1,156 @@ +import { useGameStore } from '@/stores/gameStore' +import { useUniverseStore } from '@/stores/universeStore' +import { useNPCStore } from '@/stores/npcStore' +import { createDirtyFlags, type DirtyFlags } from '@/logic/dirtyFlags' +import { createTickContext, type TickContext, type TranslateFn, type NotifyFn, type NotifyUnlockFn } from './tickContext' +import { profiler } from './profiler' + +export interface GameEngine { + /** 初始化引擎(加载/保存钩子等) */ + init(): void + /** 每帧更新 */ + tick(now: number, deltaMs: number): void + /** 暂停游戏 */ + pause(): void + /** 恢复游戏 */ + resume(): void + /** 清理资源 */ + dispose(): void + /** 是否已暂停 */ + isPaused(): boolean +} + +export interface GameEngineOptions { + /** 国际化函数 */ + t: TranslateFn + /** 自定义 tick 处理函数(用于渐进式迁移,接收 TickContext) */ + onTick?: (ctx: TickContext) => void | Promise + /** 暂停状态变化回调 */ + onPauseChange?: (paused: boolean) => void + /** 通知回调 */ + notify?: NotifyFn + /** 解锁通知回调 */ + notifyUnlock?: NotifyUnlockFn +} + +/** + * 创建游戏引擎 + * 作为游戏逻辑的中心协调器,管理所有"每帧发生的事情" + */ +export const createGameEngine = (options: GameEngineOptions): GameEngine => { + let initialized = false + let disposed = false + + // 获取 stores(延迟初始化,确保 Pinia 已准备好) + let gameStore: ReturnType | null = null + let universeStore: ReturnType | null = null + let npcStore: ReturnType | null = null + + // 脏标志(性能优化) + let dirtyFlags: DirtyFlags | null = null + + const getStores = () => { + if (!gameStore) gameStore = useGameStore() + if (!universeStore) universeStore = useUniverseStore() + if (!npcStore) npcStore = useNPCStore() + return { gameStore, universeStore, npcStore } + } + + const init = () => { + if (initialized) return + if (disposed) { + console.warn('[GameEngine] Cannot init after dispose') + return + } + + // 初始化 stores + getStores() + + // 初始化脏标志 + dirtyFlags = createDirtyFlags() + + initialized = true + } + + const tick = async (now: number, deltaMs: number) => { + if (!initialized) { + console.warn('[GameEngine] tick called before init') + return + } + if (disposed) return + + profiler.start('tick:total') + + const stores = getStores() + + // 检查暂停状态 + if (stores.gameStore.isPaused) { + profiler.end('tick:total') + return + } + + // 更新游戏时间 + stores.gameStore.gameTime = now + + // 构建 TickContext + const ctx = createTickContext( + { + ...stores, + t: options.t, + notify: options.notify, + notifyUnlock: options.notifyUnlock, + dirtyFlags: dirtyFlags! + }, + now, + deltaMs + ) + + // 调用自定义 tick 处理函数(用于渐进式迁移) + if (options.onTick) { + await options.onTick(ctx) + } + + profiler.end('tick:total') + profiler.reset() // 重置当前快照,准备下一次 tick + } + + const pause = () => { + const { gameStore } = getStores() + if (!gameStore.isPaused) { + gameStore.isPaused = true + options.onPauseChange?.(true) + } + } + + const resume = () => { + const { gameStore } = getStores() + if (gameStore.isPaused) { + gameStore.isPaused = false + options.onPauseChange?.(false) + } + } + + const isPaused = () => { + const { gameStore } = getStores() + return gameStore.isPaused + } + + const dispose = () => { + if (disposed) return + disposed = true + initialized = false + gameStore = null + universeStore = null + npcStore = null + dirtyFlags = null + } + + return { + init, + tick, + pause, + resume, + dispose, + isPaused + } +} diff --git a/src/services/missionEngine.ts b/src/services/missionEngine.ts new file mode 100644 index 0000000..da9bac2 --- /dev/null +++ b/src/services/missionEngine.ts @@ -0,0 +1,1014 @@ +import type { TickContext } from './tickContext' +import type { FleetMission, MissileAttack, NPC, MissionReport, Planet } from '@/types/game' +import { MissionType, DiplomaticEventType, ShipType } from '@/types/game' +import { DIPLOMATIC_CONFIG } from '@/config/gameConfig' +import * as fleetLogic from '@/logic/fleetLogic' +import * as shipLogic from '@/logic/shipLogic' +import * as resourceLogic from '@/logic/resourceLogic' +import * as gameLogic from '@/logic/gameLogic' +import * as npcBehaviorLogic from '@/logic/npcBehaviorLogic' +import * as diplomaticLogic from '@/logic/diplomaticLogic' +import * as missileLogic from '@/logic/missileLogic' // 静态导入,避免每次动态导入的开销 +import { markFleetDirty, markEconomyDirty, markNpcDirty } from '@/logic/dirtyFlags' + +export interface MissionEngine { + /** 处理所有任务的 tick */ + tick(ctx: TickContext): Promise +} + +export interface MissionEngineOptions { + /** 移除即将到来的舰队警报 */ + removeIncomingFleetAlert?: (missionId: string) => void +} + +/** + * 创建任务引擎 + * 处理玩家和NPC的舰队任务、导弹攻击 + */ +export const createMissionEngine = (options: MissionEngineOptions = {}): MissionEngine => { + const removeIncomingFleetAlert = options.removeIncomingFleetAlert ?? (() => {}) + + // 缓存 NPC 最早到达时间,避免每 tick 遍历所有 NPC + let cachedNpcEarliestArrival = Number.MAX_SAFE_INTEGER + + /** + * 处理玩家任务返回 + */ + const processPlayerMissionReturn = (ctx: TickContext, mission: FleetMission) => { + const { gameStore } = ctx + const originPlanet = gameStore.player.planets.find(p => p.id === mission.originPlanetId) + if (!originPlanet) return + + // 返还舰队和货物 + shipLogic.addFleet(originPlanet.fleet, mission.fleet) + resourceLogic.addResources(originPlanet.resources, mission.cargo) + + // 从任务列表中移除 + const missionIndex = gameStore.player.fleetMissions.indexOf(mission) + if (missionIndex > -1) { + gameStore.player.fleetMissions.splice(missionIndex, 1) + } + } + + /** + * 查找目标星球 + */ + const findTargetPlanet = (ctx: TickContext, mission: FleetMission): Planet | undefined => { + const { gameStore, universeStore } = ctx + const targetKey = gameLogic.generatePositionKey( + mission.targetPosition.galaxy, + mission.targetPosition.system, + mission.targetPosition.position + ) + + // 先从玩家星球中查找,再从宇宙地图中查找 + // 如果任务指定了targetIsMoon,需要精确匹配行星或月球 + return ( + gameStore.player.planets.find(p => { + const positionMatch = + p.position.galaxy === mission.targetPosition.galaxy && + p.position.system === mission.targetPosition.system && + p.position.position === mission.targetPosition.position + // 如果任务明确指定目标类型,按类型匹配 + if (mission.targetIsMoon !== undefined) { + return positionMatch && p.isMoon === mission.targetIsMoon + } + // 兼容旧任务:默认优先匹配行星(非月球) + return positionMatch && !p.isMoon + }) || + // 如果没有匹配到指定类型,尝试匹配同位置的任何星球 + gameStore.player.planets.find( + p => + p.position.galaxy === mission.targetPosition.galaxy && + p.position.system === mission.targetPosition.system && + p.position.position === mission.targetPosition.position + ) || + universeStore.planets[targetKey] + ) + } + + /** + * 处理玩家任务到达 + */ + const processPlayerMissionArrival = async (ctx: TickContext, mission: FleetMission) => { + const { gameStore, universeStore, npcStore, t } = ctx + + const targetPlanet = findTargetPlanet(ctx, mission) + const targetKey = gameLogic.generatePositionKey( + mission.targetPosition.galaxy, + mission.targetPosition.system, + mission.targetPosition.position + ) + + // 获取起始星球名称(用于报告) + const originPlanet = gameStore.player.planets.find(p => p.id === mission.originPlanetId) + const originPlanetName = originPlanet?.name || t('fleetView.unknownPlanet') + + // 确保 missionReports 存在 + if (!gameStore.player.missionReports) { + gameStore.player.missionReports = [] + } + + if (mission.missionType === MissionType.Transport) { + // 在处理任务之前保存货物信息(因为processTransportArrival会清空cargo) + const transportedResources = { ...mission.cargo } + const isGiftMission = mission.isGift && mission.giftTargetNpcId + const result = fleetLogic.processTransportArrival(mission, targetPlanet, gameStore.player, npcStore.npcs) + + // 更新成就统计(仅在成功时追踪) + if (result.success) { + const totalTransported = + transportedResources.metal + transportedResources.crystal + transportedResources.deuterium + transportedResources.darkMatter + if (isGiftMission) { + // 送礼成功 + gameLogic.trackDiplomacyStats(gameStore.player, 'gift', { resourcesAmount: totalTransported }) + } else { + // 普通运输任务成功 + gameLogic.trackMissionStats(gameStore.player, 'transport', { resourcesAmount: totalTransported }) + } + } + + // 生成失败原因消息 + let transportFailMessage = t('missionReports.transportFailed') + if (!result.success && result.failReason) { + if (result.failReason === 'targetNotFound') { + transportFailMessage = t('missionReports.transportFailedTargetNotFound') + } else if (result.failReason === 'giftRejected') { + transportFailMessage = t('missionReports.transportFailedGiftRejected') + } + } + + // 生成运输任务报告 + gameStore.player.missionReports.push({ + id: `mission-report-${mission.id}`, + timestamp: Date.now(), + missionType: MissionType.Transport, + originPlanetId: mission.originPlanetId, + originPlanetName, + targetPosition: mission.targetPosition, + targetPlanetId: targetPlanet?.id, + targetPlanetName: + targetPlanet?.name || `[${mission.targetPosition.galaxy}:${mission.targetPosition.system}:${mission.targetPosition.position}]`, + success: result.success, + message: result.success ? t('missionReports.transportSuccess') : transportFailMessage, + details: { + transportedResources, + failReason: result.failReason + }, + read: false + }) + } else if (mission.missionType === MissionType.Attack) { + const attackResult = await fleetLogic.processAttackArrival(mission, targetPlanet, gameStore.player, null, gameStore.player.planets) + if (attackResult) { + gameStore.player.battleReports.push(attackResult.battleResult) + + // 更新成就统计 - 攻击 + const debrisValue = attackResult.debrisField + ? attackResult.debrisField.resources.metal + attackResult.debrisField.resources.crystal + : 0 + const won = attackResult.battleResult.winner === 'attacker' + gameLogic.trackAttackStats(gameStore.player, attackResult.battleResult, won, debrisValue) + + // 检查是否攻击了NPC星球,更新外交关系 + if (targetPlanet) { + const targetNpc = npcStore.npcs.find(npc => npc.planets.some(p => p.id === targetPlanet.id)) + if (targetNpc) { + diplomaticLogic.handleAttackReputation(gameStore.player, targetNpc, attackResult.battleResult, npcStore.npcs, gameStore.locale) + + // 同步战斗损失到NPC的实际星球数据 + const npcPlanet = targetNpc.planets.find(p => p.id === targetPlanet.id) + if (npcPlanet) { + // 同步舰队损失 + Object.entries(attackResult.battleResult.defenderLosses.fleet).forEach(([shipType, lost]) => { + npcPlanet.fleet[shipType as ShipType] = Math.max(0, (npcPlanet.fleet[shipType as ShipType] || 0) - lost) + }) + // 同步防御损失(修复后的数据已在targetPlanet中) + npcPlanet.defense = { ...targetPlanet.defense } + // 同步资源(被掠夺后的) + npcPlanet.resources = { ...targetPlanet.resources } + } + } + } + + if (attackResult.moon) { + gameStore.player.planets.push(attackResult.moon) + } + if (attackResult.debrisField) { + // 将残骸场添加到游戏状态 + universeStore.debrisFields[attackResult.debrisField.id] = attackResult.debrisField + } + } + } else if (mission.missionType === MissionType.Colonize) { + const colonizeResult = fleetLogic.processColonizeArrival(mission, targetPlanet, gameStore.player, t('planet.colonyPrefix')) + const newPlanet = colonizeResult.planet + + // 更新成就统计 - 殖民 + if (colonizeResult.success && newPlanet) { + gameLogic.trackMissionStats(gameStore.player, 'colonize') + } + + // 生成失败原因消息 + let failMessage = t('missionReports.colonizeFailed') + if (!colonizeResult.success && colonizeResult.failReason) { + if (colonizeResult.failReason === 'positionOccupied') { + failMessage = t('missionReports.colonizeFailedOccupied') + } else if (colonizeResult.failReason === 'maxColoniesReached') { + failMessage = t('missionReports.colonizeFailedMaxColonies') + } + } + + // 生成殖民任务报告 + gameStore.player.missionReports.push({ + id: `mission-report-${mission.id}`, + timestamp: Date.now(), + missionType: MissionType.Colonize, + originPlanetId: mission.originPlanetId, + originPlanetName, + targetPosition: mission.targetPosition, + targetPlanetId: newPlanet?.id, + targetPlanetName: newPlanet?.name, + success: colonizeResult.success, + message: colonizeResult.success ? t('missionReports.colonizeSuccess') : failMessage, + details: newPlanet + ? { + newPlanetId: newPlanet.id, + newPlanetName: newPlanet.name + } + : { failReason: colonizeResult.failReason }, + read: false + }) + if (newPlanet) { + gameStore.player.planets.push(newPlanet) + } + } else if (mission.missionType === MissionType.Spy) { + const spyResult = fleetLogic.processSpyArrival(mission, targetPlanet, gameStore.player, null, npcStore.npcs) + if (spyResult.success && spyResult.report) { + gameStore.player.spyReports.push(spyResult.report) + // 更新成就统计 - 侦查 + gameLogic.trackMissionStats(gameStore.player, 'spy') + } + + // 生成侦查任务报告(即使失败也生成) + let spyFailMessage = t('missionReports.spyFailed') + if (!spyResult.success && spyResult.failReason) { + if (spyResult.failReason === 'targetNotFound') { + spyFailMessage = t('missionReports.spyFailedTargetNotFound') + } + } + + gameStore.player.missionReports.push({ + id: `mission-report-${mission.id}`, + timestamp: Date.now(), + missionType: MissionType.Spy, + originPlanetId: mission.originPlanetId, + originPlanetName, + targetPosition: mission.targetPosition, + targetPlanetId: targetPlanet?.id, + targetPlanetName: + targetPlanet?.name || `[${mission.targetPosition.galaxy}:${mission.targetPosition.system}:${mission.targetPosition.position}]`, + success: spyResult.success, + message: spyResult.success ? t('missionReports.spySuccess') : spyFailMessage, + details: spyResult.success ? { spyReportId: spyResult.report?.id } : { failReason: spyResult.failReason }, + read: false + }) + } else if (mission.missionType === MissionType.Deploy) { + const deployed = fleetLogic.processDeployArrival(mission, targetPlanet, gameStore.player.id, gameStore.player.technologies) + + // 更新成就统计 - 部署 + if (deployed.success) { + gameLogic.trackMissionStats(gameStore.player, 'deploy') + } + + // 生成失败原因消息 + let deployFailMessage = t('missionReports.deployFailed') + if (!deployed.success && deployed.failReason) { + if (deployed.failReason === 'targetNotFound') { + deployFailMessage = t('missionReports.deployFailedTargetNotFound') + } else if (deployed.failReason === 'notOwnPlanet') { + deployFailMessage = t('missionReports.deployFailedNotOwnPlanet') + } + } + + // 生成部署任务报告 + gameStore.player.missionReports.push({ + id: `mission-report-${mission.id}`, + timestamp: Date.now(), + missionType: MissionType.Deploy, + originPlanetId: mission.originPlanetId, + originPlanetName, + targetPosition: mission.targetPosition, + targetPlanetId: targetPlanet?.id, + targetPlanetName: + targetPlanet?.name || `[${mission.targetPosition.galaxy}:${mission.targetPosition.system}:${mission.targetPosition.position}]`, + success: deployed.success, + message: deployed.success ? t('missionReports.deploySuccess') : deployFailMessage, + details: { + deployedFleet: mission.fleet, + failReason: deployed.failReason + }, + read: false + }) + if (deployed.success && !deployed.overflow) { + const missionIndex = gameStore.player.fleetMissions.indexOf(mission) + if (missionIndex > -1) gameStore.player.fleetMissions.splice(missionIndex, 1) + return + } + } else if (mission.missionType === MissionType.Recycle) { + // 处理回收任务 + const debrisId = `debris_${mission.targetPosition.galaxy}_${mission.targetPosition.system}_${mission.targetPosition.position}` + const debrisField = universeStore.debrisFields[debrisId] + const recycleResult = fleetLogic.processRecycleArrival(mission, debrisField) + + // 更新成就统计 - 回收(无论是否有残骸都算飞行任务,但只有成功回收才计入回收资源量) + const totalRecycled = + recycleResult.success && recycleResult.collectedResources + ? recycleResult.collectedResources.metal + recycleResult.collectedResources.crystal + : 0 + gameLogic.trackMissionStats(gameStore.player, 'recycle', { resourcesAmount: totalRecycled }) + + // 生成失败原因消息 + let recycleFailMessage = t('missionReports.recycleFailed') + if (!recycleResult.success && recycleResult.failReason) { + if (recycleResult.failReason === 'noDebrisField') { + recycleFailMessage = t('missionReports.recycleFailedNoDebris') + } else if (recycleResult.failReason === 'debrisEmpty') { + recycleFailMessage = t('missionReports.recycleFailedDebrisEmpty') + } + } + + // 生成回收任务报告 + gameStore.player.missionReports.push({ + id: `mission-report-${mission.id}`, + timestamp: Date.now(), + missionType: MissionType.Recycle, + originPlanetId: mission.originPlanetId, + originPlanetName, + targetPosition: mission.targetPosition, + success: recycleResult.success, + message: recycleResult.success ? t('missionReports.recycleSuccess') : recycleFailMessage, + details: recycleResult.success + ? { + recycledResources: recycleResult.collectedResources, + remainingDebris: recycleResult.remainingDebris || undefined + } + : { failReason: recycleResult.failReason }, + read: false + }) + + if (recycleResult.success && recycleResult.collectedResources && debrisField) { + if (recycleResult.remainingDebris && (recycleResult.remainingDebris.metal > 0 || recycleResult.remainingDebris.crystal > 0)) { + // 更新残骸场 + universeStore.debrisFields[debrisId] = { + id: debrisField.id, + position: debrisField.position, + resources: recycleResult.remainingDebris, + createdAt: debrisField.createdAt, + expiresAt: debrisField.expiresAt + } + } else { + // 残骸场已被完全收集,删除 + delete universeStore.debrisFields[debrisId] + } + } + } else if (mission.missionType === MissionType.Destroy) { + // 处理行星毁灭任务(需要先战斗,再计算毁灭概率) + const destroyResult = await fleetLogic.processDestroyArrival(mission, targetPlanet, gameStore.player, null, gameStore.player.planets) + + // 处理战斗报告(如果发生了战斗) + if (destroyResult.battleResult) { + gameStore.player.battleReports.push(destroyResult.battleResult) + + // 处理战斗对NPC的影响 + if (targetPlanet) { + const targetNpc = npcStore.npcs.find(npc => npc.planets.some(p => p.id === targetPlanet.id)) + if (targetNpc) { + diplomaticLogic.handleAttackReputation(gameStore.player, targetNpc, destroyResult.battleResult, npcStore.npcs, gameStore.locale) + + // 同步战斗损失到NPC的实际星球数据 + const npcPlanet = targetNpc.planets.find(p => p.id === targetPlanet.id) + if (npcPlanet) { + Object.entries(destroyResult.battleResult.defenderLosses.fleet).forEach(([shipType, lost]) => { + npcPlanet.fleet[shipType as ShipType] = Math.max(0, (npcPlanet.fleet[shipType as ShipType] || 0) - lost) + }) + npcPlanet.defense = { ...targetPlanet.defense } + npcPlanet.resources = { ...targetPlanet.resources } + } + } + } + } + + // 处理新生成的月球 + if (destroyResult.moon) { + gameStore.player.planets.push(destroyResult.moon) + } + + // 处理残骸场 + if (destroyResult.debrisField) { + universeStore.debrisFields[destroyResult.debrisField.id] = destroyResult.debrisField + } + + // 更新成就统计 - 行星毁灭 + if (destroyResult.success) { + gameLogic.trackMissionStats(gameStore.player, 'destroy') + } + + // 生成失败原因消息 + let destroyFailMessage = t('missionReports.destroyFailed') + if (!destroyResult.success && destroyResult.failReason) { + if (destroyResult.failReason === 'targetNotFound') { + destroyFailMessage = t('missionReports.destroyFailedTargetNotFound') + } else if (destroyResult.failReason === 'ownPlanet') { + destroyFailMessage = t('missionReports.destroyFailedOwnPlanet') + } else if (destroyResult.failReason === 'noDeathstar') { + destroyFailMessage = t('missionReports.destroyFailedNoDeathstar') + } else if (destroyResult.failReason === 'chanceFailed') { + destroyFailMessage = t('missionReports.destroyFailedChance', { chance: destroyResult.destructionChance.toFixed(1) }) + } + } + + // 生成毁灭任务报告 + gameStore.player.missionReports.push({ + id: `mission-report-${mission.id}`, + timestamp: Date.now(), + missionType: MissionType.Destroy, + originPlanetId: mission.originPlanetId, + originPlanetName, + targetPosition: mission.targetPosition, + targetPlanetId: targetPlanet?.id, + targetPlanetName: targetPlanet?.name, + success: destroyResult.success, + message: destroyResult.success ? t('missionReports.destroySuccess') : destroyFailMessage, + details: destroyResult.success + ? { + destroyedPlanetName: + targetPlanet?.name || + `[${mission.targetPosition.galaxy}:${mission.targetPosition.system}:${mission.targetPosition.position}]`, + hadBattle: !!destroyResult.battleResult + } + : { + failReason: destroyResult.failReason, + destructionChance: destroyResult.destructionChance, + deathstarsLost: destroyResult.deathstarsLost, + hadBattle: !!destroyResult.battleResult + }, + read: false + }) + + if (destroyResult.success && destroyResult.planetId) { + // 星球被摧毁 + + // 处理外交关系(如果目标是NPC星球) + if (targetPlanet && targetPlanet.ownerId) { + const planetOwner = npcStore.npcs.find(npc => npc.id === targetPlanet.ownerId) + if (planetOwner) { + diplomaticLogic.handlePlanetDestructionReputation(gameStore.player, targetPlanet, planetOwner, npcStore.npcs, gameStore.locale) + + // 从NPC的星球列表中移除被摧毁的星球 + const npcPlanetIndex = planetOwner.planets.findIndex(p => p.id === destroyResult.planetId) + if (npcPlanetIndex > -1) { + planetOwner.planets.splice(npcPlanetIndex, 1) + } + + // 检查并处理被消灭的NPC(所有星球都被摧毁的NPC) + const eliminatedNpcIds = diplomaticLogic.checkAndHandleEliminatedNPCs(npcStore.npcs, gameStore.player, gameStore.locale) + + // 从npcStore中移除被消灭的NPC + if (eliminatedNpcIds.length > 0) { + npcStore.npcs = npcStore.npcs.filter(npc => !eliminatedNpcIds.includes(npc.id)) + } + } + } + + // 从玩家星球列表中移除(如果是玩家的星球) + const planetIndex = gameStore.player.planets.findIndex(p => p.id === destroyResult.planetId) + if (planetIndex > -1) { + gameStore.player.planets.splice(planetIndex, 1) + } else { + // 不是玩家星球,从宇宙地图中移除 + delete universeStore.planets[targetKey] + } + + // 取消所有前往该位置的NPC任务(回收、攻击、侦查等) + const destroyedDebrisId = `debris_${mission.targetPosition.galaxy}_${mission.targetPosition.system}_${mission.targetPosition.position}` + npcStore.npcs.forEach(npc => { + if (npc.fleetMissions) { + // 找到需要取消的任务(前往已摧毁星球位置的outbound任务) + const missionsToCancel = npc.fleetMissions.filter(m => { + if (m.status !== 'outbound') return false + // 检查回收任务的残骸场ID + if (m.missionType === MissionType.Recycle && m.debrisFieldId === destroyedDebrisId) { + return true + } + // 检查其他任务的目标星球ID + if (m.targetPlanetId === destroyResult.planetId) { + return true + } + return false + }) + + // 将这些任务的舰队返回给NPC + missionsToCancel.forEach(m => { + const npcOriginPlanet = npc.planets.find(p => p.id === m.originPlanetId) + if (npcOriginPlanet) { + shipLogic.addFleet(npcOriginPlanet.fleet, m.fleet) + } + }) + + // 从任务列表中移除这些任务 + npc.fleetMissions = npc.fleetMissions.filter(m => !missionsToCancel.includes(m)) + } + + // 清理关于被摧毁星球的侦查报告 + if (npc.playerSpyReports && destroyResult.planetId && destroyResult.planetId in npc.playerSpyReports) { + delete npc.playerSpyReports[destroyResult.planetId] + } + }) + + // 同时删除该位置的残骸场(星球被摧毁后残骸场也消失) + delete universeStore.debrisFields[destroyedDebrisId] + } + } else if (mission.missionType === MissionType.Expedition) { + // 处理探险任务 + const expeditionResult = fleetLogic.processExpeditionArrival(mission) + + // 确保返回时间正确设置(兼容旧版本任务数据) + // 如果 returnTime 不存在或已过期,重新计算 + const now = Date.now() + if (!mission.returnTime || mission.returnTime <= now) { + // 返回时间应该等于当前时间加上单程飞行时间 + const flightDuration = mission.arrivalTime - mission.departureTime + mission.returnTime = now + flightDuration + } + + // 更新成就统计 - 探险 + const isSuccessful = + expeditionResult.eventType === 'resources' || expeditionResult.eventType === 'darkMatter' || expeditionResult.eventType === 'fleet' + gameLogic.trackMissionStats(gameStore.player, 'expedition', { successful: isSuccessful }) + + // 根据事件类型生成不同的报告消息 + let reportMessage = '' + let reportDetails: Record = { + // 保存探险区域信息 + expeditionZone: mission.expeditionZone + } + + switch (expeditionResult.eventType) { + case 'resources': + reportMessage = t('missionReports.expeditionResources') + reportDetails.foundResources = expeditionResult.resources + break + case 'darkMatter': + reportMessage = t('missionReports.expeditionDarkMatter') + reportDetails.foundResources = expeditionResult.resources + break + case 'fleet': + reportMessage = t('missionReports.expeditionFleet') + reportDetails.foundFleet = expeditionResult.fleet + break + case 'pirates': + reportMessage = expeditionResult.fleetLost + ? t('missionReports.expeditionPiratesAttack') + : t('missionReports.expeditionPiratesEscaped') + if (expeditionResult.fleetLost) reportDetails.fleetLost = expeditionResult.fleetLost + break + case 'aliens': + reportMessage = expeditionResult.fleetLost + ? t('missionReports.expeditionAliensAttack') + : t('missionReports.expeditionAliensEscaped') + if (expeditionResult.fleetLost) reportDetails.fleetLost = expeditionResult.fleetLost + break + default: + reportMessage = t('missionReports.expeditionNothing') + } + + gameStore.player.missionReports.push({ + id: `mission-report-${mission.id}`, + timestamp: Date.now(), + missionType: MissionType.Expedition, + originPlanetId: mission.originPlanetId, + originPlanetName, + targetPosition: mission.targetPosition, + success: expeditionResult.eventType !== 'nothing', + message: reportMessage, + details: reportDetails, + read: false + }) + } + } + + /** + * 处理NPC任务到达 + */ + const processNPCMissionArrival = async (ctx: TickContext, npc: NPC, mission: FleetMission) => { + const { gameStore, universeStore } = ctx + + if (mission.missionType === MissionType.Recycle) { + // NPC回收任务 + const debrisId = mission.debrisFieldId + if (!debrisId) { + console.warn('[NPC Mission] Recycle mission missing debrisFieldId') + mission.status = 'returning' + mission.returnTime = Date.now() + (mission.arrivalTime - mission.departureTime) + return + } + + const debrisField = universeStore.debrisFields[debrisId] + const recycleResult = fleetLogic.processRecycleArrival(mission, debrisField) + + if (recycleResult && debrisField && recycleResult.collectedResources) { + const totalRecycled = recycleResult.collectedResources.metal + recycleResult.collectedResources.crystal + if (totalRecycled > 0) { + gameLogic.trackDiplomacyStats(gameStore.player, 'debrisRecycledByNPC', { resourcesAmount: totalRecycled }) + } + + if (recycleResult.remainingDebris && (recycleResult.remainingDebris.metal > 0 || recycleResult.remainingDebris.crystal > 0)) { + universeStore.debrisFields[debrisId] = { + id: debrisField.id, + position: debrisField.position, + resources: recycleResult.remainingDebris, + createdAt: debrisField.createdAt + } + } else { + delete universeStore.debrisFields[debrisId] + } + } + + removeIncomingFleetAlert(mission.id) + mission.returnTime = Date.now() + (mission.arrivalTime - mission.departureTime) + return + } + + // 找到目标星球 + const targetKey = gameLogic.generatePositionKey( + mission.targetPosition.galaxy, + mission.targetPosition.system, + mission.targetPosition.position + ) + const targetPlanet = + gameStore.player.planets.find( + p => + p.position.galaxy === mission.targetPosition.galaxy && + p.position.system === mission.targetPosition.system && + p.position.position === mission.targetPosition.position + ) || universeStore.planets[targetKey] + + if (!targetPlanet) { + console.warn('[NPC Mission] Target planet not found') + return + } + + if (mission.missionType === MissionType.Spy) { + // NPC侦查 + const { spiedNotification, spyReport } = npcBehaviorLogic.processNPCSpyArrival(npc, mission, targetPlanet, gameStore.player) + + gameLogic.trackDiplomacyStats(gameStore.player, 'spiedByNPC') + + if (!npc.playerSpyReports) { + npc.playerSpyReports = {} + } + npc.playerSpyReports[targetPlanet.id] = spyReport + + if (!gameStore.player.spiedNotifications) { + gameStore.player.spiedNotifications = [] + } + gameStore.player.spiedNotifications.push(spiedNotification) + + removeIncomingFleetAlert(mission.id) + } else if (mission.missionType === MissionType.Attack) { + // NPC攻击 + const attackResult = await fleetLogic.processNPCAttackArrival(npc, mission, targetPlanet, gameStore.player, gameStore.player.planets) + + if (attackResult) { + gameLogic.trackDiplomacyStats(gameStore.player, 'attackedByNPC') + const debrisValue = attackResult.debrisField + ? attackResult.debrisField.resources.metal + attackResult.debrisField.resources.crystal + : 0 + const won = attackResult.battleResult.winner === 'defender' + gameLogic.trackDefenseStats(gameStore.player, attackResult.battleResult, won, debrisValue) + + gameStore.player.battleReports.push(attackResult.battleResult) + + if (attackResult.moon) { + gameStore.player.planets.push(attackResult.moon) + } + + if (attackResult.debrisField) { + const existingDebris = universeStore.debrisFields[attackResult.debrisField.id] + if (existingDebris) { + universeStore.debrisFields[attackResult.debrisField.id] = { + ...existingDebris, + resources: { + metal: existingDebris.resources.metal + attackResult.debrisField.resources.metal, + crystal: existingDebris.resources.crystal + attackResult.debrisField.resources.crystal + } + } + } else { + universeStore.debrisFields[attackResult.debrisField.id] = attackResult.debrisField + } + } + } + + removeIncomingFleetAlert(mission.id) + } + } + + /** + * 处理NPC任务返回 + */ + const processNPCMissionReturn = (_ctx: TickContext, npc: NPC, mission: FleetMission) => { + const originPlanet = npc.planets.find(p => p.id === mission.originPlanetId) + if (!originPlanet) return + + // 返还舰队 + shipLogic.addFleet(originPlanet.fleet, mission.fleet) + + // 返还掠夺资源 + if (mission.cargo) { + originPlanet.resources.metal += mission.cargo.metal + originPlanet.resources.crystal += mission.cargo.crystal + originPlanet.resources.deuterium += mission.cargo.deuterium + } + + // 从NPC任务列表中移除 + if (npc.fleetMissions) { + const missionIndex = npc.fleetMissions.indexOf(mission) + if (missionIndex > -1) { + npc.fleetMissions.splice(missionIndex, 1) + } + } + } + + /** + * 处理导弹攻击到达 + */ + const processMissileAttackArrival = (ctx: TickContext, missileAttack: MissileAttack) => { + const { gameStore, universeStore, npcStore, t } = ctx + // missileLogic 已在顶部静态导入 + + // 找到目标星球 + const targetKey = gameLogic.generatePositionKey( + missileAttack.targetPosition.galaxy, + missileAttack.targetPosition.system, + missileAttack.targetPosition.position + ) + const targetPlanet = + gameStore.player.planets.find( + p => + p.position.galaxy === missileAttack.targetPosition.galaxy && + p.position.system === missileAttack.targetPosition.system && + p.position.position === missileAttack.targetPosition.position + ) || universeStore.planets[targetKey] + + // 确保 missionReports 存在 + if (!gameStore.player.missionReports) { + gameStore.player.missionReports = [] + } + + const originPlanetName = gameStore.player.planets.find(p => p.id === missileAttack.originPlanetId)?.name || t('fleetView.unknownPlanet') + + // 如果目标星球不存在,导弹失败 + if (!targetPlanet) { + missileAttack.status = 'arrived' + gameStore.player.missionReports.push({ + id: `missile-report-${missileAttack.id}`, + timestamp: Date.now(), + missionType: MissionType.MissileAttack, + originPlanetId: missileAttack.originPlanetId, + originPlanetName, + targetPosition: missileAttack.targetPosition, + targetPlanetId: undefined, + targetPlanetName: `[${missileAttack.targetPosition.galaxy}:${missileAttack.targetPosition.system}:${missileAttack.targetPosition.position}]`, + success: false, + message: t('missionReports.missileAttackFailed'), + details: { + missileCount: missileAttack.missileCount, + missileHits: 0, + missileIntercepted: 0, + defenseLosses: {} + }, + read: false + } as MissionReport) + return + } + + // 计算导弹攻击结果 + const impactResult = missileLogic.calculateMissileImpact(missileAttack.missileCount, targetPlanet) + + // 应用损失到目标星球 + missileLogic.applyMissileAttackResult(targetPlanet, impactResult.defenseLosses) + + // 如果目标是NPC的星球,同步损失到NPC实际数据并扣除外交好感度 + if (targetPlanet.ownerId && targetPlanet.ownerId !== gameStore.player.id) { + const targetNpc = npcStore.npcs.find(npc => npc.id === targetPlanet.ownerId) + if (targetNpc) { + const npcPlanet = targetNpc.planets.find(p => p.id === targetPlanet.id) + if (npcPlanet) { + missileLogic.applyMissileAttackResult(npcPlanet, impactResult.defenseLosses) + } + + // 导弹攻击扣除好感度 + const { REPUTATION_CHANGES } = DIPLOMATIC_CONFIG + const reputationLoss = REPUTATION_CHANGES.ATTACK / 2 + + if (!targetNpc.relations) { + targetNpc.relations = {} + } + const npcRelation = diplomaticLogic.getOrCreateRelation(targetNpc.relations, targetNpc.id, gameStore.player.id) + targetNpc.relations[gameStore.player.id] = diplomaticLogic.updateReputation( + npcRelation, + reputationLoss, + DiplomaticEventType.Attack, + t('diplomacy.reports.wasAttackedByMissile') + ) + } + } + + // 标记导弹攻击为已到达 + missileAttack.status = 'arrived' + + // 生成导弹攻击报告 + const reportMessage = + impactResult.missileHits > 0 + ? `${t('missionReports.missileAttackSuccess')}: ${impactResult.missileHits} ${t('missionReports.hits')}` + : t('missionReports.missileAttackIntercepted') + + gameStore.player.missionReports.push({ + id: `missile-report-${missileAttack.id}`, + timestamp: Date.now(), + missionType: MissionType.MissileAttack, + originPlanetId: missileAttack.originPlanetId, + originPlanetName, + targetPosition: missileAttack.targetPosition, + targetPlanetId: targetPlanet.id, + targetPlanetName: targetPlanet.name, + success: true, + message: reportMessage, + details: { + missileCount: missileAttack.missileCount, + missileHits: impactResult.missileHits, + missileIntercepted: impactResult.missileIntercepted, + defenseLosses: impactResult.defenseLosses + }, + read: false + } as MissionReport) + } + + /** + * 查找玩家任务最早到达时间(快速,只遍历玩家数据) + */ + const findPlayerEarliestArrival = ( + playerMissions: FleetMission[], + missileAttacks: MissileAttack[] + ): number => { + let earliest = Number.MAX_SAFE_INTEGER + + for (const m of playerMissions) { + if (m.status === 'outbound' && m.arrivalTime < earliest) { + earliest = m.arrivalTime + } else if (m.status === 'returning' && m.returnTime && m.returnTime < earliest) { + earliest = m.returnTime + } + } + + for (const m of missileAttacks) { + if (m.status === 'flying' && m.arrivalTime < earliest) { + earliest = m.arrivalTime + } + } + + return earliest + } + + /** + * 计算 NPC 任务最早到达时间 + */ + const calcNpcMissionStats = (npcs: NPC[]): { earliest: number } => { + let earliest = Number.MAX_SAFE_INTEGER + + for (const npc of npcs) { + if (!npc.fleetMissions) continue + for (const m of npc.fleetMissions) { + if (m.status === 'outbound' && m.arrivalTime < earliest) { + earliest = m.arrivalTime + } else if (m.status === 'returning' && m.returnTime && m.returnTime < earliest) { + earliest = m.returnTime + } + } + } + + return { earliest } + } + + /** + * 获取 NPC 任务最早到达时间(带缓存) + * 只在缓存过期时重新计算(避免每 tick 遍历 200+ NPC) + */ + const getNpcEarliestArrival = (npcs: NPC[], now: number): number => { + // 只有当缓存时间已过期时才重新计算 + // 这意味着新添加的 NPC 任务可能延迟最多 1 tick 被发现,但性能提升很大 + if (cachedNpcEarliestArrival <= now) { + const stats = calcNpcMissionStats(npcs) + cachedNpcEarliestArrival = stats.earliest + } + + return cachedNpcEarliestArrival + } + + /** + * 主 tick 函数 + * 优化:使用缓存的 NPC 到达时间,避免每 tick 遍历 200+ NPC + */ + const tick = async (ctx: TickContext) => { + const { gameStore, npcStore, now, dirtyFlags } = ctx + + // 诊断计时 + const t0 = performance.now() + + const playerMissions = gameStore.player.fleetMissions + const missileAttacks = gameStore.player.missileAttacks + const npcs = npcStore.npcs + + const t1 = performance.now() + + // 快速计算玩家任务最早到达时间(通常只有几个任务) + const playerEarliest = findPlayerEarliestArrival(playerMissions, missileAttacks) + + const t2 = performance.now() + + // 获取 NPC 任务最早到达时间(使用缓存,避免遍历 200+ NPC) + const npcEarliest = getNpcEarliestArrival(npcs, now) + + const t3 = performance.now() + + // 计算全局最早到达时间 + const earliestArrival = Math.min(playerEarliest, npcEarliest) + + // 诊断日志:定位性能瓶颈 + const storeAccess = t1 - t0 + const playerCalc = t2 - t1 + const npcCalc = t3 - t2 + if (storeAccess > 5 || playerCalc > 5 || npcCalc > 5) { + console.log('[MissionEngine] Timing breakdown:', { + storeAccess: storeAccess.toFixed(2) + 'ms', + playerCalc: playerCalc.toFixed(2) + 'ms', + npcCalc: npcCalc.toFixed(2) + 'ms', + earlyExit: earliestArrival > now + }) + } + + // 如果还没到达时间点则跳过 + if (earliestArrival > now) { + return + } + + let missionProcessed = false + + // 处理玩家舰队任务(只处理到达时间 <= now 的) + for (const mission of playerMissions) { + // 处理到达(outbound -> 任务执行) + if (mission.status === 'outbound' && now >= mission.arrivalTime) { + await processPlayerMissionArrival(ctx, mission) + missionProcessed = true + } + // 处理返回(returning -> 返回母星) + else if (mission.status === 'returning' && mission.returnTime && now >= mission.returnTime) { + processPlayerMissionReturn(ctx, mission) + missionProcessed = true + } + } + + // 处理导弹攻击(反向循环以便安全删除) + for (let i = missileAttacks.length - 1; i >= 0; i--) { + const missileAttack = missileAttacks[i] + if (missileAttack && missileAttack.status === 'flying' && now >= missileAttack.arrivalTime) { + processMissileAttackArrival(ctx, missileAttack) + missileAttacks.splice(i, 1) + missionProcessed = true + } + } + + // 处理 NPC 舰队任务 + for (const npc of npcs) { + if (!npc.fleetMissions || npc.fleetMissions.length === 0) continue + + for (const mission of npc.fleetMissions) { + if (mission.status === 'outbound' && now >= mission.arrivalTime) { + await processNPCMissionArrival(ctx, npc, mission) + missionProcessed = true + } else if (mission.status === 'returning' && mission.returnTime && now >= mission.returnTime) { + processNPCMissionReturn(ctx, npc, mission) + missionProcessed = true + } + } + } + + // 如果有任务被处理,标记相关标志为脏(需要重新计算) + if (missionProcessed) { + markFleetDirty(dirtyFlags) + markEconomyDirty(dirtyFlags) // 任务可能改变资源 + markNpcDirty(dirtyFlags) // 任务可能影响NPC状态 + } + } + + return { tick } +} diff --git a/src/services/npcEngine.ts b/src/services/npcEngine.ts new file mode 100644 index 0000000..b2983dc --- /dev/null +++ b/src/services/npcEngine.ts @@ -0,0 +1,425 @@ +import type { TickContext } from './tickContext' +import type { NPC } from '@/types/game' +import * as gameLogic from '@/logic/gameLogic' +import * as npcGrowthLogic from '@/logic/npcGrowthLogic' +import * as npcBehaviorLogic from '@/logic/npcBehaviorLogic' +import * as diplomaticLogic from '@/logic/diplomaticLogic' +import { generateNPCName } from '@/logic/npcNameGenerator' + +export interface NpcEngine { + /** 每 tick 调用,处理 NPC 更新 */ + tick(ctx: TickContext): void + /** 设置每 tick 更新的 NPC 数量 */ + setUpdateSlice(size: number): void + /** 获取当前 slice 大小 */ + getUpdateSlice(): number +} + +export interface NpcEngineOptions { + /** 每 tick 更新的 NPC 数量(默认 20) */ + sliceSize?: number + /** Growth 更新间隔(秒,默认 5) */ + growthInterval?: number + /** Behavior 更新间隔(秒,默认 5) */ + behaviorInterval?: number + /** 通知回调 */ + onTradeOffer?: (offer: any) => void + onAttitudeChange?: (notification: any) => void + onIntelReport?: (report: any) => void + onJointAttackInvite?: (invite: any) => void + onAidReceived?: (notification: any) => void +} + +/** + * 创建 NPC 引擎 + * 负责 NPC 成长和行为更新,支持 throttling + */ +export const createNpcEngine = (options: NpcEngineOptions = {}): NpcEngine => { + const { sliceSize: initialSliceSize = 20, growthInterval = 5, behaviorInterval = 5 } = options + + // 内部状态 + let sliceSize = initialSliceSize + let growthCursorIndex = 0 + let behaviorCursorIndex = 0 + let growthAccumulator = 0 + let behaviorAccumulator = 0 + let initialized = false + + /** + * 同步 NPC 星球数据到 universeStore + */ + const syncNPCPlanetToUniverse = (ctx: TickContext, npc: NPC) => { + const { universeStore } = ctx + npc.planets.forEach(npcPlanet => { + const planetKey = gameLogic.generatePositionKey(npcPlanet.position.galaxy, npcPlanet.position.system, npcPlanet.position.position) + const universePlanet = universeStore.planets[planetKey] + if (universePlanet) { + universePlanet.resources = { ...npcPlanet.resources } + universePlanet.buildings = { ...npcPlanet.buildings } + universePlanet.fleet = { ...npcPlanet.fleet } + universePlanet.defense = { ...npcPlanet.defense } + } + }) + } + + /** + * 初始化 NPC(如果 store 为空) + */ + const initializeNPCs = (ctx: TickContext) => { + const { gameStore, universeStore, npcStore } = ctx + const allPlanets = Object.values(universeStore.planets) + + if (npcStore.npcs.length > 0) { + initialized = true + return + } + + const npcMap = new Map() + const now = Date.now() + + allPlanets.forEach(planet => { + if (planet.ownerId === gameStore.player.id || !planet.ownerId) return + + if (!npcMap.has(planet.ownerId)) { + const randomSpyOffset = Math.random() * 240 * 1000 + const randomAttackOffset = Math.random() * 480 * 1000 + + const initialRelations: Record = {} + initialRelations[gameStore.player.id] = { + fromId: planet.ownerId, + toId: gameStore.player.id, + reputation: 0, + status: 'neutral' as const, + lastUpdated: now, + history: [] + } + + npcMap.set(planet.ownerId, { + id: planet.ownerId, + name: generateNPCName(planet.ownerId, gameStore.locale), + planets: [], + technologies: {}, + difficulty: 'medium' as const, + relations: initialRelations, + allies: [], + enemies: [], + lastSpyTime: now - randomSpyOffset, + lastAttackTime: now - randomAttackOffset, + fleetMissions: [], + playerSpyReports: {} + } as unknown as NPC) + } + + npcMap.get(planet.ownerId)!.planets.push(planet as any) + }) + + npcStore.npcs = Array.from(npcMap.values()) + + if (npcStore.npcs.length > 0) { + const homeworld = gameStore.player.planets.find(p => !p.isMoon) + if (homeworld) { + npcStore.npcs.forEach(npc => { + npcGrowthLogic.initializeNPCByDistance(npc, homeworld.position) + syncNPCPlanetToUniverse(ctx, npc) + }) + } + npcGrowthLogic.initializeNPCDiplomacy(npcStore.npcs) + } + + initialized = true + } + + /** + * 确保 NPC 数据完整性(修复旧版本数据) + */ + const ensureNPCDataIntegrity = (ctx: TickContext) => { + const { gameStore, npcStore } = ctx + const now = Date.now() + const homeworld = gameStore.player.planets.find(p => !p.isMoon) + + if (npcStore.npcs.length === 0) return + + // 确保所有 NPC 都有间谍探测器 + npcGrowthLogic.ensureNPCSpyProbes(npcStore.npcs) + + // 确保所有 NPC 都有 AI 类型 + npcGrowthLogic.ensureAllNPCsAIType(npcStore.npcs) + + // 确保所有 NPC 都与玩家建立了关系 + npcStore.npcs.forEach(npc => { + if (!npc.relations) { + npc.relations = {} + } + if (!npc.relations[gameStore.player.id]) { + npc.relations[gameStore.player.id] = { + fromId: npc.id, + toId: gameStore.player.id, + reputation: 0, + status: 'neutral' as const, + lastUpdated: now, + history: [] + } + } + + // 迁移旧存档:如果 NPC 没有距离数据 + if (homeworld && npc.distanceToHomeworld === undefined) { + const npcPlanet = npc.planets[0] + if (npcPlanet) { + npc.distanceToHomeworld = npcGrowthLogic.calculateDistanceToHomeworld(npcPlanet.position, homeworld.position) + npc.difficultyLevel = npcGrowthLogic.calculateDifficultyLevel(npc.distanceToHomeworld) + npcGrowthLogic.initializeNPCByDistance(npc, homeworld.position) + syncNPCPlanetToUniverse(ctx, npc) + } + } + }) + } + + /** + * 更新 NPC 成长(分片处理) + */ + const updateGrowth = (ctx: TickContext, elapsedSeconds: number) => { + const { gameStore, npcStore } = ctx + + if (npcStore.npcs.length === 0) return + + const homeworld = gameStore.player.planets.find(p => !p.isMoon) + if (!homeworld) return + + // 计算本次要处理的 NPC 范围 + const totalNPCs = npcStore.npcs.length + const effectiveSlice = Math.min(sliceSize, totalNPCs) + const startIndex = growthCursorIndex + const endIndex = Math.min(startIndex + effectiveSlice, totalNPCs) + + // 处理当前分片的 NPC + for (let i = startIndex; i < endIndex; i++) { + const npc = npcStore.npcs[i] + if (!npc) continue + npcGrowthLogic.updateNPCGrowthByDistance(npc, homeworld.position, elapsedSeconds, gameStore.gameSpeed) + syncNPCPlanetToUniverse(ctx, npc) + } + + // 更新游标(循环) + growthCursorIndex = endIndex >= totalNPCs ? 0 : endIndex + } + + /** + * 更新 NPC 行为(分片处理) + */ + const updateBehavior = (ctx: TickContext) => { + const { gameStore, universeStore, npcStore, t, notify } = ctx + + if (npcStore.npcs.length === 0) return + + const now = Date.now() + const allPlanets = [...gameStore.player.planets, ...Object.values(universeStore.planets)] + + // 计算当前活跃任务数量 + let activeSpyMissions = 0 + let activeAttackMissions = 0 + npcStore.npcs.forEach(npc => { + if (npc.fleetMissions) { + npc.fleetMissions.forEach(mission => { + if (mission.status === 'outbound') { + if (mission.missionType === 'spy') activeSpyMissions++ + else if (mission.missionType === 'attack') activeAttackMissions++ + } + }) + } + }) + + const config = npcBehaviorLogic.calculateDynamicBehavior(gameStore.player.points) + + // 计算本次要处理的 NPC 范围 + const totalNPCs = npcStore.npcs.length + const effectiveSlice = Math.min(sliceSize, totalNPCs) + const startIndex = behaviorCursorIndex + const endIndex = Math.min(startIndex + effectiveSlice, totalNPCs) + + // 处理当前分片的 NPC(随机顺序处理该分片) + const sliceNpcs = npcStore.npcs.slice(startIndex, endIndex) + const shuffledSlice = [...sliceNpcs].sort(() => Math.random() - 0.5) + + shuffledSlice.forEach(npc => { + npcBehaviorLogic.updateNPCBehaviorWithLimit(npc, gameStore.player, allPlanets, universeStore.debrisFields, now, { + activeSpyMissions, + activeAttackMissions, + config + }) + + // 重新计算并发数 + activeSpyMissions = 0 + activeAttackMissions = 0 + npcStore.npcs.forEach(n => { + if (n.fleetMissions) { + n.fleetMissions.forEach(mission => { + if (mission.status === 'outbound') { + if (mission.missionType === 'spy') activeSpyMissions++ + else if (mission.missionType === 'attack') activeAttackMissions++ + } + }) + } + }) + + // 处理增强 NPC 行为 + const relation = npc.relations?.[gameStore.player.id] + if (relation?.status === 'neutral') { + const neutralResult = npcBehaviorLogic.updateNeutralNPCBehavior(npc, npcStore.npcs, gameStore.player, now) + + if (neutralResult.tradeOffer) { + if (!gameStore.player.tradeOffers) { + gameStore.player.tradeOffers = [] + } + gameStore.player.tradeOffers.push(neutralResult.tradeOffer) + options.onTradeOffer?.(neutralResult.tradeOffer) + notify?.( + `${t('npcBehavior.tradeOfferReceived')}: ${t('npcBehavior.tradeOfferDesc', { npcName: neutralResult.tradeOffer.npcName })}`, + 'info' + ) + } + + if (neutralResult.swingDirection) { + if (!gameStore.player.attitudeChangeNotifications) { + gameStore.player.attitudeChangeNotifications = [] + } + const notification = { + id: `attitude_${Date.now()}_${npc.id}`, + timestamp: now, + npcId: npc.id, + npcName: npc.name, + previousStatus: 'neutral' as const, + newStatus: neutralResult.swingDirection, + reason: 'attitude_swing', + read: false + } + gameStore.player.attitudeChangeNotifications.push(notification) + options.onAttitudeChange?.(notification) + const statusKey = neutralResult.swingDirection === 'friendly' ? 'npcBehavior.becameFriendly' : 'npcBehavior.becameHostile' + notify?.(`${t('npcBehavior.attitudeChanged')}: ${t(statusKey, { npcName: npc.name })}`, 'info') + } + } else if (relation?.status === 'friendly') { + const friendlyResult = npcBehaviorLogic.updateFriendlyNPCBehavior(npc, npcStore.npcs, gameStore.player, now) + + if (friendlyResult.intelReport) { + if (!gameStore.player.intelReports) { + gameStore.player.intelReports = [] + } + gameStore.player.intelReports.push(friendlyResult.intelReport) + options.onIntelReport?.(friendlyResult.intelReport) + notify?.( + `${t('npcBehavior.intelReceived')}: ${t('npcBehavior.intelReceivedDesc', { npcName: friendlyResult.intelReport.fromNpcName })}`, + 'info' + ) + } + + if (friendlyResult.jointAttackInvite) { + if (!gameStore.player.jointAttackInvites) { + gameStore.player.jointAttackInvites = [] + } + gameStore.player.jointAttackInvites.push(friendlyResult.jointAttackInvite) + options.onJointAttackInvite?.(friendlyResult.jointAttackInvite) + notify?.( + `${t('npcBehavior.jointAttackInvite')}: ${t('npcBehavior.jointAttackInviteDesc', { + npcName: friendlyResult.jointAttackInvite.fromNpcName + })}`, + 'info' + ) + } + + if (friendlyResult.aidProvided) { + if (!gameStore.player.aidNotifications) { + gameStore.player.aidNotifications = [] + } + const notification = { + id: `aid_${Date.now()}_${npc.id}`, + timestamp: now, + npcId: npc.id, + npcName: npc.name, + aidResources: friendlyResult.aidProvided, + read: false + } + gameStore.player.aidNotifications.push(notification) + options.onAidReceived?.(notification) + const totalAid = friendlyResult.aidProvided.metal + friendlyResult.aidProvided.crystal + friendlyResult.aidProvided.deuterium + notify?.( + `${t('npcBehavior.aidReceived')}: ${t('npcBehavior.aidReceivedDesc', { + npcName: npc.name, + amount: totalAid.toLocaleString() + })}`, + 'success' + ) + } + } + }) + + // 更新游标(循环) + behaviorCursorIndex = endIndex >= totalNPCs ? 0 : endIndex + } + + /** + * 更新 NPC 关系统计 + */ + const updateRelationStats = (ctx: TickContext) => { + const { gameStore, npcStore } = ctx + let friendlyCount = 0 + let hostileCount = 0 + const playerId = gameStore.player.id + + npcStore.npcs.forEach(npc => { + const relation = npc.relations?.[playerId] + if (relation) { + const status = diplomaticLogic.calculateRelationStatus(relation.reputation) + if (status === 'friendly') friendlyCount++ + else if (status === 'hostile') hostileCount++ + } + }) + + gameLogic.trackDiplomacyStats(gameStore.player, 'updateRelations', { + friendlyCount, + hostileCount + }) + } + + /** + * 主 tick 函数 + */ + const tick = (ctx: TickContext) => { + const deltaSeconds = ctx.deltaMs / 1000 + + // 初始化 NPC(如果需要) + if (!initialized) { + initializeNPCs(ctx) + ensureNPCDataIntegrity(ctx) + } + + // 如果没有 NPC,直接返回 + if (ctx.npcStore.npcs.length === 0) return + + // 累积 Growth 时间 + growthAccumulator += deltaSeconds + if (growthAccumulator >= growthInterval) { + updateGrowth(ctx, growthAccumulator) + updateRelationStats(ctx) + growthAccumulator = 0 + } + + // 累积 Behavior 时间 + behaviorAccumulator += deltaSeconds + if (behaviorAccumulator >= behaviorInterval) { + updateBehavior(ctx) + behaviorAccumulator = 0 + } + } + + const setUpdateSlice = (size: number) => { + sliceSize = Math.max(1, size) + } + + const getUpdateSlice = () => sliceSize + + return { + tick, + setUpdateSlice, + getUpdateSlice + } +} diff --git a/src/services/profiler.ts b/src/services/profiler.ts new file mode 100644 index 0000000..5491b6e --- /dev/null +++ b/src/services/profiler.ts @@ -0,0 +1,158 @@ +/** + * 性能分析器 + * 用于追踪每个 tick 中各子系统的耗时 + */ + +/** + * 性能分析器接口 + */ +export interface Profiler { + /** 开始计时 */ + start(label: string): void + /** 结束计时 */ + end(label: string): void + /** 获取当前快照(各标签耗时,单位 ms) */ + snapshot(): Record + /** 重置所有计时数据 */ + reset(): void + /** 获取平均耗时(最近 N 次) */ + averages(): Record + /** 是否启用 */ + enabled: boolean +} + +/** + * 性能分析器选项 + */ +export interface ProfilerOptions { + /** 是否启用(默认仅在开发环境启用) */ + enabled?: boolean + /** 保留的历史记录数量(用于计算平均值) */ + historySize?: number + /** 是否在控制台输出警告(耗时超过阈值时) */ + warnThreshold?: number +} + +/** + * 创建性能分析器 + */ +export const createProfiler = (options: ProfilerOptions = {}): Profiler => { + const { + enabled = import.meta.env.DEV, + historySize = 60, + warnThreshold = 16 // 超过 16ms(60fps 帧时间)时警告 + } = options + + // 当前 tick 的计时数据 + const currentTimings: Map = new Map() + // 开始时间戳 + const startTimes: Map = new Map() + // 历史记录(用于计算平均值) + const history: Map = new Map() + + let isEnabled = enabled + + const start = (label: string): void => { + if (!isEnabled) return + startTimes.set(label, performance.now()) + } + + const end = (label: string): void => { + if (!isEnabled) return + + const startTime = startTimes.get(label) + if (startTime === undefined) { + console.warn(`[Profiler] end() called without matching start() for label: ${label}`) + return + } + + const duration = performance.now() - startTime + currentTimings.set(label, duration) + startTimes.delete(label) + + // 添加到历史记录 + let labelHistory = history.get(label) + if (!labelHistory) { + labelHistory = [] + history.set(label, labelHistory) + } + labelHistory.push(duration) + if (labelHistory.length > historySize) { + labelHistory.shift() + } + + // 超过阈值时警告 + if (warnThreshold > 0 && duration > warnThreshold) { + console.warn(`[Profiler] ${label} took ${duration.toFixed(2)}ms (threshold: ${warnThreshold}ms)`) + } + } + + const snapshot = (): Record => { + const result: Record = {} + for (const [label, duration] of currentTimings) { + result[label] = Math.round(duration * 100) / 100 // 保留2位小数 + } + return result + } + + const reset = (): void => { + currentTimings.clear() + startTimes.clear() + } + + const averages = (): Record => { + const result: Record = {} + for (const [label, durations] of history) { + if (durations.length > 0) { + const sum = durations.reduce((a, b) => a + b, 0) + result[label] = Math.round((sum / durations.length) * 100) / 100 + } + } + return result + } + + return { + start, + end, + snapshot, + reset, + averages, + get enabled() { + return isEnabled + }, + set enabled(value: boolean) { + isEnabled = value + if (!value) { + currentTimings.clear() + startTimes.clear() + history.clear() + } + } + } +} + +/** + * 全局性能分析器单例 + */ +export const profiler = createProfiler() + +/** + * 便捷装饰器:测量函数执行时间 + */ +export const measure = unknown>(label: string, fn: T): T => { + return ((...args: unknown[]) => { + profiler.start(label) + try { + const result = fn(...args) + // 处理 Promise + if (result instanceof Promise) { + return result.finally(() => profiler.end(label)) + } + profiler.end(label) + return result + } catch (error) { + profiler.end(label) + throw error + } + }) as T +} diff --git a/src/services/progressionEngine.ts b/src/services/progressionEngine.ts new file mode 100644 index 0000000..6bbda74 --- /dev/null +++ b/src/services/progressionEngine.ts @@ -0,0 +1,151 @@ +import type { TickContext } from './tickContext' +import { createScheduler, type Scheduler } from './scheduler' +import * as gameLogic from '@/logic/gameLogic' +import * as campaignLogic from '@/logic/campaignLogic' +import * as diplomaticLogic from '@/logic/diplomaticLogic' + +export interface ProgressionEngine { + /** 初始化引擎,注册所有定时任务 */ + init(ctx: TickContext): void + /** 每 tick 调用,更新调度器 */ + tick(ctx: TickContext): void +} + +export interface ProgressionEngineOptions { + /** 成就检查间隔(毫秒,默认 5000) */ + achievementInterval?: number + /** 战役进度检查间隔(毫秒,默认 5000) */ + campaignInterval?: number + /** 外交清理间隔(毫秒,默认 15000) */ + diplomacyCleanupInterval?: number + /** 成就解锁通知回调 */ + onAchievementUnlock?: (unlock: { id: string; tier: string }) => void +} + +/** + * 创建进度引擎 + * 负责低频任务调度:成就检查、战役进度、外交清理等 + */ +export const createProgressionEngine = (options: ProgressionEngineOptions = {}): ProgressionEngine => { + const { + achievementInterval = 5000, + campaignInterval = 5000, + diplomacyCleanupInterval = 15000 + } = options + + const scheduler: Scheduler = createScheduler() + let initialized = false + // 保存最新的 context 引用,供调度任务使用 + let currentCtx: TickContext | null = null + + /** + * 检查成就解锁 + */ + const checkAchievements = () => { + if (!currentCtx) return + const { gameStore } = currentCtx + + const unlocks = gameLogic.checkAndUnlockAchievements(gameStore.player) + + // 触发解锁通知 + unlocks.forEach(unlock => { + options.onAchievementUnlock?.(unlock) + }) + } + + /** + * 检查战役任务进度 + */ + const checkCampaignProgress = () => { + if (!currentCtx) return + const { gameStore, npcStore } = currentCtx + + if (gameStore.player.campaignProgress) { + campaignLogic.checkAllActiveQuestsProgress(gameStore.player, npcStore.npcs) + } + } + + /** + * 清理被消灭的 NPC(外交清理) + */ + const cleanupEliminatedNPCs = () => { + if (!currentCtx) return + const { gameStore, universeStore, npcStore } = currentCtx + + const eliminatedNpcIds = diplomaticLogic.checkAndHandleEliminatedNPCs( + npcStore.npcs, + gameStore.player, + gameStore.locale + ) + + if (eliminatedNpcIds.length === 0) return + + // 从 universeStore 中移除被消灭 NPC 的星球数据,并收集需要清理的任务 ID + const missionIdsToRemove: string[] = [] + + eliminatedNpcIds.forEach(npcId => { + const npc = npcStore.npcs.find(n => n.id === npcId) + if (npc) { + // 遍历 NPC 的所有星球,从 universeStore 中删除 + if (npc.planets) { + npc.planets.forEach(planet => { + const planetKey = gameLogic.generatePositionKey( + planet.position.galaxy, + planet.position.system, + planet.position.position + ) + if (universeStore.planets[planetKey]) { + delete universeStore.planets[planetKey] + } + }) + } + // 收集该 NPC 所有任务的 ID(用于清理玩家的警报) + if (npc.fleetMissions) { + npc.fleetMissions.forEach(m => missionIdsToRemove.push(m.id)) + } + } + }) + + // 清理玩家的即将到来舰队警报(移除已消灭 NPC 的任务警报) + if (gameStore.player.incomingFleetAlerts && missionIdsToRemove.length > 0) { + gameStore.player.incomingFleetAlerts = gameStore.player.incomingFleetAlerts.filter( + alert => !missionIdsToRemove.includes(alert.id) + ) + } + + // 从 NPC 列表中移除被消灭的 NPC + npcStore.npcs = npcStore.npcs.filter(npc => !eliminatedNpcIds.includes(npc.id)) + } + + /** + * 初始化引擎,注册所有定时任务 + */ + const init = (ctx: TickContext): void => { + if (initialized) return + + currentCtx = ctx + + // 注册定时任务 + scheduler.every(achievementInterval, checkAchievements) + scheduler.every(campaignInterval, checkCampaignProgress) + scheduler.every(diplomacyCleanupInterval, cleanupEliminatedNPCs) + + initialized = true + } + + /** + * 每 tick 调用,更新调度器 + */ + const tick = (ctx: TickContext): void => { + // 更新 context 引用 + currentCtx = ctx + + // 执行调度器 tick + scheduler.tick(ctx.now) + } + + return { + init, + tick + } +} diff --git a/src/services/saveSystem.ts b/src/services/saveSystem.ts new file mode 100644 index 0000000..a3b6109 --- /dev/null +++ b/src/services/saveSystem.ts @@ -0,0 +1,245 @@ +import { encryptData, decryptData } from '@/utils/crypto' +import { useGameStore } from '@/stores/gameStore' +import { useUniverseStore } from '@/stores/universeStore' +import { useNPCStore } from '@/stores/npcStore' +import pkg from '../../package.json' + +/** + * 存档信封类型 + * 包含版本号、创建时间和加密数据 + */ +export type SaveEnvelope = { + version: number + createdAt: number + data: any +} + +/** + * 存档数据载荷类型 + */ +type SavePayload = { + game: any + universe?: any | null + npcs?: any | null +} + +/** + * 存档系统接口 + */ +export interface SaveSystem { + /** 从 localStorage 加载存档 */ + load(): Promise + /** 保存到 localStorage */ + save(): Promise + /** 导出存档为字符串 */ + exportSave(): string + /** 从字符串导入存档 */ + importSave(payload: string): Promise +} + +/** 当前存档版本号(从 package.json 版本号派生,如 1.6.0 → 160) */ +const CURRENT_SAVE_VERSION = parseInt(pkg.version.replace(/\./g, ''), 10) || 1 + +/** 默认存档键名 */ +const DEFAULT_SAVE_KEY = `${pkg.name}-save` + +/** + * 迁移函数映射 + * 键为源版本号,值为迁移函数 + * 示例:{ 1: (d) => migrateV1ToV2(d) } + */ +const migrations: Record SavePayload> = { + // 未来迁移示例: + // 1: (payload) => { + // // 从 v1 迁移到 v2 + // return { ...payload, game: { ...payload.game, newField: 'default' } } + // } +} + +/** + * 检查是否为有效的 SaveEnvelope + */ +const isSaveEnvelope = (value: unknown): value is SaveEnvelope => { + if (typeof value !== 'object' || value === null) return false + const obj = value as Record + return typeof obj.version === 'number' && typeof obj.createdAt === 'number' && 'data' in obj +} + +/** + * 检查是否为 GameStore 状态结构 + */ +const isLikelyGameStoreState = (value: unknown): value is Record => { + if (typeof value !== 'object' || value === null) return false + const obj = value as Record + return 'player' in obj && 'locale' in obj +} + +/** + * 构建存档信封 + */ +const buildEnvelope = (payload: SavePayload): SaveEnvelope => ({ + version: CURRENT_SAVE_VERSION, + createdAt: Date.now(), + data: encryptData(payload) +}) + +/** + * 应用迁移 + * 从当前版本逐步迁移到最新版本 + */ +const applyMigrations = (version: number, payload: SavePayload): SavePayload => { + if (version > CURRENT_SAVE_VERSION) { + throw new Error(`Unsupported save version: ${version}`) + } + + let migrated = payload + for (let v = version; v < CURRENT_SAVE_VERSION; v++) { + const migrationFn = migrations[v] + if (migrationFn) { + migrated = migrationFn(migrated) + } + } + + return migrated +} + +/** + * 规范化导入的存档数据 + * 支持新格式(SaveEnvelope)和旧格式(直接加密数据) + */ +const normalizeImportedPayload = (raw: string): { version: number; payload: SavePayload } => { + const trimmed = raw.trim() + if (!trimmed) throw new Error('Empty payload') + + // 尝试解析为 JSON + let parsed: unknown = null + try { + parsed = JSON.parse(trimmed) + } catch { + parsed = null + } + + // 1) 新格式:JSON SaveEnvelope + if (isSaveEnvelope(parsed)) { + const decrypted = typeof parsed.data === 'string' ? decryptData(parsed.data) : typeof parsed.data === 'object' ? parsed.data : null + + if (!decrypted || typeof decrypted !== 'object' || !('game' in decrypted)) { + throw new Error('Invalid save payload') + } + return { version: parsed.version, payload: decrypted as SavePayload } + } + + // 2) 旧格式:{ game: , universe: , npcs: } + if (parsed && typeof parsed === 'object' && 'game' in parsed) { + const legacyParsed = parsed as Record + const gameState = typeof legacyParsed.game === 'string' ? decryptData(legacyParsed.game) : null + if (!gameState) throw new Error('Invalid legacy save payload') + + const universeState = typeof legacyParsed.universe === 'string' ? decryptData(legacyParsed.universe) : null + const npcsState = + typeof (legacyParsed.npcs ?? legacyParsed.npc) === 'string' ? decryptData((legacyParsed.npcs ?? legacyParsed.npc) as string) : null + + return { + version: 0, + payload: { + game: gameState, + universe: universeState, + npcs: npcsState + } + } + } + + // 3) 最旧格式:直接是加密字符串 + if (typeof parsed === 'string') { + const gameState = decryptData(parsed) + if (!gameState) throw new Error('Invalid save payload') + return { version: 0, payload: { game: gameState } } + } + + // 4) 原始加密字符串(非 JSON) + const decrypted = decryptData(trimmed) + if (decrypted && typeof decrypted === 'object') { + if ('game' in decrypted) { + return { version: 0, payload: decrypted as SavePayload } + } + if (isLikelyGameStoreState(decrypted)) { + return { version: 0, payload: { game: decrypted } } + } + } + + // 5) 纯 JSON GameStore 状态 + if (parsed && isLikelyGameStoreState(parsed)) { + return { version: 0, payload: { game: parsed } } + } + + throw new Error('Unsupported save format') +} + +/** + * 创建存档系统 + */ +export const createSaveSystem = (): SaveSystem => { + let gameStore: ReturnType | null = null + let universeStore: ReturnType | null = null + let npcStore: ReturnType | null = null + + /** 延迟获取 Store 引用 */ + const getStores = () => { + if (!gameStore) gameStore = useGameStore() + if (!universeStore) universeStore = useUniverseStore() + if (!npcStore) npcStore = useNPCStore() + return { gameStore, universeStore, npcStore } + } + + /** 导出存档为字符串 */ + const exportSave = (): string => { + const { gameStore, universeStore, npcStore } = getStores() + const payload: SavePayload = { + game: gameStore.$state, + universe: universeStore.$state, + npcs: npcStore.$state + } + return JSON.stringify(buildEnvelope(payload)) + } + + /** 从字符串导入存档 */ + const importSave = async (raw: string): Promise => { + const { version, payload } = normalizeImportedPayload(raw) + const migrated = applyMigrations(version, payload) + + const { gameStore, universeStore, npcStore } = getStores() + + // 重置所有 Store + gameStore.$reset() + universeStore.$reset() + npcStore.$reset() + + if (!migrated.game) throw new Error('Save payload missing game state') + + // 应用迁移后的数据 + gameStore.$patch(migrated.game) + if (migrated.universe) universeStore.$patch(migrated.universe) + if (migrated.npcs) npcStore.$patch(migrated.npcs) + } + + /** 保存到 localStorage */ + const save = async (): Promise => { + if (typeof localStorage === 'undefined') return + localStorage.setItem(DEFAULT_SAVE_KEY, exportSave()) + } + + /** 从 localStorage 加载存档 */ + const load = async (): Promise => { + if (typeof localStorage === 'undefined') return + const saved = localStorage.getItem(DEFAULT_SAVE_KEY) + if (!saved) return + await importSave(saved) + } + + return { + load, + save, + exportSave, + importSave + } +} diff --git a/src/services/scheduler.ts b/src/services/scheduler.ts new file mode 100644 index 0000000..90cef70 --- /dev/null +++ b/src/services/scheduler.ts @@ -0,0 +1,65 @@ +/** + * 任务函数类型 + */ +export type Job = () => void + +/** + * 调度器内部任务记录 + */ +interface ScheduledJob { + /** 任务执行间隔(毫秒) */ + intervalMs: number + /** 任务函数 */ + job: Job + /** 上次执行时间 */ + lastRun: number +} + +/** + * 调度器接口 + * 用于管理低频任务(如成就检查、外交清理、自动保存等) + */ +export interface Scheduler { + /** 注册一个定时任务 */ + every(ms: number, job: Job): void + /** 每 tick 调用,检查并执行到期的任务 */ + tick(now: number): void + /** 清除所有任务 */ + clear(): void +} + +/** + * 创建调度器 + * 用于管理不需要每秒运行的低频任务 + */ +export const createScheduler = (): Scheduler => { + const jobs: ScheduledJob[] = [] + + const every = (ms: number, job: Job): void => { + jobs.push({ + intervalMs: ms, + job, + lastRun: 0 // 初始为0,确保首次 tick 时立即执行 + }) + } + + const tick = (now: number): void => { + jobs.forEach(scheduledJob => { + const elapsed = now - scheduledJob.lastRun + if (elapsed >= scheduledJob.intervalMs) { + scheduledJob.job() + scheduledJob.lastRun = now + } + }) + } + + const clear = (): void => { + jobs.length = 0 + } + + return { + every, + tick, + clear + } +} diff --git a/src/services/tickContext.ts b/src/services/tickContext.ts new file mode 100644 index 0000000..cbd37bb --- /dev/null +++ b/src/services/tickContext.ts @@ -0,0 +1,107 @@ +import type { useGameStore } from '@/stores/gameStore' +import type { useUniverseStore } from '@/stores/universeStore' +import type { useNPCStore } from '@/stores/npcStore' +import type { Locale } from '@/locales' +import type { DirtyFlags } from '@/logic/dirtyFlags' +import type { Profiler } from './profiler' +import { profiler as globalProfiler } from './profiler' + +/** + * 翻译函数类型 + */ +export type TranslateFn = (key: string, params?: Record) => string + +/** + * 通知回调类型 + */ +export type NotifyFn = (message: string, type?: 'info' | 'success' | 'warning' | 'error') => void + +/** + * 解锁项目类型 + */ +export interface UnlockedItem { + type: 'building' | 'technology' + id: string + name: string +} + +/** + * 解锁通知回调类型 + */ +export type NotifyUnlockFn = (unlockedItems: UnlockedItem[]) => void + +/** + * Tick 上下文接口 + * 集中管理所有 tick 逻辑需要的依赖,避免分散的 imports + */ +export interface TickContext { + // 时间信息 + now: number + deltaMs: number + gameSpeed: number + + // Stores(具体类型化) + gameStore: ReturnType + universeStore: ReturnType + npcStore: ReturnType + + // 国际化 + locale: Locale + t: TranslateFn + + // 通知服务 + notify: NotifyFn + notifyUnlock: NotifyUnlockFn + + // 性能优化:脏标志 + dirtyFlags: DirtyFlags + + // 性能分析器(仅开发环境有效) + profiler: Profiler +} + +/** + * 创建 TickContext 的选项 + */ +export interface CreateTickContextOptions { + gameStore: ReturnType + universeStore: ReturnType + npcStore: ReturnType + t: TranslateFn + notify?: NotifyFn + notifyUnlock?: NotifyUnlockFn + dirtyFlags: DirtyFlags +} + +/** + * 创建 Tick 上下文 + */ +export const createTickContext = (options: CreateTickContextOptions, now: number, deltaMs: number): TickContext => { + const { gameStore, universeStore, npcStore, t, dirtyFlags } = options + + return { + // 时间信息 + now, + deltaMs, + gameSpeed: gameStore.gameSpeed, + + // Stores + gameStore, + universeStore, + npcStore, + + // 国际化 + locale: gameStore.locale, + t, + + // 通知服务(提供默认空实现) + notify: options.notify ?? (() => {}), + notifyUnlock: options.notifyUnlock ?? (() => {}), + + // 性能优化:脏标志 + dirtyFlags, + + // 性能分析器 + profiler: globalProfiler + } +} diff --git a/src/services/timeSource.ts b/src/services/timeSource.ts new file mode 100644 index 0000000..58b198e --- /dev/null +++ b/src/services/timeSource.ts @@ -0,0 +1,186 @@ +/** + * 时间源服务 + * 提供稳定的时间源,处理离线/标签页不活跃场景 + * 防止时间跳跃导致的资源溢出或其他异常 + */ + +/** + * 时间源接口 + */ +export interface TimeSource { + /** 获取当前时间戳(毫秒) */ + now(): number + /** 获取上次记录的时间 */ + getLastTime(): number + /** 更新上次时间记录 */ + setLastTime(time: number): void + /** 计算并返回安全的 deltaMs(已限制最大值) */ + getDeltaMs(currentTime: number): number + /** 检查是否需要追赶(离线时间过长) */ + needsCatchUp(currentTime: number): boolean + /** 获取待追赶的时间(毫秒) */ + getPendingCatchUp(): number + /** 消耗一部分追赶时间 */ + consumeCatchUp(amount: number): void + /** 重置追赶时间 */ + resetCatchUp(): void +} + +/** + * 时间源配置 + */ +export interface TimeSourceConfig { + /** 单次 tick 最大 deltaMs(默认 60 秒) */ + maxDeltaMs: number + /** 触发追赶模式的阈值(默认 5 分钟) */ + catchUpThreshold: number + /** 每次追赶的时间量(默认 60 秒) */ + catchUpChunkSize: number + /** 是否启用追赶模式(默认 true) */ + enableCatchUp: boolean + /** 最大追赶时间(默认 24 小时,防止离线太久导致资源爆炸) */ + maxCatchUpTime: number +} + +/** + * 默认配置 + */ +const DEFAULT_CONFIG: TimeSourceConfig = { + maxDeltaMs: 60 * 1000, // 60 秒 + catchUpThreshold: 5 * 60 * 1000, // 5 分钟 + catchUpChunkSize: 60 * 1000, // 60 秒 + enableCatchUp: true, + maxCatchUpTime: 24 * 60 * 60 * 1000 // 24 小时 +} + +/** + * 创建时间源 + * @param config 可选配置 + */ +export const createTimeSource = (config?: Partial): TimeSource => { + const cfg: TimeSourceConfig = { ...DEFAULT_CONFIG, ...config } + + let lastTime = Date.now() + let pendingCatchUp = 0 + + /** + * 获取当前时间戳 + */ + const now = (): number => { + return Date.now() + } + + /** + * 获取上次记录的时间 + */ + const getLastTime = (): number => { + return lastTime + } + + /** + * 设置上次时间记录 + */ + const setLastTime = (time: number): void => { + lastTime = time + } + + /** + * 计算安全的 deltaMs + * - 限制最大值防止时间跳跃 + * - 将超出部分存入追赶队列 + */ + const getDeltaMs = (currentTime: number): number => { + const rawDelta = currentTime - lastTime + + // 负值处理(系统时间被调整) + if (rawDelta < 0) { + console.warn('[TimeSource] 检测到负 delta,可能是系统时间被调整') + lastTime = currentTime + return 0 + } + + // 如果 delta 超过阈值,启用追赶模式 + if (cfg.enableCatchUp && rawDelta > cfg.catchUpThreshold) { + // 计算需要追赶的时间(限制最大追赶时间) + const excessTime = rawDelta - cfg.maxDeltaMs + pendingCatchUp = Math.min(pendingCatchUp + excessTime, cfg.maxCatchUpTime) + + // 返回限制后的 delta + lastTime = currentTime + return cfg.maxDeltaMs + } + + // 正常情况:限制最大 delta + const clampedDelta = Math.min(rawDelta, cfg.maxDeltaMs) + lastTime = currentTime + + return clampedDelta + } + + /** + * 检查是否需要追赶 + */ + const needsCatchUp = (currentTime: number): boolean => { + if (!cfg.enableCatchUp) return false + return pendingCatchUp > 0 || currentTime - lastTime > cfg.catchUpThreshold + } + + /** + * 获取待追赶时间 + */ + const getPendingCatchUp = (): number => { + return pendingCatchUp + } + + /** + * 消耗追赶时间 + */ + const consumeCatchUp = (amount: number): void => { + pendingCatchUp = Math.max(0, pendingCatchUp - amount) + } + + /** + * 重置追赶时间 + */ + const resetCatchUp = (): void => { + pendingCatchUp = 0 + } + + return { + now, + getLastTime, + setLastTime, + getDeltaMs, + needsCatchUp, + getPendingCatchUp, + consumeCatchUp, + resetCatchUp + } +} + +/** + * 格式化时间差(用于调试/显示) + */ +export const formatTimeDelta = (ms: number): string => { + if (ms < 1000) return `${ms}ms` + if (ms < 60000) return `${(ms / 1000).toFixed(1)}s` + if (ms < 3600000) return `${(ms / 60000).toFixed(1)}m` + return `${(ms / 3600000).toFixed(1)}h` +} + +/** + * 计算离线期间应获得的资源比例 + * 可以根据离线时间长度给予不同的效率 + * @param offlineMs 离线时间(毫秒) + * @returns 效率系数(0-1) + */ +export const calcOfflineEfficiency = (offlineMs: number): number => { + // 前 1 小时:100% 效率 + if (offlineMs <= 3600000) return 1.0 + // 1-6 小时:80% 效率 + if (offlineMs <= 6 * 3600000) return 0.8 + // 6-24 小时:50% 效率 + if (offlineMs <= 24 * 3600000) return 0.5 + // 超过 24 小时:30% 效率 + return 0.3 +} diff --git a/src/views/SettingsView.vue b/src/views/SettingsView.vue index 3e6e798..2132d74 100644 --- a/src/views/SettingsView.vue +++ b/src/views/SettingsView.vue @@ -399,8 +399,10 @@ import WebDAVFileListDialog from '@/components/settings/WebDAVFileListDialog.vue' import { useHints } from '@/composables/useHints' import { uploadToWebDAV, downloadFromWebDAV } from '@/services/webdavService' + import { createSaveSystem } from '@/services/saveSystem' const { t } = useI18n() + const saveSystem = createSaveSystem() const { hintsEnabled, setHintsEnabled, resetHints } = useHints() const gameStore = useGameStore() @@ -568,27 +570,10 @@ try { isExporting.value = true - // 获取游戏数据 - const gameData = localStorage.getItem(pkg.name) - // 获取地图数据 - const universeData = localStorage.getItem(`${pkg.name}-universe`) - // 获取npc数据 - const npcData = localStorage.getItem(`${pkg.name}-npcs`) - - if (!gameData) { - toast.error(t('settings.exportFailed')) - return - } - - // 合并数据 - const exportData = { - game: gameData, - npcs: npcData, - universe: universeData || null - } + // 使用 SaveSystem 导出(带版本号的 SaveEnvelope 格式) + const jsonString = saveSystem.exportSave() const fileName = `${pkg.name}-${new Date().toISOString().slice(0, 10)}-${Date.now()}.json` - const jsonString = JSON.stringify(exportData, null, 2) // Android 保存到公共 Downloads 目录 if (Capacitor.isNativePlatform()) { @@ -643,33 +628,12 @@ const importData = async (file: File) => { try { const reader = new FileReader() - reader.onload = e => { + reader.onload = async e => { try { const result = e.target?.result if (typeof result === 'string') { - const importData = JSON.parse(result) - - // 兼容旧版本:如果是旧格式(直接是字符串),只导入游戏数据 - if (typeof importData === 'string' || !importData.game) { - localStorage.setItem(pkg.name, result) - toast.success(t('settings.importSuccess')) - setTimeout(() => window.location.reload(), 1000) - return - } - - // 新格式:分别导入游戏数据和地图数据 - if (importData.game) { - localStorage.setItem(pkg.name, importData.game) - } - - if (importData.universe) { - localStorage.setItem(`${pkg.name}-universe`, importData.universe) - } - - if (importData.npcs) { - localStorage.setItem(`${pkg.name}-npcs`, importData.npcs) - } - + // 使用 SaveSystem 导入(支持新旧格式自动转换和迁移) + await saveSystem.importSave(result) toast.success(t('settings.importSuccess')) // 延迟刷新页面以让toast显示 setTimeout(() => window.location.reload(), 1000) @@ -778,24 +742,8 @@ isWebDAVUploading.value = true try { - // 获取游戏数据 - const gameData = localStorage.getItem(pkg.name) - const universeData = localStorage.getItem(`${pkg.name}-universe`) - const npcData = localStorage.getItem(`${pkg.name}-npcs`) - - if (!gameData) { - toast.error(t('settings.exportFailed')) - return - } - - // 合并数据 - const exportData = { - game: gameData, - npcs: npcData, - universe: universeData || null - } - - const jsonString = JSON.stringify(exportData, null, 2) + // 使用 SaveSystem 导出(带版本号的 SaveEnvelope 格式) + const jsonString = saveSystem.exportSave() const result = await uploadToWebDAV(webdavConfig.value, jsonString) if (result.success) { @@ -829,25 +777,10 @@ showConfirmDialog.value = true gameStore.isPaused = true - confirmCallback = () => { + confirmCallback = async () => { try { - const importData = JSON.parse(result.data!) - - // 兼容旧版本格式 - if (typeof importData === 'string' || !importData.game) { - localStorage.setItem(pkg.name, result.data!) - } else { - if (importData.game) { - localStorage.setItem(pkg.name, importData.game) - } - if (importData.universe) { - localStorage.setItem(`${pkg.name}-universe`, importData.universe) - } - if (importData.npcs) { - localStorage.setItem(`${pkg.name}-npcs`, importData.npcs) - } - } - + // 使用 SaveSystem 导入(支持新旧格式自动转换和迁移) + await saveSystem.importSave(result.data!) toast.success(t('settings.importSuccess')) setTimeout(() => window.location.reload(), 1000) } catch (error) { diff --git a/src/workers/battle.worker.ts b/src/workers/battle.worker.ts index 278a69d..9f08d64 100644 --- a/src/workers/battle.worker.ts +++ b/src/workers/battle.worker.ts @@ -5,8 +5,9 @@ import type { Fleet, Resources } from '@/types/game' import { ShipType, DefenseType } from '@/types/game' import { SHIPS, DEFENSES } from '@/config/gameConfig' -import type { WorkerRequestMessage, WorkerResponseMessage, BattleSideData, BattleSimulationResult } from '@/types/worker' -import { WorkerMessageType } from '@/types/worker' +import type { BattleSideData, BattleSimulationResult } from '@/types/worker' +import type { WorkerRequest, WorkerResponse } from './types' +import { WorkerMessageType } from './types' // 战斗单位接口 interface CombatUnit { @@ -420,7 +421,7 @@ const calculateDebrisField = ( // Worker 消息处理 // ============================================================================ -self.onmessage = (event: MessageEvent) => { +self.onmessage = (event: MessageEvent) => { const { id, type, payload } = event.data try { @@ -467,19 +468,17 @@ self.onmessage = (event: MessageEvent) => { } // 发送成功响应 - const response: WorkerResponseMessage = { + const response: WorkerResponse = { id, - type: WorkerMessageType.SUCCESS, - success: true, - data: result + ok: true, + payload: result } self.postMessage(response) } catch (error) { // 发送错误响应 - const response: WorkerResponseMessage = { + const response: WorkerResponse = { id, - type: WorkerMessageType.ERROR, - success: false, + ok: false, error: error instanceof Error ? error.message : String(error) } self.postMessage(response) diff --git a/src/workers/types.ts b/src/workers/types.ts new file mode 100644 index 0000000..9e583ba --- /dev/null +++ b/src/workers/types.ts @@ -0,0 +1,44 @@ +/** + * Worker 通用类型定义 + * 统一的请求/响应格式 + */ + +/** + * Worker 请求消息 + * @template T - payload 类型 + */ +export interface WorkerRequest { + /** 唯一消息 ID */ + id: string + /** 消息类型 */ + type: string + /** 请求载荷 */ + payload: T +} + +/** + * Worker 响应消息 + * @template T - payload 类型 + */ +export interface WorkerResponse { + /** 对应请求的消息 ID */ + id: string + /** 是否成功 */ + ok: boolean + /** 响应载荷(成功时) */ + payload?: T + /** 错误信息(失败时) */ + error?: string +} + +/** + * Worker 消息类型枚举 + */ +export const WorkerMessageType = { + // 战斗模拟相关 + SIMULATE_BATTLE: 'SIMULATE_BATTLE', + CALCULATE_PLUNDER: 'CALCULATE_PLUNDER', + CALCULATE_DEBRIS: 'CALCULATE_DEBRIS' +} as const + +export type WorkerMessageType = (typeof WorkerMessageType)[keyof typeof WorkerMessageType] diff --git a/src/workers/workerManager.ts b/src/workers/workerManager.ts index 01b1964..776734d 100644 --- a/src/workers/workerManager.ts +++ b/src/workers/workerManager.ts @@ -2,8 +2,8 @@ * Worker 管理器 * 统一管理所有 Worker 的创建、通信和销毁 */ -import type { WorkerRequestMessage, WorkerResponseMessage, WorkerMessageType } from '@/types/worker' -import { WorkerMessageType as MsgType } from '@/types/worker' +import type { WorkerRequest, WorkerResponse, WorkerMessageType } from './types' +import { WorkerMessageType as MsgType } from './types' import { toRaw } from 'vue' import BattleWorker from './battle.worker?worker' @@ -53,7 +53,7 @@ class WorkerManager { private battleWorker: Worker | null = null private pendingTasks: Map = new Map() private messageIdCounter = 0 - private readonly defaultTimeout = 10000 // 30秒超时 + private readonly defaultTimeout = 10000 // 10秒超时 /** * 初始化战斗 Worker @@ -69,8 +69,8 @@ class WorkerManager { * 设置 Worker 消息处理器 */ private setupWorkerHandlers(worker: Worker, workerName: string): void { - worker.onmessage = (event: MessageEvent) => { - const { id, success, data, error } = event.data + worker.onmessage = (event: MessageEvent) => { + const { id, ok, payload, error } = event.data const task = this.pendingTasks.get(id) if (!task) { @@ -87,8 +87,8 @@ class WorkerManager { this.pendingTasks.delete(id) // 处理响应 - if (success) { - task.resolve(data) + if (ok) { + task.resolve(payload) } else { task.reject(new Error(error || 'Worker task failed')) } @@ -157,7 +157,7 @@ class WorkerManager { }) // 发送消息(使用 toPlainObject 转换 Vue Proxy 对象,然后使用浏览器内置的 structured clone) - const message: WorkerRequestMessage = { id, type, payload: toPlainObject(payload) } + const message: WorkerRequest = { id, type, payload: toPlainObject(payload) } worker.postMessage(message) }) }