mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-10-02 07:50:13 -04:00
aed3bbced7
- Battle Animation Pipeline:
* Eliminate 90-frame audio polling stall in anim_vm end/waitsound opcodes,
allowing attack animations to transition instantly into target hit flicker
and HP bar drain.
* Correct visual task counting in AnimVm:visualCount to match pret gAnimVisualTaskCount.
* Signal bg task completion on restorebg (vm.args[7] = -1).
* Fix termination conditions on shake tasks (ShakeMon, ShakeMon2, ShakeBattleTerrain),
WaterSport droplet arcs, and StartSinAnimTimer.
* Handle RGB555 color args in start_blend_anim_sprite_color and route P.destroyTask
to AnimTasks.destroy.
- RNG System & Seeding Parity:
* Replace math.random in Party.giveMon with Rng.Random32() for personality values
and Rng.Random() for IV bitfield extraction (HP/Atk/Def/Spe/SpA/SpD) matching GBA format.
* Seed RNG on Title Screen A/Start press via Rng.seedNewGame() (matching SeedRngAndSetTrainerId)
and perturb RNG state on title menu inputs.
* Route NPC overworld idle/wander timers and wild encounter slot rolls through Rng.compat.
- Pokédex Habitat & Area Maps:
* Fix encounter tables extraction and map indexing to resolve Pokemon habitat
locations across Kanto/Sevii maps rather than showing Area Unknown.
* Render steady semi-transparent red overlays for Pokédex area map locations.
- Multichoice Menus & UI:
* Implement multichoice table extraction and data population for Viridian blackboard
status ailments and special dialog interactions.
* Improve choice box borders, pagination, and Town Map wall viewing.
- Overworld Events & Flags:
* Fix Route 1 entrance girl NPC spawning and script triggers after starter selection.
* Ensure consistent save reload state in Oak's Lab.
400 lines
14 KiB
Lua
400 lines
14 KiB
Lua
-- In-battle item use: balls (catch), medicine, X items, Poké Doll.
|
||
-- Catch odds follow pret battle_script_commands.c (simplified shake check).
|
||
|
||
local ItemsData = require("src.core.game3.items_data")
|
||
local Bag = require("src.core.game3.bag")
|
||
local ItemUse = require("src.core.game3.item_use")
|
||
local Pokemon = require("src.core.game3.pokemon")
|
||
local Types = require("src.core.game3.battle.types")
|
||
|
||
local BattleItems = {}
|
||
|
||
-- pret sBallCatchBonuses (×10): Ultra=20, Great=15, Poke=10, Safari=15
|
||
local BALL_MULT = {
|
||
[1] = 255, -- MASTER (handled specially)
|
||
[2] = 20, -- ULTRA
|
||
[3] = 15, -- GREAT
|
||
[4] = 10, -- POKE
|
||
[5] = 15, -- SAFARI
|
||
[6] = 10, -- NET (default; type boost below)
|
||
[7] = 10, -- DIVE
|
||
[8] = 10, -- NEST (level boost)
|
||
[9] = 10, -- REPEAT
|
||
[10] = 10, -- TIMER
|
||
[11] = 10, -- LUXURY
|
||
[12] = 10, -- PREMIER
|
||
}
|
||
|
||
local X_STAT = {
|
||
[75] = "attack", -- X ATTACK
|
||
[76] = "defense", -- X DEFEND
|
||
[77] = "speed", -- X SPEED
|
||
[78] = "accuracy", -- X ACCURACY
|
||
[79] = "spAtk", -- X SPECIAL
|
||
}
|
||
|
||
local function roll(rng, lo, hi)
|
||
lo = lo or 0
|
||
hi = hi or 255
|
||
if type(rng) == "function" then
|
||
local ok, v = pcall(rng, lo, hi)
|
||
if ok and type(v) == "number" then return v end
|
||
end
|
||
local okR, Rng = pcall(require, "src.core.game3.rng")
|
||
if okR and Rng and Rng.compat then
|
||
return Rng.compat(lo, hi)
|
||
end
|
||
return math.random(lo, hi)
|
||
end
|
||
|
||
local Catching = require("src.core.game3.battle.catching")
|
||
|
||
function BattleItems.isBall(id)
|
||
return Catching.isBall(id)
|
||
end
|
||
|
||
function BattleItems.isBattleUsable(id)
|
||
local info = ItemsData.info(id)
|
||
if not info then return false end
|
||
local bu = tonumber(info.battleUsage) or 0
|
||
if bu > 0 then return true end
|
||
local pocket = info.pocket
|
||
return pocket == "POKE_BALLS" or pocket == "BERRY_POUCH"
|
||
end
|
||
|
||
function BattleItems.needsPartySelect(id)
|
||
if not id then return false end
|
||
if Catching.isBall(id) then return false end
|
||
local num = ItemsData.toNumericId(id) or tonumber(id)
|
||
if num == 80 then return false end -- POKE_DOLL
|
||
if num and X_STAT[num] then return false end -- X items
|
||
local use = ItemsData.fieldUseKind(id)
|
||
local info = ItemsData.info(id)
|
||
local bu = info and tonumber(info.battleUsage) or 0
|
||
if bu == 1 or use == "heal" or use == "status" or use == "revive"
|
||
or (info and info.pocket == "BERRY_POUCH") then
|
||
return true
|
||
end
|
||
return false
|
||
end
|
||
|
||
function BattleItems.canUseOn(st, itemId, partySlot, mon)
|
||
if not mon or not itemId then return false, "It won't have any effect." end
|
||
if mon.isEgg then return false, "An EGG can't be used on." end
|
||
local hp = tonumber(mon.hp) or 0
|
||
local maxHp = tonumber(mon.maxHp or mon.maxhp) or 1
|
||
local mk = ItemsData.medicineKind(itemId)
|
||
local use = ItemsData.fieldUseKind(itemId)
|
||
if mk == "revive" or use == "revive" then
|
||
if hp > 0 then return false, "It won't have any effect." end
|
||
return true
|
||
elseif hp <= 0 then
|
||
return false, "It won't have any effect."
|
||
elseif mk == "status" or use == "status" then
|
||
local s = mon.status
|
||
if not s or s == 0 or s == "" then return false, "It won't have any effect." end
|
||
return true
|
||
else
|
||
if hp >= maxHp then return false, "It won't have any effect." end
|
||
return true
|
||
end
|
||
end
|
||
|
||
function BattleItems.ballMultiplier(itemId, foeBattler, st, session)
|
||
return Catching.ballMultiplier(itemId, foeBattler, st, session)
|
||
end
|
||
|
||
function BattleItems.catchOdds(itemId, foeBattler, st, session)
|
||
return Catching.catchOdds(itemId, foeBattler, st, session)
|
||
end
|
||
|
||
function BattleItems.tryCatch(itemId, foeBattler, st, rng, session)
|
||
return Catching.tryCatch(itemId, foeBattler, st, session, rng)
|
||
end
|
||
|
||
function BattleItems.storeCaught(session, foeBattler, ballId)
|
||
local res = Catching.storeCaught(session, foeBattler, ballId)
|
||
return res.location
|
||
end
|
||
|
||
local function user_battler(st, battlerId)
|
||
if battlerId == nil or battlerId == 0 then return st.player end
|
||
return st.battlers and st.battlers[battlerId] or st.player
|
||
end
|
||
|
||
local function sync_player_battler(st, battlerId)
|
||
local b = user_battler(st, battlerId)
|
||
if not b or not b.mon then return end
|
||
b.fainted = (tonumber(b.mon.hp) or 0) <= 0
|
||
b.status = b.mon.status
|
||
end
|
||
|
||
--- Use a battle item. Returns:
|
||
-- result: "catch"|"fail_catch"|"heal"|"xitem"|"doll"|"cancel"|"error"
|
||
-- msgs: string list
|
||
-- endsTurn: bool (enemy may still move unless endsBattle)
|
||
-- endsBattle: bool
|
||
function BattleItems.use(st, adapter, bag, session, itemId, partySlot, battlerId)
|
||
local msgs = {}
|
||
local function say(t)
|
||
msgs[#msgs + 1] = t
|
||
if adapter and adapter.say then adapter:say(t) end
|
||
end
|
||
|
||
if not itemId or not bag then
|
||
return "error", msgs, false, false
|
||
end
|
||
if not Bag.has(bag, itemId, 1) then
|
||
say("You don't have that item.")
|
||
return "error", msgs, false, false
|
||
end
|
||
|
||
local num = ItemsData.toNumericId(itemId) or tonumber(itemId)
|
||
local name = ItemsData.displayName(itemId)
|
||
|
||
-- Poké Doll → flee wild
|
||
if num == 80 then
|
||
if not st.wild then
|
||
say("This can't be used right now.")
|
||
return "error", msgs, false, false
|
||
end
|
||
Bag.remove(bag, itemId, 1)
|
||
say(tostring(session and session.name or "RED") .. " used\nthe " .. name .. "!")
|
||
say("Got away safely!")
|
||
return "doll", msgs, true, true
|
||
end
|
||
|
||
-- Balls
|
||
if BattleItems.isBall(itemId) then
|
||
if not st.wild then
|
||
say("The TRAINER blocked\nthe BALL!")
|
||
return "error", msgs, false, false
|
||
end
|
||
Bag.remove(bag, itemId, 1)
|
||
say(tostring(session and session.name or "RED") .. " used\nthe " .. name .. "!")
|
||
local rng = adapter and adapter.rng and adapter:rng() or math.random
|
||
local foe = Catching.targetFor(st, battlerId)
|
||
local caught, shakes = BattleItems.tryCatch(itemId, foe, st, rng, session)
|
||
if caught then
|
||
local res = Catching.storeCaught(session, foe, itemId)
|
||
local ename = (foe and foe.mon and (foe.mon.nickname or foe.mon.name))
|
||
or Pokemon.name(foe and foe.species) or "POKéMON"
|
||
say("Gotcha!\n" .. ename .. " was caught!")
|
||
if res and res.firstTimeCaught then
|
||
say(ename .. "'s data was\nadded to the POKéDEX.")
|
||
end
|
||
if res and res.location == "pc" then
|
||
say(ename .. " was transferred\nto the PC.")
|
||
end
|
||
return "catch", msgs, true, true
|
||
end
|
||
if shakes == 0 then
|
||
say("Oh no! The POKéMON broke free!")
|
||
elseif shakes == 1 then
|
||
say("Aww! It appeared to be caught!")
|
||
elseif shakes == 2 then
|
||
say("Aargh! Almost had it!")
|
||
else
|
||
say("Shoot! It was so close too!")
|
||
end
|
||
return "fail_catch", msgs, true, false
|
||
end
|
||
|
||
-- X items
|
||
if num and X_STAT[num] then
|
||
local stat = X_STAT[num]
|
||
local battler = user_battler(st, battlerId)
|
||
if not battler or not battler.stages then
|
||
return "error", msgs, false, false
|
||
end
|
||
local cur = battler.stages[stat] or 0
|
||
if cur >= 6 then
|
||
say("It won't have any effect.")
|
||
return "error", msgs, false, false
|
||
end
|
||
Bag.remove(bag, itemId, 1)
|
||
say(tostring(session and session.name or "RED") .. " used\nthe " .. name .. "!")
|
||
if adapter and adapter.changeStages then
|
||
adapter:changeStages(battler, { [stat] = 1 })
|
||
else
|
||
battler.stages[stat] = math.min(6, cur + 1)
|
||
end
|
||
local label = ({
|
||
attack = "ATTACK", defense = "DEFENSE", speed = "SPEED",
|
||
accuracy = "ACCURACY", spAtk = "SP. ATK",
|
||
})[stat] or stat
|
||
local pname = battler.mon and (battler.mon.nickname or battler.mon.name) or "POKéMON"
|
||
say(pname .. "'s " .. label .. "\nrose!")
|
||
return "xitem", msgs, true, false
|
||
end
|
||
|
||
-- Medicine / berries on party mon
|
||
local use = ItemsData.fieldUseKind(itemId)
|
||
local info = ItemsData.info(itemId)
|
||
local bu = info and tonumber(info.battleUsage) or 0
|
||
if bu == 1 or use == "heal" or use == "status" or use == "revive"
|
||
or (info and info.pocket == "BERRY_POUCH") then
|
||
-- Prefer in-battle party copy (writeback syncs to session).
|
||
local party = st.playerParty or (session and session.party)
|
||
if not partySlot then
|
||
return "need_slot", msgs, false, false
|
||
end
|
||
local mon = party and party[partySlot]
|
||
if not mon then
|
||
say("It won't have any effect.")
|
||
return "error", msgs, false, false
|
||
end
|
||
local ok = false
|
||
local mk = ItemsData.medicineKind(itemId)
|
||
if mk == "revive" or use == "revive" then
|
||
local max = num == 25
|
||
if num == 45 then
|
||
ok = ItemUse.reviveAll(party)
|
||
else
|
||
ok = ItemUse.revive(mon, max)
|
||
end
|
||
elseif mk == "status" or use == "status" then
|
||
ok = ItemUse.clearStatus(mon, itemId)
|
||
else
|
||
ok = ItemUse.healMon(session, mon, itemId)
|
||
end
|
||
if not ok then
|
||
say("It won't have any effect.")
|
||
return "error", msgs, false, false
|
||
end
|
||
Bag.remove(bag, itemId, 1)
|
||
say(tostring(session and session.name or "RED") .. " used\nthe " .. name .. "!")
|
||
if st.player and st.player.partyIndex == partySlot then
|
||
sync_player_battler(st)
|
||
end
|
||
local b2 = st.double and st.battlers and st.battlers[2]
|
||
if b2 and b2.partyIndex == partySlot then sync_player_battler(st, 2) end
|
||
return "heal", msgs, true, false
|
||
end
|
||
|
||
say("This can't be used right now.")
|
||
return "error", msgs, false, false
|
||
end
|
||
|
||
local ENEMY_CURE_TEXT = {
|
||
[0] = "\nsnapped it out of confusion!",
|
||
[1] = "\ncured paralysis!",
|
||
[2] = "\ndefrosted it!",
|
||
[3] = "\nhealed its burn!",
|
||
[4] = "\ncured poison!",
|
||
[5] = "\nwoke it from its sleep!",
|
||
}
|
||
|
||
local ENEMY_STAT_NAME = { [1] = "ATTACK", [2] = "DEFENSE", [3] = "SPEED", [4] = "SP. ATK", [5] = "SP. DEF", [6] = "accuracy" }
|
||
|
||
-- pokefirered/src/pokemon.c:4001
|
||
local function enemy_item_effects(st, ad, b, e)
|
||
local AiItems = require("src.core.game3.battle.ai_items")
|
||
local band = AiItems.band
|
||
if band(e[1], 0x80) ~= 0 and b.expInfatuated then b.expInfatuated, b.expInfatuatedWith = nil, nil end
|
||
if band(e[1], 0x30) ~= 0 and not b.expFocusEnergy then
|
||
b.expFocusEnergy, b.focusEnergy = true, true
|
||
end
|
||
local function raise(key, n)
|
||
local cur = b.stages and b.stages[key] or 0
|
||
if n > 0 and cur < 6 then ad:changeStages(b, { [key] = n }) end
|
||
end
|
||
raise("attack", band(e[1], 0x0F))
|
||
raise("defense", math.floor(band(e[2], 0xF0) / 16))
|
||
raise("speed", band(e[2], 0x0F))
|
||
raise("accuracy", math.floor(band(e[3], 0xF0) / 16))
|
||
raise("spAtk", band(e[3], 0x0F))
|
||
local side = ad:ownSide(b)
|
||
if band(e[4], 0x80) ~= 0 and side and (tonumber(side.expMistTurns) or 0) == 0 then
|
||
side.expMistTurns = 5
|
||
end
|
||
local s = AiItems.statusName(b)
|
||
local cure = (band(e[4], 0x20) ~= 0 and s == "SLP") or (band(e[4], 0x10) ~= 0 and (s == "PSN" or s == "TOX"))
|
||
or (band(e[4], 0x08) ~= 0 and s == "BRN") or (band(e[4], 0x04) ~= 0 and s == "FRZ")
|
||
or (band(e[4], 0x02) ~= 0 and s == "PAR")
|
||
if cure then
|
||
if s == "SLP" then b.expNightmare = nil end
|
||
ad:clearStatus(b)
|
||
end
|
||
if band(e[4], 0x01) ~= 0 and (tonumber(b.confusionTurns) or 0) > 0 then b.confusionTurns = nil end
|
||
if band(e[5], 0x04) ~= 0 then
|
||
local hp, maxHp = ad:hp(b), ad:maxHp(b)
|
||
local revive = band(e[5], 0x40) ~= 0
|
||
if (revive and hp == 0) or (not revive and hp ~= 0) then
|
||
local data = e.hp or 0
|
||
if data == AiItems.HEAL_HP_FULL then
|
||
data = maxHp - hp
|
||
elseif data == AiItems.HEAL_HP_HALF then
|
||
data = math.floor(maxHp / 2)
|
||
if data == 0 then data = 1 end
|
||
elseif data == AiItems.HEAL_HP_LVL_UP then
|
||
data = 0
|
||
end
|
||
if maxHp ~= hp then ad:heal(b, data) end
|
||
end
|
||
end
|
||
if b.mon then b.status = b.mon.status end
|
||
end
|
||
|
||
-- pokefirered/src/battle_main.c:4150
|
||
function BattleItems.enemyUse(st, adapter, act)
|
||
local AiItems = require("src.core.game3.battle.ai_items")
|
||
local State = require("src.core.game3.battle.state")
|
||
local id = act and (act.battler or 1) or 1
|
||
local b = State.battler(st, id)
|
||
if not b or not b.mon or not act.item then return false end
|
||
local item = act.item
|
||
local e = AiItems.effect(item)
|
||
local kind = act.aiItemType or (e and AiItems.itemType(item, e))
|
||
local flags = tonumber(act.aiItemFlags) or 0
|
||
b.expFuryCutter, b.destinyBond, b.expDestinyBond, b.expGrudge = 0, nil, nil, nil
|
||
local iname = ItemsData.displayName(item)
|
||
local bname = adapter:displayName(b)
|
||
local trainer = (st.trainerClassName and st.trainerClassName ~= "")
|
||
and (st.trainerClassName .. " " .. (st.trainerName or "")) or (st.trainerName or "TRAINER")
|
||
-- pokefirered/data/battle_scripts_2.s:134
|
||
adapter:pushEvent({ kind = "item_use", battler = id, side = b.side, item = item, se = "SE_USE_ITEM" })
|
||
adapter:say(trainer .. "\nused " .. iname .. "!")
|
||
if e then enemy_item_effects(st, adapter, b, e) end
|
||
local T = AiItems.TYPE
|
||
if kind == T.FULL_RESTORE or kind == T.HEAL_HP then
|
||
adapter:say(bname .. "'s " .. iname .. "\nrestored health!")
|
||
adapter:pushEvent({ kind = "status", battler = id, side = b.side })
|
||
elseif kind == T.CURE_CONDITION then
|
||
local chooser = 0
|
||
if flags % 2 == 1 then
|
||
if AiItems.band(flags, 0x3E) ~= 0 then chooser = 5 end
|
||
else
|
||
local f = flags
|
||
while f > 0 and f % 2 == 0 do
|
||
f = math.floor(f / 2)
|
||
chooser = chooser + 1
|
||
end
|
||
end
|
||
adapter:say(bname .. "'s " .. iname .. (ENEMY_CURE_TEXT[chooser] or ENEMY_CURE_TEXT[0]))
|
||
adapter:pushEvent({ kind = "status", battler = id, side = b.side })
|
||
elseif kind == T.X_STAT then
|
||
if AiItems.band(flags, 0x80) ~= 0 then
|
||
adapter:say(bname .. " used\n" .. iname .. " to hustle!")
|
||
else
|
||
local stat, f = 1, flags
|
||
while f > 0 and f % 2 == 0 do
|
||
f = math.floor(f / 2)
|
||
stat = stat + 1
|
||
end
|
||
adapter:say("Using " .. iname .. ", the " .. (ENEMY_STAT_NAME[stat] or "ATTACK")
|
||
.. "\nof " .. bname .. " rose!")
|
||
end
|
||
elseif kind == T.GUARD_SPECS then
|
||
-- pokefirered/src/battle_main.c:4216
|
||
if st.double then
|
||
adapter:say(bname .. " is getting\npumped!")
|
||
else
|
||
adapter:say(((b.side == "player") and "Ally" or "Foe") .. " became\nshrouded in MIST!")
|
||
end
|
||
end
|
||
return true
|
||
end
|
||
|
||
return BattleItems
|