mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-25 23:11:15 +02:00
242 lines
10 KiB
Lua
242 lines
10 KiB
Lua
-- Prize money for beating a trainer.
|
|
--
|
|
-- Two routines, in two files, and both halves matter:
|
|
--
|
|
-- ComputeTrainerReward (engine/battle/read_trainer_party.asm) runs when the
|
|
-- party is READ, not when it is beaten: wBattleReward = the class's
|
|
-- TRNATTR_BASE_REWARD times wCurPartyLevel, and wCurPartyLevel at that
|
|
-- point is whatever the LAST row of data/trainers/parties.asm left there.
|
|
-- So Falkner pays for his level 9 Pidgeotto and not for the level 7 Pidgey
|
|
-- that came out first, whichever of them faints last.
|
|
-- WinTrainerBattle (engine/battle/core.asm) is the trainer-defeated arm, and
|
|
-- it hands out wBattleReward FOUR TIMES -- `ld c, 4`, one add per pass --
|
|
-- before doubling the figure twice for the text. That factor of four is
|
|
-- the difference between Falkner's ¥900 and a ¥225 that would look
|
|
-- plausible and be wrong.
|
|
--
|
|
-- The split is the other half of Bank of Mom: those four quarters are dealt
|
|
-- between wMoney and wMomsMoney by wMomSavingMoney, so "save some money for
|
|
-- me" is a standing 25% deduction on every trainer you beat, not a thing that
|
|
-- only happens when you walk into the house.
|
|
--
|
|
-- love-free and save-shaped: takes the Gold save table
|
|
-- (src/core/gen2/Save.lua) and writes the two accounts on it, so the battle
|
|
-- engine, the world and the tests all reach the same routine.
|
|
|
|
local Strings = require("src.core.Strings")
|
|
|
|
local Prize = {}
|
|
|
|
-- constants/misc_constants.asm. The same cap Save.MAX_MONEY carries; spelled
|
|
-- out here so this module stays usable against a bare save-shaped table.
|
|
Prize.MAX_MONEY = 999999
|
|
|
|
-- .DoubleReward saturates: `sla [hl] / rl [hl] / rl [hl] / ret nc` and then
|
|
-- $ff into all three bytes, so the shift tops out at 24 bits rather than
|
|
-- wrapping. wBattleReward is three bytes, which is where the number comes
|
|
-- from -- it is NOT the money cap.
|
|
local REWARD_CAP = 0xffffff
|
|
|
|
-- constants/ram_constants.asm, wMomSavingMoney's low bits. MOM_ACTIVE_F (bit
|
|
-- 7) is deliberately outside MOM_SAVING_MONEY_MASK: it says the bank
|
|
-- conversation has happened, not that anything is being skimmed.
|
|
--
|
|
-- "All" is bits 0 AND 1 together, not MOM_SAVING_ALL_MONEY_F -- that third
|
|
-- bit is inside the mask and is never written by anything, which is why
|
|
-- WinTrainerBattle compares against `(1 << SOME) | (1 << HALF)` rather than
|
|
-- against it.
|
|
local MOM_SAVING_MONEY_MASK = 7
|
|
local MOM_SAVING_SOME, MOM_SAVING_HALF = 1, 2
|
|
local MOM_SAVING_ALL = MOM_SAVING_SOME + MOM_SAVING_HALF
|
|
|
|
-- data/items/attributes.asm: HELD_AMULET_COIN is the only held effect that
|
|
-- reaches this file. CheckAmuletCoin (engine/battle/core.asm) latches
|
|
-- wAmuletCoin when a mon holding one is SENT OUT, and nothing clears it for
|
|
-- the rest of the battle, so the coin still pays after its holder has fainted.
|
|
Prize.AMULET_COIN = "AMULET_COIN"
|
|
|
|
-- data/text/battle.asm. Declared here and formatted at the call site so
|
|
-- Strings.source is what registers them, the same way Decorations declares
|
|
-- its own five. No line markers: every battle message in this port is one
|
|
-- flowing string that Chrome.wrap breaks to the box, except SentSomeToMomText,
|
|
-- which keeps the cart's own `line`/`cont` breaks because it does not fit two
|
|
-- rows.
|
|
local GOT_MONEY = Strings.source("%s got %s%d for winning!")
|
|
-- data/text/battle.asm:179-185
|
|
local SENT_SOME = Strings.source("%s got %s%d\nfor winning!\vSent some to MOM!")
|
|
-- The half and all texts really are this short on the cart: they replace the
|
|
-- money line rather than following it, which is a quirk no Gold player can
|
|
-- see because BankOfMom only ever writes MOM_SAVING_SOME_MONEY_F.
|
|
local SENT_HALF = Strings.source("Sent half to MOM!")
|
|
local SENT_ALL = Strings.source("Sent all to MOM!")
|
|
-- BattleText_PlayerPickedUpPayDayMoney (data/text/battle.asm:3-8).
|
|
local PICKED_UP = Strings.source("%s picked up %s%d!")
|
|
|
|
-- charmap.asm: the currency glyph, the same one Chrome.money floats in front
|
|
-- of a six-digit field.
|
|
local YEN = "\xc2\xa5"
|
|
|
|
--------------------------------------------------------------------------
|
|
-- ComputeTrainerReward
|
|
--------------------------------------------------------------------------
|
|
|
|
-- hProduct is four bytes and wBattleReward takes the low two of them with a
|
|
-- zero on top, so the product is kept modulo 65536. No vanilla class can
|
|
-- reach that (255 * 100 = 25500), but a mod that raises a base reward should
|
|
-- truncate the way the cart does rather than quietly pay more.
|
|
function Prize.reward(baseMoney, level)
|
|
local base = math.floor(tonumber(baseMoney) or 0)
|
|
local lvl = math.floor(tonumber(level) or 0)
|
|
if base < 0 then base = 0 end
|
|
if lvl < 0 then lvl = 0 end
|
|
return (base * lvl) % 0x10000
|
|
end
|
|
|
|
-- The level ComputeTrainerReward would have seen: wCurPartyLevel after
|
|
-- ReadTrainerParty's loop, which is the last row it built.
|
|
function Prize.rewardLevel(party)
|
|
local last = party and party[#party]
|
|
return (last and last.level) or 0
|
|
end
|
|
|
|
local function doubleReward(value)
|
|
local doubled = (value or 0) * 2
|
|
if doubled > REWARD_CAP then return REWARD_CAP end
|
|
return doubled
|
|
end
|
|
|
|
--------------------------------------------------------------------------
|
|
-- The accounts
|
|
--------------------------------------------------------------------------
|
|
|
|
local function playerMoney(save)
|
|
local player = save and save.player
|
|
return (player and player.money) or 0
|
|
end
|
|
|
|
local function momMoney(save)
|
|
local mom = save and save.mom
|
|
return (mom and mom.savedMoney) or 0
|
|
end
|
|
|
|
-- AddBattleMoneyToAccount: a 24-bit add followed by a compare against
|
|
-- MAX_MONEY, and the overflow arm WRITES the cap rather than refusing the
|
|
-- add. The cart clamps; it does not wrap and it does not reject.
|
|
local function addToAccount(have, amount)
|
|
local total = have + amount
|
|
if total > Prize.MAX_MONEY then return Prize.MAX_MONEY end
|
|
return total
|
|
end
|
|
|
|
local function setPlayerMoney(save, value)
|
|
local player = save and save.player
|
|
if player then player.money = value end
|
|
end
|
|
|
|
local function setMomMoney(save, value)
|
|
local mom = save and save.mom
|
|
if mom then mom.savedMoney = value end
|
|
end
|
|
|
|
-- wMomSavingMoney & MOM_SAVING_MONEY_MASK. BankOfMom (engine/events/mom.asm)
|
|
-- only ever stores (1 << MOM_ACTIVE_F) or that plus (1 <<
|
|
-- MOM_SAVING_SOME_MONEY_F), so in Gold the masked byte is 0 or 1 and nothing
|
|
-- else -- which is why `savingMoney` is a boolean on this save rather than a
|
|
-- number. MOM_SAVING_HALF / _ALL are kept below anyway because the split
|
|
-- loop reads them and a Crystal-shaped save would set them.
|
|
function Prize.savingMode(save)
|
|
local mom = save and save.mom
|
|
if not (mom and mom.active and mom.savingMoney) then return 0 end
|
|
if type(mom.savingMoney) == "number" then
|
|
return mom.savingMoney % (MOM_SAVING_MONEY_MASK + 1)
|
|
end
|
|
return MOM_SAVING_SOME
|
|
end
|
|
|
|
-- `ld b, a` then the two loops: b quarters to Mom, 4 - b to the wallet. The
|
|
-- `cp (1 << SOME) | (1 << HALF) / inc a` is what turns the setting into a
|
|
-- count -- 3 means ALL, which is four quarters, not three. A masked byte of
|
|
-- 4 or more is not a value anything writes, and the cart's own text lookup
|
|
-- would run off the end of .SentToMomTexts for one, so it is read as nothing
|
|
-- rather than guessed at.
|
|
local function quartersToMom(mode)
|
|
if mode == MOM_SAVING_ALL then return 4 end
|
|
if mode == MOM_SAVING_HALF then return 2 end
|
|
if mode == MOM_SAVING_SOME then return 1 end
|
|
return 0
|
|
end
|
|
|
|
Prize.QUARTERS = 4
|
|
|
|
--------------------------------------------------------------------------
|
|
-- WinTrainerBattle
|
|
--------------------------------------------------------------------------
|
|
|
|
-- opts:
|
|
-- baseMoney the class's TRNATTR_BASE_REWARD (Trainers.lookup's baseMoney)
|
|
-- level wCurPartyLevel, i.e. Prize.rewardLevel(the trainer's party)
|
|
-- amuletCoin wAmuletCoin, latched by CheckAmuletCoin
|
|
--
|
|
-- Returns a record of what happened, which is what the caller turns into the
|
|
-- message: `total` is the figure the text prints (the quarter doubled twice),
|
|
-- `toMom` is how many of the four quarters Mom took, and `mode` is the
|
|
-- wMomSavingMoney setting the text is chosen by.
|
|
function Prize.award(save, opts)
|
|
opts = opts or {}
|
|
local quarter = Prize.reward(opts.baseMoney, opts.level)
|
|
-- `ld a, [wAmuletCoin] / and a / call nz, .DoubleReward` -- before the
|
|
-- split, so Mom's cut doubles with everything else.
|
|
if opts.amuletCoin then quarter = doubleReward(quarter) end
|
|
|
|
-- .CheckMaxedOutMomMoney: carry means wMomsMoney is BELOW the cap. With no
|
|
-- carry the whole reward goes to the wallet and the text is .KeepItAll,
|
|
-- however the savings setting is left -- Mom stops skimming once she is
|
|
-- full rather than throwing the quarter away.
|
|
local mode = 0
|
|
if momMoney(save) < Prize.MAX_MONEY then mode = Prize.savingMode(save) end
|
|
local toMom = quartersToMom(mode)
|
|
|
|
local wallet, saved = playerMoney(save), momMoney(save)
|
|
for _ = 1, toMom do saved = addToAccount(saved, quarter) end
|
|
for _ = 1, Prize.QUARTERS - toMom do wallet = addToAccount(wallet, quarter) end
|
|
setPlayerMoney(save, wallet)
|
|
setMomMoney(save, saved)
|
|
|
|
return {
|
|
quarter = quarter,
|
|
total = doubleReward(doubleReward(quarter)),
|
|
toMom = toMom,
|
|
mode = mode,
|
|
wallet = wallet,
|
|
saved = saved,
|
|
}
|
|
end
|
|
|
|
-- The line StdBattleTextbox prints, chosen by .SentToMomTexts / .KeepItAll.
|
|
function Prize.message(award, playerName)
|
|
local name = playerName or "PLAYER"
|
|
local total = (award and award.total) or 0
|
|
local mode = (award and award.mode) or 0
|
|
if mode == MOM_SAVING_ALL then return Strings(SENT_ALL) end
|
|
if mode == MOM_SAVING_HALF then return Strings(SENT_HALF) end
|
|
if mode == MOM_SAVING_SOME then
|
|
return Strings(SENT_SOME, name, YEN, total)
|
|
end
|
|
return Strings(GOT_MONEY, name, YEN, total)
|
|
end
|
|
|
|
-- CheckPayDay (engine/battle/core.asm:8014-8042): the Amulet Coin doubles the
|
|
-- accumulated total once, and the wallet is written directly, no Mom split.
|
|
function Prize.payDay(save, amount, amuletCoin)
|
|
if not (save and save.player) or (amount or 0) <= 0 then return nil end
|
|
if amuletCoin then amount = amount * 2 end
|
|
setPlayerMoney(save, addToAccount(playerMoney(save), amount))
|
|
return amount
|
|
end
|
|
|
|
function Prize.payDayMessage(amount, playerName)
|
|
return Strings(PICKED_UP, playerName or "PLAYER", YEN, amount or 0)
|
|
end
|
|
|
|
return Prize
|