This commit is contained in:
2025-09-01 20:39:33 +08:00
parent 315156f705
commit e6d855924f
12 changed files with 409 additions and 7 deletions

Binary file not shown.

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8" ?>
<Defs>
<AbilityDef ParentName="AbilityTouchBase">
<defName>ARA_BindDrone</defName>
<label>虫群联结</label>
<description>Allows the hive mind master to bind with an unlinked drone, bringing it under direct psychic control.</description>
<iconPath>UI/Commands/EggSpew</iconPath> <!-- Placeholder: You'll need to create this icon -->
<jobDef>CastAbilityOnThing</jobDef>
<targetRequired>true</targetRequired>
<cooldownTicksRange>60</cooldownTicksRange> <!-- 1 second cooldown -->
<verbProperties>
<verbClass>Verb_CastAbility</verbClass>
<warmupTime>0.5</warmupTime>
<range>99</range> <!-- Short range, like touch -->
<targetParams>
<canTargetPawns>true</canTargetPawns>
<canTargetBuildings>false</canTargetBuildings>
<canTargetAnimals>true</canTargetAnimals>
<canTargetMechs>false</canTargetMechs>
<canTargetCorpses>false</canTargetCorpses>
<canTargetSelf>false</canTargetSelf>
</targetParams>
</verbProperties>
<comps>
<li Class="ArachnaeSwarm.CompProperties_AbilityBindDrone">
<!-- No custom properties needed for now -->
</li>
</comps>
</AbilityDef>
</Defs>

View File

@@ -15,6 +15,9 @@
<Shooting>2</Shooting>
<Melee>2</Melee>
</skillGains>
<forcedHediffs>
<li>ARA_HiveMindMaster</li>
</forcedHediffs>
<spawnCategories>
<li>ArachnaeQueen_spawnCategoriesA</li>
@@ -53,6 +56,10 @@
<li>ArachnaeNode_spawnCategoriesA</li>
</spawnCategories>
<forcedHediffs>
<li>ARA_HiveMindDrone</li>
</forcedHediffs>
<requiresSpawnCategory>true</requiresSpawnCategory>
</AlienRace.AlienBackstoryDef>

View File

@@ -3,7 +3,7 @@
<HediffDef>
<defName>ARA_HiveMindMaster</defName>
<label>hive mind master</label>
<label>阿拉克涅主巢</label>
<description>The central node of a hive mind, connected to multiple drone beings.</description>
<hediffClass>ArachnaeSwarm.Hediff_HiveMindMaster</hediffClass>
<defaultLabelColor>(0.8, 0.3, 0.8)</defaultLabelColor>
@@ -19,18 +19,20 @@
</stages>
</HediffDef>
<HediffDef ParentName="HediffWithTargetBase">
<HediffDef>
<defName>ARA_HiveMindDrone</defName>
<label>hive mind drone</label>
<label>阿拉克涅工蜂</label>
<description>A drone being, psychically linked to a master node. If the master dies, this unit will cease to function.</description>
<hediffClass>ArachnaeSwarm.Hediff_HiveMindDrone</hediffClass>
<defaultLabelColor>(0.6, 0.4, 0.8)</defaultLabelColor>
<isBad>false</isBad>
<scenarioCanAdd>true</scenarioCanAdd>
<stages>
<li>
<label>linked</label>
<comps>
<li Class="ArachnaeSwarm.HediffCompProperties_HiveMindDrone">
<unlinkedDieDelayTicks>6400</unlinkedDieDelayTicks> <!-- Default to 30 seconds -->
</li>
</comps>
<stages>
</stages>
</HediffDef>

View File

@@ -16,6 +16,7 @@
<abilities>
<li>ARA_EggSpew</li>
<li>ARA_AcidSprayBurst</li>
<li>ARA_BindDrone</li>
</abilities>
<xenotypeSet>
<xenotypeChances>

View File

