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 + + PowerBeam + + + WULA_EnergyLance_Base + + 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 @@ + + + + + + + + + + +