### HediffComp_GestaltNode - 添加 pawn 空引用检查,防止 NullReferenceException - 将 Log.Message 改为 ArachnaeLog.Debug 统一日志管理 - 新增 Notify_PawnDied 方法处理 Pawn 死亡时的过渡状态清理 - 修复 UpdateTransitionState 中重复声明 pawn 变量的问题 ### CompAbilityEffect_LaunchMultiProjectile - 添加目标有效性检查,处理目标死亡或消失的情况 - 实现动态目标追踪,更新目标位置 - 移除未使用的 parametersInitialized 字段 - 新增 ForceReinitialize 方法支持状态变化时重新初始化 ### CompHediffGiver - 改进异常处理,记录警告日志而非静默吞掉异常 - 重构 IsSymmetricalPart 方法,使用翻译键和 BodyPartTagDef 支持本地化 ### HediffComp_Spawner - 将 DebugSettings.debugLogging 改为 Prefs.DevMode - 修复所有 UTF-8 编码乱码注释(约30处) ### Comp_PawnResearchBlueprintReader - 修复灵能科研点消耗时机,确保先检查→再消耗→最后添加进度 - 提高研究进度添加的原子性
993 lines
37 KiB
C#
993 lines
37 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using RimWorld;
|
||
using RimWorld.Planet;
|
||
using Verse;
|
||
|
||
namespace ArachnaeSwarm
|
||
{
|
||
public class HediffComp_Spawner : HediffComp
|
||
{
|
||
public HediffCompProperties_Spawner Props
|
||
{
|
||
get
|
||
{
|
||
return (HediffCompProperties_Spawner)this.props;
|
||
}
|
||
}
|
||
|
||
public override void CompExposeData()
|
||
{
|
||
Scribe_Values.Look<int>(ref this.ticksUntilSpawn, "ticksUntilSpawn", 0, false);
|
||
Scribe_Values.Look<int>(ref this.initialTicksUntilSpawn, "initialTicksUntilSpawn", 0, false);
|
||
Scribe_Values.Look<float>(ref this.calculatedMinDaysB4Next, "calculatedMinDaysB4Next", 0f, false);
|
||
Scribe_Values.Look<float>(ref this.calculatedMaxDaysB4Next, "calculatedMaxDaysB4Next", 0f, false);
|
||
Scribe_Values.Look<int>(ref this.calculatedQuantity, "calculatedQuantity", 0, false);
|
||
Scribe_Values.Look<int>(ref this.graceTicks, "graceTicks", 0, false);
|
||
}
|
||
|
||
public override void CompPostMake()
|
||
{
|
||
this.myDebug = this.Props.debug;
|
||
Warn(string.Concat(new string[]
|
||
{
|
||
">>> ",
|
||
this.parent.pawn.Label,
|
||
" - ",
|
||
this.parent.def.defName,
|
||
" - CompPostMake start"
|
||
}), this.myDebug);
|
||
TraceProps();
|
||
CheckProps();
|
||
CalculateValues();
|
||
CheckCalculatedValues();
|
||
TraceCalculatedValues();
|
||
if (this.initialTicksUntilSpawn == 0)
|
||
{
|
||
Warn("Reseting countdown bc initialTicksUntilSpawn == 0 (comppostmake)", this.myDebug);
|
||
ResetCountdown();
|
||
}
|
||
}
|
||
|
||
public override void CompPostTick(ref float severityAdjustment)
|
||
{
|
||
this.pawn = this.parent.pawn;
|
||
if (!OkPawn(this.pawn))
|
||
{
|
||
return;
|
||
}
|
||
if (this.blockSpawn)
|
||
{
|
||
return;
|
||
}
|
||
if (this.graceTicks > 0)
|
||
{
|
||
this.graceTicks--;
|
||
return;
|
||
}
|
||
if (this.Props.hungerRelative && IsHungry(this.pawn, this.myDebug))
|
||
{
|
||
int num = (int)(this.RandomGraceDays() * 60000f);
|
||
this.hungerReset++;
|
||
this.graceTicks = num;
|
||
return;
|
||
}
|
||
if (this.Props.healthRelative && IsInjured(this.pawn, this.myDebug))
|
||
{
|
||
int num2 = (int)(this.RandomGraceDays() * 60000f);
|
||
this.healthReset++;
|
||
this.graceTicks = num2;
|
||
return;
|
||
}
|
||
this.hungerReset = (this.healthReset = 0);
|
||
if (this.CheckShouldSpawn())
|
||
{
|
||
Warn("Reseting countdown bc spawned thing", this.myDebug);
|
||
CalculateValues();
|
||
CheckCalculatedValues();
|
||
ResetCountdown();
|
||
if (Rand.Chance(this.Props.randomGrace))
|
||
{
|
||
int num3 = (int)(this.RandomGraceDays() * 60000f);
|
||
this.graceTicks = num3;
|
||
}
|
||
}
|
||
}
|
||
|
||
private void TraceProps()
|
||
{
|
||
Warn(string.Concat(new string[]
|
||
{
|
||
"Props => minDaysB4Next: ",
|
||
this.Props.minDaysB4Next.ToString(),
|
||
"; maxDaysB4Next: ",
|
||
this.Props.maxDaysB4Next.ToString(),
|
||
"; randomGrace: ",
|
||
this.Props.randomGrace.ToString(),
|
||
"; graceDays: ",
|
||
this.Props.graceDays.ToString(),
|
||
"; hungerRelative: ",
|
||
this.Props.hungerRelative.ToString(),
|
||
"; healthRelative: ",
|
||
this.Props.healthRelative.ToString(),
|
||
"; destroyRandomBodyPart: ",
|
||
this.Props.destroyRandomBodyPart.ToString(),
|
||
"; "
|
||
}), this.myDebug);
|
||
if (this.Props.animalThing)
|
||
{
|
||
Warn(string.Concat(new string[]
|
||
{
|
||
"animalThing: ",
|
||
this.Props.animalThing.ToString(),
|
||
"; animalName: ",
|
||
this.Props.animalToSpawn.defName,
|
||
"; factionOfPlayerAnimal: ",
|
||
this.Props.factionOfPlayerAnimal.ToString(),
|
||
"; "
|
||
}), this.myDebug);
|
||
}
|
||
if (this.Props.ageWeightedQuantity)
|
||
{
|
||
Warn(string.Concat(new string[]
|
||
{
|
||
"ageWeightedQuantity:",
|
||
this.Props.ageWeightedQuantity.ToString(),
|
||
"; olderBiggerQuantity:",
|
||
this.Props.olderBiggerQuantity.ToString(),
|
||
"; ",
|
||
this.myDebug.ToString()
|
||
}), false);
|
||
if (this.Props.exponentialQuantity)
|
||
{
|
||
Warn(string.Concat(new string[]
|
||
{
|
||
"exponentialQuantity:",
|
||
this.Props.exponentialQuantity.ToString(),
|
||
"; exponentialRatioLimit:",
|
||
this.Props.exponentialRatioLimit.ToString(),
|
||
"; "
|
||
}), this.myDebug);
|
||
}
|
||
}
|
||
Warn(string.Concat(new string[]
|
||
{
|
||
"ageWeightedPeriod:",
|
||
this.Props.ageWeightedPeriod.ToString(),
|
||
"; olderSmallerPeriod:",
|
||
this.Props.olderSmallerPeriod.ToString(),
|
||
"; ",
|
||
this.myDebug.ToString()
|
||
}), false);
|
||
}
|
||
|
||
private void CalculateValues()
|
||
{
|
||
float num = GetPawnAgeOverlifeExpectancyRatio(this.parent.pawn, this.myDebug);
|
||
num = ((num > 1f) ? 1f : num);
|
||
this.calculatedMinDaysB4Next = this.Props.minDaysB4Next;
|
||
this.calculatedMaxDaysB4Next = this.Props.maxDaysB4Next;
|
||
if (this.Props.ageWeightedPeriod)
|
||
{
|
||
float num2 = this.Props.olderSmallerPeriod ? (-num) : num;
|
||
this.calculatedMinDaysB4Next = this.Props.minDaysB4Next * (1f + num2);
|
||
this.calculatedMaxDaysB4Next = this.Props.maxDaysB4Next * (1f + num2);
|
||
Warn(string.Concat(new string[]
|
||
{
|
||
" ageWeightedPeriod: ",
|
||
this.Props.ageWeightedPeriod.ToString(),
|
||
" ageRatio: ",
|
||
num.ToString(),
|
||
" minDaysB4Next: ",
|
||
this.Props.minDaysB4Next.ToString(),
|
||
" maxDaysB4Next: ",
|
||
this.Props.maxDaysB4Next.ToString(),
|
||
" daysAgeRatio: ",
|
||
num2.ToString(),
|
||
" calculatedMinDaysB4Next: ",
|
||
this.calculatedMinDaysB4Next.ToString(),
|
||
"; calculatedMaxDaysB4Next: ",
|
||
this.calculatedMaxDaysB4Next.ToString(),
|
||
"; "
|
||
}), this.myDebug);
|
||
}
|
||
this.calculatedQuantity = this.Props.spawnCount;
|
||
if (this.Props.ageWeightedQuantity)
|
||
{
|
||
float num3 = this.Props.olderBiggerQuantity ? num : (-num);
|
||
Warn("quantityAgeRatio: " + num3.ToString(), this.myDebug);
|
||
this.calculatedQuantity = (int)Math.Round((double)this.Props.spawnCount * (double)(1f + num3));
|
||
if (this.Props.exponentialQuantity)
|
||
{
|
||
num3 = 1f - num;
|
||
if (num3 == 0f)
|
||
{
|
||
Warn(">ERROR< quantityAgeRatio is f* up : " + num3.ToString(), this.myDebug);
|
||
this.blockSpawn = true;
|
||
DestroyParentHediff(this.parent, this.myDebug);
|
||
return;
|
||
}
|
||
float num4 = this.Props.olderBiggerQuantity ? (1f / num3) : (num3 * num3);
|
||
bool flag = false;
|
||
bool flag2 = false;
|
||
if (num4 > (float)this.Props.exponentialRatioLimit)
|
||
{
|
||
num4 = (float)this.Props.exponentialRatioLimit;
|
||
flag = true;
|
||
}
|
||
this.calculatedQuantity = (int)Math.Round((double)this.Props.spawnCount * (double)num4);
|
||
if (this.calculatedQuantity < 1)
|
||
{
|
||
this.calculatedQuantity = 1;
|
||
flag2 = true;
|
||
}
|
||
Warn(string.Concat(new string[]
|
||
{
|
||
" exponentialQuantity: ",
|
||
this.Props.exponentialQuantity.ToString(),
|
||
"; expoFactor: ",
|
||
num4.ToString(),
|
||
"; gotLimited: ",
|
||
flag.ToString(),
|
||
"; gotAugmented: ",
|
||
flag2.ToString()
|
||
}), this.myDebug);
|
||
}
|
||
Warn("; Props.spawnCount: " + this.Props.spawnCount.ToString() + "; calculatedQuantity: " + this.calculatedQuantity.ToString(), this.myDebug);
|
||
}
|
||
}
|
||
|
||
private void CheckCalculatedValues()
|
||
{
|
||
if (this.calculatedQuantity > this.errorSpawnCount)
|
||
{
|
||
Warn(string.Concat(new string[]
|
||
{
|
||
">ERROR< calculatedQuantity is too high: ",
|
||
this.calculatedQuantity.ToString(),
|
||
"(>",
|
||
this.errorSpawnCount.ToString(),
|
||
"), check and adjust your hediff props"
|
||
}), this.myDebug);
|
||
this.blockSpawn = true;
|
||
DestroyParentHediff(this.parent, this.myDebug);
|
||
return;
|
||
}
|
||
if (this.calculatedMinDaysB4Next < this.errorMinDaysB4Next)
|
||
{
|
||
this.calculatedMinDaysB4Next = this.errorMinDaysB4Next;
|
||
}
|
||
if (this.calculatedMaxDaysB4Next < this.errorMinDaysB4Next)
|
||
{
|
||
this.calculatedMaxDaysB4Next = this.errorMinDaysB4Next;
|
||
}
|
||
}
|
||
|
||
private void TraceCalculatedValues()
|
||
{
|
||
Warn("calculatedMinDaysB4Next:" + this.calculatedMinDaysB4Next.ToString(), this.myDebug);
|
||
Warn("calculatedMaxDaysB4Next:" + this.calculatedMaxDaysB4Next.ToString(), this.myDebug);
|
||
Warn("calculatedQuantity:" + this.calculatedQuantity.ToString(), this.myDebug);
|
||
}
|
||
|
||
private void CheckProps()
|
||
{
|
||
if (this.Props.animalThing && this.Props.animalToSpawn == null)
|
||
{
|
||
Warn(this.parent.pawn.Label + " has a hediffcomp_spawner with animalflag but without animalToSpawn", true);
|
||
this.blockSpawn = true;
|
||
DestroyParentHediff(this.parent, this.myDebug);
|
||
return;
|
||
}
|
||
if (this.Props.minDaysB4Next <= 0f)
|
||
{
|
||
Warn(this.parent.pawn.Label + " has a hediffcomp_spawner with null/negative minDaysB4Next", true);
|
||
this.blockSpawn = true;
|
||
DestroyParentHediff(this.parent, this.myDebug);
|
||
return;
|
||
}
|
||
if (this.Props.maxDaysB4Next <= 0f)
|
||
{
|
||
Warn(this.parent.pawn.Label + " has a hediffcomp_spawner with null/negative maxDaysB4Next", true);
|
||
this.blockSpawn = true;
|
||
DestroyParentHediff(this.parent, this.myDebug);
|
||
return;
|
||
}
|
||
if (this.Props.maxDaysB4Next < this.Props.minDaysB4Next)
|
||
{
|
||
Warn(this.parent.pawn.Label + " has a hediffcomp_spawner with maxDaysB4Next < minDaysB4Next", true);
|
||
this.blockSpawn = true;
|
||
DestroyParentHediff(this.parent, this.myDebug);
|
||
return;
|
||
}
|
||
if (this.Props.spawnCount <= 0)
|
||
{
|
||
Warn(this.parent.pawn.Label + " has a hediffcomp_spawner with null/negative spawnCount", true);
|
||
this.blockSpawn = true;
|
||
DestroyParentHediff(this.parent, this.myDebug);
|
||
return;
|
||
}
|
||
if (!this.Props.animalThing && this.Props.thingToSpawn == null)
|
||
{
|
||
Warn(this.parent.pawn.Label + " has a hediffcomp_spawner without thingToSpawn", true);
|
||
this.blockSpawn = true;
|
||
DestroyParentHediff(this.parent, this.myDebug);
|
||
return;
|
||
}
|
||
if (this.Props.ageWeightedQuantity && this.Props.exponentialQuantity && this.Props.exponentialRatioLimit > this.errorExponentialLimit)
|
||
{
|
||
Warn(string.Concat(new string[]
|
||
{
|
||
this.parent.pawn.Label,
|
||
" has a hediffcomp_spawner with exponentialRatioLimit>",
|
||
this.errorExponentialLimit.ToString(),
|
||
" this is not allowed. It will be set to ",
|
||
this.errorExponentialLimit.ToString()
|
||
}), true);
|
||
this.Props.exponentialRatioLimit = this.errorExponentialLimit;
|
||
}
|
||
}
|
||
|
||
private bool CheckShouldSpawn()
|
||
{
|
||
this.ticksUntilSpawn--;
|
||
if (this.ticksUntilSpawn <= 0)
|
||
{
|
||
if (this.TryDoSpawn())
|
||
{
|
||
return true;
|
||
}
|
||
Warn("Did not spawn, reseting countdown", this.myDebug);
|
||
ResetCountdown();
|
||
}
|
||
return false;
|
||
}
|
||
|
||
public bool TryDoSpawn()
|
||
{
|
||
Pawn pawn = this.parent.pawn;
|
||
|
||
if (this.Props.animalThing)
|
||
{
|
||
// 动物生成逻辑保持不变
|
||
if (this.Props.spawnMaxAdjacent > 0 && pawn.Map.mapPawns.AllPawns.Where(delegate(Pawn mP)
|
||
{
|
||
ThingDef defToCompare = this.Props.animalThing ? this.Props.animalToSpawn?.race : this.Props.thingToSpawn;
|
||
if (defToCompare?.race == null)
|
||
{
|
||
return false;
|
||
}
|
||
return mP.def == defToCompare && mP.Position.InHorDistOf(pawn.Position, (float)this.Props.spawnMaxAdjacent);
|
||
}).Count<Pawn>() >= this.Props.spawnMaxAdjacent)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (this.Props.animalToSpawn == null)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
Faction faction = this.Props.factionOfPlayerAnimal ? Faction.OfPlayer : null;
|
||
int i = 0;
|
||
while (i < this.calculatedQuantity)
|
||
{
|
||
IntVec3 intVec;
|
||
if (!this.TryFindSpawnCell(out intVec))
|
||
{
|
||
return false;
|
||
}
|
||
Pawn pawn2 = PawnGenerator.GeneratePawn(this.Props.animalToSpawn, faction);
|
||
if (pawn2 == null)
|
||
{
|
||
return false;
|
||
}
|
||
GenSpawn.Spawn(pawn2, intVec, pawn.Map, WipeMode.Vanish);
|
||
pawn2.SetFaction(faction, null);
|
||
FilthMaker.TryMakeFilth(intVec, pawn.Map, ThingDefOf.Filth_AmnioticFluid, pawn.LabelIndefinite(), 5, FilthSourceFlags.None);
|
||
if (!this.Props.spawnForbidden)
|
||
{
|
||
pawn2.playerSettings.Master = pawn;
|
||
pawn2.training.Train(TrainableDefOf.Obedience, pawn, true);
|
||
}
|
||
i++;
|
||
continue;
|
||
}
|
||
if (PawnUtility.ShouldSendNotificationAbout(pawn))
|
||
{
|
||
Messages.Message(this.Props.spawnVerb.Translate(pawn.Named("PAWN")), pawn, MessageTypeDefOf.PositiveEvent, true);
|
||
}
|
||
return true;
|
||
}
|
||
else
|
||
{
|
||
// 重新设计物品生成逻辑:按优先级顺序尝试
|
||
bool success = TrySpawnItemWithPriority(pawn);
|
||
|
||
// === 新增:如果配置了销毁随机部位,在成功生成物品后执行 ===
|
||
if (success && this.Props.destroyRandomBodyPart)
|
||
{
|
||
DestroyRandomBodyPart(pawn);
|
||
}
|
||
|
||
return success;
|
||
}
|
||
}
|
||
|
||
// 新增:销毁随机身体部位(参考 CompAbilityEffect_DestroyOwnBodyPart)
|
||
private void DestroyRandomBodyPart(Pawn pawn)
|
||
{
|
||
try
|
||
{
|
||
if (pawn == null || pawn.Dead)
|
||
{
|
||
Warn($"Cannot destroy body part for null or dead pawn", this.myDebug);
|
||
return;
|
||
}
|
||
|
||
// 获取所有可以销毁的身体部位
|
||
List<BodyPartRecord> possibleParts = GetDestroyableBodyParts(pawn);
|
||
|
||
if (possibleParts.Count == 0)
|
||
{
|
||
Warn($"No destroyable body parts found for {pawn.LabelShort}", this.myDebug);
|
||
return;
|
||
}
|
||
|
||
// 随机选择一个部位
|
||
BodyPartRecord partToDestroy = possibleParts.RandomElement();
|
||
|
||
if (partToDestroy == null)
|
||
{
|
||
Warn($"Selected null body part for {pawn.LabelShort}", this.myDebug);
|
||
return;
|
||
}
|
||
|
||
// 记录部位名称
|
||
string partName = partToDestroy.def?.label ?? "未知部位";
|
||
|
||
// 添加缺失部位hediff
|
||
pawn.health.AddHediff(HediffDefOf.MissingBodyPart, partToDestroy);
|
||
|
||
Warn($"Destroyed {partName} on {pawn.LabelShort}", this.myDebug);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Log.Error($"Error destroying random body part for {pawn?.LabelShort}: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
// 新增:获取可以销毁的身体部位列表
|
||
private List<BodyPartRecord> GetDestroyableBodyParts(Pawn pawn)
|
||
{
|
||
List<BodyPartRecord> destroyableParts = new List<BodyPartRecord>();
|
||
|
||
if (pawn?.health?.hediffSet == null)
|
||
return destroyableParts;
|
||
|
||
// 获取所有身体部位
|
||
List<BodyPartRecord> allParts = pawn.RaceProps.body.AllParts;
|
||
|
||
foreach (BodyPartRecord part in allParts)
|
||
{
|
||
// 排除核心部位(避免死亡)
|
||
if (IsCriticalBodyPart(part))
|
||
continue;
|
||
|
||
// 排除已经缺失的部位
|
||
if (pawn.health.hediffSet.PartIsMissing(part))
|
||
continue;
|
||
|
||
// 排除已经有严重伤害的部位(可选)
|
||
if (pawn.health.hediffSet.PartOrAnyAncestorHasDirectlyAddedParts(part))
|
||
continue;
|
||
|
||
if (PartOrAnyDescendantHasDirectlyAddedParts(pawn, part))
|
||
continue;
|
||
|
||
destroyableParts.Add(part);
|
||
}
|
||
|
||
return destroyableParts;
|
||
}
|
||
|
||
// 新增:检查是否是关键身体部位
|
||
private bool IsCriticalBodyPart(BodyPartRecord part)
|
||
{
|
||
if (part == null)
|
||
return false;
|
||
|
||
// 根据标签判断是否为关键部位
|
||
if (part.def.tags != null)
|
||
{
|
||
// 这些标签通常表示关键部位
|
||
if (part.def.tags.Contains(BodyPartTagDefOf.ConsciousnessSource) || // 意识源(大脑)
|
||
part.def.tags.Contains(BodyPartTagDefOf.BloodPumpingSource) || // 血液泵源(心脏)
|
||
part.def.tags.Contains(BodyPartTagDefOf.BreathingSource) || // 呼吸源(肺)
|
||
part.def.tags.Contains(BodyPartTagDefOf.MetabolismSource) // 代谢源(肝脏)
|
||
)
|
||
{
|
||
return true;
|
||
}
|
||
}
|
||
|
||
// 根据深度判断(深度较深的通常是内部器官)
|
||
if (part.depth == BodyPartDepth.Inside && part.parent == null)
|
||
return true;
|
||
|
||
return false;
|
||
}
|
||
|
||
// 新增:按优先级顺序尝试生成物品
|
||
private bool TrySpawnItemWithPriority(Pawn pawn)
|
||
{
|
||
Thing thing = ThingMaker.MakeThing(this.Props.thingToSpawn, null);
|
||
if (thing == null)
|
||
{
|
||
Warn("Failed to create thing: " + this.Props.thingToSpawn?.defName, this.myDebug);
|
||
return false;
|
||
}
|
||
thing.stackCount = this.calculatedQuantity;
|
||
|
||
ThingDef originalThingDef = thing.def;
|
||
int originalStackCount = thing.stackCount;
|
||
|
||
bool success = false;
|
||
string spawnMethod = "";
|
||
Thing spawnedThing = null;
|
||
int addedInventoryCount = 0;
|
||
|
||
if (!success)
|
||
{
|
||
success = TrySpawnAtNearbyEmptyCell(pawn, thing, ref spawnMethod, out spawnedThing);
|
||
}
|
||
|
||
if (!success)
|
||
{
|
||
success = TrySpawnAtPawnPosition(pawn, thing, ref spawnMethod, out spawnedThing);
|
||
}
|
||
|
||
if (!success)
|
||
{
|
||
success = TrySpawnInInventory(pawn, thing, ref spawnMethod, out spawnedThing, out addedInventoryCount);
|
||
}
|
||
|
||
if (success)
|
||
{
|
||
bool verified = VerifySpawnSuccess(
|
||
pawn,
|
||
originalThingDef,
|
||
originalStackCount,
|
||
spawnMethod,
|
||
spawnedThing,
|
||
addedInventoryCount
|
||
);
|
||
|
||
if (!verified && spawnMethod != "inventory")
|
||
{
|
||
Warn($"Spawn verification failed for {spawnMethod}, attempting inventory fallback", this.myDebug);
|
||
|
||
Thing fallbackThing = ThingMaker.MakeThing(originalThingDef, null);
|
||
fallbackThing.stackCount = originalStackCount;
|
||
|
||
success = TrySpawnInInventory(pawn, fallbackThing, ref spawnMethod, out spawnedThing, out addedInventoryCount);
|
||
|
||
if (success)
|
||
{
|
||
spawnMethod = "inventory_fallback";
|
||
verified = VerifySpawnSuccess(
|
||
pawn,
|
||
originalThingDef,
|
||
originalStackCount,
|
||
spawnMethod,
|
||
spawnedThing,
|
||
addedInventoryCount
|
||
);
|
||
if (!verified)
|
||
{
|
||
Warn("Inventory fallback also failed verification", this.myDebug);
|
||
success = false;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (success && verified)
|
||
{
|
||
if (PawnUtility.ShouldSendNotificationAbout(pawn))
|
||
{
|
||
Thing messageThing = spawnedThing ?? thing;
|
||
bool spawnedInInventory = spawnMethod == "inventory" || spawnMethod == "inventory_fallback";
|
||
Messages.Message(
|
||
this.Props.spawnVerb.Translate(pawn.Named("PAWN"), messageThing.Named("THING")),
|
||
spawnedInInventory ? pawn : messageThing,
|
||
MessageTypeDefOf.PositiveEvent,
|
||
true
|
||
);
|
||
}
|
||
Warn($"Successfully spawned {originalStackCount}x {originalThingDef.defName} via {spawnMethod}", this.myDebug);
|
||
return true;
|
||
}
|
||
}
|
||
|
||
Warn($"Failed to spawn {originalStackCount}x {originalThingDef.defName} after all attempts", this.myDebug);
|
||
return false;
|
||
}
|
||
|
||
private bool TrySpawnAtNearbyEmptyCell(Pawn pawn, Thing thing, ref string spawnMethod, out Thing spawnedThing)
|
||
{
|
||
spawnedThing = null;
|
||
IntVec3 spawnCell;
|
||
if (TryFindSpawnCell(out spawnCell))
|
||
{
|
||
if (this.Props.spawnForbidden)
|
||
{
|
||
thing.SetForbidden(true, true);
|
||
}
|
||
|
||
Thing resultingThing;
|
||
if (GenPlace.TryPlaceThing(thing, spawnCell, pawn.Map, ThingPlaceMode.Direct, out resultingThing, null, null, default(Rot4)))
|
||
{
|
||
spawnMethod = "nearby_cell";
|
||
spawnedThing = resultingThing;
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
private bool TrySpawnAtPawnPosition(Pawn pawn, Thing thing, ref string spawnMethod, out Thing spawnedThing)
|
||
{
|
||
spawnedThing = null;
|
||
IntVec3 pawnPosition = pawn.Position;
|
||
if (pawnPosition.IsValid && pawnPosition.Walkable(pawn.Map))
|
||
{
|
||
if (this.Props.spawnForbidden)
|
||
{
|
||
thing.SetForbidden(true, true);
|
||
}
|
||
|
||
Thing resultingThing;
|
||
if (GenPlace.TryPlaceThing(thing, pawnPosition, pawn.Map, ThingPlaceMode.Direct, out resultingThing, null, null, default(Rot4)))
|
||
{
|
||
spawnMethod = "pawn_position";
|
||
spawnedThing = resultingThing;
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
private bool TrySpawnInInventory(Pawn pawn, Thing thing, ref string spawnMethod, out Thing spawnedThing, out int addedCount)
|
||
{
|
||
spawnedThing = null;
|
||
addedCount = 0;
|
||
if (pawn.inventory == null)
|
||
{
|
||
Warn($"Pawn {pawn.Label} does not have an inventory", this.myDebug);
|
||
return false;
|
||
}
|
||
|
||
int before = CountDefInInventory(pawn, thing.def);
|
||
if (pawn.inventory.innerContainer.TryAdd(thing))
|
||
{
|
||
int after = CountDefInInventory(pawn, thing.def);
|
||
addedCount = Math.Max(0, after - before);
|
||
spawnMethod = "inventory";
|
||
spawnedThing = pawn.inventory.innerContainer.FirstOrDefault(t => t.def == thing.def);
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
private bool VerifySpawnSuccess(Pawn pawn, ThingDef thingDef, int expectedCount, string spawnMethod, Thing spawnedThing, int addedInventoryCount)
|
||
{
|
||
bool verificationSuccess = false;
|
||
|
||
switch (spawnMethod)
|
||
{
|
||
case "nearby_cell":
|
||
case "pawn_position":
|
||
verificationSuccess =
|
||
spawnedThing != null &&
|
||
spawnedThing.def == thingDef &&
|
||
spawnedThing.Map == pawn.Map &&
|
||
spawnedThing.Position.InHorDistOf(pawn.Position, 2f);
|
||
break;
|
||
|
||
case "inventory":
|
||
case "inventory_fallback":
|
||
verificationSuccess = addedInventoryCount >= expectedCount;
|
||
break;
|
||
}
|
||
|
||
if (!verificationSuccess)
|
||
{
|
||
Warn($"Spawn verification failed for {thingDef.defName} via {spawnMethod}", this.myDebug);
|
||
}
|
||
else
|
||
{
|
||
Warn($"Spawn verification successful for {thingDef.defName} via {spawnMethod}", this.myDebug);
|
||
}
|
||
|
||
return verificationSuccess;
|
||
}
|
||
|
||
private int CountDefInInventory(Pawn pawn, ThingDef thingDef)
|
||
{
|
||
if (pawn?.inventory?.innerContainer == null || thingDef == null)
|
||
{
|
||
return 0;
|
||
}
|
||
return pawn.inventory.innerContainer.Where(t => t.def == thingDef).Sum(t => t.stackCount);
|
||
}
|
||
|
||
private bool TryFindSpawnCell(out IntVec3 result)
|
||
{
|
||
result = IntVec3.Invalid;
|
||
bool result2;
|
||
if (this.pawn == null)
|
||
{
|
||
result2 = false;
|
||
}
|
||
else
|
||
{
|
||
Map map = this.pawn.Map;
|
||
if (map == null)
|
||
{
|
||
result2 = false;
|
||
}
|
||
else
|
||
{
|
||
// 修改这里:将半径从3减少到2,让生成位置更靠近Pawn
|
||
int searchRadius = 2;
|
||
|
||
// 首先尝试在Pawn的相邻单元格生成(半径为1)
|
||
result = CellFinder.RandomClosewalkCellNear(this.pawn.Position, map, 1, null);
|
||
|
||
// 如果相邻单元格找不到合适位置,再尝试稍远一点(半径为2)
|
||
if (!result.IsValid)
|
||
{
|
||
result = CellFinder.RandomClosewalkCellNear(this.pawn.Position, map, searchRadius, null);
|
||
}
|
||
|
||
// 如果还是找不到,尝试Pawn当前位置(作为最后手段)
|
||
if (!result.IsValid && this.pawn.Position.IsValid && this.pawn.Position.Walkable(map))
|
||
{
|
||
result = this.pawn.Position;
|
||
}
|
||
|
||
result2 = result.IsValid;
|
||
}
|
||
}
|
||
return result2;
|
||
}
|
||
|
||
private void ResetCountdown()
|
||
{
|
||
this.ticksUntilSpawn = (int)(this.RandomDays2wait() * 60000f);
|
||
this.initialTicksUntilSpawn = this.ticksUntilSpawn;
|
||
}
|
||
|
||
private float RandomDays2wait()
|
||
{
|
||
return Rand.Range(this.calculatedMinDaysB4Next, this.calculatedMaxDaysB4Next);
|
||
}
|
||
|
||
private float RandomGraceDays()
|
||
{
|
||
return Rand.Range(this.Props.graceDays / 2f, this.Props.graceDays);
|
||
}
|
||
|
||
public override string CompTipStringExtra
|
||
{
|
||
get
|
||
{
|
||
if (!this.myDebug)
|
||
{
|
||
return null;
|
||
}
|
||
string text = "ticksUntilSpawn: " + this.ticksUntilSpawn.ToString() + "\n";
|
||
string text2 = text;
|
||
text = string.Concat(new string[]
|
||
{
|
||
text2,
|
||
"initialTicksUntilSpawn: ",
|
||
this.initialTicksUntilSpawn.ToString(),
|
||
"\n"
|
||
});
|
||
text2 = text;
|
||
text = string.Concat(new string[]
|
||
{
|
||
text2,
|
||
"graceTicks: ",
|
||
this.graceTicks.ToString(),
|
||
"\n"
|
||
});
|
||
text2 = text;
|
||
text = string.Concat(new string[]
|
||
{
|
||
text2,
|
||
"hunger resets: ",
|
||
this.hungerReset.ToString(),
|
||
"\n"
|
||
});
|
||
text2 = text;
|
||
text = string.Concat(new string[]
|
||
{
|
||
text2,
|
||
"health resets: ",
|
||
this.healthReset.ToString(),
|
||
"\n"
|
||
});
|
||
text2 = text;
|
||
text = string.Concat(new string[]
|
||
{
|
||
text2,
|
||
"calculatedMinDaysB4Next: ",
|
||
this.calculatedMinDaysB4Next.ToString(),
|
||
"\n"
|
||
});
|
||
text2 = text;
|
||
text = string.Concat(new string[]
|
||
{
|
||
text2,
|
||
"calculatedMaxDaysB4Next: ",
|
||
this.calculatedMaxDaysB4Next.ToString(),
|
||
"\n"
|
||
});
|
||
text2 = text;
|
||
text = string.Concat(new string[]
|
||
{
|
||
text2,
|
||
"calculatedQuantity: ",
|
||
this.calculatedQuantity.ToString(),
|
||
"\n"
|
||
});
|
||
return text + "blockSpawn: " + this.blockSpawn.ToString();
|
||
}
|
||
}
|
||
|
||
// === 整合的 Tools 方法 ===
|
||
|
||
private void DestroyParentHediff(Hediff parentHediff, bool debug = false)
|
||
{
|
||
if (parentHediff.pawn != null && parentHediff.def.defName != null && debug)
|
||
{
|
||
Warn(parentHediff.pawn.Label + "'s Hediff: " + parentHediff.def.defName + " says goodbye.", debug);
|
||
}
|
||
parentHediff.Severity = 0f;
|
||
}
|
||
|
||
private float GetPawnAgeOverlifeExpectancyRatio(Pawn pawn, bool debug = false)
|
||
{
|
||
float result = 1f;
|
||
if (pawn == null)
|
||
{
|
||
if (debug)
|
||
{
|
||
Warn("GetPawnAgeOverlifeExpectancyRatio pawn NOT OK", debug);
|
||
}
|
||
return result;
|
||
}
|
||
result = pawn.ageTracker.AgeBiologicalYearsFloat / pawn.RaceProps.lifeExpectancy;
|
||
if (debug)
|
||
{
|
||
Warn(string.Concat(new string[]
|
||
{
|
||
pawn.Label,
|
||
" Age: ",
|
||
pawn.ageTracker.AgeBiologicalYearsFloat.ToString(),
|
||
"; lifeExpectancy: ",
|
||
pawn.RaceProps.lifeExpectancy.ToString(),
|
||
"; ratio:",
|
||
result.ToString()
|
||
}), debug);
|
||
}
|
||
return result;
|
||
}
|
||
private bool OkPawn(Pawn pawn)
|
||
{
|
||
return pawn != null && pawn.Map != null;
|
||
}
|
||
|
||
private bool IsHungry(Pawn pawn, bool debug = false)
|
||
{
|
||
if (pawn == null)
|
||
{
|
||
if (debug)
|
||
{
|
||
Warn("pawn is null - IsHungry", debug);
|
||
}
|
||
return false;
|
||
}
|
||
|
||
bool starving = pawn.needs.food != null && pawn.needs.food.CurCategory == HungerCategory.Starving;
|
||
if (debug && starving)
|
||
{
|
||
Warn(pawn.Label + " is hungry", debug);
|
||
}
|
||
return starving;
|
||
}
|
||
|
||
private bool IsInjured(Pawn pawn, bool debug = false)
|
||
{
|
||
if (pawn == null)
|
||
{
|
||
if (debug)
|
||
{
|
||
Warn("pawn is null - IsInjured", debug);
|
||
}
|
||
return false;
|
||
}
|
||
|
||
float injurySeverity = 0f;
|
||
List<Hediff> hediffs = pawn.health.hediffSet.hediffs;
|
||
for (int i = 0; i < hediffs.Count; i++)
|
||
{
|
||
if (hediffs[i] is Hediff_Injury && !hediffs[i].IsPermanent())
|
||
{
|
||
injurySeverity += hediffs[i].Severity;
|
||
}
|
||
}
|
||
|
||
bool injured = injurySeverity > 0f;
|
||
if (debug && injured)
|
||
{
|
||
Warn(pawn.Label + " is injured", debug);
|
||
}
|
||
return injured;
|
||
}
|
||
|
||
private bool PartOrAnyDescendantHasDirectlyAddedParts(Pawn pawn, BodyPartRecord part)
|
||
{
|
||
if (pawn?.health?.hediffSet == null || part == null)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (pawn.health.hediffSet.HasDirectlyAddedPartFor(part))
|
||
{
|
||
return true;
|
||
}
|
||
|
||
if (part.parts == null || part.parts.Count == 0)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
for (int i = 0; i < part.parts.Count; i++)
|
||
{
|
||
if (PartOrAnyDescendantHasDirectlyAddedParts(pawn, part.parts[i]))
|
||
{
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
private void Warn(string warning, bool debug = false)
|
||
{
|
||
if (debug && Prefs.DevMode)
|
||
{
|
||
Log.Message($"[HediffComp_Spawner] {warning}");
|
||
}
|
||
}
|
||
|
||
private int ticksUntilSpawn;
|
||
private int initialTicksUntilSpawn;
|
||
private int hungerReset;
|
||
private int healthReset;
|
||
private int graceTicks;
|
||
private Pawn pawn;
|
||
private float calculatedMaxDaysB4Next = 2f;
|
||
private float calculatedMinDaysB4Next = 1f;
|
||
private int calculatedQuantity = 1;
|
||
private bool blockSpawn;
|
||
private bool myDebug;
|
||
private readonly float errorMinDaysB4Next = 0.001f;
|
||
private readonly int errorExponentialLimit = 20;
|
||
private readonly int errorSpawnCount = 750;
|
||
}
|
||
}
|