This commit is contained in:
2025-09-19 20:01:32 +08:00
parent b5fe07e968
commit 4fdfad7021
10 changed files with 342 additions and 4 deletions

Binary file not shown.

View File

@@ -33,6 +33,31 @@
<allowedArchonexusCount>150</allowedArchonexusCount>
</ThingDef>
<!-- The meal produced by the new dispenser -->
<ThingDef ParentName="MealBase">
<defName>ARA_NutrientPasteMeal</defName>
<label>阿拉克涅蜜晶糕</label>
<description>一种由虫蜜合成的黏糊糕点,能提供虫族生存所需,味道对于虫族来说不太可口,不过普通的人类也许会喜欢这种虫族合成的糕点。</description>
<graphicData>
<texPath>Things/Item/Meal/NutrientPaste</texPath>
<graphicClass>Graphic_StackCount</graphicClass>
</graphicData>
<statBases>
<MarketValue>10</MarketValue>
<Nutrition>2.0</Nutrition>
</statBases>
<ingestible>
<foodType>AnimalProduct</foodType>
<preferability>MealFine</preferability>
<joy>0.04</joy>
<joyKind>Gluttonous</joyKind>
<ingestEffect>EatVegetarian</ingestEffect>
<ingestSound>Meal_Eat</ingestSound>
<lowPriorityCaravanFood>true</lowPriorityCaravanFood>
<babiesCanIngest>true</babiesCanIngest>
</ingestible>
</ThingDef>
<ThingDef ParentName="ResourceBase">
<defName>ARA_Carapace</defName>
<label>甲壳素</label>

View File

@@ -0,0 +1,83 @@
<?xml version="1.0" encoding="utf-8" ?>
<Defs>
<ThingDef ParentName="BuildingBase">
<defName>ARANutrientDispenser</defName>
<label>阿拉克涅蜜晶虫</label>
<description>一个生产阿拉克涅蜜晶糕的虫虫。直接消耗虫蜜来生产可食用的蜜晶糕。</description>
<thingClass>ArachnaeSwarm.Building_ARANutrientDispenser</thingClass>
<graphicData>
<texPath>Things/Building/Production/NutrientDispenser</texPath>
<graphicClass>Graphic_Multi</graphicClass>
<color>(232,255,191)</color>
<shaderType>CutoutComplex</shaderType>
<drawSize>(3,4)</drawSize>
<damageData>
<rect>(0.02,0.25,2.96,2.85)</rect>
<cornerTL>Damage/Corner</cornerTL>
<cornerTR>Damage/Corner</cornerTR>
</damageData>
<shadowData>
<volume>(2.87,0.75,3.05)</volume>
<offset>(0,0,0.38)</offset>
</shadowData>
</graphicData>
<altitudeLayer>Building</altitudeLayer>
<passability>Impassable</passability>
<pathCost>150</pathCost>
<fillPercent>1.0</fillPercent>
<statBases>
<MaxHitPoints>250</MaxHitPoints>
<WorkToBuild>2000</WorkToBuild>
<Flammability>0.5</Flammability>
</statBases>
<size>(3,4)</size>
<costList>
<ARA_Carapace>50</ARA_Carapace>
</costList>
<tickerType>Normal</tickerType>
<terrainAffordanceNeeded>ARA_Creep</terrainAffordanceNeeded>
<comps>
<li Class="ArachnaeSwarm.CompProperties_RefuelableNutrition">
<fuelCapacity>10.0</fuelCapacity>
<fuelFilter>
<thingDefs>
<li>ARA_InsectJelly</li>
</thingDefs>
</fuelFilter>
<fuelGizmoLabel>虫蜜</fuelGizmoLabel>
<showAllowAutoRefuelToggle>true</showAllowAutoRefuelToggle>
<targetFuelLevelConfigurable>true</targetFuelLevelConfigurable>
<consumeFuelOnlyWhenUsed>true</consumeFuelOnlyWhenUsed>
</li>
<li Class="CompProperties_AffectedByFacilities">
<linkableFacilities>
<li>ARA_NutrientNetworkTower</li>
</linkableFacilities>
</li>
<!-- This component acts as a perpetual power source with no consumption, ensuring powerComp.PowerOn is always true -->
<li Class="CompProperties_Power">
<compClass>CompPowerPlant</compClass>
<basePowerConsumption>-1</basePowerConsumption> <!-- Negative value makes it a generator -->
<transmitsPower>false</transmitsPower> <!-- This is CRITICAL. It prevents the building from powering the whole grid. -->
</li>
</comps>
<modExtensions>
<li Class="ArachnaeSwarm.ARAFoodDispenserProperties">
<thingToDispense>ARA_NutrientPasteMeal</thingToDispense>
<nutritionCostPerDispense>0.5</nutritionCostPerDispense>
<soundDispense>DispensePaste</soundDispense>
</li>
</modExtensions>
<building>
<isMealSource>true</isMealSource>
</building>
<interactionCellOffset>(0,0,3)</interactionCellOffset>
<hasInteractionCell>true</hasInteractionCell>
<designationCategory>ARA_Buildings</designationCategory>
<researchPrerequisites>
<li>NutrientPaste</li>
</researchPrerequisites>
</ThingDef>
</Defs>

