This commit is contained in:
2025-12-29 13:16:19 +08:00
parent f1108935e9
commit ab68728cab
16 changed files with 0 additions and 6161 deletions

View File

@@ -1,514 +0,0 @@
# 动物工作模式解决方案文档(最终实现版)
## 1. 项目概述
本项目旨在实现一个功能,使动物(特别是昆虫)能够像机械体一样持续工作,而不需要通过机械师控制 WorkMode。通过为动物启用工作系统并创建自定义行为树动物将始终处于工作状态持续执行允许的工作类型。
## 2. 原版代码分析
### 2.1 昆虫 ThinkTreeDef 结构
原版昆虫的 ThinkTree (`Insect.xml`) 包含以下主要逻辑:
1. **紧急情况处理**:倒地、燃烧、精神状态等
2. **基本需求满足**:睡觉、吃饭等
3. **驯服动物行为**:跟随主人、救援等
4. **闲置行为**:漫游、待命等
### 2.2 机械体 ThinkTreeDef 结构
原版机械体的 ThinkTree (`Mechanoid.xml`) 包含 WorkMode 系统,这是动物不具备的。
## 3. 为什么机械体有 workSettings 而动物没有
### 3.1 核心原因分析
机械体和动物虽然都是 Pawn但它们的 workSettings 初始化逻辑不同:
1. **`Pawn_WorkSettings` 的存在性**
- `Pawn` 类内部持有一个 `Pawn_WorkSettings` 类型的引用 `priorities`,默认为 `null`
- `public bool EverWork => priorities != null;` 属性表明只有当 `priorities` 不为 `null`Pawn 才有工作能力
2. **初始化条件**
- `EnableAndInitializeIfNotAlreadyInitialized()` 方法检查 `priorities` 是否为 `null`,如果为 `null` 则调用 `EnableAndInitialize()`
- `EnableAndInitialize()` 方法是真正执行初始化的地方,创建 `priorities` 对象并根据条件设置初始优先级
3. **Mechanoid 有 WorkSettings 的证据**
- `EnableAndInitialize()` 方法明确包含对机械体的特殊处理:
```csharp
if (ModsConfig.BiotechActive && pawn.RaceProps.IsMechanoid && !pawn.RaceProps.mechWorkTypePriorities.NullOrEmpty())
{
for (int i = 0; i < pawn.RaceProps.mechWorkTypePriorities.Count; i++)
{
MechWorkTypePriority mechWorkTypePriority = pawn.RaceProps.mechWorkTypePriorities[i];
SetPriority(mechWorkTypePriority.def, mechWorkTypePriority.priority);
}
}
```
- 这表明机械体的设计意图就是拥有并使用 `Pawn_WorkSettings`
4. **Animal 没有 WorkSettings 的证据**
- **直接初始化逻辑**`EnableAndInitialize()` 方法在为 Pawn 分配默认"活跃"工作类型时,会过滤掉 `pawn.WorkTypeIsDisabled(w)` 的工作类型。对于大多数动物来说,它们的 `RaceProps` 会禁用几乎所有需要手动分配的 `WorkTypeDef`
- **工作类型的定义**:绝大多数工作类型都包含限制,使得动物 Pawn 默认无法执行这些工作
- **特定工作提供者的处理**:针对动物的操作是通过独立的 `WorkGiver` 系统直接处理的,不依赖于通用的 `workSettings` 优先级系统
### 3.2 结论
机械体被设计为拥有 `Pawn_WorkSettings`,因为它们的工作优先级可以通过其 `RaceProps.mechWorkTypePriorities` 进行预设,并且游戏逻辑会在需要时初始化它。
动物通常没有有效的 `Pawn_WorkSettings`,因为:
1. 它们被设计为不能执行绝大多数需要手动分配的工作
2. 它们与玩家互动的特定工作是通过独立的 `WorkGiver` 系统直接处理的
3. 因此,除非有特定的游戏逻辑或 Mod 显式地为动物调用 `EnableAndInitialize()`,否则它们的 `priorities` 对象很可能保持 `null` 状态
## 4. 为什么 `RaceProps.mechWorkTypePriorities` 只对机械体有效
### 4.1 核心解释
`mechWorkTypePriorities` 是 `RaceProperties` 类中的一个字段专门用于定义机械体Mechanoid在生成时其工作类型Work Types的默认优先级。它的设计和使用都与机械体的独特机制紧密相关而这些机制在动物或其他生物上并不存在或不以相同方式运作。
### 4.2 详细证据和解释
1. **字段定义与命名**
- `mechWorkTypePriorities` 被明确定义为:
```csharp
public List<MechWorkTypePriority> mechWorkTypePriorities;
```
- 其名称 `mechWorkTypePriorities` 包含了 "Mech" 前缀,明确指示其用途是针对机械体的。
2. **`MechWorkTypePriority` 类型**
- 该字段存储的是 `MechWorkTypePriority` 对象的列表。虽然代码片段中没有直接给出 `MechWorkTypePriority` 的定义,但其名称和用途(存储工作类型及其优先级)强烈暗示它是专门为机械体设计的数据结构。
3. **使用场景 - 初始化工作设置**
- 关键证据出现在 `Pawn_WorkSettings.EnableAndInitialize()` 方法中:
```csharp
// ... (其他初始化代码) ...
if (ModsConfig.BiotechActive && pawn.RaceProps.IsMechanoid && !pawn.RaceProps.mechWorkTypePriorities.NullOrEmpty())
{
for (int i = 0; i < pawn.RaceProps.mechWorkTypePriorities.Count; i++)
{
MechWorkTypePriority mechWorkTypePriority = pawn.RaceProps.mechWorkTypePriorities[i];
SetPriority(mechWorkTypePriority.def, mechWorkTypePriority.priority);
}
}
// ... (其他初始化代码,如处理禁用的工作类型) ...
```
- 这段代码明确地检查了三个条件:
* `ModsConfig.BiotechActive`生物技术Biotech模组是否激活。这表明该功能是与 Biotech 模组引入的机械体相关的。
* `pawn.RaceProps.IsMechanoid`:当前 Pawn单位的种族属性是否为机械体。
* `!pawn.RaceProps.mechWorkTypePriorities.NullOrEmpty()`:该机械体的 `mechWorkTypePriorities` 列表是否存在且非空。
- **只有当这三个条件都满足时,才会遍历 `mechWorkTypePriorities` 列表,并根据其中定义的 `WorkTypeDef` 和优先级来设置该机械体 Pawn 的工作优先级。**
- 对于动物或其他非机械体生物,`pawn.RaceProps.IsMechanoid` 条件为假,因此即使在它们的 `ThingDef` 中定义了 `mechWorkTypePriorities`,这段代码也不会执行,这些优先级设置就会被忽略。
4. **机制差异 - 动物 vs 机械体**
- **动物**:动物的工作能力(如驯服、训练、特定任务)通常是通过 `trainability`(可训练性)、`trainableTags`(可训练标签)等属性来定义的。它们的工作行为(如觅食、繁殖、战斗)主要由其 `intelligence`(智力)、`thinkTreeMain`(主思考树)和 `dutyBoss`(职责)等属性驱动。它们的初始工作优先级(如果有的话)通常是固定的或基于简单规则随机分配,而不是通过一个专门的优先级列表来精确控制。
- **机械体**:机械体是 Biotech 模组引入的复杂单位,它们拥有类似殖民者的工作系统。它们可以被指派执行各种殖民者能做的工作(如建造、种植、烹饪等),其能力范围和初始工作优先级需要更精细的控制。`mechWorkTypePriorities` 就是为了满足这种需求而设计的,允许设计者为每种机械体明确指定哪些工作类型应该默认开启以及它们的优先级。
5. **辅助证据 - MechWorkUtility**
- 在 `MechWorkUtility` 类中,多个方法(如 `SpecialDisplayStats`, `AnyWorkMechCouldDo`)在处理与机械体工作相关逻辑时,都会首先检查 `parentDef.race.IsMechanoid`。这进一步证明了与机械体工作类型相关的逻辑是严格限定于机械体的。
### 4.3 结论
`RaceProperties.mechWorkTypePriorities` 虽然是 `RaceProperties` 类的一个通用属性,但它被设计并实现为**仅对机械体Mechanoid生效**。这是因为:
1. 它的命名和数据类型 (`MechWorkTypePriority`) 明确指向机械体。
2. 核心的使用代码 (`Pawn_WorkSettings.EnableAndInitialize`) 通过 `pawn.RaceProps.IsMechanoid` 条件严格限制了其应用范围。
3. 机械体和动物在工作系统上的根本差异决定了需要不同的机制来管理其初始工作能力,`mechWorkTypePriorities` 是为满足机械体特有需求而生的。
因此,即使在动物的 `ThingDef` 文件中添加了 `mechWorkTypePriorities` 部分,游戏代码也不会读取或应用这些设置,因为动物不是机械体,相关的初始化逻辑不会被执行。
## 5. 通过 Harmony 补丁拦截 `pawn.RaceProps.IsMechanoid` 的分析
### 5.1 用户提供的实现思路
用户提供了一种更精确和安全的实现思路:通过 Harmony 补丁拦截 `Pawn_WorkSettings.EnableAndInitialize` 方法,在特定条件下局部伪装成机械体来为动物启用工作系统。
### 5.2 实现方法
1. **引入 Harmony**:确保你的 Mod 项目中引用了 Harmony 库。
2. **定义补丁类**:创建一个静态类来存放你的 Harmony 补丁。
3. **编写前缀补丁**
```csharp
[HarmonyPatch(typeof(Pawn_WorkSettings), nameof(Pawn_WorkSettings.EnableAndInitialize))]
public static class Patch_Pawn_WorkSettings_EnableAndInitialize
{
public static bool Prefix(Pawn_WorkSettings __instance, Pawn ___pawn)
{
// 检查是否是我们想要启用工作系统的动物
if (___pawn.Faction != null && ___pawn.Faction.IsPlayer &&
!___pawn.RaceProps.IsMechanoid && // 真实身份不是机械体
ShouldEnableWorkSystem(___pawn)) // 但我们需要为它启用工作系统
{
// 局部伪装成机械体来执行初始化逻辑
// 执行机械体工作优先级初始化逻辑
if (ModsConfig.BiotechActive && !___pawn.RaceProps.mechWorkTypePriorities.NullOrEmpty())
{
for (int i = 0; i < ___pawn.RaceProps.mechWorkTypePriorities.Count; i++)
{
var priority = ___pawn.RaceProps.mechWorkTypePriorities[i];
__instance.SetPriority(priority.def, priority.priority);
}
}
// 同时也可以初始化 mechEnabledWorkTypes 中的工作类型
if (!___pawn.RaceProps.mechEnabledWorkTypes.NullOrEmpty())
{
foreach (var workType in ___pawn.RaceProps.mechEnabledWorkTypes)
{
if (!__instance.WorkIsActive(workType) && !___pawn.WorkTypeIsDisabled(workType))
{
__instance.SetPriority(workType, 3); // 默认优先级
}
}
}
// 阻止原方法继续执行
return false;
}
return true; // 其他情况正常执行原逻辑
}
private static bool ShouldEnableWorkSystem(Pawn pawn)
{
// 检查是否有特定的 Comp
if (pawn.TryGetComp<CompWorkForNonMechs>() != null)
return true;
// 检查是否有 mechWorkTypePriorities 或 mechEnabledWorkTypes 配置
if (!pawn.RaceProps.mechWorkTypePriorities.NullOrEmpty() ||
!pawn.RaceProps.mechEnabledWorkTypes.NullOrEmpty())
return true;
return false;
}
}
```
4. **应用补丁**
```csharp
[StaticConstructorOnStartup]
public static class AnimalWorkSystemPatcher
{
static AnimalWorkSystemPatcher()
{
var harmony = new Harmony("com.yourname.animalworksystem");
harmony.PatchAll();
}
}
```
### 5.3 潜在问题和风险
1. **副作用**
- 这种方法比直接补丁 `IsMechanoid` 属性更加精确,因为它只在 `Pawn_WorkSettings.EnableAndInitialize` 方法中进行伪装,不影响其他系统。
- 但仍需注意确保伪装逻辑只在特定条件下触发,避免意外影响其他 Pawn。
2. **补丁冲突**
- 其他 Mod 也可能使用 Harmony 来修改 `Pawn_WorkSettings` 或相关逻辑。
- 需要确保补丁逻辑具有良好的兼容性。
3. **性能影响**
- Harmony 补丁会增加方法调用的开销,但在这个场景下影响很小。
4. **维护困难**
- 如果游戏更新修改了相关逻辑,你的补丁可能失效或产生新的问题,需要随之更新。
### 5.4 结论和建议
用户提供的实现思路是一种更加精确和安全的方法,通过局部伪装成机械体来为动物启用工作系统。这种方法的优点包括:
1. **精确性**:只在特定条件下(动物是玩家阵营且需要启用工作系统)才进行伪装。
2. **安全性**:只在 `Pawn_WorkSettings.EnableAndInitialize` 方法中进行伪装,不影响其他系统。
3. **兼容性**:通过检查特定的 Comp 或配置来决定是否启用工作系统,具有良好的可扩展性。
## 6. 完整实现方案
### 6.1 核心思路
1. 为动物添加 `CompWorkForNonMechs` 以启用工作系统
2. 创建 `ThinkNode_AnimalWorker` 替代默认行为,强制工作
3. 使用 Harmony 补丁局部伪装成机械体来为动物启用工作系统
4. 使用自定义 ThinkTree 替代默认动物行为树
5. 定义允许的工作类型,并确保其与 WorkGiver 兼容
### 6.2 设计要点
1. **为动物启用 WorkSettings 系统**:通过 Harmony 补丁局部伪装成机械体来为动物启用工作系统
2. **使用自定义 ThinkNode 强制工作行为**:创建 ThinkNode跳过所有动物默认行为直接进入工作逻辑
3. **修改动物定义**:添加 ThinkTree 和 Comp
4. **为动物添加工作类型支持**:定义允许的工作类型
## 7. 实现细节
### 7.1 CompWorkForNonMechs.cs
```csharp
using System.Collections.Generic;
using RimWorld;
using Verse;
namespace YourModName
{
public class CompWorkForNonMechs : ThingComp
{
public class CompProperties_WorkForNonMechs : CompProperties
{
public List<WorkTypeDef> workTypes;
public CompProperties_WorkForNonMechs()
{
compClass = typeof(CompWorkForNonMechs);
}
}
public override void PostSpawnSetup(bool respawningAfterLoad)
{
base.PostSpawnSetup(respawningAfterLoad);
var pawn = parent as Pawn;
if (pawn == null || pawn.Faction == null || !pawn.Faction.IsPlayer) return;
// 启用工作设置
if (pawn.workSettings == null)
{
pawn.workSettings = new Pawn_WorkSettings(pawn);
}
pawn.workSettings.EnableAndInitialize();
// 启用指定的工作类型
var props = (CompProperties_WorkForNonMechs)props;
if (props.workTypes != null)
{
foreach (var workType in props.workTypes)
{
if (!pawn.WorkTypeIsDisabled(workType))
{
pawn.workSettings.SetPriority(workType, 3); // 默认优先级
}
}
}
}
}
}
```
### 7.2 ThinkNode_AnimalWorker.cs
```csharp
using RimWorld;
using Verse;
namespace YourModName
{
public class ThinkNode_AnimalWorker : ThinkNode_Priority
{
public override ThinkResult TryIssueJobPackage(Pawn pawn, JobIssueParams jobParams)
{
if (pawn.workSettings == null || !pawn.Faction.IsPlayer)
{
return ThinkResult.NoJob;
}
// 优先执行紧急工作
var job = WorkGiverUtility.GetPriorityWork(pawn, emergency: true);
if (job != null) return new ThinkResult(job, this);
// 然后执行普通工作
job = WorkGiverUtility.GetPriorityWork(pawn, emergency: false);
if (job != null) return new ThinkResult(job, this);
return ThinkResult.NoJob;
}
}
}
```
### 7.3 AnimalWorkSystemPatcher.cs
```csharp
using System.Collections.Generic;
using System.Reflection;
using HarmonyLib;
using RimWorld;
using Verse;
namespace YourModName
{
[StaticConstructorOnStartup]
public static class AnimalWorkSystemPatcher
{
static AnimalWorkSystemPatcher()
{
var harmony = new Harmony("com.yourname.animalworksystem");
harmony.PatchAll();
}
}
[HarmonyPatch(typeof(Pawn_WorkSettings), nameof(Pawn_WorkSettings.EnableAndInitialize))]
public static class Patch_Pawn_WorkSettings_EnableAndInitialize
{
// 缓存原始的 IsMechanoid 属性 Getter
private static PropertyInfo isMechanoidProperty =
typeof(RaceProperties).GetProperty("IsMechanoid", BindingFlags.Public | BindingFlags.Instance);
public static bool Prefix(Pawn_WorkSettings __instance, Pawn ___pawn)
{
// 检查是否是我们想要启用工作系统的动物
if (___pawn.Faction != null && ___pawn.Faction.IsPlayer &&
!___pawn.RaceProps.IsMechanoid && // 真实身份不是机械体
ShouldEnableWorkSystem(___pawn)) // 但我们需要为它启用工作系统
{
// 局部伪装成机械体来执行初始化逻辑
// 执行机械体工作优先级初始化逻辑(来自 Pawn_WorkSettings.EnableAndInitialize
if (ModsConfig.BiotechActive && !___pawn.RaceProps.mechWorkTypePriorities.NullOrEmpty())
{
for (int i = 0; i < ___pawn.RaceProps.mechWorkTypePriorities.Count; i++)
{
var priority = ___pawn.RaceProps.mechWorkTypePriorities[i];
__instance.SetPriority(priority.def, priority.priority);
}
}
// 同时也可以初始化 mechEnabledWorkTypes 中的工作类型(如果你需要)
if (!___pawn.RaceProps.mechEnabledWorkTypes.NullOrEmpty())
{
foreach (var workType in ___pawn.RaceProps.mechEnabledWorkTypes)
{
if (!__instance.WorkIsActive(workType) && !___pawn.WorkTypeIsDisabled(workType))
{
__instance.SetPriority(workType, 3); // 默认优先级
}
}
}
// 阻止原方法继续执行(因为我们已经手动处理了初始化)
return false;
}
return true; // 其他情况正常执行原逻辑
}
private static bool ShouldEnableWorkSystem(Pawn pawn)
{
// 你可以通过多种方式判断:
// 1. 检查是否有特定的 Comp
if (pawn.TryGetComp<CompWorkForNonMechs>() != null)
return true;
// 2. 检查是否有 mechWorkTypePriorities 或 mechEnabledWorkTypes 配置
if (!pawn.RaceProps.mechWorkTypePriorities.NullOrEmpty() ||
!pawn.RaceProps.mechEnabledWorkTypes.NullOrEmpty())
return true;
// 3. 检查特定的标签或 defName
// return pawn.def.defName.Contains("Worker");
return false;
}
}
}
```
### 7.4 修改动物定义
```xml
<ThingDef ParentName="BaseInsect">
<defName>Megascarab_Worker</defName>
<label>worker megascarab</label>
<description>A genetically modified megascarab, capable of performing simple mechanical tasks.</description>
<statBases>
<MoveSpeed>3.0</MoveSpeed>
<MarketValue>300</MarketValue>
</statBases>
<race>
<thinkTreeMain>InsectWorker</thinkTreeMain>
<thinkTreeConstant>AnimalConstant</thinkTreeConstant>
<mechWorkTypePriorities>
<li>
<def>Hauling</def>
<priority>3</priority>
</li>
<li>
<def>Cleaning</def>
<priority>2</priority>
</li>
</mechWorkTypePriorities>
</race>
<comps>
<li Class="YourModName.CompWorkForNonMechs+CompProperties_WorkForNonMechs">
<workTypes>
<li>Hauling</li>
<li>Cleaning</li>
<li>BasicWorker</li>
</workTypes>
</li>
</comps>
</ThingDef>
```
### 7.5 自定义 ThinkTreeDef
```xml
<ThinkTreeDef>
<defName>InsectWorker</defName>
<thinkRoot Class="ThinkNode_Priority">
<subNodes>
<!-- 紧急情况处理 -->
<li Class="ThinkNode_Subtree"><treeDef>Downed</treeDef></li>
<li Class="ThinkNode_Subtree"><treeDef>BurningResponse</treeDef></li>
<li Class="ThinkNode_Subtree"><treeDef>MentalStateCritical</treeDef></li>
<!-- 强制工作模式 -->
<li Class="YourModName.ThinkNode_AnimalWorker" />
<!-- 闲置行为 -->
<li Class="JobGiver_IdleError"/>
</subNodes>
</thinkRoot>
</ThinkTreeDef>
```
## 8. 原始方案的反驳分析
### 8.1 项目概述:不成立
**反驳证据:**
- RimWorld 原版设计中,动物的行为树是完全独立于机械体的,没有 WorkMode 系统。
- 动物不能直接复用机械体的工作逻辑。
### 8.2 ThinkNode_AnimalAlwaysWork 类:不成立
**反驳证据:**
- `ThinkNode_Priority` 是一个标准的 ThinkNode它会按顺序尝试子节点直到某一个返回 `ThinkResult`。
- 该类不会实现"始终工作"的功能,因为它没有改变行为树的执行逻辑。
### 8.3 InsectWorker ThinkTreeDef不成立
**反驳证据:**
- RimWorld 中,动物的 ThinkTree 并不会调用 `JobGiver_Work`,因为该类是为机械体设计的。
- 即使强制插入 `JobGiver_Work`,它也会因为动物的 `WorkSettings` 未初始化而失败。
### 8.4 Harmony 补丁拦截 IsMechanoid不成立
**反驳证据:**
- `RaceProperties.IsMechanoid` 是一个只读属性,由 `<race><isMechanoid>true</isMechanoid></race>` 控制。
- Patch 该属性会导致所有依赖该属性的逻辑被干扰,风险极高。
## 9. 使用说明
1. **编译代码**:将所有 C# 文件编译到您的 Mod 中
2. **添加 XML 文件**:将新的 ThinkTreeDef 和动物定义添加到您的 Mod 中
3. **配置工作类型**:在动物定义中配置 `mechWorkTypePriorities` 和 `CompWorkForNonMechs` 组件
4. **测试**:在游戏中生成修改后的动物,观察其工作行为
## 10. 注意事项
1. **兼容性**:此方案绕过了 WorkMode 系统,动物将始终处于工作状态
2. **性能**:频繁的工作查找可能会有轻微性能影响
3. **调试**:如果遇到问题,请检查日志文件中的错误信息
4. **避免 patch IsMechanoid**:不干扰原版逻辑
5. **副作用风险**:直接 patch `IsMechanoid` 会导致严重的副作用,强烈不推荐使用
6. **精确补丁**:用户提供的实现通过局部伪装成机械体来为动物启用工作系统,是一种更加精确和安全的方法

