mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-25 23:11:15 +02:00
ca4d3d283c
Crystal boots from a user-supplied ROM, imports a full cache and is playable: copyright, the Crystal intro movie, the animated title, gender select, Oak, and out into Johto. 122 of the cart's 169 script specials are implemented. Import and data - tools/make_crystal_manifest.py derives the manifest by importing make_gold_manifest as a library, with three additive keyword seams. Gold and Silver still regenerate byte-identical, which is the standing requirement for touching that generator. - crystal_symbol_deltas.py and crystal_movie_symbols.py carry the symbol delta: Crystal renames the credits mons, splits the trainer card, Pokegear and pack-pal blocks by gender, and replaces the intro and title outright. - Crystal-only manifest keys: engineFlagOrder (162 flags to Gold's 93, so the badge block sits one higher) and unownCharmap (the main charmap parser stops at the first newcharmap so the two cannot contaminate each other). Extractor - RomExtractorGen2 becomes three-edition. Crystal corrections: PAL_MAP_BANK 0x13, a flat PICS_FIX pic bank, audio bank 0x5e, the mapSongs id-100 hole, seven NPC trades, a TradeTexts stride of 8, the five Crystal tileset anim steps with per-row degrade, and the column-major trainer card portraits. - New: animated front sprites (frames, bitmasks, play and idle scripts), the Battle Tower roster, Kris assets, Mobile System GB art, and the Crystal intro and title via src/import/CrystalMovie.lua. Engine - GameVersion gains engine(id) and fixes(id). Gold and Silver keep their original bugs where the bug is not hardware dependent; Crystal gets the fixes Crystal shipped: Lucky Number boxes 10-14, surfing onto an NPC, and the Reflect and Light Screen defence overflow. - Crystal story: Suicune and Eusine, Celebi behind the GS Ball flag, the Ruins of Alph chambers, Buena, the Move Tutor, the Poke Seer, and the Battle Tower including the wInBattleTowerBattle badge-boost guard. - Kris and the gender flag, animated fronts in battle and the summary screen, and mon caught data. Verification - Every extracted asset is pixel-compared against pret's own source PNGs. - Gold caches are byte-identical before and after, file for file. - New Crystal suites plus a T2 Gen 2 tier; the full suite passes.
517 lines
22 KiB
Lua
517 lines
22 KiB
Lua
-- Gen 2 evolution: which species a party member turns into, whether its
|
|
-- condition is met right now, and what the party record becomes afterwards.
|
|
--
|
|
-- Love-free on purpose, the same way src/core/gen2/Boxes.lua is: every
|
|
-- question a screen asks here is table math over data/generated/pokemon.lua's
|
|
-- `evolutions` rows, so tests/gen2_evolution_test.lua can drive a whole
|
|
-- evolution with no window. src/ui/gen2/EvolutionAnim.lua is the only half
|
|
-- that draws.
|
|
--
|
|
-- Ported from engine/pokemon/evolve.asm:
|
|
-- EvolveAfterBattle the master loop over the party, one flagged
|
|
-- slot at a time, and the condition walk inside
|
|
-- each species' EvosAttacks rows
|
|
-- UpdateSpeciesNameIfNotNicknamed the nickname keeps only if it is a real
|
|
-- nickname and not the old species' own name
|
|
-- LearnLevelMoves the new species' moves for the level it is
|
|
-- already at, run right after the pic changes
|
|
-- and the frame counts of engine/movie/evolution_animation.asm, which live
|
|
-- here rather than in the screen so the schedule is assertable.
|
|
--
|
|
-- What flags a slot: engine/battle/core.asm sets wEvolvableFlags for a mon the
|
|
-- moment it levels up (right after its LearnLevelMoves run), and ExitBattle
|
|
-- calls EvolveAfterBattle only on a win. src/ui/gen2/BattleState.lua keeps
|
|
-- that flag set from the battle's own `level` events.
|
|
--
|
|
-- Everything a party member becomes is built by src/battle/gen2/Mon.lua and
|
|
-- nothing else: Evolution.apply recomputes stats through Mon.stats and rebuilds
|
|
-- the record through Mon.new, so an evolved mon can never end up with the
|
|
-- half-filled shape a second builder would hand back.
|
|
|
|
local Mon = require("src.battle.gen2.Mon")
|
|
local Runtime = require("src.mods.Runtime")
|
|
|
|
local Evolution = {}
|
|
|
|
-- constants/pokemon_data_constants.asm, as the extractor spells them into
|
|
-- pokemon.lua's `evolutions` rows.
|
|
Evolution.LEVEL = "EVOLVE_LEVEL"
|
|
Evolution.ITEM = "EVOLVE_ITEM"
|
|
Evolution.TRADE = "EVOLVE_TRADE"
|
|
Evolution.HAPPINESS = "EVOLVE_HAPPINESS"
|
|
Evolution.STAT = "EVOLVE_STAT"
|
|
|
|
-- HAPPINESS_TO_EVOLVE EQU 220.
|
|
Evolution.HAPPINESS_TO_EVOLVE = 220
|
|
|
|
-- IsMonHoldingEverstone: one item id, checked before LEVEL, HAPPINESS, STAT
|
|
-- and TRADE. It is deliberately NOT checked on the ITEM path -- .item in
|
|
-- EvolveAfterBattle never calls it -- which is why a stone still works on a
|
|
-- mon holding an Everstone in Gen 2.
|
|
Evolution.EVERSTONE = "EVERSTONE"
|
|
|
|
-- EVOLVE_HAPPINESS triggers (TR_ANYTIME / TR_MORNDAY / TR_NITE). A row with
|
|
-- no `time` is TR_ANYTIME, the first constant.
|
|
Evolution.ANYTIME = "ANYTIME"
|
|
Evolution.MORNDAY = "MORNDAY"
|
|
Evolution.NITE = "NITE"
|
|
|
|
-- EVOLVE_STAT comparisons (ATK_GT_DEF / ATK_LT_DEF / ATK_EQ_DEF).
|
|
Evolution.ATK_GT_DEF = "ATK_GT_DEF"
|
|
Evolution.ATK_LT_DEF = "ATK_LT_DEF"
|
|
Evolution.ATK_EQ_DEF = "ATK_EQ_DEF"
|
|
|
|
--------------------------------------------------------------------------
|
|
-- Conditions
|
|
--------------------------------------------------------------------------
|
|
|
|
function Evolution.holdsEverstone(mon)
|
|
return (mon and mon.item) == Evolution.EVERSTONE
|
|
end
|
|
|
|
-- .got_tyrogue_evo: CompareBytes over wTempMonAttack vs wTempMonDefense, so
|
|
-- the comparison is on the mon's CURRENT stats, not its base stats or DVs.
|
|
function Evolution.statComparison(mon)
|
|
local stats = (mon and mon.stats) or {}
|
|
local attack, defense = stats.attack or 0, stats.defense or 0
|
|
if attack == defense then return Evolution.ATK_EQ_DEF end
|
|
if attack < defense then return Evolution.ATK_LT_DEF end
|
|
return Evolution.ATK_GT_DEF
|
|
end
|
|
|
|
-- wTimeOfDay is compared against NITE_F and nothing else, so every daytime
|
|
-- that is not night reads the same to a happiness evolution.
|
|
local function isNight(timeOfDay)
|
|
return timeOfDay == Evolution.NITE or timeOfDay == "NITE_F"
|
|
end
|
|
|
|
-- One EvosAttacks row against one mon. Returns true, or false plus the short
|
|
-- reason the row was skipped (for tests and the driver; the cart just falls
|
|
-- through to .dont_evolve_N).
|
|
--
|
|
-- `ctx` is the state EvolveAfterBattle reads out of WRAM:
|
|
-- link wLinkMode ~= 0 (a trade is in progress)
|
|
-- timeCapsule wLinkMode == LINK_TIMECAPSULE
|
|
-- force wForceEvolution ~= 0 (an evolution stone was just used),
|
|
-- which is what gates the ITEM path ON and every other
|
|
-- non-trade path OFF
|
|
-- item wCurItem, the stone being used
|
|
-- timeOfDay wTimeOfDay, one of MORN / DAY / NITE / DARK
|
|
-- One record per EvosAttacks method, in the shape src/mods/Schemas.lua's
|
|
-- `evolution_methods` registry validates. Same registry NAME Gen 1 fills from
|
|
-- src/pokemon/Evolution.lua, because a mod that adds a way to evolve should not
|
|
-- have to learn a second noun -- only the ids differ, and they have to: Gold's
|
|
-- extractor writes EVOLVE_LEVEL where Red's writes LEVEL.
|
|
--
|
|
-- `check` is the schema's required field and keeps its Gen 1 job of answering
|
|
-- "does this row fire right now"; the signature is Gold's own
|
|
-- fn(entry, mon, ctx) -> ok, reason, consumesHeldItem, because Gold reads an
|
|
-- EvosAttacks row and a party record rather than Gen 1's game/mon/evo/trigger.
|
|
--
|
|
-- Two fields Gen 2 adds rather than renaming anything, and both exist because
|
|
-- EvolveAfterBattle's two cross-cutting gates are per-method:
|
|
--
|
|
-- requiresLink EVOLVE_TRADE, the one method a link ENABLES rather than
|
|
-- blocks, so it is tested ahead of the link gate
|
|
-- requiresForce EVOLVE_ITEM, which only ever fires from a stone's use, so
|
|
-- wForceEvolution does not block it the way it blocks the rest
|
|
Evolution.METHODS = {
|
|
[Evolution.TRADE] = {
|
|
requiresLink = true,
|
|
check = function(entry, mon, ctx)
|
|
if not ctx.link then return false, "not trading" end
|
|
if Evolution.holdsEverstone(mon) then return false, "everstone" end
|
|
-- `ld a, [hli] / ld b, a / inc a / jr z, .proceed`: $ff (the extractor
|
|
-- writes that as no item at all) means any trade will do.
|
|
if entry.item then
|
|
if ctx.timeCapsule then return false, "time capsule" end
|
|
if (mon and mon.item) ~= entry.item then return false, "wrong item" end
|
|
-- The held item is consumed by the trade evolution.
|
|
return true, nil, true
|
|
end
|
|
return true
|
|
end,
|
|
},
|
|
[Evolution.ITEM] = {
|
|
requiresForce = true,
|
|
check = function(entry, _, ctx)
|
|
if entry.item and ctx.item ~= entry.item then
|
|
return false, "wrong item"
|
|
end
|
|
-- .item's own `ld a, [wForceEvolution] / and a / jp z, .dont_evolve_3`:
|
|
-- a stone evolution only ever fires from the item's use, never from the
|
|
-- after-battle sweep.
|
|
if not ctx.force then return false, "not forced" end
|
|
return true
|
|
end,
|
|
},
|
|
[Evolution.LEVEL] = {
|
|
check = function(entry, mon, _)
|
|
if ((mon and mon.level) or 1) < (entry.level or 0) then
|
|
return false, "level"
|
|
end
|
|
if Evolution.holdsEverstone(mon) then return false, "everstone" end
|
|
return true
|
|
end,
|
|
},
|
|
[Evolution.HAPPINESS] = {
|
|
check = function(entry, mon, ctx)
|
|
if ((mon and mon.happiness) or 0) < Evolution.HAPPINESS_TO_EVOLVE then
|
|
return false, "happiness"
|
|
end
|
|
if Evolution.holdsEverstone(mon) then return false, "everstone" end
|
|
local trigger = entry.time or Evolution.ANYTIME
|
|
if trigger == Evolution.NITE and not isNight(ctx.timeOfDay) then
|
|
return false, "daytime"
|
|
end
|
|
if trigger == Evolution.MORNDAY and isNight(ctx.timeOfDay) then
|
|
return false, "night"
|
|
end
|
|
return true
|
|
end,
|
|
},
|
|
[Evolution.STAT] = {
|
|
check = function(entry, mon, _)
|
|
if ((mon and mon.level) or 1) < (entry.level or 0) then
|
|
return false, "level"
|
|
end
|
|
if Evolution.holdsEverstone(mon) then return false, "everstone" end
|
|
if entry.comparison ~= Evolution.statComparison(mon) then
|
|
return false, "stats"
|
|
end
|
|
return true
|
|
end,
|
|
},
|
|
}
|
|
|
|
-- vanilla registrations, engine-owned (Schemas.ENGINE), so a mod's register of
|
|
-- one of these ids collides the way it does on Red and has to say override
|
|
function Evolution.registerInto(registry, _, owner)
|
|
for id, record in pairs(Evolution.METHODS) do
|
|
registry:register(id, record, owner)
|
|
end
|
|
end
|
|
|
|
-- the merged `evolution_methods` record for a method id, the module's own when
|
|
-- no loader ran (src/pokemon/Evolution.lua:pendingFor is the Gen 1 twin)
|
|
function Evolution.methodFor(data, method)
|
|
if method == nil then return nil end
|
|
local merged = data and data.gen2EvolutionMethods
|
|
return (merged and merged[method]) or Evolution.METHODS[method]
|
|
end
|
|
|
|
function Evolution.rowMatches(entry, mon, ctx, data)
|
|
ctx = ctx or {}
|
|
if not (entry and entry.method and entry.into) then return false, "empty" end
|
|
local record = Evolution.methodFor(data, entry.method)
|
|
local check = record and record.check
|
|
|
|
-- The two cross-cutting gates in EvolveAfterBattle's own order, with each
|
|
-- method's exemption tested just ahead of the gate it is exempt from -- so
|
|
-- an unknown method still reports "linked" or "forced" first, exactly as the
|
|
-- if-chain this replaced did.
|
|
--
|
|
-- .trade runs BEFORE the link check, because it is the one method that
|
|
-- requires a link rather than being blocked by one.
|
|
if check and record.requiresLink then return check(entry, mon, ctx) end
|
|
-- `ld a, [wLinkMode] / and a / jp nz, .dont_evolve_2`: nothing else fires
|
|
-- while a link is up.
|
|
if ctx.link then return false, "linked" end
|
|
-- .item runs before the force check for the mirror reason: a stone
|
|
-- evolution only ever fires WITH wForceEvolution set.
|
|
if check and record.requiresForce then return check(entry, mon, ctx) end
|
|
-- Everything else is blocked once wForceEvolution is set, so using a stone
|
|
-- cannot also trip a level or happiness evolution on the same mon.
|
|
if ctx.force then return false, "forced" end
|
|
|
|
if not check then return false, "unknown method" end
|
|
return check(entry, mon, ctx)
|
|
end
|
|
|
|
-- The first row of `def.evolutions` that fires, walked in EvosAttacks order
|
|
-- exactly the way .loop does -- the order in the ROM is the tiebreak, which is
|
|
-- why Poliwhirl's WATER_STONE row beats its KING'S ROCK trade row.
|
|
--
|
|
-- Returns entry, consumesHeldItem.
|
|
--
|
|
-- Each row's decision is wrapped by the evolution.check hook so a mod can
|
|
-- cancel or force any evolution. The contract is the Gen 1 one verbatim
|
|
-- (src/pokemon/Evolution.lua:pendingFor): four arguments, and the chain
|
|
-- returns ONE boolean. Positions 2, 3 and 4 carry the same things in both
|
|
-- games -- the mon, the evolutions[] row, the trigger -- so a wrap written
|
|
-- once serves both. Position 1 is Gen 1's `game`, which does not exist this
|
|
-- deep in Gold; it carries `data` here, which is the object a listener would
|
|
-- reach through game.data anyway.
|
|
--
|
|
-- rowMatches is a pure predicate, so running it as the chain's vanilla costs
|
|
-- nothing when a mod skips it. `consumes` (the trade row that eats its held
|
|
-- item) rides an upvalue rather than a second return, because a Gen 1 mod
|
|
-- returns a bare boolean and would otherwise silently clear it; a mod that
|
|
-- forces an evolution without calling next() therefore gets consumes = false,
|
|
-- which is the safe direction -- an item not eaten, never one eaten twice.
|
|
function Evolution.check(def, mon, ctx, data)
|
|
local hooked = Runtime.wantsHook("evolution.check")
|
|
for _, entry in ipairs((def and def.evolutions) or {}) do
|
|
local consumes = false
|
|
local function vanilla()
|
|
local matched, _, eats = Evolution.rowMatches(entry, mon, ctx, data)
|
|
consumes = eats or false
|
|
return matched and true or false
|
|
end
|
|
local ok
|
|
if hooked then
|
|
ok = Runtime.call("evolution.check", vanilla, data, mon, entry, ctx)
|
|
else
|
|
ok = vanilla()
|
|
end
|
|
if ok then return entry, consumes end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
-- The same, looking the species up in pokemon.lua for the caller. `data`
|
|
-- carries on to rowMatches, which is where the merged evolution_methods
|
|
-- registry is read.
|
|
function Evolution.checkMon(data, mon, ctx)
|
|
local def = data and data.pokemon and mon and data.pokemon[mon.species]
|
|
if not def then return nil end
|
|
return Evolution.check(def, mon, ctx, data)
|
|
end
|
|
|
|
-- ExitBattle's gate: the sweep runs only when `wBattleResult & $f` is WIN, so
|
|
-- a loss (and the whiteout that follows it) never evolves anything.
|
|
function Evolution.runsAfterBattle(outcome)
|
|
return outcome ~= "lose" and outcome ~= "draw"
|
|
end
|
|
|
|
-- EvolveAfterBattle_MasterLoop: the flagged party slots in party order, each
|
|
-- with the row that will fire. `flags` is a set of party indices, matching
|
|
-- wEvolvableFlags; nil means every slot is eligible (the item path, which sets
|
|
-- the flag for wCurPartyMon only, passes a single-entry set).
|
|
function Evolution.plan(data, party, flags, ctx)
|
|
local out = {}
|
|
for index, mon in ipairs(party or {}) do
|
|
if not flags or flags[index] then
|
|
local entry, consumes = Evolution.checkMon(data, mon, ctx)
|
|
if entry then
|
|
out[#out + 1] = {
|
|
index = index,
|
|
mon = mon,
|
|
entry = entry,
|
|
into = entry.into,
|
|
consumesHeldItem = consumes,
|
|
}
|
|
end
|
|
end
|
|
end
|
|
return out
|
|
end
|
|
|
|
--------------------------------------------------------------------------
|
|
-- Applying it
|
|
--------------------------------------------------------------------------
|
|
|
|
-- The species' display name, which is what the nickname is compared against.
|
|
function Evolution.speciesName(data, species)
|
|
local def = data and data.pokemon and data.pokemon[species]
|
|
return (def and def.name) or species
|
|
end
|
|
|
|
-- UpdateSpeciesNameIfNotNicknamed: wStringBuffer2 (the nickname captured
|
|
-- before the animation) is compared byte for byte against the OLD species'
|
|
-- name, and only a mon whose "nickname" is not that name keeps it. The port
|
|
-- stores nil for an un-nicknamed mon, so both shapes have to read as "no
|
|
-- nickname" here.
|
|
function Evolution.keptNickname(data, mon)
|
|
local nickname = mon and mon.nickname
|
|
if not nickname or nickname == "" then return nil end
|
|
if nickname == Evolution.speciesName(data, mon.species) then return nil end
|
|
return nickname
|
|
end
|
|
|
|
-- LearnLevelMoves at wCurPartyLevel: the NEW species' level-up moves whose
|
|
-- level is EXACTLY the level the mon is already at (`cp b / jr nz`, not a
|
|
-- range), skipping any it already knows. An evolution at level 16 therefore
|
|
-- teaches only the moves the new species learns at 16 -- everything it "should"
|
|
-- have learned earlier stays unlearned, which is the cart's behaviour.
|
|
function Evolution.learnedOnEvolve(data, species, level, mon)
|
|
local def = data and data.pokemon and data.pokemon[species]
|
|
local known = {}
|
|
for _, move in ipairs((mon and mon.moves) or {}) do known[move.id] = true end
|
|
local out = {}
|
|
for _, row in ipairs((def and def.levelMoves) or {}) do
|
|
if row.level == level and not known[row.move] then
|
|
known[row.move] = true
|
|
out[#out + 1] = row.move
|
|
end
|
|
end
|
|
return out
|
|
end
|
|
|
|
-- Every field src/battle/gen2/Mon.lua's builder writes. Evolution.apply hands
|
|
-- all of these to Mon.new (or sets them straight after) and carries only the
|
|
-- keys outside this set across, so the two files cannot drift into disagreeing
|
|
-- about who owns a party record's shape.
|
|
Evolution.MON_FIELDS = {
|
|
species = true, name = true, nickname = true, level = true,
|
|
experience = true, dvs = true, stats = true, hp = true, maxHp = true,
|
|
types = true, moves = true, item = true, status = true, happiness = true,
|
|
caughtLevel = true, shiny = true, gender = true,
|
|
caughtTime = true, caughtLocation = true, caughtByGender = true,
|
|
}
|
|
|
|
-- Turn `mon` into `entry.into`. Returns the NEW record; the caller writes it
|
|
-- back into the party slot the way `.pop de / pop hl / ld [hl], a` does.
|
|
--
|
|
-- The cart's order, and the reason each step is where it is:
|
|
-- UpdateSpeciesNameIfNotNicknamed before GetBaseData, so the comparison is
|
|
-- still against the old species' name
|
|
-- GetBaseData + CalcMonStats new stats at the SAME level and DVs
|
|
-- HP += (newMaxHP - oldMaxHP) the delta, not a refill and not a refill
|
|
-- to full: a mon that walked in at half
|
|
-- health walks out at half health plus the
|
|
-- max-HP gain
|
|
-- CopyBytes tempmon -> party slot
|
|
-- LearnLevelMoves handled by the caller so it can print
|
|
-- SetSeenAndCaughtMon Evolution.markPokedex
|
|
function Evolution.apply(data, mon, entry)
|
|
local species = entry and entry.into
|
|
local def = data and data.pokemon and species and data.pokemon[species]
|
|
if not def then return nil end
|
|
|
|
local level = mon.level or 1
|
|
-- CalcMonStats runs through the one builder, so an evolved mon's stats can
|
|
-- never disagree with a freshly built one's.
|
|
local stats = Mon.stats(def.baseStats, mon.dvs, level, mon.statExp)
|
|
local previousMax = mon.maxHp or (mon.stats and mon.stats.hp) or stats.hp
|
|
local hp = (mon.hp or previousMax) + (stats.hp - previousMax)
|
|
-- The cart does not clamp; the bound only matters for data where an
|
|
-- evolution LOSES max HP, which no shipped species does.
|
|
hp = math.max(0, math.min(stats.hp, hp))
|
|
|
|
-- `xor a / ld [wTempMonItem], a`: only the trade branch that DEMANDED a held
|
|
-- item consumes it; the `$ff` (any trade) branch jumps to .proceed with the
|
|
-- item still on. Spelled out rather than as `cond and nil or item`, which
|
|
-- would quietly evaluate to the item in both cases.
|
|
local heldItem = mon.item
|
|
if entry.method == Evolution.TRADE and entry.item then heldItem = nil end
|
|
|
|
local evolved = Mon.new(data, species, level, {
|
|
dvs = mon.dvs,
|
|
moves = mon.moves,
|
|
hp = hp,
|
|
item = heldItem,
|
|
happiness = mon.happiness,
|
|
nickname = Evolution.keptNickname(data, mon),
|
|
})
|
|
if not evolved then return nil end
|
|
|
|
-- wTempMonExp is never touched: the mon keeps the experience it walked in
|
|
-- with, so an evolution cannot push it up or down a level.
|
|
evolved.experience = mon.experience
|
|
evolved.status = mon.status
|
|
evolved.caughtLevel = mon.caughtLevel
|
|
-- Nothing between GetBaseData and the PARTYMON_STRUCT_LENGTH copy back
|
|
-- touches MON_CAUGHTDATA -- engine/pokemon/evolve.asm:291-293.
|
|
evolved.caughtTime = mon.caughtTime
|
|
evolved.caughtLocation = mon.caughtLocation
|
|
evolved.caughtByGender = mon.caughtByGender
|
|
-- Anything a future field adds to a party record (mail, pokerus) rides along
|
|
-- rather than being silently dropped. Only fields Mon.new does NOT own may
|
|
-- be carried: copying `nickname` back would undo
|
|
-- UpdateSpeciesNameIfNotNicknamed, and copying `item` back would undo the
|
|
-- trade evolution's `xor a / ld [wTempMonItem], a`.
|
|
for key, value in pairs(mon) do
|
|
if Evolution.MON_FIELDS[key] == nil then evolved[key] = value end
|
|
end
|
|
-- Same name and payload keys as the Gen 1 site (src/pokemon/Evolution.lua),
|
|
-- so one subscription covers both games. `mon` is the EVOLVED record, not
|
|
-- the one that walked in: Gen 1 emits after the species swap, and a listener
|
|
-- reading mon.species expects the new one. `via` is the method id that
|
|
-- fired (EVOLVE_LEVEL, EVOLVE_ITEM, EVOLVE_TRADE, EVOLVE_HAPPINESS, ...),
|
|
-- which is Gen 2's equivalent of Gen 1's trigger kind.
|
|
Runtime.emit("pokemon.evolved", {
|
|
mon = evolved, fromSpecies = mon.species, toSpecies = species,
|
|
via = entry.method,
|
|
})
|
|
return evolved
|
|
end
|
|
|
|
-- SetSeenAndCaughtMon: an evolution ticks the new species off as BOTH seen and
|
|
-- caught, the same pair GivePoke sets, because the mon is in the party.
|
|
function Evolution.markPokedex(save, species)
|
|
if not (save and species) then return false end
|
|
save.pokedex = save.pokedex or {}
|
|
save.pokedex.seen = save.pokedex.seen or {}
|
|
save.pokedex.caught = save.pokedex.caught or {}
|
|
save.pokedex.seen[species] = true
|
|
save.pokedex.caught[species] = true
|
|
return true
|
|
end
|
|
|
|
--------------------------------------------------------------------------
|
|
-- Animation schedule (engine/movie/evolution_animation.asm)
|
|
--------------------------------------------------------------------------
|
|
|
|
-- EvolveAfterBattle prints EvolvingText ("What? <NICK> is evolving!") and then
|
|
-- `ld c, 50 / call DelayFrames` before it clears the top 12 rows and starts
|
|
-- the animation. The text box itself is NOT cleared, so that line stays under
|
|
-- the pic for the whole animation.
|
|
Evolution.EVOLVING_FRAMES = 50
|
|
|
|
-- The old mon's cry, then MUSIC_EVOLUTION, then `ld c, 80 / call DelayFrames`
|
|
-- before the palette goes to PREDEFPAL_BLACKOUT and the flashing starts.
|
|
Evolution.MUSIC_FRAMES = 80
|
|
|
|
-- Each .ReplaceFrontpic ends in WaitBGMap, i.e. one frame per pic swap, and a
|
|
-- "flash" is two of them: the new stage's tiles, then back to the old.
|
|
Evolution.SWAP_FRAMES = 1
|
|
|
|
-- .PlayEvolvedSFX: 32 frames spawning balls of light (two every other frame,
|
|
-- 32 in all) and then `ld c, 32` more frames animating them out.
|
|
Evolution.BALL_SPAWN_FRAMES = 32
|
|
Evolution.BALL_TAIL_FRAMES = 32
|
|
|
|
-- After the animation: CongratulationsYourPokemonText, EvolvedIntoText,
|
|
-- MUSIC_NONE, SFX_CAUGHT_MON, WaitSFX, then `ld c, 40 / call DelayFrames`.
|
|
Evolution.CONGRATS_FRAMES = 40
|
|
|
|
-- `lb bc, 1, 16` then, per round, `inc b / dec c / dec c`: eight rounds of
|
|
-- "hold the old pic for c frames (watching for B), then alternate the two pics
|
|
-- b times". The hold shrinks by two frames a round while the alternation gets
|
|
-- one flash longer, which is what makes the flicker accelerate.
|
|
function Evolution.flashRounds()
|
|
local rounds = {}
|
|
local flashes, wait = 1, 16
|
|
while wait > 0 do
|
|
rounds[#rounds + 1] = { wait = wait, flashes = flashes }
|
|
flashes = flashes + 1
|
|
wait = wait - 2
|
|
end
|
|
return rounds
|
|
end
|
|
|
|
-- How many frames the flashing half of the animation takes, end to end.
|
|
function Evolution.flashFrames()
|
|
local total = 0
|
|
for _, round in ipairs(Evolution.flashRounds()) do
|
|
total = total + round.wait + round.flashes * 2 * Evolution.SWAP_FRAMES
|
|
end
|
|
return total
|
|
end
|
|
|
|
-- .GenerateBallOfLight spawns two balls on every EVEN jumptable index over the
|
|
-- 32 spawn frames, 180 degrees apart, and AnimSeq_RevealNewMon walks each one
|
|
-- out from radius $10 in steps of $08 until it passes $80.
|
|
Evolution.BALL_RADIUS_START = 0x10
|
|
Evolution.BALL_RADIUS_STEP = 0x08
|
|
Evolution.BALL_RADIUS_END = 0x80
|
|
|
|
-- depixel 9, 11 -- Y TILE FIRST -- and an OAM object draws at (x - 8, y - 16),
|
|
-- so the balls come out of (80, 56), just under the middle of the 7x7 pic box
|
|
-- at hlcoord 7, 2.
|
|
Evolution.BALL_ORIGIN_X = 11 * 8 - 8
|
|
Evolution.BALL_ORIGIN_Y = 9 * 8 - 16
|
|
|
|
return Evolution
|