Read the perishable timer instead of assuming it, and fix a pcall idiom
test / test (push) Successful in 14s
test / test (push) Successful in 14s
Two fixes from a review pass, plus the first tests to reach the post-run
recap at all.
The perishable caution said "gains die in 5 rounds" as a literal, but the
count is a game variable (G.GAME.perishable_rounds, game.lua:1961) and
vanilla's own sticker tooltip reads it (common_events.lua:3339). A challenge
or another mod moving it made the advisor state a wrong number, in the one
place this mod promises mechanical facts. It now reads perish_tally when the
sticker is already ticking, falls back to the game value, then to 5, and
says "1 round" rather than "1 rounds". The new tests fail against the old
literal, so this one was real.
The win screen's recap fallback tested only the second pcall return:
local _, inserted = pcall(insert_recap, ui, 'from_game_won', 2, 1)
if not inserted then ... win_cta ... end
The first return is the ok flag, so on a throw `inserted` holds the error
MESSAGE -- a truthy string -- and a failure reads as a success. That is a
trap worth removing, but being honest about its blast radius: it is NOT
currently reachable. Every way the first call can throw (recap_row, or
find_trail on a malformed tree) throws identically for win_cta, and the
table.insert cannot go out of bounds because spot.index indexes the very
list being inserted into. The whole suite passes with the old idiom. This
is a latent correctness fix, not a live bug -- it stops the trap biting if
insert_recap ever grows an anchor-specific failure.
The recap machinery (recap_row, find_trail, insert_recap) had no coverage.
It does now, including the win_cta fallback and the empty-run case.
925 tests pass on 5.4 and LuaJIT, 18/18 in the real game.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -342,7 +342,14 @@ function JCA.sticker_cautions(card)
|
|||||||
local key = card.config and card.config.center and card.config.center.key
|
local key = card.config and card.config.center and card.config.center.key
|
||||||
local a = card.ability or {}
|
local a = card.ability or {}
|
||||||
if a.perishable and key and JCA.scalers[key] then
|
if a.perishable and key and JCA.scalers[key] then
|
||||||
out[#out + 1] = 'perishable: gains die in 5 rounds'
|
-- Read the round count, never hardcode it: perishable_rounds is a game
|
||||||
|
-- variable (game.lua:1961 defaults it to 5, and vanilla's own sticker
|
||||||
|
-- tooltip reads it at common_events.lua:3339), so a challenge or
|
||||||
|
-- another mod can move it. perish_tally is what this card has left
|
||||||
|
-- (card.lua:680) and is the honest number when it is already ticking.
|
||||||
|
local n = a.perish_tally or (G.GAME and G.GAME.perishable_rounds) or 5
|
||||||
|
out[#out + 1] = ('perishable: gains die in %d round%s')
|
||||||
|
:format(n, n == 1 and '' or 's')
|
||||||
end
|
end
|
||||||
if a.rental and key and JCA.db[key] and JCA.db[key].gives.money then
|
if a.rental and key and JCA.db[key] and JCA.db[key].gives.money then
|
||||||
out[#out + 1] = 'rental: rent may eat its payout'
|
out[#out + 1] = 'rental: rent may eat its payout'
|
||||||
@@ -1282,12 +1289,17 @@ end
|
|||||||
-- Win: the buttons live inside the right-hand stats column; two levels up
|
-- Win: the buttons live inside the right-hand stats column; two levels up
|
||||||
-- places the recap below the whole two-column stats row. 'win_cta' replaces
|
-- places the recap below the whole two-column stats row. 'win_cta' replaces
|
||||||
-- 'from_game_won' when the CTA variant of the screen is shown.
|
-- 'from_game_won' when the CTA variant of the screen is shown.
|
||||||
|
--
|
||||||
|
-- Keep BOTH pcall returns. The first is the ok flag; on a throw the second is
|
||||||
|
-- the error MESSAGE, which is a truthy string -- so testing it alone reads a
|
||||||
|
-- failure as a success and skips the win_cta fallback, which exists precisely
|
||||||
|
-- because the two win screens anchor at different ids.
|
||||||
if create_UIBox_win then
|
if create_UIBox_win then
|
||||||
local orig_win = create_UIBox_win
|
local orig_win = create_UIBox_win
|
||||||
function create_UIBox_win(...)
|
function create_UIBox_win(...)
|
||||||
local ui = orig_win(...)
|
local ui = orig_win(...)
|
||||||
local _, inserted = pcall(insert_recap, ui, 'from_game_won', 2, 1)
|
local ok, inserted = pcall(insert_recap, ui, 'from_game_won', 2, 1)
|
||||||
if not inserted then pcall(insert_recap, ui, 'win_cta', 2, 1) end
|
if not (ok and inserted) then pcall(insert_recap, ui, 'win_cta', 2, 1) end
|
||||||
return ui
|
return ui
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -148,6 +148,24 @@ for _, f in ipairs(FusionJokers.fusions) do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Post-run recap screens. main.lua wraps these at load, so they have to exist
|
||||||
|
-- before it is required. Each returns a FRESH tree shaped like vanilla's
|
||||||
|
-- (root > column > row > button), because the recap is inserted by walking up
|
||||||
|
-- from the button id. RECAP_ANCHOR picks which id the win screen exposes, so
|
||||||
|
-- the win_cta fallback can be exercised.
|
||||||
|
RECAP_ANCHOR = 'from_game_won'
|
||||||
|
local function button_tree(id)
|
||||||
|
return {n = 'ROOT', nodes = {
|
||||||
|
{n = 'C', nodes = {
|
||||||
|
{n = 'R', nodes = {
|
||||||
|
{n = 'C', config = {id = id}},
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
}}
|
||||||
|
end
|
||||||
|
create_UIBox_game_over = function() return button_tree('from_game_over') end
|
||||||
|
create_UIBox_win = function() return button_tree(RECAP_ANCHOR) end
|
||||||
|
|
||||||
local data = dofile('synergies.lua')
|
local data = dofile('synergies.lua')
|
||||||
dofile('main.lua')
|
dofile('main.lua')
|
||||||
|
|
||||||
@@ -506,6 +524,42 @@ eq(G.GAME.jca_peak_synergy, 7, 'selling down does not lower the recorded peak')
|
|||||||
G.GAME = {}
|
G.GAME = {}
|
||||||
JCA.config.discovered, JCA.config.themes_found = saved_disc, saved_themes
|
JCA.config.discovered, JCA.config.themes_found = saved_disc, saved_themes
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
section('Engine: the post-run recap finds its anchor on both screens')
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- The recap is inserted relative to a vanilla button id by walking UP the
|
||||||
|
-- parent trail, and the two win screens anchor at different ids -- so the
|
||||||
|
-- fallback is the whole point of the hook, not decoration.
|
||||||
|
local function tree_texts(node, out)
|
||||||
|
out = out or {}
|
||||||
|
if type(node) ~= 'table' then return out end
|
||||||
|
if node.config and node.config.text then out[#out + 1] = node.config.text end
|
||||||
|
for _, child in ipairs(node.nodes or {}) do tree_texts(child, out) end
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
local function has_recap(ui)
|
||||||
|
for _, t in ipairs(tree_texts(ui)) do
|
||||||
|
if t == 'Combos fielded this run' then return true end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
G.GAME = {jca_run_combos = {['j_baron|j_mime'] = true}}
|
||||||
|
ok(has_recap(create_UIBox_game_over()), 'the game over screen gets the recap')
|
||||||
|
ok(has_recap(create_UIBox_win()), 'so does the win screen, at its own anchor')
|
||||||
|
|
||||||
|
-- The CTA variant of the win screen replaces 'from_game_won' with 'win_cta';
|
||||||
|
-- the first insert finds nothing and the fallback has to carry it.
|
||||||
|
RECAP_ANCHOR = 'win_cta'
|
||||||
|
ok(has_recap(create_UIBox_win()), 'and the win_cta variant falls back correctly')
|
||||||
|
RECAP_ANCHOR = 'from_game_won'
|
||||||
|
|
||||||
|
-- Nothing fielded and no peak: no row at all, rather than an empty heading.
|
||||||
|
G.GAME = {}
|
||||||
|
ok(not has_recap(create_UIBox_win()), 'a run with no combos gets no recap row')
|
||||||
|
G.GAME = {}
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
section('Engine: deck context counts base suits')
|
section('Engine: deck context counts base suits')
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
@@ -708,6 +762,20 @@ eq(#JCA.sticker_cautions(stickered('j_ride_the_bus', {})), 0,
|
|||||||
eq(#JCA.sticker_cautions(stickered('j_mod_unknown', {perishable = true, rental = true})), 0,
|
eq(#JCA.sticker_cautions(stickered('j_mod_unknown', {perishable = true, rental = true})), 0,
|
||||||
'unknown keys never warn and never error')
|
'unknown keys never warn and never error')
|
||||||
|
|
||||||
|
-- The round count is a GAME VARIABLE (G.GAME.perishable_rounds, game.lua:1961),
|
||||||
|
-- not the constant 5 -- a challenge or another mod can move it, and vanilla's
|
||||||
|
-- own sticker tooltip reads it. Hardcoding it would state a wrong number.
|
||||||
|
G.GAME = {perishable_rounds = 3}
|
||||||
|
eq(JCA.sticker_cautions(stickered('j_ride_the_bus', {perishable = true}))[1],
|
||||||
|
'perishable: gains die in 3 rounds', 'the caution reads the game round count')
|
||||||
|
eq(JCA.sticker_cautions(stickered('j_ride_the_bus',
|
||||||
|
{perishable = true, perish_tally = 1}))[1],
|
||||||
|
'perishable: gains die in 1 round',
|
||||||
|
'a ticking sticker reports what is LEFT, and says "round" once')
|
||||||
|
G.GAME = {}
|
||||||
|
eq(JCA.sticker_cautions(stickered('j_ride_the_bus', {perishable = true}))[1],
|
||||||
|
'perishable: gains die in 5 rounds', 'falling back to vanilla 5 outside a run')
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
section('Data: themes are reachable')
|
section('Data: themes are reachable')
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user