mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-28 18:06:09 +02:00
big palette energy
This commit is contained in:
@@ -20,8 +20,8 @@ return function(game)
|
||||
end
|
||||
assert(layoutRow and hudRow, "battle layout/HUD rows are present")
|
||||
|
||||
menu.index = 6
|
||||
menu.scroll = 3
|
||||
-- BATTLE HUD lives on the BATTLE OPTIONS page now, so focus it there.
|
||||
assert(menu:focusRow("battleHud"), "BATTLE HUD row is reachable")
|
||||
assert(hudRow.value(game) == "EXTENDED", "WIDE displays EXTENDED")
|
||||
assert(U.shot(game, BEFORE), "WIDE/EXTENDED screenshot was written")
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
-- Diagnostic probe: forces a loud custom COLORS ramp (hot pink paper, blue
|
||||
-- ink) and shoots StartMenu, PartyMenu, and PackMenu, so a gap between
|
||||
-- those screens and the rest of the UI can be checked by eye.
|
||||
--
|
||||
-- POKEPORT_GAME=gold POKEPORT_DRIVER=tests/drivers/custom_ramp_probe.lua love .
|
||||
local U = require("tests.drivers.util")
|
||||
|
||||
local GbcPalette = require("src.render.GbcPalette")
|
||||
local StartMenu = require("src.ui.gen2.StartMenu")
|
||||
local PartyMenu = require("src.ui.gen2.PartyMenu")
|
||||
local PackMenu = require("src.ui.gen2.PackMenu")
|
||||
|
||||
return function(game)
|
||||
local out = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/custom-ramp-probe"
|
||||
|
||||
local function shot(name)
|
||||
U.wait(3)
|
||||
U.shot(game, ("%s/%s.png"):format(out, name))
|
||||
end
|
||||
|
||||
local function show(name, state)
|
||||
game.stack:push(state)
|
||||
shot(name)
|
||||
game.stack:pop()
|
||||
end
|
||||
|
||||
U.wait(45)
|
||||
assert(game.world and game.world.map, "gold world did not boot")
|
||||
|
||||
print("[driver] GbcPalette.available() = " .. tostring(GbcPalette.available()))
|
||||
|
||||
GbcPalette.setCustomRamp({
|
||||
{ 255, 100, 220 }, { 220, 60, 180 }, { 140, 20, 120 }, { 20, 0, 60 },
|
||||
})
|
||||
print("[driver] customRamp set, mode=" .. tostring(GbcPalette.mode))
|
||||
|
||||
local save = game.save
|
||||
save.party = save.party or {}
|
||||
save.inventory = { POTION = 5, SUPER_POTION = 2, ANTIDOTE = 1 }
|
||||
|
||||
show("01-startmenu-custom-ramp", StartMenu.new(game, { save = save }))
|
||||
show("02-party-custom-ramp", PartyMenu.new(game, { prompt = "choose" }))
|
||||
show("03-pack-items-custom-ramp", PackMenu.new(game, { pocket = "ITEM" }))
|
||||
|
||||
print("[driver] PASS custom ramp probe shots in " .. out)
|
||||
end
|
||||
@@ -28,8 +28,8 @@ return function(game)
|
||||
assert(bgRow.step(game, 1) == false, "AUTO background row is locked")
|
||||
assert(options.battleBg == "black", "locked AUTO does not overwrite the stored value")
|
||||
|
||||
menu.index = bgIndex
|
||||
menu.scroll = math.max(0, bgIndex - 5)
|
||||
-- BATTLE BG lives on the BATTLE OPTIONS page now, so focus it there.
|
||||
assert(menu:focusRow("battleBg"), "BATTLE BG row is reachable")
|
||||
U.wait(2)
|
||||
local autoPath = DIR .. "/fill_extended_auto_menu.png"
|
||||
os.remove(autoPath)
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
-- The grouped Gen 2 OPTION screen: the top level, one page, and a long
|
||||
-- COLOR value scrolling under the cursor.
|
||||
--
|
||||
-- POKEPORT_GAME=gold POKEPORT_DRIVER=tests/drivers/gold_options_groups_shots.lua love .
|
||||
local U = require("tests.drivers.util")
|
||||
|
||||
return function(game)
|
||||
local OptionsMenu = require("src.ui.gen2.OptionsMenu")
|
||||
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
|
||||
U.wait(5)
|
||||
local menu = OptionsMenu.new(game, { options = game.options })
|
||||
game.stack:push(menu)
|
||||
U.wait(3)
|
||||
U.shot(game, DIR .. "/g2_0_top.png")
|
||||
|
||||
local page = menu:focusRow("color")
|
||||
U.wait(2)
|
||||
U.shot(game, DIR .. "/g2_1_graphics.png")
|
||||
game.options.palette = "1-A (Default) (BGB)"
|
||||
U.wait(2)
|
||||
U.shot(game, DIR .. "/g2_2_color_start.png")
|
||||
U.wait(85)
|
||||
U.shot(game, DIR .. "/g2_3_color_scrolled.png")
|
||||
|
||||
page.index = #page.view
|
||||
U.wait(3)
|
||||
U.shot(game, DIR .. "/g2_4_back.png")
|
||||
U.log("GOLD_OPTIONS_GROUPS_DONE")
|
||||
game.driverDone = true
|
||||
end
|
||||
@@ -0,0 +1,33 @@
|
||||
-- Driver: the grouped OPTION screen -- the top level with its group openers,
|
||||
-- one group's page, and the COLORS row scrolling its overlong value.
|
||||
return function(game)
|
||||
local U = dofile("tests/drivers/util.lua")
|
||||
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
|
||||
U.teleport(game, "PALLET_TOWN", 10, 8, "down")
|
||||
local OptionsMenu = require("src.ui.OptionsMenu")
|
||||
local menu = OptionsMenu.new(game)
|
||||
game.stack:push(menu)
|
||||
U.wait(5)
|
||||
U.shot(game, DIR .. "/opt_0_top.png")
|
||||
|
||||
menu:focusRow("group.battle")
|
||||
U.wait(2)
|
||||
U.shot(game, DIR .. "/opt_1_battle_opener.png")
|
||||
U.tap(game, "a"); U.wait(3)
|
||||
U.shot(game, DIR .. "/opt_2_battle_page.png")
|
||||
U.tap(game, "b"); U.wait(3)
|
||||
|
||||
game.save.options.palette = "1-A (Default) (BGB)"
|
||||
-- COLORS is on the GRAPHICS page, so focusRow hands back that page.
|
||||
local graphics = menu:focusRow("colors")
|
||||
U.wait(2)
|
||||
U.shot(game, DIR .. "/opt_3_colors_start.png")
|
||||
U.wait(85)
|
||||
U.shot(game, DIR .. "/opt_4_colors_scrolled.png")
|
||||
|
||||
graphics.index = #graphics.rows + 1 -- the page's own BACK
|
||||
U.wait(3)
|
||||
U.shot(game, DIR .. "/opt_5_back.png")
|
||||
U.log("OPTIONS_GROUPS_SHOTS_DONE")
|
||||
game.driverDone = true
|
||||
end
|
||||
@@ -1,18 +1,21 @@
|
||||
-- Driver: the options screen with the port's audio rows (MUSIC VOL /
|
||||
-- SFX VOL / MUSIC FILTER) to prove the 4-box viewport, the ▼ scroll
|
||||
-- marker, and CANCEL fixed on the bottom line. The menu is pushed
|
||||
-- directly (title-menu row order shifts when a save file exists, so
|
||||
-- blind taps are unreliable).
|
||||
-- marker, and BACK fixed on the bottom line. The audio rows live on the
|
||||
-- AUDIO page now, so the menu focuses its way there rather than tapping
|
||||
-- down a fixed number of times. The menu is pushed directly (title-menu
|
||||
-- row order shifts when a save file exists, so blind taps are unreliable).
|
||||
return function(game)
|
||||
local U = dofile("tests/drivers/util.lua")
|
||||
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
|
||||
U.teleport(game, "PALLET_TOWN", 10, 8, "down")
|
||||
local OptionsMenu = require("src.ui.OptionsMenu")
|
||||
game.stack:push(OptionsMenu.new(game))
|
||||
local menu = OptionsMenu.new(game)
|
||||
game.stack:push(menu)
|
||||
U.wait(5)
|
||||
U.shot(game, DIR .. "/options_0_top.png")
|
||||
for _ = 1, 4 do U.tap(game, "down"); U.wait(2) end
|
||||
U.shot(game, DIR .. "/options_1_musicvol.png") -- scrolled, ▼ visible
|
||||
local audio = menu:focusRow("musicVol")
|
||||
U.wait(2)
|
||||
U.shot(game, DIR .. "/options_1_musicvol.png") -- the AUDIO page
|
||||
U.tap(game, "left"); U.wait(2)
|
||||
U.tap(game, "left"); U.wait(2)
|
||||
U.shot(game, DIR .. "/options_2_musicvol_5.png")
|
||||
@@ -20,6 +23,7 @@ return function(game)
|
||||
U.tap(game, "down"); U.wait(2)
|
||||
U.tap(game, "right"); U.wait(2) -- MUSIC FILTER -> 1X
|
||||
U.shot(game, DIR .. "/options_3_filter_1x.png")
|
||||
U.tap(game, "down"); U.wait(2) -- CANCEL, tail rows behind it
|
||||
U.shot(game, DIR .. "/options_4_cancel.png")
|
||||
audio.index = #audio.view + 1 -- BACK, the page's rows behind it
|
||||
U.wait(2)
|
||||
U.shot(game, DIR .. "/options_4_back.png")
|
||||
end
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
-- The COLORS picker after the palette rename: the root folder list and one
|
||||
-- pack's contents.
|
||||
return function(game)
|
||||
local U = dofile("tests/drivers/util.lua")
|
||||
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
|
||||
U.teleport(game, "PALLET_TOWN", 10, 8, "down")
|
||||
local Screens = require("src.ui.Screens")
|
||||
Screens.push(game, "PaletteScreen")
|
||||
U.wait(5)
|
||||
U.shot(game, DIR .. "/pal_0_root.png")
|
||||
for _ = 1, 6 do U.tap(game, "down"); U.wait(1) end
|
||||
U.shot(game, DIR .. "/pal_1_scrolled.png")
|
||||
U.tap(game, "a"); U.wait(4)
|
||||
U.shot(game, DIR .. "/pal_2_inside.png")
|
||||
U.log("PALETTE_PICKER_SHOTS_DONE")
|
||||
game.driverDone = true
|
||||
end
|
||||
@@ -0,0 +1,52 @@
|
||||
-- BattleAnimView:fillBackground() used to draw a flat literal white
|
||||
-- rectangle with no GbcPalette involvement, leaving a stark white gap
|
||||
-- during the battle intro's slide-in under a custom COLORS ramp.
|
||||
--
|
||||
-- No real shader runs headless, so this asserts on which seam gets called
|
||||
-- rather than on rendered pixels, same as every palette-seam test here.
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.harness")
|
||||
|
||||
love = require("tests.love_stub")
|
||||
|
||||
require("src.core.Logger").warn = function() end
|
||||
|
||||
local GbcPalette = require("src.render.GbcPalette")
|
||||
local BattleAnimView = require("src.ui.gen2.BattleAnimView")
|
||||
|
||||
local calls
|
||||
local function spy(name)
|
||||
local original = GbcPalette[name]
|
||||
GbcPalette[name] = function(...)
|
||||
calls[name] = (calls[name] or 0) + 1
|
||||
return original(...)
|
||||
end
|
||||
end
|
||||
|
||||
do
|
||||
GbcPalette.available = function() return true end
|
||||
GbcPalette.setCustomRamp({ { 255, 0, 255 }, { 200, 0, 200 }, { 100, 0, 100 },
|
||||
{ 0, 0, 0 } })
|
||||
calls = {}
|
||||
spy("resolve"); spy("use"); spy("with")
|
||||
local view = BattleAnimView.new({}, {})
|
||||
view:fillBackground()
|
||||
T.check((calls.resolve or 0) > 0 or (calls.use or 0) > 0 or (calls.with or 0) > 0,
|
||||
"BattleAnimView:fillBackground() reaches the GbcPalette seam")
|
||||
end
|
||||
|
||||
-- Unthemed / no-shader boot must still degrade to the plain literal white
|
||||
-- fill, byte-identical to before this fix.
|
||||
do
|
||||
GbcPalette.available = function() return false end
|
||||
calls = {}
|
||||
spy("resolve"); spy("use"); spy("with")
|
||||
local view = BattleAnimView.new({}, {})
|
||||
local ok = pcall(function() view:fillBackground() end)
|
||||
T.check(ok, "BattleAnimView:fillBackground() does not error with no shader")
|
||||
T.eq((calls.resolve or 0) + (calls.use or 0) + (calls.with or 0), 0,
|
||||
"BattleAnimView:fillBackground() does not touch GbcPalette with no shader")
|
||||
end
|
||||
|
||||
T.finish()
|
||||
@@ -0,0 +1,58 @@
|
||||
-- BattleState:drawTextArea drew the FIGHT/ITEM/RUN command box with a flat
|
||||
-- setColor, the same gap TextBox.lua had. Gen 1 gets colour from
|
||||
-- PaletteFX's whole-frame remap, but Gen 2 has no such pass outside
|
||||
-- CLASSIC mode, so the command box needed its own fix.
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.harness")
|
||||
|
||||
love = require("tests.love_stub")
|
||||
|
||||
require("src.core.Logger").warn = function() end
|
||||
|
||||
local GbcPalette = require("src.render.GbcPalette")
|
||||
local BattleState = require("src.battle.BattleState")
|
||||
|
||||
GbcPalette.available = function() return true end
|
||||
GbcPalette.setCustomRamp({ { 255, 0, 255 }, { 200, 0, 200 }, { 100, 0, 100 },
|
||||
{ 0, 0, 0 } })
|
||||
|
||||
local calls
|
||||
local function spy(name)
|
||||
local original = GbcPalette[name]
|
||||
GbcPalette[name] = function(...)
|
||||
calls[name] = (calls[name] or 0) + 1
|
||||
return original(...)
|
||||
end
|
||||
end
|
||||
|
||||
local function fakeState()
|
||||
return {
|
||||
phase = "menu", menuIndex = 1, safari = nil, demo = false,
|
||||
bottomUIVisible = function() return true end,
|
||||
game = { save = { generation = 2, version = "gold" } },
|
||||
}
|
||||
end
|
||||
|
||||
do
|
||||
calls = {}
|
||||
spy("resolve"); spy("use"); spy("with")
|
||||
BattleState.drawTextArea(fakeState())
|
||||
T.check((calls.resolve or 0) > 0 or (calls.use or 0) > 0 or (calls.with or 0) > 0,
|
||||
"Gen 2 battle command box reaches the GbcPalette seam")
|
||||
end
|
||||
|
||||
-- Gen 1 recolours through PaletteFX's whole-frame zone remap, not per draw
|
||||
-- call, so this must stay untouched. Same guard gen2_textbox_palette_test
|
||||
-- checks for TextBox.
|
||||
do
|
||||
calls = {}
|
||||
spy("resolve"); spy("use"); spy("with")
|
||||
local state = fakeState()
|
||||
state.game = { save = { generation = 1, version = "red" } }
|
||||
BattleState.drawTextArea(state)
|
||||
T.eq((calls.resolve or 0) + (calls.use or 0) + (calls.with or 0), 0,
|
||||
"Gen 1 battle command box is untouched, it never calls GbcPalette")
|
||||
end
|
||||
|
||||
T.finish()
|
||||
@@ -0,0 +1,69 @@
|
||||
-- src/ui/gen2/BattleState.lua is the real Gen 2 battle screen (wired in
|
||||
-- through Gen2Compat.lua's facade); it drew every label through flat
|
||||
-- Chrome.print/cursor/printRight, never touching GbcPalette. Added
|
||||
-- Chrome.printRightThrough alongside this fix since printRight had no
|
||||
-- palette-aware counterpart.
|
||||
--
|
||||
-- No real shader runs headless, so this asserts on which seam gets called
|
||||
-- rather than on rendered pixels. Exercises drawMoveInfoBox, drawStatsBox,
|
||||
-- and drawPanel's no-battle fallback as representative cases.
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.harness")
|
||||
|
||||
love = require("tests.love_stub")
|
||||
|
||||
require("src.core.Logger").warn = function() end
|
||||
|
||||
local GbcPalette = require("src.render.GbcPalette")
|
||||
local BattleState = require("src.ui.gen2.BattleState")
|
||||
|
||||
GbcPalette.available = function() return true end
|
||||
GbcPalette.setCustomRamp({ { 255, 0, 255 }, { 200, 0, 200 }, { 100, 0, 100 },
|
||||
{ 0, 0, 0 } })
|
||||
|
||||
local calls
|
||||
local function spy(name)
|
||||
local original = GbcPalette[name]
|
||||
GbcPalette[name] = function(...)
|
||||
calls[name] = (calls[name] or 0) + 1
|
||||
return original(...)
|
||||
end
|
||||
end
|
||||
|
||||
local function hits()
|
||||
return (calls.resolve or 0) + (calls.use or 0) + (calls.with or 0)
|
||||
end
|
||||
|
||||
do
|
||||
calls = {}
|
||||
spy("resolve"); spy("use"); spy("with")
|
||||
BattleState.drawMoveInfoBox({}, { id = "TACKLE", pp = 10, maxPp = 35 })
|
||||
T.check(hits() > 0, "drawMoveInfoBox's TYPE/PP labels reach the GbcPalette seam")
|
||||
end
|
||||
|
||||
do
|
||||
calls = {}
|
||||
spy("resolve"); spy("use"); spy("with")
|
||||
BattleState.drawMoveInfoBox({}, nil)
|
||||
T.eq(hits(), 0, "drawMoveInfoBox with no move draws nothing")
|
||||
end
|
||||
|
||||
do
|
||||
local mon = { species = "RATTATA",
|
||||
stats = { attack = 10, defense = 9, specialAttack = 8, specialDefense = 8,
|
||||
speed = 12 } }
|
||||
calls = {}
|
||||
spy("resolve"); spy("use"); spy("with")
|
||||
BattleState.drawStatsBox({}, mon)
|
||||
T.check(hits() > 0, "drawStatsBox's stat names/numbers reach the GbcPalette seam")
|
||||
end
|
||||
|
||||
do
|
||||
calls = {}
|
||||
spy("resolve"); spy("use"); spy("with")
|
||||
BattleState.drawPanel({ battle = nil, tutorial = false })
|
||||
T.check(hits() > 0, "drawPanel's NO BATTLE fallback reaches the GbcPalette seam")
|
||||
end
|
||||
|
||||
T.finish()
|
||||
@@ -0,0 +1,56 @@
|
||||
-- Chrome.clear() is the base background wipe ~30 opaque Gen 2 menu screens
|
||||
-- start their frame with. It used to draw a flat literal white rectangle,
|
||||
-- never touching GbcPalette, so every screen's background stayed white
|
||||
-- regardless of a picked COLORS palette even though its boxes and text
|
||||
-- already recoloured correctly.
|
||||
--
|
||||
-- No real shader runs headless, so this can't check a rendered pixel, same
|
||||
-- limitation gen2_textbox_palette_test notes: force GbcPalette.available()
|
||||
-- true and assert on which seam gets called, not on pixels.
|
||||
-- tests/drivers/gold_menu_shots.lua is where a real rendered screen gets
|
||||
-- checked by eye.
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.harness")
|
||||
|
||||
love = require("tests.love_stub")
|
||||
|
||||
require("src.core.Logger").warn = function() end
|
||||
|
||||
local GbcPalette = require("src.render.GbcPalette")
|
||||
local Chrome = require("src.ui.gen2.Chrome")
|
||||
|
||||
local calls
|
||||
local function spy(name)
|
||||
local original = GbcPalette[name]
|
||||
GbcPalette[name] = function(...)
|
||||
calls[name] = (calls[name] or 0) + 1
|
||||
return original(...)
|
||||
end
|
||||
end
|
||||
|
||||
do
|
||||
GbcPalette.available = function() return true end
|
||||
GbcPalette.setCustomRamp({ { 255, 0, 255 }, { 200, 0, 200 }, { 100, 0, 100 },
|
||||
{ 0, 0, 0 } })
|
||||
calls = {}
|
||||
spy("resolve"); spy("use"); spy("with")
|
||||
Chrome.clear()
|
||||
T.check((calls.resolve or 0) > 0 or (calls.use or 0) > 0 or (calls.with or 0) > 0,
|
||||
"Chrome.clear() reaches the GbcPalette seam when a shader is available")
|
||||
end
|
||||
|
||||
-- Unthemed / no-shader boot must still degrade to the plain literal white
|
||||
-- wipe, byte-identical to before this fix, same guard Chrome.paletteBox
|
||||
-- already takes.
|
||||
do
|
||||
GbcPalette.available = function() return false end
|
||||
calls = {}
|
||||
spy("resolve"); spy("use"); spy("with")
|
||||
local ok = pcall(Chrome.clear)
|
||||
T.check(ok, "Chrome.clear() does not error with no shader available")
|
||||
T.eq((calls.resolve or 0) + (calls.use or 0) + (calls.with or 0), 0,
|
||||
"Chrome.clear() does not touch GbcPalette when no shader is available")
|
||||
end
|
||||
|
||||
T.finish()
|
||||
@@ -0,0 +1,73 @@
|
||||
-- Chrome.print / Chrome.cursor / Chrome.printRight are the flat,
|
||||
-- hardcoded-black primitives ~40 Gen 2 screens still call directly, so any
|
||||
-- glyph through them rendered solid black regardless of COLORS. Made the
|
||||
-- primitives themselves palette-aware instead of migrating 40 files.
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.harness")
|
||||
|
||||
love = require("tests.love_stub")
|
||||
|
||||
require("src.core.Logger").warn = function() end
|
||||
|
||||
local GbcPalette = require("src.render.GbcPalette")
|
||||
local Chrome = require("src.ui.gen2.Chrome")
|
||||
|
||||
local calls
|
||||
local function spy(name)
|
||||
local original = GbcPalette[name]
|
||||
GbcPalette[name] = function(...)
|
||||
calls[name] = (calls[name] or 0) + 1
|
||||
return original(...)
|
||||
end
|
||||
end
|
||||
|
||||
local function reset()
|
||||
calls = {}
|
||||
spy("resolve"); spy("use"); spy("useRaw"); spy("with")
|
||||
end
|
||||
|
||||
local function touched()
|
||||
return (calls.resolve or 0) + (calls.use or 0) + (calls.useRaw or 0) + (calls.with or 0)
|
||||
end
|
||||
|
||||
do
|
||||
GbcPalette.available = function() return true end
|
||||
GbcPalette.setCustomRamp({ { 255, 0, 255 }, { 200, 0, 200 }, { 100, 0, 100 },
|
||||
{ 0, 0, 0 } })
|
||||
|
||||
reset()
|
||||
Chrome.print("HELLO", 1, 1)
|
||||
T.check(touched() > 0, "Chrome.print() reaches the GbcPalette seam when a shader is available")
|
||||
|
||||
reset()
|
||||
Chrome.cursor(1, 1)
|
||||
T.check(touched() > 0, "Chrome.cursor() reaches the GbcPalette seam when a shader is available")
|
||||
|
||||
reset()
|
||||
Chrome.printRight("42", 10, 1)
|
||||
T.check(touched() > 0, "Chrome.printRight() reaches the GbcPalette seam when a shader is available")
|
||||
end
|
||||
|
||||
-- Unthemed / no-shader boot must still degrade to the plain literal black
|
||||
-- glyph draw, byte-identical to before this fix.
|
||||
do
|
||||
GbcPalette.available = function() return false end
|
||||
|
||||
reset()
|
||||
local ok = pcall(Chrome.print, "HELLO", 1, 1)
|
||||
T.check(ok, "Chrome.print() does not error with no shader available")
|
||||
T.eq(touched(), 0, "Chrome.print() does not touch GbcPalette when no shader is available")
|
||||
|
||||
reset()
|
||||
ok = pcall(Chrome.cursor, 1, 1)
|
||||
T.check(ok, "Chrome.cursor() does not error with no shader available")
|
||||
T.eq(touched(), 0, "Chrome.cursor() does not touch GbcPalette when no shader is available")
|
||||
|
||||
reset()
|
||||
ok = pcall(Chrome.printRight, "42", 10, 1)
|
||||
T.check(ok, "Chrome.printRight() does not error with no shader available")
|
||||
T.eq(touched(), 0, "Chrome.printRight() does not touch GbcPalette when no shader is available")
|
||||
end
|
||||
|
||||
T.finish()
|
||||
@@ -0,0 +1,64 @@
|
||||
-- Chrome.List:draw() backs nearly every Gen 2 menu's row text, but drew
|
||||
-- each row through flat, hardcoded-black Chrome.print/cursor, so rows
|
||||
-- stayed black-on-white even though the box behind them recoloured. Fixed
|
||||
-- by giving List a `palette` option that routes rows through
|
||||
-- printThrough/cursorThrough.
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.harness")
|
||||
|
||||
love = require("tests.love_stub")
|
||||
|
||||
require("src.core.Logger").warn = function() end
|
||||
|
||||
local GbcPalette = require("src.render.GbcPalette")
|
||||
local Chrome = require("src.ui.gen2.Chrome")
|
||||
|
||||
GbcPalette.available = function() return true end
|
||||
GbcPalette.setCustomRamp({ { 255, 0, 255 }, { 200, 0, 200 }, { 100, 0, 100 },
|
||||
{ 0, 0, 0 } })
|
||||
|
||||
local calls
|
||||
local function spy(name)
|
||||
local original = GbcPalette[name]
|
||||
GbcPalette[name] = function(...)
|
||||
calls[name] = (calls[name] or 0) + 1
|
||||
return original(...)
|
||||
end
|
||||
end
|
||||
|
||||
do
|
||||
local list = Chrome.List.new({ items = { "POKEMON", "PACK", "QUIT" }, x = 2, y = 2 })
|
||||
calls = {}
|
||||
spy("resolve"); spy("use"); spy("with")
|
||||
list:draw()
|
||||
T.check((calls.resolve or 0) > 0 or (calls.use or 0) > 0 or (calls.with or 0) > 0,
|
||||
"Chrome.List:draw() reaches the GbcPalette seam by default")
|
||||
end
|
||||
|
||||
-- A caller that explicitly opts out (palette = false) keeps the old flat
|
||||
-- black draw untouched by GbcPalette.
|
||||
do
|
||||
local list = Chrome.List.new({ items = { "A", "B" }, x = 2, y = 2, palette = false })
|
||||
calls = {}
|
||||
spy("resolve"); spy("use"); spy("with")
|
||||
list:draw()
|
||||
T.eq((calls.resolve or 0) + (calls.use or 0) + (calls.with or 0), 0,
|
||||
"Chrome.List:draw() with palette = false does not touch GbcPalette")
|
||||
end
|
||||
|
||||
-- The scroll-hint down arrow, drawn only when the list is longer than its
|
||||
-- visible rows, must also go through the palette rather than a bare
|
||||
-- Font.drawCode.
|
||||
do
|
||||
local items = {}
|
||||
for i = 1, 5 do items[i] = "ITEM " .. i end
|
||||
local list = Chrome.List.new({ items = items, x = 2, y = 2, rows = 2 })
|
||||
calls = {}
|
||||
spy("resolve"); spy("use"); spy("with")
|
||||
list:draw()
|
||||
T.check((calls.resolve or 0) > 0 or (calls.use or 0) > 0 or (calls.with or 0) > 0,
|
||||
"Chrome.List:draw()'s scroll-hint arrow reaches the GbcPalette seam")
|
||||
end
|
||||
|
||||
T.finish()
|
||||
@@ -0,0 +1,109 @@
|
||||
-- World's per-map colour bake keyed its cache on GbcPalette.mode alone, so
|
||||
-- two different picked packs (both mode == "custom") kept showing
|
||||
-- whichever was baked first until a full reload.
|
||||
-- luajit tests/engine/gen2_color_cache_invalidation_test.lua
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
if not _G.love then _G.love = require("tests.love_stub") end
|
||||
|
||||
local T = require("tests.harness")
|
||||
local check, eq = T.check, T.eq
|
||||
|
||||
local GbcPalette = require("src.render.GbcPalette")
|
||||
local BorderFill = require("src.world.gen2.BorderFill")
|
||||
local World = require("src.world.gen2.World")
|
||||
|
||||
GbcPalette.setMode("gbc")
|
||||
GbcPalette.setCustomRamp(nil)
|
||||
|
||||
-- ------- mapCacheKey: must depend on customRamp identity, not just mode
|
||||
|
||||
do
|
||||
local fakeSelf = { daytime = "DAY", flickerPhase = 1 }
|
||||
local key1 = World.mapCacheKey(fakeSelf, "map1")
|
||||
|
||||
GbcPalette.setCustomRamp(
|
||||
{ { 1, 1, 1 }, { 2, 2, 2 }, { 3, 3, 3 }, { 4, 4, 4 } })
|
||||
local key2 = World.mapCacheKey(fakeSelf, "map1")
|
||||
|
||||
check(key1 ~= key2,
|
||||
"BUG FIX: mapCacheKey differs once a custom ramp is active, even " ..
|
||||
"though GbcPalette.mode ('gbc') never changed")
|
||||
|
||||
GbcPalette.setCustomRamp(
|
||||
{ { 9, 9, 9 }, { 8, 8, 8 }, { 7, 7, 7 }, { 6, 6, 6 } })
|
||||
local key3 = World.mapCacheKey(fakeSelf, "map1")
|
||||
check(key2 ~= key3,
|
||||
"BUG FIX: mapCacheKey differs again for a second, different ramp " ..
|
||||
"under the same mode")
|
||||
|
||||
GbcPalette.setCustomRamp(nil)
|
||||
eq(World.mapCacheKey(fakeSelf, "map1"), key1,
|
||||
"clearing the ramp returns to the original no-ramp key")
|
||||
end
|
||||
|
||||
-- ------- refreshColorMode: must refetch on a customRamp-only change
|
||||
|
||||
do
|
||||
local fakeSelf = { daytime = "DAY", flickerPhase = 1 }
|
||||
local calls = 0
|
||||
fakeSelf.imageFor = function(_, id) calls = calls + 1; return "img:" .. id end
|
||||
fakeSelf.rebuildNeighbors = function() end
|
||||
fakeSelf.map = { id = "map1" }
|
||||
|
||||
World.refreshColorMode(fakeSelf)
|
||||
eq(calls, 1, "first refreshColorMode call bakes/fetches once")
|
||||
|
||||
World.refreshColorMode(fakeSelf)
|
||||
eq(calls, 1, "a second call with nothing changed does not refetch")
|
||||
|
||||
GbcPalette.setCustomRamp(
|
||||
{ { 5, 5, 5 }, { 4, 4, 4 }, { 3, 3, 3 }, { 2, 2, 2 } })
|
||||
World.refreshColorMode(fakeSelf)
|
||||
eq(calls, 2,
|
||||
"BUG FIX: a customRamp change alone (mode unchanged) still refetches")
|
||||
|
||||
GbcPalette.setCustomRamp(nil)
|
||||
World.refreshColorMode(fakeSelf)
|
||||
eq(calls, 3, "clearing the ramp (mode still unchanged) refetches too")
|
||||
end
|
||||
|
||||
-- ------- World:borderImageFor: the void/border bake had the identical gap
|
||||
-- in its own separately-inlined cache key (it doesn't call mapCacheKey). A
|
||||
-- border baked while GEN 2 (no ramp) was active stayed cached forever,
|
||||
-- since picking a pack never showed up in this key, only the mode did.
|
||||
|
||||
do
|
||||
local def = { id = "map1", tileset = "TILESET_JOHTO", borderBlock = 0 }
|
||||
local fakeSelf = {
|
||||
daytime = "DAY", flickerPhase = 1,
|
||||
maps = { map1 = def },
|
||||
tilesets = { TILESET_JOHTO = { blocks = { [1] = {} }, tilesPerRow = 16 } },
|
||||
mapImages = {},
|
||||
borderWaterFrame = function() return nil end,
|
||||
atlasFor = function() return nil end,
|
||||
}
|
||||
|
||||
GbcPalette.setCustomRamp(nil)
|
||||
-- Seed the cache under exactly the key borderImageFor computes today for
|
||||
-- "no ramp active", with a sentinel, proving the fix without touching
|
||||
-- the real love.graphics bake pipeline.
|
||||
local keyNoRamp =
|
||||
BorderFill.cacheKey("map1|DAY|gbc|nil|1|fade|0|0")
|
||||
fakeSelf.mapImages[keyNoRamp] = "SENTINEL-no-ramp"
|
||||
eq(World.borderImageFor(fakeSelf, "map1"), "SENTINEL-no-ramp",
|
||||
"sanity: the no-ramp key matches what borderImageFor builds today")
|
||||
|
||||
GbcPalette.setCustomRamp(
|
||||
{ { 1, 1, 1 }, { 2, 2, 2 }, { 3, 3, 3 }, { 4, 4, 4 } })
|
||||
local got = World.borderImageFor(fakeSelf, "map1")
|
||||
check(got ~= "SENTINEL-no-ramp",
|
||||
"BUG FIX: borderImageFor must not reuse the no-ramp cached void " ..
|
||||
"bake once a custom ramp is active")
|
||||
|
||||
GbcPalette.setCustomRamp(nil)
|
||||
end
|
||||
|
||||
GbcPalette.setCustomRamp(nil)
|
||||
GbcPalette.setMode("gbc")
|
||||
|
||||
T.finish("gen2 color cache invalidation")
|
||||
@@ -54,13 +54,41 @@ local function rowIndex(rows, id)
|
||||
end
|
||||
end
|
||||
|
||||
-- TEXT SPEED and the other cart rows live on a group page now, so the
|
||||
-- screen a row draws on is the page its group opens, not the top level.
|
||||
local function pageFor(menu, id)
|
||||
for _, row in ipairs(menu.view) do
|
||||
if row.group then
|
||||
row.activate(menu.game)
|
||||
local page = menu.game.stack:top()
|
||||
if rowIndex(page.view, id) then return page end
|
||||
menu.game.stack:pop()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function fakeGame()
|
||||
local stack = { items = {} }
|
||||
function stack:push(s) self.items[#self.items + 1] = s; return s end
|
||||
function stack:pop() self.items[#self.items] = nil end
|
||||
function stack:top() return self.items[#self.items] end
|
||||
return { stack = stack }
|
||||
end
|
||||
|
||||
-- ---------------------------------------------- vanilla: no mod catalog
|
||||
do
|
||||
local menu = OptionsMenu.new({})
|
||||
local menu = OptionsMenu.new(fakeGame())
|
||||
drawn = {}
|
||||
menu:drawPanel()
|
||||
T.eq(drawnAt(LABEL_X, 2 * 8), "SPEED",
|
||||
"row 1 is the SPEED group with no mod loaded")
|
||||
|
||||
local page = pageFor(menu, "textSpeed")
|
||||
T.check(page ~= nil, "TEXT SPEED is on a group page")
|
||||
drawn = {}
|
||||
page:drawPanel()
|
||||
T.eq(drawnAt(LABEL_X, 2 * 8), "TEXT SPEED",
|
||||
"row 1's label draws in English with no mod loaded")
|
||||
"the page's first label draws in English")
|
||||
-- Save.DEFAULT_OPTIONS.textSpeed is "MID", the cart's own default.
|
||||
T.eq(drawnAt(VALUE_X, 3 * 8), "MID ",
|
||||
"and its cart-original display value too")
|
||||
@@ -73,13 +101,20 @@ do
|
||||
["TEXT SPEED"] = "VITESSE TEXTE",
|
||||
["MID "] = "MOY ",
|
||||
["CONTROLS"] = "COMMANDES",
|
||||
["CANCEL"] = "ANNULER",
|
||||
["SPEED"] = "VITESSE",
|
||||
["BACK"] = "RETOUR",
|
||||
},
|
||||
})
|
||||
|
||||
local menu = OptionsMenu.new({})
|
||||
local menu = OptionsMenu.new(fakeGame())
|
||||
drawn = {}
|
||||
menu:drawPanel()
|
||||
T.eq(drawnAt(LABEL_X, 2 * 8), "VITESSE",
|
||||
"a mod catalog reaches a group opener's label too")
|
||||
|
||||
local page = pageFor(menu, "textSpeed")
|
||||
drawn = {}
|
||||
page:drawPanel()
|
||||
T.eq(drawnAt(LABEL_X, 2 * 8), "VITESSE TEXTE",
|
||||
"a mod catalog reaches a cart-original row's label")
|
||||
T.eq(drawnAt(VALUE_X, 3 * 8), "MOY ",
|
||||
@@ -87,7 +122,7 @@ do
|
||||
|
||||
-- CONTROLS is the first port-added row; scroll to it so it lands in the
|
||||
-- VISIBLE_ROWS=7 window drawPanel actually draws.
|
||||
local index = rowIndex(menu.rows, "controls")
|
||||
local index = rowIndex(menu.view, "controls")
|
||||
T.check(index ~= nil, "CONTROLS is one of the rows")
|
||||
menu.index = index
|
||||
menu:ensureVisible()
|
||||
@@ -97,17 +132,17 @@ do
|
||||
T.eq(drawnAt(LABEL_X, (2 + (slot - 1) * 2) * 8), "COMMANDES",
|
||||
"and a port-added row's label is translated too")
|
||||
|
||||
-- CANCEL is the last row, built into ROWS like any other -- there is no
|
||||
-- BACK is the last row, built into ROWS like any other -- there is no
|
||||
-- separate hook to fall through if this one row is missed.
|
||||
local cancelMenu = OptionsMenu.new({})
|
||||
local cancelIndex = #cancelMenu.rows
|
||||
T.check(cancelMenu.rows[cancelIndex].cancel, "the last row is CANCEL")
|
||||
local cancelMenu = OptionsMenu.new(fakeGame())
|
||||
local cancelIndex = #cancelMenu.view
|
||||
T.check(cancelMenu.view[cancelIndex].cancel, "the last row is BACK")
|
||||
cancelMenu.index = cancelIndex
|
||||
cancelMenu:ensureVisible()
|
||||
drawn = {}
|
||||
cancelMenu:drawPanel()
|
||||
local cancelSlot = cancelIndex - cancelMenu.scroll
|
||||
T.eq(drawnAt(LABEL_X, (2 + (cancelSlot - 1) * 2) * 8), "ANNULER",
|
||||
T.eq(drawnAt(LABEL_X, (2 + (cancelSlot - 1) * 2) * 8), "RETOUR",
|
||||
"CANCEL, the way out of the menu, is translated too")
|
||||
|
||||
-- Module state is process-global (see tests/gen2_clock_test.lua's own
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
-- PackMenu:drawList and PackGfx:draw stayed flat white/black regardless of
|
||||
-- COLORS, since PackGfx only repaints its left five columns and the
|
||||
-- item-list columns kept their initial bare-rectangle fill. Fixed by
|
||||
-- routing that fill through Chrome.paletteFill and PackMenu's prints
|
||||
-- through printThrough/cursorThrough.
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.harness")
|
||||
|
||||
love = require("tests.love_stub")
|
||||
|
||||
require("src.core.Logger").warn = function() end
|
||||
|
||||
local GbcPalette = require("src.render.GbcPalette")
|
||||
local PackGfx = require("src.ui.gen2.PackGfx")
|
||||
local PackMenu = require("src.ui.gen2.PackMenu")
|
||||
|
||||
GbcPalette.available = function() return true end
|
||||
GbcPalette.setCustomRamp({ { 255, 0, 255 }, { 200, 0, 200 }, { 100, 0, 100 },
|
||||
{ 0, 0, 0 } })
|
||||
|
||||
local calls
|
||||
local function spy(name)
|
||||
local original = GbcPalette[name]
|
||||
GbcPalette[name] = function(...)
|
||||
calls[name] = (calls[name] or 0) + 1
|
||||
return original(...)
|
||||
end
|
||||
end
|
||||
|
||||
do
|
||||
-- An empty gfx.pack table (no header/background/pack-image paths): draw()
|
||||
-- still runs its header/pattern loops, they just find nothing to blit, so
|
||||
-- only the base fill itself is exercised.
|
||||
local gfx = PackGfx.new({ pack = {} })
|
||||
calls = {}
|
||||
spy("resolve"); spy("use"); spy("with")
|
||||
gfx:draw("ITEMS")
|
||||
T.check((calls.resolve or 0) > 0 or (calls.use or 0) > 0 or (calls.with or 0) > 0,
|
||||
"PackGfx:draw()'s base fill reaches the GbcPalette seam")
|
||||
end
|
||||
|
||||
local function fakePackMenu()
|
||||
return {
|
||||
rows = {
|
||||
{ id = "POTION", name = "POTION", showCount = true, count = 1 },
|
||||
},
|
||||
scroll = 0,
|
||||
index = 1,
|
||||
switching = nil,
|
||||
gfx = PackGfx.new(nil),
|
||||
cursorAt = PackMenu.cursorAt,
|
||||
total = function() return 2 end, -- 1 row + CANCEL
|
||||
}
|
||||
end
|
||||
|
||||
do
|
||||
calls = {}
|
||||
spy("resolve"); spy("use"); spy("with")
|
||||
PackMenu.drawList(fakePackMenu(), 5, 4)
|
||||
T.check((calls.resolve or 0) > 0 or (calls.use or 0) > 0 or (calls.with or 0) > 0,
|
||||
"PackMenu:drawList() reaches the GbcPalette seam for item labels")
|
||||
end
|
||||
|
||||
T.finish()
|
||||
@@ -0,0 +1,275 @@
|
||||
-- Gen 2's answer to tests/engine/palette_test.lua: the same COLORS picker,
|
||||
-- wired onto Gold's COLOR row and its own substitution seam
|
||||
-- (src/render/GbcPalette.lua) instead of Gen 1's PaletteFX.
|
||||
-- luajit tests/engine/gen2_palette_picker_test.lua
|
||||
--
|
||||
-- Covers GbcPalette.customRamp/setCustomRamp (the seam every Gen 2 draw call
|
||||
-- shares), that a stored palette beats CLASSIC's present pass the same way
|
||||
-- it beats OG/OG INV/CLASSIC on Gen 1, and that the COLOR row opens the
|
||||
-- picker while GBC/DMG/CLASSIC stay reachable inside it.
|
||||
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
if not _G.love then _G.love = require("tests.love_stub") end
|
||||
|
||||
local T = require("tests.harness")
|
||||
local check, eq = T.check, T.eq
|
||||
|
||||
local function hex(c)
|
||||
return type(c) == "table" and ("%02x%02x%02x"):format(c[1], c[2], c[3]) or "?"
|
||||
end
|
||||
|
||||
local Palette = require("src.render.Palette")
|
||||
local GbcPalette = require("src.render.GbcPalette")
|
||||
|
||||
-- ------- GbcPalette seam: a custom ramp wins outright
|
||||
|
||||
do
|
||||
GbcPalette.setCustomRamp(nil)
|
||||
GbcPalette.setMode("gbc")
|
||||
|
||||
local cartColors = { { 9, 9, 9 }, { 8, 8, 8 }, { 7, 7, 7 }, { 6, 6, 6 } }
|
||||
eq(GbcPalette.resolve(cartColors), cartColors,
|
||||
"no custom ramp: GBC mode passes the cart's own colours through")
|
||||
|
||||
local id = Palette.packId("GalleryWebApp Palettes", "Original GB")
|
||||
local ramp = Palette.ramp(id)
|
||||
GbcPalette.setCustomRamp(ramp)
|
||||
|
||||
local out = GbcPalette.resolve(cartColors)
|
||||
for i = 1, 4 do
|
||||
eq(hex(out[i]), hex(ramp[i]),
|
||||
"resolve[" .. i .. "] is the custom ramp, not the cart's own colours")
|
||||
end
|
||||
|
||||
-- it should win even over DMG, since DMG's whole effect lives at this seam
|
||||
GbcPalette.setMode("dmg")
|
||||
local out2 = GbcPalette.resolve(cartColors)
|
||||
eq(hex(out2[1]), hex(ramp[1]), "a custom ramp beats DMG too")
|
||||
|
||||
GbcPalette.setMode("gbc")
|
||||
GbcPalette.setCustomRamp(nil)
|
||||
eq(GbcPalette.resolve(cartColors), cartColors,
|
||||
"clearing the ramp restores the cart's own colours")
|
||||
end
|
||||
|
||||
-- ------- CLASSIC is a present pass, same as Gen 1: it has to be dropped
|
||||
|
||||
do
|
||||
GbcPalette.setCustomRamp(nil)
|
||||
GbcPalette.setMode("classic")
|
||||
check(GbcPalette.presentColors() ~= nil,
|
||||
"CLASSIC has a present pass before any palette is picked")
|
||||
|
||||
local id = Palette.packId("BGB", "Red")
|
||||
local ramp = Palette.ramp(id)
|
||||
GbcPalette.setCustomRamp(ramp)
|
||||
-- resolve() alone can't save CLASSIC: presentColors bypasses resolve
|
||||
-- entirely, so the picker's opts have to drop the mode itself. Covered
|
||||
-- below via the real row wiring.
|
||||
GbcPalette.setMode("gbc")
|
||||
GbcPalette.setCustomRamp(nil)
|
||||
end
|
||||
|
||||
-- ------- applyOptions: a saved palette re-applies on load, like Gen 1's
|
||||
|
||||
do
|
||||
GbcPalette.setCustomRamp(nil)
|
||||
local id = Palette.packId("BGB", "Blue")
|
||||
GbcPalette.applyOptions({ color = "dmg", palette = id })
|
||||
eq(GbcPalette.mode, "dmg", "applyOptions still sets the mode")
|
||||
check(GbcPalette.customRamp ~= nil, "applyOptions applied the saved palette")
|
||||
eq(hex(GbcPalette.customRamp[1]), hex(Palette.ramp(id)[1]),
|
||||
"and it's the right ramp")
|
||||
|
||||
GbcPalette.applyOptions({ color = "gbc", palette = "" })
|
||||
check(GbcPalette.customRamp == nil,
|
||||
"applyOptions with no palette clears the custom ramp")
|
||||
end
|
||||
|
||||
-- ------- the COLOR row's real wiring: activate opens the picker
|
||||
|
||||
local OptionsMenu = require("src.ui.gen2.OptionsMenu")
|
||||
|
||||
local function rowNamed(rows, label)
|
||||
for _, row in ipairs(rows) do
|
||||
if row.label == label then return row end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
do
|
||||
local colorRow = rowNamed(OptionsMenu.ROWS, "COLOR")
|
||||
check(colorRow ~= nil, "COLOR is still a row")
|
||||
check(colorRow.cycle == nil, "COLOR no longer cycles in place")
|
||||
check(type(colorRow.activate) == "function",
|
||||
"COLOR opens the picker instead")
|
||||
|
||||
GbcPalette.setCustomRamp(nil)
|
||||
GbcPalette.setMode("classic")
|
||||
|
||||
local pushed
|
||||
local game = {
|
||||
options = { color = "classic", palette = "" },
|
||||
stack = { states = {},
|
||||
push = function(self, inst) pushed = inst end },
|
||||
persistOptions = function() end,
|
||||
}
|
||||
|
||||
colorRow.activate(game)
|
||||
check(pushed ~= nil, "activate pushed a screen")
|
||||
check(pushed.view == "root", "the pushed screen is a real PaletteScreen")
|
||||
eq(game.options.color, "gbc",
|
||||
"opening the picker while CLASSIC was active dropped it to GBC")
|
||||
eq(GbcPalette.mode, "gbc", "and the live mode followed")
|
||||
|
||||
-- the root offers the engine's own GBC/DMG/CLASSIC ladder alongside the
|
||||
-- 699-pack folders and MONOCHROME: "the original options" still work
|
||||
local sawModes = {}
|
||||
for _, row in ipairs(pushed.rows) do
|
||||
if row.mode then sawModes[row.mode] = true end
|
||||
end
|
||||
check(sawModes.gbc and sawModes.dmg and sawModes.classic,
|
||||
"GBC, DMG and CLASSIC are all still reachable from the picker")
|
||||
|
||||
local id = Palette.packId("BGB", "Blue")
|
||||
pushed.set(id)
|
||||
eq(game.options.palette, id, "storing a palette writes options.palette")
|
||||
check(GbcPalette.customRamp ~= nil, "and GbcPalette.setCustomRamp actually ran")
|
||||
eq(hex(GbcPalette.customRamp[1]), hex(Palette.ramp(id)[1]), "with the right ramp")
|
||||
|
||||
pushed.set("")
|
||||
check(GbcPalette.customRamp == nil, "storing empty clears the custom ramp")
|
||||
eq(game.options.color, "gbc",
|
||||
"clearing a palette lands back on GEN 2, not whatever mode was active")
|
||||
eq(GbcPalette.mode, "gbc", "and the live mode followed")
|
||||
|
||||
-- picking an engine mode through the picker still works and clears the
|
||||
-- stored palette, exactly like Gen 1's picker
|
||||
pushed.set(id)
|
||||
pushed.setMode("dmg")
|
||||
eq(game.options.color, "dmg", "setMode still writes options.color")
|
||||
eq(GbcPalette.mode, "dmg", "and drives the live module")
|
||||
|
||||
GbcPalette.setCustomRamp(nil)
|
||||
GbcPalette.setMode("gbc")
|
||||
end
|
||||
|
||||
-- ------- GBC/GEN 2 relabel: GbcPalette.MODES keeps its id "gbc", but the
|
||||
-- label the player sees for it moved to "GEN 2", freeing "GBC" up as
|
||||
-- CUSTOM_MODE's own label, the state a stored palette actually lands on.
|
||||
|
||||
do
|
||||
eq(GbcPalette.MODE_LABELS.gbc, "GEN 2",
|
||||
"GEN 2 is the engine's own unique-colour mode's display name now")
|
||||
eq(GbcPalette.MODE_LABELS[GbcPalette.CUSTOM_MODE], "GBC",
|
||||
"GBC is the label for a stored custom palette's state")
|
||||
check(not (function()
|
||||
for _, id in ipairs(GbcPalette.MODES) do
|
||||
if id == GbcPalette.CUSTOM_MODE then return true end
|
||||
end
|
||||
return false
|
||||
end)(), "CUSTOM_MODE is deliberately not one of the root ladder's own modes")
|
||||
end
|
||||
|
||||
-- ------- the bug this session's relabel fixes: picking a palette from a
|
||||
-- non-GEN-2 mode used to strand the player there once the palette cleared,
|
||||
-- because only CLASSIC ever got dropped back. Now ANY mode lands on
|
||||
-- CUSTOM_MODE the moment a palette is picked, so clearing it always finds
|
||||
-- its way back to GEN 2's own unique colours.
|
||||
|
||||
do
|
||||
local OptionsMenu = require("src.ui.gen2.OptionsMenu")
|
||||
local colorRow = nil
|
||||
for _, r in ipairs(OptionsMenu.ROWS) do
|
||||
if r.key == "color" then colorRow = r end
|
||||
end
|
||||
check(colorRow ~= nil, "found the COLOR row")
|
||||
|
||||
GbcPalette.setCustomRamp(nil)
|
||||
GbcPalette.setMode("dmg")
|
||||
|
||||
local pushed
|
||||
local game = {
|
||||
options = { color = "dmg", palette = "" },
|
||||
stack = { states = {}, push = function(self, inst) pushed = inst end },
|
||||
persistOptions = function() end,
|
||||
}
|
||||
colorRow.activate(game)
|
||||
|
||||
-- opening the picker from DMG (not CLASSIC) doesn't touch the mode yet:
|
||||
-- dropClassic only fires for CLASSIC, exactly as before.
|
||||
eq(game.options.color, "dmg", "opening the picker from DMG leaves it alone")
|
||||
|
||||
local id = Palette.packId("BGB", "Blue")
|
||||
pushed.set(id)
|
||||
eq(game.options.color, GbcPalette.CUSTOM_MODE,
|
||||
"picking a palette from DMG now lands on CUSTOM_MODE, not DMG")
|
||||
eq(GbcPalette.mode, GbcPalette.CUSTOM_MODE, "and the live mode followed")
|
||||
|
||||
pushed.set("")
|
||||
eq(game.options.color, "gbc",
|
||||
"clearing it lands on GEN 2 (the bug: this used to stay on DMG, " ..
|
||||
"with no selectable row left to get back to GEN 2's unique colours)")
|
||||
eq(GbcPalette.mode, "gbc", "and the live mode followed")
|
||||
|
||||
GbcPalette.setCustomRamp(nil)
|
||||
GbcPalette.setMode("gbc")
|
||||
end
|
||||
|
||||
-- ------- TitleState's two baked art sets: GEN 2 and a picked custom
|
||||
-- palette both want the colour set (there's no live recolour of this
|
||||
-- screen's art either way); only DMG/CLASSIC want the grey set.
|
||||
|
||||
do
|
||||
local TitleState = require("src.ui.gen2.TitleState")
|
||||
GbcPalette.setMode("gbc")
|
||||
check(not TitleState.gray(), "GEN 2 shows the colour art")
|
||||
GbcPalette.setMode(GbcPalette.CUSTOM_MODE)
|
||||
check(not TitleState.gray(), "a picked custom palette also shows the colour art")
|
||||
GbcPalette.setMode("dmg")
|
||||
check(TitleState.gray(), "DMG shows the grey art")
|
||||
GbcPalette.setMode("classic")
|
||||
check(TitleState.gray(), "CLASSIC shows the grey art (then the green present pass)")
|
||||
GbcPalette.setMode("gbc")
|
||||
end
|
||||
|
||||
-- ------- the "2" hotkey ladder (Game2:hotkey) and GbcPalette.cycle(): the
|
||||
-- bug this session fixes. Unlike the picker's own row.mode branch (tested
|
||||
-- above), this ladder used to change GbcPalette.mode/options.color without
|
||||
-- touching customRamp or options.palette, so resolve() stayed locked onto
|
||||
-- the old pack's colours no matter how many times "2" landed back on "gbc".
|
||||
|
||||
do
|
||||
local Game2 = require("src.core.Game2")
|
||||
local cartColors = { { 9, 9, 9 }, { 8, 8, 8 }, { 7, 7, 7 }, { 6, 6, 6 } }
|
||||
|
||||
GbcPalette.setCustomRamp(nil)
|
||||
GbcPalette.setMode("gbc")
|
||||
|
||||
local id = Palette.packId("BGB", "Blue")
|
||||
local ramp = Palette.ramp(id)
|
||||
local game = {
|
||||
options = { color = GbcPalette.CUSTOM_MODE, palette = id },
|
||||
persistOptions = function() end,
|
||||
}
|
||||
GbcPalette.setMode(GbcPalette.CUSTOM_MODE)
|
||||
GbcPalette.setCustomRamp(ramp)
|
||||
|
||||
-- cycle() isn't in GbcPalette.MODES so a custom-mode start is treated as
|
||||
-- index 1: three "2" presses walk dmg -> classic -> gbc.
|
||||
for _ = 1, 3 do Game2.hotkey(game, "2") end
|
||||
|
||||
eq(game.options.color, "gbc", "three presses land back on GEN 2's label")
|
||||
eq(game.options.palette, "",
|
||||
"BUG FIX: the hotkey ladder clears options.palette too, not just color")
|
||||
check(GbcPalette.customRamp == nil,
|
||||
"BUG FIX: GbcPalette.cycle() drops the live custom ramp")
|
||||
eq(GbcPalette.resolve(cartColors), cartColors,
|
||||
"BUG FIX: resolve() answers with the cart's own colours again, not " ..
|
||||
"the stale custom ramp")
|
||||
|
||||
GbcPalette.setCustomRamp(nil)
|
||||
GbcPalette.setMode("gbc")
|
||||
end
|
||||
|
||||
T.finish("gen2 palette picker")
|
||||
@@ -0,0 +1,83 @@
|
||||
-- Follow-up to gen2_pokegear_native_colors_test.lua's "keep the gear
|
||||
-- vanilla" fix: CLASSIC swept the native-coloured gear for free, so this
|
||||
-- extends the same whole-panel tint to a picked custom COLORS ramp, with
|
||||
-- Pokegear.CUSTOM_RAMP_FILM = false as an easy revert.
|
||||
--
|
||||
-- No real shader/asset loads headless, so this isolates drawPanel()'s new
|
||||
-- branch with a minimal fake self, same "stub the rest, assert the seam"
|
||||
-- approach as the TrainerCard drawPanel() test.
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.harness")
|
||||
|
||||
love = require("tests.love_stub")
|
||||
|
||||
require("src.core.Logger").warn = function() end
|
||||
|
||||
local GbcPalette = require("src.render.GbcPalette")
|
||||
local Pokegear = require("src.ui.gen2.Pokegear")
|
||||
|
||||
GbcPalette.available = function() return true end
|
||||
|
||||
local calls
|
||||
local function spy(name)
|
||||
local original = GbcPalette[name]
|
||||
GbcPalette[name] = function(...)
|
||||
calls[name] = (calls[name] or 0) + 1
|
||||
return original(...)
|
||||
end
|
||||
end
|
||||
|
||||
local function hits()
|
||||
return (calls.resolve or 0) + (calls.use or 0) + (calls.useRaw or 0)
|
||||
+ (calls.with or 0)
|
||||
end
|
||||
|
||||
local function fakeGear()
|
||||
return {
|
||||
styled = function() return true end,
|
||||
groundColor = function() return { 0, 0, 0 } end,
|
||||
card = function() return { id = "clock" } end,
|
||||
drawClock = function() end,
|
||||
drawMap = function() end,
|
||||
drawRadio = function() end,
|
||||
drawPhone = function() end,
|
||||
drawModeArrow = function() end,
|
||||
}
|
||||
end
|
||||
|
||||
do
|
||||
Pokegear.CUSTOM_RAMP_FILM = true
|
||||
GbcPalette.setCustomRamp({ { 227, 249, 227 }, { 180, 200, 180 },
|
||||
{ 90, 100, 90 }, { 0, 0, 0 } })
|
||||
calls = {}
|
||||
spy("resolve"); spy("use"); spy("useRaw"); spy("with")
|
||||
Pokegear.drawPanel(fakeGear())
|
||||
T.check(hits() > 0,
|
||||
"with the film on and a custom ramp active, drawPanel() reaches GbcPalette")
|
||||
end
|
||||
|
||||
do
|
||||
Pokegear.CUSTOM_RAMP_FILM = false
|
||||
GbcPalette.setCustomRamp({ { 227, 249, 227 }, { 180, 200, 180 },
|
||||
{ 90, 100, 90 }, { 0, 0, 0 } })
|
||||
calls = {}
|
||||
spy("resolve"); spy("use"); spy("useRaw"); spy("with")
|
||||
Pokegear.drawPanel(fakeGear())
|
||||
T.eq(hits(), 0,
|
||||
"Pokegear.CUSTOM_RAMP_FILM = false is a one-line revert to fully native")
|
||||
end
|
||||
|
||||
do
|
||||
Pokegear.CUSTOM_RAMP_FILM = true
|
||||
GbcPalette.setCustomRamp(nil)
|
||||
calls = {}
|
||||
spy("resolve"); spy("use"); spy("useRaw"); spy("with")
|
||||
Pokegear.drawPanel(fakeGear())
|
||||
T.eq(hits(), 0,
|
||||
"with no custom ramp picked, the film never engages -- native GEN 2 stays untouched")
|
||||
end
|
||||
|
||||
Pokegear.CUSTOM_RAMP_FILM = true
|
||||
|
||||
T.finish()
|
||||
@@ -0,0 +1,66 @@
|
||||
-- The Pokegear's six hardware palette banks guarantee a highlighted cell
|
||||
-- contrasts with its text, a guarantee GbcPalette.resolve() breaks by
|
||||
-- collapsing every bank onto the same 4 shades. Product decision: the
|
||||
-- Pokegear stays on native hardware colours always, via a `raw` path
|
||||
-- (GbcPalette.withRaw, Chrome.*Through's `raw` param, TileSheet's `raw`
|
||||
-- option) that calls useRaw directly and skips resolve.
|
||||
--
|
||||
-- No real shader runs headless, so this asserts on which entry point gets
|
||||
-- called rather than on rendered pixels.
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.harness")
|
||||
|
||||
love = require("tests.love_stub")
|
||||
|
||||
require("src.core.Logger").warn = function() end
|
||||
|
||||
local GbcPalette = require("src.render.GbcPalette")
|
||||
local Chrome = require("src.ui.gen2.Chrome")
|
||||
local TileSheet = require("src.ui.gen2.TileSheet")
|
||||
local Pokegear = require("src.ui.gen2.Pokegear")
|
||||
|
||||
GbcPalette.available = function() return true end
|
||||
GbcPalette.setCustomRamp({ { 255, 0, 255 }, { 200, 0, 200 }, { 100, 0, 100 },
|
||||
{ 0, 0, 0 } })
|
||||
|
||||
local calls
|
||||
local function spy(name)
|
||||
local original = GbcPalette[name]
|
||||
GbcPalette[name] = function(...)
|
||||
calls[name] = (calls[name] or 0) + 1
|
||||
return original(...)
|
||||
end
|
||||
end
|
||||
|
||||
local BANK_1 = { { 227, 249, 227 }, { 180, 200, 180 }, { 90, 100, 90 },
|
||||
{ 0, 0, 0 } }
|
||||
|
||||
do
|
||||
calls = {}
|
||||
spy("resolve"); spy("useRaw")
|
||||
Pokegear.text({ pals = function() return { BANK_1 } end }, "SUNDAY", 6, 6)
|
||||
T.eq(calls.resolve or 0, 0,
|
||||
"Pokegear:text() never substitutes the active COLORS ramp")
|
||||
T.check((calls.useRaw or 0) > 0,
|
||||
"Pokegear:text() still shades its own native colours through the shader")
|
||||
end
|
||||
|
||||
do
|
||||
local sheet = TileSheet.new({ palette = BANK_1, raw = true })
|
||||
-- No real image asset in the headless harness, so this exercises the
|
||||
-- option's plumbing (raw stored, read by :draw) rather than a full
|
||||
-- render; gen2_trainer_card_text_palette_test.lua and friends take the
|
||||
-- same "assert the seam, not the pixel" approach one level up.
|
||||
T.check(sheet.raw == true, "TileSheet:new stores the raw option")
|
||||
end
|
||||
|
||||
-- A sheet built without raw must still go through the normal substituting
|
||||
-- seam, guarding the new option from silently becoming the default for
|
||||
-- every other Gen 2 screen's tile art.
|
||||
do
|
||||
local sheet = TileSheet.new({ palette = BANK_1 })
|
||||
T.eq(sheet.raw, nil, "TileSheet:new defaults raw to unset")
|
||||
end
|
||||
|
||||
T.finish()
|
||||
@@ -0,0 +1,66 @@
|
||||
-- StartMenu:draw drew the OPTION row's description tooltip, the "Change
|
||||
-- settings" box under the list, with a raw love.graphics.rectangle at a
|
||||
-- flat literal white, never touching GbcPalette. Every other box on this
|
||||
-- screen already recoloured correctly with a picked COLORS palette, so
|
||||
-- this one sub-region stood out as structurally white/black regardless.
|
||||
--
|
||||
-- No real shader runs headless, so this can't check a rendered pixel, same
|
||||
-- limitation every palette test in this directory notes: force
|
||||
-- GbcPalette.available() true and assert on which seam gets called, not
|
||||
-- on pixels. tests/drivers/custom_ramp_probe.lua is where a real rendered
|
||||
-- screen gets checked by eye.
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.harness")
|
||||
|
||||
love = require("tests.love_stub")
|
||||
|
||||
require("src.core.Logger").warn = function() end
|
||||
|
||||
local GbcPalette = require("src.render.GbcPalette")
|
||||
local StartMenu = require("src.ui.gen2.StartMenu")
|
||||
|
||||
local calls
|
||||
local function spy(name)
|
||||
local original = GbcPalette[name]
|
||||
GbcPalette[name] = function(...)
|
||||
calls[name] = (calls[name] or 0) + 1
|
||||
return original(...)
|
||||
end
|
||||
end
|
||||
|
||||
-- Minimal fake: draw() only needs #self.items (box height), a list stub,
|
||||
-- and a current item with a desc, the same shape start_menu.asm carries
|
||||
-- for every row (see StartMenu.lua's ITEMS table).
|
||||
local function fakeMenu()
|
||||
return {
|
||||
items = { { id = "option" } },
|
||||
list = {
|
||||
draw = function() end,
|
||||
current = function() return { desc = { "Change", "settings" } } end,
|
||||
},
|
||||
phase = nil,
|
||||
showDescription = true,
|
||||
}
|
||||
end
|
||||
|
||||
do
|
||||
GbcPalette.available = function() return true end
|
||||
GbcPalette.setCustomRamp({ { 255, 0, 255 }, { 200, 0, 200 }, { 100, 0, 100 },
|
||||
{ 0, 0, 0 } })
|
||||
calls = {}
|
||||
spy("resolve"); spy("use"); spy("with")
|
||||
StartMenu.draw(fakeMenu())
|
||||
T.check((calls.resolve or 0) > 0 or (calls.use or 0) > 0 or (calls.with or 0) > 0,
|
||||
"StartMenu's description tooltip reaches the GbcPalette seam")
|
||||
end
|
||||
|
||||
do
|
||||
GbcPalette.available = function() return false end
|
||||
calls = {}
|
||||
spy("resolve"); spy("use"); spy("with")
|
||||
local ok = pcall(StartMenu.draw, fakeMenu())
|
||||
T.check(ok, "StartMenu:draw() does not error with no shader available")
|
||||
end
|
||||
|
||||
T.finish()
|
||||
@@ -0,0 +1,63 @@
|
||||
-- TextBox drew its box border and glyphs through Font.drawBox/drawCode
|
||||
-- with a flat setColor, never touching GbcPalette, so dialogue text never
|
||||
-- picked up a COLORS palette even though every other Chrome.lua-based
|
||||
-- screen did.
|
||||
--
|
||||
-- No real shader runs headless, so this asserts on which seam gets called
|
||||
-- rather than checking a rendered pixel; tests/drivers/gold_menu_shots.lua
|
||||
-- is where a real box gets checked by eye.
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.harness")
|
||||
|
||||
love = require("tests.love_stub")
|
||||
|
||||
require("src.core.Logger").warn = function() end
|
||||
|
||||
local GbcPalette = require("src.render.GbcPalette")
|
||||
local TextBox = require("src.render.TextBox")
|
||||
|
||||
GbcPalette.available = function() return true end
|
||||
GbcPalette.setCustomRamp({ { 255, 0, 255 }, { 200, 0, 200 }, { 100, 0, 100 },
|
||||
{ 0, 0, 0 } })
|
||||
|
||||
local calls
|
||||
local function spy(name)
|
||||
local original = GbcPalette[name]
|
||||
GbcPalette[name] = function(...)
|
||||
calls[name] = (calls[name] or 0) + 1
|
||||
return original(...)
|
||||
end
|
||||
end
|
||||
|
||||
local function newGame(generation)
|
||||
return {
|
||||
save = { player = { name = "RED" }, generation = generation,
|
||||
version = generation == 2 and "gold" or "red", options = {} },
|
||||
stringBuffer = "", boxNumString = "", boxMonNicks = {},
|
||||
data = {},
|
||||
}
|
||||
end
|
||||
|
||||
do
|
||||
calls = {}
|
||||
spy("resolve"); spy("use"); spy("with")
|
||||
local box = TextBox.new(newGame(2), "Hello world!", nil, {})
|
||||
box:draw()
|
||||
T.check((calls.resolve or 0) > 0 or (calls.use or 0) > 0 or (calls.with or 0) > 0,
|
||||
"Gen 2 dialogue box reaches the GbcPalette seam")
|
||||
end
|
||||
|
||||
-- Gen 1 recolours through a whole-frame zone remap upstream of this file
|
||||
-- (PaletteFX), not per draw call, so TextBox must not start touching
|
||||
-- GbcPalette for a Gen 1 game.
|
||||
do
|
||||
calls = {}
|
||||
spy("resolve"); spy("use"); spy("with")
|
||||
local box = TextBox.new(newGame(1), "Hello world!", nil, {})
|
||||
box:draw()
|
||||
T.eq((calls.resolve or 0) + (calls.use or 0) + (calls.with or 0), 0,
|
||||
"Gen 1 dialogue box is untouched, it never calls GbcPalette")
|
||||
end
|
||||
|
||||
T.finish()
|
||||
@@ -0,0 +1,69 @@
|
||||
-- TrainerCard had two palette gaps: its own text drew through flat
|
||||
-- Chrome.print/cursor, fixed with a print/:cursor pair routed through
|
||||
-- printThrough/cursorThrough via self:colorsAt; and drawPanel() painted
|
||||
-- the screen with a flat white rectangle before anything else, leaving the
|
||||
-- NAME/ID/MONEY/STATUS/BADGES interiors white, fixed by routing that fill
|
||||
-- through Chrome.paletteFill.
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.harness")
|
||||
|
||||
love = require("tests.love_stub")
|
||||
|
||||
require("src.core.Logger").warn = function() end
|
||||
|
||||
local GbcPalette = require("src.render.GbcPalette")
|
||||
local TrainerCard = require("src.ui.gen2.TrainerCard")
|
||||
|
||||
GbcPalette.available = function() return true end
|
||||
GbcPalette.setCustomRamp({ { 255, 0, 255 }, { 200, 0, 200 }, { 100, 0, 100 },
|
||||
{ 0, 0, 0 } })
|
||||
|
||||
local calls
|
||||
local function spy(name)
|
||||
local original = GbcPalette[name]
|
||||
GbcPalette[name] = function(...)
|
||||
calls[name] = (calls[name] or 0) + 1
|
||||
return original(...)
|
||||
end
|
||||
end
|
||||
|
||||
local function fakeCard()
|
||||
return {
|
||||
colorsAt = function() return { { 255, 255, 255 }, { 200, 0, 200 },
|
||||
{ 100, 0, 100 }, { 0, 0, 0 } } end,
|
||||
}
|
||||
end
|
||||
|
||||
do
|
||||
calls = {}
|
||||
spy("resolve"); spy("use"); spy("with")
|
||||
TrainerCard.print(fakeCard(), "NAME/", 2, 2)
|
||||
T.check((calls.resolve or 0) > 0 or (calls.use or 0) > 0 or (calls.with or 0) > 0,
|
||||
"TrainerCard:print() reaches the GbcPalette seam via self:colorsAt")
|
||||
end
|
||||
|
||||
do
|
||||
calls = {}
|
||||
spy("resolve"); spy("use"); spy("with")
|
||||
TrainerCard.cursor(fakeCard(), 18, 15)
|
||||
T.check((calls.resolve or 0) > 0 or (calls.use or 0) > 0 or (calls.with or 0) > 0,
|
||||
"TrainerCard:cursor() reaches the GbcPalette seam via self:colorsAt")
|
||||
end
|
||||
|
||||
-- drawPanel() painted the whole screen with a flat white rectangle before
|
||||
-- drawing the card, and self:frame only draws border tiles, so panel
|
||||
-- interiors stayed white regardless of the picked palette. drawCard is
|
||||
-- stubbed here to isolate that base fill from the print/cursor seam
|
||||
-- covered above.
|
||||
do
|
||||
local fake = { styled = function() return true end, page = 1,
|
||||
drawCard = function() end }
|
||||
calls = {}
|
||||
spy("resolve"); spy("use"); spy("with")
|
||||
TrainerCard.drawPanel(fake)
|
||||
T.check((calls.resolve or 0) > 0 or (calls.use or 0) > 0 or (calls.with or 0) > 0,
|
||||
"TrainerCard:drawPanel()'s base fill reaches the GbcPalette seam")
|
||||
end
|
||||
|
||||
T.finish()
|
||||
@@ -0,0 +1,46 @@
|
||||
-- UnownWords:draw already routed its tile art through GbcPalette.with, but
|
||||
-- drew the frame around it with a plain Font.drawBox, so the puzzle's own
|
||||
-- glyphs picked up a selected COLORS palette while the box border stayed
|
||||
-- white/black, the same shape of gap gen2_textbox_palette_test.lua found
|
||||
-- in TextBox.lua. Fixed by routing the box through Chrome.paletteBox.
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.harness")
|
||||
|
||||
love = require("tests.love_stub")
|
||||
|
||||
require("src.core.Logger").warn = function() end
|
||||
|
||||
local GbcPalette = require("src.render.GbcPalette")
|
||||
local UnownWords = require("src.world.gen2.UnownWords")
|
||||
|
||||
GbcPalette.available = function() return true end
|
||||
GbcPalette.setCustomRamp({ { 255, 0, 255 }, { 200, 0, 200 }, { 100, 0, 100 },
|
||||
{ 0, 0, 0 } })
|
||||
|
||||
local calls
|
||||
local function spy(name)
|
||||
local original = GbcPalette[name]
|
||||
GbcPalette[name] = function(...)
|
||||
calls[name] = (calls[name] or 0) + 1
|
||||
return original(...)
|
||||
end
|
||||
end
|
||||
|
||||
-- data/events/unown_walls.asm:15 MenuHeaders_UnownWalls's ESCAPE row, the
|
||||
-- same fixture tests/gen2_unown_words_test.lua uses for boxRect.
|
||||
local ESCAPE_WALL = { x1 = 3, y1 = 4, x2 = 16, y2 = 9,
|
||||
chars = { 0x08, 0x44, 0x04, 0x00, 0x2e, 0x08 } }
|
||||
|
||||
do
|
||||
calls = {}
|
||||
spy("resolve"); spy("use"); spy("with")
|
||||
-- no opts.world -> :tileset() returns nil -> draw() exits right after the
|
||||
-- box, isolating the box's own palette seam from the tile-art body wrap
|
||||
local words = UnownWords.new({}, { wall = ESCAPE_WALL })
|
||||
words:draw()
|
||||
T.check((calls.resolve or 0) > 0 or (calls.use or 0) > 0 or (calls.with or 0) > 0,
|
||||
"Unown wall box reaches the GbcPalette seam")
|
||||
end
|
||||
|
||||
T.finish()
|
||||
@@ -0,0 +1,152 @@
|
||||
-- The OPTION screen's grouped pages and its scrolling text.
|
||||
--
|
||||
-- The flat row list outgrew the four-box viewport, so the battle, audio,
|
||||
-- video and speed rows collapse into one opener each. Grouping happens after
|
||||
-- the ui.options.rows hook and only builds self.view, so self.rows stays the
|
||||
-- flat list a mod reads and edits.
|
||||
--
|
||||
-- Text longer than its line used to run off the 160px panel with no clipping
|
||||
-- (COLORS showed "1-A (Default) (B"); the highlighted row now scrolls instead.
|
||||
-- luajit tests/engine/options_submenu_marquee_test.lua
|
||||
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.modkit")
|
||||
local OptionsMenu = require("src.ui.OptionsMenu")
|
||||
local Marquee = require("src.ui.Marquee")
|
||||
|
||||
local function stubGame()
|
||||
local stack = { items = {} }
|
||||
function stack:push(s) self.items[#self.items + 1] = s; return s end
|
||||
function stack:pop() self.items[#self.items] = nil end
|
||||
function stack:top() return self.items[#self.items] end
|
||||
return {
|
||||
data = { rulesets = { gen1_faithful = { name = "GEN 1" } }, constants = {} },
|
||||
save = { options = {} },
|
||||
stack = stack,
|
||||
modStatus = { available = {} },
|
||||
}
|
||||
end
|
||||
|
||||
local function ids(rows)
|
||||
local out = {}
|
||||
for i, row in ipairs(rows) do out[i] = row.id end
|
||||
return table.concat(out, ",")
|
||||
end
|
||||
|
||||
local function find(rows, id)
|
||||
for i, row in ipairs(rows) do if row.id == id then return row, i end end
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------- grouping
|
||||
|
||||
local game = stubGame()
|
||||
local menu = OptionsMenu.new(game)
|
||||
|
||||
T.check(find(menu.rows, "battleBg"), "self.rows keeps the flat list for mods")
|
||||
T.check(find(menu.rows, "musicVol"), "self.rows keeps the audio rows too")
|
||||
for _, id in ipairs({ "battleBg", "speedBattle", "uiLayout", "tilt", "zoom",
|
||||
"voidFill", "colors", "shaderfx", "shaderfx2",
|
||||
"musicVol", "videoMode" }) do
|
||||
T.check(not find(menu.view, id), id .. " moved onto a page")
|
||||
end
|
||||
|
||||
for _, spec in ipairs({
|
||||
{ "group.battle", 6 }, { "group.audio", 3 }, { "group.video", 5 },
|
||||
{ "group.speed", 4 }, { "group.graphics", 4 }, { "group.extras", 3 },
|
||||
}) do
|
||||
local row = find(menu.view, spec[1])
|
||||
T.check(row, spec[1] .. " has an opener row")
|
||||
T.eq(row.group, true, spec[1] .. " is marked as a group")
|
||||
end
|
||||
|
||||
-- Six battle rows on this build (PIKACHU VOL is Yellow-only, so AUDIO is 3).
|
||||
T.eq(select(1, find(menu.view, "group.battle")).value(game), "6 OPTIONS",
|
||||
"BATTLE OPTIONS counts its six rows")
|
||||
|
||||
-- The top level is explicitly ordered, groups and singles interleaved.
|
||||
T.eq(ids(menu.view):gsub("group%.", ""),
|
||||
"speed,video,graphics,audio,performance,ruleset,battle,extras,mods," ..
|
||||
"controls,dateFormat,timeFormat",
|
||||
"the top level reads in the order ORDER names")
|
||||
T.check(not find(menu.view, "textSpeed"), "TEXT SPEED moved onto the SPEED page")
|
||||
T.check(#menu.view < #menu.rows, "the view is shorter than the flat list")
|
||||
|
||||
-- A row in no group is untouched and keeps its order.
|
||||
T.check(find(menu.view, "ruleset"), "RULESET stays on the top level")
|
||||
T.check(find(menu.view, "performance"), "so does PERFORMANCE")
|
||||
T.check(find(menu.view, "mods"), "and MODS")
|
||||
|
||||
-- ------------------------------------------------------------- submenus
|
||||
|
||||
local graphics = find(menu.view, "group.graphics")
|
||||
graphics.activate(game)
|
||||
T.eq(ids(game.stack:top().view), "colors,uiLetterbox,shaderfx,shaderfx2",
|
||||
"GRAPHICS carries COLORS, UI LETTERBOX and both SHADER FX slots")
|
||||
game.stack:pop()
|
||||
|
||||
local extras = find(menu.view, "group.extras")
|
||||
extras.activate(game)
|
||||
T.eq(ids(game.stack:top().view), "tilt,zoom,voidFill",
|
||||
"EXTRAS carries TILT, ZOOM and VOID FILL")
|
||||
game.stack:pop()
|
||||
|
||||
local video = find(menu.view, "group.video")
|
||||
video.activate(game)
|
||||
T.eq(ids(game.stack:top().view):match("^uiLayout"), "uiLayout",
|
||||
"UI LAYOUT heads the VIDEO page")
|
||||
game.stack:pop()
|
||||
|
||||
local speed = find(menu.view, "group.speed")
|
||||
speed.activate(game)
|
||||
T.eq(ids(game.stack:top().view),
|
||||
"textSpeed,speedOverworld,speedBattle,speedMenu",
|
||||
"TEXT SPEED heads the SPEED page")
|
||||
game.stack:pop()
|
||||
|
||||
local battle, at = find(menu.view, "group.battle")
|
||||
menu.index = at
|
||||
battle.activate(game)
|
||||
local sub = game.stack:top()
|
||||
T.check(sub ~= menu, "opening a group pushes a screen")
|
||||
T.eq(ids(sub.view),
|
||||
"animations,battleStyle,battleLayout,battleFit,battleHud,battleBg",
|
||||
"the page carries exactly its six battle rows, in order")
|
||||
T.eq(sub.sub, true, "the page knows it is a submenu")
|
||||
T.eq(sub.onCancel, nil, "BACK out of a page never fires the caller's close hook")
|
||||
|
||||
-- focusRow reaches a grouped row by opening its page.
|
||||
local game2 = stubGame()
|
||||
local menu2 = OptionsMenu.new(game2)
|
||||
local landed = menu2:focusRow("battleHud")
|
||||
T.check(landed and landed ~= menu2, "focusRow opens the page a grouped row lives on")
|
||||
T.eq(landed.view[landed.index].id, "battleHud", "and lands the cursor on it")
|
||||
T.eq(menu2:focusRow("ruleset"), menu2, "an ungrouped row focuses in place")
|
||||
T.eq(menu2.view[menu2.index].id, "ruleset", "on the row asked for")
|
||||
T.eq(menu2:focusRow("nope"), nil, "an unknown id focuses nothing")
|
||||
|
||||
-- ------------------------------------------------------------- marquee
|
||||
|
||||
-- 16 characters fit beside a value's x=24 before the box frame at x=152;
|
||||
-- the label's x=16 fits 17.
|
||||
local long = "1-A (Default) (BGB)"
|
||||
T.check(#long > 16, "the COLORS value really is too long for the line")
|
||||
|
||||
-- 19 characters, 16 fit: three steps of overflow. Hold 1.0s, one char per
|
||||
-- 0.25s, hold 1.0s on the tail, then repeat -- a 2.75s cycle.
|
||||
T.eq(Marquee.at(long, 16, 0), long:sub(1, 16), "it holds at the start")
|
||||
T.eq(Marquee.at(long, 16, 1.3), long:sub(2, 17), "then steps one char")
|
||||
T.eq(Marquee.at(long, 16, 1.6), long:sub(3, 18), "then another")
|
||||
T.eq(Marquee.at(long, 16, 2.4), long:sub(4, 19), "then holds on the tail")
|
||||
T.eq(Marquee.at(long, 16, 2.8), long:sub(1, 16), "before wrapping back")
|
||||
T.eq(Marquee.at(long, 16, 2.4):sub(-4), "BGB)",
|
||||
"so the tail the panel used to clip becomes readable")
|
||||
for _, at2 in ipairs({ 0, 1.3, 1.6, 2.4, 2.8 }) do
|
||||
T.eq(#Marquee.at(long, 16, at2), 16,
|
||||
"every frame fills the line exactly")
|
||||
end
|
||||
|
||||
T.eq(Marquee.at("SHORT", 16, 2.0), "SHORT",
|
||||
"text that fits is never scrolled")
|
||||
|
||||
T.finish("options submenu + marquee")
|
||||
@@ -0,0 +1,447 @@
|
||||
-- Tests the COLORS picker's 699-palette pack (src/render/Palette.lua) and
|
||||
-- its substitution seam in src/render/PaletteFX.lua. ROM-free, since the
|
||||
-- pack is committed data rather than ROM-imported.
|
||||
-- luajit tests/engine/palette_test.lua
|
||||
--
|
||||
-- Covers the ramp ordering flip in Palette.ramp() (the pack stores darkest
|
||||
-- first, PaletteFX wants lightest first), the substitution seam itself (a
|
||||
-- custom palette has to win outright and get a zone invented for it, same
|
||||
-- as OG/OG INV/CLASSIC), and that BattleState/WideBattle's mono checks stay
|
||||
-- in sync with that seam.
|
||||
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
if not _G.love then _G.love = require("tests.love_stub") end
|
||||
|
||||
local T = require("tests.harness")
|
||||
local check, eq = T.check, T.eq
|
||||
|
||||
local function hex(c)
|
||||
return type(c) == "table" and ("%02x%02x%02x"):format(c[1], c[2], c[3]) or "?"
|
||||
end
|
||||
|
||||
local Palette = require("src.render.Palette")
|
||||
local PaletteFX = require("src.render.PaletteFX")
|
||||
|
||||
-- ------- the pack
|
||||
|
||||
do
|
||||
eq(#Palette.packs(), 32, "32 groups")
|
||||
local total = 0
|
||||
for _, group in ipairs(Palette.packs()) do
|
||||
total = total + #(group.palettes or {})
|
||||
end
|
||||
eq(total, 699, "699 palettes")
|
||||
end
|
||||
|
||||
-- ------- the folders
|
||||
|
||||
do
|
||||
local seen, total = {}, 0
|
||||
for _, category in ipairs(Palette.categories()) do
|
||||
for _, item in ipairs(category.palettes) do
|
||||
total = total + 1
|
||||
seen[item.id] = category.key
|
||||
end
|
||||
end
|
||||
eq(total, 699, "every palette is filed")
|
||||
|
||||
local counted = 0
|
||||
for _ in pairs(seen) do counted = counted + 1 end
|
||||
eq(counted, 699, "no palette is filed twice")
|
||||
|
||||
-- the DMG ramp is the case HUE_ARC was tuned around
|
||||
eq(seen[Palette.packId("GalleryWebApp Palettes", "Original GB")],
|
||||
"single", "the DMG green is one colour")
|
||||
eq(seen[Palette.packId("BGB", "Grey")], "grey", "BGB Grey is grey")
|
||||
eq(seen[Palette.packId("GB Color (Unique Palettes)", "GBC Palette 119 (World)")],
|
||||
"full", "a yellow and red cart palette is full colour")
|
||||
-- the GBC boot palette for Red is white/pink/dark red/black: a red ramp
|
||||
eq(seen[Palette.packId("GB Color (Unique Palettes)",
|
||||
"GBC Palette 093 (USA, Europe)")],
|
||||
"single", "a tinted grey ramp is one colour")
|
||||
|
||||
local first = Palette.categories()[1].palettes[1]
|
||||
check(type(first.id) == "string" and Palette.ramp(first.id) ~= nil,
|
||||
"a bucket entry carries a resolvable id")
|
||||
check(type(first.pack) == "string" and first.pack ~= "",
|
||||
"a bucket entry remembers its source pack")
|
||||
|
||||
local key, at = Palette.locate("p:BGB/Blue")
|
||||
eq(key, "single", "locate answers the bucket")
|
||||
eq(Palette.categories()[2].palettes[at].id, "p:BGB/Blue",
|
||||
"and the tile")
|
||||
check(Palette.locate("p:Nope/Nothing") == nil, "an unknown id is nowhere")
|
||||
end
|
||||
|
||||
-- ------- classifying a ramp
|
||||
|
||||
do
|
||||
local function cat(a, b, c, d) return Palette.categoryOf({ a, b, c, d }) end
|
||||
eq(cat({ 255, 255, 255 }, { 170, 170, 170 }, { 85, 85, 85 }, { 0, 0, 0 }),
|
||||
"grey", "four greys are greyscale")
|
||||
eq(cat({ 155, 188, 15 }, { 139, 172, 15 }, { 48, 98, 48 }, { 15, 56, 15 }),
|
||||
"single", "one hue at four strengths is a single colour")
|
||||
eq(cat({ 255, 255, 255 }, { 255, 132, 132 }, { 148, 58, 58 }, { 0, 0, 0 }),
|
||||
"single", "greys among one hue do not make it full colour")
|
||||
eq(cat({ 255, 0, 0 }, { 0, 255, 0 }, { 0, 0, 255 }, { 0, 0, 0 }),
|
||||
"full", "three hues is full colour")
|
||||
eq(cat({ 255, 40, 20 }, { 255, 10, 40 }, { 128, 5, 20 }, { 40, 2, 6 }),
|
||||
"single", "a ramp straddling 0 degrees is one colour")
|
||||
check(Palette.categoryOf(nil) == nil, "nothing classifies as nothing")
|
||||
end
|
||||
|
||||
-- ------- hex decoding
|
||||
|
||||
do
|
||||
local c = Palette.unhex("0f380f30623077a1129bbc0f")
|
||||
check(c and hex(c[1]) == "0f380f", "first colour is the string's first")
|
||||
check(c and hex(c[4]) == "9bbc0f", "last colour is the string's last")
|
||||
check(Palette.unhex("abc") == nil, "short string rejected")
|
||||
check(Palette.unhex("zz380f30623077a1129bbc0f") == nil, "non-hex rejected")
|
||||
check(Palette.unhex(nil) == nil, "nil rejected")
|
||||
end
|
||||
|
||||
-- ------- ordering
|
||||
--
|
||||
-- #9BBC0F is the PAPER and #0F380F is the INK, so ramp()[1] must be pale.
|
||||
|
||||
do
|
||||
local id = Palette.packId("GalleryWebApp Palettes", "Original GB")
|
||||
local ramp = Palette.ramp(id)
|
||||
check(ramp ~= nil, "Original GB resolves")
|
||||
eq(hex(ramp[1]), "9bbc0f", "ramp[1] is the lightest (paper)")
|
||||
eq(hex(ramp[4]), "0f380f", "ramp[4] is the darkest (ink)")
|
||||
|
||||
local sw = Palette.swatch(id)
|
||||
eq(hex(sw[1]), "0f380f", "swatch[1] is the darkest")
|
||||
eq(hex(sw[4]), "9bbc0f", "swatch[4] is the lightest")
|
||||
end
|
||||
|
||||
-- cross-check against PaletteFX.GBC_BG, the same palette hardcoded there
|
||||
do
|
||||
local ramp = Palette.ramp(Palette.packId(
|
||||
"GB Color (Unique Palettes)", "GBC Palette 093 (USA, Europe)"))
|
||||
for i = 1, 4 do
|
||||
eq(hex(ramp[i]), hex(PaletteFX.GBC_BG[i]),
|
||||
"pack Red ramp[" .. i .. "] agrees with PaletteFX.GBC_BG")
|
||||
end
|
||||
end
|
||||
|
||||
-- ------- monochrome
|
||||
|
||||
do
|
||||
local id = Palette.monoId(0x9b, 0xbc, 0x0f)
|
||||
eq(id, "m:9bbc0f", "id encodes as hex")
|
||||
local ramp = Palette.ramp(id)
|
||||
eq(hex(ramp[1]), "9bbc0f", "lightest is the chosen colour")
|
||||
check(ramp[4][1] < ramp[1][1], "darkest is darker than lightest")
|
||||
eq(Palette.label(id), "GREEN", "a known colour gets its name")
|
||||
eq(Palette.label("m:123456"), "MONO", "an unknown one falls back")
|
||||
end
|
||||
|
||||
-- ------- ids
|
||||
|
||||
do
|
||||
check(Palette.label("") == nil, "empty id has no label")
|
||||
eq(Palette.label("p:BGB/Blue"), "Blue", "pack label is the name")
|
||||
check(Palette.ramp("") == nil, "empty id resolves to nothing")
|
||||
check(Palette.ramp("p:Nope/Nothing") == nil, "missing palette resolves to nothing")
|
||||
check(Palette.ramp("garbage") == nil, "malformed id resolves to nothing")
|
||||
end
|
||||
|
||||
-- ------- every palette in the pack parses
|
||||
|
||||
do
|
||||
local bad, checked = {}, 0
|
||||
for _, group in ipairs(Palette.packs()) do
|
||||
for _, entry in ipairs(group.palettes or {}) do
|
||||
local ramp = Palette.ramp(Palette.packId(group.name, entry[1]))
|
||||
checked = checked + 1
|
||||
if not ramp then bad[#bad + 1] = group.name .. "/" .. tostring(entry[1]) end
|
||||
end
|
||||
end
|
||||
eq(checked, 699, "checked every palette")
|
||||
eq(#bad, 0, "all resolve (" .. table.concat(bad, ", ", 1, math.min(#bad, 3)) .. ")")
|
||||
end
|
||||
|
||||
-- ------- the picker's state machine
|
||||
--
|
||||
-- update() touches no love API, so we can drive it with a stub game.
|
||||
|
||||
local PaletteScreen = require("src.ui.PaletteScreen")
|
||||
|
||||
do
|
||||
local pressed = {}
|
||||
|
||||
local function open(stored)
|
||||
local state = { stored = stored or "", popped = 0, mode = "redpp" }
|
||||
state.screen = PaletteScreen.new({
|
||||
input = { wasPressed = function(_, k) return pressed[k] == true end },
|
||||
stack = { states = {},
|
||||
pop = function() state.popped = state.popped + 1 end },
|
||||
}, {
|
||||
palette = Palette,
|
||||
get = function() return state.stored end,
|
||||
set = function(v) state.stored = v end,
|
||||
modes = { { "SGB", "gbc" }, { "ADVANCED", "redpp" } },
|
||||
getMode = function() return state.mode end,
|
||||
setMode = function(v) state.mode = v end,
|
||||
})
|
||||
return state
|
||||
end
|
||||
|
||||
local function press(state, key)
|
||||
pressed = { [key] = true }
|
||||
state.screen:update()
|
||||
pressed = {}
|
||||
end
|
||||
|
||||
local fresh = open()
|
||||
eq(fresh.screen.view, "root", "a fresh profile opens at the root")
|
||||
eq(#fresh.screen.rows, 6, "four folders and two modes")
|
||||
eq(fresh.screen.rows[4].label, "MONOCHROME")
|
||||
check(fresh.screen.rows[4].folder, "MONOCHROME is the fourth folder")
|
||||
eq(fresh.screen.rows[5].mode, "gbc")
|
||||
check(not fresh.screen.rows[5].folder, "an engine mode is a row and not a folder")
|
||||
|
||||
press(fresh, "a")
|
||||
eq(fresh.screen.view, "tiles", "A on a folder opens its grid one level down")
|
||||
eq(#fresh.screen.stackTrail, 1)
|
||||
press(fresh, "b")
|
||||
eq(fresh.screen.view, "root", "B steps back up to the root")
|
||||
eq(fresh.popped, 0)
|
||||
press(fresh, "b")
|
||||
eq(fresh.popped, 1, "B at the root closes the screen")
|
||||
|
||||
-- a stored selection should open straight into its grid
|
||||
local stored = open("p:BGB/Blue")
|
||||
eq(stored.screen.view, "tiles", "a stored palette opens in its folder")
|
||||
eq(stored.screen.group.key, "single", "and in the RIGHT folder")
|
||||
eq(stored.screen:idAt(stored.screen.index), "p:BGB/Blue",
|
||||
"with the cursor on the stored tile")
|
||||
eq(#stored.screen.stackTrail, 1, "the skipped level is on the trail")
|
||||
press(stored, "b")
|
||||
eq(stored.screen.view, "root", "B steps up instead of closing")
|
||||
eq(stored.popped, 0)
|
||||
eq(stored.screen.index, 2, "and lands on the folder it came from")
|
||||
|
||||
-- an engine mode and a palette share the same slot
|
||||
local modes = open("p:BGB/Blue")
|
||||
press(modes, "b")
|
||||
for _ = 1, 3 do press(modes, "down") end
|
||||
eq(modes.screen.rows[modes.screen.index].mode, "gbc", "walked to the SGB row")
|
||||
press(modes, "a")
|
||||
eq(modes.stored, "", "picking an engine mode clears the palette")
|
||||
eq(modes.mode, "gbc", "and sets the mode")
|
||||
|
||||
local pick = open()
|
||||
press(pick, "a")
|
||||
press(pick, "right")
|
||||
local wanted = pick.screen:idAt(pick.screen.index)
|
||||
press(pick, "a")
|
||||
eq(pick.stored, wanted, "A on a tile stores that tile's id")
|
||||
check(wanted ~= nil)
|
||||
end
|
||||
|
||||
-- ------- the picker's REAL wiring: PaletteScreen.new(game) with no opts
|
||||
--
|
||||
-- Everything above injects its own opts to keep update() engine-free, so
|
||||
-- none of it would catch a bug in liveOpts/dropConflictingMode. This
|
||||
-- builds the screen the way OptionsMenu actually does, with no opts at all.
|
||||
|
||||
do
|
||||
local game = {
|
||||
input = { wasPressed = function() return false end },
|
||||
stack = { states = {}, pop = function() end },
|
||||
save = { options = { colors = "redpp" } },
|
||||
}
|
||||
PaletteFX.mode = "redpp"
|
||||
local screen = PaletteScreen.new(game)
|
||||
check(screen.view == "root", "a live-opts screen opens at the root")
|
||||
check(type(screen.get) == "function" and type(screen.set) == "function",
|
||||
"liveOpts wired real get/set functions")
|
||||
eq(game.save.options.colors, "gbc",
|
||||
"opening the picker while ADVANCED was active dropped it to SGB "
|
||||
.. "(dropConflictingMode ran)")
|
||||
eq(PaletteFX.mode, "gbc", "and the live mode followed")
|
||||
|
||||
local id = Palette.packId("BGB", "Blue")
|
||||
screen.set(id)
|
||||
eq(game.save.options.palette, id, "storing a palette writes save.options.palette")
|
||||
eq(hex(PaletteFX.customRamp[1]), hex(Palette.ramp(id)[1]),
|
||||
"and PaletteFX.setCustomRamp actually ran")
|
||||
|
||||
screen.set("")
|
||||
check(PaletteFX.customRamp == nil, "storing empty clears the custom ramp")
|
||||
PaletteFX.mode = "gbc"
|
||||
end
|
||||
|
||||
-- ------- PaletteFX seam: a custom ramp wins outright
|
||||
|
||||
do
|
||||
PaletteFX.setCustomRamp(nil)
|
||||
PaletteFX.mode = "gbc"
|
||||
check(not PaletteFX.forcesRawGrays(), "no custom ramp: forcesRawGrays is false")
|
||||
|
||||
local id = Palette.packId("GalleryWebApp Palettes", "Original GB")
|
||||
local ramp = Palette.ramp(id)
|
||||
PaletteFX.setCustomRamp(ramp)
|
||||
check(PaletteFX.forcesRawGrays(), "a stored custom ramp: forcesRawGrays is true")
|
||||
|
||||
local someZoneColors = { { 9, 9, 9 }, { 8, 8, 8 }, { 7, 7, 7 }, { 6, 6, 6 } }
|
||||
local out = PaletteFX.effectiveColors(someZoneColors)
|
||||
for i = 1, 4 do
|
||||
eq(hex(out[i]), hex(ramp[i]),
|
||||
"effectiveColors[" .. i .. "] is the custom ramp, not the zone's own colours")
|
||||
end
|
||||
|
||||
-- it should win even over a mode with its own fixed palette
|
||||
PaletteFX.mode = "classic"
|
||||
local out2 = PaletteFX.effectiveColors(someZoneColors)
|
||||
eq(hex(out2[1]), hex(ramp[1]), "a custom ramp beats CLASSIC too")
|
||||
PaletteFX.mode = "gbc"
|
||||
end
|
||||
|
||||
-- ------- picker immunity
|
||||
--
|
||||
-- The picker's swatches are real RGB, so effectiveColors and ensureZones
|
||||
-- both have to back off while it's on top or it would recolour its own
|
||||
-- swatches.
|
||||
|
||||
do
|
||||
local Game = require("src.core.Game")
|
||||
|
||||
Game.stack = nil
|
||||
check(not PaletteFX.pickerActive(), "no stack: pickerActive is false")
|
||||
|
||||
Game.stack = { states = { { screenId = "OptionsMenu" } } }
|
||||
check(not PaletteFX.pickerActive(), "some other screen on top: pickerActive is false")
|
||||
|
||||
Game.stack = { states = { { screenId = "OptionsMenu" },
|
||||
{ screenId = "PaletteScreen" } } }
|
||||
check(PaletteFX.pickerActive(), "PaletteScreen on top of the stack: pickerActive is true")
|
||||
|
||||
local id = Palette.packId("BGB", "Red")
|
||||
local ramp = Palette.ramp(id)
|
||||
PaletteFX.setCustomRamp(ramp)
|
||||
PaletteFX.mode = "gbc"
|
||||
|
||||
local someZoneColors = { { 9, 9, 9 }, { 8, 8, 8 }, { 7, 7, 7 }, { 6, 6, 6 } }
|
||||
local out = PaletteFX.effectiveColors(someZoneColors)
|
||||
eq(hex(out[1]), hex(someZoneColors[1]),
|
||||
"with the picker on top, effectiveColors does NOT substitute the custom ramp")
|
||||
|
||||
check(PaletteFX.ensureZones(nil) == nil,
|
||||
"with the picker on top, ensureZones invents nothing for the custom ramp")
|
||||
|
||||
-- closing the picker should restore the substitution immediately
|
||||
Game.stack = { states = { { screenId = "OptionsMenu" } } }
|
||||
check(not PaletteFX.pickerActive(), "closing the picker: pickerActive is false again")
|
||||
local out2 = PaletteFX.effectiveColors(someZoneColors)
|
||||
eq(hex(out2[1]), hex(ramp[1]), "and the custom ramp substitutes again")
|
||||
local invented = PaletteFX.ensureZones(nil)
|
||||
check(invented and #invented == 1, "and ensureZones invents its zone again")
|
||||
|
||||
Game.stack = nil
|
||||
PaletteFX.setCustomRamp(nil)
|
||||
end
|
||||
|
||||
-- ------- PaletteFX seam: the invented zone, end to end
|
||||
--
|
||||
-- A state with no SGB zones of its own gets one invented by ensureZones,
|
||||
-- and effectiveColors then has to turn that zone's colours into the
|
||||
-- chosen ramp.
|
||||
|
||||
do
|
||||
PaletteFX.setCustomRamp(nil)
|
||||
check(PaletteFX.ensureZones(nil) == nil,
|
||||
"no custom ramp, no mono mode: ensureZones invents nothing")
|
||||
|
||||
local id = Palette.packId("BGB", "Red")
|
||||
local ramp = Palette.ramp(id)
|
||||
PaletteFX.setCustomRamp(ramp)
|
||||
|
||||
local invented = PaletteFX.ensureZones(nil)
|
||||
check(invented and #invented == 1, "a custom ramp invents one zone")
|
||||
eq(invented[1].w, 160, "the invented zone is whole-screen (w)")
|
||||
eq(invented[1].h, 144, "the invented zone is whole-screen (h)")
|
||||
for i = 1, 4 do
|
||||
eq(hex(invented[1].colors[i]), hex(PaletteFX.GRAYS[i]),
|
||||
"the invented zone is plain DMG grays, not the ramp itself")
|
||||
end
|
||||
|
||||
-- run it back through effectiveColors, the way Renderer.sendColors would
|
||||
local final = PaletteFX.effectiveColors(invented[1].colors)
|
||||
for i = 1, 4 do
|
||||
eq(hex(final[i]), hex(ramp[i]),
|
||||
"the invented zone resolves to the custom ramp at send time")
|
||||
end
|
||||
|
||||
local realZones = { { colors = { { 1, 1, 1 } }, x = 0, y = 0, w = 40, h = 40 } }
|
||||
check(PaletteFX.ensureZones(realZones) == realZones,
|
||||
"a state with real zones is left alone")
|
||||
|
||||
PaletteFX.setCustomRamp(nil)
|
||||
check(not PaletteFX.forcesRawGrays(), "clearing the ramp clears forcesRawGrays")
|
||||
end
|
||||
|
||||
-- ------- the four sync points
|
||||
--
|
||||
-- BattleState:picImage, BattleState:drawZonePass, WideBattle's monoMode and
|
||||
-- PaletteFX.ensureZones all gate on the same "forced raw grays" condition.
|
||||
-- This is a structural check (source-level, no ROM) that none of the four
|
||||
-- got missed when forcesRawGrays was added.
|
||||
|
||||
do
|
||||
local function read(path)
|
||||
local f = assert(io.open(path, "r"))
|
||||
local src = f:read("*a")
|
||||
f:close()
|
||||
return src
|
||||
end
|
||||
|
||||
local battleSrc = read("src/battle/BattleState.lua")
|
||||
local picImage = battleSrc:match("function BattleState:picImage%(img%)(.-)\nend")
|
||||
check(picImage and picImage:find("forcesRawGrays", 1, true) ~= nil,
|
||||
"picImage's mono check includes forcesRawGrays")
|
||||
|
||||
local drawZonePass = battleSrc:match(
|
||||
"function BattleState:drawZonePass%(src, sx, sy%)(.-)\nend")
|
||||
check(drawZonePass and drawZonePass:find("forcesRawGrays", 1, true) ~= nil,
|
||||
"drawZonePass's mono check includes forcesRawGrays")
|
||||
|
||||
local wideSrc = read("src/battle/WideBattle.lua")
|
||||
local monoMode = wideSrc:match("local function monoMode%(%)(.-)\nend")
|
||||
check(monoMode and monoMode:find("forcesRawGrays", 1, true) ~= nil,
|
||||
"WideBattle's monoMode includes forcesRawGrays")
|
||||
|
||||
local fxSrc = read("src/render/PaletteFX.lua")
|
||||
local ensureZones = fxSrc:match("function PaletteFX.ensureZones%(zones%)(.-)\nend")
|
||||
check(ensureZones and ensureZones:find("forcesRawGrays", 1, true) ~= nil,
|
||||
"ensureZones itself includes forcesRawGrays")
|
||||
end
|
||||
|
||||
-- ------- COLORS row wiring
|
||||
--
|
||||
-- OptionsMenu's colors row should open the picker instead of cycling in place.
|
||||
|
||||
do
|
||||
local optSrc = (function()
|
||||
local f = assert(io.open("src/ui/OptionsMenu.lua", "r"))
|
||||
local s = f:read("*a")
|
||||
f:close()
|
||||
return s
|
||||
end)()
|
||||
-- bounded to the row itself, or the scan would run into TILT's own step
|
||||
local block = optSrc:match('{ id = "colors",(.-)\n { id =')
|
||||
check(block ~= nil, "colors row block found")
|
||||
check(block:find("activate = function%(g%)") ~= nil,
|
||||
"colors row has an activate")
|
||||
check(block:find('Screens"%).push%(g, "PaletteScreen"%)', 1, false) ~= nil
|
||||
or block:find('Screens").push(g, "PaletteScreen")', 1, true) ~= nil,
|
||||
"colors row's activate pushes PaletteScreen")
|
||||
check(block:find("step = function") == nil,
|
||||
"colors row no longer has a step")
|
||||
end
|
||||
|
||||
T.finish("palette")
|
||||
@@ -180,6 +180,58 @@ do
|
||||
T.eq(eng.phase, "idle", "and the sync ends idle")
|
||||
end
|
||||
|
||||
-- Identical playtime is not a fork. The Gold prompt the player saw offered
|
||||
-- "GOLD - 8 badges - 3:46 - 9 seen" against "GOLD - 8 badges - 3:46 - 9 seen":
|
||||
-- the same minute of the same playthrough, two saves that differ only in
|
||||
-- their savedAt stamp. Nothing to choose between, so nothing to ask.
|
||||
do
|
||||
T.eq(SyncEngine.samePlaytime(
|
||||
{ summary = { timeText = "3:46" } },
|
||||
{ summary = { timeText = "3:46" } }), true,
|
||||
"the same H:MM is the same point in the playthrough")
|
||||
T.eq(SyncEngine.samePlaytime(
|
||||
{ summary = { timeText = "3:46" } },
|
||||
{ summary = { timeText = "3:47" } }), false,
|
||||
"one minute apart is a real fork")
|
||||
T.eq(SyncEngine.samePlaytime({ playTime = 13560 }, { playTime = 13599 }),
|
||||
true, "Gen 1's seconds count compares to the minute, not the second")
|
||||
T.eq(SyncEngine.samePlaytime({ playTime = 13560 }, { playTime = 13620 }),
|
||||
false, "and a whole minute apart still forks")
|
||||
-- Gen 2 stores playTime as a table, so meta.playTime is nil on a Gold
|
||||
-- save and only timeText survives; a missing time must never read as a
|
||||
-- match, or an unknowable conflict would be silently discarded.
|
||||
T.eq(SyncEngine.samePlaytime({}, {}), false,
|
||||
"two unknown playtimes are not a match")
|
||||
T.eq(SyncEngine.samePlaytime({ summary = { timeText = "3:46" } }, {}), false,
|
||||
"and neither is one unknown side")
|
||||
end
|
||||
|
||||
do
|
||||
local state = linkedState()
|
||||
SyncState.setRev(state, "red/abc", 7, 500)
|
||||
local entry = saveEntry("red", "abc", 700, 650)
|
||||
entry.meta.summary.timeText = "3:46"
|
||||
local eng, transport = engine({
|
||||
["GET /sync/state"] = { code = 200,
|
||||
body = '{"saves":{"red/abc":{"rev":9,"meta":{"savedAt":760,' ..
|
||||
'"sessionStart":600,"summary":{"name":"ASH","timeText":"3:46"}}}}}' },
|
||||
["PUT /sync/save"] = { code = 200, body = '{"ok":true,"rev":10}' },
|
||||
}, { entry }, state)
|
||||
|
||||
eng:syncNow()
|
||||
pump(eng)
|
||||
T.eq(#eng.conflicts, 0, "matching playtime raises no conflict")
|
||||
T.eq(#eng.state.pendingConflicts, 0, "and leaves nothing pending")
|
||||
T.neq(eng.phase, "conflict", "so the player is never prompted")
|
||||
local put
|
||||
for _, req in ipairs(transport.sent) do
|
||||
if req.method == "PUT" then put = req end
|
||||
end
|
||||
T.check(put ~= nil, "this device's copy is pushed instead")
|
||||
T.check(put and put.body:find('"force":true', 1, true) ~= nil,
|
||||
"forced past the moved rev, since there is nothing to preserve")
|
||||
end
|
||||
|
||||
local function conflictEngine()
|
||||
local state = linkedState()
|
||||
SyncState.setRev(state, "red/abc", 7, 500)
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
-- UI LETTERBOX (save.options.uiLetterbox): what fills the window around a
|
||||
-- 160x144 screen. AUTO keeps what each screen was authored with, so the
|
||||
-- default is byte-identical to the look before the option existed.
|
||||
-- luajit tests/engine/ui_letterbox_test.lua
|
||||
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.modkit")
|
||||
local Letterbox = require("src.render.Letterbox")
|
||||
|
||||
T.eq(require("src.core.SaveData").newGame().options.uiLetterbox, "auto",
|
||||
"Gen 1 starts on AUTO")
|
||||
T.eq(require("src.core.gen2.Save").DEFAULT_OPTIONS.uiLetterbox, "auto",
|
||||
"so does Gen 2")
|
||||
|
||||
-- AUTO hands back the colour the caller was authored with, untouched: the
|
||||
-- Pokedex stays black, a menu stays white, the naming screen stays paper.
|
||||
Letterbox.setMode("auto")
|
||||
T.same({ Letterbox.fill(0, 0, 0) }, { 0, 0, 0 }, "AUTO keeps an authored black")
|
||||
T.same({ Letterbox.fill(1, 1, 1) }, { 1, 1, 1 }, "AUTO keeps an authored white")
|
||||
T.same({ Letterbox.fill(0.5, 0.25, 0.75) }, { 0.5, 0.25, 0.75 },
|
||||
"AUTO keeps an authored paper colour")
|
||||
|
||||
-- PALETTE reads the caller's paper, because the two generations keep a picked
|
||||
-- ramp in different modules. Gen 1 used to come back white here: anything that
|
||||
-- sniffed for GbcPalette found it present on Gen 1 too and answered with Gen
|
||||
-- 2's white default instead of PaletteFX's actual paper.
|
||||
Letterbox.setMode("palette")
|
||||
T.same({ Letterbox.fill(0, 0, 0, function() return 0.25, 0.5, 1 end) },
|
||||
{ 0.25, 0.5, 1 }, "PALETTE takes the caller's paper")
|
||||
T.same({ Letterbox.fill(0, 0, 0) }, { 0, 0, 0 },
|
||||
"with no paper reader it falls back to the authored colour, never to white")
|
||||
T.same({ Letterbox.fill(1, 1, 1, function() return nil end) }, { 1, 1, 1 },
|
||||
"and a reader that cannot answer falls back too")
|
||||
|
||||
Letterbox.setMode("black")
|
||||
T.same({ Letterbox.fill(1, 1, 1) }, { 0, 0, 0 }, "BLACK overrides a white screen")
|
||||
Letterbox.setMode("white")
|
||||
T.same({ Letterbox.fill(0, 0, 0) }, { 1, 1, 1 }, "WHITE overrides a black screen")
|
||||
|
||||
-- An unknown or absent value degrades to AUTO rather than to a colour, so a
|
||||
-- save written before the option existed keeps its look.
|
||||
T.eq(Letterbox.normalize(nil), "auto", "no stored value is AUTO")
|
||||
T.eq(Letterbox.normalize("nonsense"), "auto", "and so is a value we cannot read")
|
||||
Letterbox.setMode(nil)
|
||||
T.same({ Letterbox.fill(0, 0, 0) }, { 0, 0, 0 }, "so an old save is unchanged")
|
||||
|
||||
-- The ladder wraps both ways, like every other option row.
|
||||
T.eq(Letterbox.cycle("auto", 1), "black", "right steps off AUTO")
|
||||
T.eq(Letterbox.cycle("palette", 1), "auto", "and wraps at the end")
|
||||
T.eq(Letterbox.cycle("auto", -1), "palette", "left wraps back")
|
||||
for _, id in ipairs(Letterbox.MODES) do
|
||||
T.check(Letterbox.label(id) ~= nil, id .. " has a label")
|
||||
end
|
||||
T.eq(Letterbox.label("uiLetterbox"), "AUTO", "an unknown id still labels")
|
||||
|
||||
-- applyOptions is the seam both Game:applyOptions and Game2:applyOptions call.
|
||||
Letterbox.applyOptions({ uiLetterbox = "white" })
|
||||
T.eq(Letterbox.mode, "white", "applyOptions installs the saved mode")
|
||||
Letterbox.applyOptions({})
|
||||
T.eq(Letterbox.mode, "auto", "and an options table without one is AUTO")
|
||||
|
||||
Letterbox.setMode("auto")
|
||||
T.finish("ui letterbox")
|
||||
+52
-36
@@ -360,33 +360,51 @@ local options = OptionsMenu.new(optionsGame, {
|
||||
-- speed, display, SHADER FX + SHADER FX 2 (the second slot added alongside
|
||||
-- the dual-shader feature), video mode, screen position, the mobile-gated
|
||||
-- touch three (buildRows), MAX FPS, BATTLE BG and CANCEL.
|
||||
check("twenty-seven rows", #OptionsMenu.ROWS, 27)
|
||||
check("twenty-eight rows", #OptionsMenu.ROWS, 28)
|
||||
check("the cart's rows come first", OptionsMenu.ROWS[7].key, "frame")
|
||||
check("then the rebind screen", OptionsMenu.ROWS[8].id, "controls")
|
||||
check("then the port's audio group", OptionsMenu.ROWS[9].key, "musicVol")
|
||||
check("last row is CANCEL", OptionsMenu.ROWS[#OptionsMenu.ROWS].cancel, true)
|
||||
check("starts on TEXT SPEED", options:row().key, "textSpeed")
|
||||
check("default text speed", options.options.textSpeed, "MID")
|
||||
check("last row is BACK", OptionsMenu.ROWS[#OptionsMenu.ROWS].cancel, true)
|
||||
-- PRINT is wGBPrinterBrightness and there is no Game Boy Printer here, so
|
||||
-- buildRows hides it: the descriptor and the save key survive, the row does
|
||||
-- not reach the screen.
|
||||
local function hasRow(rows, key)
|
||||
for _, row in ipairs(rows) do if row.key == key then return true end end
|
||||
return false
|
||||
end
|
||||
check("PRINT is still a descriptor", hasRow(OptionsMenu.ROWS, "print"), true)
|
||||
check("but never reaches the screen", hasRow(options.rows, "print"), false)
|
||||
check("and the save keeps its value",
|
||||
Save.defaultOptions().print ~= nil, true)
|
||||
-- The rows are grouped into pages now, so the top level opens on the SPEED
|
||||
-- group and TEXT SPEED is the first row of the page it opens.
|
||||
check("starts on the SPEED group", options:row().id, "group.speed")
|
||||
local speedPage = options:focusRow("textSpeed")
|
||||
check("TEXT SPEED is on a page", speedPage ~= options, true)
|
||||
check("and the cursor lands on it", speedPage:row().key, "textSpeed")
|
||||
check("default text speed", speedPage.options.textSpeed, "MID")
|
||||
optionsInput:press("right")
|
||||
options:update(0)
|
||||
speedPage:update(0)
|
||||
check("right cycles forward", options.options.textSpeed, "SLOW")
|
||||
optionsInput:press("right")
|
||||
options:update(0)
|
||||
speedPage:update(0)
|
||||
check("right wraps", options.options.textSpeed, "FAST")
|
||||
optionsInput:press("left")
|
||||
options:update(0)
|
||||
speedPage:update(0)
|
||||
check("left wraps back", options.options.textSpeed, "SLOW")
|
||||
check("a page edits the caller's own options table",
|
||||
speedPage.options == options.options, true)
|
||||
|
||||
optionsInput:press("down")
|
||||
options:update(0)
|
||||
check("down moves a row", options:row().key, "battleScene")
|
||||
local battlePage = options:focusRow("battleScene")
|
||||
check("BATTLE SCENE is on the battle page", battlePage:row().key, "battleScene")
|
||||
optionsInput:press("right")
|
||||
options:update(0)
|
||||
battlePage:update(0)
|
||||
check("battle scene toggles off", options.options.battleScene, false)
|
||||
|
||||
-- FRAME is 1-8 and wraps.
|
||||
options.index = 7
|
||||
check("frame row", options:row().frame, true)
|
||||
local framePage = options:focusRow("frame")
|
||||
check("frame row", framePage:row().frame, true)
|
||||
options = framePage
|
||||
options.options.frame = 8
|
||||
optionsInput:press("right")
|
||||
options:update(0)
|
||||
@@ -406,10 +424,10 @@ local exiting = OptionsMenu.new(exitGame, {
|
||||
})
|
||||
-- The screen's own rows, not ROWS: buildRows drops the touch three off a
|
||||
-- desktop, so the raw descriptor count overshoots CANCEL.
|
||||
exiting.index = #exiting.rows
|
||||
exiting.index = #exiting.view
|
||||
exitInput:press("a")
|
||||
exiting:update(0)
|
||||
check("CANCEL leaves", savedOptions ~= nil, true)
|
||||
check("BACK leaves", savedOptions ~= nil, true)
|
||||
|
||||
-- --------------------------------------------------------- start menu
|
||||
|
||||
@@ -934,16 +952,13 @@ local colorRow = select(2, rowNamed("COLOR"))
|
||||
check("COLOR is a row", colorRow ~= nil, true)
|
||||
check("and it defaults to the cart's own colour",
|
||||
Save.DEFAULT_OPTIONS.color, "gbc")
|
||||
scrollOptions.options.color = "gbc"
|
||||
scrollOptions:cycle(colorRow, 1)
|
||||
check("right steps to DMG", scrollOptions.options.color, "dmg")
|
||||
scrollOptions:cycle(colorRow, 1)
|
||||
check("then CLASSIC", scrollOptions.options.color, "classic")
|
||||
scrollOptions:cycle(colorRow, 1)
|
||||
check("and wraps back to GBC", scrollOptions.options.color, "gbc")
|
||||
scrollOptions:cycle(colorRow, -1)
|
||||
check("left walks the ladder the other way", scrollOptions.options.color,
|
||||
"classic")
|
||||
-- COLOR no longer cycles in place (tests/engine/gen2_palette_picker_test.lua
|
||||
-- covers the picker it opens instead, end to end); the ladder itself still
|
||||
-- steps via the `2` hotkey (src/core/Game2.lua:hotkey), untouched here.
|
||||
check("COLOR no longer cycles in place", colorRow.cycle, nil)
|
||||
check("COLOR opens the picker instead", type(colorRow.activate), "function")
|
||||
scrollOptions.options.color = "classic"
|
||||
GbcPalette.setMode("classic")
|
||||
check("CLASSIC is the only mode with a present pass",
|
||||
GbcPalette.presentColors() ~= nil, true)
|
||||
GbcPalette.setMode("dmg")
|
||||
@@ -965,16 +980,16 @@ check("VOID FILL follows ZOOM", OptionsMenu.ROWS[zoomIndex + 1].label,
|
||||
"VOID FILL")
|
||||
check("and TILT follows VOID FILL", OptionsMenu.ROWS[zoomIndex + 2].label,
|
||||
"TILT")
|
||||
check("SHADER FX follows COLOR follows TILT", OptionsMenu.ROWS[tiltIndex + 2].label,
|
||||
"SHADER FX")
|
||||
check("SHADER FX 2 follows SHADER FX", OptionsMenu.ROWS[tiltIndex + 3].label,
|
||||
"SHADER FX 2")
|
||||
check("VIDEO MODE follows SHADER FX 2", OptionsMenu.ROWS[tiltIndex + 4].label,
|
||||
"VIDEO MODE")
|
||||
check("and SCREEN POS follows it", OptionsMenu.ROWS[tiltIndex + 5].label,
|
||||
"SCREEN POS")
|
||||
check("and TOUCH PAD follows that", OptionsMenu.ROWS[tiltIndex + 6].label,
|
||||
"TOUCH PAD")
|
||||
-- By sequence rather than by offset, so inserting a row in the display block
|
||||
-- moves the whole run instead of breaking six separate index assertions.
|
||||
do
|
||||
local run = { "TILT", "COLOR", "UI LETTERBOX", "SHADER FX",
|
||||
"SHADER FX 2", "VIDEO MODE", "SCREEN POS", "TOUCH PAD" }
|
||||
for at, want in ipairs(run) do
|
||||
check("display block order: " .. want,
|
||||
OptionsMenu.ROWS[tiltIndex + at - 1].label, want)
|
||||
end
|
||||
end
|
||||
|
||||
local videoRow = select(2, rowNamed("VIDEO MODE"))
|
||||
check("VIDEO MODE is a row", videoRow ~= nil, true)
|
||||
@@ -2518,7 +2533,8 @@ local function modRowChecks()
|
||||
local og, oi = newGame(nil)
|
||||
local om = OptionsMenu.new(og, { options = Save.defaultOptions() })
|
||||
check("gen2 OPTION takes the hook's row", om.rows[#om.rows].id, "modrow")
|
||||
om.index = #om.rows
|
||||
-- A hook row is in no group, so it stays on the top level.
|
||||
check("and keeps it reachable there", om:focusRow("modrow"), om)
|
||||
oi:press("a")
|
||||
om:update(0)
|
||||
check("A on a mod row calls activate", fired, 1)
|
||||
|
||||
@@ -146,8 +146,23 @@ Chrome.cursor = function(tx, ty) cursorAt = { x = tx, y = ty } end
|
||||
Chrome.clear = function() end
|
||||
Chrome.textbox = function() end
|
||||
|
||||
local options = OptionsMenu.new({}, { options = {} })
|
||||
options:drawPanel()
|
||||
-- TEXT SPEED and FRAME sit on group pages now; the pages are the same
|
||||
-- screen and draw at the same StringOptions coordinates, which is what this
|
||||
-- pins. A stack, because opening a page pushes one.
|
||||
local optStack = { items = {} }
|
||||
function optStack:push(s) self.items[#self.items + 1] = s; return s end
|
||||
function optStack:pop() self.items[#self.items] = nil end
|
||||
function optStack:top() return self.items[#self.items] end
|
||||
|
||||
local options = OptionsMenu.new({ stack = optStack }, { options = {} })
|
||||
options:focusRow("textSpeed"):drawPanel()
|
||||
local framePage = options:focusRow("frame")
|
||||
framePage.index = 1
|
||||
for i, row in ipairs(framePage:visible()) do
|
||||
if row.frame then framePage.index = i end
|
||||
end
|
||||
framePage:ensureVisible()
|
||||
framePage:drawPanel()
|
||||
|
||||
Chrome.print, Chrome.cursor = realPrint, realCursor
|
||||
Chrome.clear, Chrome.textbox = realClear, realBox
|
||||
|
||||
@@ -293,9 +293,8 @@ do
|
||||
end },
|
||||
stack = { pop = function() end } }
|
||||
local menu = OptionsMenu.new(game)
|
||||
for i, row in ipairs(menu.rows) do
|
||||
if row.id == "ruleset" then menu.index = i end
|
||||
end
|
||||
-- RULESET is in no group, so this focuses it on the top level.
|
||||
check(menu:focusRow("ruleset") == menu, "RULESET focuses in place")
|
||||
local function press(key)
|
||||
pressed = { [key] = true }
|
||||
menu:update(1 / 60)
|
||||
|
||||
@@ -285,7 +285,7 @@ local WANT_IDS = { "textSpeed", "animations", "battleStyle", "battleLayout",
|
||||
"battleFit", "battleHud", "battleBg", "uiLayout",
|
||||
"ruleset", "musicVol", "sfxVol", "musicFilter",
|
||||
"performance", "colors",
|
||||
"tilt", "shaderfx", "shaderfx2", "zoom", "voidFill",
|
||||
"tilt", "uiLetterbox", "shaderfx", "shaderfx2", "zoom", "voidFill",
|
||||
"videoMode", "faithfulRes", "screenPos", "fpsCap",
|
||||
"speedOverworld", "speedBattle", "speedMenu",
|
||||
"mods", "controls", "dateFormat", "timeFormat" }
|
||||
@@ -369,10 +369,10 @@ check(om.game.save.options.voidFill == "trees", "VOID FILL wraps to TREES")
|
||||
-- mirrors it one row down, opening the same shared screen on
|
||||
-- "secondary" instead.
|
||||
local ShaderFX = require("src.render.ShaderFX")
|
||||
check(om.rows[16].id == "shaderfx", "row 16 is the SHADER FX row")
|
||||
check(om.rows[16].value(om.game) == "OFF", "SHADER FX shows OFF with no presets")
|
||||
check(om.rows[16].step == nil, "SHADER FX row has no step() any more")
|
||||
om.rows[16].activate(om.game)
|
||||
local sfx = orow(om, "shaderfx")
|
||||
check(sfx.value(om.game) == "OFF", "SHADER FX shows OFF with no presets")
|
||||
check(sfx.step == nil, "SHADER FX row has no step() any more")
|
||||
sfx.activate(om.game)
|
||||
local sfxScreen = om.game.stack:top()
|
||||
check(sfxScreen and sfxScreen.title == "SHADER FX",
|
||||
"SHADER FX row.activate() pushes a ShaderFXScreen")
|
||||
@@ -383,9 +383,9 @@ sfxScreen.onChoose(sfxScreen.items[1])
|
||||
check(ShaderFX.active("main") == false, "choosing OFF on an empty list stays a safe no-op")
|
||||
check(om.game.stack:top() == nil, "ShaderFXScreen pops itself after onChoose")
|
||||
|
||||
check(om.rows[17].id == "shaderfx2", "row 17 is the SHADER FX 2 row")
|
||||
check(om.rows[17].value(om.game) == "OFF", "SHADER FX 2 shows OFF with no presets")
|
||||
om.rows[17].activate(om.game)
|
||||
local sfx2 = orow(om, "shaderfx2")
|
||||
check(sfx2.value(om.game) == "OFF", "SHADER FX 2 shows OFF with no presets")
|
||||
sfx2.activate(om.game)
|
||||
local sfx2Screen = om.game.stack:top()
|
||||
check(sfx2Screen and sfx2Screen.title == "SHADER FX 2",
|
||||
"SHADER FX 2 row.activate() pushes the shared ShaderFXScreen on the secondary slot")
|
||||
|
||||
Reference in New Issue
Block a user