mirror of
https://github.com/setube/ogame-vue-ts.git
synced 2026-05-12 07:55:11 +08:00
refactor: 优化UI组件结构与积分系统
重构部分UI组件脚本结构,统一导入风格,提升可维护性。CardUnlockOverlay解锁条件弹窗改为列表展示,提升可读性。修复QueueNotifications滚动区域高度。ScrollableDialogContent增加最大高度。StarsBackground与ParticlesBg组件代码格式优化。App.vue引入玩家积分定时更新逻辑,NPC成长系统补充间谍探测器修复。
This commit is contained in:
66
src/App.vue
66
src/App.vue
@@ -329,13 +329,7 @@
|
|||||||
<main class="flex-1 overflow-y-auto">
|
<main class="flex-1 overflow-y-auto">
|
||||||
<Transition name="page" mode="out-in">
|
<Transition name="page" mode="out-in">
|
||||||
<div :key="$route.fullPath" class="h-full">
|
<div :key="$route.fullPath" class="h-full">
|
||||||
<StarsBackground
|
<StarsBackground v-if="isDark" :factor="0.05" :speed="50" star-color="#fff" class="h-full">
|
||||||
v-if="isDark"
|
|
||||||
:factor="0.05"
|
|
||||||
:speed="50"
|
|
||||||
star-color="#fff"
|
|
||||||
class="h-full"
|
|
||||||
>
|
|
||||||
<div class="relative z-10 h-full">
|
<div class="relative z-10 h-full">
|
||||||
<RouterView />
|
<RouterView />
|
||||||
</div>
|
</div>
|
||||||
@@ -346,16 +340,8 @@
|
|||||||
<RouterView />
|
<RouterView />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ParticlesBg
|
<ParticlesBg class="absolute inset-0 z-0" :quantity="100" :ease="100" color="#000" :staticity="10" refresh />
|
||||||
class="absolute inset-0 z-0"
|
|
||||||
:quantity="100"
|
|
||||||
:ease="100"
|
|
||||||
color="#000"
|
|
||||||
:staticity="10"
|
|
||||||
refresh
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
</main>
|
</main>
|
||||||
@@ -477,12 +463,13 @@
|
|||||||
import * as npcGrowthLogic from '@/logic/npcGrowthLogic'
|
import * as npcGrowthLogic from '@/logic/npcGrowthLogic'
|
||||||
import * as npcBehaviorLogic from '@/logic/npcBehaviorLogic'
|
import * as npcBehaviorLogic from '@/logic/npcBehaviorLogic'
|
||||||
import * as diplomaticLogic from '@/logic/diplomaticLogic'
|
import * as diplomaticLogic from '@/logic/diplomaticLogic'
|
||||||
|
import * as publicLogic from '@/logic/publicLogic'
|
||||||
import pkg from '../package.json'
|
import pkg from '../package.json'
|
||||||
import { toast } from 'vue-sonner'
|
import { toast } from 'vue-sonner'
|
||||||
import { migrateGameData } from '@/utils/migration'
|
import { migrateGameData } from '@/utils/migration'
|
||||||
import { checkLatestVersion } from '@/utils/versionCheck'
|
import { checkLatestVersion } from '@/utils/versionCheck'
|
||||||
import {StarsBackground} from "@/components/ui/bg-stars";
|
import { StarsBackground } from '@/components/ui/bg-stars'
|
||||||
import {ParticlesBg} from "@/components/ui/particles-bg";
|
import { ParticlesBg } from '@/components/ui/particles-bg'
|
||||||
|
|
||||||
// 执行数据迁移(在 store 初始化之前)
|
// 执行数据迁移(在 store 初始化之前)
|
||||||
migrateGameData()
|
migrateGameData()
|
||||||
@@ -539,6 +526,10 @@
|
|||||||
if (Object.keys(universeStore.planets).length === 0) {
|
if (Object.keys(universeStore.planets).length === 0) {
|
||||||
generateNPCPlanets()
|
generateNPCPlanets()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 初始化或更新玩家积分
|
||||||
|
gameStore.player.points = publicLogic.calculatePlayerPoints(gameStore.player)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
gameStore.player = gameLogic.initializePlayer(gameStore.player.id, t('common.playerName'))
|
gameStore.player = gameLogic.initializePlayer(gameStore.player.id, t('common.playerName'))
|
||||||
@@ -547,6 +538,8 @@
|
|||||||
gameStore.currentPlanetId = initialPlanet.id
|
gameStore.currentPlanetId = initialPlanet.id
|
||||||
// 新玩家初始化时生成NPC星球
|
// 新玩家初始化时生成NPC星球
|
||||||
generateNPCPlanets()
|
generateNPCPlanets()
|
||||||
|
// 初始化玩家积分
|
||||||
|
gameStore.player.points = publicLogic.calculatePlayerPoints(gameStore.player)
|
||||||
}
|
}
|
||||||
|
|
||||||
const generateNPCPlanets = () => {
|
const generateNPCPlanets = () => {
|
||||||
@@ -562,8 +555,8 @@
|
|||||||
|
|
||||||
const updateGame = async () => {
|
const updateGame = async () => {
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
gameStore.gameTime = now
|
|
||||||
if (gameStore.isPaused) return
|
if (gameStore.isPaused) return
|
||||||
|
gameStore.gameTime = now
|
||||||
// 检查军官过期
|
// 检查军官过期
|
||||||
gameLogic.checkOfficersExpiration(gameStore.player.officers, now)
|
gameLogic.checkOfficersExpiration(gameStore.player.officers, now)
|
||||||
// 处理游戏更新(建造队列、研究队列等)
|
// 处理游戏更新(建造队列、研究队列等)
|
||||||
@@ -1045,11 +1038,7 @@
|
|||||||
if (!gameStore.player.diplomaticRelations) {
|
if (!gameStore.player.diplomaticRelations) {
|
||||||
gameStore.player.diplomaticRelations = {}
|
gameStore.player.diplomaticRelations = {}
|
||||||
}
|
}
|
||||||
const relation = diplomaticLogic.getOrCreateRelation(
|
const relation = diplomaticLogic.getOrCreateRelation(gameStore.player.diplomaticRelations, gameStore.player.id, targetNpc.id)
|
||||||
gameStore.player.diplomaticRelations,
|
|
||||||
gameStore.player.id,
|
|
||||||
targetNpc.id
|
|
||||||
)
|
|
||||||
gameStore.player.diplomaticRelations[targetNpc.id] = diplomaticLogic.updateReputation(
|
gameStore.player.diplomaticRelations[targetNpc.id] = diplomaticLogic.updateReputation(
|
||||||
relation,
|
relation,
|
||||||
reputationLoss,
|
reputationLoss,
|
||||||
@@ -1123,7 +1112,7 @@
|
|||||||
|
|
||||||
// NPC成长系统更新函数
|
// NPC成长系统更新函数
|
||||||
let npcUpdateCounter = 0 // 累计秒数
|
let npcUpdateCounter = 0 // 累计秒数
|
||||||
const NPC_UPDATE_INTERVAL = 1 // 每1秒更新一次NPC,确保发育速度与玩家相当
|
const NPC_UPDATE_INTERVAL = 5 // 每1秒更新一次NPC,确保发育速度与玩家相当
|
||||||
|
|
||||||
const updateNPCGrowth = (deltaSeconds: number) => {
|
const updateNPCGrowth = (deltaSeconds: number) => {
|
||||||
// 累积时间
|
// 累积时间
|
||||||
@@ -1155,7 +1144,11 @@
|
|||||||
difficulty: 'medium' as const, // 默认中等难度
|
difficulty: 'medium' as const, // 默认中等难度
|
||||||
relations: {}, // 外交关系
|
relations: {}, // 外交关系
|
||||||
allies: [], // 盟友列表
|
allies: [], // 盟友列表
|
||||||
enemies: [] // 敌人列表
|
enemies: [], // 敌人列表
|
||||||
|
lastSpyTime: 0, // 上次侦查时间
|
||||||
|
lastAttackTime: 0, // 上次攻击时间
|
||||||
|
fleetMissions: [], // 舰队任务
|
||||||
|
playerSpyReports: {} // 对玩家的侦查报告
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1184,6 +1177,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 确保所有NPC都有间谍探测器(修复旧版本保存的数据)
|
||||||
|
if (npcStore.npcs.length > 0) {
|
||||||
|
npcGrowthLogic.ensureNPCSpyProbes(npcStore.npcs)
|
||||||
|
}
|
||||||
|
|
||||||
// 如果没有NPC,直接返回
|
// 如果没有NPC,直接返回
|
||||||
if (npcStore.npcs.length === 0) {
|
if (npcStore.npcs.length === 0) {
|
||||||
npcUpdateCounter = 0
|
npcUpdateCounter = 0
|
||||||
@@ -1238,6 +1236,7 @@
|
|||||||
|
|
||||||
// 游戏循环定时器
|
// 游戏循环定时器
|
||||||
let gameLoop: ReturnType<typeof setInterval> | null = null
|
let gameLoop: ReturnType<typeof setInterval> | null = null
|
||||||
|
let pointsUpdateInterval: ReturnType<typeof setInterval> | null = null
|
||||||
let konamiCleanup: (() => void) | null = null
|
let konamiCleanup: (() => void) | null = null
|
||||||
let versionCheckInterval: ReturnType<typeof setInterval> | null = null
|
let versionCheckInterval: ReturnType<typeof setInterval> | null = null
|
||||||
|
|
||||||
@@ -1255,6 +1254,18 @@
|
|||||||
}, interval)
|
}, interval)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 启动积分更新定时器(每10秒更新一次)
|
||||||
|
const startPointsUpdate = () => {
|
||||||
|
if (pointsUpdateInterval) {
|
||||||
|
clearInterval(pointsUpdateInterval)
|
||||||
|
}
|
||||||
|
pointsUpdateInterval = setInterval(() => {
|
||||||
|
if (!gameStore.isPaused) {
|
||||||
|
gameStore.player.points = publicLogic.calculatePlayerPoints(gameStore.player)
|
||||||
|
}
|
||||||
|
}, 10000) // 10秒更新一次
|
||||||
|
}
|
||||||
|
|
||||||
// 监听游戏速度变化,重新启动游戏循环
|
// 监听游戏速度变化,重新启动游戏循环
|
||||||
watch(
|
watch(
|
||||||
() => gameStore.gameSpeed,
|
() => gameStore.gameSpeed,
|
||||||
@@ -1275,6 +1286,8 @@
|
|||||||
await initGame()
|
await initGame()
|
||||||
// 启动游戏循环
|
// 启动游戏循环
|
||||||
startGameLoop()
|
startGameLoop()
|
||||||
|
// 启动积分更新定时器
|
||||||
|
startPointsUpdate()
|
||||||
// 启动科乐美秘籍监听
|
// 启动科乐美秘籍监听
|
||||||
konamiCleanup = setupKonamiCode()
|
konamiCleanup = setupKonamiCode()
|
||||||
|
|
||||||
@@ -1326,6 +1339,7 @@
|
|||||||
// 清理定时器
|
// 清理定时器
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (gameLoop) clearInterval(gameLoop)
|
if (gameLoop) clearInterval(gameLoop)
|
||||||
|
if (pointsUpdateInterval) clearInterval(pointsUpdateInterval)
|
||||||
if (konamiCleanup) konamiCleanup()
|
if (konamiCleanup) konamiCleanup()
|
||||||
if (versionCheckInterval) clearInterval(versionCheckInterval)
|
if (versionCheckInterval) clearInterval(versionCheckInterval)
|
||||||
// 移除队列取消事件监听
|
// 移除队列取消事件监听
|
||||||
|
|||||||
@@ -17,8 +17,14 @@
|
|||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>{{ requirementsDialogTitle }}</AlertDialogTitle>
|
<AlertDialogTitle>{{ requirementsDialogTitle }}</AlertDialogTitle>
|
||||||
<AlertDialogDescription class="whitespace-pre-line">
|
<AlertDialogDescription>
|
||||||
{{ requirementsDialogMessage }}
|
<div class="space-y-2">
|
||||||
|
<div v-for="(req, index) in requirementsDialogItems" :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>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
@@ -35,7 +41,7 @@
|
|||||||
import { useI18n } from '@/composables/useI18n'
|
import { useI18n } from '@/composables/useI18n'
|
||||||
import { useGameConfig } from '@/composables/useGameConfig'
|
import { useGameConfig } from '@/composables/useGameConfig'
|
||||||
import { BuildingType, TechnologyType } from '@/types/game'
|
import { BuildingType, TechnologyType } from '@/types/game'
|
||||||
import { Lock } from 'lucide-vue-next'
|
import { Lock, Check, X } from 'lucide-vue-next'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
@@ -61,7 +67,7 @@
|
|||||||
// AlertDialog 状态
|
// AlertDialog 状态
|
||||||
const requirementsDialogOpen = ref(false)
|
const requirementsDialogOpen = ref(false)
|
||||||
const requirementsDialogTitle = ref('')
|
const requirementsDialogTitle = ref('')
|
||||||
const requirementsDialogMessage = ref('')
|
const requirementsDialogItems = ref<Array<{ name: string; requiredLevel: number; currentLevel: number; met: boolean }>>([])
|
||||||
|
|
||||||
const isUnlocked = computed(() => {
|
const isUnlocked = computed(() => {
|
||||||
// 如果已经建造过(level > 0),则认为已解锁,不显示遮罩
|
// 如果已经建造过(level > 0),则认为已解锁,不显示遮罩
|
||||||
@@ -70,34 +76,32 @@
|
|||||||
return publicLogic.checkRequirements(gameStore.currentPlanet, gameStore.player.technologies, props.requirements)
|
return publicLogic.checkRequirements(gameStore.currentPlanet, gameStore.player.technologies, props.requirements)
|
||||||
})
|
})
|
||||||
|
|
||||||
const getRequirementsList = (): string => {
|
const getRequirementsList = (): Array<{ name: string; requiredLevel: number; currentLevel: number; met: boolean }> => {
|
||||||
if (!props.requirements || !gameStore.currentPlanet) return ''
|
if (!props.requirements || !gameStore.currentPlanet) return []
|
||||||
|
|
||||||
const lines: string[] = []
|
const items: Array<{ name: string; requiredLevel: number; currentLevel: number; met: boolean }> = []
|
||||||
for (const [key, requiredLevel] of Object.entries(props.requirements)) {
|
for (const [key, requiredLevel] of Object.entries(props.requirements)) {
|
||||||
// 检查是否为建筑类型
|
// 检查是否为建筑类型
|
||||||
if (Object.values(BuildingType).includes(key as BuildingType)) {
|
if (Object.values(BuildingType).includes(key as BuildingType)) {
|
||||||
const buildingType = key as BuildingType
|
const buildingType = key as BuildingType
|
||||||
const currentLevel = gameStore.currentPlanet.buildings[buildingType] || 0
|
const currentLevel = gameStore.currentPlanet.buildings[buildingType] || 0
|
||||||
const name = BUILDINGS.value[buildingType]?.name || buildingType
|
const name = BUILDINGS.value[buildingType]?.name || buildingType
|
||||||
const status = currentLevel >= requiredLevel ? '✓' : '✗'
|
items.push({ name, requiredLevel, currentLevel, met: currentLevel >= requiredLevel })
|
||||||
lines.push(`${status} ${name}: Lv ${requiredLevel} (${t('common.current')}: Lv ${currentLevel})`)
|
|
||||||
}
|
}
|
||||||
// 检查是否为科技类型
|
// 检查是否为科技类型
|
||||||
else if (Object.values(TechnologyType).includes(key as TechnologyType)) {
|
else if (Object.values(TechnologyType).includes(key as TechnologyType)) {
|
||||||
const techType = key as TechnologyType
|
const techType = key as TechnologyType
|
||||||
const currentLevel = gameStore.player.technologies[techType] || 0
|
const currentLevel = gameStore.player.technologies[techType] || 0
|
||||||
const name = TECHNOLOGIES.value[techType]?.name || techType
|
const name = TECHNOLOGIES.value[techType]?.name || techType
|
||||||
const status = currentLevel >= requiredLevel ? '✓' : '✗'
|
items.push({ name, requiredLevel, currentLevel, met: currentLevel >= requiredLevel })
|
||||||
lines.push(`${status} ${name}: Lv ${requiredLevel} (${t('common.current')}: Lv ${currentLevel})`)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return lines.join('\n')
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
const showRequirements = () => {
|
const showRequirements = () => {
|
||||||
requirementsDialogTitle.value = t('common.requirementsNotMet')
|
requirementsDialogTitle.value = t('common.requirementsNotMet')
|
||||||
requirementsDialogMessage.value = getRequirementsList()
|
requirementsDialogItems.value = getRequirementsList()
|
||||||
requirementsDialogOpen.value = true
|
requirementsDialogOpen.value = true
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<div class="flex items-center justify-between p-4 border-b">
|
<div class="flex items-center justify-between p-4 border-b">
|
||||||
<h3 class="font-semibold">{{ t('queue.title') }}</h3>
|
<h3 class="font-semibold">{{ t('queue.title') }}</h3>
|
||||||
</div>
|
</div>
|
||||||
<ScrollArea class="max-h-96">
|
<ScrollArea class="h-[480px]">
|
||||||
<div v-if="totalQueueCount === 0" class="p-8 text-center text-muted-foreground">
|
<div v-if="totalQueueCount === 0" class="p-8 text-center text-muted-foreground">
|
||||||
{{ t('queue.empty') }}
|
{{ t('queue.empty') }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,26 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
:class="
|
:class="cn('relative size-full overflow-hidden bg-[radial-gradient(ellipse_at_bottom,_#262626_0%,_#000_100%)]', props.class)"
|
||||||
cn(
|
|
||||||
'relative size-full overflow-hidden bg-[radial-gradient(ellipse_at_bottom,_#262626_0%,_#000_100%)]',
|
|
||||||
props.class,
|
|
||||||
)
|
|
||||||
"
|
|
||||||
@mousemove="handleMouseMove"
|
@mousemove="handleMouseMove"
|
||||||
>
|
>
|
||||||
<motion.div :style="{ x: springX, y: springY }">
|
<motion.div :style="{ x: springX, y: springY }">
|
||||||
<!-- Star Layer 1 -->
|
<!-- Star Layer 1 -->
|
||||||
<motion.div
|
<motion.div class="absolute top-0 left-0 w-full h-[2000px]" :animate="{ y: [0, -2000] }" :transition="starLayer1Transition">
|
||||||
class="absolute top-0 left-0 w-full h-[2000px]"
|
|
||||||
:animate="{ y: [0, -2000] }"
|
|
||||||
:transition="starLayer1Transition"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
class="absolute bg-transparent rounded-full"
|
class="absolute bg-transparent rounded-full"
|
||||||
:style="{
|
:style="{
|
||||||
width: '1px',
|
width: '1px',
|
||||||
height: '1px',
|
height: '1px',
|
||||||
boxShadow: boxShadow1,
|
boxShadow: boxShadow1
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
@@ -28,23 +19,19 @@
|
|||||||
:style="{
|
:style="{
|
||||||
width: '1px',
|
width: '1px',
|
||||||
height: '1px',
|
height: '1px',
|
||||||
boxShadow: boxShadow1,
|
boxShadow: boxShadow1
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<!-- Star Layer 2 -->
|
<!-- Star Layer 2 -->
|
||||||
<motion.div
|
<motion.div class="absolute top-0 left-0 w-full h-[2000px]" :animate="{ y: [0, -2000] }" :transition="starLayer2Transition">
|
||||||
class="absolute top-0 left-0 w-full h-[2000px]"
|
|
||||||
:animate="{ y: [0, -2000] }"
|
|
||||||
:transition="starLayer2Transition"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
class="absolute bg-transparent rounded-full"
|
class="absolute bg-transparent rounded-full"
|
||||||
:style="{
|
:style="{
|
||||||
width: '2px',
|
width: '2px',
|
||||||
height: '2px',
|
height: '2px',
|
||||||
boxShadow: boxShadow2,
|
boxShadow: boxShadow2
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
@@ -52,23 +39,19 @@
|
|||||||
:style="{
|
:style="{
|
||||||
width: '2px',
|
width: '2px',
|
||||||
height: '2px',
|
height: '2px',
|
||||||
boxShadow: boxShadow2,
|
boxShadow: boxShadow2
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<!-- Star Layer 3 -->
|
<!-- Star Layer 3 -->
|
||||||
<motion.div
|
<motion.div class="absolute top-0 left-0 w-full h-[2000px]" :animate="{ y: [0, -2000] }" :transition="starLayer3Transition">
|
||||||
class="absolute top-0 left-0 w-full h-[2000px]"
|
|
||||||
:animate="{ y: [0, -2000] }"
|
|
||||||
:transition="starLayer3Transition"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
class="absolute bg-transparent rounded-full"
|
class="absolute bg-transparent rounded-full"
|
||||||
:style="{
|
:style="{
|
||||||
width: '3px',
|
width: '3px',
|
||||||
height: '3px',
|
height: '3px',
|
||||||
boxShadow: boxShadow3,
|
boxShadow: boxShadow3
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
@@ -76,7 +59,7 @@
|
|||||||
:style="{
|
:style="{
|
||||||
width: '3px',
|
width: '3px',
|
||||||
height: '3px',
|
height: '3px',
|
||||||
boxShadow: boxShadow3,
|
boxShadow: boxShadow3
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -88,89 +71,89 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type {SpringOptions, Transition} from "motion-v";
|
import type { SpringOptions, Transition } from 'motion-v'
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from '@/lib/utils'
|
||||||
import { motion, useMotionValue, useSpring } from "motion-v";
|
import { motion, useMotionValue, useSpring } from 'motion-v'
|
||||||
import { computed, onMounted, ref, watch } from "vue";
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
interface StarsBackgroundProps {
|
interface StarsBackgroundProps {
|
||||||
factor?: number;
|
factor?: number
|
||||||
speed?: number;
|
speed?: number
|
||||||
transition?: SpringOptions;
|
transition?: SpringOptions
|
||||||
starColor?: string;
|
starColor?: string
|
||||||
class?: string;
|
class?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<StarsBackgroundProps>(), {
|
const props = withDefaults(defineProps<StarsBackgroundProps>(), {
|
||||||
factor: 0.05,
|
factor: 0.05,
|
||||||
speed: 50,
|
speed: 50,
|
||||||
transition: () => ({ stiffness: 50, damping: 20 }),
|
transition: () => ({ stiffness: 50, damping: 20 }),
|
||||||
starColor: "#fff",
|
starColor: '#fff'
|
||||||
});
|
})
|
||||||
|
|
||||||
// For slot content
|
// For slot content
|
||||||
defineSlots();
|
defineSlots()
|
||||||
|
|
||||||
function generateStars(count: number, starColor: string) {
|
function generateStars(count: number, starColor: string) {
|
||||||
const shadows: string[] = [];
|
const shadows: string[] = []
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
const x = Math.floor(Math.random() * 4000) - 2000;
|
const x = Math.floor(Math.random() * 4000) - 2000
|
||||||
const y = Math.floor(Math.random() * 4000) - 2000;
|
const y = Math.floor(Math.random() * 4000) - 2000
|
||||||
shadows.push(`${x}px ${y}px ${starColor}`);
|
shadows.push(`${x}px ${y}px ${starColor}`)
|
||||||
}
|
}
|
||||||
return shadows.join(", ");
|
return shadows.join(', ')
|
||||||
}
|
}
|
||||||
|
|
||||||
const offsetX = useMotionValue(1);
|
const offsetX = useMotionValue(1)
|
||||||
const offsetY = useMotionValue(1);
|
const offsetY = useMotionValue(1)
|
||||||
|
|
||||||
const springX = useSpring(offsetX, props.transition);
|
const springX = useSpring(offsetX, props.transition)
|
||||||
const springY = useSpring(offsetY, props.transition);
|
const springY = useSpring(offsetY, props.transition)
|
||||||
|
|
||||||
function handleMouseMove(e: MouseEvent) {
|
function handleMouseMove(e: MouseEvent) {
|
||||||
const centerX = window.innerWidth / 2;
|
const centerX = window.innerWidth / 2
|
||||||
const centerY = window.innerHeight / 2;
|
const centerY = window.innerHeight / 2
|
||||||
const newOffsetX = -(e.clientX - centerX) * props.factor;
|
const newOffsetX = -(e.clientX - centerX) * props.factor
|
||||||
const newOffsetY = -(e.clientY - centerY) * props.factor;
|
const newOffsetY = -(e.clientY - centerY) * props.factor
|
||||||
offsetX.set(newOffsetX);
|
offsetX.set(newOffsetX)
|
||||||
offsetY.set(newOffsetY);
|
offsetY.set(newOffsetY)
|
||||||
}
|
}
|
||||||
|
|
||||||
const boxShadow1 = ref("");
|
const boxShadow1 = ref('')
|
||||||
const boxShadow2 = ref("");
|
const boxShadow2 = ref('')
|
||||||
const boxShadow3 = ref("");
|
const boxShadow3 = ref('')
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
boxShadow1.value = generateStars(1000, props.starColor);
|
boxShadow1.value = generateStars(1000, props.starColor)
|
||||||
boxShadow2.value = generateStars(400, props.starColor);
|
boxShadow2.value = generateStars(400, props.starColor)
|
||||||
boxShadow3.value = generateStars(200, props.starColor);
|
boxShadow3.value = generateStars(200, props.starColor)
|
||||||
});
|
})
|
||||||
|
|
||||||
// Watch for starColor changes
|
// Watch for starColor changes
|
||||||
watch(
|
watch(
|
||||||
() => props.starColor,
|
() => props.starColor,
|
||||||
(newColor) => {
|
newColor => {
|
||||||
boxShadow1.value = generateStars(1000, newColor);
|
boxShadow1.value = generateStars(1000, newColor)
|
||||||
boxShadow2.value = generateStars(400, newColor);
|
boxShadow2.value = generateStars(400, newColor)
|
||||||
boxShadow3.value = generateStars(200, newColor);
|
boxShadow3.value = generateStars(200, newColor)
|
||||||
},
|
}
|
||||||
);
|
)
|
||||||
|
|
||||||
const starLayer1Transition = computed<Transition>(() => ({
|
const starLayer1Transition = computed<Transition>(() => ({
|
||||||
repeat: Infinity,
|
repeat: Infinity,
|
||||||
duration: props.speed,
|
duration: props.speed,
|
||||||
ease: "linear" as const,
|
ease: 'linear' as const
|
||||||
}));
|
}))
|
||||||
|
|
||||||
const starLayer2Transition = computed<Transition>(() => ({
|
const starLayer2Transition = computed<Transition>(() => ({
|
||||||
repeat: Infinity,
|
repeat: Infinity,
|
||||||
duration: props.speed * 2,
|
duration: props.speed * 2,
|
||||||
ease: "linear" as const,
|
ease: 'linear' as const
|
||||||
}));
|
}))
|
||||||
|
|
||||||
const starLayer3Transition = computed<Transition>(() => ({
|
const starLayer3Transition = computed<Transition>(() => ({
|
||||||
repeat: Infinity,
|
repeat: Infinity,
|
||||||
duration: props.speed * 3,
|
duration: props.speed * 3,
|
||||||
ease: "linear" as const,
|
ease: 'linear' as const
|
||||||
}));
|
}))
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export { default as StarsBackground } from "./StarsBackground.vue";
|
export { default as StarsBackground } from './StarsBackground.vue'
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 可滚动的内容区域 -->
|
<!-- 可滚动的内容区域 -->
|
||||||
<div class="overflow-y-auto px-4 py-3 sm:px-6 sm:py-4">
|
<div class="overflow-y-auto px-4 py-3 sm:px-6 sm:py-4 max-h-[60vh]">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,139 +1,135 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div ref="canvasContainerRef" :class="$props.class" aria-hidden="true">
|
||||||
ref="canvasContainerRef"
|
<canvas ref="canvasRef" />
|
||||||
:class="$props.class"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<canvas ref="canvasRef"></canvas>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useMouse, useDevicePixelRatio } from "@vueuse/core";
|
import { useMouse, useDevicePixelRatio } from '@vueuse/core'
|
||||||
import { ref, onMounted, onBeforeUnmount, watch, computed, reactive } from "vue";
|
import { ref, onMounted, onBeforeUnmount, watch, computed, reactive } from 'vue'
|
||||||
|
|
||||||
type Circle = {
|
type Circle = {
|
||||||
x: number;
|
x: number
|
||||||
y: number;
|
y: number
|
||||||
translateX: number;
|
translateX: number
|
||||||
translateY: number;
|
translateY: number
|
||||||
size: number;
|
size: number
|
||||||
alpha: number;
|
alpha: number
|
||||||
targetAlpha: number;
|
targetAlpha: number
|
||||||
dx: number;
|
dx: number
|
||||||
dy: number;
|
dy: number
|
||||||
magnetism: number;
|
magnetism: number
|
||||||
};
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
color?: string;
|
color?: string
|
||||||
quantity?: number;
|
quantity?: number
|
||||||
staticity?: number;
|
staticity?: number
|
||||||
ease?: number;
|
ease?: number
|
||||||
class?: string;
|
class?: string
|
||||||
};
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
color: "#FFF",
|
color: '#FFF',
|
||||||
quantity: 100,
|
quantity: 100,
|
||||||
staticity: 50,
|
staticity: 50,
|
||||||
ease: 50,
|
ease: 50,
|
||||||
class: "",
|
class: ''
|
||||||
});
|
})
|
||||||
|
|
||||||
const canvasRef = ref<HTMLCanvasElement | null>(null);
|
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
||||||
const canvasContainerRef = ref<HTMLDivElement | null>(null);
|
const canvasContainerRef = ref<HTMLDivElement | null>(null)
|
||||||
const context = ref<CanvasRenderingContext2D | null>(null);
|
const context = ref<CanvasRenderingContext2D | null>(null)
|
||||||
const circles = ref<Circle[]>([]);
|
const circles = ref<Circle[]>([])
|
||||||
const mouse = reactive<{ x: number; y: number }>({ x: 0, y: 0 });
|
const mouse = reactive<{ x: number; y: number }>({ x: 0, y: 0 })
|
||||||
const canvasSize = reactive<{ w: number; h: number }>({ w: 0, h: 0 });
|
const canvasSize = reactive<{ w: number; h: number }>({ w: 0, h: 0 })
|
||||||
const { x: mouseX, y: mouseY } = useMouse();
|
const { x: mouseX, y: mouseY } = useMouse()
|
||||||
const { pixelRatio } = useDevicePixelRatio();
|
const { pixelRatio } = useDevicePixelRatio()
|
||||||
|
|
||||||
const color = computed(() => {
|
const color = computed(() => {
|
||||||
// Remove the leading '#' if it's present
|
// Remove the leading '#' if it's present
|
||||||
let hex = props.color.replace(/^#/, "");
|
let hex = props.color.replace(/^#/, '')
|
||||||
|
|
||||||
// If the hex code is 3 characters, expand it to 6 characters
|
// If the hex code is 3 characters, expand it to 6 characters
|
||||||
if (hex.length === 3) {
|
if (hex.length === 3) {
|
||||||
hex = hex
|
hex = hex
|
||||||
.split("")
|
.split('')
|
||||||
.map((char) => char + char)
|
.map(char => char + char)
|
||||||
.join("");
|
.join('')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse the r, g, b values from the hex string
|
// Parse the r, g, b values from the hex string
|
||||||
const bigint = parseInt(hex, 16);
|
const bigint = parseInt(hex, 16)
|
||||||
const r = (bigint >> 16) & 255; // Extract the red component
|
const r = (bigint >> 16) & 255 // Extract the red component
|
||||||
const g = (bigint >> 8) & 255; // Extract the green component
|
const g = (bigint >> 8) & 255 // Extract the green component
|
||||||
const b = bigint & 255; // Extract the blue component
|
const b = bigint & 255 // Extract the blue component
|
||||||
|
|
||||||
// Return the RGB values as a string separated by spaces
|
// Return the RGB values as a string separated by spaces
|
||||||
return `${r} ${g} ${b}`;
|
return `${r} ${g} ${b}`
|
||||||
});
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (canvasRef.value) {
|
if (canvasRef.value) {
|
||||||
context.value = canvasRef.value.getContext("2d");
|
context.value = canvasRef.value.getContext('2d')
|
||||||
}
|
}
|
||||||
|
|
||||||
initCanvas();
|
initCanvas()
|
||||||
animate();
|
animate()
|
||||||
window.addEventListener("resize", initCanvas);
|
window.addEventListener('resize', initCanvas)
|
||||||
});
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
window.removeEventListener("resize", initCanvas);
|
window.removeEventListener('resize', initCanvas)
|
||||||
});
|
})
|
||||||
|
|
||||||
watch([mouseX, mouseY], () => {
|
watch([mouseX, mouseY], () => {
|
||||||
onMouseMove();
|
onMouseMove()
|
||||||
});
|
})
|
||||||
|
|
||||||
function initCanvas() {
|
function initCanvas() {
|
||||||
resizeCanvas();
|
resizeCanvas()
|
||||||
drawParticles();
|
drawParticles()
|
||||||
}
|
}
|
||||||
|
|
||||||
function onMouseMove() {
|
function onMouseMove() {
|
||||||
if (canvasRef.value) {
|
if (canvasRef.value) {
|
||||||
const rect = canvasRef.value.getBoundingClientRect();
|
const rect = canvasRef.value.getBoundingClientRect()
|
||||||
const { w, h } = canvasSize;
|
const { w, h } = canvasSize
|
||||||
const x = mouseX.value - rect.left - w / 2;
|
const x = mouseX.value - rect.left - w / 2
|
||||||
const y = mouseY.value - rect.top - h / 2;
|
const y = mouseY.value - rect.top - h / 2
|
||||||
|
|
||||||
const inside = x < w / 2 && x > -w / 2 && y < h / 2 && y > -h / 2;
|
const inside = x < w / 2 && x > -w / 2 && y < h / 2 && y > -h / 2
|
||||||
if (inside) {
|
if (inside) {
|
||||||
mouse.x = x;
|
mouse.x = x
|
||||||
mouse.y = y;
|
mouse.y = y
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function resizeCanvas() {
|
function resizeCanvas() {
|
||||||
if (canvasContainerRef.value && canvasRef.value && context.value) {
|
if (canvasContainerRef.value && canvasRef.value && context.value) {
|
||||||
circles.value.length = 0;
|
circles.value.length = 0
|
||||||
canvasSize.w = canvasContainerRef.value.offsetWidth;
|
canvasSize.w = canvasContainerRef.value.offsetWidth
|
||||||
canvasSize.h = canvasContainerRef.value.offsetHeight;
|
canvasSize.h = canvasContainerRef.value.offsetHeight
|
||||||
canvasRef.value.width = canvasSize.w * pixelRatio.value;
|
canvasRef.value.width = canvasSize.w * pixelRatio.value
|
||||||
canvasRef.value.height = canvasSize.h * pixelRatio.value;
|
canvasRef.value.height = canvasSize.h * pixelRatio.value
|
||||||
canvasRef.value.style.width = canvasSize.w + "px";
|
canvasRef.value.style.width = canvasSize.w + 'px'
|
||||||
canvasRef.value.style.height = canvasSize.h + "px";
|
canvasRef.value.style.height = canvasSize.h + 'px'
|
||||||
context.value.scale(pixelRatio.value, pixelRatio.value);
|
context.value.scale(pixelRatio.value, pixelRatio.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function circleParams(): Circle {
|
function circleParams(): Circle {
|
||||||
const x = Math.floor(Math.random() * canvasSize.w);
|
const x = Math.floor(Math.random() * canvasSize.w)
|
||||||
const y = Math.floor(Math.random() * canvasSize.h);
|
const y = Math.floor(Math.random() * canvasSize.h)
|
||||||
const translateX = 0;
|
const translateX = 0
|
||||||
const translateY = 0;
|
const translateY = 0
|
||||||
const size = Math.floor(Math.random() * 2) + 1;
|
const size = Math.floor(Math.random() * 2) + 1
|
||||||
const alpha = 0;
|
const alpha = 0
|
||||||
const targetAlpha = parseFloat((Math.random() * 0.6 + 0.1).toFixed(1));
|
const targetAlpha = parseFloat((Math.random() * 0.6 + 0.1).toFixed(1))
|
||||||
const dx = (Math.random() - 0.5) * 0.2;
|
const dx = (Math.random() - 0.5) * 0.2
|
||||||
const dy = (Math.random() - 0.5) * 0.2;
|
const dy = (Math.random() - 0.5) * 0.2
|
||||||
const magnetism = 0.1 + Math.random() * 4;
|
const magnetism = 0.1 + Math.random() * 4
|
||||||
return {
|
return {
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
@@ -144,79 +140,71 @@ function circleParams(): Circle {
|
|||||||
targetAlpha,
|
targetAlpha,
|
||||||
dx,
|
dx,
|
||||||
dy,
|
dy,
|
||||||
magnetism,
|
magnetism
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawCircle(circle: Circle, update = false) {
|
function drawCircle(circle: Circle, update = false) {
|
||||||
if (context.value) {
|
if (context.value) {
|
||||||
const { x, y, translateX, translateY, size, alpha } = circle;
|
const { x, y, translateX, translateY, size, alpha } = circle
|
||||||
context.value.translate(translateX, translateY);
|
context.value.translate(translateX, translateY)
|
||||||
context.value.beginPath();
|
context.value.beginPath()
|
||||||
context.value.arc(x, y, size, 0, 2 * Math.PI);
|
context.value.arc(x, y, size, 0, 2 * Math.PI)
|
||||||
context.value.fillStyle = `rgba(${color.value.split(" ").join(", ")}, ${alpha})`;
|
context.value.fillStyle = `rgba(${color.value.split(' ').join(', ')}, ${alpha})`
|
||||||
context.value.fill();
|
context.value.fill()
|
||||||
context.value.setTransform(pixelRatio.value, 0, 0, pixelRatio.value, 0, 0);
|
context.value.setTransform(pixelRatio.value, 0, 0, pixelRatio.value, 0, 0)
|
||||||
|
|
||||||
if (!update) {
|
if (!update) {
|
||||||
circles.value.push(circle);
|
circles.value.push(circle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearContext() {
|
function clearContext() {
|
||||||
if (context.value) {
|
if (context.value) {
|
||||||
context.value.clearRect(0, 0, canvasSize.w, canvasSize.h);
|
context.value.clearRect(0, 0, canvasSize.w, canvasSize.h)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawParticles() {
|
function drawParticles() {
|
||||||
clearContext();
|
clearContext()
|
||||||
const particleCount = props.quantity;
|
const particleCount = props.quantity
|
||||||
for (let i = 0; i < particleCount; i++) {
|
for (let i = 0; i < particleCount; i++) {
|
||||||
const circle = circleParams();
|
const circle = circleParams()
|
||||||
drawCircle(circle);
|
drawCircle(circle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function remapValue(
|
function remapValue(value: number, start1: number, end1: number, start2: number, end2: number): number {
|
||||||
value: number,
|
const remapped = ((value - start1) * (end2 - start2)) / (end1 - start1) + start2
|
||||||
start1: number,
|
return remapped > 0 ? remapped : 0
|
||||||
end1: number,
|
|
||||||
start2: number,
|
|
||||||
end2: number,
|
|
||||||
): number {
|
|
||||||
const remapped = ((value - start1) * (end2 - start2)) / (end1 - start1) + start2;
|
|
||||||
return remapped > 0 ? remapped : 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function animate() {
|
function animate() {
|
||||||
clearContext();
|
clearContext()
|
||||||
circles.value.forEach((circle, i) => {
|
circles.value.forEach((circle, i) => {
|
||||||
// Handle the alpha value
|
// Handle the alpha value
|
||||||
const edge = [
|
const edge = [
|
||||||
circle.x + circle.translateX - circle.size, // distance from left edge
|
circle.x + circle.translateX - circle.size, // distance from left edge
|
||||||
canvasSize.w - circle.x - circle.translateX - circle.size, // distance from right edge
|
canvasSize.w - circle.x - circle.translateX - circle.size, // distance from right edge
|
||||||
circle.y + circle.translateY - circle.size, // distance from top edge
|
circle.y + circle.translateY - circle.size, // distance from top edge
|
||||||
canvasSize.h - circle.y - circle.translateY - circle.size, // distance from bottom edge
|
canvasSize.h - circle.y - circle.translateY - circle.size // distance from bottom edge
|
||||||
];
|
]
|
||||||
|
|
||||||
const closestEdge = edge.reduce((a, b) => Math.min(a, b));
|
const closestEdge = edge.reduce((a, b) => Math.min(a, b))
|
||||||
const remapClosestEdge = parseFloat(remapValue(closestEdge, 0, 20, 0, 1).toFixed(2));
|
const remapClosestEdge = parseFloat(remapValue(closestEdge, 0, 20, 0, 1).toFixed(2))
|
||||||
|
|
||||||
if (remapClosestEdge > 1) {
|
if (remapClosestEdge > 1) {
|
||||||
circle.alpha += 0.02;
|
circle.alpha += 0.02
|
||||||
if (circle.alpha > circle.targetAlpha) circle.alpha = circle.targetAlpha;
|
if (circle.alpha > circle.targetAlpha) circle.alpha = circle.targetAlpha
|
||||||
} else {
|
} else {
|
||||||
circle.alpha = circle.targetAlpha * remapClosestEdge;
|
circle.alpha = circle.targetAlpha * remapClosestEdge
|
||||||
}
|
}
|
||||||
|
|
||||||
circle.x += circle.dx;
|
circle.x += circle.dx
|
||||||
circle.y += circle.dy;
|
circle.y += circle.dy
|
||||||
circle.translateX +=
|
circle.translateX += (mouse.x / (props.staticity / circle.magnetism) - circle.translateX) / props.ease
|
||||||
(mouse.x / (props.staticity / circle.magnetism) - circle.translateX) / props.ease;
|
circle.translateY += (mouse.y / (props.staticity / circle.magnetism) - circle.translateY) / props.ease
|
||||||
circle.translateY +=
|
|
||||||
(mouse.y / (props.staticity / circle.magnetism) - circle.translateY) / props.ease;
|
|
||||||
|
|
||||||
// circle gets out of the canvas
|
// circle gets out of the canvas
|
||||||
if (
|
if (
|
||||||
@@ -226,10 +214,10 @@ function animate() {
|
|||||||
circle.y > canvasSize.h + circle.size
|
circle.y > canvasSize.h + circle.size
|
||||||
) {
|
) {
|
||||||
// remove the circle from the array
|
// remove the circle from the array
|
||||||
circles.value.splice(i, 1);
|
circles.value.splice(i, 1)
|
||||||
// create a new circle
|
// create a new circle
|
||||||
const newCircle = circleParams();
|
const newCircle = circleParams()
|
||||||
drawCircle(newCircle);
|
drawCircle(newCircle)
|
||||||
// update the circle position
|
// update the circle position
|
||||||
} else {
|
} else {
|
||||||
drawCircle(
|
drawCircle(
|
||||||
@@ -239,12 +227,12 @@ function animate() {
|
|||||||
y: circle.y,
|
y: circle.y,
|
||||||
translateX: circle.translateX,
|
translateX: circle.translateX,
|
||||||
translateY: circle.translateY,
|
translateY: circle.translateY,
|
||||||
alpha: circle.alpha,
|
alpha: circle.alpha
|
||||||
},
|
},
|
||||||
true,
|
true
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
window.requestAnimationFrame(animate);
|
window.requestAnimationFrame(animate)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export { default as ParticlesBg } from "./ParticlesBg.vue";
|
export { default as ParticlesBg } from './ParticlesBg.vue'
|
||||||
|
|||||||
@@ -1,20 +1,3 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { SeparatorProps } from "reka-ui"
|
|
||||||
import type { HTMLAttributes } from "vue"
|
|
||||||
import { reactiveOmit } from "@vueuse/core"
|
|
||||||
import { Separator } from "reka-ui"
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<
|
|
||||||
SeparatorProps & { class?: HTMLAttributes["class"] }
|
|
||||||
>(), {
|
|
||||||
orientation: "horizontal",
|
|
||||||
decorative: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
const delegatedProps = reactiveOmit(props, "class")
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Separator
|
<Separator
|
||||||
data-slot="separator"
|
data-slot="separator"
|
||||||
@@ -22,8 +5,23 @@ const delegatedProps = reactiveOmit(props, "class")
|
|||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px',
|
'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px',
|
||||||
props.class,
|
props.class
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { SeparatorProps } from 'reka-ui'
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { reactiveOmit } from '@vueuse/core'
|
||||||
|
import { Separator } from 'reka-ui'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<SeparatorProps & { class?: HTMLAttributes['class'] }>(), {
|
||||||
|
orientation: 'horizontal',
|
||||||
|
decorative: true
|
||||||
|
})
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, 'class')
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export { default as Separator } from "./Separator.vue"
|
export { default as Separator } from './Separator.vue'
|
||||||
|
|||||||
@@ -1,19 +1,15 @@
|
|||||||
|
<template>
|
||||||
|
<DialogRoot v-slot="slotProps" data-slot="sheet" v-bind="forwarded">
|
||||||
|
<slot v-bind="slotProps" />
|
||||||
|
</DialogRoot>
|
||||||
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { DialogRootEmits, DialogRootProps } from "reka-ui"
|
import type { DialogRootEmits, DialogRootProps } from 'reka-ui'
|
||||||
import { DialogRoot, useForwardPropsEmits } from "reka-ui"
|
import { DialogRoot, useForwardPropsEmits } from 'reka-ui'
|
||||||
|
|
||||||
const props = defineProps<DialogRootProps>()
|
const props = defineProps<DialogRootProps>()
|
||||||
const emits = defineEmits<DialogRootEmits>()
|
const emits = defineEmits<DialogRootEmits>()
|
||||||
|
|
||||||
const forwarded = useForwardPropsEmits(props, emits)
|
const forwarded = useForwardPropsEmits(props, emits)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
|
||||||
<DialogRoot
|
|
||||||
v-slot="slotProps"
|
|
||||||
data-slot="sheet"
|
|
||||||
v-bind="forwarded"
|
|
||||||
>
|
|
||||||
<slot v-bind="slotProps" />
|
|
||||||
</DialogRoot>
|
|
||||||
</template>
|
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { DialogCloseProps } from "reka-ui"
|
|
||||||
import { DialogClose } from "reka-ui"
|
|
||||||
|
|
||||||
const props = defineProps<DialogCloseProps>()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DialogClose
|
<DialogClose data-slot="sheet-close" v-bind="props">
|
||||||
data-slot="sheet-close"
|
|
||||||
v-bind="props"
|
|
||||||
>
|
|
||||||
<slot />
|
<slot />
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { DialogCloseProps } from 'reka-ui'
|
||||||
|
import { DialogClose } from 'reka-ui'
|
||||||
|
|
||||||
|
const props = defineProps<DialogCloseProps>()
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -1,52 +1,21 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { DialogContentEmits, DialogContentProps } from "reka-ui"
|
|
||||||
import type { HTMLAttributes } from "vue"
|
|
||||||
import { reactiveOmit } from "@vueuse/core"
|
|
||||||
import { X } from "lucide-vue-next"
|
|
||||||
import {
|
|
||||||
DialogClose,
|
|
||||||
DialogContent,
|
|
||||||
DialogPortal,
|
|
||||||
useForwardPropsEmits,
|
|
||||||
} from "reka-ui"
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import SheetOverlay from "./SheetOverlay.vue"
|
|
||||||
|
|
||||||
interface SheetContentProps extends DialogContentProps {
|
|
||||||
class?: HTMLAttributes["class"]
|
|
||||||
side?: "top" | "right" | "bottom" | "left"
|
|
||||||
}
|
|
||||||
|
|
||||||
defineOptions({
|
|
||||||
inheritAttrs: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<SheetContentProps>(), {
|
|
||||||
side: "right",
|
|
||||||
})
|
|
||||||
const emits = defineEmits<DialogContentEmits>()
|
|
||||||
|
|
||||||
const delegatedProps = reactiveOmit(props, "class", "side")
|
|
||||||
|
|
||||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DialogPortal>
|
<DialogPortal>
|
||||||
<SheetOverlay />
|
<SheetOverlay />
|
||||||
<DialogContent
|
<DialogContent
|
||||||
data-slot="sheet-content"
|
data-slot="sheet-content"
|
||||||
:class="cn(
|
:class="
|
||||||
|
cn(
|
||||||
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
|
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
|
||||||
side === 'right'
|
side === 'right' &&
|
||||||
&& 'data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm',
|
'data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm',
|
||||||
side === 'left'
|
side === 'left' &&
|
||||||
&& 'data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm',
|
'data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm',
|
||||||
side === 'top'
|
side === 'top' && 'data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b',
|
||||||
&& 'data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b',
|
side === 'bottom' &&
|
||||||
side === 'bottom'
|
'data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t',
|
||||||
&& 'data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t',
|
props.class
|
||||||
props.class)"
|
)
|
||||||
|
"
|
||||||
v-bind="{ ...$attrs, ...forwarded }"
|
v-bind="{ ...$attrs, ...forwarded }"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
@@ -60,3 +29,31 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</DialogPortal>
|
</DialogPortal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { DialogContentEmits, DialogContentProps } from 'reka-ui'
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { reactiveOmit } from '@vueuse/core'
|
||||||
|
import { X } from 'lucide-vue-next'
|
||||||
|
import { DialogClose, DialogContent, DialogPortal, useForwardPropsEmits } from 'reka-ui'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import SheetOverlay from './SheetOverlay.vue'
|
||||||
|
|
||||||
|
interface SheetContentProps extends DialogContentProps {
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
side?: 'top' | 'right' | 'bottom' | 'left'
|
||||||
|
}
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<SheetContentProps>(), {
|
||||||
|
side: 'right'
|
||||||
|
})
|
||||||
|
const emits = defineEmits<DialogContentEmits>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, 'class', 'side')
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -1,21 +1,17 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { DialogDescriptionProps } from "reka-ui"
|
|
||||||
import type { HTMLAttributes } from "vue"
|
|
||||||
import { reactiveOmit } from "@vueuse/core"
|
|
||||||
import { DialogDescription } from "reka-ui"
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes["class"] }>()
|
|
||||||
|
|
||||||
const delegatedProps = reactiveOmit(props, "class")
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DialogDescription
|
<DialogDescription data-slot="sheet-description" :class="cn('text-muted-foreground text-sm', props.class)" v-bind="delegatedProps">
|
||||||
data-slot="sheet-description"
|
|
||||||
:class="cn('text-muted-foreground text-sm', props.class)"
|
|
||||||
v-bind="delegatedProps"
|
|
||||||
>
|
|
||||||
<slot />
|
<slot />
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { DialogDescriptionProps } from 'reka-ui'
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { reactiveOmit } from '@vueuse/core'
|
||||||
|
import { DialogDescription } from 'reka-ui'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes['class'] }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, 'class')
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { HTMLAttributes } from "vue"
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div data-slot="sheet-footer" :class="cn('mt-auto flex flex-col gap-2 p-4', props.class)">
|
||||||
data-slot="sheet-footer"
|
|
||||||
:class="cn('mt-auto flex flex-col gap-2 p-4', props.class)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<{ class?: HTMLAttributes['class'] }>()
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { HTMLAttributes } from "vue"
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div data-slot="sheet-header" :class="cn('flex flex-col gap-1.5 p-4', props.class)">
|
||||||
data-slot="sheet-header"
|
|
||||||
:class="cn('flex flex-col gap-1.5 p-4', props.class)"
|
|
||||||
>
|
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<{ class?: HTMLAttributes['class'] }>()
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -1,21 +1,26 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { DialogOverlayProps } from "reka-ui"
|
|
||||||
import type { HTMLAttributes } from "vue"
|
|
||||||
import { reactiveOmit } from "@vueuse/core"
|
|
||||||
import { DialogOverlay } from "reka-ui"
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const props = defineProps<DialogOverlayProps & { class?: HTMLAttributes["class"] }>()
|
|
||||||
|
|
||||||
const delegatedProps = reactiveOmit(props, "class")
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DialogOverlay
|
<DialogOverlay
|
||||||
data-slot="sheet-overlay"
|
data-slot="sheet-overlay"
|
||||||
:class="cn('data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80', props.class)"
|
:class="
|
||||||
|
cn(
|
||||||
|
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80',
|
||||||
|
props.class
|
||||||
|
)
|
||||||
|
"
|
||||||
v-bind="delegatedProps"
|
v-bind="delegatedProps"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</DialogOverlay>
|
</DialogOverlay>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { DialogOverlayProps } from 'reka-ui'
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { reactiveOmit } from '@vueuse/core'
|
||||||
|
import { DialogOverlay } from 'reka-ui'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<DialogOverlayProps & { class?: HTMLAttributes['class'] }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, 'class')
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -1,21 +1,17 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { DialogTitleProps } from "reka-ui"
|
|
||||||
import type { HTMLAttributes } from "vue"
|
|
||||||
import { reactiveOmit } from "@vueuse/core"
|
|
||||||
import { DialogTitle } from "reka-ui"
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const props = defineProps<DialogTitleProps & { class?: HTMLAttributes["class"] }>()
|
|
||||||
|
|
||||||
const delegatedProps = reactiveOmit(props, "class")
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DialogTitle
|
<DialogTitle data-slot="sheet-title" :class="cn('text-foreground font-semibold', props.class)" v-bind="delegatedProps">
|
||||||
data-slot="sheet-title"
|
|
||||||
:class="cn('text-foreground font-semibold', props.class)"
|
|
||||||
v-bind="delegatedProps"
|
|
||||||
>
|
|
||||||
<slot />
|
<slot />
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { DialogTitleProps } from 'reka-ui'
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { reactiveOmit } from '@vueuse/core'
|
||||||
|
import { DialogTitle } from 'reka-ui'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<DialogTitleProps & { class?: HTMLAttributes['class'] }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, 'class')
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { DialogTriggerProps } from "reka-ui"
|
|
||||||
import { DialogTrigger } from "reka-ui"
|
|
||||||
|
|
||||||
const props = defineProps<DialogTriggerProps>()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DialogTrigger
|
<DialogTrigger data-slot="sheet-trigger" v-bind="props">
|
||||||
data-slot="sheet-trigger"
|
|
||||||
v-bind="props"
|
|
||||||
>
|
|
||||||
<slot />
|
<slot />
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { DialogTriggerProps } from 'reka-ui'
|
||||||
|
import { DialogTrigger } from 'reka-ui'
|
||||||
|
|
||||||
|
const props = defineProps<DialogTriggerProps>()
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
export { default as Sheet } from "./Sheet.vue"
|
export { default as Sheet } from './Sheet.vue'
|
||||||
export { default as SheetClose } from "./SheetClose.vue"
|
export { default as SheetClose } from './SheetClose.vue'
|
||||||
export { default as SheetContent } from "./SheetContent.vue"
|
export { default as SheetContent } from './SheetContent.vue'
|
||||||
export { default as SheetDescription } from "./SheetDescription.vue"
|
export { default as SheetDescription } from './SheetDescription.vue'
|
||||||
export { default as SheetFooter } from "./SheetFooter.vue"
|
export { default as SheetFooter } from './SheetFooter.vue'
|
||||||
export { default as SheetHeader } from "./SheetHeader.vue"
|
export { default as SheetHeader } from './SheetHeader.vue'
|
||||||
export { default as SheetTitle } from "./SheetTitle.vue"
|
export { default as SheetTitle } from './SheetTitle.vue'
|
||||||
export { default as SheetTrigger } from "./SheetTrigger.vue"
|
export { default as SheetTrigger } from './SheetTrigger.vue'
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
|
<template>
|
||||||
|
<div data-slot="skeleton" :class="cn('animate-pulse rounded-md bg-primary/10', props.class)" />
|
||||||
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { HTMLAttributes } from "vue"
|
import type { HTMLAttributes } from 'vue'
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
interface SkeletonProps {
|
interface SkeletonProps {
|
||||||
class?: HTMLAttributes["class"]
|
class?: HTMLAttributes['class']
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<SkeletonProps>()
|
const props = defineProps<SkeletonProps>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
data-slot="skeleton"
|
|
||||||
:class="cn('animate-pulse rounded-md bg-primary/10', props.class)"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export { default as Skeleton } from "./Skeleton.vue"
|
export { default as Skeleton } from './Skeleton.vue'
|
||||||
|
|||||||
@@ -444,12 +444,12 @@ export const TECHNOLOGIES: Record<TechnologyType, TechnologyConfig> = {
|
|||||||
[TechnologyType.ComputerTechnology]: {
|
[TechnologyType.ComputerTechnology]: {
|
||||||
id: TechnologyType.ComputerTechnology,
|
id: TechnologyType.ComputerTechnology,
|
||||||
name: '计算机技术',
|
name: '计算机技术',
|
||||||
description: '增加研究队列数量,每级+1队列',
|
description: '增加研究队列和舰队任务槽位,每级+1队列+1槽位',
|
||||||
baseCost: { metal: 0, crystal: 400, deuterium: 600, darkMatter: 0, energy: 0 },
|
baseCost: { metal: 0, crystal: 400, deuterium: 600, darkMatter: 0, energy: 0 },
|
||||||
baseTime: 60,
|
baseTime: 60,
|
||||||
costMultiplier: 2,
|
costMultiplier: 2,
|
||||||
fleetStorageBonus: 500, // 每级全局增加500舰队仓储
|
fleetStorageBonus: 500, // 每级全局增加500舰队仓储
|
||||||
maxLevel: 10, // 最多10级(最多11个研究队列)
|
maxLevel: 10, // 最多10级(最多11个研究队列和11个舰队槽位)
|
||||||
requirements: { [BuildingType.ResearchLab]: 1 },
|
requirements: { [BuildingType.ResearchLab]: 1 },
|
||||||
levelRequirements: {
|
levelRequirements: {
|
||||||
3: { [BuildingType.ResearchLab]: 5 },
|
3: { [BuildingType.ResearchLab]: 5 },
|
||||||
@@ -460,7 +460,7 @@ export const TECHNOLOGIES: Record<TechnologyType, TechnologyConfig> = {
|
|||||||
[TechnologyType.EspionageTechnology]: {
|
[TechnologyType.EspionageTechnology]: {
|
||||||
id: TechnologyType.EspionageTechnology,
|
id: TechnologyType.EspionageTechnology,
|
||||||
name: '间谍技术',
|
name: '间谍技术',
|
||||||
description: '提高间谍探测效果,每级提高1级侦查深度',
|
description: '提高间谍探测效果,每级提高1级侦查深度。侦察等级=己方等级-对方等级+侦察船数/5。≥-1显示舰队,≥1显示防御,≥3显示建筑,≥5显示科技',
|
||||||
baseCost: { metal: 200, crystal: 1000, deuterium: 200, darkMatter: 0, energy: 0 },
|
baseCost: { metal: 200, crystal: 1000, deuterium: 200, darkMatter: 0, energy: 0 },
|
||||||
baseTime: 60,
|
baseTime: 60,
|
||||||
costMultiplier: 2,
|
costMultiplier: 2,
|
||||||
@@ -530,7 +530,7 @@ export const TECHNOLOGIES: Record<TechnologyType, TechnologyConfig> = {
|
|||||||
id: TechnologyType.GravitonTechnology,
|
id: TechnologyType.GravitonTechnology,
|
||||||
name: '引力技术',
|
name: '引力技术',
|
||||||
description: '研究引力操纵,死星的必要技术',
|
description: '研究引力操纵,死星的必要技术',
|
||||||
baseCost: { metal: 0, crystal: 0, deuterium: 0, darkMatter: 300000, energy: 0 },
|
baseCost: { metal: 0, crystal: 0, deuterium: 0, darkMatter: 100000, energy: 0 },
|
||||||
baseTime: 0,
|
baseTime: 0,
|
||||||
costMultiplier: 3,
|
costMultiplier: 3,
|
||||||
maxLevel: 1, // 只有1级
|
maxLevel: 1, // 只有1级
|
||||||
@@ -872,7 +872,7 @@ export const SHIPS: Record<ShipType, ShipConfig> = {
|
|||||||
id: ShipType.Deathstar,
|
id: ShipType.Deathstar,
|
||||||
name: '死星',
|
name: '死星',
|
||||||
description: '终极武器,能够摧毁整个行星',
|
description: '终极武器,能够摧毁整个行星',
|
||||||
cost: { metal: 5000000, crystal: 4000000, deuterium: 1000000, darkMatter: 50000, energy: 0 },
|
cost: { metal: 5000000, crystal: 4000000, deuterium: 1000000, darkMatter: 20000, energy: 0 },
|
||||||
buildTime: 600,
|
buildTime: 600,
|
||||||
cargoCapacity: 1000000,
|
cargoCapacity: 1000000,
|
||||||
attack: 200000,
|
attack: 200000,
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export default {
|
|||||||
goToBuildings: 'Zu Gebäuden',
|
goToBuildings: 'Zu Gebäuden',
|
||||||
locked: 'Gesperrt',
|
locked: 'Gesperrt',
|
||||||
viewRequirements: 'Anforderungen anzeigen',
|
viewRequirements: 'Anforderungen anzeigen',
|
||||||
|
requirements: 'Anforderungen',
|
||||||
requirementsNotMet: 'Anforderungen nicht erfüllt',
|
requirementsNotMet: 'Anforderungen nicht erfüllt',
|
||||||
current: 'Aktuell',
|
current: 'Aktuell',
|
||||||
level: 'Stufe',
|
level: 'Stufe',
|
||||||
@@ -140,6 +141,7 @@ export default {
|
|||||||
jumpGate: 'Sprungtor',
|
jumpGate: 'Sprungtor',
|
||||||
planetDestroyerFactory: 'Planetenzerstörer-Fabrik',
|
planetDestroyerFactory: 'Planetenzerstörer-Fabrik',
|
||||||
buildTime: 'Bauzeit',
|
buildTime: 'Bauzeit',
|
||||||
|
build: '',
|
||||||
production: 'Produktion',
|
production: 'Produktion',
|
||||||
consumption: 'Verbrauch',
|
consumption: 'Verbrauch',
|
||||||
totalCost: 'Gesamtkosten',
|
totalCost: 'Gesamtkosten',
|
||||||
@@ -281,8 +283,8 @@ export default {
|
|||||||
ionTechnology: 'Ionenwaffentechnologie',
|
ionTechnology: 'Ionenwaffentechnologie',
|
||||||
hyperspaceTechnology: 'Hyperraumsprung-Technologie',
|
hyperspaceTechnology: 'Hyperraumsprung-Technologie',
|
||||||
plasmaTechnology: 'Plasmawaffentechnologie',
|
plasmaTechnology: 'Plasmawaffentechnologie',
|
||||||
computerTechnology: 'Erhöht Forschungsauftragskapazität, +1 pro Stufe (max 10 Stufen)',
|
computerTechnology: 'Erhöht Forschungswarteschlange und Flottenmissionsslots, +1 Warteschlange +1 Slot pro Stufe (max 10 Stufen)',
|
||||||
espionageTechnology: 'Verbessert Sondenwirksamkeit, +1 Spionagestufe pro Stufe',
|
espionageTechnology: 'Verbessert Sondenwirksamkeit, +1 Spionagestufe pro Stufe. Spionagestufe = eigene Stufe - Gegnerstufe + Sonden/5. ≥-1 zeigt Flotte, ≥1 zeigt Verteidigung, ≥3 zeigt Gebäude, ≥5 zeigt Technologien',
|
||||||
weaponsTechnology: 'Erhöht Angriffskraft von Schiffen und Verteidigung um 10% pro Stufe',
|
weaponsTechnology: 'Erhöht Angriffskraft von Schiffen und Verteidigung um 10% pro Stufe',
|
||||||
shieldingTechnology: 'Erhöht Schilde von Schiffen und Verteidigung um 10% pro Stufe',
|
shieldingTechnology: 'Erhöht Schilde von Schiffen und Verteidigung um 10% pro Stufe',
|
||||||
armourTechnology: 'Erhöht Panzerung von Schiffen und Verteidigung um 10% pro Stufe',
|
armourTechnology: 'Erhöht Panzerung von Schiffen und Verteidigung um 10% pro Stufe',
|
||||||
@@ -352,6 +354,7 @@ export default {
|
|||||||
gmModeActivated: '',
|
gmModeActivated: '',
|
||||||
upgradeCost: 'Ausbaukosten',
|
upgradeCost: 'Ausbaukosten',
|
||||||
buildTime: 'Bauzeit',
|
buildTime: 'Bauzeit',
|
||||||
|
build: 'Bauen',
|
||||||
upgrade: 'Ausbauen',
|
upgrade: 'Ausbauen',
|
||||||
maxLevelReached: 'Maximale Stufe erreicht',
|
maxLevelReached: 'Maximale Stufe erreicht',
|
||||||
requirementsNotMet: 'Anforderungen nicht erfüllt',
|
requirementsNotMet: 'Anforderungen nicht erfüllt',
|
||||||
@@ -383,6 +386,7 @@ export default {
|
|||||||
fuelConsumption: 'Treibstoffverbrauch',
|
fuelConsumption: 'Treibstoffverbrauch',
|
||||||
buildCost: 'Baukosten',
|
buildCost: 'Baukosten',
|
||||||
buildTime: 'Bauzeit',
|
buildTime: 'Bauzeit',
|
||||||
|
build: '',
|
||||||
perUnit: 'Pro Einheit',
|
perUnit: 'Pro Einheit',
|
||||||
batchCalculator: 'Batch-Rechner',
|
batchCalculator: 'Batch-Rechner',
|
||||||
quantity: 'Menge',
|
quantity: 'Menge',
|
||||||
@@ -413,6 +417,7 @@ export default {
|
|||||||
armor: 'Panzerung',
|
armor: 'Panzerung',
|
||||||
buildCost: 'Baukosten',
|
buildCost: 'Baukosten',
|
||||||
buildTime: 'Bauzeit',
|
buildTime: 'Bauzeit',
|
||||||
|
build: '',
|
||||||
perUnit: 'Pro Einheit',
|
perUnit: 'Pro Einheit',
|
||||||
batchCalculator: 'Batch-Rechner',
|
batchCalculator: 'Batch-Rechner',
|
||||||
quantity: 'Menge',
|
quantity: 'Menge',
|
||||||
@@ -426,6 +431,7 @@ export default {
|
|||||||
shield: 'Schild',
|
shield: 'Schild',
|
||||||
armor: 'Panzerung',
|
armor: 'Panzerung',
|
||||||
buildTime: 'Bauzeit',
|
buildTime: 'Bauzeit',
|
||||||
|
build: '',
|
||||||
seconds: 's',
|
seconds: 's',
|
||||||
unitCost: 'Stückkosten',
|
unitCost: 'Stückkosten',
|
||||||
buildQuantity: 'Baumenge',
|
buildQuantity: 'Baumenge',
|
||||||
@@ -469,6 +475,7 @@ export default {
|
|||||||
colonize: 'Kolonisieren',
|
colonize: 'Kolonisieren',
|
||||||
spy: 'Spionage',
|
spy: 'Spionage',
|
||||||
deploy: 'Stationieren',
|
deploy: 'Stationieren',
|
||||||
|
expedition: 'Expedition',
|
||||||
recycle: 'Recyceln',
|
recycle: 'Recyceln',
|
||||||
transportResources: 'Ressourcen transportieren',
|
transportResources: 'Ressourcen transportieren',
|
||||||
totalCargoCapacity: 'Gesamtladekapazität',
|
totalCargoCapacity: 'Gesamtladekapazität',
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export default {
|
|||||||
goToBuildings: 'Go to Buildings',
|
goToBuildings: 'Go to Buildings',
|
||||||
locked: 'Locked',
|
locked: 'Locked',
|
||||||
viewRequirements: 'View Requirements',
|
viewRequirements: 'View Requirements',
|
||||||
|
requirements: 'Requirements',
|
||||||
requirementsNotMet: 'Requirements Not Met',
|
requirementsNotMet: 'Requirements Not Met',
|
||||||
current: 'Current',
|
current: 'Current',
|
||||||
level: 'Level',
|
level: 'Level',
|
||||||
@@ -280,8 +281,8 @@ export default {
|
|||||||
ionTechnology: 'Ion weapon technology',
|
ionTechnology: 'Ion weapon technology',
|
||||||
hyperspaceTechnology: 'Hyperspace jump technology',
|
hyperspaceTechnology: 'Hyperspace jump technology',
|
||||||
plasmaTechnology: 'Plasma weapon technology',
|
plasmaTechnology: 'Plasma weapon technology',
|
||||||
computerTechnology: 'Increases research queue capacity, +1 per level (max 10 levels)',
|
computerTechnology: 'Increases research queue and fleet mission slots, +1 queue +1 slot per level (max 10 levels)',
|
||||||
espionageTechnology: 'Improves spy probe effectiveness, +1 espionage level per level',
|
espionageTechnology: 'Improves spy probe effectiveness, +1 espionage level per level. Spy level = your level - enemy level + probes/5. ≥-1 shows fleet, ≥1 shows defense, ≥3 shows buildings, ≥5 shows technologies',
|
||||||
weaponsTechnology: 'Increases ship and defense attack power by 10% per level',
|
weaponsTechnology: 'Increases ship and defense attack power by 10% per level',
|
||||||
shieldingTechnology: 'Increases ship and defense shields by 10% per level',
|
shieldingTechnology: 'Increases ship and defense shields by 10% per level',
|
||||||
armourTechnology: 'Increases ship and defense armour by 10% per level',
|
armourTechnology: 'Increases ship and defense armour by 10% per level',
|
||||||
@@ -353,6 +354,7 @@ export default {
|
|||||||
level: 'Level',
|
level: 'Level',
|
||||||
upgradeCost: 'Upgrade Cost',
|
upgradeCost: 'Upgrade Cost',
|
||||||
buildTime: 'Build Time',
|
buildTime: 'Build Time',
|
||||||
|
build: 'Build',
|
||||||
upgrade: 'Upgrade',
|
upgrade: 'Upgrade',
|
||||||
maxLevelReached: 'Max Level Reached',
|
maxLevelReached: 'Max Level Reached',
|
||||||
requirementsNotMet: 'Requirements Not Met',
|
requirementsNotMet: 'Requirements Not Met',
|
||||||
@@ -467,6 +469,7 @@ export default {
|
|||||||
colonize: 'Colonize',
|
colonize: 'Colonize',
|
||||||
spy: 'Spy',
|
spy: 'Spy',
|
||||||
deploy: 'Deploy',
|
deploy: 'Deploy',
|
||||||
|
expedition: 'Expedition',
|
||||||
recycle: 'Recycle',
|
recycle: 'Recycle',
|
||||||
destroy: 'Planet Destruction',
|
destroy: 'Planet Destruction',
|
||||||
transportResources: 'Transport Resources',
|
transportResources: 'Transport Resources',
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export default {
|
|||||||
goToBuildings: '建物へ移動',
|
goToBuildings: '建物へ移動',
|
||||||
locked: 'ロック済み',
|
locked: 'ロック済み',
|
||||||
viewRequirements: '必要条件を表示',
|
viewRequirements: '必要条件を表示',
|
||||||
|
requirements: '必要条件',
|
||||||
requirementsNotMet: '必要条件が満たされていません',
|
requirementsNotMet: '必要条件が満たされていません',
|
||||||
current: '現在',
|
current: '現在',
|
||||||
level: 'レベル',
|
level: 'レベル',
|
||||||
@@ -140,6 +141,7 @@ export default {
|
|||||||
jumpGate: 'ジャンプゲート',
|
jumpGate: 'ジャンプゲート',
|
||||||
planetDestroyerFactory: '惑星破壊工場',
|
planetDestroyerFactory: '惑星破壊工場',
|
||||||
buildTime: '建設時間',
|
buildTime: '建設時間',
|
||||||
|
build: '',
|
||||||
production: '生産量',
|
production: '生産量',
|
||||||
consumption: '消費',
|
consumption: '消費',
|
||||||
totalCost: '総コスト',
|
totalCost: '総コスト',
|
||||||
@@ -281,8 +283,8 @@ export default {
|
|||||||
ionTechnology: 'イオン兵器技術',
|
ionTechnology: 'イオン兵器技術',
|
||||||
hyperspaceTechnology: 'ハイパースペースジャンプ技術',
|
hyperspaceTechnology: 'ハイパースペースジャンプ技術',
|
||||||
plasmaTechnology: 'プラズマ兵器技術',
|
plasmaTechnology: 'プラズマ兵器技術',
|
||||||
computerTechnology: '研究キュー数を増加、レベル毎に+1(最大10レベル)',
|
computerTechnology: '研究キューと艦隊任務スロットを増加、レベル毎に+1キュー+1スロット(最大10レベル)',
|
||||||
espionageTechnology: 'スパイ探査機の効果を向上、レベル毎に偵察深度+1',
|
espionageTechnology: 'スパイ探査機の効果を向上、レベル毎に偵察深度+1。偵察レベル=自分のレベル-相手のレベル+探査機数/5。≥-1で艦隊表示、≥1で防御表示、≥3で建物表示、≥5で技術表示',
|
||||||
weaponsTechnology: '艦船と防御の攻撃力をレベル毎に10%増加',
|
weaponsTechnology: '艦船と防御の攻撃力をレベル毎に10%増加',
|
||||||
shieldingTechnology: '艦船と防御のシールドをレベル毎に10%増加',
|
shieldingTechnology: '艦船と防御のシールドをレベル毎に10%増加',
|
||||||
armourTechnology: '艦船と防御の装甲をレベル毎に10%増加',
|
armourTechnology: '艦船と防御の装甲をレベル毎に10%増加',
|
||||||
@@ -341,6 +343,7 @@ export default {
|
|||||||
fuelConsumption: '燃料消費',
|
fuelConsumption: '燃料消費',
|
||||||
buildCost: '建設コスト',
|
buildCost: '建設コスト',
|
||||||
buildTime: '建設時間',
|
buildTime: '建設時間',
|
||||||
|
build: '',
|
||||||
perUnit: 'ユニットあたり',
|
perUnit: 'ユニットあたり',
|
||||||
batchCalculator: '一括計算機',
|
batchCalculator: '一括計算機',
|
||||||
quantity: '数量',
|
quantity: '数量',
|
||||||
@@ -368,6 +371,7 @@ export default {
|
|||||||
gmModeActivated: '',
|
gmModeActivated: '',
|
||||||
upgradeCost: 'アップグレードコスト',
|
upgradeCost: 'アップグレードコスト',
|
||||||
buildTime: '建設時間',
|
buildTime: '建設時間',
|
||||||
|
build: '建設',
|
||||||
upgrade: 'アップグレード',
|
upgrade: 'アップグレード',
|
||||||
maxLevelReached: '最大レベルに達しました',
|
maxLevelReached: '最大レベルに達しました',
|
||||||
requirementsNotMet: '要件が満たされていません',
|
requirementsNotMet: '要件が満たされていません',
|
||||||
@@ -395,6 +399,7 @@ export default {
|
|||||||
armor: '装甲',
|
armor: '装甲',
|
||||||
buildCost: '建設コスト',
|
buildCost: '建設コスト',
|
||||||
buildTime: '建設時間',
|
buildTime: '建設時間',
|
||||||
|
build: '',
|
||||||
perUnit: 'ユニットあたり',
|
perUnit: 'ユニットあたり',
|
||||||
batchCalculator: '一括計算機',
|
batchCalculator: '一括計算機',
|
||||||
quantity: '数量',
|
quantity: '数量',
|
||||||
@@ -425,6 +430,7 @@ export default {
|
|||||||
shield: 'シールド',
|
shield: 'シールド',
|
||||||
armor: '装甲',
|
armor: '装甲',
|
||||||
buildTime: '建設時間',
|
buildTime: '建設時間',
|
||||||
|
build: '',
|
||||||
seconds: '秒',
|
seconds: '秒',
|
||||||
unitCost: 'ユニットコスト',
|
unitCost: 'ユニットコスト',
|
||||||
buildQuantity: '建造数',
|
buildQuantity: '建造数',
|
||||||
@@ -467,6 +473,7 @@ export default {
|
|||||||
colonize: '植民',
|
colonize: '植民',
|
||||||
spy: '偵察',
|
spy: '偵察',
|
||||||
deploy: '配備',
|
deploy: '配備',
|
||||||
|
expedition: '探検',
|
||||||
recycle: '回収',
|
recycle: '回収',
|
||||||
transportResources: '資源輸送',
|
transportResources: '資源輸送',
|
||||||
totalCargoCapacity: '総積載量',
|
totalCargoCapacity: '総積載量',
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export default {
|
|||||||
goToBuildings: '건물로 이동',
|
goToBuildings: '건물로 이동',
|
||||||
locked: '잠김',
|
locked: '잠김',
|
||||||
viewRequirements: '요구사항 보기',
|
viewRequirements: '요구사항 보기',
|
||||||
|
requirements: '요구사항',
|
||||||
requirementsNotMet: '요구사항 미충족',
|
requirementsNotMet: '요구사항 미충족',
|
||||||
current: '현재',
|
current: '현재',
|
||||||
level: '레벨',
|
level: '레벨',
|
||||||
@@ -140,6 +141,7 @@ export default {
|
|||||||
jumpGate: '점프 게이트',
|
jumpGate: '점프 게이트',
|
||||||
planetDestroyerFactory: '행성 파괴 공장',
|
planetDestroyerFactory: '행성 파괴 공장',
|
||||||
buildTime: '건설 시간',
|
buildTime: '건설 시간',
|
||||||
|
build: '',
|
||||||
production: '생산량',
|
production: '생산량',
|
||||||
consumption: '소비',
|
consumption: '소비',
|
||||||
totalCost: '총 비용',
|
totalCost: '총 비용',
|
||||||
@@ -281,8 +283,8 @@ export default {
|
|||||||
ionTechnology: '이온 무기 기술',
|
ionTechnology: '이온 무기 기술',
|
||||||
hyperspaceTechnology: '초공간 점프 기술',
|
hyperspaceTechnology: '초공간 점프 기술',
|
||||||
plasmaTechnology: '플라즈마 무기 기술',
|
plasmaTechnology: '플라즈마 무기 기술',
|
||||||
computerTechnology: '연구 대기열 수 증가, 레벨당 +1 (최대 10레벨)',
|
computerTechnology: '연구 대기열 및 함대 임무 슬롯 증가, 레벨당 +1 대기열 +1 슬롯 (최대 10레벨)',
|
||||||
espionageTechnology: '스파이 탐사기 효과 향상, 레벨당 정찰 깊이 +1',
|
espionageTechnology: '스파이 탐사기 효과 향상, 레벨당 정찰 깊이 +1. 정찰 레벨 = 내 레벨 - 상대 레벨 + 탐사기 수/5. ≥-1 함대 표시, ≥1 방어 표시, ≥3 건물 표시, ≥5 기술 표시',
|
||||||
weaponsTechnology: '함선과 방어의 공격력 레벨당 10% 증가',
|
weaponsTechnology: '함선과 방어의 공격력 레벨당 10% 증가',
|
||||||
shieldingTechnology: '함선과 방어의 실드 레벨당 10% 증가',
|
shieldingTechnology: '함선과 방어의 실드 레벨당 10% 증가',
|
||||||
armourTechnology: '함선과 방어의 장갑 레벨당 10% 증가',
|
armourTechnology: '함선과 방어의 장갑 레벨당 10% 증가',
|
||||||
@@ -352,6 +354,7 @@ export default {
|
|||||||
gmModeActivated: '',
|
gmModeActivated: '',
|
||||||
upgradeCost: '업그레이드 비용',
|
upgradeCost: '업그레이드 비용',
|
||||||
buildTime: '건설 시간',
|
buildTime: '건설 시간',
|
||||||
|
build: '건설',
|
||||||
upgrade: '업그레이드',
|
upgrade: '업그레이드',
|
||||||
maxLevelReached: '최대 레벨 도달',
|
maxLevelReached: '최대 레벨 도달',
|
||||||
requirementsNotMet: '요구 사항 미충족',
|
requirementsNotMet: '요구 사항 미충족',
|
||||||
@@ -382,6 +385,7 @@ export default {
|
|||||||
fuelConsumption: '연료 소비',
|
fuelConsumption: '연료 소비',
|
||||||
buildCost: '건설 비용',
|
buildCost: '건설 비용',
|
||||||
buildTime: '건설 시간',
|
buildTime: '건설 시간',
|
||||||
|
build: '',
|
||||||
perUnit: '단위당',
|
perUnit: '단위당',
|
||||||
batchCalculator: '일괄 계산기',
|
batchCalculator: '일괄 계산기',
|
||||||
quantity: '수량',
|
quantity: '수량',
|
||||||
@@ -412,6 +416,7 @@ export default {
|
|||||||
armor: '장갑',
|
armor: '장갑',
|
||||||
buildCost: '건설 비용',
|
buildCost: '건설 비용',
|
||||||
buildTime: '건설 시간',
|
buildTime: '건설 시간',
|
||||||
|
build: '',
|
||||||
perUnit: '단위당',
|
perUnit: '단위당',
|
||||||
batchCalculator: '일괄 계산기',
|
batchCalculator: '일괄 계산기',
|
||||||
quantity: '수량',
|
quantity: '수량',
|
||||||
@@ -425,6 +430,7 @@ export default {
|
|||||||
shield: '실드',
|
shield: '실드',
|
||||||
armor: '장갑',
|
armor: '장갑',
|
||||||
buildTime: '건설 시간',
|
buildTime: '건설 시간',
|
||||||
|
build: '',
|
||||||
seconds: '초',
|
seconds: '초',
|
||||||
unitCost: '단위 비용',
|
unitCost: '단위 비용',
|
||||||
buildQuantity: '건조 수량',
|
buildQuantity: '건조 수량',
|
||||||
@@ -467,6 +473,7 @@ export default {
|
|||||||
colonize: '식민',
|
colonize: '식민',
|
||||||
spy: '정찰',
|
spy: '정찰',
|
||||||
deploy: '배치',
|
deploy: '배치',
|
||||||
|
expedition: '탐험',
|
||||||
recycle: '회수',
|
recycle: '회수',
|
||||||
transportResources: '자원 수송',
|
transportResources: '자원 수송',
|
||||||
totalCargoCapacity: '총 적재량',
|
totalCargoCapacity: '총 적재량',
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export default {
|
|||||||
goToBuildings: 'К зданиям',
|
goToBuildings: 'К зданиям',
|
||||||
locked: 'Заблокировано',
|
locked: 'Заблокировано',
|
||||||
viewRequirements: 'Просмотр требований',
|
viewRequirements: 'Просмотр требований',
|
||||||
|
requirements: 'Требования',
|
||||||
requirementsNotMet: 'Требования не выполнены',
|
requirementsNotMet: 'Требования не выполнены',
|
||||||
current: 'Текущий',
|
current: 'Текущий',
|
||||||
level: 'Уровень',
|
level: 'Уровень',
|
||||||
@@ -140,6 +141,7 @@ export default {
|
|||||||
jumpGate: 'Прыжковые ворота',
|
jumpGate: 'Прыжковые ворота',
|
||||||
planetDestroyerFactory: 'Фабрика разрушителей планет',
|
planetDestroyerFactory: 'Фабрика разрушителей планет',
|
||||||
buildTime: 'Время строительства',
|
buildTime: 'Время строительства',
|
||||||
|
build: '',
|
||||||
production: 'Производство',
|
production: 'Производство',
|
||||||
consumption: 'Потребление',
|
consumption: 'Потребление',
|
||||||
totalCost: 'Общая стоимость',
|
totalCost: 'Общая стоимость',
|
||||||
@@ -281,8 +283,8 @@ export default {
|
|||||||
ionTechnology: 'Технология ионного оружия',
|
ionTechnology: 'Технология ионного оружия',
|
||||||
hyperspaceTechnology: 'Технология гиперпространственных прыжков',
|
hyperspaceTechnology: 'Технология гиперпространственных прыжков',
|
||||||
plasmaTechnology: 'Технология плазменного оружия',
|
plasmaTechnology: 'Технология плазменного оружия',
|
||||||
computerTechnology: 'Увеличивает вместимость очереди исследований, +1 за уровень (макс 10 уровней)',
|
computerTechnology: 'Увеличивает очередь исследований и слоты флотских миссий, +1 очередь +1 слот за уровень (макс 10 уровней)',
|
||||||
espionageTechnology: 'Повышает эффективность зондов, +1 уровень шпионажа за уровень',
|
espionageTechnology: 'Повышает эффективность зондов, +1 уровень шпионажа за уровень. Уровень разведки = ваш уровень - уровень врага + зонды/5. ≥-1 показывает флот, ≥1 показывает оборону, ≥3 показывает здания, ≥5 показывает технологии',
|
||||||
weaponsTechnology: 'Увеличивает силу атаки кораблей и обороны на 10% за уровень',
|
weaponsTechnology: 'Увеличивает силу атаки кораблей и обороны на 10% за уровень',
|
||||||
shieldingTechnology: 'Увеличивает щиты кораблей и обороны на 10% за уровень',
|
shieldingTechnology: 'Увеличивает щиты кораблей и обороны на 10% за уровень',
|
||||||
armourTechnology: 'Увеличивает броню кораблей и обороны на 10% за уровень',
|
armourTechnology: 'Увеличивает броню кораблей и обороны на 10% за уровень',
|
||||||
@@ -353,6 +355,7 @@ export default {
|
|||||||
gmModeActivated: '',
|
gmModeActivated: '',
|
||||||
upgradeCost: 'Стоимость улучшения',
|
upgradeCost: 'Стоимость улучшения',
|
||||||
buildTime: 'Время строительства',
|
buildTime: 'Время строительства',
|
||||||
|
build: 'Построить',
|
||||||
upgrade: 'Улучшить',
|
upgrade: 'Улучшить',
|
||||||
maxLevelReached: 'Достигнут максимальный уровень',
|
maxLevelReached: 'Достигнут максимальный уровень',
|
||||||
requirementsNotMet: 'Требования не выполнены',
|
requirementsNotMet: 'Требования не выполнены',
|
||||||
@@ -384,6 +387,7 @@ export default {
|
|||||||
fuelConsumption: 'Расход топлива',
|
fuelConsumption: 'Расход топлива',
|
||||||
buildCost: 'Стоимость постройки',
|
buildCost: 'Стоимость постройки',
|
||||||
buildTime: 'Время строительства',
|
buildTime: 'Время строительства',
|
||||||
|
build: '',
|
||||||
perUnit: 'За единицу',
|
perUnit: 'За единицу',
|
||||||
batchCalculator: 'Калькулятор партий',
|
batchCalculator: 'Калькулятор партий',
|
||||||
quantity: 'Количество',
|
quantity: 'Количество',
|
||||||
@@ -414,6 +418,7 @@ export default {
|
|||||||
armor: 'Броня',
|
armor: 'Броня',
|
||||||
buildCost: 'Стоимость постройки',
|
buildCost: 'Стоимость постройки',
|
||||||
buildTime: 'Время строительства',
|
buildTime: 'Время строительства',
|
||||||
|
build: '',
|
||||||
perUnit: 'За единицу',
|
perUnit: 'За единицу',
|
||||||
batchCalculator: 'Калькулятор партий',
|
batchCalculator: 'Калькулятор партий',
|
||||||
quantity: 'Количество',
|
quantity: 'Количество',
|
||||||
@@ -470,6 +475,7 @@ export default {
|
|||||||
colonize: 'Колонизация',
|
colonize: 'Колонизация',
|
||||||
spy: 'Разведка',
|
spy: 'Разведка',
|
||||||
deploy: 'Размещение',
|
deploy: 'Размещение',
|
||||||
|
expedition: 'Экспедиция',
|
||||||
recycle: 'Переработка',
|
recycle: 'Переработка',
|
||||||
transportResources: 'Транспортировка ресурсов',
|
transportResources: 'Транспортировка ресурсов',
|
||||||
totalCargoCapacity: 'Общая грузоподъёмность',
|
totalCargoCapacity: 'Общая грузоподъёмность',
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export default {
|
|||||||
goToBuildings: '前往建筑页面',
|
goToBuildings: '前往建筑页面',
|
||||||
locked: '已锁定',
|
locked: '已锁定',
|
||||||
viewRequirements: '查看前置条件',
|
viewRequirements: '查看前置条件',
|
||||||
|
requirements: '前置条件',
|
||||||
requirementsNotMet: '前置条件未满足',
|
requirementsNotMet: '前置条件未满足',
|
||||||
current: '当前',
|
current: '当前',
|
||||||
level: '等级',
|
level: '等级',
|
||||||
@@ -281,8 +282,8 @@ export default {
|
|||||||
ionTechnology: '离子武器技术',
|
ionTechnology: '离子武器技术',
|
||||||
hyperspaceTechnology: '超空间跳跃技术',
|
hyperspaceTechnology: '超空间跳跃技术',
|
||||||
plasmaTechnology: '等离子武器技术',
|
plasmaTechnology: '等离子武器技术',
|
||||||
computerTechnology: '增加研究队列数量,每级+1队列(最多10级)',
|
computerTechnology: '增加研究队列和舰队任务槽位,每级+1队列+1槽位(最多10级)',
|
||||||
espionageTechnology: '提高间谍探测效果,每级提高1级侦查深度',
|
espionageTechnology: '提高间谍探测效果,每级提高1级侦查深度。侦察等级=己方等级-对方等级+侦察船数/5。≥-1显示舰队,≥1显示防御,≥3显示建筑,≥5显示科技',
|
||||||
weaponsTechnology: '提高舰船和防御的攻击力,每级+10%',
|
weaponsTechnology: '提高舰船和防御的攻击力,每级+10%',
|
||||||
shieldingTechnology: '提高舰船和防御的护盾值,每级+10%',
|
shieldingTechnology: '提高舰船和防御的护盾值,每级+10%',
|
||||||
armourTechnology: '提高舰船和防御的装甲值,每级+10%',
|
armourTechnology: '提高舰船和防御的装甲值,每级+10%',
|
||||||
@@ -352,6 +353,7 @@ export default {
|
|||||||
level: '等级',
|
level: '等级',
|
||||||
upgradeCost: '升级消耗',
|
upgradeCost: '升级消耗',
|
||||||
buildTime: '建造时间',
|
buildTime: '建造时间',
|
||||||
|
build: '建造',
|
||||||
upgrade: '升级',
|
upgrade: '升级',
|
||||||
maxLevelReached: '等级已满',
|
maxLevelReached: '等级已满',
|
||||||
requirementsNotMet: '条件不足',
|
requirementsNotMet: '条件不足',
|
||||||
@@ -461,6 +463,7 @@ export default {
|
|||||||
colonize: '殖民',
|
colonize: '殖民',
|
||||||
spy: '侦察',
|
spy: '侦察',
|
||||||
deploy: '部署',
|
deploy: '部署',
|
||||||
|
expedition: '探险',
|
||||||
recycle: '回收',
|
recycle: '回收',
|
||||||
destroy: '行星毁灭',
|
destroy: '行星毁灭',
|
||||||
transportResources: '运输资源',
|
transportResources: '运输资源',
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export default {
|
|||||||
goToBuildings: '前往建築頁面',
|
goToBuildings: '前往建築頁面',
|
||||||
locked: '已鎖定',
|
locked: '已鎖定',
|
||||||
viewRequirements: '查看前置條件',
|
viewRequirements: '查看前置條件',
|
||||||
|
requirements: '前置條件',
|
||||||
requirementsNotMet: '前置條件未滿足',
|
requirementsNotMet: '前置條件未滿足',
|
||||||
current: '當前',
|
current: '當前',
|
||||||
level: '等級',
|
level: '等級',
|
||||||
@@ -140,6 +141,7 @@ export default {
|
|||||||
jumpGate: '跳躍門',
|
jumpGate: '跳躍門',
|
||||||
planetDestroyerFactory: '行星毀滅者工廠',
|
planetDestroyerFactory: '行星毀滅者工廠',
|
||||||
buildTime: '建造時間',
|
buildTime: '建造時間',
|
||||||
|
build: '',
|
||||||
production: '產量',
|
production: '產量',
|
||||||
consumption: '消耗',
|
consumption: '消耗',
|
||||||
totalCost: '累積成本',
|
totalCost: '累積成本',
|
||||||
@@ -283,8 +285,8 @@ export default {
|
|||||||
ionTechnology: '離子武器技術',
|
ionTechnology: '離子武器技術',
|
||||||
hyperspaceTechnology: '超空間跳躍技術',
|
hyperspaceTechnology: '超空間跳躍技術',
|
||||||
plasmaTechnology: '等離子武器技術',
|
plasmaTechnology: '等離子武器技術',
|
||||||
computerTechnology: '增加研究佇列數量,每級+1佇列(最多10級)',
|
computerTechnology: '增加研究佇列和艦隊任務槽位,每級+1佇列+1槽位(最多10級)',
|
||||||
espionageTechnology: '提高間諜探測效果,每級提高1級偵查深度',
|
espionageTechnology: '提高間諜探測效果,每級提高1級偵查深度。偵察等級=己方等級-對方等級+偵察船數/5。≥-1顯示艦隊,≥1顯示防禦,≥3顯示建築,≥5顯示科技',
|
||||||
weaponsTechnology: '提高艦船和防禦的攻擊力,每級+10%',
|
weaponsTechnology: '提高艦船和防禦的攻擊力,每級+10%',
|
||||||
shieldingTechnology: '提高艦船和防禦的護盾值,每級+10%',
|
shieldingTechnology: '提高艦船和防禦的護盾值,每級+10%',
|
||||||
armourTechnology: '提高艦船和防禦的裝甲值,每級+10%',
|
armourTechnology: '提高艦船和防禦的裝甲值,每級+10%',
|
||||||
@@ -354,6 +356,7 @@ export default {
|
|||||||
gmModeActivated: '',
|
gmModeActivated: '',
|
||||||
upgradeCost: '升級消耗',
|
upgradeCost: '升級消耗',
|
||||||
buildTime: '建造時間',
|
buildTime: '建造時間',
|
||||||
|
build: '建造',
|
||||||
upgrade: '升級',
|
upgrade: '升級',
|
||||||
maxLevelReached: '等級已滿',
|
maxLevelReached: '等級已滿',
|
||||||
requirementsNotMet: '條件不足',
|
requirementsNotMet: '條件不足',
|
||||||
@@ -384,6 +387,7 @@ export default {
|
|||||||
fuelConsumption: '燃料消耗',
|
fuelConsumption: '燃料消耗',
|
||||||
buildCost: '建造成本',
|
buildCost: '建造成本',
|
||||||
buildTime: '建造時間',
|
buildTime: '建造時間',
|
||||||
|
build: '',
|
||||||
perUnit: '每個單位',
|
perUnit: '每個單位',
|
||||||
batchCalculator: '批量建造計算器',
|
batchCalculator: '批量建造計算器',
|
||||||
quantity: '數量',
|
quantity: '數量',
|
||||||
@@ -414,6 +418,7 @@ export default {
|
|||||||
armor: '裝甲',
|
armor: '裝甲',
|
||||||
buildCost: '建造成本',
|
buildCost: '建造成本',
|
||||||
buildTime: '建造時間',
|
buildTime: '建造時間',
|
||||||
|
build: '',
|
||||||
perUnit: '每個單位',
|
perUnit: '每個單位',
|
||||||
batchCalculator: '批量建造計算器',
|
batchCalculator: '批量建造計算器',
|
||||||
quantity: '數量',
|
quantity: '數量',
|
||||||
@@ -469,6 +474,7 @@ export default {
|
|||||||
colonize: '殖民',
|
colonize: '殖民',
|
||||||
spy: '偵察',
|
spy: '偵察',
|
||||||
deploy: '部署',
|
deploy: '部署',
|
||||||
|
expedition: '探險',
|
||||||
recycle: '回收',
|
recycle: '回收',
|
||||||
transportResources: '運輸資源',
|
transportResources: '運輸資源',
|
||||||
totalCargoCapacity: '總載貨量',
|
totalCargoCapacity: '總載貨量',
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { NPC, Planet, Player, FleetMission, SpyReport, SpiedNotification, I
|
|||||||
import { MissionType, ShipType, TechnologyType, RelationStatus } from '@/types/game'
|
import { MissionType, ShipType, TechnologyType, RelationStatus } from '@/types/game'
|
||||||
import * as fleetLogic from './fleetLogic'
|
import * as fleetLogic from './fleetLogic'
|
||||||
import * as diplomaticLogic from './diplomaticLogic'
|
import * as diplomaticLogic from './diplomaticLogic'
|
||||||
import { DIPLOMATIC_CONFIG } from '@/config/gameConfig'
|
import { DIPLOMATIC_CONFIG, SHIPS } from '@/config/gameConfig'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* NPC行为决策系统
|
* NPC行为决策系统
|
||||||
@@ -31,47 +31,47 @@ export interface DynamicBehaviorConfig {
|
|||||||
*/
|
*/
|
||||||
export const calculateDynamicBehavior = (playerPoints: number): DynamicBehaviorConfig => {
|
export const calculateDynamicBehavior = (playerPoints: number): DynamicBehaviorConfig => {
|
||||||
if (playerPoints < 1000) {
|
if (playerPoints < 1000) {
|
||||||
// 新手阶段:NPC很温和
|
// 新手阶段:NPC温和但会主动侦查攻击
|
||||||
return {
|
return {
|
||||||
spyInterval: 2400, // 40分钟侦查一次
|
spyInterval: 300, // 5分钟侦查一次(让新玩家快速体验游戏内容)
|
||||||
attackInterval: 4800, // 80分钟攻击一次
|
attackInterval: 600, // 10分钟攻击一次
|
||||||
attackProbability: 0.15, // 15%概率攻击
|
attackProbability: 0.4, // 40%概率攻击
|
||||||
minSpyProbes: 1,
|
minSpyProbes: 1,
|
||||||
attackFleetSizeRatio: 0.3 // 只派30%舰队
|
attackFleetSizeRatio: 0.3 // 只派30%舰队
|
||||||
}
|
}
|
||||||
} else if (playerPoints < 5000) {
|
} else if (playerPoints < 5000) {
|
||||||
// 初级阶段:NPC稍微激进
|
// 初级阶段:NPC比较激进
|
||||||
return {
|
return {
|
||||||
spyInterval: 1800, // 30分钟侦查一次
|
spyInterval: 420, // 7分钟侦查一次
|
||||||
attackInterval: 3600, // 60分钟攻击一次
|
attackInterval: 900, // 15分钟攻击一次
|
||||||
attackProbability: 0.25, // 25%概率攻击
|
attackProbability: 0.45, // 45%概率攻击
|
||||||
minSpyProbes: 2,
|
minSpyProbes: 2,
|
||||||
attackFleetSizeRatio: 0.5 // 派50%舰队
|
attackFleetSizeRatio: 0.5 // 派50%舰队
|
||||||
}
|
}
|
||||||
} else if (playerPoints < 20000) {
|
} else if (playerPoints < 20000) {
|
||||||
// 中级阶段:NPC比较激进
|
// 中级阶段:NPC很激进
|
||||||
return {
|
return {
|
||||||
spyInterval: 1200, // 20分钟侦查一次
|
spyInterval: 360, // 6分钟侦查一次
|
||||||
attackInterval: 2400, // 40分钟攻击一次
|
attackInterval: 720, // 12分钟攻击一次
|
||||||
attackProbability: 0.4, // 40%概率攻击
|
attackProbability: 0.55, // 55%概率攻击
|
||||||
minSpyProbes: 3,
|
minSpyProbes: 3,
|
||||||
attackFleetSizeRatio: 0.7 // 派70%舰队
|
attackFleetSizeRatio: 0.7 // 派70%舰队
|
||||||
}
|
}
|
||||||
} else if (playerPoints < 50000) {
|
} else if (playerPoints < 50000) {
|
||||||
// 高级阶段:NPC很激进
|
// 高级阶段:NPC非常激进
|
||||||
return {
|
return {
|
||||||
spyInterval: 900, // 15分钟侦查一次
|
spyInterval: 300, // 5分钟侦查一次
|
||||||
attackInterval: 1800, // 30分钟攻击一次
|
attackInterval: 600, // 10分钟攻击一次
|
||||||
attackProbability: 0.55, // 55%概率攻击
|
attackProbability: 0.65, // 65%概率攻击
|
||||||
minSpyProbes: 4,
|
minSpyProbes: 4,
|
||||||
attackFleetSizeRatio: 0.85 // 派85%舰队
|
attackFleetSizeRatio: 0.85 // 派85%舰队
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 专家阶段:NPC非常激进
|
// 专家阶段:NPC极度激进
|
||||||
return {
|
return {
|
||||||
spyInterval: 600, // 10分钟侦查一次
|
spyInterval: 240, // 4分钟侦查一次
|
||||||
attackInterval: 1200, // 20分钟攻击一次
|
attackInterval: 480, // 8分钟攻击一次
|
||||||
attackProbability: 0.7, // 70%概率攻击
|
attackProbability: 0.8, // 80%概率攻击
|
||||||
minSpyProbes: 5,
|
minSpyProbes: 5,
|
||||||
attackFleetSizeRatio: 0.95 // 派95%舰队
|
attackFleetSizeRatio: 0.95 // 派95%舰队
|
||||||
}
|
}
|
||||||
@@ -89,12 +89,12 @@ export const shouldNPCSpyPlayer = (npc: NPC, player: Player, currentTime: number
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查外交关系 - 根据关系状态调整侦查概率
|
// 检查外交关系 - 只有中立和敌对NPC才会侦查
|
||||||
const relation = npc.relations?.[player.id]
|
const relation = npc.relations?.[player.id]
|
||||||
if (relation) {
|
if (relation) {
|
||||||
if (relation.status === RelationStatus.Friendly) {
|
if (relation.status === RelationStatus.Friendly) {
|
||||||
// 友好NPC侦查频率降低到50%
|
// 友好NPC不侦查玩家
|
||||||
return Math.random() < 0.5
|
return false
|
||||||
}
|
}
|
||||||
if (relation.status === RelationStatus.Hostile) {
|
if (relation.status === RelationStatus.Hostile) {
|
||||||
// 敌对NPC必定侦查
|
// 敌对NPC必定侦查
|
||||||
@@ -102,6 +102,7 @@ export const shouldNPCSpyPlayer = (npc: NPC, player: Player, currentTime: number
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 中立或无关系:正常侦查
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,7 +117,7 @@ export const shouldNPCAttackPlayer = (npc: NPC, player: Player, currentTime: num
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查外交关系
|
// 检查外交关系 - 只有中立和敌对NPC才会攻击
|
||||||
const relation = npc.relations?.[player.id]
|
const relation = npc.relations?.[player.id]
|
||||||
if (relation) {
|
if (relation) {
|
||||||
if (relation.status === RelationStatus.Friendly) {
|
if (relation.status === RelationStatus.Friendly) {
|
||||||
@@ -125,11 +126,11 @@ export const shouldNPCAttackPlayer = (npc: NPC, player: Player, currentTime: num
|
|||||||
}
|
}
|
||||||
if (relation.status === RelationStatus.Hostile) {
|
if (relation.status === RelationStatus.Hostile) {
|
||||||
// 敌对NPC攻击概率翻倍
|
// 敌对NPC攻击概率翻倍
|
||||||
return Math.random() < config.attackProbability * 2.0
|
return Math.random() < Math.min(config.attackProbability * 2.0, 1.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 中立或无关系:正常概率
|
// 中立或无关系:正常概率攻击
|
||||||
return Math.random() < config.attackProbability
|
return Math.random() < config.attackProbability
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1059,3 +1060,136 @@ export const createNPCRevengeMission = (npc: NPC, allPlanets: Planet[], config:
|
|||||||
|
|
||||||
return mission
|
return mission
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NPC状态诊断函数 - 用于调试和了解NPC当前状态
|
||||||
|
*/
|
||||||
|
export interface NPCDiagnosticInfo {
|
||||||
|
npcId: string
|
||||||
|
npcName: string
|
||||||
|
difficulty: string
|
||||||
|
relationStatus: string
|
||||||
|
reputation: number
|
||||||
|
canSpy: boolean
|
||||||
|
canAttack: boolean
|
||||||
|
spyProbes: number
|
||||||
|
totalFleetPower: number
|
||||||
|
lastSpyTime: number
|
||||||
|
lastAttackTime: number
|
||||||
|
timeSinceLastSpy: number
|
||||||
|
timeSinceLastAttack: number
|
||||||
|
nextSpyIn: number
|
||||||
|
nextAttackIn: number
|
||||||
|
attackProbability: number
|
||||||
|
reasons: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const diagnoseNPCBehavior = (
|
||||||
|
npcs: NPC[],
|
||||||
|
player: Player,
|
||||||
|
currentTime: number
|
||||||
|
): NPCDiagnosticInfo[] => {
|
||||||
|
const playerPoints = player.points || 0
|
||||||
|
const config = calculateDynamicBehavior(playerPoints)
|
||||||
|
|
||||||
|
return npcs.map(npc => {
|
||||||
|
const planet = npc.planets[0]
|
||||||
|
const relation = npc.relations?.[player.id]
|
||||||
|
const reasons: string[] = []
|
||||||
|
|
||||||
|
// 检查外交关系
|
||||||
|
let canSpy = true
|
||||||
|
let canAttack = true
|
||||||
|
let relationStatus = '无关系'
|
||||||
|
let reputation = 0
|
||||||
|
|
||||||
|
if (relation) {
|
||||||
|
relationStatus = relation.status === RelationStatus.Friendly ? '友好' :
|
||||||
|
relation.status === RelationStatus.Hostile ? '敌对' : '中立'
|
||||||
|
reputation = relation.reputation || 0
|
||||||
|
|
||||||
|
if (relation.status === RelationStatus.Friendly) {
|
||||||
|
canSpy = false
|
||||||
|
canAttack = false
|
||||||
|
reasons.push('友好NPC不会侦查或攻击玩家')
|
||||||
|
} else if (relation.status === RelationStatus.Hostile) {
|
||||||
|
reasons.push('敌对NPC攻击概率翻倍')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查侦查探测器数量
|
||||||
|
const spyProbes = planet?.fleet?.[ShipType.EspionageProbe] || 0
|
||||||
|
if (spyProbes < config.minSpyProbes) {
|
||||||
|
canSpy = false
|
||||||
|
reasons.push(`侦查探测器不足 (${spyProbes}/${config.minSpyProbes})`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算舰队战力
|
||||||
|
let totalFleetPower = 0
|
||||||
|
if (planet?.fleet) {
|
||||||
|
Object.entries(planet.fleet).forEach(([shipType, count]) => {
|
||||||
|
const shipConfig = SHIPS[shipType as ShipType]
|
||||||
|
if (shipConfig) {
|
||||||
|
const power = shipConfig.attack + shipConfig.shield + shipConfig.armor / 10
|
||||||
|
totalFleetPower += power * (count as number)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalFleetPower === 0) {
|
||||||
|
canAttack = false
|
||||||
|
reasons.push('没有战斗舰队')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 时间检查
|
||||||
|
const lastSpyTime = npc.lastSpyTime || 0
|
||||||
|
const lastAttackTime = npc.lastAttackTime || 0
|
||||||
|
const timeSinceLastSpy = Math.floor((currentTime - lastSpyTime) / 1000)
|
||||||
|
const timeSinceLastAttack = Math.floor((currentTime - lastAttackTime) / 1000)
|
||||||
|
|
||||||
|
const nextSpyIn = Math.max(0, config.spyInterval - timeSinceLastSpy)
|
||||||
|
const nextAttackIn = Math.max(0, config.attackInterval - timeSinceLastAttack)
|
||||||
|
|
||||||
|
if (timeSinceLastSpy < config.spyInterval) {
|
||||||
|
reasons.push(`侦查冷却中 (${Math.floor(nextSpyIn / 60)}分${nextSpyIn % 60}秒)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeSinceLastAttack < config.attackInterval) {
|
||||||
|
reasons.push(`攻击冷却中 (${Math.floor(nextAttackIn / 60)}分${nextAttackIn % 60}秒)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否已经侦查过玩家
|
||||||
|
const hasSpiedPlayer = npc.playerSpyReports && Object.keys(npc.playerSpyReports).length > 0
|
||||||
|
|
||||||
|
if (!hasSpiedPlayer && canAttack) {
|
||||||
|
canAttack = false
|
||||||
|
reasons.push('尚未侦查过玩家,无法攻击')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算实际攻击概率
|
||||||
|
let actualAttackProbability = config.attackProbability
|
||||||
|
if (relation?.status === RelationStatus.Hostile) {
|
||||||
|
actualAttackProbability = Math.min(config.attackProbability * 2.0, 1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
npcId: npc.id,
|
||||||
|
npcName: npc.name,
|
||||||
|
difficulty: npc.difficulty,
|
||||||
|
relationStatus,
|
||||||
|
reputation,
|
||||||
|
canSpy,
|
||||||
|
canAttack,
|
||||||
|
spyProbes,
|
||||||
|
totalFleetPower: Math.floor(totalFleetPower),
|
||||||
|
lastSpyTime,
|
||||||
|
lastAttackTime,
|
||||||
|
timeSinceLastSpy,
|
||||||
|
timeSinceLastAttack,
|
||||||
|
nextSpyIn,
|
||||||
|
nextAttackIn,
|
||||||
|
attackProbability: actualAttackProbability,
|
||||||
|
reasons
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,25 +23,25 @@ export interface NPCGrowthGameState {
|
|||||||
// NPC成长配置(旧版,保留用于兼容)
|
// NPC成长配置(旧版,保留用于兼容)
|
||||||
export const NPC_GROWTH_CONFIG = {
|
export const NPC_GROWTH_CONFIG = {
|
||||||
easy: {
|
easy: {
|
||||||
powerRatio: 0.6, // 实力比例(相对玩家)
|
powerRatio: 1.0, // 实力比例(相对玩家) - 提升到1.0,与玩家势均力敌
|
||||||
checkInterval: 300, // 检查间隔(秒) - 5分钟
|
checkInterval: 300, // 检查间隔(秒) - 5分钟
|
||||||
resourceGrowthRate: 0.5, // 资源增长速率系数
|
resourceGrowthRate: 1.3, // 资源增长速率系数 - 大幅提升
|
||||||
buildingGrowthSpeed: 0.5, // 建筑升级速度系数
|
buildingGrowthSpeed: 1.0, // 建筑升级速度系数
|
||||||
techGrowthSpeed: 0.5 // 科技研究速度系数
|
techGrowthSpeed: 1.0 // 科技研究速度系数
|
||||||
},
|
},
|
||||||
medium: {
|
medium: {
|
||||||
powerRatio: 0.8,
|
powerRatio: 1.5, // 提升到1.5,超越玩家
|
||||||
checkInterval: 180, // 3分钟
|
checkInterval: 180, // 3分钟
|
||||||
resourceGrowthRate: 0.8,
|
resourceGrowthRate: 1.8, // 大幅提升资源增长
|
||||||
buildingGrowthSpeed: 0.8,
|
buildingGrowthSpeed: 1.5,
|
||||||
techGrowthSpeed: 0.8
|
techGrowthSpeed: 1.5
|
||||||
},
|
},
|
||||||
hard: {
|
hard: {
|
||||||
powerRatio: 1.1,
|
powerRatio: 2.0, // 提升到2.0,远超玩家
|
||||||
checkInterval: 120, // 2分钟
|
checkInterval: 120, // 2分钟
|
||||||
resourceGrowthRate: 1.2,
|
resourceGrowthRate: 2.5, // 极高资源增长
|
||||||
buildingGrowthSpeed: 1.0,
|
buildingGrowthSpeed: 2.0,
|
||||||
techGrowthSpeed: 1.0
|
techGrowthSpeed: 2.0
|
||||||
}
|
}
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
@@ -61,58 +61,58 @@ export const calculateDynamicDifficulty = (playerPoints: number): DynamicDifficu
|
|||||||
// 积分区间和对应的难度参数
|
// 积分区间和对应的难度参数
|
||||||
if (playerPoints < 1000) {
|
if (playerPoints < 1000) {
|
||||||
// 新手期:0-1,000分
|
// 新手期:0-1,000分
|
||||||
// NPC保持30-50%实力,给予充分发展空间,但资源增长速度加快
|
// NPC保持50-70%实力,给予发展空间但保持挑战
|
||||||
const ratio = 0.3 + (playerPoints / 1000) * 0.2
|
const ratio = 0.5 + (playerPoints / 1000) * 0.2
|
||||||
return {
|
return {
|
||||||
powerRatio: ratio,
|
powerRatio: ratio,
|
||||||
checkInterval: 300, // 5分钟
|
checkInterval: 300, // 5分钟
|
||||||
resourceGrowthRate: 0.8, // 从0.4提升到0.8,确保NPC有足够资源发育
|
resourceGrowthRate: 1.2, // 提升资源增长,确保NPC快速发育
|
||||||
buildingGrowthSpeed: 0.6, // 从0.4提升到0.6
|
|
||||||
techGrowthSpeed: 0.6 // 从0.4提升到0.6
|
|
||||||
}
|
|
||||||
} else if (playerPoints < 5000) {
|
|
||||||
// 初级期:1,000-5,000分
|
|
||||||
// NPC保持50-70%实力,逐渐增加挑战
|
|
||||||
const ratio = 0.5 + ((playerPoints - 1000) / 4000) * 0.2
|
|
||||||
return {
|
|
||||||
powerRatio: ratio,
|
|
||||||
checkInterval: 240, // 4分钟
|
|
||||||
resourceGrowthRate: 1.0, // 从0.6提升到1.0,与玩家资源产出相当
|
|
||||||
buildingGrowthSpeed: 0.8, // 从0.6提升到0.8
|
|
||||||
techGrowthSpeed: 0.8 // 从0.6提升到0.8
|
|
||||||
}
|
|
||||||
} else if (playerPoints < 20000) {
|
|
||||||
// 中级期:5,000-20,000分
|
|
||||||
// NPC保持70-90%实力,持续挑战
|
|
||||||
const ratio = 0.7 + ((playerPoints - 5000) / 15000) * 0.2
|
|
||||||
return {
|
|
||||||
powerRatio: ratio,
|
|
||||||
checkInterval: 180, // 3分钟
|
|
||||||
resourceGrowthRate: 0.8,
|
|
||||||
buildingGrowthSpeed: 0.8,
|
buildingGrowthSpeed: 0.8,
|
||||||
techGrowthSpeed: 0.8
|
techGrowthSpeed: 0.8
|
||||||
}
|
}
|
||||||
|
} else if (playerPoints < 5000) {
|
||||||
|
// 初级期:1,000-5,000分
|
||||||
|
// NPC保持70-110%实力,快速追赶玩家
|
||||||
|
const ratio = 0.7 + ((playerPoints - 1000) / 4000) * 0.4
|
||||||
|
return {
|
||||||
|
powerRatio: ratio,
|
||||||
|
checkInterval: 240, // 4分钟
|
||||||
|
resourceGrowthRate: 1.5, // 大幅提升资源增长速度
|
||||||
|
buildingGrowthSpeed: 1.2,
|
||||||
|
techGrowthSpeed: 1.2
|
||||||
|
}
|
||||||
|
} else if (playerPoints < 20000) {
|
||||||
|
// 中级期:5,000-20,000分
|
||||||
|
// NPC保持110-150%实力,形成强大威胁
|
||||||
|
const ratio = 1.1 + ((playerPoints - 5000) / 15000) * 0.4
|
||||||
|
return {
|
||||||
|
powerRatio: ratio,
|
||||||
|
checkInterval: 180, // 3分钟
|
||||||
|
resourceGrowthRate: 1.8, // 极大幅提升资源增长
|
||||||
|
buildingGrowthSpeed: 1.5,
|
||||||
|
techGrowthSpeed: 1.5
|
||||||
|
}
|
||||||
} else if (playerPoints < 50000) {
|
} else if (playerPoints < 50000) {
|
||||||
// 高级期:20,000-50,000分
|
// 高级期:20,000-50,000分
|
||||||
// NPC保持90-110%实力,与玩家势均力敌
|
// NPC保持150-200%实力,远超玩家
|
||||||
const ratio = 0.9 + ((playerPoints - 20000) / 30000) * 0.2
|
const ratio = 1.5 + ((playerPoints - 20000) / 30000) * 0.5
|
||||||
return {
|
return {
|
||||||
powerRatio: ratio,
|
powerRatio: ratio,
|
||||||
checkInterval: 150, // 2.5分钟
|
checkInterval: 150, // 2.5分钟
|
||||||
resourceGrowthRate: 1.0,
|
resourceGrowthRate: 2.2, // 极高资源增长
|
||||||
buildingGrowthSpeed: 1.0,
|
buildingGrowthSpeed: 1.8,
|
||||||
techGrowthSpeed: 1.0
|
techGrowthSpeed: 1.8
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 专家期:50,000+分
|
// 专家期:50,000+分
|
||||||
// NPC保持110-130%实力,超越玩家
|
// NPC保持200-250%实力,成为超强对手
|
||||||
const ratio = Math.min(1.3, 1.1 + ((playerPoints - 50000) / 50000) * 0.2)
|
const ratio = Math.min(2.5, 2.0 + ((playerPoints - 50000) / 50000) * 0.5)
|
||||||
return {
|
return {
|
||||||
powerRatio: ratio,
|
powerRatio: ratio,
|
||||||
checkInterval: 120, // 2分钟
|
checkInterval: 120, // 2分钟
|
||||||
resourceGrowthRate: 1.2,
|
resourceGrowthRate: 2.5, // 极高的资源增长速度
|
||||||
buildingGrowthSpeed: 1.2,
|
buildingGrowthSpeed: 2.0,
|
||||||
techGrowthSpeed: 1.2
|
techGrowthSpeed: 2.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -543,6 +543,50 @@ export const initializeNPCStartingPower = (
|
|||||||
planet.resources.crystal = 50000 * config.powerRatio
|
planet.resources.crystal = 50000 * config.powerRatio
|
||||||
planet.resources.deuterium = 20000 * config.powerRatio
|
planet.resources.deuterium = 20000 * config.powerRatio
|
||||||
planet.resources.darkMatter = 1000 * config.powerRatio
|
planet.resources.darkMatter = 1000 * config.powerRatio
|
||||||
|
|
||||||
|
// 给予起始舰队(确保NPC能够立即侦查和攻击)
|
||||||
|
// 使用平方根函数来平滑舰队数量增长,避免初期过于强大
|
||||||
|
const fleetRatio = Math.sqrt(config.powerRatio)
|
||||||
|
|
||||||
|
// 间谍探测器 - 必需,用于侦查玩家
|
||||||
|
planet.fleet[ShipType.EspionageProbe] = Math.max(5, Math.floor(10 * fleetRatio))
|
||||||
|
|
||||||
|
// 基础战斗舰队
|
||||||
|
planet.fleet[ShipType.LightFighter] = Math.floor(20 * fleetRatio)
|
||||||
|
planet.fleet[ShipType.HeavyFighter] = Math.floor(10 * fleetRatio)
|
||||||
|
planet.fleet[ShipType.Cruiser] = Math.floor(5 * fleetRatio)
|
||||||
|
|
||||||
|
// 运输和回收舰船
|
||||||
|
planet.fleet[ShipType.SmallCargo] = Math.floor(5 * fleetRatio)
|
||||||
|
planet.fleet[ShipType.Recycler] = Math.floor(3 * fleetRatio)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确保所有NPC都有最低数量的间谍探测器
|
||||||
|
* 用于修复旧版本保存的NPC数据
|
||||||
|
*/
|
||||||
|
export const ensureNPCSpyProbes = (npcs: NPC[]): void => {
|
||||||
|
npcs.forEach(npc => {
|
||||||
|
const planet = npc.planets[0]
|
||||||
|
if (!planet) return
|
||||||
|
|
||||||
|
// 如果没有舰队数据,初始化
|
||||||
|
if (!planet.fleet) {
|
||||||
|
planet.fleet = {} as any
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查间谍探测器数量
|
||||||
|
const currentProbes = planet.fleet[ShipType.EspionageProbe] || 0
|
||||||
|
|
||||||
|
// 如果没有探测器,根据NPC难度给予基础数量
|
||||||
|
if (currentProbes === 0) {
|
||||||
|
const config = NPC_GROWTH_CONFIG[npc.difficulty]
|
||||||
|
const fleetRatio = Math.sqrt(config.powerRatio)
|
||||||
|
planet.fleet[ShipType.EspionageProbe] = Math.max(5, Math.floor(10 * fleetRatio))
|
||||||
|
|
||||||
|
console.log(`[NPC Migration] Added ${planet.fleet[ShipType.EspionageProbe]} spy probes to NPC ${npc.name}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3,12 +3,13 @@
|
|||||||
* 提供跨模块共享的通用业务逻辑功能
|
* 提供跨模块共享的通用业务逻辑功能
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { BuildingType, TechnologyType } from '@/types/game'
|
import { BuildingType, TechnologyType, ShipType, DefenseType } from '@/types/game'
|
||||||
import type { Planet, Resources, Officer, BuildingConfig, TechnologyConfig } from '@/types/game'
|
import type { Planet, Resources, Officer, BuildingConfig, TechnologyConfig, Player } from '@/types/game'
|
||||||
import { OfficerType } from '@/types/game'
|
import { OfficerType } from '@/types/game'
|
||||||
import * as officerLogic from '@/logic/officerLogic'
|
import * as officerLogic from '@/logic/officerLogic'
|
||||||
import * as resourceLogic from '@/logic/resourceLogic'
|
import * as resourceLogic from '@/logic/resourceLogic'
|
||||||
import { scaleResources } from '@/utils/speed'
|
import { scaleResources } from '@/utils/speed'
|
||||||
|
import { BUILDINGS, TECHNOLOGIES, SHIPS, DEFENSES } from '@/config/gameConfig'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取特定等级的升级条件
|
* 获取特定等级的升级条件
|
||||||
@@ -139,8 +140,128 @@ export const getMaxResearchQueue = (technologies: Partial<Record<TechnologyType,
|
|||||||
/**
|
/**
|
||||||
* 计算最大舰队任务数量
|
* 计算最大舰队任务数量
|
||||||
* @param additionalFleetSlots 军官提供的额外槽位数量
|
* @param additionalFleetSlots 军官提供的额外槽位数量
|
||||||
* @returns 最大舰队任务数量(基础1个 + 军官加成,最多10个)
|
* @param computerTechnologyLevel 计算机技术等级
|
||||||
|
* @returns 最大舰队任务数量(基础1个 + 计算机技术等级 + 军官加成,最多20个)
|
||||||
*/
|
*/
|
||||||
export const getMaxFleetMissions = (additionalFleetSlots: number = 0): number => {
|
export const getMaxFleetMissions = (additionalFleetSlots: number = 0, computerTechnologyLevel: number = 0): number => {
|
||||||
return Math.min(1 + additionalFleetSlots, 10)
|
return Math.min(1 + computerTechnologyLevel + additionalFleetSlots, 20)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算建筑的总成本(从等级1到目标等级的累计成本)
|
||||||
|
* @param buildingType 建筑类型
|
||||||
|
* @param level 目标等级
|
||||||
|
* @returns 总资源成本(金属+水晶+重氢)
|
||||||
|
*/
|
||||||
|
const calculateBuildingTotalCost = (buildingType: BuildingType, level: number): number => {
|
||||||
|
if (level <= 0) return 0
|
||||||
|
|
||||||
|
const config = BUILDINGS[buildingType]
|
||||||
|
if (!config) return 0
|
||||||
|
|
||||||
|
let totalCost = 0
|
||||||
|
const { baseCost, costMultiplier } = config
|
||||||
|
|
||||||
|
// 累加从等级1到目标等级的所有成本
|
||||||
|
for (let i = 1; i <= level; i++) {
|
||||||
|
const levelCost = {
|
||||||
|
metal: Math.floor(baseCost.metal * Math.pow(costMultiplier, i - 1)),
|
||||||
|
crystal: Math.floor(baseCost.crystal * Math.pow(costMultiplier, i - 1)),
|
||||||
|
deuterium: Math.floor(baseCost.deuterium * Math.pow(costMultiplier, i - 1))
|
||||||
|
}
|
||||||
|
totalCost += levelCost.metal + levelCost.crystal + levelCost.deuterium
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalCost
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算科技的总成本(从等级1到目标等级的累计成本)
|
||||||
|
* @param techType 科技类型
|
||||||
|
* @param level 目标等级
|
||||||
|
* @returns 总资源成本(金属+水晶+重氢)
|
||||||
|
*/
|
||||||
|
const calculateTechnologyTotalCost = (techType: TechnologyType, level: number): number => {
|
||||||
|
if (level <= 0) return 0
|
||||||
|
|
||||||
|
const config = TECHNOLOGIES[techType]
|
||||||
|
if (!config) return 0
|
||||||
|
|
||||||
|
let totalCost = 0
|
||||||
|
const { baseCost, costMultiplier } = config
|
||||||
|
|
||||||
|
// 累加从等级1到目标等级的所有成本
|
||||||
|
for (let i = 1; i <= level; i++) {
|
||||||
|
const levelCost = {
|
||||||
|
metal: Math.floor(baseCost.metal * Math.pow(costMultiplier, i - 1)),
|
||||||
|
crystal: Math.floor(baseCost.crystal * Math.pow(costMultiplier, i - 1)),
|
||||||
|
deuterium: Math.floor(baseCost.deuterium * Math.pow(costMultiplier, i - 1))
|
||||||
|
}
|
||||||
|
totalCost += levelCost.metal + levelCost.crystal + levelCost.deuterium
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalCost
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算单个舰船的成本
|
||||||
|
* @param shipType 舰船类型
|
||||||
|
* @returns 单个舰船的资源成本(金属+水晶+重氢)
|
||||||
|
*/
|
||||||
|
const calculateShipUnitCost = (shipType: ShipType): number => {
|
||||||
|
const config = SHIPS[shipType]
|
||||||
|
if (!config) return 0
|
||||||
|
|
||||||
|
return config.cost.metal + config.cost.crystal + config.cost.deuterium
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算单个防御的成本
|
||||||
|
* @param defenseType 防御类型
|
||||||
|
* @returns 单个防御的资源成本(金属+水晶+重氢)
|
||||||
|
*/
|
||||||
|
const calculateDefenseUnitCost = (defenseType: DefenseType): number => {
|
||||||
|
const config = DEFENSES[defenseType]
|
||||||
|
if (!config) return 0
|
||||||
|
|
||||||
|
return config.cost.metal + config.cost.crystal + config.cost.deuterium
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算玩家的总积分
|
||||||
|
* 积分规则:(建筑成本 + 科技成本 + 舰队成本 + 防御成本) / 1000
|
||||||
|
* @param player 玩家对象
|
||||||
|
* @returns 玩家总积分
|
||||||
|
*/
|
||||||
|
export const calculatePlayerPoints = (player: Player): number => {
|
||||||
|
let totalCost = 0
|
||||||
|
|
||||||
|
// 1. 计算所有星球的建筑成本
|
||||||
|
player.planets.forEach(planet => {
|
||||||
|
Object.entries(planet.buildings).forEach(([buildingType, level]) => {
|
||||||
|
totalCost += calculateBuildingTotalCost(buildingType as BuildingType, level)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 2. 计算科技成本
|
||||||
|
Object.entries(player.technologies).forEach(([techType, level]) => {
|
||||||
|
totalCost += calculateTechnologyTotalCost(techType as TechnologyType, level)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 3. 计算所有星球的舰队成本
|
||||||
|
player.planets.forEach(planet => {
|
||||||
|
Object.entries(planet.fleet).forEach(([shipType, count]) => {
|
||||||
|
totalCost += calculateShipUnitCost(shipType as ShipType) * count
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 4. 计算所有星球的防御成本
|
||||||
|
player.planets.forEach(planet => {
|
||||||
|
Object.entries(planet.defense).forEach(([defenseType, count]) => {
|
||||||
|
totalCost += calculateDefenseUnitCost(defenseType as DefenseType) * count
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 每1000资源 = 1积分
|
||||||
|
return Math.floor(totalCost / 1000)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ export const calculateResourceProduction = (
|
|||||||
metal: metalMineLevel * 1500 * Math.pow(1.5, metalMineLevel) * resourceBonus * productionEfficiency,
|
metal: metalMineLevel * 1500 * Math.pow(1.5, metalMineLevel) * resourceBonus * productionEfficiency,
|
||||||
crystal: crystalMineLevel * 1000 * Math.pow(1.5, crystalMineLevel) * resourceBonus * productionEfficiency,
|
crystal: crystalMineLevel * 1000 * Math.pow(1.5, crystalMineLevel) * resourceBonus * productionEfficiency,
|
||||||
deuterium: deuteriumSynthesizerLevel * 500 * Math.pow(1.5, deuteriumSynthesizerLevel) * resourceBonus * productionEfficiency,
|
deuterium: deuteriumSynthesizerLevel * 500 * Math.pow(1.5, deuteriumSynthesizerLevel) * resourceBonus * productionEfficiency,
|
||||||
darkMatter: darkMatterCollectorLevel * 25 * Math.pow(1.5, darkMatterCollectorLevel) * darkMatterBonus,
|
darkMatter: darkMatterCollectorLevel * 100 * Math.pow(1.5, darkMatterCollectorLevel) * darkMatterBonus,
|
||||||
energy: energyProduction
|
energy: energyProduction
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -149,7 +149,8 @@ export const validateFleetDispatch = (
|
|||||||
fleet: Partial<Fleet>,
|
fleet: Partial<Fleet>,
|
||||||
cargo: Resources,
|
cargo: Resources,
|
||||||
officers: Record<OfficerType, Officer>,
|
officers: Record<OfficerType, Officer>,
|
||||||
currentFleetMissions: number = 0
|
currentFleetMissions: number = 0,
|
||||||
|
technologies: Partial<Record<TechnologyType, number>> = {}
|
||||||
): {
|
): {
|
||||||
valid: boolean
|
valid: boolean
|
||||||
reason?: string
|
reason?: string
|
||||||
@@ -159,7 +160,8 @@ export const validateFleetDispatch = (
|
|||||||
const bonuses = officerLogic.calculateActiveBonuses(officers, Date.now())
|
const bonuses = officerLogic.calculateActiveBonuses(officers, Date.now())
|
||||||
|
|
||||||
// 检查舰队任务槽位是否已满
|
// 检查舰队任务槽位是否已满
|
||||||
const maxFleetMissions = publicLogic.getMaxFleetMissions(bonuses.additionalFleetSlots)
|
const computerTechLevel = technologies[TechnologyType.ComputerTechnology] || 0
|
||||||
|
const maxFleetMissions = publicLogic.getMaxFleetMissions(bonuses.additionalFleetSlots, computerTechLevel)
|
||||||
if (currentFleetMissions >= maxFleetMissions) {
|
if (currentFleetMissions >= maxFleetMissions) {
|
||||||
return { valid: false, reason: 'errors.fleetMissionsFull' }
|
return { valid: false, reason: 'errors.fleetMissionsFull' }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Planet, DebrisField } from '@/types/game'
|
import type { Planet, DebrisField, NPC } from '@/types/game'
|
||||||
import { decryptData, encryptData } from './crypto'
|
import { decryptData, encryptData } from './crypto'
|
||||||
import pkg from '../../package.json'
|
import pkg from '../../package.json'
|
||||||
|
|
||||||
@@ -65,10 +65,48 @@ export const migrateGameData = (): void => {
|
|||||||
universeData.debrisFields = oldData.debrisFields
|
universeData.debrisFields = oldData.debrisFields
|
||||||
delete oldData.debrisFields
|
delete oldData.debrisFields
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 修复NPC数据(确保所有必需字段都存在)
|
||||||
|
if (oldData.npcs && Array.isArray(oldData.npcs)) {
|
||||||
|
oldData.npcs.forEach((npc: NPC) => {
|
||||||
|
// 确保NPC有必需的时间字段
|
||||||
|
if (npc.lastSpyTime === undefined) {
|
||||||
|
npc.lastSpyTime = 0
|
||||||
|
}
|
||||||
|
if (npc.lastAttackTime === undefined) {
|
||||||
|
npc.lastAttackTime = 0
|
||||||
|
}
|
||||||
|
// 确保NPC有必需的数组字段
|
||||||
|
if (!npc.fleetMissions) {
|
||||||
|
npc.fleetMissions = []
|
||||||
|
}
|
||||||
|
if (!npc.playerSpyReports) {
|
||||||
|
npc.playerSpyReports = {}
|
||||||
|
}
|
||||||
|
if (!npc.relations) {
|
||||||
|
npc.relations = {}
|
||||||
|
}
|
||||||
|
if (!npc.allies) {
|
||||||
|
npc.allies = []
|
||||||
|
}
|
||||||
|
if (!npc.enemies) {
|
||||||
|
npc.enemies = []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化玩家积分(如果不存在)
|
||||||
|
if (oldData.player && oldData.player.points === undefined) {
|
||||||
|
// 积分会在游戏启动时通过 initGame 计算,这里设置为0
|
||||||
|
oldData.player.points = 0
|
||||||
|
}
|
||||||
|
|
||||||
// 保存迁移后的数据
|
// 保存迁移后的数据
|
||||||
localStorage.setItem(universeStorageKey, encryptData(universeData))
|
localStorage.setItem(universeStorageKey, encryptData(universeData))
|
||||||
localStorage.setItem(storageKey, encryptData(oldData))
|
localStorage.setItem(storageKey, encryptData(oldData))
|
||||||
|
|
||||||
|
console.log('[Migration] Game data migrated successfully')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error('[Migration] Failed to migrate game data:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,9 +128,18 @@
|
|||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>{{ alertDialogTitle }}</AlertDialogTitle>
|
<AlertDialogTitle>{{ alertDialogTitle }}</AlertDialogTitle>
|
||||||
<AlertDialogDescription class="whitespace-pre-line">
|
<AlertDialogDescription v-if="!alertDialogShowRequirements" class="whitespace-pre-line">
|
||||||
{{ alertDialogMessage }}
|
{{ alertDialogMessage }}
|
||||||
</AlertDialogDescription>
|
</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>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogAction>{{ t('common.confirm') }}</AlertDialogAction>
|
<AlertDialogAction>{{ t('common.confirm') }}</AlertDialogAction>
|
||||||
@@ -179,7 +188,7 @@
|
|||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle
|
AlertDialogTitle
|
||||||
} from '@/components/ui/alert-dialog'
|
} from '@/components/ui/alert-dialog'
|
||||||
import { Clock, Grid3x3 } from 'lucide-vue-next'
|
import { Clock, Grid3x3, Check, X } from 'lucide-vue-next'
|
||||||
import { formatNumber, formatTime, getResourceCostColor } from '@/utils/format'
|
import { formatNumber, formatTime, getResourceCostColor } from '@/utils/format'
|
||||||
import * as buildingLogic from '@/logic/buildingLogic'
|
import * as buildingLogic from '@/logic/buildingLogic'
|
||||||
import * as buildingValidation from '@/logic/buildingValidation'
|
import * as buildingValidation from '@/logic/buildingValidation'
|
||||||
@@ -196,6 +205,8 @@
|
|||||||
const alertDialogOpen = ref(false)
|
const alertDialogOpen = ref(false)
|
||||||
const alertDialogTitle = ref('')
|
const alertDialogTitle = ref('')
|
||||||
const alertDialogMessage = 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 demolishConfirmOpen = ref(false)
|
||||||
@@ -248,7 +259,9 @@
|
|||||||
// 检查前置条件
|
// 检查前置条件
|
||||||
if (!checkUpgradeRequirements(buildingType)) {
|
if (!checkUpgradeRequirements(buildingType)) {
|
||||||
alertDialogTitle.value = t('common.requirementsNotMet')
|
alertDialogTitle.value = t('common.requirementsNotMet')
|
||||||
alertDialogMessage.value = getRequirementsList(buildingType)
|
alertDialogRequirements.value = getRequirementsList(buildingType)
|
||||||
|
alertDialogShowRequirements.value = true
|
||||||
|
alertDialogMessage.value = ''
|
||||||
alertDialogOpen.value = true
|
alertDialogOpen.value = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -257,6 +270,7 @@
|
|||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
alertDialogTitle.value = t('buildingsView.upgradeFailed')
|
alertDialogTitle.value = t('buildingsView.upgradeFailed')
|
||||||
alertDialogMessage.value = result.reason ? t(result.reason) : t('buildingsView.upgradeFailedMessage')
|
alertDialogMessage.value = result.reason ? t(result.reason) : t('buildingsView.upgradeFailedMessage')
|
||||||
|
alertDialogShowRequirements.value = false
|
||||||
alertDialogOpen.value = true
|
alertDialogOpen.value = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -292,18 +306,23 @@
|
|||||||
return t('buildingsView.maxLevelReached') // "等级已满"
|
return t('buildingsView.maxLevelReached') // "等级已满"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (planet.value.buildQueue.length > 0) return t('buildingsView.upgrade')
|
// 0级为建造,1级及以上为升级
|
||||||
|
const buttonTextKey = currentLevel === 0 ? 'buildingsView.build' : 'buildingsView.upgrade'
|
||||||
|
|
||||||
|
if (planet.value.buildQueue.length > 0) return t(buttonTextKey)
|
||||||
|
|
||||||
// 检查前置条件
|
// 检查前置条件
|
||||||
if (!checkUpgradeRequirements(buildingType)) {
|
if (!checkUpgradeRequirements(buildingType)) {
|
||||||
return t('buildingsView.requirementsNotMet')
|
return t('buildingsView.requirementsNotMet')
|
||||||
}
|
}
|
||||||
|
|
||||||
return t('buildingsView.upgrade')
|
return t(buttonTextKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取前置条件列表文本
|
// 获取前置条件列表
|
||||||
const getRequirementsList = (buildingType: BuildingType): string => {
|
const getRequirementsList = (
|
||||||
|
buildingType: BuildingType
|
||||||
|
): Array<{ name: string; requiredLevel: number; currentLevel: number; met: boolean }> => {
|
||||||
const config = BUILDINGS.value[buildingType]
|
const config = BUILDINGS.value[buildingType]
|
||||||
const currentLevel = getBuildingLevel(buildingType)
|
const currentLevel = getBuildingLevel(buildingType)
|
||||||
const targetLevel = currentLevel + 1
|
const targetLevel = currentLevel + 1
|
||||||
@@ -311,28 +330,59 @@
|
|||||||
// 获取目标等级的所有前置条件(包括等级门槛)
|
// 获取目标等级的所有前置条件(包括等级门槛)
|
||||||
const requirements = publicLogic.getLevelRequirements(config, targetLevel)
|
const requirements = publicLogic.getLevelRequirements(config, targetLevel)
|
||||||
|
|
||||||
if (!requirements || !planet.value) return ''
|
if (!requirements || !planet.value) return []
|
||||||
|
|
||||||
const lines: string[] = []
|
const items: Array<{ name: string; requiredLevel: number; currentLevel: number; met: boolean }> = []
|
||||||
for (const [key, requiredLevel] of Object.entries(requirements)) {
|
for (const [key, requiredLevel] of Object.entries(requirements)) {
|
||||||
// 检查是否为建筑类型
|
// 检查是否为建筑类型
|
||||||
if (Object.values(BuildingType).includes(key as BuildingType)) {
|
if (Object.values(BuildingType).includes(key as BuildingType)) {
|
||||||
const bt = key as BuildingType
|
const bt = key as BuildingType
|
||||||
const currentLevel = planet.value.buildings[bt] || 0
|
const currentLevel = planet.value.buildings[bt] || 0
|
||||||
const name = BUILDINGS.value[bt]?.name || bt
|
const name = BUILDINGS.value[bt]?.name || bt
|
||||||
const status = currentLevel >= requiredLevel ? '✓' : '✗'
|
items.push({ name, requiredLevel, currentLevel, met: currentLevel >= requiredLevel })
|
||||||
lines.push(`${status} ${name}: Lv ${requiredLevel} (${t('common.current')}: Lv ${currentLevel})`)
|
|
||||||
}
|
}
|
||||||
// 检查是否为科技类型
|
// 检查是否为科技类型
|
||||||
else if (Object.values(TechnologyType).includes(key as TechnologyType)) {
|
else if (Object.values(TechnologyType).includes(key as TechnologyType)) {
|
||||||
const tt = key as TechnologyType
|
const tt = key as TechnologyType
|
||||||
const currentLevel = gameStore.player.technologies[tt] || 0
|
const currentLevel = gameStore.player.technologies[tt] || 0
|
||||||
const name = TECHNOLOGIES.value[tt]?.name || tt
|
const name = TECHNOLOGIES.value[tt]?.name || tt
|
||||||
const status = currentLevel >= requiredLevel ? '✓' : '✗'
|
items.push({ name, requiredLevel, currentLevel, met: currentLevel >= requiredLevel })
|
||||||
lines.push(`${status} ${name}: Lv ${requiredLevel} (${t('common.current')}: Lv ${currentLevel})`)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return lines.join('\n')
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取前置条件显示(简化版,用于卡片内显示)
|
||||||
|
const getRequirementsDisplay = (buildingType: BuildingType): Array<{ name: string; level: number; met: boolean }> => {
|
||||||
|
if (!planet.value) return []
|
||||||
|
|
||||||
|
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 []
|
||||||
|
|
||||||
|
const items: Array<{ name: string; level: 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, level: requiredLevel, 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, level: requiredLevel, met: currentLevel >= requiredLevel })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否可以升级
|
// 检查是否可以升级
|
||||||
|
|||||||
@@ -5,8 +5,107 @@
|
|||||||
<h1 class="text-2xl sm:text-3xl font-bold">{{ t('diplomacy.title') }}</h1>
|
<h1 class="text-2xl sm:text-3xl font-bold">{{ t('diplomacy.title') }}</h1>
|
||||||
<p class="text-sm text-muted-foreground mt-1">{{ t('diplomacy.description') }}</p>
|
<p class="text-sm text-muted-foreground mt-1">{{ t('diplomacy.description') }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- NPC诊断按钮 -->
|
||||||
|
<Button @click="showNPCDiagnostic" variant="outline" size="sm">
|
||||||
|
<Search class="mr-2 h-4 w-4" />
|
||||||
|
NPC状态诊断
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- NPC诊断对话框 -->
|
||||||
|
<Dialog v-model:open="npcDiagnosticOpen">
|
||||||
|
<ScrollableDialogContent container-class="max-w-4xl">
|
||||||
|
<template #header>
|
||||||
|
<DialogTitle>NPC状态诊断报告</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
<div class="text-sm mt-2">
|
||||||
|
玩家积分: {{ gameStore.player.points || 0 }} | 侦查间隔: {{ Math.floor(behaviorConfig.spyInterval / 60) }}分钟 | 攻击间隔:
|
||||||
|
{{ Math.floor(behaviorConfig.attackInterval / 60) }}分钟 | 攻击概率:
|
||||||
|
{{ (behaviorConfig.attackProbability * 100).toFixed(0) }}%
|
||||||
|
</div>
|
||||||
|
</DialogDescription>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-if="npcDiagnostics.length === 0" class="text-center py-8 text-muted-foreground">暂无NPC数据</div>
|
||||||
|
<div v-else class="space-y-4">
|
||||||
|
<div v-for="diagnostic in npcDiagnostics" :key="diagnostic.npcId" class="border rounded-lg p-4">
|
||||||
|
<div class="flex items-center justify-between mb-3">
|
||||||
|
<h3 class="font-bold text-lg">{{ diagnostic.npcName }}</h3>
|
||||||
|
<Badge
|
||||||
|
:variant="
|
||||||
|
diagnostic.relationStatus === '友好' ? 'default' : diagnostic.relationStatus === '敌对' ? 'destructive' : 'secondary'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ diagnostic.relationStatus }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-2 text-sm mb-3">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-muted-foreground">难度:</span>
|
||||||
|
<span class="font-medium">{{ diagnostic.difficulty }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-muted-foreground">好感度:</span>
|
||||||
|
<span class="font-medium">{{ diagnostic.reputation }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-muted-foreground">侦查探测器:</span>
|
||||||
|
<span class="font-medium">{{ diagnostic.spyProbes }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-muted-foreground">舰队战力:</span>
|
||||||
|
<span class="font-medium">{{ diagnostic.totalFleetPower }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-muted-foreground">可以侦查:</span>
|
||||||
|
<span :class="diagnostic.canSpy ? 'text-green-600 font-semibold' : 'text-red-600 font-semibold'">
|
||||||
|
{{ diagnostic.canSpy ? '是' : '否' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-muted-foreground">可以攻击:</span>
|
||||||
|
<span :class="diagnostic.canAttack ? 'text-green-600 font-semibold' : 'text-red-600 font-semibold'">
|
||||||
|
{{ diagnostic.canAttack ? '是' : '否' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-muted-foreground">攻击概率:</span>
|
||||||
|
<span class="font-medium">{{ (diagnostic.attackProbability * 100).toFixed(0) }}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-muted-foreground">下次侦查:</span>
|
||||||
|
<span class="font-medium">
|
||||||
|
<template v-if="diagnostic.nextSpyIn > 0">
|
||||||
|
{{ Math.floor(diagnostic.nextSpyIn / 60) }}分{{ diagnostic.nextSpyIn % 60 }}秒
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span class="text-green-600">随时</span>
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-2 flex items-center gap-2">
|
||||||
|
<span class="text-muted-foreground">下次攻击:</span>
|
||||||
|
<span class="font-medium">
|
||||||
|
<template v-if="diagnostic.nextAttackIn > 0">
|
||||||
|
{{ Math.floor(diagnostic.nextAttackIn / 60) }}分{{ diagnostic.nextAttackIn % 60 }}秒
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span class="text-green-600">随时</span>
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="diagnostic.reasons.length > 0" class="mt-3 p-3 bg-muted rounded text-xs">
|
||||||
|
<div class="font-semibold mb-2">状态说明:</div>
|
||||||
|
<ul class="list-disc list-inside space-y-1">
|
||||||
|
<li v-for="(reason, idx) in diagnostic.reasons" :key="idx">{{ reason }}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ScrollableDialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
<!-- 关系状态过滤标签 -->
|
<!-- 关系状态过滤标签 -->
|
||||||
<Tabs v-model="activeTab" class="w-full">
|
<Tabs v-model="activeTab" class="w-full">
|
||||||
<TabsList class="grid w-full grid-cols-4">
|
<TabsList class="grid w-full grid-cols-4">
|
||||||
@@ -218,10 +317,15 @@
|
|||||||
import { useI18n } from '@/composables/useI18n'
|
import { useI18n } from '@/composables/useI18n'
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Dialog, DialogDescription, DialogTitle } from '@/components/ui/dialog'
|
||||||
|
import ScrollableDialogContent from '@/components/ui/dialog/ScrollableDialogContent.vue'
|
||||||
import { Pagination, PaginationContent, PaginationItem, PaginationNext, PaginationPrevious } from '@/components/ui/pagination'
|
import { Pagination, PaginationContent, PaginationItem, PaginationNext, PaginationPrevious } from '@/components/ui/pagination'
|
||||||
import NpcRelationCard from '@/components/NpcRelationCard.vue'
|
import NpcRelationCard from '@/components/NpcRelationCard.vue'
|
||||||
import { RelationStatus } from '@/types/game'
|
import { RelationStatus } from '@/types/game'
|
||||||
import type { DiplomaticRelation } from '@/types/game'
|
import type { DiplomaticRelation } from '@/types/game'
|
||||||
|
import * as npcBehaviorLogic from '@/logic/npcBehaviorLogic'
|
||||||
|
import { Search } from 'lucide-vue-next'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const npcStore = useNPCStore()
|
const npcStore = useNPCStore()
|
||||||
@@ -229,6 +333,19 @@
|
|||||||
|
|
||||||
const activeTab = ref('all')
|
const activeTab = ref('all')
|
||||||
|
|
||||||
|
// NPC诊断功能
|
||||||
|
const npcDiagnosticOpen = ref(false)
|
||||||
|
const npcDiagnostics = ref<npcBehaviorLogic.NPCDiagnosticInfo[]>([])
|
||||||
|
const behaviorConfig = computed(() => {
|
||||||
|
return npcBehaviorLogic.calculateDynamicBehavior(gameStore.player.points || 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
const showNPCDiagnostic = () => {
|
||||||
|
const currentTime = Date.now()
|
||||||
|
npcDiagnostics.value = npcBehaviorLogic.diagnoseNPCBehavior(npcStore.npcs, gameStore.player, currentTime)
|
||||||
|
npcDiagnosticOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
// 检测并生成NPC盟友
|
// 检测并生成NPC盟友
|
||||||
const initializeNPCAllies = () => {
|
const initializeNPCAllies = () => {
|
||||||
const npcs = npcStore.npcs
|
const npcs = npcStore.npcs
|
||||||
|
|||||||
@@ -156,7 +156,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3 sm:gap-4">
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label for="cargo-metal" class="text-xs sm:text-sm flex items-center gap-2">
|
<Label for="cargo-metal" class="text-xs sm:text-sm flex items-center gap-2">
|
||||||
<ResourceIcon type="metal" size="sm" />
|
<ResourceIcon type="metal" size="sm" />
|
||||||
@@ -192,6 +192,20 @@
|
|||||||
placeholder="0"
|
placeholder="0"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="cargo-darkMatter" class="text-xs sm:text-sm flex items-center gap-2">
|
||||||
|
<ResourceIcon type="darkMatter" size="sm" />
|
||||||
|
{{ t('resources.darkMatter') }} ({{ t('fleetView.available') }}: {{ formatNumber(planet.resources.darkMatter) }})
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="cargo-darkMatter"
|
||||||
|
v-model.number="cargo.darkMatter"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
:max="planet.resources.darkMatter"
|
||||||
|
placeholder="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs sm:text-sm text-muted-foreground mt-2">
|
<p class="text-xs sm:text-sm text-muted-foreground mt-2">
|
||||||
{{ t('fleetView.totalCargoCapacity') }}: {{ formatNumber(getTotalCargoCapacity()) }} | {{ t('fleetView.used') }}:
|
{{ t('fleetView.totalCargoCapacity') }}: {{ formatNumber(getTotalCargoCapacity()) }} | {{ t('fleetView.used') }}:
|
||||||
@@ -344,7 +358,7 @@
|
|||||||
import { useGameConfig } from '@/composables/useGameConfig'
|
import { useGameConfig } from '@/composables/useGameConfig'
|
||||||
import { computed, ref, onMounted, onUnmounted, watch } from 'vue'
|
import { computed, ref, onMounted, onUnmounted, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { ShipType, MissionType, BuildingType } from '@/types/game'
|
import { ShipType, MissionType, BuildingType, TechnologyType } from '@/types/game'
|
||||||
import type { Fleet, Resources } from '@/types/game'
|
import type { Fleet, Resources } from '@/types/game'
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||||
@@ -366,7 +380,7 @@
|
|||||||
AlertDialogTitle
|
AlertDialogTitle
|
||||||
} from '@/components/ui/alert-dialog'
|
} from '@/components/ui/alert-dialog'
|
||||||
import UnlockRequirement from '@/components/UnlockRequirement.vue'
|
import UnlockRequirement from '@/components/UnlockRequirement.vue'
|
||||||
import { Sword, Package, Rocket as RocketIcon, Eye, Users, Recycle, Skull, Gift } from 'lucide-vue-next'
|
import { Sword, Package, Rocket as RocketIcon, Eye, Users, Recycle, Skull, Gift, Compass } from 'lucide-vue-next'
|
||||||
import { formatNumber, formatTime } from '@/utils/format'
|
import { formatNumber, formatTime } from '@/utils/format'
|
||||||
import * as shipValidation from '@/logic/shipValidation'
|
import * as shipValidation from '@/logic/shipValidation'
|
||||||
import * as fleetLogic from '@/logic/fleetLogic'
|
import * as fleetLogic from '@/logic/fleetLogic'
|
||||||
@@ -397,7 +411,8 @@
|
|||||||
// 计算最大舰队任务槽位
|
// 计算最大舰队任务槽位
|
||||||
const maxFleetMissions = computed(() => {
|
const maxFleetMissions = computed(() => {
|
||||||
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, Date.now())
|
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, Date.now())
|
||||||
return publicLogic.getMaxFleetMissions(bonuses.additionalFleetSlots)
|
const computerTechLevel = gameStore.player.technologies[TechnologyType.ComputerTechnology] || 0
|
||||||
|
return publicLogic.getMaxFleetMissions(bonuses.additionalFleetSlots, computerTechLevel)
|
||||||
})
|
})
|
||||||
|
|
||||||
const activeTab = ref<'fleet' | 'send' | 'missions'>('fleet')
|
const activeTab = ref<'fleet' | 'send' | 'missions'>('fleet')
|
||||||
@@ -504,6 +519,7 @@
|
|||||||
{ type: MissionType.Colonize, name: t('fleetView.colonize'), icon: RocketIcon },
|
{ type: MissionType.Colonize, name: t('fleetView.colonize'), icon: RocketIcon },
|
||||||
{ type: MissionType.Spy, name: t('fleetView.spy'), icon: Eye },
|
{ type: MissionType.Spy, name: t('fleetView.spy'), icon: Eye },
|
||||||
{ type: MissionType.Deploy, name: t('fleetView.deploy'), icon: Users },
|
{ type: MissionType.Deploy, name: t('fleetView.deploy'), icon: Users },
|
||||||
|
{ type: MissionType.Expedition, name: t('fleetView.expedition'), icon: Compass },
|
||||||
{ type: MissionType.Recycle, name: t('fleetView.recycle'), icon: Recycle },
|
{ type: MissionType.Recycle, name: t('fleetView.recycle'), icon: Recycle },
|
||||||
{ type: MissionType.Destroy, name: t('fleetView.destroy'), icon: Skull }
|
{ type: MissionType.Destroy, name: t('fleetView.destroy'), icon: Skull }
|
||||||
])
|
])
|
||||||
@@ -616,7 +632,8 @@
|
|||||||
fleet,
|
fleet,
|
||||||
cargo,
|
cargo,
|
||||||
gameStore.player.officers,
|
gameStore.player.officers,
|
||||||
currentMissions
|
currentMissions,
|
||||||
|
gameStore.player.technologies
|
||||||
)
|
)
|
||||||
if (!validation.valid) return false
|
if (!validation.valid) return false
|
||||||
const shouldDeductCargo = missionType === MissionType.Transport
|
const shouldDeductCargo = missionType === MissionType.Transport
|
||||||
|
|||||||
@@ -331,6 +331,7 @@
|
|||||||
} from '@/components/ui/alert-dialog'
|
} from '@/components/ui/alert-dialog'
|
||||||
import { BuildingType, TechnologyType, ShipType, DefenseType, OfficerType } from '@/types/game'
|
import { BuildingType, TechnologyType, ShipType, DefenseType, OfficerType } from '@/types/game'
|
||||||
import * as npcBehaviorLogic from '@/logic/npcBehaviorLogic'
|
import * as npcBehaviorLogic from '@/logic/npcBehaviorLogic'
|
||||||
|
import * as publicLogic from '@/logic/publicLogic'
|
||||||
import { Home } from 'lucide-vue-next'
|
import { Home } from 'lucide-vue-next'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -340,6 +341,11 @@
|
|||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const { BUILDINGS, TECHNOLOGIES, SHIPS, DEFENSES, OFFICERS } = useGameConfig()
|
const { BUILDINGS, TECHNOLOGIES, SHIPS, DEFENSES, OFFICERS } = useGameConfig()
|
||||||
|
|
||||||
|
// 更新玩家积分的辅助函数
|
||||||
|
const updatePlayerPoints = () => {
|
||||||
|
gameStore.player.points = publicLogic.calculatePlayerPoints(gameStore.player)
|
||||||
|
}
|
||||||
|
|
||||||
const goHome = () => {
|
const goHome = () => {
|
||||||
router.push('/')
|
router.push('/')
|
||||||
}
|
}
|
||||||
@@ -407,22 +413,26 @@
|
|||||||
const setBuildingLevel = (building: BuildingType, level: number) => {
|
const setBuildingLevel = (building: BuildingType, level: number) => {
|
||||||
if (selectedPlanet.value) {
|
if (selectedPlanet.value) {
|
||||||
selectedPlanet.value.buildings[building] = level
|
selectedPlanet.value.buildings[building] = level
|
||||||
|
updatePlayerPoints()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const setTechnologyLevel = (tech: TechnologyType, level: number) => {
|
const setTechnologyLevel = (tech: TechnologyType, level: number) => {
|
||||||
gameStore.player.technologies[tech] = level
|
gameStore.player.technologies[tech] = level
|
||||||
|
updatePlayerPoints()
|
||||||
}
|
}
|
||||||
|
|
||||||
const setShipCount = (ship: ShipType, count: number) => {
|
const setShipCount = (ship: ShipType, count: number) => {
|
||||||
if (selectedPlanet.value) {
|
if (selectedPlanet.value) {
|
||||||
selectedPlanet.value.fleet[ship] = (selectedPlanet.value.fleet[ship] || 0) + count
|
selectedPlanet.value.fleet[ship] = (selectedPlanet.value.fleet[ship] || 0) + count
|
||||||
|
updatePlayerPoints()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const setDefenseCount = (defense: DefenseType, count: number) => {
|
const setDefenseCount = (defense: DefenseType, count: number) => {
|
||||||
if (selectedPlanet.value) {
|
if (selectedPlanet.value) {
|
||||||
selectedPlanet.value.defense[defense] = (selectedPlanet.value.defense[defense] || 0) + count
|
selectedPlanet.value.defense[defense] = (selectedPlanet.value.defense[defense] || 0) + count
|
||||||
|
updatePlayerPoints()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -614,7 +624,7 @@
|
|||||||
const maxAllResources = () => {
|
const maxAllResources = () => {
|
||||||
if (!selectedPlanet.value) return
|
if (!selectedPlanet.value) return
|
||||||
|
|
||||||
const maxAmount = 1000000000 // 10亿
|
const maxAmount = 1000000000000000000
|
||||||
selectedPlanet.value.resources.metal = maxAmount
|
selectedPlanet.value.resources.metal = maxAmount
|
||||||
selectedPlanet.value.resources.crystal = maxAmount
|
selectedPlanet.value.resources.crystal = maxAmount
|
||||||
selectedPlanet.value.resources.deuterium = maxAmount
|
selectedPlanet.value.resources.deuterium = maxAmount
|
||||||
@@ -708,6 +718,9 @@
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 更新玩家积分(因为建筑/科技/舰队/防御可能已改变)
|
||||||
|
updatePlayerPoints()
|
||||||
|
|
||||||
toast.success(
|
toast.success(
|
||||||
t('gmView.completeQueuesSuccess', {
|
t('gmView.completeQueuesSuccess', {
|
||||||
buildingCount,
|
buildingCount,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
<!-- 标签切换 -->
|
<!-- 标签切换 -->
|
||||||
<Tabs v-model="activeTab" class="w-full">
|
<Tabs v-model="activeTab" class="w-full">
|
||||||
<TabsList class="grid w-full grid-cols-2 sm:grid-cols-4">
|
<TabsList class="grid w-full grid-cols-2 sm:grid-cols-4" :tab-count="4">
|
||||||
<TabsTrigger v-for="tab in tabs" :key="tab.value" :value="tab.value" class="flex items-center justify-center gap-1 px-2">
|
<TabsTrigger v-for="tab in tabs" :key="tab.value" :value="tab.value" class="flex items-center justify-center gap-1 px-2">
|
||||||
<component :is="tab.icon" class="h-3 w-3 sm:h-4 sm:w-4" />
|
<component :is="tab.icon" class="h-3 w-3 sm:h-4 sm:w-4" />
|
||||||
<span class="text-xs sm:text-sm truncate">{{ tab.label }}</span>
|
<span class="text-xs sm:text-sm truncate">{{ tab.label }}</span>
|
||||||
@@ -555,7 +555,7 @@
|
|||||||
import BattleReportDialog from '@/components/BattleReportDialog.vue'
|
import BattleReportDialog from '@/components/BattleReportDialog.vue'
|
||||||
import SpyReportDialog from '@/components/SpyReportDialog.vue'
|
import SpyReportDialog from '@/components/SpyReportDialog.vue'
|
||||||
import { formatDate } from '@/utils/format'
|
import { formatDate } from '@/utils/format'
|
||||||
import { X, Sword, Eye, AlertTriangle, Package, Recycle, Gift, Ban, Check, Users, Skull, Globe } from 'lucide-vue-next'
|
import { X, Sword, Eye, AlertTriangle, Package, Recycle, Gift, Ban, Check, Users, Skull, Globe, Compass } from 'lucide-vue-next'
|
||||||
import type {
|
import type {
|
||||||
BattleResult,
|
BattleResult,
|
||||||
SpyReport,
|
SpyReport,
|
||||||
@@ -837,6 +837,7 @@
|
|||||||
[MissionType.Transport]: t('fleetView.transport'),
|
[MissionType.Transport]: t('fleetView.transport'),
|
||||||
[MissionType.Colonize]: t('fleetView.colonize'),
|
[MissionType.Colonize]: t('fleetView.colonize'),
|
||||||
[MissionType.Deploy]: t('fleetView.deploy'),
|
[MissionType.Deploy]: t('fleetView.deploy'),
|
||||||
|
[MissionType.Expedition]: t('fleetView.expedition'),
|
||||||
[MissionType.Recycle]: t('fleetView.recycle'),
|
[MissionType.Recycle]: t('fleetView.recycle'),
|
||||||
[MissionType.Destroy]: t('fleetView.destroy'),
|
[MissionType.Destroy]: t('fleetView.destroy'),
|
||||||
[MissionType.MissileAttack]: t('galaxyView.missileAttack')
|
[MissionType.MissileAttack]: t('galaxyView.missileAttack')
|
||||||
@@ -963,6 +964,8 @@
|
|||||||
return Recycle
|
return Recycle
|
||||||
case MissionType.Colonize:
|
case MissionType.Colonize:
|
||||||
return Globe
|
return Globe
|
||||||
|
case MissionType.Expedition:
|
||||||
|
return Compass
|
||||||
case MissionType.Destroy:
|
case MissionType.Destroy:
|
||||||
return Skull
|
return Skull
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -65,9 +65,18 @@
|
|||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>{{ alertDialogTitle }}</AlertDialogTitle>
|
<AlertDialogTitle>{{ alertDialogTitle }}</AlertDialogTitle>
|
||||||
<AlertDialogDescription class="whitespace-pre-line">
|
<AlertDialogDescription v-if="!alertDialogShowRequirements" class="whitespace-pre-line">
|
||||||
{{ alertDialogMessage }}
|
{{ alertDialogMessage }}
|
||||||
</AlertDialogDescription>
|
</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>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogAction>{{ t('common.confirm') }}</AlertDialogAction>
|
<AlertDialogAction>{{ t('common.confirm') }}</AlertDialogAction>
|
||||||
@@ -99,6 +108,7 @@
|
|||||||
AlertDialogTitle
|
AlertDialogTitle
|
||||||
} from '@/components/ui/alert-dialog'
|
} from '@/components/ui/alert-dialog'
|
||||||
import CardUnlockOverlay from '@/components/CardUnlockOverlay.vue'
|
import CardUnlockOverlay from '@/components/CardUnlockOverlay.vue'
|
||||||
|
import { Check, X } from 'lucide-vue-next'
|
||||||
import { formatNumber, getResourceCostColor } from '@/utils/format'
|
import { formatNumber, getResourceCostColor } from '@/utils/format'
|
||||||
import * as publicLogic from '@/logic/publicLogic'
|
import * as publicLogic from '@/logic/publicLogic'
|
||||||
import * as researchLogic from '@/logic/researchLogic'
|
import * as researchLogic from '@/logic/researchLogic'
|
||||||
@@ -115,6 +125,8 @@
|
|||||||
const alertDialogOpen = ref(false)
|
const alertDialogOpen = ref(false)
|
||||||
const alertDialogTitle = ref('')
|
const alertDialogTitle = ref('')
|
||||||
const alertDialogMessage = ref('')
|
const alertDialogMessage = ref('')
|
||||||
|
const alertDialogRequirements = ref<Array<{ name: string; requiredLevel: number; currentLevel: number; met: boolean }>>([])
|
||||||
|
const alertDialogShowRequirements = ref(false)
|
||||||
|
|
||||||
// 资源类型配置(用于成本显示)
|
// 资源类型配置(用于成本显示)
|
||||||
const costResourceTypes = [
|
const costResourceTypes = [
|
||||||
@@ -185,8 +197,10 @@
|
|||||||
return t('researchView.research') // "研究"
|
return t('researchView.research') // "研究"
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取前置条件列表文本
|
// 获取前置条件列表
|
||||||
const getRequirementsList = (techType: TechnologyType): string => {
|
const getRequirementsList = (
|
||||||
|
techType: TechnologyType
|
||||||
|
): Array<{ name: string; requiredLevel: number; currentLevel: number; met: boolean }> => {
|
||||||
const config = TECHNOLOGIES.value[techType]
|
const config = TECHNOLOGIES.value[techType]
|
||||||
const currentLevel = getTechLevel(techType)
|
const currentLevel = getTechLevel(techType)
|
||||||
const targetLevel = currentLevel + 1
|
const targetLevel = currentLevel + 1
|
||||||
@@ -194,28 +208,59 @@
|
|||||||
// 获取目标等级的所有前置条件(包括等级门槛)
|
// 获取目标等级的所有前置条件(包括等级门槛)
|
||||||
const requirements = publicLogic.getLevelRequirements(config, targetLevel)
|
const requirements = publicLogic.getLevelRequirements(config, targetLevel)
|
||||||
|
|
||||||
if (!requirements || !planet.value) return ''
|
if (!requirements || !planet.value) return []
|
||||||
|
|
||||||
const lines: string[] = []
|
const items: Array<{ name: string; requiredLevel: number; currentLevel: number; met: boolean }> = []
|
||||||
for (const [key, requiredLevel] of Object.entries(requirements)) {
|
for (const [key, requiredLevel] of Object.entries(requirements)) {
|
||||||
// 检查是否为建筑类型
|
// 检查是否为建筑类型
|
||||||
if (Object.values(BuildingType).includes(key as BuildingType)) {
|
if (Object.values(BuildingType).includes(key as BuildingType)) {
|
||||||
const bt = key as BuildingType
|
const bt = key as BuildingType
|
||||||
const currentLevel = planet.value.buildings[bt] || 0
|
const currentLevel = planet.value.buildings[bt] || 0
|
||||||
const name = BUILDINGS.value[bt]?.name || bt
|
const name = BUILDINGS.value[bt]?.name || bt
|
||||||
const status = currentLevel >= requiredLevel ? '✓' : '✗'
|
items.push({ name, requiredLevel, currentLevel, met: currentLevel >= requiredLevel })
|
||||||
lines.push(`${status} ${name}: Lv ${requiredLevel} (${t('common.current')}: Lv ${currentLevel})`)
|
|
||||||
}
|
}
|
||||||
// 检查是否为科技类型
|
// 检查是否为科技类型
|
||||||
else if (Object.values(TechnologyType).includes(key as TechnologyType)) {
|
else if (Object.values(TechnologyType).includes(key as TechnologyType)) {
|
||||||
const tt = key as TechnologyType
|
const tt = key as TechnologyType
|
||||||
const currentLevel = gameStore.player.technologies[tt] || 0
|
const currentLevel = gameStore.player.technologies[tt] || 0
|
||||||
const name = TECHNOLOGIES.value[tt]?.name || tt
|
const name = TECHNOLOGIES.value[tt]?.name || tt
|
||||||
const status = currentLevel >= requiredLevel ? '✓' : '✗'
|
items.push({ name, requiredLevel, currentLevel, met: currentLevel >= requiredLevel })
|
||||||
lines.push(`${status} ${name}: Lv ${requiredLevel} (${t('common.current')}: Lv ${currentLevel})`)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return lines.join('\n')
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取前置条件显示(简化版,用于卡片内显示)
|
||||||
|
const getRequirementsDisplay = (techType: TechnologyType): Array<{ name: string; level: number; met: boolean }> => {
|
||||||
|
if (!planet.value) return []
|
||||||
|
|
||||||
|
const config = TECHNOLOGIES.value[techType]
|
||||||
|
const currentLevel = getTechLevel(techType)
|
||||||
|
const targetLevel = currentLevel + 1
|
||||||
|
|
||||||
|
// 获取目标等级的所有前置条件(包括等级门槛)
|
||||||
|
const requirements = publicLogic.getLevelRequirements(config, targetLevel)
|
||||||
|
|
||||||
|
if (!requirements || Object.keys(requirements).length === 0) return []
|
||||||
|
|
||||||
|
const items: Array<{ name: string; level: 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, level: requiredLevel, 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, level: requiredLevel, met: currentLevel >= requiredLevel })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
// 研究科技
|
// 研究科技
|
||||||
@@ -223,7 +268,9 @@
|
|||||||
// 检查前置条件
|
// 检查前置条件
|
||||||
if (!checkUpgradeRequirements(techType)) {
|
if (!checkUpgradeRequirements(techType)) {
|
||||||
alertDialogTitle.value = t('common.requirementsNotMet')
|
alertDialogTitle.value = t('common.requirementsNotMet')
|
||||||
alertDialogMessage.value = getRequirementsList(techType)
|
alertDialogRequirements.value = getRequirementsList(techType)
|
||||||
|
alertDialogShowRequirements.value = true
|
||||||
|
alertDialogMessage.value = ''
|
||||||
alertDialogOpen.value = true
|
alertDialogOpen.value = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -232,6 +279,7 @@
|
|||||||
if (!success) {
|
if (!success) {
|
||||||
alertDialogTitle.value = t('researchView.researchFailed')
|
alertDialogTitle.value = t('researchView.researchFailed')
|
||||||
alertDialogMessage.value = t('researchView.researchFailedMessage')
|
alertDialogMessage.value = t('researchView.researchFailedMessage')
|
||||||
|
alertDialogShowRequirements.value = false
|
||||||
alertDialogOpen.value = true
|
alertDialogOpen.value = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -253,6 +301,12 @@
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查队列中是否已存在该科技的研究任务
|
||||||
|
const existingQueueItem = player.value.researchQueue.find(item => item.type === 'technology' && item.itemType === techType)
|
||||||
|
if (existingQueueItem) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// 检查研究队列是否已满
|
// 检查研究队列是否已满
|
||||||
const maxQueue = publicLogic.getMaxResearchQueue(gameStore.player.technologies)
|
const maxQueue = publicLogic.getMaxResearchQueue(gameStore.player.technologies)
|
||||||
if (player.value.researchQueue.length >= maxQueue) {
|
if (player.value.researchQueue.length >= maxQueue) {
|
||||||
|
|||||||
Reference in New Issue
Block a user