Initial release: Joker Combo Advisor v1.0.0
Steamodded mod that teaches Balatro joker synergies: hover tooltips explain why jokers combo (famous-pair blurbs + tag-based reasons), combo discovery tracking with catalog grey-out and run recap, an in-game synergy catalog with real card sprites, learning mode, and a HUD shortcut. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
.claude/
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project
|
||||||
|
|
||||||
|
"Joker Combo Advisor" — a Steamodded (SMODS) mod for Balatro that scores shop/pack
|
||||||
|
jokers against the player's current jokers, shows a "Combo Advisor" tooltip on hover,
|
||||||
|
and pulses strongly recommended cards. Pure Lua, no build step.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
Syntax check everything:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
luac -p main.lua synergies.lua config.lua
|
||||||
|
```
|
||||||
|
|
||||||
|
There is no test framework. Test the engine standalone by stubbing the game globals
|
||||||
|
before loading `main.lua` (the game itself cannot be launched headlessly here):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
lua - <<'EOF'
|
||||||
|
SMODS = { load_file = function(f) return loadfile(f) end,
|
||||||
|
current_mod = { config = dofile('config.lua') } }
|
||||||
|
Card = { generate_UIBox_ability_table = function() end, update = function() end }
|
||||||
|
G = { UIT = {ROOT='ROOT', R='R', T='T'},
|
||||||
|
C = { UI = {TEXT_DARK='dark'}, GREEN='green', CLEAR='clear' },
|
||||||
|
STAGES = {RUN=1}, FUNCS = {} }
|
||||||
|
dofile('main.lua')
|
||||||
|
print(JCA.score('j_baron', 'j_mime')) -- pairwise score
|
||||||
|
-- simulate owned jokers via G.jokers.cards; each card needs
|
||||||
|
-- { config = { center = { key=..., name=..., set='Joker' } } }
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected score tiers: famous explicit pair + tags ≈ 6, hand-type/suit cluster = 4,
|
||||||
|
single tag match = 2, generic mult+xmult complement = 1, unrelated = 0.
|
||||||
|
|
||||||
|
## In-game verification (this machine)
|
||||||
|
|
||||||
|
- The mod is **symlinked** into the live Mods folder:
|
||||||
|
`~/.local/share/Balatro/Mods/JokerComboAdvisor -> ~/Documents/Balatro Mod`.
|
||||||
|
Edits here go live on next game launch — do not copy files anywhere.
|
||||||
|
- Game: Steam/Proton at `/mnt/games/Steam/steamapps/common/Balatro` (note: second
|
||||||
|
Steam library, not the default paths). Lovely injector + Steamodded 1.0.0-beta
|
||||||
|
are already installed; mods are managed via Balatro Mod Manager.
|
||||||
|
- Crash/load logs: `~/.local/share/Balatro/Mods/lovely/log/` (newest file).
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
Three-layer split; `JCA` is the single global namespace:
|
||||||
|
|
||||||
|
- `synergies.lua` — data only. Returns `{ jokers = {...}, pairs = {...} }`. Each of
|
||||||
|
the 150 vanilla jokers gets `gives` (capabilities it adds) and `wants`
|
||||||
|
(capabilities that strengthen it) tag lists, converted to sets at the bottom of
|
||||||
|
the file. `PAIRS` lists famous explicit combos worth a flat +4.
|
||||||
|
- `main.lua` — engine + game integration:
|
||||||
|
- Scoring: `JCA.score(a, b)` = explicit pair bonus (+4) + 2 per give→want tag
|
||||||
|
match in each direction + 1 for the generic "+Mult/+Chips source with an
|
||||||
|
×Mult joker" complement. `JCA.partners_for(card)` aggregates over
|
||||||
|
`G.jokers.cards`; total ≥ `JCA.config.threshold` ⇒ recommended.
|
||||||
|
- UI hooks: wraps `Card:generate_UIBox_ability_table` to append a tooltip entry
|
||||||
|
to `aut.info` (format: array of rows-of-UIT-nodes plus a `.name` string —
|
||||||
|
must match what vanilla `info_tip_from_rows` expects), and wraps
|
||||||
|
`Card:update` for the ~2.5s pulse on recommended shop cards. Both hook bodies
|
||||||
|
are pcall-wrapped so a scoring bug can never crash a run — keep it that way.
|
||||||
|
- 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 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
|
||||||
|
callback — the same pattern as SMODS's achievements tab. Because pages
|
||||||
|
contain live Card/CardArea objects, tab definitions must be rebuilt on
|
||||||
|
every call — never cache the node trees. The same tabs open in-run via
|
||||||
|
a "Combos" HUD button (wrapped `create_UIBox_HUD`, injected into the
|
||||||
|
`button_area` node under Run Info/Options); the pagers work there
|
||||||
|
because vanilla `create_tabs` also names its body `tab_contents`.
|
||||||
|
- Discovery: `CardArea:emplace` hook calls `JCA.check_discoveries` when a
|
||||||
|
joker lands in `G.jokers`; famous pairs fielded together persist in
|
||||||
|
`config.discovered` (saved immediately via `SMODS.save_mod_config`) and
|
||||||
|
per-run in `G.GAME.jca_run_combos`. Undiscovered pairs render face-down
|
||||||
|
with a `???` caption in the Combos tab; blurb replaces it once fielded.
|
||||||
|
- 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
|
||||||
|
`win_cta`) via a parent-trail walk — anchor depths differ per screen.
|
||||||
|
- Learning mode (`config.learning_mode`): suppresses the pulse and the
|
||||||
|
"Strong pick!" line but never the explanations. No-synergy buy-area
|
||||||
|
tooltips show "Looking for:"/"Offers:" hints from `TAG_LABEL`.
|
||||||
|
`synergies.lua` also exports `theme_info` (one teaching sentence per
|
||||||
|
catalog tag — keep ≤ ~90 chars and cover every THEME_TABS tag).
|
||||||
|
- The real (Lovely-patched) game source is readable at
|
||||||
|
`~/.local/share/Balatro/Mods/lovely/dump/` — check it before assuming
|
||||||
|
what a vanilla function returns or expects.
|
||||||
|
- `config.lua` — default settings (`pulse`, `owned_tooltip`, `threshold`).
|
||||||
|
Steamodded loads and persists this table itself; read it only via
|
||||||
|
`SMODS.current_mod.config` (exposed as `JCA.config`).
|
||||||
|
|
||||||
|
## Conventions and gotchas
|
||||||
|
|
||||||
|
- **Cluster tags** (hand types `pair`/`straight`/`flush`/…, suits, `planet`,
|
||||||
|
`spectral_gen`): put the same tag in *both* `gives` and `wants` of every member
|
||||||
|
so members mutually reinforce (2+2 = 4). Pure enablers (e.g. Four Fingers,
|
||||||
|
Smeared, Pareidolia) only `give`; pure payoffs only `want`.
|
||||||
|
- Every `wants` tag must be given by at least one joker — the standalone test's
|
||||||
|
orphan-tag check enforces this idea; re-run it after database edits.
|
||||||
|
- Tooltips teach: `JCA.explain(hovered, partner)` picks one reason per partner
|
||||||
|
— famous-pair blurb (third element of each `PAIRS` entry), else the first
|
||||||
|
`TAG_ORDER` match phrased from `TAG_TEXT` (give/want/both variants), else
|
||||||
|
the generic mult×xmult line. Every non-engine tag (anything besides
|
||||||
|
mult/chips/xmult) MUST have a `TAG_TEXT` entry; new `PAIRS` entries need a
|
||||||
|
blurb under ~34 chars so name + blurb fits one tooltip row.
|
||||||
|
- Vanilla center keys contain intentional misspellings/abbreviations — do not
|
||||||
|
"fix" them: `j_gluttenous_joker`, `j_selzer`, `j_ticket` (Golden Ticket),
|
||||||
|
`j_trousers` (Spare Trousers), `j_ring_master` (Showman), `j_caino` (Canio),
|
||||||
|
`j_delayed_grat`, `j_todo_list`.
|
||||||
|
- Unknown joker keys (from other mods) must keep scoring 0 — never index
|
||||||
|
`JCA.db[key]` without a nil guard.
|
||||||
|
- Display names go through `localize{type='name_text', set='Joker', ...}` with a
|
||||||
|
pcall + `center.name` fallback; anti-synergies are deliberately not modeled.
|
||||||
|
- The UI hooks are the only untested-in-game surface; if a tooltip regression is
|
||||||
|
reported, suspect the `aut.info` entry format first and check the lovely log.
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"id": "JokerComboAdvisor",
|
||||||
|
"name": "Joker Combo Advisor",
|
||||||
|
"display_name": "Combo Advisor",
|
||||||
|
"author": ["Alex"],
|
||||||
|
"description": "Recommends joker combos: highlights shop jokers that synergize with your current build and explains why on hover.",
|
||||||
|
"prefix": "jca",
|
||||||
|
"main_file": "main.lua",
|
||||||
|
"priority": 0,
|
||||||
|
"badge_colour": "F5A623",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"dependencies": ["Steamodded (>=1.0.0~BETA)"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
# Joker Combo Advisor
|
||||||
|
|
||||||
|
A [Steamodded](https://github.com/Steamodded/smods) mod for Balatro that
|
||||||
|
recommends joker combos while you play:
|
||||||
|
|
||||||
|
- **Hover any joker in the shop or a Buffoon Pack** and a *Combo Advisor*
|
||||||
|
tooltip appears next to its description, naming which of your current
|
||||||
|
jokers it synergizes with **and why** ("Mime: retriggers cards in hand"),
|
||||||
|
so you learn the mechanics rather than just the verdict. Famous pairs get
|
||||||
|
hand-written explanations; everything else is phrased from the synergy
|
||||||
|
tags. Jokers with no current synergy teach instead what to pair them with
|
||||||
|
("Looking for: Tarot cards" / "Offers: face cards"). Strong picks also get
|
||||||
|
a "Strong pick for this build!" callout and **pulse periodically** in the
|
||||||
|
shop so you can spot them without hovering.
|
||||||
|
- **Combo discovery**: the first time you field a famous pair together, a
|
||||||
|
"New combo!" toast fires and the pair is permanently inked into the
|
||||||
|
catalog — until then its cards sit face-down with a "???" caption, so
|
||||||
|
filling in the Combos tab becomes its own collection hunt.
|
||||||
|
- **Hover a joker you own** to see its active combos with the rest of your
|
||||||
|
lineup.
|
||||||
|
- **Browse the synergy catalog** any time: a purple **Combos** button in the
|
||||||
|
run HUD (under Run Info / Options) opens it in an overlay, and the same
|
||||||
|
tabs live under Mods > Combo Advisor. The *Combos* tab shows the famous
|
||||||
|
pairs as card duos with their explanation once fielded; *Engine* /
|
||||||
|
*Economy* / *Hands* tabs page through each synergy theme with a one-line
|
||||||
|
explanation of the mechanic and its **Sources** and **Payoffs** as real
|
||||||
|
cards — hover any card for its normal description tooltip. All of it is generated
|
||||||
|
from the same database the advisor scores with. Jokers you haven't
|
||||||
|
discovered yet show face-down (no spoilers) unless the profile has used
|
||||||
|
the game's own **Unlock All** setting, in which case everything is shown.
|
||||||
|
- **Post-run combo recap**: the win and game-over screens list the famous
|
||||||
|
pairs you fielded during that run.
|
||||||
|
|
||||||
|
## How recommendations work
|
||||||
|
|
||||||
|
`synergies.lua` tags all 150 vanilla jokers with what they *give* a build
|
||||||
|
(e.g. `retrig_scoring`, `gold_gen`, `xmult`) and what they *want*
|
||||||
|
(e.g. `faces`, `probability`, `money`). Two jokers score:
|
||||||
|
|
||||||
|
| Signal | Points |
|
||||||
|
| --- | --- |
|
||||||
|
| Famous explicit pair (Baron + Mime, Midas Mask + Golden Ticket, …) | +4 |
|
||||||
|
| Each *gives* tag the other *wants* (both directions) | +2 |
|
||||||
|
| Generic engine complement (+Mult/+Chips source with an ×Mult joker) | +1 |
|
||||||
|
|
||||||
|
A shop joker is *recommended* (pulses) when its total score against your
|
||||||
|
owned jokers reaches the configured threshold (default 4). Partners with a
|
||||||
|
pairwise score of 2+ are listed in the tooltip, strongest first.
|
||||||
|
|
||||||
|
## Settings
|
||||||
|
|
||||||
|
In-game: **Mods > Combo Advisor > Config**. Changes persist across sessions.
|
||||||
|
|
||||||
|
- **Pulse recommended jokers in shop** — turn the visual pulse on/off.
|
||||||
|
- **Show combo tooltip on owned jokers** — the "Active combos" section on
|
||||||
|
jokers you already hold.
|
||||||
|
- **Learning mode** — hides the verdicts (pulse and "Strong pick!") while
|
||||||
|
keeping all the explanations, for players who want to learn the synergies
|
||||||
|
rather than be told what to buy.
|
||||||
|
- **Recommendation threshold** — 2 (chatty) to 8 (only slam-dunk combos).
|
||||||
|
|
||||||
|
Defaults live in `config.lua`.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Install [Lovely](https://github.com/ethangreen-dev/lovely-injector) and
|
||||||
|
[Steamodded](https://github.com/Steamodded/smods) (follow their READMEs).
|
||||||
|
2. Copy this whole folder into your Balatro `Mods` directory:
|
||||||
|
- **Windows:** `%AppData%/Balatro/Mods/`
|
||||||
|
- **Linux (Steam/Proton):**
|
||||||
|
`~/.local/share/Steam/steamapps/compatdata/2379780/pfx/drive_c/users/steamuser/AppData/Roaming/Balatro/Mods/`
|
||||||
|
(or the same path under `~/.steam/steam/`, or
|
||||||
|
`~/.var/app/com.valvesoftware.Steam/.local/share/Steam/...` for Flatpak Steam)
|
||||||
|
- **macOS:** `~/Library/Application Support/Balatro/Mods/`
|
||||||
|
3. Launch Balatro. The mod should appear in the in-game **Mods** menu.
|
||||||
|
If something goes wrong, check the Lovely log
|
||||||
|
(`Mods/lovely/log/` next to the game, or the console window on launch).
|
||||||
|
|
||||||
|
## Extending it
|
||||||
|
|
||||||
|
- **Add or fix a synergy:** edit `synergies.lua`. Give a joker a tag with
|
||||||
|
`j('j_key', {gives...}, {wants...})`, or add a famous pair to the `PAIRS`
|
||||||
|
table at the bottom. Modded jokers can be added the same way using their
|
||||||
|
full key (e.g. `j_mymod_cooljoker`) — unknown jokers simply score 0.
|
||||||
|
- **Tune sensitivity:** use the in-game Config tab, or change the defaults
|
||||||
|
in `config.lua`.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
| --- | --- |
|
||||||
|
| `JokerComboAdvisor.json` | Steamodded mod metadata |
|
||||||
|
| `main.lua` | Scoring engine, hover tooltip, shop pulse, config tab |
|
||||||
|
| `synergies.lua` | Tag database for all 150 vanilla jokers + famous pairs |
|
||||||
|
| `config.lua` | Default settings (persisted by Steamodded once changed in-game) |
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- Default settings for Joker Combo Advisor. Steamodded loads this table,
|
||||||
|
-- persists changes made in the in-game Config tab, and restores them on boot.
|
||||||
|
return {
|
||||||
|
pulse = true, -- recommended shop jokers pulse periodically
|
||||||
|
owned_tooltip = true, -- show "Active combos" tooltip on owned jokers
|
||||||
|
threshold = 4, -- total synergy score needed to recommend a card
|
||||||
|
learning_mode = false, -- hide verdicts (pulse, "Strong pick!"), keep reasons
|
||||||
|
discovered = {}, -- famous pairs the player has fielded (pair keys)
|
||||||
|
}
|
||||||
@@ -0,0 +1,842 @@
|
|||||||
|
--- Joker Combo Advisor
|
||||||
|
--- Scores jokers in the shop (and in booster packs) against the jokers you
|
||||||
|
--- currently own, shows a "Combo Advisor" tooltip on hover explaining which
|
||||||
|
--- of your jokers they synergize with, and makes strong picks pulse.
|
||||||
|
|
||||||
|
JCA = JCA or {}
|
||||||
|
|
||||||
|
-- Diagnostics: prints land in Mods/lovely/log as "[G] JCA: ..." lines.
|
||||||
|
JCA.DEBUG = false
|
||||||
|
local function dbg(...)
|
||||||
|
if JCA.DEBUG then print('JCA:', ...) end
|
||||||
|
end
|
||||||
|
|
||||||
|
local data = assert(SMODS.load_file('synergies.lua'))()
|
||||||
|
JCA.db = data.jokers
|
||||||
|
|
||||||
|
-- Settings from config.lua, persisted by Steamodded across sessions.
|
||||||
|
-- Fallback defaults keep the engine usable outside the game (tests).
|
||||||
|
JCA.mod = SMODS.current_mod
|
||||||
|
JCA.config = SMODS.current_mod and SMODS.current_mod.config
|
||||||
|
or { pulse = true, owned_tooltip = true, threshold = 4, discovered = {} }
|
||||||
|
JCA.config.discovered = JCA.config.discovered or {}
|
||||||
|
|
||||||
|
-- Explicit famous pairs, stored under a canonical "a|b" key (a < b).
|
||||||
|
JCA.pair_list = data.pairs
|
||||||
|
JCA.pair_bonus = {}
|
||||||
|
JCA.pair_blurb = {}
|
||||||
|
local function pair_key(a, b)
|
||||||
|
if a < b then return a .. '|' .. b end
|
||||||
|
return b .. '|' .. a
|
||||||
|
end
|
||||||
|
for _, p in ipairs(data.pairs) do
|
||||||
|
JCA.pair_bonus[pair_key(p[1], p[2])] = true
|
||||||
|
JCA.pair_blurb[pair_key(p[1], p[2])] = p[3]
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Scoring -------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- Synergy score between two joker center keys. 0 = no known synergy.
|
||||||
|
function JCA.score(a, b)
|
||||||
|
local s = 0
|
||||||
|
if JCA.pair_bonus[pair_key(a, b)] then s = s + 4 end
|
||||||
|
local A, B = JCA.db[a], JCA.db[b]
|
||||||
|
if A and B then
|
||||||
|
for t in pairs(A.gives) do
|
||||||
|
if B.wants[t] then s = s + 2 end
|
||||||
|
end
|
||||||
|
for t in pairs(B.gives) do
|
||||||
|
if A.wants[t] then s = s + 2 end
|
||||||
|
end
|
||||||
|
-- Generic engine complement: a +Mult/+Chips source and an xMult
|
||||||
|
-- multiplier always help each other a little.
|
||||||
|
local a_scaler = A.gives.mult or A.gives.chips
|
||||||
|
local b_scaler = B.gives.mult or B.gives.chips
|
||||||
|
if (A.gives.xmult and b_scaler) or (B.gives.xmult and a_scaler) then
|
||||||
|
s = s + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return s
|
||||||
|
end
|
||||||
|
|
||||||
|
-- One short reason why `partner` works with `hovered`, phrased from the
|
||||||
|
-- partner's side ("Mime: retriggers cards in hand"). Most specific wins:
|
||||||
|
-- famous-pair blurb, then the first tag match in TAG_ORDER (what the partner
|
||||||
|
-- gives to this card before what it takes), then the generic engine
|
||||||
|
-- complement.
|
||||||
|
function JCA.explain(hovered, partner)
|
||||||
|
local blurb = JCA.pair_blurb[pair_key(hovered, partner)]
|
||||||
|
if blurb then return blurb end
|
||||||
|
local A, B = JCA.db[hovered], JCA.db[partner]
|
||||||
|
if not (A and B) then return nil end
|
||||||
|
for _, t in ipairs(data.tag_order) do
|
||||||
|
local text = data.tag_text[t]
|
||||||
|
if text then
|
||||||
|
local cluster = text.both
|
||||||
|
and ((A.gives[t] and A.wants[t]) or (B.gives[t] and B.wants[t]))
|
||||||
|
if B.gives[t] and A.wants[t] then
|
||||||
|
return cluster and text.both or text.give
|
||||||
|
elseif A.gives[t] and B.wants[t] then
|
||||||
|
return cluster and text.both or text.want
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if B.gives.xmult and (A.gives.mult or A.gives.chips) then
|
||||||
|
return 'multiplies what this adds'
|
||||||
|
elseif A.gives.xmult and (B.gives.mult or B.gives.chips) then
|
||||||
|
return 'feeds this multiplier'
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function name_of(key)
|
||||||
|
local ok, name = pcall(function()
|
||||||
|
return localize{type = 'name_text', set = 'Joker', key = key}
|
||||||
|
end)
|
||||||
|
if ok and type(name) == 'string' and name ~= 'ERROR' then return name end
|
||||||
|
local center = G.P_CENTERS and G.P_CENTERS[key]
|
||||||
|
return center and center.name or key
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Owned jokers with synergy score >= 2 against `card`, strongest first.
|
||||||
|
-- Returns the partner list and the total score across all owned jokers.
|
||||||
|
function JCA.partners_for(card)
|
||||||
|
local partners, total = {}, 0
|
||||||
|
if not (G.jokers and G.jokers.cards) then return partners, 0 end
|
||||||
|
local key = card.config.center.key
|
||||||
|
for _, owned in ipairs(G.jokers.cards) do
|
||||||
|
if owned ~= card and owned.config.center.set == 'Joker' then
|
||||||
|
local s = JCA.score(key, owned.config.center.key)
|
||||||
|
total = total + s
|
||||||
|
if s >= 2 then
|
||||||
|
partners[#partners + 1] = {
|
||||||
|
key = owned.config.center.key,
|
||||||
|
name = name_of(owned.config.center.key),
|
||||||
|
score = s,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.sort(partners, function(x, y) return x.score > y.score end)
|
||||||
|
return partners, total
|
||||||
|
end
|
||||||
|
|
||||||
|
-- A shop/pack joker is "recommended" (pulses) when its combined synergy with
|
||||||
|
-- the current build clears the configured threshold.
|
||||||
|
function JCA.is_recommended(card)
|
||||||
|
local _, total = JCA.partners_for(card)
|
||||||
|
return total >= JCA.config.threshold
|
||||||
|
end
|
||||||
|
|
||||||
|
local function in_buy_area(card)
|
||||||
|
return card.area and (card.area == G.shop_jokers or card.area == G.pack_cards)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function is_joker(card)
|
||||||
|
return card.config and card.config.center and card.config.center.set == 'Joker'
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Hover tooltip ---------------------------------------------------------------
|
||||||
|
|
||||||
|
-- Wrap partner names into short tooltip lines.
|
||||||
|
local function wrap_names(partners, max_width, max_lines)
|
||||||
|
local lines, line = {}, nil
|
||||||
|
for i, p in ipairs(partners) do
|
||||||
|
local nxt = line and (line .. ', ' .. p.name) or p.name
|
||||||
|
if line and #nxt > max_width then
|
||||||
|
lines[#lines + 1] = line
|
||||||
|
line = p.name
|
||||||
|
else
|
||||||
|
line = nxt
|
||||||
|
end
|
||||||
|
if #lines >= max_lines then
|
||||||
|
lines[#lines] = lines[#lines] .. ' (+' .. (#partners - i + 1) .. ' more)'
|
||||||
|
return lines
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if line then lines[#lines + 1] = line end
|
||||||
|
return lines
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Short theme nouns for the "Looking for:"/"Offers:" hints (engine roles
|
||||||
|
-- mult/chips/xmult are deliberately absent — they hint nothing specific).
|
||||||
|
local TAG_LABEL = {
|
||||||
|
faces = 'face cards', retrig_scoring = 'played-card retriggers',
|
||||||
|
retrig_held = 'held-card retriggers', hand_size = 'hand size',
|
||||||
|
hands_up = 'extra hands', discards_up = 'discards',
|
||||||
|
probability = 'listed odds', money = 'money', hand_level = 'hand levels',
|
||||||
|
tarot_gen = 'Tarot cards', planet = 'Planet cards',
|
||||||
|
spectral_gen = 'Spectral cards', gold_gen = 'Gold cards',
|
||||||
|
stone_gen = 'Stone cards', enhance_gen = 'card enhancing',
|
||||||
|
card_gen = 'deck additions', joker_gen = 'Joker creation',
|
||||||
|
sell_value = 'sell value', copy_target = 'copy targets',
|
||||||
|
suit_flex = 'suit conversion', pair = 'Pairs', two_pair = 'Two Pair',
|
||||||
|
three_kind = 'Three of a Kind', four_kind = 'Four of a Kind',
|
||||||
|
straight = 'Straights', flush = 'Flushes', hearts = 'Hearts',
|
||||||
|
diamonds = 'Diamonds', spades = 'Spades', clubs = 'Clubs',
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Labels for a joker's tag set, in stable data.tag_order order.
|
||||||
|
local function tag_labels(tag_set)
|
||||||
|
local labels = {}
|
||||||
|
for _, t in ipairs(data.tag_order) do
|
||||||
|
if tag_set[t] and TAG_LABEL[t] then labels[#labels + 1] = {name = TAG_LABEL[t]} end
|
||||||
|
end
|
||||||
|
return labels
|
||||||
|
end
|
||||||
|
|
||||||
|
local function tooltip_rows(card)
|
||||||
|
local partners, total = JCA.partners_for(card)
|
||||||
|
|
||||||
|
local rows = {}
|
||||||
|
local function text_row(str, colour)
|
||||||
|
rows[#rows + 1] = {{n = G.UIT.T, config = {
|
||||||
|
text = str, colour = colour or G.C.UI.TEXT_DARK, scale = 0.32,
|
||||||
|
}}}
|
||||||
|
end
|
||||||
|
|
||||||
|
if #partners == 0 then
|
||||||
|
text_row(in_buy_area(card) and 'No synergy with your jokers.'
|
||||||
|
or 'No combos with your other jokers.')
|
||||||
|
-- Teach what to pair it with later: what it wants, else what it gives.
|
||||||
|
local entry = in_buy_area(card) and JCA.db[card.config.center.key]
|
||||||
|
if entry then
|
||||||
|
local labels, prefix = tag_labels(entry.wants), 'Looking for: '
|
||||||
|
if #labels == 0 then
|
||||||
|
labels, prefix = tag_labels(entry.gives), 'Offers: '
|
||||||
|
end
|
||||||
|
if #labels > 0 then
|
||||||
|
for i, l in ipairs(wrap_names(labels, 36, 2)) do
|
||||||
|
rows[#rows + 1] = {{n = G.UIT.T, config = {
|
||||||
|
text = (i == 1 and prefix or ' ') .. l,
|
||||||
|
colour = G.C.UI.TEXT_DARK, scale = 0.3,
|
||||||
|
}}}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if in_buy_area(card) then
|
||||||
|
text_row('Synergizes with your jokers:')
|
||||||
|
else
|
||||||
|
text_row('Active combos with:')
|
||||||
|
end
|
||||||
|
local key = card.config.center.key
|
||||||
|
local shown = math.min(3, #partners)
|
||||||
|
for i = 1, shown do
|
||||||
|
local p = partners[i]
|
||||||
|
local reason = JCA.explain(key, p.key) or 'shared build theme'
|
||||||
|
rows[#rows + 1] = {
|
||||||
|
{n = G.UIT.T, config = {text = p.name .. ': ',
|
||||||
|
colour = G.C.RED, scale = 0.3}},
|
||||||
|
{n = G.UIT.T, config = {text = reason,
|
||||||
|
colour = G.C.UI.TEXT_DARK, scale = 0.3}},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
if #partners > shown then
|
||||||
|
local rest = {}
|
||||||
|
for i = shown + 1, #partners do rest[#rest + 1] = partners[i] end
|
||||||
|
for i, l in ipairs(wrap_names(rest, 34, 2)) do
|
||||||
|
text_row((i == 1 and 'also: ' or ' ') .. l, G.C.UI.TEXT_DARK)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if in_buy_area(card) and total >= JCA.config.threshold
|
||||||
|
and not JCA.config.learning_mode then
|
||||||
|
text_row('Strong pick for this build!', G.C.GREEN)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
rows.name = 'Combo Advisor'
|
||||||
|
return rows
|
||||||
|
end
|
||||||
|
|
||||||
|
local orig_gen_aut = Card.generate_UIBox_ability_table
|
||||||
|
function Card:generate_UIBox_ability_table(vars_only)
|
||||||
|
-- vars_only returns multiple values; pass that path through untouched
|
||||||
|
if vars_only then return orig_gen_aut(self, vars_only) end
|
||||||
|
local aut = orig_gen_aut(self)
|
||||||
|
if aut and aut.info and is_joker(self) then
|
||||||
|
dbg('hover joker', self.config.center.key,
|
||||||
|
'in_buy_area=', in_buy_area(self), 'owned=', self.area == G.jokers)
|
||||||
|
if in_buy_area(self)
|
||||||
|
or (self.area == G.jokers and JCA.config.owned_tooltip) then
|
||||||
|
local ok, rows = pcall(tooltip_rows, self)
|
||||||
|
if not ok then
|
||||||
|
dbg('tooltip_rows ERROR:', rows)
|
||||||
|
elseif rows then
|
||||||
|
aut.info[#aut.info + 1] = rows
|
||||||
|
dbg('tooltip appended,', #rows, 'rows')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return aut
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Shop pulse ------------------------------------------------------------------
|
||||||
|
|
||||||
|
local orig_update = Card.update
|
||||||
|
function Card:update(dt)
|
||||||
|
orig_update(self, dt)
|
||||||
|
if JCA.config.pulse and not JCA.config.learning_mode
|
||||||
|
and G.STAGE == G.STAGES.RUN
|
||||||
|
and is_joker(self) and in_buy_area(self) then
|
||||||
|
self.jca_timer = (self.jca_timer or 2.5) + dt
|
||||||
|
if self.jca_timer >= 2.5 then
|
||||||
|
self.jca_timer = 0
|
||||||
|
local ok, rec = pcall(JCA.is_recommended, self)
|
||||||
|
if ok and rec then self:juice_up(0.25, 0.15) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Combo discovery tracking -----------------------------------------------------
|
||||||
|
-- A famous pair counts as discovered the first time both jokers are fielded
|
||||||
|
-- together. All-time discoveries persist in config.discovered; pairs fielded
|
||||||
|
-- during the current run are also recorded in G.GAME.jca_run_combos (saved
|
||||||
|
-- with the run) for end-of-run display.
|
||||||
|
|
||||||
|
function JCA.check_discoveries()
|
||||||
|
if not (G.jokers and G.jokers.cards) then return end
|
||||||
|
local keys = {}
|
||||||
|
for _, c in ipairs(G.jokers.cards) do
|
||||||
|
if is_joker(c) then keys[#keys + 1] = c.config.center.key end
|
||||||
|
end
|
||||||
|
local new_pair
|
||||||
|
for i = 1, #keys do
|
||||||
|
for j = i + 1, #keys do
|
||||||
|
local pk = pair_key(keys[i], keys[j])
|
||||||
|
if JCA.pair_bonus[pk] then
|
||||||
|
if G.GAME then
|
||||||
|
G.GAME.jca_run_combos = G.GAME.jca_run_combos or {}
|
||||||
|
G.GAME.jca_run_combos[pk] = true
|
||||||
|
end
|
||||||
|
if not JCA.config.discovered[pk] then
|
||||||
|
JCA.config.discovered[pk] = true
|
||||||
|
new_pair = new_pair or (name_of(keys[i]) .. ' + ' .. name_of(keys[j]))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if new_pair then
|
||||||
|
if JCA.mod and SMODS.save_mod_config then SMODS.save_mod_config(JCA.mod) end
|
||||||
|
pcall(function()
|
||||||
|
attention_text({
|
||||||
|
text = 'New combo: ' .. new_pair .. '!',
|
||||||
|
scale = 0.7, hold = 4, major = G.play, align = 'cm',
|
||||||
|
offset = {x = 0, y = -3.5}, silent = true,
|
||||||
|
backdrop_colour = G.C.SECONDARY_SET.Tarot,
|
||||||
|
})
|
||||||
|
play_sound('card1', 1.1, 0.6)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if CardArea and type(CardArea) == 'table' then
|
||||||
|
local orig_emplace = CardArea.emplace
|
||||||
|
function CardArea:emplace(...)
|
||||||
|
orig_emplace(self, ...)
|
||||||
|
if self == G.jokers then
|
||||||
|
pcall(JCA.check_discoveries)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Post-run combo recap -----------------------------------------------------------
|
||||||
|
-- Lists the famous pairs fielded during the run (G.GAME.jca_run_combos) on the
|
||||||
|
-- win and game-over screens.
|
||||||
|
|
||||||
|
local function recap_row()
|
||||||
|
local combos = G.GAME and G.GAME.jca_run_combos
|
||||||
|
if not combos or not next(combos) then return nil end
|
||||||
|
local names = {}
|
||||||
|
for pk in pairs(combos) do
|
||||||
|
local a, b = pk:match('^(.-)|(.+)$')
|
||||||
|
if a then names[#names + 1] = name_of(a) .. ' + ' .. name_of(b) end
|
||||||
|
end
|
||||||
|
if #names == 0 then return nil end
|
||||||
|
table.sort(names)
|
||||||
|
local rows = {{n = G.UIT.R, config = {align = 'cm', padding = 0.03}, nodes = {
|
||||||
|
{n = G.UIT.T, config = {text = 'Combos fielded this run',
|
||||||
|
colour = G.C.GOLD, scale = 0.35, shadow = true}},
|
||||||
|
}}}
|
||||||
|
for i = 1, math.min(#names, 6) do
|
||||||
|
rows[#rows + 1] = {n = G.UIT.R, config = {align = 'cm', padding = 0.02}, nodes = {
|
||||||
|
{n = G.UIT.T, config = {text = names[i],
|
||||||
|
colour = G.C.WHITE, scale = 0.3, shadow = true}},
|
||||||
|
}}
|
||||||
|
end
|
||||||
|
if #names > 6 then
|
||||||
|
rows[#rows + 1] = {n = G.UIT.R, config = {align = 'cm', padding = 0.02}, nodes = {
|
||||||
|
{n = G.UIT.T, config = {text = '+' .. (#names - 6) .. ' more',
|
||||||
|
colour = G.C.UI.TEXT_LIGHT, scale = 0.3}},
|
||||||
|
}}
|
||||||
|
end
|
||||||
|
return {n = G.UIT.R, config = {align = 'cm', padding = 0.06}, nodes = rows}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Trail of {node, index} pairs from `node` down to the child carrying the id,
|
||||||
|
-- so callers can insert siblings at any ancestor level.
|
||||||
|
local function find_trail(node, id, trail)
|
||||||
|
trail = trail or {}
|
||||||
|
for i, child in ipairs(node.nodes or {}) do
|
||||||
|
trail[#trail + 1] = {node = node, index = i}
|
||||||
|
if child.config and child.config.id == id then return trail end
|
||||||
|
local hit = find_trail(child, id, trail)
|
||||||
|
if hit then return hit end
|
||||||
|
trail[#trail] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Insert the recap `up` ancestor levels above the anchor's parent, before
|
||||||
|
-- (offset 0) or after (offset 1) that branch. Returns true on success.
|
||||||
|
local function insert_recap(ui, id, up, offset)
|
||||||
|
local row = recap_row()
|
||||||
|
if not row then return end
|
||||||
|
local trail = find_trail(ui, id)
|
||||||
|
if not trail or #trail <= up then return end
|
||||||
|
local spot = trail[#trail - up]
|
||||||
|
table.insert(spot.node.nodes, spot.index + offset, row)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Game over: 'from_game_over' sits in the buttons row of the central column;
|
||||||
|
-- one level up places the recap between the stats box and the buttons.
|
||||||
|
if create_UIBox_game_over then
|
||||||
|
local orig_game_over = create_UIBox_game_over
|
||||||
|
function create_UIBox_game_over(...)
|
||||||
|
local ui = orig_game_over(...)
|
||||||
|
pcall(insert_recap, ui, 'from_game_over', 1, 0)
|
||||||
|
return ui
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 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
|
||||||
|
-- 'from_game_won' when the CTA variant of the screen is shown.
|
||||||
|
if create_UIBox_win then
|
||||||
|
local orig_win = create_UIBox_win
|
||||||
|
function create_UIBox_win(...)
|
||||||
|
local ui = orig_win(...)
|
||||||
|
local _, inserted = pcall(insert_recap, ui, 'from_game_won', 2, 1)
|
||||||
|
if not inserted then pcall(insert_recap, ui, 'win_cta', 2, 1) end
|
||||||
|
return ui
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Synergy catalog (Mods menu > Combo Advisor > Combos/Engine/Economy/Hands) --
|
||||||
|
-- Generated from the database at menu time so it can never drift from the
|
||||||
|
-- scores the advisor actually uses.
|
||||||
|
|
||||||
|
local THEME_TABS = {
|
||||||
|
{ label = 'Engine', themes = {
|
||||||
|
{tag = 'faces', name = 'Face cards'},
|
||||||
|
{tag = 'retrig_scoring', name = 'Retrigger played cards'},
|
||||||
|
{tag = 'retrig_held', name = 'Retrigger held cards'},
|
||||||
|
{tag = 'copy_target', name = 'Copy targets (Blueprint/Brainstorm)'},
|
||||||
|
{tag = 'probability', name = 'Probability'},
|
||||||
|
{tag = 'hand_size', name = 'Hand size'},
|
||||||
|
{tag = 'hands_up', name = 'Extra hands'},
|
||||||
|
{tag = 'discards_up', name = 'Discards'},
|
||||||
|
}},
|
||||||
|
{ label = 'Economy', themes = {
|
||||||
|
{tag = 'money', name = 'Money'},
|
||||||
|
{tag = 'sell_value', name = 'Sell value'},
|
||||||
|
{tag = 'joker_gen', name = 'Joker creation'},
|
||||||
|
{tag = 'tarot_gen', name = 'Tarot cards'},
|
||||||
|
{tag = 'planet', name = 'Planet cards'},
|
||||||
|
{tag = 'spectral_gen', name = 'Spectral cards'},
|
||||||
|
{tag = 'gold_gen', name = 'Gold cards'},
|
||||||
|
{tag = 'stone_gen', name = 'Stone cards'},
|
||||||
|
{tag = 'enhance_gen', name = 'Card enhancing'},
|
||||||
|
{tag = 'card_gen', name = 'Deck additions'},
|
||||||
|
}},
|
||||||
|
{ label = 'Hands', themes = {
|
||||||
|
{tag = 'pair', name = 'Pairs'},
|
||||||
|
{tag = 'two_pair', name = 'Two Pair'},
|
||||||
|
{tag = 'three_kind', name = 'Three of a Kind'},
|
||||||
|
{tag = 'four_kind', name = 'Four of a Kind'},
|
||||||
|
{tag = 'straight', name = 'Straights'},
|
||||||
|
{tag = 'flush', name = 'Flushes'},
|
||||||
|
{tag = 'suit_flex', name = 'Suit conversion'},
|
||||||
|
{tag = 'hearts', name = 'Hearts'},
|
||||||
|
{tag = 'diamonds', name = 'Diamonds'},
|
||||||
|
{tag = 'spades', name = 'Spades'},
|
||||||
|
{tag = 'clubs', name = 'Clubs'},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Joker keys for a tag, split into pure sources (give only), pure payoffs
|
||||||
|
-- (want only), and cluster members (both). Sorted by display name.
|
||||||
|
local function theme_members(tag)
|
||||||
|
local sources, payoffs, both = {}, {}, {}
|
||||||
|
for key, entry in pairs(JCA.db) do
|
||||||
|
local g, w = entry.gives[tag], entry.wants[tag]
|
||||||
|
if g and w then both[#both + 1] = key
|
||||||
|
elseif g then sources[#sources + 1] = key
|
||||||
|
elseif w then payoffs[#payoffs + 1] = key
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local by_name = function(a, b) return name_of(a) < name_of(b) end
|
||||||
|
table.sort(sources, by_name); table.sort(payoffs, by_name); table.sort(both, by_name)
|
||||||
|
return sources, payoffs, both
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Card display: real Card objects in CardAreas (like the Collection), so
|
||||||
|
-- hovering shows each joker's own description tooltip. Areas and cards are
|
||||||
|
-- built fresh on every tab/page build and are cleaned up with the UIBox, so
|
||||||
|
-- none of these node trees may be cached.
|
||||||
|
|
||||||
|
local CARDS_PER_ROW = 8
|
||||||
|
|
||||||
|
-- Undiscovered jokers show face-down (vanilla spoiler protection), unless
|
||||||
|
-- the player has pressed Unlock All in the settings. That button already
|
||||||
|
-- marks every center discovered, but the profile flag also covers centers
|
||||||
|
-- added later (e.g. new mods); pages rebuild on every open, so checking the
|
||||||
|
-- flag at build time reacts to Unlock All without any event hook.
|
||||||
|
local function reveal_params()
|
||||||
|
local profile = G.PROFILES and G.SETTINGS and G.PROFILES[G.SETTINGS.profile]
|
||||||
|
if profile and profile.all_unlocked then
|
||||||
|
return {bypass_discovery_center = true, bypass_discovery_ui = true}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- `face_down` renders the cards back-up (used for unfielded famous pairs);
|
||||||
|
-- face-down cards also show no hover tooltip, keeping the mystery.
|
||||||
|
local function card_row_node(keys, scale, face_down)
|
||||||
|
local area = CardArea(
|
||||||
|
G.ROOM.T.x + 0.2 * G.ROOM.T.w / 2, G.ROOM.T.h,
|
||||||
|
(scale * #keys + 0.25) * G.CARD_W,
|
||||||
|
scale * G.CARD_H,
|
||||||
|
{card_limit = #keys, type = 'title', highlight_limit = 0, collection = true})
|
||||||
|
local reveal = reveal_params()
|
||||||
|
for _, key in ipairs(keys) do
|
||||||
|
local center = G.P_CENTERS[key]
|
||||||
|
if center then
|
||||||
|
local card = Card(area.T.x + area.T.w / 2, area.T.y,
|
||||||
|
G.CARD_W * scale, G.CARD_H * scale, G.P_CARDS.empty, center, reveal)
|
||||||
|
if face_down then
|
||||||
|
card.facing = 'back'
|
||||||
|
card.sprite_facing = 'back'
|
||||||
|
end
|
||||||
|
area:emplace(card)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return {n = G.UIT.O, config = {object = area}}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function add_caption(nodes, str)
|
||||||
|
nodes[#nodes + 1] = {n = G.UIT.R, config = {align = 'cm', padding = 0.02}, nodes = {
|
||||||
|
{n = G.UIT.T, config = {text = str, colour = G.C.GOLD, scale = 0.3}},
|
||||||
|
}}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Word-wrap a plain sentence for small caption rows.
|
||||||
|
local function wrap_plain(s, width)
|
||||||
|
local lines, line = {}, nil
|
||||||
|
for word in s:gmatch('%S+') do
|
||||||
|
local nxt = line and (line .. ' ' .. word) or word
|
||||||
|
if line and #nxt > width then
|
||||||
|
lines[#lines + 1] = line
|
||||||
|
line = word
|
||||||
|
else
|
||||||
|
line = nxt
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if line then lines[#lines + 1] = line end
|
||||||
|
return lines
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Explainer sentence rendered as small light rows under a caption.
|
||||||
|
local function add_info_rows(nodes, sentence, width, scale)
|
||||||
|
for _, l in ipairs(wrap_plain(sentence or '', width)) do
|
||||||
|
nodes[#nodes + 1] = {n = G.UIT.R, config = {align = 'cm', padding = 0.01}, nodes = {
|
||||||
|
{n = G.UIT.T, config = {text = l, colour = G.C.UI.TEXT_LIGHT, scale = scale}},
|
||||||
|
}}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function add_card_rows(nodes, keys, scale, per_row)
|
||||||
|
per_row = per_row or CARDS_PER_ROW
|
||||||
|
for i = 1, #keys, per_row do
|
||||||
|
local row = {}
|
||||||
|
for j = i, math.min(i + per_row - 1, #keys) do
|
||||||
|
row[#row + 1] = keys[j]
|
||||||
|
end
|
||||||
|
nodes[#nodes + 1] = {n = G.UIT.R,
|
||||||
|
config = {align = 'cm', padding = 0.05, no_fill = true},
|
||||||
|
nodes = {card_row_node(row, scale)}}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Card layouts from largest to most compact; all stay under ~8.4 units wide.
|
||||||
|
-- A page uses the largest layout whose estimated height fits the menu box,
|
||||||
|
-- so dense themes (Money, Retriggers) shrink instead of overflowing.
|
||||||
|
local CARD_LAYOUTS = {
|
||||||
|
{scale = 0.68, per_row = 8},
|
||||||
|
{scale = 0.60, per_row = 9},
|
||||||
|
{scale = 0.52, per_row = 11},
|
||||||
|
{scale = 0.44, per_row = 13},
|
||||||
|
}
|
||||||
|
local PAGE_HEIGHT_BUDGET = 5.5
|
||||||
|
|
||||||
|
local function pick_layout(groups, text_rows)
|
||||||
|
for _, layout in ipairs(CARD_LAYOUTS) do
|
||||||
|
local card_rows = 0
|
||||||
|
for _, keys in ipairs(groups) do
|
||||||
|
card_rows = card_rows + math.ceil(#keys / layout.per_row)
|
||||||
|
end
|
||||||
|
local est = text_rows * 0.33 + 0.6 -- captions/blurb + pager
|
||||||
|
+ card_rows * (layout.scale * G.CARD_H + 0.1)
|
||||||
|
if est <= PAGE_HEIGHT_BUDGET then return layout end
|
||||||
|
end
|
||||||
|
return CARD_LAYOUTS[#CARD_LAYOUTS]
|
||||||
|
end
|
||||||
|
|
||||||
|
local function pager_row(options, current, callback)
|
||||||
|
return {n = G.UIT.R, config = {align = 'cm', padding = 0.05}, nodes = {
|
||||||
|
create_option_cycle{
|
||||||
|
options = options, current_option = current, w = 4.5,
|
||||||
|
cycle_shoulders = true, no_pips = true, colour = G.C.RED,
|
||||||
|
opt_callback = callback,
|
||||||
|
focus_args = {snap_to = true, nav = 'wide'},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- One theme per page; the pager is labelled with theme names.
|
||||||
|
local function theme_tab_def(group, page_no)
|
||||||
|
page_no = math.min(page_no or 1, #group.themes)
|
||||||
|
local theme = group.themes[page_no]
|
||||||
|
local sources, payoffs, both = theme_members(theme.tag)
|
||||||
|
|
||||||
|
-- A tiny cluster list (e.g. To the Moon under Money) folds into the
|
||||||
|
-- sources/payoffs lists instead of costing its own row.
|
||||||
|
if #both > 0 and #both <= 2 and (#sources > 0 or #payoffs > 0) then
|
||||||
|
for _, k in ipairs(both) do
|
||||||
|
if #sources > 0 then sources[#sources + 1] = k end
|
||||||
|
if #payoffs > 0 then payoffs[#payoffs + 1] = k end
|
||||||
|
end
|
||||||
|
both = {}
|
||||||
|
local by_name = function(a, b) return name_of(a) < name_of(b) end
|
||||||
|
table.sort(sources, by_name)
|
||||||
|
table.sort(payoffs, by_name)
|
||||||
|
end
|
||||||
|
|
||||||
|
local blurb_lines = wrap_plain(data.theme_info[theme.tag] or '', 70)
|
||||||
|
local groups, captions = {}, 0
|
||||||
|
if #both > 0 then groups[#groups + 1] = both end
|
||||||
|
if #sources > 0 then groups[#groups + 1] = sources; captions = captions + 1 end
|
||||||
|
if #payoffs > 0 then groups[#groups + 1] = payoffs; captions = captions + 1 end
|
||||||
|
local layout = pick_layout(groups, 1 + #blurb_lines + captions)
|
||||||
|
|
||||||
|
local nodes = {}
|
||||||
|
add_caption(nodes, theme.name)
|
||||||
|
add_info_rows(nodes, data.theme_info[theme.tag], 70, 0.26)
|
||||||
|
if #both > 0 then
|
||||||
|
add_card_rows(nodes, both, layout.scale, layout.per_row)
|
||||||
|
end
|
||||||
|
if #sources > 0 then
|
||||||
|
add_caption(nodes, 'Sources - these provide it')
|
||||||
|
add_card_rows(nodes, sources, layout.scale, layout.per_row)
|
||||||
|
end
|
||||||
|
if #payoffs > 0 then
|
||||||
|
add_caption(nodes, 'Payoffs - these want it')
|
||||||
|
add_card_rows(nodes, payoffs, layout.scale, layout.per_row)
|
||||||
|
end
|
||||||
|
local options = {}
|
||||||
|
for i, t in ipairs(group.themes) do options[i] = t.name end
|
||||||
|
nodes[#nodes + 1] = pager_row(options, page_no, 'jca_page_' .. group.label)
|
||||||
|
return {n = G.UIT.ROOT, config = {align = 'tm', padding = 0.05,
|
||||||
|
colour = G.C.CLEAR, minw = 8.5, minh = 5.6}, nodes = nodes}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Famous pairs as side-by-side card duos, six pairs per page.
|
||||||
|
local PAIRS_PER_PAGE = 6
|
||||||
|
local sorted_pairs
|
||||||
|
local function combos_tab_def(page_no)
|
||||||
|
if not sorted_pairs then
|
||||||
|
sorted_pairs = {}
|
||||||
|
for _, p in ipairs(JCA.pair_list) do
|
||||||
|
sorted_pairs[#sorted_pairs + 1] = {p[1], p[2], score = JCA.score(p[1], p[2])}
|
||||||
|
end
|
||||||
|
table.sort(sorted_pairs, function(a, b)
|
||||||
|
if a.score ~= b.score then return a.score > b.score end
|
||||||
|
return name_of(a[1]) < name_of(b[1])
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
local pages = math.ceil(#sorted_pairs / PAIRS_PER_PAGE)
|
||||||
|
page_no = math.min(page_no or 1, pages)
|
||||||
|
local nodes = {}
|
||||||
|
local found = 0
|
||||||
|
for _, p in ipairs(sorted_pairs) do
|
||||||
|
if JCA.config.discovered[pair_key(p[1], p[2])] then found = found + 1 end
|
||||||
|
end
|
||||||
|
add_caption(nodes, ('Famous pairs - fielded %d of %d (face-down pairs are undiscovered)')
|
||||||
|
:format(found, #sorted_pairs))
|
||||||
|
local first = (page_no - 1) * PAIRS_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 pair = sorted_pairs[first + r * 3 + c]
|
||||||
|
if pair then
|
||||||
|
local pk = pair_key(pair[1], pair[2])
|
||||||
|
local fielded = JCA.config.discovered[pk]
|
||||||
|
-- blurb caption once fielded; '???' keeps the mystery until then
|
||||||
|
local col = {{n = G.UIT.R, config = {align = 'cm'},
|
||||||
|
nodes = {card_row_node({pair[1], pair[2]}, 0.62, not fielded)}}}
|
||||||
|
local cap = fielded and wrap_plain(JCA.pair_blurb[pk] or '', 30) or {'???'}
|
||||||
|
for i = 1, math.min(#cap, 2) 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.UI.TEXT_LIGHT, scale = 0.22}},
|
||||||
|
}}
|
||||||
|
end
|
||||||
|
row.nodes[#row.nodes + 1] = {n = G.UIT.C,
|
||||||
|
config = {align = 'cm', padding = 0.08}, nodes = col}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if #row.nodes > 0 then nodes[#nodes + 1] = row end
|
||||||
|
end
|
||||||
|
if pages > 1 then
|
||||||
|
local options = {}
|
||||||
|
for i = 1, pages do options[i] = 'Page ' .. i .. '/' .. pages end
|
||||||
|
nodes[#nodes + 1] = pager_row(options, page_no, 'jca_page_Combos')
|
||||||
|
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
|
||||||
|
|
||||||
|
-- Page cyclers swap the tab body in place (same pattern SMODS uses for its
|
||||||
|
-- achievements tab).
|
||||||
|
local function register_pager(name, def_fn)
|
||||||
|
G.FUNCS['jca_page_' .. name] = function(args)
|
||||||
|
if not args or not args.cycle_config then return end
|
||||||
|
local tab_contents = G.OVERLAY_MENU:get_UIE_by_ID('tab_contents')
|
||||||
|
tab_contents.config.object:remove()
|
||||||
|
tab_contents.config.object = UIBox{
|
||||||
|
definition = def_fn(args.cycle_config.current_option),
|
||||||
|
config = {offset = {x = 0, y = 0}, parent = tab_contents, type = 'cm'},
|
||||||
|
}
|
||||||
|
tab_contents.UIBox:recalculate()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
register_pager('Combos', combos_tab_def)
|
||||||
|
for _, group in ipairs(THEME_TABS) do
|
||||||
|
register_pager(group.label, function(p) return theme_tab_def(group, p) end)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function catalog_tabs()
|
||||||
|
local tabs = {{
|
||||||
|
label = 'Combos',
|
||||||
|
chosen = true,
|
||||||
|
tab_definition_function = function() return combos_tab_def(1) end,
|
||||||
|
}}
|
||||||
|
for _, t in ipairs(THEME_TABS) do
|
||||||
|
tabs[#tabs + 1] = {
|
||||||
|
label = t.label,
|
||||||
|
tab_definition_function = function() return theme_tab_def(t, 1) end,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
return tabs
|
||||||
|
end
|
||||||
|
|
||||||
|
if SMODS.current_mod then
|
||||||
|
SMODS.current_mod.extra_tabs = catalog_tabs
|
||||||
|
end
|
||||||
|
|
||||||
|
-- In-run "Combos" HUD button ---------------------------------------------------
|
||||||
|
-- Adds a third button under Run Info / Options that opens the synergy catalog
|
||||||
|
-- in an overlay. The overlay reuses catalog_tabs(); the page cyclers work
|
||||||
|
-- unchanged because vanilla create_tabs also names its body 'tab_contents'.
|
||||||
|
|
||||||
|
G.FUNCS.jca_open_catalog = function()
|
||||||
|
G.FUNCS.overlay_menu{definition = create_UIBox_generic_options({
|
||||||
|
back_func = 'exit_overlay_menu',
|
||||||
|
contents = {
|
||||||
|
{n = G.UIT.R, config = {align = 'tm', padding = 0}, nodes = {
|
||||||
|
create_tabs({
|
||||||
|
tabs = catalog_tabs(),
|
||||||
|
snap_to_nav = true,
|
||||||
|
colour = G.C.BOOSTER,
|
||||||
|
}),
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function find_by_id(node, id)
|
||||||
|
if node.config and node.config.id == id then return node end
|
||||||
|
for _, child in ipairs(node.nodes or {}) do
|
||||||
|
local hit = find_by_id(child, id)
|
||||||
|
if hit then return hit end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if create_UIBox_HUD then
|
||||||
|
local orig_hud = create_UIBox_HUD
|
||||||
|
function create_UIBox_HUD(...)
|
||||||
|
local hud = orig_hud(...)
|
||||||
|
local ok, area = pcall(find_by_id, hud, 'button_area')
|
||||||
|
if ok and area and area.nodes then
|
||||||
|
area.nodes[#area.nodes + 1] =
|
||||||
|
{n = G.UIT.R, config = {align = 'cm', minh = 0.9, minw = 1.5,
|
||||||
|
padding = 0.05, r = 0.1, hover = true, colour = G.C.PURPLE,
|
||||||
|
button = 'jca_open_catalog', shadow = true}, nodes = {
|
||||||
|
{n = G.UIT.C, config = {align = 'cm', maxw = 1.4}, nodes = {
|
||||||
|
{n = G.UIT.T, config = {text = 'Combos', scale = 0.38,
|
||||||
|
colour = G.C.UI.TEXT_LIGHT, shadow = true}},
|
||||||
|
}},
|
||||||
|
}}
|
||||||
|
end
|
||||||
|
return hud
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Config tab (Mods menu > Combo Advisor > Config) -----------------------------
|
||||||
|
|
||||||
|
JCA.THRESHOLD_OPTIONS = {2, 3, 4, 5, 6, 8}
|
||||||
|
|
||||||
|
G.FUNCS.jca_set_threshold = function(args)
|
||||||
|
JCA.config.threshold = args.to_val
|
||||||
|
end
|
||||||
|
|
||||||
|
local db_count = 0
|
||||||
|
for _ in pairs(JCA.db) do db_count = db_count + 1 end
|
||||||
|
dbg('loaded;', db_count, 'jokers in db; config threshold =', JCA.config.threshold)
|
||||||
|
|
||||||
|
if SMODS.current_mod then
|
||||||
|
SMODS.current_mod.config_tab = function()
|
||||||
|
local current = 3 -- index of default (4) in THRESHOLD_OPTIONS
|
||||||
|
for i, v in ipairs(JCA.THRESHOLD_OPTIONS) do
|
||||||
|
if v == JCA.config.threshold then current = i end
|
||||||
|
end
|
||||||
|
return {n = G.UIT.ROOT, config = {align = 'cm', padding = 0.1, colour = G.C.CLEAR}, nodes = {
|
||||||
|
{n = G.UIT.R, config = {align = 'cm', padding = 0.05}, nodes = {
|
||||||
|
create_toggle{
|
||||||
|
label = 'Pulse recommended jokers in shop',
|
||||||
|
ref_table = JCA.config, ref_value = 'pulse',
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
{n = G.UIT.R, config = {align = 'cm', padding = 0.05}, nodes = {
|
||||||
|
create_toggle{
|
||||||
|
label = 'Show combo tooltip on owned jokers',
|
||||||
|
ref_table = JCA.config, ref_value = 'owned_tooltip',
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
{n = G.UIT.R, config = {align = 'cm', padding = 0.05}, nodes = {
|
||||||
|
create_toggle{
|
||||||
|
label = 'Learning mode (no verdicts, just reasons)',
|
||||||
|
ref_table = JCA.config, ref_value = 'learning_mode',
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
{n = G.UIT.R, config = {align = 'cm', padding = 0.05}, nodes = {
|
||||||
|
create_option_cycle{
|
||||||
|
label = 'Recommendation threshold (lower = more picks)',
|
||||||
|
scale = 0.8, w = 4,
|
||||||
|
options = JCA.THRESHOLD_OPTIONS,
|
||||||
|
current_option = current,
|
||||||
|
opt_callback = 'jca_set_threshold',
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
}}
|
||||||
|
end
|
||||||
|
end
|
||||||
+308
@@ -0,0 +1,308 @@
|
|||||||
|
--- Synergy database for Joker Combo Advisor.
|
||||||
|
--- Each joker lists tags it GIVES (capabilities it adds to a build) and tags
|
||||||
|
--- it WANTS (capabilities that make it stronger). The scorer matches one
|
||||||
|
--- joker's gives against another's wants, in both directions.
|
||||||
|
---
|
||||||
|
--- Tag glossary:
|
||||||
|
--- mult / chips / xmult engine roles (also used for the generic
|
||||||
|
--- "scaler + multiplier" complement rule)
|
||||||
|
--- money earns money; money = wants money (econ payoffs)
|
||||||
|
--- faces makes cards count as face cards / wants face cards
|
||||||
|
--- retrig_scoring retriggers (or multiplies triggers of) played cards
|
||||||
|
--- retrig_held retriggers cards held in hand
|
||||||
|
--- hand_size / hands_up / discards_up resource increases
|
||||||
|
--- probability improves listed probabilities / cares about them
|
||||||
|
--- tarot_gen / spectral_gen / planet consumable economies
|
||||||
|
--- stone_gen / gold_gen / enhance_gen / card_gen deck modification
|
||||||
|
--- joker_gen / sell_value sell-fodder economies
|
||||||
|
--- copy_target a strong effect worth copying (for Blueprint et al.)
|
||||||
|
--- pair, two_pair, three_kind, four_kind, straight, flush hand-type
|
||||||
|
--- clusters (jokers keying the same hand reinforce
|
||||||
|
--- each other; enablers like Four Fingers give these)
|
||||||
|
--- hearts, diamonds, spades, clubs, suit_flex suit clusters
|
||||||
|
---
|
||||||
|
--- Jokers not in this table (e.g. from other mods) simply score 0.
|
||||||
|
|
||||||
|
local J = {}
|
||||||
|
local function j(key, gives, wants)
|
||||||
|
J[key] = { gives = gives or {}, wants = wants or {} }
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Common jokers ------------------------------------------------------------
|
||||||
|
j('j_joker', {'mult'})
|
||||||
|
j('j_greedy_joker', {'mult','diamonds'}, {'diamonds','suit_flex'})
|
||||||
|
j('j_lusty_joker', {'mult','hearts'}, {'hearts','suit_flex'})
|
||||||
|
j('j_wrathful_joker', {'mult','spades'}, {'spades','suit_flex'})
|
||||||
|
j('j_gluttenous_joker', {'mult','clubs'}, {'clubs','suit_flex'})
|
||||||
|
j('j_jolly', {'mult','pair'}, {'pair'})
|
||||||
|
j('j_zany', {'mult','three_kind'}, {'three_kind'})
|
||||||
|
j('j_mad', {'mult','two_pair'}, {'two_pair'})
|
||||||
|
j('j_crazy', {'mult','straight'}, {'straight'})
|
||||||
|
j('j_droll', {'mult','flush'}, {'flush'})
|
||||||
|
j('j_sly', {'chips','pair'}, {'pair'})
|
||||||
|
j('j_wily', {'chips','three_kind'}, {'three_kind'})
|
||||||
|
j('j_clever', {'chips','two_pair'}, {'two_pair'})
|
||||||
|
j('j_devious', {'chips','straight'}, {'straight'})
|
||||||
|
j('j_crafty', {'chips','flush'}, {'flush'})
|
||||||
|
j('j_half', {'mult'})
|
||||||
|
j('j_credit_card', {})
|
||||||
|
j('j_banner', {'chips'}, {'discards_up'})
|
||||||
|
j('j_mystic_summit', {'mult'})
|
||||||
|
j('j_8_ball', {'tarot_gen'}, {'probability'})
|
||||||
|
j('j_misprint', {'mult'})
|
||||||
|
j('j_raised_fist', {'mult'}, {'retrig_held','hand_size'})
|
||||||
|
j('j_chaos', {})
|
||||||
|
j('j_scary_face', {'chips'}, {'faces','retrig_scoring'})
|
||||||
|
j('j_abstract', {'mult'})
|
||||||
|
j('j_delayed_grat', {'money'})
|
||||||
|
j('j_gros_michel', {'mult'})
|
||||||
|
j('j_even_steven', {'mult'}, {'retrig_scoring'})
|
||||||
|
j('j_odd_todd', {'chips'}, {'retrig_scoring'})
|
||||||
|
j('j_scholar', {'chips','mult'}, {'retrig_scoring'})
|
||||||
|
j('j_business', {'money'}, {'faces','probability','retrig_scoring'})
|
||||||
|
j('j_supernova', {'mult'})
|
||||||
|
j('j_ride_the_bus', {'mult'})
|
||||||
|
j('j_egg', {'sell_value'})
|
||||||
|
j('j_runner', {'chips','straight'}, {'straight'})
|
||||||
|
j('j_ice_cream', {'chips'})
|
||||||
|
j('j_splash', {'retrig_scoring'}) -- makes every played card score
|
||||||
|
j('j_blue_joker', {'chips'})
|
||||||
|
j('j_faceless', {'money'}, {'faces','discards_up'})
|
||||||
|
j('j_green_joker', {'mult'})
|
||||||
|
j('j_superposition', {'tarot_gen','straight'}, {'straight'})
|
||||||
|
j('j_todo_list', {'money'})
|
||||||
|
j('j_cavendish', {'xmult'})
|
||||||
|
j('j_red_card', {'mult'})
|
||||||
|
j('j_square', {'chips'})
|
||||||
|
j('j_riff_raff', {'joker_gen'})
|
||||||
|
j('j_shortcut', {'straight'})
|
||||||
|
j('j_vagabond', {'tarot_gen'})
|
||||||
|
j('j_cloud_9', {'money'})
|
||||||
|
j('j_rocket', {'money'})
|
||||||
|
j('j_luchador', {})
|
||||||
|
j('j_gift', {'sell_value'})
|
||||||
|
j('j_turtle_bean', {'hand_size'})
|
||||||
|
j('j_erosion', {'mult'})
|
||||||
|
j('j_reserved_parking', {'money'}, {'faces','retrig_held','probability','hand_size'})
|
||||||
|
j('j_mail', {'money'}, {'discards_up'})
|
||||||
|
j('j_to_the_moon', {'money'}, {'money'})
|
||||||
|
j('j_hallucination', {'tarot_gen'}, {'probability'})
|
||||||
|
j('j_fortune_teller', {'mult'}, {'tarot_gen'})
|
||||||
|
j('j_juggler', {'hand_size'})
|
||||||
|
j('j_drunkard', {'discards_up'})
|
||||||
|
j('j_golden', {'money'})
|
||||||
|
j('j_popcorn', {'mult'})
|
||||||
|
j('j_walkie_talkie', {'chips','mult'}, {'retrig_scoring'})
|
||||||
|
j('j_smiley', {'mult'}, {'faces'})
|
||||||
|
j('j_ticket', {'money'}, {'gold_gen'})
|
||||||
|
j('j_swashbuckler', {'mult'}, {'sell_value','joker_gen'})
|
||||||
|
j('j_hanging_chad', {'retrig_scoring'})
|
||||||
|
j('j_shoot_the_moon', {'mult'}, {'retrig_held','hand_size'})
|
||||||
|
|
||||||
|
-- Uncommon jokers ----------------------------------------------------------
|
||||||
|
j('j_stencil', {'xmult'})
|
||||||
|
j('j_four_fingers', {'straight','flush'})
|
||||||
|
j('j_mime', {'retrig_held','copy_target'})
|
||||||
|
j('j_ceremonial', {'mult'})
|
||||||
|
j('j_marble', {'stone_gen','card_gen','enhance_gen'})
|
||||||
|
j('j_loyalty_card', {'xmult'})
|
||||||
|
j('j_dusk', {'retrig_scoring'})
|
||||||
|
j('j_fibonacci', {'mult'}, {'retrig_scoring'})
|
||||||
|
j('j_steel_joker', {'xmult'})
|
||||||
|
j('j_hack', {'retrig_scoring'})
|
||||||
|
j('j_pareidolia', {'faces'})
|
||||||
|
j('j_space', {'hand_level'}, {'probability'})
|
||||||
|
j('j_burglar', {'hands_up'})
|
||||||
|
j('j_blackboard', {'xmult'}, {'spades','clubs'})
|
||||||
|
j('j_sixth_sense', {'spectral_gen'}, {'spectral_gen'})
|
||||||
|
j('j_constellation', {'xmult','planet'}, {'planet'})
|
||||||
|
j('j_hiker', {'chips'}, {'retrig_scoring'})
|
||||||
|
j('j_card_sharp', {'xmult'})
|
||||||
|
j('j_madness', {'xmult','copy_target'})
|
||||||
|
j('j_seance', {'spectral_gen'}, {'straight','flush','spectral_gen'})
|
||||||
|
j('j_vampire', {'xmult'}, {'enhance_gen'})
|
||||||
|
j('j_hologram', {'xmult','copy_target'}, {'card_gen'})
|
||||||
|
j('j_cartomancer', {'tarot_gen'})
|
||||||
|
j('j_astronomer', {'planet'}, {'planet'})
|
||||||
|
j('j_burnt', {'hand_level'}, {'discards_up'})
|
||||||
|
j('j_bootstraps', {'mult'}, {'money'})
|
||||||
|
j('j_dna', {'card_gen'})
|
||||||
|
j('j_midas_mask', {'gold_gen','enhance_gen'}, {'faces'})
|
||||||
|
j('j_photograph', {'xmult','copy_target'}, {'faces','retrig_scoring'})
|
||||||
|
j('j_stone', {'chips'}, {'stone_gen'})
|
||||||
|
j('j_lucky_cat', {'xmult'}, {'probability'})
|
||||||
|
j('j_baseball', {'xmult'})
|
||||||
|
j('j_bull', {'chips'}, {'money'})
|
||||||
|
j('j_diet_cola', {})
|
||||||
|
j('j_trading', {'money'}, {'discards_up'})
|
||||||
|
j('j_flash', {'mult'})
|
||||||
|
j('j_trousers', {'mult','two_pair'}, {'two_pair'})
|
||||||
|
j('j_ramen', {'xmult'})
|
||||||
|
j('j_selzer', {'retrig_scoring'})
|
||||||
|
j('j_castle', {'chips'}, {'discards_up'})
|
||||||
|
j('j_mr_bones', {})
|
||||||
|
j('j_acrobat', {'xmult'}, {'hands_up'})
|
||||||
|
j('j_sock_and_buskin', {'retrig_scoring'}, {'faces'})
|
||||||
|
j('j_troubadour', {'hand_size'})
|
||||||
|
j('j_certificate', {'card_gen'})
|
||||||
|
j('j_smeared', {'flush','suit_flex'})
|
||||||
|
j('j_throwback', {'xmult'})
|
||||||
|
j('j_rough_gem', {'money','diamonds'}, {'retrig_scoring','diamonds','suit_flex'})
|
||||||
|
j('j_bloodstone', {'xmult','hearts'}, {'retrig_scoring','hearts','suit_flex','probability'})
|
||||||
|
j('j_arrowhead', {'chips','spades'}, {'retrig_scoring','spades','suit_flex'})
|
||||||
|
j('j_onyx_agate', {'mult','clubs'}, {'retrig_scoring','clubs','suit_flex'})
|
||||||
|
j('j_glass', {'xmult'})
|
||||||
|
j('j_ring_master', {})
|
||||||
|
j('j_flower_pot', {'xmult'}, {'suit_flex'})
|
||||||
|
j('j_merry_andy', {'discards_up'})
|
||||||
|
j('j_oops', {'probability'})
|
||||||
|
j('j_idol', {'xmult','copy_target'}, {'retrig_scoring'})
|
||||||
|
j('j_seeing_double', {'xmult','clubs'}, {'clubs','suit_flex'})
|
||||||
|
j('j_matador', {'money'})
|
||||||
|
j('j_hit_the_road', {'xmult'}, {'discards_up'})
|
||||||
|
j('j_satellite', {'money','planet'}, {'planet'})
|
||||||
|
|
||||||
|
-- Rare jokers ---------------------------------------------------------------
|
||||||
|
j('j_obelisk', {'xmult'})
|
||||||
|
j('j_ancient', {'xmult'}, {'suit_flex'})
|
||||||
|
j('j_campfire', {'xmult'}, {'joker_gen','sell_value'})
|
||||||
|
j('j_baron', {'xmult','copy_target'}, {'retrig_held','hand_size'})
|
||||||
|
j('j_duo', {'xmult','pair'}, {'pair'})
|
||||||
|
j('j_trio', {'xmult','three_kind'}, {'three_kind'})
|
||||||
|
j('j_family', {'xmult','four_kind'}, {'four_kind'})
|
||||||
|
j('j_order', {'xmult','straight'}, {'straight'})
|
||||||
|
j('j_tribe', {'xmult','flush'}, {'flush'})
|
||||||
|
j('j_stuntman', {'chips'})
|
||||||
|
j('j_invisible', {})
|
||||||
|
j('j_blueprint', {}, {'copy_target'})
|
||||||
|
j('j_brainstorm', {}, {'copy_target'})
|
||||||
|
j('j_drivers_license', {'xmult'}, {'enhance_gen'})
|
||||||
|
j('j_wee', {'chips'}, {'retrig_scoring'})
|
||||||
|
|
||||||
|
-- Legendary jokers ----------------------------------------------------------
|
||||||
|
j('j_caino', {'xmult','copy_target'}, {'faces'})
|
||||||
|
j('j_triboulet', {'xmult','copy_target'}, {'retrig_scoring'})
|
||||||
|
j('j_yorick', {'xmult','copy_target'}, {'discards_up'})
|
||||||
|
j('j_chicot', {})
|
||||||
|
j('j_perkeo', {'tarot_gen'}, {'tarot_gen'})
|
||||||
|
|
||||||
|
-- Famous explicit combos (flat bonus on top of tag matching), each with a
|
||||||
|
-- short "why it works" line shown in the tooltip. Keep blurbs under ~34
|
||||||
|
-- characters so a name + blurb fits on one tooltip row.
|
||||||
|
local PAIRS = {
|
||||||
|
{'j_baron', 'j_mime', 'held Kings trigger twice'},
|
||||||
|
{'j_baron', 'j_shoot_the_moon', 'Kings and Queens pay in hand'},
|
||||||
|
{'j_burglar', 'j_mystic_summit', 'zero discards, max Mult'},
|
||||||
|
{'j_burglar', 'j_acrobat', 'more hands, big final hand'},
|
||||||
|
{'j_hack', 'j_wee', 'retriggered 2s grow Wee fast'},
|
||||||
|
{'j_hanging_chad', 'j_photograph', 'first face scores three times'},
|
||||||
|
{'j_pareidolia', 'j_sock_and_buskin', 'every card is a face to retrigger'},
|
||||||
|
{'j_pareidolia', 'j_photograph', 'any first card is the face'},
|
||||||
|
{'j_pareidolia', 'j_midas_mask', 'every played card turns Gold'},
|
||||||
|
{'j_midas_mask', 'j_ticket', 'Gold cards feed Golden Ticket'},
|
||||||
|
{'j_midas_mask', 'j_vampire', 'eats the new Gold for xMult'},
|
||||||
|
{'j_dna', 'j_hologram', 'copied cards grow Hologram'},
|
||||||
|
{'j_constellation', 'j_astronomer', 'cheap Planets stack xMult'},
|
||||||
|
{'j_fortune_teller','j_cartomancer', 'free Tarots grow the Mult'},
|
||||||
|
{'j_egg', 'j_swashbuckler', 'growing sell value becomes Mult'},
|
||||||
|
{'j_gift', 'j_swashbuckler', 'inflated sell values become Mult'},
|
||||||
|
{'j_four_fingers', 'j_square', 'four-card hands suit them both'},
|
||||||
|
{'j_marble', 'j_stone', 'adds Stones that pay chips'},
|
||||||
|
{'j_oops', 'j_lucky_cat', 'better odds, more Lucky payoffs'},
|
||||||
|
}
|
||||||
|
|
||||||
|
-- How to phrase a tag link in the tooltip, from the partner's perspective:
|
||||||
|
-- `give` when the partner provides the tag, `want` when the partner consumes
|
||||||
|
-- it, `both` for cluster tags where the pair simply shares the theme.
|
||||||
|
-- Every tag that appears in the joker table must have an entry here.
|
||||||
|
local TAG_TEXT = {
|
||||||
|
faces = {give = 'turns cards into faces', want = 'pays off face cards'},
|
||||||
|
retrig_scoring = {give = 'retriggers scored cards', want = 'pays per extra trigger'},
|
||||||
|
retrig_held = {give = 'retriggers cards in hand', want = 'pays off held-card triggers'},
|
||||||
|
hand_size = {give = 'adds hand size', want = 'wants a big hand'},
|
||||||
|
hands_up = {give = 'grants extra hands', want = 'uses the extra hands'},
|
||||||
|
discards_up = {give = 'grants extra discards', want = 'feeds on discards'},
|
||||||
|
probability = {give = 'improves listed odds', want = 'relies on listed odds'},
|
||||||
|
money = {give = 'earns money', want = 'scales with your money'},
|
||||||
|
hand_level = {give = 'levels up poker hands', want = 'rewards hand levels'},
|
||||||
|
tarot_gen = {give = 'creates Tarot cards', want = 'rewards Tarot use'},
|
||||||
|
planet = {both = 'shares the Planet economy'},
|
||||||
|
spectral_gen = {both = 'shares the Spectral theme'},
|
||||||
|
gold_gen = {give = 'creates Gold cards', want = 'pays off Gold cards'},
|
||||||
|
stone_gen = {give = 'adds Stone cards', want = 'pays off Stone cards'},
|
||||||
|
enhance_gen = {give = 'enhances cards', want = 'feeds on enhancements'},
|
||||||
|
card_gen = {give = 'adds cards to the deck', want = 'grows as the deck grows'},
|
||||||
|
joker_gen = {give = 'creates spare Jokers', want = 'profits when Jokers sell'},
|
||||||
|
sell_value = {give = 'raises sell values', want = 'turns sell value into Mult'},
|
||||||
|
copy_target = {give = 'is a prime copy target', want = 'copies your best Joker'},
|
||||||
|
suit_flex = {give = 'makes suits count as others', want = 'cares which suits score'},
|
||||||
|
pair = {both = 'both reward Pairs'},
|
||||||
|
two_pair = {both = 'both reward Two Pair'},
|
||||||
|
three_kind = {both = 'both reward Three of a Kind'},
|
||||||
|
four_kind = {both = 'both reward Four of a Kind'},
|
||||||
|
straight = {both = 'both reward Straights'},
|
||||||
|
flush = {both = 'both reward Flushes'},
|
||||||
|
hearts = {both = 'both love Hearts'},
|
||||||
|
diamonds = {both = 'both love Diamonds'},
|
||||||
|
spades = {both = 'both love Spades'},
|
||||||
|
clubs = {both = 'both love Clubs'},
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Order in which tags are considered when picking the one reason to show;
|
||||||
|
-- most specific mechanics first, broad engine roles last.
|
||||||
|
local TAG_ORDER = {
|
||||||
|
'retrig_held', 'retrig_scoring', 'faces',
|
||||||
|
'gold_gen', 'stone_gen', 'enhance_gen', 'card_gen',
|
||||||
|
'tarot_gen', 'spectral_gen', 'planet',
|
||||||
|
'probability', 'discards_up', 'hands_up', 'hand_size', 'hand_level',
|
||||||
|
'money', 'sell_value', 'joker_gen',
|
||||||
|
'hearts', 'diamonds', 'spades', 'clubs',
|
||||||
|
'pair', 'two_pair', 'three_kind', 'four_kind', 'straight', 'flush',
|
||||||
|
'copy_target', 'suit_flex',
|
||||||
|
}
|
||||||
|
|
||||||
|
-- One-sentence mechanic explainers shown at the top of each catalog theme
|
||||||
|
-- page. Teaching voice: explain how the mechanic works, not what to buy.
|
||||||
|
-- Every tag listed in main.lua's THEME_TABS needs an entry (max ~90 chars).
|
||||||
|
local THEME_INFO = {
|
||||||
|
faces = 'Face cards trigger these effects; makers like Pareidolia turn every card into one.',
|
||||||
|
retrig_scoring = 'Retriggers make each scored card count again, so per-card payoffs double up.',
|
||||||
|
retrig_held = 'Some jokers pay for cards sitting in hand; retriggering the hand pays them again.',
|
||||||
|
copy_target = 'Blueprint and Brainstorm mirror a joker - the stronger the target, the better.',
|
||||||
|
probability = 'Listed "1 in X" odds are dice rolls; odds boosters make gambling jokers hit more.',
|
||||||
|
hand_size = 'A bigger hand holds more Kings, steel and gold - held-card builds love the space.',
|
||||||
|
hands_up = 'Extra hands per round are fuel for jokers that reward or count hands played.',
|
||||||
|
discards_up = 'Extra discards are fuel: several jokers grow or pay out each time you discard.',
|
||||||
|
money = 'Money is an engine too - some jokers earn it, others turn savings into power.',
|
||||||
|
sell_value = 'Sell value can be inflated first, then cashed in as Mult or a big sale.',
|
||||||
|
joker_gen = 'Spare jokers are fodder: sell them for profit or feed them to scaling jokers.',
|
||||||
|
tarot_gen = 'Free Tarot cards fix your deck and feed the jokers that count Tarot use.',
|
||||||
|
planet = 'Planets level up poker hands; this crew makes them cheaper and rewards each one.',
|
||||||
|
spectral_gen = 'Spectral cards warp the deck; these jokers create or lean on that chaos.',
|
||||||
|
gold_gen = 'Gold cards pay while held - creators supply them, payoffs cash them in.',
|
||||||
|
stone_gen = 'Stone cards are pure chips; add them, then pay off the heavier deck.',
|
||||||
|
enhance_gen = 'Enhanced cards (gold, steel, stone...) power the jokers that eat or count them.',
|
||||||
|
card_gen = 'Adding cards grows the deck - and the jokers that scale with its size.',
|
||||||
|
pair = 'These all fire on a Pair, so one played hand triggers the whole set at once.',
|
||||||
|
two_pair = 'Two Pair sets off every one of these together - stack them and replay it.',
|
||||||
|
three_kind = 'Three of a Kind triggers all of these in a single played hand.',
|
||||||
|
four_kind = 'Four of a Kind is rare but sets off this whole crew at once.',
|
||||||
|
straight = 'Straights trigger all of these; enablers make gaps and 4-card runs count.',
|
||||||
|
flush = 'Flushes trigger all of these; enablers make them far easier to assemble.',
|
||||||
|
suit_flex = 'Suit benders make cards count as two suits, unlocking every suit joker.',
|
||||||
|
hearts = 'Hearts pay these jokers when scored - suit benders widen the supply.',
|
||||||
|
diamonds = 'Diamonds pay these jokers when scored - suit benders widen the supply.',
|
||||||
|
spades = 'Spades pay these jokers when scored - suit benders widen the supply.',
|
||||||
|
clubs = 'Clubs pay these jokers when scored - suit benders widen the supply.',
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Convert gives/wants arrays into sets for fast lookup.
|
||||||
|
for _, entry in pairs(J) do
|
||||||
|
local g, w = {}, {}
|
||||||
|
for _, t in ipairs(entry.gives) do g[t] = true end
|
||||||
|
for _, t in ipairs(entry.wants) do w[t] = true end
|
||||||
|
entry.gives, entry.wants = g, w
|
||||||
|
end
|
||||||
|
|
||||||
|
return { jokers = J, pairs = PAIRS, tag_text = TAG_TEXT, tag_order = TAG_ORDER,
|
||||||
|
theme_info = THEME_INFO }
|
||||||
Reference in New Issue
Block a user