mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-23 05:58:26 +02:00
Merge pull request #1723 from 1Jamie/bullet-train-anim-launcher-lc-yellow-pallet-g1-link-flow
This commit is contained in:
@@ -82,6 +82,43 @@ end
|
||||
local editorHost, editorVersion, editorWindow
|
||||
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
|
||||
-- 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
|
||||
@@ -180,9 +217,9 @@ local function openEditor(version, slotId)
|
||||
if EditorApp.unload then pcall(EditorApp.unload) end
|
||||
EditorApp = nil
|
||||
if version then
|
||||
require("src.import.CacheFs").unmountVersion(version)
|
||||
require("src.core.Data"):unloadGenerated()
|
||||
teardownMountedSession(version)
|
||||
end
|
||||
flushEditorPackageLoaded()
|
||||
restoreWindow()
|
||||
Importer = editorHost
|
||||
editorHost = nil
|
||||
@@ -197,21 +234,17 @@ end
|
||||
-- Back to the launcher. Everything the editor mounted or cached has to come
|
||||
-- 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
|
||||
-- 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()
|
||||
local version = editorVersion
|
||||
editorMode = false
|
||||
if EditorApp and EditorApp.unload then EditorApp.unload() end
|
||||
EditorApp = nil
|
||||
if version then
|
||||
require("src.import.CacheFs").unmountVersion(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
|
||||
teardownMountedSession(version)
|
||||
end
|
||||
flushEditorPackageLoaded()
|
||||
editorVersion = nil
|
||||
restoreWindow()
|
||||
Importer = editorHost
|
||||
@@ -333,16 +366,11 @@ local function returnToLauncher()
|
||||
|
||||
local GameVersion = require("src.core.GameVersion")
|
||||
local currentVersion = GameVersion.get()
|
||||
if currentVersion then
|
||||
require("src.import.CacheFs").unmountVersion(currentVersion)
|
||||
end
|
||||
require("src.core.Data"):unloadGenerated()
|
||||
teardownMountedSession(currentVersion)
|
||||
|
||||
local Runtime = require("src.mods.Runtime")
|
||||
if Runtime.reset then
|
||||
Runtime.reset()
|
||||
if Game.reset then
|
||||
pcall(function() Game:reset() end)
|
||||
end
|
||||
|
||||
Game = nil
|
||||
autopilot = nil
|
||||
driverCo = nil
|
||||
|
||||
@@ -1381,4 +1381,26 @@ function Game:restoreCheckpointBattle(battle)
|
||||
if battle.resumeCheckpoint then battle:resumeCheckpoint() 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
|
||||
|
||||
@@ -2123,4 +2123,22 @@ function Game2:joystickremoved()
|
||||
TouchControls:joystickremoved()
|
||||
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
|
||||
|
||||
+22
-8
@@ -614,14 +614,18 @@ function CacheFs.mountVersion(version)
|
||||
return true
|
||||
end
|
||||
|
||||
-- Undo mountVersion. A process normally mounts exactly one version and then
|
||||
-- boots it, but the launcher can open the save editor on one game's save,
|
||||
-- close it, and press 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.
|
||||
-- Undo mountVersion in LIFO order relative to mountVersion: generated-tree
|
||||
-- overlays first (assets, then data -- reverse of mountGeneratedTrees), then
|
||||
-- the version folder. PHYSFS resolves by stack order; peeling the wrong
|
||||
-- layer first can leave another version's generated files winning a name.
|
||||
--
|
||||
-- A process normally mounts exactly one version and then boots it, but the
|
||||
-- launcher can open the save editor on one game's save, close it, and press
|
||||
-- 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.
|
||||
function CacheFs.unmountVersion(version)
|
||||
@@ -633,6 +637,16 @@ function CacheFs.unmountVersion(version)
|
||||
base = love.filesystem.getSaveDirectory()
|
||||
end
|
||||
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()
|
||||
if fn and base then
|
||||
done = fn(base .. SEP .. sub) or done
|
||||
|
||||
+16
-10
@@ -397,9 +397,12 @@ function PaletteFX.uiSpriteRedraws()
|
||||
end
|
||||
|
||||
-- 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)
|
||||
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()
|
||||
if g then return g end
|
||||
end
|
||||
@@ -449,24 +452,27 @@ function PaletteFX.pal(data, name)
|
||||
local fromRom = romNamedPal(data, name)
|
||||
if fromRom then return fromRom end
|
||||
end
|
||||
if PaletteFX.usesYellowCgb() then
|
||||
local fromCgb = yellowCgbNamedPal(data, name)
|
||||
if fromCgb then return fromCgb end
|
||||
if GameVersion.isYellow() then
|
||||
if PaletteFX.usesYellowCgb() then
|
||||
local fromCgb = yellowCgbNamedPal(data, name)
|
||||
if fromCgb then return fromCgb end
|
||||
end
|
||||
local y = PaletteFX.yellowPack()
|
||||
local yc = y and y.palettes and y.palettes[name]
|
||||
if yc then return yc end
|
||||
local fromRom = romNamedPal(data, name)
|
||||
if fromRom then return fromRom 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 yc = y and y.palettes and y.palettes[name]
|
||||
if yc then return yc end
|
||||
end
|
||||
if PaletteFX.usesGbcPack() then
|
||||
return romNamedPal(data, name)
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
|
||||
-- the species' palette (data/pokemon/palettes.asm), MEWMON for unknowns.
|
||||
-- transformed forces PAL_GRAYMON (Ditto's palette) regardless of species
|
||||
-- (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)
|
||||
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()
|
||||
-- 160x144 real pixels, never DPI-scaled: see src/render/PixelCanvas.lua
|
||||
-- (#208). Every canvas below is sized in framebuffer pixels for the same
|
||||
-- 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.canvas = PixelCanvas.new(self.uiWidth, self.uiHeight, "nearest")
|
||||
self.battleHUDCanvas = nil
|
||||
|
||||
@@ -951,6 +951,13 @@ function TileRenderer.invalidate()
|
||||
frameImages = {}
|
||||
toggleImages = {}
|
||||
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
|
||||
|
||||
Assets.register(TileRenderer.invalidate)
|
||||
|
||||
+536
-142
@@ -34,10 +34,12 @@ local DEFAULT_ART = {
|
||||
cableBall = "assets/generated/trade/cable_ball.png",
|
||||
cableBallAlt = "assets/generated/trade/cable_ball_alt.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)
|
||||
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)
|
||||
return ok and img or nil
|
||||
end
|
||||
@@ -89,6 +91,92 @@ local SEQ = {
|
||||
"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)
|
||||
opts = opts or {}
|
||||
local self = setmetatable({}, TradeAnim)
|
||||
@@ -120,6 +208,8 @@ function TradeAnim.new(game, opts)
|
||||
cableBall = tryImage(art.cableBall or DEFAULT_ART.cableBall),
|
||||
cableBallAlt = tryImage(art.cableBallAlt or DEFAULT_ART.cableBallAlt),
|
||||
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.recvSprite, self.recvSpriteTrueColor = spriteOf(game, self.received)
|
||||
@@ -136,6 +226,14 @@ function TradeAnim.new(game, opts)
|
||||
self.monVisible = true
|
||||
self.waitingText = 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
|
||||
end
|
||||
|
||||
@@ -151,52 +249,100 @@ function TradeAnim:advance()
|
||||
self.sub = nil
|
||||
self.flash = false
|
||||
self.cableFlash = false
|
||||
self.poof = nil
|
||||
self.activeBallBlock = nil
|
||||
self.activePoofBlock = nil
|
||||
self.slidingBox = false
|
||||
self.wx = 0
|
||||
|
||||
if self.phase == "done" then
|
||||
self.game.stack:pop()
|
||||
if self.onDone then self.onDone() end
|
||||
elseif self.phase == "show_enemy" then
|
||||
self.monVisible = false
|
||||
self.activeBallBlock = 4
|
||||
self.activeBallX, self.activeBallY = 72, 40
|
||||
self.activePoofBlock = nil
|
||||
self.cableSlideOut = 0
|
||||
elseif self.phase == "transfer_lr" then
|
||||
-- wBaseCoord $54, $1c OAM -> screen (76, 12)
|
||||
self.monX, self.monY = 76, 12
|
||||
-- BaseCoord $54, $1c -> Screen (92, 28) for 16x16 icon (center 100, 36)
|
||||
self.monX, self.monY = 92, 28
|
||||
self.scx = 0
|
||||
elseif self.phase == "transfer_rl" then
|
||||
-- wBaseCoord $64, $44 OAM -> screen (92, 52), right GB on screen
|
||||
self.monX, self.monY = 92, 52
|
||||
self.scx = 160
|
||||
-- BaseCoord $64, $44 -> Screen (108, 68) for 16x16 icon (center 116, 76)
|
||||
self.monX, self.monY = 108, 68
|
||||
self.scx = 256
|
||||
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
|
||||
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
|
||||
self.scx = 0x50
|
||||
-- SCX $a0 (160) -> $f0 (240): cable slides in from right, open end rests at x64
|
||||
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")
|
||||
end
|
||||
end
|
||||
|
||||
function TradeAnim:say(text, delay, thenFn)
|
||||
self.waitingText = true
|
||||
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 } }))
|
||||
function TradeAnim:skipHeld()
|
||||
return self.game.input:wasPressed("a") or self.game.input:wasPressed("b")
|
||||
end
|
||||
|
||||
function TradeAnim:skipHeld()
|
||||
return self.game.input:wasPressed("a") or self.game.input:isDown("a")
|
||||
function TradeAnim:drawFrameBlock(blockId, x, y)
|
||||
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
|
||||
|
||||
function TradeAnim:update(dt)
|
||||
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
|
||||
local p = self.phase
|
||||
|
||||
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 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
|
||||
self.scx = 0
|
||||
self.sub = "hold"
|
||||
@@ -206,52 +352,131 @@ function TradeAnim:update(dt)
|
||||
if skip or self.t >= 80 then
|
||||
self.sub = "poof"
|
||||
self.t = 0
|
||||
self.monVisible = false
|
||||
Sound.play(self.game.data, "Ball_Poof")
|
||||
end
|
||||
elseif self.sub == "poof" then
|
||||
self.poof = math.max(0, 16 - self.t)
|
||||
if skip or self.t >= 16 then
|
||||
local step = math.floor(self.t / 6)
|
||||
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)
|
||||
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:advance()
|
||||
end
|
||||
end
|
||||
|
||||
elseif p == "open_cable" or p == "open_cable2" then
|
||||
-- 20 steps of 4px, matching the SCX loop
|
||||
self.scx = math.max(0, 0x50 - self.t * 4)
|
||||
-- Trade_DrawOpenEndOfLinkCable: SCX $a0 (160) -> $f0 (240) in 20 steps of 4px
|
||||
self.scx = math.max(0, 80 - self.t * 4)
|
||||
if skip or self.scx <= 0 then
|
||||
self.scx = 0
|
||||
self:advance()
|
||||
end
|
||||
|
||||
elseif p == "ball_enter" then
|
||||
-- TRADE_BALL_SHAKE then ball rides the cable; X from $60 toward $a0
|
||||
if self.t < 20 then
|
||||
if skip then self.t = 20 end
|
||||
return
|
||||
-- Trade_AnimateBallEnteringLinkCable:
|
||||
-- 1. TRADE_BALL_SHAKE_ANIM (16 ticks) + TradeShakePokeball suction jump (-12, -12, -8 over 9 ticks):
|
||||
-- - Shakes at (72, 64) for 16 ticks (FrameBlock 04, 03, 05, 03)
|
||||
-- - 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
|
||||
if skip then self.ballX = 0xA0 end
|
||||
if self.ballX >= 0xA0 then self:advance() end
|
||||
|
||||
elseif p == "transfer_lr" then
|
||||
-- scroll left GB off while the mon rides the cable, then the sprite
|
||||
-- itself moves right and down into the right GB (Trade_AnimMonMoveVertical)
|
||||
if self.t <= 80 then
|
||||
self.scx = math.min(160, self.t * 2)
|
||||
self.monX, self.monY = 76, 12
|
||||
elseif self.t <= 80 + 32 then
|
||||
self.monX = 76 + math.floor((self.t - 80) / 8) * 4
|
||||
elseif self.t <= 80 + 64 then
|
||||
self.monX = 92
|
||||
self.monY = 12 + math.floor((self.t - 112) / 8) * 10
|
||||
-- 16 units of 16px (256px total scroll) at 2px/frame = 128 frames (SCX 0 -> 256)
|
||||
if self.t <= 128 then
|
||||
self.scx = math.min(256, self.t * 2)
|
||||
self.monX, self.monY = 92, 28
|
||||
elseif self.t <= 128 + 32 then
|
||||
local step = math.floor((self.t - 128) / 8) + 1
|
||||
step = math.min(4, step)
|
||||
self.scx = 256
|
||||
self.monX = 92 + step * 4
|
||||
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
|
||||
self:advance()
|
||||
return
|
||||
@@ -260,17 +485,22 @@ function TradeAnim:update(dt)
|
||||
if skip then self:advance() end
|
||||
|
||||
elseif p == "transfer_rl" then
|
||||
-- mirror: sprite climbs out of the right GB, then the screen scrolls
|
||||
-- back to the left GB
|
||||
-- 64 frames vertical ascent + 128 frames horizontal scroll (SCX 256 -> 0)
|
||||
if self.t <= 32 then
|
||||
self.scx = 160
|
||||
self.monX, self.monY = 92, 52 - math.floor(self.t / 8) * 10
|
||||
local step = math.floor(self.t / 8) + 1
|
||||
step = math.min(4, step)
|
||||
self.scx = 256
|
||||
self.monX = 108
|
||||
self.monY = 68 - step * 10
|
||||
elseif self.t <= 64 then
|
||||
self.monX = 92 - math.floor((self.t - 32) / 8) * 4
|
||||
self.monY = 12
|
||||
elseif self.t <= 64 + 80 then
|
||||
self.scx = math.max(0, 160 - (self.t - 64) * 2)
|
||||
self.monX, self.monY = 76, 12
|
||||
local step = math.floor((self.t - 32) / 8) + 1
|
||||
step = math.min(4, step)
|
||||
self.scx = 256
|
||||
self.monX = 108 - step * 4
|
||||
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
|
||||
self:advance()
|
||||
return
|
||||
@@ -282,54 +512,185 @@ function TradeAnim:update(dt)
|
||||
if skip or self.t >= 100 then self:advance() end
|
||||
|
||||
elseif p == "went_to" then
|
||||
local text = expand(self.game, "_TradeWentToText", {
|
||||
["RAM:wStringBuffer"] = speciesName(self.game, self.sent),
|
||||
["RAM:wLinkEnemyTrainerName"] = self.enemyName,
|
||||
})
|
||||
self:say(text, 200)
|
||||
if not self.sub then
|
||||
self.sub = "text"
|
||||
self.t = 0
|
||||
local text = expand(self.game, "_TradeWentToText", {
|
||||
["RAM:wStringBuffer"] = speciesName(self.game, self.sent),
|
||||
["RAM:wLinkEnemyTrainerName"] = self.enemyName,
|
||||
})
|
||||
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
|
||||
local a = expand(self.game, "_TradeForText", {
|
||||
["RAM:wStringBuffer"] = speciesName(self.game, self.sent),
|
||||
})
|
||||
local b = expand(self.game, "_TradeSendsText", {
|
||||
["RAM:wLinkEnemyTrainerName"] = self.enemyName,
|
||||
["RAM:wNameBuffer"] = nameOf(self.game, self.received),
|
||||
})
|
||||
self.waitingText = true
|
||||
self.game.stack:push(TextBox.new(self.game, a, function()
|
||||
self.game.stack:push(TextBox.new(self.game, b, function()
|
||||
self.waitingText = false
|
||||
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),
|
||||
})
|
||||
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:wNameBuffer"] = nameOf(self.game, self.received),
|
||||
})
|
||||
end
|
||||
elseif self.sub == "sends_text" then
|
||||
if skip or self.t >= 80 then
|
||||
self.dialogText = nil
|
||||
self.sub = nil
|
||||
self:advance()
|
||||
end, { auto = { delay = 80 } }))
|
||||
end, { auto = { delay = 80 } }))
|
||||
end
|
||||
end
|
||||
|
||||
elseif p == "farewell" then
|
||||
local a = expand(self.game, "_TradeWavesFarewellText", {
|
||||
["RAM:wLinkEnemyTrainerName"] = self.enemyName,
|
||||
})
|
||||
local b = expand(self.game, "_TradeTransferredText", {
|
||||
["RAM:wNameBuffer"] = nameOf(self.game, self.received),
|
||||
})
|
||||
self.waitingText = true
|
||||
self.game.stack:push(TextBox.new(self.game, a, function()
|
||||
self.game.stack:push(TextBox.new(self.game, b, function()
|
||||
self.waitingText = false
|
||||
if not self.sub then
|
||||
self.sub = "farewell_text"
|
||||
self.t = 0
|
||||
self.dialogText = expand(self.game, "_TradeWavesFarewellText", {
|
||||
["RAM:wLinkEnemyTrainerName"] = self.enemyName,
|
||||
})
|
||||
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),
|
||||
})
|
||||
end
|
||||
elseif self.sub == "transferred_text" then
|
||||
if skip or self.t >= 80 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, { auto = { delay = 80 } }))
|
||||
end, { auto = { delay = 80 } }))
|
||||
end
|
||||
end
|
||||
|
||||
elseif p == "show_enemy" then
|
||||
if self.t == 1 then
|
||||
Sound.play(self.game.data, "Ball_Poof")
|
||||
self.monVisible = true
|
||||
elseif self.t == 20 then
|
||||
Sound.playCry(self.game.data, self.received.species)
|
||||
elseif self.t >= 120 or skip then
|
||||
local text = expand(self.game, "_TradeTakeCareText", {
|
||||
["RAM:wNameBuffer"] = nameOf(self.game, self.received),
|
||||
})
|
||||
self:say(text, 80)
|
||||
-- Trade_ShowEnemyMon (engine/movie/trade.asm:354):
|
||||
-- 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.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)
|
||||
else
|
||||
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),
|
||||
})
|
||||
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
|
||||
@@ -337,10 +698,18 @@ end
|
||||
local function drawCableHoriz(self, y, x0, x1)
|
||||
local w = math.max(0, x1 - x0)
|
||||
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
|
||||
local iw, ih = self.img.cableHoriz:getDimensions()
|
||||
local quad = love.graphics.newQuad(0, 0, math.min(w, iw), ih, iw, ih)
|
||||
love.graphics.draw(self.img.cableHoriz, quad, x0, y)
|
||||
for x = x0, x1 - 1, iw do
|
||||
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
|
||||
for x = x0, x1 - 8, 8 do
|
||||
love.graphics.draw(self.img.cableSeg, x, y)
|
||||
@@ -348,13 +717,11 @@ local function drawCableHoriz(self, y, x0, x1)
|
||||
else
|
||||
love.graphics.setColor(0.2, 0.2, 0.2, 1)
|
||||
love.graphics.rectangle("fill", x0, y + 1, w, 6)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
end
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
end
|
||||
|
||||
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)
|
||||
local y0 = boxTy * 8
|
||||
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)
|
||||
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)
|
||||
if self.img.bubble then
|
||||
if not self.bubbleQuad then
|
||||
@@ -415,11 +772,14 @@ function TradeAnim:drawGameBoy(x, y)
|
||||
end
|
||||
|
||||
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
|
||||
love.graphics.draw(self.img.cableConn, 88, 32)
|
||||
end
|
||||
drawCableHoriz(self, 32, 96, 160)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
drawCableHoriz(self, 32, 96, 256)
|
||||
self:drawGameBoy(40, 24)
|
||||
Font.drawBox(4, 12, 9, 4)
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
@@ -429,6 +789,9 @@ end
|
||||
|
||||
function TradeAnim:drawRightGB()
|
||||
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.cableVert then
|
||||
for i = 1, 4 do
|
||||
@@ -437,6 +800,7 @@ function TradeAnim:drawRightGB()
|
||||
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
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
self:drawGameBoy(56, 64)
|
||||
Font.drawBox(6, 0, 9, 4)
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
@@ -444,6 +808,24 @@ function TradeAnim:drawRightGB()
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
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()
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.rectangle("fill", 0, 0, 160, 144)
|
||||
@@ -452,10 +834,7 @@ function TradeAnim:draw()
|
||||
if p == "show_player" then
|
||||
love.graphics.push()
|
||||
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
|
||||
-- engine/movie/trade.asm:751
|
||||
love.graphics.draw(self.sentSprite, 56 + self.sentSprite:getWidth(), 16, 0, -1, 1)
|
||||
if self.sentSpriteTrueColor then
|
||||
require("src.render.PaletteFX").markTrueColor(
|
||||
@@ -464,64 +843,79 @@ function TradeAnim:draw()
|
||||
end
|
||||
self:drawMonInfo(self.sent, self.playerOt, self.playerOtId, 10)
|
||||
love.graphics.pop()
|
||||
if self.poof and self.poof > 0 then
|
||||
love.graphics.setColor(0, 0, 0, self.poof / 16)
|
||||
love.graphics.circle("fill", 80, 40, 20 - (self.poof or 0))
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
|
||||
if self.activePoofBlock then
|
||||
self:drawFrameBlock(self.activePoofBlock, self.activePoofX, self.activePoofY)
|
||||
end
|
||||
if self.activeBallBlock then
|
||||
self:drawFrameBlock(self.activeBallBlock, self.activeBallX, self.activeBallY)
|
||||
end
|
||||
|
||||
elseif p == "open_cable" or p == "open_cable2" then
|
||||
love.graphics.push()
|
||||
love.graphics.translate(self.scx, 0)
|
||||
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
|
||||
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
|
||||
if self.img.openCable then
|
||||
love.graphics.draw(self.img.openCable, 64, 16)
|
||||
end
|
||||
-- OAM coords carry a (+8, +16) hardware offset
|
||||
local ball = self.flash and (self.img.cableBallAlt or self.img.cableBall)
|
||||
or self.img.cableBall
|
||||
if ball then
|
||||
love.graphics.draw(ball, self.ballX - 8, self.ballY - 16)
|
||||
else
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
love.graphics.circle("fill", self.ballX, self.ballY - 8, 6)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
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)
|
||||
or self.img.cableBall
|
||||
if ball then
|
||||
love.graphics.draw(ball, self.ballX - 8, self.ballY - 16)
|
||||
else
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
love.graphics.circle("fill", self.ballX, self.ballY - 8, 6)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
end
|
||||
end
|
||||
|
||||
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.translate(-self.scx, 0)
|
||||
self:drawLeftGB()
|
||||
love.graphics.translate(160, 0)
|
||||
love.graphics.translate(256, 0)
|
||||
self:drawRightGB()
|
||||
love.graphics.pop()
|
||||
local mon = p == "transfer_lr" and self.sent or self.received
|
||||
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
|
||||
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
|
||||
-- engine/movie/trade.asm:751
|
||||
love.graphics.draw(self.recvSprite, 56 + self.recvSprite:getWidth(), 16, 0, -1, 1)
|
||||
if self.recvSpriteTrueColor then
|
||||
require("src.render.PaletteFX").markTrueColor(
|
||||
56, 16, self.recvSprite:getDimensions())
|
||||
end
|
||||
end
|
||||
self:drawMonInfo(self.received, self.enemyName, self.enemyOtId, 10)
|
||||
if self.monVisible and self.sub ~= "take_care" and self.sub ~= "delay_end" then
|
||||
self:drawMonInfo(self.received, self.enemyName, self.enemyOtId, 10)
|
||||
end
|
||||
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
|
||||
-- went_to / for_sends / farewell / delay: cleared window; TextBox draws
|
||||
|
||||
self:drawDialog()
|
||||
end
|
||||
|
||||
return TradeAnim
|
||||
|
||||
+155
-36
@@ -22,12 +22,14 @@
|
||||
-- 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).
|
||||
--
|
||||
-- Two hardware details are not reproduced. The four player OBJs all carry
|
||||
-- OAM_PRIO, so on the cart they sit behind background colours 1-3 and only
|
||||
-- show through the window's colour 0; here they are drawn straight over the
|
||||
-- background, which looks the same everywhere the window is transparent and
|
||||
-- differs only if the sprite drifts over solid train tiles. And the ride
|
||||
-- cannot be skipped, exactly as on the cart: the loop reads no input at all.
|
||||
-- Two hardware details from the cart are faithfully reproduced:
|
||||
-- 1. Viewport wrapping: the 256x144 background scrolls in 3 LY override bands,
|
||||
-- wrapped strictly within the 160x144 GB screen so pixels do not bleed into
|
||||
-- the widescreen surround.
|
||||
-- 2. OAM Priority (OAM_PRIO): the four player OBJs sit behind background
|
||||
-- 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 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 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
|
||||
|
||||
-- opts: toGoldenrod (the wScriptVar the officer's script set), onDone()
|
||||
@@ -97,7 +123,7 @@ function MagnetTrainRide:palette(slot)
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
-- Sheets
|
||||
-- Sheets & Shaders
|
||||
--------------------------------------------------------------------------
|
||||
|
||||
local function sheetFor(path)
|
||||
@@ -159,22 +185,66 @@ function MagnetTrainRide:playerQuad(vtile)
|
||||
return quad
|
||||
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
|
||||
-- canvas. Tiles are drawn palette group by palette group so the three
|
||||
-- palettes cost three shader switches rather than one per tile.
|
||||
function MagnetTrainRide:background()
|
||||
if self.bgCanvas ~= nil then return self.bgCanvas or nil end
|
||||
-- DrawMagnetTrain plus SetMagnetTrainPals, rendered once into 256x144 canvases.
|
||||
-- Base canvas contains the full opaque background; Overlay canvas contains
|
||||
-- solid shades 1-3 with shade 0 transparent for OAM_PRIO window masking.
|
||||
function MagnetTrainRide:backgrounds()
|
||||
if self.bgCanvas ~= nil then
|
||||
return self.bgCanvas or nil, self.bgOverlayCanvas or nil
|
||||
end
|
||||
self.bgCanvas = false
|
||||
self.bgOverlayCanvas = false
|
||||
local rows = self.ride:tilemap()
|
||||
local sheet = self:tileSheet()
|
||||
if not (rows and sheet) then return nil end
|
||||
local ok, canvas = pcall(love.graphics.newCanvas, BG_W, SCREEN_H)
|
||||
if not ok then return nil end
|
||||
canvas:setFilter("nearest", "nearest")
|
||||
if not (rows and sheet) then return nil, nil end
|
||||
|
||||
local ok1, baseCanvas = pcall(love.graphics.newCanvas, BG_W, SCREEN_H)
|
||||
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 = {}
|
||||
for row = 1, #rows do
|
||||
@@ -188,11 +258,12 @@ function MagnetTrainRide:background()
|
||||
|
||||
local G = love.graphics
|
||||
local previous = G.getCanvas()
|
||||
-- A canvas does not reset the transform: without this the map lands under
|
||||
-- the renderer's letterbox scale and off the edge.
|
||||
G.push()
|
||||
G.push("all")
|
||||
G.origin()
|
||||
G.setCanvas(canvas)
|
||||
G.setScissor()
|
||||
|
||||
-- 1. Base background canvas (shades 0-3)
|
||||
G.setCanvas(baseCanvas)
|
||||
G.clear(0, 0, 0, 1)
|
||||
G.setColor(1, 1, 1, 1)
|
||||
local shader = GbcPalette.available()
|
||||
@@ -205,10 +276,35 @@ function MagnetTrainRide:background()
|
||||
end
|
||||
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.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
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
@@ -240,19 +336,24 @@ function MagnetTrainRide:update(_dt)
|
||||
end
|
||||
end
|
||||
|
||||
function MagnetTrainRide:drawBackground()
|
||||
local canvas = self:background()
|
||||
-- Draws the 3 LY override bands strictly wrapped inside the 160px GB screen.
|
||||
function MagnetTrainRide:drawBands(canvas)
|
||||
if not canvas then return end
|
||||
local G = love.graphics
|
||||
G.setColor(1, 1, 1, 1)
|
||||
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
|
||||
local top, bottom, scx = band[1], band[2], band[3] % BG_W
|
||||
local height = bottom - top + 1
|
||||
self.bandQuad:setViewport(0, top, BG_W, height, BG_W, SCREEN_H)
|
||||
G.draw(canvas, self.bandQuad, -scx, top)
|
||||
G.draw(canvas, self.bandQuad, -scx + BG_W, top)
|
||||
local w1 = math.min(SCREEN_W, BG_W - scx)
|
||||
self.bandQuad:setViewport(scx, top, w1, height, BG_W, SCREEN_H)
|
||||
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
|
||||
|
||||
@@ -277,16 +378,19 @@ function MagnetTrainRide:drawPlayer()
|
||||
if colors then GbcPalette.use(colors) end
|
||||
G.setColor(1, 1, 1, 1)
|
||||
for _, entry in ipairs(oam) do
|
||||
local quad = self:playerQuad(entry.tile)
|
||||
if quad then
|
||||
G.draw(sheet.image, quad,
|
||||
entry.x + (entry.xflip and 8 or 0), entry.y, 0,
|
||||
entry.xflip and -1 or 1, 1)
|
||||
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)
|
||||
if quad then
|
||||
G.draw(sheet.image, quad,
|
||||
entry.x + (entry.xflip and 8 or 0), entry.y, 0,
|
||||
entry.xflip and -1 or 1, 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
if colors then GbcPalette.clear() end
|
||||
end
|
||||
|
||||
|
||||
-- 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.
|
||||
function MagnetTrainRide:backdrop()
|
||||
@@ -299,13 +403,24 @@ function MagnetTrainRide:drawPanel()
|
||||
G.setColor(backdrop[1] / 255, backdrop[2] / 255, backdrop[3] / 255, 1)
|
||||
G.rectangle("fill", 0, 0, SCREEN_W, SCREEN_H)
|
||||
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()
|
||||
-- 3. Train body / frame overlay (colors 1-3 mask sprite; color 0 is transparent)
|
||||
self:drawBands(overlay)
|
||||
|
||||
G.setColor(1, 1, 1, 1)
|
||||
end
|
||||
|
||||
function MagnetTrainRide:draw()
|
||||
local G = love.graphics
|
||||
G.push("all")
|
||||
G.setScissor(0, 0, SCREEN_W, SCREEN_H)
|
||||
self:drawPanel()
|
||||
G.pop()
|
||||
end
|
||||
|
||||
-- MagnetTrain_LoadGFX_PlayMusic opens on ClearBGPalettes / ClearSprites
|
||||
@@ -323,7 +438,8 @@ function MagnetTrainRide:drawWidescreen(winW, winH)
|
||||
G.setColor(1, 1, 1, 1)
|
||||
local scale = Chrome.fitScale(winW, winH)
|
||||
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.scale(scale, scale)
|
||||
self:drawPanel()
|
||||
@@ -331,3 +447,6 @@ function MagnetTrainRide:drawWidescreen(winW, winH)
|
||||
end
|
||||
|
||||
return MagnetTrainRide
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -77,4 +77,26 @@ do
|
||||
check(Runtime.modRequire == nil, "Runtime.reset clears modRequire")
|
||||
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")
|
||||
|
||||
@@ -57,6 +57,36 @@ eq(love.filesystem.read("assets/generated/fonts/font.png"), "font-bytes",
|
||||
eq(love.filesystem.read("data/generated/constants.lua"), "return {}",
|
||||
"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
|
||||
love.filesystem.remove("red/rom-cache.complete")
|
||||
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")
|
||||
@@ -266,6 +266,18 @@ do
|
||||
"and with no hook at all the special is a no-op, not a clobber")
|
||||
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 ride is only half the feature: the two station scripts end
|
||||
|
||||
+3
-1
@@ -56,7 +56,9 @@ stub.graphics = {
|
||||
end,
|
||||
newQuad = function(x, y, w, h) return { x = x, y = y, w = w, h = h } end,
|
||||
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,
|
||||
newSpriteBatch = function(image, size)
|
||||
local batch = { image = image, sprites = {} }
|
||||
|
||||
@@ -143,6 +143,28 @@ local ok, err = pcall(function()
|
||||
check(#report.lostMons == 0 and probe.party[1].species == "EDITMON",
|
||||
"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
|
||||
-- manifest stays out of Gold (no ROM cache / App.load("gold") required).
|
||||
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()
|
||||
Reference in New Issue
Block a user