feat: 新增Android平台支持及构建流程

集成Android平台相关目录与配置文件,包含Gradle构建脚本、资源文件、启动图标、Java入口、Proguard规则等,完善.gitignore以排除Android构建产物。更新CI流程,支持自动构建并发布Android APK。移除README中项目结构说明,简化文档。
This commit is contained in:
谦君
2025-12-20 00:48:36 +08:00
parent 20fb2bb6a4
commit 1368bb4445
97 changed files with 7859 additions and 335 deletions

View File

@@ -0,0 +1,343 @@
<template>
<div class="container mx-auto p-4 sm:p-6 space-y-4 sm:space-y-6">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<h1 class="text-2xl sm:text-3xl font-bold">{{ t('achievements.title') }}</h1>
<div class="flex items-center gap-2">{{ unlockedCount }} / {{ totalCount }} {{ t('achievements.unlocked') }}</div>
</div>
<!-- 分类标签 -->
<Tabs v-model="activeCategory" class="w-full">
<TabsList class="w-full grid grid-cols-5 h-10">
<TabsTrigger v-for="category in categories" :key="category.value" :value="category.value" class="text-xs sm:text-sm">
{{ t(`achievements.categories.${category.value}`) }}
<Badge v-if="getCategoryUnlockedCount(category.value) > 0" class="ml-1 h-5 px-1.5 text-[10px] bg-primary text-primary-foreground">
{{ getCategoryUnlockedCount(category.value) }}
</Badge>
</TabsTrigger>
</TabsList>
<!-- 成就卡片网格 -->
<TabsContent v-for="category in categories" :key="category.value" :value="category.value" class="mt-4">
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
<Card v-for="achievement in getAchievementsByCategory(category.value)" :key="achievement.id" class="relative overflow-hidden">
<!-- 等级指示条 -->
<div class="absolute top-0 left-0 right-0 h-1 flex">
<div v-for="tier in tierOrder" :key="tier" class="flex-1" :class="getTierBarClass(achievement.id, tier)" />
</div>
<CardHeader class="pt-4">
<div class="flex items-start gap-3">
<div class="p-2 rounded-lg" :class="getIconBgClass(achievement.id)">
<component :is="getIcon(achievement.icon)" class="h-6 w-6" :class="getIconClass(achievement.id)" />
</div>
<div class="flex-1 min-w-0">
<CardTitle class="text-sm sm:text-base flex items-center gap-2">
{{ t(`achievements.names.${achievement.id}`) }}
<Badge v-if="getCurrentTier(achievement.id)" :class="getTierBadgeClass(getCurrentTier(achievement.id)!)">
{{ t(`achievements.tiers.${getCurrentTier(achievement.id)}`) }}
</Badge>
</CardTitle>
<CardDescription class="text-xs mt-1">
{{ t(`achievements.descriptions.${achievement.id}`) }}
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent class="space-y-3">
<!-- 进度条 -->
<div class="space-y-1">
<div class="flex justify-between text-xs">
<span class="text-muted-foreground">{{ t('achievements.progress') }}</span>
<span class="font-medium">
{{ formatNumber(getCurrentValue(achievement.id)) }} /
{{ formatNumber(getNextTarget(achievement.id) || getCurrentValue(achievement.id)) }}
</span>
</div>
<Progress :model-value="getProgressPercentage(achievement.id)" class="h-2" />
</div>
<!-- 下一等级奖励 -->
<div v-if="getNextTierConfig(achievement.id)" class="p-2 bg-muted/50 rounded-lg">
<p class="text-xs text-muted-foreground mb-1">
{{ t('achievements.nextTier') }}: {{ t(`achievements.tiers.${getNextTierConfig(achievement.id)!.tier}`) }}
</p>
<div class="flex items-center gap-3 text-xs">
<div v-if="getNextTierConfig(achievement.id)!.reward.darkMatter" class="flex items-center gap-1">
<Sparkles class="h-3 w-3 text-purple-500" />
<span>+{{ formatNumber(getNextTierConfig(achievement.id)!.reward.darkMatter!) }}</span>
</div>
<div v-if="getNextTierConfig(achievement.id)!.reward.points" class="flex items-center gap-1">
<Star class="h-3 w-3 text-yellow-500" />
<span>+{{ formatNumber(getNextTierConfig(achievement.id)!.reward.points!) }}</span>
</div>
</div>
</div>
<!-- 已达最高等级 -->
<div
v-else-if="getCurrentTier(achievement.id) === 'diamond'"
class="p-2 bg-gradient-to-r from-purple-500/10 to-blue-500/10 rounded-lg"
>
<p class="text-xs text-center font-medium text-purple-600 dark:text-purple-400">
{{ t('achievements.maxTierReached') }}
</p>
</div>
</CardContent>
</Card>
</div>
</TabsContent>
</Tabs>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { useGameStore } from '@/stores/gameStore'
import { useI18n } from '@/composables/useI18n'
import { formatNumber } from '@/utils/format'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Progress } from '@/components/ui/progress'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { AchievementCategory, AchievementTier, type AchievementTierConfig } from '@/types/game'
import { ACHIEVEMENTS, ACHIEVEMENT_MAP, TIER_ORDER, getNextTier } from '@/config/achievementConfig'
import { getAchievementProgress } from '@/logic/achievementLogic'
import {
Sparkles,
Star,
Gem,
Diamond,
Droplet,
Flame,
Building2,
FlaskConical,
Rocket,
Shield,
Swords,
Crown,
ShieldCheck,
Bomb,
Trash2,
Skull,
ShieldOff,
Plane,
Truck,
Package,
Flag,
Eye,
ArrowDownToLine,
Compass,
Sparkle,
Recycle,
Pickaxe,
Zap,
Fuel,
Handshake as HandshakeIcon,
Angry,
Gift,
HeartHandshake,
Target,
ScanEye,
Banknote,
BadgeDollarSign
} from 'lucide-vue-next'
const { t } = useI18n()
const gameStore = useGameStore()
const activeCategory = ref<AchievementCategory>(AchievementCategory.Resource)
const categories = [
{ value: AchievementCategory.Resource },
{ value: AchievementCategory.Building },
{ value: AchievementCategory.Combat },
{ value: AchievementCategory.Mission },
{ value: AchievementCategory.Diplomacy }
]
const tierOrder = TIER_ORDER
// 图标映射
const iconMap: Record<string, any> = {
Gem,
Diamond,
Droplet,
Sparkles,
Flame,
Building2,
FlaskConical,
Rocket,
Shield,
Swords,
Crown,
ShieldCheck,
Bomb,
Trash2,
Skull,
ShieldOff,
Plane,
Truck,
Package,
Flag,
Eye,
ArrowDownToLine,
Compass,
Sparkle,
Recycle,
Pickaxe,
Zap,
Fuel,
HandshakeIcon,
Angry,
Gift,
HeartHandshake,
Target,
ScanEye,
Banknote,
BadgeDollarSign
}
const getIcon = (iconName: string) => {
return iconMap[iconName] || Sparkles
}
// 获取成就进度
const getProgress = (achievementId: string) => {
return gameStore.player.achievements?.[achievementId]
}
const getCurrentTier = (achievementId: string) => {
return getProgress(achievementId)?.currentTier || null
}
const getCurrentValue = (achievementId: string) => {
return getProgress(achievementId)?.currentValue || 0
}
const getNextTarget = (achievementId: string) => {
const config = ACHIEVEMENT_MAP[achievementId]
if (!config) return null
const currentTier = getCurrentTier(achievementId)
const nextTier = getNextTier(currentTier)
if (!nextTier) return null
const tierConfig = config.tiers.find(t => t.tier === nextTier)
return tierConfig?.target ?? null
}
const getNextTierConfig = (achievementId: string): AchievementTierConfig | null => {
const config = ACHIEVEMENT_MAP[achievementId]
if (!config) return null
const currentTier = getCurrentTier(achievementId)
const nextTier = getNextTier(currentTier)
if (!nextTier) return null
return config.tiers.find(t => t.tier === nextTier) || null
}
const getProgressPercentage = (achievementId: string) => {
const currentValue = getCurrentValue(achievementId)
const currentTier = getCurrentTier(achievementId)
return getAchievementProgress(achievementId, currentValue, currentTier)
}
// 按类别获取成就
const getAchievementsByCategory = (category: AchievementCategory) => {
return ACHIEVEMENTS.filter(a => a.category === category)
}
// 统计
const unlockedCount = computed(() => {
if (!gameStore.player.achievements) return 0
return Object.values(gameStore.player.achievements).filter(p => p.currentTier !== null).length
})
const totalCount = computed(() => ACHIEVEMENTS.length)
const getCategoryUnlockedCount = (category: AchievementCategory) => {
if (!gameStore.player.achievements) return 0
const categoryAchievements = ACHIEVEMENTS.filter(a => a.category === category)
return categoryAchievements.filter(a => {
const progress = gameStore.player.achievements?.[a.id]
return progress?.currentTier !== null
}).length
}
// 样式函数
const getTierBarClass = (achievementId: string, tier: AchievementTier) => {
const progress = getProgress(achievementId)
if (!progress) return 'bg-muted'
const tierUnlock = progress.tierUnlocks[tier]
if (tierUnlock !== null) {
// 已解锁
switch (tier) {
case AchievementTier.Bronze:
return 'bg-amber-600'
case AchievementTier.Silver:
return 'bg-gray-400'
case AchievementTier.Gold:
return 'bg-yellow-500'
case AchievementTier.Platinum:
return 'bg-cyan-400'
case AchievementTier.Diamond:
return 'bg-purple-500'
}
}
return 'bg-muted'
}
const getTierBadgeClass = (tier: AchievementTier) => {
switch (tier) {
case AchievementTier.Bronze:
return 'bg-amber-600 text-white'
case AchievementTier.Silver:
return 'bg-gray-400 text-white'
case AchievementTier.Gold:
return 'bg-yellow-500 text-black'
case AchievementTier.Platinum:
return 'bg-cyan-400 text-black'
case AchievementTier.Diamond:
return 'bg-gradient-to-r from-purple-500 to-blue-500 text-white'
}
}
const getIconBgClass = (achievementId: string) => {
const tier = getCurrentTier(achievementId)
if (!tier) return 'bg-muted'
switch (tier) {
case AchievementTier.Bronze:
return 'bg-amber-100 dark:bg-amber-900/30'
case AchievementTier.Silver:
return 'bg-gray-100 dark:bg-gray-800'
case AchievementTier.Gold:
return 'bg-yellow-100 dark:bg-yellow-900/30'
case AchievementTier.Platinum:
return 'bg-cyan-100 dark:bg-cyan-900/30'
case AchievementTier.Diamond:
return 'bg-purple-100 dark:bg-purple-900/30'
}
}
const getIconClass = (achievementId: string) => {
const tier = getCurrentTier(achievementId)
if (!tier) return 'text-muted-foreground'
switch (tier) {
case AchievementTier.Bronze:
return 'text-amber-600'
case AchievementTier.Silver:
return 'text-gray-500'
case AchievementTier.Gold:
return 'text-yellow-600'
case AchievementTier.Platinum:
return 'text-cyan-500'
case AchievementTier.Diamond:
return 'text-purple-500'
}
}
</script>

