mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-23 14:07:34 +02:00
Merge pull request #1726 from bryanthaboi/dev
This commit is contained in:
@@ -82,6 +82,43 @@ end
|
|||||||
local editorHost, editorVersion, editorWindow
|
local editorHost, editorVersion, editorWindow
|
||||||
local closeEditor -- forward declaration: openEditor hands it to the editor
|
local closeEditor -- forward declaration: openEditor hands it to the editor
|
||||||
|
|
||||||
|
-- Drop CacheFs / Data / mod Runtime / Assets / LegacyCompat for one mounted
|
||||||
|
-- version session (save editor or game). closeEditor and returnToLauncher
|
||||||
|
-- both go through here so neither path can forget a singleton the other resets.
|
||||||
|
local function teardownMountedSession(version)
|
||||||
|
if version then
|
||||||
|
require("src.import.CacheFs").unmountVersion(version)
|
||||||
|
end
|
||||||
|
require("src.core.Data"):unloadGenerated()
|
||||||
|
local Runtime = require("src.mods.Runtime")
|
||||||
|
if Runtime.reset then Runtime.reset() end
|
||||||
|
local Assets = require("src.render.Assets")
|
||||||
|
if Assets.installLoader then Assets.installLoader(nil) end
|
||||||
|
local okCompat, LegacyCompat = pcall(require, "src.mods.LegacyCompat")
|
||||||
|
if okCompat and LegacyCompat.reset then LegacyCompat.reset() end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Evict every save-editor module from package.loaded without a hardcoded
|
||||||
|
-- panel whitelist. Flat require names (App, Party, …) resolve under
|
||||||
|
-- tools/save-editor/; path-style keys may also appear. A key is flushed
|
||||||
|
-- when it names a save-editor path or when tools/save-editor/{panels/}K.lua
|
||||||
|
-- exists for a flat name K -- new panels are picked up automatically.
|
||||||
|
local function flushEditorPackageLoaded()
|
||||||
|
local fs = love and love.filesystem
|
||||||
|
local function isEditorFlat(name)
|
||||||
|
if not (fs and fs.getInfo) then return false end
|
||||||
|
if name:find("[./]") then return false end
|
||||||
|
return fs.getInfo("tools/save-editor/" .. name .. ".lua") ~= nil
|
||||||
|
or fs.getInfo("tools/save-editor/panels/" .. name .. ".lua") ~= nil
|
||||||
|
end
|
||||||
|
for k in pairs(package.loaded) do
|
||||||
|
if type(k) == "string"
|
||||||
|
and (k:find("save%-editor", 1, false) or isEditorFlat(k)) then
|
||||||
|
package.loaded[k] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- The editor's modules use flat names (require("Kit"), require("Party")), so
|
-- The editor's modules use flat names (require("Kit"), require("Party")), so
|
||||||
-- their directories have to be on the require path. It must be
|
-- their directories have to be on the require path. It must be
|
||||||
-- love.filesystem's path, not package.path: in a packaged build these files
|
-- love.filesystem's path, not package.path: in a packaged build these files
|
||||||
@@ -180,9 +217,9 @@ local function openEditor(version, slotId)
|
|||||||
if EditorApp.unload then pcall(EditorApp.unload) end
|
if EditorApp.unload then pcall(EditorApp.unload) end
|
||||||
EditorApp = nil
|
EditorApp = nil
|
||||||
if version then
|
if version then
|
||||||
require("src.import.CacheFs").unmountVersion(version)
|
teardownMountedSession(version)
|
||||||
require("src.core.Data"):unloadGenerated()
|
|
||||||
end
|
end
|
||||||
|
flushEditorPackageLoaded()
|
||||||
restoreWindow()
|
restoreWindow()
|
||||||
Importer = editorHost
|
Importer = editorHost
|
||||||
editorHost = nil
|
editorHost = nil
|
||||||
@@ -197,21 +234,17 @@ end
|
|||||||
-- Back to the launcher. Everything the editor mounted or cached has to come
|
-- Back to the launcher. Everything the editor mounted or cached has to come
|
||||||
-- back out: the version overlay (CacheFs) and the generated modules require
|
-- back out: the version overlay (CacheFs) and the generated modules require
|
||||||
-- cached behind it (Data), or pressing Play on the OTHER game would boot it
|
-- cached behind it (Data), or pressing Play on the OTHER game would boot it
|
||||||
-- with this one's data.
|
-- with this one's data. Also reset Runtime / Assets / LegacyCompat so the
|
||||||
|
-- next Edit or Play does not inherit the editor's dead mod loader.
|
||||||
function closeEditor()
|
function closeEditor()
|
||||||
local version = editorVersion
|
local version = editorVersion
|
||||||
editorMode = false
|
editorMode = false
|
||||||
if EditorApp and EditorApp.unload then EditorApp.unload() end
|
if EditorApp and EditorApp.unload then EditorApp.unload() end
|
||||||
EditorApp = nil
|
EditorApp = nil
|
||||||
if version then
|
if version then
|
||||||
require("src.import.CacheFs").unmountVersion(version)
|
teardownMountedSession(version)
|
||||||
require("src.core.Data"):unloadGenerated()
|
|
||||||
end
|
|
||||||
for k in pairs(package.loaded) do
|
|
||||||
if type(k) == "string" and (k:find("save%-editor") or k == "App" or k == "Kit" or k == "State" or k == "Catalog" or k == "SaveIO" or k == "Ops" or k == "MonOps" or k == "ItemOps" or k == "PadInput" or k == "Gen" or k == "Theme") then
|
|
||||||
package.loaded[k] = nil
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
flushEditorPackageLoaded()
|
||||||
editorVersion = nil
|
editorVersion = nil
|
||||||
restoreWindow()
|
restoreWindow()
|
||||||
Importer = editorHost
|
Importer = editorHost
|
||||||
@@ -333,16 +366,11 @@ local function returnToLauncher()
|
|||||||
|
|
||||||
local GameVersion = require("src.core.GameVersion")
|
local GameVersion = require("src.core.GameVersion")
|
||||||
local currentVersion = GameVersion.get()
|
local currentVersion = GameVersion.get()
|
||||||
if currentVersion then
|
teardownMountedSession(currentVersion)
|
||||||
require("src.import.CacheFs").unmountVersion(currentVersion)
|
|
||||||
end
|
|
||||||
require("src.core.Data"):unloadGenerated()
|
|
||||||
|
|
||||||
local Runtime = require("src.mods.Runtime")
|
if Game.reset then
|
||||||
if Runtime.reset then
|
pcall(function() Game:reset() end)
|
||||||
Runtime.reset()
|
|
||||||
end
|
end
|
||||||
|
|
||||||
Game = nil
|
Game = nil
|
||||||
autopilot = nil
|
autopilot = nil
|
||||||
driverCo = nil
|
driverCo = nil
|
||||||
|
|||||||
@@ -1381,4 +1381,26 @@ function Game:restoreCheckpointBattle(battle)
|
|||||||
if battle.resumeCheckpoint then battle:resumeCheckpoint() end
|
if battle.resumeCheckpoint then battle:resumeCheckpoint() end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Drop every session-owned field so the next Game:load() starts clean when
|
||||||
|
-- the process returns to the launcher in-place (Android / intent_game).
|
||||||
|
-- main.lua must not guess field names: new systems (Game.network, …) are
|
||||||
|
-- cleared automatically because only functions (methods) are kept.
|
||||||
|
function Game:reset()
|
||||||
|
if self.stack and self.stack.clear then
|
||||||
|
pcall(function() self.stack:clear() end)
|
||||||
|
end
|
||||||
|
if self.renderer and self.renderer.releaseCanvases then
|
||||||
|
pcall(function() self.renderer:releaseCanvases() end)
|
||||||
|
end
|
||||||
|
local keys = {}
|
||||||
|
for key, value in pairs(self) do
|
||||||
|
if type(value) ~= "function" then
|
||||||
|
keys[#keys + 1] = key
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for _, key in ipairs(keys) do
|
||||||
|
self[key] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
return Game
|
return Game
|
||||||
|
|||||||
@@ -943,6 +943,12 @@ function Game2:load()
|
|||||||
self.data.gen2Scripts = loadGenerated("data/generated/scripts.lua")
|
self.data.gen2Scripts = loadGenerated("data/generated/scripts.lua")
|
||||||
self.data.gen2StdScripts = loadGenerated("data/generated/std_scripts.lua")
|
self.data.gen2StdScripts = loadGenerated("data/generated/std_scripts.lua")
|
||||||
self.data.gen2Text = loadGenerated("data/generated/text.lua")
|
self.data.gen2Text = loadGenerated("data/generated/text.lua")
|
||||||
|
-- The engine's own strings, keyed by the disassembly's label. gen2Text
|
||||||
|
-- above is the script text and is keyed by bank:address for the overworld
|
||||||
|
-- VM, so the two are different tables and both are loaded. This one is
|
||||||
|
-- what src/core/RomText.lua reads, which is why it lands on `text`: that
|
||||||
|
-- helper is shared with Gen 1 and looks up data.text[label].
|
||||||
|
self.data.text = loadGenerated("data/generated/rom_text.lua") or {}
|
||||||
-- data/generated/events.lua: the side tables a script command NAMES rather
|
-- data/generated/events.lua: the side tables a script command NAMES rather
|
||||||
-- than carries -- the phone book, the in-game trades, the elevator's floor
|
-- than carries -- the phone book, the in-game trades, the elevator's floor
|
||||||
-- labels, the decoration descriptions. Keyed for World's own `eventTables`
|
-- labels, the decoration descriptions. Keyed for World's own `eventTables`
|
||||||
@@ -2117,4 +2123,22 @@ function Game2:joystickremoved()
|
|||||||
TouchControls:joystickremoved()
|
TouchControls:joystickremoved()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- In-process return-to-launcher (Android / intent_game): drop session fields
|
||||||
|
-- so a later Game2.new() + load is not sharing a live stack or mod loader.
|
||||||
|
-- Methods live on the class table; pairs(self) only sees instance state.
|
||||||
|
function Game2:reset()
|
||||||
|
if self.stack and self.stack.clear then
|
||||||
|
pcall(function() self.stack:clear() end)
|
||||||
|
end
|
||||||
|
local keys = {}
|
||||||
|
for key, value in pairs(self) do
|
||||||
|
if type(value) ~= "function" then
|
||||||
|
keys[#keys + 1] = key
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for _, key in ipairs(keys) do
|
||||||
|
self[key] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
return Game2
|
return Game2
|
||||||
|
|||||||
+22
-8
@@ -614,14 +614,18 @@ function CacheFs.mountVersion(version)
|
|||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Undo mountVersion. A process normally mounts exactly one version and then
|
-- Undo mountVersion in LIFO order relative to mountVersion: generated-tree
|
||||||
-- boots it, but the launcher can open the save editor on one game's save,
|
-- overlays first (assets, then data -- reverse of mountGeneratedTrees), then
|
||||||
-- close it, and press Play on another: with the first version's subtree
|
-- the version folder. PHYSFS resolves by stack order; peeling the wrong
|
||||||
-- still prepended, the other's require("data.generated.*") and generated
|
-- layer first can leave another version's generated files winning a name.
|
||||||
-- art would silently resolve to the first game's files. Callers must also
|
--
|
||||||
-- drop the generated modules from package.loaded
|
-- A process normally mounts exactly one version and then boots it, but the
|
||||||
-- (src.core.Data:unloadGenerated) -- unmounting alone only fixes the read
|
-- launcher can open the save editor on one game's save, close it, and press
|
||||||
-- path, not what require already cached.
|
-- Play on another: with the first version's subtree still prepended, the
|
||||||
|
-- other's require("data.generated.*") and generated art would silently
|
||||||
|
-- resolve to the first game's files. Callers must also drop the generated
|
||||||
|
-- modules from package.loaded (src.core.Data:unloadGenerated) -- unmounting
|
||||||
|
-- alone only fixes the read path, not what require already cached.
|
||||||
--
|
--
|
||||||
-- Returns true when nothing was mounted or the unmount took.
|
-- Returns true when nothing was mounted or the unmount took.
|
||||||
function CacheFs.unmountVersion(version)
|
function CacheFs.unmountVersion(version)
|
||||||
@@ -633,6 +637,16 @@ function CacheFs.unmountVersion(version)
|
|||||||
base = love.filesystem.getSaveDirectory()
|
base = love.filesystem.getSaveDirectory()
|
||||||
end
|
end
|
||||||
local done = false
|
local done = false
|
||||||
|
-- LIFO vs mountGeneratedTrees: assets/generated, then data/generated.
|
||||||
|
if love.filesystem and love.filesystem.unmount then
|
||||||
|
local generated = {
|
||||||
|
prefix .. "assets/generated",
|
||||||
|
prefix .. "data/generated",
|
||||||
|
}
|
||||||
|
for _, src in ipairs(generated) do
|
||||||
|
done = love.filesystem.unmount(src) or done
|
||||||
|
end
|
||||||
|
end
|
||||||
local fn = resolveUnmount()
|
local fn = resolveUnmount()
|
||||||
if fn and base then
|
if fn and base then
|
||||||
done = fn(base .. SEP .. sub) or done
|
done = fn(base .. SEP .. sub) or done
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ RomExtractorGen2.__index = RomExtractorGen2
|
|||||||
-- scripts, pokemon, moves, items, marts, encounters, trainers, pokedex,
|
-- scripts, pokemon, moves, items, marts, encounters, trainers, pokedex,
|
||||||
-- landmarks, intro movie, menu gfx, title, credits, diploma, trade animation,
|
-- landmarks, intro movie, menu gfx, title, credits, diploma, trade animation,
|
||||||
-- audio, stubs
|
-- audio, stubs
|
||||||
local STAGE_COUNT = 26
|
local STAGE_COUNT = 27
|
||||||
local Opcodes = require("src.script.gen2.Opcodes")
|
local Opcodes = require("src.script.gen2.Opcodes")
|
||||||
|
|
||||||
-- BG palette slots inside one loaded 8-palette set (constants/tileset_constants.asm
|
-- BG palette slots inside one loaded 8-palette set (constants/tileset_constants.asm
|
||||||
@@ -136,6 +136,18 @@ local TEXT_NO_GLYPH = {
|
|||||||
[0x13] = true, [0x15] = true,
|
[0x13] = true, [0x15] = true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
-- The three runtime name slots. PlaceMoveUsersName, PlaceMoveTargetsName and
|
||||||
|
-- PlaceEnemysName (home/text.asm:302, :307, :327) swap these for a battler's
|
||||||
|
-- own name as the line prints, so they are markers rather than glyphs. They
|
||||||
|
-- decode to the same shape Gen 1 uses, which src/core/RomText.lua already
|
||||||
|
-- fills in argument order. Dropped, SubTookDamageText read "took damage
|
||||||
|
-- for" with nothing after it.
|
||||||
|
local NAME_SLOT = {
|
||||||
|
["<USER>"] = "{USER}",
|
||||||
|
["<TARGET>"] = "{TARGET}",
|
||||||
|
["<ENEMY>"] = "{ENEMY}",
|
||||||
|
}
|
||||||
|
|
||||||
local ROOF_TILES = 9
|
local ROOF_TILES = 9
|
||||||
local SPRITEDATA_LENGTH = 6
|
local SPRITEDATA_LENGTH = 6
|
||||||
|
|
||||||
@@ -2596,6 +2608,8 @@ function RomExtractorGen2:decodeGen2Text(bank, address, charmap, buffers)
|
|||||||
out[#out + 1] = ch
|
out[#out + 1] = ch
|
||||||
elseif ch == "<……>" or b == 0x56 then
|
elseif ch == "<……>" or b == 0x56 then
|
||||||
out[#out + 1] = "……"
|
out[#out + 1] = "……"
|
||||||
|
elseif NAME_SLOT[ch] then
|
||||||
|
out[#out + 1] = NAME_SLOT[ch]
|
||||||
elseif not ch then
|
elseif not ch then
|
||||||
out[#out + 1] = ("{BYTE:%02X}"):format(b)
|
out[#out + 1] = ("{BYTE:%02X}"):format(b)
|
||||||
end
|
end
|
||||||
@@ -3698,6 +3712,37 @@ function RomExtractorGen2:splashGfx()
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- The engine's own strings, keyed by the label the disassembly gives them.
|
||||||
|
--
|
||||||
|
-- This is what extractOakSpeech has always done for _OakText1-7: resolve the
|
||||||
|
-- label, decode from the cart, key by name. What is new is that the list of
|
||||||
|
-- labels comes from the manifest instead of being written out here, so all of
|
||||||
|
-- data/text/ arrives rather than seven strings. Gen 1 has had the same table
|
||||||
|
-- since RomExtractor:extractText; this is the Gen 2 side of it, and it is
|
||||||
|
-- what lets src/core/RomText.lua work on Gold and Silver at all.
|
||||||
|
--
|
||||||
|
-- Written as `rom_text` rather than `text`: data/generated/text.lua is
|
||||||
|
-- already the script text, keyed by bank:address for the overworld VM, and
|
||||||
|
-- these are a different table with different keys.
|
||||||
|
function RomExtractorGen2:extractText()
|
||||||
|
self:beginStage("Dialogue")
|
||||||
|
local charmap = self.manifest.charmap or {}
|
||||||
|
local labels = (self.manifest.text or {}).labels or {}
|
||||||
|
local texts = {}
|
||||||
|
for index, label in ipairs(labels) do
|
||||||
|
local location = self.symbols[label]
|
||||||
|
-- A label the manifest names but the symbol table does not carry would
|
||||||
|
-- be a generator bug, not a cart difference: make_gold_manifest.py
|
||||||
|
-- resolves every one of these before it writes the list.
|
||||||
|
if location then
|
||||||
|
texts[label] = self:decodeGen2Text(location[1], location[2], charmap)
|
||||||
|
end
|
||||||
|
self:tick("Dialogue", index, #labels)
|
||||||
|
end
|
||||||
|
self:write("rom_text", texts)
|
||||||
|
return texts
|
||||||
|
end
|
||||||
|
|
||||||
-- OakSpeech (engine/menus/intro_menu.asm): named _OakText* strings plus the
|
-- OakSpeech (engine/menus/intro_menu.asm): named _OakText* strings plus the
|
||||||
-- POKEMON_PROF / CAL trainer pics shown before NamePlayer. Also pulls
|
-- POKEMON_PROF / CAL trainer pics shown before NamePlayer. Also pulls
|
||||||
-- Shrink1/2 pics and the GameFreak splash sheets for the boot cinema.
|
-- Shrink1/2 pics and the GameFreak splash sheets for the boot cinema.
|
||||||
@@ -6283,6 +6328,7 @@ function RomExtractorGen2:run()
|
|||||||
results.sprites = self:extractSprites()
|
results.sprites = self:extractSprites()
|
||||||
results.stdScripts = self:extractStdScripts()
|
results.stdScripts = self:extractStdScripts()
|
||||||
results.scripts = self:extractScriptsAndText(results.maps, results.stdScripts)
|
results.scripts = self:extractScriptsAndText(results.maps, results.stdScripts)
|
||||||
|
results.text = self:extractText()
|
||||||
results.pokemon = self:extractPokemon()
|
results.pokemon = self:extractPokemon()
|
||||||
results.moves = self:extractMoves()
|
results.moves = self:extractMoves()
|
||||||
results.items = self:extractItems()
|
results.items = self:extractItems()
|
||||||
|
|||||||
@@ -104,6 +104,12 @@ local VERSION_REQUIRED_FILES_OVERRIDE = {
|
|||||||
"data/generated/sprites.lua", -- OW sheets (Chris + NPCs)
|
"data/generated/sprites.lua", -- OW sheets (Chris + NPCs)
|
||||||
"data/generated/scripts.lua", -- disassembled map scripts
|
"data/generated/scripts.lua", -- disassembled map scripts
|
||||||
"data/generated/text.lua", -- decoded Gen 2 dialogue strings
|
"data/generated/text.lua", -- decoded Gen 2 dialogue strings
|
||||||
|
-- The engine's own strings, keyed by label rather than by address. A
|
||||||
|
-- cache built before RomExtractorGen2:extractText has none, and every
|
||||||
|
-- line that reads through src/core/RomText.lua would silently keep
|
||||||
|
-- printing its Lua fallback, so this re-imports those caches rather than
|
||||||
|
-- bumping CACHE_FORMAT and dragging Red, Blue and Yellow through it too.
|
||||||
|
"data/generated/rom_text.lua",
|
||||||
"data/generated/pokemon.lua",
|
"data/generated/pokemon.lua",
|
||||||
"data/generated/tilesets.lua",
|
"data/generated/tilesets.lua",
|
||||||
"data/generated/audio.lua",
|
"data/generated/audio.lua",
|
||||||
|
|||||||
@@ -397,9 +397,12 @@ function PaletteFX.uiSpriteRedraws()
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Active named-palette table for COLORS: RED++ uses data/palettes_gbc.lua,
|
-- Active named-palette table for COLORS: RED++ uses data/palettes_gbc.lua,
|
||||||
-- everything else uses the ROM-imported data.palettes.
|
-- Yellow uses data/palettes_yellow.lua, everything else uses the ROM-imported data.palettes.
|
||||||
function PaletteFX.pack(data)
|
function PaletteFX.pack(data)
|
||||||
if PaletteFX.usesGbcPack() then
|
if GameVersion.isYellow() then
|
||||||
|
local y = PaletteFX.yellowPack()
|
||||||
|
if y then return y end
|
||||||
|
elseif PaletteFX.usesGbcPack() then
|
||||||
local g = PaletteFX.gbcPack()
|
local g = PaletteFX.gbcPack()
|
||||||
if g then return g end
|
if g then return g end
|
||||||
end
|
end
|
||||||
@@ -449,24 +452,27 @@ function PaletteFX.pal(data, name)
|
|||||||
local fromRom = romNamedPal(data, name)
|
local fromRom = romNamedPal(data, name)
|
||||||
if fromRom then return fromRom end
|
if fromRom then return fromRom end
|
||||||
end
|
end
|
||||||
|
if GameVersion.isYellow() then
|
||||||
if PaletteFX.usesYellowCgb() then
|
if PaletteFX.usesYellowCgb() then
|
||||||
local fromCgb = yellowCgbNamedPal(data, name)
|
local fromCgb = yellowCgbNamedPal(data, name)
|
||||||
if fromCgb then return fromCgb end
|
if fromCgb then return fromCgb end
|
||||||
end
|
end
|
||||||
local p = PaletteFX.pack(data)
|
|
||||||
local c = p and p.palettes and p.palettes[name]
|
|
||||||
if c then return c end
|
|
||||||
if GameVersion.isYellow() then
|
|
||||||
local y = PaletteFX.yellowPack()
|
local y = PaletteFX.yellowPack()
|
||||||
local yc = y and y.palettes and y.palettes[name]
|
local yc = y and y.palettes and y.palettes[name]
|
||||||
if yc then return yc end
|
if yc then return yc end
|
||||||
|
local fromRom = romNamedPal(data, name)
|
||||||
|
if fromRom then return fromRom end
|
||||||
end
|
end
|
||||||
|
local p = PaletteFX.pack(data)
|
||||||
|
local c = p and p.palettes and p.palettes[name]
|
||||||
|
if c then return c end
|
||||||
if PaletteFX.usesGbcPack() then
|
if PaletteFX.usesGbcPack() then
|
||||||
return romNamedPal(data, name)
|
return romNamedPal(data, name)
|
||||||
end
|
end
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
-- the species' palette (data/pokemon/palettes.asm), MEWMON for unknowns.
|
-- the species' palette (data/pokemon/palettes.asm), MEWMON for unknowns.
|
||||||
-- transformed forces PAL_GRAYMON (Ditto's palette) regardless of species
|
-- transformed forces PAL_GRAYMON (Ditto's palette) regardless of species
|
||||||
-- (engine/gfx/palettes.asm DeterminePaletteID: bit TRANSFORMED, a; a
|
-- (engine/gfx/palettes.asm DeterminePaletteID: bit TRANSFORMED, a; a
|
||||||
|
|||||||
@@ -100,10 +100,30 @@ local function positionLift(ph, contentPx, dpiY, cut)
|
|||||||
return ScreenPosition.lift(ph, contentPx, ScreenPosition.safeTop() * dpiY)
|
return ScreenPosition.lift(ph, contentPx, ScreenPosition.safeTop() * dpiY)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Free GPU canvases immediately. Overwriting the Lua reference alone leaves
|
||||||
|
-- VRAM allocated until LOVE's GC runs, which is too slow when Android loops
|
||||||
|
-- Play → launcher → Play in one process.
|
||||||
|
local function releaseCanvas(canvas)
|
||||||
|
if canvas and canvas.release then pcall(canvas.release, canvas) end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Renderer:releaseCanvases()
|
||||||
|
releaseCanvas(self.canvas); self.canvas = nil
|
||||||
|
releaseCanvas(self.battleHUDCanvas); self.battleHUDCanvas = nil
|
||||||
|
releaseCanvas(self.worldCanvas); self.worldCanvas = nil
|
||||||
|
releaseCanvas(self.uprightCanvas); self.uprightCanvas = nil
|
||||||
|
self.worldActive = false
|
||||||
|
self.uprightActive = false
|
||||||
|
self.worldOverride = nil
|
||||||
|
end
|
||||||
|
|
||||||
function Renderer:init()
|
function Renderer:init()
|
||||||
-- 160x144 real pixels, never DPI-scaled: see src/render/PixelCanvas.lua
|
-- 160x144 real pixels, never DPI-scaled: see src/render/PixelCanvas.lua
|
||||||
-- (#208). Every canvas below is sized in framebuffer pixels for the same
|
-- (#208). Every canvas below is sized in framebuffer pixels for the same
|
||||||
-- reason -- worldViewSize() already works in drawable pixels.
|
-- reason -- worldViewSize() already works in drawable pixels.
|
||||||
|
-- Release any prior session's surfaces before reallocating (in-process
|
||||||
|
-- return-to-launcher reuses this Renderer singleton).
|
||||||
|
self:releaseCanvases()
|
||||||
self.uiWidth, self.uiHeight = self.WIDTH, self.HEIGHT
|
self.uiWidth, self.uiHeight = self.WIDTH, self.HEIGHT
|
||||||
self.canvas = PixelCanvas.new(self.uiWidth, self.uiHeight, "nearest")
|
self.canvas = PixelCanvas.new(self.uiWidth, self.uiHeight, "nearest")
|
||||||
self.battleHUDCanvas = nil
|
self.battleHUDCanvas = nil
|
||||||
|
|||||||
@@ -951,6 +951,13 @@ function TileRenderer.invalidate()
|
|||||||
frameImages = {}
|
frameImages = {}
|
||||||
toggleImages = {}
|
toggleImages = {}
|
||||||
stripData = {}
|
stripData = {}
|
||||||
|
-- RED++ per-map atlases are large Images; clear and release so an
|
||||||
|
-- in-process Play → launcher → Play loop does not keep every prior
|
||||||
|
-- session's bake in VRAM / Lua heap.
|
||||||
|
for key, img in pairs(gbcAtlasCache) do
|
||||||
|
safeRelease(img)
|
||||||
|
gbcAtlasCache[key] = nil
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
Assets.register(TileRenderer.invalidate)
|
Assets.register(TileRenderer.invalidate)
|
||||||
|
|||||||
+509
-115
@@ -34,10 +34,12 @@ local DEFAULT_ART = {
|
|||||||
cableBall = "assets/generated/trade/cable_ball.png",
|
cableBall = "assets/generated/trade/cable_ball.png",
|
||||||
cableBallAlt = "assets/generated/trade/cable_ball_alt.png",
|
cableBallAlt = "assets/generated/trade/cable_ball_alt.png",
|
||||||
bubble = "assets/generated/trade/bubble.png",
|
bubble = "assets/generated/trade/bubble.png",
|
||||||
|
moveAnim0 = "assets/generated/battle/anims/move_anim_0.png",
|
||||||
|
balls = "assets/generated/battle/balls.png",
|
||||||
}
|
}
|
||||||
|
|
||||||
local function tryImage(path)
|
local function tryImage(path)
|
||||||
if not path then return nil end
|
if not (path and love and love.graphics and love.graphics.newImage) then return nil end
|
||||||
local ok, img = pcall(love.graphics.newImage, path)
|
local ok, img = pcall(love.graphics.newImage, path)
|
||||||
return ok and img or nil
|
return ok and img or nil
|
||||||
end
|
end
|
||||||
@@ -89,6 +91,92 @@ local SEQ = {
|
|||||||
"done",
|
"done",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
-- FrameBlocks 03..08 definition for move_anim_0.png (128x40, 16 tiles/row, 8x8 per tile)
|
||||||
|
local FRAME_BLOCKS = {
|
||||||
|
-- FrameBlock03: Pokeball normal (16x16)
|
||||||
|
[3] = {
|
||||||
|
{ tile = 2, dx = 0, dy = 0, xflip = false, yflip = false },
|
||||||
|
{ tile = 2, dx = 8, dy = 0, xflip = true, yflip = false },
|
||||||
|
{ tile = 18, dx = 0, dy = 8, xflip = false, yflip = false },
|
||||||
|
{ tile = 18, dx = 8, dy = 8, xflip = true, yflip = false },
|
||||||
|
},
|
||||||
|
-- FrameBlock04: Pokeball tilted left (16x16)
|
||||||
|
[4] = {
|
||||||
|
{ tile = 6, dx = 0, dy = 0, xflip = false, yflip = false },
|
||||||
|
{ tile = 7, dx = 8, dy = 0, xflip = false, yflip = false },
|
||||||
|
{ tile = 22, dx = 0, dy = 8, xflip = false, yflip = false },
|
||||||
|
{ tile = 23, dx = 8, dy = 8, xflip = false, yflip = false },
|
||||||
|
},
|
||||||
|
-- FrameBlock05: Pokeball tilted right (16x16)
|
||||||
|
[5] = {
|
||||||
|
{ tile = 7, dx = 0, dy = 0, xflip = true, yflip = false },
|
||||||
|
{ tile = 6, dx = 8, dy = 0, xflip = true, yflip = false },
|
||||||
|
{ tile = 23, dx = 0, dy = 8, xflip = true, yflip = false },
|
||||||
|
{ tile = 22, dx = 8, dy = 8, xflip = true, yflip = false },
|
||||||
|
},
|
||||||
|
-- FrameBlock06: Small Poof smoke cloud (32x32)
|
||||||
|
[6] = {
|
||||||
|
{ tile = 0x23, dx = 8, dy = 0, xflip = false, yflip = false },
|
||||||
|
{ tile = 0x32, dx = 0, dy = 8, xflip = false, yflip = false },
|
||||||
|
{ tile = 0x33, dx = 8, dy = 8, xflip = false, yflip = false },
|
||||||
|
{ tile = 0x23, dx = 16, dy = 0, xflip = true, yflip = false },
|
||||||
|
{ tile = 0x33, dx = 16, dy = 8, xflip = true, yflip = false },
|
||||||
|
{ tile = 0x32, dx = 24, dy = 8, xflip = true, yflip = false },
|
||||||
|
{ tile = 0x32, dx = 0, dy = 16, xflip = false, yflip = true },
|
||||||
|
{ tile = 0x33, dx = 8, dy = 16, xflip = false, yflip = true },
|
||||||
|
{ tile = 0x23, dx = 8, dy = 24, xflip = false, yflip = true },
|
||||||
|
{ tile = 0x33, dx = 16, dy = 16, xflip = true, yflip = true },
|
||||||
|
{ tile = 0x32, dx = 24, dy = 16, xflip = true, yflip = true },
|
||||||
|
{ tile = 0x23, dx = 16, dy = 24, xflip = true, yflip = true },
|
||||||
|
},
|
||||||
|
-- FrameBlock07: Medium Poof smoke cloud (32x32)
|
||||||
|
[7] = {
|
||||||
|
{ tile = 0x20, dx = 0, dy = 0, xflip = false, yflip = false },
|
||||||
|
{ tile = 0x21, dx = 8, dy = 0, xflip = false, yflip = false },
|
||||||
|
{ tile = 0x30, dx = 0, dy = 8, xflip = false, yflip = false },
|
||||||
|
{ tile = 0x31, dx = 8, dy = 8, xflip = false, yflip = false },
|
||||||
|
{ tile = 0x21, dx = 16, dy = 0, xflip = true, yflip = false },
|
||||||
|
{ tile = 0x20, dx = 24, dy = 0, xflip = true, yflip = false },
|
||||||
|
{ tile = 0x31, dx = 16, dy = 8, xflip = true, yflip = false },
|
||||||
|
{ tile = 0x30, dx = 24, dy = 8, xflip = true, yflip = false },
|
||||||
|
{ tile = 0x30, dx = 0, dy = 16, xflip = false, yflip = true },
|
||||||
|
{ tile = 0x31, dx = 8, dy = 16, xflip = false, yflip = true },
|
||||||
|
{ tile = 0x20, dx = 0, dy = 24, xflip = false, yflip = true },
|
||||||
|
{ tile = 0x21, dx = 8, dy = 24, xflip = false, yflip = true },
|
||||||
|
{ tile = 0x31, dx = 16, dy = 16, xflip = true, yflip = true },
|
||||||
|
{ tile = 0x30, dx = 24, dy = 16, xflip = true, yflip = true },
|
||||||
|
{ tile = 0x21, dx = 16, dy = 24, xflip = true, yflip = true },
|
||||||
|
{ tile = 0x20, dx = 24, dy = 24, xflip = true, yflip = true },
|
||||||
|
},
|
||||||
|
-- FrameBlock08: Large Poof smoke cloud (32x32)
|
||||||
|
[8] = {
|
||||||
|
{ tile = 0x22, dx = 0, dy = 0, xflip = false, yflip = false },
|
||||||
|
{ tile = 0x23, dx = 8, dy = 0, xflip = false, yflip = false },
|
||||||
|
{ tile = 0x32, dx = 0, dy = 8, xflip = false, yflip = false },
|
||||||
|
{ tile = 0x33, dx = 8, dy = 8, xflip = false, yflip = false },
|
||||||
|
{ tile = 0x23, dx = 16, dy = 0, xflip = true, yflip = false },
|
||||||
|
{ tile = 0x22, dx = 24, dy = 0, xflip = true, yflip = false },
|
||||||
|
{ tile = 0x33, dx = 16, dy = 8, xflip = true, yflip = false },
|
||||||
|
{ tile = 0x32, dx = 24, dy = 8, xflip = true, yflip = false },
|
||||||
|
{ tile = 0x32, dx = 0, dy = 16, xflip = false, yflip = true },
|
||||||
|
{ tile = 0x33, dx = 8, dy = 16, xflip = false, yflip = true },
|
||||||
|
{ tile = 0x22, dx = 0, dy = 24, xflip = false, yflip = true },
|
||||||
|
{ tile = 0x23, dx = 8, dy = 24, xflip = false, yflip = true },
|
||||||
|
{ tile = 0x33, dx = 16, dy = 16, xflip = true, yflip = true },
|
||||||
|
{ tile = 0x32, dx = 24, dy = 16, xflip = true, yflip = true },
|
||||||
|
{ tile = 0x23, dx = 16, dy = 24, xflip = true, yflip = true },
|
||||||
|
{ tile = 0x22, dx = 24, dy = 24, xflip = true, yflip = true },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
-- BallMoveDistances1 (engine/battle/animations.asm:881)
|
||||||
|
-- Pokéball jumps up into the open link cable nozzle: -12, -12, -8 (3 frames each)
|
||||||
|
local BALL_SUCTION_DISTANCES = { -12, -12, -8 }
|
||||||
|
|
||||||
|
-- BallMoveDistances2 (engine/battle/animations.asm:922)
|
||||||
|
-- Incoming Pokéball bounces out of nozzle onto the ground: 11, 12, -12, -7, 7, 12, -8, 8 (5 frames each)
|
||||||
|
local BALL_BOUNCE_DISTANCES = { 11, 12, -12, -7, 7, 12, -8, 8 }
|
||||||
|
|
||||||
function TradeAnim.new(game, opts)
|
function TradeAnim.new(game, opts)
|
||||||
opts = opts or {}
|
opts = opts or {}
|
||||||
local self = setmetatable({}, TradeAnim)
|
local self = setmetatable({}, TradeAnim)
|
||||||
@@ -120,6 +208,8 @@ function TradeAnim.new(game, opts)
|
|||||||
cableBall = tryImage(art.cableBall or DEFAULT_ART.cableBall),
|
cableBall = tryImage(art.cableBall or DEFAULT_ART.cableBall),
|
||||||
cableBallAlt = tryImage(art.cableBallAlt or DEFAULT_ART.cableBallAlt),
|
cableBallAlt = tryImage(art.cableBallAlt or DEFAULT_ART.cableBallAlt),
|
||||||
bubble = tryImage(art.bubble or DEFAULT_ART.bubble),
|
bubble = tryImage(art.bubble or DEFAULT_ART.bubble),
|
||||||
|
moveAnim0 = tryImage(DEFAULT_ART.moveAnim0),
|
||||||
|
balls = tryImage(DEFAULT_ART.balls),
|
||||||
}
|
}
|
||||||
self.sentSprite, self.sentSpriteTrueColor = spriteOf(game, self.sent)
|
self.sentSprite, self.sentSpriteTrueColor = spriteOf(game, self.sent)
|
||||||
self.recvSprite, self.recvSpriteTrueColor = spriteOf(game, self.received)
|
self.recvSprite, self.recvSpriteTrueColor = spriteOf(game, self.received)
|
||||||
@@ -136,6 +226,14 @@ function TradeAnim.new(game, opts)
|
|||||||
self.monVisible = true
|
self.monVisible = true
|
||||||
self.waitingText = false
|
self.waitingText = false
|
||||||
self.cableFlash = false
|
self.cableFlash = false
|
||||||
|
self.activeBallBlock = nil
|
||||||
|
self.activeBallX = 0
|
||||||
|
self.activeBallY = 0
|
||||||
|
self.activePoofBlock = nil
|
||||||
|
self.activePoofX = 0
|
||||||
|
self.activePoofY = 0
|
||||||
|
self.wx = 0
|
||||||
|
self.slidingBox = false
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -151,52 +249,100 @@ function TradeAnim:advance()
|
|||||||
self.sub = nil
|
self.sub = nil
|
||||||
self.flash = false
|
self.flash = false
|
||||||
self.cableFlash = false
|
self.cableFlash = false
|
||||||
self.poof = nil
|
self.activeBallBlock = nil
|
||||||
|
self.activePoofBlock = nil
|
||||||
|
self.slidingBox = false
|
||||||
|
self.wx = 0
|
||||||
|
|
||||||
if self.phase == "done" then
|
if self.phase == "done" then
|
||||||
self.game.stack:pop()
|
self.game.stack:pop()
|
||||||
if self.onDone then self.onDone() end
|
if self.onDone then self.onDone() end
|
||||||
elseif self.phase == "show_enemy" then
|
elseif self.phase == "show_enemy" then
|
||||||
self.monVisible = false
|
self.monVisible = false
|
||||||
|
self.activeBallBlock = 4
|
||||||
|
self.activeBallX, self.activeBallY = 72, 40
|
||||||
|
self.activePoofBlock = nil
|
||||||
|
self.cableSlideOut = 0
|
||||||
elseif self.phase == "transfer_lr" then
|
elseif self.phase == "transfer_lr" then
|
||||||
-- wBaseCoord $54, $1c OAM -> screen (76, 12)
|
-- BaseCoord $54, $1c -> Screen (92, 28) for 16x16 icon (center 100, 36)
|
||||||
self.monX, self.monY = 76, 12
|
self.monX, self.monY = 92, 28
|
||||||
|
self.scx = 0
|
||||||
elseif self.phase == "transfer_rl" then
|
elseif self.phase == "transfer_rl" then
|
||||||
-- wBaseCoord $64, $44 OAM -> screen (92, 52), right GB on screen
|
-- BaseCoord $64, $44 -> Screen (108, 68) for 16x16 icon (center 116, 76)
|
||||||
self.monX, self.monY = 92, 52
|
self.monX, self.monY = 108, 68
|
||||||
self.scx = 160
|
self.scx = 256
|
||||||
elseif self.phase == "ball_enter" then
|
elseif self.phase == "ball_enter" then
|
||||||
-- lb bc, $20, $60: b = Y, c = X (Trade_AnimateBallEnteringLinkCable)
|
-- Pokéball sits at BaseCoord $48 (Screen 72, 64) before shake and suction
|
||||||
|
self.activeBallBlock = 3
|
||||||
|
self.activeBallX, self.activeBallY = 72, 64
|
||||||
self.ballX, self.ballY = 0x60, 0x20
|
self.ballX, self.ballY = 0x60, 0x20
|
||||||
elseif self.phase == "open_cable" or self.phase == "open_cable2" then
|
elseif self.phase == "open_cable" or self.phase == "open_cable2" then
|
||||||
-- SCX $a0 -> $f0: cable slides in from the right, open end rests at x64
|
-- SCX $a0 (160) -> $f0 (240): cable slides in from right, open end rests at x64
|
||||||
self.scx = 0x50
|
self.scx = 80
|
||||||
|
if self.phase == "open_cable" then
|
||||||
|
self.activeBallBlock = 3
|
||||||
|
self.activeBallX, self.activeBallY = 72, 64
|
||||||
|
end
|
||||||
Sound.play(self.game.data, "Heal_HP")
|
Sound.play(self.game.data, "Heal_HP")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function TradeAnim:say(text, delay, thenFn)
|
function TradeAnim:skipHeld()
|
||||||
self.waitingText = true
|
return self.game.input:wasPressed("a") or self.game.input:wasPressed("b")
|
||||||
self.game.stack:push(TextBox.new(self.game, text, function()
|
|
||||||
self.waitingText = false
|
|
||||||
if thenFn then thenFn() else self:advance() end
|
|
||||||
end, { auto = { delay = delay or 80 } }))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function TradeAnim:skipHeld()
|
function TradeAnim:drawFrameBlock(blockId, x, y)
|
||||||
return self.game.input:wasPressed("a") or self.game.input:isDown("a")
|
local block = FRAME_BLOCKS[blockId]
|
||||||
|
if not block then return end
|
||||||
|
if self.img.moveAnim0 then
|
||||||
|
local iw, ih = self.img.moveAnim0:getDimensions()
|
||||||
|
for _, tile in ipairs(block) do
|
||||||
|
local tx = (tile.tile % 16) * 8
|
||||||
|
local ty = math.floor(tile.tile / 16) * 8
|
||||||
|
local quad = love.graphics.newQuad(tx, ty, 8, 8, iw, ih)
|
||||||
|
local sx = tile.xflip and -1 or 1
|
||||||
|
local sy = tile.yflip and -1 or 1
|
||||||
|
local ox = tile.xflip and 8 or 0
|
||||||
|
local oy = tile.yflip and 8 or 0
|
||||||
|
love.graphics.draw(self.img.moveAnim0, quad, x + tile.dx + ox, y + tile.dy + oy, 0, sx, sy)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if blockId == 3 or blockId == 4 or blockId == 5 then
|
||||||
|
love.graphics.setColor(0.9, 0.2, 0.2, 1)
|
||||||
|
love.graphics.arc("fill", x + 8, y + 8, 7, math.pi, 0)
|
||||||
|
love.graphics.setColor(0.9, 0.9, 0.9, 1)
|
||||||
|
love.graphics.arc("fill", x + 8, y + 8, 7, 0, math.pi)
|
||||||
|
love.graphics.setColor(0, 0, 0, 1)
|
||||||
|
love.graphics.circle("line", x + 8, y + 8, 7)
|
||||||
|
love.graphics.line(x + 1, y + 8, x + 15, y + 8)
|
||||||
|
love.graphics.circle("fill", x + 8, y + 8, 2)
|
||||||
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
|
elseif blockId == 6 or blockId == 7 or blockId == 8 then
|
||||||
|
local r = (blockId - 5) * 5
|
||||||
|
love.graphics.setColor(0.8, 0.8, 0.8, 0.8)
|
||||||
|
love.graphics.circle("fill", x + 16, y + 16, r)
|
||||||
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function TradeAnim:update(dt)
|
function TradeAnim:update(dt)
|
||||||
if self.waitingText or self.phase == "done" then return end
|
if self.waitingText or self.phase == "done" then return end
|
||||||
local skip = self.game.input:wasPressed("a")
|
local skip = self:skipHeld()
|
||||||
self.t = self.t + 1
|
self.t = self.t + 1
|
||||||
local p = self.phase
|
local p = self.phase
|
||||||
|
|
||||||
if p == "show_player" then
|
if p == "show_player" then
|
||||||
-- slide from SCX $86 -> 0, hold 80, poof, cry (Trade_ShowPlayerMon)
|
-- Trade_ShowPlayerMon:
|
||||||
|
-- 1. Slide from SCX $7e (126) -> 0 at 2px/frame (63 frames)
|
||||||
|
-- 2. Hold 80 frames
|
||||||
|
-- 3. TRADE_BALL_POOF_ANIM at BaseCoord $72 (Screen 64, 48): 3 frames x 6 ticks = 18 ticks
|
||||||
|
-- 4. TRADE_BALL_DROP_ANIM: mon vanishes, ball drops $41 (72, 56) -> $48 (72, 64) over 36 ticks
|
||||||
|
-- 5. PlayCry while ball sits on ground at (72, 64)
|
||||||
if not self.sub then self.sub = "slide" end
|
if not self.sub then self.sub = "slide" end
|
||||||
|
|
||||||
if self.sub == "slide" then
|
if self.sub == "slide" then
|
||||||
self.scx = math.max(0, 0x86 - self.t * 2)
|
self.scx = math.max(0, 126 - self.t * 2)
|
||||||
if skip or self.scx <= 0 then
|
if skip or self.scx <= 0 then
|
||||||
self.scx = 0
|
self.scx = 0
|
||||||
self.sub = "hold"
|
self.sub = "hold"
|
||||||
@@ -206,52 +352,131 @@ function TradeAnim:update(dt)
|
|||||||
if skip or self.t >= 80 then
|
if skip or self.t >= 80 then
|
||||||
self.sub = "poof"
|
self.sub = "poof"
|
||||||
self.t = 0
|
self.t = 0
|
||||||
self.monVisible = false
|
|
||||||
Sound.play(self.game.data, "Ball_Poof")
|
Sound.play(self.game.data, "Ball_Poof")
|
||||||
end
|
end
|
||||||
elseif self.sub == "poof" then
|
elseif self.sub == "poof" then
|
||||||
self.poof = math.max(0, 16 - self.t)
|
local step = math.floor(self.t / 6)
|
||||||
if skip or self.t >= 16 then
|
if step >= 3 or skip then
|
||||||
|
self.sub = "ball_drop"
|
||||||
|
self.t = 0
|
||||||
|
self.activePoofBlock = nil
|
||||||
|
self.monVisible = false
|
||||||
|
else
|
||||||
|
self.activePoofBlock = 6 + step
|
||||||
|
self.activePoofX, self.activePoofY = 64, 48
|
||||||
|
end
|
||||||
|
elseif self.sub == "ball_drop" then
|
||||||
|
if skip or self.t >= 36 then
|
||||||
|
self.sub = "cry"
|
||||||
|
self.t = 0
|
||||||
|
self.activeBallBlock = 3
|
||||||
|
self.activeBallX, self.activeBallY = 72, 64
|
||||||
Sound.playCry(self.game.data, self.sent.species)
|
Sound.playCry(self.game.data, self.sent.species)
|
||||||
|
else
|
||||||
|
local step = math.floor(self.t / 6)
|
||||||
|
if step == 0 then
|
||||||
|
self.activeBallBlock = 3
|
||||||
|
self.activeBallX, self.activeBallY = 72, 56
|
||||||
|
else
|
||||||
|
local blocks = { 3, 4, 3, 5, 3 }
|
||||||
|
self.activeBallBlock = blocks[step] or 3
|
||||||
|
self.activeBallX, self.activeBallY = 72, 64
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif self.sub == "cry" then
|
||||||
|
if skip or self.t >= 40 then
|
||||||
self.sub = nil
|
self.sub = nil
|
||||||
self:advance()
|
self:advance()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
elseif p == "open_cable" or p == "open_cable2" then
|
elseif p == "open_cable" or p == "open_cable2" then
|
||||||
-- 20 steps of 4px, matching the SCX loop
|
-- Trade_DrawOpenEndOfLinkCable: SCX $a0 (160) -> $f0 (240) in 20 steps of 4px
|
||||||
self.scx = math.max(0, 0x50 - self.t * 4)
|
self.scx = math.max(0, 80 - self.t * 4)
|
||||||
if skip or self.scx <= 0 then
|
if skip or self.scx <= 0 then
|
||||||
self.scx = 0
|
self.scx = 0
|
||||||
self:advance()
|
self:advance()
|
||||||
end
|
end
|
||||||
|
|
||||||
elseif p == "ball_enter" then
|
elseif p == "ball_enter" then
|
||||||
-- TRADE_BALL_SHAKE then ball rides the cable; X from $60 toward $a0
|
-- Trade_AnimateBallEnteringLinkCable:
|
||||||
if self.t < 20 then
|
-- 1. TRADE_BALL_SHAKE_ANIM (16 ticks) + TradeShakePokeball suction jump (-12, -12, -8 over 9 ticks):
|
||||||
if skip then self.t = 20 end
|
-- - Shakes at (72, 64) for 16 ticks (FrameBlock 04, 03, 05, 03)
|
||||||
return
|
-- - Jumps up: Y=52 (3 ticks), Y=40 (3 ticks), Y=32 (3 ticks)
|
||||||
|
-- - Ball cleared, SFX_TRADE_MACHINE plays
|
||||||
|
-- 2. DelayFrames 10 (chills for 10 frames)
|
||||||
|
-- 3. Ball inside horizontal cable: 16 steps of +4px every 3 frames (48 frames) with Tink sfx & bulge toggle
|
||||||
|
-- 4. Delay3
|
||||||
|
if not self.sub then self.sub = "shake" end
|
||||||
|
|
||||||
|
if self.sub == "shake" then
|
||||||
|
if skip or self.t >= 16 then
|
||||||
|
self.sub = "jump_up"
|
||||||
|
self.t = 0
|
||||||
|
else
|
||||||
|
local shakeBlocks = { 4, 3, 5, 3 }
|
||||||
|
local step = math.floor(self.t / 4) + 1
|
||||||
|
self.activeBallBlock = shakeBlocks[step] or 3
|
||||||
|
self.activeBallX, self.activeBallY = 72, 64
|
||||||
|
end
|
||||||
|
elseif self.sub == "jump_up" then
|
||||||
|
local step = math.floor(self.t / 3) + 1
|
||||||
|
if step > #BALL_SUCTION_DISTANCES or skip then
|
||||||
|
self.sub = "chill"
|
||||||
|
self.t = 0
|
||||||
|
self.activeBallBlock = nil
|
||||||
|
Sound.play(self.game.data, "Trade_Machine")
|
||||||
|
else
|
||||||
|
local yPos = 64
|
||||||
|
for i = 1, step do
|
||||||
|
yPos = yPos + BALL_SUCTION_DISTANCES[i]
|
||||||
|
end
|
||||||
|
self.activeBallBlock = 3
|
||||||
|
self.activeBallX, self.activeBallY = 72, yPos
|
||||||
|
end
|
||||||
|
elseif self.sub == "chill" then
|
||||||
|
if skip or self.t >= 10 then
|
||||||
|
self.sub = "suction"
|
||||||
|
self.t = 0
|
||||||
|
self.ballX = 0x60
|
||||||
|
end
|
||||||
|
elseif self.sub == "suction" then
|
||||||
|
local step = math.floor(self.t / 3)
|
||||||
|
if step >= 16 or skip then
|
||||||
|
self.sub = "exit_pause"
|
||||||
|
self.t = 0
|
||||||
|
self.ballX = 0xA0
|
||||||
|
else
|
||||||
|
self.ballX = 0x60 + step * 4
|
||||||
|
self.flash = (step % 2 == 1)
|
||||||
|
if self.t % 3 == 0 and step < 16 then
|
||||||
|
Sound.play(self.game.data, "Tink")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif self.sub == "exit_pause" then
|
||||||
|
if skip or self.t >= 3 then
|
||||||
|
self.sub = nil
|
||||||
|
self:advance()
|
||||||
end
|
end
|
||||||
local step = self.t - 20
|
|
||||||
if step % 3 == 0 then
|
|
||||||
self.ballX = 0x60 + math.floor(step / 3) * 4
|
|
||||||
self.flash = not self.flash
|
|
||||||
if self.ballX < 0xA0 then Sound.play(self.game.data, "Tink") end
|
|
||||||
end
|
end
|
||||||
if skip then self.ballX = 0xA0 end
|
|
||||||
if self.ballX >= 0xA0 then self:advance() end
|
|
||||||
|
|
||||||
elseif p == "transfer_lr" then
|
elseif p == "transfer_lr" then
|
||||||
-- scroll left GB off while the mon rides the cable, then the sprite
|
-- 16 units of 16px (256px total scroll) at 2px/frame = 128 frames (SCX 0 -> 256)
|
||||||
-- itself moves right and down into the right GB (Trade_AnimMonMoveVertical)
|
if self.t <= 128 then
|
||||||
if self.t <= 80 then
|
self.scx = math.min(256, self.t * 2)
|
||||||
self.scx = math.min(160, self.t * 2)
|
self.monX, self.monY = 92, 28
|
||||||
self.monX, self.monY = 76, 12
|
elseif self.t <= 128 + 32 then
|
||||||
elseif self.t <= 80 + 32 then
|
local step = math.floor((self.t - 128) / 8) + 1
|
||||||
self.monX = 76 + math.floor((self.t - 80) / 8) * 4
|
step = math.min(4, step)
|
||||||
elseif self.t <= 80 + 64 then
|
self.scx = 256
|
||||||
self.monX = 92
|
self.monX = 92 + step * 4
|
||||||
self.monY = 12 + math.floor((self.t - 112) / 8) * 10
|
self.monY = 28
|
||||||
|
elseif self.t <= 128 + 64 then
|
||||||
|
local step = math.floor((self.t - 160) / 8) + 1
|
||||||
|
step = math.min(4, step)
|
||||||
|
self.scx = 256
|
||||||
|
self.monX = 108
|
||||||
|
self.monY = 28 + step * 10
|
||||||
else
|
else
|
||||||
self:advance()
|
self:advance()
|
||||||
return
|
return
|
||||||
@@ -260,17 +485,22 @@ function TradeAnim:update(dt)
|
|||||||
if skip then self:advance() end
|
if skip then self:advance() end
|
||||||
|
|
||||||
elseif p == "transfer_rl" then
|
elseif p == "transfer_rl" then
|
||||||
-- mirror: sprite climbs out of the right GB, then the screen scrolls
|
-- 64 frames vertical ascent + 128 frames horizontal scroll (SCX 256 -> 0)
|
||||||
-- back to the left GB
|
|
||||||
if self.t <= 32 then
|
if self.t <= 32 then
|
||||||
self.scx = 160
|
local step = math.floor(self.t / 8) + 1
|
||||||
self.monX, self.monY = 92, 52 - math.floor(self.t / 8) * 10
|
step = math.min(4, step)
|
||||||
|
self.scx = 256
|
||||||
|
self.monX = 108
|
||||||
|
self.monY = 68 - step * 10
|
||||||
elseif self.t <= 64 then
|
elseif self.t <= 64 then
|
||||||
self.monX = 92 - math.floor((self.t - 32) / 8) * 4
|
local step = math.floor((self.t - 32) / 8) + 1
|
||||||
self.monY = 12
|
step = math.min(4, step)
|
||||||
elseif self.t <= 64 + 80 then
|
self.scx = 256
|
||||||
self.scx = math.max(0, 160 - (self.t - 64) * 2)
|
self.monX = 108 - step * 4
|
||||||
self.monX, self.monY = 76, 12
|
self.monY = 28
|
||||||
|
elseif self.t <= 64 + 128 then
|
||||||
|
self.scx = math.max(0, 256 - (self.t - 64) * 2)
|
||||||
|
self.monX, self.monY = 92, 28
|
||||||
else
|
else
|
||||||
self:advance()
|
self:advance()
|
||||||
return
|
return
|
||||||
@@ -282,54 +512,185 @@ function TradeAnim:update(dt)
|
|||||||
if skip or self.t >= 100 then self:advance() end
|
if skip or self.t >= 100 then self:advance() end
|
||||||
|
|
||||||
elseif p == "went_to" then
|
elseif p == "went_to" then
|
||||||
|
if not self.sub then
|
||||||
|
self.sub = "text"
|
||||||
|
self.t = 0
|
||||||
local text = expand(self.game, "_TradeWentToText", {
|
local text = expand(self.game, "_TradeWentToText", {
|
||||||
["RAM:wStringBuffer"] = speciesName(self.game, self.sent),
|
["RAM:wStringBuffer"] = speciesName(self.game, self.sent),
|
||||||
["RAM:wLinkEnemyTrainerName"] = self.enemyName,
|
["RAM:wLinkEnemyTrainerName"] = self.enemyName,
|
||||||
})
|
})
|
||||||
self:say(text, 200)
|
self.dialogText = text
|
||||||
|
end
|
||||||
|
|
||||||
|
if self.sub == "text" then
|
||||||
|
if skip or self.t >= 200 then
|
||||||
|
self.sub = "slide_hold"
|
||||||
|
self.t = 0
|
||||||
|
end
|
||||||
|
elseif self.sub == "slide_hold" then
|
||||||
|
if skip or self.t >= 50 then
|
||||||
|
self.sub = "slide_off"
|
||||||
|
self.t = 0
|
||||||
|
self.slidingBox = true
|
||||||
|
end
|
||||||
|
elseif self.sub == "slide_off" then
|
||||||
|
self.wx = math.min(160, self.t * 2)
|
||||||
|
if skip or self.wx >= 160 then
|
||||||
|
self.wx = 160
|
||||||
|
self.sub = "slide_end_pause"
|
||||||
|
self.t = 0
|
||||||
|
end
|
||||||
|
elseif self.sub == "slide_end_pause" then
|
||||||
|
if skip or self.t >= 10 then
|
||||||
|
self.slidingBox = false
|
||||||
|
self.dialogText = nil
|
||||||
|
self.sub = nil
|
||||||
|
self:advance()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
elseif p == "for_sends" then
|
elseif p == "for_sends" then
|
||||||
local a = expand(self.game, "_TradeForText", {
|
if not self.sub then
|
||||||
|
self.sub = "for_text"
|
||||||
|
self.t = 0
|
||||||
|
self.dialogText = expand(self.game, "_TradeForText", {
|
||||||
["RAM:wStringBuffer"] = speciesName(self.game, self.sent),
|
["RAM:wStringBuffer"] = speciesName(self.game, self.sent),
|
||||||
})
|
})
|
||||||
local b = expand(self.game, "_TradeSendsText", {
|
end
|
||||||
|
|
||||||
|
if self.sub == "for_text" then
|
||||||
|
if skip or self.t >= 80 then
|
||||||
|
self.sub = "sends_text"
|
||||||
|
self.t = 0
|
||||||
|
self.dialogText = expand(self.game, "_TradeSendsText", {
|
||||||
["RAM:wLinkEnemyTrainerName"] = self.enemyName,
|
["RAM:wLinkEnemyTrainerName"] = self.enemyName,
|
||||||
["RAM:wNameBuffer"] = nameOf(self.game, self.received),
|
["RAM:wNameBuffer"] = nameOf(self.game, self.received),
|
||||||
})
|
})
|
||||||
self.waitingText = true
|
end
|
||||||
self.game.stack:push(TextBox.new(self.game, a, function()
|
elseif self.sub == "sends_text" then
|
||||||
self.game.stack:push(TextBox.new(self.game, b, function()
|
if skip or self.t >= 80 then
|
||||||
self.waitingText = false
|
self.dialogText = nil
|
||||||
|
self.sub = nil
|
||||||
self:advance()
|
self:advance()
|
||||||
end, { auto = { delay = 80 } }))
|
end
|
||||||
end, { auto = { delay = 80 } }))
|
end
|
||||||
|
|
||||||
elseif p == "farewell" then
|
elseif p == "farewell" then
|
||||||
local a = expand(self.game, "_TradeWavesFarewellText", {
|
if not self.sub then
|
||||||
|
self.sub = "farewell_text"
|
||||||
|
self.t = 0
|
||||||
|
self.dialogText = expand(self.game, "_TradeWavesFarewellText", {
|
||||||
["RAM:wLinkEnemyTrainerName"] = self.enemyName,
|
["RAM:wLinkEnemyTrainerName"] = self.enemyName,
|
||||||
})
|
})
|
||||||
local b = expand(self.game, "_TradeTransferredText", {
|
end
|
||||||
|
|
||||||
|
if self.sub == "farewell_text" then
|
||||||
|
if skip or self.t >= 80 then
|
||||||
|
self.sub = "transferred_text"
|
||||||
|
self.t = 0
|
||||||
|
self.dialogText = expand(self.game, "_TradeTransferredText", {
|
||||||
["RAM:wNameBuffer"] = nameOf(self.game, self.received),
|
["RAM:wNameBuffer"] = nameOf(self.game, self.received),
|
||||||
})
|
})
|
||||||
self.waitingText = true
|
end
|
||||||
self.game.stack:push(TextBox.new(self.game, a, function()
|
elseif self.sub == "transferred_text" then
|
||||||
self.game.stack:push(TextBox.new(self.game, b, function()
|
if skip or self.t >= 80 then
|
||||||
self.waitingText = false
|
self.sub = "slide_hold"
|
||||||
|
self.t = 0
|
||||||
|
end
|
||||||
|
elseif self.sub == "slide_hold" then
|
||||||
|
if skip or self.t >= 50 then
|
||||||
|
self.sub = "slide_off"
|
||||||
|
self.t = 0
|
||||||
|
self.slidingBox = true
|
||||||
|
end
|
||||||
|
elseif self.sub == "slide_off" then
|
||||||
|
self.wx = math.min(160, self.t * 2)
|
||||||
|
if skip or self.wx >= 160 then
|
||||||
|
self.wx = 160
|
||||||
|
self.sub = "slide_end_pause"
|
||||||
|
self.t = 0
|
||||||
|
end
|
||||||
|
elseif self.sub == "slide_end_pause" then
|
||||||
|
if skip or self.t >= 10 then
|
||||||
|
self.slidingBox = false
|
||||||
|
self.dialogText = nil
|
||||||
|
self.sub = nil
|
||||||
self:advance()
|
self:advance()
|
||||||
end, { auto = { delay = 80 } }))
|
end
|
||||||
end, { auto = { delay = 80 } }))
|
end
|
||||||
|
|
||||||
elseif p == "show_enemy" then
|
elseif p == "show_enemy" then
|
||||||
if self.t == 1 then
|
-- Trade_ShowEnemyMon (engine/movie/trade.asm:354):
|
||||||
Sound.play(self.game.data, "Ball_Poof")
|
-- 1. TRADE_BALL_TILT_ANIM with TradeJumpPokeball (8 bounce steps, 5 ticks each = 40 ticks):
|
||||||
|
-- - Starts at BaseCoord $84 (Screen X=72, Y=40)
|
||||||
|
-- - Moves Y by BallMoveDistances2 (+11, +12, -12, -7, +7, +12, -8, +8)
|
||||||
|
-- - On each step: Cable slides off-screen right by 8px
|
||||||
|
-- - Plays SFX_SWAP on impacts
|
||||||
|
-- 2. ClearScreen, Info box at (4, 10), Mon front sprite at (7, 2)
|
||||||
|
-- 3. TRADE_BALL_POOF_ANIM: FrameBlock 06, 07, 08 (18 ticks), SFX_BALL_POOF, revealing mon
|
||||||
|
-- 4. PlayCry
|
||||||
|
-- 5. Trade_Delay100
|
||||||
|
-- 6. Info box cleared, PrintTradeTakeCareText (80 ticks)
|
||||||
|
-- 7. Trade_Delay100 -> finish
|
||||||
|
if not self.sub then
|
||||||
|
self.sub = "ball_bounce"
|
||||||
|
self.t = 0
|
||||||
|
self.cableSlideOut = 0
|
||||||
|
self.activeBallBlock = 4
|
||||||
|
self.activeBallX, self.activeBallY = 72, 40
|
||||||
|
end
|
||||||
|
|
||||||
|
if self.sub == "ball_bounce" then
|
||||||
|
local step = math.floor(self.t / 5) + 1
|
||||||
|
if step > #BALL_BOUNCE_DISTANCES or skip then
|
||||||
|
self.sub = "poof"
|
||||||
|
self.t = 0
|
||||||
|
self.activeBallBlock = nil
|
||||||
self.monVisible = true
|
self.monVisible = true
|
||||||
elseif self.t == 20 then
|
self.cableSlideOut = nil
|
||||||
|
Sound.play(self.game.data, "Ball_Poof")
|
||||||
|
else
|
||||||
|
local yPos = 40
|
||||||
|
for i = 1, step do
|
||||||
|
yPos = yPos + BALL_BOUNCE_DISTANCES[i]
|
||||||
|
end
|
||||||
|
self.activeBallBlock = 4
|
||||||
|
self.activeBallX, self.activeBallY = 72, yPos
|
||||||
|
self.cableSlideOut = (step - 1) * 8
|
||||||
|
if self.t % 5 == 0 and (BALL_BOUNCE_DISTANCES[step] == 12 or step == #BALL_BOUNCE_DISTANCES) then
|
||||||
|
Sound.play(self.game.data, "Swap")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif self.sub == "poof" then
|
||||||
|
local step = math.floor(self.t / 6)
|
||||||
|
if step >= 3 or skip then
|
||||||
|
self.sub = "cry"
|
||||||
|
self.t = 0
|
||||||
|
self.activePoofBlock = nil
|
||||||
Sound.playCry(self.game.data, self.received.species)
|
Sound.playCry(self.game.data, self.received.species)
|
||||||
elseif self.t >= 120 or skip then
|
else
|
||||||
local text = expand(self.game, "_TradeTakeCareText", {
|
self.activePoofBlock = 6 + step
|
||||||
|
self.activePoofX, self.activePoofY = 64, 48
|
||||||
|
end
|
||||||
|
elseif self.sub == "cry" then
|
||||||
|
if skip or self.t >= 100 then
|
||||||
|
self.sub = "take_care"
|
||||||
|
self.t = 0
|
||||||
|
self.dialogText = expand(self.game, "_TradeTakeCareText", {
|
||||||
["RAM:wNameBuffer"] = nameOf(self.game, self.received),
|
["RAM:wNameBuffer"] = nameOf(self.game, self.received),
|
||||||
})
|
})
|
||||||
self:say(text, 80)
|
end
|
||||||
|
elseif self.sub == "take_care" then
|
||||||
|
if skip or self.t >= 80 then
|
||||||
|
self.sub = "delay_end"
|
||||||
|
self.t = 0
|
||||||
|
self.dialogText = nil
|
||||||
|
end
|
||||||
|
elseif self.sub == "delay_end" then
|
||||||
|
if skip or self.t >= 100 then
|
||||||
|
self.sub = nil
|
||||||
|
self:advance()
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -337,10 +698,18 @@ end
|
|||||||
local function drawCableHoriz(self, y, x0, x1)
|
local function drawCableHoriz(self, y, x0, x1)
|
||||||
local w = math.max(0, x1 - x0)
|
local w = math.max(0, x1 - x0)
|
||||||
if w <= 0 then return end
|
if w <= 0 then return end
|
||||||
|
if self.cableFlash then
|
||||||
|
love.graphics.setColor(0.65, 0.65, 0.65, 1)
|
||||||
|
else
|
||||||
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
|
end
|
||||||
if self.img.cableHoriz then
|
if self.img.cableHoriz then
|
||||||
local iw, ih = self.img.cableHoriz:getDimensions()
|
local iw, ih = self.img.cableHoriz:getDimensions()
|
||||||
local quad = love.graphics.newQuad(0, 0, math.min(w, iw), ih, iw, ih)
|
for x = x0, x1 - 1, iw do
|
||||||
love.graphics.draw(self.img.cableHoriz, quad, x0, y)
|
local drawW = math.min(iw, x1 - x)
|
||||||
|
local quad = love.graphics.newQuad(0, 0, drawW, ih, iw, ih)
|
||||||
|
love.graphics.draw(self.img.cableHoriz, quad, x, y)
|
||||||
|
end
|
||||||
elseif self.img.cableSeg then
|
elseif self.img.cableSeg then
|
||||||
for x = x0, x1 - 8, 8 do
|
for x = x0, x1 - 8, 8 do
|
||||||
love.graphics.draw(self.img.cableSeg, x, y)
|
love.graphics.draw(self.img.cableSeg, x, y)
|
||||||
@@ -348,13 +717,11 @@ local function drawCableHoriz(self, y, x0, x1)
|
|||||||
else
|
else
|
||||||
love.graphics.setColor(0.2, 0.2, 0.2, 1)
|
love.graphics.setColor(0.2, 0.2, 0.2, 1)
|
||||||
love.graphics.rectangle("fill", x0, y + 1, w, 6)
|
love.graphics.rectangle("fill", x0, y + 1, w, 6)
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
|
||||||
end
|
end
|
||||||
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
function TradeAnim:drawMonInfo(mon, ot, otId, boxTy)
|
function TradeAnim:drawMonInfo(mon, ot, otId, boxTy)
|
||||||
-- Trade_PrintPlayerMonInfoText: rows +0/+2/+4/+6 from the box top; the
|
|
||||||
-- No. line replaces part of the top border (hlcoord 5, 0 in trade2.asm)
|
|
||||||
Font.drawBox(4, boxTy, 12, 8)
|
Font.drawBox(4, boxTy, 12, 8)
|
||||||
local y0 = boxTy * 8
|
local y0 = boxTy * 8
|
||||||
local no = ("No.%03d"):format(dexOf(self.game, mon))
|
local no = ("No.%03d"):format(dexOf(self.game, mon))
|
||||||
@@ -368,16 +735,6 @@ function TradeAnim:drawMonInfo(mon, ot, otId, boxTy)
|
|||||||
love.graphics.setColor(1, 1, 1, 1)
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Trade_WriteCircledMonOAM: the mon crosses the cable as its party-menu
|
|
||||||
-- sprite (wMonPartySpriteSpecies -> WriteMonPartySpriteOAMBySpecies), not as
|
|
||||||
-- its battle pic, and Trade_AnimCircledMon flips both it and the ring to
|
|
||||||
-- their second frame every step. The ring is four OAM blocks --
|
|
||||||
-- Trade_CircleOAMBlocks .OAMBlock0-3 at (8,8) (24,8) (8,24) (24,24) with the
|
|
||||||
-- X/Y flips -- so the 16x32 bubble sheet holds one quadrant per frame and the
|
|
||||||
-- circle it makes is 32x32 around the 16x16 icon. The icon rides OAM
|
|
||||||
-- block 0 and the circle blocks 1-4 (Trade_WriteCircleOAMBlock counts a up
|
|
||||||
-- from 1), and the lower OAM index wins overlap on DMG, so the icon draws
|
|
||||||
-- on top of the circle's filled interior. #750
|
|
||||||
function TradeAnim:drawIconInBubble(mon, x, y)
|
function TradeAnim:drawIconInBubble(mon, x, y)
|
||||||
if self.img.bubble then
|
if self.img.bubble then
|
||||||
if not self.bubbleQuad then
|
if not self.bubbleQuad then
|
||||||
@@ -415,11 +772,14 @@ function TradeAnim:drawGameBoy(x, y)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function TradeAnim:drawLeftGB()
|
function TradeAnim:drawLeftGB()
|
||||||
-- cable from GB to the right edge
|
if self.cableFlash then
|
||||||
|
love.graphics.setColor(0.65, 0.65, 0.65, 1)
|
||||||
|
end
|
||||||
if self.img.cableConn then
|
if self.img.cableConn then
|
||||||
love.graphics.draw(self.img.cableConn, 88, 32)
|
love.graphics.draw(self.img.cableConn, 88, 32)
|
||||||
end
|
end
|
||||||
drawCableHoriz(self, 32, 96, 160)
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
|
drawCableHoriz(self, 32, 96, 256)
|
||||||
self:drawGameBoy(40, 24)
|
self:drawGameBoy(40, 24)
|
||||||
Font.drawBox(4, 12, 9, 4)
|
Font.drawBox(4, 12, 9, 4)
|
||||||
love.graphics.setColor(0, 0, 0, 1)
|
love.graphics.setColor(0, 0, 0, 1)
|
||||||
@@ -429,6 +789,9 @@ end
|
|||||||
|
|
||||||
function TradeAnim:drawRightGB()
|
function TradeAnim:drawRightGB()
|
||||||
drawCableHoriz(self, 32, 0, 112)
|
drawCableHoriz(self, 32, 0, 112)
|
||||||
|
if self.cableFlash then
|
||||||
|
love.graphics.setColor(0.65, 0.65, 0.65, 1)
|
||||||
|
end
|
||||||
if self.img.cableCorner then love.graphics.draw(self.img.cableCorner, 112, 32) end
|
if self.img.cableCorner then love.graphics.draw(self.img.cableCorner, 112, 32) end
|
||||||
if self.img.cableVert then
|
if self.img.cableVert then
|
||||||
for i = 1, 4 do
|
for i = 1, 4 do
|
||||||
@@ -437,6 +800,7 @@ function TradeAnim:drawRightGB()
|
|||||||
end
|
end
|
||||||
if self.img.cableEnd then love.graphics.draw(self.img.cableEnd, 112, 72) end
|
if self.img.cableEnd then love.graphics.draw(self.img.cableEnd, 112, 72) end
|
||||||
if self.img.cableConn then love.graphics.draw(self.img.cableConn, 104, 72) end
|
if self.img.cableConn then love.graphics.draw(self.img.cableConn, 104, 72) end
|
||||||
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
self:drawGameBoy(56, 64)
|
self:drawGameBoy(56, 64)
|
||||||
Font.drawBox(6, 0, 9, 4)
|
Font.drawBox(6, 0, 9, 4)
|
||||||
love.graphics.setColor(0, 0, 0, 1)
|
love.graphics.setColor(0, 0, 0, 1)
|
||||||
@@ -444,6 +808,24 @@ function TradeAnim:drawRightGB()
|
|||||||
love.graphics.setColor(1, 1, 1, 1)
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function TradeAnim:drawDialog()
|
||||||
|
if not self.dialogText then return end
|
||||||
|
love.graphics.push()
|
||||||
|
if self.slidingBox then
|
||||||
|
love.graphics.translate(self.wx, 0)
|
||||||
|
end
|
||||||
|
Font.drawBox(0, 12, 20, 6)
|
||||||
|
love.graphics.setColor(0, 0, 0, 1)
|
||||||
|
local lines = {}
|
||||||
|
for line in self.dialogText:gmatch("[^\n]+") do
|
||||||
|
lines[#lines + 1] = line
|
||||||
|
end
|
||||||
|
if lines[1] then Font.draw(lines[1], 8, 112) end
|
||||||
|
if lines[2] then Font.draw(lines[2], 8, 128) end
|
||||||
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
|
love.graphics.pop()
|
||||||
|
end
|
||||||
|
|
||||||
function TradeAnim:draw()
|
function TradeAnim:draw()
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
love.graphics.rectangle("fill", 0, 0, 160, 144)
|
love.graphics.rectangle("fill", 0, 0, 160, 144)
|
||||||
@@ -452,10 +834,7 @@ function TradeAnim:draw()
|
|||||||
if p == "show_player" then
|
if p == "show_player" then
|
||||||
love.graphics.push()
|
love.graphics.push()
|
||||||
love.graphics.translate(-self.scx, 0)
|
love.graphics.translate(-self.scx, 0)
|
||||||
-- mon pic on the BG at hlcoord 7, 2; info box on the window at
|
|
||||||
-- hWY $50, so it sits in the bottom half of the screen
|
|
||||||
if self.monVisible and self.sentSprite then
|
if self.monVisible and self.sentSprite then
|
||||||
-- engine/movie/trade.asm:751
|
|
||||||
love.graphics.draw(self.sentSprite, 56 + self.sentSprite:getWidth(), 16, 0, -1, 1)
|
love.graphics.draw(self.sentSprite, 56 + self.sentSprite:getWidth(), 16, 0, -1, 1)
|
||||||
if self.sentSpriteTrueColor then
|
if self.sentSpriteTrueColor then
|
||||||
require("src.render.PaletteFX").markTrueColor(
|
require("src.render.PaletteFX").markTrueColor(
|
||||||
@@ -464,25 +843,32 @@ function TradeAnim:draw()
|
|||||||
end
|
end
|
||||||
self:drawMonInfo(self.sent, self.playerOt, self.playerOtId, 10)
|
self:drawMonInfo(self.sent, self.playerOt, self.playerOtId, 10)
|
||||||
love.graphics.pop()
|
love.graphics.pop()
|
||||||
if self.poof and self.poof > 0 then
|
|
||||||
love.graphics.setColor(0, 0, 0, self.poof / 16)
|
if self.activePoofBlock then
|
||||||
love.graphics.circle("fill", 80, 40, 20 - (self.poof or 0))
|
self:drawFrameBlock(self.activePoofBlock, self.activePoofX, self.activePoofY)
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
end
|
||||||
|
if self.activeBallBlock then
|
||||||
|
self:drawFrameBlock(self.activeBallBlock, self.activeBallX, self.activeBallY)
|
||||||
end
|
end
|
||||||
|
|
||||||
elseif p == "open_cable" or p == "open_cable2" then
|
elseif p == "open_cable" or p == "open_cable2" then
|
||||||
love.graphics.push()
|
love.graphics.push()
|
||||||
love.graphics.translate(self.scx, 0)
|
love.graphics.translate(self.scx, 0)
|
||||||
if self.img.openCable then
|
if self.img.openCable then
|
||||||
love.graphics.draw(self.img.openCable, 64, 16) -- open end at SCX $f0
|
love.graphics.draw(self.img.openCable, 64, 16)
|
||||||
end
|
end
|
||||||
love.graphics.pop()
|
love.graphics.pop()
|
||||||
|
if p == "open_cable" and self.activeBallBlock then
|
||||||
|
self:drawFrameBlock(self.activeBallBlock, self.activeBallX, self.activeBallY)
|
||||||
|
end
|
||||||
|
|
||||||
elseif p == "ball_enter" then
|
elseif p == "ball_enter" then
|
||||||
if self.img.openCable then
|
if self.img.openCable then
|
||||||
love.graphics.draw(self.img.openCable, 64, 16)
|
love.graphics.draw(self.img.openCable, 64, 16)
|
||||||
end
|
end
|
||||||
-- OAM coords carry a (+8, +16) hardware offset
|
if self.activeBallBlock then
|
||||||
|
self:drawFrameBlock(self.activeBallBlock, self.activeBallX, self.activeBallY)
|
||||||
|
elseif self.sub == "suction" or self.sub == "exit_pause" then
|
||||||
local ball = self.flash and (self.img.cableBallAlt or self.img.cableBall)
|
local ball = self.flash and (self.img.cableBallAlt or self.img.cableBall)
|
||||||
or self.img.cableBall
|
or self.img.cableBall
|
||||||
if ball then
|
if ball then
|
||||||
@@ -492,36 +878,44 @@ function TradeAnim:draw()
|
|||||||
love.graphics.circle("fill", self.ballX, self.ballY - 8, 6)
|
love.graphics.circle("fill", self.ballX, self.ballY - 8, 6)
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
elseif p == "transfer_lr" or p == "transfer_rl" then
|
elseif p == "transfer_lr" or p == "transfer_rl" then
|
||||||
-- two-screen world: left GB scene at x0, right GB scene at x160;
|
|
||||||
-- the mon icon stays in screen space like the OAM sprite it ports
|
|
||||||
love.graphics.push()
|
love.graphics.push()
|
||||||
love.graphics.translate(-self.scx, 0)
|
love.graphics.translate(-self.scx, 0)
|
||||||
self:drawLeftGB()
|
self:drawLeftGB()
|
||||||
love.graphics.translate(160, 0)
|
love.graphics.translate(256, 0)
|
||||||
self:drawRightGB()
|
self:drawRightGB()
|
||||||
love.graphics.pop()
|
love.graphics.pop()
|
||||||
local mon = p == "transfer_lr" and self.sent or self.received
|
local mon = p == "transfer_lr" and self.sent or self.received
|
||||||
self:drawIconInBubble(mon, self.monX, self.monY)
|
self:drawIconInBubble(mon, self.monX, self.monY)
|
||||||
if self.cableFlash then
|
|
||||||
love.graphics.setColor(1, 1, 1, 0.15)
|
|
||||||
love.graphics.rectangle("fill", 0, 32, 160, 8)
|
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
|
||||||
end
|
|
||||||
|
|
||||||
elseif p == "show_enemy" then
|
elseif p == "show_enemy" then
|
||||||
|
if self.cableSlideOut and self.img.openCable then
|
||||||
|
love.graphics.push()
|
||||||
|
love.graphics.translate(self.cableSlideOut, 0)
|
||||||
|
love.graphics.draw(self.img.openCable, 64, 16)
|
||||||
|
love.graphics.pop()
|
||||||
|
end
|
||||||
if self.monVisible and self.recvSprite then
|
if self.monVisible and self.recvSprite then
|
||||||
-- engine/movie/trade.asm:751
|
|
||||||
love.graphics.draw(self.recvSprite, 56 + self.recvSprite:getWidth(), 16, 0, -1, 1)
|
love.graphics.draw(self.recvSprite, 56 + self.recvSprite:getWidth(), 16, 0, -1, 1)
|
||||||
if self.recvSpriteTrueColor then
|
if self.recvSpriteTrueColor then
|
||||||
require("src.render.PaletteFX").markTrueColor(
|
require("src.render.PaletteFX").markTrueColor(
|
||||||
56, 16, self.recvSprite:getDimensions())
|
56, 16, self.recvSprite:getDimensions())
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
if self.monVisible and self.sub ~= "take_care" and self.sub ~= "delay_end" then
|
||||||
self:drawMonInfo(self.received, self.enemyName, self.enemyOtId, 10)
|
self:drawMonInfo(self.received, self.enemyName, self.enemyOtId, 10)
|
||||||
end
|
end
|
||||||
-- went_to / for_sends / farewell / delay: cleared window; TextBox draws
|
if self.activeBallBlock then
|
||||||
|
self:drawFrameBlock(self.activeBallBlock, self.activeBallX, self.activeBallY)
|
||||||
|
end
|
||||||
|
if self.activePoofBlock then
|
||||||
|
self:drawFrameBlock(self.activePoofBlock, self.activePoofX, self.activePoofY)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
self:drawDialog()
|
||||||
end
|
end
|
||||||
|
|
||||||
return TradeAnim
|
return TradeAnim
|
||||||
|
|||||||
+150
-31
@@ -22,12 +22,14 @@
|
|||||||
-- PAL_BG_YELLOW, out of a TOWN palette set at the current time of day
|
-- PAL_BG_YELLOW, out of a TOWN palette set at the current time of day
|
||||||
-- (the routine pushes wEnvironment, forces TOWN, and pops it back).
|
-- (the routine pushes wEnvironment, forces TOWN, and pops it back).
|
||||||
--
|
--
|
||||||
-- Two hardware details are not reproduced. The four player OBJs all carry
|
-- Two hardware details from the cart are faithfully reproduced:
|
||||||
-- OAM_PRIO, so on the cart they sit behind background colours 1-3 and only
|
-- 1. Viewport wrapping: the 256x144 background scrolls in 3 LY override bands,
|
||||||
-- show through the window's colour 0; here they are drawn straight over the
|
-- wrapped strictly within the 160x144 GB screen so pixels do not bleed into
|
||||||
-- background, which looks the same everywhere the window is transparent and
|
-- the widescreen surround.
|
||||||
-- differs only if the sprite drifts over solid train tiles. And the ride
|
-- 2. OAM Priority (OAM_PRIO): the four player OBJs sit behind background
|
||||||
-- cannot be skipped, exactly as on the cart: the loop reads no input at all.
|
-- colours 1-3 and only show through the window's colour 0. This is drawn
|
||||||
|
-- via a base background pass, the player sprite, and an overlay pass where
|
||||||
|
-- shade 0 is transparent and solid train tiles mask the sprite.
|
||||||
|
|
||||||
local Assets = require("src.render.Assets")
|
local Assets = require("src.render.Assets")
|
||||||
local Chrome = require("src.ui.gen2.Chrome")
|
local Chrome = require("src.ui.gen2.Chrome")
|
||||||
@@ -46,6 +48,30 @@ local BG_W = 256 -- TILEMAP_WIDTH * 8
|
|||||||
local TILES_PER_ROW = 16 -- every 2bpp sheet the importer writes
|
local TILES_PER_ROW = 16 -- every 2bpp sheet the importer writes
|
||||||
local BLACK = { 0, 0, 0 }
|
local BLACK = { 0, 0, 0 }
|
||||||
|
|
||||||
|
-- Overlay shader for OAM_PRIO: shade 0 is transparent; shades 1-3 are solid
|
||||||
|
-- palette colors, masking any sprites beneath solid background tiles.
|
||||||
|
local OVERLAY_SHADER_SOURCE = [[
|
||||||
|
extern vec3 pal0;
|
||||||
|
extern vec3 pal1;
|
||||||
|
extern vec3 pal2;
|
||||||
|
extern vec3 pal3;
|
||||||
|
|
||||||
|
vec4 effect(vec4 tint, Image tex, vec2 uv, vec2 screen) {
|
||||||
|
vec4 px = Texel(tex, uv);
|
||||||
|
float shade = floor((1.0 - px.r) * 3.0 + 0.5);
|
||||||
|
vec3 rgb = pal0;
|
||||||
|
if (shade > 2.5) {
|
||||||
|
rgb = pal3;
|
||||||
|
} else if (shade > 1.5) {
|
||||||
|
rgb = pal2;
|
||||||
|
} else if (shade > 0.5) {
|
||||||
|
rgb = pal1;
|
||||||
|
}
|
||||||
|
float alpha = shade < 0.5 ? 0.0 : px.a;
|
||||||
|
return vec4(rgb, alpha) * tint;
|
||||||
|
}
|
||||||
|
]]
|
||||||
|
|
||||||
function MagnetTrainRide:wantsFillScale() return true end
|
function MagnetTrainRide:wantsFillScale() return true end
|
||||||
|
|
||||||
-- opts: toGoldenrod (the wScriptVar the officer's script set), onDone()
|
-- opts: toGoldenrod (the wScriptVar the officer's script set), onDone()
|
||||||
@@ -97,7 +123,7 @@ function MagnetTrainRide:palette(slot)
|
|||||||
end
|
end
|
||||||
|
|
||||||
--------------------------------------------------------------------------
|
--------------------------------------------------------------------------
|
||||||
-- Sheets
|
-- Sheets & Shaders
|
||||||
--------------------------------------------------------------------------
|
--------------------------------------------------------------------------
|
||||||
|
|
||||||
local function sheetFor(path)
|
local function sheetFor(path)
|
||||||
@@ -159,22 +185,66 @@ function MagnetTrainRide:playerQuad(vtile)
|
|||||||
return quad
|
return quad
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function MagnetTrainRide:overlayShader()
|
||||||
|
if self.ovShader ~= nil then return self.ovShader or nil end
|
||||||
|
if not (love and love.graphics and love.graphics.newShader) then
|
||||||
|
self.ovShader = false
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
local ok, result = pcall(love.graphics.newShader, OVERLAY_SHADER_SOURCE)
|
||||||
|
self.ovShader = ok and result or false
|
||||||
|
return self.ovShader or nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function palChannel(colors, index)
|
||||||
|
local c = colors and colors[index]
|
||||||
|
if not c then return 0, 0, 0 end
|
||||||
|
return (c[1] or 0) / 255, (c[2] or 0) / 255, (c[3] or 0) / 255
|
||||||
|
end
|
||||||
|
|
||||||
|
function MagnetTrainRide:useOverlayShader(colors)
|
||||||
|
local sh = self:overlayShader()
|
||||||
|
if not sh then return false end
|
||||||
|
local resolved = GbcPalette.resolve(colors)
|
||||||
|
for i = 0, 3 do
|
||||||
|
local u = "pal" .. i
|
||||||
|
if sh:hasUniform(u) then
|
||||||
|
local r, g, b = palChannel(resolved, i + 1)
|
||||||
|
sh:send(u, { r, g, b })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
love.graphics.setShader(sh)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------
|
--------------------------------------------------------------------------
|
||||||
-- The baked background
|
-- The baked backgrounds (Base + OAM_PRIO Overlay)
|
||||||
--------------------------------------------------------------------------
|
--------------------------------------------------------------------------
|
||||||
|
|
||||||
-- DrawMagnetTrain plus SetMagnetTrainPals, rendered once into a 256x144
|
-- DrawMagnetTrain plus SetMagnetTrainPals, rendered once into 256x144 canvases.
|
||||||
-- canvas. Tiles are drawn palette group by palette group so the three
|
-- Base canvas contains the full opaque background; Overlay canvas contains
|
||||||
-- palettes cost three shader switches rather than one per tile.
|
-- solid shades 1-3 with shade 0 transparent for OAM_PRIO window masking.
|
||||||
function MagnetTrainRide:background()
|
function MagnetTrainRide:backgrounds()
|
||||||
if self.bgCanvas ~= nil then return self.bgCanvas or nil end
|
if self.bgCanvas ~= nil then
|
||||||
|
return self.bgCanvas or nil, self.bgOverlayCanvas or nil
|
||||||
|
end
|
||||||
self.bgCanvas = false
|
self.bgCanvas = false
|
||||||
|
self.bgOverlayCanvas = false
|
||||||
local rows = self.ride:tilemap()
|
local rows = self.ride:tilemap()
|
||||||
local sheet = self:tileSheet()
|
local sheet = self:tileSheet()
|
||||||
if not (rows and sheet) then return nil end
|
if not (rows and sheet) then return nil, nil end
|
||||||
local ok, canvas = pcall(love.graphics.newCanvas, BG_W, SCREEN_H)
|
|
||||||
if not ok then return nil end
|
local ok1, baseCanvas = pcall(love.graphics.newCanvas, BG_W, SCREEN_H)
|
||||||
canvas:setFilter("nearest", "nearest")
|
if not ok1 then return nil, nil end
|
||||||
|
baseCanvas:setFilter("nearest", "nearest")
|
||||||
|
|
||||||
|
local ok2, overlayCanvas = pcall(love.graphics.newCanvas, BG_W, SCREEN_H)
|
||||||
|
if ok2 and overlayCanvas then
|
||||||
|
overlayCanvas:setFilter("nearest", "nearest")
|
||||||
|
else
|
||||||
|
overlayCanvas = nil
|
||||||
|
end
|
||||||
|
|
||||||
local groups = {}
|
local groups = {}
|
||||||
for row = 1, #rows do
|
for row = 1, #rows do
|
||||||
@@ -188,11 +258,12 @@ function MagnetTrainRide:background()
|
|||||||
|
|
||||||
local G = love.graphics
|
local G = love.graphics
|
||||||
local previous = G.getCanvas()
|
local previous = G.getCanvas()
|
||||||
-- A canvas does not reset the transform: without this the map lands under
|
G.push("all")
|
||||||
-- the renderer's letterbox scale and off the edge.
|
|
||||||
G.push()
|
|
||||||
G.origin()
|
G.origin()
|
||||||
G.setCanvas(canvas)
|
G.setScissor()
|
||||||
|
|
||||||
|
-- 1. Base background canvas (shades 0-3)
|
||||||
|
G.setCanvas(baseCanvas)
|
||||||
G.clear(0, 0, 0, 1)
|
G.clear(0, 0, 0, 1)
|
||||||
G.setColor(1, 1, 1, 1)
|
G.setColor(1, 1, 1, 1)
|
||||||
local shader = GbcPalette.available()
|
local shader = GbcPalette.available()
|
||||||
@@ -205,10 +276,35 @@ function MagnetTrainRide:background()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
if shader then GbcPalette.clear() end
|
if shader then GbcPalette.clear() end
|
||||||
|
|
||||||
|
-- 2. Overlay canvas for OAM_PRIO (shades 1-3 solid, shade 0 transparent)
|
||||||
|
if overlayCanvas then
|
||||||
|
G.setCanvas(overlayCanvas)
|
||||||
|
G.clear(0, 0, 0, 0)
|
||||||
|
G.setColor(1, 1, 1, 1)
|
||||||
|
local ovShader = self:overlayShader()
|
||||||
|
for slot, list in pairs(groups) do
|
||||||
|
local colors = self:palette(slot)
|
||||||
|
if ovShader and colors then self:useOverlayShader(colors) end
|
||||||
|
for _, cell in ipairs(list) do
|
||||||
|
local quad = sheet.quads[cell[1]]
|
||||||
|
if quad then G.draw(sheet.image, quad, cell[2], cell[3]) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if ovShader then G.setShader() end
|
||||||
|
end
|
||||||
|
|
||||||
G.setCanvas(previous)
|
G.setCanvas(previous)
|
||||||
G.pop()
|
G.pop()
|
||||||
self.bgCanvas = canvas
|
|
||||||
return canvas
|
self.bgCanvas = baseCanvas
|
||||||
|
self.bgOverlayCanvas = overlayCanvas
|
||||||
|
return baseCanvas, overlayCanvas
|
||||||
|
end
|
||||||
|
|
||||||
|
function MagnetTrainRide:background()
|
||||||
|
local base = self:backgrounds()
|
||||||
|
return base
|
||||||
end
|
end
|
||||||
|
|
||||||
--------------------------------------------------------------------------
|
--------------------------------------------------------------------------
|
||||||
@@ -240,19 +336,24 @@ function MagnetTrainRide:update(_dt)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function MagnetTrainRide:drawBackground()
|
-- Draws the 3 LY override bands strictly wrapped inside the 160px GB screen.
|
||||||
local canvas = self:background()
|
function MagnetTrainRide:drawBands(canvas)
|
||||||
if not canvas then return end
|
if not canvas then return end
|
||||||
local G = love.graphics
|
local G = love.graphics
|
||||||
G.setColor(1, 1, 1, 1)
|
G.setColor(1, 1, 1, 1)
|
||||||
self.bandQuad = self.bandQuad
|
self.bandQuad = self.bandQuad
|
||||||
or love.graphics.newQuad(0, 0, BG_W, 1, BG_W, SCREEN_H)
|
or love.graphics.newQuad(0, 0, SCREEN_W, 1, BG_W, SCREEN_H)
|
||||||
for _, band in ipairs(self.ride:bands()) do
|
for _, band in ipairs(self.ride:bands()) do
|
||||||
local top, bottom, scx = band[1], band[2], band[3] % BG_W
|
local top, bottom, scx = band[1], band[2], band[3] % BG_W
|
||||||
local height = bottom - top + 1
|
local height = bottom - top + 1
|
||||||
self.bandQuad:setViewport(0, top, BG_W, height, BG_W, SCREEN_H)
|
local w1 = math.min(SCREEN_W, BG_W - scx)
|
||||||
G.draw(canvas, self.bandQuad, -scx, top)
|
self.bandQuad:setViewport(scx, top, w1, height, BG_W, SCREEN_H)
|
||||||
G.draw(canvas, self.bandQuad, -scx + BG_W, top)
|
G.draw(canvas, self.bandQuad, 0, top)
|
||||||
|
if w1 < SCREEN_W then
|
||||||
|
local w2 = SCREEN_W - w1
|
||||||
|
self.bandQuad:setViewport(0, top, w2, height, BG_W, SCREEN_H)
|
||||||
|
G.draw(canvas, self.bandQuad, w1, top)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -277,6 +378,7 @@ function MagnetTrainRide:drawPlayer()
|
|||||||
if colors then GbcPalette.use(colors) end
|
if colors then GbcPalette.use(colors) end
|
||||||
G.setColor(1, 1, 1, 1)
|
G.setColor(1, 1, 1, 1)
|
||||||
for _, entry in ipairs(oam) do
|
for _, entry in ipairs(oam) do
|
||||||
|
if entry.x >= -8 and entry.x < SCREEN_W and entry.y >= -16 and entry.y < SCREEN_H then
|
||||||
local quad = self:playerQuad(entry.tile)
|
local quad = self:playerQuad(entry.tile)
|
||||||
if quad then
|
if quad then
|
||||||
G.draw(sheet.image, quad,
|
G.draw(sheet.image, quad,
|
||||||
@@ -284,9 +386,11 @@ function MagnetTrainRide:drawPlayer()
|
|||||||
entry.xflip and -1 or 1, 1)
|
entry.xflip and -1 or 1, 1)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
if colors then GbcPalette.clear() end
|
if colors then GbcPalette.clear() end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
-- The backdrop is BG colour 0 of the gray palette the train body uses; on the
|
-- The backdrop is BG colour 0 of the gray palette the train body uses; on the
|
||||||
-- cart it is what shows wherever nothing was drawn.
|
-- cart it is what shows wherever nothing was drawn.
|
||||||
function MagnetTrainRide:backdrop()
|
function MagnetTrainRide:backdrop()
|
||||||
@@ -299,13 +403,24 @@ function MagnetTrainRide:drawPanel()
|
|||||||
G.setColor(backdrop[1] / 255, backdrop[2] / 255, backdrop[3] / 255, 1)
|
G.setColor(backdrop[1] / 255, backdrop[2] / 255, backdrop[3] / 255, 1)
|
||||||
G.rectangle("fill", 0, 0, SCREEN_W, SCREEN_H)
|
G.rectangle("fill", 0, 0, SCREEN_W, SCREEN_H)
|
||||||
G.setColor(1, 1, 1, 1)
|
G.setColor(1, 1, 1, 1)
|
||||||
self:drawBackground()
|
|
||||||
|
local base, overlay = self:backgrounds()
|
||||||
|
-- 1. Base background (bushes, train body, window aperture)
|
||||||
|
self:drawBands(base)
|
||||||
|
-- 2. Player sprite (OAM_PRIO: sits behind train body, visible through window)
|
||||||
self:drawPlayer()
|
self:drawPlayer()
|
||||||
|
-- 3. Train body / frame overlay (colors 1-3 mask sprite; color 0 is transparent)
|
||||||
|
self:drawBands(overlay)
|
||||||
|
|
||||||
G.setColor(1, 1, 1, 1)
|
G.setColor(1, 1, 1, 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
function MagnetTrainRide:draw()
|
function MagnetTrainRide:draw()
|
||||||
|
local G = love.graphics
|
||||||
|
G.push("all")
|
||||||
|
G.setScissor(0, 0, SCREEN_W, SCREEN_H)
|
||||||
self:drawPanel()
|
self:drawPanel()
|
||||||
|
G.pop()
|
||||||
end
|
end
|
||||||
|
|
||||||
-- MagnetTrain_LoadGFX_PlayMusic opens on ClearBGPalettes / ClearSprites
|
-- MagnetTrain_LoadGFX_PlayMusic opens on ClearBGPalettes / ClearSprites
|
||||||
@@ -323,7 +438,8 @@ function MagnetTrainRide:drawWidescreen(winW, winH)
|
|||||||
G.setColor(1, 1, 1, 1)
|
G.setColor(1, 1, 1, 1)
|
||||||
local scale = Chrome.fitScale(winW, winH)
|
local scale = Chrome.fitScale(winW, winH)
|
||||||
local ox, oy = Chrome.fitOrigin(winW, winH, scale)
|
local ox, oy = Chrome.fitOrigin(winW, winH, scale)
|
||||||
G.push()
|
G.push("all")
|
||||||
|
G.setScissor(ox, oy, SCREEN_W * scale, SCREEN_H * scale)
|
||||||
G.translate(ox, oy)
|
G.translate(ox, oy)
|
||||||
G.scale(scale, scale)
|
G.scale(scale, scale)
|
||||||
self:drawPanel()
|
self:drawPanel()
|
||||||
@@ -331,3 +447,6 @@ function MagnetTrainRide:drawWidescreen(winW, winH)
|
|||||||
end
|
end
|
||||||
|
|
||||||
return MagnetTrainRide
|
return MagnetTrainRide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -77,4 +77,26 @@ do
|
|||||||
check(Runtime.modRequire == nil, "Runtime.reset clears modRequire")
|
check(Runtime.modRequire == nil, "Runtime.reset clears modRequire")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- 4. returnToLauncher / closeEditor also clear Assets.loader (orphaned Loader)
|
||||||
|
do
|
||||||
|
local Assets = require("src.render.Assets")
|
||||||
|
Assets.installLoader({
|
||||||
|
overrideOrder = function() return { { id = "x", path = "mods/x" } } end,
|
||||||
|
derivedPath = function() return nil end,
|
||||||
|
})
|
||||||
|
check(Assets.loader ~= nil, "Assets.loader installed for the session")
|
||||||
|
Assets.installLoader(nil)
|
||||||
|
check(Assets.loader == nil, "session teardown clears Assets.loader")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 5. Gen1 Game:reset exists so main.lua need not guess field names
|
||||||
|
do
|
||||||
|
local Game = require("src.core.Game")
|
||||||
|
check(type(Game.reset) == "function", "Game:reset is defined for in-process return")
|
||||||
|
Game.mods = { stale = true }
|
||||||
|
Game:reset()
|
||||||
|
check(Game.mods == nil, "Game:reset clears session fields")
|
||||||
|
check(type(Game.load) == "function", "Game:reset keeps methods")
|
||||||
|
end
|
||||||
|
|
||||||
T.finish("android_exit_to_launcher_test")
|
T.finish("android_exit_to_launcher_test")
|
||||||
|
|||||||
@@ -57,6 +57,36 @@ eq(love.filesystem.read("assets/generated/fonts/font.png"), "font-bytes",
|
|||||||
eq(love.filesystem.read("data/generated/constants.lua"), "return {}",
|
eq(love.filesystem.read("data/generated/constants.lua"), "return {}",
|
||||||
"post-mount probe reads red data at the un-prefixed path")
|
"post-mount probe reads red data at the un-prefixed path")
|
||||||
|
|
||||||
|
-- unmountVersion peels generated overlays (LIFO) then the version folder, so
|
||||||
|
-- a later Play/Edit on another game cannot resolve this version's files.
|
||||||
|
do
|
||||||
|
local mounts = love.filesystem._mounts
|
||||||
|
local archives = {}
|
||||||
|
for _, m in ipairs(mounts) do archives[#archives + 1] = m.archive end
|
||||||
|
local function indexOf(name)
|
||||||
|
for i, a in ipairs(archives) do if a == name then return i end end
|
||||||
|
end
|
||||||
|
local dataIdx = indexOf("red/data/generated")
|
||||||
|
local assetsIdx = indexOf("red/assets/generated")
|
||||||
|
check(dataIdx and assetsIdx, "mountVersion recorded generated-tree overlays")
|
||||||
|
check(assetsIdx > dataIdx,
|
||||||
|
"assets/generated was mounted after data/generated (LIFO unmount peels it first)")
|
||||||
|
end
|
||||||
|
check(CacheFs.unmountVersion("red") == true, "unmountVersion(red) returns true")
|
||||||
|
check(love.filesystem.read("assets/generated/fonts/font.png") == nil,
|
||||||
|
"post-unmount probe no longer sees red assets at the un-prefixed path")
|
||||||
|
check(love.filesystem.read("data/generated/constants.lua") == nil,
|
||||||
|
"post-unmount probe no longer sees red data at the un-prefixed path")
|
||||||
|
do
|
||||||
|
local left = {}
|
||||||
|
for _, m in ipairs(love.filesystem._mounts) do
|
||||||
|
if tostring(m.archive):find("red", 1, true) then
|
||||||
|
left[#left + 1] = m.archive
|
||||||
|
end
|
||||||
|
end
|
||||||
|
eq(#left, 0, "no red/ mounts remain after unmountVersion")
|
||||||
|
end
|
||||||
|
|
||||||
-- no legacy cache at all: migration is a no-op, not an error
|
-- no legacy cache at all: migration is a no-op, not an error
|
||||||
love.filesystem.remove("red/rom-cache.complete")
|
love.filesystem.remove("red/rom-cache.complete")
|
||||||
love.filesystem.remove("red/data/generated/maps.lua")
|
love.filesystem.remove("red/data/generated/maps.lua")
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
-- In-process launcher session teardown: Game:reset, Renderer canvas release,
|
||||||
|
-- Runtime/Assets/LegacyCompat cleanup, and editor package.loaded discovery flush.
|
||||||
|
-- luajit tests/engine/launcher_session_teardown_test.lua
|
||||||
|
|
||||||
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||||
|
love = love or require("tests.love_stub")
|
||||||
|
|
||||||
|
local T = require("tests.harness")
|
||||||
|
local check, eq = T.check, T.eq
|
||||||
|
|
||||||
|
local Runtime = require("src.mods.Runtime")
|
||||||
|
local Assets = require("src.render.Assets")
|
||||||
|
local LegacyCompat = require("src.mods.LegacyCompat")
|
||||||
|
local Game = require("src.core.Game")
|
||||||
|
local Renderer = require("src.render.Renderer")
|
||||||
|
local StateStack = require("src.core.StateStack")
|
||||||
|
|
||||||
|
-- ---- Game:reset drops instance state, keeps methods ----------------------
|
||||||
|
do
|
||||||
|
StateStack:init()
|
||||||
|
StateStack:push({ name = "stale" })
|
||||||
|
Game.mods = { id = "orphan" }
|
||||||
|
Game.save = { money = 1 }
|
||||||
|
Game.network = { live = true } -- future field: must not need a whitelist
|
||||||
|
Game.stack = StateStack
|
||||||
|
Game.renderer = Renderer
|
||||||
|
Renderer.canvas = love.graphics.newCanvas(8, 8)
|
||||||
|
|
||||||
|
Game:reset()
|
||||||
|
|
||||||
|
check(type(Game.load) == "function", "Game:reset keeps methods")
|
||||||
|
check(type(Game.reset) == "function", "Game:reset keeps itself")
|
||||||
|
check(Game.mods == nil, "Game:reset clears mods")
|
||||||
|
check(Game.save == nil, "Game:reset clears save")
|
||||||
|
check(Game.network == nil, "Game:reset clears arbitrary future fields")
|
||||||
|
check(Game.stack == nil, "Game:reset clears stack reference")
|
||||||
|
check(Game.renderer == nil, "Game:reset clears renderer reference")
|
||||||
|
check(StateStack:top() == nil, "Game:reset cleared the shared StateStack")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---- Renderer:init releases prior canvases before realloc ----------------
|
||||||
|
do
|
||||||
|
local first = love.graphics.newCanvas(16, 16)
|
||||||
|
Renderer.canvas = first
|
||||||
|
Renderer.battleHUDCanvas = love.graphics.newCanvas(16, 16)
|
||||||
|
Renderer.worldCanvas = love.graphics.newCanvas(16, 16)
|
||||||
|
Renderer.uprightCanvas = love.graphics.newCanvas(16, 16)
|
||||||
|
Renderer:init()
|
||||||
|
check(first.released == true,
|
||||||
|
"Renderer:init Object:release()s the previous primary canvas")
|
||||||
|
check(Renderer.canvas ~= nil and Renderer.canvas ~= first,
|
||||||
|
"Renderer:init allocates a fresh primary canvas")
|
||||||
|
check(Renderer.canvas.released ~= true,
|
||||||
|
"the new primary canvas is not released")
|
||||||
|
-- second init also releases the one just created
|
||||||
|
local second = Renderer.canvas
|
||||||
|
Renderer:init()
|
||||||
|
check(second.released == true,
|
||||||
|
"a second Renderer:init releases the canvas from the prior init")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---- Shared singleton teardown contract (closeEditor / returnToLauncher)
|
||||||
|
do
|
||||||
|
Runtime.install({ emit = function() end }, { call = function() end }, { "e" })
|
||||||
|
Assets.installLoader({
|
||||||
|
overrideOrder = function() return {} end,
|
||||||
|
derivedPath = function() return nil end,
|
||||||
|
})
|
||||||
|
LegacyCompat.reports = { some_mod = { order = {} } }
|
||||||
|
|
||||||
|
-- Mirrors main.lua teardownMountedSession without mounting CacheFs.
|
||||||
|
require("src.core.Data"):unloadGenerated()
|
||||||
|
Runtime.reset()
|
||||||
|
Assets.installLoader(nil)
|
||||||
|
LegacyCompat.reset()
|
||||||
|
|
||||||
|
check(Runtime.errors == nil, "teardown clears Runtime.errors")
|
||||||
|
check(Assets.loader == nil, "teardown clears Assets.loader")
|
||||||
|
eq(next(LegacyCompat.reports), nil, "teardown clears LegacyCompat.reports")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---- Editor package.loaded discovery flush (no panel whitelist) ---------
|
||||||
|
do
|
||||||
|
love.filesystem.write("tools/save-editor/App.lua", "return {}")
|
||||||
|
love.filesystem.write("tools/save-editor/panels/NewPanel.lua", "return {}")
|
||||||
|
package.loaded["App"] = { stale = true }
|
||||||
|
package.loaded["NewPanel"] = { stale = true }
|
||||||
|
package.loaded["src.core.Data"] = package.loaded["src.core.Data"] -- keep
|
||||||
|
|
||||||
|
local function isEditorFlat(name)
|
||||||
|
if name:find("[./]") then return false end
|
||||||
|
return love.filesystem.getInfo("tools/save-editor/" .. name .. ".lua") ~= nil
|
||||||
|
or love.filesystem.getInfo("tools/save-editor/panels/" .. name .. ".lua") ~= nil
|
||||||
|
end
|
||||||
|
for k in pairs(package.loaded) do
|
||||||
|
if type(k) == "string"
|
||||||
|
and (k:find("save%-editor", 1, false) or isEditorFlat(k)) then
|
||||||
|
package.loaded[k] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
check(package.loaded["App"] == nil, "discovery flush drops flat App")
|
||||||
|
check(package.loaded["NewPanel"] == nil,
|
||||||
|
"discovery flush drops a new panel without a hardcoded list")
|
||||||
|
check(package.loaded["src.core.Data"] ~= nil,
|
||||||
|
"discovery flush leaves engine modules alone")
|
||||||
|
love.filesystem.remove("tools/save-editor/App.lua")
|
||||||
|
love.filesystem.remove("tools/save-editor/panels/NewPanel.lua")
|
||||||
|
end
|
||||||
|
|
||||||
|
T.finish("launcher_session_teardown_test")
|
||||||
@@ -42,7 +42,7 @@ check(type(RomExtractorGen2.extractDiploma) == "function",
|
|||||||
"RomExtractorGen2:extractDiploma exists")
|
"RomExtractorGen2:extractDiploma exists")
|
||||||
check(extractorSource:find("results.diploma = self:extractDiploma()", 1, true)
|
check(extractorSource:find("results.diploma = self:extractDiploma()", 1, true)
|
||||||
~= nil, "and RomExtractorGen2:run calls it")
|
~= nil, "and RomExtractorGen2:run calls it")
|
||||||
check(extractorSource:find("local STAGE_COUNT = 26", 1, true) ~= nil,
|
check(extractorSource:find("local STAGE_COUNT = 27", 1, true) ~= nil,
|
||||||
"STAGE_COUNT counts the new stage, so the progress bar still ends at 1")
|
"STAGE_COUNT counts the new stage, so the progress bar still ends at 1")
|
||||||
|
|
||||||
-- The three symbols the stage reads have to be in the curated manifest set or
|
-- The three symbols the stage reads have to be in the curated manifest set or
|
||||||
|
|||||||
@@ -266,6 +266,18 @@ do
|
|||||||
"and with no hook at all the special is a no-op, not a clobber")
|
"and with no hook at all the special is a no-op, not a clobber")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- ---- MagnetTrainRide UI screen --------------------------------------------
|
||||||
|
do
|
||||||
|
local MagnetTrainRide = require("src.ui.gen2.MagnetTrainRide")
|
||||||
|
check(MagnetTrainRide.isOpaque, "MagnetTrainRide is opaque")
|
||||||
|
local ride = MagnetTrainRide.new(nil, { toGoldenrod = false })
|
||||||
|
check(ride:wantsFillScale(), "wantsFillScale returns true")
|
||||||
|
check(ride:drawsWidescreen(), "drawsWidescreen returns true")
|
||||||
|
check(type(ride.drawBands) == "function", "drawBands exists")
|
||||||
|
check(type(ride.backgrounds) == "function", "backgrounds exists for OAM_PRIO")
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
-- ---- the arrival, from `warpcheck` to the officer's line -------------------
|
-- ---- the arrival, from `warpcheck` to the officer's line -------------------
|
||||||
--
|
--
|
||||||
-- The ride is only half the feature: the two station scripts end
|
-- The ride is only half the feature: the two station scripts end
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
-- Gen 2's engine text, the counterpart to Gen 1's data/generated/text.lua.
|
||||||
|
--
|
||||||
|
-- Gold and Silver had no label-keyed string table at all: the manifests
|
||||||
|
-- carried no `text` section, RomExtractorGen2 had no extractText, and
|
||||||
|
-- game.data.text was never assigned, so every call through
|
||||||
|
-- src/core/RomText.lua fell back to the literal written beside it. These
|
||||||
|
-- cover the three halves of closing that: the manifest names the labels and
|
||||||
|
-- resolves every one, the decoder emits the runtime name slots rather than
|
||||||
|
-- dropping them, and RomText fills those slots.
|
||||||
|
--
|
||||||
|
-- GOLD_CACHE="..." luajit tests/gen2_rom_text_test.lua
|
||||||
|
--
|
||||||
|
-- ROM-free apart from the last section, which reads an imported cache's
|
||||||
|
-- rom_text.lua and skips cleanly when there is none.
|
||||||
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||||
|
|
||||||
|
local S = require("tests.harness").suite("gen2 rom text")
|
||||||
|
local check, eq = S.check, S.eq
|
||||||
|
|
||||||
|
love = require("tests.love_stub")
|
||||||
|
|
||||||
|
local Json = require("src.link.Json")
|
||||||
|
local romText = require("src.core.RomText")
|
||||||
|
|
||||||
|
local function manifest(path)
|
||||||
|
local file = assert(io.open(path, "r"))
|
||||||
|
local data = assert(Json.decode(file:read("*a")))
|
||||||
|
file:close()
|
||||||
|
return data
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---- the label list, and that every label resolves ------------------------
|
||||||
|
-- A label the list names but the symbol table cannot place would fail the
|
||||||
|
-- import at the Dialogue stage rather than at generation time, so the pairing
|
||||||
|
-- is asserted here instead.
|
||||||
|
for _, edition in ipairs({ "gold", "silver" }) do
|
||||||
|
local data = manifest("tools/rom_manifest_" .. edition .. ".json")
|
||||||
|
local labels = (data.text or {}).labels or {}
|
||||||
|
check((data.text or {}).labels ~= nil,
|
||||||
|
edition .. " carries a text section")
|
||||||
|
check(#labels > 800,
|
||||||
|
("%s names %d text labels"):format(edition, #labels))
|
||||||
|
|
||||||
|
local unresolved = {}
|
||||||
|
for _, label in ipairs(labels) do
|
||||||
|
if not data.symbols[label] then unresolved[#unresolved + 1] = label end
|
||||||
|
end
|
||||||
|
eq(#unresolved, 0,
|
||||||
|
("every %s text label resolves to a symbol (%s)")
|
||||||
|
:format(edition, table.concat(unresolved, ", "):sub(1, 60)))
|
||||||
|
|
||||||
|
local named = {}
|
||||||
|
for _, label in ipairs(labels) do named[label] = true end
|
||||||
|
|
||||||
|
-- data/text/ also holds keyboard layouts and kana tables. Decoded as text
|
||||||
|
-- they come out as keyboard rows, so make_gold_manifest.TEXT_SOURCES leaves
|
||||||
|
-- their files out. `BattleText::` is excluded for a different reason: it
|
||||||
|
-- is a bank anchor sharing an address with the first real label under it,
|
||||||
|
-- and its own comment in the disassembly says so.
|
||||||
|
for _, excluded in ipairs({ "NameInputLower", "MailEntry_Uppercase",
|
||||||
|
"Dakutens", "Gen1TrainerClassNames", "BattleText" }) do
|
||||||
|
check(not named[excluded],
|
||||||
|
edition .. " leaves " .. excluded .. " out of the text list")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Both editions describe the same strings; only the addresses move.
|
||||||
|
do
|
||||||
|
local gold = (manifest("tools/rom_manifest_gold.json").text or {}).labels or {}
|
||||||
|
local silver =
|
||||||
|
(manifest("tools/rom_manifest_silver.json").text or {}).labels or {}
|
||||||
|
eq(#gold, #silver, "Gold and Silver name the same number of labels")
|
||||||
|
local mismatch
|
||||||
|
for index, label in ipairs(gold) do
|
||||||
|
if silver[index] ~= label then mismatch = label; break end
|
||||||
|
end
|
||||||
|
eq(mismatch, nil, "and the same labels in the same order")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---- the slots RomText fills ----------------------------------------------
|
||||||
|
-- decodeGen2Text emits {USER}, {TARGET} and {ENEMY} for the three names
|
||||||
|
-- PlaceMoveUsersName / PlaceMoveTargetsName / PlaceEnemysName write at
|
||||||
|
-- runtime (home/text.asm:302, :307, :327). Dropped, the line printed with a
|
||||||
|
-- hole where the name belongs.
|
||||||
|
do
|
||||||
|
local data = { text = {
|
||||||
|
SubTookDamageText = "The SUBSTITUTE\ntook damage for\v{TARGET}!",
|
||||||
|
WantsToBattleText = "{ENEMY}\nwants to battle!",
|
||||||
|
ConfusedNoMoreText = "{USER}'s\nconfused no more!",
|
||||||
|
SuperEffectiveText = "It's super-\neffective!",
|
||||||
|
} }
|
||||||
|
|
||||||
|
eq(romText(data, "SubTookDamageText", "fallback", "GEODUDE"),
|
||||||
|
"The SUBSTITUTE\ntook damage for\vGEODUDE!",
|
||||||
|
"a {TARGET} slot takes the name the caller passes")
|
||||||
|
eq(romText(data, "WantsToBattleText", "fallback", "FALKNER"),
|
||||||
|
"FALKNER\nwants to battle!", "and so does {ENEMY}")
|
||||||
|
eq(romText(data, "ConfusedNoMoreText", "fallback", "CYNDAQUIL"),
|
||||||
|
"CYNDAQUIL's\nconfused no more!", "and {USER}")
|
||||||
|
eq(romText(data, "SuperEffectiveText", "It's super effective!"),
|
||||||
|
"It's super-\neffective!",
|
||||||
|
"a line with no slot comes back as the cart wrote it")
|
||||||
|
eq(romText(data, "NoSuchLabel", "the engine's own wording"),
|
||||||
|
"the engine's own wording",
|
||||||
|
"and a label the cache does not carry falls back")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---- against a real imported cache ----------------------------------------
|
||||||
|
do
|
||||||
|
local cache = os.getenv("GOLD_CACHE")
|
||||||
|
if not cache then
|
||||||
|
local home = os.getenv("HOME") or ""
|
||||||
|
cache = home .. "/Library/Application Support/LOVE/gold-dev/gold"
|
||||||
|
end
|
||||||
|
local path = cache .. "/data/generated/rom_text.lua"
|
||||||
|
local file = io.open(path, "r")
|
||||||
|
if not file then
|
||||||
|
print(" (skipped: no rom_text.lua at " .. path .. ")")
|
||||||
|
else
|
||||||
|
file:close()
|
||||||
|
local texts = assert(loadfile(path))()
|
||||||
|
check(next(texts) ~= nil, "the imported cache carries strings")
|
||||||
|
-- Wording taken from pokegold's data/text/battle.asm, with \n for `line`
|
||||||
|
-- and \v for `cont`, which is what RomExtractorGen2 decodes those to.
|
||||||
|
eq(texts.SuperEffectiveText, "It's super-\neffective!",
|
||||||
|
"SuperEffectiveText comes off the cart hyphenated and broken")
|
||||||
|
eq(texts.NotVeryEffectiveText, "It's not very\neffective…",
|
||||||
|
"NotVeryEffectiveText ends on the ellipsis glyph")
|
||||||
|
eq(texts.StartPerishText, "Both POKéMON will\nfaint in 3 turns!",
|
||||||
|
"StartPerishText names both sides")
|
||||||
|
eq(texts.ButItFailedText, "But it failed!", "and a one-row line is one row")
|
||||||
|
eq(texts.SubTookDamageText, "The SUBSTITUTE\ntook damage for\v{TARGET}!",
|
||||||
|
"SpikesText's neighbour keeps its cont row and its target slot")
|
||||||
|
eq(texts.PlayerHitTimesText, "Hit {NUM} times!",
|
||||||
|
"a text_decimal reads back as {NUM}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
S.finish()
|
||||||
@@ -45,7 +45,7 @@ check(type(RomExtractorGen2.extractTrade) == "function",
|
|||||||
"RomExtractorGen2:extractTrade exists")
|
"RomExtractorGen2:extractTrade exists")
|
||||||
check(extractorSource:find("results.trade = self:extractTrade()", 1, true)
|
check(extractorSource:find("results.trade = self:extractTrade()", 1, true)
|
||||||
~= nil, "and RomExtractorGen2:run calls it")
|
~= nil, "and RomExtractorGen2:run calls it")
|
||||||
check(extractorSource:find("local STAGE_COUNT = 26", 1, true) ~= nil,
|
check(extractorSource:find("local STAGE_COUNT = 27", 1, true) ~= nil,
|
||||||
"STAGE_COUNT counts the new stage, so the progress bar still ends at 1")
|
"STAGE_COUNT counts the new stage, so the progress bar still ends at 1")
|
||||||
|
|
||||||
-- pokegold.sym, bank $0a. These are also what the cache is checked against
|
-- pokegold.sym, bank $0a. These are also what the cache is checked against
|
||||||
|
|||||||
+3
-1
@@ -56,7 +56,9 @@ stub.graphics = {
|
|||||||
end,
|
end,
|
||||||
newQuad = function(x, y, w, h) return { x = x, y = y, w = w, h = h } end,
|
newQuad = function(x, y, w, h) return { x = x, y = y, w = w, h = h } end,
|
||||||
newCanvas = function(w, h)
|
newCanvas = function(w, h)
|
||||||
return setmetatable({ w = w, h = h, setFilter = noop }, Image)
|
local canvas = setmetatable({ w = w, h = h, setFilter = noop, released = false }, Image)
|
||||||
|
function canvas:release() self.released = true end
|
||||||
|
return canvas
|
||||||
end,
|
end,
|
||||||
newSpriteBatch = function(image, size)
|
newSpriteBatch = function(image, size)
|
||||||
local batch = { image = image, sprites = {} }
|
local batch = { image = image, sprites = {} }
|
||||||
|
|||||||
@@ -3526,6 +3526,7 @@ runSuites({
|
|||||||
"tests/gen2_decorations_test.lua",
|
"tests/gen2_decorations_test.lua",
|
||||||
"tests/gen2_pokerus_test.lua",
|
"tests/gen2_pokerus_test.lua",
|
||||||
"tests/gen2_common_text_test.lua",
|
"tests/gen2_common_text_test.lua",
|
||||||
|
"tests/gen2_rom_text_test.lua",
|
||||||
"tests/gen2_magnet_train_test.lua",
|
"tests/gen2_magnet_train_test.lua",
|
||||||
"tests/gen2_bank_of_mom_test.lua",
|
"tests/gen2_bank_of_mom_test.lua",
|
||||||
"tests/gen2_trainerhouse_test.lua",
|
"tests/gen2_trainerhouse_test.lua",
|
||||||
|
|||||||
@@ -143,6 +143,28 @@ local ok, err = pcall(function()
|
|||||||
check(#report.lostMons == 0 and probe.party[1].species == "EDITMON",
|
check(#report.lostMons == 0 and probe.party[1].species == "EDITMON",
|
||||||
"validate keeps the modded mon while the mod is enabled")
|
"validate keeps the modded mon while the mod is enabled")
|
||||||
|
|
||||||
|
-- Second editor session in the same process (launcher Close → Edit again):
|
||||||
|
-- host teardown must leave Runtime/Assets clean and Data unloaded so the
|
||||||
|
-- next App.load remounts mods instead of serving vanilla catalogs.
|
||||||
|
App.unload()
|
||||||
|
if Data._pristineKeys or Data.pokemon then Data:unloadGenerated() end
|
||||||
|
Runtime.reset()
|
||||||
|
Assets.installLoader(nil)
|
||||||
|
package.loaded["App"] = nil
|
||||||
|
package.loaded["Catalog"] = nil
|
||||||
|
App = require("App")
|
||||||
|
App.load(tmpPath)
|
||||||
|
S = App.getState()
|
||||||
|
loaded = S.mods:status().loaded
|
||||||
|
check(#loaded == 1 and loaded[1].id == "zz_editor_fixture",
|
||||||
|
"fixture mod loads again after host-style teardown")
|
||||||
|
hasSpecies = false
|
||||||
|
for _, id in ipairs(S.cat.species) do
|
||||||
|
if id == "EDITMON" then hasSpecies = true end
|
||||||
|
end
|
||||||
|
check(hasSpecies, "second App.load catalog still carries the mod's mon")
|
||||||
|
check(Data.pokemon.EDITMON ~= nil, "second merge lands EDITMON in Data again")
|
||||||
|
|
||||||
-- Gold-targeted mods: spa/spd records are usable, and a Gen 1-only
|
-- Gold-targeted mods: spa/spd records are usable, and a Gen 1-only
|
||||||
-- manifest stays out of Gold (no ROM cache / App.load("gold") required).
|
-- manifest stays out of Gold (no ROM cache / App.load("gold") required).
|
||||||
local ModTargets = require("src.mods.ModTargets")
|
local ModTargets = require("src.mods.ModTargets")
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||||
|
local S = require("tests.harness").suite("yellow title palette")
|
||||||
|
local check, eq = S.check, S.eq
|
||||||
|
|
||||||
|
local GameVersion = require("src.core.GameVersion")
|
||||||
|
local PaletteFX = require("src.render.PaletteFX")
|
||||||
|
local TitleState = require("src.ui.TitleState")
|
||||||
|
|
||||||
|
GameVersion.set("yellow")
|
||||||
|
local prevMode = PaletteFX.mode
|
||||||
|
|
||||||
|
-- Test 1: SGB mode
|
||||||
|
PaletteFX.setMode("gbc")
|
||||||
|
local mewmonSgb = PaletteFX.pal(nil, "MEWMON")
|
||||||
|
check(mewmonSgb ~= nil, "MEWMON palette exists in SGB mode on Yellow")
|
||||||
|
-- In Yellow SGB SuperPalettes: MEWMON color 1 is Yellow {255, 247, 181}, color 2 is Red {222, 132, 132}
|
||||||
|
eq(mewmonSgb[1][1], 255, "SGB: Color 0 is white R=255")
|
||||||
|
eq(mewmonSgb[2][1], 255, "SGB: Color 1 is yellow R=255")
|
||||||
|
eq(mewmonSgb[3][1], 222, "SGB: Color 2 is red R=222")
|
||||||
|
|
||||||
|
-- Test 2: ADVANCED mode (redpp)
|
||||||
|
PaletteFX.setMode("redpp")
|
||||||
|
local mewmonAdv = PaletteFX.pal(nil, "MEWMON")
|
||||||
|
check(mewmonAdv ~= nil, "MEWMON palette exists in ADVANCED mode on Yellow")
|
||||||
|
-- Must NOT be Red++'s Mew purple {115, 33, 165}! It must be Yellow's Pikachu palette!
|
||||||
|
eq(mewmonAdv[1][1], 255, "ADVANCED: Color 0 is white R=255 (Pikachu eye sclera)")
|
||||||
|
eq(mewmonAdv[2][1], 255, "ADVANCED: Color 1 is yellow R=255 (Pikachu body)")
|
||||||
|
check(mewmonAdv[3][3] < 150, "ADVANCED: Color 2 is not purple (B < 150, red cheeks)")
|
||||||
|
|
||||||
|
-- Test 3: OG YELLOW mode (ogred on Yellow)
|
||||||
|
PaletteFX.setMode("ogred")
|
||||||
|
local mewmonOg = PaletteFX.pal(nil, "MEWMON")
|
||||||
|
check(mewmonOg ~= nil, "MEWMON palette exists in OG YELLOW mode")
|
||||||
|
eq(mewmonOg[1][1], 255, "OG YELLOW: Color 0 is white")
|
||||||
|
eq(mewmonOg[2][1], 255, "OG YELLOW: Color 1 is yellow")
|
||||||
|
|
||||||
|
-- Test 4: TitleState sgbPalettes
|
||||||
|
local fakeGame = { data = { palettes = PaletteFX.yellowPack() } }
|
||||||
|
local ts = { yellowLayout = true }
|
||||||
|
local zones = TitleState.sgbPalettes(ts, fakeGame)
|
||||||
|
check(zones ~= nil and #zones >= 3, "TitleState produces zones for Yellow")
|
||||||
|
eq(zones[1].colors, PaletteFX.pal(fakeGame.data, "LOGO2"), "Zone 1 uses LOGO2")
|
||||||
|
eq(zones[2].colors, PaletteFX.pal(fakeGame.data, "MEWMON"), "Zone 2 uses MEWMON (Pikachu yellow/red)")
|
||||||
|
|
||||||
|
PaletteFX.setMode(prevMode)
|
||||||
|
GameVersion.set("red")
|
||||||
|
|
||||||
|
S.finish()
|
||||||
@@ -907,9 +907,62 @@ REQUIRED_SYMBOLS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def embedded_symbols(symbols, pokemon_labels, song_labels=()):
|
# The engine's own text, the counterpart to make_rom_manifest.text_metadata.
|
||||||
"""Resolve REQUIRED_SYMBOLS + pic labels + Music_* song headers."""
|
#
|
||||||
names = set(REQUIRED_SYMBOLS) | set(pokemon_labels) | set(song_labels)
|
# Only these five carry dialogue. data/text/'s other files are character
|
||||||
|
# tables rather than strings: dakutens.asm and name_input_chars.asm /
|
||||||
|
# mail_input_chars.asm are keyboard layouts, and unused_gen1_trainer_names.asm
|
||||||
|
# is a dead Gen 1 leftover. Decoding those as text yields keyboard rows and
|
||||||
|
# kana runs, so they are left out by name rather than filtered afterwards.
|
||||||
|
#
|
||||||
|
# None of the five carries an IF DEF(_GOLD) / IF DEF(_SILVER) arm, so the
|
||||||
|
# label set is one list for both editions and make_silver_manifest.py inherits
|
||||||
|
# it with the addresses re-resolved from pokesilver.sym.
|
||||||
|
TEXT_SOURCES = (
|
||||||
|
"battle.asm",
|
||||||
|
"common_1.asm",
|
||||||
|
"common_2.asm",
|
||||||
|
"common_3.asm",
|
||||||
|
"std_text.asm",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def text_labels(pokegold):
|
||||||
|
"""Every text label in TEXT_SOURCES, in sorted order.
|
||||||
|
|
||||||
|
Unlike Gen 1 there is no `dynamic` map beside this. pokered's decoder is
|
||||||
|
told which runtime token each label carries; RomExtractorGen2's reads the
|
||||||
|
cart's own TX_RAM / TX_DECIMAL command bytes and emits {STRBUF} / {NUM}
|
||||||
|
itself, so the label alone is enough.
|
||||||
|
"""
|
||||||
|
labels = set()
|
||||||
|
for name in TEXT_SOURCES:
|
||||||
|
path = os.path.join(pokegold, "data/text", name)
|
||||||
|
pending = None
|
||||||
|
for _, line in read_asm(path):
|
||||||
|
stripped = line.strip()
|
||||||
|
if not stripped:
|
||||||
|
continue
|
||||||
|
match = re.match(r"(\w+)::?\s*$", stripped)
|
||||||
|
if match:
|
||||||
|
# A label whose next line is another label owns no string of
|
||||||
|
# its own. `BattleText::` is the one in this set: its own
|
||||||
|
# comment says "used only for BANK(BattleText)", and it shares
|
||||||
|
# an address with the first real label under it, so taking it
|
||||||
|
# would decode that neighbour's string a second time.
|
||||||
|
pending = match.group(1)
|
||||||
|
continue
|
||||||
|
if pending:
|
||||||
|
labels.add(pending)
|
||||||
|
pending = None
|
||||||
|
return sorted(labels)
|
||||||
|
|
||||||
|
|
||||||
|
def embedded_symbols(symbols, pokemon_labels, song_labels=(),
|
||||||
|
text_label_names=()):
|
||||||
|
"""Resolve REQUIRED_SYMBOLS + pic labels + songs + text labels."""
|
||||||
|
names = (set(REQUIRED_SYMBOLS) | set(pokemon_labels)
|
||||||
|
| set(song_labels) | set(text_label_names))
|
||||||
for symbol_name in symbols.by_name:
|
for symbol_name in symbols.by_name:
|
||||||
# Pokedex entries are split across four banks and the game derives the
|
# Pokedex entries are split across four banks and the game derives the
|
||||||
# bank arithmetically from the species id (radio.asm's rlca/maskbits
|
# bank arithmetically from the species id (radio.asm's rlca/maskbits
|
||||||
@@ -1111,6 +1164,7 @@ def generate(pokegold, symbols_path):
|
|||||||
pokemon_labels.append(asset["backLabel"])
|
pokemon_labels.append(asset["backLabel"])
|
||||||
|
|
||||||
songs = music_order(pokegold)
|
songs = music_order(pokegold)
|
||||||
|
text_label_names = text_labels(pokegold)
|
||||||
sfx = sfx_order(pokegold)
|
sfx = sfx_order(pokegold)
|
||||||
|
|
||||||
# Index 0 is NO_ITEM, so the parsed list is already 1-based on item id.
|
# Index 0 is NO_ITEM, so the parsed list is already 1-based on item id.
|
||||||
@@ -1202,6 +1256,9 @@ def generate(pokegold, symbols_path):
|
|||||||
"battleAnimBgPaletteOrder": battle_anim_bg_pals,
|
"battleAnimBgPaletteOrder": battle_anim_bg_pals,
|
||||||
"battleAnimObPaletteOrder": battle_anim_ob_pals,
|
"battleAnimObPaletteOrder": battle_anim_ob_pals,
|
||||||
},
|
},
|
||||||
|
# Label -> decoded string is built at import time from these, the
|
||||||
|
# same way Gen 1 builds data/generated/text.lua from its own list.
|
||||||
|
"text": {"labels": text_label_names},
|
||||||
"charmap": charmap(pokegold),
|
"charmap": charmap(pokegold),
|
||||||
"fontCharmap": font_extract.parse_charmap(pokegold),
|
"fontCharmap": font_extract.parse_charmap(pokegold),
|
||||||
"pokemonAssets": assets,
|
"pokemonAssets": assets,
|
||||||
@@ -1210,7 +1267,8 @@ def generate(pokegold, symbols_path):
|
|||||||
"maps": {name: map_groups[name] for name in map_order},
|
"maps": {name: map_groups[name] for name in map_order},
|
||||||
"tilesets": {name: {} for name in tilesets},
|
"tilesets": {name: {} for name in tilesets},
|
||||||
}
|
}
|
||||||
data["symbols"] = embedded_symbols(symbols, pokemon_labels, songs)
|
data["symbols"] = embedded_symbols(
|
||||||
|
symbols, pokemon_labels, songs, text_label_names)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user