Files
ArachnaeSwarm/Source/Documents/PawnBuildingTransformer_Design.md
2025-09-11 18:38:21 +08:00

558 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
<ThingDef ParentName="BasePawn">
<!-- ... other pawn properties ... -->
<comps>
<li Class="YourNamespace.CompProperties_Morphable">
<buildingDef>Your_BuildingDef_Name</buildingDef>
<transformSound>Your_TransformSound</transformSound>
<transformEffect>Your_TransformEffect</transformEffect>
<transformTicks>120</transformTicks> <!-- 2 seconds -->
</li>
</comps>
</ThingDef>
```
**Building Def:**
```xml
<ThingDef ParentName="BuildingBase">
<!-- ... other building properties ... -->
<comps>
<!-- 容器组件,必须有 -->
<li Class="CompProperties_ThingContainer">
<compClass>CompThingContainer</compClass>
</li>
<!-- 变形逻辑组件 -->
<li Class="YourNamespace.CompProperties_Morphable">
<!-- pawnDef是可选的如果为空则恢复被存储的pawn -->
<pawnDef>Your_PawnDef_Name</pawnDef>
<transformSound>Your_TransformBackSound</transformSound>
<transformEffect>Your_TransformBackEffect</transformEffect>
</li>
</comps>
</ThingDef>
```
### 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<Gizmo> 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<Toil> MakeNewToils()
{
this.FailOnDespawnedNullOrForbidden(TargetIndex.A);
// 1. 检查空间
yield return Toils_General.Do(() => {
// ... 检查逻辑 ...
// 如果失败: EndJobWith(JobCondition.Incompletable);
});
// 2. 等待并播放特效
var props = pawn.GetComp<CompMorphable>().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<CompThingContainer>().GetDirectlyHeldThings().TryAdd(pawn);
building.GetComp<CompMorphable>().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<CompMorphable>();
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<Gizmo> 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<Toil> MakeNewToils()
{
this.FailOnDespawnedNullOrForbidden(TargetIndex.A);
var comp = pawn.GetComp<CompMorphable>();
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<CompAssignableToPawn>().TryAssignPawn(pawn);
}
// 存储Pawn
var container = building.GetComp<CompThingContainer>();
if (container != null)
{
container.GetDirectlyHeldThings().TryAdd(pawn);
}
var newMorphComp = building.GetComp<CompMorphable>();
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<CompMorphable>();
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
<?xml version="1.0" encoding="utf-8" ?>
<Defs>
<JobDef>
<defName>Your_TransformingJob</defName>
<driverClass>YourNamespace.JobDriver_Transforming</driverClass>
<reportString>正在变形...</reportString>
</JobDef>
</Defs>
```