Table of contents
- Cookbook: battle and mechanics
- R19 - Add a custom AI brain
- R27 - React to a battle event
- R28 - Wrap the damage hook
- R29 - Add a status condition
- R30 - Add a type + chart row
- R36 - Scale a battle sprite
- R38 - Gen-2-like ruleset toggles
- R39 - Shiny indicator overlay
- R49 - Pick Pokémon sprites on the fly
- R50 - Swap the player's trainer art
- R53 - Gen-2-style battle QoL toggles
- R56 - Color a trainer portrait, and give one party a real name
Cookbook: battle and mechanics
Start with a data change before adding a callback. Random battle results need a controlled inspection or repeated comparison.
Setup: a working mod from Getting Started. Examples target Red/local single-player unless a recipe explicitly says otherwise. Use Modding Basics for syntax and Glossary for unfamiliar terms. Save and restart after content edits. A validation pass and an in-game checkpoint are different checks.
Where code goes: insert Lua fragments inside your existing
return function(mod) ... end in main.lua. A complete-file label or named
other file overrides that default. Do not paste a second entry function
inside the first. Read the recipe's requirements before copying its code.
For output, use Read log output; for IDs,
use Find internal IDs.
Hook contracts: Reference: Hooks; worked path: Tutorial 10.
R19 - Add a custom AI brain
Advanced callbacks. Before you paste: Use a Red save before Brock and understand functions/loops. A brain chooses enemy actions; begin with the simple deterministic first-usable-move rule shown here.
Complete walkthrough / required concept.
Insert in main.lua inside the entry function (unless a filename is shown).
mod.content.trainers:patch("OPP_BROCK", {
brain = function(battle)
-- always pick the enemy's first usable move
for _, mv in ipairs(battle.enemy.curMoves) do
if mv.pp > 0 then return mv end
end
end,
})
Checkpoint: Brock picks moves your way; a nil return falls back to
Struggle handling. A brain supersedes the class action and move scoring
entirely (src/battle/BattleState.lua vanillaEnemyAction). Reusable
brains live in the ai_classes registry (kind = "brain"), named from
a trainer's aiClass; the battle.enemy_action hook intercepts every
trainer at once.
Gen 2 / Gen 3: a brain on the trainer record is Gen 1 only, but
battle.enemy_action has a call site on all three. Gold's ai_classes
are scoring layers with no brain kind. On Gen 3, an api 2 or api 3 mod registers an
ai_classes record with kind = "brain" and names it from the trainer row's
aiClass; the brain gets view.moves and returns a slot from 1 to 4. A
layer there scores with higher-is-better, the reverse of Gen 1.
R27 - React to a battle event
Events. Before you paste: Start a battle and choose a move while watching terminal log output. The event reports a completed move-use action; the listener does not change damage.
Complete walkthrough / required concept.
Insert in main.lua inside the entry function (unless a filename is shown).
mod.events:on("battle.move_used", function(ev)
mod.log:info("%s used %s", ev.user.name, ev.move.name)
end)
Checkpoint: a log line per move. The full battle event set
(battle.started, battle.turn_started/turn_ended,
battle.damage_dealt, battle.fainted, pokemon.caught, ...) is in
Reference: Events.
Gen 2 / Gen 3: fires on all three with the same keys. ev.move.name is
portable; ev.user is Gold's party mon and a native battler on Gen 3, which
also adds moveId and side.
R28 - Wrap the damage hook
Hooks. Before you paste: Read the hook contract first. next(ctx) runs the remaining calculation; this wrapper keeps both returned values and scales enemy damage.
Complete walkthrough / required concept.
Insert in main.lua inside the entry function (unless a filename is shown).
mod.hooks:wrap("battle.damage", function(next, ctx)
local dmg, info = next(ctx)
if not ctx.user.isPlayer then dmg = math.floor(dmg * 12 / 10) end
return dmg, info
end)
Checkpoint: enemy hits land 20% harder; with the mod disabled the chain
is empty and numbers are vanilla. Return both values; the second is
{ crit, typeMult }.
Gen 2 / Gen 3: the hook runs on both, but isPlayer is Gen 1's battler
field: Gold passes the party mon itself, and Gen 3 a native battler with
side ("player" or "enemy"). The second value is each game's own info
table.
R29 - Add a status condition
Advanced mechanics. Before you paste: Create a move effect that inflicts CURSE as described below. Registering the status alone does not apply it to anyone. HUD means the battle information display.
Complete walkthrough / required concept.
Insert in main.lua inside the entry function (unless a filename is shown).
mod.content.statuses:register("CURSE", {
label = "CURSE", hudLabel = "CRS",
residual = function(battler)
local mon = battler.mon
local max = mon.maxHp or mon.stats.hp
mon.hp = math.max(0, mon.hp - math.max(1, math.floor(max / 16)))
return { ("%s's\nhurt by the curse!"):format(battler.name) }
end,
catchBonus = 12, cureOnSwitch = true,
})
Checkpoint: a move effect calling ctx.inflict(ctx.target, "CURSE")
ticks residual damage each turn and shows CRS on the HUD. One record
feeds the HUD, the catch math, and the turn loop
(src/battle/Status.lua).
Gen 3: an api 2 or api 3 mod registers this record (gen3Statuses). The status
lives in its own per-battler slot beside the native status bits and lasts
the battle. canInflict, onInflict, beforeMove, residual,
cureOnSwitch, statPenalty and catchBonus are honoured, label or
hudLabel shows in the healthbox, and shakeBonus is not read. Gen 3
battlers have mon.maxHp, not mon.stats, which is why the residual above
reads both. Gold routes to gen2Statuses.
R30 - Add a type + chart row
Basic Lua + mechanics. Before you paste: Assign FAIRY to a move and use it on a Dragon target to exercise this matchup. The two registrations only define the type and matchup.
Complete walkthrough / required concept.
Insert in main.lua inside the entry function (unless a filename is shown).
mod.content.type_chart:register("FAIRY", { category = "special" })
mod.content.type_chart:register("FAIRY>DRAGON", { multiplier = 20 })
Checkpoint: FAIRY hits Dragons super-effectively. multiplier is base
10; bare ids are types (mind PSYCHIC_TYPE), "A>B" ids are matchup
rows; one registry holds both.
Gen 2 / Gen 3: both read this registry. Gen 3 seeds its 18 native types
and chart, gives a new type a free index from 18 to 255 (or the record's
index), and accepts PSYCHIC and MYSTERY as aliases.
R36 - Scale a battle sprite
Basic Lua + art. Before you paste: Choose a species or an explicit image path. Battle-scale numbers multiply image dimensions; they are not Pokemon stats. Test the back sprite with that species in your party.
Complete walkthrough / required concept.
Insert in main.lua inside the entry function (unless a filename is shown).
-- MEW's back pic renders 1.5x; its front pic is untouched
mod.content.pokemon:patch("MEW", { battleScaleBack = 1.5 })
-- or target one image by path (beats the species scale for that pic)
mod.content.battle_sprite_scales:register("abra_back", {
path = "assets/generated/battle/back/abrab.png",
scale = 1.5,
})
Checkpoint: the rescaled pic draws bigger with its feet still flush on
the ground line. Defaults are 1x front / 2x back, exactly the Game Boy
layout; battleScaleFront scales the enemy pic, battleScaleBack the
player pic, both 0.25 .. 4.0. The image-level registry is the only way
to reach non-species pics such as the player's trainer back sprite.
Anchoring and the send-out grow are handled for you
(src/battle/BattleState.lua resolveBattleScale, frontPlacement,
backPlacement).
Gen 2 / Gen 3: Gold reads both in the same order, but its default back
scale is 1x (48x48 pics), so a scale tuned on Red draws twice as large there.
Gen 3 has no target: battle_sprite_scales is dropped and reported, and an
api 3 mod gets unknown field battleScaleBack.
R38 - Gen-2-like ruleset toggles
Advanced mechanics. Before you paste: This copies the existing ruleset and registers a selectable variant. Choose CLEAN in Options after loading; registration alone does not select it.
Complete walkthrough / required concept.
rulesets accept unknown fields. Ship a derived ruleset (see
example_weather) and gate quirks the engine already reads:
| Field | gen1_faithful |
modern_clean |
|---|---|---|
oneIn256Miss |
true | false |
focusEnergyBug |
true | false |
critUsesBaseSpeed |
true | true |
enemyUnlimitedPP |
true | false |
hyperBeamSkipRechargeOnKO |
true | false |
Insert in main.lua inside the entry function (unless a filename is shown).
local base = mod.content.rulesets:get("gen1_faithful")
local clean = {}
for k, v in pairs(base) do clean[k] = v end
clean.name = "CLEAN"
clean.oneIn256Miss = false
clean.focusEnergyBug = false
clean.enemyUnlimitedPP = false
clean.hyperBeamSkipRechargeOnKO = false
mod.content.rulesets:register("clean_gen1", clean)
Checkpoint: pick CLEAN in Options → RULESET; enemies spend PP, 1/256
misses are gone, and Hyper Beam always recharges. Type-chart Gen-2 fixes
go through mod.content.type_chart separately.
Gen 2 / Gen 3: rulesets has no Gen 2 or Gen 3 target; those games run
their own cart rules.
R39 - Shiny indicator overlay
Advanced rendering. Before you paste: DV means determinant value. The marker only appears for a creature whose DVs satisfy the shiny check; ordinary encounters are not a dependable immediate test.
Complete walkthrough / required concept.
Insert in main.lua inside the entry function (unless a filename is shown).
local Stats = require("src.pokemon.Stats")
mod.hooks:wrap("battle.overlay", function(next, battle)
next(battle)
local mon = battle.enemy and battle.enemy.mon
if mon and Stats.isShiny(mon.dvs) then
mod.ui.Font.draw("*", 8, 8) -- stand-in sparkle
end
end)
Checkpoint: a gift / stationary / fishing mon with shiny DVs shows the marker; random grass encounters almost never will (Gen 1 DV odds).
Gen 2 / Gen 3: battle.overlay is raised on both with that game's
battle state. DVs are a Game Boy idea: on Gen 3 shininess comes from the
personality value, so Stats.isShiny(mon.dvs) does not apply there.
R49 - Pick Pokémon sprites on the fly
Advanced outline. Before you paste: This is only the rendering half of a skin selector. Supply the named PNGs and your own supported way to store/read a creature's skin choice; no picker or persistence is created here.
Complete walkthrough / required concept.
Content freezes after load, so you cannot pokemon:patch a skin mid-
session. Use pokemon.sprite (and pokemon.icon for the party menu):
Advanced rendering excerpt: supply the skin state and assets first.
-- store the player's pick on the mon somehow (mod.save, a link_fields
-- bag field, nickname tag, …); this example reads mon.skin
mod.hooks:wrap("pokemon.sprite", function(next, path, ctx)
path = next(path, ctx)
local skin = ctx.mon and ctx.mon.skin
if not skin then return path end
ctx.trueColor = true
local side = ctx.side == "back" and "back" or "front"
return mod.assets:path(("skins/%s_%s_%s.png"):format(ctx.species, skin, side))
end)
mod.hooks:wrap("pokemon.icon", function(next, path, ctx)
path = next(path, ctx)
local skin = ctx.mon and ctx.mon.skin
if not skin then return path end
return mod.assets:path(("skins/%s_%s_icon.png"):format(ctx.species, skin))
end)
Checkpoint: after setting mon.skin = "alt", the summary / dex / battle
pics and party icon use the alternate art wherever the hook receives that
Pokemon instance. Views without a ctx.mon fall back to the normal art. Battle pics
resolve when the battler is built; switch out and back (or start a new
fight) to refresh mid-battle.
Gen 2 / Gen 3: Gold raises pokemon.sprite through the shared sprite
module (battle, dex, Hall of Fame, trade) and has no pokemon.icon site. Gen 3 raises both
(src/mods/Gen3Compat.lua), but its pokemon.sprite ctx has species,
side and shiny and no mon, so a per-mon skin reaches only the party
icon there.
R50 - Swap the player's trainer art
Art or advanced hooks. Before you paste: Choose one route: fixed original PNGs, paths for an authored hero, or a saved outfit selection. Each still lives in a valid mod folder with a manifest.
Complete walkthrough / required concept.
The player's own pics are three assets: the battle back pic
(battle/redb.png), the catch tutorial's old man (battle/oldmanb.png),
and the front pic the intro, trainer card and Hall of Fame share
(trainer_card/red.png). Three routes reach them, cheapest first.
One flat replacement, no code. Drop the PNGs in overrides/; they
shadow the generated cache wherever those pics are drawn:
mods/my_mod/overrides/battle/redb.png
mods/my_mod/overrides/trainer_card/red.png
Your own art, by path. A total conversion that ships a different
hero points field.playerPics at it
instead, with no dependency on the vanilla path layout:
Insert in main.lua inside the entry function (unless a filename is shown).
mod.content.field:patch("playerPics", {
back = mod.assets:path("art/hero_back.png"),
front = mod.assets:path("art/hero_front.png"),
})
Per-save or conditional. field freezes after load, so a pic that
follows an outfit / gender / story choice goes through the
player.sprite hook, which stays live:
Insert in main.lua inside the entry function (unless a filename is shown).
mod.hooks:wrap("player.sprite", function(next, path, ctx)
path = next(path, ctx)
if ctx.demo then return path end
local outfit = mod.save:get("outfit") or "default"
return mod.assets:path(("art/%s_%s.png"):format(outfit, ctx.side))
end)
Back pics are drawn at 2x with their feet flush on the text-box top, and
the engine measures your PNG's transparent rows to place it, so art of
any height grounds correctly with no offsets to tune. To draw it at
another size, register your path (not the vanilla one) in
battle_sprite_scales:
Insert in main.lua inside the entry function (unless a filename is shown).
mod.content.battle_sprite_scales:register("hero_back", {
path = mod.assets:path("art/hero_back.png"),
scale = 2.5,
})
Checkpoint: start any battle; the new back pic slides in with the intro, stands with its feet on the text box, and clears on "Go!". The trainer card and Hall of Fame show the new front pic. Setting the outfit key mid-game changes the pic from the next battle on.
Gen 2 / Gen 3: field.playerPics is Gen 1 only (Gold's field reads
only boot, and Gen 3 has no field target). Gold raises player.sprite
for its battle back pic, Oak speech and Hall of Fame; Gen 3 has no call site.
battle_sprite_scales also works on Gold; Gen 3 has no target for it.
R53 - Gen-2-style battle QoL toggles
UI hooks. Before you paste: Catch a species before testing its owned marker; the flag is not shown for every wild encounter.
Complete walkthrough / required concept.
Three opt-in presentation hooks default to vanilla (false) and turn on
per frame when a wrapper says so:
Insert in main.lua inside the entry function (unless a filename is shown).
-- the caught Ball marker beside a wild enemy's name (asked only when
-- the species is already owned)
mod.hooks:wrap("battle.caught_marker_visible", function(next, state)
return true
end)
-- left/right grid movement on the move list and the battle party menu
mod.hooks:wrap("battle.move_grid_navigation", function(next, state)
return true
end)
mod.hooks:wrap("ui.party.grid_navigation", function(next, menu)
return true
end)
Checkpoint: a wild encounter with an owned species shows the Ball marker by the name; LEFT/RIGHT hop columns in the move list and the in-battle party menu.
Gen 2 / Gen 3: all three hooks have call sites on Gold and on every Gen 3 game (Reference: Hooks).
R56 - Color a trainer portrait, and give one party a real name
Basic Lua + art. Before you paste: Use the Gen 1 trainer class shown. Party indexes identify different teams in that class, not positions within one team.
Complete walkthrough / required concept.
Gen 1 trainers are a class and nothing else: one portrait, one palette
picked by the battle intro (the hardware's MEWMON row), and one name for
every party the class fights. Two record fields lift both limits.
A palette of your own. trainers.palette takes a palettes id, the
same per-record override species already carry, and wins over both
paletteSource and the MEWMON fallback:
Insert in main.lua inside the entry function (unless a filename is shown).
mod.content.palettes:register("BELTPAL",
{ {255,255,255}, {200,120,60}, {90,60,30}, {0,0,0} })
mod.content.trainers:patch("OPP_BLACKBELT", {
palette = "BELTPAL",
trueColor = false, -- keep the 4-shade remap; true skips it entirely
})
A full-color portrait wants trueColor = true and no palette at all:
that flag skips the GBC/SGB quantize and draws the PNG as authored.
A name per party. trainers.partyNames maps a party index to a
personal name, the [class] [name] split Gen 2 has and Gen 1 does not.
Sparse is fine; an index with no entry keeps the bare class name:
Insert in main.lua inside the entry function (unless a filename is shown).
mod.content.trainers:patch("OPP_BLACKBELT", {
partyNames = { [1] = "TAKESHI", [3] = "YOSHI" },
})
Party 1 now introduces itself as BLACKBELT TAKESHI and carries that
name through every battle line: the intro, the switch-in, the defeat.
Party 2 is still plain BLACKBELT. The composed name is built per
battle, so the class record itself is never rewritten; className and
personalName stay separately readable on the battle's trainer table for
anything that wants the halves.
Gold needs neither field: its trainer classes already carry one record per
named trainer, and its palettes are patched through the trainers context
of the palettes registry.
Checkpoint: the class fights in your colors, and its first party is introduced by name.
Gen 3: every trainer already has its own name, and rows may carry
className, class, baseMoney and a pic image path. Gen 3 palettes
recolor species and overworld sheets only (MEWMON, MEWMON_SHINY,
OBJ_EVENT_GFX_<NAME>), as a luminance ramp over the cart art; register
them (from an api 2 or api 3 mod; the record is native shape), since there is no vanilla record to override.
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.