3 Tutorial 10 New Mechanic
bryanthaboi edited this page 2026-10-20 17:01:40 -04:00

Tutorial 10: New mechanic

Make: Create a curse status that removes HP over time, and receive an Abra that can apply it.

Before you start: Advanced. Complete Tutorial 05, Tutorial 06, and Events and Hooks. This lesson introduces a battle callback; it is a larger step than editing a stat.

Game: Red, local single-player. Use a practice save for experiments. If file creation is new to you, start with Tutorial 00; unfamiliar words are in the Glossary.

1. Set up the files

Create a new tutorial_10 folder in your mod location. Disable earlier tutorial mods for this exercise. The starting state is this empty folder; the finished state is all the files below.

This is a mod folder, not the engine root. Do not replace the engine's own main.lua. Preserve filenames and avoid an extra .txt extension.

Finished folder layout:

mods/tutorial_10/
  main.lua
  manifest.json

Each link opens a complete file you can save at the matching path. The JSON and Lua are also printed in full below; do not append them to another lesson's entry function.

File in this mod Complete example
main.lua Download main.lua
manifest.json Download manifest.json

2. Describe the mod

Complete file: replace tutorial_10/manifest.json:

{
  "id": "tutorial_10",
  "name": "Tutorial 10",
  "version": "1.0.0",
  "entry": "main.lua",
  "api": 2,
  "games": [
    "red"
  ]
}

id is the stable internal name, name is the label in MODS, and entry tells the loader which Lua file to open. games limits these examples to Red. Keep commas between JSON fields and no comma after the last field.

3. Write the change

Complete file: replace tutorial_10/main.lua:

return function(mod)
  mod.content.statuses:register("TUTORIAL_CURSE", {
    label = "CURSE",
    hudLabel = "CRS",
    cureOnSwitch = true,
    residual = function(battler)
      local mon = battler.mon
      local damage = math.max(1, math.floor(mon.stats.hp / 16))
      mon.hp = math.max(0, mon.hp - damage)
      return { battler.name .. " is cursed!" }
    end,
  })
  mod.content.move_effects:register("TUTORIAL_CURSE_EFFECT", {
    kind = "primary",
    accuracyChecked = true,
    run = function(ctx)
      return ctx.inflict(ctx.target, "TUTORIAL_CURSE")
    end,
  })
  mod.content.moves:register("TUTORIAL_CURSE_MOVE", {
    id = "TUTORIAL_CURSE_MOVE",
    name = "CURSE",
    type = "GHOST",
    category = "status",
    power = 0,
    accuracy = 100,
    pp = 10,
    effect = "TUTORIAL_CURSE_EFFECT",
  })
  mod.content.pokemon:patch("ABRA", {
    learnset = { __append = { { level = 5, move = "TUTORIAL_CURSE_MOVE" } } },
  })
  mod.content.maps:patch("PALLET_TOWN", {
    objects = { __append = {
      {
        index = 93, x = 10, y = 7, sprite = "SPRITE_SCIENTIST",
        movement = "STAY", range = "NONE", text = "TEXT_TUTORIAL_CURSE_GIVER",
        name = "TUTORIAL_CURSE_GIVER",
      },
    } },
  })
  mod.content.map_scripts:register("PALLET_TOWN", {
    talk = {
      TEXT_TUTORIAL_CURSE_GIVER = {
        { "check_flag", "mod:abra_given" }, { "jump_if_true", "after" },
        { "give_pokemon", "ABRA", 5, true },
        { "jump_if_false", "full" }, { "set_field", "mod:abra_given", true },
        { "show_text", "This ABRA knows\nCURSE. Try a battle!" }, { "jump", "end" },
        { "label", "full" }, { "show_text", "Make room and\ntalk again." }, { "jump", "end" },
        { "label", "after" }, { "show_text", "Watch CURSE deal\ndamage each turn." },
        { "label", "end" },
      },
    },
  })
end

Understand what you wrote

Three records cooperate: a status describes ongoing behavior, a move effect applies that status, and a move points to the effect. Their internal IDs connect the pieces. The scientist and Abra learnset provide a way to test them in play.

