feat: 重构战报弹窗与模拟器视图,优化UI与逻辑

重构BattleReportDialog和BattleSimulatorView相关静态资源,替换旧版JS/CSS文件,提升界面一致性和交互体验。新增和优化空状态、滚动区域等通用UI组件,移除部分冗余composable,完善多语言内容。引入导弹逻辑,补充版本检测工具,提升整体代码结构和可维护性。
This commit is contained in:
谦君
2025-12-15 20:04:40 +08:00
parent 9b9fda0400
commit 59dd7bfd05
126 changed files with 3944 additions and 1487 deletions

View File

@@ -13,15 +13,67 @@
<!-- 星球信息 -->
<SidebarGroup v-if="planet" class="border-b group-data-[collapsible=icon]:hidden">
<div class="px-4 py-3 space-y-2 text-sm">
<div>
<p class="font-semibold mb-1">
{{ planet.name }}
<Badge v-if="planet.isMoon" variant="secondary" class="ml-1 text-xs">{{ t('planet.moon') }}</Badge>
</p>
<p class="text-muted-foreground text-xs">
[{{ planet.position.galaxy }}:{{ planet.position.system }}:{{ planet.position.position }}]
</p>
</div>
<!-- 星球切换器 -->
<Popover>
<PopoverTrigger as-child>
<Button
variant="outline"
class="w-full justify-between h-auto px-3 py-2.5 border-2 hover:bg-accent hover:border-primary transition-colors"
>
<div class="flex items-start gap-2.5 flex-1 min-w-0">
<Globe class="h-5 w-5 flex-shrink-0 mt-0.5 text-primary" />
<div class="flex-1 min-w-0 text-left">
<div class="text-[10px] text-muted-foreground uppercase tracking-wider mb-0.5">
{{ t('planet.currentPlanet') }}
</div>
<div class="flex items-center gap-1.5 mb-0.5">
<span class="truncate font-semibold text-sm">{{ planet.name }}</span>
<Badge v-if="planet.isMoon" variant="secondary" class="text-[10px] px-1 py-0 h-4">
{{ t('planet.moon') }}
</Badge>
</div>
<div class="text-[11px] text-muted-foreground">
[{{ planet.position.galaxy }}:{{ planet.position.system }}:{{ planet.position.position }}]
</div>
</div>
</div>
<ChevronsUpDown class="h-4 w-4 flex-shrink-0 text-muted-foreground ml-2" />
</Button>
</PopoverTrigger>
<PopoverContent class="w-72 p-0" side="bottom" align="start">
<div class="p-2">
<div class="px-2 py-1.5 mb-1 text-xs font-semibold text-muted-foreground">
{{ t('planet.switchPlanet') }}
</div>
<div class="space-y-0.5 max-h-80 overflow-y-auto">
<Button
v-for="p in gameStore.player.planets"
:key="p.id"
@click="switchToPlanet(p.id)"
:variant="p.id === planet.id ? 'secondary' : 'ghost'"
class="w-full justify-start h-auto py-2 px-2"
size="sm"
>
<div class="flex items-start gap-2 w-full min-w-0">
<Globe class="h-4 w-4 flex-shrink-0 mt-0.5" :class="p.id === planet.id ? 'text-primary' : ''" />
<div class="flex-1 min-w-0 text-left">
<div class="flex items-center gap-1.5 mb-0.5">
<span class="truncate font-medium text-sm">{{ p.name }}</span>
<Badge v-if="p.isMoon" variant="outline" class="text-[10px] px-1 py-0 h-4">
{{ t('planet.moon') }}
</Badge>
</div>
<div class="text-[11px] text-muted-foreground">
[{{ p.position.galaxy }}:{{ p.position.system }}:{{ p.position.position }}]
</div>
</div>
</div>
</Button>
</div>
</div>
</PopoverContent>
</Popover>
<!-- 玩家积分显示 -->
<div class="bg-muted/50 rounded-lg p-2">
<div class="flex items-center justify-between">
@@ -50,9 +102,19 @@
<component :is="item.icon" />
<span>{{ item.name.value }}</span>
<!-- 未读消息数量 -->
<SidebarMenuBadge v-if="item.path === '/messages' && unreadMessagesCount > 0">
<SidebarMenuBadge
v-if="item.path === '/messages' && unreadMessagesCount > 0"
class="bg-destructive text-destructive-foreground"
>
{{ unreadMessagesCount }}
</SidebarMenuBadge>
<!-- 正在执行的舰队任务数量 -->
<SidebarMenuBadge
v-if="item.path === '/fleet' && activeFleetMissionsCount > 0"
class="bg-primary text-primary-foreground"
>
{{ activeFleetMissionsCount }}
</SidebarMenuBadge>
</RouterLink>
</SidebarMenuButton>
</SidebarMenuItem>
@@ -72,7 +134,11 @@
<span>{{ localeNames[gameStore.locale] }}</span>
</SidebarMenuButton>
</PopoverTrigger>
<PopoverContent class="w-48 p-2" side="right" align="end">
<PopoverContent
class="w-48 p-2"
:side="sidebarOpen || innerWidth < 768 ? 'top' : 'right'"
:align="sidebarOpen || innerWidth < 768 ? 'center' : 'end'"
>
<div class="space-y-1">
<Button
v-for="locale in locales"
@@ -111,49 +177,139 @@
<!-- 主内容区 -->
<SidebarInset>
<div class="flex flex-col h-full overflow-hidden">
<!-- 顶部资源栏 -->
<header v-if="planet" class="bg-card border-b px-4 sm:px-6 py-6.5 shadow-md">
<div class="flex items-center justify-between gap-3 sm:gap-6">
<!-- 汉堡菜单移动端- 左侧占位 -->
<div class="lg:flex-1">
<SidebarTrigger class="lg:hidden" />
</div>
<div class="flex flex-col h-full overflow-hidden pt-[60px]">
<!-- 顶部资源栏 - 固定定位 -->
<header
v-if="planet"
class="fixed top-0 right-0 left-0 z-40 bg-card border-b px-4 sm:px-6 py-3 shadow-md"
:class="sidebarOpen ? 'lg:left-[var(--sidebar-width)]' : 'lg:left-[var(--sidebar-width-icon)]'"
>
<div class="flex flex-col gap-3">
<!-- 第一行菜单资源预览状态 -->
<div class="grid items-center gap-3 sm:gap-6" style="grid-template-columns: auto 1fr auto">
<!-- 左侧汉堡菜单移动端/ 占位PC端 -->
<div>
<SidebarTrigger class="lg:hidden" />
</div>
<!-- 资源显示 - PC端居中 -->
<div class="flex items-center gap-3 sm:gap-6 flex-1 lg:flex-none overflow-x-auto lg:justify-center">
<div v-for="resourceType in resourceTypes" :key="resourceType.key" class="flex items-center gap-1.5 sm:gap-2 flex-shrink-0">
<ResourceIcon :type="resourceType.key" size="md" />
<div class="min-w-0">
<!-- 所有资源统一显示当前值/容量 -->
<p
class="text-xs sm:text-sm font-medium truncate"
:class="getResourceColor(planet.resources[resourceType.key], capacity?.[resourceType.key] || Infinity)"
>
{{ formatNumber(planet.resources[resourceType.key]) }} / {{ formatNumber(capacity?.[resourceType.key] || 0) }}
</p>
<p class="text-[10px] sm:text-xs text-muted-foreground truncate">
+{{ formatNumber(Math.round((production?.[resourceType.key] || 0) / 60)) }}/{{ t('resources.perMinute') }}
</p>
<!-- 资源显示 - PC端居中移动端可折叠 -->
<div :class="['flex items-center gap-3 sm:gap-6 justify-center', resourceBarExpanded ? 'hidden' : 'overflow-x-auto']">
<div v-for="resourceType in resourceTypes" :key="resourceType.key" class="flex items-center gap-1.5 sm:gap-2 flex-shrink-0">
<ResourceIcon :type="resourceType.key" size="md" />
<div class="min-w-0">
<!-- 电力显示净产量和效率 -->
<template v-if="resourceType.key === 'energy'">
<p
class="text-xs sm:text-sm font-medium truncate"
:class="netEnergy >= 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'"
>
{{ netEnergy >= 0 ? '+' : '' }}{{ formatNumber(netEnergy) }}
</p>
<p class="text-[10px] sm:text-xs text-muted-foreground truncate">
{{ formatNumber(production?.energy || 0) }} / {{ formatNumber(energyConsumption) }}
</p>
</template>
<!-- 其他资源统一显示当前值/容量 -->
<template v-else>
<p
class="text-xs sm:text-sm font-medium truncate"
:class="getResourceColor(planet.resources[resourceType.key], capacity?.[resourceType.key] || Infinity)"
>
{{ formatNumber(planet.resources[resourceType.key]) }} / {{ formatNumber(capacity?.[resourceType.key] || 0) }}
</p>
<p class="text-[10px] sm:text-xs text-muted-foreground truncate">
+{{ formatNumber(Math.round((production?.[resourceType.key] || 0) / 60)) }}/{{ t('resources.perMinute') }}
</p>
</template>
</div>
</div>
</div>
</div>
<!-- 右侧状态 - 右侧占位 -->
<div class="flex items-center gap-2 sm:gap-4 flex-shrink-0 lg:flex-1 lg:justify-end">
<!-- 建造队列状态 -->
<div v-if="planet.buildQueue.length > 0" class="flex items-center gap-1.5 sm:gap-2 text-xs sm:text-sm">
<div class="h-2 w-2 rounded-full bg-green-500 animate-pulse" />
<span class="text-muted-foreground hidden sm:inline">{{ t('queue.building') }}</span>
</div>
<div v-if="gameStore.player.researchQueue.length > 0" class="flex items-center gap-1.5 sm:gap-2 text-xs sm:text-sm">
<div class="h-2 w-2 rounded-full bg-blue-500 animate-pulse" />
<span class="text-muted-foreground hidden sm:inline">{{ t('queue.researching') }}</span>
<!-- 右侧展开按钮仅移动端 + 状态 -->
<div class="flex items-center gap-2 sm:gap-3 flex-shrink-0 justify-end">
<!-- 移动端展开按钮 -->
<Button @click="resourceBarExpanded = !resourceBarExpanded" variant="ghost" size="sm" class="lg:hidden h-8 w-8 p-0">
<ChevronDown v-if="!resourceBarExpanded" class="h-4 w-4" />
<ChevronUp v-else class="h-4 w-4" />
</Button>
<!-- 建造队列状态 -->
<div v-if="planet.buildQueue.length > 0" class="flex items-center gap-1.5 text-xs sm:text-sm">
<div class="h-2 w-2 rounded-full bg-green-500 animate-pulse" />
<span class="text-muted-foreground hidden sm:inline">{{ t('queue.building') }}</span>
</div>
<div v-if="gameStore.player.researchQueue.length > 0" class="flex items-center gap-1.5 text-xs sm:text-sm">
<div class="h-2 w-2 rounded-full bg-blue-500 animate-pulse" />
<span class="text-muted-foreground hidden sm:inline">{{ t('queue.researching') }}</span>
</div>
</div>
</div>
</div>
</header>
<!-- 展开的资源详情仅移动端且展开时显示 - absolute定位覆盖在内容上带过渡动画 -->
<Transition
enter-active-class="transition-all duration-300 ease-out"
enter-from-class="opacity-0 -translate-y-2"
enter-to-class="opacity-100 translate-y-0"
leave-active-class="transition-all duration-200 ease-in"
leave-from-class="opacity-100 translate-y-0"
leave-to-class="opacity-0 -translate-y-2"
>
<div
v-if="planet && resourceBarExpanded"
class="fixed top-[60px] right-0 left-0 z-30 bg-card border-b px-4 py-3 shadow-md lg:hidden"
:class="sidebarOpen ? 'lg:left-[var(--sidebar-width)]' : 'lg:left-[var(--sidebar-width-icon)]'"
>
<div class="grid grid-cols-2 gap-3">
<div v-for="resourceType in resourceTypes" :key="resourceType.key" class="bg-muted/50 rounded-lg p-2.5">
<div class="flex items-center justify-center gap-2 mb-1.5">
<ResourceIcon :type="resourceType.key" size="md" />
<span class="text-xs font-medium text-muted-foreground">{{ t(`resources.${resourceType.key}`) }}</span>
</div>
<div class="space-y-0.5 text-center">
<!-- 电力显示净产量和效率 -->
<template v-if="resourceType.key === 'energy'">
<p
class="text-sm font-semibold"
:class="netEnergy >= 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'"
>
{{ netEnergy >= 0 ? '+' : '' }}{{ formatNumber(netEnergy) }}
</p>
<p class="text-[10px] text-muted-foreground">
{{ t('resources.production') }}: {{ formatNumber(production?.energy || 0) }} / {{ formatNumber(energyConsumption) }}
</p>
</template>
<!-- 其他资源统一显示当前值/容量 -->
<template v-else>
<p
class="text-sm font-semibold"
:class="getResourceColor(planet.resources[resourceType.key], capacity?.[resourceType.key] || Infinity)"
>
{{ formatNumber(planet.resources[resourceType.key]) }}
</p>
<p class="text-[10px] text-muted-foreground">
{{ t('resources.capacity') }}: {{ formatNumber(capacity?.[resourceType.key] || 0) }}
</p>
<p class="text-[10px] text-muted-foreground">
{{ t('resources.production') }}: +{{ formatNumber(Math.round((production?.[resourceType.key] || 0) / 60)) }}/{{
t('resources.perMinute')
}}
</p>
</template>
</div>
</div>
</div>
</div>
</Transition>
<!-- 即将到来的敌对舰队警告 -->
<IncomingFleetAlerts
v-if="gameStore.player.incomingFleetAlerts && gameStore.player.incomingFleetAlerts.length > 0"
:alerts="gameStore.player.incomingFleetAlerts"
@mark-as-read="removeIncomingFleetAlert"
/>
<!-- 建造队列 -->
<div
v-if="planet && (planet.buildQueue.length > 0 || gameStore.player.researchQueue.length > 0)"
@@ -232,11 +388,13 @@
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{{ confirmDialogTitle }}</AlertDialogTitle>
<AlertDialogDescription>{{ confirmDialogMessage }}</AlertDialogDescription>
<AlertDialogDescription class="whitespace-pre-line">
{{ confirmDialogMessage }}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{{ t('common.cancel') }}</AlertDialogCancel>
<AlertDialogAction @click="handleConfirmAction">{{ t('common.confirm') }}</AlertDialogAction>
<AlertDialogAction @click="handleConfirmDialogConfirm">{{ t('common.confirm') }}</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
@@ -244,15 +402,20 @@
<!-- 详情弹窗 -->
<DetailDialog />
<!-- 更新弹窗 -->
<UpdateDialog v-model:open="showUpdateDialog" :version-info="updateInfo" />
<!-- Toast 通知 -->
<Sonner position="top-center" />
</SidebarProvider>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, computed, ref } from 'vue'
import { onMounted, onUnmounted, computed, ref, watch } from 'vue'
import { RouterView, RouterLink } from 'vue-router'
import { useGameStore } from '@/stores/gameStore'
import { useUniverseStore } from '@/stores/universeStore'
import { useNPCStore } from '@/stores/npcStore'
import { useTheme } from '@/composables/useTheme'
import { useI18n } from '@/composables/useI18n'
import { localeNames, detectBrowserLocale, type Locale } from '@/locales'
@@ -260,6 +423,7 @@
import { Badge } from '@/components/ui/badge'
import { Progress } from '@/components/ui/progress'
import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover'
import IncomingFleetAlerts from '@/components/IncomingFleetAlerts.vue'
import {
Sidebar,
SidebarContent,
@@ -275,8 +439,6 @@
SidebarTrigger
} from '@/components/ui/sidebar'
import ResourceIcon from '@/components/ResourceIcon.vue'
import DetailDialog from '@/components/DetailDialog.vue'
import Sonner from '@/components/ui/sonner/Sonner.vue'
import {
AlertDialog,
AlertDialogAction,
@@ -287,6 +449,12 @@
AlertDialogHeader,
AlertDialogTitle
} from '@/components/ui/alert-dialog'
import DetailDialog from '@/components/DetailDialog.vue'
import UpdateDialog from '@/components/UpdateDialog.vue'
import Sonner from '@/components/ui/sonner/Sonner.vue'
import { MissionType } from '@/types/game'
import type { BuildQueueItem, FleetMission, NPC, IncomingFleetAlert, MissileAttack } from '@/types/game'
import type { VersionInfo } from '@/utils/versionCheck'
import { formatNumber, formatTime, getResourceColor } from '@/utils/format'
import {
Moon,
@@ -304,62 +472,737 @@
Languages,
Settings,
Wrench,
ChevronsLeft
ChevronsLeft,
ChevronsUpDown,
ChevronDown,
ChevronUp,
Handshake
} from 'lucide-vue-next'
import * as gameLogic from '@/logic/gameLogic'
import * as planetLogic from '@/logic/planetLogic'
import * as officerLogic from '@/logic/officerLogic'
import * as buildingValidation from '@/logic/buildingValidation'
import * as resourceLogic from '@/logic/resourceLogic'
import { useGameLifecycle } from '@/composables/useGameLifecycle'
import { useMissionHandler } from '@/composables/useMissionHandler'
import { useNPCHandler } from '@/composables/useNPCHandler'
import { useQueueHandler } from '@/composables/useQueueHandler'
import { useGameUpdate } from '@/composables/useGameUpdate'
import { migrateGameData } from '@/utils/migration'
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 pkg from '../package.json'
import { toast } from 'vue-sonner'
import { migrateGameData } from '@/utils/migration'
import { checkLatestVersion } from '@/utils/versionCheck'
// 执行数据迁移(在 store 初始化之前)
migrateGameData()
const gameStore = useGameStore()
const universeStore = useUniverseStore()
const npcStore = useNPCStore()
const { isDark } = useTheme()
const { t } = useI18n()
// ConfirmDialog 状态
const confirmDialogOpen = ref(false)
const confirmDialogTitle = ref('')
const confirmDialogMessage = ref('')
const innerWidth = computed(() => window.innerWidth)
const confirmDialogAction = ref<(() => void) | null>(null)
// 更新弹窗状态
const showUpdateDialog = ref(false)
const updateInfo = ref<VersionInfo | null>(null)
const handleConfirmDialogConfirm = () => {
if (confirmDialogAction.value) {
confirmDialogAction.value()
}
confirmDialogOpen.value = false
}
// 所有可用的语言选项
const locales: Locale[] = ['zh-CN', 'zh-TW', 'en', 'de', 'ru', 'ko', 'ja']
// 侧边栏状态(不持久化,根据屏幕尺寸初始化)
// PC端≥1024px默认打开移动端默认关闭
const sidebarOpen = ref(window.innerWidth >= 1024)
// 初始化 composables
const { initGame } = useGameLifecycle()
// 移动端资源栏展开状态
const resourceBarExpanded = ref(false)
const { processMissionArrival, processMissionReturn } = useMissionHandler(t)
const initGame = async () => {
const shouldInit = gameLogic.shouldInitializeGame(gameStore.player.planets)
if (!shouldInit) {
const now = Date.now()
const { processNPCMissionArrival, processNPCMissionReturn, updateNPCGrowth, updateNPCBehavior } = useNPCHandler()
// 计算离线收益(直接同步计算)
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, now)
gameStore.player.planets.forEach(planet => {
resourceLogic.updatePlanetResources(planet, now, bonuses)
})
const { handleCancelBuild, handleCancelResearch, getItemName, getRemainingTime, getQueueProgress } = useQueueHandler(
t,
confirmDialogOpen,
confirmDialogTitle,
confirmDialogMessage,
confirmDialogAction
)
// 只在没有NPC星球时才生成首次加载已有玩家数据时
if (Object.keys(universeStore.planets).length === 0) {
generateNPCPlanets()
}
return
}
gameStore.player = gameLogic.initializePlayer(gameStore.player.id, t('common.playerName'))
const initialPlanet = planetLogic.createInitialPlanet(gameStore.player.id, t('planet.homePlanet'))
gameStore.player.planets = [initialPlanet]
gameStore.currentPlanetId = initialPlanet.id
// 新玩家初始化时生成NPC星球
generateNPCPlanets()
}
const { updateGame } = useGameUpdate(
processMissionArrival,
processMissionReturn,
processNPCMissionArrival,
processNPCMissionReturn,
updateNPCGrowth,
updateNPCBehavior
)
const generateNPCPlanets = () => {
const npcCount = 200
for (let i = 0; i < npcCount; i++) {
const position = gameLogic.generateRandomPosition()
const key = gameLogic.generatePositionKey(position.galaxy, position.system, position.position)
if (universeStore.planets[key]) continue
const npcPlanet = planetLogic.createNPCPlanet(i, position, t('planet.planetPrefix'))
universeStore.planets[key] = npcPlanet
}
}
const updateGame = async () => {
if (gameStore.isPaused) return
const now = Date.now()
gameStore.gameTime = now
// 检查军官过期
gameLogic.checkOfficersExpiration(gameStore.player.officers, now)
// 处理游戏更新(建造队列、研究队列等)
const result = gameLogic.processGameUpdate(gameStore.player, now)
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)
}
const processMissionArrival = async (mission: FleetMission) => {
// 从宇宙星球地图中查找目标星球
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]
// 获取起始星球名称(用于报告)
const originPlanet = gameStore.player.planets.find(p => p.id === mission.originPlanetId)
const originPlanetName = originPlanet?.name || t('fleetView.unknownPlanet')
if (mission.missionType === MissionType.Transport) {
const result = fleetLogic.processTransportArrival(mission, targetPlanet, gameStore.player, npcStore.npcs)
// 生成运输任务报告
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') : t('missionReports.transportFailed'),
details: {
transportedResources: mission.cargo
},
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)
// 检查是否攻击了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)
}
}
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 newPlanet = fleetLogic.processColonizeArrival(mission, targetPlanet, gameStore.player, t('planet.colonyPrefix'))
// 生成殖民任务报告
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: !!newPlanet,
message: newPlanet ? t('missionReports.colonizeSuccess') : t('missionReports.colonizeFailed'),
details: newPlanet
? {
newPlanetId: newPlanet.id,
newPlanetName: newPlanet.name
}
: undefined,
read: false
})
if (newPlanet) {
gameStore.player.planets.push(newPlanet)
}
} else if (mission.missionType === MissionType.Spy) {
const spyReport = fleetLogic.processSpyArrival(mission, targetPlanet, gameStore.player, null, npcStore.npcs)
if (spyReport) gameStore.player.spyReports.push(spyReport)
} else if (mission.missionType === MissionType.Deploy) {
const deployed = fleetLogic.processDeployArrival(mission, targetPlanet, gameStore.player.id)
// 生成部署任务报告
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,
message: deployed ? t('missionReports.deploySuccess') : t('missionReports.deployFailed'),
details: {
deployedFleet: mission.fleet
},
read: false
})
if (deployed) {
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)
// 生成回收任务报告
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,
message: recycleResult ? t('missionReports.recycleSuccess') : t('missionReports.recycleFailed'),
details: recycleResult
? {
recycledResources: recycleResult.collectedResources,
remainingDebris: recycleResult.remainingDebris || undefined
}
: undefined,
read: false
})
if (recycleResult && 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 = fleetLogic.processDestroyArrival(mission, targetPlanet, gameStore.player)
// 生成毁灭任务报告
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 || false,
message: destroyResult?.success ? t('missionReports.destroySuccess') : t('missionReports.destroyFailed'),
details: destroyResult?.success
? {
destroyedPlanetName:
targetPlanet?.name ||
`[${mission.targetPosition.galaxy}:${mission.targetPosition.system}:${mission.targetPosition.position}]`
}
: undefined,
read: false
})
if (destroyResult && destroyResult.success && destroyResult.planetId) {
// 星球被摧毁
// 从玩家星球列表中移除(如果是玩家的星球)
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]
}
}
}
}
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) {
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用于后续攻击决策
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) {
// 添加战斗报告给玩家
gameStore.player.battleReports.push(attackResult.battleResult)
// 如果生成月球,添加到玩家星球列表
if (attackResult.moon) {
gameStore.player.planets.push(attackResult.moon)
}
// 如果生成残骸场,添加到宇宙残骸场列表
if (attackResult.debrisField) {
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)
// 标记导弹攻击为已到达
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 removeIncomingFleetAlert = (alert: IncomingFleetAlert) => {
if (!gameStore.player.incomingFleetAlerts) return
const index = gameStore.player.incomingFleetAlerts.indexOf(alert)
if (index > -1) {
gameStore.player.incomingFleetAlerts.splice(index, 1)
}
}
const removeIncomingFleetAlertById = (missionId: string) => {
if (!gameStore.player.incomingFleetAlerts) return
const index = gameStore.player.incomingFleetAlerts.findIndex(a => a.id === missionId)
if (index > -1) {
gameStore.player.incomingFleetAlerts.splice(index, 1)
}
}
// NPC成长系统更新函数
let npcUpdateCounter = 0 // 累计秒数
const NPC_UPDATE_INTERVAL = 10 // 每10秒更新一次NPC减少性能开销
const updateNPCGrowth = (deltaSeconds: number) => {
// 累积时间
npcUpdateCounter += deltaSeconds
// 只在达到更新间隔时才执行
if (npcUpdateCounter < NPC_UPDATE_INTERVAL) {
return
}
// 获取所有星球
const allPlanets = Object.values(universeStore.planets)
// 如果NPC store为空从星球数据中初始化NPC
if (npcStore.npcs.length === 0) {
const npcMap = new Map<string, any>()
allPlanets.forEach(planet => {
// 跳过玩家的星球
if (planet.ownerId === gameStore.player.id || !planet.ownerId) return
// 这是NPC的星球
if (!npcMap.has(planet.ownerId)) {
npcMap.set(planet.ownerId, {
id: planet.ownerId,
name: `NPC-${planet.ownerId.substring(0, 8)}`,
planets: [],
technologies: {}, // 初始化空科技树
difficulty: 'medium' as const, // 默认中等难度
relations: {}, // 外交关系
allies: [], // 盟友列表
enemies: [] // 敌人列表
})
}
npcMap.get(planet.ownerId)!.planets.push(planet)
})
// 保存到store
npcStore.npcs = Array.from(npcMap.values())
// 如果有NPC基于玩家实力初始化NPC
if (npcStore.npcs.length > 0) {
const gameState: npcGrowthLogic.NPCGrowthGameState = {
planets: allPlanets,
player: gameStore.player,
npcs: npcStore.npcs
}
const playerPower = npcGrowthLogic.calculatePlayerAveragePower(gameState)
npcStore.npcs.forEach(npc => {
npcGrowthLogic.initializeNPCStartingPower(npc, playerPower)
})
// 初始化NPC之间的外交关系盟友/敌人)
npcGrowthLogic.initializeNPCDiplomacy(npcStore.npcs)
}
}
// 如果没有NPC直接返回
if (npcStore.npcs.length === 0) {
npcUpdateCounter = 0
return
}
// 构建游戏状态
const gameState: npcGrowthLogic.NPCGrowthGameState = {
planets: allPlanets,
player: gameStore.player,
npcs: npcStore.npcs
}
// 使用累积的时间更新每个NPC
npcStore.npcs.forEach(npc => {
npcGrowthLogic.updateNPCGrowth(npc, gameState, npcUpdateCounter)
})
// 重置计数器
npcUpdateCounter = 0
}
// NPC行为系统更新函数侦查和攻击决策
let npcBehaviorCounter = 0
const NPC_BEHAVIOR_INTERVAL = 5 // 每5秒检查一次NPC行为
const updateNPCBehavior = (deltaSeconds: number) => {
// 累积时间
npcBehaviorCounter += deltaSeconds
// 只在达到更新间隔时才执行
if (npcBehaviorCounter < NPC_BEHAVIOR_INTERVAL) {
return
}
// 如果没有NPC直接返回
if (npcStore.npcs.length === 0) {
npcBehaviorCounter = 0
return
}
const now = Date.now()
const allPlanets = Object.values(universeStore.planets)
// 更新每个NPC的行为
npcStore.npcs.forEach(npc => {
npcBehaviorLogic.updateNPCBehavior(npc, gameStore.player, allPlanets, universeStore.debrisFields, now)
})
npcBehaviorCounter = 0
}
// 游戏循环定时器
let gameLoop: ReturnType<typeof setInterval> | null = null
let konamiCleanup: (() => void) | null = null
let versionCheckInterval: ReturnType<typeof setInterval> | null = null
// 清理定时器
onUnmounted(() => {
if (gameLoop) clearInterval(gameLoop)
})
// 启动游戏循环
const startGameLoop = () => {
// 清理旧的定时器
if (gameLoop) {
clearInterval(gameLoop)
}
// 根据游戏速度计算间隔时间
const interval = 1000 / (gameStore.gameSpeed || 1)
// 启动新的游戏循环
gameLoop = setInterval(() => {
updateGame()
}, interval)
}
// 监听游戏速度变化,重新启动游戏循环
watch(
() => gameStore.gameSpeed,
() => {
if (gameLoop) {
startGameLoop()
}
}
)
// 初始化游戏
onMounted(async () => {
@@ -368,17 +1211,91 @@
if (isFirstVisit) {
gameStore.locale = detectBrowserLocale()
}
await initGame(t('common.playerName'), t('planet.homePlanet'), t('planet.planetPrefix'))
await initGame()
// 启动游戏循环
gameLoop = setInterval(() => {
updateGame()
}, 1000) // 每1秒更新一次
startGameLoop()
// 启动科乐美秘籍监听
konamiCleanup = setupKonamiCode()
// 首次检查版本(被动检测)
const versionInfo = await checkLatestVersion(gameStore.player.lastVersionCheckTime || 0, (time: number) => {
gameStore.player.lastVersionCheckTime = time
})
if (versionInfo) {
updateInfo.value = versionInfo
toast.info(t('settings.newVersionAvailable', { version: versionInfo.version }), {
duration: Infinity,
dismissible: true,
action: {
label: t('settings.viewUpdate'),
onClick: () => {
showUpdateDialog.value = true
}
}
})
}
// 启动版本检查定时器每5分钟被动检查一次
versionCheckInterval = setInterval(async () => {
const versionInfo = await checkLatestVersion(gameStore.player.lastVersionCheckTime || 0, (time: number) => {
gameStore.player.lastVersionCheckTime = time
})
if (versionInfo) {
updateInfo.value = versionInfo
toast.info(t('settings.newVersionAvailable', { version: versionInfo.version }), {
duration: Infinity,
dismissible: true,
action: {
label: t('settings.viewUpdate'),
onClick: () => {
showUpdateDialog.value = true
}
}
})
}
}, 5 * 60 * 1000)
})
// 清理定时器
onUnmounted(() => {
if (gameLoop) clearInterval(gameLoop)
if (konamiCleanup) konamiCleanup()
if (versionCheckInterval) clearInterval(versionCheckInterval)
})
// 科乐美秘籍上上下下左左右右BA
const setupKonamiCode = () => {
const konamiCode = ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown', 'ArrowLeft', 'ArrowLeft', 'ArrowRight', 'ArrowRight', 'b', 'a']
let konamiIndex = 0
const handleKeyDown = (event: KeyboardEvent) => {
// 如果已经激活GM模式直接返回
if (gameStore.player.isGMEnabled) return
const key = event.key.toLowerCase()
// 检查是否匹配当前秘籍序列
if (key === konamiCode[konamiIndex] || event.key === konamiCode[konamiIndex]) {
konamiIndex++
// 如果完成整个秘籍序列
if (konamiIndex === konamiCode.length) {
gameStore.player.isGMEnabled = true
// 显示成功消息
toast.success(t('common.gmModeActivated'))
konamiIndex = 0
}
} else {
// 如果按错了键,重置序列
konamiIndex = 0
}
}
window.addEventListener('keydown', handleKeyDown)
// 返回清理函数
return () => {
window.removeEventListener('keydown', handleKeyDown)
}
}
// 定义 planet computed需要在 watch 之前定义)
const planet = computed(() => gameStore.currentPlanet)
const navItems = [
const navItems = computed(() => [
{ name: computed(() => t('nav.overview')), path: '/', icon: Home },
{ name: computed(() => t('nav.buildings')), path: '/buildings', icon: Building2 },
{ name: computed(() => t('nav.research')), path: '/research', icon: FlaskConical },
@@ -388,11 +1305,12 @@
{ name: computed(() => t('nav.officers')), path: '/officers', icon: Users },
{ name: computed(() => t('nav.simulator')), path: '/battle-simulator', icon: Swords },
{ name: computed(() => t('nav.galaxy')), path: '/galaxy', icon: Globe },
{ name: computed(() => t('nav.diplomacy')), path: '/diplomacy', icon: Handshake },
{ name: computed(() => t('nav.messages')), path: '/messages', icon: Mail },
{ name: computed(() => t('nav.settings')), path: '/settings', icon: Settings },
// GM菜单仅在开发模式显示
...(import.meta.env.DEV ? [{ name: computed(() => t('nav.gm')), path: '/gm', icon: Wrench }] : [])
]
// GM菜单在启用GM模式显示
...(gameStore.player.isGMEnabled ? [{ name: computed(() => t('nav.gm')), path: '/gm', icon: Wrench }] : [])
])
// 使用直接计算,不再缓存
const production = computed(() => {
@@ -413,11 +1331,35 @@
return resourceLogic.calculateResourceCapacity(planet.value, bonuses.storageCapacityBonus)
})
// 电力消耗
const energyConsumption = computed(() => {
if (!planet.value) return 0
return resourceLogic.calculateEnergyConsumption(planet.value)
})
// 净电力(产量 - 消耗)
const netEnergy = computed(() => {
if (!planet.value || !production.value) return 0
return production.value.energy - energyConsumption.value
})
// 未读消息数量
const unreadMessagesCount = computed(() => {
const unreadBattles = gameStore.player.battleReports.filter(r => !r.read).length
const unreadSpies = gameStore.player.spyReports.filter(r => !r.read).length
return unreadBattles + unreadSpies
const unreadSpied = gameStore.player.spiedNotifications?.filter(n => !n.read).length || 0
const unreadMissions = gameStore.player.missionReports?.filter(r => !r.read).length || 0
const unreadNPCActivity = gameStore.player.npcActivityNotifications?.filter(n => !n.read).length || 0
const unreadGifts = gameStore.player.giftNotifications?.filter(n => !n.read).length || 0
const unreadGiftRejected = gameStore.player.giftRejectedNotifications?.filter(n => !n.read).length || 0
return unreadBattles + unreadSpies + unreadSpied + unreadMissions + unreadNPCActivity + unreadGifts + unreadGiftRejected
})
// 正在执行的舰队任务数量(包括飞行中的导弹)
const activeFleetMissionsCount = computed(() => {
const fleetMissions = gameStore.player.fleetMissions.filter(m => m.status === 'outbound' || m.status === 'returning').length
const flyingMissiles = gameStore.player.missileAttacks?.filter(m => m.status === 'flying').length || 0
return fleetMissions + flyingMissiles
})
// 资源类型配置
@@ -450,17 +1392,79 @@
}
}
// 切换到指定星球
const switchToPlanet = (planetId: string) => {
gameStore.currentPlanetId = planetId
}
// 切换侧边栏
const toggleSidebar = () => {
sidebarOpen.value = !sidebarOpen.value
}
// 处理确认对话框的确认操作
const handleConfirmAction = () => {
if (confirmDialogAction.value) {
confirmDialogAction.value()
// 获取队列项的名称
const getItemName = (item: BuildQueueItem): string => {
if (item.type === 'building' || item.type === 'demolish') {
const buildingName = t(`buildings.${item.itemType}`)
return item.type === 'demolish' ? `${t('buildingsView.demolish')} - ${buildingName}` : buildingName
} else if (item.type === 'technology') {
return t(`technologies.${item.itemType}`)
} else if (item.type === 'ship') {
return t(`ships.${item.itemType}`)
} else if (item.type === 'defense') {
return t(`defenses.${item.itemType}`)
}
confirmDialogOpen.value = false
return item.itemType
}
// 获取剩余时间
const getRemainingTime = (item: BuildQueueItem): number => {
const now = Date.now()
return Math.max(0, Math.floor((item.endTime - now) / 1000))
}
// 获取队列进度
const getQueueProgress = (item: BuildQueueItem): number => {
const now = Date.now()
const total = item.endTime - item.startTime
const elapsed = now - item.startTime
return Math.min(100, Math.max(0, (elapsed / total) * 100))
}
// 取消建造
const handleCancelBuild = (queueId: string) => {
confirmDialogTitle.value = t('queue.cancelBuild')
confirmDialogMessage.value = t('queue.confirmCancel')
confirmDialogAction.value = () => {
if (!gameStore.currentPlanet) return false
const { item, index } = buildingValidation.findQueueItem(gameStore.currentPlanet.buildQueue, queueId)
if (!item) return false
if (item.type === 'building') {
const refund = buildingValidation.cancelBuildingUpgrade(gameStore.currentPlanet, item)
resourceLogic.addResources(gameStore.currentPlanet.resources, refund)
}
gameStore.currentPlanet.buildQueue.splice(index, 1)
return true
}
confirmDialogOpen.value = true
}
// 取消研究
const handleCancelResearch = (queueId: string) => {
confirmDialogTitle.value = t('queue.cancelResearch')
confirmDialogMessage.value = t('queue.confirmCancel')
confirmDialogAction.value = () => {
if (!gameStore.currentPlanet) return false
const { item, index } = buildingValidation.findQueueItem(gameStore.player.researchQueue, queueId)
if (!item) return false
if (item.type === 'technology') {
const refund = researchValidation.cancelTechnologyResearch(item)
resourceLogic.addResources(gameStore.currentPlanet.resources, refund)
}
gameStore.player.researchQueue.splice(index, 1)
return true
}
confirmDialogOpen.value = true
}
</script>

View File

@@ -15,23 +15,37 @@
<TableHead v-if="type === 'building' && showProductionColumn" class="text-center">{{ t('buildings.production') }}</TableHead>
<TableHead v-if="type === 'building' && showConsumptionColumn" class="text-center">{{ t('buildings.consumption') }}</TableHead>
<TableHead v-if="type === 'building' && showCapacityColumn" class="text-center">{{ t('buildings.storageCapacity') }}</TableHead>
<TableHead v-if="type === 'building' && showFleetStorageColumn" class="text-center">{{ t('buildings.fleetStorage') }}</TableHead>
<TableHead v-if="type === 'building' && showBuildQueueColumn" class="text-center">{{ t('buildings.buildQueueBonus') }}</TableHead>
<TableHead v-if="type === 'building' && showFleetStorageColumn" class="text-center">
{{ t('buildings.fleetStorage') }}
</TableHead>
<TableHead v-if="type === 'building' && showBuildQueueColumn" class="text-center">
{{ t('buildings.buildQueueBonus') }}
</TableHead>
<TableHead v-if="type === 'building' && showSpaceColumn" class="text-center">{{ t('buildings.spaceBonus') }}</TableHead>
<TableHead v-if="type === 'building' && showMissileColumn" class="text-center">{{ t('buildings.missileCapacity') }}</TableHead>
<TableHead v-if="type === 'building' && showBuildSpeedColumn" class="text-center">{{ t('buildings.buildSpeedBonus') }}</TableHead>
<TableHead v-if="type === 'building' && showResearchSpeedColumn" class="text-center">{{ t('buildings.researchSpeedBonus') }}</TableHead>
<TableHead v-if="type === 'building' && showBuildSpeedColumn" class="text-center">
{{ t('buildings.buildSpeedBonus') }}
</TableHead>
<TableHead v-if="type === 'building' && showResearchSpeedColumn" class="text-center">
{{ t('buildings.researchSpeedBonus') }}
</TableHead>
<!-- 科技相关列 -->
<TableHead v-if="type === 'technology' && showAttackBonusColumn" class="text-center">{{ t('research.attackBonus') }}</TableHead>
<TableHead v-if="type === 'technology' && showShieldBonusColumn" class="text-center">{{ t('research.shieldBonus') }}</TableHead>
<TableHead v-if="type === 'technology' && showArmorBonusColumn" class="text-center">{{ t('research.armorBonus') }}</TableHead>
<TableHead v-if="type === 'technology' && showSpyLevelColumn" class="text-center">{{ t('research.spyLevel') }}</TableHead>
<TableHead v-if="type === 'technology' && showFleetStorageColumn" class="text-center">{{ t('buildings.fleetStorage') }}</TableHead>
<TableHead v-if="type === 'technology' && showResearchQueueColumn" class="text-center">{{ t('research.researchQueueBonus') }}</TableHead>
<TableHead v-if="type === 'technology' && showFleetStorageColumn" class="text-center">
{{ t('buildings.fleetStorage') }}
</TableHead>
<TableHead v-if="type === 'technology' && showResearchQueueColumn" class="text-center">
{{ t('research.researchQueueBonus') }}
</TableHead>
<TableHead v-if="type === 'technology' && showColonySlotsColumn" class="text-center">{{ t('research.colonySlots') }}</TableHead>
<TableHead v-if="type === 'technology' && showSpaceColumn" class="text-center">{{ t('buildings.spaceBonus') }}</TableHead>
<TableHead v-if="type === 'technology' && showSpeedBonusColumn" class="text-center">{{ t('research.speedBonus') }}</TableHead>
<TableHead v-if="type === 'technology' && showResearchSpeedColumn" class="text-center">{{ t('buildings.researchSpeedBonus') }}</TableHead>
<TableHead v-if="type === 'technology' && showResearchSpeedColumn" class="text-center">
{{ t('buildings.researchSpeedBonus') }}
</TableHead>
<TableHead class="text-center">{{ t('player.points') }}</TableHead>
</TableRow>
</TableHeader>
@@ -78,7 +92,8 @@
</TableCell>
<TableCell v-if="type === 'building' && showFleetStorageColumn" class="text-center text-sm">
<span v-if="getLevelData(level).fleetStorage > 0" class="text-blue-600 dark:text-blue-400">
+<NumberWithTooltip :value="getLevelData(level).fleetStorage" />
+
<NumberWithTooltip :value="getLevelData(level).fleetStorage" />
</span>
<span v-else>-</span>
</TableCell>
@@ -87,7 +102,8 @@
</TableCell>
<TableCell v-if="type === 'building' && showSpaceColumn" class="text-center text-sm">
<span v-if="getLevelData(level).spaceBonus > 0" class="text-green-600 dark:text-green-400">
+<NumberWithTooltip :value="getLevelData(level).spaceBonus" />
+
<NumberWithTooltip :value="getLevelData(level).spaceBonus" />
</span>
<span v-else>-</span>
</TableCell>
@@ -95,8 +111,12 @@
<span class="text-orange-600 dark:text-orange-400">+10</span>
</TableCell>
<TableCell v-if="type === 'building' && showBuildSpeedColumn" class="text-center text-sm">
<span v-if="itemType === 'roboticsFactory'" class="text-cyan-600 dark:text-cyan-400">+{{ getLevelData(level).buildSpeedBonus * 100 }}%</span>
<span v-else-if="itemType === 'naniteFactory'" class="text-cyan-600 dark:text-cyan-400">+{{ getLevelData(level).buildSpeedBonus * 100 }}%</span>
<span v-if="itemType === 'roboticsFactory'" class="text-cyan-600 dark:text-cyan-400">
+{{ getLevelData(level).buildSpeedBonus * 100 }}%
</span>
<span v-else-if="itemType === 'naniteFactory'" class="text-cyan-600 dark:text-cyan-400">
+{{ getLevelData(level).buildSpeedBonus * 100 }}%
</span>
</TableCell>
<TableCell v-if="type === 'building' && showResearchSpeedColumn" class="text-center text-sm">
<span class="text-indigo-600 dark:text-indigo-400">+{{ (getLevelData(level).researchSpeedBonus - 1) * 100 }}%</span>
@@ -115,7 +135,10 @@
<span class="text-purple-600 dark:text-purple-400">+{{ level }}</span>
</TableCell>
<TableCell v-if="type === 'technology' && showFleetStorageColumn" class="text-center text-sm">
<span class="text-blue-600 dark:text-blue-400">+<NumberWithTooltip :value="level * 500" /></span>
<span class="text-blue-600 dark:text-blue-400">
+
<NumberWithTooltip :value="level * 500" />
</span>
</TableCell>
<TableCell v-if="type === 'technology' && showResearchQueueColumn" class="text-center text-sm">
<span class="text-purple-600 dark:text-purple-400">+1</span>
@@ -124,7 +147,7 @@
<span class="text-green-600 dark:text-green-400">+1</span>
</TableCell>
<TableCell v-if="type === 'technology' && showSpaceColumn" class="text-center text-sm">
<span class="text-green-600 dark:text-green-400">+5 {{ t('research.forAllPlanets') }}</span>
<span class="text-green-600 dark:text-green-400">+30 {{ t('research.forAllPlanets') }}</span>
</TableCell>
<TableCell v-if="type === 'technology' && showSpeedBonusColumn" class="text-center text-sm">
<span class="text-yellow-600 dark:text-yellow-400">+{{ level * 10 }}%</span>
@@ -275,7 +298,12 @@
<CardTitle class="text-sm">{{ t(`${typeKey}.buildCost`) }}</CardTitle>
</CardHeader>
<CardContent class="space-y-2">
<div v-for="resourceType in costResourceTypes" :key="resourceType.key" v-show="unitCost[resourceType.key] > 0" class="flex items-center justify-between text-sm">
<div
v-for="resourceType in costResourceTypes"
:key="resourceType.key"
v-show="unitCost[resourceType.key] > 0"
class="flex items-center justify-between text-sm"
>
<span class="text-muted-foreground">{{ t(`resources.${resourceType.key}`) }}:</span>
<span class="font-medium"><NumberWithTooltip :value="unitCost[resourceType.key]" /></span>
</div>
@@ -311,9 +339,17 @@
<div class="space-y-2">
<p class="text-sm text-muted-foreground">{{ t(`${typeKey}.totalCost`) }}:</p>
<div class="space-y-1 text-sm">
<div v-for="resourceType in costResourceTypes" :key="resourceType.key" class="flex justify-between">
<span>{{ t(`resources.${resourceType.key}`) }}:</span>
<span class="font-medium"><NumberWithTooltip :value="batchCost[resourceType.key]" /></span>
<div class="flex justify-between">
<span>{{ t('resources.metal') }}:</span>
<span class="font-medium"><NumberWithTooltip :value="batchCost.metal" /></span>
</div>
<div class="flex justify-between">
<span>{{ t('resources.crystal') }}:</span>
<span class="font-medium"><NumberWithTooltip :value="batchCost.crystal" /></span>
</div>
<div class="flex justify-between">
<span>{{ t('resources.deuterium') }}:</span>
<span class="font-medium"><NumberWithTooltip :value="batchCost.deuterium" /></span>
</div>
</div>
</div>
@@ -654,9 +690,9 @@
} else if (buildingType === 'shipyard') {
fleetStorage = 1000 * level
} else if (buildingType === 'terraformer') {
spaceBonus = 5
spaceBonus = 30
} else if (buildingType === 'lunarBase') {
spaceBonus = 5
spaceBonus = 30
} else if (buildingType === 'roboticsFactory') {
buildSpeedBonus = level
} else if (buildingType === 'naniteFactory') {
@@ -685,7 +721,18 @@
}
const points = pointsLogic.calculateTechnologyPoints(techType, level - 1, level)
return { cost, time, production: 0, consumption: 0, points, capacity: 0, fleetStorage: 0, spaceBonus: 0, buildSpeedBonus: 0, researchSpeedBonus }
return {
cost,
time,
production: 0,
consumption: 0,
points,
capacity: 0,
fleetStorage: 0,
spaceBonus: 0,
buildSpeedBonus: 0,
researchSpeedBonus
}
}
}

