diff --git a/1.6/1.6/Assemblies/WulaFallenEmpire.dll b/1.6/1.6/Assemblies/WulaFallenEmpire.dll
index 2c897c8f..d2344cbf 100644
Binary files a/1.6/1.6/Assemblies/WulaFallenEmpire.dll and b/1.6/1.6/Assemblies/WulaFallenEmpire.dll differ
diff --git a/1.6/1.6/Defs/AbilityDefs/WULA_Flyover_Ability.xml b/1.6/1.6/Defs/AbilityDefs/WULA_Flyover_Ability.xml
index ff67dd3e..327acf3c 100644
--- a/1.6/1.6/Defs/AbilityDefs/WULA_Flyover_Ability.xml
+++ b/1.6/1.6/Defs/AbilityDefs/WULA_Flyover_Ability.xml
@@ -635,11 +635,10 @@
-
WULA_Firepower_Minigun_Strafe
链炮扫射
- 以战舰上的自动链炮对目标区域进行可选方向的扫射,射击速度和冷却都很快,但是威力欠佳。
+ 以战舰上的自动链炮对目标区域进行可选方向的扫射,射击速度和冷却都很快,对轻甲目标有效,但是威力欠佳。
1
Misc12
@@ -682,4 +681,127 @@
+
+ WULA_Firepower_Cannon_Salvo
+ 轻型舰炮齐射
+ 指挥战舰侧弦的副炮,进行一轮共计12发炮弹的齐射,造成中规中矩的毁伤。
+
+ 1
+ Misc12
+ false
+
+ Verb_CastAbility
+ false
+ false
+ 1
+ 120
+ true
+
+ false
+ true
+
+
+
+
+ 1000
+ BombardmentFacility
+
+
+ 10
+ 3
+ 90
+ 12
+ 120
+
+
+ true
+ 5
+
+ WULA_Firepower_Cannon_Salvo_Skyfaller
+ true
+ false
+
+
+
+ BombardmentFacility
+ 需要拥有<color=#BD2F31><i>武器阵列</i></color>设施的战舰在地图上才能进行轨道炮击支援
+
+
+
+
+ WULA_Firepower_Cannon_Surveillance
+ 轻型舰炮监视
+ 指挥战舰侧弦的副炮,监视一个区域30秒,对任何进入范围的敌对目标进行炮击。
+
+ 1
+ Misc12
+ false
+
+ Verb_CastAbility
+ false
+ false
+ 1
+ 120
+ true
+
+ false
+ true
+
+
+
+
+ 1000
+ BombardmentFacility
+
+
+ 180
+ WULA_Firepower_Cannon_Surveillance_Skyfaller
+ 12
+ (0.85,0.85,0.3,0.5)
+
+
+
+ BombardmentFacility
+ 需要拥有<color=#BD2F31><i>武器阵列</i></color>设施的战舰在地图上才能进行轨道炮击支援
+
+
+
+
+ WULA_Firepower_EnergyLance_Strafe
+ 光矛扫射
+ 指挥战舰侧弦的光矛阵列,发射一道光矛横扫战场,造成大量的热能伤害。
+
+ 1
+ Misc12
+ false
+
+ Verb_CastAbility
+ false
+ false
+ 1
+ 120
+ true
+
+ false
+ true
+
+
+
+
+ 1000
+ BombardmentFacility
+
+
+ 600
+ 20
+ true
+ 3
+
+
+
+ BombardmentFacility
+ 需要拥有<color=#BD2F31><i>武器阵列</i></color>设施的战舰在地图上才能进行轨道炮击支援
+
+
+
\ No newline at end of file
diff --git a/1.6/1.6/Defs/HediffDefs/WULA_FM_Hediffs.xml b/1.6/1.6/Defs/HediffDefs/WULA_FM_Hediffs.xml
index 16f2232c..80e169e6 100644
--- a/1.6/1.6/Defs/HediffDefs/WULA_FM_Hediffs.xml
+++ b/1.6/1.6/Defs/HediffDefs/WULA_FM_Hediffs.xml
@@ -74,6 +74,7 @@
WULA_Firepower_Minigun_Strafe
+ WULA_Firepower_Cannon_Salvo
diff --git a/1.6/1.6/Defs/ThingDefs_Misc/WULA_Flyover_Item.xml b/1.6/1.6/Defs/ThingDefs_Misc/WULA_Flyover_Item.xml
index a4b52107..00a77073 100644
--- a/1.6/1.6/Defs/ThingDefs_Misc/WULA_Flyover_Item.xml
+++ b/1.6/1.6/Defs/ThingDefs_Misc/WULA_Flyover_Item.xml
@@ -749,4 +749,187 @@
25
Explosion_Bomb
+
+ WULA_Firepower_Cannon_Salvo_Skyfaller
+ 轻型副炮炮弹
+ (1, 1)
+
+ Wula/Projectile/WULA_Bullet_ChargeLanceShot_Red
+ Graphic_Single
+ MoteGlow
+ 8
+
+
+ Accelerate
+ Things/Skyfaller/SkyfallerShadowDropPod
+ (1, 1)
+ DropPod_Fall
+ 100
+ Explosion_Bomb
+ 0.05
+ 1
+ 1
+
+
+ (0,180)
+ (1, 181)
+
+
+
+
+ (0,0)
+ (1, 1)
+
+
+ 6
+ Bomb
+ 0.85
+
+
+
+ WULA_Firepower_Cannon_Surveillance_Skyfaller
+ 舰炮监视信标
+ (1,1)
+
+ Wula/Building/WULA_WeaponArmor_Productor_Incoming
+ Graphic_Single
+ CutoutFlying
+ (1,1)
+
+
+ Accelerate
+ Things/Skyfaller/SkyfallerShadowDropPod
+ (5, 5)
+ DropPod_Fall
+ 100
+ Explosion_Vaporize
+ 0.05
+ 1
+ 1
+
+
+ (0,0)
+ (1, 1)
+
+
+ WULA_Firepower_Cannon_Surveillance_Beacon
+
+
+
+ WULA_Firepower_Cannon_Surveillance_Beacon
+ 舰炮监视信标
+ 一枚标定监视区的轰炸信标,进入此处的敌对势力都会被乌拉帝国舰队的舰炮狠狠打击。
+ Wula/Building/WULA_WeaponArmor_Productor
+ Normal
+ true
+ false
+ (1,1)
+
+ Wula/Building/WULA_Dropping_Building_Cleanzone
+ Graphic_Multi
+ (3,3)
+
+ false
+
+
+ Building
+ Standable
+ false
+ 0.5
+ false
+ 0
+ false
+ false
+
+ 1
+ 0
+ 0
+
+ 0
+
+ BuildingDestroyed_Metal_Small
+
+
+
+ 0.2
+ (255, 20, 20, 242)
+
+
+ 12
+ (1, 1, 1)
+ 0
+ true
+ 射程
+ 该信标所标定的舰炮监视范围,进入其中的敌军将遭到炮击
+ true
+
+
+ 12
+ true
+ false
+ false
+ 3
+ 15
+ 120
+ 3
+ WULA_Firepower_Cannon_Salvo_Skyfaller
+
+
+
+ true
+ false
+
+
+ 3600
+
+
+ 6
+ (0.85,0.85,0.3,0.5)
+
+
+
+
+ WULA_EnergyLance_Base
+ power beam
+ PowerBeam
+
+
+ WULA_EnergyLance_Base
+ energy lance
+ WulaFallenEmpire.EnergyLance
+
+
+ Flame
+ true
+ 0.9
+
+ 60
+ 95
+
+
+ 5
+ 10
+
+
+
+
+
+
+ 6
+ (255, 200, 50, 242)
+ OrbitalBeam
+
+
+ 0.02
+
+
+
+ (255, 220, 180)
+ (220, 200, 160)
+ (255, 240, 200)
+ 1.2
+
+
+
+
\ No newline at end of file
diff --git a/Source/WulaFallenEmpire/Ability/WULA_AbilityCircularBombardment/CompAbilityEffect_CircularBombardment.cs b/Source/WulaFallenEmpire/Ability/WULA_AbilityCircularBombardment/CompAbilityEffect_CircularBombardment.cs
new file mode 100644
index 00000000..e4af6f81
--- /dev/null
+++ b/Source/WulaFallenEmpire/Ability/WULA_AbilityCircularBombardment/CompAbilityEffect_CircularBombardment.cs
@@ -0,0 +1,531 @@
+using RimWorld;
+using Verse;
+using UnityEngine;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace WulaFallenEmpire
+{
+ public class CompAbilityEffect_CircularBombardment : CompAbilityEffect
+ {
+ public new CompProperties_AbilityCircularBombardment Props => (CompProperties_AbilityCircularBombardment)props;
+
+ // 轰炸状态
+ private CircularBombardmentState currentState = CircularBombardmentState.Idle;
+ private List targetCells = new List();
+ private List remainingTargets = new List();
+ private IntVec3 bombardmentCenter;
+ private int warmupTicksRemaining = 0;
+ private int nextLaunchTick = 0;
+ private int launchesCompleted = 0;
+
+ // 组内间隔状态
+ private List currentGroupTargets = new List();
+ private int currentGroupIndex = 0;
+ private int nextInnerLaunchTick = 0;
+ private bool isInGroupLaunch = false;
+
+ // 预览状态
+ private List currentPreviewCells = new List();
+ private List impactPreviewCells = new List();
+
+ public override void Apply(LocalTargetInfo target, LocalTargetInfo dest)
+ {
+ base.Apply(target, dest);
+
+ if (parent.pawn == null || parent.pawn.Map == null)
+ return;
+
+ try
+ {
+ Log.Message($"[CircularBombardment] Starting circular bombardment at {target.Cell}");
+
+ // 设置轰炸中心
+ bombardmentCenter = target.Cell;
+
+ // 选择目标格子
+ SelectTargetCells();
+
+ // 初始化剩余目标列表
+ remainingTargets = new List(targetCells);
+
+ // 开始前摇
+ StartWarmup();
+
+ Log.Message($"[CircularBombardment] Bombardment initialized: {targetCells.Count} targets, " +
+ $"{Props.simultaneousLaunches} simultaneous launches, " +
+ $"independent intervals: {Props.useIndependentIntervals}");
+ }
+ catch (System.Exception ex)
+ {
+ Log.Error($"[CircularBombardment] Error starting bombardment: {ex}");
+ }
+ }
+
+ // 绘制预览效果
+ public override void DrawEffectPreview(LocalTargetInfo target)
+ {
+ base.DrawEffectPreview(target);
+
+ if (!Props.showBombardmentArea || parent.pawn == null || parent.pawn.Map == null)
+ return;
+
+ try
+ {
+ // 计算预览区域
+ CalculatePreviewArea(target.Cell);
+
+ // 绘制轰炸区域预览
+ DrawCircularAreaPreview(target.Cell);
+
+ // 绘制预计落点预览
+ if (Props.showImpactPreview)
+ {
+ DrawImpactPreview(target.Cell);
+ }
+ }
+ catch (System.Exception)
+ {
+ // 忽略预览绘制错误
+ }
+ }
+
+ // 计算预览区域
+ private void CalculatePreviewArea(IntVec3 center)
+ {
+ Map map = parent.pawn.Map;
+ currentPreviewCells.Clear();
+ impactPreviewCells.Clear();
+
+ // 计算圆形区域内的所有单元格
+ currentPreviewCells = GenRadial.RadialCellsAround(center, Props.radius, true).ToList();
+
+ // 随机选择一些单元格作为预计落点预览
+ var potentialTargets = currentPreviewCells
+ .Where(cell => cell.InBounds(map))
+ .Where(cell => IsValidTargetCell(cell, map))
+ .ToList();
+
+ // 随机选择预览目标
+ int previewCount = Mathf.Min(Props.maxTargets, potentialTargets.Count);
+ impactPreviewCells = potentialTargets
+ .InRandomOrder()
+ .Take(previewCount)
+ .ToList();
+ }
+
+ // 绘制圆形区域预览
+ private void DrawCircularAreaPreview(IntVec3 center)
+ {
+ Map map = parent.pawn.Map;
+
+ // 绘制圆形区域边界
+ foreach (var cell in currentPreviewCells)
+ {
+ if (cell.InBounds(map))
+ {
+ GenDraw.DrawFieldEdges(new List { cell }, Props.areaPreviewColor, 0.2f);
+ }
+ }
+
+ // 绘制圆形边界线
+ DrawCircularBoundary(center);
+ }
+
+ // 绘制圆形边界
+ private void DrawCircularBoundary(IntVec3 center)
+ {
+ Map map = parent.pawn.Map;
+
+ // 绘制圆形边界(使用多个线段近似)
+ int segments = 36; // 36段,每段10度
+ float angleStep = 360f / segments;
+
+ Vector3 centerPos = center.ToVector3Shifted();
+
+ for (int i = 0; i < segments; i++)
+ {
+ float angle1 = i * angleStep * Mathf.Deg2Rad;
+ float angle2 = (i + 1) * angleStep * Mathf.Deg2Rad;
+
+ Vector3 point1 = centerPos + new Vector3(
+ Mathf.Cos(angle1) * Props.radius,
+ 0,
+ Mathf.Sin(angle1) * Props.radius
+ );
+
+ Vector3 point2 = centerPos + new Vector3(
+ Mathf.Cos(angle2) * Props.radius,
+ 0,
+ Mathf.Sin(angle2) * Props.radius
+ );
+
+ GenDraw.DrawLineBetween(point1, point2, SimpleColor.Orange, 0.2f);
+ }
+ }
+
+ // 绘制预计落点预览
+ private void DrawImpactPreview(IntVec3 center)
+ {
+ Map map = parent.pawn.Map;
+
+ foreach (var cell in impactPreviewCells)
+ {
+ if (cell.InBounds(map))
+ {
+ // 绘制落点标记
+ GenDraw.DrawTargetHighlight(cell);
+
+ // 绘制落点范围指示
+ GenDraw.DrawRadiusRing(cell, 1f, Color.red, (c) => true);
+ }
+ }
+ }
+
+ // 选择目标格子
+ private void SelectTargetCells()
+ {
+ Map map = parent.pawn.Map;
+
+ // 获取圆形区域内的所有单元格
+ var areaCells = GenRadial.RadialCellsAround(bombardmentCenter, Props.radius, true)
+ .Where(cell => cell.InBounds(map))
+ .Where(cell => IsValidTargetCell(cell, map))
+ .ToList();
+
+ var selectedCells = new List();
+ var missedCells = new List();
+
+ // 根据概率选择目标格子
+ foreach (var cell in areaCells)
+ {
+ if (Rand.Value <= Props.targetSelectionChance)
+ {
+ selectedCells.Add(cell);
+ }
+ else
+ {
+ missedCells.Add(cell);
+ }
+ }
+
+ // 应用最小/最大限制
+ if (selectedCells.Count < Props.minTargets)
+ {
+ // 补充不足的格子
+ int needed = Props.minTargets - selectedCells.Count;
+ if (missedCells.Count > 0)
+ {
+ selectedCells.AddRange(missedCells.InRandomOrder().Take(Mathf.Min(needed, missedCells.Count)));
+ }
+ }
+ else if (selectedCells.Count > Props.maxTargets)
+ {
+ // 随机移除多余的格子
+ selectedCells = selectedCells.InRandomOrder().Take(Props.maxTargets).ToList();
+ }
+
+ targetCells = selectedCells;
+ Log.Message($"[CircularBombardment] Selected {targetCells.Count} target cells from {areaCells.Count} area cells");
+ }
+
+ // 检查单元格是否有效
+ private bool IsValidTargetCell(IntVec3 cell, Map map)
+ {
+ // 检查最小距离
+ if (Props.minDistanceFromCenter > 0)
+ {
+ float distance = Vector3.Distance(cell.ToVector3(), bombardmentCenter.ToVector3());
+ if (distance < Props.minDistanceFromCenter)
+ return false;
+ }
+
+ // 检查友军误伤
+ if (Props.avoidFriendlyFire)
+ {
+ var pawnsInCell = map.thingGrid.ThingsListAt(cell).OfType();
+ foreach (var pawn in pawnsInCell)
+ {
+ if (pawn.Faction != null && pawn.Faction == parent.pawn.Faction)
+ return false;
+ }
+ }
+
+ // 检查建筑物
+ if (Props.avoidBuildings)
+ {
+ var buildingsInCell = map.thingGrid.ThingsListAt(cell).OfType();
+ if (buildingsInCell.Any())
+ return false;
+ }
+
+ return true;
+ }
+
+ private void StartWarmup()
+ {
+ currentState = CircularBombardmentState.Warmup;
+ warmupTicksRemaining = Props.warmupTicks;
+ launchesCompleted = 0;
+ currentGroupIndex = 0;
+
+ Log.Message($"[CircularBombardment] Warmup started: {warmupTicksRemaining} ticks remaining");
+ }
+
+ private void UpdateWarmup()
+ {
+ warmupTicksRemaining--;
+
+ if (warmupTicksRemaining <= 0)
+ {
+ // 前摇结束,开始发射
+ currentState = CircularBombardmentState.Launching;
+ nextLaunchTick = Find.TickManager.TicksGame;
+ Log.Message($"[CircularBombardment] Warmup completed, starting launches");
+ }
+ }
+
+ // 开始新的一组发射
+ private void StartNewGroup()
+ {
+ if (remainingTargets.Count == 0 || launchesCompleted >= Props.maxLaunches)
+ {
+ currentState = CircularBombardmentState.Completed;
+ Log.Message($"[CircularBombardment] All launches completed: {launchesCompleted}/{Props.maxLaunches}");
+ return;
+ }
+
+ // 选择本组的目标
+ int groupSize = Mathf.Min(Props.simultaneousLaunches, remainingTargets.Count);
+ currentGroupTargets = remainingTargets.Take(groupSize).ToList();
+ remainingTargets.RemoveRange(0, groupSize);
+
+ if (Props.useIndependentIntervals)
+ {
+ // 启用组内独立间隔
+ isInGroupLaunch = true;
+ nextInnerLaunchTick = Find.TickManager.TicksGame;
+ currentGroupIndex++;
+
+ Log.Message($"[CircularBombardment] Starting group {currentGroupIndex} with {currentGroupTargets.Count} targets, using independent intervals");
+ }
+ else
+ {
+ // 传统模式:同时发射所有目标
+ foreach (var target in currentGroupTargets)
+ {
+ if (launchesCompleted < Props.maxLaunches)
+ {
+ LaunchSkyfaller(target);
+ launchesCompleted++;
+ }
+ }
+
+ // 设置下一组发射时间
+ nextLaunchTick = Find.TickManager.TicksGame + Props.launchIntervalTicks;
+ Log.Message($"[CircularBombardment] Launched group {currentGroupIndex + 1} simultaneously: {currentGroupTargets.Count} targets");
+ }
+ }
+
+ // 更新组内独立发射
+ private void UpdateIndependentGroupLaunch()
+ {
+ if (Find.TickManager.TicksGame < nextInnerLaunchTick)
+ return;
+
+ if (currentGroupTargets.Count == 0)
+ {
+ // 当前组发射完毕
+ isInGroupLaunch = false;
+ nextLaunchTick = Find.TickManager.TicksGame + Props.launchIntervalTicks;
+ Log.Message($"[CircularBombardment] Group {currentGroupIndex} completed");
+ return;
+ }
+
+ // 发射当前目标
+ var target = currentGroupTargets[0];
+ currentGroupTargets.RemoveAt(0);
+
+ LaunchSkyfaller(target);
+ launchesCompleted++;
+
+ // 设置下一个组内发射时间
+ if (currentGroupTargets.Count > 0 && launchesCompleted < Props.maxLaunches)
+ {
+ nextInnerLaunchTick = Find.TickManager.TicksGame + Props.innerLaunchIntervalTicks;
+ }
+ else
+ {
+ // 当前组发射完毕或达到最大发射数量
+ isInGroupLaunch = false;
+ nextLaunchTick = Find.TickManager.TicksGame + Props.launchIntervalTicks;
+ }
+
+ Log.Message($"[CircularBombardment] Launched target in group {currentGroupIndex} ({launchesCompleted}/{Props.maxLaunches})");
+ }
+
+ // 更新发射逻辑
+ private void UpdateLaunching()
+ {
+ if (Props.useIndependentIntervals && isInGroupLaunch)
+ {
+ UpdateIndependentGroupLaunch();
+ }
+ else
+ {
+ if (Find.TickManager.TicksGame >= nextLaunchTick)
+ {
+ StartNewGroup();
+ }
+ }
+
+ // 检查是否完成
+ if (launchesCompleted >= Props.maxLaunches ||
+ (remainingTargets.Count == 0 && !isInGroupLaunch))
+ {
+ currentState = CircularBombardmentState.Completed;
+ Log.Message($"[CircularBombardment] Bombardment completed: {launchesCompleted} launches");
+ }
+ }
+
+ private void LaunchSkyfaller(IntVec3 targetCell)
+ {
+ try
+ {
+ if (Props.skyfallerDef != null)
+ {
+ // 使用 Skyfaller
+ Skyfaller skyfaller = SkyfallerMaker.MakeSkyfaller(Props.skyfallerDef);
+ GenSpawn.Spawn(skyfaller, targetCell, parent.pawn.Map);
+ }
+ else if (Props.projectileDef != null)
+ {
+ // 使用抛射体作为备用
+ LaunchProjectileAt(targetCell);
+ }
+ else
+ {
+ Log.Error($"[CircularBombardment] No skyfaller or projectile defined");
+ }
+ }
+ catch (System.Exception ex)
+ {
+ Log.Error($"[CircularBombardment] Error launching at {targetCell}: {ex}");
+ }
+ }
+
+ private void LaunchProjectileAt(IntVec3 targetCell)
+ {
+ // 从上方发射抛射体
+ IntVec3 spawnCell = new IntVec3(targetCell.x, 0, targetCell.z);
+ Vector3 spawnPos = spawnCell.ToVector3() + new Vector3(0, 20f, 0);
+
+ Projectile projectile = (Projectile)GenSpawn.Spawn(Props.projectileDef, spawnCell, parent.pawn.Map);
+ if (projectile != null)
+ {
+ projectile.Launch(
+ parent.pawn,
+ spawnPos,
+ new LocalTargetInfo(targetCell),
+ new LocalTargetInfo(targetCell),
+ ProjectileHitFlags.All,
+ false
+ );
+ }
+ }
+
+ private void Cleanup()
+ {
+ // 重置状态
+ currentState = CircularBombardmentState.Idle;
+ targetCells.Clear();
+ remainingTargets.Clear();
+ currentGroupTargets.Clear();
+ currentPreviewCells.Clear();
+ impactPreviewCells.Clear();
+ currentGroupIndex = 0;
+ isInGroupLaunch = false;
+ launchesCompleted = 0;
+
+ Log.Message($"[CircularBombardment] Cleanup completed");
+ }
+
+ public override void CompTick()
+ {
+ base.CompTick();
+
+ if (currentState == CircularBombardmentState.Idle)
+ return;
+
+ switch (currentState)
+ {
+ case CircularBombardmentState.Warmup:
+ UpdateWarmup();
+ break;
+
+ case CircularBombardmentState.Launching:
+ UpdateLaunching();
+ break;
+
+ case CircularBombardmentState.Completed:
+ Cleanup();
+ break;
+ }
+ }
+
+ public override void PostExposeData()
+ {
+ base.PostExposeData();
+
+ Scribe_Values.Look(ref currentState, "currentState", CircularBombardmentState.Idle);
+ Scribe_Collections.Look(ref targetCells, "targetCells", LookMode.Value);
+ Scribe_Collections.Look(ref remainingTargets, "remainingTargets", LookMode.Value);
+ Scribe_Collections.Look(ref currentGroupTargets, "currentGroupTargets", LookMode.Value);
+ Scribe_Values.Look(ref warmupTicksRemaining, "warmupTicksRemaining", 0);
+ Scribe_Values.Look(ref nextLaunchTick, "nextLaunchTick", 0);
+ Scribe_Values.Look(ref nextInnerLaunchTick, "nextInnerLaunchTick", 0);
+ Scribe_Values.Look(ref launchesCompleted, "launchesCompleted", 0);
+ Scribe_Values.Look(ref currentGroupIndex, "currentGroupIndex", 0);
+ Scribe_Values.Look(ref isInGroupLaunch, "isInGroupLaunch", false);
+ }
+
+ // 技能提示信息
+ public override string ExtraLabelMouseAttachment(LocalTargetInfo target)
+ {
+ string baseInfo = $"圆形轰炸: 半径{Props.radius}格";
+
+ if (Props.useIndependentIntervals)
+ {
+ baseInfo += $"\n组内间隔: {Props.innerLaunchIntervalTicks}刻";
+ baseInfo += $"\n每组数量: {Props.simultaneousLaunches}个";
+ }
+ else
+ {
+ baseInfo += $"\n同时发射: {Props.simultaneousLaunches}个";
+ }
+
+ baseInfo += $"\n最大数量: {Props.maxLaunches}个";
+ baseInfo += $"\n组间间隔: {Props.launchIntervalTicks}刻";
+
+ return baseInfo;
+ }
+
+ public override bool Valid(LocalTargetInfo target, bool throwMessages = false)
+ {
+ return base.Valid(target, throwMessages) &&
+ parent.pawn != null &&
+ parent.pawn.Map != null &&
+ target.Cell.IsValid &&
+ target.Cell.InBounds(parent.pawn.Map);
+ }
+ }
+
+ // 圆形轰炸状态枚举
+ public enum CircularBombardmentState
+ {
+ Idle,
+ Warmup,
+ Launching,
+ Completed
+ }
+}
diff --git a/Source/WulaFallenEmpire/Ability/WULA_AbilityCircularBombardment/CompProperties_AbilityCircularBombardment.cs b/Source/WulaFallenEmpire/Ability/WULA_AbilityCircularBombardment/CompProperties_AbilityCircularBombardment.cs
new file mode 100644
index 00000000..a0e395ed
--- /dev/null
+++ b/Source/WulaFallenEmpire/Ability/WULA_AbilityCircularBombardment/CompProperties_AbilityCircularBombardment.cs
@@ -0,0 +1,47 @@
+using RimWorld;
+using Verse;
+using UnityEngine;
+
+namespace WulaFallenEmpire
+{
+ public class CompProperties_AbilityCircularBombardment : CompProperties_AbilityEffect
+ {
+ // 轰炸区域配置
+ public float radius = 10f; // 圆形区域半径
+ public bool useFixedRadius = true; // 是否使用固定半径
+
+ // 目标选择配置
+ public float targetSelectionChance = 0.5f; // 每个位置被选中的概率
+ public int minTargets = 3; // 最小目标数量
+ public int maxTargets = 15; // 最大目标数量
+
+ // 发射配置
+ public int simultaneousLaunches = 2; // 同时发射数量
+ public int launchIntervalTicks = 30; // 组间发射间隔(刻)
+ public int maxLaunches = 10; // 最大发射数量
+ public int warmupTicks = 120; // 前摇时间
+
+ // 组内独立间隔配置
+ public bool useIndependentIntervals = false; // 是否使用独立间隔
+ public int innerLaunchIntervalTicks = 10; // 组内发射间隔(刻)
+
+ // Skyfaller 配置
+ public ThingDef skyfallerDef; // 使用的 Skyfaller
+ public ThingDef projectileDef; // 备用的抛射体定义
+
+ // 视觉效果配置
+ public bool showBombardmentArea = true; // 是否显示轰炸区域
+ public Color areaPreviewColor = new Color(1f, 0.5f, 0.1f, 0.3f); // 区域预览颜色
+ public bool showImpactPreview = true; // 是否显示预计落点
+
+ // 随机分布配置
+ public float minDistanceFromCenter = 0f; // 距离中心的最小距离
+ public bool avoidFriendlyFire = true; // 避免友军误伤
+ public bool avoidBuildings = false; // 避免建筑物
+
+ public CompProperties_AbilityCircularBombardment()
+ {
+ this.compClass = typeof(CompAbilityEffect_CircularBombardment);
+ }
+ }
+}
diff --git a/Source/WulaFallenEmpire/Ability/WULA_AbilityEnergyLance/CompAbilityEffect_EnergyLance.cs b/Source/WulaFallenEmpire/Ability/WULA_AbilityEnergyLance/CompAbilityEffect_EnergyLance.cs
new file mode 100644
index 00000000..2da71393
--- /dev/null
+++ b/Source/WulaFallenEmpire/Ability/WULA_AbilityEnergyLance/CompAbilityEffect_EnergyLance.cs
@@ -0,0 +1,129 @@
+using RimWorld;
+using Verse;
+using UnityEngine;
+
+namespace WulaFallenEmpire
+{
+ public class CompAbilityEffect_EnergyLance : CompAbilityEffect_WithDest
+ {
+ public new CompProperties_EnergyLance Props => (CompProperties_EnergyLance)props;
+ public override void Apply(LocalTargetInfo target, LocalTargetInfo dest)
+ {
+ base.Apply(target, dest);
+
+ // 计算光束的起点和方向
+ IntVec3 startPos = target.Cell;
+ IntVec3 endPos = dest.Cell;
+
+ // 如果使用固定距离,则从起点向终点方向移动固定距离
+ if (Props.useFixedDistance)
+ {
+ Vector3 direction = (endPos.ToVector3() - startPos.ToVector3()).normalized;
+ Vector3 offset = direction * Props.moveDistance;
+ endPos = startPos + new IntVec3(Mathf.RoundToInt(offset.x), 0, Mathf.RoundToInt(offset.z));
+ }
+
+ // 创建移动的能量光束
+ EnergyLance obj = (EnergyLance)GenSpawn.Spawn(ThingDef.Named("EnergyLance"), startPos, parent.pawn.Map);
+ obj.duration = Props.durationTicks;
+ obj.instigator = parent.pawn;
+ obj.startPos = startPos;
+ obj.endPos = endPos;
+ obj.moveDistance = Props.moveDistance;
+ obj.useFixedDistance = Props.useFixedDistance;
+ obj.firesPerTick = Props.firesPerTick;
+ // 不再需要传递伤害范围,因为现在从ModExtension读取
+ obj.StartStrike();
+
+ Log.Message($"[EnergyLance] Created energy lance from {startPos} to {endPos}, distance: {Props.moveDistance}");
+ }
+
+ // 绘制预览效果
+ public override void DrawEffectPreview(LocalTargetInfo target)
+ {
+ base.DrawEffectPreview(target);
+
+ if (parent.pawn == null || parent.pawn.Map == null || !target.IsValid)
+ return;
+
+ try
+ {
+ // 绘制起点预览
+ GenDraw.DrawTargetHighlight(target.Cell);
+
+ // 如果选择了终点,绘制移动路径预览
+ if (selectedTarget.IsValid)
+ {
+ DrawMovePathPreview(target.Cell, selectedTarget.Cell);
+ }
+ }
+ catch (System.Exception)
+ {
+ // 忽略预览绘制错误
+ }
+ }
+
+ private void DrawMovePathPreview(IntVec3 startPos, IntVec3 endPos)
+ {
+ Map map = parent.pawn.Map;
+
+ // 计算实际终点
+ IntVec3 actualEndPos = endPos;
+ if (Props.useFixedDistance)
+ {
+ Vector3 direction = (endPos.ToVector3() - startPos.ToVector3()).normalized;
+ Vector3 offset = direction * Props.moveDistance;
+ actualEndPos = startPos + new IntVec3(Mathf.RoundToInt(offset.x), 0, Mathf.RoundToInt(offset.z));
+ }
+
+ // 绘制移动路径
+ Vector3 startVec = startPos.ToVector3Shifted();
+ Vector3 endVec = actualEndPos.ToVector3Shifted();
+
+ GenDraw.DrawLineBetween(startVec, endVec, SimpleColor.Yellow, 0.2f);
+
+ // 绘制终点预览
+ GenDraw.DrawTargetHighlight(actualEndPos);
+
+ // 绘制作用范围预览(在移动路径上)
+ DrawEffectRangePreview(startPos, actualEndPos);
+ }
+
+ private void DrawEffectRangePreview(IntVec3 startPos, IntVec3 endPos)
+ {
+ Map map = parent.pawn.Map;
+
+ // 沿着移动路径绘制作用范围
+ Vector3 currentPos = startPos.ToVector3();
+ Vector3 direction = (endPos.ToVector3() - startPos.ToVector3()).normalized;
+ float totalDistance = Vector3.Distance(startPos.ToVector3(), endPos.ToVector3());
+ float step = 1f; // 每格绘制
+
+ for (float distance = 0; distance <= totalDistance; distance += step)
+ {
+ Vector3 checkPos = startPos.ToVector3() + direction * distance;
+ IntVec3 checkCell = new IntVec3(Mathf.RoundToInt(checkPos.x), 0, Mathf.RoundToInt(checkPos.z));
+
+ if (checkCell.InBounds(map))
+ {
+ // 绘制作用范围指示
+ GenDraw.DrawRadiusRing(checkCell, 1.5f, Color.red, (c) => true);
+ }
+ }
+ }
+
+ public override string ExtraLabelMouseAttachment(LocalTargetInfo target)
+ {
+ string baseInfo = $"能量长矛: 持续{Props.durationTicks}刻";
+
+ if (Props.useFixedDistance)
+ {
+ baseInfo += $"\n移动距离: {Props.moveDistance}格";
+ }
+
+ baseInfo += $"\n选择起点后,再选择移动方向";
+
+ return baseInfo;
+ }
+ }
+}
diff --git a/Source/WulaFallenEmpire/Ability/WULA_AbilityEnergyLance/CompProperties_EnergyLance.cs b/Source/WulaFallenEmpire/Ability/WULA_AbilityEnergyLance/CompProperties_EnergyLance.cs
new file mode 100644
index 00000000..5f18a848
--- /dev/null
+++ b/Source/WulaFallenEmpire/Ability/WULA_AbilityEnergyLance/CompProperties_EnergyLance.cs
@@ -0,0 +1,22 @@
+using RimWorld;
+using Verse;
+
+namespace WulaFallenEmpire
+{
+ public class CompProperties_EnergyLance : CompProperties_EffectWithDest
+ {
+ public int durationTicks = 600; // 光束持续时间
+ public float moveDistance = 15f; // 光束移动距离
+ public bool useFixedDistance = true; // 是否使用固定距离
+
+ // 伤害配置
+ public int firesPerTick = 4; // 每刻产生的火焰数量
+ public IntRange flameDamageRange = new IntRange(65, 100); // 火焰伤害范围
+ public IntRange corpseFlameDamageRange = new IntRange(5, 10); // 尸体火焰伤害范围
+
+ public CompProperties_EnergyLance()
+ {
+ this.compClass = typeof(CompAbilityEffect_EnergyLance);
+ }
+ }
+}
diff --git a/Source/WulaFallenEmpire/Ability/WULA_AbilityEnergyLance/EnergyLance.cs b/Source/WulaFallenEmpire/Ability/WULA_AbilityEnergyLance/EnergyLance.cs
new file mode 100644
index 00000000..a9530d92
--- /dev/null
+++ b/Source/WulaFallenEmpire/Ability/WULA_AbilityEnergyLance/EnergyLance.cs
@@ -0,0 +1,195 @@
+using RimWorld;
+using Verse;
+using UnityEngine;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace WulaFallenEmpire
+{
+ public class EnergyLance : OrbitalStrike
+ {
+ // 移动相关属性
+ public IntVec3 startPos;
+ public IntVec3 endPos;
+ public float moveDistance;
+ public bool useFixedDistance;
+
+ // 伤害配置
+ public int firesPerTick = 4;
+
+ // ModExtension引用
+ private EnergyLanceExtension extension;
+
+ // 移动状态
+ private Vector3 currentPos;
+ private Vector3 moveDirection;
+ private float moveSpeed;
+ private float traveledDistance;
+ private const float effectRadius = 3f; // 作用半径
+
+ private static List tmpThings = new List();
+
+ public override void StartStrike()
+ {
+ base.StartStrike();
+
+ // 获取ModExtension
+ extension = def.GetModExtension();
+ if (extension == null)
+ {
+ Log.Error($"[EnergyLance] No EnergyLanceExtension found on {def.defName}");
+ return;
+ }
+
+ // 初始化移动参数
+ currentPos = startPos.ToVector3();
+
+ if (useFixedDistance)
+ {
+ // 从起点向终点方向移动固定距离
+ Vector3 direction = (endPos.ToVector3() - startPos.ToVector3()).normalized;
+ moveDirection = direction;
+ moveSpeed = moveDistance / duration; // 根据持续时间计算移动速度
+ }
+ else
+ {
+ // 直接从起点移动到终点
+ Vector3 direction = (endPos.ToVector3() - startPos.ToVector3());
+ moveDirection = direction.normalized;
+ moveSpeed = direction.magnitude / duration;
+ }
+
+ traveledDistance = 0f;
+
+ // 创建视觉效果
+ CreateVisualEffect();
+
+ Log.Message($"[EnergyLance] Strike started from {startPos} to {endPos}, " +
+ $"damage: {extension.damageDef.defName}, speed: {moveSpeed}");
+ }
+
+ private void CreateVisualEffect()
+ {
+ // 使用ModExtension中定义的Mote,如果没有则使用默认的PowerBeam
+ if (extension.moteDef != null)
+ {
+ Mote mote = MoteMaker.MakeStaticMote(base.Position, base.Map, extension.moteDef);
+ }
+ else
+ {
+ // 使用原版PowerBeam的视觉效果
+ MoteMaker.MakePowerBeamMote(base.Position, base.Map);
+ }
+ }
+
+ protected override void Tick()
+ {
+ base.Tick();
+
+ if (!base.Destroyed && extension != null)
+ {
+ // 移动光束
+ MoveBeam();
+
+ // 造成伤害
+ for (int i = 0; i < firesPerTick; i++)
+ {
+ DoBeamDamage();
+ }
+ }
+ }
+
+ private void MoveBeam()
+ {
+ // 计算移动距离
+ float moveThisTick = moveSpeed;
+
+ // 更新位置
+ currentPos += moveDirection * moveThisTick;
+ traveledDistance += moveThisTick;
+
+ // 更新光束的实际位置
+ IntVec3 newCell = new IntVec3(Mathf.RoundToInt(currentPos.x), 0, Mathf.RoundToInt(currentPos.z));
+ if (newCell != base.Position && newCell.InBounds(base.Map))
+ {
+ base.Position = newCell;
+ }
+
+ // 检查是否到达终点
+ if (useFixedDistance && traveledDistance >= moveDistance)
+ {
+ // 固定距离模式:移动指定距离后结束
+ Destroy();
+ Log.Message($"[EnergyLance] Reached fixed distance, destroying");
+ }
+ else if (!useFixedDistance && traveledDistance >= Vector3.Distance(startPos.ToVector3(), endPos.ToVector3()))
+ {
+ // 终点模式:到达终点后结束
+ Destroy();
+ Log.Message($"[EnergyLance] Reached end position, destroying");
+ }
+ }
+
+ private void DoBeamDamage()
+ {
+ if (extension == null) return;
+
+ // 在当前光束位置周围随机选择一个单元格
+ IntVec3 targetCell = (from x in GenRadial.RadialCellsAround(base.Position, effectRadius, useCenter: true)
+ where x.InBounds(base.Map)
+ select x).RandomElementByWeight((IntVec3 x) => 1f - Mathf.Min(x.DistanceTo(base.Position) / effectRadius, 1f) + 0.05f);
+
+ // 尝试在该单元格点火(如果配置了点火)
+ if (extension.igniteFires)
+ {
+ FireUtility.TryStartFireIn(targetCell, base.Map, Rand.Range(0.1f, extension.fireIgniteChance), instigator);
+ }
+
+ // 对该单元格内的物体造成伤害
+ tmpThings.Clear();
+ tmpThings.AddRange(targetCell.GetThingList(base.Map));
+
+ for (int i = 0; i < tmpThings.Count; i++)
+ {
+ Thing thing = tmpThings[i];
+
+ // 检查是否对尸体造成伤害
+ if (!extension.applyDamageToCorpses && thing is Corpse)
+ continue;
+
+ // 计算伤害量
+ int damageAmount = (thing is Corpse) ?
+ extension.corpseDamageAmountRange.RandomInRange :
+ extension.damageAmountRange.RandomInRange;
+
+ Pawn pawn = thing as Pawn;
+ BattleLogEntry_DamageTaken battleLogEntry = null;
+
+ if (pawn != null)
+ {
+ battleLogEntry = new BattleLogEntry_DamageTaken(pawn, RulePackDefOf.DamageEvent_PowerBeam, instigator as Pawn);
+ Find.BattleLog.Add(battleLogEntry);
+ }
+
+ // 使用ModExtension中定义的伤害类型
+ DamageInfo damageInfo = new DamageInfo(extension.damageDef, damageAmount, 0f, -1f, instigator, null, weaponDef);
+ thing.TakeDamage(damageInfo).AssociateWithLog(battleLogEntry);
+
+ Log.Message($"[EnergyLance] Applied {extension.damageDef.defName} damage {damageAmount} to {thing.Label}");
+ }
+
+ tmpThings.Clear();
+ }
+
+ public override void ExposeData()
+ {
+ base.ExposeData();
+
+ Scribe_Values.Look(ref startPos, "startPos");
+ Scribe_Values.Look(ref endPos, "endPos");
+ Scribe_Values.Look(ref moveDistance, "moveDistance");
+ Scribe_Values.Look(ref useFixedDistance, "useFixedDistance");
+ Scribe_Values.Look(ref firesPerTick, "firesPerTick", 4);
+ }
+ }
+}
diff --git a/Source/WulaFallenEmpire/Ability/WULA_AbilityEnergyLance/EnergyLanceExtension.cs b/Source/WulaFallenEmpire/Ability/WULA_AbilityEnergyLance/EnergyLanceExtension.cs
new file mode 100644
index 00000000..59b98a30
--- /dev/null
+++ b/Source/WulaFallenEmpire/Ability/WULA_AbilityEnergyLance/EnergyLanceExtension.cs
@@ -0,0 +1,22 @@
+using RimWorld;
+using Verse;
+
+namespace WulaFallenEmpire
+{
+ public class EnergyLanceExtension : DefModExtension
+ {
+ // 伤害类型配置
+ public DamageDef damageDef = DamageDefOf.Flame; // 伤害类型,默认为火焰伤害
+ public bool applyDamageToCorpses = true; // 是否对尸体造成伤害
+ public bool igniteFires = true; // 是否点燃火焰
+ public float fireIgniteChance = 0.8f; // 点燃火焰的概率
+
+ // 伤害量配置
+ public IntRange damageAmountRange = new IntRange(65, 100); // 对生物的伤害范围
+ public IntRange corpseDamageAmountRange = new IntRange(5, 10); // 对尸体的伤害范围
+
+ // 视觉效果配置
+ public ThingDef moteDef; // 使用的Mote类型
+ public SoundDef impactSound; // 撞击音效
+ }
+}
diff --git a/Source/WulaFallenEmpire/Ability/WULA_AbilityGuidedEnergyLance/CompBuildingEnergyLance.cs b/Source/WulaFallenEmpire/Ability/WULA_AbilityGuidedEnergyLance/CompBuildingEnergyLance.cs
new file mode 100644
index 00000000..4bd58ae4
--- /dev/null
+++ b/Source/WulaFallenEmpire/Ability/WULA_AbilityGuidedEnergyLance/CompBuildingEnergyLance.cs
@@ -0,0 +1,361 @@
+using RimWorld;
+using Verse;
+using UnityEngine;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace WulaFallenEmpire
+{
+ public class CompBuildingEnergyLance : ThingComp
+ {
+ public CompProperties_BuildingEnergyLance Props => (CompProperties_BuildingEnergyLance)props;
+
+ // 状态管理
+ private EnergyLanceState currentState = EnergyLanceState.Idle;
+ private int nextUpdateTick = 0;
+ private int noTargetTicks = 0;
+
+ // 目标管理
+ private Pawn currentTarget = null;
+ private IntVec3 lastTargetPosition = IntVec3.Invalid;
+
+ // EnergyLance实例
+ private GuidedEnergyLance activeLance = null;
+
+ public override void PostSpawnSetup(bool respawningAfterLoad)
+ {
+ base.PostSpawnSetup(respawningAfterLoad);
+
+ if (!respawningAfterLoad)
+ {
+ // 新生成时立即开始搜索目标
+ nextUpdateTick = Find.TickManager.TicksGame;
+ currentState = EnergyLanceState.Searching;
+
+ Log.Message($"[BuildingEnergyLance] Building spawned, starting target search");
+ }
+ }
+
+ private void UpdateState()
+ {
+ switch (currentState)
+ {
+ case EnergyLanceState.Idle:
+ // 空闲状态,等待下一次更新
+ break;
+
+ case EnergyLanceState.Searching:
+ SearchForTarget();
+ break;
+
+ case EnergyLanceState.Tracking:
+ TrackTarget();
+ break;
+
+ case EnergyLanceState.NoTarget:
+ HandleNoTarget();
+ break;
+ }
+ }
+
+ private void SearchForTarget()
+ {
+ Map map = parent.Map;
+ if (map == null) return;
+
+ // 获取范围内的所有有效目标
+ var potentialTargets = new List();
+
+ foreach (var pawn in map.mapPawns.AllPawnsSpawned)
+ {
+ if (IsValidTarget(pawn) && IsInRange(pawn.Position))
+ {
+ potentialTargets.Add(pawn);
+ }
+ }
+
+ if (potentialTargets.Count > 0)
+ {
+ // 选择第一个目标(可以改为其他选择逻辑)
+ currentTarget = potentialTargets[0];
+ lastTargetPosition = currentTarget.Position;
+
+ // 创建EnergyLance
+ CreateEnergyLance();
+
+ currentState = EnergyLanceState.Tracking;
+ noTargetTicks = 0;
+
+ Log.Message($"[BuildingEnergyLance] Locked target: {currentTarget.Label}, position: {currentTarget.Position}");
+ }
+ else
+ {
+ // 没有找到目标
+ currentState = EnergyLanceState.NoTarget;
+ Log.Message($"[BuildingEnergyLance] No targets found in range");
+ }
+ }
+
+ private void TrackTarget()
+ {
+ if (currentTarget == null || !currentTarget.Spawned)
+ {
+ // 目标丢失,重新搜索
+ currentState = EnergyLanceState.Searching;
+ currentTarget = null;
+ return;
+ }
+
+ // 检查目标是否仍然有效
+ if (!IsValidTarget(currentTarget) || !IsInRange(currentTarget.Position))
+ {
+ // 目标无效或超出范围,寻找最近的敌人
+ FindNearestTarget();
+ return;
+ }
+
+ // 更新目标位置
+ lastTargetPosition = currentTarget.Position;
+
+ // 向EnergyLance发送目标位置
+ if (activeLance != null && !activeLance.Destroyed)
+ {
+ activeLance.UpdateTarget(lastTargetPosition);
+ noTargetTicks = 0;
+
+ Log.Message($"[BuildingEnergyLance] Updated target position: {lastTargetPosition}");
+ }
+ else
+ {
+ // EnergyLance丢失,重新创建
+ CreateEnergyLance();
+ }
+ }
+
+ private void FindNearestTarget()
+ {
+ Map map = parent.Map;
+ if (map == null) return;
+
+ Pawn nearestTarget = null;
+ float nearestDistance = float.MaxValue;
+
+ // 获取当前EnergyLance位置
+ IntVec3 searchCenter = (activeLance != null && !activeLance.Destroyed) ?
+ activeLance.Position : parent.Position;
+
+ foreach (var pawn in map.mapPawns.AllPawnsSpawned)
+ {
+ if (IsValidTarget(pawn) && IsInRange(pawn.Position))
+ {
+ float distance = Vector3.Distance(searchCenter.ToVector3(), pawn.Position.ToVector3());
+ if (distance < nearestDistance)
+ {
+ nearestDistance = distance;
+ nearestTarget = pawn;
+ }
+ }
+ }
+
+ if (nearestTarget != null)
+ {
+ currentTarget = nearestTarget;
+ lastTargetPosition = currentTarget.Position;
+
+ // 更新EnergyLance目标
+ if (activeLance != null && !activeLance.Destroyed)
+ {
+ activeLance.UpdateTarget(lastTargetPosition);
+ }
+
+ Log.Message($"[BuildingEnergyLance] Switched to nearest target: {currentTarget.Label}, distance: {nearestDistance}");
+ }
+ else
+ {
+ // 没有找到替代目标
+ currentState = EnergyLanceState.NoTarget;
+ currentTarget = null;
+ Log.Message($"[BuildingEnergyLance] No alternative targets found");
+ }
+ }
+
+ private void HandleNoTarget()
+ {
+ noTargetTicks++;
+
+ // 向EnergyLance发送空位置
+ if (activeLance != null && !activeLance.Destroyed)
+ {
+ activeLance.UpdateTarget(IntVec3.Invalid);
+ }
+
+ // 检查是否应该销毁EnergyLance
+ if (noTargetTicks >= Props.maxNoTargetTicks)
+ {
+ if (activeLance != null && !activeLance.Destroyed)
+ {
+ activeLance.Destroy();
+ activeLance = null;
+ }
+
+ // 回到搜索状态
+ currentState = EnergyLanceState.Searching;
+ noTargetTicks = 0;
+
+ Log.Message($"[BuildingEnergyLance] EnergyLance destroyed due to no targets");
+ }
+ else if (noTargetTicks % 60 == 0) // 每60刻检查一次是否有新目标
+ {
+ SearchForTarget();
+ }
+ }
+
+ private void CreateEnergyLance()
+ {
+ if (Props.energyLanceDef == null)
+ {
+ Log.Error($"[BuildingEnergyLance] No energyLanceDef configured");
+ return;
+ }
+
+ try
+ {
+ // 销毁现有的EnergyLance
+ if (activeLance != null && !activeLance.Destroyed)
+ {
+ activeLance.Destroy();
+ }
+
+ // 创建新的EnergyLance
+ IntVec3 spawnPos = GetLanceSpawnPosition();
+ activeLance = (GuidedEnergyLance)GenSpawn.Spawn(Props.energyLanceDef, spawnPos, parent.Map);
+
+ // 初始化EnergyLance
+ activeLance.duration = Props.energyLanceDuration;
+ activeLance.instigator = parent;
+ activeLance.controlBuilding = this.parent;
+ activeLance.firesPerTick = Props.firesPerTick;
+
+ // 如果有当前目标,设置初始位置
+ if (currentTarget != null)
+ {
+ activeLance.UpdateTarget(currentTarget.Position);
+ }
+
+ activeLance.StartStrike();
+
+ Log.Message($"[BuildingEnergyLance] Created EnergyLance at {spawnPos}");
+ }
+ catch (System.Exception ex)
+ {
+ Log.Error($"[BuildingEnergyLance] Error creating EnergyLance: {ex}");
+ }
+ }
+
+ private IntVec3 GetLanceSpawnPosition()
+ {
+ // 在建筑周围寻找一个合适的生成位置
+ Map map = parent.Map;
+ IntVec3 center = parent.Position;
+
+ // 优先选择建筑上方的位置
+ if (center.InBounds(map) && center.Walkable(map))
+ {
+ return center;
+ }
+
+ // 如果建筑位置不可用,在周围寻找
+ foreach (IntVec3 cell in GenRadial.RadialCellsAround(center, 2f, true))
+ {
+ if (cell.InBounds(map) && cell.Walkable(map))
+ {
+ return cell;
+ }
+ }
+
+ // 如果都不可用,返回建筑位置
+ return center;
+ }
+
+ private bool IsValidTarget(Pawn pawn)
+ {
+ if (pawn == null || pawn.Downed || pawn.Dead || !pawn.Spawned)
+ return false;
+
+ // 检查目标类型
+ if (Props.targetEnemies && pawn.HostileTo(parent.Faction))
+ return true;
+
+ if (Props.targetNeutrals && !pawn.HostileTo(parent.Faction) && pawn.Faction != parent.Faction)
+ return true;
+
+ if (Props.targetAnimals && pawn.RaceProps.Animal)
+ return true;
+
+ return false;
+ }
+
+ private bool IsInRange(IntVec3 position)
+ {
+ float distance = Vector3.Distance(parent.Position.ToVector3(), position.ToVector3());
+ return distance <= Props.radius;
+ }
+
+ public override void CompTick()
+ {
+ base.CompTick();
+
+ int currentTick = Find.TickManager.TicksGame;
+
+ // 检查是否需要更新状态
+ if (currentTick >= nextUpdateTick)
+ {
+ UpdateState();
+ nextUpdateTick = currentTick + Props.updateIntervalTicks;
+ }
+
+ // 检查EnergyLance状态
+ if (activeLance != null && activeLance.Destroyed)
+ {
+ activeLance = null;
+ if (currentState == EnergyLanceState.Tracking)
+ {
+ currentState = EnergyLanceState.Searching;
+ }
+ }
+ }
+
+ // 外部调用:当EnergyLance需要目标时调用
+ public bool TryGetCurrentTarget(out IntVec3 targetPos)
+ {
+ if (currentTarget != null && IsValidTarget(currentTarget) && IsInRange(currentTarget.Position))
+ {
+ targetPos = currentTarget.Position;
+ return true;
+ }
+
+ targetPos = IntVec3.Invalid;
+ return false;
+ }
+
+ public override void PostExposeData()
+ {
+ base.PostExposeData();
+
+ Scribe_Values.Look(ref currentState, "currentState", EnergyLanceState.Idle);
+ Scribe_Values.Look(ref nextUpdateTick, "nextUpdateTick", 0);
+ Scribe_Values.Look(ref noTargetTicks, "noTargetTicks", 0);
+ Scribe_References.Look(ref currentTarget, "currentTarget");
+ Scribe_Values.Look(ref lastTargetPosition, "lastTargetPosition", IntVec3.Invalid);
+ Scribe_References.Look(ref activeLance, "activeLance");
+ }
+ }
+
+ public enum EnergyLanceState
+ {
+ Idle,
+ Searching,
+ Tracking,
+ NoTarget
+ }
+}
diff --git a/Source/WulaFallenEmpire/Ability/WULA_AbilityGuidedEnergyLance/CompProperties_BuildingEnergyLance.cs b/Source/WulaFallenEmpire/Ability/WULA_AbilityGuidedEnergyLance/CompProperties_BuildingEnergyLance.cs
new file mode 100644
index 00000000..c10e475f
--- /dev/null
+++ b/Source/WulaFallenEmpire/Ability/WULA_AbilityGuidedEnergyLance/CompProperties_BuildingEnergyLance.cs
@@ -0,0 +1,28 @@
+using RimWorld;
+using Verse;
+
+namespace WulaFallenEmpire
+{
+ public class CompProperties_BuildingEnergyLance : CompProperties
+ {
+ // 基础配置
+ public float radius = 20f; // 作用半径
+ public int updateIntervalTicks = 30; // 目标更新间隔(刻)
+ public int maxNoTargetTicks = 120; // 无目标时最大存活时间
+
+ // 目标选择配置
+ public bool targetEnemies = true; // 是否以敌人为目标
+ public bool targetNeutrals = false; // 是否以中立单位为目标
+ public bool targetAnimals = false; // 是否以动物为目标
+
+ // EnergyLance配置
+ public ThingDef energyLanceDef; // 使用的EnergyLance类型
+ public int energyLanceDuration = 600; // EnergyLance基础持续时间
+ public int firesPerTick = 3; // 每刻伤害次数
+
+ public CompProperties_BuildingEnergyLance()
+ {
+ this.compClass = typeof(CompBuildingEnergyLance);
+ }
+ }
+}
diff --git a/Source/WulaFallenEmpire/Ability/WULA_AbilityGuidedEnergyLance/GuidedEnergyLance.cs b/Source/WulaFallenEmpire/Ability/WULA_AbilityGuidedEnergyLance/GuidedEnergyLance.cs
new file mode 100644
index 00000000..150f0243
--- /dev/null
+++ b/Source/WulaFallenEmpire/Ability/WULA_AbilityGuidedEnergyLance/GuidedEnergyLance.cs
@@ -0,0 +1,255 @@
+using RimWorld;
+using Verse;
+using UnityEngine;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace WulaFallenEmpire
+{
+ public class GuidedEnergyLance : OrbitalStrike
+ {
+ // 引导相关属性
+ public Thing controlBuilding; // 控制建筑
+ private IntVec3 currentTarget = IntVec3.Invalid; // 当前目标位置
+ private int lastTargetUpdateTick = -1; // 最后收到目标更新的刻
+ private int maxNoUpdateTicks = 60; // 无更新时的最大存活时间
+
+ // 移动配置
+ public float moveSpeed = 2.0f; // 移动速度(格/秒)
+ public float turnSpeed = 90f; // 转向速度(度/秒)
+
+ // 状态
+ private Vector3 currentVelocity;
+ private bool hasValidTarget = false;
+
+ // ModExtension引用
+ private EnergyLanceExtension extension;
+
+ private static List tmpThings = new List();
+
+ public override void StartStrike()
+ {
+ base.StartStrike();
+
+ // 获取ModExtension
+ extension = def.GetModExtension();
+ if (extension == null)
+ {
+ Log.Error($"[GuidedEnergyLance] No EnergyLanceExtension found on {def.defName}");
+ return;
+ }
+
+ lastTargetUpdateTick = Find.TickManager.TicksGame;
+
+ // 创建视觉效果
+ CreateVisualEffect();
+
+ Log.Message($"[GuidedEnergyLance] Guided EnergyLance started, controlled by {controlBuilding?.Label ?? "None"}");
+ }
+
+ private void CreateVisualEffect()
+ {
+ // 使用ModExtension中定义的Mote,如果没有则使用默认的PowerBeam
+ if (extension.moteDef != null)
+ {
+ Mote mote = MoteMaker.MakeStaticMote(base.Position, base.Map, extension.moteDef);
+ }
+ else
+ {
+ // 使用原版PowerBeam的视觉效果
+ MoteMaker.MakePowerBeamMote(base.Position, base.Map);
+ }
+ }
+
+ protected override void Tick()
+ {
+ base.Tick();
+
+ if (!base.Destroyed && extension != null)
+ {
+ // 检查目标更新状态
+ CheckTargetStatus();
+
+ // 移动光束
+ MoveBeam();
+
+ // 造成伤害
+ for (int i = 0; i < firesPerTick; i++)
+ {
+ DoBeamDamage();
+ }
+ }
+ }
+
+ private void CheckTargetStatus()
+ {
+ int currentTick = Find.TickManager.TicksGame;
+ int ticksSinceUpdate = currentTick - lastTargetUpdateTick;
+
+ // 检查是否长时间未收到更新
+ if (ticksSinceUpdate >= maxNoUpdateTicks)
+ {
+ Log.Message($"[GuidedEnergyLance] No target updates for {ticksSinceUpdate} ticks, destroying");
+ Destroy();
+ return;
+ }
+
+ // 检查控制建筑状态
+ if (controlBuilding == null || controlBuilding.Destroyed || !controlBuilding.Spawned)
+ {
+ Log.Message($"[GuidedEnergyLance] Control building lost, destroying");
+ Destroy();
+ return;
+ }
+ }
+
+ private void MoveBeam()
+ {
+ if (!hasValidTarget || !currentTarget.IsValid)
+ {
+ // 没有有效目标,缓慢移动或保持原地
+ ApplyMinimalMovement();
+ return;
+ }
+
+ // 计算移动方向
+ Vector3 targetDirection = (currentTarget.ToVector3() - base.Position.ToVector3()).normalized;
+
+ // 平滑转向
+ if (currentVelocity.magnitude > 0.1f)
+ {
+ float maxTurnAngle = turnSpeed * 0.0167f; // 每帧最大转向角度(假设60FPS)
+ currentVelocity = Vector3.RotateTowards(currentVelocity, targetDirection, maxTurnAngle * Mathf.Deg2Rad, moveSpeed * 0.0167f);
+ }
+ else
+ {
+ currentVelocity = targetDirection * moveSpeed * 0.0167f;
+ }
+
+ // 应用移动
+ Vector3 newPos = base.Position.ToVector3() + currentVelocity;
+ IntVec3 newCell = new IntVec3(Mathf.RoundToInt(newPos.x), 0, Mathf.RoundToInt(newPos.z));
+
+ if (newCell != base.Position && newCell.InBounds(base.Map))
+ {
+ base.Position = newCell;
+ }
+
+ // 检查是否接近目标
+ float distanceToTarget = Vector3.Distance(base.Position.ToVector3(), currentTarget.ToVector3());
+ if (distanceToTarget < 1.5f)
+ {
+ // 到达目标附近,可以减速或保持位置
+ currentVelocity *= 0.8f;
+ }
+
+ Log.Message($"[GuidedEnergyLance] Moving to {currentTarget}, distance: {distanceToTarget:F1}");
+ }
+
+ private void ApplyMinimalMovement()
+ {
+ // 无目标时的最小移动,防止完全静止
+ if (currentVelocity.magnitude < 0.1f)
+ {
+ // 随机轻微移动
+ currentVelocity = new Vector3(Rand.Range(-0.1f, 0.1f), 0f, Rand.Range(-0.1f, 0.1f));
+ }
+ else
+ {
+ // 缓慢减速
+ currentVelocity *= 0.95f;
+ }
+
+ Vector3 newPos = base.Position.ToVector3() + currentVelocity;
+ IntVec3 newCell = new IntVec3(Mathf.RoundToInt(newPos.x), 0, Mathf.RoundToInt(newPos.z));
+
+ if (newCell != base.Position && newCell.InBounds(base.Map))
+ {
+ base.Position = newCell;
+ }
+ }
+
+ private void DoBeamDamage()
+ {
+ if (extension == null) return;
+
+ // 在当前光束位置周围随机选择一个单元格
+ IntVec3 targetCell = (from x in GenRadial.RadialCellsAround(base.Position, 2f, useCenter: true)
+ where x.InBounds(base.Map)
+ select x).RandomElementByWeight((IntVec3 x) => 1f - Mathf.Min(x.DistanceTo(base.Position) / 2f, 1f) + 0.05f);
+
+ // 尝试在该单元格点火(如果配置了点火)
+ if (extension.igniteFires)
+ {
+ FireUtility.TryStartFireIn(targetCell, base.Map, Rand.Range(0.1f, extension.fireIgniteChance), instigator);
+ }
+
+ // 对该单元格内的物体造成伤害
+ tmpThings.Clear();
+ tmpThings.AddRange(targetCell.GetThingList(base.Map));
+
+ for (int i = 0; i < tmpThings.Count; i++)
+ {
+ Thing thing = tmpThings[i];
+
+ // 检查是否对尸体造成伤害
+ if (!extension.applyDamageToCorpses && thing is Corpse)
+ continue;
+
+ // 计算伤害量
+ int damageAmount = (thing is Corpse) ?
+ extension.corpseDamageAmountRange.RandomInRange :
+ extension.damageAmountRange.RandomInRange;
+
+ Pawn pawn = thing as Pawn;
+ BattleLogEntry_DamageTaken battleLogEntry = null;
+
+ if (pawn != null)
+ {
+ battleLogEntry = new BattleLogEntry_DamageTaken(pawn, RulePackDefOf.DamageEvent_PowerBeam, instigator as Pawn);
+ Find.BattleLog.Add(battleLogEntry);
+ }
+
+ // 使用ModExtension中定义的伤害类型
+ DamageInfo damageInfo = new DamageInfo(extension.damageDef, damageAmount, 0f, -1f, instigator, null, weaponDef);
+ thing.TakeDamage(damageInfo).AssociateWithLog(battleLogEntry);
+ }
+
+ tmpThings.Clear();
+ }
+
+ // 外部调用:更新目标位置
+ public void UpdateTarget(IntVec3 newTarget)
+ {
+ lastTargetUpdateTick = Find.TickManager.TicksGame;
+
+ if (newTarget.IsValid && newTarget.InBounds(base.Map))
+ {
+ currentTarget = newTarget;
+ hasValidTarget = true;
+
+ Log.Message($"[GuidedEnergyLance] Target updated to {newTarget}");
+ }
+ else
+ {
+ hasValidTarget = false;
+ currentTarget = IntVec3.Invalid;
+
+ Log.Message($"[GuidedEnergyLance] Target cleared");
+ }
+ }
+
+ public override void ExposeData()
+ {
+ base.ExposeData();
+
+ Scribe_References.Look(ref controlBuilding, "controlBuilding");
+ Scribe_Values.Look(ref currentTarget, "currentTarget", IntVec3.Invalid);
+ Scribe_Values.Look(ref lastTargetUpdateTick, "lastTargetUpdateTick", -1);
+ Scribe_Values.Look(ref maxNoUpdateTicks, "maxNoUpdateTicks", 60);
+ Scribe_Values.Look(ref moveSpeed, "moveSpeed", 2.0f);
+ Scribe_Values.Look(ref turnSpeed, "turnSpeed", 90f);
+ Scribe_Values.Look(ref firesPerTick, "firesPerTick", 3);
+ }
+ }
+}
diff --git a/Source/WulaFallenEmpire/Ability/WULA_AbilitySurveillanceBombardment/CompAbilityEffect_CallSkyfaller.cs b/Source/WulaFallenEmpire/Ability/WULA_AbilitySurveillanceBombardment/CompAbilityEffect_CallSkyfaller.cs
new file mode 100644
index 00000000..887e9ba7
--- /dev/null
+++ b/Source/WulaFallenEmpire/Ability/WULA_AbilitySurveillanceBombardment/CompAbilityEffect_CallSkyfaller.cs
@@ -0,0 +1,83 @@
+using RimWorld;
+using Verse;
+using UnityEngine;
+using System.Collections.Generic;
+
+namespace WulaFallenEmpire
+{
+ public class CompAbilityEffect_CallSkyfaller : CompAbilityEffect
+ {
+ public new CompProperties_AbilityCallSkyfaller Props => (CompProperties_AbilityCallSkyfaller)props;
+
+ public override void Apply(LocalTargetInfo target, LocalTargetInfo dest)
+ {
+ base.Apply(target, dest);
+
+ if (parent.pawn == null || parent.pawn.Map == null || !target.IsValid)
+ return;
+
+ try
+ {
+ // 创建延时召唤
+ CallSkyfallerDelayed(target.Cell);
+
+ Log.Message($"[CallSkyfaller] Scheduled skyfaller at {target.Cell} with {Props.delayTicks} ticks delay");
+ }
+ catch (System.Exception ex)
+ {
+ Log.Error($"[CallSkyfaller] Error calling skyfaller: {ex}");
+ }
+ }
+
+ private void CallSkyfallerDelayed(IntVec3 targetCell)
+ {
+ // 使用延时动作来召唤skyfaller
+ parent.pawn.Map.GetComponent()?
+ .ScheduleSkyfaller(Props.skyfallerDef, targetCell, Props.delayTicks, parent.pawn);
+ }
+
+ // 绘制预览效果
+ public override void DrawEffectPreview(LocalTargetInfo target)
+ {
+ base.DrawEffectPreview(target);
+
+ if (parent.pawn == null || parent.pawn.Map == null || !target.IsValid)
+ return;
+
+ try
+ {
+ // 绘制圆形预览区域
+ DrawCircularPreview(target.Cell);
+ }
+ catch (System.Exception)
+ {
+ // 忽略预览绘制错误
+ }
+ }
+
+ private void DrawCircularPreview(IntVec3 center)
+ {
+ Map map = parent.pawn.Map;
+
+ // 获取圆形区域内的所有单元格
+ var previewCells = GenRadial.RadialCellsAround(center, Props.previewRadius, true);
+
+ // 绘制预览区域
+ foreach (var cell in previewCells)
+ {
+ if (cell.InBounds(map))
+ {
+ GenDraw.DrawFieldEdges(new List { cell }, Props.previewColor, 0.2f);
+ }
+ }
+
+ // 绘制目标点高亮
+ GenDraw.DrawTargetHighlight(center);
+ }
+
+ public override string ExtraLabelMouseAttachment(LocalTargetInfo target)
+ {
+ return $"召唤空投舱: {Props.delayTicks}刻后到达";
+ }
+ }
+}
diff --git a/Source/WulaFallenEmpire/Ability/WULA_AbilitySurveillanceBombardment/CompBuildingBombardment.cs b/Source/WulaFallenEmpire/Ability/WULA_AbilitySurveillanceBombardment/CompBuildingBombardment.cs
new file mode 100644
index 00000000..2717eba6
--- /dev/null
+++ b/Source/WulaFallenEmpire/Ability/WULA_AbilitySurveillanceBombardment/CompBuildingBombardment.cs
@@ -0,0 +1,234 @@
+using RimWorld;
+using Verse;
+using UnityEngine;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace WulaFallenEmpire
+{
+ public class CompBuildingBombardment : ThingComp
+ {
+ public CompProperties_BuildingBombardment Props => (CompProperties_BuildingBombardment)props;
+
+ // 轰炸状态
+ private BuildingBombardmentState currentState = BuildingBombardmentState.Idle;
+ private int nextBurstTick = 0;
+ private int currentBurstCount = 0;
+ private int nextInnerBurstTick = 0;
+ private List currentTargets = new List();
+
+ public override void PostSpawnSetup(bool respawningAfterLoad)
+ {
+ base.PostSpawnSetup(respawningAfterLoad);
+
+ if (!respawningAfterLoad)
+ {
+ // 新生成时立即开始第一轮轰炸
+ StartNextBurst();
+ }
+ }
+
+ private void StartNextBurst()
+ {
+ currentState = BuildingBombardmentState.Targeting;
+ currentBurstCount = 0;
+ currentTargets.Clear();
+
+ // 选择目标
+ SelectTargets();
+
+ if (currentTargets.Count > 0)
+ {
+ currentState = BuildingBombardmentState.Bursting;
+ nextInnerBurstTick = Find.TickManager.TicksGame;
+ Log.Message($"[BuildingBombardment] Starting burst with {currentTargets.Count} targets");
+ }
+ else
+ {
+ // 没有找到目标,等待下一轮
+ currentState = BuildingBombardmentState.Idle;
+ nextBurstTick = Find.TickManager.TicksGame + Props.burstIntervalTicks;
+ Log.Message($"[BuildingBombardment] No targets found, waiting for next burst");
+ }
+ }
+
+ private void SelectTargets()
+ {
+ Map map = parent.Map;
+ if (map == null) return;
+
+ // 获取范围内的所有pawn
+ var potentialTargets = new List();
+
+ foreach (var pawn in map.mapPawns.AllPawnsSpawned)
+ {
+ if (IsValidTarget(pawn) && IsInRange(pawn.Position))
+ {
+ potentialTargets.Add(pawn);
+ }
+ }
+
+ // 随机选择目标,最多burstCount个
+ int targetCount = Mathf.Min(Props.burstCount, potentialTargets.Count);
+ currentTargets = potentialTargets
+ .InRandomOrder()
+ .Take(targetCount)
+ .Select(p => new LocalTargetInfo(p))
+ .ToList();
+ }
+
+ private bool IsValidTarget(Pawn pawn)
+ {
+ if (pawn == null || pawn.Downed || pawn.Dead) return false;
+
+ // 检查目标类型
+ if (Props.targetEnemies && pawn.HostileTo(parent.Faction))
+ return true;
+
+ if (Props.targetNeutrals && !pawn.HostileTo(parent.Faction) && pawn.Faction != parent.Faction)
+ return true;
+
+ if (Props.targetAnimals && pawn.RaceProps.Animal)
+ return true;
+
+ return false;
+ }
+
+ private bool IsInRange(IntVec3 position)
+ {
+ float distance = Vector3.Distance(parent.Position.ToVector3(), position.ToVector3());
+ return distance <= Props.radius;
+ }
+
+ private void UpdateBursting()
+ {
+ if (Find.TickManager.TicksGame < nextInnerBurstTick)
+ return;
+
+ if (currentBurstCount >= currentTargets.Count)
+ {
+ // 当前组发射完毕
+ currentState = BuildingBombardmentState.Idle;
+ nextBurstTick = Find.TickManager.TicksGame + Props.burstIntervalTicks;
+ Log.Message($"[BuildingBombardment] Burst completed, waiting for next burst");
+ return;
+ }
+
+ // 发射当前目标
+ var target = currentTargets[currentBurstCount];
+ LaunchBombardment(target);
+ currentBurstCount++;
+
+ // 设置下一个组内发射时间
+ if (currentBurstCount < currentTargets.Count)
+ {
+ nextInnerBurstTick = Find.TickManager.TicksGame + Props.innerBurstIntervalTicks;
+ }
+
+ Log.Message($"[BuildingBombardment] Launched bombardment {currentBurstCount}/{currentTargets.Count}");
+ }
+
+ private void LaunchBombardment(LocalTargetInfo target)
+ {
+ try
+ {
+ // 应用随机偏移
+ IntVec3 targetCell = ApplyRandomOffset(target.Cell);
+
+ if (Props.skyfallerDef != null)
+ {
+ // 使用 Skyfaller
+ Skyfaller skyfaller = SkyfallerMaker.MakeSkyfaller(Props.skyfallerDef);
+ GenSpawn.Spawn(skyfaller, targetCell, parent.Map);
+ }
+ else if (Props.projectileDef != null)
+ {
+ // 使用抛射体作为备用
+ LaunchProjectileAt(targetCell);
+ }
+ }
+ catch (System.Exception ex)
+ {
+ Log.Error($"[BuildingBombardment] Error launching bombardment: {ex}");
+ }
+ }
+
+ private IntVec3 ApplyRandomOffset(IntVec3 originalCell)
+ {
+ if (Props.randomOffset <= 0f)
+ return originalCell;
+
+ // 在随机偏移范围内选择一个位置
+ float offsetX = Rand.Range(-Props.randomOffset, Props.randomOffset);
+ float offsetZ = Rand.Range(-Props.randomOffset, Props.randomOffset);
+
+ IntVec3 offsetCell = new IntVec3(
+ Mathf.RoundToInt(originalCell.x + offsetX),
+ originalCell.y,
+ Mathf.RoundToInt(originalCell.z + offsetZ)
+ );
+
+ // 确保位置有效
+ if (offsetCell.InBounds(parent.Map))
+ return offsetCell;
+ else
+ return originalCell;
+ }
+
+ private void LaunchProjectileAt(IntVec3 targetCell)
+ {
+ // 从建筑位置发射抛射体
+ Vector3 spawnPos = parent.Position.ToVector3Shifted();
+
+ Projectile projectile = (Projectile)GenSpawn.Spawn(Props.projectileDef, parent.Position, parent.Map);
+ if (projectile != null)
+ {
+ projectile.Launch(
+ parent,
+ spawnPos,
+ new LocalTargetInfo(targetCell),
+ new LocalTargetInfo(targetCell),
+ ProjectileHitFlags.All,
+ false
+ );
+ }
+ }
+
+ public override void CompTick()
+ {
+ base.CompTick();
+
+ switch (currentState)
+ {
+ case BuildingBombardmentState.Idle:
+ if (Find.TickManager.TicksGame >= nextBurstTick)
+ {
+ StartNextBurst();
+ }
+ break;
+
+ case BuildingBombardmentState.Bursting:
+ UpdateBursting();
+ break;
+ }
+ }
+
+ public override void PostExposeData()
+ {
+ base.PostExposeData();
+
+ Scribe_Values.Look(ref currentState, "currentState", BuildingBombardmentState.Idle);
+ Scribe_Values.Look(ref nextBurstTick, "nextBurstTick", 0);
+ Scribe_Values.Look(ref currentBurstCount, "currentBurstCount", 0);
+ Scribe_Values.Look(ref nextInnerBurstTick, "nextInnerBurstTick", 0);
+ Scribe_Collections.Look(ref currentTargets, "currentTargets", LookMode.LocalTargetInfo);
+ }
+ }
+
+ public enum BuildingBombardmentState
+ {
+ Idle,
+ Targeting,
+ Bursting
+ }
+}
diff --git a/Source/WulaFallenEmpire/Ability/WULA_AbilitySurveillanceBombardment/CompProperties_AbilityCallSkyfaller.cs b/Source/WulaFallenEmpire/Ability/WULA_AbilitySurveillanceBombardment/CompProperties_AbilityCallSkyfaller.cs
new file mode 100644
index 00000000..74b473b1
--- /dev/null
+++ b/Source/WulaFallenEmpire/Ability/WULA_AbilitySurveillanceBombardment/CompProperties_AbilityCallSkyfaller.cs
@@ -0,0 +1,22 @@
+using RimWorld;
+using Verse;
+using UnityEngine;
+
+namespace WulaFallenEmpire
+{
+ public class CompProperties_AbilityCallSkyfaller : CompProperties_AbilityEffect
+ {
+ // 基础配置
+ public int delayTicks = 120; // 延时(刻)
+ public ThingDef skyfallerDef; // 使用的 Skyfaller
+
+ // 预览配置
+ public float previewRadius = 5f; // 预览半径
+ public Color previewColor = new Color(1f, 0.5f, 0.1f, 0.3f); // 预览颜色
+
+ public CompProperties_AbilityCallSkyfaller()
+ {
+ this.compClass = typeof(CompAbilityEffect_CallSkyfaller);
+ }
+ }
+}
diff --git a/Source/WulaFallenEmpire/Ability/WULA_AbilitySurveillanceBombardment/CompProperties_BuildingBombardment.cs b/Source/WulaFallenEmpire/Ability/WULA_AbilitySurveillanceBombardment/CompProperties_BuildingBombardment.cs
new file mode 100644
index 00000000..18c5a0b6
--- /dev/null
+++ b/Source/WulaFallenEmpire/Ability/WULA_AbilitySurveillanceBombardment/CompProperties_BuildingBombardment.cs
@@ -0,0 +1,31 @@
+using RimWorld;
+using Verse;
+
+namespace WulaFallenEmpire
+{
+ public class CompProperties_BuildingBombardment : CompProperties
+ {
+ // 轰炸区域配置
+ public float radius = 15f; // 作用半径
+
+ // 目标选择配置
+ public bool targetEnemies = true; // 是否以敌人为目标
+ public bool targetNeutrals = false; // 是否以中立单位为目标
+ public bool targetAnimals = false; // 是否以动物为目标
+
+ // 召唤配置
+ public int burstCount = 3; // 单组召唤数量
+ public int innerBurstIntervalTicks = 10; // 同组召唤间隔
+ public int burstIntervalTicks = 60; // 组间召唤间隔
+ public float randomOffset = 2f; // 随机偏移量
+
+ // Skyfaller 配置
+ public ThingDef skyfallerDef; // 使用的 Skyfaller
+ public ThingDef projectileDef; // 备用的抛射体定义
+
+ public CompProperties_BuildingBombardment()
+ {
+ this.compClass = typeof(CompBuildingBombardment);
+ }
+ }
+}
diff --git a/Source/WulaFallenEmpire/Ability/WULA_AbilitySurveillanceBombardment/MapComponent_SkyfallerDelayed.cs b/Source/WulaFallenEmpire/Ability/WULA_AbilitySurveillanceBombardment/MapComponent_SkyfallerDelayed.cs
new file mode 100644
index 00000000..d2d1573a
--- /dev/null
+++ b/Source/WulaFallenEmpire/Ability/WULA_AbilitySurveillanceBombardment/MapComponent_SkyfallerDelayed.cs
@@ -0,0 +1,81 @@
+using RimWorld;
+using Verse;
+using System.Collections.Generic;
+
+namespace WulaFallenEmpire
+{
+ public class MapComponent_SkyfallerDelayed : MapComponent
+ {
+ private List scheduledSkyfallers = new List();
+
+ public MapComponent_SkyfallerDelayed(Map map) : base(map) { }
+
+ public void ScheduleSkyfaller(ThingDef skyfallerDef, IntVec3 targetCell, int delayTicks, Pawn caster = null)
+ {
+ scheduledSkyfallers.Add(new DelayedSkyfaller
+ {
+ skyfallerDef = skyfallerDef,
+ targetCell = targetCell,
+ spawnTick = Find.TickManager.TicksGame + delayTicks,
+ caster = caster
+ });
+ }
+
+ public override void MapComponentTick()
+ {
+ base.MapComponentTick();
+
+ int currentTick = Find.TickManager.TicksGame;
+
+ // 检查并执行到期的召唤
+ for (int i = scheduledSkyfallers.Count - 1; i >= 0; i--)
+ {
+ var skyfaller = scheduledSkyfallers[i];
+ if (currentTick >= skyfaller.spawnTick)
+ {
+ SpawnSkyfaller(skyfaller);
+ scheduledSkyfallers.RemoveAt(i);
+ }
+ }
+ }
+
+ private void SpawnSkyfaller(DelayedSkyfaller delayedSkyfaller)
+ {
+ try
+ {
+ if (delayedSkyfaller.skyfallerDef != null && delayedSkyfaller.targetCell.IsValid && delayedSkyfaller.targetCell.InBounds(map))
+ {
+ Skyfaller skyfaller = SkyfallerMaker.MakeSkyfaller(delayedSkyfaller.skyfallerDef);
+ GenSpawn.Spawn(skyfaller, delayedSkyfaller.targetCell, map);
+ Log.Message($"[DelayedSkyfaller] Spawned skyfaller at {delayedSkyfaller.targetCell}");
+ }
+ }
+ catch (System.Exception ex)
+ {
+ Log.Error($"[DelayedSkyfaller] Error spawning skyfaller: {ex}");
+ }
+ }
+
+ public override void ExposeData()
+ {
+ base.ExposeData();
+ Scribe_Collections.Look(ref scheduledSkyfallers, "scheduledSkyfallers", LookMode.Deep);
+ }
+ }
+
+ public class DelayedSkyfaller : IExposable
+ {
+ public ThingDef skyfallerDef;
+ public IntVec3 targetCell;
+ public int spawnTick;
+ public Pawn caster;
+
+ public void ExposeData()
+ {
+ Scribe_Defs.Look(ref skyfallerDef, "skyfallerDef");
+ Scribe_Values.Look(ref targetCell, "targetCell");
+ Scribe_Values.Look(ref spawnTick, "spawnTick");
+ Scribe_References.Look(ref caster, "caster");
+ }
+ }
+}
diff --git a/Source/WulaFallenEmpire/WulaFallenEmpire.csproj b/Source/WulaFallenEmpire/WulaFallenEmpire.csproj
index 6a5bbb29..34950f92 100644
--- a/Source/WulaFallenEmpire/WulaFallenEmpire.csproj
+++ b/Source/WulaFallenEmpire/WulaFallenEmpire.csproj
@@ -74,6 +74,17 @@
+
+
+
+
+
+
+
+
+
+
+