feat: 新增战报弹窗与舰队模拟器,重构UI组件

新增 BattleReportDialog、SpyReportDialog、NumberWithTooltip 等组件,完善舰队模拟器功能。重构并引入 Sheet、Sidebar、Tooltip、Skeleton 等 UI 组件,优化界面结构。实现 battle.worker 支持战斗计算,增加 universeStore、fleetStorageLogic 等核心逻辑,完善多语言与类型定义。
This commit is contained in:
谦君
2025-12-13 11:14:23 +08:00
parent 8637e50115
commit 731d79673b
160 changed files with 6302 additions and 1931 deletions

View File

@@ -0,0 +1,470 @@
/**
* 战斗模拟 Worker
* 在独立线程中处理计算密集型的战斗模拟任务
*/
import type { Fleet, Resources } from '@/types/game'
import { ShipType, DefenseType } from '@/types/game'
import { SHIPS, DEFENSES } from '@/config/gameConfig'
import type { WorkerRequestMessage, WorkerResponseMessage, BattleSideData, BattleSimulationResult } from '@/types/worker'
import { WorkerMessageType } from '@/types/worker'
// 战斗单位接口
interface CombatUnit {
type: ShipType | DefenseType
count: number
attack: number
shield: number
armor: number
rapidFire?: Record<string, number>
}
/**
* 计算科技加成后的数值
*/
const applyTechBonus = (baseValue: number, techLevel: number = 0, bonusPerLevel: number = 0.1): number => {
return Math.floor(baseValue * (1 + techLevel * bonusPerLevel))
}
/**
* 将舰队和防御转换为战斗单位数组
*/
const prepareCombatUnits = (side: BattleSideData, isDefender: boolean = false): CombatUnit[] => {
const units: CombatUnit[] = []
// 处理舰船
if (side.ships) {
for (const [shipType, count] of Object.entries(side.ships)) {
if (count > 0) {
const config = SHIPS[shipType as ShipType]
units.push({
type: shipType as ShipType,
count: count,
attack: applyTechBonus(config.attack, side.weaponTech),
shield: applyTechBonus(config.shield, side.shieldTech),
armor: applyTechBonus(config.armor, side.armorTech)
})
}
}
}
// 处理防御设施(仅防守方)
if (isDefender && side.defense) {
for (const [defenseType, count] of Object.entries(side.defense)) {
if (count > 0) {
const config = DEFENSES[defenseType as DefenseType]
units.push({
type: defenseType as DefenseType,
count: count,
attack: applyTechBonus(config.attack, side.weaponTech),
shield: applyTechBonus(config.shield, side.shieldTech),
armor: applyTechBonus(config.armor, side.armorTech)
})
}
}
}
return units
}
/**
* 计算一个单位对另一个单位造成的伤害
*/
const calculateDamage = (attacker: CombatUnit, defender: CombatUnit): { destroyed: number; damagedShield: number } => {
const attackPower = attacker.attack
const defenderShield = defender.shield
const defenderArmor = defender.armor
let destroyed = 0
let damagedShield = 0
// 如果攻击力小于护盾的1%,有很大概率无法击穿护盾
if (attackPower < defenderShield * 0.01) {
if (Math.random() > 0.01) {
return { destroyed: 0, damagedShield: 0 }
}
}
// 计算伤害
let remainingDamage = attackPower
// 先消耗护盾
if (remainingDamage > defenderShield) {
remainingDamage -= defenderShield
damagedShield = defenderShield
} else {
damagedShield = remainingDamage
return { destroyed: 0, damagedShield }
}
// 再消耗装甲
if (remainingDamage > defenderArmor) {
destroyed = 1
} else {
// 有概率摧毁
const destroyChance = remainingDamage / defenderArmor
if (Math.random() < destroyChance) {
destroyed = 1
}
}
return { destroyed, damagedShield }
}
/**
* 执行一轮战斗
*/
const executeRound = (
attackerUnits: CombatUnit[],
defenderUnits: CombatUnit[]
): {
attackerLosses: Partial<Fleet>
defenderLosses: {
fleet: Partial<Fleet>
defense: Partial<Record<DefenseType, number>>
}
attackerRemainingPower: number
defenderRemainingPower: number
} => {
const attackerLosses: Partial<Fleet> = {}
const defenderShipLosses: Partial<Fleet> = {}
const defenderDefenseLosses: Partial<Record<DefenseType, number>> = {}
// 攻击方向防守方射击
for (const attacker of attackerUnits) {
for (let i = 0; i < attacker.count; i++) {
if (defenderUnits.length === 0) break
const targetIndex = Math.floor(Math.random() * defenderUnits.length)
const target = defenderUnits[targetIndex]
if (!target) continue
const { destroyed } = calculateDamage(attacker, target)
if (destroyed > 0) {
target.count -= destroyed
// 记录损失
if (Object.values(ShipType).includes(target.type as ShipType)) {
const shipType = target.type as ShipType
defenderShipLosses[shipType] = (defenderShipLosses[shipType] || 0) + destroyed
} else {
const defenseType = target.type as DefenseType
defenderDefenseLosses[defenseType] = (defenderDefenseLosses[defenseType] || 0) + destroyed
}
if (target.count <= 0) {
defenderUnits.splice(targetIndex, 1)
}
}
}
}
// 防守方向攻击方射击
for (const defender of defenderUnits) {
for (let i = 0; i < defender.count; i++) {
if (attackerUnits.length === 0) break
const targetIndex = Math.floor(Math.random() * attackerUnits.length)
const target = attackerUnits[targetIndex]
if (!target) continue
const { destroyed } = calculateDamage(defender, target)
if (destroyed > 0) {
target.count -= destroyed
const shipType = target.type as ShipType
attackerLosses[shipType] = (attackerLosses[shipType] || 0) + destroyed
if (target.count <= 0) {
attackerUnits.splice(targetIndex, 1)
}
}
}
}
// 计算剩余战斗力
const attackerPower = attackerUnits.reduce((sum, unit) => sum + unit.count * unit.attack, 0)
const defenderPower = defenderUnits.reduce((sum, unit) => sum + unit.count * unit.attack, 0)
return {
attackerLosses,
defenderLosses: {
fleet: defenderShipLosses,
defense: defenderDefenseLosses
},
attackerRemainingPower: attackerPower,
defenderRemainingPower: defenderPower
}
}
/**
* 模拟完整战斗
*/
const simulateBattle = (attacker: BattleSideData, defender: BattleSideData, maxRounds: number = 6): BattleSimulationResult => {
// 准备战斗单位
let attackerUnits = prepareCombatUnits(attacker, false)
let defenderUnits = prepareCombatUnits(defender, true)
const totalAttackerLosses: Partial<Fleet> = {}
const totalDefenderShipLosses: Partial<Fleet> = {}
const totalDefenderDefenseLosses: Partial<Record<DefenseType, number>> = {}
const roundDetails: Array<{
round: number
attackerLosses: Partial<Fleet>
defenderLosses: {
fleet: Partial<Fleet>
defense: Partial<Record<DefenseType, number>>
}
attackerRemainingPower: number
defenderRemainingPower: number
}> = []
let rounds = 0
// 执行最多maxRounds轮战斗
for (let round = 0; round < maxRounds; round++) {
if (attackerUnits.length === 0 || defenderUnits.length === 0) {
break
}
rounds++
const roundResult = executeRound(attackerUnits, defenderUnits)
// 保存当前回合详情
roundDetails.push({
round: rounds,
attackerLosses: { ...roundResult.attackerLosses },
defenderLosses: {
fleet: { ...roundResult.defenderLosses.fleet },
defense: { ...roundResult.defenderLosses.defense }
},
attackerRemainingPower: roundResult.attackerRemainingPower,
defenderRemainingPower: roundResult.defenderRemainingPower
})
// 累计损失
for (const [shipType, count] of Object.entries(roundResult.attackerLosses)) {
totalAttackerLosses[shipType as ShipType] = (totalAttackerLosses[shipType as ShipType] || 0) + count
}
for (const [shipType, count] of Object.entries(roundResult.defenderLosses.fleet)) {
totalDefenderShipLosses[shipType as ShipType] = (totalDefenderShipLosses[shipType as ShipType] || 0) + count
}
for (const [defenseType, count] of Object.entries(roundResult.defenderLosses.defense)) {
totalDefenderDefenseLosses[defenseType as DefenseType] = (totalDefenderDefenseLosses[defenseType as DefenseType] || 0) + count
}
}
// 防御设施有概率修复70%概率)
const repairedDefense: Partial<Record<DefenseType, number>> = {}
for (const [defenseType, count] of Object.entries(totalDefenderDefenseLosses)) {
const repaired = Math.floor(count * 0.7)
if (repaired > 0) {
repairedDefense[defenseType as DefenseType] = repaired
totalDefenderDefenseLosses[defenseType as DefenseType] = count - repaired
}
}
// 计算剩余单位
const attackerRemaining: Partial<Fleet> = {}
for (const unit of attackerUnits) {
if (unit.count > 0) {
attackerRemaining[unit.type as ShipType] = unit.count
}
}
const defenderShipRemaining: Partial<Fleet> = {}
const defenderDefenseRemaining: Partial<Record<DefenseType, number>> = {}
for (const unit of defenderUnits) {
if (unit.count > 0) {
if (Object.values(ShipType).includes(unit.type as ShipType)) {
defenderShipRemaining[unit.type as ShipType] = unit.count
} else {
defenderDefenseRemaining[unit.type as DefenseType] = unit.count
}
}
}
// 添加修复的防御设施
for (const [defenseType, count] of Object.entries(repairedDefense)) {
defenderDefenseRemaining[defenseType as DefenseType] = (defenderDefenseRemaining[defenseType as DefenseType] || 0) + count
}
// 判断胜负
let winner: 'attacker' | 'defender' | 'draw'
if (attackerUnits.length === 0 && defenderUnits.length === 0) {
winner = 'draw'
} else if (attackerUnits.length === 0) {
winner = 'defender'
} else if (defenderUnits.length === 0) {
winner = 'attacker'
} else {
winner = 'draw'
}
return {
winner,
rounds,
attackerLosses: totalAttackerLosses,
defenderLosses: {
fleet: totalDefenderShipLosses,
defense: totalDefenderDefenseLosses
},
attackerRemaining,
defenderRemaining: {
fleet: defenderShipRemaining,
defense: defenderDefenseRemaining
},
roundDetails
}
}
/**
* 计算掠夺的资源
*/
const calculatePlunder = (defenderResources: Resources, attackerFleet: Partial<Fleet>): Resources => {
let totalCapacity = 0
for (const [shipType, count] of Object.entries(attackerFleet)) {
const config = SHIPS[shipType as ShipType]
totalCapacity += config.cargoCapacity * count
}
const availableResources = {
metal: Math.floor(defenderResources.metal * 0.5),
crystal: Math.floor(defenderResources.crystal * 0.5),
deuterium: Math.floor(defenderResources.deuterium * 0.5),
darkMatter: Math.floor(defenderResources.darkMatter * 0.5),
energy: 0
}
const totalAvailable =
availableResources.metal + availableResources.crystal + availableResources.deuterium + availableResources.darkMatter
if (totalCapacity >= totalAvailable) {
return availableResources
}
const ratio = totalCapacity / totalAvailable
return {
metal: Math.floor(availableResources.metal * ratio),
crystal: Math.floor(availableResources.crystal * ratio),
deuterium: Math.floor(availableResources.deuterium * ratio),
darkMatter: Math.floor(availableResources.darkMatter * ratio),
energy: 0
}
}
/**
* 计算残骸场
*/
const calculateDebrisField = (
attackerLosses: Partial<Fleet>,
defenderLosses: {
fleet: Partial<Fleet>
defense: Partial<Record<DefenseType, number>>
}
): Resources => {
let totalMetal = 0
let totalCrystal = 0
for (const [shipType, count] of Object.entries(attackerLosses)) {
const config = SHIPS[shipType as ShipType]
totalMetal += config.cost.metal * count * 0.3
totalCrystal += config.cost.crystal * count * 0.3
}
for (const [shipType, count] of Object.entries(defenderLosses.fleet)) {
const config = SHIPS[shipType as ShipType]
totalMetal += config.cost.metal * count * 0.3
totalCrystal += config.cost.crystal * count * 0.3
}
for (const [defenseType, count] of Object.entries(defenderLosses.defense)) {
const config = DEFENSES[defenseType as DefenseType]
totalMetal += config.cost.metal * count * 0.3
totalCrystal += config.cost.crystal * count * 0.3
}
return {
metal: Math.floor(totalMetal),
crystal: Math.floor(totalCrystal),
deuterium: 0,
darkMatter: 0,
energy: 0
}
}
// ============================================================================
// Worker 消息处理
// ============================================================================
self.onmessage = (event: MessageEvent<WorkerRequestMessage>) => {
const { id, type, payload } = event.data
try {
let result: unknown
switch (type) {
case WorkerMessageType.SIMULATE_BATTLE: {
const {
attacker,
defender,
maxRounds = 6
} = payload as {
attacker: BattleSideData
defender: BattleSideData
maxRounds?: number
}
result = simulateBattle(attacker, defender, maxRounds)
break
}
case WorkerMessageType.CALCULATE_PLUNDER: {
const { defenderResources, attackerFleet } = payload as {
defenderResources: Resources
attackerFleet: Partial<Fleet>
}
result = calculatePlunder(defenderResources, attackerFleet)
break
}
case WorkerMessageType.CALCULATE_DEBRIS: {
const { attackerLosses, defenderLosses } = payload as {
attackerLosses: Partial<Fleet>
defenderLosses: {
fleet: Partial<Fleet>
defense: Partial<Record<DefenseType, number>>
}
}
result = calculateDebrisField(attackerLosses, defenderLosses)
break
}
default:
throw new Error(`Unknown message type: ${type}`)
}
// 发送成功响应
const response: WorkerResponseMessage = {
id,
type: WorkerMessageType.SUCCESS,
success: true,
data: result
}
self.postMessage(response)
} catch (error) {
// 发送错误响应
const response: WorkerResponseMessage = {
id,
type: WorkerMessageType.ERROR,
success: false,
error: error instanceof Error ? error.message : String(error)
}
self.postMessage(response)
}
}

