Files
ogame-vue-ts/src/views/BuildingsView.vue
谦君 1368bb4445 feat: 新增Android平台支持及构建流程
集成Android平台相关目录与配置文件,包含Gradle构建脚本、资源文件、启动图标、Java入口、Proguard规则等,完善.gitignore以排除Android构建产物。更新CI流程,支持自动构建并发布Android APK。移除README中项目结构说明,简化文档。
2025-12-20 00:48:36 +08:00

486 lines
20 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div v-if="planet" class="container mx-auto p-4 sm:p-6">
<h1 class="text-2xl sm:text-3xl font-bold mb-4 sm:mb-6">{{ t('buildingsView.title') }}</h1>
<!-- 占地显示 -->
<div class="mb-4 sm:mb-6 p-3 sm:p-4 bg-muted/50 rounded-lg border">
<div class="flex items-center justify-between">
<div class="text-sm sm:text-base font-medium flex items-center gap-2">
<Grid3x3 :size="16" />
{{ t('buildingsView.spaceUsage') }}:
</div>
<div class="text-sm sm:text-base font-bold">
<span :class="getUsedSpace(planet) > planet.maxSpace ? 'text-destructive' : 'text-primary'">
{{ formatNumber(getUsedSpace(planet)) }}
</span>
<span class="text-muted-foreground mx-1">/</span>
<span>{{ formatNumber(planet.maxSpace) }}</span>
</div>
</div>
<div class="mt-2">
<div class="w-full bg-background rounded-full h-2.5 sm:h-3 overflow-hidden">
<div
class="h-full transition-all duration-300"
:class="getUsedSpace(planet) > planet.maxSpace ? 'bg-destructive' : 'bg-primary'"
:style="{ width: `${Math.min((getUsedSpace(planet) / planet.maxSpace) * 100, 100)}%` }"
/>
</div>
</div>
</div>
<div class="grid grid-cols-2 lg:grid-cols-3 gap-3 sm:gap-4">
<Card v-for="buildingType in availableBuildings" :key="buildingType" :data-building="buildingType" class="relative">
<!-- 前置条件遮罩 -->
<CardUnlockOverlay :requirements="BUILDINGS[buildingType].requirements" :currentLevel="getBuildingLevel(buildingType)" />
<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.openBuilding(buildingType, getBuildingLevel(buildingType))"
>
{{ BUILDINGS[buildingType].name }}
</CardTitle>
<Badge variant="secondary" class="text-xs whitespace-nowrap self-start order-1 sm:order-2">
Lv {{ getBuildingLevel(buildingType) }}
</Badge>
</div>
</div>
<CardDescription class="text-xs sm:text-sm">{{ BUILDINGS[buildingType].description }}</CardDescription>
</CardHeader>
<CardContent>
<div class="space-y-3">
<div class="text-xs sm:text-sm space-y-1.5 sm:space-y-2">
<p class="text-muted-foreground mb-1 sm:mb-2">{{ t('buildingsView.upgradeCost') }}:</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' || getBuildingCost(buildingType, getBuildingLevel(buildingType) + 1).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"
:class="
getResourceCostColor(
planet.resources[resourceType.key],
getBuildingCost(buildingType, getBuildingLevel(buildingType) + 1)[resourceType.key]
)
"
>
{{ formatNumber(getBuildingCost(buildingType, getBuildingLevel(buildingType) + 1)[resourceType.key]) }}
</span>
</div>
</div>
</div>
<div class="text-xs sm:text-sm space-y-0.5 sm:space-y-1">
<div class="flex items-center gap-1.5 text-muted-foreground">
<Clock :size="14" class="flex-shrink-0" />
<span>{{ formatTime(getBuildingTime(buildingType, getBuildingLevel(buildingType) + 1)) }}</span>
</div>
<div class="flex items-center gap-1.5 text-muted-foreground">
<Grid3x3 :size="14" class="flex-shrink-0" />
<span>{{ BUILDINGS[buildingType].spaceUsage }}</span>
</div>
</div>
<!-- 升级按钮 -->
<Button @click="handleUpgrade(buildingType)" :disabled="!canUpgrade(buildingType)" class="w-full">
{{ getUpgradeButtonText(buildingType) }}
</Button>
<!-- 拆除按钮 -->
<Button
v-if="getBuildingLevel(buildingType) > 0"
@click="handleDemolish(buildingType)"
:disabled="!canDemolish(buildingType)"
variant="destructive"
class="w-full"
>
{{ t('buildingsView.demolish') }}
</Button>
<!-- 拆除信息提示 -->
<div v-if="getBuildingLevel(buildingType) > 0" class="text-xs text-muted-foreground">
<p>{{ t('buildingsView.demolishRefund') }}:</p>
<div class="flex gap-2 flex-wrap">
<span>{{ formatNumber(getDemolishRefund(buildingType).metal) }} {{ t('resources.metal') }}</span>
<span>{{ formatNumber(getDemolishRefund(buildingType).crystal) }} {{ t('resources.crystal') }}</span>
<span>{{ formatNumber(getDemolishRefund(buildingType).deuterium) }} {{ t('resources.deuterium') }}</span>
<span v-if="getDemolishRefund(buildingType).darkMatter > 0">
{{ formatNumber(getDemolishRefund(buildingType).darkMatter) }} {{ t('resources.darkMatter') }}
</span>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
<!-- 提示对话框 -->
<AlertDialog :open="alertDialogOpen" @update:open="alertDialogOpen = $event">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{{ alertDialogTitle }}</AlertDialogTitle>
<AlertDialogDescription v-if="!alertDialogShowRequirements" class="whitespace-pre-line">
{{ alertDialogMessage }}
</AlertDialogDescription>
<AlertDialogDescription v-else>
<div class="space-y-2">
<div v-for="(req, index) in alertDialogRequirements" :key="index" class="flex items-center gap-2 text-sm">
<Check v-if="req.met" :size="16" class="text-green-500 flex-shrink-0" />
<X v-else :size="16" class="text-red-500 flex-shrink-0" />
<span>{{ req.name }}: Lv {{ req.requiredLevel }} ({{ t('common.current') }}: Lv {{ req.currentLevel }})</span>
</div>
</div>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogAction>{{ t('common.confirm') }}</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<!-- 拆除确认对话框 -->
<AlertDialog :open="demolishConfirmOpen" @update:open="demolishConfirmOpen = $event">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{{ t('buildingsView.confirmDemolish') }}</AlertDialogTitle>
<AlertDialogDescription class="whitespace-pre-line">
{{ demolishConfirmMessage }}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{{ t('common.cancel') }}</AlertDialogCancel>
<AlertDialogAction @click="confirmDemolish">{{ t('common.confirm') }}</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</template>
<script setup lang="ts">
import { useGameStore } from '@/stores/gameStore'
import { useDetailDialogStore } from '@/stores/detailDialogStore'
import { useI18n } from '@/composables/useI18n'
import { useGameConfig } from '@/composables/useGameConfig'
import { computed, ref } from 'vue'
import { BuildingType, TechnologyType } from '@/types/game'
import type { Resources, Planet } from '@/types/game'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import ResourceIcon from '@/components/ResourceIcon.vue'
import CardUnlockOverlay from '@/components/CardUnlockOverlay.vue'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle
} from '@/components/ui/alert-dialog'
import { Clock, Grid3x3, Check, X } from 'lucide-vue-next'
import { formatNumber, formatTime, getResourceCostColor } from '@/utils/format'
import * as buildingLogic from '@/logic/buildingLogic'
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()
const { t } = useI18n()
const { BUILDINGS, TECHNOLOGIES } = useGameConfig()
const planet = computed(() => gameStore.currentPlanet)
// AlertDialog 状态
const alertDialogOpen = ref(false)
const alertDialogTitle = ref('')
const alertDialogMessage = ref('')
const alertDialogRequirements = ref<Array<{ name: string; requiredLevel: number; currentLevel: number; met: boolean }>>([])
const alertDialogShowRequirements = ref(false)
// 拆除确认对话框状态
const demolishConfirmOpen = ref(false)
const demolishConfirmMessage = ref('')
const pendingDemolishBuilding = ref<BuildingType | null>(null)
// 资源类型配置(用于成本显示)
const costResourceTypes = [
{ key: 'metal' as const },
{ key: 'crystal' as const },
{ key: 'deuterium' as const },
{ key: 'darkMatter' as const }
]
// 根据星球类型过滤可用建筑
const availableBuildings = computed<BuildingType[]>(() => {
if (!planet.value) return []
return (Object.values(BuildingType) as BuildingType[]).filter(buildingType => {
const config = BUILDINGS.value[buildingType]
if (planet.value!.isMoon) {
// 月球可以建造:月球专属建筑 + 非行星专属建筑(如机器人工厂、船坞、机库等)
// OGame规则月球不能建造 planetOnly 的建筑(矿场、研究实验室、纳米工厂等)
return config.planetOnly !== true
} else {
// 行星不能建造月球专属建筑
return config.moonOnly !== true
}
})
})
const upgradeBuilding = (buildingType: BuildingType): { success: boolean; reason?: string } => {
if (!gameStore.currentPlanet) return { success: false }
const validation = buildingValidation.validateBuildingUpgrade(
gameStore.currentPlanet,
buildingType,
gameStore.player.technologies,
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 }
}
const getUsedSpace = (planet: Planet): number => {
return buildingLogic.calculateUsedSpace(planet)
}
// 升级建筑
const handleUpgrade = (buildingType: BuildingType) => {
// 检查前置条件
if (!checkUpgradeRequirements(buildingType)) {
alertDialogTitle.value = t('common.requirementsNotMet')
alertDialogRequirements.value = getRequirementsList(buildingType)
alertDialogShowRequirements.value = true
alertDialogMessage.value = ''
alertDialogOpen.value = true
return
}
const result = upgradeBuilding(buildingType)
if (!result.success) {
alertDialogTitle.value = t('buildingsView.upgradeFailed')
alertDialogMessage.value = result.reason ? t(result.reason) : t('buildingsView.upgradeFailedMessage')
alertDialogShowRequirements.value = false
alertDialogOpen.value = true
}
}
// 获取建筑等级
const getBuildingLevel = (buildingType: BuildingType): number => {
return planet.value?.buildings[buildingType] || 0
}
// 检查升级前置条件是否满足
const checkUpgradeRequirements = (buildingType: BuildingType): boolean => {
if (!planet.value) return false
const config = BUILDINGS.value[buildingType]
const currentLevel = getBuildingLevel(buildingType)
const targetLevel = currentLevel + 1
// 获取目标等级的所有前置条件(包括等级门槛)
const requirements = publicLogic.getLevelRequirements(config, targetLevel)
if (!requirements || Object.keys(requirements).length === 0) return true
return publicLogic.checkRequirements(planet.value, gameStore.player.technologies, requirements)
}
// 获取升级按钮文本
const getUpgradeButtonText = (buildingType: BuildingType): string => {
if (!planet.value) return t('buildingsView.upgrade')
const config = BUILDINGS.value[buildingType]
const currentLevel = getBuildingLevel(buildingType)
// 检查是否达到等级上限
if (config.maxLevel !== undefined && currentLevel >= config.maxLevel) {
return t('buildingsView.maxLevelReached') // "等级已满"
}
// 0级为建造1级及以上为升级
const buttonTextKey = currentLevel === 0 ? 'buildingsView.build' : 'buildingsView.upgrade'
if (planet.value.buildQueue.length > 0) return t(buttonTextKey)
// 检查前置条件
if (!checkUpgradeRequirements(buildingType)) {
return t('buildingsView.requirementsNotMet')
}
return t(buttonTextKey)
}
// 获取前置条件列表
const getRequirementsList = (
buildingType: BuildingType
): Array<{ name: string; requiredLevel: number; currentLevel: number; met: boolean }> => {
const config = BUILDINGS.value[buildingType]
const currentLevel = getBuildingLevel(buildingType)
const targetLevel = currentLevel + 1
// 获取目标等级的所有前置条件(包括等级门槛)
const requirements = publicLogic.getLevelRequirements(config, targetLevel)
if (!requirements || !planet.value) return []
const items: Array<{ name: string; requiredLevel: number; currentLevel: number; met: boolean }> = []
for (const [key, requiredLevel] of Object.entries(requirements)) {
// 检查是否为建筑类型
if (Object.values(BuildingType).includes(key as BuildingType)) {
const bt = key as BuildingType
const currentLevel = planet.value.buildings[bt] || 0
const name = BUILDINGS.value[bt]?.name || bt
items.push({ name, requiredLevel, currentLevel, met: currentLevel >= requiredLevel })
}
// 检查是否为科技类型
else if (Object.values(TechnologyType).includes(key as TechnologyType)) {
const tt = key as TechnologyType
const currentLevel = gameStore.player.technologies[tt] || 0
const name = TECHNOLOGIES.value[tt]?.name || tt
items.push({ name, requiredLevel, currentLevel, met: currentLevel >= requiredLevel })
}
}
return items
}
// 检查是否可以升级
const canUpgrade = (buildingType: BuildingType): boolean => {
if (!planet.value) return false
const config = BUILDINGS.value[buildingType]
const currentLevel = getBuildingLevel(buildingType)
// 检查是否达到等级上限
if (config.maxLevel !== undefined && currentLevel >= config.maxLevel) {
return false
}
// 检查建造队列是否已满(只计算建筑类型的队列项)
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, Date.now())
const maxQueue = publicLogic.getMaxBuildQueue(planet.value, bonuses.additionalBuildQueue)
const buildingQueueCount = planet.value.buildQueue.filter(item => item.type === 'building' || item.type === 'demolish').length
if (buildingQueueCount >= maxQueue) {
return false
}
// 检查前置条件
const validation = buildingValidation.validateBuildingUpgrade(
planet.value,
buildingType,
gameStore.player.technologies,
gameStore.player.officers
)
if (!validation.valid) return false
const cost = getBuildingCost(buildingType, currentLevel + 1)
return (
planet.value.resources.metal >= cost.metal &&
planet.value.resources.crystal >= cost.crystal &&
planet.value.resources.deuterium >= cost.deuterium &&
planet.value.resources.darkMatter >= cost.darkMatter
)
}
const getBuildingCost = (buildingType: BuildingType, targetLevel: number): Resources => {
return buildingLogic.calculateBuildingCost(buildingType, targetLevel)
}
const getBuildingTime = (buildingType: BuildingType, targetLevel: number): number => {
if (!planet.value) return 0
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, Date.now())
// 获取机器人工厂和纳米工厂等级
const roboticsFactoryLevel = planet.value.buildings[BuildingType.RoboticsFactory] || 0
const naniteFactoryLevel = planet.value.buildings[BuildingType.NaniteFactory] || 0
return buildingLogic.calculateBuildingTime(
buildingType,
targetLevel,
bonuses.buildingSpeedBonus,
roboticsFactoryLevel,
naniteFactoryLevel
)
}
// 拆除建筑
const demolishBuilding = (buildingType: BuildingType): boolean => {
if (!gameStore.currentPlanet) return false
const validation = buildingValidation.validateBuildingDemolish(gameStore.currentPlanet, buildingType, gameStore.player.officers)
if (!validation.valid) return false
const queueItem = buildingValidation.executeBuildingDemolish(gameStore.currentPlanet, buildingType, gameStore.player.officers)
gameStore.currentPlanet.buildQueue.push(queueItem)
return true
}
const handleDemolish = (buildingType: BuildingType) => {
const buildingName = BUILDINGS.value[buildingType].name
const refund = getDemolishRefund(buildingType)
demolishConfirmMessage.value = `${t('buildingsView.confirmDemolishMessage')}: ${buildingName}
${t('buildingsView.demolishRefund')}:
${t('resources.metal')}: ${formatNumber(refund.metal)}
${t('resources.crystal')}: ${formatNumber(refund.crystal)}
${t('resources.deuterium')}: ${formatNumber(refund.deuterium)}${
refund.darkMatter > 0 ? `\n${t('resources.darkMatter')}: ${formatNumber(refund.darkMatter)}` : ''
}`
pendingDemolishBuilding.value = buildingType
demolishConfirmOpen.value = true
}
const confirmDemolish = () => {
if (pendingDemolishBuilding.value) {
const success = demolishBuilding(pendingDemolishBuilding.value)
if (!success) {
alertDialogTitle.value = t('buildingsView.demolishFailed')
alertDialogMessage.value = t('buildingsView.demolishFailedMessage')
alertDialogOpen.value = true
}
}
demolishConfirmOpen.value = false
pendingDemolishBuilding.value = null
}
// 检查是否可以拆除
const canDemolish = (buildingType: BuildingType): boolean => {
if (!planet.value) return false
if (getBuildingLevel(buildingType) <= 0) return false
// 检查建造队列是否已满(只计算建筑类型的队列项)
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, Date.now())
const maxQueue = publicLogic.getMaxBuildQueue(planet.value, bonuses.additionalBuildQueue)
const buildingQueueCount = planet.value.buildQueue.filter(item => item.type === 'building' || item.type === 'demolish').length
if (buildingQueueCount >= maxQueue) {
return false
}
return true
}
// 获取拆除返还资源
const getDemolishRefund = (buildingType: BuildingType): Resources => {
const currentLevel = getBuildingLevel(buildingType)
return buildingLogic.calculateDemolishRefund(buildingType, currentLevel)
}
</script>