Files
gen1recomp/src/ui/gen2/CrystalIntro.lua
T
2026-08-25 09:30:37 -04:00

1296 lines
39 KiB
Lua

-- The Crystal intro movie (pokecrystal engine/movie/intro.asm CrystalIntro),
-- transcribed: the 28-entry scene jumptable stepped once per frame
-- (engine/movie/intro.asm:58-94) over hSCX/hSCY, the two intro counters, live
-- wBGPals2 palettes and a real 32x32 BG map with CGB attributes.
-- Any button skips the whole thing (engine/movie/intro.asm:11-15).
local bit = require("bit")
local Assets = require("src.render.Assets")
local Chrome = require("src.ui.gen2.Chrome")
local GbcPalette = require("src.render.GbcPalette")
local Logger = require("src.core.Logger")
local Music = require("src.core.Music")
local Runtime = require("src.mods.Runtime")
local Sound = require("src.core.Sound")
local SpriteAnims = require("src.ui.gen2.SpriteAnims")
local CrystalIntro = {}
CrystalIntro.__index = CrystalIntro
CrystalIntro.isOpaque = true
local SCREEN_W, SCREEN_H = 160, 144
local BG_TILES = 32
local BG_PIXELS = BG_TILES * 8
local INTRO_MUSIC = "Music_CrystalOpening"
local BLACK = { 0, 0, 0 }
local WHITE = { 255, 255, 255 }
local OAM_PRIO = SpriteAnims.OAM_PRIO
local OAM_XFLIP = SpriteAnims.OAM_XFLIP
local OAM_YFLIP = SpriteAnims.OAM_YFLIP
-- OAM_BANK1 (constants/hardware.inc:997).
local OAM_BANK1 = 0x08
--------------------------------------------------------------------------
-- Sprite-anim data, registered into SpriteAnims' shared tables
--------------------------------------------------------------------------
-- dbsprite (macros/gfx.asm:67-70): y byte first, tile counts mod $100.
local function s(xTile, yTile, xPixel, yPixel, tile, attr)
return {
y = (yTile * 8 + yPixel) % 256,
x = (xTile * 8 + xPixel) % 256,
tile = tile,
attr = attr,
}
end
-- data/sprite_anims/oam.asm:815-852 .OAMData_IntroSuicune1.
local SUICUNE_1 = {
s( 1, -3, 0, 0, 0x05, 0), s( 2, -3, 0, 0, 0x06, 0), s( 3, -3, 0, 0, 0x07, 0),
s(-3, -2, 0, 0, 0x11, 0), s(-2, -2, 0, 0, 0x12, 0), s(-1, -2, 0, 0, 0x13, 0),
s( 0, -2, 0, 0, 0x14, 0), s( 1, -2, 0, 0, 0x15, 0), s( 2, -2, 0, 0, 0x16, 0),
s( 3, -2, 0, 0, 0x17, 0),
s(-4, -1, 0, 0, 0x20, 0), s(-3, -1, 0, 0, 0x21, 0), s(-2, -1, 0, 0, 0x22, 0),
s(-1, -1, 0, 0, 0x23, 0), s( 0, -1, 0, 0, 0x24, 0), s( 1, -1, 0, 0, 0x25, 0),
s( 2, -1, 0, 0, 0x26, 0), s( 3, -1, 0, 0, 0x27, 0),
s(-4, 0, 0, 0, 0x30, 0), s(-3, 0, 0, 0, 0x31, 0), s(-2, 0, 0, 0, 0x32, 0),
s(-1, 0, 0, 0, 0x33, 0), s( 0, 0, 0, 0, 0x34, 0), s( 1, 0, 0, 0, 0x35, 0),
s( 2, 0, 0, 0, 0x36, 0),
s(-4, 1, 0, 0, 0x40, 0), s(-3, 1, 0, 0, 0x41, 0), s(-2, 1, 0, 0, 0x42, 0),
s(-1, 1, 0, 0, 0x43, 0), s( 0, 1, 0, 0, 0x44, 0), s( 1, 1, 0, 0, 0x45, 0),
s( 2, 1, 0, 0, 0x46, 0), s( 3, 1, 0, 0, 0x47, 0),
s(-4, 2, 0, 0, 0x50, 0), s(-3, 2, 0, 0, 0x51, 0), s( 3, 2, 0, 0, 0x57, 0),
}
-- data/sprite_anims/oam.asm:854-883 .OAMData_IntroSuicune2.
local SUICUNE_2 = {
s( 0, -3, 0, 0, 0x04, 0), s( 1, -3, 0, 0, 0x05, 0), s( 2, -3, 0, 0, 0x06, 0),
s(-3, -2, 0, 0, 0x11, 0), s(-2, -2, 0, 0, 0x12, 0), s(-1, -2, 0, 0, 0x13, 0),
s( 0, -2, 0, 0, 0x14, 0), s( 1, -2, 0, 0, 0x15, 0), s( 2, -2, 0, 0, 0x16, 0),
s(-3, -1, 0, 0, 0x21, 0), s(-2, -1, 0, 0, 0x22, 0), s(-1, -1, 0, 0, 0x23, 0),
s( 0, -1, 0, 0, 0x24, 0), s( 1, -1, 0, 0, 0x25, 0), s( 2, -1, 0, 0, 0x26, 0),
s(-4, 0, 0, 0, 0x30, 0), s(-3, 0, 0, 0, 0x31, 0), s(-2, 0, 0, 0, 0x32, 0),
s(-1, 0, 0, 0, 0x33, 0), s( 0, 0, 0, 0, 0x34, 0), s( 1, 0, 0, 0, 0x35, 0),
s(-2, 1, 0, 0, 0x42, 0), s(-1, 1, 0, 0, 0x43, 0), s( 0, 1, 0, 0, 0x44, 0),
s( 1, 1, 0, 0, 0x45, 0),
s(-1, 2, 0, 0, 0x53, 0), s( 0, 2, 0, 0, 0x54, 0), s( 1, 2, 0, 0, 0x55, 0),
}
-- data/sprite_anims/oam.asm:885-916 .OAMData_IntroSuicune3.
local SUICUNE_3 = {
s( 0, -3, 0, 0, 0x04, 0), s( 1, -3, 0, 0, 0x05, 0),
s(-3, -2, 0, 0, 0x11, 0), s(-2, -2, 0, 0, 0x12, 0), s(-1, -2, 0, 0, 0x13, 0),
s( 0, -2, 0, 0, 0x14, 0), s( 1, -2, 0, 0, 0x15, 0), s( 2, -2, 0, 0, 0x16, 0),
s( 3, -2, 0, 0, 0x17, 0),
s(-4, -1, 0, 0, 0x20, 0), s(-3, -1, 0, 0, 0x21, 0), s(-2, -1, 0, 0, 0x22, 0),
s(-1, -1, 0, 0, 0x23, 0), s( 0, -1, 0, 0, 0x24, 0), s( 1, -1, 0, 0, 0x25, 0),
s( 2, -1, 0, 0, 0x26, 0),
s(-4, 0, 0, 0, 0x30, 0), s(-3, 0, 0, 0, 0x31, 0), s(-2, 0, 0, 0, 0x32, 0),
s(-1, 0, 0, 0, 0x33, 0), s( 0, 0, 0, 0, 0x34, 0), s( 1, 0, 0, 0, 0x35, 0),
s(-2, 1, 0, 0, 0x42, 0), s(-1, 1, 0, 0, 0x43, 0), s( 0, 1, 0, 0, 0x44, 0),
s( 1, 1, 0, 0, 0x45, 0),
s(-2, 2, 0, 0, 0x52, 0), s(-1, 2, 0, 0, 0x53, 0), s( 0, 2, 0, 0, 0x54, 0),
s( 1, 2, 0, 0, 0x55, 0),
}
-- data/sprite_anims/oam.asm:918-950 .OAMData_IntroSuicune4.
local SUICUNE_4 = {
s(-3, -2, 0, 0, 0x11, 0), s(-2, -2, 0, 0, 0x12, 0), s(-1, -2, 0, 0, 0x13, 0),
s( 0, -2, 0, 0, 0x14, 0), s( 1, -2, 0, 0, 0x15, 0), s( 2, -2, 0, 0, 0x16, 0),
s( 3, -2, 0, 0, 0x17, 0),
s(-4, -1, 0, 0, 0x20, 0), s(-3, -1, 0, 0, 0x21, 0), s(-2, -1, 0, 0, 0x22, 0),
s(-1, -1, 0, 0, 0x23, 0), s( 0, -1, 0, 0, 0x24, 0), s( 1, -1, 0, 0, 0x25, 0),
s( 2, -1, 0, 0, 0x26, 0), s( 3, -1, 0, 0, 0x27, 0),
s(-4, 0, 0, 0, 0x30, 0), s(-3, 0, 0, 0, 0x31, 0), s(-2, 0, 0, 0, 0x32, 0),
s(-1, 0, 0, 0, 0x33, 0), s( 0, 0, 0, 0, 0x34, 0), s( 1, 0, 0, 0, 0x35, 0),
s( 2, 0, 0, 0, 0x36, 0),
s(-3, 1, 0, 0, 0x41, 0), s(-2, 1, 0, 0, 0x42, 0), s(-1, 1, 0, 0, 0x43, 0),
s( 0, 1, 0, 0, 0x44, 0), s( 1, 1, 0, 0, 0x45, 0),
s(-3, 2, 0, 0, 0x51, 0), s(-2, 2, 0, 0, 0x52, 0), s( 0, 2, 0, 0, 0x54, 0),
s( 1, 2, 0, 0, 0x55, 0),
}
-- data/sprite_anims/oam.asm:952-978 .OAMData_IntroPichu: 5x5 block,
-- OBJ pal 1, VRAM bank 1.
local PICHU = {}
for row = 0, 4 do
for col = 0, 4 do
PICHU[#PICHU + 1] = s(col - 3, row - 3, 4, 4, row * 16 + col,
1 + OAM_BANK1)
end
end
-- data/sprite_anims/oam.asm:980-997 .OAMData_IntroWooper: 4x4 block,
-- OBJ pal 2, VRAM bank 1.
local WOOPER = {}
for row = 0, 3 do
for col = 0, 3 do
WOOPER[#WOOPER + 1] = s(col - 3, row - 2, 4, 0, row * 4 + col,
2 + OAM_BANK1)
end
end
-- data/sprite_anims/oam.asm:999-1017 .OAMData_IntroUnown1/2/3.
local UNOWN_1 = { s(-1, -1, 4, 4, 0x00, 0) }
local UNOWN_2 = {
s(-1, 0, 0, 0, 0x00, 0), s(-1, -1, 0, 0, 0x01, 0), s( 0, -1, 0, 0, 0x02, 0),
}
local UNOWN_3 = {
s(-2, 1, 0, 0, 0x00, 0), s(-2, 0, 0, 0, 0x01, 0), s(-2, -1, 0, 0, 0x02, 0),
s(-1, -1, 0, 0, 0x03, 0), s(-1, -2, 0, 0, 0x04, 0), s( 0, -2, 0, 0, 0x05, 0),
s( 1, -2, 0, 0, 0x06, 0),
}
-- data/sprite_anims/oam.asm:177-182 .OAMData_IntroUnownF2_1.
local UNOWN_F2_1 = {
s(-1, -1, 0, 0, 0x00, 0), s( 0, -1, 0, 0, 0x00, OAM_XFLIP),
s(-1, 0, 0, 0, 0x00, OAM_YFLIP), s( 0, 0, 0, 0, 0x00, OAM_XFLIP + OAM_YFLIP),
}
-- data/sprite_anims/oam.asm:1019-1028 .OAMData_IntroUnownF2_2.
local UNOWN_F2_2 = {
s(-2, -1, 0, 0, 0x00, 0), s(-1, -1, 0, 0, 0x01, 0),
s( 0, -1, 0, 0, 0x01, OAM_XFLIP), s( 1, -1, 0, 0, 0x00, OAM_XFLIP),
s(-2, 0, 0, 0, 0x00, OAM_YFLIP), s(-1, 0, 0, 0, 0x01, OAM_YFLIP),
s( 0, 0, 0, 0, 0x01, OAM_XFLIP + OAM_YFLIP),
s( 1, 0, 0, 0, 0x00, OAM_XFLIP + OAM_YFLIP),
}
-- data/sprite_anims/oam.asm:1030-1043 .OAMData_IntroUnownF2_3.
local UNOWN_F2_3 = {
s(-1, -3, 0, 0, 0x00, 0), s(-1, -2, 0, 0, 0x01, 0), s(-1, -1, 0, 0, 0x02, 0),
s( 0, -3, 0, 0, 0x00, OAM_XFLIP), s( 0, -2, 0, 0, 0x01, OAM_XFLIP),
s( 0, -1, 0, 0, 0x02, OAM_XFLIP),
s(-1, 0, 0, 0, 0x02, OAM_YFLIP), s(-1, 1, 0, 0, 0x01, OAM_YFLIP),
s(-1, 2, 0, 0, 0x00, OAM_YFLIP),
s( 0, 0, 0, 0, 0x02, OAM_XFLIP + OAM_YFLIP),
s( 0, 1, 0, 0, 0x01, OAM_XFLIP + OAM_YFLIP),
s( 0, 2, 0, 0, 0x00, OAM_XFLIP + OAM_YFLIP),
}
-- data/sprite_anims/oam.asm:1045-1066 .OAMData_IntroUnownF2_4_5: 4x5 block.
local UNOWN_F2_45 = {}
for row = 0, 4 do
for col = 0, 3 do
UNOWN_F2_45[#UNOWN_F2_45 + 1] = s(col - 2, row - 3, 0, 4, row * 4 + col, 0)
end
end
-- data/sprite_anims/oam.asm:1068-1089 .OAMData_IntroSuicuneAway: a zigzag of
-- twenty grass tiles, OBJ pal 1, behind the BG.
local AWAY = {}
do
local ys = { 0, 1, 2, 3, 4, 3, 2, 1, 0, 1, 2, 3, 4, 3, 2, 1 }
for index, y in ipairs(ys) do
AWAY[#AWAY + 1] = s(index, y, 0, 0, 0x00, 1 + OAM_PRIO)
end
local tail = { { -15, 0 }, { -14, 1 }, { -13, 2 }, { -12, 3 } }
for _, spot in ipairs(tail) do
AWAY[#AWAY + 1] = s(spot[1], spot[2], 0, 0, 0x00, 1 + OAM_PRIO)
end
end
-- spriteanimoam bases (data/sprite_anims/oam.asm:120-136).
local OAMSETS = {
INTRO_SUICUNE_1 = { 0x00, SUICUNE_1 },
INTRO_SUICUNE_2 = { 0x08, SUICUNE_2 },
INTRO_SUICUNE_3 = { 0x60, SUICUNE_3 },
INTRO_SUICUNE_4 = { 0x68, SUICUNE_4 },
INTRO_PICHU_1 = { 0x00, PICHU },
INTRO_PICHU_2 = { 0x05, PICHU },
INTRO_PICHU_3 = { 0x0a, PICHU },
INTRO_WOOPER = { 0x50, WOOPER },
INTRO_UNOWN_1 = { 0x00, UNOWN_1 },
INTRO_UNOWN_2 = { 0x01, UNOWN_2 },
INTRO_UNOWN_3 = { 0x04, UNOWN_3 },
INTRO_UNOWN_F_2_1 = { 0x00, UNOWN_F2_1 },
INTRO_UNOWN_F_2_2 = { 0x01, UNOWN_F2_2 },
INTRO_UNOWN_F_2_3 = { 0x03, UNOWN_F2_3 },
INTRO_UNOWN_F_2_4 = { 0x08, UNOWN_F2_45 },
INTRO_UNOWN_F_2_5 = { 0x1c, UNOWN_F2_45 },
INTRO_SUICUNE_AWAY = { 0x80, AWAY },
}
local function f(oamset, duration, flags)
return { oamset = oamset, duration = duration, flags = flags or 0 }
end
-- data/sprite_anims/framesets.asm:429-489.
local FRAMESETS = {
IntroSuicune = {
f("INTRO_SUICUNE_1", 3), f("INTRO_SUICUNE_2", 3),
f("INTRO_SUICUNE_3", 3), f("INTRO_SUICUNE_4", 3), "restart",
},
IntroSuicune2 = { f("INTRO_SUICUNE_4", 3), f("INTRO_SUICUNE_1", 7), "end" },
IntroPichu = {
f("INTRO_PICHU_1", 32), f("INTRO_PICHU_2", 7), f("INTRO_PICHU_3", 7), "end",
},
IntroWooper = { f("INTRO_WOOPER", 3), "end" },
IntroUnown1 = {
f("INTRO_UNOWN_1", 3), f("INTRO_UNOWN_2", 3), f("INTRO_UNOWN_3", 7),
f("delete", 0),
},
IntroUnown2 = {
f("INTRO_UNOWN_1", 3, OAM_XFLIP), f("INTRO_UNOWN_2", 3, OAM_XFLIP),
f("INTRO_UNOWN_3", 7, OAM_XFLIP), f("delete", 0),
},
IntroUnown3 = {
f("INTRO_UNOWN_1", 3, OAM_YFLIP), f("INTRO_UNOWN_2", 3, OAM_YFLIP),
f("INTRO_UNOWN_3", 7, OAM_YFLIP), f("delete", 0),
},
IntroUnown4 = {
f("INTRO_UNOWN_1", 3, OAM_XFLIP + OAM_YFLIP),
f("INTRO_UNOWN_2", 3, OAM_XFLIP + OAM_YFLIP),
f("INTRO_UNOWN_3", 7, OAM_XFLIP + OAM_YFLIP), f("delete", 0),
},
IntroUnownF2 = {
f("INTRO_UNOWN_F_2_1", 3), f("INTRO_UNOWN_F_2_2", 3),
f("INTRO_UNOWN_F_2_3", 3), f("INTRO_UNOWN_F_2_4", 7),
f("INTRO_UNOWN_F_2_5", 7), "end",
},
IntroSuicuneAway = { f("INTRO_SUICUNE_AWAY", 3), "end" },
IntroUnownF = { f("wait", 0), "end" },
}
local function reinit(st, framesetId)
st.framesetId = framesetId
st.duration = 0
st.frame = -1
end
local SEQUENCES = {}
-- SpriteAnimFunc_IntroSuicune (engine/sprite_anims/functions.asm:750-776):
-- idle while wIntroSceneTimer is 0, then the jump arc on a negated sine.
SEQUENCES.IntroSuicune = function(sys, st)
if (sys.timer or 0) == 0 then return end
st.yOffset = 0
st.var2 = (st.var2 + 2) % 256
st.yOffset = SpriteAnims.sine((256 - st.var2) % 256, 32)
reinit(st, "IntroSuicune2")
end
-- SpriteAnimFunc_IntroPichuWooper (engine/sprite_anims/functions.asm:778-795).
SEQUENCES.IntroPichuWooper = function(_, st)
if st.var1 >= 20 then return end
st.var1 = st.var1 + 2
st.yOffset = SpriteAnims.sine((256 - st.var1) % 256, 32)
end
-- SpriteAnimFunc_IntroUnown (engine/sprite_anims/functions.asm:797-821):
-- VAR1 is the angle, the counter is the radius (core.asm:543-545).
SEQUENCES.IntroUnown = function(_, st)
local radius = st.jt
st.jt = (st.jt + 3) % 256
st.yOffset = SpriteAnims.sine(st.var1, radius)
st.xOffset = SpriteAnims.cosine(st.var1, radius)
end
-- SpriteAnimFunc_IntroUnownF (engine/sprite_anims/functions.asm:823-829):
-- wSlotsDelay aliases wIntroSceneFrameCounter (ram/wram.asm:1602,1649).
SEQUENCES.IntroUnownF = function(sys, st)
if (sys.counter or 0) ~= 0x40 then return end
reinit(st, "IntroUnownF2")
end
-- SpriteAnimFunc_IntroSuicuneAway (engine/sprite_anims/functions.asm:831-837).
SEQUENCES.IntroSuicuneAway = function(_, st)
st.y = (st.y + 16) % 256
end
-- data/sprite_anims/objects.asm:81-92.
local OBJECTS = {
INTRO_SUICUNE = { "IntroSuicune", "IntroSuicune" },
INTRO_PICHU = { "IntroPichu", "IntroPichuWooper" },
INTRO_WOOPER = { "IntroWooper", "IntroPichuWooper" },
INTRO_UNOWN = { "IntroUnown1", "IntroUnown" },
INTRO_UNOWN_F = { "IntroUnownF", "IntroUnownF" },
INTRO_SUICUNE_AWAY = { "IntroSuicuneAway", "IntroSuicuneAway" },
}
local function register(target, entries)
for name, value in pairs(entries) do
if target[name] == nil then target[name] = value end
end
end
register(SpriteAnims.OAMSETS, OAMSETS)
register(SpriteAnims.FRAMESETS, FRAMESETS)
register(SpriteAnims.OBJECTS, OBJECTS)
register(SpriteAnims.SEQUENCES, SEQUENCES)
--------------------------------------------------------------------------
-- Palettes
--------------------------------------------------------------------------
local function scale31(value) return math.floor(value * 255 / 31 + 0.5) end
-- CrystalIntro_UnownFade's three 32-step ramps
-- (engine/movie/intro.asm:1312-1328).
local BW_FADE, LBLUE_FADE, BLUE_FADE = {}, {}, {}
for hue = 0, 31 do
BW_FADE[hue + 1] = { scale31(hue), scale31(hue), scale31(hue) }
LBLUE_FADE[hue + 1] = { 0, scale31(math.floor(hue / 2)), scale31(hue) }
BLUE_FADE[hue + 1] = { 0, 0, scale31(hue) }
end
local function palCopy(source)
local out = {}
for index = 1, 4 do
local color = source and source[index]
out[index] = color and { color[1], color[2], color[3] } or BLACK
end
return out
end
local function flatPals(color)
local out = {}
for pal = 1, 8 do out[pal] = { color, color, color, color } end
return out
end
--------------------------------------------------------------------------
-- Construction
--------------------------------------------------------------------------
function CrystalIntro:wantsFillScale() return true end
function CrystalIntro:drawsWidescreen() return true end
function CrystalIntro.new(game, opts)
opts = opts or {}
local self = setmetatable({}, CrystalIntro)
self.game = game
self.onDone = opts.onDone
local data = (game and game.data) or {}
self.assets = opts.intro or data.gen2Intro or (game and game.introData) or nil
if not (self.assets and self.assets.acts) then
Logger.warn("crystal intro: no intro.lua in the cache -- re-import "
.. "this version or the movie plays blank")
end
self.images = {}
self.sheets = {}
self.anims = SpriteAnims.new()
self.scene = 1
self.phase = 0
self.hold = 0
self.done = false
self.frames = 0
self.scx, self.scy = 0, 0
self.counter, self.timer = 0, 0
self.treeScroll, self.grassScroll = 0, 0
self.lyActive = false
self.grassFrame = nil
self.bgPals = flatPals(BLACK)
self.obPals = flatPals(BLACK)
self.map, self.attr = {}, {}
for index = 1, BG_TILES * BG_TILES do
self.map[index] = 0
self.attr[index] = 0
end
self.act = nil
self.mapDirty, self.palsDirty = true, true
return self
end
--------------------------------------------------------------------------
-- Scene plumbing
--------------------------------------------------------------------------
local function actData(self)
local acts = self.assets and self.assets.acts
return acts and self.act and acts[self.act] or nil
end
-- ClearSpriteAnims zeroes wSpriteAnimData through wGlobalAnimXOffset
-- (engine/sprite_anims/core.asm:1-11, ram/wram.asm:202-272).
local function clearAnims(self)
self.anims:clear()
self.anims.globalX, self.anims.globalY = 0, 0
end
local function fades(self)
return (self.assets and self.assets.fades) or {}
end
local function markDirty(self)
self.palsDirty = true
end
local function loadAct(self, key)
self.act = key
self.grassFrame = nil
local act = actData(self)
for index = 1, BG_TILES * BG_TILES do
self.map[index] = act and act.tilemap and act.tilemap[index] or 0
self.attr[index] = act and act.attrmap and act.attrmap[index] or 0
end
local palettes = act and act.palettes or {}
for pal = 1, 8 do
self.bgPals[pal] = palCopy(palettes.bg and palettes.bg[pal])
self.obPals[pal] = palCopy(palettes.obj and palettes.obj[pal])
end
self.mapDirty, self.palsDirty = true, true
end
-- Intro_ClearBGPals blacks all 16 palettes and burns two frames
-- (engine/movie/intro.asm:1554-1571); IntroScene26 clears to white instead
-- (engine/movie/intro.asm:1058, home/tilemap.asm:1-9,168-196).
-- Request2bpp (home/gfx.asm:1,190-260): TILES_PER_CYCLE tiles per frame,
-- then one more frame for the final short (or empty) request.
local function requestFrames(tiles)
local frames = 0
for _, count in ipairs(tiles) do
frames = frames + math.floor(count / 8) + 1
end
return frames
end
local function setup(self, white, tiles, fn)
if self.phase == 0 then
self.phase = 1
local color = white and WHITE or BLACK
self.bgPals = flatPals(color)
self.obPals = flatPals(color)
markDirty(self)
self.hold = (white and 4 or 2) + requestFrames(tiles) - 1
return
end
self.phase = 0
fn(self)
self.scene = self.scene + 1
end
function CrystalIntro:playMusic(song)
local data = self.game and self.game.data
local audio = data and data.audio
if audio and audio.runtime and audio.songs and audio.songs[song] then
Music.play(data, song)
end
end
function CrystalIntro:playSfx(name)
local data = self.game and self.game.data
local audio = data and data.audio
if audio and audio.runtime and audio.sfx and audio.sfx[name] then
Sound.play(data, name)
end
end
-- CrystalIntro_UnownFade (engine/movie/intro.asm:1231-1310): zero every BG
-- palette, then write the triangle-wave step of the three ramps into colours
-- 1-3 of palette `pal`.
local function unownFade(self, pal, t)
local step = t % 64
if step > 31 then step = 63 - step end
self.bgPals = flatPals(BLACK)
local target = self.bgPals[pal + 1]
target[2] = BW_FADE[step + 1]
target[3] = LBLUE_FADE[step + 1]
target[4] = BLUE_FADE[step + 1]
markDirty(self)
end
-- CrystalIntro_InitUnownAnim (engine/movie/intro.asm:1191-1229): four structs
-- at one spot, angles $08/$18/$28/$38, framesets 4/3/1/2.
local UNOWN_SWIRL = {
{ 0x08, "IntroUnown4" }, { 0x18, "IntroUnown3" },
{ 0x28, "IntroUnown1" }, { 0x38, "IntroUnown2" },
}
local function initUnownAnim(self, x, y)
for _, spec in ipairs(UNOWN_SWIRL) do
local st = self.anims:init("INTRO_UNOWN", x, y)
if st then
st.var1 = spec[1]
reinit(st, spec[2])
end
end
end
-- Intro_ResetLYOverrides / Intro_PerspectiveScrollBG
-- (engine/movie/intro.asm:1630-1676): trees on lines 0-94 at half speed,
-- grass on lines 95-143 at double, hSCX following the tree band.
local function resetLYOverrides(self)
self.treeScroll, self.grassScroll = 0, 0
self.lyActive = true
end
local function perspectiveScroll(self)
if self.counter % 2 == 1 then
self.treeScroll = (self.treeScroll + 1) % 256
end
self.grassScroll = (self.grassScroll + 2) % 256
self.scx = self.treeScroll
end
-- Intro_RustleGrass (engine/movie/intro.asm:1521-1547): the four tiles at
-- vTiles2 $09 cycle grass1/grass2/grass3/grass2 for the first 36 frames.
local RUSTLE = { 1, 2, 3, 2 }
local function rustleGrass(self)
if self.counter >= 36 then return end
local frame = RUSTLE[math.floor(self.counter / 4) % 4 + 1]
if frame ~= self.grassFrame then
self.grassFrame = frame
self.mapDirty = true
end
end
-- Intro_ColoredSuicuneFrameSwap (engine/movie/intro.asm:1500-1519): toggle
-- bit 3 of every non-zero id below $80 in the visible 20x18 window.
local function frameSwap(self)
for row = 0, 17 do
for col = 0, 19 do
local index = row * BG_TILES + col + 1
local id = self.map[index]
if id ~= 0 and id < 0x80 then
self.map[index] = bit.bxor(id, 8)
end
end
end
self.mapDirty = true
end
--------------------------------------------------------------------------
-- Scenes (engine/movie/intro.asm:96-1153)
--------------------------------------------------------------------------
local Scenes = {}
-- IntroScene1 (engine/movie/intro.asm:96-146).
Scenes[1] = function(self)
setup(self, false, { 64, 128, 128, 64 }, function()
loadAct(self, "unownA")
clearAnims(self)
self.scx, self.scy = 0, 0
self.counter, self.timer = 0, 0
end)
end
-- IntroScene2 (engine/movie/intro.asm:148-170).
Scenes[2] = function(self)
local a = self.counter
self.counter = (a + 1) % 256
if a >= 0x80 then
self.scene = 3
return
end
if a == 0x60 then
initUnownAnim(self, 11 * 8, 11 * 8)
self:playSfx("Sfx_IntroUnown1")
end
self.timer = a
unownFade(self, 0, a)
end
-- IntroScene3 (engine/movie/intro.asm:172-218).
Scenes[3] = function(self)
setup(self, false, { 64, 128, 64 }, function()
loadAct(self, "background")
resetLYOverrides(self)
self.scx, self.scy = 0, 0
self.counter = 0
end)
end
-- IntroScene4 (engine/movie/intro.asm:220-232).
Scenes[4] = function(self)
perspectiveScroll(self)
if self.counter == 0x80 then
self.scene = 5
return
end
self.counter = (self.counter + 1) % 256
end
-- IntroScene5 (engine/movie/intro.asm:234-285).
Scenes[5] = function(self)
setup(self, false, { 64, 128, 128, 64 }, function()
loadAct(self, "unownHI")
self.lyActive = false
clearAnims(self)
self.scx, self.scy = 0, 0
self.counter, self.timer = 0, 0
end)
end
-- IntroScene6 (engine/movie/intro.asm:287-330).
Scenes[6] = function(self)
local a = self.counter
self.counter = (a + 1) % 256
if a >= 0x80 then
self.scene = 7
return
end
if a == 0x60 then
initUnownAnim(self, 6 * 8, 14 * 8)
self:playSfx("Sfx_IntroUnown1")
self.timer = a
unownFade(self, 1, a)
return
end
if a >= 0x40 then
self.timer = a
unownFade(self, 1, a)
return
end
if a == 0x20 then
initUnownAnim(self, 15 * 8, 7 * 8)
self:playSfx("Sfx_IntroUnown2")
end
self.timer = a
unownFade(self, 0, a)
end
-- IntroScene7 (engine/movie/intro.asm:332-401).
Scenes[7] = function(self)
setup(self, false, { 64, 128, 255, 128, 64 }, function()
loadAct(self, "background")
resetLYOverrides(self)
clearAnims(self)
self.anims:init("INTRO_SUICUNE", 27 * 8, 13 * 8 + 4)
self.anims.globalX = 0xf0
self.scx, self.scy = 0, 0
self.counter, self.timer = 0, 0
end)
end
-- IntroScene8 (engine/movie/intro.asm:403-430).
Scenes[8] = function(self)
local a = self.counter
self.counter = (a + 1) % 256
if a == 0x40 then
self:playSfx("Sfx_IntroSuicune3")
elseif a < 0x40 then
perspectiveScroll(self)
return
end
if self.anims.globalX == 0 then
self:playSfx("Sfx_IntroSuicune2")
self.anims:clear()
self.scene = 9
return
end
self.anims.globalX = (self.anims.globalX - 8) % 256
end
-- IntroScene9 (engine/movie/intro.asm:432-467): palette bands over the
-- whole map, six blocking frames.
Scenes[9] = function(self)
self.lyActive = false
for row = 0, 17 do
local pal = (row < 12 and 1) or (row < 15 and 2) or 3
for col = 0, BG_TILES - 1 do
self.attr[row * BG_TILES + col + 1] = pal
end
end
self.anims.globalX = 0
self.counter = 0
self.mapDirty = true
self.hold = 6
self.scene = 10
end
-- IntroScene10 (engine/movie/intro.asm:469-500).
Scenes[10] = function(self)
rustleGrass(self)
local a = self.counter
self.counter = (a + 1) % 256
if a == 0xc0 then
self.scene = 11
return
end
if a == 0x20 then
self.anims:init("INTRO_WOOPER", 6 * 8, 22 * 8)
self:playSfx("Sfx_IntroPichu")
elseif a == 0x40 then
self.anims:init("INTRO_PICHU", 16 * 8, 21 * 8 + 1)
self:playSfx("Sfx_IntroPichu")
end
end
-- IntroScene11 (engine/movie/intro.asm:502-550).
Scenes[11] = function(self)
setup(self, false, { 64, 128, 64 }, function()
loadAct(self, "unowns")
self.lyActive = false
clearAnims(self)
self.scx, self.scy = 0, 0
self.counter, self.timer = 0, 0
end)
end
-- IntroScene12's .UnownSounds (engine/movie/intro.asm:615-624).
local UNOWN_SOUNDS = {
[0x00] = "Sfx_IntroUnown3", [0x20] = "Sfx_IntroUnown2",
[0x40] = "Sfx_IntroUnown1", [0x60] = "Sfx_IntroUnown2",
[0x80] = "Sfx_IntroUnown3", [0x90] = "Sfx_IntroUnown2",
[0xa0] = "Sfx_IntroUnown1", [0xb0] = "Sfx_IntroUnown2",
}
-- IntroScene12 (engine/movie/intro.asm:552-613).
Scenes[12] = function(self)
local a = self.counter
local sound = UNOWN_SOUNDS[a]
if sound then
Sound.sfxChannelsOff()
self:playSfx(sound)
end
self.counter = (a + 1) % 256
if a >= 0xc0 then
self.scene = 13
return
end
if a < 0x80 then
self.timer = (a % 0x20) * 2
unownFade(self, math.floor(a / 0x20), self.timer)
else
self.timer = (a % 0x10) * 4
unownFade(self, 4 + math.floor((a - 0x80) / 0x10), self.timer)
end
end
-- IntroScene13 (engine/movie/intro.asm:626-683).
Scenes[13] = function(self)
setup(self, false, { 64, 255, 128, 64 }, function()
loadAct(self, "background")
clearAnims(self)
self.anims:init("INTRO_SUICUNE", 11 * 8, 13 * 8 + 4)
self:playMusic(INTRO_MUSIC)
self.scx, self.scy = 0, 0
self.counter, self.timer = 0, 0
end)
end
-- IntroScene14 (engine/movie/intro.asm:685-728).
Scenes[14] = function(self)
self.scx = (self.scx - 10) % 256
local a = self.counter
self.counter = (a + 1) % 256
if a == 0x80 then
self.scene = 15
return
end
if a == 0x60 then
self:playSfx("Sfx_IntroSuicune4")
end
if a >= 0x60 then
self.timer = 1
local gx = self.anims.globalX
if gx < 0x88 then
self.anims:clear()
else
self.anims.globalX = (gx - 8) % 256
end
elseif a >= 0x40 then
self.anims.globalX = (self.anims.globalX - 2) % 256
end
end
-- IntroScene15 (engine/movie/intro.asm:730-792).
Scenes[15] = function(self)
setup(self, false, { 64, 128, 128, 1, 64 }, function()
loadAct(self, "suicuneJump")
clearAnims(self)
self.anims:init("INTRO_UNOWN_F", 5 * 8, 8 * 8)
self.anims:init("INTRO_SUICUNE_AWAY", 0, 12 * 8)
self.scx, self.scy = 0, SCREEN_H
self.counter, self.timer = 0, 0
end)
end
-- IntroScene16 (engine/movie/intro.asm:794-810).
Scenes[16] = function(self)
local a = self.counter
self.counter = (a + 1) % 256
if a >= 0x80 then
self.scene = 17
return
end
if a % 4 == 0 then frameSwap(self) end
if self.scy ~= 0 then
self.scy = (self.scy + 8) % 256
end
end
-- IntroScene17 (engine/movie/intro.asm:812-859).
Scenes[17] = function(self)
setup(self, false, { 64, 255, 64 }, function()
loadAct(self, "suicuneClose")
clearAnims(self)
self.scx, self.scy = 0, 0
self.counter, self.timer = 0, 0
end)
end
-- IntroScene18 (engine/movie/intro.asm:861-876).
Scenes[18] = function(self)
local a = self.counter
self.counter = (a + 1) % 256
if a >= 0x60 then
self.scene = 19
return
end
if self.scx ~= 0x60 then
self.scx = (self.scx + 8) % 256
end
end
-- IntroScene19 (engine/movie/intro.asm:878-941).
Scenes[19] = function(self)
setup(self, false, { 64, 128, 128, 1, 64 }, function()
loadAct(self, "suicuneBack")
clearAnims(self)
self.anims:init("INTRO_SUICUNE_AWAY", 0, 12 * 8)
self.scx, self.scy = 0, (-5 * 8) % 256
self.counter, self.timer = 0, 0
end)
end
-- IntroScene20 (engine/movie/intro.asm:943-988).
Scenes[20] = function(self)
local a = self.counter
self.counter = (a + 1) % 256
if a >= 0x98 then
self.scene = 21
return
end
if a >= 0x58 then return end
if a >= 0x40 then
local b = a - 0x18
if b % 4 == 3 then
local slot = math.floor(b % 0x20 / 4)
self.timer = slot
self.bgPals[slot + 1] = palCopy(fades(self).unownAppear)
markDirty(self)
end
return
end
if a >= 0x28 then return end
self.scy = (self.scy + 1) % 256
end
-- IntroScene21 (engine/movie/intro.asm:990-1000).
Scenes[21] = function(self)
frameSwap(self)
self.hold = 3
self.counter, self.timer = 0, 0
self.scene = 22
end
-- IntroScene22 (engine/movie/intro.asm:1002-1012).
Scenes[22] = function(self)
local a = self.counter
self.counter = (a + 1) % 256
if a >= 0x8 then
self.anims:clear()
self.scene = 23
end
end
-- IntroScene23 (engine/movie/intro.asm:1014-1018).
Scenes[23] = function(self)
self.counter = 0
self.scene = 24
end
-- IntroScene24 (engine/movie/intro.asm:1020-1042).
Scenes[24] = function(self)
local a = self.counter
self.counter = (a + 1) % 256
if a >= 0x20 then
self.counter = 0x40
self.scene = 25
return
end
if a % 4 ~= 0 then return end
local toWhite = fades(self).toWhite
local pal = toWhite and toWhite[math.floor(a % 0x20 / 4) + 1]
if not pal then return end
for slot = 1, 8 do
self.bgPals[slot] = palCopy(pal)
end
markDirty(self)
end
-- IntroScene25 (engine/movie/intro.asm:1044-1054).
Scenes[25] = function(self)
if self.counter - 1 == 0 then
self.scene = 26
return
end
self.counter = self.counter - 1
end
-- IntroScene26 (engine/movie/intro.asm:1056-1103).
Scenes[26] = function(self)
setup(self, true, { 64, 128, 64 }, function()
loadAct(self, "crystalUnowns")
clearAnims(self)
self.scx, self.scy = 0, 0
self.counter, self.timer = 0, 0
end)
end
-- IntroScene27 / Intro_FadeUnownWordPals
-- (engine/movie/intro.asm:1105-1128, :1390-1455).
Scenes[27] = function(self)
local a = self.counter
self.counter = (a + 1) % 256
if a >= 0x80 then
self.scene = 28
self.counter = 0x80
return
end
local t = a % 0x10
local pal = math.floor(a % 0x80 / 0x10)
local fade = fades(self)
local target = self.bgPals[pal + 1]
if fade.wordFast and fade.wordFast[t + 1] then
target[3] = { unpack(fade.wordFast[t + 1]) }
end
if fade.wordSlow and fade.wordSlow[t + 1] then
target[4] = { unpack(fade.wordSlow[t + 1]) }
end
markDirty(self)
end
-- IntroScene28 (engine/movie/intro.asm:1130-1153).
Scenes[28] = function(self)
local a = self.counter
if a == 0 then
self.done = true
return
end
self.counter = a - 1
if a == 0x18 then
self.bgPals = flatPals(WHITE)
self.obPals = flatPals(WHITE)
markDirty(self)
elseif a == 0x8 then
self:playSfx("Sfx_IntroWhoosh")
end
end
--------------------------------------------------------------------------
-- Frame loop
--------------------------------------------------------------------------
-- CrystalIntro's .loop: the scene function, then PlaySpriteAnimations, then
-- DelayFrame (engine/movie/intro.asm:11-22); in-scene DelayFrames hold the
-- sprites still, so a pending `hold` skips the anim step.
function CrystalIntro:step()
if self.done then return true end
self.frames = self.frames + 1
if self.hold > 0 then
self.hold = self.hold - 1
return false
end
local scene = Scenes[self.scene]
if scene then scene(self) end
if self.done then return true end
if self.hold > 0 then return false end
self.anims.timer = self.timer
self.anims.counter = self.counter
self.anims:playFrame()
return self.done
end
function CrystalIntro:enter()
if Runtime.wants("intro.boot.movie") then
Runtime.emit("intro.boot.movie", { screen = self, game = self.game })
end
end
function CrystalIntro:finish()
if self.finished then return end
self.finished = true
self.done = true
if Runtime.wants("intro.boot.movie_ended") then
Runtime.emit("intro.boot.movie_ended", {
screen = self, game = self.game,
skipped = self.skipped and true or false,
frames = self.frames,
})
end
if self.onDone then self.onDone() end
end
-- .ShutOffMusic (engine/movie/intro.asm:24-26).
function CrystalIntro:skip()
self.skipped = true
Music.stop()
self:finish()
end
function CrystalIntro:update(_dt)
if self.finished then return end
local input = self.game and self.game.input
if input then
for _, button in ipairs({ "a", "b", "start", "select" }) do
if input:wasPressed(button) then
self:skip()
return
end
end
end
if self:step() then self:finish() end
end
--------------------------------------------------------------------------
-- Drawing
--------------------------------------------------------------------------
function CrystalIntro:image(path)
if not path then return nil end
local cached = self.images[path]
if cached == nil then
local ok, img = pcall(Assets.image, path)
cached = ok and img or false
if cached then
cached:setFilter("nearest", "nearest")
end
self.images[path] = cached
end
return cached or nil
end
function CrystalIntro:sheet(path)
if not path then return nil end
local entry = self.sheets[path]
if entry ~= nil then return entry or nil end
local image = self:image(path)
if not image then
self.sheets[path] = false
return nil
end
local width, height = image:getDimensions()
local quads = {}
for tile = 0, math.floor(width / 8) * math.floor(height / 8) - 1 do
quads[tile] = love.graphics.newQuad(
tile % 16 * 8, math.floor(tile / 16) * 8, 8, 8, width, height)
end
entry = { image = image, quads = quads }
self.sheets[path] = entry
return entry
end
-- The three layer canvases the CGB attribute byte splits the map into:
-- per-tile colour 0 (the backdrop), colours 1-3 of ordinary tiles, and
-- colours 1-3 of PRIORITY tiles, which beat every OBJ.
local function bakeLayers(self)
if not (self.mapDirty or self.palsDirty) then
return self.backdropCanvas ~= nil
end
local G = love.graphics
if not self.backdropCanvas then
local ok, a = pcall(G.newCanvas, BG_PIXELS, BG_PIXELS)
local ok2, b = pcall(G.newCanvas, BG_PIXELS, BG_PIXELS)
local ok3, c = pcall(G.newCanvas, BG_PIXELS, BG_PIXELS)
if not (ok and ok2 and ok3) then return false end
for _, canvas in ipairs({ a, b, c }) do
canvas:setFilter("nearest", "nearest")
end
self.backdropCanvas, self.lowCanvas, self.highCanvas = a, b, c
end
local act = actData(self)
local sheet = act and self:sheet(act.tiles)
local grass = self.grassFrame
and self:sheet(self.assets and self.assets.grassFrames)
local previous = G.getCanvas()
G.push()
G.origin()
G.setCanvas(self.backdropCanvas)
G.clear(0, 0, 0, 1)
for pal = 0, 7 do
local color = GbcPalette.color(self.bgPals[pal + 1], 1) or BLACK
G.setColor(color[1] / 255, color[2] / 255, color[3] / 255, 1)
for row = 0, BG_TILES - 1 do
for col = 0, BG_TILES - 1 do
if self.attr[row * BG_TILES + col + 1] % 8 == pal then
G.rectangle("fill", col * 8, row * 8, 8, 8)
end
end
end
end
G.setColor(1, 1, 1, 1)
local shader = GbcPalette.available()
for _, layer in ipairs({
{ canvas = self.lowCanvas, prio = false },
{ canvas = self.highCanvas, prio = true },
}) do
G.setCanvas(layer.canvas)
G.clear(0, 0, 0, 0)
if sheet then
for pal = 0, 7 do
if shader then GbcPalette.use(self.bgPals[pal + 1]) end
for row = 0, BG_TILES - 1 do
for col = 0, BG_TILES - 1 do
local index = row * BG_TILES + col + 1
local attr = self.attr[index]
if attr % 8 == pal and (attr >= 0x80) == layer.prio then
local id = self.map[index]
local quad
local image = sheet.image
if grass and id >= 0x09 and id <= 0x0c then
quad = grass.quads[(self.grassFrame - 1) * 4 + id - 0x09]
image = grass.image
else
quad = sheet.quads[id]
end
if quad then G.draw(image, quad, col * 8, row * 8) end
end
end
end
end
if shader then GbcPalette.clear() end
end
end
G.setCanvas(previous)
G.pop()
self.mapDirty, self.palsDirty = false, false
return true
end
-- Present a layer at (hSCX, hSCY); with the perspective bands live each
-- scanline takes the tree or grass SCX (engine/movie/intro.asm:1647-1676).
function CrystalIntro:drawLayer(canvas)
local G = love.graphics
G.setColor(1, 1, 1, 1)
if not self.lyActive then
local scx, scy = self.scx % BG_PIXELS, self.scy % BG_PIXELS
for _, ox in ipairs({ 0, BG_PIXELS }) do
for _, oy in ipairs({ 0, BG_PIXELS }) do
G.draw(canvas, -scx + ox, -scy + oy)
end
end
return
end
self.lineQuad = self.lineQuad
or love.graphics.newQuad(0, 0, BG_PIXELS, 1, BG_PIXELS, BG_PIXELS)
for line = 0, SCREEN_H - 1 do
local srcY = (self.scy + line) % BG_PIXELS
local scx = (line < 0x5f and self.treeScroll or self.grassScroll)
% BG_PIXELS
self.lineQuad:setViewport(0, srcY, BG_PIXELS, 1, BG_PIXELS, BG_PIXELS)
G.draw(canvas, self.lineQuad, -scx, line)
G.draw(canvas, self.lineQuad, -scx + BG_PIXELS, line)
end
end
function CrystalIntro:drawObjects(priority)
local act = actData(self)
local sheet0 = act and self:sheet(act.sprites)
local sheet1 = act and act.sprites1 and self:sheet(act.sprites1)
if not (sheet0 or sheet1) then return end
local G = love.graphics
local oam = self.anims.oam
local shader = GbcPalette.available()
local current = nil
for index = #oam, 1, -1 do
local entry = oam[index]
local behind = bit.band(entry.attr, OAM_PRIO) ~= 0
if behind == priority then
local sheet = bit.band(entry.attr, OAM_BANK1) ~= 0 and sheet1 or sheet0
local quad = sheet and sheet.quads[entry.tile]
if quad then
local slot = entry.attr % 8
if shader and current ~= slot then
GbcPalette.use(self.obPals[slot + 1])
current = slot
end
local flipX = bit.band(entry.attr, OAM_XFLIP) ~= 0
local flipY = bit.band(entry.attr, OAM_YFLIP) ~= 0
G.setColor(1, 1, 1, 1)
G.draw(sheet.image, quad,
entry.x - 8 + (flipX and 8 or 0), entry.y - 16 + (flipY and 8 or 0),
0, flipX and -1 or 1, flipY and -1 or 1)
end
end
end
if current then GbcPalette.clear() end
end
function CrystalIntro:renderFrame()
local G = love.graphics
local baked = bakeLayers(self)
if not self.frameCanvas then
local ok, canvas = pcall(G.newCanvas, SCREEN_W, SCREEN_H)
if not ok then return nil end
canvas:setFilter("nearest", "nearest")
self.frameCanvas = canvas
end
local previous = G.getCanvas()
G.push()
G.origin()
G.setCanvas(self.frameCanvas)
G.clear(0, 0, 0, 1)
if baked then
self:drawLayer(self.backdropCanvas)
self:drawObjects(true)
self:drawLayer(self.lowCanvas)
self:drawObjects(false)
self:drawLayer(self.highCanvas)
end
G.setCanvas(previous)
G.pop()
return self.frameCanvas
end
function CrystalIntro:drawPanel()
local G = love.graphics
local canvas = self:renderFrame()
G.setColor(1, 1, 1, 1)
if canvas then
G.draw(canvas, 0, 0)
return
end
G.setColor(0, 0, 0, 1)
G.rectangle("fill", 0, 0, SCREEN_W, SCREEN_H)
G.setColor(1, 1, 1, 1)
end
function CrystalIntro:draw()
self:drawPanel()
end
-- The widescreen surround follows the backdrop: colour 0 of the palette the
-- top-left visible tile's attribute selects, live through the fades.
function CrystalIntro:surroundColor()
local row = math.floor(self.scy / 8) % BG_TILES
local col = math.floor(self.scx / 8) % BG_TILES
local attr = self.attr[row * BG_TILES + col + 1] or 0
return GbcPalette.color(self.bgPals[attr % 8 + 1], 1) or BLACK
end
function CrystalIntro:drawWidescreen(winW, winH)
local G = love.graphics
local fill = self:surroundColor()
G.setColor(fill[1] / 255, fill[2] / 255, fill[3] / 255, 1)
G.rectangle("fill", 0, 0, winW, winH)
local scale = Chrome.fitScale(winW, winH)
G.push()
G.translate(Chrome.fitOrigin(winW, winH, scale))
G.scale(scale, scale)
self:drawPanel()
G.pop()
end
CrystalIntro.Scenes = Scenes
CrystalIntro.OAMSETS = OAMSETS
CrystalIntro.FRAMESETS = FRAMESETS
CrystalIntro.OBJECTS = OBJECTS
CrystalIntro.SEQUENCES = SEQUENCES
CrystalIntro.unownFade = unownFade
return CrystalIntro