View File

@@ -0,0 +1,234 @@
/**
* Worker 管理器
* 统一管理所有 Worker 的创建、通信和销毁
*/
import type { WorkerRequestMessage, WorkerResponseMessage, WorkerMessageType } from '@/types/worker'
import { WorkerMessageType as MsgType } from '@/types/worker'
import { toRaw } from 'vue'
import BattleWorker from './battle.worker?worker'
/**
* Worker 任务接口
*/
interface WorkerTask {
resolve: (data: unknown) => void
reject: (error: Error) => void
timeout?: ReturnType<typeof setTimeout>
}
/**
* 将 Vue 响应式对象转换为普通对象
* 使用 toRaw() 获取原始对象,避免 Proxy 无法被 structured clone
*/
const toPlainObject = <T>(obj: T): T => {
if (obj === null || obj === undefined) return obj
if (typeof obj !== 'object') return obj
// 使用 toRaw 获取 Vue 响应式对象的原始值
const raw = toRaw(obj)
// 对于数组,递归处理每个元素
if (Array.isArray(raw)) {
return raw.map(item => toPlainObject(item)) as unknown as T
}
// 对于对象,递归处理每个属性
if (raw && typeof raw === 'object') {
const plain: any = {}
for (const key in raw) {
if (Object.prototype.hasOwnProperty.call(raw, key)) {
plain[key] = toPlainObject(raw[key])
}
}
return plain
}
return raw
}
/**
* Worker 管理类
*/
class WorkerManager {
private battleWorker: Worker | null = null
private pendingTasks: Map<string, WorkerTask> = new Map()
private messageIdCounter = 0
private readonly defaultTimeout = 10000 // 30秒超时
/**
* 初始化战斗 Worker
*/
private initBattleWorker(): void {
if (this.battleWorker) return
this.battleWorker = new BattleWorker()
this.setupWorkerHandlers(this.battleWorker, 'Battle')
}
/**
* 设置 Worker 消息处理器
*/
private setupWorkerHandlers(worker: Worker, workerName: string): void {
worker.onmessage = (event: MessageEvent<WorkerResponseMessage>) => {
const { id, success, data, error } = event.data
const task = this.pendingTasks.get(id)
if (!task) {
console.warn(`[WorkerManager] No pending task found for message ID: ${id}`)
return
}
// 清除超时定时器
if (task.timeout) {
clearTimeout(task.timeout)
}
// 移除任务
this.pendingTasks.delete(id)
// 处理响应
if (success) {
task.resolve(data)
} else {
task.reject(new Error(error || 'Worker task failed'))
}
}
worker.onerror = (error: ErrorEvent) => {
console.error(`[WorkerManager] ${workerName} worker error:`, error)
// 拒绝所有待处理的任务
for (const task of this.pendingTasks.values()) {
if (task.timeout) clearTimeout(task.timeout)
task.reject(new Error(`${workerName} worker crashed`))
}
this.pendingTasks.clear()
// 清除对应的 worker 引用
if (workerName === 'Battle') {
this.battleWorker = null
}
}
}
/**
* 生成唯一的消息 ID
*/
private generateMessageId(): string {
return `msg_${Date.now()}_${++this.messageIdCounter}`
}
/**
* 根据消息类型获取对应的 Worker
*/
private getWorkerByType(type: WorkerMessageType): Worker {
// 战斗相关消息使用 battleWorker
if (type === MsgType.SIMULATE_BATTLE || type === MsgType.CALCULATE_PLUNDER || type === MsgType.CALCULATE_DEBRIS) {
this.initBattleWorker()
return this.battleWorker!
}
throw new Error(`Unknown message type: ${type}`)
}
/**
* 发送消息到 Worker 并等待响应
*/
private sendMessage<T>(type: WorkerMessageType, payload: unknown, timeout: number = this.defaultTimeout): Promise<T> {
const worker = this.getWorkerByType(type)
if (!worker) {
return Promise.reject(new Error('Worker initialization failed'))
}
const id = this.generateMessageId()
return new Promise<T>((resolve, reject) => {
// 设置超时
const timeoutId = setTimeout(() => {
this.pendingTasks.delete(id)
reject(new Error(`Worker task timeout after ${timeout}ms`))
}, timeout)
// 保存任务
this.pendingTasks.set(id, {
resolve: resolve as (data: unknown) => void,
reject,
timeout: timeoutId
})
// 发送消息(使用 toPlainObject 转换 Vue Proxy 对象,然后使用浏览器内置的 structured clone
const message: WorkerRequestMessage = { id, type, payload: toPlainObject(payload) }
worker.postMessage(message)
})
}
/**
* 战斗模拟
*/
public async simulateBattle(params: {
attacker: {
ships: Parameters<typeof import('@/utils/battleSimulator').simulateBattle>[0]['ships']
defense?: Parameters<typeof import('@/utils/battleSimulator').simulateBattle>[0]['defense']
weaponTech?: number
shieldTech?: number
armorTech?: number
}
defender: {
ships: Parameters<typeof import('@/utils/battleSimulator').simulateBattle>[0]['ships']
defense?: Parameters<typeof import('@/utils/battleSimulator').simulateBattle>[0]['defense']
weaponTech?: number
shieldTech?: number
armorTech?: number
}
maxRounds?: number
}): Promise<ReturnType<typeof import('@/utils/battleSimulator').simulateBattle>> {
return this.sendMessage(MsgType.SIMULATE_BATTLE, params)
}
/**
* 计算掠夺资源
*/
public async calculatePlunder(params: {
defenderResources: Parameters<typeof import('@/utils/battleSimulator').calculatePlunder>[0]
attackerFleet: Parameters<typeof import('@/utils/battleSimulator').calculatePlunder>[1]
}): Promise<ReturnType<typeof import('@/utils/battleSimulator').calculatePlunder>> {
return this.sendMessage(MsgType.CALCULATE_PLUNDER, params)
}
/**
* 计算残骸场
*/
public async calculateDebris(params: {
attackerLosses: Parameters<typeof import('@/utils/battleSimulator').calculateDebrisField>[0]
defenderLosses: Parameters<typeof import('@/utils/battleSimulator').calculateDebrisField>[1]
}): Promise<ReturnType<typeof import('@/utils/battleSimulator').calculateDebrisField>> {
return this.sendMessage(MsgType.CALCULATE_DEBRIS, params)
}
/**
* 销毁所有 Worker
*/
public destroy(): void {
if (this.battleWorker) {
this.battleWorker.terminate()
this.battleWorker = null
}
// 清除所有待处理的任务
for (const task of this.pendingTasks.values()) {
if (task.timeout) clearTimeout(task.timeout)
task.reject(new Error('Worker manager destroyed'))
}
this.pendingTasks.clear()
}
/**
* 获取待处理任务数量
*/
public getPendingTaskCount(): number {
return this.pendingTasks.size
}
}
// 导出单例
export const workerManager = new WorkerManager()