residual is a callback used for the status's ongoing damage. battler.mon contains the affected Pokémon. mon.stats.hp is maximum HP; mon.hp is current HP. The code rounds maximum HP divided by 16 down, keeps damage at least 1, and prevents current HP from going below zero. It returns a list of messages for the battle to display.

hudLabel supplies the short status label shown by the battle display (HUD means heads-up display). cureOnSwitch = true removes this condition when the affected battler switches out. This example adds no catch bonus or stat penalty; those are separate optional fields.

Because CURSE has zero base power, its effect uses kind = "primary" and returns the message list from ctx.inflict. A secondary effect belongs to the damaging-move path used in Tutorial 05 and will not make this zero-power move work. accuracyChecked = true asks the engine to perform the status move's accuracy check.

The move effect asks ctx.inflict to apply the status through the engine's status rules. Do not bypass those rules by assigning an arbitrary status string to a Pokémon. The move has zero base power, so the intended damage comes from the status rather than a normal damage calculation.

The scientist gives a newly created level-5 Abra. Its patched learnset includes CURSE at level 5, so that new Pokémon knows the move. mod:abra_given prevents repeated gifts on this save; it is separate from the quest values in tutorial_world.

4. Load and check it

Save every file, restart the app, enable the mod for Red in the launcher's MODS panel, and launch. Resolve any load error before checking gameplay. Troubleshooting shows where errors appear. With default desktop bindings, A is Z/Enter/Space, B is X/Backspace, and Start is Escape/keypad Enter; use your configured controls if different.

Optional source-checkout validation

If you have Python 3, LuaJIT, and the engine source with Red imported, open a terminal in the engine root containing tools/. For a mod installed in that checkout's mods directory, run:

python3 tools/modkit.py validate tutorial_10 --base imported

If you saved it elsewhere, replace the mod ID with the quoted full path to its folder. A validation error identifies a file or content rule to fix; a pass means you can continue to the in-game checks. It does not prove rendering, sound, or the entire playthrough works. Packaged-app users can use MODS errors and the checkpoints without setting up these command-line tools.

Checkpoint

  1. On a practice Red save with room for another party member, talk to the scientist at Pallet Town player cell (10, 7). Receive a level-5 Abra and check that its move list includes CURSE.
  2. Put Abra first and enter a wild battle against a healthy target with no other status condition. Select CURSE. Observe the status label and curse message, then its repeated HP loss while the condition remains active.
  3. Use an action that keeps the battle going to observe another tick. Do not compare a defeated or already-statused target with a fresh one.
  4. For a precise source-build check, the bundled integration test's mechanic mode creates Abra with the engine's Pokémon constructor and invokes the registered status callback with known HP. Automated callback checks are separate from this in-game display check.

Common pitfalls

What you see What to check
Scientist will not give another Abra The successful gift is saved. Use a fresh practice slot; do not rename the mod merely to reset the flag.
Abra has no CURSE Use the newly gifted level-5 Abra after restarting; an existing party member is not automatically retaught.
Status does not take hold Test a healthy target without another status, and inspect errors from the effect callback.

For syntax, missing-file, and restart problems, use Troubleshooting. Compare the whole finished file before changing several things at once.

Undo and continue

Keep the practice save and its matching mod files together. Saves can contain this example's custom content or progress. To compare with vanilla Red, disable the mod, restart, and open a separate unmodified practice slot. Do not overwrite the only copy of a modded save while its content is disabled. Save recovery explains backups.

Optional: a new type or damage hook

Finish the curse check before adding another battle rule. Battle and Mechanics recipes cover a new type, its matchups, and damage hooks. Type-chart multipliers use 10 as normal effectiveness: 20 means double, 5 means half, and 0 means immunity.

A battle.damage wrapper must preserve both returned values, damage and its information record. Read the precise contract in Hook Reference before adapting it. New types and difficulty scaling are optional extensions, not hidden prerequisites for this status lesson.

Return to the tutorial index to choose the next lesson.