This commit is contained in:
2025-09-17 12:54:07 +08:00
parent 9e992fc08b
commit c77def63c2
123 changed files with 130 additions and 130 deletions

View File

@@ -0,0 +1,31 @@
using Verse;
using RimWorld;
namespace ArachnaeSwarm
{
/// <summary>
/// XML配置类用于在HediffDef的comp中指定转化后的MutantDef
/// </summary>
public class HediffCompProperties_NecroticTransformation : HediffCompProperties
{
// 在XML中需要指定的MutantDef的defName
public MutantDef mutantDef;
// 在XML中配置触发转变所需的严重性阈值
public float triggerSeverity = 0.7f;
public HediffCompProperties_NecroticTransformation()
{
compClass = typeof(HediffComp_NecroticTransformation);
}
}
/// <summary>
/// HediffComp用于在运行时存储和提供由XML定义的MutantDef
/// </summary>
public class HediffComp_NecroticTransformation : HediffComp
{
// 属性用于方便地从Comp中获取配置
public HediffCompProperties_NecroticTransformation Props => (HediffCompProperties_NecroticTransformation)props;
}
}

View File

@@ -0,0 +1,61 @@
using Verse;
using RimWorld;
namespace ArachnaeSwarm
{
/// <summary>
/// XML配置类定义共生关系
/// </summary>
public class HediffCompProperties_Symbiosis : HediffCompProperties
{
// 实现共存所需的Hediff
public HediffDef requiredHediff;
// 共存状态下的最大严重性
public float newMaxSeverity = 0.9f;
public HediffCompProperties_Symbiosis()
{
compClass = typeof(HediffComp_Symbiosis);
}
}
/// <summary>
/// HediffComp实现共生逻辑
/// </summary>
public class HediffComp_Symbiosis : HediffComp
{
private HediffCompProperties_Symbiosis Props => (HediffCompProperties_Symbiosis)props;
// 重写CompPostTick它会在游戏每一帧在计算完严重性增量之后应用增量之前被调用
public override void CompPostTick(ref float severityAdjustment)
{
base.CompPostTick(ref severityAdjustment);
// 检查宿主Pawn是否存在以及配置是否完整
if (this.Pawn == null || Props.requiredHediff == null)
{
return;
}
// 检查Pawn是否拥有“共存”所需的Hediff
if (this.Pawn.health.hediffSet.HasHediff(Props.requiredHediff))
{
// 如果当前严重性已经达到或超过上限
if (this.parent.Severity >= Props.newMaxSeverity)
{
// 将当前严重性强制拉回到上限
this.parent.Severity = Props.newMaxSeverity;
// 并且阻止任何将要发生的严重性增加将增量设为0
severityAdjustment = 0;
}
// 如果当前严重性加上即将发生的增量会超过上限
else if (this.parent.Severity + severityAdjustment > Props.newMaxSeverity)
{
// 重新计算增量,使其恰好达到上限
severityAdjustment = Props.newMaxSeverity - this.parent.Severity;
}
}
}
}
}

View File

