bingus dingus

This commit is contained in:
bryanthaboi
2026-08-04 10:32:51 -04:00
parent 626080d228
commit 8fbe819493
18 changed files with 175 additions and 70 deletions
+3 -1
View File
@@ -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
+10 -4
View File
@@ -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",
+12
View File
@@ -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
+18 -10
View File
@@ -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
+14 -4
View File
@@ -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
+1 -1
View File
@@ -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,
+3 -1
View File
@@ -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
+13 -7
View File
@@ -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")
+7
View File
@@ -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)
+27 -21
View File
@@ -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()
+6 -1
View File
@@ -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")
+12 -8
View File
@@ -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")
+10 -1
View File
@@ -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,
+4 -1
View File
@@ -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
+5 -1
View File
@@ -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 ("<mon>
-- recovered by <n>!") 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
+12 -5
View File
@@ -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)
+7 -2
View File
@@ -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()
+11 -2
View File
@@ -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))