From 1f878c3098ba910cb8b154578e40859f765ee135 Mon Sep 17 00:00:00 2001 From: johnjohto Date: Wed, 5 Aug 2026 10:18:47 -0400 Subject: [PATCH 1/3] =?UTF-8?q?Show=20the=20Pok=C3=A9dex=20entry=20for=20t?= =?UTF-8?q?he=20Fighting=20Dojo=20prize=20balls?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/scripts/story4.lua | 33 +++++--- src/ui/DexEntryMenu.lua | 11 ++- tests/drivers/fighting_dojo_bug197_test.lua | 10 ++- tests/parity_fighting_dojo_dex.lua | 89 +++++++++++++++++++++ 4 files changed, 124 insertions(+), 19 deletions(-) create mode 100644 tests/parity_fighting_dojo_dex.lua diff --git a/data/scripts/story4.lua b/data/scripts/story4.lua index 874a2b9c..80dede5c 100644 --- a/data/scripts/story4.lua +++ b/data/scripts/story4.lua @@ -155,19 +155,26 @@ local function dojoBall(species, ownBall, otherBall, askKey) push(game, "You'll have to\nbeat the master\nfirst!", done) return end - ask(game, t[askKey] or ("You want\n" .. species .. "?"), function(yes) - if not yes then done() return end - flags["EVENT_GOT_" .. species] = true - flags.EVENT_DEFEATED_FIGHTING_DOJO = true - local Commands = require("src.script.Commands") - local ctx = { save = game.save, game = game, overworld = ow } - Commands.give_pokemon(ctx, species, 30) - -- Hide ONLY the chosen ball; the other stays (FightingDojo.asm hides - -- just the picked object's index) and routes to the greedy line above - -- when talked to (#197). - Commands.hide_object(ctx, "FIGHTING_DOJO", ownBall) - push(game, ("%s got\n%s!"):format(game.save.player.name, species), done) - end) + -- Examining a ball shows that species' POKéDEX entry first + -- (DisplayPokedex in FightingDojo.asm, which also marks it seen), + -- then the yes/no take-it prompt (#853). + local Commands = require("src.script.Commands") + local ctx = { save = game.save, game = game, overworld = ow } + Commands.mark_seen(ctx, species) + local DexEntryMenu = require("src.ui.DexEntryMenu") + game.stack:push(DexEntryMenu.new(game, species, function() + ask(game, t[askKey] or ("You want\n" .. species .. "?"), function(yes) + if not yes then done() return end + flags["EVENT_GOT_" .. species] = true + flags.EVENT_DEFEATED_FIGHTING_DOJO = true + Commands.give_pokemon(ctx, species, 30) + -- Hide ONLY the chosen ball; the other stays (FightingDojo.asm hides + -- just the picked object's index) and routes to the greedy line above + -- when talked to (#197). + Commands.hide_object(ctx, "FIGHTING_DOJO", ownBall) + push(game, ("%s got\n%s!"):format(game.save.player.name, species), done) + end) + end)) end end diff --git a/src/ui/DexEntryMenu.lua b/src/ui/DexEntryMenu.lua index 11e0d808..b59e6ee1 100644 --- a/src/ui/DexEntryMenu.lua +++ b/src/ui/DexEntryMenu.lua @@ -6,6 +6,11 @@ -- StarterDex (engine/events/starter_dex.asm), which temporarily sets the -- owned bit so Oak's lab ball previews show height/weight/description -- without permanently marking the mon owned. +-- +-- `onDone` (optional) runs right after the page pops itself, the way a +-- TextBox onDone does; map scripts use it to continue once the player +-- closes the entry (the Fighting Dojo prize balls chain their take-it +-- prompt off it). local Font = require("src.render.Font") local Strings = require("src.core.Strings") @@ -31,9 +36,10 @@ local function resolveArgs(speciesOrOpts) return speciesOrOpts, false end -function DexEntryMenu.new(game, speciesOrOpts) +function DexEntryMenu.new(game, speciesOrOpts, onDone) local species, forceOwned = resolveArgs(speciesOrOpts) - local self = setmetatable({ game = game, forceOwned = forceOwned }, DexEntryMenu) + local self = setmetatable({ game = game, forceOwned = forceOwned, + onDone = onDone }, DexEntryMenu) self.def = game.data.pokemon[species] local path, trueColor = require("src.pokemon.Sprites").path( game.data, species, "front", { kind = "dex" }) @@ -52,6 +58,7 @@ function DexEntryMenu:update(dt) local input = self.game.input if input:wasPressed("a") or input:wasPressed("b") then self.game.stack:pop() + if self.onDone then self.onDone() end end end diff --git a/tests/drivers/fighting_dojo_bug197_test.lua b/tests/drivers/fighting_dojo_bug197_test.lua index ce529e1d..a234b4f9 100644 --- a/tests/drivers/fighting_dojo_bug197_test.lua +++ b/tests/drivers/fighting_dojo_bug197_test.lua @@ -3,7 +3,8 @@ -- BUG1 gate -- the master stops the player on the tile to his left -- BUG2 no speech -- no won text + no prize dialogue after the win -- BUG3 wrong re-talk -- shows the pre-battle challenge, not the after line --- BUG4 (verify) -- the ball ask() is the Gen1 descriptor, not a dex entry +-- BUG4 (verify) -- the ball ask() is the Gen1 descriptor, shown after +-- the species' dex entry (DisplayPokedex) -- BUG5 both balls -- the chosen ball AND the other one both vanish; the -- other should stay and give the "greedy" refusal -- BUG6 poster -- the north-wall posters ("Enemies on every side!") are @@ -163,8 +164,9 @@ return function(game) mashUntil(function() return game.stack:top() == ow end) ------------------------------------------------------------------ - -- BUG4 (verify-only): the Hitmonlee ball prompt is the Gen1 descriptor - -- ("You want the hard kicking HITMONLEE?"), not a Pokedex entry screen. + -- BUG4 (verify-only): the Hitmonlee ball shows the species' Pokedex + -- entry first (DisplayPokedex, #853), then the Gen1 descriptor prompt + -- ("You want the hard kicking HITMONLEE?"). ------------------------------------------------------------------ ow = resetDojo(4, 2, "up", { EVENT_BEAT_KARATE_MASTER = true }) local leeBall = npcByName(ow, "FIGHTINGDOJO_HITMONLEE_POKE_BALL") @@ -173,7 +175,7 @@ return function(game) if leeBall then ow:talkTo(leeBall) check(sawText("hard kicking") or sawText("HITMONLEE"), - "BUG4: ball asks the Gen1 descriptor prompt (no dex entry)") + "BUG4: ball asks the Gen1 descriptor prompt after the dex entry") U.shot(game, DIR .. "/dojo_4_prompt.png") ------------------------------------------------------------------ -- BUG5: choose YES -> only the chosen ball vanishes; the other stays diff --git a/tests/parity_fighting_dojo_dex.lua b/tests/parity_fighting_dojo_dex.lua new file mode 100644 index 00000000..448ac2db --- /dev/null +++ b/tests/parity_fighting_dojo_dex.lua @@ -0,0 +1,89 @@ +-- Parity: the Fighting Dojo prize balls open the Pokédex entry before the +-- take-it prompt (#853). FightingDojo.asm runs DisplayPokedex on the +-- ball's species (marking it seen) and only then prints the yes/no ask. +package.path = "./?.lua;./?/init.lua;" .. package.path +if not _G.love then _G.love = require("tests.love_stub") end +local Data = require("src.core.Data") +if not Data.maps then Data:load() end + +local S = require("tests.harness").suite("parity Fighting Dojo dex entry") +local check, eq = S.check, S.eq + +local Font = require("src.render.Font") +Font.load(Data) + +local TextBox = require("src.render.TextBox") +local DexEntryMenu = require("src.ui.DexEntryMenu") +local SaveData = require("src.core.SaveData") +local dojo = require("data.scripts.story4").FIGHTING_DOJO + +local function fakeGame() + local states = {} + local save = SaveData.newGame() + save.pokedex = { seen = {}, owned = {} } + save.flags = { EVENT_BEAT_KARATE_MASTER = true } + local game = { + data = Data, + save = save, + pressed = false, + stack = { + states = states, + push = function(_, s) states[#states + 1] = s end, + pop = function(_) states[#states] = nil end, + top = function(_) return states[#states] end, + }, + } + game.input = { wasPressed = function(_, btn) + local p = game.pressed + game.pressed = false + return p and btn == "a" + end } + return game +end + +local function pageText(box) + local out = {} + for _, page in ipairs(box.pages or {}) do + out[#out + 1] = table.concat(page, "\n") + end + return table.concat(out, "\n") +end + +-- each ball: dex entry first (seen, not owned), then the ask prompt +for _, c in ipairs({ + { textId = "TEXT_FIGHTINGDOJO_HITMONLEE_POKE_BALL", species = "HITMONLEE" }, + { textId = "TEXT_FIGHTINGDOJO_HITMONCHAN_POKE_BALL", species = "HITMONCHAN" }, +}) do + local game = fakeGame() + dojo.talk[c.textId](game, {}, nil, function() end) + local top = game.stack:top() + check(getmetatable(top) == DexEntryMenu, + c.textId .. " opens the Pokédex entry first") + eq(top and top.def and top.def.id, c.species, + "the entry shows " .. c.species) + check(game.save.pokedex.seen[c.species] == true, + "the preview marks " .. c.species .. " seen") + check(not game.save.pokedex.owned[c.species], + "the preview does not mark " .. c.species .. " owned") + game.pressed = true + top:update(0) + local ask = game.stack:top() + check(getmetatable(ask) == TextBox, + "closing the entry shows the take-it prompt") + check(ask and pageText(ask):find(c.species, 1, true) ~= nil, + "the prompt names " .. c.species) +end + +-- before the Karate Master is beaten the ball still refuses, no dex entry +do + local game = fakeGame() + game.save.flags.EVENT_BEAT_KARATE_MASTER = nil + dojo.talk.TEXT_FIGHTINGDOJO_HITMONLEE_POKE_BALL(game, {}, nil, + function() end) + check(getmetatable(game.stack:top()) == TextBox, + "an unbeaten master keeps the refusal text, not the dex entry") + check(not game.save.pokedex.seen.HITMONLEE, + "the refusal does not mark Hitmonlee seen") +end + +S.finish() From 8f0117a145392d14caca5f4143c62929d0772de0 Mon Sep 17 00:00:00 2001 From: johnjohto Date: Wed, 5 Aug 2026 10:25:19 -0400 Subject: [PATCH 2/3] Treat headless cache reads as misses in CacheFs The modkit validate and pack drivers run the real loader under plain luajit, with no love global. With --base imported, Data:load falls back to CacheFs.readActive for a generated module require cannot find (an optional module like data/generated/audio.lua is legitimately absent from developer and stale caches), and CacheFs.read indexed love.filesystem once there was no portable root, so validate and pack died with MK100 before the mod was even looked at. Headless there is no save directory to read from, so return nil like any other cache miss. Refs #850 --- src/import/CacheFs.lua | 3 +++ tests/engine/cache_fs_headless_test.lua | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 tests/engine/cache_fs_headless_test.lua diff --git a/src/import/CacheFs.lua b/src/import/CacheFs.lua index 56974cdb..ad1b9018 100644 --- a/src/import/CacheFs.lua +++ b/src/import/CacheFs.lua @@ -294,6 +294,9 @@ function CacheFs.read(rel) f:close() return data end + -- headless (plain luajit, e.g. the modkit validate/pack driver): there is + -- no save directory to read from, so a cache miss is nil, not a crash + if not (love and love.filesystem) then return nil end return love.filesystem.read(rel) end diff --git a/tests/engine/cache_fs_headless_test.lua b/tests/engine/cache_fs_headless_test.lua new file mode 100644 index 00000000..286599c4 --- /dev/null +++ b/tests/engine/cache_fs_headless_test.lua @@ -0,0 +1,20 @@ +-- CacheFs stays headless-safe: plain luajit has no love global, and the +-- modkit validate/pack driver reaches CacheFs.read through Data:load when +-- an optional generated module (audio) is missing from the checkout +-- (issue #850). With no portable root and no love there is no save +-- directory to read from, so the read is a nil miss, not a crash. +package.path = "./?.lua;./?/init.lua;" .. package.path + +local T = require("tests.harness") +local check = T.check + +check(_G.love == nil, "suite runs with no love global") + +local CacheFs = require("src.import.CacheFs") + +check(CacheFs.read("data/generated/audio.lua") == nil, + "read is a nil miss headless, not a crash") +check(CacheFs.readActive("data/generated/audio.lua") == nil, + "readActive is a nil miss headless, not a crash") + +T.finish() From 88e34ef65c1e1a326497dec8ea6bdc59b65691e7 Mon Sep 17 00:00:00 2001 From: Dorian Burton <120594826+dburton95@users.noreply.github.com> Date: Wed, 5 Aug 2026 10:55:58 -0400 Subject: [PATCH 3/3] Added Love stub table to validate codeblock This fixes the nil value returned when running validate or pack on Fedora 43. Since Love isn't running, luajit calls on an empty table. Providing a stub table resolves the nil error. --- tools/modkit.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/modkit.py b/tools/modkit.py index 56e439fa..522b8008 100644 --- a/tools/modkit.py +++ b/tools/modkit.py @@ -528,6 +528,7 @@ def cmd_scaffold(args, repo): DRIVER_TEMPLATE = """-- generated by tools/modkit.py; drives the real loader headlessly package.path = "./?.lua;./?/init.lua;" .. package.path +love = require("tests.love_stub") local data = %s local FILES = %s local overlay = {}