mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 08:21:02 +02:00
bingus dingus
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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")
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user