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

114 lines
5.0 KiB
Markdown

# 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`
```lua
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.