@@ -0,0 +1,373 @@
using System.Collections.Generic;
using RimWorld;
using UnityEngine;
using Verse;
using Verse.AI;
using Verse.AI.Group;
using Verse.Sound;
namespace ArachnaeSwarm
{
public class HediffCompProperties_ConfigurableMutant : HediffCompProperties
{
// --- 可配置的Hediff ---
public HediffDef risingHediff;
public HediffDef corpseHediff;
// --- 可配置的时间和范围 ---
public FloatRange resurrectionSecondsRange = new FloatRange(5f, 10f);
public FloatRange stunSecondsRange = new FloatRange(1f, 3f);
public FloatRange alertSecondsRange = new FloatRange(0.5f, 3f);
public IntRange selfRaiseHoursRange = new IntRange(3, 4);
public IntRange checkForTargetTicksInterval = new IntRange(900, 1800);
// --- 可配置的杂项值 ---
public float particleSpawnMTBSeconds = 1f;
public float extinguishFireMTB = 45f;
public float bioferriteOnDeathChance = 0.04f;
public int bioferriteAmountOnDeath = 10;
public HediffCompProperties_ConfigurableMutant()
{
this.compClass = typeof(HediffComp_ConfigurableMutant);
}
}
public class HediffComp_ConfigurableMutant : HediffComp
{
public HediffCompProperties_ConfigurableMutant Props => (HediffCompProperties_ConfigurableMutant)this.props;
}
public class Hediff_ConfigurableMutant : HediffWithComps
{
// 运行时状态字段,保持不变
public float headRotation;
private float resurrectTimer;
private float selfRaiseTimer;
private float alertTimer;
private int nextTargetCheckTick = -99999;
private Thing alertedTarget;
private Effecter riseEffecter;
private Sustainer riseSustainer;
private float corpseDamagePct = 1f;
private HediffComp_ConfigurableMutant PropsComp => this.TryGetComp<HediffComp_ConfigurableMutant>();
public bool IsRising => PropsComp?.Props.risingHediff != null && pawn.health.hediffSet.HasHediff(PropsComp.Props.risingHediff);
public override void PostMake()
{
base.PostMake();
headRotation = Rand.RangeSeeded(-20f, 20f, pawn.thingIDNumber);
if (!pawn.Dead)
{
pawn.timesRaisedAsShambler++;
}
}
public override void PostAdd(DamageInfo? dinfo)
{
if (!ModLister.CheckAnomaly("Shambler"))
{
pawn.health.RemoveHediff(this);
}
else
{
base.PostAdd(dinfo);
}
}
public override void Notify_Spawned()
{
base.Notify_Spawned();
pawn.Map.mapPawns.RegisterShambler(pawn);
}
public override void TickInterval(int delta)
{
base.TickInterval(delta);
var compProps = PropsComp?.Props;
if (compProps == null) return; // 如果没有Comp则不执行任何操作
if (IsRising)
{
if ((float)Find.TickManager.TicksGame > resurrectTimer)
{
FinishRising();
}
if (!pawn.Spawned) return;
if ((float)Find.TickManager.TicksGame > resurrectTimer - 15f)
{
riseSustainer?.End();
}
else if (IsRising)
{
if (riseSustainer == null || riseSustainer.Ended)
{
riseSustainer = SoundDefOf.Pawn_Shambler_Rise.TrySpawnSustainer(SoundInfo.InMap(pawn, MaintenanceType.PerTick));
}
if (riseEffecter == null)
{
riseEffecter = EffecterDefOf.ShamblerRaise.Spawn(pawn, pawn.Map);
}
if (pawn.Drawer.renderer.CurAnimation != AnimationDefOf.ShamblerRise)
{
pawn.Drawer.renderer.SetAnimation(AnimationDefOf.ShamblerRise);
}
riseSustainer.Maintain();
riseEffecter.EffectTick(pawn, TargetInfo.Invalid);
}
return;
}
if (Rand.MTBEventOccurs(compProps.particleSpawnMTBSeconds, 60f, 1f))
{
FleckMaker.ThrowShamblerParticles(pawn);
}
if (pawn.IsBurning() && !pawn.Downed && Rand.MTBEventOccurs(compProps.extinguishFireMTB, 60f, 1f))
{
((Fire)pawn.GetAttachment(ThingDefOf.Fire))?.Destroy();
pawn.records.Increment(RecordDefOf.FiresExtinguished);
}
if (pawn.Spawned && pawn.mutant != null && !pawn.mutant.IsPassive && !pawn.Drafted)
{
if (Find.TickManager.TicksGame > nextTargetCheckTick)
{
nextTargetCheckTick = Find.TickManager.TicksGame + compProps.checkForTargetTicksInterval.RandomInRange;
Thing thing = MutantUtility.FindShamblerTarget(pawn);
if (thing != null)
{
Notify_DelayedAlert(thing);
MutantUtility.ActivateNearbyShamblers(pawn, thing);
}
}
if (alertedTarget != null && (float)Find.TickManager.TicksGame > alertTimer)
{
pawn.mindState.enemyTarget = alertedTarget;
pawn.mindState.lastEngageTargetTick = Find.TickManager.TicksGame; // 直接给public字段赋值绕过internal方法
alertedTarget = null;
if (DebugViewSettings.drawShamblerAlertMote)
{
MoteMaker.MakeColonistActionOverlay(pawn, ThingDefOf.Mote_ShamblerAlert);
}
SoundDefOf.Pawn_Shambler_Alert.PlayOneShot(pawn);
}
}
if (ShouldSelfRaise())
{
StartRising();
}
}
private bool ShouldSelfRaise()
{
if (pawn.DevelopmentalStage == DevelopmentalStage.Baby) return false;
return pawn.Downed && pawn.CarriedBy == null && (float)Find.TickManager.TicksGame > selfRaiseTimer;
}
public void StartRising(int lifespanTicks = -1)
{
var compProps = PropsComp?.Props;
if (compProps?.risingHediff == null)
{
Log.Error($"[ConfigurableMutant] risingHediff is not defined in XML for {this.def.defName}");
return;
}
if (!pawn.Dead && !pawn.Downed)
{
Log.Error("Tried to raise non dead/downed pawn as shambler");
if(pawn.mutant != null) pawn.mutant.Turn(clearLord: true);
return;
}
MutantUtility.RestoreBodyParts(pawn);
pawn.Notify_DisabledWorkTypesChanged();
if (!pawn.Dead || ResurrectionUtility.TryResurrect(pawn, new ResurrectionParams { noLord = true, restoreMissingParts = false, removeDiedThoughts = false }))
{
pawn.jobs?.EndCurrentJob(JobCondition.InterruptForced);
resurrectTimer = Find.TickManager.TicksGame + compProps.resurrectionSecondsRange.RandomInRange.SecondsToTicks();
pawn.health.AddHediff(compProps.risingHediff);
}
}
private void CancelRising()
{
var risingHediff = PropsComp?.Props.risingHediff;
if (risingHediff == null) return;
riseSustainer?.End();
resurrectTimer = -99999f;
if (pawn.health.hediffSet.TryGetHediff(risingHediff, out var hediff))
{
pawn.health.RemoveHediff(hediff);
}
if (!pawn.Dead)
{
pawn.Kill(null, null);
}
}
private void FinishRising(bool stun = true)
{
var compProps = PropsComp?.Props;
if (compProps?.risingHediff != null && pawn.health.hediffSet.TryGetHediff(compProps.risingHediff, out var hediff))
{
pawn.health.RemoveHediff(hediff);
}
if (pawn.ParentHolder is Pawn_CarryTracker pawn_CarryTracker)
{
pawn_CarryTracker.TryDropCarriedThing(pawn_CarryTracker.pawn.Position, ThingPlaceMode.Near, out var _);
pawn_CarryTracker.pawn.jobs.EndCurrentJob(JobCondition.InterruptForced);
}
if (pawn.mutant != null && !pawn.mutant.HasTurned)
{
pawn.mutant.Turn();
}
pawn.timesRaisedAsShambler++;
MutantUtility.RestoreUntilNotDowned(pawn);
if (pawn.Spawned && stun)
{
pawn.Rotation = Rot4.South;
if(compProps != null)
pawn.stances.stunner.StunFor(compProps.stunSecondsRange.RandomInRange.SecondsToTicks(), pawn, addBattleLog: false, showMote: false);
}
pawn.Drawer.renderer.SetAnimation(null);
StartSelfRaiseTimer();
}
private void StartSelfRaiseTimer()
{
var selfRaiseHoursRange = PropsComp?.Props.selfRaiseHoursRange ?? new IntRange(3, 4);
selfRaiseTimer = Find.TickManager.TicksGame + 2500 * selfRaiseHoursRange.RandomInRange;
}
public override void Notify_PawnPostApplyDamage(DamageInfo dinfo, float totalDamageDealt)
{
base.Notify_PawnPostApplyDamage(dinfo, totalDamageDealt);
if (dinfo.Instigator != null && pawn.HostileTo(dinfo.Instigator))
{
if (pawn.Spawned && dinfo.Instigator is IAttackTarget && dinfo.Instigator.Spawned && pawn.CanSee(dinfo.Instigator))
{
pawn.mindState.enemyTarget = dinfo.Instigator;
pawn.mindState.lastEngageTargetTick = Find.TickManager.TicksGame; // 直接给public字段赋值绕过internal方法
pawn.jobs.EndCurrentJob(JobCondition.InterruptOptional);
pawn.GetLord()?.Notify_PawnAcquiredTarget(pawn, dinfo.Instigator);
}
MutantUtility.ActivateNearbyShamblers(pawn, dinfo.Instigator);
}
}
public void Notify_DelayedAlert(Thing target)
{
var alertSecondsRange = PropsComp?.Props.alertSecondsRange ?? new FloatRange(0.5f, 3f);
if (pawn.mutant != null && !pawn.mutant.IsPassive && !pawn.Drafted && pawn.mindState.enemyTarget == null)
{
alertTimer = Find.TickManager.TicksGame + alertSecondsRange.RandomInRange.SecondsToTicks();
alertedTarget = target;
pawn.GetLord()?.Notify_PawnAcquiredTarget(pawn, target);
}
}
public override void Notify_Downed()
{
StartSelfRaiseTimer();
}
public override void Notify_PawnKilled()
{
corpseDamagePct = pawn.health.summaryHealth.SummaryHealthPercent;
base.Notify_PawnKilled();
}
public override void Notify_PawnDied(DamageInfo? dinfo, Hediff culprit = null)
{
if (IsRising)
{
CancelRising();
}
var compProps = PropsComp?.Props;
if (compProps != null && pawn.timesRaisedAsShambler == 1 && Rand.Chance(compProps.bioferriteOnDeathChance) && pawn.SpawnedOrAnyParentSpawned)
{
Thing thing = ThingMaker.MakeThing(ThingDefOf.Bioferrite);
thing.stackCount = compProps.bioferriteAmountOnDeath;
GenPlace.TryPlaceThing(thing, pawn.PositionHeld, pawn.MapHeld, ThingPlaceMode.Near, out var lastResultingThing);
lastResultingThing.SetForbidden(value: true);
}
if (pawn.Corpse != null)
{
pawn.Corpse.HitPoints = Mathf.Max(Mathf.RoundToInt((float)pawn.Corpse.MaxHitPoints * corpseDamagePct), 10);
}
if (compProps?.corpseHediff != null)
{
pawn.health.AddHediff(compProps.corpseHediff);
}
base.Notify_PawnDied(dinfo, culprit);
}
public override void PreRemoved()
{
pawn.MapHeld?.mapPawns.DeregisterShambler(pawn);
base.PreRemoved();
}
public override void PostRemoved()
{
base.PostRemoved();
if (pawn.Dead) return;
if (IsRising) CancelRising();
if (pawn.IsMutant)
{
if (pawn.mutant.HasTurned) pawn.mutant.Revert();
else pawn.mutant = null;
}
}
public override IEnumerable<Gizmo> GetGizmos()
{
foreach (Gizmo gizmo in base.GetGizmos())
{
yield return gizmo;
}
if (DebugSettings.ShowDevGizmos && pawn.Downed && !IsRising)
{
var command_Action = new Command_Action
{
defaultLabel = "Self Raise",
action = () => StartRising()
};
yield return command_Action;
}
}
public override string GetInspectString()
{
if (ShouldSelfRaise()) return "ShamblerRegenerating".Translate();
if (IsRising) return "ShamblerRising".Translate();
if (pawn.CurJobDef == JobDefOf.Wait_Wander || pawn.CurJobDef == JobDefOf.Wait_MaintainPosture) return "ShamblerStanding".Translate();
if (pawn.CurJobDef == JobDefOf.GotoWander || pawn.CurJobDef == JobDefOf.Goto) return "ShamblerShuffling".Translate();
return "";
}
public override void ExposeData()
{
base.ExposeData();
Scribe_Values.Look(ref alertTimer, "alertTimer", 0f);
Scribe_Values.Look(ref nextTargetCheckTick, "nextTargetCheckTick", 0);
Scribe_References.Look(ref alertedTarget, "alertedTarget");
Scribe_Values.Look(ref resurrectTimer, "resurrectTimer", 0f);
Scribe_Values.Look(ref selfRaiseTimer, "selfRaiseTimer", 0f);
Scribe_Values.Look(ref headRotation, "headRotation", 0f);
}
}
}

