整理scoure

This commit is contained in:
2025-10-31 09:57:45 +08:00
parent 9e6aa98830
commit 8fee1bcfba
103 changed files with 5547 additions and 916 deletions

View File

@@ -0,0 +1,195 @@
using RimWorld;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEngine;
using Verse;
using Verse.AI.Group;
namespace WulaFallenEmpire
{
public class CompAutoMechCarrier : CompMechCarrier
{
#region Reflected Fields
private static FieldInfo spawnedPawnsField;
private static FieldInfo cooldownTicksRemainingField;
private static FieldInfo innerContainerField;
private List<Pawn> SpawnedPawns
{
get
{
if (spawnedPawnsField == null)
spawnedPawnsField = typeof(CompMechCarrier).GetField("spawnedPawns", BindingFlags.NonPublic | BindingFlags.Instance);
return (List<Pawn>)spawnedPawnsField.GetValue(this);
}
}
private int CooldownTicksRemaining
{
get
{
if (cooldownTicksRemainingField == null)
cooldownTicksRemainingField = typeof(CompMechCarrier).GetField("cooldownTicksRemaining", BindingFlags.NonPublic | BindingFlags.Instance);
return (int)cooldownTicksRemainingField.GetValue(this);
}
set
{
if (cooldownTicksRemainingField == null)
cooldownTicksRemainingField = typeof(CompMechCarrier).GetField("cooldownTicksRemaining", BindingFlags.NonPublic | BindingFlags.Instance);
cooldownTicksRemainingField.SetValue(this, value);
}
}
private ThingOwner InnerContainer
{
get
{
if (innerContainerField == null)
innerContainerField = typeof(CompMechCarrier).GetField("innerContainer", BindingFlags.NonPublic | BindingFlags.Instance);
return (ThingOwner)innerContainerField.GetValue(this);
}
}
#endregion
public CompProperties_AutoMechCarrier AutoProps => (CompProperties_AutoMechCarrier)props;
private int TotalPawnCapacity => AutoProps.productionQueue.Sum(e => e.count);
private int LiveSpawnedPawnsCount(PawnKindDef kind)
{
SpawnedPawns.RemoveAll(p => p == null || p.Destroyed);
return SpawnedPawns.Count(p => p.kindDef == kind);
}
private AcceptanceReport CanSpawnNow(PawnKindDef kind)
{
if (parent is Pawn pawn && (pawn.IsSelfShutdown() || !pawn.Awake() || pawn.Downed || pawn.Dead || !pawn.Spawned))
return false;
if (CooldownTicksRemaining > 0)
return "CooldownTime".Translate() + " " + CooldownTicksRemaining.ToStringSecondsFromTicks();
PawnProductionEntry entry = AutoProps.productionQueue.First(e => e.pawnKind == kind);
int cost = entry.cost ?? Props.costPerPawn;
if (!AutoProps.freeProduction && InnerContainer.TotalStackCountOfDef(Props.fixedIngredient) < cost)
return "MechCarrierNotEnoughResources".Translate();
return true;
}
private void TrySpawnPawn(PawnKindDef kind)
{
PawnGenerationRequest request = new PawnGenerationRequest(kind, parent.Faction, PawnGenerationContext.NonPlayer, -1, forceGenerateNewPawn: true);
Pawn pawn = PawnGenerator.GeneratePawn(request);
GenSpawn.Spawn(pawn, parent.Position, parent.Map);
SpawnedPawns.Add(pawn);
if (parent is Pawn p && p.GetLord() != null)
p.GetLord().AddPawn(pawn);
if (!AutoProps.freeProduction)
{
PawnProductionEntry entry = AutoProps.productionQueue.First(e => e.pawnKind == kind);
int costLeft = entry.cost ?? Props.costPerPawn;
List<Thing> things = new List<Thing>(InnerContainer);
for (int j = 0; j < things.Count; j++)
{
Thing thing = InnerContainer.Take(things[j], Mathf.Min(things[j].stackCount, costLeft));
costLeft -= thing.stackCount;
thing.Destroy();
if (costLeft <= 0) break;
}
}
PawnProductionEntry spawnEntry = AutoProps.productionQueue.First(e => e.pawnKind == kind);
CooldownTicksRemaining = spawnEntry.cooldownTicks ?? Props.cooldownTicks;
if (Props.spawnedMechEffecter != null)
EffecterTrigger(Props.spawnedMechEffecter, Props.attachSpawnedMechEffecter, pawn);
if (Props.spawnEffecter != null)
EffecterTrigger(Props.spawnEffecter, Props.attachSpawnedEffecter, parent);
}
private void EffecterTrigger(EffecterDef effecterDef, bool attach, Thing target)
{
Effecter effecter = new Effecter(effecterDef);
effecter.Trigger(attach ? ((TargetInfo)target) : new TargetInfo(target.Position, target.Map), TargetInfo.Invalid);
effecter.Cleanup();
}
public override void CompTick()
{
base.CompTick();
if (parent.IsHashIntervalTick(60)) // 每秒检查一次
{
// 检查是否有抑制生产的Hediff
if (AutoProps.disableHediff != null && (parent as Pawn)?.health.hediffSet.HasHediff(AutoProps.disableHediff) == true)
{
return; // 有Hediff停止生产
}
// 1. 先检查是否满员
bool isFull = true;
foreach (var entry in AutoProps.productionQueue)
{
if (LiveSpawnedPawnsCount(entry.pawnKind) < entry.count)
{
isFull = false;
break;
}
}
if (isFull)
{
return; // 如果已满员,则不进行任何操作,包括冷却计时
}
// 2. 如果未满员,才检查冷却时间
if (CooldownTicksRemaining > 0) return;
// 3. 寻找空位并生产
foreach (var entry in AutoProps.productionQueue)
{
if (LiveSpawnedPawnsCount(entry.pawnKind) < entry.count)
{
if (CanSpawnNow(entry.pawnKind).Accepted)
{
TrySpawnPawn(entry.pawnKind);
break; // 每次只生产一个
}
}
}
}
}
public override IEnumerable<Gizmo> CompGetGizmosExtra()
{
// 移除所有Gizmo逻辑
return Enumerable.Empty<Gizmo>();
}
public override string CompInspectStringExtra()
{
SpawnedPawns.RemoveAll(p => p == null || p.Destroyed);
string text = "Pawns: " + SpawnedPawns.Count + " / " + TotalPawnCapacity;
foreach (var entry in AutoProps.productionQueue)
{
text += $"\n- {entry.pawnKind.LabelCap}: {LiveSpawnedPawnsCount(entry.pawnKind)} / {entry.count}";
}
if (CooldownTicksRemaining > 0)
{
text += "\n" + "CooldownTime".Translate() + ": " + CooldownTicksRemaining.ToStringSecondsFromTicks();
}
if (!AutoProps.freeProduction)
{
text += "\n" + base.CompInspectStringExtra();
}
return text;
}
}
}

View File

@@ -0,0 +1,54 @@
using RimWorld;
using Verse;
using System.Collections.Generic;
namespace WulaFallenEmpire
{
public class CompProperties_AutoMechCarrier : CompProperties_MechCarrier
{
// XML中定义生产是否消耗资源
public bool freeProduction = false;
// 如果单位拥有这个Hediff则停止生产
public HediffDef disableHediff;
// 定义生产队列
public List<PawnProductionEntry> productionQueue = new List<PawnProductionEntry>();
public CompProperties_AutoMechCarrier()
{
// 确保这个属性类指向我们新的功能实现类
compClass = typeof(CompAutoMechCarrier);
}
public override IEnumerable<string> ConfigErrors(ThingDef parentDef)
{
foreach (string error in base.ConfigErrors(parentDef))
{
yield return error;
}
if (productionQueue.NullOrEmpty())
{
yield return "CompProperties_AutoMechCarrier must have at least one entry in productionQueue.";
}
}
public override void ResolveReferences(ThingDef parentDef)
{
base.ResolveReferences(parentDef);
// Prevent division by zero if costPerPawn is not set, which the base game AI might try to access.
if (costPerPawn <= 0)
{
costPerPawn = 1;
}
// 如果spawnPawnKind为空因为我们用了新的队列系统
// 就从队列里取第一个作为“假”值以防止基类方法在生成Gizmo标签时出错。
if (spawnPawnKind == null && !productionQueue.NullOrEmpty())
{
spawnPawnKind = productionQueue[0].pawnKind;
}
}
}
}

View File

@@ -0,0 +1,23 @@
using Verse;
namespace WulaFallenEmpire
{
/// <summary>
/// A data class to hold information about a pawn to be produced in a queue.
/// Used in XML definitions.
/// </summary>
public class PawnProductionEntry
{
// The PawnKindDef of the unit to spawn.
public PawnKindDef pawnKind;
// The maximum number of this kind of unit to maintain.
public int count = 1;
// Optional: specific cooldown for this entry. If not set, the parent comp's cooldown is used.
public int? cooldownTicks;
// Optional: specific cost for this entry. If not set, the parent comp's costPerPawn is used.
public int? cost;
}
}

