mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-17 11:11:10 +02:00
more buggies
This commit is contained in:
@@ -52,6 +52,9 @@ stub.graphics = {
|
||||
push = noop, pop = noop, translate = noop, scale = noop,
|
||||
rotate = noop, origin = noop, setShader = noop, setScissor = noop,
|
||||
getDimensions = function() return 640, 576 end,
|
||||
-- dpi=1 desktop default; issue #87 tests override these for Android density
|
||||
getPixelDimensions = function() return 640, 576 end,
|
||||
getDPIScale = function() return 1 end,
|
||||
}
|
||||
|
||||
stub.math = {
|
||||
|
||||
@@ -128,6 +128,8 @@ love.graphics = {
|
||||
translate = noop, scale = noop, rotate = noop, origin = noop,
|
||||
setScissor = noop, getColor = function() return 1, 1, 1, 1 end,
|
||||
getDimensions = function() return 640, 576 end,
|
||||
getPixelDimensions = function() return 640, 576 end,
|
||||
getDPIScale = function() return 1 end,
|
||||
}
|
||||
|
||||
-- Fresh copies of the modules that cache a compiled shader or a page set
|
||||
|
||||
+49
-1
@@ -21,12 +21,17 @@ check(hof and type(hof.onEnter) == "function", "HALL_OF_FAME.onEnter is a functi
|
||||
|
||||
local champ = init.get("CHAMPIONS_ROOM")
|
||||
check(champ ~= nil, "CHAMPIONS_ROOM map script registered")
|
||||
check(champ and type(champ.onEnter) == "function",
|
||||
"CHAMPIONS_ROOM.onEnter is a function (forced rival entrance)")
|
||||
local rows = champ and champ.talk and champ.talk.TEXT_CHAMPIONSROOM_RIVAL
|
||||
check(type(rows) == "table", "CHAMPIONS_ROOM.talk.TEXT_CHAMPIONSROOM_RIVAL exists")
|
||||
|
||||
-- (2) the rival cutscene rows contain the pokered beats in order
|
||||
rows = rows or {}
|
||||
local preds = {
|
||||
{ "play_music Music_Cities1 (OakArrives / Music_Cities1AlternateTempo)",
|
||||
function(r) return r[1] == "play_music" and r[2] == "Music_Cities1"
|
||||
and type(r[3]) == "table" and r[3].keep == true end },
|
||||
{ "show_object CHAMPIONSROOM_OAK",
|
||||
function(r) return r[1] == "show_object" and r[3] == "CHAMPIONSROOM_OAK" end },
|
||||
{ "move_npc(2,'up',5) OakEntranceAfterVictoryMovement",
|
||||
@@ -82,7 +87,13 @@ check(champToHof, "CHAMPIONS_ROOM has a warp up into HALL_OF_FAME")
|
||||
-- (5) functional: HALL_OF_FAME.onEnter consumes the one-shot marker and
|
||||
-- queues (does not directly run) the room cutscene.
|
||||
local queued
|
||||
local fakeOw = { queueScript = function(self, script, extra) queued = script; self.pendingScript = { script = script } end }
|
||||
local fakeOw = {
|
||||
map = { def = { signs = {} }, widthCells = 10, signAt = {} },
|
||||
queueScript = function(self, script, extra)
|
||||
queued = script
|
||||
self.pendingScript = { script = script }
|
||||
end,
|
||||
}
|
||||
local fakeGame = { save = { pendingHallOfFame = true } }
|
||||
hof.onEnter(fakeGame, fakeOw)
|
||||
check(queued ~= nil, "HALL_OF_FAME.onEnter queues a cutscene script when marker set")
|
||||
@@ -102,4 +113,41 @@ fakeGame.save.pendingHallOfFame = false
|
||||
hof.onEnter(fakeGame, fakeOw)
|
||||
check(queued == nil, "HALL_OF_FAME.onEnter does not replay once the marker is consumed")
|
||||
|
||||
-- (6) Champions Room forced entrance (ChampionsRoomPlayerEntersScript):
|
||||
-- from Lance (y=7), queue RivalEntrance walk then the rival battle script.
|
||||
local champQueued = {}
|
||||
local champOw = {
|
||||
player = { cellX = 3, cellY = 7 },
|
||||
npcs = { { def = { name = "CHAMPIONSROOM_RIVAL" } } },
|
||||
queueScript = function(_, script, extra)
|
||||
champQueued[#champQueued + 1] = { script = script, extra = extra }
|
||||
end,
|
||||
}
|
||||
local champGame = { save = { flags = {} } }
|
||||
champ.onEnter(champGame, champOw)
|
||||
eq(#champQueued, 2, "CHAMPIONS_ROOM.onEnter queues entrance walk + rival script")
|
||||
local walk = champQueued[1] and champQueued[1].script
|
||||
check(walk and walk[1][1] == "move_player" and walk[1][2] == "up" and walk[1][3] == 1
|
||||
and walk[2][1] == "move_player" and walk[2][2] == "right" and walk[2][3] == 1
|
||||
and walk[3][1] == "move_player" and walk[3][2] == "up" and walk[3][3] == 3,
|
||||
"entrance walk is RivalEntrance_RLEMovement (up 1, right 1, up 3)")
|
||||
check(champQueued[2] and champQueued[2].script == rows,
|
||||
"second queue is TEXT_CHAMPIONSROOM_RIVAL script rows")
|
||||
check(champQueued[2] and champQueued[2].extra
|
||||
and champQueued[2].extra.npc == champOw.npcs[1],
|
||||
"rival script receives the CHAMPIONSROOM_RIVAL npc")
|
||||
|
||||
-- already beaten this run: no forced entrance
|
||||
champQueued = {}
|
||||
champGame.save.flags.EVENT_BEAT_CHAMPION_RIVAL_THIS_RUN = true
|
||||
champ.onEnter(champGame, champOw)
|
||||
eq(#champQueued, 0, "CHAMPIONS_ROOM.onEnter idle after champion beaten this run")
|
||||
|
||||
-- Hall of Fame return (y=0) must not re-trigger even if the run flag is clear
|
||||
champQueued = {}
|
||||
champGame.save.flags.EVENT_BEAT_CHAMPION_RIVAL_THIS_RUN = nil
|
||||
champOw.player.cellY = 0
|
||||
champ.onEnter(champGame, champOw)
|
||||
eq(#champQueued, 0, "CHAMPIONS_ROOM.onEnter ignores Hall of Fame return landing")
|
||||
|
||||
S.finish()
|
||||
|
||||
+26
-1
@@ -221,10 +221,35 @@ do
|
||||
check(fe.chooser == nil, "enemy Mimic never opens a chooser")
|
||||
eq(tbe.enemy.curMoves[1].id, "SAND_ATTACK",
|
||||
"enemy Mimic copies a random player move immediately")
|
||||
eq(tbe.enemy.curMoves[1].pp, 9, "enemy Mimic also keeps the slot's PP")
|
||||
-- Gen 1 never decrements enemy PP, so the copied move inherits Mimic's
|
||||
-- full remaining PP (still 10). Player Mimic would leave 9.
|
||||
eq(tbe.enemy.curMoves[1].pp, 10, "enemy Mimic keeps full slot PP (no enemy drain)")
|
||||
check(fe.anim and hasText(fe, "learned"),
|
||||
"enemy Mimic still plays the animation and learned text")
|
||||
|
||||
-- === #94: gen1_faithful enemies never deplete PP; player still does ===
|
||||
do
|
||||
local tb = freshBattle()
|
||||
check(tb.ruleset.enemyUnlimitedPP,
|
||||
"default ruleset grants enemy unlimited PP")
|
||||
local enemyMove = { id = "TACKLE", pp = 5 }
|
||||
local playerMove = { id = "TACKLE", pp = 5 }
|
||||
tb.queue, tb.nextInsert = {}, 0
|
||||
tb:performMove(tb.enemy, tb.player, enemyMove)
|
||||
eq(enemyMove.pp, 5, "gen1_faithful: enemy move PP is not decremented")
|
||||
tb.queue, tb.nextInsert = {}, 0
|
||||
tb:performMove(tb.player, tb.enemy, playerMove)
|
||||
eq(playerMove.pp, 4, "gen1_faithful: player move PP still decrements")
|
||||
|
||||
-- modern_clean tracks enemy PP (Gen 2+ style)
|
||||
local modern = require("src.battle.rulesets.modern_clean")
|
||||
tb.ruleset = modern
|
||||
local enemyModern = { id = "TACKLE", pp = 5 }
|
||||
tb.queue, tb.nextInsert = {}, 0
|
||||
tb:performMove(tb.enemy, tb.player, enemyModern)
|
||||
eq(enemyModern.pp, 4, "modern_clean: enemy move PP decrements")
|
||||
end
|
||||
|
||||
-- link battle: the player's Mimic rolls random too (no chooser)
|
||||
local tbl = freshBattle()
|
||||
tbl.kind = "link"
|
||||
|
||||
+31
-3
@@ -115,11 +115,39 @@ do
|
||||
end
|
||||
|
||||
-- === STRUGGLE fallback when nothing is usable ===
|
||||
-- modern_clean / no unlimited flag: empty PP forces Struggle.
|
||||
do
|
||||
local aiMon = { curMoves = { { id = "TACKLE", pp = 0 } } }
|
||||
local pick = TrainerAI.chooseMove(aiMon, rngLo, { enemyAIMods = { 1 }, data = Data,
|
||||
player = { mon = {}, curTypes = {} } })
|
||||
check(pick and pick.struggle and pick.id == "STRUGGLE", "STRUGGLE fallback when no PP")
|
||||
local pick = TrainerAI.chooseMove(aiMon, rngLo, {
|
||||
enemyAIMods = { 1 }, data = Data,
|
||||
player = { mon = {}, curTypes = {} },
|
||||
ruleset = { enemyUnlimitedPP = false },
|
||||
})
|
||||
check(pick and pick.struggle and pick.id == "STRUGGLE",
|
||||
"STRUGGLE fallback when no PP (enemy PP tracked)")
|
||||
end
|
||||
|
||||
-- gen1_faithful: AI ignores PP; a 0-PP move is still selectable.
|
||||
do
|
||||
local aiMon = { curMoves = { { id = "TACKLE", pp = 0 } } }
|
||||
local pick = TrainerAI.chooseMove(aiMon, rngLo, {
|
||||
enemyAIMods = { 1 }, data = Data,
|
||||
player = { mon = {}, curTypes = {} },
|
||||
ruleset = { enemyUnlimitedPP = true },
|
||||
})
|
||||
eq(pick and pick.id, "TACKLE",
|
||||
"gen1_faithful: enemy still picks 0-PP moves (no Struggle)")
|
||||
end
|
||||
|
||||
-- Disable with unlimited PP: sole disabled move still yields Struggle.
|
||||
do
|
||||
local aiMon = { curMoves = { { id = "TACKLE", pp = 10 } }, disabledSlot = 1 }
|
||||
local pick = TrainerAI.chooseMove(aiMon, rngLo, {
|
||||
enemyAIMods = {}, data = Data,
|
||||
ruleset = { enemyUnlimitedPP = true },
|
||||
})
|
||||
check(pick and pick.struggle and pick.id == "STRUGGLE",
|
||||
"gen1_faithful: Struggle only when every move is disabled")
|
||||
end
|
||||
|
||||
-- === switchAction off-by-one fix (matches AISwitchIfEnoughMons cp 2) ===
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
-- Parity test (#96): blackouts must not play the Dig/Teleport EnterMapAnim.
|
||||
--
|
||||
-- pret HandleBlackOut fades to black and SpecialEnterMap's without setting
|
||||
-- BIT_FLY_WARP / BIT_DUNGEON_WARP, so EnterMap skips EnterMapAnim. Dig,
|
||||
-- Teleport and Escape Rope go through HandleFlyWarpOrDungeonWarp and do
|
||||
-- spin. warpToHealPoint used to always set arriveWarp="teleport", so a
|
||||
-- loss (notably Elite Four -> Indigo lobby) rematerialized with the
|
||||
-- teleport spin instead of a plain whiteout warp.
|
||||
-- Self-contained; run via `luajit tests/parity_blackout_warp.lua`.
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
if not _G.love then _G.love = require("tests.love_stub") end
|
||||
local Data = require("src.core.Data")
|
||||
if not (Data.maps and Data.maps.PALLET_TOWN) then Data:load() end
|
||||
local S = require("tests.harness").suite("parity blackout warp")
|
||||
local check, eq = S.check, S.eq
|
||||
|
||||
require("src.render.Font").load(Data)
|
||||
local Game = require("src.core.Game")
|
||||
local Input = require("src.core.Input")
|
||||
local StateStack = require("src.core.StateStack")
|
||||
local Renderer = require("src.render.Renderer")
|
||||
local SaveData = require("src.core.SaveData")
|
||||
local Pokemon = require("src.pokemon.Pokemon")
|
||||
local OW = require("src.world.OverworldController")
|
||||
|
||||
Game.data = Data
|
||||
Game.input = Input; Input:init()
|
||||
Game.renderer = Renderer; Renderer:init()
|
||||
Game.stack = StateStack; StateStack:init()
|
||||
Game.save = SaveData.newGame()
|
||||
Game.save.party = { Pokemon.new(Data, "SQUIRTLE", 5) }
|
||||
Game.save.party[1].hp = 0
|
||||
Game.save.lastHeal = { map = "INDIGO_PLATEAU_LOBBY", x = 7, y = 6,
|
||||
outdoor = { id = "INDIGO_PLATEAU", x = 9, y = 5 } }
|
||||
|
||||
Game.stack:push(OW, "LORELEIS_ROOM", 4, 5, "up")
|
||||
local ow = Game.stack:top()
|
||||
Game.overworld = ow
|
||||
|
||||
local captured
|
||||
local realStart = ow.startWarpTo
|
||||
ow.startWarpTo = function(self, mapId, x, y, facing, onDone, opts)
|
||||
captured = self.arriveWarp
|
||||
-- skip Transition; only the arrive flag matters here
|
||||
self.arriveWarp = nil
|
||||
self.transitioning = false
|
||||
end
|
||||
|
||||
-- Battle blackout (afterBattle -> warpToHealPoint): no EnterMapAnim.
|
||||
captured = "sentinel"
|
||||
ow:afterBattle("lose", { oppClass = "OPP_LORELEI" })
|
||||
eq(captured, nil,
|
||||
"Elite Four blackout does not set arriveWarp=teleport (#96)")
|
||||
|
||||
-- Poison / field blackout path uses the same helper with no opts.
|
||||
captured = "sentinel"
|
||||
ow.arriveWarp = nil
|
||||
ow:warpToHealPoint()
|
||||
eq(captured, nil, "plain warpToHealPoint has no teleport arrive FX")
|
||||
|
||||
-- Dig / Teleport / Escape Rope keep EnterMapAnim.
|
||||
captured = "sentinel"
|
||||
ow:warpToHealPoint(nil, { arrive = "teleport" })
|
||||
eq(captured, "teleport",
|
||||
"escape warps still request EnterMapAnim on arrival")
|
||||
|
||||
ow.startWarpTo = realStart
|
||||
S.finish()
|
||||
@@ -0,0 +1,157 @@
|
||||
-- Parity: Dig / Fly semi-invulnerable pic hide (#100).
|
||||
-- Dig charge SLIDE_DOWN hides the user; a cancelled Dig release (miss /
|
||||
-- type immunity) must restore the pic; a successful Dig release keeps the
|
||||
-- user hidden through the dirt subanim then emerges via SE_SLIDE_MON_UP
|
||||
-- (not a cyclic bounce). Self-contained: `luajit tests/parity_dig_pic.lua`.
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
if not _G.love then _G.love = require("tests.love_stub") end
|
||||
local Data = require("src.core.Data")
|
||||
if not (Data.maps and Data.maps.PALLET_TOWN) then Data:load() end
|
||||
local Font = require("src.render.Font")
|
||||
Font.load(Data)
|
||||
local S = require("tests.harness").suite("parity dig pic")
|
||||
local check, eq = S.check, S.eq
|
||||
|
||||
local BattleState = require("src.battle.BattleState")
|
||||
local Pokemon = require("src.pokemon.Pokemon")
|
||||
local SaveData = require("src.core.SaveData")
|
||||
local TypeChart = require("src.battle.TypeChart")
|
||||
TypeChart.load(Data)
|
||||
|
||||
local function makeGame(species, level, moves)
|
||||
local save = SaveData.newGame()
|
||||
local mon = Pokemon.new(Data, species, level)
|
||||
mon.moves = moves
|
||||
save.party = { mon }
|
||||
local stack = { states = {} }
|
||||
function stack:push(state) self.states[#self.states + 1] = state end
|
||||
function stack:pop() return table.remove(self.states) end
|
||||
function stack:top() return self.states[#self.states] end
|
||||
return { data = Data, save = save, stack = stack,
|
||||
input = { wasPressed = function() return true end } }
|
||||
end
|
||||
|
||||
local function pumpToMenu(battle)
|
||||
local kinds, anims = {}, {}
|
||||
local guard = 0
|
||||
while guard < 20000 do
|
||||
guard = guard + 1
|
||||
battle.frame = (battle.frame or 0) + 1
|
||||
battle:updateFx()
|
||||
local pf = battle.picFx and battle.picFx[battle.player]
|
||||
if pf and pf.kind and not kinds[pf.kind] then
|
||||
kinds[pf.kind] = true
|
||||
end
|
||||
if battle.animPlaying and battle.animName
|
||||
and anims[#anims] ~= battle.animName then
|
||||
anims[#anims + 1] = battle.animName
|
||||
end
|
||||
if not battle:updateQueue() then
|
||||
if battle.phase == "messages" and battle.afterQueue == "menu"
|
||||
and not battle.animPlaying and not battle.current
|
||||
and #battle.queue == 0 then
|
||||
battle.phase = "menu"
|
||||
break
|
||||
end
|
||||
if battle.phase == "menu" then break end
|
||||
if not battle.animPlaying and not battle.current
|
||||
and #battle.queue == 0 then
|
||||
if battle.afterQueue == "menu" then battle.phase = "menu" end
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
for _ = 1, 200 do
|
||||
battle.frame = battle.frame + 1
|
||||
battle:updateFx()
|
||||
end
|
||||
return kinds, anims
|
||||
end
|
||||
|
||||
local function picHidden(battle)
|
||||
local pf = battle.picFx and battle.picFx[battle.player]
|
||||
return pf and pf.hidden or false
|
||||
end
|
||||
|
||||
-- Dig charge hides; Dig miss on release restores the pic (#100 vanish).
|
||||
do
|
||||
local game = makeGame("SANDSHREW", 40, { { id = "DIG", pp = 10 } })
|
||||
local battle = BattleState.newWild(game, "RATTATA", 5)
|
||||
battle.player.curMoves = game.save.party[1].moves
|
||||
local dig = battle.player.curMoves[1]
|
||||
battle.enemyAction = function() return { id = "TACKLE", pp = 35 } end
|
||||
battle.rng = function(a) return a or 0 end
|
||||
battle:resolveTurn(dig)
|
||||
pumpToMenu(battle)
|
||||
check(battle.player.invulnerable == true, "Dig charge sets invulnerable")
|
||||
check(picHidden(battle), "Dig charge leaves the user pic hidden")
|
||||
|
||||
battle.rng = function(a, b)
|
||||
if a == 0 and b == 255 then return 255 end -- force Dig accuracy miss
|
||||
return a or 0
|
||||
end
|
||||
battle:resolveTurn(dig)
|
||||
local kinds, anims = pumpToMenu(battle)
|
||||
check(not picHidden(battle),
|
||||
"Dig miss on release restores the user pic (#100)")
|
||||
check(not kinds.bounce, "Dig release miss never starts a bounce pic fx")
|
||||
local sawDig = false
|
||||
for _, name in ipairs(anims) do
|
||||
if name == "DIG" then sawDig = true end
|
||||
end
|
||||
check(not sawDig, "Dig miss cancels the DIG release anim")
|
||||
end
|
||||
|
||||
-- Dig hit: stay hidden through DIG start, emerge via slideUp (not bounce).
|
||||
do
|
||||
local game = makeGame("SANDSHREW", 40, { { id = "DIG", pp = 10 } })
|
||||
local battle = BattleState.newWild(game, "SNORLAX", 40)
|
||||
battle.player.curMoves = game.save.party[1].moves
|
||||
local dig = battle.player.curMoves[1]
|
||||
battle.enemyAction = function() return { id = "TACKLE", pp = 35 } end
|
||||
battle.rng = function(a) return a or 0 end
|
||||
battle:resolveTurn(dig)
|
||||
pumpToMenu(battle)
|
||||
|
||||
battle:resolveTurn(dig)
|
||||
local hiddenAtDigStart = nil
|
||||
local kinds = {}
|
||||
local guard = 0
|
||||
while guard < 20000 do
|
||||
guard = guard + 1
|
||||
battle.frame = (battle.frame or 0) + 1
|
||||
battle:updateFx()
|
||||
if battle.animPlaying and battle.animName == "DIG"
|
||||
and hiddenAtDigStart == nil then
|
||||
-- right after DIG row starts (resetPicFx already ran)
|
||||
hiddenAtDigStart = picHidden(battle)
|
||||
end
|
||||
local pf = battle.picFx and battle.picFx[battle.player]
|
||||
if pf and pf.kind then kinds[pf.kind] = true end
|
||||
if not battle:updateQueue() then
|
||||
if battle.phase == "messages" and battle.afterQueue == "menu"
|
||||
and not battle.animPlaying and not battle.current
|
||||
and #battle.queue == 0 then
|
||||
battle.phase = "menu"
|
||||
break
|
||||
end
|
||||
if battle.phase == "menu" then break end
|
||||
if not battle.animPlaying and not battle.current
|
||||
and #battle.queue == 0 then
|
||||
if battle.afterQueue == "menu" then battle.phase = "menu" end
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
for _ = 1, 200 do
|
||||
battle.frame = battle.frame + 1
|
||||
battle:updateFx()
|
||||
end
|
||||
check(hiddenAtDigStart == true,
|
||||
"DIG release keeps the digger hidden until SE_SLIDE_MON_UP")
|
||||
check(kinds.slideUp == true, "Dig release uses slideUp emerge")
|
||||
check(not kinds.bounce, "Dig release must not bounce (#100)")
|
||||
check(not picHidden(battle), "Dig hit leaves the user pic shown")
|
||||
end
|
||||
|
||||
S.finish()
|
||||
@@ -113,6 +113,22 @@ eq(#game2.save.hallOfFame, 1, "winning team recorded (SaveHallOfFameTeams)")
|
||||
local HallOfFame = require("src.ui.HallOfFame")
|
||||
check(getmetatable(stack2:top()) == HallOfFame, "induction showcase pushed")
|
||||
|
||||
-- Gen1 layout (issue #102): pic rests at hlcoord (12,5); mon phase starts
|
||||
-- with the LEVEL/TYPE info box (not a top "HALL OF FAME" banner alone)
|
||||
local hofUi = stack2:top()
|
||||
eq(hofUi.phase, "mons", "induction opens on the mon showcase phase")
|
||||
eq(hofUi.scrollX < 12 * 8, true, "front pic starts off-screen left of (12,5)")
|
||||
-- drive past the scroll so the info box is armed
|
||||
local scrollGuard = 0
|
||||
while hofUi.scrollX < 12 * 8 and scrollGuard < 200 do
|
||||
scrollGuard = scrollGuard + 1
|
||||
hofUi:update(1 / 60)
|
||||
end
|
||||
eq(hofUi.scrollX, 12 * 8, "front pic settles at hlcoord (12,5)")
|
||||
eq(hofUi.showHofBanner, false, "bottom HALL OF FAME banner waits for the 80-frame hold")
|
||||
check(hofUi.timer == 80 or hofUi.timer < 80,
|
||||
"info hold uses the pokered 80 DelayFrames window")
|
||||
|
||||
-- drive induction + full credits with A held (pages are unskippable; A
|
||||
-- only advances the induction and the final THE END wait)
|
||||
pressed.a = true
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
-- Parity test: Lance room walk-in stays on the floor, and defeat dialogue
|
||||
-- includes the rival-became-champion after-battle text.
|
||||
--
|
||||
-- Sources: scripts/LancesRoom.asm (WalkToLance / LancesRoomLanceEndBattleScript),
|
||||
-- text/LancesRoom.asm (_LancesRoomLanceAfterBattleText).
|
||||
-- Self-contained: run via `luajit tests/parity_lance.lua`; also dofile'd
|
||||
-- by tests/run_tests.lua's aggregator.
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
if not _G.love then _G.love = require("tests.love_stub") end
|
||||
local Data = require("src.core.Data")
|
||||
if not (Data.maps and Data.maps.LANCES_ROOM) then Data:load() end
|
||||
local Font = require("src.render.Font")
|
||||
if not pcall(Font.encode, "A") then Font.load(Data) end
|
||||
local S = require("tests.harness").suite("parity lance")
|
||||
local check, eq = S.check, S.eq
|
||||
|
||||
local mapScripts = require("data.scripts.init")
|
||||
local hooks = mapScripts.get("LANCES_ROOM")
|
||||
check(hooks and hooks.walkInRoute, "LANCES_ROOM exposes walkInRoute")
|
||||
|
||||
local D = { up = { 0, -1 }, down = { 0, 1 }, left = { -1, 0 }, right = { 1, 0 } }
|
||||
|
||||
-- === (1) walk-in RLE lands on the door-lock trigger (6,11) ===
|
||||
do
|
||||
local x, y = 24, 16
|
||||
local steps = 0
|
||||
for _, seg in ipairs(hooks.walkInRoute) do
|
||||
local d = D[seg[1]]
|
||||
check(d ~= nil, "walk segment direction " .. tostring(seg[1]))
|
||||
for _ = 1, seg[2] do
|
||||
x, y = x + d[1], y + d[2]
|
||||
steps = steps + 1
|
||||
end
|
||||
end
|
||||
eq(x, 6, "walk-in ends at x=6")
|
||||
eq(y, 11, "walk-in ends at y=11 (door-lock / arena entry)")
|
||||
check(steps > 0, "walk-in has steps")
|
||||
end
|
||||
|
||||
-- === (2) every cell of the route is walkable with the entrance open ===
|
||||
do
|
||||
local Map = require("src.world.Map")
|
||||
local def = Data.maps.LANCES_ROOM
|
||||
local tileset = Data.tilesets[def.tileset]
|
||||
-- mutate a copy of the block list so we don't poison the registry
|
||||
local blocks = {}
|
||||
for i, b in ipairs(def.blocks) do blocks[i] = b end
|
||||
local copy = {}
|
||||
for k, v in pairs(def) do copy[k] = v end
|
||||
copy.blocks = blocks
|
||||
-- LanceShowOrHideEntranceBlocks with the door unlocked
|
||||
blocks[6 * def.width + 2 + 1] = 0x31
|
||||
blocks[6 * def.width + 3 + 1] = 0x32
|
||||
local map = Map.new(copy, tileset)
|
||||
local x, y = 24, 16
|
||||
check(map:isWalkableCell(x, y), "start (24,16) walkable")
|
||||
for _, seg in ipairs(hooks.walkInRoute) do
|
||||
local d = D[seg[1]]
|
||||
for _ = 1, seg[2] do
|
||||
x, y = x + d[1], y + d[2]
|
||||
check(map:isWalkableCell(x, y),
|
||||
("walk-in cell (%d,%d) is floor, not void/wall"):format(x, y))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- === (3) after-battle text names the rival as the real champion ===
|
||||
do
|
||||
local after = Data.text._LancesRoomLanceAfterBattleText
|
||||
check(after ~= nil, "_LancesRoomLanceAfterBattleText extracted")
|
||||
check(after:find("ELITE", 1, true) or after:find("{RIVAL}", 1, true),
|
||||
"after text mentions rival / Elite Four")
|
||||
check(after:find("champion", 1, true) or after:find("CHAMPION", 1, true),
|
||||
"after text has the champion reveal")
|
||||
check(after:find("before you", 1, true) or after:find("before you!", 1, true)
|
||||
or after:find("FOUR before", 1, true),
|
||||
"after text says rival beat the Elite Four first")
|
||||
local header = Data:trainerHeader("LancesRoom", 1)
|
||||
check(header and header.after == "_LancesRoomLanceAfterBattleText",
|
||||
"trainer header wires the after-battle label")
|
||||
check(header and header.won == "_LancesRoomLanceEndBattleText",
|
||||
"trainer header wires the won label")
|
||||
end
|
||||
|
||||
-- === (4) onStep win callback pushes the after text (not only won text) ===
|
||||
do
|
||||
local pushed = {}
|
||||
local game = {
|
||||
data = Data,
|
||||
save = {
|
||||
flags = {},
|
||||
defeatedTrainers = {},
|
||||
player = { name = "RED", rival = "BLUE" },
|
||||
},
|
||||
stack = {
|
||||
push = function(_, state)
|
||||
pushed[#pushed + 1] = state
|
||||
end,
|
||||
},
|
||||
}
|
||||
local lance = {
|
||||
def = { name = "LANCESROOM_LANCE", index = 1,
|
||||
trainerClass = "OPP_LANCE", trainerParty = 1, text = 1 },
|
||||
id = "LANCES_ROOM:1",
|
||||
facePlayer = function() end,
|
||||
}
|
||||
local engaged = false
|
||||
local ow = {
|
||||
npcs = { lance },
|
||||
player = { cellX = 6, cellY = 2 },
|
||||
trainerDefeated = function(_, npc)
|
||||
return game.save.defeatedTrainers[npc.id] == true
|
||||
end,
|
||||
engageTrainer = function(_, npc, onDone)
|
||||
engaged = npc == lance
|
||||
-- simulate engageTrainer's win path: flag the trainer, then onDone
|
||||
game.save.defeatedTrainers[npc.id] = true
|
||||
game.save.flags.EVENT_BEAT_LANCES_ROOM_TRAINER_0 = true
|
||||
if onDone then onDone() end
|
||||
end,
|
||||
}
|
||||
local handled = hooks.onStep(game, ow, 6, 2)
|
||||
check(handled == true, "Lance coord trigger engages")
|
||||
check(engaged, "engageTrainer called for Lance")
|
||||
check(#pushed == 1 and pushed[1].pages ~= nil, "after-battle TextBox pushed")
|
||||
-- TextBox.substitute already expanded {RIVAL}/{PLAYER}; page glyphs
|
||||
-- are opaque, so re-check the source label via the header + that a
|
||||
-- box was queued on win only.
|
||||
check(#pushed[1].pages > 0, "after-battle TextBox has pages")
|
||||
-- loss path must not show after text
|
||||
pushed = {}
|
||||
game.save.defeatedTrainers = {}
|
||||
ow.engageTrainer = function(_, npc, onDone)
|
||||
-- lose: do not mark defeated
|
||||
if onDone then onDone() end
|
||||
end
|
||||
hooks.onStep(game, ow, 6, 2)
|
||||
eq(#pushed, 0, "loss does not push after-battle text")
|
||||
end
|
||||
|
||||
print("parity_lance: ok")
|
||||
@@ -0,0 +1,77 @@
|
||||
-- Regression: map-connection seam steps must show walk frames (issue #93).
|
||||
--
|
||||
-- A hitch inside setMap (neighbor rebuild / map-song start) made the next
|
||||
-- real-time dt huge; FixedStep catch-up then advanced many walk frames
|
||||
-- before the next draw, which looked like a slide with no leg animation.
|
||||
-- crossConnection now discards that catch-up, starts a fresh animClock,
|
||||
-- and defers PlayMapMusic until the seam step lands.
|
||||
--
|
||||
-- Self-contained; run via `luajit tests/parity_seam_walk_anim.lua`.
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
if not _G.love then _G.love = require("tests.love_stub") end
|
||||
|
||||
local Data = require("src.core.Data")
|
||||
if not (Data.maps and Data.maps.PALLET_TOWN) then Data:load() end
|
||||
|
||||
local FixedStep = require("src.core.FixedStep")
|
||||
local Game = require("src.core.Game")
|
||||
local Input = require("src.core.Input")
|
||||
local Music = require("src.core.Music")
|
||||
local Renderer = require("src.render.Renderer")
|
||||
local SaveData = require("src.core.SaveData")
|
||||
local StateStack = require("src.core.StateStack")
|
||||
local OW = require("src.world.OverworldController")
|
||||
local S = require("tests.harness").suite("parity seam walk anim")
|
||||
local check, eq = S.check, S.eq
|
||||
|
||||
Game.data = Data
|
||||
Game.input = Input; Input:init()
|
||||
Game.renderer = Renderer; Renderer:init()
|
||||
Game.stack = StateStack
|
||||
StateStack:init()
|
||||
Game.save = SaveData.newGame()
|
||||
Game.overworld = OW
|
||||
|
||||
while Game.stack:top() do Game.stack:pop() end
|
||||
Game.stack:push(OW, "PALLET_TOWN", 10, 0, "up")
|
||||
local ow = Game.stack:top()
|
||||
local north = ow.map:connection("north")
|
||||
check(north and north.map == "ROUTE_1", "Pallet north connects to ROUTE_1")
|
||||
|
||||
-- simulate a post-hitch catch-up budget waiting in the accumulator
|
||||
FixedStep.accum = 0.24
|
||||
local played = {}
|
||||
local realPlayMap = Music.playMap
|
||||
Music.playMap = function(data, mapId, onBike, surfing)
|
||||
played[#played + 1] = mapId
|
||||
return realPlayMap(data, mapId, onBike, surfing)
|
||||
end
|
||||
|
||||
check(ow:crossConnection("up", north) == true, "Pallet -> Route 1 crosses")
|
||||
eq(ow.map.id, "ROUTE_1", "landed on ROUTE_1")
|
||||
eq(FixedStep.accum, 0, "seam cross discards FixedStep catch-up")
|
||||
eq(ow.pendingSeamMusic, "ROUTE_1", "map music deferred across the seam step")
|
||||
eq(#played, 0, "PlayMapMusic not called inside setMap for the seam")
|
||||
eq(ow.player.animClock, 0, "seam step starts a fresh walk-cycle clock")
|
||||
check(ow.player.moving, "seam step is in progress")
|
||||
|
||||
local sawWalk = false
|
||||
local phases = {}
|
||||
for _ = 1, 20 do
|
||||
ow:update(1 / 60)
|
||||
phases[#phases + 1] = ow.player:walkPhase()
|
||||
if ow.player:walkPhase() == 1 then sawWalk = true end
|
||||
if not ow.player.moving and not ow.pendingSeamMusic then break end
|
||||
end
|
||||
check(sawWalk, "seam step shows at least one walk frame")
|
||||
-- mid-cycle: frames 4..11 of a fresh animClock are walk
|
||||
local midWalk = false
|
||||
for i = 4, 11 do
|
||||
if phases[i] == 1 then midWalk = true break end
|
||||
end
|
||||
check(midWalk, "fresh animClock puts walk frames in the middle of the seam step")
|
||||
eq(ow.pendingSeamMusic, nil, "deferred music flushed after the seam step")
|
||||
eq(played[1], "ROUTE_1", "PlayMapMusic runs once the seam step lands")
|
||||
|
||||
Music.playMap = realPlayMap
|
||||
S.finish()
|
||||
@@ -0,0 +1,125 @@
|
||||
-- Parity: sleep/confusion onomatopoeia on status-check text
|
||||
-- (core.asm CheckPlayerStatusConditions / CheckEnemyStatusConditions).
|
||||
-- Self-contained: `luajit tests/parity_status_onomatopoeia.lua`; also
|
||||
-- dofile'd by tests/run_tests.lua's parity_* aggregator.
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
if not _G.love then _G.love = require("tests.love_stub") end
|
||||
local Data = require("src.core.Data")
|
||||
if not (Data.maps and Data.maps.PALLET_TOWN) then Data:load() end
|
||||
local S = require("tests.harness").suite("parity status onomatopoeia")
|
||||
local check, eq = S.check, S.eq
|
||||
|
||||
local Game = require("src.core.Game")
|
||||
Game.data = Data
|
||||
Game.save = require("src.core.SaveData").newGame()
|
||||
local Pokemon = require("src.pokemon.Pokemon")
|
||||
local BattleState = require("src.battle.BattleState")
|
||||
|
||||
local function freshBattle()
|
||||
Game.save.party = { Pokemon.new(Data, "NIDOKING", 40) }
|
||||
return BattleState.newWild(Game, "DEWGONG", 30)
|
||||
end
|
||||
|
||||
-- Capture queue order of anim/text rows inserted via *Next helpers.
|
||||
local function capture(battle)
|
||||
local seq = {}
|
||||
battle.nextInsert = 0
|
||||
battle.queue = {}
|
||||
battle.animNext = function(_, name, isPlayer)
|
||||
seq[#seq + 1] = { kind = "anim", name = name, isPlayer = isPlayer }
|
||||
end
|
||||
battle.sayNext = function(_, text)
|
||||
seq[#seq + 1] = { kind = "text", text = text }
|
||||
end
|
||||
return seq
|
||||
end
|
||||
|
||||
-- --- sleep: player anim-before-text, enemy text-before-anim -------------
|
||||
do
|
||||
local b = freshBattle()
|
||||
b.rng = function() return 255 end
|
||||
local seq = capture(b)
|
||||
b.player.mon.status = "SLP"
|
||||
b.player.sleepTurns = 3
|
||||
check(b:statusInterrupt(b.player, b.enemy) == true, "sleep interrupts the turn")
|
||||
eq(#seq, 2, "sleep queues anim + text")
|
||||
eq(seq[1].kind, "anim", "player sleep: SLP_PLAYER_ANIM before text")
|
||||
eq(seq[1].name, "SLP_PLAYER_ANIM", "player sleep uses SLP_PLAYER_ANIM")
|
||||
eq(seq[1].isPlayer, true, "player sleep anim faces the player")
|
||||
check(seq[2].text:find("is fast asleep!", 1, true),
|
||||
"player sleep text follows the anim")
|
||||
end
|
||||
|
||||
do
|
||||
local b = freshBattle()
|
||||
b.rng = function() return 255 end
|
||||
local seq = capture(b)
|
||||
b.enemy.mon.status = "SLP"
|
||||
b.enemy.sleepTurns = 3
|
||||
check(b:statusInterrupt(b.enemy, b.player) == true, "enemy sleep interrupts")
|
||||
eq(seq[1].kind, "text", "enemy sleep: FastAsleepText before anim")
|
||||
check(seq[1].text:find("Enemy ", 1, true),
|
||||
"enemy sleep text carries the Enemy prefix")
|
||||
eq(seq[2].name, "SLP_ANIM", "enemy sleep uses SLP_ANIM (enemy Z coords)")
|
||||
eq(seq[2].isPlayer, false, "enemy sleep anim faces the enemy")
|
||||
end
|
||||
|
||||
do
|
||||
local b = freshBattle()
|
||||
local seq = capture(b)
|
||||
b.player.mon.status = "SLP"
|
||||
b.player.sleepTurns = 3
|
||||
check(b:preRechargeChecks(b.player, b.enemy) == true,
|
||||
"pre-recharge sleep still loses the turn")
|
||||
eq(seq[1].name, "SLP_PLAYER_ANIM",
|
||||
"pre-recharge sleep plays the same onomatopoeia")
|
||||
end
|
||||
|
||||
-- --- confusion: text then CONF_*_ANIM (both sides) ---------------------
|
||||
-- Status.beforeMove: rng(0,255) < 128 -> hurt itself; else can still move.
|
||||
do
|
||||
local b = freshBattle()
|
||||
b.rng = function() return 200 end -- no self-hit
|
||||
local seq = capture(b)
|
||||
b.player.confusedTurns = 3
|
||||
b.player.mon.status = nil
|
||||
local stopped = b:statusInterrupt(b.player, b.enemy)
|
||||
check(stopped == false, "confusion can still allow a move")
|
||||
eq(seq[1].kind, "text", "player confusion: IsConfusedText before anim")
|
||||
check(seq[1].text:find("is confused!", 1, true), "player confusion text")
|
||||
eq(seq[2].name, "CONF_PLAYER_ANIM", "player confusion uses CONF_PLAYER_ANIM")
|
||||
eq(seq[2].isPlayer, true, "player confusion anim faces the player")
|
||||
end
|
||||
|
||||
do
|
||||
local b = freshBattle()
|
||||
b.rng = function() return 0 end -- self-hit
|
||||
local seq = capture(b)
|
||||
b.computeDamage = function() return 1 end
|
||||
b.applyDamage = function() end
|
||||
b.onFaint = function() end
|
||||
b.enemy.confusedTurns = 3
|
||||
b.enemy.mon.status = nil
|
||||
local stopped = b:statusInterrupt(b.enemy, b.player)
|
||||
check(stopped == true, "confusion self-hit interrupts")
|
||||
eq(seq[1].kind, "text", "enemy confusion text first")
|
||||
check(seq[1].text:find("is confused!", 1, true), "enemy confusion text")
|
||||
eq(seq[2].name, "CONF_ANIM", "enemy confusion uses CONF_ANIM")
|
||||
eq(seq[2].isPlayer, false, "enemy confusion anim faces the enemy")
|
||||
check(seq[3] and seq[3].text:find("hurt itself", 1, true),
|
||||
"hurt-itself text follows the confusion anim")
|
||||
end
|
||||
|
||||
-- wake stays text-only (no onomatopoeia)
|
||||
do
|
||||
local b = freshBattle()
|
||||
b.rng = function() return 0 end
|
||||
local seq = capture(b)
|
||||
b.player.mon.status = "SLP"
|
||||
b.player.sleepTurns = 1
|
||||
b:statusInterrupt(b.player, b.enemy)
|
||||
eq(#seq, 1, "waking up is text-only")
|
||||
check(seq[1].text:find("woke up!", 1, true), "wake text")
|
||||
end
|
||||
|
||||
S.finish()
|
||||
@@ -0,0 +1,63 @@
|
||||
-- Parity test: Victory Road 3F hole at (23,15) dungeon-warps the player
|
||||
-- to VICTORY_ROAD_2F (22,16).
|
||||
--
|
||||
-- scripts/VictoryRoad3F.asm VictoryRoad3FDefaultScript feeds
|
||||
-- .SwitchOrHoleCoords into IsPlayerOnDungeonWarp with destination
|
||||
-- VICTORY_ROAD_2F; data/maps/special_warps.asm DungeonWarpList entry
|
||||
-- (VICTORY_ROAD_2F, 2) lands at DungeonWarpData (22, 16). The same cell
|
||||
-- also drops a boulder (onBoulderMoved), but the player fall was missing
|
||||
-- -- CAVERN $22 is walkable, so without onStep Red stood on the hole
|
||||
-- (GitHub #86).
|
||||
--
|
||||
-- Self-contained; run via `luajit tests/parity_victory_road_hole.lua`.
|
||||
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 victory road hole")
|
||||
local check, eq = S.check, S.eq
|
||||
|
||||
local M = dofile("data/scripts/story.lua")
|
||||
local vr3 = M.VICTORY_ROAD_3F
|
||||
check(vr3 ~= nil and vr3.onStep ~= nil, "VICTORY_ROAD_3F has an onStep hole trigger")
|
||||
|
||||
local function owRecording()
|
||||
local warps = {}
|
||||
return {
|
||||
player = { facing = "right" },
|
||||
startWarpTo = function(_, mapId, x, y, facing)
|
||||
warps[#warps + 1] = { mapId = mapId, x = x, y = y, facing = facing }
|
||||
end,
|
||||
_warps = warps,
|
||||
}, warps
|
||||
end
|
||||
|
||||
-- Stepping onto the hole falls through to 2F at the dungeon-warp landing.
|
||||
do
|
||||
local ow, warps = owRecording()
|
||||
local handled = vr3.onStep({}, ow, 23, 15)
|
||||
check(handled, "stepping on (23,15) is consumed")
|
||||
eq(#warps, 1, "exactly one dungeon warp fires")
|
||||
eq(warps[1].mapId, "VICTORY_ROAD_2F", "destination is VICTORY_ROAD_2F")
|
||||
eq(warps[1].x, 22, "lands at x=22")
|
||||
eq(warps[1].y, 16, "lands at y=16")
|
||||
eq(warps[1].facing, "right", "facing is preserved across the fall")
|
||||
end
|
||||
|
||||
-- Any other cell is ignored (switch at 3,5 is boulder-only).
|
||||
do
|
||||
local ow, warps = owRecording()
|
||||
eq(vr3.onStep({}, ow, 3, 5), false, "the switch cell does not dungeon-warp")
|
||||
eq(vr3.onStep({}, ow, 22, 15), false, "a neighboring floor cell is ignored")
|
||||
eq(#warps, 0, "no warp fires off the hole")
|
||||
end
|
||||
|
||||
-- The hole collision tile stays walkable (fall, do not block).
|
||||
local Data = require("src.core.Data")
|
||||
if not (Data.maps and Data.maps.VICTORY_ROAD_3F) then Data:load() end
|
||||
local MapLoader = require("src.world.MapLoader")
|
||||
local map = MapLoader.load(Data, "VICTORY_ROAD_3F")
|
||||
check(map:isWalkableCell(23, 15), "CAVERN hole tile at (23,15) is walkable")
|
||||
eq(map:warpPadOrHoleAt(23, 15), "hole", "collision tile is the CAVERN hole ($22)")
|
||||
check(map:warpAtCell(23, 15) == nil,
|
||||
"the hole is a dungeon warp, not a map warp event")
|
||||
|
||||
S.finish()
|
||||
@@ -1659,6 +1659,47 @@ do
|
||||
eq(cam.y, 160 - (288 / 2 - 8), "wide view keeps player centered y")
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------- dpi fit scale (#87)
|
||||
-- Android density is often non-integer; fitScale must use framebuffer
|
||||
-- pixels so each GB pixel maps to a whole number of physical pixels.
|
||||
do
|
||||
local Renderer = require("src.render.Renderer")
|
||||
local Zoom = require("src.render.Zoom")
|
||||
local g = love.graphics
|
||||
local oldDim, oldPix, oldDpi = g.getDimensions, g.getPixelDimensions, g.getDPIScale
|
||||
|
||||
-- desktop / dpi=1: identical to the pre-fix unit-based floor scale
|
||||
g.getDimensions = function() return 1920, 1080 end
|
||||
g.getPixelDimensions = function() return 1920, 1080 end
|
||||
g.getDPIScale = function() return 1 end
|
||||
eq(Renderer:fitScale(), 7, "dpi=1 1080p fitScale is floor(1080/144)=7")
|
||||
|
||||
-- density 1.5 on a 1920x1080 panel → LOVE units 1280x720
|
||||
g.getDimensions = function() return 1280, 720 end
|
||||
g.getPixelDimensions = function() return 1920, 1080 end
|
||||
g.getDPIScale = function() return 1.5 end
|
||||
eq(Renderer:fitScale(), 7,
|
||||
"non-integer density still picks integer framebuffer pixels (7, not 5)")
|
||||
-- old unit-only math would have returned floor(min(1280/160,720/144))=5
|
||||
-- and 5*1.5=7.5 physical px/GB px (shimmer). 7 physical is crisp.
|
||||
|
||||
Zoom.reset()
|
||||
local vw, vh = Renderer:worldViewSize()
|
||||
check(vw % 2 == 0 and vh % 2 == 0, "world view sizes are even (integer camera)")
|
||||
-- ceil(pw/Sp)=ceil(1920/7)=275 → even 276; ceil(1080/7)=155 → even 156
|
||||
eq(vw, 276, "world fill width covers the unit window at pixel scale 7")
|
||||
eq(vh, 156, "world fill height covers the unit window at pixel scale 7")
|
||||
|
||||
-- missing pixel API falls back to getDimensions (headless / old stub)
|
||||
g.getPixelDimensions = nil
|
||||
g.getDPIScale = nil
|
||||
g.getDimensions = function() return 640, 576 end
|
||||
eq(Renderer:fitScale(), 4, "no pixel API: fitScale uses unit dimensions")
|
||||
|
||||
g.getDimensions, g.getPixelDimensions, g.getDPIScale = oldDim, oldPix, oldDpi
|
||||
Zoom.reset()
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------- spawn filter
|
||||
do
|
||||
local OW = require("src.world.OverworldController")
|
||||
@@ -2486,6 +2527,51 @@ do
|
||||
love.graphics.draw = origDraw
|
||||
eq(xs[1], 8, "player/old-man back pic also rests at x=8")
|
||||
end
|
||||
|
||||
-- Front pics: LoadUncompressedSpriteData centers in a 7x7 buffer at
|
||||
-- hlcoord 12,0. A 5x5 (40x40) Squirtle rests at (104,16), not
|
||||
-- right/bottom-aligned to (112,8).
|
||||
do
|
||||
local front = {
|
||||
getWidth = function() return 40 end,
|
||||
getHeight = function() return 40 end,
|
||||
}
|
||||
local battle = setmetatable({
|
||||
showEnemyTrainer = false,
|
||||
enemyHidden = false,
|
||||
enemySendingOut = false,
|
||||
phase = "command",
|
||||
enemy = { sprite = front, isPlayer = false },
|
||||
}, BattleState)
|
||||
function battle:picImage(i) return i end
|
||||
function battle:growInScale() return nil end
|
||||
function battle:fxHidden() return false end
|
||||
function battle:drawBattlerPic(b, x, y, scale)
|
||||
battle._ex, battle._ey, battle._scale = x, y, scale
|
||||
end
|
||||
battle:drawPicsLayer(0, 0, 0)
|
||||
eq(battle._ex, 104, "enemy 5x5 front rests at hlcoord 12,0 + hPad 1 (x=104)")
|
||||
eq(battle._ey, 16, "enemy 5x5 front bottom-aligned in 7x7 (y=16)")
|
||||
eq(battle._scale, 1, "enemy front draws at 1x")
|
||||
end
|
||||
|
||||
-- Battle message lines skip a tile row (14 then 16), matching the menu.
|
||||
do
|
||||
local Font = require("src.render.Font")
|
||||
local ys, origCode, origBox = {}, Font.drawCode, Font.drawBox
|
||||
Font.drawBox = function() end
|
||||
Font.drawCode = function(_, _, y) ys[#ys + 1] = y end
|
||||
local battle = setmetatable({
|
||||
phase = "messages",
|
||||
current = true,
|
||||
charIndex = 999,
|
||||
lines = { { 0x80 }, { 0x81 } },
|
||||
}, BattleState)
|
||||
battle:drawTextArea()
|
||||
Font.drawCode, Font.drawBox = origCode, origBox
|
||||
eq(ys[1], 112, "battle text line 1 at row 14 (y=112)")
|
||||
eq(ys[2], 128, "battle text line 2 at row 16 (y=128)")
|
||||
end
|
||||
end
|
||||
|
||||
-- ================= BUGS.md batch: ledge-shadow =================
|
||||
|
||||
Reference in New Issue
Block a user