-- The GAME FREAK splash (pokegold engine/movie/splash.asm). -- -- This is the six-scene jumptable GameFreakPresentsScene walks, not a hold on -- a logo image: a star spirals in from the edge of the screen, the logo is -- placed where the star died, sparkles fly off it for 128 frames while the -- words "GAME FREAK" appear halfway through, then "presents", then a last -- 128-frame hold. Any button skips the whole thing. -- -- scene 0 Star spawn the star, play SFX_GAME_FREAK_LOGO_GS -- scene 1 PlaceLogo wait for the star to die, then place the logo -- scene 2 LogoSparkles 128 frames of sparkles; "GAME FREAK" at 63 -- scene 3 PlacePresents "presents", reset the timer -- scene 4 WaitForTimer 128 frames -- scene 5 SetDoneFlag finished -- -- GRAPHICS. GameFreakLogoGFX is two INCBINs run together and the extractor -- splits them (RomExtractorGen2:splashGfx): a 13-tile letter strip at VRAM -- $80, the 3x5 logo at $8d, the star at $9c and three sparkle frames at $9e. -- GameFreakPresentsInit points wSpriteAnimDict at $8d, so every OAM set's -- vtile offset is relative to the logo -- which is why the system's -- vtileBase is set here and nowhere else. -- -- COLOUR. _CGB_GamefreakLogo loads PREDEFPAL_GAMEFREAK_LOGO_OB into OBJ -- palettes 0 AND 1 (white, white, yellow, yellow) and the BG palette runs -- black to white. The two OBJ palettes differ only in the DMG byte indexing -- them: OBP0 stays %11111000 (star and sparkles are yellow) while OBP1 starts -- at %00100100 and GameFreakPresents_UpdateLogoPal rotates it right two bits -- every 16 frames until it reaches %10010000 -- so the logo is white for the -- first 48 frames and yellow after that. local Chrome = require("src.ui.gen2.Chrome") local GbcPalette = require("src.render.GbcPalette") 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 TileSheet = require("src.ui.gen2.TileSheet") local GameFreakPresents = {} GameFreakPresents.__index = GameFreakPresents GameFreakPresents.isOpaque = true local SCREEN_W, SCREEN_H = 160, 144 -- wSpriteAnimDict[SPRITE_ANIM_DICT_GS_SPLASH] = $8d (GameFreakPresentsInit). local DICT_VTILE = 0x8d -- `depixel 10, 11, 4, 0` and `depixel 11, 11`: the macro is -- (x tile, y tile, x pixel, y pixel) and lands in de as x, y. local LOGO_X, LOGO_Y = 10 * 8 + 4, 11 * 8 + 0 local SPARKLE_X, SPARKLE_Y = 11 * 8, 11 * 8 -- GameFreakPresents_PlaceGameFreak / _PlacePresents. $8d is the logo's own -- first tile borrowed as a blank, which is why "GAME FREAK" reads as ten -- tiles for nine letters and a space. local GAME_FREAK = { 0x80, 0x81, 0x82, 0x83, 0x8d, 0x84, 0x85, 0x83, 0x81, 0x86 } local GAME_FREAK_X, GAME_FREAK_Y = 5, 12 local PRESENTS = { 0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c } local PRESENTS_X, PRESENTS_Y = 7, 13 -- GameFreakPresents_Sparkle .sparkle_vectors: angle (6 bits), distance. local SPARKLE_VECTORS = { { 0x00, 0x03 }, { 0x08, 0x04 }, { 0x04, 0x03 }, { 0x0c, 0x02 }, { 0x10, 0x02 }, { 0x18, 0x03 }, { 0x14, 0x04 }, { 0x1c, 0x03 }, { 0x20, 0x02 }, { 0x28, 0x02 }, { 0x24, 0x03 }, { 0x2c, 0x04 }, { 0x30, 0x04 }, { 0x38, 0x03 }, { 0x34, 0x02 }, { 0x3c, 0x04 }, } local SCENE_TIMER = 128 -- The `ld c, 16 / call DelayFrames` every exit path runs before returning. local EXIT_FRAMES = 16 -- DmgToCgbObjPals / DmgToCgbObjPal1: the DMG palette byte is four 2-bit -- indices into the real four-colour palette, colour 0 in the low bits. local OBP0 = 0xf8 -- %11111000, the star and the sparkles local OBP1_START = 0x24 -- %00100100, the logo before the rotation runs local OBP1_FINAL = 0x90 -- %10010000, where UpdateLogoPal stops local BLACK = { 0, 0, 0 } local WHITE = { 255, 255, 255 } local YELLOW = { 206, 247, 0 } -- RGB 25,30,00 out of gfx/sgb/predef.pal local DEFAULT_OB = { WHITE, WHITE, YELLOW, YELLOW } local DEFAULT_BG = { BLACK, { 66, 90, 90 }, { 173, 173, 173 }, WHITE } local function permute(colors, byte) local out = {} for index = 0, 3 do out[index + 1] = colors[math.floor(byte / 4 ^ index) % 4 + 1] or BLACK end return out end function GameFreakPresents:wantsFillScale() return true end function GameFreakPresents:drawsWidescreen() return true end -- opts: oakSpeech (data/generated/oak_speech.lua, for its `splash` table), -- onDone function GameFreakPresents.new(game, opts) opts = opts or {} local self = setmetatable({}, GameFreakPresents) self.game = game self.onDone = opts.onDone local splash = (opts.oakSpeech or {}).splash or {} self.obColors = splash.obPalette or DEFAULT_OB self.bgColors = splash.bgPalette or DEFAULT_BG -- One sheet per INCBIN, each addressed by the VRAM id its tiles were -- loaded at, so an OAM entry's tile id picks its own sheet. self.sheets = { TileSheet.new({ path = splash.presents or "assets/generated/splash/presents.png", wide = 13, firstTile = 0x80 }), TileSheet.new({ path = splash.logo or "assets/generated/splash/logo.png", wide = 3, firstTile = 0x8d }), TileSheet.new({ path = splash.star or "assets/generated/splash/star.png", wide = 1, firstTile = 0x9c }), TileSheet.new({ path = splash.sparkle or "assets/generated/splash/sparkle.png", wide = 3, firstTile = 0x9e }), } self.anims = SpriteAnims.new() self.anims.vtileBase = DICT_VTILE self.scene = 0 self.timer = 0 self.obp1 = OBP1_START self.tiles = {} -- the BG tilemap, sparse: [y][x] = tile id self.frames = 0 -- exitTail, not `exit`: enter/exit are the StateStack's own callback names -- and a state is an ordinary table, so a field there is a call the stack -- would make on a pop. self.exitTail = nil self.done = false self.sfxPlayed = false return self end function GameFreakPresents:enter() local data = self.game and self.game.data if data and data.audio and data.audio.runtime then Music.stop() end -- intro.boot.gamefreak: scene 0 of GameFreakPresentsScene is about to run, -- i.e. the splash is up and its star has not been spawned yet. Gen 2 only: -- Red has no splash.asm counterpart, so this is a new name rather than a -- borrowed one (see CopyrightSplash for the set). It also marks the end of -- the copyright card, which is why that card has no `ended` name. if Runtime.wants("intro.boot.gamefreak") then Runtime.emit("intro.boot.gamefreak", { screen = self, game = self.game }) end end function GameFreakPresents:finish() if self.done then return end self.done = true if self.onDone then self.onDone() end end -- PlaceString into the sparse tilemap. function GameFreakPresents:placeString(tiles, tx, ty) local row = self.tiles[ty] if not row then row = {} self.tiles[ty] = row end for index, tile in ipairs(tiles) do row[tx + index - 1] = tile end end -------------------------------------------------------------------------- -- GameFreakPresentsScene -------------------------------------------------------------------------- function GameFreakPresents:sceneStar() self.anims.flag = 0 -- wIntroSceneFrameCounter local st = self.anims:init("GS_GAMEFREAK_LOGO_STAR", LOGO_X, LOGO_Y) if st then st.var1 = 0x80 end local data = self.game and self.game.data if data and data.audio and data.audio.sfx and data.audio.sfx.Sfx_GameFreakLogoGs then Sound.play(data, "Sfx_GameFreakLogoGs") end self.scene = self.scene + 1 end function GameFreakPresents:scenePlaceLogo() -- The star's own sequence sets the flag when it reaches the middle. if self.anims.flag == 0 then return end self.anims:init("GAMEFREAK_LOGO", LOGO_X, LOGO_Y) -- UpdateLogoPal is called out of the LOGO's own AnimSeq, so its clock only -- runs once the logo exists. Without this the rotation would fire on every -- frame of the star scene (the timer is still 0 there, and 0 % 16 == 0) and -- the logo would be yellow before it was ever placed. self.logoAlive = true self.scene = self.scene + 1 self.timer = SCENE_TIMER end -- GameFreakPresents_Sparkle: one new sparkle on every second frame, its -- direction taken from the low four bits of half the timer. function GameFreakPresents:sparkle(counter) if counter % 2 ~= 0 then return end local st = self.anims:init("GS_GAMEFREAK_LOGO_SPARKLE", SPARKLE_X, SPARKLE_Y) if not st then return end -- all ten structs busy, as on hardware local vector = SPARKLE_VECTORS[math.floor(counter / 2) % 16 + 1] st.jt = vector[1] st.var1 = 0 st.var2 = vector[2] end function GameFreakPresents:sceneLogoSparkles() local counter = self.timer if counter == 0 then self.timer = SCENE_TIMER self.scene = self.scene + 1 return end self.timer = self.timer - 1 if counter == 63 then self:placeString(GAME_FREAK, GAME_FREAK_X, GAME_FREAK_Y) end self:sparkle(counter) end function GameFreakPresents:scenePlacePresents() self:placeString(PRESENTS, PRESENTS_X, PRESENTS_Y) self.scene = self.scene + 1 self.timer = SCENE_TIMER end function GameFreakPresents:sceneWaitForTimer() if self.timer == 0 then self.scene = self.scene + 1 return end self.timer = self.timer - 1 end local SCENES = { GameFreakPresents.sceneStar, GameFreakPresents.scenePlaceLogo, GameFreakPresents.sceneLogoSparkles, GameFreakPresents.scenePlacePresents, GameFreakPresents.sceneWaitForTimer, -- GameFreakPresents_SetDoneFlag. function(self) self:beginExit() end, } -- The tail both exit paths share: ClearSpriteAnims, ClearTilemap and -- ClearSprites, then `ld c, 16 / call DelayFrames` on an empty screen. function GameFreakPresents:beginExit() if self.exitTail then return end self.anims:clear() self.tiles = {} self.logoAlive = false self.exitTail = 0 end -- GameFreakPresents_UpdateLogoPal, called out of the logo's AnimSeq: rotate -- OBP1 right by one colour slot every 16 frames until it reaches its final -- state, then leave it alone. function GameFreakPresents:updateLogoPal() if not self.logoAlive then return end if self.obp1 == OBP1_FINAL then return end if self.timer % 16 ~= 0 then return end local low = self.obp1 % 4 self.obp1 = math.floor(self.obp1 / 4) + low * 64 end function GameFreakPresents:update(_dt) self.frames = self.frames + 1 -- The 16-frame tail after the sequence finishes or is skipped. if self.exitTail then self.exitTail = self.exitTail + 1 if self.exitTail > EXIT_FRAMES then self:finish() end return end local input = self.game and self.game.input if input and (input:wasPressed("a") or input:wasPressed("b") or input:wasPressed("start") or input:wasPressed("select")) then -- .pressed_button: everything is torn down and the splash is over. self:beginExit() return end self.anims:playFrame() self:updateLogoPal() local scene = SCENES[self.scene + 1] if scene then scene(self) end end -------------------------------------------------------------------------- -- Drawing -------------------------------------------------------------------------- function GameFreakPresents:sheetFor(tile) for _, sheet in ipairs(self.sheets) do if tile >= sheet.firstTile and sheet:available() then local index = tile - sheet.firstTile local image = sheet:image() local _, height = image:getDimensions() if index < sheet.wide * (height / 8) then return sheet end end end return nil end function GameFreakPresents:drawTile(tile, tx, ty, colors) local sheet = self:sheetFor(tile) if not sheet then return end sheet.palette = colors sheet:draw(tile, tx, ty) end -- One pass over wShadowOAM. The palette is the OAM attribute's low bits: -- slot 1 is the logo, on the OBP1 that rotates, and everything else is the -- fixed OBP0 the star and sparkles share. function GameFreakPresents:drawObjects() local G = love.graphics local logoPal = permute(self.obColors, self.obp1) local objPal = permute(self.obColors, OBP0) local oam = self.anims.oam for index = #oam, 1, -1 do local entry = oam[index] -- OBJ palette 1 is the logo and nothing else. local isLogo = entry.attr % 8 == 1 local sheet = self:sheetFor(entry.tile) local quad = sheet and sheet:quad(entry.tile - sheet.firstTile) if quad then local flipX = math.floor(entry.attr / SpriteAnims.OAM_XFLIP) % 2 == 1 local flipY = math.floor(entry.attr / SpriteAnims.OAM_YFLIP) % 2 == 1 local colors = isLogo and logoPal or objPal local function body() 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 if GbcPalette.available() then GbcPalette.with(colors, body) else body() end end end end function GameFreakPresents:drawPanel() local G = love.graphics -- BG colour 0 is the backdrop the cleared tilemap shows. local backdrop = GbcPalette.color(self.bgColors, 1) or BLACK G.setColor(backdrop[1] / 255, backdrop[2] / 255, backdrop[3] / 255, 1) G.rectangle("fill", 0, 0, SCREEN_W, SCREEN_H) G.setColor(1, 1, 1, 1) for ty, row in pairs(self.tiles) do for tx, tile in pairs(row) do self:drawTile(tile, tx, ty, self.bgColors) end end self:drawObjects() G.setColor(1, 1, 1, 1) end function GameFreakPresents:draw() self:drawPanel() end function GameFreakPresents:drawWidescreen(winW, winH) local G = love.graphics local backdrop = GbcPalette.color(self.bgColors, 1) or BLACK G.setColor(backdrop[1] / 255, backdrop[2] / 255, backdrop[3] / 255, 1) G.rectangle("fill", 0, 0, winW, winH) local scale = Chrome.fitScale(winW, winH) G.push() G.translate(math.floor((winW - SCREEN_W * scale) / 2), math.floor((winH - SCREEN_H * scale) / 2)) G.scale(scale, scale) self:drawPanel() G.pop() end GameFreakPresents.SPARKLE_VECTORS = SPARKLE_VECTORS GameFreakPresents.permute = permute return GameFreakPresents