View File

@@ -0,0 +1,120 @@
<template>
<Dialog :open="open" @update:open="$emit('update:open', $event)">
<DialogScrollContent class="max-w-2xl max-h-[80vh] flex flex-col">
<DialogHeader class="flex-shrink-0">
<DialogTitle>{{ t('settings.newVersionAvailable', { version: versionInfo?.version || '' }) }}</DialogTitle>
<DialogDescription>{{ t('settings.updateAvailable') }}</DialogDescription>
</DialogHeader>
<div class="flex-1 overflow-y-auto min-h-0 mt-4 pr-2">
<div class="prose prose-sm dark:prose-invert max-w-none" v-html="renderedMarkdown"></div>
</div>
<DialogFooter class="flex gap-2 flex-shrink-0 mt-4">
<Button variant="outline" @click="$emit('update:open', false)">
{{ t('common.cancel') }}
</Button>
<Button @click="handleDownload">
<Download class="mr-2 h-4 w-4" />
{{ t('settings.download') }}
</Button>
</DialogFooter>
</DialogScrollContent>
</Dialog>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { marked } from 'marked'
import { useI18n } from '@/composables/useI18n'
import { Dialog, DialogScrollContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Download } from 'lucide-vue-next'
import type { VersionInfo } from '@/utils/versionCheck'
const props = defineProps<{
open: boolean
versionInfo: VersionInfo | null
}>()
defineEmits<{
'update:open': [value: boolean]
}>()
const { t } = useI18n()
const renderedMarkdown = computed(() => {
if (!props.versionInfo?.releaseNotes) return ''
return marked(props.versionInfo.releaseNotes)
})
const handleDownload = () => {
if (props.versionInfo?.downloadUrl) {
window.open(props.versionInfo.downloadUrl, '_blank')
}
}
</script>
<style scoped>
:deep(.prose) {
color: hsl(var(--foreground));
}
:deep(.prose h1) {
font-size: 1.5em;
font-weight: 700;
margin-top: 1em;
margin-bottom: 0.5em;
}
:deep(.prose h2) {
font-size: 1.25em;
font-weight: 600;
margin-top: 0.8em;
margin-bottom: 0.4em;
}
:deep(.prose h3) {
font-size: 1.1em;
font-weight: 600;
margin-top: 0.6em;
margin-bottom: 0.3em;
}
:deep(.prose p) {
margin-top: 0.5em;
margin-bottom: 0.5em;
}
:deep(.prose ul) {
margin-top: 0.5em;
margin-bottom: 0.5em;
padding-left: 1.5em;
}
:deep(.prose li) {
margin-top: 0.25em;
margin-bottom: 0.25em;
}
:deep(.prose code) {
background: hsl(var(--muted));
padding: 0.2em 0.4em;
border-radius: 0.25rem;
font-size: 0.875em;
}
:deep(.prose pre) {
background: hsl(var(--muted));
padding: 1em;
border-radius: 0.5rem;
overflow-x: auto;
margin-top: 0.5em;
margin-bottom: 0.5em;
}
:deep(.prose a) {
color: hsl(var(--primary));
text-decoration: underline;
}
</style>

View File

@@ -6,7 +6,7 @@
v-bind="{ ...$attrs, ...forwarded }"
:class="
cn(
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-2xl',
props.class
)
"

View File

@@ -6,7 +6,7 @@
v-bind="{ ...$attrs, ...forwarded }"
:class="
cn(
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 w-[calc(100vw-3rem)] translate-x-[-50%] translate-y-[-50%] rounded-lg border shadow-lg duration-200 sm:w-auto flex flex-col p-0',
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 w-[calc(100vw-3rem)] translate-x-[-50%] translate-y-[-50%] rounded-lg border shadow-lg duration-200 sm:w-auto sm:min-w-[764px] flex flex-col p-0',
containerClass
)
"

View File

@@ -0,0 +1,22 @@
<template>
<div
data-slot="empty"
:class="
cn(
'flex min-w-0 flex-1 flex-col items-center justify-center gap-6 text-balance rounded-lg border-dashed p-6 text-center md:p-12',
props.class
)
"
>
<slot />
</div>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>

View File

@@ -0,0 +1,14 @@
<template>
<div data-slot="empty-content" :class="cn('flex w-full min-w-0 max-w-sm flex-col items-center gap-4 text-balance text-sm', props.class)">
<slot />
</div>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>

View File

@@ -0,0 +1,19 @@
<template>
<p
data-slot="empty-description"
:class="
cn('text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4', $attrs.class ?? '')
"
>
<slot />
</p>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
defineProps<{
class?: HTMLAttributes['class']
}>()
</script>

View File

@@ -0,0 +1,14 @@
<template>
<div data-slot="empty-header" :class="cn('flex max-w-sm flex-col items-center gap-2 text-center', props.class)">
<slot />
</div>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>

View File

@@ -0,0 +1,17 @@
<template>
<div data-slot="empty-icon" :data-variant="variant" :class="cn(emptyMediaVariants({ variant }), props.class)">
<slot />
</div>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import type { EmptyMediaVariants } from '.'
import { cn } from '@/lib/utils'
import { emptyMediaVariants } from '.'
const props = defineProps<{
class?: HTMLAttributes['class']
variant?: EmptyMediaVariants['variant']
}>()
</script>

View File

@@ -0,0 +1,14 @@
<template>
<div data-slot="empty-title" :class="cn('text-lg font-medium tracking-tight', props.class)">
<slot />
</div>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>

View File

@@ -0,0 +1,23 @@
import type { VariantProps } from 'class-variance-authority'
import { cva } from 'class-variance-authority'
export { default as Empty } from './Empty.vue'
export { default as EmptyContent } from './EmptyContent.vue'
export { default as EmptyDescription } from './EmptyDescription.vue'
export { default as EmptyHeader } from './EmptyHeader.vue'
export { default as EmptyMedia } from './EmptyMedia.vue'
export { default as EmptyTitle } from './EmptyTitle.vue'
export const emptyMediaVariants = cva('mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0', {
variants: {
variant: {
default: 'bg-transparent',
icon: "bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6"
}
},
defaultVariants: {
variant: 'default'
}
})
export type EmptyMediaVariants = VariantProps<typeof emptyMediaVariants>

View File

@@ -0,0 +1,25 @@
<template>
<ScrollAreaRoot data-slot="scroll-area" v-bind="delegatedProps" :class="cn('relative', props.class)">
<ScrollAreaViewport
data-slot="scroll-area-viewport"
class="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
>
<slot />
</ScrollAreaViewport>
<ScrollBar />
<ScrollAreaCorner />
</ScrollAreaRoot>
</template>
<script setup lang="ts">
import type { ScrollAreaRootProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { ScrollAreaCorner, ScrollAreaRoot, ScrollAreaViewport } from 'reka-ui'
import { cn } from '@/lib/utils'
import ScrollBar from './ScrollBar.vue'
const props = defineProps<ScrollAreaRootProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
</script>

View File

@@ -0,0 +1,30 @@
<template>
<ScrollAreaScrollbar
data-slot="scroll-area-scrollbar"
v-bind="delegatedProps"
:class="
cn(
'flex touch-none p-px transition-colors select-none',
orientation === 'vertical' && 'h-full w-2.5 border-l border-l-transparent',
orientation === 'horizontal' && 'h-2.5 flex-col border-t border-t-transparent',
props.class
)
"
>
<ScrollAreaThumb data-slot="scroll-area-thumb" class="bg-border relative flex-1 rounded-full" />
</ScrollAreaScrollbar>
</template>
<script setup lang="ts">
import type { ScrollAreaScrollbarProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { ScrollAreaScrollbar, ScrollAreaThumb } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = withDefaults(defineProps<ScrollAreaScrollbarProps & { class?: HTMLAttributes['class'] }>(), {
orientation: 'vertical'
})
const delegatedProps = reactiveOmit(props, 'class')
</script>

View File

@@ -0,0 +1,2 @@
export { default as ScrollArea } from './ScrollArea.vue'
export { default as ScrollBar } from './ScrollBar.vue'

View File

@@ -37,6 +37,7 @@
import { CircleCheckIcon, InfoIcon, Loader2Icon, OctagonXIcon, TriangleAlertIcon, XIcon } from 'lucide-vue-next'
import { Toaster as Sonner } from 'vue-sonner'
import { cn } from '@/lib/utils'
import 'vue-sonner/style.css'
const props = defineProps<ToasterProps>()
</script>

View File

@@ -1,63 +0,0 @@
import { useGameStore } from '@/stores/gameStore'
import { useUniverseStore } from '@/stores/universeStore'
import * as gameLogic from '@/logic/gameLogic'
import * as planetLogic from '@/logic/planetLogic'
import * as resourceLogic from '@/logic/resourceLogic'
import * as officerLogic from '@/logic/officerLogic'
/**
* 游戏生命周期管理
* 处理游戏初始化、NPC星球生成等
*/
export const useGameLifecycle = () => {
const gameStore = useGameStore()
const universeStore = useUniverseStore()
/**
* 生成NPC星球
*/
const generateNPCPlanets = (npcCount: number, planetPrefix: string) => {
for (let i = 0; i < npcCount; i++) {
const position = gameLogic.generateRandomPosition()
const key = gameLogic.generatePositionKey(position.galaxy, position.system, position.position)
if (universeStore.planets[key]) continue
const npcPlanet = planetLogic.createNPCPlanet(i, position, planetPrefix)
universeStore.planets[key] = npcPlanet
}
}
/**
* 初始化游戏
*/
const initGame = async (playerName: string, homePlanetName: string, planetPrefix: string) => {
const shouldInit = gameLogic.shouldInitializeGame(gameStore.player.planets)
if (!shouldInit) {
const now = Date.now()
// 计算离线收益(直接同步计算)
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, now)
gameStore.player.planets.forEach(planet => {
resourceLogic.updatePlanetResources(planet, now, bonuses)
})
// 只在没有NPC星球时才生成首次加载已有玩家数据时
if (Object.keys(universeStore.planets).length === 0) {
generateNPCPlanets(200, planetPrefix)
}
return
}
gameStore.player = gameLogic.initializePlayer(gameStore.player.id, playerName)
const initialPlanet = planetLogic.createInitialPlanet(gameStore.player.id, homePlanetName)
gameStore.player.planets = [initialPlanet]
gameStore.currentPlanetId = initialPlanet.id
// 新玩家初始化时生成NPC星球
generateNPCPlanets(200, planetPrefix)
}
return {
initGame,
generateNPCPlanets
}
}

View File

@@ -1,68 +0,0 @@
import { useGameStore } from '@/stores/gameStore'
import { useNPCStore } from '@/stores/npcStore'
import type { FleetMission } from '@/types/game'
import * as gameLogic from '@/logic/gameLogic'
/**
* 游戏更新循环
* 处理游戏状态的定期更新
*/
export const useGameUpdate = (
processMissionArrival: (mission: FleetMission) => Promise<void>,
processMissionReturn: (mission: FleetMission) => void,
processNPCMissionArrival: (npc: any, mission: FleetMission) => void,
processNPCMissionReturn: (npc: any, mission: FleetMission) => void,
updateNPCGrowth: (deltaSeconds: number) => void,
updateNPCBehavior: (deltaSeconds: number) => void
) => {
const gameStore = useGameStore()
const npcStore = useNPCStore()
/**
* 游戏主更新函数
*/
const updateGame = () => {
if (gameStore.isPaused) return
const now = Date.now()
gameStore.gameTime = now
// 检查军官过期
gameLogic.checkOfficersExpiration(gameStore.player.officers, now)
// 处理游戏更新(建造队列、研究队列等)
const result = gameLogic.processGameUpdate(gameStore.player, now)
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)
}
})
// 处理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) // 传入1秒的时间间隔
// NPC行为系统更新侦查和攻击决策
updateNPCBehavior(1)
}
return {
updateGame
}
}

View File

@@ -10,7 +10,7 @@ export const useI18n = () => {
const messages = computed(() => locales[currentLocale.value])
// 获取翻译文本的辅助函数
const t = (key: string): string => {
const t = (key: string, params?: Record<string, string | number>): string => {
const keys = key.split('.')
let value: any = messages.value
@@ -22,7 +22,16 @@ export const useI18n = () => {
}
}
return typeof value === 'string' ? value : key
let result = typeof value === 'string' ? value : key
// 替换参数占位符
if (params) {
Object.entries(params).forEach(([paramKey, paramValue]) => {
result = result.replace(new RegExp(`\\{${paramKey}\\}`, 'g'), String(paramValue))
})
}
return result
}
const setLocale = (locale: Locale) => {

View File

@@ -1,249 +0,0 @@
import { useGameStore } from '@/stores/gameStore'
import { useUniverseStore } from '@/stores/universeStore'
import { useNPCStore } from '@/stores/npcStore'
import type { FleetMission } from '@/types/game'
import { MissionType } from '@/types/game'
import * as gameLogic from '@/logic/gameLogic'
import * as fleetLogic from '@/logic/fleetLogic'
import * as shipLogic from '@/logic/shipLogic'
import * as resourceLogic from '@/logic/resourceLogic'
import * as diplomaticLogic from '@/logic/diplomaticLogic'
/**
* 舰队任务处理
* 处理玩家舰队任务的到达和返回
*/
export const useMissionHandler = (t: (key: string) => string) => {
const gameStore = useGameStore()
const universeStore = useUniverseStore()
const npcStore = useNPCStore()
/**
* 处理任务到达
*/
const processMissionArrival = async (mission: FleetMission) => {
// 从宇宙星球地图中查找目标星球
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]
// 获取起始星球名称(用于报告)
const originPlanet = gameStore.player.planets.find(p => p.id === mission.originPlanetId)
const originPlanetName = originPlanet?.name || t('fleetView.unknownPlanet')
if (mission.missionType === MissionType.Transport) {
const result = fleetLogic.processTransportArrival(mission, targetPlanet, gameStore.player, npcStore.npcs)
// 生成运输任务报告
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') : t('missionReports.transportFailed'),
details: {
transportedResources: mission.cargo
},
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)
// 检查是否攻击了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)
}
}
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 newPlanet = fleetLogic.processColonizeArrival(mission, targetPlanet, gameStore.player, t('planet.colonyPrefix'))
// 生成殖民任务报告
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: !!newPlanet,
message: newPlanet ? t('missionReports.colonizeSuccess') : t('missionReports.colonizeFailed'),
details: newPlanet
? {
newPlanetId: newPlanet.id,
newPlanetName: newPlanet.name
}
: undefined,
read: false
})
if (newPlanet) {
gameStore.player.planets.push(newPlanet)
}
} else if (mission.missionType === MissionType.Spy) {
const spyReport = fleetLogic.processSpyArrival(mission, targetPlanet, gameStore.player, null, npcStore.npcs)
if (spyReport) gameStore.player.spyReports.push(spyReport)
} else if (mission.missionType === MissionType.Deploy) {
const deployed = fleetLogic.processDeployArrival(mission, targetPlanet, gameStore.player.id)
// 生成部署任务报告
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,
message: deployed ? t('missionReports.deploySuccess') : t('missionReports.deployFailed'),
details: {
deployedFleet: mission.fleet
},
read: false
})
if (deployed) {
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)
// 生成回收任务报告
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,
message: recycleResult ? t('missionReports.recycleSuccess') : t('missionReports.recycleFailed'),
details: recycleResult
? {
recycledResources: recycleResult.collectedResources,
remainingDebris: recycleResult.remainingDebris || undefined
}
: undefined,
read: false
})
if (recycleResult && 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 = fleetLogic.processDestroyArrival(mission, targetPlanet, gameStore.player)
// 生成毁灭任务报告
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 || false,
message: destroyResult?.success ? t('missionReports.destroySuccess') : t('missionReports.destroyFailed'),
details: destroyResult?.success
? {
destroyedPlanetName:
targetPlanet?.name ||
`[${mission.targetPosition.galaxy}:${mission.targetPosition.system}:${mission.targetPosition.position}]`
}
: undefined,
read: false
})
if (destroyResult && destroyResult.success && destroyResult.planetId) {
// 星球被摧毁
// 从玩家星球列表中移除(如果是玩家的星球)
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]
}
}
}
}
/**
* 处理任务返回
*/
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)
}
return {
processMissionArrival,
processMissionReturn
}
}

View File

@@ -1,300 +0,0 @@
import { useGameStore } from '@/stores/gameStore'
import { useUniverseStore } from '@/stores/universeStore'
import { useNPCStore } from '@/stores/npcStore'
import type { NPC, FleetMission, IncomingFleetAlert } from '@/types/game'
import { MissionType } from '@/types/game'
import * as gameLogic from '@/logic/gameLogic'
import * as fleetLogic from '@/logic/fleetLogic'
import * as shipLogic from '@/logic/shipLogic'
import * as npcGrowthLogic from '@/logic/npcGrowthLogic'
import * as npcBehaviorLogic from '@/logic/npcBehaviorLogic'
/**
* NPC处理
* 处理NPC舰队任务、成长系统、行为系统
*/
export const useNPCHandler = () => {
const gameStore = useGameStore()
const universeStore = useUniverseStore()
const npcStore = useNPCStore()
/**
* 移除即将到来的舰队警告
*/
const removeIncomingFleetAlert = (alert: IncomingFleetAlert) => {
if (!gameStore.player.incomingFleetAlerts) return
const index = gameStore.player.incomingFleetAlerts.indexOf(alert)
if (index > -1) {
gameStore.player.incomingFleetAlerts.splice(index, 1)
}
}
/**
* 根据任务ID移除即将到来的舰队警告
*/
const removeIncomingFleetAlertById = (missionId: string) => {
if (!gameStore.player.incomingFleetAlerts) return
const index = gameStore.player.incomingFleetAlerts.findIndex(a => a.id === missionId)
if (index > -1) {
gameStore.player.incomingFleetAlerts.splice(index, 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) {
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用于后续攻击决策
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) {
// 添加战斗报告给玩家
gameStore.player.battleReports.push(attackResult.battleResult)
// 如果生成月球,添加到玩家星球列表
if (attackResult.moon) {
gameStore.player.planets.push(attackResult.moon)
}
// 如果生成残骸场,添加到宇宙残骸场列表
if (attackResult.debrisField) {
universeStore.debrisFields[attackResult.debrisField.id] = attackResult.debrisField
}
}
// 移除即将到来的警告(攻击已到达)
removeIncomingFleetAlertById(mission.id)
})
}
}
/**
* 处理NPC任务返回
*/
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)
}
}
}
// NPC成长系统更新
let npcUpdateCounter = 0
const NPC_UPDATE_INTERVAL = 10
/**
* 更新NPC成长系统
*/
const updateNPCGrowth = (deltaSeconds: number) => {
// 累积时间
npcUpdateCounter += deltaSeconds
// 只在达到更新间隔时才执行
if (npcUpdateCounter < NPC_UPDATE_INTERVAL) {
return
}
// 获取所有星球
const allPlanets = Object.values(universeStore.planets)
// 如果NPC store为空从星球数据中初始化NPC
if (npcStore.npcs.length === 0) {
const npcMap = new Map<string, any>()
allPlanets.forEach(planet => {
// 跳过玩家的星球
if (planet.ownerId === gameStore.player.id || !planet.ownerId) return
// 这是NPC的星球
if (!npcMap.has(planet.ownerId)) {
npcMap.set(planet.ownerId, {
id: planet.ownerId,
name: `NPC-${planet.ownerId.substring(0, 8)}`,
planets: [],
technologies: {},
difficulty: 'medium' as const,
relations: {},
allies: [],
enemies: []
})
}
npcMap.get(planet.ownerId)!.planets.push(planet)
})
// 保存到store
npcStore.npcs = Array.from(npcMap.values())
// 如果有NPC基于玩家实力初始化NPC
if (npcStore.npcs.length > 0) {
const gameState: npcGrowthLogic.NPCGrowthGameState = {
planets: allPlanets,
player: gameStore.player,
npcs: npcStore.npcs
}
const playerPower = npcGrowthLogic.calculatePlayerAveragePower(gameState)
npcStore.npcs.forEach(npc => {
npcGrowthLogic.initializeNPCStartingPower(npc, playerPower)
})
// 初始化NPC之间的外交关系盟友/敌人)
npcGrowthLogic.initializeNPCDiplomacy(npcStore.npcs)
}
}
// 如果没有NPC直接返回
if (npcStore.npcs.length === 0) {
npcUpdateCounter = 0
return
}
// 构建游戏状态
const gameState: npcGrowthLogic.NPCGrowthGameState = {
planets: allPlanets,
player: gameStore.player,
npcs: npcStore.npcs
}
// 使用累积的时间更新每个NPC
npcStore.npcs.forEach(npc => {
npcGrowthLogic.updateNPCGrowth(npc, gameState, npcUpdateCounter)
})
// 重置计数器
npcUpdateCounter = 0
}
// NPC行为系统更新
let npcBehaviorCounter = 0
const NPC_BEHAVIOR_INTERVAL = 5
/**
* 更新NPC行为系统
*/
const updateNPCBehavior = (deltaSeconds: number) => {
// 累积时间
npcBehaviorCounter += deltaSeconds
// 只在达到更新间隔时才执行
if (npcBehaviorCounter < NPC_BEHAVIOR_INTERVAL) {
return
}
// 如果没有NPC直接返回
if (npcStore.npcs.length === 0) {
npcBehaviorCounter = 0
return
}
const now = Date.now()
const allPlanets = Object.values(universeStore.planets)
// 更新每个NPC的行为
npcStore.npcs.forEach(npc => {
npcBehaviorLogic.updateNPCBehavior(npc, gameStore.player, allPlanets, universeStore.debrisFields, now)
})
npcBehaviorCounter = 0
}
return {
processNPCMissionArrival,
processNPCMissionReturn,
removeIncomingFleetAlert,
removeIncomingFleetAlertById,
updateNPCGrowth,
updateNPCBehavior
}
}

