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