Files
gen1recomp/src/core/gen2/Happiness.lua
T
bryanthaboi ae6cac89e1 G2 support
2026-08-11 11:53:30 -04:00

289 lines
12 KiB
Lua

-- Gen 2 friendship.
--
-- src/battle/gen2/Mon.lua has carried a `happiness` field since the party
-- struct was ported and src/core/gen2/Evolution.lua reads it, but until this
-- module existed nothing ever MOVED it: every mon sat on BASE_HAPPINESS
-- forever, which made EVOLVE_HAPPINESS unreachable and the Goldenrod
-- friendship rater a constant.
--
-- Two separate mechanisms, both from the cart:
--
-- ChangeHappiness (engine/events/happiness_egg.asm) applies one of the
-- eighteen HAPPINESS_* events to one party mon. The step it applies is NOT
-- fixed: HappinessChanges (data/events/happiness_changes.asm) is a
-- `table_width 3` block whose three columns are "happiness < 100",
-- "happiness < 200", and "otherwise", so a mon that already likes you gains
-- less and (for the bitter herbs and a poison faint) loses MORE. That tier
-- is read off the value BEFORE the change.
--
-- StepHappiness (engine/events/happiness_egg.asm) raises the whole party by
-- one point, and it is called only when wStepCount wraps -- and then only on
-- every OTHER call, because it keeps its own wHappinessStepCount toggle.
-- The visible period is therefore 512 footfalls, not 256.
--
-- Both routines refuse to touch an EGG: ChangeHappiness `cp EGG / ret z` on
-- wPartySpecies before it even finds the byte, and StepHappiness's loop skips
-- the slot. An egg's "happiness" byte is its remaining hatch cycles
-- (engine/pokemon/move_mon.asm writes wBaseEggSteps there), so incrementing it
-- would hand the player a Togepi 512 steps early. The port keeps the two
-- apart on `mon.eggSteps` (see src/core/gen2/Breeding.lua), and this module
-- still honours the egg gate so the ORDER of events matches the cart.
local Runtime = require("src.mods.Runtime")
local Happiness = {}
-- happiness.changed, one of the handful of names Gen 2 invents because Gen 1
-- has no friendship byte at all (docs/mod-api-gen2-compat.md, "New in Gen 2").
-- Raised from the two routines that MOVE the byte and from nowhere else, so a
-- mod that mirrors friendship into its own UI sees every point:
--
-- mon the party record whose byte moved
-- event the HAPPINESS_* name/index the caller passed, or nil for a step
-- reason "event" for ChangeHappiness, "step" for StepHappiness
-- delta the signed step actually applied, AFTER the byte's own clamps
-- from, to the value either side of the change
--
-- `delta` is `to - from` rather than the table's column, because the $ff and 0
-- carry clamps are part of what the cart applied: a mon at 254 gaining "5"
-- gained 1.
local function emitChanged(mon, event, reason, from, to)
if not Runtime.wants("happiness.changed") then return end
Runtime.emit("happiness.changed", {
mon = mon, event = event, reason = reason,
delta = to - from, from = from, to = to,
})
end
-- constants/pokemon_data_constants.asm, "significant happiness values".
Happiness.BASE = 70
Happiness.FRIEND_BALL = 200
Happiness.TO_EVOLVE = 220
Happiness.THRESHOLD_1 = 100
Happiness.THRESHOLD_2 = 200
-- The byte's own ceiling; the floor is 0.
Happiness.MAX = 255
-- The HAPPINESS_* enum. Its `const_def 1` makes it ONE based, so these
-- indices line up with a 1-based Lua array without an offset -- the shift that
-- would otherwise drop the last row (HAPPINESS_GROOMING) to nil.
Happiness.EVENT = {
GAINLEVEL = 1, -- 01
USEDITEM = 2, -- 02 a vitamin
USEDXITEM = 3, -- 03 X ATTACK / X DEFEND / X SPEED / X SPECIAL
GYMBATTLE = 4, -- 04
LEARNMOVE = 5, -- 05 a TM, not an HM
FAINTED = 6, -- 06
POISONFAINT = 7, -- 07
BEATENBYSTRONGFOE = 8, -- 08
OLDERCUT1 = 9, -- 09
OLDERCUT2 = 10, -- 0a
OLDERCUT3 = 11, -- 0b
YOUNGCUT1 = 12, -- 0c
YOUNGCUT2 = 13, -- 0d
YOUNGCUT3 = 14, -- 0e
BITTERPOWDER = 15, -- 0f HEAL POWDER / ENERGYPOWDER
ENERGYROOT = 16, -- 10
REVIVALHERB = 17, -- 11
GROOMING = 18, -- 12
}
Happiness.NUM_EVENTS = 18
-- data/events/happiness_changes.asm, transcribed row for row. The three
-- columns are the three tiers below, in order.
Happiness.CHANGES = {
{ 5, 3, 2 }, -- 01 Gained a level
{ 5, 3, 2 }, -- 02 Vitamin
{ 1, 1, 0 }, -- 03 X Item
{ 3, 2, 1 }, -- 04 Battled a Gym Leader
{ 1, 1, 0 }, -- 05 Learned a move
{ -1, -1, -1 }, -- 06 Lost to an enemy
{ -5, -5, -10 }, -- 07 Fainted due to poison
{ -5, -5, -10 }, -- 08 Lost to a much stronger enemy
{ 1, 1, 1 }, -- 09 Haircut (older brother) 1
{ 3, 3, 1 }, -- 0a Haircut (older brother) 2
{ 5, 5, 2 }, -- 0b Haircut (older brother) 3
{ 1, 1, 1 }, -- 0c Haircut (younger brother) 1
{ 3, 3, 1 }, -- 0d Haircut (younger brother) 2
{ 10, 10, 4 }, -- 0e Haircut (younger brother) 3
{ -5, -5, -10 }, -- 0f Used Heal Powder or Energypowder (bitter)
{ -10, -10, -15 }, -- 10 Used Energy Root (bitter)
{ -15, -15, -20 }, -- 11 Used Revival Herb (bitter)
{ 3, 3, 1 }, -- 12 Grooming
}
-- Which of HappinessChanges' three columns a CURRENT value reads. The cart
-- builds this as `e`: 0, then +1 once the value is >= 100, then +1 again once
-- it is >= 200. Returned 1-based to index the rows above.
function Happiness.tier(value)
value = value or 0
if value < Happiness.THRESHOLD_1 then return 1 end
if value < Happiness.THRESHOLD_2 then return 2 end
return 3
end
-- Resolve an event to its index. Callers may pass the name ("GAINLEVEL"),
-- the full constant ("HAPPINESS_GAINLEVEL") or the raw number, so a hand
-- ported script and an extracted one can both say what they mean.
function Happiness.eventIndex(event)
if type(event) == "number" then
if event >= 1 and event <= Happiness.NUM_EVENTS then return event end
return nil
end
if type(event) ~= "string" then return nil end
local name = event:match("^HAPPINESS_(.+)$") or event
return Happiness.EVENT[name]
end
-- The signed step an event applies at a current value, or nil for an event
-- this table does not know. Split out so a test can assert the tier
-- boundaries without going through a mon.
function Happiness.delta(event, current)
local index = Happiness.eventIndex(event)
if not index then return nil end
local row = Happiness.CHANGES[index]
if not row then return nil end
return row[Happiness.tier(current)]
end
-- An egg is skipped, exactly as ChangeHappiness's `cp EGG / ret z` does.
-- Matches src/core/gen2/Breeding.lua's isEgg without requiring it, so this
-- module stays loadable on its own.
local function isEgg(mon)
return type(mon) == "table" and mon.isEgg == true
end
-- ChangeHappiness itself. Returns the new value, or nil when nothing moved
-- (no mon, an egg, or an event the table does not carry).
--
-- The clamps are the cart's carry checks, not a max/min bolted on: a positive
-- step that overflows the byte lands on $ff (`ld a, -1`), and a negative one
-- that underflows lands on 0 (`xor a`). Both edges are reachable in normal
-- play -- 255 from walking, 0 from a poison faint at low friendship -- so they
-- are load bearing rather than defensive.
function Happiness.change(mon, event)
if type(mon) ~= "table" or isEgg(mon) then return nil end
local current = mon.happiness or 0
local delta = Happiness.delta(event, current)
if not delta then return nil end
local value = current + delta
if value > Happiness.MAX then value = Happiness.MAX end
if value < 0 then value = 0 end
mon.happiness = value
emitChanged(mon, event, "event", current, value)
return value
end
-- The same event across a party, which is how the Gym Leader award is written
-- out longhand in engine/battle/core.asm InitEnemyTrainer:
--
-- ld a, MON_HP / call GetPartyParamLocation
-- ld a, [hli] / or [hl] / jr z, .skipfaintedmon
--
-- so a mon that is already down does not earn the leader's approval.
--
-- The OTHER party-wide site is not this loop and must not use it:
-- engine/events/poisonstep.asm walks wPoisonStepPartyFlags and awards
-- HAPPINESS_POISONFAINT to exactly the mons that just dropped, every one of
-- which is at zero HP. That caller wants Happiness.change per flagged slot,
-- or this with opts.includeFainted.
function Happiness.changeParty(party, event, opts)
opts = opts or {}
local touched = 0
for _, mon in ipairs(party or {}) do
local alive = (mon.hp or 0) > 0 or opts.includeFainted
if alive and Happiness.change(mon, event) then touched = touched + 1 end
end
return touched
end
-- StepHappiness. Its own toggle: `inc a / and 1 / ld [hl], a / ret nz` alternates
-- 1, 0, 1, 0 and only falls through on the 0, so the party gains a point every
-- SECOND time this is called. The `inc [hl] / jr nz / ld [hl], $ff` on each
-- mon is why 255 sticks rather than wrapping to 0.
--
-- Returns true on the calls that actually raised the party.
function Happiness.stepCycle(save)
if type(save) ~= "table" then return false end
save.happinessStepCount = ((save.happinessStepCount or 0) + 1) % 2
if save.happinessStepCount ~= 0 then return false end
for _, mon in ipairs(save.party or {}) do
if not isEgg(mon) then
local from = mon.happiness or 0
mon.happiness = math.min(Happiness.MAX, from + 1)
-- A mon already sitting on $ff is walked over by `inc [hl] / jr nz`
-- writing $ff back, so nothing moved and there is nothing to report.
if mon.happiness ~= from then
emitChanged(mon, nil, "step", from, mon.happiness)
end
end
end
return true
end
-- One overworld footfall, from engine/overworld/events.asm's step block:
--
-- ld hl, wStepCount / inc [hl] / jr nz, .skip_happiness / farcall StepHappiness
--
-- `inc [hl]` sets z only on the wrap, so StepHappiness runs on the step that
-- takes wStepCount from 255 back to 0 -- one call every 256 steps, and a
-- party point every 512. src/core/gen2/Breeding.lua owns that same counter
-- (`save.stepCount`, advanced by Breeding.step), so this must be called AFTER
-- Breeding.step on the same footfall or it will read the previous step's
-- value.
function Happiness.step(save)
if type(save) ~= "table" then return false end
if (save.stepCount or 0) ~= 0 then return false end
return Happiness.stepCycle(save)
end
-- How many footfalls are still owed before the party next gains a point. For
-- a driver or a test that wants to walk exactly far enough rather than 512
-- times blind.
function Happiness.stepsToGain(save)
if type(save) ~= "table" then return nil end
local cycle = 256
local toWrap = (cycle - (save.stepCount or 0)) % cycle
if toWrap == 0 then toWrap = cycle end
-- A toggle sitting at 1 means the NEXT wrap is the one that pays out.
if (save.happinessStepCount or 0) == 1 then return toWrap end
return toWrap + cycle
end
-- The three answers HappinessCheckScript (engine/events/std_scripts.asm) picks
-- between off GetFirstPokemonHappiness: `ifless 50` and `ifless 150`, so the
-- boundaries are inclusive at the top of each band.
Happiness.RATER_UNHAPPY = 50
Happiness.RATER_KINDA = 150
function Happiness.raterBand(value)
value = value or 0
if value < Happiness.RATER_UNHAPPY then return "unhappy" end
if value < Happiness.RATER_KINDA then return "kinda" end
return "happy" -- HappinessText3, the one that means "it adores you"
end
-- GetFirstPokemonHappiness: the first party slot that is NOT an egg, which is
-- what the rater and the Goldenrod NPCs read. Returns the mon and its slot.
function Happiness.firstMon(party)
for index, mon in ipairs(party or {}) do
if not isEgg(mon) then return mon, index end
end
return nil, nil
end
-- What a mon starts life on. There is no ChangeHappiness event for a TRADE in
-- Gen 2 (that arrives in Gen 3): a traded or gifted mon simply comes in
-- through the struct initialisers in engine/pokemon/move_mon.asm, every one of
-- which writes BASE_HAPPINESS. The two exceptions are a FRIEND_BALL capture
-- (engine/items/item_effects.asm writes FRIEND_BALL_HAPPINESS over it, for the
-- party AND the box copy) and a hatchling, which
-- src/core/gen2/Breeding.lua sets to its own $78.
function Happiness.forNewMon(opts)
opts = opts or {}
if opts.ball == "FRIEND_BALL" then return Happiness.FRIEND_BALL end
return Happiness.BASE
end
return Happiness