Files
gen1recomp/docs/rfcs/0018-catch-party-full.md
T
DESKTOP-8SRFDDM\cam95 7d6566fa51 engine: catch.party_full custody for a full-party catch (RFC 0018)
A capture the party cannot hold silently falls through to the box; the
new partyFullDestination seam lets a mode claim custody at that moment
instead, and pokemon.caught reports destination "mod" so the mode can
find the mon again.  Guarded call site, file-local vanilla, docs and a
public-API modkit case.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N1DVpYXGabigHqMwviDoKV
2026-08-24 21:06:07 -05:00

5.0 KiB

RFC 0018: catch.party_full — custody of a catch the party cannot hold

Status

Proposed.

Motivation

When a capture lands and the party already holds six Pokémon, the cart does not ask the player anything: AddPartyMon fails and the mon goes to SendNewMonToBox without a stop. The engine mirrors that — BattleState:storeCaughtMon falls through to Boxes.deposit — and a game mode has no way to stand in front of it.

That silence is wrong for any mode where the box is not a thing. A battle royale locks the Pokémon Center PC for the whole match (the box is a second health bar: deposit the healthy ones, fight with one, withdraw fresh ones), so a seventh catch is deposited into storage the player cannot reach — the mon is gone and everyone saw the fanfare. A Nuzlocke that counts a boxed mon as lost, a randomizer that wants to hand out a replacement instead, a challenge run that makes the player release someone for the catch — all want the decision, and today there is no seam to catch it at.

The immediate consumer is a battle-royale mode, where the fix is the game's own rule: at 6/6 you choose who makes room, and whoever leaves hits the ground as a ball. Neither the decision nor the spill is specific to it.

The decision it extends

This extends the additive, guarded seam convention Route B in CONTRIBUTING-mods.md documents, and is gated by the parity guarantee tests/engine/gate_meta_coverage.lua enforces. It sits next to catch.nickname (RFC 0015) on the same capture path: that hook answers what the mon is called once its home is decided; this one decides the home when the party cannot hold it.

There is no in-repo D-number registry to amend.

Exact API delta

New hook: catch.party_full

mod.hooks:wrap("catch.party_full", function(next, ctx)
  -- ctx = { battle = <BattleState>, mon = <Pokemon>, name = <display name>,
  --         game = <Game> }
  if myMode.active then
    takeCustody(ctx.mon)      -- the mon is the mod's problem now
    return true               -- nothing is deposited
  end
  return next(ctx)            -- false: deposit, as today
end)

Call site: BattleState:partyFullDestination(mon), called from the capture path at the moment AddPartyMon has failed and SendNewMonToBox would run. The vanilla link returns false. A truthy return skips the box entirely — the mon is neither in the party nor in any box, and pokemon.caught reports destination = "mod" so the mode can find its own custody again. Anything falsy deposits as always, "But every BOX is full!" included.

The method returns "box" or "mod", and is a method on purpose: the compatibility seam for engines that predate this RFC needs a name to ask for (see below), and battleStyle/offerNickname set the precedent.

One event value, already emitted

pokemon.caught's destination gains a third value, "mod", next to "party" and "box". Nothing that reads the event today matches on it.

No other surface changes

The call site is guarded by Runtime.wantsHook, and the vanilla link is a file-local, so a build with nothing wrapped runs the branch exactly as before and allocates nothing it did not allocate before.

Migration

Nothing changes for existing mods. A mode that was fighting the box after the fact — withdrawing the deposit it could not prevent, or eating the loss — should wrap catch.party_full and take the mon at the moment of the catch.

Verification

  • tests/modkit/cases/catch_party_full.lua — through the public mod API: with no mod a 6/6 catch lands in the box with the transfer text; a wrapped mod that claims custody leaves the mon out of both party and boxes; a fall-through deposits; the hook's ctx carries the battle, the mon, the display name and the game.
  • tests/engine/gate_hooks.lua — the name is in the live catalog and passes the no-mod parity gate (vanilla called exactly once, result unchanged, nothing allocated).
  • tests/engine/gate_meta_coverage.lua — the name is covered by the unit corpus.

Backward compatibility

Additive. No existing hook, event, registry or manifest field changes shape. storeCaughtMon's box branch keeps its text, its nickname offer and its pokemon.caught emit; the only reader-visible difference with no subscriber is one extra method on BattleState.

Compatibility seam for older engines

The call site is mid-function, so a mod on a stock engine cannot see it — the same problem world.talk has. The battle-royale mod's shim covers the gap by wrapping Boxes.deposit (the one call the old branch makes that a patch can reach): during a match it raises catch.party_full first, and a claim refuses the deposit, so nothing reaches a box even there. The mod then takes custody from the pokemon.caught emit, which carries the mon. What the shim cannot repair is the text: the old branch answers a refused deposit with "But every BOX is full!" before the mode's own prompt opens — the wrong reason for the right decision, and the argument for the seam.