View File

@@ -0,0 +1,147 @@
using RimWorld;
using Verse;
using UnityEngine;
using System.Linq;
namespace ArachnaeSwarm
{
public class Hediff_NecroticVirus : HediffWithComps
{
private const int EffectInterval = 600;
// 用于存储攻击者派系的字段
private Faction casterFaction;
// 属性来获取我们的自定义Comp
private HediffComp_NecroticTransformation PropsComp => this.TryGetComp<HediffComp_NecroticTransformation>();
public override void PostAdd(DamageInfo? dinfo)
{
base.PostAdd(dinfo);
if (dinfo.HasValue && dinfo.Value.Instigator != null)
{
this.casterFaction = dinfo.Value.Instigator.Faction;
}
}
public override void Notify_PawnDied(DamageInfo? dinfo, Hediff culprit = null)
{
base.Notify_PawnDied(dinfo, culprit);
TransformToMutant();
}
public override void PostTick()
{
base.PostTick();
if (pawn.IsHashIntervalTick(EffectInterval) && pawn.Spawned)
{
ShowVirusEffects();
}
}
private void TransformToMutant()
{
var comp = PropsComp;
if (comp == null || comp.Props.mutantDef == null)
{
Log.Error($"[NecroticVirus] HediffComp_NecroticTransformation or its mutantDef is not configured in XML for {this.def.defName}.");
return;
}
// 检查严重性是否达到XML中配置的阈值
if (this.Severity < comp.Props.triggerSeverity)
{
return;
}
try
{
if (pawn.Corpse == null || pawn.Corpse.Destroyed)
{
return;
}
Map map = pawn.Corpse.Map;
IntVec3 position = pawn.Corpse.Position;
// 使用我们自己的、更安全的检查方法
if (!NecroticTransformationUtility.CanResurrect(pawn.Corpse))
{
Log.Warning($"[NecroticVirus] Pawn {pawn.LabelShort} does not meet conditions for resurrection.");
return;
}
// **优先使用攻击者的派系,如果没有,则执行备用逻辑**
Faction faction = this.casterFaction ?? GetHostileFaction();
// **调用我们自己的工具方法传入从XML获取的mutantDef**
NecroticTransformationUtility.ResurrectAsCustomMutant(pawn, comp.Props.mutantDef, faction);
// **关键修复在成功转化后立即移除导致转化的Hediff本身防止其残留**
pawn.health.RemoveHediff(this);
// 添加转化特效
FleckMaker.ThrowSmoke(position.ToVector3Shifted(), map, 1.5f);
FleckMaker.ThrowDustPuff(position.ToVector3Shifted(), map, 1.2f);
// 发送转化消息
if (PawnUtility.ShouldSendNotificationAbout(pawn))
{
Messages.Message(
"NecroticVirus_TransformationMessage".Translate(pawn.LabelShortCap, pawn.LabelShortCap),
new LookTargets(position, map),
MessageTypeDefOf.NegativeEvent
);
}
}
catch (System.Exception ex)
{
Log.Error($"[NecroticVirus] Error during transformation: {ex}");
}
}
private Faction GetHostileFaction()
{
if (pawn.Faction != null && pawn.Faction.HostileTo(Faction.OfPlayer))
{
return pawn.Faction;
}
Faction entitiesFaction = Find.FactionManager.AllFactions.FirstOrDefault(f => f.def.defName == "Entities");
if (entitiesFaction != null && !entitiesFaction.defeated)
{
return entitiesFaction;
}
return Find.FactionManager.RandomEnemyFaction(allowNonHumanlike: true, allowHidden: false, allowDefeated: false);
}
private void ShowVirusEffects()
{
if (pawn == null || !pawn.Spawned) return;
Vector3 pos = pawn.DrawPos;
Map map = pawn.Map;
float sizeMultiplier = 1f;
if (CurStageIndex == 1) sizeMultiplier = 1.2f;
if (CurStageIndex == 2) sizeMultiplier = 1.5f;
FleckMaker.ThrowMicroSparks(pos, map);
FleckMaker.ThrowDustPuff(pos, map, 0.8f * sizeMultiplier);
}
public override string TipStringExtra
{
get
{
return base.TipStringExtra + "\n" + "NecroticVirus_EffectTip".Translate();
}
}
public override void ExposeData()
{
base.ExposeData();
Scribe_References.Look(ref this.casterFaction, "casterFaction");
}
}
}

