mirror of
https://github.com/setube/ogame-vue-ts.git
synced 2026-05-12 16:05:12 +08:00
feat: 新增Android平台支持及构建流程
集成Android平台相关目录与配置文件,包含Gradle构建脚本、资源文件、启动图标、Java入口、Proguard规则等,完善.gitignore以排除Android构建产物。更新CI流程,支持自动构建并发布Android APK。移除README中项目结构说明,简化文档。
This commit is contained in:
343
src/views/AchievementsView.vue
Normal file
343
src/views/AchievementsView.vue
Normal 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>
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }
|
||||
|
||||
Reference in New Issue
Block a user