|
| 1 | +## 代码 |
| 2 | + |
| 3 | +创建一个新的`Cards`文件夹方便管理,并创建新的`cs`文件,例如`TestCard.cs`。 |
| 4 | + |
| 5 | +```csharp |
| 6 | +using BaseLib.Abstracts; |
| 7 | +using BaseLib.Utils; |
| 8 | +using MegaCrit.Sts2.Core.Commands; |
| 9 | +using MegaCrit.Sts2.Core.Entities.Cards; |
| 10 | +using MegaCrit.Sts2.Core.GameActions.Multiplayer; |
| 11 | +using MegaCrit.Sts2.Core.Localization.DynamicVars; |
| 12 | +using MegaCrit.Sts2.Core.Models.CardPools; |
| 13 | +using MegaCrit.Sts2.Core.ValueProps; |
| 14 | + |
| 15 | +namespace Test.Scripts.Cards; |
| 16 | + |
| 17 | +// 加入哪个卡池 |
| 18 | +[Pool(typeof(ColorlessCardPool))] |
| 19 | +public class TestCard : CustomCardModel |
| 20 | +{ |
| 21 | + // 基础耗能 |
| 22 | + private const int energyCost = 1; |
| 23 | + // 卡牌类型 |
| 24 | + private const CardType type = CardType.Attack; |
| 25 | + // 卡牌稀有度 |
| 26 | + private const CardRarity rarity = CardRarity.Common; |
| 27 | + // 目标类型(AnyEnemy表示任意敌人) |
| 28 | + private const TargetType targetType = TargetType.AnyEnemy; |
| 29 | + // 是否在卡牌图鉴中显示 |
| 30 | + private const bool shouldShowInCardLibrary = true; |
| 31 | + |
| 32 | + // 卡牌的基础属性(例如这里是12点伤害) |
| 33 | + protected override IEnumerable<DynamicVar> CanonicalVars => [new DamageVar(12, ValueProp.Move)]; |
| 34 | + |
| 35 | + public TestCard() : base(energyCost, type, rarity, targetType, shouldShowInCardLibrary) |
| 36 | + { |
| 37 | + } |
| 38 | + |
| 39 | + // 打出时的效果逻辑 |
| 40 | + protected override async Task OnPlay(PlayerChoiceContext choiceContext, CardPlay cardPlay) |
| 41 | + { |
| 42 | + await DamageCmd.Attack(DynamicVars.Damage.BaseValue) // 造成伤害,数值来源于卡牌的基础伤害属性 |
| 43 | + .FromCard(this) // 伤害来源于这张卡牌 |
| 44 | + .Targeting(cardPlay.Target) // 伤害目标是玩家选择的目标 |
| 45 | + .Execute(choiceContext); |
| 46 | + } |
| 47 | + |
| 48 | + // 升级后的效果逻辑 |
| 49 | + protected override void OnUpgrade() |
| 50 | + { |
| 51 | + DynamicVars.Damage.UpgradeValueBy(4); // 升级后增加4点伤害 |
| 52 | + } |
| 53 | +} |
| 54 | +``` |
| 55 | + |
| 56 | +* `CanonicalVars`翻译是“规范值”,指卡牌的基础数值。添加一个`DamageVar`意为指定卡牌的基础伤害是多少,例如这里是`12`。 |
| 57 | + |
| 58 | +* `ValueProp`表示数值的属性,例如`ValueProp.Move`表示是通过卡牌造成的伤害/格挡,`ValueProp.Unpowered`表示不受修正影响(如力量等),`ValueProp.Unblockable`表示伤害不可被格挡,`ValueProp.SkipHurtAnim`表示跳过受伤动画。这是一个bitflag类型的枚举,你可以进行组合,例如`ValueProp.Unblockable | ValueProp.Unpowered`,不可被格挡也不受修正影响。 |
| 59 | + |
| 60 | +* 尖塔2使用了`async`和`await`来控制效果逻辑顺序执行,比如选择一张牌时就一直`await`不让后续代码执行,和尖塔1的`action`类似的生态位。此处的`OnPlay`中写了一个造成单体伤害的指令。 |
| 61 | + |
| 62 | +* 想做什么样的卡牌,看原版代码哪张有类似的效果,参考即可。 |
| 63 | + |
| 64 | +* 添加一个`Pool`的attribute,并指定要添加的颜色卡池,然后会自动注册。 |
| 65 | +* 继承`CustomCardModel`而不是`CardModel`。 |
| 66 | +* <b>注意</b>:通过`baselib`添加卡牌,其id会变成`{命名空间第一段大写}-{原卡牌id}`,例如`namespace Test.Scripts;`取`TEST`,原始卡牌id为`TEST-CARD`,是`TestCard`的大写snake-case,最后变成`TEST-TEST_CARD`。 |
| 67 | + |
| 68 | +## 卡图 |
| 69 | + |
| 70 | +可以通过在卡牌类中添加一个表达式属性来添加卡牌,这样的话可以任意指定位置:`public override string PortraitPath => $"res://{modid}/images/cards/{Id.Entry.ToLowerInvariant()}.png";`, |
| 71 | +如果这样,那么路径就是`test/images/cards/test-test_card.png`(是你类名的`snake_case`命名风格加前缀,例如`TestCard`即为`test-test_card`)。当然按你的喜好组织资源路径也可。 |
| 72 | + |
| 73 | +> 或者你也可以使用`public override string PortraitPath => $"res://{modid}/images/cards/{nameof(TestCard)}.png";`,这样更方便,卡图名字设置为`TestCard.png`就行。 |
| 74 | +
|
| 75 | +`modId`即为你`{modId}.json`中填写的。<b>不是你的根目录,而是一个新文件夹。</b> |
| 76 | + |
| 77 | + |
| 78 | +卡图任意尺寸都可,且不需要裁剪,官方使用的尺寸是普通卡1000x760,先古卡606x852。 |
| 79 | + |
| 80 | +```csharp |
| 81 | +public class TestCard : TestCardModel |
| 82 | +{ |
| 83 | + private const int energyCost = 1; |
| 84 | + private const CardType type = CardType.Attack; |
| 85 | + private const CardRarity rarity = CardRarity.Common; |
| 86 | + private const TargetType targetType = TargetType.AnyEnemy; |
| 87 | + private const bool shouldShowInCardLibrary = true; |
| 88 | + |
| 89 | + protected override IEnumerable<DynamicVar> CanonicalVars => [new DamageVar(12, ValueProp.Move)]; |
| 90 | + |
| 91 | + // 添加这一行,指定卡牌立绘路径,这里是test/images/cards/TestCard.png |
| 92 | + public override string PortraitPath => $"res://test/images/cards/{nameof(TestCard)}.png"; |
| 93 | + |
| 94 | + public TestCard() : base(energyCost, type, rarity, targetType, shouldShowInCardLibrary) |
| 95 | + { |
| 96 | + } |
| 97 | +} |
| 98 | +``` |
| 99 | + |
| 100 | + |
| 101 | + |
| 102 | +你也可以通过新增一个`abstract`类,避免每张卡都写一遍卡图路径,并且方便管理一些自定义功能。 |
| 103 | + |
| 104 | +```csharp |
| 105 | +public abstract class TestCardModel : CardModel |
| 106 | +{ |
| 107 | + public override string PortraitPath => $"res://test/images/cards/{GetType().Name}.png"; |
| 108 | + |
| 109 | + public TestCardModel(int energyCost, CardType type, CardRarity rarity, TargetType targetType, bool shouldShowInCardLibrary) : base(energyCost, type, rarity, targetType, shouldShowInCardLibrary) |
| 110 | + { |
| 111 | + } |
| 112 | +} |
| 113 | + |
| 114 | +public class TestCard : TestCardModel {} |
| 115 | +``` |
| 116 | + |
| 117 | +## 文本 |
| 118 | + |
| 119 | +此外还需要本地化文件。创建一个`{modId}/localization/{Language}/cards.json`。 |
| 120 | +* `modId`即为你`{modId}.json`中填写的。<b>不是你的根目录,而是一个新文件夹。</b> |
| 121 | +* `Language`可以写`zhs`表示简体中文。填写`{CardId}.title`(卡牌名)和`{CardId}.description`(卡牌描述): |
| 122 | + |
| 123 | +```json |
| 124 | +{ |
| 125 | + "TEST-TEST_CARD.title": "测试卡牌", |
| 126 | + "TEST-TEST_CARD.description": "造成{Damage:diff()}点伤害。" |
| 127 | +} |
| 128 | +``` |
| 129 | + |
| 130 | +编译打包`dll`和`pck`后打开游戏。如果你在对应池子中看到卡牌说明成功了。如果没有任何卡牌(或者一张在左上角的卡牌)说明出问题了。 |
| 131 | + |
| 132 | +按`~`打开控制台输入`card TEST-TEST_CARD`获得这张卡。 |
| 133 | + |
| 134 | + |
| 135 | + |
| 136 | +## 最终项目参考 |
| 137 | + |
| 138 | +如果报错,回头看看。最终项目结构参考: |
| 139 | + |
| 140 | +``` |
| 141 | +Test (你的项目文件夹) |
| 142 | +├── Scripts (你的脚本文件夹,随意) |
| 143 | +│ ├── TestCard.cs |
| 144 | +│ └── Entry.cs |
| 145 | +└── Test (不要忘了这一层文件夹,是你的modid) |
| 146 | + ├── images |
| 147 | + │ └── cards |
| 148 | + │ └── test-test_card.png |
| 149 | + └── localization |
| 150 | + └── zhs |
| 151 | + └── cards.json |
| 152 | +``` |
0 commit comments