mirror of
https://github.com/setube/ogame-vue-ts.git
synced 2026-07-11 00:12:39 +08:00
feat: 新增NPC与外交逻辑,优化UI组件结构
重构并精简了部分UI组件,移除冗余弹窗与详情组件,新增NPC相关逻辑(npcBehaviorLogic、npcGrowthLogic、npcStore等)及外交逻辑(diplomaticLogic、DiplomacyView)。完善分页、标签、复选框等通用UI组件。优化战报弹窗,调整README下载链接为相对路径,修复部分国际化内容。
This commit is contained in:
279
src/App.vue
279
src/App.vue
@@ -228,7 +228,18 @@
|
||||
</SidebarInset>
|
||||
|
||||
<!-- 确认对话框 -->
|
||||
<ConfirmDialog ref="confirmDialog" />
|
||||
<AlertDialog :open="confirmDialogOpen" @update:open="confirmDialogOpen = $event">
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{{ confirmDialogTitle }}</AlertDialogTitle>
|
||||
<AlertDialogDescription>{{ confirmDialogMessage }}</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>{{ t('common.cancel') }}</AlertDialogCancel>
|
||||
<AlertDialogAction @click="handleConfirmAction">{{ t('common.confirm') }}</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
<!-- 详情弹窗 -->
|
||||
<DetailDialog />
|
||||
@@ -242,7 +253,6 @@
|
||||
import { onMounted, onUnmounted, computed, ref } from 'vue'
|
||||
import { RouterView, RouterLink } from 'vue-router'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useUniverseStore } from '@/stores/universeStore'
|
||||
import { useTheme } from '@/composables/useTheme'
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
import { localeNames, detectBrowserLocale, type Locale } from '@/locales'
|
||||
@@ -265,11 +275,18 @@
|
||||
SidebarTrigger
|
||||
} from '@/components/ui/sidebar'
|
||||
import ResourceIcon from '@/components/ResourceIcon.vue'
|
||||
import ConfirmDialog from '@/components/ConfirmDialog.vue'
|
||||
import DetailDialog from '@/components/DetailDialog.vue'
|
||||
import Sonner from '@/components/ui/sonner/Sonner.vue'
|
||||
import { MissionType } from '@/types/game'
|
||||
import type { BuildQueueItem, FleetMission } from '@/types/game'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { formatNumber, formatTime, getResourceColor } from '@/utils/format'
|
||||
import {
|
||||
Moon,
|
||||
@@ -289,175 +306,52 @@
|
||||
Wrench,
|
||||
ChevronsLeft
|
||||
} from 'lucide-vue-next'
|
||||
import * as gameLogic from '@/logic/gameLogic'
|
||||
import * as planetLogic from '@/logic/planetLogic'
|
||||
import * as officerLogic from '@/logic/officerLogic'
|
||||
import * as buildingValidation from '@/logic/buildingValidation'
|
||||
import * as resourceLogic from '@/logic/resourceLogic'
|
||||
import * as researchValidation from '@/logic/researchValidation'
|
||||
import * as fleetLogic from '@/logic/fleetLogic'
|
||||
import * as shipLogic from '@/logic/shipLogic'
|
||||
import pkg from '../package.json'
|
||||
import { useGameLifecycle } from '@/composables/useGameLifecycle'
|
||||
import { useMissionHandler } from '@/composables/useMissionHandler'
|
||||
import { useNPCHandler } from '@/composables/useNPCHandler'
|
||||
import { useQueueHandler } from '@/composables/useQueueHandler'
|
||||
import { useGameUpdate } from '@/composables/useGameUpdate'
|
||||
import { migrateGameData } from '@/utils/migration'
|
||||
|
||||
// 执行数据迁移(在 store 初始化之前)
|
||||
import pkg from '../package.json'
|
||||
migrateGameData()
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const universeStore = useUniverseStore()
|
||||
const { isDark } = useTheme()
|
||||
const { t } = useI18n()
|
||||
const confirmDialog = ref<InstanceType<typeof ConfirmDialog> | null>(null)
|
||||
|
||||
const confirmDialogOpen = ref(false)
|
||||
const confirmDialogTitle = ref('')
|
||||
const confirmDialogMessage = ref('')
|
||||
const confirmDialogAction = ref<(() => void) | null>(null)
|
||||
// 所有可用的语言选项
|
||||
const locales: Locale[] = ['zh-CN', 'zh-TW', 'en', 'de', 'ru', 'ko', 'ja']
|
||||
|
||||
// 侧边栏状态(不持久化,根据屏幕尺寸初始化)
|
||||
// PC端(≥1024px)默认打开,移动端默认关闭
|
||||
const sidebarOpen = ref(window.innerWidth >= 1024)
|
||||
|
||||
const initGame = async () => {
|
||||
const shouldInit = gameLogic.shouldInitializeGame(gameStore.player.planets)
|
||||
if (!shouldInit) {
|
||||
const now = Date.now()
|
||||
// 初始化 composables
|
||||
const { initGame } = useGameLifecycle()
|
||||
|
||||
// 计算离线收益(直接同步计算)
|
||||
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, now)
|
||||
gameStore.player.planets.forEach(planet => {
|
||||
resourceLogic.updatePlanetResources(planet, now, bonuses)
|
||||
})
|
||||
const { processMissionArrival, processMissionReturn } = useMissionHandler(t)
|
||||
|
||||
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 { processNPCMissionArrival, processNPCMissionReturn, updateNPCGrowth, updateNPCBehavior } = useNPCHandler()
|
||||
|
||||
const generateNPCPlanets = () => {
|
||||
const npcCount = 200
|
||||
for (let i = 0; i < npcCount; i++) {
|
||||
const position = gameLogic.generateRandomPosition()
|
||||
const key = gameLogic.generatePositionKey(position.galaxy, position.system, position.position)
|
||||
if (universeStore.planets[key]) continue
|
||||
const npcPlanet = planetLogic.createNPCPlanet(i, position, t('planet.planetPrefix'))
|
||||
universeStore.planets[key] = npcPlanet
|
||||
}
|
||||
}
|
||||
const { handleCancelBuild, handleCancelResearch, getItemName, getRemainingTime, getQueueProgress } = useQueueHandler(
|
||||
t,
|
||||
confirmDialogOpen,
|
||||
confirmDialogTitle,
|
||||
confirmDialogMessage,
|
||||
confirmDialogAction
|
||||
)
|
||||
|
||||
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 = async (mission: FleetMission) => {
|
||||
// 从宇宙星球地图中查找目标星球
|
||||
const targetKey = gameLogic.generatePositionKey(
|
||||
mission.targetPosition.galaxy,
|
||||
mission.targetPosition.system,
|
||||
mission.targetPosition.position
|
||||
)
|
||||
// 先从玩家星球中查找,再从宇宙地图中查找
|
||||
const targetPlanet =
|
||||
gameStore.player.planets.find(
|
||||
p =>
|
||||
p.position.galaxy === mission.targetPosition.galaxy &&
|
||||
p.position.system === mission.targetPosition.system &&
|
||||
p.position.position === mission.targetPosition.position
|
||||
) || universeStore.planets[targetKey]
|
||||
|
||||
if (mission.missionType === MissionType.Transport) {
|
||||
fleetLogic.processTransportArrival(mission, targetPlanet)
|
||||
} else if (mission.missionType === MissionType.Attack) {
|
||||
const attackResult = await fleetLogic.processAttackArrival(mission, targetPlanet, gameStore.player, null, gameStore.player.planets)
|
||||
if (attackResult) {
|
||||
gameStore.player.battleReports.push(attackResult.battleResult)
|
||||
if (attackResult.moon) {
|
||||
gameStore.player.planets.push(attackResult.moon)
|
||||
}
|
||||
if (attackResult.debrisField) {
|
||||
// 将残骸场添加到游戏状态
|
||||
universeStore.debrisFields[attackResult.debrisField.id] = attackResult.debrisField
|
||||
}
|
||||
}
|
||||
} else if (mission.missionType === MissionType.Colonize) {
|
||||
const newPlanet = fleetLogic.processColonizeArrival(mission, targetPlanet, gameStore.player.id, t('planet.colonyPrefix'))
|
||||
if (newPlanet) {
|
||||
gameStore.player.planets.push(newPlanet)
|
||||
}
|
||||
} 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
|
||||
}
|
||||
} else if (mission.missionType === MissionType.Recycle) {
|
||||
// 处理回收任务
|
||||
const debrisId = `debris_${mission.targetPosition.galaxy}_${mission.targetPosition.system}_${mission.targetPosition.position}`
|
||||
const debrisField = universeStore.debrisFields[debrisId]
|
||||
const recycleResult = fleetLogic.processRecycleArrival(mission, debrisField)
|
||||
if (recycleResult && debrisField) {
|
||||
if (recycleResult.remainingDebris && (recycleResult.remainingDebris.metal > 0 || recycleResult.remainingDebris.crystal > 0)) {
|
||||
// 更新残骸场
|
||||
universeStore.debrisFields[debrisId] = {
|
||||
id: debrisField.id,
|
||||
position: debrisField.position,
|
||||
resources: recycleResult.remainingDebris,
|
||||
createdAt: debrisField.createdAt,
|
||||
expiresAt: debrisField.expiresAt
|
||||
}
|
||||
} else {
|
||||
// 残骸场已被完全收集,删除
|
||||
delete universeStore.debrisFields[debrisId]
|
||||
}
|
||||
}
|
||||
} else if (mission.missionType === MissionType.Destroy) {
|
||||
// 处理行星毁灭任务
|
||||
const destroyResult = fleetLogic.processDestroyArrival(mission, targetPlanet, gameStore.player)
|
||||
if (destroyResult && destroyResult.success && destroyResult.planetId) {
|
||||
// 星球被摧毁
|
||||
// 从玩家星球列表中移除(如果是玩家的星球)
|
||||
const planetIndex = gameStore.player.planets.findIndex(p => p.id === destroyResult.planetId)
|
||||
if (planetIndex > -1) {
|
||||
gameStore.player.planets.splice(planetIndex, 1)
|
||||
} else {
|
||||
// 不是玩家星球,从宇宙地图中移除
|
||||
delete universeStore.planets[targetKey]
|
||||
}
|
||||
|
||||
// TODO: 可以添加战斗报告或摧毁报告来通知玩家结果
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
const { updateGame } = useGameUpdate(
|
||||
processMissionArrival,
|
||||
processMissionReturn,
|
||||
processNPCMissionArrival,
|
||||
processNPCMissionReturn,
|
||||
updateNPCGrowth,
|
||||
updateNPCBehavior
|
||||
)
|
||||
|
||||
// 游戏循环定时器
|
||||
let gameLoop: ReturnType<typeof setInterval> | null = null
|
||||
@@ -474,7 +368,7 @@
|
||||
if (isFirstVisit) {
|
||||
gameStore.locale = detectBrowserLocale()
|
||||
}
|
||||
await initGame()
|
||||
await initGame(t('common.playerName'), t('planet.homePlanet'), t('planet.planetPrefix'))
|
||||
// 启动游戏循环
|
||||
gameLoop = setInterval(() => {
|
||||
updateGame()
|
||||
@@ -561,71 +455,12 @@
|
||||
sidebarOpen.value = !sidebarOpen.value
|
||||
}
|
||||
|
||||
// 获取队列项的名称
|
||||
const getItemName = (item: BuildQueueItem): string => {
|
||||
if (item.type === 'building' || item.type === 'demolish') {
|
||||
const buildingName = t(`buildings.${item.itemType}`)
|
||||
return item.type === 'demolish' ? `${t('buildingsView.demolish')} - ${buildingName}` : buildingName
|
||||
} else if (item.type === 'technology') {
|
||||
return t(`technologies.${item.itemType}`)
|
||||
} else if (item.type === 'ship') {
|
||||
return t(`ships.${item.itemType}`)
|
||||
} else if (item.type === 'defense') {
|
||||
return t(`defenses.${item.itemType}`)
|
||||
// 处理确认对话框的确认操作
|
||||
const handleConfirmAction = () => {
|
||||
if (confirmDialogAction.value) {
|
||||
confirmDialogAction.value()
|
||||
}
|
||||
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
|
||||
}
|
||||
})
|
||||
confirmDialogOpen.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
<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>
|
||||
@@ -1,15 +1,18 @@
|
||||
<template>
|
||||
<Dialog v-model:open="isOpen">
|
||||
<DialogContent class="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle class="flex items-center gap-2">
|
||||
<Trophy class="h-5 w-5" />
|
||||
{{ t('messagesView.battleReport') }}
|
||||
</DialogTitle>
|
||||
<DialogDescription v-if="report">
|
||||
{{ formatDate(report.timestamp) }}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<ScrollableDialogContent container-class="sm:max-w-4xl max-h-[90vh]">
|
||||
<template #header>
|
||||
<DialogHeader>
|
||||
<DialogTitle class="flex items-center gap-2">
|
||||
<Trophy class="h-5 w-5" />
|
||||
{{ t('messagesView.battleReport') }}
|
||||
</DialogTitle>
|
||||
<DialogDescription v-if="report">
|
||||
{{ formatDate(report.timestamp) }}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
</template>
|
||||
|
||||
<div v-if="report" class="space-y-4">
|
||||
<!-- 战斗双方信息 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
||||
@@ -260,7 +263,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</ScrollableDialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
@@ -270,7 +273,7 @@
|
||||
import { useUniverseStore } from '@/stores/universeStore'
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
import { useGameConfig } from '@/composables/useGameConfig'
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Dialog, ScrollableDialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import ResourceIcon from '@/components/ResourceIcon.vue'
|
||||
|
||||
@@ -13,7 +13,19 @@
|
||||
</div>
|
||||
|
||||
<!-- 前置条件详情对话框 -->
|
||||
<AlertDialog ref="requirementsDialog" />
|
||||
<AlertDialog :open="requirementsDialogOpen" @update:open="requirementsDialogOpen = $event">
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{{ requirementsDialogTitle }}</AlertDialogTitle>
|
||||
<AlertDialogDescription class="whitespace-pre-line">
|
||||
{{ requirementsDialogMessage }}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogAction>{{ t('common.confirm') }}</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -25,7 +37,15 @@
|
||||
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 {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import * as publicLogic from '@/logic/publicLogic'
|
||||
|
||||
interface Props {
|
||||
@@ -37,7 +57,11 @@
|
||||
const gameStore = useGameStore()
|
||||
const { t } = useI18n()
|
||||
const { BUILDINGS, TECHNOLOGIES } = useGameConfig()
|
||||
const requirementsDialog = ref<InstanceType<typeof AlertDialog> | null>(null)
|
||||
|
||||
// AlertDialog 状态
|
||||
const requirementsDialogOpen = ref(false)
|
||||
const requirementsDialogTitle = ref('')
|
||||
const requirementsDialogMessage = ref('')
|
||||
|
||||
const isUnlocked = computed(() => {
|
||||
// 如果已经建造过(level > 0),则认为已解锁,不显示遮罩
|
||||
@@ -72,9 +96,8 @@
|
||||
}
|
||||
|
||||
const showRequirements = () => {
|
||||
requirementsDialog.value?.show({
|
||||
title: t('common.requirementsNotMet'),
|
||||
message: getRequirementsList()
|
||||
})
|
||||
requirementsDialogTitle.value = t('common.requirementsNotMet')
|
||||
requirementsDialogMessage.value = getRequirementsList()
|
||||
requirementsDialogOpen.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
<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>
|
||||
@@ -1,81 +1,61 @@
|
||||
<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">
|
||||
<ScrollableDialogContent
|
||||
v-if="dialogStore.type && dialogStore.itemType"
|
||||
container-class="sm:max-w-[90vw] md:max-w-3xl lg:max-w-4xl max-h-[90vh]"
|
||||
>
|
||||
<template #header>
|
||||
<DialogHeader>
|
||||
<DialogTitle class="flex items-center gap-2">
|
||||
{{ t(`buildings.${dialogStore.itemType}`) }}
|
||||
<Badge variant="outline">{{ t('common.currentLevel') }} {{ dialogStore.currentLevel || 0 }}</Badge>
|
||||
{{ itemTitle }}
|
||||
<Badge v-if="dialogStore.currentLevel !== undefined" variant="outline">
|
||||
{{ t('common.currentLevel') }} {{ dialogStore.currentLevel }}
|
||||
</Badge>
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{{ t(`buildingDescriptions.${dialogStore.itemType}`) }}
|
||||
{{ itemDescription }}
|
||||
</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>
|
||||
<ItemDetailView :type="dialogStore.type" :itemType="dialogStore.itemType" :currentLevel="dialogStore.currentLevel" />
|
||||
</ScrollableDialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'
|
||||
import { computed } from 'vue'
|
||||
import { Dialog, ScrollableDialogContent, 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'
|
||||
import ItemDetailView from './ItemDetailView.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const dialogStore = useDetailDialogStore()
|
||||
|
||||
const itemTitle = computed(() => {
|
||||
if (!dialogStore.type || !dialogStore.itemType) return ''
|
||||
const typeMap = {
|
||||
building: 'buildings',
|
||||
technology: 'technologies',
|
||||
ship: 'ships',
|
||||
defense: 'defenses'
|
||||
}
|
||||
return t(`${typeMap[dialogStore.type]}.${dialogStore.itemType}`)
|
||||
})
|
||||
|
||||
const itemDescription = computed(() => {
|
||||
if (!dialogStore.type || !dialogStore.itemType) return ''
|
||||
const typeMap = {
|
||||
building: 'buildingDescriptions',
|
||||
technology: 'technologyDescriptions',
|
||||
ship: 'shipDescriptions',
|
||||
defense: 'defenseDescriptions'
|
||||
}
|
||||
return t(`${typeMap[dialogStore.type]}.${dialogStore.itemType}`)
|
||||
})
|
||||
|
||||
const handleClose = (open: boolean) => {
|
||||
if (!open) {
|
||||
dialogStore.close()
|
||||
|
||||
101
src/components/IncomingFleetAlerts.vue
Normal file
101
src/components/IncomingFleetAlerts.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<div v-if="alerts.length > 0" class="bg-destructive/10 border-b border-destructive/20">
|
||||
<div class="px-4 sm:px-6 py-2 space-y-2">
|
||||
<div
|
||||
v-for="alert in alerts"
|
||||
:key="alert.id"
|
||||
class="flex items-center justify-between gap-3 bg-destructive/5 rounded-lg px-3 py-2 border border-destructive/20"
|
||||
>
|
||||
<!-- 警告图标和信息 -->
|
||||
<div class="flex items-center gap-2 flex-1 min-w-0">
|
||||
<AlertTriangle class="h-5 w-5 text-destructive flex-shrink-0 animate-pulse" />
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-semibold text-destructive truncate">
|
||||
<template v-if="alert.missionType === 'spy'">
|
||||
{{ t('alerts.npcSpyIncoming') }}
|
||||
</template>
|
||||
<template v-else-if="alert.missionType === 'attack'">
|
||||
{{ t('alerts.npcAttackIncoming') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ t('alerts.npcFleetIncoming') }}
|
||||
</template>
|
||||
</p>
|
||||
<p class="text-xs text-muted-foreground truncate">
|
||||
{{ alert.npcName }} → {{ alert.targetPlanetName }}
|
||||
<template v-if="alert.missionType === 'attack'">({{ alert.fleetSize }} {{ t('alerts.ships') }})</template>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 倒计时 -->
|
||||
<div class="flex items-center gap-2 flex-shrink-0">
|
||||
<div class="text-right">
|
||||
<p class="text-xs font-mono text-destructive">
|
||||
{{ formatTimeRemaining(alert.arrivalTime) }}
|
||||
</p>
|
||||
<p class="text-[10px] text-muted-foreground">
|
||||
{{ formatTime(alert.arrivalTime) }}
|
||||
</p>
|
||||
</div>
|
||||
<Button @click="markAsRead(alert)" variant="ghost" size="sm" class="h-6 w-6 p-0">
|
||||
<X class="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import type { IncomingFleetAlert } from '@/types/game'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { AlertTriangle, X } from 'lucide-vue-next'
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
|
||||
const props = defineProps<{
|
||||
alerts: IncomingFleetAlert[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'markAsRead', alert: IncomingFleetAlert): void
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// 强制更新倒计时
|
||||
const now = ref(Date.now())
|
||||
let updateInterval: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
onMounted(() => {
|
||||
updateInterval = setInterval(() => {
|
||||
now.value = Date.now()
|
||||
}, 1000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (updateInterval) clearInterval(updateInterval)
|
||||
})
|
||||
|
||||
const formatTimeRemaining = (arrivalTime: number): string => {
|
||||
const remaining = Math.max(0, arrivalTime - now.value)
|
||||
const seconds = Math.floor((remaining / 1000) % 60)
|
||||
const minutes = Math.floor((remaining / (1000 * 60)) % 60)
|
||||
const hours = Math.floor(remaining / (1000 * 60 * 60))
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`
|
||||
}
|
||||
return `${minutes}:${String(seconds).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
const formatTime = (timestamp: number): string => {
|
||||
const date = new Date(timestamp)
|
||||
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
||||
}
|
||||
|
||||
const markAsRead = (alert: IncomingFleetAlert) => {
|
||||
emit('markAsRead', alert)
|
||||
}
|
||||
</script>
|
||||
733
src/components/ItemDetailView.vue
Normal file
733
src/components/ItemDetailView.vue
Normal file
@@ -0,0 +1,733 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<!-- 建筑/科技:等级范围表格 -->
|
||||
<div v-if="type === 'building' || type === 'technology'" class="border rounded-lg overflow-hidden">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead class="w-20 text-center">{{ t(`${typeKey}.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 v-if="showDarkMatterColumn" class="text-center">{{ t('resources.darkMatter') }}</TableHead>
|
||||
<TableHead class="text-center">{{ type === 'building' ? t('buildings.buildTime') : t('research.researchTime') }}</TableHead>
|
||||
<!-- 建筑相关列 -->
|
||||
<TableHead v-if="type === 'building' && showProductionColumn" class="text-center">{{ t('buildings.production') }}</TableHead>
|
||||
<TableHead v-if="type === 'building' && showConsumptionColumn" class="text-center">{{ t('buildings.consumption') }}</TableHead>
|
||||
<TableHead v-if="type === 'building' && showCapacityColumn" class="text-center">{{ t('buildings.storageCapacity') }}</TableHead>
|
||||
<TableHead v-if="type === 'building' && showFleetStorageColumn" class="text-center">{{ t('buildings.fleetStorage') }}</TableHead>
|
||||
<TableHead v-if="type === 'building' && showBuildQueueColumn" class="text-center">{{ t('buildings.buildQueueBonus') }}</TableHead>
|
||||
<TableHead v-if="type === 'building' && showSpaceColumn" class="text-center">{{ t('buildings.spaceBonus') }}</TableHead>
|
||||
<TableHead v-if="type === 'building' && showMissileColumn" class="text-center">{{ t('buildings.missileCapacity') }}</TableHead>
|
||||
<TableHead v-if="type === 'building' && showBuildSpeedColumn" class="text-center">{{ t('buildings.buildSpeedBonus') }}</TableHead>
|
||||
<TableHead v-if="type === 'building' && showResearchSpeedColumn" class="text-center">{{ t('buildings.researchSpeedBonus') }}</TableHead>
|
||||
<!-- 科技相关列 -->
|
||||
<TableHead v-if="type === 'technology' && showAttackBonusColumn" class="text-center">{{ t('research.attackBonus') }}</TableHead>
|
||||
<TableHead v-if="type === 'technology' && showShieldBonusColumn" class="text-center">{{ t('research.shieldBonus') }}</TableHead>
|
||||
<TableHead v-if="type === 'technology' && showArmorBonusColumn" class="text-center">{{ t('research.armorBonus') }}</TableHead>
|
||||
<TableHead v-if="type === 'technology' && showSpyLevelColumn" class="text-center">{{ t('research.spyLevel') }}</TableHead>
|
||||
<TableHead v-if="type === 'technology' && showFleetStorageColumn" class="text-center">{{ t('buildings.fleetStorage') }}</TableHead>
|
||||
<TableHead v-if="type === 'technology' && showResearchQueueColumn" class="text-center">{{ t('research.researchQueueBonus') }}</TableHead>
|
||||
<TableHead v-if="type === 'technology' && showColonySlotsColumn" class="text-center">{{ t('research.colonySlots') }}</TableHead>
|
||||
<TableHead v-if="type === 'technology' && showSpaceColumn" class="text-center">{{ t('buildings.spaceBonus') }}</TableHead>
|
||||
<TableHead v-if="type === 'technology' && showSpeedBonusColumn" class="text-center">{{ t('research.speedBonus') }}</TableHead>
|
||||
<TableHead v-if="type === 'technology' && showResearchSpeedColumn" class="text-center">{{ t('buildings.researchSpeedBonus') }}</TableHead>
|
||||
<TableHead class="text-center">{{ t('player.points') }}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow v-for="level in levelRange" :key="level" :class="{ 'bg-muted/50': level === safeCurrentLevel }">
|
||||
<TableCell class="text-center font-medium">
|
||||
<Badge v-if="level === safeCurrentLevel" variant="default">{{ level }}</Badge>
|
||||
<span v-else>{{ level }}</span>
|
||||
</TableCell>
|
||||
<TableCell class="text-center text-sm">
|
||||
<NumberWithTooltip :value="getLevelData(level).cost.metal" />
|
||||
</TableCell>
|
||||
<TableCell class="text-center text-sm">
|
||||
<NumberWithTooltip :value="getLevelData(level).cost.crystal" />
|
||||
</TableCell>
|
||||
<TableCell class="text-center text-sm">
|
||||
<NumberWithTooltip :value="getLevelData(level).cost.deuterium" />
|
||||
</TableCell>
|
||||
<TableCell v-if="showDarkMatterColumn" class="text-center text-sm">
|
||||
<NumberWithTooltip :value="getLevelData(level).cost.darkMatter" />
|
||||
</TableCell>
|
||||
<TableCell class="text-center text-sm">{{ formatTime(getLevelData(level).time) }}</TableCell>
|
||||
<!-- 建筑相关数据 -->
|
||||
<TableCell v-if="type === 'building' && showProductionColumn" class="text-center text-sm">
|
||||
<span v-if="getLevelData(level).production > 0" class="text-green-600 dark:text-green-400">
|
||||
+
|
||||
<NumberWithTooltip :value="getLevelData(level).production" />
|
||||
/{{ t('resources.perHour') }}
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
</TableCell>
|
||||
<TableCell v-if="type === 'building' && showConsumptionColumn" class="text-center text-sm">
|
||||
<span v-if="getLevelData(level).consumption > 0" class="text-red-600 dark:text-red-400">
|
||||
-
|
||||
<NumberWithTooltip :value="getLevelData(level).consumption" />
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
</TableCell>
|
||||
<TableCell v-if="type === 'building' && showCapacityColumn" class="text-center text-sm">
|
||||
<span v-if="getLevelData(level).capacity > 0" class="text-blue-600 dark:text-blue-400">
|
||||
<NumberWithTooltip :value="getLevelData(level).capacity" />
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
</TableCell>
|
||||
<TableCell v-if="type === 'building' && showFleetStorageColumn" class="text-center text-sm">
|
||||
<span v-if="getLevelData(level).fleetStorage > 0" class="text-blue-600 dark:text-blue-400">
|
||||
+<NumberWithTooltip :value="getLevelData(level).fleetStorage" />
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
</TableCell>
|
||||
<TableCell v-if="type === 'building' && showBuildQueueColumn" class="text-center text-sm">
|
||||
<span class="text-purple-600 dark:text-purple-400">+1</span>
|
||||
</TableCell>
|
||||
<TableCell v-if="type === 'building' && showSpaceColumn" class="text-center text-sm">
|
||||
<span v-if="getLevelData(level).spaceBonus > 0" class="text-green-600 dark:text-green-400">
|
||||
+<NumberWithTooltip :value="getLevelData(level).spaceBonus" />
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
</TableCell>
|
||||
<TableCell v-if="type === 'building' && showMissileColumn" class="text-center text-sm">
|
||||
<span class="text-orange-600 dark:text-orange-400">+10</span>
|
||||
</TableCell>
|
||||
<TableCell v-if="type === 'building' && showBuildSpeedColumn" class="text-center text-sm">
|
||||
<span v-if="itemType === 'roboticsFactory'" class="text-cyan-600 dark:text-cyan-400">+{{ getLevelData(level).buildSpeedBonus * 100 }}%</span>
|
||||
<span v-else-if="itemType === 'naniteFactory'" class="text-cyan-600 dark:text-cyan-400">+{{ getLevelData(level).buildSpeedBonus * 100 }}%</span>
|
||||
</TableCell>
|
||||
<TableCell v-if="type === 'building' && showResearchSpeedColumn" class="text-center text-sm">
|
||||
<span class="text-indigo-600 dark:text-indigo-400">+{{ (getLevelData(level).researchSpeedBonus - 1) * 100 }}%</span>
|
||||
</TableCell>
|
||||
<!-- 科技相关数据 -->
|
||||
<TableCell v-if="type === 'technology' && showAttackBonusColumn" class="text-center text-sm">
|
||||
<span class="text-red-600 dark:text-red-400">+{{ level * 10 }}%</span>
|
||||
</TableCell>
|
||||
<TableCell v-if="type === 'technology' && showShieldBonusColumn" class="text-center text-sm">
|
||||
<span class="text-blue-600 dark:text-blue-400">+{{ level * 10 }}%</span>
|
||||
</TableCell>
|
||||
<TableCell v-if="type === 'technology' && showArmorBonusColumn" class="text-center text-sm">
|
||||
<span class="text-gray-600 dark:text-gray-400">+{{ level * 10 }}%</span>
|
||||
</TableCell>
|
||||
<TableCell v-if="type === 'technology' && showSpyLevelColumn" class="text-center text-sm">
|
||||
<span class="text-purple-600 dark:text-purple-400">+{{ level }}</span>
|
||||
</TableCell>
|
||||
<TableCell v-if="type === 'technology' && showFleetStorageColumn" class="text-center text-sm">
|
||||
<span class="text-blue-600 dark:text-blue-400">+<NumberWithTooltip :value="level * 500" /></span>
|
||||
</TableCell>
|
||||
<TableCell v-if="type === 'technology' && showResearchQueueColumn" class="text-center text-sm">
|
||||
<span class="text-purple-600 dark:text-purple-400">+1</span>
|
||||
</TableCell>
|
||||
<TableCell v-if="type === 'technology' && showColonySlotsColumn" class="text-center text-sm">
|
||||
<span class="text-green-600 dark:text-green-400">+1</span>
|
||||
</TableCell>
|
||||
<TableCell v-if="type === 'technology' && showSpaceColumn" class="text-center text-sm">
|
||||
<span class="text-green-600 dark:text-green-400">+5 {{ t('research.forAllPlanets') }}</span>
|
||||
</TableCell>
|
||||
<TableCell v-if="type === 'technology' && showSpeedBonusColumn" class="text-center text-sm">
|
||||
<span class="text-yellow-600 dark:text-yellow-400">+{{ level * 10 }}%</span>
|
||||
</TableCell>
|
||||
<TableCell v-if="type === 'technology' && showResearchSpeedColumn" class="text-center text-sm">
|
||||
<span class="text-indigo-600 dark:text-indigo-400">+{{ getLevelData(level).researchSpeedBonus * 100 }}%</span>
|
||||
</TableCell>
|
||||
<TableCell class="text-center text-sm">
|
||||
<span class="text-primary font-medium">
|
||||
+
|
||||
<NumberWithTooltip :value="getLevelData(level).points" />
|
||||
</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<!-- 建筑/科技:累积统计 -->
|
||||
<div v-if="type === 'building' || type === 'technology'" class="grid grid-cols-2 gap-4">
|
||||
<Card>
|
||||
<CardHeader class="pb-3">
|
||||
<CardTitle class="text-sm">{{ t(`${typeKey}.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"><NumberWithTooltip :value="totalStats.metal" /></span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('resources.crystal') }}:</span>
|
||||
<span class="font-medium"><NumberWithTooltip :value="totalStats.crystal" /></span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('resources.deuterium') }}:</span>
|
||||
<span class="font-medium"><NumberWithTooltip :value="totalStats.deuterium" /></span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader class="pb-3">
|
||||
<CardTitle class="text-sm">{{ t(`${typeKey}.totalPoints`) }}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="text-3xl font-bold text-primary">
|
||||
<NumberWithTooltip :value="totalStats.points" />
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground mt-1">
|
||||
{{ t(`${typeKey}.levelRange`) }}: {{ Math.max(0, safeCurrentLevel - 10) }} - {{ safeCurrentLevel + 10 }}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- 舰船/防御:基础属性 -->
|
||||
<div v-if="type === 'ship' || type === 'defense'" 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(`${typeKey}.attack`) }}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="text-2xl font-bold">
|
||||
<NumberWithTooltip :value="combatUnitConfig?.attack || 0" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader class="pb-3">
|
||||
<CardTitle class="text-sm flex items-center gap-2">
|
||||
<Shield class="h-4 w-4" />
|
||||
{{ t(`${typeKey}.shield`) }}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="text-2xl font-bold">
|
||||
<NumberWithTooltip :value="combatUnitConfig?.shield || 0" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader class="pb-3">
|
||||
<CardTitle class="text-sm flex items-center gap-2">
|
||||
<ShieldCheck class="h-4 w-4" />
|
||||
{{ t(`${typeKey}.armor`) }}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="text-2xl font-bold">
|
||||
<NumberWithTooltip :value="combatUnitConfig?.armor || 0" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 仅舰船显示 -->
|
||||
<Card v-if="type === 'ship'">
|
||||
<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">
|
||||
<NumberWithTooltip :value="shipConfig?.speed || 0" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card v-if="type === 'ship'">
|
||||
<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">
|
||||
<NumberWithTooltip :value="shipConfig?.cargoCapacity || 0" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card v-if="type === 'ship'">
|
||||
<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">
|
||||
<NumberWithTooltip :value="shipConfig?.fuelConsumption || 0" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- 舰船/防御:建造成本和时间 -->
|
||||
<div v-if="type === 'ship' || type === 'defense'" class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="text-sm">{{ t(`${typeKey}.buildCost`) }}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-2">
|
||||
<div v-for="resourceType in costResourceTypes" :key="resourceType.key" v-show="unitCost[resourceType.key] > 0" class="flex items-center justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t(`resources.${resourceType.key}`) }}:</span>
|
||||
<span class="font-medium"><NumberWithTooltip :value="unitCost[resourceType.key]" /></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"><NumberWithTooltip :value="pointsPerUnit" /></span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="text-sm">{{ t(`${typeKey}.buildTime`) }}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="text-3xl font-bold">{{ formatTime(unitBuildTime) }}</div>
|
||||
<p class="text-xs text-muted-foreground mt-2">{{ t(`${typeKey}.perUnit`) }}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- 舰船/防御:批量建造计算器 -->
|
||||
<Card v-if="type === 'ship' || type === 'defense'">
|
||||
<CardHeader>
|
||||
<CardTitle class="text-sm">{{ t(`${typeKey}.batchCalculator`) }}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<div class="flex items-center gap-4">
|
||||
<Label class="w-20">{{ t(`${typeKey}.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(`${typeKey}.totalCost`) }}:</p>
|
||||
<div class="space-y-1 text-sm">
|
||||
<div v-for="resourceType in costResourceTypes" :key="resourceType.key" class="flex justify-between">
|
||||
<span>{{ t(`resources.${resourceType.key}`) }}:</span>
|
||||
<span class="font-medium"><NumberWithTooltip :value="batchCost[resourceType.key]" /></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<p class="text-sm text-muted-foreground">{{ t(`${typeKey}.totalTime`) }}:</p>
|
||||
<div class="text-xl font-bold">{{ formatTime(unitBuildTime * quantity) }}</div>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ t('player.points') }}: +
|
||||
<NumberWithTooltip :value="batchPoints" />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import type { BuildingType, TechnologyType, ShipType, DefenseType } 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 { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import NumberWithTooltip from '@/components/NumberWithTooltip.vue'
|
||||
import { Sword, Shield, ShieldCheck, Zap, Package, Fuel } from 'lucide-vue-next'
|
||||
import * as buildingLogic from '@/logic/buildingLogic'
|
||||
import * as researchLogic from '@/logic/researchLogic'
|
||||
import * as pointsLogic from '@/logic/pointsLogic'
|
||||
import * as officerLogic from '@/logic/officerLogic'
|
||||
import * as shipLogic from '@/logic/shipLogic'
|
||||
import { SHIPS, DEFENSES } from '@/config/gameConfig'
|
||||
import { formatTime } from '@/utils/format'
|
||||
|
||||
const { t } = useI18n()
|
||||
const gameStore = useGameStore()
|
||||
|
||||
const props = defineProps<{
|
||||
type: 'building' | 'technology' | 'ship' | 'defense'
|
||||
itemType: BuildingType | TechnologyType | ShipType | DefenseType
|
||||
currentLevel?: number
|
||||
}>()
|
||||
|
||||
const quantity = ref(1)
|
||||
|
||||
// 资源类型配置(用于成本显示)
|
||||
const costResourceTypes = [{ key: 'metal' as const }, { key: 'crystal' as const }, { key: 'deuterium' as const }]
|
||||
|
||||
// 获取当前星球
|
||||
const currentPlanet = computed(() => gameStore.currentPlanet)
|
||||
|
||||
// 计算当前加成
|
||||
const activeBonuses = computed(() => {
|
||||
return officerLogic.calculateActiveBonuses(gameStore.player.officers, gameStore.gameTime)
|
||||
})
|
||||
|
||||
// 获取工厂等级(用于建造时间计算)
|
||||
const roboticsFactoryLevel = computed(() => {
|
||||
if (!currentPlanet.value) return 0
|
||||
return currentPlanet.value.buildings['roboticsFactory'] || 0
|
||||
})
|
||||
|
||||
const naniteFactoryLevel = computed(() => {
|
||||
if (!currentPlanet.value) return 0
|
||||
return currentPlanet.value.buildings['naniteFactory'] || 0
|
||||
})
|
||||
|
||||
// 获取研究所等级(用于研究时间计算)
|
||||
const researchLabLevel = computed(() => {
|
||||
if (!currentPlanet.value) return 0
|
||||
return currentPlanet.value.buildings['researchLab'] || 0
|
||||
})
|
||||
|
||||
// 翻译键(转换为复数形式)
|
||||
const typeKey = computed(() => {
|
||||
const typeMap = {
|
||||
building: 'buildings',
|
||||
technology: 'research',
|
||||
ship: 'shipyard',
|
||||
defense: 'defense'
|
||||
} as const
|
||||
return typeMap[props.type]
|
||||
})
|
||||
|
||||
// 控制建筑列显示
|
||||
const showDarkMatterColumn = computed(() => {
|
||||
if (props.type === 'building') {
|
||||
const buildingType = props.itemType as BuildingType
|
||||
return buildingType === 'darkMatterCollector'
|
||||
} else if (props.type === 'technology') {
|
||||
const techType = props.itemType as TechnologyType
|
||||
return techType === 'gravitonTechnology'
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
const showProductionColumn = computed(() => {
|
||||
if (props.type !== 'building') return false
|
||||
const buildingType = props.itemType as BuildingType
|
||||
return ['metalMine', 'crystalMine', 'deuteriumSynthesizer', 'solarPlant', 'fusionReactor', 'darkMatterCollector'].includes(buildingType)
|
||||
})
|
||||
|
||||
const showConsumptionColumn = computed(() => {
|
||||
if (props.type !== 'building') return false
|
||||
const buildingType = props.itemType as BuildingType
|
||||
return ['metalMine', 'crystalMine', 'deuteriumSynthesizer'].includes(buildingType)
|
||||
})
|
||||
|
||||
const showCapacityColumn = computed(() => {
|
||||
if (props.type !== 'building') return false
|
||||
const buildingType = props.itemType as BuildingType
|
||||
return ['metalStorage', 'crystalStorage', 'deuteriumTank', 'darkMatterCollector', 'darkMatterTank'].includes(buildingType)
|
||||
})
|
||||
|
||||
const showFleetStorageColumn = computed(() => {
|
||||
if (props.type === 'building') {
|
||||
const buildingType = props.itemType as BuildingType
|
||||
return buildingType === 'shipyard'
|
||||
} else if (props.type === 'technology') {
|
||||
const techType = props.itemType as TechnologyType
|
||||
return techType === 'computerTechnology'
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
const showBuildQueueColumn = computed(() => {
|
||||
if (props.type !== 'building') return false
|
||||
const buildingType = props.itemType as BuildingType
|
||||
return buildingType === 'naniteFactory'
|
||||
})
|
||||
|
||||
const showSpaceColumn = computed(() => {
|
||||
if (props.type === 'building') {
|
||||
const buildingType = props.itemType as BuildingType
|
||||
return ['terraformer', 'lunarBase'].includes(buildingType)
|
||||
} else if (props.type === 'technology') {
|
||||
const techType = props.itemType as TechnologyType
|
||||
return techType === 'terraformingTechnology'
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
const showMissileColumn = computed(() => {
|
||||
if (props.type !== 'building') return false
|
||||
const buildingType = props.itemType as BuildingType
|
||||
return buildingType === 'missileSilo'
|
||||
})
|
||||
|
||||
const showBuildSpeedColumn = computed(() => {
|
||||
if (props.type !== 'building') return false
|
||||
const buildingType = props.itemType as BuildingType
|
||||
return ['roboticsFactory', 'naniteFactory'].includes(buildingType)
|
||||
})
|
||||
|
||||
const showResearchSpeedColumn = computed(() => {
|
||||
if (props.type === 'building') {
|
||||
const buildingType = props.itemType as BuildingType
|
||||
return buildingType === 'researchLab'
|
||||
} else if (props.type === 'technology') {
|
||||
const techType = props.itemType as TechnologyType
|
||||
return techType === 'energyTechnology'
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
// 控制科技列显示
|
||||
const showAttackBonusColumn = computed(() => {
|
||||
if (props.type !== 'technology') return false
|
||||
const techType = props.itemType as TechnologyType
|
||||
return techType === 'weaponsTechnology'
|
||||
})
|
||||
|
||||
const showShieldBonusColumn = computed(() => {
|
||||
if (props.type !== 'technology') return false
|
||||
const techType = props.itemType as TechnologyType
|
||||
return techType === 'shieldingTechnology'
|
||||
})
|
||||
|
||||
const showArmorBonusColumn = computed(() => {
|
||||
if (props.type !== 'technology') return false
|
||||
const techType = props.itemType as TechnologyType
|
||||
return techType === 'armourTechnology'
|
||||
})
|
||||
|
||||
const showSpyLevelColumn = computed(() => {
|
||||
if (props.type !== 'technology') return false
|
||||
const techType = props.itemType as TechnologyType
|
||||
return techType === 'espionageTechnology'
|
||||
})
|
||||
|
||||
const showResearchQueueColumn = computed(() => {
|
||||
if (props.type !== 'technology') return false
|
||||
const techType = props.itemType as TechnologyType
|
||||
return techType === 'computerTechnology'
|
||||
})
|
||||
|
||||
const showColonySlotsColumn = computed(() => {
|
||||
if (props.type !== 'technology') return false
|
||||
const techType = props.itemType as TechnologyType
|
||||
return techType === 'astrophysics'
|
||||
})
|
||||
|
||||
const showSpeedBonusColumn = computed(() => {
|
||||
if (props.type !== 'technology') return false
|
||||
const techType = props.itemType as TechnologyType
|
||||
return ['combustionDrive', 'impulseDrive', 'hyperspaceDrive'].includes(techType)
|
||||
})
|
||||
|
||||
// 安全的当前等级(防止undefined)
|
||||
const safeCurrentLevel = computed(() => props.currentLevel ?? 0)
|
||||
|
||||
// 类型安全:战斗单位配置(舰船/防御)
|
||||
const combatUnitConfig = computed(() => {
|
||||
if (props.type === 'ship') return SHIPS[props.itemType as ShipType]
|
||||
if (props.type === 'defense') return DEFENSES[props.itemType as DefenseType]
|
||||
return null
|
||||
})
|
||||
|
||||
// 类型安全:舰船配置
|
||||
const shipConfig = computed(() => {
|
||||
if (props.type === 'ship') return SHIPS[props.itemType as ShipType]
|
||||
return null
|
||||
})
|
||||
|
||||
// 类型安全:单位成本(处理cost vs baseCost差异)
|
||||
const unitCost = computed(() => {
|
||||
if (props.type === 'ship') return SHIPS[props.itemType as ShipType].cost
|
||||
if (props.type === 'defense') return DEFENSES[props.itemType as DefenseType].cost
|
||||
return { metal: 0, crystal: 0, deuterium: 0 }
|
||||
})
|
||||
|
||||
// 类型安全:单位建造时间(处理buildTime vs baseTime差异,应用加成)
|
||||
const unitBuildTime = computed(() => {
|
||||
if (props.type === 'ship') {
|
||||
return shipLogic.calculateShipBuildTime(
|
||||
props.itemType as ShipType,
|
||||
1, // 单个单位
|
||||
activeBonuses.value.buildingSpeedBonus,
|
||||
roboticsFactoryLevel.value,
|
||||
naniteFactoryLevel.value
|
||||
)
|
||||
}
|
||||
if (props.type === 'defense') {
|
||||
return shipLogic.calculateDefenseBuildTime(
|
||||
props.itemType as DefenseType,
|
||||
1, // 单个单位
|
||||
activeBonuses.value.buildingSpeedBonus,
|
||||
roboticsFactoryLevel.value,
|
||||
naniteFactoryLevel.value
|
||||
)
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
// 建筑/科技:等级范围
|
||||
const levelRange = computed(() => {
|
||||
if (props.type !== 'building' && props.type !== 'technology') return []
|
||||
const current = props.currentLevel || 0
|
||||
const levels = []
|
||||
for (let i = current; i <= current + 10; i++) {
|
||||
levels.push(i)
|
||||
}
|
||||
return levels
|
||||
})
|
||||
|
||||
// 建筑/科技:获取某个等级的数据
|
||||
const getLevelData = (level: number) => {
|
||||
if (level === 0) {
|
||||
return {
|
||||
cost: { metal: 0, crystal: 0, deuterium: 0, darkMatter: 0 },
|
||||
time: 0,
|
||||
production: 0,
|
||||
consumption: 0,
|
||||
points: 0,
|
||||
capacity: 0,
|
||||
fleetStorage: 0,
|
||||
spaceBonus: 0,
|
||||
buildSpeedBonus: 0,
|
||||
researchSpeedBonus: 0
|
||||
}
|
||||
}
|
||||
|
||||
if (props.type === 'building') {
|
||||
const buildingType = props.itemType as BuildingType
|
||||
const cost = buildingLogic.calculateBuildingCost(buildingType, level)
|
||||
|
||||
// 使用实际的工厂等级和加成计算建造时间
|
||||
const time = buildingLogic.calculateBuildingTime(
|
||||
buildingType,
|
||||
level,
|
||||
activeBonuses.value.buildingSpeedBonus,
|
||||
roboticsFactoryLevel.value,
|
||||
naniteFactoryLevel.value
|
||||
)
|
||||
|
||||
let production = 0
|
||||
let consumption = 0
|
||||
let capacity = 0
|
||||
let fleetStorage = 0
|
||||
let spaceBonus = 0
|
||||
let buildSpeedBonus = 0
|
||||
let researchSpeedBonus = 0
|
||||
|
||||
// 应用资源产量加成
|
||||
const resourceBonus = 1 + (activeBonuses.value.resourceProductionBonus || 0) / 100
|
||||
const energyBonus = 1 + (activeBonuses.value.energyProductionBonus || 0) / 100
|
||||
const storageBonus = 1 + (activeBonuses.value.storageCapacityBonus || 0) / 100
|
||||
const baseCapacity = 10000
|
||||
|
||||
if (buildingType === 'metalMine') {
|
||||
production = Math.floor(1500 * level * Math.pow(1.5, level) * resourceBonus)
|
||||
consumption = Math.floor(10 * level * Math.pow(1.1, level))
|
||||
} else if (buildingType === 'crystalMine') {
|
||||
production = Math.floor(1000 * level * Math.pow(1.5, level) * resourceBonus)
|
||||
consumption = Math.floor(10 * level * Math.pow(1.1, level))
|
||||
} else if (buildingType === 'deuteriumSynthesizer') {
|
||||
production = Math.floor(500 * level * Math.pow(1.5, level) * resourceBonus)
|
||||
consumption = Math.floor(10 * level * Math.pow(1.1, level))
|
||||
} else if (buildingType === 'solarPlant') {
|
||||
production = Math.floor(50 * level * Math.pow(1.1, level) * energyBonus)
|
||||
} else if (buildingType === 'metalStorage') {
|
||||
capacity = Math.floor(baseCapacity * Math.pow(2, level) * storageBonus)
|
||||
} else if (buildingType === 'crystalStorage') {
|
||||
capacity = Math.floor(baseCapacity * Math.pow(2, level) * storageBonus)
|
||||
} else if (buildingType === 'deuteriumTank') {
|
||||
capacity = Math.floor(baseCapacity * Math.pow(2, level) * storageBonus)
|
||||
} else if (buildingType === 'darkMatterCollector') {
|
||||
capacity = 1000 + level * 100
|
||||
production = Math.floor(25 * level * Math.pow(1.5, level))
|
||||
} else if (buildingType === 'darkMatterTank') {
|
||||
const darkMatterBaseCapacity = 1000
|
||||
capacity = Math.floor(darkMatterBaseCapacity * Math.pow(2, level) * storageBonus)
|
||||
} else if (buildingType === 'fusionReactor') {
|
||||
production = Math.floor(150 * level * Math.pow(1.15, level))
|
||||
} else if (buildingType === 'shipyard') {
|
||||
fleetStorage = 1000 * level
|
||||
} else if (buildingType === 'terraformer') {
|
||||
spaceBonus = 5
|
||||
} else if (buildingType === 'lunarBase') {
|
||||
spaceBonus = 5
|
||||
} else if (buildingType === 'roboticsFactory') {
|
||||
buildSpeedBonus = level
|
||||
} else if (buildingType === 'naniteFactory') {
|
||||
buildSpeedBonus = level * 2
|
||||
} else if (buildingType === 'researchLab') {
|
||||
researchSpeedBonus = level
|
||||
}
|
||||
|
||||
const points = pointsLogic.calculateBuildingPoints(buildingType, level - 1, level)
|
||||
return { cost, time, production, consumption, points, capacity, fleetStorage, spaceBonus, buildSpeedBonus, researchSpeedBonus }
|
||||
} else {
|
||||
const techType = props.itemType as TechnologyType
|
||||
const cost = researchLogic.calculateTechnologyCost(techType, level)
|
||||
|
||||
// 使用实际的研究所等级和加成计算研究时间
|
||||
const time = researchLogic.calculateTechnologyTime(
|
||||
techType,
|
||||
level - 1,
|
||||
activeBonuses.value.researchSpeedBonus,
|
||||
researchLabLevel.value
|
||||
)
|
||||
|
||||
let researchSpeedBonus = 0
|
||||
if (techType === 'energyTechnology') {
|
||||
researchSpeedBonus = level
|
||||
}
|
||||
|
||||
const points = pointsLogic.calculateTechnologyPoints(techType, level - 1, level)
|
||||
return { cost, time, production: 0, consumption: 0, points, capacity: 0, fleetStorage: 0, spaceBonus: 0, buildSpeedBonus: 0, researchSpeedBonus }
|
||||
}
|
||||
}
|
||||
|
||||
// 建筑/科技:累积统计
|
||||
const totalStats = computed(() => {
|
||||
if (props.type !== 'building' && props.type !== 'technology') {
|
||||
return { metal: 0, crystal: 0, deuterium: 0, points: 0 }
|
||||
}
|
||||
|
||||
let metal = 0,
|
||||
crystal = 0,
|
||||
deuterium = 0,
|
||||
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 pointsPerUnit = computed(() => {
|
||||
if (props.type === 'ship') return pointsLogic.calculateShipPoints(props.itemType as ShipType, 1)
|
||||
if (props.type === 'defense') return pointsLogic.calculateDefensePoints(props.itemType as DefenseType, 1)
|
||||
return 0
|
||||
})
|
||||
|
||||
// 舰船/防御:批量成本
|
||||
const batchCost = computed(() => ({
|
||||
metal: unitCost.value.metal * quantity.value,
|
||||
crystal: unitCost.value.crystal * quantity.value,
|
||||
deuterium: unitCost.value.deuterium * quantity.value
|
||||
}))
|
||||
|
||||
// 舰船/防御:批量积分
|
||||
const batchPoints = computed(() => {
|
||||
if (props.type === 'ship') return pointsLogic.calculateShipPoints(props.itemType as ShipType, quantity.value)
|
||||
if (props.type === 'defense') return pointsLogic.calculateDefensePoints(props.itemType as DefenseType, quantity.value)
|
||||
return 0
|
||||
})
|
||||
</script>
|
||||
232
src/components/NpcRelationCard.vue
Normal file
232
src/components/NpcRelationCard.vue
Normal file
@@ -0,0 +1,232 @@
|
||||
<template>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<CardTitle class="flex items-center gap-2">
|
||||
{{ npc.name }}
|
||||
<Badge :variant="statusBadgeVariant">
|
||||
{{ statusText }}
|
||||
</Badge>
|
||||
</CardTitle>
|
||||
<CardDescription class="mt-1">
|
||||
{{ npc.planets.length }} {{ t('diplomacy.planets') }}
|
||||
<span v-if="npc.allies && npc.allies.length > 0" class="ml-2">· {{ npc.allies.length }} {{ t('diplomacy.allies') }}</span>
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<!-- 好感度进度条 -->
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('diplomacy.reputation') }}</span>
|
||||
<span class="font-semibold" :class="reputationColor">{{ reputation > 0 ? '+' : '' }}{{ reputation }}</span>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<!-- 背景进度条 -->
|
||||
<div class="h-2 bg-muted rounded-full overflow-hidden">
|
||||
<!-- 负值部分(左侧,红色) -->
|
||||
<div
|
||||
v-if="reputation < 0"
|
||||
class="h-full bg-red-500 dark:bg-red-600 absolute right-1/2"
|
||||
:style="{ width: `${Math.abs(reputation) / 2}%` }"
|
||||
/>
|
||||
<!-- 正值部分(右侧,绿色) -->
|
||||
<div
|
||||
v-if="reputation > 0"
|
||||
class="h-full bg-green-500 dark:bg-green-600 absolute left-1/2"
|
||||
:style="{ width: `${reputation / 2}%` }"
|
||||
/>
|
||||
</div>
|
||||
<!-- 中心线 -->
|
||||
<div class="absolute left-1/2 top-0 bottom-0 w-px bg-border" />
|
||||
</div>
|
||||
<div class="flex justify-between text-xs text-muted-foreground">
|
||||
<span>-100</span>
|
||||
<span>0</span>
|
||||
<span>+100</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 盟友信息 -->
|
||||
<div v-if="npc.allies && npc.allies.length > 0" class="pt-2 border-t">
|
||||
<p class="text-sm text-muted-foreground mb-2">{{ t('diplomacy.alliedWith') }}:</p>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<Badge v-for="allyId in npc.allies.slice(0, 3)" :key="allyId" variant="outline" class="text-xs">
|
||||
{{ getAllyName(allyId) }}
|
||||
</Badge>
|
||||
<Badge v-if="npc.allies.length > 3" variant="outline" class="text-xs">
|
||||
+{{ npc.allies.length - 3 }} {{ t('diplomacy.more') }}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex gap-2 pt-2">
|
||||
<Button size="sm" variant="outline" class="flex-1" @click="handleGiftResources">
|
||||
<Gift class="h-4 w-4 mr-2" />
|
||||
{{ t('diplomacy.actions.gift') }}
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" class="flex-1" @click="handleViewPlanets">
|
||||
<Globe class="h-4 w-4 mr-2" />
|
||||
{{ t('diplomacy.actions.viewPlanets') }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- 最近活动 -->
|
||||
<div v-if="recentEvent" class="pt-2 border-t">
|
||||
<p class="text-xs text-muted-foreground mb-1">{{ t('diplomacy.lastEvent') }}:</p>
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<component :is="getEventIcon(recentEvent.reason)" class="h-3 w-3" />
|
||||
<span>{{ getEventText(recentEvent.reason) }}</span>
|
||||
<span class="text-muted-foreground">{{ formatTime(Date.now() - recentEvent.timestamp) }} {{ t('diplomacy.ago') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useNPCStore } from '@/stores/npcStore'
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Gift, Globe, Sword, Eye, Trash2 } from 'lucide-vue-next'
|
||||
import { RelationStatus, DiplomaticEventType } from '@/types/game'
|
||||
import type { DiplomaticRelation, NPC } from '@/types/game'
|
||||
import { formatTime } from '@/utils/format'
|
||||
|
||||
const props = defineProps<{
|
||||
npc: NPC
|
||||
relation?: DiplomaticRelation
|
||||
}>()
|
||||
|
||||
const router = useRouter()
|
||||
const npcStore = useNPCStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
// 好感度值
|
||||
const reputation = computed(() => props.relation?.reputation || 0)
|
||||
|
||||
// 关系状态
|
||||
const status = computed(() => props.relation?.status || RelationStatus.Neutral)
|
||||
|
||||
// 关系状态文本
|
||||
const statusText = computed(() => {
|
||||
switch (status.value) {
|
||||
case RelationStatus.Friendly:
|
||||
return t('diplomacy.status.friendly')
|
||||
case RelationStatus.Hostile:
|
||||
return t('diplomacy.status.hostile')
|
||||
default:
|
||||
return t('diplomacy.status.neutral')
|
||||
}
|
||||
})
|
||||
|
||||
// 关系状态Badge样式
|
||||
const statusBadgeVariant = computed(() => {
|
||||
switch (status.value) {
|
||||
case RelationStatus.Friendly:
|
||||
return 'default'
|
||||
case RelationStatus.Hostile:
|
||||
return 'destructive'
|
||||
default:
|
||||
return 'secondary'
|
||||
}
|
||||
})
|
||||
|
||||
// 好感度颜色
|
||||
const reputationColor = computed(() => {
|
||||
if (reputation.value >= 20) return 'text-green-600 dark:text-green-400'
|
||||
if (reputation.value <= -20) return 'text-red-600 dark:text-red-400'
|
||||
return 'text-muted-foreground'
|
||||
})
|
||||
|
||||
// 最近的外交事件
|
||||
const recentEvent = computed(() => {
|
||||
if (!props.relation?.history || props.relation.history.length === 0) return null
|
||||
return props.relation.history[props.relation.history.length - 1]
|
||||
})
|
||||
|
||||
// 获取盟友名称
|
||||
const getAllyName = (allyId: string) => {
|
||||
const ally = npcStore.npcs.find(n => n.id === allyId)
|
||||
return ally?.name || allyId.substring(0, 8)
|
||||
}
|
||||
|
||||
// 获取事件图标
|
||||
const getEventIcon = (eventType: string) => {
|
||||
switch (eventType) {
|
||||
case DiplomaticEventType.GiftResources:
|
||||
return Gift
|
||||
case DiplomaticEventType.Attack:
|
||||
case DiplomaticEventType.AllyAttacked:
|
||||
return Sword
|
||||
case DiplomaticEventType.Spy:
|
||||
return Eye
|
||||
case DiplomaticEventType.StealDebris:
|
||||
return Trash2
|
||||
default:
|
||||
return Gift
|
||||
}
|
||||
}
|
||||
|
||||
// 获取事件文本
|
||||
const getEventText = (eventType: string) => {
|
||||
switch (eventType) {
|
||||
case DiplomaticEventType.GiftResources:
|
||||
return t('diplomacy.events.gift')
|
||||
case DiplomaticEventType.Attack:
|
||||
return t('diplomacy.events.attack')
|
||||
case DiplomaticEventType.AllyAttacked:
|
||||
return t('diplomacy.events.allyAttacked')
|
||||
case DiplomaticEventType.Spy:
|
||||
return t('diplomacy.events.spy')
|
||||
case DiplomaticEventType.StealDebris:
|
||||
return t('diplomacy.events.stealDebris')
|
||||
default:
|
||||
return eventType
|
||||
}
|
||||
}
|
||||
|
||||
// 赠送资源
|
||||
const handleGiftResources = () => {
|
||||
// 跳转到舰队页面,自动选择第一个NPC星球
|
||||
if (props.npc.planets.length > 0) {
|
||||
const targetPlanet = props.npc.planets[0]
|
||||
if (!targetPlanet) return
|
||||
|
||||
router.push({
|
||||
path: '/fleet',
|
||||
query: {
|
||||
galaxy: targetPlanet.position.galaxy,
|
||||
system: targetPlanet.position.system,
|
||||
position: targetPlanet.position.position,
|
||||
gift: '1'
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 查看星球
|
||||
const handleViewPlanets = () => {
|
||||
// 跳转到星系视图,定位到第一个NPC星球,并传递NPC ID用于高亮
|
||||
if (props.npc.planets.length > 0) {
|
||||
const targetPlanet = props.npc.planets[0]
|
||||
if (!targetPlanet) return
|
||||
|
||||
router.push({
|
||||
path: '/galaxy',
|
||||
query: {
|
||||
galaxy: targetPlanet.position.galaxy,
|
||||
system: targetPlanet.position.system,
|
||||
highlightNpc: props.npc.id
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<Popover>
|
||||
<PopoverTrigger as-child>
|
||||
<span class="cursor-pointer underline decoration-dotted underline-offset-4 touch-manipulation">{{ abbreviatedValue }}</span>
|
||||
<span class="cursor-pointer underline decoration-dotted underline-offset-4 touch-manipulation">{{ formatNumber(value, 1) }}</span>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-auto p-2" side="top" align="center">
|
||||
<p class="font-mono text-sm">{{ formattedValue }}</p>
|
||||
@@ -12,6 +12,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
|
||||
const props = defineProps<{
|
||||
value: number
|
||||
@@ -21,30 +22,4 @@
|
||||
const formattedValue = computed(() => {
|
||||
return props.value.toLocaleString()
|
||||
})
|
||||
|
||||
// 缩写格式的数字
|
||||
const abbreviatedValue = computed(() => {
|
||||
const num = props.value
|
||||
|
||||
// 小于1000直接显示
|
||||
if (num < 1000) {
|
||||
return num.toString()
|
||||
}
|
||||
|
||||
// 1000 - 999,999: 使用 K (千)
|
||||
if (num < 1000000) {
|
||||
const k = num / 1000
|
||||
return k % 1 === 0 ? `${k}K` : `${k.toFixed(1)}K`
|
||||
}
|
||||
|
||||
// 1,000,000 - 999,999,999: 使用 M (百万)
|
||||
if (num < 1000000000) {
|
||||
const m = num / 1000000
|
||||
return m % 1 === 0 ? `${m}M` : `${m.toFixed(1)}M`
|
||||
}
|
||||
|
||||
// 1,000,000,000+: 使用 B (十亿)
|
||||
const b = num / 1000000000
|
||||
return b % 1 === 0 ? `${b}B` : `${b.toFixed(1)}B`
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
<template>
|
||||
<Dialog v-model:open="isOpen">
|
||||
<DialogContent class="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle class="flex items-center gap-2">
|
||||
<Eye class="h-5 w-5" />
|
||||
{{ t('messagesView.spyReport') }}
|
||||
</DialogTitle>
|
||||
<DialogDescription v-if="report">
|
||||
{{ formatDate(report.timestamp) }}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<ScrollableDialogContent container-class="sm:max-w-2xl max-h-[90vh]">
|
||||
<template #header>
|
||||
<DialogHeader>
|
||||
<DialogTitle class="flex items-center gap-2">
|
||||
<Eye class="h-5 w-5" />
|
||||
{{ t('messagesView.spyReport') }}
|
||||
</DialogTitle>
|
||||
<DialogDescription v-if="report">
|
||||
{{ formatDate(report.timestamp) }}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
</template>
|
||||
|
||||
<div v-if="report" class="space-y-4">
|
||||
<!-- 目标星球信息 -->
|
||||
<div class="p-3 bg-muted rounded-lg">
|
||||
<p class="text-sm font-medium mb-2">{{ t('messagesView.targetPlanet') }}</p>
|
||||
<p v-if="targetPlanet" class="text-xs text-muted-foreground">
|
||||
{{ targetPlanet.name }} [{{ targetPlanet.position.galaxy }}:{{ targetPlanet.position.system }}:{{
|
||||
targetPlanet.position.position
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ report.targetPlanetName }} [{{ report.targetPosition.galaxy }}:{{ report.targetPosition.system }}:{{
|
||||
report.targetPosition.position
|
||||
}}]
|
||||
</p>
|
||||
<p v-else class="text-xs text-muted-foreground">{{ report.targetPlanetId }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 资源 -->
|
||||
@@ -80,17 +82,15 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</ScrollableDialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useUniverseStore } from '@/stores/universeStore'
|
||||
import { ref, watch } from 'vue'
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
import { useGameConfig } from '@/composables/useGameConfig'
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Dialog, ScrollableDialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import ResourceIcon from '@/components/ResourceIcon.vue'
|
||||
import { formatNumber, formatDate } from '@/utils/format'
|
||||
import { Eye } from 'lucide-vue-next'
|
||||
@@ -105,23 +105,11 @@
|
||||
(e: 'update:open', value: boolean): void
|
||||
}>()
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const universeStore = useUniverseStore()
|
||||
const { t } = useI18n()
|
||||
const { SHIPS, DEFENSES, BUILDINGS } = useGameConfig()
|
||||
|
||||
const isOpen = ref(props.open)
|
||||
|
||||
// 获取目标星球信息
|
||||
const targetPlanet = computed(() => {
|
||||
if (!props.report) return null
|
||||
// 先从玩家星球中查找
|
||||
const playerPlanet = gameStore.player.planets.find(p => p.id === props.report!.targetPlanetId)
|
||||
if (playerPlanet) return playerPlanet
|
||||
// 再从宇宙星球地图中查找
|
||||
return Object.values(universeStore.planets).find(p => p.id === props.report!.targetPlanetId)
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.open,
|
||||
newValue => {
|
||||
|
||||
@@ -1,202 +0,0 @@
|
||||
<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">
|
||||
<NumberWithTooltip :value="getLevelData(level).cost.metal" />
|
||||
</TableCell>
|
||||
<TableCell class="text-center text-sm">
|
||||
<NumberWithTooltip :value="getLevelData(level).cost.crystal" />
|
||||
</TableCell>
|
||||
<TableCell class="text-center text-sm">
|
||||
<NumberWithTooltip :value="getLevelData(level).cost.deuterium" />
|
||||
</TableCell>
|
||||
<TableCell class="text-center text-sm">{{ formatTime(getLevelData(level).buildTime) }}</TableCell>
|
||||
<TableCell class="text-center text-sm">
|
||||
<span v-if="getLevelData(level).production > 0" class="text-green-600 dark:text-green-400">
|
||||
+
|
||||
<NumberWithTooltip :value="getLevelData(level).production" />
|
||||
/{{ t('resources.perHour') }}
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
</TableCell>
|
||||
<TableCell class="text-center text-sm">
|
||||
<span v-if="getLevelData(level).consumption > 0" class="text-red-600 dark:text-red-400">
|
||||
-
|
||||
<NumberWithTooltip :value="getLevelData(level).consumption" />
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
</TableCell>
|
||||
<TableCell class="text-center text-sm">
|
||||
<span class="text-primary font-medium">
|
||||
+
|
||||
<NumberWithTooltip :value="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">
|
||||
<NumberWithTooltip :value="totalStats.metal" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('resources.crystal') }}:</span>
|
||||
<span class="font-medium">
|
||||
<NumberWithTooltip :value="totalStats.crystal" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('resources.deuterium') }}:</span>
|
||||
<span class="font-medium">
|
||||
<NumberWithTooltip :value="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">
|
||||
<NumberWithTooltip :value="totalStats.points" />
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground mt-1">
|
||||
{{ t('buildings.levelRange') }}: {{ Math.max(0, currentLevel - 10) }} - {{ Math.min(currentLevel + 10, currentLevel + 10) }}
|
||||
</p>
|
||||
</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 NumberWithTooltip from '@/components/NumberWithTooltip.vue'
|
||||
import * as buildingLogic from '@/logic/buildingLogic'
|
||||
import * as pointsLogic from '@/logic/pointsLogic'
|
||||
import { formatTime } from '@/utils/format'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps<{
|
||||
buildingType: BuildingType
|
||||
currentLevel: number
|
||||
}>()
|
||||
|
||||
// 等级范围:当前等级 +10
|
||||
const levelRange = computed(() => {
|
||||
const end = props.currentLevel + 10
|
||||
const levels = []
|
||||
for (let i = props.currentLevel; 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
|
||||
|
||||
// 资源矿产量(与 resourceLogic.ts 保持一致)
|
||||
if (props.buildingType === 'metalMine') {
|
||||
production = Math.floor(1500 * level * Math.pow(1.5, level))
|
||||
} else if (props.buildingType === 'crystalMine') {
|
||||
production = Math.floor(1000 * level * Math.pow(1.5, level))
|
||||
} else if (props.buildingType === 'deuteriumSynthesizer') {
|
||||
production = Math.floor(500 * level * Math.pow(1.5, level))
|
||||
}
|
||||
|
||||
// 能量产出(与 resourceLogic.ts 保持一致)
|
||||
if (props.buildingType === 'solarPlant') {
|
||||
production = Math.floor(50 * 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 }
|
||||
})
|
||||
</script>
|
||||
@@ -1,179 +0,0 @@
|
||||
<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">
|
||||
<NumberWithTooltip :value="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">
|
||||
<NumberWithTooltip :value="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">
|
||||
<NumberWithTooltip :value="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">
|
||||
<NumberWithTooltip :value="config.cost.metal" />
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="config.cost.crystal > 0" class="flex items-center justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('resources.crystal') }}:</span>
|
||||
<span class="font-medium">
|
||||
<NumberWithTooltip :value="config.cost.crystal" />
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="config.cost.deuterium > 0" class="flex items-center justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('resources.deuterium') }}:</span>
|
||||
<span class="font-medium">
|
||||
<NumberWithTooltip :value="config.cost.deuterium" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-sm pt-2 border-t">
|
||||
<span class="text-muted-foreground">{{ t('player.points') }}:</span>
|
||||
<span class="font-bold text-primary">
|
||||
<NumberWithTooltip :value="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">
|
||||
<NumberWithTooltip :value="batchCost.metal" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>{{ t('resources.crystal') }}:</span>
|
||||
<span class="font-medium">
|
||||
<NumberWithTooltip :value="batchCost.crystal" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>{{ t('resources.deuterium') }}:</span>
|
||||
<span class="font-medium">
|
||||
<NumberWithTooltip :value="batchCost.deuterium" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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') }}: +
|
||||
<NumberWithTooltip :value="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 NumberWithTooltip from '@/components/NumberWithTooltip.vue'
|
||||
import { Sword, Shield, ShieldCheck } from 'lucide-vue-next'
|
||||
import * as pointsLogic from '@/logic/pointsLogic'
|
||||
import { DEFENSES } from '@/config/gameConfig'
|
||||
import { formatTime } from '@/utils/format'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
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)
|
||||
})
|
||||
</script>
|
||||
@@ -1,221 +0,0 @@
|
||||
<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">
|
||||
<NumberWithTooltip :value="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">
|
||||
<NumberWithTooltip :value="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">
|
||||
<NumberWithTooltip :value="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">
|
||||
<NumberWithTooltip :value="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">
|
||||
<NumberWithTooltip :value="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">
|
||||
<NumberWithTooltip :value="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">
|
||||
<NumberWithTooltip :value="config.cost.metal" />
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="config.cost.crystal > 0" class="flex items-center justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('resources.crystal') }}:</span>
|
||||
<span class="font-medium">
|
||||
<NumberWithTooltip :value="config.cost.crystal" />
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="config.cost.deuterium > 0" class="flex items-center justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('resources.deuterium') }}:</span>
|
||||
<span class="font-medium">
|
||||
<NumberWithTooltip :value="config.cost.deuterium" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-sm pt-2 border-t">
|
||||
<span class="text-muted-foreground">{{ t('player.points') }}:</span>
|
||||
<span class="font-bold text-primary">
|
||||
<NumberWithTooltip :value="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">
|
||||
<NumberWithTooltip :value="batchCost.metal" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>{{ t('resources.crystal') }}:</span>
|
||||
<span class="font-medium">
|
||||
<NumberWithTooltip :value="batchCost.crystal" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>{{ t('resources.deuterium') }}:</span>
|
||||
<span class="font-medium">
|
||||
<NumberWithTooltip :value="batchCost.deuterium" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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') }}: +
|
||||
<NumberWithTooltip :value="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 NumberWithTooltip from '@/components/NumberWithTooltip.vue'
|
||||
import { Sword, Shield, ShieldCheck, Zap, Package, Fuel } from 'lucide-vue-next'
|
||||
import * as pointsLogic from '@/logic/pointsLogic'
|
||||
import { SHIPS } from '@/config/gameConfig'
|
||||
import { formatTime } from '@/utils/format'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
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)
|
||||
})
|
||||
</script>
|
||||
@@ -1,158 +0,0 @@
|
||||
<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">
|
||||
<NumberWithTooltip :value="getLevelData(level).cost.metal" />
|
||||
</TableCell>
|
||||
<TableCell class="text-center text-sm">
|
||||
<NumberWithTooltip :value="getLevelData(level).cost.crystal" />
|
||||
</TableCell>
|
||||
<TableCell class="text-center text-sm">
|
||||
<NumberWithTooltip :value="getLevelData(level).cost.deuterium" />
|
||||
</TableCell>
|
||||
<TableCell class="text-center text-sm">{{ formatTime(getLevelData(level).researchTime) }}</TableCell>
|
||||
<TableCell class="text-center text-sm">
|
||||
<span class="text-primary font-medium">
|
||||
+
|
||||
<NumberWithTooltip :value="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">
|
||||
<NumberWithTooltip :value="totalStats.metal" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('resources.crystal') }}:</span>
|
||||
<span class="font-medium">
|
||||
<NumberWithTooltip :value="totalStats.crystal" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('resources.deuterium') }}:</span>
|
||||
<span class="font-medium">
|
||||
<NumberWithTooltip :value="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">
|
||||
<NumberWithTooltip :value="totalStats.points" />
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground mt-1">
|
||||
{{ t('research.levelRange') }}: {{ Math.max(0, currentLevel - 10) }} - {{ Math.min(currentLevel + 10, currentLevel + 10) }}
|
||||
</p>
|
||||
</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 NumberWithTooltip from '@/components/NumberWithTooltip.vue'
|
||||
import * as researchLogic from '@/logic/researchLogic'
|
||||
import * as pointsLogic from '@/logic/pointsLogic'
|
||||
import { formatTime } from '@/utils/format'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps<{
|
||||
technologyType: TechnologyType
|
||||
currentLevel: number
|
||||
}>()
|
||||
|
||||
// 等级范围:当前等级 +10
|
||||
const levelRange = computed(() => {
|
||||
const end = props.currentLevel + 10
|
||||
const levels = []
|
||||
for (let i = props.currentLevel; 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 }
|
||||
})
|
||||
</script>
|
||||
@@ -4,7 +4,7 @@ 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',
|
||||
'inline-flex items-center justify-center rounded-sm border h-5 min-w-5 px-1 text-xs font-medium tabular-nums select-none 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: {
|
||||
|
||||
@@ -5,20 +5,20 @@
|
||||
</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 '.'
|
||||
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']
|
||||
}
|
||||
interface Props extends PrimitiveProps {
|
||||
variant?: ButtonVariants['variant']
|
||||
size?: ButtonVariants['size']
|
||||
class?: HTMLAttributes['class']
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
as: 'button'
|
||||
})
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
as: 'button'
|
||||
})
|
||||
</script>
|
||||
|
||||
35
src/components/ui/checkbox/Checkbox.vue
Normal file
35
src/components/ui/checkbox/Checkbox.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<CheckboxRoot
|
||||
v-slot="slotProps"
|
||||
data-slot="checkbox"
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn(
|
||||
'peer border-input data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<CheckboxIndicator data-slot="checkbox-indicator" class="grid place-content-center text-current transition-none">
|
||||
<slot v-bind="slotProps">
|
||||
<Check class="size-3.5" />
|
||||
</slot>
|
||||
</CheckboxIndicator>
|
||||
</CheckboxRoot>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { CheckboxRootEmits, CheckboxRootProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { Check } from 'lucide-vue-next'
|
||||
import { CheckboxIndicator, CheckboxRoot, useForwardPropsEmits } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<CheckboxRootProps & { class?: HTMLAttributes['class'] }>()
|
||||
const emits = defineEmits<CheckboxRootEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
1
src/components/ui/checkbox/index.ts
Normal file
1
src/components/ui/checkbox/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as Checkbox } from './Checkbox.vue'
|
||||
71
src/components/ui/dialog/ScrollableDialogContent.vue
Normal file
71
src/components/ui/dialog/ScrollableDialogContent.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogContent
|
||||
data-slot="scrollable-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 w-[calc(100vw-3rem)] translate-x-[-50%] translate-y-[-50%] rounded-lg border shadow-lg duration-200 sm:w-auto flex flex-col p-0',
|
||||
containerClass
|
||||
)
|
||||
"
|
||||
>
|
||||
<!-- 固定的头部 -->
|
||||
<div class="shrink-0 px-4 pt-4 pb-3 sm:px-6 sm:pt-6 sm:pb-4 border-b">
|
||||
<slot name="header" />
|
||||
</div>
|
||||
|
||||
<!-- 可滚动的内容区域 -->
|
||||
<div class="overflow-y-auto px-4 py-3 sm:px-6 sm:py-4">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<!-- 可选的固定底部 -->
|
||||
<div v-if="$slots.footer" class="shrink-0 px-4 pb-4 pt-3 sm:px-6 sm:pb-6 sm:pt-4 border-t">
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
|
||||
<!-- 关闭按钮 -->
|
||||
<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 z-10"
|
||||
>
|
||||
<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 & {
|
||||
containerClass?: HTMLAttributes['class']
|
||||
showCloseButton?: boolean
|
||||
}
|
||||
>(),
|
||||
{
|
||||
showCloseButton: true
|
||||
}
|
||||
)
|
||||
const emits = defineEmits<DialogContentEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'containerClass')
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
@@ -6,5 +6,6 @@ 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 ScrollableDialogContent } from './ScrollableDialogContent.vue'
|
||||
export { default as DialogTitle } from './DialogTitle.vue'
|
||||
export { default as DialogTrigger } from './DialogTrigger.vue'
|
||||
|
||||
28
src/components/ui/pagination/Pagination.vue
Normal file
28
src/components/ui/pagination/Pagination.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<PaginationRoot
|
||||
v-slot="slotProps"
|
||||
data-slot="pagination"
|
||||
v-bind="forwarded"
|
||||
:class="cn('mx-auto flex w-full justify-center', props.class)"
|
||||
>
|
||||
<slot v-bind="slotProps" />
|
||||
</PaginationRoot>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { PaginationRootEmits, PaginationRootProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { PaginationRoot, useForwardPropsEmits } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<
|
||||
PaginationRootProps & {
|
||||
class?: HTMLAttributes['class']
|
||||
}
|
||||
>()
|
||||
const emits = defineEmits<PaginationRootEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
22
src/components/ui/pagination/PaginationContent.vue
Normal file
22
src/components/ui/pagination/PaginationContent.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<PaginationList
|
||||
v-slot="slotProps"
|
||||
data-slot="pagination-content"
|
||||
v-bind="delegatedProps"
|
||||
:class="cn('flex flex-row items-center gap-1', props.class)"
|
||||
>
|
||||
<slot v-bind="slotProps" />
|
||||
</PaginationList>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { PaginationListProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { PaginationList } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<PaginationListProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
</script>
|
||||
25
src/components/ui/pagination/PaginationEllipsis.vue
Normal file
25
src/components/ui/pagination/PaginationEllipsis.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<PaginationEllipsis
|
||||
data-slot="pagination-ellipsis"
|
||||
v-bind="delegatedProps"
|
||||
:class="cn('flex size-9 items-center justify-center', props.class)"
|
||||
>
|
||||
<slot>
|
||||
<MoreHorizontal class="size-4" />
|
||||
<span class="sr-only">More pages</span>
|
||||
</slot>
|
||||
</PaginationEllipsis>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { PaginationEllipsisProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { MoreHorizontal } from 'lucide-vue-next'
|
||||
import { PaginationEllipsis } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<PaginationEllipsisProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
</script>
|
||||
38
src/components/ui/pagination/PaginationFirst.vue
Normal file
38
src/components/ui/pagination/PaginationFirst.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<PaginationFirst
|
||||
data-slot="pagination-first"
|
||||
:class="cn(buttonVariants({ variant: 'ghost', size }), 'gap-1 px-2.5 sm:pr-2.5', props.class)"
|
||||
v-bind="forwarded"
|
||||
>
|
||||
<slot>
|
||||
<ChevronLeftIcon />
|
||||
<span class="hidden sm:block">First</span>
|
||||
</slot>
|
||||
</PaginationFirst>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { PaginationFirstProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import type { ButtonVariants } from '@/components/ui/button'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { ChevronLeftIcon } from 'lucide-vue-next'
|
||||
import { PaginationFirst, useForwardProps } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { buttonVariants } from '@/components/ui/button'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<
|
||||
PaginationFirstProps & {
|
||||
size?: ButtonVariants['size']
|
||||
class?: HTMLAttributes['class']
|
||||
}
|
||||
>(),
|
||||
{
|
||||
size: 'default'
|
||||
}
|
||||
)
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class', 'size')
|
||||
const forwarded = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
42
src/components/ui/pagination/PaginationItem.vue
Normal file
42
src/components/ui/pagination/PaginationItem.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<PaginationListItem
|
||||
data-slot="pagination-item"
|
||||
v-bind="delegatedProps"
|
||||
:class="
|
||||
cn(
|
||||
buttonVariants({
|
||||
variant: isActive ? 'outline' : 'ghost',
|
||||
size
|
||||
}),
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</PaginationListItem>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { PaginationListItemProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import type { ButtonVariants } from '@/components/ui/button'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { PaginationListItem } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { buttonVariants } from '@/components/ui/button'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<
|
||||
PaginationListItemProps & {
|
||||
size?: ButtonVariants['size']
|
||||
class?: HTMLAttributes['class']
|
||||
isActive?: boolean
|
||||
}
|
||||
>(),
|
||||
{
|
||||
size: 'icon'
|
||||
}
|
||||
)
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class', 'size', 'isActive')
|
||||
</script>
|
||||
38
src/components/ui/pagination/PaginationLast.vue
Normal file
38
src/components/ui/pagination/PaginationLast.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<PaginationLast
|
||||
data-slot="pagination-last"
|
||||
:class="cn(buttonVariants({ variant: 'ghost', size }), 'gap-1 px-2.5 sm:pr-2.5', props.class)"
|
||||
v-bind="forwarded"
|
||||
>
|
||||
<slot>
|
||||
<span class="hidden sm:block">Last</span>
|
||||
<ChevronRightIcon />
|
||||
</slot>
|
||||
</PaginationLast>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { PaginationLastProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import type { ButtonVariants } from '@/components/ui/button'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { ChevronRightIcon } from 'lucide-vue-next'
|
||||
import { PaginationLast, useForwardProps } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { buttonVariants } from '@/components/ui/button'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<
|
||||
PaginationLastProps & {
|
||||
size?: ButtonVariants['size']
|
||||
class?: HTMLAttributes['class']
|
||||
}
|
||||
>(),
|
||||
{
|
||||
size: 'default'
|
||||
}
|
||||
)
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class', 'size')
|
||||
const forwarded = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
38
src/components/ui/pagination/PaginationNext.vue
Normal file
38
src/components/ui/pagination/PaginationNext.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<PaginationNext
|
||||
data-slot="pagination-next"
|
||||
:class="cn(buttonVariants({ variant: 'ghost', size }), 'gap-1 px-2.5 sm:pr-2.5', props.class)"
|
||||
v-bind="forwarded"
|
||||
>
|
||||
<slot>
|
||||
<span class="hidden sm:block">Next</span>
|
||||
<ChevronRightIcon />
|
||||
</slot>
|
||||
</PaginationNext>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { PaginationNextProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import type { ButtonVariants } from '@/components/ui/button'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { ChevronRightIcon } from 'lucide-vue-next'
|
||||
import { PaginationNext, useForwardProps } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { buttonVariants } from '@/components/ui/button'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<
|
||||
PaginationNextProps & {
|
||||
size?: ButtonVariants['size']
|
||||
class?: HTMLAttributes['class']
|
||||
}
|
||||
>(),
|
||||
{
|
||||
size: 'default'
|
||||
}
|
||||
)
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class', 'size')
|
||||
const forwarded = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
38
src/components/ui/pagination/PaginationPrevious.vue
Normal file
38
src/components/ui/pagination/PaginationPrevious.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<PaginationPrev
|
||||
data-slot="pagination-previous"
|
||||
:class="cn(buttonVariants({ variant: 'ghost', size }), 'gap-1 px-2.5 sm:pr-2.5', props.class)"
|
||||
v-bind="forwarded"
|
||||
>
|
||||
<slot>
|
||||
<ChevronLeftIcon />
|
||||
<span class="hidden sm:block">Previous</span>
|
||||
</slot>
|
||||
</PaginationPrev>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { PaginationPrevProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import type { ButtonVariants } from '@/components/ui/button'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { ChevronLeftIcon } from 'lucide-vue-next'
|
||||
import { PaginationPrev, useForwardProps } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { buttonVariants } from '@/components/ui/button'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<
|
||||
PaginationPrevProps & {
|
||||
size?: ButtonVariants['size']
|
||||
class?: HTMLAttributes['class']
|
||||
}
|
||||
>(),
|
||||
{
|
||||
size: 'default'
|
||||
}
|
||||
)
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class', 'size')
|
||||
const forwarded = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
8
src/components/ui/pagination/index.ts
Normal file
8
src/components/ui/pagination/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export { default as Pagination } from './Pagination.vue'
|
||||
export { default as PaginationContent } from './PaginationContent.vue'
|
||||
export { default as PaginationEllipsis } from './PaginationEllipsis.vue'
|
||||
export { default as PaginationFirst } from './PaginationFirst.vue'
|
||||
export { default as PaginationItem } from './PaginationItem.vue'
|
||||
export { default as PaginationLast } from './PaginationLast.vue'
|
||||
export { default as PaginationNext } from './PaginationNext.vue'
|
||||
export { default as PaginationPrevious } from './PaginationPrevious.vue'
|
||||
@@ -4,12 +4,13 @@
|
||||
data-sidebar="menu-badge"
|
||||
:class="
|
||||
cn(
|
||||
'text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none',
|
||||
'text-sidebar-foreground pointer-events-none flex h-5 min-w-5 items-center justify-center rounded-sm px-1 text-xs font-medium tabular-nums select-none',
|
||||
'peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground',
|
||||
'absolute right-1',
|
||||
'peer-data-[size=sm]/menu-button:top-1',
|
||||
'peer-data-[size=default]/menu-button:top-1.5',
|
||||
'peer-data-[size=lg]/menu-button:top-2.5',
|
||||
'group-data-[collapsible=icon]:hidden',
|
||||
'group-data-[collapsible=icon]:absolute group-data-[collapsible=icon]:right-0 group-data-[collapsible=icon]:-top-1 group-data-[collapsible=icon]:h-4 group-data-[collapsible=icon]:min-w-4',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
|
||||
@@ -53,19 +53,19 @@
|
||||
passive: (props.open === undefined) as false
|
||||
}) as Ref<boolean>
|
||||
|
||||
function setOpen(value: boolean) {
|
||||
const setOpen = (value: boolean) => {
|
||||
open.value = value // emits('update:open', value)
|
||||
|
||||
// This sets the cookie to keep the sidebar state.
|
||||
document.cookie = `${SIDEBAR_COOKIE_NAME}=${open.value}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
|
||||
}
|
||||
|
||||
function setOpenMobile(value: boolean) {
|
||||
const setOpenMobile = (value: boolean) => {
|
||||
openMobile.value = value
|
||||
}
|
||||
|
||||
// Helper to toggle the sidebar.
|
||||
function toggleSidebar() {
|
||||
const toggleSidebar = () => {
|
||||
return isMobile.value ? setOpenMobile(!openMobile.value) : setOpen(!open.value)
|
||||
}
|
||||
|
||||
|
||||
19
src/components/ui/tabs/Tabs.vue
Normal file
19
src/components/ui/tabs/Tabs.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<TabsRoot v-slot="slotProps" data-slot="tabs" v-bind="forwarded" :class="cn('flex flex-col gap-2', props.class)">
|
||||
<slot v-bind="slotProps" />
|
||||
</TabsRoot>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { TabsRootEmits, TabsRootProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { TabsRoot, useForwardPropsEmits } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<TabsRootProps & { class?: HTMLAttributes['class'] }>()
|
||||
const emits = defineEmits<TabsRootEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
17
src/components/ui/tabs/TabsContent.vue
Normal file
17
src/components/ui/tabs/TabsContent.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<TabsContentRoot data-slot="tabs-content" :class="cn('flex-1 outline-none', props.class)" v-bind="delegatedProps">
|
||||
<slot />
|
||||
</TabsContentRoot>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { TabsContentProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { TabsContent as TabsContentRoot } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<TabsContentProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
</script>
|
||||
27
src/components/ui/tabs/TabsList.vue
Normal file
27
src/components/ui/tabs/TabsList.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<TabsListRoot
|
||||
data-slot="tabs-list"
|
||||
v-bind="delegatedProps"
|
||||
:class="
|
||||
cn(
|
||||
'bg-muted text-muted-foreground inline-flex w-fit items-center justify-center rounded-lg p-[3px]',
|
||||
tabCount && tabCount > 3 ? (tabCount > 6 ? 'h-[85px] sm:h-9' : 'h-[65px] sm:h-9') : 'h-9',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</TabsListRoot>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { TabsListProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { TabsList as TabsListRoot } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<TabsListProps & { class?: HTMLAttributes['class']; tabCount?: number }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class', 'tabCount')
|
||||
</script>
|
||||
34
src/components/ui/tabs/TabsTrigger.vue
Normal file
34
src/components/ui/tabs/TabsTrigger.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<TabsTriggerRoot
|
||||
data-slot="tabs-trigger"
|
||||
:class="
|
||||
cn(
|
||||
'inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-all',
|
||||
'text-muted-foreground hover:text-foreground',
|
||||
'data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-md data-[state=active]:border-border',
|
||||
'dark:data-[state=active]:bg-background dark:data-[state=active]:text-foreground dark:data-[state=active]:border-border dark:data-[state=active]:shadow-lg',
|
||||
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring focus-visible:ring-[3px] focus-visible:outline-1',
|
||||
'disabled:pointer-events-none disabled:opacity-50',
|
||||
'[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
v-bind="forwardedProps"
|
||||
>
|
||||
<slot />
|
||||
</TabsTriggerRoot>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { TabsTriggerProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { TabsTrigger as TabsTriggerRoot, useForwardProps } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<TabsTriggerProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
4
src/components/ui/tabs/index.ts
Normal file
4
src/components/ui/tabs/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export { default as Tabs } from './Tabs.vue'
|
||||
export { default as TabsContent } from './TabsContent.vue'
|
||||
export { default as TabsList } from './TabsList.vue'
|
||||
export { default as TabsTrigger } from './TabsTrigger.vue'
|
||||
@@ -25,6 +25,7 @@ export const useGameConfig = () => {
|
||||
[BuildingType.CrystalMine]: 'crystalMine',
|
||||
[BuildingType.DeuteriumSynthesizer]: 'deuteriumSynthesizer',
|
||||
[BuildingType.SolarPlant]: 'solarPlant',
|
||||
[BuildingType.FusionReactor]: 'fusionReactor',
|
||||
[BuildingType.RoboticsFactory]: 'roboticsFactory',
|
||||
[BuildingType.NaniteFactory]: 'naniteFactory',
|
||||
[BuildingType.Shipyard]: 'shipyard',
|
||||
@@ -33,6 +34,8 @@ export const useGameConfig = () => {
|
||||
[BuildingType.CrystalStorage]: 'crystalStorage',
|
||||
[BuildingType.DeuteriumTank]: 'deuteriumTank',
|
||||
[BuildingType.DarkMatterCollector]: 'darkMatterCollector',
|
||||
[BuildingType.DarkMatterTank]: 'darkMatterTank',
|
||||
[BuildingType.MissileSilo]: 'missileSilo',
|
||||
[BuildingType.Terraformer]: 'terraformer',
|
||||
[BuildingType.LunarBase]: 'lunarBase',
|
||||
[BuildingType.SensorPhalanx]: 'sensorPhalanx',
|
||||
@@ -46,11 +49,15 @@ export const useGameConfig = () => {
|
||||
[ShipType.HeavyFighter]: 'heavyFighter',
|
||||
[ShipType.Cruiser]: 'cruiser',
|
||||
[ShipType.Battleship]: 'battleship',
|
||||
[ShipType.Battlecruiser]: 'battlecruiser',
|
||||
[ShipType.Bomber]: 'bomber',
|
||||
[ShipType.Destroyer]: 'destroyer',
|
||||
[ShipType.SmallCargo]: 'smallCargo',
|
||||
[ShipType.LargeCargo]: 'largeCargo',
|
||||
[ShipType.ColonyShip]: 'colonyShip',
|
||||
[ShipType.Recycler]: 'recycler',
|
||||
[ShipType.EspionageProbe]: 'espionageProbe',
|
||||
[ShipType.SolarSatellite]: 'solarSatellite',
|
||||
[ShipType.DarkMatterHarvester]: 'darkMatterHarvester',
|
||||
[ShipType.Deathstar]: 'deathstar'
|
||||
}
|
||||
@@ -65,6 +72,8 @@ export const useGameConfig = () => {
|
||||
[DefenseType.PlasmaTurret]: 'plasmaTurret',
|
||||
[DefenseType.SmallShieldDome]: 'smallShieldDome',
|
||||
[DefenseType.LargeShieldDome]: 'largeShieldDome',
|
||||
[DefenseType.AntiBallisticMissile]: 'antiBallisticMissile',
|
||||
[DefenseType.InterplanetaryMissile]: 'interplanetaryMissile',
|
||||
[DefenseType.PlanetaryShield]: 'planetaryShield'
|
||||
}
|
||||
|
||||
@@ -76,6 +85,12 @@ export const useGameConfig = () => {
|
||||
[TechnologyType.HyperspaceTechnology]: 'hyperspaceTechnology',
|
||||
[TechnologyType.PlasmaTechnology]: 'plasmaTechnology',
|
||||
[TechnologyType.ComputerTechnology]: 'computerTechnology',
|
||||
[TechnologyType.EspionageTechnology]: 'espionageTechnology',
|
||||
[TechnologyType.WeaponsTechnology]: 'weaponsTechnology',
|
||||
[TechnologyType.ShieldingTechnology]: 'shieldingTechnology',
|
||||
[TechnologyType.ArmourTechnology]: 'armourTechnology',
|
||||
[TechnologyType.Astrophysics]: 'astrophysics',
|
||||
[TechnologyType.GravitonTechnology]: 'gravitonTechnology',
|
||||
[TechnologyType.CombustionDrive]: 'combustionDrive',
|
||||
[TechnologyType.ImpulseDrive]: 'impulseDrive',
|
||||
[TechnologyType.HyperspaceDrive]: 'hyperspaceDrive',
|
||||
|
||||
63
src/composables/useGameLifecycle.ts
Normal file
63
src/composables/useGameLifecycle.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useUniverseStore } from '@/stores/universeStore'
|
||||
import * as gameLogic from '@/logic/gameLogic'
|
||||
import * as planetLogic from '@/logic/planetLogic'
|
||||
import * as resourceLogic from '@/logic/resourceLogic'
|
||||
import * as officerLogic from '@/logic/officerLogic'
|
||||
|
||||
/**
|
||||
* 游戏生命周期管理
|
||||
* 处理游戏初始化、NPC星球生成等
|
||||
*/
|
||||
export const useGameLifecycle = () => {
|
||||
const gameStore = useGameStore()
|
||||
const universeStore = useUniverseStore()
|
||||
|
||||
/**
|
||||
* 生成NPC星球
|
||||
*/
|
||||
const generateNPCPlanets = (npcCount: number, planetPrefix: string) => {
|
||||
for (let i = 0; i < npcCount; i++) {
|
||||
const position = gameLogic.generateRandomPosition()
|
||||
const key = gameLogic.generatePositionKey(position.galaxy, position.system, position.position)
|
||||
if (universeStore.planets[key]) continue
|
||||
const npcPlanet = planetLogic.createNPCPlanet(i, position, planetPrefix)
|
||||
universeStore.planets[key] = npcPlanet
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化游戏
|
||||
*/
|
||||
const initGame = async (playerName: string, homePlanetName: string, planetPrefix: string) => {
|
||||
const shouldInit = gameLogic.shouldInitializeGame(gameStore.player.planets)
|
||||
|
||||
if (!shouldInit) {
|
||||
const now = Date.now()
|
||||
|
||||
// 计算离线收益(直接同步计算)
|
||||
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, now)
|
||||
gameStore.player.planets.forEach(planet => {
|
||||
resourceLogic.updatePlanetResources(planet, now, bonuses)
|
||||
})
|
||||
|
||||
// 只在没有NPC星球时才生成(首次加载已有玩家数据时)
|
||||
if (Object.keys(universeStore.planets).length === 0) {
|
||||
generateNPCPlanets(200, planetPrefix)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
gameStore.player = gameLogic.initializePlayer(gameStore.player.id, playerName)
|
||||
const initialPlanet = planetLogic.createInitialPlanet(gameStore.player.id, homePlanetName)
|
||||
gameStore.player.planets = [initialPlanet]
|
||||
gameStore.currentPlanetId = initialPlanet.id
|
||||
// 新玩家初始化时生成NPC星球
|
||||
generateNPCPlanets(200, planetPrefix)
|
||||
}
|
||||
|
||||
return {
|
||||
initGame,
|
||||
generateNPCPlanets
|
||||
}
|
||||
}
|
||||
68
src/composables/useGameUpdate.ts
Normal file
68
src/composables/useGameUpdate.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useNPCStore } from '@/stores/npcStore'
|
||||
import type { FleetMission } from '@/types/game'
|
||||
import * as gameLogic from '@/logic/gameLogic'
|
||||
|
||||
/**
|
||||
* 游戏更新循环
|
||||
* 处理游戏状态的定期更新
|
||||
*/
|
||||
export const useGameUpdate = (
|
||||
processMissionArrival: (mission: FleetMission) => Promise<void>,
|
||||
processMissionReturn: (mission: FleetMission) => void,
|
||||
processNPCMissionArrival: (npc: any, mission: FleetMission) => void,
|
||||
processNPCMissionReturn: (npc: any, mission: FleetMission) => void,
|
||||
updateNPCGrowth: (deltaSeconds: number) => void,
|
||||
updateNPCBehavior: (deltaSeconds: number) => void
|
||||
) => {
|
||||
const gameStore = useGameStore()
|
||||
const npcStore = useNPCStore()
|
||||
|
||||
/**
|
||||
* 游戏主更新函数
|
||||
*/
|
||||
const updateGame = () => {
|
||||
if (gameStore.isPaused) return
|
||||
const now = Date.now()
|
||||
gameStore.gameTime = now
|
||||
|
||||
// 检查军官过期
|
||||
gameLogic.checkOfficersExpiration(gameStore.player.officers, now)
|
||||
|
||||
// 处理游戏更新(建造队列、研究队列等)
|
||||
const result = gameLogic.processGameUpdate(gameStore.player, now)
|
||||
gameStore.player.researchQueue = result.updatedResearchQueue
|
||||
|
||||
// 处理舰队任务
|
||||
gameStore.player.fleetMissions.forEach(mission => {
|
||||
if (mission.status === 'outbound' && now >= mission.arrivalTime) {
|
||||
processMissionArrival(mission)
|
||||
} else if (mission.status === 'returning' && mission.returnTime && now >= mission.returnTime) {
|
||||
processMissionReturn(mission)
|
||||
}
|
||||
})
|
||||
|
||||
// 处理NPC舰队任务
|
||||
npcStore.npcs.forEach(npc => {
|
||||
if (npc.fleetMissions) {
|
||||
npc.fleetMissions.forEach(mission => {
|
||||
if (mission.status === 'outbound' && now >= mission.arrivalTime) {
|
||||
processNPCMissionArrival(npc, mission)
|
||||
} else if (mission.status === 'returning' && mission.returnTime && now >= mission.returnTime) {
|
||||
processNPCMissionReturn(npc, mission)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// NPC成长系统更新
|
||||
updateNPCGrowth(1) // 传入1秒的时间间隔
|
||||
|
||||
// NPC行为系统更新(侦查和攻击决策)
|
||||
updateNPCBehavior(1)
|
||||
}
|
||||
|
||||
return {
|
||||
updateGame
|
||||
}
|
||||
}
|
||||
249
src/composables/useMissionHandler.ts
Normal file
249
src/composables/useMissionHandler.ts
Normal file
@@ -0,0 +1,249 @@
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useUniverseStore } from '@/stores/universeStore'
|
||||
import { useNPCStore } from '@/stores/npcStore'
|
||||
import type { FleetMission } from '@/types/game'
|
||||
import { MissionType } from '@/types/game'
|
||||
import * as gameLogic from '@/logic/gameLogic'
|
||||
import * as fleetLogic from '@/logic/fleetLogic'
|
||||
import * as shipLogic from '@/logic/shipLogic'
|
||||
import * as resourceLogic from '@/logic/resourceLogic'
|
||||
import * as diplomaticLogic from '@/logic/diplomaticLogic'
|
||||
|
||||
/**
|
||||
* 舰队任务处理
|
||||
* 处理玩家舰队任务的到达和返回
|
||||
*/
|
||||
export const useMissionHandler = (t: (key: string) => string) => {
|
||||
const gameStore = useGameStore()
|
||||
const universeStore = useUniverseStore()
|
||||
const npcStore = useNPCStore()
|
||||
|
||||
/**
|
||||
* 处理任务到达
|
||||
*/
|
||||
const processMissionArrival = async (mission: FleetMission) => {
|
||||
// 从宇宙星球地图中查找目标星球
|
||||
const targetKey = gameLogic.generatePositionKey(
|
||||
mission.targetPosition.galaxy,
|
||||
mission.targetPosition.system,
|
||||
mission.targetPosition.position
|
||||
)
|
||||
// 先从玩家星球中查找,再从宇宙地图中查找
|
||||
const targetPlanet =
|
||||
gameStore.player.planets.find(
|
||||
p =>
|
||||
p.position.galaxy === mission.targetPosition.galaxy &&
|
||||
p.position.system === mission.targetPosition.system &&
|
||||
p.position.position === mission.targetPosition.position
|
||||
) || universeStore.planets[targetKey]
|
||||
|
||||
// 获取起始星球名称(用于报告)
|
||||
const originPlanet = gameStore.player.planets.find(p => p.id === mission.originPlanetId)
|
||||
const originPlanetName = originPlanet?.name || t('fleetView.unknownPlanet')
|
||||
|
||||
if (mission.missionType === MissionType.Transport) {
|
||||
const result = fleetLogic.processTransportArrival(mission, targetPlanet, gameStore.player, npcStore.npcs)
|
||||
// 生成运输任务报告
|
||||
if (!gameStore.player.missionReports) {
|
||||
gameStore.player.missionReports = []
|
||||
}
|
||||
gameStore.player.missionReports.push({
|
||||
id: `mission-report-${mission.id}`,
|
||||
timestamp: Date.now(),
|
||||
missionType: MissionType.Transport,
|
||||
originPlanetId: mission.originPlanetId,
|
||||
originPlanetName,
|
||||
targetPosition: mission.targetPosition,
|
||||
targetPlanetId: targetPlanet?.id,
|
||||
targetPlanetName:
|
||||
targetPlanet?.name || `[${mission.targetPosition.galaxy}:${mission.targetPosition.system}:${mission.targetPosition.position}]`,
|
||||
success: result.success,
|
||||
message: result.success ? t('missionReports.transportSuccess') : t('missionReports.transportFailed'),
|
||||
details: {
|
||||
transportedResources: mission.cargo
|
||||
},
|
||||
read: false
|
||||
})
|
||||
} else if (mission.missionType === MissionType.Attack) {
|
||||
const attackResult = await fleetLogic.processAttackArrival(mission, targetPlanet, gameStore.player, null, gameStore.player.planets)
|
||||
if (attackResult) {
|
||||
gameStore.player.battleReports.push(attackResult.battleResult)
|
||||
|
||||
// 检查是否攻击了NPC星球,更新外交关系
|
||||
if (targetPlanet) {
|
||||
const targetNpc = npcStore.npcs.find(npc => npc.planets.some(p => p.id === targetPlanet.id))
|
||||
if (targetNpc) {
|
||||
diplomaticLogic.handleAttackReputation(gameStore.player, targetNpc, attackResult.battleResult, npcStore.npcs)
|
||||
}
|
||||
}
|
||||
|
||||
if (attackResult.moon) {
|
||||
gameStore.player.planets.push(attackResult.moon)
|
||||
}
|
||||
if (attackResult.debrisField) {
|
||||
// 将残骸场添加到游戏状态
|
||||
universeStore.debrisFields[attackResult.debrisField.id] = attackResult.debrisField
|
||||
}
|
||||
}
|
||||
} else if (mission.missionType === MissionType.Colonize) {
|
||||
const newPlanet = fleetLogic.processColonizeArrival(mission, targetPlanet, gameStore.player, t('planet.colonyPrefix'))
|
||||
// 生成殖民任务报告
|
||||
if (!gameStore.player.missionReports) {
|
||||
gameStore.player.missionReports = []
|
||||
}
|
||||
gameStore.player.missionReports.push({
|
||||
id: `mission-report-${mission.id}`,
|
||||
timestamp: Date.now(),
|
||||
missionType: MissionType.Colonize,
|
||||
originPlanetId: mission.originPlanetId,
|
||||
originPlanetName,
|
||||
targetPosition: mission.targetPosition,
|
||||
targetPlanetId: newPlanet?.id,
|
||||
targetPlanetName: newPlanet?.name,
|
||||
success: !!newPlanet,
|
||||
message: newPlanet ? t('missionReports.colonizeSuccess') : t('missionReports.colonizeFailed'),
|
||||
details: newPlanet
|
||||
? {
|
||||
newPlanetId: newPlanet.id,
|
||||
newPlanetName: newPlanet.name
|
||||
}
|
||||
: undefined,
|
||||
read: false
|
||||
})
|
||||
if (newPlanet) {
|
||||
gameStore.player.planets.push(newPlanet)
|
||||
}
|
||||
} else if (mission.missionType === MissionType.Spy) {
|
||||
const spyReport = fleetLogic.processSpyArrival(mission, targetPlanet, gameStore.player, null, npcStore.npcs)
|
||||
if (spyReport) gameStore.player.spyReports.push(spyReport)
|
||||
} else if (mission.missionType === MissionType.Deploy) {
|
||||
const deployed = fleetLogic.processDeployArrival(mission, targetPlanet, gameStore.player.id)
|
||||
// 生成部署任务报告
|
||||
if (!gameStore.player.missionReports) {
|
||||
gameStore.player.missionReports = []
|
||||
}
|
||||
gameStore.player.missionReports.push({
|
||||
id: `mission-report-${mission.id}`,
|
||||
timestamp: Date.now(),
|
||||
missionType: MissionType.Deploy,
|
||||
originPlanetId: mission.originPlanetId,
|
||||
originPlanetName,
|
||||
targetPosition: mission.targetPosition,
|
||||
targetPlanetId: targetPlanet?.id,
|
||||
targetPlanetName:
|
||||
targetPlanet?.name || `[${mission.targetPosition.galaxy}:${mission.targetPosition.system}:${mission.targetPosition.position}]`,
|
||||
success: deployed,
|
||||
message: deployed ? t('missionReports.deploySuccess') : t('missionReports.deployFailed'),
|
||||
details: {
|
||||
deployedFleet: mission.fleet
|
||||
},
|
||||
read: false
|
||||
})
|
||||
if (deployed) {
|
||||
const missionIndex = gameStore.player.fleetMissions.indexOf(mission)
|
||||
if (missionIndex > -1) gameStore.player.fleetMissions.splice(missionIndex, 1)
|
||||
return
|
||||
}
|
||||
} else if (mission.missionType === MissionType.Recycle) {
|
||||
// 处理回收任务
|
||||
const debrisId = `debris_${mission.targetPosition.galaxy}_${mission.targetPosition.system}_${mission.targetPosition.position}`
|
||||
const debrisField = universeStore.debrisFields[debrisId]
|
||||
const recycleResult = fleetLogic.processRecycleArrival(mission, debrisField)
|
||||
|
||||
// 生成回收任务报告
|
||||
if (!gameStore.player.missionReports) {
|
||||
gameStore.player.missionReports = []
|
||||
}
|
||||
gameStore.player.missionReports.push({
|
||||
id: `mission-report-${mission.id}`,
|
||||
timestamp: Date.now(),
|
||||
missionType: MissionType.Recycle,
|
||||
originPlanetId: mission.originPlanetId,
|
||||
originPlanetName,
|
||||
targetPosition: mission.targetPosition,
|
||||
success: !!recycleResult,
|
||||
message: recycleResult ? t('missionReports.recycleSuccess') : t('missionReports.recycleFailed'),
|
||||
details: recycleResult
|
||||
? {
|
||||
recycledResources: recycleResult.collectedResources,
|
||||
remainingDebris: recycleResult.remainingDebris || undefined
|
||||
}
|
||||
: undefined,
|
||||
read: false
|
||||
})
|
||||
|
||||
if (recycleResult && debrisField) {
|
||||
if (recycleResult.remainingDebris && (recycleResult.remainingDebris.metal > 0 || recycleResult.remainingDebris.crystal > 0)) {
|
||||
// 更新残骸场
|
||||
universeStore.debrisFields[debrisId] = {
|
||||
id: debrisField.id,
|
||||
position: debrisField.position,
|
||||
resources: recycleResult.remainingDebris,
|
||||
createdAt: debrisField.createdAt,
|
||||
expiresAt: debrisField.expiresAt
|
||||
}
|
||||
} else {
|
||||
// 残骸场已被完全收集,删除
|
||||
delete universeStore.debrisFields[debrisId]
|
||||
}
|
||||
}
|
||||
} else if (mission.missionType === MissionType.Destroy) {
|
||||
// 处理行星毁灭任务
|
||||
const destroyResult = fleetLogic.processDestroyArrival(mission, targetPlanet, gameStore.player)
|
||||
|
||||
// 生成毁灭任务报告
|
||||
if (!gameStore.player.missionReports) {
|
||||
gameStore.player.missionReports = []
|
||||
}
|
||||
gameStore.player.missionReports.push({
|
||||
id: `mission-report-${mission.id}`,
|
||||
timestamp: Date.now(),
|
||||
missionType: MissionType.Destroy,
|
||||
originPlanetId: mission.originPlanetId,
|
||||
originPlanetName,
|
||||
targetPosition: mission.targetPosition,
|
||||
targetPlanetId: targetPlanet?.id,
|
||||
targetPlanetName: targetPlanet?.name,
|
||||
success: destroyResult?.success || false,
|
||||
message: destroyResult?.success ? t('missionReports.destroySuccess') : t('missionReports.destroyFailed'),
|
||||
details: destroyResult?.success
|
||||
? {
|
||||
destroyedPlanetName:
|
||||
targetPlanet?.name ||
|
||||
`[${mission.targetPosition.galaxy}:${mission.targetPosition.system}:${mission.targetPosition.position}]`
|
||||
}
|
||||
: undefined,
|
||||
read: false
|
||||
})
|
||||
|
||||
if (destroyResult && destroyResult.success && destroyResult.planetId) {
|
||||
// 星球被摧毁
|
||||
// 从玩家星球列表中移除(如果是玩家的星球)
|
||||
const planetIndex = gameStore.player.planets.findIndex(p => p.id === destroyResult.planetId)
|
||||
if (planetIndex > -1) {
|
||||
gameStore.player.planets.splice(planetIndex, 1)
|
||||
} else {
|
||||
// 不是玩家星球,从宇宙地图中移除
|
||||
delete universeStore.planets[targetKey]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理任务返回
|
||||
*/
|
||||
const processMissionReturn = (mission: FleetMission) => {
|
||||
const originPlanet = gameStore.player.planets.find(p => p.id === mission.originPlanetId)
|
||||
if (!originPlanet) return
|
||||
shipLogic.addFleet(originPlanet.fleet, mission.fleet)
|
||||
resourceLogic.addResources(originPlanet.resources, mission.cargo)
|
||||
const missionIndex = gameStore.player.fleetMissions.indexOf(mission)
|
||||
if (missionIndex > -1) gameStore.player.fleetMissions.splice(missionIndex, 1)
|
||||
}
|
||||
|
||||
return {
|
||||
processMissionArrival,
|
||||
processMissionReturn
|
||||
}
|
||||
}
|
||||
300
src/composables/useNPCHandler.ts
Normal file
300
src/composables/useNPCHandler.ts
Normal file
@@ -0,0 +1,300 @@
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useUniverseStore } from '@/stores/universeStore'
|
||||
import { useNPCStore } from '@/stores/npcStore'
|
||||
import type { NPC, FleetMission, IncomingFleetAlert } from '@/types/game'
|
||||
import { MissionType } from '@/types/game'
|
||||
import * as gameLogic from '@/logic/gameLogic'
|
||||
import * as fleetLogic from '@/logic/fleetLogic'
|
||||
import * as shipLogic from '@/logic/shipLogic'
|
||||
import * as npcGrowthLogic from '@/logic/npcGrowthLogic'
|
||||
import * as npcBehaviorLogic from '@/logic/npcBehaviorLogic'
|
||||
|
||||
/**
|
||||
* NPC处理
|
||||
* 处理NPC舰队任务、成长系统、行为系统
|
||||
*/
|
||||
export const useNPCHandler = () => {
|
||||
const gameStore = useGameStore()
|
||||
const universeStore = useUniverseStore()
|
||||
const npcStore = useNPCStore()
|
||||
|
||||
/**
|
||||
* 移除即将到来的舰队警告
|
||||
*/
|
||||
const removeIncomingFleetAlert = (alert: IncomingFleetAlert) => {
|
||||
if (!gameStore.player.incomingFleetAlerts) return
|
||||
const index = gameStore.player.incomingFleetAlerts.indexOf(alert)
|
||||
if (index > -1) {
|
||||
gameStore.player.incomingFleetAlerts.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据任务ID移除即将到来的舰队警告
|
||||
*/
|
||||
const removeIncomingFleetAlertById = (missionId: string) => {
|
||||
if (!gameStore.player.incomingFleetAlerts) return
|
||||
const index = gameStore.player.incomingFleetAlerts.findIndex(a => a.id === missionId)
|
||||
if (index > -1) {
|
||||
gameStore.player.incomingFleetAlerts.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理NPC任务到达
|
||||
*/
|
||||
const processNPCMissionArrival = (npc: NPC, mission: FleetMission) => {
|
||||
if (mission.missionType === MissionType.Recycle) {
|
||||
// NPC回收任务到达
|
||||
const debrisId = mission.debrisFieldId
|
||||
if (!debrisId) {
|
||||
console.warn('[NPC Mission] Recycle mission missing debrisFieldId')
|
||||
mission.status = 'returning'
|
||||
mission.returnTime = Date.now() + (mission.arrivalTime - mission.departureTime)
|
||||
return
|
||||
}
|
||||
|
||||
const debrisField = universeStore.debrisFields[debrisId]
|
||||
const recycleResult = fleetLogic.processRecycleArrival(mission, debrisField)
|
||||
|
||||
if (recycleResult && debrisField) {
|
||||
if (recycleResult.remainingDebris && (recycleResult.remainingDebris.metal > 0 || recycleResult.remainingDebris.crystal > 0)) {
|
||||
// 更新残骸场
|
||||
universeStore.debrisFields[debrisId] = {
|
||||
id: debrisField.id,
|
||||
position: debrisField.position,
|
||||
resources: recycleResult.remainingDebris,
|
||||
createdAt: debrisField.createdAt
|
||||
}
|
||||
} else {
|
||||
// 残骸已被完全回收,从宇宙中删除
|
||||
delete universeStore.debrisFields[debrisId]
|
||||
}
|
||||
}
|
||||
|
||||
// 移除即将到来的警告(回收任务已到达)
|
||||
removeIncomingFleetAlertById(mission.id)
|
||||
|
||||
// 设置返回时间
|
||||
mission.returnTime = Date.now() + (mission.arrivalTime - mission.departureTime)
|
||||
return
|
||||
}
|
||||
|
||||
// 找到目标星球
|
||||
const targetKey = gameLogic.generatePositionKey(
|
||||
mission.targetPosition.galaxy,
|
||||
mission.targetPosition.system,
|
||||
mission.targetPosition.position
|
||||
)
|
||||
const targetPlanet =
|
||||
gameStore.player.planets.find(
|
||||
p =>
|
||||
p.position.galaxy === mission.targetPosition.galaxy &&
|
||||
p.position.system === mission.targetPosition.system &&
|
||||
p.position.position === mission.targetPosition.position
|
||||
) || universeStore.planets[targetKey]
|
||||
|
||||
if (!targetPlanet) {
|
||||
console.warn('[NPC Mission] Target planet not found')
|
||||
return
|
||||
}
|
||||
|
||||
if (mission.missionType === MissionType.Spy) {
|
||||
// NPC侦查到达
|
||||
const { spiedNotification, spyReport } = npcBehaviorLogic.processNPCSpyArrival(npc, mission, targetPlanet, gameStore.player)
|
||||
|
||||
// 保存侦查报告到NPC(用于后续攻击决策)
|
||||
if (!npc.playerSpyReports) {
|
||||
npc.playerSpyReports = {}
|
||||
}
|
||||
npc.playerSpyReports[targetPlanet.id] = spyReport
|
||||
|
||||
// 添加被侦查通知给玩家
|
||||
if (!gameStore.player.spiedNotifications) {
|
||||
gameStore.player.spiedNotifications = []
|
||||
}
|
||||
gameStore.player.spiedNotifications.push(spiedNotification)
|
||||
|
||||
// 移除即将到来的警告(侦查已到达)
|
||||
removeIncomingFleetAlertById(mission.id)
|
||||
} else if (mission.missionType === MissionType.Attack) {
|
||||
// NPC攻击到达 - 使用专门的NPC攻击处理逻辑
|
||||
fleetLogic.processNPCAttackArrival(npc, mission, targetPlanet, gameStore.player, gameStore.player.planets).then(attackResult => {
|
||||
if (attackResult) {
|
||||
// 添加战斗报告给玩家
|
||||
gameStore.player.battleReports.push(attackResult.battleResult)
|
||||
|
||||
// 如果生成月球,添加到玩家星球列表
|
||||
if (attackResult.moon) {
|
||||
gameStore.player.planets.push(attackResult.moon)
|
||||
}
|
||||
|
||||
// 如果生成残骸场,添加到宇宙残骸场列表
|
||||
if (attackResult.debrisField) {
|
||||
universeStore.debrisFields[attackResult.debrisField.id] = attackResult.debrisField
|
||||
}
|
||||
}
|
||||
|
||||
// 移除即将到来的警告(攻击已到达)
|
||||
removeIncomingFleetAlertById(mission.id)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理NPC任务返回
|
||||
*/
|
||||
const processNPCMissionReturn = (npc: NPC, mission: FleetMission) => {
|
||||
// 找到NPC的起始星球
|
||||
const originPlanet = npc.planets.find(p => p.id === mission.originPlanetId)
|
||||
if (!originPlanet) return
|
||||
|
||||
// 返还舰队
|
||||
shipLogic.addFleet(originPlanet.fleet, mission.fleet)
|
||||
|
||||
// 如果携带掠夺资源,给NPC添加资源
|
||||
if (mission.cargo) {
|
||||
originPlanet.resources.metal += mission.cargo.metal
|
||||
originPlanet.resources.crystal += mission.cargo.crystal
|
||||
originPlanet.resources.deuterium += mission.cargo.deuterium
|
||||
}
|
||||
|
||||
// 从NPC任务列表中移除
|
||||
if (npc.fleetMissions) {
|
||||
const missionIndex = npc.fleetMissions.indexOf(mission)
|
||||
if (missionIndex > -1) {
|
||||
npc.fleetMissions.splice(missionIndex, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NPC成长系统更新
|
||||
let npcUpdateCounter = 0
|
||||
const NPC_UPDATE_INTERVAL = 10
|
||||
|
||||
/**
|
||||
* 更新NPC成长系统
|
||||
*/
|
||||
const updateNPCGrowth = (deltaSeconds: number) => {
|
||||
// 累积时间
|
||||
npcUpdateCounter += deltaSeconds
|
||||
|
||||
// 只在达到更新间隔时才执行
|
||||
if (npcUpdateCounter < NPC_UPDATE_INTERVAL) {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取所有星球
|
||||
const allPlanets = Object.values(universeStore.planets)
|
||||
|
||||
// 如果NPC store为空,从星球数据中初始化NPC
|
||||
if (npcStore.npcs.length === 0) {
|
||||
const npcMap = new Map<string, any>()
|
||||
|
||||
allPlanets.forEach(planet => {
|
||||
// 跳过玩家的星球
|
||||
if (planet.ownerId === gameStore.player.id || !planet.ownerId) return
|
||||
|
||||
// 这是NPC的星球
|
||||
if (!npcMap.has(planet.ownerId)) {
|
||||
npcMap.set(planet.ownerId, {
|
||||
id: planet.ownerId,
|
||||
name: `NPC-${planet.ownerId.substring(0, 8)}`,
|
||||
planets: [],
|
||||
technologies: {},
|
||||
difficulty: 'medium' as const,
|
||||
relations: {},
|
||||
allies: [],
|
||||
enemies: []
|
||||
})
|
||||
}
|
||||
|
||||
npcMap.get(planet.ownerId)!.planets.push(planet)
|
||||
})
|
||||
|
||||
// 保存到store
|
||||
npcStore.npcs = Array.from(npcMap.values())
|
||||
|
||||
// 如果有NPC,基于玩家实力初始化NPC
|
||||
if (npcStore.npcs.length > 0) {
|
||||
const gameState: npcGrowthLogic.NPCGrowthGameState = {
|
||||
planets: allPlanets,
|
||||
player: gameStore.player,
|
||||
npcs: npcStore.npcs
|
||||
}
|
||||
|
||||
const playerPower = npcGrowthLogic.calculatePlayerAveragePower(gameState)
|
||||
|
||||
npcStore.npcs.forEach(npc => {
|
||||
npcGrowthLogic.initializeNPCStartingPower(npc, playerPower)
|
||||
})
|
||||
|
||||
// 初始化NPC之间的外交关系(盟友/敌人)
|
||||
npcGrowthLogic.initializeNPCDiplomacy(npcStore.npcs)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有NPC,直接返回
|
||||
if (npcStore.npcs.length === 0) {
|
||||
npcUpdateCounter = 0
|
||||
return
|
||||
}
|
||||
|
||||
// 构建游戏状态
|
||||
const gameState: npcGrowthLogic.NPCGrowthGameState = {
|
||||
planets: allPlanets,
|
||||
player: gameStore.player,
|
||||
npcs: npcStore.npcs
|
||||
}
|
||||
|
||||
// 使用累积的时间更新每个NPC
|
||||
npcStore.npcs.forEach(npc => {
|
||||
npcGrowthLogic.updateNPCGrowth(npc, gameState, npcUpdateCounter)
|
||||
})
|
||||
|
||||
// 重置计数器
|
||||
npcUpdateCounter = 0
|
||||
}
|
||||
|
||||
// NPC行为系统更新
|
||||
let npcBehaviorCounter = 0
|
||||
const NPC_BEHAVIOR_INTERVAL = 5
|
||||
|
||||
/**
|
||||
* 更新NPC行为系统
|
||||
*/
|
||||
const updateNPCBehavior = (deltaSeconds: number) => {
|
||||
// 累积时间
|
||||
npcBehaviorCounter += deltaSeconds
|
||||
|
||||
// 只在达到更新间隔时才执行
|
||||
if (npcBehaviorCounter < NPC_BEHAVIOR_INTERVAL) {
|
||||
return
|
||||
}
|
||||
|
||||
// 如果没有NPC,直接返回
|
||||
if (npcStore.npcs.length === 0) {
|
||||
npcBehaviorCounter = 0
|
||||
return
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
const allPlanets = Object.values(universeStore.planets)
|
||||
|
||||
// 更新每个NPC的行为
|
||||
npcStore.npcs.forEach(npc => {
|
||||
npcBehaviorLogic.updateNPCBehavior(npc, gameStore.player, allPlanets, universeStore.debrisFields, now)
|
||||
})
|
||||
|
||||
npcBehaviorCounter = 0
|
||||
}
|
||||
|
||||
return {
|
||||
processNPCMissionArrival,
|
||||
processNPCMissionReturn,
|
||||
removeIncomingFleetAlert,
|
||||
removeIncomingFleetAlertById,
|
||||
updateNPCGrowth,
|
||||
updateNPCBehavior
|
||||
}
|
||||
}
|
||||
103
src/composables/useQueueHandler.ts
Normal file
103
src/composables/useQueueHandler.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import type { Ref } from 'vue'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import type { BuildQueueItem } from '@/types/game'
|
||||
import * as buildingValidation from '@/logic/buildingValidation'
|
||||
import * as resourceLogic from '@/logic/resourceLogic'
|
||||
import * as researchValidation from '@/logic/researchValidation'
|
||||
|
||||
/**
|
||||
* 队列处理
|
||||
* 处理建造队列和研究队列的取消操作
|
||||
*/
|
||||
export const useQueueHandler = (
|
||||
t: (key: string) => string,
|
||||
confirmDialogOpen: Ref<boolean>,
|
||||
confirmDialogTitle: Ref<string>,
|
||||
confirmDialogMessage: Ref<string>,
|
||||
confirmDialogAction: Ref<(() => void) | null>
|
||||
) => {
|
||||
const gameStore = useGameStore()
|
||||
|
||||
/**
|
||||
* 取消建造
|
||||
*/
|
||||
const handleCancelBuild = (queueId: string) => {
|
||||
confirmDialogTitle.value = t('queue.cancelBuild')
|
||||
confirmDialogMessage.value = t('queue.confirmCancel')
|
||||
confirmDialogAction.value = () => {
|
||||
if (!gameStore.currentPlanet) return false
|
||||
const { item, index } = buildingValidation.findQueueItem(gameStore.currentPlanet.buildQueue, queueId)
|
||||
if (!item) return false
|
||||
if (item.type === 'building') {
|
||||
const refund = buildingValidation.cancelBuildingUpgrade(gameStore.currentPlanet, item)
|
||||
resourceLogic.addResources(gameStore.currentPlanet.resources, refund)
|
||||
}
|
||||
gameStore.currentPlanet.buildQueue.splice(index, 1)
|
||||
return true
|
||||
}
|
||||
confirmDialogOpen.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消研究
|
||||
*/
|
||||
const handleCancelResearch = (queueId: string) => {
|
||||
confirmDialogTitle.value = t('queue.cancelResearch')
|
||||
confirmDialogMessage.value = t('queue.confirmCancel')
|
||||
confirmDialogAction.value = () => {
|
||||
if (!gameStore.currentPlanet) return false
|
||||
const { item, index } = buildingValidation.findQueueItem(gameStore.player.researchQueue, queueId)
|
||||
if (!item) return false
|
||||
if (item.type === 'technology') {
|
||||
const refund = researchValidation.cancelTechnologyResearch(item)
|
||||
resourceLogic.addResources(gameStore.currentPlanet.resources, refund)
|
||||
}
|
||||
gameStore.player.researchQueue.splice(index, 1)
|
||||
return true
|
||||
}
|
||||
confirmDialogOpen.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取队列项名称
|
||||
*/
|
||||
const getItemName = (item: BuildQueueItem): string => {
|
||||
if (item.type === 'building' || item.type === 'demolish') {
|
||||
const buildingName = t(`buildings.${item.itemType}`)
|
||||
return item.type === 'demolish' ? `${t('buildingsView.demolish')} - ${buildingName}` : buildingName
|
||||
} else if (item.type === 'technology') {
|
||||
return t(`technologies.${item.itemType}`)
|
||||
} else if (item.type === 'ship') {
|
||||
return t(`ships.${item.itemType}`)
|
||||
} else if (item.type === 'defense') {
|
||||
return t(`defenses.${item.itemType}`)
|
||||
}
|
||||
return t('common.unknown')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取剩余时间(秒)
|
||||
*/
|
||||
const getRemainingTime = (item: BuildQueueItem): number => {
|
||||
const now = Date.now()
|
||||
return Math.max(0, Math.floor((item.endTime - now) / 1000))
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取队列进度(百分比)
|
||||
*/
|
||||
const getQueueProgress = (item: BuildQueueItem): number => {
|
||||
const now = Date.now()
|
||||
const total = item.endTime - item.startTime
|
||||
const elapsed = now - item.startTime
|
||||
return Math.min(100, Math.max(0, (elapsed / total) * 100))
|
||||
}
|
||||
|
||||
return {
|
||||
handleCancelBuild,
|
||||
handleCancelResearch,
|
||||
getItemName,
|
||||
getRemainingTime,
|
||||
getQueueProgress
|
||||
}
|
||||
}
|
||||
@@ -65,6 +65,23 @@ export const BUILDINGS: Record<BuildingType, BuildingConfig> = {
|
||||
35: { [BuildingType.NaniteFactory]: 1, [BuildingType.ResearchLab]: 10 }
|
||||
}
|
||||
},
|
||||
[BuildingType.FusionReactor]: {
|
||||
id: BuildingType.FusionReactor,
|
||||
name: '核聚变反应堆',
|
||||
description: '使用重氢产生大量能源',
|
||||
baseCost: { metal: 900, crystal: 360, deuterium: 180, darkMatter: 0, energy: 0 },
|
||||
baseTime: 30,
|
||||
costMultiplier: 1.8,
|
||||
spaceUsage: 4,
|
||||
requirements: {
|
||||
[TechnologyType.EnergyTechnology]: 3,
|
||||
[BuildingType.DeuteriumSynthesizer]: 5
|
||||
},
|
||||
levelRequirements: {
|
||||
10: { [BuildingType.RoboticsFactory]: 5, [TechnologyType.EnergyTechnology]: 6 },
|
||||
20: { [BuildingType.RoboticsFactory]: 8, [TechnologyType.EnergyTechnology]: 10, [BuildingType.NaniteFactory]: 2 }
|
||||
}
|
||||
},
|
||||
[BuildingType.RoboticsFactory]: {
|
||||
id: BuildingType.RoboticsFactory,
|
||||
name: '机器人工厂',
|
||||
@@ -80,8 +97,7 @@ export const BUILDINGS: Record<BuildingType, BuildingConfig> = {
|
||||
},
|
||||
levelRequirements: {
|
||||
5: { [BuildingType.ResearchLab]: 3, [BuildingType.SolarPlant]: 8 },
|
||||
8: { [BuildingType.ResearchLab]: 6, [BuildingType.SolarPlant]: 12, [BuildingType.MetalMine]: 12, [BuildingType.CrystalMine]: 12 },
|
||||
10: { [BuildingType.ResearchLab]: 8, [BuildingType.NaniteFactory]: 1 }
|
||||
8: { [BuildingType.ResearchLab]: 6, [BuildingType.SolarPlant]: 12, [BuildingType.MetalMine]: 12, [BuildingType.CrystalMine]: 12 }
|
||||
}
|
||||
},
|
||||
[BuildingType.NaniteFactory]: {
|
||||
@@ -197,6 +213,41 @@ export const BUILDINGS: Record<BuildingType, BuildingConfig> = {
|
||||
8: { [BuildingType.ResearchLab]: 10, [TechnologyType.DarkMatterTechnology]: 5, [BuildingType.NaniteFactory]: 2 }
|
||||
}
|
||||
},
|
||||
[BuildingType.DarkMatterTank]: {
|
||||
id: BuildingType.DarkMatterTank,
|
||||
name: '暗物质储罐',
|
||||
description: '增加暗物质存储上限',
|
||||
baseCost: { metal: 10000, crystal: 10000, deuterium: 5000, darkMatter: 0, energy: 0 },
|
||||
baseTime: 20,
|
||||
costMultiplier: 2,
|
||||
spaceUsage: 2,
|
||||
planetOnly: true,
|
||||
requirements: {
|
||||
[BuildingType.DarkMatterCollector]: 2,
|
||||
[TechnologyType.DarkMatterTechnology]: 1
|
||||
},
|
||||
levelRequirements: {
|
||||
8: { [BuildingType.DarkMatterCollector]: 8, [BuildingType.RoboticsFactory]: 3 },
|
||||
12: { [BuildingType.DarkMatterCollector]: 15, [BuildingType.RoboticsFactory]: 6, [TechnologyType.DarkMatterTechnology]: 3 }
|
||||
}
|
||||
},
|
||||
[BuildingType.MissileSilo]: {
|
||||
id: BuildingType.MissileSilo,
|
||||
name: '导弹发射井',
|
||||
description: '存储和发射导弹,每级可存储10枚导弹',
|
||||
baseCost: { metal: 20000, crystal: 20000, deuterium: 1000, darkMatter: 0, energy: 0 },
|
||||
baseTime: 45,
|
||||
costMultiplier: 2,
|
||||
spaceUsage: 5,
|
||||
maxLevel: 10,
|
||||
requirements: {
|
||||
[BuildingType.Shipyard]: 1
|
||||
},
|
||||
levelRequirements: {
|
||||
5: { [BuildingType.Shipyard]: 5, [TechnologyType.ComputerTechnology]: 3 },
|
||||
8: { [BuildingType.Shipyard]: 8, [TechnologyType.ComputerTechnology]: 6, [BuildingType.NaniteFactory]: 2 }
|
||||
}
|
||||
},
|
||||
[BuildingType.Terraformer]: {
|
||||
id: BuildingType.Terraformer,
|
||||
name: '地形改造器',
|
||||
@@ -250,7 +301,7 @@ export const BUILDINGS: Record<BuildingType, BuildingConfig> = {
|
||||
id: BuildingType.JumpGate,
|
||||
name: '跳跃门',
|
||||
description: '瞬间传送舰队到其他月球',
|
||||
baseCost: { metal: 2000000, crystal: 4000000, deuterium: 2000000, darkMatter: 0, energy: 0 },
|
||||
baseCost: { metal: 2000000, crystal: 4000000, deuterium: 2000000, darkMatter: 50000, energy: 0 },
|
||||
baseTime: 240, // 减少建造时间:300→240秒
|
||||
costMultiplier: 2,
|
||||
spaceUsage: 10,
|
||||
@@ -269,7 +320,7 @@ export const BUILDINGS: Record<BuildingType, BuildingConfig> = {
|
||||
id: BuildingType.PlanetDestroyerFactory,
|
||||
name: '行星毁灭者工厂',
|
||||
description: '建造能够摧毁行星的终极武器',
|
||||
baseCost: { metal: 5000000, crystal: 4000000, deuterium: 1000000, darkMatter: 0, energy: 0 },
|
||||
baseCost: { metal: 5000000, crystal: 4000000, deuterium: 1000000, darkMatter: 100000, energy: 0 },
|
||||
baseTime: 300,
|
||||
costMultiplier: 2,
|
||||
spaceUsage: 15,
|
||||
@@ -297,7 +348,7 @@ export const TECHNOLOGIES: Record<TechnologyType, TechnologyConfig> = {
|
||||
[TechnologyType.EnergyTechnology]: {
|
||||
id: TechnologyType.EnergyTechnology,
|
||||
name: '能源技术',
|
||||
description: '提高能源利用效率',
|
||||
description: '加快研究速度',
|
||||
baseCost: { metal: 0, crystal: 800, deuterium: 400, darkMatter: 0, energy: 0 },
|
||||
baseTime: 30, // 减少研究时间:60→30秒
|
||||
costMultiplier: 2,
|
||||
@@ -391,6 +442,87 @@ export const TECHNOLOGIES: Record<TechnologyType, TechnologyConfig> = {
|
||||
8: { [BuildingType.ResearchLab]: 10, [BuildingType.NaniteFactory]: 2 }
|
||||
}
|
||||
},
|
||||
[TechnologyType.EspionageTechnology]: {
|
||||
id: TechnologyType.EspionageTechnology,
|
||||
name: '间谍技术',
|
||||
description: '提高间谍探测效果,每级提高1级侦查深度',
|
||||
baseCost: { metal: 200, crystal: 1000, deuterium: 200, darkMatter: 0, energy: 0 },
|
||||
baseTime: 60,
|
||||
costMultiplier: 2,
|
||||
requirements: { [BuildingType.ResearchLab]: 3 },
|
||||
levelRequirements: {
|
||||
5: { [BuildingType.ResearchLab]: 6, [TechnologyType.ComputerTechnology]: 3 },
|
||||
8: { [BuildingType.ResearchLab]: 8, [TechnologyType.ComputerTechnology]: 5 }
|
||||
}
|
||||
},
|
||||
[TechnologyType.WeaponsTechnology]: {
|
||||
id: TechnologyType.WeaponsTechnology,
|
||||
name: '武器技术',
|
||||
description: '提高舰船和防御的攻击力,每级+10%',
|
||||
baseCost: { metal: 800, crystal: 200, deuterium: 0, darkMatter: 0, energy: 0 },
|
||||
baseTime: 60,
|
||||
costMultiplier: 2,
|
||||
requirements: { [BuildingType.ResearchLab]: 4 },
|
||||
levelRequirements: {
|
||||
5: { [BuildingType.ResearchLab]: 7, [BuildingType.Shipyard]: 4 },
|
||||
10: { [BuildingType.ResearchLab]: 10, [BuildingType.Shipyard]: 8, [BuildingType.NaniteFactory]: 2 }
|
||||
}
|
||||
},
|
||||
[TechnologyType.ShieldingTechnology]: {
|
||||
id: TechnologyType.ShieldingTechnology,
|
||||
name: '护盾技术',
|
||||
description: '提高舰船和防御的护盾值,每级+10%',
|
||||
baseCost: { metal: 200, crystal: 600, deuterium: 0, darkMatter: 0, energy: 0 },
|
||||
baseTime: 60,
|
||||
costMultiplier: 2,
|
||||
requirements: { [BuildingType.ResearchLab]: 6, [TechnologyType.EnergyTechnology]: 3 },
|
||||
levelRequirements: {
|
||||
5: { [BuildingType.ResearchLab]: 8, [TechnologyType.EnergyTechnology]: 6 },
|
||||
10: { [BuildingType.ResearchLab]: 10, [TechnologyType.EnergyTechnology]: 10, [BuildingType.NaniteFactory]: 2 }
|
||||
}
|
||||
},
|
||||
[TechnologyType.ArmourTechnology]: {
|
||||
id: TechnologyType.ArmourTechnology,
|
||||
name: '装甲技术',
|
||||
description: '提高舰船和防御的装甲值,每级+10%',
|
||||
baseCost: { metal: 1000, crystal: 0, deuterium: 0, darkMatter: 0, energy: 0 },
|
||||
baseTime: 60,
|
||||
costMultiplier: 2,
|
||||
requirements: { [BuildingType.ResearchLab]: 2 },
|
||||
levelRequirements: {
|
||||
5: { [BuildingType.ResearchLab]: 6, [BuildingType.Shipyard]: 3 },
|
||||
10: { [BuildingType.ResearchLab]: 10, [BuildingType.Shipyard]: 7, [BuildingType.NaniteFactory]: 1 }
|
||||
}
|
||||
},
|
||||
[TechnologyType.Astrophysics]: {
|
||||
id: TechnologyType.Astrophysics,
|
||||
name: '天体物理学',
|
||||
description: '每级增加1个殖民地槽位,增加探险成功率',
|
||||
baseCost: { metal: 4000, crystal: 8000, deuterium: 4000, darkMatter: 0, energy: 0 },
|
||||
baseTime: 60,
|
||||
costMultiplier: 1.75,
|
||||
requirements: {
|
||||
[BuildingType.ResearchLab]: 3,
|
||||
[TechnologyType.EspionageTechnology]: 4,
|
||||
[TechnologyType.ImpulseDrive]: 3
|
||||
},
|
||||
levelRequirements: {
|
||||
5: { [BuildingType.ResearchLab]: 8, [TechnologyType.EspionageTechnology]: 8 },
|
||||
10: { [BuildingType.ResearchLab]: 12, [TechnologyType.HyperspaceTechnology]: 5, [BuildingType.NaniteFactory]: 3 }
|
||||
}
|
||||
},
|
||||
[TechnologyType.GravitonTechnology]: {
|
||||
id: TechnologyType.GravitonTechnology,
|
||||
name: '引力技术',
|
||||
description: '研究引力操纵,死星的必要技术',
|
||||
baseCost: { metal: 0, crystal: 0, deuterium: 0, darkMatter: 300000, energy: 0 },
|
||||
baseTime: 0,
|
||||
costMultiplier: 3,
|
||||
maxLevel: 1, // 只有1级
|
||||
requirements: {
|
||||
[BuildingType.ResearchLab]: 12
|
||||
}
|
||||
},
|
||||
[TechnologyType.CombustionDrive]: {
|
||||
id: TechnologyType.CombustionDrive,
|
||||
name: '燃烧引擎',
|
||||
@@ -470,7 +602,7 @@ export const TECHNOLOGIES: Record<TechnologyType, TechnologyConfig> = {
|
||||
id: TechnologyType.PlanetDestructionTech,
|
||||
name: '行星毁灭技术',
|
||||
description: '研究如何摧毁整个行星的恐怖技术',
|
||||
baseCost: { metal: 4000000, crystal: 8000000, deuterium: 4000000, darkMatter: 0, energy: 0 },
|
||||
baseCost: { metal: 4000000, crystal: 8000000, deuterium: 4000000, darkMatter: 200000, energy: 0 },
|
||||
baseTime: 300,
|
||||
costMultiplier: 2,
|
||||
maxLevel: 5, // 最多5级
|
||||
@@ -554,6 +686,64 @@ export const SHIPS: Record<ShipType, ShipConfig> = {
|
||||
storageUsage: 25,
|
||||
requirements: { [BuildingType.Shipyard]: 7, [TechnologyType.HyperspaceDrive]: 4 }
|
||||
},
|
||||
[ShipType.Battlecruiser]: {
|
||||
id: ShipType.Battlecruiser,
|
||||
name: '战列巡洋舰',
|
||||
description: '快速强大的战斗舰船,擅长攻击战列舰',
|
||||
cost: { metal: 30000, crystal: 40000, deuterium: 15000, darkMatter: 0, energy: 0 },
|
||||
buildTime: 70,
|
||||
cargoCapacity: 750,
|
||||
attack: 700,
|
||||
shield: 400,
|
||||
armor: 7000,
|
||||
speed: 10000,
|
||||
fuelConsumption: 250,
|
||||
storageUsage: 20,
|
||||
requirements: {
|
||||
[BuildingType.Shipyard]: 8,
|
||||
[TechnologyType.HyperspaceDrive]: 5,
|
||||
[TechnologyType.HyperspaceTechnology]: 5,
|
||||
[TechnologyType.LaserTechnology]: 12
|
||||
}
|
||||
},
|
||||
[ShipType.Bomber]: {
|
||||
id: ShipType.Bomber,
|
||||
name: '轰炸机',
|
||||
description: '专门对付防御设施的轰炸舰',
|
||||
cost: { metal: 50000, crystal: 25000, deuterium: 15000, darkMatter: 0, energy: 0 },
|
||||
buildTime: 100,
|
||||
cargoCapacity: 500,
|
||||
attack: 1000,
|
||||
shield: 500,
|
||||
armor: 7500,
|
||||
speed: 4000,
|
||||
fuelConsumption: 700,
|
||||
storageUsage: 35,
|
||||
requirements: {
|
||||
[BuildingType.Shipyard]: 8,
|
||||
[TechnologyType.ImpulseDrive]: 6,
|
||||
[TechnologyType.PlasmaTechnology]: 5
|
||||
}
|
||||
},
|
||||
[ShipType.Destroyer]: {
|
||||
id: ShipType.Destroyer,
|
||||
name: '驱逐舰',
|
||||
description: '擅长摧毁大型舰船的猎杀者',
|
||||
cost: { metal: 60000, crystal: 50000, deuterium: 15000, darkMatter: 0, energy: 0 },
|
||||
buildTime: 120,
|
||||
cargoCapacity: 2000,
|
||||
attack: 2000,
|
||||
shield: 500,
|
||||
armor: 11000,
|
||||
speed: 5000,
|
||||
fuelConsumption: 1000,
|
||||
storageUsage: 40,
|
||||
requirements: {
|
||||
[BuildingType.Shipyard]: 9,
|
||||
[TechnologyType.HyperspaceDrive]: 6,
|
||||
[TechnologyType.HyperspaceTechnology]: 5
|
||||
}
|
||||
},
|
||||
[ShipType.SmallCargo]: {
|
||||
id: ShipType.SmallCargo,
|
||||
name: '小型运输船',
|
||||
@@ -629,6 +819,21 @@ export const SHIPS: Record<ShipType, ShipConfig> = {
|
||||
storageUsage: 2,
|
||||
requirements: { [BuildingType.Shipyard]: 3, [TechnologyType.CombustionDrive]: 3 }
|
||||
},
|
||||
[ShipType.SolarSatellite]: {
|
||||
id: ShipType.SolarSatellite,
|
||||
name: '太阳能卫星',
|
||||
description: '提供额外能源,每个产生50点能量',
|
||||
cost: { metal: 0, crystal: 2000, deuterium: 500, darkMatter: 0, energy: 0 },
|
||||
buildTime: 10,
|
||||
cargoCapacity: 0,
|
||||
attack: 1,
|
||||
shield: 1,
|
||||
armor: 200,
|
||||
speed: 0,
|
||||
fuelConsumption: 0,
|
||||
storageUsage: 1,
|
||||
requirements: { [BuildingType.Shipyard]: 1 }
|
||||
},
|
||||
[ShipType.DarkMatterHarvester]: {
|
||||
id: ShipType.DarkMatterHarvester,
|
||||
name: '暗物质采集船',
|
||||
@@ -652,7 +857,7 @@ export const SHIPS: Record<ShipType, ShipConfig> = {
|
||||
id: ShipType.Deathstar,
|
||||
name: '死星',
|
||||
description: '终极武器,能够摧毁整个行星',
|
||||
cost: { metal: 5000000, crystal: 4000000, deuterium: 1000000, darkMatter: 0, energy: 0 },
|
||||
cost: { metal: 5000000, crystal: 4000000, deuterium: 1000000, darkMatter: 50000, energy: 0 },
|
||||
buildTime: 600,
|
||||
cargoCapacity: 1000000,
|
||||
attack: 200000,
|
||||
@@ -763,7 +968,7 @@ export const DEFENSES: Record<DefenseType, DefenseConfig> = {
|
||||
id: DefenseType.PlanetaryShield,
|
||||
name: '行星护盾',
|
||||
description: '保护行星免受毁灭攻击的超级护盾',
|
||||
cost: { metal: 2000000, crystal: 2000000, deuterium: 1000000, darkMatter: 0, energy: 0 },
|
||||
cost: { metal: 2000000, crystal: 2000000, deuterium: 1000000, darkMatter: 50000, energy: 0 },
|
||||
buildTime: 180,
|
||||
attack: 1,
|
||||
shield: 100000,
|
||||
@@ -773,6 +978,33 @@ export const DEFENSES: Record<DefenseType, DefenseConfig> = {
|
||||
[TechnologyType.EnergyTechnology]: 10,
|
||||
[TechnologyType.HyperspaceTechnology]: 8
|
||||
}
|
||||
},
|
||||
[DefenseType.AntiBallisticMissile]: {
|
||||
id: DefenseType.AntiBallisticMissile,
|
||||
name: '反弹道导弹',
|
||||
description: '拦截敌方导弹,每个可拦截1枚星际导弹',
|
||||
cost: { metal: 8000, crystal: 0, deuterium: 2000, darkMatter: 0, energy: 0 },
|
||||
buildTime: 20,
|
||||
attack: 1,
|
||||
shield: 1,
|
||||
armor: 800,
|
||||
requirements: {
|
||||
[BuildingType.MissileSilo]: 2
|
||||
}
|
||||
},
|
||||
[DefenseType.InterplanetaryMissile]: {
|
||||
id: DefenseType.InterplanetaryMissile,
|
||||
name: '星际导弹',
|
||||
description: '可以攻击其他星球的防御设施,射程取决于脉冲引擎等级',
|
||||
cost: { metal: 12500, crystal: 2500, deuterium: 10000, darkMatter: 0, energy: 0 },
|
||||
buildTime: 30,
|
||||
attack: 12000,
|
||||
shield: 1,
|
||||
armor: 1500,
|
||||
requirements: {
|
||||
[BuildingType.MissileSilo]: 4,
|
||||
[TechnologyType.ImpulseDrive]: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -782,8 +1014,8 @@ export const OFFICERS: Record<OfficerType, OfficerConfig> = {
|
||||
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 },
|
||||
cost: { metal: 0, crystal: 50000, deuterium: 25000, darkMatter: 7000, energy: 0 },
|
||||
weeklyMaintenance: { metal: 0, crystal: 5000, deuterium: 2500, darkMatter: 900, energy: 0 },
|
||||
benefits: {
|
||||
buildingSpeedBonus: 10, // 建筑速度 +10%
|
||||
additionalBuildQueue: 1, // 额外1个建筑队列
|
||||
@@ -794,8 +1026,8 @@ export const OFFICERS: Record<OfficerType, OfficerConfig> = {
|
||||
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 },
|
||||
cost: { metal: 50000, crystal: 25000, deuterium: 0, darkMatter: 7000, energy: 0 },
|
||||
weeklyMaintenance: { metal: 5000, crystal: 2500, deuterium: 0, darkMatter: 900, energy: 0 },
|
||||
benefits: {
|
||||
additionalFleetSlots: 2, // 额外2个舰队槽位
|
||||
fleetSpeedBonus: 10, // 舰队速度 +10%
|
||||
@@ -806,8 +1038,8 @@ export const OFFICERS: Record<OfficerType, OfficerConfig> = {
|
||||
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 },
|
||||
cost: { metal: 40000, crystal: 20000, deuterium: 10000, darkMatter: 7000, energy: 0 },
|
||||
weeklyMaintenance: { metal: 4000, crystal: 2000, deuterium: 1000, darkMatter: 900, energy: 0 },
|
||||
benefits: {
|
||||
defenseBonus: 15, // 防御力 +15%
|
||||
energyProductionBonus: 10, // 电量产出 +10%
|
||||
@@ -818,8 +1050,8 @@ export const OFFICERS: Record<OfficerType, OfficerConfig> = {
|
||||
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 },
|
||||
cost: { metal: 30000, crystal: 30000, deuterium: 20000, darkMatter: 7000, energy: 0 },
|
||||
weeklyMaintenance: { metal: 3000, crystal: 3000, deuterium: 2000, darkMatter: 900, energy: 0 },
|
||||
benefits: {
|
||||
resourceProductionBonus: 15, // 资源产量 +15%
|
||||
storageCapacityBonus: 10 // 仓储容量 +10%
|
||||
@@ -829,8 +1061,8 @@ export const OFFICERS: Record<OfficerType, OfficerConfig> = {
|
||||
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 },
|
||||
cost: { metal: 20000, crystal: 40000, deuterium: 20000, darkMatter: 7000, energy: 0 },
|
||||
weeklyMaintenance: { metal: 2000, crystal: 4000, deuterium: 2000, darkMatter: 900, energy: 0 },
|
||||
benefits: {
|
||||
researchSpeedBonus: 15 // 研究速度 +15%
|
||||
}
|
||||
@@ -870,3 +1102,81 @@ export const FLEET_STORAGE_CONFIG = {
|
||||
shipyardBonus: 1000, // 每级造船厂增加的仓储
|
||||
computerTechBonus: 500 // 每级计算机技术全局增加的仓储
|
||||
}
|
||||
|
||||
// 外交系统配置
|
||||
export const DIPLOMATIC_CONFIG = {
|
||||
// 好感度范围
|
||||
MIN_REPUTATION: -100,
|
||||
MAX_REPUTATION: 100,
|
||||
|
||||
// 关系状态阈值
|
||||
HOSTILE_THRESHOLD: -20, // 低于此值为敌对
|
||||
FRIENDLY_THRESHOLD: 20, // 高于此值为友好
|
||||
|
||||
// 各种行为的好感度变化值
|
||||
REPUTATION_CHANGES: {
|
||||
// 赠送资源(基于资源价值计算)
|
||||
GIFT_BASE: 0, // 基础好感度(移除固定奖励,完全基于资源量)
|
||||
GIFT_PER_1K_RESOURCES: 2, // 每1000资源价值增加2点(提高权重)
|
||||
GIFT_MIN_VALUE: 500, // 最小资源价值门槛(低于此值不增加好感度)
|
||||
GIFT_MAX_SINGLE: 20, // 单次赠送最大好感度增加
|
||||
|
||||
// 负面行为
|
||||
ATTACK: -15, // 攻击一次
|
||||
ATTACK_WIN: -25, // 攻击并获胜
|
||||
ATTACK_DESTROY_PLANET: -50, // 摧毁星球
|
||||
SPY_DETECTED: -5, // 侦查被发现
|
||||
SPY_UNDETECTED: -2, // 侦查未被发现
|
||||
STEAL_DEBRIS: -10, // 抢夺残骸(在NPC星球位置)
|
||||
DESTROY_FLEET: -3, // 每摧毁1单位战力扣除好感度
|
||||
|
||||
// 正面行为
|
||||
HELP_ATTACK_ENEMY: 15, // 帮助攻击NPC的敌人
|
||||
LONG_PEACE_DECAY: 1, // 长期不攻击的友好衰减(每周+1)
|
||||
TRADE: 5, // 贸易(未来功能预留)
|
||||
|
||||
// 关系网络影响
|
||||
ALLY_ATTACKED: -10, // 攻击盟友导致的好感度降低
|
||||
ALLY_HELPED: 5 // 帮助盟友导致的好感度增加
|
||||
},
|
||||
|
||||
// 好感度自然变化
|
||||
NATURAL_DECAY: {
|
||||
ENABLED: true,
|
||||
INTERVAL: 7 * 24 * 3600, // 7天(秒)
|
||||
TOWARDS_NEUTRAL_RATE: 2 // 每周向中立值回归2点
|
||||
},
|
||||
|
||||
// 基于关系的行为调整
|
||||
BEHAVIOR_MODIFIERS: {
|
||||
HOSTILE_ATTACK_MULTIPLIER: 2.0, // 敌对状态攻击频率翻倍
|
||||
HOSTILE_SPY_MULTIPLIER: 1.5, // 敌对状态侦查频率提高50%
|
||||
FRIENDLY_ATTACK_PROBABILITY: 0, // 友好状态不攻击
|
||||
FRIENDLY_SPY_PROBABILITY: 0.5, // 友好状态侦查概率降低到50%
|
||||
NEUTRAL_ATTACK_PROBABILITY: 1.0, // 中立状态正常攻击概率
|
||||
NEUTRAL_SPY_PROBABILITY: 1.0 // 中立状态正常侦查概率
|
||||
},
|
||||
|
||||
// NPC主动赠送资源配置
|
||||
NPC_GIFT_CONFIG: {
|
||||
ENABLED: true,
|
||||
MIN_REPUTATION: 60, // NPC对玩家好感度≥60才会赠送
|
||||
CHECK_INTERVAL: 24 * 3600, // 每天检查一次(秒)
|
||||
GIFT_PROBABILITY: 0.05, // 5%概率赠送
|
||||
GIFT_AMOUNT: {
|
||||
METAL: { min: 1000, max: 5000 },
|
||||
CRYSTAL: { min: 500, max: 2500 },
|
||||
DEUTERIUM: { min: 200, max: 1000 }
|
||||
}
|
||||
},
|
||||
|
||||
// 礼物接受/拒绝配置
|
||||
GIFT_ACCEPTANCE_CONFIG: {
|
||||
NPC_REJECTION_BASE_PROBABILITY: 0.3, // NPC拒绝礼物的基础概率(30%)
|
||||
NPC_REJECTION_REPUTATION_MODIFIER: 0.01, // 好感度每降低1点,拒绝概率增加1%
|
||||
MIN_REJECTION_PROBABILITY: 0.05, // 最小拒绝概率(5%,即使关系很好)
|
||||
MAX_REJECTION_PROBABILITY: 0.8, // 最大拒绝概率(80%,即使关系很差)
|
||||
GIFT_EXPIRATION_DAYS: 7, // 礼物通知过期天数
|
||||
REJECTION_REPUTATION_PENALTY: -5 // 拒绝礼物导致的好感度降低
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ export default {
|
||||
officers: 'Offiziere',
|
||||
simulator: 'Simulator',
|
||||
galaxy: 'Galaxie',
|
||||
diplomacy: 'Diplomacy',
|
||||
messages: 'Nachrichten',
|
||||
settings: 'Einstellungen',
|
||||
gm: 'GM'
|
||||
@@ -97,6 +98,8 @@ export default {
|
||||
coordinates: 'Koordinaten',
|
||||
switchToMoon: 'Zum Mond',
|
||||
backToPlanet: 'Zurück zum Planeten',
|
||||
switchPlanet: 'Planet wechseln',
|
||||
currentPlanet: 'Aktueller Planet',
|
||||
fields: 'Felder',
|
||||
temperature: 'Temperatur',
|
||||
homePlanet: 'Heimatplanet',
|
||||
@@ -112,6 +115,7 @@ export default {
|
||||
crystalMine: 'Kristallmine',
|
||||
deuteriumSynthesizer: 'Deuterium-Synthesizer',
|
||||
solarPlant: 'Solarkraftwerk',
|
||||
fusionReactor: 'Fusionsreaktor',
|
||||
roboticsFactory: 'Roboterfabrik',
|
||||
naniteFactory: 'Nanitenfabrik',
|
||||
shipyard: 'Raumschiffwerft',
|
||||
@@ -120,6 +124,8 @@ export default {
|
||||
crystalStorage: 'Kristallspeicher',
|
||||
deuteriumTank: 'Deuteriumtank',
|
||||
darkMatterCollector: 'Dunkle-Materie-Kollektor',
|
||||
darkMatterTank: 'Dunkle-Materie-Tank',
|
||||
missileSilo: 'Raketensilo',
|
||||
terraformer: 'Terraformer',
|
||||
lunarBase: 'Mondbasis',
|
||||
sensorPhalanx: 'Sensorphalanx',
|
||||
@@ -130,13 +136,26 @@ export default {
|
||||
consumption: 'Verbrauch',
|
||||
totalCost: 'Gesamtkosten',
|
||||
totalPoints: 'Gesamtpunkte',
|
||||
levelRange: 'Stufenbereich'
|
||||
levelRange: 'Stufenbereich',
|
||||
capacity: 'Capacity/Effect',
|
||||
storageCapacity: 'Capacity',
|
||||
energyProduction: 'Energy Production',
|
||||
fleetStorage: 'Fleet Storage',
|
||||
buildQueue: 'Build Queue',
|
||||
buildQueueBonus: 'Bauauftrag',
|
||||
spaceBonus: 'Raumbonus',
|
||||
buildSpeedBonus: 'Baugeschwindigkeitsbonus',
|
||||
researchSpeedBonus: 'Forschungsgeschwindigkeitsbonus',
|
||||
planetSpace: 'Planet Space',
|
||||
moonSpace: 'Moon Space',
|
||||
missileCapacity: 'Missile Capacity'
|
||||
},
|
||||
buildingDescriptions: {
|
||||
metalMine: 'Fördert Metallressourcen',
|
||||
crystalMine: 'Fördert Kristallressourcen',
|
||||
deuteriumSynthesizer: 'Synthesiert Deuteriumressourcen',
|
||||
solarPlant: 'Liefert Energie',
|
||||
fusionReactor: 'Nutzt Deuterium zur Erzeugung großer Energiemengen',
|
||||
roboticsFactory: 'Beschleunigt Baugeschwindigkeit',
|
||||
naniteFactory: 'Erhöht Bauauftragskapazität, +1 pro Stufe (max 10 Stufen)',
|
||||
shipyard: 'Baut Schiffe',
|
||||
@@ -145,6 +164,8 @@ export default {
|
||||
crystalStorage: 'Erhöht Kristallspeicherkapazität',
|
||||
deuteriumTank: 'Erhöht Deuteriumspeicherkapazität',
|
||||
darkMatterCollector: 'Sammelt seltene Dunkle-Materie-Ressourcen',
|
||||
darkMatterTank: 'Erhöht Dunkle-Materie-Speicherkapazität',
|
||||
missileSilo: 'Lagert und startet Raketen, 10 Raketen pro Stufe',
|
||||
terraformer: 'Terraformt Planetenoberfläche, erhöht verfügbaren Platz um 5 pro Stufe',
|
||||
lunarBase: 'Erhöht verfügbaren Platz auf dem Mond, +5 Platz pro Stufe',
|
||||
sensorPhalanx: 'Erkennt Flottenaktivitäten in umliegenden Systemen',
|
||||
@@ -156,11 +177,15 @@ export default {
|
||||
heavyFighter: 'Schwerer Jäger',
|
||||
cruiser: 'Kreuzer',
|
||||
battleship: 'Schlachtschiff',
|
||||
battlecruiser: 'Schlachtkreuzer',
|
||||
bomber: 'Bomber',
|
||||
destroyer: 'Zerstörer',
|
||||
smallCargo: 'Kleiner Transporter',
|
||||
largeCargo: 'Großer Transporter',
|
||||
colonyShip: 'Kolonieschiff',
|
||||
recycler: 'Recycler',
|
||||
espionageProbe: 'Spionagesonde',
|
||||
solarSatellite: 'Solarsatellit',
|
||||
darkMatterHarvester: 'Dunkle-Materie-Ernter',
|
||||
deathstar: 'Todesstern'
|
||||
},
|
||||
@@ -169,11 +194,15 @@ export default {
|
||||
heavyFighter: 'Schwer gepanzerter Jäger',
|
||||
cruiser: 'Mittleres Kriegsschiff, ausgewogene Offensive und Defensive',
|
||||
battleship: 'Mächtiges Kriegsschiff',
|
||||
battlecruiser: 'Schnelles mächtiges Kriegsschiff, hervorragend gegen Schlachtschiffe',
|
||||
bomber: 'Spezialisiertes Schiff zum Angriff auf Verteidigungsanlagen',
|
||||
destroyer: 'Jäger spezialisiert auf Zerstörung großer Schiffe',
|
||||
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',
|
||||
solarSatellite: 'Liefert zusätzliche Energie, erzeugt 50 Energie pro Satellit',
|
||||
darkMatterHarvester: 'Spezielles Schiff zum Ernten von Dunkler Materie',
|
||||
deathstar: 'Ultimative Waffe, die ganze Planeten zerstören kann'
|
||||
},
|
||||
@@ -186,6 +215,8 @@ export default {
|
||||
plasmaTurret: 'Plasmawerfer',
|
||||
smallShieldDome: 'Kleine Schildkuppel',
|
||||
largeShieldDome: 'Große Schildkuppel',
|
||||
antiBallisticMissile: 'Abfangrakete',
|
||||
interplanetaryMissile: 'Interkontinentalrakete',
|
||||
planetaryShield: 'Planetarschild'
|
||||
},
|
||||
defenseDescriptions: {
|
||||
@@ -197,13 +228,23 @@ export default {
|
||||
plasmaTurret: 'Mächtige Verteidigungsanlage',
|
||||
smallShieldDome: 'Kleiner Schild zum Schutz des gesamten Planeten',
|
||||
largeShieldDome: 'Großer Schild zum Schutz des gesamten Planeten',
|
||||
antiBallisticMissile: 'Fängt feindliche Raketen ab, kann 1 Interkontinentalrakete abfangen',
|
||||
interplanetaryMissile: 'Kann Verteidigungsanlagen auf anderen Planeten angreifen',
|
||||
planetaryShield: 'Superschild zum Schutz des Planeten vor Vernichtungsangriffen'
|
||||
},
|
||||
research: {
|
||||
researchTime: 'Forschungszeit',
|
||||
totalCost: 'Gesamtkosten',
|
||||
totalPoints: 'Gesamtpunkte',
|
||||
levelRange: 'Stufenbereich'
|
||||
levelRange: 'Stufenbereich',
|
||||
capacity: 'Capacity/Effect',
|
||||
storageCapacity: 'Capacity',
|
||||
energyProduction: 'Energy Production',
|
||||
fleetStorage: 'Fleet Storage',
|
||||
buildQueue: 'Build Queue',
|
||||
planetSpace: 'Planet Space',
|
||||
moonSpace: 'Moon Space',
|
||||
missileCapacity: 'Missile Capacity'
|
||||
},
|
||||
technologies: {
|
||||
energyTechnology: 'Energietechnik',
|
||||
@@ -212,6 +253,12 @@ export default {
|
||||
hyperspaceTechnology: 'Hyperraumtechnik',
|
||||
plasmaTechnology: 'Plasmatechnik',
|
||||
computerTechnology: 'Computertechnologie',
|
||||
espionageTechnology: 'Spionagetechnik',
|
||||
weaponsTechnology: 'Waffentechnik',
|
||||
shieldingTechnology: 'Schildtechnik',
|
||||
armourTechnology: 'Panzerung',
|
||||
astrophysics: 'Astrophysik',
|
||||
gravitonTechnology: 'Gravitontechnik',
|
||||
combustionDrive: 'Verbrennungsantrieb',
|
||||
impulseDrive: 'Impulsantrieb',
|
||||
hyperspaceDrive: 'Hyperraumantrieb',
|
||||
@@ -226,6 +273,12 @@ export default {
|
||||
hyperspaceTechnology: 'Hyperraumsprung-Technologie',
|
||||
plasmaTechnology: 'Plasmawaffentechnologie',
|
||||
computerTechnology: 'Erhöht Forschungsauftragskapazität, +1 pro Stufe (max 10 Stufen)',
|
||||
espionageTechnology: 'Verbessert Sondenwirksamkeit, +1 Spionagestufe pro Stufe',
|
||||
weaponsTechnology: 'Erhöht Angriffskraft von Schiffen und Verteidigung um 10% pro Stufe',
|
||||
shieldingTechnology: 'Erhöht Schilde von Schiffen und Verteidigung um 10% pro Stufe',
|
||||
armourTechnology: 'Erhöht Panzerung von Schiffen und Verteidigung um 10% pro Stufe',
|
||||
astrophysics: 'Jede Stufe fügt 1 Kolonieslot hinzu und erhöht Expeditionserfolgsrate',
|
||||
gravitonTechnology: 'Erforscht Gravitonmanipulation, erforderlich für Todesstern',
|
||||
combustionDrive: 'Grundlegende Antriebstechnologie',
|
||||
impulseDrive: 'Mittlere Antriebstechnologie',
|
||||
hyperspaceDrive: 'Fortgeschrittene Antriebstechnologie',
|
||||
@@ -293,7 +346,9 @@ export default {
|
||||
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.'
|
||||
demolishFailedMessage: 'Abriss nicht möglich. Bitte überprüfen Sie, ob die Bauqueue voll ist oder die Gebäudestufe 0 ist.',
|
||||
confirmDemolish: '',
|
||||
confirmDemolishMessage: ''
|
||||
},
|
||||
researchView: {
|
||||
title: 'Forschung',
|
||||
@@ -382,6 +437,7 @@ export default {
|
||||
all: 'Alle',
|
||||
targetCoordinates: 'Zielkoordinaten',
|
||||
galaxy: 'Galaxie',
|
||||
diplomacy: 'Diplomacy',
|
||||
system: 'System',
|
||||
position: 'Position',
|
||||
missionType: 'Missionstyp',
|
||||
@@ -415,7 +471,11 @@ export default {
|
||||
cannotSendToOwnPlanet: 'Flotte kann nicht zu eigenem Planeten gesendet werden',
|
||||
cargoExceedsCapacity: 'Fracht überschreitet Kapazität',
|
||||
noColonyShip: 'Kolonieschiff für Kolonisierungsmission erforderlich',
|
||||
noDebrisAtTarget: 'Kein Trümmerfeld an Zielkoordinaten oder Trümmerfeld ist leer'
|
||||
noDebrisAtTarget: 'Kein Trümmerfeld an Zielkoordinaten oder Trümmerfeld ist leer',
|
||||
noDeathstar: 'Todesstern für Zerstörungsmission erforderlich',
|
||||
giftMode: 'Geschenkmodus',
|
||||
giftModeDescription: 'Ressourcen als Geschenk senden an',
|
||||
estimatedReputationGain: 'Geschätzter Reputationsgewinn'
|
||||
},
|
||||
officersView: {
|
||||
title: 'Offiziere',
|
||||
@@ -455,11 +515,15 @@ export default {
|
||||
title: 'Galaxie',
|
||||
selectCoordinates: 'Koordinaten auswählen',
|
||||
galaxy: 'Galaxie',
|
||||
diplomacy: 'Diplomacy',
|
||||
selectGalaxy: 'Galaxie auswählen',
|
||||
system: 'System',
|
||||
selectSystem: 'System auswählen',
|
||||
view: 'Anzeigen',
|
||||
myPlanet: 'Mein Planet',
|
||||
myPlanets: 'Meine Planeten',
|
||||
npcPlanets: 'NPC-Planeten',
|
||||
selectPlanetToView: 'Planet zum Anzeigen auswählen',
|
||||
totalPositions: 'Insgesamt 10 Planetenpositionen',
|
||||
mine: 'Mein',
|
||||
hostile: 'Feindlich',
|
||||
@@ -481,21 +545,32 @@ export default {
|
||||
colonizePlanetMessage:
|
||||
'Möchten Sie wirklich Position [{coordinates}] kolonisieren?\n\nBitte gehen Sie zur Flottenseite, um ein Kolonieschiff zu senden.',
|
||||
recyclePlanetMessage:
|
||||
'Möchten Sie wirklich Trümmer bei Position [{coordinates}] recyceln?\n\nBitte gehen Sie zur Flottenseite, um Recycler zu senden.'
|
||||
'Möchten Sie wirklich Trümmer bei Position [{coordinates}] recyceln?\n\nBitte gehen Sie zur Flottenseite, um Recycler zu senden.',
|
||||
sendGift: 'Geschenk senden',
|
||||
debris: 'Trümmer',
|
||||
giftPlanetTitle: 'Geschenk senden',
|
||||
giftPlanetMessage:
|
||||
'Möchten Sie wirklich Ressourcen als Geschenk an Planet [{coordinates}] senden?\n\nBitte gehen Sie zur Flottenseite, um Transporter auszuwählen und Ressourcen zu laden.'
|
||||
},
|
||||
messagesView: {
|
||||
title: 'Nachrichten',
|
||||
battles: 'Kämpfe',
|
||||
spy: 'Spionage',
|
||||
npc: 'NPC',
|
||||
spied: 'Ausspioniert',
|
||||
battleReports: 'Kampfberichte',
|
||||
spyReports: 'Spionageberichte',
|
||||
noBattleReports: 'Keine Kampfberichte',
|
||||
noSpyReports: 'Keine Spionageberichte',
|
||||
noSpiedNotifications: 'Keine Ausspionierungs-Benachrichtigungen',
|
||||
battleReport: 'Kampfbericht',
|
||||
spyReport: 'Spionagebericht',
|
||||
spiedNotification: 'Ausspionierungs-Benachrichtigung',
|
||||
victory: 'Sieg',
|
||||
defeat: 'Niederlage',
|
||||
draw: 'Unentschieden',
|
||||
detected: 'Entdeckt',
|
||||
undetected: 'Unentdeckt',
|
||||
attackerFleet: 'Angreiferflotte',
|
||||
defenderFleet: 'Verteidigerflotte',
|
||||
defenderDefense: 'Verteidigerverteidigung',
|
||||
@@ -517,7 +592,43 @@ export default {
|
||||
hideRoundDetails: 'Rundendetails ausblenden',
|
||||
round: 'Runde {round}',
|
||||
attackerRemainingPower: 'Verbleibende Angreiferkraft',
|
||||
defenderRemainingPower: 'Verbleibende Verteidigerkraft'
|
||||
defenderRemainingPower: 'Verbleibende Verteidigerkraft',
|
||||
missions: 'Missionen',
|
||||
noMissionReports: 'Keine Missionsberichte',
|
||||
success: 'Erfolg',
|
||||
failed: 'Fehlgeschlagen',
|
||||
npcActivity: 'NPC-Aktivität',
|
||||
noNPCActivity: 'Keine NPC-Aktivitätsbenachrichtigungen',
|
||||
npcRecycleActivity: 'NPC recycelt Trümmer',
|
||||
gifts: 'Geschenke',
|
||||
giftRejected: 'Abgelehnt',
|
||||
noGiftNotifications: 'Keine Geschenkbenachrichtigungen',
|
||||
noGiftRejected: 'Keine abgelehnten Geschenke',
|
||||
giftFrom: 'Geschenk von {npcName}',
|
||||
giftRejectedBy: '{npcName} hat das Geschenk abgelehnt',
|
||||
giftResources: 'Geschenk-Ressourcen',
|
||||
rejectedResources: 'Abgelehnte Ressourcen',
|
||||
expectedReputation: 'Erwarteter Ruf',
|
||||
currentReputation: 'Aktueller Ruf',
|
||||
acceptGift: 'Annehmen',
|
||||
rejectGift: 'Ablehnen',
|
||||
rejectionReason: {
|
||||
hostile: 'Sie sind feindlich und nehmen keine Geschenke an',
|
||||
neutral_distrust: 'Sie vertrauen Ihnen nicht',
|
||||
polite_decline: 'Sie lehnten höflich ab'
|
||||
}
|
||||
},
|
||||
missionReports: {
|
||||
transportSuccess: 'Transportmission erfolgreich abgeschlossen',
|
||||
transportFailed: 'Transportmission fehlgeschlagen',
|
||||
colonizeSuccess: 'Kolonisierungsmission erfolgreich, neuer Planet gegründet',
|
||||
colonizeFailed: 'Kolonisierungsmission fehlgeschlagen',
|
||||
deploySuccess: 'Einsatzmission erfolgreich abgeschlossen',
|
||||
deployFailed: 'Einsatzmission fehlgeschlagen',
|
||||
recycleSuccess: 'Recyclingmission erfolgreich abgeschlossen',
|
||||
recycleFailed: 'Recyclingmission fehlgeschlagen, keine Trümmer am Zielort',
|
||||
destroySuccess: 'Planetenzerstörungsmission erfolgreich ausgeführt',
|
||||
destroyFailed: 'Planetenzerstörungsmission fehlgeschlagen'
|
||||
},
|
||||
simulatorView: {
|
||||
title: 'Kampfsimulator',
|
||||
@@ -570,13 +681,15 @@ export default {
|
||||
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?',
|
||||
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?',
|
||||
clearConfirmMessage:
|
||||
'Alle Spieldaten werden gelöscht und von vorne begonnen. Diese Aktion kann nicht rückgängig gemacht werden. Fortfahren?',
|
||||
gameSettings: 'Spieleinstellungen',
|
||||
gameSettingsDesc: 'Spielparameter und Einstellungen anpassen',
|
||||
gamePause: 'Spielpause',
|
||||
@@ -619,9 +732,82 @@ export default {
|
||||
modifyOfficers: 'Offiziere ändern',
|
||||
officersDesc: 'Offiziersablaufzeit schnell festlegen',
|
||||
days: 'T',
|
||||
npcTesting: 'NPC-Test',
|
||||
npcTestingDesc: 'NPC-Spionage- und Angriffsverhalten testen',
|
||||
selectNPC: 'NPC auswählen',
|
||||
chooseNPC: 'Wählen Sie einen NPC',
|
||||
targetPlanet: 'Zielplanet',
|
||||
chooseTarget: 'Zielplanet auswählen',
|
||||
testSpy: 'Spionage testen',
|
||||
testAttack: 'Angriff testen',
|
||||
testSpyAndAttack: 'Spionage & Angriff testen',
|
||||
initializeFleet: 'NPC-Flotte initialisieren',
|
||||
accelerateMissions: 'Alle Missionen beschleunigen (5s)',
|
||||
selectNPCFirst: 'Bitte wählen Sie zuerst einen NPC',
|
||||
npcNoProbes: 'NPC hat keine Spionagesonden',
|
||||
npcNoSpyReport: 'NPC muss zuerst spionieren',
|
||||
npcMissionFailed: 'Mission konnte nicht erstellt werden',
|
||||
dangerZone: 'Gefahrenzone',
|
||||
dangerZoneDesc: 'Die folgenden Vorgänge sind irreversibel',
|
||||
resetGame: 'Spiel zurücksetzen',
|
||||
resetGameConfirm: 'Möchten Sie das Spiel wirklich zurücksetzen? Alle Daten werden gelöscht!'
|
||||
},
|
||||
alerts: {
|
||||
npcSpyIncoming: 'NPC-Spionagesonde nähert sich',
|
||||
npcAttackIncoming: 'NPC-Flotten-Angriff im Anmarsch!',
|
||||
npcFleetIncoming: 'NPC-Flotte nähert sich',
|
||||
ships: 'Schiffe',
|
||||
spiedBy: 'Ausspioniert von',
|
||||
attackedBy: 'Angegriffen von',
|
||||
detectionSuccess: 'Spionage entdeckt',
|
||||
detectionFailed: 'Spionage nicht entdeckt',
|
||||
npcSpiedYourPlanet: 'NPC hat deinen Planeten ausspioniert',
|
||||
npcAttackedYourPlanet: 'NPC hat deinen Planeten angegriffen'
|
||||
},
|
||||
diplomacy: {
|
||||
title: 'Diplomatie',
|
||||
description: 'Verwalte diplomatische Beziehungen mit NPCs',
|
||||
tabs: {
|
||||
all: 'Alle',
|
||||
friendly: 'Freundlich',
|
||||
neutral: 'Neutral',
|
||||
hostile: 'Feindlich'
|
||||
},
|
||||
noNpcs: 'Keine NPCs',
|
||||
noFriendlyNpcs: 'Keine freundlichen NPCs',
|
||||
noNeutralNpcs: 'Keine neutralen NPCs',
|
||||
noHostileNpcs: 'Keine feindlichen NPCs',
|
||||
recentEvents: 'Aktuelle Ereignisse',
|
||||
recentEventsDescription: 'Protokoll der jüngsten diplomatischen Aktivitäten',
|
||||
ago: 'vor',
|
||||
status: {
|
||||
friendly: 'Freundlich',
|
||||
neutral: 'Neutral',
|
||||
hostile: 'Feindlich'
|
||||
},
|
||||
planets: 'Planeten',
|
||||
allies: 'Verbündete',
|
||||
reputation: 'Ansehen',
|
||||
alliedWith: 'Verbündet mit',
|
||||
more: 'weitere',
|
||||
actions: {
|
||||
gift: 'Geschenk senden',
|
||||
viewPlanets: 'Planeten ansehen'
|
||||
},
|
||||
lastEvent: 'Letztes Ereignis',
|
||||
events: {
|
||||
gift: 'Geschenk gesendet',
|
||||
attack: 'Angriff',
|
||||
allyAttacked: 'Verbündeter angegriffen',
|
||||
spy: 'Spionage',
|
||||
stealDebris: 'Trümmer gestohlen'
|
||||
}
|
||||
},
|
||||
pagination: {
|
||||
previous: 'Vorherige',
|
||||
next: 'Nächste',
|
||||
first: 'Erste',
|
||||
last: 'Letzte',
|
||||
page: 'Seite {page}'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ export default {
|
||||
officers: 'Officers',
|
||||
simulator: 'Simulator',
|
||||
galaxy: 'Galaxy',
|
||||
diplomacy: 'Diplomacy',
|
||||
messages: 'Messages',
|
||||
settings: 'Settings',
|
||||
gm: 'GM'
|
||||
@@ -97,6 +98,8 @@ export default {
|
||||
coordinates: 'Coordinates',
|
||||
switchToMoon: 'View Moon',
|
||||
backToPlanet: 'Back to Planet',
|
||||
switchPlanet: 'Switch Planet',
|
||||
currentPlanet: 'Current Planet',
|
||||
fields: 'Fields',
|
||||
temperature: 'Temperature',
|
||||
homePlanet: 'Home Planet',
|
||||
@@ -112,6 +115,7 @@ export default {
|
||||
crystalMine: 'Crystal Mine',
|
||||
deuteriumSynthesizer: 'Deuterium Synthesizer',
|
||||
solarPlant: 'Solar Plant',
|
||||
fusionReactor: 'Fusion Reactor',
|
||||
roboticsFactory: 'Robotics Factory',
|
||||
naniteFactory: 'Nanite Factory',
|
||||
shipyard: 'Shipyard',
|
||||
@@ -120,6 +124,8 @@ export default {
|
||||
crystalStorage: 'Crystal Storage',
|
||||
deuteriumTank: 'Deuterium Tank',
|
||||
darkMatterCollector: 'Dark Matter Collector',
|
||||
darkMatterTank: 'Dark Matter Tank',
|
||||
missileSilo: 'Missile Silo',
|
||||
terraformer: 'Terraformer',
|
||||
lunarBase: 'Lunar Base',
|
||||
sensorPhalanx: 'Sensor Phalanx',
|
||||
@@ -130,13 +136,24 @@ export default {
|
||||
consumption: 'Consumption',
|
||||
totalCost: 'Total Cost',
|
||||
totalPoints: 'Total Points',
|
||||
levelRange: 'Level Range'
|
||||
levelRange: 'Level Range',
|
||||
|
||||
storageCapacity: 'Capacity',
|
||||
energyProduction: 'Energy Production',
|
||||
fleetStorage: 'Fleet Storage',
|
||||
buildQueueBonus: 'Build Queue',
|
||||
spaceBonus: 'Space Bonus',
|
||||
buildSpeedBonus: 'Build Speed Bonus',
|
||||
researchSpeedBonus: 'Research Speed Bonus',
|
||||
|
||||
missileCapacity: 'Missile Capacity'
|
||||
},
|
||||
buildingDescriptions: {
|
||||
metalMine: 'Extracts metal resources',
|
||||
crystalMine: 'Extracts crystal resources',
|
||||
deuteriumSynthesizer: 'Synthesizes deuterium resources',
|
||||
solarPlant: 'Provides energy',
|
||||
fusionReactor: 'Uses deuterium to generate large amounts of energy',
|
||||
roboticsFactory: 'Accelerates construction speed',
|
||||
naniteFactory: 'Increases build queue capacity, +1 per level (max 10 levels)',
|
||||
shipyard: 'Constructs ships',
|
||||
@@ -145,6 +162,8 @@ export default {
|
||||
crystalStorage: 'Increases crystal storage capacity',
|
||||
deuteriumTank: 'Increases deuterium storage capacity',
|
||||
darkMatterCollector: 'Collects rare dark matter resources',
|
||||
darkMatterTank: 'Increases dark matter storage capacity',
|
||||
missileSilo: 'Stores and launches missiles, 10 missiles per level',
|
||||
terraformer: 'Terraforms planet surface, adds 5 available space per level',
|
||||
lunarBase: 'Increases available space on the moon, +5 space per level',
|
||||
sensorPhalanx: 'Detects fleet activities in surrounding systems',
|
||||
@@ -156,11 +175,15 @@ export default {
|
||||
heavyFighter: 'Heavy Fighter',
|
||||
cruiser: 'Cruiser',
|
||||
battleship: 'Battleship',
|
||||
battlecruiser: 'Battlecruiser',
|
||||
bomber: 'Bomber',
|
||||
destroyer: 'Destroyer',
|
||||
smallCargo: 'Small Cargo',
|
||||
largeCargo: 'Large Cargo',
|
||||
colonyShip: 'Colony Ship',
|
||||
recycler: 'Recycler',
|
||||
espionageProbe: 'Espionage Probe',
|
||||
solarSatellite: 'Solar Satellite',
|
||||
darkMatterHarvester: 'Dark Matter Harvester',
|
||||
deathstar: 'Deathstar'
|
||||
},
|
||||
@@ -169,11 +192,15 @@ export default {
|
||||
heavyFighter: 'Heavily armored fighter',
|
||||
cruiser: 'Medium warship, balanced offense and defense',
|
||||
battleship: 'Powerful warship',
|
||||
battlecruiser: 'Fast powerful warship, excels at attacking battleships',
|
||||
bomber: 'Specialized ship for attacking defense structures',
|
||||
destroyer: 'Hunter specialized in destroying large ships',
|
||||
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',
|
||||
solarSatellite: 'Provides extra energy, generates 50 energy per satellite',
|
||||
darkMatterHarvester: 'Special ship for harvesting dark matter',
|
||||
deathstar: 'Ultimate weapon capable of destroying entire planets'
|
||||
},
|
||||
@@ -186,6 +213,8 @@ export default {
|
||||
plasmaTurret: 'Plasma Turret',
|
||||
smallShieldDome: 'Small Shield Dome',
|
||||
largeShieldDome: 'Large Shield Dome',
|
||||
antiBallisticMissile: 'Anti-Ballistic Missile',
|
||||
interplanetaryMissile: 'Interplanetary Missile',
|
||||
planetaryShield: 'Planetary Shield'
|
||||
},
|
||||
defenseDescriptions: {
|
||||
@@ -197,13 +226,25 @@ export default {
|
||||
plasmaTurret: 'Powerful defense facility',
|
||||
smallShieldDome: 'Small shield protecting the entire planet',
|
||||
largeShieldDome: 'Large shield protecting the entire planet',
|
||||
antiBallisticMissile: 'Intercepts enemy missiles, can intercept 1 interplanetary missile each',
|
||||
interplanetaryMissile: 'Can attack defense structures on other planets',
|
||||
planetaryShield: 'Super shield protecting planet from destruction attacks'
|
||||
},
|
||||
research: {
|
||||
researchTime: 'Research Time',
|
||||
totalCost: 'Total Cost',
|
||||
totalPoints: 'Total Points',
|
||||
levelRange: 'Level Range'
|
||||
levelRange: 'Level Range',
|
||||
|
||||
attackBonus: 'Attack Bonus',
|
||||
shieldBonus: 'Shield Bonus',
|
||||
armorBonus: 'Armor Bonus',
|
||||
spyLevel: 'Spy Level',
|
||||
researchQueueBonus: 'Research Queue',
|
||||
colonySlots: 'Colony Slots',
|
||||
forAllPlanets: '(Global)',
|
||||
speedBonus: 'Speed Bonus',
|
||||
researchSpeedBonus: 'Research Speed Bonus'
|
||||
},
|
||||
technologies: {
|
||||
energyTechnology: 'Energy Technology',
|
||||
@@ -212,6 +253,12 @@ export default {
|
||||
hyperspaceTechnology: 'Hyperspace Technology',
|
||||
plasmaTechnology: 'Plasma Technology',
|
||||
computerTechnology: 'Computer Technology',
|
||||
espionageTechnology: 'Espionage Technology',
|
||||
weaponsTechnology: 'Weapons Technology',
|
||||
shieldingTechnology: 'Shielding Technology',
|
||||
armourTechnology: 'Armour Technology',
|
||||
astrophysics: 'Astrophysics',
|
||||
gravitonTechnology: 'Graviton Technology',
|
||||
combustionDrive: 'Combustion Drive',
|
||||
impulseDrive: 'Impulse Drive',
|
||||
hyperspaceDrive: 'Hyperspace Drive',
|
||||
@@ -226,6 +273,12 @@ export default {
|
||||
hyperspaceTechnology: 'Hyperspace jump technology',
|
||||
plasmaTechnology: 'Plasma weapon technology',
|
||||
computerTechnology: 'Increases research queue capacity, +1 per level (max 10 levels)',
|
||||
espionageTechnology: 'Improves spy probe effectiveness, +1 espionage level per level',
|
||||
weaponsTechnology: 'Increases ship and defense attack power by 10% per level',
|
||||
shieldingTechnology: 'Increases ship and defense shields by 10% per level',
|
||||
armourTechnology: 'Increases ship and defense armour by 10% per level',
|
||||
astrophysics: 'Each level adds 1 colony slot and increases expedition success rate',
|
||||
gravitonTechnology: 'Studies graviton manipulation, required for Death Star',
|
||||
combustionDrive: 'Basic propulsion technology',
|
||||
impulseDrive: 'Intermediate propulsion technology',
|
||||
hyperspaceDrive: 'Advanced propulsion technology',
|
||||
@@ -253,8 +306,11 @@ export default {
|
||||
darkMatterSpecialist: 'Improves dark matter collection efficiency'
|
||||
},
|
||||
queue: {
|
||||
buildQueue: 'Build Queue',
|
||||
researchQueue: 'Research Queue',
|
||||
buildQueueBonus: 'Build Queue',
|
||||
spaceBonus: 'Space Bonus',
|
||||
buildSpeedBonus: 'Build Speed Bonus',
|
||||
researchSpeedBonus: 'Research Speed Bonus',
|
||||
researchQueueBonus: 'Research Queue',
|
||||
building: 'Building',
|
||||
researching: 'Researching',
|
||||
remaining: 'Remaining',
|
||||
@@ -294,7 +350,9 @@ export default {
|
||||
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.'
|
||||
demolishFailedMessage: 'Unable to demolish this building. Please check if the build queue is full or the building level is 0.',
|
||||
confirmDemolish: 'Confirm Demolish',
|
||||
confirmDemolishMessage: 'Are you sure you want to demolish'
|
||||
},
|
||||
researchView: {
|
||||
title: 'Research',
|
||||
@@ -416,7 +474,10 @@ export default {
|
||||
cargoExceedsCapacity: 'Cargo exceeds capacity',
|
||||
noColonyShip: 'Colony ship required for colonization mission',
|
||||
noDebrisAtTarget: 'No debris field at target coordinates or debris field is empty',
|
||||
noDeathstar: 'Deathstar required for destruction mission'
|
||||
noDeathstar: 'Deathstar required for destruction mission',
|
||||
giftMode: 'Gift Mode',
|
||||
giftModeDescription: 'Send resources as a gift to',
|
||||
estimatedReputationGain: 'Estimated reputation gain'
|
||||
},
|
||||
officersView: {
|
||||
title: 'Officers',
|
||||
@@ -437,7 +498,10 @@ export default {
|
||||
fuelConsumption: 'Fuel Consumption',
|
||||
defense: 'Defense',
|
||||
storageCapacity: 'Storage Capacity',
|
||||
buildQueue: 'Build Queue',
|
||||
buildQueueBonus: 'Build Queue',
|
||||
spaceBonus: 'Space Bonus',
|
||||
buildSpeedBonus: 'Build Speed Bonus',
|
||||
researchSpeedBonus: 'Research Speed Bonus',
|
||||
fleetSlots: 'Fleet Slots',
|
||||
hire: 'Hire',
|
||||
renew: 'Renew',
|
||||
@@ -461,6 +525,9 @@ export default {
|
||||
selectSystem: 'Select System',
|
||||
view: 'View',
|
||||
myPlanet: 'My Planet',
|
||||
myPlanets: 'My Planets',
|
||||
npcPlanets: 'NPC Planets',
|
||||
selectPlanetToView: 'Select planet to view',
|
||||
totalPositions: '10 planet positions total',
|
||||
mine: 'Mine',
|
||||
hostile: 'Hostile',
|
||||
@@ -480,12 +547,19 @@ export default {
|
||||
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.',
|
||||
recyclePlanetMessage: 'Are you sure you want to recycle debris at position [{coordinates}]?\n\nPlease go to the fleet page to send recycler ships.'
|
||||
recyclePlanetMessage:
|
||||
'Are you sure you want to recycle debris at position [{coordinates}]?\n\nPlease go to the fleet page to send recycler ships.',
|
||||
sendGift: 'Send Gift',
|
||||
debris: 'Debris',
|
||||
giftPlanetTitle: 'Send Gift',
|
||||
giftPlanetMessage:
|
||||
'Are you sure you want to send resources as a gift to planet [{coordinates}]?\n\nPlease go to the fleet page to select transport ships and load resources.'
|
||||
},
|
||||
messagesView: {
|
||||
title: 'Messages',
|
||||
battles: 'Battles',
|
||||
spy: 'Spy',
|
||||
npc: 'NPC',
|
||||
battleReports: 'Battle Reports',
|
||||
spyReports: 'Spy Reports',
|
||||
noBattleReports: 'No battle reports',
|
||||
@@ -508,7 +582,48 @@ export default {
|
||||
defense: 'Defense',
|
||||
buildings: 'Buildings',
|
||||
unread: 'Unread',
|
||||
targetPlanet: 'Target Planet'
|
||||
targetPlanet: 'Target Planet',
|
||||
spied: 'Spied',
|
||||
spiedNotification: 'Spied Notification',
|
||||
noSpiedNotifications: 'No spied notifications',
|
||||
detected: 'Detected',
|
||||
undetected: 'Undetected',
|
||||
missions: 'Missions',
|
||||
noMissionReports: 'No mission reports',
|
||||
success: 'Success',
|
||||
failed: 'Failed',
|
||||
npcActivity: 'NPC Activity',
|
||||
noNPCActivity: 'No NPC activity notifications',
|
||||
npcRecycleActivity: 'NPC Recycling Debris',
|
||||
gifts: 'Gifts',
|
||||
giftRejected: 'Rejected',
|
||||
noGiftNotifications: 'No gift notifications',
|
||||
noGiftRejected: 'No rejected gifts',
|
||||
giftFrom: 'Gift from {npcName}',
|
||||
giftRejectedBy: '{npcName} rejected the gift',
|
||||
giftResources: 'Gift resources',
|
||||
rejectedResources: 'Rejected resources',
|
||||
expectedReputation: 'Expected reputation',
|
||||
currentReputation: 'Current reputation',
|
||||
acceptGift: 'Accept',
|
||||
rejectGift: 'Reject',
|
||||
rejectionReason: {
|
||||
hostile: 'They are hostile towards you and do not accept gifts',
|
||||
neutral_distrust: 'They lack trust in you',
|
||||
polite_decline: 'They politely declined'
|
||||
}
|
||||
},
|
||||
missionReports: {
|
||||
transportSuccess: 'Transport mission completed successfully',
|
||||
transportFailed: 'Transport mission failed',
|
||||
colonizeSuccess: 'Colonization mission successful, new planet established',
|
||||
colonizeFailed: 'Colonization mission failed',
|
||||
deploySuccess: 'Deployment mission completed successfully',
|
||||
deployFailed: 'Deployment mission failed',
|
||||
recycleSuccess: 'Recycling mission completed successfully',
|
||||
recycleFailed: 'Recycling mission failed, no debris at target location',
|
||||
destroySuccess: 'Planet destruction mission executed successfully',
|
||||
destroyFailed: 'Planet destruction mission failed'
|
||||
},
|
||||
simulatorView: {
|
||||
title: 'Battle Simulator',
|
||||
@@ -545,7 +660,12 @@ export default {
|
||||
hideRoundDetails: 'Hide round details',
|
||||
round: 'Round {round}',
|
||||
attackerRemainingPower: 'Attacker remaining power',
|
||||
defenderRemainingPower: 'Defender remaining power'
|
||||
defenderRemainingPower: 'Defender remaining power',
|
||||
spied: 'Spied',
|
||||
spiedNotification: 'Spied Notification',
|
||||
noSpiedNotifications: 'No spied notifications',
|
||||
detected: 'Detected',
|
||||
undetected: 'Undetected'
|
||||
},
|
||||
settings: {
|
||||
dataManagement: 'Data Management',
|
||||
@@ -610,9 +730,82 @@ export default {
|
||||
modifyOfficers: 'Modify Officers',
|
||||
officersDesc: 'Quickly set officer expiration time',
|
||||
days: 'd',
|
||||
npcTesting: 'NPC Testing',
|
||||
npcTestingDesc: 'Test NPC spy and attack behavior',
|
||||
selectNPC: 'Select NPC',
|
||||
chooseNPC: 'Choose an NPC',
|
||||
targetPlanet: 'Target Planet',
|
||||
chooseTarget: 'Choose target planet',
|
||||
testSpy: 'Test Spy',
|
||||
testAttack: 'Test Attack',
|
||||
testSpyAndAttack: 'Test Spy & Attack',
|
||||
initializeFleet: 'Initialize NPC Fleet',
|
||||
accelerateMissions: 'Accelerate All Missions (5s)',
|
||||
selectNPCFirst: 'Please select an NPC first',
|
||||
npcNoProbes: 'NPC has no spy probes',
|
||||
npcNoSpyReport: 'NPC needs to spy first',
|
||||
npcMissionFailed: 'Failed to create mission',
|
||||
dangerZone: 'Danger Zone',
|
||||
dangerZoneDesc: 'The following operations are irreversible',
|
||||
resetGame: 'Reset Game',
|
||||
resetGameConfirm: 'Are you sure you want to reset the game? This will delete all data!'
|
||||
},
|
||||
alerts: {
|
||||
npcSpyIncoming: 'NPC Spy Probe Incoming',
|
||||
npcAttackIncoming: 'NPC Fleet Attack Incoming!',
|
||||
npcFleetIncoming: 'NPC Fleet Approaching',
|
||||
ships: 'ships',
|
||||
spiedBy: 'Spied By',
|
||||
attackedBy: 'Attacked By',
|
||||
detectionSuccess: 'Spy detected',
|
||||
detectionFailed: 'Spy not detected',
|
||||
npcSpiedYourPlanet: 'NPC spied your planet',
|
||||
npcAttackedYourPlanet: 'NPC attacked your planet'
|
||||
},
|
||||
diplomacy: {
|
||||
title: 'Diplomacy',
|
||||
description: 'Manage diplomatic relations with NPCs',
|
||||
tabs: {
|
||||
all: 'All',
|
||||
friendly: 'Friendly',
|
||||
neutral: 'Neutral',
|
||||
hostile: 'Hostile'
|
||||
},
|
||||
noNpcs: 'No NPCs',
|
||||
noFriendlyNpcs: 'No friendly NPCs',
|
||||
noNeutralNpcs: 'No neutral NPCs',
|
||||
noHostileNpcs: 'No hostile NPCs',
|
||||
recentEvents: 'Recent Events',
|
||||
recentEventsDescription: 'Recent diplomatic activity log',
|
||||
ago: 'ago',
|
||||
status: {
|
||||
friendly: 'Friendly',
|
||||
neutral: 'Neutral',
|
||||
hostile: 'Hostile'
|
||||
},
|
||||
planets: 'planets',
|
||||
allies: 'allies',
|
||||
reputation: 'Reputation',
|
||||
alliedWith: 'Allied with',
|
||||
more: 'more',
|
||||
actions: {
|
||||
gift: 'Send Gift',
|
||||
viewPlanets: 'View Planets'
|
||||
},
|
||||
lastEvent: 'Last Event',
|
||||
events: {
|
||||
gift: 'Sent Gift',
|
||||
attack: 'Attack',
|
||||
allyAttacked: 'Ally Attacked',
|
||||
spy: 'Espionage',
|
||||
stealDebris: 'Debris Stolen'
|
||||
}
|
||||
},
|
||||
pagination: {
|
||||
previous: 'Previous',
|
||||
next: 'Next',
|
||||
first: 'First',
|
||||
last: 'Last',
|
||||
page: 'Page {page}'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ export default {
|
||||
officers: '士官',
|
||||
simulator: 'シミュレーター',
|
||||
galaxy: '銀河',
|
||||
diplomacy: 'Diplomacy',
|
||||
messages: 'メッセージ',
|
||||
settings: '設定',
|
||||
gm: 'GM'
|
||||
@@ -97,6 +98,8 @@ export default {
|
||||
coordinates: '座標',
|
||||
switchToMoon: '月を表示',
|
||||
backToPlanet: '母星に戻る',
|
||||
switchPlanet: '惑星を切り替える',
|
||||
currentPlanet: '現在の惑星',
|
||||
fields: 'フィールド',
|
||||
temperature: '温度',
|
||||
homePlanet: '母星',
|
||||
@@ -112,6 +115,7 @@ export default {
|
||||
crystalMine: 'クリスタル鉱山',
|
||||
deuteriumSynthesizer: '重水素合成装置',
|
||||
solarPlant: '太陽光発電所',
|
||||
fusionReactor: '核融合炉',
|
||||
roboticsFactory: 'ロボット工場',
|
||||
naniteFactory: 'ナノマシン工場',
|
||||
shipyard: '造船所',
|
||||
@@ -120,6 +124,8 @@ export default {
|
||||
crystalStorage: 'クリスタル倉庫',
|
||||
deuteriumTank: '重水素タンク',
|
||||
darkMatterCollector: 'ダークマター採取装置',
|
||||
darkMatterTank: 'ダークマタータンク',
|
||||
missileSilo: 'ミサイル格納庫',
|
||||
terraformer: 'テラフォーマー',
|
||||
lunarBase: '月面基地',
|
||||
sensorPhalanx: 'センサーファランクス',
|
||||
@@ -130,13 +136,26 @@ export default {
|
||||
consumption: '消費',
|
||||
totalCost: '総コスト',
|
||||
totalPoints: '総ポイント',
|
||||
levelRange: 'レベル範囲'
|
||||
levelRange: 'レベル範囲',
|
||||
capacity: 'Capacity/Effect',
|
||||
storageCapacity: 'Capacity',
|
||||
energyProduction: 'Energy Production',
|
||||
fleetStorage: 'Fleet Storage',
|
||||
buildQueue: 'Build Queue',
|
||||
buildQueueBonus: '建造隊列',
|
||||
spaceBonus: 'スペースボーナス',
|
||||
buildSpeedBonus: '建設速度ボーナス',
|
||||
researchSpeedBonus: '研究速度ボーナス',
|
||||
planetSpace: 'Planet Space',
|
||||
moonSpace: 'Moon Space',
|
||||
missileCapacity: 'Missile Capacity'
|
||||
},
|
||||
buildingDescriptions: {
|
||||
metalMine: '金属資源を採掘',
|
||||
crystalMine: 'クリスタル資源を採掘',
|
||||
deuteriumSynthesizer: '重水素資源を合成',
|
||||
solarPlant: 'エネルギーを供給',
|
||||
fusionReactor: '重水素を使用して大量のエネルギーを生成',
|
||||
roboticsFactory: '建設速度を向上',
|
||||
naniteFactory: '建設キュー数を増加、レベル毎に+1(最大10レベル)',
|
||||
shipyard: '艦船を建造',
|
||||
@@ -145,6 +164,8 @@ export default {
|
||||
crystalStorage: 'クリスタルの貯蔵上限を増加',
|
||||
deuteriumTank: '重水素の貯蔵上限を増加',
|
||||
darkMatterCollector: '希少なダークマター資源を収集',
|
||||
darkMatterTank: 'ダークマターの貯蔵上限を増加',
|
||||
missileSilo: 'ミサイルを保管・発射、レベル毎に10発',
|
||||
terraformer: '惑星地形を改造、レベル毎に利用可能スペース5増加',
|
||||
lunarBase: '月の利用可能スペースを増加、レベル毎に+5スペース',
|
||||
sensorPhalanx: '周辺星系の艦隊活動を探知',
|
||||
@@ -156,11 +177,15 @@ export default {
|
||||
heavyFighter: '重戦闘機',
|
||||
cruiser: '巡洋艦',
|
||||
battleship: '戦艦',
|
||||
battlecruiser: '巡洋戦艦',
|
||||
bomber: '爆撃機',
|
||||
destroyer: '駆逐艦',
|
||||
smallCargo: '小型輸送船',
|
||||
largeCargo: '大型輸送船',
|
||||
colonyShip: 'コロニーシップ',
|
||||
recycler: 'リサイクラー',
|
||||
espionageProbe: 'スパイプローブ',
|
||||
solarSatellite: '太陽光衛星',
|
||||
darkMatterHarvester: 'ダークマター採取船',
|
||||
deathstar: 'デススター'
|
||||
},
|
||||
@@ -169,11 +194,15 @@ export default {
|
||||
heavyFighter: '重装甲戦闘機',
|
||||
cruiser: '中型戦艦、攻守バランス型',
|
||||
battleship: '強力な戦艦',
|
||||
battlecruiser: '高速強力な戦闘艦、戦艦への攻撃に優れる',
|
||||
bomber: '防御施設への攻撃に特化した艦船',
|
||||
destroyer: '大型艦の破壊に特化したハンター',
|
||||
smallCargo: '少量の資源を輸送',
|
||||
largeCargo: '大量の資源を輸送',
|
||||
colonyShip: '新惑星の植民に使用',
|
||||
recycler: 'デブリフィールドの資源を回収',
|
||||
espionageProbe: '敵惑星を偵察',
|
||||
solarSatellite: '追加エネルギーを提供、衛星1つにつき50エネルギー生成',
|
||||
darkMatterHarvester: 'ダークマター採取専用の特殊艦',
|
||||
deathstar: '惑星全体を破壊できる究極兵器'
|
||||
},
|
||||
@@ -186,6 +215,8 @@ export default {
|
||||
plasmaTurret: 'プラズマタレット',
|
||||
smallShieldDome: '小型シールドドーム',
|
||||
largeShieldDome: '大型シールドドーム',
|
||||
antiBallisticMissile: '迎撃ミサイル',
|
||||
interplanetaryMissile: '惑星間ミサイル',
|
||||
planetaryShield: '惑星シールド'
|
||||
},
|
||||
defenseDescriptions: {
|
||||
@@ -197,13 +228,23 @@ export default {
|
||||
plasmaTurret: '強力な防衛施設',
|
||||
smallShieldDome: '惑星全体を保護する小型シールド',
|
||||
largeShieldDome: '惑星全体を保護する大型シールド',
|
||||
antiBallisticMissile: '敵ミサイルを迎撃、惑星間ミサイル1発を迎撃可能',
|
||||
interplanetaryMissile: '他の惑星の防御施設を攻撃可能',
|
||||
planetaryShield: '破壊攻撃から惑星を保護する超級シールド'
|
||||
},
|
||||
research: {
|
||||
researchTime: '研究時間',
|
||||
totalCost: '総コスト',
|
||||
totalPoints: '総ポイント',
|
||||
levelRange: 'レベル範囲'
|
||||
levelRange: 'レベル範囲',
|
||||
capacity: 'Capacity/Effect',
|
||||
storageCapacity: 'Capacity',
|
||||
energyProduction: 'Energy Production',
|
||||
fleetStorage: 'Fleet Storage',
|
||||
buildQueue: 'Build Queue',
|
||||
planetSpace: 'Planet Space',
|
||||
moonSpace: 'Moon Space',
|
||||
missileCapacity: 'Missile Capacity'
|
||||
},
|
||||
technologies: {
|
||||
energyTechnology: 'エネルギー技術',
|
||||
@@ -212,6 +253,12 @@ export default {
|
||||
hyperspaceTechnology: 'ハイパースペース技術',
|
||||
plasmaTechnology: 'プラズマ技術',
|
||||
computerTechnology: 'コンピューター技術',
|
||||
espionageTechnology: 'スパイ技術',
|
||||
weaponsTechnology: '兵器技術',
|
||||
shieldingTechnology: 'シールド技術',
|
||||
armourTechnology: '装甲技術',
|
||||
astrophysics: '天体物理学',
|
||||
gravitonTechnology: '重力技術',
|
||||
combustionDrive: '燃焼ドライブ',
|
||||
impulseDrive: 'インパルスドライブ',
|
||||
hyperspaceDrive: 'ハイパースペースドライブ',
|
||||
@@ -226,6 +273,12 @@ export default {
|
||||
hyperspaceTechnology: 'ハイパースペースジャンプ技術',
|
||||
plasmaTechnology: 'プラズマ兵器技術',
|
||||
computerTechnology: '研究キュー数を増加、レベル毎に+1(最大10レベル)',
|
||||
espionageTechnology: 'スパイ探査機の効果を向上、レベル毎に偵察深度+1',
|
||||
weaponsTechnology: '艦船と防御の攻撃力をレベル毎に10%増加',
|
||||
shieldingTechnology: '艦船と防御のシールドをレベル毎に10%増加',
|
||||
armourTechnology: '艦船と防御の装甲をレベル毎に10%増加',
|
||||
astrophysics: 'レベル毎に植民地スロット+1、探検成功率を向上',
|
||||
gravitonTechnology: '重力操作を研究、デススターに必要',
|
||||
combustionDrive: '基本推進技術',
|
||||
impulseDrive: '中級推進技術',
|
||||
hyperspaceDrive: '高級推進技術',
|
||||
@@ -308,7 +361,9 @@ export default {
|
||||
demolish: '解体',
|
||||
demolishRefund: '解体返還',
|
||||
demolishFailed: '解体失敗',
|
||||
demolishFailedMessage: 'この建物を解体できません。建設キューが満杯か、建物レベルが0でないか確認してください。'
|
||||
demolishFailedMessage: 'この建物を解体できません。建設キューが満杯か、建物レベルが0でないか確認してください。',
|
||||
confirmDemolish: '',
|
||||
confirmDemolishMessage: ''
|
||||
},
|
||||
researchView: {
|
||||
title: '研究',
|
||||
@@ -380,6 +435,7 @@ export default {
|
||||
all: '全て',
|
||||
targetCoordinates: '目標座標',
|
||||
galaxy: '銀河',
|
||||
diplomacy: 'Diplomacy',
|
||||
system: '星系',
|
||||
position: '位置',
|
||||
missionType: 'ミッションタイプ',
|
||||
@@ -413,7 +469,11 @@ export default {
|
||||
cannotSendToOwnPlanet: '自分の惑星に艦隊を派遣できません',
|
||||
cargoExceedsCapacity: '積載量が容量を超えています',
|
||||
noColonyShip: '植民ミッションにはコロニーシップが必要です',
|
||||
noDebrisAtTarget: '目標座標にデブリフィールドがないか、デブリフィールドが空です'
|
||||
noDebrisAtTarget: '目標座標にデブリフィールドがないか、デブリフィールドが空です',
|
||||
noDeathstar: '破壊ミッションにはデススターが必要です',
|
||||
giftMode: 'ギフトモード',
|
||||
giftModeDescription: '資源を贈り物として送る',
|
||||
estimatedReputationGain: '推定評判獲得'
|
||||
},
|
||||
officersView: {
|
||||
title: '士官',
|
||||
@@ -453,11 +513,15 @@ export default {
|
||||
title: '銀河',
|
||||
selectCoordinates: '座標選択',
|
||||
galaxy: '銀河',
|
||||
diplomacy: 'Diplomacy',
|
||||
selectGalaxy: '銀河を選択',
|
||||
system: '星系',
|
||||
selectSystem: '星系を選択',
|
||||
view: '表示',
|
||||
myPlanet: '自分の惑星',
|
||||
myPlanets: '私の惑星',
|
||||
npcPlanets: 'NPCの惑星',
|
||||
selectPlanetToView: '表示する惑星を選択',
|
||||
totalPositions: '全10惑星位置',
|
||||
mine: '自分',
|
||||
hostile: '敵対',
|
||||
@@ -475,12 +539,17 @@ export default {
|
||||
scoutPlanetMessage: '惑星[{coordinates}]にスパイプローブを送りますか?\n\n艦隊ページに移動して艦船を選択して派遣してください。',
|
||||
attackPlanetMessage: '惑星[{coordinates}]を攻撃しますか?\n\n艦隊ページに移動して艦船を選択して派遣してください。',
|
||||
colonizePlanetMessage: '位置[{coordinates}]を植民しますか?\n\n艦隊ページに移動してコロニーシップを派遣してください。',
|
||||
recyclePlanetMessage: '位置[{coordinates}]のデブリを回収しますか?\n\n艦隊ページに移動してリサイクラーを派遣してください。'
|
||||
recyclePlanetMessage: '位置[{coordinates}]のデブリを回収しますか?\n\n艦隊ページに移動してリサイクラーを派遣してください。',
|
||||
sendGift: 'ギフト送信',
|
||||
debris: '破片',
|
||||
giftPlanetTitle: 'ギフト送信',
|
||||
giftPlanetMessage: '惑星[{coordinates}]にリソースを贈りますか?\n\n艦隊ページに移動して輸送船を選択し、リソースを積載してください。'
|
||||
},
|
||||
messagesView: {
|
||||
title: 'メッセージセンター',
|
||||
battles: '戦闘',
|
||||
spy: 'スパイ',
|
||||
npc: 'NPC',
|
||||
battleReports: '戦闘レポート',
|
||||
spyReports: 'スパイレポート',
|
||||
noBattleReports: '戦闘レポートなし',
|
||||
@@ -511,7 +580,48 @@ export default {
|
||||
hideRoundDetails: 'ラウンド詳細非表示',
|
||||
round: '第{round}ラウンド',
|
||||
attackerRemainingPower: '攻撃側残存火力',
|
||||
defenderRemainingPower: '防御側残存火力'
|
||||
defenderRemainingPower: '防御側残存火力',
|
||||
spied: '偵察された',
|
||||
spiedNotification: '偵察通知',
|
||||
noSpiedNotifications: '偵察通知はありません',
|
||||
detected: '発見された',
|
||||
undetected: '未発見',
|
||||
missions: 'ミッション',
|
||||
noMissionReports: 'ミッションレポートなし',
|
||||
success: '成功',
|
||||
failed: '失敗',
|
||||
npcActivity: 'NPC活動',
|
||||
noNPCActivity: 'NPC活動通知はありません',
|
||||
npcRecycleActivity: 'NPCがデブリを回収',
|
||||
gifts: 'ギフト',
|
||||
giftRejected: '拒否',
|
||||
noGiftNotifications: 'ギフト通知はありません',
|
||||
noGiftRejected: '拒否された記録はありません',
|
||||
giftFrom: '{npcName}からのギフト',
|
||||
giftRejectedBy: '{npcName}がギフトを拒否しました',
|
||||
giftResources: 'ギフトリソース',
|
||||
rejectedResources: '拒否されたリソース',
|
||||
expectedReputation: '期待される評判',
|
||||
currentReputation: '現在の評判',
|
||||
acceptGift: '受け取る',
|
||||
rejectGift: '拒否',
|
||||
rejectionReason: {
|
||||
hostile: '相手は敵対的でギフトを受け取りません',
|
||||
neutral_distrust: '相手はあなたを信頼していません',
|
||||
polite_decline: '丁重に断りました'
|
||||
}
|
||||
},
|
||||
missionReports: {
|
||||
transportSuccess: '輸送ミッションが正常に完了しました',
|
||||
transportFailed: '輸送ミッションが失敗しました',
|
||||
colonizeSuccess: '植民ミッション成功、新しい惑星が確立されました',
|
||||
colonizeFailed: '植民ミッションが失敗しました',
|
||||
deploySuccess: '配備ミッションが正常に完了しました',
|
||||
deployFailed: '配備ミッションが失敗しました',
|
||||
recycleSuccess: '回収ミッションが正常に完了しました',
|
||||
recycleFailed: '回収ミッションが失敗しました。目標位置にデブリがありません',
|
||||
destroySuccess: '惑星破壊ミッションが正常に実行されました',
|
||||
destroyFailed: '惑星破壊ミッションが失敗しました'
|
||||
},
|
||||
simulatorView: {
|
||||
title: '戦闘シミュレーター',
|
||||
@@ -613,9 +723,82 @@ export default {
|
||||
modifyOfficers: '士官を変更',
|
||||
officersDesc: '士官の有効期限を素早く設定',
|
||||
days: '日',
|
||||
npcTesting: 'NPCテスト',
|
||||
npcTestingDesc: 'NPCの偵察と攻撃動作をテスト',
|
||||
selectNPC: 'NPCを選択',
|
||||
chooseNPC: 'NPCを選択してください',
|
||||
targetPlanet: 'ターゲット惑星',
|
||||
chooseTarget: 'ターゲット惑星を選択',
|
||||
testSpy: '偵察テスト',
|
||||
testAttack: '攻撃テスト',
|
||||
testSpyAndAttack: '偵察&攻撃テスト',
|
||||
initializeFleet: 'NPC艦隊を初期化',
|
||||
accelerateMissions: 'すべてのミッションを加速(5秒)',
|
||||
selectNPCFirst: '最初にNPCを選択してください',
|
||||
npcNoProbes: 'NPCには偵察プローブがありません',
|
||||
npcNoSpyReport: 'NPCは最初に偵察する必要があります',
|
||||
npcMissionFailed: 'ミッションの作成に失敗しました',
|
||||
dangerZone: '危険ゾーン',
|
||||
dangerZoneDesc: '以下の操作は元に戻せません',
|
||||
resetGame: 'ゲームをリセット',
|
||||
resetGameConfirm: 'ゲームをリセットしてもよろしいですか?すべてのデータが削除されます!'
|
||||
},
|
||||
alerts: {
|
||||
npcSpyIncoming: 'NPC偵察プローブが接近中',
|
||||
npcAttackIncoming: 'NPC艦隊攻撃が接近中!',
|
||||
npcFleetIncoming: 'NPC艦隊が接近中',
|
||||
ships: '隻',
|
||||
spiedBy: '偵察された',
|
||||
attackedBy: '攻撃された',
|
||||
detectionSuccess: '偵察が発見された',
|
||||
detectionFailed: '偵察が発見されなかった',
|
||||
npcSpiedYourPlanet: 'NPCがあなたの惑星を偵察しました',
|
||||
npcAttackedYourPlanet: 'NPCがあなたの惑星を攻撃しました'
|
||||
},
|
||||
diplomacy: {
|
||||
title: '外交',
|
||||
description: 'NPCとの外交関係を管理',
|
||||
tabs: {
|
||||
all: 'すべて',
|
||||
friendly: '友好的',
|
||||
neutral: '中立',
|
||||
hostile: '敵対的'
|
||||
},
|
||||
noNpcs: 'NPCなし',
|
||||
noFriendlyNpcs: '友好的なNPCなし',
|
||||
noNeutralNpcs: '中立なNPCなし',
|
||||
noHostileNpcs: '敵対的なNPCなし',
|
||||
recentEvents: '最近のイベント',
|
||||
recentEventsDescription: '最近の外交活動ログ',
|
||||
ago: '前',
|
||||
status: {
|
||||
friendly: '友好的',
|
||||
neutral: '中立',
|
||||
hostile: '敵対的'
|
||||
},
|
||||
planets: '惑星',
|
||||
allies: '同盟',
|
||||
reputation: '評判',
|
||||
alliedWith: '同盟関係',
|
||||
more: 'その他',
|
||||
actions: {
|
||||
gift: 'ギフトを送る',
|
||||
viewPlanets: '惑星を表示'
|
||||
},
|
||||
lastEvent: '最後のイベント',
|
||||
events: {
|
||||
gift: 'ギフト送信',
|
||||
attack: '攻撃',
|
||||
allyAttacked: '同盟が攻撃された',
|
||||
spy: '諜報活動',
|
||||
stealDebris: '残骸を略奪'
|
||||
}
|
||||
},
|
||||
pagination: {
|
||||
previous: '前へ',
|
||||
next: '次へ',
|
||||
first: '最初',
|
||||
last: '最後',
|
||||
page: '{page}ページ'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ export default {
|
||||
officers: '장교',
|
||||
simulator: '시뮬레이터',
|
||||
galaxy: '은하계',
|
||||
diplomacy: 'Diplomacy',
|
||||
messages: '메시지',
|
||||
settings: '설정',
|
||||
gm: 'GM'
|
||||
@@ -97,6 +98,8 @@ export default {
|
||||
coordinates: '좌표',
|
||||
switchToMoon: '위성 보기',
|
||||
backToPlanet: '모행성으로 돌아가기',
|
||||
switchPlanet: '행성 전환',
|
||||
currentPlanet: '현재 행성',
|
||||
fields: '필드',
|
||||
temperature: '온도',
|
||||
homePlanet: '모행성',
|
||||
@@ -112,6 +115,7 @@ export default {
|
||||
crystalMine: '크리스탈 광산',
|
||||
deuteriumSynthesizer: '중수소 합성기',
|
||||
solarPlant: '태양광 발전소',
|
||||
fusionReactor: '핵융합 반응로',
|
||||
roboticsFactory: '로봇 공장',
|
||||
naniteFactory: '나노 공장',
|
||||
shipyard: '조선소',
|
||||
@@ -120,6 +124,8 @@ export default {
|
||||
crystalStorage: '크리스탈 창고',
|
||||
deuteriumTank: '중수소 탱크',
|
||||
darkMatterCollector: '암흑 물질 수집기',
|
||||
darkMatterTank: '암흑 물질 탱크',
|
||||
missileSilo: '미사일 발사대',
|
||||
terraformer: '지형 변환기',
|
||||
lunarBase: '달 기지',
|
||||
sensorPhalanx: '센서 팔랑크스',
|
||||
@@ -130,13 +136,26 @@ export default {
|
||||
consumption: '소비',
|
||||
totalCost: '총 비용',
|
||||
totalPoints: '총 점수',
|
||||
levelRange: '레벨 범위'
|
||||
levelRange: '레벨 범위',
|
||||
capacity: 'Capacity/Effect',
|
||||
storageCapacity: 'Capacity',
|
||||
energyProduction: 'Energy Production',
|
||||
fleetStorage: 'Fleet Storage',
|
||||
buildQueue: 'Build Queue',
|
||||
buildQueueBonus: '건설 대기열',
|
||||
spaceBonus: '공간 보너스',
|
||||
buildSpeedBonus: '건설 속도 보너스',
|
||||
researchSpeedBonus: '연구 속도 보너스',
|
||||
planetSpace: 'Planet Space',
|
||||
moonSpace: 'Moon Space',
|
||||
missileCapacity: 'Missile Capacity'
|
||||
},
|
||||
buildingDescriptions: {
|
||||
metalMine: '금속 자원 채굴',
|
||||
crystalMine: '크리스탈 자원 채굴',
|
||||
deuteriumSynthesizer: '중수소 자원 합성',
|
||||
solarPlant: '에너지 제공',
|
||||
fusionReactor: '중수소를 사용하여 대량의 에너지 생산',
|
||||
roboticsFactory: '건설 속도 향상',
|
||||
naniteFactory: '건설 대기열 수 증가, 레벨당 +1 (최대 10레벨)',
|
||||
shipyard: '함선 건조',
|
||||
@@ -145,6 +164,8 @@ export default {
|
||||
crystalStorage: '크리스탈 저장 용량 증가',
|
||||
deuteriumTank: '중수소 저장 용량 증가',
|
||||
darkMatterCollector: '희귀한 암흑 물질 자원 수집',
|
||||
darkMatterTank: '암흑 물질 저장 용량 증가',
|
||||
missileSilo: '미사일을 저장 및 발사, 레벨당 10발',
|
||||
terraformer: '행성 지형 개조, 레벨당 가용 공간 5 증가',
|
||||
lunarBase: '달 가용 공간 증가, 레벨당 +5 공간',
|
||||
sensorPhalanx: '주변 행성계의 함대 활동 감지',
|
||||
@@ -156,11 +177,15 @@ export default {
|
||||
heavyFighter: '중전투기',
|
||||
cruiser: '순양함',
|
||||
battleship: '전함',
|
||||
battlecruiser: '순양전함',
|
||||
bomber: '폭격기',
|
||||
destroyer: '구축함',
|
||||
smallCargo: '소형 수송선',
|
||||
largeCargo: '대형 수송선',
|
||||
colonyShip: '식민선',
|
||||
recycler: '재활용선',
|
||||
espionageProbe: '정찰기',
|
||||
solarSatellite: '태양광 위성',
|
||||
darkMatterHarvester: '암흑 물질 채취선',
|
||||
deathstar: '데스스타'
|
||||
},
|
||||
@@ -169,11 +194,15 @@ export default {
|
||||
heavyFighter: '중장갑 전투기',
|
||||
cruiser: '중형 전함, 공격과 방어 균형',
|
||||
battleship: '강력한 전함',
|
||||
battlecruiser: '빠르고 강력한 전투함, 전함 공격에 탁월',
|
||||
bomber: '방어 시설 공격 전문 함선',
|
||||
destroyer: '대형 함선 파괴 전문 헌터',
|
||||
smallCargo: '소량의 자원 운송',
|
||||
largeCargo: '대량의 자원 운송',
|
||||
colonyShip: '새로운 행성 식민에 사용',
|
||||
recycler: '잔해장 자원 수집',
|
||||
espionageProbe: '적 행성 정찰',
|
||||
solarSatellite: '추가 에너지 제공, 위성당 50 에너지 생성',
|
||||
darkMatterHarvester: '암흑 물질 채취 전용 특수 함선',
|
||||
deathstar: '행성 전체를 파괴할 수 있는 궁극 병기'
|
||||
},
|
||||
@@ -186,6 +215,8 @@ export default {
|
||||
plasmaTurret: '플라즈마 포탑',
|
||||
smallShieldDome: '소형 실드 돔',
|
||||
largeShieldDome: '대형 실드 돔',
|
||||
antiBallisticMissile: '요격 미사일',
|
||||
interplanetaryMissile: '행성간 미사일',
|
||||
planetaryShield: '행성 실드'
|
||||
},
|
||||
defenseDescriptions: {
|
||||
@@ -197,13 +228,23 @@ export default {
|
||||
plasmaTurret: '강력한 방어 시설',
|
||||
smallShieldDome: '행성 전체를 보호하는 소형 실드',
|
||||
largeShieldDome: '행성 전체를 보호하는 대형 실드',
|
||||
antiBallisticMissile: '적 미사일 요격, 행성간 미사일 1발 요격 가능',
|
||||
interplanetaryMissile: '다른 행성의 방어 시설 공격 가능',
|
||||
planetaryShield: '파괴 공격으로부터 행성을 보호하는 초급 실드'
|
||||
},
|
||||
research: {
|
||||
researchTime: '연구 시간',
|
||||
totalCost: '총 비용',
|
||||
totalPoints: '총 점수',
|
||||
levelRange: '레벨 범위'
|
||||
levelRange: '레벨 범위',
|
||||
capacity: 'Capacity/Effect',
|
||||
storageCapacity: 'Capacity',
|
||||
energyProduction: 'Energy Production',
|
||||
fleetStorage: 'Fleet Storage',
|
||||
buildQueue: 'Build Queue',
|
||||
planetSpace: 'Planet Space',
|
||||
moonSpace: 'Moon Space',
|
||||
missileCapacity: 'Missile Capacity'
|
||||
},
|
||||
technologies: {
|
||||
energyTechnology: '에너지 기술',
|
||||
@@ -212,6 +253,12 @@ export default {
|
||||
hyperspaceTechnology: '초공간 기술',
|
||||
plasmaTechnology: '플라즈마 기술',
|
||||
computerTechnology: '컴퓨터 기술',
|
||||
espionageTechnology: '스파이 기술',
|
||||
weaponsTechnology: '무기 기술',
|
||||
shieldingTechnology: '실드 기술',
|
||||
armourTechnology: '장갑 기술',
|
||||
astrophysics: '천체물리학',
|
||||
gravitonTechnology: '중력자 기술',
|
||||
combustionDrive: '연소 엔진',
|
||||
impulseDrive: '임펄스 엔진',
|
||||
hyperspaceDrive: '초공간 엔진',
|
||||
@@ -226,6 +273,12 @@ export default {
|
||||
hyperspaceTechnology: '초공간 점프 기술',
|
||||
plasmaTechnology: '플라즈마 무기 기술',
|
||||
computerTechnology: '연구 대기열 수 증가, 레벨당 +1 (최대 10레벨)',
|
||||
espionageTechnology: '스파이 탐사기 효과 향상, 레벨당 정찰 깊이 +1',
|
||||
weaponsTechnology: '함선과 방어의 공격력 레벨당 10% 증가',
|
||||
shieldingTechnology: '함선과 방어의 실드 레벨당 10% 증가',
|
||||
armourTechnology: '함선과 방어의 장갑 레벨당 10% 증가',
|
||||
astrophysics: '레벨당 식민지 슬롯 +1, 탐험 성공률 향상',
|
||||
gravitonTechnology: '중력 조작 연구, 데스스타 필요 기술',
|
||||
combustionDrive: '기본 추진 기술',
|
||||
impulseDrive: '중급 추진 기술',
|
||||
hyperspaceDrive: '고급 추진 기술',
|
||||
@@ -293,7 +346,9 @@ export default {
|
||||
demolish: '철거',
|
||||
demolishRefund: '철거 환불',
|
||||
demolishFailed: '철거 실패',
|
||||
demolishFailedMessage: '이 건물을 철거할 수 없습니다. 건설 대기열이 가득 찼거나 건물 레벨이 0인지 확인하세요.'
|
||||
demolishFailedMessage: '이 건물을 철거할 수 없습니다. 건설 대기열이 가득 찼거나 건물 레벨이 0인지 확인하세요.',
|
||||
confirmDemolish: '',
|
||||
confirmDemolishMessage: ''
|
||||
},
|
||||
researchView: {
|
||||
title: '연구',
|
||||
@@ -380,6 +435,7 @@ export default {
|
||||
all: '전체',
|
||||
targetCoordinates: '목표 좌표',
|
||||
galaxy: '은하계',
|
||||
diplomacy: 'Diplomacy',
|
||||
system: '행성계',
|
||||
position: '위치',
|
||||
missionType: '임무 유형',
|
||||
@@ -413,7 +469,11 @@ export default {
|
||||
cannotSendToOwnPlanet: '자신의 행성으로 함대를 파견할 수 없습니다',
|
||||
cargoExceedsCapacity: '적재량이 용량을 초과합니다',
|
||||
noColonyShip: '식민 임무를 위해 식민선이 필요합니다',
|
||||
noDebrisAtTarget: '대상 좌표에 잔해장이 없거나 잔해장이 비어 있습니다'
|
||||
noDebrisAtTarget: '대상 좌표에 잔해장이 없거나 잔해장이 비어 있습니다',
|
||||
noDeathstar: '파괴 임무를 위해 데스스타가 필요합니다',
|
||||
giftMode: '선물 모드',
|
||||
giftModeDescription: '자원을 선물로 보내기',
|
||||
estimatedReputationGain: '예상 평판 획득'
|
||||
},
|
||||
officersView: {
|
||||
title: '장교',
|
||||
@@ -453,11 +513,15 @@ export default {
|
||||
title: '은하계',
|
||||
selectCoordinates: '좌표 선택',
|
||||
galaxy: '은하계',
|
||||
diplomacy: 'Diplomacy',
|
||||
selectGalaxy: '은하계 선택',
|
||||
system: '행성계',
|
||||
selectSystem: '행성계 선택',
|
||||
view: '보기',
|
||||
myPlanet: '내 행성',
|
||||
myPlanets: '내 행성들',
|
||||
npcPlanets: 'NPC 행성들',
|
||||
selectPlanetToView: '볼 행성 선택',
|
||||
totalPositions: '총 10개 행성 위치',
|
||||
mine: '내 것',
|
||||
hostile: '적대',
|
||||
@@ -476,12 +540,17 @@ export default {
|
||||
'행성 [{coordinates}]을(를) 정찰하기 위해 정찰기를 보내시겠습니까?\n\n함대 페이지로 이동하여 함선을 선택하고 파견하세요.',
|
||||
attackPlanetMessage: '행성 [{coordinates}]을(를) 공격하시겠습니까?\n\n함대 페이지로 이동하여 함선을 선택하고 파견하세요.',
|
||||
colonizePlanetMessage: '위치 [{coordinates}]을(를) 식민하시겠습니까?\n\n함대 페이지로 이동하여 식민선을 파견하세요.',
|
||||
recyclePlanetMessage: '위치 [{coordinates}]의 잔해를 회수하시겠습니까?\n\n함대 페이지로 이동하여 회수선을 파견하세요.'
|
||||
recyclePlanetMessage: '위치 [{coordinates}]의 잔해를 회수하시겠습니까?\n\n함대 페이지로 이동하여 회수선을 파견하세요.',
|
||||
sendGift: '선물 보내기',
|
||||
debris: '잔해',
|
||||
giftPlanetTitle: '선물 보내기',
|
||||
giftPlanetMessage: '행성 [{coordinates}]에 자원을 선물로 보내시겠습니까?\n\n함대 페이지로 이동하여 수송선을 선택하고 자원을 적재하세요.'
|
||||
},
|
||||
messagesView: {
|
||||
title: '메시지 센터',
|
||||
battles: '전투',
|
||||
spy: '정찰',
|
||||
npc: 'NPC',
|
||||
battleReports: '전투 보고서',
|
||||
spyReports: '정찰 보고서',
|
||||
noBattleReports: '전투 보고서 없음',
|
||||
@@ -512,7 +581,48 @@ export default {
|
||||
hideRoundDetails: '라운드 상세 숨기기',
|
||||
round: '제{round}라운드',
|
||||
attackerRemainingPower: '공격자 잔여 화력',
|
||||
defenderRemainingPower: '방어자 잔여 화력'
|
||||
defenderRemainingPower: '방어자 잔여 화력',
|
||||
spied: '정찰당함',
|
||||
spiedNotification: '정찰 알림',
|
||||
noSpiedNotifications: '정찰 알림 없음',
|
||||
detected: '발견됨',
|
||||
undetected: '미발견',
|
||||
missions: '임무',
|
||||
noMissionReports: '임무 보고서 없음',
|
||||
success: '성공',
|
||||
failed: '실패',
|
||||
npcActivity: 'NPC 활동',
|
||||
noNPCActivity: 'NPC 활동 알림 없음',
|
||||
npcRecycleActivity: 'NPC가 잔해 회수',
|
||||
gifts: '선물',
|
||||
giftRejected: '거부됨',
|
||||
noGiftNotifications: '선물 알림 없음',
|
||||
noGiftRejected: '거부된 기록 없음',
|
||||
giftFrom: '{npcName}의 선물',
|
||||
giftRejectedBy: '{npcName}가 선물을 거부했습니다',
|
||||
giftResources: '선물 자원',
|
||||
rejectedResources: '거부된 자원',
|
||||
expectedReputation: '예상 평판',
|
||||
currentReputation: '현재 평판',
|
||||
acceptGift: '수락',
|
||||
rejectGift: '거부',
|
||||
rejectionReason: {
|
||||
hostile: '상대방이 적대적이어서 선물을 받지 않습니다',
|
||||
neutral_distrust: '상대방이 당신을 신뢰하지 않습니다',
|
||||
polite_decline: '정중하게 거절했습니다'
|
||||
}
|
||||
},
|
||||
missionReports: {
|
||||
transportSuccess: '수송 임무가 성공적으로 완료되었습니다',
|
||||
transportFailed: '수송 임무 실패',
|
||||
colonizeSuccess: '식민 임무 성공, 새로운 행성이 건설되었습니다',
|
||||
colonizeFailed: '식민 임무 실패',
|
||||
deploySuccess: '배치 임무가 성공적으로 완료되었습니다',
|
||||
deployFailed: '배치 임무 실패',
|
||||
recycleSuccess: '회수 임무가 성공적으로 완료되었습니다',
|
||||
recycleFailed: '회수 임무 실패, 목표 위치에 잔해가 없습니다',
|
||||
destroySuccess: '행성 파괴 임무가 성공적으로 실행되었습니다',
|
||||
destroyFailed: '행성 파괴 임무 실패'
|
||||
},
|
||||
simulatorView: {
|
||||
title: '전투 시뮬레이터',
|
||||
@@ -614,9 +724,82 @@ export default {
|
||||
modifyOfficers: '장교 수정',
|
||||
officersDesc: '장교 만료 시간을 빠르게 설정',
|
||||
days: '일',
|
||||
npcTesting: 'NPC 테스트',
|
||||
npcTestingDesc: 'NPC 정찰 및 공격 동작 테스트',
|
||||
selectNPC: 'NPC 선택',
|
||||
chooseNPC: 'NPC를 선택하세요',
|
||||
targetPlanet: '목표 행성',
|
||||
chooseTarget: '목표 행성 선택',
|
||||
testSpy: '정찰 테스트',
|
||||
testAttack: '공격 테스트',
|
||||
testSpyAndAttack: '정찰 & 공격 테스트',
|
||||
initializeFleet: 'NPC 함대 초기화',
|
||||
accelerateMissions: '모든 임무 가속(5초)',
|
||||
selectNPCFirst: '먼저 NPC를 선택하세요',
|
||||
npcNoProbes: 'NPC에 정찰 프로브가 없습니다',
|
||||
npcNoSpyReport: 'NPC가 먼저 정찰해야 합니다',
|
||||
npcMissionFailed: '임무 생성 실패',
|
||||
dangerZone: '위험 구역',
|
||||
dangerZoneDesc: '다음 작업은 되돌릴 수 없습니다',
|
||||
resetGame: '게임 초기화',
|
||||
resetGameConfirm: '게임을 초기화하시겠습니까? 모든 데이터가 삭제됩니다!'
|
||||
},
|
||||
alerts: {
|
||||
npcSpyIncoming: 'NPC 정찰 프로브 접근 중',
|
||||
npcAttackIncoming: 'NPC 함대 공격 진행 중!',
|
||||
npcFleetIncoming: 'NPC 함대 접근 중',
|
||||
ships: '척',
|
||||
spiedBy: '정찰당함',
|
||||
attackedBy: '공격당함',
|
||||
detectionSuccess: '정찰 발견됨',
|
||||
detectionFailed: '정찰 미발견',
|
||||
npcSpiedYourPlanet: 'NPC가 당신의 행성을 정찰했습니다',
|
||||
npcAttackedYourPlanet: 'NPC가 당신의 행성을 공격했습니다'
|
||||
},
|
||||
diplomacy: {
|
||||
title: '외교',
|
||||
description: 'NPC와의 외교 관계 관리',
|
||||
tabs: {
|
||||
all: '전체',
|
||||
friendly: '우호적',
|
||||
neutral: '중립',
|
||||
hostile: '적대적'
|
||||
},
|
||||
noNpcs: 'NPC 없음',
|
||||
noFriendlyNpcs: '우호적인 NPC 없음',
|
||||
noNeutralNpcs: '중립적인 NPC 없음',
|
||||
noHostileNpcs: '적대적인 NPC 없음',
|
||||
recentEvents: '최근 이벤트',
|
||||
recentEventsDescription: '최근 외교 활동 로그',
|
||||
ago: '전',
|
||||
status: {
|
||||
friendly: '우호적',
|
||||
neutral: '중립',
|
||||
hostile: '적대적'
|
||||
},
|
||||
planets: '행성',
|
||||
allies: '동맹',
|
||||
reputation: '평판',
|
||||
alliedWith: '동맹 관계',
|
||||
more: '더보기',
|
||||
actions: {
|
||||
gift: '선물 보내기',
|
||||
viewPlanets: '행성 보기'
|
||||
},
|
||||
lastEvent: '최근 이벤트',
|
||||
events: {
|
||||
gift: '선물 전송',
|
||||
attack: '공격',
|
||||
allyAttacked: '동맹 공격당함',
|
||||
spy: '정찰',
|
||||
stealDebris: '잔해 약탈'
|
||||
}
|
||||
},
|
||||
pagination: {
|
||||
previous: '이전',
|
||||
next: '다음',
|
||||
first: '처음',
|
||||
last: '마지막',
|
||||
page: '{page}페이지'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ export default {
|
||||
officers: 'Офицеры',
|
||||
simulator: 'Симулятор',
|
||||
galaxy: 'Галактика',
|
||||
diplomacy: 'Diplomacy',
|
||||
messages: 'Сообщения',
|
||||
settings: 'Настройки',
|
||||
gm: 'GM'
|
||||
@@ -97,6 +98,8 @@ export default {
|
||||
coordinates: 'Координаты',
|
||||
switchToMoon: 'На луну',
|
||||
backToPlanet: 'Вернуться на планету',
|
||||
switchPlanet: 'Переключить планету',
|
||||
currentPlanet: 'Текущая планета',
|
||||
fields: 'Поля',
|
||||
temperature: 'Температура',
|
||||
homePlanet: 'Родная планета',
|
||||
@@ -112,6 +115,7 @@ export default {
|
||||
crystalMine: 'Рудник кристалла',
|
||||
deuteriumSynthesizer: 'Синтезатор дейтерия',
|
||||
solarPlant: 'Солнечная электростанция',
|
||||
fusionReactor: 'Термоядерный реактор',
|
||||
roboticsFactory: 'Фабрика роботов',
|
||||
naniteFactory: 'Нанитная фабрика',
|
||||
shipyard: 'Верфь',
|
||||
@@ -120,6 +124,8 @@ export default {
|
||||
crystalStorage: 'Хранилище кристалла',
|
||||
deuteriumTank: 'Цистерна дейтерия',
|
||||
darkMatterCollector: 'Коллектор тёмной материи',
|
||||
darkMatterTank: 'Резервуар тёмной материи',
|
||||
missileSilo: 'Ракетная шахта',
|
||||
terraformer: 'Терраформер',
|
||||
lunarBase: 'Лунная база',
|
||||
sensorPhalanx: 'Сенсорная фаланга',
|
||||
@@ -130,13 +136,26 @@ export default {
|
||||
consumption: 'Потребление',
|
||||
totalCost: 'Общая стоимость',
|
||||
totalPoints: 'Общие очки',
|
||||
levelRange: 'Диапазон уровней'
|
||||
levelRange: 'Диапазон уровней',
|
||||
capacity: 'Capacity/Effect',
|
||||
storageCapacity: 'Capacity',
|
||||
energyProduction: 'Energy Production',
|
||||
fleetStorage: 'Fleet Storage',
|
||||
buildQueue: 'Build Queue',
|
||||
buildQueueBonus: 'Очередь строительства',
|
||||
spaceBonus: 'Бонус пространства',
|
||||
buildSpeedBonus: 'Бонус скорости строительства',
|
||||
researchSpeedBonus: 'Бонус скорости исследования',
|
||||
planetSpace: 'Planet Space',
|
||||
moonSpace: 'Moon Space',
|
||||
missileCapacity: 'Missile Capacity'
|
||||
},
|
||||
buildingDescriptions: {
|
||||
metalMine: 'Добывает металлические ресурсы',
|
||||
crystalMine: 'Добывает кристаллические ресурсы',
|
||||
deuteriumSynthesizer: 'Синтезирует дейтериевые ресурсы',
|
||||
solarPlant: 'Обеспечивает энергией',
|
||||
fusionReactor: 'Использует дейтерий для производства большого количества энергии',
|
||||
roboticsFactory: 'Ускоряет скорость строительства',
|
||||
naniteFactory: 'Увеличивает вместимость очереди строительства, +1 за уровень (макс 10 уровней)',
|
||||
shipyard: 'Строит корабли',
|
||||
@@ -145,6 +164,8 @@ export default {
|
||||
crystalStorage: 'Увеличивает ёмкость хранилища кристалла',
|
||||
deuteriumTank: 'Увеличивает ёмкость хранилища дейтерия',
|
||||
darkMatterCollector: 'Собирает редкие ресурсы тёмной материи',
|
||||
darkMatterTank: 'Увеличивает ёмкость хранилища тёмной материи',
|
||||
missileSilo: 'Хранит и запускает ракеты, 10 ракет на уровень',
|
||||
terraformer: 'Терраформирует поверхность планеты, увеличивает доступное пространство на 5 за уровень',
|
||||
lunarBase: 'Увеличивает доступное пространство на луне, +5 пространства за уровень',
|
||||
sensorPhalanx: 'Обнаруживает активность флота в окружающих системах',
|
||||
@@ -156,11 +177,15 @@ export default {
|
||||
heavyFighter: 'Тяжёлый истребитель',
|
||||
cruiser: 'Крейсер',
|
||||
battleship: 'Линкор',
|
||||
battlecruiser: 'Линейный крейсер',
|
||||
bomber: 'Бомбардировщик',
|
||||
destroyer: 'Эсминец',
|
||||
smallCargo: 'Малый транспорт',
|
||||
largeCargo: 'Большой транспорт',
|
||||
colonyShip: 'Колонизатор',
|
||||
recycler: 'Переработчик',
|
||||
espionageProbe: 'Шпионский зонд',
|
||||
solarSatellite: 'Солнечный спутник',
|
||||
darkMatterHarvester: 'Сборщик тёмной материи',
|
||||
deathstar: 'Звезда Смерти'
|
||||
},
|
||||
@@ -169,11 +194,15 @@ export default {
|
||||
heavyFighter: 'Тяжелобронированный истребитель',
|
||||
cruiser: 'Средний боевой корабль, сбалансированная атака и защита',
|
||||
battleship: 'Мощный боевой корабль',
|
||||
battlecruiser: 'Быстрый мощный боевой корабль, отлично атакует линкоры',
|
||||
bomber: 'Специализированный корабль для атаки оборонительных сооружений',
|
||||
destroyer: 'Охотник, специализирующийся на уничтожении крупных кораблей',
|
||||
smallCargo: 'Транспортирует небольшое количество ресурсов',
|
||||
largeCargo: 'Транспортирует большое количество ресурсов',
|
||||
colonyShip: 'Используется для колонизации новых планет',
|
||||
recycler: 'Собирает ресурсы с поля обломков',
|
||||
espionageProbe: 'Разведывает вражеские планеты',
|
||||
solarSatellite: 'Обеспечивает дополнительную энергию, генерирует 50 энергии на спутник',
|
||||
darkMatterHarvester: 'Специальный корабль для сбора тёмной материи',
|
||||
deathstar: 'Абсолютное оружие, способное уничтожать целые планеты'
|
||||
},
|
||||
@@ -186,6 +215,8 @@ export default {
|
||||
plasmaTurret: 'Плазменная турель',
|
||||
smallShieldDome: 'Малый щитовой купол',
|
||||
largeShieldDome: 'Большой щитовой купол',
|
||||
antiBallisticMissile: 'Противоракета',
|
||||
interplanetaryMissile: 'Межпланетная ракета',
|
||||
planetaryShield: 'Планетарный щит'
|
||||
},
|
||||
defenseDescriptions: {
|
||||
@@ -197,13 +228,23 @@ export default {
|
||||
plasmaTurret: 'Мощное оборонительное сооружение',
|
||||
smallShieldDome: 'Малый щит, защищающий всю планету',
|
||||
largeShieldDome: 'Большой щит, защищающий всю планету',
|
||||
antiBallisticMissile: 'Перехватывает вражеские ракеты, может перехватить 1 межпланетную ракету',
|
||||
interplanetaryMissile: 'Может атаковать оборонительные сооружения на других планетах',
|
||||
planetaryShield: 'Суперщит, защищающий планету от атак уничтожения'
|
||||
},
|
||||
research: {
|
||||
researchTime: 'Время исследования',
|
||||
totalCost: 'Общая стоимость',
|
||||
totalPoints: 'Общие очки',
|
||||
levelRange: 'Диапазон уровней'
|
||||
levelRange: 'Диапазон уровней',
|
||||
capacity: 'Capacity/Effect',
|
||||
storageCapacity: 'Capacity',
|
||||
energyProduction: 'Energy Production',
|
||||
fleetStorage: 'Fleet Storage',
|
||||
buildQueue: 'Build Queue',
|
||||
planetSpace: 'Planet Space',
|
||||
moonSpace: 'Moon Space',
|
||||
missileCapacity: 'Missile Capacity'
|
||||
},
|
||||
technologies: {
|
||||
energyTechnology: 'Энергетическая технология',
|
||||
@@ -212,6 +253,12 @@ export default {
|
||||
hyperspaceTechnology: 'Гиперпространственная технология',
|
||||
plasmaTechnology: 'Плазменная технология',
|
||||
computerTechnology: 'Компьютерная технология',
|
||||
espionageTechnology: 'Шпионаж',
|
||||
weaponsTechnology: 'Оружие',
|
||||
shieldingTechnology: 'Щиты',
|
||||
armourTechnology: 'Броня',
|
||||
astrophysics: 'Астрофизика',
|
||||
gravitonTechnology: 'Гравитоны',
|
||||
combustionDrive: 'Реактивный двигатель',
|
||||
impulseDrive: 'Импульсный двигатель',
|
||||
hyperspaceDrive: 'Гиперпространственный двигатель',
|
||||
@@ -226,11 +273,18 @@ export default {
|
||||
hyperspaceTechnology: 'Технология гиперпространственных прыжков',
|
||||
plasmaTechnology: 'Технология плазменного оружия',
|
||||
computerTechnology: 'Увеличивает вместимость очереди исследований, +1 за уровень (макс 10 уровней)',
|
||||
espionageTechnology: 'Повышает эффективность зондов, +1 уровень шпионажа за уровень',
|
||||
weaponsTechnology: 'Увеличивает силу атаки кораблей и обороны на 10% за уровень',
|
||||
shieldingTechnology: 'Увеличивает щиты кораблей и обороны на 10% за уровень',
|
||||
armourTechnology: 'Увеличивает броню кораблей и обороны на 10% за уровень',
|
||||
astrophysics: 'Каждый уровень добавляет 1 слот колонии и повышает шанс успеха экспедиций',
|
||||
gravitonTechnology: 'Изучает манипуляцию гравитонами, требуется для Звезды смерти',
|
||||
combustionDrive: 'Базовая технология двигателей',
|
||||
impulseDrive: 'Средняя технология двигателей',
|
||||
hyperspaceDrive: 'Продвинутая технология двигателей',
|
||||
darkMatterTechnology: 'Исследование свойств и применения тёмной материи',
|
||||
terraformingTechnology: 'Исследование технологии терраформирования планет, увеличивает доступное пространство всех планет на 3 за уровень',
|
||||
terraformingTechnology:
|
||||
'Исследование технологии терраформирования планет, увеличивает доступное пространство всех планет на 3 за уровень',
|
||||
planetDestructionTech: 'Исследование ужасающей технологии уничтожения целых планет'
|
||||
},
|
||||
officers: {
|
||||
@@ -293,7 +347,9 @@ export default {
|
||||
demolish: 'Снести',
|
||||
demolishRefund: 'Возврат от сноса',
|
||||
demolishFailed: 'Снос не удался',
|
||||
demolishFailedMessage: 'Невозможно снести это здание. Проверьте, не заполнена ли очередь строительства или уровень здания не равен 0.'
|
||||
demolishFailedMessage: 'Невозможно снести это здание. Проверьте, не заполнена ли очередь строительства или уровень здания не равен 0.',
|
||||
confirmDemolish: '',
|
||||
confirmDemolishMessage: ''
|
||||
},
|
||||
researchView: {
|
||||
title: 'Исследования',
|
||||
@@ -382,6 +438,7 @@ export default {
|
||||
all: 'Все',
|
||||
targetCoordinates: 'Целевые координаты',
|
||||
galaxy: 'Галактика',
|
||||
diplomacy: 'Diplomacy',
|
||||
system: 'Система',
|
||||
position: 'Позиция',
|
||||
missionType: 'Тип миссии',
|
||||
@@ -415,7 +472,11 @@ export default {
|
||||
cannotSendToOwnPlanet: 'Невозможно отправить флот на свою планету',
|
||||
cargoExceedsCapacity: 'Груз превышает вместимость',
|
||||
noColonyShip: 'Для колонизационной миссии требуется колониальный корабль',
|
||||
noDebrisAtTarget: 'Нет поля обломков по целевым координатам или поле обломков пусто'
|
||||
noDebrisAtTarget: 'Нет поля обломков по целевым координатам или поле обломков пусто',
|
||||
noDeathstar: 'Для миссии разрушения требуется Звезда Смерти',
|
||||
giftMode: 'Режим подарка',
|
||||
giftModeDescription: 'Отправить ресурсы в подарок',
|
||||
estimatedReputationGain: 'Ожидаемый прирост репутации'
|
||||
},
|
||||
officersView: {
|
||||
title: 'Офицеры',
|
||||
@@ -455,11 +516,15 @@ export default {
|
||||
title: 'Галактика',
|
||||
selectCoordinates: 'Выбрать координаты',
|
||||
galaxy: 'Галактика',
|
||||
diplomacy: 'Diplomacy',
|
||||
selectGalaxy: 'Выбрать галактику',
|
||||
system: 'Система',
|
||||
selectSystem: 'Выбрать систему',
|
||||
view: 'Показать',
|
||||
myPlanet: 'Моя планета',
|
||||
myPlanets: 'Мои планеты',
|
||||
npcPlanets: 'Планеты NPC',
|
||||
selectPlanetToView: 'Выберите планету для просмотра',
|
||||
totalPositions: 'Всего 10 позиций планет',
|
||||
mine: 'Моя',
|
||||
hostile: 'Враждебная',
|
||||
@@ -481,12 +546,18 @@ export default {
|
||||
colonizePlanetMessage:
|
||||
'Вы уверены, что хотите колонизировать позицию [{coordinates}]?\n\nПерейдите на страницу флота, чтобы отправить колонизационный корабль.',
|
||||
recyclePlanetMessage:
|
||||
'Вы уверены, что хотите переработать обломки в позиции [{coordinates}]?\n\nПерейдите на страницу флота, чтобы отправить переработчики.'
|
||||
'Вы уверены, что хотите переработать обломки в позиции [{coordinates}]?\n\nПерейдите на страницу флота, чтобы отправить переработчики.',
|
||||
sendGift: 'Отправить подарок',
|
||||
debris: 'Обломки',
|
||||
giftPlanetTitle: 'Отправить подарок',
|
||||
giftPlanetMessage:
|
||||
'Вы уверены, что хотите отправить ресурсы в подарок планете [{coordinates}]?\n\nПерейдите на страницу флота, чтобы выбрать транспортные корабли и загрузить ресурсы.'
|
||||
},
|
||||
messagesView: {
|
||||
title: 'Сообщения',
|
||||
battles: 'Битвы',
|
||||
spy: 'Разведка',
|
||||
npc: 'NPC',
|
||||
battleReports: 'Отчёты о боях',
|
||||
spyReports: 'Отчёты разведки',
|
||||
noBattleReports: 'Нет отчётов о боях',
|
||||
@@ -517,7 +588,48 @@ export default {
|
||||
hideRoundDetails: 'Скрыть детали раундов',
|
||||
round: 'Раунд {round}',
|
||||
attackerRemainingPower: 'Оставшаяся мощь нападающего',
|
||||
defenderRemainingPower: 'Оставшаяся мощь защитника'
|
||||
defenderRemainingPower: 'Оставшаяся мощь защитника',
|
||||
spied: 'Шпионаж',
|
||||
spiedNotification: 'Уведомление о шпионаже',
|
||||
noSpiedNotifications: 'Нет уведомлений о шпионаже',
|
||||
detected: 'Обнаружено',
|
||||
undetected: 'Не обнаружено',
|
||||
missions: 'Миссии',
|
||||
noMissionReports: 'Нет отчётов о миссиях',
|
||||
success: 'Успех',
|
||||
failed: 'Неудача',
|
||||
npcActivity: 'Активность NPC',
|
||||
noNPCActivity: 'Нет уведомлений об активности NPC',
|
||||
npcRecycleActivity: 'NPC перерабатывает обломки',
|
||||
gifts: 'Подарки',
|
||||
giftRejected: 'Отклонено',
|
||||
noGiftNotifications: 'Нет уведомлений о подарках',
|
||||
noGiftRejected: 'Нет отклоненных подарков',
|
||||
giftFrom: 'Подарок от {npcName}',
|
||||
giftRejectedBy: '{npcName} отклонил подарок',
|
||||
giftResources: 'Ресурсы подарка',
|
||||
rejectedResources: 'Отклоненные ресурсы',
|
||||
expectedReputation: 'Ожидаемая репутация',
|
||||
currentReputation: 'Текущая репутация',
|
||||
acceptGift: 'Принять',
|
||||
rejectGift: 'Отклонить',
|
||||
rejectionReason: {
|
||||
hostile: 'Они враждебны и не принимают подарки',
|
||||
neutral_distrust: 'Они вам не доверяют',
|
||||
polite_decline: 'Вежливо отказались'
|
||||
}
|
||||
},
|
||||
missionReports: {
|
||||
transportSuccess: 'Миссия транспортировки успешно завершена',
|
||||
transportFailed: 'Миссия транспортировки провалена',
|
||||
colonizeSuccess: 'Миссия колонизации успешна, новая планета создана',
|
||||
colonizeFailed: 'Миссия колонизации провалена',
|
||||
deploySuccess: 'Миссия размещения успешно завершена',
|
||||
deployFailed: 'Миссия размещения провалена',
|
||||
recycleSuccess: 'Миссия переработки успешно завершена',
|
||||
recycleFailed: 'Миссия переработки провалена, нет обломков в целевой позиции',
|
||||
destroySuccess: 'Миссия уничтожения планеты успешно выполнена',
|
||||
destroyFailed: 'Миссия уничтожения планеты провалена'
|
||||
},
|
||||
simulatorView: {
|
||||
title: 'Симулятор боя',
|
||||
@@ -619,9 +731,82 @@ export default {
|
||||
modifyOfficers: 'Изменить офицеров',
|
||||
officersDesc: 'Быстрая установка времени истечения офицеров',
|
||||
days: 'д',
|
||||
npcTesting: 'Тестирование NPC',
|
||||
npcTestingDesc: 'Тестирование разведки и атак NPC',
|
||||
selectNPC: 'Выбрать NPC',
|
||||
chooseNPC: 'Выберите NPC',
|
||||
targetPlanet: 'Целевая планета',
|
||||
chooseTarget: 'Выберите целевую планету',
|
||||
testSpy: 'Тест разведки',
|
||||
testAttack: 'Тест атаки',
|
||||
testSpyAndAttack: 'Тест разведки и атаки',
|
||||
initializeFleet: 'Инициализировать флот NPC',
|
||||
accelerateMissions: 'Ускорить все миссии (5с)',
|
||||
selectNPCFirst: 'Сначала выберите NPC',
|
||||
npcNoProbes: 'У NPC нет шпионских зондов',
|
||||
npcNoSpyReport: 'NPC нужно сначала разведать',
|
||||
npcMissionFailed: 'Не удалось создать миссию',
|
||||
dangerZone: 'Опасная зона',
|
||||
dangerZoneDesc: 'Следующие операции необратимы',
|
||||
resetGame: 'Сбросить игру',
|
||||
resetGameConfirm: 'Вы уверены, что хотите сбросить игру? Все данные будут удалены!'
|
||||
},
|
||||
alerts: {
|
||||
npcSpyIncoming: 'Приближается шпионский зонд NPC',
|
||||
npcAttackIncoming: 'Атака флота NPC приближается!',
|
||||
npcFleetIncoming: 'Приближается флот NPC',
|
||||
ships: 'кораблей',
|
||||
spiedBy: 'Разведан',
|
||||
attackedBy: 'Атакован',
|
||||
detectionSuccess: 'Разведка обнаружена',
|
||||
detectionFailed: 'Разведка не обнаружена',
|
||||
npcSpiedYourPlanet: 'NPC разведал вашу планету',
|
||||
npcAttackedYourPlanet: 'NPC атаковал вашу планету'
|
||||
},
|
||||
diplomacy: {
|
||||
title: 'Дипломатия',
|
||||
description: 'Управление дипломатическими отношениями с NPC',
|
||||
tabs: {
|
||||
all: 'Все',
|
||||
friendly: 'Дружественные',
|
||||
neutral: 'Нейтральные',
|
||||
hostile: 'Враждебные'
|
||||
},
|
||||
noNpcs: 'Нет NPC',
|
||||
noFriendlyNpcs: 'Нет дружественных NPC',
|
||||
noNeutralNpcs: 'Нет нейтральных NPC',
|
||||
noHostileNpcs: 'Нет враждебных NPC',
|
||||
recentEvents: 'Недавние события',
|
||||
recentEventsDescription: 'Журнал последних дипломатических действий',
|
||||
ago: 'назад',
|
||||
status: {
|
||||
friendly: 'Дружественный',
|
||||
neutral: 'Нейтральный',
|
||||
hostile: 'Враждебный'
|
||||
},
|
||||
planets: 'планет',
|
||||
allies: 'союзников',
|
||||
reputation: 'Репутация',
|
||||
alliedWith: 'В союзе с',
|
||||
more: 'еще',
|
||||
actions: {
|
||||
gift: 'Отправить подарок',
|
||||
viewPlanets: 'Посмотреть планеты'
|
||||
},
|
||||
lastEvent: 'Последнее событие',
|
||||
events: {
|
||||
gift: 'Подарок отправлен',
|
||||
attack: 'Атака',
|
||||
allyAttacked: 'Союзник атакован',
|
||||
spy: 'Шпионаж',
|
||||
stealDebris: 'Обломки украдены'
|
||||
}
|
||||
},
|
||||
pagination: {
|
||||
previous: 'Предыдущая',
|
||||
next: 'Следующая',
|
||||
first: 'Первая',
|
||||
last: 'Последняя',
|
||||
page: 'Страница {page}'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ export default {
|
||||
officers: '军官',
|
||||
simulator: '模拟',
|
||||
galaxy: '星系',
|
||||
diplomacy: '外交',
|
||||
messages: '消息',
|
||||
settings: '设置',
|
||||
gm: 'GM'
|
||||
@@ -78,7 +79,7 @@ export default {
|
||||
crystal: '晶体',
|
||||
deuterium: '重氢',
|
||||
darkMatter: '暗物质',
|
||||
energy: '能量',
|
||||
energy: '电力',
|
||||
production: '产量',
|
||||
consumption: '消耗',
|
||||
capacity: '容量',
|
||||
@@ -87,7 +88,7 @@ export default {
|
||||
perHour: '小时',
|
||||
perMinute: '分钟',
|
||||
hour: '小时',
|
||||
noEnergy: '能量不足'
|
||||
noEnergy: '电力不足'
|
||||
},
|
||||
planet: {
|
||||
planet: '星球',
|
||||
@@ -97,6 +98,8 @@ export default {
|
||||
coordinates: '坐标',
|
||||
switchToMoon: '查看月球',
|
||||
backToPlanet: '返回母星',
|
||||
switchPlanet: '切换星球',
|
||||
currentPlanet: '当前星球',
|
||||
fields: '场地',
|
||||
temperature: '温度',
|
||||
homePlanet: '母星',
|
||||
@@ -112,6 +115,7 @@ export default {
|
||||
crystalMine: '晶体矿',
|
||||
deuteriumSynthesizer: '重氢合成器',
|
||||
solarPlant: '太阳能电站',
|
||||
fusionReactor: '核聚变反应堆',
|
||||
roboticsFactory: '机器人工厂',
|
||||
naniteFactory: '纳米工厂',
|
||||
shipyard: '船坞',
|
||||
@@ -120,6 +124,8 @@ export default {
|
||||
crystalStorage: '晶体仓库',
|
||||
deuteriumTank: '重氢罐',
|
||||
darkMatterCollector: '暗物质收集器',
|
||||
darkMatterTank: '暗物质储罐',
|
||||
missileSilo: '导弹发射井',
|
||||
terraformer: '地形改造器',
|
||||
lunarBase: '月球基地',
|
||||
sensorPhalanx: '传感器阵列',
|
||||
@@ -130,13 +136,24 @@ export default {
|
||||
consumption: '消耗',
|
||||
totalCost: '累积成本',
|
||||
totalPoints: '累积积分',
|
||||
levelRange: '等级范围'
|
||||
levelRange: '等级范围',
|
||||
|
||||
storageCapacity: '容量',
|
||||
energyProduction: '电力产出',
|
||||
fleetStorage: '舰队仓储',
|
||||
buildQueueBonus: '建造队列',
|
||||
spaceBonus: '空间加成',
|
||||
buildSpeedBonus: '建造速度加成',
|
||||
researchSpeedBonus: '研究速度加成',
|
||||
|
||||
missileCapacity: '导弹容量'
|
||||
},
|
||||
buildingDescriptions: {
|
||||
metalMine: '开采金属资源',
|
||||
crystalMine: '开采晶体资源',
|
||||
deuteriumSynthesizer: '合成重氢资源',
|
||||
solarPlant: '提供能源',
|
||||
fusionReactor: '使用重氢产生大量能源',
|
||||
roboticsFactory: '加快建造速度',
|
||||
naniteFactory: '增加建造队列数量,每级+1队列(最多10级)',
|
||||
shipyard: '建造舰船',
|
||||
@@ -145,6 +162,8 @@ export default {
|
||||
crystalStorage: '增加晶体存储上限',
|
||||
deuteriumTank: '增加重氢存储上限',
|
||||
darkMatterCollector: '收集稀有的暗物质资源',
|
||||
darkMatterTank: '增加暗物质存储上限',
|
||||
missileSilo: '存储和发射导弹,每级可存储10枚导弹',
|
||||
terraformer: '改造行星地形,每级增加5个可用空间',
|
||||
lunarBase: '增加月球可用空间,每级+5空间',
|
||||
sensorPhalanx: '侦测周围星系的舰队活动',
|
||||
@@ -156,11 +175,15 @@ export default {
|
||||
heavyFighter: '重型战斗机',
|
||||
cruiser: '巡洋舰',
|
||||
battleship: '战列舰',
|
||||
battlecruiser: '战列巡洋舰',
|
||||
bomber: '轰炸机',
|
||||
destroyer: '驱逐舰',
|
||||
smallCargo: '小型运输船',
|
||||
largeCargo: '大型运输船',
|
||||
colonyShip: '殖民船',
|
||||
recycler: '回收船',
|
||||
espionageProbe: '间谍探测器',
|
||||
solarSatellite: '太阳能卫星',
|
||||
darkMatterHarvester: '暗物质采集船',
|
||||
deathstar: '死星'
|
||||
},
|
||||
@@ -169,11 +192,15 @@ export default {
|
||||
heavyFighter: '重装战斗机',
|
||||
cruiser: '中型战舰,攻守平衡',
|
||||
battleship: '强力战舰',
|
||||
battlecruiser: '快速强大的战斗舰船,擅长攻击战列舰',
|
||||
bomber: '专门对付防御设施的轰炸舰',
|
||||
destroyer: '擅长摧毁大型舰船的猎杀者',
|
||||
smallCargo: '运输少量资源',
|
||||
largeCargo: '运输大量资源',
|
||||
colonyShip: '用于殖民新星球',
|
||||
recycler: '收集残骸场资源',
|
||||
espionageProbe: '侦察敌方星球',
|
||||
solarSatellite: '提供额外能源,每个产生50点电力',
|
||||
darkMatterHarvester: '专门用于采集暗物质的特殊飞船',
|
||||
deathstar: '终极武器,能够摧毁整个行星'
|
||||
},
|
||||
@@ -186,24 +213,38 @@ export default {
|
||||
plasmaTurret: '等离子炮塔',
|
||||
smallShieldDome: '小型护盾罩',
|
||||
largeShieldDome: '大型护盾罩',
|
||||
antiBallisticMissile: '反弹道导弹',
|
||||
interplanetaryMissile: '星际导弹',
|
||||
planetaryShield: '行星护盾'
|
||||
},
|
||||
defenseDescriptions: {
|
||||
rocketLauncher: '基础防御设施',
|
||||
lightLaser: '轻型能量武器',
|
||||
heavyLaser: '重型能量武器',
|
||||
lightLaser: '轻型电力武器',
|
||||
heavyLaser: '重型电力武器',
|
||||
gaussCannon: '高速动能武器',
|
||||
ionCannon: '破坏护盾的利器',
|
||||
plasmaTurret: '强力防御设施',
|
||||
smallShieldDome: '保护整个星球的小型护盾',
|
||||
largeShieldDome: '保护整个星球的大型护盾',
|
||||
antiBallisticMissile: '拦截敌方导弹,每个可拦截1枚星际导弹',
|
||||
interplanetaryMissile: '可以攻击其他星球的防御设施',
|
||||
planetaryShield: '保护行星免受毁灭攻击的超级护盾'
|
||||
},
|
||||
research: {
|
||||
researchTime: '研究时间',
|
||||
totalCost: '累积成本',
|
||||
totalPoints: '累积积分',
|
||||
levelRange: '等级范围'
|
||||
levelRange: '等级范围',
|
||||
|
||||
attackBonus: '攻击加成',
|
||||
shieldBonus: '护盾加成',
|
||||
armorBonus: '装甲加成',
|
||||
spyLevel: '侦查等级',
|
||||
researchQueueBonus: '研究队列',
|
||||
colonySlots: '殖民地槽位',
|
||||
forAllPlanets: '(全局)',
|
||||
speedBonus: '速度加成',
|
||||
researchSpeedBonus: '研究速度加成'
|
||||
},
|
||||
technologies: {
|
||||
energyTechnology: '能源技术',
|
||||
@@ -212,6 +253,12 @@ export default {
|
||||
hyperspaceTechnology: '超空间技术',
|
||||
plasmaTechnology: '等离子技术',
|
||||
computerTechnology: '计算机技术',
|
||||
espionageTechnology: '间谍技术',
|
||||
weaponsTechnology: '武器技术',
|
||||
shieldingTechnology: '护盾技术',
|
||||
armourTechnology: '装甲技术',
|
||||
astrophysics: '天体物理学',
|
||||
gravitonTechnology: '引力技术',
|
||||
combustionDrive: '燃烧引擎',
|
||||
impulseDrive: '脉冲引擎',
|
||||
hyperspaceDrive: '超空间引擎',
|
||||
@@ -226,6 +273,12 @@ export default {
|
||||
hyperspaceTechnology: '超空间跳跃技术',
|
||||
plasmaTechnology: '等离子武器技术',
|
||||
computerTechnology: '增加研究队列数量,每级+1队列(最多10级)',
|
||||
espionageTechnology: '提高间谍探测效果,每级提高1级侦查深度',
|
||||
weaponsTechnology: '提高舰船和防御的攻击力,每级+10%',
|
||||
shieldingTechnology: '提高舰船和防御的护盾值,每级+10%',
|
||||
armourTechnology: '提高舰船和防御的装甲值,每级+10%',
|
||||
astrophysics: '每级增加1个殖民地槽位,增加探险成功率',
|
||||
gravitonTechnology: '研究引力操纵,死星的必要技术',
|
||||
combustionDrive: '基础推进技术',
|
||||
impulseDrive: '中级推进技术',
|
||||
hyperspaceDrive: '高级推进技术',
|
||||
@@ -242,7 +295,7 @@ export default {
|
||||
darkMatterSpecialist: '暗物质专家',
|
||||
resourceBonus: '资源产量加成',
|
||||
darkMatterBonus: '暗物质产量加成',
|
||||
energyBonus: '能量产量加成'
|
||||
energyBonus: '电力产量加成'
|
||||
},
|
||||
officerDescriptions: {
|
||||
commander: '提升建筑速度和管理能力',
|
||||
@@ -253,8 +306,9 @@ export default {
|
||||
darkMatterSpecialist: '提升暗物质采集效率'
|
||||
},
|
||||
queue: {
|
||||
buildQueue: '建造队列',
|
||||
researchQueue: '研究队列',
|
||||
buildQueueBonus: '建造队列',
|
||||
spaceBonus: '空间加成',
|
||||
researchQueueBonus: '研究队列',
|
||||
building: '建造中',
|
||||
researching: '研究中',
|
||||
remaining: '剩余时间',
|
||||
@@ -273,11 +327,11 @@ export default {
|
||||
currentShips: '当前星球的舰船数量',
|
||||
productionSources: '资源获取来源',
|
||||
productionSourcesDesc: '详细的资源产量及加成信息',
|
||||
consumptionSources: '能量消耗来源',
|
||||
consumptionSourcesDesc: '各建筑的能量消耗详情',
|
||||
consumptionSources: '电力消耗来源',
|
||||
consumptionSourcesDesc: '各建筑的电力消耗详情',
|
||||
totalProduction: '总产量',
|
||||
totalConsumption: '总消耗',
|
||||
noConsumption: '当前无能量消耗'
|
||||
noConsumption: '当前无电力消耗'
|
||||
},
|
||||
buildingsView: {
|
||||
title: '建筑',
|
||||
@@ -294,7 +348,9 @@ export default {
|
||||
demolish: '拆除',
|
||||
demolishRefund: '拆除返还',
|
||||
demolishFailed: '拆除失败',
|
||||
demolishFailedMessage: '无法拆除该建筑,请检查建造队列是否已满或建筑等级是否为0。'
|
||||
demolishFailedMessage: '无法拆除该建筑,请检查建造队列是否已满或建筑等级是否为0。',
|
||||
confirmDemolish: '确认拆除',
|
||||
confirmDemolishMessage: '确定要拆除'
|
||||
},
|
||||
researchView: {
|
||||
title: '研究',
|
||||
@@ -416,7 +472,10 @@ export default {
|
||||
cargoExceedsCapacity: '载货量超出限制',
|
||||
noColonyShip: '需要殖民船才能执行殖民任务',
|
||||
noDebrisAtTarget: '目标坐标没有残骸场或残骸场已空',
|
||||
noDeathstar: '需要死星才能执行毁灭任务'
|
||||
noDeathstar: '需要死星才能执行毁灭任务',
|
||||
giftMode: '赠送模式',
|
||||
giftModeDescription: '将资源作为礼物赠送给',
|
||||
estimatedReputationGain: '预计好感度增加'
|
||||
},
|
||||
officersView: {
|
||||
title: '军官',
|
||||
@@ -461,6 +520,9 @@ export default {
|
||||
selectSystem: '选择星系',
|
||||
view: '查看',
|
||||
myPlanet: '我的星球',
|
||||
myPlanets: '我的星球',
|
||||
npcPlanets: 'NPC星球',
|
||||
selectPlanetToView: '选择要查看的星球',
|
||||
totalPositions: '共10个星球位置',
|
||||
mine: '我的',
|
||||
hostile: '敌对',
|
||||
@@ -470,20 +532,25 @@ export default {
|
||||
colonize: '殖民',
|
||||
switch: '切换',
|
||||
recycle: '回收',
|
||||
sendGift: '赠送礼物',
|
||||
debris: '残骸',
|
||||
debrisField: '残骸场',
|
||||
scoutPlanetTitle: '侦察星球',
|
||||
attackPlanetTitle: '攻击星球',
|
||||
colonizePlanetTitle: '殖民星球',
|
||||
recyclePlanetTitle: '回收残骸',
|
||||
giftPlanetTitle: '赠送礼物',
|
||||
scoutPlanetMessage: '确定要派遣间谍探测器侦察星球 [{coordinates}] 吗?\n\n请前往舰队页面选择舰船并派遣。',
|
||||
attackPlanetMessage: '确定要攻击星球 [{coordinates}] 吗?\n\n请前往舰队页面选择舰船并派遣。',
|
||||
colonizePlanetMessage: '确定要殖民位置 [{coordinates}] 吗?\n\n请前往舰队页面派遣殖民船。',
|
||||
recyclePlanetMessage: '确定要回收位置 [{coordinates}] 的残骸吗?\n\n请前往舰队页面派遣回收船。'
|
||||
recyclePlanetMessage: '确定要回收位置 [{coordinates}] 的残骸吗?\n\n请前往舰队页面派遣回收船。',
|
||||
giftPlanetMessage: '确定要向星球 [{coordinates}] 赠送资源吗?\n\n请前往舰队页面选择运输船并装载资源。'
|
||||
},
|
||||
messagesView: {
|
||||
title: '消息中心',
|
||||
battles: '战斗',
|
||||
spy: '侦查',
|
||||
npc: 'NPC',
|
||||
battleReports: '战斗报告',
|
||||
spyReports: '间谍报告',
|
||||
noBattleReports: '暂无战斗报告',
|
||||
@@ -514,7 +581,48 @@ export default {
|
||||
hideRoundDetails: '隐藏回合详情',
|
||||
round: '第{round}回合',
|
||||
attackerRemainingPower: '攻击方剩余火力',
|
||||
defenderRemainingPower: '防守方剩余火力'
|
||||
defenderRemainingPower: '防守方剩余火力',
|
||||
spied: '被侦查',
|
||||
spiedNotification: '被侦查通知',
|
||||
noSpiedNotifications: '暂无被侦查通知',
|
||||
detected: '已发现',
|
||||
undetected: '未发现',
|
||||
missions: '任务',
|
||||
noMissionReports: '暂无任务报告',
|
||||
success: '成功',
|
||||
failed: '失败',
|
||||
npcActivity: 'NPC活动',
|
||||
noNPCActivity: '暂无NPC活动通知',
|
||||
npcRecycleActivity: 'NPC回收残骸',
|
||||
gifts: '礼物',
|
||||
giftRejected: '被拒绝',
|
||||
noGiftNotifications: '暂无礼物通知',
|
||||
noGiftRejected: '暂无拒绝记录',
|
||||
giftFrom: '{npcName}的礼物',
|
||||
giftRejectedBy: '{npcName}拒绝了礼物',
|
||||
giftResources: '礼物资源',
|
||||
rejectedResources: '被拒绝的资源',
|
||||
expectedReputation: '预计好感度',
|
||||
currentReputation: '当前好感度',
|
||||
acceptGift: '接受',
|
||||
rejectGift: '拒绝',
|
||||
rejectionReason: {
|
||||
hostile: '对方对你有敌意,不接受礼物',
|
||||
neutral_distrust: '对方对你缺乏信任',
|
||||
polite_decline: '对方礼貌地拒绝了'
|
||||
}
|
||||
},
|
||||
missionReports: {
|
||||
transportSuccess: '运输任务成功完成',
|
||||
transportFailed: '运输任务失败',
|
||||
colonizeSuccess: '殖民任务成功,新星球已建立',
|
||||
colonizeFailed: '殖民任务失败',
|
||||
deploySuccess: '部署任务成功完成',
|
||||
deployFailed: '部署任务失败',
|
||||
recycleSuccess: '回收任务成功完成',
|
||||
recycleFailed: '回收任务失败,目标位置没有残骸',
|
||||
destroySuccess: '行星毁灭任务成功执行',
|
||||
destroyFailed: '行星毁灭任务失败'
|
||||
},
|
||||
simulatorView: {
|
||||
title: '战斗模拟器',
|
||||
@@ -616,9 +724,82 @@ export default {
|
||||
modifyOfficers: '修改军官',
|
||||
officersDesc: '快速设置军官到期时间',
|
||||
days: '天',
|
||||
npcTesting: 'NPC 测试',
|
||||
npcTestingDesc: '测试NPC侦查和攻击行为',
|
||||
selectNPC: '选择NPC',
|
||||
chooseNPC: '选择一个NPC',
|
||||
targetPlanet: '目标星球',
|
||||
chooseTarget: '选择目标星球',
|
||||
testSpy: '测试侦查',
|
||||
testAttack: '测试攻击',
|
||||
testSpyAndAttack: '测试侦查&攻击',
|
||||
initializeFleet: '初始化NPC舰队',
|
||||
accelerateMissions: '加速所有任务(5秒)',
|
||||
selectNPCFirst: '请先选择一个NPC',
|
||||
npcNoProbes: 'NPC没有间谍探测器',
|
||||
npcNoSpyReport: 'NPC需要先侦查',
|
||||
npcMissionFailed: '创建任务失败',
|
||||
dangerZone: '危险区域',
|
||||
dangerZoneDesc: '以下操作不可撤销,请谨慎操作',
|
||||
resetGame: '重置游戏',
|
||||
resetGameConfirm: '确定要重置游戏吗?这将删除所有数据!'
|
||||
},
|
||||
alerts: {
|
||||
npcSpyIncoming: 'NPC侦查即将到达',
|
||||
npcAttackIncoming: 'NPC舰队来袭!',
|
||||
npcFleetIncoming: 'NPC舰队接近',
|
||||
ships: '艘舰船',
|
||||
spiedBy: '被侦查',
|
||||
attackedBy: '被攻击',
|
||||
detectionSuccess: '侦查被发现',
|
||||
detectionFailed: '侦查未被发现',
|
||||
npcSpiedYourPlanet: 'NPC侦查了你的星球',
|
||||
npcAttackedYourPlanet: 'NPC攻击了你的星球'
|
||||
},
|
||||
diplomacy: {
|
||||
title: '外交',
|
||||
description: '管理与NPC的外交关系',
|
||||
tabs: {
|
||||
all: '全部',
|
||||
friendly: '友好',
|
||||
neutral: '中立',
|
||||
hostile: '敌对'
|
||||
},
|
||||
noNpcs: '暂无NPC',
|
||||
noFriendlyNpcs: '暂无友好NPC',
|
||||
noNeutralNpcs: '暂无中立NPC',
|
||||
noHostileNpcs: '暂无敌对NPC',
|
||||
recentEvents: '最近事件',
|
||||
recentEventsDescription: '最近的外交活动记录',
|
||||
ago: '前',
|
||||
status: {
|
||||
friendly: '友好',
|
||||
neutral: '中立',
|
||||
hostile: '敌对'
|
||||
},
|
||||
planets: '个星球',
|
||||
allies: '个盟友',
|
||||
reputation: '好感度',
|
||||
alliedWith: '盟友',
|
||||
more: '更多',
|
||||
actions: {
|
||||
gift: '赠送资源',
|
||||
viewPlanets: '查看星球'
|
||||
},
|
||||
lastEvent: '最近活动',
|
||||
events: {
|
||||
gift: '赠送资源',
|
||||
attack: '攻击',
|
||||
allyAttacked: '攻击盟友',
|
||||
spy: '侦查',
|
||||
stealDebris: '抢夺残骸'
|
||||
}
|
||||
},
|
||||
pagination: {
|
||||
previous: '上一页',
|
||||
next: '下一页',
|
||||
first: '首页',
|
||||
last: '末页',
|
||||
page: '第 {page} 页'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ export default {
|
||||
officers: '軍官',
|
||||
simulator: '模擬',
|
||||
galaxy: '星系',
|
||||
diplomacy: 'Diplomacy',
|
||||
messages: '訊息',
|
||||
settings: '設定',
|
||||
gm: 'GM'
|
||||
@@ -78,7 +79,7 @@ export default {
|
||||
crystal: '晶體',
|
||||
deuterium: '重氫',
|
||||
darkMatter: '暗物質',
|
||||
energy: '能量',
|
||||
energy: '電力',
|
||||
production: '產量',
|
||||
consumption: '消耗',
|
||||
capacity: '容量',
|
||||
@@ -87,7 +88,7 @@ export default {
|
||||
perHour: '小時',
|
||||
perMinute: '分鐘',
|
||||
hour: '小時',
|
||||
noEnergy: '能量不足'
|
||||
noEnergy: '電力不足'
|
||||
},
|
||||
planet: {
|
||||
planet: '星球',
|
||||
@@ -97,6 +98,8 @@ export default {
|
||||
coordinates: '座標',
|
||||
switchToMoon: '查看月球',
|
||||
backToPlanet: '返回母星',
|
||||
switchPlanet: '切換星球',
|
||||
currentPlanet: '當前星球',
|
||||
fields: '場地',
|
||||
temperature: '溫度',
|
||||
homePlanet: '母星',
|
||||
@@ -112,6 +115,7 @@ export default {
|
||||
crystalMine: '晶體礦',
|
||||
deuteriumSynthesizer: '重氫合成器',
|
||||
solarPlant: '太陽能電站',
|
||||
fusionReactor: '核聚變反應堆',
|
||||
roboticsFactory: '機器人工廠',
|
||||
naniteFactory: '納米工廠',
|
||||
shipyard: '船塢',
|
||||
@@ -120,6 +124,8 @@ export default {
|
||||
crystalStorage: '晶體倉庫',
|
||||
deuteriumTank: '重氫罐',
|
||||
darkMatterCollector: '暗物質收集器',
|
||||
darkMatterTank: '暗物質儲罐',
|
||||
missileSilo: '導彈發射井',
|
||||
terraformer: '地形改造器',
|
||||
lunarBase: '月球基地',
|
||||
sensorPhalanx: '傳感器陣列',
|
||||
@@ -130,13 +136,26 @@ export default {
|
||||
consumption: '消耗',
|
||||
totalCost: '累積成本',
|
||||
totalPoints: '累積積分',
|
||||
levelRange: '等級範圍'
|
||||
levelRange: '等級範圍',
|
||||
capacity: '容量/效果',
|
||||
storageCapacity: '容量',
|
||||
energyProduction: '電力產出',
|
||||
fleetStorage: '艦隊倉儲',
|
||||
buildQueue: '建造隊列',
|
||||
buildQueueBonus: '建造隊列',
|
||||
spaceBonus: '空間加成',
|
||||
buildSpeedBonus: '建造速度加成',
|
||||
researchSpeedBonus: '研究速度加成',
|
||||
planetSpace: '行星空間',
|
||||
moonSpace: '月球空間',
|
||||
missileCapacity: '導彈容量'
|
||||
},
|
||||
buildingDescriptions: {
|
||||
metalMine: '開採金屬資源',
|
||||
crystalMine: '開採晶體資源',
|
||||
deuteriumSynthesizer: '合成重氫資源',
|
||||
solarPlant: '提供能源',
|
||||
fusionReactor: '使用重氫產生大量能源',
|
||||
roboticsFactory: '加快建造速度',
|
||||
naniteFactory: '增加建造佇列數量,每級+1佇列(最多10級)',
|
||||
shipyard: '建造艦船',
|
||||
@@ -145,6 +164,8 @@ export default {
|
||||
crystalStorage: '增加晶體儲存上限',
|
||||
deuteriumTank: '增加重氫儲存上限',
|
||||
darkMatterCollector: '收集稀有的暗物質資源',
|
||||
darkMatterTank: '增加暗物質儲存上限',
|
||||
missileSilo: '存儲和發射導彈,每級可存儲10枚導彈',
|
||||
terraformer: '改造行星地形,每級增加5個可用空間',
|
||||
lunarBase: '增加月球可用空間,每級+5空間',
|
||||
sensorPhalanx: '偵測周圍星系的艦隊活動',
|
||||
@@ -156,11 +177,15 @@ export default {
|
||||
heavyFighter: '重型戰鬥機',
|
||||
cruiser: '巡洋艦',
|
||||
battleship: '戰列艦',
|
||||
battlecruiser: '戰列巡洋艦',
|
||||
bomber: '轟炸機',
|
||||
destroyer: '驅逐艦',
|
||||
smallCargo: '小型運輸船',
|
||||
largeCargo: '大型運輸船',
|
||||
colonyShip: '殖民船',
|
||||
recycler: '回收船',
|
||||
espionageProbe: '間諜探測器',
|
||||
solarSatellite: '太陽能衛星',
|
||||
darkMatterHarvester: '暗物質採集船',
|
||||
deathstar: '死星'
|
||||
},
|
||||
@@ -169,11 +194,15 @@ export default {
|
||||
heavyFighter: '重裝戰鬥機',
|
||||
cruiser: '中型戰艦,攻守平衡',
|
||||
battleship: '強力戰艦',
|
||||
battlecruiser: '快速強大的戰鬥艦船,擅長攻擊戰列艦',
|
||||
bomber: '專門對付防禦設施的轟炸艦',
|
||||
destroyer: '擅長摧毀大型艦船的獵殺者',
|
||||
smallCargo: '運輸少量資源',
|
||||
largeCargo: '運輸大量資源',
|
||||
colonyShip: '用於殖民新星球',
|
||||
recycler: '收集殘骸場資源',
|
||||
espionageProbe: '偵察敵方星球',
|
||||
solarSatellite: '提供額外能源,每個產生50點電力',
|
||||
darkMatterHarvester: '專門用於採集暗物質的特殊飛船',
|
||||
deathstar: '終極武器,能夠摧毀整個行星'
|
||||
},
|
||||
@@ -186,24 +215,38 @@ export default {
|
||||
plasmaTurret: '等離子炮塔',
|
||||
smallShieldDome: '小型護盾罩',
|
||||
largeShieldDome: '大型護盾罩',
|
||||
antiBallisticMissile: '反彈道導彈',
|
||||
interplanetaryMissile: '星際導彈',
|
||||
planetaryShield: '行星護盾'
|
||||
},
|
||||
defenseDescriptions: {
|
||||
rocketLauncher: '基礎防禦設施',
|
||||
lightLaser: '輕型能量武器',
|
||||
heavyLaser: '重型能量武器',
|
||||
lightLaser: '輕型電力武器',
|
||||
heavyLaser: '重型電力武器',
|
||||
gaussCannon: '高速動能武器',
|
||||
ionCannon: '破壞護盾的利器',
|
||||
plasmaTurret: '強力防禦設施',
|
||||
smallShieldDome: '保護整個星球的小型護盾',
|
||||
largeShieldDome: '保護整個星球的大型護盾',
|
||||
antiBallisticMissile: '攔截敵方導彈,每個可攔截1枚星際導彈',
|
||||
interplanetaryMissile: '可以攻擊其他星球的防禦設施',
|
||||
planetaryShield: '保護行星免受毀滅攻擊的超級護盾'
|
||||
},
|
||||
research: {
|
||||
researchTime: '研究時間',
|
||||
totalCost: '累積成本',
|
||||
totalPoints: '累積積分',
|
||||
levelRange: '等級範圍'
|
||||
levelRange: '等級範圍',
|
||||
capacity: '容量/效果',
|
||||
attackBonus: '攻擊加成',
|
||||
shieldBonus: '護盾加成',
|
||||
armorBonus: '裝甲加成',
|
||||
spyLevel: '偵查等級',
|
||||
researchQueueBonus: '研究隊列',
|
||||
colonySlots: '殖民地槽位',
|
||||
forAllPlanets: '(全局)',
|
||||
speedBonus: '速度加成',
|
||||
researchSpeedBonus: '研究速度加成'
|
||||
},
|
||||
technologies: {
|
||||
energyTechnology: '能源技術',
|
||||
@@ -212,6 +255,12 @@ export default {
|
||||
hyperspaceTechnology: '超空間技術',
|
||||
plasmaTechnology: '等離子技術',
|
||||
computerTechnology: '計算機技術',
|
||||
espionageTechnology: '間諜技術',
|
||||
weaponsTechnology: '武器技術',
|
||||
shieldingTechnology: '護盾技術',
|
||||
armourTechnology: '裝甲技術',
|
||||
astrophysics: '天體物理學',
|
||||
gravitonTechnology: '引力技術',
|
||||
combustionDrive: '燃燒引擎',
|
||||
impulseDrive: '脈衝引擎',
|
||||
hyperspaceDrive: '超空間引擎',
|
||||
@@ -226,6 +275,12 @@ export default {
|
||||
hyperspaceTechnology: '超空間跳躍技術',
|
||||
plasmaTechnology: '等離子武器技術',
|
||||
computerTechnology: '增加研究佇列數量,每級+1佇列(最多10級)',
|
||||
espionageTechnology: '提高間諜探測效果,每級提高1級偵查深度',
|
||||
weaponsTechnology: '提高艦船和防禦的攻擊力,每級+10%',
|
||||
shieldingTechnology: '提高艦船和防禦的護盾值,每級+10%',
|
||||
armourTechnology: '提高艦船和防禦的裝甲值,每級+10%',
|
||||
astrophysics: '每級增加1個殖民地槽位,增加探險成功率',
|
||||
gravitonTechnology: '研究引力操縱,死星的必要技術',
|
||||
combustionDrive: '基礎推進技術',
|
||||
impulseDrive: '中級推進技術',
|
||||
hyperspaceDrive: '高級推進技術',
|
||||
@@ -242,7 +297,7 @@ export default {
|
||||
darkMatterSpecialist: '暗物質專家',
|
||||
resourceBonus: '資源生產加成',
|
||||
darkMatterBonus: '暗物質生產加成',
|
||||
energyBonus: '能量生產加成'
|
||||
energyBonus: '電力產量加成'
|
||||
},
|
||||
officerDescriptions: {
|
||||
commander: '提升建築速度和管理能力',
|
||||
@@ -272,11 +327,11 @@ export default {
|
||||
currentShips: '當前星球的艦船數量',
|
||||
productionSources: '生產來源',
|
||||
productionSourcesDesc: '詳細資源生產和加成資訊',
|
||||
consumptionSources: '消耗來源',
|
||||
consumptionSourcesDesc: '建築能量消耗詳情',
|
||||
consumptionSources: '電力消耗來源',
|
||||
consumptionSourcesDesc: '各建築的電力消耗詳情',
|
||||
totalProduction: '總產量',
|
||||
totalConsumption: '總消耗',
|
||||
noConsumption: '無能量消耗'
|
||||
noConsumption: '當前無電力消耗'
|
||||
},
|
||||
buildingsView: {
|
||||
title: '建築',
|
||||
@@ -293,7 +348,9 @@ export default {
|
||||
demolish: '拆除',
|
||||
demolishRefund: '拆除返還',
|
||||
demolishFailed: '拆除失敗',
|
||||
demolishFailedMessage: '無法拆除該建築,請檢查建造隊列是否已滿或建築等級是否為0。'
|
||||
demolishFailedMessage: '無法拆除該建築,請檢查建造隊列是否已滿或建築等級是否為0。',
|
||||
confirmDemolish: '',
|
||||
confirmDemolishMessage: ''
|
||||
},
|
||||
researchView: {
|
||||
title: '研究',
|
||||
@@ -380,6 +437,7 @@ export default {
|
||||
all: '全部',
|
||||
targetCoordinates: '目標座標',
|
||||
galaxy: '銀河系',
|
||||
diplomacy: 'Diplomacy',
|
||||
system: '星系',
|
||||
position: '位置',
|
||||
missionType: '任務類型',
|
||||
@@ -413,7 +471,11 @@ export default {
|
||||
cannotSendToOwnPlanet: '無法派遣艦隊到自己的星球',
|
||||
cargoExceedsCapacity: '載貨量超出限制',
|
||||
noColonyShip: '需要殖民船才能執行殖民任務',
|
||||
noDebrisAtTarget: '目標坐標沒有殘骸場或殘骸場已空'
|
||||
noDebrisAtTarget: '目標坐標沒有殘骸場或殘骸場已空',
|
||||
noDeathstar: '需要死星才能執行毀滅任務',
|
||||
giftMode: '贈送模式',
|
||||
giftModeDescription: '將資源作為禮物贈送給',
|
||||
estimatedReputationGain: '預計好感度增加'
|
||||
},
|
||||
officersView: {
|
||||
title: '軍官',
|
||||
@@ -453,11 +515,15 @@ export default {
|
||||
title: '星系',
|
||||
selectCoordinates: '選擇座標',
|
||||
galaxy: '銀河系',
|
||||
diplomacy: 'Diplomacy',
|
||||
selectGalaxy: '選擇銀河系',
|
||||
system: '星系',
|
||||
selectSystem: '選擇星系',
|
||||
view: '查看',
|
||||
myPlanet: '我的星球',
|
||||
myPlanets: '我的星球',
|
||||
npcPlanets: 'NPC星球',
|
||||
selectPlanetToView: '選擇要查看的星球',
|
||||
totalPositions: '共10個星球位置',
|
||||
mine: '我的',
|
||||
hostile: '敵對',
|
||||
@@ -475,12 +541,17 @@ export default {
|
||||
scoutPlanetMessage: '確定要派遣間諜探測器偵察星球 [{coordinates}] 嗎?\n\n請前往艦隊頁面選擇艦船並派遣。',
|
||||
attackPlanetMessage: '確定要攻擊星球 [{coordinates}] 嗎?\n\n請前往艦隊頁面選擇艦船並派遣。',
|
||||
colonizePlanetMessage: '確定要殖民位置 [{coordinates}] 嗎?\n\n請前往艦隊頁面派遣殖民船。',
|
||||
recyclePlanetMessage: '確定要回收位置 [{coordinates}] 的殘骸嗎?\n\n請前往艦隊頁面派遣回收船。'
|
||||
recyclePlanetMessage: '確定要回收位置 [{coordinates}] 的殘骸嗎?\n\n請前往艦隊頁面派遣回收船。',
|
||||
sendGift: '贈送禮物',
|
||||
debris: '殘骸',
|
||||
giftPlanetTitle: '贈送禮物',
|
||||
giftPlanetMessage: '確定要向星球 [{coordinates}] 贈送資源嗎?\n\n請前往艦隊頁面選擇運輸船並裝載資源。'
|
||||
},
|
||||
messagesView: {
|
||||
title: '訊息中心',
|
||||
battles: '戰鬥',
|
||||
spy: '偵查',
|
||||
npc: 'NPC',
|
||||
battleReports: '戰鬥報告',
|
||||
spyReports: '間諜報告',
|
||||
noBattleReports: '暫無戰鬥報告',
|
||||
@@ -511,7 +582,48 @@ export default {
|
||||
hideRoundDetails: '隱藏回合詳情',
|
||||
round: '第{round}回合',
|
||||
attackerRemainingPower: '攻擊方剩餘火力',
|
||||
defenderRemainingPower: '防守方剩餘火力'
|
||||
defenderRemainingPower: '防守方剩餘火力',
|
||||
spied: '被偵查',
|
||||
spiedNotification: '被偵查通知',
|
||||
noSpiedNotifications: '暫無被偵查通知',
|
||||
detected: '已發現',
|
||||
undetected: '未發現',
|
||||
missions: '任務',
|
||||
noMissionReports: '暫無任務報告',
|
||||
success: '成功',
|
||||
failed: '失敗',
|
||||
npcActivity: 'NPC活動',
|
||||
noNPCActivity: '暫無NPC活動通知',
|
||||
npcRecycleActivity: 'NPC回收殘骸',
|
||||
gifts: '禮物',
|
||||
giftRejected: '被拒絕',
|
||||
noGiftNotifications: '暫無禮物通知',
|
||||
noGiftRejected: '暫無拒絕記錄',
|
||||
giftFrom: '{npcName}的禮物',
|
||||
giftRejectedBy: '{npcName}拒絕了禮物',
|
||||
giftResources: '禮物資源',
|
||||
rejectedResources: '被拒絕的資源',
|
||||
expectedReputation: '預計好感度',
|
||||
currentReputation: '當前好感度',
|
||||
acceptGift: '接受',
|
||||
rejectGift: '拒絕',
|
||||
rejectionReason: {
|
||||
hostile: '對方對你有敵意,不接受禮物',
|
||||
neutral_distrust: '對方對你缺乏信任',
|
||||
polite_decline: '對方禮貌地拒絕了'
|
||||
}
|
||||
},
|
||||
missionReports: {
|
||||
transportSuccess: '運輸任務成功完成',
|
||||
transportFailed: '運輸任務失敗',
|
||||
colonizeSuccess: '殖民任務成功,新星球已建立',
|
||||
colonizeFailed: '殖民任務失敗',
|
||||
deploySuccess: '部署任務成功完成',
|
||||
deployFailed: '部署任務失敗',
|
||||
recycleSuccess: '回收任務成功完成',
|
||||
recycleFailed: '回收任務失敗,目標位置沒有殘骸',
|
||||
destroySuccess: '行星毀滅任務成功執行',
|
||||
destroyFailed: '行星毀滅任務失敗'
|
||||
},
|
||||
simulatorView: {
|
||||
title: '戰鬥模擬器',
|
||||
@@ -613,9 +725,82 @@ export default {
|
||||
modifyOfficers: '修改軍官',
|
||||
officersDesc: '快速設定軍官到期時間',
|
||||
days: '天',
|
||||
npcTesting: 'NPC 測試',
|
||||
npcTestingDesc: '測試NPC偵查和攻擊行為',
|
||||
selectNPC: '選擇NPC',
|
||||
chooseNPC: '選擇一個NPC',
|
||||
targetPlanet: '目標星球',
|
||||
chooseTarget: '選擇目標星球',
|
||||
testSpy: '測試偵查',
|
||||
testAttack: '測試攻擊',
|
||||
testSpyAndAttack: '測試偵查&攻擊',
|
||||
initializeFleet: '初始化NPC艦隊',
|
||||
accelerateMissions: '加速所有任務(5秒)',
|
||||
selectNPCFirst: '請先選擇一個NPC',
|
||||
npcNoProbes: 'NPC沒有間諜探測器',
|
||||
npcNoSpyReport: 'NPC需要先偵查',
|
||||
npcMissionFailed: '創建任務失敗',
|
||||
dangerZone: '危險區域',
|
||||
dangerZoneDesc: '以下操作不可撤銷,請謹慎操作',
|
||||
resetGame: '重置遊戲',
|
||||
resetGameConfirm: '確定要重置遊戲嗎?這將刪除所有資料!'
|
||||
},
|
||||
alerts: {
|
||||
npcSpyIncoming: 'NPC偵查即將到達',
|
||||
npcAttackIncoming: 'NPC艦隊來襲!',
|
||||
npcFleetIncoming: 'NPC艦隊接近',
|
||||
ships: '艘艦船',
|
||||
spiedBy: '被偵查',
|
||||
attackedBy: '被攻擊',
|
||||
detectionSuccess: '偵查被發現',
|
||||
detectionFailed: '偵查未被發現',
|
||||
npcSpiedYourPlanet: 'NPC偵查了你的星球',
|
||||
npcAttackedYourPlanet: 'NPC攻擊了你的星球'
|
||||
},
|
||||
diplomacy: {
|
||||
title: '外交',
|
||||
description: '管理與NPC的外交關係',
|
||||
tabs: {
|
||||
all: '全部',
|
||||
friendly: '友好',
|
||||
neutral: '中立',
|
||||
hostile: '敵對'
|
||||
},
|
||||
noNpcs: '沒有NPC',
|
||||
noFriendlyNpcs: '沒有友好的NPC',
|
||||
noNeutralNpcs: '沒有中立的NPC',
|
||||
noHostileNpcs: '沒有敵對的NPC',
|
||||
recentEvents: '最近事件',
|
||||
recentEventsDescription: '最近的外交活動記錄',
|
||||
ago: '前',
|
||||
status: {
|
||||
friendly: '友好',
|
||||
neutral: '中立',
|
||||
hostile: '敵對'
|
||||
},
|
||||
planets: '星球',
|
||||
allies: '盟友',
|
||||
reputation: '聲望',
|
||||
alliedWith: '結盟對象',
|
||||
more: '更多',
|
||||
actions: {
|
||||
gift: '贈送禮物',
|
||||
viewPlanets: '查看星球'
|
||||
},
|
||||
lastEvent: '最近事件',
|
||||
events: {
|
||||
gift: '已贈送禮物',
|
||||
attack: '攻擊',
|
||||
allyAttacked: '盟友被攻擊',
|
||||
spy: '間諜活動',
|
||||
stealDebris: '掠奪殘骸'
|
||||
}
|
||||
},
|
||||
pagination: {
|
||||
previous: '上一頁',
|
||||
next: '下一頁',
|
||||
first: '首頁',
|
||||
last: '末頁',
|
||||
page: '第 {page} 頁'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { Fleet, Resources, BattleResult, Officer } from '@/types/game'
|
||||
import type { Fleet, Resources, BattleResult, Officer, TechnologyType } from '@/types/game'
|
||||
import { DefenseType, OfficerType } from '@/types/game'
|
||||
import * as officerLogic from './officerLogic'
|
||||
import { workerManager } from '@/workers/workerManager'
|
||||
|
||||
/**
|
||||
@@ -12,31 +11,34 @@ export const simulateBattle = async (
|
||||
defenderFleet: Partial<Fleet>,
|
||||
defenderDefense: Partial<Record<DefenseType, number>>,
|
||||
defenderResources: Resources,
|
||||
attackerOfficers: Record<OfficerType, Officer>,
|
||||
defenderOfficers: Record<OfficerType, Officer>
|
||||
_attackerOfficers: Record<OfficerType, Officer>,
|
||||
_defenderOfficers: Record<OfficerType, Officer>,
|
||||
attackerTechnologies: Record<TechnologyType, number>,
|
||||
defenderTechnologies: Record<TechnologyType, number>
|
||||
): Promise<BattleResult> => {
|
||||
// 计算军官加成
|
||||
const attackerBonuses = officerLogic.calculateActiveBonuses(attackerOfficers, Date.now())
|
||||
const defenderBonuses = officerLogic.calculateActiveBonuses(defenderOfficers, Date.now())
|
||||
// 从科技系统读取实际科技等级
|
||||
const attackerWeaponTech = attackerTechnologies['weaponsTechnology'] || 0
|
||||
const attackerShieldTech = attackerTechnologies['shieldingTechnology'] || 0
|
||||
const attackerArmorTech = attackerTechnologies['armourTechnology'] || 0
|
||||
|
||||
// 将防御加成转换为科技等级(简化:10%加成 = 1级科技)
|
||||
const attackerTechLevel = Math.floor(attackerBonuses.defenseBonus / 10)
|
||||
const defenderTechLevel = Math.floor(defenderBonuses.defenseBonus / 10)
|
||||
const defenderWeaponTech = defenderTechnologies['weaponsTechnology'] || 0
|
||||
const defenderShieldTech = defenderTechnologies['shieldingTechnology'] || 0
|
||||
const defenderArmorTech = defenderTechnologies['armourTechnology'] || 0
|
||||
|
||||
// 使用 Worker 执行战斗模拟
|
||||
const simulationResult = await workerManager.simulateBattle({
|
||||
attacker: {
|
||||
ships: attackerFleet,
|
||||
weaponTech: 0, // 暂时不考虑武器科技
|
||||
shieldTech: attackerTechLevel,
|
||||
armorTech: attackerTechLevel
|
||||
weaponTech: attackerWeaponTech,
|
||||
shieldTech: attackerShieldTech,
|
||||
armorTech: attackerArmorTech
|
||||
},
|
||||
defender: {
|
||||
ships: defenderFleet,
|
||||
defense: defenderDefense,
|
||||
weaponTech: 0,
|
||||
shieldTech: defenderTechLevel,
|
||||
armorTech: defenderTechLevel
|
||||
weaponTech: defenderWeaponTech,
|
||||
shieldTech: defenderShieldTech,
|
||||
armorTech: defenderArmorTech
|
||||
},
|
||||
maxRounds: 6 // 最多6回合
|
||||
})
|
||||
|
||||
@@ -20,13 +20,30 @@ export const calculateBuildingCost = (buildingType: BuildingType, targetLevel: n
|
||||
|
||||
/**
|
||||
* 计算建筑升级时间
|
||||
* @param buildingType 建筑类型
|
||||
* @param targetLevel 目标等级
|
||||
* @param buildingSpeedBonus 指挥官等提供的速度加成百分比
|
||||
* @param roboticsFactoryLevel 机器人工厂等级
|
||||
* @param naniteFactoryLevel 纳米工厂等级
|
||||
*/
|
||||
export const calculateBuildingTime = (buildingType: BuildingType, targetLevel: number, buildingSpeedBonus: number = 0): number => {
|
||||
export const calculateBuildingTime = (
|
||||
buildingType: BuildingType,
|
||||
targetLevel: number,
|
||||
buildingSpeedBonus: number = 0,
|
||||
roboticsFactoryLevel: number = 0,
|
||||
naniteFactoryLevel: number = 0
|
||||
): number => {
|
||||
const config = BUILDINGS[buildingType]
|
||||
const multiplier = Math.pow(config.costMultiplier, targetLevel - 1)
|
||||
const baseTime = config.baseTime * multiplier
|
||||
|
||||
// 机器人工厂和纳米工厂的加速:建造时间 / (1 + 机器人工厂等级 + 纳米工厂等级 × 2)
|
||||
const factorySpeedDivisor = 1 + roboticsFactoryLevel + naniteFactoryLevel * 2
|
||||
|
||||
// 指挥官等的百分比加成
|
||||
const speedMultiplier = 1 - buildingSpeedBonus / 100
|
||||
return Math.floor(baseTime * speedMultiplier)
|
||||
|
||||
return Math.floor((baseTime / factorySpeedDivisor) * speedMultiplier)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -98,7 +115,11 @@ export const createBuildQueueItem = (buildingType: BuildingType, targetLevel: nu
|
||||
/**
|
||||
* 处理建造完成
|
||||
*/
|
||||
export const completeBuildQueue = (planet: Planet, now: number, onPointsEarned?: (points: number, type: 'building' | 'ship' | 'defense', itemType: string, level?: number, quantity?: number) => void): void => {
|
||||
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) {
|
||||
// 建造完成
|
||||
@@ -167,10 +188,18 @@ export const calculateDemolishRefund = (buildingType: BuildingType, currentLevel
|
||||
* @param buildingType 建筑类型
|
||||
* @param currentLevel 当前等级
|
||||
* @param buildingSpeedBonus 建筑速度加成
|
||||
* @param roboticsFactoryLevel 机器人工厂等级
|
||||
* @param naniteFactoryLevel 纳米工厂等级
|
||||
* @returns 拆除时间(建造时间的50%)
|
||||
*/
|
||||
export const calculateDemolishTime = (buildingType: BuildingType, currentLevel: number, buildingSpeedBonus: number = 0): number => {
|
||||
const buildTime = calculateBuildingTime(buildingType, currentLevel, buildingSpeedBonus)
|
||||
export const calculateDemolishTime = (
|
||||
buildingType: BuildingType,
|
||||
currentLevel: number,
|
||||
buildingSpeedBonus: number = 0,
|
||||
roboticsFactoryLevel: number = 0,
|
||||
naniteFactoryLevel: number = 0
|
||||
): number => {
|
||||
const buildTime = calculateBuildingTime(buildingType, currentLevel, buildingSpeedBonus, roboticsFactoryLevel, naniteFactoryLevel)
|
||||
return Math.floor(buildTime * 0.5)
|
||||
}
|
||||
|
||||
|
||||
@@ -34,9 +34,10 @@ export const validateBuildingUpgrade = (
|
||||
// 计算军官加成
|
||||
const bonuses = officerLogic.calculateActiveBonuses(officers, Date.now())
|
||||
|
||||
// 检查建造队列是否已满
|
||||
// 检查建造队列是否已满(只计算建筑类型的队列项)
|
||||
const maxQueue = publicLogic.getMaxBuildQueue(planet, bonuses.additionalBuildQueue)
|
||||
if (planet.buildQueue.length >= maxQueue) {
|
||||
const buildingQueueCount = planet.buildQueue.filter(item => item.type === 'building' || item.type === 'demolish').length
|
||||
if (buildingQueueCount >= maxQueue) {
|
||||
return { valid: false, reason: 'errors.buildQueueFull' }
|
||||
}
|
||||
|
||||
@@ -61,14 +62,29 @@ export const validateBuildingUpgrade = (
|
||||
/**
|
||||
* 执行建筑升级(扣除资源,添加到队列)
|
||||
*/
|
||||
export const executeBuildingUpgrade = (planet: Planet, buildingType: BuildingType, officers: Record<OfficerType, Officer>): BuildQueueItem => {
|
||||
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)
|
||||
|
||||
// 获取机器人工厂和纳米工厂等级
|
||||
const roboticsFactoryLevel = planet.buildings[BuildingType.RoboticsFactory] || 0
|
||||
const naniteFactoryLevel = planet.buildings[BuildingType.NaniteFactory] || 0
|
||||
|
||||
const time = buildingLogic.calculateBuildingTime(
|
||||
buildingType,
|
||||
targetLevel,
|
||||
bonuses.buildingSpeedBonus,
|
||||
roboticsFactoryLevel,
|
||||
naniteFactoryLevel
|
||||
)
|
||||
|
||||
// 扣除资源
|
||||
resourceLogic.deductResources(planet.resources, cost)
|
||||
@@ -130,9 +146,10 @@ export const validateBuildingDemolish = (
|
||||
// 计算军官加成
|
||||
const bonuses = officerLogic.calculateActiveBonuses(officers, Date.now())
|
||||
|
||||
// 检查建造队列是否已满
|
||||
// 检查建造队列是否已满(只计算建筑类型的队列项)
|
||||
const maxQueue = publicLogic.getMaxBuildQueue(planet, bonuses.additionalBuildQueue)
|
||||
if (planet.buildQueue.length >= maxQueue) {
|
||||
const buildingQueueCount = planet.buildQueue.filter(item => item.type === 'building' || item.type === 'demolish').length
|
||||
if (buildingQueueCount >= maxQueue) {
|
||||
return { valid: false, reason: 'errors.buildQueueFull' }
|
||||
}
|
||||
|
||||
@@ -142,12 +159,27 @@ export const validateBuildingDemolish = (
|
||||
/**
|
||||
* 执行建筑拆除(返还资源,添加到队列)
|
||||
*/
|
||||
export const executeBuildingDemolish = (planet: Planet, buildingType: BuildingType, officers: Record<OfficerType, Officer>): BuildQueueItem => {
|
||||
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)
|
||||
|
||||
// 获取机器人工厂和纳米工厂等级
|
||||
const roboticsFactoryLevel = planet.buildings[BuildingType.RoboticsFactory] || 0
|
||||
const naniteFactoryLevel = planet.buildings[BuildingType.NaniteFactory] || 0
|
||||
|
||||
const demolishTime = buildingLogic.calculateDemolishTime(
|
||||
buildingType,
|
||||
currentLevel,
|
||||
bonuses.buildingSpeedBonus,
|
||||
roboticsFactoryLevel,
|
||||
naniteFactoryLevel
|
||||
)
|
||||
|
||||
// 返还50%资源
|
||||
const refund = buildingLogic.calculateDemolishRefund(buildingType, currentLevel)
|
||||
|
||||
626
src/logic/diplomaticLogic.ts
Normal file
626
src/logic/diplomaticLogic.ts
Normal file
@@ -0,0 +1,626 @@
|
||||
/**
|
||||
* 外交系统逻辑
|
||||
*
|
||||
* 管理玩家与NPC之间的双向好感度、关系状态和外交事件
|
||||
*/
|
||||
|
||||
import { DIPLOMATIC_CONFIG } from '@/config/gameConfig'
|
||||
import type {
|
||||
DiplomaticRelation,
|
||||
RelationStatus,
|
||||
DiplomaticEventType,
|
||||
DiplomaticReport,
|
||||
Resources,
|
||||
Player,
|
||||
NPC,
|
||||
FleetMission,
|
||||
BattleResult,
|
||||
Position,
|
||||
GiftNotification,
|
||||
GiftRejectedNotification
|
||||
} from '@/types/game'
|
||||
import { RelationStatus as RS, DiplomaticEventType as DET } from '@/types/game'
|
||||
|
||||
/**
|
||||
* 根据好感度值计算关系状态
|
||||
* @param reputation 好感度值 (-100 到 +100)
|
||||
* @returns 关系状态
|
||||
*/
|
||||
export const calculateRelationStatus = (reputation: number): RelationStatus => {
|
||||
if (reputation <= DIPLOMATIC_CONFIG.HOSTILE_THRESHOLD) {
|
||||
return RS.Hostile
|
||||
} else if (reputation >= DIPLOMATIC_CONFIG.FRIENDLY_THRESHOLD) {
|
||||
return RS.Friendly
|
||||
}
|
||||
return RS.Neutral
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化外交关系
|
||||
* @param fromId 关系发起方ID
|
||||
* @param toId 关系接收方ID
|
||||
* @returns 新的外交关系对象
|
||||
*/
|
||||
export const initializeDiplomaticRelation = (fromId: string, toId: string): DiplomaticRelation => {
|
||||
return {
|
||||
fromId,
|
||||
toId,
|
||||
reputation: 0, // 初始中立
|
||||
status: RS.Neutral,
|
||||
lastUpdated: Date.now(),
|
||||
history: []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新好感度值
|
||||
* @param relation 外交关系对象
|
||||
* @param change 好感度变化值
|
||||
* @param reason 变化原因
|
||||
* @param details 详细信息
|
||||
* @returns 更新后的外交关系对象
|
||||
*/
|
||||
export const updateReputation = (
|
||||
relation: DiplomaticRelation,
|
||||
change: number,
|
||||
reason: DiplomaticEventType,
|
||||
details?: string
|
||||
): DiplomaticRelation => {
|
||||
const oldReputation = relation.reputation
|
||||
|
||||
// 计算新的好感度(限制在范围内)
|
||||
const newReputation = Math.max(DIPLOMATIC_CONFIG.MIN_REPUTATION, Math.min(DIPLOMATIC_CONFIG.MAX_REPUTATION, oldReputation + change))
|
||||
|
||||
// 更新状态
|
||||
const newStatus = calculateRelationStatus(newReputation)
|
||||
|
||||
// 记录历史
|
||||
if (!relation.history) {
|
||||
relation.history = []
|
||||
}
|
||||
relation.history.push({
|
||||
timestamp: Date.now(),
|
||||
change,
|
||||
reason,
|
||||
details
|
||||
})
|
||||
|
||||
// 只保留最近50条历史记录
|
||||
if (relation.history.length > 50) {
|
||||
relation.history = relation.history.slice(-50)
|
||||
}
|
||||
|
||||
return {
|
||||
...relation,
|
||||
reputation: newReputation,
|
||||
status: newStatus,
|
||||
lastUpdated: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算赠送资源的好感度增加值
|
||||
* @param resources 赠送的资源
|
||||
* @returns 好感度增加值
|
||||
*/
|
||||
export const calculateGiftReputationGain = (resources: Resources): number => {
|
||||
const { REPUTATION_CHANGES } = DIPLOMATIC_CONFIG
|
||||
|
||||
// 计算资源总价值(晶体和氘气价值更高)
|
||||
const totalValue = resources.metal + resources.crystal * 1.5 + resources.deuterium * 3
|
||||
|
||||
// 检查是否达到最小价值门槛
|
||||
if (totalValue < REPUTATION_CHANGES.GIFT_MIN_VALUE) {
|
||||
return 0 // 低于门槛不获得好感度
|
||||
}
|
||||
|
||||
// 基础好感度 + 基于价值的额外好感度
|
||||
const baseGain = REPUTATION_CHANGES.GIFT_BASE
|
||||
const valueGain = Math.floor(totalValue / 1000) * REPUTATION_CHANGES.GIFT_PER_1K_RESOURCES
|
||||
|
||||
// 限制在最大值范围内
|
||||
return Math.min(baseGain + valueGain, REPUTATION_CHANGES.GIFT_MAX_SINGLE)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取或创建外交关系
|
||||
* @param relations 关系记录
|
||||
* @param fromId 关系发起方ID
|
||||
* @param toId 关系接收方ID
|
||||
* @returns 外交关系对象
|
||||
*/
|
||||
export const getOrCreateRelation = (relations: Record<string, DiplomaticRelation>, fromId: string, toId: string): DiplomaticRelation => {
|
||||
if (!relations[toId]) {
|
||||
relations[toId] = initializeDiplomaticRelation(fromId, toId)
|
||||
}
|
||||
return relations[toId]
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算NPC拒绝礼物的概率
|
||||
* @param npc NPC
|
||||
* @param player 玩家
|
||||
* @returns 拒绝概率(0-1)
|
||||
*/
|
||||
export const calculateNPCRejectionProbability = (npc: NPC, player: Player): number => {
|
||||
const { GIFT_ACCEPTANCE_CONFIG } = DIPLOMATIC_CONFIG
|
||||
|
||||
// 获取NPC对玩家的好感度
|
||||
const relation = npc.relations?.[player.id]
|
||||
const reputation = relation?.reputation || 0
|
||||
|
||||
// 基础概率 + 好感度修正
|
||||
// 好感度越低,拒绝概率越高
|
||||
let rejectionProb = GIFT_ACCEPTANCE_CONFIG.NPC_REJECTION_BASE_PROBABILITY
|
||||
|
||||
// 好感度修正:每低于0一点,增加1%拒绝概率
|
||||
if (reputation < 0) {
|
||||
rejectionProb += Math.abs(reputation) * GIFT_ACCEPTANCE_CONFIG.NPC_REJECTION_REPUTATION_MODIFIER
|
||||
} else if (reputation > 0) {
|
||||
// 好感度为正时,降低拒绝概率
|
||||
rejectionProb -= reputation * GIFT_ACCEPTANCE_CONFIG.NPC_REJECTION_REPUTATION_MODIFIER
|
||||
}
|
||||
|
||||
// 限制在最小和最大范围内
|
||||
return Math.max(
|
||||
GIFT_ACCEPTANCE_CONFIG.MIN_REJECTION_PROBABILITY,
|
||||
Math.min(GIFT_ACCEPTANCE_CONFIG.MAX_REJECTION_PROBABILITY, rejectionProb)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理赠送资源到达(需要NPC接受判定)
|
||||
* @param mission 舰队任务
|
||||
* @param player 玩家
|
||||
* @param targetNpc 目标NPC
|
||||
* @returns { accepted: boolean, reputationGain?: number }
|
||||
*/
|
||||
export const handleGiftArrival = (mission: FleetMission, player: Player, targetNpc: NPC): { accepted: boolean; reputationGain?: number } => {
|
||||
// 计算NPC拒绝概率
|
||||
const rejectionProb = calculateNPCRejectionProbability(targetNpc, player)
|
||||
const isRejected = Math.random() < rejectionProb
|
||||
|
||||
if (isRejected) {
|
||||
// NPC拒绝礼物
|
||||
handleGiftRejection(player, targetNpc, mission.cargo)
|
||||
return { accepted: false }
|
||||
}
|
||||
|
||||
// NPC接受礼物
|
||||
// 计算好感度增加值
|
||||
const reputationGain = calculateGiftReputationGain(mission.cargo)
|
||||
|
||||
// 更新玩家对NPC的关系
|
||||
if (!player.diplomaticRelations) {
|
||||
player.diplomaticRelations = {}
|
||||
}
|
||||
|
||||
const relation = getOrCreateRelation(player.diplomaticRelations, player.id, targetNpc.id)
|
||||
player.diplomaticRelations[targetNpc.id] = updateReputation(
|
||||
relation,
|
||||
reputationGain,
|
||||
DET.GiftResources,
|
||||
`Gifted ${mission.cargo.metal}M ${mission.cargo.crystal}C ${mission.cargo.deuterium}D`
|
||||
)
|
||||
|
||||
// 也更新NPC对玩家的关系(双向好感度)
|
||||
if (!targetNpc.relations) {
|
||||
targetNpc.relations = {}
|
||||
}
|
||||
|
||||
const npcRelation = getOrCreateRelation(targetNpc.relations, targetNpc.id, player.id)
|
||||
targetNpc.relations[player.id] = updateReputation(npcRelation, reputationGain, DET.GiftResources, `Received gift from player`)
|
||||
|
||||
// 生成外交报告
|
||||
generateDiplomaticReport(
|
||||
player,
|
||||
targetNpc,
|
||||
DET.GiftResources,
|
||||
reputationGain,
|
||||
`You gifted resources to ${targetNpc.name}. Reputation +${reputationGain}`
|
||||
)
|
||||
|
||||
return { accepted: true, reputationGain }
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理礼物被拒绝
|
||||
* @param player 玩家
|
||||
* @param npc NPC
|
||||
* @param rejectedResources 被拒绝的资源
|
||||
*/
|
||||
const handleGiftRejection = (player: Player, npc: NPC, rejectedResources: Resources): void => {
|
||||
const { GIFT_ACCEPTANCE_CONFIG } = DIPLOMATIC_CONFIG
|
||||
|
||||
// 创建拒绝通知
|
||||
if (!player.giftRejectedNotifications) {
|
||||
player.giftRejectedNotifications = []
|
||||
}
|
||||
|
||||
const relation = npc.relations?.[player.id]
|
||||
const currentReputation = relation?.reputation || 0
|
||||
|
||||
const notification: GiftRejectedNotification = {
|
||||
id: `gift-rejected-${Date.now()}-${Math.random()}`,
|
||||
timestamp: Date.now(),
|
||||
npcId: npc.id,
|
||||
npcName: npc.name,
|
||||
rejectedResources,
|
||||
currentReputation,
|
||||
reason: currentReputation < -20 ? 'hostile' : currentReputation < 20 ? 'neutral_distrust' : 'polite_decline',
|
||||
read: false
|
||||
}
|
||||
|
||||
player.giftRejectedNotifications.push(notification)
|
||||
|
||||
// 限制通知数量
|
||||
if (player.giftRejectedNotifications.length > 50) {
|
||||
player.giftRejectedNotifications = player.giftRejectedNotifications.slice(-50)
|
||||
}
|
||||
|
||||
// 拒绝礼物会降低好感度
|
||||
if (!npc.relations) {
|
||||
npc.relations = {}
|
||||
}
|
||||
|
||||
const npcRelation = getOrCreateRelation(npc.relations, npc.id, player.id)
|
||||
npc.relations[player.id] = updateReputation(
|
||||
npcRelation,
|
||||
GIFT_ACCEPTANCE_CONFIG.REJECTION_REPUTATION_PENALTY,
|
||||
DET.GiftResources,
|
||||
`Rejected player's gift`
|
||||
)
|
||||
|
||||
// 生成外交报告
|
||||
generateDiplomaticReport(
|
||||
player,
|
||||
npc,
|
||||
DET.GiftResources,
|
||||
GIFT_ACCEPTANCE_CONFIG.REJECTION_REPUTATION_PENALTY,
|
||||
`${npc.name} rejected your gift. Reputation ${GIFT_ACCEPTANCE_CONFIG.REJECTION_REPUTATION_PENALTY}`
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理攻击事件的好感度变化
|
||||
* @param attacker 攻击者(玩家)
|
||||
* @param defender 防御者(NPC)
|
||||
* @param battleResult 战斗结果
|
||||
* @param allNpcs 所有NPC列表
|
||||
*/
|
||||
export const handleAttackReputation = (attacker: Player, defender: NPC, battleResult: BattleResult, allNpcs: NPC[]): void => {
|
||||
const { REPUTATION_CHANGES } = DIPLOMATIC_CONFIG
|
||||
|
||||
// 计算好感度降低值
|
||||
let reputationLoss = REPUTATION_CHANGES.ATTACK
|
||||
|
||||
if (battleResult.winner === 'attacker') {
|
||||
reputationLoss = REPUTATION_CHANGES.ATTACK_WIN
|
||||
}
|
||||
|
||||
// 更新玩家对被攻击NPC的关系
|
||||
if (!attacker.diplomaticRelations) {
|
||||
attacker.diplomaticRelations = {}
|
||||
}
|
||||
|
||||
const relation = getOrCreateRelation(attacker.diplomaticRelations, attacker.id, defender.id)
|
||||
attacker.diplomaticRelations[defender.id] = updateReputation(relation, -reputationLoss, DET.Attack, `Attacked ${defender.name}`)
|
||||
|
||||
// 更新被攻击NPC对玩家的关系
|
||||
if (!defender.relations) {
|
||||
defender.relations = {}
|
||||
}
|
||||
|
||||
const defenderRelation = getOrCreateRelation(defender.relations, defender.id, attacker.id)
|
||||
defender.relations[attacker.id] = updateReputation(defenderRelation, -reputationLoss, DET.Attack, `Was attacked by player`)
|
||||
|
||||
// 检查盟友关系网络
|
||||
if (defender.allies && defender.allies.length > 0) {
|
||||
handleAllyAttackedReputation(attacker, defender, allNpcs)
|
||||
}
|
||||
|
||||
// 生成外交报告
|
||||
generateDiplomaticReport(attacker, defender, DET.Attack, -reputationLoss, `You attacked ${defender.name}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理盟友被攻击的好感度变化
|
||||
* @param attacker 攻击者(玩家)
|
||||
* @param attackedNpc 被攻击的NPC
|
||||
* @param allNpcs 所有NPC列表
|
||||
*/
|
||||
export const handleAllyAttackedReputation = (attacker: Player, attackedNpc: NPC, allNpcs: NPC[]): void => {
|
||||
const { REPUTATION_CHANGES } = DIPLOMATIC_CONFIG
|
||||
|
||||
// 找到所有盟友
|
||||
const allies = allNpcs.filter(npc => attackedNpc.allies?.includes(npc.id))
|
||||
|
||||
allies.forEach(ally => {
|
||||
// 更新盟友对玩家的关系
|
||||
if (!ally.relations) {
|
||||
ally.relations = {}
|
||||
}
|
||||
|
||||
const allyRelation = getOrCreateRelation(ally.relations, ally.id, attacker.id)
|
||||
ally.relations[attacker.id] = updateReputation(
|
||||
allyRelation,
|
||||
-REPUTATION_CHANGES.ALLY_ATTACKED,
|
||||
DET.AllyAttacked,
|
||||
`Player attacked ally ${attackedNpc.name}`
|
||||
)
|
||||
|
||||
// 生成外交报告
|
||||
generateDiplomaticReport(
|
||||
attacker,
|
||||
ally,
|
||||
DET.AllyAttacked,
|
||||
-REPUTATION_CHANGES.ALLY_ATTACKED,
|
||||
`${ally.name} is displeased that you attacked their ally ${attackedNpc.name}`
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理侦查事件的好感度变化
|
||||
* @param spy 侦查者(玩家)
|
||||
* @param target 侦查目标(NPC)
|
||||
* @param wasDetected 是否被发现
|
||||
*/
|
||||
export const handleSpyReputation = (spy: Player, target: NPC, wasDetected: boolean): void => {
|
||||
const { REPUTATION_CHANGES } = DIPLOMATIC_CONFIG
|
||||
|
||||
const reputationLoss = wasDetected ? REPUTATION_CHANGES.SPY_DETECTED : REPUTATION_CHANGES.SPY_UNDETECTED
|
||||
|
||||
// NPC对玩家的关系始终受影响
|
||||
if (!target.relations) {
|
||||
target.relations = {}
|
||||
}
|
||||
|
||||
const targetRelation = getOrCreateRelation(target.relations, target.id, spy.id)
|
||||
target.relations[spy.id] = updateReputation(targetRelation, -reputationLoss, DET.Spy, `Was spied by player (detected: ${wasDetected})`)
|
||||
|
||||
// 如果被发现,生成外交报告
|
||||
if (wasDetected) {
|
||||
generateDiplomaticReport(spy, target, DET.Spy, -reputationLoss, `Your espionage was detected by ${target.name}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理回收残骸事件的好感度变化
|
||||
* 如果残骸在NPC星球位置,视为抢夺
|
||||
* @param player 玩家
|
||||
* @param debrisPosition 残骸位置
|
||||
* @param allNpcs 所有NPC列表
|
||||
*/
|
||||
export const handleDebrisRecycleReputation = (player: Player, debrisPosition: Position, allNpcs: NPC[]): void => {
|
||||
const { REPUTATION_CHANGES } = DIPLOMATIC_CONFIG
|
||||
|
||||
// 找到该位置的NPC星球所有者
|
||||
const npcOwner = allNpcs.find(npc =>
|
||||
npc.planets.some(
|
||||
p =>
|
||||
p.position.galaxy === debrisPosition.galaxy &&
|
||||
p.position.system === debrisPosition.system &&
|
||||
p.position.position === debrisPosition.position
|
||||
)
|
||||
)
|
||||
|
||||
if (npcOwner) {
|
||||
// 这是在NPC星球位置回收残骸,视为抢夺
|
||||
if (!player.diplomaticRelations) {
|
||||
player.diplomaticRelations = {}
|
||||
}
|
||||
|
||||
const relation = getOrCreateRelation(player.diplomaticRelations, player.id, npcOwner.id)
|
||||
player.diplomaticRelations[npcOwner.id] = updateReputation(
|
||||
relation,
|
||||
-REPUTATION_CHANGES.STEAL_DEBRIS,
|
||||
DET.StealDebris,
|
||||
`Stole debris from ${npcOwner.name}'s territory`
|
||||
)
|
||||
|
||||
// 更新NPC对玩家的关系
|
||||
if (!npcOwner.relations) {
|
||||
npcOwner.relations = {}
|
||||
}
|
||||
|
||||
const npcRelation = getOrCreateRelation(npcOwner.relations, npcOwner.id, player.id)
|
||||
npcOwner.relations[player.id] = updateReputation(
|
||||
npcRelation,
|
||||
-REPUTATION_CHANGES.STEAL_DEBRIS,
|
||||
DET.StealDebris,
|
||||
`Player stole debris from territory`
|
||||
)
|
||||
|
||||
// 生成外交报告
|
||||
generateDiplomaticReport(
|
||||
player,
|
||||
npcOwner,
|
||||
DET.StealDebris,
|
||||
-REPUTATION_CHANGES.STEAL_DEBRIS,
|
||||
`You recycled debris near ${npcOwner.name}'s planet. They are displeased.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成外交报告
|
||||
* @param player 玩家
|
||||
* @param npc NPC
|
||||
* @param eventType 事件类型
|
||||
* @param reputationChange 好感度变化值
|
||||
* @param message 消息内容
|
||||
*/
|
||||
const generateDiplomaticReport = (
|
||||
player: Player,
|
||||
npc: NPC,
|
||||
eventType: DiplomaticEventType,
|
||||
reputationChange: number,
|
||||
message: string
|
||||
): void => {
|
||||
if (!player.diplomaticReports) {
|
||||
player.diplomaticReports = []
|
||||
}
|
||||
|
||||
if (!player.diplomaticRelations) {
|
||||
player.diplomaticRelations = {}
|
||||
}
|
||||
|
||||
const relation = player.diplomaticRelations[npc.id] || initializeDiplomaticRelation(player.id, npc.id)
|
||||
const oldStatus = relation.status
|
||||
const newReputation = Math.max(
|
||||
DIPLOMATIC_CONFIG.MIN_REPUTATION,
|
||||
Math.min(DIPLOMATIC_CONFIG.MAX_REPUTATION, relation.reputation + reputationChange)
|
||||
)
|
||||
const newStatus = calculateRelationStatus(newReputation)
|
||||
|
||||
const report: DiplomaticReport = {
|
||||
id: `diplomatic-${Date.now()}-${Math.random()}`,
|
||||
timestamp: Date.now(),
|
||||
npcId: npc.id,
|
||||
npcName: npc.name,
|
||||
eventType,
|
||||
reputationChange,
|
||||
newReputation,
|
||||
oldStatus,
|
||||
newStatus,
|
||||
message,
|
||||
read: false
|
||||
}
|
||||
|
||||
player.diplomaticReports.push(report)
|
||||
|
||||
// 只保留最近100条报告
|
||||
if (player.diplomaticReports.length > 100) {
|
||||
player.diplomaticReports = player.diplomaticReports.slice(-100)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理NPC赠送资源给玩家(创建礼物通知,等待玩家接受/拒绝)
|
||||
* @param npc 赠送方NPC
|
||||
* @param player 接收方玩家
|
||||
* @param giftResources 赠送的资源
|
||||
*/
|
||||
export const handleNPCGiftToPlayer = (npc: NPC, player: Player, giftResources: Resources): void => {
|
||||
const { GIFT_ACCEPTANCE_CONFIG } = DIPLOMATIC_CONFIG
|
||||
|
||||
// 创建礼物通知
|
||||
if (!player.giftNotifications) {
|
||||
player.giftNotifications = []
|
||||
}
|
||||
|
||||
const npcRelation = npc.relations?.[player.id]
|
||||
const currentReputation = npcRelation?.reputation || 0
|
||||
const expectedReputationGain = Math.floor(calculateGiftReputationGain(giftResources) * 0.5)
|
||||
|
||||
const notification: GiftNotification = {
|
||||
id: `gift-${Date.now()}-${Math.random()}`,
|
||||
timestamp: Date.now(),
|
||||
fromNpcId: npc.id,
|
||||
fromNpcName: npc.name,
|
||||
resources: giftResources,
|
||||
currentReputation,
|
||||
expectedReputationGain,
|
||||
expiresAt: Date.now() + GIFT_ACCEPTANCE_CONFIG.GIFT_EXPIRATION_DAYS * 24 * 3600 * 1000,
|
||||
read: false
|
||||
}
|
||||
|
||||
player.giftNotifications.push(notification)
|
||||
|
||||
// 限制通知数量
|
||||
if (player.giftNotifications.length > 50) {
|
||||
player.giftNotifications = player.giftNotifications.slice(-50)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 玩家接受NPC的礼物
|
||||
* @param player 玩家
|
||||
* @param npc NPC
|
||||
* @param giftNotification 礼物通知
|
||||
*/
|
||||
export const acceptNPCGift = (player: Player, npc: NPC, giftNotification: GiftNotification): void => {
|
||||
// 将资源添加到玩家主星球
|
||||
if (player.planets && player.planets.length > 0) {
|
||||
const mainPlanet = player.planets[0]
|
||||
if (mainPlanet) {
|
||||
mainPlanet.resources.metal += giftNotification.resources.metal
|
||||
mainPlanet.resources.crystal += giftNotification.resources.crystal
|
||||
mainPlanet.resources.deuterium += giftNotification.resources.deuterium
|
||||
mainPlanet.resources.darkMatter += giftNotification.resources.darkMatter
|
||||
}
|
||||
}
|
||||
|
||||
// 更新NPC对玩家的关系
|
||||
if (!npc.relations) {
|
||||
npc.relations = {}
|
||||
}
|
||||
|
||||
const npcRelation = getOrCreateRelation(npc.relations, npc.id, player.id)
|
||||
npc.relations[player.id] = updateReputation(npcRelation, giftNotification.expectedReputationGain, DET.GiftResources, `Gifted resources to player`)
|
||||
|
||||
// 也更新玩家对NPC的关系(收到礼物会增加好感)
|
||||
if (!player.diplomaticRelations) {
|
||||
player.diplomaticRelations = {}
|
||||
}
|
||||
|
||||
const playerRelation = getOrCreateRelation(player.diplomaticRelations, player.id, npc.id)
|
||||
player.diplomaticRelations[npc.id] = updateReputation(
|
||||
playerRelation,
|
||||
giftNotification.expectedReputationGain,
|
||||
DET.GiftResources,
|
||||
`Received gift from ${npc.name}`
|
||||
)
|
||||
|
||||
// 生成外交报告
|
||||
generateDiplomaticReport(
|
||||
player,
|
||||
npc,
|
||||
DET.GiftResources,
|
||||
giftNotification.expectedReputationGain,
|
||||
`You accepted a gift from ${npc.name}: ${giftNotification.resources.metal}M ${giftNotification.resources.crystal}C ${giftNotification.resources.deuterium}D`
|
||||
)
|
||||
|
||||
// 移除礼物通知
|
||||
if (player.giftNotifications) {
|
||||
player.giftNotifications = player.giftNotifications.filter(n => n.id !== giftNotification.id)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 玩家拒绝NPC的礼物
|
||||
* @param player 玩家
|
||||
* @param npc NPC
|
||||
* @param giftNotification 礼物通知
|
||||
*/
|
||||
export const rejectNPCGift = (player: Player, npc: NPC, giftNotification: GiftNotification): void => {
|
||||
const { GIFT_ACCEPTANCE_CONFIG } = DIPLOMATIC_CONFIG
|
||||
|
||||
// 拒绝礼物会降低好感度
|
||||
if (!npc.relations) {
|
||||
npc.relations = {}
|
||||
}
|
||||
|
||||
const npcRelation = getOrCreateRelation(npc.relations, npc.id, player.id)
|
||||
npc.relations[player.id] = updateReputation(
|
||||
npcRelation,
|
||||
GIFT_ACCEPTANCE_CONFIG.REJECTION_REPUTATION_PENALTY,
|
||||
DET.GiftResources,
|
||||
`Player rejected gift`
|
||||
)
|
||||
|
||||
// 生成外交报告
|
||||
generateDiplomaticReport(
|
||||
player,
|
||||
npc,
|
||||
DET.GiftResources,
|
||||
GIFT_ACCEPTANCE_CONFIG.REJECTION_REPUTATION_PENALTY,
|
||||
`You rejected a gift from ${npc.name}. Reputation ${GIFT_ACCEPTANCE_CONFIG.REJECTION_REPUTATION_PENALTY}`
|
||||
)
|
||||
|
||||
// 移除礼物通知
|
||||
if (player.giftNotifications) {
|
||||
player.giftNotifications = player.giftNotifications.filter(n => n.id !== giftNotification.id)
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { FleetMission, Planet, Resources, Fleet, BattleResult, SpyReport, Player, Officer, DebrisField } from '@/types/game'
|
||||
import { ShipType, DefenseType, MissionType, BuildingType, OfficerType } from '@/types/game'
|
||||
import type { FleetMission, Planet, Resources, Fleet, BattleResult, SpyReport, Player, Officer, DebrisField, NPC } from '@/types/game'
|
||||
import { ShipType, DefenseType, MissionType, BuildingType, OfficerType, TechnologyType } from '@/types/game'
|
||||
import { FLEET_STORAGE_CONFIG } from '@/config/gameConfig'
|
||||
import * as battleLogic from './battleLogic'
|
||||
import * as moonLogic from './moonLogic'
|
||||
import * as moonValidation from './moonValidation'
|
||||
import * as diplomaticLogic from './diplomaticLogic'
|
||||
|
||||
/**
|
||||
* 计算两个星球之间的距离
|
||||
@@ -70,15 +71,44 @@ export const createFleetMission = (
|
||||
/**
|
||||
* 处理运输任务到达
|
||||
*/
|
||||
export const processTransportArrival = (mission: FleetMission, targetPlanet: Planet | undefined): void => {
|
||||
export const processTransportArrival = (
|
||||
mission: FleetMission,
|
||||
targetPlanet: Planet | undefined,
|
||||
player?: Player,
|
||||
allNpcs?: NPC[]
|
||||
): { success: boolean; reputationGain?: number } => {
|
||||
// 检查是否是赠送任务
|
||||
if (mission.isGift && mission.giftTargetNpcId && player && allNpcs) {
|
||||
const targetNpc = allNpcs.find(n => n.id === mission.giftTargetNpcId)
|
||||
if (targetNpc) {
|
||||
const giftResult = diplomaticLogic.handleGiftArrival(mission, player, targetNpc)
|
||||
mission.status = 'returning'
|
||||
|
||||
// 如果礼物被拒绝,资源返还给玩家
|
||||
if (!giftResult.accepted) {
|
||||
// 资源保留在cargo中,返回时会退还给玩家
|
||||
return { success: false, reputationGain: undefined }
|
||||
}
|
||||
|
||||
// 礼物被接受,清空货物
|
||||
mission.cargo = { metal: 0, crystal: 0, deuterium: 0, darkMatter: 0, energy: 0 }
|
||||
return { success: true, reputationGain: giftResult.reputationGain }
|
||||
}
|
||||
}
|
||||
|
||||
// 正常运输任务
|
||||
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 }
|
||||
return { success: true }
|
||||
}
|
||||
mission.status = 'returning'
|
||||
mission.cargo = { metal: 0, crystal: 0, deuterium: 0, darkMatter: 0, energy: 0 }
|
||||
return { success: false }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -103,7 +133,9 @@ export const processAttackArrival = async (
|
||||
targetPlanet.defense,
|
||||
targetPlanet.resources,
|
||||
attacker.officers,
|
||||
defender?.officers || ({} as Record<OfficerType, Officer>)
|
||||
defender?.officers || ({} as Record<OfficerType, Officer>),
|
||||
attacker.technologies,
|
||||
defender?.technologies || ({} as Record<TechnologyType, number>)
|
||||
)
|
||||
|
||||
// 更新战斗报告ID
|
||||
@@ -177,13 +209,134 @@ export const processAttackArrival = async (
|
||||
return { battleResult, moon, debrisField }
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理NPC攻击玩家星球
|
||||
* 专门用于NPC作为攻击方的情况
|
||||
*/
|
||||
export const processNPCAttackArrival = async (
|
||||
npc: NPC,
|
||||
mission: FleetMission,
|
||||
targetPlanet: Planet,
|
||||
defender: Player,
|
||||
allPlanets: Planet[]
|
||||
): Promise<{ battleResult: BattleResult; moon: Planet | null; debrisField: DebrisField | null } | null> => {
|
||||
// 执行战斗(使用 Worker 进行异步计算)
|
||||
const battleResult = await battleLogic.simulateBattle(
|
||||
mission.fleet, // NPC舰队
|
||||
targetPlanet.fleet, // 玩家舰队
|
||||
targetPlanet.defense, // 玩家防御
|
||||
targetPlanet.resources, // 玩家资源
|
||||
{} as Record<OfficerType, Officer>, // NPC没有军官系统
|
||||
defender.officers || ({} as Record<OfficerType, Officer>), // 玩家军官
|
||||
npc.technologies, // NPC科技等级
|
||||
defender.technologies // 玩家科技等级
|
||||
)
|
||||
|
||||
// 更新战斗报告ID和参与者信息
|
||||
battleResult.id = `battle_${Date.now()}`
|
||||
battleResult.attackerId = npc.id
|
||||
battleResult.defenderId = defender.id
|
||||
battleResult.attackerPlanetId = mission.originPlanetId
|
||||
battleResult.defenderPlanetId = targetPlanet.id
|
||||
battleResult.timestamp = Date.now()
|
||||
|
||||
// 如果NPC获胜,掠夺的资源给NPC的起始星球
|
||||
if (battleResult.winner === 'attacker' && battleResult.plunder) {
|
||||
const npcOriginPlanet = npc.planets.find(p => p.id === mission.originPlanetId)
|
||||
if (npcOriginPlanet) {
|
||||
// NPC获得掠夺的资源(当舰队返回时)
|
||||
mission.cargo = battleResult.plunder
|
||||
}
|
||||
}
|
||||
|
||||
// 更新NPC舰队 - 计算幸存舰船
|
||||
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] || 0) - lost)
|
||||
})
|
||||
|
||||
Object.entries(battleResult.defenderLosses.defense).forEach(([defenseType, lost]) => {
|
||||
targetPlanet.defense[defenseType as DefenseType] = Math.max(0, (targetPlanet.defense[defenseType as DefenseType] || 0) - 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>
|
||||
|
||||
// 扣除玩家星球被掠夺的资源
|
||||
if (battleResult.plunder) {
|
||||
targetPlanet.resources.metal = Math.max(0, targetPlanet.resources.metal - battleResult.plunder.metal)
|
||||
targetPlanet.resources.crystal = Math.max(0, targetPlanet.resources.crystal - battleResult.plunder.crystal)
|
||||
targetPlanet.resources.deuterium = Math.max(0, 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')
|
||||
}
|
||||
}
|
||||
|
||||
// 创建残骸场(如果有残骸)
|
||||
let debrisField: DebrisField | null = null
|
||||
const totalDebris = battleResult.debrisField.metal + battleResult.debrisField.crystal
|
||||
if (totalDebris > 0) {
|
||||
debrisField = {
|
||||
id: `debris_${targetPlanet.position.galaxy}_${targetPlanet.position.system}_${targetPlanet.position.position}`,
|
||||
position: targetPlanet.position,
|
||||
resources: {
|
||||
metal: battleResult.debrisField.metal,
|
||||
crystal: battleResult.debrisField.crystal
|
||||
},
|
||||
createdAt: Date.now()
|
||||
}
|
||||
}
|
||||
return { battleResult, moon, debrisField }
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算玩家最大星球数量
|
||||
* 基于天体物理学技术等级
|
||||
*/
|
||||
export const calculateMaxPlanets = (astrophysicsLevel: number): number => {
|
||||
// 基础1个星球 + 每2级天体物理学增加1个殖民地槽位
|
||||
return 1 + Math.floor(astrophysicsLevel / 2)
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查玩家是否可以殖民新星球
|
||||
*/
|
||||
export const canColonize = (player: Player): boolean => {
|
||||
const astrophysicsLevel = player.technologies[TechnologyType.Astrophysics] || 0
|
||||
const maxPlanets = calculateMaxPlanets(astrophysicsLevel)
|
||||
const currentPlanets = player.planets.length
|
||||
return currentPlanets < maxPlanets
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理殖民任务到达
|
||||
*/
|
||||
export const processColonizeArrival = (
|
||||
mission: FleetMission,
|
||||
targetPlanet: Planet | undefined,
|
||||
playerId: string,
|
||||
player: Player,
|
||||
colonyNameTemplate: string = 'Colony'
|
||||
): Planet | null => {
|
||||
if (targetPlanet) {
|
||||
@@ -192,11 +345,18 @@ export const processColonizeArrival = (
|
||||
return null
|
||||
}
|
||||
|
||||
// 检查殖民地槽位限制
|
||||
if (!canColonize(player)) {
|
||||
// 超出殖民地数量限制,殖民船返回
|
||||
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,
|
||||
ownerId: player.id,
|
||||
position: mission.targetPosition,
|
||||
resources: { metal: 500, crystal: 500, deuterium: 0, darkMatter: 0, energy: 0 },
|
||||
buildings: {} as Record<BuildingType, number>,
|
||||
@@ -205,11 +365,15 @@ export const processColonizeArrival = (
|
||||
[ShipType.HeavyFighter]: 0,
|
||||
[ShipType.Cruiser]: 0,
|
||||
[ShipType.Battleship]: 0,
|
||||
[ShipType.Battlecruiser]: 0,
|
||||
[ShipType.Bomber]: 0,
|
||||
[ShipType.Destroyer]: 0,
|
||||
[ShipType.SmallCargo]: 0,
|
||||
[ShipType.LargeCargo]: 0,
|
||||
[ShipType.ColonyShip]: 0,
|
||||
[ShipType.Recycler]: 0,
|
||||
[ShipType.EspionageProbe]: 0,
|
||||
[ShipType.SolarSatellite]: 0,
|
||||
[ShipType.DarkMatterHarvester]: 0,
|
||||
[ShipType.Deathstar]: 0
|
||||
},
|
||||
@@ -222,6 +386,8 @@ export const processColonizeArrival = (
|
||||
[DefenseType.PlasmaTurret]: 0,
|
||||
[DefenseType.SmallShieldDome]: 0,
|
||||
[DefenseType.LargeShieldDome]: 0,
|
||||
[DefenseType.AntiBallisticMissile]: 0,
|
||||
[DefenseType.InterplanetaryMissile]: 0,
|
||||
[DefenseType.PlanetaryShield]: 0
|
||||
},
|
||||
buildQueue: [],
|
||||
@@ -242,27 +408,104 @@ export const processColonizeArrival = (
|
||||
return newPlanet
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算间谍侦查信息可见度
|
||||
* 根据双方间谍技术等级差异决定显示哪些信息
|
||||
*/
|
||||
export const calculateSpyVisibility = (
|
||||
attackerSpyLevel: number,
|
||||
defenderSpyLevel: number,
|
||||
spyProbeCount: number
|
||||
): {
|
||||
showFleet: boolean
|
||||
showDefense: boolean
|
||||
showBuildings: boolean
|
||||
showTechnologies: boolean
|
||||
} => {
|
||||
// 技术等级差异 (考虑间谍探测器数量加成)
|
||||
const levelDiff = attackerSpyLevel - defenderSpyLevel + Math.floor(spyProbeCount / 5)
|
||||
|
||||
return {
|
||||
showFleet: levelDiff >= -1, // Level 2: Resources + fleet
|
||||
showDefense: levelDiff >= 1, // Level 4: Resources + fleet + defense
|
||||
showBuildings: levelDiff >= 3, // Level 6: Resources + fleet + defense + buildings
|
||||
showTechnologies: levelDiff >= 5 // Level 8: Resources + fleet + defense + buildings + technologies
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算间谍侦查被发现概率
|
||||
*/
|
||||
export const calculateSpyDetectionChance = (attackerSpyLevel: number, defenderSpyLevel: number, spyProbeCount: number): number => {
|
||||
// 基础被发现概率
|
||||
let baseChance = 0.25
|
||||
|
||||
// 技术等级差异影响
|
||||
const levelDiff = defenderSpyLevel - attackerSpyLevel
|
||||
if (levelDiff > 0) {
|
||||
baseChance += levelDiff * 0.1 // 防守方技术高,增加被发现概率
|
||||
} else {
|
||||
baseChance += levelDiff * 0.05 // 攻击方技术高,减少被发现概率
|
||||
}
|
||||
|
||||
// 间谍探测器数量影响
|
||||
baseChance -= spyProbeCount * 0.01 // 每个探测器降低1%被发现概率
|
||||
|
||||
// 限制在 1% - 99% 之间
|
||||
return Math.max(0.01, Math.min(0.99, baseChance))
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理间谍任务到达
|
||||
*/
|
||||
export const processSpyArrival = (mission: FleetMission, targetPlanet: Planet | undefined, playerId: string): SpyReport | null => {
|
||||
export const processSpyArrival = (
|
||||
mission: FleetMission,
|
||||
targetPlanet: Planet | undefined,
|
||||
attacker: Player,
|
||||
defender: Player | null,
|
||||
allNpcs?: NPC[]
|
||||
): SpyReport | null => {
|
||||
if (!targetPlanet) {
|
||||
mission.status = 'returning'
|
||||
return null
|
||||
}
|
||||
|
||||
// 获取间谍技术等级
|
||||
const attackerSpyLevel = attacker.technologies[TechnologyType.EspionageTechnology] || 0
|
||||
const defenderSpyLevel = defender?.technologies[TechnologyType.EspionageTechnology] || 0
|
||||
const spyProbeCount = mission.fleet[ShipType.EspionageProbe] || 0
|
||||
|
||||
// 计算信息可见度
|
||||
const visibility = calculateSpyVisibility(attackerSpyLevel, defenderSpyLevel, spyProbeCount)
|
||||
|
||||
// 计算被发现概率
|
||||
const detectionChance = calculateSpyDetectionChance(attackerSpyLevel, defenderSpyLevel, spyProbeCount)
|
||||
|
||||
// 判断是否被发现
|
||||
const wasDetected = Math.random() < detectionChance
|
||||
|
||||
const spyReport: SpyReport = {
|
||||
id: `spy_${Date.now()}`,
|
||||
timestamp: Date.now(),
|
||||
spyId: playerId,
|
||||
spyId: attacker.id,
|
||||
targetPlanetId: targetPlanet.id,
|
||||
targetPlanetName: targetPlanet.name,
|
||||
targetPosition: { ...targetPlanet.position },
|
||||
targetPlayerId: targetPlanet.ownerId || 'unknown',
|
||||
resources: { ...targetPlanet.resources },
|
||||
fleet: { ...targetPlanet.fleet },
|
||||
defense: { ...targetPlanet.defense },
|
||||
buildings: { ...targetPlanet.buildings },
|
||||
technologies: {},
|
||||
detectionChance: 0.3
|
||||
resources: { ...targetPlanet.resources }, // 资源信息始终可见
|
||||
fleet: visibility.showFleet ? { ...targetPlanet.fleet } : undefined,
|
||||
defense: visibility.showDefense ? { ...targetPlanet.defense } : undefined,
|
||||
buildings: visibility.showBuildings ? { ...targetPlanet.buildings } : undefined,
|
||||
technologies: visibility.showTechnologies && defender ? { ...defender.technologies } : undefined,
|
||||
detectionChance
|
||||
}
|
||||
|
||||
// 如果目标是NPC星球,调用外交逻辑
|
||||
if (allNpcs && targetPlanet.ownerId) {
|
||||
const targetNpc = allNpcs.find(npc => npc.planets.some(p => p.id === targetPlanet.id))
|
||||
if (targetNpc) {
|
||||
diplomaticLogic.handleSpyReputation(attacker, targetNpc, wasDetected)
|
||||
}
|
||||
}
|
||||
|
||||
mission.status = 'returning'
|
||||
@@ -291,7 +534,9 @@ export const processDeployArrival = (mission: FleetMission, targetPlanet: Planet
|
||||
*/
|
||||
export const processRecycleArrival = (
|
||||
mission: FleetMission,
|
||||
debrisField: DebrisField | undefined
|
||||
debrisField: DebrisField | undefined,
|
||||
player?: Player,
|
||||
allNpcs?: NPC[]
|
||||
): { collectedResources: Pick<Resources, 'metal' | 'crystal'>; remainingDebris: Pick<Resources, 'metal' | 'crystal'> | null } | null => {
|
||||
if (!debrisField) {
|
||||
mission.status = 'returning'
|
||||
@@ -330,6 +575,11 @@ export const processRecycleArrival = (
|
||||
|
||||
mission.status = 'returning'
|
||||
|
||||
// 检查是否在NPC星球位置回收残骸,如果是则降低好感度
|
||||
if (player && allNpcs && collectedAmount > 0) {
|
||||
diplomaticLogic.handleDebrisRecycleReputation(player, debrisField.position, allNpcs)
|
||||
}
|
||||
|
||||
return {
|
||||
collectedResources: {
|
||||
metal: collectedMetal,
|
||||
@@ -348,11 +598,7 @@ export const processRecycleArrival = (
|
||||
/**
|
||||
* 计算行星毁灭概率
|
||||
*/
|
||||
export const calculateDestructionChance = (
|
||||
deathstarCount: number,
|
||||
planetaryShieldCount: number,
|
||||
planetDefensePower: number
|
||||
): number => {
|
||||
export const calculateDestructionChance = (deathstarCount: number, planetaryShieldCount: number, planetDefensePower: number): number => {
|
||||
// 基础摧毁概率:每艘死星 10%
|
||||
let baseChance = deathstarCount * 10
|
||||
|
||||
@@ -372,10 +618,7 @@ export const calculateDestructionChance = (
|
||||
/**
|
||||
* 计算星球总防御力量
|
||||
*/
|
||||
export const calculatePlanetDefensePower = (
|
||||
fleet: Partial<Fleet>,
|
||||
defense: Partial<Record<DefenseType, number>>
|
||||
): number => {
|
||||
export const calculatePlanetDefensePower = (fleet: Partial<Fleet>, defense: Partial<Record<DefenseType, number>>): number => {
|
||||
let totalPower = 0
|
||||
|
||||
// 计算舰队力量
|
||||
@@ -463,7 +706,8 @@ export const updateFleetMissions = async (
|
||||
debrisFields: Map<string, DebrisField>,
|
||||
attacker: Player,
|
||||
defender: Player | null,
|
||||
now: number
|
||||
now: number,
|
||||
allNpcs?: NPC[]
|
||||
): Promise<{
|
||||
completedMissions: string[]
|
||||
battleReports: BattleResult[]
|
||||
@@ -499,7 +743,7 @@ export const updateFleetMissions = async (
|
||||
|
||||
switch (mission.missionType) {
|
||||
case MissionType.Transport:
|
||||
processTransportArrival(mission, targetPlanet)
|
||||
processTransportArrival(mission, targetPlanet, attacker, allNpcs)
|
||||
break
|
||||
|
||||
case MissionType.Attack: {
|
||||
@@ -520,7 +764,7 @@ export const updateFleetMissions = async (
|
||||
}
|
||||
|
||||
case MissionType.Colonize:
|
||||
const newColony = processColonizeArrival(mission, targetPlanet, attacker.id)
|
||||
const newColony = processColonizeArrival(mission, targetPlanet, attacker)
|
||||
if (newColony) {
|
||||
newColonies.push(newColony)
|
||||
planets.set(targetKey, newColony)
|
||||
@@ -528,7 +772,7 @@ export const updateFleetMissions = async (
|
||||
break
|
||||
|
||||
case MissionType.Spy:
|
||||
const spyReport = processSpyArrival(mission, targetPlanet, attacker.id)
|
||||
const spyReport = processSpyArrival(mission, targetPlanet, attacker, defender)
|
||||
if (spyReport) {
|
||||
spyReports.push(spyReport)
|
||||
}
|
||||
@@ -544,7 +788,7 @@ export const updateFleetMissions = async (
|
||||
case MissionType.Recycle:
|
||||
const debrisId = `debris_${mission.targetPosition.galaxy}_${mission.targetPosition.system}_${mission.targetPosition.position}`
|
||||
const debrisField = debrisFields.get(debrisId)
|
||||
const recycleResult = processRecycleArrival(mission, debrisField)
|
||||
const recycleResult = processRecycleArrival(mission, debrisField, attacker, allNpcs)
|
||||
if (recycleResult) {
|
||||
if (recycleResult.remainingDebris) {
|
||||
// 更新残骸场
|
||||
@@ -582,7 +826,17 @@ export const updateFleetMissions = async (
|
||||
}
|
||||
}
|
||||
|
||||
return { completedMissions, battleReports, spyReports, newColonies, newMoons, newDebrisFields, updatedDebrisFields, removedDebrisFieldIds, destroyedPlanetIds }
|
||||
return {
|
||||
completedMissions,
|
||||
battleReports,
|
||||
spyReports,
|
||||
newColonies,
|
||||
newMoons,
|
||||
newDebrisFields,
|
||||
updatedDebrisFields,
|
||||
removedDebrisFieldIds,
|
||||
destroyedPlanetIds
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -30,10 +30,7 @@ export const calculateFleetStorageUsage = (fleet: Fleet): number => {
|
||||
* @param technologies 玩家的科技等级
|
||||
* @returns 最大舰队仓储容量
|
||||
*/
|
||||
export const calculateMaxFleetStorage = (
|
||||
planet: Planet,
|
||||
technologies: Record<TechnologyType, number>
|
||||
): number => {
|
||||
export const calculateMaxFleetStorage = (planet: Planet, technologies: Record<TechnologyType, number>): number => {
|
||||
// 1. 基础仓储
|
||||
let maxStorage = FLEET_STORAGE_CONFIG.baseStorage
|
||||
|
||||
@@ -78,11 +75,7 @@ export const hasEnoughFleetStorage = (
|
||||
* @param technologies 玩家的科技等级
|
||||
* @returns 最大可建造数量
|
||||
*/
|
||||
export const getMaxBuildableShips = (
|
||||
planet: Planet,
|
||||
shipType: ShipType,
|
||||
technologies: Record<TechnologyType, number>
|
||||
): number => {
|
||||
export const getMaxBuildableShips = (planet: Planet, shipType: ShipType, technologies: Record<TechnologyType, number>): number => {
|
||||
const currentUsage = calculateFleetStorageUsage(planet.fleet)
|
||||
const maxStorage = calculateMaxFleetStorage(planet, technologies)
|
||||
const availableStorage = maxStorage - currentUsage
|
||||
|
||||
@@ -21,6 +21,14 @@ export const initializePlayer = (playerId: string, playerName: string = 'Command
|
||||
fleetMissions: [],
|
||||
battleReports: [],
|
||||
spyReports: [],
|
||||
spiedNotifications: [],
|
||||
npcActivityNotifications: [],
|
||||
missionReports: [],
|
||||
incomingFleetAlerts: [],
|
||||
giftNotifications: [],
|
||||
giftRejectedNotifications: [],
|
||||
diplomaticRelations: {},
|
||||
diplomaticReports: [],
|
||||
points: 0
|
||||
}
|
||||
|
||||
|
||||
@@ -62,11 +62,15 @@ export const tryGenerateMoon = (
|
||||
[ShipType.HeavyFighter]: 0,
|
||||
[ShipType.Cruiser]: 0,
|
||||
[ShipType.Battleship]: 0,
|
||||
[ShipType.Battlecruiser]: 0,
|
||||
[ShipType.Bomber]: 0,
|
||||
[ShipType.Destroyer]: 0,
|
||||
[ShipType.SmallCargo]: 0,
|
||||
[ShipType.LargeCargo]: 0,
|
||||
[ShipType.ColonyShip]: 0,
|
||||
[ShipType.Recycler]: 0,
|
||||
[ShipType.EspionageProbe]: 0,
|
||||
[ShipType.SolarSatellite]: 0,
|
||||
[ShipType.DarkMatterHarvester]: 0,
|
||||
[ShipType.Deathstar]: 0
|
||||
},
|
||||
@@ -79,6 +83,8 @@ export const tryGenerateMoon = (
|
||||
[DefenseType.PlasmaTurret]: 0,
|
||||
[DefenseType.SmallShieldDome]: 0,
|
||||
[DefenseType.LargeShieldDome]: 0,
|
||||
[DefenseType.AntiBallisticMissile]: 0,
|
||||
[DefenseType.InterplanetaryMissile]: 0,
|
||||
[DefenseType.PlanetaryShield]: 0
|
||||
},
|
||||
buildQueue: [],
|
||||
|
||||
1030
src/logic/npcBehaviorLogic.ts
Normal file
1030
src/logic/npcBehaviorLogic.ts
Normal file
@@ -0,0 +1,1030 @@
|
||||
import type { NPC, Planet, Player, FleetMission, SpyReport, SpiedNotification, IncomingFleetAlert, Fleet, DebrisField } from '@/types/game'
|
||||
import { MissionType, ShipType, TechnologyType, RelationStatus } from '@/types/game'
|
||||
import * as fleetLogic from './fleetLogic'
|
||||
import * as diplomaticLogic from './diplomaticLogic'
|
||||
import { DIPLOMATIC_CONFIG } from '@/config/gameConfig'
|
||||
|
||||
/**
|
||||
* NPC行为决策系统
|
||||
*
|
||||
* 流程:
|
||||
* 1. NPC定期侦查玩家星球
|
||||
* 2. 玩家收到"被侦查"通知
|
||||
* 3. 基于侦查结果,NPC决定是否攻击
|
||||
* 4. NPC发起攻击,玩家收到实时警告
|
||||
* 5. NPC检测并回收自己星球附近的残骸
|
||||
* 6. NPC被攻击后会做出防御性反应或反击
|
||||
*/
|
||||
|
||||
// 动态行为配置接口
|
||||
export interface DynamicBehaviorConfig {
|
||||
spyInterval: number
|
||||
attackInterval: number
|
||||
attackProbability: number
|
||||
minSpyProbes: number
|
||||
attackFleetSizeRatio: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据玩家积分计算动态NPC行为配置
|
||||
* 积分越高,NPC越激进
|
||||
*/
|
||||
export const calculateDynamicBehavior = (playerPoints: number): DynamicBehaviorConfig => {
|
||||
if (playerPoints < 1000) {
|
||||
// 新手阶段:NPC很温和
|
||||
return {
|
||||
spyInterval: 2400, // 40分钟侦查一次
|
||||
attackInterval: 4800, // 80分钟攻击一次
|
||||
attackProbability: 0.15, // 15%概率攻击
|
||||
minSpyProbes: 1,
|
||||
attackFleetSizeRatio: 0.3 // 只派30%舰队
|
||||
}
|
||||
} else if (playerPoints < 5000) {
|
||||
// 初级阶段:NPC稍微激进
|
||||
return {
|
||||
spyInterval: 1800, // 30分钟侦查一次
|
||||
attackInterval: 3600, // 60分钟攻击一次
|
||||
attackProbability: 0.25, // 25%概率攻击
|
||||
minSpyProbes: 2,
|
||||
attackFleetSizeRatio: 0.5 // 派50%舰队
|
||||
}
|
||||
} else if (playerPoints < 20000) {
|
||||
// 中级阶段:NPC比较激进
|
||||
return {
|
||||
spyInterval: 1200, // 20分钟侦查一次
|
||||
attackInterval: 2400, // 40分钟攻击一次
|
||||
attackProbability: 0.4, // 40%概率攻击
|
||||
minSpyProbes: 3,
|
||||
attackFleetSizeRatio: 0.7 // 派70%舰队
|
||||
}
|
||||
} else if (playerPoints < 50000) {
|
||||
// 高级阶段:NPC很激进
|
||||
return {
|
||||
spyInterval: 900, // 15分钟侦查一次
|
||||
attackInterval: 1800, // 30分钟攻击一次
|
||||
attackProbability: 0.55, // 55%概率攻击
|
||||
minSpyProbes: 4,
|
||||
attackFleetSizeRatio: 0.85 // 派85%舰队
|
||||
}
|
||||
} else {
|
||||
// 专家阶段:NPC非常激进
|
||||
return {
|
||||
spyInterval: 600, // 10分钟侦查一次
|
||||
attackInterval: 1200, // 20分钟攻击一次
|
||||
attackProbability: 0.7, // 70%概率攻击
|
||||
minSpyProbes: 5,
|
||||
attackFleetSizeRatio: 0.95 // 派95%舰队
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查NPC是否应该侦查玩家
|
||||
*/
|
||||
export const shouldNPCSpyPlayer = (npc: NPC, player: Player, currentTime: number, config: DynamicBehaviorConfig): boolean => {
|
||||
const lastSpyTime = npc.lastSpyTime || 0
|
||||
|
||||
// 检查是否达到侦查间隔
|
||||
if (currentTime - lastSpyTime < config.spyInterval * 1000) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查外交关系 - 根据关系状态调整侦查概率
|
||||
const relation = npc.relations?.[player.id]
|
||||
if (relation) {
|
||||
if (relation.status === RelationStatus.Friendly) {
|
||||
// 友好NPC侦查频率降低到50%
|
||||
return Math.random() < 0.5
|
||||
}
|
||||
if (relation.status === RelationStatus.Hostile) {
|
||||
// 敌对NPC侦查频率提高50%
|
||||
return Math.random() < 1.5
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查NPC是否应该攻击玩家
|
||||
*/
|
||||
export const shouldNPCAttackPlayer = (npc: NPC, player: Player, currentTime: number, config: DynamicBehaviorConfig): boolean => {
|
||||
const lastAttackTime = npc.lastAttackTime || 0
|
||||
|
||||
// 检查是否达到攻击间隔
|
||||
if (currentTime - lastAttackTime < config.attackInterval * 1000) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查外交关系
|
||||
const relation = npc.relations?.[player.id]
|
||||
if (relation) {
|
||||
if (relation.status === RelationStatus.Friendly) {
|
||||
// 友好NPC不攻击玩家
|
||||
return false
|
||||
}
|
||||
if (relation.status === RelationStatus.Hostile) {
|
||||
// 敌对NPC攻击概率翻倍
|
||||
return Math.random() < config.attackProbability * 2.0
|
||||
}
|
||||
}
|
||||
|
||||
// 中立或无关系:正常概率
|
||||
return Math.random() < config.attackProbability
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查NPC是否应该赠送资源给玩家
|
||||
*/
|
||||
export const shouldNPCGiftPlayer = (npc: NPC, player: Player, currentTime: number): boolean => {
|
||||
const { NPC_GIFT_CONFIG } = DIPLOMATIC_CONFIG
|
||||
|
||||
// 检查功能是否启用
|
||||
if (!NPC_GIFT_CONFIG.ENABLED) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查上次赠送时间
|
||||
const lastGiftTime = (npc as any).lastGiftTime || 0
|
||||
if (currentTime - lastGiftTime < NPC_GIFT_CONFIG.CHECK_INTERVAL * 1000) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查NPC对玩家的好感度
|
||||
const relation = npc.relations?.[player.id]
|
||||
if (!relation || relation.reputation < NPC_GIFT_CONFIG.MIN_REPUTATION) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 随机概率
|
||||
return Math.random() < NPC_GIFT_CONFIG.GIFT_PROBABILITY
|
||||
}
|
||||
|
||||
/**
|
||||
* NPC向玩家赠送资源
|
||||
*/
|
||||
export const giftResourcesToPlayer = (npc: NPC, player: Player): void => {
|
||||
const { NPC_GIFT_CONFIG } = DIPLOMATIC_CONFIG
|
||||
|
||||
// 随机生成赠送资源量
|
||||
const giftResources = {
|
||||
metal:
|
||||
Math.floor(Math.random() * (NPC_GIFT_CONFIG.GIFT_AMOUNT.METAL.max - NPC_GIFT_CONFIG.GIFT_AMOUNT.METAL.min + 1)) +
|
||||
NPC_GIFT_CONFIG.GIFT_AMOUNT.METAL.min,
|
||||
crystal:
|
||||
Math.floor(Math.random() * (NPC_GIFT_CONFIG.GIFT_AMOUNT.CRYSTAL.max - NPC_GIFT_CONFIG.GIFT_AMOUNT.CRYSTAL.min + 1)) +
|
||||
NPC_GIFT_CONFIG.GIFT_AMOUNT.CRYSTAL.min,
|
||||
deuterium:
|
||||
Math.floor(Math.random() * (NPC_GIFT_CONFIG.GIFT_AMOUNT.DEUTERIUM.max - NPC_GIFT_CONFIG.GIFT_AMOUNT.DEUTERIUM.min + 1)) +
|
||||
NPC_GIFT_CONFIG.GIFT_AMOUNT.DEUTERIUM.min,
|
||||
darkMatter: 0,
|
||||
energy: 0
|
||||
}
|
||||
|
||||
// 处理赠送
|
||||
diplomaticLogic.handleNPCGiftToPlayer(npc, player, giftResources)
|
||||
|
||||
// 更新上次赠送时间
|
||||
;(npc as any).lastGiftTime = Date.now()
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择NPC的最佳攻击来源星球
|
||||
*/
|
||||
const selectBestNPCPlanet = (npc: NPC, targetPosition: { galaxy: number; system: number; position: number }): Planet | null => {
|
||||
if (npc.planets.length === 0) return null
|
||||
|
||||
// 选择距离最近且有舰队的星球
|
||||
let bestPlanet: Planet | null = null
|
||||
let minDistance = Infinity
|
||||
|
||||
for (const planet of npc.planets) {
|
||||
const distance = fleetLogic.calculateDistance(planet.position, targetPosition)
|
||||
const hasFleet = Object.values(planet.fleet).some(count => (count || 0) > 0)
|
||||
|
||||
if (hasFleet && distance < minDistance) {
|
||||
minDistance = distance
|
||||
bestPlanet = planet
|
||||
}
|
||||
}
|
||||
|
||||
return bestPlanet
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建NPC侦查任务
|
||||
*/
|
||||
export const createNPCSpyMission = (
|
||||
npc: NPC,
|
||||
targetPlanet: Planet,
|
||||
_planets: Planet[],
|
||||
config: DynamicBehaviorConfig
|
||||
): FleetMission | null => {
|
||||
// 选择NPC的最佳星球作为起点
|
||||
const npcPlanet = selectBestNPCPlanet(npc, targetPlanet.position)
|
||||
if (!npcPlanet) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 检查NPC是否有足够的间谍探测器
|
||||
const spyProbes = npcPlanet.fleet[ShipType.EspionageProbe] || 0
|
||||
if (spyProbes < config.minSpyProbes) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 创建侦查舰队
|
||||
const fleet: Partial<Fleet> = {
|
||||
[ShipType.EspionageProbe]: config.minSpyProbes
|
||||
}
|
||||
|
||||
// 从NPC星球扣除舰队
|
||||
npcPlanet.fleet[ShipType.EspionageProbe] = (npcPlanet.fleet[ShipType.EspionageProbe] || 0) - config.minSpyProbes
|
||||
|
||||
// 计算飞行时间
|
||||
const distance = fleetLogic.calculateDistance(npcPlanet.position, targetPlanet.position)
|
||||
const spyProbeSpeed = 100000000 // 间谍探测器速度
|
||||
const flightTime = fleetLogic.calculateFlightTime(distance, spyProbeSpeed)
|
||||
|
||||
const now = Date.now()
|
||||
const mission: FleetMission = {
|
||||
id: `npc-spy-${npc.id}-${now}`,
|
||||
playerId: npc.id,
|
||||
npcId: npc.id,
|
||||
isHostile: true,
|
||||
originPlanetId: npcPlanet.id,
|
||||
targetPosition: targetPlanet.position,
|
||||
targetPlanetId: targetPlanet.id,
|
||||
missionType: MissionType.Spy,
|
||||
fleet,
|
||||
cargo: { metal: 0, crystal: 0, deuterium: 0, darkMatter: 0, energy: 0 },
|
||||
departureTime: now,
|
||||
arrivalTime: now + flightTime * 1000,
|
||||
status: 'outbound'
|
||||
}
|
||||
|
||||
// 更新NPC的上次侦查时间
|
||||
npc.lastSpyTime = now
|
||||
|
||||
// 添加到NPC任务列表
|
||||
if (!npc.fleetMissions) {
|
||||
npc.fleetMissions = []
|
||||
}
|
||||
npc.fleetMissions.push(mission)
|
||||
|
||||
return mission
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理NPC侦查到达
|
||||
* 返回:被侦查通知(给玩家)和侦查报告(给NPC)
|
||||
*/
|
||||
export const processNPCSpyArrival = (
|
||||
npc: NPC,
|
||||
mission: FleetMission,
|
||||
targetPlanet: Planet,
|
||||
player: Player
|
||||
): { spiedNotification: SpiedNotification; spyReport: SpyReport } => {
|
||||
// 计算侦查等级(基于NPC的间谍科技)
|
||||
const npcSpyTech = npc.technologies[TechnologyType.EspionageTechnology] || 0
|
||||
const playerSpyTech = player.technologies[TechnologyType.EspionageTechnology] || 0
|
||||
const spyProbeCount = mission.fleet[ShipType.EspionageProbe] || 0
|
||||
|
||||
// 计算被发现的概率
|
||||
const detectionChance = Math.max(0, Math.min(100, 50 + (playerSpyTech - npcSpyTech) * 10 - spyProbeCount * 5))
|
||||
const detected = Math.random() * 100 < detectionChance
|
||||
|
||||
// 创建NPC的侦查报告
|
||||
const spyReport: SpyReport = {
|
||||
id: `npc-spy-report-${mission.id}`,
|
||||
timestamp: Date.now(),
|
||||
spyId: npc.id,
|
||||
targetPlanetId: targetPlanet.id,
|
||||
targetPlanetName: targetPlanet.name,
|
||||
targetPosition: targetPlanet.position,
|
||||
targetPlayerId: player.id,
|
||||
resources: { ...targetPlanet.resources },
|
||||
fleet: npcSpyTech >= 2 ? { ...targetPlanet.fleet } : undefined,
|
||||
defense: npcSpyTech >= 4 ? { ...targetPlanet.defense } : undefined,
|
||||
buildings: npcSpyTech >= 6 ? { ...targetPlanet.buildings } : undefined,
|
||||
technologies: npcSpyTech >= 8 ? { ...player.technologies } : undefined,
|
||||
detectionChance
|
||||
}
|
||||
|
||||
// 保存到NPC的侦查报告
|
||||
if (!npc.playerSpyReports) {
|
||||
npc.playerSpyReports = {}
|
||||
}
|
||||
npc.playerSpyReports[targetPlanet.id] = spyReport
|
||||
|
||||
// 创建被侦查通知(给玩家)
|
||||
const spiedNotification: SpiedNotification = {
|
||||
id: `spied-${mission.id}`,
|
||||
timestamp: Date.now(),
|
||||
npcId: npc.id,
|
||||
npcName: npc.name,
|
||||
targetPlanetId: targetPlanet.id,
|
||||
targetPlanetName: targetPlanet.name,
|
||||
detectionSuccess: detected,
|
||||
read: false
|
||||
}
|
||||
|
||||
// 舰队返回
|
||||
mission.status = 'returning'
|
||||
mission.returnTime = Date.now() + (mission.arrivalTime - mission.departureTime)
|
||||
|
||||
return { spiedNotification, spyReport }
|
||||
}
|
||||
|
||||
/**
|
||||
* 决定NPC攻击舰队组成
|
||||
* 基于侦查报告和NPC实力
|
||||
*/
|
||||
const decideAttackFleet = (_npc: NPC, npcPlanet: Planet, _spyReport: SpyReport, config: DynamicBehaviorConfig): Partial<Fleet> | null => {
|
||||
// 简单策略:派出一定比例的可用舰队
|
||||
const attackFleet: Partial<Fleet> = {}
|
||||
let hasShips = false
|
||||
|
||||
// 优先派出战斗舰船
|
||||
const combatShips = [
|
||||
ShipType.LightFighter,
|
||||
ShipType.HeavyFighter,
|
||||
ShipType.Cruiser,
|
||||
ShipType.Battleship,
|
||||
ShipType.Bomber,
|
||||
ShipType.Destroyer,
|
||||
ShipType.Battlecruiser,
|
||||
ShipType.Deathstar
|
||||
]
|
||||
|
||||
for (const shipType of combatShips) {
|
||||
const available = npcPlanet.fleet[shipType] || 0
|
||||
if (available > 0) {
|
||||
const sendCount = Math.floor(available * config.attackFleetSizeRatio)
|
||||
if (sendCount > 0) {
|
||||
attackFleet[shipType] = sendCount
|
||||
hasShips = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return hasShips ? attackFleet : null
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建NPC攻击任务
|
||||
*/
|
||||
export const createNPCAttackMission = (
|
||||
npc: NPC,
|
||||
targetPlanet: Planet,
|
||||
spyReport: SpyReport,
|
||||
config: DynamicBehaviorConfig
|
||||
): FleetMission | null => {
|
||||
// 选择NPC的最佳星球作为起点
|
||||
const npcPlanet = selectBestNPCPlanet(npc, targetPlanet.position)
|
||||
if (!npcPlanet) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 决定攻击舰队
|
||||
const attackFleet = decideAttackFleet(npc, npcPlanet, spyReport, config)
|
||||
if (!attackFleet) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 从NPC星球扣除舰队
|
||||
for (const [shipType, count] of Object.entries(attackFleet)) {
|
||||
npcPlanet.fleet[shipType as ShipType] = (npcPlanet.fleet[shipType as ShipType] || 0) - (count as number)
|
||||
}
|
||||
|
||||
// 计算飞行时间
|
||||
const distance = fleetLogic.calculateDistance(npcPlanet.position, targetPlanet.position)
|
||||
// 找出舰队中最慢的船速
|
||||
let minSpeed = Infinity
|
||||
for (const _shipType of Object.keys(attackFleet)) {
|
||||
// 这里简化处理,实际应该从SHIPS配置中获取速度
|
||||
const baseSpeed = 10000 // 简化
|
||||
minSpeed = Math.min(minSpeed, baseSpeed)
|
||||
}
|
||||
const flightTime = fleetLogic.calculateFlightTime(distance, minSpeed)
|
||||
|
||||
const now = Date.now()
|
||||
const mission: FleetMission = {
|
||||
id: `npc-attack-${npc.id}-${now}`,
|
||||
playerId: npc.id,
|
||||
npcId: npc.id,
|
||||
isHostile: true,
|
||||
originPlanetId: npcPlanet.id,
|
||||
targetPosition: targetPlanet.position,
|
||||
targetPlanetId: targetPlanet.id,
|
||||
missionType: MissionType.Attack,
|
||||
fleet: attackFleet,
|
||||
cargo: { metal: 0, crystal: 0, deuterium: 0, darkMatter: 0, energy: 0 },
|
||||
departureTime: now,
|
||||
arrivalTime: now + flightTime * 1000,
|
||||
status: 'outbound'
|
||||
}
|
||||
|
||||
// 更新NPC的上次攻击时间
|
||||
npc.lastAttackTime = now
|
||||
|
||||
// 添加到NPC任务列表
|
||||
if (!npc.fleetMissions) {
|
||||
npc.fleetMissions = []
|
||||
}
|
||||
npc.fleetMissions.push(mission)
|
||||
|
||||
return mission
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建或更新即将到来的舰队警告
|
||||
*/
|
||||
export const createIncomingFleetAlert = (mission: FleetMission, npc: NPC, targetPlanet: Planet): IncomingFleetAlert => {
|
||||
const fleetSize = Object.values(mission.fleet).reduce((sum, count) => sum + (count || 0), 0)
|
||||
|
||||
return {
|
||||
id: mission.id,
|
||||
npcId: npc.id,
|
||||
npcName: npc.name,
|
||||
missionType: mission.missionType,
|
||||
targetPlanetId: targetPlanet.id,
|
||||
targetPlanetName: targetPlanet.name,
|
||||
arrivalTime: mission.arrivalTime,
|
||||
fleetSize,
|
||||
read: false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新玩家的即将到来的舰队警告
|
||||
* 删除已到达或已返回的任务警告
|
||||
*/
|
||||
export const updateIncomingFleetAlerts = (player: Player, currentTime: number): void => {
|
||||
if (!player.incomingFleetAlerts) {
|
||||
player.incomingFleetAlerts = []
|
||||
}
|
||||
|
||||
// 删除已过期的警告(舰队已到达)
|
||||
player.incomingFleetAlerts = player.incomingFleetAlerts.filter(alert => alert.arrivalTime > currentTime)
|
||||
}
|
||||
|
||||
/**
|
||||
* NPC主动行为主更新函数
|
||||
* 应该在游戏循环中定期调用
|
||||
*/
|
||||
export const updateNPCBehavior = (
|
||||
npc: NPC,
|
||||
player: Player,
|
||||
allPlanets: Planet[],
|
||||
debrisFields: Record<string, DebrisField>,
|
||||
currentTime: number
|
||||
): void => {
|
||||
// 根据玩家积分计算动态行为配置
|
||||
const config = calculateDynamicBehavior(player.points)
|
||||
|
||||
// 1. 检查并回收附近的残骸(优先级最高)
|
||||
const nearbyDebris = findNearbyDebris(npc, debrisFields)
|
||||
if (nearbyDebris.length > 0) {
|
||||
// 检查是否已经有正在执行的回收任务
|
||||
const activeRecycleMissions = npc.fleetMissions?.filter(m => m.missionType === MissionType.Recycle && m.status === 'outbound') || []
|
||||
const activeDebrisIds = new Set(activeRecycleMissions.map(m => m.debrisFieldId).filter(Boolean))
|
||||
|
||||
// 找到还没有被回收的残骸
|
||||
const availableDebris = nearbyDebris.filter(d => !activeDebrisIds.has(d.id))
|
||||
|
||||
if (availableDebris.length > 0) {
|
||||
// 随机选择一个残骸场进行回收
|
||||
const targetDebris = availableDebris[Math.floor(Math.random() * availableDebris.length)]
|
||||
if (targetDebris) {
|
||||
createNPCRecycleMission(npc, targetDebris, player, allPlanets)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 检查是否应该反击(优先于普通攻击)
|
||||
if (shouldNPCRevenge(npc, currentTime)) {
|
||||
const revengeMission = createNPCRevengeMission(npc, allPlanets, config)
|
||||
if (revengeMission) {
|
||||
// 找到目标星球创建警告
|
||||
const targetPlanet = allPlanets.find(p => p.id === revengeMission.targetPlanetId)
|
||||
if (targetPlanet) {
|
||||
const alert = createIncomingFleetAlert(revengeMission, npc, targetPlanet)
|
||||
if (!player.incomingFleetAlerts) {
|
||||
player.incomingFleetAlerts = []
|
||||
}
|
||||
player.incomingFleetAlerts.push(alert)
|
||||
}
|
||||
// 反击后跳过普通攻击
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 检查是否应该侦查玩家
|
||||
if (shouldNPCSpyPlayer(npc, player, currentTime, config)) {
|
||||
// 随机选择一个玩家星球进行侦查
|
||||
const playerPlanets = allPlanets.filter(p => p.ownerId === player.id)
|
||||
if (playerPlanets.length > 0) {
|
||||
const targetPlanet = playerPlanets[Math.floor(Math.random() * playerPlanets.length)]
|
||||
if (!targetPlanet) {
|
||||
return
|
||||
}
|
||||
|
||||
const spyMission = createNPCSpyMission(npc, targetPlanet, allPlanets, config)
|
||||
|
||||
if (spyMission) {
|
||||
// 创建即将到来的舰队警告
|
||||
const alert = createIncomingFleetAlert(spyMission, npc, targetPlanet)
|
||||
if (!player.incomingFleetAlerts) {
|
||||
player.incomingFleetAlerts = []
|
||||
}
|
||||
player.incomingFleetAlerts.push(alert)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 检查是否应该攻击玩家
|
||||
if (shouldNPCAttackPlayer(npc, player, currentTime, config)) {
|
||||
// 检查是否有最近的侦查报告
|
||||
if (npc.playerSpyReports && Object.keys(npc.playerSpyReports).length > 0) {
|
||||
// 选择一个侦查过的星球进行攻击
|
||||
const spyReports = Object.values(npc.playerSpyReports)
|
||||
const recentReport = spyReports[Math.floor(Math.random() * spyReports.length)]
|
||||
|
||||
// 确保找到了侦查报告
|
||||
if (!recentReport) {
|
||||
return
|
||||
}
|
||||
|
||||
// 找到目标星球
|
||||
const targetPlanet = allPlanets.find(p => p.id === recentReport.targetPlanetId)
|
||||
if (targetPlanet) {
|
||||
const attackMission = createNPCAttackMission(npc, targetPlanet, recentReport, config)
|
||||
|
||||
if (attackMission) {
|
||||
// 创建即将到来的舰队警告
|
||||
const alert = createIncomingFleetAlert(attackMission, npc, targetPlanet)
|
||||
if (!player.incomingFleetAlerts) {
|
||||
player.incomingFleetAlerts = []
|
||||
}
|
||||
player.incomingFleetAlerts.push(alert)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 检查是否应该赠送资源给玩家(仅友好NPC)
|
||||
if (shouldNPCGiftPlayer(npc, player, currentTime)) {
|
||||
giftResourcesToPlayer(npc, player)
|
||||
}
|
||||
|
||||
// 6. 更新即将到来的舰队警告(删除过期的)
|
||||
updateIncomingFleetAlerts(player, currentTime)
|
||||
}
|
||||
|
||||
// ========== 测试辅助函数 ==========
|
||||
|
||||
/**
|
||||
* 测试函数:强制NPC立即侦查玩家
|
||||
* 用于开发和测试
|
||||
*/
|
||||
export const forceNPCSpyPlayer = (npc: NPC, player: Player, allPlanets: Planet[], targetPlanetIndex = 0): FleetMission | null => {
|
||||
const config = calculateDynamicBehavior(player.points)
|
||||
|
||||
// 选择目标星球
|
||||
const playerPlanets = allPlanets.filter(p => p.ownerId === player.id)
|
||||
if (playerPlanets.length === 0) {
|
||||
console.error('[Test] No player planets found')
|
||||
return null
|
||||
}
|
||||
|
||||
const targetPlanet = playerPlanets[targetPlanetIndex] || playerPlanets[0]
|
||||
if (!targetPlanet) {
|
||||
console.error('[Test] Target planet not found')
|
||||
return null
|
||||
}
|
||||
|
||||
// 创建侦查任务
|
||||
const spyMission = createNPCSpyMission(npc, targetPlanet, allPlanets, config)
|
||||
|
||||
if (spyMission) {
|
||||
// 创建即将到来的舰队警告
|
||||
const alert = createIncomingFleetAlert(spyMission, npc, targetPlanet)
|
||||
if (!player.incomingFleetAlerts) {
|
||||
player.incomingFleetAlerts = []
|
||||
}
|
||||
player.incomingFleetAlerts.push(alert)
|
||||
} else {
|
||||
console.error('[Test] Failed to create spy mission - NPC may not have spy probes')
|
||||
}
|
||||
|
||||
return spyMission
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试函数:强制NPC立即攻击玩家
|
||||
* 需要先有侦查报告
|
||||
*/
|
||||
export const forceNPCAttackPlayer = (npc: NPC, player: Player, allPlanets: Planet[], targetPlanetIndex = 0): FleetMission | null => {
|
||||
const config = calculateDynamicBehavior(player.points)
|
||||
|
||||
// 检查是否有侦查报告
|
||||
if (!npc.playerSpyReports || Object.keys(npc.playerSpyReports).length === 0) {
|
||||
console.error('[Test] No spy reports available - NPC must spy first!')
|
||||
return null
|
||||
}
|
||||
|
||||
// 选择目标星球
|
||||
const playerPlanets = allPlanets.filter(p => p.ownerId === player.id)
|
||||
if (playerPlanets.length === 0) {
|
||||
console.error('[Test] No player planets found')
|
||||
return null
|
||||
}
|
||||
|
||||
const targetPlanet = playerPlanets[targetPlanetIndex] || playerPlanets[0]
|
||||
if (!targetPlanet) {
|
||||
console.error('[Test] Target planet not found')
|
||||
return null
|
||||
}
|
||||
|
||||
// 获取该星球的侦查报告
|
||||
const spyReport = npc.playerSpyReports[targetPlanet.id]
|
||||
if (!spyReport) {
|
||||
console.error(`[Test] No spy report for ${targetPlanet.name} - spy this planet first!`)
|
||||
return null
|
||||
}
|
||||
|
||||
// 创建攻击任务
|
||||
const attackMission = createNPCAttackMission(npc, targetPlanet, spyReport, config)
|
||||
|
||||
if (attackMission) {
|
||||
// 创建即将到来的舰队警告
|
||||
const alert = createIncomingFleetAlert(attackMission, npc, targetPlanet)
|
||||
if (!player.incomingFleetAlerts) {
|
||||
player.incomingFleetAlerts = []
|
||||
}
|
||||
player.incomingFleetAlerts.push(alert)
|
||||
} else {
|
||||
console.error('[Test] Failed to create attack mission - NPC may not have ships')
|
||||
}
|
||||
|
||||
return attackMission
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试函数:强制NPC先侦查再攻击
|
||||
* 一步到位的测试函数
|
||||
*/
|
||||
export const forceNPCSpyAndAttack = (
|
||||
npc: NPC,
|
||||
player: Player,
|
||||
allPlanets: Planet[],
|
||||
targetPlanetIndex = 0
|
||||
): { spyMission: FleetMission | null; attackMission: FleetMission | null } => {
|
||||
// 1. 先侦查
|
||||
const spyMission = forceNPCSpyPlayer(npc, player, allPlanets, targetPlanetIndex)
|
||||
|
||||
if (!spyMission) {
|
||||
return { spyMission: null, attackMission: null }
|
||||
}
|
||||
|
||||
// 2. 模拟侦查到达,立即生成侦查报告
|
||||
const playerPlanets = allPlanets.filter(p => p.ownerId === player.id)
|
||||
const targetPlanet = playerPlanets[targetPlanetIndex] || playerPlanets[0]
|
||||
if (!targetPlanet) {
|
||||
console.error('[Test] Target planet not found')
|
||||
return { spyMission, attackMission: null }
|
||||
}
|
||||
|
||||
const { spyReport } = processNPCSpyArrival(npc, spyMission, targetPlanet, player)
|
||||
|
||||
// 保存侦查报告到NPC
|
||||
if (!npc.playerSpyReports) {
|
||||
npc.playerSpyReports = {}
|
||||
}
|
||||
npc.playerSpyReports[targetPlanet.id] = spyReport
|
||||
|
||||
// 3. 立即发起攻击
|
||||
const attackMission = forceNPCAttackPlayer(npc, player, allPlanets, targetPlanetIndex)
|
||||
|
||||
return { spyMission, attackMission }
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试函数:加速舰队任务到达时间
|
||||
* 将任务的到达时间设置为现在+指定秒数
|
||||
*/
|
||||
export const accelerateNPCMission = (npc: NPC, missionId: string, arriveInSeconds = 5): boolean => {
|
||||
if (!npc.fleetMissions) {
|
||||
console.error('[Test] NPC has no fleet missions')
|
||||
return false
|
||||
}
|
||||
|
||||
const mission = npc.fleetMissions.find(m => m.id === missionId)
|
||||
if (!mission) {
|
||||
console.error('[Test] Mission not found')
|
||||
return false
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
mission.arrivalTime = now + arriveInSeconds * 1000
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试函数:加速所有NPC舰队任务
|
||||
*/
|
||||
export const accelerateAllNPCMissions = (npc: NPC, arriveInSeconds = 5): number => {
|
||||
if (!npc.fleetMissions) {
|
||||
console.error('[Test] NPC has no fleet missions')
|
||||
return 0
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
let count = 0
|
||||
|
||||
npc.fleetMissions.forEach(mission => {
|
||||
if (mission.status === 'outbound') {
|
||||
mission.arrivalTime = now + arriveInSeconds * 1000
|
||||
count++
|
||||
} else if (mission.status === 'returning' && mission.returnTime) {
|
||||
mission.returnTime = now + arriveInSeconds * 1000
|
||||
count++
|
||||
}
|
||||
})
|
||||
|
||||
return count
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查NPC星球附近是否有残骸场
|
||||
* 返回NPC可以回收的残骸场列表
|
||||
* NPC会主动寻找同一星系内的残骸进行回收
|
||||
*/
|
||||
export const findNearbyDebris = (npc: NPC, debrisFields: Record<string, DebrisField>): DebrisField[] => {
|
||||
const nearbyDebris: DebrisField[] = []
|
||||
|
||||
for (const debris of Object.values(debrisFields)) {
|
||||
// 检查残骸是否在NPC的星球附近(同一星系内)
|
||||
for (const npcPlanet of npc.planets) {
|
||||
if (debris.position.galaxy === npcPlanet.position.galaxy && debris.position.system === npcPlanet.position.system) {
|
||||
// 检查残骸是否有足够资源值得回收(至少1000金属或水晶)
|
||||
if (debris.resources.metal > 1000 || debris.resources.crystal > 1000) {
|
||||
// 计算距离,确保不会太远(最多在同一星系内)
|
||||
const distance = Math.abs(debris.position.position - npcPlanet.position.position)
|
||||
if (distance <= 15) {
|
||||
// 同一星系内最多15个位置
|
||||
nearbyDebris.push(debris)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nearbyDebris
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建NPC回收残骸任务
|
||||
*/
|
||||
export const createNPCRecycleMission = (npc: NPC, debris: DebrisField, player: Player, allPlanets: Planet[]): FleetMission | null => {
|
||||
// 找到离残骸最近的NPC星球
|
||||
let closestPlanet: Planet | null = null
|
||||
let minDistance = Infinity
|
||||
|
||||
for (const npcPlanet of npc.planets) {
|
||||
if (npcPlanet.position.galaxy === debris.position.galaxy && npcPlanet.position.system === debris.position.system) {
|
||||
const distance = Math.abs(npcPlanet.position.position - debris.position.position)
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance
|
||||
closestPlanet = npcPlanet
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!closestPlanet) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 检查NPC是否有回收船
|
||||
const recyclers = closestPlanet.fleet[ShipType.Recycler] || 0
|
||||
if (recyclers === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 计算需要多少回收船(每艘回收船容量20000)
|
||||
const totalDebris = debris.resources.metal + debris.resources.crystal
|
||||
const recyclersNeeded = Math.min(Math.ceil(totalDebris / 20000), recyclers)
|
||||
|
||||
// 创建回收舰队
|
||||
const fleet: Partial<Fleet> = {
|
||||
[ShipType.Recycler]: recyclersNeeded
|
||||
}
|
||||
|
||||
// 从NPC星球扣除舰队
|
||||
closestPlanet.fleet[ShipType.Recycler] = recyclers - recyclersNeeded
|
||||
|
||||
// 计算飞行时间
|
||||
const distance = fleetLogic.calculateDistance(closestPlanet.position, debris.position)
|
||||
const recyclerSpeed = 2000 // 回收船基础速度
|
||||
const flightTime = fleetLogic.calculateFlightTime(distance, recyclerSpeed)
|
||||
|
||||
const now = Date.now()
|
||||
const mission: FleetMission = {
|
||||
id: `npc-recycle-${npc.id}-${now}`,
|
||||
playerId: npc.id,
|
||||
npcId: npc.id,
|
||||
isHostile: false,
|
||||
originPlanetId: closestPlanet.id,
|
||||
targetPosition: debris.position,
|
||||
debrisFieldId: debris.id,
|
||||
missionType: MissionType.Recycle,
|
||||
fleet,
|
||||
cargo: { metal: 0, crystal: 0, deuterium: 0, darkMatter: 0, energy: 0 },
|
||||
departureTime: now,
|
||||
arrivalTime: now + flightTime * 1000,
|
||||
status: 'outbound'
|
||||
}
|
||||
|
||||
// 添加到NPC任务列表
|
||||
if (!npc.fleetMissions) {
|
||||
npc.fleetMissions = []
|
||||
}
|
||||
npc.fleetMissions.push(mission)
|
||||
|
||||
// 检查残骸位置是否有玩家星球,如果有则发送警告
|
||||
const playerPlanetAtDebris = allPlanets.find(
|
||||
p =>
|
||||
p.ownerId === player.id &&
|
||||
p.position.galaxy === debris.position.galaxy &&
|
||||
p.position.system === debris.position.system &&
|
||||
p.position.position === debris.position.position
|
||||
)
|
||||
|
||||
if (playerPlanetAtDebris) {
|
||||
// 创建即将到来的舰队警告(非敌对)
|
||||
const alert = createIncomingFleetAlert(mission, npc, playerPlanetAtDebris)
|
||||
if (!player.incomingFleetAlerts) {
|
||||
player.incomingFleetAlerts = []
|
||||
}
|
||||
player.incomingFleetAlerts.push(alert)
|
||||
|
||||
// 创建NPC活动通知
|
||||
if (!player.npcActivityNotifications) {
|
||||
player.npcActivityNotifications = []
|
||||
}
|
||||
player.npcActivityNotifications.push({
|
||||
id: `npc-activity-${mission.id}`,
|
||||
timestamp: now,
|
||||
npcId: npc.id,
|
||||
npcName: npc.name,
|
||||
activityType: 'recycle',
|
||||
targetPosition: debris.position,
|
||||
targetPlanetId: playerPlanetAtDebris.id,
|
||||
targetPlanetName: playerPlanetAtDebris.name,
|
||||
arrivalTime: mission.arrivalTime,
|
||||
read: false
|
||||
})
|
||||
}
|
||||
|
||||
return mission
|
||||
}
|
||||
|
||||
/**
|
||||
* NPC被攻击后的反应
|
||||
* 记录攻击者,并根据情况决定是否反击或加强防御
|
||||
*/
|
||||
export const handleNPCAttacked = (npc: NPC, attackerId: string, attackerPlanetId: string | undefined): void => {
|
||||
// 初始化被攻击记录
|
||||
if (!npc.attackedBy) {
|
||||
npc.attackedBy = {}
|
||||
}
|
||||
|
||||
// 记录攻击者和被攻击次数
|
||||
if (!npc.attackedBy[attackerId]) {
|
||||
npc.attackedBy[attackerId] = {
|
||||
count: 0,
|
||||
lastAttackTime: 0,
|
||||
planetId: attackerPlanetId
|
||||
}
|
||||
}
|
||||
npc.attackedBy[attackerId].count++
|
||||
npc.attackedBy[attackerId].lastAttackTime = Date.now()
|
||||
if (attackerPlanetId) {
|
||||
npc.attackedBy[attackerId].planetId = attackerPlanetId
|
||||
}
|
||||
|
||||
// 设置警戒状态(被攻击后1小时内保持警戒)
|
||||
npc.alertUntil = Date.now() + 3600 * 1000
|
||||
|
||||
// 如果被同一个玩家攻击超过3次,标记为高优先级反击目标
|
||||
if (npc.attackedBy[attackerId].count >= 3) {
|
||||
npc.revengeTarget = attackerId
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查NPC是否应该反击
|
||||
*/
|
||||
export const shouldNPCRevenge = (npc: NPC, currentTime: number): boolean => {
|
||||
// 如果没有复仇目标,不反击
|
||||
if (!npc.revengeTarget || !npc.attackedBy) {
|
||||
return false
|
||||
}
|
||||
|
||||
const attackRecord = npc.attackedBy[npc.revengeTarget]
|
||||
if (!attackRecord) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 被攻击后24小时内可以反击
|
||||
const timeSinceLastAttack = currentTime - attackRecord.lastAttackTime
|
||||
if (timeSinceLastAttack > 24 * 3600 * 1000) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 至少等待10分钟后再反击(给NPC时间准备)
|
||||
if (timeSinceLastAttack < 600 * 1000) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建NPC反击任务
|
||||
*/
|
||||
export const createNPCRevengeMission = (npc: NPC, allPlanets: Planet[], config: DynamicBehaviorConfig): FleetMission | null => {
|
||||
if (!npc.revengeTarget || !npc.attackedBy) {
|
||||
return null
|
||||
}
|
||||
|
||||
const attackRecord = npc.attackedBy[npc.revengeTarget]
|
||||
if (!attackRecord || !attackRecord.planetId) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 找到攻击者的星球
|
||||
const targetPlanet = allPlanets.find(p => p.id === attackRecord.planetId)
|
||||
if (!targetPlanet) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 选择NPC的最佳星球作为起点
|
||||
const npcPlanet = selectBestNPCPlanet(npc, targetPlanet.position)
|
||||
if (!npcPlanet) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 反击时派出更多舰队(比正常攻击多50%)
|
||||
const revengeFleet = decideAttackFleet(npc, npcPlanet, {} as SpyReport, {
|
||||
...config,
|
||||
attackFleetSizeRatio: Math.min(1.0, config.attackFleetSizeRatio * 1.5)
|
||||
})
|
||||
|
||||
if (!revengeFleet) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 从NPC星球扣除舰队
|
||||
for (const [shipType, count] of Object.entries(revengeFleet)) {
|
||||
npcPlanet.fleet[shipType as ShipType] = (npcPlanet.fleet[shipType as ShipType] || 0) - (count as number)
|
||||
}
|
||||
|
||||
// 计算飞行时间
|
||||
const distance = fleetLogic.calculateDistance(npcPlanet.position, targetPlanet.position)
|
||||
let minSpeed = Infinity
|
||||
for (const _shipType of Object.keys(revengeFleet)) {
|
||||
const baseSpeed = 10000
|
||||
minSpeed = Math.min(minSpeed, baseSpeed)
|
||||
}
|
||||
const flightTime = fleetLogic.calculateFlightTime(distance, minSpeed)
|
||||
|
||||
const now = Date.now()
|
||||
const mission: FleetMission = {
|
||||
id: `npc-revenge-${npc.id}-${now}`,
|
||||
playerId: npc.id,
|
||||
npcId: npc.id,
|
||||
isHostile: true,
|
||||
originPlanetId: npcPlanet.id,
|
||||
targetPosition: targetPlanet.position,
|
||||
targetPlanetId: targetPlanet.id,
|
||||
missionType: MissionType.Attack,
|
||||
fleet: revengeFleet,
|
||||
cargo: { metal: 0, crystal: 0, deuterium: 0, darkMatter: 0, energy: 0 },
|
||||
departureTime: now,
|
||||
arrivalTime: now + flightTime * 1000,
|
||||
status: 'outbound'
|
||||
}
|
||||
|
||||
// 添加到NPC任务列表
|
||||
if (!npc.fleetMissions) {
|
||||
npc.fleetMissions = []
|
||||
}
|
||||
npc.fleetMissions.push(mission)
|
||||
|
||||
// 清除复仇目标(已经反击)
|
||||
npc.revengeTarget = undefined
|
||||
|
||||
return mission
|
||||
}
|
||||
588
src/logic/npcGrowthLogic.ts
Normal file
588
src/logic/npcGrowthLogic.ts
Normal file
@@ -0,0 +1,588 @@
|
||||
import type { NPC, Planet, Player } from '@/types/game'
|
||||
import { TechnologyType, BuildingType, ShipType } from '@/types/game'
|
||||
import { BUILDINGS, SHIPS, TECHNOLOGIES } from '@/config/gameConfig'
|
||||
import * as buildingLogic from './buildingLogic'
|
||||
import * as researchLogic from './researchLogic'
|
||||
|
||||
/**
|
||||
* NPC成长系统
|
||||
*
|
||||
* 策略说明:
|
||||
* - Easy: NPC实力保持在玩家平均实力的60%左右,被动成长
|
||||
* - Medium: NPC实力保持在玩家平均实力的80%左右,半主动成长
|
||||
* - Hard: NPC实力保持在玩家平均实力的100-120%,主动成长
|
||||
*/
|
||||
|
||||
// 简化的游戏状态接口(用于NPC成长系统)
|
||||
export interface NPCGrowthGameState {
|
||||
planets: Planet[] // 所有星球(包括玩家和NPC的)
|
||||
player: Player // 玩家数据
|
||||
npcs: NPC[] // 所有NPC
|
||||
}
|
||||
|
||||
// NPC成长配置(旧版,保留用于兼容)
|
||||
export const NPC_GROWTH_CONFIG = {
|
||||
easy: {
|
||||
powerRatio: 0.6, // 实力比例(相对玩家)
|
||||
checkInterval: 300, // 检查间隔(秒) - 5分钟
|
||||
resourceGrowthRate: 0.5, // 资源增长速率系数
|
||||
buildingGrowthSpeed: 0.5, // 建筑升级速度系数
|
||||
techGrowthSpeed: 0.5 // 科技研究速度系数
|
||||
},
|
||||
medium: {
|
||||
powerRatio: 0.8,
|
||||
checkInterval: 180, // 3分钟
|
||||
resourceGrowthRate: 0.8,
|
||||
buildingGrowthSpeed: 0.8,
|
||||
techGrowthSpeed: 0.8
|
||||
},
|
||||
hard: {
|
||||
powerRatio: 1.1,
|
||||
checkInterval: 120, // 2分钟
|
||||
resourceGrowthRate: 1.2,
|
||||
buildingGrowthSpeed: 1.0,
|
||||
techGrowthSpeed: 1.0
|
||||
}
|
||||
} as const
|
||||
|
||||
// 动态难度配置(基于玩家积分)
|
||||
export interface DynamicDifficultyConfig {
|
||||
powerRatio: number
|
||||
checkInterval: number
|
||||
resourceGrowthRate: number
|
||||
buildingGrowthSpeed: number
|
||||
techGrowthSpeed: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据玩家积分计算动态难度配置
|
||||
*/
|
||||
export const calculateDynamicDifficulty = (playerPoints: number): DynamicDifficultyConfig => {
|
||||
// 积分区间和对应的难度参数
|
||||
if (playerPoints < 1000) {
|
||||
// 新手期:0-1,000分
|
||||
// NPC保持30-50%实力,给予充分发展空间
|
||||
const ratio = 0.3 + (playerPoints / 1000) * 0.2
|
||||
return {
|
||||
powerRatio: ratio,
|
||||
checkInterval: 300, // 5分钟
|
||||
resourceGrowthRate: 0.4,
|
||||
buildingGrowthSpeed: 0.4,
|
||||
techGrowthSpeed: 0.4
|
||||
}
|
||||
} else if (playerPoints < 5000) {
|
||||
// 初级期:1,000-5,000分
|
||||
// NPC保持50-70%实力,逐渐增加挑战
|
||||
const ratio = 0.5 + ((playerPoints - 1000) / 4000) * 0.2
|
||||
return {
|
||||
powerRatio: ratio,
|
||||
checkInterval: 240, // 4分钟
|
||||
resourceGrowthRate: 0.6,
|
||||
buildingGrowthSpeed: 0.6,
|
||||
techGrowthSpeed: 0.6
|
||||
}
|
||||
} else if (playerPoints < 20000) {
|
||||
// 中级期:5,000-20,000分
|
||||
// NPC保持70-90%实力,持续挑战
|
||||
const ratio = 0.7 + ((playerPoints - 5000) / 15000) * 0.2
|
||||
return {
|
||||
powerRatio: ratio,
|
||||
checkInterval: 180, // 3分钟
|
||||
resourceGrowthRate: 0.8,
|
||||
buildingGrowthSpeed: 0.8,
|
||||
techGrowthSpeed: 0.8
|
||||
}
|
||||
} else if (playerPoints < 50000) {
|
||||
// 高级期:20,000-50,000分
|
||||
// NPC保持90-110%实力,与玩家势均力敌
|
||||
const ratio = 0.9 + ((playerPoints - 20000) / 30000) * 0.2
|
||||
return {
|
||||
powerRatio: ratio,
|
||||
checkInterval: 150, // 2.5分钟
|
||||
resourceGrowthRate: 1.0,
|
||||
buildingGrowthSpeed: 1.0,
|
||||
techGrowthSpeed: 1.0
|
||||
}
|
||||
} else {
|
||||
// 专家期:50,000+分
|
||||
// NPC保持110-130%实力,超越玩家
|
||||
const ratio = Math.min(1.3, 1.1 + ((playerPoints - 50000) / 50000) * 0.2)
|
||||
return {
|
||||
powerRatio: ratio,
|
||||
checkInterval: 120, // 2分钟
|
||||
resourceGrowthRate: 1.2,
|
||||
buildingGrowthSpeed: 1.2,
|
||||
techGrowthSpeed: 1.2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算玩家平均实力
|
||||
*/
|
||||
export const calculatePlayerAveragePower = (
|
||||
gameState: NPCGrowthGameState
|
||||
): {
|
||||
avgBuildingLevel: number
|
||||
avgTechLevel: number
|
||||
totalFleetPower: number
|
||||
totalResources: number
|
||||
} => {
|
||||
// 筛选出玩家的星球
|
||||
const playerPlanets = gameState.planets.filter(p => p.ownerId === gameState.player.id)
|
||||
|
||||
if (playerPlanets.length === 0) {
|
||||
return {
|
||||
avgBuildingLevel: 0,
|
||||
avgTechLevel: 0,
|
||||
totalFleetPower: 0,
|
||||
totalResources: 0
|
||||
}
|
||||
}
|
||||
|
||||
// 计算平均建筑等级
|
||||
let totalBuildingLevels = 0
|
||||
let buildingCount = 0
|
||||
playerPlanets.forEach(planet => {
|
||||
Object.values(planet.buildings).forEach(level => {
|
||||
totalBuildingLevels += level
|
||||
buildingCount++
|
||||
})
|
||||
})
|
||||
|
||||
// 计算平均科技等级
|
||||
const techLevels = Object.values(gameState.player.technologies)
|
||||
const avgTechLevel = techLevels.length > 0 ? techLevels.reduce((sum, level) => sum + level, 0) / techLevels.length : 0
|
||||
|
||||
// 计算总舰队战力
|
||||
let totalFleetPower = 0
|
||||
playerPlanets.forEach(planet => {
|
||||
Object.entries(planet.fleet).forEach(([shipType, count]) => {
|
||||
const shipConfig = SHIPS[shipType as ShipType]
|
||||
const power = shipConfig.attack + shipConfig.shield + shipConfig.armor / 10
|
||||
totalFleetPower += power * (count as number)
|
||||
})
|
||||
})
|
||||
|
||||
// 计算总资源
|
||||
const totalResources = playerPlanets.reduce(
|
||||
(sum, planet) => sum + planet.resources.metal + planet.resources.crystal + planet.resources.deuterium,
|
||||
0
|
||||
)
|
||||
|
||||
return {
|
||||
avgBuildingLevel: buildingCount > 0 ? totalBuildingLevels / buildingCount : 0,
|
||||
avgTechLevel,
|
||||
totalFleetPower,
|
||||
totalResources
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算NPC当前实力
|
||||
*/
|
||||
export const calculateNPCPower = (
|
||||
npc: NPC
|
||||
): {
|
||||
avgBuildingLevel: number
|
||||
avgTechLevel: number
|
||||
totalFleetPower: number
|
||||
totalResources: number
|
||||
} => {
|
||||
// 计算平均建筑等级
|
||||
let totalBuildingLevels = 0
|
||||
let buildingCount = 0
|
||||
npc.planets.forEach(planet => {
|
||||
Object.values(planet.buildings).forEach(level => {
|
||||
totalBuildingLevels += level
|
||||
buildingCount++
|
||||
})
|
||||
})
|
||||
|
||||
// 计算平均科技等级
|
||||
const techLevels = Object.values(npc.technologies)
|
||||
const avgTechLevel = techLevels.length > 0 ? techLevels.reduce((sum, level) => sum + level, 0) / techLevels.length : 0
|
||||
|
||||
// 计算总舰队战力
|
||||
let totalFleetPower = 0
|
||||
npc.planets.forEach(planet => {
|
||||
Object.entries(planet.fleet).forEach(([shipType, count]) => {
|
||||
const shipConfig = SHIPS[shipType as ShipType]
|
||||
const power = shipConfig.attack + shipConfig.shield + shipConfig.armor / 10
|
||||
totalFleetPower += power * (count as number)
|
||||
})
|
||||
})
|
||||
|
||||
// 计算总资源
|
||||
const totalResources = npc.planets.reduce(
|
||||
(sum, planet) => sum + planet.resources.metal + planet.resources.crystal + planet.resources.deuterium,
|
||||
0
|
||||
)
|
||||
|
||||
return {
|
||||
avgBuildingLevel: buildingCount > 0 ? totalBuildingLevels / buildingCount : 0,
|
||||
avgTechLevel,
|
||||
totalFleetPower,
|
||||
totalResources
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动升级NPC建筑
|
||||
*/
|
||||
export const autoUpgradeNPCBuildings = (npc: NPC): void => {
|
||||
const planet = npc.planets[0]
|
||||
if (!planet) return
|
||||
|
||||
// 优先级队列:资源建筑 > 设施建筑 > 其他
|
||||
const priorityBuildings: BuildingType[] = [
|
||||
BuildingType.MetalMine,
|
||||
BuildingType.CrystalMine,
|
||||
BuildingType.DeuteriumSynthesizer,
|
||||
BuildingType.SolarPlant,
|
||||
BuildingType.RoboticsFactory,
|
||||
BuildingType.Shipyard,
|
||||
BuildingType.ResearchLab,
|
||||
BuildingType.MetalStorage,
|
||||
BuildingType.CrystalStorage,
|
||||
BuildingType.DeuteriumTank,
|
||||
BuildingType.DarkMatterCollector
|
||||
]
|
||||
|
||||
// 尝试升级建筑
|
||||
for (const buildingType of priorityBuildings) {
|
||||
const currentLevel = planet.buildings[buildingType] || 0
|
||||
const targetLevel = currentLevel + 1
|
||||
|
||||
// 检查是否达到最大等级
|
||||
const buildingConfig = BUILDINGS[buildingType]
|
||||
if (buildingConfig.maxLevel && currentLevel >= buildingConfig.maxLevel) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 计算升级成本
|
||||
const cost = buildingLogic.calculateBuildingCost(buildingType, targetLevel)
|
||||
|
||||
// 检查资源是否足够
|
||||
if (
|
||||
planet.resources.metal >= cost.metal &&
|
||||
planet.resources.crystal >= cost.crystal &&
|
||||
planet.resources.deuterium >= cost.deuterium &&
|
||||
planet.resources.darkMatter >= cost.darkMatter
|
||||
) {
|
||||
// 扣除资源
|
||||
planet.resources.metal -= cost.metal
|
||||
planet.resources.crystal -= cost.crystal
|
||||
planet.resources.deuterium -= cost.deuterium
|
||||
planet.resources.darkMatter -= cost.darkMatter
|
||||
|
||||
// 直接升级(NPC不需要等待建造时间)
|
||||
planet.buildings[buildingType] = targetLevel
|
||||
|
||||
// 每次只升级一个建筑,避免一次性消耗太多资源
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动研究NPC科技
|
||||
*/
|
||||
export const autoResearchNPCTechnologies = (npc: NPC): void => {
|
||||
const planet = npc.planets[0]
|
||||
if (!planet) return
|
||||
|
||||
// 优先级队列:战斗科技 > 驱动科技 > 其他
|
||||
const priorityTechs: TechnologyType[] = [
|
||||
TechnologyType.EnergyTechnology,
|
||||
TechnologyType.ComputerTechnology,
|
||||
TechnologyType.WeaponsTechnology,
|
||||
TechnologyType.ShieldingTechnology,
|
||||
TechnologyType.ArmourTechnology,
|
||||
TechnologyType.CombustionDrive,
|
||||
TechnologyType.ImpulseDrive,
|
||||
TechnologyType.HyperspaceDrive,
|
||||
TechnologyType.LaserTechnology,
|
||||
TechnologyType.IonTechnology,
|
||||
TechnologyType.PlasmaTechnology,
|
||||
TechnologyType.Astrophysics,
|
||||
TechnologyType.EspionageTechnology
|
||||
]
|
||||
|
||||
// 尝试研究科技
|
||||
for (const techType of priorityTechs) {
|
||||
const currentLevel = npc.technologies[techType] || 0
|
||||
const targetLevel = currentLevel + 1
|
||||
|
||||
// 检查是否达到最大等级
|
||||
const techConfig = TECHNOLOGIES[techType]
|
||||
if (techConfig.maxLevel && currentLevel >= techConfig.maxLevel) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 计算研究成本
|
||||
const cost = researchLogic.calculateTechnologyCost(techType, targetLevel)
|
||||
|
||||
// 检查资源是否足够
|
||||
if (
|
||||
planet.resources.metal >= cost.metal &&
|
||||
planet.resources.crystal >= cost.crystal &&
|
||||
planet.resources.deuterium >= cost.deuterium &&
|
||||
planet.resources.darkMatter >= cost.darkMatter
|
||||
) {
|
||||
// 扣除资源
|
||||
planet.resources.metal -= cost.metal
|
||||
planet.resources.crystal -= cost.crystal
|
||||
planet.resources.deuterium -= cost.deuterium
|
||||
planet.resources.darkMatter -= cost.darkMatter
|
||||
|
||||
// 直接升级(NPC不需要等待研究时间)
|
||||
npc.technologies[techType] = targetLevel
|
||||
|
||||
// 每次只研究一个科技
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动建造NPC舰队
|
||||
*/
|
||||
export const autoBuildNPCFleet = (npc: NPC): void => {
|
||||
const planet = npc.planets[0]
|
||||
if (!planet) return
|
||||
|
||||
// 根据难度决定舰队组成
|
||||
const fleetComposition: { shipType: ShipType; priority: number }[] = [
|
||||
{ shipType: ShipType.LightFighter, priority: 1 },
|
||||
{ shipType: ShipType.HeavyFighter, priority: 2 },
|
||||
{ shipType: ShipType.Cruiser, priority: 3 },
|
||||
{ shipType: ShipType.Battleship, priority: 4 },
|
||||
{ shipType: ShipType.SmallCargo, priority: 5 },
|
||||
{ shipType: ShipType.LargeCargo, priority: 6 },
|
||||
{ shipType: ShipType.Recycler, priority: 7 },
|
||||
{ shipType: ShipType.EspionageProbe, priority: 8 }
|
||||
]
|
||||
|
||||
// 按优先级排序
|
||||
fleetComposition.sort((a, b) => a.priority - b.priority)
|
||||
|
||||
// 尝试建造舰船
|
||||
for (const { shipType } of fleetComposition) {
|
||||
const shipConfig = SHIPS[shipType]
|
||||
|
||||
// 检查建造需求
|
||||
const canBuild = Object.entries(shipConfig.requirements || {}).every(([reqType, reqLevel]) => {
|
||||
if (reqType in BuildingType) {
|
||||
return (planet.buildings[reqType as BuildingType] || 0) >= reqLevel
|
||||
}
|
||||
if (reqType in TechnologyType) {
|
||||
return (npc.technologies[reqType as TechnologyType] || 0) >= reqLevel
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
if (!canBuild) continue
|
||||
|
||||
// 根据难度和当前资源决定建造数量
|
||||
const maxAffordable = Math.floor(
|
||||
Math.min(
|
||||
planet.resources.metal / shipConfig.cost.metal,
|
||||
planet.resources.crystal / shipConfig.cost.crystal,
|
||||
planet.resources.deuterium / shipConfig.cost.deuterium,
|
||||
shipConfig.cost.darkMatter > 0 ? planet.resources.darkMatter / shipConfig.cost.darkMatter : Infinity
|
||||
)
|
||||
)
|
||||
|
||||
// 建造数量:简单1-5艘,中等5-10艘,困难10-20艘
|
||||
const buildCount = Math.min(maxAffordable, npc.difficulty === 'easy' ? 5 : npc.difficulty === 'medium' ? 10 : 20)
|
||||
|
||||
if (buildCount > 0) {
|
||||
// 扣除资源
|
||||
planet.resources.metal -= shipConfig.cost.metal * buildCount
|
||||
planet.resources.crystal -= shipConfig.cost.crystal * buildCount
|
||||
planet.resources.deuterium -= shipConfig.cost.deuterium * buildCount
|
||||
planet.resources.darkMatter -= shipConfig.cost.darkMatter * buildCount
|
||||
|
||||
// 添加舰船
|
||||
planet.fleet[shipType] = (planet.fleet[shipType] || 0) + buildCount
|
||||
|
||||
// 建造一批后退出
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 为NPC生成资源(模拟资源生产)
|
||||
*/
|
||||
export const generateNPCResources = (npc: NPC, deltaSeconds: number, config: DynamicDifficultyConfig): void => {
|
||||
const planet = npc.planets[0]
|
||||
if (!planet) return
|
||||
|
||||
// 基于建筑等级计算资源产量
|
||||
const metalMineLevel = planet.buildings[BuildingType.MetalMine] || 0
|
||||
const crystalMineLevel = planet.buildings[BuildingType.CrystalMine] || 0
|
||||
const deuteriumLevel = planet.buildings[BuildingType.DeuteriumSynthesizer] || 0
|
||||
const darkMatterLevel = planet.buildings[BuildingType.DarkMatterCollector] || 0
|
||||
|
||||
// 简化的资源产量计算(每秒产量)
|
||||
const metalProduction = 30 * metalMineLevel * Math.pow(1.1, metalMineLevel) * config.resourceGrowthRate
|
||||
const crystalProduction = 20 * crystalMineLevel * Math.pow(1.1, crystalMineLevel) * config.resourceGrowthRate
|
||||
const deuteriumProduction = 10 * deuteriumLevel * Math.pow(1.1, deuteriumLevel) * config.resourceGrowthRate
|
||||
const darkMatterProduction = ((25 * darkMatterLevel * Math.pow(1.5, darkMatterLevel)) / 3600) * config.resourceGrowthRate
|
||||
|
||||
// 增加资源
|
||||
planet.resources.metal += metalProduction * deltaSeconds
|
||||
planet.resources.crystal += crystalProduction * deltaSeconds
|
||||
planet.resources.deuterium += deuteriumProduction * deltaSeconds
|
||||
planet.resources.darkMatter += darkMatterProduction * deltaSeconds
|
||||
|
||||
// 确保不超过存储上限
|
||||
const metalStorage = planet.buildings[BuildingType.MetalStorage] || 0
|
||||
const crystalStorage = planet.buildings[BuildingType.CrystalStorage] || 0
|
||||
const deuteriumStorage = planet.buildings[BuildingType.DeuteriumTank] || 0
|
||||
const darkMatterStorage = planet.buildings[BuildingType.DarkMatterTank] || 0
|
||||
|
||||
planet.resources.metal = Math.min(planet.resources.metal, 10000 * Math.pow(2, metalStorage))
|
||||
planet.resources.crystal = Math.min(planet.resources.crystal, 10000 * Math.pow(2, crystalStorage))
|
||||
planet.resources.deuterium = Math.min(planet.resources.deuterium, 10000 * Math.pow(2, deuteriumStorage))
|
||||
planet.resources.darkMatter = Math.min(planet.resources.darkMatter, 1000 * Math.pow(2, darkMatterStorage))
|
||||
}
|
||||
|
||||
/**
|
||||
* 主NPC成长更新函数
|
||||
* 应该在游戏循环中定期调用
|
||||
*/
|
||||
export const updateNPCGrowth = (npc: NPC, gameState: NPCGrowthGameState, deltaSeconds: number): void => {
|
||||
// 使用动态难度(基于玩家积分)而不是固定难度
|
||||
const config = calculateDynamicDifficulty(gameState.player.points)
|
||||
|
||||
// 1. 持续生成资源
|
||||
generateNPCResources(npc, deltaSeconds, config)
|
||||
|
||||
// 2. 定期评估并调整实力(使用静态计数器或时间戳)
|
||||
const now = Date.now()
|
||||
const lastGrowthCheck = (npc as any).lastGrowthCheck || 0
|
||||
|
||||
if (now - lastGrowthCheck >= config.checkInterval * 1000) {
|
||||
;(npc as any).lastGrowthCheck = now
|
||||
|
||||
// 计算玩家平均实力
|
||||
const playerPower = calculatePlayerAveragePower(gameState)
|
||||
const npcPower = calculateNPCPower(npc)
|
||||
|
||||
// 计算目标实力
|
||||
const targetBuildingLevel = playerPower.avgBuildingLevel * config.powerRatio
|
||||
const targetTechLevel = playerPower.avgTechLevel * config.powerRatio
|
||||
const targetFleetPower = playerPower.totalFleetPower * config.powerRatio
|
||||
|
||||
// 3. 如果实力不足,进行升级
|
||||
if (npcPower.avgBuildingLevel < targetBuildingLevel) {
|
||||
autoUpgradeNPCBuildings(npc)
|
||||
}
|
||||
|
||||
if (npcPower.avgTechLevel < targetTechLevel) {
|
||||
autoResearchNPCTechnologies(npc)
|
||||
}
|
||||
|
||||
if (npcPower.totalFleetPower < targetFleetPower) {
|
||||
autoBuildNPCFleet(npc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化NPC时设置合理的起始实力
|
||||
*/
|
||||
export const initializeNPCStartingPower = (
|
||||
npc: NPC,
|
||||
playerPower: {
|
||||
avgBuildingLevel: number
|
||||
avgTechLevel: number
|
||||
totalFleetPower: number
|
||||
totalResources: number
|
||||
}
|
||||
): void => {
|
||||
const config = NPC_GROWTH_CONFIG[npc.difficulty]
|
||||
const planet = npc.planets[0]
|
||||
if (!planet) return
|
||||
|
||||
// 设置起始建筑等级(基于玩家实力)
|
||||
const targetBuildingLevel = Math.max(5, Math.floor(playerPower.avgBuildingLevel * config.powerRatio))
|
||||
|
||||
// 设置资源建筑
|
||||
planet.buildings[BuildingType.MetalMine] = targetBuildingLevel
|
||||
planet.buildings[BuildingType.CrystalMine] = Math.floor(targetBuildingLevel * 0.8)
|
||||
planet.buildings[BuildingType.DeuteriumSynthesizer] = Math.floor(targetBuildingLevel * 0.6)
|
||||
planet.buildings[BuildingType.SolarPlant] = targetBuildingLevel + 2
|
||||
|
||||
// 设置设施建筑
|
||||
planet.buildings[BuildingType.RoboticsFactory] = Math.floor(targetBuildingLevel * 0.5)
|
||||
planet.buildings[BuildingType.Shipyard] = Math.floor(targetBuildingLevel * 0.4)
|
||||
planet.buildings[BuildingType.ResearchLab] = Math.floor(targetBuildingLevel * 0.4)
|
||||
|
||||
// 设置仓储
|
||||
planet.buildings[BuildingType.MetalStorage] = Math.floor(targetBuildingLevel * 0.3)
|
||||
planet.buildings[BuildingType.CrystalStorage] = Math.floor(targetBuildingLevel * 0.3)
|
||||
planet.buildings[BuildingType.DeuteriumTank] = Math.floor(targetBuildingLevel * 0.3)
|
||||
|
||||
// 设置起始科技等级
|
||||
const targetTechLevel = Math.max(3, Math.floor(playerPower.avgTechLevel * config.powerRatio))
|
||||
|
||||
npc.technologies[TechnologyType.EnergyTechnology] = targetTechLevel
|
||||
npc.technologies[TechnologyType.ComputerTechnology] = Math.floor(targetTechLevel * 0.8)
|
||||
npc.technologies[TechnologyType.WeaponsTechnology] = Math.floor(targetTechLevel * 0.7)
|
||||
npc.technologies[TechnologyType.ShieldingTechnology] = Math.floor(targetTechLevel * 0.7)
|
||||
npc.technologies[TechnologyType.ArmourTechnology] = Math.floor(targetTechLevel * 0.7)
|
||||
npc.technologies[TechnologyType.CombustionDrive] = Math.floor(targetTechLevel * 0.6)
|
||||
|
||||
// 给予起始资源
|
||||
planet.resources.metal = 100000 * config.powerRatio
|
||||
planet.resources.crystal = 50000 * config.powerRatio
|
||||
planet.resources.deuterium = 20000 * config.powerRatio
|
||||
planet.resources.darkMatter = 1000 * config.powerRatio
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化NPC外交关系网络
|
||||
* 为每个NPC随机分配盟友和敌人
|
||||
*/
|
||||
export const initializeNPCDiplomacy = (npcs: NPC[]): void => {
|
||||
// 确保所有NPC都有空的外交字段
|
||||
npcs.forEach(npc => {
|
||||
if (!npc.allies) npc.allies = []
|
||||
if (!npc.enemies) npc.enemies = []
|
||||
if (!npc.relations) npc.relations = {}
|
||||
})
|
||||
|
||||
// 为每个NPC随机分配1-2个盟友
|
||||
npcs.forEach(npc => {
|
||||
// 获取还未建立关系的潜在盟友
|
||||
const potentialAllies = npcs.filter(
|
||||
n => n.id !== npc.id && !npc.allies!.includes(n.id) && !n.allies!.includes(npc.id)
|
||||
)
|
||||
|
||||
if (potentialAllies.length === 0) return
|
||||
|
||||
// 随机选择1-2个盟友
|
||||
const allyCount = Math.min(Math.floor(Math.random() * 2) + 1, potentialAllies.length)
|
||||
|
||||
for (let i = 0; i < allyCount; i++) {
|
||||
if (potentialAllies.length === 0) break
|
||||
|
||||
const allyIndex = Math.floor(Math.random() * potentialAllies.length)
|
||||
const ally = potentialAllies.splice(allyIndex, 1)[0]
|
||||
|
||||
if (!ally) continue
|
||||
|
||||
// 建立双向盟友关系
|
||||
if (!npc.allies!.includes(ally.id)) {
|
||||
npc.allies!.push(ally.id)
|
||||
}
|
||||
if (!ally.allies!.includes(npc.id)) {
|
||||
ally.allies!.push(npc.id)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -24,11 +24,15 @@ export const createInitialPlanet = (playerId: string, planetName: string = 'Home
|
||||
[ShipType.HeavyFighter]: 0,
|
||||
[ShipType.Cruiser]: 0,
|
||||
[ShipType.Battleship]: 0,
|
||||
[ShipType.Battlecruiser]: 0,
|
||||
[ShipType.Bomber]: 0,
|
||||
[ShipType.Destroyer]: 0,
|
||||
[ShipType.SmallCargo]: 0,
|
||||
[ShipType.LargeCargo]: 0,
|
||||
[ShipType.ColonyShip]: 0,
|
||||
[ShipType.Recycler]: 0,
|
||||
[ShipType.EspionageProbe]: 0,
|
||||
[ShipType.SolarSatellite]: 0,
|
||||
[ShipType.DarkMatterHarvester]: 0,
|
||||
[ShipType.Deathstar]: 0
|
||||
},
|
||||
@@ -41,6 +45,8 @@ export const createInitialPlanet = (playerId: string, planetName: string = 'Home
|
||||
[DefenseType.PlasmaTurret]: 0,
|
||||
[DefenseType.SmallShieldDome]: 0,
|
||||
[DefenseType.LargeShieldDome]: 0,
|
||||
[DefenseType.AntiBallisticMissile]: 0,
|
||||
[DefenseType.InterplanetaryMissile]: 0,
|
||||
[DefenseType.PlanetaryShield]: 0
|
||||
},
|
||||
buildQueue: [],
|
||||
@@ -84,11 +90,15 @@ export const createNPCPlanet = (
|
||||
[ShipType.HeavyFighter]: Math.floor(Math.random() * 20),
|
||||
[ShipType.Cruiser]: Math.floor(Math.random() * 10),
|
||||
[ShipType.Battleship]: Math.floor(Math.random() * 5),
|
||||
[ShipType.Battlecruiser]: Math.floor(Math.random() * 3),
|
||||
[ShipType.Bomber]: Math.floor(Math.random() * 2),
|
||||
[ShipType.Destroyer]: Math.floor(Math.random() * 2),
|
||||
[ShipType.SmallCargo]: Math.floor(Math.random() * 10),
|
||||
[ShipType.LargeCargo]: Math.floor(Math.random() * 5),
|
||||
[ShipType.ColonyShip]: 0,
|
||||
[ShipType.Recycler]: 0,
|
||||
[ShipType.EspionageProbe]: 0,
|
||||
[ShipType.SolarSatellite]: Math.floor(Math.random() * 20),
|
||||
[ShipType.DarkMatterHarvester]: 0,
|
||||
[ShipType.Deathstar]: 0
|
||||
},
|
||||
@@ -101,6 +111,8 @@ export const createNPCPlanet = (
|
||||
[DefenseType.PlasmaTurret]: Math.floor(Math.random() * 5),
|
||||
[DefenseType.SmallShieldDome]: Math.random() > 0.5 ? 1 : 0,
|
||||
[DefenseType.LargeShieldDome]: Math.random() > 0.8 ? 1 : 0,
|
||||
[DefenseType.AntiBallisticMissile]: Math.floor(Math.random() * 3),
|
||||
[DefenseType.InterplanetaryMissile]: Math.floor(Math.random() * 2),
|
||||
[DefenseType.PlanetaryShield]: 0
|
||||
},
|
||||
buildQueue: [],
|
||||
@@ -157,11 +169,15 @@ export const createMoon = (
|
||||
[ShipType.HeavyFighter]: 0,
|
||||
[ShipType.Cruiser]: 0,
|
||||
[ShipType.Battleship]: 0,
|
||||
[ShipType.Battlecruiser]: 0,
|
||||
[ShipType.Bomber]: 0,
|
||||
[ShipType.Destroyer]: 0,
|
||||
[ShipType.SmallCargo]: 0,
|
||||
[ShipType.LargeCargo]: 0,
|
||||
[ShipType.ColonyShip]: 0,
|
||||
[ShipType.Recycler]: 0,
|
||||
[ShipType.EspionageProbe]: 0,
|
||||
[ShipType.SolarSatellite]: 0,
|
||||
[ShipType.DarkMatterHarvester]: 0,
|
||||
[ShipType.Deathstar]: 0
|
||||
},
|
||||
@@ -174,6 +190,8 @@ export const createMoon = (
|
||||
[DefenseType.PlasmaTurret]: 0,
|
||||
[DefenseType.SmallShieldDome]: 0,
|
||||
[DefenseType.LargeShieldDome]: 0,
|
||||
[DefenseType.AntiBallisticMissile]: 0,
|
||||
[DefenseType.InterplanetaryMissile]: 0,
|
||||
[DefenseType.PlanetaryShield]: 0
|
||||
},
|
||||
buildQueue: [],
|
||||
|
||||
@@ -20,12 +20,31 @@ export const calculateTechnologyCost = (techType: TechnologyType, targetLevel: n
|
||||
|
||||
/**
|
||||
* 计算科技研究时间
|
||||
* @param techType 科技类型
|
||||
* @param currentLevel 当前等级
|
||||
* @param researchSpeedBonus 军官等提供的研究速度加成百分比
|
||||
* @param researchLabLevel 研究实验室等级
|
||||
* @param energyTechLevel 能源技术等级
|
||||
*/
|
||||
export const calculateTechnologyTime = (techType: TechnologyType, currentLevel: number, researchSpeedBonus: number = 0): number => {
|
||||
export const calculateTechnologyTime = (
|
||||
techType: TechnologyType,
|
||||
currentLevel: number,
|
||||
researchSpeedBonus: number = 0,
|
||||
researchLabLevel: number = 1,
|
||||
energyTechLevel: number = 0
|
||||
): number => {
|
||||
const config = TECHNOLOGIES[techType]
|
||||
const baseTime = config.baseTime * Math.pow(config.costMultiplier, currentLevel)
|
||||
|
||||
// 研究实验室和能源技术的加速:研究时间 / (研究实验室等级 × (1 + 能源技术等级))
|
||||
// 研究实验室等级至少为1,防止除以0
|
||||
const labLevel = Math.max(1, researchLabLevel)
|
||||
const techSpeedDivisor = labLevel * (1 + energyTechLevel)
|
||||
|
||||
// 军官等的百分比加成
|
||||
const speedMultiplier = 1 - researchSpeedBonus / 100
|
||||
return Math.floor(baseTime * speedMultiplier)
|
||||
|
||||
return Math.floor((baseTime / techSpeedDivisor) * speedMultiplier)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Planet, Resources, BuildQueueItem, Officer } from '@/types/game'
|
||||
import { TechnologyType, OfficerType } from '@/types/game'
|
||||
import { TechnologyType, OfficerType, BuildingType } from '@/types/game'
|
||||
import * as researchLogic from './researchLogic'
|
||||
import * as resourceLogic from './resourceLogic'
|
||||
import * as publicLogic from './publicLogic'
|
||||
@@ -47,14 +47,20 @@ export const executeTechnologyResearch = (
|
||||
planet: Planet,
|
||||
techType: TechnologyType,
|
||||
currentLevel: number,
|
||||
officers: Record<OfficerType, Officer>
|
||||
officers: Record<OfficerType, Officer>,
|
||||
technologies: Partial<Record<TechnologyType, number>>
|
||||
): { queueItem: BuildQueueItem } => {
|
||||
const targetLevel = currentLevel + 1
|
||||
const cost = researchLogic.calculateTechnologyCost(techType, targetLevel)
|
||||
|
||||
// 计算军官加成
|
||||
const bonuses = officerLogic.calculateActiveBonuses(officers, Date.now())
|
||||
const time = researchLogic.calculateTechnologyTime(techType, currentLevel, bonuses.researchSpeedBonus)
|
||||
|
||||
// 获取研究实验室等级和能源技术等级
|
||||
const researchLabLevel = planet.buildings[BuildingType.ResearchLab] || 1
|
||||
const energyTechLevel = technologies[TechnologyType.EnergyTechnology] || 0
|
||||
|
||||
const time = researchLogic.calculateTechnologyTime(techType, currentLevel, bonuses.researchSpeedBonus, researchLabLevel, energyTechLevel)
|
||||
|
||||
// 扣除资源
|
||||
resourceLogic.deductResources(planet.resources, cost)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { Planet, Resources } from '@/types/game'
|
||||
import { BuildingType } from '@/types/game'
|
||||
import type { Planet, Resources, Officer } from '@/types/game'
|
||||
import { BuildingType, OfficerType } from '@/types/game'
|
||||
import * as officerLogic from './officerLogic'
|
||||
import { OFFICERS } from '@/config/gameConfig'
|
||||
|
||||
/**
|
||||
* 计算电量产出
|
||||
@@ -11,10 +13,20 @@ export const calculateEnergyProduction = (
|
||||
}
|
||||
): number => {
|
||||
const solarPlantLevel = planet.buildings[BuildingType.SolarPlant] || 0
|
||||
const fusionReactorLevel = planet.buildings[BuildingType.FusionReactor] || 0
|
||||
const solarSatelliteCount = planet.fleet.solarSatellite || 0
|
||||
const energyBonus = 1 + (bonuses.energyProductionBonus || 0) / 100
|
||||
|
||||
// 太阳能电站每级产出:50 * 1.1^等级
|
||||
return solarPlantLevel * 50 * Math.pow(1.1, solarPlantLevel) * energyBonus
|
||||
const solarPlantProduction = solarPlantLevel * 50 * Math.pow(1.1, solarPlantLevel)
|
||||
|
||||
// 核聚变反应堆每级产出:150 * 1.15^等级(消耗重氢)
|
||||
const fusionReactorProduction = fusionReactorLevel * 150 * Math.pow(1.15, fusionReactorLevel)
|
||||
|
||||
// 太阳能卫星每个产出:50点能量
|
||||
const solarSatelliteProduction = solarSatelliteCount * 50
|
||||
|
||||
return (solarPlantProduction + fusionReactorProduction + solarSatelliteProduction) * energyBonus
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -76,17 +88,18 @@ export const calculateResourceCapacity = (planet: Planet, storageCapacityBonus:
|
||||
const metalStorageLevel = planet.buildings[BuildingType.MetalStorage] || 0
|
||||
const crystalStorageLevel = planet.buildings[BuildingType.CrystalStorage] || 0
|
||||
const deuteriumTankLevel = planet.buildings[BuildingType.DeuteriumTank] || 0
|
||||
const darkMatterCollectorLevel = planet.buildings[BuildingType.DarkMatterCollector] || 0
|
||||
const darkMatterTankLevel = planet.buildings[BuildingType.DarkMatterTank] || 0
|
||||
const solarPlantLevel = planet.buildings[BuildingType.SolarPlant] || 0
|
||||
|
||||
const bonus = 1 + (storageCapacityBonus || 0) / 100
|
||||
|
||||
const baseCapacity = 10000
|
||||
const darkMatterBaseCapacity = 1000 // 暗物质基础容量较小
|
||||
return {
|
||||
metal: baseCapacity * Math.pow(2, metalStorageLevel) * bonus,
|
||||
crystal: baseCapacity * Math.pow(2, crystalStorageLevel) * bonus,
|
||||
deuterium: baseCapacity * Math.pow(2, deuteriumTankLevel) * bonus,
|
||||
darkMatter: 1000 + darkMatterCollectorLevel * 100, // 暗物质容量较小
|
||||
darkMatter: darkMatterBaseCapacity * Math.pow(2, darkMatterTankLevel) * bonus,
|
||||
energy: 1000 + solarPlantLevel * 500 // 能量容量基于太阳能电站等级
|
||||
}
|
||||
}
|
||||
@@ -194,12 +207,20 @@ export interface ProductionDetail {
|
||||
buildingName: string // 建筑名称(用于显示)
|
||||
bonuses: ProductionBonus[] // 加成列表
|
||||
finalProduction: number // 最终产量
|
||||
sources?: ProductionSource[] // 多个产量来源(用于能量)
|
||||
}
|
||||
|
||||
export interface ProductionSource {
|
||||
name: string // 来源名称
|
||||
level: number // 等级或数量
|
||||
production: number // 产量
|
||||
}
|
||||
|
||||
export interface ProductionBonus {
|
||||
name: string // 加成名称
|
||||
value: number // 加成百分比或固定值
|
||||
type: 'percentage' | 'multiplier' // 百分比加成或倍率
|
||||
percentage: number // 加成百分比
|
||||
value: number // 实际增加的产量
|
||||
source: 'technology' | 'officer' | 'other' // 加成来源类型
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -223,11 +244,8 @@ export interface ConsumptionDetail {
|
||||
*/
|
||||
export const calculateProductionBreakdown = (
|
||||
planet: Planet,
|
||||
bonuses: {
|
||||
resourceProductionBonus: number
|
||||
darkMatterProductionBonus: number
|
||||
energyProductionBonus: number
|
||||
}
|
||||
officers: Record<OfficerType, Officer>,
|
||||
currentTime: number
|
||||
): ProductionBreakdown => {
|
||||
const metalMineLevel = planet.buildings[BuildingType.MetalMine] || 0
|
||||
const crystalMineLevel = planet.buildings[BuildingType.CrystalMine] || 0
|
||||
@@ -238,86 +256,180 @@ export const calculateProductionBreakdown = (
|
||||
const hasEnergy = planet.resources.energy > 0
|
||||
const productionEfficiency = hasEnergy ? 1 : 0
|
||||
|
||||
// 收集每个军官的加成信息
|
||||
const activeOfficerBonuses: Array<{
|
||||
type: OfficerType
|
||||
name: string
|
||||
resourceBonus: number
|
||||
darkMatterBonus: number
|
||||
energyBonus: number
|
||||
}> = []
|
||||
|
||||
Object.values(officers).forEach(officer => {
|
||||
if (officerLogic.isOfficerActive(officer, currentTime)) {
|
||||
const config = OFFICERS[officer.type]
|
||||
activeOfficerBonuses.push({
|
||||
type: officer.type,
|
||||
name: config.name,
|
||||
resourceBonus: config.benefits.resourceProductionBonus || 0,
|
||||
darkMatterBonus: config.benefits.darkMatterProductionBonus || 0,
|
||||
energyBonus: config.benefits.energyProductionBonus || 0
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 计算总加成
|
||||
const totalResourceBonus = activeOfficerBonuses.reduce((sum, officer) => sum + officer.resourceBonus, 0)
|
||||
const totalDarkMatterBonus = activeOfficerBonuses.reduce((sum, officer) => sum + officer.darkMatterBonus, 0)
|
||||
const totalEnergyBonus = activeOfficerBonuses.reduce((sum, officer) => sum + officer.energyBonus, 0)
|
||||
|
||||
// 金属矿产量
|
||||
const metalBase = metalMineLevel * 1500 * Math.pow(1.5, metalMineLevel)
|
||||
const metalBonuses: ProductionBonus[] = []
|
||||
if (bonuses.resourceProductionBonus > 0) {
|
||||
metalBonuses.push({
|
||||
name: 'officers.resourceBonus',
|
||||
value: bonuses.resourceProductionBonus,
|
||||
type: 'percentage'
|
||||
})
|
||||
}
|
||||
|
||||
// 为每个激活的军官添加加成项
|
||||
activeOfficerBonuses.forEach(officer => {
|
||||
if (officer.resourceBonus > 0) {
|
||||
const bonusValue = metalBase * (officer.resourceBonus / 100)
|
||||
metalBonuses.push({
|
||||
name: `officers.${officer.type}`,
|
||||
percentage: officer.resourceBonus,
|
||||
value: bonusValue,
|
||||
source: 'officer'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
if (!hasEnergy) {
|
||||
metalBonuses.push({
|
||||
name: 'resources.noEnergy',
|
||||
value: -100,
|
||||
type: 'percentage'
|
||||
percentage: -100,
|
||||
value: -metalBase * (1 + totalResourceBonus / 100),
|
||||
source: 'other'
|
||||
})
|
||||
}
|
||||
const metalFinal = metalBase * (1 + bonuses.resourceProductionBonus / 100) * productionEfficiency
|
||||
|
||||
const metalFinal = metalBase * (1 + totalResourceBonus / 100) * productionEfficiency
|
||||
|
||||
// 晶体矿产量
|
||||
const crystalBase = crystalMineLevel * 1000 * Math.pow(1.5, crystalMineLevel)
|
||||
const crystalBonuses: ProductionBonus[] = []
|
||||
if (bonuses.resourceProductionBonus > 0) {
|
||||
crystalBonuses.push({
|
||||
name: 'officers.resourceBonus',
|
||||
value: bonuses.resourceProductionBonus,
|
||||
type: 'percentage'
|
||||
})
|
||||
}
|
||||
|
||||
activeOfficerBonuses.forEach(officer => {
|
||||
if (officer.resourceBonus > 0) {
|
||||
const bonusValue = crystalBase * (officer.resourceBonus / 100)
|
||||
crystalBonuses.push({
|
||||
name: `officers.${officer.type}`,
|
||||
percentage: officer.resourceBonus,
|
||||
value: bonusValue,
|
||||
source: 'officer'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
if (!hasEnergy) {
|
||||
crystalBonuses.push({
|
||||
name: 'resources.noEnergy',
|
||||
value: -100,
|
||||
type: 'percentage'
|
||||
percentage: -100,
|
||||
value: -crystalBase * (1 + totalResourceBonus / 100),
|
||||
source: 'other'
|
||||
})
|
||||
}
|
||||
const crystalFinal = crystalBase * (1 + bonuses.resourceProductionBonus / 100) * productionEfficiency
|
||||
|
||||
const crystalFinal = crystalBase * (1 + totalResourceBonus / 100) * productionEfficiency
|
||||
|
||||
// 重氢合成器产量
|
||||
const deuteriumBase = deuteriumSynthesizerLevel * 500 * Math.pow(1.5, deuteriumSynthesizerLevel)
|
||||
const deuteriumBonuses: ProductionBonus[] = []
|
||||
if (bonuses.resourceProductionBonus > 0) {
|
||||
deuteriumBonuses.push({
|
||||
name: 'officers.resourceBonus',
|
||||
value: bonuses.resourceProductionBonus,
|
||||
type: 'percentage'
|
||||
})
|
||||
}
|
||||
|
||||
activeOfficerBonuses.forEach(officer => {
|
||||
if (officer.resourceBonus > 0) {
|
||||
const bonusValue = deuteriumBase * (officer.resourceBonus / 100)
|
||||
deuteriumBonuses.push({
|
||||
name: `officers.${officer.type}`,
|
||||
percentage: officer.resourceBonus,
|
||||
value: bonusValue,
|
||||
source: 'officer'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
if (!hasEnergy) {
|
||||
deuteriumBonuses.push({
|
||||
name: 'resources.noEnergy',
|
||||
value: -100,
|
||||
type: 'percentage'
|
||||
percentage: -100,
|
||||
value: -deuteriumBase * (1 + totalResourceBonus / 100),
|
||||
source: 'other'
|
||||
})
|
||||
}
|
||||
const deuteriumFinal = deuteriumBase * (1 + bonuses.resourceProductionBonus / 100) * productionEfficiency
|
||||
|
||||
const deuteriumFinal = deuteriumBase * (1 + totalResourceBonus / 100) * productionEfficiency
|
||||
|
||||
// 暗物质收集器产量
|
||||
const darkMatterBase = darkMatterCollectorLevel * 25 * Math.pow(1.5, darkMatterCollectorLevel)
|
||||
const darkMatterBonuses: ProductionBonus[] = []
|
||||
if (bonuses.darkMatterProductionBonus > 0) {
|
||||
darkMatterBonuses.push({
|
||||
name: 'officers.darkMatterBonus',
|
||||
value: bonuses.darkMatterProductionBonus,
|
||||
type: 'percentage'
|
||||
})
|
||||
}
|
||||
const darkMatterFinal = darkMatterBase * (1 + bonuses.darkMatterProductionBonus / 100)
|
||||
|
||||
// 太阳能电站产量
|
||||
const energyBase = solarPlantLevel * 50 * Math.pow(1.1, solarPlantLevel)
|
||||
const energyBonuses: ProductionBonus[] = []
|
||||
if (bonuses.energyProductionBonus > 0) {
|
||||
energyBonuses.push({
|
||||
name: 'officers.energyBonus',
|
||||
value: bonuses.energyProductionBonus,
|
||||
type: 'percentage'
|
||||
activeOfficerBonuses.forEach(officer => {
|
||||
if (officer.darkMatterBonus > 0) {
|
||||
const bonusValue = darkMatterBase * (officer.darkMatterBonus / 100)
|
||||
darkMatterBonuses.push({
|
||||
name: `officers.${officer.type}`,
|
||||
percentage: officer.darkMatterBonus,
|
||||
value: bonusValue,
|
||||
source: 'officer'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const darkMatterFinal = darkMatterBase * (1 + totalDarkMatterBonus / 100)
|
||||
|
||||
// 能量产量(包含多个来源)
|
||||
const fusionReactorLevel = planet.buildings[BuildingType.FusionReactor] || 0
|
||||
const solarSatelliteCount = planet.fleet.solarSatellite || 0
|
||||
|
||||
const solarPlantProduction = solarPlantLevel * 50 * Math.pow(1.1, solarPlantLevel)
|
||||
const fusionReactorProduction = fusionReactorLevel * 150 * Math.pow(1.15, fusionReactorLevel)
|
||||
const solarSatelliteProduction = solarSatelliteCount * 50
|
||||
|
||||
const energyBase = solarPlantProduction + fusionReactorProduction + solarSatelliteProduction
|
||||
|
||||
const energySources: ProductionSource[] = []
|
||||
if (solarPlantLevel > 0) {
|
||||
energySources.push({
|
||||
name: 'buildings.solarPlant',
|
||||
level: solarPlantLevel,
|
||||
production: solarPlantProduction
|
||||
})
|
||||
}
|
||||
const energyFinal = energyBase * (1 + bonuses.energyProductionBonus / 100)
|
||||
if (fusionReactorLevel > 0) {
|
||||
energySources.push({
|
||||
name: 'buildings.fusionReactor',
|
||||
level: fusionReactorLevel,
|
||||
production: fusionReactorProduction
|
||||
})
|
||||
}
|
||||
if (solarSatelliteCount > 0) {
|
||||
energySources.push({
|
||||
name: 'ships.solarSatellite',
|
||||
level: solarSatelliteCount,
|
||||
production: solarSatelliteProduction
|
||||
})
|
||||
}
|
||||
|
||||
const energyBonuses: ProductionBonus[] = []
|
||||
activeOfficerBonuses.forEach(officer => {
|
||||
if (officer.energyBonus > 0) {
|
||||
const bonusValue = energyBase * (officer.energyBonus / 100)
|
||||
energyBonuses.push({
|
||||
name: `officers.${officer.type}`,
|
||||
percentage: officer.energyBonus,
|
||||
value: bonusValue,
|
||||
source: 'officer'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const energyFinal = energyBase * (1 + totalEnergyBonus / 100)
|
||||
|
||||
return {
|
||||
metal: {
|
||||
@@ -353,7 +465,8 @@ export const calculateProductionBreakdown = (
|
||||
buildingLevel: solarPlantLevel,
|
||||
buildingName: 'buildings.solarPlant',
|
||||
bonuses: energyBonuses,
|
||||
finalProduction: energyFinal
|
||||
finalProduction: energyFinal,
|
||||
sources: energySources
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,22 +32,56 @@ export const calculateDefenseCost = (defenseType: DefenseType, quantity: number)
|
||||
|
||||
/**
|
||||
* 计算舰船建造时间
|
||||
* @param shipType 舰船类型
|
||||
* @param quantity 数量
|
||||
* @param buildingSpeedBonus 指挥官等提供的速度加成百分比
|
||||
* @param roboticsFactoryLevel 机器人工厂等级
|
||||
* @param naniteFactoryLevel 纳米工厂等级
|
||||
*/
|
||||
export const calculateShipBuildTime = (shipType: ShipType, quantity: number, buildingSpeedBonus: number = 0): number => {
|
||||
export const calculateShipBuildTime = (
|
||||
shipType: ShipType,
|
||||
quantity: number,
|
||||
buildingSpeedBonus: number = 0,
|
||||
roboticsFactoryLevel: number = 0,
|
||||
naniteFactoryLevel: number = 0
|
||||
): number => {
|
||||
const config = SHIPS[shipType]
|
||||
const baseTime = config.buildTime * quantity
|
||||
|
||||
// 机器人工厂和纳米工厂的加速:建造时间 / (1 + 机器人工厂等级 + 纳米工厂等级 × 2)
|
||||
const factorySpeedDivisor = 1 + roboticsFactoryLevel + naniteFactoryLevel * 2
|
||||
|
||||
// 指挥官等的百分比加成
|
||||
const speedMultiplier = 1 - buildingSpeedBonus / 100
|
||||
return Math.floor(baseTime * speedMultiplier)
|
||||
|
||||
return Math.floor((baseTime / factorySpeedDivisor) * speedMultiplier)
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算防御设施建造时间
|
||||
* @param defenseType 防御类型
|
||||
* @param quantity 数量
|
||||
* @param buildingSpeedBonus 指挥官等提供的速度加成百分比
|
||||
* @param roboticsFactoryLevel 机器人工厂等级
|
||||
* @param naniteFactoryLevel 纳米工厂等级
|
||||
*/
|
||||
export const calculateDefenseBuildTime = (defenseType: DefenseType, quantity: number, buildingSpeedBonus: number = 0): number => {
|
||||
export const calculateDefenseBuildTime = (
|
||||
defenseType: DefenseType,
|
||||
quantity: number,
|
||||
buildingSpeedBonus: number = 0,
|
||||
roboticsFactoryLevel: number = 0,
|
||||
naniteFactoryLevel: number = 0
|
||||
): number => {
|
||||
const config = DEFENSES[defenseType]
|
||||
const baseTime = config.buildTime * quantity
|
||||
|
||||
// 机器人工厂和纳米工厂的加速:建造时间 / (1 + 机器人工厂等级 + 纳米工厂等级 × 2)
|
||||
const factorySpeedDivisor = 1 + roboticsFactoryLevel + naniteFactoryLevel * 2
|
||||
|
||||
// 指挥官等的百分比加成
|
||||
const speedMultiplier = 1 - buildingSpeedBonus / 100
|
||||
return Math.floor(baseTime * speedMultiplier)
|
||||
|
||||
return Math.floor((baseTime / factorySpeedDivisor) * speedMultiplier)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -168,11 +202,7 @@ export const checkFleetAvailable = (currentFleet: Partial<Fleet>, requiredFleet:
|
||||
* @param cargo 携带的货物(可选)
|
||||
* @returns 总燃料消耗(重氢)
|
||||
*/
|
||||
export const calculateFleetFuelConsumption = (
|
||||
fleet: Partial<Fleet>,
|
||||
fuelConsumptionReduction: number = 0,
|
||||
cargo?: Resources
|
||||
): number => {
|
||||
export const calculateFleetFuelConsumption = (fleet: Partial<Fleet>, fuelConsumptionReduction: number = 0, cargo?: Resources): number => {
|
||||
// 计算舰船基础燃料消耗
|
||||
let baseFuelNeeded = 0
|
||||
for (const [shipType, count] of Object.entries(fleet)) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Planet, Resources, BuildQueueItem, Fleet, Officer } from '@/types/game'
|
||||
import { ShipType, DefenseType, TechnologyType, OfficerType } from '@/types/game'
|
||||
import { ShipType, DefenseType, TechnologyType, OfficerType, BuildingType } from '@/types/game'
|
||||
import * as shipLogic from './shipLogic'
|
||||
import * as resourceLogic from './resourceLogic'
|
||||
import * as officerLogic from './officerLogic'
|
||||
@@ -51,7 +51,18 @@ export const executeShipBuild = (
|
||||
|
||||
// 计算军官加成
|
||||
const bonuses = officerLogic.calculateActiveBonuses(officers, Date.now())
|
||||
const buildTime = shipLogic.calculateShipBuildTime(shipType, quantity, bonuses.buildingSpeedBonus)
|
||||
|
||||
// 获取机器人工厂和纳米工厂等级
|
||||
const roboticsFactoryLevel = planet.buildings[BuildingType.RoboticsFactory] || 0
|
||||
const naniteFactoryLevel = planet.buildings[BuildingType.NaniteFactory] || 0
|
||||
|
||||
const buildTime = shipLogic.calculateShipBuildTime(
|
||||
shipType,
|
||||
quantity,
|
||||
bonuses.buildingSpeedBonus,
|
||||
roboticsFactoryLevel,
|
||||
naniteFactoryLevel
|
||||
)
|
||||
|
||||
// 扣除资源
|
||||
resourceLogic.deductResources(planet.resources, totalCost)
|
||||
@@ -105,7 +116,18 @@ export const executeDefenseBuild = (
|
||||
|
||||
// 计算军官加成
|
||||
const bonuses = officerLogic.calculateActiveBonuses(officers, Date.now())
|
||||
const buildTime = shipLogic.calculateDefenseBuildTime(defenseType, quantity, bonuses.buildingSpeedBonus)
|
||||
|
||||
// 获取机器人工厂和纳米工厂等级
|
||||
const roboticsFactoryLevel = planet.buildings[BuildingType.RoboticsFactory] || 0
|
||||
const naniteFactoryLevel = planet.buildings[BuildingType.NaniteFactory] || 0
|
||||
|
||||
const buildTime = shipLogic.calculateDefenseBuildTime(
|
||||
defenseType,
|
||||
quantity,
|
||||
bonuses.buildingSpeedBonus,
|
||||
roboticsFactoryLevel,
|
||||
naniteFactoryLevel
|
||||
)
|
||||
|
||||
// 扣除资源
|
||||
resourceLogic.deductResources(planet.resources, totalCost)
|
||||
|
||||
@@ -13,6 +13,7 @@ const router = createRouter({
|
||||
{ path: '/battle-simulator', name: 'battle-simulator', component: () => import('@/views/BattleSimulatorView.vue') },
|
||||
{ path: '/messages', name: 'messages', component: () => import('@/views/MessagesView.vue') },
|
||||
{ path: '/galaxy', name: 'galaxy', component: () => import('@/views/GalaxyView.vue') },
|
||||
{ path: '/diplomacy', name: 'diplomacy', component: () => import('@/views/DiplomacyView.vue') },
|
||||
{ path: '/settings', name: 'settings', component: () => import('@/views/SettingsView.vue') },
|
||||
{
|
||||
path: '/gm',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import type { Planet, Player, BuildQueueItem, FleetMission, BattleResult, SpyReport, Officer } from '@/types/game'
|
||||
import type { Planet, Player, BuildQueueItem, FleetMission, BattleResult, SpyReport, Officer, SpiedNotification, NPCActivityNotification, IncomingFleetAlert } from '@/types/game'
|
||||
import { TechnologyType, OfficerType } from '@/types/game'
|
||||
import type { Locale } from '@/locales'
|
||||
import pkg from '../../package.json'
|
||||
@@ -18,7 +18,14 @@ export const useGameStore = defineStore('game', {
|
||||
researchQueue: [] as BuildQueueItem[],
|
||||
fleetMissions: [] as FleetMission[],
|
||||
battleReports: [] as BattleResult[],
|
||||
spyReports: [] as SpyReport[]
|
||||
spyReports: [] as SpyReport[],
|
||||
spiedNotifications: [] as SpiedNotification[],
|
||||
npcActivityNotifications: [] as NPCActivityNotification[],
|
||||
missionReports: [],
|
||||
incomingFleetAlerts: [] as IncomingFleetAlert[],
|
||||
giftNotifications: [],
|
||||
giftRejectedNotifications: [],
|
||||
points: 0
|
||||
} as Player,
|
||||
currentPlanetId: '',
|
||||
isDark: '',
|
||||
|
||||
23
src/stores/npcStore.ts
Normal file
23
src/stores/npcStore.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import type { NPC } from '@/types/game'
|
||||
import pkg from '../../package.json'
|
||||
import { encryptData, decryptData } from '@/utils/crypto'
|
||||
|
||||
/**
|
||||
* NPC Store
|
||||
* 存储和管理所有NPC数据
|
||||
*/
|
||||
export const useNPCStore = defineStore('npc', {
|
||||
state: () => ({
|
||||
npcs: [] as NPC[],
|
||||
lastGrowthCheck: {} as Record<string, number> // npcId -> timestamp
|
||||
}),
|
||||
persist: {
|
||||
key: `${pkg.name}-npcs`,
|
||||
storage: localStorage,
|
||||
serializer: {
|
||||
serialize: state => encryptData(state),
|
||||
deserialize: value => decryptData(value)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,3 +1,10 @@
|
||||
// 位置类型
|
||||
export interface Position {
|
||||
galaxy: number
|
||||
system: number
|
||||
position: number
|
||||
}
|
||||
|
||||
// 资源类型
|
||||
export interface Resources {
|
||||
metal: number
|
||||
@@ -13,6 +20,7 @@ export const BuildingType = {
|
||||
CrystalMine: 'crystalMine',
|
||||
DeuteriumSynthesizer: 'deuteriumSynthesizer',
|
||||
SolarPlant: 'solarPlant',
|
||||
FusionReactor: 'fusionReactor', // 核聚变反应堆
|
||||
RoboticsFactory: 'roboticsFactory',
|
||||
NaniteFactory: 'naniteFactory', // 纳米工厂
|
||||
Shipyard: 'shipyard',
|
||||
@@ -21,6 +29,8 @@ export const BuildingType = {
|
||||
CrystalStorage: 'crystalStorage',
|
||||
DeuteriumTank: 'deuteriumTank',
|
||||
DarkMatterCollector: 'darkMatterCollector', // 暗物质收集器
|
||||
DarkMatterTank: 'darkMatterTank', // 暗物质储罐
|
||||
MissileSilo: 'missileSilo', // 导弹发射井
|
||||
Terraformer: 'terraformer', // 地形改造器
|
||||
// 月球专属建筑
|
||||
LunarBase: 'lunarBase', // 月球基地
|
||||
@@ -63,9 +73,15 @@ export const TechnologyType = {
|
||||
HyperspaceTechnology: 'hyperspaceTechnology',
|
||||
PlasmaTechnology: 'plasmaTechnology',
|
||||
ComputerTechnology: 'computerTechnology', // 计算机技术
|
||||
EspionageTechnology: 'espionageTechnology', // 间谍技术
|
||||
CombustionDrive: 'combustionDrive',
|
||||
ImpulseDrive: 'impulseDrive',
|
||||
HyperspaceDrive: 'hyperspaceDrive',
|
||||
WeaponsTechnology: 'weaponsTechnology', // 武器技术
|
||||
ShieldingTechnology: 'shieldingTechnology', // 护盾技术
|
||||
ArmourTechnology: 'armourTechnology', // 装甲技术
|
||||
Astrophysics: 'astrophysics', // 天体物理学
|
||||
GravitonTechnology: 'gravitonTechnology', // 引力技术
|
||||
DarkMatterTechnology: 'darkMatterTechnology', // 暗物质技术
|
||||
TerraformingTechnology: 'terraformingTechnology', // 地形改造技术
|
||||
PlanetDestructionTech: 'planetDestructionTech' // 行星毁灭技术
|
||||
@@ -103,6 +119,8 @@ export const DefenseType = {
|
||||
PlasmaTurret: 'plasmaTurret',
|
||||
SmallShieldDome: 'smallShieldDome',
|
||||
LargeShieldDome: 'largeShieldDome',
|
||||
AntiBallisticMissile: 'antiBallisticMissile', // 反弹道导弹
|
||||
InterplanetaryMissile: 'interplanetaryMissile', // 星际导弹
|
||||
PlanetaryShield: 'planetaryShield' // 行星护盾
|
||||
} as const
|
||||
|
||||
@@ -127,11 +145,15 @@ export const ShipType = {
|
||||
HeavyFighter: 'heavyFighter',
|
||||
Cruiser: 'cruiser',
|
||||
Battleship: 'battleship',
|
||||
Battlecruiser: 'battlecruiser', // 战列巡洋舰
|
||||
Bomber: 'bomber', // 轰炸机
|
||||
Destroyer: 'destroyer', // 驱逐舰
|
||||
SmallCargo: 'smallCargo',
|
||||
LargeCargo: 'largeCargo',
|
||||
ColonyShip: 'colonyShip',
|
||||
Recycler: 'recycler',
|
||||
EspionageProbe: 'espionageProbe',
|
||||
SolarSatellite: 'solarSatellite', // 太阳能卫星
|
||||
DarkMatterHarvester: 'darkMatterHarvester', // 暗物质采集船
|
||||
Deathstar: 'deathstar' // 死星
|
||||
} as const
|
||||
@@ -161,11 +183,15 @@ export interface Fleet {
|
||||
[ShipType.HeavyFighter]: number
|
||||
[ShipType.Cruiser]: number
|
||||
[ShipType.Battleship]: number
|
||||
[ShipType.Battlecruiser]: number
|
||||
[ShipType.Bomber]: number
|
||||
[ShipType.Destroyer]: number
|
||||
[ShipType.SmallCargo]: number
|
||||
[ShipType.LargeCargo]: number
|
||||
[ShipType.ColonyShip]: number
|
||||
[ShipType.Recycler]: number
|
||||
[ShipType.EspionageProbe]: number
|
||||
[ShipType.SolarSatellite]: number
|
||||
[ShipType.DarkMatterHarvester]: number
|
||||
[ShipType.Deathstar]: number
|
||||
}
|
||||
@@ -185,13 +211,67 @@ export const MissionType = {
|
||||
|
||||
export type MissionType = (typeof MissionType)[keyof typeof MissionType]
|
||||
|
||||
// 外交关系状态
|
||||
export const RelationStatus = {
|
||||
Hostile: 'hostile', // 敌对
|
||||
Neutral: 'neutral', // 中立
|
||||
Friendly: 'friendly' // 友好
|
||||
} as const
|
||||
|
||||
export type RelationStatus = (typeof RelationStatus)[keyof typeof RelationStatus]
|
||||
|
||||
// 外交事件类型
|
||||
export const DiplomaticEventType = {
|
||||
GiftResources: 'giftResources', // 赠送资源
|
||||
Attack: 'attack', // 攻击
|
||||
Spy: 'spy', // 侦查
|
||||
StealDebris: 'stealDebris', // 抢夺残骸
|
||||
AllyAttacked: 'allyAttacked' // 盟友被攻击
|
||||
} as const
|
||||
|
||||
export type DiplomaticEventType = (typeof DiplomaticEventType)[keyof typeof DiplomaticEventType]
|
||||
|
||||
// 外交关系
|
||||
export interface DiplomaticRelation {
|
||||
fromId: string // 关系发起方(玩家或NPC ID)
|
||||
toId: string // 关系接收方(玩家或NPC ID)
|
||||
reputation: number // 好感度值 (-100 到 +100)
|
||||
status: RelationStatus // 关系状态
|
||||
lastUpdated: number // 最后更新时间戳
|
||||
history?: Array<{
|
||||
// 关系变化历史
|
||||
timestamp: number
|
||||
change: number
|
||||
reason: DiplomaticEventType
|
||||
details?: string
|
||||
}>
|
||||
}
|
||||
|
||||
// 外交报告(显示好感度变化通知)
|
||||
export interface DiplomaticReport {
|
||||
id: string
|
||||
timestamp: number
|
||||
npcId: string // NPC ID
|
||||
npcName: string // NPC名称
|
||||
eventType: DiplomaticEventType // 事件类型
|
||||
reputationChange: number // 好感度变化值
|
||||
newReputation: number // 新的好感度值
|
||||
oldStatus: RelationStatus // 旧的关系状态
|
||||
newStatus: RelationStatus // 新的关系状态
|
||||
message: string // 消息内容
|
||||
read?: boolean // 已读状态
|
||||
}
|
||||
|
||||
// 舰队任务
|
||||
export interface FleetMission {
|
||||
id: string
|
||||
playerId: string
|
||||
playerId: string // 玩家ID,如果是NPC任务则为NPC ID
|
||||
npcId?: string // NPC ID(如果是NPC发起的任务)
|
||||
isHostile?: boolean // 是否是敌对任务(用于警告显示)
|
||||
originPlanetId: string
|
||||
targetPosition: { galaxy: number; system: number; position: number }
|
||||
targetPlanetId?: string
|
||||
debrisFieldId?: string // 残骸场ID(用于回收任务)
|
||||
missionType: MissionType
|
||||
fleet: Partial<Fleet>
|
||||
cargo: Resources
|
||||
@@ -199,6 +279,9 @@ export interface FleetMission {
|
||||
arrivalTime: number
|
||||
returnTime?: number
|
||||
status: 'outbound' | 'returning' | 'arrived'
|
||||
// 外交系统字段
|
||||
isGift?: boolean // 是否为赠送资源任务
|
||||
giftTargetNpcId?: string // 赠送目标NPC ID
|
||||
}
|
||||
|
||||
// 战斗结果
|
||||
@@ -247,6 +330,8 @@ export interface SpyReport {
|
||||
timestamp: number
|
||||
spyId: string
|
||||
targetPlanetId: string
|
||||
targetPlanetName: string // 目标星球名称
|
||||
targetPosition: Position // 目标星球坐标
|
||||
targetPlayerId: string
|
||||
resources: Resources
|
||||
fleet?: Partial<Fleet>
|
||||
@@ -257,6 +342,100 @@ export interface SpyReport {
|
||||
read?: boolean // 已读状态
|
||||
}
|
||||
|
||||
// 被侦查通知(玩家被NPC侦查时收到)
|
||||
export interface SpiedNotification {
|
||||
id: string
|
||||
timestamp: number
|
||||
npcId: string // 侦查者NPC ID
|
||||
npcName: string // 侦查者NPC名称
|
||||
targetPlanetId: string // 被侦查的星球ID
|
||||
targetPlanetName: string // 被侦查的星球名称
|
||||
detectionSuccess: boolean // 是否被发现
|
||||
read?: boolean
|
||||
}
|
||||
|
||||
// NPC活动通知(回收残骸等)
|
||||
export interface NPCActivityNotification {
|
||||
id: string
|
||||
timestamp: number
|
||||
npcId: string // NPC ID
|
||||
npcName: string // NPC名称
|
||||
activityType: 'recycle' // 活动类型
|
||||
targetPosition: Position // 目标位置
|
||||
targetPlanetId?: string // 目标星球ID(如果在玩家星球位置)
|
||||
targetPlanetName?: string // 目标星球名称
|
||||
arrivalTime: number // 到达时间
|
||||
read?: boolean
|
||||
}
|
||||
|
||||
// 即将到来的敌对舰队警告
|
||||
export interface IncomingFleetAlert {
|
||||
id: string // 对应的FleetMission ID
|
||||
npcId: string // NPC ID
|
||||
npcName: string // NPC名称
|
||||
missionType: MissionType // 任务类型(spy/attack)
|
||||
targetPlanetId: string // 目标星球ID
|
||||
targetPlanetName: string // 目标星球名称
|
||||
arrivalTime: number // 到达时间
|
||||
fleetSize: number // 舰队总数(用于显示规模)
|
||||
read?: boolean
|
||||
}
|
||||
|
||||
// 任务报告(运输、殖民、回收、部署、毁灭等任务的结果通知)
|
||||
export interface MissionReport {
|
||||
id: string
|
||||
timestamp: number
|
||||
missionType: MissionType
|
||||
originPlanetId: string
|
||||
originPlanetName: string
|
||||
targetPosition: { galaxy: number; system: number; position: number }
|
||||
targetPlanetId?: string
|
||||
targetPlanetName?: string
|
||||
success: boolean // 任务是否成功
|
||||
message: string // 任务结果描述
|
||||
// 任务特定的详细信息
|
||||
details?: {
|
||||
// 运输任务:运输的资源
|
||||
transportedResources?: Resources
|
||||
// 殖民任务:新星球信息
|
||||
newPlanetId?: string
|
||||
newPlanetName?: string
|
||||
// 回收任务:回收的资源
|
||||
recycledResources?: Pick<Resources, 'metal' | 'crystal'>
|
||||
remainingDebris?: Pick<Resources, 'metal' | 'crystal'>
|
||||
// 毁灭任务:摧毁的星球
|
||||
destroyedPlanetName?: string
|
||||
// 部署任务:部署的舰队
|
||||
deployedFleet?: Partial<Fleet>
|
||||
}
|
||||
read?: boolean
|
||||
}
|
||||
|
||||
// 礼物通知(NPC赠送礼物,等待玩家接受或拒绝)
|
||||
export interface GiftNotification {
|
||||
id: string
|
||||
timestamp: number
|
||||
fromNpcId: string // 赠送方NPC ID
|
||||
fromNpcName: string // 赠送方NPC名称
|
||||
resources: Resources // 赠送的资源
|
||||
currentReputation: number // 当前好感度
|
||||
expectedReputationGain: number // 预期好感度增加
|
||||
expiresAt: number // 过期时间(7天后自动拒绝)
|
||||
read?: boolean
|
||||
}
|
||||
|
||||
// 礼物被拒绝通知(玩家赠送礼物被NPC拒绝)
|
||||
export interface GiftRejectedNotification {
|
||||
id: string
|
||||
timestamp: number
|
||||
npcId: string // NPC ID
|
||||
npcName: string // NPC名称
|
||||
rejectedResources: Resources // 被拒绝的资源
|
||||
currentReputation: number // 当前好感度
|
||||
reason: string // 拒绝原因
|
||||
read?: boolean
|
||||
}
|
||||
|
||||
// 残骸场
|
||||
export interface DebrisField {
|
||||
id: string
|
||||
@@ -356,7 +535,16 @@ export interface Player {
|
||||
fleetMissions: FleetMission[]
|
||||
battleReports: BattleResult[]
|
||||
spyReports: SpyReport[]
|
||||
spiedNotifications: SpiedNotification[] // 被侦查通知
|
||||
npcActivityNotifications: NPCActivityNotification[] // NPC活动通知(回收残骸等)
|
||||
missionReports: MissionReport[] // 任务报告(运输、殖民、回收等)
|
||||
incomingFleetAlerts: IncomingFleetAlert[] // 即将到来的敌对舰队警告
|
||||
giftNotifications: GiftNotification[] // 礼物通知(等待接受/拒绝)
|
||||
giftRejectedNotifications: GiftRejectedNotification[] // 礼物被拒绝通知
|
||||
points: number // 总积分(每1000资源=1分)
|
||||
// 外交系统字段
|
||||
diplomaticRelations?: Record<string, DiplomaticRelation> // 玩家对NPC的关系(key: npcId)
|
||||
diplomaticReports?: DiplomaticReport[] // 外交变化报告
|
||||
}
|
||||
|
||||
// 游戏状态
|
||||
@@ -384,4 +572,24 @@ export interface NPC {
|
||||
planets: Planet[]
|
||||
technologies: Record<TechnologyType, number>
|
||||
difficulty: 'easy' | 'medium' | 'hard'
|
||||
// 行为跟踪字段
|
||||
lastSpyTime?: number // 上次侦查玩家的时间
|
||||
lastAttackTime?: number // 上次攻击玩家的时间
|
||||
playerSpyReports?: Record<string, SpyReport> // 对玩家星球的侦查报告(key: planetId)
|
||||
fleetMissions?: FleetMission[] // NPC的舰队任务
|
||||
// 被攻击追踪
|
||||
attackedBy?: Record<
|
||||
string,
|
||||
{
|
||||
count: number // 被攻击次数
|
||||
lastAttackTime: number // 最后被攻击时间
|
||||
planetId?: string // 攻击者星球ID
|
||||
}
|
||||
>
|
||||
alertUntil?: number // 警戒状态持续到的时间戳
|
||||
revengeTarget?: string // 复仇目标玩家ID
|
||||
// 外交系统字段
|
||||
relations?: Record<string, DiplomaticRelation> // NPC对其他实体的关系(key: targetId)
|
||||
allies?: string[] // 盟友列表(NPC ID)
|
||||
enemies?: string[] // 敌人列表(NPC ID)
|
||||
}
|
||||
|
||||
@@ -120,7 +120,4 @@ export interface CalculateDebrisRequest extends WorkerRequestMessage {
|
||||
/**
|
||||
* 所有 Worker 请求类型
|
||||
*/
|
||||
export type WorkerRequest =
|
||||
| SimulateBattleRequest
|
||||
| CalculatePlunderRequest
|
||||
| CalculateDebrisRequest
|
||||
export type WorkerRequest = SimulateBattleRequest | CalculatePlunderRequest | CalculateDebrisRequest
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
/**
|
||||
* 格式化数字为英文单位(K, M, B)
|
||||
* 格式化数字为英文单位(K, M, B, T, Q)
|
||||
* @param num 数字
|
||||
* @param decimals 小数位数,默认2
|
||||
* @returns 格式化后的字符串
|
||||
*/
|
||||
export const formatNumber = (num: number, decimals: number = 2): string => {
|
||||
if (num >= 1_000_000_000) {
|
||||
if (num >= 1_000_000_000_000_000) {
|
||||
return (num / 1_000_000_000_000_000).toFixed(decimals) + 'Q'
|
||||
} else if (num >= 1_000_000_000_000) {
|
||||
return (num / 1_000_000_000_000).toFixed(decimals) + 'T'
|
||||
} else if (num >= 1_000_000_000) {
|
||||
return (num / 1_000_000_000).toFixed(decimals) + 'B'
|
||||
} else if (num >= 1_000_000) {
|
||||
return (num / 1_000_000).toFixed(decimals) + 'M'
|
||||
@@ -27,22 +31,35 @@ export const getResourceColor = (current: number, max: number): string => {
|
||||
if (ratio >= 0.7) return 'text-yellow-600 dark:text-yellow-400'
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化时间(秒转为天时分秒)
|
||||
* 格式化时间(秒转为 年:天:时:分:秒)
|
||||
* @param seconds 秒数
|
||||
* @returns 格式化后的时间字符串(例如 2d 05:30:15 或 05:30:15)
|
||||
* @returns 格式化后的时间字符串
|
||||
* 例如:
|
||||
* 1:02:03:04:05
|
||||
* 02:03:04:05
|
||||
* 03:04:05
|
||||
*/
|
||||
export const formatTime = (seconds: number): string => {
|
||||
const YEAR = 365 * 86400
|
||||
const years = Math.floor(seconds / YEAR)
|
||||
seconds %= YEAR
|
||||
const days = Math.floor(seconds / 86400)
|
||||
const hours = Math.floor((seconds % 86400) / 3600)
|
||||
const minutes = Math.floor((seconds % 3600) / 60)
|
||||
seconds %= 86400
|
||||
const hours = Math.floor(seconds / 3600)
|
||||
seconds %= 3600
|
||||
const minutes = Math.floor(seconds / 60)
|
||||
const secs = Math.floor(seconds % 60)
|
||||
|
||||
if (days > 0) {
|
||||
return `${days}:${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
|
||||
const h = hours.toString().padStart(2, '0')
|
||||
const m = minutes.toString().padStart(2, '0')
|
||||
const s = secs.toString().padStart(2, '0')
|
||||
if (years > 0) {
|
||||
return `${years}:${days}:${h}:${m}:${s}`
|
||||
}
|
||||
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
|
||||
if (days > 0) {
|
||||
return `${days}:${h}:${m}:${s}`
|
||||
}
|
||||
return `${h}:${m}:${s}`
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,131 +1,137 @@
|
||||
<template>
|
||||
<div class="container mx-auto p-4 sm:p-6 space-y-6">
|
||||
<h1 class="text-2xl sm:text-3xl font-bold">{{ t('simulatorView.title') }}</h1>
|
||||
|
||||
<!-- 标签切换 -->
|
||||
<div class="flex gap-2 border-b">
|
||||
<Button @click="activeTab = 'attacker'" :variant="activeTab === 'attacker' ? 'default' : 'ghost'" class="rounded-b-none">
|
||||
<Sword />
|
||||
{{ t('simulatorView.attacker') }}
|
||||
</Button>
|
||||
<Button @click="activeTab = 'defender'" :variant="activeTab === 'defender' ? 'default' : 'ghost'" class="rounded-b-none">
|
||||
<Shield />
|
||||
{{ t('simulatorView.defender') }}
|
||||
</Button>
|
||||
</div>
|
||||
<!-- 攻击方配置 -->
|
||||
<Card v-if="activeTab === 'attacker'">
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('simulatorView.attackerConfig') }}</CardTitle>
|
||||
<CardDescription>{{ t('simulatorView.attackerConfigDesc') }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<!-- 舰队配置 -->
|
||||
<div>
|
||||
<h3 class="text-sm font-medium mb-3">{{ t('simulatorView.fleet') }}</h3>
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
<div v-for="shipType in Object.values(ShipType)" :key="shipType" class="space-y-1">
|
||||
<Label :for="`attacker-${shipType}`" class="text-xs">{{ SHIPS[shipType].name }}</Label>
|
||||
<Input :id="`attacker-${shipType}`" v-model.number="attackerFleet[shipType]" type="number" min="0" class="h-8" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 科技等级 -->
|
||||
<div>
|
||||
<h3 class="text-sm font-medium mb-3">{{ t('simulatorView.techLevels') }}</h3>
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<div class="space-y-1">
|
||||
<Label for="attacker-weapon" class="text-xs">{{ t('simulatorView.weapon') }}</Label>
|
||||
<Input id="attacker-weapon" v-model.number="attackerTech.weapon" type="number" min="0" class="h-8" />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="attacker-shield" class="text-xs">{{ t('simulatorView.shield') }}</Label>
|
||||
<Input id="attacker-shield" v-model.number="attackerTech.shield" type="number" min="0" class="h-8" />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="attacker-armor" class="text-xs">{{ t('simulatorView.armor') }}</Label>
|
||||
<Input id="attacker-armor" v-model.number="attackerTech.armor" type="number" min="0" class="h-8" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<!-- 防守方配置 -->
|
||||
<Card v-else>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('simulatorView.defenderConfig') }}</CardTitle>
|
||||
<CardDescription>{{ t('simulatorView.defenderConfigDesc') }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<!-- 舰队配置 -->
|
||||
<div>
|
||||
<h3 class="text-sm font-medium mb-3">{{ t('simulatorView.fleet') }}</h3>
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
<div v-for="shipType in Object.values(ShipType)" :key="shipType" class="space-y-1">
|
||||
<Label :for="`defender-${shipType}`" class="text-xs">{{ SHIPS[shipType].name }}</Label>
|
||||
<Input :id="`defender-${shipType}`" v-model.number="defenderFleet[shipType]" type="number" min="0" class="h-8" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Tabs v-model="activeTab" class="w-full">
|
||||
<TabsList class="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="attacker">
|
||||
<Sword class="h-4 w-4 mr-2" />
|
||||
{{ t('simulatorView.attacker') }}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="defender">
|
||||
<Shield class="h-4 w-4 mr-2" />
|
||||
{{ t('simulatorView.defender') }}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<!-- 防御设施 -->
|
||||
<div>
|
||||
<h3 class="text-sm font-medium mb-3">{{ t('simulatorView.defenseStructures') }}</h3>
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
<div v-for="defenseType in Object.values(DefenseType)" :key="defenseType" class="space-y-1">
|
||||
<Label :for="`defense-${defenseType}`" class="text-xs">{{ DEFENSES[defenseType].name }}</Label>
|
||||
<Input :id="`defense-${defenseType}`" v-model.number="defenderDefense[defenseType]" type="number" min="0" class="h-8" />
|
||||
<!-- 攻击方配置 -->
|
||||
<TabsContent value="attacker" class="mt-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('simulatorView.attackerConfig') }}</CardTitle>
|
||||
<CardDescription>{{ t('simulatorView.attackerConfigDesc') }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<!-- 舰队配置 -->
|
||||
<div>
|
||||
<h3 class="text-sm font-medium mb-3">{{ t('simulatorView.fleet') }}</h3>
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
<div v-for="shipType in Object.values(ShipType)" :key="shipType" class="space-y-1">
|
||||
<Label :for="`attacker-${shipType}`" class="text-xs">{{ SHIPS[shipType].name }}</Label>
|
||||
<Input
|
||||
:id="`attacker-${shipType}`"
|
||||
:model-value="attackerFleet[shipType] ?? 0"
|
||||
@update:model-value="val => (attackerFleet[shipType] = typeof val === 'number' ? val : 0)"
|
||||
type="number"
|
||||
min="0"
|
||||
class="h-8"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 科技等级 -->
|
||||
<div>
|
||||
<h3 class="text-sm font-medium mb-3">{{ t('simulatorView.techLevels') }}</h3>
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<div v-for="techType in techTypes" :key="techType" class="space-y-1">
|
||||
<Label :for="`attacker-${techType}`" class="text-xs">{{ t(`simulatorView.${techType}`) }}</Label>
|
||||
<Input :id="`attacker-${techType}`" v-model.number="attackerTech[techType]" type="number" min="0" class="h-8" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<!-- 科技等级 -->
|
||||
<div>
|
||||
<h3 class="text-sm font-medium mb-3">{{ t('simulatorView.techLevels') }}</h3>
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<div class="space-y-1">
|
||||
<Label for="defender-weapon" class="text-xs">{{ t('simulatorView.weapon') }}</Label>
|
||||
<Input id="defender-weapon" v-model.number="defenderTech.weapon" type="number" min="0" class="h-8" />
|
||||
<!-- 防守方配置 -->
|
||||
<TabsContent value="defender" class="mt-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('simulatorView.defenderConfig') }}</CardTitle>
|
||||
<CardDescription>{{ t('simulatorView.defenderConfigDesc') }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<!-- 舰队配置 -->
|
||||
<div>
|
||||
<h3 class="text-sm font-medium mb-3">{{ t('simulatorView.fleet') }}</h3>
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
<div v-for="shipType in Object.values(ShipType)" :key="shipType" class="space-y-1">
|
||||
<Label :for="`defender-${shipType}`" class="text-xs">{{ SHIPS[shipType].name }}</Label>
|
||||
<Input
|
||||
:id="`defender-${shipType}`"
|
||||
:model-value="defenderFleet[shipType] ?? 0"
|
||||
@update:model-value="val => (defenderFleet[shipType] = typeof val === 'number' ? val : 0)"
|
||||
type="number"
|
||||
min="0"
|
||||
class="h-8"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="defender-shield" class="text-xs">{{ t('simulatorView.shield') }}</Label>
|
||||
<Input id="defender-shield" v-model.number="defenderTech.shield" type="number" min="0" class="h-8" />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="defender-armor" class="text-xs">{{ t('simulatorView.armor') }}</Label>
|
||||
<Input id="defender-armor" v-model.number="defenderTech.armor" type="number" min="0" class="h-8" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 防守方资源 -->
|
||||
<div>
|
||||
<h3 class="text-sm font-medium mb-3">{{ t('simulatorView.defenderResources') }}</h3>
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<div class="space-y-1">
|
||||
<Label for="defender-metal" class="text-xs flex items-center gap-1">
|
||||
<ResourceIcon type="metal" size="sm" />
|
||||
{{ t('resources.metal') }}
|
||||
</Label>
|
||||
<Input id="defender-metal" v-model.number="defenderResources.metal" type="number" min="0" class="h-8" />
|
||||
<!-- 防御设施 -->
|
||||
<div>
|
||||
<h3 class="text-sm font-medium mb-3">{{ t('simulatorView.defenseStructures') }}</h3>
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
<div v-for="defenseType in Object.values(DefenseType)" :key="defenseType" class="space-y-1">
|
||||
<Label :for="`defense-${defenseType}`" class="text-xs">{{ DEFENSES[defenseType].name }}</Label>
|
||||
<Input
|
||||
:id="`defense-${defenseType}`"
|
||||
:model-value="defenderDefense[defenseType] ?? 0"
|
||||
@update:model-value="val => (defenderDefense[defenseType] = typeof val === 'number' ? val : 0)"
|
||||
type="number"
|
||||
min="0"
|
||||
class="h-8"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="defender-crystal" class="text-xs flex items-center gap-1">
|
||||
<ResourceIcon type="crystal" size="sm" />
|
||||
{{ t('resources.crystal') }}
|
||||
</Label>
|
||||
<Input id="defender-crystal" v-model.number="defenderResources.crystal" type="number" min="0" class="h-8" />
|
||||
|
||||
<!-- 科技等级 -->
|
||||
<div>
|
||||
<h3 class="text-sm font-medium mb-3">{{ t('simulatorView.techLevels') }}</h3>
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<div v-for="techType in techTypes" :key="techType" class="space-y-1">
|
||||
<Label :for="`defender-${techType}`" class="text-xs">{{ t(`simulatorView.${techType}`) }}</Label>
|
||||
<Input :id="`defender-${techType}`" v-model.number="defenderTech[techType]" type="number" min="0" class="h-8" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="defender-deuterium" class="text-xs flex items-center gap-1">
|
||||
<ResourceIcon type="deuterium" size="sm" />
|
||||
{{ t('resources.deuterium') }}
|
||||
</Label>
|
||||
<Input id="defender-deuterium" v-model.number="defenderResources.deuterium" type="number" min="0" class="h-8" />
|
||||
|
||||
<!-- 防守方资源 -->
|
||||
<div>
|
||||
<h3 class="text-sm font-medium mb-3">{{ t('simulatorView.defenderResources') }}</h3>
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<div v-for="resourceType in resourceTypes" :key="resourceType.key" class="space-y-1">
|
||||
<Label :for="`defender-${resourceType.key}`" class="text-xs flex items-center gap-1">
|
||||
<ResourceIcon :type="resourceType.key" size="sm" />
|
||||
{{ t(`resources.${resourceType.key}`) }}
|
||||
</Label>
|
||||
<Input
|
||||
:id="`defender-${resourceType.key}`"
|
||||
v-model.number="defenderResources[resourceType.key]"
|
||||
type="number"
|
||||
min="0"
|
||||
class="h-8"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex gap-2">
|
||||
@@ -149,6 +155,7 @@
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
import { useGameConfig } from '@/composables/useGameConfig'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
@@ -163,19 +170,23 @@
|
||||
const { t } = useI18n()
|
||||
const { SHIPS, DEFENSES } = useGameConfig()
|
||||
|
||||
// 科技类型配置
|
||||
const techTypes = ['weapon', 'shield', 'armor'] as const
|
||||
|
||||
// 资源类型配置(用于防守方资源输入)
|
||||
const resourceTypes = [{ key: 'metal' as const }, { key: 'crystal' as const }, { key: 'deuterium' as const }]
|
||||
|
||||
// 动态初始化所有舰船类型为0
|
||||
const initializeFleet = (): Partial<Fleet> => {
|
||||
const fleet: Partial<Fleet> = {}
|
||||
Object.values(ShipType).forEach(shipType => {
|
||||
fleet[shipType] = 0
|
||||
})
|
||||
return fleet
|
||||
}
|
||||
|
||||
// 攻击方配置
|
||||
const attackerFleet = ref<Partial<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
|
||||
})
|
||||
const attackerFleet = ref<Partial<Fleet>>(initializeFleet())
|
||||
|
||||
const activeTab = ref('attacker')
|
||||
|
||||
@@ -186,29 +197,18 @@
|
||||
})
|
||||
|
||||
// 防守方配置
|
||||
const defenderFleet = ref<Partial<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
|
||||
})
|
||||
const defenderFleet = ref<Partial<Fleet>>(initializeFleet())
|
||||
|
||||
const defenderDefense = ref<Partial<Record<DefenseType, number>>>({
|
||||
[DefenseType.RocketLauncher]: 0,
|
||||
[DefenseType.LightLaser]: 0,
|
||||
[DefenseType.HeavyLaser]: 0,
|
||||
[DefenseType.GaussCannon]: 0,
|
||||
[DefenseType.IonCannon]: 0,
|
||||
[DefenseType.PlasmaTurret]: 0,
|
||||
[DefenseType.SmallShieldDome]: 0,
|
||||
[DefenseType.LargeShieldDome]: 0
|
||||
})
|
||||
// 动态初始化所有防御类型为0
|
||||
const initializeDefense = (): Partial<Record<DefenseType, number>> => {
|
||||
const defense: Partial<Record<DefenseType, number>> = {}
|
||||
Object.values(DefenseType).forEach(defenseType => {
|
||||
defense[defenseType] = 0
|
||||
})
|
||||
return defense
|
||||
}
|
||||
|
||||
const defenderDefense = ref<Partial<Record<DefenseType, number>>>(initializeDefense())
|
||||
|
||||
const defenderTech = ref({
|
||||
weapon: 0,
|
||||
@@ -294,15 +294,9 @@
|
||||
|
||||
// 重置模拟
|
||||
const resetSimulation = () => {
|
||||
Object.keys(attackerFleet.value).forEach(key => {
|
||||
attackerFleet.value[key as ShipType] = 0
|
||||
})
|
||||
Object.keys(defenderFleet.value).forEach(key => {
|
||||
defenderFleet.value[key as ShipType] = 0
|
||||
})
|
||||
Object.keys(defenderDefense.value).forEach(key => {
|
||||
defenderDefense.value[key as DefenseType] = 0
|
||||
})
|
||||
attackerFleet.value = initializeFleet()
|
||||
defenderFleet.value = initializeFleet()
|
||||
defenderDefense.value = initializeDefense()
|
||||
attackerTech.value = { weapon: 0, shield: 0, armor: 0 }
|
||||
defenderTech.value = { weapon: 0, shield: 0, armor: 0 }
|
||||
simulationResult.value = null
|
||||
|
||||
@@ -1,12 +1,30 @@
|
||||
<template>
|
||||
<div v-if="planet" class="container mx-auto p-4 sm:p-6">
|
||||
<div class="flex justify-between items-center mb-4 sm:mb-6 gap-2">
|
||||
<h1 class="text-2xl sm:text-3xl font-bold">{{ t('buildingsView.title') }}</h1>
|
||||
<div class="text-xs sm:text-sm">
|
||||
<span class="flex items-center gap-1.5 text-muted-foreground">
|
||||
<Grid3x3 :size="14" />
|
||||
{{ getUsedSpace(planet) }} / {{ planet.maxSpace }}
|
||||
</span>
|
||||
<h1 class="text-2xl sm:text-3xl font-bold mb-4 sm:mb-6">{{ t('buildingsView.title') }}</h1>
|
||||
|
||||
<!-- 占地显示 -->
|
||||
<div class="mb-4 sm:mb-6 p-3 sm:p-4 bg-muted/50 rounded-lg border">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-sm sm:text-base font-medium flex items-center gap-2">
|
||||
<Grid3x3 :size="16" />
|
||||
{{ t('buildingsView.spaceUsage') }}:
|
||||
</div>
|
||||
<div class="text-sm sm:text-base font-bold">
|
||||
<span :class="getUsedSpace(planet) > planet.maxSpace ? 'text-destructive' : 'text-primary'">
|
||||
{{ formatNumber(getUsedSpace(planet)) }}
|
||||
</span>
|
||||
<span class="text-muted-foreground mx-1">/</span>
|
||||
<span>{{ formatNumber(planet.maxSpace) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<div class="w-full bg-background rounded-full h-2.5 sm:h-3 overflow-hidden">
|
||||
<div
|
||||
class="h-full transition-all duration-300"
|
||||
:class="getUsedSpace(planet) > planet.maxSpace ? 'bg-destructive' : 'bg-primary'"
|
||||
:style="{ width: `${Math.min((getUsedSpace(planet) / planet.maxSpace) * 100, 100)}%` }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,64 +34,46 @@
|
||||
<CardUnlockOverlay :requirements="BUILDINGS[buildingType].requirements" :currentLevel="getBuildingLevel(buildingType)" />
|
||||
|
||||
<CardHeader>
|
||||
<div class="flex justify-between items-start gap-2">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="mb-2">
|
||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-2">
|
||||
<CardTitle
|
||||
class="text-base sm:text-lg cursor-pointer hover:text-primary transition-colors"
|
||||
class="text-sm sm:text-base lg:text-lg cursor-pointer hover:text-primary transition-colors underline decoration-dotted underline-offset-4 order-2 sm:order-1"
|
||||
@click="detailDialog.openBuilding(buildingType, getBuildingLevel(buildingType))"
|
||||
>
|
||||
{{ BUILDINGS[buildingType].name }}
|
||||
</CardTitle>
|
||||
<CardDescription class="text-xs sm:text-sm">{{ BUILDINGS[buildingType].description }}</CardDescription>
|
||||
<Badge variant="secondary" class="text-xs whitespace-nowrap self-start order-1 sm:order-2">
|
||||
Lv {{ getBuildingLevel(buildingType) }}
|
||||
</Badge>
|
||||
</div>
|
||||
<Badge variant="secondary" class="text-xs whitespace-nowrap flex-shrink-0">Lv {{ getBuildingLevel(buildingType) }}</Badge>
|
||||
</div>
|
||||
<CardDescription class="text-xs sm:text-sm">{{ BUILDINGS[buildingType].description }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="space-y-3">
|
||||
<div class="text-xs sm:text-sm space-y-1.5 sm:space-y-2">
|
||||
<p class="text-muted-foreground mb-1 sm:mb-2">{{ t('buildingsView.upgradeCost') }}:</p>
|
||||
<div class="space-y-1 sm:space-y-1.5">
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="metal" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.metal') }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="
|
||||
getResourceCostColor(planet.resources.metal, getBuildingCost(buildingType, getBuildingLevel(buildingType) + 1).metal)
|
||||
"
|
||||
>
|
||||
{{ formatNumber(getBuildingCost(buildingType, getBuildingLevel(buildingType) + 1).metal) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="crystal" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.crystal') }}:</span>
|
||||
<div
|
||||
v-for="resourceType in costResourceTypes"
|
||||
:key="resourceType.key"
|
||||
v-show="
|
||||
resourceType.key !== 'darkMatter' || getBuildingCost(buildingType, getBuildingLevel(buildingType) + 1).darkMatter > 0
|
||||
"
|
||||
class="flex items-center gap-1.5 sm:gap-2"
|
||||
>
|
||||
<ResourceIcon :type="resourceType.key" size="sm" />
|
||||
<span class="text-xs">{{ t(`resources.${resourceType.key}`) }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="
|
||||
getResourceCostColor(
|
||||
planet.resources.crystal,
|
||||
getBuildingCost(buildingType, getBuildingLevel(buildingType) + 1).crystal
|
||||
planet.resources[resourceType.key],
|
||||
getBuildingCost(buildingType, getBuildingLevel(buildingType) + 1)[resourceType.key]
|
||||
)
|
||||
"
|
||||
>
|
||||
{{ formatNumber(getBuildingCost(buildingType, getBuildingLevel(buildingType) + 1).crystal) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="deuterium" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.deuterium') }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="
|
||||
getResourceCostColor(
|
||||
planet.resources.deuterium,
|
||||
getBuildingCost(buildingType, getBuildingLevel(buildingType) + 1).deuterium
|
||||
)
|
||||
"
|
||||
>
|
||||
{{ formatNumber(getBuildingCost(buildingType, getBuildingLevel(buildingType) + 1).deuterium) }}
|
||||
{{ formatNumber(getBuildingCost(buildingType, getBuildingLevel(buildingType) + 1)[resourceType.key]) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -113,6 +113,9 @@
|
||||
<span>{{ formatNumber(getDemolishRefund(buildingType).metal) }} {{ t('resources.metal') }}</span>
|
||||
<span>{{ formatNumber(getDemolishRefund(buildingType).crystal) }} {{ t('resources.crystal') }}</span>
|
||||
<span>{{ formatNumber(getDemolishRefund(buildingType).deuterium) }} {{ t('resources.deuterium') }}</span>
|
||||
<span v-if="getDemolishRefund(buildingType).darkMatter > 0">
|
||||
{{ formatNumber(getDemolishRefund(buildingType).darkMatter) }} {{ t('resources.darkMatter') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -121,7 +124,35 @@
|
||||
</div>
|
||||
|
||||
<!-- 提示对话框 -->
|
||||
<AlertDialog ref="alertDialog" />
|
||||
<AlertDialog :open="alertDialogOpen" @update:open="alertDialogOpen = $event">
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{{ alertDialogTitle }}</AlertDialogTitle>
|
||||
<AlertDialogDescription class="whitespace-pre-line">
|
||||
{{ alertDialogMessage }}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogAction>{{ t('common.confirm') }}</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
<!-- 拆除确认对话框 -->
|
||||
<AlertDialog :open="demolishConfirmOpen" @update:open="demolishConfirmOpen = $event">
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{{ t('buildingsView.confirmDemolish') }}</AlertDialogTitle>
|
||||
<AlertDialogDescription class="whitespace-pre-line">
|
||||
{{ demolishConfirmMessage }}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>{{ t('common.cancel') }}</AlertDialogCancel>
|
||||
<AlertDialogAction @click="confirmDemolish">{{ t('common.confirm') }}</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -138,24 +169,51 @@
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import ResourceIcon from '@/components/ResourceIcon.vue'
|
||||
import CardUnlockOverlay from '@/components/CardUnlockOverlay.vue'
|
||||
import AlertDialog from '@/components/AlertDialog.vue'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { Clock, Grid3x3 } from 'lucide-vue-next'
|
||||
import { formatNumber, formatTime, getResourceCostColor } from '@/utils/format'
|
||||
import * as buildingLogic from '@/logic/buildingLogic'
|
||||
import * as buildingValidation from '@/logic/buildingValidation'
|
||||
import * as publicLogic from '@/logic/publicLogic'
|
||||
import * as officerLogic from '@/logic/officerLogic'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const detailDialog = useDetailDialogStore()
|
||||
const { t } = useI18n()
|
||||
const { BUILDINGS, TECHNOLOGIES } = useGameConfig()
|
||||
const planet = computed(() => gameStore.currentPlanet)
|
||||
const alertDialog = ref<InstanceType<typeof AlertDialog> | null>(null)
|
||||
|
||||
// AlertDialog 状态
|
||||
const alertDialogOpen = ref(false)
|
||||
const alertDialogTitle = ref('')
|
||||
const alertDialogMessage = ref('')
|
||||
|
||||
// 拆除确认对话框状态
|
||||
const demolishConfirmOpen = ref(false)
|
||||
const demolishConfirmMessage = ref('')
|
||||
const pendingDemolishBuilding = ref<BuildingType | null>(null)
|
||||
|
||||
// 资源类型配置(用于成本显示)
|
||||
const costResourceTypes = [
|
||||
{ key: 'metal' as const },
|
||||
{ key: 'crystal' as const },
|
||||
{ key: 'deuterium' as const },
|
||||
{ key: 'darkMatter' as const }
|
||||
]
|
||||
|
||||
// 根据星球类型过滤可用建筑
|
||||
const availableBuildings = computed(() => {
|
||||
const availableBuildings = computed<BuildingType[]>(() => {
|
||||
if (!planet.value) return []
|
||||
return Object.values(BuildingType).filter(buildingType => {
|
||||
return (Object.values(BuildingType) as BuildingType[]).filter(buildingType => {
|
||||
const config = BUILDINGS.value[buildingType]
|
||||
if (planet.value!.isMoon) {
|
||||
// 月球只能建造月球专属建筑
|
||||
@@ -189,19 +247,17 @@
|
||||
const handleUpgrade = (buildingType: BuildingType) => {
|
||||
// 检查前置条件
|
||||
if (!checkUpgradeRequirements(buildingType)) {
|
||||
alertDialog.value?.show({
|
||||
title: t('common.requirementsNotMet'),
|
||||
message: getRequirementsList(buildingType)
|
||||
})
|
||||
alertDialogTitle.value = t('common.requirementsNotMet')
|
||||
alertDialogMessage.value = getRequirementsList(buildingType)
|
||||
alertDialogOpen.value = true
|
||||
return
|
||||
}
|
||||
|
||||
const success = upgradeBuilding(buildingType)
|
||||
if (!success) {
|
||||
alertDialog.value?.show({
|
||||
title: t('buildingsView.upgradeFailed'),
|
||||
message: t('buildingsView.upgradeFailedMessage')
|
||||
})
|
||||
alertDialogTitle.value = t('buildingsView.upgradeFailed')
|
||||
alertDialogMessage.value = t('buildingsView.upgradeFailedMessage')
|
||||
alertDialogOpen.value = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,7 +347,13 @@
|
||||
return false
|
||||
}
|
||||
|
||||
if (planet.value.buildQueue.length > 0) return false
|
||||
// 检查建造队列是否已满(只计算建筑类型的队列项)
|
||||
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, Date.now())
|
||||
const maxQueue = publicLogic.getMaxBuildQueue(planet.value, bonuses.additionalBuildQueue)
|
||||
const buildingQueueCount = planet.value.buildQueue.filter(item => item.type === 'building' || item.type === 'demolish').length
|
||||
if (buildingQueueCount >= maxQueue) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查前置条件
|
||||
const validation = buildingValidation.validateBuildingUpgrade(
|
||||
@@ -307,7 +369,8 @@
|
||||
return (
|
||||
planet.value.resources.metal >= cost.metal &&
|
||||
planet.value.resources.crystal >= cost.crystal &&
|
||||
planet.value.resources.deuterium >= cost.deuterium
|
||||
planet.value.resources.deuterium >= cost.deuterium &&
|
||||
planet.value.resources.darkMatter >= cost.darkMatter
|
||||
)
|
||||
}
|
||||
|
||||
@@ -316,7 +379,21 @@
|
||||
}
|
||||
|
||||
const getBuildingTime = (buildingType: BuildingType, targetLevel: number): number => {
|
||||
return buildingLogic.calculateBuildingTime(buildingType, targetLevel)
|
||||
if (!planet.value) return 0
|
||||
|
||||
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, Date.now())
|
||||
|
||||
// 获取机器人工厂和纳米工厂等级
|
||||
const roboticsFactoryLevel = planet.value.buildings[BuildingType.RoboticsFactory] || 0
|
||||
const naniteFactoryLevel = planet.value.buildings[BuildingType.NaniteFactory] || 0
|
||||
|
||||
return buildingLogic.calculateBuildingTime(
|
||||
buildingType,
|
||||
targetLevel,
|
||||
bonuses.buildingSpeedBonus,
|
||||
roboticsFactoryLevel,
|
||||
naniteFactoryLevel
|
||||
)
|
||||
}
|
||||
|
||||
// 拆除建筑
|
||||
@@ -330,20 +407,47 @@
|
||||
}
|
||||
|
||||
const handleDemolish = (buildingType: BuildingType) => {
|
||||
const success = demolishBuilding(buildingType)
|
||||
if (!success) {
|
||||
alertDialog.value?.show({
|
||||
title: t('buildingsView.demolishFailed'),
|
||||
message: t('buildingsView.demolishFailedMessage')
|
||||
})
|
||||
const buildingName = BUILDINGS.value[buildingType].name
|
||||
const refund = getDemolishRefund(buildingType)
|
||||
|
||||
demolishConfirmMessage.value = `${t('buildingsView.confirmDemolishMessage')}: ${buildingName}
|
||||
|
||||
${t('buildingsView.demolishRefund')}:
|
||||
${t('resources.metal')}: ${formatNumber(refund.metal)}
|
||||
${t('resources.crystal')}: ${formatNumber(refund.crystal)}
|
||||
${t('resources.deuterium')}: ${formatNumber(refund.deuterium)}${refund.darkMatter > 0 ? `\n${t('resources.darkMatter')}: ${formatNumber(refund.darkMatter)}` : ''}`
|
||||
|
||||
pendingDemolishBuilding.value = buildingType
|
||||
demolishConfirmOpen.value = true
|
||||
}
|
||||
|
||||
const confirmDemolish = () => {
|
||||
if (pendingDemolishBuilding.value) {
|
||||
const success = demolishBuilding(pendingDemolishBuilding.value)
|
||||
if (!success) {
|
||||
alertDialogTitle.value = t('buildingsView.demolishFailed')
|
||||
alertDialogMessage.value = t('buildingsView.demolishFailedMessage')
|
||||
alertDialogOpen.value = true
|
||||
}
|
||||
}
|
||||
demolishConfirmOpen.value = false
|
||||
pendingDemolishBuilding.value = null
|
||||
}
|
||||
|
||||
// 检查是否可以拆除
|
||||
const canDemolish = (buildingType: BuildingType): boolean => {
|
||||
if (!planet.value) return false
|
||||
if (planet.value.buildQueue.length > 0) return false
|
||||
return getBuildingLevel(buildingType) > 0
|
||||
if (getBuildingLevel(buildingType) <= 0) return false
|
||||
|
||||
// 检查建造队列是否已满(只计算建筑类型的队列项)
|
||||
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, Date.now())
|
||||
const maxQueue = publicLogic.getMaxBuildQueue(planet.value, bonuses.additionalBuildQueue)
|
||||
const buildingQueueCount = planet.value.buildQueue.filter(item => item.type === 'building' || item.type === 'demolish').length
|
||||
if (buildingQueueCount >= maxQueue) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// 获取拆除返还资源
|
||||
|
||||
@@ -9,20 +9,20 @@
|
||||
<Card v-for="defenseType in Object.values(DefenseType)" :key="defenseType" class="relative">
|
||||
<CardUnlockOverlay :requirements="DEFENSES[defenseType].requirements" />
|
||||
<CardHeader>
|
||||
<div class="flex justify-between items-start gap-2">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="mb-2">
|
||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-2">
|
||||
<CardTitle
|
||||
class="text-base sm:text-lg cursor-pointer hover:text-primary transition-colors"
|
||||
class="text-sm sm:text-base lg:text-lg cursor-pointer hover:text-primary transition-colors underline decoration-dotted underline-offset-4 order-2 sm:order-1"
|
||||
@click="detailDialog.openDefense(defenseType)"
|
||||
>
|
||||
{{ DEFENSES[defenseType].name }}
|
||||
</CardTitle>
|
||||
<CardDescription class="text-xs sm:text-sm">{{ DEFENSES[defenseType].description }}</CardDescription>
|
||||
<Badge variant="secondary" class="text-xs whitespace-nowrap self-start order-1 sm:order-2">
|
||||
{{ planet.defense[defenseType] }}
|
||||
</Badge>
|
||||
</div>
|
||||
<Badge variant="secondary" class="text-xs whitespace-nowrap flex-shrink-0">
|
||||
{{ planet.defense[defenseType] }}
|
||||
</Badge>
|
||||
</div>
|
||||
<CardDescription class="text-xs sm:text-sm">{{ DEFENSES[defenseType].description }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="space-y-3 sm:space-y-4">
|
||||
@@ -48,34 +48,19 @@
|
||||
<div class="text-xs sm:text-sm space-y-1.5 sm:space-y-2">
|
||||
<p class="text-muted-foreground mb-1 sm:mb-2">{{ t('defenseView.unitCost') }}:</p>
|
||||
<div class="space-y-1 sm:space-y-1.5">
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="metal" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.metal') }}:</span>
|
||||
<div
|
||||
v-for="resourceType in costResourceTypes"
|
||||
:key="resourceType.key"
|
||||
v-show="resourceType.key !== 'darkMatter' || DEFENSES[defenseType].cost.darkMatter > 0"
|
||||
class="flex items-center gap-1.5 sm:gap-2"
|
||||
>
|
||||
<ResourceIcon :type="resourceType.key" size="sm" />
|
||||
<span class="text-xs">{{ t(`resources.${resourceType.key}`) }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="getResourceCostColor(planet.resources.metal, DEFENSES[defenseType].cost.metal)"
|
||||
:class="getResourceCostColor(planet.resources[resourceType.key], DEFENSES[defenseType].cost[resourceType.key])"
|
||||
>
|
||||
{{ formatNumber(DEFENSES[defenseType].cost.metal) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="crystal" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.crystal') }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="getResourceCostColor(planet.resources.crystal, DEFENSES[defenseType].cost.crystal)"
|
||||
>
|
||||
{{ formatNumber(DEFENSES[defenseType].cost.crystal) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="deuterium" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.deuterium') }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="getResourceCostColor(planet.resources.deuterium, DEFENSES[defenseType].cost.deuterium)"
|
||||
>
|
||||
{{ formatNumber(DEFENSES[defenseType].cost.deuterium) }}
|
||||
{{ formatNumber(DEFENSES[defenseType].cost[resourceType.key]) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -101,34 +86,19 @@
|
||||
<div v-if="quantities[defenseType] > 0" class="text-xs sm:text-sm space-y-1.5 sm:space-y-2 p-2.5 sm:p-3 bg-muted rounded-lg">
|
||||
<p class="font-medium text-muted-foreground">{{ t('defenseView.totalCost') }}:</p>
|
||||
<div class="space-y-1 sm:space-y-1.5">
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="metal" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.metal') }}:</span>
|
||||
<div
|
||||
v-for="resourceType in costResourceTypes"
|
||||
:key="resourceType.key"
|
||||
v-show="resourceType.key !== 'darkMatter' || getTotalCost(defenseType).darkMatter > 0"
|
||||
class="flex items-center gap-1.5 sm:gap-2"
|
||||
>
|
||||
<ResourceIcon :type="resourceType.key" size="sm" />
|
||||
<span class="text-xs">{{ t(`resources.${resourceType.key}`) }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="getResourceCostColor(planet.resources.metal, getTotalCost(defenseType).metal)"
|
||||
:class="getResourceCostColor(planet.resources[resourceType.key], getTotalCost(defenseType)[resourceType.key])"
|
||||
>
|
||||
{{ formatNumber(getTotalCost(defenseType).metal) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="crystal" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.crystal') }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="getResourceCostColor(planet.resources.crystal, getTotalCost(defenseType).crystal)"
|
||||
>
|
||||
{{ formatNumber(getTotalCost(defenseType).crystal) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="deuterium" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.deuterium') }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="getResourceCostColor(planet.resources.deuterium, getTotalCost(defenseType).deuterium)"
|
||||
>
|
||||
{{ formatNumber(getTotalCost(defenseType).deuterium) }}
|
||||
{{ formatNumber(getTotalCost(defenseType)[resourceType.key]) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -143,7 +113,19 @@
|
||||
</div>
|
||||
|
||||
<!-- 提示对话框 -->
|
||||
<AlertDialog ref="alertDialog" />
|
||||
<AlertDialog :open="alertDialogOpen" @update:open="alertDialogOpen = $event">
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{{ alertDialogTitle }}</AlertDialogTitle>
|
||||
<AlertDialogDescription class="whitespace-pre-line">
|
||||
{{ alertDialogMessage }}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogAction>{{ t('common.confirm') }}</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -160,7 +142,15 @@
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import ResourceIcon from '@/components/ResourceIcon.vue'
|
||||
import AlertDialog from '@/components/AlertDialog.vue'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import UnlockRequirement from '@/components/UnlockRequirement.vue'
|
||||
import CardUnlockOverlay from '@/components/CardUnlockOverlay.vue'
|
||||
import { formatNumber, getResourceCostColor } from '@/utils/format'
|
||||
@@ -172,7 +162,19 @@
|
||||
const { t } = useI18n()
|
||||
const { DEFENSES } = useGameConfig()
|
||||
const planet = computed(() => gameStore.currentPlanet)
|
||||
const alertDialog = ref<InstanceType<typeof AlertDialog> | null>(null)
|
||||
|
||||
// AlertDialog 状态
|
||||
const alertDialogOpen = ref(false)
|
||||
const alertDialogTitle = ref('')
|
||||
const alertDialogMessage = ref('')
|
||||
|
||||
// 资源类型配置(用于成本显示)
|
||||
const costResourceTypes = [
|
||||
{ key: 'metal' as const },
|
||||
{ key: 'crystal' as const },
|
||||
{ key: 'deuterium' as const },
|
||||
{ key: 'darkMatter' as const }
|
||||
]
|
||||
|
||||
// 每种防御设施的建造数量
|
||||
const quantities = ref<Record<DefenseType, number>>({
|
||||
@@ -184,6 +186,8 @@
|
||||
[DefenseType.PlasmaTurret]: 0,
|
||||
[DefenseType.SmallShieldDome]: 0,
|
||||
[DefenseType.LargeShieldDome]: 0,
|
||||
[DefenseType.AntiBallisticMissile]: 0,
|
||||
[DefenseType.InterplanetaryMissile]: 0,
|
||||
[DefenseType.PlanetaryShield]: 0
|
||||
})
|
||||
|
||||
@@ -205,19 +209,17 @@
|
||||
const handleBuild = (defenseType: DefenseType) => {
|
||||
const quantity = quantities.value[defenseType]
|
||||
if (quantity <= 0) {
|
||||
alertDialog.value?.show({
|
||||
title: t('defenseView.inputError'),
|
||||
message: t('defenseView.inputErrorMessage')
|
||||
})
|
||||
alertDialogTitle.value = t('defenseView.inputError')
|
||||
alertDialogMessage.value = t('defenseView.inputErrorMessage')
|
||||
alertDialogOpen.value = true
|
||||
return
|
||||
}
|
||||
|
||||
const success = buildDefense(defenseType, quantity)
|
||||
if (!success) {
|
||||
alertDialog.value?.show({
|
||||
title: t('defenseView.buildFailed'),
|
||||
message: t('defenseView.buildFailedMessage')
|
||||
})
|
||||
alertDialogTitle.value = t('defenseView.buildFailed')
|
||||
alertDialogMessage.value = t('defenseView.buildFailedMessage')
|
||||
alertDialogOpen.value = true
|
||||
} else {
|
||||
quantities.value[defenseType] = 0
|
||||
}
|
||||
@@ -240,14 +242,16 @@
|
||||
const totalCost = {
|
||||
metal: config.cost.metal * quantity,
|
||||
crystal: config.cost.crystal * quantity,
|
||||
deuterium: config.cost.deuterium * quantity
|
||||
deuterium: config.cost.deuterium * quantity,
|
||||
darkMatter: config.cost.darkMatter * quantity
|
||||
}
|
||||
|
||||
return (
|
||||
publicLogic.checkRequirements(planet.value, gameStore.player.technologies, config.requirements) &&
|
||||
planet.value.resources.metal >= totalCost.metal &&
|
||||
planet.value.resources.crystal >= totalCost.crystal &&
|
||||
planet.value.resources.deuterium >= totalCost.deuterium
|
||||
planet.value.resources.deuterium >= totalCost.deuterium &&
|
||||
planet.value.resources.darkMatter >= totalCost.darkMatter
|
||||
)
|
||||
}
|
||||
|
||||
@@ -258,7 +262,8 @@
|
||||
return {
|
||||
metal: config.cost.metal * quantity,
|
||||
crystal: config.cost.crystal * quantity,
|
||||
deuterium: config.cost.deuterium * quantity
|
||||
deuterium: config.cost.deuterium * quantity,
|
||||
darkMatter: config.cost.darkMatter * quantity
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
390
src/views/DiplomacyView.vue
Normal file
390
src/views/DiplomacyView.vue
Normal file
@@ -0,0 +1,390 @@
|
||||
<template>
|
||||
<div class="container mx-auto p-4 sm:p-6 space-y-6">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="text-2xl sm:text-3xl font-bold">{{ t('diplomacy.title') }}</h1>
|
||||
<p class="text-sm text-muted-foreground mt-1">{{ t('diplomacy.description') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 关系状态过滤标签 -->
|
||||
<Tabs v-model="activeTab" class="w-full">
|
||||
<TabsList class="grid w-full grid-cols-4">
|
||||
<TabsTrigger value="all">
|
||||
{{ t('diplomacy.tabs.all') }}
|
||||
<Badge variant="secondary" class="ml-2">{{ allNpcs.length }}</Badge>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="friendly">
|
||||
{{ t('diplomacy.tabs.friendly') }}
|
||||
<Badge variant="secondary" class="ml-2">{{ friendlyNpcs.length }}</Badge>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="neutral">
|
||||
{{ t('diplomacy.tabs.neutral') }}
|
||||
<Badge variant="secondary" class="ml-2">{{ neutralNpcs.length }}</Badge>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="hostile">
|
||||
{{ t('diplomacy.tabs.hostile') }}
|
||||
<Badge variant="secondary" class="ml-2">{{ hostileNpcs.length }}</Badge>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<!-- 全部NPC -->
|
||||
<TabsContent value="all" class="space-y-4 mt-6">
|
||||
<div v-if="allNpcs.length === 0" class="text-center py-12 text-muted-foreground">
|
||||
{{ t('diplomacy.noNpcs') }}
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<NpcRelationCard v-for="npc in paginatedAllNpcs" :key="npc.id" :npc="npc" :relation="getRelation(npc.id)" />
|
||||
</div>
|
||||
<Pagination
|
||||
v-if="totalPagesAll > 1"
|
||||
v-model:page="currentPage.all"
|
||||
:total="allNpcs.length"
|
||||
:items-per-page="ITEMS_PER_PAGE"
|
||||
:sibling-count="1"
|
||||
show-edges
|
||||
class="mt-6"
|
||||
>
|
||||
<PaginationContent>
|
||||
<PaginationPrevious>{{ t('pagination.previous') }}</PaginationPrevious>
|
||||
|
||||
<template v-for="(pageNum, index) in pageNumbersAll" :key="index">
|
||||
<PaginationItem v-if="typeof pageNum === 'number'" :value="pageNum" :is-active="pageNum === currentPage.all">
|
||||
{{ pageNum }}
|
||||
</PaginationItem>
|
||||
<span v-else class="px-2 text-muted-foreground">{{ pageNum }}</span>
|
||||
</template>
|
||||
|
||||
<PaginationNext>{{ t('pagination.next') }}</PaginationNext>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</template>
|
||||
</TabsContent>
|
||||
|
||||
<!-- 友好NPC -->
|
||||
<TabsContent value="friendly" class="space-y-4 mt-6">
|
||||
<div v-if="friendlyNpcs.length === 0" class="text-center py-12 text-muted-foreground">
|
||||
{{ t('diplomacy.noFriendlyNpcs') }}
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<NpcRelationCard v-for="npc in paginatedFriendlyNpcs" :key="npc.id" :npc="npc" :relation="getRelation(npc.id)" />
|
||||
</div>
|
||||
<Pagination
|
||||
v-if="totalPagesFriendly > 1"
|
||||
v-model:page="currentPage.friendly"
|
||||
:total="friendlyNpcs.length"
|
||||
:items-per-page="ITEMS_PER_PAGE"
|
||||
:sibling-count="1"
|
||||
show-edges
|
||||
class="mt-6"
|
||||
>
|
||||
<PaginationContent>
|
||||
<PaginationPrevious>{{ t('pagination.previous') }}</PaginationPrevious>
|
||||
|
||||
<template v-for="(pageNum, index) in pageNumbersFriendly" :key="index">
|
||||
<PaginationItem v-if="typeof pageNum === 'number'" :value="pageNum" :is-active="pageNum === currentPage.friendly">
|
||||
{{ pageNum }}
|
||||
</PaginationItem>
|
||||
<span v-else class="px-2 text-muted-foreground">{{ pageNum }}</span>
|
||||
</template>
|
||||
|
||||
<PaginationNext>{{ t('pagination.next') }}</PaginationNext>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</template>
|
||||
</TabsContent>
|
||||
|
||||
<!-- 中立NPC -->
|
||||
<TabsContent value="neutral" class="space-y-4 mt-6">
|
||||
<div v-if="neutralNpcs.length === 0" class="text-center py-12 text-muted-foreground">
|
||||
{{ t('diplomacy.noNeutralNpcs') }}
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<NpcRelationCard v-for="npc in paginatedNeutralNpcs" :key="npc.id" :npc="npc" :relation="getRelation(npc.id)" />
|
||||
</div>
|
||||
<Pagination
|
||||
v-if="totalPagesNeutral > 1"
|
||||
v-model:page="currentPage.neutral"
|
||||
:total="neutralNpcs.length"
|
||||
:items-per-page="ITEMS_PER_PAGE"
|
||||
:sibling-count="1"
|
||||
show-edges
|
||||
class="mt-6"
|
||||
>
|
||||
<PaginationContent>
|
||||
<PaginationPrevious>{{ t('pagination.previous') }}</PaginationPrevious>
|
||||
|
||||
<template v-for="(pageNum, index) in pageNumbersNeutral" :key="index">
|
||||
<PaginationItem v-if="typeof pageNum === 'number'" :value="pageNum" :is-active="pageNum === currentPage.neutral">
|
||||
{{ pageNum }}
|
||||
</PaginationItem>
|
||||
<span v-else class="px-2 text-muted-foreground">{{ pageNum }}</span>
|
||||
</template>
|
||||
|
||||
<PaginationNext>{{ t('pagination.next') }}</PaginationNext>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</template>
|
||||
</TabsContent>
|
||||
|
||||
<!-- 敌对NPC -->
|
||||
<TabsContent value="hostile" class="space-y-4 mt-6">
|
||||
<div v-if="hostileNpcs.length === 0" class="text-center py-12 text-muted-foreground">
|
||||
{{ t('diplomacy.noHostileNpcs') }}
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<NpcRelationCard v-for="npc in paginatedHostileNpcs" :key="npc.id" :npc="npc" :relation="getRelation(npc.id)" />
|
||||
</div>
|
||||
<Pagination
|
||||
v-if="totalPagesHostile > 1"
|
||||
v-model:page="currentPage.hostile"
|
||||
:total="hostileNpcs.length"
|
||||
:items-per-page="ITEMS_PER_PAGE"
|
||||
:sibling-count="1"
|
||||
show-edges
|
||||
class="mt-6"
|
||||
>
|
||||
<PaginationContent>
|
||||
<PaginationPrevious>{{ t('pagination.previous') }}</PaginationPrevious>
|
||||
|
||||
<template v-for="(pageNum, index) in pageNumbersHostile" :key="index">
|
||||
<PaginationItem v-if="typeof pageNum === 'number'" :value="pageNum" :is-active="pageNum === currentPage.hostile">
|
||||
{{ pageNum }}
|
||||
</PaginationItem>
|
||||
<span v-else class="px-2 text-muted-foreground">{{ pageNum }}</span>
|
||||
</template>
|
||||
|
||||
<PaginationNext>{{ t('pagination.next') }}</PaginationNext>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</template>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<!-- 外交报告历史 -->
|
||||
<Card v-if="diplomaticReports.length > 0">
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('diplomacy.recentEvents') }}</CardTitle>
|
||||
<CardDescription>{{ t('diplomacy.recentEventsDescription') }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="space-y-2 max-h-96 overflow-y-auto">
|
||||
<div
|
||||
v-for="report in diplomaticReports"
|
||||
:key="report.id"
|
||||
class="flex items-start gap-3 p-3 rounded-lg border bg-card hover:bg-accent/50 transition-colors"
|
||||
>
|
||||
<div class="flex-shrink-0 mt-0.5">
|
||||
<component :is="getEventIcon(report.eventType)" class="h-5 w-5" :class="getEventIconColor(report.reputationChange)" />
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<span class="font-medium">{{ report.npcName }}</span>
|
||||
<Badge :variant="getReputationBadgeVariant(report.reputationChange)" class="text-xs">
|
||||
{{ report.reputationChange > 0 ? '+' : '' }}{{ report.reputationChange }}
|
||||
</Badge>
|
||||
<Badge :variant="getStatusBadgeVariant(report.newStatus)" class="text-xs">
|
||||
{{ getStatusText(report.newStatus) }}
|
||||
</Badge>
|
||||
</div>
|
||||
<p class="text-sm text-muted-foreground">{{ report.message }}</p>
|
||||
<p class="text-xs text-muted-foreground mt-1">{{ formatTime(Date.now() - report.timestamp) }} {{ t('diplomacy.ago') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useNPCStore } from '@/stores/npcStore'
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Pagination, PaginationContent, PaginationItem, PaginationNext, PaginationPrevious } from '@/components/ui/pagination'
|
||||
import NpcRelationCard from '@/components/NpcRelationCard.vue'
|
||||
import { Gift, Sword, Eye, Trash2 } from 'lucide-vue-next'
|
||||
import { RelationStatus, DiplomaticEventType } from '@/types/game'
|
||||
import type { DiplomaticRelation, DiplomaticReport } from '@/types/game'
|
||||
import { formatTime } from '@/utils/format'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const npcStore = useNPCStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
const activeTab = ref('all')
|
||||
|
||||
// 分页状态
|
||||
const ITEMS_PER_PAGE = 20
|
||||
const currentPage = ref<Record<string, number>>({
|
||||
all: 1,
|
||||
friendly: 1,
|
||||
neutral: 1,
|
||||
hostile: 1
|
||||
})
|
||||
|
||||
// 获取玩家对NPC的关系
|
||||
const getRelation = (npcId: string): DiplomaticRelation | undefined => {
|
||||
return gameStore.player.diplomaticRelations?.[npcId]
|
||||
}
|
||||
|
||||
// 按关系状态分类NPC
|
||||
const allNpcs = computed(() => npcStore.npcs)
|
||||
|
||||
const friendlyNpcs = computed(() => {
|
||||
return npcStore.npcs.filter(npc => {
|
||||
const relation = getRelation(npc.id)
|
||||
return relation?.status === RelationStatus.Friendly
|
||||
})
|
||||
})
|
||||
|
||||
const neutralNpcs = computed(() => {
|
||||
return npcStore.npcs.filter(npc => {
|
||||
const relation = getRelation(npc.id)
|
||||
return !relation || relation.status === RelationStatus.Neutral
|
||||
})
|
||||
})
|
||||
|
||||
const hostileNpcs = computed(() => {
|
||||
return npcStore.npcs.filter(npc => {
|
||||
const relation = getRelation(npc.id)
|
||||
return relation?.status === RelationStatus.Hostile
|
||||
})
|
||||
})
|
||||
|
||||
// 分页辅助函数
|
||||
const getPaginatedNpcs = (npcs: typeof allNpcs.value, tabKey: string) => {
|
||||
const page = currentPage.value[tabKey] || 1
|
||||
const start = (page - 1) * ITEMS_PER_PAGE
|
||||
const end = start + ITEMS_PER_PAGE
|
||||
return npcs.slice(start, end)
|
||||
}
|
||||
|
||||
const getTotalPages = (npcs: typeof allNpcs.value) => {
|
||||
return Math.ceil(npcs.length / ITEMS_PER_PAGE)
|
||||
}
|
||||
|
||||
// 分页后的NPC列表
|
||||
const paginatedAllNpcs = computed(() => getPaginatedNpcs(allNpcs.value, 'all'))
|
||||
const paginatedFriendlyNpcs = computed(() => getPaginatedNpcs(friendlyNpcs.value, 'friendly'))
|
||||
const paginatedNeutralNpcs = computed(() => getPaginatedNpcs(neutralNpcs.value, 'neutral'))
|
||||
const paginatedHostileNpcs = computed(() => getPaginatedNpcs(hostileNpcs.value, 'hostile'))
|
||||
|
||||
// 总页数
|
||||
const totalPagesAll = computed(() => getTotalPages(allNpcs.value))
|
||||
const totalPagesFriendly = computed(() => getTotalPages(friendlyNpcs.value))
|
||||
const totalPagesNeutral = computed(() => getTotalPages(neutralNpcs.value))
|
||||
const totalPagesHostile = computed(() => getTotalPages(hostileNpcs.value))
|
||||
|
||||
// 生成页码列表(用于分页UI)
|
||||
const getPageNumbers = (currentPageNum: number, totalPages: number) => {
|
||||
const pages: (number | string)[] = []
|
||||
const maxVisible = 5 // 最多显示5个页码
|
||||
|
||||
if (totalPages <= maxVisible) {
|
||||
// 如果总页数少于等于5,显示全部
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
pages.push(i)
|
||||
}
|
||||
} else {
|
||||
// 总是显示第1页
|
||||
pages.push(1)
|
||||
|
||||
if (currentPageNum > 3) {
|
||||
pages.push('...')
|
||||
}
|
||||
|
||||
// 计算中间显示的页码范围
|
||||
const start = Math.max(2, currentPageNum - 1)
|
||||
const end = Math.min(totalPages - 1, currentPageNum + 1)
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
pages.push(i)
|
||||
}
|
||||
|
||||
if (currentPageNum < totalPages - 2) {
|
||||
pages.push('...')
|
||||
}
|
||||
|
||||
// 总是显示最后一页
|
||||
pages.push(totalPages)
|
||||
}
|
||||
|
||||
return pages
|
||||
}
|
||||
|
||||
// 各标签页的页码列表
|
||||
const pageNumbersAll = computed(() => getPageNumbers(currentPage.value.all || 1, totalPagesAll.value))
|
||||
const pageNumbersFriendly = computed(() => getPageNumbers(currentPage.value.friendly || 1, totalPagesFriendly.value))
|
||||
const pageNumbersNeutral = computed(() => getPageNumbers(currentPage.value.neutral || 1, totalPagesNeutral.value))
|
||||
const pageNumbersHostile = computed(() => getPageNumbers(currentPage.value.hostile || 1, totalPagesHostile.value))
|
||||
|
||||
// 外交报告(最近20条,按时间倒序)
|
||||
const diplomaticReports = computed(() => {
|
||||
const reports = gameStore.player.diplomaticReports || []
|
||||
return [...reports].sort((a, b) => b.timestamp - a.timestamp).slice(0, 20)
|
||||
})
|
||||
|
||||
// 获取事件图标
|
||||
const getEventIcon = (eventType: DiplomaticReport['eventType']) => {
|
||||
switch (eventType) {
|
||||
case DiplomaticEventType.GiftResources:
|
||||
return Gift
|
||||
case DiplomaticEventType.Attack:
|
||||
case DiplomaticEventType.AllyAttacked:
|
||||
return Sword
|
||||
case DiplomaticEventType.Spy:
|
||||
return Eye
|
||||
case DiplomaticEventType.StealDebris:
|
||||
return Trash2
|
||||
default:
|
||||
return Gift
|
||||
}
|
||||
}
|
||||
|
||||
// 获取事件图标颜色
|
||||
const getEventIconColor = (reputationChange: number) => {
|
||||
if (reputationChange > 0) return 'text-green-600 dark:text-green-400'
|
||||
if (reputationChange < 0) return 'text-red-600 dark:text-red-400'
|
||||
return 'text-muted-foreground'
|
||||
}
|
||||
|
||||
// 获取好感度Badge样式
|
||||
const getReputationBadgeVariant = (change: number) => {
|
||||
if (change > 0) return 'default'
|
||||
if (change < 0) return 'destructive'
|
||||
return 'secondary'
|
||||
}
|
||||
|
||||
// 获取关系状态Badge样式
|
||||
const getStatusBadgeVariant = (status: RelationStatus) => {
|
||||
switch (status) {
|
||||
case RelationStatus.Friendly:
|
||||
return 'default'
|
||||
case RelationStatus.Hostile:
|
||||
return 'destructive'
|
||||
default:
|
||||
return 'secondary'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取关系状态文本
|
||||
const getStatusText = (status: RelationStatus) => {
|
||||
switch (status) {
|
||||
case RelationStatus.Friendly:
|
||||
return t('diplomacy.status.friendly')
|
||||
case RelationStatus.Hostile:
|
||||
return t('diplomacy.status.hostile')
|
||||
default:
|
||||
return t('diplomacy.status.neutral')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -6,297 +6,330 @@
|
||||
<h1 class="text-2xl sm:text-3xl font-bold">{{ t('fleetView.title') }}</h1>
|
||||
|
||||
<!-- 标签切换 -->
|
||||
<div class="flex gap-2 border-b">
|
||||
<Button @click="activeTab = 'fleet'" :variant="activeTab === 'fleet' ? 'default' : 'ghost'" class="rounded-b-none">
|
||||
{{ t('fleetView.fleetOverview') }}
|
||||
</Button>
|
||||
<Button @click="activeTab = 'send'" :variant="activeTab === 'send' ? 'default' : 'ghost'" class="rounded-b-none">
|
||||
{{ t('fleetView.sendFleet') }}
|
||||
</Button>
|
||||
<Button @click="activeTab = 'missions'" :variant="activeTab === 'missions' ? 'default' : 'ghost'" class="rounded-b-none">
|
||||
{{ t('fleetView.flightMissions') }}
|
||||
<Badge v-if="gameStore.player.fleetMissions.length > 0" variant="secondary" class="ml-1">
|
||||
{{ gameStore.player.fleetMissions.length }}
|
||||
</Badge>
|
||||
</Button>
|
||||
</div>
|
||||
<Tabs v-model="activeTab" class="w-full">
|
||||
<TabsList class="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="fleet">{{ t('fleetView.fleetOverview') }}</TabsTrigger>
|
||||
<TabsTrigger value="send">{{ t('fleetView.sendFleet') }}</TabsTrigger>
|
||||
<TabsTrigger value="missions">
|
||||
{{ t('fleetView.flightMissions') }}
|
||||
<Badge v-if="gameStore.player.fleetMissions.length > 0" variant="destructive" class="ml-1">
|
||||
{{ gameStore.player.fleetMissions.length }}
|
||||
</Badge>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<!-- 舰队总览 -->
|
||||
<div v-if="activeTab === 'fleet'">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('fleetView.currentPlanetFleet') }}</CardTitle>
|
||||
<CardDescription>
|
||||
{{ planet.name }} [{{ planet.position.galaxy }}:{{ planet.position.system }}:{{ planet.position.position }}]
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3 sm:gap-4">
|
||||
<div v-for="(count, shipType) in planet.fleet" :key="shipType" class="p-3 sm:p-4 border rounded-lg space-y-2">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 class="font-semibold text-sm sm:text-base">{{ SHIPS[shipType].name }}</h3>
|
||||
<p class="text-xl sm:text-2xl font-bold">{{ formatNumber(count) }}</p>
|
||||
<!-- 舰队总览 -->
|
||||
<TabsContent value="fleet" class="mt-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('fleetView.currentPlanetFleet') }}</CardTitle>
|
||||
<CardDescription>
|
||||
{{ planet.name }} [{{ planet.position.galaxy }}:{{ planet.position.system }}:{{ planet.position.position }}]
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3 sm:gap-4">
|
||||
<div v-for="(count, shipType) in planet.fleet" :key="shipType" class="p-3 sm:p-4 border rounded-lg space-y-2">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 class="font-semibold text-sm sm:text-base">{{ SHIPS[shipType].name }}</h3>
|
||||
<p class="text-xl sm:text-2xl font-bold">{{ formatNumber(count) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-xs sm:text-sm text-muted-foreground space-y-1">
|
||||
<p>{{ t('fleetView.attack') }}: {{ SHIPS[shipType].attack }}</p>
|
||||
<p>{{ t('fleetView.shield') }}: {{ SHIPS[shipType].shield }}</p>
|
||||
<p>{{ t('fleetView.armor') }}: {{ SHIPS[shipType].armor }}</p>
|
||||
<p>{{ t('fleetView.speed') }}: {{ formatNumber(SHIPS[shipType].speed) }}</p>
|
||||
<p>{{ t('fleetView.cargo') }}: {{ formatNumber(SHIPS[shipType].cargoCapacity) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-xs sm:text-sm text-muted-foreground space-y-1">
|
||||
<p>{{ t('fleetView.attack') }}: {{ SHIPS[shipType].attack }}</p>
|
||||
<p>{{ t('fleetView.shield') }}: {{ SHIPS[shipType].shield }}</p>
|
||||
<p>{{ t('fleetView.armor') }}: {{ SHIPS[shipType].armor }}</p>
|
||||
<p>{{ t('fleetView.speed') }}: {{ formatNumber(SHIPS[shipType].speed) }}</p>
|
||||
<p>{{ t('fleetView.cargo') }}: {{ formatNumber(SHIPS[shipType].cargoCapacity) }}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<!-- 派遣舰队 -->
|
||||
<TabsContent value="send" class="mt-4 space-y-4">
|
||||
<!-- 舰队任务槽位信息 -->
|
||||
<Card>
|
||||
<CardContent class="py-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-sm font-medium">{{ t('fleetView.fleetMissionSlots') }}:</span>
|
||||
<span class="text-sm font-bold">{{ gameStore.player.fleetMissions.length }} / {{ maxFleetMissions }}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 选择舰队 -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('fleetView.selectFleet') }}</CardTitle>
|
||||
<CardDescription>{{ t('fleetView.selectFleetDescription') }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4">
|
||||
<div v-for="(count, shipType) in planet.fleet" :key="shipType" class="space-y-2">
|
||||
<Label :for="`ship-${shipType}`" class="text-xs sm:text-sm">
|
||||
{{ SHIPS[shipType].name }} ({{ t('fleetView.available') }}: {{ count }})
|
||||
</Label>
|
||||
<div class="flex gap-2">
|
||||
<Input
|
||||
:id="`ship-${shipType}`"
|
||||
v-model.number="selectedFleet[shipType]"
|
||||
type="number"
|
||||
min="0"
|
||||
:max="count"
|
||||
placeholder="0"
|
||||
class="text-sm"
|
||||
/>
|
||||
<Button @click="selectedFleet[shipType] = count" variant="outline" size="sm">{{ t('fleetView.all') }}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 派遣舰队 -->
|
||||
<div v-if="activeTab === 'send'" class="space-y-4">
|
||||
<!-- 舰队任务槽位信息 -->
|
||||
<Card>
|
||||
<CardContent class="py-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-sm font-medium">{{ t('fleetView.fleetMissionSlots') }}:</span>
|
||||
<span class="text-sm font-bold">{{ gameStore.player.fleetMissions.length }} / {{ maxFleetMissions }}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<!-- 目标坐标 -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('fleetView.targetCoordinates') }}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-3 gap-2 sm:gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="galaxy" class="text-xs sm:text-sm">{{ t('fleetView.galaxy') }}</Label>
|
||||
<Input id="galaxy" v-model.number="targetPosition.galaxy" type="number" min="1" max="9" placeholder="1" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="system" class="text-xs sm:text-sm">{{ t('fleetView.system') }}</Label>
|
||||
<Input id="system" v-model.number="targetPosition.system" type="number" min="1" max="10" placeholder="1" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="position" class="text-xs sm:text-sm">{{ t('fleetView.position') }}</Label>
|
||||
<Input id="position" v-model.number="targetPosition.position" type="number" min="1" max="10" placeholder="1" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 选择舰队 -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('fleetView.selectFleet') }}</CardTitle>
|
||||
<CardDescription>{{ t('fleetView.selectFleetDescription') }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4">
|
||||
<div v-for="(count, shipType) in planet.fleet" :key="shipType" class="space-y-2">
|
||||
<Label :for="`ship-${shipType}`" class="text-xs sm:text-sm">
|
||||
{{ SHIPS[shipType].name }} ({{ t('fleetView.available') }}: {{ count }})
|
||||
</Label>
|
||||
<div class="flex gap-2">
|
||||
<!-- 任务类型 -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('fleetView.missionType') }}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
||||
<Button
|
||||
v-for="mission in availableMissions"
|
||||
:key="mission.type"
|
||||
@click="selectedMission = mission.type"
|
||||
:variant="selectedMission === mission.type ? 'default' : 'outline'"
|
||||
class="justify-start"
|
||||
>
|
||||
<component :is="mission.icon" class="h-4 w-4 mr-2" />
|
||||
{{ mission.name }}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 运输资源(仅运输任务) -->
|
||||
<Card v-if="selectedMission === MissionType.Transport">
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('fleetView.transportResources') }}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<!-- 赠送模式切换(仅当目标是NPC星球时显示) -->
|
||||
<div v-if="targetNpc" class="mb-4 p-3 border rounded-lg bg-muted/50">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<Checkbox id="gift-mode" :default-value="isGiftMode" />
|
||||
<Label for="gift-mode" class="flex items-center gap-2 cursor-pointer">
|
||||
<Gift class="h-4 w-4" />
|
||||
{{ t('fleetView.giftMode') }}
|
||||
</Label>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">{{ t('fleetView.giftModeDescription') }} {{ targetNpc.name }}</p>
|
||||
<div v-if="isGiftMode && (cargo.metal > 0 || cargo.crystal > 0 || cargo.deuterium > 0)" class="mt-2 text-xs">
|
||||
<span class="text-muted-foreground">{{ t('fleetView.estimatedReputationGain') }}:</span>
|
||||
<span class="ml-1 font-semibold text-green-600 dark:text-green-400">+{{ calculateGiftReputation() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3 sm:gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="cargo-metal" class="text-xs sm:text-sm flex items-center gap-2">
|
||||
<ResourceIcon type="metal" size="sm" />
|
||||
{{ t('resources.metal') }} ({{ t('fleetView.available') }}: {{ formatNumber(planet.resources.metal) }})
|
||||
</Label>
|
||||
<Input id="cargo-metal" v-model.number="cargo.metal" type="number" min="0" :max="planet.resources.metal" placeholder="0" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="cargo-crystal" class="text-xs sm:text-sm flex items-center gap-2">
|
||||
<ResourceIcon type="crystal" size="sm" />
|
||||
{{ t('resources.crystal') }} ({{ t('fleetView.available') }}: {{ formatNumber(planet.resources.crystal) }})
|
||||
</Label>
|
||||
<Input
|
||||
:id="`ship-${shipType}`"
|
||||
v-model.number="selectedFleet[shipType]"
|
||||
id="cargo-crystal"
|
||||
v-model.number="cargo.crystal"
|
||||
type="number"
|
||||
min="0"
|
||||
:max="count"
|
||||
:max="planet.resources.crystal"
|
||||
placeholder="0"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="cargo-deuterium" class="text-xs sm:text-sm flex items-center gap-2">
|
||||
<ResourceIcon type="deuterium" size="sm" />
|
||||
{{ t('resources.deuterium') }} ({{ t('fleetView.available') }}: {{ formatNumber(planet.resources.deuterium) }})
|
||||
</Label>
|
||||
<Input
|
||||
id="cargo-deuterium"
|
||||
v-model.number="cargo.deuterium"
|
||||
type="number"
|
||||
min="0"
|
||||
:max="planet.resources.deuterium"
|
||||
placeholder="0"
|
||||
class="text-sm"
|
||||
/>
|
||||
<Button @click="selectedFleet[shipType] = count" variant="outline" size="sm">{{ t('fleetView.all') }}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<p class="text-xs sm:text-sm text-muted-foreground mt-2">
|
||||
{{ t('fleetView.totalCargoCapacity') }}: {{ formatNumber(getTotalCargoCapacity()) }} | {{ t('fleetView.used') }}:
|
||||
{{ formatNumber(getTotalCargo()) }}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 目标坐标 -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('fleetView.targetCoordinates') }}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-3 gap-2 sm:gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="galaxy" class="text-xs sm:text-sm">{{ t('fleetView.galaxy') }}</Label>
|
||||
<Input id="galaxy" v-model.number="targetPosition.galaxy" type="number" min="1" max="9" placeholder="1" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="system" class="text-xs sm:text-sm">{{ t('fleetView.system') }}</Label>
|
||||
<Input id="system" v-model.number="targetPosition.system" type="number" min="1" max="10" placeholder="1" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="position" class="text-xs sm:text-sm">{{ t('fleetView.position') }}</Label>
|
||||
<Input id="position" v-model.number="targetPosition.position" type="number" min="1" max="10" placeholder="1" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 任务类型 -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('fleetView.missionType') }}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
||||
<Button
|
||||
v-for="mission in availableMissions"
|
||||
:key="mission.type"
|
||||
@click="selectedMission = mission.type"
|
||||
:variant="selectedMission === mission.type ? 'default' : 'outline'"
|
||||
class="justify-start"
|
||||
>
|
||||
<component :is="mission.icon" class="h-4 w-4 mr-2" />
|
||||
{{ mission.name }}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 运输资源(仅运输任务) -->
|
||||
<Card v-if="selectedMission === MissionType.Transport">
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('fleetView.transportResources') }}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3 sm:gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="cargo-metal" class="text-xs sm:text-sm flex items-center gap-2">
|
||||
<ResourceIcon type="metal" size="sm" />
|
||||
{{ t('resources.metal') }} ({{ t('fleetView.available') }}: {{ formatNumber(planet.resources.metal) }})
|
||||
</Label>
|
||||
<Input id="cargo-metal" v-model.number="cargo.metal" type="number" min="0" :max="planet.resources.metal" placeholder="0" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="cargo-crystal" class="text-xs sm:text-sm flex items-center gap-2">
|
||||
<ResourceIcon type="crystal" size="sm" />
|
||||
{{ t('resources.crystal') }} ({{ t('fleetView.available') }}: {{ formatNumber(planet.resources.crystal) }})
|
||||
</Label>
|
||||
<Input
|
||||
id="cargo-crystal"
|
||||
v-model.number="cargo.crystal"
|
||||
type="number"
|
||||
min="0"
|
||||
:max="planet.resources.crystal"
|
||||
placeholder="0"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="cargo-deuterium" class="text-xs sm:text-sm flex items-center gap-2">
|
||||
<!-- 任务信息 -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('fleetView.missionInfo') }}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-2">
|
||||
<div class="flex justify-between text-xs sm:text-sm">
|
||||
<span class="text-muted-foreground">{{ t('fleetView.fuelConsumption') }}:</span>
|
||||
<span class="flex items-center gap-1.5">
|
||||
<ResourceIcon type="deuterium" size="sm" />
|
||||
{{ t('resources.deuterium') }} ({{ t('fleetView.available') }}: {{ formatNumber(planet.resources.deuterium) }})
|
||||
</Label>
|
||||
<Input
|
||||
id="cargo-deuterium"
|
||||
v-model.number="cargo.deuterium"
|
||||
type="number"
|
||||
min="0"
|
||||
:max="planet.resources.deuterium"
|
||||
placeholder="0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs sm:text-sm text-muted-foreground mt-2">
|
||||
{{ t('fleetView.totalCargoCapacity') }}: {{ formatNumber(getTotalCargoCapacity()) }} | {{ t('fleetView.used') }}:
|
||||
{{ formatNumber(getTotalCargo()) }}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 任务信息 -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('fleetView.missionInfo') }}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-2">
|
||||
<div class="flex justify-between text-xs sm:text-sm">
|
||||
<span class="text-muted-foreground">{{ t('fleetView.fuelConsumption') }}:</span>
|
||||
<span class="flex items-center gap-1.5">
|
||||
<ResourceIcon type="deuterium" size="sm" />
|
||||
<span :class="getFuelConsumption() > planet.resources.deuterium ? 'text-red-600 dark:text-red-400 font-medium' : ''">
|
||||
{{ formatNumber(getFuelConsumption()) }}
|
||||
<span :class="getFuelConsumption() > planet.resources.deuterium ? 'text-red-600 dark:text-red-400 font-medium' : ''">
|
||||
{{ formatNumber(getFuelConsumption()) }}
|
||||
</span>
|
||||
<span class="text-muted-foreground">/ {{ formatNumber(planet.resources.deuterium) }}</span>
|
||||
</span>
|
||||
<span class="text-muted-foreground">/ {{ formatNumber(planet.resources.deuterium) }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="Object.values(selectedFleet).some(c => c > 0)" class="flex justify-between text-xs sm:text-sm">
|
||||
<span class="text-muted-foreground">{{ t('fleetView.flightTime') }}:</span>
|
||||
<span>{{ formatTime(getFlightTime()) }}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 派遣按钮 -->
|
||||
<Button @click="handleSendFleet" :disabled="!canSendFleet()" class="w-full" size="lg">{{ t('fleetView.sendFleet') }}</Button>
|
||||
</div>
|
||||
|
||||
<!-- 飞行任务 -->
|
||||
<div v-if="activeTab === 'missions'" class="space-y-4">
|
||||
<Card v-if="gameStore.player.fleetMissions.length === 0">
|
||||
<CardContent class="py-8 text-center text-muted-foreground">{{ t('fleetView.noFlightMissions') }}</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card v-for="mission in gameStore.player.fleetMissions" :key="mission.id">
|
||||
<CardHeader>
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<CardTitle class="text-base sm:text-lg">{{ getMissionName(mission.missionType) }}</CardTitle>
|
||||
<CardDescription class="text-xs sm:text-sm">
|
||||
{{ getPlanetName(mission.originPlanetId) }} → [{{ mission.targetPosition.galaxy }}:{{ mission.targetPosition.system }}:{{
|
||||
mission.targetPosition.position
|
||||
}}]
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Badge :variant="mission.status === 'outbound' ? 'default' : 'secondary'">
|
||||
{{ mission.status === 'outbound' ? t('fleetView.outbound') : t('fleetView.returning') }}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-3">
|
||||
<!-- 舰队组成 -->
|
||||
<div>
|
||||
<p class="text-xs sm:text-sm font-medium mb-2">{{ t('fleetView.fleetComposition') }}:</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Badge v-for="(count, shipType) in mission.fleet" :key="shipType" variant="outline">
|
||||
{{ SHIPS[shipType].name }}: {{ count }}
|
||||
<div v-if="Object.values(selectedFleet).some(c => c > 0)" class="flex justify-between text-xs sm:text-sm">
|
||||
<span class="text-muted-foreground">{{ t('fleetView.flightTime') }}:</span>
|
||||
<span>{{ formatTime(getFlightTime()) }}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 派遣按钮 -->
|
||||
<Button @click="handleSendFleet" :disabled="!canSendFleet()" class="w-full" size="lg">{{ t('fleetView.sendFleet') }}</Button>
|
||||
</TabsContent>
|
||||
|
||||
<!-- 飞行任务 -->
|
||||
<TabsContent value="missions" class="mt-4 space-y-4">
|
||||
<Card v-if="gameStore.player.fleetMissions.length === 0">
|
||||
<CardContent class="py-8 text-center text-muted-foreground">{{ t('fleetView.noFlightMissions') }}</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card v-for="mission in gameStore.player.fleetMissions" :key="mission.id">
|
||||
<CardHeader>
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<CardTitle class="text-base sm:text-lg">{{ getMissionName(mission.missionType) }}</CardTitle>
|
||||
<CardDescription class="text-xs sm:text-sm">
|
||||
{{ getPlanetName(mission.originPlanetId) }} → [{{ mission.targetPosition.galaxy }}:{{ mission.targetPosition.system }}:{{
|
||||
mission.targetPosition.position
|
||||
}}]
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Badge :variant="mission.status === 'outbound' ? 'default' : 'secondary'">
|
||||
{{ mission.status === 'outbound' ? t('fleetView.outbound') : t('fleetView.returning') }}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 携带资源 -->
|
||||
<div v-if="mission.cargo.metal > 0 || mission.cargo.crystal > 0 || mission.cargo.deuterium > 0 || mission.cargo.darkMatter > 0">
|
||||
<p class="text-xs sm:text-sm font-medium mb-2">{{ t('fleetView.carryingResources') }}:</p>
|
||||
<div class="flex flex-wrap gap-2 text-xs">
|
||||
<span v-if="mission.cargo.metal > 0" class="flex items-center gap-1">
|
||||
<ResourceIcon type="metal" size="sm" />
|
||||
{{ formatNumber(mission.cargo.metal) }}
|
||||
</span>
|
||||
<span v-if="mission.cargo.crystal > 0" class="flex items-center gap-1">
|
||||
<ResourceIcon type="crystal" size="sm" />
|
||||
{{ formatNumber(mission.cargo.crystal) }}
|
||||
</span>
|
||||
<span v-if="mission.cargo.deuterium > 0" class="flex items-center gap-1">
|
||||
<ResourceIcon type="deuterium" size="sm" />
|
||||
{{ formatNumber(mission.cargo.deuterium) }}
|
||||
</span>
|
||||
<span v-if="mission.cargo.darkMatter > 0" class="flex items-center gap-1">
|
||||
<ResourceIcon type="darkMatter" size="sm" />
|
||||
{{ formatNumber(mission.cargo.darkMatter) }}
|
||||
</span>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-3">
|
||||
<!-- 舰队组成 -->
|
||||
<div>
|
||||
<p class="text-xs sm:text-sm font-medium mb-2">{{ t('fleetView.fleetComposition') }}:</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Badge v-for="(count, shipType) in mission.fleet" :key="shipType" variant="outline">
|
||||
{{ SHIPS[shipType].name }}: {{ count }}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 进度条 -->
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between text-xs sm:text-sm">
|
||||
<span>{{ mission.status === 'outbound' ? t('fleetView.arrivalTime') : t('fleetView.returnTime') }}:</span>
|
||||
<span>{{ formatTime(getRemainingTime(mission)) }}</span>
|
||||
<!-- 携带资源 -->
|
||||
<div v-if="mission.cargo.metal > 0 || mission.cargo.crystal > 0 || mission.cargo.deuterium > 0 || mission.cargo.darkMatter > 0">
|
||||
<p class="text-xs sm:text-sm font-medium mb-2">{{ t('fleetView.carryingResources') }}:</p>
|
||||
<div class="flex flex-wrap gap-2 text-xs">
|
||||
<span v-if="mission.cargo.metal > 0" class="flex items-center gap-1">
|
||||
<ResourceIcon type="metal" size="sm" />
|
||||
{{ formatNumber(mission.cargo.metal) }}
|
||||
</span>
|
||||
<span v-if="mission.cargo.crystal > 0" class="flex items-center gap-1">
|
||||
<ResourceIcon type="crystal" size="sm" />
|
||||
{{ formatNumber(mission.cargo.crystal) }}
|
||||
</span>
|
||||
<span v-if="mission.cargo.deuterium > 0" class="flex items-center gap-1">
|
||||
<ResourceIcon type="deuterium" size="sm" />
|
||||
{{ formatNumber(mission.cargo.deuterium) }}
|
||||
</span>
|
||||
<span v-if="mission.cargo.darkMatter > 0" class="flex items-center gap-1">
|
||||
<ResourceIcon type="darkMatter" size="sm" />
|
||||
{{ formatNumber(mission.cargo.darkMatter) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Progress :model-value="getMissionProgress(mission)" />
|
||||
</div>
|
||||
|
||||
<!-- 操作 -->
|
||||
<div class="flex gap-2">
|
||||
<Button v-if="mission.status === 'outbound'" @click="handleRecallFleet(mission.id)" variant="outline" size="sm" class="w-full">
|
||||
{{ t('fleetView.recallFleet') }}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<!-- 进度条 -->
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between text-xs sm:text-sm">
|
||||
<span>{{ mission.status === 'outbound' ? t('fleetView.arrivalTime') : t('fleetView.returnTime') }}:</span>
|
||||
<span>{{ formatTime(getRemainingTime(mission)) }}</span>
|
||||
</div>
|
||||
<Progress :model-value="getMissionProgress(mission)" />
|
||||
</div>
|
||||
|
||||
<!-- 操作 -->
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
v-if="mission.status === 'outbound'"
|
||||
@click="handleRecallFleet(mission.id)"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="w-full"
|
||||
>
|
||||
{{ t('fleetView.recallFleet') }}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<!-- 提示对话框 -->
|
||||
<AlertDialog ref="alertDialog" />
|
||||
<AlertDialog :open="alertDialogOpen" @update:open="alertDialogOpen = $event">
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{{ alertDialogTitle }}</AlertDialogTitle>
|
||||
<AlertDialogDescription class="whitespace-pre-line">
|
||||
{{ alertDialogMessage }}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogAction>{{ t('common.confirm') }}</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useUniverseStore } from '@/stores/universeStore'
|
||||
import { useNPCStore } from '@/stores/npcStore'
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
import { useGameConfig } from '@/composables/useGameConfig'
|
||||
import { computed, ref, onMounted, onUnmounted } from 'vue'
|
||||
@@ -304,30 +337,46 @@
|
||||
import { ShipType, MissionType, BuildingType } from '@/types/game'
|
||||
import type { Fleet, Resources } from '@/types/game'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Progress } from '@/components/ui/progress'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import ResourceIcon from '@/components/ResourceIcon.vue'
|
||||
import AlertDialog from '@/components/AlertDialog.vue'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import UnlockRequirement from '@/components/UnlockRequirement.vue'
|
||||
import { Sword, Package, Rocket as RocketIcon, Eye, Users, Recycle, Skull } from 'lucide-vue-next'
|
||||
import { Sword, Package, Rocket as RocketIcon, Eye, Users, Recycle, Skull, Gift } from 'lucide-vue-next'
|
||||
import { formatNumber, formatTime } from '@/utils/format'
|
||||
import * as shipValidation from '@/logic/shipValidation'
|
||||
import * as fleetLogic from '@/logic/fleetLogic'
|
||||
import * as shipLogic from '@/logic/shipLogic'
|
||||
import * as officerLogic from '@/logic/officerLogic'
|
||||
import * as publicLogic from '@/logic/publicLogic'
|
||||
import * as diplomaticLogic from '@/logic/diplomaticLogic'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const gameStore = useGameStore()
|
||||
const universeStore = useUniverseStore()
|
||||
const npcStore = useNPCStore()
|
||||
const { t } = useI18n()
|
||||
const { SHIPS } = useGameConfig()
|
||||
const planet = computed(() => gameStore.currentPlanet)
|
||||
const alertDialog = ref<InstanceType<typeof AlertDialog> | null>(null)
|
||||
|
||||
// AlertDialog 状态
|
||||
const alertDialogOpen = ref(false)
|
||||
const alertDialogTitle = ref('')
|
||||
const alertDialogMessage = ref('')
|
||||
|
||||
// 当前时间(响应式)
|
||||
const currentTime = ref(Date.now())
|
||||
@@ -372,7 +421,7 @@
|
||||
currentTime.value = Date.now()
|
||||
}, 1000) // 每秒更新一次
|
||||
|
||||
const { galaxy, system, position, mission } = route.query
|
||||
const { galaxy, system, position, mission, gift } = route.query
|
||||
|
||||
// 如果有参数,填充数据
|
||||
if (galaxy || system || position) {
|
||||
@@ -388,6 +437,10 @@
|
||||
selectedMission.value = MissionType.Attack
|
||||
} else if (mission === 'colonize') {
|
||||
selectedMission.value = MissionType.Colonize
|
||||
} else if (gift === '1') {
|
||||
// 如果有gift参数,设置为运输任务并启用赠送模式
|
||||
selectedMission.value = MissionType.Transport
|
||||
isGiftMode.value = true
|
||||
}
|
||||
|
||||
// 自动切换到派遣舰队标签
|
||||
@@ -405,6 +458,26 @@
|
||||
}
|
||||
})
|
||||
|
||||
// 检查目标是否为NPC星球
|
||||
const targetNpc = computed(() => {
|
||||
return npcStore.npcs.find(npc =>
|
||||
npc.planets.some(
|
||||
p =>
|
||||
p.position.galaxy === targetPosition.value.galaxy &&
|
||||
p.position.system === targetPosition.value.system &&
|
||||
p.position.position === targetPosition.value.position
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
// 是否为赠送模式
|
||||
const isGiftMode = ref(false)
|
||||
|
||||
// 计算赠送的预估好感度增加值
|
||||
const calculateGiftReputation = (): number => {
|
||||
return diplomaticLogic.calculateGiftReputationGain(cargo.value)
|
||||
}
|
||||
|
||||
// 可用任务类型
|
||||
const availableMissions = computed(() => [
|
||||
{ type: MissionType.Attack, name: t('fleetView.attackMission'), icon: Sword },
|
||||
@@ -467,7 +540,8 @@
|
||||
if (!hasShips) return { valid: false, errorKey: 'fleetView.noShipsSelected' }
|
||||
|
||||
// 检查是否派遣到自己的星球
|
||||
if (planet.value) {
|
||||
// 回收任务和部署任务除外(回收残骸可能在同位置,部署可能到自己的月球)
|
||||
if (planet.value && selectedMission.value !== MissionType.Recycle && selectedMission.value !== MissionType.Deploy) {
|
||||
const isSamePlanet =
|
||||
targetPosition.value.galaxy === planet.value.position.galaxy &&
|
||||
targetPosition.value.system === planet.value.position.system &&
|
||||
@@ -541,6 +615,13 @@
|
||||
cargo,
|
||||
flightTime
|
||||
)
|
||||
|
||||
// 如果是赠送模式,标记任务
|
||||
if (missionType === MissionType.Transport && isGiftMode.value && targetNpc.value) {
|
||||
mission.isGift = true
|
||||
mission.giftTargetNpcId = targetNpc.value.id
|
||||
}
|
||||
|
||||
gameStore.player.fleetMissions.push(mission)
|
||||
return true
|
||||
}
|
||||
@@ -552,10 +633,9 @@
|
||||
// 验证是否可以派遣
|
||||
const validation = canSendFleet()
|
||||
if (!validation.valid) {
|
||||
alertDialog.value?.show({
|
||||
title: t('fleetView.sendFailed'),
|
||||
message: validation.errorKey ? t(validation.errorKey) : t('fleetView.sendFailedMessage')
|
||||
})
|
||||
alertDialogTitle.value = t('fleetView.sendFailed')
|
||||
alertDialogMessage.value = validation.errorKey ? t(validation.errorKey) : t('fleetView.sendFailedMessage')
|
||||
alertDialogOpen.value = true
|
||||
return
|
||||
}
|
||||
|
||||
@@ -580,12 +660,12 @@
|
||||
selectedFleet.value[key as ShipType] = 0
|
||||
})
|
||||
cargo.value = { metal: 0, crystal: 0, deuterium: 0, darkMatter: 0, energy: 0 }
|
||||
isGiftMode.value = false
|
||||
activeTab.value = 'missions'
|
||||
} else {
|
||||
alertDialog.value?.show({
|
||||
title: t('fleetView.sendFailed'),
|
||||
message: t('fleetView.sendFailedMessage')
|
||||
})
|
||||
alertDialogTitle.value = t('fleetView.sendFailed')
|
||||
alertDialogMessage.value = t('fleetView.sendFailedMessage')
|
||||
alertDialogOpen.value = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -599,10 +679,9 @@
|
||||
const handleRecallFleet = (missionId: string) => {
|
||||
const success = recallFleet(missionId)
|
||||
if (!success) {
|
||||
alertDialog.value?.show({
|
||||
title: t('fleetView.recallFailed'),
|
||||
message: t('fleetView.recallFailedMessage')
|
||||
})
|
||||
alertDialogTitle.value = t('fleetView.recallFailed')
|
||||
alertDialogMessage.value = t('fleetView.recallFailedMessage')
|
||||
alertDialogOpen.value = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,23 +26,14 @@
|
||||
|
||||
<!-- 标签切换 -->
|
||||
<div v-if="selectedPlanet" class="flex flex-wrap gap-2 border-b">
|
||||
<Button @click="activeTab = 'resources'" :variant="activeTab === 'resources' ? 'default' : 'ghost'" class="rounded-b-none">
|
||||
{{ t('gmView.resources') }}
|
||||
</Button>
|
||||
<Button @click="activeTab = 'buildings'" :variant="activeTab === 'buildings' ? 'default' : 'ghost'" class="rounded-b-none">
|
||||
{{ t('gmView.buildings') }}
|
||||
</Button>
|
||||
<Button @click="activeTab = 'research'" :variant="activeTab === 'research' ? 'default' : 'ghost'" class="rounded-b-none">
|
||||
{{ t('gmView.research') }}
|
||||
</Button>
|
||||
<Button @click="activeTab = 'ships'" :variant="activeTab === 'ships' ? 'default' : 'ghost'" class="rounded-b-none">
|
||||
{{ t('gmView.ships') }}
|
||||
</Button>
|
||||
<Button @click="activeTab = 'defense'" :variant="activeTab === 'defense' ? 'default' : 'ghost'" class="rounded-b-none">
|
||||
{{ t('gmView.defense') }}
|
||||
</Button>
|
||||
<Button @click="activeTab = 'officers'" :variant="activeTab === 'officers' ? 'default' : 'ghost'" class="rounded-b-none">
|
||||
{{ t('gmView.officers') }}
|
||||
<Button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.value"
|
||||
@click="activeTab = tab.value"
|
||||
:variant="activeTab === tab.value ? 'default' : 'ghost'"
|
||||
class="rounded-b-none"
|
||||
>
|
||||
{{ t(tab.label) }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -177,6 +168,62 @@
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- NPC测试 -->
|
||||
<Card class="border-primary">
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('gmView.npcTesting') || 'NPC Testing' }}</CardTitle>
|
||||
<CardDescription>{{ t('gmView.npcTestingDesc') || 'Test NPC spy and attack behavior' }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-3">
|
||||
<div class="space-y-2">
|
||||
<Label>{{ t('gmView.selectNPC') || 'Select NPC' }}</Label>
|
||||
<Select v-model="selectedNPCId">
|
||||
<SelectTrigger>
|
||||
<SelectValue :placeholder="t('gmView.chooseNPC') || 'Choose NPC'" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem v-for="npc in npcStore.npcs" :key="npc.id" :value="npc.id">{{ npc.name }} ({{ npc.difficulty }})</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label>{{ t('gmView.targetPlanet') || 'Target Planet' }}</Label>
|
||||
<Select v-model="targetPlanetIndex">
|
||||
<SelectTrigger>
|
||||
<SelectValue :placeholder="t('gmView.chooseTarget') || 'Choose Target Planet'" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem v-for="(planet, index) in gameStore.player.planets" :key="planet.id" :value="index.toString()">
|
||||
{{ planet.name }} ({{ planet.position.galaxy }}:{{ planet.position.system }}:{{ planet.position.position }})
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<Button @click="testNPCSpy" variant="outline" class="w-full" :disabled="!selectedNPC">
|
||||
{{ t('gmView.testSpy') || 'Test Spy' }}
|
||||
</Button>
|
||||
<Button @click="testNPCAttack" variant="outline" class="w-full" :disabled="!selectedNPC">
|
||||
{{ t('gmView.testAttack') || 'Test Attack' }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button @click="testNPCSpyAndAttack" variant="default" class="w-full" :disabled="!selectedNPC">
|
||||
{{ t('gmView.testSpyAndAttack') || 'Test Spy & Attack' }}
|
||||
</Button>
|
||||
|
||||
<Button @click="initializeNPCFleet" variant="secondary" class="w-full" :disabled="!selectedNPC">
|
||||
{{ t('gmView.initializeFleet') || 'Initialize NPC Fleet' }}
|
||||
</Button>
|
||||
|
||||
<Button @click="accelerateAllMissions" variant="secondary" class="w-full" :disabled="!selectedNPC">
|
||||
{{ t('gmView.accelerateMissions') || 'Accelerate All Missions (5s)' }}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 危险操作 -->
|
||||
<Card class="border-destructive">
|
||||
<CardHeader>
|
||||
@@ -193,6 +240,8 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useNPCStore } from '@/stores/npcStore'
|
||||
import { useUniverseStore } from '@/stores/universeStore'
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
import { useGameConfig } from '@/composables/useGameConfig'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
@@ -202,14 +251,19 @@
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { BuildingType, TechnologyType, ShipType, DefenseType, OfficerType } from '@/types/game'
|
||||
import * as npcBehaviorLogic from '@/logic/npcBehaviorLogic'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const npcStore = useNPCStore()
|
||||
const universeStore = useUniverseStore()
|
||||
const { t } = useI18n()
|
||||
const { BUILDINGS, TECHNOLOGIES, SHIPS, DEFENSES, OFFICERS } = useGameConfig()
|
||||
|
||||
const selectedPlanetId = ref<string>(gameStore.player.planets[0]?.id || '')
|
||||
const activeTab = ref<'resources' | 'buildings' | 'research' | 'ships' | 'defense' | 'officers'>('resources')
|
||||
const officerDays = ref<Record<OfficerType, number>>({} as Record<OfficerType, number>)
|
||||
const selectedNPCId = ref<string>(npcStore.npcs[0]?.id || '')
|
||||
const targetPlanetIndex = ref<string>('0')
|
||||
|
||||
// 初始化军官天数显示
|
||||
Object.values(OfficerType).forEach(officer => {
|
||||
@@ -226,6 +280,14 @@
|
||||
return gameStore.player.planets.find(p => p.id === selectedPlanetId.value)
|
||||
})
|
||||
|
||||
const selectedNPC = computed(() => {
|
||||
return npcStore.npcs.find(npc => npc.id === selectedNPCId.value)
|
||||
})
|
||||
|
||||
const allPlanets = computed(() => {
|
||||
return [...gameStore.player.planets, ...Object.values(universeStore.planets)]
|
||||
})
|
||||
|
||||
const resourceTypes = ['metal', 'crystal', 'deuterium', 'darkMatter'] as const
|
||||
const buildingTypes = Object.values(BuildingType)
|
||||
const technologyTypes = Object.values(TechnologyType)
|
||||
@@ -233,6 +295,16 @@
|
||||
const defenseTypes = Object.values(DefenseType)
|
||||
const officerTypes = Object.values(OfficerType)
|
||||
|
||||
// Tab配置
|
||||
const tabs = [
|
||||
{ value: 'resources' as const, label: 'gmView.resources' },
|
||||
{ value: 'buildings' as const, label: 'gmView.buildings' },
|
||||
{ value: 'research' as const, label: 'gmView.research' },
|
||||
{ value: 'ships' as const, label: 'gmView.ships' },
|
||||
{ value: 'defense' as const, label: 'gmView.defense' },
|
||||
{ value: 'officers' as const, label: 'gmView.officers' }
|
||||
]
|
||||
|
||||
const setResourceAmount = (resource: string, amount: number) => {
|
||||
if (selectedPlanet.value) {
|
||||
selectedPlanet.value.resources[resource as keyof typeof selectedPlanet.value.resources] += amount
|
||||
@@ -288,4 +360,113 @@
|
||||
location.reload()
|
||||
}
|
||||
}
|
||||
|
||||
// NPC测试函数
|
||||
const testNPCSpy = () => {
|
||||
if (!selectedNPC.value) {
|
||||
alert(t('gmView.selectNPCFirst') || 'Please select an NPC first')
|
||||
return
|
||||
}
|
||||
|
||||
const mission = npcBehaviorLogic.forceNPCSpyPlayer(
|
||||
selectedNPC.value,
|
||||
gameStore.player,
|
||||
allPlanets.value,
|
||||
parseInt(targetPlanetIndex.value)
|
||||
)
|
||||
|
||||
if (mission) {
|
||||
// 加速任务到5秒后到达
|
||||
npcBehaviorLogic.accelerateNPCMission(selectedNPC.value, mission.id, 5)
|
||||
alert(`${selectedNPC.value.name} will spy in 5 seconds`)
|
||||
} else {
|
||||
alert(t('gmView.npcNoProbes') || 'NPC does not have spy probes')
|
||||
}
|
||||
}
|
||||
|
||||
const testNPCAttack = () => {
|
||||
if (!selectedNPC.value) {
|
||||
alert(t('gmView.selectNPCFirst') || 'Please select an NPC first')
|
||||
return
|
||||
}
|
||||
|
||||
const mission = npcBehaviorLogic.forceNPCAttackPlayer(
|
||||
selectedNPC.value,
|
||||
gameStore.player,
|
||||
allPlanets.value,
|
||||
parseInt(targetPlanetIndex.value)
|
||||
)
|
||||
|
||||
if (mission) {
|
||||
// 加速任务到5秒后到达
|
||||
npcBehaviorLogic.accelerateNPCMission(selectedNPC.value, mission.id, 5)
|
||||
alert(`${selectedNPC.value.name} will attack in 5 seconds`)
|
||||
} else {
|
||||
alert(t('gmView.npcNoSpyReport') || 'NPC needs to spy first')
|
||||
}
|
||||
}
|
||||
|
||||
const testNPCSpyAndAttack = () => {
|
||||
if (!selectedNPC.value) {
|
||||
alert(t('gmView.selectNPCFirst') || 'Please select an NPC first')
|
||||
return
|
||||
}
|
||||
|
||||
const { spyMission, attackMission } = npcBehaviorLogic.forceNPCSpyAndAttack(
|
||||
selectedNPC.value,
|
||||
gameStore.player,
|
||||
allPlanets.value,
|
||||
parseInt(targetPlanetIndex.value)
|
||||
)
|
||||
|
||||
if (spyMission && attackMission) {
|
||||
// 加速任务:侦查5秒后到达,攻击10秒后到达
|
||||
npcBehaviorLogic.accelerateNPCMission(selectedNPC.value, spyMission.id, 5)
|
||||
npcBehaviorLogic.accelerateNPCMission(selectedNPC.value, attackMission.id, 10)
|
||||
alert(`${selectedNPC.value.name} will spy in 5s and attack in 10s`)
|
||||
} else {
|
||||
alert(t('gmView.npcMissionFailed') || 'Failed to create missions')
|
||||
}
|
||||
}
|
||||
|
||||
const accelerateAllMissions = () => {
|
||||
if (!selectedNPC.value) {
|
||||
alert(t('gmView.selectNPCFirst') || 'Please select an NPC first')
|
||||
return
|
||||
}
|
||||
|
||||
const count = npcBehaviorLogic.accelerateAllNPCMissions(selectedNPC.value, 5)
|
||||
alert(`Accelerated ${count} missions to 5 seconds`)
|
||||
}
|
||||
|
||||
// 初始化NPC舰队
|
||||
const initializeNPCFleet = () => {
|
||||
if (!selectedNPC.value) {
|
||||
alert(t('gmView.selectNPCFirst') || 'Please select an NPC first')
|
||||
return
|
||||
}
|
||||
|
||||
// 给NPC的第一个星球添加基础舰队
|
||||
const npcPlanet = selectedNPC.value.planets[0]
|
||||
if (!npcPlanet) {
|
||||
alert('NPC has no planets')
|
||||
return
|
||||
}
|
||||
|
||||
// 添加间谍探测器
|
||||
npcPlanet.fleet[ShipType.EspionageProbe] = (npcPlanet.fleet[ShipType.EspionageProbe] || 0) + 100
|
||||
|
||||
// 添加战斗舰船
|
||||
npcPlanet.fleet[ShipType.LightFighter] = (npcPlanet.fleet[ShipType.LightFighter] || 0) + 500
|
||||
npcPlanet.fleet[ShipType.HeavyFighter] = (npcPlanet.fleet[ShipType.HeavyFighter] || 0) + 300
|
||||
npcPlanet.fleet[ShipType.Cruiser] = (npcPlanet.fleet[ShipType.Cruiser] || 0) + 200
|
||||
npcPlanet.fleet[ShipType.Battleship] = (npcPlanet.fleet[ShipType.Battleship] || 0) + 100
|
||||
npcPlanet.fleet[ShipType.Bomber] = (npcPlanet.fleet[ShipType.Bomber] || 0) + 50
|
||||
npcPlanet.fleet[ShipType.Destroyer] = (npcPlanet.fleet[ShipType.Destroyer] || 0) + 30
|
||||
npcPlanet.fleet[ShipType.Battlecruiser] = (npcPlanet.fleet[ShipType.Battlecruiser] || 0) + 20
|
||||
|
||||
alert(
|
||||
`${selectedNPC.value.name} fleet initialized:\n- 100 Spy Probes\n- 500 Light Fighters\n- 300 Heavy Fighters\n- 200 Cruisers\n- 100 Battleships\n- 50 Bombers\n- 30 Destroyers\n- 20 Battlecruisers`
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -8,12 +8,18 @@
|
||||
<CardTitle>{{ t('galaxyView.selectCoordinates') }}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3 sm:gap-4">
|
||||
<div
|
||||
:class="[
|
||||
'grid gap-3 sm:gap-4',
|
||||
highlightedNpc ? 'grid-cols-2 sm:grid-cols-4' : isInHomePlanetSystem ? 'grid-cols-2' : 'grid-cols-2 sm:grid-cols-3'
|
||||
]"
|
||||
>
|
||||
<div class="space-y-2">
|
||||
<Label for="select-galaxy" class="text-xs sm:text-sm">{{ t('galaxyView.galaxy') }}</Label>
|
||||
<Select
|
||||
:key="gameStore.locale"
|
||||
:model-value="String(selectedGalaxy)"
|
||||
:modal="false"
|
||||
@update:model-value="
|
||||
val => {
|
||||
selectedGalaxy = Number(val)
|
||||
@@ -24,7 +30,7 @@
|
||||
<SelectTrigger id="select-galaxy" class="w-full">
|
||||
<SelectValue :placeholder="t('galaxyView.selectGalaxy')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectContent position="popper">
|
||||
<SelectItem v-for="g in 9" :key="g" :value="String(g)">{{ t('galaxyView.galaxy') }} {{ g }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
@@ -34,6 +40,7 @@
|
||||
<Select
|
||||
:key="`${gameStore.locale}-system`"
|
||||
:model-value="String(selectedSystem)"
|
||||
:modal="false"
|
||||
@update:model-value="
|
||||
val => {
|
||||
selectedSystem = Number(val)
|
||||
@@ -44,16 +51,101 @@
|
||||
<SelectTrigger id="select-system" class="w-full">
|
||||
<SelectValue :placeholder="t('galaxyView.selectSystem')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectContent position="popper">
|
||||
<SelectItem v-for="s in 10" :key="s" :value="String(s)">{{ t('galaxyView.system') }} {{ s }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="col-span-2 sm:col-span-1 flex items-end">
|
||||
<Button @click="goToCurrentPlanet" variant="outline" class="w-full">
|
||||
<Home class="h-4 w-4 mr-2" />
|
||||
{{ t('galaxyView.myPlanet') }}
|
||||
</Button>
|
||||
<div v-if="!isInHomePlanetSystem" :class="highlightedNpc ? '' : 'col-span-2 sm:col-span-1'" class="space-y-2">
|
||||
<Label class="text-xs sm:text-sm opacity-0">{{ t('galaxyView.myPlanets') }}</Label>
|
||||
<!-- 不在母星星系时显示Popover选择 -->
|
||||
<Popover>
|
||||
<PopoverTrigger as-child>
|
||||
<Button variant="outline" class="w-full">
|
||||
<Home class="h-4 w-4 mr-2" />
|
||||
{{ t('galaxyView.myPlanets') }}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-72 p-2" align="start">
|
||||
<div class="space-y-1">
|
||||
<div class="px-2 py-1.5 text-xs font-semibold text-muted-foreground">
|
||||
{{ t('galaxyView.selectPlanetToView') }}
|
||||
</div>
|
||||
<Button
|
||||
v-for="p in myPlanets"
|
||||
:key="p.id"
|
||||
@click="goToPlanet(p)"
|
||||
:disabled="p.position.galaxy === currentGalaxy && p.position.system === currentSystem"
|
||||
variant="ghost"
|
||||
:class="[
|
||||
'w-full justify-start h-auto py-2 px-2 text-left',
|
||||
p.position.galaxy === currentGalaxy &&
|
||||
p.position.system === currentSystem &&
|
||||
'bg-blue-100 dark:bg-blue-950/50 border border-blue-400 dark:border-blue-600'
|
||||
]"
|
||||
size="sm"
|
||||
>
|
||||
<div class="flex items-start gap-2 w-full min-w-0">
|
||||
<Globe class="h-4 w-4 flex-shrink-0 mt-0.5" />
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-1.5 mb-0.5">
|
||||
<span class="truncate font-medium text-sm">{{ p.name }}</span>
|
||||
<Badge v-if="p.isMoon" variant="outline" class="text-[10px] px-1 py-0 h-4">
|
||||
{{ t('planet.moon') }}
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="text-[11px] text-muted-foreground">
|
||||
[{{ p.position.galaxy }}:{{ p.position.system }}:{{ p.position.position }}]
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
<!-- NPC星球列表 -->
|
||||
<div v-if="highlightedNpc" :class="isInHomePlanetSystem ? 'col-span-2 sm:col-span-2' : ''" class="space-y-2">
|
||||
<Label class="text-xs sm:text-sm opacity-0">{{ t('galaxyView.npcPlanets') }}</Label>
|
||||
<Popover>
|
||||
<PopoverTrigger as-child>
|
||||
<Button variant="outline" class="w-full border-yellow-400 dark:border-yellow-600">
|
||||
<Globe class="h-4 w-4 mr-2" />
|
||||
{{ highlightedNpc.name }} ({{ highlightedNpc.planets.length }})
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-72 p-2" align="start">
|
||||
<div class="space-y-1">
|
||||
<div class="px-2 py-1.5 text-xs font-semibold text-muted-foreground">
|
||||
{{ t('galaxyView.selectPlanetToView') }}
|
||||
</div>
|
||||
<Button
|
||||
v-for="p in highlightedNpc.planets"
|
||||
:key="p.id"
|
||||
@click="goToPlanet(p)"
|
||||
:disabled="p.position.galaxy === currentGalaxy && p.position.system === currentSystem"
|
||||
variant="ghost"
|
||||
:class="[
|
||||
'w-full justify-start h-auto py-2 px-2 text-left',
|
||||
p.position.galaxy === currentGalaxy &&
|
||||
p.position.system === currentSystem &&
|
||||
'bg-yellow-100 dark:bg-yellow-950/50 border border-yellow-400 dark:border-yellow-600'
|
||||
]"
|
||||
size="sm"
|
||||
>
|
||||
<div class="flex items-start gap-2 w-full min-w-0">
|
||||
<Globe class="h-4 w-4 flex-shrink-0 mt-0.5" />
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="truncate font-medium text-sm mb-0.5">{{ p.name }}</div>
|
||||
<div class="text-[11px] text-muted-foreground">
|
||||
[{{ p.position.galaxy }}:{{ p.position.system }}:{{ p.position.position }}]
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -72,8 +164,32 @@
|
||||
:key="slot.position"
|
||||
class="flex items-center gap-2 sm:gap-4 p-2 sm:p-3 border rounded-lg hover:bg-muted/50 transition-colors"
|
||||
:class="{
|
||||
'bg-blue-50 dark:bg-blue-950 border-blue-300 dark:border-blue-700': isMyPlanet(slot.planet),
|
||||
'bg-muted/30': !slot.planet
|
||||
// 空位置
|
||||
'bg-muted/30': !slot.planet,
|
||||
// 我的星球 - 蓝色
|
||||
'bg-blue-50 dark:bg-blue-950 border-blue-300 dark:border-blue-700': slot.planet && isMyPlanet(slot.planet),
|
||||
// 高亮NPC - 黄色
|
||||
'bg-yellow-50 dark:bg-yellow-950/30 border-yellow-400 dark:border-yellow-600 ring-2 ring-yellow-400 dark:ring-yellow-500':
|
||||
slot.planet && isHighlightedNpcPlanet(slot.planet) && !isMyPlanet(slot.planet),
|
||||
// 友好NPC - 绿色
|
||||
'bg-green-50 dark:bg-green-950/30 border-green-300 dark:border-green-700':
|
||||
slot.planet &&
|
||||
!isMyPlanet(slot.planet) &&
|
||||
!isHighlightedNpcPlanet(slot.planet) &&
|
||||
getRelation(slot.planet)?.status === RelationStatus.Friendly,
|
||||
// 敌对NPC - 红色
|
||||
'bg-red-50 dark:bg-red-950/30 border-red-300 dark:border-red-700':
|
||||
slot.planet &&
|
||||
!isMyPlanet(slot.planet) &&
|
||||
!isHighlightedNpcPlanet(slot.planet) &&
|
||||
getRelation(slot.planet)?.status === RelationStatus.Hostile,
|
||||
// 中立NPC - 灰色
|
||||
'bg-gray-50 dark:bg-gray-950/30 border-gray-300 dark:border-gray-700':
|
||||
slot.planet &&
|
||||
!isMyPlanet(slot.planet) &&
|
||||
!isHighlightedNpcPlanet(slot.planet) &&
|
||||
getPlanetNPC(slot.planet) &&
|
||||
(!getRelation(slot.planet) || getRelation(slot.planet)?.status === RelationStatus.Neutral)
|
||||
}"
|
||||
>
|
||||
<!-- 位置编号 -->
|
||||
@@ -84,32 +200,104 @@
|
||||
<!-- 星球信息 -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<div v-if="slot.planet" class="space-y-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<h3 class="font-semibold text-sm sm:text-base truncate">{{ slot.planet.name }}</h3>
|
||||
<Badge v-if="isMyPlanet(slot.planet)" variant="default" class="text-xs">{{ t('galaxyView.mine') }}</Badge>
|
||||
<Badge v-else variant="secondary" class="text-xs">{{ t('galaxyView.hostile') }}</Badge>
|
||||
<!-- 移动端:垂直布局 / PC端:水平布局 -->
|
||||
<div class="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2">
|
||||
<!-- 星球名称和坐标 -->
|
||||
<div class="flex items-baseline gap-1.5 min-w-0">
|
||||
<h3 class="font-semibold text-sm sm:text-base truncate">{{ slot.planet.name }}</h3>
|
||||
<span class="text-xs text-muted-foreground whitespace-nowrap flex-shrink-0 sm:hidden">
|
||||
[{{ slot.planet.position.galaxy }}:{{ slot.planet.position.system }}:{{ slot.planet.position.position }}]
|
||||
</span>
|
||||
</div>
|
||||
<!-- 徽章组 -->
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<Badge v-if="isMyPlanet(slot.planet)" variant="default" class="text-xs">{{ t('galaxyView.mine') }}</Badge>
|
||||
<Badge v-else :variant="getRelationBadgeVariant(slot.planet)" class="text-xs">
|
||||
{{ getRelationStatusText(slot.planet) }}
|
||||
</Badge>
|
||||
<!-- 残骸场徽章 - 紧凑显示 -->
|
||||
<Popover v-if="getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)">
|
||||
<PopoverTrigger as-child>
|
||||
<Badge
|
||||
variant="outline"
|
||||
class="text-xs cursor-pointer hover:bg-amber-50 dark:hover:bg-amber-950/30 border-amber-300 dark:border-amber-700 text-amber-700 dark:text-amber-400 gap-1"
|
||||
>
|
||||
<Recycle class="h-3 w-3" />
|
||||
<span class="hidden sm:inline">{{ t('galaxyView.debris') }}</span>
|
||||
</Badge>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-auto p-3" side="top" align="start">
|
||||
<div class="space-y-2">
|
||||
<p class="text-xs font-semibold text-amber-700 dark:text-amber-400">{{ t('galaxyView.debrisField') }}</p>
|
||||
<div class="space-y-1 text-xs">
|
||||
<div class="flex items-center gap-2">
|
||||
<ResourceIcon type="metal" size="sm" />
|
||||
<span class="text-muted-foreground">{{ t('resources.metal') }}:</span>
|
||||
<span class="font-medium">
|
||||
{{ formatNumber(getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)!.resources.metal) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<ResourceIcon type="crystal" size="sm" />
|
||||
<span class="text-muted-foreground">{{ t('resources.crystal') }}:</span>
|
||||
<span class="font-medium">
|
||||
{{ formatNumber(getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)!.resources.crystal) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
<!-- PC端:显示坐标 -->
|
||||
<p class="text-xs text-muted-foreground hidden sm:block">
|
||||
[{{ slot.planet.position.galaxy }}:{{ slot.planet.position.system }}:{{ slot.planet.position.position }}]
|
||||
</p>
|
||||
<!-- 好感度显示(仅NPC星球) -->
|
||||
<div v-if="!isMyPlanet(slot.planet) && getReputationValue(slot.planet) !== null" class="text-xs">
|
||||
<span class="text-muted-foreground">{{ t('diplomacy.reputation') }}:</span>
|
||||
<span class="ml-1 font-semibold" :class="getReputationColor(getReputationValue(slot.planet))">
|
||||
{{ getReputationValue(slot.planet)! > 0 ? '+' : '' }}{{ getReputationValue(slot.planet) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-sm text-muted-foreground">{{ t('galaxyView.emptySlot') }}</div>
|
||||
|
||||
<!-- 残骸场信息 -->
|
||||
<div v-if="getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)" class="mt-2 p-2 bg-amber-50 dark:bg-amber-950/30 border border-amber-200 dark:border-amber-800 rounded text-xs">
|
||||
<div class="flex items-center gap-2 text-amber-700 dark:text-amber-400 font-medium mb-1">
|
||||
<span>{{ t('galaxyView.debrisField') }}</span>
|
||||
</div>
|
||||
<div class="flex gap-3 text-xs">
|
||||
<span class="flex items-center gap-1">
|
||||
<span class="text-muted-foreground">{{ t('resources.metal') }}:</span>
|
||||
<span class="font-medium">{{ formatNumber(getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)!.resources.metal) }}</span>
|
||||
</span>
|
||||
<span class="flex items-center gap-1">
|
||||
<span class="text-muted-foreground">{{ t('resources.crystal') }}:</span>
|
||||
<span class="font-medium">{{ formatNumber(getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)!.resources.crystal) }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<!-- 空位置 -->
|
||||
<div v-else class="space-y-1">
|
||||
<div class="text-sm text-muted-foreground">{{ t('galaxyView.emptySlot') }}</div>
|
||||
<!-- 残骸场徽章 - 空位置时也显示 -->
|
||||
<Popover v-if="getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)">
|
||||
<PopoverTrigger as-child>
|
||||
<Badge
|
||||
variant="outline"
|
||||
class="text-xs cursor-pointer hover:bg-amber-50 dark:hover:bg-amber-950/30 border-amber-300 dark:border-amber-700 text-amber-700 dark:text-amber-400 gap-1 inline-flex"
|
||||
>
|
||||
<Recycle class="h-3 w-3" />
|
||||
<span>{{ t('galaxyView.debris') }}</span>
|
||||
</Badge>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-auto p-3" side="top" align="start">
|
||||
<div class="space-y-2">
|
||||
<p class="text-xs font-semibold text-amber-700 dark:text-amber-400">{{ t('galaxyView.debrisField') }}</p>
|
||||
<div class="space-y-1 text-xs">
|
||||
<div class="flex items-center gap-2">
|
||||
<ResourceIcon type="metal" size="sm" />
|
||||
<span class="text-muted-foreground">{{ t('resources.metal') }}:</span>
|
||||
<span class="font-medium">
|
||||
{{ formatNumber(getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)!.resources.metal) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<ResourceIcon type="crystal" size="sm" />
|
||||
<span class="text-muted-foreground">{{ t('resources.crystal') }}:</span>
|
||||
<span class="font-medium">
|
||||
{{ formatNumber(getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)!.resources.crystal) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -136,6 +324,16 @@
|
||||
<p>{{ t('galaxyView.attack') }}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip v-if="slot.planet && !isMyPlanet(slot.planet) && getPlanetNPC(slot.planet)">
|
||||
<TooltipTrigger as-child>
|
||||
<Button @click="showPlanetActions(slot.planet, 'gift')" variant="outline" size="sm" class="h-8 w-8 p-0">
|
||||
<Gift class="h-3 w-3 sm:h-4 sm:w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{{ t('galaxyView.sendGift') }}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip v-if="!slot.planet">
|
||||
<TooltipTrigger as-child>
|
||||
<Button @click="showPlanetActions(null, 'colonize', slot.position)" variant="outline" size="sm" class="h-8 w-8 p-0">
|
||||
@@ -158,7 +356,12 @@
|
||||
</Tooltip>
|
||||
<Tooltip v-if="getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)">
|
||||
<TooltipTrigger as-child>
|
||||
<Button @click="showPlanetActions(slot.planet, 'recycle', slot.position)" variant="outline" size="sm" class="h-8 w-8 p-0">
|
||||
<Button
|
||||
@click="showPlanetActions(slot.planet, 'recycle', slot.position)"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="h-8 w-8 p-0"
|
||||
>
|
||||
<Recycle class="h-3 w-3 sm:h-4 sm:w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
@@ -174,44 +377,122 @@
|
||||
</Card>
|
||||
|
||||
<!-- 快速派遣对话框 -->
|
||||
<AlertDialog ref="actionDialog" />
|
||||
<AlertDialog :open="alertDialogOpen" @update:open="alertDialogOpen = $event">
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{{ alertDialogTitle }}</AlertDialogTitle>
|
||||
<AlertDialogDescription class="whitespace-pre-line">
|
||||
{{ alertDialogMessage }}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>{{ t('common.cancel') }}</AlertDialogCancel>
|
||||
<AlertDialogAction @click="handleAlertDialogConfirm">{{ t('common.confirm') }}</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useUniverseStore } from '@/stores/universeStore'
|
||||
import { useNPCStore } from '@/stores/npcStore'
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import type { Planet, DebrisField } from '@/types/game'
|
||||
import { RelationStatus } from '@/types/game'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui/select'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import AlertDialog from '@/components/AlertDialog.vue'
|
||||
import { Home, Eye, Sword, Rocket, Recycle } from 'lucide-vue-next'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import ResourceIcon from '@/components/ResourceIcon.vue'
|
||||
import { Home, Eye, Sword, Rocket, Recycle, Gift, Globe } from 'lucide-vue-next'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import * as gameLogic from '@/logic/gameLogic'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const universeStore = useUniverseStore()
|
||||
const npcStore = useNPCStore()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const { t } = useI18n()
|
||||
const actionDialog = ref<InstanceType<typeof AlertDialog> | null>(null)
|
||||
|
||||
// AlertDialog 状态
|
||||
const alertDialogOpen = ref(false)
|
||||
const alertDialogTitle = ref('')
|
||||
const alertDialogMessage = ref('')
|
||||
const alertDialogConfirmAction = ref<(() => void) | null>(null)
|
||||
|
||||
const selectedGalaxy = ref(1)
|
||||
const selectedSystem = ref(1)
|
||||
const currentGalaxy = ref(1)
|
||||
const currentSystem = ref(1)
|
||||
|
||||
// 保存要高亮的NPC ID(从URL参数初始化,之后即使URL清除也保持)
|
||||
const highlightNpcId = ref<string | undefined>(undefined)
|
||||
|
||||
// 获取要高亮的NPC对象
|
||||
const highlightedNpc = computed(() => {
|
||||
if (!highlightNpcId.value) return null
|
||||
return npcStore.npcs.find(n => n.id === highlightNpcId.value) || null
|
||||
})
|
||||
|
||||
const systemSlots = ref<Array<{ position: number; planet: Planet | null }>>([])
|
||||
|
||||
// 获取玩家的母星
|
||||
const homePlanet = computed(() => {
|
||||
// 第一个非月球星球就是母星
|
||||
return gameStore.player.planets.find(p => !p.isMoon)
|
||||
})
|
||||
|
||||
// 获取玩家所有非月球星球
|
||||
const myPlanets = computed(() => {
|
||||
return gameStore.player.planets.filter(p => !p.isMoon)
|
||||
})
|
||||
|
||||
// 判断当前是否在母星所在星系
|
||||
const isInHomePlanetSystem = computed(() => {
|
||||
if (!homePlanet.value) return false
|
||||
return currentGalaxy.value === homePlanet.value.position.galaxy && currentSystem.value === homePlanet.value.position.system
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// 默认显示当前星球所在的星系
|
||||
if (gameStore.currentPlanet) {
|
||||
// 从URL参数中读取并保存高亮NPC ID
|
||||
if (route.query.highlightNpc) {
|
||||
highlightNpcId.value = route.query.highlightNpc as string
|
||||
}
|
||||
|
||||
// 优先检查URL参数中的星系坐标
|
||||
const queryGalaxy = route.query.galaxy ? Number(route.query.galaxy) : null
|
||||
const querySystem = route.query.system ? Number(route.query.system) : null
|
||||
|
||||
if (queryGalaxy && querySystem) {
|
||||
// 如果URL中有坐标参数,跳转到指定星系
|
||||
currentGalaxy.value = queryGalaxy
|
||||
currentSystem.value = querySystem
|
||||
selectedGalaxy.value = queryGalaxy
|
||||
selectedSystem.value = querySystem
|
||||
loadSystem()
|
||||
|
||||
// 立即清除URL参数,但保持本地变量中的highlightNpcId
|
||||
clearUrlParams()
|
||||
} else if (gameStore.currentPlanet) {
|
||||
// 否则默认显示当前星球所在的星系
|
||||
currentGalaxy.value = gameStore.currentPlanet.position.galaxy
|
||||
currentSystem.value = gameStore.currentPlanet.position.system
|
||||
selectedGalaxy.value = currentGalaxy.value
|
||||
@@ -225,11 +506,12 @@
|
||||
return positions.map(pos => {
|
||||
const key = gameLogic.generatePositionKey(galaxy, system, pos.position)
|
||||
// 先从玩家星球中查找,再从宇宙地图中查找
|
||||
const planet = gameStore.player.planets.find(p =>
|
||||
p.position.galaxy === galaxy &&
|
||||
p.position.system === system &&
|
||||
p.position.position === pos.position
|
||||
) || universeStore.planets[key] || null
|
||||
const planet =
|
||||
gameStore.player.planets.find(
|
||||
p => p.position.galaxy === galaxy && p.position.system === system && p.position.position === pos.position
|
||||
) ||
|
||||
universeStore.planets[key] ||
|
||||
null
|
||||
return { position: pos.position, planet }
|
||||
})
|
||||
}
|
||||
@@ -240,6 +522,13 @@
|
||||
return universeStore.debrisFields[debrisId] || null
|
||||
}
|
||||
|
||||
// 清除URL参数
|
||||
const clearUrlParams = () => {
|
||||
if (route.query.highlightNpc || route.query.galaxy || route.query.system) {
|
||||
router.replace({ query: {} })
|
||||
}
|
||||
}
|
||||
|
||||
// 加载星系
|
||||
const loadSystem = () => {
|
||||
currentGalaxy.value = selectedGalaxy.value
|
||||
@@ -247,15 +536,13 @@
|
||||
systemSlots.value = getSystemPlanets(currentGalaxy.value, currentSystem.value)
|
||||
}
|
||||
|
||||
// 跳转到当前星球
|
||||
const goToCurrentPlanet = () => {
|
||||
if (gameStore.currentPlanet) {
|
||||
currentGalaxy.value = gameStore.currentPlanet.position.galaxy
|
||||
currentSystem.value = gameStore.currentPlanet.position.system
|
||||
selectedGalaxy.value = currentGalaxy.value
|
||||
selectedSystem.value = currentSystem.value
|
||||
loadSystem()
|
||||
}
|
||||
// 跳转到指定星球的星系
|
||||
const goToPlanet = (planet: Planet) => {
|
||||
currentGalaxy.value = planet.position.galaxy
|
||||
currentSystem.value = planet.position.system
|
||||
selectedGalaxy.value = currentGalaxy.value
|
||||
selectedSystem.value = currentSystem.value
|
||||
systemSlots.value = getSystemPlanets(currentGalaxy.value, currentSystem.value)
|
||||
}
|
||||
|
||||
// 判断是否为我的星球
|
||||
@@ -264,14 +551,95 @@
|
||||
return planet.ownerId === gameStore.player.id
|
||||
}
|
||||
|
||||
// 判断星球是否属于高亮显示的NPC
|
||||
const isHighlightedNpcPlanet = (planet: Planet | null): boolean => {
|
||||
if (!planet || !highlightNpcId.value) return false
|
||||
const npc = npcStore.npcs.find(n => n.id === highlightNpcId.value)
|
||||
if (!npc) return false
|
||||
return npc.planets.some(p => p.id === planet.id)
|
||||
}
|
||||
|
||||
// 获取星球所属的NPC
|
||||
const getPlanetNPC = (planet: Planet | null) => {
|
||||
if (!planet || isMyPlanet(planet)) return null
|
||||
// 通过坐标匹配,而不是ID,因为universeStore中的星球和npcStore中的星球可能是不同的对象
|
||||
return npcStore.npcs.find(npc =>
|
||||
npc.planets.some(
|
||||
p =>
|
||||
p.position.galaxy === planet.position.galaxy &&
|
||||
p.position.system === planet.position.system &&
|
||||
p.position.position === planet.position.position
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// 获取外交关系
|
||||
const getRelation = (planet: Planet | null) => {
|
||||
const npc = getPlanetNPC(planet)
|
||||
if (!npc) return null
|
||||
return gameStore.player.diplomaticRelations?.[npc.id]
|
||||
}
|
||||
|
||||
// 获取关系状态Badge样式
|
||||
const getRelationBadgeVariant = (planet: Planet | null) => {
|
||||
const relation = getRelation(planet)
|
||||
if (!relation) return 'secondary'
|
||||
|
||||
switch (relation.status) {
|
||||
case RelationStatus.Friendly:
|
||||
return 'default'
|
||||
case RelationStatus.Hostile:
|
||||
return 'destructive'
|
||||
default:
|
||||
return 'secondary'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取关系状态文本
|
||||
const getRelationStatusText = (planet: Planet | null) => {
|
||||
const relation = getRelation(planet)
|
||||
if (!relation) return t('diplomacy.status.neutral')
|
||||
|
||||
switch (relation.status) {
|
||||
case RelationStatus.Friendly:
|
||||
return t('diplomacy.status.friendly')
|
||||
case RelationStatus.Hostile:
|
||||
return t('diplomacy.status.hostile')
|
||||
default:
|
||||
return t('diplomacy.status.neutral')
|
||||
}
|
||||
}
|
||||
|
||||
// 获取好感度值
|
||||
const getReputationValue = (planet: Planet | null): number | null => {
|
||||
const relation = getRelation(planet)
|
||||
return relation?.reputation ?? null
|
||||
}
|
||||
|
||||
// 获取好感度颜色
|
||||
const getReputationColor = (reputation: number | null) => {
|
||||
if (reputation === null) return 'text-muted-foreground'
|
||||
if (reputation >= 20) return 'text-green-600 dark:text-green-400'
|
||||
if (reputation <= -20) return 'text-red-600 dark:text-red-400'
|
||||
return 'text-muted-foreground'
|
||||
}
|
||||
|
||||
// 切换到指定星球
|
||||
const switchToPlanet = (planetId: string) => {
|
||||
gameStore.currentPlanetId = planetId
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
// AlertDialog 确认处理
|
||||
const handleAlertDialogConfirm = () => {
|
||||
if (alertDialogConfirmAction.value) {
|
||||
alertDialogConfirmAction.value()
|
||||
}
|
||||
alertDialogOpen.value = false
|
||||
}
|
||||
|
||||
// 显示星球操作
|
||||
const showPlanetActions = (planet: Planet | null, action: 'spy' | 'attack' | 'colonize' | 'recycle', position?: number) => {
|
||||
const showPlanetActions = (planet: Planet | null, action: 'spy' | 'attack' | 'colonize' | 'recycle' | 'gift', position?: number) => {
|
||||
const targetPos = planet ? planet.position : { galaxy: currentGalaxy.value, system: currentSystem.value, position: position! }
|
||||
const coordinates = `${targetPos.galaxy}:${targetPos.system}:${targetPos.position}`
|
||||
|
||||
@@ -289,23 +657,27 @@
|
||||
} else if (action === 'recycle') {
|
||||
title = t('galaxyView.recyclePlanetTitle')
|
||||
message = t('galaxyView.recyclePlanetMessage').replace('{coordinates}', coordinates)
|
||||
} else if (action === 'gift') {
|
||||
title = t('galaxyView.giftPlanetTitle')
|
||||
message = t('galaxyView.giftPlanetMessage').replace('{coordinates}', coordinates)
|
||||
}
|
||||
|
||||
actionDialog.value?.show({
|
||||
title,
|
||||
message,
|
||||
onConfirm: () => {
|
||||
// 跳转到舰队页面并填充目标坐标
|
||||
router.push({
|
||||
path: '/fleet',
|
||||
query: {
|
||||
galaxy: targetPos.galaxy,
|
||||
system: targetPos.system,
|
||||
position: targetPos.position,
|
||||
mission: action
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
// 设置对话框状态
|
||||
alertDialogTitle.value = title
|
||||
alertDialogMessage.value = message
|
||||
alertDialogConfirmAction.value = () => {
|
||||
// 跳转到舰队页面并填充目标坐标
|
||||
router.push({
|
||||
path: '/fleet',
|
||||
query: {
|
||||
galaxy: targetPos.galaxy,
|
||||
system: targetPos.system,
|
||||
position: targetPos.position,
|
||||
mission: action === 'gift' ? undefined : action,
|
||||
gift: action === 'gift' ? '1' : undefined
|
||||
}
|
||||
})
|
||||
}
|
||||
alertDialogOpen.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -3,85 +3,293 @@
|
||||
<h1 class="text-2xl sm:text-3xl font-bold">{{ t('messagesView.title') }}</h1>
|
||||
|
||||
<!-- 标签切换 -->
|
||||
<div class="flex gap-2 border-b">
|
||||
<Button @click="activeTab = 'battles'" :variant="activeTab === 'battles' ? 'default' : 'ghost'" class="rounded-b-none">
|
||||
<Sword class="h-4 w-4 mr-2" />
|
||||
{{ t('messagesView.battles') }}
|
||||
<Badge v-if="unreadBattles > 0" variant="destructive" class="ml-2">{{ unreadBattles }}</Badge>
|
||||
</Button>
|
||||
<Button @click="activeTab = 'spy'" :variant="activeTab === 'spy' ? 'default' : 'ghost'" class="rounded-b-none">
|
||||
<Eye class="h-4 w-4 mr-2" />
|
||||
{{ t('messagesView.spy') }}
|
||||
<Badge v-if="unreadSpyReports > 0" variant="destructive" class="ml-2">{{ unreadSpyReports }}</Badge>
|
||||
</Button>
|
||||
</div>
|
||||
<Tabs v-model="activeTab" class="w-full">
|
||||
<TabsList class="grid w-full grid-cols-2 sm:grid-cols-4" :tab-count="4">
|
||||
<TabsTrigger v-for="tab in tabs" :key="tab.value" :value="tab.value" class="flex items-center justify-center gap-1 px-2">
|
||||
<component :is="tab.icon" class="h-3 w-3 sm:h-4 sm:w-4" />
|
||||
<span class="text-xs sm:text-sm truncate">{{ tab.label }}</span>
|
||||
<Badge v-if="tab.unreadCount > 0" variant="destructive" class="hidden sm:flex ml-1">
|
||||
{{ tab.unreadCount }}
|
||||
</Badge>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<!-- 战斗报告列表 -->
|
||||
<div v-if="activeTab === 'battles'" class="space-y-2">
|
||||
<Card v-if="gameStore.player.battleReports.length === 0">
|
||||
<CardContent class="py-8 text-center text-muted-foreground">{{ t('messagesView.noBattleReports') }}</CardContent>
|
||||
</Card>
|
||||
<!-- 战斗报告列表 -->
|
||||
<TabsContent value="battles" class="mt-4 space-y-2">
|
||||
<Card v-if="gameStore.player.battleReports.length === 0">
|
||||
<CardContent class="py-8 text-center text-muted-foreground">{{ t('messagesView.noBattleReports') }}</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
v-for="report in sortedBattleReports"
|
||||
:key="report.id"
|
||||
@click="openBattleReport(report)"
|
||||
class="cursor-pointer hover:shadow-md transition-shadow"
|
||||
>
|
||||
<CardHeader class="pb-3">
|
||||
<div class="flex justify-between items-center gap-2">
|
||||
<div class="flex items-center gap-2 min-w-0 flex-1">
|
||||
<Sword class="h-4 w-4 flex-shrink-0" />
|
||||
<CardTitle class="text-base sm:text-lg">{{ t('messagesView.battleReport') }}</CardTitle>
|
||||
<Badge v-if="!report.read" variant="default" class="text-xs">{{ t('messagesView.unread') }}</Badge>
|
||||
<Badge
|
||||
:variant="report.winner === 'attacker' ? 'default' : report.winner === 'defender' ? 'destructive' : 'secondary'"
|
||||
class="text-xs"
|
||||
<Card
|
||||
v-for="report in sortedBattleReports"
|
||||
:key="report.id"
|
||||
@click="openBattleReport(report)"
|
||||
class="cursor-pointer hover:shadow-md transition-shadow"
|
||||
>
|
||||
<CardHeader class="pb-3">
|
||||
<div class="flex justify-between items-center gap-2">
|
||||
<div class="flex items-center gap-2 min-w-0 flex-1">
|
||||
<Sword class="h-4 w-4 flex-shrink-0" />
|
||||
<CardTitle class="text-base sm:text-lg">{{ t('messagesView.battleReport') }}</CardTitle>
|
||||
<Badge v-if="!report.read" variant="default" class="text-xs">{{ t('messagesView.unread') }}</Badge>
|
||||
<Badge :variant="getBattleResultVariant(report)" class="text-xs">
|
||||
{{ getBattleResultText(report) }}
|
||||
</Badge>
|
||||
</div>
|
||||
<Button @click.stop="deleteBattleReport(report.id)" variant="ghost" size="icon" class="h-8 w-8 flex-shrink-0">
|
||||
<X class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<CardDescription class="text-xs sm:text-sm">
|
||||
{{ formatDate(report.timestamp) }}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<!-- 间谍报告列表(合并:侦查报告 + 被侦查通知) -->
|
||||
<TabsContent value="spy" class="mt-4 space-y-2">
|
||||
<Card v-if="gameStore.player.spyReports.length === 0 && sortedSpiedNotifications.length === 0">
|
||||
<CardContent class="py-8 text-center text-muted-foreground">{{ t('messagesView.noSpyReports') }}</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 侦查报告 -->
|
||||
<Card
|
||||
v-for="report in sortedSpyReports"
|
||||
:key="report.id"
|
||||
@click="openSpyReport(report)"
|
||||
class="cursor-pointer hover:shadow-md transition-shadow"
|
||||
>
|
||||
<CardHeader class="pb-3">
|
||||
<div class="flex justify-between items-center gap-2">
|
||||
<div class="flex items-center gap-2 min-w-0 flex-1">
|
||||
<Eye class="h-4 w-4 flex-shrink-0" />
|
||||
<CardTitle class="text-base sm:text-lg">{{ t('messagesView.spyReport') }}</CardTitle>
|
||||
<Badge v-if="!report.read" variant="default" class="text-xs">{{ t('messagesView.unread') }}</Badge>
|
||||
<Badge variant="outline" class="text-xs">{{ report.targetPlanetId }}</Badge>
|
||||
</div>
|
||||
<Button @click.stop="deleteSpyReport(report.id)" variant="ghost" size="icon" class="h-8 w-8 flex-shrink-0">
|
||||
<X class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<CardDescription class="text-xs sm:text-sm">
|
||||
{{ formatDate(report.timestamp) }}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
|
||||
<!-- 被侦查通知 -->
|
||||
<Card
|
||||
v-for="notification in sortedSpiedNotifications"
|
||||
:key="notification.id"
|
||||
@click="openSpiedNotification(notification)"
|
||||
class="cursor-pointer hover:shadow-md transition-shadow"
|
||||
>
|
||||
<CardHeader class="pb-3">
|
||||
<div class="flex justify-between items-center gap-2">
|
||||
<div class="flex items-center gap-2 min-w-0 flex-1">
|
||||
<AlertTriangle class="h-4 w-4 flex-shrink-0 text-destructive" />
|
||||
<CardTitle class="text-base sm:text-lg">{{ t('messagesView.spiedNotification') }}</CardTitle>
|
||||
<Badge v-if="!notification.read" variant="default" class="text-xs">{{ t('messagesView.unread') }}</Badge>
|
||||
<Badge :variant="notification.detectionSuccess ? 'destructive' : 'secondary'" class="text-xs">
|
||||
{{ notification.detectionSuccess ? t('messagesView.detected') : t('messagesView.undetected') }}
|
||||
</Badge>
|
||||
</div>
|
||||
<Button @click.stop="deleteSpiedNotification(notification.id)" variant="ghost" size="icon" class="h-8 w-8 flex-shrink-0">
|
||||
<X class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<CardDescription class="text-xs sm:text-sm">
|
||||
{{ notification.npcName }} → {{ notification.targetPlanetName }} · {{ formatDate(notification.timestamp) }}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<!-- NPC相关消息(活动、礼物、被拒绝) -->
|
||||
<TabsContent value="npc" class="mt-4 space-y-2">
|
||||
<Card
|
||||
v-if="
|
||||
sortedNPCActivityNotifications.length === 0 &&
|
||||
sortedGiftNotifications.length === 0 &&
|
||||
sortedGiftRejectedNotifications.length === 0
|
||||
"
|
||||
>
|
||||
<CardContent class="py-8 text-center text-muted-foreground">{{ t('messagesView.noNPCActivity') }}</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- NPC活动通知 -->
|
||||
<Card
|
||||
v-for="notification in sortedNPCActivityNotifications"
|
||||
:key="notification.id"
|
||||
@click="openNPCActivityNotification(notification)"
|
||||
class="cursor-pointer hover:shadow-md transition-shadow"
|
||||
>
|
||||
<CardHeader class="pb-3">
|
||||
<div class="flex justify-between items-center gap-2">
|
||||
<div class="flex items-center gap-2 min-w-0 flex-1">
|
||||
<Recycle class="h-4 w-4 flex-shrink-0 text-blue-500" />
|
||||
<CardTitle class="text-base sm:text-lg">{{ t('messagesView.npcRecycleActivity') }}</CardTitle>
|
||||
<Badge v-if="!notification.read" variant="default" class="text-xs">{{ t('messagesView.unread') }}</Badge>
|
||||
</div>
|
||||
<Button
|
||||
@click.stop="deleteNPCActivityNotification(notification.id)"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8 flex-shrink-0"
|
||||
>
|
||||
{{ report.winner === 'attacker' ? t('messagesView.victory') : report.winner === 'defender' ? t('messagesView.defeat') : t('messagesView.draw') }}
|
||||
</Badge>
|
||||
<X class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<Button @click.stop="deleteBattleReport(report.id)" variant="ghost" size="icon" class="h-8 w-8 flex-shrink-0">
|
||||
<X class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<CardDescription class="text-xs sm:text-sm">
|
||||
{{ formatDate(report.timestamp) }}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</div>
|
||||
<CardDescription class="text-xs sm:text-sm">
|
||||
{{ notification.npcName }} →
|
||||
{{
|
||||
notification.targetPlanetName ||
|
||||
`[${notification.targetPosition.galaxy}:${notification.targetPosition.system}:${notification.targetPosition.position}]`
|
||||
}}
|
||||
· {{ formatDate(notification.timestamp) }}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
|
||||
<!-- 间谍报告列表 -->
|
||||
<div v-if="activeTab === 'spy'" class="space-y-2">
|
||||
<Card v-if="gameStore.player.spyReports.length === 0">
|
||||
<CardContent class="py-8 text-center text-muted-foreground">{{ t('messagesView.noSpyReports') }}</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
v-for="report in sortedSpyReports"
|
||||
:key="report.id"
|
||||
@click="openSpyReport(report)"
|
||||
class="cursor-pointer hover:shadow-md transition-shadow"
|
||||
>
|
||||
<CardHeader class="pb-3">
|
||||
<div class="flex justify-between items-center gap-2">
|
||||
<div class="flex items-center gap-2 min-w-0 flex-1">
|
||||
<Eye class="h-4 w-4 flex-shrink-0" />
|
||||
<CardTitle class="text-base sm:text-lg">{{ t('messagesView.spyReport') }}</CardTitle>
|
||||
<Badge v-if="!report.read" variant="default" class="text-xs">{{ t('messagesView.unread') }}</Badge>
|
||||
<Badge variant="outline" class="text-xs">{{ report.targetPlanetId }}</Badge>
|
||||
<!-- 礼物通知 -->
|
||||
<Card
|
||||
v-for="gift in sortedGiftNotifications"
|
||||
:key="gift.id"
|
||||
@click="markGiftAsRead(gift)"
|
||||
class="hover:shadow-md transition-shadow"
|
||||
>
|
||||
<CardHeader class="pb-3">
|
||||
<div class="flex justify-between items-center gap-2">
|
||||
<div class="flex items-center gap-2 min-w-0 flex-1">
|
||||
<Gift class="h-4 w-4 flex-shrink-0 text-green-600" />
|
||||
<CardTitle class="text-base sm:text-lg">{{ t('messagesView.giftFrom').replace('{npcName}', gift.fromNpcName) }}</CardTitle>
|
||||
<Badge v-if="!gift.read" variant="default" class="text-xs">{{ t('messagesView.unread') }}</Badge>
|
||||
</div>
|
||||
<Button @click.stop="deleteGiftNotification(gift.id)" variant="ghost" size="icon" class="h-8 w-8 flex-shrink-0">
|
||||
<X class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<Button @click.stop="deleteSpyReport(report.id)" variant="ghost" size="icon" class="h-8 w-8 flex-shrink-0">
|
||||
<X class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<CardDescription class="text-xs sm:text-sm">
|
||||
{{ formatDate(report.timestamp) }}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</div>
|
||||
<CardDescription class="text-xs sm:text-sm">{{ formatDate(gift.timestamp) }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="space-y-3">
|
||||
<div class="text-sm">
|
||||
<div class="font-semibold mb-1">{{ t('messagesView.giftResources') }}:</div>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<div v-if="gift.resources.metal > 0">{{ t('resources.metal') }}: {{ gift.resources.metal.toLocaleString() }}</div>
|
||||
<div v-if="gift.resources.crystal > 0">{{ t('resources.crystal') }}: {{ gift.resources.crystal.toLocaleString() }}</div>
|
||||
<div v-if="gift.resources.deuterium > 0">
|
||||
{{ t('resources.deuterium') }}: {{ gift.resources.deuterium.toLocaleString() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-xs text-muted-foreground">
|
||||
{{ t('messagesView.expectedReputation') }}:
|
||||
<span class="text-green-600">+{{ gift.expectedReputationGain }}</span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<Button @click.stop="acceptGift(gift)" variant="default" size="sm" class="flex-1">
|
||||
<Check class="h-4 w-4 mr-1" />
|
||||
{{ t('messagesView.acceptGift') }}
|
||||
</Button>
|
||||
<Button @click.stop="rejectGift(gift)" variant="outline" size="sm" class="flex-1">
|
||||
<Ban class="h-4 w-4 mr-1" />
|
||||
{{ t('messagesView.rejectGift') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 礼物被拒绝通知 -->
|
||||
<Card
|
||||
v-for="rejection in sortedGiftRejectedNotifications"
|
||||
:key="rejection.id"
|
||||
@click="markGiftRejectedAsRead(rejection)"
|
||||
class="hover:shadow-md transition-shadow"
|
||||
>
|
||||
<CardHeader class="pb-3">
|
||||
<div class="flex justify-between items-center gap-2">
|
||||
<div class="flex items-center gap-2 min-w-0 flex-1">
|
||||
<Ban class="h-4 w-4 flex-shrink-0 text-red-600" />
|
||||
<CardTitle class="text-base sm:text-lg">
|
||||
{{ t('messagesView.giftRejectedBy').replace('{npcName}', rejection.npcName) }}
|
||||
</CardTitle>
|
||||
<Badge v-if="!rejection.read" variant="default" class="text-xs">{{ t('messagesView.unread') }}</Badge>
|
||||
</div>
|
||||
<Button @click.stop="deleteGiftRejectedNotification(rejection.id)" variant="ghost" size="icon" class="h-8 w-8 flex-shrink-0">
|
||||
<X class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<CardDescription class="text-xs sm:text-sm">{{ formatDate(rejection.timestamp) }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="space-y-2">
|
||||
<div class="text-sm">
|
||||
<div class="font-semibold mb-1">{{ t('messagesView.rejectedResources') }}:</div>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<div v-if="rejection.rejectedResources.metal > 0">
|
||||
{{ t('resources.metal') }}: {{ rejection.rejectedResources.metal.toLocaleString() }}
|
||||
</div>
|
||||
<div v-if="rejection.rejectedResources.crystal > 0">
|
||||
{{ t('resources.crystal') }}: {{ rejection.rejectedResources.crystal.toLocaleString() }}
|
||||
</div>
|
||||
<div v-if="rejection.rejectedResources.deuterium > 0">
|
||||
{{ t('resources.deuterium') }}: {{ rejection.rejectedResources.deuterium.toLocaleString() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-xs text-muted-foreground">
|
||||
{{ t('messagesView.currentReputation') }}:
|
||||
<span :class="rejection.currentReputation >= 0 ? 'text-green-600' : 'text-red-600'">{{ rejection.currentReputation }}</span>
|
||||
</div>
|
||||
<div class="text-xs text-muted-foreground">
|
||||
{{ t('messagesView.rejectionReason.' + rejection.reason) }}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<!-- 任务报告列表 -->
|
||||
<TabsContent value="missions" class="mt-4 space-y-2">
|
||||
<Card v-if="sortedMissionReports.length === 0">
|
||||
<CardContent class="py-8 text-center text-muted-foreground">{{ t('messagesView.noMissionReports') }}</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
v-for="report in sortedMissionReports"
|
||||
:key="report.id"
|
||||
@click="openMissionReport(report)"
|
||||
class="cursor-pointer hover:shadow-md transition-shadow"
|
||||
>
|
||||
<CardHeader class="pb-3">
|
||||
<div class="flex justify-between items-center gap-2">
|
||||
<div class="flex items-center gap-2 min-w-0 flex-1">
|
||||
<Package class="h-4 w-4 flex-shrink-0" />
|
||||
<CardTitle class="text-base sm:text-lg">{{ getMissionTypeName(report.missionType) }}</CardTitle>
|
||||
<Badge v-if="!report.read" variant="default" class="text-xs">{{ t('messagesView.unread') }}</Badge>
|
||||
<Badge :variant="report.success ? 'default' : 'destructive'" class="text-xs">
|
||||
{{ report.success ? t('messagesView.success') : t('messagesView.failed') }}
|
||||
</Badge>
|
||||
</div>
|
||||
<Button @click.stop="deleteMissionReport(report.id)" variant="ghost" size="icon" class="h-8 w-8 flex-shrink-0">
|
||||
<X class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<CardDescription class="text-xs sm:text-sm">
|
||||
{{ report.originPlanetName }} →
|
||||
{{
|
||||
report.targetPlanetName ||
|
||||
`[${report.targetPosition.galaxy}:${report.targetPosition.system}:${report.targetPosition.position}]`
|
||||
}}
|
||||
· {{ formatDate(report.timestamp) }}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<!-- 战斗报告对话框 -->
|
||||
<BattleReportDialog v-model:open="showBattleDialog" :report="selectedBattleReport" />
|
||||
@@ -96,17 +304,30 @@
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
import { computed, ref } from 'vue'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import BattleReportDialog from '@/components/BattleReportDialog.vue'
|
||||
import SpyReportDialog from '@/components/SpyReportDialog.vue'
|
||||
import { formatDate } from '@/utils/format'
|
||||
import { X, Sword, Eye } from 'lucide-vue-next'
|
||||
import type { BattleResult, SpyReport } from '@/types/game'
|
||||
import { X, Sword, Eye, AlertTriangle, Package, Recycle, Gift, Ban, Check, Users } from 'lucide-vue-next'
|
||||
import type {
|
||||
BattleResult,
|
||||
SpyReport,
|
||||
SpiedNotification,
|
||||
NPCActivityNotification,
|
||||
MissionReport,
|
||||
GiftNotification,
|
||||
GiftRejectedNotification
|
||||
} from '@/types/game'
|
||||
import { MissionType } from '@/types/game'
|
||||
import { useNPCStore } from '@/stores/npcStore'
|
||||
import * as diplomaticLogic from '@/logic/diplomaticLogic'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const npcStore = useNPCStore()
|
||||
const { t } = useI18n()
|
||||
const activeTab = ref<'battles' | 'spy'>('battles')
|
||||
const activeTab = ref<'battles' | 'spy' | 'missions' | 'npc'>('battles')
|
||||
|
||||
// 对话框状态
|
||||
const showBattleDialog = ref(false)
|
||||
@@ -124,6 +345,30 @@
|
||||
return [...gameStore.player.spyReports].sort((a, b) => b.timestamp - a.timestamp)
|
||||
})
|
||||
|
||||
// 排序后的被侦查通知(最新的在前)
|
||||
const sortedSpiedNotifications = computed(() => {
|
||||
if (!gameStore.player.spiedNotifications) {
|
||||
return []
|
||||
}
|
||||
return [...gameStore.player.spiedNotifications].sort((a, b) => b.timestamp - a.timestamp)
|
||||
})
|
||||
|
||||
// 排序后的任务报告(最新的在前)
|
||||
const sortedMissionReports = computed(() => {
|
||||
if (!gameStore.player.missionReports) {
|
||||
return []
|
||||
}
|
||||
return [...gameStore.player.missionReports].sort((a, b) => b.timestamp - a.timestamp)
|
||||
})
|
||||
|
||||
// 排序后的NPC活动通知(最新的在前)
|
||||
const sortedNPCActivityNotifications = computed(() => {
|
||||
if (!gameStore.player.npcActivityNotifications) {
|
||||
return []
|
||||
}
|
||||
return [...gameStore.player.npcActivityNotifications].sort((a, b) => b.timestamp - a.timestamp)
|
||||
})
|
||||
|
||||
// 未读战斗报告数量
|
||||
const unreadBattles = computed(() => {
|
||||
return gameStore.player.battleReports.filter(r => !r.read).length
|
||||
@@ -134,6 +379,126 @@
|
||||
return gameStore.player.spyReports.filter(r => !r.read).length
|
||||
})
|
||||
|
||||
// 未读被侦查通知数量
|
||||
const unreadSpiedNotifications = computed(() => {
|
||||
if (!gameStore.player.spiedNotifications) {
|
||||
return 0
|
||||
}
|
||||
return gameStore.player.spiedNotifications.filter(n => !n.read).length
|
||||
})
|
||||
|
||||
// 未读NPC活动通知数量
|
||||
const unreadNPCActivity = computed(() => {
|
||||
if (!gameStore.player.npcActivityNotifications) {
|
||||
return 0
|
||||
}
|
||||
return gameStore.player.npcActivityNotifications.filter(n => !n.read).length
|
||||
})
|
||||
|
||||
// 未读任务报告数量
|
||||
const unreadMissionReports = computed(() => {
|
||||
if (!gameStore.player.missionReports) {
|
||||
return 0
|
||||
}
|
||||
return gameStore.player.missionReports.filter(r => !r.read).length
|
||||
})
|
||||
|
||||
// 未读礼物通知数量
|
||||
const unreadGiftNotifications = computed(() => {
|
||||
if (!gameStore.player.giftNotifications) {
|
||||
return 0
|
||||
}
|
||||
return gameStore.player.giftNotifications.filter(n => !n.read).length
|
||||
})
|
||||
|
||||
// 未读礼物被拒绝通知数量
|
||||
const unreadGiftRejected = computed(() => {
|
||||
if (!gameStore.player.giftRejectedNotifications) {
|
||||
return 0
|
||||
}
|
||||
return gameStore.player.giftRejectedNotifications.filter(n => !n.read).length
|
||||
})
|
||||
|
||||
// 合并:侦查相关未读总数(侦查报告 + 被侦查通知)
|
||||
const unreadSpyTotal = computed(() => {
|
||||
return unreadSpyReports.value + unreadSpiedNotifications.value
|
||||
})
|
||||
|
||||
// 合并:NPC相关未读总数(NPC活动 + 礼物通知 + 礼物被拒绝)
|
||||
const unreadNPCTotal = computed(() => {
|
||||
return unreadNPCActivity.value + unreadGiftNotifications.value + unreadGiftRejected.value
|
||||
})
|
||||
|
||||
// 标签页配置
|
||||
const tabs = computed(() => [
|
||||
{
|
||||
value: 'battles',
|
||||
icon: Sword,
|
||||
label: t('messagesView.battles'),
|
||||
unreadCount: unreadBattles.value
|
||||
},
|
||||
{
|
||||
value: 'spy',
|
||||
icon: Eye,
|
||||
label: t('messagesView.spy'),
|
||||
unreadCount: unreadSpyTotal.value
|
||||
},
|
||||
{
|
||||
value: 'missions',
|
||||
icon: Package,
|
||||
label: t('messagesView.missions'),
|
||||
unreadCount: unreadMissionReports.value
|
||||
},
|
||||
{
|
||||
value: 'npc',
|
||||
icon: Users,
|
||||
label: t('messagesView.npc'),
|
||||
unreadCount: unreadNPCTotal.value
|
||||
}
|
||||
])
|
||||
|
||||
// 排序后的礼物通知(最新的在前)
|
||||
const sortedGiftNotifications = computed(() => {
|
||||
if (!gameStore.player.giftNotifications) {
|
||||
return []
|
||||
}
|
||||
return [...gameStore.player.giftNotifications].sort((a, b) => b.timestamp - a.timestamp)
|
||||
})
|
||||
|
||||
// 排序后的礼物被拒绝通知(最新的在前)
|
||||
const sortedGiftRejectedNotifications = computed(() => {
|
||||
if (!gameStore.player.giftRejectedNotifications) {
|
||||
return []
|
||||
}
|
||||
return [...gameStore.player.giftRejectedNotifications].sort((a, b) => b.timestamp - a.timestamp)
|
||||
})
|
||||
|
||||
// 判断战斗结果Badge颜色
|
||||
const getBattleResultVariant = (report: BattleResult): 'default' | 'destructive' | 'secondary' => {
|
||||
if (report.winner === 'draw') {
|
||||
return 'secondary'
|
||||
}
|
||||
|
||||
// 判断玩家是攻击方还是防守方
|
||||
const isPlayerAttacker = report.attackerId === gameStore.player.id
|
||||
const playerWon = isPlayerAttacker ? report.winner === 'attacker' : report.winner === 'defender'
|
||||
|
||||
return playerWon ? 'default' : 'destructive'
|
||||
}
|
||||
|
||||
// 获取战斗结果文本
|
||||
const getBattleResultText = (report: BattleResult): string => {
|
||||
if (report.winner === 'draw') {
|
||||
return t('messagesView.draw')
|
||||
}
|
||||
|
||||
// 判断玩家是攻击方还是防守方
|
||||
const isPlayerAttacker = report.attackerId === gameStore.player.id
|
||||
const playerWon = isPlayerAttacker ? report.winner === 'attacker' : report.winner === 'defender'
|
||||
|
||||
return playerWon ? t('messagesView.victory') : t('messagesView.defeat')
|
||||
}
|
||||
|
||||
// 打开战斗报告
|
||||
const openBattleReport = (report: BattleResult) => {
|
||||
selectedBattleReport.value = report
|
||||
@@ -154,6 +519,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 打开被侦查通知
|
||||
const openSpiedNotification = (notification: SpiedNotification) => {
|
||||
// 标记为已读
|
||||
if (!notification.read) {
|
||||
notification.read = true
|
||||
}
|
||||
}
|
||||
|
||||
// 删除战斗报告
|
||||
const deleteBattleReport = (reportId: string) => {
|
||||
const index = gameStore.player.battleReports.findIndex(r => r.id === reportId)
|
||||
@@ -169,4 +542,117 @@
|
||||
gameStore.player.spyReports.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除被侦查通知
|
||||
const deleteSpiedNotification = (notificationId: string) => {
|
||||
if (!gameStore.player.spiedNotifications) {
|
||||
return
|
||||
}
|
||||
const index = gameStore.player.spiedNotifications.findIndex(n => n.id === notificationId)
|
||||
if (index > -1) {
|
||||
gameStore.player.spiedNotifications.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 打开NPC活动通知
|
||||
const openNPCActivityNotification = (notification: NPCActivityNotification) => {
|
||||
// 标记为已读
|
||||
if (!notification.read) {
|
||||
notification.read = true
|
||||
}
|
||||
}
|
||||
|
||||
// 删除NPC活动通知
|
||||
const deleteNPCActivityNotification = (notificationId: string) => {
|
||||
if (!gameStore.player.npcActivityNotifications) {
|
||||
return
|
||||
}
|
||||
const index = gameStore.player.npcActivityNotifications.findIndex(n => n.id === notificationId)
|
||||
if (index > -1) {
|
||||
gameStore.player.npcActivityNotifications.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取任务类型名称
|
||||
const getMissionTypeName = (missionType: string): string => {
|
||||
const typeMap: Record<string, string> = {
|
||||
[MissionType.Transport]: t('fleetView.transport'),
|
||||
[MissionType.Colonize]: t('fleetView.colonize'),
|
||||
[MissionType.Deploy]: t('fleetView.deploy'),
|
||||
[MissionType.Recycle]: t('fleetView.recycle'),
|
||||
[MissionType.Destroy]: t('fleetView.destroy')
|
||||
}
|
||||
return typeMap[missionType] || missionType
|
||||
}
|
||||
|
||||
// 打开任务报告
|
||||
const openMissionReport = (report: MissionReport) => {
|
||||
// 标记为已读
|
||||
if (!report.read) {
|
||||
report.read = true
|
||||
}
|
||||
}
|
||||
|
||||
// 删除任务报告
|
||||
const deleteMissionReport = (reportId: string) => {
|
||||
if (!gameStore.player.missionReports) {
|
||||
return
|
||||
}
|
||||
const index = gameStore.player.missionReports.findIndex(r => r.id === reportId)
|
||||
if (index > -1) {
|
||||
gameStore.player.missionReports.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 标记礼物通知为已读
|
||||
const markGiftAsRead = (gift: GiftNotification) => {
|
||||
if (!gift.read) {
|
||||
gift.read = true
|
||||
}
|
||||
}
|
||||
|
||||
// 接受礼物
|
||||
const acceptGift = (gift: GiftNotification) => {
|
||||
const npc = npcStore.npcs.find(n => n.id === gift.fromNpcId)
|
||||
if (npc) {
|
||||
diplomaticLogic.acceptNPCGift(gameStore.player, npc, gift)
|
||||
}
|
||||
}
|
||||
|
||||
// 拒绝礼物
|
||||
const rejectGift = (gift: GiftNotification) => {
|
||||
const npc = npcStore.npcs.find(n => n.id === gift.fromNpcId)
|
||||
if (npc) {
|
||||
diplomaticLogic.rejectNPCGift(gameStore.player, npc, gift)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除礼物通知
|
||||
const deleteGiftNotification = (giftId: string) => {
|
||||
if (!gameStore.player.giftNotifications) {
|
||||
return
|
||||
}
|
||||
const index = gameStore.player.giftNotifications.findIndex(g => g.id === giftId)
|
||||
if (index > -1) {
|
||||
gameStore.player.giftNotifications.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 标记礼物被拒绝通知为已读
|
||||
const markGiftRejectedAsRead = (rejection: GiftRejectedNotification) => {
|
||||
if (!rejection.read) {
|
||||
rejection.read = true
|
||||
}
|
||||
}
|
||||
|
||||
// 删除礼物被拒绝通知
|
||||
const deleteGiftRejectedNotification = (rejectionId: string) => {
|
||||
if (!gameStore.player.giftRejectedNotifications) {
|
||||
return
|
||||
}
|
||||
const index = gameStore.player.giftRejectedNotifications.findIndex(r => r.id === rejectionId)
|
||||
if (index > -1) {
|
||||
gameStore.player.giftRejectedNotifications.splice(index, 1)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -5,14 +5,18 @@
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 gap-4 sm:gap-6">
|
||||
<Card v-for="officerType in Object.values(OfficerType)" :key="officerType">
|
||||
<CardHeader>
|
||||
<div class="flex justify-between items-start gap-2">
|
||||
<div class="min-w-0 flex-1">
|
||||
<CardTitle class="text-lg sm:text-xl">{{ OFFICERS[officerType].name }}</CardTitle>
|
||||
<CardDescription class="text-xs sm:text-sm">{{ OFFICERS[officerType].description }}</CardDescription>
|
||||
<div class="mb-2">
|
||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-2">
|
||||
<CardTitle class="text-sm sm:text-base lg:text-lg order-2 sm:order-1">{{ OFFICERS[officerType].name }}</CardTitle>
|
||||
<Badge v-if="isOfficerActive(officerType)" variant="default" class="text-xs whitespace-nowrap self-start order-1 sm:order-2">
|
||||
{{ t('officersView.activated') }}
|
||||
</Badge>
|
||||
<Badge v-else variant="outline" class="text-xs whitespace-nowrap self-start order-1 sm:order-2">
|
||||
{{ t('officersView.inactive') }}
|
||||
</Badge>
|
||||
</div>
|
||||
<Badge v-if="isOfficerActive(officerType)" variant="default" class="text-xs">{{ t('officersView.activated') }}</Badge>
|
||||
<Badge v-else variant="outline" class="text-xs">{{ t('officersView.inactive') }}</Badge>
|
||||
</div>
|
||||
<CardDescription class="text-xs sm:text-sm">{{ OFFICERS[officerType].description }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<!-- 状态信息 -->
|
||||
@@ -30,44 +34,21 @@
|
||||
<div class="space-y-2">
|
||||
<p class="text-sm font-medium text-muted-foreground">{{ t('officersView.recruitCost') }} (7{{ t('officersView.days') }}):</p>
|
||||
<div class="space-y-1.5">
|
||||
<div class="flex items-center gap-2">
|
||||
<ResourceIcon type="metal" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.metal') }}:</span>
|
||||
<div
|
||||
v-for="resourceType in costResourceTypes"
|
||||
:key="resourceType.key"
|
||||
v-show="resourceType.key !== 'darkMatter' || OFFICERS[officerType].cost.darkMatter > 0"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<ResourceIcon :type="resourceType.key" size="sm" />
|
||||
<span class="text-xs">{{ t(`resources.${resourceType.key}`) }}:</span>
|
||||
<span
|
||||
class="font-medium text-sm"
|
||||
:class="planet ? getResourceCostColor(planet.resources.metal, OFFICERS[officerType].cost.metal) : ''"
|
||||
:class="
|
||||
planet ? getResourceCostColor(planet.resources[resourceType.key], OFFICERS[officerType].cost[resourceType.key]) : ''
|
||||
"
|
||||
>
|
||||
{{ formatNumber(OFFICERS[officerType].cost.metal) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<ResourceIcon type="crystal" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.crystal') }}:</span>
|
||||
<span
|
||||
class="font-medium text-sm"
|
||||
:class="planet ? getResourceCostColor(planet.resources.crystal, OFFICERS[officerType].cost.crystal) : ''"
|
||||
>
|
||||
{{ formatNumber(OFFICERS[officerType].cost.crystal) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<ResourceIcon type="deuterium" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.deuterium') }}:</span>
|
||||
<span
|
||||
class="font-medium text-sm"
|
||||
:class="planet ? getResourceCostColor(planet.resources.deuterium, OFFICERS[officerType].cost.deuterium) : ''"
|
||||
>
|
||||
{{ formatNumber(OFFICERS[officerType].cost.deuterium) }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="OFFICERS[officerType].cost.darkMatter > 0" class="flex items-center gap-2">
|
||||
<ResourceIcon type="darkMatter" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.darkMatter') }}:</span>
|
||||
<span
|
||||
class="font-medium text-sm"
|
||||
:class="planet ? getResourceCostColor(planet.resources.darkMatter, OFFICERS[officerType].cost.darkMatter) : ''"
|
||||
>
|
||||
{{ formatNumber(OFFICERS[officerType].cost.darkMatter) }}
|
||||
{{ formatNumber(OFFICERS[officerType].cost[resourceType.key]) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -121,14 +102,19 @@
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex gap-2">
|
||||
<Button v-if="!isOfficerActive(officerType)" @click="handleHire(officerType)" :disabled="!canHire(officerType)" class="flex-1">
|
||||
<div class="flex flex-col sm:flex-row gap-2">
|
||||
<Button v-if="!isOfficerActive(officerType)" @click="handleHire(officerType)" :disabled="!canHire(officerType)" class="w-full">
|
||||
{{ t('officersView.hire') }} (7{{ t('officersView.days') }})
|
||||
</Button>
|
||||
<Button v-if="isOfficerActive(officerType)" @click="handleRenew(officerType)" :disabled="!canHire(officerType)" class="flex-1">
|
||||
<Button
|
||||
v-if="isOfficerActive(officerType)"
|
||||
@click="handleRenew(officerType)"
|
||||
:disabled="!canHire(officerType)"
|
||||
class="w-full sm:flex-1"
|
||||
>
|
||||
{{ t('officersView.renew') }} (7{{ t('officersView.days') }})
|
||||
</Button>
|
||||
<Button v-if="isOfficerActive(officerType)" @click="handleDismiss(officerType)" variant="outline" size="sm">
|
||||
<Button v-if="isOfficerActive(officerType)" @click="handleDismiss(officerType)" variant="outline" class="w-full sm:w-auto">
|
||||
{{ t('officersView.dismiss') }}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -136,9 +122,36 @@
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- 提示对话框 -->
|
||||
<AlertDialog :open="alertDialogOpen" @update:open="alertDialogOpen = $event">
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{{ alertDialogTitle }}</AlertDialogTitle>
|
||||
<AlertDialogDescription class="whitespace-pre-line">
|
||||
{{ alertDialogMessage }}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogAction>{{ t('common.confirm') }}</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
<!-- 确认对话框 -->
|
||||
<AlertDialog ref="alertDialog" />
|
||||
<ConfirmDialog ref="confirmDialog" />
|
||||
<AlertDialog :open="confirmDialogOpen" @update:open="confirmDialogOpen = $event">
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{{ confirmDialogTitle }}</AlertDialogTitle>
|
||||
<AlertDialogDescription class="whitespace-pre-line">
|
||||
{{ confirmDialogMessage }}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>{{ t('common.cancel') }}</AlertDialogCancel>
|
||||
<AlertDialogAction @click="handleConfirmDialogConfirm">{{ t('common.confirm') }}</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -150,8 +163,16 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import ResourceIcon from '@/components/ResourceIcon.vue'
|
||||
import AlertDialog from '@/components/AlertDialog.vue'
|
||||
import ConfirmDialog from '@/components/ConfirmDialog.vue'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { formatNumber, formatTime, formatDate, getResourceCostColor } from '@/utils/format'
|
||||
import * as officerLogic from '@/logic/officerLogic'
|
||||
import * as resourceLogic from '@/logic/resourceLogic'
|
||||
@@ -162,8 +183,32 @@
|
||||
const { OFFICERS } = useGameConfig()
|
||||
const gameStore = useGameStore()
|
||||
const planet = computed(() => gameStore.currentPlanet)
|
||||
const alertDialog = ref<InstanceType<typeof AlertDialog> | null>(null)
|
||||
const confirmDialog = ref<InstanceType<typeof ConfirmDialog> | null>(null)
|
||||
|
||||
// AlertDialog 状态
|
||||
const alertDialogOpen = ref(false)
|
||||
const alertDialogTitle = ref('')
|
||||
const alertDialogMessage = ref('')
|
||||
|
||||
// ConfirmDialog 状态
|
||||
const confirmDialogOpen = ref(false)
|
||||
const confirmDialogTitle = ref('')
|
||||
const confirmDialogMessage = ref('')
|
||||
const confirmDialogAction = ref<(() => void) | null>(null)
|
||||
|
||||
const handleConfirmDialogConfirm = () => {
|
||||
if (confirmDialogAction.value) {
|
||||
confirmDialogAction.value()
|
||||
}
|
||||
confirmDialogOpen.value = false
|
||||
}
|
||||
|
||||
// 资源类型配置(用于成本显示)
|
||||
const costResourceTypes = [
|
||||
{ key: 'metal' as const },
|
||||
{ key: 'crystal' as const },
|
||||
{ key: 'deuterium' as const },
|
||||
{ key: 'darkMatter' as const }
|
||||
]
|
||||
|
||||
// 检查军官是否激活
|
||||
const isOfficerActive = (officerType: OfficerType): boolean => {
|
||||
@@ -212,19 +257,17 @@
|
||||
|
||||
// 招募军官
|
||||
const handleHire = (officerType: OfficerType) => {
|
||||
confirmDialog.value?.show({
|
||||
title: t('officersView.hireTitle'),
|
||||
message: t('officersView.hireMessage').replace('{name}', OFFICERS.value[officerType].name),
|
||||
onConfirm: () => {
|
||||
const success = hireOfficer(officerType, 7)
|
||||
if (!success) {
|
||||
alertDialog.value?.show({
|
||||
title: t('officersView.hireFailed'),
|
||||
message: t('officersView.insufficientResources')
|
||||
})
|
||||
}
|
||||
confirmDialogTitle.value = t('officersView.hireTitle')
|
||||
confirmDialogMessage.value = t('officersView.hireMessage').replace('{name}', OFFICERS.value[officerType].name)
|
||||
confirmDialogAction.value = () => {
|
||||
const success = hireOfficer(officerType, 7)
|
||||
if (!success) {
|
||||
alertDialogTitle.value = t('officersView.hireFailed')
|
||||
alertDialogMessage.value = t('officersView.insufficientResources')
|
||||
alertDialogOpen.value = true
|
||||
}
|
||||
})
|
||||
}
|
||||
confirmDialogOpen.value = true
|
||||
}
|
||||
|
||||
const renewOfficer = (officerType: OfficerType, duration: number = 7): boolean => {
|
||||
@@ -241,29 +284,26 @@
|
||||
|
||||
// 续约军官
|
||||
const handleRenew = (officerType: OfficerType) => {
|
||||
confirmDialog.value?.show({
|
||||
title: t('officersView.renewTitle'),
|
||||
message: t('officersView.renewMessage').replace('{name}', OFFICERS.value[officerType].name),
|
||||
onConfirm: () => {
|
||||
const success = renewOfficer(officerType, 7)
|
||||
if (!success) {
|
||||
alertDialog.value?.show({
|
||||
title: t('officersView.renewFailed'),
|
||||
message: t('officersView.insufficientResources')
|
||||
})
|
||||
}
|
||||
confirmDialogTitle.value = t('officersView.renewTitle')
|
||||
confirmDialogMessage.value = t('officersView.renewMessage').replace('{name}', OFFICERS.value[officerType].name)
|
||||
confirmDialogAction.value = () => {
|
||||
const success = renewOfficer(officerType, 7)
|
||||
if (!success) {
|
||||
alertDialogTitle.value = t('officersView.renewFailed')
|
||||
alertDialogMessage.value = t('officersView.insufficientResources')
|
||||
alertDialogOpen.value = true
|
||||
}
|
||||
})
|
||||
}
|
||||
confirmDialogOpen.value = true
|
||||
}
|
||||
|
||||
// 解雇军官
|
||||
const handleDismiss = (officerType: OfficerType): void => {
|
||||
confirmDialog.value?.show({
|
||||
title: t('officersView.dismissTitle'),
|
||||
message: t('officersView.dismissMessage').replace('{name}', OFFICERS.value[officerType].name),
|
||||
onConfirm: () => {
|
||||
gameStore.player.officers[officerType] = officerLogic.createInactiveOfficer(officerType)
|
||||
}
|
||||
})
|
||||
confirmDialogTitle.value = t('officersView.dismissTitle')
|
||||
confirmDialogMessage.value = t('officersView.dismissMessage').replace('{name}', OFFICERS.value[officerType].name)
|
||||
confirmDialogAction.value = () => {
|
||||
gameStore.player.officers[officerType] = officerLogic.createInactiveOfficer(officerType)
|
||||
}
|
||||
confirmDialogOpen.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -21,160 +21,153 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 资源显示 -->
|
||||
<!-- 资源管理 -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('overview.resourceOverview') }}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>{{ t('common.resourceType') }}</TableHead>
|
||||
<TableHead class="text-right">{{ t('resources.current') }}</TableHead>
|
||||
<TableHead class="text-right">{{ t('resources.max') }}</TableHead>
|
||||
<TableHead class="text-right">{{ t('resources.production') }}{{ t('resources.perHour') }}</TableHead>
|
||||
<TableHead class="text-right">{{ t('resources.consumption') }}{{ t('resources.perHour') }}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow v-for="resourceType in resourceTypes" :key="resourceType.key">
|
||||
<TableCell class="font-medium">
|
||||
<div class="flex items-center gap-2">
|
||||
<Tabs default-value="overview" class="w-full">
|
||||
<TabsList class="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="overview">概览</TabsTrigger>
|
||||
<TabsTrigger value="production">产量详情</TabsTrigger>
|
||||
<TabsTrigger value="consumption">消耗详情</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<!-- 概览标签页 -->
|
||||
<TabsContent value="overview" class="mt-4">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>{{ t('common.resourceType') }}</TableHead>
|
||||
<TableHead class="text-right">{{ t('resources.current') }}</TableHead>
|
||||
<TableHead class="text-right">{{ t('resources.max') }}</TableHead>
|
||||
<TableHead class="text-right">{{ t('resources.production') }}{{ t('resources.perHour') }}</TableHead>
|
||||
<TableHead class="text-right">{{ t('resources.consumption') }}{{ t('resources.perHour') }}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow v-for="resourceType in resourceTypes" :key="resourceType.key">
|
||||
<TableCell class="font-medium">
|
||||
<div class="flex items-center gap-2">
|
||||
<ResourceIcon :type="resourceType.key" size="sm" />
|
||||
{{ t(`resources.${resourceType.key}`) }}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class="text-right"
|
||||
:class="getResourceColor(planet.resources[resourceType.key], capacity?.[resourceType.key] || Infinity)"
|
||||
>
|
||||
{{ formatNumber(planet.resources[resourceType.key]) }}
|
||||
</TableCell>
|
||||
<TableCell class="text-right text-muted-foreground">
|
||||
{{ formatNumber(capacity?.[resourceType.key] || 0) }}
|
||||
</TableCell>
|
||||
<TableCell class="text-right text-green-600 dark:text-green-400">
|
||||
+{{ formatNumber(production?.[resourceType.key] || 0) }}
|
||||
</TableCell>
|
||||
<TableCell class="text-right text-red-600 dark:text-red-400">
|
||||
<template v-if="resourceType.key === 'energy'">-{{ formatNumber(energyConsumption) }}</template>
|
||||
<template v-else>-</template>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TabsContent>
|
||||
|
||||
<!-- 产量详情标签页 -->
|
||||
<TabsContent value="production" class="mt-4">
|
||||
<div class="space-y-4">
|
||||
<div v-for="resourceType in resourceTypes" :key="resourceType.key" class="border-b last:border-b-0 pb-4 last:pb-0">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<ResourceIcon :type="resourceType.key" size="sm" />
|
||||
{{ t(`resources.${resourceType.key}`) }}
|
||||
<span class="font-semibold">{{ t(`resources.${resourceType.key}`) }}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<!-- 所有资源统一显示 -->
|
||||
<TableCell
|
||||
class="text-right"
|
||||
:class="getResourceColor(planet.resources[resourceType.key], capacity?.[resourceType.key] || Infinity)"
|
||||
|
||||
<div v-if="productionBreakdown" class="ml-6 space-y-1 text-sm">
|
||||
<!-- 电力有多个来源 -->
|
||||
<template v-if="resourceType.key === 'energy' && productionBreakdown.energy.sources">
|
||||
<div v-for="(source, idx) in productionBreakdown.energy.sources" :key="idx" class="flex justify-between">
|
||||
<span class="text-muted-foreground">
|
||||
{{ t(source.name) }}
|
||||
<template v-if="source.name.startsWith('buildings.')">({{ t('common.level') }} {{ source.level }})</template>
|
||||
<template v-else>({{ source.level }})</template>
|
||||
</span>
|
||||
<span class="text-green-600 dark:text-green-400">
|
||||
+{{ formatNumber(Math.floor(source.production)) }}/{{ t('resources.hour') }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 其他资源单一建筑产量 -->
|
||||
<template v-else>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">
|
||||
{{ t(productionBreakdown[resourceType.key].buildingName) }}
|
||||
({{ t('common.level') }} {{ productionBreakdown[resourceType.key].buildingLevel }})
|
||||
</span>
|
||||
<span class="text-green-600 dark:text-green-400">
|
||||
+{{ formatNumber(Math.floor(productionBreakdown[resourceType.key].baseProduction)) }}/{{ t('resources.hour') }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 加成列表 -->
|
||||
<div v-for="(bonus, idx) in productionBreakdown[resourceType.key].bonuses" :key="idx" class="flex justify-between">
|
||||
<span class="text-muted-foreground ml-4">
|
||||
{{ t(bonus.name) }} ({{ bonus.percentage > 0 ? '+' : '' }}{{ bonus.percentage }}%)
|
||||
</span>
|
||||
<span :class="bonus.value > 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'">
|
||||
{{ bonus.value > 0 ? '+' : '' }}{{ formatNumber(Math.floor(bonus.value)) }}/{{ t('resources.hour') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 最终产量 -->
|
||||
<div class="flex justify-between font-semibold pt-1 border-t mt-1">
|
||||
<span>{{ t('overview.totalProduction') }}</span>
|
||||
<span class="text-green-600 dark:text-green-400">
|
||||
+{{ formatNumber(Math.floor(productionBreakdown[resourceType.key].finalProduction)) }}/{{ t('resources.hour') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<!-- 消耗详情标签页 -->
|
||||
<TabsContent value="consumption" class="mt-4">
|
||||
<div class="space-y-2">
|
||||
<!-- 各建筑消耗 -->
|
||||
<div
|
||||
v-for="consumptionType in consumptionTypes"
|
||||
:key="consumptionType.key"
|
||||
v-show="consumptionBreakdown && consumptionBreakdown[consumptionType.key].buildingLevel > 0"
|
||||
class="flex justify-between text-sm"
|
||||
>
|
||||
{{ formatNumber(planet.resources[resourceType.key]) }}
|
||||
</TableCell>
|
||||
<TableCell class="text-right text-muted-foreground">
|
||||
{{ formatNumber(capacity?.[resourceType.key] || 0) }}
|
||||
</TableCell>
|
||||
<TableCell class="text-right text-green-600 dark:text-green-400">
|
||||
+{{ formatNumber(production?.[resourceType.key] || 0) }}
|
||||
</TableCell>
|
||||
<TableCell class="text-right text-red-600 dark:text-red-400">
|
||||
<template v-if="resourceType.key === 'energy'">
|
||||
-{{ formatNumber(energyConsumption) }}
|
||||
</template>
|
||||
<template v-else>
|
||||
-
|
||||
</template>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 资源获取来源 -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('overview.productionSources') }}</CardTitle>
|
||||
<CardDescription>{{ t('overview.productionSourcesDesc') }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="space-y-4">
|
||||
<div v-for="resourceType in resourceTypes" :key="resourceType.key" class="border-b last:border-b-0 pb-4 last:pb-0">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<ResourceIcon :type="resourceType.key" size="sm" />
|
||||
<span class="font-semibold">{{ t(`resources.${resourceType.key}`) }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="productionBreakdown" class="ml-6 space-y-1 text-sm">
|
||||
<!-- 建筑基础产量 -->
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">
|
||||
{{ t(productionBreakdown[resourceType.key].buildingName) }}
|
||||
({{ t('common.level') }} {{ productionBreakdown[resourceType.key].buildingLevel }})
|
||||
<span v-if="consumptionBreakdown" class="text-muted-foreground">
|
||||
{{ t(consumptionBreakdown[consumptionType.key].buildingName) }}
|
||||
({{ t('common.level') }} {{ consumptionBreakdown[consumptionType.key].buildingLevel }})
|
||||
</span>
|
||||
<span class="text-green-600 dark:text-green-400">
|
||||
+{{ formatNumber(Math.floor(productionBreakdown[resourceType.key].baseProduction)) }}/{{ t('resources.hour') }}
|
||||
<span v-if="consumptionBreakdown" class="text-red-600 dark:text-red-400">
|
||||
-{{ formatNumber(Math.floor(consumptionBreakdown[consumptionType.key].consumption)) }}/{{ t('resources.hour') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 加成列表 -->
|
||||
<div v-for="(bonus, idx) in productionBreakdown[resourceType.key].bonuses" :key="idx" class="flex justify-between">
|
||||
<span class="text-muted-foreground ml-4">
|
||||
{{ t(bonus.name) }}
|
||||
</span>
|
||||
<span :class="bonus.value > 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'">
|
||||
{{ bonus.value > 0 ? '+' : '' }}{{ bonus.value }}%
|
||||
<!-- 总消耗 -->
|
||||
<div v-if="consumptionBreakdown" class="flex justify-between font-semibold pt-2 border-t">
|
||||
<span>{{ t('overview.totalConsumption') }}</span>
|
||||
<span class="text-red-600 dark:text-red-400">
|
||||
-{{ formatNumber(Math.floor(consumptionBreakdown.total)) }}/{{ t('resources.hour') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 最终产量 -->
|
||||
<div class="flex justify-between font-semibold pt-1 border-t mt-1">
|
||||
<span>{{ t('overview.totalProduction') }}</span>
|
||||
<span class="text-green-600 dark:text-green-400">
|
||||
+{{ formatNumber(Math.floor(productionBreakdown[resourceType.key].finalProduction)) }}/{{ t('resources.hour') }}
|
||||
</span>
|
||||
<!-- 无消耗提示 -->
|
||||
<div v-if="consumptionBreakdown && consumptionBreakdown.total === 0" class="text-sm text-muted-foreground text-center py-2">
|
||||
{{ t('overview.noConsumption') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 资源消耗来源 -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('overview.consumptionSources') }}</CardTitle>
|
||||
<CardDescription>{{ t('overview.consumptionSourcesDesc') }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="space-y-2">
|
||||
<!-- 金属矿消耗 -->
|
||||
<div v-if="consumptionBreakdown && consumptionBreakdown.metalMine.buildingLevel > 0" class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">
|
||||
{{ t(consumptionBreakdown.metalMine.buildingName) }}
|
||||
({{ t('common.level') }} {{ consumptionBreakdown.metalMine.buildingLevel }})
|
||||
</span>
|
||||
<span class="text-red-600 dark:text-red-400">
|
||||
-{{ formatNumber(Math.floor(consumptionBreakdown.metalMine.consumption)) }}/{{ t('resources.hour') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 晶体矿消耗 -->
|
||||
<div v-if="consumptionBreakdown && consumptionBreakdown.crystalMine.buildingLevel > 0" class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">
|
||||
{{ t(consumptionBreakdown.crystalMine.buildingName) }}
|
||||
({{ t('common.level') }} {{ consumptionBreakdown.crystalMine.buildingLevel }})
|
||||
</span>
|
||||
<span class="text-red-600 dark:text-red-400">
|
||||
-{{ formatNumber(Math.floor(consumptionBreakdown.crystalMine.consumption)) }}/{{ t('resources.hour') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 重氢合成器消耗 -->
|
||||
<div v-if="consumptionBreakdown && consumptionBreakdown.deuteriumSynthesizer.buildingLevel > 0" class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">
|
||||
{{ t(consumptionBreakdown.deuteriumSynthesizer.buildingName) }}
|
||||
({{ t('common.level') }} {{ consumptionBreakdown.deuteriumSynthesizer.buildingLevel }})
|
||||
</span>
|
||||
<span class="text-red-600 dark:text-red-400">
|
||||
-{{ formatNumber(Math.floor(consumptionBreakdown.deuteriumSynthesizer.consumption)) }}/{{ t('resources.hour') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 总消耗 -->
|
||||
<div v-if="consumptionBreakdown" class="flex justify-between font-semibold pt-2 border-t">
|
||||
<span>{{ t('overview.totalConsumption') }}</span>
|
||||
<span class="text-red-600 dark:text-red-400">
|
||||
-{{ formatNumber(Math.floor(consumptionBreakdown.total)) }}/{{ t('resources.hour') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 无消耗提示 -->
|
||||
<div v-if="consumptionBreakdown && consumptionBreakdown.total === 0" class="text-sm text-muted-foreground text-center py-2">
|
||||
{{ t('overview.noConsumption') }}
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -202,6 +195,7 @@
|
||||
import { useGameConfig } from '@/composables/useGameConfig'
|
||||
import { computed } from 'vue'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
@@ -210,7 +204,6 @@
|
||||
import type { Planet } from '@/types/game'
|
||||
import * as publicLogic from '@/logic/publicLogic'
|
||||
import * as resourceLogic from '@/logic/resourceLogic'
|
||||
import * as officerLogic from '@/logic/officerLogic'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const { t } = useI18n()
|
||||
@@ -228,8 +221,7 @@
|
||||
// 资源产量详细breakdown
|
||||
const productionBreakdown = computed(() => {
|
||||
if (!planet.value) return null
|
||||
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, Date.now())
|
||||
return resourceLogic.calculateProductionBreakdown(planet.value, bonuses)
|
||||
return resourceLogic.calculateProductionBreakdown(planet.value, gameStore.player.officers, Date.now())
|
||||
})
|
||||
|
||||
// 资源消耗详细breakdown
|
||||
@@ -243,10 +235,13 @@
|
||||
{ key: 'metal' as const },
|
||||
{ key: 'crystal' as const },
|
||||
{ key: 'deuterium' as const },
|
||||
{ key: 'darkMatter' as const },
|
||||
{ key: 'energy' as const }
|
||||
{ key: 'energy' as const },
|
||||
{ key: 'darkMatter' as const }
|
||||
]
|
||||
|
||||
// 消耗类型配置
|
||||
const consumptionTypes = [{ key: 'metalMine' as const }, { key: 'crystalMine' as const }, { key: 'deuteriumSynthesizer' as const }]
|
||||
|
||||
// 月球相关
|
||||
const moon = computed(() => {
|
||||
if (!planet.value || planet.value.isMoon) return null
|
||||
|
||||
@@ -9,54 +9,44 @@
|
||||
<Card v-for="techType in Object.values(TechnologyType)" :key="techType" class="relative">
|
||||
<CardUnlockOverlay :requirements="TECHNOLOGIES[techType].requirements" :currentLevel="getTechLevel(techType)" />
|
||||
<CardHeader>
|
||||
<div class="flex justify-between items-start gap-2">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="mb-2">
|
||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-2">
|
||||
<CardTitle
|
||||
class="text-base sm:text-lg cursor-pointer hover:text-primary transition-colors"
|
||||
class="text-sm sm:text-base lg:text-lg cursor-pointer hover:text-primary transition-colors underline decoration-dotted underline-offset-4 order-2 sm:order-1"
|
||||
@click="detailDialog.openTechnology(techType, getTechLevel(techType))"
|
||||
>
|
||||
{{ TECHNOLOGIES[techType].name }}
|
||||
</CardTitle>
|
||||
<CardDescription class="text-xs sm:text-sm">{{ TECHNOLOGIES[techType].description }}</CardDescription>
|
||||
<Badge variant="secondary" class="text-xs whitespace-nowrap self-start order-1 sm:order-2">
|
||||
Lv {{ getTechLevel(techType) }}
|
||||
</Badge>
|
||||
</div>
|
||||
<Badge variant="secondary" class="text-xs whitespace-nowrap flex-shrink-0">Lv {{ getTechLevel(techType) }}</Badge>
|
||||
</div>
|
||||
<CardDescription class="text-xs sm:text-sm">{{ TECHNOLOGIES[techType].description }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="space-y-2.5 sm:space-y-3">
|
||||
<div class="text-xs sm:text-sm space-y-1.5 sm:space-y-2">
|
||||
<p class="text-muted-foreground mb-1 sm:mb-2">{{ t('researchView.researchCost') }}:</p>
|
||||
<div class="space-y-1 sm:space-y-1.5">
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="metal" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.metal') }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="getResourceCostColor(planet.resources.metal, getTechnologyCost(techType, getTechLevel(techType) + 1).metal)"
|
||||
>
|
||||
{{ formatNumber(getTechnologyCost(techType, getTechLevel(techType) + 1).metal) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="crystal" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.crystal') }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="getResourceCostColor(planet.resources.crystal, getTechnologyCost(techType, getTechLevel(techType) + 1).crystal)"
|
||||
>
|
||||
{{ formatNumber(getTechnologyCost(techType, getTechLevel(techType) + 1).crystal) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="deuterium" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.deuterium') }}:</span>
|
||||
<div
|
||||
v-for="resourceType in costResourceTypes"
|
||||
:key="resourceType.key"
|
||||
v-show="resourceType.key !== 'darkMatter' || getTechnologyCost(techType, getTechLevel(techType) + 1).darkMatter > 0"
|
||||
class="flex items-center gap-1.5 sm:gap-2"
|
||||
>
|
||||
<ResourceIcon :type="resourceType.key" size="sm" />
|
||||
<span class="text-xs">{{ t(`resources.${resourceType.key}`) }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="
|
||||
getResourceCostColor(planet.resources.deuterium, getTechnologyCost(techType, getTechLevel(techType) + 1).deuterium)
|
||||
getResourceCostColor(
|
||||
planet.resources[resourceType.key],
|
||||
getTechnologyCost(techType, getTechLevel(techType) + 1)[resourceType.key]
|
||||
)
|
||||
"
|
||||
>
|
||||
{{ formatNumber(getTechnologyCost(techType, getTechLevel(techType) + 1).deuterium) }}
|
||||
{{ formatNumber(getTechnologyCost(techType, getTechLevel(techType) + 1)[resourceType.key]) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -71,7 +61,19 @@
|
||||
</div>
|
||||
|
||||
<!-- 提示对话框 -->
|
||||
<AlertDialog ref="alertDialog" />
|
||||
<AlertDialog :open="alertDialogOpen" @update:open="alertDialogOpen = $event">
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{{ alertDialogTitle }}</AlertDialogTitle>
|
||||
<AlertDialogDescription class="whitespace-pre-line">
|
||||
{{ alertDialogMessage }}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogAction>{{ t('common.confirm') }}</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -87,7 +89,15 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import ResourceIcon from '@/components/ResourceIcon.vue'
|
||||
import AlertDialog from '@/components/AlertDialog.vue'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import UnlockRequirement from '@/components/UnlockRequirement.vue'
|
||||
import CardUnlockOverlay from '@/components/CardUnlockOverlay.vue'
|
||||
import { formatNumber, getResourceCostColor } from '@/utils/format'
|
||||
@@ -101,7 +111,19 @@
|
||||
const { TECHNOLOGIES, BUILDINGS } = useGameConfig()
|
||||
const planet = computed(() => gameStore.currentPlanet)
|
||||
const player = computed(() => gameStore.player)
|
||||
const alertDialog = ref<InstanceType<typeof AlertDialog> | null>(null)
|
||||
|
||||
// AlertDialog 状态
|
||||
const alertDialogOpen = ref(false)
|
||||
const alertDialogTitle = ref('')
|
||||
const alertDialogMessage = ref('')
|
||||
|
||||
// 资源类型配置(用于成本显示)
|
||||
const costResourceTypes = [
|
||||
{ key: 'metal' as const },
|
||||
{ key: 'crystal' as const },
|
||||
{ key: 'deuterium' as const },
|
||||
{ key: 'darkMatter' as const }
|
||||
]
|
||||
|
||||
const researchTechnology = (techType: TechnologyType): boolean => {
|
||||
if (!gameStore.currentPlanet) return false
|
||||
@@ -117,7 +139,8 @@
|
||||
gameStore.currentPlanet,
|
||||
techType,
|
||||
currentLevel,
|
||||
gameStore.player.officers
|
||||
gameStore.player.officers,
|
||||
gameStore.player.technologies
|
||||
)
|
||||
gameStore.player.researchQueue.push(queueItem)
|
||||
return true
|
||||
@@ -149,7 +172,11 @@
|
||||
return t('researchView.maxLevelReached') // "等级已满"
|
||||
}
|
||||
|
||||
if (player.value.researchQueue.length > 0) return t('researchView.research')
|
||||
// 检查研究队列是否已满
|
||||
const maxQueue = publicLogic.getMaxResearchQueue(gameStore.player.technologies)
|
||||
if (player.value.researchQueue.length >= maxQueue) {
|
||||
return t('researchView.research')
|
||||
}
|
||||
|
||||
// 检查前置条件
|
||||
if (!checkUpgradeRequirements(techType)) {
|
||||
@@ -196,19 +223,17 @@
|
||||
const handleResearch = (techType: TechnologyType) => {
|
||||
// 检查前置条件
|
||||
if (!checkUpgradeRequirements(techType)) {
|
||||
alertDialog.value?.show({
|
||||
title: t('common.requirementsNotMet'),
|
||||
message: getRequirementsList(techType)
|
||||
})
|
||||
alertDialogTitle.value = t('common.requirementsNotMet')
|
||||
alertDialogMessage.value = getRequirementsList(techType)
|
||||
alertDialogOpen.value = true
|
||||
return
|
||||
}
|
||||
|
||||
const success = researchTechnology(techType)
|
||||
if (!success) {
|
||||
alertDialog.value?.show({
|
||||
title: t('researchView.researchFailed'),
|
||||
message: t('researchView.researchFailedMessage')
|
||||
})
|
||||
alertDialogTitle.value = t('researchView.researchFailed')
|
||||
alertDialogMessage.value = t('researchView.researchFailedMessage')
|
||||
alertDialogOpen.value = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,7 +254,11 @@
|
||||
return false
|
||||
}
|
||||
|
||||
if (player.value.researchQueue.length > 0) return false
|
||||
// 检查研究队列是否已满
|
||||
const maxQueue = publicLogic.getMaxResearchQueue(gameStore.player.technologies)
|
||||
if (player.value.researchQueue.length >= maxQueue) {
|
||||
return false
|
||||
}
|
||||
|
||||
const cost = getTechnologyCost(techType, currentLevel + 1)
|
||||
|
||||
@@ -237,7 +266,8 @@
|
||||
publicLogic.checkRequirements(planet.value, gameStore.player.technologies, config.requirements) &&
|
||||
planet.value.resources.metal >= cost.metal &&
|
||||
planet.value.resources.crystal >= cost.crystal &&
|
||||
planet.value.resources.deuterium >= cost.deuterium
|
||||
planet.value.resources.deuterium >= cost.deuterium &&
|
||||
planet.value.resources.darkMatter >= cost.darkMatter
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -164,7 +164,7 @@
|
||||
let confirmCallback: (() => void) | null = null
|
||||
|
||||
const openGithub = () => {
|
||||
window.open(`https://github.com/${pkg.author}/${pkg.name}`, '_blank')
|
||||
window.open(`https://github.com/${pkg.author.name}/${pkg.name}`, '_blank')
|
||||
}
|
||||
|
||||
const openQQGroup = () => {
|
||||
@@ -180,6 +180,8 @@
|
||||
const gameData = localStorage.getItem(pkg.name)
|
||||
// 获取地图数据
|
||||
const universeData = localStorage.getItem(`${pkg.name}-universe`)
|
||||
// 获取npc数据
|
||||
const npcData = localStorage.getItem(`${pkg.name}-npcs`)
|
||||
|
||||
if (!gameData) {
|
||||
toast.error(t('settings.exportFailed'))
|
||||
@@ -189,6 +191,7 @@
|
||||
// 合并数据
|
||||
const exportData = {
|
||||
game: gameData,
|
||||
npcs: npcData,
|
||||
universe: universeData || null
|
||||
}
|
||||
|
||||
@@ -247,6 +250,10 @@
|
||||
localStorage.setItem(`${pkg.name}-universe`, importData.universe)
|
||||
}
|
||||
|
||||
if (importData.npcs) {
|
||||
localStorage.setItem(`${pkg.name}-npcs`, importData.npcs)
|
||||
}
|
||||
|
||||
toast.success(t('settings.importSuccess'))
|
||||
// 延迟刷新页面以让toast显示
|
||||
setTimeout(() => window.location.reload(), 1000)
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
class="h-full transition-all duration-300"
|
||||
:class="fleetStorageUsage > maxFleetStorage ? 'bg-destructive' : 'bg-primary'"
|
||||
:style="{ width: `${Math.min((fleetStorageUsage / maxFleetStorage) * 100, 100)}%` }"
|
||||
></div>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -31,9 +31,9 @@
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3 sm:gap-4">
|
||||
<Card v-for="shipType in Object.values(ShipType)" :key="shipType" class="relative">
|
||||
<CardUnlockOverlay :requirements="SHIPS[shipType].requirements" />
|
||||
<CardHeader>
|
||||
<CardHeader class="pb-3">
|
||||
<CardTitle
|
||||
class="text-base sm:text-lg cursor-pointer hover:text-primary transition-colors"
|
||||
class="text-sm sm:text-base lg:text-lg cursor-pointer hover:text-primary transition-colors underline decoration-dotted underline-offset-4 mb-2"
|
||||
@click="detailDialog.openShip(shipType)"
|
||||
>
|
||||
{{ SHIPS[shipType].name }}
|
||||
@@ -64,34 +64,19 @@
|
||||
<div class="text-xs sm:text-sm space-y-1.5 sm:space-y-2">
|
||||
<p class="text-muted-foreground mb-1 sm:mb-2">{{ t('shipyardView.unitCost') }}:</p>
|
||||
<div class="space-y-1 sm:space-y-1.5">
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="metal" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.metal') }}:</span>
|
||||
<div
|
||||
v-for="resourceType in costResourceTypes"
|
||||
:key="resourceType.key"
|
||||
v-show="resourceType.key !== 'darkMatter' || SHIPS[shipType].cost.darkMatter > 0"
|
||||
class="flex items-center gap-1.5 sm:gap-2"
|
||||
>
|
||||
<ResourceIcon :type="resourceType.key" size="sm" />
|
||||
<span class="text-xs">{{ t(`resources.${resourceType.key}`) }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="getResourceCostColor(planet.resources.metal, SHIPS[shipType].cost.metal)"
|
||||
:class="getResourceCostColor(planet.resources[resourceType.key], SHIPS[shipType].cost[resourceType.key])"
|
||||
>
|
||||
{{ formatNumber(SHIPS[shipType].cost.metal) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="crystal" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.crystal') }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="getResourceCostColor(planet.resources.crystal, SHIPS[shipType].cost.crystal)"
|
||||
>
|
||||
{{ formatNumber(SHIPS[shipType].cost.crystal) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="deuterium" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.deuterium') }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="getResourceCostColor(planet.resources.deuterium, SHIPS[shipType].cost.deuterium)"
|
||||
>
|
||||
{{ formatNumber(SHIPS[shipType].cost.deuterium) }}
|
||||
{{ formatNumber(SHIPS[shipType].cost[resourceType.key]) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -112,34 +97,19 @@
|
||||
<div v-if="quantities[shipType] > 0" class="text-xs sm:text-sm space-y-1.5 sm:space-y-2 p-2.5 sm:p-3 bg-muted rounded-lg">
|
||||
<p class="font-medium text-muted-foreground">{{ t('shipyardView.totalCost') }}:</p>
|
||||
<div class="space-y-1 sm:space-y-1.5">
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="metal" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.metal') }}:</span>
|
||||
<div
|
||||
v-for="resourceType in costResourceTypes"
|
||||
:key="resourceType.key"
|
||||
v-show="resourceType.key !== 'darkMatter' || getTotalCost(shipType).darkMatter > 0"
|
||||
class="flex items-center gap-1.5 sm:gap-2"
|
||||
>
|
||||
<ResourceIcon :type="resourceType.key" size="sm" />
|
||||
<span class="text-xs">{{ t(`resources.${resourceType.key}`) }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="getResourceCostColor(planet.resources.metal, getTotalCost(shipType).metal)"
|
||||
:class="getResourceCostColor(planet.resources[resourceType.key], getTotalCost(shipType)[resourceType.key])"
|
||||
>
|
||||
{{ formatNumber(getTotalCost(shipType).metal) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="crystal" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.crystal') }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="getResourceCostColor(planet.resources.crystal, getTotalCost(shipType).crystal)"
|
||||
>
|
||||
{{ formatNumber(getTotalCost(shipType).crystal) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="deuterium" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.deuterium') }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="getResourceCostColor(planet.resources.deuterium, getTotalCost(shipType).deuterium)"
|
||||
>
|
||||
{{ formatNumber(getTotalCost(shipType).deuterium) }}
|
||||
{{ formatNumber(getTotalCost(shipType)[resourceType.key]) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -152,7 +122,19 @@
|
||||
</div>
|
||||
|
||||
<!-- 提示对话框 -->
|
||||
<AlertDialog ref="alertDialog" />
|
||||
<AlertDialog :open="alertDialogOpen" @update:open="alertDialogOpen = $event">
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{{ alertDialogTitle }}</AlertDialogTitle>
|
||||
<AlertDialogDescription class="whitespace-pre-line">
|
||||
{{ alertDialogMessage }}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogAction>{{ t('common.confirm') }}</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -168,7 +150,15 @@
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import ResourceIcon from '@/components/ResourceIcon.vue'
|
||||
import AlertDialog from '@/components/AlertDialog.vue'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import UnlockRequirement from '@/components/UnlockRequirement.vue'
|
||||
import CardUnlockOverlay from '@/components/CardUnlockOverlay.vue'
|
||||
import { formatNumber, getResourceCostColor } from '@/utils/format'
|
||||
@@ -181,7 +171,19 @@
|
||||
const { t } = useI18n()
|
||||
const { SHIPS } = useGameConfig()
|
||||
const planet = computed(() => gameStore.currentPlanet)
|
||||
const alertDialog = ref<InstanceType<typeof AlertDialog> | null>(null)
|
||||
|
||||
// AlertDialog 状态
|
||||
const alertDialogOpen = ref(false)
|
||||
const alertDialogTitle = ref('')
|
||||
const alertDialogMessage = ref('')
|
||||
|
||||
// 资源类型配置(用于成本显示)
|
||||
const costResourceTypes = [
|
||||
{ key: 'metal' as const },
|
||||
{ key: 'crystal' as const },
|
||||
{ key: 'deuterium' as const },
|
||||
{ key: 'darkMatter' as const }
|
||||
]
|
||||
|
||||
// 舰队仓储使用量
|
||||
const fleetStorageUsage = computed(() => {
|
||||
@@ -201,11 +203,15 @@
|
||||
[ShipType.HeavyFighter]: 0,
|
||||
[ShipType.Cruiser]: 0,
|
||||
[ShipType.Battleship]: 0,
|
||||
[ShipType.Battlecruiser]: 0,
|
||||
[ShipType.Bomber]: 0,
|
||||
[ShipType.Destroyer]: 0,
|
||||
[ShipType.SmallCargo]: 0,
|
||||
[ShipType.LargeCargo]: 0,
|
||||
[ShipType.ColonyShip]: 0,
|
||||
[ShipType.Recycler]: 0,
|
||||
[ShipType.EspionageProbe]: 0,
|
||||
[ShipType.SolarSatellite]: 0,
|
||||
[ShipType.DarkMatterHarvester]: 0,
|
||||
[ShipType.Deathstar]: 0
|
||||
})
|
||||
@@ -223,19 +229,17 @@
|
||||
const handleBuild = (shipType: ShipType) => {
|
||||
const quantity = quantities.value[shipType]
|
||||
if (quantity <= 0) {
|
||||
alertDialog.value?.show({
|
||||
title: t('shipyardView.inputError'),
|
||||
message: t('shipyardView.inputErrorMessage')
|
||||
})
|
||||
alertDialogTitle.value = t('shipyardView.inputError')
|
||||
alertDialogMessage.value = t('shipyardView.inputErrorMessage')
|
||||
alertDialogOpen.value = true
|
||||
return
|
||||
}
|
||||
|
||||
const success = buildShip(shipType, quantity)
|
||||
if (!success) {
|
||||
alertDialog.value?.show({
|
||||
title: t('shipyardView.buildFailed'),
|
||||
message: t('shipyardView.buildFailedMessage')
|
||||
})
|
||||
alertDialogTitle.value = t('shipyardView.buildFailed')
|
||||
alertDialogMessage.value = t('shipyardView.buildFailedMessage')
|
||||
alertDialogOpen.value = true
|
||||
} else {
|
||||
quantities.value[shipType] = 0
|
||||
}
|
||||
@@ -252,14 +256,16 @@
|
||||
const totalCost = {
|
||||
metal: config.cost.metal * quantity,
|
||||
crystal: config.cost.crystal * quantity,
|
||||
deuterium: config.cost.deuterium * quantity
|
||||
deuterium: config.cost.deuterium * quantity,
|
||||
darkMatter: config.cost.darkMatter * quantity
|
||||
}
|
||||
|
||||
return (
|
||||
publicLogic.checkRequirements(planet.value, gameStore.player.technologies, config.requirements) &&
|
||||
planet.value.resources.metal >= totalCost.metal &&
|
||||
planet.value.resources.crystal >= totalCost.crystal &&
|
||||
planet.value.resources.deuterium >= totalCost.deuterium
|
||||
planet.value.resources.deuterium >= totalCost.deuterium &&
|
||||
planet.value.resources.darkMatter >= totalCost.darkMatter
|
||||
)
|
||||
}
|
||||
|
||||
@@ -270,7 +276,8 @@
|
||||
return {
|
||||
metal: config.cost.metal * quantity,
|
||||
crystal: config.cost.crystal * quantity,
|
||||
deuterium: config.cost.deuterium * quantity
|
||||
deuterium: config.cost.deuterium * quantity,
|
||||
darkMatter: config.cost.darkMatter * quantity
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user