diff --git a/docs/modding.md b/docs/modding.md index 264ad9d5..abc4a629 100644 --- a/docs/modding.md +++ b/docs/modding.md @@ -278,10 +278,16 @@ The shared Red, Blue, Yellow, and Gold intents are: - `{ kind = "move", slot = 1..4 }` - `{ kind = "back" }` while the move menu is active +Red, Blue, and Yellow also expose their generation-specific choices: + +- `{ kind = "safari", action = "ball" }` (`bait`, `rock`, and `run` are the + other accepted actions) +- `{ kind = "mimic", index = 1 }` using an entry's snapshot `index` + Menu choices and moves use the same engine methods as the native controls; `party` and `item` open the native screens rather than exposing or duplicating -their mutable logic. Tutorial, link, Safari, forced, stale, and covered battle -states refuse these core intents. Use `mod.input` for ordinary text advance. +their mutable logic. Tutorial, link, forced, stale, and covered battle states +refuse core intents. Use `mod.input` for ordinary text advance. ## Rendering pipelines diff --git a/src/battle/BattleAPI.lua b/src/battle/BattleAPI.lua index 6d94339f..2ea4d97b 100644 --- a/src/battle/BattleAPI.lua +++ b/src/battle/BattleAPI.lua @@ -218,13 +218,20 @@ function BattleAPI:submit(intent) return nil, "stale battle context" end local kind = battle:battleKind() - if kind == "oldman" or kind == "link" or kind == "safari" then + if kind == "oldman" or kind == "link" then return nil, "battle kind is not controllable" end if top ~= battle then return nil, "battle menu is covered" end local ok, err - if intent.kind == "menu" then + if intent.kind == "safari" then + if kind ~= "safari" then return nil, "safari menu is not active" end + ok, err = battle:chooseSafari(intent.action) + elseif kind == "safari" then + return nil, "battle kind is not controllable" + elseif intent.kind == "mimic" then + ok, err = battle:chooseMimic(intent.index) + elseif intent.kind == "menu" then if battle.phase ~= "menu" then return nil, "battle menu is not active" end if not MENU_CHOICES[intent.choice] then return nil, "unknown battle menu choice" diff --git a/src/battle/BattleState.lua b/src/battle/BattleState.lua index 8e19da36..18185937 100644 --- a/src/battle/BattleState.lua +++ b/src/battle/BattleState.lua @@ -2018,6 +2018,38 @@ function BattleState:cancelMove() return true end +local SAFARI_ACTION_INDEX = { ball = 1, bait = 2, rock = 3, run = 4 } + +function BattleState:chooseSafari(action) + if self.phase ~= "menu" or not self.safari then + return nil, "safari menu is not active" + end + if self.safari.balls <= 0 then return nil, "no safari balls remain" end + local index = SAFARI_ACTION_INDEX[action] + if not index then return nil, "invalid safari action" end + self.menuIndex = index + self:safariAction(action) + return true +end + +function BattleState:chooseMimic(index) + if self.phase ~= "mimicSelect" then + return nil, "mimic menu is not active" + end + if type(index) ~= "number" or index % 1 ~= 0 then + return nil, "invalid mimic slot" + end + local pick = self.mimicMoves and self.mimicMoves[index] + local ctx = self.mimicCtx + if not pick or not ctx then return nil, "invalid mimic slot" end + self.mimicIndex = index + self.mimicMoves, self.mimicCtx = nil, nil + self.phase = "messages" + self.nextInsert = 0 -- the copy's anim + text go to the queue head + self:applyMimic(ctx.user, ctx.target, ctx.moveInst, pick.slot) + return true +end + function BattleState:swapMoves(i, j) if i == j then return end local moves = self.player.curMoves @@ -2139,7 +2171,7 @@ function BattleState:update(dt) self.menuIndex = row * 2 + col + 1 if input:wasPressed("a") then require("src.core.Sound").play(self.data, "Press_AB") - self:safariAction(({ "ball", "bait", "rock", "run" })[self.menuIndex]) + self:chooseSafari(({ "ball", "bait", "rock", "run" })[self.menuIndex]) end return end @@ -2247,12 +2279,7 @@ function BattleState:update(dt) self.mimicIndex = self.mimicIndex < #moves and self.mimicIndex + 1 or 1 elseif input:wasPressed("a") then require("src.core.Sound").play(self.data, "Press_AB") - local pick = moves[self.mimicIndex] - local ctx = self.mimicCtx - self.mimicMoves, self.mimicCtx = nil, nil - self.phase = "messages" - self.nextInsert = 0 -- the copy's anim + text go to the queue head - self:applyMimic(ctx.user, ctx.target, ctx.moveInst, pick.slot) + self:chooseMimic(self.mimicIndex) end return end diff --git a/tests/mod_battle_snapshot_test.lua b/tests/mod_battle_snapshot_test.lua index 863fb00a..851d3646 100644 --- a/tests/mod_battle_snapshot_test.lua +++ b/tests/mod_battle_snapshot_test.lua @@ -48,7 +48,7 @@ local battle = { enemy = { mon = { species = "TESTMON", level = 4, hp = 12, stats = { hp = 12 }, moves = {} }, curTypes = { "NORMAL" }, stages = {} }, } -function battle:battleKind() return "wild" end +function battle:battleKind() return self.kind or "wild" end function battle:effectRecord() return { accuracyChecked = true } end function battle:visibleText() return { "Wild TESTMON appeared!" } end function battle:menuLockedAction() return nil end @@ -63,6 +63,14 @@ function battle:chooseMove(slot) return true end function battle:cancelMove() self.phase = "menu" return true end +function battle:chooseSafari(action) + self.chosenSafari, self.phase = action, "messages" + return true +end +function battle:chooseMimic(slot) + self.chosenMimic, self.phase = slot, "messages" + return true +end function battle:catchChance(ball) return require("src.battle.Catching").chance(ball, self.enemy.mon, game.data.pokemon[self.enemy.mon.species]) @@ -123,6 +131,18 @@ check(api:submit({ id = 3, revision = back.revision, kind = "back" }), "Gen 1 accepts move-menu back") eq(battle.phase, "menu", "Gen 1 back restores the command menu") +battle.kind, battle.safari = "safari", { balls = 30 } +local safari = api:snapshot() +check(api:submit({ id = 4, revision = safari.revision, + kind = "safari", action = "rock" }), "Gen 1 accepts a Safari action") +eq(battle.chosenSafari, "rock", "Gen 1 uses the semantic Safari path") +battle.kind, battle.safari = "wild", nil +battle.phase, battle.mimicMoves = "mimicSelect", { { slot = 1 } } +local mimic = api:snapshot() +check(api:submit({ id = 5, revision = mimic.revision, + kind = "mimic", index = 1 }), "Gen 1 accepts a Mimic choice") +eq(battle.chosenMimic, 1, "Gen 1 uses the semantic Mimic path") + local player2 = { species = "CHIKORITA", level = 5, hp = 20, maxHp = 21, moves = { { id = "TACKLE", pp = 35, maxPp = 35 } } } local enemy2 = { species = "RATTATA", level = 3, hp = 12, maxHp = 12, @@ -228,6 +248,29 @@ do eq(real.phase, "menu", "native Gen 1 move-menu back still works") end +do + local state = setmetatable({ phase = "menu", safari = { balls = 30 }, + menuIndex = 1 }, { __index = Gen1BattleState }) + function state:safariAction(action) self.safariChoice = action end + local ok, err = state:chooseSafari("missing") + check(not ok and err == "invalid safari action", + "native Safari rejects an unknown action") + check(state:chooseSafari("rock"), "native Safari choice is accepted") + eq(state.menuIndex, 3, "native Safari cursor follows the semantic choice") + eq(state.safariChoice, "rock", "native Safari action uses the shared path") + + state.phase = "mimicSelect" + state.mimicMoves = { { slot = 4 } } + state.mimicCtx = { user = {}, target = {}, moveInst = {} } + function state:applyMimic(_, _, _, slot) self.mimicSlot = slot end + ok, err = state:chooseMimic(2) + check(not ok and err == "invalid mimic slot", + "native Mimic rejects an unknown choice") + check(state:chooseMimic(1), "native Mimic choice is accepted") + eq(state.phase, "messages", "native Mimic choice resumes battle messages") + eq(state.mimicSlot, 4, "native Mimic choice copies the selected move slot") +end + local Loader = require("src.mods.Loader") local fs = { read = function() end, getInfo = function() end, getDirectoryItems = function() return {} end }