暂存文档
This commit is contained in:
227
Source/Documents/How_SpecialTrainables_Work.md
Normal file
227
Source/Documents/How_SpecialTrainables_Work.md
Normal file
@@ -0,0 +1,227 @@
|
||||
# 技术文档: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**: 编译并进行游戏内测试。
|
||||
Reference in New Issue
Block a user