121 - API文档汉化 - 自定义卡牌费用系统

Updated a week ago

自定义卡牌费用系统

自定义卡牌费用

在《邪恶冥刻》中,玩家将遇到四种不同的资源类型:血祭(Blood)、骨头/兽骨(Bones)、能量(Energy)以及玛珂/宝石(Mox/Gems)。
这些资源用于支付具有相应费用类型的卡牌,大多数卡牌至少需要消耗其中一种资源。
通过本API,您可以创建需要消耗多种资源类型的卡牌,为模组开发者提供更丰富的创作空间。

当基础费用类型无法满足需求时,或者您希望创建全新费用类型时,API提供了通过CardCostManager创建基础卡牌费用的多种方式。

社区补丁中包含一个TestCost类,实现了本页将介绍的诸多功能。

创建新费用类型

要创建新的卡牌费用类型,需要新建一个继承自API中CustomCardCost类的子类。

CustomCardCost类已提供确保费用正常运作的基础功能。
请注意:API不提供自定义费用资源的支持,您需要通过补丁自行实现相关逻辑。

创建费用类后,需向API注册该类型。完整示例如下:

// 为简化示例,本费用类型复制了能量费用的逻辑
public class MyCardCost : CustomCardCost
{
    // 必需字段,应与注册时传入API的名称一致
    public override string CostName => "TestCost";

    // 检查当前卡牌是否满足费用支付条件
    public override bool CostSatisfied(int cardCost, PlayableCard card)
    {
        // 判断玩家能量是否足够支付(需考虑基础能量消耗)
        return cardCost <= (ResourcesManager.Instance.PlayerEnergy - card.EnergyCost);
    }

    // 当费用不足时显示的提示文本
    public override string CostUnsatisfiedHint(int cardCost, PlayableCard card)
    {
        return $"Eat your greens aby. {card.Info.DisplayedNameLocalized}";
    }

    // 卡牌成功打出后触发的逻辑
    // 如需消耗资源,应在此处实现相关逻辑
    public override IEnumerator OnPlayed(int cardCost, PlayableCard card)
    {
        // 扣除玩家对应数量的能量
        yield return ResourcesManager.Instance.SpendEnergy(cardCost);
    }
}

public void AddCost()
{
    // 注册时需要提供两个Func:分别用于获取3D模式下的费用纹理和第二章的像素纹理
    // 若费用仅限特定章节使用,可为对应Func传入null
    CardCostManager.Register("api", "TestCost", typeof(TestCost), TextureMethod, PixelTextureMethod);
}

注册费用时,可直接使用静态方法代替委托创建。
这些方法必须包含三个参数类型:int, CardInfo, PlayableCard;并返回Texture2D对象。
int参数表示卡牌费用值,CardInfo和PlayableCard参数表示当前检查的卡牌信息(PlayableCard可能为null)

public static Texture2D TextureMethod(int cardCost, CardInfo info, PlayableCard card)
{
    return TextureHelper.GetImageAsTexture($"myCost_{cardCost}");
}

public static Texture2D PixelTextureMethod(int cardCost, CardInfo info, PlayableCard card)
{
    return TextureHelper.GetImageAsTexture($"myCost_pixel_{cardCost}");

    // 如需API自动处理堆叠数字显示,可提供7x8纹理如下:
    // return Part2CardCostRender.CombineIconAndCount(cardCost, TextureHelper.GetImageAsTexture("myCost_pixel_7x8"));
}

不同章节的费用纹理规格要求:
第一章需64x28像素;第二章最大30x8像素(或7x8像素,见上);第三章无固定尺寸,但建议不超过300x78像素。

负费用、费用等级与第二回合可打出机制

API允许定义是否允许负费用值,默认情况下为false(负值会被视为0,但实际赋值不变)。
可通过SetCanBeNegative方法修改该设置,或直接修改CanBeNegative字段。

费用等级(Cost Tier)是表示卡牌消耗量的整数值,每种费用类型有其独立的计算公式。
例如骨头费用的计算公式为(数量/3)向下取整。
默认情况下,自定义费用不计入卡牌费用等级计算,可通过SetCostTier方法定义计算函数。

public static void Init()
{
    FullCardCost cost = CardCostManager.Register(InscryptionAPIPlugin.ModGUID, "TestCost", typeof(TestCost), Texture3D, TexturePixel);
    
    cost.SetCostTier(CostTier);
}

public static int CostTier(int amount)
{
    return Mathf.FloorToInt(amount / 2f);
}

公平手牌机制是游戏核心玩法之一:
战斗开始时,系统会保证玩家至少获得一张可立即打出的卡牌,以及一张第二回合可打出的卡牌。
默认情况下,带有自定义费用的卡牌在检查第二回合可打出性时总是返回2(即使实际不可打出)。
需通过设置CanBePlayedByTurn2WithHand函数修正该行为:

