diff --git a/.github/ISSUE_TEMPLATE/BUG反馈.md b/.github/ISSUE_TEMPLATE/BUG反馈.md deleted file mode 100644 index 217e6f5..0000000 --- a/.github/ISSUE_TEMPLATE/BUG反馈.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -name: BUG反馈 -about: 报告项目中发现的缺陷或问题 -title: '[BUG] 简要描述问题' -labels: 'bug' -assignees: '' ---- - -**问题描述** -清晰准确地描述遇到的问题 - -**重现步骤** - -1. 第一步操作 -2. 第二步操作 -3. 出现问题的操作 - -**期望行为** -描述您认为正确的行为应该是怎样的 - -**实际行为** -描述实际发生的错误行为 - -**环境信息** - -- 操作系统: -- 浏览器(如适用): -- 项目版本: - -**截图或日志(可选)** -如果有错误截图或日志,请提供 diff --git a/.github/ISSUE_TEMPLATE/功能请求.md b/.github/ISSUE_TEMPLATE/功能请求.md deleted file mode 100644 index ea39f99..0000000 --- a/.github/ISSUE_TEMPLATE/功能请求.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -name: 功能请求 -about: 请求添加新功能或改进现有功能 -title: '[功能] 简要描述功能' -labels: 'enhancement' -assignees: '' ---- - -**功能描述** -清晰描述您希望添加的功能 - -**功能背景** -说明为什么需要这个功能,它能解决什么问题 - -**建议实现方案(可选)** -如果有具体的实现想法,可以在这里描述 - -**附加信息** -任何其他有助于理解这个功能的信息 diff --git a/.github/ISSUE_TEMPLATE/反馈建议.md b/.github/ISSUE_TEMPLATE/反馈建议.md deleted file mode 100644 index 493ee03..0000000 --- a/.github/ISSUE_TEMPLATE/反馈建议.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -name: 反馈建议 -about: 为这个项目提出功能建议或改进意见 -title: '[建议] 简要描述您的建议' -labels: 'enhancement' -assignees: '' ---- - -**您的建议是什么?** -请清晰描述您希望添加的功能或改进点 - -**为什么需要这个功能/改进?** -说明这个建议会解决什么问题或带来什么价值 - -**您期望的实现方式(可选)** -如果有具体的实现想法,可以在这里描述 - -**附加信息(可选)** -任何其他有助于理解这个建议的信息 diff --git a/.github/ISSUE_TEMPLATE/文档改进.md b/.github/ISSUE_TEMPLATE/文档改进.md deleted file mode 100644 index b89f864..0000000 --- a/.github/ISSUE_TEMPLATE/文档改进.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -name: 文档改进 -about: 报告文档问题或建议改进 -title: '[文档] 简要描述问题' -labels: 'documentation' -assignees: '' ---- - -**文档位置** -指出需要改进的文档路径或 URL - -**当前问题** -描述当前文档存在的问题或不清晰的地方 - -**改进建议** -提出具体的改进建议 - -**附加信息(可选)** -任何其他有助于改进文档的信息 diff --git a/.github/workflows/ogame-vue-ts.yml b/.github/workflows/ogame-vue-ts.yml index 5779926..55cbadc 100644 --- a/.github/workflows/ogame-vue-ts.yml +++ b/.github/workflows/ogame-vue-ts.yml @@ -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 \ No newline at end of file + outputs: type=image,name=target,annotation-index.org.opencontainers.image.description=OGame Vue Ts \ No newline at end of file diff --git a/README-EN.md b/README-EN.md index 7dd4843..9174c9a 100644 --- a/README-EN.md +++ b/README-EN.md @@ -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/). diff --git a/README.md b/README.md index a7d82de..edb5470 100644 --- a/README.md +++ b/README.md @@ -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/) 进行许可。 diff --git a/package-lock.json b/package-lock.json index b981153..2e4028d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8187,4 +8187,4 @@ } } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index f5c1afb..be571aa 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/App.vue b/src/App.vue index 22b55e3..21c17c9 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,7 +1,13 @@ @@ -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 | 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(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 | null>(null) + const pointsUpdateInterval = ref | null>(null) + const konamiCleanup = ref<(() => void) | null>(null) + const versionCheckInterval = ref | null>(null) // 重命名星球相关状态 + const renameDialogOpen = ref(false) + const renamingPlanetId = ref(null) + const newPlanetName = ref('') + // 功能解锁要求配置 + const featureRequirements: Record = { + '/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,7 +1149,20 @@ // 如果生成残骸场,添加到宇宙残骸场列表 if (attackResult.debrisField) { - universeStore.debrisFields[attackResult.debrisField.id] = 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 + } } } @@ -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 = {} + 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 | null = null - let pointsUpdateInterval: ReturnType | null = null - let konamiCleanup: (() => void) | null = null - let versionCheckInterval: ReturnType | 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 = { - '/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) + }) diff --git a/src/components/ui/dialog/DialogContent.vue b/src/components/ui/dialog/DialogContent.vue index 61c8de3..38b0f99 100644 --- a/src/components/ui/dialog/DialogContent.vue +++ b/src/components/ui/dialog/DialogContent.vue @@ -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 ) " diff --git a/src/components/ui/dialog/DialogOverlay.vue b/src/components/ui/dialog/DialogOverlay.vue index 6ca56f2..20738b2 100644 --- a/src/components/ui/dialog/DialogOverlay.vue +++ b/src/components/ui/dialog/DialogOverlay.vue @@ -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 ) " diff --git a/src/components/ui/dialog/ScrollableDialogContent.vue b/src/components/ui/dialog/ScrollableDialogContent.vue index 678eb47..f620f45 100644 --- a/src/components/ui/dialog/ScrollableDialogContent.vue +++ b/src/components/ui/dialog/ScrollableDialogContent.vue @@ -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 ) " diff --git a/src/components/ui/pagination/FixedPagination.vue b/src/components/ui/pagination/FixedPagination.vue new file mode 100644 index 0000000..5ae060d --- /dev/null +++ b/src/components/ui/pagination/FixedPagination.vue @@ -0,0 +1,93 @@ + + + diff --git a/src/components/ui/pagination/index.ts b/src/components/ui/pagination/index.ts index 8eba5fe..94b76f1 100644 --- a/src/components/ui/pagination/index.ts +++ b/src/components/ui/pagination/index.ts @@ -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' diff --git a/src/components/ui/sidebar/Sidebar.vue b/src/components/ui/sidebar/Sidebar.vue index e53c68f..ee89f3a 100644 --- a/src/components/ui/sidebar/Sidebar.vue +++ b/src/components/ui/sidebar/Sidebar.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) } } diff --git a/src/components/ui/sidebar/SidebarProvider.vue b/src/components/ui/sidebar/SidebarProvider.vue index 8ae3e6d..c425d31 100644 --- a/src/components/ui/sidebar/SidebarProvider.vue +++ b/src/components/ui/sidebar/SidebarProvider.vue @@ -16,18 +16,11 @@ + + diff --git a/src/views/FleetView.vue b/src/views/FleetView.vue index 8014737..e928f7a 100644 --- a/src/views/FleetView.vue +++ b/src/views/FleetView.vue @@ -8,11 +8,9 @@ - {{ t('fleetView.fleetOverview') }} - {{ t('fleetView.sendFleet') }} - - {{ t('fleetView.flightMissions') }} - + + {{ t(`fleetView.${tab.labelKey}`) }} + {{ gameStore.player.fleetMissions.length }} @@ -97,17 +95,9 @@
-
- - -
-
- - -
-
- - +
+ +
@@ -157,52 +147,17 @@
-
- - -
-
-
-
+

{{ t('fleetView.carryingResources') }}:

- - - {{ formatNumber(mission.cargo.metal) }} - - - - {{ formatNumber(mission.cargo.crystal) }} - - - - {{ formatNumber(mission.cargo.deuterium) }} - - - - {{ formatNumber(mission.cargo.darkMatter) }} - +
@@ -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>({ [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.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()) diff --git a/src/views/GMView.vue b/src/views/GMView.vue index 68c93e5..7e4eed8 100644 --- a/src/views/GMView.vue +++ b/src/views/GMView.vue @@ -77,110 +77,36 @@ - - + + - {{ t('gmView.modifyBuildings') }} - {{ t('gmView.buildingsDesc') }} + {{ t(section.titleKey) }} + {{ t(section.descKey) }}
-
- +
+
- - - -
-
-
- - - - - - - - - {{ t('gmView.modifyResearch') }} - {{ t('gmView.researchDesc') }} - - -
-
- -
- - - -
-
-
-
-
-
- - - - - - {{ t('gmView.modifyShips') }} - {{ t('gmView.shipsDesc') }} - - -
-
- -
- - - -
-
-
-
-
-
- - - - - - {{ t('gmView.modifyDefense') }} - {{ t('gmView.defenseDesc') }} - - -
-
- -
- - - -
-
-
-
-
-
- - - - - - {{ t('gmView.modifyOfficers') }} - {{ t('gmView.officersDesc') }} - - -
-
- -
- - - - + +
@@ -204,7 +130,9 @@ - {{ npc.name }} ({{ npc.difficulty }}) + + {{ npc.name }} ({{ t(`diplomacy.diagnostic.difficultyLevels.${npc.difficulty}`) }}) +
@@ -293,6 +221,9 @@ {{ alertDialogMessage }} + + {{ alertDialogTitle }} + {{ t('common.confirm') }} @@ -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) => { - if (selectedPlanet.value) { - selectedPlanet.value.buildings[building] = level - updatePlayerPoints() - } + // 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 setTechnologyLevel = (tech: TechnologyType, level: number) => { - gameStore.player.technologies[tech] = level - updatePlayerPoints() - } - - const setShipCount = (ship: ShipType, count: number) => { - if (selectedPlanet.value) { - selectedPlanet.value.fleet[ship] = (selectedPlanet.value.fleet[ship] || 0) + count - updatePlayerPoints() - } - } - - const setDefenseCount = (defense: DefenseType, count: number) => { - if (selectedPlanet.value) { - selectedPlanet.value.defense[defense] = (selectedPlanet.value.defense[defense] || 0) + count - updatePlayerPoints() - } - } - - const setOfficerDays = (officer: OfficerType, days: number) => { - officerDays.value[officer] = days - const now = Date.now() - const expiresAt = now + days * 24 * 60 * 60 * 1000 - - if (!gameStore.player.officers[officer]) { - gameStore.player.officers[officer] = { - type: officer, - active: true, - hiredAt: now, - expiresAt: expiresAt + const gmSections = computed(() => [ + { + 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[item] = val + updatePlayerPoints() + } + }, + onButtonClick: (item: BuildingType, val: number) => { + if (selectedPlanet.value) { + selectedPlanet.value.buildings[item] = val + updatePlayerPoints() + } } - } 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 + }, + { + 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.fleet[item] = val + updatePlayerPoints() + } + }, + onButtonClick: (item: ShipType, val: number) => { + if (selectedPlanet.value) { + selectedPlanet.value.fleet[item] = (selectedPlanet.value.fleet[item] || 0) + val + updatePlayerPoints() + } + } + }, + { + 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[item]) { + gameStore.player.officers[item] = { + type: item, + active: true, + hiredAt: now, + expiresAt: expiresAt + } + } else { + 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 = () => { diff --git a/src/views/GalaxyView.vue b/src/views/GalaxyView.vue index f698a88..e5c43c7 100644 --- a/src/views/GalaxyView.vue +++ b/src/views/GalaxyView.vue @@ -205,16 +205,30 @@
-

{{ slot.planet.name }}

+

+ {{ isMyPlanet(slot.planet) ? slot.planet.name : getNpcPlanetDisplayName(slot.planet) }} +

[{{ slot.planet.position.galaxy }}:{{ slot.planet.position.system }}:{{ slot.planet.position.position }}] {{ t('galaxyView.mine') }} - - {{ getRelationStatusText(slot.planet) }} - + + + + {{ getRelationStatusText(slot.planet) }} + + + +

+ {{ t('diplomacy.reputation') }}: + + {{ getReputationValue(slot.planet)! > 0 ? '+' : '' }}{{ getReputationValue(slot.planet) }} + +

+
+
- +

{{ t('galaxyView.debrisField') }}

@@ -247,13 +261,6 @@
- -
- {{ t('diplomacy.reputation') }}: - - {{ getReputationValue(slot.planet)! > 0 ? '+' : '' }}{{ getReputationValue(slot.planet) }} - -
@@ -389,11 +396,27 @@
-

{{ slot.planet.name }}

+

+ {{ isMyPlanet(slot.planet) ? slot.planet.name : getNpcPlanetDisplayName(slot.planet) }} +

{{ t('galaxyView.mine') }} - - {{ getRelationStatusText(slot.planet) }} - + + + + + {{ getRelationStatusText(slot.planet) }} + + + +

+ {{ t('diplomacy.reputation') }}: + + {{ getReputationValue(slot.planet)! > 0 ? '+' : '' }}{{ getReputationValue(slot.planet) }} + +

+
+
+
@@ -432,13 +455,6 @@

[{{ slot.planet.position.galaxy }}:{{ slot.planet.position.system }}:{{ slot.planet.position.position }}]

- -
- {{ t('diplomacy.reputation') }}: - - {{ getReputationValue(slot.planet)! > 0 ? '+' : '' }}{{ getReputationValue(slot.planet) }} - -
@@ -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 diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue new file mode 100644 index 0000000..1b17ebb --- /dev/null +++ b/src/views/HomeView.vue @@ -0,0 +1,159 @@ + + + + + diff --git a/src/views/MessagesView.vue b/src/views/MessagesView.vue index b759e6b..09a6083 100644 --- a/src/views/MessagesView.vue +++ b/src/views/MessagesView.vue @@ -1,6 +1,36 @@ @@ -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() @@ -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(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 + } diff --git a/tsconfig.node.json b/tsconfig.node.json index 38c425a..f8f92bf 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -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, diff --git a/update-build-date.js b/update-build-date.js index 5d2edfd..50ea887 100644 --- a/update-build-date.js +++ b/update-build-date.js @@ -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) diff --git a/vite.config.ts b/vite.config.ts index 5487b59..9d4636b 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -10,14 +10,14 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)) export default defineConfig(async () => { const plugins = [ - vue(), + vue(), tailwindcss(), VitePWA({ registerType: 'autoUpdate', 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) }