-- The Silph Scope unveil in the POKEMON_TOWER_6F battle (#492). -- -- PrintBeginningBattleText .isMarowak (engine/battle/common_text.asm:49-60): -- with the scope in the bag the battle still ENTERS disguised -- InitWildBattle -- takes its .isGhost branch on wCurOpponent == RESTLESS_SOUL either way -- and -- the scope only buys the unveil that is played over the disguise: -- EnemyAppearedText, UnveiledGhostText, LoadEnemyMonData, MarowakAnim, then -- WildMonAppearedText. The port used to hand the scope-carrying player a -- battle that opened with MAROWAK already on screen and no unveil at all. -- -- The story3 dispatch half (which branch the trigger picks, and that the ball -- dodge survives the unveil) is tests/parity_marowak.lua and -- tests/parity_marowak_ball.lua. package.path = "./?.lua;./?/init.lua;" .. package.path local T = require("tests.modkit") local BattleState = require("src.battle.BattleState") T.check(type(BattleState.makeUnveiledGhost) == "function", "BattleState:makeUnveiledGhost exists") T.check(BattleState.makeUnveiledGhost ~= BattleState.makeGhost, "the unveiled battle is not just the scope-less ghost battle") local REAL_SPRITE = { marker = "the MAROWAK front pic" } local function newBattle() return setmetatable({ kind = "wild", queue = {}, frame = 0, data = { text = {}, pokemon = {} }, enemy = { name = "MAROWAK", sprite = REAL_SPRITE, mon = { species = "MAROWAK", level = 30 } }, }, BattleState) end -- ------------------------------------------------------- entering disguised local b = newBattle() b:makeUnveiledGhost() T.eq(b.enemy.name, "GHOST", "with the scope the battle still OPENS as the GHOST (#492)") T.check(b.enemy.sprite ~= REAL_SPRITE, "and wears the ghost pic, not the MAROWAK one") T.check(b.scopeReveal == true, "the unveil is armed") T.check(not b.ghost, "but IsGhostBattle stays false: the mon can be attacked and flees normally") T.eq(b.ghostReal.name, "MAROWAK", "the real nick is kept for LoadEnemyMonData") T.eq(b.ghostReal.sprite, REAL_SPRITE, "...and the real pic with it") local ghostSprite = b.enemy.sprite -- ------------------------------------------------------------- the row order -- .isMarowak prints over the "GHOST appeared!" box enter() already queued: the -- unveil line, MarowakAnim (a wait row parked over the fx state machine), then -- "Wild MAROWAK appeared!" under the restored name. b:queueScopeReveal() T.eq(#b.queue, 4, "the unveil is four queue rows") T.check(type(b.queue[1].text) == "string" and b.queue[1].text:find("SILPH SCOPE", 1, true) ~= nil, "row 1 is UnveiledGhostText") T.check(type(b.queue[2].fn) == "function", "row 2 starts MarowakAnim") T.eq(b.queue[3].wait, BattleState.GHOST_REVEAL_FRAMES, "row 3 holds the queue for the length of the animation") T.check(type(b.queue[4].text) == "string" and b.queue[4].text:find("MAROWAK", 1, true) ~= nil, "row 4 is WildMonAppearedText under the real name") T.check(b.queue[4].text:find("GHOST", 1, true) == nil, "...and not under the disguise") -- ------------------------------------------------------------ the animation -- MarowakAnim (engine/battle/ghost_marowak_anim.asm): FlashSprite8Times, the -- rOBP1 fade-out, the pic swap, the fade-in. The whole point of #492 is that -- the swap comes AFTER the ghost has faded off the paper, so every frame up to -- there still shows the GHOST. b.queue[2].fn() T.check(b.ghostReveal ~= nil, "the fn arms the reveal state") local FLASH = BattleState.GHOST_FLASH_FRAMES local FADE_OUT = BattleState.GHOST_FADE_OUT_FRAMES local swapFrame, fades, stillGhost = nil, {}, true for frame = 1, BattleState.GHOST_REVEAL_FRAMES do b:updateFx() local pf = b.picFx and b.picFx[b.enemy] fades[frame] = pf and pf.fade if not swapFrame and b.enemy.name ~= "GHOST" then swapFrame = frame end if frame <= FLASH + FADE_OUT and b.enemy.name ~= "GHOST" then stillGhost = false end end T.check(stillGhost, "the GHOST is on screen for the whole flash and fade-out (#492)") T.eq(swapFrame, FLASH + FADE_OUT + 1, "the pic and nick swap the frame after the ghost has faded out") T.eq(b.enemy.name, "MAROWAK", "the real nick is back by the end") T.eq(b.enemy.sprite, REAL_SPRITE, "and the real pic with it") T.check(b.enemy.sprite ~= ghostSprite, "the ghost pic is gone") -- FlashSprite8Times xors rOBP1 with $80 every 10 frames, so the body alternates -- between two shades rather than sitting opaque T.eq(fades[5], 1, "the flash opens on the ghost at full strength") T.eq(fades[15], 0.5, "...drops a shade 10 frames in") T.eq(fades[25], 1, "...and comes back") T.check(fades[FLASH + FADE_OUT] == 0, "the ghost is fully faded out when the swap lands") T.check(fades[FLASH + FADE_OUT + 1] > 0 and fades[FLASH + FADE_OUT + 1] < 1, "the MAROWAK fades IN rather than popping on") T.eq(fades[BattleState.GHOST_REVEAL_FRAMES - 1], 1, "and reaches full strength before the box turns") T.check(b.ghostReveal == nil, "the reveal state clears itself") T.check(b.scopeReveal == nil, "so does the flag that gated the intro cry") local pf = b.picFx and b.picFx[b.enemy] T.check(pf == nil or pf.fade == nil, "no leftover alpha on the pic for the rest of the battle") -- ------------------------------------------------ the scope-less ghost still -- Nothing above may change the battle you get WITHOUT the scope: still a real -- ghost battle, still no unveil queued. local noScope = newBattle() noScope:makeGhost() T.check(noScope.ghost == true, "without the scope IsGhostBattle is true") T.eq(noScope.enemy.name, "GHOST", "and the disguise is the same one") T.check(noScope.scopeReveal == nil, "with no unveil armed") T.eq(#noScope.queue, 0, "and no unveil rows queued") T.finish("ghost unveil")