public static void Init()
{
    CardCostManager.FullCardCost fullCardCost = CardCostManager.Register(InscryptionAPIPlugin.ModGUID, "TestCost", typeof(TestCost), Texture3D, TexturePixel);
    fullCardCost.SetCanBePlayedByTurn2WithHand(CanBePlayed);
}

// amount参数为卡牌费用值,hand参数为玩家手牌列表
public static bool CanBePlayed(int amount, CardInfo card, List<CardInfo> hand)
{
    // TestCost仿照能量费用逻辑,费用值≤2即可第二回合打出
    return amount <= 2;
}

费用选择节点

如需在第一章的费用选择节点出现您的自定义费用,API提供如下实现方式:

public static void Init()
{
    CardCostManager.FullCardCost fullCardCost = CardCostManager.Register(InscryptionAPIPlugin.ModGUID, "TestCost", typeof(TestCost), Texture3D, TexturePixel);

    // 设为true将为该费用分配专属ResourceType,使其出现在选择节点
    // rewardBack参数为选择节点显示的纹理(125x190像素)
    fullCardCost.SetFoundAtChoiceNodes(isChoice: true, rewardBack: (Texture2D)ResourceBank.Get<Texture>("Art/Cards/RewardBacks/card_rewardback_bird"));
}

您可能对ResourceType产生疑问——简言之,该值用于决定向玩家提供哪些费用选择,以及玩家可获得哪些卡牌。

不同费用数量

除ResourceType外,系统还会根据资源数量进一步区分选项。
默认情况下,选择节点的自定义费用将提供所有有效卡牌(无论所需资源数量)。
这与骨头和能量的处理方式类似,但若您希望像血祭费用那样提供1/2/3不同等级的选项,可使用如下变体方法:

public static void Init()
{
    CardCostManager.FullCardCost fullCardCost = CardCostManager.Register(InscryptionAPIPlugin.ModGUID, "TestCost", typeof(TestCost), Texture3D, TexturePixel);

    // 此版本方法需提供Func而非Texture2D
    // 该Func用于确定每个有效数量对应的奖励背景纹理
    fullCardCost.SetFoundAtChoiceNodes(isChoice: true, rewardBackFunc: GetRewardBack, 1, 2, 4);

    // 后续费用选择节点可能提供消耗1/2/4个TestCost的卡牌选项
}

也可直接设置FullCardCost对象的ChoiceAmounts字段(需传入包含数量值的整型数组)。

费用类型分组

当添加多个自定义费用时,您可能希望将它们归入同一选择组。
无论出于关联性考虑,还是避免挤占其他选项空间,API均支持此功能。

private void Example()
{
    FullCardCost cost = CardCostManager.Register(...);
    cost.ChoiceAmounts = new int[] { 1, 4, 7 };
}

分组自定义费用必须共享相同的ResourceType值:

public static void Init()
{
    // 首先标记首个费用为可选
    CardCostManager.FullCardCost fullCardCost = CardCostManager.Register(InscryptionAPIPlugin.ModGUID, "TestCost", typeof(TestCost), Texture3D, TexturePixel);
    fullCardCost.SetFoundAtChoiceNodes(isChoice: true, rewardBack: (Texture2D)ResourceBank.Get<Texture>("Art/Cards/RewardBacks/card_rewardback_bird"));

    // 后续费用需设置相同ResourceType
    CardCostManager.FullCardCost fullCardCost2 = CardCostManager.Register(InscryptionAPIPlugin.ModGUID, "TestCost2", typeof(TestCost2), Texture3D2, TexturePixel2);
    fullCardCost2.ResourceType = fullCardCost.ResourceType;
}

注意:分组费用与多数量费用不兼容,若分组中某费用已定义多种可选数量,这些设置将被忽略。

为卡牌添加费用

通过API的扩展属性系统为卡牌添加自定义费用,并可通过相同方式访问。
为明确用途,API提供以下扩展方法设置CardInfo的自定义费用:

public void AddCard()
{
    CardInfo info = CardManager.New("myMod", "custom_card", "Card", 1, 1);
    info.SetCustomCost("TestCost", 1);

    int cost = info.GetCustomCost("Test") // 返回1
}

死亡卡牌的自定义费用

死亡卡牌(Death cards)本质上是一种应用到特定应用到模板卡牌上的修改了的原版卡牌。
因此直接添加扩展属性无效(属性会应用于该卡牌的所有副本)。
如需创建使用自定义费用的死亡卡牌,必须新建CardInfo并添加属性。

幸运的是,API的DeathCardManager可处理此流程。
CreateCustomDeathCard()方法将返回代表自定义死亡卡牌的CardInfo对象,并根据提供的CardModificationInfo设置名称、属性等数据。