View File

@@ -0,0 +1,26 @@
using RimWorld;
using Verse;
using Verse.AI;
namespace WulaFallenEmpire
{
public class MentalBreakWorker_BrokenPersonality : MentalBreakWorker
{
public override bool TryStart(Pawn pawn, string reason, bool causedByMood)
{
// 先尝试启动精神状态
if (base.TryStart(pawn, reason, causedByMood))
{
// 成功启动后,执行附加逻辑
var extension = def.mentalState.GetModExtension<MentalStateDefExtension_BrokenPersonality>();
if (extension != null && extension.traitToAdd != null && !pawn.story.traits.HasTrait(extension.traitToAdd))
{
pawn.story.traits.GainTrait(new Trait(extension.traitToAdd));
}
return true;
}
return false;
}
}
}

View File

@@ -0,0 +1,12 @@
using RimWorld;
using Verse;
namespace WulaFallenEmpire
{
public class MentalStateDefExtension_BrokenPersonality : DefModExtension
{
public TraitDef traitToAdd;
public FactionDef factionToJoin;
public float skillLevelFactor = 1f;
}
}

View File

@@ -0,0 +1,93 @@
using RimWorld;
using Verse;
using Verse.AI;
using Verse.AI.Group;
namespace WulaFallenEmpire
{
public class MentalState_BrokenPersonality : MentalState
{
public override void PostStart(string reason)
{
base.PostStart(reason);
// 发送信件
if (PawnUtility.ShouldSendNotificationAbout(pawn))
{
// 手动实现备用逻辑:如果信件标题(beginLetterLabel)为空,则使用精神状态的通用标签(label)
string labelText = def.beginLetterLabel;
if (string.IsNullOrEmpty(labelText))
{
labelText = def.label;
}
TaggedString letterLabel = labelText.Formatted(pawn.LabelShort, pawn.Named("PAWN")).CapitalizeFirst();
TaggedString letterText = def.beginLetter.Formatted(pawn.LabelShort, pawn.Named("PAWN")).CapitalizeFirst();
if (reason != null)
{
letterText += "\n\n" + reason;
}
Find.LetterStack.ReceiveLetter(letterLabel, letterText, LetterDefOf.ThreatBig, pawn);
}
var extension = def.GetModExtension<MentalStateDefExtension_BrokenPersonality>();
if (extension != null)
{
bool alreadyBroken = pawn.story.traits.HasTrait(extension.traitToAdd);
if (!alreadyBroken)
{
// 移除所有技能热情
foreach (SkillRecord skill in pawn.skills.skills)
{
skill.passion = Passion.None;
}
// 所有技能等级减半
foreach (SkillRecord skill in pawn.skills.skills)
{
int currentLevel = skill.Level;
skill.Level = (int)(currentLevel * extension.skillLevelFactor);
}
}
// 改变派系
Faction newFaction = Find.FactionManager.FirstFactionOfDef(extension.factionToJoin);
if (newFaction == null)
{
newFaction = Find.FactionManager.FirstFactionOfDef(FactionDefOf.AncientsHostile);
}
if (newFaction != null)
{
pawn.SetFaction(newFaction, null);
}
}
// 离开地图
Lord lord = pawn.GetLord();
if (lord == null)
{
LordJob_ExitMapBest lordJob = new LordJob_ExitMapBest(LocomotionUrgency.Jog, canDig: true, canDefendSelf: true);
lord = LordMaker.MakeNewLord(pawn.Faction, lordJob, pawn.Map, Gen.YieldSingle(pawn));
}
else
{
lord.ReceiveMemo("PawnBroken");
}
// 强制恢复以避免状态无限持续
this.forceRecoverAfterTicks = 150;
}
public override void MentalStateTick(int delta)
{
base.MentalStateTick(delta);
// 确保在下一帧就恢复,因为所有效果都已经应用
if (age > 0)
{
RecoverFromState();
}
}
}
}

View File

@@ -0,0 +1,101 @@
using RimWorld;
using System.Collections.Generic;
using Verse;
namespace WulaFallenEmpire
{
public class CompProperties_ChargingBed : CompProperties
{
public HediffDef hediffDef;
public string raceDefName;
public CompProperties_ChargingBed()
{
compClass = typeof(CompChargingBed);
}
}
public class CompChargingBed : ThingComp
{
public CompProperties_ChargingBed Props => (CompProperties_ChargingBed)props;
private List<Pawn> chargingPawns = new List<Pawn>();
public override void CompTick()
{
base.CompTick();
var bed = (Building_Bed)parent;
var powerComp = parent.GetComp<CompPowerTrader>();
// 如果床没电,停止所有充电
if (powerComp is { PowerOn: false })
{
StopAllCharging();
return;
}
var currentOccupants = new HashSet<Pawn>(bed.CurOccupants);
// 移除已经不在床上的 pawn 的充电效果
for (int i = chargingPawns.Count - 1; i >= 0; i--)
{
var pawn = chargingPawns[i];
if (!currentOccupants.Contains(pawn))
{
StopCharging(pawn);
chargingPawns.RemoveAt(i);
}
}
// 为床上的新 pawn 开始充电
foreach (var pawn in currentOccupants)
{
if (ShouldCharge(pawn) && !chargingPawns.Contains(pawn))
{
StartCharging(pawn);
}
}
}
private bool ShouldCharge(Pawn pawn)
{
return pawn.def.defName == Props.raceDefName;
}
private void StartCharging(Pawn pawn)
{
if (pawn.health.hediffSet.HasHediff(Props.hediffDef)) return;
pawn.health.AddHediff(Props.hediffDef);
if (!chargingPawns.Contains(pawn))
{
chargingPawns.Add(pawn);
}
}
private void StopCharging(Pawn pawn)
{
var hediff = pawn.health.hediffSet.GetFirstHediffOfDef(Props.hediffDef);
if (hediff != null)
{
pawn.health.RemoveHediff(hediff);
}
}
private void StopAllCharging()
{
for (int i = chargingPawns.Count - 1; i >= 0; i--)
{
StopCharging(chargingPawns[i]);
chargingPawns.RemoveAt(i);
}
}
public override void PostExposeData()
{
base.PostExposeData();
Scribe_Collections.Look(ref chargingPawns, "chargingPawns", LookMode.Reference);
}
}
}

View File

@@ -0,0 +1,65 @@
using Verse;
using RimWorld;
namespace WulaFallenEmpire
{
public class HediffCompProperties_WulaCharging : HediffCompProperties
{
public float energyPerTick = 0.001f; // 每tick补充的能量量
public int durationTicks = 600; // 持续时间例如600ticks = 10秒
public HediffCompProperties_WulaCharging()
{
this.compClass = typeof(HediffComp_WulaCharging);
}
}
public class HediffComp_WulaCharging : HediffComp
{
public HediffCompProperties_WulaCharging Props => (HediffCompProperties_WulaCharging)this.props;
private int ticksPassed = 0;
private Thing sourceThing; // 新增字段,用于存储能量核心物品
public void SetSourceThing(Thing thing)
{
this.sourceThing = thing;
}
public override void CompPostTick(ref float severityAdjustment)
{
base.CompPostTick(ref severityAdjustment);
ticksPassed++;
if (ticksPassed >= Props.durationTicks)
{
// 持续时间结束移除Hediff
this.parent.pawn.health.RemoveHediff(this.parent);
return;
}
Need_WulaEnergy energyNeed = this.parent.pawn.needs.TryGetNeed<Need_WulaEnergy>();
if (energyNeed != null)
{
// 从sourceThing的ThingDefExtension_EnergySource获取能量量
ThingDefExtension_EnergySource ext = sourceThing?.def.GetModExtension<ThingDefExtension_EnergySource>();
if (ext != null)
{
energyNeed.CurLevel += ext.energyAmount / Props.durationTicks; // 将总能量量分摊到每个tick
}
else
{
// 如果没有找到能量来源扩展则使用默认的energyPerTick
energyNeed.CurLevel += Props.energyPerTick;
}
}
}
public override void CompExposeData()
{
base.CompExposeData();
Scribe_Values.Look(ref ticksPassed, "ticksPassed", 0);
Scribe_References.Look(ref sourceThing, "sourceThing"); // 保存sourceThing
}
}
}

View File