View File

@@ -1,103 +0,0 @@
import type { Ref } from 'vue'
import { useGameStore } from '@/stores/gameStore'
import type { BuildQueueItem } from '@/types/game'
import * as buildingValidation from '@/logic/buildingValidation'
import * as resourceLogic from '@/logic/resourceLogic'
import * as researchValidation from '@/logic/researchValidation'
/**
* 队列处理
* 处理建造队列和研究队列的取消操作
*/
export const useQueueHandler = (
t: (key: string) => string,
confirmDialogOpen: Ref<boolean>,
confirmDialogTitle: Ref<string>,
confirmDialogMessage: Ref<string>,
confirmDialogAction: Ref<(() => void) | null>
) => {
const gameStore = useGameStore()
/**
* 取消建造
*/
const handleCancelBuild = (queueId: string) => {
confirmDialogTitle.value = t('queue.cancelBuild')
confirmDialogMessage.value = t('queue.confirmCancel')
confirmDialogAction.value = () => {
if (!gameStore.currentPlanet) return false
const { item, index } = buildingValidation.findQueueItem(gameStore.currentPlanet.buildQueue, queueId)
if (!item) return false
if (item.type === 'building') {
const refund = buildingValidation.cancelBuildingUpgrade(gameStore.currentPlanet, item)
resourceLogic.addResources(gameStore.currentPlanet.resources, refund)
}
gameStore.currentPlanet.buildQueue.splice(index, 1)
return true
}
confirmDialogOpen.value = true
}
/**
* 取消研究
*/
const handleCancelResearch = (queueId: string) => {
confirmDialogTitle.value = t('queue.cancelResearch')
confirmDialogMessage.value = t('queue.confirmCancel')
confirmDialogAction.value = () => {
if (!gameStore.currentPlanet) return false
const { item, index } = buildingValidation.findQueueItem(gameStore.player.researchQueue, queueId)
if (!item) return false
if (item.type === 'technology') {
const refund = researchValidation.cancelTechnologyResearch(item)
resourceLogic.addResources(gameStore.currentPlanet.resources, refund)
}
gameStore.player.researchQueue.splice(index, 1)
return true
}
confirmDialogOpen.value = true
}
/**
* 获取队列项名称
*/
const getItemName = (item: BuildQueueItem): string => {
if (item.type === 'building' || item.type === 'demolish') {
const buildingName = t(`buildings.${item.itemType}`)
return item.type === 'demolish' ? `${t('buildingsView.demolish')} - ${buildingName}` : buildingName
} else if (item.type === 'technology') {
return t(`technologies.${item.itemType}`)
} else if (item.type === 'ship') {
return t(`ships.${item.itemType}`)
} else if (item.type === 'defense') {
return t(`defenses.${item.itemType}`)
}
return t('common.unknown')
}
/**
* 获取剩余时间(秒)
*/
const getRemainingTime = (item: BuildQueueItem): number => {
const now = Date.now()
return Math.max(0, Math.floor((item.endTime - now) / 1000))
}
/**
* 获取队列进度(百分比)
*/
const getQueueProgress = (item: BuildQueueItem): number => {
const now = Date.now()
const total = item.endTime - item.startTime
const elapsed = now - item.startTime
return Math.min(100, Math.max(0, (elapsed / total) * 100))
}
return {
handleCancelBuild,
handleCancelResearch,
getItemName,
getRemainingTime,
getQueueProgress
}
}

View File

