mirror of
https://github.com/setube/ogame-vue-ts.git
synced 2026-05-12 16:05:12 +08:00
1.6.0更新
This commit is contained in:
@@ -57,9 +57,15 @@
|
||||
<!-- 防守方配置 -->
|
||||
<TabsContent value="defender" class="mt-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('simulatorView.defenderConfig') }}</CardTitle>
|
||||
<CardDescription>{{ t('simulatorView.defenderConfigDesc') }}</CardDescription>
|
||||
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<div class="space-y-1">
|
||||
<CardTitle>{{ t('simulatorView.defenderConfig') }}</CardTitle>
|
||||
<CardDescription>{{ t('simulatorView.defenderConfigDesc') }}</CardDescription>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" @click="showSpyReportSelector = true" :disabled="!gameStore.player?.spyReports?.length">
|
||||
<FileDown class="h-4 w-4 mr-2" />
|
||||
{{ t('simulatorView.importFromSpyReport') }}
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<!-- 舰队配置 -->
|
||||
@@ -147,28 +153,80 @@
|
||||
|
||||
<!-- 战斗结果对话框 -->
|
||||
<BattleReportDialog v-model:open="showResultDialog" :report="simulationResult" />
|
||||
|
||||
<!-- 侦查报告选择对话框 -->
|
||||
<Dialog v-model:open="showSpyReportSelector">
|
||||
<DialogContent class="max-w-2xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ t('simulatorView.selectSpyReport') }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div class="space-y-2">
|
||||
<div v-if="!sortedSpyReports.length" class="text-center py-8 text-muted-foreground">
|
||||
{{ t('simulatorView.noSpyReports') }}
|
||||
</div>
|
||||
<div
|
||||
v-for="report in sortedSpyReports"
|
||||
:key="report.id"
|
||||
@click="importFromSpyReport(report)"
|
||||
class="p-3 border rounded-lg cursor-pointer hover:bg-accent transition-colors"
|
||||
>
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<div class="font-medium">{{ report.targetPlanetName }}</div>
|
||||
<div class="text-sm text-muted-foreground">
|
||||
[{{ report.targetPosition.galaxy }}:{{ report.targetPosition.system }}:{{ report.targetPosition.position }}]
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm text-muted-foreground">
|
||||
{{ formatTime(report.timestamp) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 flex gap-4 text-xs">
|
||||
<span class="flex items-center gap-1">
|
||||
<ResourceIcon type="metal" size="sm" />
|
||||
{{ formatNumber(report.resources.metal) }}
|
||||
</span>
|
||||
<span class="flex items-center gap-1">
|
||||
<ResourceIcon type="crystal" size="sm" />
|
||||
{{ formatNumber(report.resources.crystal) }}
|
||||
</span>
|
||||
<span class="flex items-center gap-1">
|
||||
<ResourceIcon type="deuterium" size="sm" />
|
||||
{{ formatNumber(report.resources.deuterium) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, toRaw } from 'vue'
|
||||
import { ref, toRaw, computed } from 'vue'
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
import { useGameConfig } from '@/composables/useGameConfig'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { ShipType, DefenseType } from '@/types/game'
|
||||
import type { Fleet, BattleResult } from '@/types/game'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { ShipType, DefenseType, TechnologyType } from '@/types/game'
|
||||
import type { Fleet, BattleResult, SpyReport } from '@/types/game'
|
||||
import { workerManager } from '@/workers/workerManager'
|
||||
import ResourceIcon from '@/components/common/ResourceIcon.vue'
|
||||
import BattleReportDialog from '@/components/dialogs/BattleReportDialog.vue'
|
||||
import { Sword, Shield, Zap, RotateCcw } from 'lucide-vue-next'
|
||||
import { Sword, Shield, Zap, RotateCcw, FileDown } from 'lucide-vue-next'
|
||||
import * as planetLogic from '@/logic/planetLogic'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { SHIPS, DEFENSES } = useGameConfig()
|
||||
const gameStore = useGameStore()
|
||||
|
||||
// 侦查报告选择对话框状态
|
||||
const showSpyReportSelector = ref(false)
|
||||
|
||||
// 科技类型配置
|
||||
const techTypes = ['weapon', 'shield', 'armor'] as const
|
||||
@@ -249,7 +307,8 @@
|
||||
// 使用 Worker 执行战斗模拟
|
||||
const result = await workerManager.simulateBattle({
|
||||
attacker: attackerSide,
|
||||
defender: defenderSide
|
||||
defender: defenderSide,
|
||||
maxRounds: gameStore.battleToFinish ? 100 : 6
|
||||
})
|
||||
|
||||
// 计算掠夺和残骸场
|
||||
@@ -302,4 +361,65 @@
|
||||
simulationResult.value = null
|
||||
showResultDialog.value = false
|
||||
}
|
||||
|
||||
// 按时间排序的侦查报告
|
||||
const sortedSpyReports = computed(() => {
|
||||
return [...(gameStore.player?.spyReports || [])].sort((a, b) => b.timestamp - a.timestamp)
|
||||
})
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (timestamp: number) => {
|
||||
return new Date(timestamp).toLocaleString()
|
||||
}
|
||||
|
||||
// 格式化数字
|
||||
const formatNumber = (num: number) => {
|
||||
if (num >= 1e9) return (num / 1e9).toFixed(1) + 'B'
|
||||
if (num >= 1e6) return (num / 1e6).toFixed(1) + 'M'
|
||||
if (num >= 1e3) return (num / 1e3).toFixed(1) + 'K'
|
||||
return num.toString()
|
||||
}
|
||||
|
||||
// 从侦查报告导入数据
|
||||
const importFromSpyReport = (report: SpyReport) => {
|
||||
// 先重置防守方数据
|
||||
defenderFleet.value = initializeFleet()
|
||||
defenderDefense.value = initializeDefense()
|
||||
|
||||
// 填入资源
|
||||
if (report.resources) {
|
||||
defenderResources.value = {
|
||||
metal: report.resources.metal || 0,
|
||||
crystal: report.resources.crystal || 0,
|
||||
deuterium: report.resources.deuterium || 0,
|
||||
darkMatter: report.resources.darkMatter || 0,
|
||||
energy: 0
|
||||
}
|
||||
}
|
||||
|
||||
// 填入舰队
|
||||
if (report.fleet) {
|
||||
Object.entries(report.fleet).forEach(([key, value]) => {
|
||||
defenderFleet.value[key as keyof Fleet] = value || 0
|
||||
})
|
||||
}
|
||||
|
||||
// 填入防御
|
||||
if (report.defense) {
|
||||
Object.entries(report.defense).forEach(([key, value]) => {
|
||||
defenderDefense.value[key as DefenseType] = value || 0
|
||||
})
|
||||
}
|
||||
|
||||
// 填入科技
|
||||
if (report.technologies) {
|
||||
defenderTech.value.weapon = report.technologies[TechnologyType.WeaponsTechnology] || 0
|
||||
defenderTech.value.shield = report.technologies[TechnologyType.ShieldingTechnology] || 0
|
||||
defenderTech.value.armor = report.technologies[TechnologyType.ArmourTechnology] || 0
|
||||
}
|
||||
|
||||
// 关闭对话框并切换到防守方标签
|
||||
showSpyReportSelector.value = false
|
||||
activeTab.value = 'defender'
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
<TabsList :class="['grid', 'w-full', showJumpGateTab ? 'grid-cols-3' : 'grid-cols-2']">
|
||||
<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 v-if="tab.value === 'missions' && totalMissionsCount > 0" variant="destructive" class="ml-1">
|
||||
{{ totalMissionsCount }}
|
||||
</Badge>
|
||||
<Badge v-if="tab.value === 'jumpGate' && jumpGateReady" variant="default" class="ml-1">✓</Badge>
|
||||
</TabsTrigger>
|
||||
@@ -294,13 +294,14 @@
|
||||
|
||||
<!-- 飞行任务 -->
|
||||
<TabsContent value="missions" class="mt-4 space-y-4">
|
||||
<Empty v-if="gameStore.player.fleetMissions.length === 0" class="border rounded-lg">
|
||||
<Empty v-if="totalMissionsCount === 0" class="border rounded-lg">
|
||||
<EmptyContent>
|
||||
<RocketIcon class="h-10 w-10 text-muted-foreground" />
|
||||
<EmptyDescription>{{ t('fleetView.noFlightMissions') }}</EmptyDescription>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
|
||||
<!-- 舰队任务 -->
|
||||
<Card v-for="mission in gameStore.player.fleetMissions" :key="mission.id">
|
||||
<CardHeader>
|
||||
<div class="flex justify-between items-start">
|
||||
@@ -378,6 +379,44 @@
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 导弹攻击任务 -->
|
||||
<Card v-for="missileAttack in flyingMissileAttacks" :key="missileAttack.id">
|
||||
<CardHeader>
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<CardTitle class="text-base sm:text-lg flex items-center gap-2">
|
||||
<Crosshair class="h-4 w-4 text-destructive" />
|
||||
{{ t('galaxyView.missileAttackTitle') }}
|
||||
</CardTitle>
|
||||
<CardDescription class="text-xs sm:text-sm">
|
||||
{{ getPlanetName(missileAttack.originPlanetId) }} → [{{ missileAttack.targetPosition.galaxy }}:{{ missileAttack.targetPosition.system }}:{{
|
||||
missileAttack.targetPosition.position
|
||||
}}]
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Badge variant="destructive">
|
||||
{{ t('fleetView.outbound') }}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-3">
|
||||
<!-- 导弹数量 -->
|
||||
<div>
|
||||
<p class="text-xs sm:text-sm font-medium mb-2">{{ t('galaxyView.missileCount') }}:</p>
|
||||
<Badge variant="outline">{{ missileAttack.missileCount }}</Badge>
|
||||
</div>
|
||||
|
||||
<!-- 进度条 -->
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between text-xs sm:text-sm">
|
||||
<span>{{ t('fleetView.arrivalTime') }}:</span>
|
||||
<span>{{ formatTime(getMissileRemainingTime(missileAttack)) }}</span>
|
||||
</div>
|
||||
<Progress :model-value="getMissileProgress(missileAttack)" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<!-- 跳跃门 -->
|
||||
@@ -591,7 +630,8 @@
|
||||
Clock,
|
||||
Check,
|
||||
Globe,
|
||||
Moon
|
||||
Moon,
|
||||
Crosshair
|
||||
} from 'lucide-vue-next'
|
||||
import { formatNumber, formatTime } from '@/utils/format'
|
||||
import * as shipValidation from '@/logic/shipValidation'
|
||||
@@ -628,6 +668,16 @@
|
||||
return publicLogic.getMaxFleetMissions(bonuses.additionalFleetSlots, computerTechLevel)
|
||||
})
|
||||
|
||||
// 飞行中的导弹攻击
|
||||
const flyingMissileAttacks = computed(() => {
|
||||
return gameStore.player.missileAttacks?.filter(m => m.status === 'flying') || []
|
||||
})
|
||||
|
||||
// 总任务数量(舰队任务 + 导弹攻击)
|
||||
const totalMissionsCount = computed(() => {
|
||||
return gameStore.player.fleetMissions.length + flyingMissileAttacks.value.length
|
||||
})
|
||||
|
||||
const activeTab = ref<'send' | 'missions' | 'jumpGate'>('send')
|
||||
|
||||
// Tab 配置
|
||||
@@ -837,6 +887,8 @@
|
||||
selectedMission.value = MissionType.Attack
|
||||
} else if (mission === 'colonize') {
|
||||
selectedMission.value = MissionType.Colonize
|
||||
} else if (mission === 'recycle') {
|
||||
selectedMission.value = MissionType.Recycle
|
||||
} else if (gift === '1') {
|
||||
// 如果有gift参数,设置为运输任务并启用赠送模式
|
||||
selectedMission.value = MissionType.Transport
|
||||
@@ -1413,4 +1465,18 @@
|
||||
return Math.max(0, Math.min(100, (elapsed / total) * 100))
|
||||
}
|
||||
}
|
||||
|
||||
// 获取导弹任务剩余时间
|
||||
const getMissileRemainingTime = (missileAttack: any): number => {
|
||||
const now = currentTime.value
|
||||
return Math.max(0, (missileAttack.arrivalTime - now) / 1000)
|
||||
}
|
||||
|
||||
// 获取导弹任务进度
|
||||
const getMissileProgress = (missileAttack: any): number => {
|
||||
const now = currentTime.value
|
||||
const total = missileAttack.arrivalTime - missileAttack.launchTime
|
||||
const elapsed = now - missileAttack.launchTime
|
||||
return Math.max(0, Math.min(100, (elapsed / total) * 100))
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
{{ t('galaxyView.myPlanets') }}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-72 p-2" align="start">
|
||||
<PopoverContent class="w-87 p-2 max-h-80 overflow-y-auto" align="start">
|
||||
<div class="space-y-1">
|
||||
<div class="px-2 py-1.5 text-xs font-semibold text-muted-foreground">
|
||||
{{ t('galaxyView.selectPlanetToView') }}
|
||||
@@ -114,7 +114,7 @@
|
||||
{{ highlightedNpc.name }} ({{ highlightedNpc.planets.length }})
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-72 p-2" align="start">
|
||||
<PopoverContent class="w-72 p-2 max-h-96 overflow-y-auto" align="start">
|
||||
<div class="space-y-1">
|
||||
<div class="px-2 py-1.5 text-xs font-semibold text-muted-foreground">
|
||||
{{ t('galaxyView.selectPlanetToView') }}
|
||||
|
||||
@@ -149,11 +149,11 @@
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
transform: translate3d(0, -10px, 0);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -22,8 +22,47 @@
|
||||
<div v-if="planet.isMoon" class="mt-2">
|
||||
<Button @click="switchToParentPlanet" variant="outline" size="sm">{{ t('planet.backToPlanet') }}</Button>
|
||||
</div>
|
||||
<!-- 放弃殖民地按钮 -->
|
||||
<div v-if="canShowAbandonButton" class="mt-4">
|
||||
<Button @click="showAbandonDialog = true" variant="destructive" size="sm">
|
||||
{{ t('planet.abandonColony') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 放弃殖民地确认对话框 -->
|
||||
<AlertDialog :open="showAbandonDialog" @update:open="showAbandonDialog = $event">
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{{ t('planet.confirmAbandon') }}</AlertDialogTitle>
|
||||
<AlertDialogDescription class="whitespace-pre-line">
|
||||
{{ t('planet.abandonWarning', { name: planet.name }) }}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>{{ t('common.cancel') }}</AlertDialogCancel>
|
||||
<AlertDialogAction @click="handleAbandonColony" class="bg-destructive text-destructive-foreground hover:bg-destructive/90">
|
||||
{{ t('planet.confirmAbandonButton') }}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
<!-- 错误提示对话框 -->
|
||||
<AlertDialog :open="showErrorDialog" @update:open="showErrorDialog = $event">
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{{ t('planet.abandonFailed') }}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{{ errorMessage }}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogAction>{{ t('common.confirm') }}</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
<!-- 资源管理 -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -195,25 +234,45 @@
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
import { useGameConfig } from '@/composables/useGameConfig'
|
||||
import { computed } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import ResourceIcon from '@/components/common/ResourceIcon.vue'
|
||||
import { formatNumber, getResourceColor } from '@/utils/format'
|
||||
import { scaleNumber } from '@/utils/speed'
|
||||
import type { Planet } from '@/types/game'
|
||||
import { TechnologyType } from '@/types/game'
|
||||
import * as publicLogic from '@/logic/publicLogic'
|
||||
import * as resourceLogic from '@/logic/resourceLogic'
|
||||
import * as planetLogic from '@/logic/planetLogic'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const { t } = useI18n()
|
||||
const { SHIPS } = useGameConfig()
|
||||
const planet = computed(() => gameStore.currentPlanet)
|
||||
|
||||
// 获取科技加成
|
||||
const techBonuses = computed(() => ({
|
||||
mineralResearchLevel: gameStore.player.technologies[TechnologyType.MineralResearch] || 0,
|
||||
crystalResearchLevel: gameStore.player.technologies[TechnologyType.CrystalResearch] || 0,
|
||||
fuelResearchLevel: gameStore.player.technologies[TechnologyType.FuelResearch] || 0
|
||||
}))
|
||||
|
||||
const production = computed(() =>
|
||||
planet.value ? publicLogic.getResourceProduction(planet.value, gameStore.player.officers, gameStore.gameSpeed) : null
|
||||
planet.value ? publicLogic.getResourceProduction(planet.value, gameStore.player.officers, gameStore.gameSpeed, techBonuses.value) : null
|
||||
)
|
||||
const capacity = computed(() => (planet.value ? publicLogic.getResourceCapacity(planet.value, gameStore.player.officers) : null))
|
||||
|
||||
@@ -226,7 +285,7 @@
|
||||
// 资源产量详细breakdown
|
||||
const productionBreakdown = computed(() => {
|
||||
if (!planet.value) return null
|
||||
return resourceLogic.calculateProductionBreakdown(planet.value, gameStore.player.officers, Date.now(), gameStore.gameSpeed)
|
||||
return resourceLogic.calculateProductionBreakdown(planet.value, gameStore.player.officers, Date.now(), gameStore.gameSpeed, techBonuses.value)
|
||||
})
|
||||
|
||||
// 资源消耗详细breakdown
|
||||
@@ -286,4 +345,45 @@
|
||||
gameStore.currentPlanetId = planet.value.parentPlanetId
|
||||
}
|
||||
}
|
||||
|
||||
// 放弃殖民地相关
|
||||
const showAbandonDialog = ref(false)
|
||||
const showErrorDialog = ref(false)
|
||||
const errorMessage = ref('')
|
||||
|
||||
// 是否显示放弃按钮(非主星才显示)
|
||||
const canShowAbandonButton = computed(() => {
|
||||
if (!planet.value) return false
|
||||
// 找到主星(第一个非月球星球)
|
||||
const mainPlanet = gameStore.player.planets.find(p => !p.isMoon)
|
||||
// 当前星球不是主星时才显示放弃按钮
|
||||
return mainPlanet && mainPlanet.id !== planet.value.id
|
||||
})
|
||||
|
||||
// 处理放弃殖民地
|
||||
const handleAbandonColony = () => {
|
||||
if (!planet.value) return
|
||||
|
||||
const check = planetLogic.canAbandonColony(gameStore.player.planets, planet.value.id)
|
||||
if (!check.canAbandon) {
|
||||
showAbandonDialog.value = false
|
||||
errorMessage.value = check.reason ? t(check.reason) : t('planet.abandonFailed')
|
||||
showErrorDialog.value = true
|
||||
return
|
||||
}
|
||||
|
||||
// 记录当前星球ID用于后续切换
|
||||
const abandonedPlanetId = planet.value.id
|
||||
|
||||
// 执行放弃
|
||||
gameStore.player.planets = planetLogic.abandonColony(gameStore.player.planets, abandonedPlanetId)
|
||||
|
||||
// 切换到主星
|
||||
const mainPlanet = gameStore.player.planets.find(p => !p.isMoon)
|
||||
if (mainPlanet) {
|
||||
gameStore.currentPlanetId = mainPlanet.id
|
||||
}
|
||||
|
||||
showAbandonDialog.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -175,7 +175,8 @@
|
||||
techType,
|
||||
currentLevel,
|
||||
gameStore.player.officers,
|
||||
gameStore.player.technologies
|
||||
gameStore.player.technologies,
|
||||
gameStore.player.planets
|
||||
)
|
||||
gameStore.player.researchQueue.push(queueItem)
|
||||
return true
|
||||
@@ -333,11 +334,33 @@
|
||||
const getResearchTime = (techType: TechnologyType): number => {
|
||||
if (!planet.value) return 0
|
||||
const currentLevel = getTechLevel(techType)
|
||||
const researchLabLevel = planet.value.buildings['researchLab'] || 0
|
||||
const intergalacticResearchNetworkLevel = player.value.technologies[TechnologyType.IntergalacticResearchNetwork] || 0
|
||||
|
||||
// 计算有效研究实验室等级(考虑星际研究网络)
|
||||
let researchLabLevel: number
|
||||
if (intergalacticResearchNetworkLevel > 0) {
|
||||
researchLabLevel = researchLogic.calculateEffectiveLabLevel(
|
||||
gameStore.player.planets,
|
||||
planet.value.id,
|
||||
intergalacticResearchNetworkLevel
|
||||
)
|
||||
} else {
|
||||
researchLabLevel = planet.value.buildings['researchLab'] || 0
|
||||
}
|
||||
|
||||
const energyTechLevel = player.value.technologies['energyTechnology'] || 0
|
||||
const universityLevel = planet.value.buildings['university'] || 0
|
||||
const bonuses = officerLogic.calculateActiveBonuses(player.value.officers, gameStore.gameTime)
|
||||
|
||||
return researchLogic.calculateTechnologyTime(techType, currentLevel, bonuses.researchSpeedBonus, researchLabLevel, energyTechLevel)
|
||||
return researchLogic.calculateTechnologyTime(
|
||||
techType,
|
||||
currentLevel,
|
||||
bonuses.researchSpeedBonus,
|
||||
researchLabLevel,
|
||||
energyTechLevel,
|
||||
1,
|
||||
universityLevel
|
||||
)
|
||||
}
|
||||
|
||||
// 检查是否可以添加到等待队列
|
||||
|
||||
@@ -114,6 +114,15 @@
|
||||
{{ gameStore.isPaused ? t('settings.resume') : t('settings.pause') }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- 战斗模式 -->
|
||||
<div class="flex items-center justify-between p-4 border rounded-lg">
|
||||
<div class="space-y-1">
|
||||
<h3 class="font-medium">{{ t('settings.battleMode') }}</h3>
|
||||
<p class="text-sm text-muted-foreground">{{ t('settings.battleModeDesc') }}</p>
|
||||
</div>
|
||||
<Switch :checked="gameStore.battleToFinish" @update:checked="(val: boolean) => (gameStore.battleToFinish = val)" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -581,12 +590,22 @@
|
||||
const fileName = `${pkg.name}-${new Date().toISOString().slice(0, 10)}-${Date.now()}.json`
|
||||
const jsonString = JSON.stringify(exportData, null, 2)
|
||||
|
||||
// Android 保存到 Documents 目录
|
||||
// Android 保存到公共 Downloads 目录
|
||||
if (Capacitor.isNativePlatform()) {
|
||||
// 检查并请求存储权限
|
||||
const permStatus = await Filesystem.checkPermissions()
|
||||
if (permStatus.publicStorage !== 'granted') {
|
||||
const reqResult = await Filesystem.requestPermissions()
|
||||
if (reqResult.publicStorage !== 'granted') {
|
||||
toast.error(t('settings.storagePermissionDenied'))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const result = await Filesystem.writeFile({
|
||||
path: fileName,
|
||||
path: `Download/${fileName}`,
|
||||
data: jsonString,
|
||||
directory: Directory.Documents,
|
||||
directory: Directory.ExternalStorage,
|
||||
encoding: Encoding.UTF8
|
||||
})
|
||||
toast.success(t('settings.exportSuccessWithPath', { path: result.uri }))
|
||||
|
||||
@@ -101,6 +101,20 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 拆除数量输入 -->
|
||||
<div v-if="(planet.fleet[shipType] || 0) > 0" class="space-y-2">
|
||||
<Label :for="`scrap-quantity-${shipType}`" class="text-xs sm:text-sm text-destructive">{{ t('shipyardView.scrapQuantity') }}</Label>
|
||||
<Input
|
||||
:id="`scrap-quantity-${shipType}`"
|
||||
v-model.number="scrapQuantities[shipType]"
|
||||
type="number"
|
||||
min="0"
|
||||
:max="planet.fleet[shipType] || 0"
|
||||
placeholder="0"
|
||||
class="text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="quantities[shipType] > 0" class="text-xs sm:text-sm space-y-1.5 sm:space-y-2 p-2.5 sm:p-3 bg-muted rounded-lg">
|
||||
<p class="font-medium text-muted-foreground">{{ t('shipyardView.totalCost') }}:</p>
|
||||
<div class="space-y-1 sm:space-y-1.5">
|
||||
@@ -133,6 +147,36 @@
|
||||
>
|
||||
{{ t('queue.addToWaiting') }}
|
||||
</Button>
|
||||
|
||||
<!-- 拆除返还资源显示 -->
|
||||
<div v-if="scrapQuantities[shipType] > 0" class="text-xs sm:text-sm space-y-1.5 sm:space-y-2 p-2.5 sm:p-3 bg-destructive/10 rounded-lg border border-destructive/30">
|
||||
<p class="font-medium text-destructive">{{ t('shipyardView.scrapRefund') }}:</p>
|
||||
<div class="space-y-1 sm:space-y-1.5">
|
||||
<div
|
||||
v-for="resourceType in costResourceTypes"
|
||||
:key="resourceType.key"
|
||||
v-show="resourceType.key !== 'darkMatter' || getScrapRefund(shipType).darkMatter > 0"
|
||||
class="flex items-center gap-1.5 sm:gap-2"
|
||||
>
|
||||
<ResourceIcon :type="resourceType.key" size="sm" />
|
||||
<span class="text-xs">{{ t(`resources.${resourceType.key}`) }}:</span>
|
||||
<span class="font-medium text-xs sm:text-sm text-green-500">
|
||||
+{{ formatNumber(getScrapRefund(shipType)[resourceType.key]) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 拆除按钮 -->
|
||||
<Button
|
||||
v-if="(planet.fleet[shipType] || 0) > 0"
|
||||
@click="handleScrap(shipType, $event)"
|
||||
:disabled="!canScrap(shipType)"
|
||||
variant="destructive"
|
||||
class="w-full"
|
||||
>
|
||||
{{ t('shipyardView.scrap') }}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -243,6 +287,25 @@
|
||||
[ShipType.Deathstar]: 0
|
||||
})
|
||||
|
||||
// 每种舰船的拆除数量
|
||||
const scrapQuantities = ref<Record<ShipType, number>>({
|
||||
[ShipType.LightFighter]: 0,
|
||||
[ShipType.HeavyFighter]: 0,
|
||||
[ShipType.Cruiser]: 0,
|
||||
[ShipType.Battleship]: 0,
|
||||
[ShipType.Battlecruiser]: 0,
|
||||
[ShipType.Bomber]: 0,
|
||||
[ShipType.Destroyer]: 0,
|
||||
[ShipType.SmallCargo]: 0,
|
||||
[ShipType.LargeCargo]: 0,
|
||||
[ShipType.ColonyShip]: 0,
|
||||
[ShipType.Recycler]: 0,
|
||||
[ShipType.EspionageProbe]: 0,
|
||||
[ShipType.SolarSatellite]: 0,
|
||||
[ShipType.DarkMatterHarvester]: 0,
|
||||
[ShipType.Deathstar]: 0
|
||||
})
|
||||
|
||||
const buildShip = (shipType: ShipType, quantity: number): { success: boolean; reason?: string } => {
|
||||
if (!gameStore.currentPlanet) return { success: false }
|
||||
const validation = shipValidation.validateShipBuild(gameStore.currentPlanet, shipType, quantity, gameStore.player.technologies)
|
||||
@@ -380,4 +443,54 @@
|
||||
waitingQueueLogic.addToBuildWaitingQueue(planet.value, item)
|
||||
quantities.value[shipType] = 0
|
||||
}
|
||||
|
||||
// 计算拆除返还资源
|
||||
const getScrapRefund = (shipType: ShipType) => {
|
||||
const quantity = scrapQuantities.value[shipType]
|
||||
return shipLogic.calculateShipScrapRefund(shipType, quantity)
|
||||
}
|
||||
|
||||
// 检查是否可以拆除
|
||||
const canScrap = (shipType: ShipType): boolean => {
|
||||
if (!planet.value) return false
|
||||
|
||||
const quantity = scrapQuantities.value[shipType]
|
||||
if (quantity <= 0) return false
|
||||
|
||||
const available = planet.value.fleet[shipType] || 0
|
||||
return available >= quantity
|
||||
}
|
||||
|
||||
// 拆除舰船
|
||||
const handleScrap = (shipType: ShipType, _event: MouseEvent) => {
|
||||
// 防抖:防止快速点击
|
||||
if (isProcessing.value) return
|
||||
isProcessing.value = true
|
||||
setTimeout(() => {
|
||||
isProcessing.value = false
|
||||
}, DEBOUNCE_DELAY)
|
||||
|
||||
const quantity = scrapQuantities.value[shipType]
|
||||
if (quantity <= 0) {
|
||||
alertDialogTitle.value = t('shipyardView.inputError')
|
||||
alertDialogMessage.value = t('shipyardView.inputErrorMessage')
|
||||
alertDialogOpen.value = true
|
||||
return
|
||||
}
|
||||
|
||||
if (!gameStore.currentPlanet) return
|
||||
|
||||
const validation = shipValidation.validateShipScrap(gameStore.currentPlanet, shipType, quantity)
|
||||
if (!validation.valid) {
|
||||
alertDialogTitle.value = t('shipyardView.scrapFailed')
|
||||
alertDialogMessage.value = validation.reason ? t(validation.reason) : t('shipyardView.scrapFailedMessage')
|
||||
alertDialogOpen.value = true
|
||||
return
|
||||
}
|
||||
|
||||
// 执行拆除
|
||||
const queueItem = shipValidation.executeShipScrap(gameStore.currentPlanet, shipType, quantity, gameStore.player.officers)
|
||||
gameStore.currentPlanet.buildQueue.push(queueItem)
|
||||
scrapQuantities.value[shipType] = 0
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user