+ 1{PAWN_nameDef} 是一只巨大的节肢类昆虫,多对附肢、镜面反光的外骨骼和扭动的分节身体足以引发人类心底埋藏的强烈恐惧感。\n\n额,你该不会真以为它们是一群美少女吧?-1
diff --git a/Content/Textures/ArachnaeSwarm/Wire.png b/Content/Textures/ArachnaeSwarm/Wire.png
new file mode 100644
index 0000000..17f9a1e
Binary files /dev/null and b/Content/Textures/ArachnaeSwarm/Wire.png differ
diff --git a/Source/ArachnaeSwarm/ARAFoodDispenserProperties.cs b/Source/ArachnaeSwarm/ARAFoodDispenserProperties.cs
new file mode 100644
index 0000000..7f62cfa
--- /dev/null
+++ b/Source/ArachnaeSwarm/ARAFoodDispenserProperties.cs
@@ -0,0 +1,12 @@
+using Verse;
+using Verse.Sound;
+
+namespace ArachnaeSwarm
+{
+ public class ARAFoodDispenserProperties : DefModExtension
+ {
+ public ThingDef thingToDispense;
+ public float nutritionCostPerDispense = 0.3f;
+ public SoundDef soundDispense;
+ }
+}
\ No newline at end of file
diff --git a/Source/ArachnaeSwarm/ArachnaeSwarm.csproj b/Source/ArachnaeSwarm/ArachnaeSwarm.csproj
index 1a67108..d91fef6 100644
--- a/Source/ArachnaeSwarm/ArachnaeSwarm.csproj
+++ b/Source/ArachnaeSwarm/ArachnaeSwarm.csproj
@@ -92,7 +92,6 @@
-
@@ -173,7 +172,6 @@
-
@@ -196,6 +194,9 @@
+
+
+
diff --git a/Source/ArachnaeSwarm/Building_ARANutrientDispenser.cs b/Source/ArachnaeSwarm/Building_ARANutrientDispenser.cs
new file mode 100644
index 0000000..733faab
--- /dev/null
+++ b/Source/ArachnaeSwarm/Building_ARANutrientDispenser.cs
@@ -0,0 +1,66 @@
+using RimWorld;
+using Verse;
+using Verse.Sound; // Ensure this is present
+
+namespace ArachnaeSwarm
+{
+ // By inheriting from Building_NutrientPasteDispenser, we automatically pass all "is Building_NutrientPasteDispenser" checks in the game's code.
+ public class Building_ARANutrientDispenser : Building_NutrientPasteDispenser
+ {
+ private CompRefuelableNutrition nutritionComp;
+ private ARAFoodDispenserProperties dispenserProps;
+
+ public override void SpawnSetup(Map map, bool respawningAfterLoad)
+ {
+ // We MUST call the base method for it to initialize the `powerComp` field,
+ // which we will provide via a dummy CompPowerPlant in the XML. This prevents NullReferenceExceptions.
+ base.SpawnSetup(map, respawningAfterLoad);
+ this.nutritionComp = this.GetComp();
+ this.dispenserProps = this.def.GetModExtension();
+ }
+
+ // We don't need to override CanDispenseNow. The base method is perfect if we handle its dependencies.
+ // The base CanDispenseNow checks powerComp.PowerOn (which our XML hack makes always true)
+ // and HasEnoughFeedstockInHoppers(), which we DO override below.
+
+ public override Thing TryDispenseFood()
+ {
+ // The base method is tightly coupled with hoppers. We must fully override it.
+ if (!this.HasEnoughFeedstockInHoppers()) // Directly check our condition
+ {
+ return null;
+ }
+
+ this.nutritionComp.ConsumeFuel(this.dispenserProps.nutritionCostPerDispense);
+
+ if (this.dispenserProps.soundDispense != null)
+ {
+ this.dispenserProps.soundDispense.PlayOneShot(new TargetInfo(this.Position, this.Map));
+ }
+
+ return ThingMaker.MakeThing(this.DispensableDef);
+ }
+
+ public override ThingDef DispensableDef
+ {
+ get
+ {
+ if (this.dispenserProps == null) return base.DispensableDef;
+ return this.dispenserProps.thingToDispense;
+ }
+ }
+
+ // --- Hopper-related overrides to decouple from them ---
+ public override Building AdjacentReachableHopper(Pawn reacher) => null;
+
+ public override Thing FindFeedInAnyHopper() => null;
+
+ public override bool HasEnoughFeedstockInHoppers()
+ {
+ // This is the crucial hook. The base CanDispenseNow and the AI's food search call this method.
+ // We replace the hopper check with our fuel check.
+ if (this.dispenserProps == null) return false;
+ return this.nutritionComp != null && this.nutritionComp.Fuel >= this.dispenserProps.nutritionCostPerDispense;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/ArachnaeSwarm/Patch_DispenserFoodSearch.cs b/Source/ArachnaeSwarm/Patch_DispenserFoodSearch.cs
new file mode 100644
index 0000000..ae95862
--- /dev/null
+++ b/Source/ArachnaeSwarm/Patch_DispenserFoodSearch.cs
@@ -0,0 +1,148 @@
+using HarmonyLib;
+using RimWorld;
+using Verse;
+using System.Linq;
+
+namespace ArachnaeSwarm
+{
+ [HarmonyPatch(typeof(FoodUtility), nameof(FoodUtility.BestFoodSourceOnMap))]
+ public static class Patch_DispenserFoodSearch
+ {
+ // A state variable to pass our found dispenser from Prefix to Postfix
+ private static Building_ARANutrientDispenser foundCustomDispenser = null;
+
+ ///
+ /// Runs BEFORE the original BestFoodSourceOnMap.
+ /// Its job is to specifically find OUR custom dispenser.
+ ///
+ [HarmonyPrefix]
+ public static bool Prefix(Pawn getter, Pawn eater, bool desperate = false, FoodPreferability maxPref = FoodPreferability.MealLavish)
+ {
+ // Reset state at the beginning of each check
+ foundCustomDispenser = null;
+
+ // First, check if our custom dispenser can even be a candidate.
+ // We find a representative def for our dispenser to get the meal properties.
+ // This is a bit of a hack, but necessary to know the preferability without an instance.
+ var representativeDef = ThingDef.Named("ARANutrientDispenser"); // Assuming this is the defName of your dispenser
+ var props = representativeDef?.GetModExtension();
+ var customMealDef = props?.thingToDispense;
+
+ if (customMealDef == null) {
+ // If we can't find our meal def, something is wrong with the XML.
+ // It's safer to not interfere.
+ return true;
+ }
+
+ // Don't interfere if the pawn is looking for something better than what we can offer.
+ if (maxPref < customMealDef.ingestible.preferability)
+ {
+ return true; // Let the original method run, it's looking for fancy food.
+ }
+
+ // Find the best available custom dispenser on the map
+ var bestDispenser = FindBestCustomDispenser(getter, eater, desperate);
+
+ if (bestDispenser != null)
+ {
+ // We found one! Store it for the Postfix.
+ foundCustomDispenser = bestDispenser;
+ Log.Message($"[ArachnaeSwarm Prefix] Found a potential custom dispenser for {eater.LabelShort}: {bestDispenser.Label}");
+ }
+
+ // ALWAYS let the original method run.
+ // This allows us to compare its result with ours in the Postfix, ensuring maximum compatibility.
+ return true;
+ }
+
+ ///
+ /// Runs AFTER the original BestFoodSourceOnMap.
+ /// Compares the original method's result with our custom dispenser (if any) and picks the best one.
+ ///
+ [HarmonyPostfix]
+ public static void Postfix(ref Thing __result, ref ThingDef foodDef, Pawn eater, Pawn getter)
+ {
+ if (foundCustomDispenser == null)
+ {
+ // Our prefix didn't find any custom dispensers, so we don't need to do anything.
+ return;
+ }
+
+ var customDispenserMealDef = foundCustomDispenser.DispensableDef;
+ if (customDispenserMealDef == null)
+ {
+ Log.Warning($"[ArachnaeSwarm Postfix] Custom dispenser {foundCustomDispenser.Label} has a null DispensableDef.");
+ return;
+ }
+
+ // If the original method found NO food, then our dispenser is the best (and only) choice.
+ if (__result == null)
+ {
+ Log.Message($"[ArachnaeSwarm Postfix] Original method found no food. Using our custom dispenser: {foundCustomDispenser.Label}");
+ __result = foundCustomDispenser;
+ foodDef = customDispenserMealDef;
+ return;
+ }
+
+ // Both we and the original method found food. We must now choose the better one.
+ // "FoodOptimality" is the score RimWorld uses to decide. Higher is better.
+ float ourScore = FoodUtility.FoodOptimality(eater, foundCustomDispenser, customDispenserMealDef, (getter.Position - foundCustomDispenser.Position).LengthManhattan);
+ float theirScore = FoodUtility.FoodOptimality(eater, __result, foodDef, (getter.Position - __result.Position).LengthManhattan);
+
+ Log.Message($"[ArachnaeSwarm Postfix] Comparing food sources: Our Dispenser (Score: {ourScore:F2}) vs Original Result '{__result.Label}' (Score: {theirScore:F2}).");
+
+ // If our dispenser is a better choice, override the result.
+ if (ourScore > theirScore)
+ {
+ Log.Message($"[ArachnaeSwarm Postfix] Our dispenser is better. Overriding result.");
+ __result = foundCustomDispenser;
+ foodDef = customDispenserMealDef;
+ }
+ else
+ {
+ Log.Message($"[ArachnaeSwarm Postfix] Original result is better or equal. Keeping it.");
+ }
+ }
+
+ ///
+ /// A helper to find the best custom dispenser for a pawn.
+ ///
+ private static Building_ARANutrientDispenser FindBestCustomDispenser(Pawn getter, Pawn eater, bool desperate)
+ {
+ if (getter.Map == null) return null;
+
+ var allCustomDispensers = getter.Map.listerBuildings.AllBuildingsColonistOfClass();
+
+ Building_ARANutrientDispenser bestDispenser = null;
+ float bestScore = float.MinValue;
+
+ foreach (var dispenser in allCustomDispensers)
+ {
+ var currentMealDef = dispenser.DispensableDef;
+ if (currentMealDef == null) continue;
+
+ // Check if the dispenser is usable
+ if (!dispenser.CanDispenseNow || dispenser.IsForbidden(getter) || !eater.WillEat(currentMealDef, getter))
+ {
+ continue;
+ }
+
+ // Check reachability
+ if (!getter.CanReach(dispenser, Verse.AI.PathEndMode.InteractionCell, Danger.Some))
+ {
+ continue;
+ }
+
+ // Calculate score
+ float score = FoodUtility.FoodOptimality(eater, dispenser, currentMealDef, (getter.Position - dispenser.Position).LengthManhattan);
+ if (score > bestScore)
+ {
+ bestScore = score;
+ bestDispenser = dispenser;
+ }
+ }
+
+ return bestDispenser;
+ }
+ }
+}
\ No newline at end of file