feat: 初始化项目结构与核心功能

引入项目基础目录结构,包含多语言支持、主要页面与组件、核心游戏逻辑、UI 组件库、加密与本地持久化、自动化 Docker 构建流程、GitHub issue 模板(中英文)、README(中英文)、LICENSE 及开发配置文件。实现 OGame 单机版主要功能模块,为后续开发和扩展奠定基础。
This commit is contained in:
谦君
2025-12-11 14:49:25 +08:00
commit 705ee8c3db
178 changed files with 17258 additions and 0 deletions

556
src/App.vue Normal file
View File

@@ -0,0 +1,556 @@
<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'
]"
>
<!-- 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>
</div>
</div>
<!-- 导航菜单 -->
<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] }}
</Button>
</div>
</PopoverContent>
</Popover>
</div>
<!-- 夜间模式切换 -->
<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>
<!-- 主内容区 -->
<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>
<!-- 资源显示 - 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>
<p
class="text-xs sm:text-sm font-medium truncate"
:class="getResourceColor(planet.resources[resourceType.key], capacity?.[resourceType.key] || Infinity)"
>
{{ formatNumber(planet.resources[resourceType.key]) }} / {{ formatNumber(capacity?.[resourceType.key] || 0) }}
</p>
<p class="text-[10px] sm:text-xs text-muted-foreground truncate">
+{{ formatNumber(production?.[resourceType.key] || 0) }}/{{ t('resources.perHour') }}
</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
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" />
</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>
</div>
<!-- 内容区域 -->
<main class="flex-1 overflow-y-auto">
<div class="animate-fade-in">
<RouterView />
</div>
</main>
</div>
<!-- 确认对话框 -->
<ConfirmDialog ref="confirmDialog" />
<!-- 详情弹窗 -->
<DetailDialog />
</div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, computed, ref } from 'vue'
import { RouterView, RouterLink } from 'vue-router'
import { useGameStore } from '@/stores/gameStore'
import { useTheme } from '@/composables/useTheme'
import { useI18n } from '@/composables/useI18n'
import { localeNames, detectBrowserLocale, type Locale } from '@/locales'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Progress } from '@/components/ui/progress'
import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover'
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 type { BuildQueueItem, FleetMission } from '@/types/game'
import { BUILDINGS, TECHNOLOGIES, SHIPS, DEFENSES } from '@/config/gameConfig'
import { formatNumber, formatTime, getResourceColor } from '@/utils/format'
import {
Moon,
Sun,
Home,
Building2,
FlaskConical,
Ship,
Rocket,
Shield,
Mail,
Globe,
ChevronLeft,
ChevronRight,
Menu,
X,
Users,
Swords,
Languages,
Settings
} 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'
import * as researchValidation from '@/logic/researchValidation'
import * as fleetLogic from '@/logic/fleetLogic'
import * as shipLogic from '@/logic/shipLogic'
import pkg from '../package.json'
const gameStore = useGameStore()
const { isDark } = useTheme()
const { t } = useI18n()
const confirmDialog = ref<InstanceType<typeof ConfirmDialog> | null>(null)
// 所有可用的语言选项
const locales: Locale[] = ['zh-CN', 'zh-TW', 'en', 'de', 'ru', 'ko', 'ja']
const initGame = () => {
const shouldInit = gameLogic.shouldInitializeGame(gameStore.player.planets)
if (!shouldInit) {
const now = Date.now()
gameLogic.updatePlanetsLastUpdate(gameStore.player.planets, now)
gameStore.player.planets.forEach(planet => {
const key = gameLogic.generatePositionKey(planet.position.galaxy, planet.position.system, planet.position.position)
gameStore.universePlanets[key] = planet
})
generateNPCPlanets()
return
}
gameStore.player = gameLogic.initializePlayer(gameStore.player.id, t('common.playerName'))
const initialPlanet = planetLogic.createInitialPlanet(gameStore.player.id, t('planet.homePlanet'))
gameStore.player.planets = [initialPlanet]
gameStore.currentPlanetId = initialPlanet.id
const key = gameLogic.generatePositionKey(initialPlanet.position.galaxy, initialPlanet.position.system, initialPlanet.position.position)
gameStore.universePlanets[key] = initialPlanet
}
const generateNPCPlanets = () => {
const npcCount = 200
for (let i = 0; i < npcCount; i++) {
const position = gameLogic.generateRandomPosition()
const key = gameLogic.generatePositionKey(position.galaxy, position.system, position.position)
if (gameStore.universePlanets[key]) continue
const npcPlanet = planetLogic.createNPCPlanet(i, position, t('planet.planetPrefix'))
gameStore.universePlanets[key] = npcPlanet
}
}
const updateGame = () => {
if (gameStore.isPaused) return
const now = Date.now()
gameStore.gameTime = now
gameLogic.checkOfficersExpiration(gameStore.player.officers, now)
const result = gameLogic.processGameUpdate(gameStore.player, now)
gameStore.player.researchQueue = result.updatedResearchQueue
gameStore.player.fleetMissions.forEach(mission => {
if (mission.status === 'outbound' && now >= mission.arrivalTime) {
processMissionArrival(mission)
} else if (mission.status === 'returning' && mission.returnTime && now >= mission.returnTime) {
processMissionReturn(mission)
}
})
}
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
)
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)
if (attackResult) {
gameStore.player.battleReports.push(attackResult.battleResult)
if (attackResult.moon) {
gameStore.player.planets.push(attackResult.moon)
}
}
} 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)
} else if (mission.missionType === MissionType.Spy) {
const spyReport = fleetLogic.processSpyArrival(mission, targetPlanet, gameStore.player.id)
if (spyReport) gameStore.player.spyReports.push(spyReport)
} else if (mission.missionType === MissionType.Deploy) {
const deployed = fleetLogic.processDeployArrival(mission, targetPlanet, gameStore.player.id)
if (deployed) {
const missionIndex = gameStore.player.fleetMissions.indexOf(mission)
if (missionIndex > -1) gameStore.player.fleetMissions.splice(missionIndex, 1)
return
}
}
}
const processMissionReturn = (mission: FleetMission) => {
const originPlanet = gameStore.player.planets.find(p => p.id === mission.originPlanetId)
if (!originPlanet) return
shipLogic.addFleet(originPlanet.fleet, mission.fleet)
resourceLogic.addResources(originPlanet.resources, mission.cargo)
const missionIndex = gameStore.player.fleetMissions.indexOf(mission)
if (missionIndex > -1) gameStore.player.fleetMissions.splice(missionIndex, 1)
}
// 初始化游戏
onMounted(() => {
// 如果是首次访问(没有星球数据),使用浏览器语言自动检测
const isFirstVisit = gameStore.player.planets.length === 0
if (isFirstVisit) {
gameStore.locale = detectBrowserLocale()
}
initGame()
// 启动游戏循环
const gameLoop = setInterval(() => {
updateGame()
}, 1000) // 每秒更新一次
// 清理定时器
onUnmounted(() => {
clearInterval(gameLoop)
})
})
const navItems = [
{ name: computed(() => t('nav.overview')), path: '/', icon: Home },
{ name: computed(() => t('nav.buildings')), path: '/buildings', icon: Building2 },
{ name: computed(() => t('nav.research')), path: '/research', icon: FlaskConical },
{ name: computed(() => t('nav.shipyard')), path: '/shipyard', icon: Ship },
{ name: computed(() => t('nav.defense')), path: '/defense', icon: Shield },
{ name: computed(() => t('nav.fleet')), path: '/fleet', icon: Rocket },
{ name: computed(() => t('nav.officers')), path: '/officers', icon: Users },
{ name: computed(() => t('nav.simulator')), path: '/battle-simulator', icon: Swords },
{ name: computed(() => t('nav.galaxy')), path: '/galaxy', icon: Globe },
{ name: computed(() => t('nav.messages')), path: '/messages', icon: Mail },
{ name: computed(() => t('nav.settings')), path: '/settings', icon: Settings }
]
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 energyConsumption = computed(() => {
if (!planet.value) return 0
return resourceLogic.calculateEnergyConsumption(planet.value)
})
// 资源类型配置
const resourceTypes = [
{ key: 'metal' as const },
{ key: 'crystal' as const },
{ key: 'deuterium' as const },
{ key: 'energy' as const },
{ key: 'darkMatter' as const }
]
// 月球相关
const moon = computed(() => {
if (!planet.value || planet.value.isMoon) return null
return gameStore.getMoonForPlanet(planet.value.id)
})
const hasMoon = computed(() => !!moon.value)
// 切换到月球
const switchToMoon = () => {
if (moon.value) {
gameStore.currentPlanetId = moon.value.id
}
}
// 切换回母星
const switchToParentPlanet = () => {
if (planet.value?.parentPlanetId) {
gameStore.currentPlanetId = planet.value.parentPlanetId
}
}
// 切换侧边栏
const toggleSidebar = () => {
gameStore.sidebarCollapsed = !gameStore.sidebarCollapsed
}
// 获取队列项的名称
const getItemName = (item: BuildQueueItem): string => {
if (item.type === 'building' || item.type === 'demolish') {
const buildingName = BUILDINGS[item.itemType as BuildingType]?.name || 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
} else if (item.type === 'ship') {
return SHIPS[item.itemType as ShipType]?.name || item.itemType
} else if (item.type === 'defense') {
return DEFENSES[item.itemType as DefenseType]?.name || item.itemType
}
return item.itemType
}
// 获取剩余时间
const getRemainingTime = (item: BuildQueueItem): number => {
const now = Date.now()
return Math.max(0, Math.floor((item.endTime - now) / 1000))
}
// 获取队列进度
const getQueueProgress = (item: BuildQueueItem): number => {
const now = Date.now()
const total = item.endTime - item.startTime
const elapsed = now - item.startTime
return Math.min(100, Math.max(0, (elapsed / total) * 100))
}
// 取消建造
const handleCancelBuild = (queueId: string) => {
confirmDialog.value?.show({
title: t('queue.cancelBuild'),
message: t('queue.confirmCancel'),
onConfirm: () => {
if (!gameStore.currentPlanet) return false
const { item, index } = buildingValidation.findQueueItem(gameStore.currentPlanet.buildQueue, queueId)
if (!item) return false
if (item.type === 'building') {
const refund = buildingValidation.cancelBuildingUpgrade(gameStore.currentPlanet, item)
resourceLogic.addResources(gameStore.currentPlanet.resources, refund)
}
gameStore.currentPlanet.buildQueue.splice(index, 1)
return true
}
})
}
// 取消研究
const handleCancelResearch = (queueId: string) => {
confirmDialog.value?.show({
title: t('queue.cancelResearch'),
message: t('queue.confirmCancel'),
onConfirm: () => {
if (!gameStore.currentPlanet) return false
const { item, index } = buildingValidation.findQueueItem(gameStore.player.researchQueue, queueId)
if (!item) return false
if (item.type === 'technology') {
const refund = researchValidation.cancelTechnologyResearch(item)
resourceLogic.addResources(gameStore.currentPlanet.resources, refund)
}
gameStore.player.researchQueue.splice(index, 1)
return true
}
})
}
</script>
<style scoped>
/* 平滑滚动 */
main {
scroll-behavior: smooth;
}
</style>

1
src/assets/logo.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,51 @@
<template>
<Teleport to="body">
<div v-if="isOpen" class="fixed inset-0 z-50 flex items-center justify-center">
<div class="fixed inset-0 bg-black/50" @click="handleClose" />
<div class="relative bg-card border rounded-lg shadow-lg p-6 max-w-md w-full mx-4 z-10">
<h2 class="text-lg font-semibold mb-2">{{ dialogProps?.title }}</h2>
<p class="text-sm text-muted-foreground mb-6 whitespace-pre-line">{{ dialogProps?.message }}</p>
<div class="flex justify-end gap-2">
<Button v-if="dialogProps?.onConfirm" @click="handleClose" variant="outline">{{ t('common.cancel') }}</Button>
<Button @click="handleConfirm" variant="default">{{ t('common.confirm') }}</Button>
</div>
</div>
</div>
</Teleport>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { Button } from '@/components/ui/button'
import { useI18n } from '@/composables/useI18n'
const { t } = useI18n()
interface AlertDialogProps {
title: string
message: string
onConfirm?: () => void
}
const isOpen = ref(false)
const dialogProps = ref<AlertDialogProps | null>(null)
const show = (props: AlertDialogProps) => {
dialogProps.value = props
isOpen.value = true
}
const handleConfirm = () => {
if (dialogProps.value?.onConfirm) {
dialogProps.value.onConfirm()
}
isOpen.value = false
}
const handleClose = () => {
isOpen.value = false
}
defineExpose({ show })
</script>

View File

@@ -0,0 +1,77 @@
<template>
<div v-if="!isUnlocked" class="absolute inset-0 z-10 bg-background/70 backdrop-blur-[2px] rounded-lg flex items-center justify-center">
<div class="text-center p-4 space-y-2">
<div class="flex justify-center">
<div class="rounded-full bg-muted p-2">
<Lock :size="20" class="text-muted-foreground" />
</div>
</div>
<p class="text-xs font-medium text-muted-foreground">{{ t('common.locked') }}</p>
<Button variant="outline" size="sm" @click="showRequirements" class="text-xs">
{{ t('common.viewRequirements') }}
</Button>
</div>
<!-- 前置条件详情对话框 -->
<AlertDialog ref="requirementsDialog" />
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { useGameStore } from '@/stores/gameStore'
import { useI18n } from '@/composables/useI18n'
import { useGameConfig } from '@/composables/useGameConfig'
import { BuildingType, TechnologyType } from '@/types/game'
import { Lock } from 'lucide-vue-next'
import { Button } from '@/components/ui/button'
import AlertDialog from '@/components/AlertDialog.vue'
import * as publicLogic from '@/logic/publicLogic'
interface Props {
requirements?: Partial<Record<BuildingType | TechnologyType, number>>
}
const props = defineProps<Props>()
const gameStore = useGameStore()
const { t } = useI18n()
const { BUILDINGS, TECHNOLOGIES } = useGameConfig()
const requirementsDialog = ref<InstanceType<typeof AlertDialog> | null>(null)
const isUnlocked = computed(() => {
if (!props.requirements || !gameStore.currentPlanet) return true
return publicLogic.checkRequirements(gameStore.currentPlanet, gameStore.player.technologies, props.requirements)
})
const getRequirementsList = (): string => {
if (!props.requirements || !gameStore.currentPlanet) return ''
const lines: string[] = []
for (const [key, requiredLevel] of Object.entries(props.requirements)) {
// 检查是否为建筑类型
if (Object.values(BuildingType).includes(key as BuildingType)) {
const buildingType = key as BuildingType
const currentLevel = gameStore.currentPlanet.buildings[buildingType] || 0
const name = BUILDINGS.value[buildingType]?.name || buildingType
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 techType = key as TechnologyType
const currentLevel = gameStore.player.technologies[techType] || 0
const name = TECHNOLOGIES.value[techType]?.name || techType
const status = currentLevel >= requiredLevel ? '✓' : '✗'
lines.push(`${status} ${name}: Lv ${requiredLevel} (${t('common.current')}: Lv ${currentLevel})`)
}
}
return lines.join('\n')
}
const showRequirements = () => {
requirementsDialog.value?.show({
title: t('common.requirementsNotMet'),
message: getRequirementsList()
})
}
</script>

View File

@@ -0,0 +1,51 @@
<template>
<Teleport to="body">
<div v-if="isOpen" class="fixed inset-0 z-50 flex items-center justify-center">
<div class="fixed inset-0 bg-black/50" @click="handleCancel" />
<div class="relative bg-card border rounded-lg shadow-lg p-6 max-w-md w-full mx-4 z-10">
<h2 class="text-lg font-semibold mb-2">{{ dialogProps?.title }}</h2>
<p class="text-sm text-muted-foreground mb-6">{{ dialogProps?.message }}</p>
<div class="flex justify-end gap-3">
<Button @click="handleCancel" variant="outline">{{ t('common.cancel') }}</Button>
<Button @click="handleConfirm" variant="default">{{ t('common.confirm') }}</Button>
</div>
</div>
</div>
</Teleport>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { Button } from '@/components/ui/button'
import { useI18n } from '@/composables/useI18n'
const { t } = useI18n()
interface ConfirmDialogProps {
title: string
message: string
onConfirm: () => void
}
const isOpen = ref(false)
const dialogProps = ref<ConfirmDialogProps | null>(null)
const show = (props: ConfirmDialogProps) => {
dialogProps.value = props
isOpen.value = true
}
const handleConfirm = () => {
if (dialogProps.value) {
dialogProps.value.onConfirm()
}
isOpen.value = false
}
const handleCancel = () => {
isOpen.value = false
}
defineExpose({ show })
</script>

View File

@@ -0,0 +1,84 @@
<template>
<Dialog :open="dialogStore.isOpen" @update:open="handleClose">
<DialogContent class="max-w-[calc(100%-1rem)] sm:max-w-[90vw] md:max-w-3xl lg:max-w-4xl max-h-[90vh] flex flex-col p-0">
<!-- 建筑详情 -->
<template v-if="dialogStore.type === 'building' && dialogStore.itemType">
<DialogHeader class="px-6 pt-6 pb-4 shrink-0">
<DialogTitle class="flex items-center gap-2">
{{ t(`buildings.${dialogStore.itemType}`) }}
<Badge variant="outline">{{ t('common.currentLevel') }} {{ dialogStore.currentLevel || 0 }}</Badge>
</DialogTitle>
<DialogDescription>
{{ t(`buildingDescriptions.${dialogStore.itemType}`) }}
</DialogDescription>
</DialogHeader>
<div class="overflow-y-auto px-6 pb-6">
<BuildingDetailView :buildingType="dialogStore.itemType as BuildingType" :currentLevel="dialogStore.currentLevel || 0" />
</div>
</template>
<!-- 科技详情 -->
<template v-else-if="dialogStore.type === 'technology' && dialogStore.itemType">
<DialogHeader class="px-6 pt-6 pb-4 shrink-0">
<DialogTitle class="flex items-center gap-2">
{{ t(`technologies.${dialogStore.itemType}`) }}
<Badge variant="outline">{{ t('common.currentLevel') }} {{ dialogStore.currentLevel || 0 }}</Badge>
</DialogTitle>
<DialogDescription>
{{ t(`technologyDescriptions.${dialogStore.itemType}`) }}
</DialogDescription>
</DialogHeader>
<div class="overflow-y-auto px-6 pb-6">
<TechnologyDetailView :technologyType="dialogStore.itemType as TechnologyType" :currentLevel="dialogStore.currentLevel || 0" />
</div>
</template>
<!-- 舰船详情 -->
<template v-else-if="dialogStore.type === 'ship' && dialogStore.itemType">
<DialogHeader class="px-6 pt-6 pb-4 shrink-0">
<DialogTitle>{{ t(`ships.${dialogStore.itemType}`) }}</DialogTitle>
<DialogDescription>
{{ t(`shipDescriptions.${dialogStore.itemType}`) }}
</DialogDescription>
</DialogHeader>
<div class="overflow-y-auto px-6 pb-6">
<ShipDetailView :shipType="dialogStore.itemType as ShipType" />
</div>
</template>
<!-- 防御详情 -->
<template v-else-if="dialogStore.type === 'defense' && dialogStore.itemType">
<DialogHeader class="px-6 pt-6 pb-4 shrink-0">
<DialogTitle>{{ t(`defenses.${dialogStore.itemType}`) }}</DialogTitle>
<DialogDescription>
{{ t(`defenseDescriptions.${dialogStore.itemType}`) }}
</DialogDescription>
</DialogHeader>
<div class="overflow-y-auto px-6 pb-6">
<DefenseDetailView :defenseType="dialogStore.itemType as DefenseType" />
</div>
</template>
</DialogContent>
</Dialog>
</template>
<script setup lang="ts">
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'
import { Badge } from '@/components/ui/badge'
import { useDetailDialogStore } from '@/stores/detailDialogStore'
import { useI18n } from '@/composables/useI18n'
import type { BuildingType, TechnologyType, ShipType, DefenseType } from '@/types/game'
import BuildingDetailView from './detail-views/BuildingDetailView.vue'
import TechnologyDetailView from './detail-views/TechnologyDetailView.vue'
import ShipDetailView from './detail-views/ShipDetailView.vue'
import DefenseDetailView from './detail-views/DefenseDetailView.vue'
const { t } = useI18n()
const dialogStore = useDetailDialogStore()
const handleClose = (open: boolean) => {
if (!open) {
dialogStore.close()
}
}
</script>

View File

@@ -0,0 +1,28 @@
<template>
<div :class="[colors[type], sizes[size], 'rounded shadow-sm']" />
</template>
<script setup lang="ts">
interface Props {
type: 'metal' | 'crystal' | 'deuterium' | 'darkMatter' | 'energy'
size?: 'sm' | 'md' | 'lg'
}
const props = withDefaults(defineProps<Props>(), {
size: 'md'
})
const colors = {
metal: 'bg-gradient-to-br from-slate-400 to-slate-600',
crystal: 'bg-gradient-to-br from-cyan-400 to-blue-600',
deuterium: 'bg-gradient-to-br from-green-400 to-emerald-600',
darkMatter: 'bg-gradient-to-br from-purple-600 to-indigo-900',
energy: 'bg-gradient-to-br from-yellow-400 to-orange-500'
}
const sizes = {
sm: 'w-3 h-3',
md: 'w-4 h-4',
lg: 'w-5 h-5'
}
</script>

View File

@@ -0,0 +1,73 @@
<template>
<div v-if="!isUnlocked" class="fixed inset-0 z-50 bg-background/80 backdrop-blur-sm flex items-center justify-center p-4">
<Card class="max-w-md w-full">
<CardHeader class="text-center">
<div class="flex justify-center mb-4">
<div class="rounded-full bg-muted p-4">
<Lock :size="48" class="text-muted-foreground" />
</div>
</div>
<CardTitle class="text-xl sm:text-2xl">{{ t('common.featureLocked') }}</CardTitle>
<CardDescription class="text-sm sm:text-base">{{ t('common.unlockRequired') }}</CardDescription>
</CardHeader>
<CardContent class="space-y-4">
<div class="p-4 bg-muted rounded-lg space-y-2">
<p class="text-sm font-medium text-center">{{ t('common.requiredBuilding') }}:</p>
<div class="flex items-center justify-center gap-2">
<span class="text-base sm:text-lg font-bold">{{ buildingName }}</span>
<Badge variant="default">Lv {{ requiredLevel }}</Badge>
</div>
<p v-if="currentLevel !== undefined" class="text-xs text-center text-muted-foreground">
{{ t('common.currentLevel') }}: Lv {{ currentLevel }}
</p>
</div>
<div class="flex gap-2">
<Button @click="goToBuildings" class="flex-1">
<Building2 :size="16" class="mr-2" />
{{ t('common.goToBuildings') }}
</Button>
</div>
</CardContent>
</Card>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import { useGameStore } from '@/stores/gameStore'
import { useI18n } from '@/composables/useI18n'
import { useGameConfig } from '@/composables/useGameConfig'
import { BuildingType } 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 { Lock, Building2 } from 'lucide-vue-next'
interface Props {
requiredBuilding: BuildingType
requiredLevel: number
}
const props = defineProps<Props>()
const router = useRouter()
const gameStore = useGameStore()
const { t } = useI18n()
const { BUILDINGS } = useGameConfig()
const buildingName = computed(() => BUILDINGS.value[props.requiredBuilding]?.name || props.requiredBuilding)
const currentLevel = computed(() => {
if (!gameStore.currentPlanet) return 0
return gameStore.currentPlanet.buildings[props.requiredBuilding] || 0
})
const isUnlocked = computed(() => {
return currentLevel.value >= props.requiredLevel
})
const goToBuildings = () => {
router.push('/buildings')
}
</script>

View File

@@ -0,0 +1,195 @@
<template>
<div class="space-y-4">
<!-- 建筑等级范围表格 -->
<div class="border rounded-lg overflow-hidden">
<Table>
<TableHeader>
<TableRow>
<TableHead class="w-20 text-center">{{ t('buildings.levelRange') }}</TableHead>
<TableHead class="text-center">{{ t('resources.metal') }}</TableHead>
<TableHead class="text-center">{{ t('resources.crystal') }}</TableHead>
<TableHead class="text-center">{{ t('resources.deuterium') }}</TableHead>
<TableHead class="text-center">{{ t('buildings.buildTime') }}</TableHead>
<TableHead class="text-center">{{ t('buildings.production') }}</TableHead>
<TableHead class="text-center">{{ t('buildings.consumption') }}</TableHead>
<TableHead class="text-center">{{ t('player.points') }}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="level in levelRange" :key="level" :class="{ 'bg-muted/50': level === currentLevel }">
<TableCell class="text-center font-medium">
<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">{{ 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') }}
</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) }}
</span>
<span v-else>-</span>
</TableCell>
<TableCell class="text-center text-sm">
<span class="text-primary font-medium">+{{ getLevelData(level).points }}</span>
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
<!-- 累积统计 -->
<div class="grid grid-cols-2 gap-4">
<Card>
<CardHeader class="pb-3">
<CardTitle class="text-sm">{{ t('buildings.totalCost') }}</CardTitle>
</CardHeader>
<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>
</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>
</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>
</div>
</CardContent>
</Card>
<Card>
<CardHeader class="pb-3">
<CardTitle class="text-sm">{{ t('buildings.totalPoints') }}</CardTitle>
</CardHeader>
<CardContent>
<div class="text-3xl font-bold text-primary">{{ formatNumber(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>
</CardContent>
</Card>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from '@/composables/useI18n'
import type { BuildingType } from '@/types/game'
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 * as buildingLogic from '@/logic/buildingLogic'
import * as pointsLogic from '@/logic/pointsLogic'
const { t } = useI18n()
const props = defineProps<{
buildingType: BuildingType
currentLevel: number
}>()
// 等级范围:当前等级 ±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++) {
levels.push(i)
}
return levels
})
// 获取某个等级的详细数据
const getLevelData = (level: number) => {
if (level === 0) {
return {
cost: { metal: 0, crystal: 0, deuterium: 0 },
buildTime: 0,
production: 0,
consumption: 0,
points: 0
}
}
const cost = buildingLogic.calculateBuildingCost(props.buildingType, level)
const buildTime = buildingLogic.calculateBuildingTime(props.buildingType, level)
// 计算产量和消耗
let production = 0
let consumption = 0
// 资源矿产量
if (props.buildingType === 'metalMine') {
production = Math.floor(30 * level * Math.pow(1.1, level))
} else if (props.buildingType === 'crystalMine') {
production = Math.floor(20 * level * Math.pow(1.1, level))
} else if (props.buildingType === 'deuteriumSynthesizer') {
production = Math.floor(10 * level * Math.pow(1.1, level))
}
// 能量产出
if (props.buildingType === 'solarPlant') {
production = Math.floor(20 * level * Math.pow(1.1, level))
}
// 能量消耗(矿场和合成器)
if (['metalMine', 'crystalMine', 'deuteriumSynthesizer'].includes(props.buildingType)) {
consumption = Math.floor(10 * level * Math.pow(1.1, level))
}
// 计算积分
const points = pointsLogic.calculateBuildingPoints(props.buildingType, level - 1, level)
return {
cost,
buildTime,
production,
consumption,
points
}
}
// 累积统计
const totalStats = computed(() => {
let metal = 0
let crystal = 0
let deuterium = 0
let points = 0
for (const level of levelRange.value) {
if (level === 0) continue
const data = getLevelData(level)
metal += data.cost.metal
crystal += data.cost.crystal
deuterium += data.cost.deuterium
points += data.points
}
return { metal, crystal, deuterium, points }
})
const formatNumber = (num: number): string => {
return num.toLocaleString()
}
const formatTime = (seconds: number): string => {
if (seconds < 60) return `${seconds}${t('common.timeSecond')}`
const minutes = Math.floor(seconds / 60)
const secs = seconds % 60
if (minutes < 60) return `${minutes}${t('common.timeMinute')}${secs}${t('common.timeSecond')}`
const hours = Math.floor(minutes / 60)
const mins = minutes % 60
return `${hours}${t('common.timeHour')}${mins}${t('common.timeMinute')}`
}
</script>

View File

