Skip to main content

Enchants

Enchants are handled two ways: a strategy-wide policy that says which enchant to prefer for each action family, and a per-cast chain that spells out an exact sequence.

Policies

enchant:
auto: true
damage: { prefer: epic, fallback: colossal }
mutation: { prefer: shadow }
blade: { prefer: sharpen }
trap: { fallback: potent }
shield: { prefer: aegis }
heal: { prefer: epic }
support: { prefer: sharpen }
KeyApplies to
autoEnchant damage spells automatically each round
damageDamage spells
mutationMutation enchants — shadow variants and similar
bladeBlades and charms
trapTraps and wards
shieldShields
healHeals
supportGeneric fallback for non-damage actions with no specific policy
prefer / fallbackLegacy global defaults, used when nested policies are absent

Each policy has prefer and fallback. Both are card-name substrings, not exact names — prefer: epic matches any enchant whose name contains "epic". Try prefer first, then fallback, then give up.

Which policy applies

This is the part that catches people, because the choice depends on what the card is, not on what you called the action.

The actionUsesThen
blade:enchant.bladeenchant.support, then the top-level prefer/fallback
trap:enchant.trapenchant.support, then the top-level prefer/fallback
shield:enchant.shieldenchant.support, then the top-level prefer/fallback
heal:enchant.healenchant.support, then the top-level prefer/fallback
cast: on a damage cardenchant.damagethe top-level prefer/fallbacknot support
cast: on any other spellenchant.supportthe top-level prefer/fallback
A chain step type: damageenchant.damagethe top-level prefer/fallback
A chain step type: mutationenchant.mutationthe top-level prefer/fallbacknot support
A chain step of any other typethe top-level prefer/fallback only
enchant:
prefer: epic # legacy global
support: { prefer: sharpen }
trap: { prefer: potent }

A trap: action uses potent. A blade: action has no blade: policy, so it falls to support and uses sharpen. A cast: that matched a damage spell has no damage: policy, so it falls to the global and uses epic.

Two edges worth knowing:

  • An empty family block is not the same as an absent one. damage: {} stops the lookup there and yields no policy — it does not fall through to support or the global. Delete the block rather than emptying it.
  • A cast: that matched something with no obvious family — a spell the game reports as "other", or one whose type could not be read — goes straight to the global prefer/fallback, and if those are empty it will take the first castable enchant card in hand. That is the one case where enchant: required can grab an enchant you did not ask for.
enchant.blade does not apply to cast:

A cast: action is classified by the spell type of the card it matched. If that card is a blade, it is a charm, so it takes enchant.support — your enchant.blade policy is not consulted. The same goes for a cast: that lands on a trap, a shield, or a heal.

# BROKEN: no support policy, so `required` fails and this never casts
enchant:
blade: { prefer: sharpen }
actions:
- cast: { name: Fire Blade, enchant: required }

# Fixed, either way:
enchant:
blade: { prefer: sharpen }
support: { prefer: sharpen } # ...or spell the chain out on the action:
actions:
- cast:
name: Fire Blade
enchant: required
enchants:
- prefer: sharpen

An explicit enchants: chain sidesteps the whole question, which is why the shipped team example uses one.

enchant: required with no policy anywhere fails the action

When nothing resolves to a prefer or fallback — no family policy, no support, no top-level prefer/fallback, and no explicit enchants: — an action marked enchant: required is skipped, every round, silently. It is not "enchant if you can"; it is "only cast this if a policy told me what to enchant with".

Common enchants

FamilyTypical
Damageepic, colossal, gargantuan, monstrous
MutationShadow mutations for cards like Glowbug Squall
Blade / charmsharpen
Trap / wardpotent
Shieldaegis
Other supportindemnity

Sharpen and Potent make an effect bigger; Aegis and Indemnity make it protected from removal. They are two different axes, and the target-scoped conditions expose them separately: enchanted: true asks "did this effect come from an enchanted card", which is how you ask whether the good blade is already down, and protected: true asks whether it is shielded from being stripped.

Per-action enchant control

- cast: { type: damage, school: storm, enchant: required }
- cast: { type: damage, school: storm, enchant: none }
- cast: { type: damage, school: storm }
ValueBehaviour
requiredFail the action if no policy resolves, or if no matching enchant is in hand
noneCast the card plain, ignoring policy
omittedDo nothing per-action; only the round-start enchant.auto pass can still touch the card

