-
Notifications
You must be signed in to change notification settings - Fork 15
Cookbook Battle And Mechanics
Hook contracts: Reference: Hooks; worked path: Tutorial 10.
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.
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.
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 }.
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).
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.
-- 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).
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.
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).
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.
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.
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.
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
- Translations
- Total Conversions
- Link Compatibility
- Publishing a Mod
- Docs Style Guide
Playing