Files
gen1recomp/mods/examples/example_weather
2026-07-28 06:16:21 -04:00
..
2026-07-19 16:18:18 -04:00
2026-07-19 16:18:18 -04:00
2026-07-19 16:18:18 -04:00
2026-07-25 12:36:53 -04:00
2026-07-19 16:18:18 -04:00

Weather Battles Example

Adds rain: for the first five turns of every battle, WATER moves deal 1.5x damage and FIRE moves deal 0.5x. Only under the WEATHER ruleset — pick gen1_faithful and the mod is installed and inert.

Persona: the Mechanic Designer. A new battle mechanic, no engine fork. The status and ruleset registries plus one hook carry the whole thing.

Try it

python3 tools/modkit.py validate mods/examples/example_weather --base imported
luajit mods/examples/example_weather/tests/example_weather_test.lua

Enable it (example_weather = true under mods in options.lua, or the F10 manager), then OPTIONS → RULESET → WEATHER.

What it demonstrates

Seam Where
content.statuses:register main.lua — declaring a field effect
content.rulesets:register main.lua — a ruleset the OPTIONS menu lists automatically
content.rulesets:get main.lua — deriving from vanilla without requiring a private module
hooks:wrap("battle.damage") main.lua — the one behavior change
events:on("battle.started" / "battle.turn_started" / "battle.ended") main.lua — the rain counter
mod.save:get/set main.lua — per-mod state, not a global

Parity, at the mod level

The engine's promise is that a mod-free game is unchanged. This example makes the same promise one level up: a player who installs it but does not select the ruleset gets vanilla battles.

if not (ctx.ruleset and ctx.ruleset.exampleWeather and raining()) then
  return next(ctx)
end

next(ctx) with the arguments it was handed is the vanilla call. No allocation, no rounding, no reordering — the same number the engine would have produced. Everything above that line is a gate, and every gate that fails defers.

Preserving multiple returns

Damage.compute returns two values: the damage number and an info table carrying the crit flag and the type multiplier. A wrapper that returns only the first silently throws the second away, and the battle log stops saying "A critical hit!".

local damage, info = next(ctx)
if type(damage) ~= "number" then return damage, info end
return math.max(1, math.floor(damage * scale)), info

Hook chains preserve every return value, so passing info back through is all it takes.

Deriving a ruleset from vanilla

A ruleset record is the whole rule table — oneIn256Miss, critUsesBaseSpeed, randMin, randMax and the rest. Registering one that only sets name would silently drop every Gen 1 quirk. So this mod reads the vanilla record out of the merged registry and copies it:

local base = mod.content.rulesets:get("gen1_faithful")
local weather = {}
for key, value in pairs(base) do weather[key] = value end
weather.name = "WEATHER"
weather.exampleWeather = true

:get on a registry is the public path to engine content. It needs no permission, and it composes: if another mod patched gen1_faithful first, this ruleset inherits that patch too.

exampleWeather is not in the ruleset schema. Unknown fields on a record registry are preserved rather than rejected — that is what makes rulesets extensible, and it is how the damage hook recognizes its own ruleset without a second lookup.

Missing dependency, handled

If another mod removed gen1_faithful, :get returns nil. This example logs a remediation line and returns — the rest of the game keeps working and the manager shows one attributed message. No assert, no crash.

Credits

  • pret/pokered — the damage formula the hook scales.