private void AddCustomDeathCard()
{
    CardModificationInfo deathCardMod = new CardModificationInfo(2, 2)
        .SetNameReplacement("Mabel").SetSingletonId("wstl_mabel")
        .SetBonesCost(2).AddAbilities(Ability.SplitStrike)
        .SetDeathCardPortrait(CompositeFigurine.FigurineType.SettlerWoman, 5, 2)
        .AddCustomCostId("CustomCost", 1);

    // 可将新建的死亡卡牌加入默认列表
    DeathCardManager.AddDefaultDeathCard(deathCardMod);
}

进阶功能

旧版API中,自定义费用(至少其纹理)由社区补丁处理。
这些方法仍可使用,本节将介绍旧版纹理添加方式。

using InscryptionCommunityPatch.Card;
using InscryptionAPI.Helpers;

Part1CardCostRender.UpdateCardCost += delegate(CardInfo card, List<Texture2D> costs)
{
    int myCustomCost = card.GetExtensionPropertyAsInt("myCustomCardCost") ?? 0; // GetExtensionPropertyAsInt可能返回null
    if (myCustomCost > 0)
        costs.Add(TextureHelper.GetImageAsTexture($"custom_cost_{myCustomCost}.png"));
}

为第二章添加自定义费用有两种主要方式:

using InscryptionCommunityPatch.Card;
using InscryptionAPI.Helpers;

// 方式一:使用7x8像素图标(由API处理堆叠数字)
Part2CardCostRender.UpdateCardCost += delegate(CardInfo card, List<Texture2D> costs)
{
    int myCustomCost = card.GetExtensionPropertyAsInt("myCustomCardCost_pixel") ?? 0;
    if (myCustomCost > 0)
    {
        Texture2D customCostTexture = TextureHelper.GetImageAsTexture($"custom_cost_pixel.png");
        costs.Add(Part2CardCostRender.CombineIconAndCount(myCustomCost, customCostTexture));
    }
}

// 方式二:使用30x8像素纹理(自主控制显示效果)
Part2CardCostRender.UpdateCardCost += delegate(CardInfo card, List<Texture2D> costs)
{
    int myCustomCost = card.GetExtensionPropertyAsInt("myCustomCardCost_pixel") ?? 0;
    if (myCustomCost > 0)
    {
        costs.Add(TextureHelper.GetImageAsTexture($"custom_cost_{myCustomCost}_pixel.png"));
    }
}

第三章实现

第三章的卡牌费用显示更为复杂,因其采用3D对象而非平面纹理。
这意味着您对费用外观拥有更高控制权。

提供两个自定义费用事件:
Part3CardCostRender.UpdateCardCostSimple:快速提供/修改卡牌费用纹理
Part3CardCostRender.UpdateCardCostComplex:支持直接修改GameObject,可附加任意组件

基础费用实现

若只需显示基础费用,准备代表单个费用单位的图标即可(如货币费用使用"$"符号)。
图标尺寸无严格限制,但显示区域为300x73像素,因此高度不应超过73像素,宽度越大则显示数量越少。

Part3CardCostRender.GetIconifiedCostTexture辅助方法接收图标和费用值,生成包含默认纹理(albedo)和自发光纹理(emissive)的纹理对。
当300x73区域可容纳全部图标时显示重复图标,否则显示7段数码管样式。
以货币费用为例:

  • 费用3显示为"$$$"
  • 费用10显示为"$ x10"

实现代码如下:

using InscryptionCommunityPatch.Card;
using InscryptionAPI.Helpers;

MyIconTexture = TextureHelper.GetImageAsTexture("cost_icon.png");
Part3CardCostRender.UpdateCardCostSimple += delegate(CardInfo card, List<Part3CardCostRender.CustomCostRenderInfo> costs)
{
    int myCustomCost = card.GetExtensionPropertyAsInt("myCustomCardCost");
    costs.add(new ("MyCustomCost", Part3CardCostRender.GetIconifiedCostTexture(MyIconTexture, myCustomCost)));
}

高级费用实现

通过UpdateCardCostComplex事件可直接修改GameObject,但需要掌握Unity引擎知识:

using InscryptionCommunityPatch.Card;
using InscryptionAPI.Helpers;
using UnityEngine;

Part3CardCostRender.UpdateCardCostSimple += delegate(CardInfo card, List<Part3CardCostRender.CustomCostRenderInfo> costs)
{
    costs.add(new ("MyCustomCost"));
}

Part3CardCostRender.UpdateCardCostComplex += delegate(CardInfo card, List<Part3CardCostRender.CustomCostRenderInfo> costs)
{
    GameObject costObject = costs.Find(c => c.name.Equals("MyCustomCost")).CostContainer;
    // 可直接操作costObject添加组件
}