本指南将帮助你为《杀戮尖塔2》制作Mod。本游戏使用C# + Godot 4.5开发,Mod系统基于Harmony补丁和Hook扩展点。
mods/ # 本地Mod目录
├── my_mod.pck # 打包的Mod文件
│ ├── mod_manifest.json # Mod清单(必需)
│ ├── my_mod.dll # C#代码(可选)
│ └── localization/ # 本地化文件(可选)
│ └── eng/
│ └── cards.json
| 来源 | 位置 | 说明 |
|-----|------|------|
| 本地Mod | exe所在目录/mods/ | .pck文件 |
| Steam创意工坊 | Steam Workshop | 自动下载 |
每个Mod必须包含 mod_manifest.json:
{
"pck_name": "my_awesome_mod",
"name": "My Awesome Mod",
"author": "YourName",
"description": "Adds cool new features!",
"version": "1.0.0"
}
创建一个C#类库项目,引用以下程序集:
-
MegaCrit.Sts2.Core.dll(游戏核心) -
GodotSharp.dll(Godot引擎) -
0Harmony.dll(Harmony补丁库)
using System;
using MegaCrit.Sts2.Core.Modding;
using HarmonyLib;
[ModInitializer("Initialize")]
public class MyMod
{
public static void Initialize()
{
// 在这里初始化你的Mod
Harmony harmony = new Harmony("com.yourname.awesomemod");
harmony.PatchAll();
}
}[ModInitializer("OnModLoad")]
public static class ModEntry
{
public static void OnModLoad()
{
// 初始化代码
}
}游戏提供100+个Hook点,允许你在特定时机插入代码。
| 分类 | 数量 | 示例 |
|------|------|------|
| 战斗Hook | 40+ | BeforeAttack, AfterCardPlayed |
| 卡牌Hook | 30+ | AfterCardDrawn, BeforeCardPlayed |
| 遗物Hook | 20+ | AfterRelicObtained, OnBattleEnd |
| 地图Hook | 10+ | AfterActEntered, BeforeMapNodeChosen |
在AbstractModel中重写hook方法:
using MegaCrit.Sts2.Core.Models;
using MegaCrit.Sts2.Core.Combat;
using MegaCrit.Sts2.Core.Commands.Builders;
using MegaCrit.Sts2.Core.Entities.Cards;
using System.Threading.Tasks;
public class MyPower : PowerModel
{
public override bool ShouldReceiveCombatHooks => true;
// 在攻击前触发
public override Task BeforeAttack(AttackCommand command)
{
// 增加50%伤害
command.Damage *= 1.5m;
return Task.CompletedTask;
}
// 在卡牌打出后触发
public override Task AfterCardPlayed(PlayerChoiceContext context, CardPlay cardPlay)
{
// 你的逻辑
return Task.CompletedTask;
}
}// 攻击
Task BeforeAttack(AttackCommand command)
Task AfterAttack(AttackCommand command)
// 护甲
Task BeforeBlockGained(Creature creature, decimal amount, ValueProp props, CardModel? cardSource)
Task AfterBlockGained(Creature creature, decimal amount, ValueProp props, CardModel? cardSource)
Task AfterBlockBroken(CombatState combatState, Creature creature)
Task AfterBlockCleared(CombatState combatState, Creature creature)
// 死亡
Task BeforeDeath(IRunState runState, CombatState? combatState, Creature creature)
Task AfterDeath(IRunState runState, CombatState? combatState, Creature creature, bool wasRemovalPrevented, float deathAnimLength)// 打牌
Task BeforeCardPlayed(CardPlay cardPlay)
Task AfterCardPlayed(PlayerChoiceContext context, CardPlay cardPlay)
Task AfterCardPlayedLate(PlayerChoiceContext context, CardPlay cardPlay)
// 抽牌
Task AfterCardDrawnEarly(PlayerChoiceContext context, CardModel card, bool fromHandDraw)
Task AfterCardDrawn(PlayerChoiceContext context, CardModel card, bool fromHandDraw)
// 弃牌
Task AfterCardDiscarded(PlayerChoiceContext context, CardModel card)
// 消耗
Task AfterCardExhausted(PlayerChoiceContext context, CardModel card, bool causedByEthereal)
// 堆叠变动
Task AfterCardChangedPiles(IRunState runState, CombatState? combatState, CardModel card, PileType oldPile, AbstractModel? source)Task AfterRelicObtained(IRunState runState, RelicModel relic)
Task AfterRelicLost(IRunState runState, RelicModel relic)
Task OnBattleEnd(IRunState runState, CombatState combatState)
Task OnBattleStart(CombatState combatState)
Task OnScry(IRunState runState, int amount)Task OnPlayerTurnStart(CombatState combatState)
Task OnPlayerTurnEnd(CombatState combatState)
Task OnEnemyTurnStart(CombatState combatState)
Task OnEnemyTurnEnd(CombatState combatState)using MegaCrit.Sts2.Core.Modding;
using MegaCrit.Sts2.Core.Models;
public class MyMod
{
public static void Initialize()
{
// 添加自定义卡牌到卡池
ModHelper.AddModelToPool<CardPoolModel, MyCustomCardModel>();
}
}ModHelper.AddModelToPool<RelicPoolModel, MyCustomRelicModel>();ModHelper.AddModelToPool<EncounterModel, MyCustomEncounterModel>();在.pck中包含以下结构:
localization/
└── eng/
├── cards.json
├── relics.json
└── powers.json
{
"my_custom_card_NAME": "My Custom Card",
"my_custom_card_DESCRIPTION": "Deal {0} damage. ",
"my_custom_card_EXTENDED_DESCRIPTION": "Deal {0} damage. Extra info."
}
// 在模型中使用LocString
public class MyCard : CardModel
{
public LocString Name = new LocString("my_custom_card", "MY_CUSTOM_CARD");
public LocString Description = new LocString("my_custom_card", "MY_CUSTOM_CARD_DESCRIPTION");
}对于Hook无法覆盖的场景,可以使用Harmony直接修改游戏代码。
using HarmonyLib;
using MegaCrit.Sts2.Core.Combat;
[HarmonyPatch]
public class MyPatches
{
[HarmonyPatch(typeof(CombatState), nameof(CombatState.StartCombat))]
[HarmonyPrefix]
public static void StartCombat_Prefix(CombatState __instance)
{
// 在战斗开始前执行的代码
}
[HarmonyPatch(typeof(CombatState), nameof(CombatState.StartCombat))]
[HarmonyPostfix]
public static void StartCombat_Postfix(CombatState __instance)
{
// 在战斗开始后执行的代码
}
}[HarmonyPatch(typeof(CardModel), nameof(CardModel.GetDamage))]
[HarmonyPrefix]
public static bool ModifyDamage(CardModel __instance, ref int __result)
{
// 将伤害翻倍
__result *= 2;
return false; // 跳过原方法
}[HarmonyPatch(typeof(SomeClass), "SomeMethod")]
[HarmonyTranspiler]
public static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
{
// 修改IL代码
}using System;
using System.Collections.Generic;
using MegaCrit.Sts2.Core.Models;
using MegaCrit.Sts2.Core.Entities.Cards;
using MegaCrit.Sts2.Core.Localization;
using MegaCrit.Sts2.Core.ValueProps;
[Serializable]
public class DoubleStrikeCard : CardModel
{
public DoubleStrikeCard()
{
Id = ModelId.FromString("double_strike");
Type = CardType.Attack;
Damage = 6;
BaseDamage = 6;
Cost = 1;
UpgradedCost = 0;
Rarity = CardRarity.Common;
DamageType = DamageType.Normal;
Name = new LocString("double_strike", "DOUBLE_STRIKE");
Description = new LocString("double_strike", "DOUBLE_STRIKE_DESCRIPTION");
ExtendedDescription = new LocString("double_strike", "DOUBLE_STRIKE_EXTENDED_DESCRIPTION");
}
public override bool ShouldReceiveCombatHooks => true;
public override Task AfterCardPlayed(PlayerChoiceContext context, CardPlay cardPlay)
{
// 造成两次伤害
var target = cardPlay.Target;
if (target != null)
{
for (int i = 0; i < 2; i++)
{
AttackCommand command = new AttackCommand
{
Target = target,
Damage = Damage,
Source = cardPlay.Card,
DamageType = DamageType
};
// 执行攻击...
}
}
return Task.CompletedTask;
}
}using MegaCrit.Sts2.Core.Modding;
[ModInitializer("Initialize")]
public class DoubleStrikeMod
{
public static void Initialize()
{
// 添加到普通卡池
ModHelper.AddModelToPool<CardPoolModel, DoubleStrikeCard>();
}
}{
"DOUBLE_STRIKE": "Double Strike",
"DOUBLE_STRIKE_DESCRIPTION": "Deal 6 damage twice.",
"DOUBLE_STRIKE_EXTENDED_DESCRIPTION": "Deal 6 damage twice."
}
using MegaCrit.Sts2.Core.Logging;
Log.Info("My mod is loaded!");
Log.Warn("Something unexpected happened");
Log.Error("Something went wrong");首次运行游戏后,在设置中同意Mod加载协议。
| 错误 | 解决方案 |
|------|----------|
| Mod manifest not found | 确保mod_manifest.json在.pck根目录 |
| Assembly load failed | 检查C#程序集引用是否正确 |
| Hook not firing | 确保模型注册到了正确的池 |
-
编译你的C#项目为DLL
-
创建一个.pck包,包含:
- mod_manifest.json
- 你的DLL
- 本地化文件(如果有)
- 将.pck文件放入
mods/目录
游戏支持Steam创意工坊分发:
-
打包.pck文件
-
上传到Steam创意工坊
-
玩家订阅后自动下载
| 类 | 用途 |
|---|------|
| AbstractModel | 所有游戏实体基类 |
| CardModel | 卡牌数据 |
| RelicModel | 遗物数据 |
| PowerModel | 能力/状态数据 |
| MonsterModel | 怪物数据 |
| CombatState | 战斗状态 |
| IRunState | Run状态接口 |
| 接口 | 用途 |
|------|------|
| IPoolModel | 可加入卡池的模型 |
| ITemporaryPower | 临时能力 |
| 类 | 用途 |
|---|------|
| ModHelper | Mod辅助API |
| ModelDb | 模型数据库 |
| Hook | 静态钩子触发器 |
| LocManager | 本地化管理 |
| Rng | 随机数生成 |
使用 Net* 开头的类处理网络同步:
-
NetPlayCardAction -
NetEndPlayerTurnAction
实现 IAutoPlayable 接口支持自动战斗。
使用 EventModel 创建自定义事件。
Q: 如何添加新角色?
A: 继承相关模型类并注册到游戏系统。
Q: 如何修改现有卡牌?
A: 使用Harmony的Postfix修改属性值。
Q: Mod会破坏存档吗?
A: 建议使用Mod时创建新存档,游戏不会阻止但可能产生兼容性问题。
Q: 如何卸载Mod?
A: 从mods目录删除.pck文件,或在游戏设置中禁用。
完整Hook列表请参考 src/Core/Hooks/Hook.cs,该文件包含100+个静态方法,每个对应一个游戏时机。
-
framework.md - 项目架构详解
-
Hook.cs源文件 - Hook API完整定义
-
ModManager.cs源文件 - Mod加载器实现