@@ -0,0 +1,87 @@
using System.Collections.Generic;
using Verse;
using Verse.AI;
using RimWorld;
namespace WulaFallenEmpire
{
public class JobDriver_FeedWulaPatient : JobDriver
{
private const TargetIndex FoodSourceInd = TargetIndex.A;
private const TargetIndex PatientInd = TargetIndex.B;
protected Thing Food => job.GetTarget(FoodSourceInd).Thing;
protected Pawn Patient => (Pawn)job.GetTarget(PatientInd).Thing;
public override bool TryMakePreToilReservations(bool errorOnFailed)
{
if (!pawn.Reserve(Patient, job, 1, -1, null, errorOnFailed))
{
return false;
}
if (!pawn.Reserve(Food, job, 1, -1, null, errorOnFailed))
{
return false;
}
return true;
}
protected override IEnumerable<Toil> MakeNewToils()
{
this.FailOnDespawnedNullOrForbidden(PatientInd);
// The job should fail if the patient is no longer in bed.
this.FailOn(() => !Patient.InBed());
if (pawn.inventory != null && pawn.inventory.Contains(Food))
{
yield return Toils_Misc.TakeItemFromInventoryToCarrier(pawn, FoodSourceInd);
}
else
{
yield return Toils_Goto.GotoThing(FoodSourceInd, PathEndMode.ClosestTouch).FailOnForbidden(FoodSourceInd);
yield return Toils_Ingest.PickupIngestible(FoodSourceInd, pawn);
}
yield return Toils_Goto.GotoThing(PatientInd, PathEndMode.Touch);
yield return Toils_Ingest.ChewIngestible(Patient, 1.5f, FoodSourceInd, TargetIndex.None).FailOnCannotTouch(PatientInd, PathEndMode.Touch);
// Custom Finalize Ingest Logic
Toil finalizeToil = new Toil();
finalizeToil.initAction = delegate
{
Pawn patient = Patient;
Thing food = Food;
if (patient == null || food == null)
{
return;
}
// If it's not an energy core, use the default vanilla method.
if (food.def.defName != "WULA_Charge_Cube")
{
patient.needs.food.CurLevel += FoodUtility.GetNutrition(patient, food, food.def);
}
else
{
// Our custom logic for energy core
// 1. Apply the charging hediff
Hediff hediff = HediffMaker.MakeHediff(HediffDef.Named("WULA_ChargingHediff"), patient);
hediff.Severity = 1.0f;
patient.health.AddHediff(hediff);
// 2. Spawn the used core
Thing usedCore = ThingMaker.MakeThing(ThingDef.Named("WULA_Charge_Cube_No_Power"));
GenPlace.TryPlaceThing(usedCore, pawn.Position, pawn.Map, ThingPlaceMode.Near);
}
// Destroy the food item (it has been carried by the feeder)
if (!food.Destroyed)
{
food.Destroy();
}
};
finalizeToil.defaultCompleteMode = ToilCompleteMode.Instant;
yield return finalizeToil;
}
}
}

View File

@@ -0,0 +1,123 @@
using System.Collections.Generic;
using Verse;
using Verse.AI;
using RimWorld;
namespace WulaFallenEmpire
{
public class JobDriver_IngestWulaEnergy : JobDriver
{
private bool eatingFromInventory;
private const TargetIndex IngestibleSourceInd = TargetIndex.A;
private Thing IngestibleSource => job.GetTarget(IngestibleSourceInd).Thing;
private float ChewDurationMultiplier
{
get
{
Thing ingestibleSource = IngestibleSource;
if (ingestibleSource.def.ingestible != null)
{
return 1f / pawn.GetStatValue(StatDefOf.EatingSpeed);
}
return 1f;
}
}
public override void ExposeData()
{
base.ExposeData();
Scribe_Values.Look(ref eatingFromInventory, "eatingFromInventory", defaultValue: false);
}
public override void Notify_Starting()
{
base.Notify_Starting();
eatingFromInventory = pawn.inventory != null && pawn.inventory.Contains(IngestibleSource);
}
public override bool TryMakePreToilReservations(bool errorOnFailed)
{
if (pawn.Faction != null)
{
Thing ingestibleSource = IngestibleSource;
int maxAmountToPickup = FoodUtility.GetMaxAmountToPickup(ingestibleSource, pawn, job.count);
if (!pawn.Reserve(ingestibleSource, job, 10, maxAmountToPickup, null, errorOnFailed))
{
return false;
}
job.count = maxAmountToPickup;
}
return true;
}
protected override IEnumerable<Toil> MakeNewToils()
{
this.FailOn(() => !IngestibleSource.Destroyed && !IngestibleSource.IngestibleNow);
Toil chew = Toils_Ingest.ChewIngestible(pawn, ChewDurationMultiplier, IngestibleSourceInd, TargetIndex.None)
.FailOn((Toil x) => !IngestibleSource.Spawned && (pawn.carryTracker == null || pawn.carryTracker.CarriedThing != IngestibleSource))
.FailOnCannotTouch(IngestibleSourceInd, PathEndMode.Touch);
foreach (Toil item in PrepareToIngestToils(chew))
{
yield return item;
}
yield return chew;
// Custom Finalize Ingest Logic
Toil finalizeToil = new Toil();
finalizeToil.initAction = delegate
{
Pawn ingester = pawn;
Thing ingestible = IngestibleSource;
if (ingester == null || ingestible == null)
{
return;
}
// If it's not an energy core, use the default vanilla method for safety, though this job should only target energy cores.
if (ingestible.def.defName != "WULA_Charge_Cube")
{
ingester.needs.food.CurLevel += FoodUtility.GetNutrition(ingester, ingestible, ingestible.def);
}
else
{
// Our custom logic for energy core
// 1. Apply the charging hediff
Hediff hediff = HediffMaker.MakeHediff(HediffDef.Named("WULA_ChargingHediff"), ingester);
hediff.Severity = 1.0f;
ingester.health.AddHediff(hediff);
// 2. Spawn the used core
Thing usedCore = ThingMaker.MakeThing(ThingDef.Named("WULA_Charge_Cube_No_Power"));
GenPlace.TryPlaceThing(usedCore, ingester.Position, ingester.Map, ThingPlaceMode.Near);
}
// Destroy the original food item
if (!ingestible.Destroyed)
{
ingestible.Destroy();
}
};
finalizeToil.defaultCompleteMode = ToilCompleteMode.Instant;
yield return finalizeToil;
}
private IEnumerable<Toil> PrepareToIngestToils(Toil chewToil)
{
if (eatingFromInventory)
{
yield return Toils_Misc.TakeItemFromInventoryToCarrier(pawn, IngestibleSourceInd);
}
else
{
yield return Toils_Goto.GotoThing(IngestibleSourceInd, PathEndMode.ClosestTouch).FailOnDespawnedNullOrForbidden(IngestibleSourceInd);
yield return Toils_Ingest.PickupIngestible(IngestibleSourceInd, pawn);
}
}
}
}

View File

@@ -0,0 +1,10 @@
using Verse;
namespace WulaFallenEmpire
{
public class JobGiverDefExtension_WulaPackEnergy : DefModExtension
{
public float packEnergyThreshold = 0.5f; // 默认打包能量阈值
public int packEnergyCount = 10; // 默认打包数量
}
}

View File

