Strip Gen 2 text terminator from intro and gender select, use × glyph in Gen 1 item lists

This commit is contained in:
bryanthaboi
2026-09-02 18:05:09 -04:00
parent 61c416b23e
commit 4e32ca1de3
21 changed files with 304 additions and 27 deletions
+2 -2
View File
@@ -2612,11 +2612,11 @@ function BattleState:openOldManBag()
-- inventory) differs by version: pokered's OldManItemList has 50
-- POKé BALLs; pokeyellow's SimulatedInputBattleItemList, shared by
-- the Viridian tutorial and Oak's catch, has one.
local qty = require("src.core.GameVersion").isYellow() and "x1" or "x50"
local qty = require("src.core.GameVersion").isYellow() and 1 or 50
-- the tutorial bag rides DisplayBagMenu's LIST_MENU_BOX over the battle
-- screen (engine/battle/core.asm:2210)
list = ListMenu.new(game, "ITEMS", {
{ value = "POKE_BALL", label = Strings("POKé BALL"), right = qty },
{ value = "POKE_BALL", label = Strings("POKé BALL"), count = qty },
}, {
itemBox = true,
script = function(l)
+2 -2
View File
@@ -24,7 +24,7 @@ local function buildItems(game)
table.insert(items, {
value = id,
label = def and def.name or id,
right = (not unsellable) and ("x" .. game.save.inventory[id]) or nil,
count = (not unsellable) and game.save.inventory[id] or nil,
})
end
-- the $ff terminator's row: CANCEL is selectable and exits like B
@@ -346,7 +346,7 @@ local function vanillaUseOn(game, battle, id, target, list, moveIndex, picker)
for i, it in ipairs(list.items) do
if it.value == id then
local left = game.save.inventory[id]
if left then it.right = "x" .. left else table.remove(list.items, i) end
if left then it.count = left else table.remove(list.items, i) end
break
end
end
+5 -3
View File
@@ -310,12 +310,14 @@ function ListMenu:drawItemBox()
elseif item.price then
-- home/list_menu.asm:410-424
Font.draw(item.price, ITEM_QTY_END - Font.width(item.price), y + 8)
elseif item.right then
elseif item.count then
-- '×' at column 14, PrintNumber's two right-aligned digits after it
-- (home/list_menu.asm:479-490)
local count = item.right:sub(2)
Font.draw(item.right:sub(1, 1), ITEM_QTY_X, y + 8)
local count = tostring(item.count)
Font.draw("\xc3\x97", ITEM_QTY_X, y + 8)
Font.draw(count, ITEM_QTY_END - Font.width(count), y + 8)
elseif item.right then
Font.draw(item.right, ITEM_QTY_END - Font.width(item.right), y + 8)
end
if i == self.index and (self.cursorBlank or 0) == 0 then
Font.drawCode(self.hollowIndex == i
+2 -2
View File
@@ -34,7 +34,7 @@ local function buildItems(game, store, order)
table.insert(items, {
value = id,
label = itemName(game, id),
right = "x" .. store[id],
count = store[id],
})
end
end
@@ -77,7 +77,7 @@ local function refreshRow(list, store, id)
for i, it in ipairs(list.items) do
if it.value == id then
if store[id] then
it.right = "x" .. store[id]
it.count = store[id]
else
table.remove(list.items, i)
end
+2 -2
View File
@@ -123,7 +123,7 @@ local function sellItems(game)
table.insert(items, {
value = id,
label = def and def.name or id,
right = (not keyed) and ("x" .. game.save.inventory[id]) or nil,
count = (not keyed) and game.save.inventory[id] or nil,
})
end
items[#items + 1] = { cancel = true, label = Strings("CANCEL") }
@@ -207,7 +207,7 @@ local function sell(game, menu)
Bag.remove(game.save, item.value, qty)
local left = game.save.inventory[item.value]
if left then
item.right = "x" .. left
item.count = left
else
list:removeCurrent()
end
+3 -2
View File
@@ -2,6 +2,7 @@
-- PlayerProfileSetup runs before OakSpeech (engine/menus/intro_menu.asm:61-83).
local Chrome = require("src.ui.gen2.Chrome")
local CommonText = require("src.core.gen2.CommonText")
local Music = require("src.core.Music")
local RomText = require("src.core.RomText")
local Sound = require("src.core.Sound")
@@ -56,8 +57,8 @@ function GenderSelect.new(game, opts)
-- `db 1 ; default option`: the cursor opens on Boy.
self.cursor = 1
self.exit = nil
self.text = RomText(self.data, "_AreYouABoyOrAreYouAGirlText",
FALLBACK)
self.text = CommonText.plain(RomText(self.data,
"_AreYouABoyOrAreYouAGirlText", FALLBACK))
return self
end
+2 -1
View File
@@ -37,6 +37,7 @@
-- love.graphics.newImage skips overrides/ and AssetTransform output.
local Assets = require("src.render.Assets")
local Chrome = require("src.ui.gen2.Chrome")
local CommonText = require("src.core.gen2.CommonText")
local FieldMoves = require("src.world.gen2.FieldMoves")
local Font = require("src.render.Font")
local Gen2Save = require("src.core.gen2.Save")
@@ -447,7 +448,7 @@ function OakSpeech:openNamePick(step)
end
function OakSpeech:lastPageLines(key)
local body = self:text(key)
local body = CommonText.plain(self:text(key))
local pages = {}
for page in (body .. "\f"):gmatch("(.-)\f") do
pages[#pages + 1] = page
+2 -1
View File
@@ -26,6 +26,7 @@ local FlagNames = require("src.core.gen2.FlagNames")
local GbcPalette = require("src.render.GbcPalette")
local Gen2Save = require("src.core.gen2.Save")
local Clock = require("src.core.gen2.Clock")
local CommonText = require("src.core.gen2.CommonText")
local Font = require("src.render.Font")
local Palettes = require("src.world.gen2.Palettes")
local Phone = require("src.core.gen2.Phone")
@@ -1141,7 +1142,7 @@ function Pokegear:phoneText(name)
if not entry then return "" end
local text = self.textData
or (self.game and self.game.world and self.game.world.text)
local extracted = text and text[entry.key]
local extracted = CommonText.plain(text and text[entry.key])
if extracted and extracted ~= "" then return extracted end
return Strings(entry.body)
end
+127
View File
@@ -0,0 +1,127 @@
-- home/list_menu.asm:478-494
-- POKEPORT_IDENTITY=red-aug28 POKEPORT_VERSION=red POKEPORT_DRIVER=tests/drivers/gen1_mart_sell_glyph_test.lua POKEPORT_SHOT_DIR=/tmp/shots love .
return function(game)
local U = dofile("tests/drivers/util.lua")
local Bag = require("src.inventory.Bag")
local Flags = require("src.script.Flags")
local ListMenu = require("src.ui.ListMenu")
local DIR = os.getenv("SHOT_DIR") or os.getenv("POKEPORT_SHOT_DIR") or "/tmp/shots"
local failures = 0
local function check(label, ok)
if not ok then failures = failures + 1 end
U.log(ok and "PASS" or "FAIL", label)
return ok
end
local function topList()
local top = game.stack:top()
if top and getmetatable(top) == ListMenu and top.itemBox then return top end
return nil
end
local function waitForList(frames)
for _ = 1, frames do
local l = topList()
if l then return l end
U.wait(1)
end
return nil
end
local function checkRows(name, list)
if not check(name .. ": an item ListMenu is open", list ~= nil) then return end
local counted, ascii = 0, 0
for _, it in ipairs(list.items) do
if type(it.count) == "number" then counted = counted + 1 end
if type(it.right) == "string" and it.right:sub(1, 1) == "x" then ascii = ascii + 1 end
end
check(name .. ": rows carry numeric counts (" .. counted .. ")", counted > 0)
check(name .. ": no row carries an ASCII 'x' prefix", ascii == 0)
end
local function closeAll()
for _ = 1, 8 do
if game.stack:top() == game.overworld then break end
U.tap(game, "b")
U.wait(8)
end
end
U.teleport(game, "PALLET_TOWN", 10, 8, "down")
game.save.inventory = {}
game.save.bagOrder = nil
for _, row in ipairs({ { "POTION", 6 }, { "POKE_BALL", 5 }, { "TM34", 1 },
{ "RARE_CANDY", 2 }, { "ANTIDOTE", 12 } }) do
Bag.add(game.save, row[1], row[2], game.data)
end
game.save.pcItems = { POTION = 3, ANTIDOTE = 10, ESCAPE_ROPE = 1 }
Flags.set(game.save, "EVENT_GOT_STARTER")
Flags.set(game.save, "EVENT_GOT_OAKS_PARCEL")
Flags.set(game.save, "EVENT_OAK_GOT_PARCEL")
local MAP = "VIRIDIAN_MART"
local list
for _, stand in ipairs({ { 2, 5 }, { 1, 5 } }) do
U.teleport(game, MAP, stand[1], stand[2], "left")
U.wait(20)
U.tap(game, "a")
U.wait(40)
for _ = 1, 6 do
local top = game.stack:top()
if top and top.items and top.items[2] and top.items[2].label == "SELL" then
top.index = 2
U.tap(game, "a")
break
end
U.tap(game, "a")
U.wait(20)
end
list = waitForList(60)
if list then break end
closeAll()
end
checkRows("mart SELL", list)
U.wait(10)
U.shot(game, DIR .. "/mart_sell.png")
closeAll()
U.wait(20)
U.teleport(game, "PALLET_TOWN", 10, 8, "down")
U.wait(10)
U.tap(game, "start")
U.wait(15)
local menu = game.stack:top()
if menu and menu.items then
for i, it in ipairs(menu.items) do
if it.label == "ITEM" then menu.index = i end
end
end
U.tap(game, "a")
list = waitForList(60)
checkRows("bag", list)
U.wait(10)
U.shot(game, DIR .. "/bag.png")
closeAll()
U.wait(20)
local pc = (game.data.field.hiddenExtras.pcTiles.REDS_HOUSE_2F or {})[1]
or { x = 0, y = 1 }
U.teleport(game, "REDS_HOUSE_2F", pc.x, pc.y + 1, "up")
U.wait(20)
U.tap(game, "a")
U.wait(40)
for _ = 1, 6 do
if topList() then break end
U.tap(game, "a")
U.wait(20)
end
list = waitForList(60)
checkRows("PC WITHDRAW", list)
U.wait(10)
U.shot(game, DIR .. "/pc_withdraw.png")
U.log(failures == 0 and "PASS" or "FAIL", "gen1 quantity glyph driver;",
"look for '×' (not 'x') before each count in", DIR)
love.event.quit()
end
+11
View File
@@ -143,10 +143,21 @@ return function(game)
-- Back into Oak for the last text page and the shrink, then the world. This
-- one has to keep pressing A: the remaining pages are text boxes waiting on a
-- button, so a passive wait would sit there forever.
local shrinkShot = false
for _ = 1, 500 do
if game.phase == "play" and game.world and game.world.map then break end
local state = top()
if not shrinkShot and isA(OakSpeech) and state.shrinkText then
shrinkShot = true
local shown = table.concat(state.shrinkText, "|")
assert(not shown:find("DONE", 1, true),
"shrink page prints the text terminator: " .. shown)
U.wait(6)
U.shot(game, out .. "/08b-shrink.png")
end
tap("a", 2)
end
assert(shrinkShot, "never saw the ShrinkPlayer page of _OakText7")
assert(game.phase == "play" and game.world and game.world.map,
"never reached the overworld (top is " .. tostring(top()) .. ")")
assert(game.save.player.name == "AB",
+2 -2
View File
@@ -33,7 +33,7 @@ end
local function newList(count)
local items = {}
for i = 1, count do
items[i] = { value = "ITEM_" .. i, label = "ITEM " .. i, right = "x" .. i }
items[i] = { value = "ITEM_" .. i, label = "ITEM " .. i, count = i }
end
return ListMenu.new({}, "ITEMS", items, { kind = "bag", itemBox = true })
end
@@ -67,7 +67,7 @@ do
-- the quantity: '×' at column 14, the count right-aligned after it
check(found("draw", function(c)
return c[2] == "x" and c[3] == 112 and c[4] == 40
return c[2] == "\xc3\x97" and c[3] == 112 and c[4] == 40
end) ~= nil, "the first quantity's '×' is a row down at column 14")
check(found("draw", function(c)
return c[2] == "1" and c[3] == 128 and c[4] == 40
@@ -153,7 +153,7 @@ do
local game = freshGame()
local ListMenu = require("src.ui.ListMenu")
local list = ListMenu.new(game, "ITEMS", {
{ value = "POKE_BALL", label = "POKé BALL", right = "x50" },
{ value = "POKE_BALL", label = "POKé BALL", count = 50 },
}, { itemBox = true, script = function(l) l.hollowIndex = l.index end })
game.stack:push(list)
list:update(1 / 60)
+1 -1
View File
@@ -30,7 +30,7 @@ local input = {
local function newList(count)
local items = {}
for i = 1, count do
items[i] = { value = "ITEM_" .. i, label = "ITEM " .. i, right = "x" .. i }
items[i] = { value = "ITEM_" .. i, label = "ITEM " .. i, count = i }
end
local list = ListMenu.new({ input = input }, "ITEMS", items,
{ kind = "bag", itemBox = true })
+87
View File
@@ -0,0 +1,87 @@
-- home/list_menu.asm:478-494
-- luajit tests/engine/list_quantity_glyph.lua
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.harness")
local check, eq = T.check, T.eq
love = love or require("tests.love_stub")
local realFont = package.loaded["src.render.Font"]
local calls = {}
package.loaded["src.render.Font"] = {
BORDER = { tl = 1, tr = 2, bl = 3, br = 4, h = 5, v = 6 },
draw = function(text, x, y) calls[#calls + 1] = { "draw", text, x, y } end,
drawCode = function(code, x, y) calls[#calls + 1] = { "code", code, x, y } end,
drawBox = function(tx, ty, tw, th) calls[#calls + 1] = { "box", tx, ty, tw, th } end,
width = function(text)
local n = 0
for _ in tostring(text):gmatch("[%z\1-\127\194-\244][\128-\191]*") do n = n + 1 end
return n * 8
end,
}
package.loaded["src.ui.ListMenu"] = nil
package.loaded["src.ui.Theme"] = nil
local ListMenu = require("src.ui.ListMenu")
local TIMES = "\xc3\x97"
local function found(kind, pred)
for _, c in ipairs(calls) do
if c[1] == kind and pred(c) then return c end
end
return nil
end
local function rowDraws(y)
local out = {}
for _, c in ipairs(calls) do
if c[1] == "draw" and c[4] == y then out[#out + 1] = c end
end
return out
end
do
local list = ListMenu.new({}, "ITEMS", {
{ value = "POTION", label = "POTION", count = 6 },
{ value = "POKE_BALL", label = "POKé BALL", count = 12 },
{ value = "BICYCLE", label = "BICYCLE" },
{ cancel = true, label = "CANCEL" },
}, { kind = "bag", itemBox = true })
calls = {}
list:draw()
local glyph = found("draw", function(c) return c[3] == 112 and c[4] == 40 end)
if check(glyph ~= nil, "something prints at the quantity column (14, row+1)") then
eq(glyph[2], TIMES, "and it is the charmap '×' ($F1), not ASCII 'x' ($B7)")
end
check(found("draw", function(c) return c[2] == "6" and c[3] == 128 and c[4] == 40 end)
~= nil, "a one-digit count sits at column 16 (PrintNumber lb bc, 1, 2)")
check(found("draw", function(c) return c[2] == "12" and c[3] == 120 and c[4] == 56 end)
~= nil, "a two-digit count fills columns 15-16")
check(found("draw", function(c) return c[2] == TIMES and c[3] == 112 and c[4] == 56 end)
~= nil, "the second row's '×' is at the same column")
check(found("draw", function(c) return c[2]:find("x", 1, true) ~= nil end) == nil,
"no row prints a lowercase x anywhere")
eq(#rowDraws(72), 0, "a key item (no count) prints nothing on its quantity row")
eq(#rowDraws(88), 0, "nor does CANCEL")
end
do
local list = ListMenu.new({}, "ITEMS", {
{ value = "A", label = "A", right = "ON" },
}, { kind = "bag", itemBox = true })
calls = {}
list:draw()
check(found("draw", function(c) return c[2] == "ON" and c[3] == 120 and c[4] == 40 end)
~= nil, "right text right-aligns to column 17 unchanged")
check(found("draw", function(c) return c[2] == "O" end) == nil,
"and is never byte-split")
end
package.loaded["src.render.Font"] = realFont
package.loaded["src.ui.ListMenu"] = nil
package.loaded["src.ui.Theme"] = nil
require("src.ui.Screens").invalidate()
T.finish()
+2 -2
View File
@@ -142,8 +142,8 @@ do
for _, it in ipairs(list.items) do
if it.value then byId[it.value] = it end
end
eq(byId.POTION.right, "x3", "ordinary stock keeps its quantity")
eq(byId.TOWN_MAP.right, nil, "IsKeyItem_ skips the quantity")
eq(byId.POTION.count, 3, "ordinary stock keeps its quantity")
eq(byId.TOWN_MAP.count, nil, "IsKeyItem_ skips the quantity")
eq(byId.HM_CUT.right, nil, "and so do the HMs")
eq(list.items[#list.items].cancel, true, "CANCEL closes the sell list too")
game.save.inventory = {}
+1 -1
View File
@@ -153,7 +153,7 @@ do
local row = rowFor(list, "RARE_CANDY")
if check(row ~= nil, "the RARE CANDY row survived the use") then
eq(list.items[row].right, "x2", "and its count followed the inventory")
eq(list.items[row].count, 2, "and its count followed the inventory")
eq(list.index, row, "with the cursor left on it (wBagSavedMenuItem), "
.. "so the next candy is one A press away")
end
+22
View File
@@ -190,6 +190,28 @@ do
Chrome.print = priorPrint
end
-- home/text.asm:566
do
local Chrome = require("src.ui.gen2.Chrome")
local priorPrint, priorWrapped = Chrome.print, Chrome.printWrapped
local printed = {}
Chrome.print = function(text) printed[#printed + 1] = text end
Chrome.printWrapped = function(text) printed[#printed + 1] = text end
local done = GenderSelect.new({ data = { text = {
_AreYouABoyOrAreYouAGirlText = "Are you a boy?\nOr are you a girl?{DONE}",
} } })
eq(done.text, "Are you a boy?\nOr are you a girl?",
"the {DONE} terminator is stripped from the prompt")
done:drawPanel()
check(not table.concat(printed, "|"):find("DONE", 1, true),
"and never reaches the tile grid")
local prompt = GenderSelect.new({ data = { text = {
_AreYouABoyOrAreYouAGirlText = "Are you a boy?{PROMPT}",
} } })
eq(prompt.text, "Are you a boy?", "nor is {PROMPT}")
Chrome.print, Chrome.printWrapped = priorPrint, priorWrapped
end
-- ------------------------------------------------- the beat in Oak's speech
local function speechFor(sprites)
+24
View File
@@ -8,6 +8,30 @@ local check, eq = S.check, S.eq
love = require("tests.love_stub")
do
local OakSpeech = require("src.ui.gen2.OakSpeech")
local stub = {
save = { player = { name = "GOLD", rival = "???" } },
data = { tokens = require("src.render.TextBox").TOKENS, audio = {} },
stack = { push = function() end, pop = function() end, top = function() end },
input = { wasPressed = function() return false end },
}
local speech = OakSpeech.new(stub, { data = { text = {
_OakText7 = "{PLAYER}, are you\nready?\fI'll be seeing you\nlater!{DONE}",
} } })
local lines = speech:lastPageLines("_OakText7")
eq(#lines, 2, "the shrink page keeps its two rows")
eq(lines[1], "I'll be seeing you", "row one of the shrink page")
eq(lines[2], "later!", "DoneText leaves no glyph on row two")
check(not table.concat(lines, "|"):find("DONE", 1, true),
"the {DONE} terminator never reaches Font.draw")
local prompt = OakSpeech.new(stub, { data = { text = {
_OakText7 = "See you!{PROMPT}",
} } })
eq(prompt:lastPageLines("_OakText7")[1], "See you!",
"nor does {PROMPT}")
end
local cache = os.getenv("GOLD_CACHE")
if not cache then
local home = os.getenv("HOME") or ""
+2 -2
View File
@@ -421,7 +421,7 @@ do
local bag = stack:top()
eq(bag.items and #bag.items, 1, "the old man's bag lists exactly one item")
eq(bag.items[1].label, "POKé BALL", "the item is a POKé BALL")
eq(bag.items[1].right, "x50", "with quantity x50 (OldManItemList)")
eq(bag.items[1].count, 50, "with quantity x50 (OldManItemList)")
-- the list script (home/list_menu.asm:65-80): input is never read --
-- B can't back out -- and the '▶' hovers POKé BALL for 80 frames
local enemyHP = demo.enemy.mon.hp
@@ -513,7 +513,7 @@ do
demo:update(1 / 60)
end
local bag = stack:top()
eq(bag.items and bag.items[1] and bag.items[1].right, "x1",
eq(bag.items and bag.items[1] and bag.items[1].count, 1,
"Yellow's old-man-style bag lists x1 (SimulatedInputBattleItemList)")
end)
GameVersion.set(oldVersion)
+1 -1
View File
@@ -157,7 +157,7 @@ do
eq(game.stack:top(), list,
"after the stat window the bag is back on top (StartMenu_Item)")
eq(list.index, row, "the cursor is still on the RARE CANDY row")
eq(list.items[row] and list.items[row].right, "x2",
eq(list.items[row] and list.items[row].count, 2,
"the count refreshed in place")
-- the point of the original's behavior: a second candy needs no menu trip
+3 -2
View File
@@ -2445,10 +2445,11 @@ end
if it.value == "HYPER_POTION" then
foundHyper = it
local nameEnd = 16 + Font.width(it.label)
local rightX = 160 - 8 - Font.width(it.right)
local rightX = 112
check(nameEnd <= rightX,
"sell HYPER POTION name does not overlap quantity")
check(not tostring(it.right):find("¥", 1, true),
eq(it.count, 99, "sell list carries the quantity as a count")
check(it.right == nil and it.price == nil,
"sell list keeps prices out of the right column")
check(tostring(it.label):find("x", 1, true) == nil,
"sell list does not glue quantity into the name")