View File

@@ -1,468 +0,0 @@
# 天灾导弹防御塔 - C# 实现计划
本文档包含构建“天灾导弹防御塔”所需的全部C#类的源代码。这些代码基于 `Rimatomics` 的跨地图打击框架,并与我们自己的 `Projectile_CruiseMissile` 弹头相结合。
---
## 1. 建筑类: `Building_CatastropheMissileSilo.cs`
**路径**: `Source/ArachnaeSwarm/Buildings/Building_CatastropheMissileSilo.cs`
**功能**: 这是导弹发射井的建筑本体,负责处理玩家的瞄准指令。它几乎是 `Building_Railgun` 的翻版,但进行了一些重命名和命名空间调整。
```csharp
using System;
using System.Collections.Generic;
using System.Linq;
using RimWorld;
using RimWorld.Planet;
using UnityEngine;
using Verse;
using Verse.Sound;
namespace ArachnaeSwarm
{
[StaticConstructorOnStartup]
public class Building_CatastropheMissileSilo : Building
{
public CompPowerTrader powerComp;
public CompRefuelable refuelableComp;
public Verb_LaunchCatastropheMissile verb;
private GlobalTargetInfo longTargetInt;
private List<Building_CatastropheMissileSilo> selectedSilos;
public static readonly Texture2D FireMissionTex = ContentFinder<Texture2D>.Get("UI/Commands/Attack", true);
public override void SpawnSetup(Map map, bool respawningAfterLoad)
{
base.SpawnSetup(map, respawningAfterLoad);
this.powerComp = base.GetComp<CompPowerTrader>();
this.refuelableComp = base.GetComp<CompRefuelable>();
// This is a placeholder, verb will be initialized from the weapon ThingDef
this.verb = (Verb_LaunchCatastropheMissile)Activator.CreateInstance(typeof(Verb_LaunchCatastropheMissile));
this.verb.caster = this;
}
public override void ExposeData()
{
base.ExposeData();
Scribe_TargetInfo.Look(ref this.longTargetInt, "longTargetInt");
}
public override IEnumerable<Gizmo> GetGizmos()
{
foreach (Gizmo c in base.GetGizmos())
{
yield return c;
}
Command_Action launch = new Command_Action
{
defaultLabel = "CommandFireMission".Translate(),
defaultDesc = "CommandFireMissionDesc".Translate(),
icon = FireMissionTex,
action = new Action(this.StartChoosingDestination)
};
if (!CanFire())
{
launch.Disable("CannotFire".Translate() + ": " + GetDisabledReason());
}
yield return launch;
}
private bool CanFire()
{
return powerComp.PowerOn && refuelableComp.HasFuel;
}
private string GetDisabledReason()
{
if (!powerComp.PowerOn) return "NoPower".Translate().CapitalizeFirst();
if (!refuelableComp.HasFuel) return "NoFuel".Translate().CapitalizeFirst();
return "Unknown";
}
private void StartChoosingDestination()
{
this.selectedSilos = Find.Selector.SelectedObjects.OfType<Building_CatastropheMissileSilo>().ToList();
CameraJumper.TryJump(CameraJumper.GetWorldTarget(this), CameraJumper.MovementMode.Pan);
Find.WorldSelector.ClearSelection();
Find.WorldTargeter.BeginTargeting(
new Func<GlobalTargetInfo, bool>(this.ChoseWorldTarget),
true, // Can target self
FireMissionTex,
true,
() => GenDraw.DrawWorldRadiusRing(this.Map.Tile, this.MaxWorldRange),
(GlobalTargetInfo t) => "Select target",
null, null, true);
}
private bool ChoseWorldTarget(GlobalTargetInfo target)
{
if (!target.IsValid)
{
Messages.Message("MessageTargetInvalid".Translate(), MessageTypeDefOf.RejectInput, true);
return false;
}
int distance = Find.WorldGrid.TraversalDistanceBetween(this.Map.Tile, target.Tile, true, int.MaxValue);
if (distance > this.MaxWorldRange)
{
Messages.Message("MessageTargetBeyondMaximumRange".Translate(), this, MessageTypeDefOf.RejectInput, true);
return false;
}
MapParent mapParent = target.WorldObject as MapParent;
if (mapParent != null && mapParent.HasMap)
{
Map targetMap = mapParent.Map;
var originalMap = base.Map;
Action onFinished = () => {
if (Current.Game.CurrentMap != originalMap) Current.Game.CurrentMap = originalMap;
};
Current.Game.CurrentMap = targetMap;
Find.Targeter.BeginTargeting(
new TargetingParameters { canTargetLocations = true },
(LocalTargetInfo localTarget) =>
{
foreach (var silo in this.selectedSilos)
{
silo.FireMission(targetMap.Tile, localTarget, targetMap.uniqueID);
}
},
null, onFinished, FireMissionTex, true);
return true;
}
else // For non-map targets (caravans, sites)
{
foreach (var silo in this.selectedSilos)
{
silo.FireMission(target.Tile, new LocalTargetInfo(target.Cell), -1);
}
return true;
}
}
public void FireMission(int tile, LocalTargetInfo targ, int mapId)
{
if (!targ.IsValid)
{
this.longTargetInt = GlobalTargetInfo.Invalid;
return;
}
Map targetMap = (mapId != -1) ? Find.Maps.FirstOrDefault(m => m.uniqueID == mapId) : null;
this.longTargetInt = new GlobalTargetInfo(targ.Cell, targetMap);
this.verb.verbProps.defaultProjectile.GetModExtension<CruiseMissileProperties>(); // Ensure verb has properties.
this.verb.TryStartCastOn(this.longTargetInt);
}
public int MaxWorldRange => 99999; // Effectively global range
public GlobalTargetInfo LongTarget => longTargetInt;
}
}
```
---
## 2. 动作类: `Verb_LaunchCatastropheMissile.cs`
**路径**: `Source/ArachnaeSwarm/Verbs/Verb_LaunchCatastropheMissile.cs`
**功能**: 定义发射动作。它会创建 `WorldObject_CatastropheMissile` 并将其发射到世界地图。
```csharp
using RimWorld;
using RimWorld.Planet;
using UnityEngine;
using Verse;
namespace ArachnaeSwarm
{
public class Verb_LaunchCatastropheMissile : Verb_Shoot
{
public override bool CanHitTargetFrom(IntVec3 root, LocalTargetInfo targ)
{
return true; // Always true for world-map targeting
}
protected override bool TryCastShot()
{
Building_CatastropheMissileSilo silo = this.caster as Building_CatastropheMissileSilo;
if (silo == null || !silo.LongTarget.IsValid) return false;
WorldObject_CatastropheMissile missile = (WorldObject_CatastropheMissile)WorldObjectMaker.MakeWorldObject(
DefDatabase<WorldObjectDef>.GetNamed("CatastropheMissile_Flying")
);
missile.Tile = silo.Map.Tile;
missile.destinationTile = silo.LongTarget.Tile;
missile.destinationCell = silo.LongTarget.Cell;
missile.Projectile = this.verbProps.defaultProjectile;
Find.WorldObjects.Add(missile);
// Consume fuel
silo.refuelableComp.ConsumeFuel(silo.refuelableComp.Props.fuelConsumptionRate);
// Visual/Sound effects at launch site
MoteMaker.MakeStaticMote(silo.TrueCenter(), silo.Map, ThingDefOf.Mote_ExplosionFlash, 10f);
SoundDefOf.RocketLaunch.PlayOneShot(new TargetInfo(silo.Position, silo.Map));
return true;
}
}
}
```
---
## 3. 飞行物类: `WorldObject_CatastropheMissile.cs`
**路径**: `Source/ArachnaeSwarm/World/WorldObject_CatastropheMissile.cs`
**功能**: 模拟导弹在世界地图上的飞行,并在抵达时生成 `Projectile_CruiseMissile`
```csharp
using RimWorld.Planet;
using UnityEngine;
using Verse;
using RimWorld;
namespace ArachnaeSwarm
{
public class WorldObject_CatastropheMissile : WorldObject
{
public int destinationTile = -1;
public IntVec3 destinationCell = IntVec3.Invalid;
public ThingDef Projectile;
private int initialTile = -1;
private float traveledPct;
private const float TravelSpeed = 0.0002f; // Faster than sabot
public override void ExposeData()
{
base.ExposeData();
Scribe_Values.Look(ref destinationTile, "destinationTile", 0);
Scribe_Values.Look(ref destinationCell, "destinationCell");
Scribe_Defs.Look(ref Projectile, "Projectile");
Scribe_Values.Look(ref initialTile, "initialTile", 0);
Scribe_Values.Look(ref traveledPct, "traveledPct", 0f);
}
public override void PostAdd()
{
base.PostAdd();
this.initialTile = this.Tile;
}
private Vector3 StartPos => Find.WorldGrid.GetTileCenter(this.initialTile);
private Vector3 EndPos => Find.WorldGrid.GetTileCenter(this.destinationTile);
public override Vector3 DrawPos => Vector3.Slerp(StartPos, EndPos, traveledPct);
public override void Tick()
{
base.Tick();
traveledPct += TravelSpeed / GenMath.SphericalDistance(StartPos.normalized, EndPos.normalized);
if (traveledPct >= 1f)
{
Arrived();
}
}
private void Arrived()
{
Map targetMap = Current.Game.FindMap(this.destinationTile);
if (targetMap != null)
{
// Target is a loaded map, spawn the projectile to hit it
IntVec3 entryCell = Find.WorldGrid.GetRotatedPos(new IntVec2(0, 1), targetMap.info.parent.Rotation).ToIntVec3() * (targetMap.Size.x / 2);
entryCell.y = 0;
entryCell.z += targetMap.Size.z / 2;
Projectile_CruiseMissile missile = (Projectile_CruiseMissile)GenSpawn.Spawn(this.Projectile, entryCell, targetMap, WipeMode.Vanish);
missile.Launch(this, this.destinationCell, this.destinationCell, ProjectileHitFlags.IntendedTarget);
}
else
{
// Target is not a loaded map (e.g., caravan, site), do direct damage
GenExplosion.DoExplosion(
this.destinationCell,
null, // No map
this.Projectile.GetModExtension<CruiseMissileProperties>()?.customExplosionRadius ?? 5f,
this.Projectile.GetModExtension<CruiseMissileProperties>()?.customDamageDef ?? DamageDefOf.Bomb,
null, // Launcher
this.Projectile.GetModExtension<CruiseMissileProperties>()?.customDamageAmount ?? 50
);
}
Find.WorldObjects.Remove(this);
}
}
}
---
## 4. XML
XML定义
### 4.1 : `ThingDef_Building_CatastropheMissileSilo.xml`
****: `1.6/Defs/ThingDefs_Buildings/ThingDef_Building_CatastropheMissileSilo.xml`
****: `Building_CatastropheMissileSilo`
```xml
<?xml version="1.0" encoding="utf-8" ?>
<Defs>
<ThingDef ParentName="BuildingBase">
<defName>CatastropheMissileSilo</defName>
<label></label>
<description></description>
<thingClass>ArachnaeSwarm.Building_CatastropheMissileSilo</thingClass>
<graphicData>
<texPath>Things/Building/Security/ShipMissileTurret</texPath> <!-- Placeholder texture -->
<graphicClass>Graphic_Single</graphicClass>
<drawSize>(3,3)</drawSize>
</graphicData>
<size>(3,3)</size>
<statBases>
<MaxHitPoints>500</MaxHitPoints>
<WorkToBuild>8000</WorkToBuild>
<Mass>1000</Mass>
<Flammability>0.5</Flammability>
</statBases>
<costList>
<Steel>400</Steel>
<Plasteel>150</Plasteel>
<ComponentIndustrial>10</ComponentIndustrial>
<ComponentSpacer>4</ComponentSpacer>
</costList>
<comps>
<li Class="CompProperties_Power">
<compClass>CompPowerTrader</compClass>
<basePowerConsumption>500</basePowerConsumption>
</li>
<li Class="CompProperties_Refuelable">
<fuelConsumptionRate>10</fuelConsumptionRate> <!-- How much fuel per launch -->
<fuelCapacity>10.0</fuelCapacity>
<fuelFilter>
<thingDefs>
<li>Chemfuel</li>
</thingDefs>
</fuelFilter>
<showFuelGizmo>true</showFuelGizmo>
</li>
</comps>
<verbs>
<!-- This defines the weapon itself -->
<li>
<verbClass>ArachnaeSwarm.Verb_LaunchCatastropheMissile</verbClass>
<hasStandardCommand>false</hasStandardCommand>
<defaultProjectile>Projectile_CatastropheMissile</defaultProjectile>
</li>
</verbs>
<building>
<ai_combatDangerous>true</ai_combatDangerous>
</building>
<researchPrerequisites>
<li>ShipbuildingBasics</li> <!-- Placeholder research -->
</researchPrerequisites>
</ThingDef>
</Defs>
```
### 4.2 武器/投射物定义: `ThingDef_Projectile_CatastropheMissile.xml`
**路径**: `1.6/Defs/ThingDefs_Misc/ThingDef_Projectile_CatastropheMissile.xml`
**功能**: 定义导弹本身 (`Projectile_CatastropheMissile`) 和它作为投射物的行为 (`ThingDef` of `Projectile_CruiseMissile`)。这里是配置集束弹头和弹道参数的地方。
```xml
<?xml version="1.0" encoding="utf-8" ?>
<Defs>
<ThingDef ParentName="BaseBullet">
<defName>Projectile_CatastropheMissile</defName>
<label>“天灾”巡航导弹</label>
<thingClass>ArachnaeSwarm.Projectile_CruiseMissile</thingClass>
<graphicData>
<texPath>Things/Projectile/Missile</texPath> <!-- Placeholder texture -->
<graphicClass>Graphic_Single</graphicClass>
</graphicData>
<projectile>
<damageDef>Bomb</damageDef>
<damageAmountBase>200</damageAmountBase>
<speed>10</speed> <!-- This speed is relative to the bezier curve, not linear -->
<explosionRadius>5.9</explosionRadius>
<soundExplode>MortarBomb_Explode</soundExplode>
</projectile>
<modExtensions>
<li Class="ArachnaeSwarm.CruiseMissileProperties">
<!-- Main Explosion -->
<customDamageDef>Bomb</customDamageDef>
<customDamageAmount>150</customDamageAmount>
<customExplosionRadius>5.9</customExplosionRadius>
<customSoundExplode>MortarBomb_Explode</customSoundExplode>
<!-- Sub Explosions (Cluster) -->
<useSubExplosions>true</useSubExplosions>
<subExplosionCount>8</subExplosionCount>
<subExplosionRadius>2.9</subExplosionRadius>
<subExplosionDamage>50</subExplosionDamage>
<subExplosionSpread>15</subExplosionSpread>
<subDamageDef>Bomb</subDamageDef>
<subSoundExplode>Fragment_Explode</subSoundExplode>
<!-- Trajectory Parameters -->
<bezierArcHeightFactor>0.05</bezierArcHeightFactor>
<bezierMinArcHeight>5</bezierMinArcHeight>
<bezierMaxArcHeight>20</bezierMaxArcHeight>
<bezierHorizontalOffsetFactor>0.1</bezierHorizontalOffsetFactor>
<bezierSideOffsetFactor>0.2</bezierSideOffsetFactor>
<bezierRandomOffsetScale>0.5</bezierRandomOffsetScale>
</li>
</modExtensions>
</ThingDef>
</Defs>
```
### 4.3 飞行物定义: `WorldObjectDef_CatastropheMissile.xml`
**路径**: `1.6/Defs/WorldObjectDefs/WorldObjectDef_CatastropheMissile.xml`
**功能**: 定义在世界地图上飞行的那个导弹实体。
```xml
<?xml version="1.0" encoding="utf-8" ?>
<Defs>
<WorldObjectDef>
<defName>CatastropheMissile_Flying</defName>
<label>巡航导弹</label>
<worldObjectClass>ArachnaeSwarm.WorldObject_CatastropheMissile</worldObjectClass>
<texture>World/WorldObjects/Missile</texture> <!-- Placeholder texture -->
<expandingIcon>true</expandingIcon>
<canBeTargeted>false</canBeTargeted>
<canHavePlayerEnemyRelation>false</canHavePlayerEnemyRelation>
<useDynamicDrawer>true</useDynamicDrawer>
</WorldObjectDef>
</Defs>
```

View File

@@ -1,147 +0,0 @@
# 蜂巢意识Hive Mind系统设计文档 - v2
## 1. 需求概述
本设计旨在为 `ArachnaeSwarm` Mod 实现一个“蜂巢意识”系统。该系统包含两种角色:
- **主节点 (Master)**:蜂巢的中心。
- **子节点 (Drone)**:蜂巢的工蜂,可以有多个。
它们之间通过 `Hediff`(健康状态)进行链接,实现以下核心功能:
1. **自动绑定**:子节点在生成时,会自动在地图上寻找携带 `ARA_HiveMindMaster` Hediff 的Pawn作为主节点。如果找到多个则选择第一个进行绑定。如果找不到主节点子节点会立即死亡。
2. **主从死亡联动**:当主节点死亡时,所有与之链接的子节点也会立即死亡。
3. **链接增益**:主节点的力量会随着链接的子节点数量增加而增强。具体表现为主节点的 `Hediff` 严重性Severity会随着子节点数量的增加而提升为后续添加属性加成提供基础。
## 2. 技术方案
为了实现上述功能,我们将创建以下组件:
### 2.1. XML 定义 (使用 ARA_ 前缀)
我们将定义两个新的 `HediffDef`:
- `HediffDef: ARA_HiveMindMaster`
- **hediffClass**: `ArachnaeSwarm.Hediff_HiveMindMaster`
- **描述**: 应用于主节点,用于管理子节点链接并根据数量调整严重性。
- `HediffDef: ARA_HiveMindDrone`
- **hediffClass**: `ArachnaeSwarm.Hediff_HiveMindDrone`
- **描述**: 应用于子节点,继承自 `HediffWithTarget`,其 `target` 字段将指向主节点。
### 2.2. C# 类定义 (命名空间: ArachnaeSwarm)
我们将实现两个新的C#类
- `Hediff_HiveMindMaster`
- 继承自 `Hediff`
- 包含一个 `List<Pawn>` 类型的字段 `drones`,用于存储所有已链接的子节点。
- 提供 `RegisterDrone(Pawn drone)``DeregisterDrone(Pawn drone)` 方法,用于添加和移除子节点。
- 在每次注册/反注册后,更新自身的 `Severity` 属性,其值等于 `drones.Count`
- 重写 `PostRemoved()` 方法在主节点死亡或Hediff被移除时杀死所有已注册的子节点。
- `Hediff_HiveMindDrone`
- 继承自 `HediffWithTarget`
- 重写 `PostAdd(DamageInfo? dinfo)` 方法。在此方法中:
1. 在当前地图上扫描所有Pawn查找携带 `ARA_HiveMindMaster` Hediff 的单位。
2. 如果找到至少一个,则选择第一个作为 `target`,并调用其 `RegisterDrone` 方法将自己注册进去。
3. 如果找不到任何主节点,立即杀死自己 (`pawn.Kill(null, null)`)。
- 重写 `PostRemoved()` 方法,在自身被移除时,如果 `target` 仍然有效,则调用其 `DeregisterDrone` 方法将自己从中移除。
-`PostTick()` 或类似方法中周期性检查 `target`(主节点)是否已死亡或 Hediff 已被移除。如果是,则立即杀死自己。
## 3. 开发步骤 (TODO List)
- [ ] **步骤 1: 创建 XML 定义**
-`ArachnaeSwarm/1.6/Defs/HediffDefs/` 目录下创建 `ARA_Hediffs_HiveMind.xml` 文件。
- [ ] **步骤 2: 实现 C# 类 `Hediff_HiveMindMaster`**
-`ArachnaeSwarm/Source/ArachnaeSwarm/` 目录下创建 `Hediff_HiveMindMaster.cs` 文件并编写代码。
- [ ] **步骤 3: 实现 C# 类 `Hediff_HiveMindDrone`**
-`ArachnaeSwarm/Source/ArachnaeSwarm/` 目录下创建 `Hediff_HiveMindDrone.cs` 文件并编写代码。
- [ ] **步骤 4: 更新项目文件**
- 将新创建的 `.cs` 文件添加到 `ArachnaeSwarm.csproj` 项目文件中。
- [ ] **步骤 5: 编译和测试**
- 运行 `dotnet build` 编译项目,修复所有编译错误。
- 进入游戏进行功能测试。
## 4. 参考代码
### `HediffWithTarget.cs`
```csharp
using Verse;
namespace RimWorld
{
public class HediffWithTarget : HediffWithComps
{
public Thing target;
public override bool ShouldRemove
{
get
{
if (target != null && !(target is Pawn { Dead: not false }))
{
return base.ShouldRemove;
}
return true;
}
}
public override void ExposeData()
{
base.ExposeData();
Scribe_References.Look(ref target, "target");
}
}
}
```
### `Hediff_PsychicBond.cs`
```csharp
using Verse;
namespace RimWorld
{
public class Hediff_PsychicBond : HediffWithTarget
{
private const int HediffCheckInterval = 65;
public override string LabelBase => base.LabelBase + " (" + target?.LabelShortCap + ")";
public override bool ShouldRemove
{
get
{
if (!base.ShouldRemove)
{
return pawn.Dead;
}
return true;
}
}
public override void PostRemoved()
{
base.PostRemoved();
Gene_PsychicBonding gene_PsychicBonding = base.pawn.genes?.GetFirstGeneOfType<Gene_PsychicBonding>();
if (gene_PsychicBonding != null)
{
gene_PsychicBonding.RemoveBond();
}
else if (target != null && target is Pawn pawn)
{
pawn.genes?.GetFirstGeneOfType<Gene_PsychicBonding>()?.RemoveBond();
}
}
public override void PostTickInterval(int delta)
{
base.PostTickInterval(delta);
if (pawn.IsHashIntervalTick(65, delta))
{
Severity = (ThoughtWorker_PsychicBondProximity.NearPsychicBondedPerson(pawn, this) ? 0.5f : 1.5f);
}
}
}
}