@@ -0,0 +1,146 @@
using RimWorld;
using System.Linq;
using Verse;
using Verse.AI;
namespace WulaFallenEmpire
{
public class JobGiver_WulaGetEnergy : ThinkNode_JobGiver
{
public float minEnergyLevelPercentage = 0.3f;
public float maxEnergyLevelPercentage = 1.0f;
public float emergencyPriority = 9.5f;
public override float GetPriority(Pawn pawn)
{
var energyNeed = pawn.needs.TryGetNeed<Need_WulaEnergy>();
if (energyNeed == null)
{
return 0f;
}
// 如果能量已充满,则不需要充电
if (energyNeed.CurLevel >= energyNeed.MaxLevel)
{
return 0f;
}
// 如果Pawn正在执行充电Job并且能量尚未充满则保持高优先级
if ((pawn.CurJobDef == JobDefOf.LayDown ||
pawn.CurJobDef == DefDatabase<JobDef>.GetNamed("WULA_IngestWulaEnergy")) &&
energyNeed.CurLevel < energyNeed.MaxLevel)
{
return emergencyPriority; // 保持高优先级,直到充满
}
// 如果能量低于阈值,则需要充电
if (energyNeed.CurLevelPercentage < minEnergyLevelPercentage)
{
return emergencyPriority;
}
return 0f; // 否则不需要充电返回0
}
protected override Job TryGiveJob(Pawn pawn)
{
var energyNeed = pawn.needs.TryGetNeed<Need_WulaEnergy>();
if (energyNeed == null)
{
return null;
}
if (energyNeed.CurLevelPercentage >= maxEnergyLevelPercentage)
{
return null;
}
if (!TryFindBestEnergySourceFor(pawn, out var energySource))
{
return null;
}
if (energySource is Building_Bed)
{
return JobMaker.MakeJob(JobDefOf.LayDown, energySource);
}
var job = JobMaker.MakeJob(DefDatabase<JobDef>.GetNamed("WULA_IngestWulaEnergy"), energySource);
job.count = 1;
return job;
}
private bool TryFindBestEnergySourceFor(Pawn pawn, out Thing energySource)
{
// 优先寻找可用的充电床
energySource = FindChargingBed(pawn);
if (energySource != null)
{
return true;
}
// No bed found, now consider consumables.
// Check for the hediff BEFORE searching for consumables.
if (pawn.health.hediffSet.HasHediff(HediffDef.Named("WULA_ChargingHediff")))
{
energySource = null;
return false;
}
// 优先从背包中寻找
Thing thing = pawn.inventory.innerContainer.FirstOrFallback(t => t.def.GetModExtension<ThingDefExtension_EnergySource>() != null && t.IngestibleNow);
if (thing != null)
{
energySource = thing;
return true;
}
// 否则,在地图上寻找
energySource = GenClosest.ClosestThingReachable(
pawn.Position,
pawn.Map,
ThingRequest.ForGroup(ThingRequestGroup.HaulableEver),
PathEndMode.ClosestTouch,
TraverseParms.For(pawn),
9999f,
t => t.def.GetModExtension<ThingDefExtension_EnergySource>() != null && t.IngestibleNow && !t.IsForbidden(pawn) && pawn.CanReserve(t)
);
return energySource != null;
}
private Building_Bed FindChargingBed(Pawn pawn)
{
// 寻找附近可用的 WULA_Charging_Station_Synth
Building_Bed bed = (Building_Bed)GenClosest.ClosestThingReachable(
pawn.Position,
pawn.Map,
ThingRequest.ForGroup(ThingRequestGroup.BuildingArtificial),
PathEndMode.InteractionCell,
TraverseParms.For(pawn),
9999f,
b =>
{
if (!(b is Building_Bed bed_internal)) return false;
if (bed_internal.GetComp<CompChargingBed>() == null) return false;
var powerComp = bed_internal.GetComp<CompPowerTrader>();
// A pawn can use a bed if:
// 1. It has power.
// 2. Its prisoner status matches the pawn's.
// 3. It's not a medical bed.
// 4. The pawn can reserve it (checks for ownership, forbidden, etc.)
return powerComp != null &&
powerComp.PowerOn &&
bed_internal.ForPrisoners == pawn.IsPrisoner &&
!bed_internal.Medical &&
pawn.CanReserve(bed_internal);
}
);
return bed;
}
}
}

View File

@@ -0,0 +1,86 @@
using System.Collections.Generic;
using UnityEngine;
using Verse;
using Verse.AI;
using RimWorld; // For JobDefOf, ThingDefOf, StatDefOf
namespace WulaFallenEmpire
{
public class JobGiver_WulaPackEnergy : ThinkNode_JobGiver
{
public float packEnergyThreshold = 0.5f; // 默认打包能量阈值
public int packEnergyCount = 2; // 默认打包数量
// 定义乌拉能量核心的ThingDef
private static ThingDef WULA_Charge_Cube_Def => ThingDef.Named("WULA_Charge_Cube");
public override ThinkNode DeepCopy(bool resolve = true)
{
JobGiver_WulaPackEnergy obj = (JobGiver_WulaPackEnergy)base.DeepCopy(resolve);
obj.packEnergyThreshold = packEnergyThreshold;
obj.packEnergyCount = packEnergyCount;
return obj;
}
protected override Job TryGiveJob(Pawn pawn)
{
if (pawn.inventory == null)
{
return null;
}
// 检查背包中是否有足够的能量核心这里可以根据Need_WulaEnergy的当前值来判断是否需要打包
// 简化逻辑:如果能量低于某个阈值,并且背包中没有能量核心,则尝试打包
Need_WulaEnergy energyNeed = pawn.needs.TryGetNeed<Need_WulaEnergy>();
if (energyNeed == null)
{
return null;
}
// 只有当能量低于阈值并且背包中能量核心数量少于2个时才尝试打包
if (energyNeed.CurLevelPercentage > packEnergyThreshold || pawn.inventory.innerContainer.TotalStackCountOfDef(WULA_Charge_Cube_Def) >= 2)
{
return null;
}
// 检查是否超重
if (MassUtility.IsOverEncumbered(pawn))
{
return null;
}
// 寻找地图上可触及的WULA_Charge_Cube
Thing thing = GenClosest.ClosestThing_Regionwise_ReachablePrioritized(
pawn.Position,
pawn.Map,
ThingRequest.ForDef(WULA_Charge_Cube_Def), // 只寻找WULA_Charge_Cube
PathEndMode.ClosestTouch,
TraverseParms.For(pawn),
20f, // 搜索距离
delegate(Thing t)
{
// 检查物品是否被禁止,是否可预留,是否社交得体
return !t.IsForbidden(pawn) && pawn.CanReserve(t) && t.IsSociallyProper(pawn);
},
(Thing x) => 0f // 优先级,这里可以根据距离或其他因素调整
);
if (thing == null)
{
return null;
}
// 计算需要打包的数量限制在1到2个
int countToTake = Mathf.Min(thing.stackCount, 2); // 限制为最多2个
if (WULA_Charge_Cube_Def != null)
{
countToTake = Mathf.Min(countToTake, WULA_Charge_Cube_Def.stackLimit);
}
// 创建TakeInventory Job
Job job = JobMaker.MakeJob(JobDefOf.TakeInventory, thing);
job.count = countToTake;
return job;
}
}
}

View File

@@ -0,0 +1,16 @@
using Verse;
namespace WulaFallenEmpire
{
public class NeedDefExtension_Energy : DefModExtension
{
// 能量每天的消耗值
public float fallPerDay = 1.6f;
// 能量上限
public float maxLevel = 1.0f;
// 运送能量的阈值
public float deliverEnergyThreshold = 0.5f;
// 自动摄取能量的阈值
public float autoIngestThreshold = 0.5f;
}
}

View File

@@ -0,0 +1,117 @@
using RimWorld;
using UnityEngine;
using Verse;
using WulaFallenEmpire;
namespace WulaFallenEmpire
{
public class Need_WulaEnergy : Need
{
private NeedDefExtension_Energy ext;
private NeedDefExtension_Energy Ext
{
get
{
if (ext == null)
{
if (def == null)
{
return null;
}
ext = def.GetModExtension<NeedDefExtension_Energy>();
}
return ext;
}
}
private float EnergyFallPerTick
{
get
{
if (Ext != null)
{
// 从XML读取每天消耗值并转换为每tick消耗值并应用StatDef因子
return (Ext.fallPerDay / 60000f) * pawn.GetStatValue(WulaStatDefOf.WulaEnergyFallRateFactor);
}
// 如果XML中没有定义则使用一个默认值
return 2.6666667E-05f;
}
}
public bool IsShutdown => CurLevel <= 0.01f;
public override int GUIChangeArrow
{
get
{
if (IsFrozen) return 0;
return -1;
}
}
public override float MaxLevel
{
get
{
if (Ext != null)
{
// 应用StatDef偏移量
return Ext.maxLevel + pawn.GetStatValue(WulaStatDefOf.WulaEnergyMaxLevelOffset);
}
return 1f;
}
}
public Need_WulaEnergy(Pawn pawn) : base(pawn)
{
}
public override void NeedInterval()
{
if (!IsFrozen)
{
CurLevel -= EnergyFallPerTick * 150f;
}
if (IsShutdown)
{
HealthUtility.AdjustSeverity(pawn, HediffDef.Named("WULA_Shutdown"), 0.05f);
}
else
{
Hediff hediff = pawn.health.hediffSet.GetFirstHediffOfDef(HediffDef.Named("WULA_Shutdown"));
if (hediff != null)
{
pawn.health.RemoveHediff(hediff);
}
}
}
public override void SetInitialLevel()
{
CurLevelPercentage = 1.0f;
}
public override string GetTipString()
{
return (LabelCap + ": " + CurLevelPercentage.ToStringPercent()).Colorize(ColoredText.TipSectionTitleColor) + "\n" +
def.description;
}
public override void DrawOnGUI(Rect rect, int maxThresholdMarkers = int.MaxValue, float customMargin = -1f, bool drawArrows = true, bool doTooltip = true, Rect? rectForTooltip = null, bool drawLabel = true)
{
if (threshPercents == null)
{
threshPercents = new System.Collections.Generic.List<float>();
}
threshPercents.Clear();
base.DrawOnGUI(rect, maxThresholdMarkers, customMargin, drawArrows, doTooltip, rectForTooltip, drawLabel);
}
public override void ExposeData()
{
base.ExposeData();
}
}
}

