Skip to content

Tutorial 10 New Mechanic

bryanthaboi edited this page Jul 19, 2026 · 1 revision

Tutorial 10 — New mechanic: status and type

Goal. Add a new status condition and a new damage type with chart rows, then scale difficulty through a battle hook — deep engine changes without forking a file.

Prerequisites. Tutorials 05 and 09; Concepts: Events and Hooks.

Files created.

mods/tutorial_10_mechanic/
├── manifest.json
└── main.lua

Steps

  1. Register a status. Statuses are records the battle engine, catch math, and HUD all read from one registry — vanilla's SLP/PSN/BRN/FRZ/ PAR are engine registrations in it (src/battle/Status.lua):

    return function(mod)
      mod.content.statuses:register("CURSE", {
        label = "CURSE", hudLabel = "CRS",
        -- end-of-turn tick, like PSN/BRN: damage the mon, return the text
        residual = function(battler, opponent, battle)
          local mon = battler.mon
          mon.hp = math.max(0, mon.hp - math.max(1, math.floor(mon.stats.hp / 16)))
          return { ("%s's\nhurt by the curse!"):format(battler.name) }
        end,
        onInflict = function(_, _, _, display)
          return { ("%s\nwas cursed!"):format(display) }
        end,
        catchBonus = 12,       -- the status term of the catch roll
        cureOnSwitch = true,
      })
    end

    Record fields (src/battle/Status.lua documents the consumers): label (required), hudLabel (the HUD glyph text), canInflict(target) / onInflict(battle, target, opts, display), beforeMove (+beforeMovePriority), residual(battler, opponent, battle) -> messages, catchBonus/shakeBonus (catch math), statPenalty ({ stat, div }, like PAR's speed or BRN's attack), cureOnSwitch. Outcome: the record validates.

  2. Inflict it from a move effect. Reuse tutorial 05's pattern:

    mod.content.move_effects:register("CURSE_EFFECT", {
      kind = "secondary",
      run = function(ctx) ctx.inflict(ctx.target, "CURSE") end,
    })
    mod.content.moves:register("MODCURSE", {
      id = "MODCURSE", name = "MODCURSE", type = "GHOST",
      category = "status", power = 0, accuracy = 100, pp = 10,
      effect = "CURSE_EFFECT",
    })

    Outcome: MODCURSE inflicts CURSE; the HUD shows CRS, residual damage ticks each turn, battle.status_inflicted fires, and the catch roll counts the bonus — every consumer reads the one record.

  3. Add a type and its chart rows. The type_chart registry holds both forms under one roof: bare type ids and "ATTACKER>DEFENDER" matchup rows.

    mod.content.type_chart:register("FAIRY", { category = "special" })
    mod.content.type_chart:register("FAIRY>DRAGON", { multiplier = 20 })
    mod.content.type_chart:register("FAIRY>FIRE", { multiplier = 5 })
    mod.content.type_chart:register("DRAGON>FAIRY", { multiplier = 0 })

    multiplier is base 10 (20 = super effective, 5 = not very, 0 = immune). The per-type category drives the physical/special split for moves that do not state their own. Mind the one odd vanilla id: the psychic type is PSYCHIC_TYPE. Outcome: a FAIRY-typed move hits Dragonite super-effectively, with the standard effectiveness text.

  4. A difficulty scaler through the damage hook.

    mod.hooks:wrap("battle.damage", function(next, ctx)
      local dmg, info = next(ctx)
      if not ctx.user.isPlayer then
        dmg = math.max(1, math.floor(dmg * 12 / 10))
      end
      return dmg, info
    end)

    Outcome: enemy damage lands 20% harder, applied after every vanilla modifier because the wrapper post-processes next's result. The link is pcall-guarded: throw from it and the chain logs, skips you, and the battle continues.

Checkpoint

CURSE ticks residual damage over several turns and clears on switch-out; a FAIRY move on a DRAGON target reports super-effective and doubles damage; enemy hits are scaled with the mod on and vanilla with it off.

Common pitfalls

  • Forgetting hudLabel. The status works but the HUD has nothing to draw for it; always name the glyph text.
  • Assuming a new type has a category. State category on the type (or on each move); an unknown type must not fall through the vanilla physical/special type list.
  • A hook that throws taking down the chain. It does not — the failing link is skipped and logged. Try it: error("boom") in the wrapper and watch the battle proceed with an attributed log line.

Next: Tutorial 11 — Custom UI Screen.

Clone this wiki locally