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

@@ -1,312 +1,65 @@
import type { Fleet, Resources, BattleResult, Officer } from '@/types/game'
import { DefenseType, ShipType, OfficerType } from '@/types/game'
import { SHIPS, DEFENSES } from '@/config/gameConfig'
import { DefenseType, OfficerType } from '@/types/game'
import * as officerLogic from './officerLogic'
/**
* 战斗单位(舰船或防御)
*/
interface BattleUnit {
type: ShipType | DefenseType
count: number
attack: number
shield: number
armor: number
isShip: boolean
}
/**
* 战斗方
*/
interface BattleSide {
fleet: BattleUnit[]
defense: BattleUnit[]
totalShields: number
totalArmor: number
}
/**
* 准备战斗方数据
*/
const prepareBattleSide = (fleet: Partial<Fleet>, defense: Partial<Record<DefenseType, number>>, defenseBonus: number = 0): BattleSide => {
const side: BattleSide = {
fleet: [],
defense: [],
totalShields: 0,
totalArmor: 0
}
// 添加舰船
Object.entries(fleet).forEach(([shipType, count]) => {
if (count > 0) {
const config = SHIPS[shipType as ShipType]
const unit: BattleUnit = {
type: shipType as ShipType,
count,
attack: config.attack,
shield: config.shield * (1 + defenseBonus / 100),
armor: config.armor * (1 + defenseBonus / 100),
isShip: true
}
side.fleet.push(unit)
side.totalShields += unit.shield * count
side.totalArmor += unit.armor * count
}
})
// 添加防御
Object.entries(defense).forEach(([defenseType, count]) => {
if (count > 0) {
const config = DEFENSES[defenseType as DefenseType]
const unit: BattleUnit = {
type: defenseType as DefenseType,
count,
attack: config.attack,
shield: config.shield * (1 + defenseBonus / 100),
armor: config.armor * (1 + defenseBonus / 100),
isShip: false
}
side.defense.push(unit)
side.totalShields += unit.shield * count
side.totalArmor += unit.armor * count
}
})
return side
}
/**
* 计算一方的总攻击力
*/
const calculateTotalAttack = (side: BattleSide): number => {
let total = 0
side.fleet.forEach(unit => {
total += unit.attack * unit.count
})
side.defense.forEach(unit => {
total += unit.attack * unit.count
})
return total
}
/**
* 执行一轮战斗
*/
const executeBattleRound = (attacker: BattleSide, defender: BattleSide): void => {
// 攻击方对防御方造成伤害
const attackerDamage = calculateTotalAttack(attacker)
applyDamage(defender, attackerDamage)
// 防御方对攻击方造成伤害
const defenderDamage = calculateTotalAttack(defender)
applyDamage(attacker, defenderDamage)
}
/**
* 对一方施加伤害
*/
const applyDamage = (side: BattleSide, totalDamage: number): void => {
let remainingDamage = totalDamage
// 先消耗护盾
const totalShields = side.totalShields
if (totalShields > 0) {
const shieldAbsorption = Math.min(remainingDamage, totalShields)
remainingDamage -= shieldAbsorption
side.totalShields -= shieldAbsorption
}
// 剩余伤害穿透护盾,破坏单位
if (remainingDamage > 0) {
destroyUnits(side, remainingDamage)
}
}
/**
* 根据伤害摧毁单位
*/
const destroyUnits = (side: BattleSide, damage: number): void => {
let remainingDamage = damage
// 随机选择单位摧毁
const allUnits = [...side.fleet, ...side.defense]
while (remainingDamage > 0 && allUnits.some(u => u.count > 0)) {
// 随机选择一个有数量的单位
const availableUnits = allUnits.filter(u => u.count > 0)
if (availableUnits.length === 0) break
const targetUnit = availableUnits[Math.floor(Math.random() * availableUnits.length)]
if (!targetUnit) break // 安全检查
// 计算破坏概率(伤害 / 装甲)
const destructionChance = Math.min(remainingDamage / targetUnit.armor, 1)
if (Math.random() < destructionChance) {
targetUnit.count--
side.totalArmor -= targetUnit.armor
remainingDamage -= targetUnit.armor
} else {
// 未破坏,但消耗一部分伤害
remainingDamage -= targetUnit.armor * destructionChance
}
}
}
/**
* 检查战斗是否结束
*/
const isBattleOver = (attacker: BattleSide, defender: BattleSide): boolean => {
const attackerHasUnits = attacker.fleet.some(u => u.count > 0) || attacker.defense.some(u => u.count > 0)
const defenderHasUnits = defender.fleet.some(u => u.count > 0) || defender.defense.some(u => u.count > 0)
return !attackerHasUnits || !defenderHasUnits
}
/**
* 计算损失
*/
const calculateLosses = (
initialSide: BattleSide,
finalSide: BattleSide
): { fleet: Partial<Fleet>; defense: Partial<Record<DefenseType, number>> } => {
const losses: { fleet: Partial<Fleet>; defense: Partial<Record<DefenseType, number>> } = {
fleet: {},
defense: {}
}
// 计算舰船损失
initialSide.fleet.forEach((initialUnit, index) => {
const finalUnit = finalSide.fleet[index]
const lost = initialUnit.count - (finalUnit?.count || 0)
if (lost > 0) {
losses.fleet[initialUnit.type as ShipType] = lost
}
})
// 计算防御损失
initialSide.defense.forEach((initialUnit, index) => {
const finalUnit = finalSide.defense[index]
const lost = initialUnit.count - (finalUnit?.count || 0)
if (lost > 0) {
losses.defense[initialUnit.type as DefenseType] = lost
}
})
return losses
}
/**
* 计算残骸场
*/
const calculateDebrisField = (
attackerLosses: Partial<Fleet>,
defenderLosses: { fleet: Partial<Fleet>; defense: Partial<Record<DefenseType, number>> }
): Resources => {
const debris: Resources = { metal: 0, crystal: 0, deuterium: 0, darkMatter: 0, energy: 0 }
const debrisRate = 0.3 // 30%的残骸回收率
// 攻击方舰船损失
Object.entries(attackerLosses).forEach(([shipType, count]) => {
const config = SHIPS[shipType as ShipType]
debris.metal += config.cost.metal * count * debrisRate
debris.crystal += config.cost.crystal * count * debrisRate
})
// 防御方舰船损失
Object.entries(defenderLosses.fleet).forEach(([shipType, count]) => {
const config = SHIPS[shipType as ShipType]
debris.metal += config.cost.metal * count * debrisRate
debris.crystal += config.cost.crystal * count * debrisRate
})
// 防御设施不产生残骸场(或产生较少)
return debris
}
/**
* 计算掠夺资源
*/
const calculatePlunder = (availableResources: Resources, attackerFleet: Partial<Fleet>, cargoCapacity: number): Resources => {
// 计算攻击方剩余运载能力
let totalCapacity = 0
Object.entries(attackerFleet).forEach(([shipType, count]) => {
const config = SHIPS[shipType as ShipType]
totalCapacity += config.cargoCapacity * count
})
// 最多掠夺50%的资源
const maxPlunder = Math.min(totalCapacity, cargoCapacity)
const plunderRate = 0.5
const plunder: Resources = {
metal: Math.min(availableResources.metal * plunderRate, maxPlunder * 0.5),
crystal: Math.min(availableResources.crystal * plunderRate, maxPlunder * 0.3),
deuterium: Math.min(availableResources.deuterium * plunderRate, maxPlunder * 0.2),
darkMatter: 0, // 暗物质无法掠夺
energy: 0
}
return plunder
}
import { workerManager } from '@/workers/workerManager'
/**
* 执行战斗模拟
* 使用 Web Worker 在后台线程中执行计算密集型的战斗模拟
*/
export const simulateBattle = (
export const simulateBattle = async (
attackerFleet: Partial<Fleet>,
defenderFleet: Partial<Fleet>,
defenderDefense: Partial<Record<DefenseType, number>>,
defenderResources: Resources,
attackerOfficers: Record<OfficerType, Officer>,
defenderOfficers: Record<OfficerType, Officer>
): BattleResult => {
): Promise<BattleResult> => {
// 计算军官加成
const attackerBonuses = officerLogic.calculateActiveBonuses(attackerOfficers, Date.now())
const defenderBonuses = officerLogic.calculateActiveBonuses(defenderOfficers, Date.now())
// 准备战斗方
const initialAttacker = prepareBattleSide(attackerFleet, {}, attackerBonuses.defenseBonus)
const initialDefender = prepareBattleSide(defenderFleet, defenderDefense, defenderBonuses.defenseBonus)
// 将防御加成转换为科技等级简化10%加成 = 1级科技
const attackerTechLevel = Math.floor(attackerBonuses.defenseBonus / 10)
const defenderTechLevel = Math.floor(defenderBonuses.defenseBonus / 10)
// 复制战斗方用于战斗
const attacker = JSON.parse(JSON.stringify(initialAttacker)) as BattleSide
const defender = JSON.parse(JSON.stringify(initialDefender)) as BattleSide
// 战斗回合最多6回合
let rounds = 0
const maxRounds = 6
while (rounds < maxRounds && !isBattleOver(attacker, defender)) {
executeBattleRound(attacker, defender)
rounds++
}
// 计算损失
const attackerLosses = calculateLosses(initialAttacker, attacker).fleet
const defenderLosses = calculateLosses(initialDefender, defender)
// 判断胜负
let winner: 'attacker' | 'defender' | 'draw' = 'draw'
const attackerSurvived = attacker.fleet.some(u => u.count > 0)
const defenderSurvived = defender.fleet.some(u => u.count > 0) || defender.defense.some(u => u.count > 0)
if (attackerSurvived && !defenderSurvived) {
winner = 'attacker'
} else if (!attackerSurvived && defenderSurvived) {
winner = 'defender'
}
// 计算残骸场
const debrisField = calculateDebrisField(attackerLosses, defenderLosses)
// 使用 Worker 执行战斗模拟
const simulationResult = await workerManager.simulateBattle({
attacker: {
ships: attackerFleet,
weaponTech: 0, // 暂时不考虑武器科技
shieldTech: attackerTechLevel,
armorTech: attackerTechLevel
},
defender: {
ships: defenderFleet,
defense: defenderDefense,
weaponTech: 0,
shieldTech: defenderTechLevel,
armorTech: defenderTechLevel
},
maxRounds: 6 // 最多6回合
})
// 计算掠夺(仅攻击方胜利时)
const plunder =
winner === 'attacker'
? calculatePlunder(defenderResources, attackerFleet, 10000)
simulationResult.winner === 'attacker'
? await workerManager.calculatePlunder({
defenderResources,
attackerFleet: simulationResult.attackerRemaining
})
: { metal: 0, crystal: 0, deuterium: 0, darkMatter: 0, energy: 0 }
// 计算残骸场
const debrisField = await workerManager.calculateDebris({
attackerLosses: simulationResult.attackerLosses,
defenderLosses: simulationResult.defenderLosses
})
// 计算月球生成概率(根据残骸场总量)
const totalDebris = debrisField.metal + debrisField.crystal
const moonChance = Math.min(totalDebris / 100000, 0.2) // 最高20%概率
// 生成战斗报告
const battleResult: BattleResult = {
id: `battle_${Date.now()}`,
@@ -318,11 +71,17 @@ export const simulateBattle = (
attackerFleet,
defenderFleet,
defenderDefense,
attackerLosses,
defenderLosses,
winner,
attackerLosses: simulationResult.attackerLosses,
defenderLosses: simulationResult.defenderLosses,
winner: simulationResult.winner,
plunder,
debrisField
debrisField,
// 新增详细信息
rounds: simulationResult.rounds,
attackerRemaining: simulationResult.attackerRemaining,
defenderRemaining: simulationResult.defenderRemaining,
roundDetails: simulationResult.roundDetails,
moonChance
}
return battleResult