@@ -0,0 +1,168 @@
<template>
<div class="space-y-4">
<!-- 防御基础信息 -->
<div class="grid grid-cols-2 md:grid-cols-3 gap-4">
<Card>
<CardHeader class="pb-3">
<CardTitle class="text-sm flex items-center gap-2">
<Sword class="h-4 w-4" />
{{ t('defense.attack') }}
</CardTitle>
</CardHeader>
<CardContent>
<div class="text-2xl font-bold">{{ formatNumber(config.attack) }}</div>
</CardContent>
</Card>
<Card>
<CardHeader class="pb-3">
<CardTitle class="text-sm flex items-center gap-2">
<Shield class="h-4 w-4" />
{{ t('defense.shield') }}
</CardTitle>
</CardHeader>
<CardContent>
<div class="text-2xl font-bold">{{ formatNumber(config.shield) }}</div>
</CardContent>
</Card>
<Card>
<CardHeader class="pb-3">
<CardTitle class="text-sm flex items-center gap-2">
<ShieldCheck class="h-4 w-4" />
{{ t('defense.armor') }}
</CardTitle>
</CardHeader>
<CardContent>
<div class="text-2xl font-bold">{{ formatNumber(config.armor) }}</div>
</CardContent>
</Card>
</div>
<!-- 建造成本和时间 -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card>
<CardHeader>
<CardTitle class="text-sm">{{ t('defense.buildCost') }}</CardTitle>
</CardHeader>
<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>
</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>
</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>
</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>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle class="text-sm">{{ t('defense.buildTime') }}</CardTitle>
</CardHeader>
<CardContent>
<div class="text-3xl font-bold">{{ formatTime(config.buildTime) }}</div>
<p class="text-xs text-muted-foreground mt-2">{{ t('defense.perUnit') }}</p>
</CardContent>
</Card>
</div>
<!-- 批量建造计算器 -->
<Card>
<CardHeader>
<CardTitle class="text-sm">{{ t('defense.batchCalculator') }}</CardTitle>
</CardHeader>
<CardContent class="space-y-4">
<div class="flex items-center gap-4">
<Label class="w-20">{{ t('defense.quantity') }}:</Label>
<Input v-model.number="quantity" type="number" min="1" class="flex-1" />
</div>
<div class="grid grid-cols-2 gap-4 pt-4 border-t">
<div class="space-y-2">
<p class="text-sm text-muted-foreground">{{ t('defense.totalCost') }}:</p>
<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>
</div>
<div class="flex justify-between">
<span>{{ t('resources.crystal') }}:</span>
<span class="font-medium">{{ formatNumber(batchCost.crystal) }}</span>
</div>
<div class="flex justify-between">
<span>{{ t('resources.deuterium') }}:</span>
<span class="font-medium">{{ formatNumber(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>
</div>
</div>
</CardContent>
</Card>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useI18n } from '@/composables/useI18n'
import type { DefenseType } from '@/types/game'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Sword, Shield, ShieldCheck } from 'lucide-vue-next'
import * as pointsLogic from '@/logic/pointsLogic'
import { DEFENSES } from '@/config/gameConfig'
const { t } = useI18n()
const props = defineProps<{
defenseType: DefenseType
}>()
const config = computed(() => DEFENSES[props.defenseType])
const quantity = ref(1)
// 单个防御的积分
const pointsPerUnit = computed(() => {
return pointsLogic.calculateDefensePoints(props.defenseType, 1)
})
// 批量建造成本
const batchCost = computed(() => ({
metal: config.value.cost.metal * quantity.value,
crystal: config.value.cost.crystal * quantity.value,
deuterium: config.value.cost.deuterium * quantity.value
}))
// 批量建造积分
const batchPoints = computed(() => {
return pointsLogic.calculateDefensePoints(props.defenseType, quantity.value)
})
const formatNumber = (num: number): string => {
return num.toLocaleString()
}
const formatTime = (seconds: number): string => {
if (seconds < 60) return `${seconds}${t('common.timeSecond')}`
const minutes = Math.floor(seconds / 60)
const secs = seconds % 60
if (minutes < 60) return `${minutes}${t('common.timeMinute')}${secs}${t('common.timeSecond')}`
const hours = Math.floor(minutes / 60)
const mins = minutes % 60
return `${hours}${t('common.timeHour')}${mins}${t('common.timeMinute')}`
}
</script>

View File

@@ -0,0 +1,204 @@
<template>
<div class="space-y-4">
<!-- 舰船基础信息 -->
<div class="grid grid-cols-2 md:grid-cols-3 gap-4">
<Card>
<CardHeader class="pb-3">
<CardTitle class="text-sm flex items-center gap-2">
<Sword class="h-4 w-4" />
{{ t('shipyard.attack') }}
</CardTitle>
</CardHeader>
<CardContent>
<div class="text-2xl font-bold">{{ formatNumber(config.attack) }}</div>
</CardContent>
</Card>
<Card>
<CardHeader class="pb-3">
<CardTitle class="text-sm flex items-center gap-2">
<Shield class="h-4 w-4" />
{{ t('shipyard.shield') }}
</CardTitle>
</CardHeader>
<CardContent>
<div class="text-2xl font-bold">{{ formatNumber(config.shield) }}</div>
</CardContent>
</Card>
<Card>
<CardHeader class="pb-3">
<CardTitle class="text-sm flex items-center gap-2">
<ShieldCheck class="h-4 w-4" />
{{ t('shipyard.armor') }}
</CardTitle>
</CardHeader>
<CardContent>
<div class="text-2xl font-bold">{{ formatNumber(config.armor) }}</div>
</CardContent>
</Card>
<Card>
<CardHeader class="pb-3">
<CardTitle class="text-sm flex items-center gap-2">
<Zap class="h-4 w-4" />
{{ t('shipyard.speed') }}
</CardTitle>
</CardHeader>
<CardContent>
<div class="text-2xl font-bold">{{ formatNumber(config.speed) }}</div>
</CardContent>
</Card>
<Card>
<CardHeader class="pb-3">
<CardTitle class="text-sm flex items-center gap-2">
<Package class="h-4 w-4" />
{{ t('shipyard.cargoCapacity') }}
</CardTitle>
</CardHeader>
<CardContent>
<div class="text-2xl font-bold">{{ formatNumber(config.cargoCapacity) }}</div>
</CardContent>
</Card>
<Card>
<CardHeader class="pb-3">
<CardTitle class="text-sm flex items-center gap-2">
<Fuel class="h-4 w-4" />
{{ t('shipyard.fuelConsumption') }}
</CardTitle>
</CardHeader>
<CardContent>
<div class="text-2xl font-bold">{{ formatNumber(config.fuelConsumption) }}</div>
</CardContent>
</Card>
</div>
<!-- 建造成本和时间 -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card>
<CardHeader>
<CardTitle class="text-sm">{{ t('shipyard.buildCost') }}</CardTitle>
</CardHeader>
<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>
</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>
</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>
</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>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle class="text-sm">{{ t('shipyard.buildTime') }}</CardTitle>
</CardHeader>
<CardContent>
<div class="text-3xl font-bold">{{ formatTime(config.buildTime) }}</div>
<p class="text-xs text-muted-foreground mt-2">{{ t('shipyard.perUnit') }}</p>
</CardContent>
</Card>
</div>
<!-- 批量建造计算器 -->
<Card>
<CardHeader>
<CardTitle class="text-sm">{{ t('shipyard.batchCalculator') }}</CardTitle>
</CardHeader>
<CardContent class="space-y-4">
<div class="flex items-center gap-4">
<Label class="w-20">{{ t('shipyard.quantity') }}:</Label>
<Input v-model.number="quantity" type="number" min="1" class="flex-1" />
</div>
<div class="grid grid-cols-2 gap-4 pt-4 border-t">
<div class="space-y-2">
<p class="text-sm text-muted-foreground">{{ t('shipyard.totalCost') }}:</p>
<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>
</div>
<div class="flex justify-between">
<span>{{ t('resources.crystal') }}:</span>
<span class="font-medium">{{ formatNumber(batchCost.crystal) }}</span>
</div>
<div class="flex justify-between">
<span>{{ t('resources.deuterium') }}:</span>
<span class="font-medium">{{ formatNumber(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>
</div>
</div>
</CardContent>
</Card>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useI18n } from '@/composables/useI18n'
import type { ShipType } from '@/types/game'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Sword, Shield, ShieldCheck, Zap, Package, Fuel } from 'lucide-vue-next'
import * as pointsLogic from '@/logic/pointsLogic'
import { SHIPS } from '@/config/gameConfig'
const { t } = useI18n()
const props = defineProps<{
shipType: ShipType
}>()
const config = computed(() => SHIPS[props.shipType])
const quantity = ref(1)
// 单艘舰船的积分
const pointsPerUnit = computed(() => {
return pointsLogic.calculateShipPoints(props.shipType, 1)
})
// 批量建造成本
const batchCost = computed(() => ({
metal: config.value.cost.metal * quantity.value,
crystal: config.value.cost.crystal * quantity.value,
deuterium: config.value.cost.deuterium * quantity.value
}))
// 批量建造积分
const batchPoints = computed(() => {
return pointsLogic.calculateShipPoints(props.shipType, quantity.value)
})
const formatNumber = (num: number): string => {
return num.toLocaleString()
}
const formatTime = (seconds: number): string => {
if (seconds < 60) return `${seconds}${t('common.timeSecond')}`
const minutes = Math.floor(seconds / 60)
const secs = seconds % 60
if (minutes < 60) return `${minutes}${t('common.timeMinute')}${secs}${t('common.timeSecond')}`
const hours = Math.floor(minutes / 60)
const mins = minutes % 60
return `${hours}${t('common.timeHour')}${mins}${t('common.timeMinute')}`
}
</script>

View File

@@ -0,0 +1,154 @@
<template>
<div class="space-y-4">
<!-- 科技等级范围表格 -->
<div class="border rounded-lg overflow-hidden">
<Table>
<TableHeader>
<TableRow>
<TableHead class="w-20 text-center">{{ t('research.levelRange') }}</TableHead>
<TableHead class="text-center">{{ t('resources.metal') }}</TableHead>
<TableHead class="text-center">{{ t('resources.crystal') }}</TableHead>
<TableHead class="text-center">{{ t('resources.deuterium') }}</TableHead>
<TableHead class="text-center">{{ t('research.researchTime') }}</TableHead>
<TableHead class="text-center">{{ t('player.points') }}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="level in levelRange" :key="level" :class="{ 'bg-muted/50': level === currentLevel }">
<TableCell class="text-center font-medium">
<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">{{ formatTime(getLevelData(level).researchTime) }}</TableCell>
<TableCell class="text-center text-sm">
<span class="text-primary font-medium">+{{ getLevelData(level).points }}</span>
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
<!-- 累积统计 -->
<div class="grid grid-cols-2 gap-4">
<Card>
<CardHeader class="pb-3">
<CardTitle class="text-sm">{{ t('research.totalCost') }}</CardTitle>
</CardHeader>
<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>
</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>
</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>
</div>
</CardContent>
</Card>
<Card>
<CardHeader class="pb-3">
<CardTitle class="text-sm">{{ t('research.totalPoints') }}</CardTitle>
</CardHeader>
<CardContent>
<div class="text-3xl font-bold text-primary">{{ formatNumber(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>
</CardContent>
</Card>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from '@/composables/useI18n'
import type { TechnologyType } from '@/types/game'
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 * as researchLogic from '@/logic/researchLogic'
import * as pointsLogic from '@/logic/pointsLogic'
const { t } = useI18n()
const props = defineProps<{
technologyType: TechnologyType
currentLevel: number
}>()
// 等级范围:当前等级 ±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++) {
levels.push(i)
}
return levels
})
// 获取某个等级的详细数据
const getLevelData = (level: number) => {
if (level === 0) {
return {
cost: { metal: 0, crystal: 0, deuterium: 0 },
researchTime: 0,
points: 0
}
}
const cost = researchLogic.calculateTechnologyCost(props.technologyType, level)
const researchTime = researchLogic.calculateTechnologyTime(props.technologyType, level - 1)
// 计算积分
const points = pointsLogic.calculateTechnologyPoints(props.technologyType, level - 1, level)
return {
cost,
researchTime,
points
}
}
// 累积统计
const totalStats = computed(() => {
let metal = 0
let crystal = 0
let deuterium = 0
let points = 0
for (const level of levelRange.value) {
if (level === 0) continue
const data = getLevelData(level)
metal += data.cost.metal
crystal += data.cost.crystal
deuterium += data.cost.deuterium
points += data.points
}
return { metal, crystal, deuterium, points }
})
const formatNumber = (num: number): string => {
return num.toLocaleString()
}
const formatTime = (seconds: number): string => {
if (seconds < 60) return `${seconds}${t('common.timeSecond')}`
const minutes = Math.floor(seconds / 60)
const secs = seconds % 60
if (minutes < 60) return `${minutes}${t('common.timeMinute')}${secs}${t('common.timeSecond')}`
const hours = Math.floor(minutes / 60)
const mins = minutes % 60
return `${hours}${t('common.timeHour')}${mins}${t('common.timeMinute')}`
}
</script>

View File

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

View File

@@ -0,0 +1,18 @@
<template>
<AlertDialogAction v-bind="delegatedProps" :class="cn(buttonVariants(), props.class)">
<slot />
</AlertDialogAction>
</template>
<script setup lang="ts">
import type { AlertDialogActionProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { AlertDialogAction } from 'reka-ui'
import { cn } from '@/lib/utils'
import { buttonVariants } from '@/components/ui/button'
const props = defineProps<AlertDialogActionProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
</script>

View File

@@ -0,0 +1,18 @@
<template>
<AlertDialogCancel v-bind="delegatedProps" :class="cn(buttonVariants({ variant: 'outline' }), 'mt-2 sm:mt-0', props.class)">
<slot />
</AlertDialogCancel>
</template>
<script setup lang="ts">
import type { AlertDialogCancelProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { AlertDialogCancel } from 'reka-ui'
import { cn } from '@/lib/utils'
import { buttonVariants } from '@/components/ui/button'
const props = defineProps<AlertDialogCancelProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
</script>

View File

@@ -0,0 +1,39 @@
<template>
<AlertDialogPortal>
<AlertDialogOverlay
data-slot="alert-dialog-overlay"
class="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"
/>
<AlertDialogContent
data-slot="alert-dialog-content"
v-bind="{ ...$attrs, ...forwarded }"
:class="
cn(
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
props.class
)
"
>
<slot />
</AlertDialogContent>
</AlertDialogPortal>
</template>
<script setup lang="ts">
import type { AlertDialogContentEmits, AlertDialogContentProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { AlertDialogContent, AlertDialogOverlay, AlertDialogPortal, useForwardPropsEmits } from 'reka-ui'
import { cn } from '@/lib/utils'
defineOptions({
inheritAttrs: false
})
const props = defineProps<AlertDialogContentProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<AlertDialogContentEmits>()
const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>

View File

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

View File

@@ -0,0 +1,14 @@
<template>
<div data-slot="alert-dialog-footer" :class="cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', props.class)">
<slot />
</div>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,9 @@
export { default as AlertDialog } from './AlertDialog.vue'
export { default as AlertDialogAction } from './AlertDialogAction.vue'
export { default as AlertDialogCancel } from './AlertDialogCancel.vue'
export { default as AlertDialogContent } from './AlertDialogContent.vue'
export { default as AlertDialogDescription } from './AlertDialogDescription.vue'
export { default as AlertDialogFooter } from './AlertDialogFooter.vue'
export { default as AlertDialogHeader } from './AlertDialogHeader.vue'
export { default as AlertDialogTitle } from './AlertDialogTitle.vue'
export { default as AlertDialogTrigger } from './AlertDialogTrigger.vue'

View File

@@ -0,0 +1,24 @@
<template>
<Primitive data-slot="badge" :class="cn(badgeVariants({ variant }), props.class)" v-bind="delegatedProps">
<slot />
</Primitive>
</template>
<script setup lang="ts">
import type { PrimitiveProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import type { BadgeVariants } from '.'
import { reactiveOmit } from '@vueuse/core'
import { Primitive } from 'reka-ui'
import { cn } from '@/lib/utils'
import { badgeVariants } from '.'
const props = defineProps<
PrimitiveProps & {
variant?: BadgeVariants['variant']
class?: HTMLAttributes['class']
}
>()
const delegatedProps = reactiveOmit(props, 'class')
</script>

View File

@@ -0,0 +1,23 @@
import type { VariantProps } from 'class-variance-authority'
import { cva } from 'class-variance-authority'
export { default as Badge } from './Badge.vue'
export const badgeVariants = cva(
'inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden',
{
variants: {
variant: {
default: 'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90',
secondary: 'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90',
destructive:
'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
outline: 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground'
}
},
defaultVariants: {
variant: 'default'
}
}
)
export type BadgeVariants = VariantProps<typeof badgeVariants>

View File

@@ -0,0 +1,24 @@
<template>
<Primitive data-slot="button" :as="as" :as-child="asChild" :class="cn(buttonVariants({ variant, size }), props.class)">
<slot />
</Primitive>
</template>
<script setup lang="ts">
import type { PrimitiveProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import type { ButtonVariants } from '.'
import { Primitive } from 'reka-ui'
import { cn } from '@/lib/utils'
import { buttonVariants } from '.'
interface Props extends PrimitiveProps {
variant?: ButtonVariants['variant']
size?: ButtonVariants['size']
class?: HTMLAttributes['class']
}
const props = withDefaults(defineProps<Props>(), {
as: 'button'
})
</script>

View File

@@ -0,0 +1,35 @@
import type { VariantProps } from 'class-variance-authority'
import { cva } from 'class-variance-authority'
export { default as Button } from './Button.vue'
export const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive:
'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
outline:
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
link: 'text-primary underline-offset-4 hover:underline'
},
size: {
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
icon: 'size-9',
'icon-sm': 'size-8',
'icon-lg': 'size-10'
}
},
defaultVariants: {
variant: 'default',
size: 'default'
}
}
)
export type ButtonVariants = VariantProps<typeof buttonVariants>

View File

@@ -0,0 +1,14 @@
<template>
<div data-slot="card" :class="cn('bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm', props.class)">
<slot />
</div>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>

View File

@@ -0,0 +1,14 @@
<template>
<div data-slot="card-action" :class="cn('col-start-2 row-span-2 row-start-1 self-start justify-self-end', props.class)">
<slot />
</div>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>

View File

@@ -0,0 +1,14 @@
<template>
<div data-slot="card-content" :class="cn('px-6', props.class)">
<slot />
</div>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>

View File

@@ -0,0 +1,14 @@
<template>
<p data-slot="card-description" :class="cn('text-muted-foreground text-sm', props.class)">
<slot />
</p>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>

View File

@@ -0,0 +1,14 @@
<template>
<div data-slot="card-footer" :class="cn('flex items-center px-6 [.border-t]:pt-6', props.class)">
<slot />
</div>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>

View File

@@ -0,0 +1,22 @@
<template>
<div
data-slot="card-header"
:class="
cn(
'@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6',
props.class
)
"
>
<slot />
</div>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>

View File

@@ -0,0 +1,14 @@
<template>
<h3 data-slot="card-title" :class="cn('leading-none font-semibold', props.class)">
<slot />
</h3>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>

View File

@@ -0,0 +1,7 @@
export { default as Card } from './Card.vue'
export { default as CardAction } from './CardAction.vue'
export { default as CardContent } from './CardContent.vue'
export { default as CardDescription } from './CardDescription.vue'
export { default as CardFooter } from './CardFooter.vue'
export { default as CardHeader } from './CardHeader.vue'
export { default as CardTitle } from './CardTitle.vue'

View File

@@ -0,0 +1,15 @@
<template>
<DialogRoot v-slot="slotProps" data-slot="dialog" v-bind="forwarded">
<slot v-bind="slotProps" />
</DialogRoot>
</template>
<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>

View File

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

View File

@@ -0,0 +1,49 @@
<template>
<DialogPortal>
<DialogOverlay />
<DialogContent
data-slot="dialog-content"
v-bind="{ ...$attrs, ...forwarded }"
:class="
cn(
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
props.class
)
"
>
<slot />
<DialogClose
v-if="showCloseButton"
data-slot="dialog-close"
class="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground 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 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<X />
<span class="sr-only">Close</span>
</DialogClose>
</DialogContent>
</DialogPortal>
</template>
<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 DialogOverlay from './DialogOverlay.vue'
defineOptions({
inheritAttrs: false
})
const props = withDefaults(defineProps<DialogContentProps & { class?: HTMLAttributes['class']; showCloseButton?: boolean }>(), {
showCloseButton: true
})
const emits = defineEmits<DialogContentEmits>()
const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>

View File

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

View File

@@ -0,0 +1,12 @@
<template>
<div data-slot="dialog-footer" :class="cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', props.class)">
<slot />
</div>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{ class?: HTMLAttributes['class'] }>()
</script>

View File

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

View File

@@ -0,0 +1,26 @@
<template>
<DialogOverlay
data-slot="dialog-overlay"
v-bind="delegatedProps"
: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
)
"
>
<slot />
</DialogOverlay>
</template>
<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>

View File

@@ -0,0 +1,51 @@
<template>
<DialogPortal>
<DialogOverlay
class="fixed inset-0 z-50 grid place-items-center overflow-y-auto bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
>
<DialogContent
:class="
cn(
'relative z-50 grid w-full max-w-lg my-8 gap-4 border border-border bg-background p-6 shadow-lg duration-200 sm:rounded-lg md:w-full',
props.class
)
"
v-bind="{ ...$attrs, ...forwarded }"
@pointer-down-outside="(event) => {
const originalEvent = event.detail.originalEvent;
const target = originalEvent.target as HTMLElement;
if (originalEvent.offsetX > target.clientWidth || originalEvent.offsetY > target.clientHeight) {
event.preventDefault();
}
}"
>
<slot />
<DialogClose class="absolute top-4 right-4 p-0.5 transition-colors rounded-md hover:bg-secondary">
<X class="w-4 h-4" />
<span class="sr-only">Close</span>
</DialogClose>
</DialogContent>
</DialogOverlay>
</DialogPortal>
</template>
<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, DialogOverlay, DialogPortal, useForwardPropsEmits } from 'reka-ui'
import { cn } from '@/lib/utils'
defineOptions({
inheritAttrs: false
})
const props = defineProps<DialogContentProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<DialogContentEmits>()
const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>

View File

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

View File

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

View File

@@ -0,0 +1,10 @@
export { default as Dialog } from './Dialog.vue'
export { default as DialogClose } from './DialogClose.vue'
export { default as DialogContent } from './DialogContent.vue'
export { default as DialogDescription } from './DialogDescription.vue'
export { default as DialogFooter } from './DialogFooter.vue'
export { default as DialogHeader } from './DialogHeader.vue'
export { default as DialogOverlay } from './DialogOverlay.vue'
export { default as DialogScrollContent } from './DialogScrollContent.vue'
export { default as DialogTitle } from './DialogTitle.vue'
export { default as DialogTrigger } from './DialogTrigger.vue'

View File

@@ -0,0 +1,35 @@
<template>
<input
v-model="modelValue"
data-slot="input"
:class="
cn(
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
props.class
)
"
/>
</template>
<script setup lang="ts">
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 emits = defineEmits<{
(e: 'update:modelValue', payload: string | number): void
}>()
const modelValue = useVModel(props, 'modelValue', emits, {
passive: true,
defaultValue: props.defaultValue
})
</script>

View File

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

View File

