G2 support

This commit is contained in:
bryanthaboi
2026-08-11 11:52:56 -04:00
parent 79ed37699e
commit ae6cac89e1
489 changed files with 226677 additions and 1798 deletions
+165
View File
@@ -0,0 +1,165 @@
-- The in-game trades (engine/events/npc_trade.asm, data/events/npc_trades.asm).
--
-- Six of them, one per NPC_TRADE_* constant, reached by the `trade` script
-- command. Each row names the mon the NPC wants, the mon it hands over, and
-- everything that mon arrives wearing: its nickname, its DVs, its held item,
-- its original trainer's name and ID, and which gender of the requested mon it
-- will accept.
--
-- love-free: the conversation is src/ui/gen2/TradeMenu.lua, this is the rules.
--
-- Facts worth keeping:
--
-- * NPCTRADE_GIVEMON is what YOU hand over and NPCTRADE_GETMON what you get,
-- which is the opposite way round from the macro's own argument comment
-- ("requested mon, offered mon"). GetTradeAttr reads them by name, so the
-- comment is the only thing that is backwards.
-- * The row's DVs are TWO RAW BYTES, not a number: attack/defense in the
-- high and low nibbles of the first, speed/special of the second. The
-- mon's gender and shininess fall straight out of them, which is why every
-- one of these trades hands over the same mon to every player.
-- * The OT ID is stored little-endian in the table and byte-swapped into the
-- party struct (Trade_CopyTwoBytesReverseEndian), so the number the table
-- holds IS the ID the player sees.
-- * `trade` writes no wScriptVar. Every outcome -- the refusal, the wrong
-- mon, the completed trade -- prints its line and returns, and the script
-- after it carries on either way.
-- * The trade is one-shot, tracked in wTradeFlags by the trade's own id. A
-- second visit prints TRADE_DIALOG_AFTER and nothing else, which is the
-- check that happens BEFORE the intro line.
-- * ComputeNPCTrademonStats runs at the END, on the mon that just landed in
-- the last party slot: the received mon keeps the LEVEL of the one handed
-- over and recomputes its stats from the new species' bases.
local Mail = require("src.core.gen2.Mail")
local Mon = require("src.battle.gen2.Mon")
local NpcTrade = {}
-- constants/npc_trade_constants.asm
NpcTrade.NUM_NPC_TRADES = 6
NpcTrade.TRADE_GENDER_EITHER = "TRADE_GENDER_EITHER"
NpcTrade.TRADE_GENDER_MALE = "TRADE_GENDER_MALE"
NpcTrade.TRADE_GENDER_FEMALE = "TRADE_GENDER_FEMALE"
-- The outcomes, which are also the TRADE_DIALOG_* rows PrintTradeText picks.
NpcTrade.DIALOG_INTRO = "TRADE_DIALOG_INTRO"
NpcTrade.DIALOG_CANCEL = "TRADE_DIALOG_CANCEL"
NpcTrade.DIALOG_WRONG = "TRADE_DIALOG_WRONG"
NpcTrade.DIALOG_COMPLETE = "TRADE_DIALOG_COMPLETE"
NpcTrade.DIALOG_AFTER = "TRADE_DIALOG_AFTER"
-- data/generated/events.lua `trades`, 1-based over the 0-based NPC_TRADE_*.
function NpcTrade.row(eventTables, id)
local rows = type(eventTables) == "table" and eventTables.trades
if type(rows) ~= "table" then return nil end
return rows[(tonumber(id) or 0) + 1]
end
-- wTradeFlags, a bit per trade id. Save-side it is a plain set.
function NpcTrade.done(save, id)
local flags = save and save.tradeFlags
return (flags and flags[tonumber(id) or -1]) == true
end
function NpcTrade.markDone(save, id)
if not save then return end
save.tradeFlags = save.tradeFlags or {}
save.tradeFlags[tonumber(id) or 0] = true
end
-- The row's two DV bytes as the port's named-DV table. `dn attack, defense`
-- then `dn speed, special` -- the same packing wild mons use.
function NpcTrade.dvs(row)
local raw = (row and row.dvs) or {}
local dvs = {
attack = math.floor((raw[1] or 0) / 16),
defense = (raw[1] or 0) % 16,
speed = math.floor((raw[2] or 0) / 16),
special = (raw[2] or 0) % 16,
}
dvs.hp = Mon.hpDV(dvs)
return dvs
end
-- NPCTRADE_ITEM is an item id BYTE (data/events/npc_trades.asm's `db \5, \6,
-- \7` tail), and DoNPCTrade copies that byte straight into wPartyMon1Item of
-- the last party slot, so the received mon wears it like any other held item.
-- Everywhere else in this port a held item is a KEY of data/generated/items.lua
-- -- wild base data, trainer party mons and `givepokemail` are named at
-- extraction, and `givepoke` names its own byte at runtime through World's
-- itemByIndex -- so the byte is named here too and nothing downstream has to
-- know the row is raw. A row item of 0 is NO_ITEM. A cache that already
-- carries the name passes straight through.
function NpcTrade.item(data, row)
local raw = row and row.item
if raw == nil or raw == 0 then return nil end
if type(raw) == "string" then return raw end
local items = data and data.items
if type(items) == "table" then
for id, def in pairs(items) do
if type(def) == "table" and def.index == raw then return id end
end
end
local order = data and data.constants and data.constants.itemOrder
return (order and order[raw]) or nil
end
-- CheckTradeGender. EITHER takes anything; the other two run GetGender on the
-- mon the player picked and refuse on a mismatch. A genderless species
-- ("unknown") satisfies neither, which is the `jr nz` / `jr z` pair falling to
-- .not_matching.
function NpcTrade.genderOk(row, mon)
local want = row and row.gender
if not want or want == NpcTrade.TRADE_GENDER_EITHER then return true end
local gender = mon and mon.gender
if want == NpcTrade.TRADE_GENDER_MALE then return gender == "male" end
return gender == "female"
end
-- The three refusals NPCTrade checks in order, before any swap happens.
-- Answers the TRADE_DIALOG_* the conversation should print, or nil for "go
-- ahead".
function NpcTrade.check(row, mon)
if not row then return NpcTrade.DIALOG_CANCEL end
if not mon then return NpcTrade.DIALOG_CANCEL end
if mon.species ~= row.give then return NpcTrade.DIALOG_WRONG end
if not NpcTrade.genderOk(row, mon) then return NpcTrade.DIALOG_WRONG end
return nil
end
-- DoNPCTrade: the mon at `index` leaves the party and the row's mon takes the
-- last slot, at the SAME level, with the row's DVs, nickname, held item, OT
-- name and OT ID. Answers the two mons, given away first.
--
-- RemoveMonFromPartyOrBox runs before TryAddMonToParty, so the incoming mon
-- lands in the slot vacated by the outgoing one only when that was the last
-- slot -- otherwise the party closes up and the new mon goes on the end. That
-- reordering is visible in the party list, so it is reproduced rather than
-- tidied into an in-place swap.
function NpcTrade.perform(data, save, row, index)
local party = save and save.party
local given = party and party[index]
if not (data and given and row) then return nil end
local received = Mon.new(data, row.get, given.level, {
dvs = NpcTrade.dvs(row),
nickname = row.nickname,
item = NpcTrade.item(data, row),
})
if not received then return nil end
-- `ot` is what Breeding reads and `otName` what the summary screen prints;
-- both are set rather than picking one, because the two halves of the port
-- already disagree and a traded mon has to answer both.
received.ot, received.otName = row.otName, row.otName
received.otId = row.otId
table.remove(party, index)
-- RemoveMonFromPartyOrBox's "Mail time!" tail. NPCTrade itself has no mail
-- check -- unlike the Day-Care and the PC, it will trade a mon holding a
-- letter away -- so the shift here is what stops the mon that closes up into
-- that slot inheriting it (src/core/gen2/Mail.lua).
Mail.removeSlot(save, index)
party[#party + 1] = received
return given, received
end
return NpcTrade