feat(Flyover): 天巫集群掠食 — 空投拦截系统

新增天巫种对敌方空投袭击的自动拦截功能:
- GameComponent_DropPodInterceptor: 全局拦截状态管理与核心逻辑
- Harmony Prefix 挂钩 EdgeDrop/CenterDrop Arrive(),拦截 1-3 个运输仓
- 被拦截 Pawn 击杀(Bite)后尸体以空投仓形式落地
- 拦截时生成天巫种 FlyOver 视觉飞越 + PositiveEvent 信件通知
- 引航种新增 ARA_ToggleDropPodIntercept 自释放能力切换开关
- 前置检查:开关启用 + 天巫升空 + 敌对派系,至少保留 1 名袭击者

新增文件:
- Source/.../GameComponent_DropPodInterceptor.cs
- Source/.../Patch_DropPodIntercept.cs
- Source/.../CompAbilityEffect_ToggleDropPodIntercept.cs
- Defs/AbilityDefs/Ability_DropPodIntercept.xml

修改文件:
- ARA_PawnKinds.xml (Skyraider abilities)
- AirStrike_Keys.xml (10 localization keys)
- ArachnaeSwarm.csproj (3 Compile entries)
This commit is contained in:
2026-02-17 15:54:35 +08:00
parent 4c2bf41f19
commit 988967439f
11 changed files with 598 additions and 3 deletions

View File

@@ -0,0 +1,79 @@
using RimWorld;
using Verse;
namespace ArachnaeSwarm
{
public class CompProperties_ToggleDropPodIntercept : CompProperties_AbilityEffect
{
public string enabledMessage = "ARA_InterceptDropPod_Enabled";
public string disabledMessage = "ARA_InterceptDropPod_Disabled";
public string noAircraftMessage = "ARA_InterceptDropPod_NoAircraft";
public ThingDef requiredAircraftType;
public CompProperties_ToggleDropPodIntercept()
{
compClass = typeof(CompAbilityEffect_ToggleDropPodIntercept);
}
}
public class CompAbilityEffect_ToggleDropPodIntercept : CompAbilityEffect
{
public new CompProperties_ToggleDropPodIntercept Props => (CompProperties_ToggleDropPodIntercept)props;
public override void Apply(LocalTargetInfo target, LocalTargetInfo dest)
{
base.Apply(target, dest);
GameComponent_DropPodInterceptor interceptor = Current.Game?.GetComponent<GameComponent_DropPodInterceptor>();
if (interceptor == null)
{
return;
}
bool enabled = interceptor.ToggleIntercept();
string messageKey = enabled ? Props.enabledMessage : Props.disabledMessage;
Messages.Message(messageKey.Translate(), parent.pawn, MessageTypeDefOf.PositiveEvent);
}
public override bool Valid(LocalTargetInfo target, bool throwMessages = false)
{
if (!base.Valid(target, throwMessages))
{
return false;
}
GameComponent_DropPodInterceptor interceptor = Current.Game?.GetComponent<GameComponent_DropPodInterceptor>();
if (interceptor == null)
{
return false;
}
if (!interceptor.HasAirborneTianwu(Props.requiredAircraftType))
{
if (throwMessages)
{
Messages.Message(Props.noAircraftMessage.Translate(), parent.pawn, MessageTypeDefOf.RejectInput);
}
return false;
}
return true;
}
public override string ExtraLabelMouseAttachment(LocalTargetInfo target)
{
GameComponent_DropPodInterceptor interceptor = Current.Game?.GetComponent<GameComponent_DropPodInterceptor>();
if (interceptor == null)
{
return base.ExtraLabelMouseAttachment(target);
}
string status = interceptor.IsInterceptEnabled
? "ARA_InterceptDropPod_StatusOn".Translate()
: "ARA_InterceptDropPod_StatusOff".Translate();
return "ARA_InterceptDropPod_Status".Translate(status);
}
}
}

View File

