-- pokeyellow engine/pokemon/evos_moves.asm:366 -- pokeyellow data/pikachu/pikachu_pic_animation.asm:281 -- pokeyellow engine/pikachu/pikachu_pic_animation.asm:790 return function(game) local U = dofile("tests/drivers/util.lua") local GameVersion = require("src.core.GameVersion") local Pokemon = require("src.pokemon.Pokemon") local BattleState = require("src.battle.BattleState") local PikachuFollower = require("src.world.PikachuFollower") local PaletteFX = require("src.render.PaletteFX") local Sound = require("src.core.Sound") local Music = require("src.core.Music") local SHOT_DIR = os.getenv("POKEPORT_SHOT_DIR") or os.getenv("SHOT_DIR") or "/tmp/shots" local OPPOSITE = { up = "down", down = "up", left = "right", right = "left" } local failures = 0 local function check(label, ok) U.log(ok and "PASS" or "FAIL", label) if not ok then failures = failures + 1 end return ok end local startMode = PaletteFX.mode local function quit() if PaletteFX.mode ~= startMode then PaletteFX.setMode(startMode) end U.log(failures == 0 and "ALL PASS" or ("DONE " .. failures .. " check(s) failed")) love.event.quit(failures == 0 and 0 or 1) while true do coroutine.yield() end end if not check("2347_yellow_cache", GameVersion.isYellow()) then quit() end if PaletteFX.usesGbcPack() then PaletteFX.setMode("gbc") end local moveSounds, ducks = {}, 0 local realPlayMove, realDuck = Sound.playMove, Music.duckForFanfare Sound.playMove = function(data, anim) moveSounds[#moveSounds + 1] = anim and anim.sound or "?" return realPlayMove(data, anim) end Music.duckForFanfare = function(src) ducks = ducks + 1 return realDuck(src) end game.save.player.name = "bryan" local pika = Pokemon.new(game.data, "PIKACHU", 30) BattleState.stampOT(game.save, pika) pika.moves = { { id = "THUNDERSHOCK", pp = 30 }, { id = "GROWL", pp = 40 }, { id = "THUNDER_WAVE", pp = 20 }, { id = "QUICK_ATTACK", pp = 30 }, } game.save.party = { pika } game.save.flags = game.save.flags or {} game.save.flags.EVENT_GOT_STARTER = true game.save.flags.EVENT_BATTLED_RIVAL_IN_OAKS_LAB = true game.save.pikachuInBall = false game.save.pikachuHappiness = 120 game.save.pikachuMood = 128 game.save.pikachuEmotionModifier = nil game.save.flashLit = nil game.save.repelSteps = 250 U.teleport(game, "ROCK_TUNNEL_1F", 19, 6, "right") U.wait(20) local ow = game.overworld check("2347_rock_tunnel_dark", ow.dark == true) local function follower() for _, n in ipairs(ow.npcs or {}) do if n.pikachuFollower then return n end end return nil end local npc = follower() if not check("2347_follower_spawned", npc ~= nil) then quit() end local DIRS = { { "up", 0, -1 }, { "left", -1, 0 }, { "right", 1, 0 }, { "down", 0, 1 } } local function freeDir() for _, d in ipairs({ { "left", -1, 0 }, { "right", 1, 0 }, { "up", 0, -1 }, { "down", 0, 1 } }) do local cx, cy = ow.player.cellX + d[2], ow.player.cellY + d[3] if ow.map:inBounds(cx, cy) and ow.map:isWalkableCell(cx, cy) and not ow:npcAtCell(cx, cy) then return d[1] end end return nil end local function walk(dir) local sx, sy = ow.player.cellX, ow.player.cellY for _ = 1, 60 do U.hold(game, dir, 1) if ow.player.cellX ~= sx or ow.player.cellY ~= sy then break end end for _ = 1, 40 do if not ow.player.moving then break end U.wait(1) end U.wait(4) return ow.player.cellX ~= sx or ow.player.cellY ~= sy end if not check("2347_step_room", freeDir() ~= nil) then quit() end local function stepAndBack() local dir = freeDir() if not dir then return false end local out = walk(dir) local back = walk(OPPOSITE[dir]) return out and back end local function faceFollower() for _, d in ipairs(DIRS) do if npc.cellX == ow.player.cellX + d[2] and npc.cellY == ow.player.cellY + d[3] then if ow.player.facing ~= d[1] then U.tap(game, d[1]) U.wait(6) end break end end local fx, fy = ow.player:facingCell() return ow:npcAtCell(fx, fy) == npc end stepAndBack() U.shot(game, SHOT_DIR .. "/2347_01_dark_before.png") pika.moves[4] = { id = "THUNDERBOLT", pp = 15 } PikachuFollower.onMoveLearned(game.save, pika, "THUNDERBOLT") check("2347_learn_arms_modifier", game.save.pikachuEmotionModifier == 5 and game.save.pikachuMood == 0x85) local function startTalk(tag) if not check(tag .. "_facing_follower", faceFollower()) then return false end for _ = 1, 5 do U.tap(game, "a") for _ = 1, 60 do if ow.emote then break end U.wait(1) end if ow.emote then break end end return ow.emote ~= nil end local function isBoltMap(m) return m and m[0] == 0 and m[1] == 0 and m[2] == 0 and m[3] == 3 end local function isDarkMap(m) return m and m[0] == 2 and m[1] == 3 and m[2] == 3 and m[3] == 3 end moveSounds, ducks = {}, 0 if not check("2347_talk1_emote", startTalk("2347_talk1")) then quit() end check("2347_talk1_bolt_bubble", ow.emote.bubble and not ow.emote.pikaPic and true or false) for _ = 1, 200 do if ow.emote and ow.emote.pikaPic then break end U.wait(1) end local pic = ow.emote and ow.emote.pikaPic or "" check("2347_talk1_pikapic_25", pic:find("pikapic_25", 1, true) ~= nil) local emote = ow.emote check("2347_talk1_bolt_spec", emote and emote.boltAt == 45) local bgpSeq, poseSeq, doneSeq, frames = {}, {}, {}, 0 local prebolt = 0 local function poseName(e) local path, lift = PikachuFollower.picFrame(e) if (lift or 0) ~= 0 then return "lifted" end return path and path:match("([^/]+)%.png$") or false end local PROBE = (os.getenv("TMPDIR") or "/tmp/") .. "2347_probe.png" local function drawnShade() U.still(game, PROBE) os.remove(PROBE) return PaletteFX.shadeMap() end local darkDuringPre, whiteDrawn, litTail while ow.emote == emote and frames < 1500 do U.wait(1) frames = frames + 1 if ow.emote ~= emote then break end bgpSeq[#bgpSeq + 1] = emote.bgp or false poseSeq[#bgpSeq] = poseName(emote) doneSeq[#bgpSeq] = emote.boltDone and true or false if not emote.bgp then prebolt = prebolt + 1 end local k = (emote.boltT or 0) - emote.boltAt - 2 if darkDuringPre == nil and not emote.bgp and frames > 10 then darkDuringPre = isDarkMap(drawnShade()) elseif whiteDrawn == nil and emote.bgp == 0xC0 then whiteDrawn = isBoltMap(drawnShade()) elseif litTail == nil and k >= 81 and not emote.boltDone then litTail = drawnShade() == nil end end local strobeOk, seen = true, 0 local first for i, b in ipairs(bgpSeq) do if b then first = first or i end end if first then for row = 1, 20 do local want = (row % 2 == 1) and 0xC0 or 0xE4 for f = 1, 4 do local i = first + (row - 1) * 4 + f - 1 if bgpSeq[i] ~= want then strobeOk = false end seen = seen + 1 end end end U.log("prebolt frames", prebolt, "strobe start", tostring(first), "total", frames) check("2347_talk1_strobe_alternates_4f", first ~= nil and strobeOk and seen == 80) check("2347_talk1_prebolt_dark", darkDuringPre == true) check("2347_talk1_strobe_white_drawn", whiteDrawn == true) check("2347_talk1_lit_tail_while_box_up", litTail == true) local boltPose, sawE7863, blank = first ~= nil, false, 0 for i = 1, #poseSeq do if poseSeq[i] == "gfx_e7863" then sawE7863 = true end if first and i >= first then if poseSeq[i] == false then blank = blank + 1 elseif blank > 0 or poseSeq[i] ~= "gfx_e79f3" then boltPose = false end end end U.log("pose at strobe start", tostring(first and poseSeq[first]), "blank close frames", blank) check("2347_talk1_e7863_before_bolt", sawE7863) check("2347_talk1_bolt_pose", boltPose) local closeSeq = {} for i = 1, #poseSeq do if doneSeq[i] then closeSeq[#closeSeq + 1] = tostring(poseSeq[i]) end end U.log("close frames", table.concat(closeSeq, ",")) check("2347_talk1_empty_box_at_close", table.concat(closeSeq, ",") == "gfx_e79f3,gfx_e79f3,gfx_e79f3,false,false,false") check("2347_talk1_thunderbolt_sound", moveSounds[1] == "Battle_2F") check("2347_talk1_music_muted", ducks == 1) U.wait(6) check("2347_talk1_dark_after_close", ow.emote == nil and isDarkMap(drawnShade())) check("2347_modifier_survives_talk", game.save.pikachuEmotionModifier == 5) if not check("2347_talk2_emote", startTalk("2347_talk2")) then quit() end U.still(game, SHOT_DIR .. "/2347_02_bolt_bubble.png") for _ = 1, 200 do if ow.emote and ow.emote.pikaPic then break end U.wait(1) end emote = ow.emote check("2347_talk2_replays_bolt", emote and emote.boltAt ~= nil) U.wait(20) U.still(game, SHOT_DIR .. "/2347_03_pikapic_prebolt.png") for _ = 1, 200 do if emote.bgp == 0xC0 then break end U.wait(1) end local function drawn() return tostring(ow.pikaPicDrawn):match("([^/]+)%.png$") end U.still(game, SHOT_DIR .. "/2347_04_strobe_white.png") check("2347_talk2_c0_draws_e79f3", emote.bgp == 0xC0 and drawn() == "gfx_e79f3") for _ = 1, 200 do if emote.bgp == 0xE4 then break end U.wait(1) end U.still(game, SHOT_DIR .. "/2347_05_strobe_lit.png") check("2347_talk2_e4_draws_e79f3", emote.bgp == 0xE4 and drawn() == "gfx_e79f3") for _ = 1, 400 do if (emote.boltT or 0) - emote.boltAt > 2 + 80 or ow.emote ~= emote then break end U.wait(1) end if ow.emote == emote and not emote.boltDone then U.still(game, SHOT_DIR .. "/2347_06_lit_tail.png") check("2347_talk2_tail_draws_e79f3", drawn() == "gfx_e79f3") end for _ = 1, 1500 do if ow.emote ~= emote then break end U.wait(1) end U.wait(10) U.still(game, SHOT_DIR .. "/2347_07_after_close_dark.png") for _ = 1, 3 do stepAndBack() U.log("step", ow.player.cellX, ow.player.cellY, "mood", tostring(game.save.pikachuMood), "modifier", tostring(game.save.pikachuEmotionModifier)) end check("2347_modifier_clears_after_steps", game.save.pikachuEmotionModifier == nil and game.save.pikachuMood == 128) moveSounds = {} if not check("2347_talk3_emote", startTalk("2347_talk3")) then quit() end for _ = 1, 200 do if ow.emote and ow.emote.pikaPic then break end U.wait(1) end U.wait(20) U.still(game, SHOT_DIR .. "/2347_08_after_steps_mood_talk.png") check("2347_talk3_no_bolt", ow.emote and ow.emote.pikaPic and not ow.emote.boltAt and true or false) for _ = 1, 600 do if not ow.emote then break end U.wait(1) end check("2347_talk3_no_sound", #moveSounds == 0) PaletteFX.setMode("redpp") U.wait(10) ow = game.overworld npc = follower() or npc stepAndBack() if ow:bakedWorldColors() then PikachuFollower.onMoveLearned(game.save, pika, "THUNDERBOLT") if startTalk("2347_baked") then for _ = 1, 200 do if ow.emote and ow.emote.pikaPic then break end U.wait(1) end emote = ow.emote local tiles = ow.map.renderer local darkAtlas = tiles.image local function atlasFor(byte) local imgs = tiles.bgpImages or {} return imgs[byte] and tiles.image == imgs[byte] and tiles.curBgp == byte and tiles.baseImage == darkAtlas end local shotC0, shotE4 = false, false local litOnE4, c0Atlas, veilOnC0 = true, true, false for _ = 1, 1500 do if ow.emote ~= emote then break end U.wait(1) if ow.emote ~= emote then break end if emote.bgp == 0xC0 and not shotC0 then shotC0 = true U.still(game, SHOT_DIR .. "/2347_09_baked_strobe_white.png") local v = game.renderer and game.renderer.screenVeil if v then veilOnC0 = true end if not atlasFor(0xC0) then c0Atlas = false end check("2347_baked_c0_draws_e79f3", drawn() == "gfx_e79f3") elseif emote.bgp == 0xE4 and shotC0 and not shotE4 then shotE4 = true U.still(game, SHOT_DIR .. "/2347_10_baked_strobe_lit.png") if not atlasFor(0xE4) then litOnE4 = false end check("2347_baked_e4_draws_e79f3", drawn() == "gfx_e79f3") end end check("2347_baked_strobe_seen", shotC0 and shotE4) check("2347_baked_lit_atlas", shotE4 and litOnE4) check("2347_baked_c0_atlas", shotC0 and c0Atlas) check("2347_baked_no_white_veil", shotC0 and not veilOnC0) U.wait(10) U.still(game, SHOT_DIR .. "/2347_11_baked_after_close_dark.png") check("2347_baked_dark_after_close", ow.emote == nil and tiles.image == darkAtlas and tiles.curBgp == nil and tiles.baseImage == nil and ow.dark == true) end else U.log("SKIP 2347_baked (no gbc atlas in this cache)") end if PaletteFX.mode == "redpp" then U.teleport(game, "PALLET_TOWN", 11, 11, "down") U.wait(20) ow = game.overworld npc = follower() local tiles = ow.map.renderer local renderers = { tiles } for _, nb in ipairs(ow.neighbors or {}) do if nb.map.renderer and nb.map.renderer.gbcAtlas then renderers[#renderers + 1] = nb.map.renderer end end check("2347_outdoor_lit", ow.dark ~= true and ow:bakedWorldColors()) check("2347_outdoor_neighbors_baked", #renderers >= 2) local animated = 0 for _, a in ipairs(tiles.anims or {}) do if a.spec then animated = animated + 1 end end check("2347_outdoor_water_flower_anims", animated >= 2) local function allOn(byte) for _, r in ipairs(renderers) do local img = r.bgpImages and r.bgpImages[byte] if not (img and r.image == img and r.curBgp == byte) then return false end for _, a in ipairs(r.anims or {}) do if a.spec and not (a.bgpTextures and a.textures == a.bgpTextures[byte]) then return false end end end return true end local function allBase() for _, r in ipairs(renderers) do if r.curBgp ~= nil or r.baseImage ~= nil then return false end for _, a in ipairs(r.anims or {}) do if a.baseTextures then return false end for _, t in pairs(a.bgpTextures or {}) do if a.textures == t then return false end end end end return true end PikachuFollower.onMoveLearned(game.save, pika, "THUNDERBOLT") if npc then PikachuFollower.talk(game, ow, npc, function() end) end for _ = 1, 200 do if ow.emote and ow.emote.pikaPic then break end U.wait(1) end emote = ow.emote if check("2347_outdoor_emote", emote ~= nil) then local onC0, onE4, sawC0, sawE4 = nil, nil, 0, 0 local last for _ = 1, 2000 do if ow.emote ~= emote then break end U.wait(1) if ow.emote ~= emote then break end local bgp = emote.bgp if bgp ~= last and (bgp == 0xC0 and sawC0 < 3 or bgp == 0xE4 and sawE4 < 3) then if bgp == 0xC0 then sawC0 = sawC0 + 1 U.still(game, SHOT_DIR .. "/2347_12_outdoor_strobe_white_" .. sawC0 .. ".png") onC0 = (onC0 ~= false) and allOn(0xC0) else sawE4 = sawE4 + 1 U.still(game, SHOT_DIR .. "/2347_13_outdoor_strobe_lit_" .. sawE4 .. ".png") onE4 = (onE4 ~= false) and allBase() end end last = bgp end check("2347_outdoor_strobe_seen", sawC0 == 3 and sawE4 == 3) check("2347_outdoor_c0_every_renderer_and_anim", onC0 == true) check("2347_outdoor_e4_lit_base", onE4 == true) U.wait(6) U.still(game, SHOT_DIR .. "/2347_14_outdoor_after_close.png") check("2347_outdoor_base_after_close", ow.emote == nil and allBase()) end end Sound.playMove, Music.duckForFanfare = realPlayMove, realDuck quit() end