Files
gen1recomp/src/core/game3/pokemon.lua
T
bryanthaboi d920f7acdb clean up
2026-09-22 17:05:12 -04:00

1609 lines
52 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- Runtime FRLG species names / menu icons / types (extracted pack).
local CachePaths = require("src.core.game3.cache_paths")
local PokemonExtract = require("src.import.gba.pokemon_extract")
local Versions = require("src.import.gba.versions")
local ModRuntime = require("src.mods.Runtime")
local Strings = require("src.core.Strings")
local Pokemon = {}
Pokemon._cache = nil
Pokemon._names = nil
Pokemon._types = nil
Pokemon._national = nil
Pokemon._manifest = nil
Pokemon._byName = nil -- normalized host/FRLG name → internal SPECIES
Pokemon._icons = {} -- [species] = { image, w, h }
Pokemon._front = {} -- [species] = { image, w, h }
Pokemon._romBytes = nil -- cached full ROM string for lazy front-pic decode
Pokemon._stats = nil
Pokemon._abilities = nil
Pokemon._abilityNames = nil
Pokemon._speciesMeta = nil
Pokemon._moveNames = nil
-- The ROM's English move and ability names, copied at install before a mod
-- renames entries of _moveNames/_abilityNames in place. Anything keyed by
-- the English name (the summary's descriptions) reads these.
Pokemon._romMoveNames = nil
Pokemon._romAbilityNames = nil
Pokemon._learnsets = nil
Pokemon._eggMoves = nil
Pokemon._evolutions = nil
Pokemon._tmhm = nil
Pokemon._dex = nil
Pokemon._battleMoves = nil
Pokemon._logged = false
local ROOT = (CachePaths.CACHE_ROOT or "data/generated/gba") .. "/pokemon"
local function log(msg)
if Pokemon._logged then return end
Pokemon._logged = true
print("[game3/pokemon] " .. tostring(msg))
end
local function resolve_cache(cache)
if cache and cache.read then return cache end
local okD, Dataset = pcall(require, "src.core.game3.dataset")
if okD and Dataset and Dataset.cache then
local c = Dataset.cache()
if c then return c end
end
return {
read = function(_, rel)
local ok, CacheFs = pcall(require, "src.import.CacheFs")
if ok and CacheFs and CacheFs.readActive then
local data = CacheFs.readActive(rel)
if data then return data end
end
local f = io.open(rel, "rb") or io.open("data/generated/gba/" .. rel, "rb")
if f then
local data = f:read("*a")
f:close()
return data
end
return nil
end,
}
end
local function copy_names(names)
if type(names) ~= "table" then return nil end
local out = {}
for k, v in pairs(names) do out[k] = v end
return out
end
local pkLoadWarned = false
local function load_lua(cache, rel)
cache = resolve_cache(cache)
local src = cache:read(rel)
if not src then return nil end
local chunk = load(src, "@" .. rel, "t", {})
if not chunk then return nil end
local ok, t = pcall(chunk)
if ok then return t end
if not pkLoadWarned then
pkLoadWarned = true
print("[game3/pokemon] load failed for " .. tostring(rel) .. ": " .. tostring(t))
end
return nil
end
--- Normalize host id / FRLG display name for reverse lookup.
-- "KYOGRE" / "NIDORAN_F" / "FARFETCH'D" / "HO-OH" / "MR. MIME"
local function norm_key(s)
if s == nil then return nil end
s = tostring(s):upper()
-- Common host aliases before stripping.
s = s:gsub("NIDORAN_F", "NIDORANF"):gsub("NIDORAN_M", "NIDORANM")
s = s:gsub("MR_MIME", "MRMIME"):gsub("MIME_JR", "MIMEJR")
s = s:gsub("FARFETCH[_']?D", "FARFETCHD")
s = s:gsub("HO[_%-]?OH", "HOOH")
s = s:gsub("NIDORAN♀", "NIDORANF"):gsub("NIDORAN♂", "NIDORANM")
s = s:gsub("FARFETCH'D", "FARFETCHD")
s = s:gsub("MR%.%s*MIME", "MRMIME"):gsub("MR%.", "MR")
s = s:gsub("HO%-OH", "HOOH")
s = s:gsub("[^A-Z0-9]", "")
return s
end
local function build_name_index(names)
local by = {}
if type(names) ~= "table" then return by end
for id, name in pairs(names) do
if type(id) == "number" and type(name) == "string" and name ~= "" then
local k = norm_key(name)
if k and k ~= "" and not by[k] then
by[k] = id
end
-- Extra keys for gendered / punctuated FRLG glyphs.
if name:find("♀", 1, true) then
by["NIDORANF"] = by["NIDORANF"] or id
end
if name:find("♂", 1, true) then
by["NIDORANM"] = by["NIDORANM"] or id
end
end
end
return by
end
function Pokemon.install(cache)
Pokemon._cache = resolve_cache(cache)
Pokemon._names = nil
Pokemon._types = nil
Pokemon._national = nil
Pokemon._manifest = nil
Pokemon._byName = nil
Pokemon._stats = nil
Pokemon._abilities = nil
Pokemon._abilityNames = nil
Pokemon._speciesMeta = nil
Pokemon._moveNames = nil
Pokemon._romMoveNames = nil
Pokemon._romAbilityNames = nil
Pokemon._learnsets = nil
Pokemon._eggMoves = nil
Pokemon._evolutions = nil
Pokemon._tmhm = nil
Pokemon._dex = nil
Pokemon._battleMoves = nil
Pokemon._icons = {}
Pokemon._front = {}
Pokemon._romBytes = nil
Pokemon._logged = false
local root = (CachePaths.CACHE_ROOT or "data/generated/gba") .. "/pokemon"
local c = Pokemon._cache
Pokemon._manifest = load_lua(c, root .. "/manifest.lua")
Pokemon._names = load_lua(c, root .. "/names.lua")
Pokemon._types = load_lua(c, root .. "/types.lua")
Pokemon._national = load_lua(c, root .. "/national.lua")
Pokemon._stats = load_lua(c, root .. "/stats.lua")
Pokemon._abilities = load_lua(c, root .. "/abilities.lua")
Pokemon._abilityNames = load_lua(c, root .. "/ability_names.lua")
Pokemon._speciesMeta = load_lua(c, root .. "/meta.lua")
Pokemon._moveNames = load_lua(c, root .. "/move_names.lua")
Pokemon._romMoveNames = copy_names(Pokemon._moveNames)
Pokemon._romAbilityNames = copy_names(Pokemon._abilityNames)
Pokemon._learnsets = load_lua(c, root .. "/learnsets.lua")
Pokemon._eggMoves = load_lua(c, root .. "/egg_moves.lua")
Pokemon._evolutions = load_lua(c, root .. "/evolutions.lua")
Pokemon._tmhm = load_lua(c, root .. "/tmhm.lua")
Pokemon._dex = load_lua(c, root .. "/dex.lua")
local battlePack = load_lua(c, root .. "/battle_moves.lua")
Pokemon._battleMoves = battlePack and battlePack.moves or nil
Pokemon._byName = build_name_index(Pokemon._names)
if Pokemon._names then
log("species pack ready (" .. tostring(Pokemon._manifest and Pokemon._manifest.numSpecies) .. ")")
else
log("species pack missing — re-import FireRed ROM")
end
Pokemon._runReloadHooks()
end
Pokemon._reloadHooks = {}
function Pokemon.onReload(fn, key)
if type(fn) ~= "function" then return function() end end
local hooks = Pokemon._reloadHooks
for i = #hooks, 1, -1 do
local h = hooks[i]
if h.fn == fn or (key ~= nil and h.key == key) then
table.remove(hooks, i)
end
end
local entry = { fn = fn, key = key }
hooks[#hooks + 1] = entry
return function()
for i = #hooks, 1, -1 do
if hooks[i] == entry then table.remove(hooks, i) end
end
end
end
function Pokemon._runReloadHooks()
local snapshot = {}
for i, h in ipairs(Pokemon._reloadHooks) do snapshot[i] = h end
for _, h in ipairs(snapshot) do
local ok, err = pcall(h.fn, Pokemon)
if not ok then log("onReload callback failed: " .. tostring(err)) end
end
end
function Pokemon.invalidate()
Pokemon._icons = {}
Pokemon._front = {}
Pokemon._back = nil
Pokemon._romBytes = nil
Pokemon._names = nil
Pokemon._types = nil
Pokemon._national = nil
Pokemon._manifest = nil
Pokemon._installTried = nil
Pokemon._installWarned = nil
Pokemon._byName = nil
Pokemon._stats = nil
Pokemon._abilities = nil
Pokemon._abilityNames = nil
Pokemon._speciesMeta = nil
Pokemon._moveNames = nil
Pokemon._romMoveNames = nil
Pokemon._romAbilityNames = nil
Pokemon._learnsets = nil
Pokemon._eggMoves = nil
Pokemon._evolutions = nil
Pokemon._tmhm = nil
Pokemon._dex = nil
Pokemon._battleMoves = nil
Pokemon._logged = false
end
function Pokemon.ready()
if Pokemon._names then return true end
local cache = Pokemon._cache
return PokemonExtract.ready(cache, CachePaths.CACHE_ROOT)
or load_lua(cache, ROOT .. "/names.lua") ~= nil
end
--- Internal FRLG SPECIES id → display name.
function Pokemon.name(species)
species = tonumber(species)
if not species or species < 1 then return "?????" end
if not Pokemon._names then Pokemon.install(Pokemon._cache) end
local n = Pokemon._names and Pokemon._names[species]
if n and n ~= "" and n ~= "??????????" then return n end
return Strings("POKéMON %03d", species)
end
function Pokemon.keyName(species)
species = tonumber(species)
if not species or species < 1 then return nil end
if not Pokemon._names then Pokemon.install(Pokemon._cache) end
local n = Pokemon._names and Pokemon._names[species]
if type(n) ~= "string" or n == "" or n == "??????????" then return nil end
n = n:upper()
n = n:gsub("♀", "_F"):gsub("♂", "_M")
n = n:gsub("[%.']", "")
n = n:gsub("[%s%-]+", "_")
return n
end
function Pokemon.speciesFromName(name)
if name == nil then return nil end
if not Pokemon._byName then Pokemon.install(Pokemon._cache) end
local k = norm_key(name)
if not k then return nil end
return Pokemon._byName and Pokemon._byName[k]
end
--- National dex number → internal SPECIES (when pack present).
function Pokemon.speciesFromNational(nat)
nat = tonumber(nat)
if not nat or nat < 1 then return nil end
if not Pokemon._national then Pokemon.install(Pokemon._cache) end
local t = Pokemon._national
if t and t.toSpecies and t.toSpecies[nat] then
return t.toSpecies[nat]
end
if nat <= 251 then return nat end
return nil
end
function Pokemon.national(species)
species = tonumber(species)
if not species or species < 1 then return nil end
if not Pokemon._national then Pokemon.install(Pokemon._cache) end
local t = Pokemon._national
if t and t.toNational then return t.toNational[species] end
if species <= 251 then return species end
return nil
end
function Pokemon.types(species)
species = tonumber(species)
if not species then return { 0, 0 } end
if not Pokemon._types then Pokemon.install(Pokemon._cache) end
local t = Pokemon._types and Pokemon._types[species]
if t then return { t[1] or 0, t[2] or 0 } end
return { 0, 0 }
end
--- Base stats table { hp, atk, def, spe, spa, spd } or nil.
function Pokemon.stats(species)
species = tonumber(species)
if not species then return nil end
if not Pokemon._stats then Pokemon.install(Pokemon._cache) end
return Pokemon._stats and Pokemon._stats[species]
end
--- Ability ids { ability1, ability2 }.
function Pokemon.abilities(species)
species = tonumber(species)
if not species then return { 0, 0 } end
if not Pokemon._abilities then Pokemon.install(Pokemon._cache) end
local a = Pokemon._abilities and Pokemon._abilities[species]
if a then return { a[1] or 0, a[2] or 0 } end
return { 0, 0 }
end
function Pokemon.abilityName(abilityId)
abilityId = tonumber(abilityId)
if not abilityId or abilityId < 1 then return "-------" end
if not Pokemon._abilityNames then Pokemon.install(Pokemon._cache) end
local n = Pokemon._abilityNames and Pokemon._abilityNames[abilityId]
if n and n ~= "" then return n end
return Strings("ABILITY %d", abilityId)
end
function Pokemon.speciesMeta(species)
species = tonumber(species)
if not species then return nil end
if not Pokemon._speciesMeta then Pokemon.install(Pokemon._cache) end
return Pokemon._speciesMeta and Pokemon._speciesMeta[species]
end
--- ROM BaseStats.expYield (species meta from extract).
function Pokemon.expYield(species)
local meta = Pokemon.speciesMeta(species)
return (meta and tonumber(meta.expYield)) or 0
end
--- ROM BaseStats.growthRate — pret GROWTH_* index into gExperienceTables.
function Pokemon.growthRate(species)
local meta = Pokemon.speciesMeta(species)
return (meta and tonumber(meta.growthRate) or 0) % 6
end
-- Gen3 genderRatio specials (match pret constants/pokemon.h).
Pokemon.GENDER_MALE = 0x00
Pokemon.GENDER_FEMALE = 0xFE
Pokemon.GENDER_GENDERLESS = 0xFF
-- Nature → { atk, def, spe, spa, spd } deltas (+1 / −1 / 0).
local NATURE_DELTAS = {
[0] = { 0, 0, 0, 0, 0 }, -- Hardy
[1] = { 1, -1, 0, 0, 0 }, -- Lonely
[2] = { 1, 0, -1, 0, 0 }, -- Brave
[3] = { 1, 0, 0, -1, 0 }, -- Adamant
[4] = { 1, 0, 0, 0, -1 }, -- Naughty
[5] = { -1, 1, 0, 0, 0 }, -- Bold
[6] = { 0, 0, 0, 0, 0 }, -- Docile
[7] = { 0, 1, -1, 0, 0 }, -- Relaxed
[8] = { 0, 1, 0, -1, 0 }, -- Impish
[9] = { 0, 1, 0, 0, -1 }, -- Lax
[10] = { -1, 0, 1, 0, 0 }, -- Timid
[11] = { 0, -1, 1, 0, 0 }, -- Hasty
[12] = { 0, 0, 0, 0, 0 }, -- Serious
[13] = { 0, 0, 1, -1, 0 }, -- Jolly
[14] = { 0, 0, 1, 0, -1 }, -- Naive
[15] = { -1, 0, 0, 1, 0 }, -- Modest
[16] = { 0, -1, 0, 1, 0 }, -- Mild
[17] = { 0, 0, -1, 1, 0 }, -- Quiet
[18] = { 0, 0, 0, 0, 0 }, -- Bashful
[19] = { 0, 0, 0, 1, -1 }, -- Rash
[20] = { -1, 0, 0, 0, 1 }, -- Calm
[21] = { 0, -1, 0, 0, 1 }, -- Gentle
[22] = { 0, 0, -1, 0, 1 }, -- Sassy
[23] = { 0, 0, 0, -1, 1 }, -- Careful
[24] = { 0, 0, 0, 0, 0 }, -- Quirky
}
function Pokemon.natureId(personality)
return (tonumber(personality) or 0) % 25
end
function Pokemon.gender(species, personality)
species = tonumber(species)
personality = tonumber(personality) or 0
local meta = Pokemon.speciesMeta(species)
local ratio = meta and meta.genderRatio
if ratio == nil then return "U" end
if ratio == Pokemon.GENDER_MALE then return "M" end
if ratio == Pokemon.GENDER_FEMALE then return "F" end
if ratio == Pokemon.GENDER_GENDERLESS then return "U" end
if ratio > (personality % 256) then return "F" end
return "M"
end
--- Ability id for species + personality (ability1 vs ability2).
function Pokemon.abilityId(species, personality)
species = tonumber(species)
personality = tonumber(personality) or 0
local pair = Pokemon.abilities(species)
local a1, a2 = pair[1] or 0, pair[2] or 0
if a2 ~= 0 and (personality % 2) == 1 then
return a2
end
return a1
end
local function nature_mul(nature, statIndex)
-- statIndex: 1=atk 2=def 3=spe 4=spa 5=spd
local d = NATURE_DELTAS[nature % 25]
if not d then return 1 end
local delta = d[statIndex] or 0
if delta > 0 then return 1.1 end
if delta < 0 then return 0.9 end
return 1
end
--- Gen3 CalculateMonStats → { maxHp, attack, defense, speed, spAtk, spDef }.
function Pokemon.calcStats(species, level, ivs, evs, personality)
species = tonumber(species)
level = tonumber(level) or 1
ivs = ivs or {}
evs = evs or {}
local base = Pokemon.stats(species)
if not base then
return {
maxHp = 15 + level * 2,
attack = 10, defense = 10, speed = 10, spAtk = 10, spDef = 10,
}
end
local nature = Pokemon.natureId(personality)
local function iv(k) return tonumber(ivs[k]) or 0 end
local function ev(k) return tonumber(evs[k]) or 0 end
local maxHp
if species == 303 then -- pokefirered/src/pokemon.c:2124 SPECIES_SHEDINJA
maxHp = 1
else
maxHp = math.floor(((2 * base.hp + iv("hp") + math.floor(ev("hp") / 4)) * level) / 100)
+ level + 10
end
local function other(baseStat, ivKey, evKey, natureIdx)
local n = math.floor(((2 * baseStat + iv(ivKey) + math.floor(ev(evKey) / 4)) * level) / 100) + 5
return math.floor(n * nature_mul(nature, natureIdx))
end
return {
maxHp = maxHp,
attack = other(base.atk, "atk", "atk", 1),
defense = other(base.def, "def", "def", 2),
speed = other(base.spe, "spe", "spe", 3),
spAtk = other(base.spa, "spa", "spa", 4),
spDef = other(base.spd, "spd", "spd", 5),
}
end
--- Fill battle/display stats on an opaque mon (mutates and returns mon).
function Pokemon.applyStats(mon)
if type(mon) ~= "table" then return mon end
local species = tonumber(mon.species or mon.speciesId) or 1
local level = tonumber(mon.level) or 5
local ivs = mon.ivs or {}
local evs = mon.evs or {}
local personality = mon.personality or 0
local st = Pokemon.calcStats(species, level, ivs, evs, personality)
mon.maxHp = st.maxHp
if mon.hp == nil or mon.hp < 0 or mon.hp > st.maxHp then
mon.hp = st.maxHp
end
mon.attack = st.attack
mon.defense = st.defense
mon.speed = st.speed
mon.spAtk = st.spAtk
mon.spDef = st.spDef
-- Aliases used by some battle paths.
mon.atk = st.attack
mon.def = st.defense
mon.spe = st.speed
mon.spa = st.spAtk
mon.spd = st.spDef
return mon
end
-- pokefirered/include/constants/pokemon.h:215
Pokemon.FRIENDSHIP_EVENT_GROW_LEVEL = 0
Pokemon.FRIENDSHIP_EVENT_VITAMIN = 1
Pokemon.FRIENDSHIP_EVENT_BATTLE_ITEM = 2
Pokemon.FRIENDSHIP_EVENT_LEAGUE_BATTLE = 3
Pokemon.FRIENDSHIP_EVENT_LEARN_TMHM = 4
Pokemon.FRIENDSHIP_EVENT_WALKING = 5
Pokemon.FRIENDSHIP_EVENT_MASSAGE = 6
Pokemon.FRIENDSHIP_EVENT_FAINT_SMALL = 7
Pokemon.FRIENDSHIP_EVENT_FAINT_OUTSIDE_BATTLE = 8
Pokemon.FRIENDSHIP_EVENT_FAINT_LARGE = 9
-- pokefirered/include/constants/pokemon.h:226,233
Pokemon.MAX_FRIENDSHIP = 255
Pokemon.MAX_PER_STAT_EVS = 255
Pokemon.MAX_TOTAL_EVS = 510
Pokemon.EV_ITEM_RAISE_LIMIT = 100
Pokemon.SPECIES_EGG = 412
local ITEM_LUXURY_BALL = 11 -- pokefirered/include/constants/items.h:15
local HOLD_EFFECT_MACHO_BRACE = 24 -- pokefirered/include/constants/hold_effects.h:28
local HOLD_EFFECT_FRIENDSHIP_UP = 27 -- pokefirered/include/constants/hold_effects.h:31
-- pokefirered/src/pokemon.c:1618 sFriendshipEventDeltas
local FRIENDSHIP_DELTAS = {
[0] = { 5, 3, 2 },
[1] = { 5, 3, 2 },
[2] = { 1, 1, 0 },
[3] = { 3, 2, 1 },
[4] = { 1, 1, 0 },
[5] = { 1, 1, 1 },
[6] = { 3, 3, 3 },
[7] = { -1, -1, -1 },
[8] = { -5, -5, -10 },
[9] = { -5, -5, -10 },
}
-- pokefirered/include/constants/pokemon.h:166
local EV_KEYS = { "hp", "atk", "def", "spe", "spa", "spd" }
local EV_YIELD_KEYS = { "evHp", "evAtk", "evDef", "evSpe", "evSpa", "evSpd" }
Pokemon.EV_KEYS = EV_KEYS
-- pokefirered/include/pokemon.h:229
function Pokemon.baseFriendship(species)
local meta = Pokemon.speciesMeta(species)
local v = meta and tonumber(meta.friendship)
if v then return v end
return 70
end
-- pokefirered/include/pokemon.h:219
function Pokemon.evYield(species)
if type(species) == "table" then
species = tonumber(species.species or species.speciesId)
end
local meta = Pokemon.speciesMeta(species)
local out = {}
for i = 1, 6 do
out[EV_KEYS[i]] = (meta and tonumber(meta[EV_YIELD_KEYS[i]])) or 0
end
return out
end
-- pokefirered/src/pokemon.c:1815
function Pokemon.friendshipOf(mon)
if type(mon) ~= "table" then return 0 end
local v = tonumber(mon.friendship or mon.happiness)
if v then return v end
return Pokemon.baseFriendship(tonumber(mon.species or mon.speciesId))
end
function Pokemon.setFriendship(mon, value)
if type(mon) ~= "table" then return 0 end
value = math.floor(tonumber(value) or 0)
if value < 0 then value = 0 end
if value > Pokemon.MAX_FRIENDSHIP then value = Pokemon.MAX_FRIENDSHIP end
mon.friendship = value
mon.happiness = value
return value
end
function Pokemon.evsOf(mon)
if type(mon) ~= "table" then return {} end
local evs = mon.evs
if type(evs) ~= "table" then
evs = {}
mon.evs = evs
end
for i = 1, 6 do
local k = EV_KEYS[i]
evs[k] = math.max(0, math.min(Pokemon.MAX_PER_STAT_EVS, math.floor(tonumber(evs[k]) or 0)))
end
return evs
end
-- pokefirered/src/pokemon.c:5597 GetMonEVCount
function Pokemon.evCount(mon)
local evs = Pokemon.evsOf(mon)
local count = 0
for i = 1, 6 do
count = count + (tonumber(evs[EV_KEYS[i]]) or 0)
end
return count
end
local function hold_effect_of(mon)
local ok, HeldItems = pcall(require, "src.core.game3.battle.held_items")
if not ok or not HeldItems or not HeldItems.effectOf then return 0 end
local effect = HeldItems.effectOf(mon and (mon.item or mon.heldItem))
return tonumber(effect) or 0
end
-- pokefirered/src/pokemon.c:5630 CheckPartyPokerus
function Pokemon.hasPokerus(mon)
return (tonumber(mon and mon.pokerus) or 0) % 16 ~= 0
end
-- pokefirered/src/pokemon.c:5646 CheckPartyHasHadPokerus
function Pokemon.hasHadPokerus(mon)
return (tonumber(mon and mon.pokerus) or 0) ~= 0
end
function Pokemon.checkPartyPokerus(party, selection)
if type(party) ~= "table" then return 0 end
if not selection or selection == 0 then
return Pokemon.hasPokerus(party[1]) and 1 or 0
end
local retVal, curBit, index = 0, 1, 1
while selection ~= 0 do
if selection % 2 == 1 and Pokemon.hasPokerus(party[index]) then
retVal = retVal + curBit
end
index = index + 1
curBit = curBit * 2
selection = math.floor(selection / 2)
end
return retVal
end
function Pokemon.checkPartyHasHadPokerus(party, selection)
if type(party) ~= "table" then return 0 end
if not selection or selection == 0 then
return Pokemon.hasHadPokerus(party[1]) and 1 or 0
end
local retVal, curBit, index = 0, 1, 1
while selection ~= 0 do
if selection % 2 == 1 and Pokemon.hasHadPokerus(party[index]) then
retVal = retVal + curBit
end
index = index + 1
curBit = curBit * 2
selection = math.floor(selection / 2)
end
return retVal
end
-- pokefirered/src/pokemon.c:5612, :5676, :5682 (all stubbed in FRLG)
function Pokemon.randomlyGivePartyPokerus(_) end
function Pokemon.updatePartyPokerusTime(_) end
function Pokemon.partySpreadPokerus(_) end
-- pokefirered/src/pokemon.c:5512 MonGainEVs
function Pokemon.gainEVs(mon, defeatedSpecies)
if type(mon) ~= "table" then return nil end
local evs = Pokemon.evsOf(mon)
local cur, total = {}, 0
for i = 1, 6 do
cur[i] = tonumber(evs[EV_KEYS[i]]) or 0
total = total + cur[i]
end
local yield = Pokemon.evYield(defeatedSpecies)
local multiplier = Pokemon.hasHadPokerus(mon) and 2 or 1
local macho = hold_effect_of(mon) == HOLD_EFFECT_MACHO_BRACE
local gained = 0
for i = 1, 6 do
if total >= Pokemon.MAX_TOTAL_EVS then break end
local inc = (tonumber(yield[EV_KEYS[i]]) or 0) * multiplier
if macho then inc = inc * 2 end
if total + inc > Pokemon.MAX_TOTAL_EVS then
inc = Pokemon.MAX_TOTAL_EVS - total
end
if cur[i] + inc > Pokemon.MAX_PER_STAT_EVS then
inc = Pokemon.MAX_PER_STAT_EVS - cur[i]
end
cur[i] = cur[i] + inc
total = total + inc
gained = gained + inc
evs[EV_KEYS[i]] = cur[i]
end
return gained
end
-- pokefirered/src/overworld.c:1265 GetCurrentRegionMapSectionId
function Pokemon.currentMapSec(session)
session = session or {}
local sec = tonumber(session.regionMapSectionId or session.mapSec)
if sec then return sec end
local Runtime = package.loaded["src.core.game3.runtime"]
local game = Runtime and Runtime._game
local def = session.map and game and game.data and game.data.maps and game.data.maps[session.map]
return def and tonumber(def.regionMapSectionId) or nil
end
local function player_identity(player)
player = player or {}
local id = player.trainerId or player.id or player.playerId
local name = player.name or player.playerName or player.otName
if id == nil or name == nil then
local Runtime = package.loaded["src.core.game3.runtime"]
local ok, sess = pcall(function()
return Runtime and Runtime.getSession and Runtime.getSession()
end)
if ok and sess then
id = id or sess.trainerId or sess.id or sess.playerId
name = name or sess.name or sess.playerName
end
end
return tonumber(id), name
end
-- pokefirered/src/pokemon.c:5974 IsOtherTrainer
function Pokemon.isOtherTrainer(otId, otName, player)
local playerId, playerName = player_identity(player)
if playerId == nil then return false end
if tonumber(otId) ~= playerId then return true end
local mine = tostring(playerName or "")
local theirs = tostring(otName or "")
for i = 1, #theirs do
if theirs:sub(i, i) ~= mine:sub(i, i) then return true end
end
return false
end
-- pokefirered/src/pokemon.c:5965 IsTradedMon
function Pokemon.isTradedMon(mon, player)
if type(mon) ~= "table" or mon.otId == nil then return false end
return Pokemon.isOtherTrainer(mon.otId, mon.otName or mon.ot, player)
end
local function friendship_bonuses(mon, friendship, ctx)
if (tonumber(mon.pokeball or mon.ball) or 0) == ITEM_LUXURY_BALL then
friendship = friendship + 1
end
local sec = ctx and tonumber(ctx.mapSec)
local met = tonumber(mon.metLocation)
if sec and met and met == sec then
friendship = friendship + 1
end
return friendship
end
-- pokefirered/include/constants/trainers.h:267
Pokemon.LEAGUE_TRAINER_CLASSES = { [84] = true, [87] = true, [90] = true }
-- pokefirered/src/pokemon.c:5480
function Pokemon.isLeagueTrainerClass(trainerClass)
local cls = tonumber(trainerClass)
return (cls ~= nil) and Pokemon.LEAGUE_TRAINER_CLASSES[cls] == true
end
-- pokefirered/src/pokemon.c:5440 AdjustFriendship
function Pokemon.adjustFriendship(mon, event, ctx)
if type(mon) ~= "table" then return false end
ctx = ctx or {}
local species = tonumber(mon.species or mon.speciesId) or 0
if species == 0 or species == Pokemon.SPECIES_EGG or mon.isEgg or mon.egg then
return false
end
local deltas = FRIENDSHIP_DELTAS[tonumber(event)]
if not deltas then return false end
local friendship = Pokemon.friendshipOf(mon)
local tier = 1
if friendship >= 100 then tier = tier + 1 end
if friendship >= 200 then tier = tier + 1 end
if event == Pokemon.FRIENDSHIP_EVENT_WALKING then
local okR, Rng = pcall(require, "src.core.game3.rng")
if okR and Rng and Rng.Random and Rng.Random() % 2 == 1 then return false end
end
if event == Pokemon.FRIENDSHIP_EVENT_LEAGUE_BATTLE and not ctx.leagueBattle then
return false
end
local delta = deltas[tier]
if delta > 0 and hold_effect_of(mon) == HOLD_EFFECT_FRIENDSHIP_UP then
delta = math.floor(150 * delta / 100)
end
friendship = friendship + delta
if delta > 0 then
friendship = friendship_bonuses(mon, friendship, ctx)
end
Pokemon.setFriendship(mon, friendship)
return true
end
-- pokefirered/src/battle_util2.c:78 AdjustFriendshipOnBattleFaint
function Pokemon.adjustFriendshipOnBattleFaint(mon, faintedLevel, opposingLevel, ctx)
faintedLevel = tonumber(faintedLevel) or 1
opposingLevel = tonumber(opposingLevel) or 1
local event = Pokemon.FRIENDSHIP_EVENT_FAINT_SMALL
if opposingLevel > faintedLevel and opposingLevel - faintedLevel > 29 then
event = Pokemon.FRIENDSHIP_EVENT_FAINT_LARGE
end
return Pokemon.adjustFriendship(mon, event, ctx)
end
-- pokefirered/src/pokemon.c:3976 UPDATE_FRIENDSHIP_FROM_ITEM
function Pokemon.itemFriendship(mon, lowMidHigh, ctx)
if type(mon) ~= "table" or type(lowMidHigh) ~= "table" then return false end
local friendship = Pokemon.friendshipOf(mon)
local change
if friendship < 100 then
change = tonumber(lowMidHigh[1])
elseif friendship < 200 then
change = tonumber(lowMidHigh[2])
else
change = tonumber(lowMidHigh[3])
end
if not change or change == 0 then return false end
if change > 0 and hold_effect_of(mon) == HOLD_EFFECT_FRIENDSHIP_UP then
friendship = friendship + math.floor(150 * change / 100)
else
friendship = friendship + change
end
if change > 0 then
friendship = friendship_bonuses(mon, friendship, ctx)
end
Pokemon.setFriendship(mon, friendship)
return true
end
-- pokefirered/src/data/pokemon/item_effects.h:163 VITAMIN_FRIENDSHIP_CHANGE
Pokemon.VITAMIN_FRIENDSHIP_CHANGE = { 5, 3, 2 }
-- pokefirered/src/data/pokemon/item_effects.h:225 STAT_BOOST_FRIENDSHIP_CHANGE
Pokemon.STAT_BOOST_FRIENDSHIP_CHANGE = { 1, 1, 0 }
-- pokefirered/src/pokemon.c:4229
function Pokemon.raiseEvFromItem(mon, statKey, amount)
local evs = Pokemon.evsOf(mon)
if evs[statKey] == nil then return nil end
local evCount = Pokemon.evCount(mon)
if evCount >= Pokemon.MAX_TOTAL_EVS then return nil end
local data = tonumber(evs[statKey]) or 0
if data >= Pokemon.EV_ITEM_RAISE_LIMIT then return nil end
amount = math.floor(tonumber(amount) or 0)
local delta = amount
if data + amount > Pokemon.EV_ITEM_RAISE_LIMIT then
delta = Pokemon.EV_ITEM_RAISE_LIMIT - data
end
if evCount + delta > Pokemon.MAX_TOTAL_EVS then
delta = Pokemon.MAX_TOTAL_EVS - evCount
end
evs[statKey] = data + delta
-- pokefirered/src/pokemon.c:2155
local oldMaxHp = tonumber(mon.maxHp) or 0
local oldHp = tonumber(mon.hp) or 0
Pokemon.applyStats(mon)
local newMaxHp = tonumber(mon.maxHp) or oldMaxHp
if oldHp ~= 0 or oldMaxHp ~= 0 then
mon.hp = math.max(0, math.min(newMaxHp, oldHp + (newMaxHp - oldMaxHp)))
end
return delta
end
-- The ROM's English name for a move or ability number, whatever a mod renamed
-- it to; nil when the pack has none.
function Pokemon.romMoveName(num)
num = tonumber(num)
if not num then return nil end
if not Pokemon._moveNames then Pokemon.install(Pokemon._cache) end
return Pokemon._romMoveNames and Pokemon._romMoveNames[num]
end
function Pokemon.romAbilityName(abilityId)
abilityId = tonumber(abilityId)
if not abilityId then return nil end
if not Pokemon._abilityNames then Pokemon.install(Pokemon._cache) end
return Pokemon._romAbilityNames and Pokemon._romAbilityNames[abilityId]
end
function Pokemon.moveName(moveId)
if type(moveId) == "table" then
moveId = moveId.id or moveId.move or moveId.moveId or moveId.num or moveId.name or moveId[1]
end
local num = tonumber(moveId)
if not num and type(moveId) == "string" then
local Moves = package.loaded["src.core.game3.battle.moves"]
if Moves and Moves.numForName then
num = Moves.numForName(moveId)
end
if not num then
if moveId ~= "" and moveId ~= "-------" then
return tostring(moveId):gsub("_", " "):upper()
end
end
end
if not num or num < 1 then return "-------" end
if not Pokemon._moveNames then Pokemon.install(Pokemon._cache) end
local n = Pokemon._moveNames and Pokemon._moveNames[num]
if n and n ~= "" then return n end
return Strings("MOVE %d", num)
end
function Pokemon.learnset(species)
if type(species) == "table" then species = Pokemon.speciesOf(species) end
if type(species) == "string" then species = Pokemon.speciesFromName(species) or tonumber(species) end
species = tonumber(species)
if not species then return {} end
if not Pokemon._learnsets then Pokemon.install(Pokemon._cache) end
return (Pokemon._learnsets and Pokemon._learnsets[species]) or {}
end
--- Egg move ids for a species (FRLG gEggMoves), or nil when it has none.
--- Mirrors Pokemon.learnset's species coercion so mods can pass either form.
function Pokemon.eggMoves(species)
if type(species) == "table" then species = Pokemon.speciesOf(species) end
if type(species) == "string" then species = Pokemon.speciesFromName(species) or tonumber(species) end
species = tonumber(species)
if not species then return nil end
if not Pokemon._eggMoves then Pokemon.install(Pokemon._cache) end
local list = Pokemon._eggMoves and Pokemon._eggMoves[species]
if type(list) ~= "table" or #list == 0 then return nil end
return list
end
function Pokemon.evolutions(species)
if type(species) == "table" then species = Pokemon.speciesOf(species) end
if type(species) == "string" then species = Pokemon.speciesFromName(species) or tonumber(species) end
species = tonumber(species)
if not species then return {} end
if not Pokemon._evolutions then Pokemon.install(Pokemon._cache) end
return (Pokemon._evolutions and Pokemon._evolutions[species]) or {}
end
function Pokemon.dexEntry(species)
if type(species) == "table" then species = Pokemon.speciesOf(species) end
if type(species) == "string" then species = Pokemon.speciesFromName(species) or tonumber(species) end
species = tonumber(species)
if not species then return nil end
if not Pokemon._dex then Pokemon.install(Pokemon._cache) end
local nat = Pokemon.national(species)
if not nat or not Pokemon._dex then return nil end
return Pokemon._dex[nat]
end
function Pokemon.battleMove(moveId)
moveId = tonumber(moveId)
if not moveId then return nil end
if not Pokemon._battleMoves then Pokemon.install(Pokemon._cache) end
return Pokemon._battleMoves and Pokemon._battleMoves[moveId]
end
function Pokemon.movePp(moveId)
moveId = tonumber(moveId)
if not moveId or moveId < 1 then return 5 end
local row = Pokemon.battleMove(moveId)
return (row and tonumber(row.pp)) or 5
end
Pokemon.moveMaxPp = Pokemon.movePp
--- FRLG GiveBoxMonInitialMoveset: learn all ≤ level; if full, drop first; avoid duplicates.
function Pokemon.movesAtLevel(species, level)
if type(species) == "table" then species = Pokemon.speciesOf(species) end
if type(species) == "string" then species = Pokemon.speciesFromName(species) or tonumber(species) end
species = tonumber(species)
level = tonumber(level) or 1
local set = Pokemon.learnset(species)
local moves = {}
local pp = {}
local maxPp = {}
local function giveMove(moveId)
if not moveId or moveId <= 0 then return end
for i = 1, #moves do
if moves[i] == moveId then
return -- already knows this move (pret GiveMoveToBoxMon)
end
end
local mpp = Pokemon.movePp(moveId)
if #moves < 4 then
moves[#moves + 1] = moveId
pp[#pp + 1] = mpp
maxPp[#maxPp + 1] = mpp
else
table.remove(moves, 1)
table.remove(pp, 1)
table.remove(maxPp, 1)
moves[4] = moveId
pp[4] = mpp
maxPp[4] = mpp
end
end
for _, e in ipairs(set) do
local lv = e[1] or e.level or 0
local mv = tonumber(e[2] or e.move) or 0
if lv > level then
break
end
giveMove(mv)
end
if #moves == 0 then
giveMove(33) -- fallback to Tackle if learnset empty
end
return moves, pp, maxPp
end
--- Moves learned at exactly `level` (ROM learnset). Order preserved.
function Pokemon.movesLearnedAt(species, level)
if type(species) == "table" then species = Pokemon.speciesOf(species) end
if type(species) == "string" then species = Pokemon.speciesFromName(species) or tonumber(species) end
species = tonumber(species)
level = tonumber(level) or 0
local out = {}
if not species or level < 1 then return out end
for _, e in ipairs(Pokemon.learnset(species)) do
local lv = e[1] or e.level or 0
local mv = tonumber(e[2] or e.move) or 0
if lv == level and mv > 0 then
out[#out + 1] = mv
elseif lv > level then
break
end
end
return out
end
function Pokemon.moveIdAt(mon, slot)
if not mon or not mon.moves then return nil end
local entry = mon.moves[slot]
if type(entry) == "table" then return tonumber(entry.id or entry.move) end
return tonumber(entry)
end
function Pokemon.knowsMove(mon, moveId)
moveId = tonumber(moveId)
if not mon or not moveId then return false end
for i = 1, 4 do
if Pokemon.moveIdAt(mon, i) == moveId then return true end
end
return false
end
function Pokemon.moveSlotCount(mon)
local n = 0
for i = 1, 4 do
local id = Pokemon.moveIdAt(mon, i)
if id and id > 0 then n = n + 1 end
end
return n
end
-- FRLG HM move IDs (cannot forget).
local HM_MOVES = {
[15] = true, -- CUT
[19] = true, -- FLY
[57] = true, -- SURF
[70] = true, -- STRENGTH
[148] = true, -- FLASH
[249] = true, -- ROCK SMASH
[127] = true, -- WATERFALL
[291] = true, -- DIVE
}
function Pokemon.isHmMove(moveId)
return HM_MOVES[tonumber(moveId) or 0] == true
end
--- Item id 289 (TM01) … 346 (HM08) → battle move id via extracted sTMHMMoves.
function Pokemon.moveFromTmItem(itemId)
local num = tonumber(itemId)
if not num then
local ItemsData = require("src.core.game3.items_data")
num = ItemsData.toNumericId(itemId)
end
if not num or num < 289 or num > 346 then return nil end
if not Pokemon._tmhm then Pokemon.install(Pokemon._cache) end
local machines = Pokemon._tmhm and Pokemon._tmhm.machines
if not machines then return nil end
local tmIndex = num - 289 -- 0-based TM01
return tonumber(machines[tmIndex])
end
--- Can this species learn TM/HM machine index (0..57)?
function Pokemon.canLearnTmIndex(species, tmIndex)
species = tonumber(species)
tmIndex = tonumber(tmIndex)
if not species or not tmIndex or tmIndex < 0 or tmIndex > 57 then return false end
if not Pokemon._tmhm then Pokemon.install(Pokemon._cache) end
local row = Pokemon._tmhm and Pokemon._tmhm.learnsets and Pokemon._tmhm.learnsets[species]
if not row then return false end
local lo = tonumber(row.lo) or 0
local hi = tonumber(row.hi) or 0
if tmIndex < 32 then
return math.floor(lo / (2 ^ tmIndex)) % 2 == 1
end
return math.floor(hi / (2 ^ (tmIndex - 32))) % 2 == 1
end
function Pokemon.canLearnTmItem(species, itemId)
local num = tonumber(itemId)
if not num then
local ItemsData = require("src.core.game3.items_data")
num = ItemsData.toNumericId(itemId)
end
if not num or num < 289 or num > 346 then return false end
return Pokemon.canLearnTmIndex(species, num - 289)
end
local function move_max_pp(moveId)
local row = Pokemon.battleMove(moveId)
return (row and tonumber(row.pp)) or 5
end
--- Teach move into first empty slot. Returns true if taught.
function Pokemon.teachMove(mon, moveId)
moveId = tonumber(moveId)
if not mon or not moveId or moveId < 1 then return false end
if Pokemon.knowsMove(mon, moveId) then return false end
mon.moves = mon.moves or {}
mon.pp = mon.pp or {}
mon.maxPp = mon.maxPp or {}
for i = 1, 4 do
local id = Pokemon.moveIdAt(mon, i)
if not id or id == 0 then
mon.moves[i] = moveId
local max = move_max_pp(moveId)
mon.pp[i] = max
mon.maxPp[i] = max
-- pokefirered/src/pokemon.c:2208
if ModRuntime.wants("pokemon.move_learned") then
ModRuntime.emit("pokemon.move_learned", {
mon = mon, moveId = require("src.mods.Gen3Compat").moveName(moveId),
moveNum = moveId, slot = i,
})
end
return true, i
end
end
return false
end
--- Replace move at 1-based slot. Returns forgotten move id.
function Pokemon.replaceMove(mon, slot, newMoveId)
slot = tonumber(slot)
newMoveId = tonumber(newMoveId)
if not mon or not slot or slot < 1 or slot > 4 or not newMoveId then return nil end
local old = Pokemon.moveIdAt(mon, slot)
if Pokemon.isHmMove(old) then return nil, "hm" end
mon.moves = mon.moves or {}
mon.pp = mon.pp or {}
mon.maxPp = mon.maxPp or {}
mon.moves[slot] = newMoveId
local max = move_max_pp(newMoveId)
mon.pp[slot] = max
mon.maxPp[slot] = max
-- pokefirered/src/pokemon.c:2248
if ModRuntime.wants("pokemon.move_learned") then
local G3 = require("src.mods.Gen3Compat")
ModRuntime.emit("pokemon.move_learned", {
mon = mon, moveId = G3.moveName(newMoveId), moveNum = newMoveId, slot = slot,
forgotten = G3.moveName(old), forgottenNum = tonumber(old),
})
end
return old
end
-- An egg reads as the language's own EGG whatever its nickname holds: pret's
-- GetMonData(MON_DATA_NICKNAME) returns gText_EggNickname for any egg
-- (pokefirered/src/pokemon.c:3020). The stored nickname is only a placeholder
-- -- the cart's daycare writes タマゴ (daycare.c:1100), this engine "EGG".
local function eggName(mon)
if Pokemon.isEgg(mon) then return Strings("EGG") end
end
function Pokemon.displayMonName(mon)
if not mon then return "POKéMON" end
local egg = eggName(mon)
if egg then return egg end
local nick = mon.nickname
if type(nick) == "string" and nick ~= "" then return nick end
if mon.name and mon.name ~= "" then return mon.name end
local sp = Pokemon.speciesOf(mon)
return (sp and Pokemon.name(sp)) or "POKéMON"
end
--- Atomically swap two move slots on a Pokémon, keeping move ID, PP, and PP bonuses in sync.
--- Solves the PP Swap Trap.
function Pokemon.swapMoves(mon, slotA, slotB)
if not mon or not slotA or not slotB then return false end
slotA = tonumber(slotA)
slotB = tonumber(slotB)
if not slotA or not slotB or slotA < 1 or slotA > 4 or slotB < 1 or slotB > 4 then
return false
end
if slotA == slotB then return true end
-- 1. If mon.moves is an array of tables: { id = ..., pp = ..., ppBonuses = ... }
if type(mon.moves) == "table" then
local entryA = mon.moves[slotA]
local entryB = mon.moves[slotB]
mon.moves[slotA] = entryB
mon.moves[slotB] = entryA
end
-- 2. If parallel array mon.moveIds exists
if type(mon.moveIds) == "table" then
local idA = mon.moveIds[slotA]
mon.moveIds[slotA] = mon.moveIds[slotB]
mon.moveIds[slotB] = idA
end
-- 3. If parallel array mon.pp exists
if type(mon.pp) == "table" then
local ppA = mon.pp[slotA]
mon.pp[slotA] = mon.pp[slotB]
mon.pp[slotB] = ppA
end
-- 4. If parallel array mon.ppBonuses / mon.ppBonus / mon.ppUp exists
if type(mon.ppBonuses) == "table" then
local bA = mon.ppBonuses[slotA]
mon.ppBonuses[slotA] = mon.ppBonuses[slotB]
mon.ppBonuses[slotB] = bA
end
if type(mon.ppBonus) == "table" then
local bA = mon.ppBonus[slotA]
mon.ppBonus[slotA] = mon.ppBonus[slotB]
mon.ppBonus[slotB] = bA
end
if type(mon.ppUp) == "table" then
local bA = mon.ppUp[slotA]
mon.ppUp[slotA] = mon.ppUp[slotB]
mon.ppUp[slotB] = bA
end
-- 5. If PP bonuses are stored as packed bits (Gen 3 BoxMon / Pokemon: 2 bits per slot)
if type(mon.ppBonusesPacked) == "number" then
local packed = mon.ppBonusesPacked
local shiftA = (slotA - 1) * 2
local shiftB = (slotB - 1) * 2
local bonusA = bit.band(bit.rshift(packed, shiftA), 3)
local bonusB = bit.band(bit.rshift(packed, shiftB), 3)
packed = bit.band(packed, bit.bnot(bit.bor(bit.lshift(3, shiftA), bit.lshift(3, shiftB))))
packed = bit.bor(packed, bit.lshift(bonusA, shiftB), bit.lshift(bonusB, shiftA))
mon.ppBonusesPacked = packed
end
-- 6. If an active overlay exists on the mon (e.g. party menu overlay)
if type(mon.overlay) == "table" then
local ovA = mon.overlay[slotA]
mon.overlay[slotA] = mon.overlay[slotB]
mon.overlay[slotB] = ovA
end
return true
end
function Pokemon.isEgg(mon)
if not mon then return false end
return (mon.isEgg == true) or (mon.egg == true) or (mon.species == 412)
end
-- pokefirered/src/pokemon.c:3245 MON_DATA_SPECIES_OR_EGG: an egg's menu icon is
-- SPECIES_EGG's, not the species it will hatch into (party_menu.c:2655).
function Pokemon.speciesOrEgg(mon)
if Pokemon.isEgg(mon) then return Pokemon.SPECIES_EGG end
return Pokemon.speciesOf(mon)
end
local function read_rgba(species)
local cache = resolve_cache(Pokemon._cache)
local root = (CachePaths.CACHE_ROOT or "data/generated/gba") .. "/pokemon"
local rel = root .. "/icons/" .. species .. ".rgba"
local d = cache:read(rel)
if type(d) == "string" and #d > 0 then return d end
return nil
end
local function image_from_rgba(rgba, w, h)
if not (love and love.image and love.graphics) then return nil end
if not rgba or #rgba < w * h * 4 then return nil end
local ok, imageData = pcall(love.image.newImageData, w, h, "rgba8", rgba)
if not ok or not imageData then
imageData = love.image.newImageData(w, h)
local i = 1
for y = 0, h - 1 do
for x = 0, w - 1 do
imageData:setPixel(x, y,
(rgba:byte(i) or 0) / 255,
(rgba:byte(i + 1) or 0) / 255,
(rgba:byte(i + 2) or 0) / 255,
(rgba:byte(i + 3) or 0) / 255)
i = i + 4
end
end
end
local image = love.graphics.newImage(imageData)
if image.setFilter then image:setFilter("nearest", "nearest") end
return image
end
--- Love Image for menu icon, or nil.
function Pokemon.icon(species)
species = tonumber(species)
if not species or species < 1 then return nil end
if Pokemon._icons[species] then return Pokemon._icons[species] end
local w = (Pokemon._manifest and Pokemon._manifest.iconW) or Versions.MON_ICON_W or 32
local h = (Pokemon._manifest and Pokemon._manifest.iconH) or Versions.MON_ICON_H or 32
local rgba = read_rgba(species)
if not rgba then return nil end
local actualH = (#rgba >= w * (h * 2) * 4) and (h * 2) or h
local image = image_from_rgba(rgba, w, actualH)
if not image then return nil end
local frames = math.max(1, math.floor(actualH / h))
local quads = {}
for f = 0, frames - 1 do
quads[f] = love.graphics.newQuad(0, f * h, w, h, w, actualH)
end
local entry = { image = image, w = w, h = h, sheetH = actualH, frames = frames, quads = quads }
Pokemon._icons[species] = entry
return entry
end
local function bgr555_to_rgb8(c)
c = (tonumber(c) or 0) % 32768
local r5 = c % 32
local g5 = math.floor(c / 32) % 32
local b5 = math.floor(c / 1024) % 32
return math.floor(r5 * 255 / 31 + 0.5),
math.floor(g5 * 255 / 31 + 0.5),
math.floor(b5 * 255 / 31 + 0.5)
end
local function load_rom_bytes()
if Pokemon._romBytes then return Pokemon._romBytes end
local candidates = {
"1636 - Pokemon Fire Red (U)(Squirrels).gba",
"firered.gba",
"Pokemon FireRed.gba",
}
for _, path in ipairs(candidates) do
local f = io.open(path, "rb")
if f then
local data = f:read("*a")
f:close()
if data and #data >= 0x1000000 then
Pokemon._romBytes = data
return data
end
end
if love and love.filesystem and love.filesystem.read then
local ok, data = pcall(love.filesystem.read, path)
if ok and data and #data >= 0x1000000 then
Pokemon._romBytes = data
return data
end
end
end
local okC, CacheFs = pcall(require, "src.import.CacheFs")
if okC and CacheFs and CacheFs.readActive then
for _, path in ipairs(candidates) do
local data = CacheFs.readActive(path)
if data and #data >= 0x1000000 then
Pokemon._romBytes = data
return data
end
end
end
return nil
end
local function rom_u8(data, off)
return data:byte(off + 1) or 0
end
local function rom_u16(data, off)
return rom_u8(data, off) + rom_u8(data, off + 1) * 256
end
local function rom_u32(data, off)
return rom_u8(data, off)
+ rom_u8(data, off + 1) * 256
+ rom_u8(data, off + 2) * 65536
+ rom_u8(data, off + 3) * 16777216
end
--- Linear 4bpp decode helper shared by front/back.
local function decode_pic_rgba(species, picTable, palTable, cacheRel, form, fileOffs)
species = tonumber(species)
if not species or species < 1 then return nil end
local cache = resolve_cache(Pokemon._cache)
if cache and cache.read then
local d = cache:read(cacheRel)
if d and #d >= 64 * 64 * 4 then return d end
end
local data = load_rom_bytes()
if not data then return nil end
local Lz77 = require("src.import.gba.lz77")
local tileFile, palFile
if fileOffs then
tileFile, palFile = fileOffs[1], fileOffs[2]
else
tileFile = Versions.gbaToFile(rom_u32(data, picTable + species * 8))
palFile = Versions.gbaToFile(rom_u32(data, palTable + species * 8))
end
if not tileFile or not palFile then return nil end
form = tonumber(form) or 0
local function get(i)
return rom_u8(data, i)
end
local okT, tiles = pcall(Lz77.decompress, get, tileFile)
local okP, palBytes = pcall(Lz77.decompress, get, palFile)
if not okT or not okP or type(tiles) ~= "table" or type(palBytes) ~= "table" then
return nil
end
local pal = {}
for c = 0, 15 do
local lo = palBytes[form * 32 + c * 2 + 1] or 0
local hi = palBytes[form * 32 + c * 2 + 2] or 0
pal[c] = lo + hi * 256
end
local w, h = 64, 64
local rgb = {}
for c = 0, 15 do
local r, g, b = bgr555_to_rgb8(pal[c] or 0)
rgb[c] = { r, g, b }
end
local tilesW, tilesH = 8, 8
local chunks = {}
local ti = 0
for ty = 0, tilesH - 1 do
for tx = 0, tilesW - 1 do
local tileOff = form * 2048 + ti * 32
for row = 0, 7 do
for bx = 0, 3 do
local bi = tileOff + row * 4 + bx + 1
local byte = tiles[bi] or 0
local p0 = byte % 16
local p1 = math.floor(byte / 16) % 16
local x0 = tx * 8 + bx * 2
local y0 = ty * 8 + row
local function put(x, y, idx)
local i = y * w + x + 1
if idx == 0 then
chunks[i] = string.char(0, 0, 0, 0)
else
local c = rgb[idx] or rgb[0]
chunks[i] = string.char(c[1], c[2], c[3], 255)
end
end
put(x0, y0, p0)
put(x0 + 1, y0, p1)
end
end
ti = ti + 1
end
end
local rgba = table.concat(chunks)
if cache and cache.write then
pcall(cache.write, cache, cacheRel, rgba)
end
return rgba
end
local SPECIES_CASTFORM = 385
local function form_of(species, form)
form = tonumber(form) or 0
if species ~= SPECIES_CASTFORM or form < 1 or form > 3 then return 0 end
return form
end
local function pic_rel(kind, species, form)
local root = (CachePaths.CACHE_ROOT or "data/generated/gba") .. "/pokemon/" .. kind .. "/"
if form > 0 then return root .. species .. "_" .. form .. ".rgba" end
return root .. species .. ".rgba"
end
local function decode_front_rgba(species, form)
species = tonumber(species)
if not species or species < 1 then return nil end
local picTable = (Versions.OAK_SPEECH and Versions.OAK_SPEECH.mon_front_pic_table) or 0x2350AC
local palTable = (Versions.OAK_SPEECH and Versions.OAK_SPEECH.mon_palette_table) or 0x23730C
return decode_pic_rgba(species, picTable, palTable, pic_rel("front", species, form), form)
end
local function decode_back_rgba(species, form)
species = tonumber(species)
if not species or species < 1 then return nil end
local picTable = Versions.MON_BACK_PIC_TABLE or 0x23654C
local palTable = (Versions.OAK_SPEECH and Versions.OAK_SPEECH.mon_palette_table) or 0x23730C
return decode_pic_rgba(species, picTable, palTable, pic_rel("back", species, form), form)
end
local function pic_entry(store, key, rgba)
local image = image_from_rgba(rgba, 64, 64)
if not image then return nil end
local entry = { image = image, w = 64, h = 64 }
store[key] = entry
return entry
end
--- 64×64 front pic for showmonpic (ROM-lazy or cache), else nil.
-- pokefirered/src/battle_gfx_sfx_util.c:354
function Pokemon.frontPic(species, form)
species = tonumber(species)
if not species or species < 1 then return nil end
form = form_of(species, form)
local key = form > 0 and (species .. "_" .. form) or species
if Pokemon._front[key] then return Pokemon._front[key] end
local entry = pic_entry(Pokemon._front, key, decode_front_rgba(species, form))
if not entry and form > 0 then return Pokemon.frontPic(species) end
return entry
end
Pokemon.frontSprite = Pokemon.frontPic
--- 64×64 back pic for battle (ROM-lazy or cache).
function Pokemon.backPic(species, form)
species = tonumber(species)
if not species or species < 1 then return nil end
Pokemon._back = Pokemon._back or {}
form = form_of(species, form)
local key = form > 0 and (species .. "_" .. form) or species
if Pokemon._back[key] then return Pokemon._back[key] end
local entry = pic_entry(Pokemon._back, key, decode_back_rgba(species, form))
if not entry and form > 0 then return Pokemon.backPic(species) end
return entry
end
-- pokefirered/src/battle_gfx_sfx_util.c:422
function Pokemon.ghostPic()
if Pokemon._front.ghost then return Pokemon._front.ghost end
local offs = Versions.GHOST_FRONT_PIC and Versions.GHOST_PALETTE
and { Versions.GHOST_FRONT_PIC, Versions.GHOST_PALETTE } or nil
local rgba = decode_pic_rgba(1, 0, 0, pic_rel("front", "ghost", 0), 0, offs or { false, false })
return pic_entry(Pokemon._front, "ghost", rgba)
end
Pokemon.NUMBERING_INTERNAL = "internal"
Pokemon.NUMBERING_NATIONAL = "national"
-- pokefirered/src/data/text/species_names.h:254
function Pokemon.isInternalSpecies(n)
n = tonumber(n)
if not n or n < 1 then return false end
if not Pokemon._names then Pokemon.install(Pokemon._cache) end
local name = Pokemon._names and Pokemon._names[n]
if type(name) ~= "string" or name == "" then return false end
return name:match("^%?+$") == nil
end
function Pokemon.numberingOf(mon)
if type(mon) ~= "table" then return nil end
local tag = mon.speciesNumbering
if tag == Pokemon.NUMBERING_INTERNAL or tag == Pokemon.NUMBERING_NATIONAL then return tag end
return nil
end
function Pokemon.tagNumbering(mon, kind)
if type(mon) ~= "table" then return mon end
if kind ~= Pokemon.NUMBERING_NATIONAL then kind = Pokemon.NUMBERING_INTERNAL end
mon.speciesNumbering = kind
return mon
end
--- Resolve display species for a host/opaque mon table.
-- Host mons use string ids ("KYOGRE"); FRLG scripts use internal SPECIES ints.
function Pokemon.speciesOf(mon)
if not mon then return nil end
local raw = mon.species or mon.speciesId or mon.id
if raw == nil then return nil end
if type(raw) == "string" then
local byName = Pokemon.speciesFromName(raw)
if byName then return byName end
local n = tonumber(raw)
if n then raw = n else return nil end
end
local n = tonumber(raw)
if not n or n < 1 then return nil end
local numbering = Pokemon.numberingOf(mon)
if numbering == Pokemon.NUMBERING_NATIONAL then
return Pokemon.speciesFromNational(n) or n
end
if numbering == Pokemon.NUMBERING_INTERNAL then return n end
if Pokemon.isInternalSpecies(n) then return n end
return Pokemon.speciesFromNational(n) or n
end
function Pokemon.displayName(mon)
if not mon then return "?????" end
local egg = eggName(mon)
if egg then return egg end
if mon.nickname and mon.nickname ~= "" then return tostring(mon.nickname) end
-- Prefer pack name over host species string when we can resolve.
local sp = Pokemon.speciesOf(mon)
if sp then return Pokemon.name(sp) end
if mon.name and mon.name ~= "" then return tostring(mon.name) end
if type(mon.species) == "string" then return mon.species end
return "?????"
end
return Pokemon