This commit is contained in:
2025-11-06 17:31:49 +08:00
parent d8a88ddfd5
commit 234fb427aa
11 changed files with 902 additions and 1206 deletions

View File

@@ -42,7 +42,7 @@
<AbilityDef>
<defName>WULA_Spawn_Fighter_Rocket</defName>
<label>战机巡航</label>
<description>指挥乌拉帝国的机,中速掠过战场,使用其导弹对目标区域发起打击。\n\n在飞行期间它会使用磁轨炮打击遇到的敌人。</description>
<description>指挥乌拉帝国的攻击机,中速掠过战场,使用其导弹对目标区域发起打击。\n\n在飞行期间它会使用磁轨炮打击遇到的敌人。</description>
<!-- <iconPath>ArachnaeSwarm/UI/Abilities/ARA_Spawn_ARA_HiveCorvette_Rocket</iconPath> -->
<cooldownTicksRange>1</cooldownTicksRange>
<hotKey>Misc12</hotKey>
@@ -93,6 +93,11 @@
<showSectorPreview>true</showSectorPreview>
<sectorPreviewColor>(0.3,0.7,1.0,0.3)</sectorPreviewColor>
</li>
<li Class="WulaFallenEmpire.CompProperties_RequireFlyOverFacility">
<!-- <flyOverDef></flyOverDef> -->
<requiredFacility>HarborFacility</requiredFacility>
<facilityNotFoundMessage>需要拥有&lt;color=#BD952F>&lt;i>机库&lt;/i>&lt;/color>设施的战舰在地图上才能发起空袭</facilityNotFoundMessage>
</li>
</comps>
</AbilityDef>
<AbilityDef>
@@ -136,6 +141,54 @@
<showSectorPreview>true</showSectorPreview>
<sectorPreviewColor>(0.3,0.7,1.0,0.3)</sectorPreviewColor>
</li>
<li Class="WulaFallenEmpire.CompProperties_RequireFlyOverFacility">
<!-- <flyOverDef></flyOverDef> -->
<requiredFacility>HarborFacility</requiredFacility>
<facilityNotFoundMessage>需要拥有&lt;color=#BD952F>&lt;i>机库&lt;/i>&lt;/color>设施的战舰在地图上才能发起空袭</facilityNotFoundMessage>
</li>
</comps>
</AbilityDef>
<AbilityDef>
<defName>WULA_Spawn_ShipChunkIncoming</defName>
<label>战舰轰炸</label>
<description>我炸死你</description>
<!-- <iconPath>ArachnaeSwarm/UI/Abilities/ARA_Spawn_ARA_HiveCorvette_Rocket</iconPath> -->
<cooldownTicksRange>1</cooldownTicksRange>
<hotKey>Misc12</hotKey>
<casterMustBeCapableOfViolence>false</casterMustBeCapableOfViolence>
<verbProperties>
<verbClass>Verb_CastAbility</verbClass>
<drawAimPie>false</drawAimPie>
<requireLineOfSight>false</requireLineOfSight>
<warmupTime>1</warmupTime>
<range>120</range>
<targetable>true</targetable>
<targetParams>
<canTargetSelf>false</canTargetSelf>
<canTargetLocations>true</canTargetLocations>
</targetParams>
</verbProperties>
<comps>
<li Class="WulaFallenEmpire.CompProperties_AbilityBombardment">
<bombardmentWidth>5</bombardmentWidth>
<bombardmentLength>35</bombardmentLength>
<targetSelectionChance>0.6</targetSelectionChance>
<minTargetCells>25</minTargetCells>
<maxTargetCells>30</maxTargetCells>
<warmupTicks>120</warmupTicks>
<rowDelayTicks>5</rowDelayTicks>
<impactDelayTicks>2</impactDelayTicks>
<showBombardmentArea>true</showBombardmentArea>
<effecterScale>1.5</effecterScale>
<areaPreviewColor>1.0,0.3,0.1,0.3</areaPreviewColor>
<skyfallerDef>ShipChunkIncoming</skyfallerDef>
<randomBombardmentDirection>true</randomBombardmentDirection>
</li>
<li Class="WulaFallenEmpire.CompProperties_RequireFlyOverFacility">
<!-- <flyOverDef></flyOverDef> -->
<requiredFacility>BombardmentFacility</requiredFacility>
<facilityNotFoundMessage>需要拥有&lt;color=#BD2F31>&lt;i>武器阵列&lt;/i>&lt;/color>设施的战舰在地图上才能进行轨道炮击支援</facilityNotFoundMessage>
</li>
</comps>
</AbilityDef>
</Defs>

View File

@@ -8,7 +8,8 @@
<titleShort>科鲁普</titleShort>
<description>[PAWN_nameDef] 是产自科鲁普星区的合成人。这片星区是一个四战之地——由于其地处边缘且地缘政治复杂,乌拉帝国从未真正掌控这片星区,但是此处优秀的稀有水晶和异星天然气使得帝国不愿放弃此星区的掌控。\n\n在此处生产的合成人使用的战略物资远高于其他星区这使得其拥有更高的机体耐久和更低的能量消耗并且内置了一定的战斗和手工能力。</description>
<skillGains>
<Shooting>3</Shooting>
<Shooting>4</Shooting>
<Crafting>2</Crafting>
</skillGains>
<spawnCategories>
<li>Wula_Backstory_Categories</li>
@@ -46,8 +47,8 @@
<titleShort>代达罗斯</titleShort>
<description>[PAWN_nameDef] 是产自代达罗斯星区的合成人。这片星区是乌拉帝国靠近边缘世界的一个工厂星域,无数供给乌拉舰队之用的高质量合金从此处诞生,是诸工业区搏动的心脏。\n\n从此生产的合成人配置了强大的生产系统使得它们可以抵消此型合成人在采矿和建造上的天然劣势。</description>
<skillGains>
<Shooting>3</Shooting>
<Melee>3</Melee>
<Mining>5</Mining>
<Construction>3</Construction>
</skillGains>
<spawnCategories>
<li>Wula_Backstory_Categories</li>

View File

@@ -2,7 +2,7 @@
<Defs>
<ThingDef ParentName="EtherealThingBase">
<defName>WULA_BattleShip</defName>
<label>乌拉帝国</label>
<label>乌拉帝国</label>
<thingClass>WulaFallenEmpire.FlyOver</thingClass>
<tickerType>Normal</tickerType>
<drawerType>RealtimeOnly</drawerType>
@@ -59,6 +59,14 @@
<flyOverType>WULA_Flyover_BaseBuilder</flyOverType>
<isRequiredForDrop>true</isRequiredForDrop>
</li>
<li Class="WulaFallenEmpire.CompProperties_FlyOverFacilities">
<availableFacilities>
<li>BombardmentFacility</li>
<li>HarborFacility</li>
<li>FactoryFacility</li>
<li>BuildingdropperFacility</li>
</availableFacilities>
</li>
</comps>
</ThingDef>
<ThingDef Name="WULA_Fighter_Base" ParentName="EtherealThingBase" Abstract="True">

View File