@@ -0,0 +1,26 @@
<template>
<Label
data-slot="label"
v-bind="delegatedProps"
:class="
cn(
'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
props.class
)
"
>
<slot />
</Label>
</template>
<script setup lang="ts">
import type { LabelProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { Label } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<LabelProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
</script>

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,45 @@
<script setup lang="ts">
import type { PopoverContentEmits, PopoverContentProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import {
PopoverContent,
PopoverPortal,
useForwardPropsEmits,
} from "reka-ui"
import { cn } from "@/lib/utils"
defineOptions({
inheritAttrs: false,
})
const props = withDefaults(
defineProps<PopoverContentProps & { class?: HTMLAttributes["class"] }>(),
{
align: "center",
sideOffset: 4,
},
)
const emits = defineEmits<PopoverContentEmits>()
const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<PopoverPortal>
<PopoverContent
data-slot="popover-content"
v-bind="{ ...$attrs, ...forwarded }"
:class="
cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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-72 rounded-md border p-4 shadow-md origin-(--reka-popover-content-transform-origin) outline-hidden',
props.class,
)
"
>
<slot />
</PopoverContent>
</PopoverPortal>
</template>

View File

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

View File

@@ -0,0 +1,4 @@
export { default as Popover } from "./Popover.vue"
export { default as PopoverAnchor } from "./PopoverAnchor.vue"
export { default as PopoverContent } from "./PopoverContent.vue"
export { default as PopoverTrigger } from "./PopoverTrigger.vue"

View File

@@ -0,0 +1,27 @@
<template>
<ProgressRoot
data-slot="progress"
v-bind="delegatedProps"
:class="cn('bg-primary/20 relative h-2 w-full overflow-hidden rounded-full', props.class)"
>
<ProgressIndicator
data-slot="progress-indicator"
class="bg-primary h-full w-full flex-1 transition-all"
:style="`transform: translateX(-${100 - (props.modelValue ?? 0)}%);`"
/>
</ProgressRoot>
</template>
<script setup lang="ts">
import type { ProgressRootProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { ProgressIndicator, ProgressRoot } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = withDefaults(defineProps<ProgressRootProps & { class?: HTMLAttributes['class'] }>(), {
modelValue: 0
})
const delegatedProps = reactiveOmit(props, 'class')
</script>

View File

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

View File

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

View File

@@ -0,0 +1,51 @@
<template>
<SelectPortal>
<SelectContent
data-slot="select-content"
v-bind="{ ...$attrs, ...forwarded }"
:class="
cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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 relative z-50 max-h-(--reka-select-content-available-height) min-w-[8rem] overflow-x-hidden overflow-y-auto rounded-md border shadow-md',
position === 'popper' &&
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
props.class
)
"
>
<SelectScrollUpButton />
<SelectViewport
:class="
cn(
'p-1',
position === 'popper' && 'h-[var(--reka-select-trigger-height)] w-full min-w-[var(--reka-select-trigger-width)] scroll-my-1'
)
"
>
<slot />
</SelectViewport>
<SelectScrollDownButton />
</SelectContent>
</SelectPortal>
</template>
<script setup lang="ts">
import type { SelectContentEmits, SelectContentProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { SelectContent, SelectPortal, SelectViewport, useForwardPropsEmits } from 'reka-ui'
import { cn } from '@/lib/utils'
import { SelectScrollDownButton, SelectScrollUpButton } from '.'
defineOptions({
inheritAttrs: false
})
const props = withDefaults(defineProps<SelectContentProps & { class?: HTMLAttributes['class'] }>(), {
position: 'popper'
})
const emits = defineEmits<SelectContentEmits>()
const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>

View File

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

View File

@@ -0,0 +1,39 @@
<template>
<SelectItem
data-slot="select-item"
v-bind="forwardedProps"
:class="
cn(
'focus:bg-accent focus:text-accent-foreground [&_svg:not([class*=\'text-\'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2',
props.class
)
"
>
<span class="absolute right-2 flex size-3.5 items-center justify-center">
<SelectItemIndicator>
<slot name="indicator-icon">
<Check class="size-4" />
</slot>
</SelectItemIndicator>
</span>
<SelectItemText>
<slot />
</SelectItemText>
</SelectItem>
</template>
<script setup lang="ts">
import type { SelectItemProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { Check } from 'lucide-vue-next'
import { SelectItem, SelectItemIndicator, SelectItemText, useForwardProps } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<SelectItemProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
const forwardedProps = useForwardProps(delegatedProps)
</script>

View File

@@ -0,0 +1,12 @@
<template>
<SelectItemText data-slot="select-item-text" v-bind="props">
<slot />
</SelectItemText>
</template>
<script setup lang="ts">
import type { SelectItemTextProps } from 'reka-ui'
import { SelectItemText } from 'reka-ui'
const props = defineProps<SelectItemTextProps>()
</script>

View File

@@ -0,0 +1,14 @@
<template>
<SelectLabel data-slot="select-label" :class="cn('text-muted-foreground px-2 py-1.5 text-xs', props.class)">
<slot />
</SelectLabel>
</template>
<script setup lang="ts">
import type { SelectLabelProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { SelectLabel } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<SelectLabelProps & { class?: HTMLAttributes['class'] }>()
</script>

View File

@@ -0,0 +1,26 @@
<template>
<SelectScrollDownButton
data-slot="select-scroll-down-button"
v-bind="forwardedProps"
:class="cn('flex cursor-default items-center justify-center py-1', props.class)"
>
<slot>
<ChevronDown class="size-4" />
</slot>
</SelectScrollDownButton>
</template>
<script setup lang="ts">
import type { SelectScrollDownButtonProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { ChevronDown } from 'lucide-vue-next'
import { SelectScrollDownButton, useForwardProps } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<SelectScrollDownButtonProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
const forwardedProps = useForwardProps(delegatedProps)
</script>

View File

@@ -0,0 +1,26 @@
<template>
<SelectScrollUpButton
data-slot="select-scroll-up-button"
v-bind="forwardedProps"
:class="cn('flex cursor-default items-center justify-center py-1', props.class)"
>
<slot>
<ChevronUp class="size-4" />
</slot>
</SelectScrollUpButton>
</template>
<script setup lang="ts">
import type { SelectScrollUpButtonProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { ChevronUp } from 'lucide-vue-next'
import { SelectScrollUpButton, useForwardProps } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<SelectScrollUpButtonProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
const forwardedProps = useForwardProps(delegatedProps)
</script>

View File

@@ -0,0 +1,19 @@
<template>
<SelectSeparator
data-slot="select-separator"
v-bind="delegatedProps"
:class="cn('bg-border pointer-events-none -mx-1 my-1 h-px', props.class)"
/>
</template>
<script setup lang="ts">
import type { SelectSeparatorProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { SelectSeparator } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<SelectSeparatorProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
</script>

View File

@@ -0,0 +1,34 @@
<template>
<SelectTrigger
data-slot="select-trigger"
:data-size="size"
v-bind="forwardedProps"
:class="
cn(
'border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*=\'text-\'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
props.class
)
"
>
<slot />
<SelectIcon as-child>
<ChevronDown class="size-4 opacity-50" />
</SelectIcon>
</SelectTrigger>
</template>
<script setup lang="ts">
import type { SelectTriggerProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { ChevronDown } from 'lucide-vue-next'
import { SelectIcon, SelectTrigger, useForwardProps } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = withDefaults(defineProps<SelectTriggerProps & { class?: HTMLAttributes['class']; size?: 'sm' | 'default' }>(), {
size: 'default'
})
const delegatedProps = reactiveOmit(props, 'class', 'size')
const forwardedProps = useForwardProps(delegatedProps)
</script>

View File

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

View File

@@ -0,0 +1,11 @@
export { default as Select } from './Select.vue'
export { default as SelectContent } from './SelectContent.vue'
export { default as SelectGroup } from './SelectGroup.vue'
export { default as SelectItem } from './SelectItem.vue'
export { default as SelectItemText } from './SelectItemText.vue'
export { default as SelectLabel } from './SelectLabel.vue'
export { default as SelectScrollDownButton } from './SelectScrollDownButton.vue'
export { default as SelectScrollUpButton } from './SelectScrollUpButton.vue'
export { default as SelectSeparator } from './SelectSeparator.vue'
export { default as SelectTrigger } from './SelectTrigger.vue'
export { default as SelectValue } from './SelectValue.vue'

View File

@@ -0,0 +1,42 @@
<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)"
:style="{
'--normal-bg': 'var(--popover)',
'--normal-text': 'var(--popover-foreground)',
'--normal-border': 'var(--border)',
'--border-radius': 'var(--radius)',
}"
v-bind="props"
>
<template #success-icon>
<CircleCheckIcon class="size-4" />
</template>
<template #info-icon>
<InfoIcon class="size-4" />
</template>
<template #warning-icon>
<TriangleAlertIcon class="size-4" />
</template>
<template #error-icon>
<OctagonXIcon class="size-4" />
</template>
<template #loading-icon>
<div>
<Loader2Icon class="size-4 animate-spin" />
</div>
</template>
<template #close-icon>
<XIcon class="size-4" />
</template>
</Sonner>
</template>

View File

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

View File

@@ -0,0 +1,16 @@
<template>
<div data-slot="table-container" class="relative w-full overflow-auto">
<table data-slot="table" :class="cn('w-full caption-bottom text-sm', props.class)">
<slot />
</table>
</div>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>

View File

@@ -0,0 +1,14 @@
<template>
<tbody data-slot="table-body" :class="cn('[&_tr:last-child]:border-0', props.class)">
<slot />
</tbody>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>

View File

@@ -0,0 +1,14 @@
<template>
<caption data-slot="table-caption" :class="cn('text-muted-foreground mt-4 text-sm', props.class)">
<slot />
</caption>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>

View File

@@ -0,0 +1,17 @@
<template>
<td
data-slot="table-cell"
:class="cn('p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]', props.class)"
>
<slot />
</td>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>

View File

@@ -0,0 +1,29 @@
<template>
<TableRow>
<TableCell :class="cn('p-4 whitespace-nowrap align-middle text-sm text-foreground', props.class)" v-bind="delegatedProps">
<div class="flex items-center justify-center py-10">
<slot />
</div>
</TableCell>
</TableRow>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { cn } from '@/lib/utils'
import TableCell from './TableCell.vue'
import TableRow from './TableRow.vue'
const props = withDefaults(
defineProps<{
class?: HTMLAttributes['class']
colspan?: number
}>(),
{
colspan: 1
}
)
const delegatedProps = reactiveOmit(props, 'class')
</script>

View File

@@ -0,0 +1,14 @@
<template>
<tfoot data-slot="table-footer" :class="cn('bg-muted/50 border-t font-medium [&>tr]:last:border-b-0', props.class)">
<slot />
</tfoot>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>

View File

@@ -0,0 +1,22 @@
<template>
<th
data-slot="table-head"
:class="
cn(
'text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
props.class
)
"
>
<slot />
</th>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>

View File

@@ -0,0 +1,14 @@
<template>
<thead data-slot="table-header" :class="cn('[&_tr]:border-b', props.class)">
<slot />
</thead>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>

View File

@@ -0,0 +1,14 @@
<template>
<tr data-slot="table-row" :class="cn('hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors', props.class)">
<slot />
</tr>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>

View File

@@ -0,0 +1,9 @@
export { default as Table } from './Table.vue'
export { default as TableBody } from './TableBody.vue'
export { default as TableCaption } from './TableCaption.vue'
export { default as TableCell } from './TableCell.vue'
export { default as TableEmpty } from './TableEmpty.vue'
export { default as TableFooter } from './TableFooter.vue'
export { default as TableHead } from './TableHead.vue'
export { default as TableHeader } from './TableHeader.vue'
export { default as TableRow } from './TableRow.vue'

View File

@@ -0,0 +1,8 @@
import type { Updater } from '@tanstack/vue-table'
import type { Ref } from 'vue'
import { isFunction } from '@tanstack/vue-table'
export const valueUpdater = <T>(updaterOrValue: Updater<T>, ref: Ref<T>) => {
ref.value = isFunction(updaterOrValue) ? updaterOrValue(ref.value) : updaterOrValue
}

View File

@@ -0,0 +1,173 @@
import { computed } from 'vue'
import { useI18n } from './useI18n'
import {
BUILDINGS as ORIGINAL_BUILDINGS,
SHIPS as ORIGINAL_SHIPS,
DEFENSES as ORIGINAL_DEFENSES,
TECHNOLOGIES as ORIGINAL_TECHNOLOGIES,
OFFICERS as ORIGINAL_OFFICERS
} from '@/config/gameConfig'
import { BuildingType, ShipType, DefenseType, TechnologyType, OfficerType } from '@/types/game'
import type { BuildingConfig, ShipConfig, DefenseConfig, TechnologyConfig, OfficerConfig } from '@/types/game'
/**
* 提供翻译后的游戏配置对象的 Composable
* 将 i18n 系统的翻译覆盖到原始 gameConfig 上
*/
export const useGameConfig = () => {
const { t } = useI18n()
// 建筑类型枚举值到翻译键的映射
const buildingKeyMap: Record<BuildingType, string> = {
[BuildingType.MetalMine]: 'metalMine',
[BuildingType.CrystalMine]: 'crystalMine',
[BuildingType.DeuteriumSynthesizer]: 'deuteriumSynthesizer',
[BuildingType.SolarPlant]: 'solarPlant',
[BuildingType.RoboticsFactory]: 'roboticsFactory',
[BuildingType.NaniteFactory]: 'naniteFactory',
[BuildingType.Shipyard]: 'shipyard',
[BuildingType.ResearchLab]: 'researchLab',
[BuildingType.MetalStorage]: 'metalStorage',
[BuildingType.CrystalStorage]: 'crystalStorage',
[BuildingType.DeuteriumTank]: 'deuteriumTank',
[BuildingType.DarkMatterCollector]: 'darkMatterCollector',
[BuildingType.LunarBase]: 'lunarBase',
[BuildingType.SensorPhalanx]: 'sensorPhalanx',
[BuildingType.JumpGate]: 'jumpGate'
}
// 舰船类型枚举值到翻译键的映射
const shipKeyMap: Record<ShipType, string> = {
[ShipType.LightFighter]: 'lightFighter',
[ShipType.HeavyFighter]: 'heavyFighter',
[ShipType.Cruiser]: 'cruiser',
[ShipType.Battleship]: 'battleship',
[ShipType.SmallCargo]: 'smallCargo',
[ShipType.LargeCargo]: 'largeCargo',
[ShipType.ColonyShip]: 'colonyShip',
[ShipType.Recycler]: 'recycler',
[ShipType.EspionageProbe]: 'espionageProbe',
[ShipType.DarkMatterHarvester]: 'darkMatterHarvester'
}
// 防御设施类型枚举值到翻译键的映射
const defenseKeyMap: Record<DefenseType, string> = {
[DefenseType.RocketLauncher]: 'rocketLauncher',
[DefenseType.LightLaser]: 'lightLaser',
[DefenseType.HeavyLaser]: 'heavyLaser',
[DefenseType.GaussCannon]: 'gaussCannon',
[DefenseType.IonCannon]: 'ionCannon',
[DefenseType.PlasmaTurret]: 'plasmaTurret',
[DefenseType.SmallShieldDome]: 'smallShieldDome',
[DefenseType.LargeShieldDome]: 'largeShieldDome'
}
// 科技类型枚举值到翻译键的映射
const technologyKeyMap: Record<TechnologyType, string> = {
[TechnologyType.EnergyTechnology]: 'energyTechnology',
[TechnologyType.LaserTechnology]: 'laserTechnology',
[TechnologyType.IonTechnology]: 'ionTechnology',
[TechnologyType.HyperspaceTechnology]: 'hyperspaceTechnology',
[TechnologyType.PlasmaTechnology]: 'plasmaTechnology',
[TechnologyType.ComputerTechnology]: 'computerTechnology',
[TechnologyType.CombustionDrive]: 'combustionDrive',
[TechnologyType.ImpulseDrive]: 'impulseDrive',
[TechnologyType.HyperspaceDrive]: 'hyperspaceDrive',
[TechnologyType.DarkMatterTechnology]: 'darkMatterTechnology'
}
// 军官类型枚举值到翻译键的映射
const officerKeyMap: Record<OfficerType, string> = {
[OfficerType.Commander]: 'commander',
[OfficerType.Admiral]: 'admiral',
[OfficerType.Engineer]: 'engineer',
[OfficerType.Geologist]: 'geologist',
[OfficerType.Technocrat]: 'technocrat',
[OfficerType.DarkMatterSpecialist]: 'darkMatterSpecialist'
}
// 翻译后的建筑配置
const BUILDINGS = computed(() => {
const translated: Record<BuildingType, BuildingConfig> = {} as Record<BuildingType, BuildingConfig>
for (const [key, config] of Object.entries(ORIGINAL_BUILDINGS)) {
const buildingType = key as BuildingType
const translationKey = buildingKeyMap[buildingType]
translated[buildingType] = {
...config,
name: t(`buildings.${translationKey}`),
description: t(`buildingDescriptions.${translationKey}`)
}
}
return translated
})
// 翻译后的舰船配置
const SHIPS = computed(() => {
const translated: Record<ShipType, ShipConfig> = {} as Record<ShipType, ShipConfig>
for (const [key, config] of Object.entries(ORIGINAL_SHIPS)) {
const shipType = key as ShipType
const translationKey = shipKeyMap[shipType]
translated[shipType] = {
...config,
name: t(`ships.${translationKey}`),
description: t(`shipDescriptions.${translationKey}`)
}
}
return translated
})
// 翻译后的防御设施配置
const DEFENSES = computed(() => {
const translated: Record<DefenseType, DefenseConfig> = {} as Record<DefenseType, DefenseConfig>
for (const [key, config] of Object.entries(ORIGINAL_DEFENSES)) {
const defenseType = key as DefenseType
const translationKey = defenseKeyMap[defenseType]
translated[defenseType] = {
...config,
name: t(`defenses.${translationKey}`),
description: t(`defenseDescriptions.${translationKey}`)
}
}
return translated
})
// 翻译后的科技配置
const TECHNOLOGIES = computed(() => {
const translated: Record<TechnologyType, TechnologyConfig> = {} as Record<TechnologyType, TechnologyConfig>
for (const [key, config] of Object.entries(ORIGINAL_TECHNOLOGIES)) {
const technologyType = key as TechnologyType
const translationKey = technologyKeyMap[technologyType]
translated[technologyType] = {
...config,
name: t(`technologies.${translationKey}`),
description: t(`technologyDescriptions.${translationKey}`)
}
}
return translated
})
// 翻译后的军官配置
const OFFICERS = computed(() => {
const translated: Record<OfficerType, OfficerConfig> = {} as Record<OfficerType, OfficerConfig>
for (const [key, config] of Object.entries(ORIGINAL_OFFICERS)) {
const officerType = key as OfficerType
const translationKey = officerKeyMap[officerType]
translated[officerType] = {
...config,
name: t(`officers.${translationKey}`),
description: t(`officerDescriptions.${translationKey}`)
}
}
return translated
})
return {
BUILDINGS,
SHIPS,
DEFENSES,
TECHNOLOGIES,
OFFICERS
}
}

View File

@@ -0,0 +1,38 @@
import { computed } from 'vue'
import { useGameStore } from '@/stores/gameStore'
import { locales, type Locale } from '@/locales'
export const useI18n = () => {
const gameStore = useGameStore()
const currentLocale = computed(() => gameStore.locale)
const messages = computed(() => locales[currentLocale.value])
// 获取翻译文本的辅助函数
const t = (key: string): string => {
const keys = key.split('.')
let value: any = messages.value
for (const k of keys) {
if (value && typeof value === 'object' && k in value) {
value = value[k]
} else {
return key // 如果找不到翻译,返回原始 key
}
}
return typeof value === 'string' ? value : key
}
const setLocale = (locale: Locale) => {
gameStore.locale = locale
}
return {
t,
locale: currentLocale,
setLocale,
messages
}
}

View File

@@ -0,0 +1,49 @@
import { ref, onMounted, watch } from 'vue'
import { useGameStore } from '@/stores/gameStore'
type Theme = 'light' | 'dark'
const isDark = ref<boolean>(false)
export const useTheme = () => {
const gameStore = useGameStore()
// 初始化主题
onMounted(() => {
if (!gameStore.isDark) {
// 首次访问,使用系统主题偏好
isDark.value = window.matchMedia('(prefers-color-scheme: dark)').matches
gameStore.isDark = isDark.value ? 'dark' : 'light'
} else {
// 从 gameStore 读取保存的主题Pinia会自动从localStorage恢复
const savedTheme = gameStore.isDark as Theme
isDark.value = savedTheme === 'dark'
}
applyTheme()
})
// 监听主题变化
watch(isDark, () => {
applyTheme()
gameStore.isDark = isDark.value ? 'dark' : 'light'
})
// 应用主题
const applyTheme = () => {
if (isDark.value) {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
}
// 切换主题
const toggleTheme = () => {
isDark.value = !isDark.value
}
return {
isDark,
toggleTheme
}
}

570
src/config/gameConfig.ts Normal file
View File

@@ -0,0 +1,570 @@
import { BuildingType, TechnologyType, ShipType, DefenseType, OfficerType } from '@/types/game'
import type { BuildingConfig, TechnologyConfig, ShipConfig, DefenseConfig, OfficerConfig } from '@/types/game'
// 建筑配置数据
export const BUILDINGS: Record<BuildingType, BuildingConfig> = {
[BuildingType.MetalMine]: {
id: BuildingType.MetalMine,
name: '金属矿',
description: '开采金属资源',
baseCost: { metal: 60, crystal: 15, deuterium: 0, darkMatter: 0, energy: 0 },
baseTime: 15, // 减少建造时间30→15秒
costMultiplier: 1.5,
spaceUsage: 3,
planetOnly: true
},
[BuildingType.CrystalMine]: {
id: BuildingType.CrystalMine,
name: '晶体矿',
description: '开采晶体资源',
baseCost: { metal: 48, crystal: 24, deuterium: 0, darkMatter: 0, energy: 0 },
baseTime: 15, // 减少建造时间30→15秒
costMultiplier: 1.6,
spaceUsage: 3,
planetOnly: true
},
[BuildingType.DeuteriumSynthesizer]: {
id: BuildingType.DeuteriumSynthesizer,
name: '重氢合成器',
description: '合成重氢资源',
baseCost: { metal: 225, crystal: 75, deuterium: 0, darkMatter: 0, energy: 0 },
baseTime: 20, // 减少建造时间30→20秒
costMultiplier: 1.5,
spaceUsage: 4,
planetOnly: true
},
[BuildingType.SolarPlant]: {
id: BuildingType.SolarPlant,
name: '太阳能电站',
description: '提供能源',
baseCost: { metal: 75, crystal: 30, deuterium: 0, darkMatter: 0, energy: 0 },
baseTime: 15, // 减少建造时间30→15秒
costMultiplier: 1.5,
spaceUsage: 5
},
[BuildingType.RoboticsFactory]: {
id: BuildingType.RoboticsFactory,
name: '机器人工厂',
description: '加快建造速度',
baseCost: { metal: 400, crystal: 120, deuterium: 200, darkMatter: 0, energy: 0 },
baseTime: 40, // 减少建造时间60→40秒
costMultiplier: 2,
spaceUsage: 6
},
[BuildingType.NaniteFactory]: {
id: BuildingType.NaniteFactory,
name: '纳米工厂',
description: '增加建造队列数量,每级+1队列',
baseCost: { metal: 1000000, crystal: 500000, deuterium: 100000, darkMatter: 0, energy: 0 },
baseTime: 240, // 减少建造时间300→240秒
costMultiplier: 2,
spaceUsage: 15,
requirements: { [BuildingType.RoboticsFactory]: 10 }
},
[BuildingType.Shipyard]: {
id: BuildingType.Shipyard,
name: '船坞',
description: '建造舰船',
baseCost: { metal: 400, crystal: 200, deuterium: 100, darkMatter: 0, energy: 0 },
baseTime: 30, // 减少建造时间60→30秒
costMultiplier: 2,
spaceUsage: 8
},
[BuildingType.ResearchLab]: {
id: BuildingType.ResearchLab,
name: '研究实验室',
description: '研究科技',
baseCost: { metal: 200, crystal: 400, deuterium: 200, darkMatter: 0, energy: 0 },
baseTime: 30, // 减少建造时间60→30秒
costMultiplier: 2,
spaceUsage: 5
},
[BuildingType.MetalStorage]: {
id: BuildingType.MetalStorage,
name: '金属仓库',
description: '增加金属存储上限',
baseCost: { metal: 1000, crystal: 0, deuterium: 0, darkMatter: 0, energy: 0 },
baseTime: 15, // 减少建造时间30→15秒
costMultiplier: 2,
spaceUsage: 2
},
[BuildingType.CrystalStorage]: {
id: BuildingType.CrystalStorage,
name: '晶体仓库',
description: '增加晶体存储上限',
baseCost: { metal: 1000, crystal: 500, deuterium: 0, darkMatter: 0, energy: 0 },
baseTime: 15, // 减少建造时间30→15秒
costMultiplier: 2,
spaceUsage: 2
},
[BuildingType.DeuteriumTank]: {
id: BuildingType.DeuteriumTank,
name: '重氢罐',
description: '增加重氢存储上限',
baseCost: { metal: 1000, crystal: 1000, deuterium: 0, darkMatter: 0, energy: 0 },
baseTime: 15, // 减少建造时间30→15秒
costMultiplier: 2,
spaceUsage: 2
},
[BuildingType.DarkMatterCollector]: {
id: BuildingType.DarkMatterCollector,
name: '暗物质收集器',
description: '收集稀有的暗物质资源',
baseCost: { metal: 50000, crystal: 100000, deuterium: 50000, darkMatter: 0, energy: 0 },
baseTime: 90, // 减少建造时间120→90秒
costMultiplier: 2,
spaceUsage: 10,
planetOnly: true
},
// 月球专属建筑
[BuildingType.LunarBase]: {
id: BuildingType.LunarBase,
name: '月球基地',
description: '增加月球可用空间',
baseCost: { metal: 20000, crystal: 40000, deuterium: 20000, darkMatter: 0, energy: 0 },
baseTime: 45, // 减少建造时间60→45秒
costMultiplier: 2,
spaceUsage: 0, // 月球基地本身不占用空间,反而增加空间
moonOnly: true
},
[BuildingType.SensorPhalanx]: {
id: BuildingType.SensorPhalanx,
name: '传感器阵列',
description: '侦测周围星系的舰队活动',
baseCost: { metal: 20000, crystal: 40000, deuterium: 20000, darkMatter: 0, energy: 0 },
baseTime: 60, // 减少建造时间90→60秒
costMultiplier: 2,
spaceUsage: 10,
moonOnly: true
},
[BuildingType.JumpGate]: {
id: BuildingType.JumpGate,
name: '跳跃门',
description: '瞬间传送舰队到其他月球',
baseCost: { metal: 2000000, crystal: 4000000, deuterium: 2000000, darkMatter: 0, energy: 0 },
baseTime: 240, // 减少建造时间300→240秒
costMultiplier: 2,
spaceUsage: 20,
moonOnly: true
}
}
// 科技配置数据
export const TECHNOLOGIES: Record<TechnologyType, TechnologyConfig> = {
[TechnologyType.EnergyTechnology]: {
id: TechnologyType.EnergyTechnology,
name: '能源技术',
description: '提高能源利用效率',
baseCost: { metal: 0, crystal: 800, deuterium: 400, darkMatter: 0, energy: 0 },
baseTime: 30, // 减少研究时间60→30秒
costMultiplier: 2,
requirements: { [BuildingType.ResearchLab]: 1 }
},
[TechnologyType.LaserTechnology]: {
id: TechnologyType.LaserTechnology,
name: '激光技术',
description: '开发激光武器',
baseCost: { metal: 200, crystal: 100, deuterium: 0, darkMatter: 0, energy: 0 },
baseTime: 60,
costMultiplier: 2,
requirements: { [BuildingType.ResearchLab]: 1, [TechnologyType.EnergyTechnology]: 2 }
},
[TechnologyType.IonTechnology]: {
id: TechnologyType.IonTechnology,
name: '离子技术',
description: '开发离子武器',
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 }
},
[TechnologyType.HyperspaceTechnology]: {
id: TechnologyType.HyperspaceTechnology,
name: '超空间技术',
description: '研究超空间跳跃',
baseCost: { metal: 0, crystal: 4000, deuterium: 2000, darkMatter: 0, energy: 0 },
baseTime: 60,
costMultiplier: 2,
requirements: { [BuildingType.ResearchLab]: 7, [TechnologyType.EnergyTechnology]: 5 }
},
[TechnologyType.PlasmaTechnology]: {
id: TechnologyType.PlasmaTechnology,
name: '等离子技术',
description: '开发等离子武器',
baseCost: { metal: 2000, crystal: 4000, deuterium: 1000, darkMatter: 0, energy: 0 },
baseTime: 60,
costMultiplier: 2,
requirements: {
[BuildingType.ResearchLab]: 4,
[TechnologyType.EnergyTechnology]: 8,
[TechnologyType.LaserTechnology]: 10,
[TechnologyType.IonTechnology]: 5
}
},
[TechnologyType.ComputerTechnology]: {
id: TechnologyType.ComputerTechnology,
name: '计算机技术',
description: '增加研究队列数量,每级+1队列',
baseCost: { metal: 0, crystal: 400, deuterium: 600, darkMatter: 0, energy: 0 },
baseTime: 60,
costMultiplier: 2,
requirements: { [BuildingType.ResearchLab]: 1 }
},
[TechnologyType.CombustionDrive]: {
id: TechnologyType.CombustionDrive,
name: '燃烧引擎',
description: '基础推进系统',
baseCost: { metal: 400, crystal: 0, deuterium: 600, darkMatter: 0, energy: 0 },
baseTime: 60,
costMultiplier: 2,
requirements: { [BuildingType.ResearchLab]: 1, [TechnologyType.EnergyTechnology]: 1 }
},
[TechnologyType.ImpulseDrive]: {
id: TechnologyType.ImpulseDrive,
name: '脉冲引擎',
description: '高级推进系统',
baseCost: { metal: 2000, crystal: 4000, deuterium: 600, darkMatter: 0, energy: 0 },
baseTime: 60,
costMultiplier: 2,
requirements: { [BuildingType.ResearchLab]: 2, [TechnologyType.EnergyTechnology]: 1 }
},
[TechnologyType.HyperspaceDrive]: {
id: TechnologyType.HyperspaceDrive,
name: '超空间引擎',
description: '超空间推进系统',
baseCost: { metal: 10000, crystal: 20000, deuterium: 6000, darkMatter: 0, energy: 0 },
baseTime: 60,
costMultiplier: 2,
requirements: { [BuildingType.ResearchLab]: 7, [TechnologyType.HyperspaceTechnology]: 3 }
},
[TechnologyType.DarkMatterTechnology]: {
id: TechnologyType.DarkMatterTechnology,
name: '暗物质技术',
description: '研究暗物质的性质和应用',
baseCost: { metal: 100000, crystal: 200000, deuterium: 100000, darkMatter: 0, energy: 0 },
baseTime: 180,
costMultiplier: 2,
requirements: { [BuildingType.ResearchLab]: 8, [TechnologyType.HyperspaceTechnology]: 5 }
}
}
// 舰船配置数据
export const SHIPS: Record<ShipType, ShipConfig> = {
[ShipType.LightFighter]: {
id: ShipType.LightFighter,
name: '轻型战斗机',
description: '基础战斗单位',
cost: { metal: 3000, crystal: 1000, deuterium: 0, darkMatter: 0, energy: 0 },
buildTime: 20,
cargoCapacity: 50,
attack: 50,
shield: 10,
armor: 400,
speed: 12500,
fuelConsumption: 20,
requirements: { [BuildingType.Shipyard]: 1, [TechnologyType.CombustionDrive]: 1 }
},
[ShipType.HeavyFighter]: {
id: ShipType.HeavyFighter,
name: '重型战斗机',
description: '强力战斗单位',
cost: { metal: 6000, crystal: 4000, deuterium: 0, darkMatter: 0, energy: 0 },
buildTime: 30,
cargoCapacity: 100,
attack: 150,
shield: 25,
armor: 1000,
speed: 10000,
fuelConsumption: 75,
requirements: { [BuildingType.Shipyard]: 3, [TechnologyType.ImpulseDrive]: 2 }
},
[ShipType.Cruiser]: {
id: ShipType.Cruiser,
name: '巡洋舰',
description: '中型战舰',
cost: { metal: 20000, crystal: 7000, deuterium: 2000, darkMatter: 0, energy: 0 },
buildTime: 60,
cargoCapacity: 800,
attack: 400,
shield: 50,
armor: 2700,
speed: 15000,
fuelConsumption: 300,
requirements: { [BuildingType.Shipyard]: 5, [TechnologyType.ImpulseDrive]: 4, [TechnologyType.IonTechnology]: 2 }
},
[ShipType.Battleship]: {
id: ShipType.Battleship,
name: '战列舰',
description: '重型战舰',
cost: { metal: 45000, crystal: 15000, deuterium: 0, darkMatter: 0, energy: 0 },
buildTime: 90,
cargoCapacity: 1500,
attack: 1000,
shield: 200,
armor: 6000,
speed: 10000,
fuelConsumption: 500,
requirements: { [BuildingType.Shipyard]: 7, [TechnologyType.HyperspaceDrive]: 4 }
},
[ShipType.SmallCargo]: {
id: ShipType.SmallCargo,
name: '小型运输船',
description: '运输资源',
cost: { metal: 2000, crystal: 2000, deuterium: 0, darkMatter: 0, energy: 0 },
buildTime: 15,
cargoCapacity: 5000,
attack: 5,
shield: 10,
armor: 400,
speed: 5000,
fuelConsumption: 10,
requirements: { [BuildingType.Shipyard]: 2, [TechnologyType.CombustionDrive]: 2 }
},
[ShipType.LargeCargo]: {
id: ShipType.LargeCargo,
name: '大型运输船',
description: '大量运输资源',
cost: { metal: 6000, crystal: 6000, deuterium: 0, darkMatter: 0, energy: 0 },
buildTime: 30,
cargoCapacity: 25000,
attack: 5,
shield: 25,
armor: 1200,
speed: 7500,
fuelConsumption: 50,
requirements: { [BuildingType.Shipyard]: 4, [TechnologyType.CombustionDrive]: 6 }
},
[ShipType.ColonyShip]: {
id: ShipType.ColonyShip,
name: '殖民船',
description: '建立新殖民地',
cost: { metal: 10000, crystal: 20000, deuterium: 10000, darkMatter: 0, energy: 0 },
buildTime: 120,
cargoCapacity: 7500,
attack: 50,
shield: 100,
armor: 3000,
speed: 2500,
fuelConsumption: 1000,
requirements: { [BuildingType.Shipyard]: 4, [TechnologyType.ImpulseDrive]: 3 }
},
[ShipType.Recycler]: {
id: ShipType.Recycler,
name: '回收船',
description: '回收废墟资源',
cost: { metal: 10000, crystal: 6000, deuterium: 2000, darkMatter: 0, energy: 0 },
buildTime: 60,
cargoCapacity: 20000,
attack: 1,
shield: 10,
armor: 1600,
speed: 2000,
fuelConsumption: 300,
requirements: { [BuildingType.Shipyard]: 4, [TechnologyType.CombustionDrive]: 6 }
},
[ShipType.EspionageProbe]: {
id: ShipType.EspionageProbe,
name: '间谍探测器',
description: '侦察敌方星球',
cost: { metal: 0, crystal: 1000, deuterium: 0, darkMatter: 0, energy: 0 },
buildTime: 5,
cargoCapacity: 5,
attack: 0,
shield: 0,
armor: 100,
speed: 100000000,
fuelConsumption: 1,
requirements: { [BuildingType.Shipyard]: 3, [TechnologyType.CombustionDrive]: 3 }
},
[ShipType.DarkMatterHarvester]: {
id: ShipType.DarkMatterHarvester,
name: '暗物质采集船',
description: '专门用于采集暗物质的特殊飞船',
cost: { metal: 100000, crystal: 150000, deuterium: 50000, darkMatter: 0, energy: 0 },
buildTime: 120,
cargoCapacity: 1000, // 暗物质专用储存
attack: 10,
shield: 50,
armor: 2000,
speed: 5000,
fuelConsumption: 500,
requirements: {
[BuildingType.Shipyard]: 8,
[TechnologyType.HyperspaceDrive]: 5,
[TechnologyType.DarkMatterTechnology]: 1
}
}
}
// 防御设施配置数据
export const DEFENSES: Record<DefenseType, DefenseConfig> = {
[DefenseType.RocketLauncher]: {
id: DefenseType.RocketLauncher,
name: '火箭发射器',
description: '基础防御设施',
cost: { metal: 2000, crystal: 0, deuterium: 0, darkMatter: 0, energy: 0 },
buildTime: 10,
attack: 80,
shield: 20,
armor: 200,
requirements: { [BuildingType.Shipyard]: 1 }
},
[DefenseType.LightLaser]: {
id: DefenseType.LightLaser,
name: '轻型激光炮',
description: '激光防御武器',
cost: { metal: 1500, crystal: 500, deuterium: 0, darkMatter: 0, energy: 0 },
buildTime: 12,
attack: 100,
shield: 25,
armor: 200,
requirements: { [BuildingType.Shipyard]: 2, [TechnologyType.LaserTechnology]: 3 }
},
[DefenseType.HeavyLaser]: {
id: DefenseType.HeavyLaser,
name: '重型激光炮',
description: '强力激光武器',
cost: { metal: 6000, crystal: 2000, deuterium: 0, darkMatter: 0, energy: 0 },
buildTime: 20,
attack: 250,
shield: 100,
armor: 800,
requirements: { [BuildingType.Shipyard]: 4, [TechnologyType.LaserTechnology]: 6 }
},
[DefenseType.GaussCannon]: {
id: DefenseType.GaussCannon,
name: '高斯炮',
description: '电磁加速武器',
cost: { metal: 20000, crystal: 15000, deuterium: 2000, darkMatter: 0, energy: 0 },
buildTime: 35,
attack: 1100,
shield: 200,
armor: 3500,
requirements: { [BuildingType.Shipyard]: 6, [TechnologyType.EnergyTechnology]: 6 }
},
[DefenseType.IonCannon]: {
id: DefenseType.IonCannon,
name: '离子炮',
description: '离子武器系统',
cost: { metal: 2000, crystal: 6000, deuterium: 0, darkMatter: 0, energy: 0 },
buildTime: 30,
attack: 150,
shield: 500,
armor: 800,
requirements: { [BuildingType.Shipyard]: 4, [TechnologyType.IonTechnology]: 4 }
},
[DefenseType.PlasmaTurret]: {
id: DefenseType.PlasmaTurret,
name: '等离子炮台',
description: '最强防御武器',
cost: { metal: 50000, crystal: 50000, deuterium: 30000, darkMatter: 0, energy: 0 },
buildTime: 60,
attack: 3000,
shield: 300,
armor: 10000,
requirements: { [BuildingType.Shipyard]: 8, [TechnologyType.PlasmaTechnology]: 7 }
},
[DefenseType.SmallShieldDome]: {
id: DefenseType.SmallShieldDome,
name: '小型护盾罩',
description: '保护星球的能量护盾',
cost: { metal: 10000, crystal: 10000, deuterium: 0, darkMatter: 0, energy: 0 },
buildTime: 30,
attack: 1,
shield: 2000,
armor: 2000,
requirements: { [BuildingType.Shipyard]: 6, [TechnologyType.EnergyTechnology]: 3 }
},
[DefenseType.LargeShieldDome]: {
id: DefenseType.LargeShieldDome,
name: '大型护盾罩',
description: '强大的星球护盾',
cost: { metal: 50000, crystal: 50000, deuterium: 0, darkMatter: 0, energy: 0 },
buildTime: 60,
attack: 1,
shield: 10000,
armor: 10000,
requirements: { [BuildingType.Shipyard]: 6, [TechnologyType.EnergyTechnology]: 6 }
}
}
// 军官配置数据
export const OFFICERS: Record<OfficerType, OfficerConfig> = {
[OfficerType.Commander]: {
id: OfficerType.Commander,
name: '指挥官',
description: '提升建筑速度和管理能力',
cost: { metal: 0, crystal: 50000, deuterium: 25000, darkMatter: 0, energy: 0 },
weeklyMaintenance: { metal: 0, crystal: 5000, deuterium: 2500, darkMatter: 0, energy: 0 },
benefits: {
buildingSpeedBonus: 10, // 建筑速度 +10%
additionalBuildQueue: 1, // 额外1个建筑队列
storageCapacityBonus: 10 // 仓储容量 +10%
}
},
[OfficerType.Admiral]: {
id: OfficerType.Admiral,
name: '上将',
description: '提升舰队作战能力',
cost: { metal: 50000, crystal: 25000, deuterium: 0, darkMatter: 0, energy: 0 },
weeklyMaintenance: { metal: 5000, crystal: 2500, deuterium: 0, darkMatter: 0, energy: 0 },
benefits: {
additionalFleetSlots: 2, // 额外2个舰队槽位
fleetSpeedBonus: 10, // 舰队速度 +10%
fuelConsumptionReduction: 10 // 燃料消耗 -10%
}
},
[OfficerType.Engineer]: {
id: OfficerType.Engineer,
name: '工程师',
description: '增强防御和能量系统',
cost: { metal: 40000, crystal: 20000, deuterium: 10000, darkMatter: 0, energy: 0 },
weeklyMaintenance: { metal: 4000, crystal: 2000, deuterium: 1000, darkMatter: 0, energy: 0 },
benefits: {
defenseBonus: 15, // 防御力 +15%
energyProductionBonus: 10, // 电量产出 +10%
buildingSpeedBonus: 5 // 建筑速度 +5%
}
},
[OfficerType.Geologist]: {
id: OfficerType.Geologist,
name: '地质学家',
description: '提高资源开采效率',
cost: { metal: 30000, crystal: 30000, deuterium: 20000, darkMatter: 0, energy: 0 },
weeklyMaintenance: { metal: 3000, crystal: 3000, deuterium: 2000, darkMatter: 0, energy: 0 },
benefits: {
resourceProductionBonus: 15, // 资源产量 +15%
storageCapacityBonus: 10 // 仓储容量 +10%
}
},
[OfficerType.Technocrat]: {
id: OfficerType.Technocrat,
name: '技术专家',
description: '加快科技研究速度',
cost: { metal: 20000, crystal: 40000, deuterium: 20000, darkMatter: 0, energy: 0 },
weeklyMaintenance: { metal: 2000, crystal: 4000, deuterium: 2000, darkMatter: 0, energy: 0 },
benefits: {
researchSpeedBonus: 15 // 研究速度 +15%
}
},
[OfficerType.DarkMatterSpecialist]: {
id: OfficerType.DarkMatterSpecialist,
name: '暗物质专家',
description: '提升暗物质采集效率',
cost: { metal: 50000, crystal: 100000, deuterium: 50000, darkMatter: 100, energy: 0 },
weeklyMaintenance: { metal: 5000, crystal: 10000, deuterium: 5000, darkMatter: 10, energy: 0 },
benefits: {
darkMatterProductionBonus: 25 // 暗物质产量 +25%
}
}
}
// 月球配置
export const MOON_CONFIG = {
minDebrisField: 100000, // 最小残骸场 (金属+晶体)
baseChance: 1, // 基础1%概率
maxChance: 20, // 最大20%概率
chancePerDebris: 100000, // 每10万资源增加1%概率
baseSize: 100, // 月球基础空间
lunarBaseSpaceBonus: 3 // 每级月球基地增加的空间
}

7
src/env.d.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}

7
src/lib/utils.ts Normal file
View File

@@ -0,0 +1,7 @@
import type { ClassValue } from 'clsx'
import { clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
export const cn = (...inputs: ClassValue[]) => {
return twMerge(clsx(inputs))
}

536
src/locales/de.ts Normal file
View File

@@ -0,0 +1,536 @@
export default {
common: {
confirm: 'Bestätigen',
cancel: 'Abbrechen',
delete: 'Löschen',
edit: 'Bearbeiten',
save: 'Speichern',
close: 'Schließen',
back: 'Zurück',
next: 'Weiter',
previous: 'Vorherige',
submit: 'Absenden',
reset: 'Zurücksetzen',
search: 'Suchen',
filter: 'Filtern',
loading: 'Laden...',
noData: 'Keine Daten',
error: 'Fehler',
success: 'Erfolg',
warning: 'Warnung',
info: 'Info',
resourceType: 'Ressourcentyp',
playerName: 'Kommandant',
timeHour: 'Std',
timeMinute: 'Min',
timeSecond: 'Sek',
featureLocked: 'Funktion gesperrt',
unlockRequired: 'Gebäude erforderlich',
requiredBuilding: 'Erforderliches Gebäude',
currentLevel: 'Aktuelles Level',
goToBuildings: 'Zu Gebäuden',
locked: 'Gesperrt',
viewRequirements: 'Anforderungen anzeigen',
requirementsNotMet: 'Anforderungen nicht erfüllt',
current: 'Aktuell'
},
errors: {
requirementsNotMet: 'Anforderungen nicht erfüllt',
insufficientResources: 'Unzureichende Ressourcen',
shieldDomeLimit: 'Schildkuppel-Limit erreicht',
fleetMissionsFull: 'Flottenmissionsplätze voll',
insufficientFleet: 'Unzureichende Flotte',
insufficientFuel: 'Unzureichender Treibstoff',
planetOnly: 'Dieses Gebäude kann nur auf Planeten gebaut werden',
moonOnly: 'Dieses Gebäude kann nur auf Monden gebaut werden',
buildQueueFull: 'Bauauftrag voll',
insufficientSpace: 'Unzureichender Platz',
buildingLevelZero: 'Gebäudelevel ist 0, kann nicht abgerissen werden',
researchQueueFull: 'Forschungsauftrag voll',
moonExists: 'Mond existiert bereits',
insufficientDebris: 'Unzureichendes Trümmerfeld'
},
nav: {
overview: 'Übersicht',
buildings: 'Gebäude',
research: 'Forschung',
shipyard: 'Raumschiffwerft',
defense: 'Verteidigung',
fleet: 'Flotte',
officers: 'Offiziere',
simulator: 'Simulator',
galaxy: 'Galaxie',
messages: 'Nachrichten',
settings: 'Einstellungen'
},
sidebar: {
language: 'Sprache',
lightMode: 'Heller Modus',
darkMode: 'Dunkler Modus',
collapse: 'Einklappen',
expand: 'Ausklappen'
},
resources: {
metal: 'Metall',
crystal: 'Kristall',
deuterium: 'Deuterium',
darkMatter: 'Dunkle Materie',
energy: 'Energie',
production: 'Produktion',
capacity: 'Kapazität',
current: 'Aktuell',
max: 'Max. Kapazität',
perHour: 'Stunde'
},
planet: {
planet: 'Planet',
moon: 'Mond',
colony: 'Kolonie',
position: 'Position',
coordinates: 'Koordinaten',
switchToMoon: 'Zum Mond',
backToPlanet: 'Zurück zum Planeten',
fields: 'Felder',
temperature: 'Temperatur',
homePlanet: 'Heimatplanet',
planetPrefix: 'Planet',
moonSuffix: 's Mond',
colonyPrefix: 'Kolonie'
},
player: {
points: 'Gesamtpunkte'
},
buildings: {
metalMine: 'Metallmine',
crystalMine: 'Kristallmine',
deuteriumSynthesizer: 'Deuterium-Synthesizer',
solarPlant: 'Solarkraftwerk',
roboticsFactory: 'Roboterfabrik',
naniteFactory: 'Nanitenfabrik',
shipyard: 'Raumschiffwerft',
researchLab: 'Forschungslabor',
metalStorage: 'Metallspeicher',
crystalStorage: 'Kristallspeicher',
deuteriumTank: 'Deuteriumtank',
darkMatterCollector: 'Dunkle-Materie-Kollektor',
lunarBase: 'Mondbasis',
sensorPhalanx: 'Sensorphalanx',
jumpGate: 'Sprungtor',
buildTime: 'Bauzeit',
production: 'Produktion',
consumption: 'Verbrauch',
totalCost: 'Gesamtkosten',
totalPoints: 'Gesamtpunkte',
levelRange: 'Stufenbereich'
},
buildingDescriptions: {
metalMine: 'Fördert Metallressourcen',
crystalMine: 'Fördert Kristallressourcen',
deuteriumSynthesizer: 'Synthesiert Deuteriumressourcen',
solarPlant: 'Liefert Energie',
roboticsFactory: 'Beschleunigt Baugeschwindigkeit',
naniteFactory: 'Erhöht Bauauftragskapazität, +1 pro Stufe (max 10)',
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',
sensorPhalanx: 'Erkennt Flottenaktivitäten in umliegenden Systemen',
jumpGate: 'Überträgt Flotten sofort zu anderen Monden'
},
ships: {
lightFighter: 'Leichter Jäger',
heavyFighter: 'Schwerer Jäger',
cruiser: 'Kreuzer',
battleship: 'Schlachtschiff',
smallCargo: 'Kleiner Transporter',
largeCargo: 'Großer Transporter',
colonyShip: 'Kolonieschiff',
recycler: 'Recycler',
espionageProbe: 'Spionagesonde',
darkMatterHarvester: 'Dunkle-Materie-Ernter'
},
shipDescriptions: {
lightFighter: 'Grundlegende Kampfeinheit',
heavyFighter: 'Schwer gepanzerter Jäger',
cruiser: 'Mittleres Kriegsschiff, ausgewogene Offensive und Defensive',
battleship: 'Mächtiges Kriegsschiff',
smallCargo: 'Transportiert kleine Mengen Ressourcen',
largeCargo: 'Transportiert große Mengen Ressourcen',
colonyShip: 'Zur Kolonisierung neuer Planeten',
recycler: 'Sammelt Trümmerfeld-Ressourcen',
espionageProbe: 'Späht feindliche Planeten aus',
darkMatterHarvester: 'Spezielles Schiff zum Ernten von Dunkler Materie'
},
defenses: {
rocketLauncher: 'Raketenwerfer',
lightLaser: 'Leichtes Lasergeschütz',
heavyLaser: 'Schweres Lasergeschütz',
gaussCannon: 'Gaußkanone',
ionCannon: 'Ionengeschütz',
plasmaTurret: 'Plasmawerfer',
smallShieldDome: 'Kleine Schildkuppel',
largeShieldDome: 'Große Schildkuppel'
},
defenseDescriptions: {
rocketLauncher: 'Grundlegende Verteidigungsanlage',
lightLaser: 'Leichte Energiewaffe',
heavyLaser: 'Schwere Energiewaffe',
gaussCannon: 'Hochgeschwindigkeits-Kinetikwaffe',
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'
},
research: {
researchTime: 'Forschungszeit',
totalCost: 'Gesamtkosten',
totalPoints: 'Gesamtpunkte',
levelRange: 'Stufenbereich'
},
technologies: {
energyTechnology: 'Energietechnik',
laserTechnology: 'Lasertechnik',
ionTechnology: 'Ionentechnik',
hyperspaceTechnology: 'Hyperraumtechnik',
plasmaTechnology: 'Plasmatechnik',
computerTechnology: 'Computertechnologie',
combustionDrive: 'Verbrennungsantrieb',
impulseDrive: 'Impulsantrieb',
hyperspaceDrive: 'Hyperraumantrieb',
darkMatterTechnology: 'Dunkle-Materie-Technologie'
},
technologyDescriptions: {
energyTechnology: 'Verbessert Energieeffizienz',
laserTechnology: 'Grundlage für Laserwaffen und -verteidigung',
ionTechnology: 'Ionenwaffentechnologie',
hyperspaceTechnology: 'Hyperraumsprung-Technologie',
plasmaTechnology: 'Plasmawaffentechnologie',
computerTechnology: 'Erhöht Forschungsauftragskapazität, +1 pro Stufe (max 10)',
combustionDrive: 'Grundlegende Antriebstechnologie',
impulseDrive: 'Mittlere Antriebstechnologie',
hyperspaceDrive: 'Fortgeschrittene Antriebstechnologie',
darkMatterTechnology: 'Forschung zu Eigenschaften und Anwendungen von Dunkler Materie'
},
officers: {
commander: 'Kommandant',
admiral: 'Admiral',
engineer: 'Ingenieur',
geologist: 'Geologe',
technocrat: 'Technokrat',
darkMatterSpecialist: 'Dunkle-Materie-Spezialist'
},
officerDescriptions: {
commander: 'Verbessert Baugeschwindigkeit und Management',
admiral: 'Verbessert Flottenkampf und Geschwindigkeit',
engineer: 'Verbessert Energie und Verteidigung',
geologist: 'Verbessert Ressourcenproduktion',
technocrat: 'Verbessert Forschungsgeschwindigkeit und Spionage',
darkMatterSpecialist: 'Verbessert Dunkle-Materie-Sammlungseffizienz'
},
queue: {
buildQueue: 'Bauauftrag',
researchQueue: 'Forschungsauftrag',
building: 'Im Bau',
researching: 'In Forschung',
remaining: 'Verbleibend',
cancel: 'Abbrechen',
cancelBuild: 'Bau abbrechen',
cancelResearch: 'Forschung abbrechen',
confirmCancel: 'Möchten Sie wirklich abbrechen? 50% der Ressourcen werden zurückerstattet.',
level: 'Stufe',
upgradeToLevel: 'Auf Stufe aufrüsten'
},
overview: {
title: 'Planetenübersicht',
resourceOverview: 'Ressourcen',
fleetInfo: 'Flotte',
currentShips: 'Schiffe auf diesem Planeten'
},
buildingsView: {
title: 'Gebäude',
usedSpace: 'Verwendeter Platz',
spaceUsage: 'Platzbedarf',
level: 'Stufe',
upgradeCost: 'Ausbaukosten',
buildTime: 'Bauzeit',
upgrade: 'Ausbauen',
upgradeFailed: 'Ausbau fehlgeschlagen',
upgradeFailedMessage: 'Bitte überprüfen Sie, ob Sie genügend Ressourcen, Platz oder keine anderen Bauaufträge haben.',
demolish: 'Abreißen',
demolishRefund: 'Abriss-Rückerstattung',
demolishFailed: 'Abriss fehlgeschlagen',
demolishFailedMessage: 'Abriss nicht möglich. Bitte überprüfen Sie, ob die Bauqueue voll ist oder die Gebäudestufe 0 ist.'
},
researchView: {
title: 'Forschung',
researchCost: 'Forschungskosten',
research: 'Forschen',
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.'
},
shipyard: {
attack: 'Angriff',
shield: 'Schild',
armor: 'Panzerung',
speed: 'Geschwindigkeit',
cargoCapacity: 'Ladekapazität',
fuelConsumption: 'Treibstoffverbrauch',
buildCost: 'Baukosten',
buildTime: 'Bauzeit',
perUnit: 'Pro Einheit',
batchCalculator: 'Batch-Rechner',
quantity: 'Menge',
totalCost: 'Gesamtkosten',
totalTime: 'Gesamtzeit'
},
shipyardView: {
title: 'Raumschiffwerft',
attack: 'Angriff',
shield: 'Schild',
speed: 'Geschwindigkeit',
cargoCapacity: 'Ladekapazität',
unitCost: 'Stückkosten',
buildQuantity: 'Baumenge',
totalCost: 'Gesamtkosten',
build: 'Bauen',
inputError: 'Eingabefehler',
inputErrorMessage: 'Bitte Baumenge eingeben!',
buildFailed: 'Bau fehlgeschlagen',
buildFailedMessage: 'Bitte überprüfen Sie, ob Sie genügend Ressourcen haben oder die Voraussetzungen erfüllt sind.'
},
defense: {
attack: 'Angriff',
shield: 'Schild',
armor: 'Panzerung',
buildCost: 'Baukosten',
buildTime: 'Bauzeit',
perUnit: 'Pro Einheit',
batchCalculator: 'Batch-Rechner',
quantity: 'Menge',
totalCost: 'Gesamtkosten',
totalTime: 'Gesamtzeit'
},
defenseView: {
title: 'Verteidigung',
attack: 'Angriff',
shield: 'Schild',
armor: 'Panzerung',
buildTime: 'Bauzeit',
seconds: 's',
unitCost: 'Stückkosten',
buildQuantity: 'Baumenge',
totalCost: 'Gesamtkosten',
build: 'Bauen',
shieldDomeBuilt: 'Schildkuppel bereits gebaut',
inputError: 'Eingabefehler',
inputErrorMessage: 'Bitte Baumenge eingeben!',
buildFailed: 'Bau fehlgeschlagen',
buildFailedMessage:
'Bitte überprüfen Sie, ob Sie genügend Ressourcen haben oder die Voraussetzungen erfüllt sind. Schildkuppeln können nur einmal gebaut werden.'
},
fleetView: {
title: 'Flottenverwaltung',
fleetOverview: 'Flottenübersicht',
sendFleet: 'Flotte senden',
flightMissions: 'Flugmissionen',
currentPlanetFleet: 'Flotte auf diesem Planeten',
attack: 'Angriff',
shield: 'Schild',
armor: 'Panzerung',
speed: 'Geschwindigkeit',
cargo: 'Fracht',
selectFleet: 'Flotte auswählen',
selectFleetDescription: 'Wählen Sie die Anzahl der zu sendenden Schiffe',
available: 'Verfügbar',
all: 'Alle',
targetCoordinates: 'Zielkoordinaten',
galaxy: 'Galaxie',
system: 'System',
position: 'Position',
missionType: 'Missionstyp',
missionInfo: 'Missionsinfo',
fuelConsumption: 'Treibstoffverbrauch',
flightTime: 'Flugzeit',
attackMission: 'Angriff',
transport: 'Transport',
colonize: 'Kolonisieren',
spy: 'Spionage',
deploy: 'Stationieren',
transportResources: 'Ressourcen transportieren',
totalCargoCapacity: 'Gesamtladekapazität',
used: 'Verwendet',
noFlightMissions: 'Keine Flugmissionen',
outbound: 'Hinflug',
returning: 'Rückflug',
fleetComposition: 'Flottenzusammensetzung',
carryingResources: 'Transportierte Ressourcen',
arrivalTime: 'Ankunftszeit',
returnTime: 'Rückkehrzeit',
recallFleet: 'Flotte zurückrufen',
sendFailed: 'Senden fehlgeschlagen',
sendFailedMessage: 'Bitte überprüfen Sie Flottenanzahl, Treibstoffverfügbarkeit oder Ladekapazitätsgrenzen.',
recallFailed: 'Zurückrufen fehlgeschlagen',
recallFailedMessage: 'Diese Mission kann nicht zurückgerufen werden.',
unknownPlanet: 'Unbekannter Planet',
fleetMissionSlots: 'Flottenmissionsplätze'
},
officersView: {
title: 'Offiziere',
activated: 'Aktiviert',
inactive: 'Inaktiv',
activeStatus: 'Aktivierungsstatus',
expirationTime: 'Ablaufzeit',
remainingTime: 'Verbleibende Zeit',
recruitCost: 'Rekrutierungskosten',
days: 'Tage',
benefitsBonus: 'Vorteile',
resourceProduction: 'Ressourcenproduktion',
darkMatterProduction: 'Dunkle-Materie-Produktion',
energyProduction: 'Energieproduktion',
buildingSpeed: 'Baugeschwindigkeit',
researchSpeed: 'Forschungsgeschwindigkeit',
fleetSpeed: 'Flottengeschwindigkeit',
fuelConsumption: 'Treibstoffverbrauch',
defense: 'Verteidigung',
storageCapacity: 'Lagerkapazität',
buildQueue: 'Bauauftrag',
fleetSlots: 'Flottenslots',
hire: 'Rekrutieren',
renew: 'Verlängern',
dismiss: 'Entlassen',
hireTitle: 'Offizier rekrutieren',
hireMessage: 'Möchten Sie wirklich {name} rekrutieren? Gültig für 7 Tage.',
renewTitle: 'Offizier verlängern',
renewMessage: 'Möchten Sie wirklich {name} für 7 Tage verlängern?',
dismissTitle: 'Offizier entlassen',
dismissMessage: 'Möchten Sie wirklich {name} entlassen? Es werden keine Kosten zurückerstattet.',
hireFailed: 'Rekrutierung fehlgeschlagen',
renewFailed: 'Verlängerung fehlgeschlagen',
insufficientResources: 'Nicht genug Ressourcen!'
},
galaxyView: {
title: 'Galaxie',
selectCoordinates: 'Koordinaten auswählen',
galaxy: 'Galaxie',
selectGalaxy: 'Galaxie auswählen',
system: 'System',
selectSystem: 'System auswählen',
view: 'Anzeigen',
myPlanet: 'Mein Planet',
totalPositions: 'Insgesamt 10 Planetenpositionen',
mine: 'Mein',
hostile: 'Feindlich',
emptySlot: 'Leer - Kolonisierbar',
scout: 'Spähen',
attack: 'Angriff',
colonize: 'Kolonisieren',
switch: 'Wechseln',
scoutPlanetTitle: 'Planet ausspionieren',
attackPlanetTitle: 'Planet angreifen',
colonizePlanetTitle: 'Planet kolonisieren',
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.'
},
messagesView: {
title: 'Nachrichten',
battleReports: 'Kampfberichte',
spyReports: 'Spionageberichte',
noBattleReports: 'Keine Kampfberichte',
noSpyReports: 'Keine Spionageberichte',
battleReport: 'Kampfbericht',
spyReport: 'Spionagebericht',
victory: 'Sieg',
defeat: 'Niederlage',
draw: 'Unentschieden',
attackerFleet: 'Angreiferflotte',
defenderFleet: 'Verteidigerflotte',
defenderDefense: 'Verteidigerverteidigung',
attackerLosses: 'Angreiferverluste',
defenderLosses: 'Verteidigerverluste',
noLosses: 'Keine Verluste',
plunder: 'Beute',
debrisField: 'Trümmerfeld',
resources: 'Ressourcen',
fleet: 'Flotte',
defense: 'Verteidigung',
buildings: 'Gebäude'
},
simulatorView: {
title: 'Kampfsimulator',
attacker: 'Angreifer',
defender: 'Verteidiger',
attackerConfig: 'Angreiferkonfiguration',
attackerConfigDesc: 'Angreiferflotte und Technologiestufen konfigurieren',
defenderConfig: 'Verteidigerkonfiguration',
defenderConfigDesc: 'Verteidigerflotte, Verteidigung und Technologiestufen konfigurieren',
fleet: 'Flotte',
defenseStructures: 'Verteidigungsanlagen',
techLevels: 'Technologiestufen',
weapon: 'Waffe',
shield: 'Schild',
armor: 'Panzerung',
defenderResources: 'Verteidigerressourcen (für Beuteberechnung)',
startSimulation: 'Simulation starten',
reset: 'Zurücksetzen',
battleResult: 'Kampfergebnis',
attackerVictory: 'Angreifer Sieg',
defenderVictory: 'Verteidiger Sieg',
draw: 'Unentschieden',
afterRounds: 'Nach {rounds} Runden',
attackerLosses: 'Angreiferverluste',
defenderLosses: 'Verteidigerverluste',
noLosses: 'Keine Verluste',
attackerRemaining: 'Angreifer verblieben',
defenderRemaining: 'Verteidiger verblieben',
allDestroyed: 'Alle zerstört',
plunderableResources: 'Erbeutbare Ressourcen',
debrisField: 'Trümmerfeld',
moonChance: 'Mondchance',
showRoundDetails: 'Rundendetails anzeigen',
hideRoundDetails: 'Rundendetails ausblenden',
round: 'Runde {round}',
attackerRemainingPower: 'Verbleibende Angreiferkraft',
defenderRemainingPower: 'Verbleibende Verteidigerkraft'
},
settings: {
dataManagement: 'Datenverwaltung',
dataManagementDesc: 'Spieldaten exportieren, importieren oder löschen',
exportData: 'Daten exportieren',
exportDataDesc: 'Spielfortschritt als JSON-Datei exportieren',
export: 'Exportieren',
exporting: 'Exportieren...',
exportSuccess: 'Export erfolgreich',
exportFailed: 'Export fehlgeschlagen, bitte erneut versuchen',
importData: 'Daten importieren',
importDataDesc: 'Spielfortschritt aus JSON-Datei wiederherstellen',
selectFile: 'Datei auswählen',
importSuccess: 'Import erfolgreich',
importConfirmTitle: 'Import bestätigen',
importConfirmMessage: 'Beim Importieren wird der aktuelle Spielfortschritt überschrieben. Diese Aktion kann nicht rückgängig gemacht werden. Fortfahren?',
importFailed: 'Import fehlgeschlagen, bitte Dateiformat überprüfen',
clearData: 'Daten löschen',
clearDataDesc: 'Alle Spieldaten löschen und zurücksetzen',
clear: 'Löschen',
clearConfirmTitle: 'Löschen bestätigen',
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',
playerName: 'Spielername',
gameSpeed: 'Spielgeschwindigkeit',
gameSpeedDesc: 'Aktueller Spielgeschwindigkeitsmultiplikator',
about: 'Über',
version: 'Version',
buildDate: 'Build-Datum',
community: 'Community',
github: 'GitHub-Repository',
qqGroup: 'QQ-Gruppe'
}
}

533
src/locales/en.ts Normal file
View File

@@ -0,0 +1,533 @@
export default {
common: {
confirm: 'Confirm',
cancel: 'Cancel',
delete: 'Delete',
edit: 'Edit',
save: 'Save',
close: 'Close',
back: 'Back',
next: 'Next',
previous: 'Previous',
submit: 'Submit',
reset: 'Reset',
search: 'Search',
filter: 'Filter',
loading: 'Loading...',
noData: 'No Data',
error: 'Error',
success: 'Success',
warning: 'Warning',
info: 'Info',
resourceType: 'Resource Type',
playerName: 'Commander',
timeHour: 'h',
timeMinute: 'm',
timeSecond: 's',
featureLocked: 'Feature Locked',
unlockRequired: 'Building Required',
requiredBuilding: 'Required Building',
currentLevel: 'Current Level',
goToBuildings: 'Go to Buildings',
locked: 'Locked',
viewRequirements: 'View Requirements',
requirementsNotMet: 'Requirements Not Met',
current: 'Current'
},
errors: {
requirementsNotMet: 'Requirements not met',
insufficientResources: 'Insufficient resources',
shieldDomeLimit: 'Shield dome limit reached',
fleetMissionsFull: 'Fleet mission slots full',
insufficientFleet: 'Insufficient fleet',
insufficientFuel: 'Insufficient fuel',
planetOnly: 'This building can only be built on planets',
moonOnly: 'This building can only be built on moons',
buildQueueFull: 'Build queue full',
insufficientSpace: 'Insufficient space',
buildingLevelZero: 'Building level is 0, cannot demolish',
researchQueueFull: 'Research queue full',
moonExists: 'Moon already exists',
insufficientDebris: 'Insufficient debris field'
},
nav: {
overview: 'Overview',
buildings: 'Buildings',
research: 'Research',
shipyard: 'Shipyard',
defense: 'Defense',
fleet: 'Fleet',
officers: 'Officers',
simulator: 'Simulator',
galaxy: 'Galaxy',
messages: 'Messages',
settings: 'Settings'
},
sidebar: {
language: 'Language',
lightMode: 'Light Mode',
darkMode: 'Dark Mode',
collapse: 'Collapse',
expand: 'Expand'
},
resources: {
metal: 'Metal',
crystal: 'Crystal',
deuterium: 'Deuterium',
darkMatter: 'Dark Matter',
energy: 'Energy',
production: 'Production',
capacity: 'Capacity',
current: 'Current',
max: 'Max Capacity',
perHour: 'hour'
},
planet: {
planet: 'Planet',
moon: 'Moon',
colony: 'Colony',
position: 'Position',
coordinates: 'Coordinates',
switchToMoon: 'View Moon',
backToPlanet: 'Back to Planet',
fields: 'Fields',
temperature: 'Temperature',
homePlanet: 'Home Planet',
planetPrefix: 'Planet',
moonSuffix: "'s Moon",
colonyPrefix: 'Colony'
},
player: {
points: 'Total Points'
},
buildings: {
metalMine: 'Metal Mine',
crystalMine: 'Crystal Mine',
deuteriumSynthesizer: 'Deuterium Synthesizer',
solarPlant: 'Solar Plant',
roboticsFactory: 'Robotics Factory',
naniteFactory: 'Nanite Factory',
shipyard: 'Shipyard',
researchLab: 'Research Lab',
metalStorage: 'Metal Storage',
crystalStorage: 'Crystal Storage',
deuteriumTank: 'Deuterium Tank',
darkMatterCollector: 'Dark Matter Collector',
lunarBase: 'Lunar Base',
sensorPhalanx: 'Sensor Phalanx',
jumpGate: 'Jump Gate',
buildTime: 'Build Time',
production: 'Production',
consumption: 'Consumption',
totalCost: 'Total Cost',
totalPoints: 'Total Points',
levelRange: 'Level Range'
},
buildingDescriptions: {
metalMine: 'Extracts metal resources',
crystalMine: 'Extracts crystal resources',
deuteriumSynthesizer: 'Synthesizes deuterium resources',
solarPlant: 'Provides energy',
roboticsFactory: 'Accelerates construction speed',
naniteFactory: 'Increases build queue capacity, +1 per level (max 10)',
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',
sensorPhalanx: 'Detects fleet activities in surrounding systems',
jumpGate: 'Instantly transfers fleets to other moons'
},
ships: {
lightFighter: 'Light Fighter',
heavyFighter: 'Heavy Fighter',
cruiser: 'Cruiser',
battleship: 'Battleship',
smallCargo: 'Small Cargo',
largeCargo: 'Large Cargo',
colonyShip: 'Colony Ship',
recycler: 'Recycler',
espionageProbe: 'Espionage Probe',
darkMatterHarvester: 'Dark Matter Harvester'
},
shipDescriptions: {
lightFighter: 'Basic combat unit',
heavyFighter: 'Heavily armored fighter',
cruiser: 'Medium warship, balanced offense and defense',
battleship: 'Powerful warship',
smallCargo: 'Transports small amounts of resources',
largeCargo: 'Transports large amounts of resources',
colonyShip: 'Used to colonize new planets',
recycler: 'Collects debris field resources',
espionageProbe: 'Scouts enemy planets',
darkMatterHarvester: 'Special ship for harvesting dark matter'
},
defenses: {
rocketLauncher: 'Rocket Launcher',
lightLaser: 'Light Laser',
heavyLaser: 'Heavy Laser',
gaussCannon: 'Gauss Cannon',
ionCannon: 'Ion Cannon',
plasmaTurret: 'Plasma Turret',
smallShieldDome: 'Small Shield Dome',
largeShieldDome: 'Large Shield Dome'
},
defenseDescriptions: {
rocketLauncher: 'Basic defense facility',
lightLaser: 'Light energy weapon',
heavyLaser: 'Heavy energy weapon',
gaussCannon: 'High-speed kinetic weapon',
ionCannon: 'Effective against shields',
plasmaTurret: 'Powerful defense facility',
smallShieldDome: 'Small shield protecting the entire planet',
largeShieldDome: 'Large shield protecting the entire planet'
},
research: {
researchTime: 'Research Time',
totalCost: 'Total Cost',
totalPoints: 'Total Points',
levelRange: 'Level Range'
},
technologies: {
energyTechnology: 'Energy Technology',
laserTechnology: 'Laser Technology',
ionTechnology: 'Ion Technology',
hyperspaceTechnology: 'Hyperspace Technology',
plasmaTechnology: 'Plasma Technology',
computerTechnology: 'Computer Technology',
combustionDrive: 'Combustion Drive',
impulseDrive: 'Impulse Drive',
hyperspaceDrive: 'Hyperspace Drive',
darkMatterTechnology: 'Dark Matter Technology'
},
technologyDescriptions: {
energyTechnology: 'Improves energy efficiency',
laserTechnology: 'Foundation of laser weapons and defense',
ionTechnology: 'Ion weapon technology',
hyperspaceTechnology: 'Hyperspace jump technology',
plasmaTechnology: 'Plasma weapon technology',
computerTechnology: 'Increases research queue capacity, +1 per level (max 10)',
combustionDrive: 'Basic propulsion technology',
impulseDrive: 'Intermediate propulsion technology',
hyperspaceDrive: 'Advanced propulsion technology',
darkMatterTechnology: 'Research into dark matter properties and applications'
},
officers: {
commander: 'Commander',
admiral: 'Admiral',
engineer: 'Engineer',
geologist: 'Geologist',
technocrat: 'Technocrat',
darkMatterSpecialist: 'Dark Matter Specialist'
},
officerDescriptions: {
commander: 'Improves building speed and management',
admiral: 'Improves fleet combat and speed',
engineer: 'Improves energy and defense',
geologist: 'Improves resource production',
technocrat: 'Improves research speed and espionage',
darkMatterSpecialist: 'Improves dark matter collection efficiency'
},
queue: {
buildQueue: 'Build Queue',
researchQueue: 'Research Queue',
building: 'Building',
researching: 'Researching',
remaining: 'Remaining',
cancel: 'Cancel',
cancelBuild: 'Cancel Build',
cancelResearch: 'Cancel Research',
confirmCancel: 'Are you sure you want to cancel? 50% of resources will be refunded.',
level: 'Level',
upgradeToLevel: 'Upgrade to Level'
},
overview: {
title: 'Planet Overview',
resourceOverview: 'Resources',
fleetInfo: 'Fleet',
currentShips: 'Ships on this planet'
},
buildingsView: {
title: 'Buildings',
usedSpace: 'Used Space',
spaceUsage: 'Space Usage',
level: 'Level',
upgradeCost: 'Upgrade Cost',
buildTime: 'Build Time',
upgrade: 'Upgrade',
upgradeFailed: 'Upgrade Failed',
upgradeFailedMessage: 'Please check if you have enough resources, space, or if there are other build tasks.',
demolish: 'Demolish',
demolishRefund: 'Demolish Refund',
demolishFailed: 'Demolish Failed',
demolishFailedMessage: 'Unable to demolish this building. Please check if the build queue is full or the building level is 0.'
},
researchView: {
title: 'Research',
researchCost: 'Research Cost',
research: 'Research',
researchFailed: 'Research Failed',
researchFailedMessage: 'Please check if you have enough resources, prerequisites are met, or if there are other research tasks.'
},
shipyard: {
attack: 'Attack',
shield: 'Shield',
armor: 'Armor',
speed: 'Speed',
cargoCapacity: 'Cargo Capacity',
fuelConsumption: 'Fuel Consumption',
buildCost: 'Build Cost',
buildTime: 'Build Time',
perUnit: 'Per Unit',
batchCalculator: 'Batch Calculator',
quantity: 'Quantity',
totalCost: 'Total Cost',
totalTime: 'Total Time'
},
shipyardView: {
title: 'Shipyard',
attack: 'Attack',
shield: 'Shield',
speed: 'Speed',
cargoCapacity: 'Cargo Capacity',
unitCost: 'Unit Cost',
buildQuantity: 'Build Quantity',
totalCost: 'Total Cost',
build: 'Build',
inputError: 'Input Error',
inputErrorMessage: 'Please enter build quantity!',
buildFailed: 'Build Failed',
buildFailedMessage: 'Please check if you have enough resources or if prerequisites are met.'
},
defense: {
attack: 'Attack',
shield: 'Shield',
armor: 'Armor',
buildCost: 'Build Cost',
buildTime: 'Build Time',
perUnit: 'Per Unit',
batchCalculator: 'Batch Calculator',
quantity: 'Quantity',
totalCost: 'Total Cost',
totalTime: 'Total Time'
},
defenseView: {
title: 'Defense',
attack: 'Attack',
shield: 'Shield',
armor: 'Armor',
buildTime: 'Build Time',
seconds: 's',
unitCost: 'Unit Cost',
buildQuantity: 'Build Quantity',
totalCost: 'Total Cost',
build: 'Build',
shieldDomeBuilt: 'Shield dome already built',
inputError: 'Input Error',
inputErrorMessage: 'Please enter build quantity!',
buildFailed: 'Build Failed',
buildFailedMessage: 'Please check if you have enough resources or if prerequisites are met. Shield domes can only be built once.'
},
fleetView: {
title: 'Fleet Management',
fleetOverview: 'Fleet Overview',
sendFleet: 'Send Fleet',
flightMissions: 'Flight Missions',
currentPlanetFleet: 'Current Planet Fleet',
attack: 'Attack',
shield: 'Shield',
armor: 'Armor',
speed: 'Speed',
cargo: 'Cargo',
selectFleet: 'Select Fleet',
selectFleetDescription: 'Select the number of ships to send',
available: 'Available',
all: 'All',
targetCoordinates: 'Target Coordinates',
galaxy: 'Galaxy',
system: 'System',
position: 'Position',
missionType: 'Mission Type',
missionInfo: 'Mission Info',
fuelConsumption: 'Fuel Consumption',
flightTime: 'Flight Time',
attackMission: 'Attack',
transport: 'Transport',
colonize: 'Colonize',
spy: 'Spy',
deploy: 'Deploy',
transportResources: 'Transport Resources',
totalCargoCapacity: 'Total Cargo Capacity',
used: 'Used',
noFlightMissions: 'No flight missions',
outbound: 'Outbound',
returning: 'Returning',
fleetComposition: 'Fleet Composition',
carryingResources: 'Carrying Resources',
arrivalTime: 'Arrival Time',
returnTime: 'Return Time',
recallFleet: 'Recall Fleet',
sendFailed: 'Send Failed',
sendFailedMessage: 'Please check fleet count, fuel availability, or cargo capacity limits.',
recallFailed: 'Recall Failed',
recallFailedMessage: 'This mission cannot be recalled.',
unknownPlanet: 'Unknown Planet',
fleetMissionSlots: 'Fleet Mission Slots'
},
officersView: {
title: 'Officers',
activated: 'Activated',
inactive: 'Inactive',
activeStatus: 'Active Status',
expirationTime: 'Expiration Time',
remainingTime: 'Remaining Time',
recruitCost: 'Recruitment Cost',
days: 'days',
benefitsBonus: 'Benefits Bonus',
resourceProduction: 'Resource Production',
darkMatterProduction: 'Dark Matter Production',
energyProduction: 'Energy Production',
buildingSpeed: 'Building Speed',
researchSpeed: 'Research Speed',
fleetSpeed: 'Fleet Speed',
fuelConsumption: 'Fuel Consumption',
defense: 'Defense',
storageCapacity: 'Storage Capacity',
buildQueue: 'Build Queue',
fleetSlots: 'Fleet Slots',
hire: 'Hire',
renew: 'Renew',
dismiss: 'Dismiss',
hireTitle: 'Hire Officer',
hireMessage: 'Are you sure you want to hire {name}? Valid for 7 days.',
renewTitle: 'Renew Officer',
renewMessage: 'Are you sure you want to renew {name} for 7 days?',
dismissTitle: 'Dismiss Officer',
dismissMessage: 'Are you sure you want to dismiss {name}? No refunds will be given.',
hireFailed: 'Hire Failed',
renewFailed: 'Renew Failed',
insufficientResources: 'Insufficient resources!'
},
galaxyView: {
title: 'Galaxy',
selectCoordinates: 'Select Coordinates',
galaxy: 'Galaxy',
selectGalaxy: 'Select Galaxy',
system: 'System',
selectSystem: 'Select System',
view: 'View',
myPlanet: 'My Planet',
totalPositions: '10 planet positions total',
mine: 'Mine',
hostile: 'Hostile',
emptySlot: 'Empty - Colonizable',
scout: 'Scout',
attack: 'Attack',
colonize: 'Colonize',
switch: 'Switch',
scoutPlanetTitle: 'Scout Planet',
attackPlanetTitle: 'Attack Planet',
colonizePlanetTitle: 'Colonize Planet',
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.'
},
messagesView: {
title: 'Messages',
battleReports: 'Battle Reports',
spyReports: 'Spy Reports',
noBattleReports: 'No battle reports',
noSpyReports: 'No spy reports',
battleReport: 'Battle Report',
spyReport: 'Spy Report',
victory: 'Victory',
defeat: 'Defeat',
draw: 'Draw',
attackerFleet: 'Attacker Fleet',
defenderFleet: 'Defender Fleet',
defenderDefense: 'Defender Defense',
attackerLosses: 'Attacker Losses',
defenderLosses: 'Defender Losses',
noLosses: 'No losses',
plunder: 'Plunder',
debrisField: 'Debris Field',
resources: 'Resources',
fleet: 'Fleet',
defense: 'Defense',
buildings: 'Buildings'
},
simulatorView: {
title: 'Battle Simulator',
attacker: 'Attacker',
defender: 'Defender',
attackerConfig: 'Attacker Configuration',
attackerConfigDesc: 'Configure attacker fleet and technology levels',
defenderConfig: 'Defender Configuration',
defenderConfigDesc: 'Configure defender fleet, defense, and technology levels',
fleet: 'Fleet',
defenseStructures: 'Defense Structures',
techLevels: 'Technology Levels',
weapon: 'Weapon',
shield: 'Shield',
armor: 'Armor',
defenderResources: 'Defender Resources (for plunder calculation)',
startSimulation: 'Start Simulation',
reset: 'Reset',
battleResult: 'Battle Result',
attackerVictory: 'Attacker Victory',
defenderVictory: 'Defender Victory',
draw: 'Draw',
afterRounds: 'After {rounds} rounds',
attackerLosses: 'Attacker Losses',
defenderLosses: 'Defender Losses',
noLosses: 'No losses',
attackerRemaining: 'Attacker Remaining',
defenderRemaining: 'Defender Remaining',
allDestroyed: 'All destroyed',
plunderableResources: 'Plunderable Resources',
debrisField: 'Debris Field',
moonChance: 'Moon chance',
showRoundDetails: 'Show round details',
hideRoundDetails: 'Hide round details',
round: 'Round {round}',
attackerRemainingPower: 'Attacker remaining power',
defenderRemainingPower: 'Defender remaining power'
},
settings: {
dataManagement: 'Data Management',
dataManagementDesc: 'Export, import, or clear game data',
exportData: 'Export Data',
exportDataDesc: 'Export game progress as JSON file',
export: 'Export',
exporting: 'Exporting...',
exportSuccess: 'Export successful',
exportFailed: 'Export failed, please try again',
importData: 'Import Data',
importDataDesc: 'Restore game progress from JSON file',
selectFile: 'Select File',
importSuccess: 'Import successful',
importConfirmTitle: 'Confirm Import',
importConfirmMessage: 'Importing will overwrite current game progress. This action cannot be undone. Continue?',
importFailed: 'Import failed, please check file format',
clearData: 'Clear Data',
clearDataDesc: 'Delete all game data and reset',
clear: 'Clear',
clearConfirmTitle: 'Confirm Clear Data',
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',
playerName: 'Player Name',
gameSpeed: 'Game Speed',
gameSpeedDesc: 'Current game speed multiplier',
about: 'About',
version: 'Version',
buildDate: 'Build Date',
community: 'Community',
github: 'GitHub Repository',
qqGroup: 'QQ Group'
}
}

53
src/locales/index.ts Normal file
View File

@@ -0,0 +1,53 @@
import zhCN from './zh-CN'
import zhTW from './zh-TW'
import en from './en'
import de from './de'
import ru from './ru'
import ko from './ko'
import ja from './ja'
export type Locale = 'zh-CN' | 'zh-TW' | 'en' | 'de' | 'ru' | 'ko' | 'ja'
export const locales = { 'zh-CN': zhCN, 'zh-TW': zhTW, en, de, ru, ko, ja }
export const localeNames: Record<Locale, string> = {
'zh-CN': '简体中文',
'zh-TW': '繁體中文',
en: 'English',
de: 'Deutsch',
ru: 'Русский',
ko: '한국어',
ja: '日本語'
}
/**
* 根据浏览器语言检测并返回应用支持的语言
* @returns 检测到的语言代码
*/
export const detectBrowserLocale = (): Locale => {
// 获取浏览器语言
const browserLang = navigator.language || (navigator.languages && navigator.languages[0]) || 'zh-CN'
const lang = browserLang.toLowerCase()
// 映射浏览器语言到应用支持的语言
if (lang.startsWith('zh-tw') || lang.startsWith('zh-hant') || lang.startsWith('zh-hk') || lang.startsWith('zh-mo')) {
return 'zh-TW'
} else if (lang.startsWith('zh')) {
return 'zh-CN'
} else if (lang.startsWith('ja')) {
return 'ja'
} else if (lang.startsWith('ko')) {
return 'ko'
} else if (lang.startsWith('en')) {
return 'en'
} else if (lang.startsWith('de')) {
return 'de'
} else if (lang.startsWith('ru')) {
return 'ru'
}
// 默认返回简体中文
return 'zh-CN'
}
export type TranslationSchema = typeof zhCN

531
src/locales/ja.ts Normal file
View File

@@ -0,0 +1,531 @@
export default {
common: {
confirm: '確認',
cancel: 'キャンセル',
delete: '削除',
edit: '編集',
save: '保存',
close: '閉じる',
back: '戻る',
next: '次へ',
previous: '前へ',
submit: '送信',
reset: 'リセット',
search: '検索',
filter: 'フィルター',
loading: '読み込み中...',
noData: 'データなし',
error: 'エラー',
success: '成功',
warning: '警告',
info: '情報',
resourceType: '資源タイプ',
playerName: '司令官',
timeHour: '時間',
timeMinute: '分',
timeSecond: '秒',
featureLocked: '機能がロックされています',
unlockRequired: '建物が必要です',
requiredBuilding: '必要な建物',
currentLevel: '現在のレベル',
goToBuildings: '建物へ移動',
locked: 'ロック済み',
viewRequirements: '必要条件を表示',
requirementsNotMet: '必要条件が満たされていません',
current: '現在'
},
errors: {
requirementsNotMet: '前提条件を満たしていません',
insufficientResources: '資源が不足しています',
shieldDomeLimit: 'シールドドームの上限に達しました',
fleetMissionsFull: '艦隊ミッションスロットが満杯です',
insufficientFleet: '艦隊が不足しています',
insufficientFuel: '燃料が不足しています',
planetOnly: 'この建物は惑星でのみ建設できます',
moonOnly: 'この建物は月でのみ建設できます',
buildQueueFull: '建設キューが満杯です',
insufficientSpace: 'スペースが不足しています',
buildingLevelZero: '建物レベルが0のため、解体できません',
researchQueueFull: '研究キューが満杯です',
moonExists: '月は既に存在します',
insufficientDebris: '残骸フィールドが不足しています'
},
nav: {
overview: '概要',
buildings: '建物',
research: '研究',
shipyard: '造船所',
defense: '防衛',
fleet: '艦隊',
officers: '士官',
simulator: 'シミュレーター',
galaxy: '銀河',
messages: 'メッセージ',
settings: '設定'
},
sidebar: {
language: '言語',
lightMode: 'ライトモード',
darkMode: 'ダークモード',
collapse: 'メニューを閉じる',
expand: 'メニューを開く'
},
resources: {
metal: '金属',
crystal: 'クリスタル',
deuterium: '重水素',
darkMatter: 'ダークマター',
energy: 'エネルギー',
production: '生産量',
capacity: '容量',
current: '現在の貯蔵量',
max: '最大容量',
perHour: '時間'
},
planet: {
planet: '惑星',
moon: '月',
colony: 'コロニー',
position: '位置',
coordinates: '座標',
switchToMoon: '月を表示',
backToPlanet: '母星に戻る',
fields: 'フィールド',
temperature: '温度',
homePlanet: '母星',
planetPrefix: '惑星',
moonSuffix: 'の月',
colonyPrefix: 'コロニー'
},
player: {
points: '総ポイント'
},
buildings: {
metalMine: '金属鉱山',
crystalMine: 'クリスタル鉱山',
deuteriumSynthesizer: '重水素合成装置',
solarPlant: '太陽光発電所',
roboticsFactory: 'ロボット工場',
naniteFactory: 'ナノマシン工場',
shipyard: '造船所',
researchLab: '研究所',
metalStorage: '金属倉庫',
crystalStorage: 'クリスタル倉庫',
deuteriumTank: '重水素タンク',
darkMatterCollector: 'ダークマター採取装置',
lunarBase: '月面基地',
sensorPhalanx: 'センサーファランクス',
jumpGate: 'ジャンプゲート',
buildTime: '建設時間',
production: '生産量',
consumption: '消費',
totalCost: '総コスト',
totalPoints: '総ポイント',
levelRange: 'レベル範囲'
},
buildingDescriptions: {
metalMine: '金属資源を採掘',
crystalMine: 'クリスタル資源を採掘',
deuteriumSynthesizer: '重水素資源を合成',
solarPlant: 'エネルギーを供給',
roboticsFactory: '建設速度を向上',
naniteFactory: '建設キュー数を増加、レベル毎に+1最大10',
shipyard: '艦船を建造',
researchLab: '技術を研究',
metalStorage: '金属の貯蔵上限を増加',
crystalStorage: 'クリスタルの貯蔵上限を増加',
deuteriumTank: '重水素の貯蔵上限を増加',
darkMatterCollector: '希少なダークマター資源を収集',
lunarBase: '月の利用可能スペースを増加',
sensorPhalanx: '周辺星系の艦隊活動を探知',
jumpGate: '他の月へ艦隊を瞬間移動'
},
ships: {
lightFighter: '軽戦闘機',
heavyFighter: '重戦闘機',
cruiser: '巡洋艦',
battleship: '戦艦',
smallCargo: '小型輸送船',
largeCargo: '大型輸送船',
colonyShip: 'コロニーシップ',
recycler: 'リサイクラー',
espionageProbe: 'スパイプローブ',
darkMatterHarvester: 'ダークマター採取船'
},
shipDescriptions: {
lightFighter: '基本戦闘ユニット',
heavyFighter: '重装甲戦闘機',
cruiser: '中型戦艦、攻守バランス型',
battleship: '強力な戦艦',
smallCargo: '少量の資源を輸送',
largeCargo: '大量の資源を輸送',
colonyShip: '新惑星の植民に使用',
recycler: 'デブリフィールドの資源を回収',
espionageProbe: '敵惑星を偵察',
darkMatterHarvester: 'ダークマター採取専用の特殊艦'
},
defenses: {
rocketLauncher: 'ロケットランチャー',
lightLaser: 'ライトレーザー',
heavyLaser: 'ヘビーレーザー',
gaussCannon: 'ガウスキャノン',
ionCannon: 'イオンキャノン',
plasmaTurret: 'プラズマタレット',
smallShieldDome: '小型シールドドーム',
largeShieldDome: '大型シールドドーム'
},
defenseDescriptions: {
rocketLauncher: '基本防衛施設',
lightLaser: '軽量エネルギー兵器',
heavyLaser: '重型エネルギー兵器',
gaussCannon: '高速運動エネルギー兵器',
ionCannon: 'シールド破壊に効果的',
plasmaTurret: '強力な防衛施設',
smallShieldDome: '惑星全体を保護する小型シールド',
largeShieldDome: '惑星全体を保護する大型シールド'
},
research: {
researchTime: '研究時間',
totalCost: '総コスト',
totalPoints: '総ポイント',
levelRange: 'レベル範囲'
},
technologies: {
energyTechnology: 'エネルギー技術',
laserTechnology: 'レーザー技術',
ionTechnology: 'イオン技術',
hyperspaceTechnology: 'ハイパースペース技術',
plasmaTechnology: 'プラズマ技術',
computerTechnology: 'コンピューター技術',
combustionDrive: '燃焼ドライブ',
impulseDrive: 'インパルスドライブ',
hyperspaceDrive: 'ハイパースペースドライブ',
darkMatterTechnology: 'ダークマター技術'
},
technologyDescriptions: {
energyTechnology: 'エネルギー利用効率を向上',
laserTechnology: 'レーザー兵器と防衛の基礎',
ionTechnology: 'イオン兵器技術',
hyperspaceTechnology: 'ハイパースペースジャンプ技術',
plasmaTechnology: 'プラズマ兵器技術',
computerTechnology: '研究キュー数を増加、レベル毎に+1最大10',
combustionDrive: '基本推進技術',
impulseDrive: '中級推進技術',
hyperspaceDrive: '高級推進技術',
darkMatterTechnology: 'ダークマターの性質と応用を研究'
},
officers: {
commander: '司令官',
admiral: '提督',
engineer: 'エンジニア',
geologist: '地質学者',
technocrat: '技術専門家',
darkMatterSpecialist: 'ダークマター専門家'
},
officerDescriptions: {
commander: '建設速度と管理能力を向上',
admiral: '艦隊戦闘力と速度を向上',
engineer: 'エネルギーと防御力を向上',
geologist: '資源生産量を向上',
technocrat: '研究速度と偵察能力を向上',
darkMatterSpecialist: 'ダークマター採取効率を向上'
},
queue: {
buildQueue: '建設キュー',
researchQueue: '研究キュー',
building: '建設中',
researching: '研究中',
remaining: '残り時間',
cancel: 'キャンセル',
cancelBuild: '建設キャンセル',
cancelResearch: '研究キャンセル',
confirmCancel: 'キャンセルしますか資源の50%が返還されます。',
level: 'レベル',
upgradeToLevel: 'レベルにアップグレード'
},
shipyard: {
attack: '攻撃力',
shield: 'シールド',
armor: '装甲',
speed: '速度',
cargoCapacity: '貨物容量',
fuelConsumption: '燃料消費',
buildCost: '建設コスト',
buildTime: '建設時間',
perUnit: 'ユニットあたり',
batchCalculator: '一括計算機',
quantity: '数量',
totalCost: '総コスト',
totalTime: '総時間'
},
overview: {
title: '惑星概要',
resourceOverview: '資源概要',
fleetInfo: '艦隊',
currentShips: '現在の惑星の艦船数'
},
buildingsView: {
title: '建物',
usedSpace: '使用済みスペース',
spaceUsage: 'スペース使用量',
level: 'レベル',
upgradeCost: 'アップグレードコスト',
buildTime: '建設時間',
upgrade: 'アップグレード',
upgradeFailed: 'アップグレード失敗',
upgradeFailedMessage: '資源が十分か、スペースが十分か、または他の建設タスクがないか確認してください。',
demolish: '解体',
demolishRefund: '解体返還',
demolishFailed: '解体失敗',
demolishFailedMessage: 'この建物を解体できません。建設キューが満杯か、建物レベルが0でないか確認してください。'
},
researchView: {
title: '研究',
researchCost: '研究コスト',
research: '研究',
researchFailed: '研究失敗',
researchFailedMessage: '資源が十分か、前提条件が満たされているか、または他の研究タスクがないか確認してください。'
},
defense: {
attack: '攻撃力',
shield: 'シールド',
armor: '装甲',
buildCost: '建設コスト',
buildTime: '建設時間',
perUnit: 'ユニットあたり',
batchCalculator: '一括計算機',
quantity: '数量',
totalCost: '総コスト',
totalTime: '総時間'
},
shipyardView: {
title: '造船所',
attack: '攻撃力',
shield: 'シールド',
speed: '速度',
cargoCapacity: '積載量',
unitCost: 'ユニットコスト',
buildQuantity: '建造数',
totalCost: '総コスト',
build: '建造',
inputError: '入力エラー',
inputErrorMessage: '建造数を入力してください!',
buildFailed: '建造失敗',
buildFailedMessage: '資源が十分か、前提条件が満たされているか確認してください。'
},
defenseView: {
title: '防衛施設',
attack: '攻撃力',
shield: 'シールド',
armor: '装甲',
buildTime: '建設時間',
seconds: '秒',
unitCost: 'ユニットコスト',
buildQuantity: '建造数',
totalCost: '総コスト',
build: '建造',
shieldDomeBuilt: 'シールドドーム建設済み',
inputError: '入力エラー',
inputErrorMessage: '建造数を入力してください!',
buildFailed: '建造失敗',
buildFailedMessage: '資源が十分か、前提条件が満たされているか確認してください。シールドドームは1つのみ建設できます。'
},
fleetView: {
title: '艦隊管理',
fleetOverview: '艦隊概要',
sendFleet: '艦隊派遣',
flightMissions: '飛行ミッション',
currentPlanetFleet: '現在の惑星艦隊',
attack: '攻撃',
shield: 'シールド',
armor: '装甲',
speed: '速度',
cargo: '貨物',
selectFleet: '艦隊選択',
selectFleetDescription: '派遣する艦船数を選択',
available: '利用可能',
all: '全て',
targetCoordinates: '目標座標',
galaxy: '銀河',
system: '星系',
position: '位置',
missionType: 'ミッションタイプ',
missionInfo: 'ミッション情報',
fuelConsumption: '燃料消費',
flightTime: '飛行時間',
attackMission: '攻撃',
transport: '輸送',
colonize: '植民',
spy: '偵察',
deploy: '配備',
transportResources: '資源輸送',
totalCargoCapacity: '総積載量',
used: '使用済み',
noFlightMissions: '飛行ミッションなし',
outbound: '往路',
returning: '帰路',
fleetComposition: '艦隊構成',
carryingResources: '運搬資源',
arrivalTime: '到着時刻',
returnTime: '帰還時刻',
recallFleet: '艦隊召還',
sendFailed: '派遣失敗',
sendFailedMessage: '艦隊数、燃料の充足、または積載量の制限を確認してください。',
recallFailed: '召還失敗',
recallFailedMessage: 'このミッションは召還できません。',
unknownPlanet: '未知の惑星',
fleetMissionSlots: '艦隊ミッションスロット'
},
officersView: {
title: '士官',
activated: 'アクティブ',
inactive: '非アクティブ',
activeStatus: 'アクティブ状態',
expirationTime: '期限切れ時刻',
remainingTime: '残り時間',
recruitCost: '募集コスト',
days: '日',
benefitsBonus: '効果ボーナス',
resourceProduction: '資源生産量',
darkMatterProduction: 'ダークマター生産量',
energyProduction: 'エネルギー生産量',
buildingSpeed: '建設速度',
researchSpeed: '研究速度',
fleetSpeed: '艦隊速度',
fuelConsumption: '燃料消費',
defense: '防御力',
storageCapacity: '貯蔵容量',
buildQueue: '建設キュー',
fleetSlots: '艦隊スロット',
hire: '雇用',
renew: '更新',
dismiss: '解雇',
hireTitle: '士官雇用',
hireMessage: '{name}を雇用しますか有効期限は7日間です。',
renewTitle: '士官更新',
renewMessage: '{name}を7日間更新しますか',
dismissTitle: '士官解雇',
dismissMessage: '{name}を解雇しますか?費用は返金されません。',
hireFailed: '雇用失敗',
renewFailed: '更新失敗',
insufficientResources: '資源不足!'
},
galaxyView: {
title: '銀河',
selectCoordinates: '座標選択',
galaxy: '銀河',
selectGalaxy: '銀河を選択',
system: '星系',
selectSystem: '星系を選択',
view: '表示',
myPlanet: '自分の惑星',
totalPositions: '全10惑星位置',
mine: '自分',
hostile: '敵対',
emptySlot: '空き - 植民可能',
scout: '偵察',
attack: '攻撃',
colonize: '植民',
switch: '切り替え',
scoutPlanetTitle: '惑星偵察',
attackPlanetTitle: '惑星攻撃',
colonizePlanetTitle: '惑星植民',
scoutPlanetMessage: '惑星[{coordinates}]にスパイプローブを送りますか?\n\n艦隊ページに移動して艦船を選択して派遣してください。',
attackPlanetMessage: '惑星[{coordinates}]を攻撃しますか?\n\n艦隊ページに移動して艦船を選択して派遣してください。',
colonizePlanetMessage: '位置[{coordinates}]を植民しますか?\n\n艦隊ページに移動してコロニーシップを派遣してください。'
},
messagesView: {
title: 'メッセージセンター',
battleReports: '戦闘レポート',
spyReports: 'スパイレポート',
noBattleReports: '戦闘レポートなし',
noSpyReports: 'スパイレポートなし',
battleReport: '戦闘レポート',
spyReport: 'スパイレポート',
victory: '勝利',
defeat: '敗北',
draw: '引き分け',
attackerFleet: '攻撃側艦隊',
defenderFleet: '防御側艦隊',
defenderDefense: '防御側防衛',
attackerLosses: '攻撃側損失',
defenderLosses: '防御側損失',
noLosses: '損失なし',
plunder: '略奪資源',
debrisField: 'デブリフィールド',
resources: '資源',
fleet: '艦隊',
defense: '防衛',
buildings: '建物'
},
simulatorView: {
title: '戦闘シミュレーター',
attacker: '攻撃側',
defender: '防御側',
attackerConfig: '攻撃側設定',
attackerConfigDesc: '攻撃側の艦隊と技術レベルを設定',
defenderConfig: '防御側設定',
defenderConfigDesc: '防御側の艦隊、防衛、技術レベルを設定',
fleet: '艦隊',
defenseStructures: '防衛施設',
techLevels: '技術レベル',
weapon: '武器',
shield: 'シールド',
armor: '装甲',
defenderResources: '防御側資源(略奪計算用)',
startSimulation: 'シミュレーション開始',
reset: 'リセット',
battleResult: '戦闘結果',
attackerVictory: '攻撃側勝利',
defenderVictory: '防御側勝利',
draw: '引き分け',
afterRounds: '{rounds}ラウンド後',
attackerLosses: '攻撃側損失',
defenderLosses: '防御側損失',
noLosses: '損失なし',
attackerRemaining: '攻撃側残存',
defenderRemaining: '防御側残存',
allDestroyed: '全て破壊',
plunderableResources: '略奪可能資源',
debrisField: 'デブリフィールド',
moonChance: '月生成確率',
showRoundDetails: 'ラウンド詳細表示',
hideRoundDetails: 'ラウンド詳細非表示',
round: '第{round}ラウンド',
attackerRemainingPower: '攻撃側残存火力',
defenderRemainingPower: '防御側残存火力'
},
settings: {
dataManagement: 'データ管理',
dataManagementDesc: 'ゲームデータのエクスポート、インポート、またはクリア',
exportData: 'データエクスポート',
exportDataDesc: 'ゲームの進行状況をJSONファイルとしてエクスポート',
export: 'エクスポート',
exporting: 'エクスポート中...',
exportSuccess: 'エクスポート成功',
exportFailed: 'エクスポートに失敗しました。もう一度お試しください',
importData: 'データインポート',
importDataDesc: 'JSONファイルからゲームの進行状況を復元',
selectFile: 'ファイルを選択',
importSuccess: 'インポート成功',
importConfirmTitle: 'インポート確認',
importConfirmMessage: 'インポートすると現在のゲームの進行状況が上書きされます。この操作は元に戻せません。続行しますか?',
importFailed: 'インポートに失敗しました。ファイル形式を確認してください',
clearData: 'データクリア',
clearDataDesc: 'すべてのゲームデータを削除してリセット',
clear: 'クリア',
clearConfirmTitle: 'データクリア確認',
clearConfirmMessage: 'すべてのゲームデータが削除され、最初からやり直します。この操作は元に戻せません。続行しますか?',
gameSettings: 'ゲーム設定',
gameSettingsDesc: 'ゲームパラメータと設定を調整',
playerName: 'プレイヤー名',
gameSpeed: 'ゲーム速度',
gameSpeedDesc: '現在のゲーム速度倍率',
about: 'について',
version: 'バージョン',
buildDate: 'ビルド日',
community: 'コミュニティ',
github: 'GitHubリポジトリ',
qqGroup: 'QQグループ'
}
}

532
src/locales/ko.ts Normal file
View File

@@ -0,0 +1,532 @@
export default {
common: {
confirm: '확인',
cancel: '취소',
delete: '삭제',
edit: '편집',
save: '저장',
close: '닫기',
back: '돌아가기',
next: '다음',
previous: '이전',
submit: '제출',
reset: '초기화',
search: '검색',
filter: '필터',
loading: '로딩 중...',
noData: '데이터 없음',
error: '오류',
success: '성공',
warning: '경고',
info: '정보',
resourceType: '자원 유형',
playerName: '사령관',
timeHour: '시간',
timeMinute: '분',
timeSecond: '초',
featureLocked: '기능 잠김',
unlockRequired: '건물 필요',
requiredBuilding: '필요한 건물',
currentLevel: '현재 레벨',
goToBuildings: '건물로 이동',
locked: '잠김',
viewRequirements: '요구사항 보기',
requirementsNotMet: '요구사항 미충족',
current: '현재'
},
errors: {
requirementsNotMet: '전제 조건 미충족',
insufficientResources: '자원 부족',
shieldDomeLimit: '실드 돔 한도 도달',
fleetMissionsFull: '함대 임무 슬롯 가득 참',
insufficientFleet: '함대 부족',
insufficientFuel: '연료 부족',
planetOnly: '이 건물은 행성에서만 지을 수 있습니다',
moonOnly: '이 건물은 위성에서만 지을 수 있습니다',
buildQueueFull: '건설 대기열 가득 참',
insufficientSpace: '공간 부족',
buildingLevelZero: '건물 레벨이 0이므로 철거할 수 없습니다',
researchQueueFull: '연구 대기열 가득 참',
moonExists: '위성이 이미 존재합니다',
insufficientDebris: '잔해장 부족'
},
nav: {
overview: '개요',
buildings: '건물',
research: '연구',
shipyard: '조선소',
defense: '방어',
fleet: '함대',
officers: '장교',
simulator: '시뮬레이터',
galaxy: '은하계',
messages: '메시지',
settings: '설정'
},
sidebar: {
language: '언어',
lightMode: '라이트 모드',
darkMode: '다크 모드',
collapse: '메뉴 접기',
expand: '메뉴 펼치기'
},
resources: {
metal: '금속',
crystal: '크리스탈',
deuterium: '중수소',
darkMatter: '암흑 물질',
energy: '에너지',
production: '생산량',
capacity: '용량',
current: '현재 저장량',
max: '최대 용량',
perHour: '시간'
},
planet: {
planet: '행성',
moon: '위성',
colony: '식민지',
position: '위치',
coordinates: '좌표',
switchToMoon: '위성 보기',
backToPlanet: '모행성으로 돌아가기',
fields: '필드',
temperature: '온도',
homePlanet: '모행성',
planetPrefix: '행성',
moonSuffix: '의 위성',
colonyPrefix: '식민지'
},
player: {
points: '총 점수'
},
buildings: {
metalMine: '금속 광산',
crystalMine: '크리스탈 광산',
deuteriumSynthesizer: '중수소 합성기',
solarPlant: '태양광 발전소',
roboticsFactory: '로봇 공장',
naniteFactory: '나노 공장',
shipyard: '조선소',
researchLab: '연구소',
metalStorage: '금속 창고',
crystalStorage: '크리스탈 창고',
deuteriumTank: '중수소 탱크',
darkMatterCollector: '암흑 물질 수집기',
lunarBase: '달 기지',
sensorPhalanx: '센서 팔랑크스',
jumpGate: '점프 게이트',
buildTime: '건설 시간',
production: '생산량',
consumption: '소비',
totalCost: '총 비용',
totalPoints: '총 점수',
levelRange: '레벨 범위'
},
buildingDescriptions: {
metalMine: '금속 자원 채굴',
crystalMine: '크리스탈 자원 채굴',
deuteriumSynthesizer: '중수소 자원 합성',
solarPlant: '에너지 제공',
roboticsFactory: '건설 속도 향상',
naniteFactory: '건설 대기열 수 증가, 레벨당 +1 (최대 10개)',
shipyard: '함선 건조',
researchLab: '기술 연구',
metalStorage: '금속 저장 용량 증가',
crystalStorage: '크리스탈 저장 용량 증가',
deuteriumTank: '중수소 저장 용량 증가',
darkMatterCollector: '희귀한 암흑 물질 자원 수집',
lunarBase: '달 가용 공간 증가',
sensorPhalanx: '주변 행성계의 함대 활동 감지',
jumpGate: '다른 위성으로 함대 순간 이동'
},
ships: {
lightFighter: '경전투기',
heavyFighter: '중전투기',
cruiser: '순양함',
battleship: '전함',
smallCargo: '소형 수송선',
largeCargo: '대형 수송선',
colonyShip: '식민선',
recycler: '재활용선',
espionageProbe: '정찰기',
darkMatterHarvester: '암흑 물질 채취선'
},
shipDescriptions: {
lightFighter: '기본 전투 유닛',
heavyFighter: '중장갑 전투기',
cruiser: '중형 전함, 공격과 방어 균형',
battleship: '강력한 전함',
smallCargo: '소량의 자원 운송',
largeCargo: '대량의 자원 운송',
colonyShip: '새로운 행성 식민에 사용',
recycler: '잔해장 자원 수집',
espionageProbe: '적 행성 정찰',
darkMatterHarvester: '암흑 물질 채취 전용 특수 함선'
},
defenses: {
rocketLauncher: '로켓 발사대',
lightLaser: '경량 레이저포',
heavyLaser: '중형 레이저포',
gaussCannon: '가우스 캐논',
ionCannon: '이온 캐논',
plasmaTurret: '플라즈마 포탑',
smallShieldDome: '소형 실드 돔',
largeShieldDome: '대형 실드 돔'
},
defenseDescriptions: {
rocketLauncher: '기본 방어 시설',
lightLaser: '경량 에너지 무기',
heavyLaser: '중형 에너지 무기',
gaussCannon: '고속 운동 에너지 무기',
ionCannon: '실드 파괴의 이기',
plasmaTurret: '강력한 방어 시설',
smallShieldDome: '행성 전체를 보호하는 소형 실드',
largeShieldDome: '행성 전체를 보호하는 대형 실드'
},
research: {
researchTime: '연구 시간',
totalCost: '총 비용',
totalPoints: '총 점수',
levelRange: '레벨 범위'
},
technologies: {
energyTechnology: '에너지 기술',
laserTechnology: '레이저 기술',
ionTechnology: '이온 기술',
hyperspaceTechnology: '초공간 기술',
plasmaTechnology: '플라즈마 기술',
computerTechnology: '컴퓨터 기술',
combustionDrive: '연소 엔진',
impulseDrive: '임펄스 엔진',
hyperspaceDrive: '초공간 엔진',
darkMatterTechnology: '암흑 물질 기술'
},
technologyDescriptions: {
energyTechnology: '에너지 이용 효율 향상',
laserTechnology: '레이저 무기와 방어의 기초',
ionTechnology: '이온 무기 기술',
hyperspaceTechnology: '초공간 점프 기술',
plasmaTechnology: '플라즈마 무기 기술',
computerTechnology: '연구 대기열 수 증가, 레벨당 +1 (최대 10개)',
combustionDrive: '기본 추진 기술',
impulseDrive: '중급 추진 기술',
hyperspaceDrive: '고급 추진 기술',
darkMatterTechnology: '암흑 물질의 성질과 응용 연구'
},
officers: {
commander: '사령관',
admiral: '제독',
engineer: '엔지니어',
geologist: '지질학자',
technocrat: '기술 전문가',
darkMatterSpecialist: '암흑 물질 전문가'
},
officerDescriptions: {
commander: '건설 속도 및 관리 능력 향상',
admiral: '함대 전투력 및 속도 향상',
engineer: '에너지 및 방어력 향상',
geologist: '자원 생산량 향상',
technocrat: '연구 속도 및 정찰 능력 향상',
darkMatterSpecialist: '암흑 물질 수집 효율 향상'
},
queue: {
buildQueue: '건설 대기열',
researchQueue: '연구 대기열',
building: '건설 중',
researching: '연구 중',
remaining: '남은 시간',
cancel: '취소',
cancelBuild: '건설 취소',
cancelResearch: '연구 취소',
confirmCancel: '취소하시겠습니까? 자원의 50%가 환불됩니다.',
level: '레벨',
upgradeToLevel: '레벨로 업그레이드'
},
overview: {
title: '행성 개요',
resourceOverview: '자원 개요',
fleetInfo: '함대',
currentShips: '현재 행성의 함선 수'
},
buildingsView: {
title: '건물',
usedSpace: '사용된 공간',
spaceUsage: '공간 사용',
level: '레벨',
upgradeCost: '업그레이드 비용',
buildTime: '건설 시간',
upgrade: '업그레이드',
upgradeFailed: '업그레이드 실패',
upgradeFailedMessage: '자원이 충분한지, 공간이 충분한지, 또는 다른 건설 작업이 있는지 확인하세요.',
demolish: '철거',
demolishRefund: '철거 환불',
demolishFailed: '철거 실패',
demolishFailedMessage: '이 건물을 철거할 수 없습니다. 건설 대기열이 가득 찼거나 건물 레벨이 0인지 확인하세요.'
},
researchView: {
title: '연구',
researchCost: '연구 비용',
research: '연구',
researchFailed: '연구 실패',
researchFailedMessage: '자원이 충분한지, 전제 조건이 충족되었는지, 또는 다른 연구 작업이 있는지 확인하세요.'
},
shipyard: {
attack: '공격력',
shield: '쉴드',
armor: '장갑',
speed: '속도',
cargoCapacity: '화물 용량',
fuelConsumption: '연료 소비',
buildCost: '건설 비용',
buildTime: '건설 시간',
perUnit: '단위당',
batchCalculator: '일괄 계산기',
quantity: '수량',
totalCost: '총 비용',
totalTime: '총 시간'
},
shipyardView: {
title: '조선소',
attack: '공격력',
shield: '실드',
speed: '속도',
cargoCapacity: '적재량',
unitCost: '단위 비용',
buildQuantity: '건조 수량',
totalCost: '총 비용',
build: '건조',
inputError: '입력 오류',
inputErrorMessage: '건조 수량을 입력하세요!',
buildFailed: '건조 실패',
buildFailedMessage: '자원이 충분한지 또는 전제 조건이 충족되었는지 확인하세요.'
},
defense: {
attack: '공격력',
shield: '쉴드',
armor: '장갑',
buildCost: '건설 비용',
buildTime: '건설 시간',
perUnit: '단위당',
batchCalculator: '일괄 계산기',
quantity: '수량',
totalCost: '총 비용',
totalTime: '총 시간'
},
defenseView: {
title: '방어 시설',
attack: '공격력',
shield: '실드',
armor: '장갑',
buildTime: '건설 시간',
seconds: '초',
unitCost: '단위 비용',
buildQuantity: '건조 수량',
totalCost: '총 비용',
build: '건조',
shieldDomeBuilt: '실드 돔이 이미 건설됨',
inputError: '입력 오류',
inputErrorMessage: '건조 수량을 입력하세요!',
buildFailed: '건조 실패',
buildFailedMessage: '자원이 충분한지 또는 전제 조건이 충족되었는지 확인하세요. 실드 돔은 하나만 건설할 수 있습니다.'
},
fleetView: {
title: '함대 관리',
fleetOverview: '함대 개요',
sendFleet: '함대 파견',
flightMissions: '비행 임무',
currentPlanetFleet: '현재 행성 함대',
attack: '공격',
shield: '실드',
armor: '장갑',
speed: '속도',
cargo: '화물',
selectFleet: '함대 선택',
selectFleetDescription: '파견할 함선 수 선택',
available: '사용 가능',
all: '전체',
targetCoordinates: '목표 좌표',
galaxy: '은하계',
system: '행성계',
position: '위치',
missionType: '임무 유형',
missionInfo: '임무 정보',
fuelConsumption: '연료 소비',
flightTime: '비행 시간',
attackMission: '공격',
transport: '수송',
colonize: '식민',
spy: '정찰',
deploy: '배치',
transportResources: '자원 수송',
totalCargoCapacity: '총 적재량',
used: '사용됨',
noFlightMissions: '비행 임무 없음',
outbound: '이동 중',
returning: '귀환 중',
fleetComposition: '함대 구성',
carryingResources: '운반 자원',
arrivalTime: '도착 시간',
returnTime: '귀환 시간',
recallFleet: '함대 소환',
sendFailed: '파견 실패',
sendFailedMessage: '함대 수, 연료 충분 여부 또는 적재량 한계를 확인하세요.',
recallFailed: '소환 실패',
recallFailedMessage: '이 임무는 소환할 수 없습니다.',
unknownPlanet: '알 수 없는 행성',
fleetMissionSlots: '함대 임무 슬롯'
},
officersView: {
title: '장교',
activated: '활성화됨',
inactive: '비활성',
activeStatus: '활성 상태',
expirationTime: '만료 시간',
remainingTime: '남은 시간',
recruitCost: '모집 비용',
days: '일',
benefitsBonus: '효과 보너스',
resourceProduction: '자원 생산량',
darkMatterProduction: '암흑 물질 생산량',
energyProduction: '에너지 생산량',
buildingSpeed: '건설 속도',
researchSpeed: '연구 속도',
fleetSpeed: '함대 속도',
fuelConsumption: '연료 소비',
defense: '방어력',
storageCapacity: '저장 용량',
buildQueue: '건설 대기열',
fleetSlots: '함대 슬롯',
hire: '고용',
renew: '갱신',
dismiss: '해고',
hireTitle: '장교 고용',
hireMessage: '{name}을(를) 고용하시겠습니까? 유효 기간은 7일입니다.',
renewTitle: '장교 갱신',
renewMessage: '{name}을(를) 7일간 갱신하시겠습니까?',
dismissTitle: '장교 해고',
dismissMessage: '{name}을(를) 해고하시겠습니까? 비용은 환불되지 않습니다.',
hireFailed: '고용 실패',
renewFailed: '갱신 실패',
insufficientResources: '자원 부족!'
},
galaxyView: {
title: '은하계',
selectCoordinates: '좌표 선택',
galaxy: '은하계',
selectGalaxy: '은하계 선택',
system: '행성계',
selectSystem: '행성계 선택',
view: '보기',
myPlanet: '내 행성',
totalPositions: '총 10개 행성 위치',
mine: '내 것',
hostile: '적대',
emptySlot: '빈 자리 - 식민 가능',
scout: '정찰',
attack: '공격',
colonize: '식민',
switch: '전환',
scoutPlanetTitle: '행성 정찰',
attackPlanetTitle: '행성 공격',
colonizePlanetTitle: '행성 식민',
scoutPlanetMessage:
'행성 [{coordinates}]을(를) 정찰하기 위해 정찰기를 보내시겠습니까?\n\n함대 페이지로 이동하여 함선을 선택하고 파견하세요.',
attackPlanetMessage: '행성 [{coordinates}]을(를) 공격하시겠습니까?\n\n함대 페이지로 이동하여 함선을 선택하고 파견하세요.',
colonizePlanetMessage: '위치 [{coordinates}]을(를) 식민하시겠습니까?\n\n함대 페이지로 이동하여 식민선을 파견하세요.'
},
messagesView: {
title: '메시지 센터',
battleReports: '전투 보고서',
spyReports: '정찰 보고서',
noBattleReports: '전투 보고서 없음',
noSpyReports: '정찰 보고서 없음',
battleReport: '전투 보고서',
spyReport: '정찰 보고서',
victory: '승리',
defeat: '패배',
draw: '무승부',
attackerFleet: '공격자 함대',
defenderFleet: '방어자 함대',
defenderDefense: '방어자 방어',
attackerLosses: '공격자 손실',
defenderLosses: '방어자 손실',
noLosses: '손실 없음',
plunder: '약탈 자원',
debrisField: '잔해장',
resources: '자원',
fleet: '함대',
defense: '방어',
buildings: '건물'
},
simulatorView: {
title: '전투 시뮬레이터',
attacker: '공격자',
defender: '방어자',
attackerConfig: '공격자 설정',
attackerConfigDesc: '공격자의 함대와 기술 레벨 설정',
defenderConfig: '방어자 설정',
defenderConfigDesc: '방어자의 함대, 방어 및 기술 레벨 설정',
fleet: '함대',
defenseStructures: '방어 시설',
techLevels: '기술 레벨',
weapon: '무기',
shield: '실드',
armor: '장갑',
defenderResources: '방어자 자원 (약탈 계산용)',
startSimulation: '시뮬레이션 시작',
reset: '초기화',
battleResult: '전투 결과',
attackerVictory: '공격자 승리',
defenderVictory: '방어자 승리',
draw: '무승부',
afterRounds: '{rounds}회 전투 후',
attackerLosses: '공격자 손실',
defenderLosses: '방어자 손실',
noLosses: '손실 없음',
attackerRemaining: '공격자 잔여',
defenderRemaining: '방어자 잔여',
allDestroyed: '모두 파괴됨',
plunderableResources: '약탈 가능 자원',
debrisField: '잔해장',
moonChance: '위성 생성 확률',
showRoundDetails: '라운드 상세 표시',
hideRoundDetails: '라운드 상세 숨기기',
round: '제 {round} 라운드',
attackerRemainingPower: '공격자 잔여 화력',
defenderRemainingPower: '방어자 잔여 화력'
},
settings: {
dataManagement: '데이터 관리',
dataManagementDesc: '게임 데이터 내보내기, 가져오기 또는 지우기',
exportData: '데이터 내보내기',
exportDataDesc: '게임 진행 상황을 JSON 파일로 내보내기',
export: '내보내기',
exporting: '내보내는 중...',
exportSuccess: '내보내기 성공',
exportFailed: '내보내기 실패, 다시 시도해주세요',
importData: '데이터 가져오기',
importDataDesc: 'JSON 파일에서 게임 진행 상황 복원',
selectFile: '파일 선택',
importSuccess: '가져오기 성공',
importConfirmTitle: '가져오기 확인',
importConfirmMessage: '가져오기를 하면 현재 게임 진행 상황이 덮어쓰기됩니다. 이 작업은 되돌릴 수 없습니다. 계속하시겠습니까?',
importFailed: '가져오기 실패, 파일 형식을 확인해주세요',
clearData: '데이터 지우기',
clearDataDesc: '모든 게임 데이터 삭제 및 초기화',
clear: '지우기',
clearConfirmTitle: '데이터 지우기 확인',
clearConfirmMessage: '모든 게임 데이터가 삭제되고 처음부터 시작됩니다. 이 작업은 되돌릴 수 없습니다. 계속하시겠습니까?',
gameSettings: '게임 설정',
gameSettingsDesc: '게임 매개변수 및 설정 조정',
playerName: '플레이어 이름',
gameSpeed: '게임 속도',
gameSpeedDesc: '현재 게임 속도 배율',
about: '정보',
version: '버전',
buildDate: '빌드 날짜',
community: '커뮤니티',
github: 'GitHub 저장소',
qqGroup: 'QQ 그룹'
}
}

536
src/locales/ru.ts Normal file
View File

@@ -0,0 +1,536 @@
export default {
common: {
confirm: 'Подтвердить',
cancel: 'Отмена',
delete: 'Удалить',
edit: 'Редактировать',
save: 'Сохранить',
close: 'Закрыть',
back: 'Назад',
next: 'Далее',
previous: 'Предыдущий',
submit: 'Отправить',
reset: 'Сбросить',
search: 'Поиск',
filter: 'Фильтр',
loading: 'Загрузка...',
noData: 'Нет данных',
error: 'Ошибка',
success: 'Успешно',
warning: 'Предупреждение',
info: 'Информация',
resourceType: 'Тип ресурса',
playerName: 'Командир',
timeHour: 'ч',
timeMinute: 'мин',
timeSecond: 'сек',
featureLocked: 'Функция заблокирована',
unlockRequired: 'Требуется здание',
requiredBuilding: 'Необходимое здание',
currentLevel: 'Текущий уровень',
goToBuildings: 'К зданиям',
locked: 'Заблокировано',
viewRequirements: 'Просмотр требований',
requirementsNotMet: 'Требования не выполнены',
current: 'Текущий'
},
errors: {
requirementsNotMet: 'Требования не выполнены',
insufficientResources: 'Недостаточно ресурсов',
shieldDomeLimit: 'Достигнут лимит щитовых куполов',
fleetMissionsFull: 'Слоты миссий флота заполнены',
insufficientFleet: 'Недостаточно флота',
insufficientFuel: 'Недостаточно топлива',
planetOnly: 'Это здание можно построить только на планетах',
moonOnly: 'Это здание можно построить только на лунах',
buildQueueFull: 'Очередь строительства заполнена',
insufficientSpace: 'Недостаточно места',
buildingLevelZero: 'Уровень здания 0, нельзя снести',
researchQueueFull: 'Очередь исследований заполнена',
moonExists: 'Луна уже существует',
insufficientDebris: 'Недостаточно обломков'
},
nav: {
overview: 'Обзор',
buildings: 'Здания',
research: 'Исследования',
shipyard: 'Верфь',
defense: 'Оборона',
fleet: 'Флот',
officers: 'Офицеры',
simulator: 'Симулятор',
galaxy: 'Галактика',
messages: 'Сообщения',
settings: 'Настройки'
},
sidebar: {
language: 'Язык',
lightMode: 'Светлая тема',
darkMode: 'Тёмная тема',
collapse: 'Свернуть',
expand: 'Развернуть'
},
resources: {
metal: 'Металл',
crystal: 'Кристалл',
deuterium: 'Дейтерий',
darkMatter: 'Тёмная материя',
energy: 'Энергия',
production: 'Производство',
capacity: 'Вместимость',
current: 'Текущий',
max: 'Макс. вместимость',
perHour: 'час'
},
planet: {
planet: 'Планета',
moon: 'Луна',
colony: 'Колония',
position: 'Позиция',
coordinates: 'Координаты',
switchToMoon: 'На луну',
backToPlanet: 'Вернуться на планету',
fields: 'Поля',
temperature: 'Температура',
homePlanet: 'Родная планета',
planetPrefix: 'Планета',
moonSuffix: 'я луна',
colonyPrefix: 'Колония'
},
player: {
points: 'Всего очков'
},
buildings: {
metalMine: 'Рудник металла',
crystalMine: 'Рудник кристалла',
deuteriumSynthesizer: 'Синтезатор дейтерия',
solarPlant: 'Солнечная электростанция',
roboticsFactory: 'Фабрика роботов',
naniteFactory: 'Нанитная фабрика',
shipyard: 'Верфь',
researchLab: 'Исследовательская лаборатория',
metalStorage: 'Хранилище металла',
crystalStorage: 'Хранилище кристалла',
deuteriumTank: 'Цистерна дейтерия',
darkMatterCollector: 'Коллектор тёмной материи',
lunarBase: 'Лунная база',
sensorPhalanx: 'Сенсорная фаланга',
jumpGate: 'Прыжковые ворота',
buildTime: 'Время строительства',
production: 'Производство',
consumption: 'Потребление',
totalCost: 'Общая стоимость',
totalPoints: 'Общие очки',
levelRange: 'Диапазон уровней'
},
buildingDescriptions: {
metalMine: 'Добывает металлические ресурсы',
crystalMine: 'Добывает кристаллические ресурсы',
deuteriumSynthesizer: 'Синтезирует дейтериевые ресурсы',
solarPlant: 'Обеспечивает энергией',
roboticsFactory: 'Ускоряет скорость строительства',
naniteFactory: 'Увеличивает вместимость очереди строительства, +1 за уровень (макс 10)',
shipyard: 'Строит корабли',
researchLab: 'Исследует технологии',
metalStorage: 'Увеличивает ёмкость хранилища металла',
crystalStorage: 'Увеличивает ёмкость хранилища кристалла',
deuteriumTank: 'Увеличивает ёмкость хранилища дейтерия',
darkMatterCollector: 'Собирает редкие ресурсы тёмной материи',
lunarBase: 'Увеличивает доступное пространство на луне',
sensorPhalanx: 'Обнаруживает активность флота в окружающих системах',
jumpGate: 'Мгновенно переносит флоты на другие луны'
},
ships: {
lightFighter: 'Лёгкий истребитель',
heavyFighter: 'Тяжёлый истребитель',
cruiser: 'Крейсер',
battleship: 'Линкор',
smallCargo: 'Малый транспорт',
largeCargo: 'Большой транспорт',
colonyShip: 'Колонизатор',
recycler: 'Переработчик',
espionageProbe: 'Шпионский зонд',
darkMatterHarvester: 'Сборщик тёмной материи'
},
shipDescriptions: {
lightFighter: 'Базовая боевая единица',
heavyFighter: 'Тяжелобронированный истребитель',
cruiser: 'Средний боевой корабль, сбалансированная атака и защита',
battleship: 'Мощный боевой корабль',
smallCargo: 'Транспортирует небольшое количество ресурсов',
largeCargo: 'Транспортирует большое количество ресурсов',
colonyShip: 'Используется для колонизации новых планет',
recycler: 'Собирает ресурсы с поля обломков',
espionageProbe: 'Разведывает вражеские планеты',
darkMatterHarvester: 'Специальный корабль для сбора тёмной материи'
},
defenses: {
rocketLauncher: 'Ракетная установка',
lightLaser: 'Лёгкий лазер',
heavyLaser: 'Тяжёлый лазер',
gaussCannon: 'Гауссова пушка',
ionCannon: 'Ионное орудие',
plasmaTurret: 'Плазменная турель',
smallShieldDome: 'Малый щитовой купол',
largeShieldDome: 'Большой щитовой купол'
},
defenseDescriptions: {
rocketLauncher: 'Базовое оборонительное сооружение',
lightLaser: 'Лёгкое энергетическое оружие',
heavyLaser: 'Тяжёлое энергетическое оружие',
gaussCannon: 'Высокоскоростное кинетическое оружие',
ionCannon: 'Эффективно против щитов',
plasmaTurret: 'Мощное оборонительное сооружение',
smallShieldDome: 'Малый щит, защищающий всю планету',
largeShieldDome: 'Большой щит, защищающий всю планету'
},
research: {
researchTime: 'Время исследования',
totalCost: 'Общая стоимость',
totalPoints: 'Общие очки',
levelRange: 'Диапазон уровней'
},
technologies: {
energyTechnology: 'Энергетическая технология',
laserTechnology: 'Лазерная технология',
ionTechnology: 'Ионная технология',
hyperspaceTechnology: 'Гиперпространственная технология',
plasmaTechnology: 'Плазменная технология',
computerTechnology: 'Компьютерная технология',
combustionDrive: 'Реактивный двигатель',
impulseDrive: 'Импульсный двигатель',
hyperspaceDrive: 'Гиперпространственный двигатель',
darkMatterTechnology: 'Технология тёмной материи'
},
technologyDescriptions: {
energyTechnology: 'Улучшает энергоэффективность',
laserTechnology: 'Основа лазерного оружия и обороны',
ionTechnology: 'Технология ионного оружия',
hyperspaceTechnology: 'Технология гиперпространственных прыжков',
plasmaTechnology: 'Технология плазменного оружия',
computerTechnology: 'Увеличивает вместимость очереди исследований, +1 за уровень (макс 10)',
combustionDrive: 'Базовая технология двигателей',
impulseDrive: 'Средняя технология двигателей',
hyperspaceDrive: 'Продвинутая технология двигателей',
darkMatterTechnology: 'Исследование свойств и применения тёмной материи'
},
officers: {
commander: 'Командир',
admiral: 'Адмирал',
engineer: 'Инженер',
geologist: 'Геолог',
technocrat: 'Технократ',
darkMatterSpecialist: 'Специалист по тёмной материи'
},
officerDescriptions: {
commander: 'Улучшает скорость строительства и управление',
admiral: 'Улучшает боевую мощь и скорость флота',
engineer: 'Улучшает энергию и оборону',
geologist: 'Улучшает производство ресурсов',
technocrat: 'Улучшает скорость исследований и шпионаж',
darkMatterSpecialist: 'Улучшает эффективность сбора тёмной материи'
},
queue: {
buildQueue: 'Очередь строительства',
researchQueue: 'Очередь исследований',
building: 'Строится',
researching: 'Исследуется',
remaining: 'Осталось',
cancel: 'Отменить',
cancelBuild: 'Отменить строительство',
cancelResearch: 'Отменить исследование',
confirmCancel: 'Вы уверены, что хотите отменить? 50% ресурсов будет возвращено.',
level: 'Уровень',
upgradeToLevel: 'Улучшить до уровня'
},
overview: {
title: 'Обзор планеты',
resourceOverview: 'Ресурсы',
fleetInfo: 'Флот',
currentShips: 'Корабли на этой планете'
},
buildingsView: {
title: 'Здания',
usedSpace: 'Использовано полей',
spaceUsage: 'Использование полей',
level: 'Уровень',
upgradeCost: 'Стоимость улучшения',
buildTime: 'Время строительства',
upgrade: 'Улучшить',
upgradeFailed: 'Улучшение не удалось',
upgradeFailedMessage: 'Пожалуйста, проверьте, достаточно ли у вас ресурсов, места или нет других задач строительства.',
demolish: 'Снести',
demolishRefund: 'Возврат от сноса',
demolishFailed: 'Снос не удался',
demolishFailedMessage: 'Невозможно снести это здание. Проверьте, не заполнена ли очередь строительства или уровень здания не равен 0.'
},
researchView: {
title: 'Исследования',
researchCost: 'Стоимость исследования',
research: 'Исследовать',
researchFailed: 'Исследование не удалось',
researchFailedMessage:
'Пожалуйста, проверьте, достаточно ли у вас ресурсов, выполнены ли предварительные условия или нет других исследовательских задач.'
},
shipyard: {
attack: 'Атака',
shield: 'Щит',
armor: 'Броня',
speed: 'Скорость',
cargoCapacity: 'Грузоподъёмность',
fuelConsumption: 'Расход топлива',
buildCost: 'Стоимость постройки',
buildTime: 'Время строительства',
perUnit: 'За единицу',
batchCalculator: 'Калькулятор партий',
quantity: 'Количество',
totalCost: 'Общая стоимость',
totalTime: 'Общее время'
},
shipyardView: {
title: 'Верфь',
attack: 'Атака',
shield: 'Щит',
speed: 'Скорость',
cargoCapacity: 'Грузоподъёмность',
unitCost: 'Стоимость единицы',
buildQuantity: 'Количество для постройки',
totalCost: 'Общая стоимость',
build: 'Построить',
inputError: 'Ошибка ввода',
inputErrorMessage: 'Пожалуйста, введите количество для постройки!',
buildFailed: 'Постройка не удалась',
buildFailedMessage: 'Пожалуйста, проверьте, достаточно ли у вас ресурсов или выполнены ли предварительные условия.'
},
defense: {
attack: 'Атака',
shield: 'Щит',
armor: 'Броня',
buildCost: 'Стоимость постройки',
buildTime: 'Время строительства',
perUnit: 'За единицу',
batchCalculator: 'Калькулятор партий',
quantity: 'Количество',
totalCost: 'Общая стоимость',
totalTime: 'Общее время'
},
defenseView: {
title: 'Оборона',
attack: 'Атака',
shield: 'Щит',
armor: 'Броня',
buildTime: 'Время постройки',
seconds: 'с',
unitCost: 'Стоимость единицы',
buildQuantity: 'Количество для постройки',
totalCost: 'Общая стоимость',
build: 'Построить',
shieldDomeBuilt: 'Щитовой купол уже построен',
inputError: 'Ошибка ввода',
inputErrorMessage: 'Пожалуйста, введите количество для постройки!',
buildFailed: 'Постройка не удалась',
buildFailedMessage:
'Пожалуйста, проверьте, достаточно ли у вас ресурсов или выполнены ли предварительные условия. Щитовые купола можно построить только один раз.'
},
fleetView: {
title: 'Управление флотом',
fleetOverview: 'Обзор флота',
sendFleet: 'Отправить флот',
flightMissions: 'Полетные миссии',
currentPlanetFleet: 'Флот на этой планете',
attack: 'Атака',
shield: 'Щит',
armor: 'Броня',
speed: 'Скорость',
cargo: 'Груз',
selectFleet: 'Выбрать флот',
selectFleetDescription: 'Выберите количество кораблей для отправки',
available: 'Доступно',
all: 'Все',
targetCoordinates: 'Целевые координаты',
galaxy: 'Галактика',
system: 'Система',
position: 'Позиция',
missionType: 'Тип миссии',
missionInfo: 'Информация о миссии',
fuelConsumption: 'Расход топлива',
flightTime: 'Время полета',
attackMission: 'Атака',
transport: 'Транспорт',
colonize: 'Колонизация',
spy: 'Разведка',
deploy: 'Размещение',
transportResources: 'Транспортировка ресурсов',
totalCargoCapacity: 'Общая грузоподъёмность',
used: 'Использовано',
noFlightMissions: 'Нет полетных миссий',
outbound: 'Туда',
returning: 'Возвращение',
fleetComposition: 'Состав флота',
carryingResources: 'Перевозимые ресурсы',
arrivalTime: 'Время прибытия',
returnTime: 'Время возврата',
recallFleet: 'Отозвать флот',
sendFailed: 'Отправка не удалась',
sendFailedMessage: 'Пожалуйста, проверьте количество флота, наличие топлива или ограничения грузоподъёмности.',
recallFailed: 'Отзыв не удался',
recallFailedMessage: 'Эта миссия не может быть отозвана.',
unknownPlanet: 'Неизвестная планета',
fleetMissionSlots: 'Слоты миссий флота'
},
officersView: {
title: 'Офицеры',
activated: 'Активирован',
inactive: 'Неактивен',
activeStatus: 'Статус активации',
expirationTime: 'Время истечения',
remainingTime: 'Оставшееся время',
recruitCost: 'Стоимость найма',
days: 'дн.',
benefitsBonus: 'Бонусы',
resourceProduction: 'Производство ресурсов',
darkMatterProduction: 'Производство тёмной материи',
energyProduction: 'Производство энергии',
buildingSpeed: 'Скорость строительства',
researchSpeed: 'Скорость исследований',
fleetSpeed: 'Скорость флота',
fuelConsumption: 'Расход топлива',
defense: 'Защита',
storageCapacity: 'Вместимость хранилища',
buildQueue: 'Очередь строительства',
fleetSlots: 'Слоты флота',
hire: 'Нанять',
renew: 'Продлить',
dismiss: 'Уволить',
hireTitle: 'Нанять офицера',
hireMessage: 'Вы уверены, что хотите нанять {name}? Действует 7 дней.',
renewTitle: 'Продлить офицера',
renewMessage: 'Вы уверены, что хотите продлить {name} на 7 дней?',
dismissTitle: 'Уволить офицера',
dismissMessage: 'Вы уверены, что хотите уволить {name}? Средства не возвращаются.',
hireFailed: 'Найм не удался',
renewFailed: 'Продление не удалось',
insufficientResources: 'Недостаточно ресурсов!'
},
galaxyView: {
title: 'Галактика',
selectCoordinates: 'Выбрать координаты',
galaxy: 'Галактика',
selectGalaxy: 'Выбрать галактику',
system: 'Система',
selectSystem: 'Выбрать систему',
view: 'Показать',
myPlanet: 'Моя планета',
totalPositions: 'Всего 10 позиций планет',
mine: 'Моя',
hostile: 'Враждебная',
emptySlot: 'Пусто - можно колонизировать',
scout: 'Разведка',
attack: 'Атака',
colonize: 'Колонизация',
switch: 'Переключить',
scoutPlanetTitle: 'Разведать планету',
attackPlanetTitle: 'Атаковать планету',
colonizePlanetTitle: 'Колонизировать планету',
scoutPlanetMessage:
'Вы уверены, что хотите отправить шпионские зонды для разведки планеты [{coordinates}]?\n\nПерейдите на страницу флота, чтобы выбрать корабли и отправить.',
attackPlanetMessage:
'Вы уверены, что хотите атаковать планету [{coordinates}]?\n\nПерейдите на страницу флота, чтобы выбрать корабли и отправить.',
colonizePlanetMessage:
'Вы уверены, что хотите колонизировать позицию [{coordinates}]?\n\nПерейдите на страницу флота, чтобы отправить колонизационный корабль.'
},
messagesView: {
title: 'Сообщения',
battleReports: 'Отчёты о боях',
spyReports: 'Отчёты разведки',
noBattleReports: 'Нет отчётов о боях',
noSpyReports: 'Нет отчётов разведки',
battleReport: 'Отчёт о бое',
spyReport: 'Отчёт разведки',
victory: 'Победа',
defeat: 'Поражение',
draw: 'Ничья',
attackerFleet: 'Флот нападающего',
defenderFleet: 'Флот защитника',
defenderDefense: 'Оборона защитника',
attackerLosses: 'Потери нападающего',
defenderLosses: 'Потери защитника',
noLosses: 'Без потерь',
plunder: 'Добыча',
debrisField: 'Поле обломков',
resources: 'Ресурсы',
fleet: 'Флот',
defense: 'Оборона',
buildings: 'Здания'
},
simulatorView: {
title: 'Симулятор боя',
attacker: 'Нападающий',
defender: 'Защитник',
attackerConfig: 'Настройки нападающего',
attackerConfigDesc: 'Настроить флот и уровни технологий нападающего',
defenderConfig: 'Настройки защитника',
defenderConfigDesc: 'Настроить флот, оборону и уровни технологий защитника',
fleet: 'Флот',
defenseStructures: 'Оборонительные сооружения',
techLevels: 'Уровни технологий',
weapon: 'Оружие',
shield: 'Щит',
armor: 'Броня',
defenderResources: 'Ресурсы защитника (для расчёта добычи)',
startSimulation: 'Начать симуляцию',
reset: 'Сбросить',
battleResult: 'Результат боя',
attackerVictory: 'Победа нападающего',
defenderVictory: 'Победа защитника',
draw: 'Ничья',
afterRounds: 'После {rounds} раундов',
attackerLosses: 'Потери нападающего',
defenderLosses: 'Потери защитника',
noLosses: 'Без потерь',
attackerRemaining: 'Осталось у нападающего',
defenderRemaining: 'Осталось у защитника',
allDestroyed: 'Всё уничтожено',
plunderableResources: 'Доступная добыча',
debrisField: 'Поле обломков',
moonChance: 'Шанс появления луны',
showRoundDetails: 'Показать детали раундов',
hideRoundDetails: 'Скрыть детали раундов',
round: 'Раунд {round}',
attackerRemainingPower: 'Оставшаяся мощь нападающего',
defenderRemainingPower: 'Оставшаяся мощь защитника'
},
settings: {
dataManagement: 'Управление данными',
dataManagementDesc: 'Экспорт, импорт или очистка игровых данных',
exportData: 'Экспорт данных',
exportDataDesc: 'Экспортировать прогресс игры в JSON файл',
export: 'Экспорт',
exporting: 'Экспорт...',
exportSuccess: 'Экспорт успешен',
exportFailed: 'Экспорт не удался, попробуйте еще раз',
importData: 'Импорт данных',
importDataDesc: 'Восстановить прогресс игры из JSON файла',
selectFile: 'Выбрать файл',
importSuccess: 'Импорт успешен',
importConfirmTitle: 'Подтвердить импорт',
importConfirmMessage: 'Импорт перезапишет текущий прогресс игры. Это действие невозможно отменить. Продолжить?',
importFailed: 'Импорт не удался, проверьте формат файла',
clearData: 'Очистить данные',
clearDataDesc: 'Удалить все игровые данные и сбросить',
clear: 'Очистить',
clearConfirmTitle: 'Подтвердить очистку данных',
clearConfirmMessage: 'Все игровые данные будут удалены и игра начнется заново. Это действие невозможно отменить. Продолжить?',
gameSettings: 'Настройки игры',
gameSettingsDesc: 'Настроить параметры и предпочтения игры',
playerName: 'Имя игрока',
gameSpeed: 'Скорость игры',
gameSpeedDesc: 'Текущий множитель скорости игры',
about: 'О программе',
version: 'Версия',
buildDate: 'Дата сборки',
community: 'Сообщество',
github: 'Репозиторий GitHub',
qqGroup: 'Группа QQ'
}
}

531
src/locales/zh-CN.ts Normal file
View File

@@ -0,0 +1,531 @@
export default {
common: {
confirm: '确认',
cancel: '取消',
delete: '删除',
edit: '编辑',
save: '保存',
close: '关闭',
back: '返回',
next: '下一步',
previous: '上一步',
submit: '提交',
reset: '重置',
search: '搜索',
filter: '筛选',
loading: '加载中...',
noData: '暂无数据',
error: '错误',
success: '成功',
warning: '警告',
info: '信息',
resourceType: '资源类型',
playerName: '指挥官',
timeHour: '时',
timeMinute: '分',
timeSecond: '秒',
featureLocked: '功能已锁定',
unlockRequired: '需要解锁前置建筑',
requiredBuilding: '所需建筑',
currentLevel: '当前等级',
goToBuildings: '前往建筑页面',
locked: '已锁定',
viewRequirements: '查看前置条件',
requirementsNotMet: '前置条件未满足',
current: '当前'
},
errors: {
requirementsNotMet: '不满足前置条件',
insufficientResources: '资源不足',
shieldDomeLimit: '护盾罩数量限制',
fleetMissionsFull: '舰队任务槽位已满',
insufficientFleet: '舰队数量不足',
insufficientFuel: '燃料不足',
planetOnly: '该建筑只能在行星上建造',
moonOnly: '该建筑只能在月球上建造',
buildQueueFull: '建造队列已满',
insufficientSpace: '空间不足',
buildingLevelZero: '建筑等级为0无法拆除',
researchQueueFull: '研究队列已满',
moonExists: '已存在月球',
insufficientDebris: '残骸场不足'
},
nav: {
overview: '总览',
buildings: '建筑',
research: '研究',
shipyard: '船坞',
defense: '防御',
fleet: '舰队',
officers: '军官',
simulator: '模拟',
galaxy: '星系',
messages: '消息',
settings: '设置'
},
sidebar: {
language: '语言',
lightMode: '日间模式',
darkMode: '夜间模式',
collapse: '收起菜单',
expand: '展开菜单'
},
resources: {
metal: '金属',
crystal: '晶体',
deuterium: '重氢',
darkMatter: '暗物质',
energy: '能量',
production: '产量',
capacity: '容量',
current: '当前储量',
max: '最大容量',
perHour: '小时'
},
planet: {
planet: '星球',
moon: '月球',
colony: '殖民地',
position: '位置',
coordinates: '坐标',
switchToMoon: '查看月球',
backToPlanet: '返回母星',
fields: '场地',
temperature: '温度',
homePlanet: '母星',
planetPrefix: '星球',
moonSuffix: '的月球',
colonyPrefix: '殖民地'
},
player: {
points: '总积分'
},
buildings: {
metalMine: '金属矿',
crystalMine: '晶体矿',
deuteriumSynthesizer: '重氢合成器',
solarPlant: '太阳能电站',
roboticsFactory: '机器人工厂',
naniteFactory: '纳米工厂',
shipyard: '船坞',
researchLab: '研究实验室',
metalStorage: '金属仓库',
crystalStorage: '晶体仓库',
deuteriumTank: '重氢罐',
darkMatterCollector: '暗物质收集器',
lunarBase: '月球基地',
sensorPhalanx: '传感器阵列',
jumpGate: '跳跃门',
buildTime: '建造时间',
production: '产量',
consumption: '消耗',
totalCost: '累积成本',
totalPoints: '累积积分',
levelRange: '等级范围'
},
buildingDescriptions: {
metalMine: '开采金属资源',
crystalMine: '开采晶体资源',
deuteriumSynthesizer: '合成重氢资源',
solarPlant: '提供能源',
roboticsFactory: '加快建造速度',
naniteFactory: '增加建造队列数量,每级+1队列最多10个',
shipyard: '建造舰船',
researchLab: '研究科技',
metalStorage: '增加金属存储上限',
crystalStorage: '增加晶体存储上限',
deuteriumTank: '增加重氢存储上限',
darkMatterCollector: '收集稀有的暗物质资源',
lunarBase: '增加月球可用空间',
sensorPhalanx: '侦测周围星系的舰队活动',
jumpGate: '瞬间传送舰队到其他月球'
},
ships: {
lightFighter: '轻型战斗机',
heavyFighter: '重型战斗机',
cruiser: '巡洋舰',
battleship: '战列舰',
smallCargo: '小型运输船',
largeCargo: '大型运输船',
colonyShip: '殖民船',
recycler: '回收船',
espionageProbe: '间谍探测器',
darkMatterHarvester: '暗物质采集船'
},
shipDescriptions: {
lightFighter: '基础战斗单位',
heavyFighter: '重装战斗机',
cruiser: '中型战舰,攻守平衡',
battleship: '强力战舰',
smallCargo: '运输少量资源',
largeCargo: '运输大量资源',
colonyShip: '用于殖民新星球',
recycler: '收集残骸场资源',
espionageProbe: '侦察敌方星球',
darkMatterHarvester: '专门用于采集暗物质的特殊飞船'
},
defenses: {
rocketLauncher: '火箭发射器',
lightLaser: '轻型激光炮',
heavyLaser: '重型激光炮',
gaussCannon: '高斯炮',
ionCannon: '离子炮',
plasmaTurret: '等离子炮塔',
smallShieldDome: '小型护盾罩',
largeShieldDome: '大型护盾罩'
},
defenseDescriptions: {
rocketLauncher: '基础防御设施',
lightLaser: '轻型能量武器',
heavyLaser: '重型能量武器',
gaussCannon: '高速动能武器',
ionCannon: '破坏护盾的利器',
plasmaTurret: '强力防御设施',
smallShieldDome: '保护整个星球的小型护盾',
largeShieldDome: '保护整个星球的大型护盾'
},
research: {
researchTime: '研究时间',
totalCost: '累积成本',
totalPoints: '累积积分',
levelRange: '等级范围'
},
technologies: {
energyTechnology: '能源技术',
laserTechnology: '激光技术',
ionTechnology: '离子技术',
hyperspaceTechnology: '超空间技术',
plasmaTechnology: '等离子技术',
computerTechnology: '计算机技术',
combustionDrive: '燃烧引擎',
impulseDrive: '脉冲引擎',
hyperspaceDrive: '超空间引擎',
darkMatterTechnology: '暗物质技术'
},
technologyDescriptions: {
energyTechnology: '提高能源利用效率',
laserTechnology: '激光武器和防御的基础',
ionTechnology: '离子武器技术',
hyperspaceTechnology: '超空间跳跃技术',
plasmaTechnology: '等离子武器技术',
computerTechnology: '增加研究队列数量,每级+1队列最多10个',
combustionDrive: '基础推进技术',
impulseDrive: '中级推进技术',
hyperspaceDrive: '高级推进技术',
darkMatterTechnology: '研究暗物质的性质和应用'
},
officers: {
commander: '指挥官',
admiral: '上将',
engineer: '工程师',
geologist: '地质学家',
technocrat: '技术专家',
darkMatterSpecialist: '暗物质专家'
},
officerDescriptions: {
commander: '提升建筑速度和管理能力',
admiral: '提升舰队战斗力和速度',
engineer: '提升能源和防御能力',
geologist: '提升资源产量',
technocrat: '提升研究速度和间谍能力',
darkMatterSpecialist: '提升暗物质采集效率'
},
queue: {
buildQueue: '建造队列',
researchQueue: '研究队列',
building: '建造中',
researching: '研究中',
remaining: '剩余时间',
cancel: '取消',
cancelBuild: '取消建造',
cancelResearch: '取消研究',
confirmCancel: '确定要取消吗将返还50%的资源。',
level: '等级',
upgradeToLevel: '升级到等级'
},
overview: {
title: '星球总览',
resourceOverview: '资源概览',
fleetInfo: '舰队',
currentShips: '当前星球的舰船数量'
},
buildingsView: {
title: '建筑',
usedSpace: '已用空间',
spaceUsage: '占用空间',
level: '等级',
upgradeCost: '升级消耗',
buildTime: '建造时间',
upgrade: '升级',
upgradeFailed: '升级失败',
upgradeFailedMessage: '请检查资源是否足够、空间是否充足或是否有其他建造任务。',
demolish: '拆除',
demolishRefund: '拆除返还',
demolishFailed: '拆除失败',
demolishFailedMessage: '无法拆除该建筑请检查建造队列是否已满或建筑等级是否为0。'
},
researchView: {
title: '研究',
researchCost: '研究消耗',
research: '研究',
researchFailed: '研究失败',
researchFailedMessage: '请检查资源是否足够、前置条件是否满足,或是否有其他研究任务。'
},
shipyard: {
attack: '攻击力',
shield: '护盾',
armor: '装甲',
speed: '速度',
cargoCapacity: '载货量',
fuelConsumption: '燃料消耗',
buildCost: '建造成本',
buildTime: '建造时间',
perUnit: '每个单位',
batchCalculator: '批量建造计算器',
quantity: '数量',
totalCost: '总成本',
totalTime: '总时间'
},
shipyardView: {
title: '船坞',
attack: '攻击力',
shield: '护盾',
speed: '速度',
cargoCapacity: '载货量',
unitCost: '单位成本',
buildQuantity: '建造数量',
totalCost: '总成本',
build: '建造',
inputError: '输入错误',
inputErrorMessage: '请输入建造数量!',
buildFailed: '建造失败',
buildFailedMessage: '请检查资源是否足够或前置条件是否满足。'
},
defense: {
attack: '攻击力',
shield: '护盾',
armor: '装甲',
buildCost: '建造成本',
buildTime: '建造时间',
perUnit: '每个单位',
batchCalculator: '批量建造计算器',
quantity: '数量',
totalCost: '总成本',
totalTime: '总时间'
},
defenseView: {
title: '防御设施',
attack: '攻击力',
shield: '护盾',
armor: '装甲',
buildTime: '建造时间',
seconds: '秒',
unitCost: '单位成本',
buildQuantity: '建造数量',
totalCost: '总成本',
build: '建造',
shieldDomeBuilt: '护盾罩已建造',
inputError: '输入错误',
inputErrorMessage: '请输入建造数量!',
buildFailed: '建造失败',
buildFailedMessage: '请检查资源是否足够或前置条件是否满足。护盾罩只能建造一个。'
},
fleetView: {
title: '舰队管理',
fleetOverview: '舰队总览',
sendFleet: '派遣舰队',
flightMissions: '飞行任务',
currentPlanetFleet: '当前星球舰队',
attack: '攻击',
shield: '护盾',
armor: '装甲',
speed: '速度',
cargo: '载货',
selectFleet: '选择舰队',
selectFleetDescription: '选择要派遣的舰船数量',
available: '可用',
all: '全部',
targetCoordinates: '目标坐标',
galaxy: '银河系',
system: '星系',
position: '位置',
missionType: '任务类型',
missionInfo: '任务信息',
fuelConsumption: '燃料消耗',
flightTime: '飞行时间',
attackMission: '攻击',
transport: '运输',
colonize: '殖民',
spy: '侦察',
deploy: '部署',
transportResources: '运输资源',
totalCargoCapacity: '总载货量',
used: '已用',
noFlightMissions: '暂无飞行任务',
outbound: '前往',
returning: '返回',
fleetComposition: '舰队组成',
carryingResources: '携带资源',
arrivalTime: '到达时间',
returnTime: '返回时间',
recallFleet: '召回舰队',
sendFailed: '派遣失败',
sendFailedMessage: '请检查舰队数量、燃料是否充足,或载货量是否超出限制。',
recallFailed: '召回失败',
recallFailedMessage: '该任务无法召回。',
unknownPlanet: '未知星球',
fleetMissionSlots: '舰队任务槽位'
},
officersView: {
title: '军官',
activated: '已激活',
inactive: '未激活',
activeStatus: '激活状态',
expirationTime: '到期时间',
remainingTime: '剩余时间',
recruitCost: '招募成本',
days: '天',
benefitsBonus: '效果加成',
resourceProduction: '资源产量',
darkMatterProduction: '暗物质产量',
energyProduction: '电量产出',
buildingSpeed: '建筑速度',
researchSpeed: '研究速度',
fleetSpeed: '舰队速度',
fuelConsumption: '燃料消耗',
defense: '防御力',
storageCapacity: '仓储容量',
buildQueue: '建筑队列',
fleetSlots: '舰队槽位',
hire: '招募',
renew: '续约',
dismiss: '解雇',
hireTitle: '招募军官',
hireMessage: '确定要招募 {name} 吗有效期为7天。',
renewTitle: '续约军官',
renewMessage: '确定要为 {name} 续约7天吗',
dismissTitle: '解雇军官',
dismissMessage: '确定要解雇 {name} 吗?不会返还任何费用。',
hireFailed: '招募失败',
renewFailed: '续约失败',
insufficientResources: '资源不足!'
},
galaxyView: {
title: '星系',
selectCoordinates: '选择坐标',
galaxy: '银河系',
selectGalaxy: '选择银河系',
system: '星系',
selectSystem: '选择星系',
view: '查看',
myPlanet: '我的星球',
totalPositions: '共10个星球位置',
mine: '我的',
hostile: '敌对',
emptySlot: '空位 - 可殖民',
scout: '侦察',
attack: '攻击',
colonize: '殖民',
switch: '切换',
scoutPlanetTitle: '侦察星球',
attackPlanetTitle: '攻击星球',
colonizePlanetTitle: '殖民星球',
scoutPlanetMessage: '确定要派遣间谍探测器侦察星球 [{coordinates}] 吗?\n\n请前往舰队页面选择舰船并派遣。',
attackPlanetMessage: '确定要攻击星球 [{coordinates}] 吗?\n\n请前往舰队页面选择舰船并派遣。',
colonizePlanetMessage: '确定要殖民位置 [{coordinates}] 吗?\n\n请前往舰队页面派遣殖民船。'
},
messagesView: {
title: '消息中心',
battleReports: '战斗报告',
spyReports: '间谍报告',
noBattleReports: '暂无战斗报告',
noSpyReports: '暂无间谍报告',
battleReport: '战斗报告',
spyReport: '间谍报告',
victory: '胜利',
defeat: '失败',
draw: '平局',
attackerFleet: '攻击方舰队',
defenderFleet: '防守方舰队',
defenderDefense: '防守方防御',
attackerLosses: '攻击方损失',
defenderLosses: '防守方损失',
noLosses: '无损失',
plunder: '掠夺资源',
debrisField: '残骸场',
resources: '资源',
fleet: '舰队',
defense: '防御',
buildings: '建筑'
},
simulatorView: {
title: '战斗模拟器',
attacker: '攻击方',
defender: '防守方',
attackerConfig: '攻击方配置',
attackerConfigDesc: '设置攻击方的舰队和科技等级',
defenderConfig: '防守方配置',
defenderConfigDesc: '设置防守方的舰队、防御和科技等级',
fleet: '舰队',
defenseStructures: '防御设施',
techLevels: '科技等级',
weapon: '武器',
shield: '护盾',
armor: '装甲',
defenderResources: '防守方资源(用于掠夺计算)',
startSimulation: '开始模拟',
reset: '重置',
battleResult: '战斗结果',
attackerVictory: '攻击方胜利',
defenderVictory: '防守方胜利',
draw: '平局',
afterRounds: '经过 {rounds} 回合战斗',
attackerLosses: '攻击方损失',
defenderLosses: '防守方损失',
noLosses: '无损失',
attackerRemaining: '攻击方剩余',
defenderRemaining: '防守方剩余',
allDestroyed: '全部摧毁',
plunderableResources: '可掠夺资源',
debrisField: '残骸场',
moonChance: '月球生成概率',
showRoundDetails: '显示回合详情',
hideRoundDetails: '隐藏回合详情',
round: '第 {round} 回合',
attackerRemainingPower: '攻击方剩余火力',
defenderRemainingPower: '防守方剩余火力'
},
settings: {
dataManagement: '数据管理',
dataManagementDesc: '导出、导入或清除游戏数据',
exportData: '导出数据',
exportDataDesc: '将游戏进度导出为JSON文件',
export: '导出',
exporting: '导出中...',
exportSuccess: '导出成功',
exportFailed: '导出失败,请重试',
importData: '导入数据',
importDataDesc: '从JSON文件恢复游戏进度',
selectFile: '导入',
importSuccess: '导入成功',
importConfirmTitle: '确认导入数据',
importConfirmMessage: '导入数据将覆盖当前游戏进度,此操作不可撤销。确定要继续吗?',
importFailed: '导入失败,请检查文件格式',
clearData: '清除数据',
clearDataDesc: '删除所有游戏数据并重置游戏',
clear: '清除',
clearConfirmTitle: '确认清除数据',
clearConfirmMessage: '这将删除所有游戏数据并重新开始,此操作不可撤销。确定要继续吗?',
gameSettings: '游戏设置',
gameSettingsDesc: '调整游戏参数和偏好设置',
playerName: '玩家名称',
gameSpeed: '游戏速度',
gameSpeedDesc: '当前游戏速度倍率',
about: '关于',
version: '版本',
buildDate: '构建日期',
community: '社区',
github: 'GitHub 仓库',
qqGroup: 'QQ 交流群'
}
}

531
src/locales/zh-TW.ts Normal file
View File

@@ -0,0 +1,531 @@
export default {
common: {
confirm: '確認',
cancel: '取消',
delete: '刪除',
edit: '編輯',
save: '儲存',
close: '關閉',
back: '返回',
next: '下一步',
previous: '上一步',
submit: '提交',
reset: '重置',
search: '搜尋',
filter: '篩選',
loading: '載入中...',
noData: '暫無資料',
error: '錯誤',
success: '成功',
warning: '警告',
info: '資訊',
resourceType: '資源類型',
playerName: '指揮官',
timeHour: '時',
timeMinute: '分',
timeSecond: '秒',
featureLocked: '功能已鎖定',
unlockRequired: '需要解鎖前置建築',
requiredBuilding: '所需建築',
currentLevel: '當前等級',
goToBuildings: '前往建築頁面',
locked: '已鎖定',
viewRequirements: '查看前置條件',
requirementsNotMet: '前置條件未滿足',
current: '當前'
},
errors: {
requirementsNotMet: '不滿足前置條件',
insufficientResources: '資源不足',
shieldDomeLimit: '護盾罩數量限制',
fleetMissionsFull: '艦隊任務槽位已滿',
insufficientFleet: '艦隊數量不足',
insufficientFuel: '燃料不足',
planetOnly: '該建築只能在行星上建造',
moonOnly: '該建築只能在月球上建造',
buildQueueFull: '建造隊列已滿',
insufficientSpace: '空間不足',
buildingLevelZero: '建築等級為0無法拆除',
researchQueueFull: '研究隊列已滿',
moonExists: '已存在月球',
insufficientDebris: '殘骸場不足'
},
nav: {
overview: '總覽',
buildings: '建築',
research: '研究',
shipyard: '船塢',
defense: '防禦',
fleet: '艦隊',
officers: '軍官',
simulator: '模擬',
galaxy: '星系',
messages: '訊息',
settings: '設定'
},
sidebar: {
language: '語言',
lightMode: '日間模式',
darkMode: '夜間模式',
collapse: '收起選單',
expand: '展開選單'
},
resources: {
metal: '金屬',
crystal: '晶體',
deuterium: '重氫',
darkMatter: '暗物質',
energy: '能量',
production: '產量',
capacity: '容量',
current: '當前儲量',
max: '最大容量',
perHour: '小時'
},
planet: {
planet: '星球',
moon: '月球',
colony: '殖民地',
position: '位置',
coordinates: '座標',
switchToMoon: '查看月球',
backToPlanet: '返回母星',
fields: '場地',
temperature: '溫度',
homePlanet: '母星',
planetPrefix: '星球',
moonSuffix: '的月球',
colonyPrefix: '殖民地'
},
player: {
points: '總積分'
},
buildings: {
metalMine: '金屬礦',
crystalMine: '晶體礦',
deuteriumSynthesizer: '重氫合成器',
solarPlant: '太陽能電站',
roboticsFactory: '機器人工廠',
naniteFactory: '納米工廠',
shipyard: '船塢',
researchLab: '研究實驗室',
metalStorage: '金屬倉庫',
crystalStorage: '晶體倉庫',
deuteriumTank: '重氫罐',
darkMatterCollector: '暗物質收集器',
lunarBase: '月球基地',
sensorPhalanx: '傳感器陣列',
jumpGate: '跳躍門',
buildTime: '建造時間',
production: '產量',
consumption: '消耗',
totalCost: '累積成本',
totalPoints: '累積積分',
levelRange: '等級範圍'
},
buildingDescriptions: {
metalMine: '開採金屬資源',
crystalMine: '開採晶體資源',
deuteriumSynthesizer: '合成重氫資源',
solarPlant: '提供能源',
roboticsFactory: '加快建造速度',
naniteFactory: '增加建造佇列數量,每級+1佇列最多10個',
shipyard: '建造艦船',
researchLab: '研究科技',
metalStorage: '增加金屬儲存上限',
crystalStorage: '增加晶體儲存上限',
deuteriumTank: '增加重氫儲存上限',
darkMatterCollector: '收集稀有的暗物質資源',
lunarBase: '增加月球可用空間',
sensorPhalanx: '偵測周圍星系的艦隊活動',
jumpGate: '瞬間傳送艦隊到其他月球'
},
ships: {
lightFighter: '輕型戰鬥機',
heavyFighter: '重型戰鬥機',
cruiser: '巡洋艦',
battleship: '戰列艦',
smallCargo: '小型運輸船',
largeCargo: '大型運輸船',
colonyShip: '殖民船',
recycler: '回收船',
espionageProbe: '間諜探測器',
darkMatterHarvester: '暗物質採集船'
},
shipDescriptions: {
lightFighter: '基礎戰鬥單位',
heavyFighter: '重裝戰鬥機',
cruiser: '中型戰艦,攻守平衡',
battleship: '強力戰艦',
smallCargo: '運輸少量資源',
largeCargo: '運輸大量資源',
colonyShip: '用於殖民新星球',
recycler: '收集殘骸場資源',
espionageProbe: '偵察敵方星球',
darkMatterHarvester: '專門用於採集暗物質的特殊飛船'
},
defenses: {
rocketLauncher: '火箭發射器',
lightLaser: '輕型激光炮',
heavyLaser: '重型激光炮',
gaussCannon: '高斯炮',
ionCannon: '離子炮',
plasmaTurret: '等離子炮塔',
smallShieldDome: '小型護盾罩',
largeShieldDome: '大型護盾罩'
},
defenseDescriptions: {
rocketLauncher: '基礎防禦設施',
lightLaser: '輕型能量武器',
heavyLaser: '重型能量武器',
gaussCannon: '高速動能武器',
ionCannon: '破壞護盾的利器',
plasmaTurret: '強力防禦設施',
smallShieldDome: '保護整個星球的小型護盾',
largeShieldDome: '保護整個星球的大型護盾'
},
research: {
researchTime: '研究時間',
totalCost: '累積成本',
totalPoints: '累積積分',
levelRange: '等級範圍'
},
technologies: {
energyTechnology: '能源技術',
laserTechnology: '激光技術',
ionTechnology: '離子技術',
hyperspaceTechnology: '超空間技術',
plasmaTechnology: '等離子技術',
computerTechnology: '計算機技術',
combustionDrive: '燃燒引擎',
impulseDrive: '脈衝引擎',
hyperspaceDrive: '超空間引擎',
darkMatterTechnology: '暗物質技術'
},
technologyDescriptions: {
energyTechnology: '提高能源利用效率',
laserTechnology: '激光武器和防禦的基礎',
ionTechnology: '離子武器技術',
hyperspaceTechnology: '超空間跳躍技術',
plasmaTechnology: '等離子武器技術',
computerTechnology: '增加研究佇列數量,每級+1佇列最多10個',
combustionDrive: '基礎推進技術',
impulseDrive: '中級推進技術',
hyperspaceDrive: '高級推進技術',
darkMatterTechnology: '研究暗物質的性質和應用'
},
officers: {
commander: '指揮官',
admiral: '上將',
engineer: '工程師',
geologist: '地質學家',
technocrat: '技術專家',
darkMatterSpecialist: '暗物質專家'
},
officerDescriptions: {
commander: '提升建築速度和管理能力',
admiral: '提升艦隊戰鬥力和速度',
engineer: '提升能源和防禦能力',
geologist: '提升資源產量',
technocrat: '提升研究速度和間諜能力',
darkMatterSpecialist: '提升暗物質採集效率'
},
queue: {
buildQueue: '建造佇列',
researchQueue: '研究佇列',
building: '建造中',
researching: '研究中',
remaining: '剩餘時間',
cancel: '取消',
cancelBuild: '取消建造',
cancelResearch: '取消研究',
confirmCancel: '確定要取消嗎將返還50%的資源。',
level: '等級',
upgradeToLevel: '升級到等級'
},
overview: {
title: '星球總覽',
resourceOverview: '資源概覽',
fleetInfo: '艦隊資訊',
currentShips: '當前星球的艦船數量'
},
buildingsView: {
title: '建築',
usedSpace: '已用空間',
spaceUsage: '佔用空間',
level: '等級',
upgradeCost: '升級消耗',
buildTime: '建造時間',
upgrade: '升級',
upgradeFailed: '升級失敗',
upgradeFailedMessage: '請檢查資源是否足夠、空間是否充足或是否有其他建造任務。',
demolish: '拆除',
demolishRefund: '拆除返還',
demolishFailed: '拆除失敗',
demolishFailedMessage: '無法拆除該建築請檢查建造隊列是否已滿或建築等級是否為0。'
},
researchView: {
title: '研究',
researchCost: '研究消耗',
research: '研究',
researchFailed: '研究失敗',
researchFailedMessage: '請檢查資源是否足夠、前置條件是否滿足,或是否有其他研究任務。'
},
shipyard: {
attack: '攻擊力',
shield: '護盾',
armor: '裝甲',
speed: '速度',
cargoCapacity: '載貨量',
fuelConsumption: '燃料消耗',
buildCost: '建造成本',
buildTime: '建造時間',
perUnit: '每個單位',
batchCalculator: '批量建造計算器',
quantity: '數量',
totalCost: '總成本',
totalTime: '總時間'
},
shipyardView: {
title: '船塢',
attack: '攻擊力',
shield: '護盾',
speed: '速度',
cargoCapacity: '載貨量',
unitCost: '單位成本',
buildQuantity: '建造數量',
totalCost: '總成本',
build: '建造',
inputError: '輸入錯誤',
inputErrorMessage: '請輸入建造數量!',
buildFailed: '建造失敗',
buildFailedMessage: '請檢查資源是否足夠或前置條件是否滿足。'
},
defense: {
attack: '攻擊力',
shield: '護盾',
armor: '裝甲',
buildCost: '建造成本',
buildTime: '建造時間',
perUnit: '每個單位',
batchCalculator: '批量建造計算器',
quantity: '數量',
totalCost: '總成本',
totalTime: '總時間'
},
defenseView: {
title: '防禦設施',
attack: '攻擊力',
shield: '護盾',
armor: '裝甲',
buildTime: '建造時間',
seconds: '秒',
unitCost: '單位成本',
buildQuantity: '建造數量',
totalCost: '總成本',
build: '建造',
shieldDomeBuilt: '護盾罩已建造',
inputError: '輸入錯誤',
inputErrorMessage: '請輸入建造數量!',
buildFailed: '建造失敗',
buildFailedMessage: '請檢查資源是否足夠或前置條件是否滿足。護盾罩只能建造一個。'
},
fleetView: {
title: '艦隊管理',
fleetOverview: '艦隊總覽',
sendFleet: '派遣艦隊',
flightMissions: '飛行任務',
currentPlanetFleet: '當前星球艦隊',
attack: '攻擊',
shield: '護盾',
armor: '裝甲',
speed: '速度',
cargo: '載貨',
selectFleet: '選擇艦隊',
selectFleetDescription: '選擇要派遣的艦船數量',
available: '可用',
all: '全部',
targetCoordinates: '目標座標',
galaxy: '銀河系',
system: '星系',
position: '位置',
missionType: '任務類型',
missionInfo: '任務資訊',
fuelConsumption: '燃料消耗',
flightTime: '飛行時間',
attackMission: '攻擊',
transport: '運輸',
colonize: '殖民',
spy: '偵察',
deploy: '部署',
transportResources: '運輸資源',
totalCargoCapacity: '總載貨量',
used: '已用',
noFlightMissions: '暫無飛行任務',
outbound: '前往',
returning: '返回',
fleetComposition: '艦隊組成',
carryingResources: '攜帶資源',
arrivalTime: '到達時間',
returnTime: '返回時間',
recallFleet: '召回艦隊',
sendFailed: '派遣失敗',
sendFailedMessage: '請檢查艦隊數量、燃料是否充足,或載貨量是否超出限制。',
recallFailed: '召回失敗',
recallFailedMessage: '該任務無法召回。',
unknownPlanet: '未知星球',
fleetMissionSlots: '艦隊任務槽位'
},
officersView: {
title: '軍官',
activated: '已啟用',
inactive: '未啟用',
activeStatus: '啟用狀態',
expirationTime: '到期時間',
remainingTime: '剩餘時間',
recruitCost: '招募成本',
days: '天',
benefitsBonus: '效果加成',
resourceProduction: '資源產量',
darkMatterProduction: '暗物質產量',
energyProduction: '電量產出',
buildingSpeed: '建築速度',
researchSpeed: '研究速度',
fleetSpeed: '艦隊速度',
fuelConsumption: '燃料消耗',
defense: '防禦力',
storageCapacity: '倉儲容量',
buildQueue: '建築佇列',
fleetSlots: '艦隊槽位',
hire: '招募',
renew: '續約',
dismiss: '解雇',
hireTitle: '招募軍官',
hireMessage: '確定要招募 {name} 嗎有效期為7天。',
renewTitle: '續約軍官',
renewMessage: '確定要為 {name} 續約7天嗎',
dismissTitle: '解雇軍官',
dismissMessage: '確定要解雇 {name} 嗎?不會返還任何費用。',
hireFailed: '招募失敗',
renewFailed: '續約失敗',
insufficientResources: '資源不足!'
},
galaxyView: {
title: '星系',
selectCoordinates: '選擇座標',
galaxy: '銀河系',
selectGalaxy: '選擇銀河系',
system: '星系',
selectSystem: '選擇星系',
view: '查看',
myPlanet: '我的星球',
totalPositions: '共10個星球位置',
mine: '我的',
hostile: '敵對',
emptySlot: '空位 - 可殖民',
scout: '偵察',
attack: '攻擊',
colonize: '殖民',
switch: '切換',
scoutPlanetTitle: '偵察星球',
attackPlanetTitle: '攻擊星球',
colonizePlanetTitle: '殖民星球',
scoutPlanetMessage: '確定要派遣間諜探測器偵察星球 [{coordinates}] 嗎?\n\n請前往艦隊頁面選擇艦船並派遣。',
attackPlanetMessage: '確定要攻擊星球 [{coordinates}] 嗎?\n\n請前往艦隊頁面選擇艦船並派遣。',
colonizePlanetMessage: '確定要殖民位置 [{coordinates}] 嗎?\n\n請前往艦隊頁面派遣殖民船。'
},
messagesView: {
title: '訊息中心',
battleReports: '戰鬥報告',
spyReports: '間諜報告',
noBattleReports: '暫無戰鬥報告',
noSpyReports: '暫無間諜報告',
battleReport: '戰鬥報告',
spyReport: '間諜報告',
victory: '勝利',
defeat: '失敗',
draw: '平局',
attackerFleet: '攻擊方艦隊',
defenderFleet: '防守方艦隊',
defenderDefense: '防守方防禦',
attackerLosses: '攻擊方損失',
defenderLosses: '防守方損失',
noLosses: '無損失',
plunder: '掠奪資源',
debrisField: '殘骸場',
resources: '資源',
fleet: '艦隊',
defense: '防禦',
buildings: '建築'
},
simulatorView: {
title: '戰鬥模擬器',
attacker: '攻擊方',
defender: '防守方',
attackerConfig: '攻擊方配置',
attackerConfigDesc: '設置攻擊方的艦隊和科技等級',
defenderConfig: '防守方配置',
defenderConfigDesc: '設置防守方的艦隊、防禦和科技等級',
fleet: '艦隊',
defenseStructures: '防禦設施',
techLevels: '科技等級',
weapon: '武器',
shield: '護盾',
armor: '裝甲',
defenderResources: '防守方資源(用於掠奪計算)',
startSimulation: '開始模擬',
reset: '重置',
battleResult: '戰鬥結果',
attackerVictory: '攻擊方勝利',
defenderVictory: '防守方勝利',
draw: '平局',
afterRounds: '經過 {rounds} 回合戰鬥',
attackerLosses: '攻擊方損失',
defenderLosses: '防守方損失',
noLosses: '無損失',
attackerRemaining: '攻擊方剩餘',
defenderRemaining: '防守方剩餘',
allDestroyed: '全部摧毀',
plunderableResources: '可掠奪資源',
debrisField: '殘骸場',
moonChance: '月球生成機率',
showRoundDetails: '顯示回合詳情',
hideRoundDetails: '隱藏回合詳情',
round: '第 {round} 回合',
attackerRemainingPower: '攻擊方剩餘火力',
defenderRemainingPower: '防守方剩餘火力'
},
settings: {
dataManagement: '資料管理',
dataManagementDesc: '匯出、匯入或清除遊戲資料',
exportData: '匯出資料',
exportDataDesc: '將遊戲進度匯出為JSON檔案',
export: '匯出',
exporting: '匯出中...',
exportSuccess: '匯出成功',
exportFailed: '匯出失敗,請重試',
importData: '匯入資料',
importDataDesc: '從JSON檔案恢復遊戲進度',
selectFile: '匯入',
importSuccess: '匯入成功',
importConfirmTitle: '確認匯入資料',
importConfirmMessage: '匯入資料將覆蓋目前遊戲進度,此操作不可撤銷。確定要繼續嗎?',
importFailed: '匯入失敗,請檢查檔案格式',
clearData: '清除資料',
clearDataDesc: '刪除所有遊戲資料並重置遊戲',
clear: '清除',
clearConfirmTitle: '確認清除資料',
clearConfirmMessage: '這將刪除所有遊戲資料並重新開始,此操作不可撤銷。確定要繼續嗎?',
gameSettings: '遊戲設定',
gameSettingsDesc: '調整遊戲參數和偏好設定',
playerName: '玩家名稱',
gameSpeed: '遊戲速度',
gameSpeedDesc: '目前遊戲速度倍率',
about: '關於',
version: '版本',
buildDate: '建置日期',
community: '社群',
github: 'GitHub 儲存庫',
qqGroup: 'QQ 交流群'
}
}

353
src/logic/battleLogic.ts Normal file
View File

@@ -0,0 +1,353 @@
import type { Fleet, Resources, BattleResult, Officer } from '@/types/game'
import { DefenseType, ShipType, OfficerType } from '@/types/game'
import { SHIPS, DEFENSES } from '@/config/gameConfig'
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
}
/**
* 执行战斗模拟
*/
export const simulateBattle = (
attackerFleet: Partial<Fleet>,
defenderFleet: Partial<Fleet>,
defenderDefense: Partial<Record<DefenseType, number>>,
defenderResources: Resources,
attackerOfficers: Record<OfficerType, Officer>,
defenderOfficers: Record<OfficerType, Officer>
): 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)
// 复制战斗方用于战斗
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)
// 计算掠夺(仅攻击方胜利时)
const plunder =
winner === 'attacker'
? calculatePlunder(defenderResources, attackerFleet, 10000)
: { metal: 0, crystal: 0, deuterium: 0, darkMatter: 0, energy: 0 }
// 生成战斗报告
const battleResult: BattleResult = {
id: `battle_${Date.now()}`,
timestamp: Date.now(),
attackerId: '',
defenderId: '',
attackerPlanetId: '',
defenderPlanetId: '',
attackerFleet,
defenderFleet,
defenderDefense,
attackerLosses,
defenderLosses,
winner,
plunder,
debrisField
}
return battleResult
}
/**
* 计算防御设施修复防御有70%概率修复)
*/
export const repairDefense = (
defenseBeforeBattle: Partial<Record<DefenseType, number>>,
defenseAfterBattle: Partial<Record<DefenseType, number>>
): Partial<Record<DefenseType, number>> => {
const repaired: Partial<Record<DefenseType, number>> = { ...defenseAfterBattle }
Object.keys(defenseBeforeBattle).forEach(defenseType => {
const before = defenseBeforeBattle[defenseType as DefenseType] || 0
const after = defenseAfterBattle[defenseType as DefenseType] || 0
const lost = before - after
if (lost > 0) {
// 70%修复概率
const repairedCount = Math.floor(lost * 0.7)
repaired[defenseType as DefenseType] = after + repairedCount
}
})
return repaired
}

194
src/logic/buildingLogic.ts Normal file
View File

@@ -0,0 +1,194 @@
import type { Planet, Resources, BuildQueueItem } from '@/types/game'
import { BuildingType, TechnologyType, ShipType, DefenseType } from '@/types/game'
import { BUILDINGS } from '@/config/gameConfig'
import * as pointsLogic from './pointsLogic'
/**
* 计算建筑升级成本
*/
export const calculateBuildingCost = (buildingType: BuildingType, targetLevel: number): Resources => {
const config = BUILDINGS[buildingType]
const multiplier = Math.pow(config.costMultiplier, targetLevel - 1)
return {
metal: Math.floor(config.baseCost.metal * multiplier),
crystal: Math.floor(config.baseCost.crystal * multiplier),
deuterium: Math.floor(config.baseCost.deuterium * multiplier),
darkMatter: Math.floor(config.baseCost.darkMatter * multiplier),
energy: 0
}
}
/**
* 计算建筑升级时间
*/
export const calculateBuildingTime = (buildingType: BuildingType, targetLevel: number, buildingSpeedBonus: number = 0): number => {
const config = BUILDINGS[buildingType]
const multiplier = Math.pow(config.costMultiplier, targetLevel - 1)
const baseTime = config.baseTime * multiplier
const speedMultiplier = 1 - buildingSpeedBonus / 100
return Math.floor(baseTime * speedMultiplier)
}
/**
* 计算已用空间
*/
export const calculateUsedSpace = (planet: Planet): number => {
let usedSpace = 0
Object.entries(planet.buildings).forEach(([buildingType, level]) => {
if (level > 0) {
const config = BUILDINGS[buildingType as BuildingType]
usedSpace += config.spaceUsage * level
}
})
return usedSpace
}
/**
* 检查建筑升级条件
*/
export const checkBuildingRequirements = (
buildingType: BuildingType,
planet: Planet,
technologies: Partial<Record<TechnologyType, number>>
): boolean => {
const config = BUILDINGS[buildingType]
const requirements = (config as any).requirements
if (!requirements) return true
for (const [key, level] of Object.entries(requirements)) {
const requiredLevel = level as number
if (Object.values(BuildingType).includes(key as BuildingType)) {
if ((planet.buildings[key as BuildingType] || 0) < requiredLevel) {
return false
}
} else if (Object.values(TechnologyType).includes(key as TechnologyType)) {
if ((technologies[key as TechnologyType] || 0) < requiredLevel) {
return false
}
}
}
return true
}
/**
* 检查是否有足够空间建造
*/
export const checkSpaceAvailable = (planet: Planet, buildingType: BuildingType): boolean => {
const usedSpace = calculateUsedSpace(planet)
const buildingConfig = BUILDINGS[buildingType]
const requiredSpace = buildingConfig.spaceUsage
return usedSpace + requiredSpace <= planet.maxSpace
}
/**
* 创建建造队列项
*/
export const createBuildQueueItem = (buildingType: BuildingType, targetLevel: number, buildTime: number): BuildQueueItem => {
const now = Date.now()
return {
id: `build_${now}`,
type: 'building',
itemType: buildingType,
targetLevel,
startTime: now,
endTime: now + buildTime * 1000
}
}
/**
* 处理建造完成
*/
export const completeBuildQueue = (planet: Planet, now: number, onPointsEarned?: (points: number, type: 'building' | 'ship' | 'defense', itemType: string, level?: number, quantity?: number) => void): void => {
planet.buildQueue = planet.buildQueue.filter(item => {
if (now >= item.endTime) {
// 建造完成
if (item.type === 'building') {
const oldLevel = planet.buildings[item.itemType as BuildingType] || 0
const newLevel = item.targetLevel || 0
planet.buildings[item.itemType as BuildingType] = newLevel
// 计算并累积积分
if (onPointsEarned && newLevel > oldLevel) {
const points = pointsLogic.calculateBuildingPoints(item.itemType as BuildingType, oldLevel, newLevel)
onPointsEarned(points, 'building', item.itemType, newLevel)
}
} else if (item.type === 'ship') {
const shipType = item.itemType as ShipType
const quantity = item.quantity || 0
planet.fleet[shipType] = (planet.fleet[shipType] || 0) + quantity
// 计算并累积积分
if (onPointsEarned && quantity > 0) {
const points = pointsLogic.calculateShipPoints(shipType, quantity)
onPointsEarned(points, 'ship', item.itemType, undefined, quantity)
}
} else if (item.type === 'defense') {
const defenseType = item.itemType as DefenseType
const quantity = item.quantity || 0
planet.defense[defenseType] = (planet.defense[defenseType] || 0) + quantity
// 计算并累积积分
if (onPointsEarned && quantity > 0) {
const points = pointsLogic.calculateDefensePoints(defenseType, quantity)
onPointsEarned(points, 'defense', item.itemType, undefined, quantity)
}
} else if (item.type === 'demolish') {
// 拆除完成,降低建筑等级
// 注意:拆除不会扣除积分,积分只增不减
const buildingType = item.itemType as BuildingType
const currentLevel = planet.buildings[buildingType] || 0
planet.buildings[buildingType] = Math.max(0, currentLevel - 1)
}
return false
}
return true
})
}
/**
* 计算拆除返还资源
* @param buildingType 建筑类型
* @param currentLevel 当前等级
* @returns 返还50%的当前等级建造成本
*/
export const calculateDemolishRefund = (buildingType: BuildingType, currentLevel: number): Resources => {
const cost = calculateBuildingCost(buildingType, currentLevel)
return {
metal: Math.floor(cost.metal * 0.5),
crystal: Math.floor(cost.crystal * 0.5),
deuterium: Math.floor(cost.deuterium * 0.5),
darkMatter: Math.floor(cost.darkMatter * 0.5),
energy: 0
}
}
/**
* 计算拆除时间
* @param buildingType 建筑类型
* @param currentLevel 当前等级
* @param buildingSpeedBonus 建筑速度加成
* @returns 拆除时间建造时间的50%
*/
export const calculateDemolishTime = (buildingType: BuildingType, currentLevel: number, buildingSpeedBonus: number = 0): number => {
const buildTime = calculateBuildingTime(buildingType, currentLevel, buildingSpeedBonus)
return Math.floor(buildTime * 0.5)
}
/**
* 创建拆除队列项
* @param buildingType 建筑类型
* @param currentLevel 当前等级
* @param demolishTime 拆除时间
* @returns 拆除队列项
*/
export const createDemolishQueueItem = (buildingType: BuildingType, currentLevel: number, demolishTime: number): BuildQueueItem => {
const now = Date.now()
return {
id: `demolish_${now}`,
type: 'demolish',
itemType: buildingType,
targetLevel: currentLevel - 1, // 目标等级为当前等级-1
startTime: now,
endTime: now + demolishTime * 1000
}
}

View File

@@ -0,0 +1,161 @@
import type { Planet, Resources, BuildQueueItem, Officer } from '@/types/game'
import { BuildingType, TechnologyType, OfficerType } from '@/types/game'
import * as buildingLogic from './buildingLogic'
import * as resourceLogic from './resourceLogic'
import * as publicLogic from './publicLogic'
import * as officerLogic from './officerLogic'
import { BUILDINGS } from '@/config/gameConfig'
/**
* 验证建筑升级的所有条件
*/
export const validateBuildingUpgrade = (
planet: Planet,
buildingType: BuildingType,
technologies: Partial<Record<TechnologyType, number>>,
officers: Record<OfficerType, Officer>
): {
valid: boolean
reason?: string
} => {
const currentLevel = planet.buildings[buildingType] || 0
const targetLevel = currentLevel + 1
const cost = buildingLogic.calculateBuildingCost(buildingType, targetLevel)
const buildingConfig = BUILDINGS[buildingType]
// 检查星球/月球限制
if (buildingConfig.planetOnly && planet.isMoon) {
return { valid: false, reason: 'errors.planetOnly' }
}
if (buildingConfig.moonOnly && !planet.isMoon) {
return { valid: false, reason: 'errors.moonOnly' }
}
// 计算军官加成
const bonuses = officerLogic.calculateActiveBonuses(officers, Date.now())
// 检查建造队列是否已满
const maxQueue = publicLogic.getMaxBuildQueue(planet, bonuses.additionalBuildQueue)
if (planet.buildQueue.length >= maxQueue) {
return { valid: false, reason: 'errors.buildQueueFull' }
}
// 检查空间
if (!buildingLogic.checkSpaceAvailable(planet, buildingType)) {
return { valid: false, reason: 'errors.insufficientSpace' }
}
// 检查资源
if (!resourceLogic.checkResourcesAvailable(planet.resources, cost)) {
return { valid: false, reason: 'errors.insufficientResources' }
}
// 检查前置条件
if (!buildingLogic.checkBuildingRequirements(buildingType, planet, technologies)) {
return { valid: false, reason: 'errors.requirementsNotMet' }
}
return { valid: true }
}
/**
* 执行建筑升级(扣除资源,添加到队列)
*/
export const executeBuildingUpgrade = (planet: Planet, buildingType: BuildingType, officers: Record<OfficerType, Officer>): BuildQueueItem => {
const currentLevel = planet.buildings[buildingType] || 0
const targetLevel = currentLevel + 1
const cost = buildingLogic.calculateBuildingCost(buildingType, targetLevel)
// 计算军官加成
const bonuses = officerLogic.calculateActiveBonuses(officers, Date.now())
const time = buildingLogic.calculateBuildingTime(buildingType, targetLevel, bonuses.buildingSpeedBonus)
// 扣除资源
resourceLogic.deductResources(planet.resources, cost)
// 创建队列项
return buildingLogic.createBuildQueueItem(buildingType, targetLevel, time)
}
/**
* 取消建造并计算返还资源
*/
export const cancelBuildingUpgrade = (_planet: Planet, queueItem: BuildQueueItem): Resources => {
const cost = buildingLogic.calculateBuildingCost(queueItem.itemType as BuildingType, queueItem.targetLevel || 1)
return {
metal: Math.floor(cost.metal * 0.5),
crystal: Math.floor(cost.crystal * 0.5),
deuterium: Math.floor(cost.deuterium * 0.5),
darkMatter: Math.floor(cost.darkMatter * 0.5),
energy: 0
}
}
/**
* 查找队列项
*/
export const findQueueItem = (
queue: BuildQueueItem[],
queueId: string
): {
item: BuildQueueItem | null
index: number
} => {
const index = queue.findIndex(q => q.id === queueId)
if (index === -1) {
return { item: null, index: -1 }
}
return { item: queue[index] || null, index }
}
/**
* 验证建筑拆除的所有条件
*/
export const validateBuildingDemolish = (
planet: Planet,
buildingType: BuildingType,
officers: Record<OfficerType, Officer>
): {
valid: boolean
reason?: string
} => {
const currentLevel = planet.buildings[buildingType] || 0
// 检查建筑等级
if (currentLevel <= 0) {
return { valid: false, reason: 'errors.buildingLevelZero' }
}
// 计算军官加成
const bonuses = officerLogic.calculateActiveBonuses(officers, Date.now())
// 检查建造队列是否已满
const maxQueue = publicLogic.getMaxBuildQueue(planet, bonuses.additionalBuildQueue)
if (planet.buildQueue.length >= maxQueue) {
return { valid: false, reason: 'errors.buildQueueFull' }
}
return { valid: true }
}
/**
* 执行建筑拆除(返还资源,添加到队列)
*/
export const executeBuildingDemolish = (planet: Planet, buildingType: BuildingType, officers: Record<OfficerType, Officer>): BuildQueueItem => {
const currentLevel = planet.buildings[buildingType] || 0
// 计算军官加成
const bonuses = officerLogic.calculateActiveBonuses(officers, Date.now())
const demolishTime = buildingLogic.calculateDemolishTime(buildingType, currentLevel, bonuses.buildingSpeedBonus)
// 返还50%资源
const refund = buildingLogic.calculateDemolishRefund(buildingType, currentLevel)
planet.resources.metal += refund.metal
planet.resources.crystal += refund.crystal
planet.resources.deuterium += refund.deuterium
planet.resources.darkMatter += refund.darkMatter
// 创建拆除队列项
return buildingLogic.createDemolishQueueItem(buildingType, currentLevel, demolishTime)
}

374
src/logic/fleetLogic.ts Normal file
View File

@@ -0,0 +1,374 @@
import type { FleetMission, Planet, Resources, Fleet, BattleResult, SpyReport, Player, Officer } from '@/types/game'
import { ShipType, DefenseType, MissionType, BuildingType, OfficerType } from '@/types/game'
import * as battleLogic from './battleLogic'
import * as moonLogic from './moonLogic'
import * as moonValidation from './moonValidation'
/**
* 计算两个星球之间的距离
*/
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))
}
/**
* 计算飞行时间
*/
export const calculateFlightTime = (distance: number, minSpeed: number): number => {
return Math.max(10, Math.floor((distance * 10000) / minSpeed)) // 至少10秒
}
/**
* 创建舰队任务
*/
export const createFleetMission = (
playerId: string,
originPlanetId: string,
targetPosition: { galaxy: number; system: number; position: number },
missionType: MissionType,
fleet: Partial<Fleet>,
cargo: Resources,
flightTime: number
): FleetMission => {
const now = Date.now()
return {
id: `mission_${now}`,
playerId,
originPlanetId,
targetPosition,
missionType,
fleet,
cargo,
departureTime: now,
arrivalTime: now + flightTime * 1000,
returnTime: now + flightTime * 2 * 1000,
status: 'outbound'
}
}
/**
* 处理运输任务到达
*/
export const processTransportArrival = (mission: FleetMission, targetPlanet: Planet | undefined): void => {
if (targetPlanet) {
targetPlanet.resources.metal += mission.cargo.metal
targetPlanet.resources.crystal += mission.cargo.crystal
targetPlanet.resources.deuterium += mission.cargo.deuterium
targetPlanet.resources.darkMatter += mission.cargo.darkMatter
}
mission.status = 'returning'
mission.cargo = { metal: 0, crystal: 0, deuterium: 0, darkMatter: 0, energy: 0 }
}
/**
* 处理攻击任务到达
*/
export const processAttackArrival = (
mission: FleetMission,
targetPlanet: Planet | undefined,
attacker: Player,
defender: Player | null,
allPlanets: Planet[]
): { battleResult: BattleResult; moon: Planet | null } | null => {
if (!targetPlanet || targetPlanet.ownerId === attacker.id) {
mission.status = 'returning'
return null
}
// 执行战斗
const battleResult = battleLogic.simulateBattle(
mission.fleet,
targetPlanet.fleet,
targetPlanet.defense,
targetPlanet.resources,
attacker.officers,
defender?.officers || ({} as Record<OfficerType, Officer>)
)
// 更新战斗报告ID
battleResult.id = `battle_${Date.now()}`
battleResult.attackerId = attacker.id
battleResult.defenderId = targetPlanet.ownerId || 'unknown'
battleResult.attackerPlanetId = mission.originPlanetId
battleResult.defenderPlanetId = targetPlanet.id
// 如果攻击方获胜,掠夺资源已经在战斗模拟中计算
mission.cargo = battleResult.plunder
// 更新舰队 - 计算幸存舰船
const survivingFleet: Partial<Fleet> = {}
Object.entries(mission.fleet).forEach(([shipType, initialCount]) => {
const lost = battleResult.attackerLosses[shipType as ShipType] || 0
const surviving = initialCount - lost
if (surviving > 0) {
survivingFleet[shipType as ShipType] = surviving
}
})
mission.fleet = survivingFleet
// 更新目标星球舰队和防御
Object.entries(battleResult.defenderLosses.fleet).forEach(([shipType, lost]) => {
targetPlanet.fleet[shipType as ShipType] = Math.max(0, targetPlanet.fleet[shipType as ShipType] - lost)
})
Object.entries(battleResult.defenderLosses.defense).forEach(([defenseType, lost]) => {
targetPlanet.defense[defenseType as DefenseType] = Math.max(0, targetPlanet.defense[defenseType as DefenseType] - lost)
})
// 防御设施修复70%概率)
const defenseBeforeBattle: Partial<Record<DefenseType, number>> = { ...targetPlanet.defense }
Object.entries(battleResult.defenderLosses.defense).forEach(([defenseType, lost]) => {
defenseBeforeBattle[defenseType as DefenseType] = (defenseBeforeBattle[defenseType as DefenseType] || 0) + lost
})
targetPlanet.defense = battleLogic.repairDefense(defenseBeforeBattle, targetPlanet.defense) as Record<DefenseType, number>
// 扣除掠夺的资源
targetPlanet.resources.metal -= battleResult.plunder.metal
targetPlanet.resources.crystal -= battleResult.plunder.crystal
targetPlanet.resources.deuterium -= battleResult.plunder.deuterium
mission.status = 'returning'
// 尝试生成月球(如果该位置还没有月球)
let moon: Planet | null = null
const moonCheck = moonValidation.canCreateMoon(allPlanets, targetPlanet.position, battleResult.debrisField)
if (moonCheck.canCreate && moonCheck.chance) {
if (moonValidation.shouldGenerateMoon(moonCheck.chance)) {
moon = moonLogic.tryGenerateMoon(battleResult.debrisField, targetPlanet.position, targetPlanet.id, targetPlanet.ownerId || 'unknown')
}
}
return { battleResult, moon }
}
/**
* 处理殖民任务到达
*/
export const processColonizeArrival = (
mission: FleetMission,
targetPlanet: Planet | undefined,
playerId: string,
colonyNameTemplate: string = 'Colony'
): Planet | null => {
if (targetPlanet) {
// 位置已被占用
mission.status = 'returning'
return null
}
// 创建新殖民地
const newPlanet: Planet = {
id: `planet_${Date.now()}`,
name: `${colonyNameTemplate} ${mission.targetPosition.galaxy}:${mission.targetPosition.system}:${mission.targetPosition.position}`,
ownerId: playerId,
position: mission.targetPosition,
resources: { metal: 500, crystal: 500, deuterium: 0, darkMatter: 0, energy: 0 },
buildings: {} as Record<BuildingType, number>,
fleet: {
[ShipType.LightFighter]: 0,
[ShipType.HeavyFighter]: 0,
[ShipType.Cruiser]: 0,
[ShipType.Battleship]: 0,
[ShipType.SmallCargo]: 0,
[ShipType.LargeCargo]: 0,
[ShipType.ColonyShip]: 0,
[ShipType.Recycler]: 0,
[ShipType.EspionageProbe]: 0,
[ShipType.DarkMatterHarvester]: 0
},
defense: {
[DefenseType.RocketLauncher]: 0,
[DefenseType.LightLaser]: 0,
[DefenseType.HeavyLaser]: 0,
[DefenseType.GaussCannon]: 0,
[DefenseType.IonCannon]: 0,
[DefenseType.PlasmaTurret]: 0,
[DefenseType.SmallShieldDome]: 0,
[DefenseType.LargeShieldDome]: 0
},
buildQueue: [],
lastUpdate: Date.now(),
maxSpace: 200,
isMoon: false
}
Object.values(BuildingType).forEach(building => {
newPlanet.buildings[building] = 0
})
// 殖民船被消耗
mission.fleet[ShipType.ColonyShip] = (mission.fleet[ShipType.ColonyShip] || 1) - 1
mission.status = 'returning'
return newPlanet
}
/**
* 处理间谍任务到达
*/
export const processSpyArrival = (mission: FleetMission, targetPlanet: Planet | undefined, playerId: string): SpyReport | null => {
if (!targetPlanet) {
mission.status = 'returning'
return null
}
const spyReport: SpyReport = {
id: `spy_${Date.now()}`,
timestamp: Date.now(),
spyId: playerId,
targetPlanetId: targetPlanet.id,
targetPlayerId: targetPlanet.ownerId || 'unknown',
resources: { ...targetPlanet.resources },
fleet: { ...targetPlanet.fleet },
defense: { ...targetPlanet.defense },
buildings: { ...targetPlanet.buildings },
technologies: {},
detectionChance: 0.3
}
mission.status = 'returning'
return spyReport
}
/**
* 处理部署任务到达
*/
export const processDeployArrival = (mission: FleetMission, targetPlanet: Planet | undefined, playerId: string): boolean => {
if (!targetPlanet || targetPlanet.ownerId !== playerId) {
mission.status = 'returning'
return false
}
for (const [shipType, count] of Object.entries(mission.fleet)) {
targetPlanet.fleet[shipType as ShipType] += count
}
// 部署任务直接完成,不返回
return true
}
/**
* 处理舰队任务返回
*/
export const processFleetReturn = (mission: FleetMission, originPlanet: Planet): void => {
// 舰船返回
Object.entries(mission.fleet).forEach(([shipType, count]) => {
if (count > 0) {
originPlanet.fleet[shipType as ShipType] += count
}
})
// 资源返回(掠夺物或运输货物)
originPlanet.resources.metal += mission.cargo.metal
originPlanet.resources.crystal += mission.cargo.crystal
originPlanet.resources.deuterium += mission.cargo.deuterium
originPlanet.resources.darkMatter += mission.cargo.darkMatter
}
/**
* 更新舰队任务状态
*/
export const updateFleetMissions = (
missions: FleetMission[],
planets: Map<string, Planet>,
attacker: Player,
defender: Player | null,
now: number
): {
completedMissions: string[]
battleReports: BattleResult[]
spyReports: SpyReport[]
newColonies: Planet[]
newMoons: Planet[]
} => {
const completedMissions: string[] = []
const battleReports: BattleResult[] = []
const spyReports: SpyReport[] = []
const newColonies: Planet[] = []
const newMoons: Planet[] = []
// 获取所有星球列表(用于月球生成检查)
const allPlanets = Array.from(planets.values())
missions.forEach(mission => {
const originPlanet = attacker.planets.find(p => p.id === mission.originPlanetId)
if (mission.status === 'outbound' && now >= mission.arrivalTime) {
// 任务到达目标
const targetKey = `${mission.targetPosition.galaxy}:${mission.targetPosition.system}:${mission.targetPosition.position}`
const targetPlanet = planets.get(targetKey)
switch (mission.missionType) {
case MissionType.Transport:
processTransportArrival(mission, targetPlanet)
break
case MissionType.Attack:
const attackResult = processAttackArrival(mission, targetPlanet, attacker, defender, allPlanets)
if (attackResult) {
battleReports.push(attackResult.battleResult)
if (attackResult.moon) {
newMoons.push(attackResult.moon)
// 将月球添加到planets map中
const moonKey = `${attackResult.moon.position.galaxy}:${attackResult.moon.position.system}:${attackResult.moon.position.position}`
planets.set(moonKey, attackResult.moon)
}
}
break
case MissionType.Colonize:
const newColony = processColonizeArrival(mission, targetPlanet, attacker.id)
if (newColony) {
newColonies.push(newColony)
planets.set(targetKey, newColony)
}
break
case MissionType.Spy:
const spyReport = processSpyArrival(mission, targetPlanet, attacker.id)
if (spyReport) {
spyReports.push(spyReport)
}
break
case MissionType.Deploy:
const deployed = processDeployArrival(mission, targetPlanet, attacker.id)
if (deployed) {
completedMissions.push(mission.id)
}
break
}
}
if (mission.status === 'returning' && mission.returnTime && now >= mission.returnTime) {
// 舰队返回
if (originPlanet) {
processFleetReturn(mission, originPlanet)
}
completedMissions.push(mission.id)
}
})
return { completedMissions, battleReports, spyReports, newColonies, newMoons }
}
/**
* 召回舰队
*/
export const recallFleetMission = (mission: FleetMission, now: number): boolean => {
if (mission.status !== 'outbound') return false
const elapsedTime = now - mission.departureTime
// 如果还在飞行途中,立即返回
if (now < mission.arrivalTime) {
mission.status = 'returning'
mission.returnTime = now + elapsedTime // 返回时间等于已飞行的时间
return true
}
return false
}

