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
- 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.
- 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.
- 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.
- For a precise source-build check, the bundled integration test's
mechanicmode 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.
Start here
Playing
Tutorials
- Choose a tutorial
- 01 Sprite and Text Tweak
- 02 Balance Patch
- 03 New Species
- 04 New Item and Ball
- 05 New Move
- 06 NPC and Dialogue
- 07 New Map
- 08 Quest
- 09 Custom Music
- 10 New Mechanic
- 11 Custom UI Screen
- 12 Mini Total Conversion
Creative guides
Concepts
Recipes and reference
Author and developer tools
Home · Start Playing · Glossary · Troubleshooting · Repository · Community
Game data is imported locally from a supported ROM supplied by the player. Shared mods contain original work or transform recipes, never extracted game content. Check each guide's target game before using its examples.