@@ -77,6 +77,12 @@
<Compile Include="Hediffs\Hediff_CurseFlame.cs" />
<Compile Include="CompAbilityEffect_NeedCost.cs" />
<Compile Include="CompAbilityEffect_BodyPartCheck.cs" />
<Compile Include="Hediff_HiveMindMaster.cs" />
<Compile Include="Hediff_HiveMindDrone.cs" />
<Compile Include="HediffCompProperties_HiveMindDrone.cs" />
<Compile Include="HediffComp_HiveMindDrone.cs" />
<Compile Include="CompAbilityEffect_BindDrone.cs" />
<Compile Include="CompProperties_AbilityBindDrone.cs" />
</ItemGroup>
<ItemGroup />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />

View File

@@ -0,0 +1,108 @@
using RimWorld;
using Verse;
using System.Linq; // For LINQ operations
namespace ArachnaeSwarm
{
public class CompAbilityEffect_BindDrone : CompAbilityEffect
{
public override void Apply(LocalTargetInfo target, LocalTargetInfo dest)
{
base.Apply(target, dest);
Pawn dronePawn = target.Pawn;
Pawn masterPawn = parent.pawn; // The pawn casting the ability
if (masterPawn != null && dronePawn != null)
{
Hediff_HiveMindMaster masterHediff = masterPawn.health.hediffSet.GetFirstHediffOfDef(HediffDef.Named("ARA_HiveMindMaster")) as Hediff_HiveMindMaster;
if (masterHediff != null)
{
if (masterHediff.TryBindDrone(dronePawn))
{
Messages.Message($"Successfully bound {dronePawn.LabelShort} to {masterPawn.LabelShort}'s hive mind.", MessageTypeDefOf.PositiveEvent, historical: false);
}
else
{
Messages.Message($"Failed to bind {dronePawn.LabelShort} to {masterPawn.LabelShort}'s hive mind. Check logs for details.", MessageTypeDefOf.NegativeEvent, historical: false);
}
}
else
{
Log.Error($"[ArachnaeSwarm] Master {masterPawn.LabelShort} tried to bind a drone but does not have Hediff_HiveMindMaster.");
}
}
}
public override bool Valid(LocalTargetInfo target, bool throwMessages = false)
{
if (!base.Valid(target, throwMessages))
{
return false;
}
Pawn dronePawn = target.Pawn;
Pawn masterPawn = parent.pawn;
// Target must be a pawn
if (dronePawn == null)
{
if (throwMessages)
{
Messages.Message("MustTargetPawn".Translate(parent.def.label), MessageTypeDefOf.RejectInput, historical: false);
}
return false;
}
// Target must be on the same map as the caster
if (dronePawn.Map != masterPawn.Map)
{
if (throwMessages)
{
Messages.Message("CannotTargetDifferentMap".Translate(), MessageTypeDefOf.RejectInput, historical: false);
}
return false;
}
// Target must have ARA_HiveMindDrone hediff
Hediff_HiveMindDrone droneHediff = dronePawn.health.hediffSet.GetFirstHediffOfDef(HediffDef.Named("ARA_HiveMindDrone")) as Hediff_HiveMindDrone;
if (droneHediff == null)
{
if (throwMessages)
{
Messages.Message($"Target {dronePawn.LabelShort} does not have the 'ARA_HiveMindDrone' hediff.", MessageTypeDefOf.RejectInput, historical: false);
}
return false;
}
// Target must not be already bound to another master
if (droneHediff.target != null && droneHediff.target != masterPawn)
{
if (throwMessages)
{
Messages.Message($"Target {dronePawn.LabelShort} is already bound to {droneHediff.target.LabelShort}.", MessageTypeDefOf.RejectInput, historical: false);
}
return false;
}
// Caster must have ARA_HiveMindMaster hediff
Hediff_HiveMindMaster masterHediff = masterPawn.health.hediffSet.GetFirstHediffOfDef(HediffDef.Named("ARA_HiveMindMaster")) as Hediff_HiveMindMaster;
if (masterHediff == null)
{
if (throwMessages)
{
Messages.Message($"Caster {masterPawn.LabelShort} does not have the 'ARA_HiveMindMaster' hediff.", MessageTypeDefOf.RejectInput, historical: false);
}
return false;
}
// All checks passed
return true;
}
public override bool CanApplyOn(LocalTargetInfo target, LocalTargetInfo dest)
{
return Valid(target);
}
}
}

