This commit is contained in:
bryanthaboi
2026-07-26 14:03:58 -04:00
parent 3461937dbc
commit b02f8c7daf
9 changed files with 296 additions and 113 deletions
@@ -8,6 +8,8 @@
-- Case 1 (level path, cancelable): open EvolutionState directly, wait a
-- few frames into the flash (t well under FLASH_FRAMES=220), hold B, and
-- assert the mon stays CATERPIE with "stopped evolving" text on screen.
-- Case 1b: after cancel, checkParty with no level-ups must not re-offer;
-- a subsequent level-up set must offer again (EvolveAfterBattle parity).
-- Case 2 (control): let the flash run to completion with no input and
-- assert the mon becomes METAPOD with the "Congratulations!" text.
--
@@ -103,6 +105,27 @@ return function(game)
assert(done1, "cancel onDone never fired")
assert(mon.species == "CATERPIE", "species changed after cancel tail")
-- === Case 1b: after B-cancel, checkParty without a level-up must not
-- re-offer (regression: cancelled mons stuck at threshold tried again
-- after every later battle, even non-participants). ===
local nQuiet = Evolution.checkParty(game, nil, {})
assert(nQuiet == 0, "checkParty with no level-ups re-offered after cancel")
assert(not evoTop(), "EvolutionState opened after quiet afterBattle")
assert(mon.species == "CATERPIE", "species changed on quiet checkParty")
local nLevel = Evolution.checkParty(game, nil, { [mon] = true })
assert(nLevel == 1, "checkParty after a real level-up should offer once")
if not waitFor(evoTop, 300) then
error("EvolutionState never opened after level-up re-offer")
end
U.wait(20)
U.hold(game, "b", 20) -- cancel so case 2 stays independent
if not waitFor(function() return not evoTop() end, 240) then
error("level-up re-offer did not abort on B")
end
mashUntil(function() return not findText("stopped evolving") end, 80)
assert(mon.species == "CATERPIE", "species changed after re-offer cancel")
-- === Case 2 (control): no input -> evolution completes ===
local mon2 = Pokemon.new(game.data, "CATERPIE", 7)
table.insert(game.save.party, 1, mon2)
@@ -0,0 +1,46 @@
-- Driver: party list must not sit under the bottom message box (#262).
-- Gen1 (pokered engine/menus/party_menu.asm RedrawPartyMenu_) places the
-- first name at hlcoord 3, 0 and advances each entry by 2*SCREEN_WIDTH.
-- The recomp used y = (i-1)*16 + 12, which shoved a full party down so
-- slot 6 was clipped by the "Choose a POKéMON." text box (tile row 12).
-- This driver opens a 6-mon party menu, screenshots it, and asserts
-- PartyMenu.entryY keeps slot 6's HP row above y=96.
-- POKEPORT_DRIVER=tests/drivers/party_bug262_offset_test.lua \
-- POKEPORT_IDENTITY=bug262 POKEPORT_TOUCH=0 POKEPORT_VERSION=red love .
return function(game)
local U = dofile("tests/drivers/util.lua")
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
local Pokemon = require("src.pokemon.Pokemon")
local Screens = require("src.ui.Screens")
local PartyMenu = require("src.ui.PartyMenu")
local pass, fail = 0, 0
local function check(label, ok)
if ok then pass = pass + 1; U.log("PASS", label)
else fail = fail + 1; U.log("FAIL", label) end
end
game.save.party = {
Pokemon.new(game.data, "CHARMANDER", 12),
Pokemon.new(game.data, "PARAS", 10),
Pokemon.new(game.data, "DIGLETT", 15),
Pokemon.new(game.data, "FARFETCHD", 5),
Pokemon.new(game.data, "GASTLY", 20),
Pokemon.new(game.data, "SANDSHREW", 22),
}
U.teleport(game, "ROUTE_1", 5, 5, "down")
Screens.push(game, "PartyMenu")
U.wait(8)
U.shot(game, DIR .. "/party_bug262_offset.png")
local pm = game.stack:top()
check("top is PartyMenu", getmetatable(pm) == PartyMenu)
check("entryY(1) == 0", PartyMenu.entryY(1) == 0)
check("entryY(6) == 80", PartyMenu.entryY(6) == 80)
check("slot 6 HP row < message box y=96", PartyMenu.entryY(6) + 8 < 96)
check("field message == 'Choose a POKéMON.'",
pm and pm.bottomMessage and pm:bottomMessage() == "Choose a POKéMON.")
U.log(("RESULT pass=%d fail=%d"):format(pass, fail))
end
+40
View File
@@ -512,6 +512,46 @@ do
"the registered method's own gate holds")
end
-- ------- checkParty only offers evolutions for mons that leveled (#213)
do
local caterpie = Pokemon.new(Data, "CATERPIE", 7) -- at LEVEL evo threshold
local bench = Pokemon.new(Data, "PIDGEY", 5)
local save = SaveData.newGame()
save.party = { caterpie, bench }
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
local game = { data = Data, save = save, stack = stack }
local offered = {}
local origEvolve = Evolution.evolve
Evolution.evolve = function(_, mon, to, onDone)
offered[#offered + 1] = { mon = mon, to = to }
if onDone then onDone() end
end
check(Evolution.checkParty(game) == 0,
"checkParty with no leveledUp set offers nothing")
check(#offered == 0, "no evolve calls without leveledUp")
check(Evolution.checkParty(game, nil, {}) == 0,
"checkParty with empty leveledUp offers nothing")
check(#offered == 0, "cancel-at-threshold does not re-offer next battle")
check(Evolution.checkParty(game, nil, { [bench] = true }) == 0,
"a leveled mon with no pending evo is skipped")
check(#offered == 0, "bench level-up alone does not evolve CATERPIE")
check(Evolution.checkParty(game, nil, { [caterpie] = true }) == 1,
"checkParty offers evo for the mon that leveled")
check(#offered == 1 and offered[1].mon == caterpie and offered[1].to == "METAPOD",
"the leveled CATERPIE is queued for METAPOD")
Evolution.evolve = origEvolve
end
-- ------- the rare-candy flow runs the hook-wrapped dispatch
do
+47 -18
View File
@@ -1845,9 +1845,12 @@ do
eq(cam.y, 160 - (288 / 2 - 8), "wide view keeps player centered y")
end
-- ---------------------------------------------------------------- dpi fit scale (#87)
-- ---------------------------------------------------------------- dpi fit scale (#87 / #208)
-- Android density is often non-integer; fitScale must use framebuffer
-- pixels so each GB pixel maps to a whole number of physical pixels.
-- When axis unit→pixel ratios diverge (truncated highdpi sizes, dual-screen
-- / forced-rotation mismatch), draw scales must be anisotropic so X and Y
-- both land on the same integer physical count (square pixels).
do
local Renderer = require("src.render.Renderer")
local Zoom = require("src.render.Zoom")
@@ -1873,27 +1876,46 @@ do
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")
eq(vw, 276, "world fill width covers the drawable at pixel scale 7")
eq(vh, 156, "world fill height covers the drawable at pixel scale 7")
-- #208: on a dual-screen surface (AYN Thor, forced landscape) LOVE can
-- report drawable == unit size (pw/ww == 1) while its real coordinate->pixel
-- transform is 1.5. fitScale stays keyed off the drawable pixel size, but
-- the unit<->pixel conversion must use the REAL getDPIScale, not pw/ww, so
-- each GB pixel still lands on a WHOLE number of physical pixels (square) --
-- not the stretched 6-vs-9-device-px fractional pixels the reporter saw.
-- Before the fix drawScale()==Sp/(pw/ww)==7 and 7*1.5==10.5 px/GB px.
-- #208: axis DPI mismatch. LOVE's projection uses pw/ww on X and ph/wh
-- on Y independently; getDPIScale() is only ph/wh. A single-dpi draw
-- scale makes one axis fractional → stretched / non-square GB pixels
-- (fonts especially). Truncated density-2.75 unit sizes on 1080p:
local dpiX = 1920 / 698
local dpiY = 1080 / 392
Zoom.reset()
g.getDimensions = function() return 1920, 1080 end
g.getDimensions = function() return 698, 392 end
g.getPixelDimensions = function() return 1920, 1080 end
g.getDPIScale = function() return 1.5 end
g.getDPIScale = function() return dpiY end -- real love.graphics.getDPIScale
eq(Renderer:fitScale(), 7,
"#208 divergent DPI: fitScale still 7 from drawable pixels")
local physical = Renderer:drawScale() * g.getDPIScale()
local rounded = math.floor(physical + 0.5)
check(math.abs(physical - rounded) < 1e-9,
"#208 GB pixel lands on a whole number of physical pixels (square)")
eq(rounded, 7, "#208 each GB pixel covers exactly fitScale (7) physical px")
"#208 anisotropic DPI: fitScale still 7 from drawable pixels")
local physX = Renderer:drawScaleX() * dpiX
local physY = Renderer:drawScaleY() * dpiY
check(math.abs(physX - 7) < 1e-9,
"#208 GB pixel covers exactly fitScale (7) physical px on X")
check(math.abs(physY - 7) < 1e-9,
"#208 GB pixel covers exactly fitScale (7) physical px on Y")
check(math.abs(physX - physY) < 1e-9,
"#208 physical X/Y match (square pixels)")
-- single-dpi (getDPIScale-only) path would stretch X:
-- 7 * dpiX / dpiY ≈ 6.989 ≠ 7
check(math.abs(7 * dpiX / dpiY - 7) > 1e-6,
"#208 fixture really has dpiX ≠ dpiY (guards the regression)")
-- #208 severe case: unit aspect swapped vs drawable (forced-rotation /
-- dual-screen mis-report). Uniform dpi would heavily stretch one axis;
-- anisotropic scales keep 7x7 physical GB pixels.
Zoom.reset()
g.getDimensions = function() return 1080, 1920 end
g.getPixelDimensions = function() return 1920, 1080 end
g.getDPIScale = function() return 1080 / 1920 end
eq(Renderer:fitScale(), 7, "#208 swapped-aspect: fitScale from drawable")
physX = Renderer:drawScaleX() * (1920 / 1080)
physY = Renderer:drawScaleY() * (1080 / 1920)
check(math.abs(physX - 7) < 1e-9 and math.abs(physY - 7) < 1e-9,
"#208 swapped-aspect still yields square 7x7 physical GB pixels")
-- missing pixel API falls back to getDimensions (headless / old stub)
g.getPixelDimensions = nil
@@ -3003,6 +3025,13 @@ eq(frameFor("WATER", true), 3, "WATER animates to the walk frame")
-- icons outside the table keep the old uniform fallback
eq(frameFor("BALL", true, 96), 3, "fallback: 16x96 sheet animates to 3")
eq(frameFor("HELIX", true, 32), 1, "fallback: 16x32 sheet animates to 1")
-- party list Y (pokered party_menu.asm hlcoord 3, 0 + 2-row stride). #262
-- A +12 offset pushed slot 6 into the bottom message box at tile row 12.
local entryY = require("src.ui.PartyMenu").entryY
eq(entryY(1), 0, "party slot 1 name row is y=0")
eq(entryY(6), 80, "party slot 6 name row is y=80")
check(entryY(6) + 8 < 96, "slot 6 HP row stays above the message box (y=96)")
end
-- ---------------------------------------------- suite discovery