View File

@@ -1,227 +0,0 @@
# 技术文档RimWorld 中 `specialTrainables` 的工作机制解析 (V6 - 终极证据版)
## 1. 目标
本文档旨在深入剖析 RimWorld 中 `<specialTrainables>` 标签的底层工作机制,并最终得出一套可复用的“配方”,用于让动物学会在无需训练、无衰减的情况下,执行任何原版已存在的工作。本文档包含所有必要的、完整的原版代码引用作为证据,以确保其自包含性、正确性和健壮性,可供任何人在无上下文的情况下阅读并执行。
## 2. 核心机制深度解析
`specialTrainables` 的功能是一套精巧的、深度集成在 AI 思维逻辑中的 **条件行为分支**
### 2.1. 玩家授权机制
**关键发现**: 游戏通过一个统一的 UI 列 (`PawnColumnWorker_Trainable_Special`) 来管理所有被标记为 `specialTrainable` 的技能。当玩家点击该列的复选框时,会调用 `pawn.training.SetWantedRecursive()` 来设置 `Pawn_TrainingTracker``wantedTrainables` 字段的值。
* **证据**: 以下是 `PawnColumnWorker_Trainable_Special.txt` 的完整内容,它清晰地展示了 `SetWantedRecursive` 是如何被调用的。
```csharp
using System.Text;
using UnityEngine;
using Verse;
namespace RimWorld
{
public class PawnColumnWorker_Trainable_Special : PawnColumnWorker
{
public override void DoHeader(Rect rect, PawnTable table)
{
base.DoHeader(rect, table);
MouseoverSounds.DoRegion(rect);
}
public override void DoCell(Rect rect, Pawn pawn, PawnTable table)
{
if (pawn.training != null && !pawn.RaceProps.specialTrainables.NullOrEmpty())
{
int num = (int)((rect.width - 24f) / 2f);
int num2 = Mathf.Max(3, 0);
Rect rect2 = new Rect(rect.x + (float)num, rect.y + (float)num2, 24f, 24f);
DoSpecialTrainableCheckbox(rect2, pawn, doTooltip: true);
}
}
private void DoSpecialTrainableCheckbox(Rect rect, Pawn pawn, bool doTooltip)
{
GetStatus(pawn, out var learned, out var checkOn, out var canTrain, out var _);
bool flag = checkOn;
Texture2D texChecked = (learned ? TrainingCardUtility.LearnedTrainingTex : null);
Texture2D texUnchecked = (learned ? TrainingCardUtility.LearnedNotTrainingTex : null);
Widgets.Checkbox(rect.position, ref checkOn, rect.width, !canTrain, paintable: true, texChecked, texUnchecked);
if (checkOn != flag)
{
PlayerKnowledgeDatabase.KnowledgeDemonstrated(ConceptDefOf.AnimalTraining, KnowledgeAmount.Total);
foreach (TrainableDef specialTrainable in pawn.RaceProps.specialTrainables)
{
pawn.training.SetWantedRecursive(specialTrainable, checkOn);
}
}
if (doTooltip)
{
DoSpecialTrainableTooltip(rect, pawn);
}
}
private void DoSpecialTrainableTooltip(Rect rect, Pawn pawn)
{
if (!Mouse.IsOver(rect))
{
return;
}
TooltipHandler.TipRegion(rect, delegate
{
StringBuilder stringBuilder = new StringBuilder();
foreach (TrainableDef specialTrainable in pawn.RaceProps.specialTrainables)
{
bool visible;
AcceptanceReport acceptanceReport = pawn.training.CanAssignToTrain(specialTrainable, out visible);
stringBuilder.AppendLineIfNotEmpty();
stringBuilder.AppendLine(specialTrainable.LabelCap + "\n\n" + specialTrainable.description);
if (!acceptanceReport.Accepted)
{
stringBuilder.AppendLine().AppendLine(acceptanceReport.Reason);
}
else if (!specialTrainable.prerequisites.NullOrEmpty())
{
stringBuilder.AppendLine();
foreach (TrainableDef prerequisite in specialTrainable.prerequisites)
{
if (!pawn.training.HasLearned(prerequisite))
{
stringBuilder.AppendLine("TrainingNeedsPrerequisite".Translate(prerequisite.LabelCap));
}
}
}
}
return stringBuilder.ToString();
}, (int)(rect.y * 511f + rect.x));
}
public override int GetMinWidth(PawnTable table)
{
return Mathf.Max(base.GetMinWidth(table), 24);
}
public override int GetMaxWidth(PawnTable table)
{
return Mathf.Min(base.GetMaxWidth(table), GetMinWidth(table));
}
public override int GetMinCellHeight(Pawn pawn)
{
return Mathf.Max(base.GetMinCellHeight(pawn), 24);
}
public override int Compare(Pawn a, Pawn b)
{
return GetValueToCompare(a).CompareTo(GetValueToCompare(b));
}
private int GetValueToCompare(Pawn pawn)
{
if (pawn.training == null || pawn.RaceProps.specialTrainables.NullOrEmpty())
{
return int.MinValue;
}
GetStatus(pawn, out var learned, out var checkOn, out var canTrain, out var visible);
if (learned)
{
return 4;
}
if (!visible)
{
return 0;
}
if (!canTrain)
{
return 1;
}
if (!checkOn)
{
return 2;
}
return 3;
}
private static void GetStatus(Pawn pawn, out bool learned, out bool checkOn, out bool canTrain, out bool visible)
{
learned = true;
checkOn = true;
canTrain = true;
visible = false;
foreach (TrainableDef specialTrainable in pawn.RaceProps.specialTrainables)
{
if (!pawn.training.HasLearned(specialTrainable))
{
learned = false;
}
if (!pawn.training.GetWanted(specialTrainable))
{
checkOn = false;
}
if (!pawn.training.CanAssignToTrain(specialTrainable, out var visible2))
{
canTrain = false;
}
if (visible2)
{
visible = true;
}
}
}
}
}
```
### 2.2. AI 决策机制
* **关键发现**: 我们可以为每个技能创建一个继承自 `ThinkNode_Conditional` 的决策节点,在 `Satisfied` 方法中通过检查 `pawn.training.GetWanted(def)` 来判断玩家是否授权。
## 3. 最终实施蓝图
### 阶段一:核心机制实现 (通用)
* **1.1: 实现瞬间训练 (`CompInstantTrain.cs`)**: 创建一个 `ThingComp`,在 `PostSpawnSetup` 中调用 `pawn.training.Train(def, null, true)`。
* **1.2: 引入 Harmony**: 在项目中添加 `0Harmony.dll` 引用并创建 `MainHarmony.cs` 初始化补丁。
* **1.3: 阻止训练衰减 (`Patch_TrainingTracker_TickRare.cs`)**: 创建一个对 `Pawn_TrainingTracker.TrainingTrackerTickRare` 的前缀补丁,对特殊动物返回 `false`。
### 阶段二:实现“种植” (`Growing`) 功能
* **2.1: (XML) 创建 `TrainableDef`**: 创建 `Defs/TrainableDefs/ARA_Sowing.xml`,定义 `ARA_Sowing`,并设 `<specialTrainable>true</specialTrainable>`。
* **2.2: (C#) 创建 AI 决策节点**: 创建 `Source/ArachnaeSwarm/ThinkNode_ConditionalAnimalShouldSow.cs`,并使用静态缓存 Def。
```csharp
using Verse;
using Verse.AI;
namespace ArachnaeSwarm
{
[DefOf]
public static class ARA_TrainableDefOf
{
public static TrainableDef ARA_Sowing;
public static TrainableDef ARA_PlantCutting;
static ARA_TrainableDefOf() { DefOfHelper.EnsureInitializedInCtor(typeof(ARA_TrainableDefOf)); }
}
public class ThinkNode_ConditionalAnimalShouldSow : ThinkNode_Conditional
{
protected override bool Satisfied(Pawn pawn)
{
if (pawn.training == null) return false;
return pawn.training.HasLearned(ARA_TrainableDefOf.ARA_Sowing) &&
pawn.training.GetWanted(ARA_TrainableDefOf.ARA_Sowing);
}
}
}
```
* **2.3: (XML) 关联组件**:
* **PawnKindDef**: 在 `comps` 列表中添加 `CompProperties_InstantTrain` 并配置 `trainables`。
* **ThingDef**: 在 `specialTrainables` 列表中添加 `<li>ARA_Sowing</li>`。
* **ThinkTreeDef**: 添加 `<li Class="ArachnaeSwarm.ThinkNode_ConditionalAnimalShouldSow">` 节点,其子节点为 `<li Class="RimWorld.JobGiver_Work"><workType>Growing</workType></li>`。
### 阶段三:实现“植物割除” (`PlantCutting`) 功能
此阶段完全重复阶段二的模式,仅替换相应的名称 (`ARA_PlantCutting`, `ThinkNode_ConditionalAnimalShouldPlantCut`, `workType: PlantCutting`)。
### 阶段四:最终审查与打包
* **4.1**: 审查所有代码和 XML。
* **4.2**: 编译并进行游戏内测试。

View File

@@ -1,787 +0,0 @@
<thing Class="Pawn">
<def>Human</def>
<tickDelta>2</tickDelta>
<id>Human846</id>
<map>0</map>
<pos>(146, 0, 131)</pos>
<rot>1</rot>
<faction>Faction_18</faction>
<questTags IsNull="True" />
<spawnedTick>0</spawnedTick>
<despawnedTick>-1</despawnedTick>
<beenRevealed>True</beenRevealed>
<targetHolder>null</targetHolder>
<lastStudiedTick>-9999999</lastStudiedTick>
<AlienRaces_AlienComp>
<addonVariants />
<addonColors />
<colorChannels>
<keys>
<li>base</li>
<li>hair</li>
<li>skin</li>
<li>skinBase</li>
<li>tattoo</li>
<li>favorite</li>
<li>ideo</li>
<li>mech</li>
</keys>
<values>
<li>
<first>RGBA(1.000, 1.000, 1.000, 1.000)</first>
<second>RGBA(1.000, 1.000, 1.000, 1.000)</second>
</li>
<li>
<first>RGBA(0.343, 0.310, 0.288, 1.000)</first>
</li>
<li>
<first>RGBA(1.000, 0.937, 0.788, 1.000)</first>
</li>
<li>
<first>RGBA(1.000, 0.937, 0.788, 1.000)</first>
</li>
<li>
<first>RGBA(1.000, 0.937, 0.788, 0.800)</first>
</li>
<li>
<first>RGBA(0.890, 0.451, 1.000, 1.000)</first>
<second>RGBA(0.890, 0.451, 1.000, 1.000)</second>
</li>
<li>
<first>RGBA(0.600, 0.500, 0.900, 1.000)</first>
<second>RGBA(0.549, 0.458, 0.824, 1.000)</second>
</li>
<li>
<first>RGBA(0.000, 0.737, 0.847, 1.000)</first>
</li>
</values>
</colorChannels>
<colorChannelLinks>
<keys />
<values />
</colorChannelLinks>
<headVariant>0</headVariant>
<bodyVariant>0</bodyVariant>
<headMaskVariant>0</headMaskVariant>
<bodyMaskVariant>0</bodyMaskVariant>
</AlienRaces_AlienComp>
<kindDef>Colonist</kindDef>
<gender>Female</gender>
<name Class="NameTriple">
<first>Yue</first>
<nick>Moon</nick>
<last>Ren</last>
</name>
<deadlifeDustFaction>null</deadlifeDustFaction>
<mindState>
<meleeThreat>null</meleeThreat>
<enemyTarget>null</enemyTarget>
<knownExploder>null</knownExploder>
<lastMannedThing>null</lastMannedThing>
<droppedWeapon>null</droppedWeapon>
<lastAttackedTarget>(0, 0, 0)</lastAttackedTarget>
<thinkData>
<keys />
<values />
</thinkData>
<lastJobTag>Idle</lastJobTag>
<nextApparelOptimizeTick>6673</nextApparelOptimizeTick>
<lastEngageTargetTick>-99999</lastEngageTargetTick>
<lastAttackTargetTick>-99999</lastAttackTargetTick>
<canFleeIndividual>True</canFleeIndividual>
<lastMeleeThreatHarmTick>-99999</lastMeleeThreatHarmTick>
<duty IsNull="True" />
<mentalStateHandler>
<curState IsNull="True" />
</mentalStateHandler>
<mentalBreaker />
<mentalFitGenerator />
<inspirationHandler>
<curState IsNull="True" />
</inspirationHandler>
<priorityWork>
<prioritizedCell>(-1000, -1000, -1000)</prioritizedCell>
</priorityWork>
<lastSelfTendTick>-99999</lastSelfTendTick>
<breachingTarget IsNull="True" />
<babyAutoBreastfeedMoms>
<keys />
<values />
</babyAutoBreastfeedMoms>
<babyCaravanBreastfeed>
<keys />
<values />
</babyCaravanBreastfeed>
<resurrectTarget IsNull="True" />
<lastRangedHarmTick>0</lastRangedHarmTick>
<lastDayInteractionTick>16777</lastDayInteractionTick>
</mindState>
<jobs>
<curJob>
<commTarget>null</commTarget>
<verbToUse>null</verbToUse>
<bill>null</bill>
<lord>null</lord>
<quest>null</quest>
<def>GotoWander</def>
<loadID>221</loadID>
<targetA>(156, 0, 116)</targetA>
<targetQueueA IsNull="True" />
<targetQueueB IsNull="True" />
<countQueue IsNull="True" />
<startTick>1576</startTick>
<checkOverrideOnExpire>True</checkOverrideOnExpire>
<placedThings IsNull="True" />
<locomotionUrgency>Walk</locomotionUrgency>
<jobGiverThinkTree>Humanlike</jobGiverThinkTree>
<psyfocusTargetLast>-1</psyfocusTargetLast>
<ability>null</ability>
<source>null</source>
<interactableIndex>-1</interactableIndex>
<lastJobGiverKey>-1672709817</lastJobGiverKey>
</curJob>
<curDriver Class="JobDriver_Goto">
<curToilIndex>0</curToilIndex>
<ticksLeftThisToil>-203</ticksLeftThisToil>
<startTick>1576</startTick>
<locomotionUrgencySameAs>null</locomotionUrgencySameAs>
</curDriver>
<jobQueue>
<jobs />
</jobQueue>
<formingCaravanTick>-1</formingCaravanTick>
</jobs>
<stances>
<stunner>
<showStunMote>True</showStunMote>
<adaptationTicksLeft>
<keys />
<values />
</adaptationTicksLeft>
</stunner>
<stagger />
<curStance Class="Stance_Mobile" />
</stances>
<infectionVectors>
<givenPrearrival>True</givenPrearrival>
<pathways>
<keys />
<values />
</pathways>
</infectionVectors>
<verbTracker>
<verbs>
<li Class="Verb_MeleeAttackDamage">
<loadID>Thing_Human846_0_Smash</loadID>
<currentTarget>(0, 0, 0)</currentTarget>
<currentDestination>(0, 0, 0)</currentDestination>
<lastShotTick>-999999</lastShotTick>
<canHitNonTargetPawnsNow>True</canHitNonTargetPawnsNow>
</li>
<li Class="Verb_MeleeAttackDamage">
<loadID>Thing_Human846_1_Smash</loadID>
<currentTarget>(0, 0, 0)</currentTarget>
<currentDestination>(0, 0, 0)</currentDestination>
<lastShotTick>-999999</lastShotTick>
<canHitNonTargetPawnsNow>True</canHitNonTargetPawnsNow>
</li>
<li Class="Verb_MeleeAttackDamage">
<loadID>Thing_Human846_2_Bite</loadID>
<currentTarget>(0, 0, 0)</currentTarget>
<currentDestination>(0, 0, 0)</currentDestination>
<lastShotTick>-999999</lastShotTick>
<canHitNonTargetPawnsNow>True</canHitNonTargetPawnsNow>
</li>
<li Class="Verb_MeleeAttackDamage">
<loadID>Thing_Human846_3_Smash</loadID>
<currentTarget>(0, 0, 0)</currentTarget>
<currentDestination>(0, 0, 0)</currentDestination>
<lastShotTick>-999999</lastShotTick>
<canHitNonTargetPawnsNow>True</canHitNonTargetPawnsNow>
</li>
</verbs>
</verbTracker>
<natives>
<verbTracker>
<verbs IsNull="True" />
</verbTracker>
</natives>
<meleeVerbs>
<curMeleeVerb>null</curMeleeVerb>
<terrainVerbs IsNull="True" />
</meleeVerbs>
<rotationTracker />
<pather>
<nextCell>(147, 0, 130)</nextCell>
<nextCellCostLeft>48</nextCellCostLeft>
<nextCellCostInitial>50</nextCellCostInitial>
<peMode>OnCell</peMode>
<cellsUntilClamor>4</cellsUntilClamor>
<lastEnteredCellTick>1777</lastEnteredCellTick>
<lastMovedTick>1779</lastMovedTick>
<destination>(156, 0, 116)</destination>
</pather>
<carryTracker>
<innerContainer>
<maxStacks>1</maxStacks>
<innerList />
</innerContainer>
</carryTracker>
<apparel>
<wornApparel>
<innerList>
<li>
<def>Apparel_Pants</def>
<id>Apparel_Pants847</id>
<health>130</health>
<stackCount>1</stackCount>
<stuff>Synthread</stuff>
<questTags IsNull="True" />
<despawnedTick>-1</despawnedTick>
<quality>Normal</quality>
<sourcePrecept>null</sourcePrecept>
<everSeenByPlayer>True</everSeenByPlayer>
<abilities />
</li>
<li>
<def>Apparel_Parka</def>
<id>Apparel_Parka848</id>
<health>235</health>
<stackCount>1</stackCount>
<stuff>Synthread</stuff>
<questTags IsNull="True" />
<despawnedTick>-1</despawnedTick>
<quality>Normal</quality>
<sourcePrecept>null</sourcePrecept>
<everSeenByPlayer>True</everSeenByPlayer>
<abilities />
</li>
</innerList>
</wornApparel>
<lockedApparel IsNull="True" />
<lastApparelWearoutTick>111</lastApparelWearoutTick>
</apparel>
<story>
<bodyType>Fat</bodyType>
<hairDef>Elisabeth</hairDef>
<hairColor>RGBA(0.343, 0.310, 0.288, 1.000)</hairColor>
<traits>
<allTraits>
<li>
<def>SpeedOffset</def>
<sourceGene>null</sourceGene>
<degree>-1</degree>
<suppressedBy>null</suppressedBy>
</li>
<li>
<def>Jealous</def>
<sourceGene>null</sourceGene>
<suppressedBy>null</suppressedBy>
</li>
</allTraits>
</traits>
<birthLastName>Ren</birthLastName>
<favoriteColorDef>LightPurple</favoriteColorDef>
<headType>Female_AveragePointy</headType>
<childhood>WarRefugee51</childhood>
<adulthood>MedievalMinstrel95</adulthood>
</story>
<equipment>
<equipment>
<innerList />
</equipment>
<bondedWeapon>null</bondedWeapon>
</equipment>
<drafter>
<autoUndrafter />
</drafter>
<ageTracker>
<ageBiologicalTicks>139350193</ageBiologicalTicks>
<birthAbsTicks>-297730916</birthAbsTicks>
<growth>1</growth>
<nextGrowthCheckTick>9223372036854775807</nextGrowthCheckTick>
<ageReversalDemandedAtAgeTicks>141208416</ageReversalDemandedAtAgeTicks>
</ageTracker>
<healthTracker>
<hediffSet>
<hediffs />
</hediffSet>
<surgeryBills>
<bills />
</surgeryBills>
<immunity>
<imList />
</immunity>
</healthTracker>
<records>
<records>
<vals>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>1840</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
</vals>
</records>
<battleActive>null</battleActive>
</records>
<inventory>
<itemsNotForSale />
<unpackedCaravanItems />
<innerContainer>
<innerList />
</innerContainer>
</inventory>
<filth>
<lastTerrainFilthDef>Filth_Sand</lastTerrainFilthDef>
<carriedFilth>
<li>
<def>Filth_Sand</def>
<id>Filth_Sand14506</id>
<questTags IsNull="True" />
<despawnedTick>-1</despawnedTick>
</li>
</carriedFilth>
</filth>
<roping>
<hitchingPostInt>null</hitchingPostInt>
<ropees />
</roping>
<needs>
<needs>
<li Class="Need_Mood">
<def>Mood</def>
<curLevel>0.586400032</curLevel>
<thoughts>
<memories>
<memories>
<li Class="Thought_MemorySocial">
<def>CrashedTogether</def>
<sourcePrecept>null</sourcePrecept>
<otherPawn>Thing_Human849</otherPawn>
<age>1800</age>
<opinionOffset>25</opinionOffset>
</li>
<li Class="Thought_MemorySocial">
<def>CrashedTogether</def>
<sourcePrecept>null</sourcePrecept>
<otherPawn>Thing_Human852</otherPawn>
<age>1800</age>
<opinionOffset>25</opinionOffset>
</li>
<li>
<def>NewColonyOptimism</def>
<sourcePrecept>null</sourcePrecept>
<otherPawn>null</otherPawn>
<age>1800</age>
</li>
<li Class="Thought_MemorySocialCumulative">
<def>Chitchat</def>
<sourcePrecept>null</sourcePrecept>
<otherPawn>Thing_Human852</otherPawn>
<moodPowerFactor>0.773389459</moodPowerFactor>
<age>1650</age>
<opinionOffset>0.510437071</opinionOffset>
</li>
</memories>
</memories>
</thoughts>
<recentMemory>
<lastLightTick>1711</lastLightTick>
<lastOutdoorTick>1711</lastOutdoorTick>
</recentMemory>
</li>
<li Class="Need_Food">
<def>Food</def>
<curLevel>0.751999915</curLevel>
<lastNonStarvingTick>1711</lastNonStarvingTick>
</li>
<li Class="Need_Rest">
<def>Rest</def>
<curLevel>0.879273593</curLevel>
</li>
<li Class="Need_Joy">
<def>Joy</def>
<curLevel>0.513921499</curLevel>
<tolerances>
<vals>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
</vals>
</tolerances>
<bored>
<vals>
<li>False</li>
<li>False</li>
<li>False</li>
<li>False</li>
<li>False</li>
<li>False</li>
<li>False</li>
<li>False</li>
<li>False</li>
<li>False</li>
</vals>
</bored>
</li>
<li Class="Need_Beauty">
<def>Beauty</def>
<curLevel>0.442400098</curLevel>
</li>
<li Class="Need_Comfort">
<def>Comfort</def>
<curLevel>0.47119987</curLevel>
</li>
<li Class="Need_Outdoors">
<def>Outdoors</def>
<curLevel>1</curLevel>
</li>
<li Class="Need_Chemical_Any">
<def>DrugDesire</def>
<curLevel>0.5</curLevel>
</li>
<li Class="Need_RoomSize">
<def>RoomSize</def>
<curLevel>1</curLevel>
</li>
</needs>
</needs>
<guest>
<hostFaction>null</hostFaction>
<slaveFaction>null</slaveFaction>
<joinStatus>JoinAsColonist</joinStatus>
<interactionMode>MaintainOnly</interactionMode>
<slaveInteractionMode>NoInteraction</slaveInteractionMode>
<spotToWaitInsteadOfEscaping>(-1000, -1000, -1000)</spotToWaitInsteadOfEscaping>
<lastPrisonBreakTicks>-1</lastPrisonBreakTicks>
<ideoForConversion>null</ideoForConversion>
<recruitable>False</recruitable>
<enabledNonExclusiveInteractions />
<lastResistanceInteractionData IsNull="True" />
<finalResistanceInteractionData IsNull="True" />
</guest>
<guilt />
<royalty>
<titles />
<favor>
<keys />
<values />
</favor>
<highestTitles>
<keys />
<values />
</highestTitles>
<heirs>
<keys />
<values />
</heirs>
<permits />
<abilities />
</royalty>
<social>
<directRelations>
<li>
<def>ExSpouse</def>
<otherPawn>Thing_Human849</otherPawn>
</li>
<li>
<def>Parent</def>
<otherPawn>Thing_Human853</otherPawn>
</li>
<li>
<def>Parent</def>
<otherPawn>Thing_Human857</otherPawn>
</li>
</directRelations>
<virtualRelations />
<relativeInvolvedInRescueQuest>null</relativeInvolvedInRescueQuest>
<pregnancyApproaches>
<keys />
<values />
</pregnancyApproaches>
<romanceEnableTick>-1</romanceEnableTick>
</social>
<psychicEntropy>
<limitEntropyAmount>True</limitEntropyAmount>
</psychicEntropy>
<shambler IsNull="True" />
<ownership>
<ownedBed>null</ownedBed>
<assignedMeditationSpot>null</assignedMeditationSpot>
<assignedGrave>null</assignedGrave>
<assignedThrone>null</assignedThrone>
<assignedDeathrestCasket>null</assignedDeathrestCasket>
</ownership>
<interactions>
<lastInteraction>Chitchat</lastInteraction>
<lastInteractionTime>121</lastInteractionTime>
<lastInteractionDef>Chitchat</lastInteractionDef>
</interactions>
<skills>
<skills>
<li>
<def>Shooting</def>
<level>3</level>
</li>
<li>
<def>Melee</def>
<level>5</level>
</li>
<li>
<def>Construction</def>
<level>2</level>
</li>
<li>
<def>Mining</def>
<level>2</level>
</li>
<li>
<def>Cooking</def>
<level>7</level>
</li>
<li>
<def>Plants</def>
<level>1</level>
</li>
<li>
<def>Animals</def>
<level>3</level>
<passion>Minor</passion>
</li>
<li>
<def>Crafting</def>
<level>3</level>
</li>
<li>
<def>Artistic</def>
<level>5</level>
<passion>Minor</passion>
</li>
<li>
<def>Medicine</def>
<level>2</level>
<passion>Minor</passion>
</li>
<li>
<def>Social</def>
<level>7</level>
<passion>Major</passion>
</li>
<li>
<def>Intellectual</def>
</li>
</skills>
<lastXpSinceMidnightResetTimestamp>-1</lastXpSinceMidnightResetTimestamp>
</skills>
<abilities>
<abilities />
</abilities>
<ideo>
<ideo>Ideo_10</ideo>
<previousIdeos />
<certainty>0.605378687</certainty>
<babyIdeoExposure IsNull="True" />
</ideo>
<workSettings>
<priorities>
<vals>
<li>3</li>
<li>3</li>
<li>0</li>
<li>3</li>
<li>3</li>
<li>3</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>0</li>
<li>3</li>
<li>0</li>
<li>3</li>
<li>0</li>
<li>0</li>
</vals>
</priorities>
</workSettings>
<trader IsNull="True" />
<outfits>
<curOutfit>ApparelPolicy_任意_1</curOutfit>
<overrideHandler>
<forcedAps />
</overrideHandler>
</outfits>
<drugs>
<curAssignedDrugs>DrugPolicy_社交成瘾品_1</curAssignedDrugs>
<drugTakeRecords />
</drugs>
<foodRestriction>
<curRestriction>null</curRestriction>
<allowedBabyFoodTypes IsNull="True" />
</foodRestriction>
<timetable>
<times>
<li>Sleep</li>
<li>Sleep</li>
<li>Sleep</li>
<li>Sleep</li>
<li>Sleep</li>
<li>Sleep</li>
<li>Anything</li>
<li>Anything</li>
<li>Anything</li>
<li>Anything</li>
<li>Anything</li>
<li>Anything</li>
<li>Anything</li>
<li>Anything</li>
<li>Anything</li>
<li>Anything</li>
<li>Anything</li>
<li>Anything</li>
<li>Anything</li>
<li>Anything</li>
<li>Anything</li>
<li>Anything</li>
<li>Sleep</li>
<li>Sleep</li>
</times>
</timetable>
<playerSettings>
<medCare>Best</medCare>
<allowedAreas>
<keys />
<values />
</allowedAreas>
<master>null</master>
<displayOrder>1</displayOrder>
</playerSettings>
<training IsNull="True" />
<style>
<beardDef>NoBeard</beardDef>
<faceTattoo>NoTattoo_Face</faceTattoo>
<bodyTattoo>NoTattoo_Body</bodyTattoo>
</style>
<styleObserver />
<connections>
<connectedThings />
</connections>
<inventoryStock>
<stockEntries>
<keys />
<values />
</stockEntries>
</inventoryStock>
<treeSightings>
<miniTreeSightings />
<fullTreeSightings />
<superTreeSightings />
</treeSightings>
<thinker />
<mechanitor IsNull="True" />
<genes>
<xenogenes />
<endogenes>
<li>
<def>Skin_Melanin3</def>
<pawn>Thing_Human846</pawn>
<overriddenByGene>null</overriddenByGene>
<loadID>132</loadID>
</li>
<li>
<def>Hair_MidBlack</def>
<pawn>Thing_Human846</pawn>
<overriddenByGene>null</overriddenByGene>
<loadID>133</loadID>
</li>
</endogenes>
<xenotype>Baseliner</xenotype>
</genes>
<learning IsNull="True" />
<reading>
<curAssignment>null</curAssignment>
</reading>
<creepjoiner IsNull="True" />
<duplicate />
<flight />
</thing>

View File

@@ -1,122 +0,0 @@
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<CompThingContainer>();
public override bool TryMakePreToilReservations(bool errorOnFailed)
{
return true;
}
protected override IEnumerable<Toil> 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<CompThingCarrier_Custom>();
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<CompThingContainer_Milian>();
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<CompInitiatable>();
if (initiatableComp != null)
{
initiatableComp.initiationDelayTicksOverride = initiationDelayTicks;
}
}
}
}