View File

@@ -0,0 +1,9 @@
using Verse;
namespace WulaFallenEmpire
{
public class ThingDefExtension_EnergySource : DefModExtension
{
public float energyAmount = 1.0f; // Amount of energy this item provides
}
}

View File

@@ -0,0 +1,10 @@
using Verse;
namespace WulaFallenEmpire
{
public class WorkGiverDefExtension_FeedWula : DefModExtension
{
public float feedThreshold = 0.25f;
public ThingDef energySourceDef;
}
}

View File

@@ -0,0 +1,95 @@
using RimWorld;
using Verse;
using Verse.AI;
using System.Collections.Generic;
using System.Linq;
namespace WulaFallenEmpire
{
public class WorkGiver_FeedWulaPatient : WorkGiver_Scanner
{
public override ThingRequest PotentialWorkThingRequest => ThingRequest.ForGroup(ThingRequestGroup.Pawn);
public override PathEndMode PathEndMode => PathEndMode.ClosestTouch;
public override Danger MaxPathDanger(Pawn pawn) => Danger.Deadly;
public override IEnumerable<Thing> PotentialWorkThingsGlobal(Pawn pawn)
{
return pawn.Map.mapPawns.AllPawns.Where(p => p.needs.TryGetNeed<Need_WulaEnergy>() != null && p.InBed());
}
public override bool HasJobOnThing(Pawn pawn, Thing t, bool forced = false)
{
if (!(t is Pawn patient) || patient == pawn)
{
return false;
}
// 如果病患正在充能,则不需要喂食
if (patient.health.hediffSet.HasHediff(DefDatabase<HediffDef>.GetNamed("WULA_ChargingHediff")))
{
return false;
}
Need_WulaEnergy energyNeed = patient.needs.TryGetNeed<Need_WulaEnergy>();
var extension = def.GetModExtension<WorkGiverDefExtension_FeedWula>();
if (energyNeed == null || energyNeed.CurLevelPercentage >= extension.feedThreshold)
{
return false;
}
// A Wula patient should be fed if they are in bed. If the job is not forced, they must also be unable to move.
if (!patient.InBed() || (!forced && patient.health.capacities.CapableOf(PawnCapacityDefOf.Moving)))
{
return false;
}
if (!pawn.CanReserveAndReach(patient, PathEndMode.Touch, Danger.Deadly, 1, -1, null, forced))
{
return false;
}
if (!TryFindBestEnergySourceFor(pawn, patient, out _, out _))
{
JobFailReason.Is("NoWulaEnergyToFeed".Translate(patient.LabelShort, patient));
return false;
}
return true;
}
public override Job JobOnThing(Pawn pawn, Thing t, bool forced = false)
{
Pawn patient = (Pawn)t;
if (TryFindBestEnergySourceFor(pawn, patient, out Thing energySource, out _))
{
Job job = JobMaker.MakeJob(DefDatabase<JobDef>.GetNamed("WULA_FeedWulaPatient"), energySource, patient);
job.count = 1;
return job;
}
return null;
}
private bool TryFindBestEnergySourceFor(Pawn getter, Pawn eater, out Thing energySource, out ThingDef energyDef)
{
energySource = null;
energyDef = null;
var allowedThings = getter.Map.listerThings.ThingsInGroup(ThingRequestGroup.HaulableEver)
.Where(x => x.def.GetModExtension<ThingDefExtension_EnergySource>() != null);
Thing thing = GenClosest.ClosestThing_Global(eater.Position, allowedThings, 99999f,
t => t.IngestibleNow && !t.IsForbidden(getter) && getter.CanReserve(t));
if (thing != null)
{
energySource = thing;
energyDef = thing.def;
return true;
}
return false;
}
}
}

View File

@@ -0,0 +1,134 @@
using RimWorld;
using Verse;
using Verse.AI;
namespace WulaFallenEmpire
{
public class WorkGiver_Warden_DeliverEnergy : WorkGiver_Scanner
{
private WorkGiverDefExtension_FeedWula ext;
private WorkGiverDefExtension_FeedWula Ext
{
get
{
if (ext == null)
{
ext = def.GetModExtension<WorkGiverDefExtension_FeedWula>();
}
return ext;
}
}
public override ThingRequest PotentialWorkThingRequest => ThingRequest.ForDef(ThingDef.Named("WulaSpecies"));
public override PathEndMode PathEndMode => PathEndMode.ClosestTouch;
public override Danger MaxPathDanger(Pawn pawn) => Danger.Deadly;
public override bool HasJobOnThing(Pawn pawn, Thing t, bool forced = false)
{
Pawn prisoner = t as Pawn;
if (prisoner == null || prisoner == pawn || !prisoner.IsPrisonerOfColony || !prisoner.guest.CanBeBroughtFood)
{
return false;
}
Need_WulaEnergy energyNeed = prisoner.needs.TryGetNeed<Need_WulaEnergy>();
if (energyNeed == null)
{
return false;
}
if (energyNeed.CurLevelPercentage > Ext.feedThreshold)
{
return false;
}
if (WardenFeedUtility.ShouldBeFed(prisoner))
{
return false;
}
if (!pawn.CanReserveAndReach(prisoner, PathEndMode.Touch, Danger.Deadly, 1, -1, null, forced))
{
return false;
}
if (Ext == null || Ext.energySourceDef == null)
{
Log.ErrorOnce("WorkGiver_Warden_DeliverEnergy is missing the DefModExtension with a valid energySourceDef.", def.GetHashCode());
return false;
}
if (!FindBestEnergySourceFor(pawn, prisoner, out _, out _))
{
JobFailReason.Is("NoFood".Translate());
return false;
}
return true;
}
public override Job JobOnThing(Pawn pawn, Thing t, bool forced = false)
{
Pawn prisoner = (Pawn)t;
if (FindBestEnergySourceFor(pawn, prisoner, out Thing energySource, out _))
{
Job job = JobMaker.MakeJob(JobDefOf.DeliverFood, energySource, prisoner);
job.count = 1;
job.targetC = RCellFinder.SpotToChewStandingNear(prisoner, energySource);
return job;
}
return null;
}
private bool FindBestEnergySourceFor(Pawn getter, Pawn eater, out Thing foodSource, out ThingDef foodDef)
{
foodSource = null;
foodDef = null;
if (Ext == null || Ext.energySourceDef == null)
{
return false;
}
// Check if there's already an energy source in the eater's room that the eater can reach and use.
Thing existingEnergyInRoom = GenClosest.ClosestThingReachable(
eater.Position, // Start search from eater's position
eater.Map,
ThingRequest.ForDef(Ext.energySourceDef),
PathEndMode.OnCell,
TraverseParms.For(eater, Danger.Deadly, TraverseMode.ByPawn, false), // Use eater's traverse parms
9999f,
(Thing x) => !x.IsForbidden(eater) && eater.CanReserve(x) && x.GetRoom() == eater.GetRoom()
);
if (existingEnergyInRoom != null)
{
// If there's already an energy source in the room, no need for the warden to bring another.
return false;
}
// Search for an energy source anywhere, now that we've confirmed none are in the room.
foodSource = GenClosest.ClosestThingReachable(
getter.Position,
getter.Map,
ThingRequest.ForDef(Ext.energySourceDef),
PathEndMode.OnCell,
TraverseParms.For(getter),
9999f,
(Thing x) => !x.IsForbidden(getter) && getter.CanReserve(x)
);
if (foodSource != null)
{
foodDef = foodSource.def;
return true;
}
return false;
}
}
}

View File