required and none are the only two values. Anything else — true, auto, yes — is treated as neither: the action still tries to enchant, but a failure is not fatal. The schema rejects those, so your editor will catch them; the loader will not.

Only cast, blade, trap, shield, and heal take enchant:. wand_hit, discard, summon_minion, and the bare scalars do not.

required is the useful one. It turns "enchant if you can" into "only cast this if it can be enchanted", which is how you write "throw the big nuke, but only buffed":

actions:
- cast: { name: 'Glowbug Squall', enchant: required } # only if enchantable
- cast: { name: 'Tempest', enchant: none } # otherwise the cheap one

Without required on the first line, an unenchanted Glowbug Squall gets thrown and wasted.

Enchant chains

enchants: applies several enchants to one card, in order:

- cast:
name: Glowbug Squall
enchant: required
enchants:
- type: mutation
- type: damage

Each step accepts type, prefer, fallback, and optional.

enchants:
- type: mutation
prefer: shadow
- type: damage
prefer: epic
fallback: colossal
optional: true

enchants: is a cast: field only — the support actions do not take one.

optional: true lets the chain continue when that step finds nothing. It only means anything under enchant: required; without required, every step is best-effort already and optional is redundant.

Chains matter for cards that need a mutation before a damage buff — mutate the card to the right school first, then add damage. Order is the order you write.

Two mechanics make chains work that the single-policy path does not allow:

  • A step after the first may enchant an already-enchanted card. That is the whole point — the second enchant lands on top of the first. The plain policy path refuses an already-enchanted card and returns it untouched.
  • The hand is re-read between steps, so step two sees the board as step one left it.

A step's prefer/fallback override the policy; leave them out and the policy for that step's type fills them in (see which policy applies).

Step types

Valid type values, with their aliases:

CanonicalAliases
damagecard_damage
accuracyaccurate
mutationmutate, variant, shadow
rankpip, pips
(any)any, or omit type entirely

Anything else is a load error naming the step index:

action 1 in phase "main" of strategy "x" has unknown enchant step type "sharpen" at index 1

That error catches the common mistake of putting a card name where a type belongs. sharpen is a prefer value, not a type:

# wrong
enchants:
- type: sharpen

# right
enchants:
- prefer: sharpen

Omitting type matches any enchant, which is what you want when you only care about the preferred card name. The example strategy life_fire.yml does exactly that:

- cast:
name: 'Tri Blade'
enchant: required
enchants:
- prefer: sharpen

auto

enchant:
auto: true
damage: { prefer: epic, fallback: colossal }

At the start of each round, before any phase is evaluated, auto walks your hand and enchants every damage spell it can using the damage policy. It skips cards that are already enchanted and cards the game says cannot be enchanted, and it touches damage spells only — blades, traps, shields, and heals are never auto-enchanted.

auto: true on its own does nothing

auto uses the damage policy, and if there is no enchant.damage block and no top-level prefer/fallback, there is no policy — so the pass runs and enchants nothing. It does not fall back to enchant.support and it does not pick an arbitrary enchant.

# does nothing
enchant:
auto: true
blade: { prefer: sharpen }

# works
enchant:
auto: true
damage: { prefer: epic }

Convenient for a straightforward hitter; less good when you want enchants saved for specific cards. Turn it off and use enchant: required per action when your enchant supply is limited and you care which card gets buffed.

Debugging

An action with enchant: required that never fires has three possible causes, in the order worth checking:

  1. No policy resolves. Work through which policy applies for the card the action matched, not the action's name. This is the common one.
  2. No matching enchant is in hand.
  3. The card cannot take another enchant — it is already enchanted and this is not a multi-step chain.

To see the hand, turn on the console's debug logging:

enable dbg

Each round the runner then prints the whole hand, with each card's spell type and whether it is castable, followed by the enchant attempt itself — which step it is on, the prefer and fallback it is using, and whether anything matched:

strategy[storm]: hand=[Epic{type=Enchant castable=true}, Tempest{type=Damage castable=true}]
tryEnchantCard: mode="required" step=1 type="damage" card=Tempest{...} prefer="epic" fallback=""

That trace is console-only; the desktop app does not wire it up.

From a plugin, c.combat:GetHand() gives you the same list — see CombatController → Reading.

Either way, confirm that a card actually contains the prefer/fallback substring you configured. Matching is a case-insensitive substring test against both the card's display name and its internal name — not fuzzy. prefer: epic will not find Epicc, and, more usefully, it will match anything else containing "epic".

If a chain step is failing, add optional: true temporarily to isolate which step is the problem.