View File

@@ -0,0 +1,13 @@
using Verse;
using RimWorld;
namespace ArachnaeSwarm
{
public class CompProperties_AbilityBindDrone : CompProperties_AbilityEffect
{
public CompProperties_AbilityBindDrone()
{
this.compClass = typeof(CompAbilityEffect_BindDrone);
}
}
}

View File

@@ -0,0 +1,14 @@
using Verse;
namespace ArachnaeSwarm
{
public class HediffCompProperties_HiveMindDrone : HediffCompProperties
{
public int unlinkedDieDelayTicks = 1800; // Default to 30 seconds
public HediffCompProperties_HiveMindDrone()
{
this.compClass = typeof(HediffComp_HiveMindDrone); // Reference the Comp class
}
}
}

View File

@@ -0,0 +1,57 @@
using Verse;
using RimWorld;
namespace ArachnaeSwarm
{
public class HediffComp_HiveMindDrone : HediffComp
{
public HediffCompProperties_HiveMindDrone Props => (HediffCompProperties_HiveMindDrone)this.props;
public int TicksUnlinked => ticksUnlinked; // Expose as public property
private int ticksUnlinked = 0;
public override void CompExposeData()
{
base.CompExposeData();
Scribe_Values.Look(ref ticksUnlinked, "ticksUnlinked", 0);
}
public override void CompPostTick(ref float severityAdjustment)
{
base.CompPostTick(ref severityAdjustment);
// Only check if pawn is spawned and on a map
if (parent.pawn.Spawned && parent.pawn.Map != null)
{
// We use parent.pawn.IsHashIntervalTick(60) for performance
if (!parent.pawn.IsHashIntervalTick(60))
return;
Hediff_HiveMindDrone droneHediff = parent as Hediff_HiveMindDrone;
if (droneHediff == null) return; // Should not happen
Pawn masterPawn = droneHediff.target as Pawn;
if (masterPawn == null || masterPawn.Destroyed || masterPawn.Dead || !masterPawn.health.hediffSet.HasHediff(HediffDef.Named("ARA_HiveMindMaster")))
{
// Master is invalid or unlinked, start/continue unlinked timer
ticksUnlinked += 60; // Increment by 60 because we check every 60 ticks
if (ticksUnlinked >= Props.unlinkedDieDelayTicks)
{
Log.Message($"[ArachnaeSwarm] Drone {parent.pawn.LabelShort} was unlinked from master for too long and will die. Forcing death.");
// Ensure the pawn is killed only once and prevent further ticks
if (!parent.pawn.Dead && !parent.pawn.Destroyed)
{
parent.pawn.Kill(null, parent);
}
}
}
else
{
// Master is valid, reset unlinked timer
ticksUnlinked = 0;
}
}
}
}
}

View File

