6 Commits

Author SHA1 Message Date
谦君
cfcde0b024 feat: 新增队列与外交通知组件及新手引导
引入队列通知(QueueNotifications)和外交通知(DiplomaticNotifications)组件,优化主界面队列与外交报告展示,支持一键查看与跳转。重构App.vue,移除原有队列展示,改为弹出式通知,支持功能解锁提示与新手引导(TutorialOverlay)。完善NPC外交事件处理,导弹攻击等行为影响好感度并生成报告。优化部分UI细节与多语言文本,提升交互体验。
2025-12-17 21:06:34 +08:00
谦君
053bd24855 fix(package): 解决buildDate字段冲突
合并package.json中的buildDate字段,移除合并冲突标记,保持字段一致性。
2025-12-15 22:36:33 +08:00
谦君
7d1f36046d Merge pull request #9 from coolxitech/main
chore(workflow): 更新 GitHub Pages 工作流名称
2025-12-15 22:35:11 +08:00
谦君
22ae07de90 Merge branch 'main' into main 2025-12-15 22:35:02 +08:00
谦君
a76909a2c7 Merge pull request #8 from setube/revert-7-main
Revert "chore(github-pages): 更新GitHub Pages构建工作流"
2025-12-15 22:31:18 +08:00
coolxitech
8144f305e2 chore(workflow): 更新 GitHub Pages 工作流名称
- 将工作流名称从 "构建Github Pages" 更改为 "构建 Github Pages"
- 保持其他配置不变
2025-12-15 22:31:03 +08:00
39 changed files with 3606 additions and 425 deletions

View File

@@ -1,4 +1,4 @@
name: Deploy Vue Project
name: 构建 Github Pages
on:
push:

View File

