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
Cookbook — battle and mechanics
Hook contracts: Reference: Hooks; worked path: Tutorial 10.
R19 — Add a custom AI brain
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.
R27 — React to a battle event
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.
R28 — Wrap the damage hook
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 }.
R29 — Add a status condition
mod.content.statuses:register("CURSE", {
label = "CURSE", hudLabel = "CRS",
residual = function(battler)
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,
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).
R30 — Add a type + chart row
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.
R36 - Scale a battle sprite
-- 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).
R38 — Gen-2-like ruleset toggles
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 |
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.
R39 — Shiny indicator overlay
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).
R49 — Pick Pokémon sprites on the fly
Content freezes after load, so you cannot pokemon:patch a skin mid-
session. Use pokemon.sprite (and pokemon.icon for the party menu):
-- 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 all show the alt art without restarting. Battle pics
resolve when the battler is built — switch out and back (or start a new
fight) to refresh mid-battle.
R50 — Swap the player's trainer art
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 — no dependency on the vanilla path layout:
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:
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:
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.
R53 - Gen-2-style battle QoL toggles
Three opt-in presentation hooks default to vanilla (false) and turn on
per frame when a wrapper says so:
-- 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. All three are Gen 1 call sites only for now (Reference: Hooks).
Start here
Concepts
Tutorials
- The ladder
- 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
Reference
Guides
- Art Pipeline
- Audio Authoring
- Preparing Your Mod for Gen 2
- Translations
- Total Conversions
- Link Compatibility
- Publishing a Mod
- Custom Carts
- Docs Style Guide
Playing
Engine 1.0.0 · mod API 2 · repository · Discord
This project ships no ROM data. All game content is decoded on the player's machine from their own verified Pokemon Red ROM; mods ship recipes and original assets, never extracted content.