View File

@@ -1,158 +0,0 @@
# 开发说明书: 交互式品质生成器 (V5.1 - 最终补充版)
## 1. 核心概念
`CompInteractiveProducer` 是一个**主控制器**组件。它管理的建筑拥有一个统一的“生物质燃料”池。玩家可以通过**精确配置的燃料白/黑名单**,决定哪些物品可以作为燃料。
当玩家启动一个生产流程时,该流程会有一个**专属的、固定的总燃料消耗量**和**生产时间**。在生产过程中,组件会持续消耗燃料池,并根据燃料和温度的理想条件,计算最终产物的品质。
## 2. 架构设计
- **`CompInteractiveProducer` (控制器)**: 继承自 `ThingComp`,并实现 `IStoreSettingsParent`,负责所有逻辑。
- **`CompProperties_InteractiveProducer` (数据)**: 在 XML 中定义所有生产流程及其对应的参数,以及全局的燃料接受规则。
---
## 3. 依赖项说明
**重要**: 本组件的正常工作依赖于一个在 XML 中预先定义的 `JobDef`。交互菜单会尝试创建这个 Job 并分配给 Pawn。
**示例 `JobDef` 定义 (`Jobs.xml`):**
```xml
<?xml version="1.0" encoding="utf-8" ?>
<Defs>
<JobDef>
<defName>ARA_IncubateJob</defName>
<driverClass>ArachnaeSwarm.JobDriver_Incubate</driverClass>
<reportString>正在启动生产 TargetA.</reportString>
<allowOpportunisticPrefix>true</allowOpportunisticPrefix>
</JobDef>
</Defs>
```
*注: `JobDriver_Incubate` 是一个简单的 JobDriver其核心逻辑就是让 Pawn 走到建筑旁,然后调用 `comp.StartProduction(process)`。*
---
## 4. 实现步骤与完整代码
### **第 1 步: 定义支持精准配置的属性类**
**目的**: 创建 C# 类来映射新的、更详细的 XML 结构。
**产出代码 (属性类 V5):**
```csharp
// (代码与上一版相同,此处为简洁省略)
```
### **第 2 步: 实现完整的主组件**
**目的**: 编写最终的、包含所有新逻辑的 `CompInteractiveProducer` 类。
#### **代码解析与补充说明**
```csharp
// (此处为完整的 V5 版本 C# 代码)
// ...
// --- 交互与生产流程 ---
public override IEnumerable<FloatMenuOption> CompFloatMenuOptions(Pawn selPawn)
{
// ...
// **补充说明**: 此处创建的 Job "ARA_IncubateJob" 必须在 XML 中有对应定义。
// 该 Job 的 Driver 应包含走到 parent 旁边,然后调用 StartProduction() 的逻辑。
// ...
}
private void FinishProduction()
{
// ...
// **补充说明**: 最终品质的计算公式为:
// finalQualityScore = Clamp01( (ticksUnderOptimalConditions / totalTicks) - temperaturePenaltyPercent )
// 这意味着温度惩罚是直接从基础品质分中扣除的。
// ...
}
private void ResetProduction()
{
// **补充说明**: 此方法会清空所有生产进度。
// 如果玩家通过 Gizmo 中途取消,所有累积的“理想时间”和“温度惩罚”都会丢失。
_selectedProcess = null;
productionUntilTick = -1;
ticksUnderOptimalConditions = 0;
temperaturePenaltyPercent = 0f;
}
// --- 燃料系统方法 ---
private float GetNutritionInContainer()
{
// **性能备注**: 此方法会遍历容器。在绝大多数情况下性能良好。
// 如果 Mod 允许容器内有成百上千的物品,可考虑增加缓存,不必每帧都计算。
// ...
}
// --- IStoreSettingsParent & IThingHolder 实现 ---
// **说明**: 这些接口的实现让我们的组件能被游戏原生的运输和存储系统识别。
// GetStoreSettings() 暴露我们的配置,让小人知道可以运什么东西过来。
// GetDirectlyHeldThings() 暴露我们的内部容器,让游戏知道我们持有哪些物品。
// --- UI 与 Gizmos ---
public override string CompInspectStringExtra()
{
// ...
// **UI 设计补充**:
// 生产中: 应清晰显示 "预计品质",其计算公式为 (当前理想 tick 数 / 已进行 tick 数) - 当前温度惩罚。
// 空闲时: 除了显示总燃料,还可增加一行提示,如 "可由 [白名单Pawn名称] 启动"。
// ...
}
public override IEnumerable<Gizmo> GetGizmos()
{
// ...
// **Gizmo 设计补充**:
// 取消按钮 (Command_Action) 的 action 应直接调用 ResetProduction()。
// 可在开发者模式下增加调试按钮,如:
// - "DEV: +10 营养"
// - "DEV: 立即完成生产"
// ...
}
public override void PostDestroy(DestroyMode mode, Map previousMap)
{
base.PostDestroy(mode, previousMap);
// **边缘情况处理**: 建筑被摧毁或卸载时,清空内部容器,
// 默认情况下,容器内的物品会被丢弃在地上,这符合预期。
innerContainer.TryDropAll(parent.Position, previousMap, ThingPlaceMode.Near);
}
```
---
这份 V5.1 版本的说明书,在 V5 的基础上补充了对依赖项、UI细节、边缘情况和性能的考量使其作为开发蓝图更加健壮和周全。这应该是我们开始编码前所需要的最终版本了。
## 5. 新增组件:温度损坏组件 (CompTemperatureRuinableDamage)
### 5.1 设计目标
创建一个新的组件,用于在极端温度下对物品造成持续伤害,并在温度恢复正常时逐渐恢复损坏进度。
### 5.2 组件属性类 (CompProperties_TemperatureRuinableDamage)
- `minSafeTemperature`: 安全温度范围的最低温度
- `maxSafeTemperature`: 安全温度范围的最高温度默认100
- `progressPerDegreePerTick`: 每度温度每tick造成的损坏进度默认1E-05f
- `damagePerTick`: 每tick造成的伤害值默认1
- `recoveryRate`: 温度恢复正常时的恢复速率默认0.001f
### 5.3 组件类 (CompTemperatureRuinableDamage)
- 继承自ThingComp实现温度监控逻辑
- 当物品温度超出安全范围时根据温度差值累积损坏进度并每tick造成持续伤害
- 当温度恢复正常时,逐渐减少损坏进度而不是立即重置
- 支持保存和加载损坏进度状态
### 5.4 使用方法
在ThingDef的comps部分添加以下配置
```xml
<li Class="ArachnaeSwarm.CompProperties_TemperatureRuinableDamage">
<minSafeTemperature>13</minSafeTemperature>
<maxSafeTemperature>28</maxSafeTemperature>
<progressPerDegreePerTick>0.00005</progressPerDegreePerTick>
<damagePerTick>1</damagePerTick>
<recoveryRate>0.001</recoveryRate>
</li>
```

