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 }
| Key | Applies to |
|---|---|
auto | Enchant damage spells automatically each round |
damage | Damage spells |
mutation | Mutation enchants — shadow variants and similar |
blade | Blades and charms |
trap | Traps and wards |
shield | Shields |
heal | Heals |
support | Generic fallback for non-damage actions with no specific policy |
prefer / fallback | Legacy 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 action | Uses | Then |
|---|---|---|
blade: | enchant.blade | enchant.support, then the top-level prefer/fallback |
trap: | enchant.trap | enchant.support, then the top-level prefer/fallback |
shield: | enchant.shield | enchant.support, then the top-level prefer/fallback |
heal: | enchant.heal | enchant.support, then the top-level prefer/fallback |
cast: on a damage card | enchant.damage | the top-level prefer/fallback — not support |
cast: on any other spell | enchant.support | the top-level prefer/fallback |
A chain step type: damage | enchant.damage | the top-level prefer/fallback |
A chain step type: mutation | enchant.mutation | the top-level prefer/fallback — not support |
| A chain step of any other type | the 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 tosupportor 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 globalprefer/fallback, and if those are empty it will take the first castable enchant card in hand. That is the one case whereenchant: requiredcan 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 actionWhen 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
| Family | Typical |
|---|---|
| Damage | epic, colossal, gargantuan, monstrous |
| Mutation | Shadow mutations for cards like Glowbug Squall |
| Blade / charm | sharpen |
| Trap / ward | potent |
| Shield | aegis |
| Other support | indemnity |
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 }
| Value | Behaviour |
|---|---|
required | Fail the action if no policy resolves, or if no matching enchant is in hand |
none | Cast the card plain, ignoring policy |
| omitted | Do 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:
| Canonical | Aliases |
|---|---|
damage | card_damage |
accuracy | accurate |
mutation | mutate, variant, shadow |
rank | pip, 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 nothingauto 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:
- No policy resolves. Work through which policy applies for the card the action matched, not the action's name. This is the common one.
- No matching enchant is in hand.
- 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.