@@ -0,0 +1,85 @@
using RimWorld;
using Verse;
using Verse.AI;
using System.Linq;
namespace WulaFallenEmpire
{
public class WorkGiver_Warden_FeedWula : WorkGiver_Scanner
{
public override ThingRequest PotentialWorkThingRequest => ThingRequest.ForGroup(ThingRequestGroup.Pawn);
public override PathEndMode PathEndMode => PathEndMode.ClosestTouch;
public override Danger MaxPathDanger(Pawn pawn) => Danger.Deadly;
public override bool HasJobOnThing(Pawn pawn, Thing t, bool forced = false)
{
if (!(t is Pawn prisoner) || pawn == prisoner)
return false;
if (!ShouldFeed(pawn, prisoner))
return false;
Need_WulaEnergy energyNeed = prisoner.needs.TryGetNeed<Need_WulaEnergy>();
var extension = def.GetModExtension<WorkGiverDefExtension_FeedWula>();
if (energyNeed == null || energyNeed.CurLevelPercentage >= extension.feedThreshold)
return false;
if (prisoner.health.hediffSet.HasHediff(DefDatabase<HediffDef>.GetNamed("WULA_ChargingHediff")))
return false;
if (!prisoner.InBed() || (!forced && prisoner.health.capacities.CapableOf(PawnCapacityDefOf.Moving)))
return false;
if (!pawn.CanReserveAndReach(prisoner, PathEndMode.Touch, Danger.Deadly, 1, -1, null, forced))
return false;
if (!TryFindBestEnergySourceFor(pawn, prisoner, out _, out _))
{
JobFailReason.Is("NoWulaEnergyToFeed".Translate(prisoner.LabelShort, prisoner));
return false;
}
return true;
}
public override Job JobOnThing(Pawn pawn, Thing t, bool forced = false)
{
Pawn prisoner = (Pawn)t;
if (TryFindBestEnergySourceFor(pawn, prisoner, out Thing energySource, out _))
{
Job job = JobMaker.MakeJob(DefDatabase<JobDef>.GetNamed("WULA_FeedWulaPatient"), energySource, prisoner);
job.count = 1;
return job;
}
return null;
}
private bool ShouldFeed(Pawn warden, Pawn prisoner)
{
return prisoner.IsPrisonerOfColony && prisoner.guest.CanBeBroughtFood && prisoner.needs.TryGetNeed<Need_WulaEnergy>() != null;
}
private bool TryFindBestEnergySourceFor(Pawn getter, Pawn eater, out Thing energySource, out ThingDef energyDef)
{
energySource = null;
energyDef = null;
var allowedThings = getter.Map.listerThings.ThingsInGroup(ThingRequestGroup.HaulableEver)
.Where(x => x.def.GetModExtension<ThingDefExtension_EnergySource>() != null);
Thing thing = GenClosest.ClosestThing_Global(eater.Position, allowedThings, 99999f,
t => t.IngestibleNow && !t.IsForbidden(getter) && getter.CanReserve(t));
if (thing != null)
{
energySource = thing;
energyDef = thing.def;
return true;
}
return false;
}
}
}

View File

@@ -0,0 +1,11 @@
using Verse;
namespace WulaFallenEmpire
{
public class WulaCaravanEnergyDef : Def
{
public float consumeThreshold = 0.9f;
public string energyItemDefName;
public string hediffDefNameToAdd;
}
}

View File

@@ -0,0 +1,17 @@
using RimWorld;
using Verse;
namespace WulaFallenEmpire
{
[DefOf]
public static class WulaStatDefOf
{
public static StatDef WulaEnergyMaxLevelOffset;
public static StatDef WulaEnergyFallRateFactor;
static WulaStatDefOf()
{
DefOfHelper.EnsureInitializedInCtor(typeof(WulaStatDefOf));
}
}
}

View File

@@ -0,0 +1,35 @@
using RimWorld;
using Verse;
namespace WulaFallenEmpire
{
public class CompProperties_MaintenanceCycle : CompProperties_BiosculpterPod_BaseCycle
{
public HediffDef hediffToRemove;
public CompProperties_MaintenanceCycle()
{
compClass = typeof(CompMaintenanceCycle);
}
}
public class CompMaintenanceCycle : CompBiosculpterPod_Cycle
{
public new CompProperties_MaintenanceCycle Props => (CompProperties_MaintenanceCycle)props;
public override void CycleCompleted(Pawn pawn)
{
if (pawn == null)
{
return;
}
Hediff hediff = pawn.health.hediffSet.GetFirstHediffOfDef(Props.hediffToRemove);
if (hediff != null)
{
hediff.Severity = 0f;
Messages.Message("WULA_MaintenanceCycleComplete".Translate(pawn.Named("PAWN")), pawn, MessageTypeDefOf.PositiveEvent);
}
}
}
}

View File