@@ -0,0 +1,73 @@
using System.Linq;
using RimWorld;
using Verse;
namespace ArachnaeSwarm
{
public class Hediff_HiveMindDrone : HediffWithTarget
{
public override string LabelBase
{
get
{
string baseLabel = base.LabelBase + " (" + (target != null ? target.LabelShortCap : "未连接") + ")";
// Get the HediffComp_HiveMindDrone to access ticksUnlinked
HediffComp_HiveMindDrone comp = this.TryGetComp<HediffComp_HiveMindDrone>();
if (comp != null)
{
// Safely cast target to Pawn for Dead and health checks
Pawn masterPawn = target as Pawn;
if (masterPawn == null || masterPawn.Destroyed || masterPawn.Dead || !masterPawn.health.hediffSet.HasHediff(HediffDef.Named("ARA_HiveMindMaster")))
{
float timeLeftSecs = (comp.Props.unlinkedDieDelayTicks - comp.TicksUnlinked) / 60f;
if (timeLeftSecs > 0)
{
return baseLabel + " (死亡倒计时: " + timeLeftSecs.ToString("F1") + "s)";
}
}
}
return baseLabel;
}
}
public override void PostAdd(DamageInfo? dinfo)
{
base.PostAdd(dinfo);
// No direct linking in PostAdd, master will link manually
}
public override bool ShouldRemove
{
get
{
// Only remove if base ShouldRemove is true, OR if pawn is dead
// We do NOT want to remove it just because target is invalid, as the Comp will handle delayed death
if (base.ShouldRemove && pawn.Dead)
{
return true;
}
// Also remove if pawn is no longer spawned or on a map
if (!pawn.Spawned || pawn.Map == null)
{
return true;
}
return false; // Let the Comp handle the unlinked death
}
}
public override void PostRemoved()
{
base.PostRemoved();
// Deregister from the master when this hediff is removed
// Ensure target is a Pawn and not dead before attempting to deregister
if (this.target is Pawn master && master != null && !master.Destroyed && !master.Dead)
{
var masterHediff = master.health.hediffSet.GetFirstHediffOfDef(HediffDef.Named("ARA_HiveMindMaster")) as Hediff_HiveMindMaster;
masterHediff?.DeregisterDrone(this.pawn);
}
}
// PostTick logic moved to HediffComp_HiveMindDrone
}
}

View File

@@ -0,0 +1,86 @@
using System.Collections.Generic;
using System.Linq;
using RimWorld;
using Verse;
namespace ArachnaeSwarm
{
public class Hediff_HiveMindMaster : Hediff
{
private List<Pawn> drones = new List<Pawn>();
public override string LabelInBrackets => drones.Count.ToString();
public override void ExposeData()
{
base.ExposeData();
Scribe_Collections.Look(ref drones, "drones", LookMode.Reference);
if (drones == null)
{
drones = new List<Pawn>();
}
}
public bool TryBindDrone(Pawn drone)
{
if (drone == null || drone.Dead || !drone.Spawned || drone.Map != this.pawn.Map)
{
Log.Message($"[ArachnaeSwarm] Cannot bind drone {drone?.LabelShort ?? "null"}: Invalid pawn state.");
return false;
}
Hediff_HiveMindDrone droneHediff = drone.health.hediffSet.GetFirstHediffOfDef(HediffDef.Named("ARA_HiveMindDrone")) as Hediff_HiveMindDrone;
if (droneHediff == null)
{
Log.Message($"[ArachnaeSwarm] Cannot bind drone {drone.LabelShort}: Does not have ARA_HiveMindDrone hediff.");
return false;
}
if (droneHediff.target != null && droneHediff.target != this.pawn)
{
Log.Message($"[ArachnaeSwarm] Cannot bind drone {drone.LabelShort}: Already bound to another master ({droneHediff.target.LabelShort}).");
return false;
}
if (drones.Contains(drone))
{
Log.Message($"[ArachnaeSwarm] Drone {drone.LabelShort} is already bound to this master.");
return false;
}
droneHediff.target = this.pawn; // Set the drone's target to this master
drones.Add(drone);
UpdateSeverity();
Log.Message($"[ArachnaeSwarm] Master {this.pawn.LabelShort} successfully bound drone {drone.LabelShort}.");
return true;
}
public void DeregisterDrone(Pawn drone)
{
if (drones.Contains(drone))
{
drones.Remove(drone);
UpdateSeverity();
}
}
private void UpdateSeverity()
{
this.Severity = drones.Count;
}
public override void PostRemoved()
{
base.PostRemoved();
// Kill all drones when the master hediff is removed (e.g., master dies)
foreach (var drone in drones.ToList()) // ToList() to avoid collection modification issues
{
if (drone != null && !drone.Dead)
{
Log.Message($"[ArachnaeSwarm] Master {pawn.LabelShort} died, killing drone {drone.LabelShort}.");
drone.Kill(null, this);
}
}
}
}
}