mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-26 07:21:22 +02:00
1296 lines
39 KiB
Lua
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
|