diff --git a/src/battle/BattleState.lua b/src/battle/BattleState.lua index f5a966f4..5a2a9bc2 100644 --- a/src/battle/BattleState.lua +++ b/src/battle/BattleState.lua @@ -3323,7 +3323,9 @@ end local function primaryEffectFailed(msgs) if not msgs or #msgs == 0 then return true end if msgs.failed then return true end - local m = msgs[1] + -- the extracted lines keep the ROM's own trailing blank ("But, it + -- failed! "), so match with it trimmed or a refused status animates + local m = msgs[1]:gsub("%s+$", "") if m == "But, it failed!" or m == "Nothing happened!" then return true end if m:find("didn't affect", 1, true) then return true end if m:find("is unaffected", 1, true) then return true end diff --git a/src/import/LauncherView.lua b/src/import/LauncherView.lua index d49e457d..7c2a6c02 100644 --- a/src/import/LauncherView.lua +++ b/src/import/LauncherView.lua @@ -1845,12 +1845,18 @@ end local function buildConfirmModal(imp, m) local c = imp._modConfirm local overlay = modalOverlay(imp, m, "confirm-out") - local panel = modalPanel(overlay, m, 420 * m.s) - label(panel, c.title or Strings("Confirm"), 15 * m.s + 2, C("white")) + -- roomier than the shared 420 default: the install confirm carries the + -- compat issue list and the trust warning, and those lines need air + local panel = modalPanel(overlay, m, 520 * m.s, { + gap = 12 * m.s, padding = { horizontal = 22, vertical = 20 }, + }) + label(panel, c.title or Strings("Confirm"), 17 * m.s + 2, C("white")) for _, line in ipairs(c.lines or {}) do - label(panel, line, 12 * m.s + 1, C("detail")) + label(panel, line, 13 * m.s + 1, C("detail")) end - local btnRow = mk({ parent = panel, width = "100%", + -- explicit height: an auto-sized row measures short while the panel + -- auto-sizes, which clipped the buttons at the panel's bottom border + local btnRow = mk({ parent = panel, width = "100%", height = m.btnH, positioning = "flex", flexDirection = "horizontal", gap = 10 * m.s }) button(imp, btnRow, "confirm-yes", c.yesLabel or Strings("OK"), { flex = 1, h = m.btnH, size = 13 * m.s + 1, kind = "primary", diff --git a/src/import/RomImporter.lua b/src/import/RomImporter.lua index 74e863b6..5409a371 100644 --- a/src/import/RomImporter.lua +++ b/src/import/RomImporter.lua @@ -1830,6 +1830,18 @@ function RomImporter:update(dt) end local tab = os.getenv("POKEPORT_LAUNCHER_TAB") if tab and tab ~= "" then self:_switchTab(tab) end + -- POKEPORT_LAUNCHER_CONFIRM=1 arms a representative install confirm so + -- a capture can see the modal (it is otherwise only reachable by click) + if os.getenv("POKEPORT_LAUNCHER_CONFIRM") == "1" then + self._modConfirm = { + kind = "update", + title = "Install mod", + yesLabel = "Install", + lines = { "JP GREEN - Poketto Monsuta Midori v0.4.4", + "by bryanthaboi", + "Mods are not reviewed - trust the author." }, + } + end local query = os.getenv("POKEPORT_LAUNCHER_QUERY") if query and query ~= "" then self.findQuery = query diff --git a/src/mods/Schemas.lua b/src/mods/Schemas.lua index 246ead7a..7ccac8a9 100644 --- a/src/mods/Schemas.lua +++ b/src/mods/Schemas.lua @@ -74,7 +74,11 @@ function f.map(key, value) desc = ("map of %s -> %s"):format(key.desc, value.desc) } end -function f.rec(fields) +-- opts.strict closes the record to unknown fields even at the extensible +-- top level. A union alternative whose fields are ALL optional needs this: +-- with top-level leniency it matches every table, so the union stops +-- rejecting anything (the font "ttf" shape was the first such alternative). +function f.rec(fields, opts) local names = {} for name in pairs(fields) do names[#names + 1] = name end table.sort(names) @@ -83,7 +87,7 @@ function f.rec(fields) local ft = fields[name] parts[#parts + 1] = name .. (ft.kind == "opt" and "?" or "") end - return { kind = "rec", fields = fields, + return { kind = "rec", fields = fields, strict = opts and opts.strict or nil, desc = "{" .. table.concat(parts, ", ") .. "}" } end @@ -175,7 +179,7 @@ checkValue = function(t, value, path, patchMode, errors, top) -- unknown keys are preserved unless they read as a typo of a known -- field. Nested recs stay strict, that is where typos hide. local hint = suggest(t.fields, key) - if hint or not top then + if hint or not top or t.strict then errors[#errors + 1] = ("%s.%s: unknown field%s"):format(path, tostring(key), hint and (' (did you mean "' .. hint .. '"?)') or "") end @@ -1077,13 +1081,17 @@ R.font = { advance = f.opt(f.int(1)), charmap = f.opt(f.list(f.rec{ code = f.int(0), seq = f.str })) }, f.rec{ seq = f.str, code = f.int(0) }, - f.rec{ file = f.opt(f.path), size = f.opt(f.int(1)), - spacing = f.opt(f.num), yOffset = f.opt(f.num), - bold = f.opt(f.bool), - -- characters that keep their ROM tile instead of coming from the - -- TTF: a string of them, or a list when a multi-character charmap - -- sequence is meant (src/render/Font.lua) - tiles = f.opt(f.union{ f.str, f.list(f.str) }) }, + -- strict: every field here is optional ({} is a legal "ttf" entry), so + -- with the usual top-level leniency this alternative would match ANY + -- table and let malformed pages through the union unchecked + f.rec({ file = f.opt(f.path), size = f.opt(f.int(1)), + spacing = f.opt(f.num), yOffset = f.opt(f.num), + bold = f.opt(f.bool), + -- characters that keep their ROM tile instead of coming from the + -- TTF: a string of them, or a list when a multi-character charmap + -- sequence is meant (src/render/Font.lua) + tiles = f.opt(f.union{ f.str, f.list(f.str) }) }, + { strict = true }), }, extra = function(id, value) if fontIsCharmap(id) then diff --git a/src/render/Transition.lua b/src/render/Transition.lua index 23784ca0..f970b5f6 100644 --- a/src/render/Transition.lua +++ b/src/render/Transition.lua @@ -59,7 +59,13 @@ local function styleOf(game, id) return record or Transition.STYLES[id] end -function Transition.new(game, onMidpoint, onDone) +-- `warp` marks the map-change fade: PlayMapChangeSound's GBFadeOutToBlack +-- has no matching fade in (LoadGBPal restores the palettes in one write), so +-- warps land with framesIn 0. Script fades that bracket a HideObject +-- (ViridianGym.asm .afterBeat, RocketHideoutB4F BeatGiovanniScript) call +-- GBFadeOutToBlack -> GBFadeInFromBlack instead, so the default keeps the +-- symmetric 32-frame fade back in (home/fade.asm:21, b = 4). +function Transition.new(game, onMidpoint, onDone, warp) local self = setmetatable({}, Transition) self.game = game self.onMidpoint = onMidpoint @@ -68,9 +74,13 @@ function Transition.new(game, onMidpoint, onDone) self.phase = "out" local style = styleOf(game, "warp_fade") self.frames = style.frames or FRAMES - -- a style may still ask for a fade in (mods, and the record is data-driven); - -- the built-in warp is 0, matching hardware - self.framesIn = style.framesIn or FRAMES_IN + if warp then + -- a style may still ask for a fade in (mods, and the record is + -- data-driven); the built-in warp is 0, matching hardware + self.framesIn = style.framesIn or FRAMES_IN + else + self.framesIn = Timing.FADE_IN_FROM_BLACK + end return self end diff --git a/src/world/OverworldController.lua b/src/world/OverworldController.lua index 98c9ca9c..a0772860 100644 --- a/src/world/OverworldController.lua +++ b/src/world/OverworldController.lua @@ -4009,7 +4009,7 @@ function OverworldState:startWarpTo(mapId, x, y, facing, onDone, opts) end, function() self.transitioning = false if onDone then onDone() end - end)) + end, true)) -- warp shape: no fade back in (LoadGBPal restores in one write) end -- Re-read a map record after its data changed (WorldAPI:invalidateMap, diff --git a/tests/engine/timing_parity.lua b/tests/engine/timing_parity.lua index e08f3a71..cddee324 100644 --- a/tests/engine/timing_parity.lua +++ b/tests/engine/timing_parity.lua @@ -235,8 +235,10 @@ local mid, done, tpopped = 0, 0, 0 local g5 = { data = Data, save = SaveData.newGame() } g5.stack = { push = function() end, pop = function() tpopped = tpopped + 1 end, top = function() return nil end } +-- warp = true: the map-change shape with no fade back in. Script fades +-- (ViridianGym.asm .afterBeat) keep the symmetric GBFadeInFromBlack. local fade = Transition.new(g5, function() mid = mid + 1 end, - function() done = done + 1 end) + function() done = done + 1 end, true) local f = 0 while done == 0 and f < 500 do diff --git a/tests/mod_graphics_tests.lua b/tests/mod_graphics_tests.lua index 8100a907..b2b543a2 100644 --- a/tests/mod_graphics_tests.lua +++ b/tests/mod_graphics_tests.lua @@ -612,9 +612,13 @@ check(PaletteFX.usesGbcPack(), "redpp mode selects the gbc pack") local gbc = PaletteFX.gbcPack() check(gbc ~= nil and gbc.palettes.BULBASAUR ~= nil, "data/palettes_gbc.lua ships per-species pals") -check(PaletteFX.monPalName({ palettes = nil }, "BULBASAUR") == "BULBASAUR", +-- the pack's species map follows pokered-gbc's Gen 1 (non-GEN_2_GRAPHICS) +-- palette assignments -- data/pokemon/palettes.asm ELSE branch -- so +-- Bulbasaur wears GREENMON, not a per-species PAL_BULBASAUR authored for +-- Gen 2 sprite art (see the pokemon table comment in data/palettes_gbc.lua) +check(PaletteFX.monPalName({ palettes = nil }, "BULBASAUR") == "GREENMON", "RED++ monPalName resolves to the species palette id") -check(PaletteFX.monPal({ palettes = nil }, "BULBASAUR") == gbc.palettes.BULBASAUR, +check(PaletteFX.monPal({ palettes = nil }, "BULBASAUR") == gbc.palettes.GREENMON, "RED++ monPal reads the species colors without a ROM pack") check(PaletteFX.pal({ palettes = nil }, "ROUTE") == gbc.palettes.ROUTE, "RED++ still has ROUTE (aliased from VIRIDIAN)") @@ -813,9 +817,9 @@ do Renderer:beginWorldPass() Renderer:endWorldPass() wipe:draw() - check(Renderer.battleCascadeProg ~= nil - and Renderer.battleCascadeProg > 0 - and Renderer.battleCascadeProg < 1, + check(Renderer.battleWipe ~= nil + and Renderer.battleWipe.prog > 0 + and Renderer.battleWipe.prog < 1, "battle wipe publishes mid-progress cascade to the renderer") rects = {} Renderer:endFrame(nil, fullWorldZones()) @@ -911,7 +915,8 @@ local vanilla = BattleTransition.new({ stack = stack }, nil, { trainer = true, stronger = true }) check(vanilla.style == "spiralout", "the vanilla 3-bit select is the hook's default (trainer+stronger)") -check(vanilla.wipeLen == 40, "the selected wipe brings its own length") +check(vanilla.wipeLen == BattleTransition.STYLES.spiralout.frames, + "the selected wipe brings its own length") local savedRuntime = { events = Runtime.events, hooks = Runtime.hooks, errors = Runtime.errors } @@ -924,7 +929,8 @@ hooks:wrap("transition.style", function(nextLink, ctx) end, 0, "test") local hooked = BattleTransition.new({ stack = stack }, nil, { trainer = true }) check(hooked.style == "hstripes", "a transition.style hook picks the wipe") -check(hooked.wipeLen == 24, "the hooked style brings its own length") +check(hooked.wipeLen == BattleTransition.STYLES.hstripes.frames, + "the hooked style brings its own length") check(seenCtx.trainer == true and seenCtx.stronger == nil, "the hook receives the selection bits as context") diff --git a/tests/mod_qol_hooks_tests.lua b/tests/mod_qol_hooks_tests.lua index 2d4157ef..25a8b9ff 100644 --- a/tests/mod_qol_hooks_tests.lua +++ b/tests/mod_qol_hooks_tests.lua @@ -124,6 +124,12 @@ end do Zoom.reset() + -- Zoom.reset() clears only the offset: allowSurvey is the performance + -- tier's clamp (Game:applyOptions), and an earlier suite in the same + -- process may have applied a LOW tier. Pin the vanilla precondition and + -- restore whatever the run had afterwards. + local savedSurvey = Zoom.allowSurvey + Zoom.allowSurvey = true local lo, hi = Zoom.offsetRange(4) check(lo == -3 and hi == 4, "vanilla zoom.range is (1-S, S)") local unsub = wrap("zoom.range", function(next, a, b, S) @@ -139,6 +145,7 @@ do unsub() Zoom.reset() check(Zoom.scale(4) == 4, "unwrapped zoom returns to FIT") + Zoom.allowSurvey = savedSurvey end -- ------- battle.overlay (shiny sparkles / HUD chrome) diff --git a/tests/mod_ui_tests.lua b/tests/mod_ui_tests.lua index 5f0c6578..71f99cda 100644 --- a/tests/mod_ui_tests.lua +++ b/tests/mod_ui_tests.lua @@ -282,9 +282,11 @@ local function optGame() end local om = OptionsMenu.new(optGame()) local WANT_IDS = { "textSpeed", "animations", "battleStyle", "battleLayout", + "battleFit", "battleBg", "uiLayout", "ruleset", "musicVol", "sfxVol", "musicFilter", "performance", "colors", - "tilt", "gbcfx", "zoom", "voidFill", "videoMode", "fpsCap", + "tilt", "gbcfx", "zoom", "voidFill", "videoMode", + "faithfulRes", "fpsCap", "speed", "mods", "controls" } check(#om.rows == #WANT_IDS, "vanilla options row count (plus MODS/CONTROLS)") for i, id in ipairs(WANT_IDS) do @@ -293,11 +295,11 @@ end -- ruleset row cycles the sorted non-hidden registry ids showing name om.game.save.options.ruleset = "gen1_faithful" -check(om.rows[5].value(om.game) == "GEN 1", "ruleset row shows record.name") -om.rows[5].step(om.game, 1) +check(om.rows[8].value(om.game) == "GEN 1", "ruleset row shows record.name") +om.rows[8].step(om.game, 1) check(om.game.save.options.ruleset == "modern_clean", "ruleset row cycles sorted registry ids") -om.rows[5].step(om.game, 1) +om.rows[8].step(om.game, 1) check(om.game.save.options.ruleset == "gen1_faithful", "hidden rulesets are excluded from the cycle") @@ -316,41 +318,42 @@ check(om.game.save.options.battleLayout == "wide", "battle layout flips to WIDE" check(om.rows[4].value(om.game) == "WIDE", "the WIDE layout renders its label") om.rows[4].step(om.game, 1) check(om.game.save.options.battleLayout == "og", "battle layout flips back") -om.rows[6].step(om.game, -1) +om.rows[9].step(om.game, -1) check(om.game.save.options.musicVol == 6, "music volume steps down") -for _ = 1, 10 do om.rows[6].step(om.game, -1) end +for _ = 1, 10 do om.rows[9].step(om.game, -1) end check(om.game.save.options.musicVol == 0, "music volume clamps at 0") --- ZOOM / VOID FILL rows (indices shifted +1 by the PERFORMANCE row spliced --- in ahead of COLORS) +-- ZOOM / VOID FILL rows (indices track WANT_IDS above; the battle +-- composition rows -- BATTLE SIZE / BATTLE BG / UI LAYOUT -- sit ahead of +-- RULESET, and FAITHFUL RATIO lands between VIDEO MODE and MAX FPS) local Zoom = require("src.render.Zoom") local TileRenderer = require("src.render.TileRenderer") om.game.save.options.zoom = 0 Zoom.offset = 0 -check(om.rows[13].value(om.game) == "FIT", "ZOOM row shows FIT at offset 0") -om.rows[13].step(om.game, 1) +check(om.rows[16].value(om.game) == "FIT", "ZOOM row shows FIT at offset 0") +om.rows[16].step(om.game, 1) check(om.game.save.options.zoom == 1 and Zoom.offset == 1, "ZOOM row steps to IN1") -om.rows[14].step(om.game, 1) +om.rows[17].step(om.game, 1) check(om.game.save.options.voidFill == "water" and TileRenderer.voidFill == "water", "VOID FILL row cycles TREES → WATER") -om.rows[14].step(om.game, 1) +om.rows[17].step(om.game, 1) check(om.game.save.options.voidFill == "black", "VOID FILL steps to BLACK") -om.rows[14].step(om.game, 1) +om.rows[17].step(om.game, 1) check(om.game.save.options.voidFill == "trees", "VOID FILL wraps to TREES") -- the MAX FPS row cycles the render-cap steps and shows the value plain om.game.save.options.fpsCap = nil -check(om.rows[16].value(om.game) == "60", +check(om.rows[20].value(om.game) == "60", "MAX FPS row defaults to 60 with no saved cap") -om.rows[16].step(om.game, 1) +om.rows[20].step(om.game, 1) check(om.game.save.options.fpsCap == 75, "MAX FPS steps up from 60 to 75") -check(om.rows[16].value(om.game) == "75", "the MAX FPS row renders the cap") +check(om.rows[20].value(om.game) == "75", "the MAX FPS row renders the cap") om.game.save.options.fpsCap = 160 -om.rows[16].step(om.game, 1) +om.rows[20].step(om.game, 1) check(om.game.save.options.fpsCap == 30, "MAX FPS wraps past the ceiling to 30") -om.rows[16].step(om.game, -1) +om.rows[20].step(om.game, -1) check(om.game.save.options.fpsCap == 160, "MAX FPS wraps back down to the ceiling") -- ------- FrameCap normalize / cycle (issue #88) @@ -380,7 +383,7 @@ check(FrameCap.current == 60, "FrameCap.applyOptions defaults a missing key to 6 -- the MODS row is the manager's discoverable home local mgGame = optGame() om = OptionsMenu.new(mgGame) -om.rows[18].activate(mgGame) +om.rows[22].activate(mgGame) check(getmetatable(mgGame.stack:top()) == ManagerState, "the MODS row opens the manager") check(mgGame.stack:top().screenId == "ManagerState", @@ -390,7 +393,7 @@ check(mgGame.stack:top().screenId == "ManagerState", local BindingsMenu = require("src.ui.BindingsMenu") local cbGame = optGame() om = OptionsMenu.new(cbGame) -om.rows[19].activate(cbGame) +om.rows[23].activate(cbGame) local bm = cbGame.stack:top() check(getmetatable(bm) == BindingsMenu, "the CONTROLS row opens the rebind list") @@ -435,9 +438,12 @@ local Input = require("src.core.Input") local gpGame = { stack = newStack() } local sawPad gpGame.stack:push({ onGamepadPressed = function(_, b) sawPad = b end }) +-- gamepadpressed reads Input:isDown("select") for the display-chord and +-- shoulder-hotkey gates before it routes to the capturing state, so the +-- button state table must exist first +Input:init() Game.gamepadpressed(gpGame, nil, "y") check(sawPad == "y", "pad buttons reach a capturing top state") -Input:init() gpGame.stack:pop() Game.gamepadpressed(gpGame, nil, "a") Input:step() diff --git a/tests/parity_J.lua b/tests/parity_J.lua index 4e807a9a..5316ca07 100644 --- a/tests/parity_J.lua +++ b/tests/parity_J.lua @@ -428,10 +428,15 @@ do bag:update(1 / 60) end check(stack:top() ~= bag, "the ball is thrown without input") + -- Battle exit now rides Transition.battleReturn (MapEntryAfterBattle's + -- GBFadeInFromWhite, home/overworld.asm:749-753): the battle pops itself + -- and pushes the fade, which fires onFinish only once ITS update counts + -- down -- so pump whatever sits on top of the stack, not the demo state. for _ = 1, 2000 do if finished then break end pressed.a = true - demo:update(1 / 60) + local top = stack:top() + if top then top:update(1 / 60) else break end end pressed.a = false check(finished, "the throw ends the demo battle") diff --git a/tests/parity_L.lua b/tests/parity_L.lua index 817886d8..11c8ae37 100644 --- a/tests/parity_L.lua +++ b/tests/parity_L.lua @@ -59,7 +59,11 @@ local t = battler{ disabledSlot = 1, disabledTurns = 2, xAccuracy = true, mon = { status = "SLP" }, name = "TARGET", } -local msg = MoveEffects.primary.HAZE_EFFECT(nil, u, t) +-- Effect rows take the battle handle first so romText can serve the ROM's +-- own wording; a data-only stub is all these pure rows dereference. +local B = { data = Data } + +local msg = MoveEffects.primary.HAZE_EFFECT(B, u, t) check(next(u.stages) == nil, "Haze clears all of the user's stat stages") check(next(t.stages) == nil, "Haze clears all of the target's stat stages") @@ -89,13 +93,13 @@ eq(msg[1], "All STATUS changes\nare eliminated!", "Haze prints the elimination t -- FRZ target also forfeits its move. local frz = battler{ mon = { status = "FRZ" }, name = "FROZEN" } -MoveEffects.primary.HAZE_EFFECT(nil, battler{ mon = {} }, frz) +MoveEffects.primary.HAZE_EFFECT(B, battler{ mon = {} }, frz) eq(frz.mon.status, nil, "target's freeze is cured") check(frz.skipMove == true, "curing target's freeze forfeits its move") -- Badly-poisoned TARGET: status cured, no forfeit, toxic counter gone. local psnT = battler{ mon = { status = "PSN" }, toxicCounter = 4, name = "PSN_T" } -MoveEffects.primary.HAZE_EFFECT(nil, battler{ mon = {} }, psnT) +MoveEffects.primary.HAZE_EFFECT(B, battler{ mon = {} }, psnT) eq(psnT.mon.status, nil, "badly-poisoned target is fully cured of poison") check(psnT.toxicCounter == nil, "badly-poisoned target's toxic counter cleared") check(not psnT.skipMove, "curing poison does NOT forfeit the target's move") @@ -103,14 +107,14 @@ check(not psnT.skipMove, "curing poison does NOT forfeit the target's move") -- BRN / PAR targets: cured, no forfeit. for _, st in ipairs({ "BRN", "PAR" }) do local tb = battler{ mon = { status = st }, name = st } - MoveEffects.primary.HAZE_EFFECT(nil, battler{ mon = {} }, tb) + MoveEffects.primary.HAZE_EFFECT(B, battler{ mon = {} }, tb) eq(tb.mon.status, nil, "target's " .. st .. " is cured") check(not tb.skipMove, st .. " target keeps its move (no sleep/freeze forfeit)") end -- A burned USER keeps its own burn (status not the one Haze cures). local burnedUser = battler{ mon = { status = "BRN" }, name = "BURNER" } -MoveEffects.primary.HAZE_EFFECT(nil, burnedUser, battler{ mon = {} }) +MoveEffects.primary.HAZE_EFFECT(B, burnedUser, battler{ mon = {} }) eq(burnedUser.mon.status, "BRN", "user's own burn is not cured by Haze") -- ===================================================================== @@ -145,7 +149,7 @@ eq(dBurnedHaze, dHealthy, "Haze lifts the burn Attack-halving (damage == unburne -- A stat-stage change re-bakes the penalty (effects.asm:505-506). Bump the -- attacker's DEFENSE (irrelevant to its own offense) so only hazeStatReset flips. -MoveEffects.primary.DEFENSE_UP1_EFFECT(nil, hazedAtk, nil) +MoveEffects.primary.DEFENSE_UP1_EFFECT(B, hazedAtk, nil) check(hazedAtk.hazeStatReset == nil, "a stat-stage change re-arms the burn penalty") local dAfter = Damage.compute(ruleset, hazedAtk, defender, move, opts) eq(dAfter, dBurnedRaw, "burn Attack-halving returns after the stage change") @@ -159,10 +163,10 @@ local para = battler{ mon = { status = "PAR", level = 50, stats = { hp = 100 } }, name = "PARA", } eq(TurnOrder.effectiveSpeed(para), 25, "paralysis quarters speed before Haze (100 -> 25)") -MoveEffects.primary.HAZE_EFFECT(nil, para, battler{ mon = {} }) +MoveEffects.primary.HAZE_EFFECT(B, para, battler{ mon = {} }) eq(TurnOrder.effectiveSpeed(para), 100, "Haze lifts paralysis Speed-quartering") -- Re-arm via an ATTACK stage change (irrelevant to the speed calc). -MoveEffects.primary.ATTACK_UP1_EFFECT(nil, para, nil) +MoveEffects.primary.ATTACK_UP1_EFFECT(B, para, nil) check(para.hazeStatReset == nil, "stage change re-arms the paralysis penalty") eq(TurnOrder.effectiveSpeed(para), 25, "Speed-quartering resumes after the stage change") diff --git a/tests/parity_android_permissions.lua b/tests/parity_android_permissions.lua index ebe72ee6..8db14fde 100644 --- a/tests/parity_android_permissions.lua +++ b/tests/parity_android_permissions.lua @@ -39,7 +39,16 @@ check(type(script) == "string", SCRIPT .. " is readable") -- The trim is only dangerous because it edits the tracked manifest in place; if -- that stops being true, everything below tests a file the build never touches. if script then - local target = script:match('local manifest="([^"]+)"') + -- The script has grown other `local manifest=` locals (the Yellow ROM + -- import manifest recovery), so scan every assignment for the one that + -- names the Android manifest instead of trusting the first match. + local target + for candidate in script:gmatch('local manifest="([^"]+)"') do + if candidate:find("AndroidManifest.xml", 1, true) then + target = candidate + break + end + end check(target ~= nil, "build_android.sh names the manifest it rewrites") check(target ~= nil and target:find("app/src/main/AndroidManifest.xml", 1, true) ~= nil, diff --git a/tests/parity_applying_attack_anim.lua b/tests/parity_applying_attack_anim.lua index 6b167580..fc11dd56 100644 --- a/tests/parity_applying_attack_anim.lua +++ b/tests/parity_applying_attack_anim.lua @@ -190,7 +190,10 @@ do eq(tb.fx.shakeProg, nil, "type 4 arms no shake (#354 must not regress it)") check(tb.fx.blink ~= nil and tb.fx.blink.target == tb.enemy, "type 4 blinks the enemy pic") - eq(tb.waitFrames, 20, "for the 20 frames AnimationBlinkEnemyMon takes") + -- AnimationBlinkMon (animations.asm:1360-1376) is `ld c, 6` iterations + -- of hide + DelayFrames 5 + show + DelayFrames 5 = 60 frames; the port + -- once ran it in 20, a third of its length (Timing.BLINK_MON). + eq(tb.waitFrames, 60, "for the 60 frames AnimationBlinkEnemyMon takes") end -- the OPTIONS animation toggle still gates the whole thing; the sound does not diff --git a/tests/parity_battle_item_turn.lua b/tests/parity_battle_item_turn.lua index 508d88d1..cf54bedf 100644 --- a/tests/parity_battle_item_turn.lua +++ b/tests/parity_battle_item_turn.lua @@ -172,7 +172,11 @@ do local box = game.stack:top() check(isBox(box), "the restored-HP message opened (#379)") if isBox(box) then - check(box.text:find("was restored", 1, true) ~= nil, + -- the line is _PotionText itself when the cache carries it (" + -- recovered by !") and the engine's "was restored" wording on a + -- dataset without the label (src/core/RomText.lua) + check((box.text:find("recovered by", 1, true) + or box.text:find("was restored", 1, true)) ~= nil, "and it is the restored-HP line: " .. tostring(box.text)) dismiss(game.stack, box) end diff --git a/tests/parity_trade_gift.lua b/tests/parity_trade_gift.lua index ab0383c4..6ed91552 100644 --- a/tests/parity_trade_gift.lua +++ b/tests/parity_trade_gift.lua @@ -98,9 +98,13 @@ for _, modname in ipairs({ "data.scripts.story", "data.scripts.story2", check(WIRED[idx] == flag, ("%s/%s: trade %s pairs with %s"):format( mapId, const, tostring(idx), tostring(flag))) - check(not seen[idx], - ("trade index %s wired by only one NPC"):format(tostring(idx))) - seen[idx] = true + -- one NPC per index PER VERSION: Route 18 gate wires slot 6 + -- twice on purpose (Red's YOUNGSTER/MARC, Yellow's COOK/SPIKE; + -- pokeyellow/scripts/Route18Gate2F.asm, #651) -- each version's + -- map only spawns its own NPC, so a same-map twin is fine + check(not seen[idx] or seen[idx] == mapId, + ("trade index %s wired by only one NPC per version"):format(tostring(idx))) + seen[idx] = mapId end end end @@ -116,9 +120,12 @@ check(not seen[3], "unused CHIKUCHIKU trade (index 3) stays unwired") -- === harness: run a talk script headless, recording show_text ids === local shown = {} local origShow = Commands.show_text -Commands.show_text = function(ctx, textId, subs) +-- forward extraOpts too: Commands.ask rides show_text's 4th argument +-- (opts.choice, so the YES/NO box pops over the still-visible question); +-- dropping it here would strand every prompt on the NO branch +Commands.show_text = function(ctx, textId, subs, ...) table.insert(shown, textId) - return origShow(ctx, textId, subs) + return origShow(ctx, textId, subs, ...) end -- pressFn returns the Input.pressed table for this frame (default: A) diff --git a/tests/parity_trainer_evolution_order.lua b/tests/parity_trainer_evolution_order.lua index f4f39b8e..d457f4bc 100644 --- a/tests/parity_trainer_evolution_order.lua +++ b/tests/parity_trainer_evolution_order.lua @@ -77,8 +77,13 @@ check(#Game.stack.states == 1, Game.stack:pop() moreText.onDone() -local evolutionText = Game.stack:top() -check(stateHas(evolutionText, "evolving"), +-- The evolution runs as the EvolutionState cutscene screen now (the +-- evolve_mon.asm sequence lives in src/ui/EvolutionState.lua), not a bare +-- "evolving!" text box, so assert the screen itself took the stack. +local evolution = Game.stack:top() +check(evolution ~= nil + and (evolution.screenId == "EvolutionState" + or stateHas(evolution, "evolving")), "the level evolution starts after trainer after-text closes") S.finish() diff --git a/tests/run_tests.lua b/tests/run_tests.lua index c50f93f3..8dbd9d63 100644 --- a/tests/run_tests.lua +++ b/tests/run_tests.lua @@ -114,6 +114,12 @@ do "cont boundary waits for A with contAdvance") check(not box.done, "cont wait is not the final done prompt") eq(box.lineIndex, 2, "cont wait stays on the finished line until A") + -- ProtectedDelay3 (home/text.asm:265): the ▼ swallows the button for + -- three frames before ManualTextScroll starts listening + for _ = 1, 80 do + if (box.preWait or 0) == 0 then break end + box:update(0) + end pressed.a = true box:update(0) check(not box.waiting and not box.contAdvance, "A clears cont wait") @@ -1126,14 +1132,17 @@ do eq(pb:sendOutText("PIKA"), "The enemy's weak!\nGet'm! PIKA!", "send-out below 10%") - -- HP-bar drain converges at UpdateHPBar's pixel pace (maxHP/96/frame) + -- HP-bar drain converges at UpdateHPBar's per-side pace (2 frames per + -- bar pixel, enemy HP steps free; hp_bar.asm:81-148 via Timing) local db = BattleState.newWild(Game, "RATTATA", 5) local maxHP = db.enemy.mon.stats.hp + local startHP = db.enemy.mon.hp db.enemy.mon.hp = math.max(0, db.enemy.mon.hp - 5) local frames = 0 while db:stepHPDrain() and frames < 2000 do frames = frames + 1 end eq(db.enemy.shownHP, db.enemy.mon.hp, "drain settles on the true HP") - local expect = math.ceil(5 / (maxHP / 96)) + local expect = require("src.core.Timing").hpDrainFrames( + startHP, db.enemy.mon.hp, maxHP, false) check(math.abs(frames - expect) <= 1, ("drain speed ~2 frames per bar pixel (%d ~ %d)"):format(frames, expect))