This commit is contained in:
2025-09-08 20:05:20 +08:00
parent ebe217223d
commit 30ee96a196
10 changed files with 546 additions and 4 deletions

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Project ToolsVersion="15.0"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
@@ -137,6 +138,11 @@
</ItemGroup>
<ItemGroup>
<Compile Include="HediffComp_Temperature.cs" />
<Compile Include="CompPawnFlight.cs" />
<Compile Include="CompProperties_PawnFlight.cs" />
<Compile Include="HarmonyPatches.cs" />
<Compile Include="PawnRenderNode_AnimatedAttachment.cs" />
<Compile Include="DynamicPawnRenderNodeSetup_FlightWings.cs" />
</ItemGroup>
<ItemGroup>
<Compile Include="Abilities\CompAbilityEffect_TrackingCharge.cs" />

View File

@@ -0,0 +1,127 @@
using UnityEngine;
using Verse;
using Verse.AI;
using RimWorld;
using System.Collections.Generic;
namespace ArachnaeSwarm
{
public class CompPawnFlight : ThingComp
{
private enum FlightState { Grounded, TakingOff, Flying, Landing }
private FlightState flightState;
private int flightTicks = -1;
private int flightCooldownTicks;
private int lerpTick;
private Dictionary<Rot4, List<Graphic>> cachedGraphics = new Dictionary<Rot4, List<Graphic>>();
private PawnRenderNode_AnimatedAttachment activeWingNode;
private Pawn Pawn => (Pawn)parent;
public CompProperties_PawnFlight Props => (CompProperties_PawnFlight)props;
public bool Flying => flightState != FlightState.Grounded; // Public property for Harmony patch
public bool ShouldShowWings => flightState != FlightState.Grounded;
public override void CompTick()
{
base.CompTick();
if (!parent.Spawned) return;
FlightState oldState = flightState;
switch (flightState)
{
case FlightState.TakingOff:
lerpTick++;
if (lerpTick >= Props.takeoffDurationTicks) { flightState = FlightState.Flying; lerpTick = 0; }
break;
case FlightState.Landing:
lerpTick++;
if (lerpTick >= Props.landingDurationTicks) { flightState = FlightState.Grounded; lerpTick = 0; flightCooldownTicks = (int)(Props.flightCooldownSeconds * 60f); }
break;
case FlightState.Flying:
flightTicks++;
if (flightTicks >= Props.maxFlightTimeSeconds * 60f) { flightState = FlightState.Landing; }
break;
case FlightState.Grounded:
if (flightCooldownTicks > 0) { flightCooldownTicks--; }
break;
}
if (oldState != flightState)
{
StateChanged();
}
}
private void StateChanged()
{
Pawn.Drawer.renderer.SetAllGraphicsDirty();
}
public void Notify_JobStarted(Job job)
{
bool isFlyingOrTakingOff = flightState == FlightState.Flying || flightState == FlightState.TakingOff;
bool wantsToFly = (job.def.tryStartFlying || (job.def.ifFlyingKeepFlying && isFlyingOrTakingOff));
if (wantsToFly && flightState == FlightState.Grounded && flightCooldownTicks <= 0 && Rand.Chance(Props.flightStartChanceOnJobStart))
{
flightState = FlightState.TakingOff;
flightTicks = 0;
lerpTick = 0;
StateChanged();
}
else if (!wantsToFly && isFlyingOrTakingOff)
{
flightState = FlightState.Landing;
lerpTick = 0;
StateChanged();
}
}
public void LinkToRenderNode(PawnRenderNode_AnimatedAttachment node)
{
activeWingNode = node;
}
public int GetCurrentFrame(int totalFrames)
{
if (totalFrames == 0) return 0;
int currentTickInAnim = (flightState == FlightState.Flying) ? flightTicks : lerpTick;
return (currentTickInAnim / Props.ticksPerFrame) % totalFrames;
}
public List<Graphic> GetGraphicsForRotation(Rot4 rot)
{
if (cachedGraphics.TryGetValue(rot, out var graphics)) return graphics;
var newGraphics = new List<Graphic>();
bool isFemale = Pawn.gender == Gender.Female && !string.IsNullOrEmpty(Props.flyingAnimationFramePathPrefixFemale);
string prefix = isFemale ? Props.flyingAnimationFramePathPrefixFemale : Props.flyingAnimationFramePathPrefix;
string suffix = (rot == Rot4.North) ? "_north" : (rot == Rot4.South) ? "_south" : "_east";
if (rot == Rot4.West) suffix = "_east";
for (int i = 1; i <= Props.flyingAnimationFrameCount; i++)
{
string path = prefix + i + suffix;
Color color = Props.inheritColors ? Pawn.story.SkinColor : Color.white;
var graphic = GraphicDatabase.Get<Graphic_Single>(path, ShaderDatabase.Transparent, Vector2.one * Props.drawSize, color);
newGraphics.Add(graphic);
}
cachedGraphics[rot] = newGraphics;
return newGraphics;
}
public override void PostExposeData()
{
base.PostExposeData();
Scribe_Values.Look(ref flightTicks, "flightTicks", -1);
Scribe_Values.Look(ref flightCooldownTicks, "flightCooldownTicks", 0);
Scribe_Values.Look(ref lerpTick, "lerpTick", 0);
Scribe_Values.Look(ref flightState, "flightState", FlightState.Grounded);
}
}
}