View File

@@ -1,134 +0,0 @@
# 中央营养供给网络设计文档 (v2)
## 1. 概述
为了解决需要手动为大量建筑补充营养(燃料)的繁琐操作,本项目旨在设计并实现一个“中央营养供给网络”。该网络由一个或多个“中央供能塔”组成,它们能够自动、无线地为地图上所有需要营养的建筑进行补给。此外,系统还支持通过辅助建筑来提升供能塔的燃料传输效率。
## 2. 设计目标
* **自动化**: 消除手动为每个建筑“加油”的需求。
* **策略性**: 引入效率加成建筑,鼓励玩家进行更优化的基地布局。
* **可扩展性**: 系统应支持多个供能塔、消费者和增效器建筑。
* **性能友好**: 设计应考虑到性能,避免在游戏后期造成明显的卡顿。
* **用户友好**: 玩家应能轻松理解网络的工作状态,并对其进行基本控制。
## 3. 系统架构
本系统采用**去中心化**的设计。每个“中央供能塔”都是一个独立的网络节点。同时,它也作为一个可以被其他设施影响的单位,以接收效率加成。
### 3.1. 主要组件与建筑
#### a. `CompNutrientNetworkFeeder` (供能塔核心组件)
* **职责**:
1. **扫描与链接**: 定期扫描地图,查找所有在范围内的消费者建筑,并维护一个持久化的 `linkedConsumers` 列表。
2. **计算效率**: 检查自身 `Stat`,获取由“增效器”提供的 `NutrientTransmissionEfficiency` 总加成。
3. **分配与供能**: 从 `linkedConsumers` 列表中选择目标,计算需要补充的燃料量 `X`。根据效率加成,从自身燃料库中扣除 `X * (1 - EfficiencyBonus)` 的燃料。
4. **冲突避免**: 使用一个临时标记 `isBeingFueledNow` 来防止多个供能塔同时为一个目标补充。
5. **视觉效果**: 借鉴原版 `CompFacility`,在选中供能塔时,持续绘制到所有已连接消费者的连接线。
* **依附于**: 中央供能塔 (`Building_NutrientTower`)。
#### b. `CompRefuelableNutrition` (修改现有组件)
* **职责**:
1. **接收燃料**: 增加一个新的公共方法 `ReceiveFuelFromNetwork(float amount)`
2. **状态标记**: 增加一个临时的 `bool isBeingFueledNow` 字段。
#### c. “营养网络增效器” (新建筑)
* **`ThingDef`**: 一个新的建筑,例如 `ARA_NutrientNetworkBooster`
* **`comps`**:
* 拥有 `<li Class="CompProperties_Facility">`
*`statOffsets` 中提供一个新的 `StatDef` 加成:`<NutrientTransmissionEfficiency>0.1</NutrientTransmissionEfficiency>`
#### d. “中央供能塔” (修改)
* **`comps`**:
* 除了原计划的组件,新增 `<li Class="CompProperties_AffectedByFacilities">`,使其可以链接到“增效器”。
### 3.2. 工作流程 (Mermaid)
```mermaid
graph TD
subgraph "增效器 (Booster)"
C(Building_Booster)
C -- 提供加成 --> B
end
subgraph "中央供能塔 (Provider)"
B(Building_NutrientTower)
B -- 拥有 --> B1[CompAffectedByFacilities]
B -- 拥有 --> B2[CompNutrientNetworkFeeder]
end
subgraph "消费者建筑 (Consumer)"
A(Building_Existing)
end
B2 -- 1. 扫描并连接 --> A
B2 -- 2. 选中时绘制连接线 --> A
C -- "statOffsets" --> B1
B2 -- 3. 从B1读取效率加成 --> B1
B2 -- 4. 根据效率进行供能 --> A
```
## 4. 实现细节与挑战
* **自定义StatDef**: 需要在XML中创建一个新的 `StatDef`,名为 `NutrientTransmissionEfficiency`
* **性能优化**: 保持合理的扫描间隔。
* **范围限制**: 供能塔的服务范围和增效器的影响范围都应是可配置的。
* **视觉反馈**:
* 供能塔 -> 消费者:绘制连接线,颜色可根据消费者燃料状态变化。
* 增效器 -> 供能塔:使用原版的 `CompFacility` 机制,在选中增效器时会自动显示其影响的供能塔。
## 5. XML 定义 (示例)
#### a. 新的 StatDef
```xml
<StatDef>
<defName>NutrientTransmissionEfficiency</defName>
<label>nutrient transmission efficiency</label>
<description>Reduces the amount of fuel consumed when transfering nutrients wirelessly. The final cost is multiplied by (1 - efficiency).</description>
<category>Building</category>
<defaultBaseValue>0</defaultBaseValue>
<minValue>0</minValue>
<toStringStyle>PercentZero</toStringStyle>
</StatDef>
```
#### b. 增效器 ThingDef
```xml
<ThingDef ParentName="BuildingBase">
<defName>ARA_NutrientNetworkBooster</defName>
<!-- ...其他属性... -->
<comps>
<li Class="CompProperties_Facility">
<linkableBuildings>
<li>ARA_NutrientNetworkTower</li>
</linkableBuildings>
<statOffsets>
<NutrientTransmissionEfficiency>0.1</NutrientTransmissionEfficiency>
</statOffsets>
</li>
</comps>
</ThingDef>
```
#### c. 供能塔 ThingDef
```xml
<ThingDef ParentName="BuildingBase">
<defName>ARA_NutrientNetworkTower</defName>
<!-- ...其他属性... -->
<comps>
<li Class="CompProperties_RefuelableNutrition">...</li>
<li Class="ArachnaeSwarm.CompProperties_NutrientNetworkFeeder">...</li>
<li Class="CompProperties_AffectedByFacilities">
<linkableFacilities>
<li>ARA_NutrientNetworkBooster</li>
</linkableFacilities>
</li>
<!-- ...Flickable, Breakdownable, Power... -->
</comps>
</ThingDef>
```
---
这份文档现在包含了我们所有的设计决策,形成了一个完整且强大的功能蓝图。

View File

@@ -1,558 +0,0 @@
# 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>
```

View File

@@ -1,188 +0,0 @@
# 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<Thing>(this, false, LookMode.Deep, true);
}
// --- IThingHolder 接口实现 ---
public IThingHolder ParentHolder
{
get
{
// 对于Hediff来说它的父容器就是持有它的Pawn
return this.pawn;
}
}
public void GetChildHolders(List<IThingHolder> 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<Pawn>(ref this.victim, "victim", false);
// 2. 深度保存容器内容
// 'Scribe_Deep.Look' 会序列化容器内的Pawn的所有数据
Scribe_Deep.Look<ThingOwner>(ref this.innerContainer, "innerContainer", new object[]
{
this
});
// 兼容性处理:确保旧存档在加载后也能正确初始化容器
if (Scribe.mode == LoadSaveMode.PostLoadInit)
{
if (this.innerContainer == null)
{
this.innerContainer = new ThingOwner<Thing>(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<ThingOwner>(ref this.innerContainer, ...)`: 这行代码是魔法发生的地方。当游戏保存时,`Scribe_Deep` 会深入到 `innerContainer` 内部,并因为我们之前设置了 `LookMode.Deep`,它会对容器里的每一个 `Pawn` 进行递归式的深度保存。
* 当游戏加载时,`Scribe_Deep` 会读取存档中的数据,重建 `innerContainer`,并利用深度保存的数据重建一个与存入时状态完全一致的 `Pawn`
## 总结
通过实现 `IThingHolder` 接口并利用一个配置为 `LookMode.Deep``ThingOwner` 容器,我们可以将一个 `Hediff` 转变为一个功能强大的、能够随宿主移动的“Pawn胶囊”。这个“胶囊”可以安全地携带一个`Pawn`穿越存档的海洋,确保其数据的完整性和一致性。
这项技术是实现诸如吞噬、俘获、传送、特殊休眠仓等高级Mod功能的基石。

View File

