mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-18 11:44:42 +02:00
feat(gen2): expose stock ball catch previews
This commit is contained in:
+4
-4
@@ -268,10 +268,10 @@ an optional stock `catchChance` percentage.
|
||||
`prompt` describes the currently visible choice (`menu`, `moves`, `party`,
|
||||
`advance`, `safari`, or `mimic`) and is `locked` when another screen or battle
|
||||
phase owns input. Generation-specific features remain optional: Gen 1 includes
|
||||
battle medicine, balls, catch previews, Safari balls, and Mimic choices;
|
||||
Gold currently returns an empty `items` list rather than guessing at its
|
||||
pocketed PACK flow. Callers should ignore unknown fields and tolerate absent
|
||||
optional ones.
|
||||
battle medicine, balls, catch previews, Safari balls, and Mimic choices. Gold
|
||||
exposes balls and their exact stock catch previews; targeted medicine remains
|
||||
screen-owned and is omitted rather than guessing at its pocketed PACK flow.
|
||||
Callers should ignore unknown fields and tolerate absent optional ones.
|
||||
|
||||
## Battle menu intents
|
||||
|
||||
|
||||
@@ -39,6 +39,21 @@ local function messageCopy(screen)
|
||||
return #lines > 0 and lines or nil
|
||||
end
|
||||
|
||||
local function itemCopies(game, screen, catchable)
|
||||
local out = {}
|
||||
for id, count in pairs((game.save and game.save.inventory) or {}) do
|
||||
local def = game.data.items and game.data.items[id]
|
||||
if count > 0 and def and def.pocket == "BALL" then
|
||||
out[#out + 1] = { id = id, name = def.name or id, count = count,
|
||||
ball = true, needsTarget = false,
|
||||
catchChance = catchable and screen.catchChance
|
||||
and screen:catchChance(id) or nil }
|
||||
end
|
||||
end
|
||||
table.sort(out, function(a, b) return a.name < b.name end)
|
||||
return out
|
||||
end
|
||||
|
||||
local function signature(game, screen, top)
|
||||
if not screen then return "none" end
|
||||
local battle = screen.battle or {}
|
||||
@@ -56,6 +71,9 @@ local function signature(game, screen, top)
|
||||
parts[#parts + 1] = tostring(mon.hp)
|
||||
parts[#parts + 1] = tostring(mon.status)
|
||||
end
|
||||
for _, item in ipairs(itemCopies(game, screen, false)) do
|
||||
parts[#parts + 1] = item.id .. "=" .. tostring(item.count)
|
||||
end
|
||||
return table.concat(parts, "|")
|
||||
end
|
||||
|
||||
@@ -111,9 +129,9 @@ function BattleAPI:snapshot()
|
||||
player = monCopy(game.data, battle.player, true),
|
||||
enemy = monCopy(game.data, battle.enemy, true),
|
||||
party = party, moves = moveCopies(game, battle),
|
||||
-- Gold's PACK is pocketed and target selection is screen-owned. Omit it
|
||||
-- until the engine can expose the same semantic item records as Gen 1.
|
||||
items = {} }
|
||||
-- Targeted medicine remains screen-owned, but balls are complete semantic
|
||||
-- records and can safely expose the same read-only preview as Gen 1.
|
||||
items = itemCopies(game, screen, battle.wild and not screen.tutorial) }
|
||||
end
|
||||
|
||||
local MENU_CHOICES = { fight = true, party = true, item = true, run = true }
|
||||
|
||||
@@ -305,6 +305,15 @@ function Catching.rate(opts)
|
||||
return math.min(255, rate), false
|
||||
end
|
||||
|
||||
-- Exact stock catch probability for read-only previews. A catch.rate hook
|
||||
-- may replace the roll entirely, so nil is safer than presenting a guess.
|
||||
function Catching.chance(opts)
|
||||
if Runtime.wantsHook("catch.rate") then return nil end
|
||||
local rate, guaranteed = Catching.rate(opts)
|
||||
if guaranteed or rate >= 255 then return 100 end
|
||||
return rate * 100 / 256
|
||||
end
|
||||
|
||||
-- The status half of the rate, off the merged `statuses` record the same way
|
||||
-- src/battle/Catching.lua reads record.catchBonus on Gen 1. Gold's records
|
||||
-- live on src/battle/gen2/Battle.lua (Battle.STATUSES) and carry BOTH numbers:
|
||||
|
||||
+33
-30
@@ -2945,6 +2945,37 @@ function BattleState:openDexEntry(species)
|
||||
})
|
||||
end
|
||||
|
||||
function BattleState:catchOptions(itemId)
|
||||
local data = self.game and self.game.data or {}
|
||||
local battle = self.battle
|
||||
local enemy = battle and battle.enemy
|
||||
if not enemy then return nil end
|
||||
local enemyDef = data.pokemon and data.pokemon[enemy.species]
|
||||
local dexEntry = data.gen2Pokedex and data.gen2Pokedex[enemy.species]
|
||||
local evolveItem
|
||||
for _, entry in ipairs((enemyDef and enemyDef.evolutions) or {}) do
|
||||
if entry.method == "EVOLVE_ITEM" then evolveItem = entry.item end
|
||||
end
|
||||
local player = battle.player
|
||||
return {
|
||||
battle = battle, mon = enemy, def = enemyDef,
|
||||
maxHp = enemy.maxHp or (enemy.stats and enemy.stats.hp), hp = enemy.hp,
|
||||
catchRate = enemyDef and enemyDef.catchRate or 45, ball = itemId,
|
||||
status = enemy.status, random = battle.random,
|
||||
weight = dexEntry and dexEntry.weight, level = enemy.level,
|
||||
playerLevel = player and player.level,
|
||||
fishing = battle.battleType == "fish", species = enemy.species,
|
||||
gender = enemy.gender, playerSpecies = player and player.species,
|
||||
playerGender = player and player.gender, evolveItem = evolveItem,
|
||||
}
|
||||
end
|
||||
|
||||
function BattleState:catchChance(itemId)
|
||||
if self.tutorial then return 100 end
|
||||
local opts = self:catchOptions(itemId)
|
||||
return opts and Catching.chance(opts) or nil
|
||||
end
|
||||
|
||||
-- Items in battle: balls try a catch, the stat items apply their stage, and
|
||||
-- everything with a ported party effect runs the same item_effects.asm routine
|
||||
-- the field pack runs. Anything else reports that it cannot be used, which is
|
||||
@@ -2974,7 +3005,6 @@ function BattleState:useItem(itemId)
|
||||
return
|
||||
end
|
||||
local enemy = self.battle.enemy
|
||||
local enemyDef = data.pokemon and data.pokemon[enemy.species]
|
||||
local caught, rate
|
||||
if self.tutorial then
|
||||
-- `ld a, [wBattleType] / cp BATTLETYPE_TUTORIAL /
|
||||
@@ -2988,35 +3018,8 @@ function BattleState:useItem(itemId)
|
||||
caught, rate = true, 255
|
||||
else
|
||||
-- The specialty-ball conditions (BallMultiplierFunctionTable): each one
|
||||
-- is something this screen already knows. Heavy Ball reads the dex
|
||||
-- weight, Moon Ball the species' stone row, Love Ball both genders,
|
||||
-- Level Ball the two levels, Lure Ball wBattleType.
|
||||
local dexEntry = data.gen2Pokedex and data.gen2Pokedex[enemy.species]
|
||||
local evolveItem
|
||||
for _, entry in ipairs((enemyDef and enemyDef.evolutions) or {}) do
|
||||
if entry.method == "EVOLVE_ITEM" then evolveItem = entry.item end
|
||||
end
|
||||
local player = self.battle.player
|
||||
caught, rate = Catching.attempt({
|
||||
battle = self.battle,
|
||||
mon = enemy,
|
||||
def = enemyDef,
|
||||
maxHp = enemy.maxHp or (enemy.stats and enemy.stats.hp),
|
||||
hp = enemy.hp,
|
||||
catchRate = enemyDef and enemyDef.catchRate or 45,
|
||||
ball = itemId,
|
||||
status = enemy.status,
|
||||
random = self.battle.random,
|
||||
weight = dexEntry and dexEntry.weight,
|
||||
level = enemy.level,
|
||||
playerLevel = player and player.level,
|
||||
fishing = self.battle.battleType == "fish",
|
||||
species = enemy.species,
|
||||
gender = enemy.gender,
|
||||
playerSpecies = player and player.species,
|
||||
playerGender = player and player.gender,
|
||||
evolveItem = evolveItem,
|
||||
})
|
||||
-- is also used by the read-only preview, so both paths stay exact.
|
||||
caught, rate = Catching.attempt(self:catchOptions(itemId))
|
||||
end
|
||||
-- wWildMon carries the answer through the animation, and
|
||||
-- wThrownBallWobbleCount is the counter GetPokeBallWobble bumps once per
|
||||
|
||||
@@ -490,6 +490,11 @@ check("hurt is easier to catch", hurtRate > fullRate, true)
|
||||
local asleepRate = Catching.rate({
|
||||
maxHp = 60, hp = 1, catchRate = 45, ball = "POKE_BALL", status = "sleep" })
|
||||
check("sleep adds 10", asleepRate, math.min(255, hurtRate + 10))
|
||||
checkNear("preview converts the exact byte roll", Catching.chance({
|
||||
maxHp = 60, hp = 60, catchRate = 45, ball = "POKE_BALL" }),
|
||||
fullRate * 100 / 256, 0.000001)
|
||||
check("master ball preview is certain", Catching.chance({
|
||||
maxHp = 60, hp = 60, catchRate = 1, ball = "MASTER_BALL" }), 100)
|
||||
-- The cart's bug: burn/poison/paralysis add nothing.
|
||||
check("poison adds nothing (cart bug)", Catching.rate({
|
||||
maxHp = 60, hp = 1, catchRate = 45, ball = "POKE_BALL",
|
||||
|
||||
@@ -282,6 +282,16 @@ local function runToMenu(screen, cap)
|
||||
return false
|
||||
end
|
||||
|
||||
do
|
||||
local screen = newScreen({ inventory = {
|
||||
MASTER_BALL = 1, POKE_BALL = 1,
|
||||
} })
|
||||
eq(screen:catchChance("MASTER_BALL"), 100,
|
||||
"the battle screen exposes a certain Master Ball preview")
|
||||
check(type(screen:catchChance("POKE_BALL")) == "number",
|
||||
"the battle screen exposes the live wild catch preview")
|
||||
end
|
||||
|
||||
-- ---- BattleMenu empties the textbox ---------------------------------------
|
||||
do
|
||||
local screen = newScreen()
|
||||
|
||||
@@ -163,14 +163,23 @@ function screen2:chooseMove(slot)
|
||||
return true
|
||||
end
|
||||
function screen2:cancelMove() self.phase = "menu" return true end
|
||||
function screen2:catchChance(ball)
|
||||
return ball == "MASTER_BALL" and 100 or 37.5
|
||||
end
|
||||
local game2 = {
|
||||
data = {
|
||||
pokemon = { CHIKORITA = { name = "CHIKORITA" },
|
||||
RATTATA = { name = "RATTATA" } },
|
||||
moves = { TACKLE = { name = "TACKLE", type = "NORMAL",
|
||||
power = 35, accuracy = 95, pp = 35 } },
|
||||
items = {
|
||||
MASTER_BALL = { name = "MASTER BALL", pocket = "BALL" },
|
||||
POTION = { name = "POTION", pocket = "ITEM" },
|
||||
},
|
||||
},
|
||||
save = { party = { player2 } }, stack = { states = { screen2 } },
|
||||
save = { party = { player2 }, inventory = {
|
||||
MASTER_BALL = 1, POTION = 2,
|
||||
} }, stack = { states = { screen2 } },
|
||||
}
|
||||
|
||||
local api2 = require("src.battle.gen2.BattleAPI").new(game2)
|
||||
@@ -179,6 +188,9 @@ check(snapshot2 and snapshot2.kind == "wild" and snapshot2.prompt == "menu",
|
||||
"Gold battle is discovered through its screen id")
|
||||
eq(snapshot2.player.maxHp, 21, "Gold max HP uses the mon field")
|
||||
eq(snapshot2.moves[1].name, "TACKLE", "Gold moves are copied")
|
||||
eq(#snapshot2.items, 1, "Gold exposes balls without guessing targeted items")
|
||||
eq(snapshot2.items[1].catchChance, 100,
|
||||
"Gold ball records expose the exact catch preview")
|
||||
snapshot2.player.hp = 0
|
||||
snapshot2.moves[1].pp = 0
|
||||
eq(player2.hp, 20, "changing a snapshot cannot change a Gold Pokemon")
|
||||
|
||||
Reference in New Issue
Block a user