mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-16 00:02:23 +02:00
289 lines
12 KiB
Lua
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
|