@@ -0,0 +1,446 @@
using RimWorld;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;
using Verse;
using Verse.AI;
using Verse.Sound;
namespace WulaFallenEmpire
{
public class CompProperties_MaintenancePod : CompProperties
{
public SoundDef enterSound;
public SoundDef exitSound;
public EffecterDef operatingEffecter;
public int baseDurationTicks = 60000;
public float ticksPerSeverity = 0f;
public float powerConsumptionRunning = 250f;
public float powerConsumptionIdle = 50f;
public HediffDef hediffToRemove;
public float componentCostPerSeverity = 1f;
public int baseComponentCost = 0;
public float minSeverityToMaintain = 0.75f;
public float hediffSeverityAfterCycle = 0.01f;
public CompProperties_MaintenancePod()
{
compClass = typeof(CompMaintenancePod);
}
}
[StaticConstructorOnStartup]
public class CompMaintenancePod : ThingComp, IThingHolder
{
// ===================== Fields =====================
private ThingOwner innerContainer;
private CompPowerTrader powerComp;
private CompRefuelable refuelableComp;
private int ticksRemaining;
private MaintenancePodState state = MaintenancePodState.Idle;
private static readonly Texture2D CancelIcon = ContentFinder<Texture2D>.Get("UI/Designators/Cancel");
private static readonly Texture2D EnterIcon = ContentFinder<Texture2D>.Get("UI/Commands/PodEject");
// ===================== Properties =====================
public CompProperties_MaintenancePod Props => (CompProperties_MaintenancePod)props;
public MaintenancePodState State => state;
public Pawn Occupant => innerContainer.FirstOrDefault() as Pawn;
public bool PowerOn => powerComp != null && powerComp.PowerOn;
public float RequiredComponents(Pawn pawn)
{
if (pawn == null || Props.hediffToRemove == null) return Props.baseComponentCost;
Hediff hediff = pawn.health.hediffSet.GetFirstHediffOfDef(Props.hediffToRemove);
if (hediff == null) return Props.baseComponentCost;
return Props.baseComponentCost + (int)(hediff.Severity * Props.componentCostPerSeverity);
}
public int RequiredDuration(Pawn pawn)
{
if (pawn == null || Props.hediffToRemove == null) return Props.baseDurationTicks;
Hediff hediff = pawn.health.hediffSet.GetFirstHediffOfDef(Props.hediffToRemove);
if (hediff == null) return Props.baseDurationTicks;
return Props.baseDurationTicks + (int)(hediff.Severity * Props.ticksPerSeverity);
}
// ===================== Setup =====================
public CompMaintenancePod()
{
innerContainer = new ThingOwner<Thing>(this, false, LookMode.Deep);
}
public override void PostSpawnSetup(bool respawningAfterLoad)
{
base.PostSpawnSetup(respawningAfterLoad);
powerComp = parent.TryGetComp<CompPowerTrader>();
refuelableComp = parent.TryGetComp<CompRefuelable>();
}
public override void PostExposeData()
{
base.PostExposeData();
Scribe_Values.Look(ref state, "state", MaintenancePodState.Idle);
Scribe_Values.Look(ref ticksRemaining, "ticksRemaining", 0);
Scribe_Deep.Look(ref innerContainer, "innerContainer", this);
}
public override void PostDestroy(DestroyMode mode, Map previousMap)
{
base.PostDestroy(mode, previousMap);
// If the pod is deconstructed or destroyed, eject the occupant to prevent deletion.
if (mode == DestroyMode.Deconstruct || mode == DestroyMode.KillFinalize)
{
Log.Warning($"[WulaPodDebug] Pod destroyed (mode: {mode}). Ejecting pawn.");
EjectPawn();
}
}
// ===================== IThingHolder Implementation =====================
public void GetChildHolders(List<IThingHolder> outChildren)
{
ThingOwnerUtility.AppendThingHoldersFromThings(outChildren, GetDirectlyHeldThings());
}
public ThingOwner GetDirectlyHeldThings()
{
return innerContainer;
}
// ===================== Core Logic =====================
public override void CompTick()
{
base.CompTick();
if (!parent.Spawned) return;
if (state == MaintenancePodState.Running)
{
if (PowerOn)
{
ticksRemaining--;
if (ticksRemaining <= 0)
{
CycleFinished();
}
}
}
if (powerComp != null)
{
powerComp.PowerOutput = -(state == MaintenancePodState.Running ? Props.powerConsumptionRunning : Props.powerConsumptionIdle);
}
}
public void StartCycle(Pawn pawn)
{
Log.Warning($"[WulaPodDebug] StartCycle called for pawn: {pawn.LabelShortCap}");
float required = RequiredComponents(pawn);
if (refuelableComp.Fuel < required)
{
Log.Error($"[WulaPodDebug] ERROR: Tried to start cycle for {pawn.LabelShort} without enough components.");
return;
}
if (required > 0)
{
refuelableComp.ConsumeFuel(required);
}
Log.Warning($"[WulaPodDebug] Pawn state before action: holdingOwner is {(pawn.holdingOwner == null ? "NULL" : "NOT NULL")}, Spawned is {pawn.Spawned}");
// THE ACTUAL FIX: A pawn, whether held or not, must be despawned before being put in a container.
if (pawn.Spawned)
{
Log.Warning($"[WulaPodDebug] Pawn is spawned. Despawning...");
pawn.DeSpawn(DestroyMode.Vanish);
}
Log.Warning($"[WulaPodDebug] Attempting to add/transfer pawn to container.");
innerContainer.TryAddOrTransfer(pawn);
state = MaintenancePodState.Running;
ticksRemaining = RequiredDuration(pawn);
Log.Warning($"[WulaPodDebug] Cycle started. Ticks remaining: {ticksRemaining}");
}
private void CycleFinished()
{
Pawn occupant = Occupant;
Log.Warning($"[WulaPodDebug] CycleFinished. Occupant: {(occupant == null ? "NULL" : occupant.LabelShortCap)}");
if (occupant == null)
{
Log.Error("[WulaPodDebug] ERROR: Maintenance cycle finished, but no one was inside.");
state = MaintenancePodState.Idle;
return;
}
// 1. Fix the maintenance hediff
bool maintenanceDone = false;
if (Props.hediffToRemove != null)
{
Hediff hediff = occupant.health.hediffSet.GetFirstHediffOfDef(Props.hediffToRemove);
if (hediff != null)
{
hediff.Severity = Props.hediffSeverityAfterCycle;
Messages.Message("WULA_MaintenanceComplete".Translate(occupant.Named("PAWN")), occupant, MessageTypeDefOf.PositiveEvent);
maintenanceDone = true;
}
}
// 2. Heal all other injuries and missing parts
int injuriesHealed = 0;
// Loop until no more health conditions can be fixed
while (HealthUtility.TryGetWorstHealthCondition(occupant, out var hediffToFix, out var _))
{
// Ensure we don't try to "heal" the maintenance hediff itself, as it's handled separately.
if (hediffToFix != null && hediffToFix.def == Props.hediffToRemove)
{
break;
}
// Store the state before attempting to fix
int initialHediffCount = occupant.health.hediffSet.hediffs.Count;
var hediffsBefore = new HashSet<Hediff>(occupant.health.hediffSet.hediffs);
// Attempt to fix the worst condition
HealthUtility.FixWorstHealthCondition(occupant);
// Check if a change actually occurred
bool conditionFixed = initialHediffCount > occupant.health.hediffSet.hediffs.Count ||
!hediffsBefore.SetEquals(occupant.health.hediffSet.hediffs);
if (conditionFixed)
{
injuriesHealed++;
}
else
{
// If FixWorstHealthCondition did nothing, it means it can't handle
// the current worst condition. We must break to avoid an infinite loop.
Log.Warning($"[WulaPodDebug] Halting healing loop. FixWorstHealthCondition did not resolve: {hediffToFix?.LabelCap ?? "a missing part"}.");
break;
}
}
if (injuriesHealed > 0)
{
Messages.Message("WULA_MaintenanceHealedAllWounds".Translate(occupant.Named("PAWN")), occupant, MessageTypeDefOf.PositiveEvent);
}
else if (!maintenanceDone)
{
// If nothing was done at all, give a neutral message
Messages.Message("WULA_MaintenanceNoEffect".Translate(occupant.Named("PAWN")), occupant, MessageTypeDefOf.NeutralEvent);
}
EjectPawn();
}
public void EjectPawn(bool interrupted = false)
{
Pawn occupant = Occupant;
Log.Warning($"[WulaPodDebug] EjectPawn. Occupant: {(occupant == null ? "NULL" : occupant.LabelShortCap)}");
if (occupant != null)
{
Map mapToUse = parent.Map ?? Find.CurrentMap;
if (mapToUse == null)
{
// Try to find the map from nearby things
mapToUse = GenClosest.ClosestThing_Global(occupant.Position, Gen.YieldSingle(parent), 99999f, (thing) => thing.Map != null)?.Map;
}
if (mapToUse != null)
{
innerContainer.TryDropAll(parent.InteractionCell, mapToUse, ThingPlaceMode.Near);
if (Props.exitSound != null)
{
SoundStarter.PlayOneShot(Props.exitSound, new TargetInfo(parent.Position, mapToUse));
}
}
else
{
Log.Error($"[WulaPodDebug] EjectPawn FAILED: No valid map found to eject {occupant.LabelShortCap}. The pawn will be lost.");
}
// Additional logic to handle occupant if needed
if (interrupted)
{
occupant.needs?.mood.thoughts.memories.TryGainMemory(ThoughtDefOf.SoakingWet);
occupant.health?.AddHediff(HediffDefOf.BiosculptingSickness);
}
}
innerContainer.Clear();
state = MaintenancePodState.Idle;
Log.Warning($"[WulaPodDebug] EjectPawn finished. State set to Idle.");
}
// ===================== UI & Gizmos =====================
public override string CompInspectStringExtra()
{
StringBuilder sb = new StringBuilder();
sb.AppendLine("WULA_MaintenancePod_Status".Translate() + ": " + $"WULA_MaintenancePod_State_{state}".Translate());
if (state == MaintenancePodState.Running)
{
if (Occupant != null)
{
sb.AppendLine("Contains".Translate() + ": " + Occupant.NameShortColored.Resolve());
}
sb.AppendLine("TimeLeft".Translate() + ": " + ticksRemaining.ToStringTicksToPeriod());
}
if (!PowerOn)
{
sb.AppendLine("NoPower".Translate().Colorize(Color.red));
}
return sb.ToString().TrimEnd();
}
public override IEnumerable<Gizmo> CompGetGizmosExtra()
{
foreach (var gizmo in base.CompGetGizmosExtra())
{
yield return gizmo;
}
if (state == MaintenancePodState.Idle && PowerOn)
{
var enterCommand = new Command_Action
{
defaultLabel = "WULA_MaintenancePod_Enter".Translate(),
defaultDesc = "WULA_MaintenancePod_EnterDesc".Translate(),
icon = EnterIcon,
action = () =>
{
List<FloatMenuOption> options = GetPawnOptions();
if (options.Any())
{
Find.WindowStack.Add(new FloatMenu(options));
}
else
{
Messages.Message("WULA_MaintenancePod_NoOneNeeds".Translate(), MessageTypeDefOf.RejectInput);
}
}
};
yield return enterCommand;
}
if (state == MaintenancePodState.Running)
{
var cancelCommand = new Command_Action
{
defaultLabel = "CommandCancelConstructionLabel".Translate(),
defaultDesc = "WULA_MaintenancePod_CancelDesc".Translate(),
icon = CancelIcon,
action = () =>
{
EjectPawn();
Messages.Message("WULA_MaintenanceCanceled".Translate(), MessageTypeDefOf.NegativeEvent);
}
};
yield return cancelCommand;
}
// DEV GIZMO
if (DebugSettings.godMode && state == MaintenancePodState.Running)
{
var finishCommand = new Command_Action
{
defaultLabel = "DEV: Finish Cycle",
action = () =>
{
Log.Warning("[WulaPodDebug] DEV: Force finishing cycle.");
CycleFinished();
}
};
yield return finishCommand;
}
}
private List<FloatMenuOption> GetPawnOptions()
{
List<FloatMenuOption> options = new List<FloatMenuOption>();
// Now iterates over all pawns on the map, not just colonists.
foreach (Pawn p in parent.Map.mapPawns.AllPawns.Where(pawn => pawn.def.defName == "WulaSpecies" || pawn.def.defName == "WulaSpeciesReal"))
{
if (p.health.hediffSet.HasHediff(Props.hediffToRemove))
{
// If the pawn is downed or not a free colonist, they need to be brought to the pod.
if (p.Downed || !p.IsFreeColonist)
{
float required = RequiredComponents(p);
if (refuelableComp.Fuel < required)
{
options.Add(new FloatMenuOption(p.LabelShortCap + " (" + p.KindLabel + ", " + "WULA_MaintenancePod_NotEnoughComponents".Translate(required.ToString("F0")) + ")", null));
}
else
{
// Find colonists who can haul the pawn.
var potentialHaulers = parent.Map.mapPawns.FreeColonistsSpawned.Where(colonist =>
!colonist.Downed && colonist.CanReserveAndReach(p, PathEndMode.OnCell, Danger.Deadly) && colonist.CanReserveAndReach(parent, PathEndMode.InteractionCell, Danger.Deadly));
if (!potentialHaulers.Any())
{
// If no one can haul, then it's unreachable.
options.Add(new FloatMenuOption(p.LabelShortCap + " (" + p.KindLabel + ", " + "CannotReach".Translate() + ")", null));
}
else
{
Action action = delegate
{
// Create a menu to select which colonist should do the hauling.
var haulerOptions = new List<FloatMenuOption>();
foreach (var hauler in potentialHaulers)
{
haulerOptions.Add(new FloatMenuOption(hauler.LabelCap, delegate
{
var haulJob = JobMaker.MakeJob(JobDefOf_WULA.WULA_HaulToMaintenancePod, p, parent);
haulJob.count = 1;
hauler.jobs.TryTakeOrderedJob(haulJob, JobTag.Misc);
}));
}
Find.WindowStack.Add(new FloatMenu(haulerOptions));
};
options.Add(new FloatMenuOption(p.LabelShortCap + " (" + p.KindLabel + ")", action));
}
}
}
// If the pawn is a free colonist and can walk, they can go on their own.
else
{
if (!p.CanReach(parent, PathEndMode.InteractionCell, Danger.Deadly))
{
options.Add(new FloatMenuOption(p.LabelShortCap + " (" + "CannotReach".Translate() + ")", null));
}
else
{
float required = RequiredComponents(p);
if (refuelableComp.Fuel >= required)
{
options.Add(new FloatMenuOption(p.LabelShortCap, () =>
{
Job job = JobMaker.MakeJob(JobDefOf_WULA.WULA_EnterMaintenancePod, parent);
p.jobs.TryTakeOrderedJob(job, JobTag.Misc);
}));
}
else
{
options.Add(new FloatMenuOption(p.LabelShortCap + " (" + "WULA_MaintenancePod_NotEnoughComponents".Translate(required.ToString("F0")) + ")", null));
}
}
}
}
}
return options;
}
}
public enum MaintenancePodState
{
Idle,
Running,
}
}

