Integrate with the mods actually installed beside us
New integrations.lua, loaded pcall-wrapped at the end of main.lua; every integration gates on its target's global so a missing mod is silence. Both targets load before us by priority. JokerDisplay: each known joker's display definition gains a live '(2 combos)' counter in JD's own parenthetical reminder style -- the same partner count the hover tooltip names -- by appending a dynamic element to reminder_text (created when absent) and wrapping calc_function to fill card.joker_display_values. The wrapper pcalls the original calc and blanks the text when the new jd_combos config toggle is off, so it flips mid-run in both directions. Too Many Jokers: registers the SEARCH_FIELD_FUNCS hook its api.md documents, indexing every joker under its tag labels on both sides, so the collection search finds jokers by 'money', 'Hearts', 'held-card retriggers'. Unknown centers stay nil. The smoke scenario now asserts the JokerDisplay patch against the real mod: 13/13 in-game. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,7 +13,7 @@ and pulses strongly recommended cards. Pure Lua, no build step.
|
|||||||
Syntax check, then run the test suite — **always both, after every edit**:
|
Syntax check, then run the test suite — **always both, after every edit**:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
luac -p main.lua synergies.lua config.lua test.lua
|
luac -p main.lua synergies.lua config.lua integrations.lua test.lua
|
||||||
lua test.lua
|
lua test.lua
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -192,6 +192,21 @@ Three-layer split; `JCA` is the single global namespace:
|
|||||||
- `config.lua` — default settings (`pulse`, `owned_tooltip`, `threshold`).
|
- `config.lua` — default settings (`pulse`, `owned_tooltip`, `threshold`).
|
||||||
Steamodded loads and persists this table itself; read it only via
|
Steamodded loads and persists this table itself; read it only via
|
||||||
`SMODS.current_mod.config` (exposed as `JCA.config`).
|
`SMODS.current_mod.config` (exposed as `JCA.config`).
|
||||||
|
- `integrations.lua` — cross-mod integrations, loaded at the very end of
|
||||||
|
`main.lua` (pcall-wrapped; counts land in `JCA.integrations`). Every
|
||||||
|
integration gates on its target's global — a missing mod is silence — and
|
||||||
|
both targets load before us (JokerDisplay priority -1e9, TMJ -1000, ours 0):
|
||||||
|
- **JokerDisplay**: appends a `(2 combos)` counter element to each known
|
||||||
|
joker's `reminder_text` (created when absent) and wraps the definition's
|
||||||
|
`calc_function` to fill `card.joker_display_values.jca_combos` from
|
||||||
|
`partners_for`. The wrapper pcalls the original calc and blanks the text
|
||||||
|
when `config.jd_combos` is off, so the toggle works mid-run both ways.
|
||||||
|
Definitions are marked `jca_combo_counter` against double patching.
|
||||||
|
- **Too Many Jokers**: registers a `TMJ.SEARCH_FIELD_FUNCS` entry (its
|
||||||
|
api.md's documented hook) returning the joker's `TAG_LABEL`s on both
|
||||||
|
sides, so the collection search finds jokers by 'money', 'Hearts',
|
||||||
|
'held-card retriggers'... Unknown centers return nil, never an error.
|
||||||
|
The smoke scenario asserts the JokerDisplay patch against the real mod.
|
||||||
|
|
||||||
## Conventions and gotchas
|
## Conventions and gotchas
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ return {
|
|||||||
threshold = 4, -- total synergy score needed to recommend a card
|
threshold = 4, -- total synergy score needed to recommend a card
|
||||||
learning_mode = false, -- hide verdicts (pulse, "Strong pick!"), keep reasons
|
learning_mode = false, -- hide verdicts (pulse, "Strong pick!"), keep reasons
|
||||||
touch_mode = true, -- enlarge advice text/buttons for phone screens
|
touch_mode = true, -- enlarge advice text/buttons for phone screens
|
||||||
|
jd_combos = true, -- combo counter under jokers (needs JokerDisplay)
|
||||||
discovered = {}, -- famous pairs the player has fielded (pair keys)
|
discovered = {}, -- famous pairs the player has fielded (pair keys)
|
||||||
themes_found = {}, -- synergy themes the player has assembled (tags)
|
themes_found = {}, -- synergy themes the player has assembled (tags)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
--- Integrations with other installed mods, applied at the end of main.lua.
|
||||||
|
--- Every integration gates on its target mod's global, so a missing mod is
|
||||||
|
--- silence rather than an error, and each touches only jokers this database
|
||||||
|
--- knows. Both targets load before this mod does (JokerDisplay at priority
|
||||||
|
--- -1e9, Too Many Jokers at -1000, this mod at 0), so their tables exist by
|
||||||
|
--- the time these run. Each returns how much it patched, surfaced in
|
||||||
|
--- JCA.integrations for the tests and the debug log.
|
||||||
|
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
--- JokerDisplay: a live combo counter under every joker it displays.
|
||||||
|
--- Its model: JokerDisplay.Definitions[key] holds per-joker display specs;
|
||||||
|
--- dynamic text reads card.joker_display_values, which the definition's
|
||||||
|
--- calc_function fills on every display update. We append a counter element
|
||||||
|
--- in JokerDisplay's own parenthetical reminder style -- '(2 combos)', the
|
||||||
|
--- same partner count the hover tooltip names -- and wrap calc_function to
|
||||||
|
--- keep the value fresh. Definitions without a reminder_text row gain one.
|
||||||
|
--- The wrapper blanks the text when config.jd_combos is off, so the toggle
|
||||||
|
--- works mid-run in both directions; the count call is pcall-wrapped like
|
||||||
|
--- every other advisor path that runs inside the game's draw loop.
|
||||||
|
function M.jokerdisplay()
|
||||||
|
if not (JokerDisplay and JokerDisplay.Definitions) then return 0 end
|
||||||
|
local patched = 0
|
||||||
|
for key in pairs(JCA.db) do
|
||||||
|
local def = JokerDisplay.Definitions[key]
|
||||||
|
if def and not def.jca_combo_counter then
|
||||||
|
def.jca_combo_counter = true
|
||||||
|
local node = { ref_table = 'card.joker_display_values',
|
||||||
|
ref_value = 'jca_combos', colour = G.C.GREEN }
|
||||||
|
if def.reminder_text then
|
||||||
|
def.reminder_text[#def.reminder_text + 1] = { text = ' ' }
|
||||||
|
def.reminder_text[#def.reminder_text + 1] = node
|
||||||
|
else
|
||||||
|
def.reminder_text = { node }
|
||||||
|
end
|
||||||
|
local orig = def.calc_function
|
||||||
|
def.calc_function = function(card)
|
||||||
|
-- pcall like every advisor path in the draw loop: if the
|
||||||
|
-- original calc fails, the counter must still not crash.
|
||||||
|
if orig then pcall(orig, card) end
|
||||||
|
local text = ''
|
||||||
|
if JCA.config.jd_combos ~= false then
|
||||||
|
local ok, partners = pcall(JCA.partners_for, card)
|
||||||
|
local n = ok and #partners or 0
|
||||||
|
if n > 0 then
|
||||||
|
text = ('(%d combo%s)'):format(n, n == 1 and '' or 's')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
card.joker_display_values = card.joker_display_values or {}
|
||||||
|
card.joker_display_values.jca_combos = text
|
||||||
|
end
|
||||||
|
patched = patched + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return patched
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Too Many Jokers: its api.md asks for a TMJ.SEARCH_FIELD_FUNCS entry
|
||||||
|
--- mapping a center to searchable strings. We hand it every tag label on
|
||||||
|
--- both sides of a joker, so the collection search finds jokers by what
|
||||||
|
--- they give and want: 'money', 'held-card retriggers', 'Hearts'...
|
||||||
|
--- Unknown centers (other mods' jokers) return nil, the same "score 0"
|
||||||
|
--- silence the engine promises everywhere else.
|
||||||
|
function M.toomanyjokers()
|
||||||
|
if not (TMJ and TMJ.SEARCH_FIELD_FUNCS) then return 0 end
|
||||||
|
TMJ.SEARCH_FIELD_FUNCS[#TMJ.SEARCH_FIELD_FUNCS + 1] = function(center)
|
||||||
|
local entry = center and center.key and JCA.db[center.key]
|
||||||
|
if not entry then return nil end
|
||||||
|
local fields = {}
|
||||||
|
for t in pairs(entry.gives) do
|
||||||
|
if JCA.tag_label[t] then fields[#fields + 1] = JCA.tag_label[t] end
|
||||||
|
end
|
||||||
|
for t in pairs(entry.wants) do
|
||||||
|
if JCA.tag_label[t] then fields[#fields + 1] = JCA.tag_label[t] end
|
||||||
|
end
|
||||||
|
return fields
|
||||||
|
end
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
||||||
@@ -1613,6 +1613,12 @@ if SMODS.current_mod then
|
|||||||
ref_table = JCA.config, ref_value = 'touch_mode',
|
ref_table = JCA.config, ref_value = 'touch_mode',
|
||||||
},
|
},
|
||||||
}},
|
}},
|
||||||
|
{n = G.UIT.R, config = {align = 'cm', padding = 0.05}, nodes = {
|
||||||
|
create_toggle{
|
||||||
|
label = 'Combo counter under jokers (JokerDisplay)',
|
||||||
|
ref_table = JCA.config, ref_value = 'jd_combos',
|
||||||
|
},
|
||||||
|
}},
|
||||||
{n = G.UIT.R, config = {align = 'cm', padding = 0.05}, nodes = {
|
{n = G.UIT.R, config = {align = 'cm', padding = 0.05}, nodes = {
|
||||||
create_option_cycle{
|
create_option_cycle{
|
||||||
label = 'Recommendation threshold (lower = more picks)',
|
label = 'Recommendation threshold (lower = more picks)',
|
||||||
@@ -1625,3 +1631,27 @@ if SMODS.current_mod then
|
|||||||
}}
|
}}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Cross-mod integrations -------------------------------------------------------
|
||||||
|
-- integrations.lua patches JokerDisplay (combo counter under jokers) and Too
|
||||||
|
-- Many Jokers (search the collection by synergy tags) when they are present.
|
||||||
|
-- pcall on the whole load: a partner mod's surprise API change must degrade
|
||||||
|
-- to a log line, never take the advisor down with it.
|
||||||
|
|
||||||
|
do
|
||||||
|
local loaded, integrations = pcall(function()
|
||||||
|
return assert(SMODS.load_file('integrations.lua'))()
|
||||||
|
end)
|
||||||
|
if loaded and integrations then
|
||||||
|
JCA.integrations = {}
|
||||||
|
for name, apply in pairs(integrations) do
|
||||||
|
local ok, count = pcall(apply)
|
||||||
|
JCA.integrations[name] = ok and count or 0
|
||||||
|
if not ok then print('JCA: integration ' .. name .. ' failed: ' .. tostring(count)) end
|
||||||
|
end
|
||||||
|
dbg('integrations:', (JCA.integrations.jokerdisplay or 0) .. ' JokerDisplay defs,',
|
||||||
|
(JCA.integrations.toomanyjokers or 0) .. ' TMJ search hooks')
|
||||||
|
else
|
||||||
|
print('JCA: integrations failed to load: ' .. tostring(integrations))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|||||||
@@ -111,6 +111,19 @@ UIBox = function(args) return {def = args and args.definition, remove = function
|
|||||||
create_option_cycle = function() return {n = 'R', config = {}, nodes = {}} end
|
create_option_cycle = function() return {n = 'R', config = {}, nodes = {}} end
|
||||||
localize = function() error('no localization outside the game') end
|
localize = function() error('no localization outside the game') end
|
||||||
|
|
||||||
|
-- Cross-mod integration stubs, present BEFORE main.lua loads (both real mods
|
||||||
|
-- load before this one: JokerDisplay at priority -1e9, TMJ at -1000). One
|
||||||
|
-- JokerDisplay definition of each shape: with a reminder_text row and without.
|
||||||
|
JokerDisplay = {Definitions = {
|
||||||
|
j_baron = {text = {{text = 'x'}},
|
||||||
|
calc_function = function(card)
|
||||||
|
card.joker_display_values = card.joker_display_values or {}
|
||||||
|
card.joker_display_values.orig_ran = true
|
||||||
|
end},
|
||||||
|
j_mime = {text = {{text = 'x'}}, reminder_text = {{text = '(hand)'}}},
|
||||||
|
}}
|
||||||
|
TMJ = {SEARCH_FIELD_FUNCS = {}}
|
||||||
|
|
||||||
local data = dofile('synergies.lua')
|
local data = dofile('synergies.lua')
|
||||||
dofile('main.lua')
|
dofile('main.lua')
|
||||||
|
|
||||||
@@ -673,6 +686,46 @@ 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')
|
eq(JCA.score('j_mod_test', 'j_baron') - before, 4, 'JCA.register_pair adds the +4 pair bonus')
|
||||||
JCA.db.j_mod_test = nil -- keep the UI section on vanilla data only
|
JCA.db.j_mod_test = nil -- keep the UI section on vanilla data only
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
section('Integrations: JokerDisplay counter and TMJ search')
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- JokerDisplay: every known joker with a definition gains the counter, in
|
||||||
|
-- both definition shapes.
|
||||||
|
eq(JCA.integrations.jokerdisplay, 2, 'both stub JokerDisplay definitions were patched')
|
||||||
|
ok(JokerDisplay.Definitions.j_baron.reminder_text,
|
||||||
|
'a definition without reminder_text gains one for the counter')
|
||||||
|
eq(#JokerDisplay.Definitions.j_mime.reminder_text, 3,
|
||||||
|
'an existing reminder_text grows by separator + counter')
|
||||||
|
|
||||||
|
-- The wrapped calc_function fills the live count and keeps the original.
|
||||||
|
field('j_baron', 'j_mime')
|
||||||
|
local jd_card = G.jokers.cards[1]
|
||||||
|
jd_card.joker_display_values = {}
|
||||||
|
JokerDisplay.Definitions.j_baron.calc_function(jd_card)
|
||||||
|
eq(jd_card.joker_display_values.jca_combos, '(1 combo)',
|
||||||
|
'the counter shows the live board partner count')
|
||||||
|
ok(jd_card.joker_display_values.orig_ran,
|
||||||
|
'and the original calc_function still runs first')
|
||||||
|
|
||||||
|
JCA.config.jd_combos = false
|
||||||
|
JokerDisplay.Definitions.j_baron.calc_function(jd_card)
|
||||||
|
eq(jd_card.joker_display_values.jca_combos, '',
|
||||||
|
'the config toggle blanks the counter mid-run')
|
||||||
|
JCA.config.jd_combos = true
|
||||||
|
|
||||||
|
-- Too Many Jokers: the search hook indexes jokers by tag labels and stays
|
||||||
|
-- silent on centers the database does not know.
|
||||||
|
eq(JCA.integrations.toomanyjokers, 1, 'the TMJ search hook registered')
|
||||||
|
local tmj_fields = TMJ.SEARCH_FIELD_FUNCS[1]({key = 'j_baron'})
|
||||||
|
local tmj_hit = false
|
||||||
|
for _, f in ipairs(tmj_fields) do
|
||||||
|
if f == 'held-card retriggers' then tmj_hit = true end
|
||||||
|
end
|
||||||
|
ok(tmj_hit, 'TMJ search finds Baron under its tag labels')
|
||||||
|
eq(TMJ.SEARCH_FIELD_FUNCS[1]({key = 'j_totally_modded'}), nil,
|
||||||
|
'an unknown center returns nil, not an error')
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
section('UI: catalog pages build and animate')
|
section('UI: catalog pages build and animate')
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user