@@ -1,618 +0,0 @@
# “抱脸虫夺舍”技能实现说明书 (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`并将其附加到目标身上,从而启动整个夺舍流程。
## 3. 最终数据迁移规范 (Final Data Transfer Specification)
通过对真实存档文件 (`Human.xml`) 的深度分析,我们最终确定了“灵魂”与“肉体”的数据边界。`PawnDataUtility.TransferSoul` 方法将严格遵循以下规范进行数据迁移:
### 3.1 必须复制的“灵魂”数据
这些数据定义了Pawn的身份、经历、思想和核心能力将**完全从抱脸虫(源)复制到宿主(目标)**。
- **核心身份 (`Name`, `Story`, `Faction`)**:
- `Name`: 姓名与昵称。
- `Story`: 童年和成年背景 (`Childhood`, `Adulthood`)。
- `Traits`: 所有特性。
- `Faction`: 所属阵营。
- **成长与经历 (`Skills`, `Records`)**:
- `Skills`: 所有技能的等级、经验和热情。
- `Records`: 全部生平记录 (如击杀数、建造数等)。
- **思想与设定 (`Needs`, `WorkSettings`, etc.)**:
- `Needs`: 主要是指`thoughts.memories` (思想和记忆)。
- `WorkSettings`: 工作优先级。
- `Timetable`: 时间表。
- `PlayerSettings`: 玩家设定 (如医疗策略)。
- `Ownership`: 对床、王座等的所有权。
- `Outfits` & `Drugs`: 穿着和药物策略。
- `FoodRestriction`: 食物策略。
- **DLC核心数据 (`Ideo`, `Royalty`)**:
- `Ideo`: 完整的信仰体系。
- `Royalty`: 完整的贵族系统,包括头衔、恩惠、许可、灵能和相关技能 (`abilities`)。
- **社交 (`Relations`)**:
- 将采用**简化处理**:清空目标的旧关系,然后只复制源的**非亲属**直接关系 (如朋友、对手、爱人)。这可以避免破坏家族树。
### 3.2 必须保留的“肉体”数据
这些数据属于物理身体的范畴,在夺舍过程中将**完全保留宿主原有的数据**,不进行任何复制。
- **健康与生理 (`Health`, `Age`)**:
- `Health`: 所有伤口、疤痕、疾病和植入物。
- `Age`: 生物年龄和时间年龄。
- **外观与基因 (`Style`, `Genes`, `BodyType`)**:
- `Style`: 发型、胡须、纹身。
- `Genes`: 所有内生和异种基因。
- `BodyType`, `HeadType`, `HairColor`: 身体类型、头型和发色。
- **装备与物品 (`Apparel`, `Equipment`, `Inventory`)**:
- `Apparel`: 身上穿着的衣物。
- `Equipment`: 手中持有的装备。
- `Inventory`: 物品栏中的物品。
- **物理状态 (`Position`, `Stances`, `Pather`)**:
- Pawn在世界中的位置、姿态和寻路信息。
---
```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<Thing>(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<IThingHolder> 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<WorkTypeDef>.AllDefs)
{
bodyTarget.workSettings.SetPriority(workDef, soulSource.workSettings.GetPriority(workDef));
}
}
// 时间表
if (soulSource.timetable != null && bodyTarget.timetable != null)
{
bodyTarget.timetable.times = new List<TimeAssignmentDef>(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
<!-- 路径: 1.6/1.6/Defs/HediffDefs/ARA_Hediffs_Possession.xml -->
<?xml version="1.0" encoding="utf-8" ?>
<Defs>
<HediffDef>
<!-- 内部唯一的定义名 -->
<defName>ARA_Possession</defName>
<!-- 游戏中显示的标签 -->
<label>阿拉克涅原虫寄生</label>
<!-- 鼠标悬浮时的描述 -->
<description>这个生物的身体正被另一个实体所控制。</description>
<!-- 关键将这个XML定义与我们的C#实现类关联起来 -->
<hediffClass>ArachnaeSwarm.Possession.Hediff_Possession</hediffClass>
<!-- 其他标准Hediff属性 -->
<isBad>false</isBad>
<scenarioCanAdd>false</scenarioCanAdd>
<maxSeverity>1.0</maxSeverity>
<stages>
<li>
<label>被寄生</label>
</li>
</stages>
</HediffDef>
</Defs>
```
### 4.2 `ARA_Possession_Defs.xml` - 定义技能、种族和身体
这个文件定义了技能本身,以及我们的“阿拉克涅原虫”作为一个完整的生物所需的一切。
```xml
<!-- 路径: 1.6/1.6/Defs/Misc/ARA_Possession_Defs.xml -->
<?xml version="1.0" encoding="utf-8" ?>
<Defs>
<!-- ==================== 技能定义 ==================== -->
<AbilityDef>
<defName>ARA_Ability_Possess</defName>
<label>阿拉克涅寄生</label>
<description>将你的意识注入另一个生物的身体,完全占据它。</description>
<iconPath>UI/Abilities/Possess</iconPath> <!-- TODO: 需要一张图标 -->
<cooldownTicks>600</cooldownTicks>
<verbProperties>
<verbClass>Verb_CastAbility</verbClass>
<warmupTime>1.5</warmupTime>
<range>5.9</range>
<targetParams>
<canTargetPawns>true</canTargetPawns>
<canTargetBuildings>false</canTargetBuildings>
<canTargetSelf>false</canTargetSelf>
<canTargetLocations>false</canTargetLocations>
</targetParams>
</verbProperties>
<comps>
<!-- 关键将这个技能与我们的C#属性类关联起来 -->
<li Class="ArachnaeSwarm.Possession.CompProperties_AbilityPossess"/>
</comps>
</AbilityDef>
<!-- ==================== 生物AI定义 (可选) ==================== -->
<ThinkTreeDef>
<defName>ARA_Facehugger</defName>
<insertTag>Humanlike_PostMentalState</insertTag>
<insertPriority>100</insertPriority>
<thinkRoot Class="ThinkNode_Priority">
<subNodes>
<!-- 在这里可以为抱脸虫添加自定义AI例如当它空闲时自动寻找宿主 -->
</subNodes>
</thinkRoot>
</ThinkTreeDef>
<!-- ==================== 生物类型定义 ==================== -->
<PawnKindDef>
<defName>ARA_Facehugger</defName>
<label>阿拉克涅原虫</label>
<!-- 关联下面的种族定义 -->
<race>ARA_FacehuggerRace</race>
<combatPower>25</combatPower>
<lifeStages>
<li>
<bodyGraphicData>
<texPath>Things/Pawn/Animal/ARA_Facehugger</texPath> <!-- TODO: 需要贴图 -->
<drawSize>0.8</drawSize>
</bodyGraphicData>
<dessicatedBodyGraphicData>
<texPath>Things/Pawn/Animal/Dessicated/CritterDessicated</texPath>
<drawSize>0.8</drawSize>
</dessicatedBodyGraphicData>
</li>
</lifeStages>
<aiThinkTree>ARA_Facehugger</aiThinkTree>
<abilities>
<!-- 赋予该生物我们的夺舍技能 -->
<li>ARA_Ability_Possess</li>
</abilities>
</PawnKindDef>
<!-- ==================== 种族定义 ==================== -->
<ThingDef ParentName="AnimalThingBase">
<defName>ARA_FacehuggerRace</defName>
<label>阿拉克涅原虫</label>
<description>一种小型的、脆弱的寄生生物,其唯一的生存目的就是寻找并占据一个更强大的宿主。它通过将自己的意识注入目标来完成这一过程。</description>
<statBases>
<MoveSpeed>4.0</MoveSpeed>
<MarketValue>50</MarketValue>
<ComfyTemperatureMin>-10</ComfyTemperatureMin>
<ComfyTemperatureMax>50</ComfyTemperatureMax>
</statBases>
<tools>
<li>
<label>tiny claws</label>
<capacities>
<li>Scratch</li>
</capacities>
<power>2</power>
<cooldownTime>1.5</cooldownTime>
</li>
</tools>
<race>
<thinkTreeMain>Animal</thinkTreeMain>
<!-- 关联下面的身体定义 -->
<body>ARA_FacehuggerBody</body>
<baseBodySize>0.2</baseBodySize>
<baseHealthScale>0.3</baseHealthScale>
<baseHungerRate>0.1</baseHungerRate>
<lifeStageAges>
<li>
<def>AnimalAdult</def>
<minAge>0</minAge>
</li>
</lifeStageAges>
</race>
</ThingDef>
<!-- ==================== 身体结构定义 ==================== -->
<BodyDef>
<defName>ARA_FacehuggerBody</defName>
<label>facehugger</label>
<corePart>
<def>Body</def>
<height>20</height>
<depth>20</depth>
<parts>
<li>
<def>Head</def>
<coverage>0.3</coverage>
<parts>
<li>
<def>Skull</def>
<coverage>0.2</coverage>
<depth>Inside</depth>
<parts>
<li>
<def>Brain</def>
<coverage>0.1</coverage>
<depth>Inside</depth>
</li>
</parts>
</li>
<li>
<def>Eye</def>
<customLabel>left eye</customLabel>
<coverage>0.07</coverage>
</li>
<li>
<def>Eye</def>
<customLabel>right eye</customLabel>
<coverage>0.07</coverage>
</li>
</parts>
</li>
<li>
<def>Leg</def>
<customLabel>front left leg</customLabel>
<coverage>0.1</coverage>
</li>
<li>
<def>Leg</def>
<customLabel>front right leg</customLabel>
<coverage>0.1</coverage>
</li>
<li>
<def>Leg</def>
<customLabel>rear left leg</customLabel>
<coverage>0.1</coverage>
</li>
<li>
<def>Leg</def>
<customLabel>rear right leg</customLabel>
<coverage>0.1</coverage>
</li>
</parts>
</corePart>
</BodyDef>
</Defs>
```
---
这份详尽的文档现在包含了我们所有的最终代码和XML并附有详细的注释解释了每一步的作用和它们之间的关联。

View File

@@ -1,38 +0,0 @@
# 项目:可交互的虫卵囊
## 1. 核心目标
创建一个可交互的虫卵囊,它允许一个特定的 Pawn阿拉克涅女皇种通过右键菜单与它交互从一个可配置的列表中选择一个 Pawn并在经过一段可配置的延迟后生成这个 Pawn。
## 2. 已完成的功能
* **创建了新的 VS 项目**: [`ArachnaeSwarm.csproj`](Source/ArachnaeSwarm/ArachnaeSwarm.csproj)
* **实现了核心的生成逻辑**:
* `CompProperties_SpawnPawnFromList.cs`: 定义了 XML 中可配置的属性,包括:
* `pawnKinds`: 可生成的 Pawn 列表。
* `whitelist`: 可以与虫卵囊交互的 Pawn 列表。
* `delay`: 孵化延迟。
* `spawnCount`: 生成数量。
* `destroyOnSpawn`: 生成后是否摧毁自身。
* `lordJob`: 生成的 Pawn 要执行的集体任务。
* `CompSpawnPawnFromList.cs`: 实现了核心的生成逻辑,包括:
* 生成右键菜单。
* 处理孵化倒计时。
* 生成指定数量的 Pawn。
* 在检查面板上显示孵化状态和提示信息。
* **实现了交互的 Job**:
* `ARA_Jobs.xml`: 定义了 `ARA_IncubateJob`
* `JobDriver_Incubate.cs`: 实现了让 Pawn 走到虫卵囊旁边并启动孵化过程的逻辑。
* **实现了动态的图形切换**:
* `Building_Incubator.cs`: 创建了一个新的建筑基类,它会根据虫卵囊是否正在孵化来动态地改变自身的图形。
* **创建了测试用的 Defs**:
* `ARA_InteractiveEggSac.xml`: 定义了一个可交互的虫卵囊,用于在游戏中测试新功能。
* `ArachnaeSwarm_Keys.xml`: 定义了相关的本地化 `key`
## 3. 当前状态
目前,项目已经基本完成了所有的核心功能,并且能够成功编译。但是,在最后一次构建时,我们遇到了一个编译错误,导致我们无法进行最终的测试。
## 4. 下一步计划
解决当前的编译错误,并成功构建项目,以便在游戏中进行最终的测试。

View File