@@ -137,6 +137,7 @@
<Compile Include="Abilities\ARA_TerrainRestriction\CompProperties_TerrainRestriction.cs" />
<Compile Include="Abilities\CompAbilityEffect_DRM_Deaddustpop.cs" />
<Compile Include="Abilities\CompAbilityEffect_RandomHediff.cs" />
<Compile Include="Abilities\CompAbilityEffect_ToggleDropPodIntercept.cs" />
<Compile Include="Abilities\CompAbilityEffect_TransformCorpse.cs" />
<Compile Include="Abilities\ARA_PsychicBrainburn\CompAbilityEffect_PsychicBrainburn.cs" />
<Compile Include="Abilities\ARA_PsychicBrainburn\CompProperties_PsychicBrainburn.cs" />
@@ -269,10 +270,12 @@
<Compile Include="Flyover\ARA_ShipArtillery\CompShipArtillery.cs" />
<Compile Include="Flyover\ARA_SpawnFlyOver\CompAbilityEffect_SpawnFlyOver.cs" />
<Compile Include="Flyover\ARA_SpawnFlyOver\CompProperties_AbilitySpawnFlyOver.cs" />
<Compile Include="Flyover\GameComponent_DropPodInterceptor.cs" />
<Compile Include="Flyover\ThingclassFlyOver.cs" />
<Compile Include="HarmonyPatches\DestroyRemovesResearch\CompDestroyRemovesResearch.cs" />
<Compile Include="HarmonyPatches\DestroyRemovesResearch\CompProperties_DestroyRemovesResearch.cs" />
<Compile Include="HarmonyPatches\Patch_DraftableAnimals.cs" />
<Compile Include="HarmonyPatches\Patch_DropPodIntercept.cs" />
<Compile Include="HarmonyPatches\Patch_ForceTargetable.cs" />
<Compile Include="HarmonyPatches\Patch_PlantPollutionNullCheck.cs" />
<Compile Include="HarmonyPatches\Patch_ResearchManager_AddRemoveMethod.cs" />
@@ -467,4 +470,4 @@
<RemoveDir Directories="$(ProjectDir)obj\Debug" />
<RemoveDir Directories="$(ProjectDir)obj\Release" />
</Target>
</Project>
</Project>

View File

