mirror of
https://github.com/setube/ogame-vue-ts.git
synced 2026-05-11 23:45:11 +08:00
refactor: 优化主界面布局与通知系统
重构App.vue,首页独立无侧边栏,其他页面采用统一侧边栏布局。新增右下角固定通知区,集成返回顶部、队列通知、外交通知和敌方警报。移除新手引导组件,替换为弱引导提示系统。支持星球重命名弹窗。优化NPC成长与行为定时器逻辑,提升性能和可维护性。删除issue模板及相关文档描述。
This commit is contained in:
31
.github/ISSUE_TEMPLATE/BUG反馈.md
vendored
31
.github/ISSUE_TEMPLATE/BUG反馈.md
vendored
@@ -1,31 +0,0 @@
|
||||
---
|
||||
name: BUG反馈
|
||||
about: 报告项目中发现的缺陷或问题
|
||||
title: '[BUG] 简要描述问题'
|
||||
labels: 'bug'
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
**问题描述**
|
||||
清晰准确地描述遇到的问题
|
||||
|
||||
**重现步骤**
|
||||
|
||||
1. 第一步操作
|
||||
2. 第二步操作
|
||||
3. 出现问题的操作
|
||||
|
||||
**期望行为**
|
||||
描述您认为正确的行为应该是怎样的
|
||||
|
||||
**实际行为**
|
||||
描述实际发生的错误行为
|
||||
|
||||
**环境信息**
|
||||
|
||||
- 操作系统:
|
||||
- 浏览器(如适用):
|
||||
- 项目版本:
|
||||
|
||||
**截图或日志(可选)**
|
||||
如果有错误截图或日志,请提供
|
||||
19
.github/ISSUE_TEMPLATE/功能请求.md
vendored
19
.github/ISSUE_TEMPLATE/功能请求.md
vendored
@@ -1,19 +0,0 @@
|
||||
---
|
||||
name: 功能请求
|
||||
about: 请求添加新功能或改进现有功能
|
||||
title: '[功能] 简要描述功能'
|
||||
labels: 'enhancement'
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
**功能描述**
|
||||
清晰描述您希望添加的功能
|
||||
|
||||
**功能背景**
|
||||
说明为什么需要这个功能,它能解决什么问题
|
||||
|
||||
**建议实现方案(可选)**
|
||||
如果有具体的实现想法,可以在这里描述
|
||||
|
||||
**附加信息**
|
||||
任何其他有助于理解这个功能的信息
|
||||
19
.github/ISSUE_TEMPLATE/反馈建议.md
vendored
19
.github/ISSUE_TEMPLATE/反馈建议.md
vendored
@@ -1,19 +0,0 @@
|
||||
---
|
||||
name: 反馈建议
|
||||
about: 为这个项目提出功能建议或改进意见
|
||||
title: '[建议] 简要描述您的建议'
|
||||
labels: 'enhancement'
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
**您的建议是什么?**
|
||||
请清晰描述您希望添加的功能或改进点
|
||||
|
||||
**为什么需要这个功能/改进?**
|
||||
说明这个建议会解决什么问题或带来什么价值
|
||||
|
||||
**您期望的实现方式(可选)**
|
||||
如果有具体的实现想法,可以在这里描述
|
||||
|
||||
**附加信息(可选)**
|
||||
任何其他有助于理解这个建议的信息
|
||||
19
.github/ISSUE_TEMPLATE/文档改进.md
vendored
19
.github/ISSUE_TEMPLATE/文档改进.md
vendored
@@ -1,19 +0,0 @@
|
||||
---
|
||||
name: 文档改进
|
||||
about: 报告文档问题或建议改进
|
||||
title: '[文档] 简要描述问题'
|
||||
labels: 'documentation'
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
**文档位置**
|
||||
指出需要改进的文档路径或 URL
|
||||
|
||||
**当前问题**
|
||||
描述当前文档存在的问题或不清晰的地方
|
||||
|
||||
**改进建议**
|
||||
提出具体的改进建议
|
||||
|
||||
**附加信息(可选)**
|
||||
任何其他有助于改进文档的信息
|
||||
2
.github/workflows/ogame-vue-ts.yml
vendored
2
.github/workflows/ogame-vue-ts.yml
vendored
@@ -57,4 +57,4 @@ jobs:
|
||||
${{ vars.DOCKERHUB_USERNAME != '' && format('docker.io/{0}/ogame-vue-ts:{1}', vars.DOCKERHUB_USERNAME, github.sha) || '' }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
outputs: type=image,name=target,annotation-index.org.opencontainers.image.description=OGame Vue
|
||||
outputs: type=image,name=target,annotation-index.org.opencontainers.image.description=OGame Vue Ts
|
||||
@@ -213,13 +213,6 @@ The application supports full theme customization through Tailwind CSS variables
|
||||
|
||||
Contributions are welcome! Please feel free to submit issues or pull requests.
|
||||
|
||||
### Issue Templates
|
||||
We provide the following issue templates in both Chinese and English:
|
||||
- Bug Report
|
||||
- Feature Request
|
||||
- Documentation Improvement
|
||||
- eedback & Suggestion
|
||||
|
||||
## License
|
||||
|
||||
This work is licensed under the [Creative Commons Attribution-NonCommercial 4.0 International License](https://creativecommons.org/licenses/by-nc/4.0/).
|
||||
|
||||
@@ -213,13 +213,6 @@ ogame-vue-ts/
|
||||
|
||||
欢迎贡献!请随时提交 issue 或 pull request。
|
||||
|
||||
### Issue 模板
|
||||
我们提供以下中英文 issue 模板:
|
||||
- BUG反馈 / Bug Report
|
||||
- 功能请求 / Feature Request
|
||||
- 文档改进 / Documentation Improvement
|
||||
- 反馈建议 / Feedback & Suggestion
|
||||
|
||||
## 许可证
|
||||
|
||||
本作品采用 [知识共享署名-非商业性使用 4.0 国际许可协议](https://creativecommons.org/licenses/by-nc/4.0/) 进行许可。
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
"email": "1962257451@qq.com"
|
||||
},
|
||||
"private": true,
|
||||
"version": "1.3.0",
|
||||
"buildDate": "2025/12/18 04:52:39",
|
||||
"version": "1.4.0",
|
||||
"buildDate": "2025/12/19 12:01:23",
|
||||
"main": "dist-electron/main.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
717
src/App.vue
717
src/App.vue
@@ -1,7 +1,13 @@
|
||||
<template>
|
||||
<SidebarProvider :open="sidebarOpen" @update:open="handleSidebarOpenChange">
|
||||
<!-- 首页:无侧边栏/头部 -->
|
||||
<template v-if="isHomePage">
|
||||
<RouterView />
|
||||
</template>
|
||||
|
||||
<!-- 其他页面:完整布局(含侧边栏) -->
|
||||
<SidebarProvider v-else :open="sidebarOpen" @update:open="handleSidebarOpenChange">
|
||||
<Sidebar collapsible="icon">
|
||||
<!-- Logo -->
|
||||
<!-- 标志 -->
|
||||
<SidebarHeader class="border-b">
|
||||
<div class="flex items-center justify-center p-4 group-data-[collapsible=icon]:p-2">
|
||||
<img src="@/assets/logo.svg" class="w-10 group-data-[collapsible=icon]:w-8" />
|
||||
@@ -47,12 +53,11 @@
|
||||
{{ t('planet.switchPlanet') }}
|
||||
</div>
|
||||
<div class="space-y-0.5 max-h-80 overflow-y-auto">
|
||||
<div v-for="p in gameStore.player.planets" :key="p.id" class="flex items-center gap-1">
|
||||
<Button
|
||||
v-for="p in gameStore.player.planets"
|
||||
:key="p.id"
|
||||
@click="switchToPlanet(p.id)"
|
||||
:variant="p.id === planet.id ? 'secondary' : 'ghost'"
|
||||
class="w-full justify-start h-auto py-2 px-2"
|
||||
class="flex-1 justify-start h-auto py-2 px-2"
|
||||
size="sm"
|
||||
>
|
||||
<div class="flex items-start gap-2 w-full min-w-0">
|
||||
@@ -60,6 +65,15 @@
|
||||
<div class="flex-1 min-w-0 text-left">
|
||||
<div class="flex items-center gap-1.5 mb-0.5">
|
||||
<span class="truncate font-medium text-sm">{{ p.name }}</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-2 w-2 p-0 flex-shrink-0"
|
||||
@click.stop="openRenameDialog(p.id, p.name)"
|
||||
:title="t('planet.renamePlanet')"
|
||||
>
|
||||
<Pencil class="h-2 w-2" />
|
||||
</Button>
|
||||
<Badge v-if="p.isMoon" variant="outline" class="text-[10px] px-1 py-0 h-4">
|
||||
{{ t('planet.moon') }}
|
||||
</Badge>
|
||||
@@ -72,6 +86,7 @@
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
@@ -185,7 +200,7 @@
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<SidebarInset>
|
||||
<div class="flex flex-col h-full overflow-hidden pt-[60px]">
|
||||
<div class="flex flex-col h-full pt-[60px]">
|
||||
<!-- 顶部资源栏 - 固定定位 -->
|
||||
<header
|
||||
v-if="planet"
|
||||
@@ -244,19 +259,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:展开按钮(仅移动端) + 状态 -->
|
||||
<!-- 右侧:展开按钮(仅移动端) -->
|
||||
<div class="flex items-center gap-2 sm:gap-3 flex-shrink-0 justify-end">
|
||||
<!-- 移动端展开按钮 -->
|
||||
<Button @click="resourceBarExpanded = !resourceBarExpanded" variant="ghost" size="sm" class="lg:hidden h-8 w-8 p-0">
|
||||
<ChevronDown v-if="!resourceBarExpanded" class="h-4 w-4" />
|
||||
<ChevronUp v-else class="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<!-- 外交通知 -->
|
||||
<DiplomaticNotifications />
|
||||
|
||||
<!-- 队列通知 -->
|
||||
<QueueNotifications />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -319,16 +328,14 @@
|
||||
</Transition>
|
||||
|
||||
<!-- 即将到来的敌对舰队警告 -->
|
||||
<IncomingFleetAlerts
|
||||
v-if="gameStore.player.incomingFleetAlerts && gameStore.player.incomingFleetAlerts.length > 0"
|
||||
:alerts="gameStore.player.incomingFleetAlerts"
|
||||
@mark-as-read="removeIncomingFleetAlert"
|
||||
/>
|
||||
<IncomingFleetAlerts @open-panel="openEnemyAlertPanel" />
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<main class="flex-1 overflow-y-auto">
|
||||
<main class="flex-1">
|
||||
<Transition name="page" mode="out-in">
|
||||
<div :key="$route.fullPath" class="h-full">
|
||||
<!-- 背景动画开启时 -->
|
||||
<template v-if="gameStore.player.backgroundEnabled">
|
||||
<StarsBackground v-if="isDark" :factor="0.05" :speed="50" star-color="#fff" class="h-full">
|
||||
<div class="relative z-10 h-full">
|
||||
<RouterView />
|
||||
@@ -342,12 +349,33 @@
|
||||
|
||||
<ParticlesBg class="absolute inset-0 z-0" :quantity="100" :ease="100" color="#000" :staticity="10" refresh />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 背景动画关闭时 -->
|
||||
<div v-else class="h-full">
|
||||
<RouterView />
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</main>
|
||||
</div>
|
||||
</SidebarInset>
|
||||
|
||||
<!-- 右下角固定通知按钮 -->
|
||||
<div class="fixed bottom-4 right-4 z-50 flex flex-col gap-2">
|
||||
<!-- 返回顶部 -->
|
||||
<BackToTop />
|
||||
|
||||
<!-- 队列通知 -->
|
||||
<QueueNotifications />
|
||||
|
||||
<!-- 外交通知 -->
|
||||
<DiplomaticNotifications />
|
||||
|
||||
<!-- 敌方警报 -->
|
||||
<EnemyAlertNotifications ref="enemyAlertNotificationsRef" />
|
||||
</div>
|
||||
|
||||
<!-- 确认对话框 -->
|
||||
<AlertDialog :open="confirmDialogOpen" @update:open="confirmDialogOpen = $event">
|
||||
<AlertDialogContent>
|
||||
@@ -370,11 +398,32 @@
|
||||
<!-- 更新弹窗 -->
|
||||
<UpdateDialog v-model:open="showUpdateDialog" :version-info="updateInfo" />
|
||||
|
||||
<!-- 新手引导 -->
|
||||
<TutorialOverlay />
|
||||
<!-- 弱引导提示系统 -->
|
||||
<HintToast />
|
||||
|
||||
<!-- Toast 通知 -->
|
||||
<Sonner position="top-center" />
|
||||
|
||||
<!-- 重命名星球对话框 -->
|
||||
<Dialog v-model:open="renameDialogOpen">
|
||||
<DialogContent class="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ t('planet.renamePlanetTitle') }}</DialogTitle>
|
||||
<DialogDescription class="sr-only">{{ t('planet.renamePlanetTitle') }}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div class="py-4">
|
||||
<Input v-model="newPlanetName" :placeholder="t('planet.planetNamePlaceholder')" @keyup.enter="confirmRenamePlanet" />
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="renameDialogOpen = false">
|
||||
{{ t('common.cancel') }}
|
||||
</Button>
|
||||
<Button @click="confirmRenamePlanet" :disabled="!newPlanetName.trim()">
|
||||
{{ t('planet.rename') }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</SidebarProvider>
|
||||
</template>
|
||||
|
||||
@@ -387,13 +436,15 @@
|
||||
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 { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover'
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import IncomingFleetAlerts from '@/components/IncomingFleetAlerts.vue'
|
||||
import DiplomaticNotifications from '@/components/DiplomaticNotifications.vue'
|
||||
import EnemyAlertNotifications from '@/components/EnemyAlertNotifications.vue'
|
||||
import QueueNotifications from '@/components/QueueNotifications.vue'
|
||||
import {
|
||||
Sidebar,
|
||||
@@ -422,14 +473,15 @@
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import DetailDialog from '@/components/DetailDialog.vue'
|
||||
import UpdateDialog from '@/components/UpdateDialog.vue'
|
||||
import TutorialOverlay from '@/components/TutorialOverlay.vue'
|
||||
import HintToast from '@/components/HintToast.vue'
|
||||
import BackToTop from '@/components/BackToTop.vue'
|
||||
import Sonner from '@/components/ui/sonner/Sonner.vue'
|
||||
import { MissionType, BuildingType, TechnologyType, DiplomaticEventType } from '@/types/game'
|
||||
import type { FleetMission, NPC, IncomingFleetAlert, MissileAttack } from '@/types/game'
|
||||
import type { FleetMission, NPC, MissileAttack } from '@/types/game'
|
||||
import { DIPLOMATIC_CONFIG } from '@/config/gameConfig'
|
||||
import type { VersionInfo } from '@/utils/versionCheck'
|
||||
import { formatNumber, getResourceColor } from '@/utils/format'
|
||||
import { getGameLoopIntervalMs, scaleNumber, scaleResources } from '@/utils/speed'
|
||||
import { scaleNumber, scaleResources } from '@/utils/speed'
|
||||
import {
|
||||
Moon,
|
||||
Sun,
|
||||
@@ -450,7 +502,8 @@
|
||||
ChevronsUpDown,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Handshake
|
||||
Handshake,
|
||||
Pencil
|
||||
} from 'lucide-vue-next'
|
||||
import * as gameLogic from '@/logic/gameLogic'
|
||||
import * as planetLogic from '@/logic/planetLogic'
|
||||
@@ -481,18 +534,140 @@
|
||||
const { isDark } = useTheme()
|
||||
const { t } = useI18n()
|
||||
const { BUILDINGS, TECHNOLOGIES } = useGameConfig()
|
||||
const { startTutorial, tutorialState, currentStep } = useTutorial()
|
||||
|
||||
const enemyAlertNotificationsRef = ref<InstanceType<typeof EnemyAlertNotifications> | null>(null)
|
||||
// ConfirmDialog 状态
|
||||
const confirmDialogOpen = ref(false)
|
||||
const confirmDialogTitle = ref('')
|
||||
const confirmDialogMessage = ref('')
|
||||
const innerWidth = computed(() => window.innerWidth)
|
||||
const confirmDialogAction = ref<(() => void) | null>(null)
|
||||
|
||||
// 更新弹窗状态
|
||||
const showUpdateDialog = ref(false)
|
||||
const updateInfo = ref<VersionInfo | null>(null)
|
||||
// 所有可用的语言选项
|
||||
const locales: Locale[] = ['zh-CN', 'zh-TW', 'en', 'de', 'ru', 'ko', 'ja']
|
||||
// 侧边栏状态(不持久化,根据屏幕尺寸初始化)
|
||||
// PC端(≥1024px)默认打开,移动端默认关闭
|
||||
const sidebarOpen = ref(window.innerWidth >= 1024)
|
||||
// 移动端资源栏展开状态
|
||||
const resourceBarExpanded = ref(false)
|
||||
const npcUpdateCounter = ref(0) // 累计秒数
|
||||
const NPC_UPDATE_INTERVAL = 5 // 每1秒更新一次NPC,确保发育速度与玩家相当
|
||||
// NPC行为系统更新函数(侦查和攻击决策)
|
||||
const npcBehaviorCounter = ref(0)
|
||||
const NPC_BEHAVIOR_INTERVAL = 5 // 每5秒检查一次NPC行为
|
||||
|
||||
// 游戏循环定时器
|
||||
const gameLoop = ref<ReturnType<typeof setInterval> | null>(null)
|
||||
const pointsUpdateInterval = ref<ReturnType<typeof setInterval> | null>(null)
|
||||
const konamiCleanup = ref<(() => void) | null>(null)
|
||||
const versionCheckInterval = ref<ReturnType<typeof setInterval> | null>(null) // 重命名星球相关状态
|
||||
const renameDialogOpen = ref(false)
|
||||
const renamingPlanetId = ref<string | null>(null)
|
||||
const newPlanetName = ref('')
|
||||
// 功能解锁要求配置
|
||||
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 isHomePage = computed(() => router.currentRoute.value.path === '/')
|
||||
|
||||
// 定义 planet computed(需要在 watch 之前定义)
|
||||
const planet = computed(() => gameStore.currentPlanet)
|
||||
|
||||
// 资源类型配置
|
||||
const resourceTypes = [
|
||||
{ key: 'metal' as const },
|
||||
{ key: 'crystal' as const },
|
||||
{ key: 'deuterium' as const },
|
||||
{ key: 'energy' as const },
|
||||
{ key: 'darkMatter' as const }
|
||||
]
|
||||
|
||||
const navItems = computed(() => [
|
||||
{ name: computed(() => t('nav.overview')), path: '/overview', icon: Home },
|
||||
{ name: computed(() => t('nav.buildings')), path: '/buildings', icon: Building2 },
|
||||
{ name: computed(() => t('nav.research')), path: '/research', icon: FlaskConical },
|
||||
{ name: computed(() => t('nav.shipyard')), path: '/shipyard', icon: Ship },
|
||||
{ name: computed(() => t('nav.defense')), path: '/defense', icon: Shield },
|
||||
{ name: computed(() => t('nav.fleet')), path: '/fleet', icon: Rocket },
|
||||
{ name: computed(() => t('nav.officers')), path: '/officers', icon: Users },
|
||||
{ name: computed(() => t('nav.simulator')), path: '/battle-simulator', icon: Swords },
|
||||
{ name: computed(() => t('nav.galaxy')), path: '/galaxy', icon: Globe },
|
||||
{ name: computed(() => t('nav.diplomacy')), path: '/diplomacy', icon: Handshake },
|
||||
{ name: computed(() => t('nav.messages')), path: '/messages', icon: Mail },
|
||||
{ name: computed(() => t('nav.settings')), path: '/settings', icon: Settings },
|
||||
// GM菜单在启用GM模式时显示
|
||||
...(gameStore.player.isGMEnabled ? [{ name: computed(() => t('nav.gm')), path: '/gm', icon: Wrench }] : [])
|
||||
])
|
||||
|
||||
// 使用直接计算,不再缓存
|
||||
const production = computed(() => {
|
||||
if (!planet.value) return null
|
||||
const now = Date.now()
|
||||
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, now)
|
||||
const base = resourceLogic.calculateResourceProduction(planet.value, {
|
||||
resourceProductionBonus: bonuses.resourceProductionBonus,
|
||||
darkMatterProductionBonus: bonuses.darkMatterProductionBonus,
|
||||
energyProductionBonus: bonuses.energyProductionBonus
|
||||
})
|
||||
return scaleResources(base, gameStore.gameSpeed)
|
||||
})
|
||||
|
||||
const capacity = computed(() => {
|
||||
if (!planet.value) return null
|
||||
const now = Date.now()
|
||||
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, now)
|
||||
return resourceLogic.calculateResourceCapacity(planet.value, bonuses.storageCapacityBonus)
|
||||
})
|
||||
|
||||
// 电力消耗
|
||||
const energyConsumption = computed(() => {
|
||||
if (!planet.value) return 0
|
||||
return scaleNumber(resourceLogic.calculateEnergyConsumption(planet.value), gameStore.gameSpeed)
|
||||
})
|
||||
|
||||
// 净电力(产量 - 消耗)
|
||||
const netEnergy = computed(() => {
|
||||
if (!planet.value || !production.value) return 0
|
||||
return production.value.energy - energyConsumption.value
|
||||
})
|
||||
|
||||
// 未读消息数量
|
||||
const unreadMessagesCount = computed(() => {
|
||||
const unreadBattles = gameStore.player.battleReports.filter(r => !r.read).length
|
||||
const unreadSpies = gameStore.player.spyReports.filter(r => !r.read).length
|
||||
const unreadSpied = gameStore.player.spiedNotifications?.filter(n => !n.read).length || 0
|
||||
const unreadMissions = gameStore.player.missionReports?.filter(r => !r.read).length || 0
|
||||
const unreadNPCActivity = gameStore.player.npcActivityNotifications?.filter(n => !n.read).length || 0
|
||||
const unreadGifts = gameStore.player.giftNotifications?.filter(n => !n.read).length || 0
|
||||
const unreadGiftRejected = gameStore.player.giftRejectedNotifications?.filter(n => !n.read).length || 0
|
||||
return unreadBattles + unreadSpies + unreadSpied + unreadMissions + unreadNPCActivity + unreadGifts + unreadGiftRejected
|
||||
})
|
||||
|
||||
// 正在执行的舰队任务数量(包括飞行中的导弹)
|
||||
const activeFleetMissionsCount = computed(() => {
|
||||
const fleetMissions = gameStore.player.fleetMissions.filter(m => m.status === 'outbound' || m.status === 'returning').length
|
||||
const flyingMissiles = gameStore.player.missileAttacks?.filter(m => m.status === 'flying').length || 0
|
||||
return fleetMissions + flyingMissiles
|
||||
})
|
||||
|
||||
// 未读外交报告数量
|
||||
const unreadDiplomaticReportsCount = computed(() => {
|
||||
return (gameStore.player.diplomaticReports || []).filter(r => !r.read).length
|
||||
})
|
||||
|
||||
// 月球相关
|
||||
const moon = computed(() => {
|
||||
if (!planet.value || planet.value.isMoon) return null
|
||||
return gameStore.getMoonForPlanet(planet.value.id)
|
||||
})
|
||||
|
||||
const hasMoon = computed(() => !!moon.value)
|
||||
|
||||
const handleNotification = (type: string, itemType: string, level?: number) => {
|
||||
const settings = gameStore.notificationSettings
|
||||
@@ -502,7 +677,7 @@
|
||||
if (!settings.browser && !settings.inApp) return
|
||||
|
||||
// 检查具体类型开关
|
||||
let typeKey = ''
|
||||
let typeKey: 'construction' | 'research'
|
||||
let title = ''
|
||||
let body = ''
|
||||
|
||||
@@ -545,16 +720,6 @@
|
||||
confirmDialogOpen.value = false
|
||||
}
|
||||
|
||||
// 所有可用的语言选项
|
||||
const locales: Locale[] = ['zh-CN', 'zh-TW', 'en', 'de', 'ru', 'ko', 'ja']
|
||||
|
||||
// 侧边栏状态(不持久化,根据屏幕尺寸初始化)
|
||||
// PC端(≥1024px)默认打开,移动端默认关闭
|
||||
const sidebarOpen = ref(window.innerWidth >= 1024)
|
||||
|
||||
// 移动端资源栏展开状态
|
||||
const resourceBarExpanded = ref(false)
|
||||
|
||||
const initGame = async () => {
|
||||
const shouldInit = gameLogic.shouldInitializeGame(gameStore.player.planets)
|
||||
if (!shouldInit) {
|
||||
@@ -687,6 +852,8 @@
|
||||
const originPlanetName = originPlanet?.name || t('fleetView.unknownPlanet')
|
||||
|
||||
if (mission.missionType === MissionType.Transport) {
|
||||
// 在处理任务之前保存货物信息(因为processTransportArrival会清空cargo)
|
||||
const transportedResources = { ...mission.cargo }
|
||||
const result = fleetLogic.processTransportArrival(mission, targetPlanet, gameStore.player, npcStore.npcs)
|
||||
// 生成运输任务报告
|
||||
if (!gameStore.player.missionReports) {
|
||||
@@ -705,7 +872,7 @@
|
||||
success: result.success,
|
||||
message: result.success ? t('missionReports.transportSuccess') : t('missionReports.transportFailed'),
|
||||
details: {
|
||||
transportedResources: mission.cargo
|
||||
transportedResources
|
||||
},
|
||||
read: false
|
||||
})
|
||||
@@ -982,9 +1149,22 @@
|
||||
|
||||
// 如果生成残骸场,添加到宇宙残骸场列表
|
||||
if (attackResult.debrisField) {
|
||||
const existingDebris = universeStore.debrisFields[attackResult.debrisField.id]
|
||||
if (existingDebris) {
|
||||
// 累加残骸资源
|
||||
universeStore.debrisFields[attackResult.debrisField.id] = {
|
||||
...existingDebris,
|
||||
resources: {
|
||||
metal: existingDebris.resources.metal + attackResult.debrisField.resources.metal,
|
||||
crystal: existingDebris.resources.crystal + attackResult.debrisField.resources.crystal
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 新残骸场
|
||||
universeStore.debrisFields[attackResult.debrisField.id] = attackResult.debrisField
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 移除即将到来的警告(攻击已到达)
|
||||
removeIncomingFleetAlertById(mission.id)
|
||||
@@ -1078,19 +1258,7 @@
|
||||
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对玩家的关系
|
||||
// 更新NPC对玩家的关系(统一使用 npc.relations 作为唯一数据源)
|
||||
if (!targetNpc.relations) {
|
||||
targetNpc.relations = {}
|
||||
}
|
||||
@@ -1137,13 +1305,9 @@
|
||||
})
|
||||
}
|
||||
|
||||
// 移除即将到来的舰队警告
|
||||
const removeIncomingFleetAlert = (alert: IncomingFleetAlert) => {
|
||||
if (!gameStore.player.incomingFleetAlerts) return
|
||||
const index = gameStore.player.incomingFleetAlerts.indexOf(alert)
|
||||
if (index > -1) {
|
||||
gameStore.player.incomingFleetAlerts.splice(index, 1)
|
||||
}
|
||||
// 打开敌方警报面板
|
||||
const openEnemyAlertPanel = () => {
|
||||
enemyAlertNotificationsRef.value?.open()
|
||||
}
|
||||
|
||||
const removeIncomingFleetAlertById = (missionId: string) => {
|
||||
@@ -1154,16 +1318,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
// NPC成长系统更新函数
|
||||
let npcUpdateCounter = 0 // 累计秒数
|
||||
const NPC_UPDATE_INTERVAL = 5 // 每1秒更新一次NPC,确保发育速度与玩家相当
|
||||
|
||||
const updateNPCGrowth = (deltaSeconds: number) => {
|
||||
// 累积时间
|
||||
npcUpdateCounter += deltaSeconds
|
||||
npcUpdateCounter.value += deltaSeconds
|
||||
|
||||
// 只在达到更新间隔时才执行
|
||||
if (npcUpdateCounter < NPC_UPDATE_INTERVAL) {
|
||||
if (npcUpdateCounter.value < NPC_UPDATE_INTERVAL) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1185,13 +1345,24 @@
|
||||
const randomSpyOffset = Math.random() * 240 * 1000 // 0-4分钟的随机延迟
|
||||
const randomAttackOffset = Math.random() * 480 * 1000 // 0-8分钟的随机延迟
|
||||
|
||||
// 初始化NPC与玩家的中立关系
|
||||
const initialRelations: Record<string, any> = {}
|
||||
initialRelations[gameStore.player.id] = {
|
||||
fromId: planet.ownerId,
|
||||
toId: gameStore.player.id,
|
||||
reputation: 0,
|
||||
status: 'neutral' as const,
|
||||
lastUpdated: now,
|
||||
history: []
|
||||
}
|
||||
|
||||
npcMap.set(planet.ownerId, {
|
||||
id: planet.ownerId,
|
||||
name: `NPC-${planet.ownerId.substring(0, 8)}`,
|
||||
planets: [],
|
||||
technologies: {}, // 初始化空科技树
|
||||
difficulty: 'medium' as const, // 默认中等难度
|
||||
relations: {}, // 外交关系
|
||||
relations: initialRelations, // 外交关系(默认与玩家中立)
|
||||
allies: [], // 盟友列表
|
||||
enemies: [], // 敌人列表
|
||||
lastSpyTime: now - randomSpyOffset, // 设置随机的上次侦查时间
|
||||
@@ -1231,9 +1402,30 @@
|
||||
npcGrowthLogic.ensureNPCSpyProbes(npcStore.npcs)
|
||||
}
|
||||
|
||||
// 确保所有NPC都与玩家建立了关系(修复旧版本保存的数据)
|
||||
if (npcStore.npcs.length > 0) {
|
||||
const now = Date.now()
|
||||
npcStore.npcs.forEach(npc => {
|
||||
if (!npc.relations) {
|
||||
npc.relations = {}
|
||||
}
|
||||
// 如果NPC没有与玩家的关系,建立中立关系
|
||||
if (!npc.relations[gameStore.player.id]) {
|
||||
npc.relations[gameStore.player.id] = {
|
||||
fromId: npc.id,
|
||||
toId: gameStore.player.id,
|
||||
reputation: 0,
|
||||
status: 'neutral' as const,
|
||||
lastUpdated: now,
|
||||
history: []
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 如果没有NPC,直接返回
|
||||
if (npcStore.npcs.length === 0) {
|
||||
npcUpdateCounter = 0
|
||||
npcUpdateCounter.value = 0
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1244,31 +1436,27 @@
|
||||
npcs: npcStore.npcs
|
||||
}
|
||||
|
||||
// 使用累积的时间更新每个NPC
|
||||
// 使用累积的时间更新每个NPC(应用游戏速度倍率)
|
||||
npcStore.npcs.forEach(npc => {
|
||||
npcGrowthLogic.updateNPCGrowth(npc, gameState, npcUpdateCounter)
|
||||
npcGrowthLogic.updateNPCGrowth(npc, gameState, npcUpdateCounter.value, gameStore.gameSpeed)
|
||||
})
|
||||
|
||||
// 重置计数器
|
||||
npcUpdateCounter = 0
|
||||
npcUpdateCounter.value = 0
|
||||
}
|
||||
|
||||
// NPC行为系统更新函数(侦查和攻击决策)
|
||||
let npcBehaviorCounter = 0
|
||||
const NPC_BEHAVIOR_INTERVAL = 5 // 每5秒检查一次NPC行为
|
||||
|
||||
const updateNPCBehavior = (deltaSeconds: number) => {
|
||||
// 累积时间
|
||||
npcBehaviorCounter += deltaSeconds
|
||||
npcBehaviorCounter.value += deltaSeconds
|
||||
|
||||
// 只在达到更新间隔时才执行
|
||||
if (npcBehaviorCounter < NPC_BEHAVIOR_INTERVAL) {
|
||||
if (npcBehaviorCounter.value < NPC_BEHAVIOR_INTERVAL) {
|
||||
return
|
||||
}
|
||||
|
||||
// 如果没有NPC,直接返回
|
||||
if (npcStore.npcs.length === 0) {
|
||||
npcBehaviorCounter = 0
|
||||
npcBehaviorCounter.value = 0
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1276,127 +1464,90 @@
|
||||
// 合并玩家星球和NPC星球到allPlanets(NPC需要能够侦查和攻击玩家星球)
|
||||
const allPlanets = [...gameStore.player.planets, ...Object.values(universeStore.planets)]
|
||||
|
||||
// 更新每个NPC的行为
|
||||
// 计算当前所有正在进行的侦查和攻击任务数量
|
||||
let activeSpyMissions = 0
|
||||
let activeAttackMissions = 0
|
||||
npcStore.npcs.forEach(npc => {
|
||||
npcBehaviorLogic.updateNPCBehavior(npc, gameStore.player, allPlanets, universeStore.debrisFields, now)
|
||||
if (npc.fleetMissions) {
|
||||
npc.fleetMissions.forEach(mission => {
|
||||
if (mission.status === 'outbound') {
|
||||
if (mission.missionType === 'spy') {
|
||||
activeSpyMissions++
|
||||
} else if (mission.missionType === 'attack') {
|
||||
activeAttackMissions++
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
npcBehaviorCounter = 0
|
||||
}
|
||||
// 获取并发限制配置
|
||||
const config = npcBehaviorLogic.calculateDynamicBehavior(gameStore.player.points)
|
||||
|
||||
// 游戏循环定时器
|
||||
let gameLoop: ReturnType<typeof setInterval> | null = null
|
||||
let pointsUpdateInterval: ReturnType<typeof setInterval> | null = null
|
||||
let konamiCleanup: (() => void) | null = null
|
||||
let versionCheckInterval: ReturnType<typeof setInterval> | null = null
|
||||
// 更新每个NPC的行为(随机顺序,避免总是优先处理同一批NPC)
|
||||
const shuffledNpcs = [...npcStore.npcs].sort(() => Math.random() - 0.5)
|
||||
shuffledNpcs.forEach(npc => {
|
||||
// 在更新前检查当前并发数,如果已达上限则跳过该NPC
|
||||
npcBehaviorLogic.updateNPCBehaviorWithLimit(npc, gameStore.player, allPlanets, universeStore.debrisFields, now, {
|
||||
activeSpyMissions,
|
||||
activeAttackMissions,
|
||||
config
|
||||
})
|
||||
|
||||
// 重新计算当前并发数(因为可能新增了任务)
|
||||
activeSpyMissions = 0
|
||||
activeAttackMissions = 0
|
||||
npcStore.npcs.forEach(n => {
|
||||
if (n.fleetMissions) {
|
||||
n.fleetMissions.forEach(mission => {
|
||||
if (mission.status === 'outbound') {
|
||||
if (mission.missionType === 'spy') activeSpyMissions++
|
||||
else if (mission.missionType === 'attack') activeAttackMissions++
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
npcBehaviorCounter.value = 0
|
||||
}
|
||||
|
||||
// 启动游戏循环
|
||||
const startGameLoop = () => {
|
||||
if (gameStore.isPaused) return
|
||||
// 清理旧的定时器
|
||||
if (gameLoop) {
|
||||
clearInterval(gameLoop)
|
||||
if (gameLoop.value) {
|
||||
clearInterval(gameLoop.value)
|
||||
}
|
||||
// 根据游戏速度计算间隔时间
|
||||
const interval = getGameLoopIntervalMs(gameStore.gameSpeed)
|
||||
// 游戏循环固定为1秒,避免高倍速时的卡顿
|
||||
// gameSpeed 只作用于资源产出和时间消耗的倍率
|
||||
const interval = 1000
|
||||
// 启动新的游戏循环
|
||||
gameLoop = setInterval(() => {
|
||||
gameLoop.value = setInterval(() => {
|
||||
updateGame()
|
||||
}, interval)
|
||||
}
|
||||
|
||||
// 停止游戏循环
|
||||
const stopGameLoop = () => {
|
||||
if (gameLoop.value) {
|
||||
clearInterval(gameLoop.value)
|
||||
gameLoop.value = null
|
||||
}
|
||||
}
|
||||
|
||||
// 启动积分更新定时器(每10秒更新一次)
|
||||
const startPointsUpdate = () => {
|
||||
if (pointsUpdateInterval) {
|
||||
clearInterval(pointsUpdateInterval)
|
||||
if (pointsUpdateInterval.value) {
|
||||
clearInterval(pointsUpdateInterval.value)
|
||||
}
|
||||
pointsUpdateInterval = setInterval(() => {
|
||||
pointsUpdateInterval.value = setInterval(() => {
|
||||
if (!gameStore.isPaused) {
|
||||
gameStore.player.points = publicLogic.calculatePlayerPoints(gameStore.player)
|
||||
}
|
||||
}, 10000) // 10秒更新一次
|
||||
}
|
||||
|
||||
// 监听游戏速度变化,重新启动游戏循环
|
||||
watch(
|
||||
() => gameStore.gameSpeed,
|
||||
() => {
|
||||
if (gameLoop) {
|
||||
startGameLoop()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 初始化游戏
|
||||
onMounted(async () => {
|
||||
// 如果是首次访问(没有星球数据),使用浏览器语言自动检测
|
||||
const isFirstVisit = gameStore.player.planets.length === 0
|
||||
if (isFirstVisit) {
|
||||
gameStore.locale = detectBrowserLocale()
|
||||
}
|
||||
await initGame()
|
||||
// 启动游戏循环
|
||||
startGameLoop()
|
||||
// 启动积分更新定时器
|
||||
startPointsUpdate()
|
||||
// 启动科乐美秘籍监听
|
||||
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
|
||||
})
|
||||
if (versionInfo) {
|
||||
updateInfo.value = versionInfo
|
||||
toast.info(t('settings.newVersionAvailable', { version: versionInfo.version }), {
|
||||
duration: Infinity,
|
||||
dismissible: true,
|
||||
action: {
|
||||
label: t('settings.viewUpdate'),
|
||||
onClick: () => {
|
||||
showUpdateDialog.value = true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
// 启动版本检查定时器(每5分钟被动检查一次)
|
||||
versionCheckInterval = setInterval(async () => {
|
||||
const versionInfo = await checkLatestVersion(gameStore.player.lastVersionCheckTime || 0, (time: number) => {
|
||||
gameStore.player.lastVersionCheckTime = time
|
||||
})
|
||||
if (versionInfo) {
|
||||
updateInfo.value = versionInfo
|
||||
toast.info(t('settings.newVersionAvailable', { version: versionInfo.version }), {
|
||||
duration: Infinity,
|
||||
dismissible: true,
|
||||
action: {
|
||||
label: t('settings.viewUpdate'),
|
||||
onClick: () => {
|
||||
showUpdateDialog.value = true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}, 5 * 60 * 1000)
|
||||
})
|
||||
|
||||
// 清理定时器
|
||||
onUnmounted(() => {
|
||||
if (gameLoop) clearInterval(gameLoop)
|
||||
if (pointsUpdateInterval) clearInterval(pointsUpdateInterval)
|
||||
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)
|
||||
@@ -1438,32 +1589,25 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 定义 planet computed(需要在 watch 之前定义)
|
||||
const planet = computed(() => gameStore.currentPlanet)
|
||||
// 打开重命名对话框
|
||||
const openRenameDialog = (planetId: string, currentName: string) => {
|
||||
renamingPlanetId.value = planetId
|
||||
newPlanetName.value = currentName
|
||||
renameDialogOpen.value = true
|
||||
}
|
||||
|
||||
const navItems = computed(() => [
|
||||
{ name: computed(() => t('nav.overview')), path: '/', icon: Home },
|
||||
{ name: computed(() => t('nav.buildings')), path: '/buildings', icon: Building2 },
|
||||
{ name: computed(() => t('nav.research')), path: '/research', icon: FlaskConical },
|
||||
{ name: computed(() => t('nav.shipyard')), path: '/shipyard', icon: Ship },
|
||||
{ name: computed(() => t('nav.defense')), path: '/defense', icon: Shield },
|
||||
{ name: computed(() => t('nav.fleet')), path: '/fleet', icon: Rocket },
|
||||
{ name: computed(() => t('nav.officers')), path: '/officers', icon: Users },
|
||||
{ name: computed(() => t('nav.simulator')), path: '/battle-simulator', icon: Swords },
|
||||
{ name: computed(() => t('nav.galaxy')), path: '/galaxy', icon: Globe },
|
||||
{ name: computed(() => t('nav.diplomacy')), path: '/diplomacy', icon: Handshake },
|
||||
{ name: computed(() => t('nav.messages')), path: '/messages', icon: Mail },
|
||||
{ name: computed(() => t('nav.settings')), path: '/settings', icon: Settings },
|
||||
// GM菜单在启用GM模式时显示
|
||||
...(gameStore.player.isGMEnabled ? [{ name: computed(() => t('nav.gm')), path: '/gm', icon: Wrench }] : [])
|
||||
])
|
||||
// 确认重命名
|
||||
const confirmRenamePlanet = () => {
|
||||
if (!renamingPlanetId.value || !newPlanetName.value.trim()) return
|
||||
|
||||
// 功能解锁要求配置
|
||||
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 targetPlanet = gameStore.player.planets.find(p => p.id === renamingPlanetId.value)
|
||||
if (targetPlanet) {
|
||||
targetPlanet.name = newPlanetName.value.trim()
|
||||
}
|
||||
|
||||
renameDialogOpen.value = false
|
||||
renamingPlanetId.value = null
|
||||
newPlanetName.value = ''
|
||||
}
|
||||
|
||||
// 检查功能是否解锁
|
||||
@@ -1508,78 +1652,6 @@
|
||||
router.push(path)
|
||||
}
|
||||
|
||||
// 使用直接计算,不再缓存
|
||||
const production = computed(() => {
|
||||
if (!planet.value) return null
|
||||
const now = Date.now()
|
||||
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, now)
|
||||
const base = resourceLogic.calculateResourceProduction(planet.value, {
|
||||
resourceProductionBonus: bonuses.resourceProductionBonus,
|
||||
darkMatterProductionBonus: bonuses.darkMatterProductionBonus,
|
||||
energyProductionBonus: bonuses.energyProductionBonus
|
||||
})
|
||||
return scaleResources(base, gameStore.gameSpeed)
|
||||
})
|
||||
|
||||
const capacity = computed(() => {
|
||||
if (!planet.value) return null
|
||||
const now = Date.now()
|
||||
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, now)
|
||||
return resourceLogic.calculateResourceCapacity(planet.value, bonuses.storageCapacityBonus)
|
||||
})
|
||||
|
||||
// 电力消耗
|
||||
const energyConsumption = computed(() => {
|
||||
if (!planet.value) return 0
|
||||
return scaleNumber(resourceLogic.calculateEnergyConsumption(planet.value), gameStore.gameSpeed)
|
||||
})
|
||||
|
||||
// 净电力(产量 - 消耗)
|
||||
const netEnergy = computed(() => {
|
||||
if (!planet.value || !production.value) return 0
|
||||
return production.value.energy - energyConsumption.value
|
||||
})
|
||||
|
||||
// 未读消息数量
|
||||
const unreadMessagesCount = computed(() => {
|
||||
const unreadBattles = gameStore.player.battleReports.filter(r => !r.read).length
|
||||
const unreadSpies = gameStore.player.spyReports.filter(r => !r.read).length
|
||||
const unreadSpied = gameStore.player.spiedNotifications?.filter(n => !n.read).length || 0
|
||||
const unreadMissions = gameStore.player.missionReports?.filter(r => !r.read).length || 0
|
||||
const unreadNPCActivity = gameStore.player.npcActivityNotifications?.filter(n => !n.read).length || 0
|
||||
const unreadGifts = gameStore.player.giftNotifications?.filter(n => !n.read).length || 0
|
||||
const unreadGiftRejected = gameStore.player.giftRejectedNotifications?.filter(n => !n.read).length || 0
|
||||
return unreadBattles + unreadSpies + unreadSpied + unreadMissions + unreadNPCActivity + unreadGifts + unreadGiftRejected
|
||||
})
|
||||
|
||||
// 正在执行的舰队任务数量(包括飞行中的导弹)
|
||||
const activeFleetMissionsCount = computed(() => {
|
||||
const fleetMissions = gameStore.player.fleetMissions.filter(m => m.status === 'outbound' || m.status === 'returning').length
|
||||
const flyingMissiles = gameStore.player.missileAttacks?.filter(m => m.status === 'flying').length || 0
|
||||
return fleetMissions + flyingMissiles
|
||||
})
|
||||
|
||||
// 未读外交报告数量
|
||||
const unreadDiplomaticReportsCount = computed(() => {
|
||||
return (gameStore.player.diplomaticReports || []).filter(r => !r.read).length
|
||||
})
|
||||
|
||||
// 资源类型配置
|
||||
const resourceTypes = [
|
||||
{ key: 'metal' as const },
|
||||
{ key: 'crystal' as const },
|
||||
{ key: 'deuterium' as const },
|
||||
{ key: 'energy' as const },
|
||||
{ key: 'darkMatter' as const }
|
||||
]
|
||||
|
||||
// 月球相关
|
||||
const moon = computed(() => {
|
||||
if (!planet.value || planet.value.isMoon) return null
|
||||
return gameStore.getMoonForPlanet(planet.value.id)
|
||||
})
|
||||
const hasMoon = computed(() => !!moon.value)
|
||||
|
||||
// 切换到月球
|
||||
const switchToMoon = () => {
|
||||
if (moon.value) {
|
||||
@@ -1606,19 +1678,6 @@
|
||||
|
||||
// 处理侧边栏打开/关闭状态变化
|
||||
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
|
||||
}
|
||||
}
|
||||
// 其他情况正常更新
|
||||
sidebarOpen.value = open
|
||||
}
|
||||
|
||||
@@ -1657,6 +1716,92 @@
|
||||
}
|
||||
confirmDialogOpen.value = true
|
||||
}
|
||||
|
||||
// 监听暂停状态变化
|
||||
watch(
|
||||
() => gameStore.isPaused,
|
||||
isPaused => {
|
||||
if (isPaused) {
|
||||
stopGameLoop()
|
||||
} else {
|
||||
startGameLoop()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 初始化游戏
|
||||
onMounted(async () => {
|
||||
try {
|
||||
// 如果是首次访问(没有星球数据),使用浏览器语言自动检测
|
||||
const isFirstVisit = gameStore.player.planets.length === 0
|
||||
if (isFirstVisit) {
|
||||
gameStore.locale = detectBrowserLocale()
|
||||
}
|
||||
await initGame()
|
||||
// 启动游戏循环
|
||||
startGameLoop()
|
||||
// 启动积分更新定时器
|
||||
startPointsUpdate()
|
||||
// 启动科乐美秘籍监听
|
||||
konamiCleanup.value = setupKonamiCode()
|
||||
|
||||
// 添加队列取消事件监听
|
||||
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
|
||||
})
|
||||
if (versionInfo) {
|
||||
updateInfo.value = versionInfo
|
||||
toast.info(t('settings.newVersionAvailable', { version: versionInfo.version }), {
|
||||
duration: Infinity,
|
||||
dismissible: true,
|
||||
action: {
|
||||
label: t('settings.viewUpdate'),
|
||||
onClick: () => {
|
||||
showUpdateDialog.value = true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
// 启动版本检查定时器(每5分钟被动检查一次)
|
||||
versionCheckInterval.value = setInterval(async () => {
|
||||
const versionInfo = await checkLatestVersion(gameStore.player.lastVersionCheckTime || 0, (time: number) => {
|
||||
gameStore.player.lastVersionCheckTime = time
|
||||
})
|
||||
if (versionInfo) {
|
||||
updateInfo.value = versionInfo
|
||||
toast.info(t('settings.newVersionAvailable', { version: versionInfo.version }), {
|
||||
duration: Infinity,
|
||||
dismissible: true,
|
||||
action: {
|
||||
label: t('settings.viewUpdate'),
|
||||
onClick: () => {
|
||||
showUpdateDialog.value = true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}, 5 * 60 * 1000)
|
||||
} catch (error) {
|
||||
console.error('Error during game initialization:', error)
|
||||
// 即使初始化失败,也尝试启动基本的游戏循环
|
||||
startGameLoop()
|
||||
}
|
||||
})
|
||||
|
||||
// 清理定时器
|
||||
onUnmounted(() => {
|
||||
if (gameLoop.value) clearInterval(gameLoop.value)
|
||||
if (pointsUpdateInterval.value) clearInterval(pointsUpdateInterval.value)
|
||||
if (konamiCleanup.value) konamiCleanup.value()
|
||||
if (versionCheckInterval.value) clearInterval(versionCheckInterval.value)
|
||||
// 移除队列取消事件监听
|
||||
window.removeEventListener('cancel-build', handleCancelBuildEvent as EventListener)
|
||||
window.removeEventListener('cancel-research', handleCancelResearchEvent as EventListener)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -76,6 +76,7 @@
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
@@ -84,9 +85,11 @@
|
||||
html {
|
||||
color-scheme: light dark;
|
||||
}
|
||||
|
||||
html.dark {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
html.light {
|
||||
color-scheme: light;
|
||||
}
|
||||
48
src/components/BackToTop.vue
Normal file
48
src/components/BackToTop.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<Transition
|
||||
enter-active-class="transition-all duration-300 ease-out"
|
||||
enter-from-class="translate-y-4 opacity-0"
|
||||
enter-to-class="translate-y-0 opacity-100"
|
||||
leave-active-class="transition-all duration-200 ease-in"
|
||||
leave-from-class="translate-y-0 opacity-100"
|
||||
leave-to-class="translate-y-4 opacity-0"
|
||||
>
|
||||
<Button v-if="isVisible" variant="outline" size="icon" @click="scrollToTop">
|
||||
<ChevronUp class="h-4 w-4" />
|
||||
</Button>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { ChevronUp } from 'lucide-vue-next'
|
||||
|
||||
// 显示阈值(滚动超过这个距离才显示按钮)
|
||||
const SCROLL_THRESHOLD = 300
|
||||
|
||||
const isVisible = ref(false)
|
||||
|
||||
// 监听滚动事件
|
||||
const handleScroll = () => {
|
||||
isVisible.value = window.scrollY > SCROLL_THRESHOLD
|
||||
}
|
||||
|
||||
// 丝滑返回顶部
|
||||
const scrollToTop = () => {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth'
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('scroll', handleScroll, { passive: true })
|
||||
// 初始检查
|
||||
handleScroll()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('scroll', handleScroll)
|
||||
})
|
||||
</script>
|
||||
@@ -40,15 +40,9 @@
|
||||
</div>
|
||||
|
||||
<!-- 胜利者 -->
|
||||
<div class="text-center p-4 rounded-lg" :class="getWinnerStyle(report.winner)">
|
||||
<div class="text-center p-4 rounded-lg" :class="getPlayerResultStyle()">
|
||||
<p class="text-lg font-bold">
|
||||
{{
|
||||
report.winner === 'attacker'
|
||||
? t('messagesView.victory')
|
||||
: report.winner === 'defender'
|
||||
? t('messagesView.defeat')
|
||||
: t('messagesView.draw')
|
||||
}}
|
||||
{{ report.winner === 'draw' ? t('messagesView.draw') : isPlayerVictory ? t('messagesView.victory') : t('messagesView.defeat') }}
|
||||
</p>
|
||||
<p v-if="report.rounds" class="text-sm mt-1">{{ t('simulatorView.afterRounds').replace('{rounds}', String(report.rounds)) }}</p>
|
||||
</div>
|
||||
@@ -92,29 +86,32 @@
|
||||
</div>
|
||||
|
||||
<!-- 剩余单位 -->
|
||||
<div v-if="report.attackerRemaining || report.defenderRemaining" class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div v-if="hasAnyRemaining" class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- 攻击方剩余 -->
|
||||
<div v-if="report.attackerRemaining && Object.keys(report.attackerRemaining).length > 0" class="space-y-2">
|
||||
<div class="space-y-2">
|
||||
<p class="text-sm font-medium text-blue-600 dark:text-blue-400">{{ t('messagesView.attackerRemaining') }}</p>
|
||||
<div class="p-3 bg-muted rounded-lg space-y-1 text-xs">
|
||||
<template v-if="report.attackerRemaining && Object.keys(report.attackerRemaining).length > 0">
|
||||
<div v-for="(count, shipType) in report.attackerRemaining" :key="shipType">
|
||||
<span class="text-muted-foreground">{{ SHIPS[shipType].name }}:</span>
|
||||
<span class="ml-2 font-medium">{{ count }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<p v-else class="text-muted-foreground">{{ t('messagesView.allDestroyed') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 防守方剩余 -->
|
||||
<div
|
||||
<div class="space-y-2">
|
||||
<p class="text-sm font-medium text-blue-600 dark:text-blue-400">{{ t('messagesView.defenderRemaining') }}</p>
|
||||
<div class="p-3 bg-muted rounded-lg space-y-1 text-xs">
|
||||
<template
|
||||
v-if="
|
||||
report.defenderRemaining &&
|
||||
(Object.keys(report.defenderRemaining.fleet || {}).length > 0 ||
|
||||
Object.keys(report.defenderRemaining.defense || {}).length > 0)
|
||||
"
|
||||
class="space-y-2"
|
||||
>
|
||||
<p class="text-sm font-medium text-blue-600 dark:text-blue-400">{{ t('messagesView.defenderRemaining') }}</p>
|
||||
<div class="p-3 bg-muted rounded-lg space-y-1 text-xs">
|
||||
<div v-for="(count, shipType) in report.defenderRemaining.fleet" :key="shipType">
|
||||
<span class="text-muted-foreground">{{ SHIPS[shipType].name }}:</span>
|
||||
<span class="ml-2 font-medium">{{ count }}</span>
|
||||
@@ -123,19 +120,19 @@
|
||||
<span class="text-muted-foreground">{{ DEFENSES[defenseType].name }}:</span>
|
||||
<span class="ml-2 font-medium">{{ count }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<p v-else class="text-muted-foreground">{{ t('messagesView.allDestroyed') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 战利品和残骸 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- 掠夺资源 -->
|
||||
<div
|
||||
v-if="report.plunder && (report.plunder.metal > 0 || report.plunder.crystal > 0 || report.plunder.deuterium > 0)"
|
||||
class="p-3 bg-green-50 dark:bg-green-950 rounded-lg"
|
||||
>
|
||||
<p class="text-sm font-medium mb-2 text-green-600 dark:text-green-400">{{ t('messagesView.plunder') }}</p>
|
||||
<div class="flex flex-wrap gap-3 text-xs">
|
||||
<div class="flex flex-wrap gap-3 text-xs justify-center">
|
||||
<span v-if="report.plunder.metal > 0" class="flex items-center gap-1">
|
||||
<ResourceIcon type="metal" size="sm" />
|
||||
{{ formatNumber(report.plunder.metal) }}
|
||||
@@ -154,10 +151,10 @@
|
||||
<!-- 残骸场 -->
|
||||
<div
|
||||
v-if="report.debrisField && (report.debrisField.metal > 0 || report.debrisField.crystal > 0)"
|
||||
class="p-3 bg-muted rounded-lg"
|
||||
class="text-center p-4 bg-muted rounded-lg"
|
||||
>
|
||||
<p class="text-sm font-medium mb-2">{{ t('messagesView.debrisField') }}</p>
|
||||
<div class="flex flex-wrap gap-3 text-xs">
|
||||
<div class="flex flex-wrap gap-3 text-xs justify-center">
|
||||
<span v-if="report.debrisField.metal > 0" class="flex items-center gap-1">
|
||||
<ResourceIcon type="metal" size="sm" />
|
||||
{{ formatNumber(report.debrisField.metal) }}
|
||||
@@ -172,7 +169,6 @@
|
||||
{{ t('messagesView.moonChance') }}: {{ (report.moonChance * 100).toFixed(1) }}%
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 回合详情 -->
|
||||
<div v-if="report.roundDetails && report.roundDetails.length > 0" class="space-y-2">
|
||||
@@ -301,7 +297,11 @@
|
||||
// 获取攻击方星球信息
|
||||
const attackerPlanet = computed(() => {
|
||||
if (!props.report) return null
|
||||
return gameStore.player.planets.find(p => p.id === props.report!.attackerPlanetId)
|
||||
// 先从玩家星球中查找
|
||||
const playerPlanet = gameStore.player.planets.find(p => p.id === props.report!.attackerPlanetId)
|
||||
if (playerPlanet) return playerPlanet
|
||||
// 再从宇宙星球地图中查找(包括 NPC 星球)
|
||||
return Object.values(universeStore.planets).find(p => p.id === props.report!.attackerPlanetId)
|
||||
})
|
||||
|
||||
// 获取防守方星球信息
|
||||
@@ -310,10 +310,35 @@
|
||||
// 先从玩家星球中查找
|
||||
const playerPlanet = gameStore.player.planets.find(p => p.id === props.report!.defenderPlanetId)
|
||||
if (playerPlanet) return playerPlanet
|
||||
// 再从宇宙星球地图中查找
|
||||
// 再从宇宙星球地图中查找(包括 NPC 星球)
|
||||
return Object.values(universeStore.planets).find(p => p.id === props.report!.defenderPlanetId)
|
||||
})
|
||||
|
||||
// 判断玩家是攻击方还是防守方
|
||||
const isPlayerAttacker = computed(() => {
|
||||
if (!props.report) return false
|
||||
return gameStore.player.planets.some(p => p.id === props.report!.attackerPlanetId)
|
||||
})
|
||||
|
||||
// 判断玩家是否胜利
|
||||
const isPlayerVictory = computed(() => {
|
||||
if (!props.report) return false
|
||||
if (props.report.winner === 'draw') return false
|
||||
// 玩家是攻击方且攻击方胜利,或者玩家是防守方且防守方胜利
|
||||
return (isPlayerAttacker.value && props.report.winner === 'attacker') || (!isPlayerAttacker.value && props.report.winner === 'defender')
|
||||
})
|
||||
|
||||
// 判断是否有任何剩余单位需要显示
|
||||
const hasAnyRemaining = computed(() => {
|
||||
if (!props.report) return false
|
||||
const hasAttackerRemaining = props.report.attackerRemaining && Object.keys(props.report.attackerRemaining).length > 0
|
||||
const hasDefenderRemaining =
|
||||
props.report.defenderRemaining &&
|
||||
(Object.keys(props.report.defenderRemaining.fleet || {}).length > 0 ||
|
||||
Object.keys(props.report.defenderRemaining.defense || {}).length > 0)
|
||||
return hasAttackerRemaining || hasDefenderRemaining
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.open,
|
||||
newValue => {
|
||||
@@ -328,10 +353,11 @@
|
||||
emit('update:open', newValue)
|
||||
})
|
||||
|
||||
// 获取胜利者样式
|
||||
const getWinnerStyle = (winner: string) => {
|
||||
if (winner === 'attacker') return 'bg-green-50 dark:bg-green-950 text-green-700 dark:text-green-300'
|
||||
if (winner === 'defender') return 'bg-red-50 dark:bg-red-950 text-red-700 dark:text-red-300'
|
||||
return 'bg-gray-50 dark:bg-gray-950 text-gray-700 dark:text-gray-300'
|
||||
// 获取玩家战斗结果样式
|
||||
const getPlayerResultStyle = () => {
|
||||
if (!props.report) return 'bg-gray-50 dark:bg-gray-950 text-gray-700 dark:text-gray-300'
|
||||
if (props.report.winner === 'draw') return 'bg-gray-50 dark:bg-gray-950 text-gray-700 dark:text-gray-300'
|
||||
if (isPlayerVictory.value) return 'bg-green-50 dark:bg-green-950 text-green-700 dark:text-green-300'
|
||||
return 'bg-red-50 dark:bg-red-950 text-red-700 dark:text-red-300'
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
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 }}
|
||||
{{ unreadCount }}
|
||||
</Badge>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
@@ -20,38 +20,51 @@
|
||||
</Button>
|
||||
</div>
|
||||
<ScrollArea class="h-96">
|
||||
<div v-if="reports.length === 0" class="p-8 text-center text-muted-foreground">
|
||||
{{ t('diplomacy.noReports') }}
|
||||
</div>
|
||||
<Empty v-if="reports.length === 0" class="border-0">
|
||||
<EmptyContent>
|
||||
<ScrollText class="h-10 w-10 text-muted-foreground" />
|
||||
<EmptyDescription>{{ t('diplomacy.noReports') }}</EmptyDescription>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
<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="p-3 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 class="flex items-center gap-3">
|
||||
<!-- 左侧:事件图标 -->
|
||||
<div class="flex-shrink-0">
|
||||
<component :is="getEventIcon(report.eventType)" class="h-5 w-5" :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">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium text-sm truncate">{{ report.npcName }}</span>
|
||||
<Badge :variant="getStatusBadgeVariant(report.newStatus)" class="text-xs flex-shrink-0">
|
||||
{{ getStatusText(report.newStatus) }}
|
||||
</Badge>
|
||||
<span v-if="!report.read" class="ml-auto">
|
||||
<Badge variant="destructive" class="h-2 w-2 p-0 rounded-full" />
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">
|
||||
{{ getEventTypeText(report.eventType) }}
|
||||
</p>
|
||||
</div>
|
||||
<!-- 右侧:好感度变化和时间 -->
|
||||
<div class="flex-shrink-0 text-right">
|
||||
<span
|
||||
class="text-sm font-bold block"
|
||||
:class="report.reputationChange >= 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'"
|
||||
>
|
||||
{{ report.reputationChange >= 0 ? '+' : '' }}{{ report.reputationChange }}
|
||||
</span>
|
||||
<span class="text-[10px] text-muted-foreground">
|
||||
{{ formatRelativeTime((Date.now() - report.timestamp) / 1000, t) }}{{ t('diplomacy.ago') }}
|
||||
</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>
|
||||
<!-- 未读标记 -->
|
||||
<span v-if="!report.read" class="h-2 w-2 rounded-full bg-destructive flex-shrink-0" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -77,6 +90,9 @@
|
||||
/>
|
||||
{{ t('diplomacy.reportDetails') }}
|
||||
</DialogTitle>
|
||||
<DialogDescription class="sr-only">
|
||||
{{ t('diplomacy.reportDetails') }}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div v-if="selectedReport" class="space-y-4">
|
||||
@@ -183,9 +199,10 @@
|
||||
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 { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { ScrollText, Gift, Sword, Eye, Trash2, Skull } from 'lucide-vue-next'
|
||||
import { Empty, EmptyContent, EmptyDescription } from '@/components/ui/empty'
|
||||
import { RelationStatus, DiplomaticEventType } from '@/types/game'
|
||||
import type { DiplomaticReport } from '@/types/game'
|
||||
import { formatRelativeTime } from '@/utils/format'
|
||||
@@ -241,6 +258,25 @@
|
||||
}
|
||||
}
|
||||
|
||||
const getEventTypeText = (eventType: DiplomaticReport['eventType']) => {
|
||||
switch (eventType) {
|
||||
case DiplomaticEventType.GiftResources:
|
||||
return t('diplomacy.eventType.gift')
|
||||
case DiplomaticEventType.Attack:
|
||||
return t('diplomacy.eventType.attack')
|
||||
case DiplomaticEventType.AllyAttacked:
|
||||
return t('diplomacy.eventType.allyAttacked')
|
||||
case DiplomaticEventType.Spy:
|
||||
return t('diplomacy.eventType.spy')
|
||||
case DiplomaticEventType.StealDebris:
|
||||
return t('diplomacy.eventType.stealDebris')
|
||||
case DiplomaticEventType.DestroyPlanet:
|
||||
return t('diplomacy.eventType.destroyPlanet')
|
||||
default:
|
||||
return t('diplomacy.eventType.unknown')
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusBadgeVariant = (status: RelationStatus) => {
|
||||
switch (status) {
|
||||
case RelationStatus.Hostile:
|
||||
@@ -275,11 +311,12 @@
|
||||
const handleReportClick = (report: DiplomaticReport) => {
|
||||
// 标记为已读
|
||||
report.read = true
|
||||
// 设置选中的报告并打开详情对话框
|
||||
// 设置选中的报告
|
||||
selectedReport.value = report
|
||||
detailDialogOpen.value = true
|
||||
// 关闭通知面板
|
||||
isOpen.value = false
|
||||
isOpen.value = true
|
||||
// 打开对话框
|
||||
detailDialogOpen.value = true
|
||||
}
|
||||
|
||||
const markAllAsRead = () => {
|
||||
@@ -294,7 +331,8 @@
|
||||
}
|
||||
|
||||
const goToDiplomacyFromDialog = () => {
|
||||
const npcId = selectedReport.value?.npcId
|
||||
detailDialogOpen.value = false
|
||||
router.push('/diplomacy')
|
||||
router.push(npcId ? `/diplomacy?npcId=${npcId}` : '/diplomacy')
|
||||
}
|
||||
</script>
|
||||
|
||||
304
src/components/EnemyAlertNotifications.vue
Normal file
304
src/components/EnemyAlertNotifications.vue
Normal file
@@ -0,0 +1,304 @@
|
||||
<template>
|
||||
<Popover v-model:open="isOpen">
|
||||
<PopoverTrigger as-child>
|
||||
<Button variant="outline" size="icon" class="relative">
|
||||
<Siren class="h-4 w-4" />
|
||||
<Badge
|
||||
v-if="activeAlerts.length > 0"
|
||||
variant="destructive"
|
||||
class="absolute -top-1 -right-1 h-5 w-5 p-0 flex items-center justify-center text-xs animate-pulse"
|
||||
>
|
||||
{{ activeAlerts.length }}
|
||||
</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('enemyAlert.title') }}</h3>
|
||||
<Button v-if="activeAlerts.length > 0" variant="ghost" size="sm" @click="markAllAsRead">
|
||||
{{ t('enemyAlert.markAllRead') }}
|
||||
</Button>
|
||||
</div>
|
||||
<ScrollArea class="h-96">
|
||||
<Empty v-if="activeAlerts.length === 0" class="border-0">
|
||||
<EmptyContent>
|
||||
<Shield class="h-10 w-10 text-muted-foreground" />
|
||||
<EmptyDescription>{{ t('enemyAlert.noAlerts') }}</EmptyDescription>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
<div v-else class="divide-y">
|
||||
<div
|
||||
v-for="alert in activeAlerts"
|
||||
:key="alert.id"
|
||||
class="p-3 hover:bg-muted/50 cursor-pointer transition-colors"
|
||||
:class="{ 'bg-destructive/10': !alert.read }"
|
||||
@click="handleAlertClick(alert)"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<!-- 左侧:任务图标 -->
|
||||
<div class="flex-shrink-0">
|
||||
<component :is="getMissionIcon(alert.missionType)" class="h-5 w-5" :class="getMissionIconColor(alert.missionType)" />
|
||||
</div>
|
||||
<!-- 中间:主要信息 -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium text-sm truncate">{{ alert.npcName }}</span>
|
||||
<Badge :variant="getMissionBadgeVariant(alert.missionType)" class="text-xs flex-shrink-0">
|
||||
{{ getMissionTypeText(alert.missionType) }}
|
||||
</Badge>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">
|
||||
{{ alert.targetPlanetName }} · {{ t('enemyAlert.fleetSize') }}: {{ alert.fleetSize }}
|
||||
</p>
|
||||
</div>
|
||||
<!-- 右侧:倒计时 -->
|
||||
<div class="flex-shrink-0 text-right">
|
||||
<span class="text-sm font-bold block" :class="getRemainingTimeColor(alert)">
|
||||
{{ formatRemainingTime(alert) }}
|
||||
</span>
|
||||
</div>
|
||||
<!-- 未读标记 -->
|
||||
<span v-if="!alert.read" class="h-2 w-2 rounded-full bg-destructive flex-shrink-0 animate-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
<div v-if="activeAlerts.length > 0" class="p-2 border-t">
|
||||
<Button variant="ghost" size="sm" class="w-full" @click="goToFleet">
|
||||
{{ t('enemyAlert.viewFleet') }}
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<!-- 警报详情对话框 -->
|
||||
<Dialog :open="detailDialogOpen" @update:open="detailDialogOpen = $event">
|
||||
<DialogContent class="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle class="flex items-center gap-2">
|
||||
<component
|
||||
v-if="selectedAlert"
|
||||
:is="getMissionIcon(selectedAlert.missionType)"
|
||||
class="h-5 w-5"
|
||||
:class="getMissionIconColor(selectedAlert.missionType)"
|
||||
/>
|
||||
{{ t('enemyAlert.alertDetails') }}
|
||||
</DialogTitle>
|
||||
<DialogDescription class="sr-only">
|
||||
{{ t('enemyAlert.alertDetails') }}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div v-if="selectedAlert" class="space-y-4">
|
||||
<!-- 敌方信息 -->
|
||||
<div class="flex items-center gap-3 p-4 bg-destructive/10 rounded-lg">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<h3 class="font-semibold text-lg">{{ selectedAlert.npcName }}</h3>
|
||||
<Badge :variant="getMissionBadgeVariant(selectedAlert.missionType)">
|
||||
{{ getMissionTypeText(selectedAlert.missionType) }}
|
||||
</Badge>
|
||||
</div>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{{ t('enemyAlert.fleetSize') }}: {{ selectedAlert.fleetSize }} {{ t('enemyAlert.ships') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 目标信息 -->
|
||||
<div class="space-y-2">
|
||||
<h4 class="font-semibold text-sm">{{ t('enemyAlert.targetInfo') }}</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">{{ selectedAlert.targetPlanetName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 到达时间 -->
|
||||
<div class="space-y-2">
|
||||
<h4 class="font-semibold text-sm">{{ t('enemyAlert.arrivalTime') }}</h4>
|
||||
<div class="p-3 bg-muted/30 rounded-md">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-muted-foreground">{{ t('enemyAlert.countdown') }}</span>
|
||||
<span class="font-bold text-lg" :class="getRemainingTimeColor(selectedAlert)">
|
||||
{{ formatRemainingTime(selectedAlert) }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground mt-1">
|
||||
{{ formatDate(selectedAlert.arrivalTime) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 警告提示 -->
|
||||
<div class="p-3 bg-destructive/10 rounded-md border border-destructive/20">
|
||||
<p class="text-sm text-destructive dark:text-red-400">
|
||||
{{ getMissionWarningText(selectedAlert.missionType) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="detailDialogOpen = false">{{ t('common.close') }}</Button>
|
||||
<Button @click="goToMessagesFromDialog">{{ t('enemyAlert.viewMessages') }}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted } 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, DialogDescription, DialogFooter } from '@/components/ui/dialog'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Empty, EmptyContent, EmptyDescription } from '@/components/ui/empty'
|
||||
import { Siren, Eye, Sword, Shield, Globe } from 'lucide-vue-next'
|
||||
import { MissionType } from '@/types/game'
|
||||
import type { IncomingFleetAlert } from '@/types/game'
|
||||
import { formatDate, formatTime } from '@/utils/format'
|
||||
|
||||
const router = useRouter()
|
||||
const gameStore = useGameStore()
|
||||
const { t } = useI18n()
|
||||
const isOpen = ref(false)
|
||||
const detailDialogOpen = ref(false)
|
||||
const selectedAlert = ref<IncomingFleetAlert | null>(null)
|
||||
const currentTime = ref(Date.now())
|
||||
let timeInterval: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
// 启动计时器,用于实时更新倒计时
|
||||
onMounted(() => {
|
||||
timeInterval = setInterval(() => {
|
||||
currentTime.value = Date.now()
|
||||
}, 1000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timeInterval) {
|
||||
clearInterval(timeInterval)
|
||||
timeInterval = null
|
||||
}
|
||||
})
|
||||
|
||||
// 获取活跃的警报(未到达的)
|
||||
const activeAlerts = computed(() => {
|
||||
const now = currentTime.value
|
||||
return (gameStore.player.incomingFleetAlerts || [])
|
||||
.filter(alert => alert.arrivalTime > now)
|
||||
.sort((a, b) => a.arrivalTime - b.arrivalTime) // 按到达时间排序
|
||||
})
|
||||
|
||||
// 获取任务类型图标
|
||||
const getMissionIcon = (missionType: MissionType) => {
|
||||
switch (missionType) {
|
||||
case MissionType.Spy:
|
||||
return Eye
|
||||
case MissionType.Attack:
|
||||
return Sword
|
||||
default:
|
||||
return Siren
|
||||
}
|
||||
}
|
||||
|
||||
// 获取任务类型图标颜色
|
||||
const getMissionIconColor = (missionType: MissionType) => {
|
||||
switch (missionType) {
|
||||
case MissionType.Spy:
|
||||
return 'text-purple-500'
|
||||
case MissionType.Attack:
|
||||
return 'text-red-500'
|
||||
default:
|
||||
return 'text-yellow-500'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取任务类型Badge样式
|
||||
const getMissionBadgeVariant = (missionType: MissionType): 'destructive' | 'secondary' => {
|
||||
return missionType === MissionType.Attack ? 'destructive' : 'secondary'
|
||||
}
|
||||
|
||||
// 获取任务类型文本
|
||||
const getMissionTypeText = (missionType: MissionType) => {
|
||||
switch (missionType) {
|
||||
case MissionType.Spy:
|
||||
return t('enemyAlert.missionType.spy')
|
||||
case MissionType.Attack:
|
||||
return t('enemyAlert.missionType.attack')
|
||||
default:
|
||||
return t('enemyAlert.missionType.unknown')
|
||||
}
|
||||
}
|
||||
|
||||
// 获取任务警告文本
|
||||
const getMissionWarningText = (missionType: MissionType) => {
|
||||
switch (missionType) {
|
||||
case MissionType.Spy:
|
||||
return t('enemyAlert.warning.spy')
|
||||
case MissionType.Attack:
|
||||
return t('enemyAlert.warning.attack')
|
||||
default:
|
||||
return t('enemyAlert.warning.unknown')
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化剩余时间
|
||||
const formatRemainingTime = (alert: IncomingFleetAlert) => {
|
||||
const remaining = Math.max(0, Math.floor((alert.arrivalTime - currentTime.value) / 1000))
|
||||
if (remaining <= 0) {
|
||||
return t('enemyAlert.arrived')
|
||||
}
|
||||
return formatTime(remaining)
|
||||
}
|
||||
|
||||
// 获取剩余时间颜色
|
||||
const getRemainingTimeColor = (alert: IncomingFleetAlert) => {
|
||||
const remaining = alert.arrivalTime - currentTime.value
|
||||
if (remaining <= 0) return 'text-red-600 dark:text-red-400 font-bold' // 已到达
|
||||
if (remaining < 60000) return 'text-red-600 dark:text-red-400' // < 1分钟
|
||||
if (remaining < 300000) return 'text-orange-600 dark:text-orange-400' // < 5分钟
|
||||
return 'text-muted-foreground'
|
||||
}
|
||||
|
||||
// 处理警报点击
|
||||
const handleAlertClick = (alert: IncomingFleetAlert) => {
|
||||
alert.read = true
|
||||
selectedAlert.value = alert
|
||||
isOpen.value = true
|
||||
// 打开对话框
|
||||
detailDialogOpen.value = true
|
||||
}
|
||||
|
||||
// 标记所有为已读
|
||||
const markAllAsRead = () => {
|
||||
gameStore.player.incomingFleetAlerts?.forEach(alert => {
|
||||
alert.read = true
|
||||
})
|
||||
}
|
||||
|
||||
// 跳转到舰队页面
|
||||
const goToFleet = () => {
|
||||
isOpen.value = false
|
||||
router.push('/fleet')
|
||||
}
|
||||
|
||||
// 从对话框跳转到消息页面
|
||||
const goToMessagesFromDialog = () => {
|
||||
detailDialogOpen.value = false
|
||||
router.push('/messages')
|
||||
}
|
||||
|
||||
// 打开弹窗(供外部调用)
|
||||
const open = () => {
|
||||
isOpen.value = true
|
||||
}
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
open
|
||||
})
|
||||
</script>
|
||||
86
src/components/HintToast.vue
Normal file
86
src/components/HintToast.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<Transition
|
||||
enter-active-class="transition-all duration-300 ease-out"
|
||||
enter-from-class="-translate-y-4 opacity-0"
|
||||
enter-to-class="translate-y-0 opacity-100"
|
||||
leave-active-class="transition-all duration-200 ease-in"
|
||||
leave-from-class="translate-y-0 opacity-100"
|
||||
leave-to-class="-translate-y-4 opacity-0"
|
||||
>
|
||||
<div
|
||||
v-if="isHintVisible && currentHint"
|
||||
class="fixed top-2 right-2 max-w-[280px] sm:top-4 sm:right-4 sm:max-w-xs z-50 pointer-events-auto"
|
||||
>
|
||||
<div class="bg-card border rounded-lg shadow-lg p-3" role="alert" aria-live="polite">
|
||||
<!-- 标题栏 -->
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<component :is="getIcon(currentHint.icon)" class="h-4 w-4 text-primary flex-shrink-0" />
|
||||
<h4 class="font-medium text-sm">{{ t(currentHint.titleKey) }}</h4>
|
||||
</div>
|
||||
|
||||
<!-- 内容 -->
|
||||
<p class="text-sm text-muted-foreground mb-3 line-clamp-3">{{ t(currentHint.messageKey) }}</p>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex items-center justify-end">
|
||||
<Button size="sm" class="text-xs h-7" @click="handleDismiss">
|
||||
{{ t('hints.dontShowAgain') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useHints } from '@/composables/useHints'
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Home,
|
||||
Building,
|
||||
FlaskConical,
|
||||
Rocket,
|
||||
Plane,
|
||||
Globe,
|
||||
Handshake,
|
||||
Mail,
|
||||
Shield,
|
||||
Lightbulb,
|
||||
Users,
|
||||
Swords,
|
||||
Settings,
|
||||
Wand2
|
||||
} from 'lucide-vue-next'
|
||||
import type { Component } from 'vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { currentHint, isHintVisible, dismissHint } = useHints()
|
||||
|
||||
// 图标名称到组件的映射
|
||||
const iconMap: Record<string, Component> = {
|
||||
home: Home,
|
||||
building: Building,
|
||||
flask: FlaskConical,
|
||||
rocket: Rocket,
|
||||
plane: Plane,
|
||||
globe: Globe,
|
||||
handshake: Handshake,
|
||||
mail: Mail,
|
||||
shield: Shield,
|
||||
users: Users,
|
||||
swords: Swords,
|
||||
settings: Settings,
|
||||
wand: Wand2
|
||||
}
|
||||
|
||||
const getIcon = (iconName?: string) => {
|
||||
if (!iconName) return Lightbulb
|
||||
return iconMap[iconName] || Lightbulb
|
||||
}
|
||||
|
||||
// 不再显示 - 永久关闭
|
||||
const handleDismiss = () => {
|
||||
dismissHint(true)
|
||||
}
|
||||
</script>
|
||||
@@ -1,67 +1,40 @@
|
||||
<template>
|
||||
<div v-if="alerts.length > 0" class="bg-destructive/10 border-b border-destructive/20">
|
||||
<div class="px-4 sm:px-6 py-2 space-y-2 max-h-[230px] overflow-y-auto">
|
||||
<div
|
||||
v-for="alert in alerts"
|
||||
:key="alert.id"
|
||||
class="flex items-center justify-between gap-3 bg-destructive/5 rounded-lg px-3 py-2 border border-destructive/20"
|
||||
>
|
||||
<!-- 警告图标和信息 -->
|
||||
<div v-if="activeAlerts.length > 0" class="bg-destructive/10 border-b border-destructive/20">
|
||||
<div class="px-4 sm:px-6 py-2 flex items-center justify-between gap-3">
|
||||
<!-- 警告图标和汇总信息 -->
|
||||
<div class="flex items-center gap-2 flex-1 min-w-0">
|
||||
<AlertTriangle class="h-5 w-5 text-destructive flex-shrink-0 animate-pulse" />
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-semibold text-destructive truncate">
|
||||
<template v-if="alert.missionType === 'spy'">
|
||||
{{ t('alerts.npcSpyIncoming') }}
|
||||
</template>
|
||||
<template v-else-if="alert.missionType === 'attack'">
|
||||
{{ t('alerts.npcAttackIncoming') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ t('alerts.npcFleetIncoming') }}
|
||||
</template>
|
||||
<p class="text-sm font-semibold text-destructive">
|
||||
{{ getAlertSummary() }}
|
||||
</p>
|
||||
<p class="text-xs text-muted-foreground truncate">
|
||||
{{ alert.npcName }} → {{ alert.targetPlanetName }}
|
||||
<template v-if="alert.missionType === 'attack'">({{ alert.fleetSize }} {{ t('alerts.ships') }})</template>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ t('enemyAlert.countdown') }}: {{ formatTimeRemaining(nearestAlert?.arrivalTime || 0) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 倒计时 -->
|
||||
<div class="flex items-center gap-2 flex-shrink-0">
|
||||
<div class="text-right">
|
||||
<p class="text-xs font-mono text-destructive">
|
||||
{{ formatTimeRemaining(alert.arrivalTime) }}
|
||||
</p>
|
||||
<p class="text-[10px] text-muted-foreground">
|
||||
{{ formatTime(alert.arrivalTime) }}
|
||||
</p>
|
||||
</div>
|
||||
<Button @click="markAsRead(alert)" variant="ghost" size="sm" class="h-6 w-6 p-0">
|
||||
<X class="h-3 w-3" />
|
||||
<!-- 查看按钮 -->
|
||||
<Button @click="openAlertPanel" variant="outline" size="sm" class="flex-shrink-0">
|
||||
{{ t('common.view') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import type { IncomingFleetAlert } from '@/types/game'
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { AlertTriangle, X } from 'lucide-vue-next'
|
||||
import { AlertTriangle } from 'lucide-vue-next'
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
|
||||
const props = defineProps<{
|
||||
alerts: IncomingFleetAlert[]
|
||||
}>()
|
||||
import { MissionType } from '@/types/game'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'markAsRead', alert: IncomingFleetAlert): void
|
||||
(e: 'openPanel'): void
|
||||
}>()
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
// 强制更新倒计时
|
||||
@@ -78,6 +51,46 @@
|
||||
if (updateInterval) clearInterval(updateInterval)
|
||||
})
|
||||
|
||||
// 获取活跃的警报(未到达的)
|
||||
const activeAlerts = computed(() => {
|
||||
return (gameStore.player.incomingFleetAlerts || [])
|
||||
.filter(alert => alert.arrivalTime > now.value)
|
||||
.sort((a, b) => a.arrivalTime - b.arrivalTime)
|
||||
})
|
||||
|
||||
// 最近的警报
|
||||
const nearestAlert = computed(() => activeAlerts.value[0] || null)
|
||||
|
||||
// 统计各类型警报数量
|
||||
const alertCounts = computed(() => {
|
||||
const counts = { spy: 0, attack: 0, other: 0 }
|
||||
activeAlerts.value.forEach(alert => {
|
||||
if (alert.missionType === MissionType.Spy) {
|
||||
counts.spy++
|
||||
} else if (alert.missionType === MissionType.Attack) {
|
||||
counts.attack++
|
||||
} else {
|
||||
counts.other++
|
||||
}
|
||||
})
|
||||
return counts
|
||||
})
|
||||
|
||||
// 生成警报汇总文本
|
||||
const getAlertSummary = (): string => {
|
||||
const parts: string[] = []
|
||||
if (alertCounts.value.attack > 0) {
|
||||
parts.push(`${alertCounts.value.attack} ${t('enemyAlert.missionType.attack')}`)
|
||||
}
|
||||
if (alertCounts.value.spy > 0) {
|
||||
parts.push(`${alertCounts.value.spy} ${t('enemyAlert.missionType.spy')}`)
|
||||
}
|
||||
if (alertCounts.value.other > 0) {
|
||||
parts.push(`${alertCounts.value.other} ${t('enemyAlert.missionType.unknown')}`)
|
||||
}
|
||||
return t('alerts.incomingFleets', { count: activeAlerts.value.length }) + ': ' + parts.join(', ')
|
||||
}
|
||||
|
||||
const formatTimeRemaining = (arrivalTime: number): string => {
|
||||
const remaining = Math.max(0, arrivalTime - now.value)
|
||||
const seconds = Math.floor((remaining / 1000) % 60)
|
||||
@@ -90,12 +103,7 @@
|
||||
return `${minutes}:${String(seconds).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
const formatTime = (timestamp: number): string => {
|
||||
const date = new Date(timestamp)
|
||||
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
||||
}
|
||||
|
||||
const markAsRead = (alert: IncomingFleetAlert) => {
|
||||
emit('markAsRead', alert)
|
||||
const openAlertPanel = () => {
|
||||
emit('openPanel')
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -679,7 +679,9 @@
|
||||
const baseCapacity = 10000
|
||||
|
||||
// Building calculation configuration
|
||||
const buildingCalculations: Record<string, (level: number) => Partial<{
|
||||
const buildingCalculations: Record<
|
||||
string,
|
||||
(level: number) => Partial<{
|
||||
production: number
|
||||
consumption: number
|
||||
capacity: number
|
||||
@@ -687,45 +689,46 @@
|
||||
spaceBonus: number
|
||||
buildSpeedBonus: number
|
||||
researchSpeedBonus: number
|
||||
}>> = {
|
||||
metalMine: (lvl) => ({
|
||||
}>
|
||||
> = {
|
||||
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) => ({
|
||||
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) => ({
|
||||
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) => ({
|
||||
solarPlant: lvl => ({
|
||||
production: Math.floor(50 * lvl * Math.pow(1.1, lvl) * energyBonus)
|
||||
}),
|
||||
metalStorage: (lvl) => ({
|
||||
metalStorage: lvl => ({
|
||||
capacity: Math.floor(baseCapacity * Math.pow(2, lvl) * storageBonus)
|
||||
}),
|
||||
crystalStorage: (lvl) => ({
|
||||
crystalStorage: lvl => ({
|
||||
capacity: Math.floor(baseCapacity * Math.pow(2, lvl) * storageBonus)
|
||||
}),
|
||||
deuteriumTank: (lvl) => ({
|
||||
deuteriumTank: lvl => ({
|
||||
capacity: Math.floor(baseCapacity * Math.pow(2, lvl) * storageBonus)
|
||||
}),
|
||||
darkMatterCollector: (lvl) => ({
|
||||
darkMatterCollector: lvl => ({
|
||||
capacity: 1000 + lvl * 100,
|
||||
production: Math.floor(25 * lvl * Math.pow(1.5, lvl))
|
||||
}),
|
||||
darkMatterTank: (lvl) => ({
|
||||
darkMatterTank: lvl => ({
|
||||
capacity: Math.floor(1000 * Math.pow(2, lvl) * storageBonus)
|
||||
}),
|
||||
fusionReactor: (lvl) => ({
|
||||
fusionReactor: lvl => ({
|
||||
production: Math.floor(150 * lvl * Math.pow(1.15, lvl))
|
||||
}),
|
||||
shipyard: (lvl) => ({
|
||||
shipyard: lvl => ({
|
||||
fleetStorage: 1000 * lvl
|
||||
}),
|
||||
hangar: (lvl) => ({
|
||||
hangar: lvl => ({
|
||||
fleetStorage: 500 * lvl
|
||||
}),
|
||||
terraformer: () => ({
|
||||
@@ -734,13 +737,13 @@
|
||||
lunarBase: () => ({
|
||||
spaceBonus: 30
|
||||
}),
|
||||
roboticsFactory: (lvl) => ({
|
||||
roboticsFactory: lvl => ({
|
||||
buildSpeedBonus: lvl
|
||||
}),
|
||||
naniteFactory: (lvl) => ({
|
||||
naniteFactory: lvl => ({
|
||||
buildSpeedBonus: lvl * 2
|
||||
}),
|
||||
researchLab: (lvl) => ({
|
||||
researchLab: lvl => ({
|
||||
researchSpeedBonus: lvl
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<template>
|
||||
<div class="rounded-lg transition-shadow duration-300">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<CardTitle class="flex items-center gap-2">
|
||||
{{ npc.name }}
|
||||
<span v-if="npc.note" class="text-muted-foreground font-normal">({{ npc.note }})</span>
|
||||
<Badge :variant="statusBadgeVariant">
|
||||
{{ statusText }}
|
||||
</Badge>
|
||||
@@ -14,6 +16,16 @@
|
||||
<span v-if="npc.allies && npc.allies.length > 0" class="ml-2">· {{ npc.allies.length }} {{ t('diplomacy.allies') }}</span>
|
||||
</CardDescription>
|
||||
</div>
|
||||
<!-- 编辑备注按钮 -->
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-8 w-8 p-0"
|
||||
@click="openNoteDialog"
|
||||
:title="npc.note ? t('diplomacy.actions.editNote') : t('diplomacy.actions.addNote')"
|
||||
>
|
||||
<Pencil class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
@@ -58,6 +70,7 @@
|
||||
:key="allyId"
|
||||
variant="outline"
|
||||
class="text-xs cursor-pointer hover:bg-accent transition-colors"
|
||||
:class="getAllyBorderClass(allyId)"
|
||||
@click="scrollToAlly(allyId)"
|
||||
>
|
||||
{{ getAllyName(allyId) }}
|
||||
@@ -93,17 +106,38 @@
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 备注编辑对话框 -->
|
||||
<Dialog v-model:open="noteDialogOpen">
|
||||
<DialogContent class="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ npc.note ? t('diplomacy.actions.editNote') : t('diplomacy.actions.addNote') }}</DialogTitle>
|
||||
<DialogDescription class="sr-only">{{ t('diplomacy.note') }}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div class="py-4">
|
||||
<Input v-model="noteInput" :placeholder="t('diplomacy.notePlaceholder')" @keyup.enter="saveNote" />
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="noteDialogOpen = false">{{ t('common.cancel') }}</Button>
|
||||
<Button @click="saveNote">{{ t('common.save') }}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useNPCStore } from '@/stores/npcStore'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Gift, Globe, Sword, Eye, Trash2 } from 'lucide-vue-next'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Gift, Globe, Sword, Eye, Trash2, Pencil } from 'lucide-vue-next'
|
||||
import { RelationStatus, DiplomaticEventType } from '@/types/game'
|
||||
import type { DiplomaticRelation, NPC } from '@/types/game'
|
||||
import { formatRelativeTime } from '@/utils/format'
|
||||
@@ -115,8 +149,28 @@
|
||||
|
||||
const router = useRouter()
|
||||
const npcStore = useNPCStore()
|
||||
const gameStore = useGameStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
// 备注对话框状态
|
||||
const noteDialogOpen = ref(false)
|
||||
const noteInput = ref('')
|
||||
|
||||
// 打开备注对话框
|
||||
const openNoteDialog = () => {
|
||||
noteInput.value = props.npc.note || ''
|
||||
noteDialogOpen.value = true
|
||||
}
|
||||
|
||||
// 保存备注
|
||||
const saveNote = () => {
|
||||
const npc = npcStore.npcs.find(n => n.id === props.npc.id)
|
||||
if (npc) {
|
||||
npc.note = noteInput.value.trim() || undefined
|
||||
}
|
||||
noteDialogOpen.value = false
|
||||
}
|
||||
|
||||
// 好感度值
|
||||
const reputation = computed(() => props.relation?.reputation || 0)
|
||||
|
||||
@@ -163,7 +217,26 @@
|
||||
// 获取盟友名称
|
||||
const getAllyName = (allyId: string) => {
|
||||
const ally = npcStore.npcs.find(n => n.id === allyId)
|
||||
return ally?.name || allyId.substring(0, 8)
|
||||
if (!ally) return allyId.substring(0, 8)
|
||||
return ally.note ? `${ally.name}(${ally.note})` : ally.name
|
||||
}
|
||||
|
||||
// 获取盟友与玩家的外交关系状态对应的边框样式
|
||||
const getAllyBorderClass = (allyId: string) => {
|
||||
const ally = npcStore.npcs.find(n => n.id === allyId)
|
||||
if (!ally) return ''
|
||||
|
||||
const allyRelation = ally.relations?.[gameStore.player.id]
|
||||
if (!allyRelation) return '' // 无关系,使用默认边框
|
||||
|
||||
switch (allyRelation.status) {
|
||||
case RelationStatus.Friendly:
|
||||
return 'border-green-500 dark:border-green-400'
|
||||
case RelationStatus.Hostile:
|
||||
return 'border-red-500 dark:border-red-400'
|
||||
default:
|
||||
return '' // 中立,使用默认边框
|
||||
}
|
||||
}
|
||||
|
||||
// 获取事件图标
|
||||
|
||||
323
src/components/NpcRelationRow.vue
Normal file
323
src/components/NpcRelationRow.vue
Normal file
@@ -0,0 +1,323 @@
|
||||
<template>
|
||||
<div class="rounded-lg transition-shadow duration-300">
|
||||
<div class="p-3 rounded-lg border hover:bg-accent/50 transition-colors cursor-pointer" @click="toggleExpand">
|
||||
<!-- 桌面端:单行布局 -->
|
||||
<div class="hidden sm:flex items-center gap-3">
|
||||
<!-- 状态指示器 -->
|
||||
<div
|
||||
class="w-2 h-2 rounded-full flex-shrink-0"
|
||||
:class="{
|
||||
'bg-green-500': status === RelationStatus.Friendly,
|
||||
'bg-red-500': status === RelationStatus.Hostile,
|
||||
'bg-gray-400': status === RelationStatus.Neutral
|
||||
}"
|
||||
/>
|
||||
|
||||
<!-- 名称和备注 -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium truncate">{{ npc.name }}</span>
|
||||
<span v-if="npc.note" class="text-muted-foreground text-sm truncate">({{ npc.note }})</span>
|
||||
</div>
|
||||
<div class="text-xs text-muted-foreground">
|
||||
{{ npc.planets.length }} {{ t('diplomacy.planets') }}
|
||||
<span v-if="npc.allies && npc.allies.length > 0">· {{ npc.allies.length }} {{ t('diplomacy.allies') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 好感度 -->
|
||||
<div class="flex items-center gap-2 flex-shrink-0">
|
||||
<div class="w-16 h-1.5 bg-muted rounded-full overflow-hidden relative">
|
||||
<div v-if="reputation < 0" class="h-full bg-red-500 absolute right-1/2" :style="{ width: `${Math.abs(reputation) / 2}%` }" />
|
||||
<div v-if="reputation > 0" class="h-full bg-green-500 absolute left-1/2" :style="{ width: `${reputation / 2}%` }" />
|
||||
<div class="absolute left-1/2 top-0 bottom-0 w-px bg-border" />
|
||||
</div>
|
||||
<span class="text-sm font-medium w-10 text-right" :class="reputationColor">{{ reputation > 0 ? '+' : '' }}{{ reputation }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex items-center gap-1 flex-shrink-0">
|
||||
<Button variant="ghost" size="icon" class="h-8 w-8" @click.stop="handleGiftResources" :title="t('diplomacy.actions.gift')">
|
||||
<Gift class="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" class="h-8 w-8" @click.stop="handleViewPlanets" :title="t('diplomacy.actions.viewPlanets')">
|
||||
<Globe class="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
@click.stop="openNoteDialog"
|
||||
:title="npc.note ? t('diplomacy.actions.editNote') : t('diplomacy.actions.addNote')"
|
||||
>
|
||||
<Pencil class="h-4 w-4" />
|
||||
</Button>
|
||||
<ChevronDown class="h-4 w-4 text-muted-foreground transition-transform" :class="{ 'rotate-180': isExpanded }" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 移动端:两行布局 -->
|
||||
<div class="sm:hidden space-y-2">
|
||||
<!-- 第一行:状态、名称、展开箭头 -->
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
class="w-2 h-2 rounded-full flex-shrink-0"
|
||||
:class="{
|
||||
'bg-green-500': status === RelationStatus.Friendly,
|
||||
'bg-red-500': status === RelationStatus.Hostile,
|
||||
'bg-gray-400': status === RelationStatus.Neutral
|
||||
}"
|
||||
/>
|
||||
<div class="flex-1 min-w-0">
|
||||
<span class="font-medium truncate">{{ npc.name }}</span>
|
||||
<span v-if="npc.note" class="text-muted-foreground text-sm ml-1">({{ npc.note }})</span>
|
||||
</div>
|
||||
<ChevronDown class="h-4 w-4 text-muted-foreground transition-transform flex-shrink-0" :class="{ 'rotate-180': isExpanded }" />
|
||||
</div>
|
||||
|
||||
<!-- 第二行:星球数、好感度、操作按钮 -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-xs text-muted-foreground">
|
||||
{{ npc.planets.length }} {{ t('diplomacy.planets') }}
|
||||
<span v-if="npc.allies && npc.allies.length > 0">· {{ npc.allies.length }} {{ t('diplomacy.allies') }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<!-- 好感度数值 -->
|
||||
<span class="text-xs font-medium mr-1" :class="reputationColor">{{ reputation > 0 ? '+' : '' }}{{ reputation }}</span>
|
||||
<!-- 操作按钮 -->
|
||||
<Button variant="ghost" size="icon" class="h-7 w-7" @click.stop="handleGiftResources" :title="t('diplomacy.actions.gift')">
|
||||
<Gift class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" class="h-7 w-7" @click.stop="handleViewPlanets" :title="t('diplomacy.actions.viewPlanets')">
|
||||
<Globe class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7"
|
||||
@click.stop="openNoteDialog"
|
||||
:title="npc.note ? t('diplomacy.actions.editNote') : t('diplomacy.actions.addNote')"
|
||||
>
|
||||
<Pencil class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 展开详情 -->
|
||||
<div v-if="isExpanded" class="ml-5 pl-3 border-l-2 border-muted py-2 space-y-2">
|
||||
<!-- 盟友信息 -->
|
||||
<div v-if="npc.allies && npc.allies.length > 0" class="flex items-center gap-2 flex-wrap">
|
||||
<span class="text-xs text-muted-foreground">{{ t('diplomacy.alliedWith') }}:</span>
|
||||
<Badge
|
||||
v-for="allyId in npc.allies.slice(0, 5)"
|
||||
:key="allyId"
|
||||
variant="outline"
|
||||
class="text-xs cursor-pointer hover:bg-accent transition-colors"
|
||||
:class="getAllyBorderClass(allyId)"
|
||||
@click="scrollToAlly(allyId)"
|
||||
>
|
||||
{{ getAllyName(allyId) }}
|
||||
</Badge>
|
||||
<Badge v-if="npc.allies.length > 5" variant="outline" class="text-xs">+{{ npc.allies.length - 5 }} {{ t('diplomacy.more') }}</Badge>
|
||||
</div>
|
||||
|
||||
<!-- 最近活动 -->
|
||||
<div v-if="recentEvent" class="flex items-center gap-2 text-xs">
|
||||
<span class="text-muted-foreground">{{ t('diplomacy.lastEvent') }}:</span>
|
||||
<component :is="getEventIcon(recentEvent.reason)" class="h-3 w-3" />
|
||||
<span>{{ getEventText(recentEvent.reason) }}</span>
|
||||
<span class="text-muted-foreground">
|
||||
{{ formatRelativeTime((Date.now() - recentEvent.timestamp) / 1000, t) }}{{ t('diplomacy.ago') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 备注编辑对话框 -->
|
||||
<Dialog v-model:open="noteDialogOpen">
|
||||
<DialogContent class="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ npc.note ? t('diplomacy.actions.editNote') : t('diplomacy.actions.addNote') }}</DialogTitle>
|
||||
<DialogDescription class="sr-only">{{ t('diplomacy.note') }}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div class="py-4">
|
||||
<Input v-model="noteInput" :placeholder="t('diplomacy.notePlaceholder')" @keyup.enter="saveNote" />
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="noteDialogOpen = false">{{ t('common.cancel') }}</Button>
|
||||
<Button @click="saveNote">{{ t('common.save') }}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useNPCStore } from '@/stores/npcStore'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Gift, Globe, Pencil, ChevronDown, Sword, Eye, Trash2 } from 'lucide-vue-next'
|
||||
import { RelationStatus, DiplomaticEventType } from '@/types/game'
|
||||
import type { DiplomaticRelation, NPC } from '@/types/game'
|
||||
import { formatRelativeTime } from '@/utils/format'
|
||||
|
||||
const props = defineProps<{
|
||||
npc: NPC
|
||||
relation?: DiplomaticRelation
|
||||
}>()
|
||||
|
||||
const router = useRouter()
|
||||
const npcStore = useNPCStore()
|
||||
const gameStore = useGameStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
// 展开状态
|
||||
const isExpanded = ref(false)
|
||||
const toggleExpand = () => {
|
||||
isExpanded.value = !isExpanded.value
|
||||
}
|
||||
|
||||
// 备注对话框状态
|
||||
const noteDialogOpen = ref(false)
|
||||
const noteInput = ref('')
|
||||
|
||||
const openNoteDialog = () => {
|
||||
noteInput.value = props.npc.note || ''
|
||||
noteDialogOpen.value = true
|
||||
}
|
||||
|
||||
const saveNote = () => {
|
||||
const npc = npcStore.npcs.find(n => n.id === props.npc.id)
|
||||
if (npc) {
|
||||
npc.note = noteInput.value.trim() || undefined
|
||||
}
|
||||
noteDialogOpen.value = false
|
||||
}
|
||||
|
||||
// 好感度值
|
||||
const reputation = computed(() => props.relation?.reputation || 0)
|
||||
|
||||
// 关系状态
|
||||
const status = computed(() => props.relation?.status || RelationStatus.Neutral)
|
||||
|
||||
// 好感度颜色
|
||||
const reputationColor = computed(() => {
|
||||
if (reputation.value >= 20) return 'text-green-600 dark:text-green-400'
|
||||
if (reputation.value <= -20) return 'text-red-600 dark:text-red-400'
|
||||
return 'text-muted-foreground'
|
||||
})
|
||||
|
||||
// 最近的外交事件
|
||||
const recentEvent = computed(() => {
|
||||
if (!props.relation?.history || props.relation.history.length === 0) return null
|
||||
return props.relation.history[props.relation.history.length - 1]
|
||||
})
|
||||
|
||||
// 获取盟友名称
|
||||
const getAllyName = (allyId: string) => {
|
||||
const ally = npcStore.npcs.find(n => n.id === allyId)
|
||||
if (!ally) return allyId.substring(0, 8)
|
||||
return ally.note ? `${ally.name}(${ally.note})` : ally.name
|
||||
}
|
||||
|
||||
// 获取盟友边框样式
|
||||
const getAllyBorderClass = (allyId: string) => {
|
||||
const ally = npcStore.npcs.find(n => n.id === allyId)
|
||||
if (!ally) return ''
|
||||
|
||||
const allyRelation = ally.relations?.[gameStore.player.id]
|
||||
if (!allyRelation) return ''
|
||||
|
||||
switch (allyRelation.status) {
|
||||
case RelationStatus.Friendly:
|
||||
return 'border-green-500 dark:border-green-400'
|
||||
case RelationStatus.Hostile:
|
||||
return 'border-red-500 dark:border-red-400'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
// 获取事件图标
|
||||
const getEventIcon = (eventType: string) => {
|
||||
switch (eventType) {
|
||||
case DiplomaticEventType.GiftResources:
|
||||
return Gift
|
||||
case DiplomaticEventType.Attack:
|
||||
case DiplomaticEventType.AllyAttacked:
|
||||
return Sword
|
||||
case DiplomaticEventType.Spy:
|
||||
return Eye
|
||||
case DiplomaticEventType.StealDebris:
|
||||
return Trash2
|
||||
default:
|
||||
return Gift
|
||||
}
|
||||
}
|
||||
|
||||
// 获取事件文本
|
||||
const getEventText = (eventType: string) => {
|
||||
switch (eventType) {
|
||||
case DiplomaticEventType.GiftResources:
|
||||
return t('diplomacy.events.gift')
|
||||
case DiplomaticEventType.Attack:
|
||||
return t('diplomacy.events.attack')
|
||||
case DiplomaticEventType.AllyAttacked:
|
||||
return t('diplomacy.events.allyAttacked')
|
||||
case DiplomaticEventType.Spy:
|
||||
return t('diplomacy.events.spy')
|
||||
case DiplomaticEventType.StealDebris:
|
||||
return t('diplomacy.events.stealDebris')
|
||||
default:
|
||||
return eventType
|
||||
}
|
||||
}
|
||||
|
||||
// 赠送资源
|
||||
const handleGiftResources = () => {
|
||||
if (props.npc.planets.length > 0) {
|
||||
const targetPlanet = props.npc.planets[0]
|
||||
if (!targetPlanet) return
|
||||
|
||||
router.push({
|
||||
path: '/fleet',
|
||||
query: {
|
||||
galaxy: targetPlanet.position.galaxy,
|
||||
system: targetPlanet.position.system,
|
||||
position: targetPlanet.position.position,
|
||||
gift: '1'
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 查看星球
|
||||
const handleViewPlanets = () => {
|
||||
if (props.npc.planets.length > 0) {
|
||||
const targetPlanet = props.npc.planets[0]
|
||||
if (!targetPlanet) return
|
||||
|
||||
router.push({
|
||||
path: '/galaxy',
|
||||
query: {
|
||||
galaxy: targetPlanet.position.galaxy,
|
||||
system: targetPlanet.position.system,
|
||||
highlightNpc: props.npc.id
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 滚动到盟友卡片
|
||||
const scrollToAlly = (allyId: string) => {
|
||||
const event = new CustomEvent('scrollToNpc', { detail: { npcId: allyId }, bubbles: true })
|
||||
document.dispatchEvent(event)
|
||||
}
|
||||
</script>
|
||||
90
src/components/PrivacyDialog.vue
Normal file
90
src/components/PrivacyDialog.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<Dialog v-model:open="open">
|
||||
<DialogContent class="max-w-2xl max-h-[80vh] overflow-hidden flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ t('privacy.title') }}</DialogTitle>
|
||||
<DialogDescription class="sr-only">{{ t('privacy.title') }}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div class="flex-1 overflow-y-auto pr-2 space-y-4 text-sm">
|
||||
<!-- 简介 -->
|
||||
<section>
|
||||
<h3 class="font-semibold mb-1">{{ t('privacy.sections.introduction.title') }}</h3>
|
||||
<p class="text-muted-foreground">{{ t('privacy.sections.introduction.content') }}</p>
|
||||
</section>
|
||||
|
||||
<!-- 数据收集 -->
|
||||
<section>
|
||||
<h3 class="font-semibold mb-1">{{ t('privacy.sections.dataCollection.title') }}</h3>
|
||||
<p class="text-muted-foreground mb-1">{{ t('privacy.sections.dataCollection.content') }}</p>
|
||||
<ul class="list-disc list-inside text-muted-foreground ml-2 space-y-0.5">
|
||||
<li>{{ t('privacy.sections.dataCollection.items.gameProgress') }}</li>
|
||||
<li>{{ t('privacy.sections.dataCollection.items.settings') }}</li>
|
||||
<li>{{ t('privacy.sections.dataCollection.items.language') }}</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- 数据存储 -->
|
||||
<section>
|
||||
<h3 class="font-semibold mb-1">{{ t('privacy.sections.dataStorage.title') }}</h3>
|
||||
<p class="text-muted-foreground">{{ t('privacy.sections.dataStorage.content') }}</p>
|
||||
</section>
|
||||
|
||||
<!-- 无服务器通信 -->
|
||||
<section>
|
||||
<h3 class="font-semibold mb-1">{{ t('privacy.sections.noServer.title') }}</h3>
|
||||
<p class="text-muted-foreground">{{ t('privacy.sections.noServer.content') }}</p>
|
||||
</section>
|
||||
|
||||
<!-- 第三方服务 -->
|
||||
<section>
|
||||
<h3 class="font-semibold mb-1">{{ t('privacy.sections.thirdParty.title') }}</h3>
|
||||
<p class="text-muted-foreground">{{ t('privacy.sections.thirdParty.content') }}</p>
|
||||
</section>
|
||||
|
||||
<!-- 数据控制 -->
|
||||
<section>
|
||||
<h3 class="font-semibold mb-1">{{ t('privacy.sections.dataControl.title') }}</h3>
|
||||
<p class="text-muted-foreground mb-1">{{ t('privacy.sections.dataControl.content') }}</p>
|
||||
<ul class="list-disc list-inside text-muted-foreground ml-2 space-y-0.5">
|
||||
<li>{{ t('privacy.sections.dataControl.items.export') }}</li>
|
||||
<li>{{ t('privacy.sections.dataControl.items.import') }}</li>
|
||||
<li>{{ t('privacy.sections.dataControl.items.delete') }}</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- 联系我们 -->
|
||||
<section>
|
||||
<h3 class="font-semibold mb-1">{{ t('privacy.sections.contact.title') }}</h3>
|
||||
<p class="text-muted-foreground">
|
||||
{{ t('privacy.sections.contact.content') }}
|
||||
<a
|
||||
:href="`https://github.com/${pkg.author.name}/${pkg.name}/issues`"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-primary hover:underline"
|
||||
>
|
||||
GitHub Issues
|
||||
</a>
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
<DialogFooter class="mt-4">
|
||||
<Button variant="outline" @click="open = false">
|
||||
{{ t('common.close') }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import pkg from '../../package.json'
|
||||
|
||||
// 双向绑定 open 状态
|
||||
const open = defineModel<boolean>('open', { default: false })
|
||||
|
||||
const { t } = useI18n()
|
||||
</script>
|
||||
@@ -8,65 +8,58 @@
|
||||
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 }}
|
||||
{{ 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="h-[480px]">
|
||||
<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" />
|
||||
<h3 class="font-semibold">{{ t('queue.title') }} ({{ totalQueueCount }})</h3>
|
||||
</div>
|
||||
|
||||
<!-- 研究队列 -->
|
||||
<div v-for="item in researchQueue" :key="item.id" class="space-y-1.5">
|
||||
<Tabs v-model="activeTab" class="w-full">
|
||||
<TabsList class="w-full grid grid-cols-5 h-9 rounded-none border-b bg-transparent">
|
||||
<TabsTrigger v-for="tab in tabConfig" :key="tab.value" :value="tab.value" class="text-xs px-1 data-[state=active]:bg-muted">
|
||||
{{ t(`queue.tabs.${tab.value}`) }}
|
||||
<Badge v-if="tab.items.length > 0" variant="secondary" class="ml-1 h-4 px-1 text-[10px]">
|
||||
{{ tab.items.length }}
|
||||
</Badge>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<ScrollArea class="h-[420px]">
|
||||
<TabsContent v-for="tab in tabConfig" :key="tab.value" :value="tab.value" class="mt-0">
|
||||
<Empty v-if="tab.items.length === 0" class="border-0">
|
||||
<EmptyContent>
|
||||
<Inbox class="h-10 w-10 text-muted-foreground" />
|
||||
<EmptyDescription>{{ t('queue.empty') }}</EmptyDescription>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
<div v-else class="divide-y p-4 space-y-3">
|
||||
<div v-for="item in tab.items" :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" />
|
||||
<div class="h-2 w-2 rounded-full animate-pulse flex-shrink-0" :class="getStatusDotClass(item)" />
|
||||
<span class="font-medium truncate">{{ getItemName(item) }}</span>
|
||||
<span class="text-muted-foreground text-[10px] sm:text-xs">→ {{ t('queue.level') }} {{ item.targetLevel }}</span>
|
||||
<span class="text-muted-foreground text-[10px] sm:text-xs">
|
||||
{{
|
||||
item.type === 'ship' || item.type === 'defense'
|
||||
? `→ ${t('queue.quantity')} ${item.quantity}`
|
||||
: item.type === 'demolish'
|
||||
? `→ ${t('queue.demolishing')}`
|
||||
: `→ ${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"
|
||||
@click.stop="handleCancel(item)"
|
||||
>
|
||||
{{ t('queue.cancel') }}
|
||||
</Button>
|
||||
@@ -75,19 +68,23 @@
|
||||
<Progress :model-value="getQueueProgress(item)" class="h-1.5" />
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</ScrollArea>
|
||||
</Tabs>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { ListOrdered } from 'lucide-vue-next'
|
||||
import { computed, ref, onUnmounted, watch } from 'vue'
|
||||
import { ListOrdered, Inbox } 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 { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Empty, EmptyContent, EmptyDescription } from '@/components/ui/empty'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useGameConfig } from '@/composables/useGameConfig'
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
@@ -99,6 +96,35 @@
|
||||
const { BUILDINGS, SHIPS, DEFENSES, TECHNOLOGIES } = useGameConfig()
|
||||
|
||||
const isOpen = ref(false)
|
||||
const activeTab = ref('all')
|
||||
|
||||
// 响应式时间戳,用于驱动时间和进度的动态更新
|
||||
const currentTime = ref(Date.now())
|
||||
let timerInterval: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
// 当弹窗打开时启动计时器,关闭时停止
|
||||
watch(isOpen, open => {
|
||||
if (open) {
|
||||
// 启动每秒更新的计时器
|
||||
timerInterval = setInterval(() => {
|
||||
currentTime.value = Date.now()
|
||||
}, 1000)
|
||||
} else {
|
||||
// 停止计时器
|
||||
if (timerInterval) {
|
||||
clearInterval(timerInterval)
|
||||
timerInterval = null
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 组件卸载时清理计时器
|
||||
onUnmounted(() => {
|
||||
if (timerInterval) {
|
||||
clearInterval(timerInterval)
|
||||
timerInterval = null
|
||||
}
|
||||
})
|
||||
|
||||
// 获取当前星球的建造队列
|
||||
const buildQueue = computed(() => {
|
||||
@@ -115,6 +141,15 @@
|
||||
return buildQueue.value.length + researchQueue.value.length
|
||||
})
|
||||
|
||||
// 标签页配置(用于循环渲染)
|
||||
const tabConfig = computed(() => [
|
||||
{ value: 'all', items: [...buildQueue.value, ...researchQueue.value] },
|
||||
{ value: 'buildings', items: buildQueue.value.filter(item => item.type === 'building' || item.type === 'demolish') },
|
||||
{ value: 'research', items: researchQueue.value },
|
||||
{ value: 'ships', items: buildQueue.value.filter(item => item.type === 'ship') },
|
||||
{ value: 'defense', items: buildQueue.value.filter(item => item.type === 'defense') }
|
||||
])
|
||||
|
||||
// 获取队列项名称
|
||||
const getItemName = (item: BuildQueueItem): string => {
|
||||
if (item.type === 'building' || item.type === 'demolish') {
|
||||
@@ -129,16 +164,14 @@
|
||||
return ''
|
||||
}
|
||||
|
||||
// 获取剩余时间
|
||||
// 获取剩余时间(使用响应式 currentTime 确保动态更新)
|
||||
const getRemainingTime = (item: BuildQueueItem): number => {
|
||||
const now = Date.now()
|
||||
return Math.max(0, Math.floor((item.endTime - now) / 1000))
|
||||
return Math.max(0, Math.floor((item.endTime - currentTime.value) / 1000))
|
||||
}
|
||||
|
||||
// 获取队列进度
|
||||
// 获取队列进度(使用响应式 currentTime 确保动态更新)
|
||||
const getQueueProgress = (item: BuildQueueItem): number => {
|
||||
const now = Date.now()
|
||||
const elapsed = now - item.startTime
|
||||
const elapsed = currentTime.value - item.startTime
|
||||
const total = item.endTime - item.startTime
|
||||
return Math.min(100, (elapsed / total) * 100)
|
||||
}
|
||||
@@ -158,9 +191,10 @@
|
||||
window.dispatchEvent(event)
|
||||
}
|
||||
|
||||
// 取消研究
|
||||
const handleCancelResearch = (queueId: string) => {
|
||||
const event = new CustomEvent('cancel-research', { detail: queueId })
|
||||
window.dispatchEvent(event)
|
||||
// 获取状态指示点颜色
|
||||
const getStatusDotClass = (item: BuildQueueItem): string => {
|
||||
if (item.type === 'demolish') return 'bg-destructive'
|
||||
if (item.type === 'technology') return 'bg-blue-500'
|
||||
return 'bg-green-500'
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,438 +0,0 @@
|
||||
<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>
|
||||
@@ -6,7 +6,7 @@
|
||||
v-bind="{ ...$attrs, ...forwarded }"
|
||||
:class="
|
||||
cn(
|
||||
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-2xl',
|
||||
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-[60] grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-2xl',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
v-bind="delegatedProps"
|
||||
:class="
|
||||
cn(
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80',
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-[60] bg-black/80',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
v-bind="{ ...$attrs, ...forwarded }"
|
||||
:class="
|
||||
cn(
|
||||
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 w-[calc(100vw-3rem)] translate-x-[-50%] translate-y-[-50%] rounded-lg border shadow-lg duration-200 sm:w-auto sm:min-w-[764px] flex flex-col p-0',
|
||||
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-[60] w-[calc(100vw-3rem)] translate-x-[-50%] translate-y-[-50%] rounded-lg border shadow-lg duration-200 sm:w-auto sm:min-w-[764px] flex flex-col p-0',
|
||||
containerClass
|
||||
)
|
||||
"
|
||||
|
||||
93
src/components/ui/pagination/FixedPagination.vue
Normal file
93
src/components/ui/pagination/FixedPagination.vue
Normal file
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<div v-if="totalPages > 1" class="fixed bottom-4 left-1/2 -translate-x-1/2 z-40">
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- 上一页按钮 - 圆形胶囊 -->
|
||||
<button
|
||||
v-if="currentPage > 1"
|
||||
@click="emit('update:page', currentPage - 1)"
|
||||
class="h-10 w-10 rounded-full bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 border shadow-lg flex items-center justify-center hover:bg-accent transition-colors"
|
||||
>
|
||||
<ChevronLeft class="h-5 w-5" />
|
||||
</button>
|
||||
|
||||
<!-- 页码 - 椭圆形胶囊 -->
|
||||
<div class="bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 border rounded-full py-2 px-3 shadow-lg flex items-center gap-1">
|
||||
<button
|
||||
v-for="pageNum in pageNumbers"
|
||||
:key="pageNum"
|
||||
@click="emit('update:page', pageNum)"
|
||||
class="h-8 min-w-8 px-2 rounded-full text-sm font-medium transition-colors"
|
||||
:class="
|
||||
pageNum === currentPage
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'hover:bg-accent'
|
||||
"
|
||||
>
|
||||
{{ pageNum }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 下一页按钮 - 圆形胶囊 -->
|
||||
<button
|
||||
v-if="currentPage < totalPages"
|
||||
@click="emit('update:page', currentPage + 1)"
|
||||
class="h-10 w-10 rounded-full bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 border shadow-lg flex items-center justify-center hover:bg-accent transition-colors"
|
||||
>
|
||||
<ChevronRight class="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-vue-next'
|
||||
|
||||
interface Props {
|
||||
page: number
|
||||
totalPages: number
|
||||
maxVisible?: number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
maxVisible: 3
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:page': [page: number]
|
||||
}>()
|
||||
|
||||
const currentPage = computed(() => props.page)
|
||||
|
||||
// 生成页码列表 - 最多显示指定数量页码,不含省略号
|
||||
const pageNumbers = computed(() => {
|
||||
const pages: number[] = []
|
||||
const { totalPages, maxVisible } = props
|
||||
const current = currentPage.value
|
||||
|
||||
if (totalPages <= maxVisible) {
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
pages.push(i)
|
||||
}
|
||||
} else {
|
||||
let start = current - Math.floor(maxVisible / 2)
|
||||
let end = current + Math.floor(maxVisible / 2)
|
||||
|
||||
// 边界调整
|
||||
if (start < 1) {
|
||||
start = 1
|
||||
end = maxVisible
|
||||
}
|
||||
if (end > totalPages) {
|
||||
end = totalPages
|
||||
start = totalPages - maxVisible + 1
|
||||
}
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
pages.push(i)
|
||||
}
|
||||
}
|
||||
|
||||
return pages
|
||||
})
|
||||
</script>
|
||||
@@ -6,3 +6,4 @@ export { default as PaginationItem } from './PaginationItem.vue'
|
||||
export { default as PaginationLast } from './PaginationLast.vue'
|
||||
export { default as PaginationNext } from './PaginationNext.vue'
|
||||
export { default as PaginationPrevious } from './PaginationPrevious.vue'
|
||||
export { default as FixedPagination } from './FixedPagination.vue'
|
||||
|
||||
@@ -86,7 +86,6 @@
|
||||
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({
|
||||
@@ -101,47 +100,17 @@
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,18 +16,11 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes, Ref } from 'vue'
|
||||
import { defaultDocument, useEventListener, useMediaQuery, useVModel } from '@vueuse/core'
|
||||
import { useMediaQuery, useVModel } from '@vueuse/core'
|
||||
import { TooltipProvider } from 'reka-ui'
|
||||
import { computed, ref } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
import {
|
||||
provideSidebarContext,
|
||||
SIDEBAR_COOKIE_MAX_AGE,
|
||||
SIDEBAR_COOKIE_NAME,
|
||||
SIDEBAR_KEYBOARD_SHORTCUT,
|
||||
SIDEBAR_WIDTH,
|
||||
SIDEBAR_WIDTH_ICON
|
||||
} from './utils'
|
||||
import { provideSidebarContext, SIDEBAR_WIDTH, SIDEBAR_WIDTH_ICON } from './utils'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -36,7 +29,7 @@
|
||||
class?: HTMLAttributes['class']
|
||||
}>(),
|
||||
{
|
||||
defaultOpen: !defaultDocument?.cookie.includes(`${SIDEBAR_COOKIE_NAME}=false`),
|
||||
defaultOpen: true,
|
||||
open: undefined
|
||||
}
|
||||
)
|
||||
@@ -54,30 +47,17 @@
|
||||
}) as Ref<boolean>
|
||||
|
||||
const setOpen = (value: boolean) => {
|
||||
open.value = value // emits('update:open', value)
|
||||
|
||||
// This sets the cookie to keep the sidebar state.
|
||||
document.cookie = `${SIDEBAR_COOKIE_NAME}=${open.value}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
|
||||
open.value = value
|
||||
}
|
||||
|
||||
const setOpenMobile = (value: boolean) => {
|
||||
openMobile.value = value
|
||||
}
|
||||
|
||||
// Helper to toggle the sidebar.
|
||||
const toggleSidebar = () => {
|
||||
return isMobile.value ? setOpenMobile(!openMobile.value) : setOpen(!open.value)
|
||||
}
|
||||
|
||||
useEventListener('keydown', (event: KeyboardEvent) => {
|
||||
if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {
|
||||
event.preventDefault()
|
||||
toggleSidebar()
|
||||
}
|
||||
})
|
||||
|
||||
// We add a state so that we can do data-state="expanded" or "collapsed".
|
||||
// This makes it easier to style the sidebar with Tailwind classes.
|
||||
const state = computed(() => (open.value ? 'expanded' : 'collapsed'))
|
||||
|
||||
provideSidebarContext({
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import type { ComputedRef, Ref } from 'vue'
|
||||
import { createContext } from 'reka-ui'
|
||||
|
||||
export const SIDEBAR_COOKIE_NAME = 'sidebar_state'
|
||||
export const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
|
||||
export const SIDEBAR_WIDTH = '16rem'
|
||||
export const SIDEBAR_WIDTH_MOBILE = '18rem'
|
||||
export const SIDEBAR_WIDTH_ICON = '3rem'
|
||||
export const SIDEBAR_KEYBOARD_SHORTCUT = 'b'
|
||||
|
||||
export const [useSidebar, provideSidebarContext] = createContext<{
|
||||
state: ComputedRef<'expanded' | 'collapsed'>
|
||||
|
||||
239
src/composables/useHints.ts
Normal file
239
src/composables/useHints.ts
Normal file
@@ -0,0 +1,239 @@
|
||||
/**
|
||||
* 弱引导系统 - 非阻塞式、可关闭的页面提示
|
||||
* 用温和的引导替代强制性教程
|
||||
*/
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
|
||||
// 提示定义 - 用户首次访问页面时显示一次
|
||||
export interface Hint {
|
||||
id: string
|
||||
route: string // 显示此提示的路由路径
|
||||
titleKey: string // 标题的 i18n 键
|
||||
messageKey: string // 消息的 i18n 键
|
||||
icon?: string // 可选的图标名称
|
||||
delay?: number // 显示前的延迟毫秒数(默认 500)
|
||||
}
|
||||
|
||||
// 所有可用的提示
|
||||
const hints: Hint[] = [
|
||||
{
|
||||
id: 'overview_intro',
|
||||
route: '/overview',
|
||||
titleKey: 'hints.overview.title',
|
||||
messageKey: 'hints.overview.message',
|
||||
icon: 'home',
|
||||
delay: 1000
|
||||
},
|
||||
{
|
||||
id: 'buildings_intro',
|
||||
route: '/buildings',
|
||||
titleKey: 'hints.buildings.title',
|
||||
messageKey: 'hints.buildings.message',
|
||||
icon: 'building',
|
||||
delay: 500
|
||||
},
|
||||
{
|
||||
id: 'research_intro',
|
||||
route: '/research',
|
||||
titleKey: 'hints.research.title',
|
||||
messageKey: 'hints.research.message',
|
||||
icon: 'flask',
|
||||
delay: 500
|
||||
},
|
||||
{
|
||||
id: 'shipyard_intro',
|
||||
route: '/shipyard',
|
||||
titleKey: 'hints.shipyard.title',
|
||||
messageKey: 'hints.shipyard.message',
|
||||
icon: 'rocket',
|
||||
delay: 500
|
||||
},
|
||||
{
|
||||
id: 'fleet_intro',
|
||||
route: '/fleet',
|
||||
titleKey: 'hints.fleet.title',
|
||||
messageKey: 'hints.fleet.message',
|
||||
icon: 'plane',
|
||||
delay: 500
|
||||
},
|
||||
{
|
||||
id: 'galaxy_intro',
|
||||
route: '/galaxy',
|
||||
titleKey: 'hints.galaxy.title',
|
||||
messageKey: 'hints.galaxy.message',
|
||||
icon: 'globe',
|
||||
delay: 500
|
||||
},
|
||||
{
|
||||
id: 'diplomacy_intro',
|
||||
route: '/diplomacy',
|
||||
titleKey: 'hints.diplomacy.title',
|
||||
messageKey: 'hints.diplomacy.message',
|
||||
icon: 'handshake',
|
||||
delay: 500
|
||||
},
|
||||
{
|
||||
id: 'messages_intro',
|
||||
route: '/messages',
|
||||
titleKey: 'hints.messages.title',
|
||||
messageKey: 'hints.messages.message',
|
||||
icon: 'mail',
|
||||
delay: 500
|
||||
},
|
||||
{
|
||||
id: 'defense_intro',
|
||||
route: '/defense',
|
||||
titleKey: 'hints.defense.title',
|
||||
messageKey: 'hints.defense.message',
|
||||
icon: 'shield',
|
||||
delay: 500
|
||||
},
|
||||
{
|
||||
id: 'officers_intro',
|
||||
route: '/officers',
|
||||
titleKey: 'hints.officers.title',
|
||||
messageKey: 'hints.officers.message',
|
||||
icon: 'users',
|
||||
delay: 500
|
||||
},
|
||||
{
|
||||
id: 'simulator_intro',
|
||||
route: '/battle-simulator',
|
||||
titleKey: 'hints.simulator.title',
|
||||
messageKey: 'hints.simulator.message',
|
||||
icon: 'swords',
|
||||
delay: 500
|
||||
},
|
||||
{
|
||||
id: 'settings_intro',
|
||||
route: '/settings',
|
||||
titleKey: 'hints.settings.title',
|
||||
messageKey: 'hints.settings.message',
|
||||
icon: 'settings',
|
||||
delay: 500
|
||||
},
|
||||
{
|
||||
id: 'gm_intro',
|
||||
route: '/gm',
|
||||
titleKey: 'hints.gm.title',
|
||||
messageKey: 'hints.gm.message',
|
||||
icon: 'wand',
|
||||
delay: 500
|
||||
}
|
||||
]
|
||||
|
||||
// 全局UI状态(不需要持久化)
|
||||
const currentHint = ref<Hint | null>(null)
|
||||
const isHintVisible = ref(false)
|
||||
|
||||
let hintTimeout: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
export function useHints() {
|
||||
const router = useRouter()
|
||||
const gameStore = useGameStore()
|
||||
|
||||
// 确保 dismissedHints 数组已初始化
|
||||
if (!gameStore.player.dismissedHints) {
|
||||
gameStore.player.dismissedHints = []
|
||||
}
|
||||
|
||||
// 确保 hintsEnabled 已初始化(默认为 true)
|
||||
if (gameStore.player.hintsEnabled === undefined) {
|
||||
gameStore.player.hintsEnabled = true
|
||||
}
|
||||
|
||||
// 检查提示是否已被关闭
|
||||
const isHintDismissed = (hintId: string): boolean => {
|
||||
return gameStore.player.dismissedHints?.includes(hintId) ?? false
|
||||
}
|
||||
|
||||
// 检查当前路由是否应该显示提示
|
||||
const checkForHint = (routePath: string) => {
|
||||
if (!gameStore.player.hintsEnabled) return
|
||||
|
||||
// 清除任何待显示的提示
|
||||
if (hintTimeout) {
|
||||
clearTimeout(hintTimeout)
|
||||
hintTimeout = null
|
||||
}
|
||||
|
||||
// 导航时隐藏当前提示
|
||||
isHintVisible.value = false
|
||||
currentHint.value = null
|
||||
|
||||
// 查找此路由对应的提示
|
||||
const hint = hints.find(h => routePath.startsWith(h.route))
|
||||
if (!hint) return
|
||||
|
||||
// 检查是否已经关闭过
|
||||
if (isHintDismissed(hint.id)) return
|
||||
|
||||
// 延迟后显示提示
|
||||
const delay = hint.delay ?? 500
|
||||
hintTimeout = setTimeout(() => {
|
||||
currentHint.value = hint
|
||||
isHintVisible.value = true
|
||||
}, delay)
|
||||
}
|
||||
|
||||
// 关闭当前提示(不再显示)
|
||||
const dismissHint = (dontShowAgain = true) => {
|
||||
if (currentHint.value && dontShowAgain) {
|
||||
if (!gameStore.player.dismissedHints) {
|
||||
gameStore.player.dismissedHints = []
|
||||
}
|
||||
if (!gameStore.player.dismissedHints.includes(currentHint.value.id)) {
|
||||
gameStore.player.dismissedHints.push(currentHint.value.id)
|
||||
}
|
||||
}
|
||||
isHintVisible.value = false
|
||||
currentHint.value = null
|
||||
}
|
||||
|
||||
// 暂时关闭提示(下次访问还会显示)
|
||||
const closeHint = () => {
|
||||
isHintVisible.value = false
|
||||
currentHint.value = null
|
||||
}
|
||||
|
||||
// 重置所有提示(重新显示)
|
||||
const resetHints = () => {
|
||||
gameStore.player.dismissedHints = []
|
||||
}
|
||||
|
||||
// 切换提示开关
|
||||
const setHintsEnabled = (enabled: boolean) => {
|
||||
gameStore.player.hintsEnabled = enabled
|
||||
if (!enabled) {
|
||||
isHintVisible.value = false
|
||||
currentHint.value = null
|
||||
}
|
||||
}
|
||||
|
||||
// 监听路由变化
|
||||
watch(
|
||||
() => router.currentRoute.value.path,
|
||||
newPath => {
|
||||
checkForHint(newPath)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
return {
|
||||
// 状态
|
||||
currentHint: computed(() => currentHint.value),
|
||||
isHintVisible: computed(() => isHintVisible.value),
|
||||
hintsEnabled: computed(() => gameStore.player.hintsEnabled ?? true),
|
||||
dismissedCount: computed(() => gameStore.player.dismissedHints?.length ?? 0),
|
||||
totalHints: computed(() => hints.length),
|
||||
|
||||
// 操作
|
||||
dismissHint,
|
||||
closeHint,
|
||||
resetHints,
|
||||
setHintsEnabled,
|
||||
checkForHint
|
||||
}
|
||||
}
|
||||
@@ -1,562 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -460,7 +460,8 @@ export const TECHNOLOGIES: Record<TechnologyType, TechnologyConfig> = {
|
||||
[TechnologyType.EspionageTechnology]: {
|
||||
id: TechnologyType.EspionageTechnology,
|
||||
name: '间谍技术',
|
||||
description: '提高间谍探测效果,每级提高1级侦查深度。侦察等级=己方等级-对方等级+侦察船数/5。≥-1显示舰队,≥1显示防御,≥3显示建筑,≥5显示科技',
|
||||
description:
|
||||
'提高间谍探测效果,每级提高1级侦查深度。侦察等级=己方等级-对方等级+侦察船数/5。≥-1显示舰队,≥1显示防御,≥3显示建筑,≥5显示科技',
|
||||
baseCost: { metal: 200, crystal: 1000, deuterium: 200, darkMatter: 0, energy: 0 },
|
||||
baseTime: 60,
|
||||
costMultiplier: 2,
|
||||
@@ -620,7 +621,7 @@ export const TECHNOLOGIES: Record<TechnologyType, TechnologyConfig> = {
|
||||
baseCost: { metal: 4000000, crystal: 8000000, deuterium: 4000000, darkMatter: 200000, energy: 0 },
|
||||
baseTime: 300,
|
||||
costMultiplier: 2,
|
||||
maxLevel: 5, // 最多5级
|
||||
maxLevel: 10,
|
||||
requirements: {
|
||||
[BuildingType.ResearchLab]: 12,
|
||||
[TechnologyType.HyperspaceTechnology]: 8,
|
||||
@@ -844,7 +845,7 @@ export const SHIPS: Record<ShipType, ShipConfig> = {
|
||||
attack: 1,
|
||||
shield: 1,
|
||||
armor: 200,
|
||||
speed: 0,
|
||||
speed: 1, // 极低速度,可被舰队携带但非常慢
|
||||
fuelConsumption: 0,
|
||||
storageUsage: 1,
|
||||
requirements: { [BuildingType.Shipyard]: 1 }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ClassValue } from "clsx"
|
||||
import { clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import type { ClassValue } from 'clsx'
|
||||
import { clsx } from 'clsx'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
export default {
|
||||
home: {
|
||||
subtitle: 'Erobere die Sterne',
|
||||
startGame: 'Spiel starten',
|
||||
privacyAgreement: 'Datenschutzvereinbarung',
|
||||
privacyAgreementDesc: 'Bitte lesen und akzeptieren Sie unsere Datenschutzrichtlinie, bevor Sie das Spiel starten.',
|
||||
agreeToPrivacy: 'Ich habe gelesen und stimme zu',
|
||||
viewFullPolicy: 'Vollständige Richtlinie anzeigen',
|
||||
agreeAndStart: 'Zustimmen & Starten'
|
||||
},
|
||||
common: {
|
||||
confirm: 'Bestätigen',
|
||||
cancel: 'Abbrechen',
|
||||
@@ -36,7 +45,8 @@ export default {
|
||||
requirementsNotMet: 'Anforderungen nicht erfüllt',
|
||||
current: 'Aktuell',
|
||||
level: 'Stufe',
|
||||
gmModeActivated: 'GM-Modus aktiviert! Überprüfen Sie das Navigationsmenü.'
|
||||
gmModeActivated: 'GM-Modus aktiviert! Überprüfen Sie das Navigationsmenü.',
|
||||
view: 'Anzeigen'
|
||||
},
|
||||
errors: {
|
||||
requirementsNotMet: 'Anforderungen nicht erfüllt',
|
||||
@@ -113,7 +123,13 @@ export default {
|
||||
homePlanet: 'Heimatplanet',
|
||||
planetPrefix: 'Planet',
|
||||
moonSuffix: 's Mond',
|
||||
colonyPrefix: 'Kolonie'
|
||||
colonyPrefix: 'Kolonie',
|
||||
renamePlanet: 'Planet umbenennen',
|
||||
renamePlanetTitle: 'Planet umbenennen',
|
||||
newPlanetName: 'Neuer Name',
|
||||
planetNamePlaceholder: 'Neuen Planetennamen eingeben',
|
||||
rename: 'Umbenennen',
|
||||
renameSuccess: 'Planet wurde in {name} umbenannt'
|
||||
},
|
||||
player: {
|
||||
points: 'Gesamtpunkte'
|
||||
@@ -284,7 +300,8 @@ export default {
|
||||
hyperspaceTechnology: 'Hyperraumsprung-Technologie',
|
||||
plasmaTechnology: 'Plasmawaffentechnologie',
|
||||
computerTechnology: 'Erhöht Forschungswarteschlange und Flottenmissionsslots, +1 Warteschlange +1 Slot pro Stufe (max 10 Stufen)',
|
||||
espionageTechnology: 'Verbessert Sondenwirksamkeit, +1 Spionagestufe pro Stufe. Spionagestufe = eigene Stufe - Gegnerstufe + Sonden/5. ≥-1 zeigt Flotte, ≥1 zeigt Verteidigung, ≥3 zeigt Gebäude, ≥5 zeigt Technologien',
|
||||
espionageTechnology:
|
||||
'Verbessert Sondenwirksamkeit, +1 Spionagestufe pro Stufe. Spionagestufe = eigene Stufe - Gegnerstufe + Sonden/5. ≥-1 zeigt Flotte, ≥1 zeigt Verteidigung, ≥3 zeigt Gebäude, ≥5 zeigt Technologien',
|
||||
weaponsTechnology: 'Erhöht Angriffskraft von Schiffen und Verteidigung um 10% pro Stufe',
|
||||
shieldingTechnology: 'Erhöht Schilde von Schiffen und Verteidigung um 10% pro Stufe',
|
||||
armourTechnology: 'Erhöht Panzerung von Schiffen und Verteidigung um 10% pro Stufe',
|
||||
@@ -317,8 +334,8 @@ export default {
|
||||
darkMatterSpecialist: 'Verbessert Dunkle-Materie-Sammlungseffizienz'
|
||||
},
|
||||
queue: {
|
||||
title: 'Bauauftrag',
|
||||
empty: 'Keine aktiven Aufgaben',
|
||||
title: 'Aktive Aufgaben',
|
||||
empty: 'Keine aktiven Warteschlangen',
|
||||
buildQueue: 'Bauauftrag',
|
||||
researchQueue: 'Forschungsauftrag',
|
||||
building: 'Im Bau',
|
||||
@@ -331,7 +348,14 @@ export default {
|
||||
confirmCancel: 'Möchten Sie wirklich abbrechen? 50% der Ressourcen werden zurückerstattet.',
|
||||
level: 'Stufe',
|
||||
gmModeActivated: '',
|
||||
upgradeToLevel: 'Auf Stufe aufrüsten'
|
||||
upgradeToLevel: 'Auf Stufe aufrüsten',
|
||||
tabs: {
|
||||
all: 'Alle',
|
||||
buildings: 'Gebäude',
|
||||
research: 'Forschung',
|
||||
ships: 'Schiffe',
|
||||
defense: 'Verteidigung'
|
||||
}
|
||||
},
|
||||
overview: {
|
||||
title: 'Planetenübersicht',
|
||||
@@ -592,7 +616,8 @@ export default {
|
||||
debris: 'Trümmer',
|
||||
giftPlanetTitle: 'Geschenk senden',
|
||||
giftPlanetMessage:
|
||||
'Möchten Sie wirklich Ressourcen als Geschenk an Planet [{coordinates}] senden?\n\nBitte gehen Sie zur Flottenseite, um Transporter auszuwählen und Ressourcen zu laden.'
|
||||
'Möchten Sie wirklich Ressourcen als Geschenk an Planet [{coordinates}] senden?\n\nBitte gehen Sie zur Flottenseite, um Transporter auszuwählen und Ressourcen zu laden.',
|
||||
npcPlanetName: '{name}s Planet'
|
||||
},
|
||||
messagesView: {
|
||||
title: 'Nachrichten',
|
||||
@@ -631,6 +656,7 @@ export default {
|
||||
targetPlanet: 'Zielplanet',
|
||||
attackerRemaining: 'Angreifer verblieben',
|
||||
defenderRemaining: 'Verteidiger verblieben',
|
||||
allDestroyed: 'Alle zerstört',
|
||||
moonChance: 'Mondchance',
|
||||
showRoundDetails: 'Rundendetails anzeigen',
|
||||
hideRoundDetails: 'Rundendetails ausblenden',
|
||||
@@ -691,7 +717,17 @@ export default {
|
||||
activityDescription: '',
|
||||
npcActivityMessage: '',
|
||||
arrivalTime: '',
|
||||
npcActivityTip: ''
|
||||
npcActivityTip: '',
|
||||
clearMessages: 'Nachrichten löschen',
|
||||
clearMessageTypes: 'Nachrichtentypen zum Löschen auswählen',
|
||||
clearBattleReports: 'Kampfberichte',
|
||||
clearSpyReports: 'Spionageberichte',
|
||||
clearSpiedNotifications: 'Spionagebenachrichtigungen',
|
||||
clearMissionReports: 'Missionsberichte',
|
||||
clearNPCActivity: 'NPC-Aktivität',
|
||||
clearGiftNotifications: 'Geschenkbenachrichtigungen',
|
||||
clearGiftRejected: 'Abgelehnte Geschenke',
|
||||
clearNow: 'Jetzt löschen'
|
||||
},
|
||||
missionReports: {
|
||||
transportSuccess: 'Transportmission erfolgreich abgeschlossen',
|
||||
@@ -796,6 +832,7 @@ export default {
|
||||
community: 'Community',
|
||||
github: 'GitHub-Repository',
|
||||
qqGroup: 'QQ-Gruppe',
|
||||
privacyPolicy: 'Datenschutzrichtlinie',
|
||||
notifications: 'Benachrichtigungseinstellungen',
|
||||
notificationsDesc: 'Verwalten Sie Benachrichtigungen im Spiel',
|
||||
notificationTypes: 'Benachrichtigungstypen',
|
||||
@@ -878,6 +915,7 @@ export default {
|
||||
completeQueuesSuccess: ''
|
||||
},
|
||||
alerts: {
|
||||
incomingFleets: '{count} feindliche Flotten im Anmarsch',
|
||||
npcSpyIncoming: 'NPC-Spionagesonde nähert sich',
|
||||
npcAttackIncoming: 'NPC-Flotten-Angriff im Anmarsch!',
|
||||
npcFleetIncoming: 'NPC-Flotte nähert sich',
|
||||
@@ -889,6 +927,30 @@ export default {
|
||||
npcSpiedYourPlanet: 'NPC hat deinen Planeten ausspioniert',
|
||||
npcAttackedYourPlanet: 'NPC hat deinen Planeten angegriffen'
|
||||
},
|
||||
enemyAlert: {
|
||||
title: 'Feindalarm',
|
||||
markAllRead: 'Alle gelesen',
|
||||
noAlerts: 'Keine Alarme',
|
||||
fleetSize: 'Flottengröße',
|
||||
ships: 'Schiffe',
|
||||
viewFleet: 'Flotte anzeigen',
|
||||
alertDetails: 'Alarmdetails',
|
||||
targetInfo: 'Zielinfo',
|
||||
arrivalTime: 'Ankunftszeit',
|
||||
countdown: 'Countdown',
|
||||
viewMessages: 'Nachrichten anzeigen',
|
||||
arrived: 'Angekommen',
|
||||
missionType: {
|
||||
spy: 'Spionage',
|
||||
attack: 'Angriff',
|
||||
unknown: 'Unbekannt'
|
||||
},
|
||||
warning: {
|
||||
spy: 'Feindliche Spionage im Anmarsch!',
|
||||
attack: 'Feindlicher Angriff im Anmarsch!',
|
||||
unknown: 'Feindliche Flotte im Anmarsch!'
|
||||
}
|
||||
},
|
||||
diplomacy: {
|
||||
title: 'Diplomatie',
|
||||
description: 'Verwalte diplomatische Beziehungen mit NPCs',
|
||||
@@ -921,16 +983,30 @@ export default {
|
||||
more: 'weitere',
|
||||
actions: {
|
||||
gift: 'Geschenk senden',
|
||||
viewPlanets: 'Planeten ansehen'
|
||||
viewPlanets: 'Planeten ansehen',
|
||||
addNote: 'Notiz hinzufügen',
|
||||
editNote: 'Notiz bearbeiten'
|
||||
},
|
||||
note: 'Notiz',
|
||||
notePlaceholder: 'Notiz eingeben...',
|
||||
noteEmpty: 'Keine Notiz',
|
||||
lastEvent: 'Letztes Ereignis',
|
||||
reportDetails: '',
|
||||
eventDescription: '',
|
||||
reputationChange: '',
|
||||
before: '',
|
||||
after: '',
|
||||
statusChange: '',
|
||||
viewDiplomacy: '',
|
||||
reportDetails: 'Diplomatischer Bericht Details',
|
||||
eventDescription: 'Ereignisbeschreibung',
|
||||
reputationChange: 'Ansehensänderung',
|
||||
before: 'Vorher',
|
||||
after: 'Nachher',
|
||||
statusChange: 'Statusänderung',
|
||||
viewDiplomacy: 'Diplomatie-Seite anzeigen',
|
||||
eventType: {
|
||||
gift: 'Ressourcen geschenkt',
|
||||
attack: 'Angriff gestartet',
|
||||
allyAttacked: 'Verbündeten angegriffen',
|
||||
spy: 'Spionage durchgeführt',
|
||||
stealDebris: 'Trümmer gestohlen',
|
||||
destroyPlanet: 'Planet zerstört',
|
||||
unknown: 'Unbekanntes Ereignis'
|
||||
},
|
||||
events: {
|
||||
gift: 'Geschenk gesendet',
|
||||
attack: 'Angriff',
|
||||
@@ -967,6 +1043,50 @@ export default {
|
||||
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.'
|
||||
},
|
||||
searchPlaceholder: 'NPC-Name suchen...',
|
||||
viewMode: {
|
||||
card: 'Karte',
|
||||
list: 'Liste'
|
||||
},
|
||||
diagnostic: {
|
||||
button: 'NPC-Diagnose',
|
||||
title: 'NPC-Statusdiagnose',
|
||||
description:
|
||||
'Spielerpunkte: {points}, Spionageintervall: {spyInterval}Min, Angriffsintervall: {attackInterval}Min, Angriffswahrscheinlichkeit: {attackProb}%',
|
||||
noData: 'Keine NPC-Daten',
|
||||
difficulty: 'Schwierigkeit',
|
||||
difficultyLevels: {
|
||||
easy: 'Einfach',
|
||||
medium: 'Mittel',
|
||||
hard: 'Schwer'
|
||||
},
|
||||
reputation: 'Ansehen',
|
||||
spyProbes: 'Spionagesonden',
|
||||
fleetPower: 'Flottenstärke',
|
||||
canSpy: 'Kann spionieren',
|
||||
canAttack: 'Kann angreifen',
|
||||
attackProbability: 'Angriffswahrscheinlichkeit',
|
||||
nextSpy: 'Nächste Spionage',
|
||||
nextAttack: 'Nächster Angriff',
|
||||
yes: 'Ja',
|
||||
no: 'Nein',
|
||||
timeFormat: '{min}m {sec}s',
|
||||
anytime: 'Jederzeit',
|
||||
statusExplanation: 'Statuserklärung',
|
||||
noRelation: 'Keine Beziehung',
|
||||
noRelationNeutral: 'Keine Beziehung (Neutral)',
|
||||
reasons: {
|
||||
friendlyNoAction: 'Freundliche Beziehung, wird nicht handeln',
|
||||
neutralNoAction: 'Neutrale Beziehung, wird nicht handeln',
|
||||
hostileWillAct: 'Feindliche Beziehung, kann handeln',
|
||||
noRelationNeutral: 'Keine diplomatische Beziehung, als neutral behandelt',
|
||||
insufficientProbes: 'Unzureichende Sonden (Aktuell: {current}, Benötigt: {required})',
|
||||
noFleet: 'Keine Kampfflotte',
|
||||
spyCooldown: 'Spionage auf Abklingzeit ({min}m {sec}s)',
|
||||
attackCooldown: 'Angriff auf Abklingzeit ({min}m {sec}s)',
|
||||
notSpiedYet: 'Noch nicht spioniert, zuerst Spionage nötig'
|
||||
}
|
||||
}
|
||||
},
|
||||
pagination: {
|
||||
@@ -982,6 +1102,53 @@ export default {
|
||||
description: 'Entschuldigung, die gesuchte Seite existiert nicht',
|
||||
goHome: 'Zur Startseite'
|
||||
},
|
||||
privacy: {
|
||||
title: 'Datenschutzrichtlinie',
|
||||
sections: {
|
||||
introduction: {
|
||||
title: 'Einführung',
|
||||
content:
|
||||
'Diese Datenschutzrichtlinie erklärt, wie OGame-Vue-Ts Ihre Daten behandelt. Wir sind dem Schutz Ihrer Privatsphäre verpflichtet, und dieses Spiel wurde mit vollständigem Respekt für die Privatsphäre der Benutzer entwickelt.'
|
||||
},
|
||||
dataCollection: {
|
||||
title: 'Datenerfassung',
|
||||
content: 'Dieses Spiel erfasst und speichert nur die folgenden Daten lokal in Ihrem Browser:',
|
||||
items: {
|
||||
gameProgress: 'Spielfortschritt (Gebäudestufen, Flotten, Ressourcen usw.)',
|
||||
settings: 'Spieleinstellungen (Benachrichtigungseinstellungen, Anzeigeoptionen usw.)',
|
||||
language: 'Sprachpräferenz'
|
||||
}
|
||||
},
|
||||
dataStorage: {
|
||||
title: 'Datenspeicherung',
|
||||
content:
|
||||
'Alle Daten werden im lokalen Speicher (localStorage) Ihres Browsers gespeichert. Dies bedeutet, dass Ihre Daten immer auf Ihrem eigenen Gerät verbleiben und wir keinen Zugriff auf Ihre Spieldaten haben, diese nicht einsehen oder erfassen können.'
|
||||
},
|
||||
noServer: {
|
||||
title: 'Keine Serverkommunikation',
|
||||
content:
|
||||
'Dieses Spiel ist ein vollständig offline funktionierendes Einzelspielerspiel. Abgesehen von der Update-Prüfungsfunktion (die Versionsinformationen von GitHub abruft) kommuniziert das Spiel mit keinem Server. Ihre Spieldaten verlassen niemals Ihr Gerät.'
|
||||
},
|
||||
thirdParty: {
|
||||
title: 'Drittanbieterdienste',
|
||||
content:
|
||||
'Dieses Spiel verwendet Analyse-Dienste von Drittanbietern, um Besucherstatistiken und Traffic-Quellen zu erfassen. Dies hilft uns, Nutzungsmuster zu verstehen und das Spielerlebnis zu verbessern. Diese Analysedaten sind anonym und enthalten keine persönlich identifizierbaren Informationen. Wir verwenden keine Werbedienste oder andere kommerzielle Tracking-Tools.'
|
||||
},
|
||||
dataControl: {
|
||||
title: 'Datenkontrolle',
|
||||
content: 'Sie haben die vollständige Kontrolle über Ihre Daten:',
|
||||
items: {
|
||||
export: 'Sie können Ihre Spieldaten jederzeit exportieren',
|
||||
import: 'Sie können Daten aus Sicherungsdateien importieren',
|
||||
delete: 'Sie können alle Daten löschen, indem Sie die Browserdaten löschen oder die Funktion "Daten löschen" im Spiel verwenden'
|
||||
}
|
||||
},
|
||||
contact: {
|
||||
title: 'Kontakt',
|
||||
content: 'Wenn Sie Fragen zu dieser Datenschutzrichtlinie haben, kontaktieren Sie uns bitte über:'
|
||||
}
|
||||
}
|
||||
},
|
||||
time: {
|
||||
days: 'Tage',
|
||||
hours: 'Stunden',
|
||||
@@ -1015,5 +1182,78 @@ export default {
|
||||
'Klicken Sie auf das Warteschlangensymbol oben rechts, um den Baufortschritt anzuzeigen. Sie können weiter andere Seiten durchsuchen - der Bau läuft im Hintergrund.'
|
||||
}
|
||||
}
|
||||
},
|
||||
hints: {
|
||||
close: 'Schließen',
|
||||
gotIt: 'Verstanden',
|
||||
dontShowAgain: 'Nicht mehr anzeigen',
|
||||
resetHints: 'Hinweise zurücksetzen',
|
||||
resetHintsDesc: 'Alle Hinweise wieder anzeigen',
|
||||
hintsEnabled: 'Hinweise aktivieren',
|
||||
hintsEnabledDesc: 'Hilfreiche Hinweise beim Seitenbesuch anzeigen',
|
||||
overview: {
|
||||
title: 'Planetenübersicht',
|
||||
message:
|
||||
'Hier sehen Sie Planetenressourcen, Flottenstatus und Produktionsdetails. Schauen Sie regelmäßig vorbei, um Ihren Fortschritt zu überwachen!'
|
||||
},
|
||||
buildings: {
|
||||
title: 'Gebäude',
|
||||
message:
|
||||
'Bauen und verbessern Sie Strukturen hier. Beginnen Sie mit dem Solarkraftwerk für Energie, dann Ressourcenminen. Tipp: Roboterfabrik beschleunigt den Bau!'
|
||||
},
|
||||
research: {
|
||||
title: 'Forschungslabor',
|
||||
message:
|
||||
'Erforschen Sie Technologien, um neue Schiffe freizuschalten, Kampfkraft zu verbessern und Ihre Zivilisation voranzubringen. Energietechnik ist ein guter Anfang!'
|
||||
},
|
||||
shipyard: {
|
||||
title: 'Raumschiffswerft',
|
||||
message:
|
||||
'Bauen Sie Schiffe zum Erkunden, Transportieren von Ressourcen und Verteidigen Ihres Imperiums. Frachter helfen beim Transport zwischen Planeten.'
|
||||
},
|
||||
fleet: {
|
||||
title: 'Flottenkommando',
|
||||
message:
|
||||
'Senden Sie Ihre Schiffe auf Missionen: Feinde angreifen, Ressourcen transportieren, neue Planeten besiedeln oder Trümmerfelder erkunden.'
|
||||
},
|
||||
galaxy: {
|
||||
title: 'Galaxiekarte',
|
||||
message:
|
||||
'Erkunden Sie die Galaxie, um leere Planeten zum Besiedeln, Trümmerfelder zum Ernten und Feinde zum Angreifen zu finden. Nutzen Sie zuerst Spionagesonden!'
|
||||
},
|
||||
diplomacy: {
|
||||
title: 'Diplomatie',
|
||||
message:
|
||||
'Verwalten Sie Beziehungen mit NPCs. Senden Sie Geschenke, um den Ruf zu verbessern, oder stellen Sie sich feindlichen Angriffen. Verbündete Ihrer Feinde könnten auch feindlich werden!'
|
||||
},
|
||||
messages: {
|
||||
title: 'Nachrichten',
|
||||
message:
|
||||
'Sehen Sie hier Kampfberichte, Spionageberichte und diplomatische Benachrichtigungen. Verfolgen Sie Ihre Aktivitäten und Feindkontakte.'
|
||||
},
|
||||
defense: {
|
||||
title: 'Planetenverteidigung',
|
||||
message:
|
||||
'Bauen Sie Verteidigungsstrukturen, um Ihren Planeten vor Angriffen zu schützen. Schilde und Geschütze können Angreifer abschrecken!'
|
||||
},
|
||||
officers: {
|
||||
title: 'Offiziere',
|
||||
message:
|
||||
'Rekrutieren Sie Offiziere für verschiedene Boni! Kommandant beschleunigt den Bau, Geologe steigert Ressourcenproduktion, Admiral verbessert Flottenkapazitäten.'
|
||||
},
|
||||
simulator: {
|
||||
title: 'Kampfsimulator',
|
||||
message:
|
||||
'Simulieren Sie Kampfergebnisse vor dem Angriff. Geben Sie Flotten und Technologiestufen ein, um Sieg, Verluste und Beute vorherzusagen.'
|
||||
},
|
||||
settings: {
|
||||
title: 'Einstellungen',
|
||||
message: 'Verwalten Sie hier Spieldaten, Benachrichtigungen und Import/Export. Sichern Sie regelmäßig Ihren Fortschritt!'
|
||||
},
|
||||
gm: {
|
||||
title: 'GM-Panel',
|
||||
message:
|
||||
'Der GM-Modus ermöglicht schnelle Änderung von Ressourcen, Gebäuden und Technologiestufen. Nutzen Sie ihn zum Testen oder für vollständige Spielinhalte.'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
export default {
|
||||
home: {
|
||||
subtitle: 'Conquer the Stars',
|
||||
startGame: 'Start Game',
|
||||
privacyAgreement: 'Privacy Agreement',
|
||||
privacyAgreementDesc: 'Please read and agree to our privacy policy before starting the game.',
|
||||
agreeToPrivacy: 'I have read and agree to',
|
||||
viewFullPolicy: 'View Full Policy',
|
||||
agreeAndStart: 'Agree & Start'
|
||||
},
|
||||
common: {
|
||||
confirm: 'Confirm',
|
||||
cancel: 'Cancel',
|
||||
@@ -35,7 +44,8 @@ export default {
|
||||
requirementsNotMet: 'Requirements Not Met',
|
||||
current: 'Current',
|
||||
level: 'Level',
|
||||
gmModeActivated: 'GM Mode Activated! Check the navigation menu.'
|
||||
gmModeActivated: 'GM Mode Activated! Check the navigation menu.',
|
||||
view: 'View'
|
||||
},
|
||||
errors: {
|
||||
requirementsNotMet: 'Requirements not met',
|
||||
@@ -112,7 +122,13 @@ export default {
|
||||
homePlanet: 'Home Planet',
|
||||
planetPrefix: 'Planet',
|
||||
moonSuffix: "'s Moon",
|
||||
colonyPrefix: 'Colony'
|
||||
colonyPrefix: 'Colony',
|
||||
renamePlanet: 'Rename Planet',
|
||||
renamePlanetTitle: 'Rename Planet',
|
||||
newPlanetName: 'New Name',
|
||||
planetNamePlaceholder: 'Enter new planet name',
|
||||
rename: 'Rename',
|
||||
renameSuccess: 'Planet renamed to {name}'
|
||||
},
|
||||
player: {
|
||||
points: 'Total Points'
|
||||
@@ -282,7 +298,8 @@ export default {
|
||||
hyperspaceTechnology: 'Hyperspace jump technology',
|
||||
plasmaTechnology: 'Plasma weapon technology',
|
||||
computerTechnology: 'Increases research queue and fleet mission slots, +1 queue +1 slot per level (max 10 levels)',
|
||||
espionageTechnology: 'Improves spy probe effectiveness, +1 espionage level per level. Spy level = your level - enemy level + probes/5. ≥-1 shows fleet, ≥1 shows defense, ≥3 shows buildings, ≥5 shows technologies',
|
||||
espionageTechnology:
|
||||
'Improves spy probe effectiveness, +1 espionage level per level. Spy level = your level - enemy level + probes/5. ≥-1 shows fleet, ≥1 shows defense, ≥3 shows buildings, ≥5 shows technologies',
|
||||
weaponsTechnology: 'Increases ship and defense attack power by 10% per level',
|
||||
shieldingTechnology: 'Increases ship and defense shields by 10% per level',
|
||||
armourTechnology: 'Increases ship and defense armour by 10% per level',
|
||||
@@ -315,8 +332,8 @@ export default {
|
||||
darkMatterSpecialist: 'Improves dark matter collection efficiency'
|
||||
},
|
||||
queue: {
|
||||
title: 'Build Queue',
|
||||
empty: 'No active tasks',
|
||||
title: 'Active Tasks',
|
||||
empty: 'No active queues',
|
||||
buildQueueBonus: 'Build Queue',
|
||||
spaceBonus: 'Space Bonus',
|
||||
buildSpeedBonus: 'Build Speed Bonus',
|
||||
@@ -332,7 +349,14 @@ export default {
|
||||
confirmCancel: 'Are you sure you want to cancel? 50% of resources will be refunded.',
|
||||
level: 'Level',
|
||||
quantity: 'Quantity',
|
||||
upgradeToLevel: 'Upgrade to Level'
|
||||
upgradeToLevel: 'Upgrade to Level',
|
||||
tabs: {
|
||||
all: 'All',
|
||||
buildings: 'Buildings',
|
||||
research: 'Research',
|
||||
ships: 'Ships',
|
||||
defense: 'Defense'
|
||||
}
|
||||
},
|
||||
overview: {
|
||||
title: 'Planet Overview',
|
||||
@@ -590,7 +614,8 @@ export default {
|
||||
debris: 'Debris',
|
||||
giftPlanetTitle: 'Send Gift',
|
||||
giftPlanetMessage:
|
||||
'Are you sure you want to send resources as a gift to planet [{coordinates}]?\n\nPlease go to the fleet page to select transport ships and load resources.'
|
||||
'Are you sure you want to send resources as a gift to planet [{coordinates}]?\n\nPlease go to the fleet page to select transport ships and load resources.',
|
||||
npcPlanetName: "{name}'s Planet"
|
||||
},
|
||||
messagesView: {
|
||||
title: 'Messages',
|
||||
@@ -622,6 +647,9 @@ export default {
|
||||
buildings: 'Buildings',
|
||||
unread: 'Unread',
|
||||
targetPlanet: 'Target Planet',
|
||||
attackerRemaining: 'Attacker Remaining',
|
||||
defenderRemaining: 'Defender Remaining',
|
||||
allDestroyed: 'All destroyed',
|
||||
spied: 'Spied',
|
||||
spiedNotification: 'Spied Notification',
|
||||
noSpiedNotifications: 'No spied notifications',
|
||||
@@ -681,7 +709,18 @@ export default {
|
||||
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'
|
||||
npcActivityTip: 'NPCs may collect debris from battles. You can try to reach the location first if you want to compete for resources',
|
||||
// Clear messages
|
||||
clearMessages: 'Clear Messages',
|
||||
clearMessageTypes: 'Select message types to clear',
|
||||
clearBattleReports: 'Battle Reports',
|
||||
clearSpyReports: 'Spy Reports',
|
||||
clearSpiedNotifications: 'Spied Notifications',
|
||||
clearMissionReports: 'Mission Reports',
|
||||
clearNPCActivity: 'NPC Activity',
|
||||
clearGiftNotifications: 'Gift Notifications',
|
||||
clearGiftRejected: 'Rejected Gifts',
|
||||
clearNow: 'Clear Now'
|
||||
},
|
||||
missionReports: {
|
||||
transportSuccess: 'Transport mission completed successfully',
|
||||
@@ -789,6 +828,11 @@ export default {
|
||||
community: 'Community',
|
||||
github: 'GitHub Repository',
|
||||
qqGroup: 'QQ Group',
|
||||
privacyPolicy: 'Privacy Policy',
|
||||
displaySettings: 'Display Settings',
|
||||
displaySettingsDesc: 'Adjust visual effects',
|
||||
backgroundAnimation: 'Background Animation',
|
||||
backgroundAnimationDesc: 'Show starfield/particle background animation (may affect performance)',
|
||||
notifications: 'Notification Settings',
|
||||
notificationsDesc: 'Manage in-game notification alerts',
|
||||
notificationTypes: 'Notification Types',
|
||||
@@ -872,6 +916,7 @@ export default {
|
||||
'Completed {buildingCount} building queues, {researchCount} research queues, {missionCount} fleet missions, {missileCount} missile attacks'
|
||||
},
|
||||
alerts: {
|
||||
incomingFleets: '{count} Incoming Enemy Fleets',
|
||||
npcSpyIncoming: 'NPC Spy Probe Incoming',
|
||||
npcAttackIncoming: 'NPC Fleet Attack Incoming!',
|
||||
npcFleetIncoming: 'NPC Fleet Approaching',
|
||||
@@ -883,6 +928,30 @@ export default {
|
||||
npcSpiedYourPlanet: 'NPC spied your planet',
|
||||
npcAttackedYourPlanet: 'NPC attacked your planet'
|
||||
},
|
||||
enemyAlert: {
|
||||
title: 'Enemy Alert',
|
||||
markAllRead: 'Mark All Read',
|
||||
noAlerts: 'No alerts',
|
||||
fleetSize: 'Fleet Size',
|
||||
ships: 'ships',
|
||||
viewFleet: 'View Fleet',
|
||||
alertDetails: 'Alert Details',
|
||||
targetInfo: 'Target Info',
|
||||
arrivalTime: 'Arrival Time',
|
||||
countdown: 'Countdown',
|
||||
viewMessages: 'View Messages',
|
||||
arrived: 'Arrived',
|
||||
missionType: {
|
||||
spy: 'Spy',
|
||||
attack: 'Attack',
|
||||
unknown: 'Unknown'
|
||||
},
|
||||
warning: {
|
||||
spy: 'Enemy spy incoming!',
|
||||
attack: 'Enemy attack incoming!',
|
||||
unknown: 'Enemy fleet incoming!'
|
||||
}
|
||||
},
|
||||
diplomacy: {
|
||||
title: 'Diplomacy',
|
||||
description: 'Manage diplomatic relations with NPCs',
|
||||
@@ -915,8 +984,13 @@ export default {
|
||||
more: 'more',
|
||||
actions: {
|
||||
gift: 'Send Gift',
|
||||
viewPlanets: 'View Planets'
|
||||
viewPlanets: 'View Planets',
|
||||
addNote: 'Add Note',
|
||||
editNote: 'Edit Note'
|
||||
},
|
||||
note: 'Note',
|
||||
notePlaceholder: 'Enter note...',
|
||||
noteEmpty: 'No note',
|
||||
lastEvent: 'Last Event',
|
||||
reportDetails: 'Diplomatic Report Details',
|
||||
eventDescription: 'Event Description',
|
||||
@@ -925,6 +999,15 @@ export default {
|
||||
after: 'After',
|
||||
statusChange: 'Status Change',
|
||||
viewDiplomacy: 'View Diplomacy Page',
|
||||
eventType: {
|
||||
gift: 'Sent resources',
|
||||
attack: 'Launched an attack',
|
||||
allyAttacked: 'Attacked an ally',
|
||||
spy: 'Conducted espionage',
|
||||
stealDebris: 'Stole debris',
|
||||
destroyPlanet: 'Destroyed a planet',
|
||||
unknown: 'Unknown event'
|
||||
},
|
||||
events: {
|
||||
gift: 'Sent Gift',
|
||||
attack: 'Attack',
|
||||
@@ -961,6 +1044,50 @@ export default {
|
||||
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."
|
||||
},
|
||||
searchPlaceholder: 'Search NPC name...',
|
||||
viewMode: {
|
||||
card: 'Card',
|
||||
list: 'List'
|
||||
},
|
||||
diagnostic: {
|
||||
button: 'NPC Diagnostic',
|
||||
title: 'NPC Status Diagnostic',
|
||||
description:
|
||||
'Player points: {points}, Spy interval: {spyInterval}min, Attack interval: {attackInterval}min, Attack probability: {attackProb}%',
|
||||
noData: 'No NPC data',
|
||||
difficulty: 'Difficulty',
|
||||
difficultyLevels: {
|
||||
easy: 'Easy',
|
||||
medium: 'Medium',
|
||||
hard: 'Hard'
|
||||
},
|
||||
reputation: 'Reputation',
|
||||
spyProbes: 'Spy Probes',
|
||||
fleetPower: 'Fleet Power',
|
||||
canSpy: 'Can Spy',
|
||||
canAttack: 'Can Attack',
|
||||
attackProbability: 'Attack Probability',
|
||||
nextSpy: 'Next Spy',
|
||||
nextAttack: 'Next Attack',
|
||||
yes: 'Yes',
|
||||
no: 'No',
|
||||
timeFormat: '{min}m {sec}s',
|
||||
anytime: 'Anytime',
|
||||
statusExplanation: 'Status Explanation',
|
||||
noRelation: 'No Relation',
|
||||
noRelationNeutral: 'No Relation (Neutral)',
|
||||
reasons: {
|
||||
friendlyNoAction: 'Friendly relationship, will not act',
|
||||
neutralNoAction: 'Neutral relationship, will not act',
|
||||
hostileWillAct: 'Hostile relationship, may take action',
|
||||
noRelationNeutral: 'No diplomatic relation, treated as neutral',
|
||||
insufficientProbes: 'Insufficient probes (Current: {current}, Required: {required})',
|
||||
noFleet: 'No combat fleet',
|
||||
spyCooldown: 'Spy on cooldown ({min}m {sec}s)',
|
||||
attackCooldown: 'Attack on cooldown ({min}m {sec}s)',
|
||||
notSpiedYet: 'Not yet spied, need to spy first'
|
||||
}
|
||||
}
|
||||
},
|
||||
pagination: {
|
||||
@@ -975,6 +1102,53 @@ export default {
|
||||
description: 'Sorry, the page you are looking for does not exist',
|
||||
goHome: 'Go Home'
|
||||
},
|
||||
privacy: {
|
||||
title: 'Privacy Policy',
|
||||
sections: {
|
||||
introduction: {
|
||||
title: 'Introduction',
|
||||
content:
|
||||
'This privacy policy explains how OGame-Vue-Ts handles your data. We are committed to protecting your privacy, and this game is designed with complete respect for user privacy.'
|
||||
},
|
||||
dataCollection: {
|
||||
title: 'Data Collection',
|
||||
content: 'This game only collects and stores the following data locally in your browser:',
|
||||
items: {
|
||||
gameProgress: 'Game progress (building levels, fleets, resources, etc.)',
|
||||
settings: 'Game settings (notification preferences, display options, etc.)',
|
||||
language: 'Language preference'
|
||||
}
|
||||
},
|
||||
dataStorage: {
|
||||
title: 'Data Storage',
|
||||
content:
|
||||
"All data is stored in your browser's local storage (localStorage). This means your data always remains on your own device, and we cannot access, view, or collect any of your game data."
|
||||
},
|
||||
noServer: {
|
||||
title: 'No Server Communication',
|
||||
content:
|
||||
'This game is a completely offline single-player game. Except for the update check feature (which fetches version information from GitHub), the game does not communicate with any server. Your game data never leaves your device.'
|
||||
},
|
||||
thirdParty: {
|
||||
title: 'Third-Party Services',
|
||||
content:
|
||||
'This game uses third-party analytics services to track visitor statistics and traffic sources, helping us understand usage patterns and improve the gaming experience. This analytics data is anonymous and does not contain any personally identifiable information. We do not use any advertising services or other commercial tracking tools.'
|
||||
},
|
||||
dataControl: {
|
||||
title: 'Data Control',
|
||||
content: 'You have complete control over your data:',
|
||||
items: {
|
||||
export: 'You can export your game data at any time',
|
||||
import: 'You can import data from backup files',
|
||||
delete: 'You can delete all data by clearing browser data or using the in-game "Clear Data" feature'
|
||||
}
|
||||
},
|
||||
contact: {
|
||||
title: 'Contact Us',
|
||||
content: 'If you have any questions about this privacy policy, please contact us via:'
|
||||
}
|
||||
}
|
||||
},
|
||||
time: {
|
||||
days: 'days',
|
||||
hours: 'hours',
|
||||
@@ -1131,5 +1305,72 @@ export default {
|
||||
"Great! You've mastered the basics. Continue building Crystal and Deuterium facilities, then explore other features. Remember: energy first, then resources!"
|
||||
}
|
||||
}
|
||||
},
|
||||
hints: {
|
||||
close: 'Close',
|
||||
gotIt: 'Got it',
|
||||
dontShowAgain: "Don't show again",
|
||||
resetHints: 'Reset Hints',
|
||||
resetHintsDesc: 'Show all hints again',
|
||||
hintsEnabled: 'Enable Hints',
|
||||
hintsEnabledDesc: 'Show helpful hints when visiting pages',
|
||||
overview: {
|
||||
title: 'Planet Overview',
|
||||
message: 'Here you can see your planet resources, fleet status, and production details. Check back often to monitor your progress!'
|
||||
},
|
||||
buildings: {
|
||||
title: 'Buildings',
|
||||
message:
|
||||
'Build and upgrade structures here. Start with Solar Plant for energy, then resource mines. Tip: Robotics Factory speeds up construction!'
|
||||
},
|
||||
research: {
|
||||
title: 'Research Lab',
|
||||
message:
|
||||
'Research technologies to unlock new ships, improve combat, and advance your civilization. Energy Technology is a great start!'
|
||||
},
|
||||
shipyard: {
|
||||
title: 'Shipyard',
|
||||
message: 'Build ships to explore, transport resources, and defend your empire. Cargo ships help move resources between planets.'
|
||||
},
|
||||
fleet: {
|
||||
title: 'Fleet Command',
|
||||
message: 'Send your ships on missions: attack enemies, transport resources, colonize new planets, or explore debris fields.'
|
||||
},
|
||||
galaxy: {
|
||||
title: 'Galaxy Map',
|
||||
message:
|
||||
'Explore the galaxy to find empty planets to colonize, debris fields to harvest, and enemies to attack. Use spy probes first!'
|
||||
},
|
||||
diplomacy: {
|
||||
title: 'Diplomacy',
|
||||
message:
|
||||
'Manage relations with NPCs. Send gifts to improve reputation, or face hostile attacks. Allies of your enemies may turn hostile too!'
|
||||
},
|
||||
messages: {
|
||||
title: 'Messages',
|
||||
message: 'View battle reports, spy reports, and diplomatic notifications here. Keep track of your activities and enemy encounters.'
|
||||
},
|
||||
defense: {
|
||||
title: 'Planetary Defense',
|
||||
message: 'Build defense structures to protect your planet from attacks. Shield domes and turrets can deter raiders!'
|
||||
},
|
||||
officers: {
|
||||
title: 'Officers',
|
||||
message:
|
||||
'Recruit officers to gain various bonuses! Commander speeds up construction, Geologist boosts resource production, Admiral enhances fleet capabilities.'
|
||||
},
|
||||
simulator: {
|
||||
title: 'Battle Simulator',
|
||||
message: 'Simulate battle outcomes before attacking. Enter both fleets and tech levels to predict victory, losses, and loot.'
|
||||
},
|
||||
settings: {
|
||||
title: 'Settings',
|
||||
message: 'Manage game data, adjust notifications, export/import saves here. Remember to backup your progress regularly!'
|
||||
},
|
||||
gm: {
|
||||
title: 'GM Panel',
|
||||
message:
|
||||
'GM mode allows quick modification of resources, buildings, and tech levels. Use it for testing or experiencing full game content.'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
export default {
|
||||
home: {
|
||||
subtitle: '星々を征服せよ',
|
||||
startGame: 'ゲーム開始',
|
||||
privacyAgreement: 'プライバシーポリシー',
|
||||
privacyAgreementDesc: 'ゲームを開始する前に、プライバシーポリシーをお読みになり、同意してください。',
|
||||
agreeToPrivacy: '読んで同意しました',
|
||||
viewFullPolicy: '全文を見る',
|
||||
agreeAndStart: '同意して開始'
|
||||
},
|
||||
common: {
|
||||
confirm: '確認',
|
||||
cancel: 'キャンセル',
|
||||
@@ -36,7 +45,8 @@ export default {
|
||||
requirementsNotMet: '必要条件が満たされていません',
|
||||
current: '現在',
|
||||
level: 'レベル',
|
||||
gmModeActivated: 'GMモードが有効になりました!ナビゲーションメニューをご確認ください。'
|
||||
gmModeActivated: 'GMモードが有効になりました!ナビゲーションメニューをご確認ください。',
|
||||
view: '表示'
|
||||
},
|
||||
errors: {
|
||||
requirementsNotMet: '前提条件を満たしていません',
|
||||
@@ -113,7 +123,13 @@ export default {
|
||||
homePlanet: '母星',
|
||||
planetPrefix: '惑星',
|
||||
moonSuffix: 'の月',
|
||||
colonyPrefix: 'コロニー'
|
||||
colonyPrefix: 'コロニー',
|
||||
renamePlanet: '惑星名を変更',
|
||||
renamePlanetTitle: '惑星名を変更',
|
||||
newPlanetName: '新しい名前',
|
||||
planetNamePlaceholder: '新しい惑星名を入力',
|
||||
rename: '名前変更',
|
||||
renameSuccess: '惑星名を {name} に変更しました'
|
||||
},
|
||||
player: {
|
||||
points: '総ポイント'
|
||||
@@ -284,7 +300,8 @@ export default {
|
||||
hyperspaceTechnology: 'ハイパースペースジャンプ技術',
|
||||
plasmaTechnology: 'プラズマ兵器技術',
|
||||
computerTechnology: '研究キューと艦隊任務スロットを増加、レベル毎に+1キュー+1スロット(最大10レベル)',
|
||||
espionageTechnology: 'スパイ探査機の効果を向上、レベル毎に偵察深度+1。偵察レベル=自分のレベル-相手のレベル+探査機数/5。≥-1で艦隊表示、≥1で防御表示、≥3で建物表示、≥5で技術表示',
|
||||
espionageTechnology:
|
||||
'スパイ探査機の効果を向上、レベル毎に偵察深度+1。偵察レベル=自分のレベル-相手のレベル+探査機数/5。≥-1で艦隊表示、≥1で防御表示、≥3で建物表示、≥5で技術表示',
|
||||
weaponsTechnology: '艦船と防御の攻撃力をレベル毎に10%増加',
|
||||
shieldingTechnology: '艦船と防御のシールドをレベル毎に10%増加',
|
||||
armourTechnology: '艦船と防御の装甲をレベル毎に10%増加',
|
||||
@@ -317,8 +334,8 @@ export default {
|
||||
darkMatterSpecialist: 'ダークマター採取効率を向上'
|
||||
},
|
||||
queue: {
|
||||
title: '建設キュー',
|
||||
empty: '進行中のタスクはありません',
|
||||
title: '進行中のタスク',
|
||||
empty: '進行中のキューはありません',
|
||||
buildQueue: '建設キュー',
|
||||
researchQueue: '研究キュー',
|
||||
building: '建設中',
|
||||
@@ -331,7 +348,14 @@ export default {
|
||||
confirmCancel: 'キャンセルしますか?資源の50%が返還されます。',
|
||||
level: 'レベル',
|
||||
gmModeActivated: '',
|
||||
upgradeToLevel: 'レベルにアップグレード'
|
||||
upgradeToLevel: 'レベルにアップグレード',
|
||||
tabs: {
|
||||
all: 'すべて',
|
||||
buildings: '建物',
|
||||
research: '研究',
|
||||
ships: '艦船',
|
||||
defense: '防衛'
|
||||
}
|
||||
},
|
||||
shipyard: {
|
||||
attack: '攻撃力',
|
||||
@@ -585,7 +609,8 @@ export default {
|
||||
sendGift: 'ギフト送信',
|
||||
debris: '破片',
|
||||
giftPlanetTitle: 'ギフト送信',
|
||||
giftPlanetMessage: '惑星[{coordinates}]にリソースを贈りますか?\n\n艦隊ページに移動して輸送船を選択し、リソースを積載してください。'
|
||||
giftPlanetMessage: '惑星[{coordinates}]にリソースを贈りますか?\n\n艦隊ページに移動して輸送船を選択し、リソースを積載してください。',
|
||||
npcPlanetName: '{name}の惑星'
|
||||
},
|
||||
messagesView: {
|
||||
title: 'メッセージセンター',
|
||||
@@ -619,6 +644,7 @@ export default {
|
||||
targetPlanet: '目標惑星',
|
||||
attackerRemaining: '攻撃側残存',
|
||||
defenderRemaining: '防御側残存',
|
||||
allDestroyed: '全て破壊',
|
||||
moonChance: '月生成確率',
|
||||
showRoundDetails: 'ラウンド詳細表示',
|
||||
hideRoundDetails: 'ラウンド詳細非表示',
|
||||
@@ -684,7 +710,17 @@ export default {
|
||||
activityDescription: '',
|
||||
npcActivityMessage: '',
|
||||
arrivalTime: '',
|
||||
npcActivityTip: ''
|
||||
npcActivityTip: '',
|
||||
clearMessages: 'メッセージをクリア',
|
||||
clearMessageTypes: 'クリアするメッセージタイプを選択',
|
||||
clearBattleReports: '戦闘レポート',
|
||||
clearSpyReports: '偵察レポート',
|
||||
clearSpiedNotifications: '偵察通知',
|
||||
clearMissionReports: 'ミッションレポート',
|
||||
clearNPCActivity: 'NPCアクティビティ',
|
||||
clearGiftNotifications: 'ギフト通知',
|
||||
clearGiftRejected: '拒否されたギフト',
|
||||
clearNow: '今すぐクリア'
|
||||
},
|
||||
missionReports: {
|
||||
transportSuccess: '輸送ミッションが正常に完了しました',
|
||||
@@ -787,6 +823,7 @@ export default {
|
||||
community: 'コミュニティ',
|
||||
github: 'GitHubリポジトリ',
|
||||
qqGroup: 'QQグループ',
|
||||
privacyPolicy: 'プライバシーポリシー',
|
||||
notifications: '通知設定',
|
||||
notificationsDesc: 'ゲーム内の通知アラートを管理',
|
||||
notificationTypes: '通知タイプ',
|
||||
@@ -868,6 +905,7 @@ export default {
|
||||
completeQueuesSuccess: ''
|
||||
},
|
||||
alerts: {
|
||||
incomingFleets: '{count}機の敵艦隊が接近中',
|
||||
npcSpyIncoming: 'NPC偵察プローブが接近中',
|
||||
npcAttackIncoming: 'NPC艦隊攻撃が接近中!',
|
||||
npcFleetIncoming: 'NPC艦隊が接近中',
|
||||
@@ -879,6 +917,30 @@ export default {
|
||||
npcSpiedYourPlanet: 'NPCがあなたの惑星を偵察しました',
|
||||
npcAttackedYourPlanet: 'NPCがあなたの惑星を攻撃しました'
|
||||
},
|
||||
enemyAlert: {
|
||||
title: '敵警報',
|
||||
markAllRead: 'すべて既読',
|
||||
noAlerts: '警報なし',
|
||||
fleetSize: '艦隊規模',
|
||||
ships: '隻',
|
||||
viewFleet: '艦隊を見る',
|
||||
alertDetails: '警報詳細',
|
||||
targetInfo: 'ターゲット情報',
|
||||
arrivalTime: '到着時間',
|
||||
countdown: 'カウントダウン',
|
||||
viewMessages: 'メッセージを見る',
|
||||
arrived: '到着済み',
|
||||
missionType: {
|
||||
spy: '偵察',
|
||||
attack: '攻撃',
|
||||
unknown: '不明'
|
||||
},
|
||||
warning: {
|
||||
spy: '敵の偵察が接近中!',
|
||||
attack: '敵の攻撃が接近中!',
|
||||
unknown: '敵艦隊が接近中!'
|
||||
}
|
||||
},
|
||||
diplomacy: {
|
||||
title: '外交',
|
||||
description: 'NPCとの外交関係を管理',
|
||||
@@ -911,16 +973,30 @@ export default {
|
||||
more: 'その他',
|
||||
actions: {
|
||||
gift: 'ギフトを送る',
|
||||
viewPlanets: '惑星を表示'
|
||||
viewPlanets: '惑星を表示',
|
||||
addNote: 'メモを追加',
|
||||
editNote: 'メモを編集'
|
||||
},
|
||||
note: 'メモ',
|
||||
notePlaceholder: 'メモを入力...',
|
||||
noteEmpty: 'メモなし',
|
||||
lastEvent: '最後のイベント',
|
||||
reportDetails: '',
|
||||
eventDescription: '',
|
||||
reputationChange: '',
|
||||
before: '',
|
||||
after: '',
|
||||
statusChange: '',
|
||||
viewDiplomacy: '',
|
||||
reportDetails: '外交レポート詳細',
|
||||
eventDescription: 'イベント説明',
|
||||
reputationChange: '評判変化',
|
||||
before: '前',
|
||||
after: '後',
|
||||
statusChange: '関係状態変化',
|
||||
viewDiplomacy: '外交ページを表示',
|
||||
eventType: {
|
||||
gift: 'リソースを贈呈',
|
||||
attack: '攻撃を開始',
|
||||
allyAttacked: '同盟を攻撃',
|
||||
spy: '偵察を実施',
|
||||
stealDebris: '残骸を略奪',
|
||||
destroyPlanet: '惑星を破壊',
|
||||
unknown: '不明なイベント'
|
||||
},
|
||||
events: {
|
||||
gift: 'ギフト送信',
|
||||
attack: '攻撃',
|
||||
@@ -957,6 +1033,49 @@ export default {
|
||||
allyOutraged: '{allyName}はあなたが同盟{targetName}の{planetName}を破壊したことに激怒しています',
|
||||
npcEliminated: 'NPC {npcName}は完全に排除されました',
|
||||
npcEliminatedMessage: 'あなたは{npcName}のすべての惑星を破壊しました!この勢力は完全に壊滅しました。'
|
||||
},
|
||||
searchPlaceholder: 'NPC名で検索...',
|
||||
viewMode: {
|
||||
card: 'カード',
|
||||
list: 'リスト'
|
||||
},
|
||||
diagnostic: {
|
||||
button: 'NPC状態診断',
|
||||
title: 'NPC状態診断',
|
||||
description: 'プレイヤーポイント:{points}、偵察間隔:{spyInterval}分、攻撃間隔:{attackInterval}分、攻撃確率:{attackProb}%',
|
||||
noData: 'NPCデータがありません',
|
||||
difficulty: '難易度',
|
||||
difficultyLevels: {
|
||||
easy: '簡単',
|
||||
medium: '普通',
|
||||
hard: '難しい'
|
||||
},
|
||||
reputation: '評判',
|
||||
spyProbes: '偵察機数',
|
||||
fleetPower: '艦隊戦力',
|
||||
canSpy: '偵察可能',
|
||||
canAttack: '攻撃可能',
|
||||
attackProbability: '攻撃確率',
|
||||
nextSpy: '次の偵察',
|
||||
nextAttack: '次の攻撃',
|
||||
yes: 'はい',
|
||||
no: 'いいえ',
|
||||
timeFormat: '{min}分{sec}秒',
|
||||
anytime: 'いつでも',
|
||||
statusExplanation: '状態説明',
|
||||
noRelation: '関係なし',
|
||||
noRelationNeutral: '関係なし(中立)',
|
||||
reasons: {
|
||||
friendlyNoAction: '友好関係、行動しない',
|
||||
neutralNoAction: '中立関係、行動しない',
|
||||
hostileWillAct: '敵対関係、行動する可能性あり',
|
||||
noRelationNeutral: '外交関係なし、中立として扱う',
|
||||
insufficientProbes: '偵察機不足(現在:{current}、必要:{required})',
|
||||
noFleet: '戦闘艦隊がない',
|
||||
spyCooldown: '偵察クールダウン中({min}分{sec}秒)',
|
||||
attackCooldown: '攻撃クールダウン中({min}分{sec}秒)',
|
||||
notSpiedYet: '未偵察、先に偵察が必要'
|
||||
}
|
||||
}
|
||||
},
|
||||
pagination: {
|
||||
@@ -972,6 +1091,53 @@ export default {
|
||||
description: '申し訳ございません。お探しのページは存在しません',
|
||||
goHome: 'ホームに戻る'
|
||||
},
|
||||
privacy: {
|
||||
title: 'プライバシーポリシー',
|
||||
sections: {
|
||||
introduction: {
|
||||
title: 'はじめに',
|
||||
content:
|
||||
'このプライバシーポリシーは、OGame-Vue-Tsがお客様のデータをどのように取り扱うかを説明しています。私たちはお客様のプライバシーの保護に取り組んでおり、このゲームはユーザーのプライバシーを完全に尊重して設計されています。'
|
||||
},
|
||||
dataCollection: {
|
||||
title: 'データ収集',
|
||||
content: 'このゲームは、以下のデータのみをブラウザにローカルで収集・保存します:',
|
||||
items: {
|
||||
gameProgress: 'ゲームの進行状況(建物レベル、艦隊、資源など)',
|
||||
settings: 'ゲーム設定(通知設定、表示オプションなど)',
|
||||
language: '言語設定'
|
||||
}
|
||||
},
|
||||
dataStorage: {
|
||||
title: 'データ保存',
|
||||
content:
|
||||
'すべてのデータはブラウザのローカルストレージ(localStorage)に保存されます。これは、お客様のデータが常にお客様自身のデバイスに留まることを意味し、私たちはお客様のゲームデータにアクセス、閲覧、収集することはできません。'
|
||||
},
|
||||
noServer: {
|
||||
title: 'サーバー通信なし',
|
||||
content:
|
||||
'このゲームは完全にオフラインのシングルプレイヤーゲームです。アップデート確認機能(GitHubからバージョン情報を取得)を除き、ゲームはいかなるサーバーとも通信しません。お客様のゲームデータがデバイスから離れることはありません。'
|
||||
},
|
||||
thirdParty: {
|
||||
title: 'サードパーティサービス',
|
||||
content:
|
||||
'このゲームは、訪問者統計とトラフィックソースを追跡するためにサードパーティの分析サービスを使用しています。これにより、利用パターンを理解し、ゲーム体験を向上させることができます。この分析データは匿名であり、個人を特定できる情報は含まれていません。広告サービスやその他の商業的追跡ツールは使用していません。'
|
||||
},
|
||||
dataControl: {
|
||||
title: 'データ管理',
|
||||
content: 'お客様はご自身のデータを完全に管理できます:',
|
||||
items: {
|
||||
export: 'いつでもゲームデータをエクスポートできます',
|
||||
import: 'バックアップファイルからデータをインポートできます',
|
||||
delete: 'ブラウザデータの削除またはゲーム内の「データ削除」機能を使用してすべてのデータを削除できます'
|
||||
}
|
||||
},
|
||||
contact: {
|
||||
title: 'お問い合わせ',
|
||||
content: 'このプライバシーポリシーについてご質問がある場合は、以下よりお問い合わせください:'
|
||||
}
|
||||
}
|
||||
},
|
||||
time: {
|
||||
days: '日',
|
||||
hours: '時間',
|
||||
@@ -1005,5 +1171,67 @@ export default {
|
||||
'右上のキューアイコンをクリックして建設進度を確認できます。他のページを閲覧し続けることができます。建設はバックグラウンドで進行します。'
|
||||
}
|
||||
}
|
||||
},
|
||||
hints: {
|
||||
close: '閉じる',
|
||||
gotIt: '了解',
|
||||
dontShowAgain: '今後表示しない',
|
||||
resetHints: 'ヒントをリセット',
|
||||
resetHintsDesc: 'すべてのヒントを再表示',
|
||||
hintsEnabled: 'ヒントを有効化',
|
||||
hintsEnabledDesc: 'ページ訪問時にヘルプヒントを表示',
|
||||
overview: {
|
||||
title: '惑星概要',
|
||||
message: 'ここで惑星の資源、艦隊状況、生産詳細を確認できます。進捗を監視するために定期的にチェックしましょう!'
|
||||
},
|
||||
buildings: {
|
||||
title: '建物',
|
||||
message:
|
||||
'ここで建物を建設・アップグレードします。まず太陽光発電所でエネルギーを確保し、次に資源鉱山を建設。ヒント:ロボット工場で建設速度アップ!'
|
||||
},
|
||||
research: {
|
||||
title: '研究ラボ',
|
||||
message: '技術を研究して新しい船を解放、戦闘力を向上、文明を発展させましょう。エネルギー技術から始めるのがおすすめ!'
|
||||
},
|
||||
shipyard: {
|
||||
title: '造船所',
|
||||
message: '船を建造して探索、資源輸送、帝国防衛に活用。貨物船は惑星間で資源を運びます。'
|
||||
},
|
||||
fleet: {
|
||||
title: '艦隊司令',
|
||||
message: '船をミッションに派遣:敵を攻撃、資源を輸送、新惑星を植民、または残骸場を探索。'
|
||||
},
|
||||
galaxy: {
|
||||
title: '銀河マップ',
|
||||
message: '銀河を探索して植民可能な空き惑星、回収可能な残骸場、攻撃対象の敵を見つけましょう。まずスパイプローブで偵察!'
|
||||
},
|
||||
diplomacy: {
|
||||
title: '外交',
|
||||
message: 'NPCとの関係を管理。贈り物で評判を上げるか、敵対攻撃を受けるか。敵の同盟者もあなたに敵対するかも!'
|
||||
},
|
||||
messages: {
|
||||
title: 'メッセージ',
|
||||
message: 'ここで戦闘レポート、スパイレポート、外交通知を確認。あなたの活動と敵との遭遇を追跡。'
|
||||
},
|
||||
defense: {
|
||||
title: '惑星防衛',
|
||||
message: '防衛施設を建設して攻撃から惑星を守りましょう。シールドとタレットで侵略者を威嚇!'
|
||||
},
|
||||
officers: {
|
||||
title: '士官',
|
||||
message: '士官を雇用して様々なボーナスを獲得!司令官は建設を加速、地質学者は資源生産を向上、提督は艦隊能力を強化。'
|
||||
},
|
||||
simulator: {
|
||||
title: '戦闘シミュレーター',
|
||||
message: '攻撃前に戦闘結果をシミュレート。双方の艦隊と技術レベルを入力して、勝敗と損失を予測。'
|
||||
},
|
||||
settings: {
|
||||
title: '設定',
|
||||
message: 'ここでゲームデータの管理、通知設定、セーブのエクスポート/インポートができます。定期的にバックアップを!'
|
||||
},
|
||||
gm: {
|
||||
title: 'GM管理パネル',
|
||||
message: 'GMモードでは資源、建物、技術レベルを素早く変更できます。テストや完全なゲームコンテンツの体験に使用。'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
export default {
|
||||
home: {
|
||||
subtitle: '별들을 정복하라',
|
||||
startGame: '게임 시작',
|
||||
privacyAgreement: '개인정보 처리방침',
|
||||
privacyAgreementDesc: '게임을 시작하기 전에 개인정보 처리방침을 읽고 동의해 주세요.',
|
||||
agreeToPrivacy: '읽었으며 동의합니다',
|
||||
viewFullPolicy: '전체 정책 보기',
|
||||
agreeAndStart: '동의 후 시작'
|
||||
},
|
||||
common: {
|
||||
confirm: '확인',
|
||||
cancel: '취소',
|
||||
@@ -36,7 +45,8 @@ export default {
|
||||
requirementsNotMet: '요구사항 미충족',
|
||||
current: '현재',
|
||||
level: '레벨',
|
||||
gmModeActivated: 'GM 모드가 활성화되었습니다! 탐색 메뉴를 확인하세요.'
|
||||
gmModeActivated: 'GM 모드가 활성화되었습니다! 탐색 메뉴를 확인하세요.',
|
||||
view: '보기'
|
||||
},
|
||||
errors: {
|
||||
requirementsNotMet: '전제 조건 미충족',
|
||||
@@ -113,7 +123,13 @@ export default {
|
||||
homePlanet: '모행성',
|
||||
planetPrefix: '행성',
|
||||
moonSuffix: '의 위성',
|
||||
colonyPrefix: '식민지'
|
||||
colonyPrefix: '식민지',
|
||||
renamePlanet: '행성 이름 변경',
|
||||
renamePlanetTitle: '행성 이름 변경',
|
||||
newPlanetName: '새 이름',
|
||||
planetNamePlaceholder: '새 행성 이름 입력',
|
||||
rename: '이름 변경',
|
||||
renameSuccess: '행성 이름이 {name}(으)로 변경되었습니다'
|
||||
},
|
||||
player: {
|
||||
points: '총 점수'
|
||||
@@ -284,7 +300,8 @@ export default {
|
||||
hyperspaceTechnology: '초공간 점프 기술',
|
||||
plasmaTechnology: '플라즈마 무기 기술',
|
||||
computerTechnology: '연구 대기열 및 함대 임무 슬롯 증가, 레벨당 +1 대기열 +1 슬롯 (최대 10레벨)',
|
||||
espionageTechnology: '스파이 탐사기 효과 향상, 레벨당 정찰 깊이 +1. 정찰 레벨 = 내 레벨 - 상대 레벨 + 탐사기 수/5. ≥-1 함대 표시, ≥1 방어 표시, ≥3 건물 표시, ≥5 기술 표시',
|
||||
espionageTechnology:
|
||||
'스파이 탐사기 효과 향상, 레벨당 정찰 깊이 +1. 정찰 레벨 = 내 레벨 - 상대 레벨 + 탐사기 수/5. ≥-1 함대 표시, ≥1 방어 표시, ≥3 건물 표시, ≥5 기술 표시',
|
||||
weaponsTechnology: '함선과 방어의 공격력 레벨당 10% 증가',
|
||||
shieldingTechnology: '함선과 방어의 실드 레벨당 10% 증가',
|
||||
armourTechnology: '함선과 방어의 장갑 레벨당 10% 증가',
|
||||
@@ -317,8 +334,8 @@ export default {
|
||||
darkMatterSpecialist: '암흑 물질 수집 효율 향상'
|
||||
},
|
||||
queue: {
|
||||
title: '건설 대기열',
|
||||
empty: '활성 작업 없음',
|
||||
title: '진행 중인 작업',
|
||||
empty: '활성 대기열 없음',
|
||||
buildQueue: '건설 대기열',
|
||||
researchQueue: '연구 대기열',
|
||||
building: '건설 중',
|
||||
@@ -331,7 +348,14 @@ export default {
|
||||
confirmCancel: '취소하시겠습니까? 자원의 50%가 환불됩니다.',
|
||||
level: '레벨',
|
||||
gmModeActivated: '',
|
||||
upgradeToLevel: '레벨로 업그레이드'
|
||||
upgradeToLevel: '레벨로 업그레이드',
|
||||
tabs: {
|
||||
all: '전체',
|
||||
buildings: '건물',
|
||||
research: '연구',
|
||||
ships: '함선',
|
||||
defense: '방어'
|
||||
}
|
||||
},
|
||||
overview: {
|
||||
title: '행성 개요',
|
||||
@@ -586,7 +610,9 @@ export default {
|
||||
sendGift: '선물 보내기',
|
||||
debris: '잔해',
|
||||
giftPlanetTitle: '선물 보내기',
|
||||
giftPlanetMessage: '행성 [{coordinates}]에 자원을 선물로 보내시겠습니까?\n\n함대 페이지로 이동하여 수송선을 선택하고 자원을 적재하세요.'
|
||||
giftPlanetMessage:
|
||||
'행성 [{coordinates}]에 자원을 선물로 보내시겠습니까?\n\n함대 페이지로 이동하여 수송선을 선택하고 자원을 적재하세요.',
|
||||
npcPlanetName: '{name}의 행성'
|
||||
},
|
||||
messagesView: {
|
||||
title: '메시지 센터',
|
||||
@@ -620,6 +646,7 @@ export default {
|
||||
targetPlanet: '목표 행성',
|
||||
attackerRemaining: '공격자 잔여',
|
||||
defenderRemaining: '방어자 잔여',
|
||||
allDestroyed: '모두 파괴됨',
|
||||
moonChance: '위성 생성 확률',
|
||||
showRoundDetails: '라운드 상세 표시',
|
||||
hideRoundDetails: '라운드 상세 숨기기',
|
||||
@@ -685,7 +712,17 @@ export default {
|
||||
activityDescription: '',
|
||||
npcActivityMessage: '',
|
||||
arrivalTime: '',
|
||||
npcActivityTip: ''
|
||||
npcActivityTip: '',
|
||||
clearMessages: '메시지 삭제',
|
||||
clearMessageTypes: '삭제할 메시지 유형 선택',
|
||||
clearBattleReports: '전투 보고서',
|
||||
clearSpyReports: '정찰 보고서',
|
||||
clearSpiedNotifications: '정찰 알림',
|
||||
clearMissionReports: '임무 보고서',
|
||||
clearNPCActivity: 'NPC 활동',
|
||||
clearGiftNotifications: '선물 알림',
|
||||
clearGiftRejected: '거절된 선물',
|
||||
clearNow: '지금 삭제'
|
||||
},
|
||||
missionReports: {
|
||||
transportSuccess: '수송 임무가 성공적으로 완료되었습니다',
|
||||
@@ -788,6 +825,7 @@ export default {
|
||||
community: '커뮤니티',
|
||||
github: 'GitHub 저장소',
|
||||
qqGroup: 'QQ 그룹',
|
||||
privacyPolicy: '개인정보처리방침',
|
||||
notifications: '알림 설정',
|
||||
notificationsDesc: '게임 내 알림 관리',
|
||||
notificationTypes: '알림 유형',
|
||||
@@ -869,6 +907,7 @@ export default {
|
||||
completeQueuesSuccess: ''
|
||||
},
|
||||
alerts: {
|
||||
incomingFleets: '{count}개의 적 함대 접근 중',
|
||||
npcSpyIncoming: 'NPC 정찰 프로브 접근 중',
|
||||
npcAttackIncoming: 'NPC 함대 공격 진행 중!',
|
||||
npcFleetIncoming: 'NPC 함대 접근 중',
|
||||
@@ -880,6 +919,30 @@ export default {
|
||||
npcSpiedYourPlanet: 'NPC가 당신의 행성을 정찰했습니다',
|
||||
npcAttackedYourPlanet: 'NPC가 당신의 행성을 공격했습니다'
|
||||
},
|
||||
enemyAlert: {
|
||||
title: '적 경보',
|
||||
markAllRead: '모두 읽음',
|
||||
noAlerts: '경보 없음',
|
||||
fleetSize: '함대 규모',
|
||||
ships: '척',
|
||||
viewFleet: '함대 보기',
|
||||
alertDetails: '경보 상세',
|
||||
targetInfo: '목표 정보',
|
||||
arrivalTime: '도착 시간',
|
||||
countdown: '카운트다운',
|
||||
viewMessages: '메시지 보기',
|
||||
arrived: '도착함',
|
||||
missionType: {
|
||||
spy: '정찰',
|
||||
attack: '공격',
|
||||
unknown: '알 수 없음'
|
||||
},
|
||||
warning: {
|
||||
spy: '적 정찰 접근 중!',
|
||||
attack: '적 공격 접근 중!',
|
||||
unknown: '적 함대 접근 중!'
|
||||
}
|
||||
},
|
||||
diplomacy: {
|
||||
title: '외교',
|
||||
description: 'NPC와의 외교 관계 관리',
|
||||
@@ -912,16 +975,30 @@ export default {
|
||||
more: '더보기',
|
||||
actions: {
|
||||
gift: '선물 보내기',
|
||||
viewPlanets: '행성 보기'
|
||||
viewPlanets: '행성 보기',
|
||||
addNote: '메모 추가',
|
||||
editNote: '메모 편집'
|
||||
},
|
||||
note: '메모',
|
||||
notePlaceholder: '메모 입력...',
|
||||
noteEmpty: '메모 없음',
|
||||
lastEvent: '최근 이벤트',
|
||||
reportDetails: '',
|
||||
eventDescription: '',
|
||||
reputationChange: '',
|
||||
before: '',
|
||||
after: '',
|
||||
statusChange: '',
|
||||
viewDiplomacy: '',
|
||||
reportDetails: '외교 보고서 상세',
|
||||
eventDescription: '이벤트 설명',
|
||||
reputationChange: '평판 변화',
|
||||
before: '이전',
|
||||
after: '이후',
|
||||
statusChange: '관계 상태 변화',
|
||||
viewDiplomacy: '외교 페이지 보기',
|
||||
eventType: {
|
||||
gift: '자원을 선물함',
|
||||
attack: '공격을 시작함',
|
||||
allyAttacked: '동맹을 공격함',
|
||||
spy: '정찰을 수행함',
|
||||
stealDebris: '잔해를 약탈함',
|
||||
destroyPlanet: '행성을 파괴함',
|
||||
unknown: '알 수 없는 이벤트'
|
||||
},
|
||||
events: {
|
||||
gift: '선물 전송',
|
||||
attack: '공격',
|
||||
@@ -958,6 +1035,49 @@ export default {
|
||||
allyOutraged: '{allyName}은(는) 당신이 동맹 {targetName}의 {planetName}을(를) 파괴한 것에 분노하고 있습니다',
|
||||
npcEliminated: 'NPC {npcName}이(가) 완전히 제거되었습니다',
|
||||
npcEliminatedMessage: '당신은 {npcName}의 모든 행성을 파괴했습니다! 이 세력은 완전히 소멸되었습니다.'
|
||||
},
|
||||
searchPlaceholder: 'NPC 이름 검색...',
|
||||
viewMode: {
|
||||
card: '카드',
|
||||
list: '목록'
|
||||
},
|
||||
diagnostic: {
|
||||
button: 'NPC 상태 진단',
|
||||
title: 'NPC 상태 진단',
|
||||
description: '플레이어 점수: {points}, 정찰 간격: {spyInterval}분, 공격 간격: {attackInterval}분, 공격 확률: {attackProb}%',
|
||||
noData: 'NPC 데이터 없음',
|
||||
difficulty: '난이도',
|
||||
difficultyLevels: {
|
||||
easy: '쉬움',
|
||||
medium: '보통',
|
||||
hard: '어려움'
|
||||
},
|
||||
reputation: '평판',
|
||||
spyProbes: '정찰기 수',
|
||||
fleetPower: '함대 전력',
|
||||
canSpy: '정찰 가능',
|
||||
canAttack: '공격 가능',
|
||||
attackProbability: '공격 확률',
|
||||
nextSpy: '다음 정찰',
|
||||
nextAttack: '다음 공격',
|
||||
yes: '예',
|
||||
no: '아니오',
|
||||
timeFormat: '{min}분 {sec}초',
|
||||
anytime: '언제든지',
|
||||
statusExplanation: '상태 설명',
|
||||
noRelation: '관계 없음',
|
||||
noRelationNeutral: '관계 없음 (중립)',
|
||||
reasons: {
|
||||
friendlyNoAction: '우호적 관계, 행동하지 않음',
|
||||
neutralNoAction: '중립적 관계, 행동하지 않음',
|
||||
hostileWillAct: '적대적 관계, 행동할 수 있음',
|
||||
noRelationNeutral: '외교 관계 없음, 중립으로 취급',
|
||||
insufficientProbes: '정찰기 부족 (현재: {current}, 필요: {required})',
|
||||
noFleet: '전투 함대 없음',
|
||||
spyCooldown: '정찰 쿨다운 중 ({min}분 {sec}초)',
|
||||
attackCooldown: '공격 쿨다운 중 ({min}분 {sec}초)',
|
||||
notSpiedYet: '아직 정찰하지 않음, 먼저 정찰 필요'
|
||||
}
|
||||
}
|
||||
},
|
||||
pagination: {
|
||||
@@ -973,6 +1093,53 @@ export default {
|
||||
description: '죄송합니다. 찾으시는 페이지가 존재하지 않습니다',
|
||||
goHome: '홈으로 이동'
|
||||
},
|
||||
privacy: {
|
||||
title: '개인정보처리방침',
|
||||
sections: {
|
||||
introduction: {
|
||||
title: '소개',
|
||||
content:
|
||||
'이 개인정보처리방침은 OGame-Vue-Ts가 귀하의 데이터를 어떻게 처리하는지 설명합니다. 우리는 귀하의 개인정보 보호에 전념하며, 이 게임은 사용자 개인정보를 완전히 존중하도록 설계되었습니다.'
|
||||
},
|
||||
dataCollection: {
|
||||
title: '데이터 수집',
|
||||
content: '이 게임은 다음 데이터만 브라우저에 로컬로 수집하고 저장합니다:',
|
||||
items: {
|
||||
gameProgress: '게임 진행 상황 (건물 레벨, 함대, 자원 등)',
|
||||
settings: '게임 설정 (알림 설정, 표시 옵션 등)',
|
||||
language: '언어 설정'
|
||||
}
|
||||
},
|
||||
dataStorage: {
|
||||
title: '데이터 저장',
|
||||
content:
|
||||
'모든 데이터는 브라우저의 로컬 스토리지(localStorage)에 저장됩니다. 이는 귀하의 데이터가 항상 귀하의 장치에 남아 있으며, 우리는 귀하의 게임 데이터에 접근, 조회 또는 수집할 수 없음을 의미합니다.'
|
||||
},
|
||||
noServer: {
|
||||
title: '서버 통신 없음',
|
||||
content:
|
||||
'이 게임은 완전히 오프라인인 싱글 플레이어 게임입니다. 업데이트 확인 기능(GitHub에서 버전 정보를 가져옴)을 제외하고 게임은 어떤 서버와도 통신하지 않습니다. 귀하의 게임 데이터는 절대로 장치를 떠나지 않습니다.'
|
||||
},
|
||||
thirdParty: {
|
||||
title: '제3자 서비스',
|
||||
content:
|
||||
'이 게임은 방문자 통계 및 트래픽 소스를 추적하기 위해 제3자 분석 서비스를 사용합니다. 이를 통해 사용 패턴을 이해하고 게임 경험을 개선할 수 있습니다. 이 분석 데이터는 익명이며 개인 식별 정보를 포함하지 않습니다. 광고 서비스나 기타 상업적 추적 도구는 사용하지 않습니다.'
|
||||
},
|
||||
dataControl: {
|
||||
title: '데이터 제어',
|
||||
content: '귀하는 데이터를 완전히 제어할 수 있습니다:',
|
||||
items: {
|
||||
export: '언제든지 게임 데이터를 내보낼 수 있습니다',
|
||||
import: '백업 파일에서 데이터를 가져올 수 있습니다',
|
||||
delete: '브라우저 데이터를 지우거나 게임 내 "데이터 삭제" 기능을 사용하여 모든 데이터를 삭제할 수 있습니다'
|
||||
}
|
||||
},
|
||||
contact: {
|
||||
title: '문의하기',
|
||||
content: '이 개인정보처리방침에 대한 질문이 있으시면 다음을 통해 문의해 주세요:'
|
||||
}
|
||||
}
|
||||
},
|
||||
time: {
|
||||
days: '일',
|
||||
hours: '시간',
|
||||
@@ -1006,5 +1173,67 @@ export default {
|
||||
'오른쪽 상단의 대기열 아이콘을 클릭하여 건설 진행 상황을 확인하세요. 다른 페이지를 계속 탐색할 수 있으며, 건설은 백그라운드에서 진행됩니다.'
|
||||
}
|
||||
}
|
||||
},
|
||||
hints: {
|
||||
close: '닫기',
|
||||
gotIt: '알겠습니다',
|
||||
dontShowAgain: '다시 표시 안 함',
|
||||
resetHints: '힌트 재설정',
|
||||
resetHintsDesc: '모든 힌트 다시 표시',
|
||||
hintsEnabled: '힌트 활성화',
|
||||
hintsEnabledDesc: '페이지 방문 시 도움말 힌트 표시',
|
||||
overview: {
|
||||
title: '행성 개요',
|
||||
message: '여기서 행성 자원, 함대 상태, 생산 세부 정보를 확인할 수 있습니다. 진행 상황을 모니터링하려면 자주 확인하세요!'
|
||||
},
|
||||
buildings: {
|
||||
title: '건물',
|
||||
message:
|
||||
'여기서 구조물을 건설하고 업그레이드합니다. 태양광 발전소로 에너지를 확보한 다음 자원 광산을 건설하세요. 팁: 로봇 공장이 건설 속도를 높입니다!'
|
||||
},
|
||||
research: {
|
||||
title: '연구소',
|
||||
message: '기술을 연구하여 새로운 함선을 해제하고, 전투력을 향상시키고, 문명을 발전시키세요. 에너지 기술이 좋은 시작점입니다!'
|
||||
},
|
||||
shipyard: {
|
||||
title: '조선소',
|
||||
message: '함선을 건조하여 탐험, 자원 운송, 제국 방어에 활용하세요. 화물선은 행성 간 자원을 운반합니다.'
|
||||
},
|
||||
fleet: {
|
||||
title: '함대 사령부',
|
||||
message: '함선을 임무에 파견하세요: 적 공격, 자원 수송, 새 행성 식민지화, 또는 잔해장 탐색.'
|
||||
},
|
||||
galaxy: {
|
||||
title: '은하 지도',
|
||||
message: '은하를 탐색하여 식민지화할 빈 행성, 수확할 잔해장, 공격할 적을 찾으세요. 먼저 정찰 탐침을 사용하세요!'
|
||||
},
|
||||
diplomacy: {
|
||||
title: '외교',
|
||||
message: 'NPC와의 관계를 관리하세요. 선물을 보내 평판을 높이거나 적대적 공격에 직면하세요. 적의 동맹도 적대적으로 변할 수 있습니다!'
|
||||
},
|
||||
messages: {
|
||||
title: '메시지',
|
||||
message: '여기서 전투 보고서, 정찰 보고서, 외교 알림을 확인하세요. 활동과 적 조우를 추적하세요.'
|
||||
},
|
||||
defense: {
|
||||
title: '행성 방어',
|
||||
message: '방어 구조물을 건설하여 공격으로부터 행성을 보호하세요. 방패와 포탑이 침입자를 억제합니다!'
|
||||
},
|
||||
officers: {
|
||||
title: '장교',
|
||||
message: '장교를 고용하여 다양한 보너스를 획득하세요! 사령관은 건설 가속, 지질학자는 자원 생산 증가, 제독은 함대 능력 강화.'
|
||||
},
|
||||
simulator: {
|
||||
title: '전투 시뮬레이터',
|
||||
message: '공격 전에 전투 결과를 시뮬레이션하세요. 양측 함대와 기술 레벨을 입력하여 승패와 손실을 예측.'
|
||||
},
|
||||
settings: {
|
||||
title: '설정',
|
||||
message: '여기서 게임 데이터 관리, 알림 설정, 저장 내보내기/가져오기가 가능합니다. 정기적으로 백업하세요!'
|
||||
},
|
||||
gm: {
|
||||
title: 'GM 관리 패널',
|
||||
message: 'GM 모드에서는 자원, 건물, 기술 레벨을 빠르게 수정할 수 있습니다. 테스트나 전체 게임 콘텐츠 체험에 사용.'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
export default {
|
||||
home: {
|
||||
subtitle: 'Покори звёзды',
|
||||
startGame: 'Начать игру',
|
||||
privacyAgreement: 'Политика конфиденциальности',
|
||||
privacyAgreementDesc: 'Пожалуйста, прочитайте и примите нашу политику конфиденциальности перед началом игры.',
|
||||
agreeToPrivacy: 'Я прочитал и согласен с',
|
||||
viewFullPolicy: 'Просмотреть полную политику',
|
||||
agreeAndStart: 'Согласиться и начать'
|
||||
},
|
||||
common: {
|
||||
confirm: 'Подтвердить',
|
||||
cancel: 'Отмена',
|
||||
@@ -36,7 +45,8 @@ export default {
|
||||
requirementsNotMet: 'Требования не выполнены',
|
||||
current: 'Текущий',
|
||||
level: 'Уровень',
|
||||
gmModeActivated: 'Режим GM активирован! Проверьте навигационное меню.'
|
||||
gmModeActivated: 'Режим GM активирован! Проверьте навигационное меню.',
|
||||
view: 'Просмотр'
|
||||
},
|
||||
errors: {
|
||||
requirementsNotMet: 'Требования не выполнены',
|
||||
@@ -113,7 +123,13 @@ export default {
|
||||
homePlanet: 'Родная планета',
|
||||
planetPrefix: 'Планета',
|
||||
moonSuffix: 'я луна',
|
||||
colonyPrefix: 'Колония'
|
||||
colonyPrefix: 'Колония',
|
||||
renamePlanet: 'Переименовать планету',
|
||||
renamePlanetTitle: 'Переименовать планету',
|
||||
newPlanetName: 'Новое название',
|
||||
planetNamePlaceholder: 'Введите новое название планеты',
|
||||
rename: 'Переименовать',
|
||||
renameSuccess: 'Планета переименована в {name}'
|
||||
},
|
||||
player: {
|
||||
points: 'Всего очков'
|
||||
@@ -284,7 +300,8 @@ export default {
|
||||
hyperspaceTechnology: 'Технология гиперпространственных прыжков',
|
||||
plasmaTechnology: 'Технология плазменного оружия',
|
||||
computerTechnology: 'Увеличивает очередь исследований и слоты флотских миссий, +1 очередь +1 слот за уровень (макс 10 уровней)',
|
||||
espionageTechnology: 'Повышает эффективность зондов, +1 уровень шпионажа за уровень. Уровень разведки = ваш уровень - уровень врага + зонды/5. ≥-1 показывает флот, ≥1 показывает оборону, ≥3 показывает здания, ≥5 показывает технологии',
|
||||
espionageTechnology:
|
||||
'Повышает эффективность зондов, +1 уровень шпионажа за уровень. Уровень разведки = ваш уровень - уровень врага + зонды/5. ≥-1 показывает флот, ≥1 показывает оборону, ≥3 показывает здания, ≥5 показывает технологии',
|
||||
weaponsTechnology: 'Увеличивает силу атаки кораблей и обороны на 10% за уровень',
|
||||
shieldingTechnology: 'Увеличивает щиты кораблей и обороны на 10% за уровень',
|
||||
armourTechnology: 'Увеличивает броню кораблей и обороны на 10% за уровень',
|
||||
@@ -318,8 +335,8 @@ export default {
|
||||
darkMatterSpecialist: 'Улучшает эффективность сбора тёмной материи'
|
||||
},
|
||||
queue: {
|
||||
title: 'Очередь строительства',
|
||||
empty: 'Нет активных задач',
|
||||
title: 'Активные задачи',
|
||||
empty: 'Нет активных очередей',
|
||||
buildQueue: 'Очередь строительства',
|
||||
researchQueue: 'Очередь исследований',
|
||||
building: 'Строится',
|
||||
@@ -332,7 +349,14 @@ export default {
|
||||
confirmCancel: 'Вы уверены, что хотите отменить? 50% ресурсов будет возвращено.',
|
||||
level: 'Уровень',
|
||||
gmModeActivated: '',
|
||||
upgradeToLevel: 'Улучшить до уровня'
|
||||
upgradeToLevel: 'Улучшить до уровня',
|
||||
tabs: {
|
||||
all: 'Все',
|
||||
buildings: 'Здания',
|
||||
research: 'Исследования',
|
||||
ships: 'Корабли',
|
||||
defense: 'Оборона'
|
||||
}
|
||||
},
|
||||
overview: {
|
||||
title: 'Обзор планеты',
|
||||
@@ -593,7 +617,8 @@ export default {
|
||||
debris: 'Обломки',
|
||||
giftPlanetTitle: 'Отправить подарок',
|
||||
giftPlanetMessage:
|
||||
'Вы уверены, что хотите отправить ресурсы в подарок планете [{coordinates}]?\n\nПерейдите на страницу флота, чтобы выбрать транспортные корабли и загрузить ресурсы.'
|
||||
'Вы уверены, что хотите отправить ресурсы в подарок планете [{coordinates}]?\n\nПерейдите на страницу флота, чтобы выбрать транспортные корабли и загрузить ресурсы.',
|
||||
npcPlanetName: 'Планета {name}'
|
||||
},
|
||||
messagesView: {
|
||||
title: 'Сообщения',
|
||||
@@ -627,6 +652,7 @@ export default {
|
||||
targetPlanet: 'Целевая планета',
|
||||
attackerRemaining: 'Осталось у нападающего',
|
||||
defenderRemaining: 'Осталось у защитника',
|
||||
allDestroyed: 'Всё уничтожено',
|
||||
moonChance: 'Шанс появления луны',
|
||||
showRoundDetails: 'Показать детали раундов',
|
||||
hideRoundDetails: 'Скрыть детали раундов',
|
||||
@@ -692,7 +718,17 @@ export default {
|
||||
activityDescription: '',
|
||||
npcActivityMessage: '',
|
||||
arrivalTime: '',
|
||||
npcActivityTip: ''
|
||||
npcActivityTip: '',
|
||||
clearMessages: 'Очистить сообщения',
|
||||
clearMessageTypes: 'Выберите типы сообщений для очистки',
|
||||
clearBattleReports: 'Боевые отчёты',
|
||||
clearSpyReports: 'Разведывательные отчёты',
|
||||
clearSpiedNotifications: 'Уведомления о разведке',
|
||||
clearMissionReports: 'Отчёты о миссиях',
|
||||
clearNPCActivity: 'Активность NPC',
|
||||
clearGiftNotifications: 'Уведомления о подарках',
|
||||
clearGiftRejected: 'Отклонённые подарки',
|
||||
clearNow: 'Очистить сейчас'
|
||||
},
|
||||
missionReports: {
|
||||
transportSuccess: 'Миссия транспортировки успешно завершена',
|
||||
@@ -795,6 +831,7 @@ export default {
|
||||
community: 'Сообщество',
|
||||
github: 'Репозиторий GitHub',
|
||||
qqGroup: 'Группа QQ',
|
||||
privacyPolicy: 'Политика конфиденциальности',
|
||||
notifications: 'Настройки уведомлений',
|
||||
notificationsDesc: 'Управление внутриигровыми уведомлениями',
|
||||
notificationTypes: 'Типы уведомлений',
|
||||
@@ -877,6 +914,7 @@ export default {
|
||||
completeQueuesSuccess: ''
|
||||
},
|
||||
alerts: {
|
||||
incomingFleets: '{count} вражеских флотов приближается',
|
||||
npcSpyIncoming: 'Приближается шпионский зонд NPC',
|
||||
npcAttackIncoming: 'Атака флота NPC приближается!',
|
||||
npcFleetIncoming: 'Приближается флот NPC',
|
||||
@@ -888,6 +926,30 @@ export default {
|
||||
npcSpiedYourPlanet: 'NPC разведал вашу планету',
|
||||
npcAttackedYourPlanet: 'NPC атаковал вашу планету'
|
||||
},
|
||||
enemyAlert: {
|
||||
title: 'Тревога',
|
||||
markAllRead: 'Отметить прочитанным',
|
||||
noAlerts: 'Нет тревог',
|
||||
fleetSize: 'Размер флота',
|
||||
ships: 'кораблей',
|
||||
viewFleet: 'Просмотр флота',
|
||||
alertDetails: 'Детали тревоги',
|
||||
targetInfo: 'Информация о цели',
|
||||
arrivalTime: 'Время прибытия',
|
||||
countdown: 'Обратный отсчёт',
|
||||
viewMessages: 'Просмотр сообщений',
|
||||
arrived: 'Прибыл',
|
||||
missionType: {
|
||||
spy: 'Разведка',
|
||||
attack: 'Атака',
|
||||
unknown: 'Неизвестно'
|
||||
},
|
||||
warning: {
|
||||
spy: 'Вражеская разведка приближается!',
|
||||
attack: 'Вражеская атака приближается!',
|
||||
unknown: 'Вражеский флот приближается!'
|
||||
}
|
||||
},
|
||||
diplomacy: {
|
||||
title: 'Дипломатия',
|
||||
description: 'Управление дипломатическими отношениями с NPC',
|
||||
@@ -920,16 +982,30 @@ export default {
|
||||
more: 'еще',
|
||||
actions: {
|
||||
gift: 'Отправить подарок',
|
||||
viewPlanets: 'Посмотреть планеты'
|
||||
viewPlanets: 'Посмотреть планеты',
|
||||
addNote: 'Добавить заметку',
|
||||
editNote: 'Редактировать заметку'
|
||||
},
|
||||
note: 'Заметка',
|
||||
notePlaceholder: 'Введите заметку...',
|
||||
noteEmpty: 'Нет заметки',
|
||||
lastEvent: 'Последнее событие',
|
||||
reportDetails: '',
|
||||
eventDescription: '',
|
||||
reputationChange: '',
|
||||
before: '',
|
||||
after: '',
|
||||
statusChange: '',
|
||||
viewDiplomacy: '',
|
||||
reportDetails: 'Детали дипломатического отчёта',
|
||||
eventDescription: 'Описание события',
|
||||
reputationChange: 'Изменение репутации',
|
||||
before: 'До',
|
||||
after: 'После',
|
||||
statusChange: 'Изменение статуса',
|
||||
viewDiplomacy: 'Перейти к дипломатии',
|
||||
eventType: {
|
||||
gift: 'Подарил ресурсы',
|
||||
attack: 'Провёл атаку',
|
||||
allyAttacked: 'Атаковал союзника',
|
||||
spy: 'Провёл разведку',
|
||||
stealDebris: 'Украл обломки',
|
||||
destroyPlanet: 'Уничтожил планету',
|
||||
unknown: 'Неизвестное событие'
|
||||
},
|
||||
events: {
|
||||
gift: 'Подарок отправлен',
|
||||
attack: 'Атака',
|
||||
@@ -966,6 +1042,50 @@ export default {
|
||||
allyOutraged: '{allyName} возмущен тем, что вы уничтожили {planetName} их союзника {targetName}',
|
||||
npcEliminated: 'NPC {npcName} полностью уничтожен',
|
||||
npcEliminatedMessage: 'Вы уничтожили все планеты {npcName}! Эта фракция полностью уничтожена.'
|
||||
},
|
||||
searchPlaceholder: 'Поиск NPC по имени...',
|
||||
viewMode: {
|
||||
card: 'Карточки',
|
||||
list: 'Список'
|
||||
},
|
||||
diagnostic: {
|
||||
button: 'Диагностика NPC',
|
||||
title: 'Диагностика состояния NPC',
|
||||
description:
|
||||
'Очки игрока: {points}, Интервал разведки: {spyInterval}мин, Интервал атаки: {attackInterval}мин, Вероятность атаки: {attackProb}%',
|
||||
noData: 'Нет данных NPC',
|
||||
difficulty: 'Сложность',
|
||||
difficultyLevels: {
|
||||
easy: 'Лёгкая',
|
||||
medium: 'Средняя',
|
||||
hard: 'Сложная'
|
||||
},
|
||||
reputation: 'Репутация',
|
||||
spyProbes: 'Шпионские зонды',
|
||||
fleetPower: 'Мощь флота',
|
||||
canSpy: 'Может шпионить',
|
||||
canAttack: 'Может атаковать',
|
||||
attackProbability: 'Вероятность атаки',
|
||||
nextSpy: 'Следующая разведка',
|
||||
nextAttack: 'Следующая атака',
|
||||
yes: 'Да',
|
||||
no: 'Нет',
|
||||
timeFormat: '{min}м {sec}с',
|
||||
anytime: 'В любой момент',
|
||||
statusExplanation: 'Объяснение статуса',
|
||||
noRelation: 'Нет отношений',
|
||||
noRelationNeutral: 'Нет отношений (Нейтральный)',
|
||||
reasons: {
|
||||
friendlyNoAction: 'Дружественные отношения, не будет действовать',
|
||||
neutralNoAction: 'Нейтральные отношения, не будет действовать',
|
||||
hostileWillAct: 'Враждебные отношения, может действовать',
|
||||
noRelationNeutral: 'Нет дипломатических отношений, считается нейтральным',
|
||||
insufficientProbes: 'Недостаточно зондов (Текущее: {current}, Требуется: {required})',
|
||||
noFleet: 'Нет боевого флота',
|
||||
spyCooldown: 'Разведка на перезарядке ({min}м {sec}с)',
|
||||
attackCooldown: 'Атака на перезарядке ({min}м {sec}с)',
|
||||
notSpiedYet: 'Ещё не разведан, сначала нужна разведка'
|
||||
}
|
||||
}
|
||||
},
|
||||
pagination: {
|
||||
@@ -981,6 +1101,53 @@ export default {
|
||||
description: 'Извините, страница, которую вы ищете, не существует',
|
||||
goHome: 'На главную'
|
||||
},
|
||||
privacy: {
|
||||
title: 'Политика конфиденциальности',
|
||||
sections: {
|
||||
introduction: {
|
||||
title: 'Введение',
|
||||
content:
|
||||
'Эта политика конфиденциальности объясняет, как OGame-Vue-Ts обрабатывает ваши данные. Мы стремимся защищать вашу конфиденциальность, и эта игра разработана с полным уважением к приватности пользователей.'
|
||||
},
|
||||
dataCollection: {
|
||||
title: 'Сбор данных',
|
||||
content: 'Эта игра собирает и хранит только следующие данные локально в вашем браузере:',
|
||||
items: {
|
||||
gameProgress: 'Прогресс игры (уровни зданий, флоты, ресурсы и т.д.)',
|
||||
settings: 'Настройки игры (настройки уведомлений, параметры отображения и т.д.)',
|
||||
language: 'Языковые настройки'
|
||||
}
|
||||
},
|
||||
dataStorage: {
|
||||
title: 'Хранение данных',
|
||||
content:
|
||||
'Все данные хранятся в локальном хранилище вашего браузера (localStorage). Это означает, что ваши данные всегда остаются на вашем собственном устройстве, и мы не можем получить доступ, просматривать или собирать какие-либо ваши игровые данные.'
|
||||
},
|
||||
noServer: {
|
||||
title: 'Нет связи с сервером',
|
||||
content:
|
||||
'Эта игра является полностью офлайн одиночной игрой. За исключением функции проверки обновлений (которая получает информацию о версии с GitHub), игра не взаимодействует ни с каким сервером. Ваши игровые данные никогда не покидают ваше устройство.'
|
||||
},
|
||||
thirdParty: {
|
||||
title: 'Сторонние сервисы',
|
||||
content:
|
||||
'Эта игра использует сторонние аналитические сервисы для отслеживания статистики посещений и источников трафика, что помогает нам понять модели использования и улучшить игровой опыт. Эти аналитические данные являются анонимными и не содержат никакой персонально идентифицируемой информации. Мы не используем рекламные сервисы или другие коммерческие инструменты отслеживания.'
|
||||
},
|
||||
dataControl: {
|
||||
title: 'Контроль данных',
|
||||
content: 'Вы имеете полный контроль над своими данными:',
|
||||
items: {
|
||||
export: 'Вы можете экспортировать данные игры в любое время',
|
||||
import: 'Вы можете импортировать данные из резервных файлов',
|
||||
delete: 'Вы можете удалить все данные, очистив данные браузера или используя функцию "Очистить данные" в игре'
|
||||
}
|
||||
},
|
||||
contact: {
|
||||
title: 'Свяжитесь с нами',
|
||||
content: 'Если у вас есть вопросы по поводу этой политики конфиденциальности, пожалуйста, свяжитесь с нами через:'
|
||||
}
|
||||
}
|
||||
},
|
||||
time: {
|
||||
days: 'дней',
|
||||
hours: 'часов',
|
||||
@@ -1014,5 +1181,76 @@ export default {
|
||||
'Нажмите на значок очереди в правом верхнем углу, чтобы увидеть прогресс строительства. Вы можете продолжать просматривать другие страницы - строительство происходит в фоновом режиме.'
|
||||
}
|
||||
}
|
||||
},
|
||||
hints: {
|
||||
close: 'Закрыть',
|
||||
gotIt: 'Понятно',
|
||||
dontShowAgain: 'Больше не показывать',
|
||||
resetHints: 'Сбросить подсказки',
|
||||
resetHintsDesc: 'Показать все подсказки снова',
|
||||
hintsEnabled: 'Включить подсказки',
|
||||
hintsEnabledDesc: 'Показывать полезные подсказки при посещении страниц',
|
||||
overview: {
|
||||
title: 'Обзор планеты',
|
||||
message:
|
||||
'Здесь вы можете видеть ресурсы планеты, статус флота и детали производства. Регулярно проверяйте, чтобы отслеживать прогресс!'
|
||||
},
|
||||
buildings: {
|
||||
title: 'Здания',
|
||||
message:
|
||||
'Стройте и улучшайте сооружения здесь. Начните с солнечной электростанции для энергии, затем ресурсные шахты. Совет: Фабрика роботов ускоряет строительство!'
|
||||
},
|
||||
research: {
|
||||
title: 'Исследовательская лаборатория',
|
||||
message:
|
||||
'Исследуйте технологии, чтобы разблокировать новые корабли, улучшить боеспособность и развить цивилизацию. Энергетическая технология - отличное начало!'
|
||||
},
|
||||
shipyard: {
|
||||
title: 'Верфь',
|
||||
message:
|
||||
'Стройте корабли для исследования, транспортировки ресурсов и защиты империи. Грузовые корабли помогают перевозить ресурсы между планетами.'
|
||||
},
|
||||
fleet: {
|
||||
title: 'Командование флотом',
|
||||
message:
|
||||
'Отправляйте корабли на миссии: атакуйте врагов, транспортируйте ресурсы, колонизируйте новые планеты или исследуйте поля обломков.'
|
||||
},
|
||||
galaxy: {
|
||||
title: 'Карта галактики',
|
||||
message:
|
||||
'Исследуйте галактику, чтобы найти пустые планеты для колонизации, поля обломков для сбора и врагов для атаки. Сначала используйте шпионские зонды!'
|
||||
},
|
||||
diplomacy: {
|
||||
title: 'Дипломатия',
|
||||
message:
|
||||
'Управляйте отношениями с NPC. Отправляйте подарки для улучшения репутации или столкнитесь с враждебными атаками. Союзники ваших врагов тоже могут стать враждебными!'
|
||||
},
|
||||
messages: {
|
||||
title: 'Сообщения',
|
||||
message:
|
||||
'Просматривайте боевые отчёты, отчёты разведки и дипломатические уведомления. Отслеживайте свои действия и столкновения с врагами.'
|
||||
},
|
||||
defense: {
|
||||
title: 'Планетарная оборона',
|
||||
message: 'Стройте оборонительные сооружения для защиты планеты от атак. Щиты и турели могут отпугнуть захватчиков!'
|
||||
},
|
||||
officers: {
|
||||
title: 'Офицеры',
|
||||
message:
|
||||
'Нанимайте офицеров для получения бонусов! Командир ускоряет строительство, Геолог увеличивает добычу ресурсов, Адмирал усиливает флот.'
|
||||
},
|
||||
simulator: {
|
||||
title: 'Симулятор боя',
|
||||
message: 'Симулируйте результаты боя перед атакой. Введите флоты и уровни технологий для прогноза победы, потерь и добычи.'
|
||||
},
|
||||
settings: {
|
||||
title: 'Настройки',
|
||||
message: 'Управляйте игровыми данными, уведомлениями, импортом/экспортом сохранений. Регулярно создавайте резервные копии!'
|
||||
},
|
||||
gm: {
|
||||
title: 'Панель ГМ',
|
||||
message:
|
||||
'Режим ГМ позволяет быстро изменять ресурсы, здания и уровни технологий. Используйте для тестирования или полного доступа к контенту.'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
export default {
|
||||
home: {
|
||||
subtitle: '征服星辰大海',
|
||||
startGame: '开始游戏',
|
||||
privacyAgreement: '隐私协议',
|
||||
privacyAgreementDesc: '开始游戏前,请阅读并同意我们的隐私协议。',
|
||||
agreeToPrivacy: '我已阅读并同意',
|
||||
viewFullPolicy: '查看完整协议',
|
||||
agreeAndStart: '同意并开始'
|
||||
},
|
||||
common: {
|
||||
confirm: '确认',
|
||||
cancel: '取消',
|
||||
@@ -35,7 +44,8 @@ export default {
|
||||
requirementsNotMet: '前置条件未满足',
|
||||
current: '当前',
|
||||
level: '等级',
|
||||
gmModeActivated: 'GM 模式已激活!请查看导航菜单。'
|
||||
gmModeActivated: 'GM 模式已激活!请查看导航菜单。',
|
||||
view: '查看'
|
||||
},
|
||||
errors: {
|
||||
requirementsNotMet: '不满足前置条件',
|
||||
@@ -113,7 +123,13 @@ export default {
|
||||
homePlanet: '母星',
|
||||
planetPrefix: '星球',
|
||||
moonSuffix: '的月球',
|
||||
colonyPrefix: '殖民地'
|
||||
colonyPrefix: '殖民地',
|
||||
renamePlanet: '重命名星球',
|
||||
renamePlanetTitle: '重命名星球',
|
||||
newPlanetName: '新名称',
|
||||
planetNamePlaceholder: '输入新的星球名称',
|
||||
rename: '重命名',
|
||||
renameSuccess: '星球已重命名为 {name}'
|
||||
},
|
||||
player: {
|
||||
points: '总积分'
|
||||
@@ -283,7 +299,8 @@ export default {
|
||||
hyperspaceTechnology: '超空间跳跃技术',
|
||||
plasmaTechnology: '等离子武器技术',
|
||||
computerTechnology: '增加研究队列和舰队任务槽位,每级+1队列+1槽位(最多10级)',
|
||||
espionageTechnology: '提高间谍探测效果,每级提高1级侦查深度。侦察等级=己方等级-对方等级+侦察船数/5。≥-1显示舰队,≥1显示防御,≥3显示建筑,≥5显示科技',
|
||||
espionageTechnology:
|
||||
'提高间谍探测效果,每级提高1级侦查深度。侦察等级=己方等级-对方等级+侦察船数/5。≥-1显示舰队,≥1显示防御,≥3显示建筑,≥5显示科技',
|
||||
weaponsTechnology: '提高舰船和防御的攻击力,每级+10%',
|
||||
shieldingTechnology: '提高舰船和防御的护盾值,每级+10%',
|
||||
armourTechnology: '提高舰船和防御的装甲值,每级+10%',
|
||||
@@ -316,8 +333,8 @@ export default {
|
||||
darkMatterSpecialist: '提升暗物质采集效率'
|
||||
},
|
||||
queue: {
|
||||
title: '建造队列',
|
||||
empty: '当前没有进行中的任务',
|
||||
title: '进行中的任务',
|
||||
empty: '当前没有进行中的队列',
|
||||
buildQueueBonus: '建造队列',
|
||||
spaceBonus: '空间加成',
|
||||
researchQueueBonus: '研究队列',
|
||||
@@ -331,7 +348,14 @@ export default {
|
||||
confirmCancel: '确定要取消吗?将返还50%的资源。',
|
||||
level: '等级',
|
||||
quantity: '数量',
|
||||
upgradeToLevel: '升级到等级'
|
||||
upgradeToLevel: '升级到等级',
|
||||
tabs: {
|
||||
all: '全部',
|
||||
buildings: '建筑',
|
||||
research: '研究',
|
||||
ships: '舰船',
|
||||
defense: '防御'
|
||||
}
|
||||
},
|
||||
overview: {
|
||||
title: '星球总览',
|
||||
@@ -577,7 +601,8 @@ export default {
|
||||
cancel: '取消',
|
||||
colonizePlanetMessage: '确定要殖民位置 [{coordinates}] 吗?\n\n请前往舰队页面派遣殖民船。',
|
||||
recyclePlanetMessage: '确定要回收位置 [{coordinates}] 的残骸吗?\n\n请前往舰队页面派遣回收船。',
|
||||
giftPlanetMessage: '确定要向星球 [{coordinates}] 赠送资源吗?\n\n请前往舰队页面选择运输船并装载资源。'
|
||||
giftPlanetMessage: '确定要向星球 [{coordinates}] 赠送资源吗?\n\n请前往舰队页面选择运输船并装载资源。',
|
||||
npcPlanetName: '{name}的星球'
|
||||
},
|
||||
messagesView: {
|
||||
title: '消息中心',
|
||||
@@ -611,6 +636,7 @@ export default {
|
||||
targetPlanet: '目标星球',
|
||||
attackerRemaining: '攻击方剩余',
|
||||
defenderRemaining: '防守方剩余',
|
||||
allDestroyed: '全部摧毁',
|
||||
moonChance: '月球生成概率',
|
||||
showRoundDetails: '显示回合详情',
|
||||
hideRoundDetails: '隐藏回合详情',
|
||||
@@ -676,7 +702,18 @@ export default {
|
||||
activityDescription: '活动描述',
|
||||
npcActivityMessage: '{npc}正在{position}{activity}',
|
||||
arrivalTime: '到达时间',
|
||||
npcActivityTip: 'NPC可能会收集战斗产生的残骸。如果你想竞争资源,可以尝试先到达该位置'
|
||||
npcActivityTip: 'NPC可能会收集战斗产生的残骸。如果你想竞争资源,可以尝试先到达该位置',
|
||||
// 清空消息
|
||||
clearMessages: '清空消息',
|
||||
clearMessageTypes: '选择要清空的消息类型',
|
||||
clearBattleReports: '战斗报告',
|
||||
clearSpyReports: '间谍报告',
|
||||
clearSpiedNotifications: '被侦查通知',
|
||||
clearMissionReports: '任务报告',
|
||||
clearNPCActivity: 'NPC活动',
|
||||
clearGiftNotifications: '礼物通知',
|
||||
clearGiftRejected: '拒绝记录',
|
||||
clearNow: '立即清空'
|
||||
},
|
||||
missionReports: {
|
||||
transportSuccess: '运输任务成功完成',
|
||||
@@ -783,6 +820,11 @@ export default {
|
||||
community: '社区',
|
||||
github: 'GitHub 仓库',
|
||||
qqGroup: 'QQ 交流群',
|
||||
privacyPolicy: '隐私协议',
|
||||
displaySettings: '显示设置',
|
||||
displaySettingsDesc: '调整游戏的视觉效果',
|
||||
backgroundAnimation: '背景动画',
|
||||
backgroundAnimationDesc: '开启后显示星空/粒子背景动画(可能影响性能)',
|
||||
notifications: '通知设置',
|
||||
notificationsDesc: '管理游戏内的通知提醒',
|
||||
notificationTypes: '通知类型',
|
||||
@@ -865,6 +907,7 @@ export default {
|
||||
'已完成 {buildingCount} 个建筑队列、{researchCount} 个科技队列、{missionCount} 个飞行任务、{missileCount} 个导弹任务'
|
||||
},
|
||||
alerts: {
|
||||
incomingFleets: '{count}支敌方舰队来袭',
|
||||
npcSpyIncoming: 'NPC侦查即将到达',
|
||||
npcAttackIncoming: 'NPC舰队来袭!',
|
||||
npcFleetIncoming: 'NPC舰队接近',
|
||||
@@ -876,6 +919,30 @@ export default {
|
||||
npcSpiedYourPlanet: 'NPC侦查了你的星球',
|
||||
npcAttackedYourPlanet: 'NPC攻击了你的星球'
|
||||
},
|
||||
enemyAlert: {
|
||||
title: '敌方警报',
|
||||
markAllRead: '全部已读',
|
||||
noAlerts: '暂无警报',
|
||||
fleetSize: '舰队规模',
|
||||
ships: '艘',
|
||||
viewFleet: '查看舰队',
|
||||
alertDetails: '警报详情',
|
||||
targetInfo: '目标信息',
|
||||
arrivalTime: '到达时间',
|
||||
countdown: '倒计时',
|
||||
viewMessages: '查看消息',
|
||||
arrived: '已到达',
|
||||
missionType: {
|
||||
spy: '侦查',
|
||||
attack: '攻击',
|
||||
unknown: '未知'
|
||||
},
|
||||
warning: {
|
||||
spy: '敌方侦查即将到达!',
|
||||
attack: '敌方攻击即将到达!',
|
||||
unknown: '敌方舰队即将到达!'
|
||||
}
|
||||
},
|
||||
diplomacy: {
|
||||
title: '外交',
|
||||
description: '管理与NPC的外交关系',
|
||||
@@ -908,8 +975,13 @@ export default {
|
||||
more: '更多',
|
||||
actions: {
|
||||
gift: '赠送资源',
|
||||
viewPlanets: '查看星球'
|
||||
viewPlanets: '查看星球',
|
||||
addNote: '添加备注',
|
||||
editNote: '编辑备注'
|
||||
},
|
||||
note: '备注',
|
||||
notePlaceholder: '输入备注...',
|
||||
noteEmpty: '无备注',
|
||||
lastEvent: '最近活动',
|
||||
reportDetails: '外交报告详情',
|
||||
eventDescription: '事件描述',
|
||||
@@ -925,6 +997,15 @@ export default {
|
||||
spy: '侦查',
|
||||
stealDebris: '抢夺残骸'
|
||||
},
|
||||
eventType: {
|
||||
gift: '赠送了资源',
|
||||
attack: '发起了攻击',
|
||||
allyAttacked: '攻击了盟友',
|
||||
spy: '进行了侦查',
|
||||
stealDebris: '抢夺了残骸',
|
||||
destroyPlanet: '摧毁了星球',
|
||||
unknown: '未知事件'
|
||||
},
|
||||
reports: {
|
||||
giftedResources: '赠送了 {metal}金属 {crystal}晶体 {deuterium}氘',
|
||||
receivedGiftFromPlayer: '收到玩家的礼物',
|
||||
@@ -953,6 +1034,49 @@ export default {
|
||||
allyOutraged: '{allyName}对你摧毁盟友{targetName}的{planetName}感到愤怒',
|
||||
npcEliminated: 'NPC {npcName}已被彻底消灭',
|
||||
npcEliminatedMessage: '你消灭了{npcName}的所有星球!该势力已被彻底摧毁。'
|
||||
},
|
||||
searchPlaceholder: '搜索NPC名称...',
|
||||
viewMode: {
|
||||
card: '卡片',
|
||||
list: '列表'
|
||||
},
|
||||
diagnostic: {
|
||||
button: 'NPC状态诊断',
|
||||
title: 'NPC状态诊断',
|
||||
description: '当前玩家积分:{points},侦查间隔:{spyInterval}分钟,攻击间隔:{attackInterval}分钟,攻击概率:{attackProb}%',
|
||||
noData: '暂无NPC数据',
|
||||
difficulty: '难度',
|
||||
difficultyLevels: {
|
||||
easy: '简单',
|
||||
medium: '普通',
|
||||
hard: '困难'
|
||||
},
|
||||
reputation: '好感度',
|
||||
spyProbes: '侦察机数量',
|
||||
fleetPower: '舰队战力',
|
||||
canSpy: '可以侦查',
|
||||
canAttack: '可以攻击',
|
||||
attackProbability: '攻击概率',
|
||||
nextSpy: '下次侦查',
|
||||
nextAttack: '下次攻击',
|
||||
yes: '是',
|
||||
no: '否',
|
||||
timeFormat: '{min}分{sec}秒',
|
||||
anytime: '随时可能',
|
||||
statusExplanation: '状态说明',
|
||||
noRelation: '无关系',
|
||||
noRelationNeutral: '无关系(中立)',
|
||||
reasons: {
|
||||
friendlyNoAction: '关系友好,不会主动行动',
|
||||
neutralNoAction: '关系中立,不会主动行动',
|
||||
hostileWillAct: '关系敌对,可能采取行动',
|
||||
noRelationNeutral: '无外交关系,视为中立',
|
||||
insufficientProbes: '侦察机不足(当前:{current},需要:{required})',
|
||||
noFleet: '没有战斗舰队',
|
||||
spyCooldown: '侦查冷却中({min}分{sec}秒)',
|
||||
attackCooldown: '攻击冷却中({min}分{sec}秒)',
|
||||
notSpiedYet: '尚未侦查过,需要先进行侦查'
|
||||
}
|
||||
}
|
||||
},
|
||||
pagination: {
|
||||
@@ -967,6 +1091,52 @@ export default {
|
||||
description: '抱歉,您访问的页面不存在',
|
||||
goHome: '返回首页'
|
||||
},
|
||||
privacy: {
|
||||
title: '隐私协议',
|
||||
sections: {
|
||||
introduction: {
|
||||
title: '简介',
|
||||
content: '本隐私协议说明了 OGame-Vue-Ts 如何处理您的数据。我们致力于保护您的隐私,本游戏的设计完全尊重用户隐私。'
|
||||
},
|
||||
dataCollection: {
|
||||
title: '数据收集',
|
||||
content: '本游戏仅在您的本地浏览器中收集和存储以下数据:',
|
||||
items: {
|
||||
gameProgress: '游戏进度(建筑等级、舰队、资源等)',
|
||||
settings: '游戏设置(通知偏好、显示选项等)',
|
||||
language: '语言偏好'
|
||||
}
|
||||
},
|
||||
dataStorage: {
|
||||
title: '数据存储',
|
||||
content:
|
||||
'所有数据均存储在您浏览器的本地存储(localStorage)中。这意味着您的数据始终保留在您自己的设备上,我们无法访问、查看或收集任何您的游戏数据。'
|
||||
},
|
||||
noServer: {
|
||||
title: '无服务器通信',
|
||||
content:
|
||||
'本游戏是一个完全离线的单机游戏。除了检查更新功能(从 GitHub 获取版本信息)外,游戏不会与任何服务器通信。您的游戏数据永远不会离开您的设备。'
|
||||
},
|
||||
thirdParty: {
|
||||
title: '第三方服务',
|
||||
content:
|
||||
'本游戏使用第三方流量分析服务来统计访问量和流量来源,帮助我们了解用户使用情况并改进游戏体验。这些分析数据是匿名的,不包含任何个人身份信息。我们不使用任何广告服务或其他商业追踪工具。'
|
||||
},
|
||||
dataControl: {
|
||||
title: '数据控制',
|
||||
content: '您对自己的数据拥有完全控制权:',
|
||||
items: {
|
||||
export: '您可以随时导出游戏数据',
|
||||
import: '您可以从备份文件导入数据',
|
||||
delete: '您可以通过清除浏览器数据或使用游戏内的"清除数据"功能来删除所有数据'
|
||||
}
|
||||
},
|
||||
contact: {
|
||||
title: '联系我们',
|
||||
content: '如果您对本隐私协议有任何问题,请通过以下方式联系我们:'
|
||||
}
|
||||
}
|
||||
},
|
||||
time: {
|
||||
days: '天',
|
||||
hours: '小时',
|
||||
@@ -1105,5 +1275,66 @@ export default {
|
||||
content: '很好!您已经掌握了基础操作。继续建造晶体矿和重氢合成器,然后探索其他功能。记住:先能量,再资源!'
|
||||
}
|
||||
}
|
||||
},
|
||||
hints: {
|
||||
close: '关闭',
|
||||
gotIt: '知道了',
|
||||
dontShowAgain: '不再显示',
|
||||
resetHints: '重置提示',
|
||||
resetHintsDesc: '重新显示所有提示',
|
||||
hintsEnabled: '启用提示',
|
||||
hintsEnabledDesc: '访问页面时显示帮助提示',
|
||||
overview: {
|
||||
title: '星球总览',
|
||||
message: '在这里查看星球资源、舰队状态和生产详情。经常查看以监控进度!'
|
||||
},
|
||||
buildings: {
|
||||
title: '建筑',
|
||||
message: '在这里建造和升级建筑。先建太阳能电站获取能量,然后是资源矿。提示:机器人工厂可加速建造!'
|
||||
},
|
||||
research: {
|
||||
title: '研究实验室',
|
||||
message: '研究科技以解锁新舰船、提升战斗力和推进文明。能量科技是个好起点!'
|
||||
},
|
||||
shipyard: {
|
||||
title: '船坞',
|
||||
message: '建造舰船来探索、运输资源和保卫帝国。货船可以在星球之间运送资源。'
|
||||
},
|
||||
fleet: {
|
||||
title: '舰队指挥',
|
||||
message: '派遣舰船执行任务:攻击敌人、运输资源、殖民新星球或探索废墟场。'
|
||||
},
|
||||
galaxy: {
|
||||
title: '星系地图',
|
||||
message: '探索星系寻找可殖民的空星球、可回收的废墟场和可攻击的敌人。先用间谍探测器侦查!'
|
||||
},
|
||||
diplomacy: {
|
||||
title: '外交',
|
||||
message: '管理与NPC的关系。送礼可提升声望,否则可能遭受敌对攻击。敌人的盟友也可能对你敌对!'
|
||||
},
|
||||
messages: {
|
||||
title: '消息',
|
||||
message: '在这里查看战斗报告、间谍报告和外交通知。追踪你的活动和敌人遭遇。'
|
||||
},
|
||||
defense: {
|
||||
title: '星球防御',
|
||||
message: '建造防御设施保护星球免受攻击。护盾和炮塔可以威慑袭击者!'
|
||||
},
|
||||
officers: {
|
||||
title: '军官',
|
||||
message: '招募军官获得各种加成!指挥官加速建造,地质学家提升资源产量,上将增强舰队能力。'
|
||||
},
|
||||
simulator: {
|
||||
title: '战斗模拟器',
|
||||
message: '在发动攻击前模拟战斗结果。输入双方舰队和科技等级,预测胜负和损失。'
|
||||
},
|
||||
settings: {
|
||||
title: '设置',
|
||||
message: '在这里管理游戏数据、调整通知设置、导出/导入存档。记得定期备份你的进度!'
|
||||
},
|
||||
gm: {
|
||||
title: 'GM管理面板',
|
||||
message: 'GM模式可以快速修改资源、建筑、科技等级。用于测试或体验完整游戏内容。'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
export default {
|
||||
home: {
|
||||
subtitle: '征服星辰大海',
|
||||
startGame: '開始遊戲',
|
||||
privacyAgreement: '隱私協議',
|
||||
privacyAgreementDesc: '開始遊戲前,請閱讀並同意我們的隱私協議。',
|
||||
agreeToPrivacy: '我已閱讀並同意',
|
||||
viewFullPolicy: '查看完整協議',
|
||||
agreeAndStart: '同意並開始'
|
||||
},
|
||||
common: {
|
||||
confirm: '確認',
|
||||
cancel: '取消',
|
||||
@@ -36,7 +45,8 @@ export default {
|
||||
requirementsNotMet: '前置條件未滿足',
|
||||
current: '當前',
|
||||
level: '等級',
|
||||
gmModeActivated: 'GM 模式已啟用!請查看導航選單。'
|
||||
gmModeActivated: 'GM 模式已啟用!請查看導航選單。',
|
||||
view: '查看'
|
||||
},
|
||||
errors: {
|
||||
requirementsNotMet: '不滿足前置條件',
|
||||
@@ -113,7 +123,13 @@ export default {
|
||||
homePlanet: '母星',
|
||||
planetPrefix: '星球',
|
||||
moonSuffix: '的月球',
|
||||
colonyPrefix: '殖民地'
|
||||
colonyPrefix: '殖民地',
|
||||
renamePlanet: '重命名星球',
|
||||
renamePlanetTitle: '重命名星球',
|
||||
newPlanetName: '新名稱',
|
||||
planetNamePlaceholder: '輸入新的星球名稱',
|
||||
rename: '重命名',
|
||||
renameSuccess: '星球已重命名為 {name}'
|
||||
},
|
||||
player: {
|
||||
points: '總積分'
|
||||
@@ -286,7 +302,8 @@ export default {
|
||||
hyperspaceTechnology: '超空間跳躍技術',
|
||||
plasmaTechnology: '等離子武器技術',
|
||||
computerTechnology: '增加研究佇列和艦隊任務槽位,每級+1佇列+1槽位(最多10級)',
|
||||
espionageTechnology: '提高間諜探測效果,每級提高1級偵查深度。偵察等級=己方等級-對方等級+偵察船數/5。≥-1顯示艦隊,≥1顯示防禦,≥3顯示建築,≥5顯示科技',
|
||||
espionageTechnology:
|
||||
'提高間諜探測效果,每級提高1級偵查深度。偵察等級=己方等級-對方等級+偵察船數/5。≥-1顯示艦隊,≥1顯示防禦,≥3顯示建築,≥5顯示科技',
|
||||
weaponsTechnology: '提高艦船和防禦的攻擊力,每級+10%',
|
||||
shieldingTechnology: '提高艦船和防禦的護盾值,每級+10%',
|
||||
armourTechnology: '提高艦船和防禦的裝甲值,每級+10%',
|
||||
@@ -319,8 +336,8 @@ export default {
|
||||
darkMatterSpecialist: '提升暗物質採集效率'
|
||||
},
|
||||
queue: {
|
||||
title: '建造佇列',
|
||||
empty: '當前沒有進行中的任務',
|
||||
title: '進行中的任務',
|
||||
empty: '當前沒有進行中的隊列',
|
||||
buildQueue: '建造佇列',
|
||||
researchQueue: '研究佇列',
|
||||
building: '建造中',
|
||||
@@ -333,7 +350,14 @@ export default {
|
||||
confirmCancel: '確定要取消嗎?將返還50%的資源。',
|
||||
level: '等級',
|
||||
gmModeActivated: '',
|
||||
upgradeToLevel: '升級到等級'
|
||||
upgradeToLevel: '升級到等級',
|
||||
tabs: {
|
||||
all: '全部',
|
||||
buildings: '建築',
|
||||
research: '研究',
|
||||
ships: '艦船',
|
||||
defense: '防禦'
|
||||
}
|
||||
},
|
||||
overview: {
|
||||
title: '星球總覽',
|
||||
@@ -587,7 +611,8 @@ export default {
|
||||
sendGift: '贈送禮物',
|
||||
debris: '殘骸',
|
||||
giftPlanetTitle: '贈送禮物',
|
||||
giftPlanetMessage: '確定要向星球 [{coordinates}] 贈送資源嗎?\n\n請前往艦隊頁面選擇運輸船並裝載資源。'
|
||||
giftPlanetMessage: '確定要向星球 [{coordinates}] 贈送資源嗎?\n\n請前往艦隊頁面選擇運輸船並裝載資源。',
|
||||
npcPlanetName: '{name}的星球'
|
||||
},
|
||||
messagesView: {
|
||||
title: '訊息中心',
|
||||
@@ -621,6 +646,7 @@ export default {
|
||||
targetPlanet: '目標星球',
|
||||
attackerRemaining: '攻擊方剩餘',
|
||||
defenderRemaining: '防守方剩餘',
|
||||
allDestroyed: '全部摧毀',
|
||||
moonChance: '月球生成機率',
|
||||
showRoundDetails: '顯示回合詳情',
|
||||
hideRoundDetails: '隱藏回合詳情',
|
||||
@@ -686,7 +712,17 @@ export default {
|
||||
activityDescription: '',
|
||||
npcActivityMessage: '',
|
||||
arrivalTime: '',
|
||||
npcActivityTip: ''
|
||||
npcActivityTip: '',
|
||||
clearMessages: '清空訊息',
|
||||
clearMessageTypes: '選擇要清空的訊息類型',
|
||||
clearBattleReports: '戰鬥報告',
|
||||
clearSpyReports: '間諜報告',
|
||||
clearSpiedNotifications: '被偵查通知',
|
||||
clearMissionReports: '任務報告',
|
||||
clearNPCActivity: 'NPC活動',
|
||||
clearGiftNotifications: '禮物通知',
|
||||
clearGiftRejected: '拒絕記錄',
|
||||
clearNow: '立即清空'
|
||||
},
|
||||
missionReports: {
|
||||
transportSuccess: '運輸任務成功完成',
|
||||
@@ -789,6 +825,7 @@ export default {
|
||||
community: '社群',
|
||||
github: 'GitHub 儲存庫',
|
||||
qqGroup: 'QQ 交流群',
|
||||
privacyPolicy: '隱私協議',
|
||||
notifications: '通知設定',
|
||||
notificationsDesc: '管理遊戲內的通知提醒',
|
||||
notificationTypes: '通知類型',
|
||||
@@ -870,6 +907,7 @@ export default {
|
||||
completeQueuesSuccess: ''
|
||||
},
|
||||
alerts: {
|
||||
incomingFleets: '{count}支敵方艦隊來襲',
|
||||
npcSpyIncoming: 'NPC偵查即將到達',
|
||||
npcAttackIncoming: 'NPC艦隊來襲!',
|
||||
npcFleetIncoming: 'NPC艦隊接近',
|
||||
@@ -881,6 +919,30 @@ export default {
|
||||
npcSpiedYourPlanet: 'NPC偵查了你的星球',
|
||||
npcAttackedYourPlanet: 'NPC攻擊了你的星球'
|
||||
},
|
||||
enemyAlert: {
|
||||
title: '敵方警報',
|
||||
markAllRead: '全部已讀',
|
||||
noAlerts: '暫無警報',
|
||||
fleetSize: '艦隊規模',
|
||||
ships: '艘',
|
||||
viewFleet: '查看艦隊',
|
||||
alertDetails: '警報詳情',
|
||||
targetInfo: '目標資訊',
|
||||
arrivalTime: '到達時間',
|
||||
countdown: '倒數計時',
|
||||
viewMessages: '查看訊息',
|
||||
arrived: '已到達',
|
||||
missionType: {
|
||||
spy: '偵查',
|
||||
attack: '攻擊',
|
||||
unknown: '未知'
|
||||
},
|
||||
warning: {
|
||||
spy: '敵方偵查即將到達!',
|
||||
attack: '敵方攻擊即將到達!',
|
||||
unknown: '敵方艦隊即將到達!'
|
||||
}
|
||||
},
|
||||
diplomacy: {
|
||||
title: '外交',
|
||||
description: '管理與NPC的外交關係',
|
||||
@@ -913,8 +975,13 @@ export default {
|
||||
more: '更多',
|
||||
actions: {
|
||||
gift: '贈送禮物',
|
||||
viewPlanets: '查看星球'
|
||||
viewPlanets: '查看星球',
|
||||
addNote: '新增備註',
|
||||
editNote: '編輯備註'
|
||||
},
|
||||
note: '備註',
|
||||
notePlaceholder: '輸入備註...',
|
||||
noteEmpty: '無備註',
|
||||
lastEvent: '最近事件',
|
||||
reportDetails: '外交報告詳情',
|
||||
eventDescription: '事件描述',
|
||||
@@ -923,6 +990,15 @@ export default {
|
||||
after: '之後',
|
||||
statusChange: '關係狀態變化',
|
||||
viewDiplomacy: '查看外交頁面',
|
||||
eventType: {
|
||||
gift: '贈送了資源',
|
||||
attack: '發起了攻擊',
|
||||
allyAttacked: '攻擊了盟友',
|
||||
spy: '進行了偵查',
|
||||
stealDebris: '搶奪了殘骸',
|
||||
destroyPlanet: '摧毀了星球',
|
||||
unknown: '未知事件'
|
||||
},
|
||||
events: {
|
||||
gift: '已贈送禮物',
|
||||
attack: '攻擊',
|
||||
@@ -959,6 +1035,49 @@ export default {
|
||||
allyOutraged: '{allyName}對你摧毀盟友{targetName}的{planetName}感到憤怒',
|
||||
npcEliminated: 'NPC {npcName}已被徹底消滅',
|
||||
npcEliminatedMessage: '你消滅了{npcName}的所有星球!該勢力已被徹底摧毀。'
|
||||
},
|
||||
searchPlaceholder: '搜索NPC名稱...',
|
||||
viewMode: {
|
||||
card: '卡片',
|
||||
list: '列表'
|
||||
},
|
||||
diagnostic: {
|
||||
button: 'NPC狀態診斷',
|
||||
title: 'NPC狀態診斷',
|
||||
description: '當前玩家積分:{points},偵查間隔:{spyInterval}分鐘,攻擊間隔:{attackInterval}分鐘,攻擊概率:{attackProb}%',
|
||||
noData: '暫無NPC數據',
|
||||
difficulty: '難度',
|
||||
difficultyLevels: {
|
||||
easy: '簡單',
|
||||
medium: '普通',
|
||||
hard: '困難'
|
||||
},
|
||||
reputation: '好感度',
|
||||
spyProbes: '偵察機數量',
|
||||
fleetPower: '艦隊戰力',
|
||||
canSpy: '可以偵查',
|
||||
canAttack: '可以攻擊',
|
||||
attackProbability: '攻擊概率',
|
||||
nextSpy: '下次偵查',
|
||||
nextAttack: '下次攻擊',
|
||||
yes: '是',
|
||||
no: '否',
|
||||
timeFormat: '{min}分{sec}秒',
|
||||
anytime: '隨時可能',
|
||||
statusExplanation: '狀態說明',
|
||||
noRelation: '無關係',
|
||||
noRelationNeutral: '無關係(中立)',
|
||||
reasons: {
|
||||
friendlyNoAction: '關係友好,不會主動行動',
|
||||
neutralNoAction: '關係中立,不會主動行動',
|
||||
hostileWillAct: '關係敵對,可能採取行動',
|
||||
noRelationNeutral: '無外交關係,視為中立',
|
||||
insufficientProbes: '偵察機不足(當前:{current},需要:{required})',
|
||||
noFleet: '沒有戰鬥艦隊',
|
||||
spyCooldown: '偵查冷卻中({min}分{sec}秒)',
|
||||
attackCooldown: '攻擊冷卻中({min}分{sec}秒)',
|
||||
notSpiedYet: '尚未偵查過,需要先進行偵查'
|
||||
}
|
||||
}
|
||||
},
|
||||
pagination: {
|
||||
@@ -974,6 +1093,52 @@ export default {
|
||||
description: '抱歉,您訪問的頁面不存在',
|
||||
goHome: '返回首頁'
|
||||
},
|
||||
privacy: {
|
||||
title: '隱私協議',
|
||||
sections: {
|
||||
introduction: {
|
||||
title: '簡介',
|
||||
content: '本隱私協議說明了 OGame-Vue-Ts 如何處理您的資料。我們致力於保護您的隱私,本遊戲的設計完全尊重用戶隱私。'
|
||||
},
|
||||
dataCollection: {
|
||||
title: '資料收集',
|
||||
content: '本遊戲僅在您的本地瀏覽器中收集和存儲以下資料:',
|
||||
items: {
|
||||
gameProgress: '遊戲進度(建築等級、艦隊、資源等)',
|
||||
settings: '遊戲設定(通知偏好、顯示選項等)',
|
||||
language: '語言偏好'
|
||||
}
|
||||
},
|
||||
dataStorage: {
|
||||
title: '資料存儲',
|
||||
content:
|
||||
'所有資料均存儲在您瀏覽器的本地存儲(localStorage)中。這意味著您的資料始終保留在您自己的設備上,我們無法訪問、查看或收集任何您的遊戲資料。'
|
||||
},
|
||||
noServer: {
|
||||
title: '無伺服器通訊',
|
||||
content:
|
||||
'本遊戲是一個完全離線的單機遊戲。除了檢查更新功能(從 GitHub 獲取版本資訊)外,遊戲不會與任何伺服器通訊。您的遊戲資料永遠不會離開您的設備。'
|
||||
},
|
||||
thirdParty: {
|
||||
title: '第三方服務',
|
||||
content:
|
||||
'本遊戲使用第三方流量分析服務來統計訪問量和流量來源,幫助我們了解用戶使用情況並改進遊戲體驗。這些分析資料是匿名的,不包含任何個人身份資訊。我們不使用任何廣告服務或其他商業追蹤工具。'
|
||||
},
|
||||
dataControl: {
|
||||
title: '資料控制',
|
||||
content: '您對自己的資料擁有完全控制權:',
|
||||
items: {
|
||||
export: '您可以隨時匯出遊戲資料',
|
||||
import: '您可以從備份檔案匯入資料',
|
||||
delete: '您可以通過清除瀏覽器資料或使用遊戲內的「清除資料」功能來刪除所有資料'
|
||||
}
|
||||
},
|
||||
contact: {
|
||||
title: '聯繫我們',
|
||||
content: '如果您對本隱私協議有任何問題,請通過以下方式聯繫我們:'
|
||||
}
|
||||
}
|
||||
},
|
||||
time: {
|
||||
days: '天',
|
||||
hours: '小時',
|
||||
@@ -1004,5 +1169,66 @@ export default {
|
||||
content: '點擊右上角的佇列圖示可以查看建造進度。您可以繼續瀏覽其他頁面,建造會在背景進行。'
|
||||
}
|
||||
}
|
||||
},
|
||||
hints: {
|
||||
close: '關閉',
|
||||
gotIt: '知道了',
|
||||
dontShowAgain: '不再顯示',
|
||||
resetHints: '重置提示',
|
||||
resetHintsDesc: '重新顯示所有提示',
|
||||
hintsEnabled: '啟用提示',
|
||||
hintsEnabledDesc: '訪問頁面時顯示幫助提示',
|
||||
overview: {
|
||||
title: '星球總覽',
|
||||
message: '在這裡查看星球資源、艦隊狀態和生產詳情。經常查看以監控進度!'
|
||||
},
|
||||
buildings: {
|
||||
title: '建築',
|
||||
message: '在這裡建造和升級建築。先建太陽能電站獲取能量,然後是資源礦。提示:機器人工廠可加速建造!'
|
||||
},
|
||||
research: {
|
||||
title: '研究實驗室',
|
||||
message: '研究科技以解鎖新艦船、提升戰鬥力和推進文明。能量科技是個好起點!'
|
||||
},
|
||||
shipyard: {
|
||||
title: '船塢',
|
||||
message: '建造艦船來探索、運輸資源和保衛帝國。貨船可以在星球之間運送資源。'
|
||||
},
|
||||
fleet: {
|
||||
title: '艦隊指揮',
|
||||
message: '派遣艦船執行任務:攻擊敵人、運輸資源、殖民新星球或探索廢墟場。'
|
||||
},
|
||||
galaxy: {
|
||||
title: '星系地圖',
|
||||
message: '探索星系尋找可殖民的空星球、可回收的廢墟場和可攻擊的敵人。先用間諜探測器偵查!'
|
||||
},
|
||||
diplomacy: {
|
||||
title: '外交',
|
||||
message: '管理與NPC的關係。送禮可提升聲望,否則可能遭受敵對攻擊。敵人的盟友也可能對你敵對!'
|
||||
},
|
||||
messages: {
|
||||
title: '訊息',
|
||||
message: '在這裡查看戰鬥報告、間諜報告和外交通知。追蹤你的活動和敵人遭遇。'
|
||||
},
|
||||
defense: {
|
||||
title: '星球防禦',
|
||||
message: '建造防禦設施保護星球免受攻擊。護盾和砲塔可以威懾襲擊者!'
|
||||
},
|
||||
officers: {
|
||||
title: '軍官',
|
||||
message: '招募軍官獲得各種加成!指揮官加速建造,地質學家提升資源產量,上將增強艦隊能力。'
|
||||
},
|
||||
simulator: {
|
||||
title: '戰鬥模擬器',
|
||||
message: '在發動攻擊前模擬戰鬥結果。輸入雙方艦隊和科技等級,預測勝負和損失。'
|
||||
},
|
||||
settings: {
|
||||
title: '設置',
|
||||
message: '在這裡管理遊戲數據、調整通知設置、導出/導入存檔。記得定期備份你的進度!'
|
||||
},
|
||||
gm: {
|
||||
title: 'GM管理面板',
|
||||
message: 'GM模式可以快速修改資源、建築、科技等級。用於測試或體驗完整遊戲內容。'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,24 +234,7 @@ export const handleGiftArrival = (
|
||||
// 计算好感度增加值
|
||||
const reputationGain = calculateGiftReputationGain(mission.cargo)
|
||||
|
||||
// 更新玩家对NPC的关系
|
||||
if (!player.diplomaticRelations) {
|
||||
player.diplomaticRelations = {}
|
||||
}
|
||||
|
||||
const relation = getOrCreateRelation(player.diplomaticRelations, player.id, targetNpc.id)
|
||||
player.diplomaticRelations[targetNpc.id] = updateReputation(
|
||||
relation,
|
||||
reputationGain,
|
||||
DET.GiftResources,
|
||||
t('diplomacy.reports.giftedResources', locale, {
|
||||
metal: mission.cargo.metal.toString(),
|
||||
crystal: mission.cargo.crystal.toString(),
|
||||
deuterium: mission.cargo.deuterium.toString()
|
||||
})
|
||||
)
|
||||
|
||||
// 也更新NPC对玩家的关系(双向好感度)
|
||||
// 更新NPC对玩家的关系(统一使用 npc.relations)
|
||||
if (!targetNpc.relations) {
|
||||
targetNpc.relations = {}
|
||||
}
|
||||
@@ -261,7 +244,11 @@ export const handleGiftArrival = (
|
||||
npcRelation,
|
||||
reputationGain,
|
||||
DET.GiftResources,
|
||||
t('diplomacy.reports.receivedGiftFromPlayer', locale)
|
||||
t('diplomacy.reports.giftedResources', locale, {
|
||||
metal: mission.cargo.metal.toString(),
|
||||
crystal: mission.cargo.crystal.toString(),
|
||||
deuterium: mission.cargo.deuterium.toString()
|
||||
})
|
||||
)
|
||||
|
||||
// 生成外交报告
|
||||
@@ -362,20 +349,7 @@ export const handleAttackReputation = (
|
||||
reputationLoss = REPUTATION_CHANGES.ATTACK_WIN
|
||||
}
|
||||
|
||||
// 更新玩家对被攻击NPC的关系
|
||||
if (!attacker.diplomaticRelations) {
|
||||
attacker.diplomaticRelations = {}
|
||||
}
|
||||
|
||||
const relation = getOrCreateRelation(attacker.diplomaticRelations, attacker.id, defender.id)
|
||||
attacker.diplomaticRelations[defender.id] = updateReputation(
|
||||
relation,
|
||||
reputationLoss,
|
||||
DET.Attack,
|
||||
t('diplomacy.reports.attackedNpc', locale, { npcName: defender.name })
|
||||
)
|
||||
|
||||
// 更新被攻击NPC对玩家的关系
|
||||
// 更新NPC对玩家的关系(统一使用 npc.relations)
|
||||
if (!defender.relations) {
|
||||
defender.relations = {}
|
||||
}
|
||||
@@ -495,19 +469,7 @@ export const handleDebrisRecycleReputation = (player: Player, debrisPosition: Po
|
||||
|
||||
if (npcOwner) {
|
||||
// 这是在NPC星球位置回收残骸,视为抢夺
|
||||
if (!player.diplomaticRelations) {
|
||||
player.diplomaticRelations = {}
|
||||
}
|
||||
|
||||
const relation = getOrCreateRelation(player.diplomaticRelations, player.id, npcOwner.id)
|
||||
player.diplomaticRelations[npcOwner.id] = updateReputation(
|
||||
relation,
|
||||
REPUTATION_CHANGES.STEAL_DEBRIS,
|
||||
DET.StealDebris,
|
||||
t('diplomacy.reports.stoleDebrisFromTerritory', locale, { npcName: npcOwner.name })
|
||||
)
|
||||
|
||||
// 更新NPC对玩家的关系
|
||||
// 更新NPC对玩家的关系(统一使用 npc.relations)
|
||||
if (!npcOwner.relations) {
|
||||
npcOwner.relations = {}
|
||||
}
|
||||
@@ -517,7 +479,7 @@ export const handleDebrisRecycleReputation = (player: Player, debrisPosition: Po
|
||||
npcRelation,
|
||||
REPUTATION_CHANGES.STEAL_DEBRIS,
|
||||
DET.StealDebris,
|
||||
t('diplomacy.reports.playerStoleDebris', locale)
|
||||
t('diplomacy.reports.stoleDebrisFromTerritory', locale, { npcName: npcOwner.name })
|
||||
)
|
||||
|
||||
// 生成外交报告
|
||||
@@ -550,34 +512,7 @@ export const handlePlanetDestructionReputation = (
|
||||
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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// 更新星球所有者对玩家的关系 - 直接设为敌对
|
||||
// 更新星球所有者对玩家的关系 - 直接设为敌对(统一使用 npc.relations)
|
||||
if (!planetOwner.relations) {
|
||||
planetOwner.relations = {}
|
||||
}
|
||||
@@ -703,11 +638,8 @@ const generateDiplomaticReport = (
|
||||
player.diplomaticReports = []
|
||||
}
|
||||
|
||||
if (!player.diplomaticRelations) {
|
||||
player.diplomaticRelations = {}
|
||||
}
|
||||
|
||||
const relation = player.diplomaticRelations[npc.id] || initializeDiplomaticRelation(player.id, npc.id)
|
||||
// 使用 npc.relations 作为唯一数据源
|
||||
const relation = npc.relations?.[player.id] || initializeDiplomaticRelation(npc.id, player.id)
|
||||
const oldStatus = relation.status
|
||||
const newReputation = Math.max(
|
||||
DIPLOMATIC_CONFIG.MIN_REPUTATION,
|
||||
@@ -809,19 +741,6 @@ export const acceptNPCGift = (player: Player, npc: NPC, giftNotification: GiftNo
|
||||
t('diplomacy.reports.giftedResourcesToPlayer', locale)
|
||||
)
|
||||
|
||||
// 也更新玩家对NPC的关系(收到礼物会增加好感)
|
||||
if (!player.diplomaticRelations) {
|
||||
player.diplomaticRelations = {}
|
||||
}
|
||||
|
||||
const playerRelation = getOrCreateRelation(player.diplomaticRelations, player.id, npc.id)
|
||||
player.diplomaticRelations[npc.id] = updateReputation(
|
||||
playerRelation,
|
||||
giftNotification.expectedReputationGain,
|
||||
DET.GiftResources,
|
||||
t('diplomacy.reports.receivedGiftFromNpc', locale, { npcName: npc.name })
|
||||
)
|
||||
|
||||
// 生成外交报告
|
||||
generateDiplomaticReport(
|
||||
player,
|
||||
@@ -893,15 +812,15 @@ export const rejectNPCGift = (player: Player, npc: NPC, giftNotification: GiftNo
|
||||
export const handleNPCElimination = (eliminatedNpc: NPC, player: Player, allNpcs: NPC[], locale: Locale): void => {
|
||||
const { HOSTILE_THRESHOLD } = DIPLOMATIC_CONFIG
|
||||
|
||||
// 1. 将玩家对该NPC的关系设为最低(敌对状态)
|
||||
if (!player.diplomaticRelations) {
|
||||
player.diplomaticRelations = {}
|
||||
// 1. 将NPC对玩家的关系设为最低(敌对状态)
|
||||
if (!eliminatedNpc.relations) {
|
||||
eliminatedNpc.relations = {}
|
||||
}
|
||||
|
||||
const relation = getOrCreateRelation(player.diplomaticRelations, player.id, eliminatedNpc.id)
|
||||
const relation = getOrCreateRelation(eliminatedNpc.relations, eliminatedNpc.id, player.id)
|
||||
const now = Date.now()
|
||||
|
||||
player.diplomaticRelations[eliminatedNpc.id] = {
|
||||
eliminatedNpc.relations[player.id] = {
|
||||
...relation,
|
||||
reputation: HOSTILE_THRESHOLD, // 设为敌对阈值
|
||||
status: RS.Hostile,
|
||||
|
||||
@@ -317,6 +317,23 @@ export const processNPCAttackArrival = async (
|
||||
createdAt: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
// NPC攻击玩家后降低好感度
|
||||
if (!npc.relations) {
|
||||
npc.relations = {}
|
||||
}
|
||||
if (!npc.relations[defender.id]) {
|
||||
npc.relations[defender.id] = diplomaticLogic.initializeDiplomaticRelation(npc.id, defender.id)
|
||||
}
|
||||
|
||||
// 根据战斗结果降低好感度
|
||||
// NPC获胜降低更多好感度,失败降低较少
|
||||
const reputationChange = battleResult.winner === 'attacker' ? -15 : -10
|
||||
const relation = npc.relations[defender.id]
|
||||
if (relation) {
|
||||
diplomaticLogic.updateReputation(relation, reputationChange, 'attack', `NPC ${npc.name} attacked player ${defender.name}`)
|
||||
}
|
||||
|
||||
return { battleResult, moon, debrisField }
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@ export const initializePlayer = (playerId: string, playerName: string = 'Command
|
||||
incomingFleetAlerts: [],
|
||||
giftNotifications: [],
|
||||
giftRejectedNotifications: [],
|
||||
diplomaticRelations: {},
|
||||
diplomaticReports: [],
|
||||
points: 0
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ export interface DynamicBehaviorConfig {
|
||||
attackProbability: number
|
||||
minSpyProbes: number
|
||||
attackFleetSizeRatio: number
|
||||
maxConcurrentSpyMissions: number // 同时最多多少个侦查任务
|
||||
maxConcurrentAttackMissions: number // 同时最多多少个攻击任务
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -33,47 +35,57 @@ export const calculateDynamicBehavior = (playerPoints: number): DynamicBehaviorC
|
||||
if (playerPoints < 1000) {
|
||||
// 新手阶段:NPC温和但会主动侦查攻击
|
||||
return {
|
||||
spyInterval: 300, // 5分钟侦查一次(让新玩家快速体验游戏内容)
|
||||
attackInterval: 600, // 10分钟攻击一次
|
||||
attackProbability: 0.4, // 40%概率攻击
|
||||
spyInterval: 300, // 5分钟侦查一次
|
||||
attackInterval: 300, // 5分钟攻击一次(与侦查同步,侦查完就攻击)
|
||||
attackProbability: 0.4,
|
||||
minSpyProbes: 1,
|
||||
attackFleetSizeRatio: 0.3 // 只派30%舰队
|
||||
attackFleetSizeRatio: 0.3, // 只派30%舰队
|
||||
maxConcurrentSpyMissions: 3,
|
||||
maxConcurrentAttackMissions: 2
|
||||
}
|
||||
} else if (playerPoints < 5000) {
|
||||
// 初级阶段:NPC比较激进
|
||||
return {
|
||||
spyInterval: 420, // 7分钟侦查一次
|
||||
attackInterval: 900, // 15分钟攻击一次
|
||||
attackProbability: 0.45, // 45%概率攻击
|
||||
attackInterval: 420, // 7分钟攻击一次(与侦查同步)
|
||||
attackProbability: 0.45,
|
||||
minSpyProbes: 2,
|
||||
attackFleetSizeRatio: 0.5 // 派50%舰队
|
||||
attackFleetSizeRatio: 0.5, // 派50%舰队
|
||||
maxConcurrentSpyMissions: 5,
|
||||
maxConcurrentAttackMissions: 3
|
||||
}
|
||||
} else if (playerPoints < 20000) {
|
||||
// 中级阶段:NPC很激进
|
||||
return {
|
||||
spyInterval: 360, // 6分钟侦查一次
|
||||
attackInterval: 720, // 12分钟攻击一次
|
||||
attackProbability: 0.55, // 55%概率攻击
|
||||
attackInterval: 360, // 6分钟攻击一次(与侦查同步)
|
||||
attackProbability: 0.55,
|
||||
minSpyProbes: 3,
|
||||
attackFleetSizeRatio: 0.7 // 派70%舰队
|
||||
attackFleetSizeRatio: 0.7, // 派70%舰队
|
||||
maxConcurrentSpyMissions: 8,
|
||||
maxConcurrentAttackMissions: 5
|
||||
}
|
||||
} else if (playerPoints < 50000) {
|
||||
// 高级阶段:NPC非常激进
|
||||
return {
|
||||
spyInterval: 300, // 5分钟侦查一次
|
||||
attackInterval: 600, // 10分钟攻击一次
|
||||
attackProbability: 0.65, // 65%概率攻击
|
||||
attackInterval: 300, // 5分钟攻击一次(与侦查同步)
|
||||
attackProbability: 0.65,
|
||||
minSpyProbes: 4,
|
||||
attackFleetSizeRatio: 0.85 // 派85%舰队
|
||||
attackFleetSizeRatio: 0.85, // 派85%舰队
|
||||
maxConcurrentSpyMissions: 10,
|
||||
maxConcurrentAttackMissions: 8
|
||||
}
|
||||
} else {
|
||||
// 专家阶段:NPC极度激进
|
||||
return {
|
||||
spyInterval: 240, // 4分钟侦查一次
|
||||
attackInterval: 480, // 8分钟攻击一次
|
||||
attackProbability: 0.8, // 80%概率攻击
|
||||
attackInterval: 240, // 4分钟攻击一次(与侦查同步)
|
||||
attackProbability: 0.8,
|
||||
minSpyProbes: 5,
|
||||
attackFleetSizeRatio: 0.95 // 派95%舰队
|
||||
attackFleetSizeRatio: 0.95, // 派95%舰队
|
||||
maxConcurrentSpyMissions: 15,
|
||||
maxConcurrentAttackMissions: 12
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,27 +100,31 @@ export const shouldNPCSpyPlayer = (npc: NPC, player: Player, currentTime: number
|
||||
return false
|
||||
}
|
||||
|
||||
const lastSpyTime = npc.lastSpyTime || 0
|
||||
// 检查外交关系 - 统一使用 npc.relations
|
||||
const relation = npc.relations?.[player.id]
|
||||
|
||||
// 检查是否达到侦查间隔
|
||||
// 如果没有关系数据,视为中立,不侦查
|
||||
if (!relation) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 友好或中立NPC不侦查
|
||||
if (relation.status === RelationStatus.Friendly || relation.status === RelationStatus.Neutral) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 只有敌对NPC才会继续
|
||||
if (relation.status !== RelationStatus.Hostile) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 只有敌对NPC才会到达这里,检查冷却时间
|
||||
const lastSpyTime = npc.lastSpyTime || 0
|
||||
if (currentTime - lastSpyTime < config.spyInterval * 1000) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查外交关系 - 只有中立和敌对NPC才会侦查
|
||||
const relation = npc.relations?.[player.id]
|
||||
if (relation) {
|
||||
if (relation.status === RelationStatus.Friendly) {
|
||||
// 友好NPC不侦查玩家
|
||||
return false
|
||||
}
|
||||
if (relation.status === RelationStatus.Hostile) {
|
||||
// 敌对NPC必定侦查
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// 中立或无关系:正常侦查
|
||||
// 敌对NPC且冷却结束,执行侦查
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -122,32 +138,38 @@ export const shouldNPCAttackPlayer = (npc: NPC, player: Player, currentTime: num
|
||||
return false
|
||||
}
|
||||
|
||||
const lastAttackTime = npc.lastAttackTime || 0
|
||||
// 检查外交关系 - 统一使用 npc.relations
|
||||
const relation = npc.relations?.[player.id]
|
||||
|
||||
// 检查是否达到攻击间隔
|
||||
// 如果没有关系数据,视为中立,不攻击
|
||||
if (!relation) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 友好或中立NPC不攻击
|
||||
if (relation.status === RelationStatus.Friendly || relation.status === RelationStatus.Neutral) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 只有敌对NPC才会继续
|
||||
if (relation.status !== RelationStatus.Hostile) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查攻击冷却
|
||||
const lastAttackTime = npc.lastAttackTime || 0
|
||||
if (currentTime - lastAttackTime < config.attackInterval * 1000) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查外交关系
|
||||
const relation = npc.relations?.[player.id]
|
||||
if (relation) {
|
||||
if (relation.status === RelationStatus.Friendly) {
|
||||
// 友好NPC不攻击玩家
|
||||
// 必须有侦查报告才能攻击
|
||||
if (!npc.playerSpyReports || Object.keys(npc.playerSpyReports).length === 0) {
|
||||
return false
|
||||
}
|
||||
if (relation.status === RelationStatus.Neutral) {
|
||||
// 中立NPC有概率攻击玩家(使用正常概率)
|
||||
return Math.random() < config.attackProbability
|
||||
}
|
||||
if (relation.status === RelationStatus.Hostile) {
|
||||
// 敌对NPC攻击概率翻倍(更激进)
|
||||
return Math.random() < Math.min(config.attackProbability * 2.0, 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
// 无关系的NPC:使用正常概率攻击
|
||||
return Math.random() < config.attackProbability
|
||||
// 有侦查报告的情况下,敌对NPC一定会攻击(移除概率限制)
|
||||
// 这样保证侦查后会跟进攻击,而不是无意义地反复侦查
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -167,7 +189,7 @@ export const shouldNPCGiftPlayer = (npc: NPC, player: Player, currentTime: numbe
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查NPC对玩家的好感度
|
||||
// 检查好感度 - 统一使用 npc.relations
|
||||
const relation = npc.relations?.[player.id]
|
||||
if (!relation || relation.reputation < NPC_GIFT_CONFIG.MIN_REPUTATION) {
|
||||
return false
|
||||
@@ -228,6 +250,35 @@ const selectBestNPCPlanet = (npc: NPC, targetPosition: { galaxy: number; system:
|
||||
return bestPlanet
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算NPC星球的战斗舰队总攻击力
|
||||
* 用于判断NPC是否有足够的战斗力来发起有意义的攻击
|
||||
*/
|
||||
const calculateNPCCombatPower = (npcPlanet: Planet): number => {
|
||||
// 各舰船的攻击力
|
||||
const shipAttackPower: Record<string, number> = {
|
||||
[ShipType.LightFighter]: 50,
|
||||
[ShipType.HeavyFighter]: 150,
|
||||
[ShipType.Cruiser]: 400,
|
||||
[ShipType.Battleship]: 1200,
|
||||
[ShipType.Bomber]: 700,
|
||||
[ShipType.Destroyer]: 2500,
|
||||
[ShipType.Battlecruiser]: 1000,
|
||||
[ShipType.Deathstar]: 200000
|
||||
}
|
||||
|
||||
let totalPower = 0
|
||||
for (const [shipType, attack] of Object.entries(shipAttackPower)) {
|
||||
const count = npcPlanet.fleet[shipType as ShipType] || 0
|
||||
totalPower += count * attack
|
||||
}
|
||||
return totalPower
|
||||
}
|
||||
|
||||
// 最小战斗力阈值:相当于约10艘轻型战斗机的攻击力
|
||||
// 这样避免NPC只有几艘小飞机就频繁侦查骚扰玩家
|
||||
const MIN_COMBAT_POWER_FOR_SPY = 500
|
||||
|
||||
/**
|
||||
* 创建NPC侦查任务
|
||||
*/
|
||||
@@ -249,6 +300,13 @@ export const createNPCSpyMission = (
|
||||
return null
|
||||
}
|
||||
|
||||
// 检查NPC是否有足够的战斗力
|
||||
// 战斗力太低的话侦查没有意义,也避免频繁骚扰玩家
|
||||
const combatPower = calculateNPCCombatPower(npcPlanet)
|
||||
if (combatPower < MIN_COMBAT_POWER_FOR_SPY) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 创建侦查舰队
|
||||
const fleet: Partial<Fleet> = {
|
||||
[ShipType.EspionageProbe]: config.minSpyProbes
|
||||
@@ -376,13 +434,13 @@ const decideAttackFleet = (_npc: NPC, npcPlanet: Planet, _spyReport: SpyReport,
|
||||
for (const shipType of combatShips) {
|
||||
const available = npcPlanet.fleet[shipType] || 0
|
||||
if (available > 0) {
|
||||
const sendCount = Math.floor(available * config.attackFleetSizeRatio)
|
||||
if (sendCount > 0) {
|
||||
// 使用 Math.ceil 确保至少派出1艘(如果有的话)
|
||||
// 但不能超过可用数量
|
||||
const sendCount = Math.min(available, Math.max(1, Math.floor(available * config.attackFleetSizeRatio)))
|
||||
attackFleet[shipType] = sendCount
|
||||
hasShips = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return hasShips ? attackFleet : null
|
||||
}
|
||||
@@ -598,6 +656,111 @@ export const updateNPCBehavior = (
|
||||
updateIncomingFleetAlerts(player, currentTime)
|
||||
}
|
||||
|
||||
/**
|
||||
* 带并发限制的NPC行为更新函数
|
||||
* 防止同时产生过多侦查和攻击任务导致游戏卡顿
|
||||
*/
|
||||
export const updateNPCBehaviorWithLimit = (
|
||||
npc: NPC,
|
||||
player: Player,
|
||||
allPlanets: Planet[],
|
||||
debrisFields: Record<string, DebrisField>,
|
||||
currentTime: number,
|
||||
limits: {
|
||||
activeSpyMissions: number
|
||||
activeAttackMissions: number
|
||||
config: DynamicBehaviorConfig
|
||||
}
|
||||
): { spyCreated: boolean; attackCreated: boolean } => {
|
||||
const { activeSpyMissions, activeAttackMissions, config } = limits
|
||||
let spyCreated = false
|
||||
let attackCreated = false
|
||||
|
||||
// 1. 检查并回收附近的残骸(优先级最高,不受并发限制)
|
||||
const nearbyDebris = findNearbyDebris(npc, debrisFields)
|
||||
if (nearbyDebris.length > 0) {
|
||||
const activeRecycleMissions = npc.fleetMissions?.filter(m => m.missionType === MissionType.Recycle && m.status === 'outbound') || []
|
||||
const activeDebrisIds = new Set(activeRecycleMissions.map(m => m.debrisFieldId).filter(Boolean))
|
||||
const availableDebris = nearbyDebris.filter(d => !activeDebrisIds.has(d.id))
|
||||
|
||||
if (availableDebris.length > 0) {
|
||||
const targetDebris = availableDebris[Math.floor(Math.random() * availableDebris.length)]
|
||||
if (targetDebris) {
|
||||
createNPCRecycleMission(npc, targetDebris, player, allPlanets)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 检查是否应该反击(优先于普通攻击,受攻击并发限制)
|
||||
if (activeAttackMissions < config.maxConcurrentAttackMissions && shouldNPCRevenge(npc, currentTime)) {
|
||||
const revengeMission = createNPCRevengeMission(npc, allPlanets, config)
|
||||
if (revengeMission) {
|
||||
const targetPlanet = allPlanets.find(p => p.id === revengeMission.targetPlanetId)
|
||||
if (targetPlanet) {
|
||||
const alert = createIncomingFleetAlert(revengeMission, npc, targetPlanet)
|
||||
if (!player.incomingFleetAlerts) {
|
||||
player.incomingFleetAlerts = []
|
||||
}
|
||||
player.incomingFleetAlerts.push(alert)
|
||||
attackCreated = true
|
||||
}
|
||||
return { spyCreated, attackCreated }
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 检查是否应该侦查玩家(受侦查并发限制)
|
||||
if (activeSpyMissions < config.maxConcurrentSpyMissions && shouldNPCSpyPlayer(npc, player, currentTime, config)) {
|
||||
const playerPlanets = allPlanets.filter(p => p.ownerId === player.id)
|
||||
if (playerPlanets.length > 0) {
|
||||
const targetPlanet = playerPlanets[Math.floor(Math.random() * playerPlanets.length)]
|
||||
if (targetPlanet) {
|
||||
const spyMission = createNPCSpyMission(npc, targetPlanet, allPlanets, config)
|
||||
if (spyMission) {
|
||||
const alert = createIncomingFleetAlert(spyMission, npc, targetPlanet)
|
||||
if (!player.incomingFleetAlerts) {
|
||||
player.incomingFleetAlerts = []
|
||||
}
|
||||
player.incomingFleetAlerts.push(alert)
|
||||
spyCreated = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 检查是否应该攻击玩家(受攻击并发限制)
|
||||
if (activeAttackMissions < config.maxConcurrentAttackMissions && shouldNPCAttackPlayer(npc, player, currentTime, config)) {
|
||||
if (npc.playerSpyReports && Object.keys(npc.playerSpyReports).length > 0) {
|
||||
const spyReports = Object.values(npc.playerSpyReports)
|
||||
const recentReport = spyReports[Math.floor(Math.random() * spyReports.length)]
|
||||
|
||||
if (recentReport) {
|
||||
const targetPlanet = allPlanets.find(p => p.id === recentReport.targetPlanetId)
|
||||
if (targetPlanet) {
|
||||
const attackMission = createNPCAttackMission(npc, targetPlanet, recentReport, config)
|
||||
if (attackMission) {
|
||||
const alert = createIncomingFleetAlert(attackMission, npc, targetPlanet)
|
||||
if (!player.incomingFleetAlerts) {
|
||||
player.incomingFleetAlerts = []
|
||||
}
|
||||
player.incomingFleetAlerts.push(alert)
|
||||
attackCreated = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 检查是否应该赠送资源给玩家(仅友好NPC,不受并发限制)
|
||||
if (shouldNPCGiftPlayer(npc, player, currentTime)) {
|
||||
giftResourcesToPlayer(npc, player)
|
||||
}
|
||||
|
||||
// 6. 更新即将到来的舰队警告(删除过期的)
|
||||
updateIncomingFleetAlerts(player, currentTime)
|
||||
|
||||
return { spyCreated, attackCreated }
|
||||
}
|
||||
|
||||
// ========== 测试辅助函数 ==========
|
||||
|
||||
/**
|
||||
@@ -1080,11 +1243,22 @@ export const createNPCRevengeMission = (npc: NPC, allPlanets: Planet[], config:
|
||||
/**
|
||||
* NPC状态诊断函数 - 用于调试和了解NPC当前状态
|
||||
*/
|
||||
|
||||
// 诊断原因类型,包含翻译键和参数
|
||||
export interface DiagnosticReason {
|
||||
key: string // 翻译键,如 'friendlyNoAction', 'insufficientProbes'
|
||||
params?: Record<string, string | number> // 翻译参数,如 { current: 5, required: 10 }
|
||||
}
|
||||
|
||||
// 关系状态翻译键类型
|
||||
export type RelationStatusKey = 'friendly' | 'hostile' | 'neutral' | 'noRelation' | 'noRelationNeutral'
|
||||
|
||||
export interface NPCDiagnosticInfo {
|
||||
npcId: string
|
||||
npcName: string
|
||||
difficulty: string
|
||||
relationStatus: string
|
||||
relationStatus: string // 保持原有字段用于显示
|
||||
relationStatusKey: RelationStatusKey // 翻译键
|
||||
reputation: number
|
||||
canSpy: boolean
|
||||
canAttack: boolean
|
||||
@@ -1097,47 +1271,55 @@ export interface NPCDiagnosticInfo {
|
||||
nextSpyIn: number
|
||||
nextAttackIn: number
|
||||
attackProbability: number
|
||||
reasons: string[]
|
||||
reasons: DiagnosticReason[] // 改为结构化原因数组
|
||||
}
|
||||
|
||||
export const diagnoseNPCBehavior = (
|
||||
npcs: NPC[],
|
||||
player: Player,
|
||||
currentTime: number
|
||||
): NPCDiagnosticInfo[] => {
|
||||
export const diagnoseNPCBehavior = (npcs: NPC[], player: Player, currentTime: number): NPCDiagnosticInfo[] => {
|
||||
const playerPoints = player.points || 0
|
||||
const config = calculateDynamicBehavior(playerPoints)
|
||||
|
||||
return npcs.map(npc => {
|
||||
const planet = npc.planets[0]
|
||||
const relation = npc.relations?.[player.id]
|
||||
const reasons: string[] = []
|
||||
const reasons: DiagnosticReason[] = []
|
||||
|
||||
// 检查外交关系
|
||||
let canSpy = true
|
||||
let canAttack = true
|
||||
let relationStatus = '无关系'
|
||||
let canSpy = false // 默认不能侦查,只有敌对NPC才能侦查
|
||||
let canAttack = false // 默认不能攻击,只有敌对NPC才能攻击
|
||||
let relationStatus = ''
|
||||
let relationStatusKey: RelationStatusKey = 'noRelation'
|
||||
let reputation = 0
|
||||
|
||||
if (relation) {
|
||||
relationStatus = relation.status === RelationStatus.Friendly ? '友好' :
|
||||
relation.status === RelationStatus.Hostile ? '敌对' : '中立'
|
||||
reputation = relation.reputation || 0
|
||||
|
||||
if (relation.status === RelationStatus.Friendly) {
|
||||
canSpy = false
|
||||
canAttack = false
|
||||
reasons.push('友好NPC不会侦查或攻击玩家')
|
||||
relationStatus = 'friendly'
|
||||
relationStatusKey = 'friendly'
|
||||
reasons.push({ key: 'friendlyNoAction' })
|
||||
} else if (relation.status === RelationStatus.Neutral) {
|
||||
relationStatus = 'neutral'
|
||||
relationStatusKey = 'neutral'
|
||||
reasons.push({ key: 'neutralNoAction' })
|
||||
} else if (relation.status === RelationStatus.Hostile) {
|
||||
reasons.push('敌对NPC攻击概率翻倍')
|
||||
relationStatus = 'hostile'
|
||||
relationStatusKey = 'hostile'
|
||||
canSpy = true
|
||||
canAttack = true
|
||||
reasons.push({ key: 'hostileWillAct' })
|
||||
}
|
||||
} else {
|
||||
// 无关系的NPC视为中立
|
||||
relationStatus = 'noRelationNeutral'
|
||||
relationStatusKey = 'noRelationNeutral'
|
||||
reasons.push({ key: 'noRelationNeutral' })
|
||||
}
|
||||
|
||||
// 检查侦查探测器数量
|
||||
const spyProbes = planet?.fleet?.[ShipType.EspionageProbe] || 0
|
||||
if (spyProbes < config.minSpyProbes) {
|
||||
canSpy = false
|
||||
reasons.push(`侦查探测器不足 (${spyProbes}/${config.minSpyProbes})`)
|
||||
reasons.push({ key: 'insufficientProbes', params: { current: spyProbes, required: config.minSpyProbes } })
|
||||
}
|
||||
|
||||
// 计算舰队战力
|
||||
@@ -1154,7 +1336,7 @@ export const diagnoseNPCBehavior = (
|
||||
|
||||
if (totalFleetPower === 0) {
|
||||
canAttack = false
|
||||
reasons.push('没有战斗舰队')
|
||||
reasons.push({ key: 'noFleet' })
|
||||
}
|
||||
|
||||
// 时间检查
|
||||
@@ -1167,11 +1349,11 @@ export const diagnoseNPCBehavior = (
|
||||
const nextAttackIn = Math.max(0, config.attackInterval - timeSinceLastAttack)
|
||||
|
||||
if (timeSinceLastSpy < config.spyInterval) {
|
||||
reasons.push(`侦查冷却中 (${Math.floor(nextSpyIn / 60)}分${nextSpyIn % 60}秒)`)
|
||||
reasons.push({ key: 'spyCooldown', params: { min: Math.floor(nextSpyIn / 60), sec: nextSpyIn % 60 } })
|
||||
}
|
||||
|
||||
if (timeSinceLastAttack < config.attackInterval) {
|
||||
reasons.push(`攻击冷却中 (${Math.floor(nextAttackIn / 60)}分${nextAttackIn % 60}秒)`)
|
||||
reasons.push({ key: 'attackCooldown', params: { min: Math.floor(nextAttackIn / 60), sec: nextAttackIn % 60 } })
|
||||
}
|
||||
|
||||
// 检查是否已经侦查过玩家
|
||||
@@ -1179,7 +1361,7 @@ export const diagnoseNPCBehavior = (
|
||||
|
||||
if (!hasSpiedPlayer && canAttack) {
|
||||
canAttack = false
|
||||
reasons.push('尚未侦查过玩家,无法攻击')
|
||||
reasons.push({ key: 'notSpiedYet' })
|
||||
}
|
||||
|
||||
// 计算实际攻击概率
|
||||
@@ -1193,6 +1375,7 @@ export const diagnoseNPCBehavior = (
|
||||
npcName: npc.name,
|
||||
difficulty: npc.difficulty,
|
||||
relationStatus,
|
||||
relationStatusKey,
|
||||
reputation,
|
||||
canSpy,
|
||||
canAttack,
|
||||
|
||||
@@ -417,7 +417,7 @@ export const autoBuildNPCFleet = (npc: NPC): void => {
|
||||
/**
|
||||
* 为NPC生成资源(模拟资源生产)
|
||||
*/
|
||||
export const generateNPCResources = (npc: NPC, deltaSeconds: number, config: DynamicDifficultyConfig): void => {
|
||||
export const generateNPCResources = (npc: NPC, deltaSeconds: number, config: DynamicDifficultyConfig, gameSpeed: number = 1): void => {
|
||||
const planet = npc.planets[0]
|
||||
if (!planet) return
|
||||
|
||||
@@ -433,11 +433,14 @@ export const generateNPCResources = (npc: NPC, deltaSeconds: number, config: Dyn
|
||||
const deuteriumProduction = 10 * deuteriumLevel * Math.pow(1.1, deuteriumLevel) * config.resourceGrowthRate
|
||||
const darkMatterProduction = ((25 * darkMatterLevel * Math.pow(1.5, darkMatterLevel)) / 3600) * config.resourceGrowthRate
|
||||
|
||||
// 应用游戏速度倍率到时间
|
||||
const effectiveDeltaSeconds = deltaSeconds * gameSpeed
|
||||
|
||||
// 增加资源
|
||||
planet.resources.metal += metalProduction * deltaSeconds
|
||||
planet.resources.crystal += crystalProduction * deltaSeconds
|
||||
planet.resources.deuterium += deuteriumProduction * deltaSeconds
|
||||
planet.resources.darkMatter += darkMatterProduction * deltaSeconds
|
||||
planet.resources.metal += metalProduction * effectiveDeltaSeconds
|
||||
planet.resources.crystal += crystalProduction * effectiveDeltaSeconds
|
||||
planet.resources.deuterium += deuteriumProduction * effectiveDeltaSeconds
|
||||
planet.resources.darkMatter += darkMatterProduction * effectiveDeltaSeconds
|
||||
|
||||
// 确保不超过存储上限
|
||||
const metalStorage = planet.buildings[BuildingType.MetalStorage] || 0
|
||||
@@ -455,12 +458,12 @@ export const generateNPCResources = (npc: NPC, deltaSeconds: number, config: Dyn
|
||||
* 主NPC成长更新函数
|
||||
* 应该在游戏循环中定期调用
|
||||
*/
|
||||
export const updateNPCGrowth = (npc: NPC, gameState: NPCGrowthGameState, deltaSeconds: number): void => {
|
||||
export const updateNPCGrowth = (npc: NPC, gameState: NPCGrowthGameState, deltaSeconds: number, gameSpeed: number = 1): void => {
|
||||
// 使用动态难度(基于玩家积分)而不是固定难度
|
||||
const config = calculateDynamicDifficulty(gameState.player.points)
|
||||
|
||||
// 1. 持续生成资源
|
||||
generateNPCResources(npc, deltaSeconds, config)
|
||||
// 1. 持续生成资源(应用游戏速度倍率)
|
||||
generateNPCResources(npc, deltaSeconds, config, gameSpeed)
|
||||
|
||||
// 2. 定期评估并调整实力(使用静态计数器或时间戳)
|
||||
const now = Date.now()
|
||||
|
||||
@@ -67,10 +67,14 @@ export const calculateResourceProduction = (
|
||||
// 计算能量产出(每小时)
|
||||
const energyProduction = calculateEnergyProduction(planet, { energyProductionBonus: bonuses.energyProductionBonus })
|
||||
|
||||
// 检查当前能量是否充足
|
||||
// 如果当前能量 <= 0,矿场停止生产
|
||||
const hasEnergy = planet.resources.energy > 0
|
||||
const productionEfficiency = hasEnergy ? 1 : 0
|
||||
// 计算能量消耗(每小时)
|
||||
const energyConsumption = calculateEnergyConsumption(planet)
|
||||
|
||||
// 检查能量平衡是否充足
|
||||
// 如果能量产出 >= 能量消耗,矿场正常生产
|
||||
// 这样即使浏览器关闭后再打开,只要能量平衡是正的,就能正常生产
|
||||
const hasPositiveEnergyBalance = energyProduction >= energyConsumption
|
||||
const productionEfficiency = hasPositiveEnergyBalance ? 1 : 0
|
||||
|
||||
return {
|
||||
metal: metalMineLevel * 1500 * Math.pow(1.5, metalMineLevel) * resourceBonus * productionEfficiency,
|
||||
@@ -296,8 +300,11 @@ export const calculateProductionBreakdown = (
|
||||
const darkMatterCollectorLevel = planet.buildings[BuildingType.DarkMatterCollector] || 0
|
||||
const solarPlantLevel = planet.buildings[BuildingType.SolarPlant] || 0
|
||||
|
||||
const hasEnergy = planet.resources.energy > 0
|
||||
const productionEfficiency = hasEnergy ? 1 : 0
|
||||
// 计算能量平衡(基于产出vs消耗,而不是当前能量值)
|
||||
const energyProduction = calculateEnergyProduction(planet, { energyProductionBonus: 0 })
|
||||
const energyConsumption = calculateEnergyConsumption(planet)
|
||||
const hasPositiveEnergyBalance = energyProduction >= energyConsumption
|
||||
const productionEfficiency = hasPositiveEnergyBalance ? 1 : 0
|
||||
|
||||
// 收集每个军官的加成信息
|
||||
const activeOfficerBonuses: Array<{
|
||||
@@ -343,7 +350,7 @@ export const calculateProductionBreakdown = (
|
||||
}
|
||||
})
|
||||
|
||||
if (!hasEnergy) {
|
||||
if (!hasPositiveEnergyBalance) {
|
||||
metalBonuses.push({
|
||||
name: 'resources.noEnergy',
|
||||
percentage: -100,
|
||||
@@ -370,7 +377,7 @@ export const calculateProductionBreakdown = (
|
||||
}
|
||||
})
|
||||
|
||||
if (!hasEnergy) {
|
||||
if (!hasPositiveEnergyBalance) {
|
||||
crystalBonuses.push({
|
||||
name: 'resources.noEnergy',
|
||||
percentage: -100,
|
||||
@@ -397,7 +404,7 @@ export const calculateProductionBreakdown = (
|
||||
}
|
||||
})
|
||||
|
||||
if (!hasEnergy) {
|
||||
if (!hasPositiveEnergyBalance) {
|
||||
deuteriumBonuses.push({
|
||||
name: 'resources.noEnergy',
|
||||
percentage: -100,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes: [
|
||||
{ path: '/', name: 'overview', component: () => import('@/views/OverviewView.vue') },
|
||||
{ path: '/', name: 'home', component: () => import('@/views/HomeView.vue') },
|
||||
{ path: '/overview', name: 'overview', component: () => import('@/views/OverviewView.vue') },
|
||||
{ path: '/buildings', name: 'buildings', component: () => import('@/views/BuildingsView.vue') },
|
||||
{ path: '/research', name: 'research', component: () => import('@/views/ResearchView.vue') },
|
||||
{ path: '/shipyard', name: 'shipyard', component: () => import('@/views/ShipyardView.vue') },
|
||||
@@ -20,4 +22,31 @@ const router = createRouter({
|
||||
]
|
||||
})
|
||||
|
||||
// 路由守卫:检查隐私协议同意状态
|
||||
router.beforeEach((to, _from, next) => {
|
||||
const gameStore = useGameStore()
|
||||
|
||||
// 已同意隐私协议
|
||||
if (gameStore.player.privacyAgreed) {
|
||||
// 已同意但访问首页,重定向到总览页
|
||||
if (to.path === '/') {
|
||||
next('/overview')
|
||||
return
|
||||
}
|
||||
// 正常访问其他页面
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
// 未同意隐私协议
|
||||
// 允许访问首页(用于显示隐私协议同意弹窗)
|
||||
if (to.path === '/') {
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
// 未同意隐私协议且访问其他页面,重定向到首页
|
||||
next('/')
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
@@ -569,11 +569,17 @@ export interface Player {
|
||||
isGMEnabled?: boolean // GM模式开关(默认false,通过秘籍激活)
|
||||
lastVersionCheckTime?: number // 最后一次自动检查版本的时间戳(被动检测)
|
||||
lastManualUpdateCheck?: number // 最后一次手动检查更新的时间戳(主动检测)
|
||||
// 外交系统字段
|
||||
diplomaticRelations?: Record<string, DiplomaticRelation> // 玩家对NPC的关系(key: npcId)
|
||||
// 外交系统字段(外交关系存储在 NPC.relations 中)
|
||||
diplomaticReports?: DiplomaticReport[] // 外交变化报告
|
||||
// 新手引导字段
|
||||
tutorialProgress?: TutorialProgress // 新手引导进度
|
||||
// 隐私协议同意状态
|
||||
privacyAgreed?: boolean // 是否已同意隐私协议
|
||||
// 弱引导系统
|
||||
dismissedHints?: string[] // 已关闭的提示ID列表
|
||||
hintsEnabled?: boolean // 是否启用弱引导提示(默认true)
|
||||
// 显示设置
|
||||
backgroundEnabled?: boolean // 是否启用背景动画(默认false)
|
||||
}
|
||||
|
||||
export interface NotificationSettings {
|
||||
@@ -609,6 +615,7 @@ export interface Universe {
|
||||
export interface NPC {
|
||||
id: string
|
||||
name: string
|
||||
note?: string // 玩家添加的备注
|
||||
planets: Planet[]
|
||||
technologies: Record<TechnologyType, number>
|
||||
difficulty: 'easy' | 'medium' | 'hard'
|
||||
|
||||
@@ -33,10 +33,116 @@ export const migrateGameData = (): void => {
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否需要迁移
|
||||
const hasOldMapData = oldData.universePlanets || oldData.debrisFields
|
||||
if (!hasOldMapData) return
|
||||
// 标记是否有数据需要保存
|
||||
let needsSave = false
|
||||
|
||||
// 修复NPC数据(确保所有必需字段都存在)
|
||||
if (oldData.npcs && Array.isArray(oldData.npcs)) {
|
||||
const now = Date.now()
|
||||
const playerId = oldData.player?.id
|
||||
|
||||
oldData.npcs.forEach((npc: NPC) => {
|
||||
// 确保NPC有必需的时间字段,并设置随机冷却避免同时行动
|
||||
if (npc.lastSpyTime === undefined || npc.lastSpyTime === 0) {
|
||||
// 0-4分钟的随机延迟
|
||||
const randomSpyOffset = Math.random() * 240 * 1000
|
||||
npc.lastSpyTime = now - randomSpyOffset
|
||||
needsSave = true
|
||||
}
|
||||
if (npc.lastAttackTime === undefined || npc.lastAttackTime === 0) {
|
||||
// 0-8分钟的随机延迟
|
||||
const randomAttackOffset = Math.random() * 480 * 1000
|
||||
npc.lastAttackTime = now - randomAttackOffset
|
||||
needsSave = true
|
||||
}
|
||||
// 确保NPC有必需的数组字段
|
||||
if (!npc.fleetMissions) {
|
||||
npc.fleetMissions = []
|
||||
needsSave = true
|
||||
}
|
||||
if (!npc.playerSpyReports) {
|
||||
npc.playerSpyReports = {}
|
||||
needsSave = true
|
||||
}
|
||||
if (!npc.relations) {
|
||||
npc.relations = {}
|
||||
needsSave = true
|
||||
}
|
||||
if (!npc.allies) {
|
||||
npc.allies = []
|
||||
needsSave = true
|
||||
}
|
||||
if (!npc.enemies) {
|
||||
npc.enemies = []
|
||||
needsSave = true
|
||||
}
|
||||
|
||||
// 如果NPC与玩家没有建立关系,自动建立中立关系
|
||||
if (playerId && !npc.relations[playerId]) {
|
||||
npc.relations[playerId] = {
|
||||
fromId: npc.id,
|
||||
toId: playerId,
|
||||
reputation: 0,
|
||||
status: 'neutral' as const,
|
||||
lastUpdated: now,
|
||||
history: []
|
||||
}
|
||||
needsSave = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化玩家积分(如果不存在)
|
||||
if (oldData.player && oldData.player.points === undefined) {
|
||||
// 积分会在游戏启动时通过 initGame 计算,这里设置为0
|
||||
oldData.player.points = 0
|
||||
needsSave = true
|
||||
}
|
||||
|
||||
// 迁移 player.diplomaticRelations 到 npc.relations
|
||||
// 旧版本使用 player.diplomaticRelations[npcId] 存储玩家对NPC的关系
|
||||
// 新版本统一使用 npc.relations[playerId] 存储NPC对玩家的关系
|
||||
if (oldData.player?.diplomaticRelations && oldData.npcs && Array.isArray(oldData.npcs)) {
|
||||
const playerId = oldData.player.id
|
||||
const playerRelations = oldData.player.diplomaticRelations as Record<string, any>
|
||||
|
||||
Object.entries(playerRelations).forEach(([npcId, relation]) => {
|
||||
const npc = oldData.npcs.find((n: NPC) => n.id === npcId)
|
||||
if (npc) {
|
||||
if (!npc.relations) {
|
||||
npc.relations = {}
|
||||
}
|
||||
// 如果NPC对玩家的关系不存在,使用玩家对NPC的关系数据
|
||||
if (!npc.relations[playerId]) {
|
||||
npc.relations[playerId] = {
|
||||
...relation,
|
||||
fromId: npcId,
|
||||
toId: playerId
|
||||
}
|
||||
needsSave = true
|
||||
} else {
|
||||
// 如果两边都有数据,使用声望值更极端的那个(偏离0更远的)
|
||||
const existingReputation = npc.relations[playerId].reputation || 0
|
||||
const playerReputation = relation.reputation || 0
|
||||
if (Math.abs(playerReputation) > Math.abs(existingReputation)) {
|
||||
npc.relations[playerId].reputation = playerReputation
|
||||
npc.relations[playerId].status = relation.status
|
||||
needsSave = true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 删除旧的 diplomaticRelations 字段
|
||||
delete oldData.player.diplomaticRelations
|
||||
needsSave = true
|
||||
console.log('[Migration] Migrated player.diplomaticRelations to npc.relations')
|
||||
}
|
||||
|
||||
// 检查是否需要迁移地图数据
|
||||
const hasOldMapData = oldData.universePlanets || oldData.debrisFields
|
||||
|
||||
if (hasOldMapData) {
|
||||
// 准备 universeStore 数据
|
||||
const universeData: {
|
||||
planets: Record<string, Planet>
|
||||
@@ -58,59 +164,25 @@ export const migrateGameData = (): void => {
|
||||
}
|
||||
})
|
||||
delete oldData.universePlanets
|
||||
needsSave = true
|
||||
}
|
||||
|
||||
// 迁移残骸场数据
|
||||
if (oldData.debrisFields) {
|
||||
universeData.debrisFields = oldData.debrisFields
|
||||
delete oldData.debrisFields
|
||||
needsSave = true
|
||||
}
|
||||
|
||||
// 修复NPC数据(确保所有必需字段都存在)
|
||||
if (oldData.npcs && Array.isArray(oldData.npcs)) {
|
||||
const now = Date.now()
|
||||
oldData.npcs.forEach((npc: NPC) => {
|
||||
// 确保NPC有必需的时间字段,并设置随机冷却避免同时行动
|
||||
if (npc.lastSpyTime === undefined || npc.lastSpyTime === 0) {
|
||||
// 0-4分钟的随机延迟
|
||||
const randomSpyOffset = Math.random() * 240 * 1000
|
||||
npc.lastSpyTime = now - randomSpyOffset
|
||||
}
|
||||
if (npc.lastAttackTime === undefined || npc.lastAttackTime === 0) {
|
||||
// 0-8分钟的随机延迟
|
||||
const randomAttackOffset = Math.random() * 480 * 1000
|
||||
npc.lastAttackTime = now - randomAttackOffset
|
||||
}
|
||||
// 确保NPC有必需的数组字段
|
||||
if (!npc.fleetMissions) {
|
||||
npc.fleetMissions = []
|
||||
}
|
||||
if (!npc.playerSpyReports) {
|
||||
npc.playerSpyReports = {}
|
||||
}
|
||||
if (!npc.relations) {
|
||||
npc.relations = {}
|
||||
}
|
||||
if (!npc.allies) {
|
||||
npc.allies = []
|
||||
}
|
||||
if (!npc.enemies) {
|
||||
npc.enemies = []
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化玩家积分(如果不存在)
|
||||
if (oldData.player && oldData.player.points === undefined) {
|
||||
// 积分会在游戏启动时通过 initGame 计算,这里设置为0
|
||||
oldData.player.points = 0
|
||||
}
|
||||
|
||||
// 保存迁移后的数据
|
||||
// 保存universeStore数据
|
||||
localStorage.setItem(universeStorageKey, encryptData(universeData))
|
||||
localStorage.setItem(storageKey, encryptData(oldData))
|
||||
}
|
||||
|
||||
// 如果有任何数据被修改,保存gameStore数据
|
||||
if (needsSave) {
|
||||
localStorage.setItem(storageKey, encryptData(oldData))
|
||||
console.log('[Migration] Game data migrated successfully')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Migration] Failed to migrate game data:', error)
|
||||
}
|
||||
|
||||
@@ -1,111 +1,183 @@
|
||||
<template>
|
||||
<div class="container mx-auto p-4 sm:p-6 space-y-6">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="text-2xl sm:text-3xl font-bold">{{ t('diplomacy.title') }}</h1>
|
||||
<p class="text-sm text-muted-foreground mt-1">{{ t('diplomacy.description') }}</p>
|
||||
</div>
|
||||
<!-- 视图切换和诊断按钮 -->
|
||||
<div class="flex items-center gap-2 flex-shrink-0">
|
||||
<!-- 视图模式切换 -->
|
||||
<div class="flex items-center border rounded-md">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-8 px-2 rounded-r-none"
|
||||
:class="{ 'bg-accent': viewMode === 'list' }"
|
||||
@click="viewMode = 'list'"
|
||||
:title="t('diplomacy.viewMode.list')"
|
||||
>
|
||||
<List class="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-8 px-2 rounded-l-none border-l"
|
||||
:class="{ 'bg-accent': viewMode === 'card' }"
|
||||
@click="viewMode = 'card'"
|
||||
:title="t('diplomacy.viewMode.card')"
|
||||
>
|
||||
<LayoutGrid class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<!-- NPC诊断按钮 -->
|
||||
<Button @click="showNPCDiagnostic" variant="outline" size="sm">
|
||||
<Search class="mr-2 h-4 w-4" />
|
||||
NPC状态诊断
|
||||
<Activity class="h-4 w-4 sm:mr-2" />
|
||||
<span class="hidden sm:inline">{{ t('diplomacy.diagnostic.button') }}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- NPC诊断对话框 -->
|
||||
<Dialog v-model:open="npcDiagnosticOpen">
|
||||
<ScrollableDialogContent container-class="max-w-4xl">
|
||||
<template #header>
|
||||
<DialogTitle>NPC状态诊断报告</DialogTitle>
|
||||
<DialogTitle>{{ t('diplomacy.diagnostic.title') }}</DialogTitle>
|
||||
<DialogDescription>
|
||||
<div class="text-sm mt-2">
|
||||
玩家积分: {{ gameStore.player.points || 0 }} | 侦查间隔: {{ Math.floor(behaviorConfig.spyInterval / 60) }}分钟 | 攻击间隔:
|
||||
{{ Math.floor(behaviorConfig.attackInterval / 60) }}分钟 | 攻击概率:
|
||||
{{ (behaviorConfig.attackProbability * 100).toFixed(0) }}%
|
||||
{{
|
||||
t('diplomacy.diagnostic.description', {
|
||||
points: gameStore.player.points || 0,
|
||||
spyInterval: Math.floor(behaviorConfig.spyInterval / 60),
|
||||
attackInterval: Math.floor(behaviorConfig.attackInterval / 60),
|
||||
attackProb: (behaviorConfig.attackProbability * 100).toFixed(0)
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
</DialogDescription>
|
||||
</template>
|
||||
|
||||
<div v-if="npcDiagnostics.length === 0" class="text-center py-8 text-muted-foreground">暂无NPC数据</div>
|
||||
<!-- 诊断对话框搜索框 -->
|
||||
<div class="relative mb-4">
|
||||
<Search class="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input v-model="diagnosticSearchQuery" type="text" :placeholder="t('diplomacy.searchPlaceholder')" class="pl-10" />
|
||||
</div>
|
||||
|
||||
<div v-if="filteredDiagnostics.length === 0" class="text-center py-8 text-muted-foreground">
|
||||
{{ t('diplomacy.diagnostic.noData') }}
|
||||
</div>
|
||||
<div v-else class="space-y-4">
|
||||
<div v-for="diagnostic in npcDiagnostics" :key="diagnostic.npcId" class="border rounded-lg p-4">
|
||||
<div v-for="diagnostic in paginatedDiagnostics" :key="diagnostic.npcId" class="border rounded-lg p-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h3 class="font-bold text-lg">{{ diagnostic.npcName }}</h3>
|
||||
<Badge
|
||||
:variant="
|
||||
diagnostic.relationStatus === '友好' ? 'default' : diagnostic.relationStatus === '敌对' ? 'destructive' : 'secondary'
|
||||
"
|
||||
>
|
||||
{{ diagnostic.relationStatus }}
|
||||
<Badge :variant="getRelationBadgeVariant(diagnostic.relationStatusKey)">
|
||||
{{ getLocalizedRelationStatus(diagnostic.relationStatusKey) }}
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-2 text-sm mb-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-muted-foreground">难度:</span>
|
||||
<span class="font-medium">{{ diagnostic.difficulty }}</span>
|
||||
<span class="text-muted-foreground">{{ t('diplomacy.diagnostic.difficulty') }}:</span>
|
||||
<span class="font-medium">{{ t(`diplomacy.diagnostic.difficultyLevels.${diagnostic.difficulty}`) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-muted-foreground">好感度:</span>
|
||||
<span class="text-muted-foreground">{{ t('diplomacy.diagnostic.reputation') }}:</span>
|
||||
<span class="font-medium">{{ diagnostic.reputation }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-muted-foreground">侦查探测器:</span>
|
||||
<span class="text-muted-foreground">{{ t('diplomacy.diagnostic.spyProbes') }}:</span>
|
||||
<span class="font-medium">{{ diagnostic.spyProbes }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-muted-foreground">舰队战力:</span>
|
||||
<span class="text-muted-foreground">{{ t('diplomacy.diagnostic.fleetPower') }}:</span>
|
||||
<span class="font-medium">{{ diagnostic.totalFleetPower }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-muted-foreground">可以侦查:</span>
|
||||
<span class="text-muted-foreground">{{ t('diplomacy.diagnostic.canSpy') }}:</span>
|
||||
<span :class="diagnostic.canSpy ? 'text-green-600 font-semibold' : 'text-red-600 font-semibold'">
|
||||
{{ diagnostic.canSpy ? '是' : '否' }}
|
||||
{{ diagnostic.canSpy ? t('diplomacy.diagnostic.yes') : t('diplomacy.diagnostic.no') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-muted-foreground">可以攻击:</span>
|
||||
<span class="text-muted-foreground">{{ t('diplomacy.diagnostic.canAttack') }}:</span>
|
||||
<span :class="diagnostic.canAttack ? 'text-green-600 font-semibold' : 'text-red-600 font-semibold'">
|
||||
{{ diagnostic.canAttack ? '是' : '否' }}
|
||||
{{ diagnostic.canAttack ? t('diplomacy.diagnostic.yes') : t('diplomacy.diagnostic.no') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-muted-foreground">攻击概率:</span>
|
||||
<span class="text-muted-foreground">{{ t('diplomacy.diagnostic.attackProbability') }}:</span>
|
||||
<span class="font-medium">{{ (diagnostic.attackProbability * 100).toFixed(0) }}%</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-muted-foreground">下次侦查:</span>
|
||||
<span class="text-muted-foreground">{{ t('diplomacy.diagnostic.nextSpy') }}:</span>
|
||||
<span class="font-medium">
|
||||
<template v-if="diagnostic.nextSpyIn > 0">
|
||||
{{ Math.floor(diagnostic.nextSpyIn / 60) }}分{{ diagnostic.nextSpyIn % 60 }}秒
|
||||
{{
|
||||
t('diplomacy.diagnostic.timeFormat', { min: Math.floor(diagnostic.nextSpyIn / 60), sec: diagnostic.nextSpyIn % 60 })
|
||||
}}
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="text-green-600">随时</span>
|
||||
<span class="text-green-600">{{ t('diplomacy.diagnostic.anytime') }}</span>
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-span-2 flex items-center gap-2">
|
||||
<span class="text-muted-foreground">下次攻击:</span>
|
||||
<span class="text-muted-foreground">{{ t('diplomacy.diagnostic.nextAttack') }}:</span>
|
||||
<span class="font-medium">
|
||||
<template v-if="diagnostic.nextAttackIn > 0">
|
||||
{{ Math.floor(diagnostic.nextAttackIn / 60) }}分{{ diagnostic.nextAttackIn % 60 }}秒
|
||||
{{
|
||||
t('diplomacy.diagnostic.timeFormat', {
|
||||
min: Math.floor(diagnostic.nextAttackIn / 60),
|
||||
sec: diagnostic.nextAttackIn % 60
|
||||
})
|
||||
}}
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="text-green-600">随时</span>
|
||||
<span class="text-green-600">{{ t('diplomacy.diagnostic.anytime') }}</span>
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="diagnostic.reasons.length > 0" class="mt-3 p-3 bg-muted rounded text-xs">
|
||||
<div class="font-semibold mb-2">状态说明:</div>
|
||||
<div class="font-semibold mb-2">{{ t('diplomacy.diagnostic.statusExplanation') }}:</div>
|
||||
<ul class="list-disc list-inside space-y-1">
|
||||
<li v-for="(reason, idx) in diagnostic.reasons" :key="idx">{{ reason }}</li>
|
||||
<li v-for="(reason, idx) in diagnostic.reasons" :key="idx">{{ translateReason(reason) }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分页控制 -->
|
||||
<Pagination
|
||||
v-if="diagnosticTotalPages > 1"
|
||||
v-model:page="diagnosticPage"
|
||||
:total="filteredDiagnostics.length"
|
||||
:items-per-page="DIAGNOSTIC_ITEMS_PER_PAGE"
|
||||
:sibling-count="1"
|
||||
show-edges
|
||||
class="mt-6"
|
||||
>
|
||||
<PaginationContent>
|
||||
<PaginationPrevious>{{ t('pagination.previous') }}</PaginationPrevious>
|
||||
|
||||
<template v-for="(pageNum, index) in diagnosticPageNumbers" :key="index">
|
||||
<PaginationItem v-if="typeof pageNum === 'number'" :value="pageNum" :is-active="pageNum === diagnosticPage">
|
||||
{{ pageNum }}
|
||||
</PaginationItem>
|
||||
<span v-else class="px-2 text-muted-foreground">{{ pageNum }}</span>
|
||||
</template>
|
||||
|
||||
<PaginationNext>{{ t('pagination.next') }}</PaginationNext>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</div>
|
||||
</ScrollableDialogContent>
|
||||
</Dialog>
|
||||
|
||||
<!-- 搜索框 -->
|
||||
<div class="relative">
|
||||
<Search class="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input v-model="searchQuery" type="text" :placeholder="t('diplomacy.searchPlaceholder')" class="pl-10" />
|
||||
</div>
|
||||
|
||||
<!-- 关系状态过滤标签 -->
|
||||
<Tabs v-model="activeTab" class="w-full">
|
||||
<TabsList class="grid w-full grid-cols-4">
|
||||
@@ -149,169 +221,149 @@
|
||||
|
||||
<!-- 全部NPC -->
|
||||
<TabsContent value="all" class="space-y-4 mt-6">
|
||||
<div v-if="allNpcs.length === 0" class="text-center py-12 text-muted-foreground">
|
||||
{{ t('diplomacy.noNpcs') }}
|
||||
</div>
|
||||
<Empty v-if="allNpcs.length === 0" class="border rounded-lg">
|
||||
<EmptyContent>
|
||||
<Users class="h-10 w-10 text-muted-foreground" />
|
||||
<EmptyDescription>{{ t('diplomacy.noNpcs') }}</EmptyDescription>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
<template v-else>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- 卡片视图 -->
|
||||
<div v-if="viewMode === 'card'" class="grid grid-cols-1 md:grid-cols-2 gap-4 pb-20">
|
||||
<NpcRelationCard
|
||||
v-for="npc in paginatedAllNpcs"
|
||||
:key="npc.id"
|
||||
:ref="setCardRef(npc.id)"
|
||||
:npc="npc"
|
||||
:relation="getRelation(npc.id)"
|
||||
:data-npc-id="npc.id"
|
||||
:class="{ 'npc-highlight': highlightedNpcId === npc.id }"
|
||||
/>
|
||||
</div>
|
||||
<!-- 列表视图 -->
|
||||
<div v-else class="space-y-2 pb-20">
|
||||
<NpcRelationRow
|
||||
v-for="npc in paginatedAllNpcs"
|
||||
:key="npc.id"
|
||||
:ref="setCardRef(npc.id)"
|
||||
:npc="npc"
|
||||
:relation="getRelation(npc.id)"
|
||||
:class="{ 'npc-highlight': highlightedNpcId === npc.id }"
|
||||
/>
|
||||
</div>
|
||||
<Pagination
|
||||
v-if="totalPagesAll > 1"
|
||||
v-model:page="currentPage.all"
|
||||
:total="allNpcs.length"
|
||||
:items-per-page="ITEMS_PER_PAGE"
|
||||
:sibling-count="1"
|
||||
show-edges
|
||||
class="mt-6"
|
||||
>
|
||||
<PaginationContent>
|
||||
<PaginationPrevious>{{ t('pagination.previous') }}</PaginationPrevious>
|
||||
|
||||
<template v-for="(pageNum, index) in pageNumbersAll" :key="index">
|
||||
<PaginationItem v-if="typeof pageNum === 'number'" :value="pageNum" :is-active="pageNum === currentPage.all">
|
||||
{{ pageNum }}
|
||||
</PaginationItem>
|
||||
<span v-else class="px-2 text-muted-foreground">{{ pageNum }}</span>
|
||||
</template>
|
||||
|
||||
<PaginationNext>{{ t('pagination.next') }}</PaginationNext>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</template>
|
||||
</TabsContent>
|
||||
|
||||
<!-- 友好NPC -->
|
||||
<TabsContent value="friendly" class="space-y-4 mt-6">
|
||||
<div v-if="friendlyNpcs.length === 0" class="text-center py-12 text-muted-foreground">
|
||||
{{ t('diplomacy.noFriendlyNpcs') }}
|
||||
</div>
|
||||
<Empty v-if="friendlyNpcs.length === 0" class="border rounded-lg">
|
||||
<EmptyContent>
|
||||
<Heart class="h-10 w-10 text-muted-foreground" />
|
||||
<EmptyDescription>{{ t('diplomacy.noFriendlyNpcs') }}</EmptyDescription>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
<template v-else>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- 卡片视图 -->
|
||||
<div v-if="viewMode === 'card'" class="grid grid-cols-1 md:grid-cols-2 gap-4 pb-20">
|
||||
<NpcRelationCard
|
||||
v-for="npc in paginatedFriendlyNpcs"
|
||||
:key="npc.id"
|
||||
:ref="setCardRef(npc.id)"
|
||||
:npc="npc"
|
||||
:relation="getRelation(npc.id)"
|
||||
:data-npc-id="npc.id"
|
||||
:class="{ 'npc-highlight': highlightedNpcId === npc.id }"
|
||||
/>
|
||||
</div>
|
||||
<!-- 列表视图 -->
|
||||
<div v-else class="space-y-2 pb-20">
|
||||
<NpcRelationRow
|
||||
v-for="npc in paginatedFriendlyNpcs"
|
||||
:key="npc.id"
|
||||
:ref="setCardRef(npc.id)"
|
||||
:npc="npc"
|
||||
:relation="getRelation(npc.id)"
|
||||
:class="{ 'npc-highlight': highlightedNpcId === npc.id }"
|
||||
/>
|
||||
</div>
|
||||
<Pagination
|
||||
v-if="totalPagesFriendly > 1"
|
||||
v-model:page="currentPage.friendly"
|
||||
:total="friendlyNpcs.length"
|
||||
:items-per-page="ITEMS_PER_PAGE"
|
||||
:sibling-count="1"
|
||||
show-edges
|
||||
class="mt-6"
|
||||
>
|
||||
<PaginationContent>
|
||||
<PaginationPrevious>{{ t('pagination.previous') }}</PaginationPrevious>
|
||||
|
||||
<template v-for="(pageNum, index) in pageNumbersFriendly" :key="index">
|
||||
<PaginationItem v-if="typeof pageNum === 'number'" :value="pageNum" :is-active="pageNum === currentPage.friendly">
|
||||
{{ pageNum }}
|
||||
</PaginationItem>
|
||||
<span v-else class="px-2 text-muted-foreground">{{ pageNum }}</span>
|
||||
</template>
|
||||
|
||||
<PaginationNext>{{ t('pagination.next') }}</PaginationNext>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</template>
|
||||
</TabsContent>
|
||||
|
||||
<!-- 中立NPC -->
|
||||
<TabsContent value="neutral" class="space-y-4 mt-6">
|
||||
<div v-if="neutralNpcs.length === 0" class="text-center py-12 text-muted-foreground">
|
||||
{{ t('diplomacy.noNeutralNpcs') }}
|
||||
</div>
|
||||
<Empty v-if="neutralNpcs.length === 0" class="border rounded-lg">
|
||||
<EmptyContent>
|
||||
<Minus class="h-10 w-10 text-muted-foreground" />
|
||||
<EmptyDescription>{{ t('diplomacy.noNeutralNpcs') }}</EmptyDescription>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
<template v-else>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- 卡片视图 -->
|
||||
<div v-if="viewMode === 'card'" class="grid grid-cols-1 md:grid-cols-2 gap-4 pb-20">
|
||||
<NpcRelationCard
|
||||
v-for="npc in paginatedNeutralNpcs"
|
||||
:key="npc.id"
|
||||
:ref="setCardRef(npc.id)"
|
||||
:npc="npc"
|
||||
:relation="getRelation(npc.id)"
|
||||
:data-npc-id="npc.id"
|
||||
:class="{ 'npc-highlight': highlightedNpcId === npc.id }"
|
||||
/>
|
||||
</div>
|
||||
<!-- 列表视图 -->
|
||||
<div v-else class="space-y-2 pb-20">
|
||||
<NpcRelationRow
|
||||
v-for="npc in paginatedNeutralNpcs"
|
||||
:key="npc.id"
|
||||
:ref="setCardRef(npc.id)"
|
||||
:npc="npc"
|
||||
:relation="getRelation(npc.id)"
|
||||
:class="{ 'npc-highlight': highlightedNpcId === npc.id }"
|
||||
/>
|
||||
</div>
|
||||
<Pagination
|
||||
v-if="totalPagesNeutral > 1"
|
||||
v-model:page="currentPage.neutral"
|
||||
:total="neutralNpcs.length"
|
||||
:items-per-page="ITEMS_PER_PAGE"
|
||||
:sibling-count="1"
|
||||
show-edges
|
||||
class="mt-6"
|
||||
>
|
||||
<PaginationContent>
|
||||
<PaginationPrevious>{{ t('pagination.previous') }}</PaginationPrevious>
|
||||
|
||||
<template v-for="(pageNum, index) in pageNumbersNeutral" :key="index">
|
||||
<PaginationItem v-if="typeof pageNum === 'number'" :value="pageNum" :is-active="pageNum === currentPage.neutral">
|
||||
{{ pageNum }}
|
||||
</PaginationItem>
|
||||
<span v-else class="px-2 text-muted-foreground">{{ pageNum }}</span>
|
||||
</template>
|
||||
|
||||
<PaginationNext>{{ t('pagination.next') }}</PaginationNext>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</template>
|
||||
</TabsContent>
|
||||
|
||||
<!-- 敌对NPC -->
|
||||
<TabsContent value="hostile" class="space-y-4 mt-6">
|
||||
<div v-if="hostileNpcs.length === 0" class="text-center py-12 text-muted-foreground">
|
||||
{{ t('diplomacy.noHostileNpcs') }}
|
||||
</div>
|
||||
<Empty v-if="hostileNpcs.length === 0" class="border rounded-lg">
|
||||
<EmptyContent>
|
||||
<Swords class="h-10 w-10 text-muted-foreground" />
|
||||
<EmptyDescription>{{ t('diplomacy.noHostileNpcs') }}</EmptyDescription>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
<template v-else>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- 卡片视图 -->
|
||||
<div v-if="viewMode === 'card'" class="grid grid-cols-1 md:grid-cols-2 gap-4 pb-20">
|
||||
<NpcRelationCard
|
||||
v-for="npc in paginatedHostileNpcs"
|
||||
:key="npc.id"
|
||||
:ref="setCardRef(npc.id)"
|
||||
:npc="npc"
|
||||
:relation="getRelation(npc.id)"
|
||||
:data-npc-id="npc.id"
|
||||
:class="{ 'npc-highlight': highlightedNpcId === npc.id }"
|
||||
/>
|
||||
</div>
|
||||
<!-- 列表视图 -->
|
||||
<div v-else class="space-y-2 pb-20">
|
||||
<NpcRelationRow
|
||||
v-for="npc in paginatedHostileNpcs"
|
||||
:key="npc.id"
|
||||
:ref="setCardRef(npc.id)"
|
||||
:npc="npc"
|
||||
:relation="getRelation(npc.id)"
|
||||
:class="{ 'npc-highlight': highlightedNpcId === npc.id }"
|
||||
/>
|
||||
</div>
|
||||
<Pagination
|
||||
v-if="totalPagesHostile > 1"
|
||||
v-model:page="currentPage.hostile"
|
||||
:total="hostileNpcs.length"
|
||||
:items-per-page="ITEMS_PER_PAGE"
|
||||
:sibling-count="1"
|
||||
show-edges
|
||||
class="mt-6"
|
||||
>
|
||||
<PaginationContent>
|
||||
<PaginationPrevious>{{ t('pagination.previous') }}</PaginationPrevious>
|
||||
|
||||
<template v-for="(pageNum, index) in pageNumbersHostile" :key="index">
|
||||
<PaginationItem v-if="typeof pageNum === 'number'" :value="pageNum" :is-active="pageNum === currentPage.hostile">
|
||||
{{ pageNum }}
|
||||
</PaginationItem>
|
||||
<span v-else class="px-2 text-muted-foreground">{{ pageNum }}</span>
|
||||
</template>
|
||||
|
||||
<PaginationNext>{{ t('pagination.next') }}</PaginationNext>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</template>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<!-- 固定底部分页 -->
|
||||
<FixedPagination v-model:page="currentPageValue" :total-pages="currentTotalPages" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useNPCStore } from '@/stores/npcStore'
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
@@ -320,22 +372,57 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Dialog, DialogDescription, DialogTitle } from '@/components/ui/dialog'
|
||||
import ScrollableDialogContent from '@/components/ui/dialog/ScrollableDialogContent.vue'
|
||||
import { Pagination, PaginationContent, PaginationItem, PaginationNext, PaginationPrevious } from '@/components/ui/pagination'
|
||||
import {
|
||||
FixedPagination,
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationItem,
|
||||
PaginationNext,
|
||||
PaginationPrevious
|
||||
} from '@/components/ui/pagination'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import NpcRelationCard from '@/components/NpcRelationCard.vue'
|
||||
import NpcRelationRow from '@/components/NpcRelationRow.vue'
|
||||
import { RelationStatus } from '@/types/game'
|
||||
import type { DiplomaticRelation } from '@/types/game'
|
||||
import * as npcBehaviorLogic from '@/logic/npcBehaviorLogic'
|
||||
import { Search } from 'lucide-vue-next'
|
||||
import { Search, Users, Heart, Minus, Swords, Activity, LayoutGrid, List } from 'lucide-vue-next'
|
||||
import { Empty, EmptyContent, EmptyDescription } from '@/components/ui/empty'
|
||||
|
||||
const route = useRoute()
|
||||
const gameStore = useGameStore()
|
||||
const npcStore = useNPCStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
const activeTab = ref('all')
|
||||
|
||||
// 视图模式: 'card' | 'list'
|
||||
const viewMode = ref<'card' | 'list'>('list')
|
||||
|
||||
// NPC卡片引用 Map(存储组件实例)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const cardRefs = ref<Map<string, any>>(new Map())
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const setCardRef = (npcId: string) => (el: any) => {
|
||||
if (el) {
|
||||
cardRefs.value.set(npcId, el)
|
||||
} else {
|
||||
cardRefs.value.delete(npcId)
|
||||
}
|
||||
}
|
||||
|
||||
// 高亮状态
|
||||
const highlightedNpcId = ref<string | null>(null)
|
||||
|
||||
// 搜索功能
|
||||
const searchQuery = ref('')
|
||||
|
||||
// NPC诊断功能
|
||||
const npcDiagnosticOpen = ref(false)
|
||||
const npcDiagnostics = ref<npcBehaviorLogic.NPCDiagnosticInfo[]>([])
|
||||
const diagnosticPage = ref(1)
|
||||
const diagnosticSearchQuery = ref('')
|
||||
const DIAGNOSTIC_ITEMS_PER_PAGE = 10
|
||||
const behaviorConfig = computed(() => {
|
||||
return npcBehaviorLogic.calculateDynamicBehavior(gameStore.player.points || 0)
|
||||
})
|
||||
@@ -343,9 +430,64 @@
|
||||
const showNPCDiagnostic = () => {
|
||||
const currentTime = Date.now()
|
||||
npcDiagnostics.value = npcBehaviorLogic.diagnoseNPCBehavior(npcStore.npcs, gameStore.player, currentTime)
|
||||
diagnosticPage.value = 1 // 重置分页
|
||||
diagnosticSearchQuery.value = '' // 重置搜索
|
||||
npcDiagnosticOpen.value = true
|
||||
}
|
||||
|
||||
// 诊断搜索过滤
|
||||
const filteredDiagnostics = computed(() => {
|
||||
if (!diagnosticSearchQuery.value.trim()) return npcDiagnostics.value
|
||||
const query = diagnosticSearchQuery.value.toLowerCase().trim()
|
||||
return npcDiagnostics.value.filter(d => d.npcName.toLowerCase().includes(query) || d.npcId.toLowerCase().includes(query))
|
||||
})
|
||||
|
||||
// 诊断弹窗分页
|
||||
const diagnosticTotalPages = computed(() => Math.ceil(filteredDiagnostics.value.length / DIAGNOSTIC_ITEMS_PER_PAGE))
|
||||
const paginatedDiagnostics = computed(() => {
|
||||
const start = (diagnosticPage.value - 1) * DIAGNOSTIC_ITEMS_PER_PAGE
|
||||
const end = start + DIAGNOSTIC_ITEMS_PER_PAGE
|
||||
return filteredDiagnostics.value.slice(start, end)
|
||||
})
|
||||
|
||||
const diagnosticPageNumbers = computed(() => getPageNumbers(diagnosticPage.value, diagnosticTotalPages.value))
|
||||
|
||||
// 诊断面板关系状态本地化
|
||||
const getLocalizedRelationStatus = (statusKey: string) => {
|
||||
switch (statusKey) {
|
||||
case 'friendly':
|
||||
return t('diplomacy.status.friendly')
|
||||
case 'hostile':
|
||||
return t('diplomacy.status.hostile')
|
||||
case 'neutral':
|
||||
return t('diplomacy.status.neutral')
|
||||
case 'noRelation':
|
||||
return t('diplomacy.diagnostic.noRelation')
|
||||
case 'noRelationNeutral':
|
||||
return t('diplomacy.diagnostic.noRelationNeutral')
|
||||
default:
|
||||
return t('diplomacy.status.neutral')
|
||||
}
|
||||
}
|
||||
|
||||
// 诊断面板关系Badge样式
|
||||
const getRelationBadgeVariant = (statusKey: string) => {
|
||||
switch (statusKey) {
|
||||
case 'friendly':
|
||||
return 'default'
|
||||
case 'hostile':
|
||||
return 'destructive'
|
||||
default:
|
||||
return 'secondary'
|
||||
}
|
||||
}
|
||||
|
||||
// 翻译诊断原因
|
||||
const translateReason = (reason: { key: string; params?: Record<string, string | number> }) => {
|
||||
const translationKey = `diplomacy.diagnostic.reasons.${reason.key}`
|
||||
return reason.params ? t(translationKey, reason.params) : t(translationKey)
|
||||
}
|
||||
|
||||
// 检测并生成NPC盟友
|
||||
const initializeNPCAllies = () => {
|
||||
const npcs = npcStore.npcs
|
||||
@@ -387,15 +529,8 @@
|
||||
})
|
||||
}
|
||||
|
||||
// 组件挂载时初始化NPC盟友
|
||||
onMounted(() => {
|
||||
initializeNPCAllies()
|
||||
|
||||
// 监听滚动到NPC卡片的事件
|
||||
const handleScrollToNpc = (event: Event) => {
|
||||
const customEvent = event as CustomEvent<{ npcId: string }>
|
||||
const npcId = customEvent.detail.npcId
|
||||
|
||||
// 滚动到指定NPC卡片
|
||||
const scrollToNpcCard = (npcId: string) => {
|
||||
// 切换到"全部"标签
|
||||
activeTab.value = 'all'
|
||||
|
||||
@@ -411,22 +546,38 @@
|
||||
|
||||
// 再次等待分页更新后滚动到卡片
|
||||
nextTick(() => {
|
||||
// 使用data属性来标识卡片
|
||||
const cards = document.querySelectorAll('[data-npc-id]')
|
||||
const targetCard = Array.from(cards).find(card => card.getAttribute('data-npc-id') === npcId)
|
||||
// 从 cardRefs 获取组件实例
|
||||
const cardComponent = cardRefs.value.get(npcId)
|
||||
const targetEl = cardComponent?.$el as HTMLElement | undefined
|
||||
|
||||
if (targetCard) {
|
||||
targetCard.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
if (targetEl) {
|
||||
targetEl.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
// 添加高亮效果
|
||||
targetCard.classList.add('ring-2', 'ring-primary', 'ring-offset-2')
|
||||
highlightedNpcId.value = npcId
|
||||
setTimeout(() => {
|
||||
targetCard.classList.remove('ring-2', 'ring-primary', 'ring-offset-2')
|
||||
}, 2000)
|
||||
highlightedNpcId.value = null
|
||||
}, 3000)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 组件挂载时初始化NPC盟友
|
||||
onMounted(() => {
|
||||
initializeNPCAllies()
|
||||
|
||||
// 检查URL query参数,如果有npcId则滚动到该NPC
|
||||
const npcIdFromQuery = route.query.npcId as string | undefined
|
||||
if (npcIdFromQuery) {
|
||||
scrollToNpcCard(npcIdFromQuery)
|
||||
}
|
||||
|
||||
// 监听滚动到NPC卡片的事件
|
||||
const handleScrollToNpc = (event: Event) => {
|
||||
const customEvent = event as CustomEvent<{ npcId: string }>
|
||||
scrollToNpcCard(customEvent.detail.npcId)
|
||||
}
|
||||
|
||||
document.addEventListener('scrollToNpc', handleScrollToNpc)
|
||||
|
||||
// 清理事件监听器
|
||||
@@ -444,16 +595,25 @@
|
||||
hostile: 1
|
||||
})
|
||||
|
||||
// 获取玩家对NPC的关系
|
||||
// 获取NPC对玩家的关系(统一使用 npc.relations)
|
||||
const getRelation = (npcId: string): DiplomaticRelation | undefined => {
|
||||
return gameStore.player.diplomaticRelations?.[npcId]
|
||||
const npc = npcStore.npcs.find(n => n.id === npcId)
|
||||
return npc?.relations?.[gameStore.player.id]
|
||||
}
|
||||
|
||||
// 按关系状态分类NPC
|
||||
const allNpcs = computed(() => npcStore.npcs)
|
||||
// 搜索过滤函数
|
||||
const matchesSearch = (npc: (typeof npcStore.npcs)[0]) => {
|
||||
if (!searchQuery.value.trim()) return true
|
||||
const query = searchQuery.value.toLowerCase().trim()
|
||||
return npc.name.toLowerCase().includes(query) || npc.id.toLowerCase().includes(query)
|
||||
}
|
||||
|
||||
// 按关系状态分类NPC(同时应用搜索过滤)
|
||||
const allNpcs = computed(() => npcStore.npcs.filter(matchesSearch))
|
||||
|
||||
const friendlyNpcs = computed(() => {
|
||||
return npcStore.npcs.filter(npc => {
|
||||
if (!matchesSearch(npc)) return false
|
||||
const relation = getRelation(npc.id)
|
||||
return relation?.status === RelationStatus.Friendly
|
||||
})
|
||||
@@ -461,6 +621,7 @@
|
||||
|
||||
const neutralNpcs = computed(() => {
|
||||
return npcStore.npcs.filter(npc => {
|
||||
if (!matchesSearch(npc)) return false
|
||||
const relation = getRelation(npc.id)
|
||||
return !relation || relation.status === RelationStatus.Neutral
|
||||
})
|
||||
@@ -468,6 +629,7 @@
|
||||
|
||||
const hostileNpcs = computed(() => {
|
||||
return npcStore.npcs.filter(npc => {
|
||||
if (!matchesSearch(npc)) return false
|
||||
const relation = getRelation(npc.id)
|
||||
return relation?.status === RelationStatus.Hostile
|
||||
})
|
||||
@@ -497,46 +659,73 @@
|
||||
const totalPagesNeutral = computed(() => getTotalPages(neutralNpcs.value))
|
||||
const totalPagesHostile = computed(() => getTotalPages(hostileNpcs.value))
|
||||
|
||||
// 生成页码列表(用于分页UI)
|
||||
// 生成页码列表(用于诊断弹窗分页UI)
|
||||
const getPageNumbers = (currentPageNum: number, totalPages: number) => {
|
||||
const pages: (number | string)[] = []
|
||||
const maxVisible = 5 // 最多显示5个页码
|
||||
const pages: number[] = []
|
||||
const maxVisible = 3
|
||||
|
||||
if (totalPages <= maxVisible) {
|
||||
// 如果总页数少于等于5,显示全部
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
pages.push(i)
|
||||
}
|
||||
} else {
|
||||
// 总是显示第1页
|
||||
pages.push(1)
|
||||
let start = currentPageNum - 1
|
||||
let end = currentPageNum + 1
|
||||
|
||||
if (currentPageNum > 3) {
|
||||
pages.push('...')
|
||||
if (start < 1) {
|
||||
start = 1
|
||||
end = maxVisible
|
||||
}
|
||||
if (end > totalPages) {
|
||||
end = totalPages
|
||||
start = totalPages - maxVisible + 1
|
||||
}
|
||||
|
||||
// 计算中间显示的页码范围
|
||||
const start = Math.max(2, currentPageNum - 1)
|
||||
const end = Math.min(totalPages - 1, currentPageNum + 1)
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
pages.push(i)
|
||||
}
|
||||
|
||||
if (currentPageNum < totalPages - 2) {
|
||||
pages.push('...')
|
||||
}
|
||||
|
||||
// 总是显示最后一页
|
||||
pages.push(totalPages)
|
||||
}
|
||||
|
||||
return pages
|
||||
}
|
||||
|
||||
// 各标签页的页码列表
|
||||
const pageNumbersAll = computed(() => getPageNumbers(currentPage.value.all || 1, totalPagesAll.value))
|
||||
const pageNumbersFriendly = computed(() => getPageNumbers(currentPage.value.friendly || 1, totalPagesFriendly.value))
|
||||
const pageNumbersNeutral = computed(() => getPageNumbers(currentPage.value.neutral || 1, totalPagesNeutral.value))
|
||||
const pageNumbersHostile = computed(() => getPageNumbers(currentPage.value.hostile || 1, totalPagesHostile.value))
|
||||
// 当前标签页的分页数据(用于固定底部分页)
|
||||
const currentTotalPages = computed(() => {
|
||||
switch (activeTab.value) {
|
||||
case 'friendly':
|
||||
return totalPagesFriendly.value
|
||||
case 'neutral':
|
||||
return totalPagesNeutral.value
|
||||
case 'hostile':
|
||||
return totalPagesHostile.value
|
||||
default:
|
||||
return totalPagesAll.value
|
||||
}
|
||||
})
|
||||
|
||||
const currentPageValue = computed({
|
||||
get: () => currentPage.value[activeTab.value] || 1,
|
||||
set: (val: number) => {
|
||||
currentPage.value[activeTab.value] = val
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* NPC卡片高亮动画 - 不使用scoped以便动态添加类生效 */
|
||||
.npc-highlight {
|
||||
animation: highlight-pulse 1.5s ease-in-out 2 !important;
|
||||
box-shadow: 0 0 0 3px var(--primary) !important;
|
||||
border-radius: 0.5rem !important;
|
||||
}
|
||||
|
||||
@keyframes highlight-pulse {
|
||||
0%,
|
||||
100% {
|
||||
box-shadow: 0 0 0 3px var(--primary);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 0 6px var(--primary), 0 0 25px var(--primary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,11 +8,9 @@
|
||||
<!-- 标签切换 -->
|
||||
<Tabs v-model="activeTab" class="w-full">
|
||||
<TabsList class="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="fleet">{{ t('fleetView.fleetOverview') }}</TabsTrigger>
|
||||
<TabsTrigger value="send">{{ t('fleetView.sendFleet') }}</TabsTrigger>
|
||||
<TabsTrigger value="missions">
|
||||
{{ t('fleetView.flightMissions') }}
|
||||
<Badge v-if="gameStore.player.fleetMissions.length > 0" variant="destructive" class="ml-1">
|
||||
<TabsTrigger v-for="tab in fleetTabs" :key="tab.value" :value="tab.value">
|
||||
{{ t(`fleetView.${tab.labelKey}`) }}
|
||||
<Badge v-if="tab.value === 'missions' && gameStore.player.fleetMissions.length > 0" variant="destructive" class="ml-1">
|
||||
{{ gameStore.player.fleetMissions.length }}
|
||||
</Badge>
|
||||
</TabsTrigger>
|
||||
@@ -97,17 +95,9 @@
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-3 gap-2 sm:gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="galaxy" class="text-xs sm:text-sm">{{ t('fleetView.galaxy') }}</Label>
|
||||
<Input id="galaxy" v-model.number="targetPosition.galaxy" type="number" min="1" max="9" placeholder="1" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="system" class="text-xs sm:text-sm">{{ t('fleetView.system') }}</Label>
|
||||
<Input id="system" v-model.number="targetPosition.system" type="number" min="1" max="10" placeholder="1" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="position" class="text-xs sm:text-sm">{{ t('fleetView.position') }}</Label>
|
||||
<Input id="position" v-model.number="targetPosition.position" type="number" min="1" max="10" placeholder="1" />
|
||||
<div v-for="coord in coordinateFields" :key="coord.key" class="space-y-2">
|
||||
<Label :for="coord.key" class="text-xs sm:text-sm">{{ t(`fleetView.${coord.key}`) }}</Label>
|
||||
<Input :id="coord.key" v-model.number="targetPosition[coord.key]" type="number" :min="1" :max="coord.max" placeholder="1" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -157,52 +147,17 @@
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="cargo-metal" class="text-xs sm:text-sm flex items-center gap-2">
|
||||
<ResourceIcon type="metal" size="sm" />
|
||||
{{ t('resources.metal') }} ({{ t('fleetView.available') }}: {{ formatNumber(planet.resources.metal) }})
|
||||
</Label>
|
||||
<Input id="cargo-metal" v-model.number="cargo.metal" type="number" min="0" :max="planet.resources.metal" placeholder="0" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="cargo-crystal" class="text-xs sm:text-sm flex items-center gap-2">
|
||||
<ResourceIcon type="crystal" size="sm" />
|
||||
{{ t('resources.crystal') }} ({{ t('fleetView.available') }}: {{ formatNumber(planet.resources.crystal) }})
|
||||
<div v-for="res in cargoResourceFields" :key="res.key" class="space-y-2">
|
||||
<Label :for="`cargo-${res.key}`" class="text-xs sm:text-sm flex items-center gap-2">
|
||||
<ResourceIcon :type="res.key" size="sm" />
|
||||
{{ t(`resources.${res.key}`) }} ({{ t('fleetView.available') }}: {{ formatNumber(planet.resources[res.key]) }})
|
||||
</Label>
|
||||
<Input
|
||||
id="cargo-crystal"
|
||||
v-model.number="cargo.crystal"
|
||||
:id="`cargo-${res.key}`"
|
||||
v-model.number="cargo[res.key]"
|
||||
type="number"
|
||||
min="0"
|
||||
:max="planet.resources.crystal"
|
||||
placeholder="0"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="cargo-deuterium" class="text-xs sm:text-sm flex items-center gap-2">
|
||||
<ResourceIcon type="deuterium" size="sm" />
|
||||
{{ t('resources.deuterium') }} ({{ t('fleetView.available') }}: {{ formatNumber(planet.resources.deuterium) }})
|
||||
</Label>
|
||||
<Input
|
||||
id="cargo-deuterium"
|
||||
v-model.number="cargo.deuterium"
|
||||
type="number"
|
||||
min="0"
|
||||
:max="planet.resources.deuterium"
|
||||
placeholder="0"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="cargo-darkMatter" class="text-xs sm:text-sm flex items-center gap-2">
|
||||
<ResourceIcon type="darkMatter" size="sm" />
|
||||
{{ t('resources.darkMatter') }} ({{ t('fleetView.available') }}: {{ formatNumber(planet.resources.darkMatter) }})
|
||||
</Label>
|
||||
<Input
|
||||
id="cargo-darkMatter"
|
||||
v-model.number="cargo.darkMatter"
|
||||
type="number"
|
||||
min="0"
|
||||
:max="planet.resources.darkMatter"
|
||||
:max="planet.resources[res.key]"
|
||||
placeholder="0"
|
||||
/>
|
||||
</div>
|
||||
@@ -243,9 +198,12 @@
|
||||
|
||||
<!-- 飞行任务 -->
|
||||
<TabsContent value="missions" class="mt-4 space-y-4">
|
||||
<Card v-if="gameStore.player.fleetMissions.length === 0">
|
||||
<CardContent class="py-8 text-center text-muted-foreground">{{ t('fleetView.noFlightMissions') }}</CardContent>
|
||||
</Card>
|
||||
<Empty v-if="gameStore.player.fleetMissions.length === 0" class="border rounded-lg">
|
||||
<EmptyContent>
|
||||
<RocketIcon class="h-10 w-10 text-muted-foreground" />
|
||||
<EmptyDescription>{{ t('fleetView.noFlightMissions') }}</EmptyDescription>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
|
||||
<Card v-for="mission in gameStore.player.fleetMissions" :key="mission.id">
|
||||
<CardHeader>
|
||||
@@ -275,25 +233,15 @@
|
||||
</div>
|
||||
|
||||
<!-- 携带资源 -->
|
||||
<div v-if="mission.cargo.metal > 0 || mission.cargo.crystal > 0 || mission.cargo.deuterium > 0 || mission.cargo.darkMatter > 0">
|
||||
<div v-if="hasCargoResources(mission.cargo)">
|
||||
<p class="text-xs sm:text-sm font-medium mb-2">{{ t('fleetView.carryingResources') }}:</p>
|
||||
<div class="flex flex-wrap gap-2 text-xs">
|
||||
<span v-if="mission.cargo.metal > 0" class="flex items-center gap-1">
|
||||
<ResourceIcon type="metal" size="sm" />
|
||||
{{ formatNumber(mission.cargo.metal) }}
|
||||
</span>
|
||||
<span v-if="mission.cargo.crystal > 0" class="flex items-center gap-1">
|
||||
<ResourceIcon type="crystal" size="sm" />
|
||||
{{ formatNumber(mission.cargo.crystal) }}
|
||||
</span>
|
||||
<span v-if="mission.cargo.deuterium > 0" class="flex items-center gap-1">
|
||||
<ResourceIcon type="deuterium" size="sm" />
|
||||
{{ formatNumber(mission.cargo.deuterium) }}
|
||||
</span>
|
||||
<span v-if="mission.cargo.darkMatter > 0" class="flex items-center gap-1">
|
||||
<ResourceIcon type="darkMatter" size="sm" />
|
||||
{{ formatNumber(mission.cargo.darkMatter) }}
|
||||
<template v-for="res in cargoResourceFields" :key="res.key">
|
||||
<span v-if="mission.cargo[res.key] > 0" class="flex items-center gap-1">
|
||||
<ResourceIcon :type="res.key" size="sm" />
|
||||
{{ formatNumber(mission.cargo[res.key]) }}
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -357,7 +305,7 @@
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
import { useGameConfig } from '@/composables/useGameConfig'
|
||||
import { computed, ref, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ShipType, MissionType, BuildingType, TechnologyType } from '@/types/game'
|
||||
import type { Fleet, Resources } from '@/types/game'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
@@ -380,6 +328,7 @@
|
||||
AlertDialogTitle
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import UnlockRequirement from '@/components/UnlockRequirement.vue'
|
||||
import { Empty, EmptyContent, EmptyDescription } from '@/components/ui/empty'
|
||||
import { Sword, Package, Rocket as RocketIcon, Eye, Users, Recycle, Skull, Gift, Compass } from 'lucide-vue-next'
|
||||
import { formatNumber, formatTime } from '@/utils/format'
|
||||
import * as shipValidation from '@/logic/shipValidation'
|
||||
@@ -390,7 +339,6 @@
|
||||
import * as diplomaticLogic from '@/logic/diplomaticLogic'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const gameStore = useGameStore()
|
||||
const universeStore = useUniverseStore()
|
||||
const npcStore = useNPCStore()
|
||||
@@ -417,6 +365,13 @@
|
||||
|
||||
const activeTab = ref<'fleet' | 'send' | 'missions'>('fleet')
|
||||
|
||||
// Tab 配置
|
||||
const fleetTabs = [
|
||||
{ value: 'fleet', labelKey: 'fleetOverview' },
|
||||
{ value: 'send', labelKey: 'sendFleet' },
|
||||
{ value: 'missions', labelKey: 'flightMissions' }
|
||||
] as const
|
||||
|
||||
// 选择的舰队
|
||||
const selectedFleet = ref<Partial<Fleet>>({
|
||||
[ShipType.LightFighter]: 0,
|
||||
@@ -435,12 +390,23 @@
|
||||
// 目标坐标
|
||||
const targetPosition = ref({ galaxy: 1, system: 1, position: 1 })
|
||||
|
||||
// 坐标字段配置
|
||||
const coordinateFields: { key: keyof typeof targetPosition.value; max: number }[] = [
|
||||
{ key: 'galaxy', max: 9 },
|
||||
{ key: 'system', max: 10 },
|
||||
{ key: 'position', max: 10 }
|
||||
]
|
||||
|
||||
// 选择的任务类型
|
||||
const selectedMission = ref<MissionType>(MissionType.Attack)
|
||||
|
||||
// 运输资源
|
||||
const cargo = ref({ metal: 0, crystal: 0, deuterium: 0, darkMatter: 0, energy: 0 })
|
||||
|
||||
// 货物资源字段配置
|
||||
type CargoKey = 'metal' | 'crystal' | 'deuterium' | 'darkMatter'
|
||||
const cargoResourceFields: { key: CargoKey }[] = [{ key: 'metal' }, { key: 'crystal' }, { key: 'deuterium' }, { key: 'darkMatter' }]
|
||||
|
||||
// 从 URL query 参数初始化
|
||||
onMounted(() => {
|
||||
// 启动定时器更新当前时间
|
||||
@@ -472,9 +438,6 @@
|
||||
|
||||
// 自动切换到派遣舰队标签
|
||||
activeTab.value = 'send'
|
||||
|
||||
// 清除 URL 参数,保持 URL 整洁
|
||||
router.replace({ path: '/fleet' })
|
||||
}
|
||||
})
|
||||
|
||||
@@ -553,6 +516,11 @@
|
||||
return cargo.value.metal + cargo.value.crystal + cargo.value.deuterium + cargo.value.darkMatter
|
||||
}
|
||||
|
||||
// 检查是否有携带资源
|
||||
const hasCargoResources = (cargoData: Resources): boolean => {
|
||||
return cargoData.metal > 0 || cargoData.crystal > 0 || cargoData.deuterium > 0 || cargoData.darkMatter > 0
|
||||
}
|
||||
|
||||
// 计算燃料消耗(包含货物重量影响)
|
||||
const getFuelConsumption = (): number => {
|
||||
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, Date.now())
|
||||
|
||||
@@ -77,110 +77,36 @@
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<!-- 建筑 -->
|
||||
<TabsContent value="buildings" class="space-y-4">
|
||||
<!-- 建筑/科技/舰船/防御/军官 - 统一配置渲染 -->
|
||||
<TabsContent v-for="section in gmSections" :key="section.tabValue" :value="section.tabValue" class="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('gmView.modifyBuildings') }}</CardTitle>
|
||||
<CardDescription>{{ t('gmView.buildingsDesc') }}</CardDescription>
|
||||
<CardTitle>{{ t(section.titleKey) }}</CardTitle>
|
||||
<CardDescription>{{ t(section.descKey) }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div v-for="building in buildingTypes" :key="building" class="space-y-2">
|
||||
<Label>{{ BUILDINGS[building].name }}</Label>
|
||||
<div v-for="item in section.items" :key="item" class="space-y-2">
|
||||
<Label>{{ section.getItemName(item) }}</Label>
|
||||
<div class="flex gap-2">
|
||||
<Input v-model.number="selectedPlanet.buildings[building]" type="number" min="0" max="100" class="flex-1" />
|
||||
<Button @click="setBuildingLevel(building, 10)" variant="outline" size="sm">Lv 10</Button>
|
||||
<Button @click="setBuildingLevel(building, 30)" variant="outline" size="sm">Lv 30</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<!-- 科技 -->
|
||||
<TabsContent value="research" class="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('gmView.modifyResearch') }}</CardTitle>
|
||||
<CardDescription>{{ t('gmView.researchDesc') }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div v-for="tech in technologyTypes" :key="tech" class="space-y-2">
|
||||
<Label>{{ TECHNOLOGIES[tech].name }}</Label>
|
||||
<div class="flex gap-2">
|
||||
<Input v-model.number="gameStore.player.technologies[tech]" type="number" min="0" max="50" class="flex-1" />
|
||||
<Button @click="setTechnologyLevel(tech, 10)" variant="outline" size="sm">Lv 10</Button>
|
||||
<Button @click="setTechnologyLevel(tech, 20)" variant="outline" size="sm">Lv 20</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<!-- 舰船 -->
|
||||
<TabsContent value="ships" class="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('gmView.modifyShips') }}</CardTitle>
|
||||
<CardDescription>{{ t('gmView.shipsDesc') }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div v-for="ship in shipTypes" :key="ship" class="space-y-2">
|
||||
<Label>{{ SHIPS[ship].name }}</Label>
|
||||
<div class="flex gap-2">
|
||||
<Input v-model.number="selectedPlanet.fleet[ship]" type="number" min="0" class="flex-1" />
|
||||
<Button @click="setShipCount(ship, 100)" variant="outline" size="sm">+100</Button>
|
||||
<Button @click="setShipCount(ship, 1000)" variant="outline" size="sm">+1K</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<!-- 防御 -->
|
||||
<TabsContent value="defense" class="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('gmView.modifyDefense') }}</CardTitle>
|
||||
<CardDescription>{{ t('gmView.defenseDesc') }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div v-for="defense in defenseTypes" :key="defense" class="space-y-2">
|
||||
<Label>{{ DEFENSES[defense].name }}</Label>
|
||||
<div class="flex gap-2">
|
||||
<Input v-model.number="selectedPlanet.defense[defense]" type="number" min="0" class="flex-1" />
|
||||
<Button @click="setDefenseCount(defense, 100)" variant="outline" size="sm">+100</Button>
|
||||
<Button @click="setDefenseCount(defense, 1000)" variant="outline" size="sm">+1K</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<!-- 军官 -->
|
||||
<TabsContent value="officers" class="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('gmView.modifyOfficers') }}</CardTitle>
|
||||
<CardDescription>{{ t('gmView.officersDesc') }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div v-for="officer in officerTypes" :key="officer" class="space-y-2">
|
||||
<Label>{{ OFFICERS[officer].name }}</Label>
|
||||
<div class="flex gap-2">
|
||||
<Input v-model.number="officerDays[officer]" type="number" min="0" :placeholder="t('gmView.days')" class="flex-1" />
|
||||
<Button @click="setOfficerDays(officer, 7)" variant="outline" size="sm">7{{ t('gmView.days') }}</Button>
|
||||
<Button @click="setOfficerDays(officer, 30)" variant="outline" size="sm">30{{ t('gmView.days') }}</Button>
|
||||
<Button @click="setOfficerDays(officer, 365)" variant="outline" size="sm">365{{ t('gmView.days') }}</Button>
|
||||
<Input
|
||||
:model-value="section.getValue(item)"
|
||||
@update:model-value="section.setValue(item, Number($event) || 0)"
|
||||
type="number"
|
||||
:min="0"
|
||||
:max="section.max"
|
||||
:placeholder="section.placeholder"
|
||||
class="flex-1"
|
||||
/>
|
||||
<Button
|
||||
v-for="btn in section.buttons"
|
||||
:key="btn.label"
|
||||
@click="section.onButtonClick(item, btn.value)"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
{{ btn.label }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -204,7 +130,9 @@
|
||||
<SelectValue :placeholder="t('gmView.chooseNPC') || 'Choose NPC'" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem v-for="npc in npcStore.npcs" :key="npc.id" :value="npc.id">{{ npc.name }} ({{ npc.difficulty }})</SelectItem>
|
||||
<SelectItem v-for="npc in npcStore.npcs" :key="npc.id" :value="npc.id">
|
||||
{{ npc.name }} ({{ t(`diplomacy.diagnostic.difficultyLevels.${npc.difficulty}`) }})
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@@ -293,6 +221,9 @@
|
||||
<AlertDialogDescription v-if="alertDialogMessage" class="whitespace-pre-line">
|
||||
{{ alertDialogMessage }}
|
||||
</AlertDialogDescription>
|
||||
<AlertDialogDescription v-else class="sr-only">
|
||||
{{ alertDialogTitle }}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogAction @click="handleAlertConfirm">{{ t('common.confirm') }}</AlertDialogAction>
|
||||
@@ -388,11 +319,6 @@
|
||||
})
|
||||
|
||||
const resourceTypes = ['metal', 'crystal', 'deuterium', 'darkMatter'] as const
|
||||
const buildingTypes = Object.values(BuildingType)
|
||||
const technologyTypes = Object.values(TechnologyType)
|
||||
const shipTypes = Object.values(ShipType)
|
||||
const defenseTypes = Object.values(DefenseType)
|
||||
const officerTypes = Object.values(OfficerType)
|
||||
|
||||
// Tab配置
|
||||
const tabs = [
|
||||
@@ -410,52 +336,161 @@
|
||||
}
|
||||
}
|
||||
|
||||
const setBuildingLevel = (building: BuildingType, level: number) => {
|
||||
// GM编辑区块配置 - 统一管理建筑/科技/舰船/防御/军官
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type GMSection = {
|
||||
tabValue: string
|
||||
titleKey: string
|
||||
descKey: string
|
||||
items: string[]
|
||||
max?: number
|
||||
placeholder?: string
|
||||
buttons: { label: string; value: number }[]
|
||||
getItemName: (item: any) => string
|
||||
getValue: (item: any) => number
|
||||
setValue: (item: any, val: number) => void
|
||||
onButtonClick: (item: any, val: number) => void
|
||||
}
|
||||
|
||||
const gmSections = computed<GMSection[]>(() => [
|
||||
{
|
||||
tabValue: 'buildings',
|
||||
titleKey: 'gmView.modifyBuildings',
|
||||
descKey: 'gmView.buildingsDesc',
|
||||
items: Object.values(BuildingType),
|
||||
max: 100,
|
||||
placeholder: undefined,
|
||||
buttons: [
|
||||
{ label: 'Lv 10', value: 10 },
|
||||
{ label: 'Lv 30', value: 30 }
|
||||
],
|
||||
getItemName: (item: BuildingType) => BUILDINGS.value[item].name,
|
||||
getValue: (item: BuildingType) => selectedPlanet.value?.buildings[item] || 0,
|
||||
setValue: (item: BuildingType, val: number) => {
|
||||
if (selectedPlanet.value) {
|
||||
selectedPlanet.value.buildings[building] = level
|
||||
selectedPlanet.value.buildings[item] = val
|
||||
updatePlayerPoints()
|
||||
}
|
||||
}
|
||||
|
||||
const setTechnologyLevel = (tech: TechnologyType, level: number) => {
|
||||
gameStore.player.technologies[tech] = level
|
||||
updatePlayerPoints()
|
||||
}
|
||||
|
||||
const setShipCount = (ship: ShipType, count: number) => {
|
||||
},
|
||||
onButtonClick: (item: BuildingType, val: number) => {
|
||||
if (selectedPlanet.value) {
|
||||
selectedPlanet.value.fleet[ship] = (selectedPlanet.value.fleet[ship] || 0) + count
|
||||
selectedPlanet.value.buildings[item] = val
|
||||
updatePlayerPoints()
|
||||
}
|
||||
}
|
||||
|
||||
const setDefenseCount = (defense: DefenseType, count: number) => {
|
||||
},
|
||||
{
|
||||
tabValue: 'research',
|
||||
titleKey: 'gmView.modifyResearch',
|
||||
descKey: 'gmView.researchDesc',
|
||||
items: Object.values(TechnologyType),
|
||||
max: 50,
|
||||
placeholder: undefined,
|
||||
buttons: [
|
||||
{ label: 'Lv 10', value: 10 },
|
||||
{ label: 'Lv 20', value: 20 }
|
||||
],
|
||||
getItemName: (item: TechnologyType) => TECHNOLOGIES.value[item].name,
|
||||
getValue: (item: TechnologyType) => gameStore.player.technologies[item] || 0,
|
||||
setValue: (item: TechnologyType, val: number) => {
|
||||
gameStore.player.technologies[item] = val
|
||||
updatePlayerPoints()
|
||||
},
|
||||
onButtonClick: (item: TechnologyType, val: number) => {
|
||||
gameStore.player.technologies[item] = val
|
||||
updatePlayerPoints()
|
||||
}
|
||||
},
|
||||
{
|
||||
tabValue: 'ships',
|
||||
titleKey: 'gmView.modifyShips',
|
||||
descKey: 'gmView.shipsDesc',
|
||||
items: Object.values(ShipType),
|
||||
max: undefined,
|
||||
placeholder: undefined,
|
||||
buttons: [
|
||||
{ label: '+100', value: 100 },
|
||||
{ label: '+1K', value: 1000 }
|
||||
],
|
||||
getItemName: (item: ShipType) => SHIPS.value[item].name,
|
||||
getValue: (item: ShipType) => selectedPlanet.value?.fleet[item] || 0,
|
||||
setValue: (item: ShipType, val: number) => {
|
||||
if (selectedPlanet.value) {
|
||||
selectedPlanet.value.defense[defense] = (selectedPlanet.value.defense[defense] || 0) + count
|
||||
selectedPlanet.value.fleet[item] = val
|
||||
updatePlayerPoints()
|
||||
}
|
||||
},
|
||||
onButtonClick: (item: ShipType, val: number) => {
|
||||
if (selectedPlanet.value) {
|
||||
selectedPlanet.value.fleet[item] = (selectedPlanet.value.fleet[item] || 0) + val
|
||||
updatePlayerPoints()
|
||||
}
|
||||
}
|
||||
|
||||
const setOfficerDays = (officer: OfficerType, days: number) => {
|
||||
officerDays.value[officer] = days
|
||||
},
|
||||
{
|
||||
tabValue: 'defense',
|
||||
titleKey: 'gmView.modifyDefense',
|
||||
descKey: 'gmView.defenseDesc',
|
||||
items: Object.values(DefenseType),
|
||||
max: undefined,
|
||||
placeholder: undefined,
|
||||
buttons: [
|
||||
{ label: '+100', value: 100 },
|
||||
{ label: '+1K', value: 1000 }
|
||||
],
|
||||
getItemName: (item: DefenseType) => DEFENSES.value[item].name,
|
||||
getValue: (item: DefenseType) => selectedPlanet.value?.defense[item] || 0,
|
||||
setValue: (item: DefenseType, val: number) => {
|
||||
if (selectedPlanet.value) {
|
||||
selectedPlanet.value.defense[item] = val
|
||||
updatePlayerPoints()
|
||||
}
|
||||
},
|
||||
onButtonClick: (item: DefenseType, val: number) => {
|
||||
if (selectedPlanet.value) {
|
||||
selectedPlanet.value.defense[item] = (selectedPlanet.value.defense[item] || 0) + val
|
||||
updatePlayerPoints()
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
tabValue: 'officers',
|
||||
titleKey: 'gmView.modifyOfficers',
|
||||
descKey: 'gmView.officersDesc',
|
||||
items: Object.values(OfficerType),
|
||||
max: undefined,
|
||||
placeholder: t('gmView.days'),
|
||||
buttons: [
|
||||
{ label: `7${t('gmView.days')}`, value: 7 },
|
||||
{ label: `30${t('gmView.days')}`, value: 30 },
|
||||
{ label: `365${t('gmView.days')}`, value: 365 }
|
||||
],
|
||||
getItemName: (item: OfficerType) => OFFICERS.value[item].name,
|
||||
getValue: (item: OfficerType) => officerDays.value[item] || 0,
|
||||
setValue: (item: OfficerType, val: number) => {
|
||||
officerDays.value[item] = val
|
||||
},
|
||||
onButtonClick: (item: OfficerType, days: number) => {
|
||||
officerDays.value[item] = days
|
||||
const now = Date.now()
|
||||
const expiresAt = now + days * 24 * 60 * 60 * 1000
|
||||
|
||||
if (!gameStore.player.officers[officer]) {
|
||||
gameStore.player.officers[officer] = {
|
||||
type: officer,
|
||||
if (!gameStore.player.officers[item]) {
|
||||
gameStore.player.officers[item] = {
|
||||
type: item,
|
||||
active: true,
|
||||
hiredAt: now,
|
||||
expiresAt: expiresAt
|
||||
}
|
||||
} else {
|
||||
gameStore.player.officers[officer].expiresAt = expiresAt
|
||||
gameStore.player.officers[officer].active = true
|
||||
if (!gameStore.player.officers[officer].hiredAt) {
|
||||
gameStore.player.officers[officer].hiredAt = now
|
||||
gameStore.player.officers[item].expiresAt = expiresAt
|
||||
gameStore.player.officers[item].active = true
|
||||
if (!gameStore.player.officers[item].hiredAt) {
|
||||
gameStore.player.officers[item].hiredAt = now
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
// 显示重置游戏确认对话框
|
||||
const showResetConfirmDialog = () => {
|
||||
|
||||
@@ -205,16 +205,30 @@
|
||||
<div v-if="slot.planet" class="space-y-1">
|
||||
<!-- 第一行:名称、坐标、状态、残骸 -->
|
||||
<div class="flex items-center gap-1.5 min-w-0 flex-wrap">
|
||||
<h3 class="font-semibold text-sm truncate">{{ slot.planet.name }}</h3>
|
||||
<h3 class="font-semibold text-sm truncate">
|
||||
{{ isMyPlanet(slot.planet) ? slot.planet.name : getNpcPlanetDisplayName(slot.planet) }}
|
||||
</h3>
|
||||
<span class="text-xs text-muted-foreground whitespace-nowrap flex-shrink-0">
|
||||
[{{ slot.planet.position.galaxy }}:{{ slot.planet.position.system }}:{{ slot.planet.position.position }}]
|
||||
</span>
|
||||
<Badge v-if="isMyPlanet(slot.planet)" variant="default" class="text-xs flex-shrink-0">
|
||||
{{ t('galaxyView.mine') }}
|
||||
</Badge>
|
||||
<Badge v-else :variant="getRelationBadgeVariant(slot.planet)" class="text-xs flex-shrink-0">
|
||||
<Popover v-else>
|
||||
<PopoverTrigger as-child>
|
||||
<Badge :variant="getRelationBadgeVariant(slot.planet)" class="text-xs flex-shrink-0 cursor-pointer">
|
||||
{{ getRelationStatusText(slot.planet) }}
|
||||
</Badge>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent v-if="getReputationValue(slot.planet) !== null" class="w-auto p-3" side="top" align="center">
|
||||
<p class="text-sm">
|
||||
{{ t('diplomacy.reputation') }}:
|
||||
<span :class="getReputationColor(getReputationValue(slot.planet))" class="font-medium">
|
||||
{{ getReputationValue(slot.planet)! > 0 ? '+' : '' }}{{ getReputationValue(slot.planet) }}
|
||||
</span>
|
||||
</p>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<Popover v-if="getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)">
|
||||
<PopoverTrigger as-child>
|
||||
<Badge
|
||||
@@ -224,7 +238,7 @@
|
||||
<Recycle class="h-3 w-3" />
|
||||
</Badge>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-auto p-3" side="top" align="start">
|
||||
<PopoverContent class="w-auto p-3" side="top" align="center">
|
||||
<div class="space-y-2">
|
||||
<p class="text-xs font-semibold text-amber-700 dark:text-amber-400">{{ t('galaxyView.debrisField') }}</p>
|
||||
<div class="space-y-1 text-xs">
|
||||
@@ -247,13 +261,6 @@
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
<!-- 第二行:好感度 -->
|
||||
<div v-if="!isMyPlanet(slot.planet) && getReputationValue(slot.planet) !== null" class="text-xs">
|
||||
<span class="text-muted-foreground">{{ t('diplomacy.reputation') }}:</span>
|
||||
<span class="ml-1 font-semibold" :class="getReputationColor(getReputationValue(slot.planet))">
|
||||
{{ getReputationValue(slot.planet)! > 0 ? '+' : '' }}{{ getReputationValue(slot.planet) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 空位置 -->
|
||||
<div v-else class="space-y-1">
|
||||
@@ -389,11 +396,27 @@
|
||||
<div v-if="slot.planet" class="space-y-1">
|
||||
<!-- PC端:标题和徽章 -->
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<h3 class="font-semibold text-base">{{ slot.planet.name }}</h3>
|
||||
<h3 class="font-semibold text-base">
|
||||
{{ isMyPlanet(slot.planet) ? slot.planet.name : getNpcPlanetDisplayName(slot.planet) }}
|
||||
</h3>
|
||||
<Badge v-if="isMyPlanet(slot.planet)" variant="default" class="text-xs">{{ t('galaxyView.mine') }}</Badge>
|
||||
<Badge v-else :variant="getRelationBadgeVariant(slot.planet)" class="text-xs">
|
||||
<TooltipProvider v-else :delay-duration="300">
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Badge :variant="getRelationBadgeVariant(slot.planet)" class="text-xs cursor-default">
|
||||
{{ getRelationStatusText(slot.planet) }}
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent v-if="getReputationValue(slot.planet) !== null">
|
||||
<p>
|
||||
{{ t('diplomacy.reputation') }}:
|
||||
<span :class="getReputationColor(getReputationValue(slot.planet))">
|
||||
{{ getReputationValue(slot.planet)! > 0 ? '+' : '' }}{{ getReputationValue(slot.planet) }}
|
||||
</span>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<!-- 残骸场徽章 -->
|
||||
<Popover v-if="getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)">
|
||||
<PopoverTrigger as-child>
|
||||
@@ -432,13 +455,6 @@
|
||||
<p class="text-xs text-muted-foreground">
|
||||
[{{ slot.planet.position.galaxy }}:{{ slot.planet.position.system }}:{{ slot.planet.position.position }}]
|
||||
</p>
|
||||
<!-- PC端:好感度显示(仅NPC星球) -->
|
||||
<div v-if="!isMyPlanet(slot.planet) && getReputationValue(slot.planet) !== null" class="text-xs">
|
||||
<span class="text-muted-foreground">{{ t('diplomacy.reputation') }}:</span>
|
||||
<span class="ml-1 font-semibold" :class="getReputationColor(getReputationValue(slot.planet))">
|
||||
{{ getReputationValue(slot.planet)! > 0 ? '+' : '' }}{{ getReputationValue(slot.planet) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 空位置 -->
|
||||
<div v-else class="space-y-1">
|
||||
@@ -744,9 +760,6 @@
|
||||
selectedGalaxy.value = queryGalaxy
|
||||
selectedSystem.value = querySystem
|
||||
loadSystem()
|
||||
|
||||
// 立即清除URL参数,但保持本地变量中的highlightNpcId
|
||||
clearUrlParams()
|
||||
} else if (gameStore.currentPlanet) {
|
||||
// 否则默认显示当前星球所在的星系
|
||||
currentGalaxy.value = gameStore.currentPlanet.position.galaxy
|
||||
@@ -778,13 +791,6 @@
|
||||
return universeStore.debrisFields[debrisId] || null
|
||||
}
|
||||
|
||||
// 清除URL参数
|
||||
const clearUrlParams = () => {
|
||||
if (route.query.highlightNpc || route.query.galaxy || route.query.system) {
|
||||
router.replace({ query: {} })
|
||||
}
|
||||
}
|
||||
|
||||
// 加载星系
|
||||
const loadSystem = () => {
|
||||
currentGalaxy.value = selectedGalaxy.value
|
||||
@@ -833,7 +839,8 @@
|
||||
const getRelation = (planet: Planet | null) => {
|
||||
const npc = getPlanetNPC(planet)
|
||||
if (!npc) return null
|
||||
return gameStore.player.diplomaticRelations?.[npc.id]
|
||||
// 从NPC的relations中获取对玩家的关系
|
||||
return npc.relations?.[gameStore.player.id]
|
||||
}
|
||||
|
||||
// 获取关系状态Badge样式
|
||||
@@ -880,6 +887,17 @@
|
||||
return 'text-muted-foreground'
|
||||
}
|
||||
|
||||
// 获取NPC星球的显示名称 - 使用"XXX的星球"格式,如果有备注则显示"NPC名称(备注)的星球"
|
||||
const getNpcPlanetDisplayName = (planet: Planet | null): string => {
|
||||
if (!planet) return ''
|
||||
const npc = getPlanetNPC(planet)
|
||||
if (npc) {
|
||||
const displayName = npc.note ? `${npc.name}(${npc.note})` : npc.name
|
||||
return t('galaxyView.npcPlanetName').replace('{name}', displayName)
|
||||
}
|
||||
return planet.name
|
||||
}
|
||||
|
||||
// 切换到指定星球
|
||||
const switchToPlanet = (planetId: string) => {
|
||||
gameStore.currentPlanetId = planetId
|
||||
|
||||
159
src/views/HomeView.vue
Normal file
159
src/views/HomeView.vue
Normal file
@@ -0,0 +1,159 @@
|
||||
<template>
|
||||
<div class="min-h-screen flex flex-col items-center justify-center bg-gradient-to-b from-background to-muted/50 p-4">
|
||||
<!-- Logo 和标题 -->
|
||||
<div class="text-center mb-8 animate-fade-in">
|
||||
<img src="@/assets/logo.svg" alt="OGame Logo" class="w-24 h-24 mx-auto mb-4" />
|
||||
<h1 class="text-4xl sm:text-5xl font-bold bg-gradient-to-r from-primary to-primary/60 bg-clip-text text-transparent">
|
||||
{{ pkg.title }}
|
||||
</h1>
|
||||
<p class="text-muted-foreground mt-2 text-sm sm:text-base">{{ t('home.subtitle') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 开始游戏按钮 -->
|
||||
<div class="flex flex-col gap-4 w-full max-w-xs mb-8">
|
||||
<Button size="lg" class="w-full text-lg h-14" @click="handleStartGame">
|
||||
<Rocket class="mr-2 h-5 w-5" />
|
||||
{{ t('home.startGame') }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<div class="flex flex-wrap items-center justify-center gap-4">
|
||||
<!-- 语言切换 -->
|
||||
<Popover>
|
||||
<PopoverTrigger as-child>
|
||||
<Button variant="outline" size="sm">
|
||||
<Languages class="mr-2 h-4 w-4" />
|
||||
{{ localeNames[gameStore.locale] }}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-48 p-2" side="top">
|
||||
<div class="space-y-1">
|
||||
<Button
|
||||
v-for="locale in availableLocales"
|
||||
:key="locale"
|
||||
@click="gameStore.locale = locale"
|
||||
:variant="gameStore.locale === locale ? 'secondary' : 'ghost'"
|
||||
class="w-full justify-start"
|
||||
size="sm"
|
||||
>
|
||||
{{ localeNames[locale] }}
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<!-- 隐私协议按钮 -->
|
||||
<Button variant="ghost" size="sm" @click="showPrivacyDialog = true">
|
||||
<Shield class="mr-2 h-4 w-4" />
|
||||
{{ t('settings.privacyPolicy') }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- 隐私协议弹窗 -->
|
||||
<PrivacyDialog v-model:open="showPrivacyDialog" />
|
||||
|
||||
<!-- 隐私协议同意确认弹窗(开始游戏时) -->
|
||||
<AlertDialog v-model:open="showAgreementDialog">
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{{ t('home.privacyAgreement') }}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{{ t('home.privacyAgreementDesc') }}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<div class="my-4 max-h-48 overflow-y-auto text-sm text-muted-foreground border rounded-lg p-3">
|
||||
<p>{{ t('privacy.sections.introduction.content') }}</p>
|
||||
<p class="mt-2">{{ t('privacy.sections.dataCollection.content') }}</p>
|
||||
<p class="mt-2">{{ t('privacy.sections.thirdParty.content') }}</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<Checkbox id="privacy-agree" v-model="privacyAgreed" />
|
||||
<label for="privacy-agree" class="text-sm cursor-pointer">
|
||||
{{ t('home.agreeToPrivacy') }}
|
||||
<Button variant="link" class="p-0 h-auto text-sm" @click.prevent="showPrivacyDialog = true">
|
||||
{{ t('home.viewFullPolicy') }}
|
||||
</Button>
|
||||
</label>
|
||||
</div>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel @click="privacyAgreed = false">{{ t('common.cancel') }}</AlertDialogCancel>
|
||||
<AlertDialogAction :disabled="!privacyAgreed" @click="confirmStartGame">
|
||||
{{ t('home.agreeAndStart') }}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
import { localeNames, type Locale } from '@/locales'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { Rocket, Languages, Shield } from 'lucide-vue-next'
|
||||
import PrivacyDialog from '@/components/PrivacyDialog.vue'
|
||||
import pkg from '../../package.json'
|
||||
|
||||
const router = useRouter()
|
||||
const gameStore = useGameStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
const showPrivacyDialog = ref(false)
|
||||
const showAgreementDialog = ref(false)
|
||||
const privacyAgreed = ref(false)
|
||||
|
||||
const availableLocales: Locale[] = ['zh-CN', 'zh-TW', 'en', 'de', 'ru', 'ko', 'ja']
|
||||
|
||||
// 处理开始游戏
|
||||
const handleStartGame = () => {
|
||||
// 如果已经同意过,直接进入总览页面
|
||||
if (gameStore.player.privacyAgreed) {
|
||||
router.push('/overview')
|
||||
} else {
|
||||
// 显示同意弹窗
|
||||
showAgreementDialog.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// 确认开始游戏
|
||||
const confirmStartGame = () => {
|
||||
if (privacyAgreed.value) {
|
||||
gameStore.player.privacyAgreed = true
|
||||
showAgreementDialog.value = false
|
||||
router.push('/overview')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,24 +1,57 @@
|
||||
<template>
|
||||
<div class="container mx-auto p-4 sm:p-6 space-y-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-2xl sm:text-3xl font-bold">{{ t('messagesView.title') }}</h1>
|
||||
|
||||
<!-- 清空消息按钮 -->
|
||||
<Popover v-model:open="showClearPopover">
|
||||
<PopoverTrigger as-child>
|
||||
<Button variant="outline" size="sm" class="gap-2">
|
||||
<Trash2 class="h-4 w-4" />
|
||||
{{ t('messagesView.clearMessages') }}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-80">
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<h4 class="font-medium leading-none">{{ t('messagesView.clearMessageTypes') }}</h4>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<div v-for="option in clearOptionFields" :key="option.key" class="flex items-center space-x-2">
|
||||
<Checkbox :id="`clear-${option.key}`" v-model="clearOptions[option.key]" />
|
||||
<label :for="`clear-${option.key}`" class="text-sm cursor-pointer">
|
||||
{{ t(`messagesView.${option.labelKey}`) }} ({{ option.count }})
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<Button @click="clearSelectedMessages" class="w-full" :disabled="!hasSelectedAny">
|
||||
{{ t('messagesView.clearNow') }}
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<!-- 标签切换 -->
|
||||
<Tabs v-model="activeTab" class="w-full">
|
||||
<TabsList class="grid w-full grid-cols-2 sm:grid-cols-4" :tab-count="4">
|
||||
<TabsTrigger v-for="tab in tabs" :key="tab.value" :value="tab.value" class="flex items-center justify-center gap-1 px-2">
|
||||
<component :is="tab.icon" class="h-3 w-3 sm:h-4 sm:w-4" />
|
||||
<span class="text-xs sm:text-sm truncate">{{ tab.label }}</span>
|
||||
<Badge v-if="tab.unreadCount > 0" variant="destructive" class="hidden sm:flex ml-1">
|
||||
<Badge v-if="tab.unreadCount > 0" variant="destructive" class="ml-1 text-xs">
|
||||
{{ tab.unreadCount }}
|
||||
</Badge>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<!-- 战斗报告列表 -->
|
||||
<TabsContent value="battles" class="mt-4 space-y-2">
|
||||
<Card v-if="gameStore.player.battleReports.length === 0">
|
||||
<CardContent class="py-8 text-center text-muted-foreground">{{ t('messagesView.noBattleReports') }}</CardContent>
|
||||
</Card>
|
||||
<TabsContent value="battles" class="mt-4 space-y-2 pb-20">
|
||||
<Empty v-if="gameStore.player.battleReports.length === 0" class="border rounded-lg">
|
||||
<EmptyContent>
|
||||
<Sword class="h-10 w-10 text-muted-foreground" />
|
||||
<EmptyDescription>{{ t('messagesView.noBattleReports') }}</EmptyDescription>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
|
||||
<Card
|
||||
v-for="report in sortedBattleReports"
|
||||
@@ -48,10 +81,13 @@
|
||||
</TabsContent>
|
||||
|
||||
<!-- 间谍报告列表(合并:侦查报告 + 被侦查通知) -->
|
||||
<TabsContent value="spy" class="mt-4 space-y-2">
|
||||
<Card v-if="gameStore.player.spyReports.length === 0 && sortedSpiedNotifications.length === 0">
|
||||
<CardContent class="py-8 text-center text-muted-foreground">{{ t('messagesView.noSpyReports') }}</CardContent>
|
||||
</Card>
|
||||
<TabsContent value="spy" class="mt-4 space-y-2 pb-20">
|
||||
<Empty v-if="gameStore.player.spyReports.length === 0 && sortedSpiedNotifications.length === 0" class="border rounded-lg">
|
||||
<EmptyContent>
|
||||
<Eye class="h-10 w-10 text-muted-foreground" />
|
||||
<EmptyDescription>{{ t('messagesView.noSpyReports') }}</EmptyDescription>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
|
||||
<!-- 侦查报告 -->
|
||||
<Card
|
||||
@@ -107,16 +143,20 @@
|
||||
</TabsContent>
|
||||
|
||||
<!-- NPC相关消息(活动、礼物、被拒绝) -->
|
||||
<TabsContent value="npc" class="mt-4 space-y-2">
|
||||
<Card
|
||||
<TabsContent value="npc" class="mt-4 space-y-2 pb-20">
|
||||
<Empty
|
||||
v-if="
|
||||
sortedNPCActivityNotifications.length === 0 &&
|
||||
sortedGiftNotifications.length === 0 &&
|
||||
sortedGiftRejectedNotifications.length === 0
|
||||
"
|
||||
class="border rounded-lg"
|
||||
>
|
||||
<CardContent class="py-8 text-center text-muted-foreground">{{ t('messagesView.noNPCActivity') }}</CardContent>
|
||||
</Card>
|
||||
<EmptyContent>
|
||||
<Users class="h-10 w-10 text-muted-foreground" />
|
||||
<EmptyDescription>{{ t('messagesView.noNPCActivity') }}</EmptyDescription>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
|
||||
<!-- NPC活动通知 -->
|
||||
<Card
|
||||
@@ -177,11 +217,11 @@
|
||||
<div class="text-sm">
|
||||
<div class="font-semibold mb-1">{{ t('messagesView.giftResources') }}:</div>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<div v-if="gift.resources.metal > 0">{{ t('resources.metal') }}: {{ gift.resources.metal.toLocaleString() }}</div>
|
||||
<div v-if="gift.resources.crystal > 0">{{ t('resources.crystal') }}: {{ gift.resources.crystal.toLocaleString() }}</div>
|
||||
<div v-if="gift.resources.deuterium > 0">
|
||||
{{ t('resources.deuterium') }}: {{ gift.resources.deuterium.toLocaleString() }}
|
||||
<template v-for="res in basicResourceFields" :key="res.key">
|
||||
<div v-if="gift.resources[res.key] > 0">
|
||||
{{ t(`resources.${res.key}`) }}: {{ gift.resources[res.key].toLocaleString() }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-xs text-muted-foreground">
|
||||
@@ -229,15 +269,11 @@
|
||||
<div class="text-sm">
|
||||
<div class="font-semibold mb-1">{{ t('messagesView.rejectedResources') }}:</div>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<div v-if="rejection.rejectedResources.metal > 0">
|
||||
{{ t('resources.metal') }}: {{ rejection.rejectedResources.metal.toLocaleString() }}
|
||||
</div>
|
||||
<div v-if="rejection.rejectedResources.crystal > 0">
|
||||
{{ t('resources.crystal') }}: {{ rejection.rejectedResources.crystal.toLocaleString() }}
|
||||
</div>
|
||||
<div v-if="rejection.rejectedResources.deuterium > 0">
|
||||
{{ t('resources.deuterium') }}: {{ rejection.rejectedResources.deuterium.toLocaleString() }}
|
||||
<template v-for="res in basicResourceFields" :key="res.key">
|
||||
<div v-if="rejection.rejectedResources[res.key] > 0">
|
||||
{{ t(`resources.${res.key}`) }}: {{ rejection.rejectedResources[res.key].toLocaleString() }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-xs text-muted-foreground">
|
||||
@@ -253,10 +289,13 @@
|
||||
</TabsContent>
|
||||
|
||||
<!-- 任务报告列表 -->
|
||||
<TabsContent value="missions" class="mt-4 space-y-2">
|
||||
<Card v-if="sortedMissionReports.length === 0">
|
||||
<CardContent class="py-8 text-center text-muted-foreground">{{ t('messagesView.noMissionReports') }}</CardContent>
|
||||
</Card>
|
||||
<TabsContent value="missions" class="mt-4 space-y-2 pb-20">
|
||||
<Empty v-if="sortedMissionReports.length === 0" class="border rounded-lg">
|
||||
<EmptyContent>
|
||||
<Package class="h-10 w-10 text-muted-foreground" />
|
||||
<EmptyDescription>{{ t('messagesView.noMissionReports') }}</EmptyDescription>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
|
||||
<Card
|
||||
v-for="report in sortedMissionReports"
|
||||
@@ -291,6 +330,9 @@
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<!-- 固定底部分页 -->
|
||||
<FixedPagination v-model:page="currentPage[activeTab]" :total-pages="getPaginationConfig(activeTab).totalPages" />
|
||||
|
||||
<!-- 战斗报告对话框 -->
|
||||
<BattleReportDialog v-model:open="showBattleDialog" :report="selectedBattleReport" />
|
||||
|
||||
@@ -305,6 +347,9 @@
|
||||
<Eye class="h-5 w-5 text-purple-500" />
|
||||
{{ t('messagesView.spiedNotificationDetails') }}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{{ t('messagesView.spyDetected') }}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div v-if="selectedSpiedNotification" class="space-y-4">
|
||||
@@ -370,6 +415,9 @@
|
||||
<component :is="getMissionIcon(selectedMissionReport?.missionType)" class="h-5 w-5" />
|
||||
{{ t('messagesView.missionReportDetails') }}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{{ t('messagesView.missionDetails') }}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div v-if="selectedMissionReport" class="space-y-4">
|
||||
@@ -417,10 +465,8 @@
|
||||
<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 v-for="res in basicResourceFields" :key="res.key">
|
||||
{{ t(`resources.${res.key}`) }}: {{ selectedMissionReport.details.transportedResources[res.key].toLocaleString() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -429,14 +475,16 @@
|
||||
<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 v-for="res in debrisResourceFields" :key="res.key">
|
||||
{{ t(`resources.${res.key}`) }}: {{ selectedMissionReport.details.recycledResources[res.key].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 v-for="res in debrisResourceFields" :key="res.key">
|
||||
{{ t(`resources.${res.key}`) }}: {{ selectedMissionReport.details.remainingDebris[res.key].toLocaleString() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -449,6 +497,34 @@
|
||||
<span class="font-medium">{{ selectedMissionReport.details.newPlanetName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 导弹攻击详情 -->
|
||||
<div v-if="selectedMissionReport.details?.missileCount !== undefined" class="mt-3 space-y-2">
|
||||
<p class="text-xs font-semibold text-muted-foreground">{{ t('galaxyView.missileAttack') }}:</p>
|
||||
<div class="grid grid-cols-3 gap-2 text-sm">
|
||||
<div>
|
||||
<span class="text-muted-foreground">{{ t('galaxyView.missileCount') }}:</span>
|
||||
<span class="ml-1 font-medium">{{ selectedMissionReport.details.missileCount }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-muted-foreground">{{ t('missionReports.hits') }}:</span>
|
||||
<span class="ml-1 font-medium text-green-600">{{ selectedMissionReport.details.missileHits }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-muted-foreground">{{ t('galaxyView.intercepted') }}:</span>
|
||||
<span class="ml-1 font-medium text-yellow-600">{{ selectedMissionReport.details.missileIntercepted }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="Object.keys(selectedMissionReport.details.defenseLosses || {}).length > 0" class="mt-2">
|
||||
<p class="text-xs font-semibold text-muted-foreground">{{ t('galaxyView.defenseLosses') }}:</p>
|
||||
<div class="grid grid-cols-2 gap-1 text-xs mt-1 p-2 bg-red-50 dark:bg-red-950/30 rounded">
|
||||
<div v-for="(count, defenseType) in selectedMissionReport.details.defenseLosses" :key="defenseType">
|
||||
<span class="text-muted-foreground">{{ t('defenses.' + defenseType) }}:</span>
|
||||
<span class="ml-1 font-medium text-red-600 dark:text-red-400">-{{ count }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -467,6 +543,9 @@
|
||||
<Recycle class="h-5 w-5 text-yellow-500" />
|
||||
{{ t('messagesView.npcActivityDetails') }}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{{ t('messagesView.activityDescription') }}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div v-if="selectedNPCActivityNotification" class="space-y-4">
|
||||
@@ -551,11 +630,15 @@
|
||||
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 { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'
|
||||
import { FixedPagination } from '@/components/ui/pagination'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
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, Skull, Globe, Compass } from 'lucide-vue-next'
|
||||
import { X, Sword, Eye, AlertTriangle, Package, Recycle, Gift, Ban, Check, Users, Skull, Globe, Compass, Trash2 } from 'lucide-vue-next'
|
||||
import { Empty, EmptyContent, EmptyDescription } from '@/components/ui/empty'
|
||||
import type {
|
||||
BattleResult,
|
||||
SpyReport,
|
||||
@@ -568,6 +651,7 @@
|
||||
import { MissionType } from '@/types/game'
|
||||
import { useNPCStore } from '@/stores/npcStore'
|
||||
import * as diplomaticLogic from '@/logic/diplomaticLogic'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
const router = useRouter()
|
||||
const gameStore = useGameStore()
|
||||
@@ -575,6 +659,107 @@
|
||||
const { t } = useI18n()
|
||||
const activeTab = ref<'battles' | 'spy' | 'missions' | 'npc'>('battles')
|
||||
|
||||
// 清空消息功能
|
||||
const showClearPopover = ref(false)
|
||||
type ClearOptionKey =
|
||||
| 'battles'
|
||||
| 'spyReports'
|
||||
| 'spiedNotifications'
|
||||
| 'missionReports'
|
||||
| 'npcActivity'
|
||||
| 'giftNotifications'
|
||||
| 'giftRejected'
|
||||
const clearOptions = ref<Record<ClearOptionKey, boolean>>({
|
||||
battles: false,
|
||||
spyReports: false,
|
||||
spiedNotifications: false,
|
||||
missionReports: false,
|
||||
npcActivity: false,
|
||||
giftNotifications: false,
|
||||
giftRejected: false
|
||||
})
|
||||
|
||||
// 清空消息选项配置
|
||||
const clearOptionFields = computed(() => [
|
||||
{ key: 'battles' as ClearOptionKey, labelKey: 'clearBattleReports', count: gameStore.player.battleReports.length },
|
||||
{ key: 'spyReports' as ClearOptionKey, labelKey: 'clearSpyReports', count: gameStore.player.spyReports.length },
|
||||
{
|
||||
key: 'spiedNotifications' as ClearOptionKey,
|
||||
labelKey: 'clearSpiedNotifications',
|
||||
count: gameStore.player.spiedNotifications?.length || 0
|
||||
},
|
||||
{ key: 'missionReports' as ClearOptionKey, labelKey: 'clearMissionReports', count: gameStore.player.missionReports?.length || 0 },
|
||||
{ key: 'npcActivity' as ClearOptionKey, labelKey: 'clearNPCActivity', count: gameStore.player.npcActivityNotifications?.length || 0 },
|
||||
{
|
||||
key: 'giftNotifications' as ClearOptionKey,
|
||||
labelKey: 'clearGiftNotifications',
|
||||
count: gameStore.player.giftNotifications?.length || 0
|
||||
},
|
||||
{ key: 'giftRejected' as ClearOptionKey, labelKey: 'clearGiftRejected', count: gameStore.player.giftRejectedNotifications?.length || 0 }
|
||||
])
|
||||
|
||||
// 基础资源字段配置(用于显示资源列表)
|
||||
type BasicResourceKey = 'metal' | 'crystal' | 'deuterium'
|
||||
const basicResourceFields: { key: BasicResourceKey }[] = [{ key: 'metal' }, { key: 'crystal' }, { key: 'deuterium' }]
|
||||
|
||||
// 残骸资源字段配置(只有金属和晶体)
|
||||
type DebrisResourceKey = 'metal' | 'crystal'
|
||||
const debrisResourceFields: { key: DebrisResourceKey }[] = [{ key: 'metal' }, { key: 'crystal' }]
|
||||
|
||||
const hasSelectedAny = computed(() => {
|
||||
return Object.values(clearOptions.value).some(v => v)
|
||||
})
|
||||
|
||||
const clearSelectedMessages = () => {
|
||||
if (clearOptions.value.battles) {
|
||||
gameStore.player.battleReports = []
|
||||
}
|
||||
if (clearOptions.value.spyReports) {
|
||||
gameStore.player.spyReports = []
|
||||
}
|
||||
if (clearOptions.value.spiedNotifications) {
|
||||
gameStore.player.spiedNotifications = []
|
||||
}
|
||||
if (clearOptions.value.missionReports) {
|
||||
gameStore.player.missionReports = []
|
||||
}
|
||||
if (clearOptions.value.npcActivity) {
|
||||
gameStore.player.npcActivityNotifications = []
|
||||
}
|
||||
if (clearOptions.value.giftNotifications) {
|
||||
gameStore.player.giftNotifications = []
|
||||
}
|
||||
if (clearOptions.value.giftRejected) {
|
||||
gameStore.player.giftRejectedNotifications = []
|
||||
}
|
||||
|
||||
// 重置选项
|
||||
clearOptions.value = {
|
||||
battles: false,
|
||||
spyReports: false,
|
||||
spiedNotifications: false,
|
||||
missionReports: false,
|
||||
npcActivity: false,
|
||||
giftNotifications: false,
|
||||
giftRejected: false
|
||||
}
|
||||
|
||||
// 关闭popover
|
||||
showClearPopover.value = false
|
||||
|
||||
// 显示成功提示
|
||||
toast.success(t('messagesView.clearSuccess'))
|
||||
}
|
||||
|
||||
// 分页状态
|
||||
const ITEMS_PER_PAGE = 10
|
||||
const currentPage = ref({
|
||||
battles: 1,
|
||||
spy: 1,
|
||||
missions: 1,
|
||||
npc: 1
|
||||
})
|
||||
|
||||
// 对话框状态
|
||||
const showBattleDialog = ref(false)
|
||||
const showSpyDialog = ref(false)
|
||||
@@ -587,40 +772,107 @@
|
||||
const selectedMissionReport = ref<MissionReport | null>(null)
|
||||
const selectedNPCActivityNotification = ref<NPCActivityNotification | null>(null)
|
||||
|
||||
// 排序后的战斗报告(最新的在前)
|
||||
const sortedBattleReports = computed(() => {
|
||||
// 排序后的战斗报告(最新的在前)- 全部数据
|
||||
const allBattleReports = computed(() => {
|
||||
return [...gameStore.player.battleReports].sort((a, b) => b.timestamp - a.timestamp)
|
||||
})
|
||||
|
||||
// 排序后的间谍报告(最新的在前)
|
||||
const sortedSpyReports = computed(() => {
|
||||
// 排序后的间谍报告(最新的在前)- 全部数据
|
||||
const allSpyReports = computed(() => {
|
||||
return [...gameStore.player.spyReports].sort((a, b) => b.timestamp - a.timestamp)
|
||||
})
|
||||
|
||||
// 排序后的被侦查通知(最新的在前)
|
||||
const sortedSpiedNotifications = computed(() => {
|
||||
// 排序后的被侦查通知(最新的在前)- 全部数据
|
||||
const allSpiedNotifications = computed(() => {
|
||||
if (!gameStore.player.spiedNotifications) {
|
||||
return []
|
||||
}
|
||||
return [...gameStore.player.spiedNotifications].sort((a, b) => b.timestamp - a.timestamp)
|
||||
})
|
||||
|
||||
// 排序后的任务报告(最新的在前)
|
||||
const sortedMissionReports = computed(() => {
|
||||
// 排序后的任务报告(最新的在前)- 全部数据
|
||||
const allMissionReports = computed(() => {
|
||||
if (!gameStore.player.missionReports) {
|
||||
return []
|
||||
}
|
||||
return [...gameStore.player.missionReports].sort((a, b) => b.timestamp - a.timestamp)
|
||||
})
|
||||
|
||||
// 排序后的NPC活动通知(最新的在前)
|
||||
const sortedNPCActivityNotifications = computed(() => {
|
||||
// 排序后的NPC活动通知(最新的在前)- 全部数据
|
||||
const allNPCActivityNotifications = computed(() => {
|
||||
if (!gameStore.player.npcActivityNotifications) {
|
||||
return []
|
||||
}
|
||||
return [...gameStore.player.npcActivityNotifications].sort((a, b) => b.timestamp - a.timestamp)
|
||||
})
|
||||
|
||||
// 战斗报告分页
|
||||
const battleReportsTotalPages = computed(() => Math.ceil(allBattleReports.value.length / ITEMS_PER_PAGE))
|
||||
const sortedBattleReports = computed(() => {
|
||||
const start = (currentPage.value.battles - 1) * ITEMS_PER_PAGE
|
||||
const end = start + ITEMS_PER_PAGE
|
||||
return allBattleReports.value.slice(start, end)
|
||||
})
|
||||
|
||||
// 侦查标签页合并数据(侦查报告 + 被侦查通知)
|
||||
const allSpyTabItems = computed(() => {
|
||||
const spyReports = allSpyReports.value.map(item => ({ ...item, type: 'spy' as const }))
|
||||
const spiedNotifications = allSpiedNotifications.value.map(item => ({ ...item, type: 'spied' as const }))
|
||||
return [...spyReports, ...spiedNotifications].sort((a, b) => b.timestamp - a.timestamp)
|
||||
})
|
||||
|
||||
const spyTabTotalPages = computed(() => Math.ceil(allSpyTabItems.value.length / ITEMS_PER_PAGE))
|
||||
const paginatedSpyTabItems = computed(() => {
|
||||
const start = (currentPage.value.spy - 1) * ITEMS_PER_PAGE
|
||||
const end = start + ITEMS_PER_PAGE
|
||||
return allSpyTabItems.value.slice(start, end)
|
||||
})
|
||||
|
||||
// 从分页后的混合数据中分离出侦查报告和被侦查通知
|
||||
const sortedSpyReports = computed(() => {
|
||||
return paginatedSpyTabItems.value.filter(item => item.type === 'spy')
|
||||
})
|
||||
|
||||
const sortedSpiedNotifications = computed(() => {
|
||||
return paginatedSpyTabItems.value.filter(item => item.type === 'spied')
|
||||
})
|
||||
|
||||
// 任务报告分页
|
||||
const missionReportsTotalPages = computed(() => Math.ceil(allMissionReports.value.length / ITEMS_PER_PAGE))
|
||||
const sortedMissionReports = computed(() => {
|
||||
const start = (currentPage.value.missions - 1) * ITEMS_PER_PAGE
|
||||
const end = start + ITEMS_PER_PAGE
|
||||
return allMissionReports.value.slice(start, end)
|
||||
})
|
||||
|
||||
// NPC标签页合并数据(活动通知 + 礼物通知 + 礼物被拒绝通知)
|
||||
const allNPCTabItems = computed(() => {
|
||||
const activities = allNPCActivityNotifications.value.map(item => ({ ...item, type: 'activity' as const }))
|
||||
const gifts = allGiftNotifications.value.map(item => ({ ...item, type: 'gift' as const }))
|
||||
const rejections = allGiftRejectedNotifications.value.map(item => ({ ...item, type: 'rejection' as const }))
|
||||
return [...activities, ...gifts, ...rejections].sort((a, b) => b.timestamp - a.timestamp)
|
||||
})
|
||||
|
||||
const npcTabTotalPages = computed(() => Math.ceil(allNPCTabItems.value.length / ITEMS_PER_PAGE))
|
||||
const paginatedNPCTabItems = computed(() => {
|
||||
const start = (currentPage.value.npc - 1) * ITEMS_PER_PAGE
|
||||
const end = start + ITEMS_PER_PAGE
|
||||
return allNPCTabItems.value.slice(start, end)
|
||||
})
|
||||
|
||||
// 从分页后的混合数据中分离出各种NPC消息
|
||||
const sortedNPCActivityNotifications = computed(() => {
|
||||
return paginatedNPCTabItems.value.filter(item => item.type === 'activity')
|
||||
})
|
||||
|
||||
const sortedGiftNotifications = computed(() => {
|
||||
return paginatedNPCTabItems.value.filter(item => item.type === 'gift')
|
||||
})
|
||||
|
||||
const sortedGiftRejectedNotifications = computed(() => {
|
||||
return paginatedNPCTabItems.value.filter(item => item.type === 'rejection')
|
||||
})
|
||||
|
||||
// 未读战斗报告数量
|
||||
const unreadBattles = computed(() => {
|
||||
return gameStore.player.battleReports.filter(r => !r.read).length
|
||||
@@ -709,22 +961,41 @@
|
||||
}
|
||||
])
|
||||
|
||||
// 排序后的礼物通知(最新的在前)
|
||||
const sortedGiftNotifications = computed(() => {
|
||||
// 礼物通知和被拒绝通知的全部数据(用于NPC标签页合并)
|
||||
const allGiftNotifications = computed(() => {
|
||||
if (!gameStore.player.giftNotifications) {
|
||||
return []
|
||||
}
|
||||
return [...gameStore.player.giftNotifications].sort((a, b) => b.timestamp - a.timestamp)
|
||||
})
|
||||
|
||||
// 排序后的礼物被拒绝通知(最新的在前)
|
||||
const sortedGiftRejectedNotifications = computed(() => {
|
||||
const allGiftRejectedNotifications = computed(() => {
|
||||
if (!gameStore.player.giftRejectedNotifications) {
|
||||
return []
|
||||
}
|
||||
return [...gameStore.player.giftRejectedNotifications].sort((a, b) => b.timestamp - a.timestamp)
|
||||
})
|
||||
|
||||
// 分页配置
|
||||
type PageKey = 'battles' | 'spy' | 'missions' | 'npc'
|
||||
const paginationConfigs = computed(() => ({
|
||||
battles: {
|
||||
totalPages: battleReportsTotalPages.value
|
||||
},
|
||||
spy: {
|
||||
totalPages: spyTabTotalPages.value
|
||||
},
|
||||
missions: {
|
||||
totalPages: missionReportsTotalPages.value
|
||||
},
|
||||
npc: {
|
||||
totalPages: npcTabTotalPages.value
|
||||
}
|
||||
}))
|
||||
|
||||
// 获取指定标签页的分页配置
|
||||
const getPaginationConfig = (key: PageKey) => paginationConfigs.value[key]
|
||||
|
||||
// 判断战斗结果Badge颜色
|
||||
const getBattleResultVariant = (report: BattleResult): 'default' | 'destructive' | 'secondary' => {
|
||||
if (report.winner === 'draw') {
|
||||
@@ -765,17 +1036,19 @@
|
||||
const openSpyReport = (report: SpyReport) => {
|
||||
selectedSpyReport.value = report
|
||||
showSpyDialog.value = true
|
||||
// 标记为已读
|
||||
if (!report.read) {
|
||||
report.read = true
|
||||
// 找到原始间谍报告对象并标记为已读(因为sortedSpyReports是副本)
|
||||
const originalReport = gameStore.player.spyReports.find(r => r.id === report.id)
|
||||
if (originalReport && !originalReport.read) {
|
||||
originalReport.read = true
|
||||
}
|
||||
}
|
||||
|
||||
// 打开被侦查通知
|
||||
const openSpiedNotification = (notification: SpiedNotification) => {
|
||||
// 标记为已读
|
||||
if (!notification.read) {
|
||||
notification.read = true
|
||||
// 找到原始通知对象并标记为已读(因为sortedSpiedNotifications是副本)
|
||||
const originalNotification = gameStore.player.spiedNotifications?.find(n => n.id === notification.id)
|
||||
if (originalNotification && !originalNotification.read) {
|
||||
originalNotification.read = true
|
||||
}
|
||||
// 设置选中的通知并打开详情对话框
|
||||
selectedSpiedNotification.value = notification
|
||||
@@ -811,9 +1084,10 @@
|
||||
|
||||
// 打开NPC活动通知
|
||||
const openNPCActivityNotification = (notification: NPCActivityNotification) => {
|
||||
// 标记为已读
|
||||
if (!notification.read) {
|
||||
notification.read = true
|
||||
// 找到原始通知对象并标记为已读(因为sortedNPCActivityNotifications是副本)
|
||||
const originalNotification = gameStore.player.npcActivityNotifications?.find(n => n.id === notification.id)
|
||||
if (originalNotification && !originalNotification.read) {
|
||||
originalNotification.read = true
|
||||
}
|
||||
// 设置选中的通知并打开详情对话框
|
||||
selectedNPCActivityNotification.value = notification
|
||||
@@ -869,8 +1143,10 @@
|
||||
|
||||
// 标记礼物通知为已读
|
||||
const markGiftAsRead = (gift: GiftNotification) => {
|
||||
if (!gift.read) {
|
||||
gift.read = true
|
||||
// 找到原始礼物通知对象并标记为已读(因为gifts是副本)
|
||||
const originalGift = gameStore.player.giftNotifications?.find(g => g.id === gift.id)
|
||||
if (originalGift && !originalGift.read) {
|
||||
originalGift.read = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -903,8 +1179,10 @@
|
||||
|
||||
// 标记礼物被拒绝通知为已读
|
||||
const markGiftRejectedAsRead = (rejection: GiftRejectedNotification) => {
|
||||
if (!rejection.read) {
|
||||
rejection.read = true
|
||||
// 找到原始拒绝通知对象并标记为已读(因为rejections是副本)
|
||||
const originalRejection = gameStore.player.giftRejectedNotifications?.find(r => r.id === rejection.id)
|
||||
if (originalRejection && !originalRejection.read) {
|
||||
originalRejection.read = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -107,7 +107,10 @@
|
||||
</div>
|
||||
|
||||
<!-- 页面聚焦时不发送 -->
|
||||
<div class="flex items-center justify-between pl-4 border-l-2" :class="{ 'opacity-50 pointer-events-none': !gameStore.notificationSettings?.browser }">
|
||||
<div
|
||||
class="flex items-center justify-between pl-4 border-l-2"
|
||||
:class="{ 'opacity-50 pointer-events-none': !gameStore.notificationSettings?.browser }"
|
||||
>
|
||||
<Label class="font-normal">{{ t('settings.suppressInFocus') }}</Label>
|
||||
<Switch
|
||||
:checked="gameStore.notificationSettings?.suppressInFocus"
|
||||
@@ -123,10 +126,7 @@
|
||||
<h3 class="font-medium">{{ t('settings.inAppNotifications') }}</h3>
|
||||
<p class="text-sm text-muted-foreground">{{ t('settings.inAppNotificationsDesc') || t('settings.inAppNotifications') }}</p>
|
||||
</div>
|
||||
<Switch
|
||||
:checked="gameStore.notificationSettings?.inApp"
|
||||
@update:checked="(val: boolean) => updateInAppSetting(val)"
|
||||
/>
|
||||
<Switch :checked="gameStore.notificationSettings?.inApp" @update:checked="(val: boolean) => updateInAppSetting(val)" />
|
||||
</div>
|
||||
|
||||
<!-- 具体通知类型 -->
|
||||
@@ -138,7 +138,13 @@
|
||||
<div class="space-y-1">
|
||||
<h3 class="font-medium">{{ t('settings.notificationTypes') }}</h3>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{{ areMainSwitchesOff ? t('settings.notificationsDisabled') : (isTypesExpanded ? t('settings.collapseTypes') : t('settings.expandTypes')) }}
|
||||
{{
|
||||
areMainSwitchesOff
|
||||
? t('settings.notificationsDisabled')
|
||||
: isTypesExpanded
|
||||
? t('settings.collapseTypes')
|
||||
: t('settings.expandTypes')
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" class="h-8 w-8 p-0">
|
||||
@@ -168,6 +174,54 @@
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 帮助提示设置 -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('hints.hintsEnabled') }}</CardTitle>
|
||||
<CardDescription>{{ t('hints.hintsEnabledDesc') }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<!-- 启用/禁用提示 -->
|
||||
<div class="flex items-center justify-between p-4 border rounded-lg">
|
||||
<div class="space-y-1">
|
||||
<h3 class="font-medium">{{ t('hints.hintsEnabled') }}</h3>
|
||||
<p class="text-sm text-muted-foreground">{{ t('hints.hintsEnabledDesc') }}</p>
|
||||
</div>
|
||||
<Switch :checked="hintsEnabled" @update:checked="setHintsEnabled" />
|
||||
</div>
|
||||
|
||||
<!-- 重置提示 -->
|
||||
<div class="flex items-center justify-between p-4 border rounded-lg">
|
||||
<div class="space-y-1">
|
||||
<h3 class="font-medium">{{ t('hints.resetHints') }}</h3>
|
||||
<p class="text-sm text-muted-foreground">{{ t('hints.resetHintsDesc') }}</p>
|
||||
</div>
|
||||
<Button @click="handleResetHints" variant="outline">
|
||||
<RotateCcw class="mr-2 h-4 w-4" />
|
||||
{{ t('settings.reset') }}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 显示设置 -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('settings.displaySettings') }}</CardTitle>
|
||||
<CardDescription>{{ t('settings.displaySettingsDesc') }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<!-- 背景动画 -->
|
||||
<div class="flex items-center justify-between p-4 border rounded-lg">
|
||||
<div class="space-y-1">
|
||||
<h3 class="font-medium">{{ t('settings.backgroundAnimation') }}</h3>
|
||||
<p class="text-sm text-muted-foreground">{{ t('settings.backgroundAnimationDesc') }}</p>
|
||||
</div>
|
||||
<Switch :checked="gameStore.player.backgroundEnabled ?? false" @update:checked="updateBackgroundSetting" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 关于 -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -198,6 +252,11 @@
|
||||
<div class="pt-2 border-t space-y-2">
|
||||
<h3 class="text-sm font-medium">{{ t('settings.community') }}</h3>
|
||||
<div class="flex flex-col gap-2">
|
||||
<!-- Privacy Policy -->
|
||||
<Button variant="outline" class="w-full justify-start" @click="openPrivacy">
|
||||
<Shield class="mr-2 h-4 w-4" />
|
||||
{{ t('settings.privacyPolicy') }}
|
||||
</Button>
|
||||
<!-- GitHub -->
|
||||
<Button variant="outline" class="w-full justify-start" @click="openGithub">
|
||||
<ExternalLink class="mr-2 h-4 w-4" />
|
||||
@@ -234,6 +293,9 @@
|
||||
|
||||
<!-- 更新对话框 -->
|
||||
<UpdateDialog v-model:open="showUpdateDialog" :version-info="updateInfo" />
|
||||
|
||||
<!-- 隐私协议弹窗 -->
|
||||
<PrivacyDialog v-model:open="showPrivacyDialog" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -255,15 +317,31 @@
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { Download, Upload, Trash2, ExternalLink, MessagesSquare, Play, Pause, RefreshCw, ChevronDown, ChevronUp } from 'lucide-vue-next'
|
||||
import {
|
||||
Download,
|
||||
Upload,
|
||||
Trash2,
|
||||
ExternalLink,
|
||||
MessagesSquare,
|
||||
Play,
|
||||
Pause,
|
||||
RefreshCw,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
RotateCcw,
|
||||
Shield
|
||||
} from 'lucide-vue-next'
|
||||
import { saveAs } from 'file-saver'
|
||||
import { toast } from 'vue-sonner'
|
||||
import pkg from '../../package.json'
|
||||
import { checkLatestVersion, canCheckVersion } from '@/utils/versionCheck'
|
||||
import type { VersionInfo } from '@/utils/versionCheck'
|
||||
import UpdateDialog from '@/components/UpdateDialog.vue'
|
||||
import PrivacyDialog from '@/components/PrivacyDialog.vue'
|
||||
import { useHints } from '@/composables/useHints'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { hintsEnabled, setHintsEnabled, resetHints } = useHints()
|
||||
const gameStore = useGameStore()
|
||||
|
||||
const fileInputRef = ref<HTMLInputElement>()
|
||||
@@ -278,7 +356,7 @@
|
||||
|
||||
const isTypesExpanded = ref(false)
|
||||
|
||||
// Ensure notification settings exist
|
||||
// 确保通知设置存在
|
||||
if (!gameStore.notificationSettings) {
|
||||
gameStore.notificationSettings = {
|
||||
browser: false,
|
||||
@@ -293,7 +371,7 @@
|
||||
return !s?.browser && !s?.inApp
|
||||
})
|
||||
|
||||
// Auto-collapse if main switches are off
|
||||
// 当主开关关闭时自动折叠
|
||||
// watch(areMainSwitchesOff, (val) => {
|
||||
// if (val) isTypesExpanded.value = false
|
||||
// })
|
||||
@@ -310,13 +388,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
const updateTypeSetting = (key: string, val: boolean) => {
|
||||
const updateTypeSetting = (key: 'construction' | 'research', val: boolean) => {
|
||||
if (gameStore.notificationSettings) {
|
||||
gameStore.notificationSettings.types[key] = val
|
||||
}
|
||||
}
|
||||
|
||||
const toggleType = (key: string) => {
|
||||
const toggleType = (key: 'construction' | 'research') => {
|
||||
if (gameStore.notificationSettings) {
|
||||
const current = gameStore.notificationSettings.types[key]
|
||||
gameStore.notificationSettings.types[key] = !current
|
||||
@@ -385,6 +463,14 @@
|
||||
window.open(`https://qm.qq.com/q/${pkg.id}`, '_blank')
|
||||
}
|
||||
|
||||
// 隐私协议弹窗状态
|
||||
const showPrivacyDialog = ref(false)
|
||||
|
||||
// 打开隐私协议弹窗
|
||||
const openPrivacy = () => {
|
||||
showPrivacyDialog.value = true
|
||||
}
|
||||
|
||||
// 手动检查版本
|
||||
const showUpdateDialog = ref(false)
|
||||
const updateInfo = ref<VersionInfo | null>(null)
|
||||
@@ -514,6 +600,7 @@
|
||||
|
||||
// 清除数据
|
||||
const handleClearData = () => {
|
||||
gameStore.isPaused = true
|
||||
confirmTitle.value = t('settings.clearConfirmTitle')
|
||||
confirmMessage.value = t('settings.clearConfirmMessage')
|
||||
showConfirmDialog.value = true
|
||||
@@ -583,4 +670,15 @@
|
||||
fileInputRef.value.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 重置提示
|
||||
const handleResetHints = () => {
|
||||
resetHints()
|
||||
toast.success(t('hints.resetHints'))
|
||||
}
|
||||
|
||||
// 更新背景设置
|
||||
const updateBackgroundSetting = (val: boolean) => {
|
||||
gameStore.player.backgroundEnabled = val
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -6,15 +6,11 @@
|
||||
"module": "ESNext",
|
||||
"types": ["node"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
|
||||
@@ -9,14 +9,10 @@ const packageJsonPath = join(__dirname, 'package.json')
|
||||
try {
|
||||
// 读取 package.json
|
||||
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'))
|
||||
|
||||
// 更新构建日期
|
||||
packageJson.buildDate = new Date().toLocaleString()
|
||||
|
||||
// 写回 package.json (保持格式化,缩进2个空格)
|
||||
writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n', 'utf-8')
|
||||
|
||||
console.log(`✓ Build date updated: ${packageJson.buildDate}`)
|
||||
} catch (error) {
|
||||
console.error('Failed to update build date:', error)
|
||||
process.exit(1)
|
||||
|
||||
@@ -17,7 +17,7 @@ export default defineConfig(async () => {
|
||||
manifest: {
|
||||
name: pkg.name,
|
||||
short_name: pkg.title,
|
||||
description: '一款现代化 OGame 太空策略游戏',
|
||||
description: '征服星辰大海',
|
||||
theme_color: '#000000',
|
||||
background_color: '#000000',
|
||||
display: 'fullscreen',
|
||||
@@ -27,21 +27,13 @@ export default defineConfig(async () => {
|
||||
src: 'logo.svg',
|
||||
sizes: 'any',
|
||||
type: 'image/svg+xml',
|
||||
purpose: 'any',
|
||||
},
|
||||
],
|
||||
|
||||
purpose: 'any'
|
||||
}
|
||||
]
|
||||
},
|
||||
workbox: {
|
||||
// 关键:确保缓存了 docs 目录下所有的 JS, CSS, HTML 和 媒体文件
|
||||
globPatterns: ['**/*.{js,css,html,ico,png,svg,mp3,wav,json}'],
|
||||
|
||||
// 如果你的游戏资源(如音效或贴图)较大,请根据需要调大这个阈值(默认 2MB)
|
||||
// 这里设置为 5MB
|
||||
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
|
||||
|
||||
// 离线策略优化:对于游戏,我们希望即使在离线状态下,
|
||||
// 所有的资源都能立刻从缓存加载,而不是去请求网络
|
||||
runtimeCaching: [
|
||||
{
|
||||
urlPattern: ({ request }) => request.destination === 'image' || request.destination === 'audio',
|
||||
@@ -50,14 +42,14 @@ export default defineConfig(async () => {
|
||||
cacheName: 'game-assets',
|
||||
expiration: {
|
||||
maxEntries: 100,
|
||||
maxAgeSeconds: 30 * 24 * 60 * 60, // 缓存 30 天
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
maxAgeSeconds: 30 * 24 * 60 * 60 // 缓存 30 天
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
];
|
||||
]
|
||||
|
||||
// 只在 ELECTRON_BUILD 环境变量存在时才加载 Electron 插件
|
||||
if (process.env.ELECTRON_BUILD) {
|
||||
@@ -65,9 +57,9 @@ export default defineConfig(async () => {
|
||||
const { default: electron } = await import('vite-plugin-electron/simple')
|
||||
const electronPlugins = await electron({
|
||||
main: {
|
||||
entry: 'electron/main.ts',
|
||||
entry: 'electron/main.ts'
|
||||
},
|
||||
renderer: {},
|
||||
renderer: {}
|
||||
})
|
||||
plugins.push(...electronPlugins)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user