@@ -0,0 +1,531 @@
// CompAbilityEffect_Bombardment.cs
using RimWorld;
using Verse;
using UnityEngine;
using System.Collections.Generic;
using System.Linq;
namespace WulaFallenEmpire
{
public class CompAbilityEffect_Bombardment : CompAbilityEffect
{
public new CompProperties_AbilityBombardment Props => (CompProperties_AbilityBombardment)props;
// 轰炸状态
private BombardmentState currentState = BombardmentState.Idle;
private List<IntVec3> targetCells = new List<IntVec3>();
private List<BombardmentRow> bombardmentRows = new List<BombardmentRow>();
private IntVec3 bombardmentCenter;
private Vector3 bombardmentDirection; // 轰炸前进方向(长度方向)
private int currentRowIndex = 0;
private int currentCellIndex = 0;
private int warmupTicksRemaining = 0;
private int nextBombardmentTick = 0;
// 视觉效果
private Effecter areaEffecter;
// 预览状态
private List<IntVec3> currentPreviewCells = new List<IntVec3>();
public override void Apply(LocalTargetInfo target, LocalTargetInfo dest)
{
base.Apply(target, dest);
if (parent.pawn == null || parent.pawn.Map == null)
return;
try
{
Log.Message($"[Bombardment] Starting bombardment at {target.Cell}");
// 计算轰炸区域和方向
CalculateBombardmentArea(target.Cell);
// 选择目标格子
SelectTargetCells();
// 组织成排
OrganizeTargetCellsIntoRows();
// 开始前摇
StartWarmup();
Log.Message($"[Bombardment] Bombardment initialized: {targetCells.Count} targets in {bombardmentRows.Count} rows");
}
catch (System.Exception ex)
{
Log.Error($"[Bombardment] 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
{
// 动态计算轰炸区域
CalculateDynamicBombardmentArea(target.Cell);
// 绘制轰炸区域预览
DrawBombardmentAreaPreview(target.Cell);
}
catch (System.Exception)
{
// 忽略预览绘制错误
}
}
// 修复:动态计算轰炸区域,确保矩形长边垂直于施法者-目标连线
private void CalculateDynamicBombardmentArea(IntVec3 targetCell)
{
Map map = parent.pawn.Map;
// 计算施法者到目标的方向
Vector3 casterToTarget = (targetCell.ToVector3() - parent.pawn.Position.ToVector3()).normalized;
// 如果方向为零向量,使用默认方向
if (casterToTarget == Vector3.zero)
{
casterToTarget = Vector3.forward;
}
// 修复:计算垂直于施法者-目标连线的方向(作为轰炸区域的长边方向)
Vector3 perpendicularDirection = new Vector3(-casterToTarget.z, 0, casterToTarget.x).normalized;
// 计算轰炸区域的所有单元格(使用正确的方向)
currentPreviewCells = CalculateBombardmentAreaCells(targetCell, perpendicularDirection, casterToTarget);
}
// 绘制轰炸区域预览
private void DrawBombardmentAreaPreview(IntVec3 targetCell)
{
Map map = parent.pawn.Map;
// 绘制轰炸区域内部的单元格
foreach (var cell in currentPreviewCells)
{
if (cell.InBounds(map))
{
GenDraw.DrawFieldEdges(new List<IntVec3> { cell }, Props.areaPreviewColor, 0.4f);
}
}
// 绘制轰炸区域边界
DrawBombardmentBoundaries(targetCell);
}
// 修复:绘制轰炸区域边界,确保矩形长边垂直于施法者-目标连线
private void DrawBombardmentBoundaries(IntVec3 targetCell)
{
Map map = parent.pawn.Map;
// 计算施法者到目标的方向
Vector3 casterToTarget = (targetCell.ToVector3() - parent.pawn.Position.ToVector3()).normalized;
// 如果方向为零向量,使用默认方向
if (casterToTarget == Vector3.zero)
{
casterToTarget = Vector3.forward;
}
// 修复:计算垂直于施法者-目标连线的方向(作为轰炸区域的长边方向)
Vector3 lengthDirection = new Vector3(-casterToTarget.z, 0, casterToTarget.x).normalized;
Vector3 widthDirection = casterToTarget; // 宽度方向沿着施法者-目标连线
// 计算轰炸区域的四个角
Vector3 targetCenter = targetCell.ToVector3();
float halfWidth = Props.bombardmentWidth * 0.5f;
float halfLength = Props.bombardmentLength * 0.5f;
Vector3 startLeft = targetCenter - lengthDirection * halfLength + widthDirection * halfWidth;
Vector3 startRight = targetCenter - lengthDirection * halfLength - widthDirection * halfWidth;
Vector3 endLeft = targetCenter + lengthDirection * halfLength + widthDirection * halfWidth;
Vector3 endRight = targetCenter + lengthDirection * halfLength - widthDirection * halfWidth;
// 转换为 IntVec3 并确保在地图范围内
IntVec3 startLeftCell = GetSafeMapPosition(new IntVec3((int)startLeft.x, (int)startLeft.y, (int)startLeft.z), map);
IntVec3 startRightCell = GetSafeMapPosition(new IntVec3((int)startRight.x, (int)startRight.y, (int)startRight.z), map);
IntVec3 endLeftCell = GetSafeMapPosition(new IntVec3((int)endLeft.x, (int)endLeft.y, (int)endLeft.z), map);
IntVec3 endRightCell = GetSafeMapPosition(new IntVec3((int)endRight.x, (int)endRight.y, (int)endRight.z), map);
// 绘制边界线
if (startLeftCell.InBounds(map) && endLeftCell.InBounds(map))
GenDraw.DrawLineBetween(startLeftCell.ToVector3Shifted(), endLeftCell.ToVector3Shifted(), SimpleColor.Red, 0.2f);
if (startRightCell.InBounds(map) && endRightCell.InBounds(map))
GenDraw.DrawLineBetween(startRightCell.ToVector3Shifted(), endRightCell.ToVector3Shifted(), SimpleColor.Red, 0.2f);
if (startLeftCell.InBounds(map) && startRightCell.InBounds(map))
GenDraw.DrawLineBetween(startLeftCell.ToVector3Shifted(), startRightCell.ToVector3Shifted(), SimpleColor.Red, 0.2f);
if (endLeftCell.InBounds(map) && endRightCell.InBounds(map))
GenDraw.DrawLineBetween(endLeftCell.ToVector3Shifted(), endRightCell.ToVector3Shifted(), SimpleColor.Red, 0.2f);
}
// 修复:计算轰炸区域的所有单元格,确保正确的方向
private List<IntVec3> CalculateBombardmentAreaCells(IntVec3 centerCell, Vector3 lengthDirection, Vector3 widthDirection)
{
var areaCells = new List<IntVec3>();
Map map = parent.pawn.Map;
Vector3 center = centerCell.ToVector3();
float halfWidth = Props.bombardmentWidth * 0.5f;
float halfLength = Props.bombardmentLength * 0.5f;
// 使用浮点步进计算所有单元格
int widthSteps = Mathf.Max(1, Props.bombardmentWidth);
int lengthSteps = Mathf.Max(1, Props.bombardmentLength);
for (int l = 0; l <= lengthSteps; l++)
{
float lengthProgress = (float)l / lengthSteps;
float lengthOffset = Mathf.Lerp(-halfLength, halfLength, lengthProgress);
for (int w = 0; w <= widthSteps; w++)
{
float widthProgress = (float)w / widthSteps;
float widthOffset = Mathf.Lerp(-halfWidth, halfWidth, widthProgress);
// 修复:使用正确的方向计算单元格位置
Vector3 cellPos = center + lengthDirection * lengthOffset + widthDirection * widthOffset;
IntVec3 cell = new IntVec3(
Mathf.RoundToInt(cellPos.x),
Mathf.RoundToInt(cellPos.y),
Mathf.RoundToInt(cellPos.z)
);
if (cell.InBounds(map) && !areaCells.Contains(cell))
{
areaCells.Add(cell);
}
}
}
return areaCells;
}
// 修复:计算轰炸区域和方向,确保轰炸前进方向垂直于施法者-目标连线
private void CalculateBombardmentArea(IntVec3 targetCell)
{
bombardmentCenter = targetCell;
// 计算施法者到目标的方向
Vector3 casterToTarget = (targetCell.ToVector3() - parent.pawn.Position.ToVector3()).normalized;
// 如果方向为零向量,使用默认方向
if (casterToTarget == Vector3.zero)
{
casterToTarget = Vector3.forward;
}
// 修复:计算垂直于施法者-目标连线的两个方向(作为可能的轰炸前进方向)
Vector3 perpendicular1 = new Vector3(-casterToTarget.z, 0, casterToTarget.x).normalized;
Vector3 perpendicular2 = new Vector3(casterToTarget.z, 0, -casterToTarget.x).normalized;
// 随机选择轰炸前进方向(垂直于施法者-目标连线)
bombardmentDirection = Rand.Value < 0.5f ? perpendicular1 : perpendicular2;
Log.Message($"[Bombardment] Bombardment direction: {bombardmentDirection} (perpendicular to caster-target line)");
}
private void SelectTargetCells()
{
// 计算施法者到目标的方向
Vector3 casterToTarget = (bombardmentCenter.ToVector3() - parent.pawn.Position.ToVector3()).normalized;
// 如果方向为零向量,使用默认方向
if (casterToTarget == Vector3.zero)
{
casterToTarget = Vector3.forward;
}
// 修复:使用正确的方向计算轰炸区域
Vector3 widthDirection = casterToTarget; // 宽度方向沿着施法者-目标连线
var areaCells = CalculateBombardmentAreaCells(bombardmentCenter, bombardmentDirection, widthDirection);
var selectedCells = new List<IntVec3>();
var missedCells = new List<IntVec3>();
// 根据概率选择目标格子
foreach (var cell in areaCells)
{
if (Rand.Value <= Props.targetSelectionChance)
{
selectedCells.Add(cell);
}
else
{
missedCells.Add(cell);
}
}
// 应用最小/最大限制
if (selectedCells.Count < Props.minTargetCells)
{
// 补充不足的格子
int needed = Props.minTargetCells - selectedCells.Count;
if (missedCells.Count > 0)
{
selectedCells.AddRange(missedCells.InRandomOrder().Take(Mathf.Min(needed, missedCells.Count)));
}
}
else if (selectedCells.Count > Props.maxTargetCells)
{
// 随机移除多余的格子
selectedCells = selectedCells.InRandomOrder().Take(Props.maxTargetCells).ToList();
}
targetCells = selectedCells;
Log.Message($"[Bombardment] Selected {targetCells.Count} target cells from {areaCells.Count} area cells");
}
// 修复:组织目标格子成排,按照轰炸前进方向分组
private void OrganizeTargetCellsIntoRows()
{
bombardmentRows.Clear();
// 计算施法者到目标的方向(作为宽度方向)
Vector3 casterToTarget = (bombardmentCenter.ToVector3() - parent.pawn.Position.ToVector3()).normalized;
Vector3 widthDirection = casterToTarget;
// 根据轰炸前进方向将格子分组到不同的排
var rows = new Dictionary<int, List<IntVec3>>();
foreach (var cell in targetCells)
{
// 计算格子相对于轰炸中心的"行索引"(在轰炸前进方向上的投影)
Vector3 cellVector = cell.ToVector3() - bombardmentCenter.ToVector3();
float dotProduct = Vector3.Dot(cellVector, bombardmentDirection);
int rowIndex = Mathf.RoundToInt(dotProduct);
if (!rows.ContainsKey(rowIndex))
{
rows[rowIndex] = new List<IntVec3>();
}
rows[rowIndex].Add(cell);
}
// 按行索引排序并创建 BombardmentRow
var sortedRowIndices = rows.Keys.OrderBy(x => x).ToList();
foreach (var rowIndex in sortedRowIndices)
{
bombardmentRows.Add(new BombardmentRow
{
rowIndex = rowIndex,
cells = rows[rowIndex].OrderBy(cell =>
{
Vector3 cellVector = cell.ToVector3() - bombardmentCenter.ToVector3();
return Vector3.Dot(cellVector, widthDirection); // 在宽度方向上排序
}).ToList()
});
}
Log.Message($"[Bombardment] Organized into {bombardmentRows.Count} rows");
}
private void StartWarmup()
{
currentState = BombardmentState.Warmup;
warmupTicksRemaining = Props.warmupTicks;
currentRowIndex = 0;
currentCellIndex = 0;
// 创建区域效果器
if (Props.showBombardmentArea)
{
CreateAreaEffecter();
}
Log.Message($"[Bombardment] Warmup started: {warmupTicksRemaining} ticks remaining");
}
private void UpdateWarmup()
{
warmupTicksRemaining--;
if (warmupTicksRemaining <= 0)
{
// 前摇结束,开始轰炸
currentState = BombardmentState.Bombarding;
nextBombardmentTick = Find.TickManager.TicksGame;
Log.Message($"[Bombardment] Warmup completed, starting bombardment");
}
}
private void UpdateBombardment()
{
if (Find.TickManager.TicksGame < nextBombardmentTick)
return;
if (currentRowIndex >= bombardmentRows.Count)
{
// 所有排都轰炸完毕
currentState = BombardmentState.Completed;
Log.Message($"[Bombardment] Bombardment completed");
return;
}
var currentRow = bombardmentRows[currentRowIndex];
if (currentCellIndex >= currentRow.cells.Count)
{
// 当前排轰炸完毕,移动到下一排
currentRowIndex++;
currentCellIndex = 0;
nextBombardmentTick = Find.TickManager.TicksGame + Props.rowDelayTicks;
Log.Message($"[Bombardment] Moving to row {currentRowIndex + 1}/{bombardmentRows.Count}");
return;
}
// 轰炸当前格子
var targetCell = currentRow.cells[currentCellIndex];
LaunchBombardment(targetCell);
currentCellIndex++;
nextBombardmentTick = Find.TickManager.TicksGame + Props.impactDelayTicks;
}
private void LaunchBombardment(IntVec3 targetCell)
{
try
{
if (Props.skyfallerDef != null)
{
// 使用 Skyfaller
Skyfaller skyfaller = SkyfallerMaker.MakeSkyfaller(Props.skyfallerDef);
GenSpawn.Spawn(skyfaller, targetCell, parent.pawn.Map);
Log.Message($"[Bombardment] Launched skyfaller at {targetCell}");
}
else if (Props.projectileDef != null)
{
// 使用抛射体作为备用
LaunchProjectileAt(targetCell);
}
else
{
Log.Error($"[Bombardment] No skyfaller or projectile defined for bombardment");
}
}
catch (System.Exception ex)
{
Log.Error($"[Bombardment] Error launching bombardment 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
);
Log.Message($"[Bombardment] Launched projectile at {targetCell}");
}
}
private void CreateAreaEffecter()
{
// 创建轰炸区域视觉效果
if (DefDatabase<EffecterDef>.GetNamedSilentFail("BombardmentArea") != null)
{
areaEffecter = DefDatabase<EffecterDef>.GetNamed("BombardmentArea").Spawn();
areaEffecter.offset = bombardmentCenter.ToVector3Shifted();
areaEffecter.scale = Props.effecterScale;
}
}
private void Cleanup()
{
// 清理效果器
areaEffecter?.Cleanup();
areaEffecter = null;
// 重置状态
currentState = BombardmentState.Idle;
targetCells.Clear();
bombardmentRows.Clear();
currentPreviewCells.Clear();
Log.Message($"[Bombardment] Cleanup completed");
}
private IntVec3 GetSafeMapPosition(IntVec3 pos, Map map)
{
if (map == null) return pos;
pos.x = Mathf.Clamp(pos.x, 0, map.Size.x - 1);
pos.z = Mathf.Clamp(pos.z, 0, map.Size.z - 1);
return pos;
}
public override void CompTick()
{
base.CompTick();
if (currentState == BombardmentState.Idle)
return;
switch (currentState)
{
case BombardmentState.Warmup:
UpdateWarmup();
break;
case BombardmentState.Bombarding:
UpdateBombardment();
break;
case BombardmentState.Completed:
Cleanup();
break;
}
}
public override void PostExposeData()
{
base.PostExposeData();
Scribe_Values.Look(ref currentState, "currentState", BombardmentState.Idle);
Scribe_Collections.Look(ref targetCells, "targetCells", LookMode.Value);
Scribe_Values.Look(ref currentRowIndex, "currentRowIndex", 0);
Scribe_Values.Look(ref currentCellIndex, "currentCellIndex", 0);
Scribe_Values.Look(ref warmupTicksRemaining, "warmupTicksRemaining", 0);
Scribe_Values.Look(ref nextBombardmentTick, "nextBombardmentTick", 0);
}
}
// 轰炸状态枚举
public enum BombardmentState
{
Idle,
Warmup,
Bombarding,
Completed
}
// 轰炸排数据结构
public struct BombardmentRow
{
public int rowIndex;
public List<IntVec3> cells;
}
}

View File

@@ -0,0 +1,38 @@
// CompProperties_AbilityBombardment.cs
using RimWorld;
using Verse;
using UnityEngine;
namespace WulaFallenEmpire
{
public class CompProperties_AbilityBombardment : CompProperties_AbilityEffect
{
// 轰炸区域配置
public int bombardmentWidth = 5; // 轰炸区域宽度
public int bombardmentLength = 8; // 轰炸区域长度
// 目标选择配置
public float targetSelectionChance = 0.6f; // 每个格子被选中的概率
public int minTargetCells = 3; // 最小目标格子数
public int maxTargetCells = 15; // 最大目标格子数
// 时间配置
public int warmupTicks = 120; // 前摇时间
public int rowDelayTicks = 30; // 每排之间的延迟
public int impactDelayTicks = 10; // 单个轰炸的延迟(同一排内)
// 视觉效果配置
public bool showBombardmentArea = true; // 是否显示轰炸区域
public float effecterScale = 1.0f; // 效果器缩放
public Color areaPreviewColor = new Color(1f, 0.3f, 0.1f, 0.3f); // 区域预览颜色
// Skyfaller 配置
public ThingDef skyfallerDef; // 使用的 Skyfaller
public ThingDef projectileDef; // 备用的抛射体定义(如果 skyfaller 不可用)
public CompProperties_AbilityBombardment()
{
this.compClass = typeof(CompAbilityEffect_Bombardment);
}
}
}

View File

@@ -0,0 +1,187 @@
// CompAbilityEffect_RequireFlyOverFacility.cs
using System.Collections.Generic;
using System.Linq;
using RimWorld;
using Verse;
namespace WulaFallenEmpire
{
public class CompAbilityEffect_RequireFlyOverFacility : CompAbilityEffect
{
new public CompProperties_RequireFlyOverFacility Props => (CompProperties_RequireFlyOverFacility)props;
public override void Apply(LocalTargetInfo target, LocalTargetInfo dest)
{
base.Apply(target, dest);
if (parent.pawn == null || parent.pawn.Map == null)
return;
// 查找可用的 FlyOver
var availableFlyOvers = GetValidFlyOvers();
if (availableFlyOvers.Count == 0)
{
Log.Error($"[RequireFlyOverFacility] No valid FlyOver found with required facility: {Props.requiredFacility}");
return;
}
// 执行技能效果
ExecuteSkillEffect(availableFlyOvers, target, dest);
}
public override bool Valid(LocalTargetInfo target, bool throwMessages = false)
{
if (!base.Valid(target, throwMessages))
return false;
// 检查是否有可用的 FlyOver
var validFlyOvers = GetValidFlyOvers();
if (validFlyOvers.Count == 0)
{
if (throwMessages)
{
Messages.Message(Props.facilityNotFoundMessage, parent.pawn, MessageTypeDefOf.RejectInput);
}
return false;
}
return true;
}
public override string ExtraLabelMouseAttachment(LocalTargetInfo target)
{
try
{
var validFlyOvers = GetValidFlyOvers();
if (validFlyOvers.Count == 0)
{
return $"需要拥有 {Props.requiredFacility} 设施的飞行器";
}
return $"可用飞行器: {validFlyOvers.Count}";
}
catch (System.Exception ex)
{
// 捕获异常避免UI崩溃
Log.Error($"[RequireFlyOverFacility] Error in ExtraLabelMouseAttachment: {ex}");
return "设施检查错误";
}
}
// 获取有效的 FlyOver 列表
private List<FlyOver> GetValidFlyOvers()
{
var validFlyOvers = new List<FlyOver>();
if (parent.pawn?.Map == null)
return validFlyOvers;
try
{
List<Thing> allFlyOvers;
if (Props.flyOverDef != null)
{
// 如果指定了特定的 FlyOver 定义,只检查该定义的物体
allFlyOvers = parent.pawn.Map.listerThings.ThingsOfDef(Props.flyOverDef);
Log.Message($"[RequireFlyOverFacility] Checking specific FlyOverDef: {Props.flyOverDef.defName}, found: {allFlyOvers.Count}");
}
else
{
// 如果没有指定 FlyOver 定义,检查所有类型为 FlyOver 的物体
// 使用更高效的方式:只检查动态物体列表,避免遍历所有物体
allFlyOvers = new List<Thing>();
var dynamicObjects = parent.pawn.Map.dynamicDrawManager.DrawThings;
foreach (var thing in dynamicObjects)
{
if (thing is FlyOver)
{
allFlyOvers.Add(thing);
}
}
Log.Message($"[RequireFlyOverFacility] Checking all FlyOver types, found: {allFlyOvers.Count}");
}
foreach (var thing in allFlyOvers)
{
if (thing is FlyOver flyOver && !flyOver.Destroyed)
{
// 检查设施
var facilitiesComp = flyOver.GetComp<CompFlyOverFacilities>();
if (facilitiesComp == null)
{
Log.Warning($"[RequireFlyOverFacility] FlyOver at {flyOver.Position} has no CompFlyOverFacilities");
continue;
}
if (!facilitiesComp.HasFacility(Props.requiredFacility))
{
Log.Message($"[RequireFlyOverFacility] FlyOver at {flyOver.Position} missing facility: {Props.requiredFacility}. Has: {string.Join(", ", facilitiesComp.GetActiveFacilities())}");
continue;
}
validFlyOvers.Add(flyOver);
Log.Message($"[RequireFlyOverFacility] Found valid FlyOver at {flyOver.Position} with facility: {Props.requiredFacility}");
}
}
return validFlyOvers;
}
catch (System.Exception ex)
{
Log.Error($"[RequireFlyOverFacility] Error in GetValidFlyOvers: {ex}");
return new List<FlyOver>();
}
}
// 执行技能效果(由子类重写)
protected virtual void ExecuteSkillEffect(List<FlyOver> availableFlyOvers, LocalTargetInfo target, LocalTargetInfo dest)
{
// 基础实现:选择第一个可用的 FlyOver
var selectedFlyOver = availableFlyOvers.FirstOrDefault();
if (selectedFlyOver != null)
{
Log.Message($"[RequireFlyOverFacility] Skill executed using FlyOver at {selectedFlyOver.Position} with facility: {Props.requiredFacility}");
}
}
// 重写 Gizmo 方法,确保不会在绘制时崩溃
public override bool GizmoDisabled(out string reason)
{
if (parent.pawn?.Map == null)
{
reason = "无法在地图外使用";
return true;
}
var validFlyOvers = GetValidFlyOvers();
if (validFlyOvers.Count == 0)
{
reason = Props.facilityNotFoundMessage;
return true;
}
return base.GizmoDisabled(out reason);
}
}
public class CompProperties_RequireFlyOverFacility : CompProperties_AbilityEffect
{
// 必需的 FlyOver 定义(可以为 null表示检查所有 FlyOver 类型)
public ThingDef flyOverDef;
// 必需的设施名称
public string requiredFacility;
// 消息文本
public string facilityNotFoundMessage = "需要拥有特定设施的飞行器";
public CompProperties_RequireFlyOverFacility()
{
compClass = typeof(CompAbilityEffect_RequireFlyOverFacility);
}
}
}

View File

@@ -0,0 +1,75 @@
// CompFlyOverFacilities.cs
using System.Collections.Generic;
using RimWorld;
using Verse;
namespace WulaFallenEmpire
{
public class CompFlyOverFacilities : ThingComp
{
public CompProperties_FlyOverFacilities Props => (CompProperties_FlyOverFacilities)props;
// 当前激活的设施列表
public List<string> activeFacilities = new List<string>();
public override void PostSpawnSetup(bool respawningAfterLoad)
{
base.PostSpawnSetup(respawningAfterLoad);
if (!respawningAfterLoad)
{
// 只在初次生成时激活所有定义的设施
activeFacilities.AddRange(Props.availableFacilities);
Log.Message($"[FlyOverFacilities] Initialized with {activeFacilities.Count} facilities: {string.Join(", ", activeFacilities)}");
}
}
// 检查是否拥有特定设施
public bool HasFacility(string facilityName)
{
return activeFacilities?.Contains(facilityName) ?? false;
}
// 获取所有激活的设施
public List<string> GetActiveFacilities()
{
return activeFacilities != null ? new List<string>(activeFacilities) : new List<string>();
}
public override void PostExposeData()
{
base.PostExposeData();
Scribe_Collections.Look(ref activeFacilities, "activeFacilities", LookMode.Value);
// 如果加载失败或列表为null重新初始化
if (Scribe.mode == LoadSaveMode.PostLoadInit && activeFacilities == null)
{
activeFacilities = new List<string>();
// 在加载后重新添加默认设施
activeFacilities.AddRange(Props.availableFacilities);
Log.Message($"[FlyOverFacilities] Reinitialized after load with {activeFacilities.Count} facilities");
}
}
public override void Initialize(CompProperties props)
{
base.Initialize(props);
// 确保列表被初始化
if (activeFacilities == null)
{
activeFacilities = new List<string>();
}
}
}
public class CompProperties_FlyOverFacilities : CompProperties
{
// 可用的设施列表(简单的字符串列表)
public List<string> availableFacilities = new List<string>();
public CompProperties_FlyOverFacilities()
{
compClass = typeof(CompFlyOverFacilities);
}
}
}

View File

@@ -1,749 +0,0 @@
// CompAbilityEffect_OrbitalBombardment.cs
using RimWorld;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using Verse;
namespace WulaFallenEmpire
{
public class CompAbilityEffect_OrbitalBombardment : CompAbilityEffect
{
public new CompProperties_AbilityOrbitalBombardment Props => (CompProperties_AbilityOrbitalBombardment)props;
public override void Apply(LocalTargetInfo target, LocalTargetInfo dest)
{
base.Apply(target, dest);
if (parent.pawn == null || parent.pawn.Map == null)
return;
try
{
Log.Message($"OrbitalBombardment skill activated by {parent.pawn.Label} at position {parent.pawn.Position}");
Log.Message($"Target cell: {target.Cell}, Dest: {dest.Cell}");
// 计算起始和结束位置
IntVec3 startPos, endPos;
if (Props.approachType == ApproachType.Perpendicular)
{
CalculatePerpendicularPath(target, out startPos, out endPos);
}
else
{
startPos = CalculateStartPosition(target);
endPos = CalculateEndPosition(target, startPos);
}
// 确保位置安全
startPos = GetSafeMapPosition(startPos, parent.pawn.Map);
endPos = GetSafeMapPosition(endPos, parent.pawn.Map);
Log.Message($"Final positions - Start: {startPos}, End: {endPos}");
// 验证位置是否有效
if (!startPos.InBounds(parent.pawn.Map))
{
Log.Warning($"Start position {startPos} is out of bounds, adjusting to map center");
startPos = parent.pawn.Map.Center;
}
if (!endPos.InBounds(parent.pawn.Map))
{
Log.Warning($"End position {endPos} is out of bounds, adjusting to map center");
endPos = parent.pawn.Map.Center;
}
// 确保起点和终点不同
if (startPos == endPos)
{
Log.Warning($"OrbitalBombardment start and end positions are the same: {startPos}. Adjusting end position.");
IntVec3 randomOffset = new IntVec3(Rand.Range(-10, 11), 0, Rand.Range(-10, 11));
endPos += randomOffset;
endPos = GetSafeMapPosition(endPos, parent.pawn.Map);
}
// 创建轨道炮击飞越
CreateOrbitalBombardmentFlyOver(startPos, endPos, target.Cell);
}
catch (System.Exception ex)
{
Log.Error($"Error spawning orbital bombardment: {ex}");
}
}
public override void DrawEffectPreview(LocalTargetInfo target)
{
base.DrawEffectPreview(target);
if (parent.pawn != null && parent.pawn.Map != null)
{
Map map = parent.pawn.Map;
try
{
// 计算飞行路径
IntVec3 startPos, endPos;
if (Props.approachType == ApproachType.Perpendicular)
{
CalculatePerpendicularPath(target, out startPos, out endPos);
}
else
{
startPos = CalculateStartPosition(target);
endPos = CalculateEndPosition(target, startPos);
}
// 确保位置在地图范围内
startPos = GetSafeMapPosition(startPos, map);
endPos = GetSafeMapPosition(endPos, map);
// 检查预览稳定性
if (!IsPreviewStable(startPos, endPos, map))
{
return;
}
// 绘制炮击区域预览
DrawBombardmentAreaPreview(startPos, endPos, target.Cell);
}
catch (System.Exception)
{
// 忽略预览绘制中的错误
}
}
}
// 绘制炮击区域预览
private void DrawBombardmentAreaPreview(IntVec3 startPos, IntVec3 endPos, IntVec3 targetCell)
{
Map map = parent.pawn.Map;
// 计算飞行方向
Vector3 flightDirection = (endPos.ToVector3() - startPos.ToVector3()).normalized;
if (flightDirection == Vector3.zero)
{
flightDirection = Vector3.forward;
}
// 计算炮击影响区域的单元格
List<IntVec3> bombardmentImpactCells = CalculateBombardmentImpactCells(targetCell, flightDirection);
// 绘制炮击影响区域的预览单元格
foreach (IntVec3 cell in bombardmentImpactCells)
{
if (cell.InBounds(map))
{
GenDraw.DrawFieldEdges(new List<IntVec3> { cell }, Props.bombardmentPreviewColor, 0.5f);
}
}
// 绘制飞行路径线
GenDraw.DrawLineBetween(startPos.ToVector3Shifted(), endPos.ToVector3Shifted(), SimpleColor.Yellow, 0.2f);
// 绘制炮击范围边界
DrawBombardmentBoundaries(targetCell, flightDirection);
}
// 计算炮击影响区域的单元格
private List<IntVec3> CalculateBombardmentImpactCells(IntVec3 targetCell, Vector3 flightDirection)
{
List<IntVec3> cells = new List<IntVec3>();
Map map = parent.pawn.Map;
// 计算垂直于飞行方向的方向
Vector3 perpendicular = new Vector3(-flightDirection.z, 0f, flightDirection.x).normalized;
// 以目标单元格为中心计算炮击区域
Vector3 targetCenter = targetCell.ToVector3();
// 计算炮击区域的起始和结束位置(基于炮击长度,以目标为中心)
float bombardmentHalfLength = Props.bombardmentLength * 0.5f;
Vector3 bombardmentStart = targetCenter - flightDirection * bombardmentHalfLength;
Vector3 bombardmentEnd = targetCenter + flightDirection * bombardmentHalfLength;
// 使用整数步进
int steps = Mathf.Max(1, Mathf.CeilToInt(Props.bombardmentLength));
for (int i = 0; i <= steps; i++)
{
float progress = (float)i / steps;
Vector3 centerPoint = Vector3.Lerp(bombardmentStart, bombardmentEnd, progress);
// 在垂直方向扩展炮击宽度
for (int w = -Props.bombardmentWidth; w <= Props.bombardmentWidth; w++)
{
Vector3 offset = perpendicular * w;
Vector3 cellPos = centerPoint + offset;
// 使用精确的单元格转换
IntVec3 cell = new IntVec3(
Mathf.RoundToInt(cellPos.x),
Mathf.RoundToInt(cellPos.y),
Mathf.RoundToInt(cellPos.z)
);
if (cell.InBounds(map) && !cells.Contains(cell))
{
cells.Add(cell);
}
}
}
Log.Message($"Bombardment Area: Calculated {cells.Count} impact cells centered at {targetCell}");
return cells;
}
// 绘制炮击范围边界
private void DrawBombardmentBoundaries(IntVec3 targetCell, Vector3 flightDirection)
{
Map map = parent.pawn.Map;
Vector3 perpendicular = new Vector3(-flightDirection.z, 0f, flightDirection.x).normalized;
// 以目标单元格为中心
Vector3 targetCenter = targetCell.ToVector3();
// 计算炮击区域的起始和结束位置
float bombardmentHalfLength = Props.bombardmentLength * 0.5f;
Vector3 bombardmentStart = targetCenter - flightDirection * bombardmentHalfLength;
Vector3 bombardmentEnd = targetCenter + flightDirection * bombardmentHalfLength;
// 计算炮击区域的四个角
Vector3 startLeft = bombardmentStart + perpendicular * Props.bombardmentWidth;
Vector3 startRight = bombardmentStart - perpendicular * Props.bombardmentWidth;
Vector3 endLeft = bombardmentEnd + perpendicular * Props.bombardmentWidth;
Vector3 endRight = bombardmentEnd - perpendicular * Props.bombardmentWidth;
// 转换为 IntVec3 并确保在地图范围内
IntVec3 startLeftCell = GetSafeMapPosition(new IntVec3((int)startLeft.x, (int)startLeft.y, (int)startLeft.z), map);
IntVec3 startRightCell = GetSafeMapPosition(new IntVec3((int)startRight.x, (int)startRight.y, (int)startRight.z), map);
IntVec3 endLeftCell = GetSafeMapPosition(new IntVec3((int)endLeft.x, (int)endLeft.y, (int)endLeft.z), map);
IntVec3 endRightCell = GetSafeMapPosition(new IntVec3((int)endRight.x, (int)endRight.y, (int)endRight.z), map);
// 绘制边界线
if (startLeftCell.InBounds(map) && endLeftCell.InBounds(map))
GenDraw.DrawLineBetween(startLeftCell.ToVector3Shifted(), endLeftCell.ToVector3Shifted(), SimpleColor.Yellow, 0.2f);
if (startRightCell.InBounds(map) && endRightCell.InBounds(map))
GenDraw.DrawLineBetween(startRightCell.ToVector3Shifted(), endRightCell.ToVector3Shifted(), SimpleColor.Yellow, 0.2f);
if (startLeftCell.InBounds(map) && startRightCell.InBounds(map))
GenDraw.DrawLineBetween(startLeftCell.ToVector3Shifted(), startRightCell.ToVector3Shifted(), SimpleColor.Yellow, 0.2f);
if (endLeftCell.InBounds(map) && endRightCell.InBounds(map))
GenDraw.DrawLineBetween(endLeftCell.ToVector3Shifted(), endRightCell.ToVector3Shifted(), SimpleColor.Yellow, 0.2f);
}
// 预处理炮击目标单元格
private List<IntVec3> PreprocessBombardmentTargets(List<IntVec3> potentialTargets, float fireChance)
{
List<IntVec3> confirmedTargets = new List<IntVec3>();
List<IntVec3> missedCells = new List<IntVec3>();
foreach (IntVec3 cell in potentialTargets)
{
if (Rand.Value <= fireChance)
{
confirmedTargets.Add(cell);
}
else
{
missedCells.Add(cell);
}
}
// 应用最小和最大炮击数限制
if (Props.maxBombardmentCount > -1 && confirmedTargets.Count > Props.maxBombardmentCount)
{
confirmedTargets = confirmedTargets.InRandomOrder().Take(Props.maxBombardmentCount).ToList();
}
if (Props.minBombardmentCount > -1 && confirmedTargets.Count < Props.minBombardmentCount)
{
int needed = Props.minBombardmentCount - confirmedTargets.Count;
if (needed > 0 && missedCells.Count > 0)
{
confirmedTargets.AddRange(missedCells.InRandomOrder().Take(Mathf.Min(needed, missedCells.Count)));
}
}
Log.Message($"Bombardment Preprocess: {confirmedTargets.Count}/{potentialTargets.Count} cells confirmed after min/max adjustment.");
return confirmedTargets;
}
// 创建轨道炮击飞越
private void CreateOrbitalBombardmentFlyOver(IntVec3 startPos, IntVec3 endPos, IntVec3 targetCell)
{
ThingDef flyOverDef = Props.flyOverDef ?? DefDatabase<ThingDef>.GetNamedSilentFail("ARA_HiveCorvette");
if (flyOverDef == null)
{
Log.Warning("No fly over def specified for orbital bombardment fly over");
return;
}
FlyOver flyOver = FlyOver.MakeFlyOver(
flyOverDef,
startPos,
endPos,
parent.pawn.Map,
Props.flightSpeed,
Props.altitude,
casterPawn: parent.pawn
);
// 设置基本属性
flyOver.spawnContentsOnImpact = Props.dropContentsOnImpact;
flyOver.playFlyOverSound = Props.playFlyOverSound;
// 获取轨道炮击组件并设置预处理后的目标单元格
CompOrbitalBombardment bombardmentComp = flyOver.GetComp<CompOrbitalBombardment>();
if (bombardmentComp != null)
{
// 计算炮击区域的所有单元格,以目标单元格为中心
Vector3 flightDirection = (endPos.ToVector3() - startPos.ToVector3()).normalized;
List<IntVec3> potentialTargetCells = CalculateBombardmentImpactCells(targetCell, flightDirection);
if (potentialTargetCells.Count > 0)
{
// 预处理:根据概率筛选实际会被炮击的单元格
List<IntVec3> confirmedTargetCells = PreprocessBombardmentTargets(
potentialTargetCells,
Props.bombardmentFireChance
);
if (confirmedTargetCells.Count > 0)
{
bombardmentComp.SetConfirmedTargets(confirmedTargetCells);
}
else
{
Log.Warning("No confirmed target cells after preprocessing!");
}
}
else
{
Log.Error("No potential target cells calculated for orbital bombardment!");
}
}
else
{
Log.Error("FlyOver def does not have CompOrbitalBombardment component!");
}
}
// 以下方法与 CompAbilityEffect_SpawnFlyOver 中的相同,需要复制过来
private IntVec3 GetSafeMapPosition(IntVec3 pos, Map map)
{
if (map == null) return pos;
pos.x = Mathf.Clamp(pos.x, 0, map.Size.x - 1);
pos.z = Mathf.Clamp(pos.z, 0, map.Size.z - 1);
return pos;
}
private bool IsPreviewStable(IntVec3 startPos, IntVec3 endPos, Map map)
{
if (map == null) return false;
if (!startPos.IsValid || !endPos.IsValid) return false;
if (!startPos.InBounds(map) || !endPos.InBounds(map)) return false;
float distance = Vector3.Distance(startPos.ToVector3(), endPos.ToVector3());
if (distance < 5f) return false;
return true;
}
private void CalculatePerpendicularPath(LocalTargetInfo target, out IntVec3 startPos, out IntVec3 endPos)
{
Map map = parent.pawn.Map;
IntVec3 casterPos = parent.pawn.Position;
IntVec3 targetPos = target.Cell;
Log.Message($"Calculating perpendicular path: Caster={casterPos}, Target={targetPos}");
Vector3 directionToTarget = (targetPos.ToVector3() - casterPos.ToVector3()).normalized;
if (directionToTarget == Vector3.zero)
{
directionToTarget = new Vector3(Rand.Range(-1f, 1f), 0, Rand.Range(-1f, 1f)).normalized;
Log.Message($"Using random direction: {directionToTarget}");
}
Vector3 perpendicularDirection = new Vector3(-directionToTarget.z, 0, directionToTarget.x).normalized;
Log.Message($"Perpendicular direction: {perpendicularDirection}");
IntVec3 edge1 = FindMapEdgeInDirection(map, targetPos, perpendicularDirection);
IntVec3 edge2 = FindMapEdgeInDirection(map, targetPos, -perpendicularDirection);
if (Rand.Value < 0.5f)
{
startPos = edge1;
endPos = edge2;
}
else
{
startPos = edge2;
endPos = edge1;
}
Log.Message($"Perpendicular path: {startPos} -> {targetPos} -> {endPos}");
}
private IntVec3 FindMapEdgeInDirection(Map map, IntVec3 fromPos, Vector3 direction)
{
if (direction == Vector3.zero)
{
direction = new Vector3(Rand.Range(-1f, 1f), 0, Rand.Range(-1f, 1f)).normalized;
}
IntVec3 mapCenter = map.Center;
IntVec3 mapSize = new IntVec3(map.Size.x, 0, map.Size.z);
Vector3 fromVec = fromPos.ToVector3();
Vector3 dirNormalized = direction.normalized;
float tMin = float.MaxValue;
IntVec3? bestEdgePos = null;
for (int i = 0; i < 4; i++)
{
float t = 0f;
IntVec3 edgePos = IntVec3.Invalid;
switch (i)
{
case 0: // 左边界 (x = 0)
if (Mathf.Abs(dirNormalized.x) > 0.001f)
{
t = (0 - fromVec.x) / dirNormalized.x;
if (t > 0)
{
float z = fromVec.z + dirNormalized.z * t;
if (z >= 0 && z < map.Size.z)
{
edgePos = new IntVec3(0, 0, Mathf.RoundToInt(z));
}
}
}
break;
case 1: // 右边界 (x = map.Size.x - 1)
if (Mathf.Abs(dirNormalized.x) > 0.001f)
{
t = (map.Size.x - 1 - fromVec.x) / dirNormalized.x;
if (t > 0)
{
float z = fromVec.z + dirNormalized.z * t;
if (z >= 0 && z < map.Size.z)
{
edgePos = new IntVec3(map.Size.x - 1, 0, Mathf.RoundToInt(z));
}
}
}
break;
case 2: // 下边界 (z = 0)
if (Mathf.Abs(dirNormalized.z) > 0.001f)
{
t = (0 - fromVec.z) / dirNormalized.z;
if (t > 0)
{
float x = fromVec.x + dirNormalized.x * t;
if (x >= 0 && x < map.Size.x)
{
edgePos = new IntVec3(Mathf.RoundToInt(x), 0, 0);
}
}
}
break;
case 3: // 上边界 (z = map.Size.z - 1)
if (Mathf.Abs(dirNormalized.z) > 0.001f)
{
t = (map.Size.z - 1 - fromVec.z) / dirNormalized.z;
if (t > 0)
{
float x = fromVec.x + dirNormalized.x * t;
if (x >= 0 && x < map.Size.x)
{
edgePos = new IntVec3(Mathf.RoundToInt(x), 0, map.Size.z - 1);
}
}
}
break;
}
if (edgePos.IsValid && edgePos.InBounds(map) && t > 0 && t < tMin)
{
tMin = t;
bestEdgePos = edgePos;
}
}
if (bestEdgePos.HasValue)
{
return bestEdgePos.Value;
}
Log.Warning($"Could not find map edge in direction {direction}, using random edge");
return GetRandomMapEdgePosition(map);
}
private IntVec3 GetRandomMapEdgePosition(Map map)
{
int edge = Rand.Range(0, 4);
int x, z;
switch (edge)
{
case 0: // 上边
x = Rand.Range(0, map.Size.x);
z = 0;
break;
case 1: // 右边
x = map.Size.x - 1;
z = Rand.Range(0, map.Size.z);
break;
case 2: // 下边
x = Rand.Range(0, map.Size.x);
z = map.Size.z - 1;
break;
case 3: // 左边
default:
x = 0;
z = Rand.Range(0, map.Size.z);
break;
}
IntVec3 edgePos = new IntVec3(x, 0, z);
Log.Message($"Random map edge position: {edgePos}");
return edgePos;
}
private IntVec3 CalculateStartPosition(LocalTargetInfo target)
{
Map map = parent.pawn.Map;
switch (Props.startPosition)
{
case StartPosition.Caster:
return parent.pawn.Position;
case StartPosition.MapEdge:
return GetMapEdgePosition(map, GetDirectionFromCasterToTarget(target));
case StartPosition.CustomOffset:
return GetSafeMapPosition(parent.pawn.Position + Props.customStartOffset, map);
case StartPosition.RandomMapEdge:
return GetRandomMapEdgePosition(map);
default:
return parent.pawn.Position;
}
}
private IntVec3 CalculateEndPosition(LocalTargetInfo target, IntVec3 startPos)
{
Map map = parent.pawn.Map;
IntVec3 endPos;
switch (Props.endPosition)
{
case EndPosition.TargetCell:
endPos = target.Cell;
break;
case EndPosition.OppositeMapEdge:
endPos = GetOppositeMapEdgeThroughCenter(map, startPos);
break;
case EndPosition.CustomOffset:
endPos = GetSafeMapPosition(target.Cell + Props.customEndOffset, map);
break;
case EndPosition.FixedDistance:
endPos = GetFixedDistancePosition(startPos, target.Cell);
break;
case EndPosition.RandomMapEdge:
endPos = GetRandomMapEdgePosition(map);
Log.Message($"Random map edge selected as end position: {endPos}");
break;
default:
endPos = target.Cell;
break;
}
return GetSafeMapPosition(endPos, map);
}
private IntVec3 GetOppositeMapEdgeThroughCenter(Map map, IntVec3 startPos)
{
IntVec3 center = map.Center;
Vector3 toCenter = (center.ToVector3() - startPos.ToVector3()).normalized;
if (toCenter == Vector3.zero)
{
toCenter = new Vector3(Rand.Range(-1f, 1f), 0, Rand.Range(-1f, 1f)).normalized;
Log.Message($"Using random direction to center: {toCenter}");
}
Vector3 fromCenter = toCenter;
IntVec3 oppositeEdge = GetMapEdgePositionFromCenter(map, fromCenter);
Log.Message($"Found opposite edge through center: {oppositeEdge}");
return oppositeEdge;
}
private IntVec3 GetMapEdgePositionFromCenter(Map map, Vector3 direction)
{
IntVec3 center = map.Center;
float maxDist = Mathf.Max(map.Size.x, map.Size.z) * 0.6f;
for (int i = 1; i <= maxDist; i++)
{
IntVec3 testPos = center + new IntVec3(
Mathf.RoundToInt(direction.x * i),
0,
Mathf.RoundToInt(direction.z * i));
if (!testPos.InBounds(map))
{
IntVec3 edgePos = FindClosestValidPosition(testPos, map);
Log.Message($"Found map edge from center: {edgePos} (direction: {direction}, distance: {i})");
return edgePos;
}
}
Log.Warning("Could not find map edge from center, using random edge");
return GetRandomMapEdgePosition(map);
}
private IntVec3 GetMapEdgePosition(Map map, Vector3 direction)
{
if (direction == Vector3.zero)
{
direction = new Vector3(Rand.Range(-1f, 1f), 0, Rand.Range(-1f, 1f)).normalized;
Log.Message($"Using random direction: {direction}");
}
IntVec3 center = map.Center;
float maxDist = Mathf.Max(map.Size.x, map.Size.z) * 0.6f;
for (int i = 1; i <= maxDist; i++)
{
IntVec3 testPos = center + new IntVec3(
Mathf.RoundToInt(direction.x * i),
0,
Mathf.RoundToInt(direction.z * i));
if (!testPos.InBounds(map))
{
IntVec3 edgePos = FindClosestValidPosition(testPos, map);
Log.Message($"Found map edge position: {edgePos} (direction: {direction}, distance: {i})");
return edgePos;
}
}
Log.Warning("Could not find map edge in direction, using random edge");
return GetRandomMapEdgePosition(map);
}
private IntVec3 FindClosestValidPosition(IntVec3 invalidPos, Map map)
{
for (int radius = 1; radius <= 5; radius++)
{
foreach (IntVec3 pos in GenRadial.RadialPatternInRadius(radius))
{
IntVec3 testPos = invalidPos + pos;
if (testPos.InBounds(map))
{
return testPos;
}
}
}
return map.Center;
}
private IntVec3 GetFixedDistancePosition(IntVec3 startPos, IntVec3 targetPos)
{
Vector3 direction = (targetPos.ToVector3() - startPos.ToVector3()).normalized;
IntVec3 endPos = startPos + new IntVec3(
(int)(direction.x * Props.flyOverDistance),
0,
(int)(direction.z * Props.flyOverDistance));
Log.Message($"Fixed distance position: {endPos} (from {startPos}, distance: {Props.flyOverDistance})");
return endPos;
}
private Vector3 GetDirectionFromCasterToTarget(LocalTargetInfo target)
{
Vector3 direction = (target.Cell.ToVector3() - parent.pawn.Position.ToVector3()).normalized;
if (direction == Vector3.zero)
{
direction = new Vector3(Rand.Range(-1f, 1f), 0, Rand.Range(-1f, 1f)).normalized;
Log.Message($"Using random direction: {direction}");
}
return direction;
}
public override string ExtraLabelMouseAttachment(LocalTargetInfo target)
{
return $"炮击区域: {Props.bombardmentWidth * 2 + 1}格宽度 × {Props.bombardmentLength}格长度";
}
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 class CompProperties_AbilityOrbitalBombardment : CompProperties_AbilityEffect
{
public ThingDef flyOverDef; // 飞越物体的 ThingDef
public ApproachType approachType = ApproachType.Standard; // 进场类型
public float flightSpeed = 1f; // 飞行速度
public float altitude = 20f; // 飞行高度
public bool dropContentsOnImpact = false; // 是否在终点投放内容物
public bool playFlyOverSound = true; // 是否播放飞越音效
// 起始位置选项
public StartPosition startPosition = StartPosition.Caster;
public IntVec3 customStartOffset = IntVec3.Zero;
// 终点位置选项
public EndPosition endPosition = EndPosition.TargetCell;
public IntVec3 customEndOffset = IntVec3.Zero;
public int flyOverDistance = 30; // 飞越距离
// 炮击配置
public int bombardmentWidth = 3; // 炮击宽度
public int bombardmentLength = 15; // 炮击长度
public float bombardmentFireChance = 0.6f; // 炮击发射概率
public int minBombardmentCount = -1; // 最小炮击数
public int maxBombardmentCount = -1; // 最大炮击数
// 炮击可视化
public bool showBombardmentPreview = true; // 是否显示炮击预览
public Color bombardmentPreviewColor = new Color(1f, 1f, 0.3f, 0.3f); // 黄色预览
public CompProperties_AbilityOrbitalBombardment()
{
this.compClass = typeof(CompAbilityEffect_OrbitalBombardment);
}
}
}

View File

@@ -1,450 +0,0 @@
// CompOrbitalBombardment.cs
using System.Collections.Generic;
using RimWorld;
using UnityEngine;
using Verse;
namespace WulaFallenEmpire
{
public class CompOrbitalBombardment : ThingComp
{
public CompProperties_OrbitalBombardment Props => (CompProperties_OrbitalBombardment)props;
// 炮击状态
private List<IntVec3> confirmedTargetCells = new List<IntVec3>();
private HashSet<IntVec3> firedCells = new HashSet<IntVec3>();
// 横向偏移状态(左右)
private float currentLateralOffsetAngle = 0f;
private int shotsFired = 0;
// 纵向偏移状态(前后)
private float currentLongitudinalOffset = 0f;
private bool isForwardPhase = true;
// 炮击间隔控制
private int nextBombardmentTick = 0;
private int currentBurstCount = 0;
public override void PostSpawnSetup(bool respawningAfterLoad)
{
base.PostSpawnSetup(respawningAfterLoad);
// 初始化偏移
if (!respawningAfterLoad)
{
currentLateralOffsetAngle = Props.lateralInitialOffsetAngle;
currentLongitudinalOffset = Props.longitudinalInitialOffset;
nextBombardmentTick = Find.TickManager.TicksGame + Props.initialDelayTicks;
}
Log.Message($"OrbitalBombardment: Initialized with {confirmedTargetCells.Count} targets, " +
$"Lateral Offset: {currentLateralOffsetAngle:F1}°, " +
$"Longitudinal Offset: {currentLongitudinalOffset:F1}");
}
public override void CompTick()
{
base.CompTick();
if (confirmedTargetCells.Count == 0 || Find.TickManager.TicksGame < nextBombardmentTick)
{
return;
}
CheckAndBombardTargets();
// 定期状态输出
if (Find.TickManager.TicksGame % 120 == 0 && confirmedTargetCells.Count > 0)
{
Log.Message($"OrbitalBombardment: {firedCells.Count}/{confirmedTargetCells.Count + firedCells.Count} targets bombarded, " +
$"Lateral: {currentLateralOffsetAngle:F1}°, Longitudinal: {currentLongitudinalOffset:F1}");
}
}
private void CheckAndBombardTargets()
{
Vector3 currentPos = parent.DrawPos;
for (int i = confirmedTargetCells.Count - 1; i >= 0; i--)
{
IntVec3 targetCell = confirmedTargetCells[i];
if (firedCells.Contains(targetCell))
{
confirmedTargetCells.RemoveAt(i);
continue;
}
float horizontalDistance = GetHorizontalDistance(currentPos, targetCell);
if (horizontalDistance <= Props.range)
{
if (LaunchSkyfallerAt(targetCell))
{
firedCells.Add(targetCell);
confirmedTargetCells.RemoveAt(i);
// 更新所有偏移参数
UpdateOffsets();
// 设置下一次炮击时间
UpdateNextBombardmentTick();
if (firedCells.Count == 1)
{
Log.Message($"First orbital bombardment at {targetCell}, " +
$"Lateral offset: {currentLateralOffsetAngle:F1}°, " +
$"Longitudinal offset: {currentLongitudinalOffset:F1}");
}
// 检查是否需要暂停(连发模式)
if (Props.burstMode && currentBurstCount >= Props.burstSize)
{
currentBurstCount = 0;
nextBombardmentTick = Find.TickManager.TicksGame + Props.burstCooldownTicks;
break;
}
}
}
}
}
// 新增:更新所有偏移参数
private void UpdateOffsets()
{
shotsFired++;
currentBurstCount++;
// 更新横向偏移
UpdateLateralOffset();
// 更新纵向偏移
UpdateLongitudinalOffset();
}
// 横向偏移逻辑(左右)
private void UpdateLateralOffset()
{
switch (Props.lateralOffsetMode)
{
case OffsetMode.Alternating:
currentLateralOffsetAngle = (shotsFired % 2 == 0) ? Props.lateralOffsetDistance : -Props.lateralOffsetDistance;
break;
case OffsetMode.Progressive:
currentLateralOffsetAngle += Props.lateralAngleIncrement;
if (Mathf.Abs(currentLateralOffsetAngle) > Props.lateralMaxOffsetAngle)
{
currentLateralOffsetAngle = Props.lateralInitialOffsetAngle;
}
break;
case OffsetMode.Random:
currentLateralOffsetAngle = Random.Range(-Props.lateralMaxOffsetAngle, Props.lateralMaxOffsetAngle);
break;
case OffsetMode.Fixed:
default:
break;
}
if (Props.lateralMaxOffsetAngle > 0)
{
currentLateralOffsetAngle = Mathf.Clamp(currentLateralOffsetAngle, -Props.lateralMaxOffsetAngle, Props.lateralMaxOffsetAngle);
}
}
// 纵向偏移逻辑(前后)
private void UpdateLongitudinalOffset()
{
switch (Props.longitudinalOffsetMode)
{
case LongitudinalOffsetMode.Alternating:
currentLongitudinalOffset = (shotsFired % 2 == 0) ? Props.longitudinalAlternationAmplitude : -Props.longitudinalAlternationAmplitude;
break;
case LongitudinalOffsetMode.Progressive:
if (isForwardPhase)
{
currentLongitudinalOffset += Props.longitudinalProgressionStep;
if (currentLongitudinalOffset >= Props.longitudinalMaxOffset)
{
isForwardPhase = false;
}
}
else
{
currentLongitudinalOffset -= Props.longitudinalProgressionStep;
if (currentLongitudinalOffset <= Props.longitudinalMinOffset)
{
isForwardPhase = true;
}
}
break;
case LongitudinalOffsetMode.Random:
currentLongitudinalOffset = Random.Range(Props.longitudinalMinOffset, Props.longitudinalMaxOffset);
break;
case LongitudinalOffsetMode.Sinusoidal:
float time = shotsFired * Props.longitudinalOscillationSpeed;
currentLongitudinalOffset = Mathf.Sin(time) * Props.longitudinalOscillationAmplitude;
break;
case LongitudinalOffsetMode.Fixed:
default:
break;
}
currentLongitudinalOffset = Mathf.Clamp(currentLongitudinalOffset, Props.longitudinalMinOffset, Props.longitudinalMaxOffset);
}
// 更新下一次炮击时间
private void UpdateNextBombardmentTick()
{
if (Props.burstMode && currentBurstCount < Props.burstSize)
{
// 连发模式中的连续射击
nextBombardmentTick = Find.TickManager.TicksGame + Props.burstIntervalTicks;
}
else
{
// 单发模式或连发模式结束
nextBombardmentTick = Find.TickManager.TicksGame + Props.cooldownTicks;
}
}
// 计算包含横向和纵向偏移的目标位置
private IntVec3 CalculateOffsetTargetPosition(IntVec3 baseTarget)
{
Vector3 basePos = baseTarget.ToVector3();
Vector3 finalPos = basePos;
// 应用横向偏移(左右)
if (Mathf.Abs(currentLateralOffsetAngle) > 0.01f)
{
Vector3 flyDirection = GetFlyOverDirection();
Vector3 perpendicular = Vector3.Cross(flyDirection, Vector3.up).normalized;
float lateralOffsetDistance = Props.lateralOffsetDistance;
Vector3 lateralOffset = perpendicular * lateralOffsetDistance * Mathf.Sin(currentLateralOffsetAngle * Mathf.Deg2Rad);
finalPos += lateralOffset;
}
// 应用纵向偏移(前后)
if (Mathf.Abs(currentLongitudinalOffset) > 0.01f)
{
Vector3 flyDirection = GetFlyOverDirection();
Vector3 longitudinalOffset = flyDirection * currentLongitudinalOffset;
finalPos += longitudinalOffset;
}
return finalPos.ToIntVec3();
}
private Vector3 GetFlyOverDirection()
{
FlyOver flyOver = parent as FlyOver;
if (flyOver != null)
{
return flyOver.MovementDirection;
}
return Vector3.forward;
}
private float GetHorizontalDistance(Vector3 fromPos, IntVec3 toCell)
{
Vector2 fromPos2D = new Vector2(fromPos.x, fromPos.z);
Vector2 toPos2D = new Vector2(toCell.x, toCell.z);
return Vector2.Distance(fromPos2D, toPos2D);
}
private bool LaunchSkyfallerAt(IntVec3 targetCell)
{
if (Props.skyfallerDef == null)
{
Log.Error("No skyfaller defined for orbital bombardment");
return false;
}
try
{
// 计算偏移后的目标位置
IntVec3 offsetTarget = CalculateOffsetTargetPosition(targetCell);
// 确保目标位置在地图范围内
if (!offsetTarget.InBounds(parent.Map))
{
Log.Warning($"OrbitalBombardment: Offset target position {offsetTarget} is out of bounds, using original target {targetCell}");
offsetTarget = targetCell;
}
// 创建 Skyfaller
Skyfaller skyfaller = SkyfallerMaker.SpawnSkyfaller(
Props.skyfallerDef,
offsetTarget,
parent.Map
);
if (skyfaller != null)
{
// 设置发射者信息(如果需要)
Thing launcher = GetLauncher();
if (launcher != null)
{
// 这里可以设置 Skyfaller 的发射者信息
// 具体取决于 Skyfaller 的实现
}
// 播放炮击特效
if (Props.spawnBombardmentEffect)
{
CreateBombardmentEffect(offsetTarget);
}
Log.Message($"OrbitalBombardment: Launched {Props.skyfallerDef.defName} at {offsetTarget}");
return true;
}
}
catch (System.Exception ex)
{
Log.Error($"Error launching orbital bombardment skyfaller: {ex}");
}
return false;
}
// 炮击特效
private void CreateBombardmentEffect(IntVec3 targetPos)
{
if (Props.bombardmentEffectDef != null)
{
MoteMaker.MakeStaticMote(
targetPos.ToVector3Shifted(),
parent.Map,
Props.bombardmentEffectDef,
Props.bombardmentEffectScale
);
}
}
private Thing GetLauncher()
{
FlyOver flyOver = parent as FlyOver;
// 如果需要,可以返回发射者信息
return parent;
}
public void SetConfirmedTargets(List<IntVec3> targets)
{
confirmedTargetCells.Clear();
firedCells.Clear();
shotsFired = 0;
currentBurstCount = 0;
currentLateralOffsetAngle = Props.lateralInitialOffsetAngle;
currentLongitudinalOffset = Props.longitudinalInitialOffset;
isForwardPhase = true;
confirmedTargetCells.AddRange(targets);
// 设置首次炮击时间
nextBombardmentTick = Find.TickManager.TicksGame + Props.initialDelayTicks;
Log.Message($"OrbitalBombardment: Set {confirmedTargetCells.Count} targets, " +
$"Lateral Mode: {Props.lateralOffsetMode}, " +
$"Longitudinal Mode: {Props.longitudinalOffsetMode}, " +
$"Initial Delay: {Props.initialDelayTicks} ticks");
if (confirmedTargetCells.Count > 0)
{
Log.Message($"First target: {confirmedTargetCells[0]}, Last target: {confirmedTargetCells[confirmedTargetCells.Count - 1]}");
}
}
public override void PostExposeData()
{
base.PostExposeData();
Scribe_Collections.Look(ref confirmedTargetCells, "confirmedTargetCells", LookMode.Value);
Scribe_Collections.Look(ref firedCells, "firedCells", LookMode.Value);
Scribe_Values.Look(ref currentLateralOffsetAngle, "currentLateralOffsetAngle", Props.lateralInitialOffsetAngle);
Scribe_Values.Look(ref currentLongitudinalOffset, "currentLongitudinalOffset", Props.longitudinalInitialOffset);
Scribe_Values.Look(ref shotsFired, "shotsFired", 0);
Scribe_Values.Look(ref isForwardPhase, "isForwardPhase", true);
Scribe_Values.Look(ref nextBombardmentTick, "nextBombardmentTick", 0);
Scribe_Values.Look(ref currentBurstCount, "currentBurstCount", 0);
}
// 调试方法
public void DebugBombardmentStatus()
{
Log.Message($"OrbitalBombardment Status:");
Log.Message($" Lateral - Angle: {currentLateralOffsetAngle:F1}°, Mode: {Props.lateralOffsetMode}");
Log.Message($" Longitudinal - Offset: {currentLongitudinalOffset:F1}, Mode: {Props.longitudinalOffsetMode}");
Log.Message($" Shots Fired: {shotsFired}, Forward Phase: {isForwardPhase}");
Log.Message($" Next Bombardment: {nextBombardmentTick}, Current Burst: {currentBurstCount}/{Props.burstSize}");
Log.Message($" Targets: {confirmedTargetCells.Count} remaining, {firedCells.Count} completed");
}
// 获取剩余目标数量
public int GetRemainingTargets()
{
return confirmedTargetCells.Count;
}
// 获取总进度
public float GetCompletionProgress()
{
int totalTargets = confirmedTargetCells.Count + firedCells.Count;
if (totalTargets == 0) return 1f;
return (float)firedCells.Count / totalTargets;
}
}
public class CompProperties_OrbitalBombardment : CompProperties
{
public ThingDef skyfallerDef; // Skyfaller 定义
public float range = 25f; // 炮击范围
// 炮击时序控制
public int initialDelayTicks = 60; // 初始延迟(游戏刻)
public int cooldownTicks = 30; // 冷却时间(游戏刻)
public bool burstMode = false; // 是否使用连发模式
public int burstSize = 3; // 连发数量
public int burstIntervalTicks = 10; // 连发间隔(游戏刻)
public int burstCooldownTicks = 60; // 连发后冷却(游戏刻)
// 横向偏移配置(左右)
public float lateralOffsetDistance = 2f;
public float lateralInitialOffsetAngle = 0f;
public float lateralMaxOffsetAngle = 45f;
public float lateralAngleIncrement = 5f;
public OffsetMode lateralOffsetMode = OffsetMode.Alternating;
// 纵向偏移配置(前后)
public float longitudinalInitialOffset = 0f;
public float longitudinalMinOffset = -2f;
public float longitudinalMaxOffset = 2f;
public LongitudinalOffsetMode longitudinalOffsetMode = LongitudinalOffsetMode.Alternating;
// 正弦波模式参数
public float longitudinalOscillationSpeed = 0.5f;
public float longitudinalOscillationAmplitude = 1f;
// 交替模式参数
public float longitudinalAlternationAmplitude = 1f;
// 渐进模式参数
public float longitudinalProgressionStep = 0.1f;
// 视觉效果和音效
public bool spawnBombardmentEffect = true;
public ThingDef bombardmentEffectDef;
public float bombardmentEffectScale = 1f;
public SoundDef bombardmentSound;
public CompProperties_OrbitalBombardment()
{
compClass = typeof(CompOrbitalBombardment);
}
}
}

View File

@@ -71,6 +71,8 @@
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="Ability\WULA_AbilityBombardment\CompAbilityEffect_Bombardment.cs" />
<Compile Include="Ability\WULA_AbilityBombardment\CompProperties_AbilityBombardment.cs" />
<Compile Include="BuildingComp\WULA_InitialFaction\CompProperties_InitialFaction.cs" />
<Compile Include="BuildingComp\WULA_MechanoidRecycler\Building_MechanoidRecycler.cs" />
<Compile Include="BuildingComp\WULA_MechanoidRecycler\CompProperties_MechanoidRecycler.cs" />
@@ -98,10 +100,10 @@
<Compile Include="Flyover\WULA_FlyOverDropPod\CompProperties_FlyOverDropPod.cs" />
<Compile Include="Flyover\WULA_FlyOverEscort\CompFlyOverEscort.cs" />
<Compile Include="Flyover\WULA_FlyOverEscort\CompProperties_FlyOverEscort.cs" />
<Compile Include="Flyover\WULA_FlyOverFacilities\CompAbilityEffect_RequireFlyOverFacility.cs" />
<Compile Include="Flyover\WULA_FlyOverFacilities\CompFlyOverFacilities.cs" />
<Compile Include="Flyover\WULA_FlyOverType\CompProperties_FlyOverType.cs" />
<Compile Include="Flyover\WULA_GroundStrafing\CompGroundStrafing.cs" />
<Compile Include="Flyover\WULA_OrbitalBombardment\CompAbilityOrbitalBombardment.cs" />
<Compile Include="Flyover\WULA_OrbitalBombardment\CompOrbitalBombardment.cs" />
<Compile Include="Flyover\WULA_SectorSurveillance\CompSectorSurveillance.cs" />
<Compile Include="Flyover\WULA_SendLetterAfterTicks\CompProperties_SendLetterAfterTicks.cs" />
<Compile Include="Flyover\WULA_SendLetterAfterTicks\CompSendLetterAfterTicks.cs" />