View File

@@ -19,7 +19,7 @@
<tickerType>Normal</tickerType> <!-- 改为 Normal 以匹配 CompRefuelable 的要求 -->
<fillPercent>0.5</fillPercent>
<statBases>
<MaxHitPoints>300</MaxHitPoints>
<MaxHitPoints>3000</MaxHitPoints>
<WorkToBuild>3000</WorkToBuild>
<Mass>20</Mass>
<Flammability>0.5</Flammability>
@@ -45,6 +45,7 @@
<li>ARA_JellyVat</li>
<li>ARA_GrowthVat</li>
<li>ARA_MorphableResearchBench</li>
<li>ARANutrientDispenser</li>
</linkableBuildings>
<maxDistance>80</maxDistance> <!-- 供能范围 -->
<maxSimultaneous>10</maxSimultaneous>
@@ -59,9 +60,10 @@
<li>ARA_JellyVat</li>
<li>ARA_GrowthVat</li>
<li>ARA_MorphableResearchBench</li>
<li>ARANutrientDispenser</li>
</linkableBuildings>
<maxDistance>80</maxDistance>
<lineTexturePath>Things/Special/Power/Wire</lineTexturePath>
<lineTexturePath>ArachnaeSwarm/Wire</lineTexturePath>
</li>
<!-- 自身的燃料库 -->

View File

@@ -5,6 +5,7 @@
<commonality>0</commonality>
<degreeDatas>
<li>
<degree>1</degree>
<label>节肢类类动物</label>
<description>{PAWN_nameDef} 是一只巨大的节肢类昆虫,多对附肢、镜面反光的外骨骼和扭动的分节身体足以引发人类心底埋藏的强烈恐惧感。\n\n额你该不会真以为它们是一群美少女吧</description>
<marketValueFactorOffset>-1</marketValueFactorOffset>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -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;
}
}

View File

@@ -92,7 +92,6 @@
<Compile Include="Abilities\TrackingCharge\CompProperties_TrackingCharge.cs" />
<Compile Include="Abilities\TrackingCharge\PawnFlyer_TrackingCharge.cs" />
<Compile Include="Abilities\TrackingCharge\Verb_CastAbilityTrackingCharge.cs" />
<Compile Include="ARADefOf.cs" />
<Compile Include="Building_Comps\ARA_BuildingTerrainSpawn\CompDelayedTerrainSpawn.cs" />
<Compile Include="Building_Comps\ARA_BuildingTerrainSpawn\CompProperties_DelayedTerrainSpawn.cs" />
<Compile Include="Building_Comps\ARA_CompInteractiveProducer\CompInteractiveProducer.cs" />
@@ -173,7 +172,6 @@
<Compile Include="Thing_Comps\ARA_CustomUniqueWeapon\CompCustomUniqueWeapon.cs" />
<Compile Include="Thing_Comps\ARA_CustomUniqueWeapon\CompProperties_CustomUniqueWeapon.cs" />
<Compile Include="Thing_Comps\OPToxicGas.cs" />
<Compile Include="Thoughts\ARA_CreepyCrawly.cs" />
<Compile Include="Verbs\Cleave\CompCleave.cs" />
<Compile Include="Verbs\Cleave\Verb_MeleeAttack_Cleave.cs" />
<Compile Include="Verbs\ExplosiveBeam\Verb_ShootBeamExplosive.cs" />
@@ -196,6 +194,9 @@
<Compile Include="Verbs\VerbProperties_Excalibur.cs" />
<Compile Include="Verbs\WeaponStealBeam\Verb_ShootWeaponStealBeam.cs" />
<Compile Include="Verbs\WeaponStealBeam\VerbProperties_WeaponStealBeam.cs" />
<Compile Include="Building_ARANutrientDispenser.cs" />
<Compile Include="ARAFoodDispenserProperties.cs" />
<Compile Include="Patch_DispenserFoodSearch.cs" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- 自定义清理任务删除obj文件夹中的临时文件 -->

View File

@@ -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<CompRefuelableNutrition>();
this.dispenserProps = this.def.GetModExtension<ARAFoodDispenserProperties>();
}
// 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;
}
}
}

View File

@@ -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;
/// <summary>
/// Runs BEFORE the original BestFoodSourceOnMap.
/// Its job is to specifically find OUR custom dispenser.
/// </summary>
[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<ARAFoodDispenserProperties>();
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;
}
/// <summary>
/// Runs AFTER the original BestFoodSourceOnMap.
/// Compares the original method's result with our custom dispenser (if any) and picks the best one.
/// </summary>
[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.");
}
}
/// <summary>
/// A helper to find the best custom dispenser for a pawn.
/// </summary>
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>();
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;
}
}
}