mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-26 07:21:22 +02:00
Add Pokemon Crystal as a sixth supported version
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.
This commit is contained in:
@@ -36,6 +36,7 @@
|
||||
-- `action` names the World method that carries it out; everything else in the
|
||||
-- table is that action's argument.
|
||||
|
||||
local GameVersion = require("src.core.GameVersion")
|
||||
local Permissions = require("src.world.gen2.Permissions")
|
||||
local Runtime = require("src.mods.Runtime")
|
||||
local Strings = require("src.core.Strings")
|
||||
@@ -152,14 +153,41 @@ FieldMoves.KANTO_BADGES = {
|
||||
-- World:setEngineFlag / World:engineFlag route badge ids here so there is one
|
||||
-- store again, the same way ENGINE_BUG_CONTEST_TIMER is routed to
|
||||
-- save.bugContest rather than kept as a second copy.
|
||||
FieldMoves.BADGE_FLAG = {}
|
||||
for index, name in ipairs(FieldMoves.JOHTO_BADGES) do
|
||||
FieldMoves.BADGE_FLAG[25 + index] = { store = "badges", name = name }
|
||||
end
|
||||
for index, name in ipairs(FieldMoves.KANTO_BADGES) do
|
||||
FieldMoves.BADGE_FLAG[33 + index] = { store = "kantoBadges", name = name }
|
||||
-- Crystal declares 162 engine flags to Gold's 93 and the badge block sits one
|
||||
-- higher (constants/engine_flags.asm:39 vs pokegold's :38), so the ids come
|
||||
-- from the cache's engineFlagOrder when it has one.
|
||||
-- Crystal only; pokegold constants/engine_flags.asm:4-111 has no such row.
|
||||
FieldMoves.FEMALE_FLAG_NAME = "ENGINE_PLAYER_IS_FEMALE"
|
||||
|
||||
function FieldMoves.bindEngineFlags(order)
|
||||
local byName = {}
|
||||
if type(order) == "table" then
|
||||
-- pairs, not ipairs: a const_skip leaves a hole and ipairs would stop
|
||||
-- there, silently dropping every badge past it.
|
||||
for index, name in pairs(order) do
|
||||
if type(index) == "number" and type(name) == "string" then
|
||||
byName[name] = index - 1
|
||||
end
|
||||
end
|
||||
end
|
||||
local flags = {}
|
||||
local function place(names, store, goldBase)
|
||||
for index, name in ipairs(names) do
|
||||
local id = byName["ENGINE_" .. name .. "BADGE"] or (goldBase + index)
|
||||
flags[id] = { store = store, name = name }
|
||||
end
|
||||
end
|
||||
place(FieldMoves.JOHTO_BADGES, "badges", 25)
|
||||
place(FieldMoves.KANTO_BADGES, "kantoBadges", 33)
|
||||
FieldMoves.BADGE_FLAG = flags
|
||||
-- The flag IS wPlayerGender's bit 0, so World routes it to the gender byte
|
||||
-- (data/events/engine_flags.asm:131, constants/engine_flags.asm:121).
|
||||
FieldMoves.FEMALE_FLAG = byName[FieldMoves.FEMALE_FLAG_NAME]
|
||||
return flags
|
||||
end
|
||||
|
||||
FieldMoves.bindEngineFlags(nil)
|
||||
|
||||
function FieldMoves.hasBadge(save, badge)
|
||||
if not badge then return true end
|
||||
local owned = save and save.player and save.player.badges
|
||||
@@ -357,6 +385,39 @@ FieldMoves.STATE_SPRITE = {
|
||||
surf_pika = "SPRITE_SURFING_PIKACHU",
|
||||
}
|
||||
|
||||
-- data/sprites/player_sprites.asm:8-13 KrisStateSprites, the other half of the
|
||||
-- table GetPlayerSprite picks between (engine/overworld/overworld.asm:55-64).
|
||||
FieldMoves.STATE_SPRITE_FEMALE = {
|
||||
normal = "SPRITE_KRIS",
|
||||
bike = "SPRITE_KRIS_BIKE",
|
||||
surf = "SPRITE_SURF",
|
||||
surf_pika = "SPRITE_SURFING_PIKACHU",
|
||||
}
|
||||
|
||||
-- wPlayerGender's PLAYERGENDER_FEMALE_F, as the save spells it
|
||||
-- (constants/ram_constants.asm:176-177).
|
||||
function FieldMoves.isFemale(gender)
|
||||
return gender == "female"
|
||||
end
|
||||
|
||||
-- GetPlayerSprite's table pick and row walk
|
||||
-- (engine/overworld/overworld.asm:57-64, :67-75).
|
||||
function FieldMoves.stateSprite(state, gender)
|
||||
local table_ = FieldMoves.isFemale(gender)
|
||||
and FieldMoves.STATE_SPRITE_FEMALE or FieldMoves.STATE_SPRITE
|
||||
return table_[state] or table_[FieldMoves.PLAYER_NORMAL]
|
||||
end
|
||||
|
||||
function FieldMoves.playerSprite(gender)
|
||||
return FieldMoves.stateSprite(FieldMoves.PLAYER_NORMAL, gender)
|
||||
end
|
||||
|
||||
-- Whether the cache carries Kris at all; Gold and Silver have no
|
||||
-- KrisStateSprites to extract (pokegold data/sprites/player_sprites.asm:1-6).
|
||||
function FieldMoves.hasGenderChoice(sprites)
|
||||
return (sprites and sprites[FieldMoves.STATE_SPRITE_FEMALE.normal]) ~= nil
|
||||
end
|
||||
|
||||
function FieldMoves.isBiking(state)
|
||||
return state == FieldMoves.PLAYER_BIKE
|
||||
end
|
||||
@@ -536,10 +597,14 @@ end
|
||||
-- FlashFunction.CheckUseFlash: badge, then wTimeOfDayPalset == DARKNESS_PALSET
|
||||
-- -- so FLASH is refused in a lit cave and on a route alike, and the refusal
|
||||
-- is FieldMoveFailed's generic "Can't use that here."
|
||||
--
|
||||
-- ../pokecrystal/engine/events/overworld.asm:284-287 puts SpecialAerodactylChamber
|
||||
-- between the two, and its carry is a second way into `.useflash`.
|
||||
function FieldMoves.flashFromMenu(ctx)
|
||||
local refused = badgeGate(ctx, "FLASH")
|
||||
if refused then return refused end
|
||||
if not ctx.dark then
|
||||
local chamber = ctx.openAerodactylWall and ctx.openAerodactylWall()
|
||||
if not ctx.dark and not chamber then
|
||||
return { ok = false, text = FieldMoves.TEXT.CANT_USE_HERE }
|
||||
end
|
||||
return { ok = true, action = "flash", text = FieldMoves.TEXT.BLINDING_FLASH }
|
||||
@@ -562,6 +627,11 @@ function FieldMoves.surfFromMenu(ctx)
|
||||
or FieldMoves.directionBlocked(ctx.playerColl, ctx.facing) then
|
||||
return { ok = false, text = FieldMoves.TEXT.CANT_SURF }
|
||||
end
|
||||
-- Crystal's added `farcall CheckFacingObject`, which pokegold's :339 tags
|
||||
-- BUG (../pokecrystal/engine/events/overworld.asm:364-365).
|
||||
if ctx.facingObject and GameVersion.fixes().surfOntoNpc then
|
||||
return { ok = false, text = FieldMoves.TEXT.CANT_SURF }
|
||||
end
|
||||
return {
|
||||
ok = true, action = "surf",
|
||||
state = FieldMoves.surfType(ctx.mon),
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
-- ../pokecrystal/engine/events/unown_walls.asm:102 DisplayUnownWords, and the
|
||||
-- Unown alphabet it writes out of ../pokecrystal/constants/charmap.asm:424;
|
||||
-- :1 HoOhChamber, :13 OmanyteChamber, :54 SpecialAerodactylChamber and :81
|
||||
-- SpecialKabutoChamber, the four routines that open the chambers' walls.
|
||||
|
||||
local Assets = require("src.render.Assets")
|
||||
local Font = require("src.render.Font")
|
||||
local GbcPalette = require("src.render.GbcPalette")
|
||||
local Palettes = require("src.world.gen2.Palettes")
|
||||
local Sound = require("src.core.Sound")
|
||||
|
||||
local UnownWords = {}
|
||||
UnownWords.__index = UnownWords
|
||||
UnownWords.isOpaque = false
|
||||
|
||||
-- engine/tilesets/map_palettes.asm:40, ../pokecrystal/constants/tileset_constants.asm:55
|
||||
UnownWords.BANK1 = 0x80
|
||||
UnownWords.BROWN = 6
|
||||
|
||||
-- ../pokecrystal/engine/events/unown_walls.asm:225 .YChar, :237 .ZChar, :249 .DashChar
|
||||
local FIXED = {
|
||||
[0x60] = { 0x5b, 0x5c, 0x4d, 0x5d },
|
||||
[0x62] = { 0x4e, 0x4f, 0x5e, 0x5f },
|
||||
[0x64] = { 0x02, 0x03, 0x03, 0x02 },
|
||||
}
|
||||
|
||||
function UnownWords.square(char)
|
||||
local fixed = FIXED[char]
|
||||
if fixed then return fixed[1], fixed[2], fixed[3], fixed[4] end
|
||||
local base = UnownWords.BANK1 + char
|
||||
return base, base + 1, base + 0x10, base + 0x11
|
||||
end
|
||||
|
||||
-- ../pokecrystal/engine/events/unown_walls.asm:122-127
|
||||
function UnownWords.origin(wall)
|
||||
return (wall.x1 or 0) + 1, (wall.y1 or 0) + 2
|
||||
end
|
||||
|
||||
-- ../pokecrystal/engine/events/unown_walls.asm:119 MenuBox, home/menu.asm:131 GetMenuBoxDims
|
||||
function UnownWords.boxRect(wall)
|
||||
local x1, y1 = wall.x1 or 0, wall.y1 or 0
|
||||
return x1, y1, (wall.x2 or x1) - x1 + 1, (wall.y2 or y1) - y1 + 1
|
||||
end
|
||||
|
||||
-- ../pokecrystal/engine/events/unown_walls.asm:182 _DisplayUnownWords_CopyWord
|
||||
function UnownWords.layout(wall)
|
||||
local tx, ty = UnownWords.origin(wall)
|
||||
local out = {}
|
||||
for index, char in ipairs(wall.chars or {}) do
|
||||
local tl, tr, bl, br = UnownWords.square(char)
|
||||
out[index] = {
|
||||
tx = tx + (index - 1) * 2, ty = ty,
|
||||
tl = tl, tr = tr, bl = bl, br = br,
|
||||
}
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
-- ../pokecrystal/constants/event_flags.asm:486-489, the four Crystal-only
|
||||
-- EVENT_WALL_OPENED_IN_*_CHAMBER bits.
|
||||
UnownWords.WALL_OPENED = {
|
||||
HO_OH = 806,
|
||||
KABUTO = 807,
|
||||
OMANYTE = 808,
|
||||
AERODACTYL = 809,
|
||||
}
|
||||
|
||||
-- ../pokecrystal/engine/events/unown_walls.asm:60, :87 -- the
|
||||
-- GetMapAttributesPointer compare the two non-special routines are gated on.
|
||||
UnownWords.CHAMBER_MAPS = {
|
||||
HO_OH = "RUINS_OF_ALPH_HO_OH_CHAMBER",
|
||||
KABUTO = "RUINS_OF_ALPH_KABUTO_CHAMBER",
|
||||
OMANYTE = "RUINS_OF_ALPH_OMANYTE_CHAMBER",
|
||||
AERODACTYL = "RUINS_OF_ALPH_AERODACTYL_CHAMBER",
|
||||
}
|
||||
|
||||
-- ../pokecrystal/engine/events/unown_walls.asm:4, :22
|
||||
UnownWords.HO_OH = "HO_OH"
|
||||
UnownWords.WATER_STONE = "WATER_STONE"
|
||||
|
||||
-- ../pokecrystal/engine/events/unown_walls.asm:16 EventFlagAction CHECK_FLAG
|
||||
function UnownWords.wallOpened(events, chamber)
|
||||
local flag = UnownWords.WALL_OPENED[chamber]
|
||||
if not (events and flag) then return false end
|
||||
return events:get(flag) and true or false
|
||||
end
|
||||
|
||||
-- ../pokecrystal/engine/events/unown_walls.asm:8 EventFlagAction SET_FLAG
|
||||
function UnownWords.openWall(events, chamber)
|
||||
local flag = UnownWords.WALL_OPENED[chamber]
|
||||
if not (events and flag) then return false end
|
||||
if events:get(flag) then return false end
|
||||
events:set(flag, true)
|
||||
return true
|
||||
end
|
||||
|
||||
-- ../pokecrystal/engine/events/unown_walls.asm:2-5: wPartySpecies[0], which
|
||||
-- holds EGG rather than the hatchling's species while a slot is an egg.
|
||||
function UnownWords.leadIsHoOh(party)
|
||||
local lead = party and party[1]
|
||||
if not lead or lead.isEgg then return false end
|
||||
return lead.species == UnownWords.HO_OH
|
||||
end
|
||||
|
||||
-- ../pokecrystal/engine/events/unown_walls.asm:28-43: wPartyCount down to 1,
|
||||
-- MON_ITEM on each, so the LAST slot holding one is the one it stops at.
|
||||
function UnownWords.waterStoneSlot(party)
|
||||
party = party or {}
|
||||
for slot = #party, 1, -1 do
|
||||
local mon = party[slot]
|
||||
if mon and mon.item == UnownWords.WATER_STONE then return slot end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- ../pokecrystal/engine/events/unown_walls.asm:54, whose carry FlashFunction
|
||||
-- jumps on at ../pokecrystal/engine/events/overworld.asm:285.
|
||||
function UnownWords.aerodactylChamber(events, mapId)
|
||||
if mapId ~= UnownWords.CHAMBER_MAPS.AERODACTYL then return false end
|
||||
UnownWords.openWall(events, "AERODACTYL")
|
||||
return true
|
||||
end
|
||||
|
||||
-- ../pokecrystal/engine/events/unown_walls.asm:81, off the escape rope arm of
|
||||
-- EscapeRopeOrDig (../pokecrystal/engine/events/overworld.asm:809).
|
||||
function UnownWords.kabutoChamber(events, mapId)
|
||||
if mapId ~= UnownWords.CHAMBER_MAPS.KABUTO then return false end
|
||||
UnownWords.openWall(events, "KABUTO")
|
||||
return true
|
||||
end
|
||||
|
||||
-- ../pokecrystal/constants/script_constants.asm:319 UNOWNWORDS_*
|
||||
function UnownWords.wallFor(data, scriptVar)
|
||||
local walls = data and data.gen2EventTables and data.gen2EventTables.unownWalls
|
||||
if not walls then return nil end
|
||||
return walls[(scriptVar or 0) + 1]
|
||||
end
|
||||
|
||||
-- opts: wall (an events.unownWalls row), world, onClose()
|
||||
function UnownWords.new(game, opts)
|
||||
opts = opts or {}
|
||||
local self = setmetatable({}, UnownWords)
|
||||
self.game = game
|
||||
self.wall = opts.wall
|
||||
self.world = opts.world
|
||||
self.onClose = opts.onClose
|
||||
self.done = false
|
||||
self.squares = self.wall and UnownWords.layout(self.wall) or {}
|
||||
return self
|
||||
end
|
||||
|
||||
-- ../pokecrystal/engine/events/unown_walls.asm:155 _DisplayUnownWords_FillAttr
|
||||
function UnownWords:tileset()
|
||||
local world = self.world
|
||||
local map = world and world.map
|
||||
local def = map and map.def
|
||||
local tileset = def and world.tilesets and world.tilesets[def.tileset]
|
||||
if not tileset or not tileset.image then return nil end
|
||||
if self.atlas == nil then
|
||||
local ok, image = pcall(Assets.image, tileset.image)
|
||||
self.atlas = ok and image or false
|
||||
if self.atlas then self.atlas:setFilter("nearest", "nearest") end
|
||||
local set = world.palettes
|
||||
and Palettes.bgSet(world.palettes, def, world.daytime or "DAY")
|
||||
self.colors = set and set[UnownWords.BROWN] or nil
|
||||
end
|
||||
if not self.atlas then return nil end
|
||||
return tileset, self.atlas
|
||||
end
|
||||
|
||||
-- ../pokecrystal/engine/events/unown_walls.asm:147 PlayClickSFX, :148 CloseWindow
|
||||
function UnownWords:finish()
|
||||
if self.done then return end
|
||||
self.done = true
|
||||
local game = self.game
|
||||
if game and game.data then Sound.play(game.data, "SFX_READ_TEXT_2") end
|
||||
if game and game.stack then game.stack:pop() end
|
||||
if self.onClose then self.onClose() end
|
||||
end
|
||||
|
||||
function UnownWords:update(_dt)
|
||||
if self.done then return end
|
||||
local input = self.game and self.game.input
|
||||
if not input then return end
|
||||
if input:wasPressed("a") or input:wasPressed("b") then self:finish() end
|
||||
end
|
||||
|
||||
function UnownWords:draw()
|
||||
local wall = self.wall
|
||||
if not wall then return end
|
||||
local bx, by, bw, bh = UnownWords.boxRect(wall)
|
||||
Font.drawBox(bx, by, bw, bh)
|
||||
local tileset, atlas = self:tileset()
|
||||
if not tileset then return end
|
||||
local perRow = tileset.tilesPerRow or 16
|
||||
local aw, ah = atlas:getDimensions()
|
||||
local quads = {}
|
||||
local function quadFor(tile)
|
||||
local q = quads[tile]
|
||||
if not q then
|
||||
q = love.graphics.newQuad((tile % perRow) * 8,
|
||||
math.floor(tile / perRow) * 8, 8, 8, aw, ah)
|
||||
quads[tile] = q
|
||||
end
|
||||
return q
|
||||
end
|
||||
local function body()
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
for _, square in ipairs(self.squares) do
|
||||
local x, y = square.tx * 8, square.ty * 8
|
||||
love.graphics.draw(atlas, quadFor(square.tl), x, y)
|
||||
love.graphics.draw(atlas, quadFor(square.tr), x + 8, y)
|
||||
love.graphics.draw(atlas, quadFor(square.bl), x, y + 8)
|
||||
love.graphics.draw(atlas, quadFor(square.br), x + 8, y + 8)
|
||||
end
|
||||
end
|
||||
if self.colors and GbcPalette.available() then
|
||||
GbcPalette.with(self.colors, body)
|
||||
else
|
||||
body()
|
||||
end
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
end
|
||||
|
||||
return UnownWords
|
||||
+214
-25
@@ -41,6 +41,7 @@ local HiddenItems = require("src.world.gen2.HiddenItems")
|
||||
local Mail = require("src.core.gen2.Mail")
|
||||
local Map = require("src.world.gen2.Map")
|
||||
local Palettes = require("src.world.gen2.Palettes")
|
||||
local UnownWords = require("src.world.gen2.UnownWords")
|
||||
local Mon = require("src.battle.gen2.Mon")
|
||||
local Movement = require("src.script.gen2.Movement")
|
||||
local Music = require("src.core.Music")
|
||||
@@ -116,6 +117,14 @@ local VAR = {
|
||||
XCOORD = 0x12,
|
||||
YCOORD = 0x13,
|
||||
SPECIALPHONECALL = 0x14,
|
||||
-- ../pokecrystal/constants/script_constants.asm:69-74, the six rows Gold's
|
||||
-- table stops short of; ../pokecrystal/engine/overworld/variables.asm:62-67.
|
||||
BT_WIN_STREAK = 0x15,
|
||||
KURT_APRICORNS = 0x16,
|
||||
CALLERID = 0x17,
|
||||
BLUECARDBALANCE = 0x18,
|
||||
BUENASPASSWORD = 0x19,
|
||||
KENJI_BREAK = 0x1a,
|
||||
}
|
||||
|
||||
-- constants/ram_constants.asm:293 wPlayerState. PLAYER_SKATE (2) has no row:
|
||||
@@ -374,6 +383,13 @@ local function itemByIndex(items, index)
|
||||
return nil
|
||||
end
|
||||
|
||||
-- One WRAM byte, the width every VAR_* store is (`ld a, [de]` / `ld [de], a`).
|
||||
local function byteOf(value)
|
||||
local n = math.floor(tonumber(value) or 0)
|
||||
if n < 0 then n = 0 end
|
||||
return n % 256
|
||||
end
|
||||
|
||||
-- CountSetBits over a { key = true } flag table: VAR.DEXCAUGHT, VAR.DEXSEEN
|
||||
-- and VAR.BADGES are all "how many of these are set" reads off one.
|
||||
local function countFlags(flags)
|
||||
@@ -810,6 +826,7 @@ function World:load()
|
||||
self.text = self:dataTable("gen2Text", "data/generated/text.lua") or {}
|
||||
self.constants =
|
||||
self:dataTable("gen2Constants", "data/generated/constants.lua") or {}
|
||||
FieldMoves.bindEngineFlags(self.constants.engineFlagOrder)
|
||||
-- The side tables a script command NAMES rather than carries: the phone
|
||||
-- book, the in-game trades, the elevator's floor labels and the decoration
|
||||
-- descriptions. A cache built before the extractor reached them has no
|
||||
@@ -1256,7 +1273,7 @@ function World:load()
|
||||
end,
|
||||
|
||||
-- ---- encounters --------------------------------------------------------
|
||||
setSwarm = function(group, mapNum) self:setSwarm(group, mapNum) end,
|
||||
setSwarm = function(group, mapNum, kind) self:setSwarm(group, mapNum, kind) end,
|
||||
rollWild = function() return self:rollWild() end,
|
||||
-- The WRAM bytes the ENGINE owns rather than the script: nil means "not
|
||||
-- mine", and the VM falls back to its own sparse store.
|
||||
@@ -1573,9 +1590,78 @@ function World:readVar(varId)
|
||||
if varId == VAR.SPECIALPHONECALL then
|
||||
return self:specialCall()
|
||||
end
|
||||
if varId >= VAR.BT_WIN_STREAK and varId <= VAR.KENJI_BREAK then
|
||||
return self:crystalVar(varId)
|
||||
end
|
||||
return 0
|
||||
end
|
||||
|
||||
-- ../pokecrystal/engine/overworld/variables.asm:62-67, the six .VarActionTable
|
||||
-- rows Crystal appends past VAR_SPECIALPHONECALL.
|
||||
function World:crystalVar(varId)
|
||||
-- ../pokecrystal/ram/wram.asm:3286 wCurCaller, which this port parks on the
|
||||
-- VM (src/script/gen2/CallAsm.lua:190).
|
||||
if varId == VAR.CALLERID then
|
||||
return (self.vm and self.vm.curPhoneCaller) or 0
|
||||
end
|
||||
local save = self.game and self.game.save
|
||||
if not save then return 0 end
|
||||
-- ../pokecrystal/ram/wram.asm:1703 wNrOfBeatenBattleTowerTrainers.
|
||||
if varId == VAR.BT_WIN_STREAK then
|
||||
return byteOf(Gen2Save.battleTowerState(save).streak)
|
||||
end
|
||||
-- ../pokecrystal/engine/events/kurt.asm:24,45 wKurtApricornQuantity.
|
||||
if varId == VAR.KURT_APRICORNS then
|
||||
return byteOf(save.kurtApricornQuantity)
|
||||
end
|
||||
local crystal = Gen2Save.crystalState(save)
|
||||
if varId == VAR.BLUECARDBALANCE then
|
||||
return byteOf(crystal.buenaPassword.balance)
|
||||
end
|
||||
if varId == VAR.BUENASPASSWORD then
|
||||
return byteOf(crystal.buenaPassword.word)
|
||||
end
|
||||
-- ../pokecrystal/engine/overworld/time.asm:136 SampleKenjiBreakCountdown.
|
||||
if varId == VAR.KENJI_BREAK then
|
||||
return byteOf(crystal.kenjiBreak)
|
||||
end
|
||||
return 0
|
||||
end
|
||||
|
||||
-- The three of them Script_writevar can reach: RETVAR_ADDR_DE rows write the
|
||||
-- variable itself, RETVAR_STRBUF2 rows write the scratch buffer and are lost
|
||||
-- (../pokecrystal/engine/overworld/variables.asm:21-25).
|
||||
function World:setCrystalVar(varId, value)
|
||||
value = byteOf(value)
|
||||
if varId == VAR.CALLERID then
|
||||
if self.vm then self.vm.curPhoneCaller = value end
|
||||
return
|
||||
end
|
||||
local save = self.game and self.game.save
|
||||
if not save then return end
|
||||
local buena = Gen2Save.crystalState(save).buenaPassword
|
||||
if varId == VAR.BLUECARDBALANCE then
|
||||
buena.balance = value
|
||||
elseif varId == VAR.BUENASPASSWORD then
|
||||
buena.word = value
|
||||
end
|
||||
end
|
||||
|
||||
-- ../pokecrystal/engine/events/kurt.asm:19-45 SelectApricornForKurt, whose
|
||||
-- byte is what `verbosegiveitemvar <BALL>, VAR_KURT_APRICORNS` hands over.
|
||||
function World:setKurtApricornQuantity(count)
|
||||
local save = self.game and self.game.save
|
||||
if not save then return end
|
||||
save.kurtApricornQuantity = byteOf(count)
|
||||
end
|
||||
|
||||
-- ../pokecrystal/engine/overworld/time.asm:136-142, the 3..6 day roll.
|
||||
function World:setKenjiBreak(days)
|
||||
local save = self.game and self.game.save
|
||||
if not save then return end
|
||||
Gen2Save.crystalState(save).kenjiBreak = byteOf(days)
|
||||
end
|
||||
|
||||
-- Script_checkver: 0 for Gold, 1 for Silver (constants/misc_constants.asm
|
||||
-- GS_VERSION).
|
||||
function World:gsVersion()
|
||||
@@ -1622,6 +1708,11 @@ function World:engineFlag(flag)
|
||||
local owned = player and player[badge.store]
|
||||
return type(owned) == "table" and owned[badge.name] == true
|
||||
end
|
||||
-- Same one-store rule for ENGINE_PLAYER_IS_FEMALE, which IS wPlayerGender
|
||||
-- (../pokecrystal/data/events/engine_flags.asm:131); Gold's FEMALE_FLAG is nil.
|
||||
if flag == FieldMoves.FEMALE_FLAG then
|
||||
return Gen2Save.isFemale(save)
|
||||
end
|
||||
-- Same one-store rule for the day care. data/events/engine_flags.asm:18-20
|
||||
-- maps the three ids onto DAYCAREMAN_HAS_EGG_F / DAYCAREMAN_HAS_MON_F /
|
||||
-- DAYCARELADY_HAS_MON_F, i.e. they ARE the bits DayCare_InitBreeding,
|
||||
@@ -1661,6 +1752,13 @@ function World:setEngineFlag(flag, value)
|
||||
save.player[badge.store][badge.name] = value and true or nil
|
||||
return
|
||||
end
|
||||
-- InitGender is the only writer on the cart, so this exists only to keep a
|
||||
-- stray setflag out of save.engineFlags (../pokecrystal/engine/menus/init_gender.asm:23-42).
|
||||
if flag == FieldMoves.FEMALE_FLAG and save then
|
||||
save.player = save.player or {}
|
||||
save.player.gender = value and "female" or "male"
|
||||
return
|
||||
end
|
||||
-- The write half of the day-care aliases. DayCareManScript_Outside's
|
||||
-- `clearflag ENGINE.DAY_CARE_MAN_HAS_EGG` (maps/Route34.asm) is the ONLY cart
|
||||
-- script that writes any of the three, and it is idempotent because
|
||||
@@ -1697,6 +1795,9 @@ function World:writeVar(varId, value)
|
||||
local state = PLAYER_STATE_BY_ID[value or 0]
|
||||
if state then self:applyPlayerState(state) end
|
||||
end
|
||||
if varId >= VAR.BT_WIN_STREAK and varId <= VAR.KENJI_BREAK then
|
||||
self:setCrystalVar(varId, value)
|
||||
end
|
||||
end
|
||||
|
||||
function World:battleType()
|
||||
@@ -1854,6 +1955,16 @@ end
|
||||
-- (maps/Route36.asm:58, and again at :70 on the DidntCatchSudowoodo arm) hands
|
||||
-- the same slot to the Route 37 twins. So the pooled objects that read
|
||||
-- through the slot have to go with it.
|
||||
-- ../pokecrystal/engine/events/battle_tower/battle_tower.asm:1564-1575 writes
|
||||
-- the sprite byte into wMapObjects, the LIVE copy, so the map def is untouched.
|
||||
function World:setObjectSprite(objectId, spriteName)
|
||||
local npc = self:objectEntity(objectId)
|
||||
local spriteDef = spriteName and self.sprites and self.sprites[spriteName]
|
||||
if not (npc and spriteDef) then return false end
|
||||
if npc:setSpriteDef(spriteDef) then self:applySpritePalette(npc) end
|
||||
return true
|
||||
end
|
||||
|
||||
function World:setVariableSprite(slot, spriteIndex)
|
||||
if slot == nil then return end
|
||||
self.variableSprites[slot] = spriteIndex
|
||||
@@ -2318,12 +2429,10 @@ end
|
||||
-- the map pair and DAILYFLAGS1_SWARM are set by the one command. A port that
|
||||
-- stored only the map would leave the Dunsparce call live forever, because
|
||||
-- CheckSwarmFlag answers off the flag and clears the pair itself.
|
||||
function World:setSwarm(group, mapNum)
|
||||
function World:setSwarm(group, mapNum, kind)
|
||||
local save = self.game and self.game.save
|
||||
if not save then return end
|
||||
save.dailyFlags = save.dailyFlags or {}
|
||||
save.dailyFlags.swarm = true
|
||||
save.swarmMap = self:mapIdByGroupMap(group, mapNum)
|
||||
Roamers.Swarm.set(save, self:mapIdByGroupMap(group, mapNum), kind)
|
||||
end
|
||||
|
||||
-- Script_loadwildmon's other half: roll the CURRENT map's own table the way a
|
||||
@@ -3472,7 +3581,7 @@ function World:specialHooks()
|
||||
takeItem = function(index, qty) return self:takeItem(index, qty) end,
|
||||
engineFlag = function(flag) return self:engineFlag(flag) end,
|
||||
setEngineFlag = function(flag, v) self:setEngineFlag(flag, v) end,
|
||||
setSwarm = function(group, mapNum) self:setSwarm(group, mapNum) end,
|
||||
setSwarm = function(group, mapNum, kind) self:setSwarm(group, mapNum, kind) end,
|
||||
dayCare = function(side, onDone) self:dayCare(side, onDone) end,
|
||||
givePokeMail = function(mail) return self:givePokeMail(mail) end,
|
||||
checkPokeMail = function(mail, onDone) self:checkPokeMail(mail, onDone) end,
|
||||
@@ -3504,6 +3613,14 @@ function World:specialHooks()
|
||||
magnetTrain = function(toGoldenrod, onDone)
|
||||
self:magnetTrain(toGoldenrod, onDone)
|
||||
end,
|
||||
-- ../pokecrystal/engine/events/battle_tower/battle_tower.asm:220-223
|
||||
startTowerBattle = function(trainer, onDone)
|
||||
return self:startBattle({ trainer = trainer, battleTower = true }, onDone)
|
||||
end,
|
||||
-- ../pokecrystal/engine/events/battle_tower/battle_tower.asm:1552-1575
|
||||
setObjectSprite = function(objectId, spriteName)
|
||||
return self:setObjectSprite(objectId, spriteName)
|
||||
end,
|
||||
pushScreen = function(id, opts) return self:pushScreen(id, opts) end,
|
||||
monName = function(index)
|
||||
local id, def = speciesByIndex(
|
||||
@@ -3537,9 +3654,36 @@ function World:specialHooks()
|
||||
self:openScriptMenu(header, "vertical", onChoose)
|
||||
end,
|
||||
rareWildMon = function() return self:rareWildMon() end,
|
||||
-- ../pokecrystal/engine/menus/save.asm:181 AskOverwriteSaveFile and :266
|
||||
-- _SaveGameData, the two halves of Link_SaveGame (:63).
|
||||
saveFileState = function() return self:saveFileState() end,
|
||||
writeSave = function() return self:writeSave() end,
|
||||
setKurtApricornQuantity = function(n) self:setKurtApricornQuantity(n) end,
|
||||
setKenjiBreak = function(days) self:setKenjiBreak(days) end,
|
||||
}
|
||||
end
|
||||
|
||||
-- AskOverwriteSaveFile's two reads: wSaveFileExists, and
|
||||
-- CompareLoadedAndSavedPlayerID (../pokecrystal/engine/menus/save.asm:224),
|
||||
-- which is what picks AlreadyASaveFileText over AnotherSaveFileText.
|
||||
function World:saveFileState()
|
||||
local save = self.game and self.game.save
|
||||
local version = save and save.version
|
||||
if not Gen2Save.exists(version) then return false, false end
|
||||
local stored = Gen2Save.load(version)
|
||||
local mine = save and save.player and save.player.id
|
||||
local theirs = stored and stored.player and stored.player.id
|
||||
return true, (mine ~= nil and mine == theirs)
|
||||
end
|
||||
|
||||
-- _SaveGameData, through the writer the SAVE menu is handed
|
||||
-- (src/core/Game2.lua:435) so the save.write veto holds here too.
|
||||
function World:writeSave()
|
||||
local game = self.game
|
||||
if not (game and game.writeSave) then return false end
|
||||
return game:writeSave() ~= false
|
||||
end
|
||||
|
||||
-- RandomUnseenWildMon's lookup half. The routine picks one of the THREE
|
||||
-- RAREST grass slots on the map (`and %11 / jr z` rerolls 0, so it is slots 5,
|
||||
-- 6 or 7 of the seven) and drops it if that species is also one of the FOUR
|
||||
@@ -4441,6 +4585,9 @@ function World:useEscapeRope(itemId)
|
||||
local items = self.game and self.game.data and self.game.data.items
|
||||
local def = items and items[itemId or "ESCAPE_ROPE"]
|
||||
self:takeItem(def and def.index, 1)
|
||||
-- ../pokecrystal/engine/events/overworld.asm:809, between .escaperope and
|
||||
-- QueueScript.
|
||||
UnownWords.kabutoChamber(self.events, self.map and self.map.id)
|
||||
self.queuedFieldMove = {
|
||||
ok = true, action = "escaperope",
|
||||
destMap = destMapId, destWarp = destWarp,
|
||||
@@ -5186,6 +5333,9 @@ function World:fieldContext(mon)
|
||||
facingX = fx, facingY = fy,
|
||||
facingColl = map:cellCollision(fx, fy),
|
||||
playerColl = map:cellCollision(p.cellX, p.cellY),
|
||||
-- Crystal's SurfFunction.TrySurf is the only field move that asks
|
||||
-- (../pokecrystal/engine/events/overworld.asm:364).
|
||||
facingObject = self:facingObject(),
|
||||
upColl = map:cellCollision(p.cellX, p.cellY - 1),
|
||||
tileset = map.def and map.def.tileset,
|
||||
facingBlock = blockId,
|
||||
@@ -5199,6 +5349,11 @@ function World:fieldContext(mon)
|
||||
-- FlashFunction tests wTimeOfDayPalset, not the map header, so a
|
||||
-- PALETTE_DARK map that FLASH has already lit refuses a second FLASH.
|
||||
dark = Palettes.isDarkness(map.def, self:hour(), self.flashUsed),
|
||||
-- ../pokecrystal/engine/events/overworld.asm:285, called by FLASH only and
|
||||
-- only after the badge gate, because it SETS the wall-opened flag.
|
||||
openAerodactylWall = function()
|
||||
return UnownWords.aerodactylChamber(self.events, map.id)
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
@@ -5328,12 +5483,26 @@ function World:refreshMapImages()
|
||||
return true
|
||||
end
|
||||
|
||||
-- wPlayerGender, the byte GetPlayerSprite and AddMapObject both branch on
|
||||
-- (engine/overworld/overworld.asm:61-64, engine/overworld/player_object.asm:32-39).
|
||||
function World:playerGender()
|
||||
local save = self.game and self.game.save
|
||||
return save and save.player and save.player.gender or nil
|
||||
end
|
||||
|
||||
-- The Chris/Kris sheet the player wears with no state on it
|
||||
-- (data/sprites/player_sprites.asm:2, :9).
|
||||
function World:playerSpriteName()
|
||||
return FieldMoves.playerSprite(self:playerGender()) or PLAYER_SPRITE
|
||||
end
|
||||
|
||||
-- UpdatePlayerSprite (data/sprites/player_sprites.asm ChrisStateSprites): the
|
||||
-- player's sprite is a pure function of wPlayerState, which is what makes
|
||||
-- getting on and off a Lapras a one-byte change rather than an animation.
|
||||
function World:applyPlayerState(state)
|
||||
self.playerState = state or FieldMoves.PLAYER_NORMAL
|
||||
local name = FieldMoves.STATE_SPRITE[self.playerState] or PLAYER_SPRITE
|
||||
local name = FieldMoves.stateSprite(self.playerState, self:playerGender())
|
||||
or PLAYER_SPRITE
|
||||
local def = self.sprites and self.sprites[name]
|
||||
if def and self.player then
|
||||
self.player:setSprite(def)
|
||||
@@ -5884,6 +6053,9 @@ function World:startBattle(opts, onDone)
|
||||
-- wBattleType, when the script armed one: the FORCESHINY / TRAP
|
||||
-- no-escape rules live in Battle:tryRun and the force-switch handler.
|
||||
battleType = opts.battleType,
|
||||
-- wInBattleTowerBattle (../pokecrystal/engine/events/battle_tower/
|
||||
-- battle_tower.asm:220-223), which turns DoBadgeTypeBoosts off.
|
||||
battleTower = opts.battleTower,
|
||||
})
|
||||
self:playBattleMusic(opts)
|
||||
local function pushBattle()
|
||||
@@ -7397,26 +7569,43 @@ function World:interact()
|
||||
return self:interactBody()
|
||||
end
|
||||
|
||||
-- CheckFacingObject (engine/overworld/npc_movement.asm:229-248): "Double the
|
||||
-- distance for counter tiles." A Pokecenter nurse and a Mart clerk stand
|
||||
-- BEHIND a COLL_COUNTER tile, so the cell the player faces is the counter
|
||||
-- itself and the object is one further on. Without this the press finds an
|
||||
-- empty wall and nothing happens -- which is to say no nurse and no clerk in
|
||||
-- the game could be talked to at all.
|
||||
--
|
||||
-- Only the OBJECT lookup is doubled, exactly as the cart does it: bg events
|
||||
-- and the tile-collision events still read the tile actually faced.
|
||||
function World:facingObjectCell()
|
||||
local p = self.player
|
||||
if not p then return nil end
|
||||
local d = Map.DELTA[p.facing] or Map.DELTA.down
|
||||
local fx, fy = p.cellX + d[1], p.cellY + d[2]
|
||||
if self.map and Permissions.isCounter(self.map:cellCollision(fx, fy)) then
|
||||
return p.cellX + d[1] * 2, p.cellY + d[2] * 2
|
||||
end
|
||||
return fx, fy
|
||||
end
|
||||
|
||||
-- The carry CheckFacingObject answers with: IsNPCAtCoord, and then only when
|
||||
-- that object's OBJECT_WALKING reads STANDING (npc_movement.asm:250-266).
|
||||
function World:facingObject()
|
||||
local ox, oy = self:facingObjectCell()
|
||||
if not ox then return nil end
|
||||
local npc = self:npcAt(ox, oy)
|
||||
if npc and npc.moving then return nil end
|
||||
return npc
|
||||
end
|
||||
|
||||
function World:interactBody()
|
||||
if self:busy() or not self.player or not self.vm then return false end
|
||||
local p = self.player
|
||||
if p.moving then return false end
|
||||
local d = Map.DELTA[p.facing]
|
||||
local fx, fy = p.cellX + d[1], p.cellY + d[2]
|
||||
-- CheckFacingObject (engine/overworld/npc_movement.asm:229): "Double the
|
||||
-- distance for counter tiles." A Pokecenter nurse and a Mart clerk stand
|
||||
-- BEHIND a COLL_COUNTER tile, so the cell the player faces is the counter
|
||||
-- itself and the object is one further on. Without this the press finds an
|
||||
-- empty wall and nothing happens -- which is to say no nurse and no clerk in
|
||||
-- the game could be talked to at all.
|
||||
--
|
||||
-- Only the OBJECT lookup is doubled, exactly as the cart does it: bg events
|
||||
-- and the tile-collision events below still read the tile actually faced.
|
||||
local ox, oy = fx, fy
|
||||
if self.map and Permissions.isCounter(self.map:cellCollision(fx, fy)) then
|
||||
ox, oy = p.cellX + d[1] * 2, p.cellY + d[2] * 2
|
||||
end
|
||||
local npc = self:npcAt(ox, oy)
|
||||
local npc = self:npcAt(self:facingObjectCell())
|
||||
-- TryObjectEvent writes hLastTalked for EVERY A-press dispatch; scripts
|
||||
-- then use LAST_TALKED (`disappear`, `applymovementlasttalked`) without any
|
||||
-- setlasttalked of their own. The port only wrote it from the explicit
|
||||
@@ -8510,13 +8699,13 @@ function World:setMap(mapId, cx, cy, facing, opts)
|
||||
-- GetWarpDestCoords / EnterMapConnection / EnterMapSpawnPoint write wXCoord
|
||||
-- and wYCoord BEFORE HandleNewMap (data/maps/setup_scripts.asm:79-106).
|
||||
local face = facing or (self.player and self.player.facing) or "down"
|
||||
local chris = self.sprites and self.sprites[PLAYER_SPRITE]
|
||||
local playerDef = self.sprites and self.sprites[self:playerSpriteName()]
|
||||
if self.player then
|
||||
self.player.cellX, self.player.cellY = cx, cy
|
||||
self.player.px, self.player.py = cx * 16, cy * 16
|
||||
self.player.facing = face
|
||||
if chris and not self.player.sprite then
|
||||
self.player:setSprite(chris)
|
||||
if playerDef and not self.player.sprite then
|
||||
self.player:setSprite(playerDef)
|
||||
end
|
||||
if not opts.seamless then
|
||||
self.player.moving = false
|
||||
@@ -8524,7 +8713,7 @@ function World:setMap(mapId, cx, cy, facing, opts)
|
||||
self.player.targetX, self.player.targetY = nil, nil
|
||||
end
|
||||
else
|
||||
self.player = Player.new(cx, cy, face, chris)
|
||||
self.player = Player.new(cx, cy, face, playerDef)
|
||||
end
|
||||
-- LoadMapObjects rebuilds OBJECT_FLAGS2 from scratch, so IN_GRASS is decided
|
||||
-- by the cell the player arrives on (engine/overworld/map_objects.asm:247).
|
||||
|
||||
Reference in New Issue
Block a user