View File

@@ -194,6 +194,7 @@
import * as buildingValidation from '@/logic/buildingValidation'
import * as publicLogic from '@/logic/publicLogic'
import * as officerLogic from '@/logic/officerLogic'
import * as gameLogic from '@/logic/gameLogic'
const gameStore = useGameStore()
const detailDialog = useDetailDialogStore()
@@ -227,8 +228,9 @@
return (Object.values(BuildingType) as BuildingType[]).filter(buildingType => {
const config = BUILDINGS.value[buildingType]
if (planet.value!.isMoon) {
// 月球只能建造月球专属建筑
return config.moonOnly === true
// 月球可以建造月球专属建筑 + 非行星专属建筑(如机器人工厂、船坞、机库等)
// OGame规则月球不能建造 planetOnly 的建筑(矿场、研究实验室、纳米工厂等)
return config.planetOnly !== true
} else {
// 行星不能建造月球专属建筑
return config.moonOnly !== true
@@ -245,6 +247,12 @@
gameStore.player.officers
)
if (!validation.valid) return { success: false, reason: validation.reason }
// 追踪资源消耗(在扣除前计算成本)
const currentLevel = gameStore.currentPlanet.buildings[buildingType] || 0
const cost = buildingLogic.calculateBuildingCost(buildingType, currentLevel + 1)
gameLogic.trackResourceConsumption(gameStore.player, cost)
const queueItem = buildingValidation.executeBuildingUpgrade(gameStore.currentPlanet, buildingType, gameStore.player.officers)
gameStore.currentPlanet.buildQueue.push(queueItem)
return { success: true }

View File

@@ -180,6 +180,7 @@
import * as publicLogic from '@/logic/publicLogic'
import * as shipValidation from '@/logic/shipValidation'
import * as shipLogic from '@/logic/shipLogic'
import * as gameLogic from '@/logic/gameLogic'
const gameStore = useGameStore()
const detailDialog = useDetailDialogStore()
@@ -236,6 +237,11 @@
if (!currentPlanet) return { success: false }
const validation = shipValidation.validateDefenseBuild(currentPlanet, defenseType, quantity, gameStore.player.technologies)
if (!validation.valid) return { success: false, reason: validation.reason }
// 追踪资源消耗(在扣除前计算成本)
const totalCost = shipLogic.calculateDefenseCost(defenseType, quantity)
gameLogic.trackResourceConsumption(gameStore.player, totalCost)
const queueItem = shipValidation.executeDefenseBuild(currentPlanet, defenseType, quantity, gameStore.player.officers)
currentPlanet.buildQueue.push(queueItem)
return { success: true }

View File

@@ -7,12 +7,13 @@
<!-- 标签切换 -->
<Tabs v-model="activeTab" class="w-full">
<TabsList class="grid w-full grid-cols-3">
<TabsTrigger v-for="tab in fleetTabs" :key="tab.value" :value="tab.value">
<TabsList :class="['grid', 'w-full', showJumpGateTab ? 'grid-cols-4' : 'grid-cols-3']">
<TabsTrigger v-for="tab in visibleTabs" :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>
<Badge v-if="tab.value === 'jumpGate' && jumpGateReady" variant="default" class="ml-1"></Badge>
</TabsTrigger>
</TabsList>
@@ -59,6 +60,80 @@
</CardContent>
</Card>
<!-- 舰队预设 -->
<Card>
<CardHeader>
<div class="flex justify-between items-center">
<div>
<CardTitle>{{ t('fleetView.fleetPresets') }}</CardTitle>
<CardDescription>{{ t('fleetView.fleetPresetsDescription') }}</CardDescription>
</div>
<Button @click="saveAsPreset" variant="outline" size="sm" :disabled="fleetPresets.length >= MAX_PRESETS">
<Save class="h-4 w-4 mr-1" />
{{ t('fleetView.savePreset') }}
</Button>
</div>
</CardHeader>
<CardContent>
<div v-if="fleetPresets.length === 0" class="text-center py-4 text-muted-foreground text-sm">
{{ t('fleetView.noPresets') }}
</div>
<div v-else class="space-y-2">
<div
v-for="preset in fleetPresets"
:key="preset.id"
class="flex items-center justify-between p-3 border rounded-lg hover:bg-muted/50 transition-colors"
:class="{ 'ring-2 ring-primary': editingPresetId === preset.id }"
>
<div class="flex-1 cursor-pointer" @click="loadPreset(preset)">
<div class="flex items-center gap-2">
<Star class="h-4 w-4 text-yellow-500" />
<span class="font-medium">{{ preset.name }}</span>
</div>
<div class="text-xs text-muted-foreground mt-1 flex flex-wrap gap-2">
<span v-if="preset.targetPosition">
[{{ preset.targetPosition.galaxy }}:{{ preset.targetPosition.system }}:{{ preset.targetPosition.position }}]
</span>
<span v-if="preset.missionType">
{{ getMissionName(preset.missionType) }}
</span>
<span>{{ Object.entries(preset.fleet).filter(([_, count]) => count > 0).length }} {{ t('fleetView.shipTypes') }}</span>
</div>
</div>
<div class="flex items-center gap-1">
<Button v-if="editingPresetId === preset.id" @click="updatePreset(preset.id)" variant="default" size="sm">
{{ t('common.save') }}
</Button>
<Button
v-if="editingPresetId !== preset.id"
@click="editingPresetId = preset.id"
variant="ghost"
size="sm"
:title="t('fleetView.editPreset')"
>
<Pencil class="h-4 w-4" />
</Button>
<Button @click.stop="startRenamePreset(preset)" variant="ghost" size="sm" :title="t('fleetView.renamePreset')">
<Type class="h-4 w-4" />
</Button>
<Button
@click.stop="deletePreset(preset.id)"
variant="ghost"
size="sm"
class="text-destructive hover:text-destructive"
:title="t('fleetView.deletePreset')"
>
<Trash2 class="h-4 w-4" />
</Button>
</div>
</div>
</div>
<p v-if="editingPresetId" class="text-xs text-muted-foreground mt-2">
{{ t('fleetView.editingPresetHint') }}
</p>
</CardContent>
</Card>
<!-- 选择舰队 -->
<Card>
<CardHeader>
@@ -93,13 +168,27 @@
<CardHeader>
<CardTitle>{{ t('fleetView.targetCoordinates') }}</CardTitle>
</CardHeader>
<CardContent>
<CardContent class="space-y-4">
<div class="grid grid-cols-3 gap-2 sm:gap-4">
<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>
<!-- 目标类型选择行星/月球 -->
<div v-if="hasMoonAtTargetPosition" class="flex items-center gap-4 p-3 bg-muted/50 rounded-lg">
<span class="text-sm font-medium">{{ t('fleetView.targetType') }}:</span>
<div class="flex gap-2">
<Button @click="targetIsMoon = false" :variant="!targetIsMoon ? 'default' : 'outline'" size="sm">
<Globe class="h-4 w-4 mr-1" />
{{ t('fleetView.planet') }}
</Button>
<Button @click="targetIsMoon = true" :variant="targetIsMoon ? 'default' : 'outline'" size="sm">
<Moon class="h-4 w-4 mr-1" />
{{ t('fleetView.moon') }}
</Button>
</div>
</div>
</CardContent>
</Card>
@@ -278,6 +367,108 @@
</CardContent>
</Card>
</TabsContent>
<!-- 跳跃门 -->
<TabsContent v-if="showJumpGateTab" value="jumpGate" class="mt-4 space-y-4">
<!-- 跳跃门状态 -->
<Card>
<CardHeader>
<CardTitle class="flex items-center gap-2">
<Zap class="h-5 w-5" />
{{ t('fleetView.jumpGate') }}
</CardTitle>
<CardDescription>{{ t('fleetView.jumpGateDescription') }}</CardDescription>
</CardHeader>
<CardContent>
<!-- 冷却状态 -->
<div v-if="!jumpGateReady" class="p-4 bg-muted/50 rounded-lg">
<div class="flex items-center gap-2 text-amber-600 dark:text-amber-400">
<Clock class="h-4 w-4" />
<span class="font-medium">{{ t('fleetView.jumpGateCooldown') }}</span>
</div>
<div class="mt-2 flex items-center gap-2">
<span class="text-sm text-muted-foreground">{{ t('fleetView.jumpGateCooldownRemaining') }}:</span>
<span class="font-bold">{{ formatTime(Math.floor(jumpGateCooldownRemaining / 1000)) }}</span>
</div>
<Progress :model-value="100 - (jumpGateCooldownRemaining / 3600000) * 100" class="mt-2" />
</div>
<!-- 就绪状态 -->
<div v-else class="p-4 bg-green-500/10 rounded-lg">
<div class="flex items-center gap-2 text-green-600 dark:text-green-400">
<Check class="h-4 w-4" />
<span class="font-medium">{{ t('fleetView.jumpGateReady') }}</span>
</div>
</div>
</CardContent>
</Card>
<!-- 选择目标月球 -->
<Card v-if="jumpGateReady">
<CardHeader>
<CardTitle>{{ t('fleetView.jumpGateSelectTarget') }}</CardTitle>
</CardHeader>
<CardContent>
<div v-if="availableJumpGateMoons.length === 0" class="text-center py-4 text-muted-foreground">
{{ t('fleetView.jumpGateNoTargetMoons') }}
</div>
<div v-else class="space-y-2">
<div
v-for="moon in availableJumpGateMoons"
:key="moon.id"
class="p-3 border rounded-lg cursor-pointer transition-colors"
:class="selectedJumpGateTarget?.id === moon.id ? 'ring-2 ring-primary bg-primary/10' : 'hover:bg-muted/50'"
@click="selectedJumpGateTarget = moon"
>
<div class="flex items-center justify-between">
<div>
<span class="font-medium">{{ moon.name }}</span>
<span class="text-sm text-muted-foreground ml-2">
[{{ moon.position.galaxy }}:{{ moon.position.system }}:{{ moon.position.position }}]
</span>
</div>
<Badge v-if="isJumpGateMoonReady(moon)" variant="default">{{ t('fleetView.jumpGateReady') }}</Badge>
</div>
</div>
</div>
</CardContent>
</Card>
<!-- 选择传送舰队 -->
<Card v-if="jumpGateReady && selectedJumpGateTarget">
<CardHeader>
<CardTitle>{{ t('fleetView.jumpGateSelectFleet') }}</CardTitle>
</CardHeader>
<CardContent>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4">
<div v-for="(count, shipType) in planet.fleet" :key="shipType" class="space-y-2">
<Label :for="`jump-ship-${shipType}`" class="text-xs sm:text-sm">
{{ SHIPS[shipType].name }} ({{ t('fleetView.available') }}: {{ count }})
</Label>
<div class="flex gap-2">
<Input
:id="`jump-ship-${shipType}`"
v-model.number="jumpGateFleet[shipType]"
type="number"
min="0"
:max="count"
placeholder="0"
class="text-sm"
/>
<Button @click="jumpGateFleet[shipType] = count" variant="outline" size="sm">{{ t('fleetView.all') }}</Button>
</div>
</div>
</div>
<!-- 传送按钮 -->
<div class="mt-6">
<Button @click="executeJumpGateTransfer" :disabled="!canExecuteJumpGate" class="w-full">
<Zap class="h-4 w-4 mr-2" />
{{ t('fleetView.jumpGateTransfer') }}
</Button>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
<!-- 提示对话框 -->
@@ -295,6 +486,40 @@
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<!-- 预设名称对话框 -->
<AlertDialog :open="showPresetNameDialog" @update:open="showPresetNameDialog = $event">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
{{ pendingPresetAction === 'save' ? t('fleetView.savePresetTitle') : t('fleetView.renamePresetTitle') }}
</AlertDialogTitle>
<AlertDialogDescription>
{{ pendingPresetAction === 'save' ? t('fleetView.savePresetDescription') : t('fleetView.renamePresetDescription') }}
</AlertDialogDescription>
</AlertDialogHeader>
<div class="py-4">
<Label for="preset-name">{{ t('fleetView.presetName') }}</Label>
<Input
id="preset-name"
v-model="editingPresetName"
:placeholder="t('fleetView.presetNamePlaceholder')"
class="mt-2"
@keyup.enter="handlePresetNameConfirm"
/>
</div>
<AlertDialogFooter>
<AlertDialogCancel
@click="() => { showPresetNameDialog = false; pendingPresetAction = null }"
>
{{ t('common.cancel') }}
</AlertDialogCancel>
<AlertDialogAction @click="handlePresetNameConfirm" :disabled="!editingPresetName.trim()">
{{ t('common.confirm') }}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</template>
@@ -307,7 +532,7 @@
import { computed, ref, onMounted, onUnmounted, watch } from 'vue'
import { useRoute } from 'vue-router'
import { ShipType, MissionType, BuildingType, TechnologyType } from '@/types/game'
import type { Fleet, Resources } from '@/types/game'
import type { Fleet, Resources, FleetPreset } from '@/types/game'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Button } from '@/components/ui/button'
@@ -329,7 +554,27 @@
} 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 {
Sword,
Package,
Rocket as RocketIcon,
Eye,
Users,
Recycle,
Skull,
Gift,
Compass,
Save,
Trash2,
Pencil,
Star,
Type,
Zap,
Clock,
Check,
Globe,
Moon
} from 'lucide-vue-next'
import { formatNumber, formatTime } from '@/utils/format'
import * as shipValidation from '@/logic/shipValidation'
import * as fleetLogic from '@/logic/fleetLogic'
@@ -337,6 +582,8 @@
import * as officerLogic from '@/logic/officerLogic'
import * as publicLogic from '@/logic/publicLogic'
import * as diplomaticLogic from '@/logic/diplomaticLogic'
import * as gameLogic from '@/logic/gameLogic'
import * as moonLogic from '@/logic/moonLogic'
const route = useRoute()
const gameStore = useGameStore()
@@ -363,15 +610,127 @@
return publicLogic.getMaxFleetMissions(bonuses.additionalFleetSlots, computerTechLevel)
})
const activeTab = ref<'fleet' | 'send' | 'missions'>('fleet')
const activeTab = ref<'fleet' | 'send' | 'missions' | 'jumpGate'>('fleet')
// Tab 配置
const fleetTabs = [
{ value: 'fleet', labelKey: 'fleetOverview' },
{ value: 'send', labelKey: 'sendFleet' },
{ value: 'missions', labelKey: 'flightMissions' }
{ value: 'missions', labelKey: 'flightMissions' },
{ value: 'jumpGate', labelKey: 'jumpGate' }
] as const
// 跳跃门相关
const selectedJumpGateTarget = ref<typeof planet.value | null>(null)
const jumpGateFleet = ref<Partial<Fleet>>({
[ShipType.LightFighter]: 0,
[ShipType.HeavyFighter]: 0,
[ShipType.Cruiser]: 0,
[ShipType.Battleship]: 0,
[ShipType.SmallCargo]: 0,
[ShipType.LargeCargo]: 0,
[ShipType.ColonyShip]: 0,
[ShipType.Recycler]: 0,
[ShipType.EspionageProbe]: 0,
[ShipType.DarkMatterHarvester]: 0,
[ShipType.Deathstar]: 0
})
// 是否显示跳跃门标签页(当前在月球上且有跳跃门)
const showJumpGateTab = computed(() => {
if (!planet.value) return false
if (!planet.value.isMoon) return false
const jumpGateLevel = planet.value.buildings[BuildingType.JumpGate] || 0
return jumpGateLevel > 0
})
// 跳跃门是否就绪(冷却完成)
const jumpGateReady = computed(() => {
if (!planet.value) return false
return moonLogic.isJumpGateReady(planet.value)
})
// 跳跃门剩余冷却时间
const jumpGateCooldownRemaining = computed(() => {
if (!planet.value) return 0
return moonLogic.getJumpGateCooldownRemaining(planet.value)
})
// 可用的目标月球(有跳跃门且冷却完成的其他月球)
const availableJumpGateMoons = computed(() => {
if (!planet.value) return []
return gameStore.player.planets.filter(p => {
if (p.id === planet.value?.id) return false // 排除当前月球
if (!p.isMoon) return false
const jumpGateLevel = p.buildings[BuildingType.JumpGate] || 0
if (jumpGateLevel <= 0) return false
return moonLogic.isJumpGateReady(p)
})
})
// 检查目标月球的跳跃门是否就绪
const isJumpGateMoonReady = (moon: typeof planet.value) => {
if (!moon) return false
return moonLogic.isJumpGateReady(moon)
}
// 是否可以执行跳跃门传送
const canExecuteJumpGate = computed(() => {
if (!planet.value || !selectedJumpGateTarget.value) return false
if (!jumpGateReady.value) return false
// 检查是否选择了至少一艘舰船
const totalShips = Object.values(jumpGateFleet.value).reduce((sum, count) => sum + (count || 0), 0)
return totalShips > 0
})
// 执行跳跃门传送
const executeJumpGateTransfer = () => {
if (!planet.value || !selectedJumpGateTarget.value) return
if (!canExecuteJumpGate.value) return
const sourceMoon = planet.value
const targetMoon = selectedJumpGateTarget.value
// 转移舰队
Object.entries(jumpGateFleet.value).forEach(([shipType, count]) => {
if (count && count > 0) {
const ship = shipType as ShipType
// 从源月球扣除
if (sourceMoon.fleet[ship] >= count) {
sourceMoon.fleet[ship] -= count
// 添加到目标月球
targetMoon.fleet[ship] = (targetMoon.fleet[ship] || 0) + count
}
}
})
// 设置两个跳跃门的冷却时间
moonLogic.useJumpGate(sourceMoon)
moonLogic.useJumpGate(targetMoon)
// 重置跳跃门舰队选择
Object.keys(jumpGateFleet.value).forEach(key => {
jumpGateFleet.value[key as ShipType] = 0
})
selectedJumpGateTarget.value = null
// 显示成功对话框
alertDialogTitle.value = t('fleetView.jumpGateSuccess')
alertDialogMessage.value = t('fleetView.jumpGateSuccessMessage', {
target: `${targetMoon.name} [${targetMoon.position.galaxy}:${targetMoon.position.system}:${targetMoon.position.position}]`
})
alertDialogCallback.value = null
alertDialogOpen.value = true
}
// 可见的标签页(根据是否有跳跃门动态显示)
const visibleTabs = computed(() => {
if (showJumpGateTab.value) {
return fleetTabs
}
return fleetTabs.filter(tab => tab.value !== 'jumpGate')
})
// 选择的舰队
const selectedFleet = ref<Partial<Fleet>>({
[ShipType.LightFighter]: 0,
@@ -390,6 +749,20 @@
// 目标坐标
const targetPosition = ref({ galaxy: 1, system: 1, position: 1 })
// 目标是否为月球(用于区分同坐标的行星和月球)
const targetIsMoon = ref(false)
// 检查目标位置是否有月球(玩家自己的)
const hasMoonAtTargetPosition = computed(() => {
return gameStore.player.planets.some(
p =>
p.isMoon &&
p.position.galaxy === targetPosition.value.galaxy &&
p.position.system === targetPosition.value.system &&
p.position.position === targetPosition.value.position
)
})
// 坐标字段配置
const coordinateFields: { key: keyof typeof targetPosition.value; max: number }[] = [
{ key: 'galaxy', max: 9 },
@@ -463,6 +836,227 @@
// 是否为赠送模式
const isGiftMode = ref(false)
// 舰队预设相关状态
const MAX_PRESETS = 3
const editingPresetId = ref<string | null>(null)
const editingPresetName = ref('')
const showPresetNameDialog = ref(false)
const pendingPresetAction = ref<'save' | 'rename' | null>(null)
// 获取预设列表
const fleetPresets = computed(() => gameStore.player.fleetPresets || [])
// 生成唯一ID
const generatePresetId = (): string => {
return `preset_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
}
// 保存当前配置为预设
const saveAsPreset = () => {
if (fleetPresets.value.length >= MAX_PRESETS) {
alertDialogTitle.value = t('fleetView.presetLimitReached')
alertDialogMessage.value = t('fleetView.presetLimitReachedMessage', { max: MAX_PRESETS.toString() })
alertDialogCallback.value = null
alertDialogOpen.value = true
return
}
// 检查是否有选择舰船
const hasShips = Object.values(selectedFleet.value).some(count => count > 0)
if (!hasShips) {
alertDialogTitle.value = t('fleetView.presetError')
alertDialogMessage.value = t('fleetView.presetNoShips')
alertDialogCallback.value = null
alertDialogOpen.value = true
return
}
pendingPresetAction.value = 'save'
editingPresetName.value = t('fleetView.presetDefaultName', { number: (fleetPresets.value.length + 1).toString() })
showPresetNameDialog.value = true
}
// 确认保存预设
const confirmSavePreset = () => {
if (!editingPresetName.value.trim()) return
// 只保存数量大于0的舰船
const fleetToSave: Partial<Fleet> = {}
for (const [shipType, count] of Object.entries(selectedFleet.value)) {
if (count && count > 0) {
fleetToSave[shipType as ShipType] = count
}
}
// 只保存数量大于0的资源
const cargoToSave: Partial<Resources> | undefined =
selectedMission.value === MissionType.Transport
? {
metal: cargo.value.metal || 0,
crystal: cargo.value.crystal || 0,
deuterium: cargo.value.deuterium || 0,
darkMatter: cargo.value.darkMatter || 0
}
: undefined
const newPreset: FleetPreset = {
id: generatePresetId(),
name: editingPresetName.value.trim(),
fleet: fleetToSave,
targetPosition: {
galaxy: targetPosition.value.galaxy,
system: targetPosition.value.system,
position: targetPosition.value.position
},
missionType: selectedMission.value,
cargo: cargoToSave
}
if (!gameStore.player.fleetPresets) {
gameStore.player.fleetPresets = []
}
gameStore.player.fleetPresets.push(newPreset)
showPresetNameDialog.value = false
editingPresetName.value = ''
pendingPresetAction.value = null
}
// 加载预设
const loadPreset = (preset: FleetPreset) => {
// 加载舰队配置
Object.keys(selectedFleet.value).forEach(key => {
selectedFleet.value[key as ShipType] = preset.fleet[key as ShipType] || 0
})
// 加载目标坐标
if (preset.targetPosition) {
targetPosition.value = { ...preset.targetPosition }
}
// 加载任务类型
if (preset.missionType) {
selectedMission.value = preset.missionType
}
// 加载运输资源
if (preset.cargo && preset.missionType === MissionType.Transport) {
cargo.value = {
metal: preset.cargo.metal || 0,
crystal: preset.cargo.crystal || 0,
deuterium: preset.cargo.deuterium || 0,
darkMatter: preset.cargo.darkMatter || 0,
energy: 0
}
}
}
// 更新预设(点击预设后修改内容)
const updatePreset = (presetId: string) => {
const presetIndex = gameStore.player.fleetPresets?.findIndex(p => p.id === presetId)
if (presetIndex === undefined || presetIndex === -1) return
const hasShips = Object.values(selectedFleet.value).some(count => count > 0)
if (!hasShips) {
alertDialogTitle.value = t('fleetView.presetError')
alertDialogMessage.value = t('fleetView.presetNoShips')
alertDialogCallback.value = null
alertDialogOpen.value = true
return
}
const existingPreset = gameStore.player.fleetPresets![presetIndex]
if (!existingPreset) return
// 只保存数量大于0的舰船
const fleetToSave: Partial<Fleet> = {}
for (const [shipType, count] of Object.entries(selectedFleet.value)) {
if (count && count > 0) {
fleetToSave[shipType as ShipType] = count
}
}
// 只保存数量大于0的资源
const cargoToSave: Partial<Resources> | undefined =
selectedMission.value === MissionType.Transport
? {
metal: cargo.value.metal || 0,
crystal: cargo.value.crystal || 0,
deuterium: cargo.value.deuterium || 0,
darkMatter: cargo.value.darkMatter || 0
}
: undefined
const updatedPreset: FleetPreset = {
id: existingPreset.id,
name: existingPreset.name,
fleet: fleetToSave,
targetPosition: {
galaxy: targetPosition.value.galaxy,
system: targetPosition.value.system,
position: targetPosition.value.position
},
missionType: selectedMission.value,
cargo: cargoToSave
}
gameStore.player.fleetPresets![presetIndex] = updatedPreset
editingPresetId.value = null
}
// 开始编辑预设名称
const startRenamePreset = (preset: FleetPreset) => {
// 保存要重命名的预设ID但不进入编辑内容模式
editingPresetName.value = preset.name
pendingPresetAction.value = 'rename'
// 使用临时变量存储要重命名的预设ID
renameTargetPresetId.value = preset.id
showPresetNameDialog.value = true
}
// 要重命名的预设ID与编辑预设内容分开
const renameTargetPresetId = ref<string | null>(null)
// 确认重命名预设
const confirmRenamePreset = () => {
if (!editingPresetName.value.trim() || !renameTargetPresetId.value) return
const preset = gameStore.player.fleetPresets?.find(p => p.id === renameTargetPresetId.value)
if (preset) {
preset.name = editingPresetName.value.trim()
}
showPresetNameDialog.value = false
renameTargetPresetId.value = null
editingPresetName.value = ''
pendingPresetAction.value = null
}
// 删除预设
const deletePreset = (presetId: string) => {
const preset = gameStore.player.fleetPresets?.find(p => p.id === presetId)
if (!preset) return
alertDialogTitle.value = t('fleetView.deletePresetTitle')
alertDialogMessage.value = t('fleetView.deletePresetMessage', { name: preset.name })
alertDialogCallback.value = () => {
const index = gameStore.player.fleetPresets?.findIndex(p => p.id === presetId)
if (index !== undefined && index > -1) {
gameStore.player.fleetPresets!.splice(index, 1)
}
}
alertDialogOpen.value = true
}
// 处理预设名称对话框确认
const handlePresetNameConfirm = () => {
if (pendingPresetAction.value === 'save') {
confirmSavePreset()
} else if (pendingPresetAction.value === 'rename') {
confirmRenamePreset()
}
}
// 监听目标NPC变化当目标不再是NPC时自动禁用赠送模式
watch(targetNpc, newValue => {
if (!newValue && isGiftMode.value) {
@@ -543,8 +1137,16 @@
if (!hasShips) return { valid: false, errorKey: 'fleetView.noShipsSelected' }
// 检查是否派遣到自己的星球
// 回收任务部署任务除外(回收残骸可能在同位置,部署可能到自己的月球)
if (planet.value && selectedMission.value !== MissionType.Recycle && selectedMission.value !== MissionType.Deploy) {
// 回收任务部署任务和运输任务除外:
// - 回收任务:可能回收同位置的残骸
// - 部署任务:可能部署到自己的月球
// - 运输任务可能从行星向同位置的月球运输资源OGame规则允许
if (
planet.value &&
selectedMission.value !== MissionType.Recycle &&
selectedMission.value !== MissionType.Deploy &&
selectedMission.value !== MissionType.Transport
) {
const isSamePlanet =
targetPosition.value.galaxy === planet.value.position.galaxy &&
targetPosition.value.system === planet.value.position.system &&
@@ -591,7 +1193,8 @@
targetPosition: { galaxy: number; system: number; position: number },
missionType: MissionType,
fleet: Partial<Fleet>,
cargo: Resources = { metal: 0, crystal: 0, deuterium: 0, darkMatter: 0, energy: 0 }
cargo: Resources = { metal: 0, crystal: 0, deuterium: 0, darkMatter: 0, energy: 0 },
isMoonTarget: boolean = false
): boolean => {
if (!gameStore.currentPlanet) return false
const currentMissions = gameStore.player.fleetMissions.length
@@ -604,6 +1207,13 @@
gameStore.player.technologies
)
if (!validation.valid) return false
// 追踪燃料消耗(同时计入资源消耗和燃料统计)
if (validation.fuelNeeded && validation.fuelNeeded > 0) {
gameLogic.trackResourceConsumption(gameStore.player, { deuterium: validation.fuelNeeded })
gameLogic.trackFuelConsumption(gameStore.player, validation.fuelNeeded)
}
const shouldDeductCargo = missionType === MissionType.Transport
shipValidation.executeFleetDispatch(gameStore.currentPlanet, fleet, validation.fuelNeeded!, shouldDeductCargo, cargo)
const distance = fleetLogic.calculateDistance(gameStore.currentPlanet.position, targetPosition)
@@ -620,6 +1230,11 @@
flightTime
)
// 如果目标是月球,设置标记
if (isMoonTarget) {
mission.targetIsMoon = true
}
// 如果是赠送模式,标记任务
if (missionType === MissionType.Transport && isGiftMode.value && targetNpc.value) {
mission.isGift = true
@@ -655,7 +1270,8 @@
targetPosition.value,
selectedMission.value,
fleet,
selectedMission.value === MissionType.Transport ? cargo.value : undefined
selectedMission.value === MissionType.Transport ? cargo.value : undefined,
targetIsMoon.value
)
if (success) {

View File

@@ -229,6 +229,15 @@
</p>
</PopoverContent>
</Popover>
<!-- NPC难度等级徽章 -->
<Badge
v-if="getNpcDifficultyLevel(slot.planet) !== null"
:variant="getDifficultyBadgeVariant(getNpcDifficultyLevel(slot.planet))"
class="text-xs flex-shrink-0"
:class="getDifficultyLevelColor(getNpcDifficultyLevel(slot.planet))"
>
Lv.{{ getNpcDifficultyLevel(slot.planet) }}
</Badge>
<Popover v-if="getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)">
<PopoverTrigger as-child>
<Badge
@@ -260,6 +269,16 @@
</div>
</PopoverContent>
</Popover>
<!-- 月球徽章 -->
<Badge
v-if="slot.moon"
variant="outline"
class="text-xs cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-800 border-slate-400 dark:border-slate-600 text-slate-600 dark:text-slate-400 gap-1"
@click.stop="switchToPlanet(slot.moon.id)"
>
<Moon class="h-3 w-3" />
<span>{{ slot.moon.name }}</span>
</Badge>
</div>
</div>
<!-- 空位置 -->
@@ -345,6 +364,16 @@
<p>{{ t('galaxyView.sendGift') }}</p>
</TooltipContent>
</Tooltip>
<Tooltip v-if="slot.planet && !isMyPlanet(slot.planet) && canScanPlanet(slot.planet)">
<TooltipTrigger as-child>
<Button @click="showPhalanxScanDialog(slot.planet)" variant="outline" size="sm" class="h-8 w-8 p-0">
<Radar class="h-3 w-3" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{{ t('galaxyView.phalanxScan') }}</p>
</TooltipContent>
</Tooltip>
<Tooltip v-if="!slot.planet">
<TooltipTrigger as-child>
<Button @click="showPlanetActions(null, 'colonize', slot.position)" variant="outline" size="sm" class="h-8 w-8 p-0">
@@ -417,6 +446,15 @@
</TooltipContent>
</Tooltip>
</TooltipProvider>
<!-- NPC难度等级徽章 -->
<Badge
v-if="getNpcDifficultyLevel(slot.planet) !== null"
:variant="getDifficultyBadgeVariant(getNpcDifficultyLevel(slot.planet))"
class="text-xs"
:class="getDifficultyLevelColor(getNpcDifficultyLevel(slot.planet))"
>
Lv.{{ getNpcDifficultyLevel(slot.planet) }}
</Badge>
<!-- 残骸场徽章 -->
<Popover v-if="getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)">
<PopoverTrigger as-child>
@@ -450,6 +488,16 @@
</div>
</PopoverContent>
</Popover>
<!-- 月球徽章 -->
<Badge
v-if="slot.moon"
variant="outline"
class="text-xs cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-800 border-slate-400 dark:border-slate-600 text-slate-600 dark:text-slate-400 gap-1"
@click.stop="switchToPlanet(slot.moon.id)"
>
<Moon class="h-3 w-3" />
<span>{{ slot.moon.name }}</span>
</Badge>
</div>
<!-- PC端坐标 -->
<p class="text-xs text-muted-foreground">
@@ -539,6 +587,16 @@
<p>{{ t('galaxyView.sendGift') }}</p>
</TooltipContent>
</Tooltip>
<Tooltip v-if="slot.planet && !isMyPlanet(slot.planet) && canScanPlanet(slot.planet)">
<TooltipTrigger as-child>
<Button @click="showPhalanxScanDialog(slot.planet)" variant="outline" size="sm" class="h-8 w-8 p-0">
<Radar class="h-3 w-3 sm:h-4 sm:w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{{ t('galaxyView.phalanxScan') }}</p>
</TooltipContent>
</Tooltip>
<Tooltip v-if="!slot.planet">
<TooltipTrigger as-child>
<Button @click="showPlanetActions(null, 'colonize', slot.position)" variant="outline" size="sm" class="h-8 w-8 p-0">
@@ -650,6 +708,97 @@
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<!-- 传感器阵列扫描对话框 -->
<Dialog :open="phalanxDialogOpen" @update:open="phalanxDialogOpen = $event">
<DialogContent class="max-w-lg">
<DialogHeader>
<DialogTitle class="flex items-center gap-2">
<Radar class="h-5 w-5" />
{{ t('galaxyView.phalanxScanTitle') }}
</DialogTitle>
<DialogDescription v-if="phalanxTargetPlanet">
{{
t('galaxyView.phalanxScanDescription').replace(
'{coordinates}',
`${phalanxTargetPlanet.position.galaxy}:${phalanxTargetPlanet.position.system}:${phalanxTargetPlanet.position.position}`
)
}}
</DialogDescription>
</DialogHeader>
<div class="space-y-4">
<!-- 扫描信息 -->
<div class="flex items-center justify-between text-sm">
<span class="text-muted-foreground">{{ t('galaxyView.phalanxCost') }}:</span>
<div class="flex items-center gap-1">
<ResourceIcon type="deuterium" size="sm" />
<span>{{ formatNumber(PHALANX_SCAN_COST) }}</span>
</div>
</div>
<!-- 扫描按钮 -->
<Button v-if="phalanxScanResults.length === 0 && !phalanxScanning" @click="executePhalanxScan" class="w-full">
<Radar class="h-4 w-4 mr-2" />
{{ t('galaxyView.phalanxScan') }}
</Button>
<!-- 扫描中 -->
<div v-if="phalanxScanning" class="flex items-center justify-center py-8">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
<!-- 扫描结果 -->
<div v-if="!phalanxScanning && phalanxScanResults.length > 0" class="space-y-3">
<div class="text-sm font-medium">
{{ t('galaxyView.phalanxFleetDetected').replace('{count}', String(phalanxScanResults.length)) }}
</div>
<div class="space-y-2 max-h-64 overflow-y-auto">
<div v-for="fleet in phalanxScanResults" :key="fleet.id" class="p-3 border rounded-lg space-y-2 text-sm">
<div class="flex items-center justify-between">
<Badge>{{ getMissionTypeText(fleet.missionType) }}</Badge>
<Badge :variant="fleet.status === 'outbound' ? 'default' : 'secondary'">
{{ fleet.status === 'outbound' ? t('galaxyView.phalanxStatusOutbound') : t('galaxyView.phalanxStatusReturning') }}
</Badge>
</div>
<div class="grid grid-cols-2 gap-2 text-xs">
<div>
<span class="text-muted-foreground">{{ t('galaxyView.phalanxOrigin') }}:</span>
<span class="ml-1">
{{ formatCoords(getPlanetPositionById(fleet.originPlanetId) || { galaxy: 0, system: 0, position: 0 }) }}
</span>
</div>
<div>
<span class="text-muted-foreground">{{ t('galaxyView.phalanxDestination') }}:</span>
<span class="ml-1">{{ formatCoords(fleet.targetPosition) }}</span>
</div>
<div>
<span class="text-muted-foreground">{{ t('galaxyView.phalanxArrival') }}:</span>
<span class="ml-1">{{ formatTime(Math.max(0, Math.floor((fleet.arrivalTime - Date.now()) / 1000))) }}</span>
</div>
<div v-if="fleet.returnTime">
<span class="text-muted-foreground">{{ t('galaxyView.phalanxReturn') }}:</span>
<span class="ml-1">{{ formatTime(Math.max(0, Math.floor((fleet.returnTime - Date.now()) / 1000))) }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- 无舰队 -->
<div
v-if="!phalanxScanning && phalanxScanResults.length === 0 && phalanxDialogOpen"
class="text-center py-4 text-muted-foreground"
>
{{ t('galaxyView.phalanxNoFleets') }}
</div>
</div>
<DialogFooter>
<Button variant="outline" @click="phalanxDialogOpen = false">{{ t('common.close') }}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</template>
@@ -681,10 +830,13 @@
AlertDialogTitle
} from '@/components/ui/alert-dialog'
import ResourceIcon from '@/components/ResourceIcon.vue'
import { Home, Eye, Sword, Rocket, Recycle, Gift, Globe, Bomb } from 'lucide-vue-next'
import { Home, Eye, Sword, Rocket, Recycle, Gift, Globe, Bomb, Moon, Radar } from 'lucide-vue-next'
import { useRouter, useRoute } from 'vue-router'
import * as gameLogic from '@/logic/gameLogic'
import { formatNumber } from '@/utils/format'
import * as moonLogic from '@/logic/moonLogic'
import { formatNumber, formatTime } from '@/utils/format'
import { BuildingType, MissionType } from '@/types/game'
import type { FleetMission } from '@/types/game'
const gameStore = useGameStore()
const universeStore = useUniverseStore()
@@ -704,6 +856,12 @@
const missileTargetPlanet = ref<Planet | null>(null)
const missileCount = ref(1)
// 传感器阵列扫描对话框状态
const phalanxDialogOpen = ref(false)
const phalanxTargetPlanet = ref<Planet | null>(null)
const phalanxScanResults = ref<FleetMission[]>([])
const phalanxScanning = ref(false)
const selectedGalaxy = ref(1)
const selectedSystem = ref(1)
const currentGalaxy = ref(1)
@@ -718,7 +876,7 @@
return npcStore.npcs.find(n => n.id === highlightNpcId.value) || null
})
const systemSlots = ref<Array<{ position: number; planet: Planet | null }>>([])
const systemSlots = ref<Array<{ position: number; planet: Planet | null; moon: Planet | null }>>([])
// 获取玩家的母星
const homePlanet = computed(() => {
@@ -770,18 +928,26 @@
}
})
const getSystemPlanets = (galaxy: number, system: number): Array<{ position: number; planet: Planet | null }> => {
const getSystemPlanets = (galaxy: number, system: number): Array<{ position: number; planet: Planet | null; moon: Planet | null }> => {
const positions = gameLogic.generateSystemPositions(galaxy, system)
return positions.map(pos => {
const key = gameLogic.generatePositionKey(galaxy, system, pos.position)
// 先从玩家星球中查找,再从宇宙地图中查找
// 先从玩家星球中查找(非月球),再从宇宙地图中查找
const planet =
gameStore.player.planets.find(
p => p.position.galaxy === galaxy && p.position.system === system && p.position.position === pos.position
p => !p.isMoon && p.position.galaxy === galaxy && p.position.system === system && p.position.position === pos.position
) ||
universeStore.planets[key] ||
null
return { position: pos.position, planet }
// 查找该位置的月球(如果有星球的话)
let moon: Planet | null = null
if (planet) {
// 从玩家星球中查找月球
moon = gameStore.player.planets.find(p => p.isMoon && p.parentPlanetId === planet.id) || null
}
return { position: pos.position, planet, moon }
})
}
@@ -898,6 +1064,32 @@
return planet.name
}
// 获取NPC难度等级
const getNpcDifficultyLevel = (planet: Planet | null): number | null => {
const npc = getPlanetNPC(planet)
return npc?.difficultyLevel ?? null
}
// 获取NPC难度等级颜色
const getDifficultyLevelColor = (level: number | null): string => {
if (level === null) return 'text-muted-foreground'
if (level <= 1) return 'text-green-600 dark:text-green-400' // 新手
if (level <= 2) return 'text-lime-600 dark:text-lime-400' // 简单
if (level <= 3) return 'text-yellow-600 dark:text-yellow-400' // 普通
if (level <= 4) return 'text-orange-600 dark:text-orange-400' // 困难
if (level <= 5) return 'text-red-600 dark:text-red-400' // 专家
if (level <= 6) return 'text-purple-600 dark:text-purple-400' // 大师
return 'text-pink-600 dark:text-pink-400' // 传奇及以上
}
// 获取NPC难度等级Badge样式
const getDifficultyBadgeVariant = (level: number | null): 'default' | 'secondary' | 'destructive' | 'outline' => {
if (level === null) return 'outline'
if (level <= 2) return 'secondary'
if (level <= 4) return 'default'
return 'destructive'
}
// 切换到指定星球
const switchToPlanet = (planetId: string) => {
gameStore.currentPlanetId = planetId
@@ -1030,4 +1222,172 @@
const secs = seconds % 60
return `${minutes}:${secs.toString().padStart(2, '0')}`
}
// ========== 传感器阵列扫描功能 ==========
// 获取拥有传感器阵列的月球列表
const moonsWithPhalanx = computed(() => {
return gameStore.player.planets.filter(p => {
if (!p.isMoon) return false
const phalanxLevel = p.buildings[BuildingType.SensorPhalanx] || 0
return phalanxLevel > 0
})
})
// 检查是否可以扫描目标(需要有传感器阵列的月球在范围内)
const canScanPlanet = (targetPlanet: Planet | null): boolean => {
if (!targetPlanet) return false
if (isMyPlanet(targetPlanet)) return false
// 检查是否有月球的传感器阵列可以扫描目标
return moonsWithPhalanx.value.some(moon => {
const phalanxLevel = moon.buildings[BuildingType.SensorPhalanx] || 0
return moonLogic.isInSensorPhalanxRange(moon.position, targetPlanet.position, phalanxLevel)
})
}
// 获取可以扫描目标的月球
const getMoonForScan = (targetPlanet: Planet): Planet | null => {
return (
moonsWithPhalanx.value.find(moon => {
const phalanxLevel = moon.buildings[BuildingType.SensorPhalanx] || 0
return moonLogic.isInSensorPhalanxRange(moon.position, targetPlanet.position, phalanxLevel)
}) || null
)
}
// 计算扫描消耗的氘每次扫描消耗5000氘
const PHALANX_SCAN_COST = 5000
// 显示传感器阵列扫描对话框
const showPhalanxScanDialog = (planet: Planet) => {
phalanxTargetPlanet.value = planet
phalanxScanResults.value = []
phalanxScanning.value = false
phalanxDialogOpen.value = true
}
// 根据星球ID获取星球坐标
const getPlanetPositionById = (planetId: string): { galaxy: number; system: number; position: number } | null => {
// 先从玩家星球中查找
const playerPlanet = gameStore.player.planets.find(p => p.id === planetId)
if (playerPlanet) return playerPlanet.position
// 再从NPC星球中查找
for (const npc of npcStore.npcs) {
const npcPlanet = npc.planets.find(p => p.id === planetId)
if (npcPlanet) return npcPlanet.position
}
// 从宇宙地图中查找
for (const key in universeStore.planets) {
const planet = universeStore.planets[key]
if (planet && planet.id === planetId) return planet.position
}
return null
}
// 执行传感器阵列扫描
const executePhalanxScan = () => {
if (!phalanxTargetPlanet.value) return
const scanMoon = getMoonForScan(phalanxTargetPlanet.value)
if (!scanMoon) {
alertDialogTitle.value = t('errors.scanFailed')
alertDialogMessage.value = t('galaxyView.phalanxNoMoon')
alertDialogOpen.value = true
return
}
// 检查氘是否足够
if (scanMoon.resources.deuterium < PHALANX_SCAN_COST) {
alertDialogTitle.value = t('errors.scanFailed')
alertDialogMessage.value = t('galaxyView.phalanxInsufficientDeuterium')
alertDialogOpen.value = true
return
}
// 扣除氘
scanMoon.resources.deuterium -= PHALANX_SCAN_COST
phalanxScanning.value = true
// 模拟扫描延迟
setTimeout(() => {
// 扫描NPC的舰队任务
const targetPos = phalanxTargetPlanet.value!.position
const npc = getPlanetNPC(phalanxTargetPlanet.value)
// 收集相关的舰队任务
const detectedFleets: FleetMission[] = []
// 检查NPC的舰队任务
if (npc) {
npc.fleetMissions?.forEach(mission => {
// 获取出发地坐标
const originPos = getPlanetPositionById(mission.originPlanetId)
// 检查任务是否与目标星球相关(出发地或目的地)
const isFromTarget =
originPos &&
originPos.galaxy === targetPos.galaxy &&
originPos.system === targetPos.system &&
originPos.position === targetPos.position
const isToTarget =
mission.targetPosition.galaxy === targetPos.galaxy &&
mission.targetPosition.system === targetPos.system &&
mission.targetPosition.position === targetPos.position
if (isFromTarget || isToTarget) {
detectedFleets.push(mission)
}
})
}
// 也检查玩家自己发往该星球的任务(自己的任务自己当然知道,但扫描也能看到)
gameStore.player.fleetMissions?.forEach(mission => {
const isToTarget =
mission.targetPosition.galaxy === targetPos.galaxy &&
mission.targetPosition.system === targetPos.system &&
mission.targetPosition.position === targetPos.position
if (isToTarget) {
detectedFleets.push(mission)
}
})
phalanxScanResults.value = detectedFleets
phalanxScanning.value = false
}, 1000)
}
// 获取任务类型文本
const getMissionTypeText = (missionType: MissionType): string => {
switch (missionType) {
case MissionType.Attack:
return t('fleetView.attack')
case MissionType.Transport:
return t('fleetView.transport')
case MissionType.Deploy:
return t('fleetView.deploy')
case MissionType.Spy:
return t('fleetView.spy')
case MissionType.Colonize:
return t('fleetView.colonize')
case MissionType.Recycle:
return t('fleetView.recycle')
case MissionType.Destroy:
return t('fleetView.destroy')
case MissionType.Expedition:
return t('fleetView.expedition')
default:
return missionType
}
}
// 格式化坐标
const formatCoords = (pos: { galaxy: number; system: number; position: number }): string => {
return `[${pos.galaxy}:${pos.system}:${pos.position}]`
}
</script>

View File

@@ -176,6 +176,7 @@
import { formatNumber, formatTime, formatDate, getResourceCostColor } from '@/utils/format'
import * as officerLogic from '@/logic/officerLogic'
import * as resourceLogic from '@/logic/resourceLogic'
import * as gameLogic from '@/logic/gameLogic'
import { useI18n } from '@/composables/useI18n'
import { useGameConfig } from '@/composables/useGameConfig'
@@ -250,6 +251,8 @@
if (!resourceLogic.checkResourcesAvailable(gameStore.currentPlanet.resources, cost)) {
return false
}
// 追踪资源消耗(在扣除前)
gameLogic.trackResourceConsumption(gameStore.player, cost)
resourceLogic.deductResources(gameStore.currentPlanet.resources, cost)
gameStore.player.officers[officerType] = officerLogic.createActiveOfficer(officerType, duration)
return true
@@ -276,6 +279,8 @@
if (!resourceLogic.checkResourcesAvailable(gameStore.currentPlanet.resources, cost)) {
return false
}
// 追踪资源消耗(在扣除前)
gameLogic.trackResourceConsumption(gameStore.player, cost)
resourceLogic.deductResources(gameStore.currentPlanet.resources, cost)
const now = Date.now()
gameStore.player.officers[officerType] = officerLogic.renewOfficerExpiration(gameStore.player.officers[officerType], duration, now)

View File

@@ -12,7 +12,6 @@
<!-- 月球信息 -->
<div v-if="!planet.isMoon && moon" class="mt-2">
<Button @click="switchToMoon" variant="outline" size="sm">
<span class="mr-2">🌙</span>
{{ t('planet.switchToMoon') }}
</Button>
</div>

View File

@@ -1,7 +1,7 @@
<template>
<div v-if="planet" class="container mx-auto p-4 sm:p-6">
<!-- 未解锁遮罩 -->
<!-- <UnlockRequirement :required-building="BuildingType.ResearchLab" :required-level="1" /> -->
<UnlockRequirement :required-building="BuildingType.ResearchLab" :required-level="1" />
<h1 class="text-2xl sm:text-3xl font-bold mb-4 sm:mb-6">{{ t('researchView.title') }}</h1>
@@ -107,12 +107,14 @@
AlertDialogHeader,
AlertDialogTitle
} from '@/components/ui/alert-dialog'
import UnlockRequirement from '@/components/UnlockRequirement.vue'
import CardUnlockOverlay from '@/components/CardUnlockOverlay.vue'
import { Check, X } from 'lucide-vue-next'
import { formatNumber, getResourceCostColor } from '@/utils/format'
import * as publicLogic from '@/logic/publicLogic'
import * as researchLogic from '@/logic/researchLogic'
import * as researchValidation from '@/logic/researchValidation'
import * as gameLogic from '@/logic/gameLogic'
const gameStore = useGameStore()
const detailDialog = useDetailDialogStore()
@@ -146,6 +148,11 @@
)
if (!validation.valid) return false
const currentLevel = gameStore.player.technologies[techType] || 0
// 追踪资源消耗(在扣除前计算成本)
const cost = researchLogic.calculateTechnologyCost(techType, currentLevel + 1)
gameLogic.trackResourceConsumption(gameStore.player, cost)
const { queueItem } = researchValidation.executeTechnologyResearch(
gameStore.currentPlanet,
techType,

View File

@@ -31,13 +31,20 @@
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3 sm:gap-4">
<Card v-for="shipType in Object.values(ShipType)" :key="shipType" class="relative">
<CardUnlockOverlay :requirements="SHIPS[shipType].requirements" />
<CardHeader class="pb-3">
<CardTitle
class="text-sm sm:text-base lg:text-lg cursor-pointer hover:text-primary transition-colors underline decoration-dotted underline-offset-4 mb-2"
@click="detailDialog.openShip(shipType)"
>
{{ SHIPS[shipType].name }}
</CardTitle>
<CardHeader>
<div class="mb-2">
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-2">
<CardTitle
class="text-sm sm:text-base lg:text-lg cursor-pointer hover:text-primary transition-colors underline decoration-dotted underline-offset-4 order-2 sm:order-1"
@click="detailDialog.openShip(shipType)"
>
{{ SHIPS[shipType].name }}
</CardTitle>
<Badge variant="secondary" class="text-xs whitespace-nowrap self-start order-1 sm:order-2">
{{ formatNumber(planet.fleet[shipType] || 0) }}
</Badge>
</div>
</div>
<CardDescription class="text-xs sm:text-sm">{{ SHIPS[shipType].description }}</CardDescription>
</CardHeader>
<CardContent>
@@ -146,6 +153,7 @@
import { computed, ref } from 'vue'
import { ShipType, BuildingType } from '@/types/game'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
@@ -165,6 +173,8 @@
import * as shipValidation from '@/logic/shipValidation'
import * as publicLogic from '@/logic/publicLogic'
import * as fleetStorageLogic from '@/logic/fleetStorageLogic'
import * as shipLogic from '@/logic/shipLogic'
import * as gameLogic from '@/logic/gameLogic'
const gameStore = useGameStore()
const detailDialog = useDetailDialogStore()
@@ -220,6 +230,11 @@
if (!gameStore.currentPlanet) return { success: false }
const validation = shipValidation.validateShipBuild(gameStore.currentPlanet, shipType, quantity, gameStore.player.technologies)
if (!validation.valid) return { success: false, reason: validation.reason }
// 追踪资源消耗(在扣除前计算成本)
const totalCost = shipLogic.calculateShipCost(shipType, quantity)
gameLogic.trackResourceConsumption(gameStore.player, totalCost)
const queueItem = shipValidation.executeShipBuild(gameStore.currentPlanet, shipType, quantity, gameStore.player.officers)
gameStore.currentPlanet.buildQueue.push(queueItem)
return { success: true }