mirror of
https://github.com/setube/ogame-vue-ts.git
synced 2026-05-12 16:05:12 +08:00
feat: 新增战报弹窗与舰队模拟器,重构UI组件
新增 BattleReportDialog、SpyReportDialog、NumberWithTooltip 等组件,完善舰队模拟器功能。重构并引入 Sheet、Sidebar、Tooltip、Skeleton 等 UI 组件,优化界面结构。实现 battle.worker 支持战斗计算,增加 universeStore、fleetStorageLogic 等核心逻辑,完善多语言与类型定义。
This commit is contained in:
611
src/App.vue
611
src/App.vue
@@ -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
|
||||
}
|
||||
|
||||
334
src/components/BattleReportDialog.vue
Normal file
334
src/components/BattleReportDialog.vue
Normal 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>
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
50
src/components/NumberWithTooltip.vue
Normal file
50
src/components/NumberWithTooltip.vue
Normal 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>
|
||||
141
src/components/SpyReportDialog.vue
Normal file
141
src/components/SpyReportDialog.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
29
src/components/ui/separator/Separator.vue
Normal file
29
src/components/ui/separator/Separator.vue
Normal 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>
|
||||
1
src/components/ui/separator/index.ts
Normal file
1
src/components/ui/separator/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as Separator } from "./Separator.vue"
|
||||
19
src/components/ui/sheet/Sheet.vue
Normal file
19
src/components/ui/sheet/Sheet.vue
Normal 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>
|
||||
15
src/components/ui/sheet/SheetClose.vue
Normal file
15
src/components/ui/sheet/SheetClose.vue
Normal 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>
|
||||
62
src/components/ui/sheet/SheetContent.vue
Normal file
62
src/components/ui/sheet/SheetContent.vue
Normal 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>
|
||||
21
src/components/ui/sheet/SheetDescription.vue
Normal file
21
src/components/ui/sheet/SheetDescription.vue
Normal 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>
|
||||
16
src/components/ui/sheet/SheetFooter.vue
Normal file
16
src/components/ui/sheet/SheetFooter.vue
Normal 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>
|
||||
15
src/components/ui/sheet/SheetHeader.vue
Normal file
15
src/components/ui/sheet/SheetHeader.vue
Normal 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>
|
||||
21
src/components/ui/sheet/SheetOverlay.vue
Normal file
21
src/components/ui/sheet/SheetOverlay.vue
Normal 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>
|
||||
21
src/components/ui/sheet/SheetTitle.vue
Normal file
21
src/components/ui/sheet/SheetTitle.vue
Normal 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>
|
||||
15
src/components/ui/sheet/SheetTrigger.vue
Normal file
15
src/components/ui/sheet/SheetTrigger.vue
Normal 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>
|
||||
8
src/components/ui/sheet/index.ts
Normal file
8
src/components/ui/sheet/index.ts
Normal 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"
|
||||
100
src/components/ui/sidebar/Sidebar.vue
Normal file
100
src/components/ui/sidebar/Sidebar.vue
Normal 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>
|
||||
18
src/components/ui/sidebar/SidebarContent.vue
Normal file
18
src/components/ui/sidebar/SidebarContent.vue
Normal 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>
|
||||
14
src/components/ui/sidebar/SidebarFooter.vue
Normal file
14
src/components/ui/sidebar/SidebarFooter.vue
Normal 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>
|
||||
14
src/components/ui/sidebar/SidebarGroup.vue
Normal file
14
src/components/ui/sidebar/SidebarGroup.vue
Normal 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>
|
||||
31
src/components/ui/sidebar/SidebarGroupAction.vue
Normal file
31
src/components/ui/sidebar/SidebarGroupAction.vue
Normal 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>
|
||||
14
src/components/ui/sidebar/SidebarGroupContent.vue
Normal file
14
src/components/ui/sidebar/SidebarGroupContent.vue
Normal 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>
|
||||
30
src/components/ui/sidebar/SidebarGroupLabel.vue
Normal file
30
src/components/ui/sidebar/SidebarGroupLabel.vue
Normal 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>
|
||||
14
src/components/ui/sidebar/SidebarHeader.vue
Normal file
14
src/components/ui/sidebar/SidebarHeader.vue
Normal 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>
|
||||
15
src/components/ui/sidebar/SidebarInput.vue
Normal file
15
src/components/ui/sidebar/SidebarInput.vue
Normal 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>
|
||||
23
src/components/ui/sidebar/SidebarInset.vue
Normal file
23
src/components/ui/sidebar/SidebarInset.vue
Normal 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>
|
||||
14
src/components/ui/sidebar/SidebarMenu.vue
Normal file
14
src/components/ui/sidebar/SidebarMenu.vue
Normal 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>
|
||||
41
src/components/ui/sidebar/SidebarMenuAction.vue
Normal file
41
src/components/ui/sidebar/SidebarMenuAction.vue
Normal 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>
|
||||
28
src/components/ui/sidebar/SidebarMenuBadge.vue
Normal file
28
src/components/ui/sidebar/SidebarMenuBadge.vue
Normal 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>
|
||||
49
src/components/ui/sidebar/SidebarMenuButton.vue
Normal file
49
src/components/ui/sidebar/SidebarMenuButton.vue
Normal 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>
|
||||
36
src/components/ui/sidebar/SidebarMenuButtonChild.vue
Normal file
36
src/components/ui/sidebar/SidebarMenuButtonChild.vue
Normal 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>
|
||||
14
src/components/ui/sidebar/SidebarMenuItem.vue
Normal file
14
src/components/ui/sidebar/SidebarMenuItem.vue
Normal 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>
|
||||
27
src/components/ui/sidebar/SidebarMenuSkeleton.vue
Normal file
27
src/components/ui/sidebar/SidebarMenuSkeleton.vue
Normal 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>
|
||||
24
src/components/ui/sidebar/SidebarMenuSub.vue
Normal file
24
src/components/ui/sidebar/SidebarMenuSub.vue
Normal 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>
|
||||
43
src/components/ui/sidebar/SidebarMenuSubButton.vue
Normal file
43
src/components/ui/sidebar/SidebarMenuSubButton.vue
Normal 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>
|
||||
14
src/components/ui/sidebar/SidebarMenuSubItem.vue
Normal file
14
src/components/ui/sidebar/SidebarMenuSubItem.vue
Normal 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>
|
||||
92
src/components/ui/sidebar/SidebarProvider.vue
Normal file
92
src/components/ui/sidebar/SidebarProvider.vue
Normal 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>
|
||||
35
src/components/ui/sidebar/SidebarRail.vue
Normal file
35
src/components/ui/sidebar/SidebarRail.vue
Normal 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>
|
||||
15
src/components/ui/sidebar/SidebarSeparator.vue
Normal file
15
src/components/ui/sidebar/SidebarSeparator.vue
Normal 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>
|
||||
27
src/components/ui/sidebar/SidebarTrigger.vue
Normal file
27
src/components/ui/sidebar/SidebarTrigger.vue
Normal 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>
|
||||
60
src/components/ui/sidebar/index.ts
Normal file
60
src/components/ui/sidebar/index.ts
Normal 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>
|
||||
19
src/components/ui/sidebar/utils.ts
Normal file
19
src/components/ui/sidebar/utils.ts
Normal 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')
|
||||
17
src/components/ui/skeleton/Skeleton.vue
Normal file
17
src/components/ui/skeleton/Skeleton.vue
Normal 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>
|
||||
1
src/components/ui/skeleton/index.ts
Normal file
1
src/components/ui/skeleton/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as Skeleton } from "./Skeleton.vue"
|
||||
@@ -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>
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { default as Toaster } from "./Sonner.vue"
|
||||
export { default as Toaster } from './Sonner.vue'
|
||||
|
||||
15
src/components/ui/tooltip/Tooltip.vue
Normal file
15
src/components/ui/tooltip/Tooltip.vue
Normal 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>
|
||||
39
src/components/ui/tooltip/TooltipContent.vue
Normal file
39
src/components/ui/tooltip/TooltipContent.vue
Normal 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>
|
||||
14
src/components/ui/tooltip/TooltipProvider.vue
Normal file
14
src/components/ui/tooltip/TooltipProvider.vue
Normal 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>
|
||||
12
src/components/ui/tooltip/TooltipTrigger.vue
Normal file
12
src/components/ui/tooltip/TooltipTrigger.vue
Normal 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>
|
||||
4
src/components/ui/tooltip/index.ts
Normal file
4
src/components/ui/tooltip/index.ts
Normal 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'
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
// 军官类型枚举值到翻译键的映射
|
||||
|
||||
@@ -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 // 每级计算机技术全局增加的仓储
|
||||
}
|
||||
|
||||
@@ -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!'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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!'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: 'ゲームをリセットしてもよろしいですか?すべてのデータが削除されます!'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: '게임을 초기화하시겠습니까? 모든 데이터가 삭제됩니다!'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: 'Вы уверены, что хотите сбросить игру? Все данные будут удалены!'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: '确定要重置游戏吗?这将删除所有数据!'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: '確定要重置遊戲嗎?這將刪除所有資料!'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
93
src/logic/fleetStorageLogic.ts
Normal file
93
src/logic/fleetStorageLogic.ts
Normal 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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 星球对象
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
|
||||
@@ -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('/')
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
26
src/stores/universeStore.ts
Normal file
26
src/stores/universeStore.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -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
126
src/types/worker.ts
Normal 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
|
||||
@@ -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 {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
74
src/utils/migration.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -183,7 +183,8 @@
|
||||
[DefenseType.IonCannon]: 0,
|
||||
[DefenseType.PlasmaTurret]: 0,
|
||||
[DefenseType.SmallShieldDome]: 0,
|
||||
[DefenseType.LargeShieldDome]: 0
|
||||
[DefenseType.LargeShieldDome]: 0,
|
||||
[DefenseType.PlanetaryShield]: 0
|
||||
})
|
||||
|
||||
// 判断是否为护盾罩
|
||||
|
||||
@@ -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
291
src/views/GMView.vue
Normal 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>
|
||||
@@ -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({
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
|
||||
// 重置文件输入
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
470
src/workers/battle.worker.ts
Normal file
470
src/workers/battle.worker.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
234
src/workers/workerManager.ts
Normal file
234
src/workers/workerManager.ts
Normal 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()
|
||||
Reference in New Issue
Block a user