Files
gen1recomp/src/core/game3/item_use.lua
T

1259 lines
44 KiB
Lua

-- FRLG field item-use handlers for game3 bag.
local ItemsData = require("src.core.game3.items_data")
local Bag = require("src.core.game3.bag")
local Pokemon = require("src.core.game3.pokemon")
local ModRuntime = require("src.mods.Runtime")
local Strings = require("src.core.Strings")
local RomText = require("src.core.game3.rom_text")
local Capabilities = require("src.core.game3.capabilities")
local ItemUse = {}
local function player_name(session)
return tostring((session and (session.name or session.playerName)) or "")
end
local function mon_text(key, mon, v2)
return (RomText.box(key, { stringVars = { Pokemon.displayMonName(mon), v2 } }))
end
-- src/party_menu.c:4528 Task_DisplayHPRestoredMessage
local function hp_restored_text(mon, restored)
return mon_text("gText_PkmnHPRestoredByVar2", mon, tostring(restored))
end
-- src/item_use.c:191 PrintNotTheTimeToUseThat
local function not_the_time(session)
return (RomText.box("gText_OakForbidsUseOfItemHere", { playerName = player_name(session) }))
end
local function no_pokemon_text()
return (RomText.box("gText_ThereIsNoPokemon"))
end
local function wont_have_effect()
return (RomText.box("gText_WontHaveEffect"))
end
local function cant_dismount_bike_text()
if RomText.has and RomText.has("gText_CantDismountBike") then
local ok, res = pcall(RomText.box, "gText_CantDismountBike")
if ok and res then return res end
end
return "You can't dismount your BIKE here."
end
-- pokefirered/src/data/pokemon/item_effects.h:80
local HERB_HEAL = { [30] = 50, [31] = 200 }
local ITEM_REVIVAL_HERB = 33
-- pokefirered/src/data/pokemon/item_effects.h:81
local BITTER_MEDICINE_FRIENDSHIP = {
[30] = { -5, -5, -10 },
[31] = { -10, -10, -15 },
[32] = { -5, -5, -10 },
[33] = { -15, -15, -20 },
}
ItemUse.BITTER_MEDICINE_FRIENDSHIP = BITTER_MEDICINE_FRIENDSHIP
ItemUse.ITEM_REVIVAL_HERB = ITEM_REVIVAL_HERB
local function heal_amount(id)
local n = ItemsData.HEAL_AMOUNT[id]
if n then return n end
local num = ItemsData.toNumericId(id) or tonumber(id)
if num and HERB_HEAL[num] then return HERB_HEAL[num] end
if num and ItemsData.HEAL_AMOUNT[num] then return ItemsData.HEAL_AMOUNT[num] end
-- Pack holdEffectParam: Potion=20, Super=50, Hyper=200, Full Restore=255→full
local info = ItemsData.info(id)
local param = info and tonumber(info.holdEffectParam)
if param and param > 0 then
if param >= 255 then return 9999 end
return param
end
return nil
end
local function mon_status(mon)
if not mon then return nil, 0 end
local st = mon.status
local sleep = tonumber(mon.sleep) or 0
if type(st) == "string" and st ~= "" and st ~= "0" then return st, sleep end
local n = tonumber(st) or 0
if n ~= 0 then return n, sleep end
if sleep > 0 then return "SLP", sleep end
return nil, 0
end
--- Apply heal to one party slot. Returns ok, restoredAmount
function ItemUse.healMon(session, mon, id)
if not mon then return false, 0 end
local kind = ItemsData.medicineKind(id)
local maxHp = tonumber(mon.maxHp) or tonumber(mon.maxhp) or 0
local hp = tonumber(mon.hp) or 0
if maxHp <= 0 then return false, 0 end
if kind == "full_restore" then
if hp <= 0 then return false, 0 end
local changed = false
local restored = 0
if hp < maxHp then
restored = maxHp - hp
mon.hp = maxHp
changed = true
end
local st = mon_status(mon)
if st then
mon.status = nil
mon.sleep = 0
changed = true
end
return changed, restored
end
local amt = heal_amount(id)
if not amt then return false, 0 end
if hp <= 0 then return false, 0 end
if hp >= maxHp then return false, 0 end
local newHp
if amt >= 9999 then
newHp = maxHp
else
newHp = math.min(maxHp, hp + amt)
end
local restored = newHp - hp
mon.hp = newHp
return true, restored
end
-- pokefirered/include/constants/item_effects.h:20
local STATUS_BIT = {
PSN = 0x10, TOX = 0x10, BRN = 0x08, FRZ = 0x04, SLP = 0x20, PAR = 0x02,
[1] = 0x10, [2] = 0x10, [3] = 0x08, [4] = 0x04, [5] = 0x20, [6] = 0x02,
}
local CURED_TEXT_KIND = { [0x10] = "poison", [0x08] = "burn", [0x04] = "freeze", [0x20] = "sleep", [0x02] = "paralysis" }
-- src/party_menu.c:4340 GetMedicineItemEffectMessage
local CURED_TEXT = {
poison = "gText_PkmnCuredOfPoison",
sleep = "gText_PkmnWokeUp2",
burn = "gText_PkmnBurnHealed",
freeze = "gText_PkmnThawedOut",
paralysis = "gText_PkmnCuredOfParalysis",
confusion = "gText_PkmnSnappedOutOfConfusion",
infatuation = "gText_PkmnGotOverInfatuation",
status = "gText_PkmnBecameHealthy",
}
-- pokefirered/src/party_menu.c:4412
local FLUTES = { [39] = true, [40] = true, [41] = true }
function ItemUse.isFlute(id)
return FLUTES[ItemsData.toNumericId(id) or tonumber(id)] == true
end
-- pokefirered/src/party_menu.c:5345 GetItemEffectType
function ItemUse.cureKind(id)
local info = ItemsData.info(id)
local e = info and info.effect
if type(e) ~= "table" then return nil end
local statusCure = bit.band(tonumber(e[4]) or 0, 0x3F)
if statusCure == 0x01 then return "confusion" end
if statusCure == 0 and bit.band(tonumber(e[1]) or 0, 0x80) ~= 0 then return "infatuation" end
return CURED_TEXT_KIND[statusCure] or "status"
end
-- pokefirered/src/party_menu.c:4510
function ItemUse.medicineText(mon, hpBefore, cured)
local gained = (tonumber(mon and mon.hp) or 0) - (tonumber(hpBefore) or 0)
if gained > 0 then return hp_restored_text(mon, gained) end
return mon_text(CURED_TEXT[cured] or CURED_TEXT.status, mon)
end
-- pokefirered/src/pokemon.c:4511
function ItemUse.clearStatus(mon, id)
if not mon then return false, nil end
local st, sleep = mon_status(mon)
if not st and sleep <= 0 then return false, nil end
local info = ItemsData.info(id)
local e = info and info.effect
if type(e) ~= "table" then return false, nil end
local mask = bit.band(tonumber(e[4]) or 0, 0x3E)
local have = STATUS_BIT[st] or ((sleep > 0) and 0x20) or 0
if bit.band(mask, have) == 0 then return false, nil end
local cured = CURED_TEXT_KIND[mask] or "status"
mon.status = nil
mon.sleep = 0
return true, cured
end
function ItemUse.revive(mon, max)
if not mon then return false, 0 end
local maxHp = tonumber(mon.maxHp) or tonumber(mon.maxhp) or 0
local hp = tonumber(mon.hp) or 0
if hp > 0 or maxHp <= 0 then return false, 0 end
if max then
mon.hp = maxHp
else
mon.hp = math.max(1, math.floor(maxHp / 2))
end
mon.status = nil
mon.sleep = 0
return true, mon.hp
end
function ItemUse.reviveAll(party)
local any = false
for _, mon in ipairs(party or {}) do
if ItemUse.revive(mon, true) then any = true end
end
return any
end
-- pokefirered/src/party_menu.c:1647
local function bag_full_text(itemId)
local pocket = ItemsData.pocketOf(itemId)
local name
if pocket == "TM_CASE" then
name = ItemsData.displayName(ItemsData.ITEM_TM_CASE)
elseif pocket == "BERRY_POUCH" then
name = ItemsData.displayName(ItemsData.ITEM_BERRY_POUCH)
else
name = RomText.plain("gText_MenuBag")
end
return RomText.box("gText_BagFullCouldNotRemoveItem", { stringVars = { name } })
end
local function held_item(mon)
local prev = mon and (mon.item or mon.heldItem)
if prev and prev ~= 0 and prev ~= "" and prev ~= "NONE" then return prev end
return nil
end
-- src/party_menu.c:5607 RemoveItemToGiveFromBag, :5617 ReturnGiveItemToBagOrPC
function ItemUse.bagGiveSource(bag)
return {
remove = function(id) return Bag.remove(bag, id, 1) end,
restore = function(id) return Bag.add(bag, id, 1) end,
-- src/party_menu.c:1580
quest = function(monName, itemName) return "GaveMonHeldItem2", { monName, itemName } end,
}
end
-- src/party_menu.c:3422 CB2_SelectBagItemToGive
function ItemUse.partyGiveSource(bag)
local source = ItemUse.bagGiveSource(bag)
-- src/party_menu.c:1579
source.quest = function(monName, itemName) return "GaveMonHeldItem", { monName, itemName } end
return source
end
-- src/party_menu.c:5455 TryGiveItemOrMailToSelectedMon
function ItemUse.checkGive(session, id, partySlot)
local party = session and session.party
local mon = party and party[partySlot]
if not mon then return "noparty", nil, no_pokemon_text() end
local pocket = ItemsData.pocketOf(id)
if pocket == "KEY_ITEMS" or pocket == "TM_CASE" then
-- src/item_menu.c:1635
return "cant_hold", nil, (RomText.box("gText_ItemCantBeHeld", { stringVars = { ItemsData.displayName(id) } }))
end
local prev = held_item(mon)
if not prev then return "give", nil, nil end
if require("src.core.game3.mail").isMailItem(ItemsData.toNumericId(prev) or prev) then
-- src/party_menu.c:5600 DisplayItemMustBeRemovedFirstMessage
return "mail", prev, (RomText.box("gText_RemoveMailBeforeItem"))
end
-- src/party_menu.c:1601 DisplayAlreadyHoldingItemSwitchMessage
return "switch", prev, mon_text("gText_PkmnAlreadyHoldingItemSwitch", mon, ItemsData.displayName(prev))
end
-- src/party_menu.c:5487 GiveItemToSelectedMon
function ItemUse.giveHeld(session, bag, id, partySlot, source)
source = source or ItemUse.bagGiveSource(bag)
local mon = session.party[partySlot]
local itemName = ItemsData.displayName(id)
local key, args = source.quest(Pokemon.displayMonName(mon), itemName)
require("src.core.game3.quest_log_recorder").event(session, key, args)
mon.item = ItemsData.toNumericId(id) or id
mon.heldItem = mon.item
source.remove(id)
-- src/party_menu.c:1586
return mon_text("gText_PkmnWasGivenItem", mon, itemName)
end
-- src/party_menu.c:5563 Task_HandleSwitchItemsFromBagYesNoInput
function ItemUse.switchHeld(session, bag, id, partySlot, source)
source = source or ItemUse.bagGiveSource(bag)
local mon = session.party[partySlot]
local prev = held_item(mon)
source.remove(id)
if not Bag.add(bag, prev, 1) then
source.restore(id)
return false, bag_full_text(prev)
end
mon.item = ItemsData.toNumericId(id) or id
mon.heldItem = mon.item
-- src/party_menu.c:1612 SetSwappedHeldItemQuestLogEvent
require("src.core.game3.quest_log_recorder").event(session, "SwappedHeldItemsOnMon",
{ Pokemon.displayMonName(mon), ItemsData.displayName(prev), ItemsData.displayName(id) })
-- src/party_menu.c:1615
return true, (RomText.box("gText_SwitchedPkmnItem",
{ stringVars = { ItemsData.displayName(id), ItemsData.displayName(prev) } }))
end
--- Give item to party mon as held item. Returns ok, reason, messageText.
function ItemUse.giveToMon(session, bag, id, partySlot, source)
local kind, _, text = ItemUse.checkGive(session, id, partySlot)
if kind == "give" then
return true, "give", ItemUse.giveHeld(session, bag, id, partySlot, source)
elseif kind == "switch" then
local ok, msg = ItemUse.switchHeld(session, bag, id, partySlot, source)
return ok, ok and "give" or "bag_full", msg
end
return false, kind, text
end
--- Take held item from party mon. Returns ok, reason, messageText.
function ItemUse.takeFromMon(session, bag, partySlot)
local party = session and session.party
local mon = party and party[partySlot]
if not mon then return false, "noparty", no_pokemon_text() end
local held = mon.item or mon.heldItem
local monName = Pokemon.displayMonName(mon)
if not held or held == 0 or held == "" or held == "NONE" then
return false, "none", mon_text("gText_PkmnNotHolding", mon)
end
if not Bag.canAdd(bag, held, 1) then
return false, "bag_full", bag_full_text(held)
end
mon.item = nil
mon.heldItem = nil
Bag.add(bag, held, 1)
-- src/party_menu.c:1596
local text = mon_text("gText_ReceivedItemFromPkmn", mon, ItemsData.displayName(held))
require("src.core.game3.quest_log_recorder").event(session,"TookHeldItemFromMon",
{monName,ItemsData.displayName(held)})
return true, "take", text
end
-- pokefirered/include/global.fieldmap.h:191 gMapHeader
local function current_map_def(session)
local mapId = session and session.map
if type(mapId) ~= "string" then return nil end
local Runtime = package.loaded["src.core.game3.runtime"]
local game = Runtime and Runtime._game
local data = game and game.data and game.data.maps
local def = data and data[mapId]
if def then return def end
local Map = package.loaded["src.core.game3.map"]
local cur = Map and Map.currentDef and Map.currentDef()
if type(cur) == "table" and cur.id == mapId then return cur end
return nil
end
-- pokefirered/src/overworld.c:948 Overworld_IsBikingAllowed
local function map_header_flag(session, key)
local def = current_map_def(session)
if def == nil or def[key] == nil then return nil end
return (tonumber(def[key]) or 0) ~= 0
end
local function is_outdoor(session)
local mapId = session and session.map
if type(mapId) ~= "string" then return false end
local def = current_map_def(session)
local pair = def and (def.pair or (def.midLayout and def.midLayout.pair))
if type(pair) == "string" and pair:find("outdoor", 1, true) then
return true
end
-- Heuristic when layout pair missing
if mapId:find("HOUSE", 1, true) or mapId:find("CENTER", 1, true)
or mapId:find("GYM", 1, true) or mapId:find("MART", 1, true)
or mapId:find("LAB", 1, true) or mapId:find("CAVE", 1, true)
or mapId:find("TUNNEL", 1, true) or mapId:find("TOWER", 1, true)
or mapId:find("MANSION", 1, true) then
return false
end
if mapId:find("ROUTE", 1, true) or mapId:find("TOWN", 1, true)
or mapId:find("CITY", 1, true) or mapId:find("ISLAND", 1, true) then
return true
end
return false
end
-- pokefirered/src/item_use.c:614 CanUseEscapeRopeOnCurrMap
local function can_escape(session)
if not session then return false end
return map_header_flag(session, "allowEscaping") == true
end
function ItemUse.useEscapeRope(session, bag, id)
if not can_escape(session) then
return false, "escape", not_the_time(session)
end
local Runtime = package.loaded["src.core.game3.runtime"]
local game = Runtime and Runtime._game
Bag.remove(bag, id, 1)
-- src/item_use.c:579 RemoveUsedItem
local t = RomText.box("gText_PlayerUsedVar2",
{ playerName = player_name(session), stringVars = { [2] = ItemsData.displayName(id) } })
local hx = session.healX or 8
local hy = session.healY or 5
local mapId = session.healMap or "FR_PLAYERS_HOUSE_1F"
-- pokefirered/src/field_effect.c:2126 SetWarpDestinationToEscapeWarp
local esc = session.escapeWarp
if type(esc) == "table" and type(esc.map) == "string" then
mapId = esc.map
hx = tonumber(esc.x) or hx
hy = tonumber(esc.y) or hy
end
local Warp = require("src.core.game3.warp")
-- pokefirered/src/item_use.c:642 Task_UseDigEscapeRopeOnField
local function leave()
Warp.startEscapeRope(game, mapId, hx, hy, function(m, x, y)
require("src.core.game3.field").respawnAtHeal({ fieldMove = true, warp = { map = m, x = x, y = y } })
end)
end
-- pokefirered/src/item_use.c:634 ItemUseOnFieldCB_EscapeRope
local function onField()
local okM, Message = pcall(require, "src.ui.game3.message")
if okM and Message and Message.show then
-- pokefirered/src/new_menu_helpers.c:641 DisplayItemMessageOnField
Message.show(t, { done = leave })
else
leave()
end
end
-- pokefirered/src/item_use.c:159 SetUpItemUseOnFieldCallback
if not ItemUse.setUpOnFieldCallback(onField) then onField() end
return true, "escape", t
end
-- pokefirered/src/item_use.c:253 FieldUseFunc_Bike
function ItemUse.useBike(session)
local Player = require("src.core.game3.player")
if Player.biking then
-- pokefirered/src/item_use.c:261: If already on bike, cannot dismount on cycling road
if Player.isOnCyclingRoad and Player.isOnCyclingRoad(session) then
return false, "bike", cant_dismount_bike_text()
end
-- Dismounting is always allowed elsewhere (even if indoors)
Player.biking = false
if session then session.biking = false end
local Runtime = package.loaded["src.core.game3.runtime"]
local curSession = Runtime and Runtime.getSession and Runtime.getSession()
if curSession then curSession.biking = false end
local game = Runtime and Runtime.getGame and Runtime.getGame()
if game and game.save then
game.save.biking = false
if game.save.position then game.save.position.biking = false end
end
require("src.core.game3.audio").bikeMusic(false)
return true, "bike", nil
else
-- pokefirered/src/overworld.c:948 Overworld_IsBikingAllowed: Mounting only allowed where biking is permitted
local biking = map_header_flag(session, "bikingAllowed")
if biking == nil then biking = is_outdoor(session) end
if not biking then
return false, "bike", not_the_time(session)
end
pcall(function()
local Audio = require("src.core.game3.audio")
local SE = require("src.core.game3.se_ids")
if Audio and Audio.playSe then Audio.playSe(SE.SE_BIKE_BELL) end
end)
Player.biking = true
if session then session.biking = true end
local Runtime = package.loaded["src.core.game3.runtime"]
local curSession = Runtime and Runtime.getSession and Runtime.getSession()
if curSession then curSession.biking = true end
local game = Runtime and Runtime.getGame and Runtime.getGame()
if game and game.save then
game.save.biking = true
if game.save.position then game.save.position.biking = true end
end
require("src.core.game3.audio").bikeMusic(true)
return true, "bike", nil
end
end
--- Check TM pre-flight compatibility and known moves matching retail FRLG.
-- Returns status ("knows" | "incompatible" | "ok"), messageText, moveId, moveName
-- src/party_menu.c:4762 ItemUseCB_TMHM
function ItemUse.checkTmPreflight(mon, tmId)
if not mon then return "none", no_pokemon_text(), nil, nil end
local moveId = Pokemon.moveFromTmItem(tmId)
if not moveId then
return "invalid", wont_have_effect(), nil, nil
end
local moveName = Pokemon.moveName(moveId)
local species = tonumber(mon.species or mon.speciesId)
if not Pokemon.canLearnTmItem(species, tmId) then
-- src/party_menu.c:4779
return "incompatible", mon_text("gText_PkmnCantLearnMove", mon, moveName), moveId, moveName
end
if Pokemon.knowsMove(mon, moveId) then
-- src/party_menu.c:4782
return "knows", mon_text("gText_PkmnAlreadyKnows", mon, moveName), moveId, moveName
end
if Pokemon.moveSlotCount(mon) >= 4 then
-- src/party_menu.c:4793
return "ok", mon_text("gText_PkmnNeedsToReplaceMove", mon, moveName), moveId, moveName
end
return "ok", nil, moveId, moveName
end
--- Teach TM/HM. partySlot required. Consumes TM (not HM).
function ItemUse.useTm(session, bag, id, partySlot)
local party = session and session.party
local mon = party and party[partySlot]
if not mon then
return false, "noparty", no_pokemon_text()
end
local status, preflightMsg, moveId, moveName = ItemUse.checkTmPreflight(mon, id)
if status ~= "ok" then
return false, status, preflightMsg
end
local monName = Pokemon.displayMonName(mon)
local isHm = ItemsData.isHm(id)
local consumed = false
local function finish_consume(learned)
if learned then
-- pokefirered/src/party_menu.c:4287
Pokemon.adjustFriendship(mon, Pokemon.FRIENDSHIP_EVENT_LEARN_TMHM,
{ mapSec = Pokemon.currentMapSec(session) })
require("src.core.game3.quest_log_recorder").event(session,
isHm and "MonLearnedMoveFromHM" or "MonLearnedMoveFromTM",{monName,moveName})
end
if learned and not isHm and not consumed then
Bag.remove(bag, id, 1)
consumed = true
end
end
if Pokemon.moveSlotCount(mon) < 4 then
local ok = Pokemon.teachMove(mon, moveId)
if ok then
finish_consume(true)
-- src/party_menu.c:4817
return true, "tm", mon_text("gText_PkmnLearnedMove3", mon, moveName)
end
end
local LearnMove = require("src.core.game3.battle.learn_move")
LearnMove.begin({
mon = mon,
moveId = moveId,
displayName = monName,
headless = true,
onDone = function(learned)
finish_consume(learned)
end,
})
if Pokemon.moveSlotCount(mon) >= 4 then
-- src/party_menu.c:4793
return false, "full", mon_text("gText_PkmnNeedsToReplaceMove", mon, moveName)
end
return true, "tm", mon_text("gText_PkmnLearnedMove3", mon, moveName)
end
--- Check if using this item requires selecting a party Pokémon target.
function ItemUse.needsPartyTarget(id)
if not id then return false end
local info = ItemsData.info(id)
if not info then return false end
local use = ItemsData.fieldUseKind(id)
if use == "heal" or use == "status" or use == "revive" or use == "tm"
or use == "pp" or use == "level" or use == "evo" or use == "vitamin" then
return true
end
if info.pocket == "TM_CASE" then return true end
return false
end
-- pokefirered/src/party_menu.c:5018
function ItemUse.levelUpEvent(mon, level)
if not ModRuntime.wants("pokemon.level_up") then return end
local G3 = require("src.mods.Gen3Compat")
local learnable, learnableIds = {}, {}
for _, mv in ipairs(Pokemon.movesLearnedAt(tonumber(mon.species or mon.speciesId), level)) do
learnable[#learnable + 1] = G3.moveName(mv)
learnableIds[#learnableIds + 1] = mv
end
ModRuntime.emit("pokemon.level_up", {
mon = mon, level = level, prevLevel = level - 1,
learnable = learnable, learnableIds = learnableIds, via = "item",
})
end
function ItemUse.useRareCandy(session, mon)
if not mon then return false, "none", no_pokemon_text() end
local lvl = tonumber(mon.level) or 1
local hp = tonumber(mon.hp) or 0
if lvl >= 100 or hp <= 0 then
return false, "no_effect", wont_have_effect()
end
local oldMax = tonumber(mon.maxHp) or tonumber(mon.maxhp) or 1
local oldHp = hp
mon.level = lvl + 1
Pokemon.applyStats(mon)
local newMax = tonumber(mon.maxHp) or tonumber(mon.maxhp) or oldMax
mon.hp = math.min(newMax, oldHp + math.max(0, newMax - oldMax))
-- pokefirered/src/data/pokemon/item_effects.h:200 sItemEffect_RareCandy
Pokemon.itemFriendship(mon, Pokemon.VITAMIN_FRIENDSHIP_CHANGE,
{ mapSec = Pokemon.currentMapSec(session) })
ItemUse.levelUpEvent(mon, mon.level)
-- src/party_menu.c:5061
local t = mon_text("gText_PkmnElevatedToLvVar2", mon, tostring(mon.level))
return true, "level", t
end
-- pokefirered/src/data/pokemon/item_effects.h:168
local VITAMIN_STAT = {
[63] = "hp", [64] = "atk", [65] = "def", [66] = "spe", [67] = "spa", [70] = "spd",
}
local VITAMIN_ADD_EV = 10
-- src/party_menu.c:4369
local VITAMIN_STAT_TEXT = {
hp = "gText_ItemEffect_HP", atk = "gText_ItemEffect_Attack", def = "gText_ItemEffect_Defense",
spe = "gText_ItemEffect_Speed", spa = "gText_ItemEffect_SpAtk", spd = "gText_ItemEffect_SpDef",
}
function ItemUse.useVitamin(session, mon, itemId)
if not mon then return false, "none", no_pokemon_text() end
local num = ItemsData.toNumericId(itemId) or tonumber(itemId)
local key = VITAMIN_STAT[num]
if not key then
return false, "no_effect", wont_have_effect()
end
-- pokefirered/src/party_menu.c:4405 NotUsingHPEVItemOnShedinja
if key == "hp" and (tonumber(mon.species or mon.speciesId) or 0) == 303 then
return false, "no_effect", wont_have_effect()
end
local gained = Pokemon.raiseEvFromItem(mon, key, VITAMIN_ADD_EV)
if not gained or gained <= 0 then
return false, "no_effect", wont_have_effect()
end
Pokemon.itemFriendship(mon, Pokemon.VITAMIN_FRIENDSHIP_CHANGE,
{ mapSec = Pokemon.currentMapSec(session) })
local t = mon_text("gText_PkmnBaseVar2StatIncreased", mon, RomText.plain(VITAMIN_STAT_TEXT[key]))
return true, "vitamin", t
end
-- pokefirered/include/constants/item_effects.h:34
local ITEM4_HEAL_PP_ALL, ITEM4_HEAL_PP_ONE, ITEM4_PP_UP = 0x08, 0x10, 0x20
local ITEM5_PP_MAX = 0x10
local function s8(v)
v = tonumber(v) or 0
if v >= 0x80 then return v - 0x100 end
return v
end
-- pokefirered/src/pokemon.c:4001
local function pp_effect(id)
local info = ItemsData.info(id)
local e = info and info.effect
if type(e) ~= "table" then return nil end
local e4, e5 = tonumber(e[5]) or 0, tonumber(e[6]) or 0
local idx = 7
for b = 0, 2 do
if bit.band(e4, bit.lshift(1, b)) ~= 0 then idx = idx + 1 end
end
local r = {
up = bit.band(e4, ITEM4_PP_UP) ~= 0,
max = bit.band(e5, ITEM5_PP_MAX) ~= 0,
}
if bit.band(e4, ITEM4_HEAL_PP_ALL) ~= 0 then
r.heal = tonumber(e[idx]) or 0
r.one = bit.band(e4, ITEM4_HEAL_PP_ONE) ~= 0
idx = idx + 1
end
if not (r.up or r.max or r.heal) then return nil end
for b = 0, 3 do
if bit.band(e5, bit.lshift(1, b)) ~= 0 then idx = idx + 1 end
end
if bit.band(e5, 0xE0) ~= 0 then
r.friendship = {}
for k, b in ipairs({ 0x20, 0x40, 0x80 }) do
if bit.band(e5, b) ~= 0 then
r.friendship[k] = s8(e[idx])
idx = idx + 1
end
end
end
return r
end
local function has_move(mon, s)
local m = Pokemon.moveIdAt(mon, s)
return m ~= nil and m > 0
end
-- pokefirered/src/pokemon.c:3898
local function pp_with_bonus(mon, s, n)
local base = tonumber(Pokemon.movePp(Pokemon.moveIdAt(mon, s))) or 0
return base + math.floor(base * 20 * n / 100)
end
local function pp_bonus(mon, s)
if type(mon.ppBonusesPacked) == "number" then
return bit.band(bit.rshift(mon.ppBonusesPacked, (s - 1) * 2), 3)
end
local m = tonumber(mon.maxPp and mon.maxPp[s])
if not m then return 0 end
local best = 0
for n = 0, 3 do
local v = pp_with_bonus(mon, s, n)
if v == m then return n end
if v <= m then best = n end
end
return best
end
-- pokefirered/src/pokemon.c:4202, :4344, :4463
local function pp_plan(mon, id, moveSlot)
local e = pp_effect(id)
if not e or not mon then return nil, nil end
local out = {}
if e.up or e.max then
local s = tonumber(moveSlot)
if s and has_move(mon, s) then
local n = pp_bonus(mon, s)
local cur = pp_with_bonus(mon, s, n)
local nn
if e.up and n < 3 and cur > 4 then nn = n + 1 end
if e.max and n < 3 then nn = 3 end
if nn then
local newMax = pp_with_bonus(mon, s, nn)
out[#out + 1] = {
slot = s, bonus = nn, max = newMax,
pp = (tonumber(mon.pp and mon.pp[s]) or 0) + newMax - cur,
}
end
end
elseif e.heal then
for s = 1, 4 do
if (not e.one or s == tonumber(moveSlot)) and has_move(mon, s) then
local max = pp_with_bonus(mon, s, pp_bonus(mon, s))
local cur = tonumber(mon.pp and mon.pp[s]) or 0
if cur ~= max then
out[#out + 1] = { slot = s, pp = math.min(max, cur + e.heal) }
end
end
end
end
return out, e
end
-- pokefirered/src/party_menu.c:4591, :4709
function ItemUse.ppItemNeedsMove(id)
local e = pp_effect(id)
return e ~= nil and (e.one or e.up or e.max) and true or false
end
function ItemUse.ppItemBoosts(id)
local e = pp_effect(id)
return e ~= nil and (e.up or e.max) and true or false
end
-- pokefirered/src/pokemon.c:4529
function ItemUse.ppItemHasEffect(mon, id, moveSlot)
local plan = pp_plan(mon, id, moveSlot)
return plan ~= nil and #plan > 0
end
-- pokefirered/src/party_menu.c:4340
function ItemUse.ppItemText(mon, id, moveSlot)
if ItemUse.ppItemBoosts(id) then
-- src/party_menu.c:4667
return (RomText.box("gText_MovesPPIncreased",
{ stringVars = { Pokemon.moveName(Pokemon.moveIdAt(mon, moveSlot)) } }))
end
return (RomText.box("gText_PPWasRestored"))
end
function ItemUse.applyPpItem(mon, id, moveSlot, battler, session)
local plan, e = pp_plan(mon, id, moveSlot)
if not plan or #plan == 0 then return false end
mon.pp = mon.pp or {}
for _, c in ipairs(plan) do
mon.pp[c.slot] = c.pp
if c.max then
mon.maxPp = mon.maxPp or {}
mon.maxPp[c.slot] = c.max
if type(mon.ppBonusesPacked) == "number" then
local shift = (c.slot - 1) * 2
mon.ppBonusesPacked = bit.bor(bit.band(mon.ppBonusesPacked, bit.bnot(bit.lshift(3, shift))),
bit.lshift(c.bonus, shift))
end
end
-- pokefirered/src/pokemon.c:4366
local bMon = battler and battler.mon
if bMon and bMon ~= mon and not battler.transformed
and (not battler.permanentSlots or battler.permanentSlots[c.slot]) then
bMon.pp = bMon.pp or {}
bMon.pp[c.slot] = c.pp
end
end
-- pokefirered/src/pokemon.c:3976
if e.friendship then
Pokemon.itemFriendship(mon, e.friendship, { mapSec = Pokemon.currentMapSec(session) })
end
return true
end
function ItemUse.useEvolutionStone(session, mon, itemId, bag)
local Evolution = require("src.core.game3.evolution")
local target = Evolution.itemTarget and Evolution.itemTarget(mon, itemId, session)
if not target then
return false, "no_evo", wont_have_effect()
end
local oldName = Pokemon.displayMonName(mon)
local newName = Pokemon.name(target)
local okEv, EvolutionScene = pcall(require, "src.ui.game3.evolution_scene")
if okEv and EvolutionScene and EvolutionScene.start and love and love.graphics then
local Audio = require("src.core.game3.audio")
EvolutionScene.start(mon, target, {
canStop = false,
session = session,
bag = bag,
savedSong = Audio._mapSong,
via = "item",
})
return true, "evo", nil
else
Evolution.apply(mon, target, session, bag, "item")
-- src/evolution_scene.c:775
local t = RomText.box("gText_CongratsPkmnEvolved", { stringVars = { oldName, newName } })
return true, "evo", t
end
end
-- pokefirered/include/constants/items.h:273
local ROD_ITEMS = { [262] = true, [263] = true, [264] = true }
local ITEM_BLACK_FLUTE = 42
local ITEM_WHITE_FLUTE = 43
local ITEM_POKE_FLUTE = 350
-- pokefirered/include/constants/items.h:432 ITEM_BICYCLE
local ITEM_BICYCLE = 360
-- pokefirered/include/constants/items.h:438 ITEM_TEACHY_TV
local ITEM_TEACHY_TV = 366
-- pokefirered/include/constants/items.h:435 ITEM_FAME_CHECKER
local ITEM_FAME_CHECKER = 363
local ITEM_AWAKENING = 17
-- pokefirered/include/constants/flags.h:1330
local FLAG_SYS_WHITE_FLUTE_ACTIVE = 0x803
local FLAG_SYS_BLACK_FLUTE_ACTIVE = 0x804
-- pokefirered/include/constants/songs.h:114
local SE_GLASS_FLUTE = 110
-- pokefirered/include/constants/songs.h:346 MUS_POKE_FLUTE
local MUS_POKE_FLUTE = 338
local function sys_flag(session, flagId, value)
local Space = package.loaded["src.core.game3.scripting.space"]
local Flags = require("src.core.game3.scripting.flags")
if Space and Space.store then
Flags.setFlag(Space.store, Space.vm and Space.vm.ctx or nil, flagId, value)
end
if session then
session.flags = session.flags or {}
session.flags[flagId] = value or nil
end
end
local function play_se(id)
pcall(function()
local Audio = require("src.core.game3.audio")
if Audio and Audio.playSe then Audio.playSe(id) end
end)
end
-- pokefirered/src/item_use.c:159 SetUpItemUseOnFieldCallback
function ItemUse.exitMenusToField()
local closed = false
local BagMenu = package.loaded["src.ui.game3.bag_menu"]
if BagMenu and BagMenu.isOpen and BagMenu.isOpen() and BagMenu.close then
BagMenu.close()
closed = true
end
local StartMenu = package.loaded["src.ui.game3.start_menu"]
if StartMenu and StartMenu.isOpen and StartMenu.isOpen() then
StartMenu.open = false
StartMenu._onClose = nil
local okS, Stack = pcall(require, "src.ui.game3.stack")
if okS and Stack and Stack.pop then Stack.pop("start") end
closed = true
end
if closed then
local okF, Fade = pcall(require, "src.ui.game3.fade")
if okF and Fade and Fade.begin and Fade.MODE then
Fade.begin(Fade.MODE.FROM_BLACK, 1)
end
end
return closed
end
-- pokefirered/src/item_use.c:159 SetUpItemUseOnFieldCallback
function ItemUse.setUpOnFieldCallback(cb)
local BagMenu = package.loaded["src.ui.game3.bag_menu"]
if not (BagMenu and BagMenu.isOpen and BagMenu.isOpen()) then return false end
ItemUse._onFieldCB = cb
return true
end
-- pokefirered/src/item_use.c:176 Task_WaitFadeIn_CallItemUseOnFieldCB
function ItemUse.runOnFieldCallback()
local cb = ItemUse._onFieldCB
ItemUse._onFieldCB = nil
if cb then cb() end
return cb ~= nil
end
-- pokefirered/src/item_use.c:182 DisplayItemMessageInCurrentContext
function ItemUse.showFieldMessage(text, onDone)
if not text then return false end
-- pokefirered/src/item_menu.c:1018 DisplayItemMessageInBag
local BagMenu = package.loaded["src.ui.game3.bag_menu"]
if BagMenu and BagMenu.isOpen and BagMenu.isOpen() and BagMenu.showMessage then
BagMenu.showMessage(text, onDone)
return true
end
-- pokefirered/src/new_menu_helpers.c:641 DisplayItemMessageOnField
local okM, Message = pcall(require, "src.ui.game3.message")
if not (okM and Message and Message.show) then return false end
Message.show(text, { done = onDone })
return true
end
-- pokefirered/src/item_use.c:296 CanFish
function ItemUse.canFish()
local P = package.loaded["src.core.game3.player"] or require("src.core.game3.player")
local Collision = require("src.core.game3.collision")
local FieldMoves = require("src.core.game3.field_moves")
local DELTA = { up = { 0, -1 }, down = { 0, 1 }, left = { -1, 0 }, right = { 1, 0 } }
local d = DELTA[P.facing or "down"] or DELTA.down
local fx, fy = P.cellX + d[1], P.cellY + d[2]
local beh = Collision.behavior and Collision.behavior(fx, fy)
if FieldMoves.isWaterfallBehavior(beh) then return false end
if not P.surfing then
-- pokefirered/src/field_player_avatar.c:1209 IsPlayerFacingSurfableFishableWater
return (Collision.isWater and Collision.isWater(fx, fy)) == true
end
return (Collision.isSurfable and Collision.isSurfable(beh)) == true
and Collision.isWater(fx, fy) == true
end
-- pokefirered/src/item_use.c:286 FieldUseFunc_Rod
function ItemUse.useRod(session, id)
if not ItemUse.canFish() then
-- pokefirered/src/item_use.c:294 PrintNotTheTimeToUseThat
local text = not_the_time(session)
return false, "rod", text
end
local info = ItemsData.info(id)
local Field = require("src.core.game3.field")
-- pokefirered/src/item_use.c:326 ItemUseOnFieldCB_Rod
Field.startFishing(tonumber(info and info.secondaryId) or 0)
return true, "rod", nil
end
-- pokefirered/src/item_use.c:359 FieldUseFunc_PokeFlute
function ItemUse.usePokeFlute(session)
local woke = false
for _, mon in ipairs((session and session.party) or {}) do
local isEgg = mon.isEgg or (type(mon.egg) == "boolean" and mon.egg)
if not isEgg and ItemUse.clearStatus(mon, ITEM_AWAKENING) then woke = true end
end
if not woke then
-- src/item_use.c:381
return true, "flute", (RomText.box("gText_PlayedPokeFluteCatchy"))
end
pcall(function()
local Audio = require("src.core.game3.audio")
if Audio and Audio.playFanfare then Audio.playFanfare(MUS_POKE_FLUTE) end
end)
-- src/item_use.c:374, :398
local text = RomText.box("gText_PlayedPokeFlute") .. "\f" .. RomText.box("gText_PokeFluteAwakenedMon")
return true, "flute", text
end
-- pokefirered/src/item_use.c:582 FieldUseFunc_BlackWhiteFlute
function ItemUse.useBlackWhiteFlute(session, num)
local ctx = {
playerName = tostring((session and (session.name or session.playerName)) or ""),
stringVars = { [2] = ItemsData.displayName(num) },
}
local text
if num == ITEM_WHITE_FLUTE then
sys_flag(session, FLAG_SYS_WHITE_FLUTE_ACTIVE, true)
sys_flag(session, FLAG_SYS_BLACK_FLUTE_ACTIVE, false)
-- pokefirered/src/item_use.c:590
text = RomText.box("gText_UsedVar2WildLured", ctx)
else
sys_flag(session, FLAG_SYS_BLACK_FLUTE_ACTIVE, true)
sys_flag(session, FLAG_SYS_WHITE_FLUTE_ACTIVE, false)
-- pokefirered/src/item_use.c:599
text = RomText.box("gText_UsedVar2WildRepelled", ctx)
end
return true, "black_white_flute", text
end
-- pokefirered/src/item_use.c:605 Task_UsedBlackWhiteFlute
ItemUse.BLACK_WHITE_FLUTE_DELAY = 8
function ItemUse.playBlackWhiteFlute()
play_se(SE_GLASS_FLUTE)
end
--- Try field use. partySlot optional for heal/status/revive/tm/give.
-- Returns ok, reason, messageText
local function useField(session, bag, id, partySlot, moveSlot)
local info = ItemsData.info(id)
if not info then return false, "unknown", Strings("Unknown item.") end
local use = ItemsData.fieldUseKind(id)
if use == "battle" then
-- src/item_use.c:902 FieldUseFunc_OakStopsYou
return false, "battle", not_the_time(session)
end
if use == "map" then
local RegionMap = require("src.ui.game3.region_map")
RegionMap.show({ session = session })
return true, "map", nil
end
if use == "bike" then
return ItemUse.useBike(session)
end
-- pokefirered/src/item_use.c:337 FieldUseFunc_CoinCase
if use == "coin_case" then
local coins = Bag.Coins and Bag.Coins.get and Bag.Coins.get(session) or 0
-- src/item_use.c:339
return true, "coin_case", (RomText.box("gText_CoinCase", { stringVars = { tostring(coins) } }))
end
-- pokefirered/src/item_use.c:348 FieldUseFunc_PowderJar
if use == "powder_jar" then
-- pokefirered/src/berry_powder.c:90 GetBerryPowder
local powder = math.floor(tonumber(session and session.berryPowder) or 0)
if powder < 0 then powder = 0 end
-- src/item_use.c:350
return true, "powder_jar", (RomText.box("gText_PowderQty", { stringVars = { tostring(powder) } }))
end
if use == "escape" then
return ItemUse.useEscapeRope(session, bag, id)
end
-- src/item_use.c:550 FieldUseFunc_Repel
if use == "repel" then
local vars = type(session.vars) == "table" and session.vars or nil
if (tonumber(session.repelSteps) or (vars and tonumber(vars[0x4020])) or 0) > 0 then
-- src/item_use.c:559
return false, "repel", (RomText.box("gText_RepelEffectsLingered"))
end
local steps = ItemsData.REPEL_STEPS[id]
or ItemsData.REPEL_STEPS[ItemsData.toNumericId(id) or -1]
or 100
session.repelSteps = steps
-- src/item_use.c:567 VarSet(VAR_REPEL_STEP_COUNT)
if vars then vars[0x4020] = steps end
Bag.remove(bag, id, 1)
-- src/item_use.c:579 RemoveUsedItem
local t = RomText.box("gText_PlayerUsedVar2",
{ playerName = player_name(session), stringVars = { [2] = ItemsData.displayName(id) } })
return true, "repel", t
end
if use == "vs_seeker" or id == ItemsData.ITEM_VS_SEEKER or id == "VS_SEEKER"
or ItemsData.toNumericId(id) == ItemsData.ITEM_VS_SEEKER then
if not Capabilities.gate(session, "vs_seeker") then
return false, "vs_seeker", nil
end
local VsSeeker = require("src.core.game3.vs_seeker")
if not VsSeeker.canUseHere(session) then
return false, "vs_seeker", VsSeeker.notTimeText(session)
end
return true, "vs_seeker", nil
end
if use == "itemfinder" or id == ItemsData.ITEM_ITEMFINDER or id == "ITEMFINDER"
or ItemsData.toNumericId(id) == ItemsData.ITEM_ITEMFINDER then
local Field = require("src.core.game3.field")
local ok, kind, text = Field.useItemfinder(session)
return ok, kind or "itemfinder", text
end
if id == ItemsData.ITEM_TM_CASE or id == "TM_CASE"
or ItemsData.toNumericId(id) == ItemsData.ITEM_TM_CASE then
local TmCase = require("src.ui.game3.tm_case")
TmCase.show(session, bag)
return true, "tm_case", nil
end
if id == ItemsData.ITEM_BERRY_POUCH or id == "BERRY_POUCH"
or ItemsData.toNumericId(id) == ItemsData.ITEM_BERRY_POUCH then
local BerryPouch = require("src.ui.game3.berry_pouch")
BerryPouch.show(session, bag)
return true, "berry_pouch", nil
end
-- pokefirered/src/item_use.c:518 FieldUseFunc_TeachyTv
if id == ITEM_TEACHY_TV or id == "TEACHY_TV"
or ItemsData.toNumericId(id) == ITEM_TEACHY_TV then
if not Capabilities.gate(session, "teachy_tv") then
return false, "teachy_tv", nil
end
local TeachyTv = require("src.core.game3.teachy_tv")
TeachyTv.show(session, bag)
return true, "teachy_tv", nil
end
-- pokefirered/src/item_use.c:680 FieldUseFunc_FameChecker
if id == ITEM_FAME_CHECKER or id == "FAME_CHECKER"
or ItemsData.toNumericId(id) == ITEM_FAME_CHECKER then
if not Capabilities.gate(session, "fame_checker") then
return false, "fame_checker", nil
end
local FameCheckerUi = require("src.ui.game3.fame_checker")
-- pokefirered/src/item_use.c:696 UseFameCheckerFromBag
local okBag, BagMenu = pcall(require, "src.ui.game3.bag_menu")
local fromBag = okBag and BagMenu and BagMenu.open and true or false
FameCheckerUi.show(session, { fromBag = fromBag })
return true, "fame_checker", nil
end
do
local num = ItemsData.toNumericId(id) or tonumber(id)
if ROD_ITEMS[num] then
return ItemUse.useRod(session, id)
end
if num == ITEM_POKE_FLUTE then
return ItemUse.usePokeFlute(session)
end
if use == "black_white_flute" then
return ItemUse.useBlackWhiteFlute(session, num)
end
-- pokefirered/src/item_use.c:253 FieldUseFunc_Bike
if num == ITEM_BICYCLE then
return ItemUse.useBike(session)
end
end
if use == "key" or use == "rod" or use == "berry" or use == "mail"
or use == "flute" or use == "none" then
return false, use, not_the_time(session)
end
if use == "heal" or use == "status" or use == "revive" or use == "tm"
or use == "pp" or use == "level" or use == "evo" or use == "vitamin" then
local party = session and session.party
if not party or #party < 1 then
return false, "noparty", no_pokemon_text()
end
if not partySlot then
return false, "need_slot", Strings("Select a POKéMON.")
end
local mon = party[partySlot]
if not mon then
return false, "noparty", no_pokemon_text()
end
local ok = false
local text = nil
local _
local num = ItemsData.toNumericId(id) or tonumber(id)
if use == "tm" then
return ItemUse.useTm(session, bag, id, partySlot)
elseif use == "evo" then
ok, _, text = ItemUse.useEvolutionStone(session, mon, id, bag)
elseif use == "level" then
ok, _, text = ItemUse.useRareCandy(session, mon)
-- pokefirered/src/pokemon.c:4258
elseif use == "revive" or num == ITEM_REVIVAL_HERB then
if num == 45 then -- Sacred Ash
-- src/party_menu.c:5280 Task_SacredAshDisplayHPRestored
local pages = {}
for _, m in ipairs(party) do
local before = tonumber(m.hp) or 0
if ItemUse.revive(m, true) then
pages[#pages + 1] = hp_restored_text(m, (tonumber(m.hp) or 0) - before)
end
end
ok = #pages > 0
if ok then text = table.concat(pages, "\f") end
else
local max = num == 25 or num == ITEM_REVIVAL_HERB or tostring(id) == "MAX_REVIVE"
local restored
ok, restored = ItemUse.revive(mon, max)
if ok then text = hp_restored_text(mon, restored) end
end
elseif use == "status" then
local stOk, cured = ItemUse.clearStatus(mon, id)
ok = stOk
if ok then text = mon_text(CURED_TEXT[cured] or CURED_TEXT.status, mon) end
elseif use == "pp" then
if moveSlot == nil and ItemUse.ppItemNeedsMove(id) then
return false, "need_move", nil
end
moveSlot = moveSlot or 1
-- pokefirered/src/party_menu.c:4657
ok = ItemUse.applyPpItem(mon, id, moveSlot, nil, session)
if ok then text = ItemUse.ppItemText(mon, id, moveSlot) end
elseif use == "vitamin" then
ok, _, text = ItemUse.useVitamin(session, mon, id)
else
local healOk, restored = ItemUse.healMon(session, mon, id)
ok = healOk
if ok and restored and restored > 0 then
text = hp_restored_text(mon, restored)
elseif ok then
-- src/party_menu.c:4366
text = mon_text(CURED_TEXT.status, mon)
end
end
if ok then
-- pokefirered/src/pokemon.c:4481
if BITTER_MEDICINE_FRIENDSHIP[num] then
Pokemon.itemFriendship(mon, BITTER_MEDICINE_FRIENDSHIP[num],
{ mapSec = Pokemon.currentMapSec(session) })
end
-- pokefirered/src/party_menu.c:4498
if not ItemUse.isFlute(id) then Bag.remove(bag, id, 1) end
return true, use, text
end
return false, "noeffect", text or wont_have_effect()
end
return false, "none", not_the_time(session)
end
function ItemUse.useField(session,bag,id,partySlot,moveSlot)
local ok,kind,text
if ModRuntime.wantsHook("item.use") then
local Runtime=package.loaded["src.core.game3.runtime"]
ok,kind,text=ModRuntime.call("item.use",function(_,_,hid,hslot)
return useField(session,bag,hid,hslot,moveSlot)
end,Runtime and Runtime._game,nil,id,partySlot,bag)
else
ok,kind,text=useField(session,bag,id,partySlot,moveSlot)
end
if ok and kind~="tm" and kind~="tm_case" and kind~="berry_pouch" and kind~="vs_seeker" then
local Items=require("src.core.game3.items")
local Pokemon=require("src.core.game3.pokemon")
local mon=partySlot and session and session.party and session.party[partySlot]
require("src.core.game3.quest_log_recorder").event(session,
mon and "UsedItemOnMonAtThisLocation" or "UsedTheItem",
{Items.displayName(id),mon and Pokemon.displayMonName(mon)})
end
return ok,kind,text
end
return ItemUse