diff --git a/1.6/1.6/Assemblies/ArachnaeSwarm.dll b/1.6/1.6/Assemblies/ArachnaeSwarm.dll index 84e0e9c..5157529b 100644 Binary files a/1.6/1.6/Assemblies/ArachnaeSwarm.dll and b/1.6/1.6/Assemblies/ArachnaeSwarm.dll differ diff --git a/1.6/1.6/Defs/HediffDefs/ARA_Hediffs_Possession.xml b/1.6/1.6/Defs/HediffDefs/ARA_Hediffs_Possession.xml new file mode 100644 index 0000000..095afb3 --- /dev/null +++ b/1.6/1.6/Defs/HediffDefs/ARA_Hediffs_Possession.xml @@ -0,0 +1,19 @@ + + + + + ARA_Possession + + 这个生物的身体正被另一个实体所控制。 + ArachnaeSwarm.Possession.Hediff_Possession + false + false + 1.0 + +
  • + +
  • +
    +
    + +
    \ No newline at end of file diff --git a/1.6/1.6/Defs/Misc/ARA_Possession_Defs.xml b/1.6/1.6/Defs/Misc/ARA_Possession_Defs.xml new file mode 100644 index 0000000..40efc0a --- /dev/null +++ b/1.6/1.6/Defs/Misc/ARA_Possession_Defs.xml @@ -0,0 +1,166 @@ + + + + + + + ARA_Ability_Possess + + 将你的意识注入另一个生物的身体,完全占据它。 + UI/Abilities/Possess + 600 + + Verb_CastAbility + 1.5 + 5.9 + + true + false + false + false + + + +
  • + + + + + ARA_Facehugger + Humanlike_PostMentalState + 100 + + + + +
  • + Possess + +
  • + + + + + + + + ARA_Facehugger + + ARA_FacehuggerRace + 25 + +
  • + + Things/Pawn/Animal/ARA_Facehugger + 0.8 + + + Things/Pawn/Animal/Dessicated/CritterDessicated + 0.8 + +
  • +
    + ARA_Facehugger + +
  • ARA_Ability_Possess
  • +
    +
    + + + + + ARA_FacehuggerRace + + 一种小型的、脆弱的寄生生物,其唯一的生存目的就是寻找并占据一个更强大的宿主。它通过将自己的意识注入目标来完成这一过程。 + + 4.0 + 50 + -10 + 50 + + +
  • + + +
  • Scratch
  • + + 2 + 1.5 + +
    + + Animal + ARA_FacehuggerBody + 0.2 + 0.3 + 0.1 + +
  • + AnimalAdult + 0 +
  • +
    +
    +
    + + + ARA_FacehuggerBody + + + Body + 20 + 20 + +
  • + Head + 0.3 + +
  • + Skull + 0.2 + Inside + +
  • + Brain + 0.1 + Inside +
  • +
    + +
  • + Eye + left eye + 0.07 +
  • +
  • + Eye + right eye + 0.07 +
  • + + +
  • + Leg + front left leg + 0.1 +
  • +
  • + Leg + front right leg + 0.1 +
  • +
  • + Leg + rear left leg + 0.1 +
  • +
  • + Leg + rear right leg + 0.1 +
  • + +
    +
    + +
    \ No newline at end of file diff --git a/Source/ArachnaeSwarm/ArachnaeSwarm.csproj b/Source/ArachnaeSwarm/ArachnaeSwarm.csproj index fa02310..39029b2 100644 --- a/Source/ArachnaeSwarm/ArachnaeSwarm.csproj +++ b/Source/ArachnaeSwarm/ArachnaeSwarm.csproj @@ -112,6 +112,12 @@ + + + + + + diff --git a/Source/ArachnaeSwarm/Possession/CompAbilityEffect_Possess.cs b/Source/ArachnaeSwarm/Possession/CompAbilityEffect_Possess.cs new file mode 100644 index 0000000..f0e2b8b --- /dev/null +++ b/Source/ArachnaeSwarm/Possession/CompAbilityEffect_Possess.cs @@ -0,0 +1,33 @@ +using RimWorld; +using Verse; + +namespace ArachnaeSwarm.Possession +{ + public class CompAbilityEffect_Possess : CompAbilityEffect + { + public override void Apply(LocalTargetInfo target, LocalTargetInfo dest) + { + base.Apply(target, dest); + + Pawn caster = this.parent.pawn; + Pawn targetPawn = target.Pawn; + + if (targetPawn == null) + { + return; + } + + // Optional: Add checks here. E.g., cannot possess mechanical, already possessed, etc. + // if (targetPawn.RaceProps.IsMechanoid) + // { + // Messages.Message("Cannot possess a mechanoid.", MessageTypeDefOf.RejectInput); + // return; + // } + + Hediff_Possession hediff = (Hediff_Possession)HediffMaker.MakeHediff(HediffDef.Named("ARA_Possession"), targetPawn); + hediff.SetCaster(caster); + + targetPawn.health.AddHediff(hediff); + } + } +} \ No newline at end of file diff --git a/Source/ArachnaeSwarm/Possession/CompProperties_AbilityPossess.cs b/Source/ArachnaeSwarm/Possession/CompProperties_AbilityPossess.cs new file mode 100644 index 0000000..7cea6a7 --- /dev/null +++ b/Source/ArachnaeSwarm/Possession/CompProperties_AbilityPossess.cs @@ -0,0 +1,12 @@ +using RimWorld; + +namespace ArachnaeSwarm.Possession +{ + public class CompProperties_AbilityPossess : CompProperties_AbilityEffect + { + public CompProperties_AbilityPossess() + { + this.compClass = typeof(CompAbilityEffect_Possess); + } + } +} \ No newline at end of file diff --git a/Source/ArachnaeSwarm/Possession/Hediff_Possession.cs b/Source/ArachnaeSwarm/Possession/Hediff_Possession.cs new file mode 100644 index 0000000..7054207 --- /dev/null +++ b/Source/ArachnaeSwarm/Possession/Hediff_Possession.cs @@ -0,0 +1,90 @@ +using System.Collections.Generic; +using RimWorld; +using Verse; + +namespace ArachnaeSwarm.Possession +{ + public class Hediff_Possession : HediffWithComps, IThingHolder + { + private ThingOwner innerContainer; + private Pawn originalCaster; + + public Hediff_Possession() + { + this.innerContainer = new ThingOwner(this, false, LookMode.Deep); + } + + public Pawn StoredCasterPawn => innerContainer.Count > 0 ? innerContainer[0] as Pawn : null; + + public IThingHolder ParentHolder => this.pawn; + + public void GetChildHolders(List outChildren) + { + ThingOwnerUtility.AppendThingHoldersFromThings(outChildren, this.GetDirectlyHeldThings()); + } + + public ThingOwner GetDirectlyHeldThings() + { + return innerContainer; + } + + public override void PostAdd(DamageInfo? dinfo) + { + base.PostAdd(dinfo); + + if (this.originalCaster == null) + { + Log.Error("Hediff_Possession was added without an original caster."); + return; + } + + this.innerContainer.TryAdd(this.originalCaster); + PawnDataUtility.TransferSoul(this.originalCaster, this.pawn); + + if (!this.originalCaster.Destroyed) + { + this.originalCaster.Destroy(DestroyMode.Vanish); + } + + Log.Message($"{this.pawn.LabelShort} has been possessed by {StoredCasterPawn.LabelShort}!"); + } + + public override void Notify_PawnDied(DamageInfo? dinfo, Hediff culprit = null) + { + base.Notify_PawnDied(dinfo, culprit); + + Pawn deadBody = this.pawn; + Pawn storedCaster = this.StoredCasterPawn; + + if (storedCaster == null) + { + Log.Error("Possessed pawn died, but no caster soul was found inside."); + return; + } + + Log.Message($"Host {deadBody.LabelShort} died. Transferring experience back to {storedCaster.LabelShort} and ejecting."); + PawnDataUtility.TransferSoul(deadBody, storedCaster); + this.EjectContents(); + } + + public void SetCaster(Pawn caster) + { + this.originalCaster = caster; + } + + public void EjectContents() + { + if (StoredCasterPawn != null) + { + this.innerContainer.TryDropAll(this.pawn.Position, this.pawn.Map, ThingPlaceMode.Near); + } + } + + public override void ExposeData() + { + base.ExposeData(); + Scribe_Deep.Look(ref innerContainer, "innerContainer", this); + Scribe_References.Look(ref originalCaster, "originalCaster"); + } + } +} \ No newline at end of file diff --git a/Source/ArachnaeSwarm/Possession/PawnDataUtility.cs b/Source/ArachnaeSwarm/Possession/PawnDataUtility.cs new file mode 100644 index 0000000..6b78f42 --- /dev/null +++ b/Source/ArachnaeSwarm/Possession/PawnDataUtility.cs @@ -0,0 +1,106 @@ +using System.Collections.Generic; +using RimWorld; +using Verse; + +namespace ArachnaeSwarm.Possession +{ + public static class PawnDataUtility + { + public static void TransferSoul(Pawn soulSource, Pawn bodyTarget) + { + if (soulSource == null || bodyTarget == null) + { + Log.Error("Cannot transfer soul: source or target is null."); + return; + } + + Log.Message($"Beginning soul transfer from {soulSource.LabelShort} to {bodyTarget.LabelShort}."); + + // Name + bodyTarget.Name = soulSource.Name; + + // Story (Backstory and Traits) + bodyTarget.story.Childhood = soulSource.story.Childhood; + bodyTarget.story.Adulthood = soulSource.story.Adulthood; + bodyTarget.story.traits.allTraits.Clear(); + foreach (Trait trait in soulSource.story.traits.allTraits) + { + bodyTarget.story.traits.GainTrait(trait); + } + + // Skills + bodyTarget.skills.skills.Clear(); + foreach (SkillRecord skill in soulSource.skills.skills) + { + SkillRecord newSkill = new SkillRecord(bodyTarget, skill.def) + { + levelInt = skill.levelInt, + xpSinceLastLevel = skill.xpSinceLastLevel, + passion = skill.passion + }; + bodyTarget.skills.skills.Add(newSkill); + } + + // Faction + if (bodyTarget.Faction != soulSource.Faction) + { + bodyTarget.SetFaction(soulSource.Faction, soulSource); + } + + // Thoughts and Memories + if (bodyTarget.needs.mood?.thoughts?.memories != null) + { + bodyTarget.needs.mood.thoughts.memories.Memories.Clear(); + } + if (soulSource.needs.mood?.thoughts?.memories != null) + { + foreach (Thought_Memory memory in soulSource.needs.mood.thoughts.memories.Memories) + { + bodyTarget.needs.mood.thoughts.memories.TryGainMemory(memory); + } + } + + // Work Settings + if (soulSource.workSettings != null && bodyTarget.workSettings != null) + { + bodyTarget.workSettings.EnableAndInitialize(); + foreach (WorkTypeDef workDef in DefDatabase.AllDefs) + { + bodyTarget.workSettings.SetPriority(workDef, soulSource.workSettings.GetPriority(workDef)); + } + } + + // Timetable + if (soulSource.timetable != null && bodyTarget.timetable != null) + { + bodyTarget.timetable.times = new List(soulSource.timetable.times); + } + + // Social Relations + if (soulSource.relations != null && bodyTarget.relations != null) + { + bodyTarget.relations.ClearAllRelations(); + foreach (DirectPawnRelation relation in soulSource.relations.DirectRelations) + { + bodyTarget.relations.AddDirectRelation(relation.def, relation.otherPawn); + } + } + + // Guest status + if (soulSource.guest != null && bodyTarget.guest != null) + { + bodyTarget.guest.SetGuestStatus(soulSource.guest.HostFaction, soulSource.guest.GuestStatus); + if (soulSource.guest.IsPrisoner) + { + bodyTarget.guest.SetExclusiveInteraction(soulSource.guest.ExclusiveInteractionMode); + } + bodyTarget.guest.joinStatus = soulSource.guest.joinStatus; + } + + // Refresh the UI and game state to reflect the changes. + bodyTarget.Drawer.renderer.SetAllGraphicsDirty(); + + Log.Message("Soul transfer complete."); + } + } +} \ No newline at end of file diff --git a/Source/Documents/Human.xml b/Source/Documents/Human.xml new file mode 100644 index 0000000..2879174 --- /dev/null +++ b/Source/Documents/Human.xml @@ -0,0 +1,787 @@ + + Human + 2 + Human846 + 0 + (146, 0, 131) + 1 + Faction_18 + + 0 + -1 + True + null + -9999999 + + + + + +
  • base
  • +
  • hair
  • +
  • skin
  • +
  • skinBase
  • +
  • tattoo
  • +
  • favorite
  • +
  • ideo
  • +
  • mech
  • +
    + +
  • + RGBA(1.000, 1.000, 1.000, 1.000) + RGBA(1.000, 1.000, 1.000, 1.000) +
  • +
  • + RGBA(0.343, 0.310, 0.288, 1.000) +
  • +
  • + RGBA(1.000, 0.937, 0.788, 1.000) +
  • +
  • + RGBA(1.000, 0.937, 0.788, 1.000) +
  • +
  • + RGBA(1.000, 0.937, 0.788, 0.800) +
  • +
  • + RGBA(0.890, 0.451, 1.000, 1.000) + RGBA(0.890, 0.451, 1.000, 1.000) +
  • +
  • + RGBA(0.600, 0.500, 0.900, 1.000) + RGBA(0.549, 0.458, 0.824, 1.000) +
  • +
  • + RGBA(0.000, 0.737, 0.847, 1.000) +
  • +
    +
    + + + + + 0 + 0 + 0 + 0 +
    + Colonist + Female + + Yue + Moon + Ren + + null + + null + null + null + null + null + (0, 0, 0) + + + + + Idle + 6673 + -99999 + -99999 + True + -99999 + + + + + + + + + + + (-1000, -1000, -1000) + + -99999 + + + + + + + + + + + 0 + 16777 + + + + null + null + null + null + null + GotoWander + 221 + (156, 0, 116) + + + + 1576 + True + + Walk + Humanlike + -1 + null + null + -1 + -1672709817 + + + 0 + -203 + 1576 + null + + + + + -1 + + + + True + + + + + + + + + + True + + + + + + + +
  • + Thing_Human846_0_Smash + (0, 0, 0) + (0, 0, 0) + -999999 + True +
  • +
  • + Thing_Human846_1_Smash + (0, 0, 0) + (0, 0, 0) + -999999 + True +
  • +
  • + Thing_Human846_2_Bite + (0, 0, 0) + (0, 0, 0) + -999999 + True +
  • +
  • + Thing_Human846_3_Smash + (0, 0, 0) + (0, 0, 0) + -999999 + True +
  • +
    +
    + + + + + + + null + + + + + (147, 0, 130) + 48 + 50 + OnCell + 4 + 1777 + 1779 + (156, 0, 116) + + + + 1 + + + + + + +
  • + Apparel_Pants + Apparel_Pants847 + 130 + 1 + Synthread + + -1 + Normal + null + True + +
  • +
  • + Apparel_Parka + Apparel_Parka848 + 235 + 1 + Synthread + + -1 + Normal + null + True + +
  • +
    +
    + + 111 +
    + + Fat + Elisabeth + RGBA(0.343, 0.310, 0.288, 1.000) + + +
  • + SpeedOffset + null + -1 + null +
  • +
  • + Jealous + null + null +
  • +
    +
    + Ren + LightPurple + Female_AveragePointy + WarRefugee51 + MedievalMinstrel95 +
    + + + + + null + + + + + + 139350193 + -297730916 + 1 + 9223372036854775807 + 141208416 + + + + + + + + + + + + + + + +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 1840
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
    +
    + null +
    + + + + + + + + + Filth_Sand + +
  • + Filth_Sand + Filth_Sand14506 + + -1 +
  • +
    +
    + + null + + + + +
  • + Mood + 0.586400032 + + + +
  • + CrashedTogether + null + Thing_Human849 + 1800 + 25 +
  • +
  • + CrashedTogether + null + Thing_Human852 + 1800 + 25 +
  • +
  • + NewColonyOptimism + null + null + 1800 +
  • +
  • + Chitchat + null + Thing_Human852 + 0.773389459 + 1650 + 0.510437071 +
  • + + + + + 1711 + 1711 + + +
  • + Food + 0.751999915 + 1711 +
  • +
  • + Rest + 0.879273593 +
  • +
  • + Joy + 0.513921499 + + +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • + + + + +
  • False
  • +
  • False
  • +
  • False
  • +
  • False
  • +
  • False
  • +
  • False
  • +
  • False
  • +
  • False
  • +
  • False
  • +
  • False
  • +
    +
    + +
  • + Beauty + 0.442400098 +
  • +
  • + Comfort + 0.47119987 +
  • +
  • + Outdoors + 1 +
  • +
  • + DrugDesire + 0.5 +
  • +
  • + RoomSize + 1 +
  • +
    +
    + + null + null + JoinAsColonist + MaintainOnly + NoInteraction + (-1000, -1000, -1000) + -1 + null + False + + + + + + + + + + + + + + + + + + + + + + + + +
  • + ExSpouse + Thing_Human849 +
  • +
  • + Parent + Thing_Human853 +
  • +
  • + Parent + Thing_Human857 +
  • +
    + + null + + + + + -1 +
    + + True + + + + null + null + null + null + null + + + Chitchat + 121 + Chitchat + + + +
  • + Shooting + 3 +
  • +
  • + Melee + 5 +
  • +
  • + Construction + 2 +
  • +
  • + Mining + 2 +
  • +
  • + Cooking + 7 +
  • +
  • + Plants + 1 +
  • +
  • + Animals + 3 + Minor +
  • +
  • + Crafting + 3 +
  • +
  • + Artistic + 5 + Minor +
  • +
  • + Medicine + 2 + Minor +
  • +
  • + Social + 7 + Major +
  • +
  • + Intellectual +
  • +
    + -1 +
    + + + + + Ideo_10 + + 0.605378687 + + + + + +
  • 3
  • +
  • 3
  • +
  • 0
  • +
  • 3
  • +
  • 3
  • +
  • 3
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 0
  • +
  • 3
  • +
  • 0
  • +
  • 3
  • +
  • 0
  • +
  • 0
  • +
    +
    +
    + + + ApparelPolicy_任意_1 + + + + + + DrugPolicy_社交成瘾品_1 + + + + null + + + + +
  • Sleep
  • +
  • Sleep
  • +
  • Sleep
  • +
  • Sleep
  • +
  • Sleep
  • +
  • Sleep
  • +
  • Anything
  • +
  • Anything
  • +
  • Anything
  • +
  • Anything
  • +
  • Anything
  • +
  • Anything
  • +
  • Anything
  • +
  • Anything
  • +
  • Anything
  • +
  • Anything
  • +
  • Anything
  • +
  • Anything
  • +
  • Anything
  • +
  • Anything
  • +
  • Anything
  • +
  • Anything
  • +
  • Sleep
  • +
  • Sleep
  • +
    +
    + + Best + + + + + null + 1 + + + + + + + + + + + + + + + + + + + + + + + +
  • + Skin_Melanin3 + Thing_Human846 + null + 132 +
  • +
  • + Hair_MidBlack + Thing_Human846 + null + 133 +
  • +
    + Baseliner +
    + + + null + + + + +
    \ No newline at end of file diff --git a/Source/Documents/PawnStorageInHediff_Analysis.md b/Source/Documents/PawnStorageInHediff_Analysis.md new file mode 100644 index 0000000..62cdef5 --- /dev/null +++ b/Source/Documents/PawnStorageInHediff_Analysis.md @@ -0,0 +1,188 @@ +# RimWorld Modding: 利用Hediff存储Pawn的深度解析 + +在RimWorld的Mod开发中,有时需要将一个`Pawn`(人物、动物等)从游戏世界中临时移除,并将其数据完整保存起来,之后再释放回游戏中。一个非常精妙且强大的实现方式就是让`Hediff`(健康效果)扮演一个“容器”的角色。 + +本文档将以`HediffAbility_PaintedSkin`为例,深入剖析其实现`Pawn`存储的核心机制。 + +## 核心概念 + +该功能主要依赖于RimWorld框架中的两个核心组件: + +1. **`IThingHolder`接口**: 一个对象(如建筑、Hediff、Pawn的装备栏等)如果实现了这个接口,就等于向游戏声明:“我是一个可以容纳其他物品(`Thing`)的容器”。 +2. **`ThingOwner`类**: 这是实现存储功能的“袋子”。它是一个专门用于管理一组`Thing`对象的集合,并负责处理这些物品的保存、加载和所有权关系。 + +## 案例分析: `HediffAbility_PaintedSkin` + +以下是`HediffAbility_PaintedSkin`的完整源代码,它完美地展示了如何利用`Hediff`来存储一个`Pawn`。 + +```csharp +using System; +using System.Collections.Generic; +using RimWorld; +using Verse; +using Verse.AI; +using Verse.Sound; + +namespace RigorMortis +{ + public class HediffAbility_PaintedSkin : HediffWithComps, IHediffAbility, IThingHolder + { + // 1. 核心存储容器 + protected ThingOwner innerContainer; + + private CompYinAndMalevolent compYin; + public Pawn victim; + + // 构造函数:初始化容器 + public HediffAbility_PaintedSkin() + { + // 'this'表示容器的所有者是当前Hediff实例 + // 'LookMode.Deep'是关键,确保能完整保存Pawn的所有数据 + this.innerContainer = new ThingOwner(this, false, LookMode.Deep, true); + } + + // --- IThingHolder 接口实现 --- + + public IThingHolder ParentHolder + { + get + { + // 对于Hediff来说,它的父容器就是持有它的Pawn + return this.pawn; + } + } + + public void GetChildHolders(List outChildren) + { + ThingOwnerUtility.AppendThingHoldersFromThings(outChildren, this.GetDirectlyHeldThings()); + } + + + + public ThingOwner GetDirectlyHeldThings() + { + return this.innerContainer; + } + + // --- 容器内容访问 --- + + public Thing ContainedThing + { + get + { + return this.innerContainer.Count > 0 ? this.innerContainer[0] : null; + } + } + + public Pawn Zombie + { + get + { + // 提供一个便捷的属性来访问被存储的Pawn + return this.ContainedThing as Pawn; + } + } + + public bool HasAnyContents + { + get + { + return this.innerContainer.Count > 0; + } + } + + // --- 存入/取出逻辑 --- + + public virtual bool Accepts(Thing thing) + { + return this.innerContainer.CanAcceptAnyOf(thing, true); + } + + public virtual bool TryAcceptThing(Thing thing, bool allowSpecialEffects = true) + { + if (!this.Accepts(thing)) + { + return false; + } + bool flag; + if (thing.holdingOwner != null) + { + // 将Pawn从当前持有者(通常是地图)转移到我们的容器中 + thing.holdingOwner.TryTransferToContainer(thing, this.innerContainer, thing.stackCount, true); + flag = true; + } + else + { + // 如果Pawn没有持有者(例如是新生成的),直接添加 + flag = this.innerContainer.TryAdd(thing, true); + } + return flag; + } + + public virtual void EjectContents() + { + // 决定在何处释放Pawn + Map map = this.pawn.MapHeld ?? Find.AnyPlayerHomeMap; + IntVec3 cell = (this.pawn.Spawned || (this.pawn.Corpse != null && this.pawn.Corpse.Spawned)) ? this.pawn.PositionHeld : ((this.pawn.CarriedBy != null) ? this.pawn.CarriedBy.PositionHeld : map.Center); + + // 将容器内的所有东西(即被存储的Pawn)扔到地图上 + this.innerContainer.TryDropAll(cell, map, ThingPlaceMode.Direct, null, null, true); + } + + // --- 存档/读档 --- + + public override void ExposeData() + { + base.ExposeData(); + Scribe_References.Look(ref this.victim, "victim", false); + + // 2. 深度保存容器内容 + // 'Scribe_Deep.Look' 会序列化容器内的Pawn的所有数据 + Scribe_Deep.Look(ref this.innerContainer, "innerContainer", new object[] + { + this + }); + + // 兼容性处理:确保旧存档在加载后也能正确初始化容器 + if (Scribe.mode == LoadSaveMode.PostLoadInit) + { + if (this.innerContainer == null) + { + this.innerContainer = new ThingOwner(this, false, LookMode.Deep, true); + } + } + } + + // --- 其他逻辑 --- + // (为了简洁,此处省略了PostTick, End, AbsolutelyKill等与存储机制非直接相关的代码) + // ... + } +} +``` + +## 机制剖析 + +### 1. 声明容器身份 (`IThingHolder`) + +通过在类声明中加入 `IThingHolder`,`HediffAbility_PaintedSkin` 就获得了“容器”的资格。这要求它必须实现接口定义的属性和方法,如 `ParentHolder` 和 `GetDirectlyHeldThings()`。`GetDirectlyHeldThings()` 方法必须返回真正的存储实例,也就是我们的 `innerContainer`。 + +### 2. 初始化存储核心 (`ThingOwner`) + +在构造函数中,我们创建了一个 `ThingOwner` 实例。这里的关键在于 `LookMode.Deep` 参数。 + +* `LookMode.Value`: 只保存简单值类型(如int, float, string)。 +* `LookMode.Reference`: 只保存一个对物体的引用ID。加载时,游戏会尝试在世界中找到这个ID对应的物体。如果物体已被销毁,引用会丢失。**这不适用于存储Pawn**,因为Pawn在被存入容器时已经从世界中移除了。 +* **`LookMode.Deep`**: 这才是我们的选择。它告诉序列化系统:“请将这个物体(`Pawn`)的所有数据——健康、技能、装备、Hediff、人际关系、思想等等——完完整整地打包保存起来。” 当游戏加载时,它会用这些数据重建一个一模一样的`Pawn`实例。 + +### 3. 序列化 (`ExposeData`) + +`ExposeData` 方法是RimWorld存档机制的核心。 + +* `Scribe_Deep.Look(ref this.innerContainer, ...)`: 这行代码是魔法发生的地方。当游戏保存时,`Scribe_Deep` 会深入到 `innerContainer` 内部,并因为我们之前设置了 `LookMode.Deep`,它会对容器里的每一个 `Pawn` 进行递归式的深度保存。 +* 当游戏加载时,`Scribe_Deep` 会读取存档中的数据,重建 `innerContainer`,并利用深度保存的数据重建一个与存入时状态完全一致的 `Pawn`。 + +## 总结 + +通过实现 `IThingHolder` 接口并利用一个配置为 `LookMode.Deep` 的 `ThingOwner` 容器,我们可以将一个 `Hediff` 转变为一个功能强大的、能够随宿主移动的“Pawn胶囊”。这个“胶囊”可以安全地携带一个`Pawn`穿越存档的海洋,确保其数据的完整性和一致性。 + +这项技术是实现诸如吞噬、俘获、传送、特殊休眠仓等高级Mod功能的基石。 \ No newline at end of file diff --git a/Source/Documents/Possession_Implementation_Guide.md b/Source/Documents/Possession_Implementation_Guide.md new file mode 100644 index 0000000..d10dcdc --- /dev/null +++ b/Source/Documents/Possession_Implementation_Guide.md @@ -0,0 +1,569 @@ +# “抱脸虫夺舍”技能实现说明书 (V3 - 最终版) + +## 1. 功能概述 + +本功能实现了一个名为“阿拉克涅寄生”的特殊技能,允许一个特定的“阿拉克涅原虫”(抱脸虫)Pawn将自己的意识(灵魂)注入另一个生物的身体,从而完全占据并控制它。 + +**核心玩法循环:** +1. **夺舍**: 抱脸虫使用技能后,其物理实体消失,其“灵魂”(名字、背景、技能、特性等)将完全覆盖目标的身体。 +2. **成长**: 玩家将控制这个新的身体进行游戏,所有获得的经验、技能和记忆都将积累在这个身体上。 +3. **重生**: 当被夺舍的身体死亡时,抱脸虫的灵魂会带着所有新的成长,从尸体中“重生”,变回一个独立的、更强大的阿拉克涅原虫Pawn,准备寻找下一个宿主。 + +这是一个高风险、高回报的玩法,允许玩家以一种独特的方式延续一个核心角色的“生命”和成长。 + +## 2. 实现流程 + +```mermaid +graph TD + A[抱脸虫] -- 使用“阿拉克涅寄生”技能 --> B(目标Pawn); + B -- 添加 Hediff_Possession --> C{执行灵魂覆盖}; + C -- 1. 存储抱脸虫Pawn的完整数据 --> D[Hediff容器]; + C -- 2. 将抱脸虫的“灵魂”数据覆盖到目标身上 --> E[被夺舍的身体]; + D -- 3. 抱脸虫物理实体消失 --> F([Vanish]); + E -- 玩家控制,积累经验和记忆 --> E; + E -- 受到致命伤害 --> G{身体死亡}; + G -- 触发Hediff.Notify_PawnDied --> H{反向同步成长}; + D -- 获取存储的抱脸虫数据 --> H; + E -- 获取身体上的成长数据 --> H; + H -- 将成长更新到抱脸虫数据上 --> I[更新后的抱脸虫]; + I -- 从Hediff中释放 --> J(更强大的抱脸虫重生); + J -- 等待下一次夺舍 --> A; +``` + +--- + +## 3. 代码详解 + +### 3.1 `CompAbilityEffect_Possess.cs` - 技能效果的起点 + +这是技能被使用时第一个被调用的C#文件。它的职责是创建`Hediff_Possession`并将其附加到目标身上,从而启动整个夺舍流程。 + +```csharp +// 路径: Source/ArachnaeSwarm/Possession/CompAbilityEffect_Possess.cs +using RimWorld; +using Verse; + +namespace ArachnaeSwarm.Possession +{ + // 继承自CompAbilityEffect,这是所有技能效果组件的基类 + public class CompAbilityEffect_Possess : CompAbilityEffect + { + // 当技能成功施放时,游戏会调用这个Apply方法 + public override void Apply(LocalTargetInfo target, LocalTargetInfo dest) + { + base.Apply(target, dest); // 调用基类方法,确保标准流程执行 + + // 获取施法者 (我们的抱脸虫) + Pawn caster = this.parent.pawn; + // 获取目标Pawn + Pawn targetPawn = target.Pawn; + + // 安全检查:如果目标不是一个Pawn,则直接返回 + if (targetPawn == null) + { + return; + } + + // TODO: 在此可以添加更多的限制条件,例如: + // 1. 不能夺舍机械体 + // if (targetPawn.RaceProps.IsMechanoid) { ... } + // 2. 不能夺舍已经被夺舍的目标 + // if (targetPawn.health.hediffSet.HasHediff(HediffDef.Named("ARA_Possession"))) { ... } + + // 步骤1: 创建Hediff实例 + // HediffMaker.MakeHediff会根据XML定义创建一个新的Hediff对象 + Hediff_Possession hediff = (Hediff_Possession)HediffMaker.MakeHediff(HediffDef.Named("ARA_Possession"), targetPawn); + + // 步骤2: 注入施法者灵魂 + // 在Hediff被正式添加到目标身上之前,将施法者的引用传递进去。 + // 这是关键一步,确保Hediff在执行PostAdd逻辑时能知道谁是施法者。 + hediff.SetCaster(caster); + + // 步骤3: 将Hediff添加到目标身上 + // 这会触发Hediff_Possession类中的PostAdd方法,从而启动真正的夺舍逻辑。 + targetPawn.health.AddHediff(hediff); + } + } +} +``` + +### 3.2 `Hediff_Possession.cs` - 夺舍与重生的核心 + +这个Hediff是整个功能的核心。它作为“灵魂容器”,负责存储抱脸虫的本体,并在恰当的时机执行“夺舍”和“重生”的逻辑。 + +```csharp +// 路径: Source/ArachnaeSwarm/Possession/Hediff_Possession.cs +using System.Collections.Generic; +using RimWorld; +using Verse; + +namespace ArachnaeSwarm.Possession +{ + // 继承HediffWithComps以支持组件,并实现IThingHolder接口来表明自己是容器 + public class Hediff_Possession : HediffWithComps, IThingHolder + { + // --- 核心字段 --- + private ThingOwner innerContainer; // 实际存储灵魂(抱脸虫Pawn)的容器 + private Pawn originalCaster; // 临时保存对原始施法者的引用 + + // --- 构造与属性 --- + public Hediff_Possession() + { + // 初始化容器。LookMode.Deep是关键,它能确保Pawn的所有数据都被完整保存。 + this.innerContainer = new ThingOwner(this, false, LookMode.Deep); + } + + // 提供一个方便的只读属性来获取容器中存储的Pawn + public Pawn StoredCasterPawn => innerContainer.Count > 0 ? innerContainer[0] as Pawn : null; + + // --- IThingHolder 接口实现 --- + public IThingHolder ParentHolder => this.pawn; // 容器的父级就是持有该Hediff的Pawn + + public void GetChildHolders(List outChildren) + { + ThingOwnerUtility.AppendThingHoldersFromThings(outChildren, this.GetDirectlyHeldThings()); + } + + public ThingOwner GetDirectlyHeldThings() + { + return innerContainer; + } + + // --- 核心逻辑 --- + + // 当Hediff被成功添加到目标身上后,此方法被自动调用 + public override void PostAdd(DamageInfo? dinfo) + { + base.PostAdd(dinfo); + + if (this.originalCaster == null) + { + Log.Error("Hediff_Possession was added without an original caster."); + return; + } + + // 1. 存储灵魂:将施法者的Pawn对象完整地存入容器 + this.innerContainer.TryAdd(this.originalCaster); + + // 2. 灵魂覆盖:调用工具类,执行数据迁移 + PawnDataUtility.TransferSoul(this.originalCaster, this.pawn); + + // 3. 销毁施法者的物理实体,因为它现在“活”在目标的身体里了 + if (!this.originalCaster.Destroyed) + { + this.originalCaster.Destroy(DestroyMode.Vanish); + } + + Log.Message($"{this.pawn.LabelShort} has been possessed by {StoredCasterPawn.LabelShort}!"); + } + + // 当持有此Hediff的Pawn(即宿主)死亡时,此方法被自动调用 + public override void Notify_PawnDied(DamageInfo? dinfo, Hediff culprit = null) + { + base.Notify_PawnDied(dinfo, culprit); + + Pawn deadBody = this.pawn; + Pawn storedCaster = this.StoredCasterPawn; + + if (storedCaster == null) + { + Log.Error("Possessed pawn died, but no caster soul was found inside."); + return; + } + + Log.Message($"Host {deadBody.LabelShort} died. Transferring experience back to {storedCaster.LabelShort} and ejecting."); + + // 1. 灵魂更新:反向调用工具类,将宿主身体上的成长同步回抱脸虫的灵魂 + PawnDataUtility.TransferSoul(deadBody, storedCaster); + + // 2. 重生:将更新后的抱脸虫灵魂从容器中释放到地图上 + this.EjectContents(); + } + + // --- 公共方法 --- + + // 由CompAbilityEffect调用,用于在添加Hediff前设置施法者 + public void SetCaster(Pawn caster) + { + this.originalCaster = caster; + } + + // 将容器内的东西(抱脸虫)扔到地图上 + public void EjectContents() + { + if (StoredCasterPawn != null) + { + this.innerContainer.TryDropAll(this.pawn.Position, this.pawn.Map, ThingPlaceMode.Near); + } + } + + // --- 存档/读档 --- + public override void ExposeData() + { + base.ExposeData(); + // Scribe_Deep是关键,确保容器内的Pawn被深度保存 + Scribe_Deep.Look(ref innerContainer, "innerContainer", this); + // 保存对原始施法者的引用(虽然它很快会被销毁,但以防万一) + Scribe_References.Look(ref originalCaster, "originalCaster"); + } + } +} +``` + +### 3.3 `PawnDataUtility.cs` - “灵魂”数据迁移的执行者 + +这是一个静态工具类,集中处理所有与Pawn数据复制相关的复杂逻辑,使得其他部分的代码更整洁。 + +```csharp +// 路径: Source/ArachnaeSwarm/Possession/PawnDataUtility.cs +using System.Collections.Generic; +using RimWorld; +using Verse; + +namespace ArachnaeSwarm.Possession +{ + public static class PawnDataUtility + { + // 核心方法:将soulSource的“灵魂”数据转移到bodyTarget上 + public static void TransferSoul(Pawn soulSource, Pawn bodyTarget) + { + if (soulSource == null || bodyTarget == null) + { + Log.Error("Cannot transfer soul: source or target is null."); + return; + } + + Log.Message($"Beginning soul transfer from {soulSource.LabelShort} to {bodyTarget.LabelShort}."); + + // --- 1. 核心身份数据 --- + + // 姓名 + bodyTarget.Name = soulSource.Name; + + // 故事 (背景和特性) + bodyTarget.story.Childhood = soulSource.story.Childhood; + bodyTarget.story.Adulthood = soulSource.story.Adulthood; + // 先清空目标的所有特性,再逐一添加源的特性 + bodyTarget.story.traits.allTraits.Clear(); + foreach (Trait trait in soulSource.story.traits.allTraits) + { + bodyTarget.story.traits.GainTrait(trait); + } + + // 技能 + // 同样地,清空后逐一添加 + bodyTarget.skills.skills.Clear(); + foreach (SkillRecord skill in soulSource.skills.skills) + { + SkillRecord newSkill = new SkillRecord(bodyTarget, skill.def) + { + levelInt = skill.levelInt, + xpSinceLastLevel = skill.xpSinceLastLevel, + passion = skill.passion + }; + bodyTarget.skills.skills.Add(newSkill); + } + + // 阵营 + if (bodyTarget.Faction != soulSource.Faction) + { + bodyTarget.SetFaction(soulSource.Faction, soulSource); + } + + // --- 2. 思想、社交和设定 --- + + // 思想和记忆 + // 清空目标的记忆,然后复制源的记忆 + if (bodyTarget.needs.mood?.thoughts?.memories != null) + { + bodyTarget.needs.mood.thoughts.memories.Memories.Clear(); + } + if (soulSource.needs.mood?.thoughts?.memories != null) + { + foreach (Thought_Memory memory in soulSource.needs.mood.thoughts.memories.Memories) + { + bodyTarget.needs.mood.thoughts.memories.TryGainMemory(memory); + } + } + + // 工作设置 + if (soulSource.workSettings != null && bodyTarget.workSettings != null) + { + bodyTarget.workSettings.EnableAndInitialize(); + foreach (WorkTypeDef workDef in DefDatabase.AllDefs) + { + bodyTarget.workSettings.SetPriority(workDef, soulSource.workSettings.GetPriority(workDef)); + } + } + + // 时间表 + if (soulSource.timetable != null && bodyTarget.timetable != null) + { + bodyTarget.timetable.times = new List(soulSource.timetable.times); + } + + // 社交关系 (简化处理) + // 警告: 直接复制关系可能很危险,这里采用清空再添加直接关系的方式 + if (soulSource.relations != null && bodyTarget.relations != null) + { + bodyTarget.relations.ClearAllRelations(); + foreach (DirectPawnRelation relation in soulSource.relations.DirectRelations) + { + bodyTarget.relations.AddDirectRelation(relation.def, relation.otherPawn); + } + } + + // 访客/囚犯状态 + if (soulSource.guest != null && bodyTarget.guest != null) + { + // 使用游戏提供的标准方法来设置,而不是直接赋值 + bodyTarget.guest.SetGuestStatus(soulSource.guest.HostFaction, soulSource.guest.GuestStatus); + if (soulSource.guest.IsPrisoner) + { + bodyTarget.guest.SetExclusiveInteraction(soulSource.guest.ExclusiveInteractionMode); + } + bodyTarget.guest.joinStatus = soulSource.guest.joinStatus; + } + + // --- 3. 收尾工作 --- + + // 强制刷新Pawn的渲染缓存,确保外观(如名字)能立刻更新 + bodyTarget.Drawer.renderer.SetAllGraphicsDirty(); + + Log.Message("Soul transfer complete."); + } + } +} +``` + +### 3.4 `CompProperties_AbilityPossess.cs` - 技能属性类 + +这是一个简单的属性类,用于将我们的技能效果组件(`CompAbilityEffect_Possess`)连接到XML定义上。 + +```csharp +// 路径: Source/ArachnaeSwarm/Possession/CompProperties_AbilityPossess.cs +using RimWorld; + +namespace ArachnaeSwarm.Possession +{ + // CompProperties类用于在XML中配置Comp组件的参数 + public class CompProperties_AbilityPossess : CompProperties_AbilityEffect + { + public CompProperties_AbilityPossess() + { + // 将这个属性类与我们的技能效果实现类关联起来 + this.compClass = typeof(CompAbilityEffect_Possess); + } + } +} +``` + +--- + +## 4. XML 定义详解 + +### 4.1 `ARA_Hediffs_Possession.xml` - 定义Hediff + +```xml + + + + + + + ARA_Possession + + + + 这个生物的身体正被另一个实体所控制。 + + ArachnaeSwarm.Possession.Hediff_Possession + + false + false + 1.0 + +
  • + +
  • +
    +
    + +
    +``` + +### 4.2 `ARA_Possession_Defs.xml` - 定义技能、种族和身体 + +这个文件定义了技能本身,以及我们的“阿拉克涅原虫”作为一个完整的生物所需的一切。 + +```xml + + + + + + + ARA_Ability_Possess + + 将你的意识注入另一个生物的身体,完全占据它。 + UI/Abilities/Possess + 600 + + Verb_CastAbility + 1.5 + 5.9 + + true + false + false + false + + + + +
  • + + + + + + ARA_Facehugger + Humanlike_PostMentalState + 100 + + + + + + + + + + ARA_Facehugger + + + ARA_FacehuggerRace + 25 + +
  • + + Things/Pawn/Animal/ARA_Facehugger + 0.8 + + + Things/Pawn/Animal/Dessicated/CritterDessicated + 0.8 + +
  • + + ARA_Facehugger + + +
  • ARA_Ability_Possess
  • +
    + + + + + ARA_FacehuggerRace + + 一种小型的、脆弱的寄生生物,其唯一的生存目的就是寻找并占据一个更强大的宿主。它通过将自己的意识注入目标来完成这一过程。 + + 4.0 + 50 + -10 + 50 + + +
  • + + +
  • Scratch
  • + + 2 + 1.5 + +
    + + Animal + + ARA_FacehuggerBody + 0.2 + 0.3 + 0.1 + +
  • + AnimalAdult + 0 +
  • +
    +
    +
    + + + + ARA_FacehuggerBody + + + Body + 20 + 20 + +
  • + Head + 0.3 + +
  • + Skull + 0.2 + Inside + +
  • + Brain + 0.1 + Inside +
  • +
    + +
  • + Eye + left eye + 0.07 +
  • +
  • + Eye + right eye + 0.07 +
  • + + +
  • + Leg + front left leg + 0.1 +
  • +
  • + Leg + front right leg + 0.1 +
  • +
  • + Leg + rear left leg + 0.1 +
  • +
  • + Leg + rear right leg + 0.1 +
  • + +
    +
    + +
    +``` + +--- + +这份详尽的文档现在包含了我们所有的最终代码和XML,并附有详细的注释,解释了每一步的作用和它们之间的关联。 \ No newline at end of file