mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-25 23:11:15 +02:00
286988a1e3
also fixes feet layering of gold with grass. Unifies gen1/gen2 grass layering into a single system
197 lines
8.0 KiB
Lua
197 lines
8.0 KiB
Lua
-- Gen 2 wild encounters (engine/overworld/wildmons.asm).
|
|
--
|
|
-- The Gen 2 mechanic Gen 1 does not have: a grass table holds three separate
|
|
-- seven-slot lists, one per time of day, plus a per-time encounter *rate*. So
|
|
-- the same patch of grass on Route 29 gives Pidgey in the morning and Hoothoot
|
|
-- at night, and the clock that decides which is the same one that decides the
|
|
-- palette -- see src/world/gen2/Palettes.lua.
|
|
--
|
|
-- Slot probabilities are Gen 2's ProbabilityTable (data/wild/probabilities.asm):
|
|
-- 30, 30, 20, 10, 5, 4, 1 percent across the seven slots, cumulative.
|
|
|
|
local Encounter = {}
|
|
|
|
-- data/wild/probabilities.asm, cumulative out of 100.
|
|
Encounter.GRASS_SLOT_CHANCES = { 30, 60, 80, 90, 95, 99, 100 }
|
|
-- Water has three slots: 60, 30, 10.
|
|
Encounter.WATER_SLOT_CHANCES = { 60, 90, 100 }
|
|
|
|
local function roll(random, n)
|
|
if random then return random(n) end
|
|
if love and love.math and love.math.random then
|
|
return love.math.random(n) - 1
|
|
end
|
|
return math.random(n) - 1
|
|
end
|
|
|
|
-- Which slot a 0..99 roll lands in.
|
|
local function slotFor(chances, value)
|
|
for index, cumulative in ipairs(chances) do
|
|
if value < cumulative then return index end
|
|
end
|
|
return #chances
|
|
end
|
|
|
|
-- Does a step in grass start a battle? The map's rate is out of 256
|
|
-- (`db 2 percent`), and the cart compares one random byte against it.
|
|
function Encounter.triggers(rate, random)
|
|
if not rate or rate <= 0 then return false end
|
|
return roll(random, 256) < rate
|
|
end
|
|
|
|
-- The grass encounter for a map at a time of day, or nil when that map has
|
|
-- none. `daytime` is "MORN"/"DAY"/"NITE"/"DARK"; DARK reuses the night list,
|
|
-- since the cart only stores three (wildmons.asm masks the palette daytime down
|
|
-- to three when indexing).
|
|
function Encounter.grassSlot(encounters, mapId, daytime, random)
|
|
local entry = encounters and encounters.grass and encounters.grass[mapId]
|
|
if not entry then return nil end
|
|
local key = (daytime == "DARK") and "NITE" or (daytime or "DAY")
|
|
local slots = entry.slots and (entry.slots[key] or entry.slots.DAY)
|
|
if not slots then return nil end
|
|
local index = slotFor(Encounter.GRASS_SLOT_CHANCES, roll(random, 100))
|
|
local slot = slots[index]
|
|
if not slot or not slot.species then return nil end
|
|
return { species = slot.species, level = slot.level, slot = index }
|
|
end
|
|
|
|
function Encounter.grassRate(encounters, mapId, daytime)
|
|
local entry = encounters and encounters.grass and encounters.grass[mapId]
|
|
if not entry then return 0 end
|
|
local key = (daytime == "DARK") and "NITE" or (daytime or "DAY")
|
|
return (entry.rates and (entry.rates[key] or entry.rates.DAY)) or 0
|
|
end
|
|
|
|
function Encounter.waterSlot(encounters, mapId, random)
|
|
local entry = encounters and encounters.water and encounters.water[mapId]
|
|
if not entry or not entry.slots then return nil end
|
|
local index = slotFor(Encounter.WATER_SLOT_CHANCES, roll(random, 100))
|
|
local slot = entry.slots[index]
|
|
if not slot or not slot.species then return nil end
|
|
return { species = slot.species, level = slot.level, slot = index }
|
|
end
|
|
|
|
function Encounter.waterRate(encounters, mapId)
|
|
local entry = encounters and encounters.water and encounters.water[mapId]
|
|
return (entry and entry.rate) or 0
|
|
end
|
|
|
|
-- Fishing: a rod's list is (cumulative chance, species, level) rows out of 256,
|
|
-- ending at 100%. A roll past the group's own `chance` is a bite of nothing.
|
|
-- Rows with `day` and `nite` sub-slots (from TimeFishGroups) resolve based on
|
|
-- `daytime` ("MORN"/"DAY" vs "NITE"/"DARK").
|
|
function Encounter.fish(encounters, fishGroup, rod, daytime, random)
|
|
if type(daytime) == "function" and random == nil then
|
|
random = daytime
|
|
daytime = nil
|
|
end
|
|
local group = encounters and encounters.fishGroups
|
|
and encounters.fishGroups[fishGroup]
|
|
if not group then return nil end
|
|
local list = group[rod or "old"]
|
|
if not list or #list == 0 then return nil end
|
|
local value = roll(random, 256)
|
|
local isNight = (daytime == "DARK" or daytime == "NITE")
|
|
local todKey = isNight and "nite" or "day"
|
|
for _, row in ipairs(list) do
|
|
if value < (row.chance or 0) then
|
|
local slot = row[todKey]
|
|
if not slot and row.timeGroup and encounters and encounters.timeFishGroups then
|
|
local tg = encounters.timeFishGroups[row.timeGroup]
|
|
slot = tg and tg[todKey]
|
|
end
|
|
slot = slot or row
|
|
if not slot.species or slot.species == 0 or slot.species == "NO_ITEM" then return nil end
|
|
return { species = slot.species, level = slot.level }
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
-- GetFishGroupIndex (engine/events/fish.asm), the fishing half of a swarm:
|
|
-- Fish calls it before it indexes FishGroups, and it swaps FISHGROUP_QWILFISH
|
|
-- for FISHGROUP_QWILFISH_SWARM (and FISHGROUP_REMORAID for
|
|
-- FISHGROUP_REMORAID_SWARM) while wFishingSwarmFlag names that swarm. Nothing
|
|
-- else is substituted: FISHGROUP_QWILFISH_NO_SWARM is a map header value of its
|
|
-- own and never becomes a swarm group. `fishSwarm` is the FISHSWARM_* byte
|
|
-- (constants/script_constants.asm), which the port keeps in
|
|
-- save.dailyFlags.fishingSwarm and reads back through Roamers.Swarm.fishing.
|
|
Encounter.FISHSWARM_NONE = 0
|
|
Encounter.FISHSWARM_QWILFISH = 1
|
|
Encounter.FISHSWARM_REMORAID = 2
|
|
|
|
local FISH_SWARM_GROUPS = {
|
|
[Encounter.FISHSWARM_QWILFISH] = {
|
|
FISHGROUP_QWILFISH = "FISHGROUP_QWILFISH_SWARM",
|
|
},
|
|
[Encounter.FISHSWARM_REMORAID] = {
|
|
FISHGROUP_REMORAID = "FISHGROUP_REMORAID_SWARM",
|
|
},
|
|
}
|
|
|
|
-- A cache built before the extractor carried the two swarm rows has no such
|
|
-- group at all, and Fish on a missing group is a bite of nothing; falling back
|
|
-- to the map's own group keeps those rods rolling their ordinary list.
|
|
function Encounter.fishGroupFor(encounters, group, fishSwarm)
|
|
local swap = FISH_SWARM_GROUPS[fishSwarm or Encounter.FISHSWARM_NONE]
|
|
local swarmed = swap and swap[group]
|
|
if not swarmed then return group end
|
|
local groups = encounters and encounters.fishGroups
|
|
if not (groups and groups[swarmed]) then return group end
|
|
return swarmed
|
|
end
|
|
|
|
-- Which fish group a MAP belongs to lives on the map record, so a caller with
|
|
-- a map id and a rod does not have to know about groups at all.
|
|
function Encounter.fishSlot(encounters, mapId, rod, random, maps, fishSwarm, daytime)
|
|
local map = maps and maps[mapId]
|
|
local group = map and map.fishGroup
|
|
if not group then
|
|
-- Callers that already hold the map (the World does) pass it in; without
|
|
-- it, fall back to the pond, which is what an unlisted map fishes.
|
|
group = "FISHGROUP_POND"
|
|
end
|
|
group = Encounter.fishGroupFor(encounters, group, fishSwarm)
|
|
local key = rod
|
|
if rod == "OLD_ROD" then key = "old"
|
|
elseif rod == "GOOD_ROD" then key = "good"
|
|
elseif rod == "SUPER_ROD" then key = "super" end
|
|
return Encounter.fish(encounters, group, key or "old", daytime, random)
|
|
end
|
|
|
|
-- Headbutt trees: TreeMonMaps says which set a map uses and TreeMons holds
|
|
-- that set's two lists. The rows are cumulative percentages ending at -1, the
|
|
-- same shape as the fishing lists.
|
|
function Encounter.treeSet(encounters, mapId)
|
|
return encounters and encounters.trees and encounters.trees[mapId] or nil
|
|
end
|
|
|
|
-- A headbutt on the tree at (cx, cy). Whether the COMMON or the RARE list is
|
|
-- rolled comes from the tree's own coordinates on the cart -- GetTreeMons
|
|
-- hashes them so the same tree always behaves the same way -- which is what
|
|
-- keeps a player from re-rolling one tree for a rare mon.
|
|
function Encounter.treeIsRare(cx, cy)
|
|
return ((cx or 0) * 5 + (cy or 0) * 7) % 10 < 1
|
|
end
|
|
|
|
function Encounter.treeSlot(encounters, mapId, cx, cy, random)
|
|
local setName = Encounter.treeSet(encounters, mapId)
|
|
if not setName then return nil end
|
|
local set = encounters and encounters.treeSets and encounters.treeSets[setName]
|
|
if not set then return nil end
|
|
local list = Encounter.treeIsRare(cx, cy) and set.rare or set.common
|
|
if not list or #list == 0 then return nil end
|
|
local value = roll(random, 100)
|
|
local total = 0
|
|
for _, row in ipairs(list) do
|
|
total = total + (row.chance or 0)
|
|
if value < total then
|
|
if not row.species then return nil end
|
|
return { species = row.species, level = row.level }
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
return Encounter
|