132
src/logic/gameLogic.ts Normal file
View File

@@ -0,0 +1,132 @@
import type { Planet, Player, BuildQueueItem, Officer } from '@/types/game'
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'
/**
* 初始化玩家数据
*/
export const initializePlayer = (playerId: string, playerName: string = 'Commander'): Player => {
const player: Player = {
id: playerId,
name: playerName,
planets: [],
technologies: {} as Record<TechnologyType, number>,
officers: {} as Record<OfficerType, Officer>,
researchQueue: [],
fleetMissions: [],
battleReports: [],
spyReports: [],
points: 0
}
// 初始化科技等级
Object.values(TechnologyType).forEach(tech => {
player.technologies[tech] = 0
})
// 初始化军官状态
Object.values(OfficerType).forEach(officer => {
player.officers[officer] = officerLogic.createInactiveOfficer(officer)
})
return player
}
/**
* 检查是否需要初始化游戏
*/
export const shouldInitializeGame = (planets: Planet[]): boolean => {
return planets.length === 0
}
/**
* 更新所有星球的最后更新时间
*/
export const updatePlanetsLastUpdate = (planets: Planet[], now: number): void => {
planets.forEach(planet => {
planet.lastUpdate = now
})
}
/**
* 生成星系位置列表
*/
export const generateSystemPositions = (
_galaxy: number,
_system: number,
count: number = 10
): Array<{ position: number; planet: Planet | null }> => {
const result: Array<{ position: number; planet: Planet | null }> = []
for (let pos = 1; pos <= count; pos++) {
result.push({ position: pos, planet: null })
}
return result
}
/**
* 生成随机NPC星球位置
*/
export const generateRandomPosition = (): { galaxy: number; system: number; position: number } => {
return {
galaxy: Math.floor(Math.random() * 9) + 1,
system: Math.floor(Math.random() * 10) + 1,
position: Math.floor(Math.random() * 10) + 1
}
}
/**
* 生成位置键
*/
export const generatePositionKey = (galaxy: number, system: number, position: number): string => {
return `${galaxy}:${system}:${position}`
}
/**
* 更新游戏状态 - 处理所有星球和任务
*/
export const processGameUpdate = (
player: Player,
now: number
): {
updatedResearchQueue: BuildQueueItem[]
} => {
// 获取军官加成
const bonuses = officerLogic.calculateActiveBonuses(player.officers, now)
// 创建积分回调函数
const onPointsEarned = (points: number, _type: string, _itemType: string, _level?: number, _quantity?: number) => {
pointsLogic.addPoints(player, points)
}
// 更新所有星球
player.planets.forEach(planet => {
// 更新资源
resourceLogic.updatePlanetResources(planet, now, bonuses)
// 检查建造队列
buildingLogic.completeBuildQueue(planet, now, onPointsEarned)
})
// 检查研究队列
const updatedResearchQueue = researchLogic.completeResearchQueue(
player.researchQueue,
player.technologies,
now,
onPointsEarned
)
return {
updatedResearchQueue
}
}
/**
* 检查并返回过期的军官列表
*/
export const checkOfficersExpiration = (officers: Record<OfficerType, Officer>, now: number): void => {
officerLogic.checkAndDeactivateExpiredOfficers(officers, now)
}

Some files were not shown because too many files have changed in this diff Show More