mirror of
https://github.com/setube/ogame-vue-ts.git
synced 2026-05-12 07:55:11 +08:00
撤回
This commit is contained in:
1438
src/App.vue
1438
src/App.vue
@@ -414,8 +414,6 @@
|
|||||||
<HintToast />
|
<HintToast />
|
||||||
<!-- Toast 通知 -->
|
<!-- Toast 通知 -->
|
||||||
<Sonner position="top-center" />
|
<Sonner position="top-center" />
|
||||||
<!-- 调试面板(仅开发环境) -->
|
|
||||||
<DebugOverlay />
|
|
||||||
<!-- 重命名星球对话框 -->
|
<!-- 重命名星球对话框 -->
|
||||||
<Dialog v-model:open="renameDialogOpen">
|
<Dialog v-model:open="renameDialogOpen">
|
||||||
<DialogContent class="sm:max-w-md">
|
<DialogContent class="sm:max-w-md">
|
||||||
@@ -478,12 +476,6 @@
|
|||||||
import { useTheme } from '@/composables/useTheme'
|
import { useTheme } from '@/composables/useTheme'
|
||||||
import { useI18n } from '@/composables/useI18n'
|
import { useI18n } from '@/composables/useI18n'
|
||||||
import { useGameConfig } from '@/composables/useGameConfig'
|
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 { localeNames, detectBrowserLocale, type Locale } from '@/locales'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
@@ -526,8 +518,9 @@
|
|||||||
import HintToast from '@/components/notifications/HintToast.vue'
|
import HintToast from '@/components/notifications/HintToast.vue'
|
||||||
import BackToTop from '@/components/common/BackToTop.vue'
|
import BackToTop from '@/components/common/BackToTop.vue'
|
||||||
import Sonner from '@/components/ui/sonner/Sonner.vue'
|
import Sonner from '@/components/ui/sonner/Sonner.vue'
|
||||||
import DebugOverlay from '@/components/debug/DebugOverlay.vue'
|
import { MissionType, BuildingType, TechnologyType, DiplomaticEventType, ShipType } from '@/types/game'
|
||||||
import { BuildingType, TechnologyType } from '@/types/game'
|
import type { FleetMission, NPC, MissileAttack } from '@/types/game'
|
||||||
|
import { DIPLOMATIC_CONFIG } from '@/config/gameConfig'
|
||||||
import type { VersionInfo } from '@/utils/versionCheck'
|
import type { VersionInfo } from '@/utils/versionCheck'
|
||||||
import { formatNumber, getResourceColor } from '@/utils/format'
|
import { formatNumber, getResourceColor } from '@/utils/format'
|
||||||
import { scaleNumber, scaleResources } from '@/utils/speed'
|
import { scaleNumber, scaleResources } from '@/utils/speed'
|
||||||
@@ -557,15 +550,21 @@
|
|||||||
Crown,
|
Crown,
|
||||||
Scroll
|
Scroll
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
|
import * as gameLogic from '@/logic/gameLogic'
|
||||||
import * as planetLogic from '@/logic/planetLogic'
|
import * as planetLogic from '@/logic/planetLogic'
|
||||||
import * as officerLogic from '@/logic/officerLogic'
|
import * as officerLogic from '@/logic/officerLogic'
|
||||||
import * as buildingValidation from '@/logic/buildingValidation'
|
import * as buildingValidation from '@/logic/buildingValidation'
|
||||||
import * as resourceLogic from '@/logic/resourceLogic'
|
import * as resourceLogic from '@/logic/resourceLogic'
|
||||||
import * as researchValidation from '@/logic/researchValidation'
|
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 publicLogic from '@/logic/publicLogic'
|
||||||
import * as oreDepositLogic from '@/logic/oreDepositLogic'
|
import * as oreDepositLogic from '@/logic/oreDepositLogic'
|
||||||
import { generateRandomPosition, generatePositionKey, shouldInitializeGame, initializePlayer } from '@/logic/gameLogic'
|
import * as campaignLogic from '@/logic/campaignLogic'
|
||||||
import { countOldFormatNPCs, updateNPCName } from '@/logic/npcNameGenerator'
|
import { generateNPCName, countOldFormatNPCs, updateNPCName } from '@/logic/npcNameGenerator'
|
||||||
import pkg from '../package.json'
|
import pkg from '../package.json'
|
||||||
import { toast } from 'vue-sonner'
|
import { toast } from 'vue-sonner'
|
||||||
import { migrateGameData } from '@/utils/migration'
|
import { migrateGameData } from '@/utils/migration'
|
||||||
@@ -602,14 +601,14 @@
|
|||||||
const sidebarOpen = ref(window.innerWidth >= 1024)
|
const sidebarOpen = ref(window.innerWidth >= 1024)
|
||||||
// 移动端资源栏展开状态
|
// 移动端资源栏展开状态
|
||||||
const resourceBarExpanded = ref(false)
|
const resourceBarExpanded = ref(false)
|
||||||
// 游戏引擎
|
const npcUpdateCounter = ref(0) // 累计秒数
|
||||||
let gameEngine: GameEngine | null = null
|
const NPC_UPDATE_INTERVAL = 5 // 每1秒更新一次NPC,确保发育速度与玩家相当
|
||||||
let missionEngine: MissionEngine | null = null
|
// NPC行为系统更新函数(侦查和攻击决策)
|
||||||
let npcEngine: NpcEngine | null = null
|
const npcBehaviorCounter = ref(0)
|
||||||
let economyEngine: EconomyEngine | null = null
|
const NPC_BEHAVIOR_INTERVAL = 5 // 每5秒检查一次NPC行为
|
||||||
let progressionEngine: ProgressionEngine | null = null
|
|
||||||
|
|
||||||
// 游戏循环定时器
|
// 游戏循环定时器
|
||||||
|
const gameLoop = ref<ReturnType<typeof setInterval> | null>(null)
|
||||||
const pointsUpdateInterval = ref<ReturnType<typeof setInterval> | null>(null)
|
const pointsUpdateInterval = ref<ReturnType<typeof setInterval> | null>(null)
|
||||||
const konamiCleanup = ref<(() => void) | null>(null)
|
const konamiCleanup = ref<(() => void) | null>(null)
|
||||||
const versionCheckInterval = ref<ReturnType<typeof setInterval> | null>(null) // 重命名星球相关状态
|
const versionCheckInterval = ref<ReturnType<typeof setInterval> | null>(null) // 重命名星球相关状态
|
||||||
@@ -819,7 +818,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const initGame = async () => {
|
const initGame = async () => {
|
||||||
const shouldInit = shouldInitializeGame(gameStore.player.planets)
|
const shouldInit = gameLogic.shouldInitializeGame(gameStore.player.planets)
|
||||||
if (!shouldInit) {
|
if (!shouldInit) {
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
// 迁移矿脉储量数据(为没有矿脉数据的星球初始化)
|
// 迁移矿脉储量数据(为没有矿脉数据的星球初始化)
|
||||||
@@ -859,7 +858,7 @@
|
|||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
gameStore.player = initializePlayer(gameStore.player.id, t('common.playerName'))
|
gameStore.player = gameLogic.initializePlayer(gameStore.player.id, t('common.playerName'))
|
||||||
const initialPlanet = planetLogic.createInitialPlanet(gameStore.player.id, t('planet.homePlanet'))
|
const initialPlanet = planetLogic.createInitialPlanet(gameStore.player.id, t('planet.homePlanet'))
|
||||||
gameStore.player.planets = [initialPlanet]
|
gameStore.player.planets = [initialPlanet]
|
||||||
gameStore.currentPlanetId = initialPlanet.id
|
gameStore.currentPlanetId = initialPlanet.id
|
||||||
@@ -872,14 +871,926 @@
|
|||||||
const generateNPCPlanets = () => {
|
const generateNPCPlanets = () => {
|
||||||
const npcCount = 200
|
const npcCount = 200
|
||||||
for (let i = 0; i < npcCount; i++) {
|
for (let i = 0; i < npcCount; i++) {
|
||||||
const position = generateRandomPosition()
|
const position = gameLogic.generateRandomPosition()
|
||||||
const key = generatePositionKey(position.galaxy, position.system, position.position)
|
const key = gameLogic.generatePositionKey(position.galaxy, position.system, position.position)
|
||||||
if (universeStore.planets[key]) continue
|
if (universeStore.planets[key]) continue
|
||||||
const npcPlanet = planetLogic.createNPCPlanet(i, position, t('planet.planetPrefix'))
|
const npcPlanet = planetLogic.createNPCPlanet(i, position, t('planet.planetPrefix'))
|
||||||
universeStore.planets[key] = npcPlanet
|
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<string, unknown> = {
|
||||||
|
// 保存探险区域信息
|
||||||
|
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 = () => {
|
const openEnemyAlertPanel = () => {
|
||||||
enemyAlertNotificationsRef.value?.open()
|
enemyAlertNotificationsRef.value?.open()
|
||||||
@@ -893,116 +1804,391 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建任务引擎(处理舰队任务和导弹攻击)
|
/**
|
||||||
missionEngine = createMissionEngine({
|
* 同步NPC星球数据到universeStore
|
||||||
removeIncomingFleetAlert: removeIncomingFleetAlertById
|
* 解决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 }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 创建NPC引擎(处理NPC成长和行为,支持分片更新)
|
const updateNPCGrowth = (deltaSeconds: number) => {
|
||||||
npcEngine = createNpcEngine({
|
// 累积时间
|
||||||
sliceSize: 20, // 每 tick 最多更新 20 个 NPC
|
npcUpdateCounter.value += deltaSeconds
|
||||||
growthInterval: 5, // 成长更新间隔 5 秒
|
|
||||||
behaviorInterval: 5 // 行为更新间隔 5 秒
|
|
||||||
})
|
|
||||||
|
|
||||||
// 创建经济引擎(处理资源生产、建造/研究队列)
|
// 只在达到更新间隔时才执行
|
||||||
economyEngine = createEconomyEngine({
|
if (npcUpdateCounter.value < NPC_UPDATE_INTERVAL) {
|
||||||
onNotification: handleNotification,
|
return
|
||||||
onUnlock: handleUnlockNotification
|
}
|
||||||
})
|
|
||||||
|
|
||||||
// 创建进度引擎(处理成就检查、战役进度、外交清理等低频任务)
|
// 获取所有星球
|
||||||
progressionEngine = createProgressionEngine({
|
const allPlanets = Object.values(universeStore.planets)
|
||||||
achievementInterval: 5000, // 每5秒检查成就
|
|
||||||
campaignInterval: 5000, // 每5秒检查战役进度
|
// 如果NPC store为空,从星球数据中初始化NPC
|
||||||
diplomacyCleanupInterval: 15000, // 每15秒清理外交数据
|
if (npcStore.npcs.length === 0) {
|
||||||
onAchievementUnlock: unlock => {
|
const npcMap = new Map<string, any>()
|
||||||
|
|
||||||
|
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<string, any> = {}
|
||||||
|
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 通知
|
||||||
const tierName = t(`achievements.tiers.${unlock.tier}`)
|
const tierName = t(`achievements.tiers.${unlock.tier}`)
|
||||||
const achievementName = t(`achievements.names.${unlock.id}`)
|
const achievementName = t(`achievements.names.${unlock.id}`)
|
||||||
toast.success(t('achievements.unlocked'), {
|
toast.success(t('achievements.unlocked'), {
|
||||||
description: `${achievementName} (${tierName})`
|
description: `${achievementName} (${tierName})`
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
achievementCheckCounter.value = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动游戏循环
|
||||||
|
const startGameLoop = () => {
|
||||||
|
if (gameStore.isPaused) return
|
||||||
|
// 清理旧的定时器
|
||||||
|
if (gameLoop.value) {
|
||||||
|
clearInterval(gameLoop.value)
|
||||||
}
|
}
|
||||||
})
|
// 游戏循环固定为1秒,避免高倍速时的卡顿
|
||||||
|
// gameSpeed 只作用于资源产出和时间消耗的倍率
|
||||||
// 创建游戏引擎
|
const interval = 1000
|
||||||
gameEngine = createGameEngine({
|
// 启动新的游戏循环
|
||||||
t,
|
gameLoop.value = setInterval(() => {
|
||||||
onTick: async ctx => {
|
updateGame()
|
||||||
const { profiler } = ctx
|
}, interval)
|
||||||
|
}
|
||||||
// 初始化 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 = () => {
|
const stopGameLoop = () => {
|
||||||
stopLoop()
|
if (gameLoop.value) {
|
||||||
}
|
clearInterval(gameLoop.value)
|
||||||
|
gameLoop.value = null
|
||||||
// 启动游戏循环(带暂停检查)
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1179,8 +2365,6 @@
|
|||||||
gameStore.locale = detectBrowserLocale()
|
gameStore.locale = detectBrowserLocale()
|
||||||
}
|
}
|
||||||
await initGame()
|
await initGame()
|
||||||
// 初始化游戏引擎
|
|
||||||
gameEngine?.init()
|
|
||||||
// 启动游戏循环
|
// 启动游戏循环
|
||||||
startGameLoop()
|
startGameLoop()
|
||||||
// 启动积分更新定时器
|
// 启动积分更新定时器
|
||||||
@@ -1192,9 +2376,6 @@
|
|||||||
window.addEventListener('cancel-build', handleCancelBuildEvent as EventListener)
|
window.addEventListener('cancel-build', handleCancelBuildEvent as EventListener)
|
||||||
window.addEventListener('cancel-research', handleCancelResearchEvent as EventListener)
|
window.addEventListener('cancel-research', handleCancelResearchEvent as EventListener)
|
||||||
|
|
||||||
// 添加页面可见性变化监听(解决离线进度问题)
|
|
||||||
document.addEventListener('visibilitychange', handleVisibilityChange)
|
|
||||||
|
|
||||||
// 首次检查版本(被动检测)
|
// 首次检查版本(被动检测)
|
||||||
const versionInfo = await checkLatestVersion(gameStore.player.lastVersionCheckTime || 0, (time: number) => {
|
const versionInfo = await checkLatestVersion(gameStore.player.lastVersionCheckTime || 0, (time: number) => {
|
||||||
gameStore.player.lastVersionCheckTime = time
|
gameStore.player.lastVersionCheckTime = time
|
||||||
@@ -1259,18 +2440,15 @@
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 清理定时器和游戏引擎
|
// 清理定时器
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
stopGameLoop()
|
if (gameLoop.value) clearInterval(gameLoop.value)
|
||||||
gameEngine?.dispose()
|
|
||||||
if (pointsUpdateInterval.value) clearInterval(pointsUpdateInterval.value)
|
if (pointsUpdateInterval.value) clearInterval(pointsUpdateInterval.value)
|
||||||
if (konamiCleanup.value) konamiCleanup.value()
|
if (konamiCleanup.value) konamiCleanup.value()
|
||||||
if (versionCheckInterval.value) clearInterval(versionCheckInterval.value)
|
if (versionCheckInterval.value) clearInterval(versionCheckInterval.value)
|
||||||
// 移除队列取消事件监听
|
// 移除队列取消事件监听
|
||||||
window.removeEventListener('cancel-build', handleCancelBuildEvent as EventListener)
|
window.removeEventListener('cancel-build', handleCancelBuildEvent as EventListener)
|
||||||
window.removeEventListener('cancel-research', handleCancelResearchEvent as EventListener)
|
window.removeEventListener('cancel-research', handleCancelResearchEvent as EventListener)
|
||||||
// 移除页面可见性变化监听
|
|
||||||
document.removeEventListener('visibilitychange', handleVisibilityChange)
|
|
||||||
// 移除 Android 返回键监听
|
// 移除 Android 返回键监听
|
||||||
if (Capacitor.isNativePlatform()) {
|
if (Capacitor.isNativePlatform()) {
|
||||||
CapacitorApp.removeAllListeners()
|
CapacitorApp.removeAllListeners()
|
||||||
|
|||||||
@@ -1,144 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
/**
|
|
||||||
* 调试面板
|
|
||||||
* 仅在开发环境显示,用于查看性能数据
|
|
||||||
*/
|
|
||||||
import { ref, onMounted, onUnmounted, computed } from 'vue'
|
|
||||||
import { profiler } from '@/services/profiler'
|
|
||||||
import { ChevronDown, ChevronUp, Bug, X } from 'lucide-vue-next'
|
|
||||||
|
|
||||||
// 是否展开
|
|
||||||
const expanded = ref(false)
|
|
||||||
// 是否显示面板
|
|
||||||
const visible = ref(true)
|
|
||||||
// 性能数据
|
|
||||||
const averages = ref<Record<string, number>>({})
|
|
||||||
// 更新定时器
|
|
||||||
let updateInterval: ReturnType<typeof setInterval> | null = null
|
|
||||||
|
|
||||||
// 是否为开发环境
|
|
||||||
const isDev = import.meta.env.DEV
|
|
||||||
|
|
||||||
// 格式化耗时
|
|
||||||
const formatTime = (ms: number) => {
|
|
||||||
if (ms < 1) return `${(ms * 1000).toFixed(0)}μs`
|
|
||||||
return `${ms.toFixed(2)}ms`
|
|
||||||
}
|
|
||||||
|
|
||||||
// 排序后的性能数据
|
|
||||||
const sortedAverages = computed(() => {
|
|
||||||
return Object.entries(averages.value)
|
|
||||||
.sort((a, b) => b[1] - a[1]) // 按耗时降序
|
|
||||||
.map(([label, time]) => ({ label, time }))
|
|
||||||
})
|
|
||||||
|
|
||||||
// 总耗时
|
|
||||||
const totalTime = computed(() => {
|
|
||||||
return averages.value['tick:total'] ?? 0
|
|
||||||
})
|
|
||||||
|
|
||||||
// 性能状态颜色
|
|
||||||
const statusColor = computed(() => {
|
|
||||||
if (totalTime.value > 16) return 'text-red-500' // 超过 16ms,低于 60fps
|
|
||||||
if (totalTime.value > 8) return 'text-yellow-500' // 超过 8ms
|
|
||||||
return 'text-green-500' // 正常
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if (!isDev) return
|
|
||||||
|
|
||||||
// 每秒更新一次平均值
|
|
||||||
updateInterval = setInterval(() => {
|
|
||||||
averages.value = profiler.averages()
|
|
||||||
}, 1000)
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (updateInterval) {
|
|
||||||
clearInterval(updateInterval)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const toggleExpanded = () => {
|
|
||||||
expanded.value = !expanded.value
|
|
||||||
}
|
|
||||||
|
|
||||||
const close = () => {
|
|
||||||
visible.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 键盘快捷键:Ctrl+Shift+D 切换显示
|
|
||||||
const handleKeydown = (e: KeyboardEvent) => {
|
|
||||||
if (e.ctrlKey && e.shiftKey && e.key === 'D') {
|
|
||||||
visible.value = !visible.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
window.addEventListener('keydown', handleKeydown)
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
window.removeEventListener('keydown', handleKeydown)
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
v-if="isDev && visible"
|
|
||||||
class="fixed bottom-4 right-4 z-[9999] bg-black/80 text-white text-xs font-mono rounded-lg shadow-lg backdrop-blur-sm border border-white/20"
|
|
||||||
:class="expanded ? 'w-64' : 'w-auto'"
|
|
||||||
>
|
|
||||||
<!-- 标题栏 -->
|
|
||||||
<div
|
|
||||||
class="flex items-center justify-between px-3 py-2 cursor-pointer hover:bg-white/10 rounded-t-lg"
|
|
||||||
@click="toggleExpanded"
|
|
||||||
>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<Bug class="w-4 h-4" />
|
|
||||||
<span>Debug</span>
|
|
||||||
<span :class="statusColor">{{ formatTime(totalTime) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-1">
|
|
||||||
<button
|
|
||||||
class="p-1 hover:bg-white/20 rounded"
|
|
||||||
@click.stop="close"
|
|
||||||
title="关闭 (Ctrl+Shift+D)"
|
|
||||||
>
|
|
||||||
<X class="w-3 h-3" />
|
|
||||||
</button>
|
|
||||||
<component :is="expanded ? ChevronDown : ChevronUp" class="w-4 h-4" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 展开内容 -->
|
|
||||||
<div v-if="expanded" class="px-3 pb-3 space-y-2 max-h-64 overflow-y-auto">
|
|
||||||
<!-- 性能数据 -->
|
|
||||||
<div v-if="sortedAverages.length > 0">
|
|
||||||
<div class="text-white/50 mb-1">平均耗时 (60帧)</div>
|
|
||||||
<div
|
|
||||||
v-for="item in sortedAverages"
|
|
||||||
:key="item.label"
|
|
||||||
class="flex justify-between py-0.5 border-b border-white/10 last:border-0"
|
|
||||||
>
|
|
||||||
<span class="text-white/70">{{ item.label }}</span>
|
|
||||||
<span
|
|
||||||
:class="{
|
|
||||||
'text-red-400': item.time > 8,
|
|
||||||
'text-yellow-400': item.time > 4 && item.time <= 8,
|
|
||||||
'text-green-400': item.time <= 4
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
{{ formatTime(item.time) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else class="text-white/50">等待数据...</div>
|
|
||||||
|
|
||||||
<!-- 提示 -->
|
|
||||||
<div class="text-white/30 text-[10px] pt-2 border-t border-white/10">
|
|
||||||
Ctrl+Shift+D 切换显示
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
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<TimeSourceConfig>
|
|
||||||
/** 追赶模式回调 */
|
|
||||||
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<ReturnType<typeof setInterval> | null>(null)
|
|
||||||
const catchUpTimerId = ref<ReturnType<typeof setInterval> | 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -981,8 +981,7 @@ export default {
|
|||||||
gamePause: 'Spielpause',
|
gamePause: 'Spielpause',
|
||||||
gamePauseDesc: 'Spielzeit und Ressourcenproduktion pausieren oder fortsetzen',
|
gamePauseDesc: 'Spielzeit und Ressourcenproduktion pausieren oder fortsetzen',
|
||||||
battleMode: 'Bis zum Ende kämpfen',
|
battleMode: 'Bis zum Ende kämpfen',
|
||||||
battleModeDesc:
|
battleModeDesc: 'Wenn aktiviert, dauern Kämpfe bis zu 100 Runden bis ein Sieger feststeht. Wenn deaktiviert, wird der klassische 6-Runden-Modus verwendet',
|
||||||
'Wenn aktiviert, dauern Kämpfe bis zu 100 Runden bis ein Sieger feststeht. Wenn deaktiviert, wird der klassische 6-Runden-Modus verwendet',
|
|
||||||
pause: 'Pausieren',
|
pause: 'Pausieren',
|
||||||
resume: 'Fortsetzen',
|
resume: 'Fortsetzen',
|
||||||
gamePaused: 'Spiel pausiert',
|
gamePaused: 'Spiel pausiert',
|
||||||
|
|||||||
@@ -1,82 +0,0 @@
|
|||||||
/**
|
|
||||||
* 脏标志系统
|
|
||||||
* 用于性能优化,追踪哪些子系统需要处理
|
|
||||||
* 当标志为 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
|
|
||||||
}
|
|
||||||
@@ -1,445 +0,0 @@
|
|||||||
/**
|
|
||||||
* 任务解析纯函数
|
|
||||||
* 所有函数都是纯函数:无副作用,相同输入总是产生相同输出
|
|
||||||
* 便于单元测试和服务端复用
|
|
||||||
*/
|
|
||||||
|
|
||||||
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<Fleet>): 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<Fleet>, 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<Fleet>,
|
|
||||||
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<Fleet>, 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<Fleet>, 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<Fleet>, 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<Fleet>,
|
|
||||||
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<Fleet>,
|
|
||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,345 +0,0 @@
|
|||||||
/**
|
|
||||||
* 生产计算纯函数
|
|
||||||
* 所有函数都是纯函数:无副作用,相同输入总是产生相同输出
|
|
||||||
* 便于单元测试和服务端复用
|
|
||||||
*/
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,332 +0,0 @@
|
|||||||
/**
|
|
||||||
* 队列处理纯函数
|
|
||||||
* 所有函数都是纯函数:无副作用,相同输入总是产生相同输出
|
|
||||||
* 便于单元测试和服务端复用
|
|
||||||
*/
|
|
||||||
|
|
||||||
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<string, number>
|
|
||||||
/** 完成的建筑列表 */
|
|
||||||
completed: Array<{ type: BuildingType; level: number }>
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 研究完成结果
|
|
||||||
*/
|
|
||||||
export interface ResearchCompletionResult {
|
|
||||||
/** 新的科技等级 */
|
|
||||||
newTechnologies: Record<string, number>
|
|
||||||
/** 完成的研究列表 */
|
|
||||||
completed: Array<{ type: TechnologyType; level: number }>
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 船厂完成结果
|
|
||||||
*/
|
|
||||||
export interface ShipyardCompletionResult {
|
|
||||||
/** 新的舰队 */
|
|
||||||
newFleet: Partial<Fleet>
|
|
||||||
/** 完成的船只列表 */
|
|
||||||
completed: Array<{ type: ShipType; count: number }>
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 防御完成结果
|
|
||||||
*/
|
|
||||||
export interface DefenseCompletionResult {
|
|
||||||
/** 新的防御设施 */
|
|
||||||
newDefense: Partial<Record<DefenseType, number>>
|
|
||||||
/** 完成的防御列表 */
|
|
||||||
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<string, number>,
|
|
||||||
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<string, number>,
|
|
||||||
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<Fleet>, 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<Record<DefenseType, number>>,
|
|
||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
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<void>
|
|
||||||
/** 暂停状态变化回调 */
|
|
||||||
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<typeof useGameStore> | null = null
|
|
||||||
let universeStore: ReturnType<typeof useUniverseStore> | null = null
|
|
||||||
let npcStore: ReturnType<typeof useNPCStore> | 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,1014 +0,0 @@
|
|||||||
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<void>
|
|
||||||
}
|
|
||||||
|
|
||||||
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<string, unknown> = {
|
|
||||||
// 保存探险区域信息
|
|
||||||
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 }
|
|
||||||
}
|
|
||||||
@@ -1,425 +0,0 @@
|
|||||||
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<string, NPC>()
|
|
||||||
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<string, any> = {}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
/**
|
|
||||||
* 性能分析器
|
|
||||||
* 用于追踪每个 tick 中各子系统的耗时
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 性能分析器接口
|
|
||||||
*/
|
|
||||||
export interface Profiler {
|
|
||||||
/** 开始计时 */
|
|
||||||
start(label: string): void
|
|
||||||
/** 结束计时 */
|
|
||||||
end(label: string): void
|
|
||||||
/** 获取当前快照(各标签耗时,单位 ms) */
|
|
||||||
snapshot(): Record<string, number>
|
|
||||||
/** 重置所有计时数据 */
|
|
||||||
reset(): void
|
|
||||||
/** 获取平均耗时(最近 N 次) */
|
|
||||||
averages(): Record<string, number>
|
|
||||||
/** 是否启用 */
|
|
||||||
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<string, number> = new Map()
|
|
||||||
// 开始时间戳
|
|
||||||
const startTimes: Map<string, number> = new Map()
|
|
||||||
// 历史记录(用于计算平均值)
|
|
||||||
const history: Map<string, number[]> = 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<string, number> => {
|
|
||||||
const result: Record<string, number> = {}
|
|
||||||
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<string, number> => {
|
|
||||||
const result: Record<string, number> = {}
|
|
||||||
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 = <T extends (...args: unknown[]) => 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
|
|
||||||
}
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,245 +0,0 @@
|
|||||||
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<void>
|
|
||||||
/** 保存到 localStorage */
|
|
||||||
save(): Promise<void>
|
|
||||||
/** 导出存档为字符串 */
|
|
||||||
exportSave(): string
|
|
||||||
/** 从字符串导入存档 */
|
|
||||||
importSave(payload: string): Promise<void>
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 当前存档版本号(从 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<number, (payload: SavePayload) => 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<string, unknown>
|
|
||||||
return typeof obj.version === 'number' && typeof obj.createdAt === 'number' && 'data' in obj
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查是否为 GameStore 状态结构
|
|
||||||
*/
|
|
||||||
const isLikelyGameStoreState = (value: unknown): value is Record<string, unknown> => {
|
|
||||||
if (typeof value !== 'object' || value === null) return false
|
|
||||||
const obj = value as Record<string, unknown>
|
|
||||||
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: <cipher>, universe: <cipher|null>, npcs: <cipher|null> }
|
|
||||||
if (parsed && typeof parsed === 'object' && 'game' in parsed) {
|
|
||||||
const legacyParsed = parsed as Record<string, unknown>
|
|
||||||
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<typeof useGameStore> | null = null
|
|
||||||
let universeStore: ReturnType<typeof useUniverseStore> | null = null
|
|
||||||
let npcStore: ReturnType<typeof useNPCStore> | 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<void> => {
|
|
||||||
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<void> => {
|
|
||||||
if (typeof localStorage === 'undefined') return
|
|
||||||
localStorage.setItem(DEFAULT_SAVE_KEY, exportSave())
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 从 localStorage 加载存档 */
|
|
||||||
const load = async (): Promise<void> => {
|
|
||||||
if (typeof localStorage === 'undefined') return
|
|
||||||
const saved = localStorage.getItem(DEFAULT_SAVE_KEY)
|
|
||||||
if (!saved) return
|
|
||||||
await importSave(saved)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
load,
|
|
||||||
save,
|
|
||||||
exportSave,
|
|
||||||
importSave
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
/**
|
|
||||||
* 任务函数类型
|
|
||||||
*/
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
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, string | number>) => 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<typeof useGameStore>
|
|
||||||
universeStore: ReturnType<typeof useUniverseStore>
|
|
||||||
npcStore: ReturnType<typeof useNPCStore>
|
|
||||||
|
|
||||||
// 国际化
|
|
||||||
locale: Locale
|
|
||||||
t: TranslateFn
|
|
||||||
|
|
||||||
// 通知服务
|
|
||||||
notify: NotifyFn
|
|
||||||
notifyUnlock: NotifyUnlockFn
|
|
||||||
|
|
||||||
// 性能优化:脏标志
|
|
||||||
dirtyFlags: DirtyFlags
|
|
||||||
|
|
||||||
// 性能分析器(仅开发环境有效)
|
|
||||||
profiler: Profiler
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 TickContext 的选项
|
|
||||||
*/
|
|
||||||
export interface CreateTickContextOptions {
|
|
||||||
gameStore: ReturnType<typeof useGameStore>
|
|
||||||
universeStore: ReturnType<typeof useUniverseStore>
|
|
||||||
npcStore: ReturnType<typeof useNPCStore>
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,186 +0,0 @@
|
|||||||
/**
|
|
||||||
* 时间源服务
|
|
||||||
* 提供稳定的时间源,处理离线/标签页不活跃场景
|
|
||||||
* 防止时间跳跃导致的资源溢出或其他异常
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 时间源接口
|
|
||||||
*/
|
|
||||||
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<TimeSourceConfig>): 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
|
|
||||||
}
|
|
||||||
@@ -399,10 +399,8 @@
|
|||||||
import WebDAVFileListDialog from '@/components/settings/WebDAVFileListDialog.vue'
|
import WebDAVFileListDialog from '@/components/settings/WebDAVFileListDialog.vue'
|
||||||
import { useHints } from '@/composables/useHints'
|
import { useHints } from '@/composables/useHints'
|
||||||
import { uploadToWebDAV, downloadFromWebDAV } from '@/services/webdavService'
|
import { uploadToWebDAV, downloadFromWebDAV } from '@/services/webdavService'
|
||||||
import { createSaveSystem } from '@/services/saveSystem'
|
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const saveSystem = createSaveSystem()
|
|
||||||
const { hintsEnabled, setHintsEnabled, resetHints } = useHints()
|
const { hintsEnabled, setHintsEnabled, resetHints } = useHints()
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
|
|
||||||
@@ -570,10 +568,27 @@
|
|||||||
try {
|
try {
|
||||||
isExporting.value = true
|
isExporting.value = true
|
||||||
|
|
||||||
// 使用 SaveSystem 导出(带版本号的 SaveEnvelope 格式)
|
// 获取游戏数据
|
||||||
const jsonString = saveSystem.exportSave()
|
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
|
||||||
|
}
|
||||||
|
|
||||||
const fileName = `${pkg.name}-${new Date().toISOString().slice(0, 10)}-${Date.now()}.json`
|
const fileName = `${pkg.name}-${new Date().toISOString().slice(0, 10)}-${Date.now()}.json`
|
||||||
|
const jsonString = JSON.stringify(exportData, null, 2)
|
||||||
|
|
||||||
// Android 保存到公共 Downloads 目录
|
// Android 保存到公共 Downloads 目录
|
||||||
if (Capacitor.isNativePlatform()) {
|
if (Capacitor.isNativePlatform()) {
|
||||||
@@ -628,12 +643,33 @@
|
|||||||
const importData = async (file: File) => {
|
const importData = async (file: File) => {
|
||||||
try {
|
try {
|
||||||
const reader = new FileReader()
|
const reader = new FileReader()
|
||||||
reader.onload = async e => {
|
reader.onload = e => {
|
||||||
try {
|
try {
|
||||||
const result = e.target?.result
|
const result = e.target?.result
|
||||||
if (typeof result === 'string') {
|
if (typeof result === 'string') {
|
||||||
// 使用 SaveSystem 导入(支持新旧格式自动转换和迁移)
|
const importData = JSON.parse(result)
|
||||||
await saveSystem.importSave(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)
|
||||||
|
}
|
||||||
|
|
||||||
toast.success(t('settings.importSuccess'))
|
toast.success(t('settings.importSuccess'))
|
||||||
// 延迟刷新页面以让toast显示
|
// 延迟刷新页面以让toast显示
|
||||||
setTimeout(() => window.location.reload(), 1000)
|
setTimeout(() => window.location.reload(), 1000)
|
||||||
@@ -742,8 +778,24 @@
|
|||||||
|
|
||||||
isWebDAVUploading.value = true
|
isWebDAVUploading.value = true
|
||||||
try {
|
try {
|
||||||
// 使用 SaveSystem 导出(带版本号的 SaveEnvelope 格式)
|
// 获取游戏数据
|
||||||
const jsonString = saveSystem.exportSave()
|
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)
|
||||||
const result = await uploadToWebDAV(webdavConfig.value, jsonString)
|
const result = await uploadToWebDAV(webdavConfig.value, jsonString)
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
@@ -777,10 +829,25 @@
|
|||||||
showConfirmDialog.value = true
|
showConfirmDialog.value = true
|
||||||
gameStore.isPaused = true
|
gameStore.isPaused = true
|
||||||
|
|
||||||
confirmCallback = async () => {
|
confirmCallback = () => {
|
||||||
try {
|
try {
|
||||||
// 使用 SaveSystem 导入(支持新旧格式自动转换和迁移)
|
const importData = JSON.parse(result.data!)
|
||||||
await saveSystem.importSave(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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
toast.success(t('settings.importSuccess'))
|
toast.success(t('settings.importSuccess'))
|
||||||
setTimeout(() => window.location.reload(), 1000)
|
setTimeout(() => window.location.reload(), 1000)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -5,9 +5,8 @@
|
|||||||
import type { Fleet, Resources } from '@/types/game'
|
import type { Fleet, Resources } from '@/types/game'
|
||||||
import { ShipType, DefenseType } from '@/types/game'
|
import { ShipType, DefenseType } from '@/types/game'
|
||||||
import { SHIPS, DEFENSES } from '@/config/gameConfig'
|
import { SHIPS, DEFENSES } from '@/config/gameConfig'
|
||||||
import type { BattleSideData, BattleSimulationResult } from '@/types/worker'
|
import type { WorkerRequestMessage, WorkerResponseMessage, BattleSideData, BattleSimulationResult } from '@/types/worker'
|
||||||
import type { WorkerRequest, WorkerResponse } from './types'
|
import { WorkerMessageType } from '@/types/worker'
|
||||||
import { WorkerMessageType } from './types'
|
|
||||||
|
|
||||||
// 战斗单位接口
|
// 战斗单位接口
|
||||||
interface CombatUnit {
|
interface CombatUnit {
|
||||||
@@ -421,7 +420,7 @@ const calculateDebrisField = (
|
|||||||
// Worker 消息处理
|
// Worker 消息处理
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
self.onmessage = (event: MessageEvent<WorkerRequest>) => {
|
self.onmessage = (event: MessageEvent<WorkerRequestMessage>) => {
|
||||||
const { id, type, payload } = event.data
|
const { id, type, payload } = event.data
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -468,17 +467,19 @@ self.onmessage = (event: MessageEvent<WorkerRequest>) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 发送成功响应
|
// 发送成功响应
|
||||||
const response: WorkerResponse = {
|
const response: WorkerResponseMessage = {
|
||||||
id,
|
id,
|
||||||
ok: true,
|
type: WorkerMessageType.SUCCESS,
|
||||||
payload: result
|
success: true,
|
||||||
|
data: result
|
||||||
}
|
}
|
||||||
self.postMessage(response)
|
self.postMessage(response)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 发送错误响应
|
// 发送错误响应
|
||||||
const response: WorkerResponse = {
|
const response: WorkerResponseMessage = {
|
||||||
id,
|
id,
|
||||||
ok: false,
|
type: WorkerMessageType.ERROR,
|
||||||
|
success: false,
|
||||||
error: error instanceof Error ? error.message : String(error)
|
error: error instanceof Error ? error.message : String(error)
|
||||||
}
|
}
|
||||||
self.postMessage(response)
|
self.postMessage(response)
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
/**
|
|
||||||
* Worker 通用类型定义
|
|
||||||
* 统一的请求/响应格式
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Worker 请求消息
|
|
||||||
* @template T - payload 类型
|
|
||||||
*/
|
|
||||||
export interface WorkerRequest<T = unknown> {
|
|
||||||
/** 唯一消息 ID */
|
|
||||||
id: string
|
|
||||||
/** 消息类型 */
|
|
||||||
type: string
|
|
||||||
/** 请求载荷 */
|
|
||||||
payload: T
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Worker 响应消息
|
|
||||||
* @template T - payload 类型
|
|
||||||
*/
|
|
||||||
export interface WorkerResponse<T = unknown> {
|
|
||||||
/** 对应请求的消息 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]
|
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
* Worker 管理器
|
* Worker 管理器
|
||||||
* 统一管理所有 Worker 的创建、通信和销毁
|
* 统一管理所有 Worker 的创建、通信和销毁
|
||||||
*/
|
*/
|
||||||
import type { WorkerRequest, WorkerResponse, WorkerMessageType } from './types'
|
import type { WorkerRequestMessage, WorkerResponseMessage, WorkerMessageType } from '@/types/worker'
|
||||||
import { WorkerMessageType as MsgType } from './types'
|
import { WorkerMessageType as MsgType } from '@/types/worker'
|
||||||
import { toRaw } from 'vue'
|
import { toRaw } from 'vue'
|
||||||
import BattleWorker from './battle.worker?worker'
|
import BattleWorker from './battle.worker?worker'
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ class WorkerManager {
|
|||||||
private battleWorker: Worker | null = null
|
private battleWorker: Worker | null = null
|
||||||
private pendingTasks: Map<string, WorkerTask> = new Map()
|
private pendingTasks: Map<string, WorkerTask> = new Map()
|
||||||
private messageIdCounter = 0
|
private messageIdCounter = 0
|
||||||
private readonly defaultTimeout = 10000 // 10秒超时
|
private readonly defaultTimeout = 10000 // 30秒超时
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始化战斗 Worker
|
* 初始化战斗 Worker
|
||||||
@@ -69,8 +69,8 @@ class WorkerManager {
|
|||||||
* 设置 Worker 消息处理器
|
* 设置 Worker 消息处理器
|
||||||
*/
|
*/
|
||||||
private setupWorkerHandlers(worker: Worker, workerName: string): void {
|
private setupWorkerHandlers(worker: Worker, workerName: string): void {
|
||||||
worker.onmessage = (event: MessageEvent<WorkerResponse>) => {
|
worker.onmessage = (event: MessageEvent<WorkerResponseMessage>) => {
|
||||||
const { id, ok, payload, error } = event.data
|
const { id, success, data, error } = event.data
|
||||||
|
|
||||||
const task = this.pendingTasks.get(id)
|
const task = this.pendingTasks.get(id)
|
||||||
if (!task) {
|
if (!task) {
|
||||||
@@ -87,8 +87,8 @@ class WorkerManager {
|
|||||||
this.pendingTasks.delete(id)
|
this.pendingTasks.delete(id)
|
||||||
|
|
||||||
// 处理响应
|
// 处理响应
|
||||||
if (ok) {
|
if (success) {
|
||||||
task.resolve(payload)
|
task.resolve(data)
|
||||||
} else {
|
} else {
|
||||||
task.reject(new Error(error || 'Worker task failed'))
|
task.reject(new Error(error || 'Worker task failed'))
|
||||||
}
|
}
|
||||||
@@ -157,7 +157,7 @@ class WorkerManager {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 发送消息(使用 toPlainObject 转换 Vue Proxy 对象,然后使用浏览器内置的 structured clone)
|
// 发送消息(使用 toPlainObject 转换 Vue Proxy 对象,然后使用浏览器内置的 structured clone)
|
||||||
const message: WorkerRequest = { id, type, payload: toPlainObject(payload) }
|
const message: WorkerRequestMessage = { id, type, payload: toPlainObject(payload) }
|
||||||
worker.postMessage(message)
|
worker.postMessage(message)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user