From 43e1ea2bc6ac3e79e5f5f7cdba9d73198fb5fe56 Mon Sep 17 00:00:00 2001 From: funman300 Date: Tue, 14 Jul 2026 15:18:24 -0700 Subject: [PATCH] Put the known traps in the catalog, as an open book A Traps tab joins the synergy catalog: clash duos rendered like the Combos tab, then the per-joker cautions, one pager spanning both page kinds. Deliberately no discovery gating and no Progress entry -- the Combos tab hides pairs to keep the fun of finding them, but a warning locked behind fielding the bad pair would reward exactly what it warns against. register_clash now maintains JCA.clash_list alongside the blurb map so third-party clashes appear on the tab, re-registration updates without duplicating, and blurbs are read back through clash_blurb at build time so the newest wording always wins. Co-Authored-By: Claude Fable 5 --- CLAUDE.md | 10 ++++- main.lua | 112 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- test.lua | 21 +++++++++- 3 files changed, 137 insertions(+), 6 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 1b06e76..0ba91be 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -157,7 +157,7 @@ Three-layer split; `JCA` is the single global namespace: - Config tab: registered on `SMODS.current_mod.config_tab`, guarded by `if SMODS.current_mod` so standalone tests don't need full stubs. - Synergy catalog: `SMODS.current_mod.extra_tabs` adds Combos/Engine/ - Economy/Hands tabs (plus a text-only Progress tab with discovery + Economy/Hands/Traps tabs (plus a text-only Progress tab with discovery completion bars) that render real `Card` objects in `CardArea`s (collection-style, so hover shows each joker's own tooltip). One theme per page; pagers swap the `tab_contents` UIBox from an option-cycle @@ -183,7 +183,13 @@ Three-layer split; `JCA` is the single global namespace: both sides needs a second joker). One toast per emplacement, famous pair outranking theme. Badge/progress render in `theme_tab_def`; toast names come from `JCA.theme_names` (filled after THEME_TABS, used only - at gameplay time). + at gameplay time). The Traps tab (clash duos, then caution jokers; one + pager spans both page kinds) is deliberately an **open book**: no + discovery gating and no Progress entry, because a warning locked behind + fielding the bad pair would reward exactly what it warns against. + `register_clash` keeps `JCA.clash_list` in sync (and sets + `JCA._traps_dirty`) so third-party clashes appear on the tab; blurbs are + read back through `clash_blurb` at build time so updates always win. - Post-run recap: wraps `create_UIBox_game_over`/`create_UIBox_win` and inserts a "Combos fielded this run" row (from `G.GAME.jca_run_combos`) relative to vanilla button ids (`from_game_over`; `from_game_won` or diff --git a/main.lua b/main.lua index 58dba6b..1c39cd6 100644 --- a/main.lua +++ b/main.lua @@ -36,9 +36,12 @@ for _, p in ipairs(data.pairs) do JCA.pair_blurb[pair_key(p[1], p[2])] = p[3] end --- Known traps: warning-only, never part of the synergy score. +-- Known traps: warning-only, never part of the synergy score. The list is +-- kept alongside the blurb map so the Traps tab can page over it; blurbs are +-- always read back through clash_blurb so register_clash updates apply. +JCA.clash_list = data.clashes or {} JCA.clash_blurb = {} -for _, p in ipairs(data.clashes or {}) do +for _, p in ipairs(JCA.clash_list) do JCA.clash_blurb[pair_key(p[1], p[2])] = p[3] end JCA.cautions = data.cautions or {} @@ -446,7 +449,12 @@ end function JCA.register_clash(a, b, warning) assert(type(a) == 'string' and type(b) == 'string', 'JCA.register_clash: two joker keys required') - JCA.clash_blurb[pair_key(a, b)] = warning + local pk = pair_key(a, b) + if not JCA.clash_blurb[pk] then + JCA.clash_list[#JCA.clash_list + 1] = {a, b, warning} + end + JCA.clash_blurb[pk] = warning + JCA._traps_dirty = true -- traps tab re-sorts on next open end local function in_buy_area(card) @@ -1434,6 +1442,99 @@ local function combos_tab_def(page_no) colour = G.C.CLEAR, minw = 8.5, minh = 5.6}, nodes = nodes} end +-- Traps tab: the known clashes and cautions as an open book. Deliberately NO +-- discovery gating and no Progress entry: the Combos tab hides pairs to keep +-- the fun of finding them, but a warning locked behind falling into the trap +-- would reward fielding exactly the pairs the advisor exists to warn against. +-- Clash pages come first (card duos, like Combos), then caution pages (single +-- jokers); one pager spans both. Blurbs are read through clash_blurb / +-- cautions at build time, so register_clash updates show without a rebuild. + +local TRAPS_PER_PAGE = 6 -- 2 rows x 3 columns, clash and caution pages alike + +-- One catalog column: the card(s) above the warning that explains the trap. +local function trap_col(keys, blurb, wrap_width, max_lines) + local col = {{n = G.UIT.R, config = {align = 'cm'}, + nodes = {card_row_node(keys, 0.62)}}} + local cap = wrap_plain(blurb or '', wrap_width) + for i = 1, math.min(#cap, max_lines) do + col[#col + 1] = {n = G.UIT.R, config = {align = 'cm', padding = 0.01}, nodes = { + {n = G.UIT.T, config = {text = cap[i], colour = G.C.RED, scale = 0.22}}, + }} + end + return {n = G.UIT.C, config = {align = 'cm', padding = 0.08}, nodes = col} +end + +local sorted_clashes, sorted_cautions +local function traps_tab_def(page_no) + begin_page() + if not sorted_clashes or JCA._traps_dirty then + JCA._traps_dirty = false + sorted_clashes = {} + for _, c in ipairs(JCA.clash_list) do + sorted_clashes[#sorted_clashes + 1] = c + end + table.sort(sorted_clashes, function(a, b) + local an, bn = name_of(a[1]), name_of(b[1]) + if an ~= bn then return an < bn end + return name_of(a[2]) < name_of(b[2]) + end) + sorted_cautions = {} + for key in pairs(JCA.cautions) do + sorted_cautions[#sorted_cautions + 1] = key + end + table.sort(sorted_cautions, function(a, b) return name_of(a) < name_of(b) end) + end + local clash_pages = math.ceil(#sorted_clashes / TRAPS_PER_PAGE) + local caution_pages = math.ceil(#sorted_cautions / TRAPS_PER_PAGE) + local pages = math.max(clash_pages + caution_pages, 1) + page_no = math.min(page_no or 1, pages) + + local nodes = {} + if page_no <= clash_pages then + add_caption(nodes, ('Clashes - %d known pairs where one eats the other\'s payoff') + :format(#sorted_clashes)) + local first = (page_no - 1) * TRAPS_PER_PAGE + for r = 0, 1 do + local row = {n = G.UIT.R, config = {align = 'cm', padding = 0.05}, nodes = {}} + for c = 1, 3 do + local clash = sorted_clashes[first + r * 3 + c] + if clash then + row.nodes[#row.nodes + 1] = trap_col({clash[1], clash[2]}, + JCA.clash_blurb[pair_key(clash[1], clash[2])], 30, 2) + end + end + if #row.nodes > 0 then nodes[#nodes + 1] = row end + end + else + add_caption(nodes, ('Cautions - %d jokers that carry their own trap') + :format(#sorted_cautions)) + local first = (page_no - clash_pages - 1) * TRAPS_PER_PAGE + for r = 0, 1 do + local row = {n = G.UIT.R, config = {align = 'cm', padding = 0.05}, nodes = {}} + for c = 1, 3 do + local key = sorted_cautions[first + r * 3 + c] + if key then + row.nodes[#row.nodes + 1] = trap_col({key}, JCA.cautions[key], 26, 3) + end + end + if #row.nodes > 0 then nodes[#nodes + 1] = row end + end + end + if pages > 1 then + local options = {} + for i = 1, clash_pages do + options[#options + 1] = ('Clashes %d/%d'):format(i, clash_pages) + end + for i = 1, caution_pages do + options[#options + 1] = ('Cautions %d/%d'):format(i, caution_pages) + end + nodes[#nodes + 1] = pager_row(options, page_no, 'jca_page_Traps') + end + return {n = G.UIT.ROOT, config = {align = 'tm', padding = 0.05, + colour = G.C.CLEAR, minw = 8.5, minh = 5.6}, nodes = nodes} +end + -- Progress tab: discovery completion at a glance. Text-only (no live Card -- objects), so unlike the other tabs nothing here would break if cached — -- but it is rebuilt each open like the rest, which keeps counts fresh. @@ -1517,6 +1618,7 @@ local function register_pager(name, def_fn) end register_pager('Combos', combos_tab_def) +register_pager('Traps', traps_tab_def) for _, group in ipairs(THEME_TABS) do register_pager(group.label, function(p) return theme_tab_def(group, p) end) end @@ -1533,6 +1635,10 @@ local function catalog_tabs() tab_definition_function = function() return theme_tab_def(t, 1) end, } end + tabs[#tabs + 1] = { + label = 'Traps', + tab_definition_function = function() return traps_tab_def(1) end, + } tabs[#tabs + 1] = { label = 'Progress', tab_definition_function = progress_tab_def, diff --git a/test.lua b/test.lua index e296209..f55b796 100644 --- a/test.lua +++ b/test.lua @@ -716,6 +716,15 @@ ok(JCA.score('j_mod_test', 'j_dusk') >= 2, 'a registered joker scores against va local before = JCA.score('j_mod_test', 'j_baron') JCA.register_pair('j_mod_test', 'j_baron', 'test blurb') eq(JCA.score('j_mod_test', 'j_baron') - before, 4, 'JCA.register_pair adds the +4 pair bonus') + +-- register_clash must feed the Traps tab too: the list gains the entry once, +-- and a re-registration updates the blurb without duplicating it. +local clashes_before = #JCA.clash_list +JCA.register_clash('j_mod_test', 'j_baron', 'test warning') +eq(#JCA.clash_list, clashes_before + 1, 'JCA.register_clash lands in the traps list') +JCA.register_clash('j_mod_test', 'j_baron', 'updated warning') +eq(#JCA.clash_list, clashes_before + 1, 're-registering a clash does not duplicate it') +ok(JCA._traps_dirty, 'and marks the traps tab for a re-sort') JCA.db.j_mod_test = nil -- keep the UI section on vanilla data only -------------------------------------------------------------------------------- @@ -787,12 +796,22 @@ G.OVERLAY_MENU = {get_UIE_by_ID = function() return {config = {object = {remove = function() end}}, UIBox = {recalculate = function() end}} end} -for _, name in ipairs({'Combos', 'Engine', 'Economy', 'Hands'}) do +for _, name in ipairs({'Combos', 'Engine', 'Economy', 'Hands', 'Traps'}) do build(name .. ' page 2', function() G.FUNCS['jca_page_' .. name]{cycle_config = {current_option = 2}} end) end +-- The Traps pager spans two page kinds; also build the first CAUTION page +-- (the option right after the last clash page) and the very last page. +local first_caution = math.ceil(#JCA.clash_list / 6) + 1 +build('Traps first caution page', function() + G.FUNCS.jca_page_Traps{cycle_config = {current_option = first_caution}} +end) +build('Traps last page', function() + G.FUNCS.jca_page_Traps{cycle_config = {current_option = 999}} +end) + -------------------------------------------------------------------------------- print(('\n%d passed, %d failed'):format(pass, fail)) os.exit(fail == 0 and 0 or 1)