Refuse re-entrant, fractional-level and no-party wild battles

overworld() resolves the world from under the stack, so a call from a
battle hook stacked a second battle over the live one -- on a loss its
afterBattle blacked out and warped with the outer battle still up.

newWild marks the species SEEN before it reports an empty party, so a
refused call still wrote the Pokedex; test the party before building it.

tonumber accepts 5.5, which Pokemon.new writes straight into the stat
calc and the exp curve.
This commit is contained in:
Jakub Lisicki
2026-08-08 12:23:05 +02:00
parent 9fc6ba5b01
commit a710c64909
2 changed files with 86 additions and 46 deletions
+25 -3
View File
@@ -7,6 +7,7 @@
local Logger = require("src.core.Logger")
local MapLoader = require("src.world.MapLoader")
local Party = require("src.pokemon.Party")
local Runtime = require("src.mods.Runtime")
local WorldAPI = {}
@@ -174,13 +175,34 @@ function WorldAPI:startWildBattle(species, level)
if not self.game.data.pokemon[species] then
return nil, "unknown species: " .. tostring(species)
end
-- Pokemon.new writes the level through verbatim -- into level, the stat
-- calc and the exp curve -- so a fraction has to be refused here rather
-- than round somewhere downstream. The % test also catches NaN, which
-- passes both range comparisons.
level = tonumber(level)
if not level or level < 1 or level > 100 then
return nil, "level must be 1..100"
if not level or level % 1 ~= 0 or level < 1 or level > 100 then
return nil, "level must be a whole number 1..100"
end
-- overworld() resolves the world from UNDER whatever sits on top of it,
-- so from a battle hook this would otherwise stack a second battle over
-- the live one -- and on a loss its afterBattle blacks out and warps
-- with the outer battle still on the stack.
local BattleTransition = require("src.render.BattleTransition")
for _, state in ipairs(self.game.stack and self.game.stack.states or {}) do
if state.awardExp or getmetatable(state) == BattleTransition then
return nil, "a battle is already running"
end
end
if ow.transitioning then return nil, "the world is mid-warp" end
-- BattleState.newWild marks the species SEEN before it reports an empty
-- party, so the party check comes first: a refused call must not leave a
-- Pokedex entry behind.
local save = self.game.save
if not (save and Party.firstHealthy(save.party or {})) then
return nil, "no healthy party"
end
local battle = require("src.battle.BattleState")
.newWild(self.game, species, level)
if battle.dead then return nil, "no healthy party" end
battle.onFinish = function(result) ow:afterBattle(result, battle) end
ow:pushBattle(battle)
return true