@@ -8,12 +8,8 @@
"email": "1962257451@qq.com"
},
"private": true,
"version": "1.2.5",
<<<<<<< Updated upstream
"buildDate": "2025/12/15 21:21:23",
=======
"buildDate": "2025/12/15 21:59:38",
>>>>>>> Stashed changes
"version": "1.3.0",
"buildDate": "2025/12/17 21:05:49",
"main": "dist-electron/main.js",
"type": "module",
"scripts": {

View File

@@ -1,5 +1,5 @@
<template>
<SidebarProvider :open="sidebarOpen" @update:open="sidebarOpen = $event">
<SidebarProvider :open="sidebarOpen" @update:open="handleSidebarOpenChange">
<Sidebar collapsible="icon">
<!-- Logo -->
<SidebarHeader class="border-b">
@@ -17,6 +17,7 @@
<Popover>
<PopoverTrigger as-child>
<Button
data-tutorial="planet-selector"
variant="outline"
class="w-full justify-between h-auto px-3 py-2.5 border-2 hover:bg-accent hover:border-primary transition-colors"
>
@@ -94,11 +95,15 @@
</SidebarGroup>
<!-- 导航菜单 -->
<SidebarGroup>
<SidebarGroup data-tutorial="navigation">
<SidebarMenu>
<SidebarMenuItem v-for="item in navItems" :key="item.path">
<SidebarMenuButton as-child :is-active="$route.path === item.path" :tooltip="item.name.value">
<RouterLink :to="item.path">
<SidebarMenuButton
:data-nav-path="item.path"
:is-active="$route.path === item.path"
:tooltip="item.name.value"
@click="handleNavClick(item.path, $event)"
>
<component :is="item.icon" />
<span>{{ item.name.value }}</span>
<!-- 未读消息数量 -->
@@ -109,13 +114,16 @@
{{ unreadMessagesCount }}
</SidebarMenuBadge>
<!-- 正在执行的舰队任务数量 -->
<SidebarMenuBadge
v-if="item.path === '/fleet' && activeFleetMissionsCount > 0"
class="bg-primary text-primary-foreground"
>
<SidebarMenuBadge v-if="item.path === '/fleet' && activeFleetMissionsCount > 0" class="bg-primary text-primary-foreground">
{{ activeFleetMissionsCount }}
</SidebarMenuBadge>
</RouterLink>
<!-- 未读外交报告数量 -->
<SidebarMenuBadge
v-if="item.path === '/diplomacy' && unreadDiplomaticReportsCount > 0"
class="bg-destructive text-destructive-foreground"
>
{{ unreadDiplomaticReportsCount }}
</SidebarMenuBadge>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
@@ -189,11 +197,14 @@
<div class="grid items-center gap-3 sm:gap-6" style="grid-template-columns: auto 1fr auto">
<!-- 左侧汉堡菜单移动端/ 占位PC端 -->
<div>
<SidebarTrigger class="lg:hidden" />
<SidebarTrigger class="lg:hidden" data-tutorial="mobile-menu" />
</div>
<!-- 资源显示 - PC端居中移动端可折叠 -->
<div :class="['flex items-center gap-3 sm:gap-6 justify-center', resourceBarExpanded ? 'hidden' : 'overflow-x-auto']">
<div
class="resource-bar flex items-center gap-3 sm:gap-6 justify-center"
:class="resourceBarExpanded ? 'hidden' : 'overflow-x-auto'"
>
<div v-for="resourceType in resourceTypes" :key="resourceType.key" class="flex items-center gap-1.5 sm:gap-2 flex-shrink-0">
<ResourceIcon :type="resourceType.key" size="md" />
<div class="min-w-0">
@@ -233,15 +244,11 @@
<ChevronUp v-else class="h-4 w-4" />
</Button>
<!-- 建造队列状态 -->
<div v-if="planet.buildQueue.length > 0" class="flex items-center gap-1.5 text-xs sm:text-sm">
<div class="h-2 w-2 rounded-full bg-green-500 animate-pulse" />
<span class="text-muted-foreground hidden sm:inline">{{ t('queue.building') }}</span>
</div>
<div v-if="gameStore.player.researchQueue.length > 0" class="flex items-center gap-1.5 text-xs sm:text-sm">
<div class="h-2 w-2 rounded-full bg-blue-500 animate-pulse" />
<span class="text-muted-foreground hidden sm:inline">{{ t('queue.researching') }}</span>
</div>
<!-- 外交通知 -->
<DiplomaticNotifications />
<!-- 队列通知 -->
<QueueNotifications />
</div>
</div>
</div>
@@ -310,70 +317,6 @@
@mark-as-read="removeIncomingFleetAlert"
/>
<!-- 建造队列 -->
<div
v-if="planet && (planet.buildQueue.length > 0 || gameStore.player.researchQueue.length > 0)"
class="bg-card border-b px-4 sm:px-6 py-4.5"
>
<div class="space-y-3">
<!-- 建造队列 -->
<div v-for="item in planet.buildQueue" :key="item.id" class="space-y-1.5">
<div class="flex items-center justify-between text-xs sm:text-sm gap-2">
<div class="flex items-center gap-1.5 sm:gap-2 min-w-0 flex-1">
<div class="h-2 w-2 rounded-full bg-green-500 animate-pulse flex-shrink-0" />
<span class="font-medium truncate">{{ getItemName(item) }}</span>
<span class="text-muted-foreground hidden sm:inline flex-shrink-0 text-[10px] sm:text-xs">
<template v-if="item.type === 'ship' || item.type === 'defense'">
{{ t('queue.quantity') }} {{ item.quantity }}
</template>
<template v-else> {{ t('queue.level') }} {{ item.targetLevel }}</template>
</span>
</div>
<div class="flex items-center gap-2 sm:gap-3 flex-shrink-0">
<span class="text-muted-foreground text-[10px] sm:text-xs whitespace-nowrap">
{{ formatTime(getRemainingTime(item)) }}
</span>
<Button
@click="handleCancelBuild(item.id)"
variant="ghost"
size="sm"
class="h-5 sm:h-6 px-1.5 sm:px-2 text-[10px] sm:text-xs"
>
{{ t('queue.cancel') }}
</Button>
</div>
</div>
<Progress :model-value="getQueueProgress(item)" class="h-1.5" />
</div>
<!-- 研究队列 -->
<div v-for="item in gameStore.player.researchQueue" :key="item.id" class="space-y-1.5">
<div class="flex items-center justify-between text-xs sm:text-sm gap-2">
<div class="flex items-center gap-1.5 sm:gap-2 min-w-0 flex-1">
<div class="h-2 w-2 rounded-full bg-blue-500 animate-pulse flex-shrink-0" />
<span class="font-medium truncate">{{ getItemName(item) }}</span>
<span class="text-muted-foreground hidden sm:inline flex-shrink-0 text-[10px] sm:text-xs">
{{ t('queue.level') }} {{ item.targetLevel }}
</span>
</div>
<div class="flex items-center gap-2 sm:gap-3 flex-shrink-0">
<span class="text-muted-foreground text-[10px] sm:text-xs whitespace-nowrap">
{{ formatTime(getRemainingTime(item)) }}
</span>
<Button
@click="handleCancelResearch(item.id)"
variant="ghost"
size="sm"
class="h-5 sm:h-6 px-1.5 sm:px-2 text-[10px] sm:text-xs"
>
{{ t('queue.cancel') }}
</Button>
</div>
</div>
<Progress :model-value="getQueueProgress(item)" class="h-1.5" />
</div>
</div>
</div>
<!-- 内容区域 -->
<main class="flex-1 overflow-y-auto">
<div class="animate-fade-in">
@@ -405,6 +348,9 @@
<!-- 更新弹窗 -->
<UpdateDialog v-model:open="showUpdateDialog" :version-info="updateInfo" />
<!-- 新手引导 -->
<TutorialOverlay />
<!-- Toast 通知 -->
<Sonner position="top-center" />
</SidebarProvider>
@@ -412,18 +358,21 @@
<script setup lang="ts">
import { onMounted, onUnmounted, computed, ref, watch } from 'vue'
import { RouterView, RouterLink } from 'vue-router'
import { RouterView, useRouter } from 'vue-router'
import { useGameStore } from '@/stores/gameStore'
import { useUniverseStore } from '@/stores/universeStore'
import { useNPCStore } from '@/stores/npcStore'
import { useTheme } from '@/composables/useTheme'
import { useI18n } from '@/composables/useI18n'
import { useGameConfig } from '@/composables/useGameConfig'
import { useTutorial } from '@/composables/useTutorial'
import { localeNames, detectBrowserLocale, type Locale } from '@/locales'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Progress } from '@/components/ui/progress'
import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover'
import IncomingFleetAlerts from '@/components/IncomingFleetAlerts.vue'
import DiplomaticNotifications from '@/components/DiplomaticNotifications.vue'
import QueueNotifications from '@/components/QueueNotifications.vue'
import {
Sidebar,
SidebarContent,
@@ -451,11 +400,13 @@
} from '@/components/ui/alert-dialog'
import DetailDialog from '@/components/DetailDialog.vue'
import UpdateDialog from '@/components/UpdateDialog.vue'
import TutorialOverlay from '@/components/TutorialOverlay.vue'
import Sonner from '@/components/ui/sonner/Sonner.vue'
import { MissionType } from '@/types/game'
import type { BuildQueueItem, FleetMission, NPC, IncomingFleetAlert, MissileAttack } from '@/types/game'
import { MissionType, BuildingType, DiplomaticEventType } from '@/types/game'
import type { FleetMission, NPC, IncomingFleetAlert, MissileAttack } from '@/types/game'
import { DIPLOMATIC_CONFIG } from '@/config/gameConfig'
import type { VersionInfo } from '@/utils/versionCheck'
import { formatNumber, formatTime, getResourceColor } from '@/utils/format'
import { formatNumber, getResourceColor } from '@/utils/format'
import {
Moon,
Sun,
@@ -497,11 +448,14 @@
// 执行数据迁移(在 store 初始化之前)
migrateGameData()
const router = useRouter()
const gameStore = useGameStore()
const universeStore = useUniverseStore()
const npcStore = useNPCStore()
const { isDark } = useTheme()
const { t } = useI18n()
const { BUILDINGS } = useGameConfig()
const { startTutorial, tutorialState, currentStep } = useTutorial()
// ConfirmDialog 状态
const confirmDialogOpen = ref(false)
@@ -536,10 +490,10 @@
if (!shouldInit) {
const now = Date.now()
// 计算离线收益(直接同步计算)
// 计算离线收益(直接同步计算,应用游戏速度
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, now)
gameStore.player.planets.forEach(planet => {
resourceLogic.updatePlanetResources(planet, now, bonuses)
resourceLogic.updatePlanetResources(planet, now, bonuses, gameStore.gameSpeed)
})
// 只在没有NPC星球时才生成首次加载已有玩家数据时
@@ -568,13 +522,13 @@
}
const updateGame = async () => {
if (gameStore.isPaused) return
const now = Date.now()
gameStore.gameTime = now
if (gameStore.isPaused) return
// 检查军官过期
gameLogic.checkOfficersExpiration(gameStore.player.officers, now)
// 处理游戏更新(建造队列、研究队列等)
const result = gameLogic.processGameUpdate(gameStore.player, now)
const result = gameLogic.processGameUpdate(gameStore.player, now, gameStore.gameSpeed)
gameStore.player.researchQueue = result.updatedResearchQueue
// 处理舰队任务
gameStore.player.fleetMissions.forEach(mission => {
@@ -613,6 +567,27 @@
// NPC行为系统更新侦查和攻击决策
updateNPCBehavior(1)
// 检查并处理被消灭的NPC所有星球都被摧毁的NPC
const eliminatedNpcIds = diplomaticLogic.checkAndHandleEliminatedNPCs(npcStore.npcs, gameStore.player, gameStore.locale)
if (eliminatedNpcIds.length > 0) {
// 从universeStore中移除被消灭NPC的星球数据
eliminatedNpcIds.forEach(npcId => {
const npc = npcStore.npcs.find(n => n.id === npcId)
if (npc && npc.planets) {
// 遍历NPC的所有星球从universeStore中删除
npc.planets.forEach(planet => {
const planetKey = gameLogic.generatePositionKey(planet.position.galaxy, planet.position.system, planet.position.position)
if (universeStore.planets[planetKey]) {
delete universeStore.planets[planetKey]
}
})
}
})
// 从NPC列表中移除被消灭的NPC
npcStore.npcs = npcStore.npcs.filter(npc => !eliminatedNpcIds.includes(npc.id))
}
}
const processMissionArrival = async (mission: FleetMission) => {
@@ -812,6 +787,15 @@
if (destroyResult && destroyResult.success && destroyResult.planetId) {
// 星球被摧毁
// 处理外交关系如果目标是NPC星球
if (targetPlanet && targetPlanet.ownerId) {
const planetOwner = npcStore.npcs.find(npc => npc.id === targetPlanet.ownerId)
if (planetOwner) {
diplomaticLogic.handlePlanetDestructionReputation(gameStore.player, targetPlanet, planetOwner, npcStore.npcs, gameStore.locale)
}
}
// 从玩家星球列表中移除(如果是玩家的星球)
const planetIndex = gameStore.player.planets.findIndex(p => p.id === destroyResult.planetId)
if (planetIndex > -1) {
@@ -1010,6 +994,44 @@
// 应用损失到目标星球
missileLogic.applyMissileAttackResult(targetPlanet, impactResult.defenseLosses)
// 如果目标是NPC的星球扣除外交好感度
if (targetPlanet.ownerId && targetPlanet.ownerId !== gameStore.player.id) {
const targetNpc = npcStore.npcs.find(npc => npc.id === targetPlanet.ownerId)
if (targetNpc) {
// 导弹攻击扣除好感度
const { REPUTATION_CHANGES } = DIPLOMATIC_CONFIG
const reputationLoss = REPUTATION_CHANGES.ATTACK / 2 // 导弹攻击的好感度惩罚是普通攻击的一半
// 更新玩家对NPC的关系
if (!gameStore.player.diplomaticRelations) {
gameStore.player.diplomaticRelations = {}
}
const relation = diplomaticLogic.getOrCreateRelation(
gameStore.player.diplomaticRelations,
gameStore.player.id,
targetNpc.id
)
gameStore.player.diplomaticRelations[targetNpc.id] = diplomaticLogic.updateReputation(
relation,
reputationLoss,
DiplomaticEventType.Attack,
t('diplomacy.reports.missileAttackNpc', { npcName: targetNpc.name })
)
// 更新NPC对玩家的关系
if (!targetNpc.relations) {
targetNpc.relations = {}
}
const npcRelation = diplomaticLogic.getOrCreateRelation(targetNpc.relations, targetNpc.id, gameStore.player.id)
targetNpc.relations[gameStore.player.id] = diplomaticLogic.updateReputation(
npcRelation,
reputationLoss,
DiplomaticEventType.Attack,
t('diplomacy.reports.wasAttackedByMissile')
)
}
}
// 标记导弹攻击为已到达
missileAttack.status = 'arrived'
@@ -1062,7 +1084,7 @@
// NPC成长系统更新函数
let npcUpdateCounter = 0 // 累计秒数
const NPC_UPDATE_INTERVAL = 10 // 每10秒更新一次NPC减少性能开销
const NPC_UPDATE_INTERVAL = 1 // 每1秒更新一次NPC确保发育速度与玩家相当
const updateNPCGrowth = (deltaSeconds: number) => {
// 累积时间
@@ -1216,6 +1238,14 @@
startGameLoop()
// 启动科乐美秘籍监听
konamiCleanup = setupKonamiCode()
// 启动新手引导(如果尚未完成)
startTutorial()
// 添加队列取消事件监听
window.addEventListener('cancel-build', handleCancelBuildEvent as EventListener)
window.addEventListener('cancel-research', handleCancelResearchEvent as EventListener)
// 首次检查版本(被动检测)
const versionInfo = await checkLatestVersion(gameStore.player.lastVersionCheckTime || 0, (time: number) => {
gameStore.player.lastVersionCheckTime = time
@@ -1259,8 +1289,21 @@
if (gameLoop) clearInterval(gameLoop)
if (konamiCleanup) konamiCleanup()
if (versionCheckInterval) clearInterval(versionCheckInterval)
// 移除队列取消事件监听
window.removeEventListener('cancel-build', handleCancelBuildEvent as EventListener)
window.removeEventListener('cancel-research', handleCancelResearchEvent as EventListener)
})
// 处理取消建造事件
const handleCancelBuildEvent = (event: CustomEvent) => {
handleCancelBuild(event.detail)
}
// 处理取消研究事件
const handleCancelResearchEvent = (event: CustomEvent) => {
handleCancelResearch(event.detail)
}
// 科乐美秘籍上上下下左左右右BA
const setupKonamiCode = () => {
const konamiCode = ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown', 'ArrowLeft', 'ArrowLeft', 'ArrowRight', 'ArrowRight', 'b', 'a']
@@ -1312,6 +1355,56 @@
...(gameStore.player.isGMEnabled ? [{ name: computed(() => t('nav.gm')), path: '/gm', icon: Wrench }] : [])
])
// 功能解锁要求配置
const featureRequirements: Record<string, { building: BuildingType; level: number }> = {
'/research': { building: BuildingType.ResearchLab, level: 1 },
'/shipyard': { building: BuildingType.Shipyard, level: 1 },
'/defense': { building: BuildingType.Shipyard, level: 1 },
'/fleet': { building: BuildingType.Shipyard, level: 1 }
}
// 检查功能是否解锁
const checkFeatureUnlocked = (path: string): { unlocked: boolean; requirement?: { building: BuildingType; level: number } } => {
const requirement = featureRequirements[path]
if (!requirement) {
return { unlocked: true }
}
const currentLevel = planet.value?.buildings[requirement.building] || 0
return {
unlocked: currentLevel >= requirement.level,
requirement
}
}
// 处理导航点击
const handleNavClick = (path: string, event: Event) => {
const { unlocked, requirement } = checkFeatureUnlocked(path)
if (!unlocked && requirement) {
event.preventDefault()
event.stopPropagation()
const buildingName = BUILDINGS.value[requirement.building]?.name || requirement.building
const currentLevel = planet.value?.buildings[requirement.building] || 0
toast.warning(t('common.featureLocked'), {
description: `${t('common.requiredBuilding')}: ${buildingName} Lv ${requirement.level} (${t(
'common.currentLevel'
)}: Lv ${currentLevel})`,
action: {
label: t('common.goToBuildings'),
onClick: () => router.push('/buildings')
},
duration: 3000
})
return
}
// 功能已解锁,正常导航
router.push(path)
}
// 使用直接计算,不再缓存
const production = computed(() => {
if (!planet.value) return null
@@ -1362,6 +1455,11 @@
return fleetMissions + flyingMissiles
})
// 未读外交报告数量
const unreadDiplomaticReportsCount = computed(() => {
return (gameStore.player.diplomaticReports || []).filter(r => !r.read).length
})
// 资源类型配置
const resourceTypes = [
{ key: 'metal' as const },
@@ -1402,33 +1500,22 @@
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 handleSidebarOpenChange = (open: boolean) => {
// 如果是移动端且在教程的菜单相关步骤,阻止关闭侧边栏
if (window.innerWidth < 768 && tutorialState.value.isActive && currentStep.value) {
// 只在第3步期间阻止关闭侧边栏让玩家必须手动打开
if (currentStep.value.id === 'menu_intro_mobile') {
// 只允许打开,不允许关闭
if (open) {
sidebarOpen.value = true
}
return item.itemType
// 如果试图关闭,忽略该操作,保持打开状态
return
}
// 获取剩余时间
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))
// 其他情况正常更新
sidebarOpen.value = open
}
// 取消建造

View File

@@ -0,0 +1,300 @@
<template>
<Popover v-model:open="isOpen">
<PopoverTrigger as-child>
<Button variant="outline" size="icon" class="relative">
<ScrollText class="h-4 w-4" />
<Badge
v-if="unreadCount > 0"
variant="destructive"
class="absolute -top-1 -right-1 h-5 w-5 p-0 flex items-center justify-center text-xs"
>
{{ unreadCount > 9 ? '9+' : unreadCount }}
</Badge>
</Button>
</PopoverTrigger>
<PopoverContent class="w-96 p-0" align="end">
<div class="flex items-center justify-between p-4 border-b">
<h3 class="font-semibold">{{ t('diplomacy.notifications') }}</h3>
<Button v-if="unreadCount > 0" variant="ghost" size="sm" @click="markAllAsRead">
{{ t('diplomacy.markAllRead') }}
</Button>
</div>
<ScrollArea class="h-96">
<div v-if="reports.length === 0" class="p-8 text-center text-muted-foreground">
{{ t('diplomacy.noReports') }}
</div>
<div v-else class="divide-y">
<div
v-for="report in reports"
:key="report.id"
class="p-4 hover:bg-muted/50 cursor-pointer transition-colors"
:class="{ 'bg-primary/5': !report.read }"
@click="handleReportClick(report)"
>
<div class="flex items-start gap-3">
<div class="flex-shrink-0 mt-1">
<component :is="getEventIcon(report.eventType)" class="h-4 w-4" :class="getEventIconColor(report.eventType)" />
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-1">
<span class="font-medium text-sm">{{ report.npcName }}</span>
<Badge :variant="getStatusBadgeVariant(report.newStatus)" class="text-xs">
{{ getStatusText(report.newStatus) }}
</Badge>
<span v-if="!report.read" class="ml-auto">
<Badge variant="destructive" class="h-2 w-2 p-0 rounded-full" />
</span>
</div>
<p class="text-sm text-muted-foreground line-clamp-2">
{{ report.messageKey && report.messageParams ? t(report.messageKey, report.messageParams) : report.message }}
</p>
<p class="text-xs text-muted-foreground mt-1">
{{ formatRelativeTime((Date.now() - report.timestamp) / 1000, t) }}{{ t('diplomacy.ago') }}
</p>
</div>
</div>
</div>
</div>
</ScrollArea>
<div v-if="reports.length > 0" class="p-2 border-t">
<Button variant="ghost" size="sm" class="w-full" @click="goToDiplomacy">
{{ t('diplomacy.viewAll') }}
</Button>
</div>
</PopoverContent>
</Popover>
<!-- 外交报告详情对话框 -->
<Dialog :open="detailDialogOpen" @update:open="detailDialogOpen = $event">
<DialogContent class="max-w-2xl">
<DialogHeader>
<DialogTitle class="flex items-center gap-2">
<component
v-if="selectedReport"
:is="getEventIcon(selectedReport.eventType)"
class="h-5 w-5"
:class="getEventIconColor(selectedReport.eventType)"
/>
{{ t('diplomacy.reportDetails') }}
</DialogTitle>
</DialogHeader>
<div v-if="selectedReport" class="space-y-4">
<!-- NPC信息 -->
<div class="flex items-center gap-3 p-4 bg-muted/50 rounded-lg">
<div class="flex-1">
<div class="flex items-center gap-2 mb-1">
<h3 class="font-semibold text-lg">{{ selectedReport.npcName }}</h3>
<Badge :variant="getStatusBadgeVariant(selectedReport.newStatus)">
{{ getStatusText(selectedReport.newStatus) }}
</Badge>
</div>
<p class="text-sm text-muted-foreground">
{{ formatRelativeTime((Date.now() - selectedReport.timestamp) / 1000, t) }}{{ t('diplomacy.ago') }}
</p>
</div>
</div>
<!-- 事件描述 -->
<div class="space-y-2">
<h4 class="font-semibold text-sm">{{ t('diplomacy.eventDescription') }}</h4>
<p class="text-sm p-3 bg-muted/30 rounded-md">
{{
selectedReport.messageKey && selectedReport.messageParams
? t(selectedReport.messageKey, selectedReport.messageParams)
: selectedReport.message
}}
</p>
</div>
<!-- 关系变化 -->
<div class="grid grid-cols-2 gap-4">
<!-- 好感度变化 -->
<div class="space-y-2">
<h4 class="font-semibold text-sm">{{ t('diplomacy.reputationChange') }}</h4>
<div class="p-3 bg-muted/30 rounded-md">
<div class="flex items-center justify-between text-sm mb-2">
<span class="text-muted-foreground">{{ t('diplomacy.before') }}</span>
<span class="font-semibold" :class="getReputationColor(selectedReport.newReputation - selectedReport.reputationChange)">
{{ selectedReport.newReputation - selectedReport.reputationChange > 0 ? '+' : ''
}}{{ selectedReport.newReputation - selectedReport.reputationChange }}
</span>
</div>
<div
class="flex items-center justify-center text-lg font-bold my-1"
:class="selectedReport.reputationChange >= 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'"
>
{{ selectedReport.reputationChange >= 0 ? '+' : '' }}{{ selectedReport.reputationChange }}
</div>
<div class="flex items-center justify-between text-sm mt-2">
<span class="text-muted-foreground">{{ t('diplomacy.after') }}</span>
<span class="font-semibold" :class="getReputationColor(selectedReport.newReputation)">
{{ selectedReport.newReputation > 0 ? '+' : '' }}{{ selectedReport.newReputation }}
</span>
</div>
</div>
</div>
<!-- 关系状态变化 -->
<div class="space-y-2">
<h4 class="font-semibold text-sm">{{ t('diplomacy.statusChange') }}</h4>
<div class="p-3 bg-muted/30 rounded-md">
<div class="flex items-center justify-between text-sm mb-2">
<span class="text-muted-foreground">{{ t('diplomacy.before') }}</span>
<Badge :variant="getStatusBadgeVariant(selectedReport.oldStatus)" class="text-xs">
{{ getStatusText(selectedReport.oldStatus) }}
</Badge>
</div>
<div class="flex items-center justify-center my-3">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 text-muted-foreground"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3" />
</svg>
</div>
<div class="flex items-center justify-between text-sm mt-2">
<span class="text-muted-foreground">{{ t('diplomacy.after') }}</span>
<Badge :variant="getStatusBadgeVariant(selectedReport.newStatus)" class="text-xs">
{{ getStatusText(selectedReport.newStatus) }}
</Badge>
</div>
</div>
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" @click="detailDialogOpen = false">{{ t('common.close') }}</Button>
<Button @click="goToDiplomacyFromDialog">{{ t('diplomacy.viewDiplomacy') }}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import { useGameStore } from '@/stores/gameStore'
import { useI18n } from '@/composables/useI18n'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
import { ScrollArea } from '@/components/ui/scroll-area'
import { ScrollText, Gift, Sword, Eye, Trash2, Skull } from 'lucide-vue-next'
import { RelationStatus, DiplomaticEventType } from '@/types/game'
import type { DiplomaticReport } from '@/types/game'
import { formatRelativeTime } from '@/utils/format'
const router = useRouter()
const gameStore = useGameStore()
const { t } = useI18n()
const isOpen = ref(false)
const detailDialogOpen = ref(false)
const selectedReport = ref<DiplomaticReport | null>(null)
const reports = computed(() => {
return (gameStore.player.diplomaticReports || []).slice().reverse().slice(0, 20) // 最近20条
})
const unreadCount = computed(() => {
return (gameStore.player.diplomaticReports || []).filter(r => !r.read).length
})
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
case DiplomaticEventType.DestroyPlanet:
return Skull
default:
return ScrollText
}
}
const getEventIconColor = (eventType: DiplomaticReport['eventType']) => {
switch (eventType) {
case DiplomaticEventType.GiftResources:
return 'text-green-500'
case DiplomaticEventType.Attack:
case DiplomaticEventType.DestroyPlanet:
return 'text-red-500'
case DiplomaticEventType.AllyAttacked:
return 'text-orange-500'
case DiplomaticEventType.Spy:
return 'text-purple-500'
case DiplomaticEventType.StealDebris:
return 'text-yellow-500'
default:
return 'text-muted-foreground'
}
}
const getStatusBadgeVariant = (status: RelationStatus) => {
switch (status) {
case RelationStatus.Hostile:
return 'destructive'
case RelationStatus.Friendly:
return 'default'
case RelationStatus.Neutral:
default:
return 'secondary'
}
}
const getStatusText = (status: RelationStatus) => {
switch (status) {
case RelationStatus.Hostile:
return t('diplomacy.status.hostile')
case RelationStatus.Friendly:
return t('diplomacy.status.friendly')
case RelationStatus.Neutral:
default:
return t('diplomacy.status.neutral')
}
}
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 handleReportClick = (report: DiplomaticReport) => {
// 标记为已读
report.read = true
// 设置选中的报告并打开详情对话框
selectedReport.value = report
detailDialogOpen.value = true
// 关闭通知面板
isOpen.value = false
}
const markAllAsRead = () => {
gameStore.player.diplomaticReports?.forEach(report => {
report.read = true
})
}
const goToDiplomacy = () => {
isOpen.value = false
router.push('/diplomacy')
}
const goToDiplomacyFromDialog = () => {
detailDialogOpen.value = false
router.push('/diplomacy')
}
</script>

View File

@@ -175,15 +175,21 @@
<CardContent class="space-y-2">
<div class="flex items-center justify-between text-sm">
<span class="text-muted-foreground">{{ t('resources.metal') }}:</span>
<span class="font-medium"><NumberWithTooltip :value="totalStats.metal" /></span>
<span class="font-medium">
<NumberWithTooltip :value="totalStats.metal" />
</span>
</div>
<div class="flex items-center justify-between text-sm">
<span class="text-muted-foreground">{{ t('resources.crystal') }}:</span>
<span class="font-medium"><NumberWithTooltip :value="totalStats.crystal" /></span>
<span class="font-medium">
<NumberWithTooltip :value="totalStats.crystal" />
</span>
</div>
<div class="flex items-center justify-between text-sm">
<span class="text-muted-foreground">{{ t('resources.deuterium') }}:</span>
<span class="font-medium"><NumberWithTooltip :value="totalStats.deuterium" /></span>
<span class="font-medium">
<NumberWithTooltip :value="totalStats.deuterium" />
</span>
</div>
</CardContent>
</Card>
@@ -305,11 +311,15 @@
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>
<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>
<span class="font-bold text-primary">
<NumberWithTooltip :value="pointsPerUnit" />
</span>
</div>
</CardContent>
</Card>
@@ -341,15 +351,21 @@
<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>
<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>
<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>
<span class="font-medium">
<NumberWithTooltip :value="batchCost.deuterium" />
</span>
</div>
</div>
</div>
@@ -470,7 +486,7 @@
const showFleetStorageColumn = computed(() => {
if (props.type === 'building') {
const buildingType = props.itemType as BuildingType
return buildingType === 'shipyard'
return buildingType === 'shipyard' || buildingType === 'hangar'
} else if (props.type === 'technology') {
const techType = props.itemType as TechnologyType
return techType === 'computerTechnology'
@@ -662,43 +678,84 @@
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 = 30
} else if (buildingType === 'lunarBase') {
spaceBonus = 30
} else if (buildingType === 'roboticsFactory') {
buildSpeedBonus = level
} else if (buildingType === 'naniteFactory') {
buildSpeedBonus = level * 2
} else if (buildingType === 'researchLab') {
researchSpeedBonus = level
// Building calculation configuration
const buildingCalculations: Record<string, (level: number) => Partial<{
production: number
consumption: number
capacity: number
fleetStorage: number
spaceBonus: number
buildSpeedBonus: number
researchSpeedBonus: number
}>> = {
metalMine: (lvl) => ({
production: Math.floor(1500 * lvl * Math.pow(1.5, lvl) * resourceBonus),
consumption: Math.floor(10 * lvl * Math.pow(1.1, lvl))
}),
crystalMine: (lvl) => ({
production: Math.floor(1000 * lvl * Math.pow(1.5, lvl) * resourceBonus),
consumption: Math.floor(10 * lvl * Math.pow(1.1, lvl))
}),
deuteriumSynthesizer: (lvl) => ({
production: Math.floor(500 * lvl * Math.pow(1.5, lvl) * resourceBonus),
consumption: Math.floor(10 * lvl * Math.pow(1.1, lvl))
}),
solarPlant: (lvl) => ({
production: Math.floor(50 * lvl * Math.pow(1.1, lvl) * energyBonus)
}),
metalStorage: (lvl) => ({
capacity: Math.floor(baseCapacity * Math.pow(2, lvl) * storageBonus)
}),
crystalStorage: (lvl) => ({
capacity: Math.floor(baseCapacity * Math.pow(2, lvl) * storageBonus)
}),
deuteriumTank: (lvl) => ({
capacity: Math.floor(baseCapacity * Math.pow(2, lvl) * storageBonus)
}),
darkMatterCollector: (lvl) => ({
capacity: 1000 + lvl * 100,
production: Math.floor(25 * lvl * Math.pow(1.5, lvl))
}),
darkMatterTank: (lvl) => ({
capacity: Math.floor(1000 * Math.pow(2, lvl) * storageBonus)
}),
fusionReactor: (lvl) => ({
production: Math.floor(150 * lvl * Math.pow(1.15, lvl))
}),
shipyard: (lvl) => ({
fleetStorage: 1000 * lvl
}),
hangar: (lvl) => ({
fleetStorage: 500 * lvl
}),
terraformer: () => ({
spaceBonus: 30
}),
lunarBase: () => ({
spaceBonus: 30
}),
roboticsFactory: (lvl) => ({
buildSpeedBonus: lvl
}),
naniteFactory: (lvl) => ({
buildSpeedBonus: lvl * 2
}),
researchLab: (lvl) => ({
researchSpeedBonus: lvl
})
}
// Apply calculations if configuration exists
const calc = buildingCalculations[buildingType]
if (calc) {
const result = calc(level)
production = result.production ?? production
consumption = result.consumption ?? consumption
capacity = result.capacity ?? capacity
fleetStorage = result.fleetStorage ?? fleetStorage
spaceBonus = result.spaceBonus ?? spaceBonus
buildSpeedBonus = result.buildSpeedBonus ?? buildSpeedBonus
researchSpeedBonus = result.researchSpeedBonus ?? researchSpeedBonus
}
const points = pointsLogic.calculateBuildingPoints(buildingType, level - 1, level)

View File

@@ -53,7 +53,13 @@
<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">
<Badge
v-for="allyId in npc.allies.slice(0, 3)"
:key="allyId"
variant="outline"
class="text-xs cursor-pointer hover:bg-accent transition-colors"
@click="scrollToAlly(allyId)"
>
{{ getAllyName(allyId) }}
</Badge>
<Badge v-if="npc.allies.length > 3" variant="outline" class="text-xs">
@@ -80,7 +86,9 @@
<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>
<span class="text-muted-foreground">
{{ formatRelativeTime((Date.now() - recentEvent.timestamp) / 1000, t) }}{{ t('diplomacy.ago') }}
</span>
</div>
</div>
</CardContent>
@@ -98,7 +106,7 @@
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'
import { formatRelativeTime } from '@/utils/format'
const props = defineProps<{
npc: NPC
@@ -229,4 +237,12 @@
})
}
}
// 滚动到盟友卡片
const scrollToAlly = (allyId: string) => {
// 触发父组件的滚动事件
// 通过emit通知父组件滚动到指定的NPC卡片
const event = new CustomEvent('scrollToNpc', { detail: { npcId: allyId }, bubbles: true })
document.dispatchEvent(event)
}
</script>

View File

@@ -0,0 +1,166 @@
<template>
<Popover v-model:open="isOpen">
<PopoverTrigger as-child>
<Button data-tutorial="queue-button" variant="outline" size="icon" class="relative">
<ListOrdered class="h-4 w-4" />
<Badge
v-if="totalQueueCount > 0"
variant="default"
class="absolute -top-1 -right-1 h-5 w-5 p-0 flex items-center justify-center text-xs"
>
{{ totalQueueCount > 9 ? '9+' : totalQueueCount }}
</Badge>
</Button>
</PopoverTrigger>
<PopoverContent class="w-96 p-0" align="end">
<div class="flex items-center justify-between p-4 border-b">
<h3 class="font-semibold">{{ t('queue.title') }}</h3>
</div>
<ScrollArea class="max-h-96">
<div v-if="totalQueueCount === 0" class="p-8 text-center text-muted-foreground">
{{ t('queue.empty') }}
</div>
<div v-else class="divide-y p-4 space-y-3">
<!-- 建造队列 -->
<div v-for="item in buildQueue" :key="item.id" class="space-y-1.5">
<div class="flex items-center justify-between text-xs sm:text-sm gap-2">
<div class="flex items-center gap-1.5 sm:gap-2 min-w-0 flex-1">
<div
class="h-2 w-2 rounded-full animate-pulse flex-shrink-0"
:class="item.type === 'demolish' ? 'bg-destructive' : 'bg-green-500'"
/>
<span class="font-medium truncate">{{ getItemName(item) }}</span>
<span class="text-muted-foreground text-[10px] sm:text-xs">
<template v-if="item.type === 'ship' || item.type === 'defense'">
{{ t('queue.quantity') }} {{ item.quantity }}
</template>
<template v-else-if="item.type === 'demolish'"> {{ t('queue.demolishing') }}</template>
<template v-else> {{ t('queue.level') }} {{ item.targetLevel }}</template>
</span>
</div>
<div class="flex items-center gap-2 sm:gap-3 flex-shrink-0">
<span class="text-muted-foreground text-[10px] sm:text-xs whitespace-nowrap">
{{ formatTime(getRemainingTime(item)) }}
</span>
<Button @click="handleCancel(item)" variant="ghost" size="sm" class="h-5 sm:h-6 px-1.5 sm:px-2 text-[10px] sm:text-xs">
{{ t('queue.cancel') }}
</Button>
</div>
</div>
<Progress :model-value="getQueueProgress(item)" class="h-1.5" />
</div>
<!-- 研究队列 -->
<div v-for="item in researchQueue" :key="item.id" class="space-y-1.5">
<div class="flex items-center justify-between text-xs sm:text-sm gap-2">
<div class="flex items-center gap-1.5 sm:gap-2 min-w-0 flex-1">
<div class="h-2 w-2 rounded-full bg-blue-500 animate-pulse flex-shrink-0" />
<span class="font-medium truncate">{{ getItemName(item) }}</span>
<span class="text-muted-foreground text-[10px] sm:text-xs"> {{ t('queue.level') }} {{ item.targetLevel }}</span>
</div>
<div class="flex items-center gap-2 sm:gap-3 flex-shrink-0">
<span class="text-muted-foreground text-[10px] sm:text-xs whitespace-nowrap">
{{ formatTime(getRemainingTime(item)) }}
</span>
<Button
@click="handleCancelResearch(item.id)"
variant="ghost"
size="sm"
class="h-5 sm:h-6 px-1.5 sm:px-2 text-[10px] sm:text-xs"
>
{{ t('queue.cancel') }}
</Button>
</div>
</div>
<Progress :model-value="getQueueProgress(item)" class="h-1.5" />
</div>
</div>
</ScrollArea>
</PopoverContent>
</Popover>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { ListOrdered } from 'lucide-vue-next'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Progress } from '@/components/ui/progress'
import { useGameStore } from '@/stores/gameStore'
import { useGameConfig } from '@/composables/useGameConfig'
import { useI18n } from '@/composables/useI18n'
import { formatTime } from '@/utils/format'
import type { BuildQueueItem, BuildingType, ShipType, DefenseType, TechnologyType } from '@/types/game'
const { t } = useI18n()
const gameStore = useGameStore()
const { BUILDINGS, SHIPS, DEFENSES, TECHNOLOGIES } = useGameConfig()
const isOpen = ref(false)
// 获取当前星球的建造队列
const buildQueue = computed(() => {
return gameStore.currentPlanet?.buildQueue || []
})
// 获取研究队列
const researchQueue = computed(() => {
return gameStore.player.researchQueue || []
})
// 总队列数量
const totalQueueCount = computed(() => {
return buildQueue.value.length + researchQueue.value.length
})
// 获取队列项名称
const getItemName = (item: BuildQueueItem): string => {
if (item.type === 'building' || item.type === 'demolish') {
return BUILDINGS.value[item.itemType as BuildingType].name
} else if (item.type === 'ship') {
return SHIPS.value[item.itemType as ShipType].name
} else if (item.type === 'defense') {
return DEFENSES.value[item.itemType as DefenseType].name
} else if (item.type === 'technology') {
return TECHNOLOGIES.value[item.itemType as TechnologyType].name
}
return ''
}
// 获取剩余时间
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 elapsed = now - item.startTime
const total = item.endTime - item.startTime
return Math.min(100, (elapsed / total) * 100)
}
// 统一的取消处理
const handleCancel = (item: BuildQueueItem) => {
let eventName: string
if (item.type === 'building' || item.type === 'ship' || item.type === 'defense' || item.type === 'demolish') {
eventName = 'cancel-build'
} else if (item.type === 'technology') {
eventName = 'cancel-research'
} else {
return
}
const event = new CustomEvent(eventName, { detail: item.id })
window.dispatchEvent(event)
}
// 取消研究
const handleCancelResearch = (queueId: string) => {
const event = new CustomEvent('cancel-research', { detail: queueId })
window.dispatchEvent(event)
}
</script>

View File

@@ -0,0 +1,438 @@
<template>
<Teleport to="body">
<div v-if="tutorialState.isActive && currentStep" class="tutorial-overlay">
<!-- Dark overlay parts (4 rectangles around the highlight) -->
<template v-if="highlightRect && currentStep.target">
<!-- Top overlay -->
<div
class="tutorial-backdrop-part"
:style="{
top: '0',
left: '0',
width: '100%',
height: `${highlightRect.top}px`
}"
/>
<!-- Bottom overlay -->
<div
class="tutorial-backdrop-part"
:style="{
top: `${highlightRect.bottom}px`,
left: '0',
width: '100%',
height: `calc(100% - ${highlightRect.bottom}px)`
}"
/>
<!-- Left overlay -->
<div
class="tutorial-backdrop-part"
:style="{
top: `${highlightRect.top}px`,
left: '0',
width: `${highlightRect.left}px`,
height: `${highlightRect.height}px`
}"
/>
<!-- Right overlay -->
<div
class="tutorial-backdrop-part"
:style="{
top: `${highlightRect.top}px`,
left: `${highlightRect.right}px`,
width: `calc(100% - ${highlightRect.right}px)`,
height: `${highlightRect.height}px`
}"
/>
<!-- Highlight border -->
<div
class="tutorial-highlight-border"
:style="{
top: `${highlightRect.top}px`,
left: `${highlightRect.left}px`,
width: `${highlightRect.width}px`,
height: `${highlightRect.height}px`
}"
/>
</template>
<!-- Full overlay for center placement (no target) -->
<div v-else class="tutorial-backdrop-full" />
<!-- Tutorial tooltip -->
<div
v-if="tooltipPosition"
class="tutorial-tooltip"
:class="`tutorial-tooltip-${currentStep.placement || 'center'}`"
:style="{
top: tooltipPosition.top,
left: tooltipPosition.left,
transform: tooltipPosition.transform
}"
>
<Card class="tutorial-card">
<CardHeader class="pb-3">
<div class="flex items-center justify-between">
<CardTitle class="text-lg">{{ t(currentStep.title) }}</CardTitle>
<Button v-if="currentStep.canSkip" variant="ghost" size="icon" class="h-6 w-6" @click="skipTutorial">
<XIcon :size="16" />
</Button>
</div>
<CardDescription class="text-sm mt-2">
{{ t(currentStep.content) }}
</CardDescription>
</CardHeader>
<CardContent class="pt-0 space-y-3">
<!-- Progress bar -->
<div class="space-y-1">
<div class="flex justify-between text-xs text-muted-foreground">
<span>{{ t('tutorial.progress') }}</span>
<span>{{ tutorialState.currentStepIndex + 1 }} / {{ totalSteps }}</span>
</div>
<div class="w-full bg-secondary rounded-full h-1.5">
<div class="bg-primary h-1.5 rounded-full transition-all duration-300" :style="{ width: `${progress}%` }" />
</div>
</div>
<!-- Navigation buttons -->
<div class="flex gap-2">
<Button v-if="tutorialState.currentStepIndex > 0" variant="outline" size="sm" @click="previousStep">
<ChevronLeftIcon :size="16" class="mr-1" />
{{ t('tutorial.previous') }}
</Button>
<Button v-if="!isLastStep" class="ml-auto" size="sm" @click="handleNext" :disabled="!canProceed">
{{ t('tutorial.next') }}
<ChevronRightIcon :size="16" class="ml-1" />
</Button>
<Button v-else class="ml-auto" size="sm" @click="completeTutorial">
{{ t('tutorial.completeButton') }}
<CheckIcon :size="16" class="ml-1" />
</Button>
</div>
</CardContent>
</Card>
</div>
</div>
</Teleport>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
import { useTutorial, getTutorialSteps } from '@/composables/useTutorial'
import { useI18n } from '@/composables/useI18n'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { XIcon, ChevronLeftIcon, ChevronRightIcon, CheckIcon } from 'lucide-vue-next'
const { t } = useI18n()
const { tutorialState, currentStep, progress, isLastStep, nextStep, previousStep, skipTutorial, completeTutorial } = useTutorial()
const highlightRect = ref<DOMRect | null>(null)
const tooltipPosition = ref<{ top: string; left: string; transform: string } | null>(null)
const totalSteps = computed(() => getTutorialSteps().length)
const isMobile = ref(false)
// Check if current step can proceed
const canProceed = computed(() => {
if (!currentStep.value) return false
// 所有步骤都允许手动点击下一步
return true
})
// 检测是否为移动端
const checkMobile = () => {
isMobile.value = window.innerWidth < 768
}
// Calculate highlight and tooltip positions
const updatePositions = () => {
if (!currentStep.value) {
highlightRect.value = null
tooltipPosition.value = null
return
}
// 检测移动端
checkMobile()
// For center placement, no target element needed
if (!currentStep.value.target || currentStep.value.placement === 'center') {
highlightRect.value = null
tooltipPosition.value = {
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)'
}
return
}
// Find target element
const targetElement = document.querySelector(currentStep.value.target)
if (!targetElement) {
// Fallback to center if target not found
highlightRect.value = null
tooltipPosition.value = {
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)'
}
return
}
// Auto-scroll target element into view
targetElement.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center'
})
// Get target element rect
const rect = targetElement.getBoundingClientRect()
const padding = currentStep.value.highlightPadding || 8
// Set highlight rect with padding
highlightRect.value = new DOMRect(rect.left - padding, rect.top - padding, rect.width + padding * 2, rect.height + padding * 2)
// 获取视口尺寸
const viewportWidth = window.innerWidth
const viewportHeight = window.innerHeight
// 气泡的预估尺寸(根据视口大小响应式调整)
const tooltipWidth = isMobile.value ? Math.min(viewportWidth - 32, 360) : 480
const tooltipHeight = isMobile.value ? 280 : 300 // 预估高度
// 计算各个方向的可用空间
const spaceTop = rect.top
const spaceBottom = viewportHeight - rect.bottom
const spaceLeft = rect.left
const spaceRight = viewportWidth - rect.right
const tooltipOffset = isMobile.value ? 8 : 16 // 移动端使用更小的间距
const edgeMargin = isMobile.value ? 8 : 16 // 距离边缘的最小距离
// 根据优先级和可用空间自动选择最佳位置
let placement = currentStep.value.placement || 'bottom'
let finalPosition: { top: string; left: string; transform: string }
// 移动端优先使用 bottom 或 top 位置
if (isMobile.value) {
// 移动端强制使用 top/bottom忽略 left/right
if (placement === 'left' || placement === 'right') {
placement = spaceBottom > spaceTop ? 'bottom' : 'top'
}
}
// 智能位置选择:如果指定位置空间不足,自动调整
const canFitTop = spaceTop >= tooltipHeight + tooltipOffset + edgeMargin
const canFitBottom = spaceBottom >= tooltipHeight + tooltipOffset + edgeMargin
const canFitLeft = spaceLeft >= tooltipWidth + tooltipOffset + edgeMargin
const canFitRight = spaceRight >= tooltipWidth + tooltipOffset + edgeMargin
// 自动调整位置
if (placement === 'top' && !canFitTop && canFitBottom) {
placement = 'bottom'
} else if (placement === 'bottom' && !canFitBottom && canFitTop) {
placement = 'top'
} else if (placement === 'left' && !canFitLeft && canFitRight) {
placement = 'right'
} else if (placement === 'right' && !canFitRight && canFitLeft) {
placement = 'left'
}
// 计算位置
switch (placement) {
case 'top': {
let left = rect.left + rect.width / 2
// 确保不超出左右边界
left = Math.max(tooltipWidth / 2 + edgeMargin, Math.min(left, viewportWidth - tooltipWidth / 2 - edgeMargin))
finalPosition = {
top: `${Math.max(edgeMargin, rect.top - tooltipOffset)}px`,
left: `${left}px`,
transform: 'translate(-50%, -100%)'
}
break
}
case 'bottom': {
let left = rect.left + rect.width / 2
// 确保不超出左右边界
left = Math.max(tooltipWidth / 2 + edgeMargin, Math.min(left, viewportWidth - tooltipWidth / 2 - edgeMargin))
finalPosition = {
top: `${Math.min(viewportHeight - tooltipHeight - edgeMargin, rect.bottom + tooltipOffset)}px`,
left: `${left}px`,
transform: 'translate(-50%, 0)'
}
break
}
case 'left': {
let top = rect.top + rect.height / 2
// 确保不超出上下边界
top = Math.max(tooltipHeight / 2 + edgeMargin, Math.min(top, viewportHeight - tooltipHeight / 2 - edgeMargin))
finalPosition = {
top: `${top}px`,
left: `${Math.max(edgeMargin, rect.left - tooltipOffset)}px`,
transform: 'translate(-100%, -50%)'
}
break
}
case 'right': {
let top = rect.top + rect.height / 2
// 确保不超出上下边界
top = Math.max(tooltipHeight / 2 + edgeMargin, Math.min(top, viewportHeight - tooltipHeight / 2 - edgeMargin))
finalPosition = {
top: `${top}px`,
left: `${Math.min(viewportWidth - tooltipWidth - edgeMargin, rect.right + tooltipOffset)}px`,
transform: 'translate(0, -50%)'
}
break
}
default:
finalPosition = {
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)'
}
}
tooltipPosition.value = finalPosition
}
// Handle next step
const handleNext = () => {
if (canProceed.value) {
nextStep()
}
}
// Update positions when step changes
watch(
() => currentStep.value,
() => {
// Wait for DOM update and route change
setTimeout(() => {
updatePositions()
}, 100)
},
{ immediate: true }
)
// Update positions on window resize or scroll
const handleResize = () => {
checkMobile()
updatePositions()
}
onMounted(() => {
checkMobile()
window.addEventListener('resize', handleResize)
window.addEventListener('scroll', handleResize, true)
updatePositions()
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
window.removeEventListener('scroll', handleResize, true)
})
</script>
<style scoped>
.tutorial-overlay {
position: fixed;
inset: 0;
z-index: 9999;
pointer-events: none;
}
.tutorial-backdrop-part {
position: fixed;
background: rgba(0, 0, 0, 0.7);
pointer-events: auto;
transition: all 0.3s ease;
}
.tutorial-backdrop-full {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.7);
pointer-events: auto;
}
.tutorial-highlight-border {
position: fixed;
background: transparent;
border: 4px solid rgba(59, 130, 246, 0.5);
border-radius: 8px;
pointer-events: none;
transition: all 0.3s ease;
z-index: 10000;
}
.tutorial-tooltip {
position: fixed;
z-index: 10001;
pointer-events: auto;
max-width: 480px;
min-width: 320px;
}
/* 移动端样式调整 */
@media (max-width: 767px) {
.tutorial-tooltip {
max-width: calc(100vw - 32px);
min-width: calc(100vw - 32px);
width: calc(100vw - 32px);
}
.tutorial-tooltip-center {
max-width: calc(100vw - 32px);
}
.tutorial-card {
font-size: 0.9rem;
}
.tutorial-highlight-border {
border-width: 2px;
}
}
.tutorial-tooltip-center {
max-width: 560px;
}
@media (max-width: 767px) {
.tutorial-tooltip-center {
max-width: calc(100vw - 32px);
}
}
.tutorial-card {
animation: tutorial-fade-in 0.3s ease;
}
@keyframes tutorial-fade-in {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
/* Dark mode adjustments */
.dark .tutorial-backdrop-part {
background: rgba(0, 0, 0, 0.85);
}
.dark .tutorial-backdrop-full {
background: rgba(0, 0, 0, 0.85);
}
.dark .tutorial-highlight-border {
border-color: rgba(59, 130, 246, 0.6);
}
</style>

View File

@@ -7,7 +7,7 @@
</DialogHeader>
<div class="flex-1 overflow-y-auto min-h-0 mt-4 pr-2">
<div class="prose prose-sm dark:prose-invert max-w-none" v-html="renderedMarkdown"></div>
<div class="prose prose-sm dark:prose-invert max-w-none" v-html="renderedMarkdown" />
</div>
<DialogFooter class="flex gap-2 flex-shrink-0 mt-4">

View File

@@ -8,7 +8,7 @@
<slot />
</div>
<Sheet v-else-if="isMobile" :open="openMobile" v-bind="$attrs" @update:open="setOpenMobile">
<Sheet v-else-if="isMobile" :open="openMobile" v-bind="$attrs" @update:open="handleOpenMobileChange">
<SheetContent
data-sidebar="sidebar"
data-slot="sidebar"
@@ -79,12 +79,15 @@
<script setup lang="ts">
import type { SidebarProps } from '.'
import { watch } from 'vue'
import { cn } from '@/lib/utils'
import { Sheet, SheetContent } from '@/components/ui/sheet'
import SheetDescription from '@/components/ui/sheet/SheetDescription.vue'
import SheetHeader from '@/components/ui/sheet/SheetHeader.vue'
import SheetTitle from '@/components/ui/sheet/SheetTitle.vue'
import { SIDEBAR_WIDTH_MOBILE, useSidebar } from './utils'
import { useTutorial } from '@/composables/useTutorial'
import { useRouter } from 'vue-router'
defineOptions({
inheritAttrs: false
@@ -96,5 +99,51 @@
collapsible: 'offcanvas'
})
const router = useRouter()
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
const { tutorialState, currentStep, nextStep } = useTutorial()
// 包装setOpenMobile以拦截教程期间的关闭操作
const handleOpenMobileChange = (open: boolean) => {
// 如果是移动端且在教程的菜单相关步骤,阻止关闭侧边栏
if (tutorialState.value.isActive && currentStep.value) {
// 只在第3步期间阻止关闭侧边栏让玩家必须手动打开
if (currentStep.value.id === 'menu_intro_mobile') {
// 只允许打开,不允许关闭
if (open) {
setOpenMobile(true)
}
// 如果试图关闭,忽略该操作,保持打开状态
return
}
}
// 其他情况正常更新
setOpenMobile(open)
}
// 监听openMobile变化在移动端教程第3步时侧边栏打开后自动推进到第4步
watch(
() => openMobile.value,
(isOpen) => {
if (isMobile.value && tutorialState.value.isActive && currentStep.value) {
// 如果在第3步且侧边栏刚打开,自动推进到第4步
if (currentStep.value.id === 'menu_intro_mobile' && isOpen) {
setTimeout(() => {
nextStep()
}, 300) // 延迟300ms让侧边栏动画完成
}
}
}
)
// 监听路由变化,在移动端关闭侧边栏
watch(
() => router.currentRoute.value.path,
() => {
if (isMobile.value && openMobile.value) {
// 路由变化时关闭移动端侧边栏
setOpenMobile(false)
}
}
)
</script>

View File

@@ -41,3 +41,9 @@
const props = defineProps<ToasterProps>()
</script>
<style>
.dark [data-sonner-toast][data-styled='true'] [data-description] {
color: oklch(0.91 0 0 / 1);
}
</style>

View File

@@ -29,6 +29,7 @@ export const useGameConfig = () => {
[BuildingType.RoboticsFactory]: 'roboticsFactory',
[BuildingType.NaniteFactory]: 'naniteFactory',
[BuildingType.Shipyard]: 'shipyard',
[BuildingType.Hangar]: 'hangar',
[BuildingType.ResearchLab]: 'researchLab',
[BuildingType.MetalStorage]: 'metalStorage',
[BuildingType.CrystalStorage]: 'crystalStorage',

View File

@@ -0,0 +1,562 @@
import { ref, computed, watch } from 'vue'
import { useRouter } from 'vue-router'
import { useGameStore } from '@/stores/gameStore'
import type { TutorialStep, TutorialState } from '@/types/game'
import { BuildingType, TechnologyType } from '@/types/game'
// 桌面端引导步骤定义
export const desktopTutorialSteps: TutorialStep[] = [
// 第1步欢迎和基础介绍
{
id: 'welcome',
title: 'tutorial.welcome.title',
content: 'tutorial.welcome.content',
placement: 'center',
route: '/',
action: 'none',
canSkip: true
},
// 第2步资源栏介绍
{
id: 'resources_intro',
title: 'tutorial.resources.title',
content: 'tutorial.resources.content',
target: '.resource-bar',
placement: 'bottom',
route: '/',
action: 'none',
highlightPadding: 8
},
// 第3步星球选择器介绍
{
id: 'planet_info',
title: 'tutorial.planet.title',
content: 'tutorial.planet.content',
target: '[data-tutorial="planet-selector"]',
placement: 'bottom',
route: '/',
action: 'none',
highlightPadding: 8
},
// 第4步导航菜单介绍
{
id: 'navigation',
title: 'tutorial.navigation.title',
content: 'tutorial.navigation.content',
target: '[data-tutorial="navigation"]',
placement: 'right',
route: '/',
action: 'none',
highlightPadding: 12
},
// 第5步前往建筑页面
{
id: 'goto_buildings',
title: 'tutorial.gotoBuildings.title',
content: 'tutorial.gotoBuildings.content',
target: '[data-nav-path="/buildings"]',
placement: 'right',
route: '/',
action: 'click',
highlightPadding: 8
},
// 第6步建造太阳能电站提供能源是所有资源建筑的前置条件
{
id: 'build_solar_plant',
title: 'tutorial.buildSolarPlant.title',
content: 'tutorial.buildSolarPlant.content',
target: `[data-building="${BuildingType.SolarPlant}"]`,
placement: 'top',
route: '/buildings',
action: 'build',
actionTarget: BuildingType.SolarPlant,
highlightPadding: 12
},
// 第7步了解建造队列
{
id: 'wait_for_build',
title: 'tutorial.waitBuild.title',
content: 'tutorial.waitBuild.content',
target: '[data-tutorial="queue-button"]',
placement: 'bottom',
route: '/buildings',
action: 'none',
highlightPadding: 8
},
// 第8步建造金属矿有了太阳能电站后才能建造
{
id: 'build_metal_mine',
title: 'tutorial.buildMetalMine.title',
content: 'tutorial.buildMetalMine.content',
target: `[data-building="${BuildingType.MetalMine}"]`,
placement: 'top',
route: '/buildings',
action: 'build',
actionTarget: BuildingType.MetalMine,
highlightPadding: 12
},
// 第9步建造晶体矿
{
id: 'build_crystal_mine',
title: 'tutorial.buildCrystalMine.title',
content: 'tutorial.buildCrystalMine.content',
target: `[data-building="${BuildingType.CrystalMine}"]`,
placement: 'top',
route: '/buildings',
action: 'build',
actionTarget: BuildingType.CrystalMine,
highlightPadding: 12
},
// 第10步建造重氢合成器
{
id: 'build_deuterium',
title: 'tutorial.buildDeuterium.title',
content: 'tutorial.buildDeuterium.content',
target: `[data-building="${BuildingType.DeuteriumSynthesizer}"]`,
placement: 'top',
route: '/buildings',
action: 'build',
actionTarget: BuildingType.DeuteriumSynthesizer,
highlightPadding: 12
},
// 第11步升级资源矿到2级为机器人工厂做准备
{
id: 'upgrade_mines_intro',
title: 'tutorial.upgradeMines.title',
content: 'tutorial.upgradeMines.content',
placement: 'center',
route: '/buildings',
action: 'none'
},
// 第12步建造机器人工厂需要三种资源矿各2级
{
id: 'build_robotics',
title: 'tutorial.buildRobotics.title',
content: 'tutorial.buildRobotics.content',
target: `[data-building="${BuildingType.RoboticsFactory}"]`,
placement: 'top',
route: '/buildings',
action: 'build',
actionTarget: BuildingType.RoboticsFactory,
highlightPadding: 12
},
// 第13步继续升级资源矿到3级为研究实验室做准备
{
id: 'upgrade_mines_for_lab',
title: 'tutorial.upgradeMinesForLab.title',
content: 'tutorial.upgradeMinesForLab.content',
placement: 'center',
route: '/buildings',
action: 'none'
},
// 第14步建造研究实验室需要三种资源矿各3级
{
id: 'build_research_lab',
title: 'tutorial.buildResearchLab.title',
content: 'tutorial.buildResearchLab.content',
target: `[data-building="${BuildingType.ResearchLab}"]`,
placement: 'top',
route: '/buildings',
action: 'build',
actionTarget: BuildingType.ResearchLab,
highlightPadding: 12
},
// 第15步前往研究页面
{
id: 'goto_research',
title: 'tutorial.gotoResearch.title',
content: 'tutorial.gotoResearch.content',
target: '[data-nav-path="/research"]',
placement: 'right',
route: '/buildings',
action: 'click',
highlightPadding: 8
},
// 第16步研究能量科技
{
id: 'research_energy',
title: 'tutorial.researchEnergy.title',
content: 'tutorial.researchEnergy.content',
target: `[data-tech="${TechnologyType.EnergyTechnology}"]`,
placement: 'top',
route: '/research',
action: 'research',
actionTarget: TechnologyType.EnergyTechnology,
highlightPadding: 12
},
// 第17步介绍船坞需要机器人工厂2级
{
id: 'shipyard_intro',
title: 'tutorial.shipyardIntro.title',
content: 'tutorial.shipyardIntro.content',
placement: 'center',
route: '/research',
action: 'none'
},
// 第18步返回建筑页面建造船坞
{
id: 'goto_buildings_for_shipyard',
title: 'tutorial.gotoBuildingsForShipyard.title',
content: 'tutorial.gotoBuildingsForShipyard.content',
target: '[data-nav-path="/buildings"]',
placement: 'right',
route: '/research',
action: 'click',
highlightPadding: 8
},
// 第19步建造船坞
{
id: 'build_shipyard',
title: 'tutorial.buildShipyard.title',
content: 'tutorial.buildShipyard.content',
target: `[data-building="${BuildingType.Shipyard}"]`,
placement: 'top',
route: '/buildings',
action: 'build',
actionTarget: BuildingType.Shipyard,
highlightPadding: 12
},
// 第20步舰队和探索介绍
{
id: 'fleet_intro',
title: 'tutorial.fleetIntro.title',
content: 'tutorial.fleetIntro.content',
placement: 'center',
route: '/buildings',
action: 'none'
},
// 第21步银河系探索介绍
{
id: 'galaxy_intro',
title: 'tutorial.galaxyIntro.title',
content: 'tutorial.galaxyIntro.content',
target: '[data-nav-path="/galaxy"]',
placement: 'right',
route: '/buildings',
action: 'none',
highlightPadding: 8
},
// 第22步引导完成
{
id: 'tutorial_complete',
title: 'tutorial.complete.title',
content: 'tutorial.complete.content',
placement: 'center',
route: '/buildings',
action: 'none'
}
]
// 移动端引导步骤定义
export const mobileTutorialSteps: TutorialStep[] = [
// 第1步欢迎移动端
{
id: 'welcome_mobile',
title: 'tutorial.mobile.welcome.title',
content: 'tutorial.mobile.welcome.content',
placement: 'center',
route: '/',
action: 'none',
canSkip: true
},
// 第2步顶部资源栏介绍
{
id: 'resources_intro_mobile',
title: 'tutorial.mobile.resources.title',
content: 'tutorial.mobile.resources.content',
target: '.resource-bar',
placement: 'bottom',
route: '/',
action: 'none',
highlightPadding: 8
},
// 第3步汉堡菜单介绍 - 引导玩家手动点击打开菜单
{
id: 'menu_intro_mobile',
title: 'tutorial.mobile.menu.title',
content: 'tutorial.mobile.menu.content',
target: '[data-tutorial="mobile-menu"]',
placement: 'bottom',
route: '/',
action: 'none', // 让玩家手动点击汉堡菜单打开侧边栏
highlightPadding: 8
},
// 第4步:前往建筑页面 - 此时侧边栏已打开,让玩家手动点击
{
id: 'goto_buildings_mobile',
title: 'tutorial.mobile.gotoBuildings.title',
content: 'tutorial.mobile.gotoBuildings.content',
target: '[data-nav-path="/buildings"]',
placement: 'right', // 改为right,因为菜单在左侧展开
route: '/',
action: 'click', // 使用click,但不会自动触发,只是用来标识这是一个点击操作
highlightPadding: 8
},
// 第5步建造太阳能电站
{
id: 'build_solar_plant_mobile',
title: 'tutorial.mobile.buildSolarPlant.title',
content: 'tutorial.mobile.buildSolarPlant.content',
target: `[data-building="${BuildingType.SolarPlant}"]`,
placement: 'bottom',
route: '/buildings',
action: 'build',
actionTarget: BuildingType.SolarPlant,
highlightPadding: 12
},
// 第6步了解建造队列
{
id: 'wait_for_build_mobile',
title: 'tutorial.mobile.waitBuild.title',
content: 'tutorial.mobile.waitBuild.content',
target: '[data-tutorial="queue-button"]',
placement: 'bottom',
route: '/buildings',
action: 'none',
highlightPadding: 8
},
// 第7步建造金属矿
{
id: 'build_metal_mine_mobile',
title: 'tutorial.mobile.buildMetalMine.title',
content: 'tutorial.mobile.buildMetalMine.content',
target: `[data-building="${BuildingType.MetalMine}"]`,
placement: 'bottom',
route: '/buildings',
action: 'build',
actionTarget: BuildingType.MetalMine,
highlightPadding: 12
},
// 第8步完成教程
{
id: 'tutorial_complete_mobile',
title: 'tutorial.mobile.complete.title',
content: 'tutorial.mobile.complete.content',
placement: 'center',
route: '/buildings',
action: 'none'
}
]
// 检测是否为移动端
const isMobileDevice = () => {
return window.innerWidth < 768
}
// 根据设备类型获取教程步骤
export const getTutorialSteps = (): TutorialStep[] => {
return isMobileDevice() ? mobileTutorialSteps : desktopTutorialSteps
}
// 导出统一的 tutorialSteps为了兼容性
export const tutorialSteps = getTutorialSteps()
const tutorialState = ref<TutorialState>({
isActive: false,
currentStepIndex: 0,
completedSteps: [],
skipped: false
})
export function useTutorial() {
const router = useRouter()
const gameStore = useGameStore()
// 动态获取教程步骤
const currentTutorialSteps = computed(() => getTutorialSteps())
const currentStep = computed(() => {
if (!tutorialState.value.isActive || tutorialState.value.currentStepIndex >= currentTutorialSteps.value.length) {
return null
}
return currentTutorialSteps.value[tutorialState.value.currentStepIndex]
})
const progress = computed(() => {
return Math.round((tutorialState.value.currentStepIndex / currentTutorialSteps.value.length) * 100)
})
const isLastStep = computed(() => {
return tutorialState.value.currentStepIndex === currentTutorialSteps.value.length - 1
})
// 初始化引导
const startTutorial = () => {
const player = gameStore.player
if (!player.tutorialProgress || !player.tutorialProgress.tutorialCompleted) {
const now = Date.now()
tutorialState.value = {
isActive: true,
currentStepIndex: 0,
completedSteps: player.tutorialProgress?.completedStepIds || [],
skipped: false,
lastActiveTime: now
}
// 如果有进度,恢复到上次的步骤
if (player.tutorialProgress?.currentStep) {
const stepIndex = currentTutorialSteps.value.findIndex((s: TutorialStep) => s.id === player.tutorialProgress?.currentStep)
if (stepIndex !== -1) {
tutorialState.value.currentStepIndex = stepIndex
}
}
// 跳转到当前步骤的路由
if (currentStep.value?.route) {
router.push(currentStep.value.route)
}
}
}
// 下一步
const nextStep = () => {
if (!currentStep.value) return
// 标记当前步骤为已完成
if (!tutorialState.value.completedSteps.includes(currentStep.value.id)) {
tutorialState.value.completedSteps.push(currentStep.value.id)
}
// 保存进度到store
saveProgress()
// 移动到下一步
if (tutorialState.value.currentStepIndex < currentTutorialSteps.value.length - 1) {
tutorialState.value.currentStepIndex++
// 跳转到新步骤的路由
if (currentStep.value?.route) {
router.push(currentStep.value.route)
}
} else {
// 引导完成
completeTutorial()
}
}
// 上一步
const previousStep = () => {
if (tutorialState.value.currentStepIndex > 0) {
tutorialState.value.currentStepIndex--
// 跳转到新步骤的路由
if (currentStep.value?.route) {
router.push(currentStep.value.route)
}
}
}
// 跳过引导
const skipTutorial = () => {
tutorialState.value.isActive = false
tutorialState.value.skipped = true
// 保存跳过状态(跳过也视为已完成,避免刷新后重新弹出)
if (!gameStore.player.tutorialProgress) {
gameStore.player.tutorialProgress = {
tutorialCompleted: true,
completedStepIds: tutorialState.value.completedSteps,
currentStep: null,
skippedAt: Date.now()
}
} else {
gameStore.player.tutorialProgress.tutorialCompleted = true
gameStore.player.tutorialProgress.skippedAt = Date.now()
gameStore.player.tutorialProgress.currentStep = null
}
}
// 完成引导
const completeTutorial = () => {
tutorialState.value.isActive = false
// 保存完成状态
gameStore.player.tutorialProgress = {
tutorialCompleted: true,
completedStepIds: tutorialState.value.completedSteps,
currentStep: null
}
}
// 保存进度
const saveProgress = () => {
if (!gameStore.player.tutorialProgress) {
gameStore.player.tutorialProgress = {
tutorialCompleted: false,
completedStepIds: [],
currentStep: null
}
}
gameStore.player.tutorialProgress.completedStepIds = [...tutorialState.value.completedSteps]
gameStore.player.tutorialProgress.currentStep = currentStep.value?.id || null
}
// 检查步骤完成条件
const checkStepCompletion = (stepId: string): boolean => {
const step = currentTutorialSteps.value.find((s: TutorialStep) => s.id === stepId)
if (!step) return false
const planet = gameStore.currentPlanet
if (!planet) return false
switch (step.action) {
case 'build':
if (step.actionTarget) {
// 简单检查队列中是否有该建筑
const inQueue = planet.buildQueue.some(
item => item.itemType === step.actionTarget && (item.type === 'building' || item.type === 'demolish')
)
return inQueue
}
return false
case 'research':
if (step.actionTarget) {
// 简单检查队列中是否有该科技
const inQueue = planet.buildQueue.some(item => item.itemType === step.actionTarget && item.type === 'technology')
return inQueue
}
return false
case 'click':
case 'none':
default:
return true
}
}
// 不再自动推进,完全由玩家手动点击"下一步"按钮控制
// 监听路由变化,自动推进需要导航的教程步骤
watch(
() => router.currentRoute.value.path,
newPath => {
if (tutorialState.value.isActive && currentStep.value) {
// 如果当前步骤需要导航到特定页面,且已经到达该页面,自动推进
if (currentStep.value.action === 'none' && currentStep.value.target && currentStep.value.target.includes('data-nav-path')) {
// 提取目标路径
const match = currentStep.value.target.match(/data-nav-path="([^"]+)"/)
if (match && match[1] === newPath) {
setTimeout(() => {
nextStep()
}, 500)
}
}
}
}
)
return {
tutorialState,
currentStep,
progress,
isLastStep,
startTutorial,
nextStep,
previousStep,
skipTutorial,
completeTutorial,
checkStepCompletion
}
}

View File

@@ -130,6 +130,21 @@ export const BUILDINGS: Record<BuildingType, BuildingConfig> = {
12: { [BuildingType.RoboticsFactory]: 8, [BuildingType.ResearchLab]: 8, [BuildingType.NaniteFactory]: 2 }
}
},
[BuildingType.Hangar]: {
id: BuildingType.Hangar,
name: '机库',
description: '专门用于扩展舰队存储容量,支持星球专业化发展',
baseCost: { metal: 200, crystal: 100, deuterium: 50, darkMatter: 0, energy: 0 },
baseTime: 20,
costMultiplier: 1.8,
spaceUsage: 3,
fleetStorageBonus: 1500, // 每级增加1500舰队仓储比船坞更高
requirements: { [BuildingType.RoboticsFactory]: 1 }, // 只需要1级机器人工厂
levelRequirements: {
10: { [BuildingType.RoboticsFactory]: 3 },
20: { [BuildingType.RoboticsFactory]: 5 }
}
},
[BuildingType.ResearchLab]: {
id: BuildingType.ResearchLab,
name: '研究实验室',
@@ -674,13 +689,13 @@ export const SHIPS: Record<ShipType, ShipConfig> = {
[ShipType.Battleship]: {
id: ShipType.Battleship,
name: '战列舰',
description: '重型战舰',
description: '重型战舰,主力作战单位',
cost: { metal: 45000, crystal: 15000, deuterium: 0, darkMatter: 0, energy: 0 },
buildTime: 90,
cargoCapacity: 1500,
attack: 1000,
shield: 200,
armor: 6000,
attack: 1200,
shield: 300,
armor: 10000,
speed: 10000,
fuelConsumption: 500,
storageUsage: 25,
@@ -728,13 +743,13 @@ export const SHIPS: Record<ShipType, ShipConfig> = {
[ShipType.Destroyer]: {
id: ShipType.Destroyer,
name: '驱逐舰',
description: '擅长摧毁大型舰船的猎杀者',
description: '专业反大型舰船战舰,高火力低防护',
cost: { metal: 60000, crystal: 50000, deuterium: 15000, darkMatter: 0, energy: 0 },
buildTime: 120,
cargoCapacity: 2000,
attack: 2000,
shield: 500,
armor: 11000,
attack: 2500,
shield: 250,
armor: 8000,
speed: 5000,
fuelConsumption: 1000,
storageUsage: 40,

View File

@@ -8,6 +8,7 @@ export default {
close: 'Schließen',
back: 'Zurück',
next: 'Weiter',
gotIt: '',
previous: 'Vorherige',
submit: 'Absenden',
reset: 'Zurücksetzen',
@@ -125,6 +126,7 @@ export default {
roboticsFactory: 'Roboterfabrik',
naniteFactory: 'Nanitenfabrik',
shipyard: 'Raumschiffwerft',
hangar: 'Hangar',
researchLab: 'Forschungslabor',
metalStorage: 'Metallspeicher',
crystalStorage: 'Kristallspeicher',
@@ -165,6 +167,7 @@ export default {
roboticsFactory: 'Beschleunigt Baugeschwindigkeit',
naniteFactory: 'Erhöht Bauauftragskapazität, +1 pro Stufe (max 10 Stufen)',
shipyard: 'Baut Schiffe',
hangar: 'Spezialisierte Einrichtung zur Erweiterung der Flottenspeicherkapazität, unterstützt Planetenspezialisierung',
researchLab: 'Erforscht Technologien',
metalStorage: 'Erhöht Metallspeicherkapazität',
crystalStorage: 'Erhöht Kristallspeicherkapazität',
@@ -199,10 +202,10 @@ export default {
lightFighter: 'Grundlegende Kampfeinheit',
heavyFighter: 'Schwer gepanzerter Jäger',
cruiser: 'Mittleres Kriegsschiff, ausgewogene Offensive und Defensive',
battleship: 'Mächtiges Kriegsschiff',
battleship: 'Schweres Hauptkriegsschiff mit starker Feuerkraft und hoher Verteidigung',
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',
destroyer: 'Spezialisiertes Anti-Großschiff mit hoher Feuerkraft aber geringer Verteidigung',
smallCargo: 'Transportiert kleine Mengen Ressourcen',
largeCargo: 'Transportiert große Mengen Ressourcen',
colonyShip: 'Zur Kolonisierung neuer Planeten',
@@ -312,10 +315,13 @@ export default {
darkMatterSpecialist: 'Verbessert Dunkle-Materie-Sammlungseffizienz'
},
queue: {
title: 'Bauauftrag',
empty: 'Keine aktiven Aufgaben',
buildQueue: 'Bauauftrag',
researchQueue: 'Forschungsauftrag',
building: 'Im Bau',
researching: 'In Forschung',
demolishing: 'Wird abgerissen',
remaining: 'Verbleibend',
cancel: 'Abbrechen',
cancelBuild: 'Bau abbrechen',
@@ -587,11 +593,13 @@ export default {
battles: 'Kämpfe',
spy: 'Spionage',
npc: 'NPC',
diplomacy: '',
spied: 'Ausspioniert',
battleReports: 'Kampfberichte',
spyReports: 'Spionageberichte',
noBattleReports: 'Keine Kampfberichte',
noSpyReports: 'Keine Spionageberichte',
noDiplomaticReports: '',
noSpiedNotifications: 'Keine Ausspionierungs-Benachrichtigungen',
battleReport: 'Kampfbericht',
spyReport: 'Spionagebericht',
@@ -646,7 +654,38 @@ export default {
hostile: 'Sie sind feindlich und nehmen keine Geschenke an',
neutral_distrust: 'Sie vertrauen Ihnen nicht',
polite_decline: 'Sie lehnten höflich ab'
}
},
// Spied notification dialog
spiedNotificationDetails: '',
spyDetected: '',
detectionResult: '',
detectionSuccess: '',
spiedNotificationMessage: '',
spiedNotificationTip: '',
viewInGalaxy: '',
// Mission report dialog
missionReportDetails: '',
missionSuccess: '',
missionFailed: '',
origin: '',
destination: '',
missionDetails: '',
transportedResources: '',
recycledResources: '',
remainingDebris: '',
newPlanet: '',
// NPC activity dialog
npcActivityDetails: '',
activityType: {
recycle: ''
},
activityLocation: '',
position: '',
nearPlanet: '',
activityDescription: '',
npcActivityMessage: '',
arrivalTime: '',
npcActivityTip: ''
},
missionReports: {
transportSuccess: 'Transportmission erfolgreich abgeschlossen',
@@ -802,7 +841,8 @@ export default {
npcWillSpyAndAttack: '{npcName} wird in 5s spionieren und in 10s angreifen',
acceleratedMissions: '{count} Missionen auf 5 Sekunden beschleunigt',
npcFleetInitialized: '{npcName} Flotte initialisiert',
npcFleetDetails: '100 Spionagesonden\n500 Leichte Jäger\n300 Schwere Jäger\n200 Kreuzer\n100 Schlachtschiffe\n50 Bomber\n30 Zerstörer\n20 Schlachtkreuzer',
npcFleetDetails:
'100 Spionagesonden\n500 Leichte Jäger\n300 Schwere Jäger\n200 Kreuzer\n100 Schlachtschiffe\n50 Bomber\n30 Zerstörer\n20 Schlachtkreuzer',
dangerZone: 'Gefahrenzone',
dangerZoneDesc: 'Die folgenden Vorgänge sind irreversibel',
resetGame: 'Spiel zurücksetzen',
@@ -840,6 +880,10 @@ export default {
recentEvents: 'Aktuelle Ereignisse',
recentEventsDescription: 'Protokoll der jüngsten diplomatischen Aktivitäten',
ago: 'vor',
notifications: '',
markAllRead: '',
noReports: '',
viewAll: '',
status: {
friendly: 'Freundlich',
neutral: 'Neutral',
@@ -855,6 +899,13 @@ export default {
viewPlanets: 'Planeten ansehen'
},
lastEvent: 'Letztes Ereignis',
reportDetails: '',
eventDescription: '',
reputationChange: '',
before: '',
after: '',
statusChange: '',
viewDiplomacy: '',
events: {
gift: 'Geschenk gesendet',
attack: 'Angriff',
@@ -883,12 +934,20 @@ export default {
receivedGiftFromNpc: 'Geschenk von {npcName} erhalten',
acceptedGiftFromNpc: 'Sie haben ein Geschenk von {npcName} angenommen: {metal}M {crystal}K {deuterium}D',
playerRejectedGift: 'Spieler hat Geschenk abgelehnt',
rejectedGiftFromNpc: 'Sie haben ein Geschenk von {npcName} abgelehnt. Ansehen {reputation}'
rejectedGiftFromNpc: 'Sie haben ein Geschenk von {npcName} abgelehnt. Ansehen {reputation}',
destroyedNpcPlanet: '{npcName}s {planetName} zerstört',
playerDestroyedPlanet: 'Spieler hat {planetName} zerstört',
youDestroyedNpcPlanet: 'Sie haben {npcName}s {planetName} zerstört. Ansehen {reputation}',
playerDestroyedAllyPlanet: 'Spieler hat Verbündeten {allyName}s {planetName} zerstört',
allyOutraged: '{allyName} ist empört, dass Sie den Planeten {planetName} ihres Verbündeten {targetName} zerstört haben',
npcEliminated: 'NPC {npcName} wurde vollständig eliminiert',
npcEliminatedMessage: 'Sie haben alle Planeten von {npcName} zerstört! Diese Fraktion wurde vollständig ausgelöscht.'
}
},
pagination: {
previous: 'Vorherige',
next: 'Nächste',
gotIt: '',
first: 'Erste',
last: 'Letzte',
page: 'Seite {page}'
@@ -897,5 +956,39 @@ export default {
title: 'Seite nicht gefunden',
description: 'Entschuldigung, die gesuchte Seite existiert nicht',
goHome: 'Zur Startseite'
},
time: {
days: 'Tage',
hours: 'Stunden',
minutes: 'Minuten',
seconds: 'Sekunden'
},
tutorial: {
welcome: {
title: 'Willkommen bei OGame',
content: 'Willkommen, Kommandant! Beginnen wir mit den Grundlagen und bauen Sie Ihr Weltraum-Imperium auf.'
},
buildSolarPlant: {
title: 'Solarkraftwerk bauen',
content:
'Bauen Sie zuerst ein Solarkraftwerk! Es liefert Energie für Ihren Planeten. Ohne Energie können andere Ressourcengebäude nicht funktionieren. Dies ist der wichtigste erste Schritt.'
},
waitBuild: {
title: 'Bauauftrag',
content:
'Ihr Gebäude befindet sich jetzt in der Bauauftragsliste. Klicken Sie auf das Warteschlangensymbol oben rechts, um alle laufenden Bau- und Forschungsaufträge anzuzeigen. Gebäude brauchen Zeit zum Fertigstellen, aber Sie können während des Wartens weitermachen.'
},
mobile: {
welcome: {
title: 'Willkommen bei OGame (Mobil)',
content:
'Willkommen, Kommandant! Dies ist ein optimiertes Tutorial für Touchscreens. Wir werden schnell die Kernfunktionen durchgehen, damit Sie mit dem Aufbau Ihres Imperiums beginnen können.'
},
waitBuild: {
title: 'Bauauftrag',
content:
'Klicken Sie auf das Warteschlangensymbol oben rechts, um den Baufortschritt anzuzeigen. Sie können weiter andere Seiten durchsuchen - der Bau läuft im Hintergrund.'
}
}
}
}

View File

@@ -125,6 +125,7 @@ export default {
roboticsFactory: 'Robotics Factory',
naniteFactory: 'Nanite Factory',
shipyard: 'Shipyard',
hangar: 'Hangar',
researchLab: 'Research Lab',
metalStorage: 'Metal Storage',
crystalStorage: 'Crystal Storage',
@@ -163,6 +164,7 @@ export default {
roboticsFactory: 'Accelerates construction speed',
naniteFactory: 'Increases build queue capacity, +1 per level (max 10 levels)',
shipyard: 'Constructs ships',
hangar: 'Specialized facility for expanding fleet storage capacity, supports planetary specialization',
researchLab: 'Researches technologies',
metalStorage: 'Increases metal storage capacity',
crystalStorage: 'Increases crystal storage capacity',
@@ -197,10 +199,10 @@ export default {
lightFighter: 'Basic combat unit',
heavyFighter: 'Heavily armored fighter',
cruiser: 'Medium warship, balanced offense and defense',
battleship: 'Powerful warship',
battleship: 'Main heavy warship with powerful firepower and strong defense',
battlecruiser: 'Fast powerful warship, excels at attacking battleships',
bomber: 'Specialized ship for attacking defense structures',
destroyer: 'Hunter specialized in destroying large ships',
destroyer: 'Specialized anti-capital ship with high firepower but low defense',
smallCargo: 'Transports small amounts of resources',
largeCargo: 'Transports large amounts of resources',
colonyShip: 'Used to colonize new planets',
@@ -312,6 +314,8 @@ export default {
darkMatterSpecialist: 'Improves dark matter collection efficiency'
},
queue: {
title: 'Build Queue',
empty: 'No active tasks',
buildQueueBonus: 'Build Queue',
spaceBonus: 'Space Bonus',
buildSpeedBonus: 'Build Speed Bonus',
@@ -319,6 +323,7 @@ export default {
researchQueueBonus: 'Research Queue',
building: 'Building',
researching: 'Researching',
demolishing: 'Demolishing',
remaining: 'Remaining',
cancel: 'Cancel',
cancelBuild: 'Cancel Build',
@@ -477,7 +482,8 @@ export default {
recallFleet: 'Recall Fleet',
abortMission: 'Abort Mission',
abortMissionTitle: 'Confirm Abort Mission',
abortMissionWarning: 'WARNING: Aborting this mission will permanently lose {ships} ships and {resources} resources!\n\nThis action is irreversible and the fleet and resources will not return.',
abortMissionWarning:
'WARNING: Aborting this mission will permanently lose {ships} ships and {resources} resources!\n\nThis action is irreversible and the fleet and resources will not return.',
abortMissionSuccess: 'Mission Aborted',
abortMissionSuccessMessage: 'Mission has been aborted, fleet and resources are lost.',
sendFailed: 'Send Failed',
@@ -588,10 +594,12 @@ export default {
battles: 'Battles',
spy: 'Spy',
npc: 'NPC',
diplomacy: 'Diplomacy',
battleReports: 'Battle Reports',
spyReports: 'Spy Reports',
noBattleReports: 'No battle reports',
noSpyReports: 'No spy reports',
noDiplomaticReports: 'No diplomatic reports',
battleReport: 'Battle Report',
spyReport: 'Spy Report',
victory: 'Victory',
@@ -639,7 +647,38 @@ export default {
hostile: 'They are hostile towards you and do not accept gifts',
neutral_distrust: 'They lack trust in you',
polite_decline: 'They politely declined'
}
},
// Spied notification dialog
spiedNotificationDetails: 'Spied Notification Details',
spyDetected: 'Spy Detected',
detectionResult: 'Detection Result',
detectionSuccess: 'Your spy probe was detected!',
spiedNotificationMessage: '{npc} attempted to spy on your planet {planet}',
spiedNotificationTip: 'Consider increasing your defense or counter-attacking if this NPC is hostile',
viewInGalaxy: 'View in Galaxy',
// Mission report dialog
missionReportDetails: 'Mission Report Details',
missionSuccess: 'Success',
missionFailed: 'Failed',
origin: 'Origin',
destination: 'Destination',
missionDetails: 'Mission Details',
transportedResources: 'Transported Resources',
recycledResources: 'Recycled Resources',
remainingDebris: 'Remaining Debris',
newPlanet: 'New Planet',
// NPC activity dialog
npcActivityDetails: 'NPC Activity Details',
activityType: {
recycle: 'Recycling Debris'
},
activityLocation: 'Activity Location',
position: 'Position',
nearPlanet: 'Near Planet',
activityDescription: 'Activity Description',
npcActivityMessage: '{npc} is {activity} at {position}',
arrivalTime: 'Arrival Time',
npcActivityTip: 'NPCs may collect debris from battles. You can try to reach the location first if you want to compete for resources'
},
missionReports: {
transportSuccess: 'Transport mission completed successfully',
@@ -798,7 +837,8 @@ export default {
npcWillSpyAndAttack: '{npcName} will spy in 5s and attack in 10s',
acceleratedMissions: 'Accelerated {count} missions to 5 seconds',
npcFleetInitialized: '{npcName} fleet initialized',
npcFleetDetails: '100 Spy Probes\n500 Light Fighters\n300 Heavy Fighters\n200 Cruisers\n100 Battleships\n50 Bombers\n30 Destroyers\n20 Battlecruisers',
npcFleetDetails:
'100 Spy Probes\n500 Light Fighters\n300 Heavy Fighters\n200 Cruisers\n100 Battleships\n50 Bombers\n30 Destroyers\n20 Battlecruisers',
dangerZone: 'Danger Zone',
dangerZoneDesc: 'The following operations are irreversible',
resetGame: 'Reset Game',
@@ -806,7 +846,8 @@ export default {
completeAllQueues: 'Complete All Queues',
completeAllQueuesDesc: 'Instantly complete all building, research, ship, defense queues and fleet missions',
completeQueues: 'Complete Queues',
completeQueuesSuccess: 'Completed {buildingCount} building queues, {researchCount} research queues, {missionCount} fleet missions, {missileCount} missile attacks'
completeQueuesSuccess:
'Completed {buildingCount} building queues, {researchCount} research queues, {missionCount} fleet missions, {missileCount} missile attacks'
},
alerts: {
npcSpyIncoming: 'NPC Spy Probe Incoming',
@@ -836,6 +877,10 @@ export default {
recentEvents: 'Recent Events',
recentEventsDescription: 'Recent diplomatic activity log',
ago: 'ago',
notifications: 'Diplomatic Notifications',
markAllRead: 'Mark All Read',
noReports: 'No diplomatic events',
viewAll: 'View All',
status: {
friendly: 'Friendly',
neutral: 'Neutral',
@@ -851,6 +896,13 @@ export default {
viewPlanets: 'View Planets'
},
lastEvent: 'Last Event',
reportDetails: 'Diplomatic Report Details',
eventDescription: 'Event Description',
reputationChange: 'Reputation Change',
before: 'Before',
after: 'After',
statusChange: 'Status Change',
viewDiplomacy: 'View Diplomacy Page',
events: {
gift: 'Sent Gift',
attack: 'Attack',
@@ -863,7 +915,7 @@ export default {
giftedResources: 'Gifted {metal}M {crystal}C {deuterium}D',
receivedGiftFromPlayer: 'Received gift from player',
giftedToNpc: 'You gifted resources to {npcName}. Reputation +{reputation}',
rejectedPlayerGift: 'Rejected player\'s gift',
rejectedPlayerGift: "Rejected player's gift",
npcRejectedGift: '{npcName} rejected your gift. Reputation {reputation}',
attackedNpc: 'Attacked {npcName}',
wasAttackedByPlayer: 'Was attacked by player',
@@ -872,14 +924,21 @@ export default {
allyDispleased: '{allyName} is displeased that you attacked their ally {targetName}',
wasSpiedByPlayer: 'Was spied by player (detected: {detected})',
spyDetected: 'Your espionage was detected by {npcName}',
stoleDebrisFromTerritory: 'Stole debris from {npcName}\'s territory',
stoleDebrisFromTerritory: "Stole debris from {npcName}'s territory",
playerStoleDebris: 'Player stole debris from territory',
recycledDebrisNearNpc: 'You recycled debris near {npcName}\'s planet. They are displeased.',
recycledDebrisNearNpc: "You recycled debris near {npcName}'s planet. They are displeased.",
giftedResourcesToPlayer: 'Gifted resources to player',
receivedGiftFromNpc: 'Received gift from {npcName}',
acceptedGiftFromNpc: 'You accepted a gift from {npcName}: {metal}M {crystal}C {deuterium}D',
playerRejectedGift: 'Player rejected gift',
rejectedGiftFromNpc: 'You rejected a gift from {npcName}. Reputation {reputation}'
rejectedGiftFromNpc: 'You rejected a gift from {npcName}. Reputation {reputation}',
destroyedNpcPlanet: "Destroyed {npcName}'s {planetName}",
playerDestroyedPlanet: 'Player destroyed {planetName}',
youDestroyedNpcPlanet: "You destroyed {npcName}'s {planetName}. Reputation {reputation}",
playerDestroyedAllyPlanet: "Player destroyed ally {allyName}'s {planetName}",
allyOutraged: "{allyName} is outraged that you destroyed their ally {targetName}'s {planetName}",
npcEliminated: 'NPC {npcName} has been completely eliminated',
npcEliminatedMessage: "You destroyed all of {npcName}'s planets! This faction has been completely wiped out."
}
},
pagination: {
@@ -893,5 +952,162 @@ export default {
title: 'Page Not Found',
description: 'Sorry, the page you are looking for does not exist',
goHome: 'Go Home'
},
time: {
days: 'days',
hours: 'hours',
minutes: 'minutes',
seconds: 'seconds'
},
tutorial: {
progress: 'Progress',
previous: 'Previous',
next: 'Next',
gotIt: 'Got It',
completeButton: 'Complete',
skip: 'Skip Tutorial',
welcome: {
title: 'Welcome to OGame',
content:
'Welcome, Commander! This tutorial will guide you through the basics of building your empire. Click "Next" to begin your journey.'
},
resources: {
title: 'Resource Overview',
content:
'These are your resources: Metal, Crystal, and Deuterium. They are essential for building structures and researching technologies. Energy is also important to power your infrastructure.'
},
planet: {
title: 'Your Planet',
content: 'This is your home planet. You can view its name, coordinates, and switch between planets here as you expand your empire.'
},
navigation: {
title: 'Navigation Menu',
content:
'Use this menu to navigate between different sections: Buildings, Research, Fleet, Galaxy, and more. Each section offers unique gameplay features.'
},
gotoBuildings: {
title: 'Go to Buildings',
content: 'Let\'s start by building some structures. Click on the "Buildings" menu item to view available structures.'
},
buildSolarPlant: {
title: 'Build Solar Plant',
content:
'First, build a Solar Plant! It generates energy for your planet. Without energy, other resource buildings cannot function. This is the most important first step.'
},
waitBuild: {
title: 'Build Queue',
content:
'Your building is now in the construction queue. Click the queue icon in the top-right corner to view all ongoing construction and research tasks. Buildings take time to complete, but you can continue working while waiting.'
},
buildMetalMine: {
title: 'Build Metal Mine',
content:
"Now that you have energy, you can build the Metal Mine. It's your primary source of metal, which is used in almost every structure and ship."
},
buildCrystalMine: {
title: 'Build Crystal Mine',
content: 'Crystal is rarer but essential for advanced technologies. Build a Crystal Mine to start collecting this valuable resource.'
},
buildDeuterium: {
title: 'Build Deuterium Synthesizer',
content:
'Deuterium is essential for ship fuel and advanced research. Build a Deuterium Synthesizer to start producing this critical resource.'
},
upgradeMines: {
title: 'Upgrade Resource Mines',
content:
'Next, you need to upgrade all three resource mines (Metal, Crystal, Deuterium) to level 2 to meet the requirements for building a Robotics Factory. Upgrade them when you have enough resources.'
},
buildRobotics: {
title: 'Build Robotics Factory',
content:
'The Robotics Factory significantly speeds up construction. It requires Metal Mine, Crystal Mine, and Deuterium Synthesizer at level 2 each. Build it to improve construction efficiency!'
},
upgradeMinesForLab: {
title: 'Continue Upgrading Mines',
content:
'Now you need to upgrade all three resource mines to level 3 to meet the requirements for the Research Lab. Keep developing your resource production.'
},
buildResearchLab: {
title: 'Build Research Lab',
content:
'The Research Lab is the foundation of technological advancement. It requires all three resource mines at level 3. Build it to unlock technology research!'
},
gotoResearch: {
title: 'Go to Research',
content: 'Now that you have a Research Lab, click on the "Research" menu to view available technologies.'
},
researchEnergy: {
title: 'Research Energy Technology',
content:
"Energy Technology improves your energy production and unlocks advanced structures. It's one of the most fundamental and important technologies."
},
shipyardIntro: {
title: 'Fleet and Shipyard',
content:
'Ships allow you to explore the galaxy, transport resources, and defend your empire. To build ships, you need a Shipyard (requires Robotics Factory level 2).'
},
gotoBuildingsForShipyard: {
title: 'Return to Buildings',
content: 'Go back to the Buildings page to construct your Shipyard.'
},
buildShipyard: {
title: 'Build Shipyard',
content: 'The Shipyard allows you to construct ships and defense systems. This is crucial for fleet operations.'
},
fleetIntro: {
title: 'Fleet Operations',
content:
'Once you have ships, you can send them on missions: transport resources, colonize planets, attack enemies, or explore debris fields.'
},
galaxyIntro: {
title: 'Explore the Galaxy',
content:
'The Galaxy view shows other planets, debris fields, and opportunities for expansion. Use it to scout targets and plan your strategy.'
},
complete: {
title: 'Tutorial Complete!',
content:
'Congratulations, Commander! You now know the basics. Continue building your empire, research technologies, and explore the galaxy. Remember: develop energy first, then resources, then factories and research! Good luck!'
},
// Mobile tutorial
mobile: {
welcome: {
title: 'Welcome to OGame (Mobile)',
content:
"Welcome, Commander! This is a streamlined tutorial designed for touchscreens. We'll quickly cover the core features to get you started building your empire."
},
resources: {
title: 'Top Resource Bar',
content: 'At the top, you see your resources: Metal, Crystal, and Deuterium. Tap to view detailed production info.'
},
menu: {
title: 'Open Navigation Menu',
content:
'Tap this menu icon to open the navigation bar. You can access Buildings, Research, Fleet, and all other features from here.'
},
gotoBuildings: {
title: 'Go to Buildings Page',
content: 'The menu is now open! Now tap "Buildings" to start constructing your infrastructure.'
},
buildSolarPlant: {
title: 'Build Solar Plant',
content: 'First, build a Solar Plant! Scroll down to find it and tap the card to build. Energy is the foundation of everything.'
},
waitBuild: {
title: 'Build Queue',
content:
'Click the queue icon in the top-right corner to view build progress. You can continue browsing other pages - construction happens in the background.'
},
buildMetalMine: {
title: 'Build Metal Mine',
content: 'Now that you have energy, build a Metal Mine. Scroll down to find it and tap to build.'
},
complete: {
title: 'Quick Tutorial Complete!',
content:
"Great! You've mastered the basics. Continue building Crystal and Deuterium facilities, then explore other features. Remember: energy first, then resources!"
}
}
}
}

View File

@@ -8,6 +8,7 @@ export default {
close: '閉じる',
back: '戻る',
next: '次へ',
gotIt: '',
previous: '前へ',
submit: '送信',
reset: 'リセット',
@@ -125,6 +126,7 @@ export default {
roboticsFactory: 'ロボット工場',
naniteFactory: 'ナノマシン工場',
shipyard: '造船所',
hangar: '格納庫',
researchLab: '研究所',
metalStorage: '金属倉庫',
crystalStorage: 'クリスタル倉庫',
@@ -165,6 +167,7 @@ export default {
roboticsFactory: '建設速度を向上',
naniteFactory: '建設キュー数を増加、レベル毎に+1最大10レベル',
shipyard: '艦船を建造',
hangar: '艦隊収容能力を拡張する専用施設、惑星の専門化をサポート',
researchLab: '技術を研究',
metalStorage: '金属の貯蔵上限を増加',
crystalStorage: 'クリスタルの貯蔵上限を増加',
@@ -199,10 +202,10 @@ export default {
lightFighter: '基本戦闘ユニット',
heavyFighter: '重装甲戦闘機',
cruiser: '中型戦艦、攻守バランス型',
battleship: '強力な戦艦',
battleship: '主力重戦艦、強力な火力と高い防御力を持つ',
battlecruiser: '高速強力な戦闘艦、戦艦への攻撃に優れる',
bomber: '防御施設への攻撃に特化した艦船',
destroyer: '大型艦の破壊に特化したハンター',
destroyer: '大型艦専用艦、高火力だが防御力が低い',
smallCargo: '少量の資源を輸送',
largeCargo: '大量の資源を輸送',
colonyShip: '新惑星の植民に使用',
@@ -312,10 +315,13 @@ export default {
darkMatterSpecialist: 'ダークマター採取効率を向上'
},
queue: {
title: '建設キュー',
empty: '進行中のタスクはありません',
buildQueue: '建設キュー',
researchQueue: '研究キュー',
building: '建設中',
researching: '研究中',
demolishing: '解体中',
remaining: '残り時間',
cancel: 'キャンセル',
cancelBuild: '建設キャンセル',
@@ -580,10 +586,12 @@ export default {
battles: '戦闘',
spy: 'スパイ',
npc: 'NPC',
diplomacy: '',
battleReports: '戦闘レポート',
spyReports: 'スパイレポート',
noBattleReports: '戦闘レポートなし',
noSpyReports: 'スパイレポートなし',
noDiplomaticReports: '',
battleReport: '戦闘レポート',
spyReport: 'スパイレポート',
victory: '勝利',
@@ -639,7 +647,38 @@ export default {
hostile: '相手は敵対的でギフトを受け取りません',
neutral_distrust: '相手はあなたを信頼していません',
polite_decline: '丁重に断りました'
}
},
// Spied notification dialog
spiedNotificationDetails: '',
spyDetected: '',
detectionResult: '',
detectionSuccess: '',
spiedNotificationMessage: '',
spiedNotificationTip: '',
viewInGalaxy: '',
// Mission report dialog
missionReportDetails: '',
missionSuccess: '',
missionFailed: '',
origin: '',
destination: '',
missionDetails: '',
transportedResources: '',
recycledResources: '',
remainingDebris: '',
newPlanet: '',
// NPC activity dialog
npcActivityDetails: '',
activityType: {
recycle: ''
},
activityLocation: '',
position: '',
nearPlanet: '',
activityDescription: '',
npcActivityMessage: '',
arrivalTime: '',
npcActivityTip: ''
},
missionReports: {
transportSuccess: '輸送ミッションが正常に完了しました',
@@ -831,6 +870,10 @@ export default {
recentEvents: '最近のイベント',
recentEventsDescription: '最近の外交活動ログ',
ago: '前',
notifications: '',
markAllRead: '',
noReports: '',
viewAll: '',
status: {
friendly: '友好的',
neutral: '中立',
@@ -846,6 +889,13 @@ export default {
viewPlanets: '惑星を表示'
},
lastEvent: '最後のイベント',
reportDetails: '',
eventDescription: '',
reputationChange: '',
before: '',
after: '',
statusChange: '',
viewDiplomacy: '',
events: {
gift: 'ギフト送信',
attack: '攻撃',
@@ -874,12 +924,20 @@ export default {
receivedGiftFromNpc: '{npcName}からギフトを受け取りました',
acceptedGiftFromNpc: '{npcName}からのギフトを受け取りました:{metal}M {crystal}C {deuterium}D',
playerRejectedGift: 'プレイヤーがギフトを拒否しました',
rejectedGiftFromNpc: '{npcName}からのギフトを拒否しました。評判{reputation}'
rejectedGiftFromNpc: '{npcName}からのギフトを拒否しました。評判{reputation}',
destroyedNpcPlanet: '{npcName}の{planetName}を破壊しました',
playerDestroyedPlanet: 'プレイヤーが{planetName}を破壊しました',
youDestroyedNpcPlanet: 'あなたは{npcName}の{planetName}を破壊しました。評判{reputation}',
playerDestroyedAllyPlanet: 'プレイヤーが同盟{allyName}の{planetName}を破壊しました',
allyOutraged: '{allyName}はあなたが同盟{targetName}の{planetName}を破壊したことに激怒しています',
npcEliminated: 'NPC {npcName}は完全に排除されました',
npcEliminatedMessage: 'あなたは{npcName}のすべての惑星を破壊しました!この勢力は完全に壊滅しました。'
}
},
pagination: {
previous: '前へ',
next: '次へ',
gotIt: '',
first: '最初',
last: '最後',
page: '{page}ページ'
@@ -888,5 +946,39 @@ export default {
title: 'ページが見つかりません',
description: '申し訳ございません。お探しのページは存在しません',
goHome: 'ホームに戻る'
},
time: {
days: '日',
hours: '時間',
minutes: '分',
seconds: '秒'
},
tutorial: {
welcome: {
title: 'OGame-Vue-Ts へようこそ',
content: 'ようこそ、司令官!基礎から始めて、宇宙帝国を築きましょう。'
},
buildSolarPlant: {
title: '太陽光発電所を建設',
content:
'まず太陽光発電所を建設しましょう!惑星にエネルギーを供給します。エネルギーがないと、他の資源施設が機能しません。これが最も重要な第一歩です。'
},
waitBuild: {
title: '建設キュー',
content:
'建物は建設キューに追加されました。右上のキューアイコンをクリックすると、進行中のすべての建設と研究タスクを確認できます。建設には時間がかかりますが、待機中も作業を続けられます。'
},
mobile: {
welcome: {
title: 'OGame-Vue-Ts へようこそ(モバイル版)',
content:
'ようこそ、司令官!これはタッチスクリーン向けに最適化された簡易チュートリアルです。帝国建設を始めるために、コア機能を素早くご紹介します。'
},
waitBuild: {
title: '建設キュー',
content:
'右上のキューアイコンをクリックして建設進度を確認できます。他のページを閲覧し続けることができます。建設はバックグラウンドで進行します。'
}
}
}
}

View File

@@ -8,6 +8,7 @@ export default {
close: '닫기',
back: '돌아가기',
next: '다음',
gotIt: '',
previous: '이전',
submit: '제출',
reset: '초기화',
@@ -125,6 +126,7 @@ export default {
roboticsFactory: '로봇 공장',
naniteFactory: '나노 공장',
shipyard: '조선소',
hangar: '격납고',
researchLab: '연구소',
metalStorage: '금속 창고',
crystalStorage: '크리스탈 창고',
@@ -165,6 +167,7 @@ export default {
roboticsFactory: '건설 속도 향상',
naniteFactory: '건설 대기열 수 증가, 레벨당 +1 (최대 10레벨)',
shipyard: '함선 건조',
hangar: '함대 저장 용량 확장 전용 시설, 행성 전문화 지원',
researchLab: '기술 연구',
metalStorage: '금속 저장 용량 증가',
crystalStorage: '크리스탈 저장 용량 증가',
@@ -199,10 +202,10 @@ export default {
lightFighter: '기본 전투 유닛',
heavyFighter: '중장갑 전투기',
cruiser: '중형 전함, 공격과 방어 균형',
battleship: '강력한 전함',
battleship: '주력 중전함, 강력한 화력과 높은 방어력 보유',
battlecruiser: '빠르고 강력한 전투함, 전함 공격에 탁월',
bomber: '방어 시설 공격 전문 함선',
destroyer: '대형 함선 파괴 전문 헌터',
destroyer: '대형 함선 전문 격파함, 높은 화력이지만 방어력 낮음',
smallCargo: '소량의 자원 운송',
largeCargo: '대량의 자원 운송',
colonyShip: '새로운 행성 식민에 사용',
@@ -312,10 +315,13 @@ export default {
darkMatterSpecialist: '암흑 물질 수집 효율 향상'
},
queue: {
title: '건설 대기열',
empty: '활성 작업 없음',
buildQueue: '건설 대기열',
researchQueue: '연구 대기열',
building: '건설 중',
researching: '연구 중',
demolishing: '철거 중',
remaining: '남은 시간',
cancel: '취소',
cancelBuild: '건설 취소',
@@ -581,10 +587,12 @@ export default {
battles: '전투',
spy: '정찰',
npc: 'NPC',
diplomacy: '',
battleReports: '전투 보고서',
spyReports: '정찰 보고서',
noBattleReports: '전투 보고서 없음',
noSpyReports: '정찰 보고서 없음',
noDiplomaticReports: '',
battleReport: '전투 보고서',
spyReport: '정찰 보고서',
victory: '승리',
@@ -640,7 +648,38 @@ export default {
hostile: '상대방이 적대적이어서 선물을 받지 않습니다',
neutral_distrust: '상대방이 당신을 신뢰하지 않습니다',
polite_decline: '정중하게 거절했습니다'
}
},
// Spied notification dialog
spiedNotificationDetails: '',
spyDetected: '',
detectionResult: '',
detectionSuccess: '',
spiedNotificationMessage: '',
spiedNotificationTip: '',
viewInGalaxy: '',
// Mission report dialog
missionReportDetails: '',
missionSuccess: '',
missionFailed: '',
origin: '',
destination: '',
missionDetails: '',
transportedResources: '',
recycledResources: '',
remainingDebris: '',
newPlanet: '',
// NPC activity dialog
npcActivityDetails: '',
activityType: {
recycle: ''
},
activityLocation: '',
position: '',
nearPlanet: '',
activityDescription: '',
npcActivityMessage: '',
arrivalTime: '',
npcActivityTip: ''
},
missionReports: {
transportSuccess: '수송 임무가 성공적으로 완료되었습니다',
@@ -832,6 +871,10 @@ export default {
recentEvents: '최근 이벤트',
recentEventsDescription: '최근 외교 활동 로그',
ago: '전',
notifications: '',
markAllRead: '',
noReports: '',
viewAll: '',
status: {
friendly: '우호적',
neutral: '중립',
@@ -847,6 +890,13 @@ export default {
viewPlanets: '행성 보기'
},
lastEvent: '최근 이벤트',
reportDetails: '',
eventDescription: '',
reputationChange: '',
before: '',
after: '',
statusChange: '',
viewDiplomacy: '',
events: {
gift: '선물 전송',
attack: '공격',
@@ -875,12 +925,20 @@ export default {
receivedGiftFromNpc: '{npcName}로부터 선물을 받았습니다',
acceptedGiftFromNpc: '{npcName}의 선물을 받았습니다: {metal}M {crystal}C {deuterium}D',
playerRejectedGift: '플레이어가 선물을 거부했습니다',
rejectedGiftFromNpc: '{npcName}의 선물을 거부했습니다. 평판 {reputation}'
rejectedGiftFromNpc: '{npcName}의 선물을 거부했습니다. 평판 {reputation}',
destroyedNpcPlanet: '{npcName}의 {planetName}을(를) 파괴했습니다',
playerDestroyedPlanet: '플레이어가 {planetName}을(를) 파괴했습니다',
youDestroyedNpcPlanet: '당신은 {npcName}의 {planetName}을(를) 파괴했습니다. 평판 {reputation}',
playerDestroyedAllyPlanet: '플레이어가 동맹 {allyName}의 {planetName}을(를) 파괴했습니다',
allyOutraged: '{allyName}은(는) 당신이 동맹 {targetName}의 {planetName}을(를) 파괴한 것에 분노하고 있습니다',
npcEliminated: 'NPC {npcName}이(가) 완전히 제거되었습니다',
npcEliminatedMessage: '당신은 {npcName}의 모든 행성을 파괴했습니다! 이 세력은 완전히 소멸되었습니다.'
}
},
pagination: {
previous: '이전',
next: '다음',
gotIt: '',
first: '처음',
last: '마지막',
page: '{page}페이지'
@@ -889,5 +947,39 @@ export default {
title: '페이지를 찾을 수 없습니다',
description: '죄송합니다. 찾으시는 페이지가 존재하지 않습니다',
goHome: '홈으로 이동'
},
time: {
days: '일',
hours: '시간',
minutes: '분',
seconds: '초'
},
tutorial: {
welcome: {
title: 'OGame에 오신 것을 환영합니다',
content: '환영합니다, 사령관! 기초부터 시작하여 우주 제국을 건설해 봅시다.'
},
buildSolarPlant: {
title: '태양광 발전소 건설',
content:
'먼저 태양광 발전소를 건설하세요! 행성에 에너지를 공급합니다. 에너지가 없으면 다른 자원 건물이 작동할 수 없습니다. 가장 중요한 첫 단계입니다.'
},
waitBuild: {
title: '건설 대기열',
content:
'건물이 건설 대기열에 추가되었습니다. 오른쪽 상단의 대기열 아이콘을 클릭하면 진행 중인 모든 건설 및 연구 작업을 확인할 수 있습니다. 건설에는 시간이 걸리지만 대기하는 동안 계속 작업할 수 있습니다.'
},
mobile: {
welcome: {
title: 'OGame에 오신 것을 환영합니다 (모바일)',
content:
'환영합니다, 사령관! 터치스크린용으로 설계된 간소화된 튜토리얼입니다. 제국 건설을 시작할 수 있도록 핵심 기능을 빠르게 소개하겠습니다.'
},
waitBuild: {
title: '건설 대기열',
content:
'오른쪽 상단의 대기열 아이콘을 클릭하여 건설 진행 상황을 확인하세요. 다른 페이지를 계속 탐색할 수 있으며, 건설은 백그라운드에서 진행됩니다.'
}
}
}
}

View File

@@ -8,6 +8,7 @@ export default {
close: 'Закрыть',
back: 'Назад',
next: 'Далее',
gotIt: '',
previous: 'Предыдущий',
submit: 'Отправить',
reset: 'Сбросить',
@@ -125,6 +126,7 @@ export default {
roboticsFactory: 'Фабрика роботов',
naniteFactory: 'Нанитная фабрика',
shipyard: 'Верфь',
hangar: 'Ангар',
researchLab: 'Исследовательская лаборатория',
metalStorage: 'Хранилище металла',
crystalStorage: 'Хранилище кристалла',
@@ -165,6 +167,7 @@ export default {
roboticsFactory: 'Ускоряет скорость строительства',
naniteFactory: 'Увеличивает вместимость очереди строительства, +1 за уровень (макс 10 уровней)',
shipyard: 'Строит корабли',
hangar: 'Специализированное сооружение для расширения вместимости флота, поддерживает специализацию планет',
researchLab: 'Исследует технологии',
metalStorage: 'Увеличивает ёмкость хранилища металла',
crystalStorage: 'Увеличивает ёмкость хранилища кристалла',
@@ -199,10 +202,10 @@ export default {
lightFighter: 'Базовая боевая единица',
heavyFighter: 'Тяжелобронированный истребитель',
cruiser: 'Средний боевой корабль, сбалансированная атака и защита',
battleship: 'Мощный боевой корабль',
battleship: 'Основной тяжёлый боевой корабль с мощной огневой мощью и высокой защитой',
battlecruiser: 'Быстрый мощный боевой корабль, отлично атакует линкоры',
bomber: 'Специализированный корабль для атаки оборонительных сооружений',
destroyer: 'Охотник, специализирующийся на уничтожении крупных кораблей',
destroyer: 'Специализированный противокапитальный корабль с высокой огневой мощью, но низкой защитой',
smallCargo: 'Транспортирует небольшое количество ресурсов',
largeCargo: 'Транспортирует большое количество ресурсов',
colonyShip: 'Используется для колонизации новых планет',
@@ -313,10 +316,13 @@ export default {
darkMatterSpecialist: 'Улучшает эффективность сбора тёмной материи'
},
queue: {
title: 'Очередь строительства',
empty: 'Нет активных задач',
buildQueue: 'Очередь строительства',
researchQueue: 'Очередь исследований',
building: 'Строится',
researching: 'Исследуется',
demolishing: 'Сносится',
remaining: 'Осталось',
cancel: 'Отменить',
cancelBuild: 'Отменить строительство',
@@ -588,10 +594,12 @@ export default {
battles: 'Битвы',
spy: 'Разведка',
npc: 'NPC',
diplomacy: '',
battleReports: 'Отчёты о боях',
spyReports: 'Отчёты разведки',
noBattleReports: 'Нет отчётов о боях',
noSpyReports: 'Нет отчётов разведки',
noDiplomaticReports: '',
battleReport: 'Отчёт о бое',
spyReport: 'Отчёт разведки',
victory: 'Победа',
@@ -647,7 +655,38 @@ export default {
hostile: 'Они враждебны и не принимают подарки',
neutral_distrust: 'Они вам не доверяют',
polite_decline: 'Вежливо отказались'
}
},
// Spied notification dialog
spiedNotificationDetails: '',
spyDetected: '',
detectionResult: '',
detectionSuccess: '',
spiedNotificationMessage: '',
spiedNotificationTip: '',
viewInGalaxy: '',
// Mission report dialog
missionReportDetails: '',
missionSuccess: '',
missionFailed: '',
origin: '',
destination: '',
missionDetails: '',
transportedResources: '',
recycledResources: '',
remainingDebris: '',
newPlanet: '',
// NPC activity dialog
npcActivityDetails: '',
activityType: {
recycle: ''
},
activityLocation: '',
position: '',
nearPlanet: '',
activityDescription: '',
npcActivityMessage: '',
arrivalTime: '',
npcActivityTip: ''
},
missionReports: {
transportSuccess: 'Миссия транспортировки успешно завершена',
@@ -801,7 +840,8 @@ export default {
npcWillSpyAndAttack: '{npcName} проведет разведку через 5с и атакует через 10с',
acceleratedMissions: 'Ускорено {count} миссий до 5 секунд',
npcFleetInitialized: 'Флот {npcName} инициализирован',
npcFleetDetails: '100 шпионских зондов\n500 легких истребителей\n300 тяжелых истребителей\n200 крейсеров\n100 линкоров\n50 бомбардировщиков\n30 эсминцев\n20 линейных крейсеров',
npcFleetDetails:
'100 шпионских зондов\n500 легких истребителей\n300 тяжелых истребителей\n200 крейсеров\n100 линкоров\n50 бомбардировщиков\n30 эсминцев\n20 линейных крейсеров',
dangerZone: 'Опасная зона',
dangerZoneDesc: 'Следующие операции необратимы',
resetGame: 'Сбросить игру',
@@ -839,6 +879,10 @@ export default {
recentEvents: 'Недавние события',
recentEventsDescription: 'Журнал последних дипломатических действий',
ago: 'назад',
notifications: '',
markAllRead: '',
noReports: '',
viewAll: '',
status: {
friendly: 'Дружественный',
neutral: 'Нейтральный',
@@ -854,6 +898,13 @@ export default {
viewPlanets: 'Посмотреть планеты'
},
lastEvent: 'Последнее событие',
reportDetails: '',
eventDescription: '',
reputationChange: '',
before: '',
after: '',
statusChange: '',
viewDiplomacy: '',
events: {
gift: 'Подарок отправлен',
attack: 'Атака',
@@ -882,12 +933,20 @@ export default {
receivedGiftFromNpc: 'Получен подарок от {npcName}',
acceptedGiftFromNpc: 'Вы приняли подарок от {npcName}: {metal}M {crystal}C {deuterium}D',
playerRejectedGift: 'Игрок отклонил подарок',
rejectedGiftFromNpc: 'Вы отклонили подарок от {npcName}. Репутация {reputation}'
rejectedGiftFromNpc: 'Вы отклонили подарок от {npcName}. Репутация {reputation}',
destroyedNpcPlanet: 'Уничтожена {planetName} игрока {npcName}',
playerDestroyedPlanet: 'Игрок уничтожил {planetName}',
youDestroyedNpcPlanet: 'Вы уничтожили {planetName} игрока {npcName}. Репутация {reputation}',
playerDestroyedAllyPlanet: 'Игрок уничтожил {planetName} союзника {allyName}',
allyOutraged: '{allyName} возмущен тем, что вы уничтожили {planetName} их союзника {targetName}',
npcEliminated: 'NPC {npcName} полностью уничтожен',
npcEliminatedMessage: 'Вы уничтожили все планеты {npcName}! Эта фракция полностью уничтожена.'
}
},
pagination: {
previous: 'Предыдущая',
next: 'Следующая',
gotIt: '',
first: 'Первая',
last: 'Последняя',
page: 'Страница {page}'
@@ -896,5 +955,39 @@ export default {
title: 'Страница не найдена',
description: 'Извините, страница, которую вы ищете, не существует',
goHome: 'На главную'
},
time: {
days: 'дней',
hours: 'часов',
minutes: 'минут',
seconds: 'секунд'
},
tutorial: {
welcome: {
title: 'Добро пожаловать в OGame',
content: 'Добро пожаловать, Командир! Давайте начнём с основ и построим вашу космическую империю.'
},
buildSolarPlant: {
title: 'Постройте солнечную электростанцию',
content:
'Сначала постройте солнечную электростанцию! Она обеспечивает энергией вашу планету. Без энергии другие ресурсные здания не могут функционировать. Это самый важный первый шаг.'
},
waitBuild: {
title: 'Очередь строительства',
content:
'Ваше здание теперь в очереди строительства. Нажмите на значок очереди в правом верхнем углу, чтобы увидеть все текущие задачи строительства и исследований. Строительство занимает время, но вы можете продолжать работать во время ожидания.'
},
mobile: {
welcome: {
title: 'Добро пожаловать в OGame (Мобильная версия)',
content:
'Добро пожаловать, Командир! Это упрощённое руководство, разработанное для сенсорных экранов. Мы быстро рассмотрим основные функции, чтобы вы могли начать строить свою империю.'
},
waitBuild: {
title: 'Очередь строительства',
content:
'Нажмите на значок очереди в правом верхнем углу, чтобы увидеть прогресс строительства. Вы можете продолжать просматривать другие страницы - строительство происходит в фоновом режиме.'
}
}
}
}

View File

@@ -126,6 +126,7 @@ export default {
roboticsFactory: '机器人工厂',
naniteFactory: '纳米工厂',
shipyard: '船坞',
hangar: '机库',
researchLab: '研究实验室',
metalStorage: '金属仓库',
crystalStorage: '晶体仓库',
@@ -164,6 +165,7 @@ export default {
roboticsFactory: '加快建造速度',
naniteFactory: '增加建造队列数量,每级+1队列最多10级',
shipyard: '建造舰船',
hangar: '专门用于扩展舰队存储容量,支持星球专业化发展',
researchLab: '研究科技',
metalStorage: '增加金属存储上限',
crystalStorage: '增加晶体存储上限',
@@ -198,10 +200,10 @@ export default {
lightFighter: '基础战斗单位',
heavyFighter: '重装战斗机',
cruiser: '中型战舰,攻守平衡',
battleship: '强力战舰',
battleship: '主力重型战舰,拥有强大的火力和防护',
battlecruiser: '快速强大的战斗舰船,擅长攻击战列舰',
bomber: '专门对付防御设施的轰炸舰',
destroyer: '擅长摧毁大型舰船的猎杀者',
destroyer: '专业反大型舰船战舰,高火力低防护',
smallCargo: '运输少量资源',
largeCargo: '运输大量资源',
colonyShip: '用于殖民新星球',
@@ -313,11 +315,14 @@ export default {
darkMatterSpecialist: '提升暗物质采集效率'
},
queue: {
title: '建造队列',
empty: '当前没有进行中的任务',
buildQueueBonus: '建造队列',
spaceBonus: '空间加成',
researchQueueBonus: '研究队列',
building: '建造中',
researching: '研究中',
demolishing: '拆除中',
remaining: '剩余时间',
cancel: '取消',
cancelBuild: '取消建造',
@@ -576,10 +581,12 @@ export default {
battles: '战斗',
spy: '侦查',
npc: 'NPC',
diplomacy: '',
battleReports: '战斗报告',
spyReports: '间谍报告',
noBattleReports: '暂无战斗报告',
noSpyReports: '暂无间谍报告',
noDiplomaticReports: '',
battleReport: '战斗报告',
spyReport: '间谍报告',
victory: '胜利',
@@ -635,7 +642,38 @@ export default {
hostile: '对方对你有敌意,不接受礼物',
neutral_distrust: '对方对你缺乏信任',
polite_decline: '对方礼貌地拒绝了'
}
},
// 被侦查通知对话框
spiedNotificationDetails: '被侦查通知详情',
spyDetected: '侦查被发现',
detectionResult: '检测结果',
detectionSuccess: '你的侦查探测被发现了!',
spiedNotificationMessage: '{npc}试图侦查你的星球{planet}',
spiedNotificationTip: '考虑增强防御或反击如果这个NPC对你有敌意',
viewInGalaxy: '在星系中查看',
// 任务报告对话框
missionReportDetails: '任务报告详情',
missionSuccess: '成功',
missionFailed: '失败',
origin: '起点',
destination: '终点',
missionDetails: '任务详情',
transportedResources: '运输资源',
recycledResources: '回收资源',
remainingDebris: '剩余残骸',
newPlanet: '新星球',
// NPC活动对话框
npcActivityDetails: 'NPC活动详情',
activityType: {
recycle: '回收残骸'
},
activityLocation: '活动位置',
position: '位置',
nearPlanet: '附近星球',
activityDescription: '活动描述',
npcActivityMessage: '{npc}正在{position}{activity}',
arrivalTime: '到达时间',
npcActivityTip: 'NPC可能会收集战斗产生的残骸。如果你想竞争资源可以尝试先到达该位置'
},
missionReports: {
transportSuccess: '运输任务成功完成',
@@ -801,7 +839,8 @@ export default {
completeAllQueues: '一键完成所有队列',
completeAllQueuesDesc: '立即完成所有建筑、科技、舰船、防御队列和飞行任务',
completeQueues: '完成队列',
completeQueuesSuccess: '已完成 {buildingCount} 个建筑队列、{researchCount} 个科技队列、{missionCount} 个飞行任务、{missileCount} 个导弹任务'
completeQueuesSuccess:
'已完成 {buildingCount} 个建筑队列、{researchCount} 个科技队列、{missionCount} 个飞行任务、{missileCount} 个导弹任务'
},
alerts: {
npcSpyIncoming: 'NPC侦查即将到达',
@@ -831,6 +870,10 @@ export default {
recentEvents: '最近事件',
recentEventsDescription: '最近的外交活动记录',
ago: '前',
notifications: '外交通知',
markAllRead: '全部已读',
noReports: '暂无外交事件',
viewAll: '查看全部',
status: {
friendly: '友好',
neutral: '中立',
@@ -846,6 +889,13 @@ export default {
viewPlanets: '查看星球'
},
lastEvent: '最近活动',
reportDetails: '外交报告详情',
eventDescription: '事件描述',
reputationChange: '好感度变化',
before: '之前',
after: '之后',
statusChange: '关系状态变化',
viewDiplomacy: '查看外交页面',
events: {
gift: '赠送资源',
attack: '攻击',
@@ -873,7 +923,14 @@ export default {
receivedGiftFromNpc: '收到了{npcName}的礼物',
acceptedGiftFromNpc: '你接受了{npcName}的礼物:{metal}金属 {crystal}晶体 {deuterium}氘',
playerRejectedGift: '玩家拒绝了礼物',
rejectedGiftFromNpc: '你拒绝了{npcName}的礼物。好感度{reputation}'
rejectedGiftFromNpc: '你拒绝了{npcName}的礼物。好感度{reputation}',
destroyedNpcPlanet: '摧毁了{npcName}的{planetName}',
playerDestroyedPlanet: '玩家摧毁了{planetName}',
youDestroyedNpcPlanet: '你摧毁了{npcName}的{planetName}。好感度{reputation}',
playerDestroyedAllyPlanet: '玩家摧毁了盟友{allyName}的{planetName}',
allyOutraged: '{allyName}对你摧毁盟友{targetName}的{planetName}感到愤怒',
npcEliminated: 'NPC {npcName}已被彻底消灭',
npcEliminatedMessage: '你消灭了{npcName}的所有星球!该势力已被彻底摧毁。'
}
},
pagination: {
@@ -887,5 +944,144 @@ export default {
title: '页面未找到',
description: '抱歉,您访问的页面不存在',
goHome: '返回首页'
},
time: {
days: '天',
hours: '小时',
minutes: '分钟',
seconds: '秒'
},
tutorial: {
progress: '进度',
previous: '上一步',
next: '下一步',
gotIt: '我知道了',
completeButton: '完成',
skip: '跳过引导',
welcome: {
title: '欢迎来到 OGame',
content: '欢迎,指挥官!本教程将引导您了解建立帝国的基础知识。点击"下一步"开始您的征程。'
},
resources: {
title: '资源概览',
content: '这些是您的资源:金属、晶体和重氢。它们是建造建筑和研究科技的必需品。能量也很重要,用于为您的基础设施供电。'
},
planet: {
title: '您的星球',
content: '这是您的母星。您可以在这里查看星球名称、坐标,并在扩张帝国时切换星球。'
},
navigation: {
title: '导航菜单',
content: '使用此菜单在不同部分之间导航:建筑、研究、舰队、星系等。每个部分都提供独特的游戏功能。'
},
gotoBuildings: {
title: '前往建筑页面',
content: '让我们从建造一些建筑开始。点击"建筑"菜单项查看可用建筑。'
},
buildSolarPlant: {
title: '建造太阳能电站',
content: '首先建造太阳能电站!它为您的星球提供能量。没有能量,其他资源建筑无法运作。这是最重要的第一步。'
},
waitBuild: {
title: '建造队列',
content:
'您的建筑现在在建造队列中。点击右上角的队列图标可以查看所有正在进行的建造和研究任务。建筑需要时间完成,但您可以在等待时继续操作。'
},
buildMetalMine: {
title: '建造金属矿',
content: '现在有了能量,可以建造金属矿了。金属矿是您的主要金属来源,金属几乎用于每个建筑和舰船。'
},
buildCrystalMine: {
title: '建造晶体矿',
content: '晶体更稀有但对高级科技至关重要。建造晶体矿开始收集这种宝贵的资源。'
},
buildDeuterium: {
title: '建造重氢合成器',
content: '重氢是舰船燃料和高级研究的必需品。建造重氢合成器开始生产这种关键资源。'
},
upgradeMines: {
title: '升级资源矿',
content: '接下来您需要升级三种资源矿金属、晶体、重氢到2级以满足建造机器人工厂的要求。资源充足后继续升级它们。'
},
buildRobotics: {
title: '建造机器人工厂',
content: '机器人工厂可以大幅加快建造速度。它需要金属矿、晶体矿和重氢合成器各达到2级。建造它来提升建造效率'
},
upgradeMinesForLab: {
title: '继续升级资源矿',
content: '现在需要将三种资源矿升级到3级以满足研究实验室的建造要求。继续发展您的资源产能。'
},
buildResearchLab: {
title: '建造研究实验室',
content: '研究实验室是技术进步的基础。它需要三种资源矿各达到3级。建造它以解锁科技研究'
},
gotoResearch: {
title: '前往研究页面',
content: '既然您有了研究实验室,点击"研究"菜单查看可用的科技。'
},
researchEnergy: {
title: '研究能量科技',
content: '能量科技可以提高您的能量产出并解锁高级建筑。这是最基础也是最重要的科技之一。'
},
shipyardIntro: {
title: '舰队与船坞',
content: '舰船让您能够探索星系、运输资源并保卫您的帝国。要建造舰船您需要船坞需要机器人工厂2级。'
},
gotoBuildingsForShipyard: {
title: '返回建筑页面',
content: '返回建筑页面来建造您的船坞。'
},
buildShipyard: {
title: '建造船坞',
content: '船坞允许您建造舰船和防御系统。这对舰队行动至关重要。'
},
fleetIntro: {
title: '舰队行动',
content: '一旦您拥有舰船,就可以派遣它们执行任务:运输资源、殖民星球、攻击敌人或探索废墟场。'
},
galaxyIntro: {
title: '探索星系',
content: '星系视图显示其他星球、废墟场和扩张机会。使用它来侦察目标并规划您的战略。'
},
complete: {
title: '教程完成!',
content:
'恭喜,指挥官!您现在了解了基础知识。继续建设您的帝国,研究科技,探索星系。记住:先发展能量,再建资源,然后是工厂和研究!祝您好运!'
},
// 移动端教程
mobile: {
welcome: {
title: '欢迎来到 OGame移动版',
content: '欢迎,指挥官!这是专为触摸屏设计的简化教程。我们将快速介绍核心功能,让您开始建设帝国。'
},
resources: {
title: '顶部资源栏',
content: '顶部显示您的资源:金属、晶体和重氢。点击可查看详细生产信息。'
},
menu: {
title: '打开导航菜单',
content: '点击这个菜单图标打开导航栏,您可以访问建筑、研究、舰队等所有功能。'
},
gotoBuildings: {
title: '前往建筑页面',
content: '菜单已打开!现在点击"建筑"选项,开始建造基础设施。'
},
buildSolarPlant: {
title: '建造太阳能电站',
content: '首先建造太阳能电站!向下滚动找到它,点击卡片进行建造。能量是一切的基础。'
},
waitBuild: {
title: '建造队列',
content: '点击右上角的队列图标可以查看建造进度。您可以继续浏览其他页面,建造会在后台进行。'
},
buildMetalMine: {
title: '建造金属矿',
content: '有了能量后,建造金属矿。向下滚动找到金属矿,点击建造。'
},
complete: {
title: '快速教程完成!',
content: '很好!您已经掌握了基础操作。继续建造晶体矿和重氢合成器,然后探索其他功能。记住:先能量,再资源!'
}
}
}
}

View File

@@ -8,6 +8,7 @@ export default {
close: '關閉',
back: '返回',
next: '下一步',
gotIt: '',
previous: '上一步',
submit: '提交',
reset: '重置',
@@ -125,6 +126,7 @@ export default {
roboticsFactory: '機器人工廠',
naniteFactory: '納米工廠',
shipyard: '船塢',
hangar: '機庫',
researchLab: '研究實驗室',
metalStorage: '金屬倉庫',
crystalStorage: '晶體倉庫',
@@ -165,6 +167,7 @@ export default {
roboticsFactory: '加快建造速度',
naniteFactory: '增加建造佇列數量,每級+1佇列最多10級',
shipyard: '建造艦船',
hangar: '專門用於擴展艦隊儲存容量,支援行星專業化發展',
researchLab: '研究科技',
metalStorage: '增加金屬儲存上限',
crystalStorage: '增加晶體儲存上限',
@@ -199,10 +202,10 @@ export default {
lightFighter: '基礎戰鬥單位',
heavyFighter: '重裝戰鬥機',
cruiser: '中型戰艦,攻守平衡',
battleship: '強力戰艦',
battleship: '主力重型戰艦,擁有強大的火力和防護',
battlecruiser: '快速強大的戰鬥艦船,擅長攻擊戰列艦',
bomber: '專門對付防禦設施的轟炸艦',
destroyer: '擅長摧毀大型艦船的獵殺者',
destroyer: '專業反大型艦船戰艦,高火力低防護',
smallCargo: '運輸少量資源',
largeCargo: '運輸大量資源',
colonyShip: '用於殖民新星球',
@@ -314,10 +317,13 @@ export default {
darkMatterSpecialist: '提升暗物質採集效率'
},
queue: {
title: '建造佇列',
empty: '當前沒有進行中的任務',
buildQueue: '建造佇列',
researchQueue: '研究佇列',
building: '建造中',
researching: '研究中',
demolishing: '拆除中',
remaining: '剩餘時間',
cancel: '取消',
cancelBuild: '取消建造',
@@ -582,10 +588,12 @@ export default {
battles: '戰鬥',
spy: '偵查',
npc: 'NPC',
diplomacy: '',
battleReports: '戰鬥報告',
spyReports: '間諜報告',
noBattleReports: '暫無戰鬥報告',
noSpyReports: '暫無間諜報告',
noDiplomaticReports: '',
battleReport: '戰鬥報告',
spyReport: '間諜報告',
victory: '勝利',
@@ -641,7 +649,38 @@ export default {
hostile: '對方對你有敵意,不接受禮物',
neutral_distrust: '對方對你缺乏信任',
polite_decline: '對方禮貌地拒絕了'
}
},
// Spied notification dialog
spiedNotificationDetails: '',
spyDetected: '',
detectionResult: '',
detectionSuccess: '',
spiedNotificationMessage: '',
spiedNotificationTip: '',
viewInGalaxy: '',
// Mission report dialog
missionReportDetails: '',
missionSuccess: '',
missionFailed: '',
origin: '',
destination: '',
missionDetails: '',
transportedResources: '',
recycledResources: '',
remainingDebris: '',
newPlanet: '',
// NPC activity dialog
npcActivityDetails: '',
activityType: {
recycle: ''
},
activityLocation: '',
position: '',
nearPlanet: '',
activityDescription: '',
npcActivityMessage: '',
arrivalTime: '',
npcActivityTip: ''
},
missionReports: {
transportSuccess: '運輸任務成功完成',
@@ -833,6 +872,10 @@ export default {
recentEvents: '最近事件',
recentEventsDescription: '最近的外交活動記錄',
ago: '前',
notifications: '',
markAllRead: '',
noReports: '',
viewAll: '',
status: {
friendly: '友好',
neutral: '中立',
@@ -848,6 +891,13 @@ export default {
viewPlanets: '查看星球'
},
lastEvent: '最近事件',
reportDetails: '外交報告詳情',
eventDescription: '事件描述',
reputationChange: '好感度變化',
before: '之前',
after: '之後',
statusChange: '關係狀態變化',
viewDiplomacy: '查看外交頁面',
events: {
gift: '已贈送禮物',
attack: '攻擊',
@@ -876,12 +926,20 @@ export default {
receivedGiftFromNpc: '收到了{npcName}的禮物',
acceptedGiftFromNpc: '你接受了{npcName}的禮物:{metal}金屬 {crystal}晶體 {deuterium}氘',
playerRejectedGift: '玩家拒絕了禮物',
rejectedGiftFromNpc: '你拒絕了{npcName}的禮物。好感度{reputation}'
rejectedGiftFromNpc: '你拒絕了{npcName}的禮物。好感度{reputation}',
destroyedNpcPlanet: '摧毀了{npcName}的{planetName}',
playerDestroyedPlanet: '玩家摧毀了{planetName}',
youDestroyedNpcPlanet: '你摧毀了{npcName}的{planetName}。好感度{reputation}',
playerDestroyedAllyPlanet: '玩家摧毀了盟友{allyName}的{planetName}',
allyOutraged: '{allyName}對你摧毀盟友{targetName}的{planetName}感到憤怒',
npcEliminated: 'NPC {npcName}已被徹底消滅',
npcEliminatedMessage: '你消滅了{npcName}的所有星球!該勢力已被徹底摧毀。'
}
},
pagination: {
previous: '上一頁',
next: '下一頁',
gotIt: '',
first: '首頁',
last: '末頁',
page: '第 {page} 頁'
@@ -890,5 +948,36 @@ export default {
title: '找不到頁面',
description: '抱歉,您訪問的頁面不存在',
goHome: '返回首頁'
},
time: {
days: '天',
hours: '小時',
minutes: '分鐘',
seconds: '秒'
},
tutorial: {
welcome: {
title: '歡迎來到 OGame',
content: '歡迎,指揮官!讓我們從基礎開始,建立您的宇宙帝國。'
},
buildSolarPlant: {
title: '建造太陽能電站',
content: '首先建造太陽能電站!它為您的星球提供能量。沒有能量,其他資源建築無法運作。這是最重要的第一步。'
},
waitBuild: {
title: '建造佇列',
content:
'您的建築現在在建造佇列中。點擊右上角的佇列圖示可以查看所有正在進行的建造和研究任務。建築需要時間完成,但您可以在等待時繼續操作。'
},
mobile: {
welcome: {
title: '歡迎來到 OGame移動版',
content: '歡迎,指揮官!這是專為觸控螢幕設計的簡化教程。我們將快速介紹核心功能,讓您開始建設帝國。'
},
waitBuild: {
title: '建造佇列',
content: '點擊右上角的佇列圖示可以查看建造進度。您可以繼續瀏覽其他頁面,建造會在背景進行。'
}
}
}
}

View File

@@ -61,10 +61,7 @@ export const simulateBattle = async (
// 计算月球生成概率(根据残骸场总量)
const totalDebris = debrisField.metal + debrisField.crystal
const moonChance = Math.min(
(MOON_CONFIG.baseChance + Math.floor(totalDebris / MOON_CONFIG.chancePerDebris)),
MOON_CONFIG.maxChance
) / 100 // 转换为0-1的概率
const moonChance = Math.min(MOON_CONFIG.baseChance + Math.floor(totalDebris / MOON_CONFIG.chancePerDebris), MOON_CONFIG.maxChance) / 100 // 转换为0-1的概率
// 生成战斗报告
const battleResult: BattleResult = {

View File

@@ -14,6 +14,7 @@ import type {
Resources,
Player,
NPC,
Planet,
FleetMission,
BattleResult,
Position,
@@ -528,20 +529,173 @@ export const handleDebrisRecycleReputation = (player: Player, debrisPosition: Po
}
}
/**
* 处理星球摧毁事件的好感度变化
* 摧毁星球是最严重的行为,直接导致敌对关系
* @param attacker 攻击者(玩家)
* @param destroyedPlanet 被摧毁的星球
* @param planetOwner 星球所有者NPC
* @param allNpcs 所有NPC列表
* @param locale 语言代码
*/
export const handlePlanetDestructionReputation = (
attacker: Player,
destroyedPlanet: Planet,
planetOwner: NPC,
allNpcs: NPC[],
locale: Locale
): void => {
const { HOSTILE_THRESHOLD } = DIPLOMATIC_CONFIG
const now = Date.now()
// 更新玩家对被摧毁星球所有者的关系 - 直接设为敌对
if (!attacker.diplomaticRelations) {
attacker.diplomaticRelations = {}
}
const relation = getOrCreateRelation(attacker.diplomaticRelations, attacker.id, planetOwner.id)
const eventDescription = t('diplomacy.reports.destroyedNpcPlanet', locale, {
npcName: planetOwner.name,
planetName: destroyedPlanet.name
})
attacker.diplomaticRelations[planetOwner.id] = {
...relation,
reputation: HOSTILE_THRESHOLD, // 直接设为敌对阈值
status: RS.Hostile,
lastUpdated: now,
history: [
...(relation.history || []),
{
timestamp: now,
change: HOSTILE_THRESHOLD - relation.reputation,
reason: DET.DestroyPlanet,
details: eventDescription
}
]
}
// 更新星球所有者对玩家的关系 - 直接设为敌对
if (!planetOwner.relations) {
planetOwner.relations = {}
}
const ownerRelation = getOrCreateRelation(planetOwner.relations, planetOwner.id, attacker.id)
const ownerEventDescription = t('diplomacy.reports.playerDestroyedPlanet', locale, {
planetName: destroyedPlanet.name
})
planetOwner.relations[attacker.id] = {
...ownerRelation,
reputation: HOSTILE_THRESHOLD, // 直接设为敌对阈值
status: RS.Hostile,
lastUpdated: now,
history: [
...(ownerRelation.history || []),
{
timestamp: now,
change: HOSTILE_THRESHOLD - ownerRelation.reputation,
reason: DET.DestroyPlanet,
details: ownerEventDescription
}
]
}
// 生成外交报告
generateDiplomaticReport(
attacker,
planetOwner,
DET.DestroyPlanet,
HOSTILE_THRESHOLD,
t('diplomacy.reports.youDestroyedNpcPlanet', locale, {
npcName: planetOwner.name,
planetName: destroyedPlanet.name,
reputation: HOSTILE_THRESHOLD
}),
'diplomacy.reports.youDestroyedNpcPlanet',
{ npcName: planetOwner.name, planetName: destroyedPlanet.name, reputation: HOSTILE_THRESHOLD }
)
// 检查盟友关系网络 - 摧毁星球对盟友的影响更严重
if (planetOwner.allies && planetOwner.allies.length > 0) {
handleAllyPlanetDestroyedReputation(attacker, planetOwner, destroyedPlanet, allNpcs, locale)
}
}
/**
* 处理盟友星球被摧毁的好感度变化
* @param attacker 攻击者(玩家)
* @param attackedNpc 星球被摧毁的NPC
* @param destroyedPlanet 被摧毁的星球
* @param allNpcs 所有NPC列表
* @param locale 语言代码
*/
export const handleAllyPlanetDestroyedReputation = (
attacker: Player,
attackedNpc: NPC,
destroyedPlanet: Planet,
allNpcs: NPC[],
locale: Locale
): 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)
const reputationLoss = REPUTATION_CHANGES.ALLY_ATTACKED * 2 // 双倍惩罚
ally.relations[attacker.id] = updateReputation(
allyRelation,
reputationLoss,
DET.DestroyPlanet,
t('diplomacy.reports.playerDestroyedAllyPlanet', locale, { allyName: attackedNpc.name, planetName: destroyedPlanet.name })
)
// 生成外交报告
generateDiplomaticReport(
attacker,
ally,
DET.DestroyPlanet,
reputationLoss,
t('diplomacy.reports.allyOutraged', locale, {
allyName: ally.name,
targetName: attackedNpc.name,
planetName: destroyedPlanet.name
}),
'diplomacy.reports.allyOutraged',
{
allyName: ally.name,
targetName: attackedNpc.name,
planetName: destroyedPlanet.name
}
)
})
}
/**
* 生成外交报告
* @param player 玩家
* @param npc NPC
* @param eventType 事件类型
* @param reputationChange 好感度变化值
* @param message 消息内容
* @param message 消息内容(已弃用,用于向后兼容)
* @param messageKey 翻译键(可选)
* @param messageParams 翻译参数(可选)
*/
const generateDiplomaticReport = (
player: Player,
npc: NPC,
eventType: DiplomaticEventType,
reputationChange: number,
message: string
message: string,
messageKey?: string,
messageParams?: Record<string, string | number>
): void => {
if (!player.diplomaticReports) {
player.diplomaticReports = []
@@ -570,6 +724,8 @@ const generateDiplomaticReport = (
oldStatus,
newStatus,
message,
messageKey,
messageParams,
read: false
}
@@ -724,3 +880,78 @@ export const rejectNPCGift = (player: Player, npc: NPC, giftNotification: GiftNo
player.giftNotifications = player.giftNotifications.filter(n => n.id !== giftNotification.id)
}
}
/**
* 处理NPC被彻底消灭所有星球被摧毁
* @param eliminatedNpc 被消灭的NPC
* @param player 玩家
* @param allNpcs 所有NPC列表
* @param locale 语言代码
*/
export const handleNPCElimination = (eliminatedNpc: NPC, player: Player, allNpcs: NPC[], locale: Locale): void => {
const { HOSTILE_THRESHOLD } = DIPLOMATIC_CONFIG
// 1. 将玩家对该NPC的关系设为最低敌对状态
if (!player.diplomaticRelations) {
player.diplomaticRelations = {}
}
const relation = getOrCreateRelation(player.diplomaticRelations, player.id, eliminatedNpc.id)
const now = Date.now()
player.diplomaticRelations[eliminatedNpc.id] = {
...relation,
reputation: HOSTILE_THRESHOLD, // 设为敌对阈值
status: RS.Hostile,
lastUpdated: now,
history: [
...(relation.history || []),
{
timestamp: now,
change: HOSTILE_THRESHOLD - relation.reputation,
reason: DET.DestroyPlanet,
details: t('diplomacy.reports.npcEliminated', locale, { npcName: eliminatedNpc.name })
}
]
}
// 2. 生成外交报告
generateDiplomaticReport(
player,
eliminatedNpc,
DET.DestroyPlanet,
HOSTILE_THRESHOLD,
t('diplomacy.reports.npcEliminatedMessage', locale, { npcName: eliminatedNpc.name }),
'diplomacy.reports.npcEliminatedMessage',
{ npcName: eliminatedNpc.name }
)
// 3. 从所有其他NPC的盟友列表中移除被消灭的NPC
allNpcs.forEach(npc => {
if (npc.id !== eliminatedNpc.id && npc.allies && npc.allies.includes(eliminatedNpc.id)) {
npc.allies = npc.allies.filter(allyId => allyId !== eliminatedNpc.id)
}
})
}
/**
* 检查并处理被消灭的NPC所有星球都被摧毁的NPC
* @param allNpcs 所有NPC列表
* @param player 玩家
* @param locale 语言代码
* @returns 被消灭的NPC ID列表
*/
export const checkAndHandleEliminatedNPCs = (allNpcs: NPC[], player: Player, locale: Locale): string[] => {
const eliminatedNpcIds: string[] = []
allNpcs.forEach(npc => {
// 检查NPC是否还有星球
if (!npc.planets || npc.planets.length === 0) {
// NPC被彻底消灭
handleNPCElimination(npc, player, allNpcs, locale)
eliminatedNpcIds.push(npc.id)
}
})
return eliminatedNpcIds
}

View File

@@ -36,9 +36,10 @@ export const calculateDistance = (
/**
* 计算飞行时间
* 平衡后的时间倍率,确保游戏节奏合理
*/
export const calculateFlightTime = (distance: number, minSpeed: number): number => {
return Math.max(10, Math.floor((distance * 10000) / minSpeed)) // 至少10秒
return Math.max(10, Math.floor((distance * 50) / minSpeed)) // 至少10秒
}
/**
@@ -717,7 +718,8 @@ export const updateFleetMissions = async (
attacker: Player,
defender: Player | null,
now: number,
allNpcs?: NPC[]
allNpcs?: NPC[],
locale?: Locale
): Promise<{
completedMissions: string[]
battleReports: BattleResult[]
@@ -767,9 +769,26 @@ export const updateFleetMissions = async (
planets.set(moonKey, attackResult.moon)
}
if (attackResult.debrisField) {
// 检查该位置是否已存在残骸场
const existingDebris = debrisFields.get(attackResult.debrisField.id)
if (existingDebris) {
// 累加残骸资源
const updatedDebris: DebrisField = {
...existingDebris,
resources: {
metal: existingDebris.resources.metal + attackResult.debrisField.resources.metal,
crystal: existingDebris.resources.crystal + attackResult.debrisField.resources.crystal
}
}
debrisFields.set(attackResult.debrisField.id, updatedDebris)
updatedDebrisFields.push(updatedDebris)
} else {
// 新建残骸场
debrisFields.set(attackResult.debrisField.id, attackResult.debrisField)
newDebrisFields.push(attackResult.debrisField)
}
}
}
break
}
@@ -821,6 +840,15 @@ export const updateFleetMissions = async (
if (destroyResult && destroyResult.success && destroyResult.planetId) {
// 星球被摧毁
destroyedPlanetIds.push(destroyResult.planetId)
// 处理外交关系如果目标是NPC星球
if (targetPlanet && targetPlanet.ownerId && allNpcs && locale) {
const planetOwner = allNpcs.find(npc => npc.id === targetPlanet.ownerId)
if (planetOwner) {
diplomaticLogic.handlePlanetDestructionReputation(attacker, targetPlanet, planetOwner, allNpcs, locale)
}
}
planets.delete(targetKey)
}
break

View File

@@ -39,7 +39,12 @@ export const calculateMaxFleetStorage = (planet: Planet, technologies: Record<Te
const shipyardBonus = BUILDINGS[BuildingType.Shipyard].fleetStorageBonus || 0
maxStorage += shipyardLevel * shipyardBonus
// 3. 计算机技术全局加成
// 3. 机库建筑加成(每个星球独立)
const hangarLevel = planet.buildings[BuildingType.Hangar] || 0
const hangarBonus = BUILDINGS[BuildingType.Hangar].fleetStorageBonus || 0
maxStorage += hangarLevel * hangarBonus
// 4. 计算机技术全局加成
const computerTechLevel = technologies[TechnologyType.ComputerTechnology] || 0
const computerTechBonus = TECHNOLOGIES[TechnologyType.ComputerTechnology].fleetStorageBonus || 0
maxStorage += computerTechLevel * computerTechBonus
@@ -82,5 +87,9 @@ export const getMaxBuildableShips = (planet: Planet, shipType: ShipType, technol
const shipStorageUsage = SHIPS[shipType].storageUsage
if (shipStorageUsage === 0) return Number.MAX_SAFE_INTEGER
// 如果当前已经超限(舰队返回等情况),则不允许建造新舰船
if (availableStorage <= 0) return 0
return Math.floor(availableStorage / shipStorageUsage)
}

View File

@@ -100,7 +100,8 @@ export const generatePositionKey = (galaxy: number, system: number, position: nu
*/
export const processGameUpdate = (
player: Player,
now: number
now: number,
gameSpeed: number = 1
): {
updatedResearchQueue: BuildQueueItem[]
} => {
@@ -114,7 +115,7 @@ export const processGameUpdate = (
// 更新所有星球资源(直接同步计算,避免 Worker 通信开销)
player.planets.forEach(planet => {
resourceLogic.updatePlanetResources(planet, now, bonuses)
resourceLogic.updatePlanetResources(planet, now, bonuses, gameSpeed)
})
// 更新所有星球其他状态

View File

@@ -31,11 +31,7 @@ export const calculateSystemDistance = (from: Position, to: Position): number =>
/**
* 检查目标是否在射程内
*/
export const isTargetInRange = (
originPosition: Position,
targetPosition: Position,
impulseDriveLevel: number
): boolean => {
export const isTargetInRange = (originPosition: Position, targetPosition: Position, impulseDriveLevel: number): boolean => {
const range = calculateMissileRange(impulseDriveLevel)
const distance = calculateSystemDistance(originPosition, targetPosition)
return distance <= range
@@ -156,11 +152,7 @@ export const calculateMissileImpact = (
const defenseTypes = Object.keys(defenderPlanet.defense) as DefenseType[]
const availableDefenses = defenseTypes.filter(type => {
// 不能摧毁护盾罩和行星护盾
if (
type === DefenseTypes.SmallShieldDome ||
type === DefenseTypes.LargeShieldDome ||
type === DefenseTypes.PlanetaryShield
) {
if (type === DefenseTypes.SmallShieldDome || type === DefenseTypes.LargeShieldDome || type === DefenseTypes.PlanetaryShield) {
return false
}
return (defenderPlanet.defense[type] || 0) > 0
@@ -196,10 +188,7 @@ export const calculateMissileImpact = (
/**
* 应用导弹攻击结果到星球
*/
export const applyMissileAttackResult = (
planet: Planet,
defenseLosses: Partial<Record<DefenseType, number>>
): void => {
export const applyMissileAttackResult = (planet: Planet, defenseLosses: Partial<Record<DefenseType, number>>): void => {
for (const [defenseType, lossCount] of Object.entries(defenseLosses)) {
const currentCount = planet.defense[defenseType as DefenseType] || 0
planet.defense[defenseType as DefenseType] = Math.max(0, currentCount - lossCount)

View File

@@ -61,14 +61,14 @@ export const calculateDynamicDifficulty = (playerPoints: number): DynamicDifficu
// 积分区间和对应的难度参数
if (playerPoints < 1000) {
// 新手期0-1,000分
// NPC保持30-50%实力,给予充分发展空间
// 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
resourceGrowthRate: 0.8, // 从0.4提升到0.8确保NPC有足够资源发育
buildingGrowthSpeed: 0.6, // 从0.4提升到0.6
techGrowthSpeed: 0.6 // 从0.4提升到0.6
}
} else if (playerPoints < 5000) {
// 初级期1,000-5,000分
@@ -77,9 +77,9 @@ export const calculateDynamicDifficulty = (playerPoints: number): DynamicDifficu
return {
powerRatio: ratio,
checkInterval: 240, // 4分钟
resourceGrowthRate: 0.6,
buildingGrowthSpeed: 0.6,
techGrowthSpeed: 0.6
resourceGrowthRate: 1.0, // 从0.6提升到1.0,与玩家资源产出相当
buildingGrowthSpeed: 0.8, // 从0.6提升到0.8
techGrowthSpeed: 0.8 // 从0.6提升到0.8
}
} else if (playerPoints < 20000) {
// 中级期5,000-20,000分
@@ -559,9 +559,7 @@ export const initializeNPCDiplomacy = (npcs: NPC[]): void => {
// 为每个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)
)
const potentialAllies = npcs.filter(n => n.id !== npc.id && !npc.allies!.includes(n.id) && !n.allies!.includes(npc.id))
if (potentialAllies.length === 0) return

View File

@@ -22,9 +22,7 @@ export const validateTechnologyResearch = (
const cost = researchLogic.calculateTechnologyCost(techType, targetLevel)
// 检查队列中是否已存在该科技的研究任务
const existingQueueItem = researchQueue.find(
item => item.type === 'technology' && item.itemType === techType
)
const existingQueueItem = researchQueue.find(item => item.type === 'technology' && item.itemType === techType)
if (existingQueueItem) {
return { valid: false, reason: 'errors.technologyAlreadyInQueue' }
}

View File

@@ -115,23 +115,27 @@ export const updatePlanetResources = (
darkMatterProductionBonus: number
energyProductionBonus: number
storageCapacityBonus: number
}
},
gameSpeed: number = 1
): void => {
const timeDiff = (now - planet.lastUpdate) / 1000 // 转换为秒
// 应用游戏速度到时间差(游戏速度影响资源产出速率)
const effectiveTimeDiff = timeDiff * gameSpeed
// 计算能量消耗(每小时)
const energyConsumption = calculateEnergyConsumption(planet)
// 先增加能量产出
const energyProduction = calculateEnergyProduction(planet, { energyProductionBonus: bonuses.energyProductionBonus })
planet.resources.energy += (energyProduction * timeDiff) / 3600
planet.resources.energy += (energyProduction * effectiveTimeDiff) / 3600
// 限制能量上限
const capacity = calculateResourceCapacity(planet, bonuses.storageCapacityBonus)
planet.resources.energy = Math.min(planet.resources.energy, capacity.energy)
// 扣除能量消耗
planet.resources.energy -= (energyConsumption * timeDiff) / 3600
planet.resources.energy -= (energyConsumption * effectiveTimeDiff) / 3600
// 能量不能为负数最低为0
planet.resources.energy = Math.max(0, planet.resources.energy)
@@ -143,11 +147,11 @@ export const updatePlanetResources = (
energyProductionBonus: bonuses.energyProductionBonus
})
// 更新资源(转换为每秒产量)
planet.resources.metal += (production.metal * timeDiff) / 3600
planet.resources.crystal += (production.crystal * timeDiff) / 3600
planet.resources.deuterium += (production.deuterium * timeDiff) / 3600
planet.resources.darkMatter += (production.darkMatter * timeDiff) / 3600
// 更新资源(转换为每秒产量,应用游戏速度
planet.resources.metal += (production.metal * effectiveTimeDiff) / 3600
planet.resources.crystal += (production.crystal * effectiveTimeDiff) / 3600
planet.resources.deuterium += (production.deuterium * effectiveTimeDiff) / 3600
planet.resources.darkMatter += (production.darkMatter * effectiveTimeDiff) / 3600
// 限制资源上限
planet.resources.metal = Math.min(planet.resources.metal, capacity.metal)

View File

@@ -1,5 +1,17 @@
import { defineStore } from 'pinia'
import type { Planet, Player, BuildQueueItem, FleetMission, BattleResult, SpyReport, Officer, SpiedNotification, NPCActivityNotification, IncomingFleetAlert, MissileAttack } from '@/types/game'
import type {
Planet,
Player,
BuildQueueItem,
FleetMission,
BattleResult,
SpyReport,
Officer,
SpiedNotification,
NPCActivityNotification,
IncomingFleetAlert,
MissileAttack
} from '@/types/game'
import { TechnologyType, OfficerType } from '@/types/game'
import type { Locale } from '@/locales'
import pkg from '../../package.json'

View File

@@ -24,6 +24,7 @@ export const BuildingType = {
RoboticsFactory: 'roboticsFactory',
NaniteFactory: 'naniteFactory', // 纳米工厂
Shipyard: 'shipyard',
Hangar: 'hangar', // 机库
ResearchLab: 'researchLab',
MetalStorage: 'metalStorage',
CrystalStorage: 'crystalStorage',
@@ -227,7 +228,8 @@ export const DiplomaticEventType = {
Attack: 'attack', // 攻击
Spy: 'spy', // 侦查
StealDebris: 'stealDebris', // 抢夺残骸
AllyAttacked: 'allyAttacked' // 盟友被攻击
AllyAttacked: 'allyAttacked', // 盟友被攻击
DestroyPlanet: 'destroyPlanet' // 摧毁星球
} as const
export type DiplomaticEventType = (typeof DiplomaticEventType)[keyof typeof DiplomaticEventType]
@@ -259,7 +261,9 @@ export interface DiplomaticReport {
newReputation: number // 新的好感度值
oldStatus: RelationStatus // 旧的关系状态
newStatus: RelationStatus // 新的关系状态
message: string // 消息内容
message: string // 消息内容(已弃用,保留用于兼容性)
messageKey?: string // 翻译键(如 'diplomacy.reports.youDestroyedNpcPlanet'
messageParams?: Record<string, string | number> // 翻译参数(如 { npcName: 'NPC-1', planetName: '星球 1:1:8', reputation: -80 }
read?: boolean // 已读状态
}
@@ -568,6 +572,8 @@ export interface Player {
// 外交系统字段
diplomaticRelations?: Record<string, DiplomaticRelation> // 玩家对NPC的关系key: npcId
diplomaticReports?: DiplomaticReport[] // 外交变化报告
// 新手引导字段
tutorialProgress?: TutorialProgress // 新手引导进度
}
// 游戏状态
@@ -616,3 +622,33 @@ export interface NPC {
allies?: string[] // 盟友列表NPC ID
enemies?: string[] // 敌人列表NPC ID
}
// 新手引导系统
export interface TutorialStep {
id: string
title: string // 标题
content: string // 内容描述
target?: string // 目标元素的选择器或ID
placement?: 'top' | 'bottom' | 'left' | 'right' | 'center' // 提示框位置
route?: string // 需要跳转的路由
action?: 'click' | 'build' | 'research' | 'none' // 需要完成的操作类型
actionTarget?: string // 操作目标建筑ID、科技ID等
completionCheck?: () => boolean // 完成条件检查函数(运行时注入)
canSkip?: boolean // 是否可跳过此步骤
highlightPadding?: number // 高亮区域的padding
}
export interface TutorialState {
isActive: boolean // 引导是否激活
currentStepIndex: number // 当前步骤索引
completedSteps: string[] // 已完成的步骤ID列表
skipped: boolean // 是否已跳过整个引导
lastActiveTime?: number // 最后活跃时间
}
export interface TutorialProgress {
tutorialCompleted: boolean // 是否完成了整个引导
completedStepIds: string[] // 已完成的步骤ID
currentStep: string | null // 当前步骤ID
skippedAt?: number // 跳过的时间戳
}

View File

@@ -1,4 +1,3 @@
/**
* 格式化数字为英文单位K, M, B, T, Q
* @param num 数字
@@ -32,6 +31,30 @@ export const getResourceColor = (current: number, max: number): string => {
if (ratio >= 0.7) return 'text-yellow-600 dark:text-yellow-400'
return ''
}
/**
* 格式化相对时间(用于显示"多久之前"
* @param seconds 秒数
* @param t 翻译函数
* @returns 格式化后的相对时间字符串
*/
export const formatRelativeTime = (seconds: number, t: (key: string) => string): string => {
const days = Math.floor(seconds / 86400)
const hours = Math.floor((seconds % 86400) / 3600)
const minutes = Math.floor((seconds % 3600) / 60)
const secs = Math.floor(seconds % 60)
if (days > 0) {
return `${days}${t('time.days')}${hours}${t('time.hours')}`
}
if (hours > 0) {
return `${hours}${t('time.hours')}${minutes}${t('time.minutes')}`
}
if (minutes > 0) {
return `${minutes}${t('time.minutes')}`
}
return `${secs}${t('time.seconds')}`
}
/**
* 格式化时间(秒转为 年:天:时:分:秒)
* @param seconds 秒数

View File

@@ -29,7 +29,7 @@
</div>
<div class="grid grid-cols-2 lg:grid-cols-3 gap-3 sm:gap-4">
<Card v-for="buildingType in availableBuildings" :key="buildingType" class="relative">
<Card v-for="buildingType in availableBuildings" :key="buildingType" :data-building="buildingType" class="relative">
<!-- 前置条件遮罩 -->
<CardUnlockOverlay :requirements="BUILDINGS[buildingType].requirements" :currentLevel="getBuildingLevel(buildingType)" />
@@ -225,18 +225,18 @@
})
})
const upgradeBuilding = (buildingType: BuildingType): boolean => {
if (!gameStore.currentPlanet) return false
const upgradeBuilding = (buildingType: BuildingType): { success: boolean; reason?: string } => {
if (!gameStore.currentPlanet) return { success: false }
const validation = buildingValidation.validateBuildingUpgrade(
gameStore.currentPlanet,
buildingType,
gameStore.player.technologies,
gameStore.player.officers
)
if (!validation.valid) return false
if (!validation.valid) return { success: false, reason: validation.reason }
const queueItem = buildingValidation.executeBuildingUpgrade(gameStore.currentPlanet, buildingType, gameStore.player.officers)
gameStore.currentPlanet.buildQueue.push(queueItem)
return true
return { success: true }
}
const getUsedSpace = (planet: Planet): number => {
@@ -253,10 +253,10 @@
return
}
const success = upgradeBuilding(buildingType)
if (!success) {
const result = upgradeBuilding(buildingType)
if (!result.success) {
alertDialogTitle.value = t('buildingsView.upgradeFailed')
alertDialogMessage.value = t('buildingsView.upgradeFailedMessage')
alertDialogMessage.value = result.reason ? t(result.reason) : t('buildingsView.upgradeFailedMessage')
alertDialogOpen.value = true
}
}

View File

@@ -231,14 +231,14 @@
return defenseType === DefenseType.SmallShieldDome || defenseType === DefenseType.LargeShieldDome
}
const buildDefense = (defenseType: DefenseType, quantity: number): boolean => {
const buildDefense = (defenseType: DefenseType, quantity: number): { success: boolean; reason?: string } => {
const currentPlanet = gameStore.currentPlanet
if (!currentPlanet) return false
if (!currentPlanet) return { success: false }
const validation = shipValidation.validateDefenseBuild(currentPlanet, defenseType, quantity, gameStore.player.technologies)
if (!validation.valid) return false
if (!validation.valid) return { success: false, reason: validation.reason }
const queueItem = shipValidation.executeDefenseBuild(currentPlanet, defenseType, quantity, gameStore.player.officers)
currentPlanet.buildQueue.push(queueItem)
return true
return { success: true }
}
// 建造防御设施
@@ -251,10 +251,10 @@
return
}
const success = buildDefense(defenseType, quantity)
if (!success) {
const result = buildDefense(defenseType, quantity)
if (!result.success) {
alertDialogTitle.value = t('defenseView.buildFailed')
alertDialogMessage.value = t('defenseView.buildFailedMessage')
alertDialogMessage.value = result.reason ? t(result.reason) : t('defenseView.buildFailedMessage')
alertDialogOpen.value = true
} else {
quantities.value[defenseType] = 0

View File

@@ -12,7 +12,12 @@
<TabsList class="grid w-full grid-cols-4">
<TabsTrigger value="all">
{{ t('diplomacy.tabs.all') }}
<Badge variant="outline" class="ml-2 bg-blue-100 dark:bg-blue-950 text-blue-700 dark:text-blue-300 border-blue-300 dark:border-blue-700">{{ allNpcs.length }}</Badge>
<Badge
variant="outline"
class="ml-2 bg-blue-100 dark:bg-blue-950 text-blue-700 dark:text-blue-300 border-blue-300 dark:border-blue-700"
>
{{ allNpcs.length }}
</Badge>
</TabsTrigger>
<TabsTrigger value="friendly">
{{ t('diplomacy.tabs.friendly') }}
@@ -50,7 +55,13 @@
</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)" />
<NpcRelationCard
v-for="npc in paginatedAllNpcs"
:key="npc.id"
:npc="npc"
:relation="getRelation(npc.id)"
:data-npc-id="npc.id"
/>
</div>
<Pagination
v-if="totalPagesAll > 1"
@@ -84,7 +95,13 @@
</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)" />
<NpcRelationCard
v-for="npc in paginatedFriendlyNpcs"
:key="npc.id"
:npc="npc"
:relation="getRelation(npc.id)"
:data-npc-id="npc.id"
/>
</div>
<Pagination
v-if="totalPagesFriendly > 1"
@@ -118,7 +135,13 @@
</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)" />
<NpcRelationCard
v-for="npc in paginatedNeutralNpcs"
:key="npc.id"
:npc="npc"
:relation="getRelation(npc.id)"
:data-npc-id="npc.id"
/>
</div>
<Pagination
v-if="totalPagesNeutral > 1"
@@ -152,7 +175,13 @@
</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)" />
<NpcRelationCard
v-for="npc in paginatedHostileNpcs"
:key="npc.id"
:npc="npc"
:relation="getRelation(npc.id)"
:data-npc-id="npc.id"
/>
</div>
<Pagination
v-if="totalPagesHostile > 1"
@@ -179,57 +208,20 @@
</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, onMounted } from 'vue'
import { computed, ref, onMounted, onUnmounted, nextTick } 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'
import { RelationStatus } from '@/types/game'
import type { DiplomaticRelation } from '@/types/game'
const gameStore = useGameStore()
const npcStore = useNPCStore()
@@ -281,6 +273,49 @@
// 组件挂载时初始化NPC盟友
onMounted(() => {
initializeNPCAllies()
// 监听滚动到NPC卡片的事件
const handleScrollToNpc = (event: Event) => {
const customEvent = event as CustomEvent<{ npcId: string }>
const npcId = customEvent.detail.npcId
// 切换到"全部"标签
activeTab.value = 'all'
// 等待DOM更新后再滚动
nextTick(() => {
// 找到目标NPC在列表中的索引
const npcIndex = allNpcs.value.findIndex(npc => npc.id === npcId)
if (npcIndex === -1) return
// 计算目标NPC所在的页面
const targetPage = Math.floor(npcIndex / ITEMS_PER_PAGE) + 1
currentPage.value.all = targetPage
// 再次等待分页更新后滚动到卡片
nextTick(() => {
// 使用data属性来标识卡片
const cards = document.querySelectorAll('[data-npc-id]')
const targetCard = Array.from(cards).find(card => card.getAttribute('data-npc-id') === npcId)
if (targetCard) {
targetCard.scrollIntoView({ behavior: 'smooth', block: 'center' })
// 添加高亮效果
targetCard.classList.add('ring-2', 'ring-primary', 'ring-offset-2')
setTimeout(() => {
targetCard.classList.remove('ring-2', 'ring-primary', 'ring-offset-2')
}, 2000)
}
})
})
}
document.addEventListener('scrollToNpc', handleScrollToNpc)
// 清理事件监听器
onUnmounted(() => {
document.removeEventListener('scrollToNpc', handleScrollToNpc)
})
})
// 分页状态
@@ -387,65 +422,4 @@
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>

View File

@@ -4,7 +4,7 @@
<!-- 标签切换 -->
<Tabs v-model="activeTab" class="w-full">
<TabsList class="grid w-full grid-cols-2 sm:grid-cols-4" :tab-count="4">
<TabsList class="grid w-full grid-cols-2 sm:grid-cols-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>
@@ -296,6 +296,249 @@
<!-- 间谍报告对话框 -->
<SpyReportDialog v-model:open="showSpyDialog" :report="selectedSpyReport" />
<!-- 被侦查通知详情对话框 -->
<Dialog :open="showSpiedDialog" @update:open="showSpiedDialog = $event">
<DialogContent class="max-w-2xl">
<DialogHeader>
<DialogTitle class="flex items-center gap-2">
<Eye class="h-5 w-5 text-purple-500" />
{{ t('messagesView.spiedNotificationDetails') }}
</DialogTitle>
</DialogHeader>
<div v-if="selectedSpiedNotification" class="space-y-4">
<!-- 侦查者信息 -->
<div class="p-4 bg-muted/50 rounded-lg">
<div class="flex items-center gap-2 mb-2">
<h3 class="font-semibold text-lg">{{ selectedSpiedNotification.npcName }}</h3>
<Badge variant="destructive">{{ t('messagesView.spyDetected') }}</Badge>
</div>
<p class="text-sm text-muted-foreground">
{{ formatDate(selectedSpiedNotification.timestamp) }}
</p>
</div>
<!-- 被侦查星球 -->
<div class="space-y-2">
<h4 class="font-semibold text-sm">{{ t('messagesView.targetPlanet') }}</h4>
<div class="p-3 bg-muted/30 rounded-md flex items-center gap-2">
<Globe class="h-4 w-4 text-blue-500" />
<span class="font-medium">{{ selectedSpiedNotification.targetPlanetName }}</span>
</div>
</div>
<!-- 检测结果 -->
<div class="space-y-2">
<h4 class="font-semibold text-sm">{{ t('messagesView.detectionResult') }}</h4>
<div class="p-3 bg-muted/30 rounded-md">
<div v-if="selectedSpiedNotification.detectionSuccess" class="flex items-center gap-2 text-yellow-600 dark:text-yellow-400">
<AlertTriangle class="h-5 w-5" />
<span class="font-medium">{{ t('messagesView.detectionSuccess') }}</span>
</div>
<p class="text-sm mt-2">
{{
t('messagesView.spiedNotificationMessage', {
npc: selectedSpiedNotification.npcName,
planet: selectedSpiedNotification.targetPlanetName
})
}}
</p>
</div>
</div>
<!-- 建议 -->
<div class="p-3 bg-blue-50 dark:bg-blue-950/30 rounded-md border border-blue-200 dark:border-blue-800">
<p class="text-sm text-blue-800 dark:text-blue-200">
{{ t('messagesView.spiedNotificationTip') }}
</p>
</div>
</div>
<DialogFooter>
<Button variant="outline" @click="showSpiedDialog = false">{{ t('common.close') }}</Button>
<Button @click="viewNPCInGalaxy(selectedSpiedNotification?.npcId)">{{ t('messagesView.viewInGalaxy') }}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<!-- 任务报告详情对话框 -->
<Dialog :open="showMissionDialog" @update:open="showMissionDialog = $event">
<DialogContent class="max-w-2xl">
<DialogHeader>
<DialogTitle class="flex items-center gap-2">
<component :is="getMissionIcon(selectedMissionReport?.missionType)" class="h-5 w-5" />
{{ t('messagesView.missionReportDetails') }}
</DialogTitle>
</DialogHeader>
<div v-if="selectedMissionReport" class="space-y-4">
<!-- 任务状态 -->
<div class="p-4 bg-muted/50 rounded-lg">
<div class="flex items-center gap-2 mb-2">
<h3 class="font-semibold text-lg">{{ getMissionTypeName(selectedMissionReport.missionType) }}</h3>
<Badge :variant="selectedMissionReport.success ? 'default' : 'destructive'">
{{ selectedMissionReport.success ? t('messagesView.missionSuccess') : t('messagesView.missionFailed') }}
</Badge>
</div>
<p class="text-sm text-muted-foreground">
{{ formatDate(selectedMissionReport.timestamp) }}
</p>
</div>
<!-- 起点和终点 -->
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<h4 class="font-semibold text-sm">{{ t('messagesView.origin') }}</h4>
<div class="p-3 bg-muted/30 rounded-md">
<p class="font-medium">{{ selectedMissionReport.originPlanetName }}</p>
</div>
</div>
<div class="space-y-2">
<h4 class="font-semibold text-sm">{{ t('messagesView.destination') }}</h4>
<div class="p-3 bg-muted/30 rounded-md">
<p class="font-medium" v-if="selectedMissionReport.targetPlanetName">{{ selectedMissionReport.targetPlanetName }}</p>
<p class="text-sm text-muted-foreground" v-else>
[{{ selectedMissionReport.targetPosition.galaxy }}:{{ selectedMissionReport.targetPosition.system }}:{{
selectedMissionReport.targetPosition.position
}}]
</p>
</div>
</div>
</div>
<!-- 任务详情 -->
<div class="space-y-2">
<h4 class="font-semibold text-sm">{{ t('messagesView.missionDetails') }}</h4>
<div class="p-3 bg-muted/30 rounded-md">
<p class="text-sm mb-2">{{ selectedMissionReport.message }}</p>
<!-- 运输任务详情 -->
<div v-if="selectedMissionReport.details?.transportedResources" class="mt-3 space-y-1">
<p class="text-xs font-semibold text-muted-foreground">{{ t('messagesView.transportedResources') }}:</p>
<div class="grid grid-cols-3 gap-2 text-sm">
<div>{{ t('resources.metal') }}: {{ selectedMissionReport.details.transportedResources.metal.toLocaleString() }}</div>
<div>{{ t('resources.crystal') }}: {{ selectedMissionReport.details.transportedResources.crystal.toLocaleString() }}</div>
<div>
{{ t('resources.deuterium') }}: {{ selectedMissionReport.details.transportedResources.deuterium.toLocaleString() }}
</div>
</div>
</div>
<!-- 回收任务详情 -->
<div v-if="selectedMissionReport.details?.recycledResources" class="mt-3 space-y-1">
<p class="text-xs font-semibold text-muted-foreground">{{ t('messagesView.recycledResources') }}:</p>
<div class="grid grid-cols-2 gap-2 text-sm">
<div>{{ t('resources.metal') }}: {{ selectedMissionReport.details.recycledResources.metal.toLocaleString() }}</div>
<div>{{ t('resources.crystal') }}: {{ selectedMissionReport.details.recycledResources.crystal.toLocaleString() }}</div>
</div>
<div v-if="selectedMissionReport.details.remainingDebris" class="mt-2">
<p class="text-xs font-semibold text-muted-foreground">{{ t('messagesView.remainingDebris') }}:</p>
<div class="grid grid-cols-2 gap-2 text-sm text-yellow-600 dark:text-yellow-400">
<div>{{ t('resources.metal') }}: {{ selectedMissionReport.details.remainingDebris.metal.toLocaleString() }}</div>
<div>{{ t('resources.crystal') }}: {{ selectedMissionReport.details.remainingDebris.crystal.toLocaleString() }}</div>
</div>
</div>
</div>
<!-- 殖民任务详情 -->
<div v-if="selectedMissionReport.details?.newPlanetName" class="mt-3">
<p class="text-xs font-semibold text-muted-foreground">{{ t('messagesView.newPlanet') }}:</p>
<div class="flex items-center gap-2 mt-1">
<Globe class="h-4 w-4 text-green-500" />
<span class="font-medium">{{ selectedMissionReport.details.newPlanetName }}</span>
</div>
</div>
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" @click="showMissionDialog = false">{{ t('common.close') }}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<!-- NPC活动通知详情对话框 -->
<Dialog :open="showNPCActivityDialog" @update:open="showNPCActivityDialog = $event">
<DialogContent class="max-w-2xl">
<DialogHeader>
<DialogTitle class="flex items-center gap-2">
<Recycle class="h-5 w-5 text-yellow-500" />
{{ t('messagesView.npcActivityDetails') }}
</DialogTitle>
</DialogHeader>
<div v-if="selectedNPCActivityNotification" class="space-y-4">
<!-- NPC信息 -->
<div class="p-4 bg-muted/50 rounded-lg">
<div class="flex items-center gap-2 mb-2">
<h3 class="font-semibold text-lg">{{ selectedNPCActivityNotification.npcName }}</h3>
<Badge variant="secondary">{{ t('messagesView.activityType.' + selectedNPCActivityNotification.activityType) }}</Badge>
</div>
<p class="text-sm text-muted-foreground">
{{ formatDate(selectedNPCActivityNotification.timestamp) }}
</p>
</div>
<!-- 活动位置 -->
<div class="space-y-2">
<h4 class="font-semibold text-sm">{{ t('messagesView.activityLocation') }}</h4>
<div class="p-3 bg-muted/30 rounded-md">
<div class="flex items-center gap-2 mb-2">
<Globe class="h-4 w-4 text-blue-500" />
<span class="font-medium">
{{ t('messagesView.position') }}: [{{ selectedNPCActivityNotification.targetPosition.galaxy }}:{{
selectedNPCActivityNotification.targetPosition.system
}}:{{ selectedNPCActivityNotification.targetPosition.position }}]
</span>
</div>
<p v-if="selectedNPCActivityNotification.targetPlanetName" class="text-sm text-muted-foreground">
{{ t('messagesView.nearPlanet') }}: {{ selectedNPCActivityNotification.targetPlanetName }}
</p>
</div>
</div>
<!-- 活动描述 -->
<div class="space-y-2">
<h4 class="font-semibold text-sm">{{ t('messagesView.activityDescription') }}</h4>
<div class="p-3 bg-muted/30 rounded-md">
<p class="text-sm">
{{
t('messagesView.npcActivityMessage', {
npc: selectedNPCActivityNotification.npcName,
activity: t('messagesView.activityType.' + selectedNPCActivityNotification.activityType),
position: `[${selectedNPCActivityNotification.targetPosition.galaxy}:${selectedNPCActivityNotification.targetPosition.system}:${selectedNPCActivityNotification.targetPosition.position}]`
})
}}
</p>
</div>
</div>
<!-- 到达时间 -->
<div class="space-y-2">
<h4 class="font-semibold text-sm">{{ t('messagesView.arrivalTime') }}</h4>
<div class="p-3 bg-muted/30 rounded-md">
<p class="font-medium">{{ formatDate(selectedNPCActivityNotification.arrivalTime) }}</p>
</div>
</div>
<!-- 提示信息 -->
<div class="p-3 bg-yellow-50 dark:bg-yellow-950/30 rounded-md border border-yellow-200 dark:border-yellow-800">
<p class="text-sm text-yellow-800 dark:text-yellow-200">
{{ t('messagesView.npcActivityTip') }}
</p>
</div>
</div>
<DialogFooter>
<Button variant="outline" @click="showNPCActivityDialog = false">{{ t('common.close') }}</Button>
<Button @click="viewLocationInGalaxy(selectedNPCActivityNotification?.targetPosition)">
{{ t('messagesView.viewInGalaxy') }}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</template>
@@ -303,14 +546,16 @@
import { useGameStore } from '@/stores/gameStore'
import { useI18n } from '@/composables/useI18n'
import { computed, ref } from 'vue'
import { useRouter } from 'vue-router'
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 { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
import BattleReportDialog from '@/components/BattleReportDialog.vue'
import SpyReportDialog from '@/components/SpyReportDialog.vue'
import { formatDate } from '@/utils/format'
import { X, Sword, Eye, AlertTriangle, Package, Recycle, Gift, Ban, Check, Users } from 'lucide-vue-next'
import { X, Sword, Eye, AlertTriangle, Package, Recycle, Gift, Ban, Check, Users, Skull, Globe } from 'lucide-vue-next'
import type {
BattleResult,
SpyReport,
@@ -324,6 +569,7 @@
import { useNPCStore } from '@/stores/npcStore'
import * as diplomaticLogic from '@/logic/diplomaticLogic'
const router = useRouter()
const gameStore = useGameStore()
const npcStore = useNPCStore()
const { t } = useI18n()
@@ -332,8 +578,14 @@
// 对话框状态
const showBattleDialog = ref(false)
const showSpyDialog = ref(false)
const showSpiedDialog = ref(false)
const showMissionDialog = ref(false)
const showNPCActivityDialog = ref(false)
const selectedBattleReport = ref<BattleResult | null>(null)
const selectedSpyReport = ref<SpyReport | null>(null)
const selectedSpiedNotification = ref<SpiedNotification | null>(null)
const selectedMissionReport = ref<MissionReport | null>(null)
const selectedNPCActivityNotification = ref<NPCActivityNotification | null>(null)
// 排序后的战斗报告(最新的在前)
const sortedBattleReports = computed(() => {
@@ -525,6 +777,9 @@
if (!notification.read) {
notification.read = true
}
// 设置选中的通知并打开详情对话框
selectedSpiedNotification.value = notification
showSpiedDialog.value = true
}
// 删除战斗报告
@@ -560,6 +815,9 @@
if (!notification.read) {
notification.read = true
}
// 设置选中的通知并打开详情对话框
selectedNPCActivityNotification.value = notification
showNPCActivityDialog.value = true
}
// 删除NPC活动通知
@@ -592,6 +850,9 @@
if (!report.read) {
report.read = true
}
// 设置选中的报告并打开详情对话框
selectedMissionReport.value = report
showMissionDialog.value = true
}
// 删除任务报告
@@ -656,4 +917,56 @@
gameStore.player.giftRejectedNotifications.splice(index, 1)
}
}
// 查看NPC在星系中的位置
const viewNPCInGalaxy = (npcId?: string) => {
if (!npcId) return
const npc = npcStore.npcs.find(n => n.id === npcId)
if (!npc || npc.planets.length === 0) return
const targetPlanet = npc.planets[0]
if (!targetPlanet) return
showSpiedDialog.value = false
router.push({
path: '/galaxy',
query: {
galaxy: targetPlanet.position.galaxy,
system: targetPlanet.position.system,
highlightNpc: npcId
}
})
}
// 查看位置在星系中
const viewLocationInGalaxy = (position?: { galaxy: number; system: number; position: number }) => {
if (!position) return
showNPCActivityDialog.value = false
router.push({
path: '/galaxy',
query: {
galaxy: position.galaxy,
system: position.system
}
})
}
// 获取任务类型图标
const getMissionIcon = (missionType?: MissionType) => {
if (!missionType) return Package
switch (missionType) {
case MissionType.Transport:
return Package
case MissionType.Recycle:
return Recycle
case MissionType.Colonize:
return Globe
case MissionType.Destroy:
return Skull
default:
return Package
}
}
</script>

View File

@@ -1,12 +1,12 @@
<template>
<div v-if="planet" class="container mx-auto p-4 sm:p-6">
<!-- 未解锁遮罩 -->
<UnlockRequirement :required-building="BuildingType.ResearchLab" :required-level="1" />
<!-- <UnlockRequirement :required-building="BuildingType.ResearchLab" :required-level="1" /> -->
<h1 class="text-2xl sm:text-3xl font-bold mb-4 sm:mb-6">{{ t('researchView.title') }}</h1>
<div class="grid grid-cols-2 lg:grid-cols-3 gap-3 sm:gap-4">
<Card v-for="techType in Object.values(TechnologyType)" :key="techType" class="relative">
<Card v-for="techType in Object.values(TechnologyType)" :key="techType" :data-tech="techType" class="relative">
<CardUnlockOverlay :requirements="TECHNOLOGIES[techType].requirements" :currentLevel="getTechLevel(techType)" />
<CardHeader>
<div class="mb-2">
@@ -98,7 +98,6 @@
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'
import * as publicLogic from '@/logic/publicLogic'

View File

@@ -22,7 +22,7 @@
<div
class="h-full transition-all duration-300"
:class="fleetStorageUsage > maxFleetStorage ? 'bg-destructive' : 'bg-primary'"
:style="{ width: `${Math.min((fleetStorageUsage / maxFleetStorage) * 100, 100)}%` }"
:style="{ width: `${maxFleetStorage > 0 ? Math.min((fleetStorageUsage / maxFleetStorage) * 100, 100) : 0}%` }"
/>
</div>
</div>
@@ -216,13 +216,13 @@
[ShipType.Deathstar]: 0
})
const buildShip = (shipType: ShipType, quantity: number): boolean => {
if (!gameStore.currentPlanet) return false
const buildShip = (shipType: ShipType, quantity: number): { success: boolean; reason?: string } => {
if (!gameStore.currentPlanet) return { success: false }
const validation = shipValidation.validateShipBuild(gameStore.currentPlanet, shipType, quantity, gameStore.player.technologies)
if (!validation.valid) return false
if (!validation.valid) return { success: false, reason: validation.reason }
const queueItem = shipValidation.executeShipBuild(gameStore.currentPlanet, shipType, quantity, gameStore.player.officers)
gameStore.currentPlanet.buildQueue.push(queueItem)
return true
return { success: true }
}
// 建造舰船
@@ -235,10 +235,10 @@
return
}
const success = buildShip(shipType, quantity)
if (!success) {
const result = buildShip(shipType, quantity)
if (!result.success) {
alertDialogTitle.value = t('shipyardView.buildFailed')
alertDialogMessage.value = t('shipyardView.buildFailedMessage')
alertDialogMessage.value = result.reason ? t(result.reason) : t('shipyardView.buildFailedMessage')
alertDialogOpen.value = true
} else {
quantities.value[shipType] = 0
@@ -260,6 +260,11 @@
darkMatter: config.cost.darkMatter * quantity
}
// 检查舰队仓储空间是否足够
if (!fleetStorageLogic.hasEnoughFleetStorage(planet.value, shipType, quantity, gameStore.player.technologies)) {
return false
}
return (
publicLogic.checkRequirements(planet.value, gameStore.player.technologies, config.requirements) &&
planet.value.resources.metal >= totalCost.metal &&