Skip to content

Tutorial Per Save Rulesets

johnjohto edited this page Jul 28, 2026 · 1 revision

Tutorial: Per-Save Rulesets

One project, two stat models. data/ruleset.json's rulesets map declares named variants over your root declaration; a save binds one of them — in-story, exactly once — and keeps it for life. "Faithful mode" and "modern mode" stop being two projects.

Declare the variants

Your root ruleset.json stays the faithful default (every save starts there). Each named entry may declare any of formula_scripts, config, and stats:

{
 "base": "gen1",
 "mon_fields": {"era": {"type": "string"}},
 "rulesets": {
  "modern_mode": {
   "formula_scripts": {
    "stat_calc2": "script:two_mode_stats",
    "ev_gain": "script:ev_caps"
   }
  }
 }
}

A key an entry declares replaces the root's wholesale; a key it omits inherits the root. What's bound is always exactly what you can read on the page — no merging, no conditions. (when-conditioned bindings are deliberately rejected: a binding that flickers with a flag can't be replayed or refused.)

stats is the ordered stat vocabulary — today the engine runs exactly ["hp","atk","def","spd","spc"], so declaring it is optional; the six-stat era will widen it here, per variant.

The identity-shaping keys — base, mon_fields, mon_wire, battle_hooks — stay root-only: every save of your project speaks the same wire.

Bind it in the story

The mode choice is a narrative moment. A new event command binds the save:

{"cmd": "ask", "text": "Which world calls to you?",
 "options": ["The old ways", "The new ways"], "result": "world_choice"},
{"cmd": "if", "cond": "world_choice == 1",
 "then": [{"cmd": "set_ruleset", "name": "modern_mode"}]},
{"cmd": "give_mon", "species": "species:sproutle", "level": 5}

Choose "the old ways" and you simply never bind — the save stays on the faithful root.

Three rules, all loud:

  • Once. A second set_ruleset refuses and aborts the event.
  • Before the first owned individual. Any mon already in the party, a box (including box banks and shared storage), the Day Care, or an in-flight trade makes the bind refuse — individuals must never predate the rules that made them.
  • Declared names only. An unknown name refuses at --validate and at load, never mid-play. Every declared variant is also checked at boot (kernels, scripts, stats) even if no save ever binds it.

Because the engine grants creator projects a stopgap starter at new game, set "starter_level": 0 in presentation/version.json and give the first mon yourself in the start event — after the choice.

What it means for a save

The binding rides the save file as one additive key ("ruleset": "modern_mode"); unbound saves are byte-identical to before. Loading re-binds first, so every kernel and knob already stands on the save's rules before anything else happens.

Linking: the binding joins the handshake identity. Two saves on different rulesets — or the same name with edited kernel scripts — refuse to link with a message naming the mismatch, instead of desyncing a lockstep battle. Same binding, same math: peers agree by construction.

Pairs with Modern Individuals (the ext marker + widened stat_calc2 two-mode kernel) and The Modern Ruleset. Gate: --rulesetbindtest.

Clone this wiki locally