@@ -0,0 +1,177 @@
using RimWorld;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using Verse;
namespace ArachnaeSwarm
{
public class GameComponent_DropPodInterceptor : GameComponent
{
private const string DefaultAircraftDefName = "ARA_HiveCorvette_Entity";
private const string DefaultInterceptFlyOverDefName = "ARA_HiveCorvette_Fake";
private bool interceptEnabled;
public bool IsInterceptEnabled => interceptEnabled;
public GameComponent_DropPodInterceptor(Game game)
{
}
public bool ToggleIntercept()
{
interceptEnabled = !interceptEnabled;
ArachnaeLog.Debug($"DropPodInterceptor toggled: {interceptEnabled}");
return interceptEnabled;
}
public bool HasAirborneTianwu(ThingDef requiredAircraftDef = null)
{
WorldComponent_AircraftManager manager = Find.World?.GetComponent<WorldComponent_AircraftManager>();
if (manager == null || Faction.OfPlayer == null)
{
return false;
}
ThingDef aircraftDef = requiredAircraftDef ?? DefDatabase<ThingDef>.GetNamedSilentFail(DefaultAircraftDefName);
if (aircraftDef == null)
{
ArachnaeLog.Debug($"DropPodInterceptor: missing aircraft def {DefaultAircraftDefName}");
return false;
}
return manager.GetAvailableAircraftCount(aircraftDef, Faction.OfPlayer) > 0;
}
public bool TryInterceptDropPods(List<Pawn> pawns, IncidentParms parms, out List<Pawn> interceptedPawns)
{
interceptedPawns = new List<Pawn>();
if (pawns == null || pawns.Count <= 1 || !interceptEnabled)
{
return false;
}
if (parms == null || parms.faction == null || Faction.OfPlayer == null || !parms.faction.HostileTo(Faction.OfPlayer))
{
return false;
}
if (!HasAirborneTianwu())
{
return false;
}
Map map = parms.target as Map;
if (map == null)
{
ArachnaeLog.Debug("DropPodInterceptor: target map missing.");
return false;
}
int validPawnCount = pawns.Count(p => p != null);
if (validPawnCount <= 1)
{
return false;
}
int maxInterceptCount = Mathf.Min(3, validPawnCount - 1);
int interceptCount = Rand.RangeInclusive(1, maxInterceptCount);
List<Pawn> selected = pawns.Where(p => p != null).InRandomOrder().Take(interceptCount).ToList();
if (selected.Count == 0)
{
return false;
}
List<Thing> corpses = new List<Thing>();
foreach (Pawn pawn in selected)
{
if (!pawns.Remove(pawn))
{
continue;
}
interceptedPawns.Add(pawn);
if (!pawn.Dead)
{
pawn.Kill(new DamageInfo(DamageDefOf.Bite, 9999f));
}
Corpse corpse = pawn.Corpse;
if (corpse != null && !corpse.Spawned && !corpse.Destroyed)
{
corpses.Add(corpse);
}
}
if (interceptedPawns.Count == 0)
{
return false;
}
IntVec3 dropCenter = parms.spawnCenter.IsValid ? parms.spawnCenter : map.Center;
if (corpses.Count > 0)
{
DropPodUtility.DropThingsNear(dropCenter, map, corpses, leaveSlag: true);
}
SpawnInterceptionFlyOver(map, dropCenter);
SendInterceptionLetter(map, interceptedPawns.Count, dropCenter);
ArachnaeLog.Debug($"DropPodInterceptor: intercepted {interceptedPawns.Count} raid pawns.");
return true;
}
private void SpawnInterceptionFlyOver(Map map, IntVec3 dropCenter)
{
ThingDef flyOverDef = DefDatabase<ThingDef>.GetNamedSilentFail(DefaultInterceptFlyOverDefName);
if (flyOverDef == null)
{
ArachnaeLog.Debug($"DropPodInterceptor: missing fly over def {DefaultInterceptFlyOverDefName}.");
return;
}
IntVec3 start = GetRandomMapEdgeCell(map);
IntVec3 end = dropCenter.IsValid && dropCenter.InBounds(map) ? dropCenter : map.Center;
FlyOver.MakeFlyOver(flyOverDef, start, end, map, speed: 5f, height: 12f);
}
private static IntVec3 GetRandomMapEdgeCell(Map map)
{
int edge = Rand.RangeInclusive(0, 3);
switch (edge)
{
case 0:
return new IntVec3(Rand.RangeInclusive(0, map.Size.x - 1), 0, 0);
case 1:
return new IntVec3(map.Size.x - 1, 0, Rand.RangeInclusive(0, map.Size.z - 1));
case 2:
return new IntVec3(Rand.RangeInclusive(0, map.Size.x - 1), 0, map.Size.z - 1);
default:
return new IntVec3(0, 0, Rand.RangeInclusive(0, map.Size.z - 1));
}
}
private void SendInterceptionLetter(Map map, int interceptedCount, IntVec3 dropCenter)
{
string label = "ARA_InterceptDropPod_LetterLabel".Translate();
string text = "ARA_InterceptDropPod_LetterText".Translate(interceptedCount);
Find.LetterStack.ReceiveLetter(
label,
text,
LetterDefOf.PositiveEvent,
new TargetInfo(dropCenter, map));
}
public override void ExposeData()
{
base.ExposeData();
Scribe_Values.Look(ref interceptEnabled, "interceptEnabled", false);
}
}
}

View File

@@ -0,0 +1,46 @@
using HarmonyLib;
using RimWorld;
using System;
using System.Collections.Generic;
using Verse;
namespace ArachnaeSwarm
{
[HarmonyPatch(typeof(PawnsArrivalModeWorker_EdgeDrop), nameof(PawnsArrivalModeWorker_EdgeDrop.Arrive))]
public static class Patch_DropPodIntercept_EdgeDrop
{
[HarmonyPrefix]
public static bool Prefix(List<Pawn> pawns, IncidentParms parms)
{
return Patch_DropPodIntercept.InterceptPrefix(pawns, parms);
}
}
[HarmonyPatch(typeof(PawnsArrivalModeWorker_CenterDrop), nameof(PawnsArrivalModeWorker_CenterDrop.Arrive))]
public static class Patch_DropPodIntercept_CenterDrop
{
[HarmonyPrefix]
public static bool Prefix(List<Pawn> pawns, IncidentParms parms)
{
return Patch_DropPodIntercept.InterceptPrefix(pawns, parms);
}
}
public static class Patch_DropPodIntercept
{
public static bool InterceptPrefix(List<Pawn> pawns, IncidentParms parms)
{
try
{
GameComponent_DropPodInterceptor interceptor = Current.Game?.GetComponent<GameComponent_DropPodInterceptor>();
interceptor?.TryInterceptDropPods(pawns, parms, out _);
}
catch (Exception ex)
{
ArachnaeLog.Debug($"DropPodInterceptor prefix exception: {ex}");
}
return true;
}
}
}