Skip to content

Tutorial Modern Individuals

johnjohto edited this page Jul 28, 2026 · 4 revisions

Tutorial: Modern Individuals (mon/2)

Give every monster a nature, ability, held item, IVs, EVs, gender, and a shiny flag -- as an opt-in wire upgrade that migrates existing saves deterministically.

1. Declare the wire in data/ruleset.json: {"mon_wire": 2}

2. Provide natures -- data/natures.json:

{
 "natures": ["nature:bold", "nature:brave", "nature:calm"],
 "effects": {"nature:brave": {"up": "atk", "down": "spd"}}
}

(mon_wire: 2 without this file refuses at boot -- the wire names natures, so it needs the vocabulary. The modern recipe imports all 25 for you.)

3. That's it. Every existing save backfills on load -- one loud line reports the count -- and every field derives deterministically from the monster's own DNA: IVs from its DVs, gender from the species ratio, shininess from the classic DV pattern, its nature from a stable hash. Two loads agree; two link partners agree. Trades refuse cleanly at hello if the other copy speaks a different wire or nature table, instead of desyncing mid-battle.

Species gain an optional gender_ratio byte (0 = all male ... 254 = all female, absent = genderless), moves an optional category (physical/special/status), and the ruleset config a shiny_odds knob. Nothing about battle math changes until a modern formulas provider engages -- declaring the wire alone is provably inert (that's a release gate).

Verify your stat math in-engine

If you bind stat kernels (stat_calc2 for the modern formula, stat_base_scale for a per-individual pre-scale), you can assert what the engine actually computes -- no inference, no reimplementation. Write a fixture:

{"species": "snorlax", "level": 50,
 "ivs": {"hp": 31, "atk": 31, "def": 31, "spd": 31, "spc": 31},
 "evs": {"hp": 252, "atk": 252}, "nature": "brave",
 "ext": {"grade": 4}}

and run:

python tools/run.py --project=path/to/yourgame --statread=fixture.json

Every input is pinned (absent fields take the pipeline's documented defaults: DV 0, IV = DV*2, EV 0, stat exp 0, neutral nature; a sexp bag pins Gen-1 stat experience for faithful-formula vectors), and the output is one line per stat -- base / prescale / final -- where prescale is your stat_base_scale's answer and final is the whole pipeline's. The same fixture always prints the same bytes, so a reference oracle's test vectors drop straight in as acceptance fixtures. A typo'd species, stat key, or nature, an ext field you never declared in mon_fields, or an ext value outside its declared schema fragment refuses loudly instead of computing something you didn't ask about.

Earning EVs: the award seam

Reading EVs is only half an economy — ev_gain is the other half. Declare a yield on any species and bind the kernel, and every way a mon earns stat experience routes through your script instead (battles and vitamins); the Gen-1 sexp pool freezes while it's bound, so there is never double bookkeeping.

In a species record:

"ev_yield": {"hp": 0, "atk": 2, "def": 0, "spd": 1, "spc": 0}

In data/ruleset.json:

"formula_scripts": {"ev_gain": "script:ev_award"}

And the script — this one is the classic 252-per-stat / 510-total rule, but the point is that your kernel owns every cap, because the award path is the only place a total-spread rule can live:

let total = ev_hp + ev_atk + ev_def + ev_spd + ev_spc
let room = 510 - total
if room <= 0 { return true }
out("atk", ev_atk + yield_atk)
out("spd", ev_spd + yield_spd)
return true

Your script sees the current spread (ev_hp …), the defeated species' declared yield (yield_hp …, zeros if undeclared), its base stats (base_hp …), the exp-split participants, and a provenance string — "battle", "battle:expall" under EXP.ALL, or "vitamin:atk"-style for vitamins (a +10 yield to that stat). out() only the stats you change; outing nothing refuses the award (a refused vitamin is "it won't have any effect" and the item is kept). No randomness is allowed in or needed by the kernel, which is what keeps link battles and trades in lockstep for free. Verify the whole economy with --statread fixtures before and after an award, and run python tools/run.py --evgaintest to see the engine's own eleven checks.

Clone this wiki locally