@@ -1,743 +0,0 @@
# Rimatomics 电磁炮战略打击系统技术文档
## 1. 引言
本文档旨在详细解析 Rimatomics Mod 中的电磁炮Railgun远程战略打击系统的实现机制。该系统允许玩家对世界地图上的任意已加载的地图进行精确的火力投送是游戏中一种强大的后期战略武器。
我们将通过分析其三个核心 C# 类:`Building_Railgun``Verb_Railgun``WorldObject_Sabot`,来深入理解其功能、设计模式以及类之间的交互。
## 2. 核心组件概览
整个系统由三个主要部分协同工作,各司其职:
* **`Building_Railgun`**: 炮塔建筑本身,是整个系统的用户交互入口和武器平台。它负责处理玩家的瞄准指令,管理武器的属性(如射程、精度),并发起射击流程。
* **`Verb_Railgun`**: 定义了电磁炮的“射击”这一具体动作。它能够智能区分常规的本地射击和需要跨地图飞行的远程打击,并根据不同模式执行相应的逻辑。
* **`WorldObject_Sabot`**: 一个在世界地图上存在的临时对象,用于模拟炮弹在飞向目标过程中的状态。它负责处理跨地图飞行、轨迹计算,并在抵达目标后触发最终的命中效果。
## 3. 类详解
### 3.1 `Building_Railgun`
`Building_Railgun` 是电磁炮塔的建筑类,继承自 `Building_EnergyWeapon`。它是玩家能直接看到并与之交互的实体。
#### 功能概述
此类作为系统的交互枢纽,主要负责以下功能:
* **提供玩家操作界面**:通过 Gizmo操作按钮让玩家启动“火控任务”。
* **处理目标选择流程**:引导玩家从世界地图选择目标区域,再到目标地图内选择精确弹着点。
* **管理武器状态与属性**:存储远程目标信息,并根据已安装的升级动态计算射程、散布等属性。
* **发起射击指令**在获取有效目标后命令自身的武器组件Verb开火。
#### 源代码
```csharp
using System;
using System.Collections.Generic;
using System.Linq;
using Multiplayer.API;
using RimWorld;
using RimWorld.Planet;
using UnityEngine;
using Verse;
using Verse.Sound;
namespace Rimatomics
{
[StaticConstructorOnStartup]
public class Building_Railgun : Building_EnergyWeapon
{
public override bool TurretBased
{
get
{
return true;
}
}
public override Vector3 TipOffset
{
get
{
Vector3 pos = this.DrawPos;
Vector3 vecOffset = new Vector3(0f, 1f, 5f);
vecOffset = vecOffset.RotatedBy(this.TurretRotation);
return pos + vecOffset;
}
}
public override bool CanFireWhileRoofed()
{
// For remote fire missions, cannot be roofed. For local targets, it's allowed.
if (this.longTargetInt == GlobalTargetInfo.Invalid)
{
return true;
}
return !base.Position.Roofed(base.Map);
}
public float RangeToWorldTarget
{
get
{
return (float)Find.WorldGrid.TraversalDistanceBetween(base.Map.Tile, this.longTarget.Tile, true, int.MaxValue, false);
}
}
public void TryChamberRound()
{
if (this.magazine.NullOrEmpty<ThingDef>())
{
return;
}
CompChangeableProjectile comp = this.gun.TryGetComp<CompChangeableProjectile>();
if (!comp.Loaded)
{
comp.LoadShell(this.magazine.FirstOrDefault<ThingDef>(), 1);
this.magazine.RemoveAt(0);
}
}
public override void MuzzleFlash()
{
Mote flash = (Mote)ThingMaker.MakeThing(Building_Railgun.Mote_RailgunMuzzleFlash, null);
flash.Scale = 5f;
flash.exactRotation = this.TurretRotation;
flash.exactPosition = this.TipOffset;
GenSpawn.Spawn(flash, base.Position, base.Map, WipeMode.Vanish);
Vector3 vecOffset = new Vector3(1f, 1f, -1f).RotatedBy(this.TurretRotation);
FleckMaker.ThrowSmoke(this.DrawPos + vecOffset, base.Map, 1.5f);
Vector3 vecOffset2 = new Vector3(-1f, 1f, -1f).RotatedBy(this.TurretRotation);
FleckMaker.ThrowSmoke(this.DrawPos + vecOffset2, base.Map, 1.5f);
}
public override IEnumerable<Gizmo> GetGizmos()
{
foreach (Gizmo c in base.GetGizmos())
{
yield return c;
}
Command_Action launch = new Command_Action
{
defaultLabel = "critCommandFireMission".Translate(),
defaultDesc = "critCommandFireMissionDesc".Translate(),
icon = Building_Railgun.FireMissionTex,
action = new Action(this.StartChoosingDestination)
};
if (base.Spawned && base.Position.Roofed(base.Map))
{
launch.Disable("CannotFire".Translate() + ": " + "Roofed".Translate().CapitalizeFirst());
}
yield return launch;
}
private void StartChoosingDestination()
{
// Record all selected railguns to fire them simultaneously
this.selectedRailguns = Find.Selector.SelectedObjects.OfType<Building_Railgun>().ToList<Building_Railgun>();
CameraJumper.TryJump(CameraJumper.GetWorldTarget(this), CameraJumper.MovementMode.Pan);
Find.WorldSelector.ClearSelection();
int tile = base.Map.Tile;
Find.WorldTargeter.BeginTargeting(
new Func<GlobalTargetInfo, bool>(this.ChoseWorldTarget), // Callback when target is chosen
false,
Building_Railgun.FireMissionTex,
true,
() => GenDraw.DrawWorldRadiusRing(tile, this.WorldRange, null), // Draws the range ring
null, null, null, false);
}
public static TargetingParameters ForFireMission()
{
// In multiplayer, disallow targeting mobile pawns to prevent desync issues.
if (MP.IsInMultiplayer)
{
return new TargetingParameters
{
canTargetPawns = false,
canTargetBuildings = false,
canTargetLocations = true
};
}
return new TargetingParameters
{
canTargetPawns = true,
canTargetBuildings = true,
canTargetLocations = true
};
}
private bool ChoseWorldTarget(GlobalTargetInfo target)
{
if (!target.IsValid)
{
Messages.Message("MessageRailgunTargetInvalid".Translate(), MessageTypeDefOf.RejectInput, true);
return false;
}
int distance = Find.WorldGrid.TraversalDistanceBetween(base.Map.Tile, target.Tile, true, int.MaxValue, false);
if (distance > this.WorldRange)
{
Messages.Message("MessageTargetBeyondMaximumRange".Translate(), this, MessageTypeDefOf.RejectInput, true);
return false;
}
MapParent mapParent = target.WorldObject as MapParent;
if (mapParent != null && mapParent.HasMap)
{
if (mapParent.Map == base.Map)
{
Messages.Message("MessageRailgunCantTargetMyMap".Translate(), MessageTypeDefOf.RejectInput, true);
return false;
}
// --- Refactored Code Block ---
// This block handles the logic for fine-grained targeting within the destination map.
Map targetMap = mapParent.Map;
var originalMap = base.Map;
// Action to execute when targeting is finished or cancelled.
Action onFinished = () => {
if (Current.Game.CurrentMap != originalMap)
{
Current.Game.CurrentMap = originalMap;
}
};
// Switch to the target map to allow the player to pick a precise location.
Current.Game.CurrentMap = targetMap;
Find.Targeter.BeginTargeting(
Building_Railgun.ForFireMission(),
(LocalTargetInfo localTarget) => // Lambda for when a local target is selected
{
// Assign the fire mission to all selected railguns.
foreach (Building_Railgun railgun in this.selectedRailguns)
{
railgun.FireMission(targetMap.Tile, localTarget, targetMap.uniqueID);
}
},
null, // Action on highlight
onFinished, // Action on finish/cancel
Building_Railgun.FireMissionTex,
true);
return true;
}
else
{
Messages.Message("MessageRailgunNeedsMap".Translate(), MessageTypeDefOf.RejectInput, true);
return false;
}
}
[SyncMethod(SyncContext.None)]
public void FireMission(int tile, LocalTargetInfo targ, int map)
{
if (!targ.IsValid)
{
this.longTargetInt = GlobalTargetInfo.Invalid;
return;
}
GlobalTargetInfo newtarget = targ.ToGlobalTargetInfo(Find.Maps.FirstOrDefault((Map x) => x.uniqueID == map));
int distance = Find.WorldGrid.TraversalDistanceBetween(base.Map.Tile, tile, true, int.MaxValue, false);
if (distance > this.WorldRange)
{
Messages.Message("MessageTargetBeyondMaximumRange".Translate(), this, MessageTypeDefOf.RejectInput, true);
return;
}
if (this.holdFire)
{
Messages.Message("MessageTurretWontFireBecauseHoldFire".Translate(this.def.label), this, MessageTypeDefOf.RejectInput, true);
return;
}
if (this.longTargetInt != newtarget)
{
this.longTargetInt = newtarget;
if (this.burstCooldownTicksLeft <= 0)
{
this.TryStartShootSomething();
}
SoundDefOf.TurretAcquireTarget.PlayOneShot(new TargetInfo(base.Position, base.Map, false));
}
}
public override float PulseSize
{
get
{
float f = base.GunProps.EnergyWep.PulseSizeScaled;
if (this.UG.HasUpgrade(DubDef.MEPS))
{
f *= 1.15f;
}
if (this.UG.HasUpgrade(DubDef.ERS))
{
f *= 0.85f; // Equivalent to f -= 0.15f * f
}
return f;
}
}
public override int WorldRange
{
get
{
// In space (SoS2 compatibility), range is unlimited.
if (base.Map != null && this.space != null && base.Map.Biome == this.space)
{
return 99999;
}
int range = base.GunProps.EnergyWep.WorldRange;
if (this.UG.HasUpgrade(DubDef.TargetingChip))
{
range += 10;
}
if (this.UG.HasUpgrade(DubDef.MEPS))
{
range += 10;
}
return range;
}
}
public int spread
{
get
{
int v = 6;
if (this.UG.HasUpgrade(DubDef.TargetingChip))
{
v -= 3;
}
return v;
}
}
public static readonly Texture2D FireMissionTex = ContentFinder<Texture2D>.Get("Rimatomics/UI/FireMission", true);
public static ThingDef Mote_RailgunMuzzleFlash = ThingDef.Named("Mote_RailgunMuzzleFlash");
private List<Building_Railgun> selectedRailguns;
public BiomeDef space = DefDatabase<BiomeDef>.GetNamed("OuterSpaceBiome", false);
}
}
```
#### 关键方法和属性详解
* **`GetGizmos()`**: 为炮塔添加“火控任务” (Fire Mission) 按钮,这是远程打击流程的起点。如果炮塔被屋顶遮挡,该按钮会被禁用。
* **`StartChoosingDestination()`**: 响应按钮点击,将视角切换到世界地图,并启动一个带射程圈的目标选择器,让玩家选择目标地块。它支持多炮塔同时选择。
* **`ChoseWorldTarget(GlobalTargetInfo target)`**: 当玩家在世界地图上选择目标后的回调函数。它会进行一系列合法性验证(如射程、是否为当前地图等),如果通过,则将视角切换到目标地图,让玩家选择精确落点。
* **`FireMission(int tile, LocalTargetInfo targ, int map)`**: 这是目标选择的最后一步。它将最终的、精确的目标信息包含地块ID和地图内坐标存入 `longTargetInt` 变量,并立即尝试触发射击 (`TryStartShootSomething()`)。该方法支持多人游戏同步。
* **`WorldRange` (get)**: 一个计算属性返回炮塔在世界地图上的最大射程。基础射程定义在XML中并会受到 `TargetingChip``MEPS` 等升级的加成。
* **`spread` (get)**: 计算属性,返回炮弹的散布半径。`TargetingChip` 升级可以减小此值,提高精度。
* **`CanFireWhileRoofed()`**: 重写方法,规定在执行远程打击 (`longTargetInt` 有效) 时,炮塔不能处于屋顶之下。
### 3.2 `Verb_Railgun`
`Verb_Railgun` 继承自 `Verb_RimatomicsVerb`,它定义了电磁炮武器的“射击”这个核心动作。它不处理玩家交互,只负责执行射击逻辑。
#### 功能概述
这个类的主要职责是根据炮塔当前的目标状态(本地目标 vs 远程目标),决定执行哪种射击模式。
* **动态炮弹选择**: 根据 `CompChangeableProjectile` 组件的状态,决定是发射已装填的特殊炮弹还是默认炮弹。
* **射击模式分发**: 检查是否存在远程目标 (`longTarget`),如果存在,则执行“火控任务”流程;否则,执行标准的本地射击。
* **远程打击发起**: 在“火控任务”模式下,负责创建并初始化 `WorldObject_Sabot`,将其发射到世界地图。
* **本地打击后效**: 为本地射击附加额外的效果,如能量消耗、数据收集和屏幕震动。
#### 源代码
```csharp
using System;
using RimWorld;
using RimWorld.Planet;
using UnityEngine;
using Verse;
namespace Rimatomics
{
[StaticConstructorOnStartup]
public class Verb_Railgun : Verb_RimatomicsVerb
{
public override ThingDef Projectile
{
get
{
ThingWithComps equipmentSource = base.EquipmentSource;
CompChangeableProjectile comp = (equipmentSource != null) ? equipmentSource.GetComp<CompChangeableProjectile>() : null;
if (comp != null && comp.Loaded)
{
return comp.Projectile;
}
return this.verbProps.defaultProjectile;
}
}
public override bool CanHitTargetFrom(IntVec3 root, LocalTargetInfo targ)
{
// If a long-range target is set, we can "hit" it regardless of local range.
if (base.GetWep.longTarget.IsValid)
{
return true;
}
return base.CanHitTargetFrom(root, targ);
}
protected bool TryCastFireMission()
{
Building_Railgun railgun = this.caster as Building_Railgun;
// Create the flying sabot object on the world map
WorldObject_Sabot sabot = (WorldObject_Sabot)WorldObjectMaker.MakeWorldObject(DefDatabase<WorldObjectDef>.GetNamed("Sabot", true));
sabot.railgun = railgun;
sabot.Tile = railgun.Map.Tile;
sabot.destinationTile = railgun.longTarget.Tile;
sabot.destinationCell = railgun.longTarget.Cell;
sabot.spread = railgun.spread;
sabot.Projectile = this.Projectile;
Find.WorldObjects.Add(sabot);
// Post-launch effects and data gathering
railgun.GatherData("PPCWeapon", 5f);
railgun.GatherData("PPCFireMission", 10f);
railgun.GatherData("PPCRailgun", 10f);
railgun.PrototypeBang(railgun.GunProps.EnergyWep.PrototypeFailureChance);
railgun.MuzzleFlash();
Find.CameraDriver.shaker.SetMinShake(0.1f);
// Spawn a dummy projectile that flies off-map. This is for visual effect only.
Vector3 shellDirection = Vector3.forward.RotatedBy(railgun.TurretRotation);
IntVec3 outcell = (railgun.DrawPos + shellDirection * 500f).ToIntVec3();
Projectile projectile2 = (Projectile)GenSpawn.Spawn(this.Projectile, railgun.Position, this.caster.Map, WipeMode.Vanish);
projectile2.Launch(railgun, railgun.DrawPos, outcell, null, ProjectileHitFlags.None, false, base.EquipmentSource, null);
// Handle shell consumption
CompChangeableProjectile comp = base.EquipmentSource?.GetComp<CompChangeableProjectile>();
if (comp != null)
{
comp.Notify_ProjectileLaunched();
}
railgun.DissipateCharge(railgun.PulseSize);
return true;
}
public override bool TryCastShot()
{
Building_Railgun railgun = this.caster as Building_Railgun;
if (!railgun.top.TargetInSights)
{
return false;
}
if (this.Projectile == null)
{
return false;
}
bool shotResult;
// --- Shooting Mode Dispatch ---
if (railgun.longTarget.IsValid)
{
// Execute long-range fire mission
shotResult = this.TryCastFireMission();
}
else
{
// Execute standard local shot
shotResult = base.TryCastShot();
if (shotResult)
{
railgun.DissipateCharge(railgun.PulseSize);
railgun.GatherData("PPCWeapon", 5f);
railgun.GatherData("PPCRailgun", 10f);
railgun.PrototypeBang(railgun.GunProps.EnergyWep.PrototypeFailureChance);
railgun.MuzzleFlash();
Find.CameraDriver.shaker.SetMinShake(0.1f);
}
}
// Chamber the next round after firing
railgun.TryChamberRound();
return shotResult;
}
}
}
```
#### 关键方法和属性详解
* **`Projectile` (get)**: 这是一个动态属性,用于获取当前应该发射的炮弹类型。它会优先返回 `CompChangeableProjectile` 中已装填的炮弹,如果未装填,则返回在 XML 中定义的默认炮弹。
* **`CanHitTargetFrom(...)`**: 重写了基类方法。当炮塔被赋予了一个远程目标 (`longTarget.IsValid`) 时,该方法直接返回 `true`,绕过了所有常规的射程和视线检查,确保远程打击流程可以启动。
* **`TryCastShot()`**: 这是射击动作的入口点。它的核心逻辑是检查 `railgun.longTarget` 是否有效。
* 如果**有效**,说明是远程打击任务,它便调用 `TryCastFireMission()`
* 如果**无效**,说明是常规的本地瞄准,它就调用基类的 `base.TryCastShot()` 来发射普通炮弹,并附加一系列开火后效。
* **`TryCastFireMission()`**: 这是发起远程打击的核心。它不直接生成命中目标的炮弹,而是:
1. 创建一个 `WorldObject_Sabot` 实例。
2. 将目标地块、精确坐标、炮弹类型、散布等关键信息从 `Building_Railgun` 传递给 `sabot` 对象。
3.`sabot` 添加到世界对象管理器 (`Find.WorldObjects.Add(sabot)`),让其开始在世界地图上“飞行”。
4. 触发炮口闪光、能量消耗、屏幕震动等本地开火效果。
5. 生成一个飞向地图外的“虚拟”炮弹,仅用于视觉表现,它不会造成任何伤害。
### 3.3 `WorldObject_Sabot`
`WorldObject_Sabot` 继承自 `WorldObject`,是远程打击流程中的“飞行”阶段的执行者。它是一个临时的、在世界地图上存在的实体,模拟了炮弹从发射点到目标点的飞行过程。
#### 功能概述
此类完全独立于发射它的炮塔,其核心职责是在世界地图上完成一段旅程,并在抵达终点时触发命中效果。
* **飞行轨迹模拟**: 通过 `DrawPos` 属性和 `Tick` 方法,平滑地计算并更新自身在世界地图上的位置,实现飞行-动画效果。
* **状态持久化**: 通过 `ExposeData` 方法保存所有关键信息(如起点、终点、飞行进度、炮弹类型),确保在游戏存读档后飞行可以继续。
* **命中触发**: 在飞行到达终点后,`Arrived` 方法负责在目标地图上生成真正的 `Projectile`(炮弹),并让其从地图边缘发射,命中最终的精确弹着点。
* **自我销毁**: 完成命中逻辑后,将自身从世界对象管理器中移除。
* **Mod兼容性**: 能够识别并处理 "Save Our Ship 2" Mod 中的轨道飞船,实现地对空、空对地和空对空打击。
#### 源代码
```csharp
using System;
using System.Linq;
using RimWorld.Planet;
using UnityEngine;
using Verse;
namespace Rimatomics
{
public class WorldObject_Sabot : WorldObject
{
private Vector3 Start
{
get
{
Vector3 startPos = Find.WorldGrid.GetTileCenter(this.initialTile);
// SoS2 compatibility: if a ship is at the tile, use its position.
if (HarmonyPatches.SoS)
{
WorldObject ship = Find.World.worldObjects.AllWorldObjects.FirstOrDefault(o =>
(o.def.defName.Equals("ShipOrbiting") || o.def.defName.Equals("SiteSpace")) && o.Tile == this.initialTile);
if (ship != null)
{
startPos = ship.DrawPos;
}
}
return startPos;
}
}
private Vector3 End
{
get
{
Vector3 endPos = Find.WorldGrid.GetTileCenter(this.destinationTile);
// SoS2 compatibility
if (HarmonyPatches.SoS)
{
WorldObject ship = Find.World.worldObjects.AllWorldObjects.FirstOrDefault(o =>
(o.def.defName.Equals("ShipOrbiting") || o.def.defName.Equals("SiteSpace")) && o.Tile == this.destinationTile);
if (ship != null)
{
endPos = ship.DrawPos;
}
}
return endPos;
}
}
public override Vector3 DrawPos
{
get
{
// Slerp for a smooth curve over the planet's surface
return Vector3.Slerp(this.Start, this.End, this.traveledPct);
}
}
private float TraveledPctStepPerTick
{
get
{
Vector3 start = this.Start;
Vector3 end = this.End;
if (start == end) return 1f;
float distance = GenMath.SphericalDistance(start.normalized, end.normalized);
if (distance == 0f) return 1f;
// Travel speed is constant
return TravelSpeed / distance;
}
}
public override void ExposeData()
{
base.ExposeData();
Scribe_Values.Look<int>(ref this.destinationTile, "destinationTile", 0, false);
Scribe_Values.Look<IntVec3>(ref this.destinationCell, "destinationCell", default(IntVec3), false);
Scribe_Values.Look<bool>(ref this.arrived, "arrived", false, false);
Scribe_Values.Look<int>(ref this.initialTile, "initialTile", 0, false);
Scribe_Values.Look<float>(ref this.traveledPct, "traveledPct", 0f, false);
Scribe_Defs.Look<ThingDef>(ref this.Projectile, "Projectile");
Scribe_References.Look<Thing>(ref this.railgun, "railgun");
Scribe_Values.Look<int>(ref this.spread, "spread", 1, false);
}
public override void PostAdd()
{
base.PostAdd();
this.initialTile = base.Tile;
}
public override void Tick()
{
base.Tick();
this.traveledPct += this.TraveledPctStepPerTick;
if (this.traveledPct >= 1f)
{
this.traveledPct = 1f;
this.Arrived();
}
}
private void Arrived()
{
if (this.arrived) return;
this.arrived = true;
Map map = Current.Game.FindMap(this.destinationTile);
if (map != null)
{
// Spawn the projectile at the edge of the map
IntVec3 entryCell = new IntVec3(CellRect.WholeMap(map).Width / 2, 0, CellRect.WholeMap(map).maxZ);
Projectile projectile = (Projectile)GenSpawn.Spawn(this.Projectile, entryCell, map, WipeMode.Vanish);
// Find a random cell near the target destination within the spread radius
IntVec3 finalDestination;
CellFinder.TryFindRandomCellNear(this.destinationCell, map, this.spread, null, out finalDestination, -1);
// Launch the projectile to the final destination
projectile.Launch(this.railgun, finalDestination, finalDestination, ProjectileHitFlags.IntendedTarget, false, null);
}
// Remove self from the world
Find.WorldObjects.Remove(this);
}
private const float TravelSpeed = 0.0001f;
private bool arrived;
public IntVec3 destinationCell = IntVec3.Invalid;
public int destinationTile = -1;
private int initialTile = -1;
public ThingDef Projectile;
public Thing railgun;
public int spread = 1;
private float traveledPct;
}
}
```
#### 关键方法和属性详解
* **`Start` / `End` (get)**: 这两个属性负责计算飞行的起点和终点坐标。它们通过检查 `HarmonyPatches.SoS` 来判断是否加载了 "Save Our Ship 2" Mod如果加载了则会尝试获取轨道上飞船的位置作为起点/终点从而实现与该Mod的无缝兼容。
* **`DrawPos` (get)**: 重写属性,用于在世界地图上渲染该对象。它使用 `Vector3.Slerp`(球面线性插值)在起点和终点之间进行插值,根据 `traveledPct`(已飞行百分比)计算出当前帧应该在的位置,从而形成一条平滑的弧形飞行轨迹。
* **`Tick()`**: 游戏引擎为每个世界对象调用的更新方法。它在每一帧增加 `traveledPct` 来推进飞行进度。当 `traveledPct` 达到1时调用 `Arrived()`
* **`Arrived()`**: 这是飞行结束、触发命中的核心方法。它首先检查目标地块是否存在一个已加载的地图。如果存在,它会在该地图的边缘生成 `Projectile` 实体,并根据 `spread`(散布)在目标点附近随机一个最终落点,然后调用炮弹的 `Launch()` 方法完成最后的攻击。无论地图是否存在,它最终都会将自己从世界上移除。
* **`ExposeData()`**: 保证了该飞行物体的所有状态(起点、终点、进度、炮弹类型等)都能被正确地保存和加载。
## 4. 系统工作流程
现在我们将三个核心组件联系起来,详细描述从玩家点击按钮到炮弹命中的完整工作流程。
### 4.1 交互时序图
下面的 Mermaid 时序图直观地展示了不同对象之间的交互顺序。
```mermaid
sequenceDiagram
participant Player as 玩家
participant Railgun as Building_Railgun
participant Verb as Verb_Railgun
participant Sabot as WorldObject_Sabot
participant Projectile as 最终炮弹
Player->>Railgun: 点击 "Fire Mission" 按钮
activate Railgun
Railgun->>Player: 显示世界地图和射程圈
Player->>Railgun: 1. 选择世界目标 (地块)
Player->>Railgun: 2. 选择地图内目标 (坐标)
Railgun->>Railgun: 调用 FireMission() 设置 longTarget
Railgun->>Verb: 调用 TryStartShootSomething()
deactivate Railgun
activate Verb
Verb->>Verb: TryCastShot() 检测到 longTarget
Verb->>Verb: 调用 TryCastFireMission()
Verb->>Sabot: new WorldObject_Sabot()
activate Sabot
Verb->>Sabot: 传递目标信息、炮弹类型等
Verb-->>Player: 触发炮口闪光和声音
deactivate Verb
loop 飞行过程 (每Tick)
Sabot->>Sabot: 更新 traveledPct (飞行进度)
end
Sabot->>Sabot: Arrived() - 到达目标
Sabot->>Projectile: GenSpawn.Spawn(炮弹)
activate Projectile
Sabot->>Projectile: Launch() - 飞向最终落点
Projectile-->>Player: 爆炸和伤害效果
deactivate Projectile
Sabot->>Sabot: Find.WorldObjects.Remove(this)
deactivate Sabot
```
### 4.2 步骤分解
1. **启动与目标选择 (玩家 -> `Building_Railgun`)**
* 玩家选中一门或多门 `Building_Railgun` 并点击其 "Fire Mission" Gizmo。
* `GetGizmos` 触发 `StartChoosingDestination` 方法,视角切换至世界地图,并显示最大射程圈。
* 玩家在世界地图上选择一个目标地块。
* `ChoseWorldTarget` 方法被回调。在通过一系列验证后,视角切换到目标地图。
* 玩家在目标地图上选择一个精确的弹着点。
2. **下达指令 (`Building_Railgun` -> `Verb_Railgun`)**
* 当精确弹着点被选定后,`ChoseWorldTarget` 方法会为所有被选中的炮塔调用 `FireMission` 方法。
* `FireMission` 将包含地块和坐标的 `GlobalTargetInfo` 存入炮塔的 `longTarget` 变量中。
* `FireMission` 随即调用 `TryStartShootSomething()`这会启动炮塔的射击冷却计时器并最终触发其Verb组件。
3. **发射与创建飞行物 (`Verb_Railgun` -> `WorldObject_Sabot`)**
* 炮塔的 `Verb_Railgun` 组件的 `TryCastShot` 方法被调用。
* `TryCastShot` 检测到 `longTarget` 是有效的,因此它不会执行常规射击,而是调用 `TryCastFireMission`
* `TryCastFireMission` 创建一个 `WorldObject_Sabot` 的实例,并将目标信息、炮弹定义、散布等关键数据从 `Building_Railgun` 复制到这个新实例中。
* `WorldObject_Sabot` 被添加到 `Find.WorldObjects` 管理器中,正式开始其生命周期。
* 同时,`Verb_Railgun` 在本地触发开火的视觉和听觉效果。
4. **跨地图飞行 (`WorldObject_Sabot`)**
* `WorldObject_Sabot` 作为一个独立的世界对象,其 `Tick` 方法被游戏引擎在每一帧调用。
* `Tick` 方法不断更新 `traveledPct` 属性,模拟飞行进度。
*`DrawPos` 属性根据 `traveledPct` 在世界地图上平滑地渲染出飞行轨迹。
5. **抵达与命中 (`WorldObject_Sabot` -> `Projectile`)**
*`traveledPct` 达到100%时,`Arrived` 方法被调用。
* `Arrived` 检查目标地块的地图是否已加载。
* 如果地图已加载,它会在地图边缘 `GenSpawn.Spawn` 一个真正的 `Projectile`(最终炮弹)。
* 根据从 `Building_Railgun` 继承来的 `spread` 值,在玩家指定的精确落点附近随机一个最终弹着点。
* 调用 `Projectile.Launch()`,使其从地图边缘飞向并命中最终弹着点,产生爆炸和伤害。
* 最后,`WorldObject_Sabot` 调用 `Find.WorldObjects.Remove(this)` 将自己从世界上移除,完成其使命。
## 5. 总结
Rimatomics的电磁炮系统是一个设计精良的远程打击模块。它通过将**交互(Building)**、**动作(Verb)**和**飞行(WorldObject)**三个阶段清晰地分离到不同的类中,实现了高度的内聚和低耦合。这种设计不仅使得代码逻辑清晰、易于维护,还通过 `WorldObject` 机制优雅地解决了跨地图状态同步和持久化的问题并为兼容其他Mod如Save Our Ship 2留出了接口。

