Skip to content

Tutorial 03 New Species

bryanthaboi edited this page Jul 19, 2026 · 1 revision

Tutorial 03 — New species

Goal. Add a brand-new species that battles, is catchable, appears in the Pokedex, shows a party icon, and plays a cry.

Prerequisites. Tutorials 01–02; Concepts: Data Model; Guide: Art Pipeline.

Files created.

mods/tutorial_03_species/
├── manifest.json
├── main.lua
└── assets/
    ├── front.png    -- your art: 4-shade grayscale, white = transparent
    ├── back.png
    ├── icon.png
    └── cry.wav

Steps

  1. Register the full record. A register (unlike patch) must carry every required field; modkit validate checks it against the schema before you ever boot:

    return function(mod)
      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", "GROWL" },
        learnset = {
          { level = 12, move = "QUICK_ATTACK" },
          { level = 20, move = "SLASH" },
        },
        evolutions = {},
        spriteFront = mod.assets:path("assets/front.png"),
        spriteBack = mod.assets:path("assets/back.png"),
        frontSize = 5,
        dexEntry = { kind = "TUTORIAL", heightFt = 2, heightIn = 4,
                     weight = 30.0, text = "Added by a mod.\nIt is very new." },
      })
    end

    Outcome: modkit validate tutorial_03_species confirms the record — a missing field is a mod-attributed load error, not a runtime crash. growthRate must name a growth_rates record (vanilla curves: MEDIUM_FAST, MEDIUM_SLOW, FAST, SLOW, SLIGHTLY_FAST, SLIGHTLY_SLOW).

  2. A dex number past 151 just works. The Pokedex scales from the merged data (constants.dexSize / the highest dex), not a hard-coded 151. Outcome: the dex lists 152 entries and shows your dexEntry text. For a sparse or padded dex, set dexSize/dexDigits explicitly (Concepts: Data Model).

  3. Give it a party icon. The icons registry is keyed by species id:

    mod.content.icons:register("MODMON", {
      image = mod.assets:path("assets/icon.png"),
    })

    Outcome: the party menu shows your icon instead of the fallback.

  4. Give it a cry. A file-based cry def:

    mod.content.cries:register("MODMON", {
      file = mod.assets:path("assets/cry.wav"),
    })

    Outcome: the cry plays on send-out and from the dex screen. (You can also derive from a vanilla cry — { base = "PIKACHU", pitch = 200 } — or author a chip cry; see Audio Authoring.)

  5. Meet it in the wild. Patch an encounter table so you can catch one:

    mod.content.encounters:patch("ROUTE_1", {
      grass = { slots = { __prepend = { { species = "MODMON", level = 5 } } } },
    })

    Outcome: Route 1 grass rolls MODMON in its top (most common) slot.

Checkpoint

Catch a MODMON, save, and reload: the mon survives the save validation pass, because a registered species is a known id. Then disable the mod and reload the same save: the mon moves to the LOST box with a report — and comes home when you re-enable the mod (Concepts: Save Model).

Common pitfalls

  • Forgetting frontSize. The battle layout reads it (1–7 tiles); validation requires it for exactly that reason.
  • Occupying a taken dex number. Two species on one dex slot fight over the list row; pick the next free number or renumber deliberately.
  • Skipping the cry. A species with no cry degrades to silence rather than crashing — but add one; the dex and send-out feel broken without it.
  • Art format. Sprites are 4-shade grayscale with white as transparency for battle pics; see Art Pipeline before drawing.

Next: Tutorial 04 — New Item and Ball.

Clone this wiki locally