Files
gen1recomp/src/core/gen2/Nests.lua
T

163 lines
6.7 KiB
Lua

-- Where does this species live? engine/overworld/wildmons.asm FindNest, which
-- is the data behind the Pokedex's AREA page and the Pokegear MAP card's
-- "<MON>'S NEST" overlay.
--
-- FindNest takes a species and a region (e: 0 Johto, 1 Kanto) and fills the
-- tilemap with LANDMARK indices -- one per map whose wild data contains the
-- species. It reads exactly three sources, in this order:
--
-- .FindGrass JohtoGrassWildMons / KantoGrassWildMons, all NUM_GRASSMON * 3
-- slots, so morning, day AND night count
-- .FindWater JohtoWaterWildMons / KantoWaterWildMons
-- .RoamMon1/2/3 the three roamers' CURRENT map, Johto only
--
-- and nothing else. Headbutt trees, fishing groups, the Bug Contest and swarms
-- are all absent from it, so a HEADBUTT-only species legitimately has no nest
-- and the page stays blank -- that is the cart's answer, not a gap.
--
-- Region is decided here by LANDMARK INDEX rather than by which of the two
-- tables a map came from: the extractor emits one `grass`/`water` table keyed
-- by map, and Johto's landmarks are the run below LANDMARK_PALLET_TOWN with
-- Kanto's above it (constants/landmark_constants.asm). Same split, different
-- spelling.
local Nests = {}
-- constants/landmark_constants.asm. LANDMARK_SPECIAL is 0 and never a nest.
Nests.LANDMARK_PALLET_TOWN = 0x2e
Nests.LANDMARK_FAST_SHIP = 0x5e
function Nests.regionOf(landmark)
if not landmark or landmark <= 0 then return nil end
if landmark >= Nests.LANDMARK_FAST_SHIP then return nil end
return (landmark < Nests.LANDMARK_PALLET_TOWN) and "johto" or "kanto"
end
-- ---------------------------------------------------------- the landmarks
--
-- data/maps/landmarks.asm, which on Gold is one index space shared by every
-- map header's `landmark` byte, the Pokegear MAP card and the #DEX AREA page
-- this file feeds. It is the `landmarks` registry (src/mods/Schemas.lua), one
-- of the Gen 2-only six: Red's town map is a different table with a different
-- id space, so the name is gated under Gen 1 and routed to
-- gen2Landmarks.landmarks under Gen 2 -- straight onto the cache's own table,
-- which means the merge lands in the very table the map card draws from and no
-- Builtins seeding is needed (the same arrangement gen2Maps has).
--
-- Two reads go through here rather than through landmarks.order, which is a
-- plain ordered list the extractor writes and a registered landmark is
-- therefore absent from: src/core/Game2.lua:currentLandmark and
-- src/ui/gen2/MapRadio.lua's region test. `index` on a record is the byte the
-- map header carries, so the lookup is by that and the order list stays the
-- fallback for a dataset whose records predate it.
-- memoized per landmark table (weak keys, so a second dataset in one process
-- does not pin the first); built on first read, which is after the merge --
-- nothing asks for a landmark before the overworld exists
local byIndex = setmetatable({}, { __mode = "k" })
-- Two records may claim one index -- a mod that registers a landmark at a byte
-- the cart already uses -- and pairs() would decide which one answers per
-- process. The cache's own row wins its own slot (landmarks.order is that
-- list), and between two newcomers the lower id wins, so the answer is the
-- same on every boot. A mod that means to MOVE a vanilla landmark patches
-- that record rather than shadowing its index.
local function indexTable(landmarks)
local hit = byIndex[landmarks]
if hit then return hit end
local map, order = {}, landmarks.order or {}
for id, record in pairs(landmarks.landmarks or {}) do
local index = type(record) == "table" and record.index
if index then
local held = map[index]
if held == nil or order[index + 1] == id
or (order[index + 1] ~= held and id < held) then
map[index] = id
end
end
end
byIndex[landmarks] = map
return map
end
-- The LANDMARK_* id at a map header's landmark byte, or nil.
function Nests.landmarkId(data, index)
local landmarks = data and data.gen2Landmarks
if not (landmarks and index) then return nil end
local hit = indexTable(landmarks)[index]
if hit then return hit end
return landmarks.order and landmarks.order[index + 1] or nil
end
-- The record behind that byte: the two-line name and the map-card position.
function Nests.landmark(data, index)
local landmarks = data and data.gen2Landmarks
local id = Nests.landmarkId(data, index)
return id and landmarks.landmarks and landmarks.landmarks[id] or nil
end
local function landmarkOfMap(data, mapId)
local def = data and data.gen2Maps and data.gen2Maps[mapId]
return def and def.landmark
end
-- Every slot of one encounter table entry, across all times of day: the cart
-- walks `NUM_GRASSMON * 3` bytes without caring which third it is in.
local function tableHasSpecies(entry, species)
if type(entry) ~= "table" then return false end
local slots = entry.slots
if type(slots) ~= "table" then return false end
for _, list in pairs(slots) do
if type(list) == "table" then
for _, slot in ipairs(list) do
if slot and slot.species == species then return true end
end
end
end
return false
end
-- The landmark indices where `species` can be met, in ascending order.
--
-- `region` is "johto" or "kanto"; nil means both, which no cart screen asks
-- for but is the useful answer for a test.
function Nests.find(data, species, region, save)
local out, seen = {}, {}
local function add(landmark)
if not landmark or landmark <= 0 or seen[landmark] then return end
local where = Nests.regionOf(landmark)
if not where then return end
if region and where ~= region then return end
seen[landmark] = true
out[#out + 1] = landmark
end
local enc = data and data.gen2Encounters
for _, key in ipairs({ "grass", "water" }) do
for mapId, entry in pairs((enc and enc[key]) or {}) do
if tableHasSpecies(entry, species) then
add(landmarkOfMap(data, mapId))
end
end
end
-- .RoamMon1/2/3: the roamer's CURRENT map, and only while it is still out
-- there -- a caught or defeated one keeps its slot but loses its species and
-- map. Johto-only on the cart, which the region filter above enforces.
--
-- The test is Roamers.active, NOT "has HP": a roamer starts life at hp 0
-- (`xor a ; generate new stats`) and only gets a real value once you have met
-- it, so an HP test would hide all three until first contact -- exactly the
-- ones the page is most useful for.
local Roamers = require("src.core.gen2.Roamers")
for _, slot in ipairs((save and save.roamers) or {}) do
if Roamers.active(slot) and slot.species == species then
add(landmarkOfMap(data, slot.map))
end
end
table.sort(out)
return out
end
return Nests