@@ -123,7 +123,7 @@ export const BUILDINGS: Record<BuildingType, BuildingConfig> = {
baseTime: 30, // 减少建造时间60→30秒
costMultiplier: 2,
spaceUsage: 5,
fleetStorageBonus: 1000, // 每级增加100舰队仓储
fleetStorageBonus: 1000, // 每级增加1000舰队仓储
requirements: { [BuildingType.RoboticsFactory]: 2 },
levelRequirements: {
8: { [BuildingType.RoboticsFactory]: 5, [BuildingType.ResearchLab]: 5 },
@@ -251,7 +251,7 @@ export const BUILDINGS: Record<BuildingType, BuildingConfig> = {
[BuildingType.Terraformer]: {
id: BuildingType.Terraformer,
name: '地形改造器',
description: '改造行星地形,每级增加5个可用空间',
description: '改造行星地形,每级增加30个可用空间',
baseCost: { metal: 0, crystal: 50000, deuterium: 100000, darkMatter: 0, energy: 0 },
baseTime: 60,
costMultiplier: 2,
@@ -433,7 +433,7 @@ export const TECHNOLOGIES: Record<TechnologyType, TechnologyConfig> = {
baseCost: { metal: 0, crystal: 400, deuterium: 600, darkMatter: 0, energy: 0 },
baseTime: 60,
costMultiplier: 2,
fleetStorageBonus: 500, // 每级全局增加50舰队仓储
fleetStorageBonus: 500, // 每级全局增加500舰队仓储
maxLevel: 10, // 最多10级最多11个研究队列
requirements: { [BuildingType.ResearchLab]: 1 },
levelRequirements: {
@@ -588,7 +588,7 @@ export const TECHNOLOGIES: Record<TechnologyType, TechnologyConfig> = {
[TechnologyType.TerraformingTechnology]: {
id: TechnologyType.TerraformingTechnology,
name: '地形改造技术',
description: '研究行星地形改造技术,每级为所有行星增加5个可用空间',
description: '研究行星地形改造技术,每级为所有行星增加30个可用空间',
baseCost: { metal: 0, crystal: 20000, deuterium: 40000, darkMatter: 0, energy: 0 },
baseTime: 90,
costMultiplier: 2,
@@ -867,7 +867,7 @@ export const SHIPS: Record<ShipType, ShipConfig> = {
fuelConsumption: 1,
storageUsage: 100,
requirements: {
[BuildingType.PlanetDestroyerFactory]: 10,
[BuildingType.PlanetDestroyerFactory]: 3,
[TechnologyType.PlanetDestructionTech]: 7,
[TechnologyType.HyperspaceDrive]: 7
}
@@ -1085,15 +1085,15 @@ export const MOON_CONFIG = {
baseChance: 1, // 基础1%概率
maxChance: 20, // 最大20%概率
chancePerDebris: 100000, // 每10万资源增加1%概率
baseSize: 60, // 月球基础空间
lunarBaseSpaceBonus: 5 // 每级月球基地增加的空间
baseSize: 100, // 月球基础空间
lunarBaseSpaceBonus: 30 // 每级月球基地增加的空间
}
// 行星配置
export const PLANET_CONFIG = {
baseSize: 200, // 行星基础空间
terraformerSpaceBonus: 5, // 每级地形改造器增加的空间
terraformingTechSpaceBonus: 3 // 每级地形改造技术增加的空间
baseSize: 300, // 行星基础空间
terraformerSpaceBonus: 30, // 每级地形改造器增加的空间
terraformingTechSpaceBonus: 30 // 每级地形改造技术增加的空间
}
// 舰队仓储配置

View File

@@ -33,13 +33,19 @@ export default {
viewRequirements: 'Anforderungen anzeigen',
requirementsNotMet: 'Anforderungen nicht erfüllt',
current: 'Aktuell',
level: 'Stufe'
level: 'Stufe',
gmModeActivated: 'GM-Modus aktiviert! Überprüfen Sie das Navigationsmenü.'
},
errors: {
requirementsNotMet: 'Anforderungen nicht erfüllt',
insufficientResources: 'Unzureichende Ressourcen',
insufficientFleetStorage: 'Unzureichender Flottenspeicher',
shieldDomeLimit: 'Schildkuppel-Limit erreicht',
missileSiloLimit: 'Raketensilokapazität überschritten',
insufficientMissiles: 'Unzureichende Interkontinentalraketen',
invalidMissileCount: 'Ungültige Raketenanzahl',
targetOutOfRange: 'Ziel außer Reichweite',
cannotAttackOwnPlanet: 'Eigenen Planeten kann nicht angegriffen werden',
fleetMissionsFull: 'Flottenmissionsplätze voll',
insufficientFleet: 'Unzureichende Flotte',
insufficientFuel: 'Unzureichender Treibstoff',
@@ -166,8 +172,8 @@ export default {
darkMatterCollector: 'Sammelt seltene Dunkle-Materie-Ressourcen',
darkMatterTank: 'Erhöht Dunkle-Materie-Speicherkapazität',
missileSilo: 'Lagert und startet Raketen, 10 Raketen pro Stufe',
terraformer: 'Terraformt Planetenoberfläche, erhöht verfügbaren Platz um 5 pro Stufe',
lunarBase: 'Erhöht verfügbaren Platz auf dem Mond, +5 Platz pro Stufe',
terraformer: 'Terraformt Planetenoberfläche, erhöht verfügbaren Platz um 30 pro Stufe',
lunarBase: 'Erhöht verfügbaren Platz auf dem Mond, +30 Platz pro Stufe',
sensorPhalanx: 'Erkennt Flottenaktivitäten in umliegenden Systemen',
jumpGate: 'Überträgt Flotten sofort zu anderen Monden',
planetDestroyerFactory: 'Konstruiert ultimative Waffen zur Zerstörung von Planeten'
@@ -283,7 +289,7 @@ export default {
impulseDrive: 'Mittlere Antriebstechnologie',
hyperspaceDrive: 'Fortgeschrittene Antriebstechnologie',
darkMatterTechnology: 'Forschung zu Eigenschaften und Anwendungen von Dunkler Materie',
terraformingTechnology: 'Forschung zur Planeten-Terraforming-Technologie, erhöht verfügbaren Platz aller Planeten um 3 pro Stufe',
terraformingTechnology: 'Forschung zur Planeten-Terraforming-Technologie, erhöht verfügbaren Platz aller Planeten um 30 pro Stufe',
planetDestructionTech: 'Schreckliche Technologie zur Zerstörung ganzer Planeten'
},
officers: {
@@ -316,6 +322,7 @@ export default {
cancelResearch: 'Forschung abbrechen',
confirmCancel: 'Möchten Sie wirklich abbrechen? 50% der Ressourcen werden zurückerstattet.',
level: 'Stufe',
gmModeActivated: '',
upgradeToLevel: 'Auf Stufe aufrüsten'
},
overview: {
@@ -336,6 +343,7 @@ export default {
usedSpace: 'Verwendeter Platz',
spaceUsage: 'Platzbedarf',
level: 'Stufe',
gmModeActivated: '',
upgradeCost: 'Ausbaukosten',
buildTime: 'Bauzeit',
upgrade: 'Ausbauen',
@@ -361,6 +369,7 @@ export default {
},
shipyard: {
attack: 'Angriff',
missileAttack: 'Raketenangriff',
shield: 'Schild',
armor: 'Panzerung',
speed: 'Geschwindigkeit',
@@ -378,6 +387,7 @@ export default {
title: 'Raumschiffwerft',
fleetStorage: 'Flottenspeicher',
attack: 'Angriff',
missileAttack: 'Raketenangriff',
shield: 'Schild',
speed: 'Geschwindigkeit',
cargoCapacity: 'Ladekapazität',
@@ -392,6 +402,7 @@ export default {
},
defense: {
attack: 'Angriff',
missileAttack: 'Raketenangriff',
shield: 'Schild',
armor: 'Panzerung',
buildCost: 'Baukosten',
@@ -405,6 +416,7 @@ export default {
defenseView: {
title: 'Verteidigung',
attack: 'Angriff',
missileAttack: 'Raketenangriff',
shield: 'Schild',
armor: 'Panzerung',
buildTime: 'Bauzeit',
@@ -414,6 +426,7 @@ export default {
totalCost: 'Gesamtkosten',
build: 'Bauen',
shieldDomeBuilt: 'Schildkuppel bereits gebaut',
missileCapacity: 'Raketenkapazität',
inputError: 'Eingabefehler',
inputErrorMessage: 'Bitte Baumenge eingeben!',
buildFailed: 'Bau fehlgeschlagen',
@@ -427,6 +440,7 @@ export default {
flightMissions: 'Flugmissionen',
currentPlanetFleet: 'Flotte auf diesem Planeten',
attack: 'Angriff',
missileAttack: 'Raketenangriff',
shield: 'Schild',
armor: 'Panzerung',
speed: 'Geschwindigkeit',
@@ -521,27 +535,38 @@ export default {
selectSystem: 'System auswählen',
view: 'Anzeigen',
myPlanet: 'Mein Planet',
myPlanets: 'Meine Planeten',
myPlanets: 'Meine Systeme ansehen',
npcPlanets: 'NPC-Planeten',
selectPlanetToView: 'Planet zum Anzeigen auswählen',
selectPlanetToView: 'Planet auswählen, um sein System anzuzeigen',
totalPositions: 'Insgesamt 10 Planetenpositionen',
mine: 'Mein',
hostile: 'Feindlich',
emptySlot: 'Leer - Kolonisierbar',
scout: 'Spähen',
attack: 'Angriff',
missileAttack: 'Raketenangriff',
colonize: 'Kolonisieren',
switch: 'Wechseln',
recycle: 'Recyceln',
debrisField: 'Trümmerfeld',
scoutPlanetTitle: 'Planet ausspionieren',
attackPlanetTitle: 'Planet angreifen',
missileAttackTitle: 'Raketenangriff',
colonizePlanetTitle: 'Planet kolonisieren',
recyclePlanetTitle: 'Trümmer recyceln',
scoutPlanetMessage:
'Möchten Sie wirklich Spionagesonden senden, um Planet [{coordinates}] auszuspionieren?\n\nBitte gehen Sie zur Flottenseite, um Schiffe auszuwählen und zu senden.',
attackPlanetMessage:
'Möchten Sie wirklich Planet [{coordinates}] angreifen?\n\nBitte gehen Sie zur Flottenseite, um Schiffe auszuwählen und zu senden.',
missileAttackMessage: 'Interkontinentalraketen starten, um Planet [{coordinates}] anzugreifen',
missileCount: 'Raketenanzahl',
availableMissiles: 'Verfügbare Raketen',
missileRange: 'Raketenreichweite',
systems: 'Systeme',
distance: 'Entfernung',
flightTime: 'Flugzeit',
launchMissile: 'Starten',
cancel: 'Abbrechen',
colonizePlanetMessage:
'Möchten Sie wirklich Position [{coordinates}] kolonisieren?\n\nBitte gehen Sie zur Flottenseite, um ein Kolonieschiff zu senden.',
recyclePlanetMessage:
@@ -699,10 +724,24 @@ export default {
gamePaused: 'Spiel pausiert',
gameResumed: 'Spiel fortgesetzt',
playerName: 'Spielername',
gameSpeed: 'Spielgeschwindigkeit',
gameSpeedDesc: 'Aktueller Spielgeschwindigkeitsmultiplikator',
gameSpeed: 'Ressourcenproduktionsgeschwindigkeit',
gameSpeedDesc: 'Aktueller Ressourcenproduktionsgeschwindigkeitsmultiplikator',
speedChanged: 'Ressourcenproduktionsgeschwindigkeit auf {speed}x geändert',
speedReset: 'Ressourcenproduktionsgeschwindigkeit auf 1x zurückgesetzt',
reset: 'Zurücksetzen',
about: 'Über',
version: 'Version',
latestVersion: 'Neueste Version',
checkUpdate: 'Update prüfen',
checking: 'Prüfen...',
newVersionAvailable: 'Neue Version {version} verfügbar',
upToDate: 'Bereits auf dem neuesten Stand',
checkUpdateCooldown: 'Bitte versuchen Sie es später erneut (5 Minuten Abklingzeit)',
checkUpdateFailed: 'Update-Prüfung fehlgeschlagen, bitte überprüfen Sie Ihre Netzwerkverbindung',
viewUpdate: 'Update ansehen',
updateAvailable: 'Eine neue Version ist verfügbar. Klicken Sie, um die Versionshinweise anzuzeigen.',
download: 'Herunterladen',
goToDownload: 'Zum Download',
buildDate: 'Build-Datum',
community: 'Community',
github: 'GitHub-Repository',
@@ -741,12 +780,22 @@ export default {
testSpy: 'Spionage testen',
testAttack: 'Angriff testen',
testSpyAndAttack: 'Spionage & Angriff testen',
testSpyMessage: 'Klicken Sie auf Bestätigen, um die Spionagemission zu beschleunigen',
testAttackMessage: 'Klicken Sie auf Bestätigen, um die Angriffsmission zu beschleunigen',
testSpyAndAttackMessage: 'Klicken Sie auf Bestätigen, um die Missionen zu beschleunigen',
initializeFleet: 'NPC-Flotte initialisieren',
accelerateMissions: 'Alle Missionen beschleunigen (5s)',
selectNPCFirst: 'Bitte wählen Sie zuerst einen NPC',
npcNoProbes: 'NPC hat keine Spionagesonden',
npcNoSpyReport: 'NPC muss zuerst spionieren',
npcMissionFailed: 'Mission konnte nicht erstellt werden',
npcNoPlanets: 'NPC hat keine Planeten',
npcWillSpyIn5s: '{npcName} wird in 5 Sekunden spionieren',
npcWillAttackIn5s: '{npcName} wird in 5 Sekunden angreifen',
npcWillSpyAndAttack: '{npcName} wird in 5s spionieren und in 10s angreifen',
acceleratedMissions: '{count} Missionen auf 5 Sekunden beschleunigt',
npcFleetInitialized: '{npcName} Flotte initialisiert',
npcFleetDetails: '100 Spionagesonden\n500 Leichte Jäger\n300 Schwere Jäger\n200 Kreuzer\n100 Schlachtschiffe\n50 Bomber\n30 Zerstörer\n20 Schlachtkreuzer',
dangerZone: 'Gefahrenzone',
dangerZoneDesc: 'Die folgenden Vorgänge sind irreversibel',
resetGame: 'Spiel zurücksetzen',
@@ -798,9 +847,32 @@ export default {
events: {
gift: 'Geschenk gesendet',
attack: 'Angriff',
missileAttack: 'Raketenangriff',
allyAttacked: 'Verbündeter angegriffen',
spy: 'Spionage',
stealDebris: 'Trümmer gestohlen'
},
reports: {
giftedResources: '{metal}M {crystal}K {deuterium}D geschenkt',
receivedGiftFromPlayer: 'Geschenk von Spieler erhalten',
giftedToNpc: 'Sie haben {npcName} Ressourcen geschenkt. Ansehen +{reputation}',
rejectedPlayerGift: 'Geschenk des Spielers abgelehnt',
npcRejectedGift: '{npcName} hat Ihr Geschenk abgelehnt. Ansehen {reputation}',
attackedNpc: '{npcName} angegriffen',
wasAttackedByPlayer: 'Wurde von Spieler angegriffen',
youAttackedNpc: 'Sie haben {npcName} angegriffen',
playerAttackedAlly: 'Spieler hat Verbündeten {allyName} angegriffen',
allyDispleased: '{allyName} ist unzufrieden, dass Sie ihren Verbündeten {targetName} angegriffen haben',
wasSpiedByPlayer: 'Wurde von Spieler ausspioniert (entdeckt: {detected})',
spyDetected: 'Ihre Spionage wurde von {npcName} entdeckt',
stoleDebrisFromTerritory: 'Trümmer aus {npcName}s Territorium gestohlen',
playerStoleDebris: 'Spieler hat Trümmer aus Territorium gestohlen',
recycledDebrisNearNpc: 'Sie haben Trümmer in der Nähe von {npcName}s Planeten recycelt. Sie sind unzufrieden.',
giftedResourcesToPlayer: 'Ressourcen an Spieler geschenkt',
receivedGiftFromNpc: 'Geschenk von {npcName} erhalten',
acceptedGiftFromNpc: 'Sie haben ein Geschenk von {npcName} angenommen: {metal}M {crystal}K {deuterium}D',
playerRejectedGift: 'Spieler hat Geschenk abgelehnt',
rejectedGiftFromNpc: 'Sie haben ein Geschenk von {npcName} abgelehnt. Ansehen {reputation}'
}
},
pagination: {
@@ -809,5 +881,10 @@ export default {
first: 'Erste',
last: 'Letzte',
page: 'Seite {page}'
},
notFound: {
title: 'Seite nicht gefunden',
description: 'Entschuldigung, die gesuchte Seite existiert nicht',
goHome: 'Zur Startseite'
}
}

View File

@@ -33,13 +33,19 @@ export default {
viewRequirements: 'View Requirements',
requirementsNotMet: 'Requirements Not Met',
current: 'Current',
level: 'Level'
level: 'Level',
gmModeActivated: 'GM Mode Activated! Check the navigation menu.'
},
errors: {
requirementsNotMet: 'Requirements not met',
insufficientResources: 'Insufficient resources',
insufficientFleetStorage: 'Insufficient fleet storage',
shieldDomeLimit: 'Shield dome limit reached',
missileSiloLimit: 'Missile silo capacity exceeded',
insufficientMissiles: 'Insufficient interplanetary missiles',
invalidMissileCount: 'Invalid missile count',
targetOutOfRange: 'Target out of range',
cannotAttackOwnPlanet: 'Cannot attack your own planet',
fleetMissionsFull: 'Fleet mission slots full',
insufficientFleet: 'Insufficient fleet',
insufficientFuel: 'Insufficient fuel',
@@ -164,8 +170,8 @@ export default {
darkMatterCollector: 'Collects rare dark matter resources',
darkMatterTank: 'Increases dark matter storage capacity',
missileSilo: 'Stores and launches missiles, 10 missiles per level',
terraformer: 'Terraforms planet surface, adds 5 available space per level',
lunarBase: 'Increases available space on the moon, +5 space per level',
terraformer: 'Terraforms planet surface, adds 30 available space per level',
lunarBase: 'Increases available space on the moon, +30 space per level',
sensorPhalanx: 'Detects fleet activities in surrounding systems',
jumpGate: 'Instantly transfers fleets to other moons',
planetDestroyerFactory: 'Constructs ultimate weapons capable of destroying planets'
@@ -283,7 +289,7 @@ export default {
impulseDrive: 'Intermediate propulsion technology',
hyperspaceDrive: 'Advanced propulsion technology',
darkMatterTechnology: 'Research into dark matter properties and applications',
terraformingTechnology: 'Research planet terraforming technology, adds 3 available space to all planets per level',
terraformingTechnology: 'Research planet terraforming technology, adds 30 available space to all planets per level',
planetDestructionTech: 'Terrifying technology for destroying entire planets'
},
officers: {
@@ -364,6 +370,7 @@ export default {
},
shipyard: {
attack: 'Attack',
missileAttack: 'Missile Attack',
shield: 'Shield',
armor: 'Armor',
speed: 'Speed',
@@ -381,6 +388,7 @@ export default {
title: 'Shipyard',
fleetStorage: 'Fleet Storage',
attack: 'Attack',
missileAttack: 'Missile Attack',
shield: 'Shield',
speed: 'Speed',
cargoCapacity: 'Cargo Capacity',
@@ -395,6 +403,7 @@ export default {
},
defense: {
attack: 'Attack',
missileAttack: 'Missile Attack',
shield: 'Shield',
armor: 'Armor',
buildCost: 'Build Cost',
@@ -408,6 +417,7 @@ export default {
defenseView: {
title: 'Defense',
attack: 'Attack',
missileAttack: 'Missile Attack',
shield: 'Shield',
armor: 'Armor',
buildTime: 'Build Time',
@@ -417,6 +427,7 @@ export default {
totalCost: 'Total Cost',
build: 'Build',
shieldDomeBuilt: 'Shield dome already built',
missileCapacity: 'Missile Capacity',
inputError: 'Input Error',
inputErrorMessage: 'Please enter build quantity!',
buildFailed: 'Build Failed',
@@ -429,6 +440,7 @@ export default {
flightMissions: 'Flight Missions',
currentPlanetFleet: 'Current Planet Fleet',
attack: 'Attack',
missileAttack: 'Missile Attack',
shield: 'Shield',
armor: 'Armor',
speed: 'Speed',
@@ -525,26 +537,37 @@ export default {
selectSystem: 'Select System',
view: 'View',
myPlanet: 'My Planet',
myPlanets: 'My Planets',
myPlanets: 'View My Systems',
npcPlanets: 'NPC Planets',
selectPlanetToView: 'Select planet to view',
selectPlanetToView: 'Select planet to view its system',
totalPositions: '10 planet positions total',
mine: 'Mine',
hostile: 'Hostile',
emptySlot: 'Empty - Colonizable',
scout: 'Scout',
attack: 'Attack',
missileAttack: 'Missile Attack',
colonize: 'Colonize',
switch: 'Switch',
recycle: 'Recycle',
debrisField: 'Debris Field',
scoutPlanetTitle: 'Scout Planet',
attackPlanetTitle: 'Attack Planet',
missileAttackTitle: 'Missile Attack',
colonizePlanetTitle: 'Colonize Planet',
recyclePlanetTitle: 'Recycle Debris',
scoutPlanetMessage:
'Are you sure you want to send espionage probes to scout planet [{coordinates}]?\n\nPlease go to the fleet page to select ships and send.',
attackPlanetMessage: 'Are you sure you want to attack planet [{coordinates}]?\n\nPlease go to the fleet page to select ships and send.',
missileAttackMessage: 'Launch interplanetary missiles to attack planet [{coordinates}]',
missileCount: 'Missile Count',
availableMissiles: 'Available Missiles',
missileRange: 'Missile Range',
systems: 'systems',
distance: 'Distance',
flightTime: 'Flight Time',
launchMissile: 'Launch',
cancel: 'Cancel',
colonizePlanetMessage:
'Are you sure you want to colonize position [{coordinates}]?\n\nPlease go to the fleet page to send a colony ship.',
recyclePlanetMessage:
@@ -697,10 +720,24 @@ export default {
gamePaused: 'Game paused',
gameResumed: 'Game resumed',
playerName: 'Player Name',
gameSpeed: 'Game Speed',
gameSpeedDesc: 'Current game speed multiplier',
gameSpeed: 'Resource Production Speed',
gameSpeedDesc: 'Current resource production speed multiplier',
speedChanged: 'Resource production speed changed to {speed}x',
speedReset: 'Resource production speed reset to 1x',
reset: 'Reset',
about: 'About',
version: 'Version',
latestVersion: 'Latest Version',
checkUpdate: 'Check Update',
checking: 'Checking...',
newVersionAvailable: 'New version {version} available',
upToDate: 'Already up to date',
checkUpdateCooldown: 'Please try again later (5 minute cooldown)',
checkUpdateFailed: 'Failed to check for updates, please check your network connection',
viewUpdate: 'View Update',
updateAvailable: 'A new version is available. Click to view release notes.',
download: 'Download',
goToDownload: 'Go to Download',
buildDate: 'Build Date',
community: 'Community',
github: 'GitHub Repository',
@@ -739,12 +776,22 @@ export default {
testSpy: 'Test Spy',
testAttack: 'Test Attack',
testSpyAndAttack: 'Test Spy & Attack',
testSpyMessage: 'Click confirm to accelerate the spy mission',
testAttackMessage: 'Click confirm to accelerate the attack mission',
testSpyAndAttackMessage: 'Click confirm to accelerate the missions',
initializeFleet: 'Initialize NPC Fleet',
accelerateMissions: 'Accelerate All Missions (5s)',
selectNPCFirst: 'Please select an NPC first',
npcNoProbes: 'NPC has no spy probes',
npcNoSpyReport: 'NPC needs to spy first',
npcMissionFailed: 'Failed to create mission',
npcNoPlanets: 'NPC has no planets',
npcWillSpyIn5s: '{npcName} will spy in 5 seconds',
npcWillAttackIn5s: '{npcName} will attack in 5 seconds',
npcWillSpyAndAttack: '{npcName} will spy in 5s and attack in 10s',
acceleratedMissions: 'Accelerated {count} missions to 5 seconds',
npcFleetInitialized: '{npcName} fleet initialized',
npcFleetDetails: '100 Spy Probes\n500 Light Fighters\n300 Heavy Fighters\n200 Cruisers\n100 Battleships\n50 Bombers\n30 Destroyers\n20 Battlecruisers',
dangerZone: 'Danger Zone',
dangerZoneDesc: 'The following operations are irreversible',
resetGame: 'Reset Game',
@@ -796,9 +843,32 @@ export default {
events: {
gift: 'Sent Gift',
attack: 'Attack',
missileAttack: 'Missile Attack',
allyAttacked: 'Ally Attacked',
spy: 'Espionage',
stealDebris: 'Debris Stolen'
},
reports: {
giftedResources: 'Gifted {metal}M {crystal}C {deuterium}D',
receivedGiftFromPlayer: 'Received gift from player',
giftedToNpc: 'You gifted resources to {npcName}. Reputation +{reputation}',
rejectedPlayerGift: 'Rejected player\'s gift',
npcRejectedGift: '{npcName} rejected your gift. Reputation {reputation}',
attackedNpc: 'Attacked {npcName}',
wasAttackedByPlayer: 'Was attacked by player',
youAttackedNpc: 'You attacked {npcName}',
playerAttackedAlly: 'Player attacked ally {allyName}',
allyDispleased: '{allyName} is displeased that you attacked their ally {targetName}',
wasSpiedByPlayer: 'Was spied by player (detected: {detected})',
spyDetected: 'Your espionage was detected by {npcName}',
stoleDebrisFromTerritory: 'Stole debris from {npcName}\'s territory',
playerStoleDebris: 'Player stole debris from territory',
recycledDebrisNearNpc: 'You recycled debris near {npcName}\'s planet. They are displeased.',
giftedResourcesToPlayer: 'Gifted resources to player',
receivedGiftFromNpc: 'Received gift from {npcName}',
acceptedGiftFromNpc: 'You accepted a gift from {npcName}: {metal}M {crystal}C {deuterium}D',
playerRejectedGift: 'Player rejected gift',
rejectedGiftFromNpc: 'You rejected a gift from {npcName}. Reputation {reputation}'
}
},
pagination: {
@@ -807,5 +877,10 @@ export default {
first: 'First',
last: 'Last',
page: 'Page {page}'
},
notFound: {
title: 'Page Not Found',
description: 'Sorry, the page you are looking for does not exist',
goHome: 'Go Home'
}
}

View File

@@ -33,13 +33,19 @@ export default {
viewRequirements: '必要条件を表示',
requirementsNotMet: '必要条件が満たされていません',
current: '現在',
level: 'レベル'
level: 'レベル',
gmModeActivated: 'GMモードが有効になりましたナビゲーションメニューをご確認ください。'
},
errors: {
requirementsNotMet: '前提条件を満たしていません',
insufficientResources: '資源が不足しています',
insufficientFleetStorage: '艦隊ストレージが不足しています',
shieldDomeLimit: 'シールドドームの上限に達しました',
missileSiloLimit: 'ミサイル格納庫の容量を超えています',
insufficientMissiles: '惑星間ミサイルが不足しています',
invalidMissileCount: 'ミサイル数が無効です',
targetOutOfRange: 'ターゲットが射程外です',
cannotAttackOwnPlanet: '自分の惑星を攻撃できません',
fleetMissionsFull: '艦隊ミッションスロットが満杯です',
insufficientFleet: '艦隊が不足しています',
insufficientFuel: '燃料が不足しています',
@@ -166,8 +172,8 @@ export default {
darkMatterCollector: '希少なダークマター資源を収集',
darkMatterTank: 'ダークマターの貯蔵上限を増加',
missileSilo: 'ミサイルを保管・発射、レベル毎に10発',
terraformer: '惑星地形を改造、レベル毎に利用可能スペース5増加',
lunarBase: '月の利用可能スペースを増加、レベル毎に+5スペース',
terraformer: '惑星地形を改造、レベル毎に利用可能スペース30増加',
lunarBase: '月の利用可能スペースを増加、レベル毎に+30スペース',
sensorPhalanx: '周辺星系の艦隊活動を探知',
jumpGate: '他の月へ艦隊を瞬間移動',
planetDestroyerFactory: '惑星を破壊できる究極兵器を建造'
@@ -283,7 +289,7 @@ export default {
impulseDrive: '中級推進技術',
hyperspaceDrive: '高級推進技術',
darkMatterTechnology: 'ダークマターの性質と応用を研究',
terraformingTechnology: '惑星地形改造技術を研究、レベル毎に全惑星の利用可能スペース3増加',
terraformingTechnology: '惑星地形改造技術を研究、レベル毎に全惑星の利用可能スペース30増加',
planetDestructionTech: '惑星全体を破壊する恐怖の技術を研究'
},
officers: {
@@ -316,10 +322,12 @@ export default {
cancelResearch: '研究キャンセル',
confirmCancel: 'キャンセルしますか資源の50%が返還されます。',
level: 'レベル',
gmModeActivated: '',
upgradeToLevel: 'レベルにアップグレード'
},
shipyard: {
attack: '攻撃力',
missileAttack: 'ミサイル攻撃',
shield: 'シールド',
armor: '装甲',
speed: '速度',
@@ -351,6 +359,7 @@ export default {
usedSpace: '使用済みスペース',
spaceUsage: 'スペース使用量',
level: 'レベル',
gmModeActivated: '',
upgradeCost: 'アップグレードコスト',
buildTime: '建設時間',
upgrade: 'アップグレード',
@@ -362,8 +371,8 @@ export default {
demolishRefund: '解体返還',
demolishFailed: '解体失敗',
demolishFailedMessage: 'この建物を解体できません。建設キューが満杯か、建物レベルが0でないか確認してください。',
confirmDemolish: '',
confirmDemolishMessage: ''
confirmDemolish: '解体確認',
confirmDemolishMessage: '以下の建物を解体しますか?'
},
researchView: {
title: '研究',
@@ -375,6 +384,7 @@ export default {
},
defense: {
attack: '攻撃力',
missileAttack: 'ミサイル攻撃',
shield: 'シールド',
armor: '装甲',
buildCost: '建設コスト',
@@ -389,6 +399,7 @@ export default {
title: '造船所',
fleetStorage: '艦隊ストレージ',
attack: '攻撃力',
missileAttack: 'ミサイル攻撃',
shield: 'シールド',
speed: '速度',
cargoCapacity: '積載量',
@@ -404,6 +415,7 @@ export default {
defenseView: {
title: '防衛施設',
attack: '攻撃力',
missileAttack: 'ミサイル攻撃',
shield: 'シールド',
armor: '装甲',
buildTime: '建設時間',
@@ -413,6 +425,7 @@ export default {
totalCost: '総コスト',
build: '建造',
shieldDomeBuilt: 'シールドドーム建設済み',
missileCapacity: 'ミサイル容量',
inputError: '入力エラー',
inputErrorMessage: '建造数を入力してください!',
buildFailed: '建造失敗',
@@ -425,6 +438,7 @@ export default {
flightMissions: '飛行ミッション',
currentPlanetFleet: '現在の惑星艦隊',
attack: '攻撃',
missileAttack: 'ミサイル攻撃',
shield: 'シールド',
armor: '装甲',
speed: '速度',
@@ -519,25 +533,36 @@ export default {
selectSystem: '星系を選択',
view: '表示',
myPlanet: '自分の惑星',
myPlanets: '私の惑星',
myPlanets: '自分の星系を表示',
npcPlanets: 'NPCの惑星',
selectPlanetToView: '表示する惑星を選択',
selectPlanetToView: '惑星を選択して星系を表示',
totalPositions: '全10惑星位置',
mine: '自分',
hostile: '敵対',
emptySlot: '空き - 植民可能',
scout: '偵察',
attack: '攻撃',
missileAttack: 'ミサイル攻撃',
colonize: '植民',
switch: '切り替え',
recycle: '回収',
debrisField: 'デブリフィールド',
scoutPlanetTitle: '惑星偵察',
attackPlanetTitle: '惑星攻撃',
missileAttackTitle: 'ミサイル攻撃',
colonizePlanetTitle: '惑星植民',
recyclePlanetTitle: 'デブリ回収',
scoutPlanetMessage: '惑星[{coordinates}]にスパイプローブを送りますか?\n\n艦隊ページに移動して艦船を選択して派遣してください。',
attackPlanetMessage: '惑星[{coordinates}]を攻撃しますか?\n\n艦隊ページに移動して艦船を選択して派遣してください。',
missileAttackMessage: '惑星[{coordinates}]に惑星間ミサイルを発射',
missileCount: 'ミサイル数',
availableMissiles: '利用可能なミサイル',
missileRange: 'ミサイル射程',
systems: 'システム',
distance: '距離',
flightTime: '飛行時間',
launchMissile: '発射',
cancel: 'キャンセル',
colonizePlanetMessage: '位置[{coordinates}]を植民しますか?\n\n艦隊ページに移動してコロニーシップを派遣してください。',
recyclePlanetMessage: '位置[{coordinates}]のデブリを回収しますか?\n\n艦隊ページに移動してリサイクラーを派遣してください。',
sendGift: 'ギフト送信',
@@ -690,10 +715,24 @@ export default {
gamePaused: 'ゲームを一時停止しました',
gameResumed: 'ゲームを再開しました',
playerName: 'プレイヤー名',
gameSpeed: 'ゲーム速度',
gameSpeedDesc: '現在のゲーム速度倍率',
gameSpeed: '資源生産速度',
gameSpeedDesc: '現在の資源生産速度倍率',
speedChanged: '資源生産速度を{speed}xに変更しました',
speedReset: '資源生産速度を1xにリセットしました',
reset: 'リセット',
about: 'について',
version: 'バージョン',
latestVersion: '最新バージョン',
checkUpdate: 'アップデート確認',
checking: '確認中...',
newVersionAvailable: '新バージョン{version}が利用可能です',
upToDate: '最新バージョンです',
checkUpdateCooldown: 'しばらくしてから再度お試しください5分間のクールダウン',
checkUpdateFailed: 'アップデートの確認に失敗しました。ネットワーク接続を確認してください',
viewUpdate: '更新を表示',
updateAvailable: '新しいバージョンが利用可能です。クリックしてリリースノートを表示します。',
download: 'ダウンロード',
goToDownload: 'ダウンロードへ',
buildDate: 'ビルド日',
community: 'コミュニティ',
github: 'GitHubリポジトリ',
@@ -732,12 +771,22 @@ export default {
testSpy: '偵察テスト',
testAttack: '攻撃テスト',
testSpyAndAttack: '偵察&攻撃テスト',
testSpyMessage: '確認をクリックして偵察ミッションを加速',
testAttackMessage: '確認をクリックして攻撃ミッションを加速',
testSpyAndAttackMessage: '確認をクリックしてミッションを加速',
initializeFleet: 'NPC艦隊を初期化',
accelerateMissions: 'すべてのミッションを加速(5秒)',
selectNPCFirst: '最初にNPCを選択してください',
npcNoProbes: 'NPCには偵察プローブがありません',
npcNoSpyReport: 'NPCは最初に偵察する必要があります',
npcMissionFailed: 'ミッションの作成に失敗しました',
npcNoPlanets: 'NPCに惑星がありません',
npcWillSpyIn5s: '{npcName}は5秒後に偵察します',
npcWillAttackIn5s: '{npcName}は5秒後に攻撃します',
npcWillSpyAndAttack: '{npcName}は5秒後に偵察し、10秒後に攻撃します',
acceleratedMissions: '{count}個のミッションを5秒後に加速しました',
npcFleetInitialized: '{npcName}艦隊が初期化されました',
npcFleetDetails: '100 偵察プローブ\n500 軽戦闘機\n300 重戦闘機\n200 巡洋艦\n100 戦艦\n50 爆撃機\n30 駆逐艦\n20 巡洋戦艦',
dangerZone: '危険ゾーン',
dangerZoneDesc: '以下の操作は元に戻せません',
resetGame: 'ゲームをリセット',
@@ -789,9 +838,32 @@ export default {
events: {
gift: 'ギフト送信',
attack: '攻撃',
missileAttack: 'ミサイル攻撃',
allyAttacked: '同盟が攻撃された',
spy: '諜報活動',
stealDebris: '残骸を略奪'
},
reports: {
giftedResources: '{metal}M {crystal}C {deuterium}Dを贈呈',
receivedGiftFromPlayer: 'プレイヤーからギフトを受け取りました',
giftedToNpc: '{npcName}にリソースを贈呈しました。評判+{reputation}',
rejectedPlayerGift: 'プレイヤーのギフトを拒否しました',
npcRejectedGift: '{npcName}があなたのギフトを拒否しました。評判{reputation}',
attackedNpc: '{npcName}を攻撃しました',
wasAttackedByPlayer: 'プレイヤーに攻撃されました',
youAttackedNpc: 'あなたは{npcName}を攻撃しました',
playerAttackedAlly: 'プレイヤーが同盟{allyName}を攻撃しました',
allyDispleased: '{allyName}はあなたが同盟{targetName}を攻撃したことに不満です',
wasSpiedByPlayer: 'プレイヤーに偵察されました(発見:{detected}',
spyDetected: 'あなたの偵察が{npcName}に発見されました',
stoleDebrisFromTerritory: '{npcName}の領域から残骸を略奪しました',
playerStoleDebris: 'プレイヤーが領域から残骸を略奪しました',
recycledDebrisNearNpc: '{npcName}の惑星近くで残骸を回収しました。彼らは不満です。',
giftedResourcesToPlayer: 'プレイヤーにリソースを贈呈しました',
receivedGiftFromNpc: '{npcName}からギフトを受け取りました',
acceptedGiftFromNpc: '{npcName}からのギフトを受け取りました:{metal}M {crystal}C {deuterium}D',
playerRejectedGift: 'プレイヤーがギフトを拒否しました',
rejectedGiftFromNpc: '{npcName}からのギフトを拒否しました。評判{reputation}'
}
},
pagination: {
@@ -800,5 +872,10 @@ export default {
first: '最初',
last: '最後',
page: '{page}ページ'
},
notFound: {
title: 'ページが見つかりません',
description: '申し訳ございません。お探しのページは存在しません',
goHome: 'ホームに戻る'
}
}

View File

@@ -33,13 +33,19 @@ export default {
viewRequirements: '요구사항 보기',
requirementsNotMet: '요구사항 미충족',
current: '현재',
level: '레벨'
level: '레벨',
gmModeActivated: 'GM 모드가 활성화되었습니다! 탐색 메뉴를 확인하세요.'
},
errors: {
requirementsNotMet: '전제 조건 미충족',
insufficientResources: '자원 부족',
insufficientFleetStorage: '함대 저장소 부족',
shieldDomeLimit: '실드 돔 한도 도달',
missileSiloLimit: '미사일 사일로 용량 초과',
insufficientMissiles: '행성간 미사일 부족',
invalidMissileCount: '잘못된 미사일 수량',
targetOutOfRange: '목표가 사정거리 밖',
cannotAttackOwnPlanet: '자신의 행성 공격 불가',
fleetMissionsFull: '함대 임무 슬롯 가득 참',
insufficientFleet: '함대 부족',
insufficientFuel: '연료 부족',
@@ -166,8 +172,8 @@ export default {
darkMatterCollector: '희귀한 암흑 물질 자원 수집',
darkMatterTank: '암흑 물질 저장 용량 증가',
missileSilo: '미사일을 저장 및 발사, 레벨당 10발',
terraformer: '행성 지형 개조, 레벨당 가용 공간 5 증가',
lunarBase: '달 가용 공간 증가, 레벨당 +5 공간',
terraformer: '행성 지형 개조, 레벨당 가용 공간 30 증가',
lunarBase: '달 가용 공간 증가, 레벨당 +30 공간',
sensorPhalanx: '주변 행성계의 함대 활동 감지',
jumpGate: '다른 위성으로 함대 순간 이동',
planetDestroyerFactory: '행성을 파괴할 수 있는 궁극 병기 건조'
@@ -283,7 +289,7 @@ export default {
impulseDrive: '중급 추진 기술',
hyperspaceDrive: '고급 추진 기술',
darkMatterTechnology: '암흑 물질의 성질과 응용 연구',
terraformingTechnology: '행성 지형 개조 기술 연구, 레벨당 모든 행성의 가용 공간 3 증가',
terraformingTechnology: '행성 지형 개조 기술 연구, 레벨당 모든 행성의 가용 공간 30 증가',
planetDestructionTech: '행성 전체를 파괴하는 공포의 기술 연구'
},
officers: {
@@ -316,6 +322,7 @@ export default {
cancelResearch: '연구 취소',
confirmCancel: '취소하시겠습니까? 자원의 50%가 환불됩니다.',
level: '레벨',
gmModeActivated: '',
upgradeToLevel: '레벨로 업그레이드'
},
overview: {
@@ -336,6 +343,7 @@ export default {
usedSpace: '사용된 공간',
spaceUsage: '공간 사용',
level: '레벨',
gmModeActivated: '',
upgradeCost: '업그레이드 비용',
buildTime: '건설 시간',
upgrade: '업그레이드',
@@ -347,8 +355,8 @@ export default {
demolishRefund: '철거 환불',
demolishFailed: '철거 실패',
demolishFailedMessage: '이 건물을 철거할 수 없습니다. 건설 대기열이 가득 찼거나 건물 레벨이 0인지 확인하세요.',
confirmDemolish: '',
confirmDemolishMessage: ''
confirmDemolish: '철거 확인',
confirmDemolishMessage: '다음 건물을 철거하시겠습니까?'
},
researchView: {
title: '연구',
@@ -360,6 +368,7 @@ export default {
},
shipyard: {
attack: '공격력',
missileAttack: '미사일 공격',
shield: '쉴드',
armor: '장갑',
speed: '속도',
@@ -377,6 +386,7 @@ export default {
title: '조선소',
fleetStorage: '함대 저장소',
attack: '공격력',
missileAttack: '미사일 공격',
shield: '실드',
speed: '속도',
cargoCapacity: '적재량',
@@ -391,6 +401,7 @@ export default {
},
defense: {
attack: '공격력',
missileAttack: '미사일 공격',
shield: '쉴드',
armor: '장갑',
buildCost: '건설 비용',
@@ -404,6 +415,7 @@ export default {
defenseView: {
title: '방어 시설',
attack: '공격력',
missileAttack: '미사일 공격',
shield: '실드',
armor: '장갑',
buildTime: '건설 시간',
@@ -413,6 +425,7 @@ export default {
totalCost: '총 비용',
build: '건조',
shieldDomeBuilt: '실드 돔이 이미 건설됨',
missileCapacity: '미사일 용량',
inputError: '입력 오류',
inputErrorMessage: '건조 수량을 입력하세요!',
buildFailed: '건조 실패',
@@ -425,6 +438,7 @@ export default {
flightMissions: '비행 임무',
currentPlanetFleet: '현재 행성 함대',
attack: '공격',
missileAttack: '미사일 공격',
shield: '실드',
armor: '장갑',
speed: '속도',
@@ -519,26 +533,37 @@ export default {
selectSystem: '행성계 선택',
view: '보기',
myPlanet: '내 행성',
myPlanets: '내 행성',
myPlanets: '내 행성계 보기',
npcPlanets: 'NPC 행성들',
selectPlanetToView: '행성 선택',
selectPlanetToView: '행성 선택하여 행성계 보기',
totalPositions: '총 10개 행성 위치',
mine: '내 것',
hostile: '적대',
emptySlot: '빈 자리 - 식민 가능',
scout: '정찰',
attack: '공격',
missileAttack: '미사일 공격',
colonize: '식민',
switch: '전환',
recycle: '회수',
debrisField: '잔해 필드',
scoutPlanetTitle: '행성 정찰',
attackPlanetTitle: '행성 공격',
missileAttackTitle: '미사일 공격',
colonizePlanetTitle: '행성 식민',
recyclePlanetTitle: '잔해 회수',
scoutPlanetMessage:
'행성 [{coordinates}]을(를) 정찰하기 위해 정찰기를 보내시겠습니까?\n\n함대 페이지로 이동하여 함선을 선택하고 파견하세요.',
attackPlanetMessage: '행성 [{coordinates}]을(를) 공격하시겠습니까?\n\n함대 페이지로 이동하여 함선을 선택하고 파견하세요.',
missileAttackMessage: '행성 [{coordinates}]에 행성간 미사일 발사',
missileCount: '미사일 수량',
availableMissiles: '사용 가능한 미사일',
missileRange: '미사일 사정거리',
systems: '시스템',
distance: '거리',
flightTime: '비행 시간',
launchMissile: '발사',
cancel: '취소',
colonizePlanetMessage: '위치 [{coordinates}]을(를) 식민하시겠습니까?\n\n함대 페이지로 이동하여 식민선을 파견하세요.',
recyclePlanetMessage: '위치 [{coordinates}]의 잔해를 회수하시겠습니까?\n\n함대 페이지로 이동하여 회수선을 파견하세요.',
sendGift: '선물 보내기',
@@ -691,10 +716,24 @@ export default {
gamePaused: '게임이 일시정지되었습니다',
gameResumed: '게임이 재개되었습니다',
playerName: '플레이어 이름',
gameSpeed: '게임 속도',
gameSpeedDesc: '현재 게임 속도 배율',
gameSpeed: '자원 생산 속도',
gameSpeedDesc: '현재 자원 생산 속도 배율',
speedChanged: '자원 생산 속도가 {speed}x로 변경되었습니다',
speedReset: '자원 생산 속도가 1x로 재설정되었습니다',
reset: '재설정',
about: '정보',
version: '버전',
latestVersion: '최신 버전',
checkUpdate: '업데이트 확인',
checking: '확인 중...',
newVersionAvailable: '새 버전 {version} 사용 가능',
upToDate: '이미 최신 버전입니다',
checkUpdateCooldown: '나중에 다시 시도해주세요 (5분 쿨다운)',
checkUpdateFailed: '업데이트 확인 실패, 네트워크 연결을 확인하세요',
viewUpdate: '업데이트 보기',
updateAvailable: '새 버전이 사용 가능합니다. 릴리스 노트를 보려면 클릭하세요.',
download: '다운로드',
goToDownload: '다운로드로 이동',
buildDate: '빌드 날짜',
community: '커뮤니티',
github: 'GitHub 저장소',
@@ -733,12 +772,22 @@ export default {
testSpy: '정찰 테스트',
testAttack: '공격 테스트',
testSpyAndAttack: '정찰 & 공격 테스트',
initializeFleet: 'NPC 함대 초기화',
testSpyMessage: '확인을 클릭하여 정찰 임무를 가속화',
testAttackMessage: '확인을 클릭하여 공격 임무를 가속화',
testSpyAndAttackMessage: '확인을 클릭하여 임무를 가속화',
initializeFleet: 'NPC 함대 초기化',
accelerateMissions: '모든 임무 가속(5초)',
selectNPCFirst: '먼저 NPC를 선택하세요',
npcNoProbes: 'NPC에 정찰 프로브가 없습니다',
npcNoSpyReport: 'NPC가 먼저 정찰해야 합니다',
npcMissionFailed: '임무 생성 실패',
npcNoPlanets: 'NPC에 행성이 없습니다',
npcWillSpyIn5s: '{npcName}이(가) 5초 후에 정찰합니다',
npcWillAttackIn5s: '{npcName}이(가) 5초 후에 공격합니다',
npcWillSpyAndAttack: '{npcName}이(가) 5초 후에 정찰하고 10초 후에 공격합니다',
acceleratedMissions: '{count}개의 임무를 5초로 가속화했습니다',
npcFleetInitialized: '{npcName} 함대가 초기화되었습니다',
npcFleetDetails: '100 정찰 프로브\n500 경전투기\n300 중전투기\n200 순양함\n100 전함\n50 폭격기\n30 구축함\n20 순양전함',
dangerZone: '위험 구역',
dangerZoneDesc: '다음 작업은 되돌릴 수 없습니다',
resetGame: '게임 초기화',
@@ -790,9 +839,32 @@ export default {
events: {
gift: '선물 전송',
attack: '공격',
missileAttack: '미사일 공격',
allyAttacked: '동맹 공격당함',
spy: '정찰',
stealDebris: '잔해 약탈'
},
reports: {
giftedResources: '{metal}M {crystal}C {deuterium}D 선물함',
receivedGiftFromPlayer: '플레이어로부터 선물을 받았습니다',
giftedToNpc: '{npcName}에게 자원을 선물했습니다. 평판 +{reputation}',
rejectedPlayerGift: '플레이어의 선물을 거부했습니다',
npcRejectedGift: '{npcName}이(가) 당신의 선물을 거부했습니다. 평판 {reputation}',
attackedNpc: '{npcName}을(를) 공격했습니다',
wasAttackedByPlayer: '플레이어에게 공격당했습니다',
youAttackedNpc: '당신은 {npcName}을(를) 공격했습니다',
playerAttackedAlly: '플레이어가 동맹 {allyName}을(를) 공격했습니다',
allyDispleased: '{allyName}은(는) 당신이 동맹 {targetName}을(를) 공격한 것에 불만입니다',
wasSpiedByPlayer: '플레이어에게 정찰당했습니다 (발견: {detected})',
spyDetected: '당신의 정찰이 {npcName}에게 발견되었습니다',
stoleDebrisFromTerritory: '{npcName}의 영역에서 잔해를 약탈했습니다',
playerStoleDebris: '플레이어가 영역에서 잔해를 약탈했습니다',
recycledDebrisNearNpc: '{npcName}의 행성 근처에서 잔해를 수집했습니다. 그들은 불만족스러워합니다.',
giftedResourcesToPlayer: '플레이어에게 자원을 선물했습니다',
receivedGiftFromNpc: '{npcName}로부터 선물을 받았습니다',
acceptedGiftFromNpc: '{npcName}의 선물을 받았습니다: {metal}M {crystal}C {deuterium}D',
playerRejectedGift: '플레이어가 선물을 거부했습니다',
rejectedGiftFromNpc: '{npcName}의 선물을 거부했습니다. 평판 {reputation}'
}
},
pagination: {
@@ -801,5 +873,10 @@ export default {
first: '처음',
last: '마지막',
page: '{page}페이지'
},
notFound: {
title: '페이지를 찾을 수 없습니다',
description: '죄송합니다. 찾으시는 페이지가 존재하지 않습니다',
goHome: '홈으로 이동'
}
}

View File

@@ -33,13 +33,19 @@ export default {
viewRequirements: 'Просмотр требований',
requirementsNotMet: 'Требования не выполнены',
current: 'Текущий',
level: 'Уровень'
level: 'Уровень',
gmModeActivated: 'Режим GM активирован! Проверьте навигационное меню.'
},
errors: {
requirementsNotMet: 'Требования не выполнены',
insufficientResources: 'Недостаточно ресурсов',
insufficientFleetStorage: 'Недостаточно места для флота',
shieldDomeLimit: 'Достигнут лимит щитовых куполов',
missileSiloLimit: 'Превышена вместимость ракетной шахты',
insufficientMissiles: 'Недостаточно межпланетных ракет',
invalidMissileCount: 'Неверное количество ракет',
targetOutOfRange: 'Цель вне дальности',
cannotAttackOwnPlanet: 'Нельзя атаковать свою планету',
fleetMissionsFull: 'Слоты миссий флота заполнены',
insufficientFleet: 'Недостаточно флота',
insufficientFuel: 'Недостаточно топлива',
@@ -166,8 +172,8 @@ export default {
darkMatterCollector: 'Собирает редкие ресурсы тёмной материи',
darkMatterTank: 'Увеличивает ёмкость хранилища тёмной материи',
missileSilo: 'Хранит и запускает ракеты, 10 ракет на уровень',
terraformer: 'Терраформирует поверхность планеты, увеличивает доступное пространство на 5 за уровень',
lunarBase: 'Увеличивает доступное пространство на луне, +5 пространства за уровень',
terraformer: 'Терраформирует поверхность планеты, увеличивает доступное пространство на 30 за уровень',
lunarBase: 'Увеличивает доступное пространство на луне, +30 пространства за уровень',
sensorPhalanx: 'Обнаруживает активность флота в окружающих системах',
jumpGate: 'Мгновенно переносит флоты на другие луны',
planetDestroyerFactory: 'Производит абсолютное оружие, способное уничтожать планеты'
@@ -284,7 +290,7 @@ export default {
hyperspaceDrive: 'Продвинутая технология двигателей',
darkMatterTechnology: 'Исследование свойств и применения тёмной материи',
terraformingTechnology:
'Исследование технологии терраформирования планет, увеличивает доступное пространство всех планет на 3 за уровень',
'Исследование технологии терраформирования планет, увеличивает доступное пространство всех планет на 30 за уровень',
planetDestructionTech: 'Исследование ужасающей технологии уничтожения целых планет'
},
officers: {
@@ -317,6 +323,7 @@ export default {
cancelResearch: 'Отменить исследование',
confirmCancel: 'Вы уверены, что хотите отменить? 50% ресурсов будет возвращено.',
level: 'Уровень',
gmModeActivated: '',
upgradeToLevel: 'Улучшить до уровня'
},
overview: {
@@ -337,6 +344,7 @@ export default {
usedSpace: 'Использовано полей',
spaceUsage: 'Использование полей',
level: 'Уровень',
gmModeActivated: '',
upgradeCost: 'Стоимость улучшения',
buildTime: 'Время строительства',
upgrade: 'Улучшить',
@@ -348,8 +356,8 @@ export default {
demolishRefund: 'Возврат от сноса',
demolishFailed: 'Снос не удался',
demolishFailedMessage: 'Невозможно снести это здание. Проверьте, не заполнена ли очередь строительства или уровень здания не равен 0.',
confirmDemolish: '',
confirmDemolishMessage: ''
confirmDemolish: 'Подтвердить снос',
confirmDemolishMessage: 'Вы уверены, что хотите снести следующее здание?'
},
researchView: {
title: 'Исследования',
@@ -362,6 +370,7 @@ export default {
},
shipyard: {
attack: 'Атака',
missileAttack: 'Ракетная атака',
shield: 'Щит',
armor: 'Броня',
speed: 'Скорость',
@@ -379,6 +388,7 @@ export default {
title: 'Верфь',
fleetStorage: 'Хранилище флота',
attack: 'Атака',
missileAttack: 'Ракетная атака',
shield: 'Щит',
speed: 'Скорость',
cargoCapacity: 'Грузоподъёмность',
@@ -393,6 +403,7 @@ export default {
},
defense: {
attack: 'Атака',
missileAttack: 'Ракетная атака',
shield: 'Щит',
armor: 'Броня',
buildCost: 'Стоимость постройки',
@@ -406,6 +417,7 @@ export default {
defenseView: {
title: 'Оборона',
attack: 'Атака',
missileAttack: 'Ракетная атака',
shield: 'Щит',
armor: 'Броня',
buildTime: 'Время постройки',
@@ -415,6 +427,7 @@ export default {
totalCost: 'Общая стоимость',
build: 'Построить',
shieldDomeBuilt: 'Щитовой купол уже построен',
missileCapacity: 'Вместимость ракет',
inputError: 'Ошибка ввода',
inputErrorMessage: 'Пожалуйста, введите количество для постройки!',
buildFailed: 'Постройка не удалась',
@@ -428,6 +441,7 @@ export default {
flightMissions: 'Полетные миссии',
currentPlanetFleet: 'Флот на этой планете',
attack: 'Атака',
missileAttack: 'Ракетная атака',
shield: 'Щит',
armor: 'Броня',
speed: 'Скорость',
@@ -522,27 +536,38 @@ export default {
selectSystem: 'Выбрать систему',
view: 'Показать',
myPlanet: 'Моя планета',
myPlanets: 'Мои планеты',
myPlanets: 'Просмотр моих систем',
npcPlanets: 'Планеты NPC',
selectPlanetToView: 'Выберите планету для просмотра',
selectPlanetToView: 'Выберите планету для просмотра её системы',
totalPositions: 'Всего 10 позиций планет',
mine: 'Моя',
hostile: 'Враждебная',
emptySlot: 'Пусто - можно колонизировать',
scout: 'Разведка',
attack: 'Атака',
missileAttack: 'Ракетная атака',
colonize: 'Колонизация',
switch: 'Переключить',
recycle: 'Переработка',
debrisField: 'Поле обломков',
scoutPlanetTitle: 'Разведать планету',
attackPlanetTitle: 'Атаковать планету',
missileAttackTitle: 'Ракетная атака',
colonizePlanetTitle: 'Колонизировать планету',
recyclePlanetTitle: 'Переработать обломки',
scoutPlanetMessage:
'Вы уверены, что хотите отправить шпионские зонды для разведки планеты [{coordinates}]?\n\nПерейдите на страницу флота, чтобы выбрать корабли и отправить.',
attackPlanetMessage:
'Вы уверены, что хотите атаковать планету [{coordinates}]?\n\nПерейдите на страницу флота, чтобы выбрать корабли и отправить.',
missileAttackMessage: 'Запустить межпланетные ракеты по планете [{coordinates}]',
missileCount: 'Количество ракет',
availableMissiles: 'Доступно ракет',
missileRange: 'Дальность ракет',
systems: 'систем',
distance: 'Расстояние',
flightTime: 'Время полета',
launchMissile: 'Запустить',
cancel: 'Отмена',
colonizePlanetMessage:
'Вы уверены, что хотите колонизировать позицию [{coordinates}]?\n\nПерейдите на страницу флота, чтобы отправить колонизационный корабль.',
recyclePlanetMessage:
@@ -698,10 +723,24 @@ export default {
gamePaused: 'Игра приостановлена',
gameResumed: 'Игра возобновлена',
playerName: 'Имя игрока',
gameSpeed: 'Скорость игры',
gameSpeedDesc: 'Текущий множитель скорости игры',
gameSpeed: 'Скорость производства ресурсов',
gameSpeedDesc: 'Текущий множитель скорости производства ресурсов',
speedChanged: 'Скорость производства ресурсов изменена на {speed}x',
speedReset: 'Скорость производства ресурсов сброшена на 1x',
reset: 'Сбросить',
about: 'О программе',
version: 'Версия',
latestVersion: 'Последняя версия',
checkUpdate: 'Проверить обновление',
checking: 'Проверка...',
newVersionAvailable: 'Доступна новая версия {version}',
upToDate: 'Уже актуальная версия',
checkUpdateCooldown: 'Пожалуйста, попробуйте позже (5 минут перезарядки)',
checkUpdateFailed: 'Не удалось проверить обновления, проверьте подключение к Интернету',
viewUpdate: 'Просмотреть обновление',
updateAvailable: 'Доступна новая версия. Нажмите, чтобы просмотреть примечания к выпуску.',
download: 'Скачать',
goToDownload: 'Перейти к загрузке',
buildDate: 'Дата сборки',
community: 'Сообщество',
github: 'Репозиторий GitHub',
@@ -740,12 +779,22 @@ export default {
testSpy: 'Тест разведки',
testAttack: 'Тест атаки',
testSpyAndAttack: 'Тест разведки и атаки',
testSpyMessage: 'Нажмите подтвердить, чтобы ускорить миссию разведки',
testAttackMessage: 'Нажмите подтвердить, чтобы ускорить миссию атаки',
testSpyAndAttackMessage: 'Нажмите подтвердить, чтобы ускорить миссии',
initializeFleet: 'Инициализировать флот NPC',
accelerateMissions: 'Ускорить все миссии (5с)',
selectNPCFirst: 'Сначала выберите NPC',
npcNoProbes: 'У NPC нет шпионских зондов',
npcNoSpyReport: 'NPC нужно сначала разведать',
npcMissionFailed: 'Не удалось создать миссию',
npcNoPlanets: 'У NPC нет планет',
npcWillSpyIn5s: '{npcName} проведет разведку через 5 секунд',
npcWillAttackIn5s: '{npcName} атакует через 5 секунд',
npcWillSpyAndAttack: '{npcName} проведет разведку через 5с и атакует через 10с',
acceleratedMissions: 'Ускорено {count} миссий до 5 секунд',
npcFleetInitialized: 'Флот {npcName} инициализирован',
npcFleetDetails: '100 шпионских зондов\n500 легких истребителей\n300 тяжелых истребителей\n200 крейсеров\n100 линкоров\n50 бомбардировщиков\n30 эсминцев\n20 линейных крейсеров',
dangerZone: 'Опасная зона',
dangerZoneDesc: 'Следующие операции необратимы',
resetGame: 'Сбросить игру',
@@ -797,9 +846,32 @@ export default {
events: {
gift: 'Подарок отправлен',
attack: 'Атака',
missileAttack: 'Ракетная атака',
allyAttacked: 'Союзник атакован',
spy: 'Шпионаж',
stealDebris: 'Обломки украдены'
},
reports: {
giftedResources: 'Подарено {metal}M {crystal}C {deuterium}D',
receivedGiftFromPlayer: 'Получен подарок от игрока',
giftedToNpc: 'Вы подарили ресурсы {npcName}. Репутация +{reputation}',
rejectedPlayerGift: 'Отклонен подарок игрока',
npcRejectedGift: '{npcName} отклонил ваш подарок. Репутация {reputation}',
attackedNpc: 'Атакован {npcName}',
wasAttackedByPlayer: 'Был атакован игроком',
youAttackedNpc: 'Вы атаковали {npcName}',
playerAttackedAlly: 'Игрок атаковал союзника {allyName}',
allyDispleased: '{allyName} недоволен тем, что вы атаковали их союзника {targetName}',
wasSpiedByPlayer: 'Был разведан игроком (обнаружен: {detected})',
spyDetected: 'Ваш шпионаж был обнаружен {npcName}',
stoleDebrisFromTerritory: 'Украдены обломки с территории {npcName}',
playerStoleDebris: 'Игрок украл обломки с территории',
recycledDebrisNearNpc: 'Вы переработали обломки возле планеты {npcName}. Они недовольны.',
giftedResourcesToPlayer: 'Подарены ресурсы игроку',
receivedGiftFromNpc: 'Получен подарок от {npcName}',
acceptedGiftFromNpc: 'Вы приняли подарок от {npcName}: {metal}M {crystal}C {deuterium}D',
playerRejectedGift: 'Игрок отклонил подарок',
rejectedGiftFromNpc: 'Вы отклонили подарок от {npcName}. Репутация {reputation}'
}
},
pagination: {
@@ -808,5 +880,10 @@ export default {
first: 'Первая',
last: 'Последняя',
page: 'Страница {page}'
},
notFound: {
title: 'Страница не найдена',
description: 'Извините, страница, которую вы ищете, не существует',
goHome: 'На главную'
}
}

View File

@@ -33,13 +33,20 @@ export default {
viewRequirements: '查看前置条件',
requirementsNotMet: '前置条件未满足',
current: '当前',
level: '等级'
level: '等级',
gmModeActivated: 'GM 模式已激活!请查看导航菜单。'
},
errors: {
requirementsNotMet: '不满足前置条件',
insufficientResources: '资源不足',
insufficientFleetStorage: '舰队仓储空间不足',
shieldDomeLimit: '护盾罩数量限制',
missileSiloLimit: '导弹发射井容量不足',
insufficientMissiles: '星际导弹数量不足',
invalidMissileCount: '导弹数量无效',
targetOutOfRange: '目标超出射程',
cannotAttackOwnPlanet: '不能攻击自己的星球',
launchFailed: '发射失败',
fleetMissionsFull: '舰队任务槽位已满',
insufficientFleet: '舰队数量不足',
insufficientFuel: '燃料不足',
@@ -164,8 +171,8 @@ export default {
darkMatterCollector: '收集稀有的暗物质资源',
darkMatterTank: '增加暗物质存储上限',
missileSilo: '存储和发射导弹每级可存储10枚导弹',
terraformer: '改造行星地形,每级增加5个可用空间',
lunarBase: '增加月球可用空间,每级+5空间',
terraformer: '改造行星地形,每级增加30个可用空间',
lunarBase: '增加月球可用空间,每级+30空间',
sensorPhalanx: '侦测周围星系的舰队活动',
jumpGate: '瞬间传送舰队到其他月球',
planetDestroyerFactory: '建造能够摧毁行星的终极武器'
@@ -283,7 +290,7 @@ export default {
impulseDrive: '中级推进技术',
hyperspaceDrive: '高级推进技术',
darkMatterTechnology: '研究暗物质的性质和应用',
terraformingTechnology: '研究行星地形改造技术每级为所有行星增加3个可用空间',
terraformingTechnology: '研究行星地形改造技术每级为所有行星增加30个可用空间',
planetDestructionTech: '研究如何摧毁整个行星的恐怖技术'
},
officers: {
@@ -415,6 +422,7 @@ export default {
totalCost: '总成本',
build: '建造',
shieldDomeBuilt: '护盾罩已建造',
missileCapacity: '导弹容量',
inputError: '输入错误',
inputErrorMessage: '请输入建造数量!',
buildFailed: '建造失败',
@@ -520,15 +528,16 @@ export default {
selectSystem: '选择星系',
view: '查看',
myPlanet: '我的星球',
myPlanets: '我的星',
myPlanets: '查看我的星',
npcPlanets: 'NPC星球',
selectPlanetToView: '选择要查看的星球',
selectPlanetToView: '选择星球以查看其所在星系',
totalPositions: '共10个星球位置',
mine: '我的',
hostile: '敌对',
emptySlot: '空位 - 可殖民',
scout: '侦察',
attack: '攻击',
missileAttack: '导弹攻击',
colonize: '殖民',
switch: '切换',
recycle: '回收',
@@ -537,11 +546,22 @@ export default {
debrisField: '残骸场',
scoutPlanetTitle: '侦察星球',
attackPlanetTitle: '攻击星球',
missileAttackTitle: '导弹攻击',
colonizePlanetTitle: '殖民星球',
recyclePlanetTitle: '回收残骸',
giftPlanetTitle: '赠送礼物',
scoutPlanetMessage: '确定要派遣间谍探测器侦察星球 [{coordinates}] 吗?\n\n请前往舰队页面选择舰船并派遣。',
attackPlanetMessage: '确定要攻击星球 [{coordinates}] 吗?\n\n请前往舰队页面选择舰船并派遣。',
missileAttackMessage: '向星球 [{coordinates}] 发射导弹',
missileCount: '导弹数量',
availableMissiles: '可用导弹',
missileRange: '射程',
systems: '系统',
distance: '距离',
flightTime: '飞行时间',
launchMissile: '发射',
missileLaunched: '导弹已发射',
cancel: '取消',
colonizePlanetMessage: '确定要殖民位置 [{coordinates}] 吗?\n\n请前往舰队页面派遣殖民船。',
recyclePlanetMessage: '确定要回收位置 [{coordinates}] 的残骸吗?\n\n请前往舰队页面派遣回收船。',
giftPlanetMessage: '确定要向星球 [{coordinates}] 赠送资源吗?\n\n请前往舰队页面选择运输船并装载资源。'
@@ -622,7 +642,11 @@ export default {
recycleSuccess: '回收任务成功完成',
recycleFailed: '回收任务失败,目标位置没有残骸',
destroySuccess: '行星毁灭任务成功执行',
destroyFailed: '行星毁灭任务失败'
destroyFailed: '行星毁灭任务失败',
missileAttackSuccess: '导弹攻击成功',
missileAttackFailed: '导弹攻击失败,目标星球不存在',
missileAttackIntercepted: '所有导弹被拦截',
hits: '枚命中'
},
simulatorView: {
title: '战斗模拟器',
@@ -691,10 +715,24 @@ export default {
gamePaused: '游戏已暂停',
gameResumed: '游戏已恢复',
playerName: '玩家名称',
gameSpeed: '游戏速度',
gameSpeedDesc: '当前游戏速度倍率',
gameSpeed: '资源产出速度',
gameSpeedDesc: '当前资源产出速度倍率',
speedChanged: '资源产出速度已更改为 {speed}x',
speedReset: '资源产出速度已重置为 1x',
reset: '重置',
about: '关于',
version: '版本',
latestVersion: '最新版本',
checkUpdate: '检查更新',
checking: '检查中...',
newVersionAvailable: '发现新版本 {version}',
upToDate: '已是最新版本',
checkUpdateCooldown: '请稍后再试5分钟冷却时间',
checkUpdateFailed: '检查更新失败,请检查网络连接',
viewUpdate: '查看更新',
updateAvailable: '有新版本可用。点击查看更新内容。',
download: '下载',
goToDownload: '前往下载',
buildDate: '构建日期',
community: '社区',
github: 'GitHub 仓库',
@@ -733,12 +771,22 @@ export default {
testSpy: '测试侦查',
testAttack: '测试攻击',
testSpyAndAttack: '测试侦查&攻击',
testSpyMessage: '点击确认以加速侦查任务',
testAttackMessage: '点击确认以加速攻击任务',
testSpyAndAttackMessage: '点击确认以加速任务执行',
initializeFleet: '初始化NPC舰队',
accelerateMissions: '加速所有任务(5秒)',
selectNPCFirst: '请先选择一个NPC',
npcNoProbes: 'NPC没有间谍探测器',
npcNoSpyReport: 'NPC需要先侦查',
npcMissionFailed: '创建任务失败',
npcNoPlanets: 'NPC没有星球',
npcWillSpyIn5s: '{npcName}将在5秒后侦查',
npcWillAttackIn5s: '{npcName}将在5秒后攻击',
npcWillSpyAndAttack: '{npcName}将在5秒后侦查10秒后攻击',
acceleratedMissions: '已加速{count}个任务至5秒后',
npcFleetInitialized: '{npcName}舰队已初始化',
npcFleetDetails: '100 间谍探测器\n500 轻型战机\n300 重型战机\n200 巡洋舰\n100 战列舰\n50 轰炸机\n30 毁灭者\n20 战列巡洋舰',
dangerZone: '危险区域',
dangerZoneDesc: '以下操作不可撤销,请谨慎操作',
resetGame: '重置游戏',
@@ -793,6 +841,28 @@ export default {
allyAttacked: '攻击盟友',
spy: '侦查',
stealDebris: '抢夺残骸'
},
reports: {
giftedResources: '赠送了 {metal}金属 {crystal}晶体 {deuterium}氘',
receivedGiftFromPlayer: '收到玩家的礼物',
giftedToNpc: '你向{npcName}赠送了资源。好感度+{reputation}',
rejectedPlayerGift: '拒绝了玩家的礼物',
npcRejectedGift: '{npcName}拒绝了你的礼物。好感度{reputation}',
attackedNpc: '攻击了{npcName}',
wasAttackedByPlayer: '被玩家攻击',
youAttackedNpc: '你攻击了{npcName}',
playerAttackedAlly: '玩家攻击了盟友{allyName}',
allyDispleased: '{allyName}对你攻击盟友{targetName}感到不满',
wasSpiedByPlayer: '被玩家侦查(被发现:{detected}',
spyDetected: '你的侦查被{npcName}发现了',
stoleDebrisFromTerritory: '从{npcName}的领地抢夺了残骸',
playerStoleDebris: '玩家从领地抢夺了残骸',
recycledDebrisNearNpc: '你在{npcName}的星球附近回收了残骸。他们很不高兴。',
giftedResourcesToPlayer: '向玩家赠送了资源',
receivedGiftFromNpc: '收到了{npcName}的礼物',
acceptedGiftFromNpc: '你接受了{npcName}的礼物:{metal}金属 {crystal}晶体 {deuterium}氘',
playerRejectedGift: '玩家拒绝了礼物',
rejectedGiftFromNpc: '你拒绝了{npcName}的礼物。好感度{reputation}'
}
},
pagination: {
@@ -801,5 +871,10 @@ export default {
first: '首页',
last: '末页',
page: '第 {page} 页'
},
notFound: {
title: '页面未找到',
description: '抱歉,您访问的页面不存在',
goHome: '返回首页'
}
}

View File

@@ -33,13 +33,19 @@ export default {
viewRequirements: '查看前置條件',
requirementsNotMet: '前置條件未滿足',
current: '當前',
level: '等級'
level: '等級',
gmModeActivated: 'GM 模式已啟用!請查看導航選單。'
},
errors: {
requirementsNotMet: '不滿足前置條件',
insufficientResources: '資源不足',
insufficientFleetStorage: '艦隊倉儲空間不足',
shieldDomeLimit: '護盾罩數量限制',
missileSiloLimit: '導彈發射井容量已滿',
insufficientMissiles: '星際導彈數量不足',
invalidMissileCount: '導彈數量無效',
targetOutOfRange: '目標超出射程',
cannotAttackOwnPlanet: '不能攻擊自己的星球',
fleetMissionsFull: '艦隊任務槽位已滿',
insufficientFleet: '艦隊數量不足',
insufficientFuel: '燃料不足',
@@ -166,8 +172,8 @@ export default {
darkMatterCollector: '收集稀有的暗物質資源',
darkMatterTank: '增加暗物質儲存上限',
missileSilo: '存儲和發射導彈每級可存儲10枚導彈',
terraformer: '改造行星地形,每級增加5個可用空間',
lunarBase: '增加月球可用空間,每級+5空間',
terraformer: '改造行星地形,每級增加30個可用空間',
lunarBase: '增加月球可用空間,每級+30空間',
sensorPhalanx: '偵測周圍星系的艦隊活動',
jumpGate: '瞬間傳送艦隊到其他月球',
planetDestroyerFactory: '建造能夠摧毀行星的終極武器'
@@ -285,7 +291,7 @@ export default {
impulseDrive: '中級推進技術',
hyperspaceDrive: '高級推進技術',
darkMatterTechnology: '研究暗物質的性質和應用',
terraformingTechnology: '研究行星地形改造技術每級為所有行星增加3個可用空間',
terraformingTechnology: '研究行星地形改造技術每級為所有行星增加30個可用空間',
planetDestructionTech: '研究如何摧毀整個行星的恐怖技術'
},
officers: {
@@ -318,6 +324,7 @@ export default {
cancelResearch: '取消研究',
confirmCancel: '確定要取消嗎將返還50%的資源。',
level: '等級',
gmModeActivated: '',
upgradeToLevel: '升級到等級'
},
overview: {
@@ -338,6 +345,7 @@ export default {
usedSpace: '已用空間',
spaceUsage: '佔用空間',
level: '等級',
gmModeActivated: '',
upgradeCost: '升級消耗',
buildTime: '建造時間',
upgrade: '升級',
@@ -349,8 +357,8 @@ export default {
demolishRefund: '拆除返還',
demolishFailed: '拆除失敗',
demolishFailedMessage: '無法拆除該建築請檢查建造隊列是否已滿或建築等級是否為0。',
confirmDemolish: '',
confirmDemolishMessage: ''
confirmDemolish: '確認拆除',
confirmDemolishMessage: '確定要拆除以下建築嗎?'
},
researchView: {
title: '研究',
@@ -362,6 +370,7 @@ export default {
},
shipyard: {
attack: '攻擊力',
missileAttack: '導彈攻擊',
shield: '護盾',
armor: '裝甲',
speed: '速度',
@@ -379,6 +388,7 @@ export default {
title: '船塢',
fleetStorage: '艦隊倉儲',
attack: '攻擊力',
missileAttack: '導彈攻擊',
shield: '護盾',
speed: '速度',
cargoCapacity: '載貨量',
@@ -393,6 +403,7 @@ export default {
},
defense: {
attack: '攻擊力',
missileAttack: '導彈攻擊',
shield: '護盾',
armor: '裝甲',
buildCost: '建造成本',
@@ -406,6 +417,7 @@ export default {
defenseView: {
title: '防禦設施',
attack: '攻擊力',
missileAttack: '導彈攻擊',
shield: '護盾',
armor: '裝甲',
buildTime: '建造時間',
@@ -415,6 +427,7 @@ export default {
totalCost: '總成本',
build: '建造',
shieldDomeBuilt: '護盾罩已建造',
missileCapacity: '導彈容量',
inputError: '輸入錯誤',
inputErrorMessage: '請輸入建造數量!',
buildFailed: '建造失敗',
@@ -427,6 +440,7 @@ export default {
flightMissions: '飛行任務',
currentPlanetFleet: '當前星球艦隊',
attack: '攻擊',
missileAttack: '導彈攻擊',
shield: '護盾',
armor: '裝甲',
speed: '速度',
@@ -521,25 +535,36 @@ export default {
selectSystem: '選擇星系',
view: '查看',
myPlanet: '我的星球',
myPlanets: '我的星',
myPlanets: '查看我的星',
npcPlanets: 'NPC星球',
selectPlanetToView: '選擇要查看的星球',
selectPlanetToView: '選擇星球以查看其所在星系',
totalPositions: '共10個星球位置',
mine: '我的',
hostile: '敵對',
emptySlot: '空位 - 可殖民',
scout: '偵察',
attack: '攻擊',
missileAttack: '導彈攻擊',
colonize: '殖民',
switch: '切換',
recycle: '回收',
debrisField: '殘骸場',
scoutPlanetTitle: '偵察星球',
attackPlanetTitle: '攻擊星球',
missileAttackTitle: '導彈攻擊',
colonizePlanetTitle: '殖民星球',
recyclePlanetTitle: '回收殘骸',
scoutPlanetMessage: '確定要派遣間諜探測器偵察星球 [{coordinates}] 嗎?\n\n請前往艦隊頁面選擇艦船並派遣。',
attackPlanetMessage: '確定要攻擊星球 [{coordinates}] 嗎?\n\n請前往艦隊頁面選擇艦船並派遣。',
missileAttackMessage: '向星球 [{coordinates}] 發射星際導彈',
missileCount: '導彈數量',
availableMissiles: '可用導彈',
missileRange: '導彈射程',
systems: '星系',
distance: '距離',
flightTime: '飛行時間',
launchMissile: '發射',
cancel: '取消',
colonizePlanetMessage: '確定要殖民位置 [{coordinates}] 嗎?\n\n請前往艦隊頁面派遣殖民船。',
recyclePlanetMessage: '確定要回收位置 [{coordinates}] 的殘骸嗎?\n\n請前往艦隊頁面派遣回收船。',
sendGift: '贈送禮物',
@@ -692,10 +717,24 @@ export default {
gamePaused: '遊戲已暫停',
gameResumed: '遊戲已恢復',
playerName: '玩家名稱',
gameSpeed: '遊戲速度',
gameSpeedDesc: '目前遊戲速度倍率',
gameSpeed: '資源產出速度',
gameSpeedDesc: '目前資源產出速度倍率',
speedChanged: '資源產出速度已更改為 {speed}x',
speedReset: '資源產出速度已重置為 1x',
reset: '重置',
about: '關於',
version: '版本',
latestVersion: '最新版本',
checkUpdate: '檢查更新',
checking: '檢查中...',
newVersionAvailable: '發現新版本 {version}',
upToDate: '已是最新版本',
checkUpdateCooldown: '請稍後再試5分鐘冷卻時間',
checkUpdateFailed: '檢查更新失敗,請檢查網路連線',
viewUpdate: '查看更新',
updateAvailable: '有新版本可用。點擊查看更新內容。',
download: '下載',
goToDownload: '前往下載',
buildDate: '建置日期',
community: '社群',
github: 'GitHub 儲存庫',
@@ -734,12 +773,22 @@ export default {
testSpy: '測試偵查',
testAttack: '測試攻擊',
testSpyAndAttack: '測試偵查&攻擊',
testSpyMessage: '點擊確認以加速偵查任務',
testAttackMessage: '點擊確認以加速攻擊任務',
testSpyAndAttackMessage: '點擊確認以加速任務執行',
initializeFleet: '初始化NPC艦隊',
accelerateMissions: '加速所有任務(5秒)',
selectNPCFirst: '請先選擇一個NPC',
npcNoProbes: 'NPC沒有間諜探測器',
npcNoSpyReport: 'NPC需要先偵查',
npcMissionFailed: '創建任務失敗',
npcNoPlanets: 'NPC沒有星球',
npcWillSpyIn5s: '{npcName}將在5秒後偵查',
npcWillAttackIn5s: '{npcName}將在5秒後攻擊',
npcWillSpyAndAttack: '{npcName}將在5秒後偵查10秒後攻擊',
acceleratedMissions: '已加速{count}個任務至5秒後',
npcFleetInitialized: '{npcName}艦隊已初始化',
npcFleetDetails: '100 間諜探測器\n500 輕型戰機\n300 重型戰機\n200 巡洋艦\n100 戰列艦\n50 轟炸機\n30 毀滅者\n20 戰列巡洋艦',
dangerZone: '危險區域',
dangerZoneDesc: '以下操作不可撤銷,請謹慎操作',
resetGame: '重置遊戲',
@@ -791,9 +840,32 @@ export default {
events: {
gift: '已贈送禮物',
attack: '攻擊',
missileAttack: '導彈攻擊',
allyAttacked: '盟友被攻擊',
spy: '間諜活動',
stealDebris: '掠奪殘骸'
},
reports: {
giftedResources: '贈送了 {metal}金屬 {crystal}晶體 {deuterium}氘',
receivedGiftFromPlayer: '收到玩家的禮物',
giftedToNpc: '你向{npcName}贈送了資源。好感度+{reputation}',
rejectedPlayerGift: '拒絕了玩家的禮物',
npcRejectedGift: '{npcName}拒絕了你的禮物。好感度{reputation}',
attackedNpc: '攻擊了{npcName}',
wasAttackedByPlayer: '被玩家攻擊',
youAttackedNpc: '你攻擊了{npcName}',
playerAttackedAlly: '玩家攻擊了盟友{allyName}',
allyDispleased: '{allyName}對你攻擊盟友{targetName}感到不滿',
wasSpiedByPlayer: '被玩家偵查(被發現:{detected}',
spyDetected: '你的偵查被{npcName}發現了',
stoleDebrisFromTerritory: '從{npcName}的領地掠奪了殘骸',
playerStoleDebris: '玩家從領地掠奪了殘骸',
recycledDebrisNearNpc: '你在{npcName}的星球附近回收了殘骸。他們很不高興。',
giftedResourcesToPlayer: '向玩家贈送了資源',
receivedGiftFromNpc: '收到了{npcName}的禮物',
acceptedGiftFromNpc: '你接受了{npcName}的禮物:{metal}金屬 {crystal}晶體 {deuterium}氘',
playerRejectedGift: '玩家拒絕了禮物',
rejectedGiftFromNpc: '你拒絕了{npcName}的禮物。好感度{reputation}'
}
},
pagination: {
@@ -802,5 +874,10 @@ export default {
first: '首頁',
last: '末頁',
page: '第 {page} 頁'
},
notFound: {
title: '找不到頁面',
description: '抱歉,您訪問的頁面不存在',
goHome: '返回首頁'
}
}

View File

@@ -1,6 +1,7 @@
import type { Fleet, Resources, BattleResult, Officer, TechnologyType } from '@/types/game'
import { DefenseType, OfficerType } from '@/types/game'
import { workerManager } from '@/workers/workerManager'
import { MOON_CONFIG } from '@/config/gameConfig'
/**
* 执行战斗模拟
@@ -60,7 +61,10 @@ export const simulateBattle = async (
// 计算月球生成概率(根据残骸场总量)
const totalDebris = debrisField.metal + debrisField.crystal
const moonChance = Math.min(totalDebris / 100000, 0.2) // 最高20%概率
const moonChance = Math.min(
(MOON_CONFIG.baseChance + Math.floor(totalDebris / MOON_CONFIG.chancePerDebris)),
MOON_CONFIG.maxChance
) / 100 // 转换为0-1的概率
// 生成战斗报告
const battleResult: BattleResult = {

View File

@@ -3,6 +3,9 @@ import { BuildingType, TechnologyType, ShipType, DefenseType } from '@/types/gam
import { BUILDINGS } from '@/config/gameConfig'
import * as pointsLogic from './pointsLogic'
// 用于生成唯一ID的计数器
let queueIdCounter = 0
/**
* 计算建筑升级成本
*/
@@ -102,8 +105,9 @@ export const checkSpaceAvailable = (planet: Planet, buildingType: BuildingType):
*/
export const createBuildQueueItem = (buildingType: BuildingType, targetLevel: number, buildTime: number): BuildQueueItem => {
const now = Date.now()
queueIdCounter++
return {
id: `build_${now}`,
id: `build_${now}_${queueIdCounter}`,
type: 'building',
itemType: buildingType,
targetLevel,
@@ -212,8 +216,9 @@ export const calculateDemolishTime = (
*/
export const createDemolishQueueItem = (buildingType: BuildingType, currentLevel: number, demolishTime: number): BuildQueueItem => {
const now = Date.now()
queueIdCounter++
return {
id: `demolish_${now}`,
id: `demolish_${now}_${queueIdCounter}`,
type: 'demolish',
itemType: buildingType,
targetLevel: currentLevel - 1, // 目标等级为当前等级-1

View File

@@ -23,6 +23,14 @@ export const validateBuildingUpgrade = (
const cost = buildingLogic.calculateBuildingCost(buildingType, targetLevel)
const buildingConfig = BUILDINGS[buildingType]
// 检查队列中是否已存在该建筑的升级或拆除任务
const existingQueueItem = planet.buildQueue.find(
item => (item.type === 'building' || item.type === 'demolish') && item.itemType === buildingType
)
if (existingQueueItem) {
return { valid: false, reason: 'errors.buildingAlreadyInQueue' }
}
// 检查星球/月球限制
if (buildingConfig.planetOnly && planet.isMoon) {
return { valid: false, reason: 'errors.planetOnly' }

View File

@@ -5,6 +5,7 @@
*/
import { DIPLOMATIC_CONFIG } from '@/config/gameConfig'
import { locales, type Locale } from '@/locales'
import type {
DiplomaticRelation,
RelationStatus,
@@ -21,6 +22,37 @@ import type {
} from '@/types/game'
import { RelationStatus as RS, DiplomaticEventType as DET } from '@/types/game'
/**
* 获取翻译文本的辅助函数
* @param key 翻译键
* @param locale 语言代码
* @param params 参数
* @returns 翻译后的文本
*/
const t = (key: string, locale: Locale, params?: Record<string, string | number>): string => {
const keys = key.split('.')
let value: any = locales[locale]
for (const k of keys) {
if (value && typeof value === 'object' && k in value) {
value = value[k]
} else {
return key // 如果找不到翻译,返回原始 key
}
}
let result = typeof value === 'string' ? value : key
// 替换参数占位符
if (params) {
Object.entries(params).forEach(([paramKey, paramValue]) => {
result = result.replace(new RegExp(`\\{${paramKey}\\}`, 'g'), String(paramValue))
})
}
return result
}
/**
* 根据好感度值计算关系状态
* @param reputation 好感度值 (-100 到 +100)
@@ -118,8 +150,11 @@ export const calculateGiftReputationGain = (resources: Resources): number => {
const baseGain = REPUTATION_CHANGES.GIFT_BASE
const valueGain = Math.floor(totalValue / 1000) * REPUTATION_CHANGES.GIFT_PER_1K_RESOURCES
// 确保达到门槛的礼物至少获得1点好感度
const totalGain = Math.max(baseGain + valueGain, 1)
// 限制在最大值范围内
return Math.min(baseGain + valueGain, REPUTATION_CHANGES.GIFT_MAX_SINGLE)
return Math.min(totalGain, REPUTATION_CHANGES.GIFT_MAX_SINGLE)
}
/**
@@ -173,16 +208,22 @@ export const calculateNPCRejectionProbability = (npc: NPC, player: Player): numb
* @param mission 舰队任务
* @param player 玩家
* @param targetNpc 目标NPC
* @param locale 语言代码
* @returns { accepted: boolean, reputationGain?: number }
*/
export const handleGiftArrival = (mission: FleetMission, player: Player, targetNpc: NPC): { accepted: boolean; reputationGain?: number } => {
export const handleGiftArrival = (
mission: FleetMission,
player: Player,
targetNpc: NPC,
locale: Locale
): { accepted: boolean; reputationGain?: number } => {
// 计算NPC拒绝概率
const rejectionProb = calculateNPCRejectionProbability(targetNpc, player)
const isRejected = Math.random() < rejectionProb
if (isRejected) {
// NPC拒绝礼物
handleGiftRejection(player, targetNpc, mission.cargo)
handleGiftRejection(player, targetNpc, mission.cargo, locale)
return { accepted: false }
}
@@ -200,7 +241,11 @@ export const handleGiftArrival = (mission: FleetMission, player: Player, targetN
relation,
reputationGain,
DET.GiftResources,
`Gifted ${mission.cargo.metal}M ${mission.cargo.crystal}C ${mission.cargo.deuterium}D`
t('diplomacy.reports.giftedResources', locale, {
metal: mission.cargo.metal.toString(),
crystal: mission.cargo.crystal.toString(),
deuterium: mission.cargo.deuterium.toString()
})
)
// 也更新NPC对玩家的关系双向好感度
@@ -209,7 +254,12 @@ export const handleGiftArrival = (mission: FleetMission, player: Player, targetN
}
const npcRelation = getOrCreateRelation(targetNpc.relations, targetNpc.id, player.id)
targetNpc.relations[player.id] = updateReputation(npcRelation, reputationGain, DET.GiftResources, `Received gift from player`)
targetNpc.relations[player.id] = updateReputation(
npcRelation,
reputationGain,
DET.GiftResources,
t('diplomacy.reports.receivedGiftFromPlayer', locale)
)
// 生成外交报告
generateDiplomaticReport(
@@ -217,7 +267,7 @@ export const handleGiftArrival = (mission: FleetMission, player: Player, targetN
targetNpc,
DET.GiftResources,
reputationGain,
`You gifted resources to ${targetNpc.name}. Reputation +${reputationGain}`
t('diplomacy.reports.giftedToNpc', locale, { npcName: targetNpc.name, reputation: reputationGain.toString() })
)
return { accepted: true, reputationGain }
@@ -228,8 +278,9 @@ export const handleGiftArrival = (mission: FleetMission, player: Player, targetN
* @param player 玩家
* @param npc NPC
* @param rejectedResources 被拒绝的资源
* @param locale 语言代码
*/
const handleGiftRejection = (player: Player, npc: NPC, rejectedResources: Resources): void => {
const handleGiftRejection = (player: Player, npc: NPC, rejectedResources: Resources, locale: Locale): void => {
const { GIFT_ACCEPTANCE_CONFIG } = DIPLOMATIC_CONFIG
// 创建拒绝通知
@@ -268,7 +319,7 @@ const handleGiftRejection = (player: Player, npc: NPC, rejectedResources: Resour
npcRelation,
GIFT_ACCEPTANCE_CONFIG.REJECTION_REPUTATION_PENALTY,
DET.GiftResources,
`Rejected player's gift`
t('diplomacy.reports.rejectedPlayerGift', locale)
)
// 生成外交报告
@@ -277,7 +328,10 @@ const handleGiftRejection = (player: Player, npc: NPC, rejectedResources: Resour
npc,
DET.GiftResources,
GIFT_ACCEPTANCE_CONFIG.REJECTION_REPUTATION_PENALTY,
`${npc.name} rejected your gift. Reputation ${GIFT_ACCEPTANCE_CONFIG.REJECTION_REPUTATION_PENALTY}`
t('diplomacy.reports.npcRejectedGift', locale, {
npcName: npc.name,
reputation: GIFT_ACCEPTANCE_CONFIG.REJECTION_REPUTATION_PENALTY.toString()
})
)
}
@@ -287,8 +341,15 @@ const handleGiftRejection = (player: Player, npc: NPC, rejectedResources: Resour
* @param defender 防御者NPC
* @param battleResult 战斗结果
* @param allNpcs 所有NPC列表
* @param locale 语言代码
*/
export const handleAttackReputation = (attacker: Player, defender: NPC, battleResult: BattleResult, allNpcs: NPC[]): void => {
export const handleAttackReputation = (
attacker: Player,
defender: NPC,
battleResult: BattleResult,
allNpcs: NPC[],
locale: Locale
): void => {
const { REPUTATION_CHANGES } = DIPLOMATIC_CONFIG
// 计算好感度降低值
@@ -304,7 +365,12 @@ export const handleAttackReputation = (attacker: Player, defender: NPC, battleRe
}
const relation = getOrCreateRelation(attacker.diplomaticRelations, attacker.id, defender.id)
attacker.diplomaticRelations[defender.id] = updateReputation(relation, -reputationLoss, DET.Attack, `Attacked ${defender.name}`)
attacker.diplomaticRelations[defender.id] = updateReputation(
relation,
reputationLoss,
DET.Attack,
t('diplomacy.reports.attackedNpc', locale, { npcName: defender.name })
)
// 更新被攻击NPC对玩家的关系
if (!defender.relations) {
@@ -312,15 +378,26 @@ export const handleAttackReputation = (attacker: Player, defender: NPC, battleRe
}
const defenderRelation = getOrCreateRelation(defender.relations, defender.id, attacker.id)
defender.relations[attacker.id] = updateReputation(defenderRelation, -reputationLoss, DET.Attack, `Was attacked by player`)
defender.relations[attacker.id] = updateReputation(
defenderRelation,
reputationLoss,
DET.Attack,
t('diplomacy.reports.wasAttackedByPlayer', locale)
)
// 检查盟友关系网络
if (defender.allies && defender.allies.length > 0) {
handleAllyAttackedReputation(attacker, defender, allNpcs)
handleAllyAttackedReputation(attacker, defender, allNpcs, locale)
}
// 生成外交报告
generateDiplomaticReport(attacker, defender, DET.Attack, -reputationLoss, `You attacked ${defender.name}`)
generateDiplomaticReport(
attacker,
defender,
DET.Attack,
-reputationLoss,
t('diplomacy.reports.youAttackedNpc', locale, { npcName: defender.name })
)
}
/**
@@ -328,8 +405,9 @@ export const handleAttackReputation = (attacker: Player, defender: NPC, battleRe
* @param attacker 攻击者(玩家)
* @param attackedNpc 被攻击的NPC
* @param allNpcs 所有NPC列表
* @param locale 语言代码
*/
export const handleAllyAttackedReputation = (attacker: Player, attackedNpc: NPC, allNpcs: NPC[]): void => {
export const handleAllyAttackedReputation = (attacker: Player, attackedNpc: NPC, allNpcs: NPC[], locale: Locale): void => {
const { REPUTATION_CHANGES } = DIPLOMATIC_CONFIG
// 找到所有盟友
@@ -344,9 +422,9 @@ export const handleAllyAttackedReputation = (attacker: Player, attackedNpc: NPC,
const allyRelation = getOrCreateRelation(ally.relations, ally.id, attacker.id)
ally.relations[attacker.id] = updateReputation(
allyRelation,
-REPUTATION_CHANGES.ALLY_ATTACKED,
REPUTATION_CHANGES.ALLY_ATTACKED,
DET.AllyAttacked,
`Player attacked ally ${attackedNpc.name}`
t('diplomacy.reports.playerAttackedAlly', locale, { allyName: attackedNpc.name })
)
// 生成外交报告
@@ -354,8 +432,8 @@ export const handleAllyAttackedReputation = (attacker: Player, attackedNpc: NPC,
attacker,
ally,
DET.AllyAttacked,
-REPUTATION_CHANGES.ALLY_ATTACKED,
`${ally.name} is displeased that you attacked their ally ${attackedNpc.name}`
REPUTATION_CHANGES.ALLY_ATTACKED,
t('diplomacy.reports.allyDispleased', locale, { allyName: ally.name, targetName: attackedNpc.name })
)
})
}
@@ -365,8 +443,9 @@ export const handleAllyAttackedReputation = (attacker: Player, attackedNpc: NPC,
* @param spy 侦查者(玩家)
* @param target 侦查目标NPC
* @param wasDetected 是否被发现
* @param locale 语言代码
*/
export const handleSpyReputation = (spy: Player, target: NPC, wasDetected: boolean): void => {
export const handleSpyReputation = (spy: Player, target: NPC, wasDetected: boolean, locale: Locale): void => {
const { REPUTATION_CHANGES } = DIPLOMATIC_CONFIG
const reputationLoss = wasDetected ? REPUTATION_CHANGES.SPY_DETECTED : REPUTATION_CHANGES.SPY_UNDETECTED
@@ -377,11 +456,16 @@ export const handleSpyReputation = (spy: Player, target: NPC, wasDetected: boole
}
const targetRelation = getOrCreateRelation(target.relations, target.id, spy.id)
target.relations[spy.id] = updateReputation(targetRelation, -reputationLoss, DET.Spy, `Was spied by player (detected: ${wasDetected})`)
target.relations[spy.id] = updateReputation(
targetRelation,
reputationLoss,
DET.Spy,
t('diplomacy.reports.wasSpiedByPlayer', locale, { detected: wasDetected ? 'true' : 'false' })
)
// 如果被发现,生成外交报告
if (wasDetected) {
generateDiplomaticReport(spy, target, DET.Spy, -reputationLoss, `Your espionage was detected by ${target.name}`)
generateDiplomaticReport(spy, target, DET.Spy, reputationLoss, t('diplomacy.reports.spyDetected', locale, { npcName: target.name }))
}
}
@@ -391,8 +475,9 @@ export const handleSpyReputation = (spy: Player, target: NPC, wasDetected: boole
* @param player 玩家
* @param debrisPosition 残骸位置
* @param allNpcs 所有NPC列表
* @param locale 语言代码
*/
export const handleDebrisRecycleReputation = (player: Player, debrisPosition: Position, allNpcs: NPC[]): void => {
export const handleDebrisRecycleReputation = (player: Player, debrisPosition: Position, allNpcs: NPC[], locale: Locale): void => {
const { REPUTATION_CHANGES } = DIPLOMATIC_CONFIG
// 找到该位置的NPC星球所有者
@@ -414,9 +499,9 @@ export const handleDebrisRecycleReputation = (player: Player, debrisPosition: Po
const relation = getOrCreateRelation(player.diplomaticRelations, player.id, npcOwner.id)
player.diplomaticRelations[npcOwner.id] = updateReputation(
relation,
-REPUTATION_CHANGES.STEAL_DEBRIS,
REPUTATION_CHANGES.STEAL_DEBRIS,
DET.StealDebris,
`Stole debris from ${npcOwner.name}'s territory`
t('diplomacy.reports.stoleDebrisFromTerritory', locale, { npcName: npcOwner.name })
)
// 更新NPC对玩家的关系
@@ -427,9 +512,9 @@ export const handleDebrisRecycleReputation = (player: Player, debrisPosition: Po
const npcRelation = getOrCreateRelation(npcOwner.relations, npcOwner.id, player.id)
npcOwner.relations[player.id] = updateReputation(
npcRelation,
-REPUTATION_CHANGES.STEAL_DEBRIS,
REPUTATION_CHANGES.STEAL_DEBRIS,
DET.StealDebris,
`Player stole debris from territory`
t('diplomacy.reports.playerStoleDebris', locale)
)
// 生成外交报告
@@ -437,8 +522,8 @@ export const handleDebrisRecycleReputation = (player: Player, debrisPosition: Po
player,
npcOwner,
DET.StealDebris,
-REPUTATION_CHANGES.STEAL_DEBRIS,
`You recycled debris near ${npcOwner.name}'s planet. They are displeased.`
REPUTATION_CHANGES.STEAL_DEBRIS,
t('diplomacy.reports.recycledDebrisNearNpc', locale, { npcName: npcOwner.name })
)
}
}
@@ -539,8 +624,9 @@ export const handleNPCGiftToPlayer = (npc: NPC, player: Player, giftResources: R
* @param player 玩家
* @param npc NPC
* @param giftNotification 礼物通知
* @param locale 语言代码
*/
export const acceptNPCGift = (player: Player, npc: NPC, giftNotification: GiftNotification): void => {
export const acceptNPCGift = (player: Player, npc: NPC, giftNotification: GiftNotification, locale: Locale): void => {
// 将资源添加到玩家主星球
if (player.planets && player.planets.length > 0) {
const mainPlanet = player.planets[0]
@@ -558,7 +644,12 @@ export const acceptNPCGift = (player: Player, npc: NPC, giftNotification: GiftNo
}
const npcRelation = getOrCreateRelation(npc.relations, npc.id, player.id)
npc.relations[player.id] = updateReputation(npcRelation, giftNotification.expectedReputationGain, DET.GiftResources, `Gifted resources to player`)
npc.relations[player.id] = updateReputation(
npcRelation,
giftNotification.expectedReputationGain,
DET.GiftResources,
t('diplomacy.reports.giftedResourcesToPlayer', locale)
)
// 也更新玩家对NPC的关系收到礼物会增加好感
if (!player.diplomaticRelations) {
@@ -570,7 +661,7 @@ export const acceptNPCGift = (player: Player, npc: NPC, giftNotification: GiftNo
playerRelation,
giftNotification.expectedReputationGain,
DET.GiftResources,
`Received gift from ${npc.name}`
t('diplomacy.reports.receivedGiftFromNpc', locale, { npcName: npc.name })
)
// 生成外交报告
@@ -579,7 +670,12 @@ export const acceptNPCGift = (player: Player, npc: NPC, giftNotification: GiftNo
npc,
DET.GiftResources,
giftNotification.expectedReputationGain,
`You accepted a gift from ${npc.name}: ${giftNotification.resources.metal}M ${giftNotification.resources.crystal}C ${giftNotification.resources.deuterium}D`
t('diplomacy.reports.acceptedGiftFromNpc', locale, {
npcName: npc.name,
metal: giftNotification.resources.metal.toString(),
crystal: giftNotification.resources.crystal.toString(),
deuterium: giftNotification.resources.deuterium.toString()
})
)
// 移除礼物通知
@@ -593,8 +689,9 @@ export const acceptNPCGift = (player: Player, npc: NPC, giftNotification: GiftNo
* @param player 玩家
* @param npc NPC
* @param giftNotification 礼物通知
* @param locale 语言代码
*/
export const rejectNPCGift = (player: Player, npc: NPC, giftNotification: GiftNotification): void => {
export const rejectNPCGift = (player: Player, npc: NPC, giftNotification: GiftNotification, locale: Locale): void => {
const { GIFT_ACCEPTANCE_CONFIG } = DIPLOMATIC_CONFIG
// 拒绝礼物会降低好感度
@@ -607,7 +704,7 @@ export const rejectNPCGift = (player: Player, npc: NPC, giftNotification: GiftNo
npcRelation,
GIFT_ACCEPTANCE_CONFIG.REJECTION_REPUTATION_PENALTY,
DET.GiftResources,
`Player rejected gift`
t('diplomacy.reports.playerRejectedGift', locale)
)
// 生成外交报告
@@ -616,7 +713,10 @@ export const rejectNPCGift = (player: Player, npc: NPC, giftNotification: GiftNo
npc,
DET.GiftResources,
GIFT_ACCEPTANCE_CONFIG.REJECTION_REPUTATION_PENALTY,
`You rejected a gift from ${npc.name}. Reputation ${GIFT_ACCEPTANCE_CONFIG.REJECTION_REPUTATION_PENALTY}`
t('diplomacy.reports.rejectedGiftFromNpc', locale, {
npcName: npc.name,
reputation: GIFT_ACCEPTANCE_CONFIG.REJECTION_REPUTATION_PENALTY.toString()
})
)
// 移除礼物通知

View File

@@ -1,4 +1,5 @@
import type { FleetMission, Planet, Resources, Fleet, BattleResult, SpyReport, Player, Officer, DebrisField, NPC } from '@/types/game'
import type { Locale } from '@/locales'
import { ShipType, DefenseType, MissionType, BuildingType, OfficerType, TechnologyType } from '@/types/game'
import { FLEET_STORAGE_CONFIG } from '@/config/gameConfig'
import * as battleLogic from './battleLogic'
@@ -75,13 +76,14 @@ export const processTransportArrival = (
mission: FleetMission,
targetPlanet: Planet | undefined,
player?: Player,
allNpcs?: NPC[]
allNpcs?: NPC[],
locale: Locale = 'zh-CN'
): { success: boolean; reputationGain?: number } => {
// 检查是否是赠送任务
if (mission.isGift && mission.giftTargetNpcId && player && allNpcs) {
const targetNpc = allNpcs.find(n => n.id === mission.giftTargetNpcId)
if (targetNpc) {
const giftResult = diplomaticLogic.handleGiftArrival(mission, player, targetNpc)
const giftResult = diplomaticLogic.handleGiftArrival(mission, player, targetNpc, locale)
mission.status = 'returning'
// 如果礼物被拒绝,资源返还给玩家
@@ -316,8 +318,8 @@ export const processNPCAttackArrival = async (
* 基于天体物理学技术等级
*/
export const calculateMaxPlanets = (astrophysicsLevel: number): number => {
// 基础1个星球 + 每2级天体物理学增加1个殖民地槽位
return 1 + Math.floor(astrophysicsLevel / 2)
// 基础1个星球(主星) + 每级天体物理学增加1个殖民地槽位
return 1 + astrophysicsLevel
}
/**
@@ -463,7 +465,8 @@ export const processSpyArrival = (
targetPlanet: Planet | undefined,
attacker: Player,
defender: Player | null,
allNpcs?: NPC[]
allNpcs?: NPC[],
locale: Locale = 'zh-CN'
): SpyReport | null => {
if (!targetPlanet) {
mission.status = 'returning'
@@ -504,7 +507,7 @@ export const processSpyArrival = (
if (allNpcs && targetPlanet.ownerId) {
const targetNpc = allNpcs.find(npc => npc.planets.some(p => p.id === targetPlanet.id))
if (targetNpc) {
diplomaticLogic.handleSpyReputation(attacker, targetNpc, wasDetected)
diplomaticLogic.handleSpyReputation(attacker, targetNpc, wasDetected, locale)
}
}
@@ -536,7 +539,8 @@ export const processRecycleArrival = (
mission: FleetMission,
debrisField: DebrisField | undefined,
player?: Player,
allNpcs?: NPC[]
allNpcs?: NPC[],
locale: Locale = 'zh-CN'
): { collectedResources: Pick<Resources, 'metal' | 'crystal'>; remainingDebris: Pick<Resources, 'metal' | 'crystal'> | null } | null => {
if (!debrisField) {
mission.status = 'returning'
@@ -558,6 +562,12 @@ export const processRecycleArrival = (
const totalDebris = debrisField.resources.metal + debrisField.resources.crystal
const collectedAmount = Math.min(totalDebris, availableCapacity)
// 防止除零如果残骸为0直接返回
if (totalDebris === 0) {
mission.status = 'returning'
return null
}
// 按比例收集金属和晶体
const metalRatio = debrisField.resources.metal / totalDebris
const crystalRatio = debrisField.resources.crystal / totalDebris
@@ -577,7 +587,7 @@ export const processRecycleArrival = (
// 检查是否在NPC星球位置回收残骸如果是则降低好感度
if (player && allNpcs && collectedAmount > 0) {
diplomaticLogic.handleDebrisRecycleReputation(player, debrisField.position, allNpcs)
diplomaticLogic.handleDebrisRecycleReputation(player, debrisField.position, allNpcs, locale)
}
return {

View File

@@ -19,6 +19,7 @@ export const initializePlayer = (playerId: string, playerName: string = 'Command
officers: {} as Record<OfficerType, Officer>,
researchQueue: [],
fleetMissions: [],
missileAttacks: [],
battleReports: [],
spyReports: [],
spiedNotifications: [],

207
src/logic/missileLogic.ts Normal file
View File

@@ -0,0 +1,207 @@
/**
* 导弹系统逻辑
* 处理星际导弹攻击、射程计算、拦截等
*/
import type { Planet, MissileAttack, DefenseType, TechnologyType, Position } from '@/types/game'
import { DefenseType as DefenseTypes } from '@/types/game'
/**
* 计算导弹射程(基于脉冲引擎等级)
* 射程 = 5 * impulseDriveLevel - 1系统距离
*/
export const calculateMissileRange = (impulseDriveLevel: number): number => {
if (impulseDriveLevel === 0) return 0
return 5 * impulseDriveLevel - 1
}
/**
* 计算两个位置之间的系统距离
*/
export const calculateSystemDistance = (from: Position, to: Position): number => {
// 如果在不同银河系,距离无限大
if (from.galaxy !== to.galaxy) {
return Infinity
}
// 同一银河系内的系统距离
return Math.abs(from.system - to.system)
}
/**
* 检查目标是否在射程内
*/
export const isTargetInRange = (
originPosition: Position,
targetPosition: Position,
impulseDriveLevel: number
): boolean => {
const range = calculateMissileRange(impulseDriveLevel)
const distance = calculateSystemDistance(originPosition, targetPosition)
return distance <= range
}
/**
* 计算导弹飞行时间(秒)
* 基础飞行时间: 30秒 + 60秒/系统距离
*/
export const calculateMissileFlightTime = (distance: number): number => {
return 30 + distance * 60
}
/**
* 创建导弹攻击任务
*/
export const createMissileAttack = (
playerId: string,
originPlanet: Planet,
targetPosition: Position,
targetPlanetId: string | undefined,
missileCount: number
): MissileAttack => {
const now = Date.now()
const distance = calculateSystemDistance(originPlanet.position, targetPosition)
const flightTime = calculateMissileFlightTime(distance) * 1000 // 转换为毫秒
return {
id: `missile_${now}_${playerId}`,
playerId,
originPlanetId: originPlanet.id,
targetPosition,
targetPlanetId,
missileCount,
launchTime: now,
arrivalTime: now + flightTime,
status: 'flying'
}
}
/**
* 验证导弹发射条件
*/
export const validateMissileLaunch = (
originPlanet: Planet,
targetPosition: Position,
missileCount: number,
technologies: Partial<Record<TechnologyType, number>>
): {
valid: boolean
reason?: string
} => {
// 检查是否有足够的星际导弹
const availableMissiles = originPlanet.defense[DefenseTypes.InterplanetaryMissile] || 0
if (availableMissiles < missileCount) {
return { valid: false, reason: 'errors.insufficientMissiles' }
}
// 检查发射数量
if (missileCount <= 0) {
return { valid: false, reason: 'errors.invalidMissileCount' }
}
// 检查射程
const impulseDriveLevel = technologies['impulseDrive'] || 0
if (!isTargetInRange(originPlanet.position, targetPosition, impulseDriveLevel)) {
return { valid: false, reason: 'errors.targetOutOfRange' }
}
// 不能攻击自己的星球
if (
originPlanet.position.galaxy === targetPosition.galaxy &&
originPlanet.position.system === targetPosition.system &&
originPlanet.position.position === targetPosition.position
) {
return { valid: false, reason: 'errors.cannotAttackOwnPlanet' }
}
return { valid: true }
}
/**
* 执行导弹发射(扣除导弹)
*/
export const executeMissileLaunch = (planet: Planet, missileCount: number): void => {
const currentMissiles = planet.defense[DefenseTypes.InterplanetaryMissile] || 0
planet.defense[DefenseTypes.InterplanetaryMissile] = currentMissiles - missileCount
}
/**
* 计算导弹攻击结果(考虑拦截)
* @returns 实际命中的导弹数量
*/
export const calculateMissileImpact = (
attackingMissiles: number,
defenderPlanet: Planet
): {
missileHits: number
missileIntercepted: number
defenseLosses: Partial<Record<DefenseType, number>>
} => {
const antiBallisticMissiles = defenderPlanet.defense[DefenseTypes.AntiBallisticMissile] || 0
// 反弹道导弹拦截1:1
const intercepted = Math.min(attackingMissiles, antiBallisticMissiles)
const missileHits = attackingMissiles - intercepted
// 计算防御损失
const defenseLosses: Partial<Record<DefenseType, number>> = {}
// 消耗的反弹道导弹
if (intercepted > 0) {
defenseLosses[DefenseTypes.AntiBallisticMissile] = intercepted
}
// 如果有导弹命中,随机摧毁防御设施
if (missileHits > 0) {
const defenseTypes = Object.keys(defenderPlanet.defense) as DefenseType[]
const availableDefenses = defenseTypes.filter(type => {
// 不能摧毁护盾罩和行星护盾
if (
type === DefenseTypes.SmallShieldDome ||
type === DefenseTypes.LargeShieldDome ||
type === DefenseTypes.PlanetaryShield
) {
return false
}
return (defenderPlanet.defense[type] || 0) > 0
})
// 每枚导弹可以摧毁一个防御设施
for (let i = 0; i < missileHits && availableDefenses.length > 0; i++) {
const randomIndex = Math.floor(Math.random() * availableDefenses.length)
const targetDefense = availableDefenses[randomIndex]
if (targetDefense) {
if (!defenseLosses[targetDefense]) {
defenseLosses[targetDefense] = 0
}
defenseLosses[targetDefense]!++
// 如果该类型防御全部摧毁,从可用列表中移除
const remaining = (defenderPlanet.defense[targetDefense] || 0) - (defenseLosses[targetDefense] || 0)
if (remaining <= 0) {
availableDefenses.splice(randomIndex, 1)
}
}
}
}
return {
missileHits,
missileIntercepted: intercepted,
defenseLosses
}
}
/**
* 应用导弹攻击结果到星球
*/
export const applyMissileAttackResult = (
planet: Planet,
defenseLosses: Partial<Record<DefenseType, number>>
): void => {
for (const [defenseType, lossCount] of Object.entries(defenseLosses)) {
const currentCount = planet.defense[defenseType as DefenseType] || 0
planet.defense[defenseType as DefenseType] = Math.max(0, currentCount - lossCount)
}
}

View File

@@ -97,8 +97,8 @@ export const shouldNPCSpyPlayer = (npc: NPC, player: Player, currentTime: number
return Math.random() < 0.5
}
if (relation.status === RelationStatus.Hostile) {
// 敌对NPC侦查频率提高50%
return Math.random() < 1.5
// 敌对NPC必定侦查
return true
}
}
@@ -695,7 +695,7 @@ export const forceNPCSpyAndAttack = (
return { spyMission, attackMission: null }
}
const { spyReport } = processNPCSpyArrival(npc, spyMission, targetPlanet, player)
const { spyReport, spiedNotification } = processNPCSpyArrival(npc, spyMission, targetPlanet, player)
// 保存侦查报告到NPC
if (!npc.playerSpyReports) {
@@ -703,6 +703,12 @@ export const forceNPCSpyAndAttack = (
}
npc.playerSpyReports[targetPlanet.id] = spyReport
// 添加被侦查通知给玩家
if (!player.spiedNotifications) {
player.spiedNotifications = []
}
player.spiedNotifications.push(spiedNotification)
// 3. 立即发起攻击
const attackMission = forceNPCAttackPlayer(npc, player, allPlanets, targetPlanetIndex)
@@ -713,7 +719,7 @@ export const forceNPCSpyAndAttack = (
* 测试函数:加速舰队任务到达时间
* 将任务的到达时间设置为现在+指定秒数
*/
export const accelerateNPCMission = (npc: NPC, missionId: string, arriveInSeconds = 5): boolean => {
export const accelerateNPCMission = (npc: NPC, missionId: string, arriveInSeconds = 5, player?: Player): boolean => {
if (!npc.fleetMissions) {
console.error('[Test] NPC has no fleet missions')
return false
@@ -726,7 +732,19 @@ export const accelerateNPCMission = (npc: NPC, missionId: string, arriveInSecond
}
const now = Date.now()
mission.arrivalTime = now + arriveInSeconds * 1000
const flightTime = arriveInSeconds * 1000 // 飞行时间(毫秒)
// 同时修改 departureTime 和 arrivalTime保持飞行时间为指定秒数
mission.departureTime = now
mission.arrivalTime = now + flightTime
// 同时更新对应的 IncomingFleetAlert
if (player && player.incomingFleetAlerts) {
const alert = player.incomingFleetAlerts.find(a => a.id === missionId)
if (alert) {
alert.arrivalTime = mission.arrivalTime
}
}
return true
}
@@ -734,21 +752,34 @@ export const accelerateNPCMission = (npc: NPC, missionId: string, arriveInSecond
/**
* 测试函数加速所有NPC舰队任务
*/
export const accelerateAllNPCMissions = (npc: NPC, arriveInSeconds = 5): number => {
export const accelerateAllNPCMissions = (npc: NPC, arriveInSeconds = 5, player?: Player): number => {
if (!npc.fleetMissions) {
console.error('[Test] NPC has no fleet missions')
return 0
}
const now = Date.now()
const flightTime = arriveInSeconds * 1000
let count = 0
npc.fleetMissions.forEach(mission => {
if (mission.status === 'outbound') {
mission.arrivalTime = now + arriveInSeconds * 1000
// 同时修改 departureTime 和 arrivalTime
mission.departureTime = now
mission.arrivalTime = now + flightTime
// 同时更新对应的 IncomingFleetAlert
if (player && player.incomingFleetAlerts) {
const alert = player.incomingFleetAlerts.find(a => a.id === mission.id)
if (alert) {
alert.arrivalTime = mission.arrivalTime
}
}
count++
} else if (mission.status === 'returning' && mission.returnTime) {
mission.returnTime = now + arriveInSeconds * 1000
// 对于返回任务,保持原来的逻辑
mission.returnTime = now + flightTime
count++
}
})

View File

@@ -3,6 +3,9 @@ import { TechnologyType, BuildingType } from '@/types/game'
import { TECHNOLOGIES } from '@/config/gameConfig'
import * as pointsLogic from './pointsLogic'
// 用于生成唯一ID的计数器
let researchQueueIdCounter = 0
/**
* 计算科技研究成本
*/
@@ -77,8 +80,9 @@ export const checkTechnologyRequirements = (
*/
export const createResearchQueueItem = (techType: TechnologyType, targetLevel: number, researchTime: number): BuildQueueItem => {
const now = Date.now()
researchQueueIdCounter++
return {
id: `research_${now}`,
id: `research_${now}_${researchQueueIdCounter}`,
type: 'technology',
itemType: techType,
targetLevel,

View File

@@ -21,6 +21,14 @@ export const validateTechnologyResearch = (
const targetLevel = currentLevel + 1
const cost = researchLogic.calculateTechnologyCost(techType, targetLevel)
// 检查队列中是否已存在该科技的研究任务
const existingQueueItem = researchQueue.find(
item => item.type === 'technology' && item.itemType === techType
)
if (existingQueueItem) {
return { valid: false, reason: 'errors.technologyAlreadyInQueue' }
}
// 检查研究队列是否已满
const maxQueue = publicLogic.getMaxResearchQueue(technologies)
if (researchQueue.length >= maxQueue) {

View File

@@ -2,6 +2,9 @@ import type { Resources, BuildQueueItem, Fleet } from '@/types/game'
import { ShipType, DefenseType, BuildingType, TechnologyType } from '@/types/game'
import { SHIPS, DEFENSES } from '@/config/gameConfig'
// 用于生成唯一ID的计数器
let shipQueueIdCounter = 0
/**
* 计算舰船建造成本
*/
@@ -153,13 +156,52 @@ export const checkShieldDomeLimit = (
return true
}
/**
* 计算导弹发射井容量
*/
export const calculateMissileSiloCapacity = (buildings: Partial<Record<BuildingType, number>>): number => {
const siloLevel = buildings[BuildingType.MissileSilo] || 0
return siloLevel * 10 // 每级存储10枚导弹
}
/**
* 计算当前导弹总数
*/
export const calculateCurrentMissileCount = (defense: Partial<Record<DefenseType, number>>): number => {
const interplanetaryMissiles = defense[DefenseType.InterplanetaryMissile] || 0
const antiBallisticMissiles = defense[DefenseType.AntiBallisticMissile] || 0
return interplanetaryMissiles + antiBallisticMissiles
}
/**
* 检查导弹容量限制
*/
export const checkMissileSiloLimit = (
defenseType: DefenseType,
currentDefense: Partial<Record<DefenseType, number>>,
buildings: Partial<Record<BuildingType, number>>,
quantity: number
): boolean => {
// 只对导弹类型进行检查
if (defenseType !== DefenseType.InterplanetaryMissile && defenseType !== DefenseType.AntiBallisticMissile) {
return true
}
const maxCapacity = calculateMissileSiloCapacity(buildings)
const currentCount = calculateCurrentMissileCount(currentDefense)
const newCount = currentCount + quantity
return newCount <= maxCapacity
}
/**
* 创建舰船建造队列项
*/
export const createShipQueueItem = (shipType: ShipType, quantity: number, buildTime: number): BuildQueueItem => {
const now = Date.now()
shipQueueIdCounter++
return {
id: `ship_${now}`,
id: `ship_${now}_${shipQueueIdCounter}`,
type: 'ship',
itemType: shipType,
quantity,
@@ -173,8 +215,9 @@ export const createShipQueueItem = (shipType: ShipType, quantity: number, buildT
*/
export const createDefenseQueueItem = (defenseType: DefenseType, quantity: number, buildTime: number): BuildQueueItem => {
const now = Date.now()
shipQueueIdCounter++
return {
id: `defense_${now}`,
id: `defense_${now}_${shipQueueIdCounter}`,
type: 'defense',
itemType: defenseType,
quantity,

View File

@@ -100,6 +100,11 @@ export const validateDefenseBuild = (
return { valid: false, reason: 'errors.shieldDomeLimit' }
}
// 导弹发射井容量限制
if (!shipLogic.checkMissileSiloLimit(defenseType, planet.defense, planet.buildings, quantity)) {
return { valid: false, reason: 'errors.missileSiloLimit' }
}
return { valid: true }
}

View File

@@ -15,19 +15,8 @@ const router = createRouter({
{ path: '/galaxy', name: 'galaxy', component: () => import('@/views/GalaxyView.vue') },
{ path: '/diplomacy', name: 'diplomacy', component: () => import('@/views/DiplomacyView.vue') },
{ path: '/settings', name: 'settings', component: () => import('@/views/SettingsView.vue') },
{
path: '/gm',
name: 'gm',
component: () => import('@/views/GMView.vue'),
beforeEnter: (_to, _from, next) => {
// GM页面仅在开发模式下可访问
if (import.meta.env.DEV) {
next()
} else {
next('/')
}
}
}
{ path: '/gm', name: 'gm', component: () => import('@/views/GMView.vue') },
{ path: '/:pathMatch(.*)*', name: 'not-found', component: () => import('@/views/NotFoundView.vue') }
]
})

View File

@@ -1,5 +1,5 @@
import { defineStore } from 'pinia'
import type { Planet, Player, BuildQueueItem, FleetMission, BattleResult, SpyReport, Officer, SpiedNotification, NPCActivityNotification, IncomingFleetAlert } from '@/types/game'
import type { Planet, Player, BuildQueueItem, FleetMission, BattleResult, SpyReport, Officer, SpiedNotification, NPCActivityNotification, IncomingFleetAlert, MissileAttack } from '@/types/game'
import { TechnologyType, OfficerType } from '@/types/game'
import type { Locale } from '@/locales'
import pkg from '../../package.json'
@@ -9,6 +9,7 @@ export const useGameStore = defineStore('game', {
state: () => ({
gameTime: Date.now(),
isPaused: false,
gameSpeed: 1,
player: {
id: 'player1',
name: '',
@@ -17,6 +18,7 @@ export const useGameStore = defineStore('game', {
officers: {} as Record<OfficerType, Officer>,
researchQueue: [] as BuildQueueItem[],
fleetMissions: [] as FleetMission[],
missileAttacks: [] as MissileAttack[],
battleReports: [] as BattleResult[],
spyReports: [] as SpyReport[],
spiedNotifications: [] as SpiedNotification[],
@@ -25,7 +27,9 @@ export const useGameStore = defineStore('game', {
incomingFleetAlerts: [] as IncomingFleetAlert[],
giftNotifications: [],
giftRejectedNotifications: [],
points: 0
points: 0,
isGMEnabled: false, // 明确设置 GM 模式默认为 false
lastVersionCheckTime: 0 // 最后一次检查版本的时间戳默认为0
} as Player,
currentPlanetId: '',
isDark: '',

View File

@@ -6,37 +6,38 @@
:root {
--radius: 0.625rem;
/* Light mode colors */
--background: oklch(0.99 0 0);
--foreground: oklch(0.129 0.042 264.695);
--card: oklch(1 0 0);
--card-foreground: oklch(0.129 0.042 264.695);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.129 0.042 264.695);
--primary: oklch(0.208 0.042 265.755);
--primary-foreground: oklch(0.984 0.003 247.858);
--secondary: oklch(0.968 0.007 247.896);
--secondary-foreground: oklch(0.208 0.042 265.755);
--muted: oklch(0.968 0.007 247.896);
--muted-foreground: oklch(0.554 0.046 257.417);
--accent: oklch(0.968 0.007 247.896);
--accent-foreground: oklch(0.208 0.042 265.755);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.929 0.013 255.508);
--input: oklch(0.929 0.013 255.508);
/* Light mode colors - 更护眼的暖色调配色 */
--background: oklch(0.95 0.008 85);
--foreground: oklch(0.3 0.02 85);
--card: oklch(0.97 0.006 85);
--card-foreground: oklch(0.3 0.02 85);
--popover: oklch(0.97 0.006 85);
--popover-foreground: oklch(0.3 0.02 85);
--primary: oklch(0.35 0.03 240);
--primary-foreground: oklch(0.98 0.005 85);
--secondary: oklch(0.92 0.01 85);
--secondary-foreground: oklch(0.3 0.02 85);
--muted: oklch(0.92 0.01 85);
--muted-foreground: oklch(0.5 0.02 85);
--accent: oklch(0.92 0.01 85);
--accent-foreground: oklch(0.3 0.02 85);
--destructive: oklch(0.5 0.15 25);
--destructive-foreground: oklch(0.98 0.005 85);
--border: oklch(0.86 0.012 85);
--input: oklch(0.86 0.012 85);
--ring: oklch(0.704 0.04 256.788);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.984 0.003 247.858);
--sidebar-foreground: oklch(0.129 0.042 264.695);
--sidebar-primary: oklch(0.208 0.042 265.755);
--sidebar-primary-foreground: oklch(0.984 0.003 247.858);
--sidebar-accent: oklch(0.968 0.007 247.896);
--sidebar-accent-foreground: oklch(0.208 0.042 265.755);
--sidebar-border: oklch(0.929 0.013 255.508);
--sidebar: oklch(0.96 0.006 85);
--sidebar-foreground: oklch(0.3 0.02 85);
--sidebar-primary: oklch(0.35 0.03 240);
--sidebar-primary-foreground: oklch(0.98 0.005 85);
--sidebar-accent: oklch(0.92 0.01 85);
--sidebar-accent-foreground: oklch(0.3 0.02 85);
--sidebar-border: oklch(0.86 0.012 85);
--sidebar-ring: oklch(0.704 0.04 256.788);
}
@@ -94,6 +95,7 @@
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);

View File

@@ -206,7 +206,8 @@ export const MissionType = {
Expedition: 'expedition',
HarvestDarkMatter: 'harvestDarkMatter', // 暗物质采集
Recycle: 'recycle', // 回收残骸
Destroy: 'destroy' // 行星毁灭
Destroy: 'destroy', // 行星毁灭
MissileAttack: 'missileAttack' // 导弹攻击
} as const
export type MissionType = (typeof MissionType)[keyof typeof MissionType]
@@ -284,6 +285,19 @@ export interface FleetMission {
giftTargetNpcId?: string // 赠送目标NPC ID
}
// 导弹攻击任务(不使用舰队系统)
export interface MissileAttack {
id: string
playerId: string
originPlanetId: string
targetPosition: { galaxy: number; system: number; position: number }
targetPlanetId?: string
missileCount: number // 发射的星际导弹数量
launchTime: number
arrivalTime: number
status: 'flying' | 'arrived' | 'intercepted'
}
// 战斗结果
export interface BattleResult {
id: string
@@ -407,6 +421,11 @@ export interface MissionReport {
destroyedPlanetName?: string
// 部署任务:部署的舰队
deployedFleet?: Partial<Fleet>
// 导弹攻击任务:导弹信息
missileCount?: number
missileHits?: number
missileIntercepted?: number
defenseLosses?: Partial<Record<DefenseType, number>>
}
read?: boolean
}
@@ -533,6 +552,7 @@ export interface Player {
officers: Record<OfficerType, Officer>
researchQueue: BuildQueueItem[]
fleetMissions: FleetMission[]
missileAttacks: MissileAttack[] // 导弹攻击任务
battleReports: BattleResult[]
spyReports: SpyReport[]
spiedNotifications: SpiedNotification[] // 被侦查通知
@@ -542,6 +562,9 @@ export interface Player {
giftNotifications: GiftNotification[] // 礼物通知(等待接受/拒绝)
giftRejectedNotifications: GiftRejectedNotification[] // 礼物被拒绝通知
points: number // 总积分每1000资源=1分
isGMEnabled?: boolean // GM模式开关默认false通过秘籍激活
lastVersionCheckTime?: number // 最后一次自动检查版本的时间戳(被动检测)
lastManualUpdateCheck?: number // 最后一次手动检查更新的时间戳(主动检测)
// 外交系统字段
diplomaticRelations?: Record<string, DiplomaticRelation> // 玩家对NPC的关系key: npcId
diplomaticReports?: DiplomaticReport[] // 外交变化报告

View File

@@ -342,6 +342,7 @@ export const simulateBattle = (
} else if (defenderUnits.length === 0) {
winner = 'attacker'
} else {
// OGame原版规则6回合后双方都有剩余单位时判定为平局
winner = 'draw'
}

View File

@@ -1,3 +1,4 @@
/**
* 格式化数字为英文单位K, M, B, T, Q
* @param num 数字

57
src/utils/versionCheck.ts Normal file
View File

@@ -0,0 +1,57 @@
import pkg from '../../package.json'
export interface VersionInfo {
version: string
releaseNotes: string
downloadUrl: string
}
// 检查GitHub最新版本
export const checkLatestVersion = async (lastCheckTime: number, updateCheckTime: (time: number) => void): Promise<VersionInfo | null> => {
const now = Date.now()
const fiveMinutes = 5 * 60 * 1000 // 5分钟
// 如果距离上次检查不到5分钟跳过
if (now - lastCheckTime < fiveMinutes) {
return null
}
try {
const response = await fetch(`https://api.github.com/repos/${pkg.author.name}/${pkg.name}/releases/latest`)
if (!response.ok) {
console.error('Failed to fetch latest version:', response.status)
// 更新检查时间避免频繁请求失败的API
updateCheckTime(now)
throw new Error(`Failed to fetch version: ${response.status}`)
}
const data = await response.json()
const githubVersion = data.tag_name?.replace(/^v/, '') // 移除开头的 'v' (如 v1.2.0 -> 1.2.0)
// 更新最后检查时间
updateCheckTime(now)
// 比较版本号
if (githubVersion && githubVersion !== pkg.version) {
return {
version: githubVersion,
releaseNotes: data.body || '',
downloadUrl: `https://github.com/${pkg.author.name}/${pkg.name}/releases/latest`
}
}
return null
} catch (error) {
console.error('Error checking version:', error)
// 更新检查时间避免频繁请求失败的API
updateCheckTime(now)
throw error
}
}
// 检查是否可以进行版本检查距离上次检查是否超过5分钟
export const canCheckVersion = (lastCheckTime: number): boolean => {
const now = Date.now()
const fiveMinutes = 5 * 60 * 1000 // 5分钟
return now - lastCheckTime >= fiveMinutes
}

View File

@@ -415,7 +415,9 @@
${t('buildingsView.demolishRefund')}:
${t('resources.metal')}: ${formatNumber(refund.metal)}
${t('resources.crystal')}: ${formatNumber(refund.crystal)}
${t('resources.deuterium')}: ${formatNumber(refund.deuterium)}${refund.darkMatter > 0 ? `\n${t('resources.darkMatter')}: ${formatNumber(refund.darkMatter)}` : ''}`
${t('resources.deuterium')}: ${formatNumber(refund.deuterium)}${
refund.darkMatter > 0 ? `\n${t('resources.darkMatter')}: ${formatNumber(refund.darkMatter)}` : ''
}`
pendingDemolishBuilding.value = buildingType
demolishConfirmOpen.value = true

View File

@@ -5,6 +5,29 @@
<h1 class="text-2xl sm:text-3xl font-bold mb-4 sm:mb-6">{{ t('defenseView.title') }}</h1>
<!-- 导弹容量显示 -->
<div v-if="missileSiloCapacity > 0" class="mb-4 sm:mb-6 p-3 sm:p-4 bg-muted/50 rounded-lg border">
<div class="flex items-center justify-between">
<div class="text-sm sm:text-base font-medium">{{ t('defenseView.missileCapacity') }}:</div>
<div class="text-sm sm:text-base font-bold">
<span :class="currentMissileCount > missileSiloCapacity ? 'text-destructive' : 'text-primary'">
{{ formatNumber(currentMissileCount) }}
</span>
<span class="text-muted-foreground mx-1">/</span>
<span>{{ formatNumber(missileSiloCapacity) }}</span>
</div>
</div>
<div class="mt-2">
<div class="w-full bg-background rounded-full h-2.5 sm:h-3 overflow-hidden">
<div
class="h-full transition-all duration-300"
:class="currentMissileCount > missileSiloCapacity ? 'bg-destructive' : 'bg-primary'"
:style="{ width: `${Math.min((currentMissileCount / missileSiloCapacity) * 100, 100)}%` }"
/>
</div>
</div>
</div>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3 sm:gap-4">
<Card v-for="defenseType in Object.values(DefenseType)" :key="defenseType" class="relative">
<CardUnlockOverlay :requirements="DEFENSES[defenseType].requirements" />
@@ -156,6 +179,7 @@
import { formatNumber, getResourceCostColor } from '@/utils/format'
import * as publicLogic from '@/logic/publicLogic'
import * as shipValidation from '@/logic/shipValidation'
import * as shipLogic from '@/logic/shipLogic'
const gameStore = useGameStore()
const detailDialog = useDetailDialogStore()
@@ -163,6 +187,17 @@
const { DEFENSES } = useGameConfig()
const planet = computed(() => gameStore.currentPlanet)
// 导弹容量相关计算
const missileSiloCapacity = computed(() => {
if (!planet.value) return 0
return shipLogic.calculateMissileSiloCapacity(planet.value.buildings)
})
const currentMissileCount = computed(() => {
if (!planet.value) return 0
return shipLogic.calculateCurrentMissileCount(planet.value.defense)
})
// AlertDialog 状态
const alertDialogOpen = ref(false)
const alertDialogTitle = ref('')
@@ -197,11 +232,12 @@
}
const buildDefense = (defenseType: DefenseType, quantity: number): boolean => {
if (!gameStore.currentPlanet) return false
const validation = shipValidation.validateDefenseBuild(gameStore.currentPlanet, defenseType, quantity, gameStore.player.technologies)
const currentPlanet = gameStore.currentPlanet
if (!currentPlanet) return false
const validation = shipValidation.validateDefenseBuild(currentPlanet, defenseType, quantity, gameStore.player.technologies)
if (!validation.valid) return false
const queueItem = shipValidation.executeDefenseBuild(gameStore.currentPlanet, defenseType, quantity, gameStore.player.officers)
gameStore.currentPlanet.buildQueue.push(queueItem)
const queueItem = shipValidation.executeDefenseBuild(currentPlanet, defenseType, quantity, gameStore.player.officers)
currentPlanet.buildQueue.push(queueItem)
return true
}

View File

@@ -12,19 +12,34 @@
<TabsList class="grid w-full grid-cols-4">
<TabsTrigger value="all">
{{ t('diplomacy.tabs.all') }}
<Badge variant="secondary" class="ml-2">{{ allNpcs.length }}</Badge>
<Badge variant="outline" class="ml-2 bg-blue-100 dark:bg-blue-950 text-blue-700 dark:text-blue-300 border-blue-300 dark:border-blue-700">{{ allNpcs.length }}</Badge>
</TabsTrigger>
<TabsTrigger value="friendly">
{{ t('diplomacy.tabs.friendly') }}
<Badge variant="secondary" class="ml-2">{{ friendlyNpcs.length }}</Badge>
<Badge
variant="outline"
class="ml-2 bg-green-100 dark:bg-green-950 text-green-700 dark:text-green-300 border-green-300 dark:border-green-700"
>
{{ friendlyNpcs.length }}
</Badge>
</TabsTrigger>
<TabsTrigger value="neutral">
{{ t('diplomacy.tabs.neutral') }}
<Badge variant="secondary" class="ml-2">{{ neutralNpcs.length }}</Badge>
<Badge
variant="outline"
class="ml-2 bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600"
>
{{ neutralNpcs.length }}
</Badge>
</TabsTrigger>
<TabsTrigger value="hostile">
{{ t('diplomacy.tabs.hostile') }}
<Badge variant="secondary" class="ml-2">{{ hostileNpcs.length }}</Badge>
<Badge
variant="outline"
class="ml-2 bg-red-100 dark:bg-red-950 text-red-700 dark:text-red-300 border-red-300 dark:border-red-700"
>
{{ hostileNpcs.length }}
</Badge>
</TabsTrigger>
</TabsList>
@@ -202,7 +217,7 @@
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { computed, ref, onMounted } from 'vue'
import { useGameStore } from '@/stores/gameStore'
import { useNPCStore } from '@/stores/npcStore'
import { useI18n } from '@/composables/useI18n'
@@ -222,6 +237,52 @@
const activeTab = ref('all')
// 检测并生成NPC盟友
const initializeNPCAllies = () => {
const npcs = npcStore.npcs
if (npcs.length < 2) return // 至少需要2个NPC才能生成盟友关系
npcs.forEach(npc => {
// 如果NPC没有盟友列表,初始化为空数组
if (!npc.allies) {
npc.allies = []
}
// 如果NPC没有盟友,随机生成1-2个盟友
if (npc.allies.length === 0) {
const otherNpcs = npcs.filter(n => n.id !== npc.id)
if (otherNpcs.length === 0) return
// 随机选择1-2个盟友
const allyCount = Math.min(Math.floor(Math.random() * 2) + 1, otherNpcs.length)
const shuffled = [...otherNpcs].sort(() => Math.random() - 0.5)
const selectedAllies = shuffled.slice(0, allyCount)
selectedAllies.forEach(ally => {
// 添加双向盟友关系
if (!npc.allies!.includes(ally.id)) {
npc.allies!.push(ally.id)
}
// 确保盟友也有盟友列表
if (!ally.allies) {
ally.allies = []
}
// 确保双向关系
if (!ally.allies.includes(npc.id)) {
ally.allies.push(npc.id)
}
})
}
})
}
// 组件挂载时初始化NPC盟友
onMounted(() => {
initializeNPCAllies()
})
// 分页状态
const ITEMS_PER_PAGE = 20
const currentPage = ref<Record<string, number>>({

View File

@@ -332,7 +332,7 @@
import { useNPCStore } from '@/stores/npcStore'
import { useI18n } from '@/composables/useI18n'
import { useGameConfig } from '@/composables/useGameConfig'
import { computed, ref, onMounted, onUnmounted } from 'vue'
import { computed, ref, onMounted, onUnmounted, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ShipType, MissionType, BuildingType } from '@/types/game'
import type { Fleet, Resources } from '@/types/game'
@@ -473,6 +473,13 @@
// 是否为赠送模式
const isGiftMode = ref(false)
// 监听目标NPC变化当目标不再是NPC时自动禁用赠送模式
watch(targetNpc, newValue => {
if (!newValue && isGiftMode.value) {
isGiftMode.value = false
}
})
// 计算赠送的预估好感度增加值
const calculateGiftReputation = (): number => {
return diplomaticLogic.calculateGiftReputationGain(cargo.value)

View File

@@ -1,5 +1,25 @@
<template>
<div class="container mx-auto p-4 sm:p-6 space-y-4 sm:space-y-6">
<!-- 无权限时显示404 -->
<div v-if="!gameStore.player.isGMEnabled" class="container mx-auto p-4 sm:p-6 flex items-center justify-center min-h-[60vh]">
<Empty class="border-0">
<EmptyMedia>
<div class="text-8xl sm:text-9xl font-bold text-muted-foreground/20">404</div>
</EmptyMedia>
<EmptyHeader>
<EmptyTitle>{{ t('notFound.title') }}</EmptyTitle>
<EmptyDescription>{{ t('notFound.description') }}</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button @click="goHome" size="lg">
<Home class="mr-2 h-4 w-4" />
{{ t('notFound.goHome') }}
</Button>
</EmptyContent>
</Empty>
</div>
<!-- 有权限时显示GM页面 -->
<div v-else class="container mx-auto p-4 sm:p-6 space-y-4 sm:space-y-6">
<div class="flex items-center justify-between">
<h1 class="text-2xl sm:text-3xl font-bold">{{ t('gmView.title') }}</h1>
<Badge variant="destructive">{{ t('gmView.adminOnly') }}</Badge>
@@ -12,7 +32,7 @@
</CardHeader>
<CardContent>
<Select v-model="selectedPlanetId">
<SelectTrigger>
<SelectTrigger class="w-full">
<SelectValue :placeholder="t('gmView.choosePlanet')" />
</SelectTrigger>
<SelectContent>
@@ -25,148 +45,144 @@
</Card>
<!-- 标签切换 -->
<div v-if="selectedPlanet" class="flex flex-wrap gap-2 border-b">
<Button
v-for="tab in tabs"
:key="tab.value"
@click="activeTab = tab.value"
:variant="activeTab === tab.value ? 'default' : 'ghost'"
class="rounded-b-none"
>
{{ t(tab.label) }}
</Button>
</div>
<Tabs v-if="selectedPlanet" default-value="resources" class="w-full">
<TabsList class="grid w-full" :style="{ gridTemplateColumns: `repeat(${tabs.length}, minmax(0, 1fr))` }">
<TabsTrigger v-for="tab in tabs" :key="tab.value" :value="tab.value">
{{ t(tab.label) }}
</TabsTrigger>
</TabsList>
<!-- 资源 -->
<div v-if="selectedPlanet && activeTab === 'resources'" class="space-y-4">
<Card>
<CardHeader>
<CardTitle>{{ t('gmView.modifyResources') }}</CardTitle>
<CardDescription>{{ t('gmView.resourcesDesc') }}</CardDescription>
</CardHeader>
<CardContent class="space-y-4">
<div v-for="resource in resourceTypes" :key="resource" class="space-y-2">
<Label>{{ t(`resources.${resource}`) }}</Label>
<div class="flex gap-2">
<Input v-model.number="selectedPlanet.resources[resource]" type="number" min="0" class="flex-1" />
<Button @click="setResourceAmount(resource, 1000000)" variant="outline" size="sm">+1M</Button>
<Button @click="setResourceAmount(resource, 10000000)" variant="outline" size="sm">+10M</Button>
</div>
</div>
</CardContent>
</Card>
</div>
<!-- 建筑 -->
<div v-if="selectedPlanet && activeTab === 'buildings'" class="space-y-4">
<Card>
<CardHeader>
<CardTitle>{{ t('gmView.modifyBuildings') }}</CardTitle>
<CardDescription>{{ t('gmView.buildingsDesc') }}</CardDescription>
</CardHeader>
<CardContent>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div v-for="building in buildingTypes" :key="building" class="space-y-2">
<Label>{{ BUILDINGS[building].name }}</Label>
<!-- 资源 -->
<TabsContent value="resources" class="space-y-4">
<Card>
<CardHeader>
<CardTitle>{{ t('gmView.modifyResources') }}</CardTitle>
<CardDescription>{{ t('gmView.resourcesDesc') }}</CardDescription>
</CardHeader>
<CardContent class="space-y-4">
<div v-for="resource in resourceTypes" :key="resource" class="space-y-2">
<Label>{{ t(`resources.${resource}`) }}</Label>
<div class="flex gap-2">
<Input v-model.number="selectedPlanet.buildings[building]" type="number" min="0" max="100" class="flex-1" />
<Button @click="setBuildingLevel(building, 10)" variant="outline" size="sm">Lv 10</Button>
<Button @click="setBuildingLevel(building, 30)" variant="outline" size="sm">Lv 30</Button>
<Input v-model.number="selectedPlanet.resources[resource]" type="number" min="0" class="flex-1" />
<Button @click="setResourceAmount(resource, 1000000)" variant="outline" size="sm">+1M</Button>
<Button @click="setResourceAmount(resource, 10000000)" variant="outline" size="sm">+10M</Button>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
</CardContent>
</Card>
</TabsContent>
<!-- 科技 -->
<div v-if="activeTab === 'research'" class="space-y-4">
<Card>
<CardHeader>
<CardTitle>{{ t('gmView.modifyResearch') }}</CardTitle>
<CardDescription>{{ t('gmView.researchDesc') }}</CardDescription>
</CardHeader>
<CardContent>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div v-for="tech in technologyTypes" :key="tech" class="space-y-2">
<Label>{{ TECHNOLOGIES[tech].name }}</Label>
<div class="flex gap-2">
<Input v-model.number="gameStore.player.technologies[tech]" type="number" min="0" max="50" class="flex-1" />
<Button @click="setTechnologyLevel(tech, 10)" variant="outline" size="sm">Lv 10</Button>
<Button @click="setTechnologyLevel(tech, 20)" variant="outline" size="sm">Lv 20</Button>
<!-- 建筑 -->
<TabsContent value="buildings" class="space-y-4">
<Card>
<CardHeader>
<CardTitle>{{ t('gmView.modifyBuildings') }}</CardTitle>
<CardDescription>{{ t('gmView.buildingsDesc') }}</CardDescription>
</CardHeader>
<CardContent>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div v-for="building in buildingTypes" :key="building" class="space-y-2">
<Label>{{ BUILDINGS[building].name }}</Label>
<div class="flex gap-2">
<Input v-model.number="selectedPlanet.buildings[building]" type="number" min="0" max="100" class="flex-1" />
<Button @click="setBuildingLevel(building, 10)" variant="outline" size="sm">Lv 10</Button>
<Button @click="setBuildingLevel(building, 30)" variant="outline" size="sm">Lv 30</Button>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
</CardContent>
</Card>
</TabsContent>
<!-- 舰船 -->
<div v-if="selectedPlanet && activeTab === 'ships'" class="space-y-4">
<Card>
<CardHeader>
<CardTitle>{{ t('gmView.modifyShips') }}</CardTitle>
<CardDescription>{{ t('gmView.shipsDesc') }}</CardDescription>
</CardHeader>
<CardContent>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div v-for="ship in shipTypes" :key="ship" class="space-y-2">
<Label>{{ SHIPS[ship].name }}</Label>
<div class="flex gap-2">
<Input v-model.number="selectedPlanet.fleet[ship]" type="number" min="0" class="flex-1" />
<Button @click="setShipCount(ship, 100)" variant="outline" size="sm">+100</Button>
<Button @click="setShipCount(ship, 1000)" variant="outline" size="sm">+1K</Button>
<!-- 科技 -->
<TabsContent value="research" class="space-y-4">
<Card>
<CardHeader>
<CardTitle>{{ t('gmView.modifyResearch') }}</CardTitle>
<CardDescription>{{ t('gmView.researchDesc') }}</CardDescription>
</CardHeader>
<CardContent>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div v-for="tech in technologyTypes" :key="tech" class="space-y-2">
<Label>{{ TECHNOLOGIES[tech].name }}</Label>
<div class="flex gap-2">
<Input v-model.number="gameStore.player.technologies[tech]" type="number" min="0" max="50" class="flex-1" />
<Button @click="setTechnologyLevel(tech, 10)" variant="outline" size="sm">Lv 10</Button>
<Button @click="setTechnologyLevel(tech, 20)" variant="outline" size="sm">Lv 20</Button>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
</CardContent>
</Card>
</TabsContent>
<!-- 防御 -->
<div v-if="selectedPlanet && activeTab === 'defense'" class="space-y-4">
<Card>
<CardHeader>
<CardTitle>{{ t('gmView.modifyDefense') }}</CardTitle>
<CardDescription>{{ t('gmView.defenseDesc') }}</CardDescription>
</CardHeader>
<CardContent>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div v-for="defense in defenseTypes" :key="defense" class="space-y-2">
<Label>{{ DEFENSES[defense].name }}</Label>
<div class="flex gap-2">
<Input v-model.number="selectedPlanet.defense[defense]" type="number" min="0" class="flex-1" />
<Button @click="setDefenseCount(defense, 100)" variant="outline" size="sm">+100</Button>
<Button @click="setDefenseCount(defense, 1000)" variant="outline" size="sm">+1K</Button>
<!-- 舰船 -->
<TabsContent value="ships" class="space-y-4">
<Card>
<CardHeader>
<CardTitle>{{ t('gmView.modifyShips') }}</CardTitle>
<CardDescription>{{ t('gmView.shipsDesc') }}</CardDescription>
</CardHeader>
<CardContent>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div v-for="ship in shipTypes" :key="ship" class="space-y-2">
<Label>{{ SHIPS[ship].name }}</Label>
<div class="flex gap-2">
<Input v-model.number="selectedPlanet.fleet[ship]" type="number" min="0" class="flex-1" />
<Button @click="setShipCount(ship, 100)" variant="outline" size="sm">+100</Button>
<Button @click="setShipCount(ship, 1000)" variant="outline" size="sm">+1K</Button>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
</CardContent>
</Card>
</TabsContent>
<!-- 军官 -->
<div v-if="activeTab === 'officers'" class="space-y-4">
<Card>
<CardHeader>
<CardTitle>{{ t('gmView.modifyOfficers') }}</CardTitle>
<CardDescription>{{ t('gmView.officersDesc') }}</CardDescription>
</CardHeader>
<CardContent>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div v-for="officer in officerTypes" :key="officer" class="space-y-2">
<Label>{{ OFFICERS[officer].name }}</Label>
<div class="flex gap-2">
<Input v-model.number="officerDays[officer]" type="number" min="0" :placeholder="t('gmView.days')" class="flex-1" />
<Button @click="setOfficerDays(officer, 7)" variant="outline" size="sm">7{{ t('gmView.days') }}</Button>
<Button @click="setOfficerDays(officer, 30)" variant="outline" size="sm">30{{ t('gmView.days') }}</Button>
<Button @click="setOfficerDays(officer, 365)" variant="outline" size="sm">365{{ t('gmView.days') }}</Button>
<!-- 防御 -->
<TabsContent value="defense" class="space-y-4">
<Card>
<CardHeader>
<CardTitle>{{ t('gmView.modifyDefense') }}</CardTitle>
<CardDescription>{{ t('gmView.defenseDesc') }}</CardDescription>
</CardHeader>
<CardContent>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div v-for="defense in defenseTypes" :key="defense" class="space-y-2">
<Label>{{ DEFENSES[defense].name }}</Label>
<div class="flex gap-2">
<Input v-model.number="selectedPlanet.defense[defense]" type="number" min="0" class="flex-1" />
<Button @click="setDefenseCount(defense, 100)" variant="outline" size="sm">+100</Button>
<Button @click="setDefenseCount(defense, 1000)" variant="outline" size="sm">+1K</Button>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
</CardContent>
</Card>
</TabsContent>
<!-- 军官 -->
<TabsContent value="officers" class="space-y-4">
<Card>
<CardHeader>
<CardTitle>{{ t('gmView.modifyOfficers') }}</CardTitle>
<CardDescription>{{ t('gmView.officersDesc') }}</CardDescription>
</CardHeader>
<CardContent>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div v-for="officer in officerTypes" :key="officer" class="space-y-2">
<Label>{{ OFFICERS[officer].name }}</Label>
<div class="flex gap-2">
<Input v-model.number="officerDays[officer]" type="number" min="0" :placeholder="t('gmView.days')" class="flex-1" />
<Button @click="setOfficerDays(officer, 7)" variant="outline" size="sm">7{{ t('gmView.days') }}</Button>
<Button @click="setOfficerDays(officer, 30)" variant="outline" size="sm">30{{ t('gmView.days') }}</Button>
<Button @click="setOfficerDays(officer, 365)" variant="outline" size="sm">365{{ t('gmView.days') }}</Button>
</div>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
<!-- NPC测试 -->
<Card class="border-primary">
@@ -175,30 +191,32 @@
<CardDescription>{{ t('gmView.npcTestingDesc') || 'Test NPC spy and attack behavior' }}</CardDescription>
</CardHeader>
<CardContent class="space-y-3">
<div class="space-y-2">
<Label>{{ t('gmView.selectNPC') || 'Select NPC' }}</Label>
<Select v-model="selectedNPCId">
<SelectTrigger>
<SelectValue :placeholder="t('gmView.chooseNPC') || 'Choose NPC'" />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="npc in npcStore.npcs" :key="npc.id" :value="npc.id">{{ npc.name }} ({{ npc.difficulty }})</SelectItem>
</SelectContent>
</Select>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<div class="space-y-2">
<Label>{{ t('gmView.selectNPC') || 'Select NPC' }}</Label>
<Select v-model="selectedNPCId">
<SelectTrigger class="w-full">
<SelectValue :placeholder="t('gmView.chooseNPC') || 'Choose NPC'" />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="npc in npcStore.npcs" :key="npc.id" :value="npc.id">{{ npc.name }} ({{ npc.difficulty }})</SelectItem>
</SelectContent>
</Select>
</div>
<div class="space-y-2">
<Label>{{ t('gmView.targetPlanet') || 'Target Planet' }}</Label>
<Select v-model="targetPlanetIndex">
<SelectTrigger>
<SelectValue :placeholder="t('gmView.chooseTarget') || 'Choose Target Planet'" />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="(planet, index) in gameStore.player.planets" :key="planet.id" :value="index.toString()">
{{ planet.name }} ({{ planet.position.galaxy }}:{{ planet.position.system }}:{{ planet.position.position }})
</SelectItem>
</SelectContent>
</Select>
<div class="space-y-2">
<Label>{{ t('gmView.targetPlanet') || 'Target Planet' }}</Label>
<Select v-model="targetPlanetIndex">
<SelectTrigger class="w-full">
<SelectValue :placeholder="t('gmView.chooseTarget') || 'Choose Target Planet'" />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="(planet, index) in gameStore.player.planets" :key="planet.id" :value="index.toString()">
{{ planet.name }} ({{ planet.position.galaxy }}:{{ planet.position.system }}:{{ planet.position.position }})
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div class="grid grid-cols-2 gap-2">
@@ -231,14 +249,46 @@
<CardDescription>{{ t('gmView.dangerZoneDesc') }}</CardDescription>
</CardHeader>
<CardContent class="space-y-2">
<Button @click="resetGame" variant="destructive" class="w-full">{{ t('gmView.resetGame') }}</Button>
<Button @click="showResetConfirmDialog" variant="destructive" class="w-full">{{ t('gmView.resetGame') }}</Button>
</CardContent>
</Card>
<!-- Reset Game 确认对话框 -->
<AlertDialog :open="resetDialogOpen" @update:open="handleResetDialogClose">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{{ t('gmView.resetGame') }}</AlertDialogTitle>
<AlertDialogDescription>
{{ t('gmView.resetGameConfirm') }}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel @click="handleResetCancel">{{ t('common.cancel') }}</AlertDialogCancel>
<AlertDialogAction @click="confirmResetGame">{{ t('common.confirm') }}</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<!-- AlertDialog 提示对话框 -->
<AlertDialog :open="alertDialogOpen" @update:open="alertDialogOpen = $event">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{{ alertDialogTitle }}</AlertDialogTitle>
<AlertDialogDescription v-if="alertDialogMessage" class="whitespace-pre-line">
{{ alertDialogMessage }}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogAction @click="handleAlertConfirm">{{ t('common.confirm') }}</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import { useGameStore } from '@/stores/gameStore'
import { useNPCStore } from '@/stores/npcStore'
import { useUniverseStore } from '@/stores/universeStore'
@@ -250,21 +300,47 @@
import { Label } from '@/components/ui/label'
import { Badge } from '@/components/ui/badge'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '@/components/ui/empty'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle
} from '@/components/ui/alert-dialog'
import { BuildingType, TechnologyType, ShipType, DefenseType, OfficerType } from '@/types/game'
import * as npcBehaviorLogic from '@/logic/npcBehaviorLogic'
import { Home } from 'lucide-vue-next'
const router = useRouter()
const gameStore = useGameStore()
const npcStore = useNPCStore()
const universeStore = useUniverseStore()
const { t } = useI18n()
const { BUILDINGS, TECHNOLOGIES, SHIPS, DEFENSES, OFFICERS } = useGameConfig()
const goHome = () => {
router.push('/')
}
const selectedPlanetId = ref<string>(gameStore.player.planets[0]?.id || '')
const activeTab = ref<'resources' | 'buildings' | 'research' | 'ships' | 'defense' | 'officers'>('resources')
const officerDays = ref<Record<OfficerType, number>>({} as Record<OfficerType, number>)
const selectedNPCId = ref<string>(npcStore.npcs[0]?.id || '')
const targetPlanetIndex = ref<string>('0')
// AlertDialog 状态
const alertDialogOpen = ref(false)
const alertDialogTitle = ref('')
const alertDialogMessage = ref('')
const alertDialogCallback = ref<(() => void) | null>(null)
// Reset Dialog 状态
const resetDialogOpen = ref(false)
// 初始化军官天数显示
Object.values(OfficerType).forEach(officer => {
const officerData = gameStore.player.officers[officer]
@@ -354,17 +430,63 @@
}
}
const resetGame = () => {
if (confirm(t('gmView.resetGameConfirm'))) {
// 显示重置游戏确认对话框
const showResetConfirmDialog = () => {
// 暂停游戏
gameStore.isPaused = true
resetDialogOpen.value = true
}
// 处理重置对话框关闭
const handleResetDialogClose = (open: boolean) => {
if (!open) {
// 如果对话框关闭,恢复游戏
gameStore.isPaused = false
}
resetDialogOpen.value = open
}
// 取消重置
const handleResetCancel = () => {
resetDialogOpen.value = false
gameStore.isPaused = false
}
// 确认重置游戏
const confirmResetGame = () => {
gameStore.isPaused = true
resetDialogOpen.value = false
try {
gameStore.player.isGMEnabled = false
localStorage.clear()
location.reload()
} catch (error) {
console.error('Failed to reset game:', error)
window.location.reload()
}
}
// 显示AlertDialog的辅助函数
const showAlert = (title: string, message: string, callback?: () => void) => {
alertDialogTitle.value = title
alertDialogMessage.value = message
alertDialogCallback.value = callback || null
alertDialogOpen.value = true
}
// AlertDialog确认处理
const handleAlertConfirm = () => {
alertDialogOpen.value = false
if (alertDialogCallback.value) {
alertDialogCallback.value()
alertDialogCallback.value = null
}
}
// NPC测试函数
const testNPCSpy = () => {
if (!selectedNPC.value) {
alert(t('gmView.selectNPCFirst') || 'Please select an NPC first')
showAlert(t('gmView.selectNPCFirst') || 'Please select an NPC first', '')
return
}
@@ -376,17 +498,18 @@
)
if (mission) {
// 加速任务到5秒后到达
npcBehaviorLogic.accelerateNPCMission(selectedNPC.value, mission.id, 5)
alert(`${selectedNPC.value.name} will spy in 5 seconds`)
showAlert(t('gmView.npcWillSpyIn5s', { npcName: selectedNPC.value.name }), t('gmView.testSpyMessage'), () => {
// 加速任务到5秒后到达在确认后执行这样时间更准确
npcBehaviorLogic.accelerateNPCMission(selectedNPC.value!, mission.id, 5, gameStore.player)
})
} else {
alert(t('gmView.npcNoProbes') || 'NPC does not have spy probes')
showAlert(t('gmView.npcNoProbes') || 'NPC does not have spy probes', '')
}
}
const testNPCAttack = () => {
if (!selectedNPC.value) {
alert(t('gmView.selectNPCFirst') || 'Please select an NPC first')
showAlert(t('gmView.selectNPCFirst') || 'Please select an NPC first', '')
return
}
@@ -398,17 +521,18 @@
)
if (mission) {
// 加速任务到5秒后到达
npcBehaviorLogic.accelerateNPCMission(selectedNPC.value, mission.id, 5)
alert(`${selectedNPC.value.name} will attack in 5 seconds`)
showAlert(t('gmView.npcWillAttackIn5s', { npcName: selectedNPC.value.name }), t('gmView.testAttackMessage'), () => {
// 加速任务到5秒后到达在确认后执行这样时间更准确
npcBehaviorLogic.accelerateNPCMission(selectedNPC.value!, mission.id, 5, gameStore.player)
})
} else {
alert(t('gmView.npcNoSpyReport') || 'NPC needs to spy first')
showAlert(t('gmView.npcNoSpyReport') || 'NPC needs to spy first', '')
}
}
const testNPCSpyAndAttack = () => {
if (!selectedNPC.value) {
alert(t('gmView.selectNPCFirst') || 'Please select an NPC first')
showAlert(t('gmView.selectNPCFirst') || 'Please select an NPC first', '')
return
}
@@ -420,36 +544,37 @@
)
if (spyMission && attackMission) {
// 加速任务侦查5秒后到达攻击10秒后到达
npcBehaviorLogic.accelerateNPCMission(selectedNPC.value, spyMission.id, 5)
npcBehaviorLogic.accelerateNPCMission(selectedNPC.value, attackMission.id, 10)
alert(`${selectedNPC.value.name} will spy in 5s and attack in 10s`)
showAlert(t('gmView.npcWillSpyAndAttack', { npcName: selectedNPC.value.name }), t('gmView.testSpyAndAttackMessage'), () => {
// 加速任务侦查5秒后到达攻击10秒后到达在确认后执行这样时间更准确
npcBehaviorLogic.accelerateNPCMission(selectedNPC.value!, spyMission.id, 5, gameStore.player)
npcBehaviorLogic.accelerateNPCMission(selectedNPC.value!, attackMission.id, 10, gameStore.player)
})
} else {
alert(t('gmView.npcMissionFailed') || 'Failed to create missions')
showAlert(t('gmView.npcMissionFailed') || 'Failed to create missions', '')
}
}
const accelerateAllMissions = () => {
if (!selectedNPC.value) {
alert(t('gmView.selectNPCFirst') || 'Please select an NPC first')
showAlert(t('gmView.selectNPCFirst') || 'Please select an NPC first', '')
return
}
const count = npcBehaviorLogic.accelerateAllNPCMissions(selectedNPC.value, 5)
alert(`Accelerated ${count} missions to 5 seconds`)
const count = npcBehaviorLogic.accelerateAllNPCMissions(selectedNPC.value, 5, gameStore.player)
showAlert(t('gmView.acceleratedMissions', { count }), '')
}
// 初始化NPC舰队
const initializeNPCFleet = () => {
if (!selectedNPC.value) {
alert(t('gmView.selectNPCFirst') || 'Please select an NPC first')
showAlert(t('gmView.selectNPCFirst') || 'Please select an NPC first', '')
return
}
// 给NPC的第一个星球添加基础舰队
const npcPlanet = selectedNPC.value.planets[0]
if (!npcPlanet) {
alert('NPC has no planets')
showAlert(t('gmView.npcNoPlanets'), '')
return
}
@@ -465,8 +590,6 @@
npcPlanet.fleet[ShipType.Destroyer] = (npcPlanet.fleet[ShipType.Destroyer] || 0) + 30
npcPlanet.fleet[ShipType.Battlecruiser] = (npcPlanet.fleet[ShipType.Battlecruiser] || 0) + 20
alert(
`${selectedNPC.value.name} fleet initialized:\n- 100 Spy Probes\n- 500 Light Fighters\n- 300 Heavy Fighters\n- 200 Cruisers\n- 100 Battleships\n- 50 Bombers\n- 30 Destroyers\n- 20 Battlecruisers`
)
showAlert(t('gmView.npcFleetInitialized', { npcName: selectedNPC.value.name }), t('gmView.npcFleetDetails'))
}
</script>

View File

@@ -162,7 +162,7 @@
<div
v-for="slot in systemSlots"
:key="slot.position"
class="flex items-center gap-2 sm:gap-4 p-2 sm:p-3 border rounded-lg hover:bg-muted/50 transition-colors"
class="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4 p-2 sm:p-3 border rounded-lg hover:bg-muted/50 transition-colors"
:class="{
// 空位置
'bg-muted/30': !slot.planet,
@@ -192,38 +192,81 @@
(!getRelation(slot.planet) || getRelation(slot.planet)?.status === RelationStatus.Neutral)
}"
>
<!-- 位置编号 -->
<div class="w-8 sm:w-12 text-center">
<Badge variant="outline" class="text-xs sm:text-sm">{{ slot.position }}</Badge>
</div>
<!-- 星球信息 -->
<div class="flex-1 min-w-0">
<div v-if="slot.planet" class="space-y-1">
<!-- 移动端:垂直布局 / PC端水平布局 -->
<div class="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2">
<!-- 星球名称和坐标 -->
<div class="flex items-baseline gap-1.5 min-w-0">
<h3 class="font-semibold text-sm sm:text-base truncate">{{ slot.planet.name }}</h3>
<span class="text-xs text-muted-foreground whitespace-nowrap flex-shrink-0 sm:hidden">
[{{ slot.planet.position.galaxy }}:{{ slot.planet.position.system }}:{{ slot.planet.position.position }}]
</span>
<!-- 移动端布局 -->
<div class="sm:hidden w-full space-y-2">
<!-- 第一行:位置编号 + 星球信息(名称、坐标、状态、残骸) -->
<div class="flex items-start gap-2 w-full">
<!-- 位置编号 -->
<div class="w-8 text-center flex-shrink-0">
<Badge variant="outline" class="text-xs">{{ slot.position }}</Badge>
</div>
<!-- 星球信息 -->
<div class="flex-1 min-w-0">
<div v-if="slot.planet" class="space-y-1">
<!-- 第一行:名称、坐标、状态、残骸 -->
<div class="flex items-center gap-1.5 min-w-0 flex-wrap">
<h3 class="font-semibold text-sm truncate">{{ slot.planet.name }}</h3>
<span class="text-xs text-muted-foreground whitespace-nowrap flex-shrink-0">
[{{ slot.planet.position.galaxy }}:{{ slot.planet.position.system }}:{{ slot.planet.position.position }}]
</span>
<Badge v-if="isMyPlanet(slot.planet)" variant="default" class="text-xs flex-shrink-0">
{{ t('galaxyView.mine') }}
</Badge>
<Badge v-else :variant="getRelationBadgeVariant(slot.planet)" class="text-xs flex-shrink-0">
{{ getRelationStatusText(slot.planet) }}
</Badge>
<Popover v-if="getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)">
<PopoverTrigger as-child>
<Badge
variant="outline"
class="text-xs cursor-pointer hover:bg-amber-50 dark:hover:bg-amber-950/30 border-amber-300 dark:border-amber-700 text-amber-700 dark:text-amber-400 gap-1"
>
<Recycle class="h-3 w-3" />
</Badge>
</PopoverTrigger>
<PopoverContent class="w-auto p-3" side="top" align="start">
<div class="space-y-2">
<p class="text-xs font-semibold text-amber-700 dark:text-amber-400">{{ t('galaxyView.debrisField') }}</p>
<div class="space-y-1 text-xs">
<div class="flex items-center gap-2">
<ResourceIcon type="metal" size="sm" />
<span class="text-muted-foreground">{{ t('resources.metal') }}:</span>
<span class="font-medium">
{{ formatNumber(getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)!.resources.metal) }}
</span>
</div>
<div class="flex items-center gap-2">
<ResourceIcon type="crystal" size="sm" />
<span class="text-muted-foreground">{{ t('resources.crystal') }}:</span>
<span class="font-medium">
{{ formatNumber(getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)!.resources.crystal) }}
</span>
</div>
</div>
</div>
</PopoverContent>
</Popover>
</div>
<!-- 第二行:好感度 -->
<div v-if="!isMyPlanet(slot.planet) && getReputationValue(slot.planet) !== null" class="text-xs">
<span class="text-muted-foreground">{{ t('diplomacy.reputation') }}:</span>
<span class="ml-1 font-semibold" :class="getReputationColor(getReputationValue(slot.planet))">
{{ getReputationValue(slot.planet)! > 0 ? '+' : '' }}{{ getReputationValue(slot.planet) }}
</span>
</div>
</div>
<!-- 徽章组 -->
<div class="flex items-center gap-2 flex-wrap">
<Badge v-if="isMyPlanet(slot.planet)" variant="default" class="text-xs">{{ t('galaxyView.mine') }}</Badge>
<Badge v-else :variant="getRelationBadgeVariant(slot.planet)" class="text-xs">
{{ getRelationStatusText(slot.planet) }}
</Badge>
<!-- 残骸场徽章 - 紧凑显示 -->
<!-- 空位置 -->
<div v-else class="space-y-1">
<div class="text-sm text-muted-foreground">{{ t('galaxyView.emptySlot') }}</div>
<!-- 残骸场徽章 - 空位置时也显示 -->
<Popover v-if="getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)">
<PopoverTrigger as-child>
<Badge
variant="outline"
class="text-xs cursor-pointer hover:bg-amber-50 dark:hover:bg-amber-950/30 border-amber-300 dark:border-amber-700 text-amber-700 dark:text-amber-400 gap-1"
class="text-xs cursor-pointer hover:bg-amber-50 dark:hover:bg-amber-950/30 border-amber-300 dark:border-amber-700 text-amber-700 dark:text-amber-400 gap-1 inline-flex"
>
<Recycle class="h-3 w-3" />
<span class="hidden sm:inline">{{ t('galaxyView.debris') }}</span>
<span>{{ t('galaxyView.debris') }}</span>
</Badge>
</PopoverTrigger>
<PopoverContent class="w-auto p-3" side="top" align="start">
@@ -250,59 +293,195 @@
</Popover>
</div>
</div>
<!-- PC端显示坐标 -->
<p class="text-xs text-muted-foreground hidden sm:block">
[{{ slot.planet.position.galaxy }}:{{ slot.planet.position.system }}:{{ slot.planet.position.position }}]
</p>
<!-- 好感度显示仅NPC星球 -->
<div v-if="!isMyPlanet(slot.planet) && getReputationValue(slot.planet) !== null" class="text-xs">
<span class="text-muted-foreground">{{ t('diplomacy.reputation') }}:</span>
<span class="ml-1 font-semibold" :class="getReputationColor(getReputationValue(slot.planet))">
{{ getReputationValue(slot.planet)! > 0 ? '+' : '' }}{{ getReputationValue(slot.planet) }}
</span>
</div>
</div>
<!-- 空位置 -->
<div v-else class="space-y-1">
<div class="text-sm text-muted-foreground">{{ t('galaxyView.emptySlot') }}</div>
<!-- 残骸场徽章 - 空位置时也显示 -->
<Popover v-if="getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)">
<PopoverTrigger as-child>
<Badge
variant="outline"
class="text-xs cursor-pointer hover:bg-amber-50 dark:hover:bg-amber-950/30 border-amber-300 dark:border-amber-700 text-amber-700 dark:text-amber-400 gap-1 inline-flex"
>
<Recycle class="h-3 w-3" />
<span>{{ t('galaxyView.debris') }}</span>
</Badge>
</PopoverTrigger>
<PopoverContent class="w-auto p-3" side="top" align="start">
<div class="space-y-2">
<p class="text-xs font-semibold text-amber-700 dark:text-amber-400">{{ t('galaxyView.debrisField') }}</p>
<div class="space-y-1 text-xs">
<div class="flex items-center gap-2">
<ResourceIcon type="metal" size="sm" />
<span class="text-muted-foreground">{{ t('resources.metal') }}:</span>
<span class="font-medium">
{{ formatNumber(getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)!.resources.metal) }}
</span>
</div>
<div class="flex items-center gap-2">
<ResourceIcon type="crystal" size="sm" />
<span class="text-muted-foreground">{{ t('resources.crystal') }}:</span>
<span class="font-medium">
{{ formatNumber(getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)!.resources.crystal) }}
</span>
</div>
</div>
</div>
</PopoverContent>
</Popover>
<!-- 第三行:操作按钮 -->
<div class="flex gap-1 pl-10">
<TooltipProvider :delay-duration="300">
<Tooltip v-if="slot.planet && !isMyPlanet(slot.planet)">
<TooltipTrigger as-child>
<Button @click="showPlanetActions(slot.planet, 'spy')" variant="outline" size="sm" class="h-8 w-8 p-0">
<Eye class="h-3 w-3" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{{ t('galaxyView.scout') }}</p>
</TooltipContent>
</Tooltip>
<Tooltip v-if="slot.planet && !isMyPlanet(slot.planet)">
<TooltipTrigger as-child>
<Button @click="showPlanetActions(slot.planet, 'attack')" variant="outline" size="sm" class="h-8 w-8 p-0">
<Sword class="h-3 w-3" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{{ t('galaxyView.attack') }}</p>
</TooltipContent>
</Tooltip>
<Tooltip v-if="slot.planet && !isMyPlanet(slot.planet) && hasInterplanetaryMissiles">
<TooltipTrigger as-child>
<Button @click="showMissileAttackDialog(slot.planet)" variant="outline" size="sm" class="h-8 w-8 p-0">
<Bomb class="h-3 w-3" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{{ t('galaxyView.missileAttack') }}</p>
</TooltipContent>
</Tooltip>
<Tooltip v-if="slot.planet && !isMyPlanet(slot.planet) && getPlanetNPC(slot.planet)">
<TooltipTrigger as-child>
<Button @click="showPlanetActions(slot.planet, 'gift')" variant="outline" size="sm" class="h-8 w-8 p-0">
<Gift class="h-3 w-3" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{{ t('galaxyView.sendGift') }}</p>
</TooltipContent>
</Tooltip>
<Tooltip v-if="!slot.planet">
<TooltipTrigger as-child>
<Button @click="showPlanetActions(null, 'colonize', slot.position)" variant="outline" size="sm" class="h-8 w-8 p-0">
<Rocket class="h-3 w-3" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{{ t('galaxyView.colonize') }}</p>
</TooltipContent>
</Tooltip>
<Tooltip v-if="slot.planet && isMyPlanet(slot.planet)">
<TooltipTrigger as-child>
<Button @click="switchToPlanet(slot.planet.id)" variant="outline" size="sm" class="h-8 w-8 p-0">
<Home class="h-3 w-3" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{{ t('galaxyView.switch') }}</p>
</TooltipContent>
</Tooltip>
<Tooltip v-if="getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)">
<TooltipTrigger as-child>
<Button
@click="showPlanetActions(slot.planet, 'recycle', slot.position)"
variant="outline"
size="sm"
class="h-8 w-8 p-0"
>
<Recycle class="h-3 w-3" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{{ t('galaxyView.recycle') }}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</div>
<!-- 操作按钮 -->
<div class="flex gap-1 sm:gap-2 flex-shrink-0">
<!-- PC端布局位置编号 + 星球信息(水平) -->
<div class="hidden sm:flex items-center gap-4 flex-1 min-w-0">
<!-- 位置编号 -->
<div class="w-12 text-center flex-shrink-0">
<Badge variant="outline" class="text-sm">{{ slot.position }}</Badge>
</div>
<!-- 星球信息 -->
<div class="flex-1 min-w-0">
<div v-if="slot.planet" class="space-y-1">
<!-- PC端标题和徽章 -->
<div class="flex items-center gap-2 flex-wrap">
<h3 class="font-semibold text-base">{{ slot.planet.name }}</h3>
<Badge v-if="isMyPlanet(slot.planet)" variant="default" class="text-xs">{{ t('galaxyView.mine') }}</Badge>
<Badge v-else :variant="getRelationBadgeVariant(slot.planet)" class="text-xs">
{{ getRelationStatusText(slot.planet) }}
</Badge>
<!-- 残骸场徽章 -->
<Popover v-if="getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)">
<PopoverTrigger as-child>
<Badge
variant="outline"
class="text-xs cursor-pointer hover:bg-amber-50 dark:hover:bg-amber-950/30 border-amber-300 dark:border-amber-700 text-amber-700 dark:text-amber-400 gap-1"
>
<Recycle class="h-3 w-3" />
<span>{{ t('galaxyView.debris') }}</span>
</Badge>
</PopoverTrigger>
<PopoverContent class="w-auto p-3" side="top" align="start">
<div class="space-y-2">
<p class="text-xs font-semibold text-amber-700 dark:text-amber-400">{{ t('galaxyView.debrisField') }}</p>
<div class="space-y-1 text-xs">
<div class="flex items-center gap-2">
<ResourceIcon type="metal" size="sm" />
<span class="text-muted-foreground">{{ t('resources.metal') }}:</span>
<span class="font-medium">
{{ formatNumber(getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)!.resources.metal) }}
</span>
</div>
<div class="flex items-center gap-2">
<ResourceIcon type="crystal" size="sm" />
<span class="text-muted-foreground">{{ t('resources.crystal') }}:</span>
<span class="font-medium">
{{ formatNumber(getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)!.resources.crystal) }}
</span>
</div>
</div>
</div>
</PopoverContent>
</Popover>
</div>
<!-- PC端坐标 -->
<p class="text-xs text-muted-foreground">
[{{ slot.planet.position.galaxy }}:{{ slot.planet.position.system }}:{{ slot.planet.position.position }}]
</p>
<!-- PC端好感度显示仅NPC星球 -->
<div v-if="!isMyPlanet(slot.planet) && getReputationValue(slot.planet) !== null" class="text-xs">
<span class="text-muted-foreground">{{ t('diplomacy.reputation') }}:</span>
<span class="ml-1 font-semibold" :class="getReputationColor(getReputationValue(slot.planet))">
{{ getReputationValue(slot.planet)! > 0 ? '+' : '' }}{{ getReputationValue(slot.planet) }}
</span>
</div>
</div>
<!-- 空位置 -->
<div v-else class="space-y-1">
<div class="text-sm text-muted-foreground">{{ t('galaxyView.emptySlot') }}</div>
<!-- 残骸场徽章 - 空位置时也显示 -->
<Popover v-if="getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)">
<PopoverTrigger as-child>
<Badge
variant="outline"
class="text-xs cursor-pointer hover:bg-amber-50 dark:hover:bg-amber-950/30 border-amber-300 dark:border-amber-700 text-amber-700 dark:text-amber-400 gap-1 inline-flex"
>
<Recycle class="h-3 w-3" />
<span>{{ t('galaxyView.debris') }}</span>
</Badge>
</PopoverTrigger>
<PopoverContent class="w-auto p-3" side="top" align="start">
<div class="space-y-2">
<p class="text-xs font-semibold text-amber-700 dark:text-amber-400">{{ t('galaxyView.debrisField') }}</p>
<div class="space-y-1 text-xs">
<div class="flex items-center gap-2">
<ResourceIcon type="metal" size="sm" />
<span class="text-muted-foreground">{{ t('resources.metal') }}:</span>
<span class="font-medium">
{{ formatNumber(getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)!.resources.metal) }}
</span>
</div>
<div class="flex items-center gap-2">
<ResourceIcon type="crystal" size="sm" />
<span class="text-muted-foreground">{{ t('resources.crystal') }}:</span>
<span class="font-medium">
{{ formatNumber(getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)!.resources.crystal) }}
</span>
</div>
</div>
</div>
</PopoverContent>
</Popover>
</div>
</div>
</div>
<!-- 操作按钮 (PC端) -->
<div class="hidden sm:flex gap-1 sm:gap-2 flex-shrink-0">
<TooltipProvider :delay-duration="300">
<Tooltip v-if="slot.planet && !isMyPlanet(slot.planet)">
<TooltipTrigger as-child>
@@ -324,6 +503,16 @@
<p>{{ t('galaxyView.attack') }}</p>
</TooltipContent>
</Tooltip>
<Tooltip v-if="slot.planet && !isMyPlanet(slot.planet) && hasInterplanetaryMissiles">
<TooltipTrigger as-child>
<Button @click="showMissileAttackDialog(slot.planet)" variant="outline" size="sm" class="h-8 w-8 p-0">
<Bomb class="h-3 w-3 sm:h-4 sm:w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{{ t('galaxyView.missileAttack') }}</p>
</TooltipContent>
</Tooltip>
<Tooltip v-if="slot.planet && !isMyPlanet(slot.planet) && getPlanetNPC(slot.planet)">
<TooltipTrigger as-child>
<Button @click="showPlanetActions(slot.planet, 'gift')" variant="outline" size="sm" class="h-8 w-8 p-0">
@@ -376,6 +565,60 @@
</CardContent>
</Card>
<!-- 导弹攻击对话框 -->
<Dialog :open="missileDialogOpen" @update:open="missileDialogOpen = $event">
<DialogContent>
<DialogHeader>
<DialogTitle>{{ t('galaxyView.missileAttackTitle') }}</DialogTitle>
<DialogDescription v-if="missileTargetPlanet">
{{
t('galaxyView.missileAttackMessage').replace(
'{coordinates}',
`${missileTargetPlanet.position.galaxy}:${missileTargetPlanet.position.system}:${missileTargetPlanet.position.position}`
)
}}
</DialogDescription>
</DialogHeader>
<div v-if="gameStore.currentPlanet && missileTargetPlanet" class="space-y-4">
<!-- 导弹数量输入 -->
<div class="space-y-2">
<Label>{{ t('galaxyView.missileCount') }}</Label>
<Input
v-model.number="missileCount"
type="number"
min="1"
:max="gameStore.currentPlanet.defense['interplanetaryMissile'] || 0"
/>
<p class="text-sm text-muted-foreground">
{{ t('galaxyView.availableMissiles') }}: {{ gameStore.currentPlanet.defense['interplanetaryMissile'] || 0 }}
</p>
</div>
<!-- 射程和距离信息 -->
<div class="space-y-2 text-sm">
<div class="flex justify-between">
<span class="text-muted-foreground">{{ t('galaxyView.missileRange') }}:</span>
<span>{{ calculateMissileRange() }} {{ t('galaxyView.systems') }}</span>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground">{{ t('galaxyView.distance') }}:</span>
<span>{{ calculateDistance(missileTargetPlanet) }} {{ t('galaxyView.systems') }}</span>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground">{{ t('galaxyView.flightTime') }}:</span>
<span>{{ formatFlightTime(calculateDistance(missileTargetPlanet)) }}</span>
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" @click="missileDialogOpen = false">{{ t('galaxyView.cancel') }}</Button>
<Button @click="launchMissileAttack">{{ t('galaxyView.launchMissile') }}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<!-- 快速派遣对话框 -->
<AlertDialog :open="alertDialogOpen" @update:open="alertDialogOpen = $event">
<AlertDialogContent>
@@ -405,10 +648,12 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Label } from '@/components/ui/label'
import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge'
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui/select'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover'
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import {
AlertDialog,
AlertDialogAction,
@@ -420,7 +665,7 @@
AlertDialogTitle
} from '@/components/ui/alert-dialog'
import ResourceIcon from '@/components/ResourceIcon.vue'
import { Home, Eye, Sword, Rocket, Recycle, Gift, Globe } from 'lucide-vue-next'
import { Home, Eye, Sword, Rocket, Recycle, Gift, Globe, Bomb } from 'lucide-vue-next'
import { useRouter, useRoute } from 'vue-router'
import * as gameLogic from '@/logic/gameLogic'
import { formatNumber } from '@/utils/format'
@@ -438,6 +683,11 @@
const alertDialogMessage = ref('')
const alertDialogConfirmAction = ref<(() => void) | null>(null)
// 导弹攻击对话框状态
const missileDialogOpen = ref(false)
const missileTargetPlanet = ref<Planet | null>(null)
const missileCount = ref(1)
const selectedGalaxy = ref(1)
const selectedSystem = ref(1)
const currentGalaxy = ref(1)
@@ -465,6 +715,12 @@
return gameStore.player.planets.filter(p => !p.isMoon)
})
// 检查当前星球是否有星际导弹
const hasInterplanetaryMissiles = computed(() => {
if (!gameStore.currentPlanet) return false
return (gameStore.currentPlanet.defense['interplanetaryMissile'] || 0) > 0
})
// 判断当前是否在母星所在星系
const isInHomePlanetSystem = computed(() => {
if (!homePlanet.value) return false
@@ -680,4 +936,80 @@
}
alertDialogOpen.value = true
}
// 显示导弹攻击对话框
const showMissileAttackDialog = (planet: Planet) => {
missileTargetPlanet.value = planet
missileCount.value = 1
missileDialogOpen.value = true
}
// 执行导弹攻击
const launchMissileAttack = () => {
if (!missileTargetPlanet.value || !gameStore.currentPlanet) return
// 导入missileLogic进行验证和执行
import('@/logic/missileLogic').then(missileLogic => {
const validation = missileLogic.validateMissileLaunch(
gameStore.currentPlanet!,
missileTargetPlanet.value!.position,
missileCount.value,
gameStore.player.technologies
)
if (!validation.valid) {
alertDialogTitle.value = t('errors.launchFailed')
alertDialogMessage.value = t(validation.reason || 'errors.unknown')
alertDialogOpen.value = true
return
}
// 创建导弹攻击任务
const missileAttack = missileLogic.createMissileAttack(
gameStore.player.id,
gameStore.currentPlanet!,
missileTargetPlanet.value!.position,
missileTargetPlanet.value!.id,
missileCount.value
)
// 扣除导弹
missileLogic.executeMissileLaunch(gameStore.currentPlanet!, missileCount.value)
// 添加到玩家的导弹攻击列表
gameStore.player.missileAttacks.push(missileAttack)
// 关闭对话框
missileDialogOpen.value = false
// 显示成功提示
alertDialogTitle.value = t('common.success')
alertDialogMessage.value = t('galaxyView.missileLaunched')
alertDialogOpen.value = true
})
}
// 计算导弹射程
const calculateMissileRange = () => {
const impulseDriveLevel = gameStore.player.technologies['impulseDrive'] || 0
if (impulseDriveLevel === 0) return 0
return 5 * impulseDriveLevel - 1
}
// 计算到目标的距离
const calculateDistance = (target: Planet) => {
if (!gameStore.currentPlanet) return 0
const from = gameStore.currentPlanet.position
const to = target.position
if (from.galaxy !== to.galaxy) return Infinity
return Math.abs(from.system - to.system)
}
// 格式化飞行时间
const formatFlightTime = (distance: number) => {
const seconds = 30 + distance * 60
const minutes = Math.floor(seconds / 60)
const secs = seconds % 60
return `${minutes}:${secs.toString().padStart(2, '0')}`
}
</script>

View File

@@ -580,7 +580,8 @@
[MissionType.Colonize]: t('fleetView.colonize'),
[MissionType.Deploy]: t('fleetView.deploy'),
[MissionType.Recycle]: t('fleetView.recycle'),
[MissionType.Destroy]: t('fleetView.destroy')
[MissionType.Destroy]: t('fleetView.destroy'),
[MissionType.MissileAttack]: t('galaxyView.missileAttack')
}
return typeMap[missionType] || missionType
}
@@ -615,7 +616,7 @@
const acceptGift = (gift: GiftNotification) => {
const npc = npcStore.npcs.find(n => n.id === gift.fromNpcId)
if (npc) {
diplomaticLogic.acceptNPCGift(gameStore.player, npc, gift)
diplomaticLogic.acceptNPCGift(gameStore.player, npc, gift, gameStore.locale)
}
}
@@ -623,7 +624,7 @@
const rejectGift = (gift: GiftNotification) => {
const npc = npcStore.npcs.find(n => n.id === gift.fromNpcId)
if (npc) {
diplomaticLogic.rejectNPCGift(gameStore.player, npc, gift)
diplomaticLogic.rejectNPCGift(gameStore.player, npc, gift, gameStore.locale)
}
}

View File

@@ -0,0 +1,34 @@
<template>
<div class="container mx-auto p-4 sm:p-6 flex items-center justify-center min-h-[60vh]">
<Empty class="border-0">
<EmptyMedia>
<div class="text-8xl sm:text-9xl font-bold text-muted-foreground/20">404</div>
</EmptyMedia>
<EmptyHeader>
<EmptyTitle>{{ t('notFound.title') }}</EmptyTitle>
<EmptyDescription>{{ t('notFound.description') }}</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button @click="goHome" size="lg">
<Home class="mr-2 h-4 w-4" />
{{ t('notFound.goHome') }}
</Button>
</EmptyContent>
</Empty>
</div>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router'
import { useI18n } from '@/composables/useI18n'
import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '@/components/ui/empty'
import { Button } from '@/components/ui/button'
import { Home } from 'lucide-vue-next'
const router = useRouter()
const { t } = useI18n()
const goHome = () => {
router.push('/')
}
</script>

View File

@@ -59,6 +59,22 @@
<CardDescription>{{ t('settings.gameSettingsDesc') }}</CardDescription>
</CardHeader>
<CardContent class="space-y-4">
<!-- 游戏倍率 -->
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between p-4 border rounded-lg gap-3">
<div class="space-y-1 flex-1">
<h3 class="font-medium">{{ t('settings.gameSpeed') }}</h3>
<p class="text-sm text-muted-foreground">{{ t('settings.gameSpeedDesc') }}</p>
</div>
<div class="flex items-center gap-2 sm:gap-4 w-full sm:w-auto">
<div class="flex items-center gap-2 flex-1 sm:flex-initial">
<Button @click="decreaseSpeed" variant="outline" size="sm" :disabled="gameStore.gameSpeed <= 0.5">-</Button>
<span class="min-w-[60px] text-center font-medium">{{ gameStore.gameSpeed || 1 }}x</span>
<Button @click="increaseSpeed" variant="outline" size="sm" :disabled="gameStore.gameSpeed >= 10">+</Button>
</div>
<Button @click="resetSpeed" variant="ghost" size="sm">{{ t('settings.reset') }}</Button>
</div>
</div>
<!-- 游戏暂停 -->
<div class="flex items-center justify-between p-4 border rounded-lg">
<div class="space-y-1">
@@ -88,6 +104,15 @@
<span class="text-muted-foreground">{{ t('settings.buildDate') }}:</span>
<span class="font-medium">{{ pkg.buildDate }}</span>
</div>
<!-- 检查更新按钮 -->
<div class="pt-2">
<Button @click="handleCheckVersion" variant="outline" size="sm" :disabled="isCheckingVersion || !canCheck" class="w-full">
<RefreshCw class="mr-2 h-4 w-4" :class="{ 'animate-spin': isCheckingVersion }" />
<template v-if="isCheckingVersion">{{ t('settings.checking') }}</template>
<template v-else-if="!canCheck && cooldownTime">{{ cooldownTime }}</template>
<template v-else>{{ t('settings.checkUpdate') }}</template>
</Button>
</div>
</div>
<!-- 社区链接 -->
@@ -127,11 +152,14 @@
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<!-- 更新对话框 -->
<UpdateDialog v-model:open="showUpdateDialog" :version-info="updateInfo" />
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useGameStore } from '@/stores/gameStore'
import { useI18n } from '@/composables/useI18n'
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'
@@ -146,23 +174,64 @@
AlertDialogHeader,
AlertDialogTitle
} from '@/components/ui/alert-dialog'
import { Download, Upload, Trash2, ExternalLink, MessagesSquare, Play, Pause } from 'lucide-vue-next'
import { Download, Upload, Trash2, ExternalLink, MessagesSquare, Play, Pause, RefreshCw } from 'lucide-vue-next'
import { saveAs } from 'file-saver'
import { toast } from 'vue-sonner'
import pkg from '../../package.json'
import 'vue-sonner/style.css'
import { checkLatestVersion, canCheckVersion } from '@/utils/versionCheck'
import type { VersionInfo } from '@/utils/versionCheck'
import UpdateDialog from '@/components/UpdateDialog.vue'
const { t } = useI18n()
const gameStore = useGameStore()
const fileInputRef = ref<HTMLInputElement>()
const isExporting = ref(false)
const isCheckingVersion = ref(false)
const cooldownTime = ref('')
const showConfirmDialog = ref(false)
const confirmTitle = ref('')
const confirmMessage = ref('')
let confirmCallback: (() => void) | null = null
// 计算是否可以检查版本主动检测5分钟内不能重复检查
const canCheck = computed(() => canCheckVersion(gameStore.player.lastManualUpdateCheck || 0))
// 计算剩余冷却时间
const updateCooldownTime = () => {
if (canCheck.value) {
cooldownTime.value = ''
return
}
const lastCheck = gameStore.player.lastManualUpdateCheck || 0
const now = Date.now()
const fiveMinutes = 5 * 60 * 1000
const timePassed = now - lastCheck
const timeRemaining = fiveMinutes - timePassed
if (timeRemaining <= 0) {
cooldownTime.value = ''
return
}
const minutes = Math.floor(timeRemaining / 60000)
const seconds = Math.floor((timeRemaining % 60000) / 1000)
cooldownTime.value = `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`
}
// 每秒更新倒计时
let cooldownInterval: ReturnType<typeof setInterval> | null = null
onMounted(() => {
updateCooldownTime()
cooldownInterval = setInterval(updateCooldownTime, 1000)
})
onUnmounted(() => {
if (cooldownInterval) clearInterval(cooldownInterval)
})
const openGithub = () => {
window.open(`https://github.com/${pkg.author.name}/${pkg.name}`, '_blank')
}
@@ -171,6 +240,32 @@
window.open(`https://qm.qq.com/q/${pkg.id}`, '_blank')
}
// 手动检查版本
const showUpdateDialog = ref(false)
const updateInfo = ref<VersionInfo | null>(null)
const handleCheckVersion = async () => {
if (isCheckingVersion.value || !canCheck.value) return
isCheckingVersion.value = true
try {
const versionInfo = await checkLatestVersion(gameStore.player.lastManualUpdateCheck || 0, (time: number) => {
gameStore.player.lastManualUpdateCheck = time
})
if (versionInfo) {
updateInfo.value = versionInfo
showUpdateDialog.value = true
} else {
toast.success(t('settings.upToDate'))
}
} catch (error) {
console.error('Failed to check for updates:', error)
toast.error(t('settings.checkUpdateFailed'))
} finally {
isCheckingVersion.value = false
}
}
// 导出数据(包含游戏数据和地图数据)
const handleExport = async () => {
try {
@@ -281,10 +376,37 @@
}
const clearData = () => {
// 清除localStorage
localStorage.clear()
// 重新加载页面
window.location.reload()
gameStore.isPaused = true
try {
localStorage.clear()
window.location.reload()
} catch (error) {
console.error('Failed to clear data:', error)
// 即使出错也尝试重新加载
window.location.reload()
}
}
// 增加游戏倍率
const increaseSpeed = () => {
if (gameStore.gameSpeed < 10) {
gameStore.gameSpeed = Math.min(10, gameStore.gameSpeed + 0.5)
toast.success(t('settings.speedChanged', { speed: gameStore.gameSpeed }))
}
}
// 减少游戏倍率
const decreaseSpeed = () => {
if (gameStore.gameSpeed > 0.5) {
gameStore.gameSpeed = Math.max(0.5, gameStore.gameSpeed - 0.5)
toast.success(t('settings.speedChanged', { speed: gameStore.gameSpeed }))
}
}
// 重置游戏倍率
const resetSpeed = () => {
gameStore.gameSpeed = 1
toast.success(t('settings.speedReset'))
}
// 切换游戏暂停状态

View File

@@ -304,6 +304,7 @@ const simulateBattle = (attacker: BattleSideData, defender: BattleSideData, maxR
} else if (defenderUnits.length === 0) {
winner = 'attacker'
} else {
// OGame原版规则6回合后双方都有剩余单位时判定为平局
winner = 'draw'
}