-
Notifications
You must be signed in to change notification settings - Fork 2
Concepts Data Model
The ground-truth record shapes mods write, the one naming rule that prevents the classic silent failure, and the two deep registries that hold the rules the engine used to hard-code.
Base-data record fields are camelCase, exactly as the importer emits
them (src/import/RomExtractor.lua): baseStats, catchRate,
spriteFront, frontSize, growthRate, dexEntry. The only snake_case
a modder meets is manifest keys (optional_dependencies, game_version —
JSON convention) and registry names (map_scripts, type_chart). Never a
record field.
The canonical mistake is base_stats:
-- WRONG: the engine never reads base_stats; the stat change silently no-ops
mod.content.pokemon:patch("PIKACHU", { base_stats = { speed = 90 } })
-- RIGHT
mod.content.pokemon:patch("PIKACHU", { baseStats = { speed = 90 } })Under api = 2 the wrong form is a load error with the suggestion
did you mean "baseStats"? — the schema normalizes case and underscores
to catch exactly this class of typo (src/mods/Schemas.lua).
Every required field, from the pokemon schema:
mod.content.pokemon:register("MODMON", {
id = "MODMON", name = "MODMON", dex = 152,
types = { "NORMAL" },
baseStats = { hp = 60, attack = 70, defense = 55, speed = 90, special = 65 },
catchRate = 120, baseExp = 100,
growthRate = "MEDIUM_FAST",
level1Moves = { "TACKLE" },
learnset = { { level = 12, move = "QUICK_ATTACK" } },
evolutions = {},
spriteFront = mod.assets:path("front.png"),
spriteBack = mod.assets:path("back.png"),
frontSize = 5,
})Optional: index, tmhm, dexEntry
({ kind, heightFt, heightIn, weight, text }), icon, cry, palette,
trueColor. Id-typed fields (types, growthRate, learnset[].move,
evolutions[].species, ...) are resolved after the merge; a dangling
reference is a load error naming the field path.
mod.content.moves:patch("BLIZZARD", { accuracy = 70 })
mod.content.items:patch("POTION", { price = 100 })
mod.content.trainers:patch("OPP_BROCK", { baseMoney = 99 })Field-by-field shapes for all 37 registries:
Reference: Registries. Notable move fields:
category ("physical" | "special" | "status") overrides the Gen 1
type-based split; effect names a move_effects record; priority,
highCrit, multiHit, fixedDamage are all data, not code.
Two deep registries hold the values that used to be Kanto/Red literals
scattered through the engine. The id is a top-level key of the target
table; register and patch merge into that key, override replaces it
outright, lists append (__prepend reaches the front), and keys the
catalog does not describe are accepted and merged unchanged, so a mod may
stash its own data beside the engine's.
mod.content.constants:patch("levelCap", 80)
mod.content.constants:patch("bagSize", 40)
mod.content.constants:patch("dexSize", 251)Seeded keys: bagSize, partyMax, boxCount, boxSize, moveMax,
levelCap, coinCap, moneyCap, dexSize, dexDigits, hmMoves,
encounterBuckets, badges. Each holds the number the engine hard-coded
(bag 20, party 6, 4 moves, ...), so a mod-free boot behaves exactly as
before — the faithful Game Boy limits are now the defaults, overridable
in one sanctioned place.
dexSize is the upper bound the Pokedex counts to and dexDigits the
zero-padded width dex numbers print with; the engine derives both at data
load from the highest dex across the merged species (151 and 3 for
vanilla), so a mod that adds species usually needs neither. Set them
explicitly for a sparse or oversized dex.
badges is the ordered badge set — list position is the badge number the
trainer card draws:
mod.content.items:register("NINTH_BADGE",
{ id = "NINTH_BADGE", name = "NINTH BADGE", price = 0 })
mod.content.constants:patch("badges", { { id = "NINTH_BADGE" } }) -- appendsEach entry is { id, name?, icon?, item? }; id must resolve to an
items record. override("badges", { ... }) replaces the whole set — the
total-conversion verb.
The overworld's data grab bag: ledges, hidden items, badge gates, fly network, town map. Per-map dictionaries merge per map, so adding content never touches Kanto's:
mod.content.field:patch("hiddenItems", {
SABLE_COVE = { { x = 3, y = 9, item = "NUGGET" } },
})
mod.content.field:patch("flyOrder", { "SABLE_COVE" }) -- appends
mod.content.field:patch("townMap", {
locations = { SABLE_COVE = { x = 4, y = 17, name = "SABLE COVE" } },
})The one key a total conversion always writes. Read when a new game starts:
mod.content.field:patch("boot", {
startMap = "SABLE_COVE", startX = 3, startY = 4, startFacing = "down",
playerName = "ALEX", rivalName = "JESS",
startMoney = 5000,
namePresets = { player = { "ALEX", "SAM" }, rival = { "JESS", "KIT" } },
})| key | meaning |
|---|---|
startMap / startX / startY / startFacing
|
where the new save spawns |
playerName / rivalName
|
default names before the naming screens |
startMoney |
starting money (vanilla 3000) |
lastHeal |
blackout return point; defaults to the spawn |
namePresets |
{ player = {...}, rival = {...} } naming-screen menus |
screens |
{ splash, title, newGame } boot-screen ids, resolved through the screens registry (src/core/Game.lua bootScreens) |
starterScript |
a map_scripts entry to run in place of the Oak speech |
title |
title-screen presentation |
Every key falls back to its vanilla value, so a partial patch — a new
spawn and nothing else — keeps the Red names, money, and boot screens.
screens resolves through the screens registry at boot
(src/core/Game.lua); starterScript and title are declared and
merged but not yet read by the boot chain — drive an intro through
screens.newGame instead
(Tutorial 12).
The third deep registry: per-map TEXT-constant bindings. Every key is a map label carrying the same shape, so one sign binding never restates the map:
mod.content.text_pointers:patch("PalletTown", {
TEXT_PALLETTOWN_SIGN = { text = "_MySign" },
})Entry fields: text, label, asm, mart (a list of item ids — how
shop stock is data), nurse, pc, cableClub.
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
Playing