View File

@@ -0,0 +1,34 @@
using Verse;
using RimWorld;
using UnityEngine;
namespace ArachnaeSwarm
{
public class CompProperties_PawnFlight : CompProperties
{
// --- Animation ---
public string flyingAnimationFramePathPrefix;
public string flyingAnimationFramePathPrefixFemale;
public int flyingAnimationFrameCount = 1;
public int ticksPerFrame = 2;
// --- Render Node Properties (Defined directly here) ---
public Vector3 offset = Vector3.zero;
public float drawSize = 1f;
public bool inheritColors = false;
public PawnRenderNodeTagDef parentTagDef; // e.g., "Body"
public float baseLayer = 85f;
// --- Flight Mechanics ---
public int takeoffDurationTicks = 50;
public int landingDurationTicks = 50;
public float maxFlightTimeSeconds = 5f;
public float flightCooldownSeconds = 2f;
public float flightStartChanceOnJobStart = 0.5f;
public CompProperties_PawnFlight()
{
compClass = typeof(CompPawnFlight);
}
}
}

View File

@@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using Verse;
using RimWorld;
using HarmonyLib; // Required for AccessTools
namespace ArachnaeSwarm
{
public class DynamicPawnRenderNodeSetup_FlightWings : DynamicPawnRenderNodeSetup
{
public override bool HumanlikeOnly => false;
public override IEnumerable<(PawnRenderNode node, PawnRenderNode parent)> GetDynamicNodes(Pawn pawn, PawnRenderTree tree)
{
CompPawnFlight flightComp = pawn.GetComp<CompPawnFlight>();
if (flightComp != null && flightComp.ShouldShowWings)
{
// Create properties directly from CompProperties
var nodeProps = new PawnRenderNodeProperties
{
nodeClass = typeof(PawnRenderNode_AnimatedAttachment),
workerClass = AccessTools.TypeByName("Verse.PawnRenderNodeWorker_Flip"),
parentTagDef = flightComp.Props.parentTagDef ?? PawnRenderNodeTagDefOf.Body,
baseLayer = flightComp.Props.baseLayer
};
// Create a new DrawData struct and set its offset, then assign it.
DrawData drawData = new DrawData();
typeof(DrawData).GetField("offset").SetValueDirect(__makeref(drawData), flightComp.Props.offset);
nodeProps.drawData = drawData;
if (tree.ShouldAddNodeToTree(nodeProps))
{
var newNode = (PawnRenderNode_AnimatedAttachment)Activator.CreateInstance(
nodeProps.nodeClass, pawn, nodeProps, tree
);
flightComp.LinkToRenderNode(newNode);
yield return (node: newNode, parent: null);
}
}
yield break;
}
}
}

