diff --git a/Source/Documents/Milira_Fortress.cs b/Source/Documents/Milira_Fortress.cs new file mode 100644 index 0000000..97fce39 --- /dev/null +++ b/Source/Documents/Milira_Fortress.cs @@ -0,0 +1,122 @@ +using AncotLibrary; +using RimWorld; +using System.Collections.Generic; +using System.Linq; +using Verse; +using Verse.AI; + +namespace Milira +{ + public class JobDriver_FortressMode : JobDriver + { + private Thing thing; + + public virtual ThingDef TurretDef => MiliraDefOf.Milian_Fortress; + + public CompThingContainer CompThingContainer => thing?.TryGetComp(); + + public override bool TryMakePreToilReservations(bool errorOnFailed) + { + return true; + } + + protected override IEnumerable MakeNewToils() + { + // 检查部署区域是否合适 + Toil check = Toils_General.Do(() => + { + foreach (var cell in GenRadial.RadialCellsAround(pawn.Position, 1.5f, false)) + { + if (!cell.IsValid || !cell.InBounds(pawn.Map) || !cell.Walkable(pawn.Map) || + !cell.GetEdifice(pawn.Map).DestroyedOrNull() || cell.Roofed(pawn.Map)) + { + // 空间不足或有障碍,技能冷却重置并结束任务 + var ability = pawn.abilities.abilities.FirstOrDefault(a => a.def.defName == "Milira_Fortress"); + ability?.StartCooldown(0); + EndJobWith(JobCondition.Incompletable); + return; + } + } + }); + yield return check; + + // 部署炮台 + yield return Toils_General.Do(DeployPod); + + // 角色进入炮台 + Toil enterPod = ToilMaker.MakeToil("EnterPod"); + enterPod.initAction = () => + { + if (thing == null) + { + EndJobWith(JobCondition.Incompletable); + return; + } + + bool pawnSelected = Find.Selector.IsSelected(pawn); + + // 角色从地图消失并进入炮台容器 + if (pawn.DeSpawnOrDeselect()) + { + CompThingContainer?.GetDirectlyHeldThings().TryAdd(pawn); + } + + // 如果之前选中了角色,现在改为选中炮台 + if (pawnSelected) + { + Find.Selector.Select(thing, playSound: false, forceDesignatorDeselect: false); + } + }; + yield return enterPod; + } + + private void DeployPod() + { + var carrierComp = pawn.TryGetComp(); + if (carrierComp == null) return; + + FleckMaker.Static(pawn.TrueCenter(), pawn.Map, FleckDefOf.Milian_FortressFormed); + thing = GenSpawn.Spawn(TurretDef, pawn.Position, pawn.Map); + thing.SetFaction(pawn.Faction); + + bool hasRapidDeployment = ModsConfig.IsActive("Ancot.MilianModification") && + pawn.health.hediffSet.GetFirstHediffOfDef(MiliraDefOf.MilianFitting_RapidDeployment) != null; + + if (hasRapidDeployment) + { + SetHitPointAndRemoveResourceInCarrier(carrierComp, 200, 60); + } + else + { + SetHitPointAndRemoveResourceInCarrier(carrierComp, 1200, 600); + } + + var containerMilian = thing.TryGetComp(); + if (containerMilian != null) + { + containerMilian.hitPointMax = thing.HitPoints; + } + } + + private void SetHitPointAndRemoveResourceInCarrier(CompThingCarrier_Custom comp, int hitPoint, int initiationDelayTicks) + { + if (comp.IngredientCount > hitPoint + 400) + { + comp.TryRemoveThingInCarrier(hitPoint); + thing.HitPoints = hitPoint; + } + else + { + int halfResources = comp.IngredientCount / 2; + comp.TryRemoveThingInCarrier(halfResources); + thing.HitPoints = halfResources; + } + + var initiatableComp = thing.TryGetComp(); + if (initiatableComp != null) + { + initiatableComp.initiationDelayTicksOverride = initiationDelayTicks; + } + } + } +} diff --git a/Source/Documents/PawnBuildingTransformer_Design.md b/Source/Documents/PawnBuildingTransformer_Design.md new file mode 100644 index 0000000..98e1b8b --- /dev/null +++ b/Source/Documents/PawnBuildingTransformer_Design.md @@ -0,0 +1,558 @@ +# Pawn 与 Building 双向转换系统设计文档 + +本文档旨在详细阐述一个通用的、可配置的系统,用于在《边缘世界》中实现角色(Pawn)与建筑(Building)之间的双向转换。 + +## 1. 核心目标 + +- **通用性**: 任何Pawn或Building都可以通过简单的XML配置获得变形能力。 +- **健壮性**: 确保在各种情况下(主动触发、被动摧毁、存档读档)Pawn的状态都能被正确保存和恢复。 +- **可扩展性**: 方便开发者添加自定义的转换条件、资源消耗和效果。 +- **状态保持**: 明确Pawn的关键状态(如健康、技能、装备、意识形态等)在转换过程中通过保存Pawn实例本身而得以完整保留。 + +## 2. 系统架构 + +系统由以下几个核心部分组成: + +- **`CompProperties_Morphable`**: 在XML中定义的配置组件,用于指定变形的目标、效果和耗时。 +- **`CompMorphable`**: 附加在物体上的核心逻辑组件,管理变形状态和触发器。 +- **`CompThingContainer`**: 用于在建筑形态下实际“存储”Pawn对象的容器。 +- **`JobDriver_Transforming`**: 一个自定义的`JobDriver`,用于处理从Pawn到Building的、有持续时间的转换过程。 +- **自定义Building类**: 需要一个继承自`Building`的子类,以重写`Destroy`方法来处理被动转换。 + +### 架构图 + +```mermaid +graph TD + subgraph Pawn + A[Pawn_Character] -- Has --> B[CompMorphable]; + end + + subgraph Building + D[Building_Morphed] -- Has --> E[CompMorphable]; + D -- Also has --> C[CompThingContainer]; + C -- Contains --> F[Stored Pawn Instance]; + end + + subgraph User_Interface + G[Gizmo Button 'Transform'] -- Triggers --> H{StartTransform}; + end + + A -- Transform --> D; + D -- Transform Back --> A; + + B -- Initiates Job --> J[JobDriver_Transforming]; + E -- Calls method --> K[TransformBackLogic]; + + subgraph XML_Configuration + L[ThingDef: Pawn_Character] -- Defines --> B; + M[ThingDef: Building_Morphed] -- Defines --> E & C; + end +``` + +--- + +## 3. 实现细节与代码示例 + +### 3.1 XML 配置 (`CompProperties_Morphable`) + +为你的Pawn和Building `ThingDef` 添加以下组件。 + +**Pawn Def:** +```xml + + + +
  • + Your_BuildingDef_Name + Your_TransformSound + Your_TransformEffect + 120 +
  • +
    +
    +``` + +**Building Def:** +```xml + + + + +
  • + CompThingContainer +
  • + +
  • + + Your_PawnDef_Name + Your_TransformBackSound + Your_TransformBackEffect +
  • +
    +
    +``` + +### 3.2 核心逻辑组件 (`CompMorphable`) + +```csharp +using RimWorld; +using Verse; + +public class CompMorphable : ThingComp +{ + private Pawn storedPawn; + public CompProperties_Morphable Props => (CompProperties_Morphable)props; + + public bool IsMorphed => parent.def == Props.buildingDef; + public Pawn StoredPawn => storedPawn; + + public void SetStoredPawn(Pawn pawn) + { + this.storedPawn = pawn; + } + + public override void PostExposeData() + { + base.PostExposeData(); + // 确保Pawn引用在存档/读档时被正确保存 + Scribe_References.Look(ref storedPawn, "storedPawn"); + } + + public override IEnumerable CompGetGizmosExtra() + { + // 为Pawn添加“变形”按钮 + if (parent is Pawn pawn) + { + yield return new Command_Action + { + defaultLabel = "变形", + action = () => { + // 创建并分配变形任务 + var job = new Job(JobDefOf.Your_TransformingJob, parent); + pawn.jobs.TryTakeOrderedJob(job); + } + }; + } + // 为Building添加“恢复”按钮 + else if (parent is Building) + { + yield return new Command_Action + { + defaultLabel = "恢复人形", + action = () => { TransformBackToPawn(); } + }; + } + } + + private void TransformBackToPawn() + { + // ... (此处实现Building -> Pawn的逻辑) ... + // 检查空间、移除建筑、重新生成Pawn + } +} +``` + +### 3.3 Pawn -> Building 转换 (`JobDriver_Transforming`) + +这个`JobDriver`负责有过程的转换。 + +```csharp +public class JobDriver_Transforming : JobDriver +{ + protected override IEnumerable MakeNewToils() + { + this.FailOnDespawnedNullOrForbidden(TargetIndex.A); + + // 1. 检查空间 + yield return Toils_General.Do(() => { + // ... 检查逻辑 ... + // 如果失败: EndJobWith(JobCondition.Incompletable); + }); + + // 2. 等待并播放特效 + var props = pawn.GetComp().Props; + yield return Toils_General.Wait(props.transformTicks) + .WithProgressBarToilDelay(TargetIndex.A); + + // 3. 执行核心转换 + Toil transform = Toils_General.Do(() => { + // 移除Pawn + pawn.DeSpawn(DestroyMode.Vanish); + // 生成Building + Building building = (Building)GenSpawn.Spawn(props.buildingDef, pawn.Position, pawn.Map); + building.SetFaction(pawn.Faction); + // 存储Pawn + building.GetComp().GetDirectlyHeldThings().TryAdd(pawn); + building.GetComp().SetStoredPawn(pawn); + }); + yield return transform; + } +} +``` + +### 3.4 Building -> Pawn 转换 (被动触发) + +需要一个自定义的Building类来处理被摧毁时的情况。 + +```csharp +public class Building_Morphable : Building +{ + public override void Destroy(DestroyMode mode) + { + var comp = this.GetComp(); + if (comp != null && comp.StoredPawn != null) + { + Pawn pawn = comp.StoredPawn; + // 在建筑消失前,先把Pawn生成出来 + GenSpawn.Spawn(pawn, this.Position, this.Map); + + // 可选:对Pawn施加伤害 + if (mode == DestroyMode.KillFinalize) + { + pawn.TakeDamage(new DamageInfo(DamageDefOf.Bomb, 20)); + } + } + base.Destroy(mode); + } +} +``` +**注意**: 你的Building `ThingDef` 的 `thingClass` 需要指向这个新的 `Building_Morphable` 类。 + +--- + +## 4. 流程图 + +### Pawn -> Building +```mermaid +sequenceDiagram + participant Player + participant Pawn + participant Gizmo + participant JobDriver_Transforming + + Player->>Gizmo: 点击“变形”按钮 + Gizmo->>Pawn: 分配 "Transforming" 任务 + Pawn->>JobDriver_Transforming: 开始执行任务 + JobDriver_Transforming->>JobDriver_Transforming: Toil 1: 检查空间 + JobDriver_Transforming->>JobDriver_Transforming: Toil 2: 等待 & 播放特效 + JobDriver_Transforming->>JobDriver_Transforming: Toil 3: DeSpawn Pawn, Spawn Building, Store Pawn + JobDriver_Transforming->>Pawn: 任务结束 +``` + +### Building -> Pawn +```mermaid +graph TD + subgraph Active_Trigger [主动触发] + A[Player Clicks Gizmo] --> B{TransformBackToPawn}; + B --> C[Check Spawn Area]; + C -- OK --> E[DeSpawn Building]; + E --> F[Spawn Pawn at Position]; + end + + subgraph Passive_Trigger [被动触发] + H[Building HP <= 0] --> I{Override 'Destroy' method}; + I --> J[Get Stored Pawn]; + J --> K[Spawn Pawn at Position]; + K --> L[Apply Damage to Pawn]; + L --> M[Call base.Destroy()]; + end +``` + +这份文档提供了从设计理念到具体实现的全套方案,您可以根据此指南开始编码工作。 + +--- + +## 5. 完整C#代码实现 + +以下是实现此系统所需的全部C#脚本。建议将它们放置在项目的一个新子目录中(例如 `Source/Morphable/`)。 + +### 5.1 `CompProperties_Morphable.cs` + +```csharp +using RimWorld; +using Verse; + +namespace YourNamespace +{ + public class CompProperties_Morphable : CompProperties + { + // 变形为建筑的目标ThingDef + public ThingDef buildingDef; + + // 从建筑恢复时,指定要生成的PawnDef(可选,为空则恢复原Pawn) + public ThingDef pawnDef; + + // 变形时的音效 + public SoundDef transformSound; + + // 变形时的视觉效果 + public EffecterDef transformEffect; + + // 变形过程所需的tick数 + public int transformTicks = 60; + + public CompProperties_Morphable() + { + compClass = typeof(CompMorphable); + } + } +} +``` + +### 5.2 `CompMorphable.cs` + +```csharp +using RimWorld; +using System.Collections.Generic; +using Verse; +using Verse.AI; + +namespace YourNamespace +{ + public class CompMorphable : ThingComp + { + private Pawn storedPawn; + public CompProperties_Morphable Props => (CompProperties_Morphable)props; + + public bool IsMorphed => parent.def == Props.buildingDef; + public Pawn StoredPawn => storedPawn; + + public void SetStoredPawn(Pawn pawn) + { + this.storedPawn = pawn; + } + + public override void PostExposeData() + { + base.PostExposeData(); + Scribe_References.Look(ref storedPawn, "storedPawn", false); + } + + public override IEnumerable CompGetGizmosExtra() + { + if (parent is Pawn pawn && pawn.Faction == Faction.OfPlayer) + { + yield return new Command_Action + { + defaultLabel = "变形", + defaultDesc = "将此单位转换为建筑形态。", + icon = TexCommand.Attack, // TODO: Replace with a proper icon + action = () => { + // 添加配置验证 + if (Props.buildingDef == null) + { + Log.Error($"CompMorphable on {parent.def.defName} has no buildingDef defined."); + return; + } + var job = JobMaker.MakeJob(JobDefOf.Your_TransformingJob, parent); + pawn.jobs.TryTakeOrderedJob(job); + } + }; + } + else if (parent is Building building && building.Faction == Faction.OfPlayer && storedPawn != null) + { + yield return new Command_Action + { + defaultLabel = "恢复人形", + defaultDesc = "将此建筑恢复为人形。", + icon = TexCommand.ReleaseAnimals, // TODO: Replace with a proper icon + action = () => { TransformBackToPawn(); } + }; + } + } + + private void TransformBackToPawn() + { + Building building = (Building)this.parent; + Map map = building.Map; + + // 播放特效 + if (Props.transformEffect != null) + { + Effecter eff = Props.transformEffect.Spawn(); + eff.Trigger(building, building); + } + if (Props.transformSound != null) + { + Props.transformSound.PlayOneShot(new TargetInfo(building.Position, map)); + } + + // 移除建筑 + building.DeSpawn(DestroyMode.Vanish); + + // 重新生成Pawn + GenSpawn.Spawn(storedPawn, building.Position, map, WipeMode.Vanish); + + // 选中Pawn + if (Find.Selector.IsSelected(building)) + { + Find.Selector.Select(storedPawn); + } + } + } +} +``` + +### 5.3 `JobDriver_Transforming.cs` + +```csharp +using RimWorld; +using System.Collections.Generic; +using Verse; +using Verse.AI; + +namespace YourNamespace +{ + public class JobDriver_Transforming : JobDriver + { + public override bool TryMakePreToilReservations(bool errorOnFailed) + { + return pawn.Reserve(pawn.Position, job); + } + + protected override IEnumerable MakeNewToils() + { + this.FailOnDespawnedNullOrForbidden(TargetIndex.A); + var comp = pawn.GetComp(); + if (comp == null) yield break; + + // 1. 检查空间是否足够 + Toil checkSpace = Toils_General.Do(() => { + foreach (var cell in GenRadial.RadialCellsAround(pawn.Position, comp.Props.buildingDef.Size.x / 2f, true)) + { + if (!cell.InBounds(Map) || !cell.Walkable(Map) || cell.GetEdifice(Map) != null) + { + pawn.jobs.EndCurrentJob(JobCondition.Incompletable, true); + if (pawn.Faction == Faction.OfPlayer) + { + Messages.Message("PawnTransformer_SpaceBlocked".Translate(pawn.Named("PAWN")), pawn, MessageTypeDefOf.RejectInput, false); + } + return; + } + } + }); + yield return checkSpace; + + // 2. 等待并播放特效 + Toil waitingToil = Toils_General.Wait(comp.Props.transformTicks, TargetIndex.None) + .WithProgressBarToilDelay(TargetIndex.A); + + waitingToil.tickAction = () => + { + if (comp.Props.transformEffect != null && pawn.IsHashIntervalTick(5)) + { + Effecter eff = comp.Props.transformEffect.Spawn(); + eff.Trigger(pawn, pawn); + } + }; + if (comp.Props.transformSound != null) + { + waitingToil.initAction = () => { comp.Props.transformSound.PlayOneShot(new TargetInfo(pawn.Position, pawn.Map)); }; + } + yield return waitingToil; + + // 3. 执行核心转换 + Toil transform = Toils_General.Do(() => { + IntVec3 position = pawn.Position; + Map map = pawn.Map; + + // 移除Pawn + pawn.DeSpawn(DestroyMode.Vanish); + + // 生成Building + Building building = (Building)GenSpawn.Spawn(comp.Props.buildingDef, position, map, WipeMode.Vanish); + building.SetFaction(pawn.Faction); + + // 继承Pawn的名称 + if (pawn.Name != null) + { + building.TryGetComp().TryAssignPawn(pawn); + } + + // 存储Pawn + var container = building.GetComp(); + if (container != null) + { + container.GetDirectlyHeldThings().TryAdd(pawn); + } + + var newMorphComp = building.GetComp(); + if (newMorphComp != null) + { + newMorphComp.SetStoredPawn(pawn); + } + }); + yield return transform; + } + } +} +``` + +### 5.4 `Building_Morphable.cs` + +```csharp +using RimWorld; +using Verse; + +namespace YourNamespace +{ + public class Building_Morphable : Building + { + public override void Destroy(DestroyMode mode) + { + var comp = this.GetComp(); + if (comp != null && comp.StoredPawn != null) + { + Pawn pawn = comp.StoredPawn; + Map map = this.Map; + IntVec3 position = this.Position; + + // 在建筑消失前,先把Pawn生成出来 + GenSpawn.Spawn(pawn, position, map, WipeMode.Vanish); + + // 如果是被摧毁,对Pawn施加伤害 + if (mode == DestroyMode.KillFinalize) + { + Messages.Message($"{pawn.LabelShort} 从被摧毁的建筑中弹出!", pawn, MessageTypeDefOf.NegativeEvent); + pawn.TakeDamage(new DamageInfo(DamageDefOf.Bomb, 20, 1, -1, this)); + } + } + base.Destroy(mode); + } + } +} +``` + +### 5.5 `JobDefOf.cs` (用于定义JobDef) + +```csharp +using RimWorld; +using Verse; + +namespace YourNamespace +{ + [DefOf] + public static class JobDefOf + { + public static JobDef Your_TransformingJob; + + static JobDefOf() + { + DefOfHelper.EnsureInitializedInCtor(typeof(JobDefOf)); + } + } +} +``` + +**最后,您还需要在 `Defs/JobDefs/` 目录下创建一个XML文件来定义 `Your_TransformingJob`:** + +```xml + + + + Your_TransformingJob + YourNamespace.JobDriver_Transforming + 正在变形... + + +``` \ No newline at end of file