-- 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