mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-10-25 06:36:36 -04:00
@@ -309,11 +309,14 @@ M.PALLET_TOWN = {
|
||||
|
||||
game.stack:push(TextBox.new(game,
|
||||
t._PalletTownOakHeyWaitDontGoOutText or "OAK: Hey! Wait!\nDon't go out!",
|
||||
nil, { auto = { delay = 10, overlap = 10, onOverlap = function()
|
||||
-- .HeyWaitDontGoOutText turns the player to face down (toward
|
||||
-- the approaching Oak) before the exclamation bubble
|
||||
function()
|
||||
ow.emote = nil
|
||||
oakAppearsAndWalks()
|
||||
end, { auto = { delay = 10, overlap = 60, onOverlap = function()
|
||||
-- scripts/PalletTown.asm:186
|
||||
ow.player.facing = "down"
|
||||
ow.emote = { npc = ow.player, frames = 50, onDone = oakAppearsAndWalks }
|
||||
-- scripts/PalletTown.asm:185
|
||||
ow.emote = { npc = ow.player, frames = 60 }
|
||||
end } }))
|
||||
return true
|
||||
end,
|
||||
|
||||
@@ -214,9 +214,7 @@ local function applySavedAudioMode()
|
||||
end)
|
||||
if not ok or type(savedOptions) ~= "table" then savedOptions = {} end
|
||||
local mode = savedOptions.audioMode or "both"
|
||||
if love and love.audio and love.audio.setMixWithSystem then
|
||||
pcall(love.audio.setMixWithSystem, mode ~= "game_only")
|
||||
end
|
||||
require("src.audio.AudioMix").set(mode ~= "game_only")
|
||||
end
|
||||
|
||||
local Game, EditorApp, Importer, TouchEditor, Studio, Prelaunch
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
local AudioMix = {}
|
||||
|
||||
local sent
|
||||
|
||||
function AudioMix.set(mix)
|
||||
mix = mix and true or false
|
||||
if sent == mix then return false end
|
||||
if not (love and love.audio and love.audio.setMixWithSystem) then
|
||||
return false
|
||||
end
|
||||
if pcall(love.audio.setMixWithSystem, mix) then sent = mix end
|
||||
return true
|
||||
end
|
||||
|
||||
function AudioMix.current()
|
||||
return sent
|
||||
end
|
||||
|
||||
function AudioMix._resetForTest()
|
||||
sent = nil
|
||||
end
|
||||
|
||||
return AudioMix
|
||||
+6
-4
@@ -573,9 +573,7 @@ end
|
||||
|
||||
function Music.setAudioMode(mode)
|
||||
currentAudioMode = mode or "both"
|
||||
if love and love.audio and love.audio.setMixWithSystem then
|
||||
pcall(love.audio.setMixWithSystem, currentAudioMode ~= "game_only")
|
||||
end
|
||||
require("src.audio.AudioMix").set(currentAudioMode ~= "game_only")
|
||||
Music.setVolumeLevel(storedVolumeLevel)
|
||||
end
|
||||
|
||||
@@ -634,7 +632,11 @@ function Music.onDeviceReset()
|
||||
local src = require("src.core.ChipAudio").currentSource()
|
||||
if not src then return end
|
||||
state.source = src
|
||||
applyVolume(src)
|
||||
if state.fade then
|
||||
fadeSetLevel(state.fade, state.fade.level)
|
||||
else
|
||||
applyVolume(src)
|
||||
end
|
||||
applyFilter(src)
|
||||
return
|
||||
end
|
||||
|
||||
@@ -370,9 +370,7 @@ function Audio.applyEngineOptions(opts)
|
||||
if type(opts) ~= "table" then return end
|
||||
local mode = opts.audioMode or "both"
|
||||
Audio._audioMode = mode
|
||||
if love and love.audio and love.audio.setMixWithSystem then
|
||||
pcall(love.audio.setMixWithSystem, mode ~= "game_only")
|
||||
end
|
||||
require("src.audio.AudioMix").set(mode ~= "game_only")
|
||||
if mode == "external_only" then
|
||||
Audio._bgmVolume = 0
|
||||
else
|
||||
@@ -467,7 +465,6 @@ function Audio.playSong(id, opts)
|
||||
}
|
||||
Audio._mapSong = Audio._mapSong or id
|
||||
|
||||
-- A new song owns the bus — cancel stale fades and fanfares (oak exit fade was killing lab BGM).
|
||||
Audio._fadeOut = nil
|
||||
Audio._fadeIn = nil
|
||||
Audio._fanfareActive = false
|
||||
@@ -1743,7 +1740,6 @@ function Audio.onFocusGained()
|
||||
end
|
||||
end
|
||||
|
||||
--- Device reset: QueueableSource may be dead — rebuild then refill.
|
||||
function Audio.rebuildPlayback()
|
||||
Audio._suspended = false
|
||||
if not (love and love.audio and love.audio.newQueueableSource) then return false end
|
||||
|
||||
+7
-3
@@ -118,6 +118,7 @@ function ListMenu.new(game, title, items, opts)
|
||||
self.onChoose = opts.onChoose
|
||||
self.onCancel = opts.onCancel
|
||||
self.footer = opts.footer
|
||||
self.listPrompt = opts.footer
|
||||
self.pageJump = opts.pageJump -- Left/Right move a page at a time
|
||||
self.wrap = opts.wrap -- Up on first / Down on last wraps
|
||||
self.keyRepeat = opts.keyRepeat -- hold Up/Down (and pageJump L/R) to scroll
|
||||
@@ -312,6 +313,10 @@ end
|
||||
-- PrintListMenuEntries, minus the price column StartMenu_Item never asks for
|
||||
-- (wPrintItemPrices = 0, engine/menus/start_sub_menus.asm)
|
||||
function ListMenu:drawItemBox()
|
||||
local hasBox = self.messageBox or self.footer
|
||||
-- engine/menus/players_pc.asm:151-162
|
||||
local boxUnder = hasBox and self.footer == self.listPrompt
|
||||
if boxUnder then drawMessageBox(self) end
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
Font.drawBox(ITEM_BOX.tx, ITEM_BOX.ty, ITEM_BOX.tw, ITEM_BOX.th)
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
@@ -356,9 +361,8 @@ function ListMenu:drawItemBox()
|
||||
and (self.arrowBlink or 0) < ARROW_BLINK_ON then
|
||||
Font.drawCode(Theme.moreArrow, ITEM_MORE_X, ITEM_MORE_Y)
|
||||
end
|
||||
-- players_pc.asm:97/151/205 PrintText the prompt before DisplayListMenuID,
|
||||
-- so the bottom box sits under the list from the first frame
|
||||
if self.messageBox or self.footer then drawMessageBox(self) end
|
||||
-- engine/menus/players_pc.asm:173
|
||||
if hasBox and not boxUnder then drawMessageBox(self) end
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
end
|
||||
|
||||
|
||||
+1
-1
@@ -159,7 +159,7 @@ local function markerSheet(def, seed)
|
||||
end
|
||||
if not colors then
|
||||
if PaletteFX.usesSpriteObp() then
|
||||
colors, group = PaletteFX.ogObjLit() -- engine/items/town_map.asm:325
|
||||
colors, group = PaletteFX.ogObjNormal() -- home/palettes.asm:24
|
||||
else
|
||||
colors, group = PaletteFX.dmgObjLit()
|
||||
end
|
||||
|
||||
@@ -204,9 +204,21 @@ local function makeFollower(game, ow, x, y, facing)
|
||||
if idle and idle.phase then return idle.phase % 2 end
|
||||
return NPC.walkPhase(self)
|
||||
end
|
||||
npc.draw = function(self, camX, camY)
|
||||
if PikachuFollower.underPlayer(ow, self) then return end
|
||||
return NPC.draw(self, camX, camY)
|
||||
end
|
||||
return npc
|
||||
end
|
||||
|
||||
-- engine/pikachu/pikachu_follow.asm:1030
|
||||
function PikachuFollower.underPlayer(ow, npc)
|
||||
local p = ow and ow.player
|
||||
if not (p and npc) or npc.moving then return false end
|
||||
return npc.cellX == (p.targetX or p.cellX)
|
||||
and npc.cellY == (p.targetY or p.cellY)
|
||||
end
|
||||
|
||||
local function findFollower(ow)
|
||||
for i, npc in ipairs(ow.npcs or {}) do
|
||||
if npc.pikachuFollower then return npc, i end
|
||||
|
||||
@@ -7067,7 +7067,8 @@ function World:startBattle(opts, onDone)
|
||||
-- whatever stood there.
|
||||
if whiteout then
|
||||
self:healParty()
|
||||
if not BugContest.isActive(game.save) then
|
||||
local inContest = BugContest.isActive(game.save)
|
||||
if not inContest then
|
||||
CallAsm.run(self, "HalveMoney")
|
||||
CallAsm.run(self, "GetWhiteoutSpawn")
|
||||
end
|
||||
@@ -7078,6 +7079,12 @@ function World:startBattle(opts, onDone)
|
||||
Runtime.emit("world.blacked_out",
|
||||
{ save = game.save, healTarget = self:healPoint() })
|
||||
end
|
||||
if inContest then
|
||||
self:restoreMapMusic()
|
||||
-- engine/events/whiteout.asm:24-25
|
||||
self:bugContestResults()
|
||||
return
|
||||
end
|
||||
-- engine/events/whiteout.asm:19-20
|
||||
self:runMapSetup(MAPSETUP.WARP, function()
|
||||
self:warpToSpawn()
|
||||
@@ -11078,9 +11085,15 @@ end
|
||||
function World:whiteOut()
|
||||
self:showText(
|
||||
Strings("You have no more\nPOKéMON that can\011fight!"), function()
|
||||
self:healParty()
|
||||
-- engine/events/whiteout.asm:15-16
|
||||
if BugContest.isActive(self.game and self.game.save) then
|
||||
-- engine/events/whiteout.asm:24-25
|
||||
self:bugContestResults()
|
||||
return
|
||||
end
|
||||
CallAsm.run(self, "HalveMoney")
|
||||
CallAsm.run(self, "GetWhiteoutSpawn")
|
||||
self:healParty()
|
||||
-- Guarded because healPoint walks the spawn table to answer: with nobody
|
||||
-- listening the blackout must not pay for a lookup warpToSpawn is about to
|
||||
-- make again anyway.
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
-- scripts/PalletTown.asm:177-188
|
||||
return function(game)
|
||||
local U = dofile("tests/drivers/util.lua")
|
||||
local DIR = os.getenv("POKEPORT_SHOT_DIR") or os.getenv("SHOT_DIR") or "/tmp/shots"
|
||||
local TextBox = require("src.render.TextBox")
|
||||
|
||||
local ok = true
|
||||
local function check(label, cond)
|
||||
if not cond then ok = false end
|
||||
U.log(cond and "PASS" or "FAIL", label)
|
||||
end
|
||||
local function done()
|
||||
love.event.quit(ok and 0 or 1)
|
||||
end
|
||||
|
||||
local f = game.save.flags
|
||||
f.EVENT_FOLLOWED_OAK_INTO_LAB = nil
|
||||
f.EVENT_GOT_STARTER = nil
|
||||
U.teleport(game, "PALLET_TOWN", 10, 3, "up")
|
||||
local ow = game.stack:top()
|
||||
|
||||
local box
|
||||
for _ = 1, 120 do
|
||||
table.insert(game.input.pressQueue, "up")
|
||||
game.input.state.up = true
|
||||
U.wait(1)
|
||||
local top = game.stack:top()
|
||||
if getmetatable(top) == TextBox then box = top break end
|
||||
end
|
||||
game.input.state.up = false
|
||||
check("hey wait text box opened", box ~= nil)
|
||||
if not box then return done() end
|
||||
|
||||
local bubbleStart
|
||||
for _ = 1, 600 do
|
||||
if ow.emote and ow.emote.npc == ow.player then bubbleStart = true break end
|
||||
U.wait(1)
|
||||
end
|
||||
check("bubble appears while text box is up", bubbleStart ~= nil and game.stack:top() == box)
|
||||
if not bubbleStart then return done() end
|
||||
U.wait(30)
|
||||
check("text box still up mid-bubble", game.stack:top() == box)
|
||||
U.still(game, DIR .. "/2843_01_bubble_with_textbox.png")
|
||||
|
||||
local boxGone, bubbleGone
|
||||
for _ = 1, 200 do
|
||||
U.wait(1)
|
||||
if not boxGone and game.stack:top() ~= box then boxGone = U.frame() end
|
||||
if not bubbleGone and not (ow.emote and ow.emote.npc == ow.player) then bubbleGone = U.frame() end
|
||||
if boxGone and bubbleGone then break end
|
||||
end
|
||||
local held = box.autoTimer and box.auto and (box.autoTimer - box.auto.delay)
|
||||
U.log("bubble frames under box", tostring(held))
|
||||
check("text box and bubble close on the same frame",
|
||||
boxGone ~= nil and boxGone == bubbleGone)
|
||||
check("bubble lasts 60 frames under the box", held == 60)
|
||||
U.still(game, DIR .. "/2843_02_box_and_bubble_gone.png")
|
||||
|
||||
local oakShown = false
|
||||
for _ = 1, 60 do
|
||||
for _, n in ipairs(ow.npcs or {}) do
|
||||
if n.def and n.def.name == "PALLETTOWN_OAK" then oakShown = true end
|
||||
end
|
||||
if oakShown then break end
|
||||
U.wait(1)
|
||||
end
|
||||
check("oak appears after the bubble", oakShown)
|
||||
done()
|
||||
end
|
||||
@@ -0,0 +1,120 @@
|
||||
-- engine/menus/main_menu.asm:106-112, home/fade_audio.asm:12-45
|
||||
return function(game)
|
||||
local U = dofile("tests/drivers/util.lua")
|
||||
local SaveData = require("src.core.SaveData")
|
||||
local Music = require("src.core.Music")
|
||||
local ChipAudio = require("src.core.ChipAudio")
|
||||
local SHOT_DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/shots/2844"
|
||||
|
||||
local fails = 0
|
||||
local function check(label, ok, detail)
|
||||
print((ok and "PASS " or "FAIL ") .. label
|
||||
.. (detail ~= nil and (" " .. tostring(detail)) or ""))
|
||||
if not ok then fails = fails + 1 end
|
||||
return ok
|
||||
end
|
||||
local function finish()
|
||||
print(fails == 0 and "PASS continue_fade_no_jump_bug2844"
|
||||
or "FAIL continue_fade_no_jump_bug2844")
|
||||
love.event.quit(fails == 0 and 0 or 1)
|
||||
while true do coroutine.yield() end
|
||||
end
|
||||
local function waitFor(pred, limit)
|
||||
for _ = 1, limit or 1800 do
|
||||
if pred() then return true end
|
||||
U.wait(1)
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local mixCalls = 0
|
||||
local realMix = love.audio.setMixWithSystem
|
||||
love.audio.setMixWithSystem = function(mix)
|
||||
mixCalls = mixCalls + 1
|
||||
love.event.push("audioreset")
|
||||
if realMix then return realMix(mix) end
|
||||
return true
|
||||
end
|
||||
|
||||
if not check("new game reached the overworld", U.newGame(game)) then
|
||||
return finish()
|
||||
end
|
||||
local ow = game.overworld
|
||||
game.save.player.map = ow.map.id
|
||||
game.save.player.x, game.save.player.y = ow.player.cellX, ow.player.cellY
|
||||
check("save written", SaveData.save(game.save) ~= false)
|
||||
local mapSong = game.data.audio.mapSongs[ow.map.id]
|
||||
|
||||
game:returnToTitle()
|
||||
local title = game.stack:top()
|
||||
for _ = 1, 120 do
|
||||
title = game.stack:top()
|
||||
if title and title.screenId == "TitleState" and title.phase == "loop" then
|
||||
break
|
||||
end
|
||||
U.tap(game, "start")
|
||||
U.wait(10)
|
||||
end
|
||||
check("title reached its loop", title and title.phase == "loop")
|
||||
local titleSong
|
||||
check("title music playing", waitFor(function()
|
||||
titleSong = Music.current()
|
||||
return titleSong ~= nil and titleSong ~= mapSong
|
||||
and ChipAudio.currentSource() ~= nil
|
||||
end, 600), titleSong)
|
||||
|
||||
U.tap(game, "start")
|
||||
check("main menu open", waitFor(function()
|
||||
return title.menuOpen and game.stack:top() ~= title
|
||||
end, 600))
|
||||
U.wait(4)
|
||||
local menu = game.stack:top()
|
||||
U.tap(game, "a")
|
||||
check("CONTINUE info window open", waitFor(function()
|
||||
local top = game.stack:top()
|
||||
return top ~= menu and top and top.titleUiBox and top.titleUiBox[1] == 4
|
||||
end, 300))
|
||||
U.wait(4)
|
||||
|
||||
local titleSource = ChipAudio.currentSource()
|
||||
mixCalls = 0
|
||||
U.tap(game, "a")
|
||||
|
||||
local fadeFrames, swapped, rose, lastVol = 0, false, false, nil
|
||||
local switchFrame, shot = nil, false
|
||||
for f = 1, 300 do
|
||||
local top = game.stack:top()
|
||||
if top and top.screenId == "QuarantineReport" then
|
||||
table.insert(game.input.pressQueue, "a")
|
||||
end
|
||||
local cur = Music.current()
|
||||
local level, _, vol
|
||||
if Music.fadeLevel then level, _, vol = Music.fadeLevel() end
|
||||
if level and cur == titleSong then
|
||||
fadeFrames = fadeFrames + 1
|
||||
if ChipAudio.currentSource() ~= titleSource then swapped = true end
|
||||
local live = ChipAudio.currentSource()
|
||||
local ok, v = pcall(function() return live:getVolume() end)
|
||||
v = ok and v or vol
|
||||
if lastVol and v and v > lastVol + 1e-6 then rose = true end
|
||||
lastVol = v or lastVol
|
||||
if fadeFrames == 30 and not shot then
|
||||
shot = true
|
||||
U.still(game, SHOT_DIR .. "/2844_01_overworld_title_fading.png")
|
||||
end
|
||||
end
|
||||
if not switchFrame and cur == mapSong then switchFrame = f end
|
||||
if switchFrame and f > switchFrame + 5 then break end
|
||||
U.wait(1)
|
||||
end
|
||||
|
||||
check("CONTINUE sends no setMixWithSystem (no audioreset)", mixCalls == 0,
|
||||
mixCalls)
|
||||
check("title fade ran", fadeFrames >= 80, fadeFrames)
|
||||
check("title keeps its queued stream through the fade (no position jump)",
|
||||
not swapped)
|
||||
check("source volume never rises during the fade", not rose)
|
||||
check("map song takes over after the fade", switchFrame ~= nil, switchFrame)
|
||||
love.audio.setMixWithSystem = realMix
|
||||
return finish()
|
||||
end
|
||||
@@ -0,0 +1,82 @@
|
||||
-- ../pokecrystal/engine/events/whiteout.asm:9-25
|
||||
-- ../pokecrystal/engine/events/std_scripts.asm:308-315
|
||||
local U = require("tests.drivers.util")
|
||||
local Mon = require("src.battle.gen2.Mon")
|
||||
local BugContest = require("src.core.gen2.BugContest")
|
||||
|
||||
local SHOT_DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/gen2-contest-whiteout-2845"
|
||||
|
||||
return function(game)
|
||||
os.execute('mkdir -p "' .. SHOT_DIR .. '" 2>/dev/null')
|
||||
local fails = 0
|
||||
local function ok(cond, line)
|
||||
if not cond then fails = fails + 1 end
|
||||
print("[2845] " .. (cond and "PASS " or "FAIL ") .. line)
|
||||
return cond
|
||||
end
|
||||
local function finish()
|
||||
print("[2845] " .. (fails == 0 and "all claims passed" or (fails .. " claims failed")))
|
||||
love.event.quit(fails == 0 and 0 or 1)
|
||||
while true do U.wait(60) end
|
||||
end
|
||||
local function mapId(world)
|
||||
return world.map and world.map.def and world.map.def.id
|
||||
end
|
||||
|
||||
U.wait(45)
|
||||
local world, save = game.world, game.save
|
||||
if not ok(world and world.map and world.vm and save, "the world booted") then finish() end
|
||||
|
||||
local lead = Mon.new(game.data, "SENTRET", 3)
|
||||
lead.hp = 1
|
||||
save.party = { lead }
|
||||
save.player = save.player or {}
|
||||
save.player.money = 3000
|
||||
BugContest.start(save)
|
||||
world:warpToMapId("NATIONAL_PARK", 10, 20, "down")
|
||||
U.wait(45)
|
||||
ok(mapId(world) == "NATIONAL_PARK", "standing in the park mid-contest")
|
||||
ok(BugContest.isActive(save), "the contest is running")
|
||||
|
||||
local wild = Mon.new(game.data, "SCYTHER", 50)
|
||||
wild.moves = { { id = "QUICK_ATTACK", pp = 30, maxPp = 30 } }
|
||||
ok(world:startBattle({ wild = wild, contest = true }), "a contest battle started")
|
||||
local screen
|
||||
for _ = 1, 900 do
|
||||
local top = game.stack:top()
|
||||
if top and top.battle then screen = top break end
|
||||
U.wait(1)
|
||||
end
|
||||
if not ok(screen ~= nil and screen.contest, "the contest battle screen came up") then finish() end
|
||||
|
||||
for _ = 1, 3000 do
|
||||
if game.stack:top() ~= screen then break end
|
||||
U.tap(game, "a")
|
||||
U.wait(3)
|
||||
end
|
||||
ok(screen.battle and screen.battle.outcome == "lose", "the battle ended in a loss")
|
||||
|
||||
local atGate = false
|
||||
for _ = 1, 600 do
|
||||
if mapId(world) == "ROUTE_36_NATIONAL_PARK_GATE" then atGate = true break end
|
||||
U.wait(2)
|
||||
end
|
||||
ok(atGate, "the wipe warped to the national park gate, not a Pokecenter (map "
|
||||
.. tostring(mapId(world)) .. ")")
|
||||
U.wait(30)
|
||||
U.still(game, SHOT_DIR .. "/2845_01_gate_after_wipe.png")
|
||||
|
||||
local deadline = love.timer.getTime() + 15
|
||||
while world.vm:running() and love.timer.getTime() < deadline do
|
||||
U.tap(game, "a")
|
||||
U.wait(3)
|
||||
end
|
||||
ok(not world.vm:running(), "the judging script ran to completion")
|
||||
ok(not BugContest.isActive(save), "the contest is over")
|
||||
ok(mapId(world) == "ROUTE_36_NATIONAL_PARK_GATE", "still at the gate after judging")
|
||||
ok(save.player.money == 3000, "the wallet was not halved")
|
||||
ok((save.party[1] and save.party[1].hp or 0) > 0, "the party was healed")
|
||||
U.wait(10)
|
||||
U.still(game, SHOT_DIR .. "/2845_02_judged_at_gate.png")
|
||||
finish()
|
||||
end
|
||||
@@ -0,0 +1,69 @@
|
||||
-- engine/menus/players_pc.asm:151-173
|
||||
return function(game)
|
||||
local U = dofile("tests/drivers/util.lua")
|
||||
local DIR = os.getenv("POKEPORT_SHOT_DIR") or os.getenv("SHOT_DIR") or "/tmp/shots"
|
||||
local Menu = require("src.ui.Menu")
|
||||
local Font = require("src.render.Font")
|
||||
|
||||
local ok = true
|
||||
local function check(label, cond)
|
||||
if not cond then ok = false end
|
||||
U.log(cond and "PASS" or "FAIL", label)
|
||||
end
|
||||
local function hasLabel(menu, needle)
|
||||
if getmetatable(menu) ~= Menu then return false end
|
||||
for _, it in ipairs(menu.items or {}) do
|
||||
if type(it.label) == "string" and it.label:find(needle, 1, true) then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
local function mash(cond)
|
||||
for _ = 1, 200 do
|
||||
if cond() then return true end
|
||||
U.tap(game, "a")
|
||||
U.wait(3)
|
||||
end
|
||||
return false
|
||||
end
|
||||
local function boxOrder(state)
|
||||
local seen = {}
|
||||
local real = Font.drawBox
|
||||
Font.drawBox = function(tx, ty, ...)
|
||||
seen[#seen + 1] = tx .. "," .. ty
|
||||
return real(tx, ty, ...)
|
||||
end
|
||||
local good, err = pcall(state.draw, state)
|
||||
Font.drawBox = real
|
||||
if not good then error(err, 0) end
|
||||
return table.concat(seen, " ")
|
||||
end
|
||||
local function done()
|
||||
love.event.quit(ok and 0 or 1)
|
||||
end
|
||||
|
||||
game.save.pcItems = { POTION = 3 }
|
||||
game.save.flags = game.save.flags or {}
|
||||
game.save.flags.EVENT_GOT_POKEDEX = true
|
||||
U.teleport(game, "VIRIDIAN_POKECENTER", 13, 4, "up")
|
||||
U.tap(game, "a")
|
||||
check("pc main menu opens", mash(function() return hasLabel(game.stack:top(), "LOG OFF") end))
|
||||
U.wait(8)
|
||||
U.tap(game, "down")
|
||||
U.wait(3)
|
||||
U.tap(game, "a")
|
||||
check("player pc opens", mash(function() return hasLabel(game.stack:top(), "WITHDRAW ITEM") end))
|
||||
U.wait(8)
|
||||
U.tap(game, "a")
|
||||
U.wait(10)
|
||||
local list = game.stack:top()
|
||||
check("withdraw list open", list and list.kind == "pc_item_withdraw")
|
||||
if not (list and list.kind == "pc_item_withdraw") then return done() end
|
||||
check("prompt box under the list", boxOrder(list):find("0,12 4,2", 1, true) ~= nil)
|
||||
U.still(game, DIR .. "/2840_01_withdraw_list_over_prompt.png")
|
||||
|
||||
U.tap(game, "a")
|
||||
U.wait(10)
|
||||
check("How many? box over the list", boxOrder(list):find("4,2 0,12", 1, true) ~= nil)
|
||||
U.still(game, DIR .. "/2840_02_how_many_over_list.png")
|
||||
done()
|
||||
end
|
||||
@@ -0,0 +1,82 @@
|
||||
-- engine/pikachu/pikachu_follow.asm:1030
|
||||
return function(game)
|
||||
local U = dofile("tests/drivers/util.lua")
|
||||
local Pokemon = require("src.pokemon.Pokemon")
|
||||
local GameVersion = require("src.core.GameVersion")
|
||||
local NPC = require("src.world.NPC")
|
||||
local PF = require("src.world.PikachuFollower")
|
||||
local SHOT_DIR = os.getenv("POKEPORT_SHOT_DIR") or os.getenv("SHOT_DIR") or "/tmp/shots"
|
||||
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
|
||||
|
||||
check("running as Yellow", GameVersion.isYellow())
|
||||
|
||||
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.party = { Pokemon.new(game.data, "PIKACHU", 20) }
|
||||
game.save.onBike = false
|
||||
|
||||
local pikaDraws = 0
|
||||
local realDraw = NPC.draw
|
||||
NPC.draw = function(self, ...)
|
||||
if self.pikachuFollower then pikaDraws = pikaDraws + 1 end
|
||||
return realDraw(self, ...)
|
||||
end
|
||||
|
||||
local function plain(map, x, y)
|
||||
return map:inBounds(x, y) and map:isWalkableCell(x, y)
|
||||
and not map:isGrassCell(x, y)
|
||||
end
|
||||
|
||||
U.teleport(game, "ROUTE_1", 10, 20, "down")
|
||||
U.wait(5)
|
||||
local ow = game.overworld
|
||||
local sx, sy
|
||||
for y = 4, 30 do
|
||||
for x = 2, 16 do
|
||||
if not sx and plain(ow.map, x, y) and plain(ow.map, x, y + 1)
|
||||
and plain(ow.map, x, y + 2) and plain(ow.map, x - 1, y)
|
||||
and plain(ow.map, x + 1, y) then
|
||||
sx, sy = x, y
|
||||
end
|
||||
end
|
||||
end
|
||||
check("found an open Route 1 cell", sx ~= nil)
|
||||
sx, sy = sx or 10, sy or 20
|
||||
U.teleport(game, "ROUTE_1", sx, sy, "down")
|
||||
U.wait(30)
|
||||
ow = game.overworld
|
||||
|
||||
local npc = PF.current(ow)
|
||||
local p = ow.player
|
||||
check("follower spawned on the Route 1 map load", npc ~= nil)
|
||||
check("spawn state 0 parks Pikachu on the player's cell",
|
||||
npc and npc.cellX == p.cellX and npc.cellY == p.cellY)
|
||||
|
||||
pikaDraws = 0
|
||||
local shotOk = U.still(game, SHOT_DIR .. "/2846_01_route1_before_step.png")
|
||||
check("Pikachu is not drawn before the first step (draws=" .. pikaDraws .. ")",
|
||||
shotOk and pikaDraws == 0)
|
||||
|
||||
U.hold(game, "down", 20)
|
||||
U.wait(20)
|
||||
npc = PF.current(ow)
|
||||
p = ow.player
|
||||
check("after one step Pikachu trails one cell behind",
|
||||
npc and p and npc.cellX == p.cellX and npc.cellY == p.cellY - 1)
|
||||
pikaDraws = 0
|
||||
shotOk = U.still(game, SHOT_DIR .. "/2846_02_route1_after_step.png")
|
||||
check("Pikachu is drawn after the first step (draws=" .. pikaDraws .. ")",
|
||||
shotOk and pikaDraws > 0)
|
||||
|
||||
NPC.draw = realDraw
|
||||
U.log(failures == 0 and "PASS all" or ("FAIL " .. failures .. " check(s)"))
|
||||
love.event.quit(failures == 0 and 0 or 1)
|
||||
end
|
||||
@@ -41,7 +41,7 @@ return function(game)
|
||||
local red = sprites.SPRITE_RED
|
||||
local colors, group
|
||||
if PaletteFX.usesSpriteObp() then
|
||||
colors, group = PaletteFX.ogObj()
|
||||
colors, group = PaletteFX.ogObjNormal()
|
||||
else
|
||||
colors, group = PaletteFX.dmgObj()
|
||||
end
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
-- engine/items/town_map.asm:325
|
||||
return function(game)
|
||||
local U = dofile("tests/drivers/util.lua")
|
||||
local Screens = require("src.ui.Screens")
|
||||
local PaletteFX = require("src.render.PaletteFX")
|
||||
local SpriteRenderer = require("src.render.SpriteRenderer")
|
||||
local DIR = os.getenv("POKEPORT_SHOT_DIR") or os.getenv("SHOT_DIR") or "/tmp/shots"
|
||||
|
||||
local ok = true
|
||||
local function check(label, cond)
|
||||
if not cond then ok = false end
|
||||
U.log(cond and "PASS" or "FAIL", label)
|
||||
end
|
||||
|
||||
game.save.visited = { PALLET_TOWN = true, VIRIDIAN_CITY = true }
|
||||
U.teleport(game, "VIRIDIAN_CITY", 20, 20, "down")
|
||||
U.wait(10)
|
||||
game.save.options = game.save.options or {}
|
||||
game.save.options.colors = "ogred"
|
||||
PaletteFX.setMode("ogred")
|
||||
U.wait(4)
|
||||
Screens.push(game, "TownMap")
|
||||
U.wait(20)
|
||||
local top = game.stack:top()
|
||||
check("town map is up", top and top.playerQuad ~= nil)
|
||||
local red = (game.data.sprites or {}).SPRITE_RED
|
||||
local colors, group = PaletteFX.ogObjNormal()
|
||||
local want = red and SpriteRenderer.obpImage(red.image, colors, group)
|
||||
check("marker is the rOBP0 $D0 bake", top and want ~= nil and top.playerSheet == want)
|
||||
local base = PaletteFX.ogObjBase()
|
||||
check("OBJ color 1 is white", table.concat(colors[2], ",") == table.concat(base[1], ","))
|
||||
for _ = 1, 60 do
|
||||
if top.blink < 16 then break end
|
||||
coroutine.yield()
|
||||
end
|
||||
U.shot(game, DIR .. "/2841_01_town_map_ogred_marker.png")
|
||||
love.event.quit(ok and 0 or 1)
|
||||
end
|
||||
@@ -36,10 +36,15 @@ Music.applyOptions({ audioMode = "both", musicVol = 7 })
|
||||
eq(Music.audioMode(), "both", "Music mode set to 'both'")
|
||||
eq(mixWithSystemCalls[#mixWithSystemCalls], true, "audioMode 'both' enables mixWithSystem")
|
||||
|
||||
mixWithSystemCalls = {}
|
||||
Music.applyOptions({ audioMode = "both", musicVol = 7 })
|
||||
eq(#mixWithSystemCalls, 0, "re-applying unchanged options sends no setMixWithSystem (#2844)")
|
||||
|
||||
mixWithSystemCalls = {}
|
||||
Music.applyOptions({ audioMode = "external_only", musicVol = 7 })
|
||||
eq(Music.audioMode(), "external_only", "Music mode set to 'external_only'")
|
||||
eq(mixWithSystemCalls[#mixWithSystemCalls], true, "audioMode 'external_only' enables mixWithSystem")
|
||||
eq(#mixWithSystemCalls, 0, "'both' -> 'external_only' keeps mixWithSystem on without a resend")
|
||||
eq(require("src.audio.AudioMix").current(), true, "audioMode 'external_only' enables mixWithSystem")
|
||||
|
||||
mixWithSystemCalls = {}
|
||||
Music.applyOptions({ audioMode = "game_only", musicVol = 7 })
|
||||
@@ -60,7 +65,7 @@ Game3Audio.applyEngineOptions({ audioMode = "external_only", musicVol = 7, sfxVo
|
||||
eq(Game3Audio._audioMode, "external_only", "Game3 audioMode set to 'external_only'")
|
||||
eq(Game3Audio._bgmVolume, 0, "Game3 'external_only' mutes in-game BGM")
|
||||
check(Game3Audio._sfxVolume > 0, "Game3 'external_only' keeps SFX active")
|
||||
eq(mixWithSystemCalls[#mixWithSystemCalls], true, "Game3 'external_only' enables mixWithSystem")
|
||||
eq(require("src.audio.AudioMix").current(), true, "Game3 'external_only' enables mixWithSystem")
|
||||
|
||||
mixWithSystemCalls = {}
|
||||
Game3Audio.applyEngineOptions({ audioMode = "game_only", musicVol = 7, sfxVol = 7 })
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
-- home/fade_audio.asm:12-35
|
||||
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.harness")
|
||||
local check = T.check
|
||||
local eq = T.eq
|
||||
|
||||
love = require("tests.love_stub")
|
||||
|
||||
local Source = {}
|
||||
Source.__index = Source
|
||||
function Source:play() self.playing = true end
|
||||
function Source:stop() self.playing = false end
|
||||
function Source:pause() self.playing = false end
|
||||
function Source:isPlaying() return self.playing end
|
||||
function Source:setLooping() end
|
||||
function Source:setVolume(v) self.volume = v end
|
||||
function Source:setPitch() end
|
||||
function Source:setFilter() end
|
||||
|
||||
local current
|
||||
local resets = 0
|
||||
local fakeChip = {
|
||||
playMusic = function()
|
||||
current = setmetatable({ playing = true }, Source)
|
||||
return current
|
||||
end,
|
||||
currentSource = function() return current end,
|
||||
rebuildPlayback = function()
|
||||
current = setmetatable({ playing = true }, Source)
|
||||
resets = resets + 1
|
||||
return true
|
||||
end,
|
||||
stopMusic = function() current = nil end,
|
||||
update = function() end,
|
||||
ensureMusicPlaying = function() end,
|
||||
holdMusic = function() end,
|
||||
awaitingFirstBuffer = function() return false end,
|
||||
applyOptions = function() return false end,
|
||||
setStereo = function() end,
|
||||
}
|
||||
package.loaded["src.core.ChipAudio"] = fakeChip
|
||||
|
||||
local Music = require("src.core.Music")
|
||||
|
||||
local mixCalls = 0
|
||||
love.audio = {
|
||||
newSource = function() return setmetatable({}, Source) end,
|
||||
setMixWithSystem = function()
|
||||
mixCalls = mixCalls + 1
|
||||
fakeChip.rebuildPlayback()
|
||||
Music.onDeviceReset()
|
||||
return true
|
||||
end,
|
||||
}
|
||||
|
||||
local data = { audio = {
|
||||
songs = {
|
||||
Music_TitleScreen = { chip = true },
|
||||
Music_PalletTown = { chip = true },
|
||||
},
|
||||
mapSongs = { PALLET_TOWN = "Music_PalletTown" },
|
||||
} }
|
||||
|
||||
local opts = { audioMode = "both", musicVol = 7, musicFilter = 0 }
|
||||
|
||||
Music.applyOptions(opts)
|
||||
Music.play(data, "Music_TitleScreen")
|
||||
local title = current
|
||||
eq(Music.current(), "Music_TitleScreen", "title theme playing")
|
||||
|
||||
mixCalls, resets = 0, 0
|
||||
Music.applyOptions(opts)
|
||||
eq(mixCalls, 0, "CONTINUE re-applying the same options sends no setMixWithSystem")
|
||||
eq(resets, 0, "so the playing title stream is not rebuilt mid-song")
|
||||
check(current == title, "the title keeps its queued stream")
|
||||
|
||||
local full = current.volume
|
||||
Music.playMap(data, "PALLET_TOWN", false, false, Music.MAP_FADE)
|
||||
eq(Music.current(), "Music_TitleScreen", "title fades before the map theme")
|
||||
for _ = 1, 3 * (Music.MAP_FADE + 1) do Music.update(data) end
|
||||
local faded = current.volume
|
||||
check(faded < full, "fade has lowered the title volume")
|
||||
|
||||
fakeChip.rebuildPlayback()
|
||||
Music.onDeviceReset()
|
||||
check(math.abs(current.volume - faded) < 1e-9,
|
||||
"a device reset mid-fade keeps the faded level")
|
||||
|
||||
T.finish("continue_fade_audio_reset_bug2844")
|
||||
@@ -0,0 +1,48 @@
|
||||
-- engine/menus/players_pc.asm:151-173
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.harness")
|
||||
local eq = T.eq
|
||||
love = love or require("tests.love_stub")
|
||||
|
||||
local Font = require("src.render.Font")
|
||||
local ListMenu = require("src.ui.ListMenu")
|
||||
|
||||
local game = { data = { items = {} }, save = {}, input = { wasPressed = function() return false end } }
|
||||
|
||||
local realBox, realDraw, realCode = Font.drawBox, Font.draw, Font.drawCode
|
||||
local boxes
|
||||
Font.drawBox = function(tx, ty) boxes[#boxes + 1] = tx .. "," .. ty end
|
||||
Font.draw = function() end
|
||||
Font.drawCode = function() end
|
||||
|
||||
local list = ListMenu.new(game, nil, { { label = "POTION", count = 1 }, { label = "CANCEL", cancel = true } }, {
|
||||
messageBox = true,
|
||||
footer = "What do you want\nto withdraw?",
|
||||
})
|
||||
|
||||
local function order()
|
||||
boxes = {}
|
||||
list:draw()
|
||||
return table.concat(boxes, " ")
|
||||
end
|
||||
|
||||
eq(order(), "0,12 4,2", "prompt box drawn first, list border on top")
|
||||
list.footer = "How many?"
|
||||
eq(order(), "4,2 0,12", "How many? box drawn over the list")
|
||||
list.footer = "Withdrew\nPOTION."
|
||||
eq(order(), "4,2 0,12", "withdrew box drawn over the list")
|
||||
list.footer = "What do you want\nto withdraw?"
|
||||
eq(order(), "0,12 4,2", "back at the prompt the list is on top again")
|
||||
|
||||
local bag = ListMenu.new(game, "ITEMS", { { label = "CANCEL", cancel = true } }, { itemBox = true })
|
||||
boxes = {}
|
||||
bag:draw()
|
||||
eq(table.concat(boxes, " "), "4,2", "bag list draws no bottom box")
|
||||
bag.footer = "The party is full!"
|
||||
boxes = {}
|
||||
bag:draw()
|
||||
eq(table.concat(boxes, " "), "4,2 0,12", "late message box goes over the list")
|
||||
|
||||
Font.drawBox, Font.draw, Font.drawCode = realBox, realDraw, realCode
|
||||
T.finish("pc item box order bug 2840")
|
||||
@@ -0,0 +1,64 @@
|
||||
-- engine/items/town_map.asm:325
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.harness")
|
||||
local check, eq = T.check, T.eq
|
||||
love = love or require("tests.love_stub")
|
||||
|
||||
local GameVersion = require("src.core.GameVersion")
|
||||
local PaletteFX = require("src.render.PaletteFX")
|
||||
local SpriteRenderer = require("src.render.SpriteRenderer")
|
||||
local TownMap = require("src.ui.TownMap")
|
||||
|
||||
local prevVersion, prevMode = GameVersion.get(), PaletteFX.mode
|
||||
local realObp = SpriteRenderer.obpImage
|
||||
|
||||
local baked = {}
|
||||
SpriteRenderer.obpImage = function(path, colors, group)
|
||||
baked[#baked + 1] = { path = path, colors = colors, group = group }
|
||||
return nil
|
||||
end
|
||||
|
||||
local function game()
|
||||
return {
|
||||
data = {
|
||||
field = {
|
||||
townMap = { PALLET_TOWN = { x = 1, y = 2, name = "PALLET TOWN" } },
|
||||
playerSprites = { walk = "SPRITE_RED" },
|
||||
},
|
||||
sprites = { SPRITE_RED = { image = "assets/generated/sprites/red_walk.png" } },
|
||||
maps = {},
|
||||
},
|
||||
save = {},
|
||||
overworld = { map = { id = "PALLET_TOWN" } },
|
||||
}
|
||||
end
|
||||
|
||||
for _, version in ipairs({ "red", "blue" }) do
|
||||
GameVersion.set(version)
|
||||
PaletteFX.setMode("ogred")
|
||||
baked = {}
|
||||
TownMap.new(game(), {})
|
||||
local bake
|
||||
for _, b in ipairs(baked) do
|
||||
if b.path == "assets/generated/sprites/red_walk.png" then bake = b end
|
||||
end
|
||||
check(bake ~= nil, version .. ": player marker baked")
|
||||
if bake then
|
||||
local want, group = PaletteFX.ogObjNormal()
|
||||
local base = PaletteFX.ogObjBase()
|
||||
eq(bake.group, group, version .. ": marker uses the rOBP0 $D0 bake")
|
||||
for i = 2, 4 do
|
||||
eq(table.concat(bake.colors[i], ","), table.concat(want[i], ","),
|
||||
version .. ": OBJ color " .. (i - 1))
|
||||
end
|
||||
eq(table.concat(bake.colors[2], ","), table.concat(base[1], ","),
|
||||
version .. ": OBJ color 1 is the white shade")
|
||||
end
|
||||
end
|
||||
|
||||
SpriteRenderer.obpImage = realObp
|
||||
GameVersion.set(prevVersion)
|
||||
PaletteFX.setMode(prevMode)
|
||||
|
||||
T.finish("town map OG rOBP0 bug 2841")
|
||||
@@ -0,0 +1,119 @@
|
||||
-- engine/events/whiteout.asm:9-25, engine/events/std_scripts.asm:308-315
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local S = require("tests.harness").suite("gen2 contest whiteout")
|
||||
local check, eq = S.check, S.eq
|
||||
|
||||
local World = require("src.world.gen2.World")
|
||||
local Screens = require("src.ui.Screens")
|
||||
local Events = require("src.world.gen2.Events")
|
||||
local Save = require("src.core.gen2.Save")
|
||||
local BugContest = require("src.core.gen2.BugContest")
|
||||
|
||||
local DATA = {
|
||||
pokemon = {
|
||||
CATERPIE = { id = "CATERPIE", name = "CATERPIE", index = 10, baseExp = 53,
|
||||
growthRate = "MEDIUM_FAST", stats = { hp = 45, attack = 30,
|
||||
defense = 35, speed = 45, specialAttack = 20, specialDefense = 20 },
|
||||
types = { "BUG" } },
|
||||
RATTATA = { id = "RATTATA", name = "RATTATA", index = 19, baseExp = 57,
|
||||
growthRate = "MEDIUM_FAST", stats = { hp = 30, attack = 56,
|
||||
defense = 35, speed = 72, specialAttack = 25, specialDefense = 35 },
|
||||
types = { "NORMAL" } },
|
||||
},
|
||||
moves = {},
|
||||
}
|
||||
|
||||
local function mon(species, level, hp)
|
||||
return { species = species, name = species, nickname = species,
|
||||
level = level, hp = hp, maxHp = 30,
|
||||
stats = { hp = 30, attack = 10, defense = 10, speed = 10,
|
||||
specialAttack = 10, specialDefense = 10 },
|
||||
moves = {}, experience = 0, statExp = {}, dvs = {} }
|
||||
end
|
||||
|
||||
local function contestSave()
|
||||
local save = Save.normalize({ party = { mon("RATTATA", 10, 0) },
|
||||
player = { name = "GOLD", money = 3000 } })
|
||||
BugContest.start(save)
|
||||
return save
|
||||
end
|
||||
|
||||
local function contestWorld(save)
|
||||
local captured, log = {}, {}
|
||||
Screens.invalidate()
|
||||
local game = {
|
||||
data = {
|
||||
pokemon = DATA.pokemon, moves = DATA.moves,
|
||||
screens = {
|
||||
Gen2BattleState = function(_, opts)
|
||||
captured.opts = opts
|
||||
return { screenId = "Gen2BattleState" }
|
||||
end,
|
||||
},
|
||||
},
|
||||
save = save,
|
||||
stack = { push = function() end, pop = function() end },
|
||||
}
|
||||
local world = World.new(game)
|
||||
game.world = world
|
||||
world.map = { def = { id = "NATIONAL_PARK" } }
|
||||
world.maps = { NATIONAL_PARK = world.map.def }
|
||||
world.events = Events.new()
|
||||
world.playBattleMusic = function() end
|
||||
world.battleMusicContext = function() return nil end
|
||||
world.pushBattleTransition = function() return nil end
|
||||
world.restoreMapMusic = function() end
|
||||
world.roamMonsAfterBattle = function() end
|
||||
world.healParty = function() log[#log + 1] = "heal" end
|
||||
world.runMapSetup = function(_, _, fn) log[#log + 1] = "mapsetup" fn() end
|
||||
world.warpToSpawn = function() log[#log + 1] = "spawn" end
|
||||
world.bugContestResults = function() log[#log + 1] = "results" return true end
|
||||
world.bugContestOver = function() log[#log + 1] = "over" return true end
|
||||
world.showText = function(_, _, onDone) if onDone then onDone() end end
|
||||
return world, captured, log
|
||||
end
|
||||
|
||||
local function has(log, entry)
|
||||
for _, e in ipairs(log) do if e == entry then return true end end
|
||||
return false
|
||||
end
|
||||
|
||||
do
|
||||
local save = contestSave()
|
||||
local world, captured, log = contestWorld(save)
|
||||
world:startBattle({ wild = mon("CATERPIE", 7, 20), contest = true })
|
||||
check(captured.opts ~= nil, "a contest encounter pushes the battle screen")
|
||||
captured.opts.onDone("lose")
|
||||
check(has(log, "heal"), "the party is healed")
|
||||
check(has(log, "results"),
|
||||
"a contest wipe runs BugContestResultsWarpScript")
|
||||
check(not has(log, "spawn"), "and does not warp to the last Pokecenter")
|
||||
check(not has(log, "over"), "and does not also run the out-of-balls script")
|
||||
eq(save.player.money, 3000, "nor halve the wallet")
|
||||
end
|
||||
|
||||
do
|
||||
local save = Save.normalize({ party = { mon("RATTATA", 10, 0) },
|
||||
player = { name = "GOLD", money = 3000 } })
|
||||
local world, captured, log = contestWorld(save)
|
||||
world:startBattle({ wild = mon("CATERPIE", 7, 20) })
|
||||
captured.opts.onDone("lose")
|
||||
check(has(log, "spawn"), "outside the contest a wipe still warps home")
|
||||
check(not has(log, "results"), "and never runs the contest results")
|
||||
eq(save.player.money, 1500, "and halves the wallet")
|
||||
end
|
||||
|
||||
do
|
||||
local save = contestSave()
|
||||
local world, _, log = contestWorld(save)
|
||||
world:whiteOut()
|
||||
check(has(log, "results"),
|
||||
"a poison wipe in the park also goes to the contest results")
|
||||
check(not has(log, "spawn"), "rather than to the last Pokecenter")
|
||||
eq(save.player.money, 3000, "and keeps the wallet whole")
|
||||
end
|
||||
|
||||
Screens.invalidate()
|
||||
|
||||
return S.finish()
|
||||
@@ -0,0 +1,60 @@
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
if not _G.love then _G.love = require("tests.love_stub") end
|
||||
|
||||
local S = require("tests.harness").suite("parity Yellow Pikachu under player")
|
||||
local check, eq = S.check, S.eq
|
||||
local Data = require("src.core.Data")
|
||||
Data:load()
|
||||
local NPC = require("src.world.NPC")
|
||||
local Follower = require("src.world.PikachuFollower")
|
||||
|
||||
local spriteDef = Data.sprites and (Data.sprites.SPRITE_PIKACHU or Data.sprites.SPRITE_RED)
|
||||
if not spriteDef then
|
||||
for _, def in pairs(Data.sprites or {}) do spriteDef = def break end
|
||||
end
|
||||
if not spriteDef then
|
||||
print("[skip] parity Yellow Pikachu under player: no sprite table")
|
||||
S.finish()
|
||||
return
|
||||
end
|
||||
|
||||
local game = { data = { sprites = { SPRITE_PIKACHU = spriteDef }, field = {} },
|
||||
save = { party = {} } }
|
||||
local walkable = function() return true end
|
||||
local ow = {
|
||||
map = { id = "ROUTE_1", inBounds = walkable, isWalkableCell = walkable },
|
||||
player = { cellX = 5, cellY = 9, facing = "down" },
|
||||
npcs = {}, entities = {},
|
||||
}
|
||||
|
||||
local restore = Follower.setShouldSpawn(function() return true end)
|
||||
local draws = 0
|
||||
local realDraw = NPC.draw
|
||||
NPC.draw = function() draws = draws + 1 end
|
||||
|
||||
Follower.onMapEntered(game, ow, nil, true)
|
||||
local pika = Follower.current(ow)
|
||||
check(pika, "a map load spawns the follower")
|
||||
eq(pika and pika.cellX, 5, "spawn state 0 puts Pikachu on the player's column")
|
||||
eq(pika and pika.cellY, 9, "spawn state 0 puts Pikachu on the player's row")
|
||||
check(pika and Follower.underPlayer(ow, pika), "the follower counts as under the player")
|
||||
pika:draw(0, 0)
|
||||
eq(draws, 0, "Pikachu on the player's cell draws nothing before the first step")
|
||||
|
||||
ow.player.targetX, ow.player.targetY = 5, 10
|
||||
pika:draw(0, 0)
|
||||
eq(draws, 1, "once the player commits a step off the cell Pikachu is drawn")
|
||||
ow.player.cellX, ow.player.cellY = 5, 10
|
||||
ow.player.targetX, ow.player.targetY = nil, nil
|
||||
pika:draw(0, 0)
|
||||
eq(draws, 2, "and stays drawn one cell behind")
|
||||
|
||||
Follower.onMapEntered(game, ow, { pikachuSpawn = 1 }, true)
|
||||
pika = Follower.current(ow)
|
||||
check(pika and not Follower.underPlayer(ow, pika), "a warp spawn beside the player is not hidden")
|
||||
pika:draw(0, 0)
|
||||
eq(draws, 3, "and draws")
|
||||
|
||||
NPC.draw = realDraw
|
||||
Follower.setShouldSpawn(restore)
|
||||
S.finish()
|
||||
@@ -897,3 +897,5 @@ tests/engine/save_editor_toast_s6.lua
|
||||
tests/engine/launcher_save_toast_s6.lua
|
||||
tests/engine/save_editor_map_point_buttons_s8.lua
|
||||
tests/engine/love_run_shell_matrix.lua
|
||||
tests/engine/pc_item_box_order_bug2840.lua
|
||||
tests/engine/town_map_og_obp0_bug2841.lua
|
||||
|
||||
Reference in New Issue
Block a user