feat: 新增战报弹窗与舰队模拟器,重构UI组件

新增 BattleReportDialog、SpyReportDialog、NumberWithTooltip 等组件,完善舰队模拟器功能。重构并引入 Sheet、Sidebar、Tooltip、Skeleton 等 UI 组件,优化界面结构。实现 battle.worker 支持战斗计算,增加 universeStore、fleetStorageLogic 等核心逻辑,完善多语言与类型定义。
This commit is contained in:
谦君
2025-12-13 11:14:23 +08:00
parent 8637e50115
commit 731d79673b
160 changed files with 6302 additions and 1931 deletions

View File

@@ -1,150 +1,131 @@
<template>
<div class="flex h-screen bg-background overflow-hidden">
<!-- 遮罩层移动端 -->
<div v-if="!gameStore.sidebarCollapsed" class="fixed inset-0 bg-black/50 z-30 lg:hidden" @click="toggleSidebar" />
<!-- 侧边导航栏 -->
<aside
:class="[
'border-r bg-card flex flex-col transition-all duration-300 ease-in-out shadow-lg z-40',
'fixed lg:relative h-full',
gameStore.sidebarCollapsed ? '-translate-x-full lg:translate-x-0 lg:w-16' : 'translate-x-0 w-64'
]"
>
<SidebarProvider :open="sidebarOpen" @update:open="sidebarOpen = $event">
<Sidebar collapsible="icon">
<!-- Logo -->
<div class="p-4 border-b flex items-center justify-center">
<h1 v-if="!gameStore.sidebarCollapsed" class="text-xl font-bold flex items-center gap-2">
<span class="text-2xl">
<img src="@/assets/logo.svg" class="w-10" />
</span>
{{ pkg.title }}
</h1>
<span v-else class="text-2xl">
<img src="@/assets/logo.svg" class="w-10" />
</span>
</div>
<!-- 星球信息 -->
<div v-if="planet && !gameStore.sidebarCollapsed" class="p-4 border-b">
<div class="text-sm space-y-2">
<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>
<!-- 玩家积分显示 -->
<div class="bg-muted/50 rounded-lg p-2">
<div class="flex items-center justify-between">
<span class="text-xs text-muted-foreground">{{ t('player.points') }}</span>
<span class="text-sm font-bold text-primary">{{ formatNumber(gameStore.player.points) }}</span>
</div>
</div>
<!-- 月球切换按钮 -->
<div v-if="hasMoon || planet.isMoon" class="flex gap-1">
<Button v-if="planet.isMoon" @click="switchToParentPlanet" variant="outline" size="sm" class="w-full text-xs h-7">
{{ t('planet.backToPlanet') }}
</Button>
<Button v-else-if="moon" @click="switchToMoon" variant="outline" size="sm" class="w-full text-xs h-7">
{{ t('planet.switchToMoon') }}
</Button>
</div>
<SidebarHeader class="border-b">
<div class="flex items-center justify-center p-4 group-data-[collapsible=icon]:p-2">
<img src="@/assets/logo.svg" class="w-10 group-data-[collapsible=icon]:w-8" />
<h1 class="text-xl font-bold ml-2 group-data-[collapsible=icon]:hidden">{{ pkg.title }}</h1>
</div>
</div>
</SidebarHeader>
<!-- 导航菜单 -->
<nav class="flex-1 p-2 space-y-1 overflow-y-auto">
<RouterLink v-for="item in navItems" :key="item.path" :to="item.path" v-slot="{ isActive: routeActive }">
<Button
:variant="routeActive ? 'secondary' : 'ghost'"
:class="['w-full transition-all', gameStore.sidebarCollapsed ? 'justify-center px-0' : 'justify-start']"
:title="gameStore.sidebarCollapsed ? item.name.value : undefined"
>
<component :is="item.icon" :class="['h-4 w-4', !gameStore.sidebarCollapsed && 'mr-3']" />
<span v-if="!gameStore.sidebarCollapsed">{{ item.name.value }}</span>
</Button>
</RouterLink>
</nav>
<!-- 语言切换 -->
<div class="p-2 border-t">
<Popover>
<PopoverTrigger as-child>
<Button variant="ghost" class="w-full" size="sm">
<Languages class="h-4 w-4" />
<span v-if="!gameStore.sidebarCollapsed" class="ml-2">{{ localeNames[gameStore.locale] }}</span>
</Button>
</PopoverTrigger>
<PopoverContent class="w-48 p-2" :align="gameStore.sidebarCollapsed ? 'start' : 'center'">
<div class="space-y-1">
<Button
v-for="locale in locales"
:key="locale"
@click="gameStore.locale = locale"
:variant="gameStore.locale === locale ? 'secondary' : 'ghost'"
class="w-full justify-start"
size="sm"
>
{{ localeNames[locale] }}
<SidebarContent>
<!-- 星球信息 -->
<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>
<!-- 玩家积分显示 -->
<div class="bg-muted/50 rounded-lg p-2">
<div class="flex items-center justify-between">
<span class="text-xs text-muted-foreground">{{ t('player.points') }}</span>
<span class="text-sm font-bold text-primary">{{ formatNumber(gameStore.player.points) }}</span>
</div>
</div>
<!-- 月球切换按钮 -->
<div v-if="hasMoon || planet.isMoon" class="flex gap-1">
<Button v-if="planet.isMoon" @click="switchToParentPlanet" variant="outline" size="sm" class="w-full text-xs h-7">
{{ t('planet.backToPlanet') }}
</Button>
<Button v-else-if="moon" @click="switchToMoon" variant="outline" size="sm" class="w-full text-xs h-7">
{{ t('planet.switchToMoon') }}
</Button>
</div>
</PopoverContent>
</Popover>
</div>
</div>
</SidebarGroup>
<!-- 夜间模式切换 -->
<div class="p-2 border-t">
<Button @click="isDark = !isDark" variant="ghost" class="w-full" size="sm">
<Sun v-if="isDark" class="h-4 w-4" />
<Moon v-else class="h-4 w-4" />
<span v-if="!gameStore.sidebarCollapsed" class="ml-2">{{ isDark ? t('sidebar.lightMode') : t('sidebar.darkMode') }}</span>
</Button>
</div>
<div class="p-2 border-t">
<Button @click="toggleSidebar" variant="ghost" class="w-full" size="sm">
<ChevronLeft v-if="!gameStore.sidebarCollapsed" class="h-4 w-4" />
<ChevronRight v-else class="h-4 w-4" />
<span v-if="!gameStore.sidebarCollapsed" class="ml-2">{{ t('sidebar.collapse') }}</span>
</Button>
</div>
</aside>
<!-- 导航菜单 -->
<SidebarGroup>
<SidebarMenu>
<SidebarMenuItem v-for="item in navItems" :key="item.path">
<SidebarMenuButton as-child :is-active="$route.path === item.path" :tooltip="item.name.value">
<RouterLink :to="item.path">
<component :is="item.icon" />
<span>{{ item.name.value }}</span>
<!-- 未读消息数量 -->
<SidebarMenuBadge v-if="item.path === '/messages' && unreadMessagesCount > 0">
{{ unreadMessagesCount }}
</SidebarMenuBadge>
</RouterLink>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
</SidebarContent>
<!-- 底部设置 -->
<SidebarFooter class="border-t">
<SidebarMenu>
<!-- 语言切换 -->
<SidebarMenuItem>
<Popover>
<PopoverTrigger as-child>
<SidebarMenuButton :tooltip="localeNames[gameStore.locale]">
<Languages />
<span>{{ localeNames[gameStore.locale] }}</span>
</SidebarMenuButton>
</PopoverTrigger>
<PopoverContent class="w-48 p-2" side="right" align="end">
<div class="space-y-1">
<Button
v-for="locale in locales"
:key="locale"
@click="gameStore.locale = locale"
:variant="gameStore.locale === locale ? 'secondary' : 'ghost'"
class="w-full justify-start"
size="sm"
>
{{ localeNames[locale] }}
</Button>
</div>
</PopoverContent>
</Popover>
</SidebarMenuItem>
<!-- 夜间模式切换 -->
<SidebarMenuItem>
<SidebarMenuButton @click="isDark = !isDark" :tooltip="isDark ? t('sidebar.lightMode') : t('sidebar.darkMode')">
<Sun v-if="isDark" />
<Moon v-else />
<span>{{ isDark ? t('sidebar.lightMode') : t('sidebar.darkMode') }}</span>
</SidebarMenuButton>
</SidebarMenuItem>
<!-- 折叠按钮 -->
<SidebarMenuItem class="hidden sm:inline">
<SidebarMenuButton @click="toggleSidebar" :tooltip="sidebarOpen ? t('sidebar.collapse') : t('sidebar.expand')">
<ChevronsLeft class="group-data-[state=collapsed]:rotate-180 transition-transform" />
<span>{{ t('sidebar.collapse') }}</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarFooter>
</Sidebar>
<!-- 主内容区 -->
<div class="flex-1 flex flex-col overflow-hidden">
<!-- 顶部资源栏 -->
<header v-if="planet" class="bg-card border-b px-4 sm:px-6 py-4.5 shadow-md">
<div class="flex items-center justify-between gap-3 sm:gap-6">
<!-- 汉堡菜单移动端- 左侧占位 -->
<div class="lg:flex-1">
<Button @click="toggleSidebar" variant="ghost" size="icon" class="lg:hidden h-8 w-8">
<component :is="gameStore.sidebarCollapsed ? Menu : X" class="h-5 w-5" />
</Button>
</div>
<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>
<!-- 资源显示 - 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">
<!-- 电量显示 -->
<template v-if="resourceType.key === 'energy'">
<p
class="text-xs sm:text-sm font-medium truncate"
:class="
planet.resources[resourceType.key] >= 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'
"
>
{{ formatNumber(planet.resources[resourceType.key]) }}
</p>
<p class="text-[10px] sm:text-xs text-muted-foreground truncate">
{{ formatNumber(energyProduction || 0) }} / {{ formatNumber(energyConsumption || 0) }}
</p>
</template>
<!-- 其他资源显示 -->
<template v-else>
<!-- 资源显示 - 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)"
@@ -152,105 +133,116 @@
{{ formatNumber(planet.resources[resourceType.key]) }} / {{ formatNumber(capacity?.[resourceType.key] || 0) }}
</p>
<p class="text-[10px] sm:text-xs text-muted-foreground truncate">
+{{ formatNumber(production?.[resourceType.key] || 0) }}/{{ t('resources.perHour') }}
+{{ formatNumber(Math.round((production?.[resourceType.key] || 0) / 60)) }}/{{ t('resources.perMinute') }}
</p>
</template>
</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>
</div>
</div>
</header>
<!-- 右侧状态 - 右侧占位 -->
<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
v-if="planet && (planet.buildQueue.length > 0 || gameStore.player.researchQueue.length > 0)"
class="bg-card border-b px-4 sm:px-6 py-4.5"
>
<div class="space-y-3">
<!-- 建造队列 -->
<div v-for="item in planet.buildQueue" :key="item.id" class="space-y-1.5">
<div class="flex items-center justify-between text-xs sm:text-sm gap-2">
<div class="flex items-center gap-1.5 sm:gap-2 min-w-0 flex-1">
<div class="h-2 w-2 rounded-full bg-green-500 animate-pulse flex-shrink-0" />
<span class="font-medium truncate">{{ getItemName(item) }}</span>
<span class="text-muted-foreground hidden sm:inline flex-shrink-0 text-[10px] sm:text-xs">
<template v-if="item.type === 'ship' || item.type === 'defense'">
{{ t('queue.quantity') }} {{ item.quantity }}
</template>
<template v-else> {{ t('queue.level') }} {{ item.targetLevel }}</template>
</span>
</div>
<div class="flex items-center gap-2 sm:gap-3 flex-shrink-0">
<span class="text-muted-foreground text-[10px] sm:text-xs whitespace-nowrap">
{{ formatTime(getRemainingTime(item)) }}
</span>
<Button
@click="handleCancelBuild(item.id)"
variant="ghost"
size="sm"
class="h-5 sm:h-6 px-1.5 sm:px-2 text-[10px] sm:text-xs"
>
{{ t('queue.cancel') }}
</Button>
</div>
</div>
<Progress :model-value="getQueueProgress(item)" class="h-1.5" />
</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 v-for="item in gameStore.player.researchQueue" :key="item.id" class="space-y-1.5">
<div class="flex items-center justify-between text-xs sm:text-sm gap-2">
<div class="flex items-center gap-1.5 sm:gap-2 min-w-0 flex-1">
<div class="h-2 w-2 rounded-full bg-blue-500 animate-pulse flex-shrink-0" />
<span class="font-medium truncate">{{ getItemName(item) }}</span>
<span class="text-muted-foreground hidden sm:inline flex-shrink-0 text-[10px] sm:text-xs">
{{ t('queue.level') }} {{ item.targetLevel }}
</span>
</div>
<div class="flex items-center gap-2 sm:gap-3 flex-shrink-0">
<span class="text-muted-foreground text-[10px] sm:text-xs whitespace-nowrap">
{{ formatTime(getRemainingTime(item)) }}
</span>
<Button
@click="handleCancelResearch(item.id)"
variant="ghost"
size="sm"
class="h-5 sm:h-6 px-1.5 sm:px-2 text-[10px] sm:text-xs"
>
{{ t('queue.cancel') }}
</Button>
</div>
</div>
<Progress :model-value="getQueueProgress(item)" class="h-1.5" />
</div>
</div>
</div>
</header>
<!-- 建造队列 -->
<div
v-if="planet && (planet.buildQueue.length > 0 || gameStore.player.researchQueue.length > 0)"
class="bg-card border-b px-4 sm:px-6 py-4.5"
>
<div class="space-y-3">
<!-- 建造队列 -->
<div v-for="item in planet.buildQueue" :key="item.id" class="space-y-1.5">
<div class="flex items-center justify-between text-xs sm:text-sm gap-2">
<div class="flex items-center gap-1.5 sm:gap-2 min-w-0 flex-1">
<div class="h-2 w-2 rounded-full bg-green-500 animate-pulse flex-shrink-0" />
<span class="font-medium truncate">{{ getItemName(item) }}</span>
<span class="text-muted-foreground hidden sm:inline flex-shrink-0 text-[10px] sm:text-xs">
{{ t('queue.level') }} {{ item.targetLevel }}
</span>
</div>
<div class="flex items-center gap-2 sm:gap-3 flex-shrink-0">
<span class="text-muted-foreground text-[10px] sm:text-xs whitespace-nowrap">{{ formatTime(getRemainingTime(item)) }}</span>
<Button
@click="handleCancelBuild(item.id)"
variant="ghost"
size="sm"
class="h-5 sm:h-6 px-1.5 sm:px-2 text-[10px] sm:text-xs"
>
{{ t('queue.cancel') }}
</Button>
</div>
</div>
<Progress :model-value="getQueueProgress(item)" class="h-1.5" />
<!-- 内容区域 -->
<main class="flex-1 overflow-y-auto">
<div class="animate-fade-in">
<RouterView />
</div>
<!-- 研究队列 -->
<div v-for="item in gameStore.player.researchQueue" :key="item.id" class="space-y-1.5">
<div class="flex items-center justify-between text-xs sm:text-sm gap-2">
<div class="flex items-center gap-1.5 sm:gap-2 min-w-0 flex-1">
<div class="h-2 w-2 rounded-full bg-blue-500 animate-pulse flex-shrink-0" />
<span class="font-medium truncate">{{ getItemName(item) }}</span>
<span class="text-muted-foreground hidden sm:inline flex-shrink-0 text-[10px] sm:text-xs">
{{ t('queue.level') }} {{ item.targetLevel }}
</span>
</div>
<div class="flex items-center gap-2 sm:gap-3 flex-shrink-0">
<span class="text-muted-foreground text-[10px] sm:text-xs whitespace-nowrap">{{ formatTime(getRemainingTime(item)) }}</span>
<Button
@click="handleCancelResearch(item.id)"
variant="ghost"
size="sm"
class="h-5 sm:h-6 px-1.5 sm:px-2 text-[10px] sm:text-xs"
>
{{ t('queue.cancel') }}
</Button>
</div>
</div>
<Progress :model-value="getQueueProgress(item)" class="h-1.5" />
</div>
</div>
</main>
</div>
<!-- 内容区域 -->
<main class="flex-1 overflow-y-auto">
<div class="animate-fade-in">
<RouterView />
</div>
</main>
</div>
</SidebarInset>
<!-- 确认对话框 -->
<ConfirmDialog ref="confirmDialog" />
<!-- 详情弹窗 -->
<DetailDialog />
</div>
<!-- Toast 通知 -->
<Sonner position="top-center" />
</SidebarProvider>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, computed, ref } from 'vue'
import { RouterView, RouterLink } from 'vue-router'
import { useGameStore } from '@/stores/gameStore'
import { useUniverseStore } from '@/stores/universeStore'
import { useTheme } from '@/composables/useTheme'
import { useI18n } from '@/composables/useI18n'
import { localeNames, detectBrowserLocale, type Locale } from '@/locales'
@@ -258,12 +250,26 @@
import { Badge } from '@/components/ui/badge'
import { Progress } from '@/components/ui/progress'
import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover'
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarHeader,
SidebarInset,
SidebarMenu,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarProvider,
SidebarTrigger
} from '@/components/ui/sidebar'
import ResourceIcon from '@/components/ResourceIcon.vue'
import ConfirmDialog from '@/components/ConfirmDialog.vue'
import DetailDialog from '@/components/DetailDialog.vue'
import { BuildingType, TechnologyType, ShipType, DefenseType, MissionType } from '@/types/game'
import Sonner from '@/components/ui/sonner/Sonner.vue'
import { MissionType } from '@/types/game'
import type { BuildQueueItem, FleetMission } from '@/types/game'
import { BUILDINGS, TECHNOLOGIES, SHIPS, DEFENSES } from '@/config/gameConfig'
import { formatNumber, formatTime, getResourceColor } from '@/utils/format'
import {
Moon,
@@ -276,18 +282,15 @@
Shield,
Mail,
Globe,
ChevronLeft,
ChevronRight,
Menu,
X,
Users,
Swords,
Languages,
Settings
Settings,
Wrench,
ChevronsLeft
} from 'lucide-vue-next'
import * as gameLogic from '@/logic/gameLogic'
import * as planetLogic from '@/logic/planetLogic'
import * as publicLogic from '@/logic/publicLogic'
import * as officerLogic from '@/logic/officerLogic'
import * as buildingValidation from '@/logic/buildingValidation'
import * as resourceLogic from '@/logic/resourceLogic'
@@ -295,8 +298,13 @@
import * as fleetLogic from '@/logic/fleetLogic'
import * as shipLogic from '@/logic/shipLogic'
import pkg from '../package.json'
import { migrateGameData } from '@/utils/migration'
// 执行数据迁移(在 store 初始化之前)
migrateGameData()
const gameStore = useGameStore()
const universeStore = useUniverseStore()
const { isDark } = useTheme()
const { t } = useI18n()
const confirmDialog = ref<InstanceType<typeof ConfirmDialog> | null>(null)
@@ -304,15 +312,21 @@
// 所有可用的语言选项
const locales: Locale[] = ['zh-CN', 'zh-TW', 'en', 'de', 'ru', 'ko', 'ja']
const initGame = () => {
// 侧边栏状态(不持久化,根据屏幕尺寸初始化)
// PC端≥1024px默认打开移动端默认关闭
const sidebarOpen = ref(window.innerWidth >= 1024)
const initGame = async () => {
const shouldInit = gameLogic.shouldInitializeGame(gameStore.player.planets)
if (!shouldInit) {
const now = Date.now()
gameLogic.updatePlanetsLastUpdate(gameStore.player.planets, now)
// 计算离线收益(直接同步计算)
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, now)
gameStore.player.planets.forEach(planet => {
const key = gameLogic.generatePositionKey(planet.position.galaxy, planet.position.system, planet.position.position)
gameStore.universePlanets[key] = planet
resourceLogic.updatePlanetResources(planet, now, bonuses)
})
generateNPCPlanets()
return
}
@@ -320,8 +334,6 @@
const initialPlanet = planetLogic.createInitialPlanet(gameStore.player.id, t('planet.homePlanet'))
gameStore.player.planets = [initialPlanet]
gameStore.currentPlanetId = initialPlanet.id
const key = gameLogic.generatePositionKey(initialPlanet.position.galaxy, initialPlanet.position.system, initialPlanet.position.position)
gameStore.universePlanets[key] = initialPlanet
}
const generateNPCPlanets = () => {
@@ -329,9 +341,9 @@
for (let i = 0; i < npcCount; i++) {
const position = gameLogic.generateRandomPosition()
const key = gameLogic.generatePositionKey(position.galaxy, position.system, position.position)
if (gameStore.universePlanets[key]) continue
if (universeStore.planets[key]) continue
const npcPlanet = planetLogic.createNPCPlanet(i, position, t('planet.planetPrefix'))
gameStore.universePlanets[key] = npcPlanet
universeStore.planets[key] = npcPlanet
}
}
@@ -339,9 +351,12 @@
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)
@@ -351,27 +366,41 @@
})
}
const processMissionArrival = (mission: FleetMission) => {
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
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]
if (mission.missionType === MissionType.Transport) {
fleetLogic.processTransportArrival(mission, targetPlanet)
} else if (mission.missionType === MissionType.Attack) {
const attackResult = fleetLogic.processAttackArrival(mission, targetPlanet, gameStore.player, null, gameStore.player.planets)
const attackResult = await fleetLogic.processAttackArrival(mission, targetPlanet, gameStore.player, null, gameStore.player.planets)
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
}
}
} else if (mission.missionType === MissionType.Colonize) {
const newPlanet = fleetLogic.processColonizeArrival(mission, targetPlanet, gameStore.player.id, t('planet.colonyPrefix'))
if (newPlanet) gameStore.player.planets.push(newPlanet)
if (newPlanet) {
gameStore.player.planets.push(newPlanet)
}
} else if (mission.missionType === MissionType.Spy) {
const spyReport = fleetLogic.processSpyArrival(mission, targetPlanet, gameStore.player.id)
if (spyReport) gameStore.player.spyReports.push(spyReport)
@@ -382,6 +411,42 @@
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 (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 (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]
}
// TODO: 可以添加战斗报告或摧毁报告来通知玩家结果
}
}
}
@@ -394,27 +459,31 @@
if (missionIndex > -1) gameStore.player.fleetMissions.splice(missionIndex, 1)
}
// 游戏循环定时器
let gameLoop: ReturnType<typeof setInterval> | null = null
// 清理定时器
onUnmounted(() => {
if (gameLoop) clearInterval(gameLoop)
})
// 初始化游戏
onMounted(() => {
onMounted(async () => {
// 如果是首次访问(没有星球数据),使用浏览器语言自动检测
const isFirstVisit = gameStore.player.planets.length === 0
if (isFirstVisit) {
gameStore.locale = detectBrowserLocale()
}
initGame()
await initGame()
// 启动游戏循环
const gameLoop = setInterval(() => {
gameLoop = setInterval(() => {
updateGame()
}, 1000) // 每秒更新一次
// 清理定时器
onUnmounted(() => {
clearInterval(gameLoop)
})
}, 1000) // 每1秒更新一次
})
// 定义 planet computed需要在 watch 之前定义)
const planet = computed(() => gameStore.currentPlanet)
const navItems = [
{ name: computed(() => t('nav.overview')), path: '/', icon: Home },
{ name: computed(() => t('nav.buildings')), path: '/buildings', icon: Building2 },
@@ -426,23 +495,35 @@
{ name: computed(() => t('nav.simulator')), path: '/battle-simulator', icon: Swords },
{ name: computed(() => t('nav.galaxy')), path: '/galaxy', icon: Globe },
{ name: computed(() => t('nav.messages')), path: '/messages', icon: Mail },
{ name: computed(() => t('nav.settings')), path: '/settings', icon: Settings }
{ name: computed(() => t('nav.settings')), path: '/settings', icon: Settings },
// GM菜单仅在开发模式下显示
...(import.meta.env.DEV ? [{ name: computed(() => t('nav.gm')), path: '/gm', icon: Wrench }] : [])
]
const planet = computed(() => gameStore.currentPlanet)
const production = computed(() => (planet.value ? publicLogic.getResourceProduction(planet.value, gameStore.player.officers) : null))
const capacity = computed(() => (planet.value ? publicLogic.getResourceCapacity(planet.value, gameStore.player.officers) : null))
// 电量产出和消耗
const energyProduction = computed(() => {
if (!planet.value) return 0
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, Date.now())
return resourceLogic.calculateEnergyProduction(planet.value, { energyProductionBonus: bonuses.energyProductionBonus })
// 使用直接计算,不再缓存
const production = computed(() => {
if (!planet.value) return null
const now = Date.now()
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, now)
return resourceLogic.calculateResourceProduction(planet.value, {
resourceProductionBonus: bonuses.resourceProductionBonus,
darkMatterProductionBonus: bonuses.darkMatterProductionBonus,
energyProductionBonus: bonuses.energyProductionBonus
})
})
const energyConsumption = computed(() => {
if (!planet.value) return 0
return resourceLogic.calculateEnergyConsumption(planet.value)
const capacity = computed(() => {
if (!planet.value) return null
const now = Date.now()
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, now)
return resourceLogic.calculateResourceCapacity(planet.value, bonuses.storageCapacityBonus)
})
// 未读消息数量
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
})
// 资源类型配置
@@ -477,20 +558,20 @@
// 切换侧边栏
const toggleSidebar = () => {
gameStore.sidebarCollapsed = !gameStore.sidebarCollapsed
sidebarOpen.value = !sidebarOpen.value
}
// 获取队列项的名称
const getItemName = (item: BuildQueueItem): string => {
if (item.type === 'building' || item.type === 'demolish') {
const buildingName = BUILDINGS[item.itemType as BuildingType]?.name || item.itemType
const buildingName = t(`buildings.${item.itemType}`)
return item.type === 'demolish' ? `${t('buildingsView.demolish')} - ${buildingName}` : buildingName
} else if (item.type === 'technology') {
return TECHNOLOGIES[item.itemType as TechnologyType]?.name || item.itemType
return t(`technologies.${item.itemType}`)
} else if (item.type === 'ship') {
return SHIPS[item.itemType as ShipType]?.name || item.itemType
return t(`ships.${item.itemType}`)
} else if (item.type === 'defense') {
return DEFENSES[item.itemType as DefenseType]?.name || item.itemType
return t(`defenses.${item.itemType}`)
}
return item.itemType
}

View File

@@ -0,0 +1,334 @@
<template>
<Dialog v-model:open="isOpen">
<DialogContent class="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle class="flex items-center gap-2">
<Trophy class="h-5 w-5" />
{{ t('messagesView.battleReport') }}
</DialogTitle>
<DialogDescription v-if="report">
{{ formatDate(report.timestamp) }}
</DialogDescription>
</DialogHeader>
<div v-if="report" class="space-y-4">
<!-- 战斗双方信息 -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
<!-- 攻击方星球 -->
<div class="p-3 bg-blue-50 dark:bg-blue-950/20 rounded-lg">
<p class="font-medium text-blue-600 dark:text-blue-400 mb-1">{{ t('simulatorView.attacker') }}</p>
<p v-if="attackerPlanet" class="text-xs text-muted-foreground">
{{ attackerPlanet.name }} [{{ attackerPlanet.position.galaxy }}:{{ attackerPlanet.position.system }}:{{
attackerPlanet.position.position
}}]
</p>
<p v-else class="text-xs text-muted-foreground">{{ report.attackerPlanetId }}</p>
</div>
<!-- 防守方星球 -->
<div class="p-3 bg-red-50 dark:bg-red-950/20 rounded-lg">
<p class="font-medium text-red-600 dark:text-red-400 mb-1">{{ t('simulatorView.defender') }}</p>
<p v-if="defenderPlanet" class="text-xs text-muted-foreground">
{{ defenderPlanet.name }} [{{ defenderPlanet.position.galaxy }}:{{ defenderPlanet.position.system }}:{{
defenderPlanet.position.position
}}]
</p>
<p v-else class="text-xs text-muted-foreground">{{ report.defenderPlanetId }}</p>
</div>
</div>
<!-- 胜利者 -->
<div class="text-center p-4 rounded-lg" :class="getWinnerStyle(report.winner)">
<p class="text-lg font-bold">
{{
report.winner === 'attacker'
? t('messagesView.victory')
: report.winner === 'defender'
? t('messagesView.defeat')
: t('messagesView.draw')
}}
</p>
<p v-if="report.rounds" class="text-sm mt-1">{{ t('simulatorView.afterRounds').replace('{rounds}', String(report.rounds)) }}</p>
</div>
<!-- 损失对比 -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- 攻击方损失 -->
<div class="space-y-2">
<p class="text-sm font-medium text-red-600 dark:text-red-400">{{ t('messagesView.attackerLosses') }}</p>
<div class="p-3 bg-muted rounded-lg space-y-1 text-xs">
<div v-for="(count, shipType) in report.attackerLosses" :key="shipType">
<span class="text-muted-foreground">{{ SHIPS[shipType].name }}:</span>
<span class="ml-2 font-medium">{{ count }}</span>
</div>
<p v-if="Object.keys(report.attackerLosses).length === 0" class="text-muted-foreground">
{{ t('messagesView.noLosses') }}
</p>
</div>
</div>
<!-- 防守方损失 -->
<div class="space-y-2">
<p class="text-sm font-medium text-red-600 dark:text-red-400">{{ t('messagesView.defenderLosses') }}</p>
<div class="p-3 bg-muted rounded-lg space-y-1 text-xs">
<div v-for="(count, shipType) in report.defenderLosses.fleet" :key="shipType">
<span class="text-muted-foreground">{{ SHIPS[shipType].name }}:</span>
<span class="ml-2 font-medium">{{ count }}</span>
</div>
<div v-for="(count, defenseType) in report.defenderLosses.defense" :key="defenseType">
<span class="text-muted-foreground">{{ DEFENSES[defenseType].name }}:</span>
<span class="ml-2 font-medium">{{ count }}</span>
</div>
<p
v-if="Object.keys(report.defenderLosses.fleet).length === 0 && Object.keys(report.defenderLosses.defense).length === 0"
class="text-muted-foreground"
>
{{ t('messagesView.noLosses') }}
</p>
</div>
</div>
</div>
<!-- 剩余单位 -->
<div v-if="report.attackerRemaining || report.defenderRemaining" class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- 攻击方剩余 -->
<div v-if="report.attackerRemaining && Object.keys(report.attackerRemaining).length > 0" class="space-y-2">
<p class="text-sm font-medium text-blue-600 dark:text-blue-400">{{ t('messagesView.attackerRemaining') }}</p>
<div class="p-3 bg-muted rounded-lg space-y-1 text-xs">
<div v-for="(count, shipType) in report.attackerRemaining" :key="shipType">
<span class="text-muted-foreground">{{ SHIPS[shipType].name }}:</span>
<span class="ml-2 font-medium">{{ count }}</span>
</div>
</div>
</div>
<!-- 防守方剩余 -->
<div
v-if="
report.defenderRemaining &&
(Object.keys(report.defenderRemaining.fleet || {}).length > 0 ||
Object.keys(report.defenderRemaining.defense || {}).length > 0)
"
class="space-y-2"
>
<p class="text-sm font-medium text-blue-600 dark:text-blue-400">{{ t('messagesView.defenderRemaining') }}</p>
<div class="p-3 bg-muted rounded-lg space-y-1 text-xs">
<div v-for="(count, shipType) in report.defenderRemaining.fleet" :key="shipType">
<span class="text-muted-foreground">{{ SHIPS[shipType].name }}:</span>
<span class="ml-2 font-medium">{{ count }}</span>
</div>
<div v-for="(count, defenseType) in report.defenderRemaining.defense" :key="defenseType">
<span class="text-muted-foreground">{{ DEFENSES[defenseType].name }}:</span>
<span class="ml-2 font-medium">{{ count }}</span>
</div>
</div>
</div>
</div>
<!-- 战利品和残骸 -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- 掠夺资源 -->
<div
v-if="report.plunder && (report.plunder.metal > 0 || report.plunder.crystal > 0 || report.plunder.deuterium > 0)"
class="p-3 bg-green-50 dark:bg-green-950 rounded-lg"
>
<p class="text-sm font-medium mb-2 text-green-600 dark:text-green-400">{{ t('messagesView.plunder') }}</p>
<div class="flex flex-wrap gap-3 text-xs">
<span v-if="report.plunder.metal > 0" class="flex items-center gap-1">
<ResourceIcon type="metal" size="sm" />
{{ formatNumber(report.plunder.metal) }}
</span>
<span v-if="report.plunder.crystal > 0" class="flex items-center gap-1">
<ResourceIcon type="crystal" size="sm" />
{{ formatNumber(report.plunder.crystal) }}
</span>
<span v-if="report.plunder.deuterium > 0" class="flex items-center gap-1">
<ResourceIcon type="deuterium" size="sm" />
{{ formatNumber(report.plunder.deuterium) }}
</span>
</div>
</div>
<!-- 残骸场 -->
<div
v-if="report.debrisField && (report.debrisField.metal > 0 || report.debrisField.crystal > 0)"
class="p-3 bg-muted rounded-lg"
>
<p class="text-sm font-medium mb-2">{{ t('messagesView.debrisField') }}</p>
<div class="flex flex-wrap gap-3 text-xs">
<span v-if="report.debrisField.metal > 0" class="flex items-center gap-1">
<ResourceIcon type="metal" size="sm" />
{{ formatNumber(report.debrisField.metal) }}
</span>
<span v-if="report.debrisField.crystal > 0" class="flex items-center gap-1">
<ResourceIcon type="crystal" size="sm" />
{{ formatNumber(report.debrisField.crystal) }}
</span>
</div>
<!-- 月球生成概率 -->
<p v-if="report.moonChance && report.moonChance > 0" class="text-xs text-muted-foreground mt-2">
{{ t('messagesView.moonChance') }}: {{ (report.moonChance * 100).toFixed(1) }}%
</p>
</div>
</div>
<!-- 回合详情 -->
<div v-if="report.roundDetails && report.roundDetails.length > 0" class="space-y-2">
<Button @click="showRoundDetails = !showRoundDetails" variant="outline" size="sm" class="w-full">
{{ showRoundDetails ? t('messagesView.hideRoundDetails') : t('messagesView.showRoundDetails') }}
</Button>
<div v-if="showRoundDetails" class="relative pl-6 space-y-4">
<!-- 时间线 -->
<div class="absolute left-2 top-0 bottom-0 w-0.5 bg-border" />
<div v-for="detail in report.roundDetails" :key="detail.round" class="relative">
<!-- 时间线节点 -->
<div class="absolute -left-6 top-3 w-4 h-4 rounded-full bg-primary border-2 border-background" />
<!-- 回合内容卡片 -->
<div class="border rounded-lg p-3 bg-card hover:shadow-md transition-shadow">
<div class="flex items-center justify-between mb-3">
<p class="text-sm font-semibold">{{ t('messagesView.round').replace('{round}', String(detail.round)) }}</p>
<TooltipProvider :delay-duration="300">
<div class="flex gap-3 text-xs text-muted-foreground">
<Tooltip>
<TooltipTrigger as-child>
<span class="flex items-center gap-1">
<Sword class="h-3 w-3" />
{{ formatNumber(detail.attackerRemainingPower) }}
</span>
</TooltipTrigger>
<TooltipContent>
<p>{{ t('messagesView.attackerRemainingPower') }}</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger as-child>
<span class="flex items-center gap-1">
<Shield class="h-3 w-3" />
{{ formatNumber(detail.defenderRemainingPower) }}
</span>
</TooltipTrigger>
<TooltipContent>
<p>{{ t('messagesView.defenderRemainingPower') }}</p>
</TooltipContent>
</Tooltip>
</div>
</TooltipProvider>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<!-- 攻击方本回合损失 -->
<div class="bg-red-50 dark:bg-red-950/20 rounded p-2">
<p class="text-xs font-medium text-red-600 dark:text-red-400 mb-1.5">{{ t('messagesView.attackerLosses') }}</p>
<div class="text-xs space-y-0.5">
<div v-for="(count, shipType) in detail.attackerLosses" :key="shipType" class="flex justify-between">
<span class="text-muted-foreground">{{ SHIPS[shipType].name }}</span>
<span class="font-medium">-{{ count }}</span>
</div>
<p v-if="Object.keys(detail.attackerLosses).length === 0" class="text-muted-foreground italic">
{{ t('messagesView.noLosses') }}
</p>
</div>
</div>
<!-- 防守方本回合损失 -->
<div class="bg-blue-50 dark:bg-blue-950/20 rounded p-2">
<p class="text-xs font-medium text-blue-600 dark:text-blue-400 mb-1.5">{{ t('messagesView.defenderLosses') }}</p>
<div class="text-xs space-y-0.5">
<div v-for="(count, shipType) in detail.defenderLosses.fleet" :key="shipType" class="flex justify-between">
<span class="text-muted-foreground">{{ SHIPS[shipType].name }}</span>
<span class="font-medium">-{{ count }}</span>
</div>
<div v-for="(count, defenseType) in detail.defenderLosses.defense" :key="defenseType" class="flex justify-between">
<span class="text-muted-foreground">{{ DEFENSES[defenseType].name }}</span>
<span class="font-medium">-{{ count }}</span>
</div>
<p
v-if="
Object.keys(detail.defenderLosses.fleet).length === 0 && Object.keys(detail.defenderLosses.defense).length === 0
"
class="text-muted-foreground italic"
>
{{ t('messagesView.noLosses') }}
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</DialogContent>
</Dialog>
</template>
<script setup lang="ts">
import { ref, watch, computed } from 'vue'
import { useGameStore } from '@/stores/gameStore'
import { useUniverseStore } from '@/stores/universeStore'
import { useI18n } from '@/composables/useI18n'
import { useGameConfig } from '@/composables/useGameConfig'
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import ResourceIcon from '@/components/ResourceIcon.vue'
import { formatNumber, formatDate } from '@/utils/format'
import { Trophy, Sword, Shield } from 'lucide-vue-next'
import type { BattleResult } from '@/types/game'
const props = defineProps<{
report: BattleResult | null
open: boolean
}>()
const emit = defineEmits<{
(e: 'update:open', value: boolean): void
}>()
const gameStore = useGameStore()
const universeStore = useUniverseStore()
const { t } = useI18n()
const { SHIPS, DEFENSES } = useGameConfig()
const isOpen = ref(props.open)
const showRoundDetails = ref(false)
// 获取攻击方星球信息
const attackerPlanet = computed(() => {
if (!props.report) return null
return gameStore.player.planets.find(p => p.id === props.report!.attackerPlanetId)
})
// 获取防守方星球信息
const defenderPlanet = computed(() => {
if (!props.report) return null
// 先从玩家星球中查找
const playerPlanet = gameStore.player.planets.find(p => p.id === props.report!.defenderPlanetId)
if (playerPlanet) return playerPlanet
// 再从宇宙星球地图中查找
return Object.values(universeStore.planets).find(p => p.id === props.report!.defenderPlanetId)
})
watch(
() => props.open,
newValue => {
isOpen.value = newValue
if (newValue) {
showRoundDetails.value = false
}
}
)
watch(isOpen, newValue => {
emit('update:open', newValue)
})
// 获取胜利者样式
const getWinnerStyle = (winner: string) => {
if (winner === 'attacker') return 'bg-green-50 dark:bg-green-950 text-green-700 dark:text-green-300'
if (winner === 'defender') return 'bg-red-50 dark:bg-red-950 text-red-700 dark:text-red-300'
return 'bg-gray-50 dark:bg-gray-950 text-gray-700 dark:text-gray-300'
}
</script>

View File

@@ -30,6 +30,7 @@
interface Props {
requirements?: Partial<Record<BuildingType | TechnologyType, number>>
currentLevel?: number // 当前建筑/科技等级,用于判断是否已解锁
}
const props = defineProps<Props>()
@@ -39,6 +40,8 @@
const requirementsDialog = ref<InstanceType<typeof AlertDialog> | null>(null)
const isUnlocked = computed(() => {
// 如果已经建造过level > 0则认为已解锁不显示遮罩
if (props.currentLevel !== undefined && props.currentLevel > 0) return true
if (!props.requirements || !gameStore.currentPlanet) return true
return publicLogic.checkRequirements(gameStore.currentPlanet, gameStore.player.technologies, props.requirements)
})

View File

@@ -0,0 +1,50 @@
<template>
<Popover>
<PopoverTrigger as-child>
<span class="cursor-pointer underline decoration-dotted underline-offset-4 touch-manipulation">{{ abbreviatedValue }}</span>
</PopoverTrigger>
<PopoverContent class="w-auto p-2" side="top" align="center">
<p class="font-mono text-sm">{{ formattedValue }}</p>
</PopoverContent>
</Popover>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
const props = defineProps<{
value: number
}>()
// 完整格式化的数字(带千位分隔符)
const formattedValue = computed(() => {
return props.value.toLocaleString()
})
// 缩写格式的数字
const abbreviatedValue = computed(() => {
const num = props.value
// 小于1000直接显示
if (num < 1000) {
return num.toString()
}
// 1000 - 999,999: 使用 K (千)
if (num < 1000000) {
const k = num / 1000
return k % 1 === 0 ? `${k}K` : `${k.toFixed(1)}K`
}
// 1,000,000 - 999,999,999: 使用 M (百万)
if (num < 1000000000) {
const m = num / 1000000
return m % 1 === 0 ? `${m}M` : `${m.toFixed(1)}M`
}
// 1,000,000,000+: 使用 B (十亿)
const b = num / 1000000000
return b % 1 === 0 ? `${b}B` : `${b.toFixed(1)}B`
})
</script>

View File

@@ -0,0 +1,141 @@
<template>
<Dialog v-model:open="isOpen">
<DialogContent class="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle class="flex items-center gap-2">
<Eye class="h-5 w-5" />
{{ t('messagesView.spyReport') }}
</DialogTitle>
<DialogDescription v-if="report">
{{ formatDate(report.timestamp) }}
</DialogDescription>
</DialogHeader>
<div v-if="report" class="space-y-4">
<!-- 目标星球信息 -->
<div class="p-3 bg-muted rounded-lg">
<p class="text-sm font-medium mb-2">{{ t('messagesView.targetPlanet') }}</p>
<p v-if="targetPlanet" class="text-xs text-muted-foreground">
{{ targetPlanet.name }} [{{ targetPlanet.position.galaxy }}:{{ targetPlanet.position.system }}:{{
targetPlanet.position.position
}}]
</p>
<p v-else class="text-xs text-muted-foreground">{{ report.targetPlanetId }}</p>
</div>
<!-- 资源 -->
<div>
<p class="text-sm font-medium mb-2">{{ t('messagesView.resources') }}:</p>
<div class="flex flex-wrap gap-3 text-xs sm:text-sm">
<span class="flex items-center gap-1">
<ResourceIcon type="metal" size="sm" />
{{ formatNumber(report.resources.metal) }}
</span>
<span class="flex items-center gap-1">
<ResourceIcon type="crystal" size="sm" />
{{ formatNumber(report.resources.crystal) }}
</span>
<span class="flex items-center gap-1">
<ResourceIcon type="deuterium" size="sm" />
{{ formatNumber(report.resources.deuterium) }}
</span>
<span class="flex items-center gap-1">
<ResourceIcon type="darkMatter" size="sm" />
{{ formatNumber(report.resources.darkMatter) }}
</span>
</div>
</div>
<!-- 舰队如果有 -->
<div v-if="report.fleet && Object.keys(report.fleet).length > 0">
<p class="text-sm font-medium mb-2">{{ t('messagesView.fleet') }}:</p>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2 text-xs sm:text-sm">
<div v-for="(count, shipType) in report.fleet" :key="shipType">
<span class="text-muted-foreground">{{ SHIPS[shipType].name }}:</span>
<span class="ml-1 font-medium">{{ count }}</span>
</div>
</div>
</div>
<!-- 防御设施如果有 -->
<div v-if="report.defense && hasDefense(report.defense)">
<p class="text-sm font-medium mb-2">{{ t('messagesView.defense') }}:</p>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2 text-xs sm:text-sm">
<div v-for="(count, defenseType) in report.defense" :key="defenseType">
<span v-if="count && count > 0" class="block">
<span class="text-muted-foreground">{{ DEFENSES[defenseType].name }}:</span>
<span class="ml-1 font-medium">{{ count }}</span>
</span>
</div>
</div>
</div>
<!-- 建筑如果有 -->
<div v-if="report.buildings && Object.keys(report.buildings).length > 0">
<p class="text-sm font-medium mb-2">{{ t('messagesView.buildings') }}:</p>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2 text-xs sm:text-sm">
<div v-for="(level, buildingType) in report.buildings" :key="buildingType">
<span class="text-muted-foreground">{{ BUILDINGS[buildingType].name }}:</span>
<span class="ml-1 font-medium">Lv.{{ level }}</span>
</div>
</div>
</div>
</div>
</DialogContent>
</Dialog>
</template>
<script setup lang="ts">
import { ref, watch, computed } from 'vue'
import { useGameStore } from '@/stores/gameStore'
import { useUniverseStore } from '@/stores/universeStore'
import { useI18n } from '@/composables/useI18n'
import { useGameConfig } from '@/composables/useGameConfig'
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import ResourceIcon from '@/components/ResourceIcon.vue'
import { formatNumber, formatDate } from '@/utils/format'
import { Eye } from 'lucide-vue-next'
import type { SpyReport } from '@/types/game'
const props = defineProps<{
report: SpyReport | null
open: boolean
}>()
const emit = defineEmits<{
(e: 'update:open', value: boolean): void
}>()
const gameStore = useGameStore()
const universeStore = useUniverseStore()
const { t } = useI18n()
const { SHIPS, DEFENSES, BUILDINGS } = useGameConfig()
const isOpen = ref(props.open)
// 获取目标星球信息
const targetPlanet = computed(() => {
if (!props.report) return null
// 先从玩家星球中查找
const playerPlanet = gameStore.player.planets.find(p => p.id === props.report!.targetPlanetId)
if (playerPlanet) return playerPlanet
// 再从宇宙星球地图中查找
return Object.values(universeStore.planets).find(p => p.id === props.report!.targetPlanetId)
})
watch(
() => props.open,
newValue => {
isOpen.value = newValue
}
)
watch(isOpen, newValue => {
emit('update:open', newValue)
})
// 检查是否有防御设施
const hasDefense = (defense: any): boolean => {
if (!defense) return false
return Object.values(defense).some((count: any) => count > 0)
}
</script>

View File

@@ -21,24 +21,36 @@
<Badge v-if="level === currentLevel" variant="default">{{ level }}</Badge>
<span v-else>{{ level }}</span>
</TableCell>
<TableCell class="text-center text-sm">{{ formatNumber(getLevelData(level).cost.metal) }}</TableCell>
<TableCell class="text-center text-sm">{{ formatNumber(getLevelData(level).cost.crystal) }}</TableCell>
<TableCell class="text-center text-sm">{{ formatNumber(getLevelData(level).cost.deuterium) }}</TableCell>
<TableCell class="text-center text-sm">
<NumberWithTooltip :value="getLevelData(level).cost.metal" />
</TableCell>
<TableCell class="text-center text-sm">
<NumberWithTooltip :value="getLevelData(level).cost.crystal" />
</TableCell>
<TableCell class="text-center text-sm">
<NumberWithTooltip :value="getLevelData(level).cost.deuterium" />
</TableCell>
<TableCell class="text-center text-sm">{{ formatTime(getLevelData(level).buildTime) }}</TableCell>
<TableCell class="text-center text-sm">
<span v-if="getLevelData(level).production > 0" class="text-green-600 dark:text-green-400">
+{{ formatNumber(getLevelData(level).production) }}/{{ t('resources.perHour') }}
+
<NumberWithTooltip :value="getLevelData(level).production" />
/{{ t('resources.perHour') }}
</span>
<span v-else>-</span>
</TableCell>
<TableCell class="text-center text-sm">
<span v-if="getLevelData(level).consumption > 0" class="text-red-600 dark:text-red-400">
-{{ formatNumber(getLevelData(level).consumption) }}
-
<NumberWithTooltip :value="getLevelData(level).consumption" />
</span>
<span v-else>-</span>
</TableCell>
<TableCell class="text-center text-sm">
<span class="text-primary font-medium">+{{ getLevelData(level).points }}</span>
<span class="text-primary font-medium">
+
<NumberWithTooltip :value="getLevelData(level).points" />
</span>
</TableCell>
</TableRow>
</TableBody>
@@ -54,15 +66,21 @@
<CardContent class="space-y-2">
<div class="flex items-center justify-between text-sm">
<span class="text-muted-foreground">{{ t('resources.metal') }}:</span>
<span class="font-medium">{{ formatNumber(totalStats.metal) }}</span>
<span class="font-medium">
<NumberWithTooltip :value="totalStats.metal" />
</span>
</div>
<div class="flex items-center justify-between text-sm">
<span class="text-muted-foreground">{{ t('resources.crystal') }}:</span>
<span class="font-medium">{{ formatNumber(totalStats.crystal) }}</span>
<span class="font-medium">
<NumberWithTooltip :value="totalStats.crystal" />
</span>
</div>
<div class="flex items-center justify-between text-sm">
<span class="text-muted-foreground">{{ t('resources.deuterium') }}:</span>
<span class="font-medium">{{ formatNumber(totalStats.deuterium) }}</span>
<span class="font-medium">
<NumberWithTooltip :value="totalStats.deuterium" />
</span>
</div>
</CardContent>
</Card>
@@ -72,7 +90,9 @@
<CardTitle class="text-sm">{{ t('buildings.totalPoints') }}</CardTitle>
</CardHeader>
<CardContent>
<div class="text-3xl font-bold text-primary">{{ formatNumber(totalStats.points) }}</div>
<div class="text-3xl font-bold text-primary">
<NumberWithTooltip :value="totalStats.points" />
</div>
<p class="text-xs text-muted-foreground mt-1">
{{ t('buildings.levelRange') }}: {{ Math.max(0, currentLevel - 10) }} - {{ Math.min(currentLevel + 10, currentLevel + 10) }}
</p>
@@ -89,8 +109,10 @@
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import NumberWithTooltip from '@/components/NumberWithTooltip.vue'
import * as buildingLogic from '@/logic/buildingLogic'
import * as pointsLogic from '@/logic/pointsLogic'
import { formatTime } from '@/utils/format'
const { t } = useI18n()
@@ -99,12 +121,11 @@
currentLevel: number
}>()
// 等级范围:当前等级 ±10
// 等级范围:当前等级 +10
const levelRange = computed(() => {
const start = Math.max(0, props.currentLevel - 10)
const end = props.currentLevel + 10
const levels = []
for (let i = start; i <= end; i++) {
for (let i = props.currentLevel; i <= end; i++) {
levels.push(i)
}
return levels
@@ -129,18 +150,18 @@
let production = 0
let consumption = 0
// 资源矿产量
// 资源矿产量(与 resourceLogic.ts 保持一致)
if (props.buildingType === 'metalMine') {
production = Math.floor(30 * level * Math.pow(1.1, level))
production = Math.floor(1500 * level * Math.pow(1.5, level))
} else if (props.buildingType === 'crystalMine') {
production = Math.floor(20 * level * Math.pow(1.1, level))
production = Math.floor(1000 * level * Math.pow(1.5, level))
} else if (props.buildingType === 'deuteriumSynthesizer') {
production = Math.floor(10 * level * Math.pow(1.1, level))
production = Math.floor(500 * level * Math.pow(1.5, level))
}
// 能量产出
// 能量产出(与 resourceLogic.ts 保持一致)
if (props.buildingType === 'solarPlant') {
production = Math.floor(20 * level * Math.pow(1.1, level))
production = Math.floor(50 * level * Math.pow(1.1, level))
}
// 能量消耗(矿场和合成器)
@@ -178,18 +199,4 @@
return { metal, crystal, deuterium, points }
})
const formatNumber = (num: number): string => {
return num.toLocaleString()
}
const formatTime = (seconds: number): string => {
if (seconds < 60) return `${seconds}${t('common.timeSecond')}`
const minutes = Math.floor(seconds / 60)
const secs = seconds % 60
if (minutes < 60) return `${minutes}${t('common.timeMinute')}${secs}${t('common.timeSecond')}`
const hours = Math.floor(minutes / 60)
const mins = minutes % 60
return `${hours}${t('common.timeHour')}${mins}${t('common.timeMinute')}`
}
</script>

View File

@@ -10,7 +10,9 @@
</CardTitle>
</CardHeader>
<CardContent>
<div class="text-2xl font-bold">{{ formatNumber(config.attack) }}</div>
<div class="text-2xl font-bold">
<NumberWithTooltip :value="config.attack" />
</div>
</CardContent>
</Card>
@@ -22,7 +24,9 @@
</CardTitle>
</CardHeader>
<CardContent>
<div class="text-2xl font-bold">{{ formatNumber(config.shield) }}</div>
<div class="text-2xl font-bold">
<NumberWithTooltip :value="config.shield" />
</div>
</CardContent>
</Card>
@@ -34,7 +38,9 @@
</CardTitle>
</CardHeader>
<CardContent>
<div class="text-2xl font-bold">{{ formatNumber(config.armor) }}</div>
<div class="text-2xl font-bold">
<NumberWithTooltip :value="config.armor" />
</div>
</CardContent>
</Card>
</div>
@@ -48,19 +54,27 @@
<CardContent class="space-y-2">
<div v-if="config.cost.metal > 0" class="flex items-center justify-between text-sm">
<span class="text-muted-foreground">{{ t('resources.metal') }}:</span>
<span class="font-medium">{{ formatNumber(config.cost.metal) }}</span>
<span class="font-medium">
<NumberWithTooltip :value="config.cost.metal" />
</span>
</div>
<div v-if="config.cost.crystal > 0" class="flex items-center justify-between text-sm">
<span class="text-muted-foreground">{{ t('resources.crystal') }}:</span>
<span class="font-medium">{{ formatNumber(config.cost.crystal) }}</span>
<span class="font-medium">
<NumberWithTooltip :value="config.cost.crystal" />
</span>
</div>
<div v-if="config.cost.deuterium > 0" class="flex items-center justify-between text-sm">
<span class="text-muted-foreground">{{ t('resources.deuterium') }}:</span>
<span class="font-medium">{{ formatNumber(config.cost.deuterium) }}</span>
<span class="font-medium">
<NumberWithTooltip :value="config.cost.deuterium" />
</span>
</div>
<div class="flex items-center justify-between text-sm pt-2 border-t">
<span class="text-muted-foreground">{{ t('player.points') }}:</span>
<span class="font-bold text-primary">{{ pointsPerUnit }}</span>
<span class="font-bold text-primary">
<NumberWithTooltip :value="pointsPerUnit" />
</span>
</div>
</CardContent>
</Card>
@@ -92,22 +106,31 @@
<div class="space-y-1 text-sm">
<div class="flex justify-between">
<span>{{ t('resources.metal') }}:</span>
<span class="font-medium">{{ formatNumber(batchCost.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">{{ formatNumber(batchCost.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">{{ formatNumber(batchCost.deuterium) }}</span>
<span class="font-medium">
<NumberWithTooltip :value="batchCost.deuterium" />
</span>
</div>
</div>
</div>
<div class="space-y-2">
<p class="text-sm text-muted-foreground">{{ t('defense.totalTime') }}:</p>
<div class="text-xl font-bold">{{ formatTime(config.buildTime * quantity) }}</div>
<p class="text-xs text-muted-foreground">{{ t('player.points') }}: +{{ formatNumber(batchPoints) }}</p>
<p class="text-xs text-muted-foreground">
{{ t('player.points') }}: +
<NumberWithTooltip :value="batchPoints" />
</p>
</div>
</div>
</CardContent>
@@ -122,9 +145,11 @@
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import NumberWithTooltip from '@/components/NumberWithTooltip.vue'
import { Sword, Shield, ShieldCheck } from 'lucide-vue-next'
import * as pointsLogic from '@/logic/pointsLogic'
import { DEFENSES } from '@/config/gameConfig'
import { formatTime } from '@/utils/format'
const { t } = useI18n()
@@ -151,18 +176,4 @@
const batchPoints = computed(() => {
return pointsLogic.calculateDefensePoints(props.defenseType, quantity.value)
})
const formatNumber = (num: number): string => {
return num.toLocaleString()
}
const formatTime = (seconds: number): string => {
if (seconds < 60) return `${seconds}${t('common.timeSecond')}`
const minutes = Math.floor(seconds / 60)
const secs = seconds % 60
if (minutes < 60) return `${minutes}${t('common.timeMinute')}${secs}${t('common.timeSecond')}`
const hours = Math.floor(minutes / 60)
const mins = minutes % 60
return `${hours}${t('common.timeHour')}${mins}${t('common.timeMinute')}`
}
</script>

View File

@@ -10,7 +10,9 @@
</CardTitle>
</CardHeader>
<CardContent>
<div class="text-2xl font-bold">{{ formatNumber(config.attack) }}</div>
<div class="text-2xl font-bold">
<NumberWithTooltip :value="config.attack" />
</div>
</CardContent>
</Card>
@@ -22,7 +24,9 @@
</CardTitle>
</CardHeader>
<CardContent>
<div class="text-2xl font-bold">{{ formatNumber(config.shield) }}</div>
<div class="text-2xl font-bold">
<NumberWithTooltip :value="config.shield" />
</div>
</CardContent>
</Card>
@@ -34,7 +38,9 @@
</CardTitle>
</CardHeader>
<CardContent>
<div class="text-2xl font-bold">{{ formatNumber(config.armor) }}</div>
<div class="text-2xl font-bold">
<NumberWithTooltip :value="config.armor" />
</div>
</CardContent>
</Card>
@@ -46,7 +52,9 @@
</CardTitle>
</CardHeader>
<CardContent>
<div class="text-2xl font-bold">{{ formatNumber(config.speed) }}</div>
<div class="text-2xl font-bold">
<NumberWithTooltip :value="config.speed" />
</div>
</CardContent>
</Card>
@@ -58,7 +66,9 @@
</CardTitle>
</CardHeader>
<CardContent>
<div class="text-2xl font-bold">{{ formatNumber(config.cargoCapacity) }}</div>
<div class="text-2xl font-bold">
<NumberWithTooltip :value="config.cargoCapacity" />
</div>
</CardContent>
</Card>
@@ -70,7 +80,9 @@
</CardTitle>
</CardHeader>
<CardContent>
<div class="text-2xl font-bold">{{ formatNumber(config.fuelConsumption) }}</div>
<div class="text-2xl font-bold">
<NumberWithTooltip :value="config.fuelConsumption" />
</div>
</CardContent>
</Card>
</div>
@@ -84,19 +96,27 @@
<CardContent class="space-y-2">
<div v-if="config.cost.metal > 0" class="flex items-center justify-between text-sm">
<span class="text-muted-foreground">{{ t('resources.metal') }}:</span>
<span class="font-medium">{{ formatNumber(config.cost.metal) }}</span>
<span class="font-medium">
<NumberWithTooltip :value="config.cost.metal" />
</span>
</div>
<div v-if="config.cost.crystal > 0" class="flex items-center justify-between text-sm">
<span class="text-muted-foreground">{{ t('resources.crystal') }}:</span>
<span class="font-medium">{{ formatNumber(config.cost.crystal) }}</span>
<span class="font-medium">
<NumberWithTooltip :value="config.cost.crystal" />
</span>
</div>
<div v-if="config.cost.deuterium > 0" class="flex items-center justify-between text-sm">
<span class="text-muted-foreground">{{ t('resources.deuterium') }}:</span>
<span class="font-medium">{{ formatNumber(config.cost.deuterium) }}</span>
<span class="font-medium">
<NumberWithTooltip :value="config.cost.deuterium" />
</span>
</div>
<div class="flex items-center justify-between text-sm pt-2 border-t">
<span class="text-muted-foreground">{{ t('player.points') }}:</span>
<span class="font-bold text-primary">{{ pointsPerUnit }}</span>
<span class="font-bold text-primary">
<NumberWithTooltip :value="pointsPerUnit" />
</span>
</div>
</CardContent>
</Card>
@@ -128,22 +148,31 @@
<div class="space-y-1 text-sm">
<div class="flex justify-between">
<span>{{ t('resources.metal') }}:</span>
<span class="font-medium">{{ formatNumber(batchCost.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">{{ formatNumber(batchCost.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">{{ formatNumber(batchCost.deuterium) }}</span>
<span class="font-medium">
<NumberWithTooltip :value="batchCost.deuterium" />
</span>
</div>
</div>
</div>
<div class="space-y-2">
<p class="text-sm text-muted-foreground">{{ t('shipyard.totalTime') }}:</p>
<div class="text-xl font-bold">{{ formatTime(config.buildTime * quantity) }}</div>
<p class="text-xs text-muted-foreground">{{ t('player.points') }}: +{{ formatNumber(batchPoints) }}</p>
<p class="text-xs text-muted-foreground">
{{ t('player.points') }}: +
<NumberWithTooltip :value="batchPoints" />
</p>
</div>
</div>
</CardContent>
@@ -158,9 +187,11 @@
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import NumberWithTooltip from '@/components/NumberWithTooltip.vue'
import { Sword, Shield, ShieldCheck, Zap, Package, Fuel } from 'lucide-vue-next'
import * as pointsLogic from '@/logic/pointsLogic'
import { SHIPS } from '@/config/gameConfig'
import { formatTime } from '@/utils/format'
const { t } = useI18n()
@@ -187,18 +218,4 @@
const batchPoints = computed(() => {
return pointsLogic.calculateShipPoints(props.shipType, quantity.value)
})
const formatNumber = (num: number): string => {
return num.toLocaleString()
}
const formatTime = (seconds: number): string => {
if (seconds < 60) return `${seconds}${t('common.timeSecond')}`
const minutes = Math.floor(seconds / 60)
const secs = seconds % 60
if (minutes < 60) return `${minutes}${t('common.timeMinute')}${secs}${t('common.timeSecond')}`
const hours = Math.floor(minutes / 60)
const mins = minutes % 60
return `${hours}${t('common.timeHour')}${mins}${t('common.timeMinute')}`
}
</script>

View File

@@ -19,12 +19,21 @@
<Badge v-if="level === currentLevel" variant="default">{{ level }}</Badge>
<span v-else>{{ level }}</span>
</TableCell>
<TableCell class="text-center text-sm">{{ formatNumber(getLevelData(level).cost.metal) }}</TableCell>
<TableCell class="text-center text-sm">{{ formatNumber(getLevelData(level).cost.crystal) }}</TableCell>
<TableCell class="text-center text-sm">{{ formatNumber(getLevelData(level).cost.deuterium) }}</TableCell>
<TableCell class="text-center text-sm">
<NumberWithTooltip :value="getLevelData(level).cost.metal" />
</TableCell>
<TableCell class="text-center text-sm">
<NumberWithTooltip :value="getLevelData(level).cost.crystal" />
</TableCell>
<TableCell class="text-center text-sm">
<NumberWithTooltip :value="getLevelData(level).cost.deuterium" />
</TableCell>
<TableCell class="text-center text-sm">{{ formatTime(getLevelData(level).researchTime) }}</TableCell>
<TableCell class="text-center text-sm">
<span class="text-primary font-medium">+{{ getLevelData(level).points }}</span>
<span class="text-primary font-medium">
+
<NumberWithTooltip :value="getLevelData(level).points" />
</span>
</TableCell>
</TableRow>
</TableBody>
@@ -40,15 +49,21 @@
<CardContent class="space-y-2">
<div class="flex items-center justify-between text-sm">
<span class="text-muted-foreground">{{ t('resources.metal') }}:</span>
<span class="font-medium">{{ formatNumber(totalStats.metal) }}</span>
<span class="font-medium">
<NumberWithTooltip :value="totalStats.metal" />
</span>
</div>
<div class="flex items-center justify-between text-sm">
<span class="text-muted-foreground">{{ t('resources.crystal') }}:</span>
<span class="font-medium">{{ formatNumber(totalStats.crystal) }}</span>
<span class="font-medium">
<NumberWithTooltip :value="totalStats.crystal" />
</span>
</div>
<div class="flex items-center justify-between text-sm">
<span class="text-muted-foreground">{{ t('resources.deuterium') }}:</span>
<span class="font-medium">{{ formatNumber(totalStats.deuterium) }}</span>
<span class="font-medium">
<NumberWithTooltip :value="totalStats.deuterium" />
</span>
</div>
</CardContent>
</Card>
@@ -58,7 +73,9 @@
<CardTitle class="text-sm">{{ t('research.totalPoints') }}</CardTitle>
</CardHeader>
<CardContent>
<div class="text-3xl font-bold text-primary">{{ formatNumber(totalStats.points) }}</div>
<div class="text-3xl font-bold text-primary">
<NumberWithTooltip :value="totalStats.points" />
</div>
<p class="text-xs text-muted-foreground mt-1">
{{ t('research.levelRange') }}: {{ Math.max(0, currentLevel - 10) }} - {{ Math.min(currentLevel + 10, currentLevel + 10) }}
</p>
@@ -75,8 +92,10 @@
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import NumberWithTooltip from '@/components/NumberWithTooltip.vue'
import * as researchLogic from '@/logic/researchLogic'
import * as pointsLogic from '@/logic/pointsLogic'
import { formatTime } from '@/utils/format'
const { t } = useI18n()
@@ -85,12 +104,11 @@
currentLevel: number
}>()
// 等级范围:当前等级 ±10
// 等级范围:当前等级 +10
const levelRange = computed(() => {
const start = Math.max(0, props.currentLevel - 10)
const end = props.currentLevel + 10
const levels = []
for (let i = start; i <= end; i++) {
for (let i = props.currentLevel; i <= end; i++) {
levels.push(i)
}
return levels
@@ -137,18 +155,4 @@
return { metal, crystal, deuterium, points }
})
const formatNumber = (num: number): string => {
return num.toLocaleString()
}
const formatTime = (seconds: number): string => {
if (seconds < 60) return `${seconds}${t('common.timeSecond')}`
const minutes = Math.floor(seconds / 60)
const secs = seconds % 60
if (minutes < 60) return `${minutes}${t('common.timeMinute')}${secs}${t('common.timeSecond')}`
const hours = Math.floor(minutes / 60)
const mins = minutes % 60
return `${hours}${t('common.timeHour')}${mins}${t('common.timeMinute')}`
}
</script>

View File

@@ -14,22 +14,22 @@
</template>
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { useVModel } from '@vueuse/core'
import { cn } from '@/lib/utils'
import type { HTMLAttributes } from 'vue'
import { useVModel } from '@vueuse/core'
import { cn } from '@/lib/utils'
const props = defineProps<{
defaultValue?: string | number
modelValue?: string | number
class?: HTMLAttributes['class']
}>()
const props = defineProps<{
defaultValue?: string | number
modelValue?: string | number
class?: HTMLAttributes['class']
}>()
const emits = defineEmits<{
(e: 'update:modelValue', payload: string | number): void
}>()
const emits = defineEmits<{
(e: 'update:modelValue', payload: string | number): void
}>()
const modelValue = useVModel(props, 'modelValue', emits, {
passive: true,
defaultValue: props.defaultValue
})
const modelValue = useVModel(props, 'modelValue', emits, {
passive: true,
defaultValue: props.defaultValue
})
</script>

View File

@@ -0,0 +1,29 @@
<script setup lang="ts">
import type { SeparatorProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { Separator } from "reka-ui"
import { cn } from "@/lib/utils"
const props = withDefaults(defineProps<
SeparatorProps & { class?: HTMLAttributes["class"] }
>(), {
orientation: "horizontal",
decorative: true,
})
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<Separator
data-slot="separator"
v-bind="delegatedProps"
:class="
cn(
'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px',
props.class,
)
"
/>
</template>

View File

@@ -0,0 +1 @@
export { default as Separator } from "./Separator.vue"

View File

@@ -0,0 +1,19 @@
<script setup lang="ts">
import type { DialogRootEmits, DialogRootProps } from "reka-ui"
import { DialogRoot, useForwardPropsEmits } from "reka-ui"
const props = defineProps<DialogRootProps>()
const emits = defineEmits<DialogRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<DialogRoot
v-slot="slotProps"
data-slot="sheet"
v-bind="forwarded"
>
<slot v-bind="slotProps" />
</DialogRoot>
</template>

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
import type { DialogCloseProps } from "reka-ui"
import { DialogClose } from "reka-ui"
const props = defineProps<DialogCloseProps>()
</script>
<template>
<DialogClose
data-slot="sheet-close"
v-bind="props"
>
<slot />
</DialogClose>
</template>

View File

@@ -0,0 +1,62 @@
<script setup lang="ts">
import type { DialogContentEmits, DialogContentProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { X } from "lucide-vue-next"
import {
DialogClose,
DialogContent,
DialogPortal,
useForwardPropsEmits,
} from "reka-ui"
import { cn } from "@/lib/utils"
import SheetOverlay from "./SheetOverlay.vue"
interface SheetContentProps extends DialogContentProps {
class?: HTMLAttributes["class"]
side?: "top" | "right" | "bottom" | "left"
}
defineOptions({
inheritAttrs: false,
})
const props = withDefaults(defineProps<SheetContentProps>(), {
side: "right",
})
const emits = defineEmits<DialogContentEmits>()
const delegatedProps = reactiveOmit(props, "class", "side")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<DialogPortal>
<SheetOverlay />
<DialogContent
data-slot="sheet-content"
:class="cn(
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
side === 'right'
&& 'data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm',
side === 'left'
&& 'data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm',
side === 'top'
&& 'data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b',
side === 'bottom'
&& 'data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t',
props.class)"
v-bind="{ ...$attrs, ...forwarded }"
>
<slot />
<DialogClose
class="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none"
>
<X class="size-4" />
<span class="sr-only">Close</span>
</DialogClose>
</DialogContent>
</DialogPortal>
</template>

View File

@@ -0,0 +1,21 @@
<script setup lang="ts">
import type { DialogDescriptionProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { DialogDescription } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<DialogDescription
data-slot="sheet-description"
:class="cn('text-muted-foreground text-sm', props.class)"
v-bind="delegatedProps"
>
<slot />
</DialogDescription>
</template>

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>
<template>
<div
data-slot="sheet-footer"
:class="cn('mt-auto flex flex-col gap-2 p-4', props.class)
"
>
<slot />
</div>
</template>

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>
<template>
<div
data-slot="sheet-header"
:class="cn('flex flex-col gap-1.5 p-4', props.class)"
>
<slot />
</div>
</template>

View File

@@ -0,0 +1,21 @@
<script setup lang="ts">
import type { DialogOverlayProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { DialogOverlay } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<DialogOverlayProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<DialogOverlay
data-slot="sheet-overlay"
:class="cn('data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80', props.class)"
v-bind="delegatedProps"
>
<slot />
</DialogOverlay>
</template>

View File

@@ -0,0 +1,21 @@
<script setup lang="ts">
import type { DialogTitleProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { DialogTitle } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<DialogTitleProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<DialogTitle
data-slot="sheet-title"
:class="cn('text-foreground font-semibold', props.class)"
v-bind="delegatedProps"
>
<slot />
</DialogTitle>
</template>

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
import type { DialogTriggerProps } from "reka-ui"
import { DialogTrigger } from "reka-ui"
const props = defineProps<DialogTriggerProps>()
</script>
<template>
<DialogTrigger
data-slot="sheet-trigger"
v-bind="props"
>
<slot />
</DialogTrigger>
</template>

View File

@@ -0,0 +1,8 @@
export { default as Sheet } from "./Sheet.vue"
export { default as SheetClose } from "./SheetClose.vue"
export { default as SheetContent } from "./SheetContent.vue"
export { default as SheetDescription } from "./SheetDescription.vue"
export { default as SheetFooter } from "./SheetFooter.vue"
export { default as SheetHeader } from "./SheetHeader.vue"
export { default as SheetTitle } from "./SheetTitle.vue"
export { default as SheetTrigger } from "./SheetTrigger.vue"

View File

@@ -0,0 +1,100 @@
<template>
<div
v-if="collapsible === 'none'"
data-slot="sidebar"
:class="cn('bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col', props.class)"
v-bind="$attrs"
>
<slot />
</div>
<Sheet v-else-if="isMobile" :open="openMobile" v-bind="$attrs" @update:open="setOpenMobile">
<SheetContent
data-sidebar="sidebar"
data-slot="sidebar"
data-mobile="true"
:side="side"
class="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
:style="{
'--sidebar-width': SIDEBAR_WIDTH_MOBILE
}"
>
<SheetHeader class="sr-only">
<SheetTitle>Sidebar</SheetTitle>
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
</SheetHeader>
<div class="flex h-full w-full flex-col">
<slot />
</div>
</SheetContent>
</Sheet>
<div
v-else
class="group peer text-sidebar-foreground hidden md:block"
data-slot="sidebar"
:data-state="state"
:data-collapsible="state === 'collapsed' ? collapsible : ''"
:data-variant="variant"
:data-side="side"
>
<!-- This is what handles the sidebar gap on desktop -->
<div
:class="
cn(
'relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear',
'group-data-[collapsible=offcanvas]:w-0',
'group-data-[side=right]:rotate-180',
variant === 'floating' || variant === 'inset'
? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]'
: 'group-data-[collapsible=icon]:w-(--sidebar-width-icon)'
)
"
/>
<div
:class="
cn(
'fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex',
side === 'left'
? 'left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]'
: 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
// Adjust the padding for floating and inset variants.
variant === 'floating' || variant === 'inset'
? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]'
: 'group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l',
props.class
)
"
v-bind="$attrs"
>
<div
data-sidebar="sidebar"
class="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
>
<slot />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { SidebarProps } from '.'
import { cn } from '@/lib/utils'
import { Sheet, SheetContent } from '@/components/ui/sheet'
import SheetDescription from '@/components/ui/sheet/SheetDescription.vue'
import SheetHeader from '@/components/ui/sheet/SheetHeader.vue'
import SheetTitle from '@/components/ui/sheet/SheetTitle.vue'
import { SIDEBAR_WIDTH_MOBILE, useSidebar } from './utils'
defineOptions({
inheritAttrs: false
})
const props = withDefaults(defineProps<SidebarProps>(), {
side: 'left',
variant: 'sidebar',
collapsible: 'offcanvas'
})
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
</script>

View File

@@ -0,0 +1,18 @@
<template>
<div
data-slot="sidebar-content"
data-sidebar="content"
:class="cn('flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden', 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="sidebar-footer" data-sidebar="footer" :class="cn('flex flex-col gap-2 p-2', 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="sidebar-group" data-sidebar="group" :class="cn('relative flex w-full min-w-0 flex-col p-2', 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,31 @@
<template>
<Primitive
data-slot="sidebar-group-action"
data-sidebar="group-action"
:as="as"
:as-child="asChild"
:class="
cn(
'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
'after:absolute after:-inset-2 md:after:hidden',
'group-data-[collapsible=icon]:hidden',
props.class
)
"
>
<slot />
</Primitive>
</template>
<script setup lang="ts">
import type { PrimitiveProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { Primitive } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<
PrimitiveProps & {
class?: HTMLAttributes['class']
}
>()
</script>

View File

@@ -0,0 +1,14 @@
<template>
<div data-slot="sidebar-group-content" data-sidebar="group-content" :class="cn('w-full 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,30 @@
<template>
<Primitive
data-slot="sidebar-group-label"
data-sidebar="group-label"
:as="as"
:as-child="asChild"
:class="
cn(
'text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
'group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0',
props.class
)
"
>
<slot />
</Primitive>
</template>
<script setup lang="ts">
import type { PrimitiveProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { Primitive } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<
PrimitiveProps & {
class?: HTMLAttributes['class']
}
>()
</script>

View File

@@ -0,0 +1,14 @@
<template>
<div data-slot="sidebar-header" data-sidebar="header" :class="cn('flex flex-col gap-2 p-2', 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,15 @@
<template>
<Input data-slot="sidebar-input" data-sidebar="input" :class="cn('bg-background h-8 w-full shadow-none', props.class)">
<slot />
</Input>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { Input } from '@/components/ui/input'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>

View File

@@ -0,0 +1,23 @@
<template>
<main
data-slot="sidebar-inset"
:class="
cn(
'bg-background relative flex w-full flex-1 flex-col',
'md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2',
props.class
)
"
>
<slot />
</main>
</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>
<ul data-slot="sidebar-menu" data-sidebar="menu" :class="cn('flex w-full min-w-0 flex-col gap-1', props.class)">
<slot />
</ul>
</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,41 @@
<template>
<Primitive
data-slot="sidebar-menu-action"
data-sidebar="menu-action"
:class="
cn(
'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
'after:absolute after:-inset-2 md:after:hidden',
'peer-data-[size=sm]/menu-button:top-1',
'peer-data-[size=default]/menu-button:top-1.5',
'peer-data-[size=lg]/menu-button:top-2.5',
'group-data-[collapsible=icon]:hidden',
showOnHover &&
'peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0',
props.class
)
"
:as="as"
:as-child="asChild"
>
<slot />
</Primitive>
</template>
<script setup lang="ts">
import type { PrimitiveProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { Primitive } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = withDefaults(
defineProps<
PrimitiveProps & {
showOnHover?: boolean
class?: HTMLAttributes['class']
}
>(),
{
as: 'button'
}
)
</script>

View File

@@ -0,0 +1,28 @@
<template>
<div
data-slot="sidebar-menu-badge"
data-sidebar="menu-badge"
:class="
cn(
'text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none',
'peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground',
'peer-data-[size=sm]/menu-button:top-1',
'peer-data-[size=default]/menu-button:top-1.5',
'peer-data-[size=lg]/menu-button:top-2.5',
'group-data-[collapsible=icon]:hidden',
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,49 @@
<template>
<SidebarMenuButtonChild v-if="!tooltip" v-bind="{ ...delegatedProps, ...$attrs }">
<slot />
</SidebarMenuButtonChild>
<Tooltip v-else>
<TooltipTrigger as-child>
<SidebarMenuButtonChild v-bind="{ ...delegatedProps, ...$attrs }">
<slot />
</SidebarMenuButtonChild>
</TooltipTrigger>
<TooltipContent side="right" align="center" :hidden="state !== 'collapsed' || isMobile">
<template v-if="typeof tooltip === 'string'">
{{ tooltip }}
</template>
<component :is="tooltip" v-else />
</TooltipContent>
</Tooltip>
</template>
<script setup lang="ts">
import type { Component } from 'vue'
import type { SidebarMenuButtonProps } from './SidebarMenuButtonChild.vue'
import { reactiveOmit } from '@vueuse/core'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import SidebarMenuButtonChild from './SidebarMenuButtonChild.vue'
import { useSidebar } from './utils'
defineOptions({
inheritAttrs: false
})
const props = withDefaults(
defineProps<
SidebarMenuButtonProps & {
tooltip?: string | Component
}
>(),
{
as: 'button',
variant: 'default',
size: 'default'
}
)
const { isMobile, state } = useSidebar()
const delegatedProps = reactiveOmit(props, 'tooltip')
</script>

View File

@@ -0,0 +1,36 @@
<template>
<Primitive
data-slot="sidebar-menu-button"
data-sidebar="menu-button"
:data-size="size"
:data-active="isActive"
:class="cn(sidebarMenuButtonVariants({ variant, size }), props.class)"
:as="as"
:as-child="asChild"
v-bind="$attrs"
>
<slot />
</Primitive>
</template>
<script setup lang="ts">
import type { PrimitiveProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import type { SidebarMenuButtonVariants } from '.'
import { Primitive } from 'reka-ui'
import { cn } from '@/lib/utils'
import { sidebarMenuButtonVariants } from '.'
export interface SidebarMenuButtonProps extends PrimitiveProps {
variant?: SidebarMenuButtonVariants['variant']
size?: SidebarMenuButtonVariants['size']
isActive?: boolean
class?: HTMLAttributes['class']
}
const props = withDefaults(defineProps<SidebarMenuButtonProps>(), {
as: 'button',
variant: 'default',
size: 'default'
})
</script>

View File

@@ -0,0 +1,14 @@
<template>
<li data-slot="sidebar-menu-item" data-sidebar="menu-item" :class="cn('group/menu-item relative', props.class)">
<slot />
</li>
</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,27 @@
<template>
<div
data-slot="sidebar-menu-skeleton"
data-sidebar="menu-skeleton"
:class="cn('flex h-8 items-center gap-2 rounded-md px-2', props.class)"
>
<Skeleton v-if="showIcon" class="size-4 rounded-md" data-sidebar="menu-skeleton-icon" />
<Skeleton class="h-4 max-w-(--skeleton-width) flex-1" data-sidebar="menu-skeleton-text" :style="{ '--skeleton-width': width }" />
</div>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { computed } from 'vue'
import { cn } from '@/lib/utils'
import { Skeleton } from '@/components/ui/skeleton'
const props = defineProps<{
showIcon?: boolean
class?: HTMLAttributes['class']
}>()
const width = computed(() => {
return `${Math.floor(Math.random() * 40) + 50}%`
})
</script>

View File

@@ -0,0 +1,24 @@
<template>
<ul
data-slot="sidebar-menu-sub"
data-sidebar="menu-badge"
:class="
cn(
'border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5',
'group-data-[collapsible=icon]:hidden',
props.class
)
"
>
<slot />
</ul>
</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,43 @@
<template>
<Primitive
data-slot="sidebar-menu-sub-button"
data-sidebar="menu-sub-button"
:as="as"
:as-child="asChild"
:data-size="size"
:data-active="isActive"
:class="
cn(
'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
'data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground',
size === 'sm' && 'text-xs',
size === 'md' && 'text-sm',
'group-data-[collapsible=icon]:hidden',
props.class
)
"
>
<slot />
</Primitive>
</template>
<script setup lang="ts">
import type { PrimitiveProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { Primitive } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = withDefaults(
defineProps<
PrimitiveProps & {
size?: 'sm' | 'md'
isActive?: boolean
class?: HTMLAttributes['class']
}
>(),
{
as: 'a',
size: 'md'
}
)
</script>

View File

@@ -0,0 +1,14 @@
<template>
<li data-slot="sidebar-menu-sub-item" data-sidebar="menu-sub-item" :class="cn('group/menu-sub-item relative', props.class)">
<slot />
</li>
</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,92 @@
<template>
<TooltipProvider :delay-duration="0">
<div
data-slot="sidebar-wrapper"
:style="{
'--sidebar-width': SIDEBAR_WIDTH,
'--sidebar-width-icon': SIDEBAR_WIDTH_ICON
}"
:class="cn('group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full', props.class)"
v-bind="$attrs"
>
<slot />
</div>
</TooltipProvider>
</template>
<script setup lang="ts">
import type { HTMLAttributes, Ref } from 'vue'
import { defaultDocument, useEventListener, useMediaQuery, useVModel } from '@vueuse/core'
import { TooltipProvider } from 'reka-ui'
import { computed, ref } from 'vue'
import { cn } from '@/lib/utils'
import {
provideSidebarContext,
SIDEBAR_COOKIE_MAX_AGE,
SIDEBAR_COOKIE_NAME,
SIDEBAR_KEYBOARD_SHORTCUT,
SIDEBAR_WIDTH,
SIDEBAR_WIDTH_ICON
} from './utils'
const props = withDefaults(
defineProps<{
defaultOpen?: boolean
open?: boolean
class?: HTMLAttributes['class']
}>(),
{
defaultOpen: !defaultDocument?.cookie.includes(`${SIDEBAR_COOKIE_NAME}=false`),
open: undefined
}
)
const emits = defineEmits<{
'update:open': [open: boolean]
}>()
const isMobile = useMediaQuery('(max-width: 768px)')
const openMobile = ref(false)
const open = useVModel(props, 'open', emits, {
defaultValue: props.defaultOpen ?? false,
passive: (props.open === undefined) as false
}) as Ref<boolean>
function setOpen(value: boolean) {
open.value = value // emits('update:open', value)
// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${open.value}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
}
function setOpenMobile(value: boolean) {
openMobile.value = value
}
// Helper to toggle the sidebar.
function toggleSidebar() {
return isMobile.value ? setOpenMobile(!openMobile.value) : setOpen(!open.value)
}
useEventListener('keydown', (event: KeyboardEvent) => {
if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {
event.preventDefault()
toggleSidebar()
}
})
// We add a state so that we can do data-state="expanded" or "collapsed".
// This makes it easier to style the sidebar with Tailwind classes.
const state = computed(() => (open.value ? 'expanded' : 'collapsed'))
provideSidebarContext({
state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar
})
</script>

View File

@@ -0,0 +1,35 @@
<template>
<button
data-sidebar="rail"
data-slot="sidebar-rail"
aria-label="Toggle Sidebar"
:tabindex="-1"
title="Toggle Sidebar"
:class="
cn(
'hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex',
'in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize',
'[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize',
'hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full',
'[[data-side=left][data-collapsible=offcanvas]_&]:-right-2',
'[[data-side=right][data-collapsible=offcanvas]_&]:-left-2',
props.class
)
"
@click="toggleSidebar"
>
<slot />
</button>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { useSidebar } from './utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
const { toggleSidebar } = useSidebar()
</script>

View File

@@ -0,0 +1,15 @@
<template>
<Separator data-slot="sidebar-separator" data-sidebar="separator" :class="cn('bg-sidebar-border mx-2 w-auto', props.class)">
<slot />
</Separator>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { Separator } from '@/components/ui/separator'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>

View File

@@ -0,0 +1,27 @@
<template>
<Button
data-sidebar="trigger"
data-slot="sidebar-trigger"
variant="ghost"
size="icon"
:class="cn('h-7 w-7', props.class)"
@click="toggleSidebar"
>
<PanelLeft />
<span class="sr-only">Toggle Sidebar</span>
</Button>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { PanelLeft } from 'lucide-vue-next'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
import { useSidebar } from './utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
const { toggleSidebar } = useSidebar()
</script>

View File

@@ -0,0 +1,60 @@
import type { VariantProps } from 'class-variance-authority'
import type { HTMLAttributes } from 'vue'
import { cva } from 'class-variance-authority'
export interface SidebarProps {
side?: 'left' | 'right'
variant?: 'sidebar' | 'floating' | 'inset'
collapsible?: 'offcanvas' | 'icon' | 'none'
class?: HTMLAttributes['class']
}
export { default as Sidebar } from './Sidebar.vue'
export { default as SidebarContent } from './SidebarContent.vue'
export { default as SidebarFooter } from './SidebarFooter.vue'
export { default as SidebarGroup } from './SidebarGroup.vue'
export { default as SidebarGroupAction } from './SidebarGroupAction.vue'
export { default as SidebarGroupContent } from './SidebarGroupContent.vue'
export { default as SidebarGroupLabel } from './SidebarGroupLabel.vue'
export { default as SidebarHeader } from './SidebarHeader.vue'
export { default as SidebarInput } from './SidebarInput.vue'
export { default as SidebarInset } from './SidebarInset.vue'
export { default as SidebarMenu } from './SidebarMenu.vue'
export { default as SidebarMenuAction } from './SidebarMenuAction.vue'
export { default as SidebarMenuBadge } from './SidebarMenuBadge.vue'
export { default as SidebarMenuButton } from './SidebarMenuButton.vue'
export { default as SidebarMenuItem } from './SidebarMenuItem.vue'
export { default as SidebarMenuSkeleton } from './SidebarMenuSkeleton.vue'
export { default as SidebarMenuSub } from './SidebarMenuSub.vue'
export { default as SidebarMenuSubButton } from './SidebarMenuSubButton.vue'
export { default as SidebarMenuSubItem } from './SidebarMenuSubItem.vue'
export { default as SidebarProvider } from './SidebarProvider.vue'
export { default as SidebarRail } from './SidebarRail.vue'
export { default as SidebarSeparator } from './SidebarSeparator.vue'
export { default as SidebarTrigger } from './SidebarTrigger.vue'
export { useSidebar } from './utils'
export const sidebarMenuButtonVariants = cva(
'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
{
variants: {
variant: {
default: 'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
outline:
'bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]'
},
size: {
default: 'h-8 text-sm',
sm: 'h-7 text-xs',
lg: 'h-12 text-sm group-data-[collapsible=icon]:p-0!'
}
},
defaultVariants: {
variant: 'default',
size: 'default'
}
}
)
export type SidebarMenuButtonVariants = VariantProps<typeof sidebarMenuButtonVariants>

View File

@@ -0,0 +1,19 @@
import type { ComputedRef, Ref } from 'vue'
import { createContext } from 'reka-ui'
export const SIDEBAR_COOKIE_NAME = 'sidebar_state'
export const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
export const SIDEBAR_WIDTH = '16rem'
export const SIDEBAR_WIDTH_MOBILE = '18rem'
export const SIDEBAR_WIDTH_ICON = '3rem'
export const SIDEBAR_KEYBOARD_SHORTCUT = 'b'
export const [useSidebar, provideSidebarContext] = createContext<{
state: ComputedRef<'expanded' | 'collapsed'>
open: Ref<boolean>
setOpen: (value: boolean) => void
isMobile: Ref<boolean>
openMobile: Ref<boolean>
setOpenMobile: (value: boolean) => void
toggleSidebar: () => void
}>('Sidebar')

View File

@@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
interface SkeletonProps {
class?: HTMLAttributes["class"]
}
const props = defineProps<SkeletonProps>()
</script>
<template>
<div
data-slot="skeleton"
:class="cn('animate-pulse rounded-md bg-primary/10', props.class)"
/>
</template>

View File

@@ -0,0 +1 @@
export { default as Skeleton } from "./Skeleton.vue"

View File

@@ -1,12 +1,3 @@
<script lang="ts" setup>
import type { ToasterProps } from "vue-sonner"
import { CircleCheckIcon, InfoIcon, Loader2Icon, OctagonXIcon, TriangleAlertIcon, XIcon } from "lucide-vue-next"
import { Toaster as Sonner } from "vue-sonner"
import { cn } from "@/lib/utils"
const props = defineProps<ToasterProps>()
</script>
<template>
<Sonner
:class="cn('toaster group', props.class)"
@@ -14,7 +5,7 @@ const props = defineProps<ToasterProps>()
'--normal-bg': 'var(--popover)',
'--normal-text': 'var(--popover-foreground)',
'--normal-border': 'var(--border)',
'--border-radius': 'var(--radius)',
'--border-radius': 'var(--radius)'
}"
v-bind="props"
>
@@ -40,3 +31,12 @@ const props = defineProps<ToasterProps>()
</template>
</Sonner>
</template>
<script lang="ts" setup>
import type { ToasterProps } from 'vue-sonner'
import { CircleCheckIcon, InfoIcon, Loader2Icon, OctagonXIcon, TriangleAlertIcon, XIcon } from 'lucide-vue-next'
import { Toaster as Sonner } from 'vue-sonner'
import { cn } from '@/lib/utils'
const props = defineProps<ToasterProps>()
</script>

View File

@@ -1 +1 @@
export { default as Toaster } from "./Sonner.vue"
export { default as Toaster } from './Sonner.vue'

View File

@@ -0,0 +1,15 @@
<template>
<TooltipRoot v-slot="slotProps" data-slot="tooltip" v-bind="forwarded">
<slot v-bind="slotProps" />
</TooltipRoot>
</template>
<script setup lang="ts">
import type { TooltipRootEmits, TooltipRootProps } from 'reka-ui'
import { TooltipRoot, useForwardPropsEmits } from 'reka-ui'
const props = defineProps<TooltipRootProps>()
const emits = defineEmits<TooltipRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>

View File

@@ -0,0 +1,39 @@
<template>
<TooltipPortal>
<TooltipContent
data-slot="tooltip-content"
v-bind="{ ...forwarded, ...$attrs }"
:class="
cn(
'bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit rounded-md px-3 py-1.5 text-xs text-balance',
props.class
)
"
>
<slot />
<TooltipArrow class="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
</TooltipContent>
</TooltipPortal>
</template>
<script setup lang="ts">
import type { TooltipContentEmits, TooltipContentProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { TooltipArrow, TooltipContent, TooltipPortal, useForwardPropsEmits } from 'reka-ui'
import { cn } from '@/lib/utils'
defineOptions({
inheritAttrs: false
})
const props = withDefaults(defineProps<TooltipContentProps & { class?: HTMLAttributes['class'] }>(), {
sideOffset: 4
})
const emits = defineEmits<TooltipContentEmits>()
const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>

View File

@@ -0,0 +1,14 @@
<template>
<TooltipProvider v-bind="props">
<slot />
</TooltipProvider>
</template>
<script setup lang="ts">
import type { TooltipProviderProps } from 'reka-ui'
import { TooltipProvider } from 'reka-ui'
const props = withDefaults(defineProps<TooltipProviderProps>(), {
delayDuration: 0
})
</script>

View File

@@ -0,0 +1,12 @@
<template>
<TooltipTrigger data-slot="tooltip-trigger" v-bind="props">
<slot />
</TooltipTrigger>
</template>
<script setup lang="ts">
import type { TooltipTriggerProps } from 'reka-ui'
import { TooltipTrigger } from 'reka-ui'
const props = defineProps<TooltipTriggerProps>()
</script>

View File

@@ -0,0 +1,4 @@
export { default as Tooltip } from './Tooltip.vue'
export { default as TooltipContent } from './TooltipContent.vue'
export { default as TooltipProvider } from './TooltipProvider.vue'
export { default as TooltipTrigger } from './TooltipTrigger.vue'

View File

@@ -33,9 +33,11 @@ export const useGameConfig = () => {
[BuildingType.CrystalStorage]: 'crystalStorage',
[BuildingType.DeuteriumTank]: 'deuteriumTank',
[BuildingType.DarkMatterCollector]: 'darkMatterCollector',
[BuildingType.Terraformer]: 'terraformer',
[BuildingType.LunarBase]: 'lunarBase',
[BuildingType.SensorPhalanx]: 'sensorPhalanx',
[BuildingType.JumpGate]: 'jumpGate'
[BuildingType.JumpGate]: 'jumpGate',
[BuildingType.PlanetDestroyerFactory]: 'planetDestroyerFactory'
}
// 舰船类型枚举值到翻译键的映射
@@ -49,7 +51,8 @@ export const useGameConfig = () => {
[ShipType.ColonyShip]: 'colonyShip',
[ShipType.Recycler]: 'recycler',
[ShipType.EspionageProbe]: 'espionageProbe',
[ShipType.DarkMatterHarvester]: 'darkMatterHarvester'
[ShipType.DarkMatterHarvester]: 'darkMatterHarvester',
[ShipType.Deathstar]: 'deathstar'
}
// 防御设施类型枚举值到翻译键的映射
@@ -61,7 +64,8 @@ export const useGameConfig = () => {
[DefenseType.IonCannon]: 'ionCannon',
[DefenseType.PlasmaTurret]: 'plasmaTurret',
[DefenseType.SmallShieldDome]: 'smallShieldDome',
[DefenseType.LargeShieldDome]: 'largeShieldDome'
[DefenseType.LargeShieldDome]: 'largeShieldDome',
[DefenseType.PlanetaryShield]: 'planetaryShield'
}
// 科技类型枚举值到翻译键的映射
@@ -75,7 +79,9 @@ export const useGameConfig = () => {
[TechnologyType.CombustionDrive]: 'combustionDrive',
[TechnologyType.ImpulseDrive]: 'impulseDrive',
[TechnologyType.HyperspaceDrive]: 'hyperspaceDrive',
[TechnologyType.DarkMatterTechnology]: 'darkMatterTechnology'
[TechnologyType.DarkMatterTechnology]: 'darkMatterTechnology',
[TechnologyType.TerraformingTechnology]: 'terraformingTechnology',
[TechnologyType.PlanetDestructionTech]: 'planetDestructionTech'
}
// 军官类型枚举值到翻译键的映射

View File

@@ -10,8 +10,14 @@ export const BUILDINGS: Record<BuildingType, BuildingConfig> = {
baseCost: { metal: 60, crystal: 15, deuterium: 0, darkMatter: 0, energy: 0 },
baseTime: 15, // 减少建造时间30→15秒
costMultiplier: 1.5,
spaceUsage: 3,
planetOnly: true
spaceUsage: 1,
planetOnly: true,
requirements: { [BuildingType.SolarPlant]: 1 },
levelRequirements: {
10: { [BuildingType.RoboticsFactory]: 2 },
20: { [BuildingType.RoboticsFactory]: 5, [BuildingType.ResearchLab]: 3 },
30: { [BuildingType.NaniteFactory]: 1, [BuildingType.ResearchLab]: 8 }
}
},
[BuildingType.CrystalMine]: {
id: BuildingType.CrystalMine,
@@ -20,8 +26,14 @@ export const BUILDINGS: Record<BuildingType, BuildingConfig> = {
baseCost: { metal: 48, crystal: 24, deuterium: 0, darkMatter: 0, energy: 0 },
baseTime: 15, // 减少建造时间30→15秒
costMultiplier: 1.6,
spaceUsage: 3,
planetOnly: true
spaceUsage: 1,
planetOnly: true,
requirements: { [BuildingType.SolarPlant]: 1 },
levelRequirements: {
10: { [BuildingType.RoboticsFactory]: 2 },
20: { [BuildingType.RoboticsFactory]: 5, [BuildingType.ResearchLab]: 3 },
30: { [BuildingType.NaniteFactory]: 1, [BuildingType.ResearchLab]: 8 }
}
},
[BuildingType.DeuteriumSynthesizer]: {
id: BuildingType.DeuteriumSynthesizer,
@@ -30,8 +42,14 @@ export const BUILDINGS: Record<BuildingType, BuildingConfig> = {
baseCost: { metal: 225, crystal: 75, deuterium: 0, darkMatter: 0, energy: 0 },
baseTime: 20, // 减少建造时间30→20秒
costMultiplier: 1.5,
spaceUsage: 4,
planetOnly: true
spaceUsage: 2,
planetOnly: true,
requirements: { [BuildingType.SolarPlant]: 1 },
levelRequirements: {
10: { [BuildingType.RoboticsFactory]: 2 },
20: { [BuildingType.RoboticsFactory]: 5, [BuildingType.ResearchLab]: 3 },
30: { [BuildingType.NaniteFactory]: 1, [BuildingType.ResearchLab]: 8 }
}
},
[BuildingType.SolarPlant]: {
id: BuildingType.SolarPlant,
@@ -40,7 +58,12 @@ export const BUILDINGS: Record<BuildingType, BuildingConfig> = {
baseCost: { metal: 75, crystal: 30, deuterium: 0, darkMatter: 0, energy: 0 },
baseTime: 15, // 减少建造时间30→15秒
costMultiplier: 1.5,
spaceUsage: 5
spaceUsage: 2,
levelRequirements: {
15: { [BuildingType.RoboticsFactory]: 3 },
25: { [BuildingType.RoboticsFactory]: 6, [BuildingType.ResearchLab]: 5 },
35: { [BuildingType.NaniteFactory]: 1, [BuildingType.ResearchLab]: 10 }
}
},
[BuildingType.RoboticsFactory]: {
id: BuildingType.RoboticsFactory,
@@ -49,7 +72,17 @@ export const BUILDINGS: Record<BuildingType, BuildingConfig> = {
baseCost: { metal: 400, crystal: 120, deuterium: 200, darkMatter: 0, energy: 0 },
baseTime: 40, // 减少建造时间60→40秒
costMultiplier: 2,
spaceUsage: 6
spaceUsage: 4,
requirements: {
[BuildingType.MetalMine]: 2,
[BuildingType.CrystalMine]: 2,
[BuildingType.DeuteriumSynthesizer]: 2
},
levelRequirements: {
5: { [BuildingType.ResearchLab]: 3, [BuildingType.SolarPlant]: 8 },
8: { [BuildingType.ResearchLab]: 6, [BuildingType.SolarPlant]: 12, [BuildingType.MetalMine]: 12, [BuildingType.CrystalMine]: 12 },
10: { [BuildingType.ResearchLab]: 8, [BuildingType.NaniteFactory]: 1 }
}
},
[BuildingType.NaniteFactory]: {
id: BuildingType.NaniteFactory,
@@ -58,8 +91,13 @@ export const BUILDINGS: Record<BuildingType, BuildingConfig> = {
baseCost: { metal: 1000000, crystal: 500000, deuterium: 100000, darkMatter: 0, energy: 0 },
baseTime: 240, // 减少建造时间300→240秒
costMultiplier: 2,
spaceUsage: 15,
requirements: { [BuildingType.RoboticsFactory]: 10 }
spaceUsage: 8,
maxLevel: 10, // 最多10级最多11个建造队列
requirements: { [BuildingType.RoboticsFactory]: 10 },
levelRequirements: {
3: { [BuildingType.ResearchLab]: 10, [BuildingType.Shipyard]: 8, [TechnologyType.ComputerTechnology]: 8 },
5: { [BuildingType.ResearchLab]: 12, [BuildingType.Shipyard]: 10, [TechnologyType.ComputerTechnology]: 10 }
}
},
[BuildingType.Shipyard]: {
id: BuildingType.Shipyard,
@@ -68,7 +106,13 @@ export const BUILDINGS: Record<BuildingType, BuildingConfig> = {
baseCost: { metal: 400, crystal: 200, deuterium: 100, darkMatter: 0, energy: 0 },
baseTime: 30, // 减少建造时间60→30秒
costMultiplier: 2,
spaceUsage: 8
spaceUsage: 5,
fleetStorageBonus: 1000, // 每级增加100舰队仓储
requirements: { [BuildingType.RoboticsFactory]: 2 },
levelRequirements: {
8: { [BuildingType.RoboticsFactory]: 5, [BuildingType.ResearchLab]: 5 },
12: { [BuildingType.RoboticsFactory]: 8, [BuildingType.ResearchLab]: 8, [BuildingType.NaniteFactory]: 2 }
}
},
[BuildingType.ResearchLab]: {
id: BuildingType.ResearchLab,
@@ -77,7 +121,21 @@ export const BUILDINGS: Record<BuildingType, BuildingConfig> = {
baseCost: { metal: 200, crystal: 400, deuterium: 200, darkMatter: 0, energy: 0 },
baseTime: 30, // 减少建造时间60→30秒
costMultiplier: 2,
spaceUsage: 5
spaceUsage: 3,
requirements: {
[BuildingType.MetalMine]: 3,
[BuildingType.CrystalMine]: 3,
[BuildingType.DeuteriumSynthesizer]: 3
},
levelRequirements: {
8: {
[BuildingType.RoboticsFactory]: 5,
[BuildingType.MetalMine]: 10,
[BuildingType.CrystalMine]: 10,
[BuildingType.DeuteriumSynthesizer]: 10
},
12: { [BuildingType.RoboticsFactory]: 8, [BuildingType.NaniteFactory]: 1, [TechnologyType.EnergyTechnology]: 5 }
}
},
[BuildingType.MetalStorage]: {
id: BuildingType.MetalStorage,
@@ -86,7 +144,12 @@ export const BUILDINGS: Record<BuildingType, BuildingConfig> = {
baseCost: { metal: 1000, crystal: 0, deuterium: 0, darkMatter: 0, energy: 0 },
baseTime: 15, // 减少建造时间30→15秒
costMultiplier: 2,
spaceUsage: 2
spaceUsage: 1,
requirements: { [BuildingType.MetalMine]: 2 },
levelRequirements: {
8: { [BuildingType.MetalMine]: 15, [BuildingType.RoboticsFactory]: 3 },
12: { [BuildingType.MetalMine]: 25, [BuildingType.RoboticsFactory]: 6 }
}
},
[BuildingType.CrystalStorage]: {
id: BuildingType.CrystalStorage,
@@ -95,7 +158,12 @@ export const BUILDINGS: Record<BuildingType, BuildingConfig> = {
baseCost: { metal: 1000, crystal: 500, deuterium: 0, darkMatter: 0, energy: 0 },
baseTime: 15, // 减少建造时间30→15秒
costMultiplier: 2,
spaceUsage: 2
spaceUsage: 1,
requirements: { [BuildingType.CrystalMine]: 2 },
levelRequirements: {
8: { [BuildingType.CrystalMine]: 15, [BuildingType.RoboticsFactory]: 3 },
12: { [BuildingType.CrystalMine]: 25, [BuildingType.RoboticsFactory]: 6 }
}
},
[BuildingType.DeuteriumTank]: {
id: BuildingType.DeuteriumTank,
@@ -104,7 +172,12 @@ export const BUILDINGS: Record<BuildingType, BuildingConfig> = {
baseCost: { metal: 1000, crystal: 1000, deuterium: 0, darkMatter: 0, energy: 0 },
baseTime: 15, // 减少建造时间30→15秒
costMultiplier: 2,
spaceUsage: 2
spaceUsage: 1,
requirements: { [BuildingType.DeuteriumSynthesizer]: 2 },
levelRequirements: {
8: { [BuildingType.DeuteriumSynthesizer]: 15, [BuildingType.RoboticsFactory]: 3 },
12: { [BuildingType.DeuteriumSynthesizer]: 25, [BuildingType.RoboticsFactory]: 6 }
}
},
[BuildingType.DarkMatterCollector]: {
id: BuildingType.DarkMatterCollector,
@@ -113,8 +186,35 @@ export const BUILDINGS: Record<BuildingType, BuildingConfig> = {
baseCost: { metal: 50000, crystal: 100000, deuterium: 50000, darkMatter: 0, energy: 0 },
baseTime: 90, // 减少建造时间120→90秒
costMultiplier: 2,
spaceUsage: 10,
planetOnly: true
spaceUsage: 6,
planetOnly: true,
requirements: {
[BuildingType.ResearchLab]: 5,
[TechnologyType.DarkMatterTechnology]: 1
},
levelRequirements: {
5: { [BuildingType.ResearchLab]: 8, [TechnologyType.DarkMatterTechnology]: 3, [BuildingType.RoboticsFactory]: 8 },
8: { [BuildingType.ResearchLab]: 10, [TechnologyType.DarkMatterTechnology]: 5, [BuildingType.NaniteFactory]: 2 }
}
},
[BuildingType.Terraformer]: {
id: BuildingType.Terraformer,
name: '地形改造器',
description: '改造行星地形每级增加5个可用空间',
baseCost: { metal: 0, crystal: 50000, deuterium: 100000, darkMatter: 0, energy: 0 },
baseTime: 60,
costMultiplier: 2,
spaceUsage: 5,
planetOnly: true,
requirements: {
[BuildingType.ResearchLab]: 10,
[BuildingType.RoboticsFactory]: 8,
[TechnologyType.TerraformingTechnology]: 1
},
levelRequirements: {
5: { [BuildingType.ResearchLab]: 12, [TechnologyType.TerraformingTechnology]: 3, [BuildingType.NaniteFactory]: 1 },
8: { [BuildingType.ResearchLab]: 14, [TechnologyType.TerraformingTechnology]: 5, [BuildingType.NaniteFactory]: 3 }
}
},
// 月球专属建筑
[BuildingType.LunarBase]: {
@@ -125,7 +225,11 @@ export const BUILDINGS: Record<BuildingType, BuildingConfig> = {
baseTime: 45, // 减少建造时间60→45秒
costMultiplier: 2,
spaceUsage: 0, // 月球基地本身不占用空间,反而增加空间
moonOnly: true
moonOnly: true,
levelRequirements: {
5: { [BuildingType.RoboticsFactory]: 5 },
8: { [BuildingType.RoboticsFactory]: 8, [BuildingType.NaniteFactory]: 1 }
}
},
[BuildingType.SensorPhalanx]: {
id: BuildingType.SensorPhalanx,
@@ -134,8 +238,13 @@ export const BUILDINGS: Record<BuildingType, BuildingConfig> = {
baseCost: { metal: 20000, crystal: 40000, deuterium: 20000, darkMatter: 0, energy: 0 },
baseTime: 60, // 减少建造时间90→60秒
costMultiplier: 2,
spaceUsage: 10,
moonOnly: true
spaceUsage: 6,
moonOnly: true,
requirements: { [BuildingType.LunarBase]: 1 },
levelRequirements: {
5: { [BuildingType.LunarBase]: 5, [TechnologyType.ComputerTechnology]: 5 },
8: { [BuildingType.LunarBase]: 8, [TechnologyType.ComputerTechnology]: 8, [BuildingType.NaniteFactory]: 2 }
}
},
[BuildingType.JumpGate]: {
id: BuildingType.JumpGate,
@@ -144,8 +253,42 @@ export const BUILDINGS: Record<BuildingType, BuildingConfig> = {
baseCost: { metal: 2000000, crystal: 4000000, deuterium: 2000000, darkMatter: 0, energy: 0 },
baseTime: 240, // 减少建造时间300→240秒
costMultiplier: 2,
spaceUsage: 20,
moonOnly: true
spaceUsage: 10,
moonOnly: true,
maxLevel: 5, // 最多5级
requirements: {
[BuildingType.LunarBase]: 1,
[TechnologyType.HyperspaceTechnology]: 7
},
levelRequirements: {
3: { [BuildingType.LunarBase]: 5, [TechnologyType.HyperspaceTechnology]: 10, [BuildingType.NaniteFactory]: 3 }
}
},
// 特殊建筑
[BuildingType.PlanetDestroyerFactory]: {
id: BuildingType.PlanetDestroyerFactory,
name: '行星毁灭者工厂',
description: '建造能够摧毁行星的终极武器',
baseCost: { metal: 5000000, crystal: 4000000, deuterium: 1000000, darkMatter: 0, energy: 0 },
baseTime: 300,
costMultiplier: 2,
spaceUsage: 15,
planetOnly: true,
maxLevel: 3, // 最多3级
requirements: {
[BuildingType.Shipyard]: 12,
[BuildingType.RoboticsFactory]: 10,
[BuildingType.NaniteFactory]: 5,
[TechnologyType.PlanetDestructionTech]: 1
},
levelRequirements: {
3: {
[BuildingType.Shipyard]: 14,
[BuildingType.NaniteFactory]: 8,
[TechnologyType.PlanetDestructionTech]: 3,
[TechnologyType.HyperspaceTechnology]: 10
}
}
}
}
@@ -158,7 +301,12 @@ export const TECHNOLOGIES: Record<TechnologyType, TechnologyConfig> = {
baseCost: { metal: 0, crystal: 800, deuterium: 400, darkMatter: 0, energy: 0 },
baseTime: 30, // 减少研究时间60→30秒
costMultiplier: 2,
requirements: { [BuildingType.ResearchLab]: 1 }
requirements: { [BuildingType.ResearchLab]: 1 },
levelRequirements: {
5: { [BuildingType.ResearchLab]: 3, [BuildingType.SolarPlant]: 10 },
8: { [BuildingType.ResearchLab]: 5, [BuildingType.SolarPlant]: 15, [BuildingType.RoboticsFactory]: 3 },
12: { [BuildingType.ResearchLab]: 8, [BuildingType.RoboticsFactory]: 6, [BuildingType.NaniteFactory]: 1 }
}
},
[TechnologyType.LaserTechnology]: {
id: TechnologyType.LaserTechnology,
@@ -167,7 +315,11 @@ export const TECHNOLOGIES: Record<TechnologyType, TechnologyConfig> = {
baseCost: { metal: 200, crystal: 100, deuterium: 0, darkMatter: 0, energy: 0 },
baseTime: 60,
costMultiplier: 2,
requirements: { [BuildingType.ResearchLab]: 1, [TechnologyType.EnergyTechnology]: 2 }
requirements: { [BuildingType.ResearchLab]: 1, [TechnologyType.EnergyTechnology]: 2 },
levelRequirements: {
6: { [BuildingType.ResearchLab]: 5, [TechnologyType.EnergyTechnology]: 5, [BuildingType.Shipyard]: 3 },
10: { [BuildingType.ResearchLab]: 8, [TechnologyType.EnergyTechnology]: 8, [BuildingType.Shipyard]: 6 }
}
},
[TechnologyType.IonTechnology]: {
id: TechnologyType.IonTechnology,
@@ -176,7 +328,11 @@ export const TECHNOLOGIES: Record<TechnologyType, TechnologyConfig> = {
baseCost: { metal: 1000, crystal: 300, deuterium: 100, darkMatter: 0, energy: 0 },
baseTime: 60,
costMultiplier: 2,
requirements: { [BuildingType.ResearchLab]: 4, [TechnologyType.LaserTechnology]: 5, [TechnologyType.EnergyTechnology]: 4 }
requirements: { [BuildingType.ResearchLab]: 4, [TechnologyType.LaserTechnology]: 5, [TechnologyType.EnergyTechnology]: 4 },
levelRequirements: {
5: { [BuildingType.ResearchLab]: 8, [TechnologyType.LaserTechnology]: 10, [TechnologyType.EnergyTechnology]: 8 },
8: { [BuildingType.ResearchLab]: 10, [TechnologyType.LaserTechnology]: 12, [BuildingType.NaniteFactory]: 2 }
}
},
[TechnologyType.HyperspaceTechnology]: {
id: TechnologyType.HyperspaceTechnology,
@@ -185,7 +341,11 @@ export const TECHNOLOGIES: Record<TechnologyType, TechnologyConfig> = {
baseCost: { metal: 0, crystal: 4000, deuterium: 2000, darkMatter: 0, energy: 0 },
baseTime: 60,
costMultiplier: 2,
requirements: { [BuildingType.ResearchLab]: 7, [TechnologyType.EnergyTechnology]: 5 }
requirements: { [BuildingType.ResearchLab]: 7, [TechnologyType.EnergyTechnology]: 5 },
levelRequirements: {
5: { [BuildingType.ResearchLab]: 10, [TechnologyType.EnergyTechnology]: 8, [BuildingType.Shipyard]: 5 },
8: { [BuildingType.ResearchLab]: 12, [TechnologyType.EnergyTechnology]: 10, [BuildingType.NaniteFactory]: 2 }
}
},
[TechnologyType.PlasmaTechnology]: {
id: TechnologyType.PlasmaTechnology,
@@ -199,6 +359,20 @@ export const TECHNOLOGIES: Record<TechnologyType, TechnologyConfig> = {
[TechnologyType.EnergyTechnology]: 8,
[TechnologyType.LaserTechnology]: 10,
[TechnologyType.IonTechnology]: 5
},
levelRequirements: {
5: {
[BuildingType.ResearchLab]: 10,
[TechnologyType.EnergyTechnology]: 12,
[TechnologyType.IonTechnology]: 8,
[BuildingType.NaniteFactory]: 1
},
8: {
[BuildingType.ResearchLab]: 12,
[TechnologyType.EnergyTechnology]: 15,
[TechnologyType.IonTechnology]: 10,
[BuildingType.NaniteFactory]: 3
}
}
},
[TechnologyType.ComputerTechnology]: {
@@ -208,7 +382,14 @@ export const TECHNOLOGIES: Record<TechnologyType, TechnologyConfig> = {
baseCost: { metal: 0, crystal: 400, deuterium: 600, darkMatter: 0, energy: 0 },
baseTime: 60,
costMultiplier: 2,
requirements: { [BuildingType.ResearchLab]: 1 }
fleetStorageBonus: 500, // 每级全局增加50舰队仓储
maxLevel: 10, // 最多10级最多11个研究队列
requirements: { [BuildingType.ResearchLab]: 1 },
levelRequirements: {
3: { [BuildingType.ResearchLab]: 5 },
5: { [BuildingType.ResearchLab]: 8, [BuildingType.RoboticsFactory]: 5 },
8: { [BuildingType.ResearchLab]: 10, [BuildingType.NaniteFactory]: 2 }
}
},
[TechnologyType.CombustionDrive]: {
id: TechnologyType.CombustionDrive,
@@ -217,7 +398,11 @@ export const TECHNOLOGIES: Record<TechnologyType, TechnologyConfig> = {
baseCost: { metal: 400, crystal: 0, deuterium: 600, darkMatter: 0, energy: 0 },
baseTime: 60,
costMultiplier: 2,
requirements: { [BuildingType.ResearchLab]: 1, [TechnologyType.EnergyTechnology]: 1 }
requirements: { [BuildingType.ResearchLab]: 1, [TechnologyType.EnergyTechnology]: 1 },
levelRequirements: {
5: { [BuildingType.ResearchLab]: 5, [TechnologyType.EnergyTechnology]: 3, [BuildingType.Shipyard]: 2 },
8: { [BuildingType.ResearchLab]: 8, [TechnologyType.EnergyTechnology]: 5, [BuildingType.Shipyard]: 5 }
}
},
[TechnologyType.ImpulseDrive]: {
id: TechnologyType.ImpulseDrive,
@@ -226,7 +411,11 @@ export const TECHNOLOGIES: Record<TechnologyType, TechnologyConfig> = {
baseCost: { metal: 2000, crystal: 4000, deuterium: 600, darkMatter: 0, energy: 0 },
baseTime: 60,
costMultiplier: 2,
requirements: { [BuildingType.ResearchLab]: 2, [TechnologyType.EnergyTechnology]: 1 }
requirements: { [BuildingType.ResearchLab]: 2, [TechnologyType.EnergyTechnology]: 1 },
levelRequirements: {
5: { [BuildingType.ResearchLab]: 6, [TechnologyType.EnergyTechnology]: 4, [BuildingType.Shipyard]: 3 },
8: { [BuildingType.ResearchLab]: 8, [TechnologyType.EnergyTechnology]: 6, [BuildingType.Shipyard]: 6 }
}
},
[TechnologyType.HyperspaceDrive]: {
id: TechnologyType.HyperspaceDrive,
@@ -235,7 +424,11 @@ export const TECHNOLOGIES: Record<TechnologyType, TechnologyConfig> = {
baseCost: { metal: 10000, crystal: 20000, deuterium: 6000, darkMatter: 0, energy: 0 },
baseTime: 60,
costMultiplier: 2,
requirements: { [BuildingType.ResearchLab]: 7, [TechnologyType.HyperspaceTechnology]: 3 }
requirements: { [BuildingType.ResearchLab]: 7, [TechnologyType.HyperspaceTechnology]: 3 },
levelRequirements: {
5: { [BuildingType.ResearchLab]: 10, [TechnologyType.HyperspaceTechnology]: 6, [BuildingType.Shipyard]: 8 },
8: { [BuildingType.ResearchLab]: 12, [TechnologyType.HyperspaceTechnology]: 8, [BuildingType.NaniteFactory]: 3 }
}
},
[TechnologyType.DarkMatterTechnology]: {
id: TechnologyType.DarkMatterTechnology,
@@ -244,7 +437,58 @@ export const TECHNOLOGIES: Record<TechnologyType, TechnologyConfig> = {
baseCost: { metal: 100000, crystal: 200000, deuterium: 100000, darkMatter: 0, energy: 0 },
baseTime: 180,
costMultiplier: 2,
requirements: { [BuildingType.ResearchLab]: 8, [TechnologyType.HyperspaceTechnology]: 5 }
requirements: { [BuildingType.ResearchLab]: 8, [TechnologyType.HyperspaceTechnology]: 5 },
levelRequirements: {
3: {
[BuildingType.ResearchLab]: 10,
[TechnologyType.HyperspaceTechnology]: 8,
[BuildingType.RoboticsFactory]: 10,
[TechnologyType.EnergyTechnology]: 10
},
5: {
[BuildingType.ResearchLab]: 12,
[TechnologyType.HyperspaceTechnology]: 10,
[BuildingType.NaniteFactory]: 2,
[TechnologyType.EnergyTechnology]: 12
}
}
},
[TechnologyType.TerraformingTechnology]: {
id: TechnologyType.TerraformingTechnology,
name: '地形改造技术',
description: '研究行星地形改造技术每级为所有行星增加5个可用空间',
baseCost: { metal: 0, crystal: 20000, deuterium: 40000, darkMatter: 0, energy: 0 },
baseTime: 90,
costMultiplier: 2,
requirements: { [BuildingType.ResearchLab]: 8, [TechnologyType.EnergyTechnology]: 6 },
levelRequirements: {
5: { [BuildingType.ResearchLab]: 12, [TechnologyType.EnergyTechnology]: 10, [BuildingType.RoboticsFactory]: 10 },
8: { [BuildingType.ResearchLab]: 14, [TechnologyType.EnergyTechnology]: 12, [BuildingType.NaniteFactory]: 3 }
}
},
[TechnologyType.PlanetDestructionTech]: {
id: TechnologyType.PlanetDestructionTech,
name: '行星毁灭技术',
description: '研究如何摧毁整个行星的恐怖技术',
baseCost: { metal: 4000000, crystal: 8000000, deuterium: 4000000, darkMatter: 0, energy: 0 },
baseTime: 300,
costMultiplier: 2,
maxLevel: 5, // 最多5级
requirements: {
[BuildingType.ResearchLab]: 12,
[TechnologyType.HyperspaceTechnology]: 8,
[TechnologyType.HyperspaceDrive]: 6,
[TechnologyType.PlasmaTechnology]: 7
},
levelRequirements: {
3: {
[BuildingType.ResearchLab]: 14,
[TechnologyType.HyperspaceTechnology]: 12,
[TechnologyType.HyperspaceDrive]: 10,
[TechnologyType.PlasmaTechnology]: 10,
[BuildingType.NaniteFactory]: 5
}
}
}
}
@@ -262,6 +506,7 @@ export const SHIPS: Record<ShipType, ShipConfig> = {
armor: 400,
speed: 12500,
fuelConsumption: 20,
storageUsage: 5,
requirements: { [BuildingType.Shipyard]: 1, [TechnologyType.CombustionDrive]: 1 }
},
[ShipType.HeavyFighter]: {
@@ -276,6 +521,7 @@ export const SHIPS: Record<ShipType, ShipConfig> = {
armor: 1000,
speed: 10000,
fuelConsumption: 75,
storageUsage: 10,
requirements: { [BuildingType.Shipyard]: 3, [TechnologyType.ImpulseDrive]: 2 }
},
[ShipType.Cruiser]: {
@@ -290,6 +536,7 @@ export const SHIPS: Record<ShipType, ShipConfig> = {
armor: 2700,
speed: 15000,
fuelConsumption: 300,
storageUsage: 15,
requirements: { [BuildingType.Shipyard]: 5, [TechnologyType.ImpulseDrive]: 4, [TechnologyType.IonTechnology]: 2 }
},
[ShipType.Battleship]: {
@@ -304,6 +551,7 @@ export const SHIPS: Record<ShipType, ShipConfig> = {
armor: 6000,
speed: 10000,
fuelConsumption: 500,
storageUsage: 25,
requirements: { [BuildingType.Shipyard]: 7, [TechnologyType.HyperspaceDrive]: 4 }
},
[ShipType.SmallCargo]: {
@@ -318,6 +566,7 @@ export const SHIPS: Record<ShipType, ShipConfig> = {
armor: 400,
speed: 5000,
fuelConsumption: 10,
storageUsage: 10,
requirements: { [BuildingType.Shipyard]: 2, [TechnologyType.CombustionDrive]: 2 }
},
[ShipType.LargeCargo]: {
@@ -332,6 +581,7 @@ export const SHIPS: Record<ShipType, ShipConfig> = {
armor: 1200,
speed: 7500,
fuelConsumption: 50,
storageUsage: 20,
requirements: { [BuildingType.Shipyard]: 4, [TechnologyType.CombustionDrive]: 6 }
},
[ShipType.ColonyShip]: {
@@ -346,6 +596,7 @@ export const SHIPS: Record<ShipType, ShipConfig> = {
armor: 3000,
speed: 2500,
fuelConsumption: 1000,
storageUsage: 40,
requirements: { [BuildingType.Shipyard]: 4, [TechnologyType.ImpulseDrive]: 3 }
},
[ShipType.Recycler]: {
@@ -360,6 +611,7 @@ export const SHIPS: Record<ShipType, ShipConfig> = {
armor: 1600,
speed: 2000,
fuelConsumption: 300,
storageUsage: 30,
requirements: { [BuildingType.Shipyard]: 4, [TechnologyType.CombustionDrive]: 6 }
},
[ShipType.EspionageProbe]: {
@@ -374,6 +626,7 @@ export const SHIPS: Record<ShipType, ShipConfig> = {
armor: 100,
speed: 100000000,
fuelConsumption: 1,
storageUsage: 2,
requirements: { [BuildingType.Shipyard]: 3, [TechnologyType.CombustionDrive]: 3 }
},
[ShipType.DarkMatterHarvester]: {
@@ -388,11 +641,31 @@ export const SHIPS: Record<ShipType, ShipConfig> = {
armor: 2000,
speed: 5000,
fuelConsumption: 500,
storageUsage: 50,
requirements: {
[BuildingType.Shipyard]: 8,
[TechnologyType.HyperspaceDrive]: 5,
[TechnologyType.DarkMatterTechnology]: 1
}
},
[ShipType.Deathstar]: {
id: ShipType.Deathstar,
name: '死星',
description: '终极武器,能够摧毁整个行星',
cost: { metal: 5000000, crystal: 4000000, deuterium: 1000000, darkMatter: 0, energy: 0 },
buildTime: 600,
cargoCapacity: 1000000,
attack: 200000,
shield: 50000,
armor: 900000,
speed: 100,
fuelConsumption: 1,
storageUsage: 100,
requirements: {
[BuildingType.PlanetDestroyerFactory]: 10,
[TechnologyType.PlanetDestructionTech]: 7,
[TechnologyType.HyperspaceDrive]: 7
}
}
}
@@ -485,6 +758,21 @@ export const DEFENSES: Record<DefenseType, DefenseConfig> = {
shield: 10000,
armor: 10000,
requirements: { [BuildingType.Shipyard]: 6, [TechnologyType.EnergyTechnology]: 6 }
},
[DefenseType.PlanetaryShield]: {
id: DefenseType.PlanetaryShield,
name: '行星护盾',
description: '保护行星免受毁灭攻击的超级护盾',
cost: { metal: 2000000, crystal: 2000000, deuterium: 1000000, darkMatter: 0, energy: 0 },
buildTime: 180,
attack: 1,
shield: 100000,
armor: 100000,
requirements: {
[BuildingType.Shipyard]: 10,
[TechnologyType.EnergyTechnology]: 10,
[TechnologyType.HyperspaceTechnology]: 8
}
}
}
@@ -565,6 +853,20 @@ export const MOON_CONFIG = {
baseChance: 1, // 基础1%概率
maxChance: 20, // 最大20%概率
chancePerDebris: 100000, // 每10万资源增加1%概率
baseSize: 100, // 月球基础空间
lunarBaseSpaceBonus: 3 // 每级月球基地增加的空间
baseSize: 60, // 月球基础空间
lunarBaseSpaceBonus: 5 // 每级月球基地增加的空间
}
// 行星配置
export const PLANET_CONFIG = {
baseSize: 200, // 行星基础空间
terraformerSpaceBonus: 5, // 每级地形改造器增加的空间
terraformingTechSpaceBonus: 3 // 每级地形改造技术增加的空间
}
// 舰队仓储配置
export const FLEET_STORAGE_CONFIG = {
baseStorage: 1000, // 基础舰队仓储
shipyardBonus: 1000, // 每级造船厂增加的仓储
computerTechBonus: 500 // 每级计算机技术全局增加的仓储
}

View File

@@ -32,11 +32,13 @@ export default {
locked: 'Gesperrt',
viewRequirements: 'Anforderungen anzeigen',
requirementsNotMet: 'Anforderungen nicht erfüllt',
current: 'Aktuell'
current: 'Aktuell',
level: 'Stufe'
},
errors: {
requirementsNotMet: 'Anforderungen nicht erfüllt',
insufficientResources: 'Unzureichende Ressourcen',
insufficientFleetStorage: 'Unzureichender Flottenspeicher',
shieldDomeLimit: 'Schildkuppel-Limit erreicht',
fleetMissionsFull: 'Flottenmissionsplätze voll',
insufficientFleet: 'Unzureichende Flotte',
@@ -61,7 +63,8 @@ export default {
simulator: 'Simulator',
galaxy: 'Galaxie',
messages: 'Nachrichten',
settings: 'Einstellungen'
settings: 'Einstellungen',
gm: 'GM'
},
sidebar: {
language: 'Sprache',
@@ -77,10 +80,14 @@ export default {
darkMatter: 'Dunkle Materie',
energy: 'Energie',
production: 'Produktion',
consumption: 'Verbrauch',
capacity: 'Kapazität',
current: 'Aktuell',
max: 'Max. Kapazität',
perHour: 'Stunde'
perHour: 'Stunde',
perMinute: 'Min',
hour: 'Stunde',
noEnergy: 'Keine Energie'
},
planet: {
planet: 'Planet',
@@ -113,9 +120,11 @@ export default {
crystalStorage: 'Kristallspeicher',
deuteriumTank: 'Deuteriumtank',
darkMatterCollector: 'Dunkle-Materie-Kollektor',
terraformer: 'Terraformer',
lunarBase: 'Mondbasis',
sensorPhalanx: 'Sensorphalanx',
jumpGate: 'Sprungtor',
planetDestroyerFactory: 'Planetenzerstörer-Fabrik',
buildTime: 'Bauzeit',
production: 'Produktion',
consumption: 'Verbrauch',
@@ -129,16 +138,18 @@ export default {
deuteriumSynthesizer: 'Synthesiert Deuteriumressourcen',
solarPlant: 'Liefert Energie',
roboticsFactory: 'Beschleunigt Baugeschwindigkeit',
naniteFactory: 'Erhöht Bauauftragskapazität, +1 pro Stufe (max 10)',
naniteFactory: 'Erhöht Bauauftragskapazität, +1 pro Stufe (max 10 Stufen)',
shipyard: 'Baut Schiffe',
researchLab: 'Erforscht Technologien',
metalStorage: 'Erhöht Metallspeicherkapazität',
crystalStorage: 'Erhöht Kristallspeicherkapazität',
deuteriumTank: 'Erhöht Deuteriumspeicherkapazität',
darkMatterCollector: 'Sammelt seltene Dunkle-Materie-Ressourcen',
lunarBase: 'Erhöht verfügbaren Platz auf dem Mond',
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',
sensorPhalanx: 'Erkennt Flottenaktivitäten in umliegenden Systemen',
jumpGate: 'Überträgt Flotten sofort zu anderen Monden'
jumpGate: 'Überträgt Flotten sofort zu anderen Monden',
planetDestroyerFactory: 'Konstruiert ultimative Waffen zur Zerstörung von Planeten'
},
ships: {
lightFighter: 'Leichter Jäger',
@@ -150,7 +161,8 @@ export default {
colonyShip: 'Kolonieschiff',
recycler: 'Recycler',
espionageProbe: 'Spionagesonde',
darkMatterHarvester: 'Dunkle-Materie-Ernter'
darkMatterHarvester: 'Dunkle-Materie-Ernter',
deathstar: 'Todesstern'
},
shipDescriptions: {
lightFighter: 'Grundlegende Kampfeinheit',
@@ -162,7 +174,8 @@ export default {
colonyShip: 'Zur Kolonisierung neuer Planeten',
recycler: 'Sammelt Trümmerfeld-Ressourcen',
espionageProbe: 'Späht feindliche Planeten aus',
darkMatterHarvester: 'Spezielles Schiff zum Ernten von Dunkler Materie'
darkMatterHarvester: 'Spezielles Schiff zum Ernten von Dunkler Materie',
deathstar: 'Ultimative Waffe, die ganze Planeten zerstören kann'
},
defenses: {
rocketLauncher: 'Raketenwerfer',
@@ -172,7 +185,8 @@ export default {
ionCannon: 'Ionengeschütz',
plasmaTurret: 'Plasmawerfer',
smallShieldDome: 'Kleine Schildkuppel',
largeShieldDome: 'Große Schildkuppel'
largeShieldDome: 'Große Schildkuppel',
planetaryShield: 'Planetarschild'
},
defenseDescriptions: {
rocketLauncher: 'Grundlegende Verteidigungsanlage',
@@ -182,7 +196,8 @@ export default {
ionCannon: 'Effektiv gegen Schilde',
plasmaTurret: 'Mächtige Verteidigungsanlage',
smallShieldDome: 'Kleiner Schild zum Schutz des gesamten Planeten',
largeShieldDome: 'Großer Schild zum Schutz des gesamten Planeten'
largeShieldDome: 'Großer Schild zum Schutz des gesamten Planeten',
planetaryShield: 'Superschild zum Schutz des Planeten vor Vernichtungsangriffen'
},
research: {
researchTime: 'Forschungszeit',
@@ -200,7 +215,9 @@ export default {
combustionDrive: 'Verbrennungsantrieb',
impulseDrive: 'Impulsantrieb',
hyperspaceDrive: 'Hyperraumantrieb',
darkMatterTechnology: 'Dunkle-Materie-Technologie'
darkMatterTechnology: 'Dunkle-Materie-Technologie',
terraformingTechnology: 'Terraforming-Technologie',
planetDestructionTech: 'Planetenzerstörungstechnologie'
},
technologyDescriptions: {
energyTechnology: 'Verbessert Energieeffizienz',
@@ -208,11 +225,13 @@ export default {
ionTechnology: 'Ionenwaffentechnologie',
hyperspaceTechnology: 'Hyperraumsprung-Technologie',
plasmaTechnology: 'Plasmawaffentechnologie',
computerTechnology: 'Erhöht Forschungsauftragskapazität, +1 pro Stufe (max 10)',
computerTechnology: 'Erhöht Forschungsauftragskapazität, +1 pro Stufe (max 10 Stufen)',
combustionDrive: 'Grundlegende Antriebstechnologie',
impulseDrive: 'Mittlere Antriebstechnologie',
hyperspaceDrive: 'Fortgeschrittene Antriebstechnologie',
darkMatterTechnology: 'Forschung zu Eigenschaften und Anwendungen von Dunkler Materie'
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',
planetDestructionTech: 'Schreckliche Technologie zur Zerstörung ganzer Planeten'
},
officers: {
commander: 'Kommandant',
@@ -220,7 +239,10 @@ export default {
engineer: 'Ingenieur',
geologist: 'Geologe',
technocrat: 'Technokrat',
darkMatterSpecialist: 'Dunkle-Materie-Spezialist'
darkMatterSpecialist: 'Dunkle-Materie-Spezialist',
resourceBonus: 'Ressourcenproduktionsbonus',
darkMatterBonus: 'Dunkle-Materie-Produktionsbonus',
energyBonus: 'Energieproduktionsbonus'
},
officerDescriptions: {
commander: 'Verbessert Baugeschwindigkeit und Management',
@@ -247,7 +269,14 @@ export default {
title: 'Planetenübersicht',
resourceOverview: 'Ressourcen',
fleetInfo: 'Flotte',
currentShips: 'Schiffe auf diesem Planeten'
currentShips: 'Schiffe auf diesem Planeten',
productionSources: 'Produktionsquellen',
productionSourcesDesc: 'Detaillierte Ressourcenproduktion und Bonusinformationen',
consumptionSources: 'Verbrauchsquellen',
consumptionSourcesDesc: 'Energieverbrauchsdetails für Gebäude',
totalProduction: 'Gesamtproduktion',
totalConsumption: 'Gesamtverbrauch',
noConsumption: 'Kein Energieverbrauch'
},
buildingsView: {
title: 'Gebäude',
@@ -257,6 +286,8 @@ export default {
upgradeCost: 'Ausbaukosten',
buildTime: 'Bauzeit',
upgrade: 'Ausbauen',
maxLevelReached: 'Maximale Stufe erreicht',
requirementsNotMet: 'Anforderungen nicht erfüllt',
upgradeFailed: 'Ausbau fehlgeschlagen',
upgradeFailedMessage: 'Bitte überprüfen Sie, ob Sie genügend Ressourcen, Platz oder keine anderen Bauaufträge haben.',
demolish: 'Abreißen',
@@ -268,6 +299,7 @@ export default {
title: 'Forschung',
researchCost: 'Forschungskosten',
research: 'Forschen',
maxLevelReached: 'Maximale Stufe erreicht',
researchFailed: 'Forschung fehlgeschlagen',
researchFailedMessage:
'Bitte überprüfen Sie, ob Sie genügend Ressourcen haben, die Voraussetzungen erfüllt sind oder keine anderen Forschungsaufträge vorhanden sind.'
@@ -289,6 +321,7 @@ export default {
},
shipyardView: {
title: 'Raumschiffwerft',
fleetStorage: 'Flottenspeicher',
attack: 'Angriff',
shield: 'Schild',
speed: 'Geschwindigkeit',
@@ -360,6 +393,7 @@ export default {
colonize: 'Kolonisieren',
spy: 'Spionage',
deploy: 'Stationieren',
recycle: 'Recyceln',
transportResources: 'Ressourcen transportieren',
totalCargoCapacity: 'Gesamtladekapazität',
used: 'Verwendet',
@@ -376,7 +410,12 @@ export default {
recallFailed: 'Zurückrufen fehlgeschlagen',
recallFailedMessage: 'Diese Mission kann nicht zurückgerufen werden.',
unknownPlanet: 'Unbekannter Planet',
fleetMissionSlots: 'Flottenmissionsplätze'
fleetMissionSlots: 'Flottenmissionsplätze',
noShipsSelected: 'Keine Schiffe ausgewählt',
cannotSendToOwnPlanet: 'Flotte kann nicht zu eigenem Planeten gesendet werden',
cargoExceedsCapacity: 'Fracht überschreitet Kapazität',
noColonyShip: 'Kolonieschiff für Kolonisierungsmission erforderlich',
noDebrisAtTarget: 'Kein Trümmerfeld an Zielkoordinaten oder Trümmerfeld ist leer'
},
officersView: {
title: 'Offiziere',
@@ -429,18 +468,25 @@ export default {
attack: 'Angriff',
colonize: 'Kolonisieren',
switch: 'Wechseln',
recycle: 'Recyceln',
debrisField: 'Trümmerfeld',
scoutPlanetTitle: 'Planet ausspionieren',
attackPlanetTitle: 'Planet angreifen',
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.',
colonizePlanetMessage:
'Möchten Sie wirklich Position [{coordinates}] kolonisieren?\n\nBitte gehen Sie zur Flottenseite, um ein Kolonieschiff zu senden.'
'Möchten Sie wirklich Position [{coordinates}] kolonisieren?\n\nBitte gehen Sie zur Flottenseite, um ein Kolonieschiff zu senden.',
recyclePlanetMessage:
'Möchten Sie wirklich Trümmer bei Position [{coordinates}] recyceln?\n\nBitte gehen Sie zur Flottenseite, um Recycler zu senden.'
},
messagesView: {
title: 'Nachrichten',
battles: 'Kämpfe',
spy: 'Spionage',
battleReports: 'Kampfberichte',
spyReports: 'Spionageberichte',
noBattleReports: 'Keine Kampfberichte',
@@ -461,7 +507,17 @@ export default {
resources: 'Ressourcen',
fleet: 'Flotte',
defense: 'Verteidigung',
buildings: 'Gebäude'
buildings: 'Gebäude',
unread: 'Ungelesen',
targetPlanet: 'Zielplanet',
attackerRemaining: 'Angreifer verblieben',
defenderRemaining: 'Verteidiger verblieben',
moonChance: 'Mondchance',
showRoundDetails: 'Rundendetails anzeigen',
hideRoundDetails: 'Rundendetails ausblenden',
round: 'Runde {round}',
attackerRemainingPower: 'Verbleibende Angreiferkraft',
defenderRemainingPower: 'Verbleibende Verteidigerkraft'
},
simulatorView: {
title: 'Kampfsimulator',
@@ -523,6 +579,12 @@ export default {
clearConfirmMessage: 'Alle Spieldaten werden gelöscht und von vorne begonnen. Diese Aktion kann nicht rückgängig gemacht werden. Fortfahren?',
gameSettings: 'Spieleinstellungen',
gameSettingsDesc: 'Spielparameter und Einstellungen anpassen',
gamePause: 'Spielpause',
gamePauseDesc: 'Spielzeit und Ressourcenproduktion pausieren oder fortsetzen',
pause: 'Pausieren',
resume: 'Fortsetzen',
gamePaused: 'Spiel pausiert',
gameResumed: 'Spiel fortgesetzt',
playerName: 'Spielername',
gameSpeed: 'Spielgeschwindigkeit',
gameSpeedDesc: 'Aktueller Spielgeschwindigkeitsmultiplikator',
@@ -532,5 +594,34 @@ export default {
community: 'Community',
github: 'GitHub-Repository',
qqGroup: 'QQ-Gruppe'
},
gmView: {
title: 'GM-Kontrollpanel',
adminOnly: 'Nur Admin',
selectPlanet: 'Planet auswählen',
choosePlanet: 'Einen Planeten auswählen',
resources: 'Ressourcen',
buildings: 'Gebäude',
research: 'Forschung',
ships: 'Schiffe',
defense: 'Verteidigung',
officers: 'Offiziere',
modifyResources: 'Ressourcen ändern',
resourcesDesc: 'Planetenressourcen schnell ändern',
modifyBuildings: 'Gebäude ändern',
buildingsDesc: 'Gebäudelevel schnell festlegen',
modifyResearch: 'Forschung ändern',
researchDesc: 'Forschungslevel schnell festlegen',
modifyShips: 'Schiffe ändern',
shipsDesc: 'Schiffsanzahl schnell festlegen',
modifyDefense: 'Verteidigung ändern',
defenseDesc: 'Verteidigungsanzahl schnell festlegen',
modifyOfficers: 'Offiziere ändern',
officersDesc: 'Offiziersablaufzeit schnell festlegen',
days: 'T',
dangerZone: 'Gefahrenzone',
dangerZoneDesc: 'Die folgenden Vorgänge sind irreversibel',
resetGame: 'Spiel zurücksetzen',
resetGameConfirm: 'Möchten Sie das Spiel wirklich zurücksetzen? Alle Daten werden gelöscht!'
}
}

View File

@@ -32,11 +32,13 @@ export default {
locked: 'Locked',
viewRequirements: 'View Requirements',
requirementsNotMet: 'Requirements Not Met',
current: 'Current'
current: 'Current',
level: 'Level'
},
errors: {
requirementsNotMet: 'Requirements not met',
insufficientResources: 'Insufficient resources',
insufficientFleetStorage: 'Insufficient fleet storage',
shieldDomeLimit: 'Shield dome limit reached',
fleetMissionsFull: 'Fleet mission slots full',
insufficientFleet: 'Insufficient fleet',
@@ -61,7 +63,8 @@ export default {
simulator: 'Simulator',
galaxy: 'Galaxy',
messages: 'Messages',
settings: 'Settings'
settings: 'Settings',
gm: 'GM'
},
sidebar: {
language: 'Language',
@@ -77,10 +80,14 @@ export default {
darkMatter: 'Dark Matter',
energy: 'Energy',
production: 'Production',
consumption: 'Consumption',
capacity: 'Capacity',
current: 'Current',
max: 'Max Capacity',
perHour: 'hour'
perHour: 'hour',
perMinute: 'min',
hour: 'hour',
noEnergy: 'No Energy'
},
planet: {
planet: 'Planet',
@@ -113,9 +120,11 @@ export default {
crystalStorage: 'Crystal Storage',
deuteriumTank: 'Deuterium Tank',
darkMatterCollector: 'Dark Matter Collector',
terraformer: 'Terraformer',
lunarBase: 'Lunar Base',
sensorPhalanx: 'Sensor Phalanx',
jumpGate: 'Jump Gate',
planetDestroyerFactory: 'Planet Destroyer Factory',
buildTime: 'Build Time',
production: 'Production',
consumption: 'Consumption',
@@ -129,16 +138,18 @@ export default {
deuteriumSynthesizer: 'Synthesizes deuterium resources',
solarPlant: 'Provides energy',
roboticsFactory: 'Accelerates construction speed',
naniteFactory: 'Increases build queue capacity, +1 per level (max 10)',
naniteFactory: 'Increases build queue capacity, +1 per level (max 10 levels)',
shipyard: 'Constructs ships',
researchLab: 'Researches technologies',
metalStorage: 'Increases metal storage capacity',
crystalStorage: 'Increases crystal storage capacity',
deuteriumTank: 'Increases deuterium storage capacity',
darkMatterCollector: 'Collects rare dark matter resources',
lunarBase: 'Increases available space on the moon',
terraformer: 'Terraforms planet surface, adds 5 available space per level',
lunarBase: 'Increases available space on the moon, +5 space per level',
sensorPhalanx: 'Detects fleet activities in surrounding systems',
jumpGate: 'Instantly transfers fleets to other moons'
jumpGate: 'Instantly transfers fleets to other moons',
planetDestroyerFactory: 'Constructs ultimate weapons capable of destroying planets'
},
ships: {
lightFighter: 'Light Fighter',
@@ -150,7 +161,8 @@ export default {
colonyShip: 'Colony Ship',
recycler: 'Recycler',
espionageProbe: 'Espionage Probe',
darkMatterHarvester: 'Dark Matter Harvester'
darkMatterHarvester: 'Dark Matter Harvester',
deathstar: 'Deathstar'
},
shipDescriptions: {
lightFighter: 'Basic combat unit',
@@ -162,7 +174,8 @@ export default {
colonyShip: 'Used to colonize new planets',
recycler: 'Collects debris field resources',
espionageProbe: 'Scouts enemy planets',
darkMatterHarvester: 'Special ship for harvesting dark matter'
darkMatterHarvester: 'Special ship for harvesting dark matter',
deathstar: 'Ultimate weapon capable of destroying entire planets'
},
defenses: {
rocketLauncher: 'Rocket Launcher',
@@ -172,7 +185,8 @@ export default {
ionCannon: 'Ion Cannon',
plasmaTurret: 'Plasma Turret',
smallShieldDome: 'Small Shield Dome',
largeShieldDome: 'Large Shield Dome'
largeShieldDome: 'Large Shield Dome',
planetaryShield: 'Planetary Shield'
},
defenseDescriptions: {
rocketLauncher: 'Basic defense facility',
@@ -182,7 +196,8 @@ export default {
ionCannon: 'Effective against shields',
plasmaTurret: 'Powerful defense facility',
smallShieldDome: 'Small shield protecting the entire planet',
largeShieldDome: 'Large shield protecting the entire planet'
largeShieldDome: 'Large shield protecting the entire planet',
planetaryShield: 'Super shield protecting planet from destruction attacks'
},
research: {
researchTime: 'Research Time',
@@ -200,7 +215,9 @@ export default {
combustionDrive: 'Combustion Drive',
impulseDrive: 'Impulse Drive',
hyperspaceDrive: 'Hyperspace Drive',
darkMatterTechnology: 'Dark Matter Technology'
darkMatterTechnology: 'Dark Matter Technology',
terraformingTechnology: 'Terraforming Technology',
planetDestructionTech: 'Planet Destruction Technology'
},
technologyDescriptions: {
energyTechnology: 'Improves energy efficiency',
@@ -208,11 +225,13 @@ export default {
ionTechnology: 'Ion weapon technology',
hyperspaceTechnology: 'Hyperspace jump technology',
plasmaTechnology: 'Plasma weapon technology',
computerTechnology: 'Increases research queue capacity, +1 per level (max 10)',
computerTechnology: 'Increases research queue capacity, +1 per level (max 10 levels)',
combustionDrive: 'Basic propulsion technology',
impulseDrive: 'Intermediate propulsion technology',
hyperspaceDrive: 'Advanced propulsion technology',
darkMatterTechnology: 'Research into dark matter properties and applications'
darkMatterTechnology: 'Research into dark matter properties and applications',
terraformingTechnology: 'Research planet terraforming technology, adds 3 available space to all planets per level',
planetDestructionTech: 'Terrifying technology for destroying entire planets'
},
officers: {
commander: 'Commander',
@@ -220,7 +239,10 @@ export default {
engineer: 'Engineer',
geologist: 'Geologist',
technocrat: 'Technocrat',
darkMatterSpecialist: 'Dark Matter Specialist'
darkMatterSpecialist: 'Dark Matter Specialist',
resourceBonus: 'Resource Production Bonus',
darkMatterBonus: 'Dark Matter Production Bonus',
energyBonus: 'Energy Production Bonus'
},
officerDescriptions: {
commander: 'Improves building speed and management',
@@ -241,13 +263,21 @@ export default {
cancelResearch: 'Cancel Research',
confirmCancel: 'Are you sure you want to cancel? 50% of resources will be refunded.',
level: 'Level',
quantity: 'Quantity',
upgradeToLevel: 'Upgrade to Level'
},
overview: {
title: 'Planet Overview',
resourceOverview: 'Resources',
fleetInfo: 'Fleet',
currentShips: 'Ships on this planet'
currentShips: 'Ships on this planet',
productionSources: 'Production Sources',
productionSourcesDesc: 'Detailed resource production and bonus information',
consumptionSources: 'Consumption Sources',
consumptionSourcesDesc: 'Energy consumption details for buildings',
totalProduction: 'Total Production',
totalConsumption: 'Total Consumption',
noConsumption: 'No energy consumption'
},
buildingsView: {
title: 'Buildings',
@@ -257,6 +287,8 @@ export default {
upgradeCost: 'Upgrade Cost',
buildTime: 'Build Time',
upgrade: 'Upgrade',
maxLevelReached: 'Max Level Reached',
requirementsNotMet: 'Requirements Not Met',
upgradeFailed: 'Upgrade Failed',
upgradeFailedMessage: 'Please check if you have enough resources, space, or if there are other build tasks.',
demolish: 'Demolish',
@@ -268,6 +300,7 @@ export default {
title: 'Research',
researchCost: 'Research Cost',
research: 'Research',
maxLevelReached: 'Max Level Reached',
researchFailed: 'Research Failed',
researchFailedMessage: 'Please check if you have enough resources, prerequisites are met, or if there are other research tasks.'
},
@@ -288,6 +321,7 @@ export default {
},
shipyardView: {
title: 'Shipyard',
fleetStorage: 'Fleet Storage',
attack: 'Attack',
shield: 'Shield',
speed: 'Speed',
@@ -358,6 +392,8 @@ export default {
colonize: 'Colonize',
spy: 'Spy',
deploy: 'Deploy',
recycle: 'Recycle',
destroy: 'Planet Destruction',
transportResources: 'Transport Resources',
totalCargoCapacity: 'Total Cargo Capacity',
used: 'Used',
@@ -374,7 +410,13 @@ export default {
recallFailed: 'Recall Failed',
recallFailedMessage: 'This mission cannot be recalled.',
unknownPlanet: 'Unknown Planet',
fleetMissionSlots: 'Fleet Mission Slots'
fleetMissionSlots: 'Fleet Mission Slots',
noShipsSelected: 'No ships selected',
cannotSendToOwnPlanet: 'Cannot send fleet to your own planet',
cargoExceedsCapacity: 'Cargo exceeds capacity',
noColonyShip: 'Colony ship required for colonization mission',
noDebrisAtTarget: 'No debris field at target coordinates or debris field is empty',
noDeathstar: 'Deathstar required for destruction mission'
},
officersView: {
title: 'Officers',
@@ -427,17 +469,23 @@ export default {
attack: 'Attack',
colonize: 'Colonize',
switch: 'Switch',
recycle: 'Recycle',
debrisField: 'Debris Field',
scoutPlanetTitle: 'Scout Planet',
attackPlanetTitle: 'Attack Planet',
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.',
colonizePlanetMessage:
'Are you sure you want to colonize position [{coordinates}]?\n\nPlease go to the fleet page to send a colony ship.'
'Are you sure you want to colonize position [{coordinates}]?\n\nPlease go to the fleet page to send a colony ship.',
recyclePlanetMessage: 'Are you sure you want to recycle debris at position [{coordinates}]?\n\nPlease go to the fleet page to send recycler ships.'
},
messagesView: {
title: 'Messages',
battles: 'Battles',
spy: 'Spy',
battleReports: 'Battle Reports',
spyReports: 'Spy Reports',
noBattleReports: 'No battle reports',
@@ -458,7 +506,9 @@ export default {
resources: 'Resources',
fleet: 'Fleet',
defense: 'Defense',
buildings: 'Buildings'
buildings: 'Buildings',
unread: 'Unread',
targetPlanet: 'Target Planet'
},
simulatorView: {
title: 'Battle Simulator',
@@ -520,6 +570,12 @@ export default {
clearConfirmMessage: 'This will delete all game data and start over. This action cannot be undone. Continue?',
gameSettings: 'Game Settings',
gameSettingsDesc: 'Adjust game parameters and preferences',
gamePause: 'Game Pause',
gamePauseDesc: 'Pause or resume game time and resource production',
pause: 'Pause',
resume: 'Resume',
gamePaused: 'Game paused',
gameResumed: 'Game resumed',
playerName: 'Player Name',
gameSpeed: 'Game Speed',
gameSpeedDesc: 'Current game speed multiplier',
@@ -529,5 +585,34 @@ export default {
community: 'Community',
github: 'GitHub Repository',
qqGroup: 'QQ Group'
},
gmView: {
title: 'GM Control Panel',
adminOnly: 'Admin Only',
selectPlanet: 'Select Planet',
choosePlanet: 'Choose a planet',
resources: 'Resources',
buildings: 'Buildings',
research: 'Research',
ships: 'Ships',
defense: 'Defense',
officers: 'Officers',
modifyResources: 'Modify Resources',
resourcesDesc: 'Quickly modify planet resources',
modifyBuildings: 'Modify Buildings',
buildingsDesc: 'Quickly set building levels',
modifyResearch: 'Modify Research',
researchDesc: 'Quickly set research levels',
modifyShips: 'Modify Ships',
shipsDesc: 'Quickly set ship counts',
modifyDefense: 'Modify Defense',
defenseDesc: 'Quickly set defense counts',
modifyOfficers: 'Modify Officers',
officersDesc: 'Quickly set officer expiration time',
days: 'd',
dangerZone: 'Danger Zone',
dangerZoneDesc: 'The following operations are irreversible',
resetGame: 'Reset Game',
resetGameConfirm: 'Are you sure you want to reset the game? This will delete all data!'
}
}

View File

@@ -32,11 +32,13 @@ export default {
locked: 'ロック済み',
viewRequirements: '必要条件を表示',
requirementsNotMet: '必要条件が満たされていません',
current: '現在'
current: '現在',
level: 'レベル'
},
errors: {
requirementsNotMet: '前提条件を満たしていません',
insufficientResources: '資源が不足しています',
insufficientFleetStorage: '艦隊ストレージが不足しています',
shieldDomeLimit: 'シールドドームの上限に達しました',
fleetMissionsFull: '艦隊ミッションスロットが満杯です',
insufficientFleet: '艦隊が不足しています',
@@ -61,7 +63,8 @@ export default {
simulator: 'シミュレーター',
galaxy: '銀河',
messages: 'メッセージ',
settings: '設定'
settings: '設定',
gm: 'GM'
},
sidebar: {
language: '言語',
@@ -77,10 +80,14 @@ export default {
darkMatter: 'ダークマター',
energy: 'エネルギー',
production: '生産量',
consumption: '消費量',
capacity: '容量',
current: '現在の貯蔵量',
max: '最大容量',
perHour: '時間'
perHour: '時間',
perMinute: '分',
hour: '時間',
noEnergy: 'エネルギー不足'
},
planet: {
planet: '惑星',
@@ -113,9 +120,11 @@ export default {
crystalStorage: 'クリスタル倉庫',
deuteriumTank: '重水素タンク',
darkMatterCollector: 'ダークマター採取装置',
terraformer: 'テラフォーマー',
lunarBase: '月面基地',
sensorPhalanx: 'センサーファランクス',
jumpGate: 'ジャンプゲート',
planetDestroyerFactory: '惑星破壊工場',
buildTime: '建設時間',
production: '生産量',
consumption: '消費',
@@ -129,16 +138,18 @@ export default {
deuteriumSynthesizer: '重水素資源を合成',
solarPlant: 'エネルギーを供給',
roboticsFactory: '建設速度を向上',
naniteFactory: '建設キュー数を増加、レベル毎に+1最大10',
naniteFactory: '建設キュー数を増加、レベル毎に+1最大10レベル',
shipyard: '艦船を建造',
researchLab: '技術を研究',
metalStorage: '金属の貯蔵上限を増加',
crystalStorage: 'クリスタルの貯蔵上限を増加',
deuteriumTank: '重水素の貯蔵上限を増加',
darkMatterCollector: '希少なダークマター資源を収集',
lunarBase: '月の利用可能スペース増加',
terraformer: '惑星地形を改造、レベル毎に利用可能スペース5増加',
lunarBase: '月の利用可能スペースを増加、レベル毎に+5スペース',
sensorPhalanx: '周辺星系の艦隊活動を探知',
jumpGate: '他の月へ艦隊を瞬間移動'
jumpGate: '他の月へ艦隊を瞬間移動',
planetDestroyerFactory: '惑星を破壊できる究極兵器を建造'
},
ships: {
lightFighter: '軽戦闘機',
@@ -150,7 +161,8 @@ export default {
colonyShip: 'コロニーシップ',
recycler: 'リサイクラー',
espionageProbe: 'スパイプローブ',
darkMatterHarvester: 'ダークマター採取船'
darkMatterHarvester: 'ダークマター採取船',
deathstar: 'デススター'
},
shipDescriptions: {
lightFighter: '基本戦闘ユニット',
@@ -162,7 +174,8 @@ export default {
colonyShip: '新惑星の植民に使用',
recycler: 'デブリフィールドの資源を回収',
espionageProbe: '敵惑星を偵察',
darkMatterHarvester: 'ダークマター採取専用の特殊艦'
darkMatterHarvester: 'ダークマター採取専用の特殊艦',
deathstar: '惑星全体を破壊できる究極兵器'
},
defenses: {
rocketLauncher: 'ロケットランチャー',
@@ -172,7 +185,8 @@ export default {
ionCannon: 'イオンキャノン',
plasmaTurret: 'プラズマタレット',
smallShieldDome: '小型シールドドーム',
largeShieldDome: '大型シールドドーム'
largeShieldDome: '大型シールドドーム',
planetaryShield: '惑星シールド'
},
defenseDescriptions: {
rocketLauncher: '基本防衛施設',
@@ -182,7 +196,8 @@ export default {
ionCannon: 'シールド破壊に効果的',
plasmaTurret: '強力な防衛施設',
smallShieldDome: '惑星全体を保護する小型シールド',
largeShieldDome: '惑星全体を保護する大型シールド'
largeShieldDome: '惑星全体を保護する大型シールド',
planetaryShield: '破壊攻撃から惑星を保護する超級シールド'
},
research: {
researchTime: '研究時間',
@@ -200,7 +215,9 @@ export default {
combustionDrive: '燃焼ドライブ',
impulseDrive: 'インパルスドライブ',
hyperspaceDrive: 'ハイパースペースドライブ',
darkMatterTechnology: 'ダークマター技術'
darkMatterTechnology: 'ダークマター技術',
terraformingTechnology: 'テラフォーミング技術',
planetDestructionTech: '惑星破壊技術'
},
technologyDescriptions: {
energyTechnology: 'エネルギー利用効率を向上',
@@ -208,11 +225,13 @@ export default {
ionTechnology: 'イオン兵器技術',
hyperspaceTechnology: 'ハイパースペースジャンプ技術',
plasmaTechnology: 'プラズマ兵器技術',
computerTechnology: '研究キュー数を増加、レベル毎に+1最大10',
computerTechnology: '研究キュー数を増加、レベル毎に+1最大10レベル',
combustionDrive: '基本推進技術',
impulseDrive: '中級推進技術',
hyperspaceDrive: '高級推進技術',
darkMatterTechnology: 'ダークマターの性質と応用を研究'
darkMatterTechnology: 'ダークマターの性質と応用を研究',
terraformingTechnology: '惑星地形改造技術を研究、レベル毎に全惑星の利用可能スペース3増加',
planetDestructionTech: '惑星全体を破壊する恐怖の技術を研究'
},
officers: {
commander: '司令官',
@@ -220,7 +239,10 @@ export default {
engineer: 'エンジニア',
geologist: '地質学者',
technocrat: '技術専門家',
darkMatterSpecialist: 'ダークマター専門家'
darkMatterSpecialist: 'ダークマター専門家',
resourceBonus: '資源生産ボーナス',
darkMatterBonus: 'ダークマター生産ボーナス',
energyBonus: 'エネルギー生産ボーナス'
},
officerDescriptions: {
commander: '建設速度と管理能力を向上',
@@ -262,7 +284,14 @@ export default {
title: '惑星概要',
resourceOverview: '資源概要',
fleetInfo: '艦隊',
currentShips: '現在の惑星の艦船数'
currentShips: '現在の惑星の艦船数',
productionSources: '生産源',
productionSourcesDesc: '詳細な資源生産とボーナス情報',
consumptionSources: '消費源',
consumptionSourcesDesc: '建物のエネルギー消費詳細',
totalProduction: '総生産量',
totalConsumption: '総消費量',
noConsumption: 'エネルギー消費なし'
},
buildingsView: {
title: '建物',
@@ -272,6 +301,8 @@ export default {
upgradeCost: 'アップグレードコスト',
buildTime: '建設時間',
upgrade: 'アップグレード',
maxLevelReached: '最大レベルに達しました',
requirementsNotMet: '要件が満たされていません',
upgradeFailed: 'アップグレード失敗',
upgradeFailedMessage: '資源が十分か、スペースが十分か、または他の建設タスクがないか確認してください。',
demolish: '解体',
@@ -283,6 +314,7 @@ export default {
title: '研究',
researchCost: '研究コスト',
research: '研究',
maxLevelReached: '最大レベルに達しました',
researchFailed: '研究失敗',
researchFailedMessage: '資源が十分か、前提条件が満たされているか、または他の研究タスクがないか確認してください。'
},
@@ -300,6 +332,7 @@ export default {
},
shipyardView: {
title: '造船所',
fleetStorage: '艦隊ストレージ',
attack: '攻撃力',
shield: 'シールド',
speed: '速度',
@@ -358,6 +391,7 @@ export default {
colonize: '植民',
spy: '偵察',
deploy: '配備',
recycle: '回収',
transportResources: '資源輸送',
totalCargoCapacity: '総積載量',
used: '使用済み',
@@ -374,7 +408,12 @@ export default {
recallFailed: '召還失敗',
recallFailedMessage: 'このミッションは召還できません。',
unknownPlanet: '未知の惑星',
fleetMissionSlots: '艦隊ミッションスロット'
fleetMissionSlots: '艦隊ミッションスロット',
noShipsSelected: '艦船が選択されていません',
cannotSendToOwnPlanet: '自分の惑星に艦隊を派遣できません',
cargoExceedsCapacity: '積載量が容量を超えています',
noColonyShip: '植民ミッションにはコロニーシップが必要です',
noDebrisAtTarget: '目標座標にデブリフィールドがないか、デブリフィールドが空です'
},
officersView: {
title: '士官',
@@ -427,15 +466,21 @@ export default {
attack: '攻撃',
colonize: '植民',
switch: '切り替え',
recycle: '回収',
debrisField: 'デブリフィールド',
scoutPlanetTitle: '惑星偵察',
attackPlanetTitle: '惑星攻撃',
colonizePlanetTitle: '惑星植民',
recyclePlanetTitle: 'デブリ回収',
scoutPlanetMessage: '惑星[{coordinates}]にスパイプローブを送りますか?\n\n艦隊ページに移動して艦船を選択して派遣してください。',
attackPlanetMessage: '惑星[{coordinates}]を攻撃しますか?\n\n艦隊ページに移動して艦船を選択して派遣してください。',
colonizePlanetMessage: '位置[{coordinates}]を植民しますか?\n\n艦隊ページに移動してコロニーシップを派遣してください。'
colonizePlanetMessage: '位置[{coordinates}]を植民しますか?\n\n艦隊ページに移動してコロニーシップを派遣してください。',
recyclePlanetMessage: '位置[{coordinates}]のデブリを回収しますか?\n\n艦隊ページに移動してリサイクラーを派遣してください。'
},
messagesView: {
title: 'メッセージセンター',
battles: '戦闘',
spy: 'スパイ',
battleReports: '戦闘レポート',
spyReports: 'スパイレポート',
noBattleReports: '戦闘レポートなし',
@@ -456,7 +501,17 @@ export default {
resources: '資源',
fleet: '艦隊',
defense: '防衛',
buildings: '建物'
buildings: '建物',
unread: '未読',
targetPlanet: '目標惑星',
attackerRemaining: '攻撃側残存',
defenderRemaining: '防御側残存',
moonChance: '月生成確率',
showRoundDetails: 'ラウンド詳細表示',
hideRoundDetails: 'ラウンド詳細非表示',
round: '第{round}ラウンド',
attackerRemainingPower: '攻撃側残存火力',
defenderRemainingPower: '防御側残存火力'
},
simulatorView: {
title: '戦闘シミュレーター',
@@ -518,6 +573,12 @@ export default {
clearConfirmMessage: 'すべてのゲームデータが削除され、最初からやり直します。この操作は元に戻せません。続行しますか?',
gameSettings: 'ゲーム設定',
gameSettingsDesc: 'ゲームパラメータと設定を調整',
gamePause: 'ゲーム一時停止',
gamePauseDesc: 'ゲーム時間と資源生産を一時停止または再開',
pause: '一時停止',
resume: '再開',
gamePaused: 'ゲームを一時停止しました',
gameResumed: 'ゲームを再開しました',
playerName: 'プレイヤー名',
gameSpeed: 'ゲーム速度',
gameSpeedDesc: '現在のゲーム速度倍率',
@@ -527,5 +588,34 @@ export default {
community: 'コミュニティ',
github: 'GitHubリポジトリ',
qqGroup: 'QQグループ'
},
gmView: {
title: 'GMコントロールパネル',
adminOnly: '管理者専用',
selectPlanet: '惑星を選択',
choosePlanet: '惑星を選択してください',
resources: '資源',
buildings: '建物',
research: '研究',
ships: '艦船',
defense: '防衛',
officers: '士官',
modifyResources: '資源を変更',
resourcesDesc: '惑星の資源を素早く変更',
modifyBuildings: '建物を変更',
buildingsDesc: '建物レベルを素早く設定',
modifyResearch: '研究を変更',
researchDesc: '研究レベルを素早く設定',
modifyShips: '艦船を変更',
shipsDesc: '艦船数を素早く設定',
modifyDefense: '防衛を変更',
defenseDesc: '防衛数を素早く設定',
modifyOfficers: '士官を変更',
officersDesc: '士官の有効期限を素早く設定',
days: '日',
dangerZone: '危険ゾーン',
dangerZoneDesc: '以下の操作は元に戻せません',
resetGame: 'ゲームをリセット',
resetGameConfirm: 'ゲームをリセットしてもよろしいですか?すべてのデータが削除されます!'
}
}

View File

@@ -32,11 +32,13 @@ export default {
locked: '잠김',
viewRequirements: '요구사항 보기',
requirementsNotMet: '요구사항 미충족',
current: '현재'
current: '현재',
level: '레벨'
},
errors: {
requirementsNotMet: '전제 조건 미충족',
insufficientResources: '자원 부족',
insufficientFleetStorage: '함대 저장소 부족',
shieldDomeLimit: '실드 돔 한도 도달',
fleetMissionsFull: '함대 임무 슬롯 가득 참',
insufficientFleet: '함대 부족',
@@ -61,7 +63,8 @@ export default {
simulator: '시뮬레이터',
galaxy: '은하계',
messages: '메시지',
settings: '설정'
settings: '설정',
gm: 'GM'
},
sidebar: {
language: '언어',
@@ -77,10 +80,14 @@ export default {
darkMatter: '암흑 물질',
energy: '에너지',
production: '생산량',
consumption: '소비량',
capacity: '용량',
current: '현재 저장량',
max: '최대 용량',
perHour: '시간'
perHour: '시간',
perMinute: '분',
hour: '시간',
noEnergy: '에너지 부족'
},
planet: {
planet: '행성',
@@ -113,9 +120,11 @@ export default {
crystalStorage: '크리스탈 창고',
deuteriumTank: '중수소 탱크',
darkMatterCollector: '암흑 물질 수집기',
terraformer: '지형 변환기',
lunarBase: '달 기지',
sensorPhalanx: '센서 팔랑크스',
jumpGate: '점프 게이트',
planetDestroyerFactory: '행성 파괴 공장',
buildTime: '건설 시간',
production: '생산량',
consumption: '소비',
@@ -129,16 +138,18 @@ export default {
deuteriumSynthesizer: '중수소 자원 합성',
solarPlant: '에너지 제공',
roboticsFactory: '건설 속도 향상',
naniteFactory: '건설 대기열 수 증가, 레벨당 +1 (최대 10)',
naniteFactory: '건설 대기열 수 증가, 레벨당 +1 (최대 10레벨)',
shipyard: '함선 건조',
researchLab: '기술 연구',
metalStorage: '금속 저장 용량 증가',
crystalStorage: '크리스탈 저장 용량 증가',
deuteriumTank: '중수소 저장 용량 증가',
darkMatterCollector: '희귀한 암흑 물질 자원 수집',
lunarBase: '달 가용 공간 증가',
terraformer: '행성 지형 개조, 레벨당 가용 공간 5 증가',
lunarBase: '달 가용 공간 증가, 레벨당 +5 공간',
sensorPhalanx: '주변 행성계의 함대 활동 감지',
jumpGate: '다른 위성으로 함대 순간 이동'
jumpGate: '다른 위성으로 함대 순간 이동',
planetDestroyerFactory: '행성을 파괴할 수 있는 궁극 병기 건조'
},
ships: {
lightFighter: '경전투기',
@@ -150,7 +161,8 @@ export default {
colonyShip: '식민선',
recycler: '재활용선',
espionageProbe: '정찰기',
darkMatterHarvester: '암흑 물질 채취선'
darkMatterHarvester: '암흑 물질 채취선',
deathstar: '데스스타'
},
shipDescriptions: {
lightFighter: '기본 전투 유닛',
@@ -162,7 +174,8 @@ export default {
colonyShip: '새로운 행성 식민에 사용',
recycler: '잔해장 자원 수집',
espionageProbe: '적 행성 정찰',
darkMatterHarvester: '암흑 물질 채취 전용 특수 함선'
darkMatterHarvester: '암흑 물질 채취 전용 특수 함선',
deathstar: '행성 전체를 파괴할 수 있는 궁극 병기'
},
defenses: {
rocketLauncher: '로켓 발사대',
@@ -172,7 +185,8 @@ export default {
ionCannon: '이온 캐논',
plasmaTurret: '플라즈마 포탑',
smallShieldDome: '소형 실드 돔',
largeShieldDome: '대형 실드 돔'
largeShieldDome: '대형 실드 돔',
planetaryShield: '행성 실드'
},
defenseDescriptions: {
rocketLauncher: '기본 방어 시설',
@@ -182,7 +196,8 @@ export default {
ionCannon: '실드 파괴의 이기',
plasmaTurret: '강력한 방어 시설',
smallShieldDome: '행성 전체를 보호하는 소형 실드',
largeShieldDome: '행성 전체를 보호하는 대형 실드'
largeShieldDome: '행성 전체를 보호하는 대형 실드',
planetaryShield: '파괴 공격으로부터 행성을 보호하는 초급 실드'
},
research: {
researchTime: '연구 시간',
@@ -200,7 +215,9 @@ export default {
combustionDrive: '연소 엔진',
impulseDrive: '임펄스 엔진',
hyperspaceDrive: '초공간 엔진',
darkMatterTechnology: '암흑 물질 기술'
darkMatterTechnology: '암흑 물질 기술',
terraformingTechnology: '지형 변환 기술',
planetDestructionTech: '행성 파괴 기술'
},
technologyDescriptions: {
energyTechnology: '에너지 이용 효율 향상',
@@ -208,11 +225,13 @@ export default {
ionTechnology: '이온 무기 기술',
hyperspaceTechnology: '초공간 점프 기술',
plasmaTechnology: '플라즈마 무기 기술',
computerTechnology: '연구 대기열 수 증가, 레벨당 +1 (최대 10)',
computerTechnology: '연구 대기열 수 증가, 레벨당 +1 (최대 10레벨)',
combustionDrive: '기본 추진 기술',
impulseDrive: '중급 추진 기술',
hyperspaceDrive: '고급 추진 기술',
darkMatterTechnology: '암흑 물질의 성질과 응용 연구'
darkMatterTechnology: '암흑 물질의 성질과 응용 연구',
terraformingTechnology: '행성 지형 개조 기술 연구, 레벨당 모든 행성의 가용 공간 3 증가',
planetDestructionTech: '행성 전체를 파괴하는 공포의 기술 연구'
},
officers: {
commander: '사령관',
@@ -220,7 +239,10 @@ export default {
engineer: '엔지니어',
geologist: '지질학자',
technocrat: '기술 전문가',
darkMatterSpecialist: '암흑 물질 전문가'
darkMatterSpecialist: '암흑 물질 전문가',
resourceBonus: '자원 생산 보너스',
darkMatterBonus: '암흑 물질 생산 보너스',
energyBonus: '에너지 생산 보너스'
},
officerDescriptions: {
commander: '건설 속도 및 관리 능력 향상',
@@ -247,7 +269,14 @@ export default {
title: '행성 개요',
resourceOverview: '자원 개요',
fleetInfo: '함대',
currentShips: '현재 행성의 함선 수'
currentShips: '현재 행성의 함선 수',
productionSources: '생산 소스',
productionSourcesDesc: '상세 자원 생산 및 보너스 정보',
consumptionSources: '소비 소스',
consumptionSourcesDesc: '건물 에너지 소비 세부 정보',
totalProduction: '총 생산량',
totalConsumption: '총 소비량',
noConsumption: '에너지 소비 없음'
},
buildingsView: {
title: '건물',
@@ -257,6 +286,8 @@ export default {
upgradeCost: '업그레이드 비용',
buildTime: '건설 시간',
upgrade: '업그레이드',
maxLevelReached: '최대 레벨 도달',
requirementsNotMet: '요구 사항 미충족',
upgradeFailed: '업그레이드 실패',
upgradeFailedMessage: '자원이 충분한지, 공간이 충분한지, 또는 다른 건설 작업이 있는지 확인하세요.',
demolish: '철거',
@@ -268,6 +299,7 @@ export default {
title: '연구',
researchCost: '연구 비용',
research: '연구',
maxLevelReached: '최대 레벨 도달',
researchFailed: '연구 실패',
researchFailedMessage: '자원이 충분한지, 전제 조건이 충족되었는지, 또는 다른 연구 작업이 있는지 확인하세요.'
},
@@ -288,6 +320,7 @@ export default {
},
shipyardView: {
title: '조선소',
fleetStorage: '함대 저장소',
attack: '공격력',
shield: '실드',
speed: '속도',
@@ -358,6 +391,7 @@ export default {
colonize: '식민',
spy: '정찰',
deploy: '배치',
recycle: '회수',
transportResources: '자원 수송',
totalCargoCapacity: '총 적재량',
used: '사용됨',
@@ -374,7 +408,12 @@ export default {
recallFailed: '소환 실패',
recallFailedMessage: '이 임무는 소환할 수 없습니다.',
unknownPlanet: '알 수 없는 행성',
fleetMissionSlots: '함대 임무 슬롯'
fleetMissionSlots: '함대 임무 슬롯',
noShipsSelected: '선택된 함선 없음',
cannotSendToOwnPlanet: '자신의 행성으로 함대를 파견할 수 없습니다',
cargoExceedsCapacity: '적재량이 용량을 초과합니다',
noColonyShip: '식민 임무를 위해 식민선이 필요합니다',
noDebrisAtTarget: '대상 좌표에 잔해장이 없거나 잔해장이 비어 있습니다'
},
officersView: {
title: '장교',
@@ -427,16 +466,22 @@ export default {
attack: '공격',
colonize: '식민',
switch: '전환',
recycle: '회수',
debrisField: '잔해 필드',
scoutPlanetTitle: '행성 정찰',
attackPlanetTitle: '행성 공격',
colonizePlanetTitle: '행성 식민',
recyclePlanetTitle: '잔해 회수',
scoutPlanetMessage:
'행성 [{coordinates}]을(를) 정찰하기 위해 정찰기를 보내시겠습니까?\n\n함대 페이지로 이동하여 함선을 선택하고 파견하세요.',
attackPlanetMessage: '행성 [{coordinates}]을(를) 공격하시겠습니까?\n\n함대 페이지로 이동하여 함선을 선택하고 파견하세요.',
colonizePlanetMessage: '위치 [{coordinates}]을(를) 식민하시겠습니까?\n\n함대 페이지로 이동하여 식민선을 파견하세요.'
colonizePlanetMessage: '위치 [{coordinates}]을(를) 식민하시겠습니까?\n\n함대 페이지로 이동하여 식민선을 파견하세요.',
recyclePlanetMessage: '위치 [{coordinates}]의 잔해를 회수하시겠습니까?\n\n함대 페이지로 이동하여 회수선을 파견하세요.'
},
messagesView: {
title: '메시지 센터',
battles: '전투',
spy: '정찰',
battleReports: '전투 보고서',
spyReports: '정찰 보고서',
noBattleReports: '전투 보고서 없음',
@@ -457,7 +502,17 @@ export default {
resources: '자원',
fleet: '함대',
defense: '방어',
buildings: '건물'
buildings: '건물',
unread: '읽지 않음',
targetPlanet: '목표 행성',
attackerRemaining: '공격자 잔여',
defenderRemaining: '방어자 잔여',
moonChance: '위성 생성 확률',
showRoundDetails: '라운드 상세 표시',
hideRoundDetails: '라운드 상세 숨기기',
round: '제{round}라운드',
attackerRemainingPower: '공격자 잔여 화력',
defenderRemainingPower: '방어자 잔여 화력'
},
simulatorView: {
title: '전투 시뮬레이터',
@@ -519,6 +574,12 @@ export default {
clearConfirmMessage: '모든 게임 데이터가 삭제되고 처음부터 시작됩니다. 이 작업은 되돌릴 수 없습니다. 계속하시겠습니까?',
gameSettings: '게임 설정',
gameSettingsDesc: '게임 매개변수 및 설정 조정',
gamePause: '게임 일시정지',
gamePauseDesc: '게임 시간 및 자원 생산 일시정지 또는 재개',
pause: '일시정지',
resume: '재개',
gamePaused: '게임이 일시정지되었습니다',
gameResumed: '게임이 재개되었습니다',
playerName: '플레이어 이름',
gameSpeed: '게임 속도',
gameSpeedDesc: '현재 게임 속도 배율',
@@ -528,5 +589,34 @@ export default {
community: '커뮤니티',
github: 'GitHub 저장소',
qqGroup: 'QQ 그룹'
},
gmView: {
title: 'GM 제어판',
adminOnly: '관리자 전용',
selectPlanet: '행성 선택',
choosePlanet: '행성을 선택하세요',
resources: '자원',
buildings: '건물',
research: '연구',
ships: '함선',
defense: '방어',
officers: '장교',
modifyResources: '자원 수정',
resourcesDesc: '행성 자원을 빠르게 수정',
modifyBuildings: '건물 수정',
buildingsDesc: '건물 레벨을 빠르게 설정',
modifyResearch: '연구 수정',
researchDesc: '연구 레벨을 빠르게 설정',
modifyShips: '함선 수정',
shipsDesc: '함선 수량을 빠르게 설정',
modifyDefense: '방어 수정',
defenseDesc: '방어 수량을 빠르게 설정',
modifyOfficers: '장교 수정',
officersDesc: '장교 만료 시간을 빠르게 설정',
days: '일',
dangerZone: '위험 구역',
dangerZoneDesc: '다음 작업은 되돌릴 수 없습니다',
resetGame: '게임 초기화',
resetGameConfirm: '게임을 초기화하시겠습니까? 모든 데이터가 삭제됩니다!'
}
}

View File

@@ -32,11 +32,13 @@ export default {
locked: 'Заблокировано',
viewRequirements: 'Просмотр требований',
requirementsNotMet: 'Требования не выполнены',
current: 'Текущий'
current: 'Текущий',
level: 'Уровень'
},
errors: {
requirementsNotMet: 'Требования не выполнены',
insufficientResources: 'Недостаточно ресурсов',
insufficientFleetStorage: 'Недостаточно места для флота',
shieldDomeLimit: 'Достигнут лимит щитовых куполов',
fleetMissionsFull: 'Слоты миссий флота заполнены',
insufficientFleet: 'Недостаточно флота',
@@ -61,7 +63,8 @@ export default {
simulator: 'Симулятор',
galaxy: 'Галактика',
messages: 'Сообщения',
settings: 'Настройки'
settings: 'Настройки',
gm: 'GM'
},
sidebar: {
language: 'Язык',
@@ -77,10 +80,14 @@ export default {
darkMatter: 'Тёмная материя',
energy: 'Энергия',
production: 'Производство',
consumption: 'Потребление',
capacity: 'Вместимость',
current: 'Текущий',
max: 'Макс. вместимость',
perHour: 'час'
perHour: 'час',
perMinute: 'мин',
hour: 'час',
noEnergy: 'Нет энергии'
},
planet: {
planet: 'Планета',
@@ -113,9 +120,11 @@ export default {
crystalStorage: 'Хранилище кристалла',
deuteriumTank: 'Цистерна дейтерия',
darkMatterCollector: 'Коллектор тёмной материи',
terraformer: 'Терраформер',
lunarBase: 'Лунная база',
sensorPhalanx: 'Сенсорная фаланга',
jumpGate: 'Прыжковые ворота',
planetDestroyerFactory: 'Фабрика разрушителей планет',
buildTime: 'Время строительства',
production: 'Производство',
consumption: 'Потребление',
@@ -129,16 +138,18 @@ export default {
deuteriumSynthesizer: 'Синтезирует дейтериевые ресурсы',
solarPlant: 'Обеспечивает энергией',
roboticsFactory: 'Ускоряет скорость строительства',
naniteFactory: 'Увеличивает вместимость очереди строительства, +1 за уровень (макс 10)',
naniteFactory: 'Увеличивает вместимость очереди строительства, +1 за уровень (макс 10 уровней)',
shipyard: 'Строит корабли',
researchLab: 'Исследует технологии',
metalStorage: 'Увеличивает ёмкость хранилища металла',
crystalStorage: 'Увеличивает ёмкость хранилища кристалла',
deuteriumTank: 'Увеличивает ёмкость хранилища дейтерия',
darkMatterCollector: 'Собирает редкие ресурсы тёмной материи',
lunarBase: 'Увеличивает доступное пространство на луне',
terraformer: 'Терраформирует поверхность планеты, увеличивает доступное пространство на 5 за уровень',
lunarBase: 'Увеличивает доступное пространство на луне, +5 пространства за уровень',
sensorPhalanx: 'Обнаруживает активность флота в окружающих системах',
jumpGate: 'Мгновенно переносит флоты на другие луны'
jumpGate: 'Мгновенно переносит флоты на другие луны',
planetDestroyerFactory: 'Производит абсолютное оружие, способное уничтожать планеты'
},
ships: {
lightFighter: 'Лёгкий истребитель',
@@ -150,7 +161,8 @@ export default {
colonyShip: 'Колонизатор',
recycler: 'Переработчик',
espionageProbe: 'Шпионский зонд',
darkMatterHarvester: 'Сборщик тёмной материи'
darkMatterHarvester: 'Сборщик тёмной материи',
deathstar: 'Звезда Смерти'
},
shipDescriptions: {
lightFighter: 'Базовая боевая единица',
@@ -162,7 +174,8 @@ export default {
colonyShip: 'Используется для колонизации новых планет',
recycler: 'Собирает ресурсы с поля обломков',
espionageProbe: 'Разведывает вражеские планеты',
darkMatterHarvester: 'Специальный корабль для сбора тёмной материи'
darkMatterHarvester: 'Специальный корабль для сбора тёмной материи',
deathstar: 'Абсолютное оружие, способное уничтожать целые планеты'
},
defenses: {
rocketLauncher: 'Ракетная установка',
@@ -172,7 +185,8 @@ export default {
ionCannon: 'Ионное орудие',
plasmaTurret: 'Плазменная турель',
smallShieldDome: 'Малый щитовой купол',
largeShieldDome: 'Большой щитовой купол'
largeShieldDome: 'Большой щитовой купол',
planetaryShield: 'Планетарный щит'
},
defenseDescriptions: {
rocketLauncher: 'Базовое оборонительное сооружение',
@@ -182,7 +196,8 @@ export default {
ionCannon: 'Эффективно против щитов',
plasmaTurret: 'Мощное оборонительное сооружение',
smallShieldDome: 'Малый щит, защищающий всю планету',
largeShieldDome: 'Большой щит, защищающий всю планету'
largeShieldDome: 'Большой щит, защищающий всю планету',
planetaryShield: 'Суперщит, защищающий планету от атак уничтожения'
},
research: {
researchTime: 'Время исследования',
@@ -200,7 +215,9 @@ export default {
combustionDrive: 'Реактивный двигатель',
impulseDrive: 'Импульсный двигатель',
hyperspaceDrive: 'Гиперпространственный двигатель',
darkMatterTechnology: 'Технология тёмной материи'
darkMatterTechnology: 'Технология тёмной материи',
terraformingTechnology: 'Технология терраформирования',
planetDestructionTech: 'Технология уничтожения планет'
},
technologyDescriptions: {
energyTechnology: 'Улучшает энергоэффективность',
@@ -208,11 +225,13 @@ export default {
ionTechnology: 'Технология ионного оружия',
hyperspaceTechnology: 'Технология гиперпространственных прыжков',
plasmaTechnology: 'Технология плазменного оружия',
computerTechnology: 'Увеличивает вместимость очереди исследований, +1 за уровень (макс 10)',
computerTechnology: 'Увеличивает вместимость очереди исследований, +1 за уровень (макс 10 уровней)',
combustionDrive: 'Базовая технология двигателей',
impulseDrive: 'Средняя технология двигателей',
hyperspaceDrive: 'Продвинутая технология двигателей',
darkMatterTechnology: 'Исследование свойств и применения тёмной материи'
darkMatterTechnology: 'Исследование свойств и применения тёмной материи',
terraformingTechnology: 'Исследование технологии терраформирования планет, увеличивает доступное пространство всех планет на 3 за уровень',
planetDestructionTech: 'Исследование ужасающей технологии уничтожения целых планет'
},
officers: {
commander: 'Командир',
@@ -220,7 +239,10 @@ export default {
engineer: 'Инженер',
geologist: 'Геолог',
technocrat: 'Технократ',
darkMatterSpecialist: 'Специалист по тёмной материи'
darkMatterSpecialist: 'Специалист по тёмной материи',
resourceBonus: 'Бонус производства ресурсов',
darkMatterBonus: 'Бонус производства тёмной материи',
energyBonus: 'Бонус производства энергии'
},
officerDescriptions: {
commander: 'Улучшает скорость строительства и управление',
@@ -247,7 +269,14 @@ export default {
title: 'Обзор планеты',
resourceOverview: 'Ресурсы',
fleetInfo: 'Флот',
currentShips: 'Корабли на этой планете'
currentShips: 'Корабли на этой планете',
productionSources: 'Источники производства',
productionSourcesDesc: 'Подробная информация о производстве ресурсов и бонусах',
consumptionSources: 'Источники потребления',
consumptionSourcesDesc: 'Детали потребления энергии зданиями',
totalProduction: 'Общее производство',
totalConsumption: 'Общее потребление',
noConsumption: 'Нет потребления энергии'
},
buildingsView: {
title: 'Здания',
@@ -257,6 +286,8 @@ export default {
upgradeCost: 'Стоимость улучшения',
buildTime: 'Время строительства',
upgrade: 'Улучшить',
maxLevelReached: 'Достигнут максимальный уровень',
requirementsNotMet: 'Требования не выполнены',
upgradeFailed: 'Улучшение не удалось',
upgradeFailedMessage: 'Пожалуйста, проверьте, достаточно ли у вас ресурсов, места или нет других задач строительства.',
demolish: 'Снести',
@@ -268,6 +299,7 @@ export default {
title: 'Исследования',
researchCost: 'Стоимость исследования',
research: 'Исследовать',
maxLevelReached: 'Достигнут максимальный уровень',
researchFailed: 'Исследование не удалось',
researchFailedMessage:
'Пожалуйста, проверьте, достаточно ли у вас ресурсов, выполнены ли предварительные условия или нет других исследовательских задач.'
@@ -289,6 +321,7 @@ export default {
},
shipyardView: {
title: 'Верфь',
fleetStorage: 'Хранилище флота',
attack: 'Атака',
shield: 'Щит',
speed: 'Скорость',
@@ -360,6 +393,7 @@ export default {
colonize: 'Колонизация',
spy: 'Разведка',
deploy: 'Размещение',
recycle: 'Переработка',
transportResources: 'Транспортировка ресурсов',
totalCargoCapacity: 'Общая грузоподъёмность',
used: 'Использовано',
@@ -376,7 +410,12 @@ export default {
recallFailed: 'Отзыв не удался',
recallFailedMessage: 'Эта миссия не может быть отозвана.',
unknownPlanet: 'Неизвестная планета',
fleetMissionSlots: 'Слоты миссий флота'
fleetMissionSlots: 'Слоты миссий флота',
noShipsSelected: 'Корабли не выбраны',
cannotSendToOwnPlanet: 'Невозможно отправить флот на свою планету',
cargoExceedsCapacity: 'Груз превышает вместимость',
noColonyShip: 'Для колонизационной миссии требуется колониальный корабль',
noDebrisAtTarget: 'Нет поля обломков по целевым координатам или поле обломков пусто'
},
officersView: {
title: 'Офицеры',
@@ -429,18 +468,25 @@ export default {
attack: 'Атака',
colonize: 'Колонизация',
switch: 'Переключить',
recycle: 'Переработка',
debrisField: 'Поле обломков',
scoutPlanetTitle: 'Разведать планету',
attackPlanetTitle: 'Атаковать планету',
colonizePlanetTitle: 'Колонизировать планету',
recyclePlanetTitle: 'Переработать обломки',
scoutPlanetMessage:
'Вы уверены, что хотите отправить шпионские зонды для разведки планеты [{coordinates}]?\n\nПерейдите на страницу флота, чтобы выбрать корабли и отправить.',
attackPlanetMessage:
'Вы уверены, что хотите атаковать планету [{coordinates}]?\n\nПерейдите на страницу флота, чтобы выбрать корабли и отправить.',
colonizePlanetMessage:
'Вы уверены, что хотите колонизировать позицию [{coordinates}]?\n\nПерейдите на страницу флота, чтобы отправить колонизационный корабль.'
'Вы уверены, что хотите колонизировать позицию [{coordinates}]?\n\nПерейдите на страницу флота, чтобы отправить колонизационный корабль.',
recyclePlanetMessage:
'Вы уверены, что хотите переработать обломки в позиции [{coordinates}]?\n\nПерейдите на страницу флота, чтобы отправить переработчики.'
},
messagesView: {
title: 'Сообщения',
battles: 'Битвы',
spy: 'Разведка',
battleReports: 'Отчёты о боях',
spyReports: 'Отчёты разведки',
noBattleReports: 'Нет отчётов о боях',
@@ -461,7 +507,17 @@ export default {
resources: 'Ресурсы',
fleet: 'Флот',
defense: 'Оборона',
buildings: 'Здания'
buildings: 'Здания',
unread: 'Непрочитано',
targetPlanet: 'Целевая планета',
attackerRemaining: 'Осталось у нападающего',
defenderRemaining: 'Осталось у защитника',
moonChance: 'Шанс появления луны',
showRoundDetails: 'Показать детали раундов',
hideRoundDetails: 'Скрыть детали раундов',
round: 'Раунд {round}',
attackerRemainingPower: 'Оставшаяся мощь нападающего',
defenderRemainingPower: 'Оставшаяся мощь защитника'
},
simulatorView: {
title: 'Симулятор боя',
@@ -523,6 +579,12 @@ export default {
clearConfirmMessage: 'Все игровые данные будут удалены и игра начнется заново. Это действие невозможно отменить. Продолжить?',
gameSettings: 'Настройки игры',
gameSettingsDesc: 'Настроить параметры и предпочтения игры',
gamePause: 'Пауза игры',
gamePauseDesc: 'Приостановить или возобновить игровое время и производство ресурсов',
pause: 'Пауза',
resume: 'Возобновить',
gamePaused: 'Игра приостановлена',
gameResumed: 'Игра возобновлена',
playerName: 'Имя игрока',
gameSpeed: 'Скорость игры',
gameSpeedDesc: 'Текущий множитель скорости игры',
@@ -532,5 +594,34 @@ export default {
community: 'Сообщество',
github: 'Репозиторий GitHub',
qqGroup: 'Группа QQ'
},
gmView: {
title: 'Панель управления GM',
adminOnly: 'Только для администратора',
selectPlanet: 'Выбрать планету',
choosePlanet: 'Выберите планету',
resources: 'Ресурсы',
buildings: 'Здания',
research: 'Исследования',
ships: 'Корабли',
defense: 'Оборона',
officers: 'Офицеры',
modifyResources: 'Изменить ресурсы',
resourcesDesc: 'Быстрое изменение ресурсов планеты',
modifyBuildings: 'Изменить здания',
buildingsDesc: 'Быстрая установка уровней зданий',
modifyResearch: 'Изменить исследования',
researchDesc: 'Быстрая установка уровней исследований',
modifyShips: 'Изменить корабли',
shipsDesc: 'Быстрая установка количества кораблей',
modifyDefense: 'Изменить оборону',
defenseDesc: 'Быстрая установка количества обороны',
modifyOfficers: 'Изменить офицеров',
officersDesc: 'Быстрая установка времени истечения офицеров',
days: 'д',
dangerZone: 'Опасная зона',
dangerZoneDesc: 'Следующие операции необратимы',
resetGame: 'Сбросить игру',
resetGameConfirm: 'Вы уверены, что хотите сбросить игру? Все данные будут удалены!'
}
}

View File

@@ -32,11 +32,13 @@ export default {
locked: '已锁定',
viewRequirements: '查看前置条件',
requirementsNotMet: '前置条件未满足',
current: '当前'
current: '当前',
level: '等级'
},
errors: {
requirementsNotMet: '不满足前置条件',
insufficientResources: '资源不足',
insufficientFleetStorage: '舰队仓储空间不足',
shieldDomeLimit: '护盾罩数量限制',
fleetMissionsFull: '舰队任务槽位已满',
insufficientFleet: '舰队数量不足',
@@ -61,7 +63,8 @@ export default {
simulator: '模拟',
galaxy: '星系',
messages: '消息',
settings: '设置'
settings: '设置',
gm: 'GM'
},
sidebar: {
language: '语言',
@@ -77,10 +80,14 @@ export default {
darkMatter: '暗物质',
energy: '能量',
production: '产量',
consumption: '消耗',
capacity: '容量',
current: '当前储量',
max: '最大容量',
perHour: '小时'
perHour: '小时',
perMinute: '分钟',
hour: '小时',
noEnergy: '能量不足'
},
planet: {
planet: '星球',
@@ -113,9 +120,11 @@ export default {
crystalStorage: '晶体仓库',
deuteriumTank: '重氢罐',
darkMatterCollector: '暗物质收集器',
terraformer: '地形改造器',
lunarBase: '月球基地',
sensorPhalanx: '传感器阵列',
jumpGate: '跳跃门',
planetDestroyerFactory: '行星毁灭者工厂',
buildTime: '建造时间',
production: '产量',
consumption: '消耗',
@@ -129,16 +138,18 @@ export default {
deuteriumSynthesizer: '合成重氢资源',
solarPlant: '提供能源',
roboticsFactory: '加快建造速度',
naniteFactory: '增加建造队列数量,每级+1队列最多10',
naniteFactory: '增加建造队列数量,每级+1队列最多10',
shipyard: '建造舰船',
researchLab: '研究科技',
metalStorage: '增加金属存储上限',
crystalStorage: '增加晶体存储上限',
deuteriumTank: '增加重氢存储上限',
darkMatterCollector: '收集稀有的暗物质资源',
lunarBase: '增加月球可用空间',
terraformer: '改造行星地形每级增加5个可用空间',
lunarBase: '增加月球可用空间,每级+5空间',
sensorPhalanx: '侦测周围星系的舰队活动',
jumpGate: '瞬间传送舰队到其他月球'
jumpGate: '瞬间传送舰队到其他月球',
planetDestroyerFactory: '建造能够摧毁行星的终极武器'
},
ships: {
lightFighter: '轻型战斗机',
@@ -150,7 +161,8 @@ export default {
colonyShip: '殖民船',
recycler: '回收船',
espionageProbe: '间谍探测器',
darkMatterHarvester: '暗物质采集船'
darkMatterHarvester: '暗物质采集船',
deathstar: '死星'
},
shipDescriptions: {
lightFighter: '基础战斗单位',
@@ -162,7 +174,8 @@ export default {
colonyShip: '用于殖民新星球',
recycler: '收集残骸场资源',
espionageProbe: '侦察敌方星球',
darkMatterHarvester: '专门用于采集暗物质的特殊飞船'
darkMatterHarvester: '专门用于采集暗物质的特殊飞船',
deathstar: '终极武器,能够摧毁整个行星'
},
defenses: {
rocketLauncher: '火箭发射器',
@@ -172,7 +185,8 @@ export default {
ionCannon: '离子炮',
plasmaTurret: '等离子炮塔',
smallShieldDome: '小型护盾罩',
largeShieldDome: '大型护盾罩'
largeShieldDome: '大型护盾罩',
planetaryShield: '行星护盾'
},
defenseDescriptions: {
rocketLauncher: '基础防御设施',
@@ -182,7 +196,8 @@ export default {
ionCannon: '破坏护盾的利器',
plasmaTurret: '强力防御设施',
smallShieldDome: '保护整个星球的小型护盾',
largeShieldDome: '保护整个星球的大型护盾'
largeShieldDome: '保护整个星球的大型护盾',
planetaryShield: '保护行星免受毁灭攻击的超级护盾'
},
research: {
researchTime: '研究时间',
@@ -200,7 +215,9 @@ export default {
combustionDrive: '燃烧引擎',
impulseDrive: '脉冲引擎',
hyperspaceDrive: '超空间引擎',
darkMatterTechnology: '暗物质技术'
darkMatterTechnology: '暗物质技术',
terraformingTechnology: '地形改造技术',
planetDestructionTech: '行星毁灭技术'
},
technologyDescriptions: {
energyTechnology: '提高能源利用效率',
@@ -208,11 +225,13 @@ export default {
ionTechnology: '离子武器技术',
hyperspaceTechnology: '超空间跳跃技术',
plasmaTechnology: '等离子武器技术',
computerTechnology: '增加研究队列数量,每级+1队列最多10',
computerTechnology: '增加研究队列数量,每级+1队列最多10',
combustionDrive: '基础推进技术',
impulseDrive: '中级推进技术',
hyperspaceDrive: '高级推进技术',
darkMatterTechnology: '研究暗物质的性质和应用'
darkMatterTechnology: '研究暗物质的性质和应用',
terraformingTechnology: '研究行星地形改造技术每级为所有行星增加3个可用空间',
planetDestructionTech: '研究如何摧毁整个行星的恐怖技术'
},
officers: {
commander: '指挥官',
@@ -220,7 +239,10 @@ export default {
engineer: '工程师',
geologist: '地质学家',
technocrat: '技术专家',
darkMatterSpecialist: '暗物质专家'
darkMatterSpecialist: '暗物质专家',
resourceBonus: '资源产量加成',
darkMatterBonus: '暗物质产量加成',
energyBonus: '能量产量加成'
},
officerDescriptions: {
commander: '提升建筑速度和管理能力',
@@ -241,13 +263,21 @@ export default {
cancelResearch: '取消研究',
confirmCancel: '确定要取消吗将返还50%的资源。',
level: '等级',
quantity: '数量',
upgradeToLevel: '升级到等级'
},
overview: {
title: '星球总览',
resourceOverview: '资源概览',
fleetInfo: '舰队',
currentShips: '当前星球的舰船数量'
currentShips: '当前星球的舰船数量',
productionSources: '资源获取来源',
productionSourcesDesc: '详细的资源产量及加成信息',
consumptionSources: '能量消耗来源',
consumptionSourcesDesc: '各建筑的能量消耗详情',
totalProduction: '总产量',
totalConsumption: '总消耗',
noConsumption: '当前无能量消耗'
},
buildingsView: {
title: '建筑',
@@ -257,6 +287,8 @@ export default {
upgradeCost: '升级消耗',
buildTime: '建造时间',
upgrade: '升级',
maxLevelReached: '等级已满',
requirementsNotMet: '条件不足',
upgradeFailed: '升级失败',
upgradeFailedMessage: '请检查资源是否足够、空间是否充足或是否有其他建造任务。',
demolish: '拆除',
@@ -268,6 +300,7 @@ export default {
title: '研究',
researchCost: '研究消耗',
research: '研究',
maxLevelReached: '等级已满',
researchFailed: '研究失败',
researchFailedMessage: '请检查资源是否足够、前置条件是否满足,或是否有其他研究任务。'
},
@@ -288,6 +321,7 @@ export default {
},
shipyardView: {
title: '船坞',
fleetStorage: '舰队仓储',
attack: '攻击力',
shield: '护盾',
speed: '速度',
@@ -358,6 +392,8 @@ export default {
colonize: '殖民',
spy: '侦察',
deploy: '部署',
recycle: '回收',
destroy: '行星毁灭',
transportResources: '运输资源',
totalCargoCapacity: '总载货量',
used: '已用',
@@ -374,7 +410,13 @@ export default {
recallFailed: '召回失败',
recallFailedMessage: '该任务无法召回。',
unknownPlanet: '未知星球',
fleetMissionSlots: '舰队任务槽位'
fleetMissionSlots: '舰队任务槽位',
noShipsSelected: '未选择舰船',
cannotSendToOwnPlanet: '无法派遣舰队到自己的星球',
cargoExceedsCapacity: '载货量超出限制',
noColonyShip: '需要殖民船才能执行殖民任务',
noDebrisAtTarget: '目标坐标没有残骸场或残骸场已空',
noDeathstar: '需要死星才能执行毁灭任务'
},
officersView: {
title: '军官',
@@ -427,15 +469,21 @@ export default {
attack: '攻击',
colonize: '殖民',
switch: '切换',
recycle: '回收',
debrisField: '残骸场',
scoutPlanetTitle: '侦察星球',
attackPlanetTitle: '攻击星球',
colonizePlanetTitle: '殖民星球',
recyclePlanetTitle: '回收残骸',
scoutPlanetMessage: '确定要派遣间谍探测器侦察星球 [{coordinates}] 吗?\n\n请前往舰队页面选择舰船并派遣。',
attackPlanetMessage: '确定要攻击星球 [{coordinates}] 吗?\n\n请前往舰队页面选择舰船并派遣。',
colonizePlanetMessage: '确定要殖民位置 [{coordinates}] 吗?\n\n请前往舰队页面派遣殖民船。'
colonizePlanetMessage: '确定要殖民位置 [{coordinates}] 吗?\n\n请前往舰队页面派遣殖民船。',
recyclePlanetMessage: '确定要回收位置 [{coordinates}] 的残骸吗?\n\n请前往舰队页面派遣回收船。'
},
messagesView: {
title: '消息中心',
battles: '战斗',
spy: '侦查',
battleReports: '战斗报告',
spyReports: '间谍报告',
noBattleReports: '暂无战斗报告',
@@ -456,7 +504,17 @@ export default {
resources: '资源',
fleet: '舰队',
defense: '防御',
buildings: '建筑'
buildings: '建筑',
unread: '未读',
targetPlanet: '目标星球',
attackerRemaining: '攻击方剩余',
defenderRemaining: '防守方剩余',
moonChance: '月球生成概率',
showRoundDetails: '显示回合详情',
hideRoundDetails: '隐藏回合详情',
round: '第{round}回合',
attackerRemainingPower: '攻击方剩余火力',
defenderRemainingPower: '防守方剩余火力'
},
simulatorView: {
title: '战斗模拟器',
@@ -518,6 +576,12 @@ export default {
clearConfirmMessage: '这将删除所有游戏数据并重新开始,此操作不可撤销。确定要继续吗?',
gameSettings: '游戏设置',
gameSettingsDesc: '调整游戏参数和偏好设置',
gamePause: '游戏暂停',
gamePauseDesc: '暂停或恢复游戏时间和资源生产',
pause: '暂停',
resume: '恢复',
gamePaused: '游戏已暂停',
gameResumed: '游戏已恢复',
playerName: '玩家名称',
gameSpeed: '游戏速度',
gameSpeedDesc: '当前游戏速度倍率',
@@ -527,5 +591,34 @@ export default {
community: '社区',
github: 'GitHub 仓库',
qqGroup: 'QQ 交流群'
},
gmView: {
title: 'GM 管理面板',
adminOnly: '仅管理员',
selectPlanet: '选择星球',
choosePlanet: '选择一个星球',
resources: '资源',
buildings: '建筑',
research: '科技',
ships: '舰船',
defense: '防御',
officers: '军官',
modifyResources: '修改资源',
resourcesDesc: '快速修改星球资源数量',
modifyBuildings: '修改建筑',
buildingsDesc: '快速设置建筑等级',
modifyResearch: '修改科技',
researchDesc: '快速设置科技等级',
modifyShips: '修改舰船',
shipsDesc: '快速设置舰船数量',
modifyDefense: '修改防御',
defenseDesc: '快速设置防御数量',
modifyOfficers: '修改军官',
officersDesc: '快速设置军官到期时间',
days: '天',
dangerZone: '危险区域',
dangerZoneDesc: '以下操作不可撤销,请谨慎操作',
resetGame: '重置游戏',
resetGameConfirm: '确定要重置游戏吗?这将删除所有数据!'
}
}

View File

@@ -32,11 +32,13 @@ export default {
locked: '已鎖定',
viewRequirements: '查看前置條件',
requirementsNotMet: '前置條件未滿足',
current: '當前'
current: '當前',
level: '等級'
},
errors: {
requirementsNotMet: '不滿足前置條件',
insufficientResources: '資源不足',
insufficientFleetStorage: '艦隊倉儲空間不足',
shieldDomeLimit: '護盾罩數量限制',
fleetMissionsFull: '艦隊任務槽位已滿',
insufficientFleet: '艦隊數量不足',
@@ -61,7 +63,8 @@ export default {
simulator: '模擬',
galaxy: '星系',
messages: '訊息',
settings: '設定'
settings: '設定',
gm: 'GM'
},
sidebar: {
language: '語言',
@@ -77,10 +80,14 @@ export default {
darkMatter: '暗物質',
energy: '能量',
production: '產量',
consumption: '消耗',
capacity: '容量',
current: '當前儲量',
max: '最大容量',
perHour: '小時'
perHour: '小時',
perMinute: '分鐘',
hour: '小時',
noEnergy: '能量不足'
},
planet: {
planet: '星球',
@@ -113,9 +120,11 @@ export default {
crystalStorage: '晶體倉庫',
deuteriumTank: '重氫罐',
darkMatterCollector: '暗物質收集器',
terraformer: '地形改造器',
lunarBase: '月球基地',
sensorPhalanx: '傳感器陣列',
jumpGate: '跳躍門',
planetDestroyerFactory: '行星毀滅者工廠',
buildTime: '建造時間',
production: '產量',
consumption: '消耗',
@@ -129,16 +138,18 @@ export default {
deuteriumSynthesizer: '合成重氫資源',
solarPlant: '提供能源',
roboticsFactory: '加快建造速度',
naniteFactory: '增加建造佇列數量,每級+1佇列最多10',
naniteFactory: '增加建造佇列數量,每級+1佇列最多10',
shipyard: '建造艦船',
researchLab: '研究科技',
metalStorage: '增加金屬儲存上限',
crystalStorage: '增加晶體儲存上限',
deuteriumTank: '增加重氫儲存上限',
darkMatterCollector: '收集稀有的暗物質資源',
lunarBase: '增加月球可用空間',
terraformer: '改造行星地形每級增加5個可用空間',
lunarBase: '增加月球可用空間,每級+5空間',
sensorPhalanx: '偵測周圍星系的艦隊活動',
jumpGate: '瞬間傳送艦隊到其他月球'
jumpGate: '瞬間傳送艦隊到其他月球',
planetDestroyerFactory: '建造能夠摧毀行星的終極武器'
},
ships: {
lightFighter: '輕型戰鬥機',
@@ -150,7 +161,8 @@ export default {
colonyShip: '殖民船',
recycler: '回收船',
espionageProbe: '間諜探測器',
darkMatterHarvester: '暗物質採集船'
darkMatterHarvester: '暗物質採集船',
deathstar: '死星'
},
shipDescriptions: {
lightFighter: '基礎戰鬥單位',
@@ -162,7 +174,8 @@ export default {
colonyShip: '用於殖民新星球',
recycler: '收集殘骸場資源',
espionageProbe: '偵察敵方星球',
darkMatterHarvester: '專門用於採集暗物質的特殊飛船'
darkMatterHarvester: '專門用於採集暗物質的特殊飛船',
deathstar: '終極武器,能夠摧毀整個行星'
},
defenses: {
rocketLauncher: '火箭發射器',
@@ -172,7 +185,8 @@ export default {
ionCannon: '離子炮',
plasmaTurret: '等離子炮塔',
smallShieldDome: '小型護盾罩',
largeShieldDome: '大型護盾罩'
largeShieldDome: '大型護盾罩',
planetaryShield: '行星護盾'
},
defenseDescriptions: {
rocketLauncher: '基礎防禦設施',
@@ -182,7 +196,8 @@ export default {
ionCannon: '破壞護盾的利器',
plasmaTurret: '強力防禦設施',
smallShieldDome: '保護整個星球的小型護盾',
largeShieldDome: '保護整個星球的大型護盾'
largeShieldDome: '保護整個星球的大型護盾',
planetaryShield: '保護行星免受毀滅攻擊的超級護盾'
},
research: {
researchTime: '研究時間',
@@ -200,7 +215,9 @@ export default {
combustionDrive: '燃燒引擎',
impulseDrive: '脈衝引擎',
hyperspaceDrive: '超空間引擎',
darkMatterTechnology: '暗物質技術'
darkMatterTechnology: '暗物質技術',
terraformingTechnology: '地形改造技術',
planetDestructionTech: '行星毀滅技術'
},
technologyDescriptions: {
energyTechnology: '提高能源利用效率',
@@ -208,11 +225,13 @@ export default {
ionTechnology: '離子武器技術',
hyperspaceTechnology: '超空間跳躍技術',
plasmaTechnology: '等離子武器技術',
computerTechnology: '增加研究佇列數量,每級+1佇列最多10',
computerTechnology: '增加研究佇列數量,每級+1佇列最多10',
combustionDrive: '基礎推進技術',
impulseDrive: '中級推進技術',
hyperspaceDrive: '高級推進技術',
darkMatterTechnology: '研究暗物質的性質和應用'
darkMatterTechnology: '研究暗物質的性質和應用',
terraformingTechnology: '研究行星地形改造技術每級為所有行星增加3個可用空間',
planetDestructionTech: '研究如何摧毀整個行星的恐怖技術'
},
officers: {
commander: '指揮官',
@@ -220,7 +239,10 @@ export default {
engineer: '工程師',
geologist: '地質學家',
technocrat: '技術專家',
darkMatterSpecialist: '暗物質專家'
darkMatterSpecialist: '暗物質專家',
resourceBonus: '資源生產加成',
darkMatterBonus: '暗物質生產加成',
energyBonus: '能量生產加成'
},
officerDescriptions: {
commander: '提升建築速度和管理能力',
@@ -247,7 +269,14 @@ export default {
title: '星球總覽',
resourceOverview: '資源概覽',
fleetInfo: '艦隊資訊',
currentShips: '當前星球的艦船數量'
currentShips: '當前星球的艦船數量',
productionSources: '生產來源',
productionSourcesDesc: '詳細資源生產和加成資訊',
consumptionSources: '消耗來源',
consumptionSourcesDesc: '建築能量消耗詳情',
totalProduction: '總產量',
totalConsumption: '總消耗',
noConsumption: '無能量消耗'
},
buildingsView: {
title: '建築',
@@ -257,6 +286,8 @@ export default {
upgradeCost: '升級消耗',
buildTime: '建造時間',
upgrade: '升級',
maxLevelReached: '等級已滿',
requirementsNotMet: '條件不足',
upgradeFailed: '升級失敗',
upgradeFailedMessage: '請檢查資源是否足夠、空間是否充足或是否有其他建造任務。',
demolish: '拆除',
@@ -268,6 +299,7 @@ export default {
title: '研究',
researchCost: '研究消耗',
research: '研究',
maxLevelReached: '等級已滿',
researchFailed: '研究失敗',
researchFailedMessage: '請檢查資源是否足夠、前置條件是否滿足,或是否有其他研究任務。'
},
@@ -288,6 +320,7 @@ export default {
},
shipyardView: {
title: '船塢',
fleetStorage: '艦隊倉儲',
attack: '攻擊力',
shield: '護盾',
speed: '速度',
@@ -358,6 +391,7 @@ export default {
colonize: '殖民',
spy: '偵察',
deploy: '部署',
recycle: '回收',
transportResources: '運輸資源',
totalCargoCapacity: '總載貨量',
used: '已用',
@@ -374,7 +408,12 @@ export default {
recallFailed: '召回失敗',
recallFailedMessage: '該任務無法召回。',
unknownPlanet: '未知星球',
fleetMissionSlots: '艦隊任務槽位'
fleetMissionSlots: '艦隊任務槽位',
noShipsSelected: '未選擇艦船',
cannotSendToOwnPlanet: '無法派遣艦隊到自己的星球',
cargoExceedsCapacity: '載貨量超出限制',
noColonyShip: '需要殖民船才能執行殖民任務',
noDebrisAtTarget: '目標坐標沒有殘骸場或殘骸場已空'
},
officersView: {
title: '軍官',
@@ -427,15 +466,21 @@ export default {
attack: '攻擊',
colonize: '殖民',
switch: '切換',
recycle: '回收',
debrisField: '殘骸場',
scoutPlanetTitle: '偵察星球',
attackPlanetTitle: '攻擊星球',
colonizePlanetTitle: '殖民星球',
recyclePlanetTitle: '回收殘骸',
scoutPlanetMessage: '確定要派遣間諜探測器偵察星球 [{coordinates}] 嗎?\n\n請前往艦隊頁面選擇艦船並派遣。',
attackPlanetMessage: '確定要攻擊星球 [{coordinates}] 嗎?\n\n請前往艦隊頁面選擇艦船並派遣。',
colonizePlanetMessage: '確定要殖民位置 [{coordinates}] 嗎?\n\n請前往艦隊頁面派遣殖民船。'
colonizePlanetMessage: '確定要殖民位置 [{coordinates}] 嗎?\n\n請前往艦隊頁面派遣殖民船。',
recyclePlanetMessage: '確定要回收位置 [{coordinates}] 的殘骸嗎?\n\n請前往艦隊頁面派遣回收船。'
},
messagesView: {
title: '訊息中心',
battles: '戰鬥',
spy: '偵查',
battleReports: '戰鬥報告',
spyReports: '間諜報告',
noBattleReports: '暫無戰鬥報告',
@@ -456,7 +501,17 @@ export default {
resources: '資源',
fleet: '艦隊',
defense: '防禦',
buildings: '建築'
buildings: '建築',
unread: '未讀',
targetPlanet: '目標星球',
attackerRemaining: '攻擊方剩餘',
defenderRemaining: '防守方剩餘',
moonChance: '月球生成機率',
showRoundDetails: '顯示回合詳情',
hideRoundDetails: '隱藏回合詳情',
round: '第{round}回合',
attackerRemainingPower: '攻擊方剩餘火力',
defenderRemainingPower: '防守方剩餘火力'
},
simulatorView: {
title: '戰鬥模擬器',
@@ -518,6 +573,12 @@ export default {
clearConfirmMessage: '這將刪除所有遊戲資料並重新開始,此操作不可撤銷。確定要繼續嗎?',
gameSettings: '遊戲設定',
gameSettingsDesc: '調整遊戲參數和偏好設定',
gamePause: '遊戲暫停',
gamePauseDesc: '暫停或恢復遊戲時間和資源生產',
pause: '暫停',
resume: '恢復',
gamePaused: '遊戲已暫停',
gameResumed: '遊戲已恢復',
playerName: '玩家名稱',
gameSpeed: '遊戲速度',
gameSpeedDesc: '目前遊戲速度倍率',
@@ -527,5 +588,34 @@ export default {
community: '社群',
github: 'GitHub 儲存庫',
qqGroup: 'QQ 交流群'
},
gmView: {
title: 'GM 管理面板',
adminOnly: '僅管理員',
selectPlanet: '選擇星球',
choosePlanet: '選擇一個星球',
resources: '資源',
buildings: '建築',
research: '科技',
ships: '艦船',
defense: '防禦',
officers: '軍官',
modifyResources: '修改資源',
resourcesDesc: '快速修改星球資源數量',
modifyBuildings: '修改建築',
buildingsDesc: '快速設定建築等級',
modifyResearch: '修改科技',
researchDesc: '快速設定科技等級',
modifyShips: '修改艦船',
shipsDesc: '快速設定艦船數量',
modifyDefense: '修改防禦',
defenseDesc: '快速設定防禦數量',
modifyOfficers: '修改軍官',
officersDesc: '快速設定軍官到期時間',
days: '天',
dangerZone: '危險區域',
dangerZoneDesc: '以下操作不可撤銷,請謹慎操作',
resetGame: '重置遊戲',
resetGameConfirm: '確定要重置遊戲嗎?這將刪除所有資料!'
}
}

View File

@@ -1,312 +1,65 @@
import type { Fleet, Resources, BattleResult, Officer } from '@/types/game'
import { DefenseType, ShipType, OfficerType } from '@/types/game'
import { SHIPS, DEFENSES } from '@/config/gameConfig'
import { DefenseType, OfficerType } from '@/types/game'
import * as officerLogic from './officerLogic'
/**
* 战斗单位(舰船或防御)
*/
interface BattleUnit {
type: ShipType | DefenseType
count: number
attack: number
shield: number
armor: number
isShip: boolean
}
/**
* 战斗方
*/
interface BattleSide {
fleet: BattleUnit[]
defense: BattleUnit[]
totalShields: number
totalArmor: number
}
/**
* 准备战斗方数据
*/
const prepareBattleSide = (fleet: Partial<Fleet>, defense: Partial<Record<DefenseType, number>>, defenseBonus: number = 0): BattleSide => {
const side: BattleSide = {
fleet: [],
defense: [],
totalShields: 0,
totalArmor: 0
}
// 添加舰船
Object.entries(fleet).forEach(([shipType, count]) => {
if (count > 0) {
const config = SHIPS[shipType as ShipType]
const unit: BattleUnit = {
type: shipType as ShipType,
count,
attack: config.attack,
shield: config.shield * (1 + defenseBonus / 100),
armor: config.armor * (1 + defenseBonus / 100),
isShip: true
}
side.fleet.push(unit)
side.totalShields += unit.shield * count
side.totalArmor += unit.armor * count
}
})
// 添加防御
Object.entries(defense).forEach(([defenseType, count]) => {
if (count > 0) {
const config = DEFENSES[defenseType as DefenseType]
const unit: BattleUnit = {
type: defenseType as DefenseType,
count,
attack: config.attack,
shield: config.shield * (1 + defenseBonus / 100),
armor: config.armor * (1 + defenseBonus / 100),
isShip: false
}
side.defense.push(unit)
side.totalShields += unit.shield * count
side.totalArmor += unit.armor * count
}
})
return side
}
/**
* 计算一方的总攻击力
*/
const calculateTotalAttack = (side: BattleSide): number => {
let total = 0
side.fleet.forEach(unit => {
total += unit.attack * unit.count
})
side.defense.forEach(unit => {
total += unit.attack * unit.count
})
return total
}
/**
* 执行一轮战斗
*/
const executeBattleRound = (attacker: BattleSide, defender: BattleSide): void => {
// 攻击方对防御方造成伤害
const attackerDamage = calculateTotalAttack(attacker)
applyDamage(defender, attackerDamage)
// 防御方对攻击方造成伤害
const defenderDamage = calculateTotalAttack(defender)
applyDamage(attacker, defenderDamage)
}
/**
* 对一方施加伤害
*/
const applyDamage = (side: BattleSide, totalDamage: number): void => {
let remainingDamage = totalDamage
// 先消耗护盾
const totalShields = side.totalShields
if (totalShields > 0) {
const shieldAbsorption = Math.min(remainingDamage, totalShields)
remainingDamage -= shieldAbsorption
side.totalShields -= shieldAbsorption
}
// 剩余伤害穿透护盾,破坏单位
if (remainingDamage > 0) {
destroyUnits(side, remainingDamage)
}
}
/**
* 根据伤害摧毁单位
*/
const destroyUnits = (side: BattleSide, damage: number): void => {
let remainingDamage = damage
// 随机选择单位摧毁
const allUnits = [...side.fleet, ...side.defense]
while (remainingDamage > 0 && allUnits.some(u => u.count > 0)) {
// 随机选择一个有数量的单位
const availableUnits = allUnits.filter(u => u.count > 0)
if (availableUnits.length === 0) break
const targetUnit = availableUnits[Math.floor(Math.random() * availableUnits.length)]
if (!targetUnit) break // 安全检查
// 计算破坏概率(伤害 / 装甲)
const destructionChance = Math.min(remainingDamage / targetUnit.armor, 1)
if (Math.random() < destructionChance) {
targetUnit.count--
side.totalArmor -= targetUnit.armor
remainingDamage -= targetUnit.armor
} else {
// 未破坏,但消耗一部分伤害
remainingDamage -= targetUnit.armor * destructionChance
}
}
}
/**
* 检查战斗是否结束
*/
const isBattleOver = (attacker: BattleSide, defender: BattleSide): boolean => {
const attackerHasUnits = attacker.fleet.some(u => u.count > 0) || attacker.defense.some(u => u.count > 0)
const defenderHasUnits = defender.fleet.some(u => u.count > 0) || defender.defense.some(u => u.count > 0)
return !attackerHasUnits || !defenderHasUnits
}
/**
* 计算损失
*/
const calculateLosses = (
initialSide: BattleSide,
finalSide: BattleSide
): { fleet: Partial<Fleet>; defense: Partial<Record<DefenseType, number>> } => {
const losses: { fleet: Partial<Fleet>; defense: Partial<Record<DefenseType, number>> } = {
fleet: {},
defense: {}
}
// 计算舰船损失
initialSide.fleet.forEach((initialUnit, index) => {
const finalUnit = finalSide.fleet[index]
const lost = initialUnit.count - (finalUnit?.count || 0)
if (lost > 0) {
losses.fleet[initialUnit.type as ShipType] = lost
}
})
// 计算防御损失
initialSide.defense.forEach((initialUnit, index) => {
const finalUnit = finalSide.defense[index]
const lost = initialUnit.count - (finalUnit?.count || 0)
if (lost > 0) {
losses.defense[initialUnit.type as DefenseType] = lost
}
})
return losses
}
/**
* 计算残骸场
*/
const calculateDebrisField = (
attackerLosses: Partial<Fleet>,
defenderLosses: { fleet: Partial<Fleet>; defense: Partial<Record<DefenseType, number>> }
): Resources => {
const debris: Resources = { metal: 0, crystal: 0, deuterium: 0, darkMatter: 0, energy: 0 }
const debrisRate = 0.3 // 30%的残骸回收率
// 攻击方舰船损失
Object.entries(attackerLosses).forEach(([shipType, count]) => {
const config = SHIPS[shipType as ShipType]
debris.metal += config.cost.metal * count * debrisRate
debris.crystal += config.cost.crystal * count * debrisRate
})
// 防御方舰船损失
Object.entries(defenderLosses.fleet).forEach(([shipType, count]) => {
const config = SHIPS[shipType as ShipType]
debris.metal += config.cost.metal * count * debrisRate
debris.crystal += config.cost.crystal * count * debrisRate
})
// 防御设施不产生残骸场(或产生较少)
return debris
}
/**
* 计算掠夺资源
*/
const calculatePlunder = (availableResources: Resources, attackerFleet: Partial<Fleet>, cargoCapacity: number): Resources => {
// 计算攻击方剩余运载能力
let totalCapacity = 0
Object.entries(attackerFleet).forEach(([shipType, count]) => {
const config = SHIPS[shipType as ShipType]
totalCapacity += config.cargoCapacity * count
})
// 最多掠夺50%的资源
const maxPlunder = Math.min(totalCapacity, cargoCapacity)
const plunderRate = 0.5
const plunder: Resources = {
metal: Math.min(availableResources.metal * plunderRate, maxPlunder * 0.5),
crystal: Math.min(availableResources.crystal * plunderRate, maxPlunder * 0.3),
deuterium: Math.min(availableResources.deuterium * plunderRate, maxPlunder * 0.2),
darkMatter: 0, // 暗物质无法掠夺
energy: 0
}
return plunder
}
import { workerManager } from '@/workers/workerManager'
/**
* 执行战斗模拟
* 使用 Web Worker 在后台线程中执行计算密集型的战斗模拟
*/
export const simulateBattle = (
export const simulateBattle = async (
attackerFleet: Partial<Fleet>,
defenderFleet: Partial<Fleet>,
defenderDefense: Partial<Record<DefenseType, number>>,
defenderResources: Resources,
attackerOfficers: Record<OfficerType, Officer>,
defenderOfficers: Record<OfficerType, Officer>
): BattleResult => {
): Promise<BattleResult> => {
// 计算军官加成
const attackerBonuses = officerLogic.calculateActiveBonuses(attackerOfficers, Date.now())
const defenderBonuses = officerLogic.calculateActiveBonuses(defenderOfficers, Date.now())
// 准备战斗方
const initialAttacker = prepareBattleSide(attackerFleet, {}, attackerBonuses.defenseBonus)
const initialDefender = prepareBattleSide(defenderFleet, defenderDefense, defenderBonuses.defenseBonus)
// 将防御加成转换为科技等级简化10%加成 = 1级科技
const attackerTechLevel = Math.floor(attackerBonuses.defenseBonus / 10)
const defenderTechLevel = Math.floor(defenderBonuses.defenseBonus / 10)
// 复制战斗方用于战斗
const attacker = JSON.parse(JSON.stringify(initialAttacker)) as BattleSide
const defender = JSON.parse(JSON.stringify(initialDefender)) as BattleSide
// 战斗回合最多6回合
let rounds = 0
const maxRounds = 6
while (rounds < maxRounds && !isBattleOver(attacker, defender)) {
executeBattleRound(attacker, defender)
rounds++
}
// 计算损失
const attackerLosses = calculateLosses(initialAttacker, attacker).fleet
const defenderLosses = calculateLosses(initialDefender, defender)
// 判断胜负
let winner: 'attacker' | 'defender' | 'draw' = 'draw'
const attackerSurvived = attacker.fleet.some(u => u.count > 0)
const defenderSurvived = defender.fleet.some(u => u.count > 0) || defender.defense.some(u => u.count > 0)
if (attackerSurvived && !defenderSurvived) {
winner = 'attacker'
} else if (!attackerSurvived && defenderSurvived) {
winner = 'defender'
}
// 计算残骸场
const debrisField = calculateDebrisField(attackerLosses, defenderLosses)
// 使用 Worker 执行战斗模拟
const simulationResult = await workerManager.simulateBattle({
attacker: {
ships: attackerFleet,
weaponTech: 0, // 暂时不考虑武器科技
shieldTech: attackerTechLevel,
armorTech: attackerTechLevel
},
defender: {
ships: defenderFleet,
defense: defenderDefense,
weaponTech: 0,
shieldTech: defenderTechLevel,
armorTech: defenderTechLevel
},
maxRounds: 6 // 最多6回合
})
// 计算掠夺(仅攻击方胜利时)
const plunder =
winner === 'attacker'
? calculatePlunder(defenderResources, attackerFleet, 10000)
simulationResult.winner === 'attacker'
? await workerManager.calculatePlunder({
defenderResources,
attackerFleet: simulationResult.attackerRemaining
})
: { metal: 0, crystal: 0, deuterium: 0, darkMatter: 0, energy: 0 }
// 计算残骸场
const debrisField = await workerManager.calculateDebris({
attackerLosses: simulationResult.attackerLosses,
defenderLosses: simulationResult.defenderLosses
})
// 计算月球生成概率(根据残骸场总量)
const totalDebris = debrisField.metal + debrisField.crystal
const moonChance = Math.min(totalDebris / 100000, 0.2) // 最高20%概率
// 生成战斗报告
const battleResult: BattleResult = {
id: `battle_${Date.now()}`,
@@ -318,11 +71,17 @@ export const simulateBattle = (
attackerFleet,
defenderFleet,
defenderDefense,
attackerLosses,
defenderLosses,
winner,
attackerLosses: simulationResult.attackerLosses,
defenderLosses: simulationResult.defenderLosses,
winner: simulationResult.winner,
plunder,
debrisField
debrisField,
// 新增详细信息
rounds: simulationResult.rounds,
attackerRemaining: simulationResult.attackerRemaining,
defenderRemaining: simulationResult.defenderRemaining,
roundDetails: simulationResult.roundDetails,
moonChance
}
return battleResult

View File

@@ -1,17 +1,35 @@
import type { FleetMission, Planet, Resources, Fleet, BattleResult, SpyReport, Player, Officer } from '@/types/game'
import type { FleetMission, Planet, Resources, Fleet, BattleResult, SpyReport, Player, Officer, DebrisField } from '@/types/game'
import { ShipType, DefenseType, MissionType, BuildingType, OfficerType } from '@/types/game'
import { FLEET_STORAGE_CONFIG } from '@/config/gameConfig'
import * as battleLogic from './battleLogic'
import * as moonLogic from './moonLogic'
import * as moonValidation from './moonValidation'
/**
* 计算两个星球之间的距离
* 使用类似 OGame 的距离计算公式
*/
export const calculateDistance = (
from: { galaxy: number; system: number; position: number },
to: { galaxy: number; system: number; position: number }
): number => {
return Math.sqrt(Math.pow(to.galaxy - from.galaxy, 2) + Math.pow(to.system - from.system, 2) + Math.pow(to.position - from.position, 2))
// 同一位置
if (from.galaxy === to.galaxy && from.system === to.system && from.position === to.position) {
return 5
}
// 同星系内不同位置
if (from.galaxy === to.galaxy && from.system === to.system) {
return 1000 + Math.abs(to.position - from.position) * 5
}
// 同系统内不同星系
if (from.galaxy === to.galaxy) {
return 2700 + Math.abs(to.system - from.system) * 95
}
// 不同系统
return 20000 + Math.abs(to.galaxy - from.galaxy) * 20000
}
/**
@@ -66,20 +84,20 @@ export const processTransportArrival = (mission: FleetMission, targetPlanet: Pla
/**
* 处理攻击任务到达
*/
export const processAttackArrival = (
export const processAttackArrival = async (
mission: FleetMission,
targetPlanet: Planet | undefined,
attacker: Player,
defender: Player | null,
allPlanets: Planet[]
): { battleResult: BattleResult; moon: Planet | null } | null => {
): Promise<{ battleResult: BattleResult; moon: Planet | null; debrisField: DebrisField | null } | null> => {
if (!targetPlanet || targetPlanet.ownerId === attacker.id) {
mission.status = 'returning'
return null
}
// 执行战斗
const battleResult = battleLogic.simulateBattle(
// 执行战斗(使用 Worker 进行异步计算)
const battleResult = await battleLogic.simulateBattle(
mission.fleet,
targetPlanet.fleet,
targetPlanet.defense,
@@ -141,7 +159,22 @@ export const processAttackArrival = (
}
}
return { battleResult, moon }
// 创建残骸场(如果有残骸)
let debrisField: DebrisField | null = null
const totalDebris = battleResult.debrisField.metal + battleResult.debrisField.crystal
if (totalDebris > 0) {
debrisField = {
id: `debris_${targetPlanet.position.galaxy}_${targetPlanet.position.system}_${targetPlanet.position.position}`,
position: targetPlanet.position,
resources: {
metal: battleResult.debrisField.metal,
crystal: battleResult.debrisField.crystal
},
createdAt: Date.now()
}
}
return { battleResult, moon, debrisField }
}
/**
@@ -177,7 +210,8 @@ export const processColonizeArrival = (
[ShipType.ColonyShip]: 0,
[ShipType.Recycler]: 0,
[ShipType.EspionageProbe]: 0,
[ShipType.DarkMatterHarvester]: 0
[ShipType.DarkMatterHarvester]: 0,
[ShipType.Deathstar]: 0
},
defense: {
[DefenseType.RocketLauncher]: 0,
@@ -187,11 +221,13 @@ export const processColonizeArrival = (
[DefenseType.IonCannon]: 0,
[DefenseType.PlasmaTurret]: 0,
[DefenseType.SmallShieldDome]: 0,
[DefenseType.LargeShieldDome]: 0
[DefenseType.LargeShieldDome]: 0,
[DefenseType.PlanetaryShield]: 0
},
buildQueue: [],
lastUpdate: Date.now(),
maxSpace: 200,
maxFleetStorage: FLEET_STORAGE_CONFIG.baseStorage,
isMoon: false
}
@@ -250,6 +286,156 @@ export const processDeployArrival = (mission: FleetMission, targetPlanet: Planet
return true
}
/**
* 处理回收任务到达
*/
export const processRecycleArrival = (
mission: FleetMission,
debrisField: DebrisField | undefined
): { collectedResources: Pick<Resources, 'metal' | 'crystal'>; remainingDebris: Pick<Resources, 'metal' | 'crystal'> | null } | null => {
if (!debrisField) {
mission.status = 'returning'
return null
}
// 计算回收船的货舱容量
const recyclerCount = mission.fleet[ShipType.Recycler] || 0
const recyclerCapacity = 20000 // 每艘回收船容量20000
const totalCapacity = recyclerCount * recyclerCapacity
// 计算已装载的货物
const currentCargo = mission.cargo.metal + mission.cargo.crystal + mission.cargo.deuterium
// 剩余容量
const availableCapacity = totalCapacity - currentCargo
// 计算可以收集的资源
const totalDebris = debrisField.resources.metal + debrisField.resources.crystal
const collectedAmount = Math.min(totalDebris, availableCapacity)
// 按比例收集金属和晶体
const metalRatio = debrisField.resources.metal / totalDebris
const crystalRatio = debrisField.resources.crystal / totalDebris
const collectedMetal = Math.floor(collectedAmount * metalRatio)
const collectedCrystal = Math.floor(collectedAmount * crystalRatio)
// 更新任务货物
mission.cargo.metal += collectedMetal
mission.cargo.crystal += collectedCrystal
// 更新残骸场
const remainingMetal = debrisField.resources.metal - collectedMetal
const remainingCrystal = debrisField.resources.crystal - collectedCrystal
mission.status = 'returning'
return {
collectedResources: {
metal: collectedMetal,
crystal: collectedCrystal
},
remainingDebris:
remainingMetal > 0 || remainingCrystal > 0
? {
metal: remainingMetal,
crystal: remainingCrystal
}
: null
}
}
/**
* 计算行星毁灭概率
*/
export const calculateDestructionChance = (
deathstarCount: number,
planetaryShieldCount: number,
planetDefensePower: number
): number => {
// 基础摧毁概率:每艘死星 10%
let baseChance = deathstarCount * 10
// 行星护盾减少概率:每个护盾 -5%
const shieldReduction = planetaryShieldCount * 5
// 防御力量减少概率:每 10000 防御力量 -1%
const defensePowerReduction = Math.floor(planetDefensePower / 10000)
// 最终概率
let finalChance = baseChance - shieldReduction - defensePowerReduction
// 限制在 1% - 99% 之间
return Math.max(1, Math.min(99, finalChance))
}
/**
* 计算星球总防御力量
*/
export const calculatePlanetDefensePower = (
fleet: Partial<Fleet>,
defense: Partial<Record<DefenseType, number>>
): number => {
let totalPower = 0
// 计算舰队力量
Object.entries(fleet).forEach(([_shipType, count]) => {
if (count > 0) {
// 简单估算:每艘船的攻击力 + 护盾 + 装甲 / 10
totalPower += count * 100 // 简化计算
}
})
// 计算防御设施力量
Object.entries(defense).forEach(([_defenseType, count]) => {
if (count > 0) {
totalPower += count * 50 // 简化计算
}
})
return totalPower
}
/**
* 处理行星毁灭任务到达
*/
export const processDestroyArrival = (
mission: FleetMission,
targetPlanet: Planet | undefined,
attacker: Player
): { success: boolean; destructionChance: number; planetId?: string } | null => {
if (!targetPlanet || targetPlanet.ownerId === attacker.id) {
mission.status = 'returning'
return null
}
// 检查是否有死星
const deathstarCount = mission.fleet[ShipType.Deathstar] || 0
if (deathstarCount === 0) {
mission.status = 'returning'
return null
}
// 计算目标星球的防御力量
const planetaryShieldCount = targetPlanet.defense[DefenseType.PlanetaryShield] || 0
const defensePower = calculatePlanetDefensePower(targetPlanet.fleet, targetPlanet.defense)
// 计算摧毁概率
const destructionChance = calculateDestructionChance(deathstarCount, planetaryShieldCount, defensePower)
// 随机判断是否成功
const randomValue = Math.random() * 100
const success = randomValue < destructionChance
mission.status = 'returning'
return {
success,
destructionChance,
planetId: success ? targetPlanet.id : undefined
}
}
/**
* 处理舰队任务返回
*/
@@ -271,29 +457,39 @@ export const processFleetReturn = (mission: FleetMission, originPlanet: Planet):
/**
* 更新舰队任务状态
*/
export const updateFleetMissions = (
export const updateFleetMissions = async (
missions: FleetMission[],
planets: Map<string, Planet>,
debrisFields: Map<string, DebrisField>,
attacker: Player,
defender: Player | null,
now: number
): {
): Promise<{
completedMissions: string[]
battleReports: BattleResult[]
spyReports: SpyReport[]
newColonies: Planet[]
newMoons: Planet[]
} => {
newDebrisFields: DebrisField[]
updatedDebrisFields: DebrisField[]
removedDebrisFieldIds: string[]
destroyedPlanetIds: string[]
}> => {
const completedMissions: string[] = []
const battleReports: BattleResult[] = []
const spyReports: SpyReport[] = []
const newColonies: Planet[] = []
const newMoons: Planet[] = []
const newDebrisFields: DebrisField[] = []
const updatedDebrisFields: DebrisField[] = []
const removedDebrisFieldIds: string[] = []
const destroyedPlanetIds: string[] = []
// 获取所有星球列表(用于月球生成检查)
const allPlanets = Array.from(planets.values())
missions.forEach(mission => {
// 使用 for...of 以支持 await
for (const mission of missions) {
const originPlanet = attacker.planets.find(p => p.id === mission.originPlanetId)
if (mission.status === 'outbound' && now >= mission.arrivalTime) {
@@ -306,8 +502,8 @@ export const updateFleetMissions = (
processTransportArrival(mission, targetPlanet)
break
case MissionType.Attack:
const attackResult = processAttackArrival(mission, targetPlanet, attacker, defender, allPlanets)
case MissionType.Attack: {
const attackResult = await processAttackArrival(mission, targetPlanet, attacker, defender, allPlanets)
if (attackResult) {
battleReports.push(attackResult.battleResult)
if (attackResult.moon) {
@@ -316,8 +512,12 @@ export const updateFleetMissions = (
const moonKey = `${attackResult.moon.position.galaxy}:${attackResult.moon.position.system}:${attackResult.moon.position.position}`
planets.set(moonKey, attackResult.moon)
}
if (attackResult.debrisField) {
newDebrisFields.push(attackResult.debrisField)
}
}
break
}
case MissionType.Colonize:
const newColony = processColonizeArrival(mission, targetPlanet, attacker.id)
@@ -340,6 +540,36 @@ export const updateFleetMissions = (
completedMissions.push(mission.id)
}
break
case MissionType.Recycle:
const debrisId = `debris_${mission.targetPosition.galaxy}_${mission.targetPosition.system}_${mission.targetPosition.position}`
const debrisField = debrisFields.get(debrisId)
const recycleResult = processRecycleArrival(mission, debrisField)
if (recycleResult) {
if (recycleResult.remainingDebris) {
// 更新残骸场
const updatedDebris: DebrisField = {
...debrisField!,
resources: recycleResult.remainingDebris
}
debrisFields.set(debrisId, updatedDebris)
updatedDebrisFields.push(updatedDebris)
} else {
// 残骸场已被完全收集,删除
debrisFields.delete(debrisId)
removedDebrisFieldIds.push(debrisId)
}
}
break
case MissionType.Destroy:
const destroyResult = processDestroyArrival(mission, targetPlanet, attacker)
if (destroyResult && destroyResult.success && destroyResult.planetId) {
// 星球被摧毁
destroyedPlanetIds.push(destroyResult.planetId)
planets.delete(targetKey)
}
break
}
}
@@ -350,9 +580,9 @@ export const updateFleetMissions = (
}
completedMissions.push(mission.id)
}
})
}
return { completedMissions, battleReports, spyReports, newColonies, newMoons }
return { completedMissions, battleReports, spyReports, newColonies, newMoons, newDebrisFields, updatedDebrisFields, removedDebrisFieldIds, destroyedPlanetIds }
}
/**

View File

@@ -0,0 +1,93 @@
/**
* 舰队仓储逻辑模块
* 处理舰队仓储容量计算和使用量统计
*/
import type { Planet, Fleet } from '@/types/game'
import { ShipType, BuildingType, TechnologyType } from '@/types/game'
import { SHIPS, FLEET_STORAGE_CONFIG, BUILDINGS, TECHNOLOGIES } from '@/config/gameConfig'
/**
* 计算舰队当前使用的仓储量
* @param fleet 舰队对象
* @returns 当前使用的仓储量
*/
export const calculateFleetStorageUsage = (fleet: Fleet): number => {
let totalUsage = 0
for (const shipType of Object.values(ShipType)) {
const shipCount = fleet[shipType] || 0
const shipConfig = SHIPS[shipType]
totalUsage += shipCount * shipConfig.storageUsage
}
return totalUsage
}
/**
* 计算星球的最大舰队仓储容量
* @param planet 星球对象
* @param technologies 玩家的科技等级
* @returns 最大舰队仓储容量
*/
export const calculateMaxFleetStorage = (
planet: Planet,
technologies: Record<TechnologyType, number>
): number => {
// 1. 基础仓储
let maxStorage = FLEET_STORAGE_CONFIG.baseStorage
// 2. 造船厂建筑加成(每个星球独立)
const shipyardLevel = planet.buildings[BuildingType.Shipyard] || 0
const shipyardBonus = BUILDINGS[BuildingType.Shipyard].fleetStorageBonus || 0
maxStorage += shipyardLevel * shipyardBonus
// 3. 计算机技术全局加成
const computerTechLevel = technologies[TechnologyType.ComputerTechnology] || 0
const computerTechBonus = TECHNOLOGIES[TechnologyType.ComputerTechnology].fleetStorageBonus || 0
maxStorage += computerTechLevel * computerTechBonus
return maxStorage
}
/**
* 检查是否有足够的舰队仓储空间建造新舰船
* @param planet 星球对象
* @param shipType 要建造的舰船类型
* @param quantity 要建造的数量
* @param technologies 玩家的科技等级
* @returns 是否有足够的空间
*/
export const hasEnoughFleetStorage = (
planet: Planet,
shipType: ShipType,
quantity: number,
technologies: Record<TechnologyType, number>
): boolean => {
const currentUsage = calculateFleetStorageUsage(planet.fleet)
const maxStorage = calculateMaxFleetStorage(planet, technologies)
const newShipUsage = SHIPS[shipType].storageUsage * quantity
return currentUsage + newShipUsage <= maxStorage
}
/**
* 计算当前可以建造的最大舰船数量(基于仓储限制)
* @param planet 星球对象
* @param shipType 要建造的舰船类型
* @param technologies 玩家的科技等级
* @returns 最大可建造数量
*/
export const getMaxBuildableShips = (
planet: Planet,
shipType: ShipType,
technologies: Record<TechnologyType, number>
): number => {
const currentUsage = calculateFleetStorageUsage(planet.fleet)
const maxStorage = calculateMaxFleetStorage(planet, technologies)
const availableStorage = maxStorage - currentUsage
const shipStorageUsage = SHIPS[shipType].storageUsage
if (shipStorageUsage === 0) return Number.MAX_SAFE_INTEGER
return Math.floor(availableStorage / shipStorageUsage)
}

View File

@@ -3,8 +3,9 @@ import { TechnologyType, OfficerType } from '@/types/game'
import * as officerLogic from './officerLogic'
import * as buildingLogic from './buildingLogic'
import * as researchLogic from './researchLogic'
import * as resourceLogic from './resourceLogic'
import * as pointsLogic from './pointsLogic'
import * as planetLogic from './planetLogic'
import * as resourceLogic from './resourceLogic'
/**
* 初始化玩家数据
@@ -102,22 +103,27 @@ export const processGameUpdate = (
pointsLogic.addPoints(player, points)
}
// 更新所有星球
// 更新所有星球资源(直接同步计算,避免 Worker 通信开销)
player.planets.forEach(planet => {
// 更新资源
resourceLogic.updatePlanetResources(planet, now, bonuses)
})
// 更新所有星球其他状态
player.planets.forEach(planet => {
// 检查建造队列
buildingLogic.completeBuildQueue(planet, now, onPointsEarned)
// 更新星球最大空间
if (planet.isMoon) {
planet.maxSpace = planetLogic.calculateMoonMaxSpace(planet)
} else {
const terraformingTechLevel = player.technologies[TechnologyType.TerraformingTechnology] || 0
planet.maxSpace = planetLogic.calculatePlanetMaxSpace(planet, terraformingTechLevel)
}
})
// 检查研究队列
const updatedResearchQueue = researchLogic.completeResearchQueue(
player.researchQueue,
player.technologies,
now,
onPointsEarned
)
const updatedResearchQueue = researchLogic.completeResearchQueue(player.researchQueue, player.technologies, now, onPointsEarned)
return {
updatedResearchQueue

View File

@@ -1,6 +1,6 @@
import type { Planet, Resources } from '@/types/game'
import { BuildingType, ShipType, DefenseType } from '@/types/game'
import { MOON_CONFIG } from '@/config/gameConfig'
import { MOON_CONFIG, FLEET_STORAGE_CONFIG } from '@/config/gameConfig'
/**
* 计算月球生成概率
@@ -67,7 +67,8 @@ export const tryGenerateMoon = (
[ShipType.ColonyShip]: 0,
[ShipType.Recycler]: 0,
[ShipType.EspionageProbe]: 0,
[ShipType.DarkMatterHarvester]: 0
[ShipType.DarkMatterHarvester]: 0,
[ShipType.Deathstar]: 0
},
defense: {
[DefenseType.RocketLauncher]: 0,
@@ -77,11 +78,13 @@ export const tryGenerateMoon = (
[DefenseType.IonCannon]: 0,
[DefenseType.PlasmaTurret]: 0,
[DefenseType.SmallShieldDome]: 0,
[DefenseType.LargeShieldDome]: 0
[DefenseType.LargeShieldDome]: 0,
[DefenseType.PlanetaryShield]: 0
},
buildQueue: [],
lastUpdate: Date.now(),
maxSpace: MOON_CONFIG.baseSize,
maxFleetStorage: FLEET_STORAGE_CONFIG.baseStorage,
isMoon: true,
parentPlanetId: planetId
}

View File

@@ -1,6 +1,6 @@
import type { Planet, Resources } from '@/types/game'
import { ShipType, DefenseType, BuildingType } from '@/types/game'
import { MOON_CONFIG } from '@/config/gameConfig'
import { MOON_CONFIG, PLANET_CONFIG, FLEET_STORAGE_CONFIG } from '@/config/gameConfig'
/**
* 创建初始星球
@@ -29,7 +29,8 @@ export const createInitialPlanet = (playerId: string, planetName: string = 'Home
[ShipType.ColonyShip]: 0,
[ShipType.Recycler]: 0,
[ShipType.EspionageProbe]: 0,
[ShipType.DarkMatterHarvester]: 0
[ShipType.DarkMatterHarvester]: 0,
[ShipType.Deathstar]: 0
},
defense: {
[DefenseType.RocketLauncher]: 0,
@@ -39,11 +40,13 @@ export const createInitialPlanet = (playerId: string, planetName: string = 'Home
[DefenseType.IonCannon]: 0,
[DefenseType.PlasmaTurret]: 0,
[DefenseType.SmallShieldDome]: 0,
[DefenseType.LargeShieldDome]: 0
[DefenseType.LargeShieldDome]: 0,
[DefenseType.PlanetaryShield]: 0
},
buildQueue: [],
lastUpdate: Date.now(),
maxSpace: 200,
maxFleetStorage: FLEET_STORAGE_CONFIG.baseStorage,
isMoon: false
}
@@ -86,7 +89,8 @@ export const createNPCPlanet = (
[ShipType.ColonyShip]: 0,
[ShipType.Recycler]: 0,
[ShipType.EspionageProbe]: 0,
[ShipType.DarkMatterHarvester]: 0
[ShipType.DarkMatterHarvester]: 0,
[ShipType.Deathstar]: 0
},
defense: {
[DefenseType.RocketLauncher]: Math.floor(Math.random() * 100),
@@ -96,11 +100,13 @@ export const createNPCPlanet = (
[DefenseType.IonCannon]: Math.floor(Math.random() * 10),
[DefenseType.PlasmaTurret]: Math.floor(Math.random() * 5),
[DefenseType.SmallShieldDome]: Math.random() > 0.5 ? 1 : 0,
[DefenseType.LargeShieldDome]: Math.random() > 0.8 ? 1 : 0
[DefenseType.LargeShieldDome]: Math.random() > 0.8 ? 1 : 0,
[DefenseType.PlanetaryShield]: 0
},
buildQueue: [],
lastUpdate: Date.now(),
maxSpace: 200,
maxFleetStorage: FLEET_STORAGE_CONFIG.baseStorage,
isMoon: false
}
@@ -156,7 +162,8 @@ export const createMoon = (
[ShipType.ColonyShip]: 0,
[ShipType.Recycler]: 0,
[ShipType.EspionageProbe]: 0,
[ShipType.DarkMatterHarvester]: 0
[ShipType.DarkMatterHarvester]: 0,
[ShipType.Deathstar]: 0
},
defense: {
[DefenseType.RocketLauncher]: 0,
@@ -166,11 +173,13 @@ export const createMoon = (
[DefenseType.IonCannon]: 0,
[DefenseType.PlasmaTurret]: 0,
[DefenseType.SmallShieldDome]: 0,
[DefenseType.LargeShieldDome]: 0
[DefenseType.LargeShieldDome]: 0,
[DefenseType.PlanetaryShield]: 0
},
buildQueue: [],
lastUpdate: Date.now(),
maxSpace: MOON_CONFIG.baseSize,
maxFleetStorage: FLEET_STORAGE_CONFIG.baseStorage,
isMoon: true,
parentPlanetId: parentPlanet.id
}
@@ -191,3 +200,22 @@ export const calculateMoonMaxSpace = (moon: Planet): number => {
const lunarBaseLevel = moon.buildings[BuildingType.LunarBase] || 0
return MOON_CONFIG.baseSize + lunarBaseLevel * MOON_CONFIG.lunarBaseSpaceBonus
}
/**
* 计算行星空间上限
*/
export const calculatePlanetMaxSpace = (planet: Planet, terraformingTechLevel: number): number => {
if (planet.isMoon) return 0
// 基础空间
let maxSpace = PLANET_CONFIG.baseSize
// 地形改造器增加的空间
const terraformerLevel = planet.buildings[BuildingType.Terraformer] || 0
maxSpace += terraformerLevel * PLANET_CONFIG.terraformerSpaceBonus
// 地形改造技术全局增加空间
maxSpace += terraformingTechLevel * PLANET_CONFIG.terraformingTechSpaceBonus
return maxSpace
}

View File

@@ -4,11 +4,53 @@
*/
import { BuildingType, TechnologyType } from '@/types/game'
import type { Planet, Resources, Officer } from '@/types/game'
import type { Planet, Resources, Officer, BuildingConfig, TechnologyConfig } from '@/types/game'
import { OfficerType } from '@/types/game'
import * as officerLogic from '@/logic/officerLogic'
import * as resourceLogic from '@/logic/resourceLogic'
/**
* 获取特定等级的升级条件
* 合并基础 requirements 和等级门槛 levelRequirements
* @param config 建筑或科技配置
* @param targetLevel 目标等级
* @returns 合并后的前置条件
*/
export const getLevelRequirements = (
config: BuildingConfig | TechnologyConfig,
targetLevel: number
): Partial<Record<BuildingType | TechnologyType, number>> => {
const requirements: Partial<Record<BuildingType | TechnologyType, number>> = {}
// 1. 添加基础 requirements如果存在
if (config.requirements) {
Object.assign(requirements, config.requirements)
}
// 2. 添加等级门槛 requirements如果存在
if (config.levelRequirements) {
// 找出所有小于等于目标等级的门槛
const applicableLevels = Object.keys(config.levelRequirements)
.map(Number)
.filter(level => level <= targetLevel)
.sort((a, b) => a - b)
// 依次合并所有适用的等级要求(后面的覆盖前面的)
for (const level of applicableLevels) {
const levelReqs = config.levelRequirements[level]
if (levelReqs) {
// 合并要求,取最大值
for (const [key, value] of Object.entries(levelReqs)) {
const currentValue = requirements[key as BuildingType | TechnologyType] || 0
requirements[key as BuildingType | TechnologyType] = Math.max(currentValue, value)
}
}
}
}
return requirements
}
/**
* 检查建造/研发前置条件是否满足
* @param planet 星球对象

View File

@@ -52,20 +52,20 @@ export const calculateResourceProduction = (
const resourceBonus = 1 + (bonuses.resourceProductionBonus || 0) / 100
const darkMatterBonus = 1 + (bonuses.darkMatterProductionBonus || 0) / 100
// 计算电量情况
// 计算能量产出(每小时)
const energyProduction = calculateEnergyProduction(planet, { energyProductionBonus: bonuses.energyProductionBonus })
const energyConsumption = calculateEnergyConsumption(planet)
const energyBalance = energyProduction - energyConsumption
// 如果电量不足,资源产量按比例减少
const productionEfficiency = energyBalance >= 0 ? 1 : Math.max(0, energyProduction / energyConsumption)
// 检查当前能量是否充足
// 如果当前能量 <= 0矿场停止生产
const hasEnergy = planet.resources.energy > 0
const productionEfficiency = hasEnergy ? 1 : 0
return {
metal: metalMineLevel * 150 * Math.pow(1.1, metalMineLevel) * resourceBonus * productionEfficiency,
crystal: crystalMineLevel * 100 * Math.pow(1.1, crystalMineLevel) * resourceBonus * productionEfficiency,
deuterium: deuteriumSynthesizerLevel * 50 * Math.pow(1.1, deuteriumSynthesizerLevel) * resourceBonus * productionEfficiency,
darkMatter: darkMatterCollectorLevel * 2.5 * Math.pow(1.1, darkMatterCollectorLevel) * darkMatterBonus,
energy: energyBalance
metal: metalMineLevel * 1500 * Math.pow(1.5, metalMineLevel) * resourceBonus * productionEfficiency,
crystal: crystalMineLevel * 1000 * Math.pow(1.5, crystalMineLevel) * resourceBonus * productionEfficiency,
deuterium: deuteriumSynthesizerLevel * 500 * Math.pow(1.5, deuteriumSynthesizerLevel) * resourceBonus * productionEfficiency,
darkMatter: darkMatterCollectorLevel * 25 * Math.pow(1.5, darkMatterCollectorLevel) * darkMatterBonus,
energy: energyProduction
}
}
@@ -77,6 +77,7 @@ export const calculateResourceCapacity = (planet: Planet, storageCapacityBonus:
const crystalStorageLevel = planet.buildings[BuildingType.CrystalStorage] || 0
const deuteriumTankLevel = planet.buildings[BuildingType.DeuteriumTank] || 0
const darkMatterCollectorLevel = planet.buildings[BuildingType.DarkMatterCollector] || 0
const solarPlantLevel = planet.buildings[BuildingType.SolarPlant] || 0
const bonus = 1 + (storageCapacityBonus || 0) / 100
@@ -86,7 +87,7 @@ export const calculateResourceCapacity = (planet: Planet, storageCapacityBonus:
crystal: baseCapacity * Math.pow(2, crystalStorageLevel) * bonus,
deuterium: baseCapacity * Math.pow(2, deuteriumTankLevel) * bonus,
darkMatter: 1000 + darkMatterCollectorLevel * 100, // 暗物质容量较小
energy: 0 // 电量不存储,实时计算
energy: 1000 + solarPlantLevel * 500 // 能量容量基于太阳能电站等级
}
}
@@ -105,7 +106,24 @@ export const updatePlanetResources = (
): void => {
const timeDiff = (now - planet.lastUpdate) / 1000 // 转换为秒
// 计算资源产量(每小时)
// 计算能量消耗(每小时)
const energyConsumption = calculateEnergyConsumption(planet)
// 先增加能量产出
const energyProduction = calculateEnergyProduction(planet, { energyProductionBonus: bonuses.energyProductionBonus })
planet.resources.energy += (energyProduction * timeDiff) / 3600
// 限制能量上限
const capacity = calculateResourceCapacity(planet, bonuses.storageCapacityBonus)
planet.resources.energy = Math.min(planet.resources.energy, capacity.energy)
// 扣除能量消耗
planet.resources.energy -= (energyConsumption * timeDiff) / 3600
// 能量不能为负数最低为0
planet.resources.energy = Math.max(0, planet.resources.energy)
// 计算资源产量(会检查能量是否充足)
const production = calculateResourceProduction(planet, {
resourceProductionBonus: bonuses.resourceProductionBonus,
darkMatterProductionBonus: bonuses.darkMatterProductionBonus,
@@ -119,7 +137,6 @@ export const updatePlanetResources = (
planet.resources.darkMatter += (production.darkMatter * timeDiff) / 3600
// 限制资源上限
const capacity = calculateResourceCapacity(planet, bonuses.storageCapacityBonus)
planet.resources.metal = Math.min(planet.resources.metal, capacity.metal)
planet.resources.crystal = Math.min(planet.resources.crystal, capacity.crystal)
planet.resources.deuterium = Math.min(planet.resources.deuterium, capacity.deuterium)
@@ -159,3 +176,216 @@ export const addResources = (currentResources: Resources, amount: Resources): vo
currentResources.deuterium += amount.deuterium
currentResources.darkMatter += amount.darkMatter
}
/**
* 资源产量详细信息用于UI展示
*/
export interface ProductionBreakdown {
metal: ProductionDetail
crystal: ProductionDetail
deuterium: ProductionDetail
darkMatter: ProductionDetail
energy: ProductionDetail
}
export interface ProductionDetail {
baseProduction: number // 建筑基础产量
buildingLevel: number // 建筑等级
buildingName: string // 建筑名称(用于显示)
bonuses: ProductionBonus[] // 加成列表
finalProduction: number // 最终产量
}
export interface ProductionBonus {
name: string // 加成名称
value: number // 加成百分比或固定值
type: 'percentage' | 'multiplier' // 百分比加成或倍率
}
/**
* 能量消耗详细信息
*/
export interface ConsumptionBreakdown {
metalMine: ConsumptionDetail
crystalMine: ConsumptionDetail
deuteriumSynthesizer: ConsumptionDetail
total: number
}
export interface ConsumptionDetail {
buildingLevel: number
buildingName: string
consumption: number
}
/**
* 计算资源产量详细breakdown
*/
export const calculateProductionBreakdown = (
planet: Planet,
bonuses: {
resourceProductionBonus: number
darkMatterProductionBonus: number
energyProductionBonus: number
}
): ProductionBreakdown => {
const metalMineLevel = planet.buildings[BuildingType.MetalMine] || 0
const crystalMineLevel = planet.buildings[BuildingType.CrystalMine] || 0
const deuteriumSynthesizerLevel = planet.buildings[BuildingType.DeuteriumSynthesizer] || 0
const darkMatterCollectorLevel = planet.buildings[BuildingType.DarkMatterCollector] || 0
const solarPlantLevel = planet.buildings[BuildingType.SolarPlant] || 0
const hasEnergy = planet.resources.energy > 0
const productionEfficiency = hasEnergy ? 1 : 0
// 金属矿产量
const metalBase = metalMineLevel * 1500 * Math.pow(1.5, metalMineLevel)
const metalBonuses: ProductionBonus[] = []
if (bonuses.resourceProductionBonus > 0) {
metalBonuses.push({
name: 'officers.resourceBonus',
value: bonuses.resourceProductionBonus,
type: 'percentage'
})
}
if (!hasEnergy) {
metalBonuses.push({
name: 'resources.noEnergy',
value: -100,
type: 'percentage'
})
}
const metalFinal = metalBase * (1 + bonuses.resourceProductionBonus / 100) * productionEfficiency
// 晶体矿产量
const crystalBase = crystalMineLevel * 1000 * Math.pow(1.5, crystalMineLevel)
const crystalBonuses: ProductionBonus[] = []
if (bonuses.resourceProductionBonus > 0) {
crystalBonuses.push({
name: 'officers.resourceBonus',
value: bonuses.resourceProductionBonus,
type: 'percentage'
})
}
if (!hasEnergy) {
crystalBonuses.push({
name: 'resources.noEnergy',
value: -100,
type: 'percentage'
})
}
const crystalFinal = crystalBase * (1 + bonuses.resourceProductionBonus / 100) * productionEfficiency
// 重氢合成器产量
const deuteriumBase = deuteriumSynthesizerLevel * 500 * Math.pow(1.5, deuteriumSynthesizerLevel)
const deuteriumBonuses: ProductionBonus[] = []
if (bonuses.resourceProductionBonus > 0) {
deuteriumBonuses.push({
name: 'officers.resourceBonus',
value: bonuses.resourceProductionBonus,
type: 'percentage'
})
}
if (!hasEnergy) {
deuteriumBonuses.push({
name: 'resources.noEnergy',
value: -100,
type: 'percentage'
})
}
const deuteriumFinal = deuteriumBase * (1 + bonuses.resourceProductionBonus / 100) * productionEfficiency
// 暗物质收集器产量
const darkMatterBase = darkMatterCollectorLevel * 25 * Math.pow(1.5, darkMatterCollectorLevel)
const darkMatterBonuses: ProductionBonus[] = []
if (bonuses.darkMatterProductionBonus > 0) {
darkMatterBonuses.push({
name: 'officers.darkMatterBonus',
value: bonuses.darkMatterProductionBonus,
type: 'percentage'
})
}
const darkMatterFinal = darkMatterBase * (1 + bonuses.darkMatterProductionBonus / 100)
// 太阳能电站产量
const energyBase = solarPlantLevel * 50 * Math.pow(1.1, solarPlantLevel)
const energyBonuses: ProductionBonus[] = []
if (bonuses.energyProductionBonus > 0) {
energyBonuses.push({
name: 'officers.energyBonus',
value: bonuses.energyProductionBonus,
type: 'percentage'
})
}
const energyFinal = energyBase * (1 + bonuses.energyProductionBonus / 100)
return {
metal: {
baseProduction: metalBase,
buildingLevel: metalMineLevel,
buildingName: 'buildings.metalMine',
bonuses: metalBonuses,
finalProduction: metalFinal
},
crystal: {
baseProduction: crystalBase,
buildingLevel: crystalMineLevel,
buildingName: 'buildings.crystalMine',
bonuses: crystalBonuses,
finalProduction: crystalFinal
},
deuterium: {
baseProduction: deuteriumBase,
buildingLevel: deuteriumSynthesizerLevel,
buildingName: 'buildings.deuteriumSynthesizer',
bonuses: deuteriumBonuses,
finalProduction: deuteriumFinal
},
darkMatter: {
baseProduction: darkMatterBase,
buildingLevel: darkMatterCollectorLevel,
buildingName: 'buildings.darkMatterCollector',
bonuses: darkMatterBonuses,
finalProduction: darkMatterFinal
},
energy: {
baseProduction: energyBase,
buildingLevel: solarPlantLevel,
buildingName: 'buildings.solarPlant',
bonuses: energyBonuses,
finalProduction: energyFinal
}
}
}
/**
* 计算能量消耗详细breakdown
*/
export const calculateConsumptionBreakdown = (planet: Planet): ConsumptionBreakdown => {
const metalMineLevel = planet.buildings[BuildingType.MetalMine] || 0
const crystalMineLevel = planet.buildings[BuildingType.CrystalMine] || 0
const deuteriumSynthesizerLevel = planet.buildings[BuildingType.DeuteriumSynthesizer] || 0
const metalConsumption = metalMineLevel * 10 * Math.pow(1.1, metalMineLevel)
const crystalConsumption = crystalMineLevel * 10 * Math.pow(1.1, crystalMineLevel)
const deuteriumConsumption = deuteriumSynthesizerLevel * 15 * Math.pow(1.1, deuteriumSynthesizerLevel)
return {
metalMine: {
buildingLevel: metalMineLevel,
buildingName: 'buildings.metalMine',
consumption: metalConsumption
},
crystalMine: {
buildingLevel: crystalMineLevel,
buildingName: 'buildings.crystalMine',
consumption: crystalConsumption
},
deuteriumSynthesizer: {
buildingLevel: deuteriumSynthesizerLevel,
buildingName: 'buildings.deuteriumSynthesizer',
consumption: deuteriumConsumption
},
total: metalConsumption + crystalConsumption + deuteriumConsumption
}
}

View File

@@ -4,6 +4,7 @@ import * as shipLogic from './shipLogic'
import * as resourceLogic from './resourceLogic'
import * as officerLogic from './officerLogic'
import * as publicLogic from './publicLogic'
import * as fleetStorageLogic from './fleetStorageLogic'
/**
* 验证舰船建造的所有条件
@@ -29,6 +30,11 @@ export const validateShipBuild = (
return { valid: false, reason: 'errors.insufficientResources' }
}
// 检查舰队仓储空间
if (!fleetStorageLogic.hasEnoughFleetStorage(planet, shipType, quantity, technologies as Record<TechnologyType, number>)) {
return { valid: false, reason: 'errors.insufficientFleetStorage' }
}
return { valid: true }
}

View File

@@ -1,7 +1,7 @@
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
history: createWebHistory(),
routes: [
{ path: '/', name: 'overview', component: () => import('@/views/OverviewView.vue') },
{ path: '/buildings', name: 'buildings', component: () => import('@/views/BuildingsView.vue') },
@@ -13,7 +13,20 @@ const router = createRouter({
{ path: '/battle-simulator', name: 'battle-simulator', component: () => import('@/views/BattleSimulatorView.vue') },
{ path: '/messages', name: 'messages', component: () => import('@/views/MessagesView.vue') },
{ path: '/galaxy', name: 'galaxy', component: () => import('@/views/GalaxyView.vue') },
{ path: '/settings', name: 'settings', component: () => import('@/views/SettingsView.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('/')
}
}
}
]
})

View File

@@ -22,9 +22,7 @@ export const useGameStore = defineStore('game', {
} as Player,
currentPlanetId: '',
isDark: '',
locale: 'zh-CN' as Locale,
sidebarCollapsed: window.innerWidth < 1024 ? false : true,
universePlanets: {} as Record<string, Planet>
locale: 'zh-CN' as Locale
}),
getters: {
currentPlanet(): Planet | undefined {

View File

@@ -0,0 +1,26 @@
import { defineStore } from 'pinia'
import type { Planet, DebrisField } from '@/types/game'
import pkg from '../../package.json'
import { encryptData, decryptData } from '@/utils/crypto'
/**
* 宇宙地图 Store
* 存储宇宙中的所有星球和残骸场
* 使用普通 localStorage 存储,不加密(地图数据是静态/共享数据)
*/
export const useUniverseStore = defineStore('universe', {
state: () => ({
// 宇宙星球地图key 格式为 "galaxy:system:position"
planets: {} as Record<string, Planet>,
// 残骸场key 格式为 "galaxy:system:position"
debrisFields: {} as Record<string, DebrisField>
}),
persist: {
key: `${pkg.name}-universe`,
storage: localStorage,
serializer: {
serialize: state => encryptData(state),
deserialize: value => decryptData(value)
}
}
})

View File

@@ -21,10 +21,13 @@ export const BuildingType = {
CrystalStorage: 'crystalStorage',
DeuteriumTank: 'deuteriumTank',
DarkMatterCollector: 'darkMatterCollector', // 暗物质收集器
Terraformer: 'terraformer', // 地形改造器
// 月球专属建筑
LunarBase: 'lunarBase', // 月球基地
SensorPhalanx: 'sensorPhalanx', // 传感器阵列
JumpGate: 'jumpGate' // 跳跃门
JumpGate: 'jumpGate', // 跳跃门
// 特殊建筑
PlanetDestroyerFactory: 'planetDestroyerFactory' // 行星毁灭者工厂
} as const
export type BuildingType = (typeof BuildingType)[keyof typeof BuildingType]
@@ -38,9 +41,12 @@ export interface BuildingConfig {
baseTime: number // 基础建造时间(秒)
costMultiplier: number // 升级成本倍数
spaceUsage: number // 占用空间
fleetStorageBonus?: number // 每级增加的舰队仓储(可选)
planetOnly?: boolean // 仅行星可建造
moonOnly?: boolean // 仅月球可建造
requirements?: Partial<Record<BuildingType | TechnologyType, number>> // 前置条件
maxLevel?: number // 最大等级(可选,不设置则无上限)
requirements?: Partial<Record<BuildingType | TechnologyType, number>> // 前置条件(初始解锁)
levelRequirements?: Record<number, Partial<Record<BuildingType | TechnologyType, number>>> // 等级升级条件
}
// 建筑实例
@@ -60,7 +66,9 @@ export const TechnologyType = {
CombustionDrive: 'combustionDrive',
ImpulseDrive: 'impulseDrive',
HyperspaceDrive: 'hyperspaceDrive',
DarkMatterTechnology: 'darkMatterTechnology' // 暗物质技术
DarkMatterTechnology: 'darkMatterTechnology', // 暗物质技术
TerraformingTechnology: 'terraformingTechnology', // 地形改造技术
PlanetDestructionTech: 'planetDestructionTech' // 行星毁灭技术
} as const
export type TechnologyType = (typeof TechnologyType)[keyof typeof TechnologyType]
@@ -73,7 +81,10 @@ export interface TechnologyConfig {
baseCost: Resources
baseTime: number
costMultiplier: number
requirements?: Partial<Record<BuildingType | TechnologyType, number>>
fleetStorageBonus?: number // 每级增加的舰队仓储(全局,可选)
maxLevel?: number // 最大等级(可选,不设置则无上限)
requirements?: Partial<Record<BuildingType | TechnologyType, number>> // 前置条件(初始解锁)
levelRequirements?: Record<number, Partial<Record<BuildingType | TechnologyType, number>>> // 等级升级条件
}
// 科技实例
@@ -91,7 +102,8 @@ export const DefenseType = {
IonCannon: 'ionCannon',
PlasmaTurret: 'plasmaTurret',
SmallShieldDome: 'smallShieldDome',
LargeShieldDome: 'largeShieldDome'
LargeShieldDome: 'largeShieldDome',
PlanetaryShield: 'planetaryShield' // 行星护盾
} as const
export type DefenseType = (typeof DefenseType)[keyof typeof DefenseType]
@@ -120,7 +132,8 @@ export const ShipType = {
ColonyShip: 'colonyShip',
Recycler: 'recycler',
EspionageProbe: 'espionageProbe',
DarkMatterHarvester: 'darkMatterHarvester' // 暗物质采集船
DarkMatterHarvester: 'darkMatterHarvester', // 暗物质采集船
Deathstar: 'deathstar' // 死星
} as const
export type ShipType = (typeof ShipType)[keyof typeof ShipType]
@@ -138,6 +151,7 @@ export interface ShipConfig {
armor: number
speed: number
fuelConsumption: number
storageUsage: number // 占用舰队仓储
requirements?: Partial<Record<BuildingType | TechnologyType, number>>
}
@@ -153,6 +167,7 @@ export interface Fleet {
[ShipType.Recycler]: number
[ShipType.EspionageProbe]: number
[ShipType.DarkMatterHarvester]: number
[ShipType.Deathstar]: number
}
// 舰队任务类型
@@ -163,7 +178,9 @@ export const MissionType = {
Spy: 'spy',
Deploy: 'deploy',
Expedition: 'expedition',
HarvestDarkMatter: 'harvestDarkMatter' // 暗物质采集
HarvestDarkMatter: 'harvestDarkMatter', // 暗物质采集
Recycle: 'recycle', // 回收残骸
Destroy: 'destroy' // 行星毁灭
} as const
export type MissionType = (typeof MissionType)[keyof typeof MissionType]
@@ -201,8 +218,27 @@ export interface BattleResult {
defense: Partial<Record<DefenseType, number>>
}
winner: 'attacker' | 'defender' | 'draw'
read?: boolean // 已读状态
plunder: Resources
debrisField: Resources
// 新增详细信息
rounds?: number
attackerRemaining?: Partial<Fleet>
defenderRemaining?: {
fleet: Partial<Fleet>
defense: Partial<Record<DefenseType, number>>
}
roundDetails?: Array<{
round: number
attackerLosses: Partial<Fleet>
defenderLosses: {
fleet: Partial<Fleet>
defense: Partial<Record<DefenseType, number>>
}
attackerRemainingPower: number
defenderRemainingPower: number
}>
moonChance?: number // 月球生成概率
}
// 间谍报告
@@ -218,6 +254,16 @@ export interface SpyReport {
buildings?: Partial<Record<BuildingType, number>>
technologies?: Partial<Record<TechnologyType, number>>
detectionChance: number
read?: boolean // 已读状态
}
// 残骸场
export interface DebrisField {
id: string
position: { galaxy: number; system: number; position: number }
resources: Pick<Resources, 'metal' | 'crystal'> // 残骸场只包含金属和晶体
createdAt: number
expiresAt?: number // 可选的过期时间
}
// 建造队列项
@@ -244,6 +290,7 @@ export interface Planet {
buildQueue: BuildQueueItem[]
lastUpdate: number
maxSpace: number // 最大空间
maxFleetStorage: number // 舰队仓储上限
isMoon: boolean // 是否为月球
parentPlanetId?: string // 如果是月球,指向母星的ID
}

126
src/types/worker.ts Normal file
View File

@@ -0,0 +1,126 @@
import type { Fleet, Resources } from './game'
import { DefenseType } from './game'
/**
* Worker 消息类型
*/
export const WorkerMessageType = {
// 战斗模拟相关
SIMULATE_BATTLE: 'SIMULATE_BATTLE',
CALCULATE_PLUNDER: 'CALCULATE_PLUNDER',
CALCULATE_DEBRIS: 'CALCULATE_DEBRIS',
// 通用响应
SUCCESS: 'SUCCESS',
ERROR: 'ERROR'
} as const
export type WorkerMessageType = (typeof WorkerMessageType)[keyof typeof WorkerMessageType]
/**
* 战斗方数据
*/
export interface BattleSideData {
ships: Partial<Fleet>
defense?: Partial<Record<DefenseType, number>>
weaponTech?: number
shieldTech?: number
armorTech?: number
}
/**
* 战斗模拟结果
*/
export interface BattleSimulationResult {
winner: 'attacker' | 'defender' | 'draw'
rounds: number
attackerLosses: Partial<Fleet>
defenderLosses: {
fleet: Partial<Fleet>
defense: Partial<Record<DefenseType, number>>
}
attackerRemaining: Partial<Fleet>
defenderRemaining: {
fleet: Partial<Fleet>
defense: Partial<Record<DefenseType, number>>
}
roundDetails: Array<{
round: number
attackerLosses: Partial<Fleet>
defenderLosses: {
fleet: Partial<Fleet>
defense: Partial<Record<DefenseType, number>>
}
attackerRemainingPower: number
defenderRemainingPower: number
}>
}
/**
* Worker 消息基础接口
*/
export interface WorkerMessageBase {
id: string
type: WorkerMessageType
}
/**
* Worker 请求消息
*/
export interface WorkerRequestMessage extends WorkerMessageBase {
payload: unknown
}
/**
* Worker 响应消息
*/
export interface WorkerResponseMessage extends WorkerMessageBase {
success: boolean
data?: unknown
error?: string
}
/**
* 战斗模拟请求
*/
export interface SimulateBattleRequest extends WorkerRequestMessage {
type: typeof WorkerMessageType.SIMULATE_BATTLE
payload: {
attacker: BattleSideData
defender: BattleSideData
maxRounds?: number
}
}
/**
* 掠夺计算请求
*/
export interface CalculatePlunderRequest extends WorkerRequestMessage {
type: typeof WorkerMessageType.CALCULATE_PLUNDER
payload: {
defenderResources: Resources
attackerFleet: Partial<Fleet>
}
}
/**
* 残骸场计算请求
*/
export interface CalculateDebrisRequest extends WorkerRequestMessage {
type: typeof WorkerMessageType.CALCULATE_DEBRIS
payload: {
attackerLosses: Partial<Fleet>
defenderLosses: {
fleet: Partial<Fleet>
defense: Partial<Record<DefenseType, number>>
}
}
}
/**
* 所有 Worker 请求类型
*/
export type WorkerRequest =
| SimulateBattleRequest
| CalculatePlunderRequest
| CalculateDebrisRequest

View File

@@ -5,11 +5,12 @@ import pkg from '../../package.json'
export const encryptData = (data: any): string => {
try {
const jsonStr = JSON.stringify(data)
return CryptoJS.AES.encrypt(jsonStr, pkg.name).toString()
const encrypted = CryptoJS.AES.encrypt(jsonStr, pkg.name).toString()
return encrypted
} catch (error) {
console.error('数据加密失败:', error)
return ''
console.error(error)
}
return ''
}
// 数据解密
@@ -19,7 +20,7 @@ export const decryptData = (data: string): any => {
const decryptedStr = bytes.toString(CryptoJS.enc.Utf8)
return JSON.parse(decryptedStr)
} catch (error) {
console.error('数据解密失败:', error)
console.error(error)
return {}
}
}
}

View File

@@ -29,15 +29,19 @@ export const getResourceColor = (current: number, max: number): string => {
}
/**
* 格式化时间(秒转为时分秒)
* 格式化时间(秒转为时分秒)
* @param seconds 秒数
* @param units 时间单位 {hour, minute, second}
* @returns 格式化后的时间字符串(双位数格式,例如 00:05:08
* @returns 格式化后的时间字符串(例如 2d 05:30:15 或 05:30:15
*/
export const formatTime = (seconds: number): string => {
const hours = Math.floor(seconds / 3600)
const days = Math.floor(seconds / 86400)
const hours = Math.floor((seconds % 86400) / 3600)
const minutes = Math.floor((seconds % 3600) / 60)
const secs = Math.floor(seconds % 60)
if (days > 0) {
return `${days}:${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
}
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
}

74
src/utils/migration.ts Normal file
View File

@@ -0,0 +1,74 @@
import type { Planet, DebrisField } from '@/types/game'
import { decryptData, encryptData } from './crypto'
import pkg from '../../package.json'
/**
* 数据迁移工具
* 用于从旧版本数据结构迁移到新版本
*/
/**
* 执行数据迁移
* 将旧版本的 universePlanets 和 debrisFields 从 gameStore 迁移到 universeStore
*/
export const migrateGameData = (): void => {
try {
const storageKey = pkg.name
const universeStorageKey = `${pkg.name}-universe`
// 读取旧的加密存档
const oldEncryptedData = localStorage.getItem(storageKey)
if (!oldEncryptedData) return
// 尝试解密(如果是加密格式)
let oldData: any
try {
oldData = decryptData(oldEncryptedData)
} catch {
// 解密失败,可能是新格式(未加密),直接解析
try {
oldData = JSON.parse(oldEncryptedData)
} catch {
return // 无法解析,放弃迁移
}
}
// 检查是否需要迁移
const hasOldMapData = oldData.universePlanets || oldData.debrisFields
if (!hasOldMapData) return
// 准备 universeStore 数据
const universeData: {
planets: Record<string, Planet>
debrisFields: Record<string, DebrisField>
} = {
planets: {},
debrisFields: {}
}
// 迁移星球数据(排除玩家星球)
if (oldData.universePlanets) {
const oldPlanets = oldData.universePlanets as Record<string, Planet>
const playerPlanets = oldData.player?.planets || []
const playerPlanetIds = new Set(playerPlanets.map((p: Planet) => p.id))
Object.entries(oldPlanets).forEach(([key, planet]) => {
// 只迁移非玩家星球
if (!playerPlanetIds.has(planet.id)) {
universeData.planets[key] = planet
}
})
delete oldData.universePlanets
}
// 迁移残骸场数据
if (oldData.debrisFields) {
universeData.debrisFields = oldData.debrisFields
delete oldData.debrisFields
}
// 保存迁移后的数据
localStorage.setItem(universeStorageKey, encryptData(universeData))
localStorage.setItem(storageKey, encryptData(oldData))
} catch (error) {
console.error(error)
}
}

View File

@@ -140,242 +140,24 @@
</div>
<!-- 战斗结果对话框 -->
<Dialog v-model:open="showResultDialog">
<DialogContent class="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle class="flex items-center gap-2">
<Trophy class="h-5 w-5" />
{{ t('simulatorView.battleResult') }}
</DialogTitle>
</DialogHeader>
<div v-if="simulationResult" class="space-y-4">
<!-- 胜利者 -->
<div class="text-center p-4 rounded-lg" :class="getWinnerStyle(simulationResult.winner)">
<p class="text-lg font-bold">
{{
simulationResult.winner === 'attacker'
? t('simulatorView.attackerVictory')
: simulationResult.winner === 'defender'
? t('simulatorView.defenderVictory')
: t('simulatorView.draw')
}}
</p>
<p class="text-sm mt-1">{{ t('simulatorView.afterRounds').replace('{rounds}', String(battleRounds)) }}</p>
</div>
<!-- 损失对比 -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- 攻击方损失 -->
<div class="space-y-2">
<p class="text-sm font-medium text-red-600 dark:text-red-400">{{ t('simulatorView.attackerLosses') }}</p>
<div class="p-3 bg-muted rounded-lg space-y-1 text-xs">
<div v-for="(count, shipType) in simulationResult.attackerLosses" :key="shipType">
<span class="text-muted-foreground">{{ SHIPS[shipType].name }}:</span>
<span class="ml-2 font-medium">{{ count }}</span>
</div>
<p v-if="Object.keys(simulationResult.attackerLosses).length === 0" class="text-muted-foreground">
{{ t('simulatorView.noLosses') }}
</p>
</div>
</div>
<!-- 防守方损失 -->
<div class="space-y-2">
<p class="text-sm font-medium text-red-600 dark:text-red-400">{{ t('simulatorView.defenderLosses') }}</p>
<div class="p-3 bg-muted rounded-lg space-y-1 text-xs">
<div v-for="(count, shipType) in simulationResult.defenderLosses.fleet" :key="shipType">
<span class="text-muted-foreground">{{ SHIPS[shipType].name }}:</span>
<span class="ml-2 font-medium">{{ count }}</span>
</div>
<div v-for="(count, defenseType) in simulationResult.defenderLosses.defense" :key="defenseType">
<span class="text-muted-foreground">{{ DEFENSES[defenseType].name }}:</span>
<span class="ml-2 font-medium">{{ count }}</span>
</div>
<p
v-if="
Object.keys(simulationResult.defenderLosses.fleet).length === 0 &&
Object.keys(simulationResult.defenderLosses.defense).length === 0
"
class="text-muted-foreground"
>
{{ t('simulatorView.noLosses') }}
</p>
</div>
</div>
</div>
<!-- 剩余单位 -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- 攻击方剩余 -->
<div class="space-y-2">
<p class="text-sm font-medium text-blue-600 dark:text-blue-400">{{ t('simulatorView.attackerRemaining') }}</p>
<div class="p-3 bg-muted rounded-lg space-y-1 text-xs">
<div v-for="(count, shipType) in attackerRemaining" :key="shipType">
<span class="text-muted-foreground">{{ SHIPS[shipType].name }}:</span>
<span class="ml-2 font-medium">{{ count }}</span>
</div>
<p v-if="Object.keys(attackerRemaining).length === 0" class="text-muted-foreground">
{{ t('simulatorView.allDestroyed') }}
</p>
</div>
</div>
<!-- 防守方剩余 -->
<div class="space-y-2">
<p class="text-sm font-medium text-blue-600 dark:text-blue-400">{{ t('simulatorView.defenderRemaining') }}</p>
<div class="p-3 bg-muted rounded-lg space-y-1 text-xs">
<div v-for="(count, shipType) in defenderRemaining.fleet" :key="shipType">
<span class="text-muted-foreground">{{ SHIPS[shipType].name }}:</span>
<span class="ml-2 font-medium">{{ count }}</span>
</div>
<div v-for="(count, defenseType) in defenderRemaining.defense" :key="defenseType">
<span class="text-muted-foreground">{{ DEFENSES[defenseType].name }}:</span>
<span class="ml-2 font-medium">{{ count }}</span>
</div>
<p
v-if="Object.keys(defenderRemaining.fleet).length === 0 && Object.keys(defenderRemaining.defense).length === 0"
class="text-muted-foreground"
>
{{ t('simulatorView.allDestroyed') }}
</p>
</div>
</div>
</div>
<!-- 战利品和残骸 -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- 掠夺资源 -->
<div
v-if="plunder.metal > 0 || plunder.crystal > 0 || plunder.deuterium > 0"
class="p-3 bg-green-50 dark:bg-green-950 rounded-lg"
>
<p class="text-sm font-medium mb-2 text-green-600 dark:text-green-400">{{ t('simulatorView.plunderableResources') }}</p>
<div class="flex flex-wrap gap-3 text-xs">
<span v-if="plunder.metal > 0" class="flex items-center gap-1">
<ResourceIcon type="metal" size="sm" />
{{ formatNumber(plunder.metal) }}
</span>
<span v-if="plunder.crystal > 0" class="flex items-center gap-1">
<ResourceIcon type="crystal" size="sm" />
{{ formatNumber(plunder.crystal) }}
</span>
<span v-if="plunder.deuterium > 0" class="flex items-center gap-1">
<ResourceIcon type="deuterium" size="sm" />
{{ formatNumber(plunder.deuterium) }}
</span>
</div>
</div>
<!-- 残骸场 -->
<div v-if="debrisField.metal > 0 || debrisField.crystal > 0" class="p-3 bg-muted rounded-lg">
<p class="text-sm font-medium mb-2">{{ t('simulatorView.debrisField') }}</p>
<div class="flex flex-wrap gap-3 text-xs">
<span v-if="debrisField.metal > 0" class="flex items-center gap-1">
<ResourceIcon type="metal" size="sm" />
{{ formatNumber(debrisField.metal) }}
</span>
<span v-if="debrisField.crystal > 0" class="flex items-center gap-1">
<ResourceIcon type="crystal" size="sm" />
{{ formatNumber(debrisField.crystal) }}
</span>
</div>
<!-- 月球生成概率 -->
<p v-if="moonChance > 0" class="text-xs text-muted-foreground mt-2">{{ t('simulatorView.moonChance') }}: {{ moonChance }}%</p>
</div>
</div>
<!-- 回合详情 -->
<div class="space-y-2">
<Button @click="showRoundDetails = !showRoundDetails" variant="outline" size="sm" class="w-full">
{{ showRoundDetails ? t('simulatorView.hideRoundDetails') : t('simulatorView.showRoundDetails') }}
</Button>
<div v-if="showRoundDetails" class="relative pl-6 space-y-4">
<!-- 时间线 -->
<div class="absolute left-2 top-0 bottom-0 w-0.5 bg-border" />
<div v-for="detail in roundDetails" :key="detail.round" class="relative">
<!-- 时间线节点 -->
<div class="absolute -left-6 top-3 w-4 h-4 rounded-full bg-primary border-2 border-background" />
<!-- 回合内容卡片 -->
<div class="border rounded-lg p-3 bg-card hover:shadow-md transition-shadow">
<div class="flex items-center justify-between mb-3">
<p class="text-sm font-semibold">{{ t('simulatorView.round').replace('{round}', String(detail.round)) }}</p>
<div class="flex gap-3 text-xs text-muted-foreground">
<span class="flex items-center gap-1" :title="t('simulatorView.attackerRemainingPower')">
<Sword class="h-3 w-3" />
{{ formatNumber(detail.attackerRemainingPower) }}
</span>
<span class="flex items-center gap-1" :title="t('simulatorView.defenderRemainingPower')">
<Shield class="h-3 w-3" />
{{ formatNumber(detail.defenderRemainingPower) }}
</span>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<!-- 攻击方本回合损失 -->
<div class="bg-red-50 dark:bg-red-950/20 rounded p-2">
<p class="text-xs font-medium text-red-600 dark:text-red-400 mb-1.5">{{ t('simulatorView.attackerLosses') }}</p>
<div class="text-xs space-y-0.5">
<div v-for="(count, shipType) in detail.attackerLosses" :key="shipType" class="flex justify-between">
<span class="text-muted-foreground">{{ SHIPS[shipType].name }}</span>
<span class="font-medium">-{{ count }}</span>
</div>
<p v-if="Object.keys(detail.attackerLosses).length === 0" class="text-muted-foreground italic">
{{ t('simulatorView.noLosses') }}
</p>
</div>
</div>
<!-- 防守方本回合损失 -->
<div class="bg-blue-50 dark:bg-blue-950/20 rounded p-2">
<p class="text-xs font-medium text-blue-600 dark:text-blue-400 mb-1.5">{{ t('simulatorView.defenderLosses') }}</p>
<div class="text-xs space-y-0.5">
<div v-for="(count, shipType) in detail.defenderLosses.fleet" :key="shipType" class="flex justify-between">
<span class="text-muted-foreground">{{ SHIPS[shipType].name }}</span>
<span class="font-medium">-{{ count }}</span>
</div>
<div v-for="(count, defenseType) in detail.defenderLosses.defense" :key="defenseType" class="flex justify-between">
<span class="text-muted-foreground">{{ DEFENSES[defenseType].name }}</span>
<span class="font-medium">-{{ count }}</span>
</div>
<p
v-if="
Object.keys(detail.defenderLosses.fleet).length === 0 && Object.keys(detail.defenderLosses.defense).length === 0
"
class="text-muted-foreground italic"
>
{{ t('simulatorView.noLosses') }}
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</DialogContent>
</Dialog>
<BattleReportDialog v-model:open="showResultDialog" :report="simulationResult" />
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ref, toRaw } from 'vue'
import { useI18n } from '@/composables/useI18n'
import { useGameConfig } from '@/composables/useGameConfig'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { ShipType, DefenseType } from '@/types/game'
import type { Fleet, BattleResult, Resources } from '@/types/game'
import { simulateBattle, calculatePlunder, calculateDebrisField } from '@/utils/battleSimulator'
import type { Fleet, BattleResult } from '@/types/game'
import { workerManager } from '@/workers/workerManager'
import ResourceIcon from '@/components/ResourceIcon.vue'
import { formatNumber } from '@/utils/format'
import { Sword, Shield, Zap, RotateCcw, Trophy } from 'lucide-vue-next'
import BattleReportDialog from '@/components/BattleReportDialog.vue'
import { Sword, Shield, Zap, RotateCcw } from 'lucide-vue-next'
import * as planetLogic from '@/logic/planetLogic'
const { t } = useI18n()
@@ -444,78 +226,45 @@
// 模拟结果
const simulationResult = ref<BattleResult | null>(null)
const battleRounds = ref<number>(0)
const attackerRemaining = ref<Partial<Fleet>>({})
const defenderRemaining = ref<{ fleet: Partial<Fleet>; defense: Partial<Record<DefenseType, number>> }>({
fleet: {},
defense: {}
})
const roundDetails = ref<
Array<{
round: number
attackerLosses: Partial<Fleet>
defenderLosses: {
fleet: Partial<Fleet>
defense: Partial<Record<DefenseType, number>>
}
attackerRemainingPower: number
defenderRemainingPower: number
}>
>([])
const showRoundDetails = ref<boolean>(false)
const showResultDialog = ref<boolean>(false)
// 计算掠夺资源
const plunder = computed(() => {
if (!simulationResult.value || simulationResult.value.winner !== 'attacker') {
return { metal: 0, crystal: 0, deuterium: 0, darkMatter: 0, energy: 0 }
}
return calculatePlunder(defenderResources.value, attackerFleet.value)
})
// 计算残骸场
const debrisField = computed(() => {
if (!simulationResult.value) {
return { metal: 0, crystal: 0, deuterium: 0, darkMatter: 0, energy: 0 }
}
return calculateDebrisField(simulationResult.value.attackerLosses, simulationResult.value.defenderLosses)
})
const calculateMoonChance = (debrisField: Resources): number => {
return planetLogic.calculateMoonChance(debrisField)
}
// 计算月球生成概率
const moonChance = computed(() => {
if (!debrisField.value) return 0
return calculateMoonChance(debrisField.value)
})
// 运行模拟
const runSimulation = () => {
// 运行模拟(使用 Web Worker 进行计算)
const runSimulation = async () => {
// 使用 toRaw 将 Vue 响应式对象转换为普通对象,以便传递给 Worker
const attackerSide = {
ships: attackerFleet.value,
ships: toRaw(attackerFleet.value),
weaponTech: attackerTech.value.weapon,
shieldTech: attackerTech.value.shield,
armorTech: attackerTech.value.armor
}
const defenderSide = {
ships: defenderFleet.value,
defense: defenderDefense.value,
ships: toRaw(defenderFleet.value),
defense: toRaw(defenderDefense.value),
weaponTech: defenderTech.value.weapon,
shieldTech: defenderTech.value.shield,
armorTech: defenderTech.value.armor
}
const result = simulateBattle(attackerSide, defenderSide)
// 使用 Worker 执行战斗模拟
const result = await workerManager.simulateBattle({
attacker: attackerSide,
defender: defenderSide
})
// 保存回合数和剩余单位
battleRounds.value = result.rounds
attackerRemaining.value = result.attackerRemaining
defenderRemaining.value = result.defenderRemaining
roundDetails.value = result.roundDetails
showRoundDetails.value = false
// 计算掠夺和残骸场
const plunder =
result.winner === 'attacker'
? await workerManager.calculatePlunder({
defenderResources: toRaw(defenderResources.value),
attackerFleet: result.attackerRemaining
})
: { metal: 0, crystal: 0, deuterium: 0, darkMatter: 0, energy: 0 }
const debrisField = await workerManager.calculateDebris({
attackerLosses: result.attackerLosses,
defenderLosses: result.defenderLosses
})
const moonChance = planetLogic.calculateMoonChance(debrisField) / 100 // 转换为 0-1 范围
simulationResult.value = {
id: `sim_${Date.now()}`,
@@ -530,8 +279,13 @@
attackerLosses: result.attackerLosses,
defenderLosses: result.defenderLosses,
winner: result.winner,
plunder: plunder.value,
debrisField: debrisField.value
plunder,
debrisField,
rounds: result.rounds,
attackerRemaining: result.attackerRemaining,
defenderRemaining: result.defenderRemaining,
roundDetails: result.roundDetails,
moonChance
}
// 显示结果对话框
@@ -552,18 +306,6 @@
attackerTech.value = { weapon: 0, shield: 0, armor: 0 }
defenderTech.value = { weapon: 0, shield: 0, armor: 0 }
simulationResult.value = null
battleRounds.value = 0
attackerRemaining.value = {}
defenderRemaining.value = { fleet: {}, defense: {} }
roundDetails.value = []
showRoundDetails.value = false
showResultDialog.value = false
}
// 获取胜利者样式
const getWinnerStyle = (winner: string) => {
if (winner === 'attacker') return 'bg-green-50 dark:bg-green-950 text-green-700 dark:text-green-300'
if (winner === 'defender') return 'bg-red-50 dark:bg-red-950 text-red-700 dark:text-red-300'
return 'bg-gray-50 dark:bg-gray-950 text-gray-700 dark:text-gray-300'
}
</script>

View File

@@ -11,7 +11,10 @@
</div>
<div class="grid grid-cols-2 lg:grid-cols-3 gap-3 sm:gap-4">
<Card v-for="buildingType in availableBuildings" :key="buildingType">
<Card v-for="buildingType in availableBuildings" :key="buildingType" class="relative">
<!-- 前置条件遮罩 -->
<CardUnlockOverlay :requirements="BUILDINGS[buildingType].requirements" :currentLevel="getBuildingLevel(buildingType)" />
<CardHeader>
<div class="flex justify-between items-start gap-2">
<div class="min-w-0 flex-1">
@@ -89,7 +92,7 @@
<!-- 升级按钮 -->
<Button @click="handleUpgrade(buildingType)" :disabled="!canUpgrade(buildingType)" class="w-full">
{{ t('buildingsView.upgrade') }}
{{ getUpgradeButtonText(buildingType) }}
</Button>
<!-- 拆除按钮 -->
@@ -128,22 +131,24 @@
import { useI18n } from '@/composables/useI18n'
import { useGameConfig } from '@/composables/useGameConfig'
import { computed, ref } from 'vue'
import { BuildingType } from '@/types/game'
import { BuildingType, TechnologyType } from '@/types/game'
import type { Resources, Planet } from '@/types/game'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import ResourceIcon from '@/components/ResourceIcon.vue'
import CardUnlockOverlay from '@/components/CardUnlockOverlay.vue'
import AlertDialog from '@/components/AlertDialog.vue'
import { Clock, Grid3x3 } from 'lucide-vue-next'
import { formatNumber, formatTime, getResourceCostColor } from '@/utils/format'
import * as buildingLogic from '@/logic/buildingLogic'
import * as buildingValidation from '@/logic/buildingValidation'
import * as publicLogic from '@/logic/publicLogic'
const gameStore = useGameStore()
const detailDialog = useDetailDialogStore()
const { t } = useI18n()
const { BUILDINGS } = useGameConfig()
const { BUILDINGS, TECHNOLOGIES } = useGameConfig()
const planet = computed(() => gameStore.currentPlanet)
const alertDialog = ref<InstanceType<typeof AlertDialog> | null>(null)
@@ -182,6 +187,15 @@
// 升级建筑
const handleUpgrade = (buildingType: BuildingType) => {
// 检查前置条件
if (!checkUpgradeRequirements(buildingType)) {
alertDialog.value?.show({
title: t('common.requirementsNotMet'),
message: getRequirementsList(buildingType)
})
return
}
const success = upgradeBuilding(buildingType)
if (!success) {
alertDialog.value?.show({
@@ -196,12 +210,98 @@
return planet.value?.buildings[buildingType] || 0
}
// 检查升级前置条件是否满足
const checkUpgradeRequirements = (buildingType: BuildingType): boolean => {
if (!planet.value) return false
const config = BUILDINGS.value[buildingType]
const currentLevel = getBuildingLevel(buildingType)
const targetLevel = currentLevel + 1
// 获取目标等级的所有前置条件(包括等级门槛)
const requirements = publicLogic.getLevelRequirements(config, targetLevel)
if (!requirements || Object.keys(requirements).length === 0) return true
return publicLogic.checkRequirements(planet.value, gameStore.player.technologies, requirements)
}
// 获取升级按钮文本
const getUpgradeButtonText = (buildingType: BuildingType): string => {
if (!planet.value) return t('buildingsView.upgrade')
const config = BUILDINGS.value[buildingType]
const currentLevel = getBuildingLevel(buildingType)
// 检查是否达到等级上限
if (config.maxLevel !== undefined && currentLevel >= config.maxLevel) {
return t('buildingsView.maxLevelReached') // "等级已满"
}
if (planet.value.buildQueue.length > 0) return t('buildingsView.upgrade')
// 检查前置条件
if (!checkUpgradeRequirements(buildingType)) {
return t('buildingsView.requirementsNotMet')
}
return t('buildingsView.upgrade')
}
// 获取前置条件列表文本
const getRequirementsList = (buildingType: BuildingType): string => {
const config = BUILDINGS.value[buildingType]
const currentLevel = getBuildingLevel(buildingType)
const targetLevel = currentLevel + 1
// 获取目标等级的所有前置条件(包括等级门槛)
const requirements = publicLogic.getLevelRequirements(config, targetLevel)
if (!requirements || !planet.value) return ''
const lines: string[] = []
for (const [key, requiredLevel] of Object.entries(requirements)) {
// 检查是否为建筑类型
if (Object.values(BuildingType).includes(key as BuildingType)) {
const bt = key as BuildingType
const currentLevel = planet.value.buildings[bt] || 0
const name = BUILDINGS.value[bt]?.name || bt
const status = currentLevel >= requiredLevel ? '✓' : '✗'
lines.push(`${status} ${name}: Lv ${requiredLevel} (${t('common.current')}: Lv ${currentLevel})`)
}
// 检查是否为科技类型
else if (Object.values(TechnologyType).includes(key as TechnologyType)) {
const tt = key as TechnologyType
const currentLevel = gameStore.player.technologies[tt] || 0
const name = TECHNOLOGIES.value[tt]?.name || tt
const status = currentLevel >= requiredLevel ? '✓' : '✗'
lines.push(`${status} ${name}: Lv ${requiredLevel} (${t('common.current')}: Lv ${currentLevel})`)
}
}
return lines.join('\n')
}
// 检查是否可以升级
const canUpgrade = (buildingType: BuildingType): boolean => {
if (!planet.value) return false
const config = BUILDINGS.value[buildingType]
const currentLevel = getBuildingLevel(buildingType)
// 检查是否达到等级上限
if (config.maxLevel !== undefined && currentLevel >= config.maxLevel) {
return false
}
if (planet.value.buildQueue.length > 0) return false
const currentLevel = getBuildingLevel(buildingType)
// 检查前置条件
const validation = buildingValidation.validateBuildingUpgrade(
planet.value,
buildingType,
gameStore.player.technologies,
gameStore.player.officers
)
if (!validation.valid) return false
const cost = getBuildingCost(buildingType, currentLevel + 1)
return (

View File

@@ -183,7 +183,8 @@
[DefenseType.IonCannon]: 0,
[DefenseType.PlasmaTurret]: 0,
[DefenseType.SmallShieldDome]: 0,
[DefenseType.LargeShieldDome]: 0
[DefenseType.LargeShieldDome]: 0,
[DefenseType.PlanetaryShield]: 0
})
// 判断是否为护盾罩

View File

@@ -296,9 +296,10 @@
<script setup lang="ts">
import { useGameStore } from '@/stores/gameStore'
import { useUniverseStore } from '@/stores/universeStore'
import { useI18n } from '@/composables/useI18n'
import { useGameConfig } from '@/composables/useGameConfig'
import { computed, ref, onMounted } from 'vue'
import { computed, ref, onMounted, onUnmounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ShipType, MissionType, BuildingType } from '@/types/game'
import type { Fleet, Resources } from '@/types/game'
@@ -311,7 +312,7 @@
import ResourceIcon from '@/components/ResourceIcon.vue'
import AlertDialog from '@/components/AlertDialog.vue'
import UnlockRequirement from '@/components/UnlockRequirement.vue'
import { Sword, Package, Rocket as RocketIcon, Eye, Users } from 'lucide-vue-next'
import { Sword, Package, Rocket as RocketIcon, Eye, Users, Recycle, Skull } from 'lucide-vue-next'
import { formatNumber, formatTime } from '@/utils/format'
import * as shipValidation from '@/logic/shipValidation'
import * as fleetLogic from '@/logic/fleetLogic'
@@ -322,11 +323,16 @@
const route = useRoute()
const router = useRouter()
const gameStore = useGameStore()
const universeStore = useUniverseStore()
const { t } = useI18n()
const { SHIPS } = useGameConfig()
const planet = computed(() => gameStore.currentPlanet)
const alertDialog = ref<InstanceType<typeof AlertDialog> | null>(null)
// 当前时间(响应式)
const currentTime = ref(Date.now())
let timeInterval: number | null = null
// 计算最大舰队任务槽位
const maxFleetMissions = computed(() => {
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, Date.now())
@@ -345,7 +351,9 @@
[ShipType.LargeCargo]: 0,
[ShipType.ColonyShip]: 0,
[ShipType.Recycler]: 0,
[ShipType.EspionageProbe]: 0
[ShipType.EspionageProbe]: 0,
[ShipType.DarkMatterHarvester]: 0,
[ShipType.Deathstar]: 0
})
// 目标坐标
@@ -359,6 +367,11 @@
// 从 URL query 参数初始化
onMounted(() => {
// 启动定时器更新当前时间
timeInterval = window.setInterval(() => {
currentTime.value = Date.now()
}, 1000) // 每秒更新一次
const { galaxy, system, position, mission } = route.query
// 如果有参数,填充数据
@@ -385,13 +398,22 @@
}
})
// 清理定时器
onUnmounted(() => {
if (timeInterval) {
clearInterval(timeInterval)
}
})
// 可用任务类型
const availableMissions = computed(() => [
{ type: MissionType.Attack, name: t('fleetView.attackMission'), icon: Sword },
{ type: MissionType.Transport, name: t('fleetView.transport'), icon: Package },
{ type: MissionType.Colonize, name: t('fleetView.colonize'), icon: RocketIcon },
{ type: MissionType.Spy, name: t('fleetView.spy'), icon: Eye },
{ type: MissionType.Deploy, name: t('fleetView.deploy'), icon: Users }
{ type: MissionType.Deploy, name: t('fleetView.deploy'), icon: Users },
{ type: MissionType.Recycle, name: t('fleetView.recycle'), icon: Recycle },
{ type: MissionType.Destroy, name: t('fleetView.destroy'), icon: Skull }
])
// 获取任务名称
@@ -439,24 +461,53 @@
}
// 检查是否可以派遣
const canSendFleet = (): boolean => {
const canSendFleet = (): { valid: boolean; errorKey?: string } => {
// 检查是否选择了舰船
const hasShips = Object.values(selectedFleet.value).some(count => count > 0)
if (!hasShips) return false
if (!hasShips) return { valid: false, errorKey: 'fleetView.noShipsSelected' }
// 检查是否派遣到自己的星球
if (planet.value) {
const isSamePlanet =
targetPosition.value.galaxy === planet.value.position.galaxy &&
targetPosition.value.system === planet.value.position.system &&
targetPosition.value.position === planet.value.position.position
if (isSamePlanet) {
return { valid: false, errorKey: 'fleetView.cannotSendToOwnPlanet' }
}
}
// 检查载货量
if (selectedMission.value === MissionType.Transport) {
if (getTotalCargo() > getTotalCargoCapacity()) return false
if (getTotalCargo() > getTotalCargoCapacity()) {
return { valid: false, errorKey: 'fleetView.cargoExceedsCapacity' }
}
}
// 检查殖民船
if (selectedMission.value === MissionType.Colonize) {
if (!selectedFleet.value[ShipType.ColonyShip] || (selectedFleet.value[ShipType.ColonyShip] ?? 0) < 1) {
return false
return { valid: false, errorKey: 'fleetView.noColonyShip' }
}
}
return true
// 检查回收任务是否有残骸
if (selectedMission.value === MissionType.Recycle) {
const debrisId = `debris_${targetPosition.value.galaxy}_${targetPosition.value.system}_${targetPosition.value.position}`
const debrisField = universeStore.debrisFields[debrisId]
if (!debrisField || (debrisField.resources.metal === 0 && debrisField.resources.crystal === 0)) {
return { valid: false, errorKey: 'fleetView.noDebrisAtTarget' }
}
}
// 检查毁灭任务是否有死星
if (selectedMission.value === MissionType.Destroy) {
if (!selectedFleet.value[ShipType.Deathstar] || (selectedFleet.value[ShipType.Deathstar] ?? 0) < 1) {
return { valid: false, errorKey: 'fleetView.noDeathstar' }
}
}
return { valid: true }
}
const sendFleet = (
@@ -498,6 +549,16 @@
const handleSendFleet = () => {
if (!planet.value) return
// 验证是否可以派遣
const validation = canSendFleet()
if (!validation.valid) {
alertDialog.value?.show({
title: t('fleetView.sendFailed'),
message: validation.errorKey ? t(validation.errorKey) : t('fleetView.sendFailedMessage')
})
return
}
// 过滤出实际选择的舰船
const fleet: Partial<Fleet> = {}
for (const [shipType, count] of Object.entries(selectedFleet.value)) {
@@ -547,23 +608,23 @@
// 获取任务剩余时间
const getRemainingTime = (mission: any): number => {
const now = Date.now()
const now = currentTime.value
const targetTime = mission.status === 'outbound' ? mission.arrivalTime : mission.returnTime
return Math.max(0, (targetTime - now) / 1000)
}
// 获取任务进度
const getMissionProgress = (mission: any): number => {
const now = Date.now()
const now = currentTime.value
if (mission.status === 'outbound') {
const total = mission.arrivalTime - mission.departureTime
const elapsed = now - mission.departureTime
return Math.min(100, (elapsed / total) * 100)
return Math.max(0, Math.min(100, (elapsed / total) * 100))
} else {
const departTime = mission.arrivalTime
const total = mission.returnTime - departTime
const elapsed = now - departTime
return Math.min(100, (elapsed / total) * 100)
return Math.max(0, Math.min(100, (elapsed / total) * 100))
}
}
</script>

291
src/views/GMView.vue Normal file
View File

@@ -0,0 +1,291 @@
<template>
<div 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>
</div>
<!-- 星球选择 -->
<Card>
<CardHeader>
<CardTitle>{{ t('gmView.selectPlanet') }}</CardTitle>
</CardHeader>
<CardContent>
<Select v-model="selectedPlanetId">
<SelectTrigger>
<SelectValue :placeholder="t('gmView.choosePlanet')" />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="planet in gameStore.player.planets" :key="planet.id" :value="planet.id">
{{ planet.name }} ({{ planet.position.galaxy }}:{{ planet.position.system }}:{{ planet.position.position }})
</SelectItem>
</SelectContent>
</Select>
</CardContent>
</Card>
<!-- 标签切换 -->
<div v-if="selectedPlanet" class="flex flex-wrap gap-2 border-b">
<Button @click="activeTab = 'resources'" :variant="activeTab === 'resources' ? 'default' : 'ghost'" class="rounded-b-none">
{{ t('gmView.resources') }}
</Button>
<Button @click="activeTab = 'buildings'" :variant="activeTab === 'buildings' ? 'default' : 'ghost'" class="rounded-b-none">
{{ t('gmView.buildings') }}
</Button>
<Button @click="activeTab = 'research'" :variant="activeTab === 'research' ? 'default' : 'ghost'" class="rounded-b-none">
{{ t('gmView.research') }}
</Button>
<Button @click="activeTab = 'ships'" :variant="activeTab === 'ships' ? 'default' : 'ghost'" class="rounded-b-none">
{{ t('gmView.ships') }}
</Button>
<Button @click="activeTab = 'defense'" :variant="activeTab === 'defense' ? 'default' : 'ghost'" class="rounded-b-none">
{{ t('gmView.defense') }}
</Button>
<Button @click="activeTab = 'officers'" :variant="activeTab === 'officers' ? 'default' : 'ghost'" class="rounded-b-none">
{{ t('gmView.officers') }}
</Button>
</div>
<!-- 资源 -->
<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>
<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>
</CardContent>
</Card>
</div>
<!-- 科技 -->
<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>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
<!-- 舰船 -->
<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>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
<!-- 防御 -->
<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>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
<!-- 军官 -->
<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>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
<!-- 危险操作 -->
<Card class="border-destructive">
<CardHeader>
<CardTitle class="text-destructive">{{ t('gmView.dangerZone') }}</CardTitle>
<CardDescription>{{ t('gmView.dangerZoneDesc') }}</CardDescription>
</CardHeader>
<CardContent class="space-y-2">
<Button @click="resetGame" variant="destructive" class="w-full">{{ t('gmView.resetGame') }}</Button>
</CardContent>
</Card>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useGameStore } from '@/stores/gameStore'
import { useI18n } from '@/composables/useI18n'
import { useGameConfig } from '@/composables/useGameConfig'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Badge } from '@/components/ui/badge'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { BuildingType, TechnologyType, ShipType, DefenseType, OfficerType } from '@/types/game'
const gameStore = useGameStore()
const { t } = useI18n()
const { BUILDINGS, TECHNOLOGIES, SHIPS, DEFENSES, OFFICERS } = useGameConfig()
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>)
// 初始化军官天数显示
Object.values(OfficerType).forEach(officer => {
const officerData = gameStore.player.officers[officer]
if (officerData && officerData.expiresAt) {
const daysLeft = Math.ceil((officerData.expiresAt - Date.now()) / (1000 * 60 * 60 * 24))
officerDays.value[officer] = Math.max(0, daysLeft)
} else {
officerDays.value[officer] = 0
}
})
const selectedPlanet = computed(() => {
return gameStore.player.planets.find(p => p.id === selectedPlanetId.value)
})
const resourceTypes = ['metal', 'crystal', 'deuterium', 'darkMatter'] as const
const buildingTypes = Object.values(BuildingType)
const technologyTypes = Object.values(TechnologyType)
const shipTypes = Object.values(ShipType)
const defenseTypes = Object.values(DefenseType)
const officerTypes = Object.values(OfficerType)
const setResourceAmount = (resource: string, amount: number) => {
if (selectedPlanet.value) {
selectedPlanet.value.resources[resource as keyof typeof selectedPlanet.value.resources] += amount
}
}
const setBuildingLevel = (building: BuildingType, level: number) => {
if (selectedPlanet.value) {
selectedPlanet.value.buildings[building] = level
}
}
const setTechnologyLevel = (tech: TechnologyType, level: number) => {
gameStore.player.technologies[tech] = level
}
const setShipCount = (ship: ShipType, count: number) => {
if (selectedPlanet.value) {
selectedPlanet.value.fleet[ship] = (selectedPlanet.value.fleet[ship] || 0) + count
}
}
const setDefenseCount = (defense: DefenseType, count: number) => {
if (selectedPlanet.value) {
selectedPlanet.value.defense[defense] = (selectedPlanet.value.defense[defense] || 0) + count
}
}
const setOfficerDays = (officer: OfficerType, days: number) => {
officerDays.value[officer] = days
const now = Date.now()
const expiresAt = now + days * 24 * 60 * 60 * 1000
if (!gameStore.player.officers[officer]) {
gameStore.player.officers[officer] = {
type: officer,
active: true,
hiredAt: now,
expiresAt: expiresAt
}
} else {
gameStore.player.officers[officer].expiresAt = expiresAt
gameStore.player.officers[officer].active = true
if (!gameStore.player.officers[officer].hiredAt) {
gameStore.player.officers[officer].hiredAt = now
}
}
}
const resetGame = () => {
if (confirm(t('gmView.resetGameConfirm'))) {
localStorage.clear()
location.reload()
}
}
</script>

View File

@@ -94,50 +94,79 @@
</p>
</div>
<div v-else class="text-sm text-muted-foreground">{{ t('galaxyView.emptySlot') }}</div>
<!-- 残骸场信息 -->
<div v-if="getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)" class="mt-2 p-2 bg-amber-50 dark:bg-amber-950/30 border border-amber-200 dark:border-amber-800 rounded text-xs">
<div class="flex items-center gap-2 text-amber-700 dark:text-amber-400 font-medium mb-1">
<span>{{ t('galaxyView.debrisField') }}</span>
</div>
<div class="flex gap-3 text-xs">
<span class="flex items-center gap-1">
<span class="text-muted-foreground">{{ t('resources.metal') }}:</span>
<span class="font-medium">{{ formatNumber(getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)!.resources.metal) }}</span>
</span>
<span class="flex items-center gap-1">
<span class="text-muted-foreground">{{ t('resources.crystal') }}:</span>
<span class="font-medium">{{ formatNumber(getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)!.resources.crystal) }}</span>
</span>
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="flex gap-1 sm:gap-2 flex-shrink-0">
<Button
v-if="slot.planet && !isMyPlanet(slot.planet)"
@click="showPlanetActions(slot.planet, 'spy')"
variant="outline"
size="sm"
class="h-8 w-8 p-0"
:title="t('galaxyView.scout')"
>
<Eye class="h-3 w-3 sm:h-4 sm:w-4" />
</Button>
<Button
v-if="slot.planet && !isMyPlanet(slot.planet)"
@click="showPlanetActions(slot.planet, 'attack')"
variant="outline"
size="sm"
class="h-8 w-8 p-0"
:title="t('galaxyView.attack')"
>
<Sword class="h-3 w-3 sm:h-4 sm:w-4" />
</Button>
<Button
v-if="!slot.planet"
@click="showPlanetActions(null, 'colonize', slot.position)"
variant="outline"
size="sm"
class="h-8 w-8 p-0"
:title="t('galaxyView.colonize')"
>
<Rocket class="h-3 w-3 sm:h-4 sm:w-4" />
</Button>
<Button
v-if="slot.planet && isMyPlanet(slot.planet)"
@click="switchToPlanet(slot.planet.id)"
variant="outline"
size="sm"
class="h-8 w-8 p-0"
:title="t('galaxyView.switch')"
>
<Home class="h-3 w-3 sm:h-4 sm:w-4" />
</Button>
<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 sm:h-4 sm:w-4" />
</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 sm:h-4 sm:w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{{ t('galaxyView.attack') }}</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 sm:h-4 sm:w-4" />
</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 sm:h-4 sm:w-4" />
</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 sm:h-4 sm:w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{{ t('galaxyView.recycle') }}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</div>
</div>
@@ -151,20 +180,24 @@
<script setup lang="ts">
import { useGameStore } from '@/stores/gameStore'
import { useUniverseStore } from '@/stores/universeStore'
import { useI18n } from '@/composables/useI18n'
import { ref, onMounted } from 'vue'
import type { Planet } from '@/types/game'
import type { Planet, DebrisField } from '@/types/game'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Label } from '@/components/ui/label'
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 AlertDialog from '@/components/AlertDialog.vue'
import { Home, Eye, Sword, Rocket } from 'lucide-vue-next'
import { Home, Eye, Sword, Rocket, Recycle } from 'lucide-vue-next'
import { useRouter } from 'vue-router'
import * as gameLogic from '@/logic/gameLogic'
import { formatNumber } from '@/utils/format'
const gameStore = useGameStore()
const universeStore = useUniverseStore()
const router = useRouter()
const { t } = useI18n()
const actionDialog = ref<InstanceType<typeof AlertDialog> | null>(null)
@@ -191,11 +224,22 @@
const positions = gameLogic.generateSystemPositions(galaxy, system)
return positions.map(pos => {
const key = gameLogic.generatePositionKey(galaxy, system, pos.position)
const planet = gameStore.universePlanets[key] || null
// 先从玩家星球中查找,再从宇宙地图中查找
const planet = gameStore.player.planets.find(p =>
p.position.galaxy === galaxy &&
p.position.system === system &&
p.position.position === pos.position
) || universeStore.planets[key] || null
return { position: pos.position, planet }
})
}
// 获取指定位置的残骸场
const getDebrisFieldAt = (galaxy: number, system: number, position: number): DebrisField | null => {
const debrisId = `debris_${galaxy}_${system}_${position}`
return universeStore.debrisFields[debrisId] || null
}
// 加载星系
const loadSystem = () => {
currentGalaxy.value = selectedGalaxy.value
@@ -223,11 +267,11 @@
// 切换到指定星球
const switchToPlanet = (planetId: string) => {
gameStore.currentPlanetId = planetId
router.push('/overview')
router.push('/')
}
// 显示星球操作
const showPlanetActions = (planet: Planet | null, action: 'spy' | 'attack' | 'colonize', position?: number) => {
const showPlanetActions = (planet: Planet | null, action: 'spy' | 'attack' | 'colonize' | 'recycle', position?: number) => {
const targetPos = planet ? planet.position : { galaxy: currentGalaxy.value, system: currentSystem.value, position: position! }
const coordinates = `${targetPos.galaxy}:${targetPos.system}:${targetPos.position}`
@@ -242,6 +286,9 @@
} else if (action === 'colonize') {
title = t('galaxyView.colonizePlanetTitle')
message = t('galaxyView.colonizePlanetMessage').replace('{coordinates}', coordinates)
} else if (action === 'recycle') {
title = t('galaxyView.recyclePlanetTitle')
message = t('galaxyView.recyclePlanetMessage').replace('{coordinates}', coordinates)
}
actionDialog.value?.show({

View File

@@ -1,239 +1,119 @@
<template>
<div class="container mx-auto p-4 sm:p-6 space-y-4 sm:space-y-6">
<div class="container mx-auto p-4 sm:p-6 space-y-6">
<h1 class="text-2xl sm:text-3xl font-bold">{{ t('messagesView.title') }}</h1>
<!-- 标签切换 -->
<div class="flex gap-2 border-b">
<Button @click="activeTab = 'battles'" :variant="activeTab === 'battles' ? 'default' : 'ghost'" class="rounded-b-none">
{{ t('messagesView.battleReports') }}
<Badge v-if="gameStore.player.battleReports.length > 0" variant="secondary" class="ml-1">
{{ gameStore.player.battleReports.length }}
</Badge>
<Sword class="h-4 w-4 mr-2" />
{{ t('messagesView.battles') }}
<Badge v-if="unreadBattles > 0" variant="destructive" class="ml-2">{{ unreadBattles }}</Badge>
</Button>
<Button @click="activeTab = 'spy'" :variant="activeTab === 'spy' ? 'default' : 'ghost'" class="rounded-b-none">
{{ t('messagesView.spyReports') }}
<Badge v-if="gameStore.player.spyReports.length > 0" variant="secondary" class="ml-1">
{{ gameStore.player.spyReports.length }}
</Badge>
<Eye class="h-4 w-4 mr-2" />
{{ t('messagesView.spy') }}
<Badge v-if="unreadSpyReports > 0" variant="destructive" class="ml-2">{{ unreadSpyReports }}</Badge>
</Button>
</div>
<!-- 战斗报告 -->
<div v-if="activeTab === 'battles'" class="space-y-4">
<!-- 战斗报告列表 -->
<div v-if="activeTab === 'battles'" class="space-y-2">
<Card v-if="gameStore.player.battleReports.length === 0">
<CardContent class="py-8 text-center text-muted-foreground">{{ t('messagesView.noBattleReports') }}</CardContent>
</Card>
<Card v-for="report in sortedBattleReports" :key="report.id">
<CardHeader>
<div class="flex justify-between items-start gap-2">
<div class="min-w-0 flex-1">
<Card
v-for="report in sortedBattleReports"
:key="report.id"
@click="openBattleReport(report)"
class="cursor-pointer hover:shadow-md transition-shadow"
>
<CardHeader class="pb-3">
<div class="flex justify-between items-center gap-2">
<div class="flex items-center gap-2 min-w-0 flex-1">
<Sword class="h-4 w-4 flex-shrink-0" />
<CardTitle class="text-base sm:text-lg">{{ t('messagesView.battleReport') }}</CardTitle>
<CardDescription class="text-xs sm:text-sm">
{{ formatDate(report.timestamp) }}
</CardDescription>
<Badge v-if="!report.read" variant="default" class="text-xs">{{ t('messagesView.unread') }}</Badge>
<Badge
:variant="report.winner === 'attacker' ? 'default' : report.winner === 'defender' ? 'destructive' : 'secondary'"
class="text-xs"
>
{{ report.winner === 'attacker' ? t('messagesView.victory') : report.winner === 'defender' ? t('messagesView.defeat') : t('messagesView.draw') }}
</Badge>
</div>
<Badge :variant="report.winner === 'attacker' ? 'default' : report.winner === 'defender' ? 'destructive' : 'secondary'">
{{ report.winner === 'attacker' ? t('messagesView.victory') : report.winner === 'defender' ? t('messagesView.defeat') : t('messagesView.draw') }}
</Badge>
<Button @click.stop="deleteBattleReport(report.id)" variant="ghost" size="icon" class="h-8 w-8 flex-shrink-0">
<X class="h-4 w-4" />
</Button>
</div>
<CardDescription class="text-xs sm:text-sm">
{{ formatDate(report.timestamp) }}
</CardDescription>
</CardHeader>
<CardContent class="space-y-4">
<!-- 攻击方舰队 -->
<div>
<p class="text-sm font-medium mb-2">{{ t('messagesView.attackerFleet') }}:</p>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2">
<div v-for="(count, shipType) in report.attackerFleet" :key="shipType" class="text-xs sm:text-sm">
<span class="text-muted-foreground">{{ SHIPS[shipType].name }}:</span>
<span class="ml-1 font-medium">{{ count }}</span>
</div>
</div>
</div>
<!-- 防守方舰队 -->
<div>
<p class="text-sm font-medium mb-2">{{ t('messagesView.defenderFleet') }}:</p>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2">
<div v-for="(count, shipType) in report.defenderFleet" :key="shipType" class="text-xs sm:text-sm">
<span class="text-muted-foreground">{{ SHIPS[shipType].name }}:</span>
<span class="ml-1 font-medium">{{ count }}</span>
</div>
</div>
</div>
<!-- 防守方防御 -->
<div v-if="hasDefense(report.defenderDefense)">
<p class="text-sm font-medium mb-2">{{ t('messagesView.defenderDefense') }}:</p>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2">
<div v-for="(count, defenseType) in report.defenderDefense" :key="defenseType" class="text-xs sm:text-sm">
<span class="text-muted-foreground">{{ DEFENSES[defenseType].name }}:</span>
<span class="ml-1 font-medium">{{ count }}</span>
</div>
</div>
</div>
<!-- 损失 -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="p-3 bg-muted rounded-lg">
<p class="text-sm font-medium mb-2 text-red-600">{{ t('messagesView.attackerLosses') }}:</p>
<div class="space-y-1 text-xs">
<div v-for="(count, shipType) in report.attackerLosses" :key="shipType">{{ SHIPS[shipType].name }}: {{ count }}</div>
<p v-if="Object.keys(report.attackerLosses).length === 0" class="text-muted-foreground">{{ t('messagesView.noLosses') }}</p>
</div>
</div>
<div class="p-3 bg-muted rounded-lg">
<p class="text-sm font-medium mb-2 text-red-600">{{ t('messagesView.defenderLosses') }}:</p>
<div class="space-y-1 text-xs">
<div v-for="(count, shipType) in report.defenderLosses.fleet" :key="shipType">{{ SHIPS[shipType].name }}: {{ count }}</div>
<div v-for="(count, defenseType) in report.defenderLosses.defense" :key="defenseType">
{{ DEFENSES[defenseType].name }}: {{ count }}
</div>
<p
v-if="Object.keys(report.defenderLosses.fleet).length === 0 && Object.keys(report.defenderLosses.defense).length === 0"
class="text-muted-foreground"
>
{{ t('messagesView.noLosses') }}
</p>
</div>
</div>
</div>
<!-- 掠夺资源 -->
<div
v-if="report.plunder.metal > 0 || report.plunder.crystal > 0 || report.plunder.deuterium > 0"
class="p-3 bg-green-50 dark:bg-green-950 rounded-lg"
>
<p class="text-sm font-medium mb-2 text-green-600">{{ t('messagesView.plunder') }}:</p>
<div class="flex flex-wrap gap-3 text-xs sm:text-sm">
<span v-if="report.plunder.metal > 0" class="flex items-center gap-1">
<ResourceIcon type="metal" size="sm" />
{{ formatNumber(report.plunder.metal) }}
</span>
<span v-if="report.plunder.crystal > 0" class="flex items-center gap-1">
<ResourceIcon type="crystal" size="sm" />
{{ formatNumber(report.plunder.crystal) }}
</span>
<span v-if="report.plunder.deuterium > 0" class="flex items-center gap-1">
<ResourceIcon type="deuterium" size="sm" />
{{ formatNumber(report.plunder.deuterium) }}
</span>
</div>
</div>
<!-- 残骸场 -->
<div v-if="report.debrisField.metal > 0 || report.debrisField.crystal > 0" class="p-3 bg-muted rounded-lg">
<p class="text-sm font-medium mb-2">{{ t('messagesView.debrisField') }}:</p>
<div class="flex flex-wrap gap-3 text-xs sm:text-sm">
<span v-if="report.debrisField.metal > 0" class="flex items-center gap-1">
<ResourceIcon type="metal" size="sm" />
{{ formatNumber(report.debrisField.metal) }}
</span>
<span v-if="report.debrisField.crystal > 0" class="flex items-center gap-1">
<ResourceIcon type="crystal" size="sm" />
{{ formatNumber(report.debrisField.crystal) }}
</span>
</div>
</div>
</CardContent>
</Card>
</div>
<!-- 间谍报告 -->
<div v-if="activeTab === 'spy'" class="space-y-4">
<!-- 间谍报告列表 -->
<div v-if="activeTab === 'spy'" class="space-y-2">
<Card v-if="gameStore.player.spyReports.length === 0">
<CardContent class="py-8 text-center text-muted-foreground">{{ t('messagesView.noSpyReports') }}</CardContent>
</Card>
<Card v-for="report in sortedSpyReports" :key="report.id">
<CardHeader>
<div class="flex justify-between items-start gap-2">
<div>
<Card
v-for="report in sortedSpyReports"
:key="report.id"
@click="openSpyReport(report)"
class="cursor-pointer hover:shadow-md transition-shadow"
>
<CardHeader class="pb-3">
<div class="flex justify-between items-center gap-2">
<div class="flex items-center gap-2 min-w-0 flex-1">
<Eye class="h-4 w-4 flex-shrink-0" />
<CardTitle class="text-base sm:text-lg">{{ t('messagesView.spyReport') }}</CardTitle>
<CardDescription class="text-xs sm:text-sm">
{{ formatDate(report.timestamp) }}
</CardDescription>
<Badge v-if="!report.read" variant="default" class="text-xs">{{ t('messagesView.unread') }}</Badge>
<Badge variant="outline" class="text-xs">{{ report.targetPlanetId }}</Badge>
</div>
<Badge variant="outline">
{{ report.targetPlanetId }}
</Badge>
<Button @click.stop="deleteSpyReport(report.id)" variant="ghost" size="icon" class="h-8 w-8 flex-shrink-0">
<X class="h-4 w-4" />
</Button>
</div>
<CardDescription class="text-xs sm:text-sm">
{{ formatDate(report.timestamp) }}
</CardDescription>
</CardHeader>
<CardContent class="space-y-4">
<!-- 资源 -->
<div>
<p class="text-sm font-medium mb-2">{{ t('messagesView.resources') }}:</p>
<div class="flex flex-wrap gap-3 text-xs sm:text-sm">
<span class="flex items-center gap-1">
<ResourceIcon type="metal" size="sm" />
{{ formatNumber(report.resources.metal) }}
</span>
<span class="flex items-center gap-1">
<ResourceIcon type="crystal" size="sm" />
{{ formatNumber(report.resources.crystal) }}
</span>
<span class="flex items-center gap-1">
<ResourceIcon type="deuterium" size="sm" />
{{ formatNumber(report.resources.deuterium) }}
</span>
</div>
</div>
<!-- 舰队 -->
<div v-if="report.fleet">
<p class="text-sm font-medium mb-2">{{ t('messagesView.fleet') }}:</p>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2">
<div v-for="(count, shipType) in report.fleet" :key="shipType" class="text-xs sm:text-sm">
<span class="text-muted-foreground">{{ SHIPS[shipType].name }}:</span>
<span class="ml-1 font-medium">{{ count }}</span>
</div>
</div>
</div>
<!-- 防御 -->
<div v-if="report.defense && hasDefense(report.defense)">
<p class="text-sm font-medium mb-2">{{ t('messagesView.defense') }}:</p>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2">
<div v-for="(count, defenseType) in report.defense" :key="defenseType" class="text-xs sm:text-sm">
<span class="text-muted-foreground">{{ DEFENSES[defenseType].name }}:</span>
<span class="ml-1 font-medium">{{ count }}</span>
</div>
</div>
</div>
<!-- 建筑 -->
<div v-if="report.buildings">
<p class="text-sm font-medium mb-2">{{ t('messagesView.buildings') }}:</p>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2">
<template v-for="(level, buildingType) in report.buildings" :key="buildingType">
<div v-if="level && level > 0" class="text-xs sm:text-sm">
<span class="text-muted-foreground">{{ BUILDINGS[buildingType].name }}:</span>
<span class="ml-1 font-medium">Lv {{ level }}</span>
</div>
</template>
</div>
</div>
</CardContent>
</Card>
</div>
<!-- 战斗报告对话框 -->
<BattleReportDialog v-model:open="showBattleDialog" :report="selectedBattleReport" />
<!-- 间谍报告对话框 -->
<SpyReportDialog v-model:open="showSpyDialog" :report="selectedSpyReport" />
</div>
</template>
<script setup lang="ts">
import { useGameStore } from '@/stores/gameStore'
import { useI18n } from '@/composables/useI18n'
import { useGameConfig } from '@/composables/useGameConfig'
import { computed, ref } from 'vue'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import ResourceIcon from '@/components/ResourceIcon.vue'
import { formatNumber, formatDate } from '@/utils/format'
import BattleReportDialog from '@/components/BattleReportDialog.vue'
import SpyReportDialog from '@/components/SpyReportDialog.vue'
import { formatDate } from '@/utils/format'
import { X, Sword, Eye } from 'lucide-vue-next'
import type { BattleResult, SpyReport } from '@/types/game'
const gameStore = useGameStore()
const { t } = useI18n()
const { SHIPS, DEFENSES, BUILDINGS } = useGameConfig()
const activeTab = ref<'battles' | 'spy'>('battles')
// 对话框状态
const showBattleDialog = ref(false)
const showSpyDialog = ref(false)
const selectedBattleReport = ref<BattleResult | null>(null)
const selectedSpyReport = ref<SpyReport | null>(null)
// 排序后的战斗报告(最新的在前)
const sortedBattleReports = computed(() => {
return [...gameStore.player.battleReports].sort((a, b) => b.timestamp - a.timestamp)
@@ -244,9 +124,49 @@
return [...gameStore.player.spyReports].sort((a, b) => b.timestamp - a.timestamp)
})
// 检查是否有防御设施
const hasDefense = (defense: any): boolean => {
if (!defense) return false
return Object.values(defense).some((count: any) => count > 0)
// 未读战斗报告数量
const unreadBattles = computed(() => {
return gameStore.player.battleReports.filter(r => !r.read).length
})
// 未读间谍报告数量
const unreadSpyReports = computed(() => {
return gameStore.player.spyReports.filter(r => !r.read).length
})
// 打开战斗报告
const openBattleReport = (report: BattleResult) => {
selectedBattleReport.value = report
showBattleDialog.value = true
// 标记为已读
if (!report.read) {
report.read = true
}
}
// 打开间谍报告
const openSpyReport = (report: SpyReport) => {
selectedSpyReport.value = report
showSpyDialog.value = true
// 标记为已读
if (!report.read) {
report.read = true
}
}
// 删除战斗报告
const deleteBattleReport = (reportId: string) => {
const index = gameStore.player.battleReports.findIndex(r => r.id === reportId)
if (index > -1) {
gameStore.player.battleReports.splice(index, 1)
}
}
// 删除间谍报告
const deleteSpyReport = (reportId: string) => {
const index = gameStore.player.spyReports.findIndex(r => r.id === reportId)
if (index > -1) {
gameStore.player.spyReports.splice(index, 1)
}
}
</script>

View File

@@ -34,6 +34,7 @@
<TableHead class="text-right">{{ t('resources.current') }}</TableHead>
<TableHead class="text-right">{{ t('resources.max') }}</TableHead>
<TableHead class="text-right">{{ t('resources.production') }}{{ t('resources.perHour') }}</TableHead>
<TableHead class="text-right">{{ t('resources.consumption') }}{{ t('resources.perHour') }}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -44,40 +45,139 @@
{{ t(`resources.${resourceType.key}`) }}
</div>
</TableCell>
<!-- 电量特殊显示 -->
<template v-if="resourceType.key === 'energy'">
<TableCell
class="text-right"
:class="planet.resources[resourceType.key] >= 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'"
>
{{ formatNumber(planet.resources[resourceType.key]) }}
</TableCell>
<TableCell class="text-right text-muted-foreground">-</TableCell>
<TableCell class="text-right text-muted-foreground">
{{ formatNumber(energyProduction) }} / {{ formatNumber(energyConsumption) }}
</TableCell>
</template>
<!-- 其他资源正常显示 -->
<template v-else>
<TableCell
class="text-right"
:class="getResourceColor(planet.resources[resourceType.key], capacity?.[resourceType.key] || Infinity)"
>
{{ formatNumber(planet.resources[resourceType.key]) }}
</TableCell>
<TableCell class="text-right text-muted-foreground">
{{ formatNumber(capacity?.[resourceType.key] || 0) }}
</TableCell>
<TableCell class="text-right text-muted-foreground">
{{ formatNumber(production?.[resourceType.key] || 0) }}
</TableCell>
</template>
<!-- 所有资源统一显示 -->
<TableCell
class="text-right"
:class="getResourceColor(planet.resources[resourceType.key], capacity?.[resourceType.key] || Infinity)"
>
{{ formatNumber(planet.resources[resourceType.key]) }}
</TableCell>
<TableCell class="text-right text-muted-foreground">
{{ formatNumber(capacity?.[resourceType.key] || 0) }}
</TableCell>
<TableCell class="text-right text-green-600 dark:text-green-400">
+{{ formatNumber(production?.[resourceType.key] || 0) }}
</TableCell>
<TableCell class="text-right text-red-600 dark:text-red-400">
<template v-if="resourceType.key === 'energy'">
-{{ formatNumber(energyConsumption) }}
</template>
<template v-else>
-
</template>
</TableCell>
</TableRow>
</TableBody>
</Table>
</CardContent>
</Card>
<!-- 资源获取来源 -->
<Card>
<CardHeader>
<CardTitle>{{ t('overview.productionSources') }}</CardTitle>
<CardDescription>{{ t('overview.productionSourcesDesc') }}</CardDescription>
</CardHeader>
<CardContent>
<div class="space-y-4">
<div v-for="resourceType in resourceTypes" :key="resourceType.key" class="border-b last:border-b-0 pb-4 last:pb-0">
<div class="flex items-center gap-2 mb-2">
<ResourceIcon :type="resourceType.key" size="sm" />
<span class="font-semibold">{{ t(`resources.${resourceType.key}`) }}</span>
</div>
<div v-if="productionBreakdown" class="ml-6 space-y-1 text-sm">
<!-- 建筑基础产量 -->
<div class="flex justify-between">
<span class="text-muted-foreground">
{{ t(productionBreakdown[resourceType.key].buildingName) }}
({{ t('common.level') }} {{ productionBreakdown[resourceType.key].buildingLevel }})
</span>
<span class="text-green-600 dark:text-green-400">
+{{ formatNumber(Math.floor(productionBreakdown[resourceType.key].baseProduction)) }}/{{ t('resources.hour') }}
</span>
</div>
<!-- 加成列表 -->
<div v-for="(bonus, idx) in productionBreakdown[resourceType.key].bonuses" :key="idx" class="flex justify-between">
<span class="text-muted-foreground ml-4">
{{ t(bonus.name) }}
</span>
<span :class="bonus.value > 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'">
{{ bonus.value > 0 ? '+' : '' }}{{ bonus.value }}%
</span>
</div>
<!-- 最终产量 -->
<div class="flex justify-between font-semibold pt-1 border-t mt-1">
<span>{{ t('overview.totalProduction') }}</span>
<span class="text-green-600 dark:text-green-400">
+{{ formatNumber(Math.floor(productionBreakdown[resourceType.key].finalProduction)) }}/{{ t('resources.hour') }}
</span>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
<!-- 资源消耗来源 -->
<Card>
<CardHeader>
<CardTitle>{{ t('overview.consumptionSources') }}</CardTitle>
<CardDescription>{{ t('overview.consumptionSourcesDesc') }}</CardDescription>
</CardHeader>
<CardContent>
<div class="space-y-2">
<!-- 金属矿消耗 -->
<div v-if="consumptionBreakdown && consumptionBreakdown.metalMine.buildingLevel > 0" class="flex justify-between text-sm">
<span class="text-muted-foreground">
{{ t(consumptionBreakdown.metalMine.buildingName) }}
({{ t('common.level') }} {{ consumptionBreakdown.metalMine.buildingLevel }})
</span>
<span class="text-red-600 dark:text-red-400">
-{{ formatNumber(Math.floor(consumptionBreakdown.metalMine.consumption)) }}/{{ t('resources.hour') }}
</span>
</div>
<!-- 晶体矿消耗 -->
<div v-if="consumptionBreakdown && consumptionBreakdown.crystalMine.buildingLevel > 0" class="flex justify-between text-sm">
<span class="text-muted-foreground">
{{ t(consumptionBreakdown.crystalMine.buildingName) }}
({{ t('common.level') }} {{ consumptionBreakdown.crystalMine.buildingLevel }})
</span>
<span class="text-red-600 dark:text-red-400">
-{{ formatNumber(Math.floor(consumptionBreakdown.crystalMine.consumption)) }}/{{ t('resources.hour') }}
</span>
</div>
<!-- 重氢合成器消耗 -->
<div v-if="consumptionBreakdown && consumptionBreakdown.deuteriumSynthesizer.buildingLevel > 0" class="flex justify-between text-sm">
<span class="text-muted-foreground">
{{ t(consumptionBreakdown.deuteriumSynthesizer.buildingName) }}
({{ t('common.level') }} {{ consumptionBreakdown.deuteriumSynthesizer.buildingLevel }})
</span>
<span class="text-red-600 dark:text-red-400">
-{{ formatNumber(Math.floor(consumptionBreakdown.deuteriumSynthesizer.consumption)) }}/{{ t('resources.hour') }}
</span>
</div>
<!-- 总消耗 -->
<div v-if="consumptionBreakdown" class="flex justify-between font-semibold pt-2 border-t">
<span>{{ t('overview.totalConsumption') }}</span>
<span class="text-red-600 dark:text-red-400">
-{{ formatNumber(Math.floor(consumptionBreakdown.total)) }}/{{ t('resources.hour') }}
</span>
</div>
<!-- 无消耗提示 -->
<div v-if="consumptionBreakdown && consumptionBreakdown.total === 0" class="text-sm text-muted-foreground text-center py-2">
{{ t('overview.noConsumption') }}
</div>
</div>
</CardContent>
</Card>
<!-- 舰队信息 -->
<Card>
<CardHeader>
@@ -109,8 +209,8 @@
import { formatNumber, getResourceColor } from '@/utils/format'
import type { Planet } from '@/types/game'
import * as publicLogic from '@/logic/publicLogic'
import * as officerLogic from '@/logic/officerLogic'
import * as resourceLogic from '@/logic/resourceLogic'
import * as officerLogic from '@/logic/officerLogic'
const gameStore = useGameStore()
const { t } = useI18n()
@@ -119,18 +219,25 @@
const production = computed(() => (planet.value ? publicLogic.getResourceProduction(planet.value, gameStore.player.officers) : null))
const capacity = computed(() => (planet.value ? publicLogic.getResourceCapacity(planet.value, gameStore.player.officers) : null))
// 电量产出和消耗
const energyProduction = computed(() => {
if (!planet.value) return 0
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, Date.now())
return resourceLogic.calculateEnergyProduction(planet.value, { energyProductionBonus: bonuses.energyProductionBonus })
})
// 能量消耗
const energyConsumption = computed(() => {
if (!planet.value) return 0
return resourceLogic.calculateEnergyConsumption(planet.value)
})
// 资源产量详细breakdown
const productionBreakdown = computed(() => {
if (!planet.value) return null
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, Date.now())
return resourceLogic.calculateProductionBreakdown(planet.value, bonuses)
})
// 资源消耗详细breakdown
const consumptionBreakdown = computed(() => {
if (!planet.value) return null
return resourceLogic.calculateConsumptionBreakdown(planet.value)
})
// 资源类型配置
const resourceTypes = [
{ key: 'metal' as const },

View File

@@ -7,7 +7,7 @@
<div class="grid grid-cols-2 lg:grid-cols-3 gap-3 sm:gap-4">
<Card v-for="techType in Object.values(TechnologyType)" :key="techType" class="relative">
<CardUnlockOverlay :requirements="TECHNOLOGIES[techType].requirements" />
<CardUnlockOverlay :requirements="TECHNOLOGIES[techType].requirements" :currentLevel="getTechLevel(techType)" />
<CardHeader>
<div class="flex justify-between items-start gap-2">
<div class="min-w-0 flex-1">
@@ -63,7 +63,7 @@
</div>
<Button @click="handleResearch(techType)" :disabled="!canResearch(techType)" class="w-full">
{{ t('researchView.research') }}
{{ getResearchButtonText(techType) }}
</Button>
</div>
</CardContent>
@@ -98,7 +98,7 @@
const gameStore = useGameStore()
const detailDialog = useDetailDialogStore()
const { t } = useI18n()
const { TECHNOLOGIES } = useGameConfig()
const { TECHNOLOGIES, BUILDINGS } = useGameConfig()
const planet = computed(() => gameStore.currentPlanet)
const player = computed(() => gameStore.player)
const alertDialog = ref<InstanceType<typeof AlertDialog> | null>(null)
@@ -123,8 +123,86 @@
return true
}
// 检查升级前置条件是否满足
const checkUpgradeRequirements = (techType: TechnologyType): boolean => {
if (!planet.value) return false
const config = TECHNOLOGIES.value[techType]
const currentLevel = getTechLevel(techType)
const targetLevel = currentLevel + 1
// 获取目标等级的所有前置条件(包括等级门槛)
const requirements = publicLogic.getLevelRequirements(config, targetLevel)
if (!requirements || Object.keys(requirements).length === 0) return true
return publicLogic.checkRequirements(planet.value, gameStore.player.technologies, requirements)
}
// 获取研究按钮文本
const getResearchButtonText = (techType: TechnologyType): string => {
if (!planet.value) return t('researchView.research')
const config = TECHNOLOGIES.value[techType]
const currentLevel = getTechLevel(techType)
// 检查是否达到等级上限
if (config.maxLevel !== undefined && currentLevel >= config.maxLevel) {
return t('researchView.maxLevelReached') // "等级已满"
}
if (player.value.researchQueue.length > 0) return t('researchView.research')
// 检查前置条件
if (!checkUpgradeRequirements(techType)) {
return t('buildingsView.requirementsNotMet') // "条件不足"
}
return t('researchView.research') // "研究"
}
// 获取前置条件列表文本
const getRequirementsList = (techType: TechnologyType): string => {
const config = TECHNOLOGIES.value[techType]
const currentLevel = getTechLevel(techType)
const targetLevel = currentLevel + 1
// 获取目标等级的所有前置条件(包括等级门槛)
const requirements = publicLogic.getLevelRequirements(config, targetLevel)
if (!requirements || !planet.value) return ''
const lines: string[] = []
for (const [key, requiredLevel] of Object.entries(requirements)) {
// 检查是否为建筑类型
if (Object.values(BuildingType).includes(key as BuildingType)) {
const bt = key as BuildingType
const currentLevel = planet.value.buildings[bt] || 0
const name = BUILDINGS.value[bt]?.name || bt
const status = currentLevel >= requiredLevel ? '✓' : '✗'
lines.push(`${status} ${name}: Lv ${requiredLevel} (${t('common.current')}: Lv ${currentLevel})`)
}
// 检查是否为科技类型
else if (Object.values(TechnologyType).includes(key as TechnologyType)) {
const tt = key as TechnologyType
const currentLevel = gameStore.player.technologies[tt] || 0
const name = TECHNOLOGIES.value[tt]?.name || tt
const status = currentLevel >= requiredLevel ? '✓' : '✗'
lines.push(`${status} ${name}: Lv ${requiredLevel} (${t('common.current')}: Lv ${currentLevel})`)
}
}
return lines.join('\n')
}
// 研究科技
const handleResearch = (techType: TechnologyType) => {
// 检查前置条件
if (!checkUpgradeRequirements(techType)) {
alertDialog.value?.show({
title: t('common.requirementsNotMet'),
message: getRequirementsList(techType)
})
return
}
const success = researchTechnology(techType)
if (!success) {
alertDialog.value?.show({
@@ -141,10 +219,18 @@
// 检查是否可以研究
const canResearch = (techType: TechnologyType): boolean => {
if (!planet.value || player.value.researchQueue.length > 0) return false
if (!planet.value) return false
const config = TECHNOLOGIES.value[techType]
const currentLevel = getTechLevel(techType)
// 检查是否达到等级上限
if (config.maxLevel !== undefined && currentLevel >= config.maxLevel) {
return false
}
if (player.value.researchQueue.length > 0) return false
const cost = getTechnologyCost(techType, currentLevel + 1)
return (

View File

@@ -59,19 +59,16 @@
<CardDescription>{{ t('settings.gameSettingsDesc') }}</CardDescription>
</CardHeader>
<CardContent class="space-y-4">
<!-- 玩家名称 -->
<div class="flex items-center justify-between">
<Label for="player-name">{{ t('settings.playerName') }}</Label>
<Input id="player-name" v-model="playerName" @blur="updatePlayerName" class="max-w-xs" />
</div>
<!-- 游戏速度 -->
<div class="flex items-center justify-between">
<!-- 游戏暂停 -->
<div class="flex items-center justify-between p-4 border rounded-lg">
<div class="space-y-1">
<Label>{{ t('settings.gameSpeed') }}</Label>
<p class="text-sm text-muted-foreground">{{ t('settings.gameSpeedDesc') }}</p>
<h3 class="font-medium">{{ t('settings.gamePause') }}</h3>
<p class="text-sm text-muted-foreground">{{ t('settings.gamePauseDesc') }}</p>
</div>
<div class="text-2xl font-bold">1x</div>
<Button @click="togglePause" :variant="gameStore.isPaused ? 'default' : 'outline'">
<component :is="gameStore.isPaused ? Play : Pause" class="mr-2 h-4 w-4" />
{{ gameStore.isPaused ? t('settings.resume') : t('settings.pause') }}
</Button>
</div>
</CardContent>
</Card>
@@ -89,7 +86,7 @@
</div>
<div class="flex items-center justify-between text-sm">
<span class="text-muted-foreground">{{ t('settings.buildDate') }}:</span>
<span class="font-medium">{{ new Date().toLocaleDateString() }}</span>
<span class="font-medium">{{ pkg.buildDate }}</span>
</div>
</div>
@@ -139,8 +136,6 @@
import { useI18n } from '@/composables/useI18n'
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import {
AlertDialog,
AlertDialogAction,
@@ -151,17 +146,17 @@
AlertDialogHeader,
AlertDialogTitle
} from '@/components/ui/alert-dialog'
import { Download, Upload, Trash2, ExternalLink, MessagesSquare } from 'lucide-vue-next'
import { Download, Upload, Trash2, ExternalLink, MessagesSquare, Play, Pause } from 'lucide-vue-next'
import { saveAs } from 'file-saver'
import { toast } from 'vue-sonner'
import pkg from '../../package.json'
import 'vue-sonner/style.css'
const { t } = useI18n()
const gameStore = useGameStore()
const fileInputRef = ref<HTMLInputElement>()
const isExporting = ref(false)
const playerName = ref(gameStore.player.name)
const showConfirmDialog = ref(false)
const confirmTitle = ref('')
@@ -176,17 +171,30 @@
window.open(`https://qm.qq.com/q/${pkg.id}`, '_blank')
}
// 导出数据
// 导出数据(包含游戏数据和地图数据)
const handleExport = async () => {
try {
isExporting.value = true
const data = localStorage.getItem(pkg.name)
if (!data) {
// 获取游戏数据
const gameData = localStorage.getItem(pkg.name)
// 获取地图数据
const universeData = localStorage.getItem(`${pkg.name}-universe`)
if (!gameData) {
toast.error(t('settings.exportFailed'))
return
}
// 合并数据
const exportData = {
game: gameData,
universe: universeData || null
}
const fileName = `${pkg.name}-${new Date().toISOString().slice(0, 10)}-${Date.now()}.json`
saveAs(new Blob([data], { type: 'application/json' }), fileName)
const jsonString = JSON.stringify(exportData, null, 2)
saveAs(new Blob([jsonString], { type: 'application/json' }), fileName)
toast.success(t('settings.exportSuccess'))
} catch (error) {
console.error('Export failed:', error)
@@ -205,14 +213,14 @@
const handleFileSelect = (event: Event) => {
const file = (event.target as HTMLInputElement).files?.[0]
if (!file) return
confirmTitle.value = t('settings.importConfirmTitle')
confirmMessage.value = t('settings.importConfirmMessage')
showConfirmDialog.value = true
gameStore.isPaused = true
confirmCallback = () => importData(file)
}
// 导入数据
// 导入数据(包含游戏数据和地图数据)
const importData = async (file: File) => {
try {
const reader = new FileReader()
@@ -220,9 +228,28 @@
try {
const result = e.target?.result
if (typeof result === 'string') {
localStorage.setItem(pkg.name, result)
const importData = JSON.parse(result)
// 兼容旧版本:如果是旧格式(直接是字符串),只导入游戏数据
if (typeof importData === 'string' || !importData.game) {
localStorage.setItem(pkg.name, result)
toast.success(t('settings.importSuccess'))
setTimeout(() => window.location.reload(), 1000)
return
}
// 新格式:分别导入游戏数据和地图数据
if (importData.game) {
localStorage.setItem(pkg.name, importData.game)
}
if (importData.universe) {
localStorage.setItem(`${pkg.name}-universe`, importData.universe)
}
toast.success(t('settings.importSuccess'))
setTimeout(() => location.reload(), 500)
// 延迟刷新页面以让toast显示
setTimeout(() => window.location.reload(), 1000)
} else {
toast.error(t('settings.importFailed'))
}
@@ -253,10 +280,13 @@
window.location.reload()
}
// 更新玩家名称
const updatePlayerName = () => {
if (playerName.value.trim()) {
gameStore.player.name = playerName.value.trim()
// 切换游戏暂停状态
const togglePause = () => {
gameStore.isPaused = !gameStore.isPaused
if (gameStore.isPaused) {
toast.info(t('settings.gamePaused'))
} else {
toast.success(t('settings.gameResumed'))
}
}
@@ -271,6 +301,7 @@
// 取消操作
const cancelAction = () => {
gameStore.isPaused = false
confirmCallback = null
showConfirmDialog.value = false
// 重置文件输入

View File

@@ -5,6 +5,29 @@
<h1 class="text-2xl sm:text-3xl font-bold mb-4 sm:mb-6">{{ t('shipyardView.title') }}</h1>
<!-- 舰队仓储显示 -->
<div 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('shipyardView.fleetStorage') }}:</div>
<div class="text-sm sm:text-base font-bold">
<span :class="fleetStorageUsage > maxFleetStorage ? 'text-destructive' : 'text-primary'">
{{ formatNumber(fleetStorageUsage) }}
</span>
<span class="text-muted-foreground mx-1">/</span>
<span>{{ formatNumber(maxFleetStorage) }}</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="fleetStorageUsage > maxFleetStorage ? 'bg-destructive' : 'bg-primary'"
:style="{ width: `${Math.min((fleetStorageUsage / maxFleetStorage) * 100, 100)}%` }"
></div>
</div>
</div>
</div>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3 sm:gap-4">
<Card v-for="shipType in Object.values(ShipType)" :key="shipType" class="relative">
<CardUnlockOverlay :requirements="SHIPS[shipType].requirements" />
@@ -151,6 +174,7 @@
import { formatNumber, getResourceCostColor } from '@/utils/format'
import * as shipValidation from '@/logic/shipValidation'
import * as publicLogic from '@/logic/publicLogic'
import * as fleetStorageLogic from '@/logic/fleetStorageLogic'
const gameStore = useGameStore()
const detailDialog = useDetailDialogStore()
@@ -159,6 +183,18 @@
const planet = computed(() => gameStore.currentPlanet)
const alertDialog = ref<InstanceType<typeof AlertDialog> | null>(null)
// 舰队仓储使用量
const fleetStorageUsage = computed(() => {
if (!planet.value) return 0
return fleetStorageLogic.calculateFleetStorageUsage(planet.value.fleet)
})
// 舰队仓储上限
const maxFleetStorage = computed(() => {
if (!planet.value) return 0
return fleetStorageLogic.calculateMaxFleetStorage(planet.value, gameStore.player.technologies)
})
// 每种舰船的建造数量
const quantities = ref<Record<ShipType, number>>({
[ShipType.LightFighter]: 0,
@@ -170,7 +206,8 @@
[ShipType.ColonyShip]: 0,
[ShipType.Recycler]: 0,
[ShipType.EspionageProbe]: 0,
[ShipType.DarkMatterHarvester]: 0
[ShipType.DarkMatterHarvester]: 0,
[ShipType.Deathstar]: 0
})
const buildShip = (shipType: ShipType, quantity: number): boolean => {

View File

@@ -0,0 +1,470 @@
/**
* 战斗模拟 Worker
* 在独立线程中处理计算密集型的战斗模拟任务
*/
import type { Fleet, Resources } from '@/types/game'
import { ShipType, DefenseType } from '@/types/game'
import { SHIPS, DEFENSES } from '@/config/gameConfig'
import type { WorkerRequestMessage, WorkerResponseMessage, BattleSideData, BattleSimulationResult } from '@/types/worker'
import { WorkerMessageType } from '@/types/worker'
// 战斗单位接口
interface CombatUnit {
type: ShipType | DefenseType
count: number
attack: number
shield: number
armor: number
rapidFire?: Record<string, number>
}
/**
* 计算科技加成后的数值
*/
const applyTechBonus = (baseValue: number, techLevel: number = 0, bonusPerLevel: number = 0.1): number => {
return Math.floor(baseValue * (1 + techLevel * bonusPerLevel))
}
/**
* 将舰队和防御转换为战斗单位数组
*/
const prepareCombatUnits = (side: BattleSideData, isDefender: boolean = false): CombatUnit[] => {
const units: CombatUnit[] = []
// 处理舰船
if (side.ships) {
for (const [shipType, count] of Object.entries(side.ships)) {
if (count > 0) {
const config = SHIPS[shipType as ShipType]
units.push({
type: shipType as ShipType,
count: count,
attack: applyTechBonus(config.attack, side.weaponTech),
shield: applyTechBonus(config.shield, side.shieldTech),
armor: applyTechBonus(config.armor, side.armorTech)
})
}
}
}
// 处理防御设施(仅防守方)
if (isDefender && side.defense) {
for (const [defenseType, count] of Object.entries(side.defense)) {
if (count > 0) {
const config = DEFENSES[defenseType as DefenseType]
units.push({
type: defenseType as DefenseType,
count: count,
attack: applyTechBonus(config.attack, side.weaponTech),
shield: applyTechBonus(config.shield, side.shieldTech),
armor: applyTechBonus(config.armor, side.armorTech)
})
}
}
}
return units
}
/**
* 计算一个单位对另一个单位造成的伤害
*/
const calculateDamage = (attacker: CombatUnit, defender: CombatUnit): { destroyed: number; damagedShield: number } => {
const attackPower = attacker.attack
const defenderShield = defender.shield
const defenderArmor = defender.armor
let destroyed = 0
let damagedShield = 0
// 如果攻击力小于护盾的1%,有很大概率无法击穿护盾
if (attackPower < defenderShield * 0.01) {
if (Math.random() > 0.01) {
return { destroyed: 0, damagedShield: 0 }
}
}
// 计算伤害
let remainingDamage = attackPower
// 先消耗护盾
if (remainingDamage > defenderShield) {
remainingDamage -= defenderShield
damagedShield = defenderShield
} else {
damagedShield = remainingDamage
return { destroyed: 0, damagedShield }
}
// 再消耗装甲
if (remainingDamage > defenderArmor) {
destroyed = 1
} else {
// 有概率摧毁
const destroyChance = remainingDamage / defenderArmor
if (Math.random() < destroyChance) {
destroyed = 1
}
}
return { destroyed, damagedShield }
}
/**
* 执行一轮战斗
*/
const executeRound = (
attackerUnits: CombatUnit[],
defenderUnits: CombatUnit[]
): {
attackerLosses: Partial<Fleet>
defenderLosses: {
fleet: Partial<Fleet>
defense: Partial<Record<DefenseType, number>>
}
attackerRemainingPower: number
defenderRemainingPower: number
} => {
const attackerLosses: Partial<Fleet> = {}
const defenderShipLosses: Partial<Fleet> = {}
const defenderDefenseLosses: Partial<Record<DefenseType, number>> = {}
// 攻击方向防守方射击
for (const attacker of attackerUnits) {
for (let i = 0; i < attacker.count; i++) {
if (defenderUnits.length === 0) break
const targetIndex = Math.floor(Math.random() * defenderUnits.length)
const target = defenderUnits[targetIndex]
if (!target) continue
const { destroyed } = calculateDamage(attacker, target)
if (destroyed > 0) {
target.count -= destroyed
// 记录损失
if (Object.values(ShipType).includes(target.type as ShipType)) {
const shipType = target.type as ShipType
defenderShipLosses[shipType] = (defenderShipLosses[shipType] || 0) + destroyed
} else {
const defenseType = target.type as DefenseType
defenderDefenseLosses[defenseType] = (defenderDefenseLosses[defenseType] || 0) + destroyed
}
if (target.count <= 0) {
defenderUnits.splice(targetIndex, 1)
}
}
}
}
// 防守方向攻击方射击
for (const defender of defenderUnits) {
for (let i = 0; i < defender.count; i++) {
if (attackerUnits.length === 0) break
const targetIndex = Math.floor(Math.random() * attackerUnits.length)
const target = attackerUnits[targetIndex]
if (!target) continue
const { destroyed } = calculateDamage(defender, target)
if (destroyed > 0) {
target.count -= destroyed
const shipType = target.type as ShipType
attackerLosses[shipType] = (attackerLosses[shipType] || 0) + destroyed
if (target.count <= 0) {
attackerUnits.splice(targetIndex, 1)
}
}
}
}
// 计算剩余战斗力
const attackerPower = attackerUnits.reduce((sum, unit) => sum + unit.count * unit.attack, 0)
const defenderPower = defenderUnits.reduce((sum, unit) => sum + unit.count * unit.attack, 0)
return {
attackerLosses,
defenderLosses: {
fleet: defenderShipLosses,
defense: defenderDefenseLosses
},
attackerRemainingPower: attackerPower,
defenderRemainingPower: defenderPower
}
}
/**
* 模拟完整战斗
*/
const simulateBattle = (attacker: BattleSideData, defender: BattleSideData, maxRounds: number = 6): BattleSimulationResult => {
// 准备战斗单位
let attackerUnits = prepareCombatUnits(attacker, false)
let defenderUnits = prepareCombatUnits(defender, true)
const totalAttackerLosses: Partial<Fleet> = {}
const totalDefenderShipLosses: Partial<Fleet> = {}
const totalDefenderDefenseLosses: Partial<Record<DefenseType, number>> = {}
const roundDetails: Array<{
round: number
attackerLosses: Partial<Fleet>
defenderLosses: {
fleet: Partial<Fleet>
defense: Partial<Record<DefenseType, number>>
}
attackerRemainingPower: number
defenderRemainingPower: number
}> = []
let rounds = 0
// 执行最多maxRounds轮战斗
for (let round = 0; round < maxRounds; round++) {
if (attackerUnits.length === 0 || defenderUnits.length === 0) {
break
}
rounds++
const roundResult = executeRound(attackerUnits, defenderUnits)
// 保存当前回合详情
roundDetails.push({
round: rounds,
attackerLosses: { ...roundResult.attackerLosses },
defenderLosses: {
fleet: { ...roundResult.defenderLosses.fleet },
defense: { ...roundResult.defenderLosses.defense }
},
attackerRemainingPower: roundResult.attackerRemainingPower,
defenderRemainingPower: roundResult.defenderRemainingPower
})
// 累计损失
for (const [shipType, count] of Object.entries(roundResult.attackerLosses)) {
totalAttackerLosses[shipType as ShipType] = (totalAttackerLosses[shipType as ShipType] || 0) + count
}
for (const [shipType, count] of Object.entries(roundResult.defenderLosses.fleet)) {
totalDefenderShipLosses[shipType as ShipType] = (totalDefenderShipLosses[shipType as ShipType] || 0) + count
}
for (const [defenseType, count] of Object.entries(roundResult.defenderLosses.defense)) {
totalDefenderDefenseLosses[defenseType as DefenseType] = (totalDefenderDefenseLosses[defenseType as DefenseType] || 0) + count
}
}
// 防御设施有概率修复70%概率)
const repairedDefense: Partial<Record<DefenseType, number>> = {}
for (const [defenseType, count] of Object.entries(totalDefenderDefenseLosses)) {
const repaired = Math.floor(count * 0.7)
if (repaired > 0) {
repairedDefense[defenseType as DefenseType] = repaired
totalDefenderDefenseLosses[defenseType as DefenseType] = count - repaired
}
}
// 计算剩余单位
const attackerRemaining: Partial<Fleet> = {}
for (const unit of attackerUnits) {
if (unit.count > 0) {
attackerRemaining[unit.type as ShipType] = unit.count
}
}
const defenderShipRemaining: Partial<Fleet> = {}
const defenderDefenseRemaining: Partial<Record<DefenseType, number>> = {}
for (const unit of defenderUnits) {
if (unit.count > 0) {
if (Object.values(ShipType).includes(unit.type as ShipType)) {
defenderShipRemaining[unit.type as ShipType] = unit.count
} else {
defenderDefenseRemaining[unit.type as DefenseType] = unit.count
}
}
}
// 添加修复的防御设施
for (const [defenseType, count] of Object.entries(repairedDefense)) {
defenderDefenseRemaining[defenseType as DefenseType] = (defenderDefenseRemaining[defenseType as DefenseType] || 0) + count
}
// 判断胜负
let winner: 'attacker' | 'defender' | 'draw'
if (attackerUnits.length === 0 && defenderUnits.length === 0) {
winner = 'draw'
} else if (attackerUnits.length === 0) {
winner = 'defender'
} else if (defenderUnits.length === 0) {
winner = 'attacker'
} else {
winner = 'draw'
}
return {
winner,
rounds,
attackerLosses: totalAttackerLosses,
defenderLosses: {
fleet: totalDefenderShipLosses,
defense: totalDefenderDefenseLosses
},
attackerRemaining,
defenderRemaining: {
fleet: defenderShipRemaining,
defense: defenderDefenseRemaining
},
roundDetails
}
}
/**
* 计算掠夺的资源
*/
const calculatePlunder = (defenderResources: Resources, attackerFleet: Partial<Fleet>): Resources => {
let totalCapacity = 0
for (const [shipType, count] of Object.entries(attackerFleet)) {
const config = SHIPS[shipType as ShipType]
totalCapacity += config.cargoCapacity * count
}
const availableResources = {
metal: Math.floor(defenderResources.metal * 0.5),
crystal: Math.floor(defenderResources.crystal * 0.5),
deuterium: Math.floor(defenderResources.deuterium * 0.5),
darkMatter: Math.floor(defenderResources.darkMatter * 0.5),
energy: 0
}
const totalAvailable =
availableResources.metal + availableResources.crystal + availableResources.deuterium + availableResources.darkMatter
if (totalCapacity >= totalAvailable) {
return availableResources
}
const ratio = totalCapacity / totalAvailable
return {
metal: Math.floor(availableResources.metal * ratio),
crystal: Math.floor(availableResources.crystal * ratio),
deuterium: Math.floor(availableResources.deuterium * ratio),
darkMatter: Math.floor(availableResources.darkMatter * ratio),
energy: 0
}
}
/**
* 计算残骸场
*/
const calculateDebrisField = (
attackerLosses: Partial<Fleet>,
defenderLosses: {
fleet: Partial<Fleet>
defense: Partial<Record<DefenseType, number>>
}
): Resources => {
let totalMetal = 0
let totalCrystal = 0
for (const [shipType, count] of Object.entries(attackerLosses)) {
const config = SHIPS[shipType as ShipType]
totalMetal += config.cost.metal * count * 0.3
totalCrystal += config.cost.crystal * count * 0.3
}
for (const [shipType, count] of Object.entries(defenderLosses.fleet)) {
const config = SHIPS[shipType as ShipType]
totalMetal += config.cost.metal * count * 0.3
totalCrystal += config.cost.crystal * count * 0.3
}
for (const [defenseType, count] of Object.entries(defenderLosses.defense)) {
const config = DEFENSES[defenseType as DefenseType]
totalMetal += config.cost.metal * count * 0.3
totalCrystal += config.cost.crystal * count * 0.3
}
return {
metal: Math.floor(totalMetal),
crystal: Math.floor(totalCrystal),
deuterium: 0,
darkMatter: 0,
energy: 0
}
}
// ============================================================================
// Worker 消息处理
// ============================================================================
self.onmessage = (event: MessageEvent<WorkerRequestMessage>) => {
const { id, type, payload } = event.data
try {
let result: unknown
switch (type) {
case WorkerMessageType.SIMULATE_BATTLE: {
const {
attacker,
defender,
maxRounds = 6
} = payload as {
attacker: BattleSideData
defender: BattleSideData
maxRounds?: number
}
result = simulateBattle(attacker, defender, maxRounds)
break
}
case WorkerMessageType.CALCULATE_PLUNDER: {
const { defenderResources, attackerFleet } = payload as {
defenderResources: Resources
attackerFleet: Partial<Fleet>
}
result = calculatePlunder(defenderResources, attackerFleet)
break
}
case WorkerMessageType.CALCULATE_DEBRIS: {
const { attackerLosses, defenderLosses } = payload as {
attackerLosses: Partial<Fleet>
defenderLosses: {
fleet: Partial<Fleet>
defense: Partial<Record<DefenseType, number>>
}
}
result = calculateDebrisField(attackerLosses, defenderLosses)
break
}
default:
throw new Error(`Unknown message type: ${type}`)
}
// 发送成功响应
const response: WorkerResponseMessage = {
id,
type: WorkerMessageType.SUCCESS,
success: true,
data: result
}
self.postMessage(response)
} catch (error) {
// 发送错误响应
const response: WorkerResponseMessage = {
id,
type: WorkerMessageType.ERROR,
success: false,
error: error instanceof Error ? error.message : String(error)
}
self.postMessage(response)
}
}

View File

@@ -0,0 +1,234 @@
/**
* Worker 管理器
* 统一管理所有 Worker 的创建、通信和销毁
*/
import type { WorkerRequestMessage, WorkerResponseMessage, WorkerMessageType } from '@/types/worker'
import { WorkerMessageType as MsgType } from '@/types/worker'
import { toRaw } from 'vue'
import BattleWorker from './battle.worker?worker'
/**
* Worker 任务接口
*/
interface WorkerTask {
resolve: (data: unknown) => void
reject: (error: Error) => void
timeout?: ReturnType<typeof setTimeout>
}
/**
* 将 Vue 响应式对象转换为普通对象
* 使用 toRaw() 获取原始对象,避免 Proxy 无法被 structured clone
*/
const toPlainObject = <T>(obj: T): T => {
if (obj === null || obj === undefined) return obj
if (typeof obj !== 'object') return obj
// 使用 toRaw 获取 Vue 响应式对象的原始值
const raw = toRaw(obj)
// 对于数组,递归处理每个元素
if (Array.isArray(raw)) {
return raw.map(item => toPlainObject(item)) as unknown as T
}
// 对于对象,递归处理每个属性
if (raw && typeof raw === 'object') {
const plain: any = {}
for (const key in raw) {
if (Object.prototype.hasOwnProperty.call(raw, key)) {
plain[key] = toPlainObject(raw[key])
}
}
return plain
}
return raw
}
/**
* Worker 管理类
*/
class WorkerManager {
private battleWorker: Worker | null = null
private pendingTasks: Map<string, WorkerTask> = new Map()
private messageIdCounter = 0
private readonly defaultTimeout = 10000 // 30秒超时
/**
* 初始化战斗 Worker
*/
private initBattleWorker(): void {
if (this.battleWorker) return
this.battleWorker = new BattleWorker()
this.setupWorkerHandlers(this.battleWorker, 'Battle')
}
/**
* 设置 Worker 消息处理器
*/
private setupWorkerHandlers(worker: Worker, workerName: string): void {
worker.onmessage = (event: MessageEvent<WorkerResponseMessage>) => {
const { id, success, data, error } = event.data
const task = this.pendingTasks.get(id)
if (!task) {
console.warn(`[WorkerManager] No pending task found for message ID: ${id}`)
return
}
// 清除超时定时器
if (task.timeout) {
clearTimeout(task.timeout)
}
// 移除任务
this.pendingTasks.delete(id)
// 处理响应
if (success) {
task.resolve(data)
} else {
task.reject(new Error(error || 'Worker task failed'))
}
}
worker.onerror = (error: ErrorEvent) => {
console.error(`[WorkerManager] ${workerName} worker error:`, error)
// 拒绝所有待处理的任务
for (const task of this.pendingTasks.values()) {
if (task.timeout) clearTimeout(task.timeout)
task.reject(new Error(`${workerName} worker crashed`))
}
this.pendingTasks.clear()
// 清除对应的 worker 引用
if (workerName === 'Battle') {
this.battleWorker = null
}
}
}
/**
* 生成唯一的消息 ID
*/
private generateMessageId(): string {
return `msg_${Date.now()}_${++this.messageIdCounter}`
}
/**
* 根据消息类型获取对应的 Worker
*/
private getWorkerByType(type: WorkerMessageType): Worker {
// 战斗相关消息使用 battleWorker
if (type === MsgType.SIMULATE_BATTLE || type === MsgType.CALCULATE_PLUNDER || type === MsgType.CALCULATE_DEBRIS) {
this.initBattleWorker()
return this.battleWorker!
}
throw new Error(`Unknown message type: ${type}`)
}
/**
* 发送消息到 Worker 并等待响应
*/
private sendMessage<T>(type: WorkerMessageType, payload: unknown, timeout: number = this.defaultTimeout): Promise<T> {
const worker = this.getWorkerByType(type)
if (!worker) {
return Promise.reject(new Error('Worker initialization failed'))
}
const id = this.generateMessageId()
return new Promise<T>((resolve, reject) => {
// 设置超时
const timeoutId = setTimeout(() => {
this.pendingTasks.delete(id)
reject(new Error(`Worker task timeout after ${timeout}ms`))
}, timeout)
// 保存任务
this.pendingTasks.set(id, {
resolve: resolve as (data: unknown) => void,
reject,
timeout: timeoutId
})
// 发送消息(使用 toPlainObject 转换 Vue Proxy 对象,然后使用浏览器内置的 structured clone
const message: WorkerRequestMessage = { id, type, payload: toPlainObject(payload) }
worker.postMessage(message)
})
}
/**
* 战斗模拟
*/
public async simulateBattle(params: {
attacker: {
ships: Parameters<typeof import('@/utils/battleSimulator').simulateBattle>[0]['ships']
defense?: Parameters<typeof import('@/utils/battleSimulator').simulateBattle>[0]['defense']
weaponTech?: number
shieldTech?: number
armorTech?: number
}
defender: {
ships: Parameters<typeof import('@/utils/battleSimulator').simulateBattle>[0]['ships']
defense?: Parameters<typeof import('@/utils/battleSimulator').simulateBattle>[0]['defense']
weaponTech?: number
shieldTech?: number
armorTech?: number
}
maxRounds?: number
}): Promise<ReturnType<typeof import('@/utils/battleSimulator').simulateBattle>> {
return this.sendMessage(MsgType.SIMULATE_BATTLE, params)
}
/**
* 计算掠夺资源
*/
public async calculatePlunder(params: {
defenderResources: Parameters<typeof import('@/utils/battleSimulator').calculatePlunder>[0]
attackerFleet: Parameters<typeof import('@/utils/battleSimulator').calculatePlunder>[1]
}): Promise<ReturnType<typeof import('@/utils/battleSimulator').calculatePlunder>> {
return this.sendMessage(MsgType.CALCULATE_PLUNDER, params)
}
/**
* 计算残骸场
*/
public async calculateDebris(params: {
attackerLosses: Parameters<typeof import('@/utils/battleSimulator').calculateDebrisField>[0]
defenderLosses: Parameters<typeof import('@/utils/battleSimulator').calculateDebrisField>[1]
}): Promise<ReturnType<typeof import('@/utils/battleSimulator').calculateDebrisField>> {
return this.sendMessage(MsgType.CALCULATE_DEBRIS, params)
}
/**
* 销毁所有 Worker
*/
public destroy(): void {
if (this.battleWorker) {
this.battleWorker.terminate()
this.battleWorker = null
}
// 清除所有待处理的任务
for (const task of this.pendingTasks.values()) {
if (task.timeout) clearTimeout(task.timeout)
task.reject(new Error('Worker manager destroyed'))
}
this.pendingTasks.clear()
}
/**
* 获取待处理任务数量
*/
public getPendingTaskCount(): number {
return this.pendingTasks.size
}
}
// 导出单例
export const workerManager = new WorkerManager()