refactor: 优化UI组件结构与积分系统

重构部分UI组件脚本结构,统一导入风格,提升可维护性。CardUnlockOverlay解锁条件弹窗改为列表展示,提升可读性。修复QueueNotifications滚动区域高度。ScrollableDialogContent增加最大高度。StarsBackground与ParticlesBg组件代码格式优化。App.vue引入玩家积分定时更新逻辑,NPC成长系统补充间谍探测器修复。
This commit is contained in:
谦君
2025-12-18 03:47:38 +08:00
parent 2e3ac1231f
commit 2ed15c4782
42 changed files with 1342 additions and 749 deletions

View File

@@ -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)
// 移除队列取消事件监听 // 移除队列取消事件监听

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -1 +1 @@
export { default as StarsBackground } from "./StarsBackground.vue"; export { default as StarsBackground } from './StarsBackground.vue'

View File

@@ -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>

View File

@@ -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>

View File

@@ -1 +1 @@
export { default as ParticlesBg } from "./ParticlesBg.vue"; export { default as ParticlesBg } from './ParticlesBg.vue'

View File

@@ -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>

View File

@@ -1 +1 @@
export { default as Separator } from "./Separator.vue" export { default as Separator } from './Separator.vue'

View File

@@ -1,19 +1,15 @@
<script setup lang="ts">
import type { DialogRootEmits, DialogRootProps } from "reka-ui"
import { DialogRoot, useForwardPropsEmits } from "reka-ui"
const props = defineProps<DialogRootProps>()
const emits = defineEmits<DialogRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template> <template>
<DialogRoot <DialogRoot v-slot="slotProps" data-slot="sheet" v-bind="forwarded">
v-slot="slotProps"
data-slot="sheet"
v-bind="forwarded"
>
<slot v-bind="slotProps" /> <slot v-bind="slotProps" />
</DialogRoot> </DialogRoot>
</template> </template>
<script setup lang="ts">
import type { DialogRootEmits, DialogRootProps } from 'reka-ui'
import { DialogRoot, useForwardPropsEmits } from 'reka-ui'
const props = defineProps<DialogRootProps>()
const emits = defineEmits<DialogRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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'

View File

@@ -1,17 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
interface SkeletonProps {
class?: HTMLAttributes["class"]
}
const props = defineProps<SkeletonProps>()
</script>
<template> <template>
<div <div data-slot="skeleton" :class="cn('animate-pulse rounded-md bg-primary/10', props.class)" />
data-slot="skeleton"
:class="cn('animate-pulse rounded-md bg-primary/10', props.class)"
/>
</template> </template>
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
interface SkeletonProps {
class?: HTMLAttributes['class']
}
const props = defineProps<SkeletonProps>()
</script>

View File

@@ -1 +1 @@
export { default as Skeleton } from "./Skeleton.vue" export { default as Skeleton } from './Skeleton.vue'

View File

@@ -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,

View File

@@ -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',

View File

@@ -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',

View File

@@ -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: '総積載量',

View File

@@ -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: '총 적재량',

View File

@@ -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: 'Общая грузоподъёмность',

View File

@@ -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: '运输资源',

View File

@@ -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: '總載貨量',

View File

@@ -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
}
})
}

View File

@@ -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}`)
}
})
} }
/** /**

View File

@@ -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)
} }

View File

@@ -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
} }
} }

View File

@@ -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' }
} }

View File

@@ -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)
} }
} }

View File

@@ -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
} }
// 检查是否可以升级 // 检查是否可以升级

View File

@@ -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

View File

@@ -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

View File

@@ -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,

View File

@@ -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:

View File

@@ -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) {