View File

@@ -0,0 +1,83 @@
using RimWorld;
using Verse;
namespace ArachnaeSwarm
{
public static class NecroticTransformationUtility
{
/// <summary>
/// 检查一个尸体是否可以被我们的逻辑转化为变异体。
/// 这是对原版 MutantUtility.CanResurrectAsShambler 的复制,但移除了对 canBecomeShambler 的检查。
/// </summary>
public static bool CanResurrect(Corpse corpse, bool ignoreIndoors = false)
{
// 只保留最核心、最必要的检查,移除所有可能导致不稳定的条件
if (corpse?.InnerPawn == null) return false;
if (!corpse.InnerPawn.RaceProps.IsFlesh) return false;
return true;
}
/// <summary>
/// 将一个Pawn复活为指定的自定义变异体.
/// 这个方法是模仿原版 MutantUtility.ResurrectAsShambler,
/// 但允许传入一个自定义的 MutantDef.
/// </summary>
public static void ResurrectAsCustomMutant(Pawn pawn, MutantDef mutantDef, Faction faction = null, int lifespanTicks = -1)
{
if (pawn?.Corpse == null || mutantDef == null)
{
return;
}
RotStage rotStage = pawn.Corpse.GetRotStage();
// 创建并附加Pawn_MutantTracker使用我们从XML传入的mutantDef
pawn.mutant = new Pawn_MutantTracker(pawn, mutantDef, rotStage);
// **调用原版健康再生方法此方法会读取MutantDef中的配置如removeAllInjuries并执行**
MutantUtility.RegenerateHealth(pawn);
// 添加变异体核心Hediff
Hediff hediff = pawn.health.AddHediff(mutantDef.hediff);
// 如果是我们自己的可配置变异体Hediff则调用其上升动画
if (hediff is Hediff_ConfigurableMutant configurableMutant)
{
configurableMutant.StartRising(lifespanTicks);
}
// 设置生命周期
var disappearsComp = hediff.TryGetComp<HediffComp_DisappearsAndKills>();
if (disappearsComp != null)
{
if (lifespanTicks > 0)
{
disappearsComp.disappearsAfterTicks = lifespanTicks;
disappearsComp.ticksToDisappear = lifespanTicks;
}
else
{
disappearsComp.disabled = true;
}
}
// 如果没有提供派系,则尝试使用变异体定义的默认派系
if (faction == null && mutantDef.defaultFaction != null)
{
faction = Find.FactionManager.FirstFactionOfDef(mutantDef.defaultFaction);
}
// 设置派系
if (faction != null && pawn.Faction != faction)
{
pawn.SetFaction(faction);
}
// 移除 pawn.mutant.Turn(clearLord: true);
// Turn() 方法应该由 Hediff_Shambler.FinishRising() 在复活过程的最后阶段调用,
// 而不应该由我们在这里手动调用。
// Hediff_Shambler 的 StartRising() 会启动这个流程。
}
}
}