Files
gen1recomp/src/battle/gen2/Prize.lua
T

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