mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-15 15:51:17 +02:00
df0be1cba6
The save editor now routes Gold vs RBY through a generation adapter so boxes, items, events, and maps write the right fields. The launcher layout is shuffled a bit, and patch notes can come from the updater, a packed file, or the iOS sidecar.
563 lines
23 KiB
Lua
563 lines
23 KiB
Lua
-- A Gen 2 party member: stats, moves, level-up and experience.
|
|
--
|
|
-- Separate from src/pokemon/Pokemon.lua because the struct itself changed:
|
|
-- Gen 2 splits `special` into Special Attack and Special Defense, adds a held
|
|
-- item, happiness and pokerus, and its level-up moves come from EvosAttacks
|
|
-- rather than a Gen 1 learnset table.
|
|
--
|
|
-- Stat formula is unchanged from Gen 1 (data/pokemon/base_stats + DVs):
|
|
-- stat = floor((base * 2 + DV * 2 + floor(sqrt(statExp) / 4)) * level / 100) + 5
|
|
-- HP is the same but + level + 10
|
|
-- with the Gen 2 twist that a mon's SpA and SpD share one Special DV, which is
|
|
-- why a high-Special DV raises both.
|
|
--
|
|
-- Experience curves come from data/growth_rates.asm, whose `growth_rate` macro
|
|
-- documents its own polynomial:
|
|
-- [1]/[2] * n^3 + [3] * n^2 + [4] * n - [5]
|
|
-- with a sign bit on the n^2 term. pokemon.lua carries those five numbers per
|
|
-- GROWTH_* so this needs no hardcoded table.
|
|
|
|
local Unown = require("src.core.gen2.Unown")
|
|
-- The mod event bus. pokemon.level_up and pokemon.move_learned are the SAME
|
|
-- names src/battle/Experience.lua and src/battle/BattleState.lua raise on
|
|
-- Gen 1, with the same payload keys: a mod that watches a Red party watches a
|
|
-- Gold one unchanged (docs/mod-api-gen2-compat.md).
|
|
local Runtime = require("src.mods.Runtime")
|
|
|
|
local Mon = {}
|
|
|
|
Mon.MAX_LEVEL = 100
|
|
Mon.PARTY_SIZE = 6
|
|
|
|
-- DVs are 0..15 each; Attack's low bit pair also decides gender and shininess.
|
|
Mon.MAX_DV = 15
|
|
|
|
local function rand(a, b)
|
|
if love and love.math and love.math.random then
|
|
return love.math.random(a, b)
|
|
end
|
|
return math.random(a, b)
|
|
end
|
|
|
|
function Mon.randomDVs()
|
|
return {
|
|
hp = nil, -- derived below
|
|
attack = rand(0, Mon.MAX_DV),
|
|
defense = rand(0, Mon.MAX_DV),
|
|
speed = rand(0, Mon.MAX_DV),
|
|
special = rand(0, Mon.MAX_DV),
|
|
}
|
|
end
|
|
|
|
-- The HP DV is not stored: it is the low bit of each of the other four
|
|
-- (Gen 1 and 2 both build it this way), which is why a perfect-HP mon needs
|
|
-- all four others odd.
|
|
function Mon.hpDV(dvs)
|
|
local function bit(value) return (value or 0) % 2 end
|
|
return bit(dvs.attack) * 8 + bit(dvs.defense) * 4
|
|
+ bit(dvs.speed) * 2 + bit(dvs.special)
|
|
end
|
|
|
|
local function statValue(base, dv, level, statExp)
|
|
local exp = math.floor(math.sqrt(statExp or 0) / 4)
|
|
return math.floor((((base or 1) * 2 + (dv or 0) * 2 + exp) * level) / 100) + 5
|
|
end
|
|
|
|
-- All six stats at a level. `statExp` is optional per-stat effort.
|
|
function Mon.stats(baseStats, dvs, level, statExp)
|
|
baseStats = baseStats or {}
|
|
dvs = dvs or {}
|
|
statExp = statExp or {}
|
|
-- engine/pokemon/move_mon.asm:1540
|
|
local specialDv = dvs.special
|
|
if specialDv == nil then
|
|
specialDv = dvs.specialAttack or dvs.specialDefense
|
|
end
|
|
-- engine/pokemon/move_mon.asm:1496
|
|
local hpDv = Mon.hpDV({
|
|
attack = dvs.attack, defense = dvs.defense,
|
|
speed = dvs.speed, special = specialDv,
|
|
})
|
|
local hp = math.floor((((baseStats.hp or 1) * 2 + hpDv * 2
|
|
+ math.floor(math.sqrt(statExp.hp or 0) / 4)) * level) / 100)
|
|
+ level + 10
|
|
return {
|
|
hp = hp,
|
|
attack = statValue(baseStats.attack, dvs.attack, level, statExp.attack),
|
|
defense = statValue(baseStats.defense, dvs.defense, level, statExp.defense),
|
|
speed = statValue(baseStats.speed, dvs.speed, level, statExp.speed),
|
|
-- One Special DV feeds both special stats, and so does one Special stat
|
|
-- exp: the Gen 2 party struct kept Gen 1's five exp words (macros/ram.asm
|
|
-- box_struct ends them at SpcExp), so SpA and SpD grow together. The
|
|
-- per-stat keys are still read as a fallback for a record written before
|
|
-- the shared word existed.
|
|
specialAttack = statValue(baseStats.specialAttack, specialDv, level,
|
|
statExp.special or statExp.specialAttack),
|
|
specialDefense = statValue(baseStats.specialDefense, specialDv, level,
|
|
statExp.special or statExp.specialDefense),
|
|
}
|
|
end
|
|
|
|
-- The species string is the source of truth. `mon.name` is a copy of that
|
|
-- species' display name (GetPokemonName), kept so menus can print without a
|
|
-- Data lookup. It is NOT the nickname: an un-nicknamed mon has nickname nil
|
|
-- and prints this copy. Changing species without rewriting it leaves the
|
|
-- previous species' name on the party list and the SUMMARY's top line.
|
|
function Mon.syncIdentity(mon, data)
|
|
if type(mon) ~= "table" then return mon end
|
|
local def = data and data.pokemon and data.pokemon[mon.species]
|
|
if not def then return mon end
|
|
mon.name = def.name or mon.species
|
|
if def.types then mon.types = def.types end
|
|
if mon.dvs then
|
|
mon.gender = Mon.gender(def, mon.dvs,
|
|
{ species = mon.species, level = mon.level })
|
|
mon.shiny = Mon.isShiny(mon.dvs,
|
|
{ species = mon.species, def = def, level = mon.level })
|
|
if mon.species == Unown.SPECIES then
|
|
mon.unownLetter = Unown.letterFromDVs(mon.dvs)
|
|
else
|
|
mon.unownLetter = nil
|
|
end
|
|
end
|
|
return mon
|
|
end
|
|
|
|
-- Every screen that prints a mon without a Data lookup should go through here:
|
|
-- nickname if the player set one, otherwise the species display copy `name`.
|
|
-- Skipping `name` and jumping to `species` is how a swapped mon can still
|
|
-- read as ABRA on one menu and RAYQUAZA on another.
|
|
function Mon.displayName(mon)
|
|
if type(mon) ~= "table" then return "?" end
|
|
return mon.nickname or mon.name or mon.species or "?"
|
|
end
|
|
|
|
-- Party, boxes, both Day-Care sides, and a pending egg. Editor CONTINUE and
|
|
-- hydrate have to walk the same set: leaving dayCare.man.mon on the old
|
|
-- `name` is the ABRA bug in a second closet.
|
|
function Mon.eachSaveMon(save, fn)
|
|
if type(save) ~= "table" or type(fn) ~= "function" then return end
|
|
for _, mon in ipairs(save.party or {}) do fn(mon) end
|
|
for _, box in pairs(save.boxes or {}) do
|
|
if type(box) == "table" then
|
|
for _, mon in ipairs(box) do fn(mon) end
|
|
end
|
|
end
|
|
local dc = save.dayCare
|
|
if type(dc) == "table" then
|
|
if dc.man and dc.man.mon then fn(dc.man.mon) end
|
|
if dc.lady and dc.lady.mon then fn(dc.lady.mon) end
|
|
if dc.egg then fn(dc.egg) end
|
|
end
|
|
if save.daycare and save.daycare.mon then fn(save.daycare.mon) end
|
|
end
|
|
|
|
function Mon.syncSaveIdentity(save, data)
|
|
Mon.eachSaveMon(save, function(mon) Mon.syncIdentity(mon, data) end)
|
|
end
|
|
|
|
function Mon.refreshStats(mon, data)
|
|
if type(mon) ~= "table" then return mon end
|
|
local def = data and data.pokemon and data.pokemon[mon.species]
|
|
if not (def and def.baseStats) then return mon end
|
|
Mon.syncIdentity(mon, data)
|
|
-- engine/pokemon/move_mon.asm:1402
|
|
local stats = Mon.stats(def.baseStats, mon.dvs, mon.level or 1, mon.statExp)
|
|
mon.stats = stats
|
|
mon.maxHp = stats.hp
|
|
if mon.hp == nil or mon.hp > stats.hp then
|
|
mon.hp = stats.hp
|
|
end
|
|
return mon
|
|
end
|
|
|
|
-- The five stat exp words, in struct order. There is no sixth: see Mon.stats.
|
|
Mon.STAT_EXP_ORDER = { "hp", "attack", "defense", "speed", "special" }
|
|
|
|
-- Each word is 16 bit and GiveExperiencePoints stops it at $ffff rather than
|
|
-- letting it wrap (.stat_exp_maxed_out).
|
|
Mon.MAX_STAT_EXP = 65535
|
|
|
|
function Mon.newStatExp()
|
|
return { hp = 0, attack = 0, defense = 0, speed = 0, special = 0 }
|
|
end
|
|
|
|
-- GiveExperiencePoints' .stat_exp_loop (engine/battle/core.asm): the defeated
|
|
-- mon's base stats are added to every participant's stat exp, and the loop runs
|
|
-- NUM_EXP_STATS = 5 times over a six-entry base stat block, so the Special word
|
|
-- takes the loser's Special ATTACK and the Special Defense base stat is never
|
|
-- read at all.
|
|
--
|
|
-- `.EvenlyDivideExpAmongParticipants` divides the base stats in place before
|
|
-- any of this, and only when two or more mons took part, which is why the
|
|
-- divisor is shared with Mon.experienceGain rather than computed here.
|
|
--
|
|
-- Pokerus adds the same value a SECOND time (`jr z, .stat_exp_awarded` skips
|
|
-- the second add when the byte is zero) -- doubled, not multiplied by a rate,
|
|
-- so it stacks with nothing.
|
|
-- `halved` is the EXP.SHARE tax: with any holder in the party the whole
|
|
-- wEnemyMon base stat block is `srl`'d in place before EITHER pass runs
|
|
-- (engine/battle/core.asm, the IsAnyMonHoldingExpShare block ahead of the
|
|
-- first GiveExperiencePoints call), so participants and holders both draw
|
|
-- stat exp from the halved values.
|
|
function Mon.gainStatExp(mon, loserDef, participants, doubled, halved)
|
|
if type(mon) ~= "table" then return nil end
|
|
local base = (loserDef and loserDef.baseStats) or {}
|
|
local share = math.max(1, math.floor(participants or 1))
|
|
mon.statExp = mon.statExp or Mon.newStatExp()
|
|
local gains = {}
|
|
for _, key in ipairs(Mon.STAT_EXP_ORDER) do
|
|
local from = (key == "special") and base.specialAttack or base[key]
|
|
from = from or 0
|
|
if halved then from = math.floor(from / 2) end
|
|
local gain = math.floor(from / share)
|
|
if doubled then gain = gain * 2 end
|
|
local value = (mon.statExp[key] or 0) + gain
|
|
if value > Mon.MAX_STAT_EXP then value = Mon.MAX_STAT_EXP end
|
|
mon.statExp[key] = value
|
|
gains[key] = gain
|
|
end
|
|
return gains
|
|
end
|
|
|
|
-- Total experience needed to *be* `level`, from a GROWTH_* record.
|
|
-- The single point every experience calculation resolves its curve through.
|
|
-- The merged growth_rates registry wins, then the extractor's own coefficient
|
|
-- rows on data.pokemon.growthRates, so a mod-free boot reads exactly the table
|
|
-- it always did and a boot with a registered curve reads that instead.
|
|
--
|
|
-- Both live at the same key space (GROWTH_MEDIUM_FAST and friends), and the
|
|
-- registry's Data path is the shared `growth_rates` one Gen 1 uses -- the
|
|
-- point of routing it there rather than to a gen2 table is that a mod writes
|
|
-- ONE record for both games (src/mods/Builtins.lua's Gen 2 registrant seeds
|
|
-- this registry from the coefficient rows, so the vanilla curves are Gold's
|
|
-- own).
|
|
function Mon.growthFor(data, curve)
|
|
if not curve then return nil end
|
|
local registered = data and data.growth_rates and data.growth_rates[curve]
|
|
if registered then return registered end
|
|
return data and data.pokemon and data.pokemon.growthRates
|
|
and data.pokemon.growthRates[curve]
|
|
end
|
|
|
|
-- Seeds the growth_rates registry with Gold's own curves, as records carrying
|
|
-- expForLevel so the id space and the record shape both match Gen 1's. Called
|
|
-- by src/mods/Builtins.lua under Gen 2. A dataset with no coefficient rows
|
|
-- (the ROM-free fixtures) seeds nothing rather than registering broken curves.
|
|
function Mon.registerInto(registry, data, owner)
|
|
local rows = data and data.pokemon and data.pokemon.growthRates
|
|
if type(rows) ~= "table" then return end
|
|
for curve, row in pairs(rows) do
|
|
-- the closure holds the coefficient row, so the registered record computes
|
|
-- exactly what the arm below would have
|
|
registry:register(curve, {
|
|
expForLevel = function(level) return Mon.experienceForLevel(row, level) end,
|
|
}, owner)
|
|
end
|
|
end
|
|
|
|
-- `growth` is either the extractor's coefficient row (numerator / denominator /
|
|
-- squared / linear / constant, straight off GrowthRates in the ROM) or a
|
|
-- growth_rates REGISTRY record, which carries expForLevel(level) instead --
|
|
-- the same record shape Gen 1's registry uses (src/pokemon/Growth.lua), so a
|
|
-- mod that registers a custom curve writes one record and it works in both
|
|
-- games. A registered curve wins outright; the coefficient arm is what a
|
|
-- mod-free boot and every driver still run.
|
|
function Mon.experienceForLevel(growth, level)
|
|
if growth and growth.expForLevel then
|
|
return math.max(0, math.floor(growth.expForLevel(level) or 0))
|
|
end
|
|
if not growth then return level * level * level end
|
|
local n = level
|
|
local numerator = growth.numerator or 1
|
|
local denominator = growth.denominator or 1
|
|
local value = math.floor(numerator * n * n * n / denominator)
|
|
value = value + (growth.squared or 0) * n * n
|
|
value = value + (growth.linear or 0) * n
|
|
value = value - (growth.constant or 0)
|
|
return math.max(0, value)
|
|
end
|
|
|
|
-- The level a total experience buys. Walks up rather than inverting the
|
|
-- polynomial, which the cart also does (it only ever compares against the next
|
|
-- level's threshold).
|
|
function Mon.levelForExperience(growth, experience)
|
|
local level = 1
|
|
while level < Mon.MAX_LEVEL do
|
|
if experience < Mon.experienceForLevel(growth, level + 1) then break end
|
|
level = level + 1
|
|
end
|
|
return level
|
|
end
|
|
|
|
-- The moves a species knows on arrival at `level`: its last four level-up
|
|
-- moves at or below it (EvosAttacks order, later moves pushing earlier ones
|
|
-- out, which is what makes a caught mon's moveset deterministic).
|
|
function Mon.movesAtLevel(def, level, moves)
|
|
local known = {}
|
|
for _, entry in ipairs((def and def.levelMoves) or {}) do
|
|
if entry.level <= level then
|
|
-- A move already known is not learned twice.
|
|
local duplicate = false
|
|
for _, existing in ipairs(known) do
|
|
if existing == entry.move then duplicate = true break end
|
|
end
|
|
if not duplicate then
|
|
known[#known + 1] = entry.move
|
|
if #known > 4 then table.remove(known, 1) end
|
|
end
|
|
end
|
|
end
|
|
local out = {}
|
|
for _, id in ipairs(known) do
|
|
local moveDef = moves and moves[id]
|
|
out[#out + 1] = {
|
|
id = id,
|
|
pp = moveDef and moveDef.pp or 0,
|
|
maxPp = moveDef and moveDef.pp or 0,
|
|
}
|
|
end
|
|
return out
|
|
end
|
|
|
|
-- Build a party member. `data` needs `pokemon` and `moves`; growth records
|
|
-- live on data.pokemon.growthRates (written by the extractor).
|
|
function Mon.new(data, species, level, opts)
|
|
opts = opts or {}
|
|
local def = data and data.pokemon and data.pokemon[species]
|
|
if not def then return nil end
|
|
level = math.max(1, math.min(Mon.MAX_LEVEL, level or 5))
|
|
local dvs = opts.dvs or Mon.randomDVs()
|
|
dvs.hp = Mon.hpDV(dvs)
|
|
local statExp = opts.statExp or Mon.newStatExp()
|
|
local stats = Mon.stats(def.baseStats, dvs, level, statExp)
|
|
local growth = Mon.growthFor(data, def.growthRate)
|
|
return {
|
|
species = species,
|
|
name = def.name or species,
|
|
nickname = opts.nickname,
|
|
level = level,
|
|
experience = Mon.experienceForLevel(growth, level),
|
|
dvs = dvs,
|
|
-- The five stat exp words. A wild or gift mon starts at zero: nothing in
|
|
-- the cart seeds them, MON_STAT_EXP is zeroed by _MoveMon.
|
|
statExp = statExp,
|
|
-- MON_PKRS. Zero is "never infected"; src/core/gen2/Pokerus.lua owns every
|
|
-- read and write of it after this.
|
|
pokerus = opts.pokerus or 0,
|
|
stats = stats,
|
|
hp = opts.hp or stats.hp,
|
|
maxHp = stats.hp,
|
|
types = def.types,
|
|
moves = opts.moves or Mon.movesAtLevel(def, level, data.moves),
|
|
-- Held item; wild mons roll one from BaseData's two item slots on the cart,
|
|
-- which is not modeled yet, so only scripted gifts carry one.
|
|
item = opts.item,
|
|
status = nil,
|
|
-- 70 for a caught mon, 120 for a gift/hatched one.
|
|
happiness = opts.happiness or 70,
|
|
caughtLevel = level,
|
|
-- shiny.roll / gender.roll get the species and level as context; opts.shiny
|
|
-- still wins, because a FORCED shiny battle (Red Gyarados) is the cart
|
|
-- overriding the roll rather than a roll to be hooked.
|
|
shiny = opts.shiny or Mon.isShiny(dvs,
|
|
{ species = species, def = def, level = level }),
|
|
gender = Mon.gender(def, dvs, { species = species, level = level }),
|
|
-- Unown has no gender and no shininess worth looking at, but it does have
|
|
-- a FORM, and the form is the same DVs read a different way
|
|
-- (GetUnownLetter, engine/gfx/load_pics.asm). Stamped at build time so
|
|
-- every screen that shows an Unown -- the battle pic, the box, the #DEX --
|
|
-- reads one field instead of each redoing the bit shuffle.
|
|
unownLetter = (species == Unown.SPECIES)
|
|
and Unown.letterFromDVs(dvs) or nil,
|
|
}
|
|
end
|
|
|
|
-- AddPartyMon copies wPlayerName into wPartyMonOTs and wPlayerID into MON_ID
|
|
-- (move_mon.asm:44-56, :143-149); SendMonIntoBox does the same (:970-994).
|
|
function Mon.stampOT(save, mon)
|
|
local player = save and save.player
|
|
if not (mon and player) then return mon end
|
|
if player.id == nil then player.id = rand(0, 65535) end
|
|
mon.ot = mon.ot or player.name
|
|
-- NpcTrade.lua:150: `ot` is what Breeding reads, `otName` what the summary prints.
|
|
mon.otName = mon.otName or mon.ot
|
|
mon.otId = mon.otId or player.id
|
|
return mon
|
|
end
|
|
|
|
-- shiny.roll and gender.roll, two of the names Gen 2 invents: Gen 1 has
|
|
-- neither shininess nor gender in the ROM at all (Red's shiny indicator mods
|
|
-- read src/pokemon/Stats.lua's virtual pattern, which is a mod-side
|
|
-- convention, not an engine seam), so there is no Gen 1 name to share.
|
|
--
|
|
-- Both wrap the DV-derived roll rather than the mon that comes out of it,
|
|
-- because on the cart these ARE the roll: CheckShininess and GetGender read
|
|
-- the same two DV bytes LoadEnemyMon just generated, and nothing later can
|
|
-- change the answer without changing the DVs. Wrapping here means a shiny-odds
|
|
-- mod and a gender-ratio mod work on every route a mon arrives by -- a wild
|
|
-- encounter, a gift, a hatch, a trade -- because Mon.new is the one builder.
|
|
--
|
|
-- Shared ctx keys:
|
|
-- dvs the DV set being read, exactly as stored on the mon
|
|
-- species the species id, nil when the caller had none to give
|
|
-- def that species' record, nil likewise
|
|
-- level the level the mon is being built at, nil for a bare query
|
|
--
|
|
-- gender.roll's ctx carries `ratio` as well, BaseData's genderRatio byte, so a
|
|
-- mod can shift the threshold rather than restate the whole rule. A chain that
|
|
-- returns something that is not one of "male" / "female" / "unknown" is
|
|
-- ignored, because every screen that prints a gender indexes by those three.
|
|
|
|
-- Gen 2 shininess: the classic DV pattern (Speed/Defense/Special all 10, and
|
|
-- Attack in {2,3,6,7,10,11,14,15}).
|
|
function Mon.vanillaShiny(dvs)
|
|
if not dvs then return false end
|
|
if dvs.speed ~= 10 or dvs.defense ~= 10 or dvs.special ~= 10 then
|
|
return false
|
|
end
|
|
local attack = dvs.attack or 0
|
|
return attack % 4 == 2 or attack % 4 == 3
|
|
end
|
|
|
|
function Mon.isShiny(dvs, ctx)
|
|
if not Runtime.wantsHook("shiny.roll") then return Mon.vanillaShiny(dvs) end
|
|
local shiny = Runtime.call("shiny.roll", function(c)
|
|
return Mon.vanillaShiny(c.dvs)
|
|
end, { dvs = dvs, species = ctx and ctx.species, def = ctx and ctx.def,
|
|
level = ctx and ctx.level })
|
|
return shiny and true or false
|
|
end
|
|
|
|
-- Gender comes from the Attack DV against the species' ratio threshold: an
|
|
-- Attack DV *below* the threshold is female (BaseData's `db GENDER_F12_5` is
|
|
-- already scaled out of 256).
|
|
function Mon.vanillaGender(def, dvs)
|
|
local ratio = def and def.genderRatio
|
|
if not ratio then return "unknown" end
|
|
if ratio == 0xff then return "unknown" end
|
|
-- The DV is 0..15; the threshold is out of 256 in steps of 16.
|
|
local threshold = math.floor(ratio / 16)
|
|
return ((dvs and dvs.attack or 0) < threshold) and "female" or "male"
|
|
end
|
|
|
|
local GENDERS = { male = true, female = true, unknown = true }
|
|
|
|
function Mon.gender(def, dvs, ctx)
|
|
if not Runtime.wantsHook("gender.roll") then
|
|
return Mon.vanillaGender(def, dvs)
|
|
end
|
|
local gender = Runtime.call("gender.roll", function(c)
|
|
return Mon.vanillaGender(c.def, c.dvs)
|
|
end, { def = def, dvs = dvs, ratio = def and def.genderRatio,
|
|
species = (ctx and ctx.species) or (def and def.id),
|
|
level = ctx and ctx.level })
|
|
if not GENDERS[gender] then return Mon.vanillaGender(def, dvs) end
|
|
return gender
|
|
end
|
|
|
|
-- Experience for defeating `loser`, per recipient. Gen 2:
|
|
-- exp = baseExp * loserLevel / 7, split among the recipients of the pass,
|
|
-- then GiveExperiencePoints' three BoostExp arms in the cart's own order,
|
|
-- each a floored x1.5 on the running amount:
|
|
-- traded the mon's OT id differs from the player's (BoostedExpPointsText)
|
|
-- trainer a trainer battle (wBattleMode)
|
|
-- luckyEgg the mon HOLDS a LUCKY_EGG -- checked by item id, not held
|
|
-- effect, exactly as the cart's `cp LUCKY_EGG` does
|
|
-- opts.halved is the EXP.SHARE tax: any holder in the party halves the base
|
|
-- exp byte before either pass (the same `srl` block that halves stat exp).
|
|
function Mon.experienceGain(loserDef, loserLevel, participants, trainer, opts)
|
|
opts = opts or {}
|
|
local baseExp = (loserDef and loserDef.baseExp) or 0
|
|
if opts.halved then baseExp = math.floor(baseExp / 2) end
|
|
local value = math.floor(baseExp * (loserLevel or 1) / 7)
|
|
value = math.floor(value / math.max(1, participants or 1))
|
|
if opts.traded then value = math.floor(value * 3 / 2) end
|
|
if trainer then value = math.floor(value * 3 / 2) end
|
|
if opts.luckyEgg then value = math.floor(value * 3 / 2) end
|
|
return math.max(1, value)
|
|
end
|
|
|
|
-- Award experience, level up as far as it reaches, and report what happened so
|
|
-- the battle can print "grew to level N!" and offer new moves.
|
|
function Mon.gainExperience(mon, amount, data)
|
|
local def = data and data.pokemon and data.pokemon[mon.species]
|
|
local growth = Mon.growthFor(data, def and def.growthRate)
|
|
mon.experience = (mon.experience or 0) + math.max(0, amount or 0)
|
|
local before = mon.level
|
|
local capped = Mon.experienceForLevel(growth, Mon.MAX_LEVEL)
|
|
if mon.experience > capped then mon.experience = capped end
|
|
local after = Mon.levelForExperience(growth, mon.experience)
|
|
if after <= before then
|
|
return { levels = 0, learned = {} }
|
|
end
|
|
mon.level = after
|
|
-- Recompute stats and carry the HP gain, the way the cart adds the delta
|
|
-- rather than refilling.
|
|
local previousMax = mon.maxHp or (mon.stats and mon.stats.hp) or 1
|
|
mon.stats = Mon.stats(def and def.baseStats, mon.dvs, after, mon.statExp)
|
|
mon.maxHp = mon.stats.hp
|
|
mon.hp = math.min(mon.maxHp, (mon.hp or previousMax)
|
|
+ (mon.maxHp - previousMax))
|
|
|
|
-- pokemon.level_up, once per level crossed and after the stats were
|
|
-- recalculated, exactly as src/battle/Experience.lua raises it on Gen 1 --
|
|
-- a jump of three levels is three events, not one. `learnable` is the moves
|
|
-- this species learns at exactly that level, the same list Gen 1 carries.
|
|
if Runtime.wants("pokemon.level_up") then
|
|
for level = before + 1, after do
|
|
local learnable = {}
|
|
for _, entry in ipairs((def and def.levelMoves) or {}) do
|
|
if entry.level == level then learnable[#learnable + 1] = entry.move end
|
|
end
|
|
Runtime.emit("pokemon.level_up", {
|
|
mon = mon, level = level, prevLevel = level - 1, learnable = learnable,
|
|
})
|
|
end
|
|
end
|
|
|
|
-- Every level-up move between the old and new level is offered.
|
|
local learned = {}
|
|
for _, entry in ipairs((def and def.levelMoves) or {}) do
|
|
if entry.level > before and entry.level <= after then
|
|
learned[#learned + 1] = entry.move
|
|
end
|
|
end
|
|
return { levels = after - before, learned = learned, from = before, to = after }
|
|
end
|
|
|
|
-- Teach a move, or report that all four slots are full so the caller can ask
|
|
-- which to forget.
|
|
function Mon.learnMove(mon, moveId, data)
|
|
mon.moves = mon.moves or {}
|
|
for _, move in ipairs(mon.moves) do
|
|
if move.id == moveId then return false, "known" end
|
|
end
|
|
local def = data and data.moves and data.moves[moveId]
|
|
local entry = {
|
|
id = moveId,
|
|
pp = def and def.pp or 0,
|
|
maxPp = def and def.pp or 0,
|
|
}
|
|
if #mon.moves >= 4 then return false, "full", entry end
|
|
mon.moves[#mon.moves + 1] = entry
|
|
-- pokemon.move_learned, the payload BattleState:learnMove emits on Gen 1.
|
|
-- This is Gen 2's single choke point for teaching a move -- the level-up
|
|
-- award, an evolution's new move and the TM path all arrive here -- so the
|
|
-- event covers all three rather than only the battle's.
|
|
Runtime.emit("pokemon.move_learned", { mon = mon, moveId = moveId })
|
|
return true
|
|
end
|
|
|
|
-- Which evolution (if any) fires at this level.
|
|
function Mon.evolutionAtLevel(def, level)
|
|
for _, entry in ipairs((def and def.evolutions) or {}) do
|
|
if entry.method == "EVOLVE_LEVEL" and (entry.level or 0) <= level then
|
|
return entry
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
return Mon
|