mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-19 12:15:31 +02:00
Merge pull request #1494 from AverageConsumer/codex/gen2-catch-previews
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`,
|
`prompt` describes the currently visible choice (`menu`, `moves`, `party`,
|
||||||
`advance`, `safari`, or `mimic`) and is `locked` when another screen or battle
|
`advance`, `safari`, or `mimic`) and is `locked` when another screen or battle
|
||||||
phase owns input. Generation-specific features remain optional: Gen 1 includes
|
phase owns input. Generation-specific features remain optional: Gen 1 includes
|
||||||
battle medicine, balls, catch previews, Safari balls, and Mimic choices;
|
battle medicine, balls, catch previews, Safari balls, and Mimic choices. Gold
|
||||||
Gold currently returns an empty `items` list rather than guessing at its
|
exposes balls and their exact stock catch previews; targeted medicine remains
|
||||||
pocketed PACK flow. Callers should ignore unknown fields and tolerate absent
|
screen-owned and is omitted rather than guessing at its pocketed PACK flow.
|
||||||
optional ones.
|
Callers should ignore unknown fields and tolerate absent optional ones.
|
||||||
|
|
||||||
## Battle menu intents
|
## Battle menu intents
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,21 @@ local function messageCopy(screen)
|
|||||||
return #lines > 0 and lines or nil
|
return #lines > 0 and lines or nil
|
||||||
end
|
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)
|
local function signature(game, screen, top)
|
||||||
if not screen then return "none" end
|
if not screen then return "none" end
|
||||||
local battle = screen.battle or {}
|
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.hp)
|
||||||
parts[#parts + 1] = tostring(mon.status)
|
parts[#parts + 1] = tostring(mon.status)
|
||||||
end
|
end
|
||||||
|
for _, item in ipairs(itemCopies(game, screen, false)) do
|
||||||
|
parts[#parts + 1] = item.id .. "=" .. tostring(item.count)
|
||||||
|
end
|
||||||
return table.concat(parts, "|")
|
return table.concat(parts, "|")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -111,9 +129,9 @@ function BattleAPI:snapshot()
|
|||||||
player = monCopy(game.data, battle.player, true),
|
player = monCopy(game.data, battle.player, true),
|
||||||
enemy = monCopy(game.data, battle.enemy, true),
|
enemy = monCopy(game.data, battle.enemy, true),
|
||||||
party = party, moves = moveCopies(game, battle),
|
party = party, moves = moveCopies(game, battle),
|
||||||
-- Gold's PACK is pocketed and target selection is screen-owned. Omit it
|
-- Targeted medicine remains screen-owned, but balls are complete semantic
|
||||||
-- until the engine can expose the same semantic item records as Gen 1.
|
-- records and can safely expose the same read-only preview as Gen 1.
|
||||||
items = {} }
|
items = itemCopies(game, screen, battle.wild and not screen.tutorial) }
|
||||||
end
|
end
|
||||||
|
|
||||||
local MENU_CHOICES = { fight = true, party = true, item = true, run = true }
|
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
|
return math.min(255, rate), false
|
||||||
end
|
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
|
-- 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
|
-- 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:
|
-- live on src/battle/gen2/Battle.lua (Battle.STATUSES) and carry BOTH numbers:
|
||||||
|
|||||||
+33
-30
@@ -2945,6 +2945,37 @@ function BattleState:openDexEntry(species)
|
|||||||
})
|
})
|
||||||
end
|
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
|
-- 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
|
-- 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
|
-- the field pack runs. Anything else reports that it cannot be used, which is
|
||||||
@@ -2974,7 +3005,6 @@ function BattleState:useItem(itemId)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
local enemy = self.battle.enemy
|
local enemy = self.battle.enemy
|
||||||
local enemyDef = data.pokemon and data.pokemon[enemy.species]
|
|
||||||
local caught, rate
|
local caught, rate
|
||||||
if self.tutorial then
|
if self.tutorial then
|
||||||
-- `ld a, [wBattleType] / cp BATTLETYPE_TUTORIAL /
|
-- `ld a, [wBattleType] / cp BATTLETYPE_TUTORIAL /
|
||||||
@@ -2988,35 +3018,8 @@ function BattleState:useItem(itemId)
|
|||||||
caught, rate = true, 255
|
caught, rate = true, 255
|
||||||
else
|
else
|
||||||
-- The specialty-ball conditions (BallMultiplierFunctionTable): each one
|
-- The specialty-ball conditions (BallMultiplierFunctionTable): each one
|
||||||
-- is something this screen already knows. Heavy Ball reads the dex
|
-- is also used by the read-only preview, so both paths stay exact.
|
||||||
-- weight, Moon Ball the species' stone row, Love Ball both genders,
|
caught, rate = Catching.attempt(self:catchOptions(itemId))
|
||||||
-- 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,
|
|
||||||
})
|
|
||||||
end
|
end
|
||||||
-- wWildMon carries the answer through the animation, and
|
-- wWildMon carries the answer through the animation, and
|
||||||
-- wThrownBallWobbleCount is the counter GetPokeBallWobble bumps once per
|
-- 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({
|
local asleepRate = Catching.rate({
|
||||||
maxHp = 60, hp = 1, catchRate = 45, ball = "POKE_BALL", status = "sleep" })
|
maxHp = 60, hp = 1, catchRate = 45, ball = "POKE_BALL", status = "sleep" })
|
||||||
check("sleep adds 10", asleepRate, math.min(255, hurtRate + 10))
|
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.
|
-- The cart's bug: burn/poison/paralysis add nothing.
|
||||||
check("poison adds nothing (cart bug)", Catching.rate({
|
check("poison adds nothing (cart bug)", Catching.rate({
|
||||||
maxHp = 60, hp = 1, catchRate = 45, ball = "POKE_BALL",
|
maxHp = 60, hp = 1, catchRate = 45, ball = "POKE_BALL",
|
||||||
|
|||||||
@@ -282,6 +282,16 @@ local function runToMenu(screen, cap)
|
|||||||
return false
|
return false
|
||||||
end
|
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 ---------------------------------------
|
-- ---- BattleMenu empties the textbox ---------------------------------------
|
||||||
do
|
do
|
||||||
local screen = newScreen()
|
local screen = newScreen()
|
||||||
|
|||||||
@@ -163,14 +163,23 @@ function screen2:chooseMove(slot)
|
|||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
function screen2:cancelMove() self.phase = "menu" 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 = {
|
local game2 = {
|
||||||
data = {
|
data = {
|
||||||
pokemon = { CHIKORITA = { name = "CHIKORITA" },
|
pokemon = { CHIKORITA = { name = "CHIKORITA" },
|
||||||
RATTATA = { name = "RATTATA" } },
|
RATTATA = { name = "RATTATA" } },
|
||||||
moves = { TACKLE = { name = "TACKLE", type = "NORMAL",
|
moves = { TACKLE = { name = "TACKLE", type = "NORMAL",
|
||||||
power = 35, accuracy = 95, pp = 35 } },
|
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)
|
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")
|
"Gold battle is discovered through its screen id")
|
||||||
eq(snapshot2.player.maxHp, 21, "Gold max HP uses the mon field")
|
eq(snapshot2.player.maxHp, 21, "Gold max HP uses the mon field")
|
||||||
eq(snapshot2.moves[1].name, "TACKLE", "Gold moves are copied")
|
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.player.hp = 0
|
||||||
snapshot2.moves[1].pp = 0
|
snapshot2.moves[1].pp = 0
|
||||||
eq(player2.hp, 20, "changing a snapshot cannot change a Gold Pokemon")
|
eq(player2.hp, 20, "changing a snapshot cannot change a Gold Pokemon")
|
||||||
|
|||||||
Reference in New Issue
Block a user