View File

@@ -1,180 +0,0 @@
# CompSpawnPawnFromList 功能设计文档
## 1. 功能概述
`CompSpawnPawnFromList` 是一个 `ThingComp` 组件,它允许一个特定的 Pawn 通过右键菜单与一个物体交互,从一个可配置的列表中选择一个 Pawn并在经过一段可配置的延迟后生成这个 Pawn。在延迟期间剩余时间会显示在建筑的检查面板上。可以选择在生成 Pawn 后是否摧毁建筑。
## 2. XML 配置
### 2.1. 配置属性
* `pawnKinds` (`List<PawnKindDef>`): 一个 `PawnKindDef` 的列表,用于填充右键菜单的选项。
* `whitelist` (`List<PawnKindDef>`): 一个 `PawnKindDef` 的白名单,只有在这个列表中的 Pawn 才能看到并使用这个右键菜单。
* `delay` (`int`): 延迟时间,单位为 Ticks (1秒 = 60 Ticks)。
* `destroyOnSpawn` (`bool`): (可选, 默认为 `false`) 如果为 `true`,则在生成 Pawn 后摧毁建筑。
* `lordJob` (`Type`): (可选) 生成的 Pawn 要执行的集体任务。
### 2.2. XML 示例
```xml
<ThingDef ParentName="BuildingBase">
<defName>ARA_PawnSpawner</defName>
<label>Pawn Spawner</label>
<description>A device that can be used to spawn pawns after a delay.</description>
<!-- 其他属性 -->
<comps>
<li Class="ArachnaeSwarm.CompProperties_SpawnPawnFromList">
<pawnKinds>
<li>Megascarab</li>
<li>Spelopede</li>
</pawnKinds>
<whitelist>
<li>Colonist</li>
</whitelist>
<delay>300</delay> <!-- 5秒 -->
<destroyOnSpawn>true</destroyOnSpawn>
<lordJob>RimWorld.LordJob_AssaultColony</lordJob>
</li>
</comps>
</ThingDef>
```
## 3. 类设计
### 3.1. `CompProperties_SpawnPawnFromList.cs`
```csharp
using System;
using System.Collections.Generic;
using Verse;
namespace ArachnaeSwarm
{
public class CompProperties_SpawnPawnFromList : CompProperties
{
public List<PawnKindDef> pawnKinds;
public List<PawnKindDef> whitelist;
public int delay = 0;
public bool destroyOnSpawn = false;
public Type lordJob;
public CompProperties_SpawnPawnFromList()
{
compClass = typeof(CompSpawnPawnFromList);
}
public override IEnumerable<string> ConfigErrors(ThingDef parentDef)
{
foreach (string item in base.ConfigErrors(parentDef))
{
yield return item;
}
if (lordJob != null && !typeof(RimWorld.LordJob).IsAssignableFrom(lordJob))
{
yield return $"lordJob {lordJob} must be of type LordJob";
}
}
}
}
```
### 3.2. `CompSpawnPawnFromList.cs`
```csharp
using System.Collections.Generic;
using Verse;
using RimWorld;
namespace ArachnaeSwarm
{
public class CompSpawnPawnFromList : ThingComp
{
private CompProperties_SpawnPawnFromList Props => (CompProperties_SpawnPawnFromList)props;
private int spawnUntilTick = -1;
private PawnKindDef spawningPawnKind;
public override IEnumerable<FloatMenuOption> CompFloatMenuOptions(Pawn selPawn)
{
if (spawnUntilTick > 0)
{
yield break; // 正在延迟中,不显示菜单
}
if (Props.whitelist == null || !Props.whitelist.Contains(selPawn.kindDef))
{
yield break;
}
if (Props.pawnKinds != null)
{
foreach (PawnKindDef pawnKind in Props.pawnKinds)
{
yield return new FloatMenuOption($"Spawn {pawnKind.label}", () =>
{
StartDelayedSpawn(pawnKind);
});
}
}
}
private void StartDelayedSpawn(PawnKindDef pawnKind)
{
spawningPawnKind = pawnKind;
spawnUntilTick = Find.TickManager.TicksGame + Props.delay;
}
public override void CompTick()
{
base.CompTick();
if (spawnUntilTick > 0 && Find.TickManager.TicksGame >= spawnUntilTick)
{
SpawnPawn(spawningPawnKind);
spawnUntilTick = -1;
spawningPawnKind = null;
}
}
private void SpawnPawn(PawnKindDef pawnKind)
{
Pawn pawn = PawnGenerator.GeneratePawn(new PawnGenerationRequest(pawnKind, parent.Faction));
GenSpawn.Spawn(pawn, parent.Position, parent.Map);
if (Props.lordJob != null)
{
Lord lord = LordMaker.MakeNewLord(parent.Faction, (LordJob)System.Activator.CreateInstance(Props.lordJob), parent.Map);
lord.AddPawn(pawn);
}
if (Props.destroyOnSpawn)
{
parent.Destroy(DestroyMode.Vanish);
}
}
public override string CompInspectStringExtra()
{
if (spawnUntilTick > 0)
{
int remainingTicks = spawnUntilTick - Find.TickManager.TicksGame;
return $"Spawning in: {remainingTicks.ToStringTicksToPeriod()}";
}
return base.CompInspectStringExtra();
}
public override void PostExposeData()
{
base.PostExposeData();
Scribe_Values.Look(ref spawnUntilTick, "spawnUntilTick", -1);
Scribe_Defs.Look(ref spawningPawnKind, "spawningPawnKind");
}
}
}
```
## 4. 使用示例
1. 一个 `Colonist` 右键点击 `ARA_PawnSpawner`,选择 "Spawn Megascarab"。
2. `ARA_PawnSpawner` 进入延迟状态。当玩家选中它时,检查面板会显示 "Spawning in: 5 seconds"。
3. 5 秒后,一个新的 `Megascarab` 被生成。
4. 由于 `destroyOnSpawn``true``ARA_PawnSpawner` 建筑被摧毁。

File diff suppressed because it is too large Load Diff

View File

@@ -1,97 +0,0 @@
# 设计文档:跟踪冲撞技能 (Tracking Charge)
**版本:** 0.1
## 1. 概述
本文档旨在详细说明一个新的 RimWorld 技能:“跟踪冲撞”。该技能允许一个 Pawn施法者像制导导弹一样冲向一个目标 Pawn。在飞行过程中它会对路径上接触到的所有敌对单位和可破坏建筑造成伤害。伤害值会随着飞行距离的增加而累积。当撞击到主目标后施法者会因惯性继续向前滑行一小段距离。
## 2. 核心功能需求
* **技能类型**: 主动施放的 targeted ability。
* **移动方式**: 动态追踪曲线移动,而非直线或抛物线。
* **伤害机制**:
* **路径伤害**: 对飞行路径上碰撞到的所有有效目标(敌方、中立、可破坏建筑)造成伤害。
* **累积伤害**: 总伤害 = 基础伤害 + (飞行距离 * 每米伤害增量)。
* **主目标伤害**: 与路径伤害计算方式相同。
* **最终效果**: 撞击主目标后,追踪停止,施法者沿最后的方向继续滑行一小段距离后停下。
## 3. 技术架构
我们将采用基于自定义 `PawnFlyer` 的核心架构。这种方法将移动和效果逻辑封装在一个临时的 `Thing` 中,而施法者 Pawn 本身在技能持续期间会从地图上暂时移除。
### 3.1. 核心组件
| 组件名称 (C# Class) | 类型 | 职责 |
| ---------------------------------- | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------- |
| `Verb_CastAbilityTrackingCharge` | `Verb` | **技能启动器**: 验证目标,从 `CompProperties` 读取配置,创建并初始化 `PawnFlyer_TrackingCharge`。 |
| `PawnFlyer_TrackingCharge` | `PawnFlyer` | **核心逻辑处理器**: 接收来自 `Verb` 的参数,实现追踪、碰撞、伤害、惯性等所有动态逻辑。 |
| `CompProperties_TrackingCharge` | `CompProperties_AbilityEffect` | **XML配置接口**: 在 `AbilityDef``<comps>` 中定义技能的所有可调参数,如伤害、速度、惯性等。 |
| `CompAbilityEffect_TrackingCharge` | `CompAbilityEffect` | **数据容器**: 作为一个轻量级的组件,其主要作用是让 `CompProperties_TrackingCharge` 能被游戏正确加载。它本身不执行复杂逻辑。 |
### 3.2. 设计决策:配置的分层与传递
* **采纳的方案 (混合模式)**: 我们将采用一种既符合直觉又技术稳健的混合模式。
1. **配置集中在 `AbilityDef`**: 使用 `CompProperties_TrackingCharge` (继承自 `CompProperties_AbilityEffect`) 来存放所有技能参数。这使得 Mod 用户可以在 `AbilityDef``<comps>` 节点中方便地调整一切,符合 RimWorld 的标准实践。
2. **`Verb` 作为数据中介**: `Verb_CastAbilityTrackingCharge` 在施法时,会从 `this.ability.def` 中轻松获取到 `CompProperties_TrackingCharge` 的实例,读取所有配置参数。
3. **`PawnFlyer` 接收参数**: `Verb` 在创建 `PawnFlyer_TrackingCharge` 实例后,会通过一个自定义的初始化方法(例如 `Initialize(...)`),将所有读取到的参数“注入”到 `PawnFlyer` 实例中。
* **优点**: 这个方案完美地解决了您的顾虑。配置的**易用性**(在 `AbilityDef` 中)和逻辑的**清晰性**`PawnFlyer` 负责执行)都得到了保证。
### 3.3. 数据流与交互
```mermaid
graph TD
subgraph Player Action
A[玩家选择目标并施放技能] --> B(AbilityDef);
end
subgraph XML Definitions
B -- contains --> E[CompProperties_TrackingCharge];
B -- uses --> C(VerbDef);
C -- verbClass --> D[Verb_CastAbilityTrackingCharge];
F(ThingDef for Flyer) -- thingClass --> G[PawnFlyer_TrackingCharge];
end
subgraph C# Logic
D -- 1. Reads Params from --> E;
D -- 2. Creates --> G;
D -- 3. Injects Params into --> G;
G -- 4. Executes Tick Logic --> H{追踪 & 碰撞};
H -- 5. Applies Damage to --> I[Things on Path];
G -- 6. Handles Inertia & Self-Destructs --> J[Final Position];
end
PlayerInput([施法者 Pawn]) -- temporarily removed --> G;
G -- places back --> PlayerInput;
```
## 4. 详细实现步骤
### 步骤 1: 创建 C# 类骨架 (最终版)
* **文件**: `CompProperties_TrackingCharge.cs`
* **继承**: `Verse.CompProperties_AbilityEffect`
* **职责**: 定义所有可在 XML 中配置的技能参数。
* **示例字段**: `public float homingSpeed;`, `public float initialDamage;`, `public float damagePerTile;`, `public float inertiaDistance;`, `public DamageDef collisionDamageDef;`, `public ThingDef flyerDef;` (用于指定飞行器的ThingDef)。
* **文件**: `CompAbilityEffect_TrackingCharge.cs`
* **继承**: `Verse.CompAbilityEffect`
* **职责**: 轻量级组件,其存在是为了让 `CompProperties_TrackingCharge` 能被游戏正确加载和访问。
* **文件**: `Verb_CastAbilityTrackingCharge.cs`
* **继承**: `Verse.Verb_CastAbility`
* **职责**:
1.`TryCastShot()` 中,从 `this.ability.GetComp<CompAbilityEffect_TrackingCharge>().Props` 获取配置。
2. 调用 `PawnFlyer.MakeFlyer(Props.flyerDef, ...)` 创建 `PawnFlyer_TrackingCharge` 实例。
3. **将配置参数设置到 `PawnFlyer` 的公共字段上**
4. 调用 `flyer.Launch()` 启动。
* **文件**: `PawnFlyer_TrackingCharge.cs`
* **继承**: `Verse.PawnFlyer`
* **职责**:
1. 定义一系列**公共字段** (`public float homingSpeed;` 等) 来接收来自 `Verb` 的参数。
2.`Tick()` 中实现所有核心的追踪、碰撞和伤害逻辑。
* **注意**: **不**创建自定义的 `Initialize()` 方法,也**不**重写 `Launch()` 来接收参数,以保持设计的简洁和标准。
---
> **下一步**: 设计文档已最终确定。我将开始实施 **步骤 1**,为您创建这四个 C# 类的代码框架。