mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-18 19:54:21 +02:00
G2 support
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
-- CountStep (engine/overworld/events.asm), the block that runs on every
|
||||
-- overworld footfall, and the four routines it calls that had no port at all.
|
||||
--
|
||||
-- This is the chain the world was missing entirely: `World` kept no step count,
|
||||
-- so `Happiness.step` and `Breeding.step` were written and tested and nothing
|
||||
-- ever called either. Eggs never hatched and scripted phone calls never fired,
|
||||
-- both main quest.
|
||||
--
|
||||
-- CheckTileEvent runs it between the coord events and the wild encounter roll,
|
||||
-- and a CARRY out of it means a player event is queued -- which is why a step
|
||||
-- that hatches an egg or drops a poisoned mon never also starts a battle.
|
||||
--
|
||||
-- CountStep:
|
||||
-- ret if wLinkMode ; not modelled: no link overworld
|
||||
-- CheckSpecialPhoneCall -> c: .doscript
|
||||
-- DoRepelStep -> c: .doscript
|
||||
-- inc wPoisonStepCount
|
||||
-- inc wStepCount -> z (the wrap): StepHappiness
|
||||
-- wStepCount == $80 : DoEggStep -> nz: .hatch
|
||||
-- DayCareStep
|
||||
-- wPoisonStepCount >= 4 : reset, DoPoisonStep -> c: .doscript
|
||||
-- DoBikeStep
|
||||
--
|
||||
-- Everything here is love-free and takes its state as arguments so the whole
|
||||
-- chain is testable without a world.
|
||||
local Bike = require("src.world.gen2.Bike")
|
||||
local Breeding = require("src.core.gen2.Breeding")
|
||||
local Happiness = require("src.core.gen2.Happiness")
|
||||
local Phone = require("src.core.gen2.Phone")
|
||||
|
||||
local StepEvents = {}
|
||||
|
||||
-- constants/pokemon_data_constants.asm: `1 << PSN`. The port stores a status
|
||||
-- as a lowercase name on the mon; the battle writes "poison"/"toxic"
|
||||
-- (Battle.STATUS_EFFECTS), older saves may carry "psn"/"tox".
|
||||
local function isPoisoned(mon)
|
||||
local status = mon and mon.status
|
||||
return status == "psn" or status == "tox" or status == "poison"
|
||||
or status == "toxic"
|
||||
end
|
||||
|
||||
-- .DamageMonIfPoisoned's two answers, kept as the cart's own bit pair so the
|
||||
-- "someone fainted beats someone hurt" test below reads like `and %10`.
|
||||
StepEvents.POISON_HURT = 1
|
||||
StepEvents.POISON_FAINTED = 2
|
||||
|
||||
-- Every 4 steps (wPoisonStepCount `cp 4 / jr c`).
|
||||
StepEvents.POISON_PERIOD = 4
|
||||
|
||||
-- DoBikeStep's threshold is `cp HIGH(1024)` on the counter's HIGH byte, so it
|
||||
-- is 1024 steps and the counter saturates at $ffff rather than wrapping.
|
||||
StepEvents.BIKE_CALL_STEPS = 1024
|
||||
StepEvents.BIKE_STEP_MAX = 0xffff
|
||||
|
||||
-- DoPoisonStep. One HP off every poisoned mon that is still standing, and the
|
||||
-- mon that runs out has its status CLEARED on the way down -- so a party wiped
|
||||
-- by poison walks into the Pokemon Center with no status left to cure.
|
||||
--
|
||||
-- The two flags are collected across the WHOLE party before either branch is
|
||||
-- taken (wPoisonStepFlagSum), which is why one faint anywhere in the party
|
||||
-- outranks five mons merely taking damage.
|
||||
function StepEvents.poisonStep(party)
|
||||
party = party or {}
|
||||
local hurt, fainted = {}, {}
|
||||
for index, mon in ipairs(party) do
|
||||
if isPoisoned(mon) and (mon.hp or 0) > 0 then
|
||||
mon.hp = mon.hp - 1
|
||||
if mon.hp <= 0 then
|
||||
mon.hp = 0
|
||||
mon.status = nil
|
||||
fainted[#fainted + 1] = index
|
||||
else
|
||||
hurt[#hurt + 1] = index
|
||||
end
|
||||
end
|
||||
end
|
||||
if #fainted > 0 then
|
||||
return { kind = "poisonFaint", fainted = fainted, hurt = hurt, blocks = true }
|
||||
end
|
||||
if #hurt > 0 then
|
||||
-- .PlayPoisonSFX and the two-frame red flash, then `xor a`: no carry, so
|
||||
-- the step still counts and the wild roll still happens.
|
||||
return { kind = "poisonHurt", hurt = hurt, blocks = false }
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- .CheckWhitedOut's tail: `predef CheckPlayerPartyForFitMon`, whose answer is
|
||||
-- what decides between closing the text box and jumping to
|
||||
-- OverworldWhiteoutScript. An egg is not a fit mon (DayCare_GiveEgg zeroes its
|
||||
-- HP), which Breeding.healthyCount already says out loud.
|
||||
function StepEvents.whitedOut(party)
|
||||
return Breeding.healthyCount(party) == 0
|
||||
end
|
||||
|
||||
-- DoRepelStep. `dec a / ret nz`: the wear-off lands on the step that takes the
|
||||
-- counter to zero, and that step is NOT counted -- so the last repel step never
|
||||
-- ticks the egg or the day care.
|
||||
function StepEvents.repelStep(save)
|
||||
local left = save.repelSteps or 0
|
||||
if left <= 0 then return false end
|
||||
save.repelSteps = left - 1
|
||||
return save.repelSteps == 0
|
||||
end
|
||||
|
||||
-- DoBikeStep. Four gates before the counter even moves, and then a quirk worth
|
||||
-- keeping: `scf` at the end is thrown away by CountStep's `.done`, which does
|
||||
-- `xor a / ret`. So queueing the bike shop's call does NOT stop the step being
|
||||
-- counted and does NOT produce a player event -- the call goes out on the NEXT
|
||||
-- footfall, through CheckSpecialPhoneCall at the top of this same block.
|
||||
--
|
||||
-- wStatusFlags2's BIKE_SHOP_CALL bit is not a byte nobody else reads: the
|
||||
-- Goldenrod bike shop clerk's own `setflag ENGINE_BIKE_SHOP_CALL_ENABLED`
|
||||
-- (maps/GoldenrodBikeShop.asm) is what turns it on, and Vm's setflag lands
|
||||
-- that on save.engineFlags under the ENGINE_* id. save.bikeShopCall is kept
|
||||
-- as the fallback for a save written before that was wired up, and is cleared
|
||||
-- alongside the flag so the two can never disagree.
|
||||
local function bikeShopCallEnabled(save)
|
||||
local flags = save.engineFlags
|
||||
if type(flags) == "table" then
|
||||
local set = flags[Bike.ENGINE_BIKE_SHOP_CALL_ENABLED]
|
||||
if set ~= nil then return set == true end
|
||||
end
|
||||
return save.bikeShopCall == true
|
||||
end
|
||||
|
||||
function StepEvents.bikeStep(save, opts)
|
||||
opts = opts or {}
|
||||
if not bikeShopCallEnabled(save) then return false end
|
||||
if opts.playerState ~= "bike" then return false end
|
||||
if opts.phoneService == false then return false end
|
||||
local steps = math.min((save.bikeStep or 0) + 1, StepEvents.BIKE_STEP_MAX)
|
||||
save.bikeStep = steps
|
||||
if steps < StepEvents.BIKE_CALL_STEPS then return false end
|
||||
-- "If a call has already been queued, don't overwrite that call."
|
||||
if Phone.hasSpecialCall(save) then return false end
|
||||
Phone.queueSpecialCall(save, Phone.SPECIALCALL.SPECIALCALL_BIKESHOP)
|
||||
-- `res STATUSFLAGS2_BIKE_SHOP_CALL_F`: one call, ever.
|
||||
if type(save.engineFlags) == "table" then
|
||||
save.engineFlags[Bike.ENGINE_BIKE_SHOP_CALL_ENABLED] = nil
|
||||
end
|
||||
save.bikeShopCall = false
|
||||
return true
|
||||
end
|
||||
|
||||
-- The whole block, in the cart's order.
|
||||
--
|
||||
-- `ctx` carries what the routines need from outside the save: `data` for the
|
||||
-- day care's species lookups, `rng` for its egg roll, `phone` for
|
||||
-- CheckSpecialPhoneCall's map/time context, `playerState` and `phoneService`
|
||||
-- for DoBikeStep.
|
||||
--
|
||||
-- Returns an event table or nil, plus whether the step was COUNTED. The
|
||||
-- event's `blocks` field is CountStep's CARRY: the caller owes the matching
|
||||
-- player-event script and must not roll a wild encounter on that step. Only
|
||||
-- `poisonHurt` reports an event without one -- DoPoisonStep's .PlayPoisonSFX
|
||||
-- arm ends `xor a`, so a party that merely takes damage still walks into grass.
|
||||
function StepEvents.count(save, ctx)
|
||||
ctx = ctx or {}
|
||||
if type(save) ~= "table" then return nil, false end
|
||||
if ctx.linkMode then return nil, false end
|
||||
|
||||
-- Neither of the next two counts the step.
|
||||
local call = Phone.checkSpecialCall(save, ctx.phone)
|
||||
if call then return { kind = "phoneCall", call = call, blocks = true }, false end
|
||||
if StepEvents.repelStep(save) then
|
||||
return { kind = "repel", blocks = true }, false
|
||||
end
|
||||
|
||||
save.poisonStepCount = ((save.poisonStepCount or 0) + 1) % 256
|
||||
|
||||
-- Breeding.step owns wStepCount: it increments, ticks the eggs at $80 and
|
||||
-- runs DayCareStep, all in the cart's order. StepHappiness sits between the
|
||||
-- increment and the egg tick on the cart and is called after both here, which
|
||||
-- is safe rather than sloppy: the wrap ($00) and the egg phase ($80) can
|
||||
-- never be the same step, so the two never run on the same footfall at all.
|
||||
local bred = Breeding.step(ctx.data, save, ctx.rng)
|
||||
Happiness.step(save)
|
||||
if bred == "hatch" then return { kind = "hatch", blocks = true }, true end
|
||||
|
||||
if save.poisonStepCount >= StepEvents.POISON_PERIOD then
|
||||
save.poisonStepCount = 0
|
||||
local poison = StepEvents.poisonStep(save.party)
|
||||
if poison and poison.kind == "poisonFaint" then
|
||||
poison.whiteout = StepEvents.whitedOut(save.party)
|
||||
return poison, true
|
||||
end
|
||||
if poison then
|
||||
-- .PlayPoisonSFX only: no carry, so the caller plays the sound and the
|
||||
-- step carries on into the wild roll.
|
||||
StepEvents.bikeStep(save, ctx)
|
||||
return poison, true
|
||||
end
|
||||
end
|
||||
|
||||
StepEvents.bikeStep(save, ctx)
|
||||
return nil, true
|
||||
end
|
||||
|
||||
return StepEvents
|
||||
Reference in New Issue
Block a user