View File

@@ -0,0 +1,70 @@
using HarmonyLib;
using Verse;
using System.Reflection;
using RimWorld;
namespace ArachnaeSwarm
{
[StaticConstructorOnStartup]
public static class HarmonyPatches
{
private static readonly FieldInfo flightField = AccessTools.Field(typeof(Pawn), "flight");
static HarmonyPatches()
{
var harmony = new Harmony("com.arachnaeswarm.flightcomp");
harmony.Patch(AccessTools.Method(typeof(PawnComponentsUtility), nameof(PawnComponentsUtility.AddComponentsForSpawn)),
postfix: new HarmonyMethod(typeof(HarmonyPatches), nameof(DisableVanillaFlightTracker)));
harmony.Patch(AccessTools.PropertyGetter(typeof(Pawn), nameof(Pawn.Flying)),
postfix: new HarmonyMethod(typeof(HarmonyPatches), nameof(OverrideFlyingProperty)));
harmony.Patch(AccessTools.Method(typeof(Pawn), nameof(Pawn.ExposeData)),
prefix: new HarmonyMethod(typeof(HarmonyPatches), nameof(PreventVanillaFlightTrackerSave_Prefix)),
postfix: new HarmonyMethod(typeof(HarmonyPatches), nameof(PreventVanillaFlightTrackerSave_Postfix)));
}
public static void DisableVanillaFlightTracker(Pawn pawn)
{
if (pawn.TryGetComp<CompPawnFlight>() != null)
{
flightField?.SetValue(pawn, null);
}
}
public static void OverrideFlyingProperty(Pawn __instance, ref bool __result)
{
var comp = __instance.TryGetComp<CompPawnFlight>();
if (comp != null)
{
__result = comp.Flying;
}
}
// Correct fix: Use 'object' to store the instance, avoiding direct type reference at compile time.
private static object tempFlightTracker;
public static void PreventVanillaFlightTrackerSave_Prefix(Pawn __instance)
{
if (__instance.TryGetComp<CompPawnFlight>() != null)
{
object flightTrackerInstance = flightField?.GetValue(__instance);
if (flightTrackerInstance != null)
{
tempFlightTracker = flightTrackerInstance;
flightField.SetValue(__instance, null);
}
}
}
public static void PreventVanillaFlightTrackerSave_Postfix(Pawn __instance)
{
if (tempFlightTracker != null)
{
flightField?.SetValue(__instance, tempFlightTracker);
tempFlightTracker = null;
}
}
}
}

View File

@@ -0,0 +1,38 @@
using System.Collections.Generic;
using UnityEngine;
using Verse;
namespace ArachnaeSwarm
{
public class PawnRenderNode_AnimatedAttachment : PawnRenderNode
{
private CompPawnFlight flightComp;
private List<Graphic> cachedGraphics;
public PawnRenderNode_AnimatedAttachment(Pawn pawn, PawnRenderNodeProperties props, PawnRenderTree tree) : base(pawn, props, tree)
{
flightComp = pawn.GetComp<CompPawnFlight>();
}
public override Graphic GraphicFor(Pawn pawn)
{
if (flightComp == null) return null;
if (cachedGraphics == null)
{
cachedGraphics = flightComp.GetGraphicsForRotation(pawn.Rotation);
}
if (cachedGraphics.NullOrEmpty()) return null;
int frame = flightComp.GetCurrentFrame(cachedGraphics.Count);
return cachedGraphics[frame];
}
// We might need to override this if west-facing graphics need to be flipped.
// public override Mesh GetMesh(PawnDrawParms parms)
// {
// return base.GetMesh(parms);
// }
}
}