View File

@@ -0,0 +1,40 @@
using Verse;
namespace WulaFallenEmpire
{
public class HediffCompProperties_MaintenanceNeed : HediffCompProperties
{
public float severityPerDayBeforeThreshold = 0.0f;
public float severityPerDayAfterThreshold = 0.0f;
public float thresholdDays = 0.0f;
public HediffCompProperties_MaintenanceNeed()
{
compClass = typeof(HediffComp_MaintenanceNeed);
}
}
public class HediffComp_MaintenanceNeed : HediffComp
{
private HediffCompProperties_MaintenanceNeed Props => (HediffCompProperties_MaintenanceNeed)props;
public override void CompPostTick(ref float severityAdjustment)
{
base.CompPostTick(ref severityAdjustment);
// We adjust severity once per game day (60000 ticks)
if (parent.ageTicks % 60000 == 0)
{
float ageInDays = (float)parent.ageTicks / 60000f;
if (ageInDays < Props.thresholdDays)
{
severityAdjustment += Props.severityPerDayBeforeThreshold;
}
else
{
severityAdjustment += Props.severityPerDayAfterThreshold;
}
}
}
}
}

View File

@@ -0,0 +1,18 @@
using RimWorld;
using Verse;
namespace WulaFallenEmpire
{
[DefOf]
public static class JobDefOf_WULA
{
public static JobDef WULA_EnterMaintenancePod;
public static JobDef WULA_HaulToMaintenancePod;
static JobDefOf_WULA()
{
DefOfHelper.EnsureInitializedInCtor(typeof(JobDefOf_WULA));
}
}
}

View File

@@ -0,0 +1,47 @@
using RimWorld;
using System;
using System.Collections.Generic;
using Verse;
using Verse.AI;
namespace WulaFallenEmpire
{
public class JobDriver_EnterMaintenancePod : JobDriver
{
private const TargetIndex PodIndex = TargetIndex.A;
protected Thing Pod => job.GetTarget(PodIndex).Thing;
public override bool TryMakePreToilReservations(bool errorOnFailed)
{
return pawn.Reserve(Pod, job, 1, -1, null, errorOnFailed);
}
protected override IEnumerable<Toil> MakeNewToils()
{
Log.Warning($"[WulaPodDebug] JobDriver_EnterMaintenancePod started for pawn: {pawn.LabelShortCap}");
this.FailOnDespawnedNullOrForbidden(PodIndex);
this.FailOnBurningImmobile(PodIndex);
var podComp = Pod.TryGetComp<CompMaintenancePod>();
this.FailOn(() => podComp == null || podComp.State != MaintenancePodState.Idle || !podComp.PowerOn);
// Go to the pod's interaction cell
Toil goToPod = Toils_Goto.GotoThing(PodIndex, PathEndMode.InteractionCell);
goToPod.AddPreInitAction(() => Log.Warning($"[WulaPodDebug] EnterJob: Pawn {pawn.LabelShortCap} is going to the pod."));
yield return goToPod;
// Enter the pod
Toil enterToil = new Toil
{
initAction = () =>
{
Log.Warning($"[WulaPodDebug] EnterJob: Pawn {pawn.LabelShortCap} has arrived and is entering the pod.");
podComp.StartCycle(pawn);
},
defaultCompleteMode = ToilCompleteMode.Instant
};
yield return enterToil;
}
}
}

View File

@@ -0,0 +1,63 @@
using System.Collections.Generic;
using RimWorld;
using Verse;
using Verse.AI;
namespace WulaFallenEmpire
{
public class JobDriver_HaulToMaintenancePod : JobDriver
{
private const TargetIndex TakeeIndex = TargetIndex.A;
private const TargetIndex PodIndex = TargetIndex.B;
protected Pawn Takee => (Pawn)job.GetTarget(TakeeIndex).Thing;
protected Building Pod => (Building)job.GetTarget(PodIndex).Thing;
protected CompMaintenancePod PodComp => Pod.TryGetComp<CompMaintenancePod>();
public override bool TryMakePreToilReservations(bool errorOnFailed)
{
return pawn.Reserve(Takee, job, 1, -1, null, errorOnFailed)
&& pawn.Reserve(Pod, job, 1, -1, null, errorOnFailed);
}
protected override IEnumerable<Toil> MakeNewToils()
{
Log.Warning($"[WulaPodDebug] JobDriver_HaulToMaintenancePod started. Hauler: {pawn.LabelShortCap}, Takee: {Takee.LabelShortCap}");
// Standard failure conditions
this.FailOnDestroyedOrNull(TakeeIndex);
this.FailOnDestroyedOrNull(PodIndex);
this.FailOnAggroMentalStateAndHostile(TakeeIndex);
this.FailOn(() => PodComp == null);
this.FailOn(() => !pawn.CanReach(Pod, PathEndMode.InteractionCell, Danger.Deadly));
this.FailOn(() => !Takee.Downed);
// Go to the pawn to be rescued
Toil goToTakee = Toils_Goto.GotoThing(TakeeIndex, PathEndMode.ClosestTouch)
.FailOnDespawnedNullOrForbidden(TakeeIndex)
.FailOnDespawnedNullOrForbidden(PodIndex)
.FailOnSomeonePhysicallyInteracting(TakeeIndex);
goToTakee.AddPreInitAction(() => Log.Warning($"[WulaPodDebug] HaulJob: {pawn.LabelShortCap} is going to pick up {Takee.LabelShortCap}."));
yield return goToTakee;
// Start carrying the pawn
Toil startCarrying = Toils_Haul.StartCarryThing(TakeeIndex, false, true, false);
startCarrying.AddPreInitAction(() => Log.Warning($"[WulaPodDebug] HaulJob: {pawn.LabelShortCap} is now carrying {Takee.LabelShortCap}."));
yield return startCarrying;
// Go to the maintenance pod
Toil goToPod = Toils_Goto.GotoThing(PodIndex, PathEndMode.InteractionCell);
goToPod.AddPreInitAction(() => Log.Warning($"[WulaPodDebug] HaulJob: {pawn.LabelShortCap} is hauling {Takee.LabelShortCap} to the pod."));
yield return goToPod;
// Place the pawn inside the pod
Toil placeInPod = ToilMaker.MakeToil("PlaceInPod");
placeInPod.initAction = delegate
{
Log.Warning($"[WulaPodDebug] HaulJob: {pawn.LabelShortCap} has arrived and is placing {Takee.LabelShortCap} in the pod.");
PodComp.StartCycle(Takee);
};
placeInPod.defaultCompleteMode = ToilCompleteMode.Instant;
yield return placeInPod;
}
}
}

View File

@@ -0,0 +1,21 @@
using RimWorld;
using Verse;
using Verse.AI;
namespace WulaFallenEmpire
{
public class WorkGiver_DoMaintenance : WorkGiver_Scanner
{
public override ThingRequest PotentialWorkThingRequest => ThingRequest.ForDef(ThingDef.Named("WULA_MaintenancePod"));
public override bool HasJobOnThing(Pawn pawn, Thing t, bool forced = false)
{
return pawn.CanReserve(t, 1, -1, null, forced);
}
public override Job JobOnThing(Pawn pawn, Thing t, bool forced = false)
{
return JobMaker.MakeJob(DefDatabase<JobDef>.GetNamed("WULA_EnterMaintenancePod"), t);
}
}
}