38d4f5d1b7
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
8.9 KiB
8.9 KiB
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:
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):
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). - Remote:
https://git.aleshym.co/funman300/JokerComboAdvisor(private, user's Gitea). Pushes authenticate via the repo-local credential helper, which reads the token from~/.config/tea/config.ymlat push time — never copy the token into the repo or the remote URL. Commit per feature.
Architecture
Three-layer split; JCA is the single global namespace:
synergies.lua— data only. Returns{ jokers = {...}, pairs = {...} }. Each of the 150 vanilla jokers getsgives(capabilities it adds) andwants(capabilities that strengthen it) tag lists, converted to sets at the bottom of the file.PAIRSlists 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 (second return value flags when that +1 applied).JCA.partners_for(card)aggregates overG.jokers.cards; total ≥JCA.config.threshold⇒ recommended. The generic +1 counts toward the total once per candidate, not per owned joker — since tag/pair scores are even and the threshold is 4, the generic bonus alone can never trigger a recommendation, so a pulsing card always has a nameable partner. Jokers whose multiplier the generic rule misreads opt out with{no_generic = true}insynergies.lua(Joker Stencil: its ×Mult grows with empty slots, so "+Mult jokers feed it" is backwards). - UI hooks: wraps
Card:generate_UIBox_ability_tableto append a tooltip entry toaut.info(format: array of rows-of-UIT-nodes plus a.namestring — must match what vanillainfo_tip_from_rowsexpects), and wrapsCard:updatefor the ~2.5s pulse on recommended shop cards and the partner highlight (hovering a joker juices its owned partners ~0.9s; partner set cached on the card asjca_hl, dropped when hover ends). All hook bodies are pcall-wrapped so a scoring bug can never crash a run — keep it that way. - Sell advisor:
JCA.weakest_link()(full board + ≥3 jokers + a strict minimum required, else nil) flags the lowest-total owned joker with a red verdict row; suppressed by learning mode, toggled byconfig.sell_advisor. - Public API for other mods:
JCA.register(key, gives, wants, opts),JCA.register_pair(a, b, blurb)(setsJCA._pairs_dirtyso the Combos tab re-sorts),JCA.register_clash(a, b, warning). Unknown tags are dropped with a log line, never an error. - Config tab: registered on
SMODS.current_mod.config_tab, guarded byif SMODS.current_modso standalone tests don't need full stubs. - Synergy catalog:
SMODS.current_mod.extra_tabsadds Combos/Engine/ Economy/Hands tabs (plus a text-only Progress tab with discovery completion bars) that render realCardobjects inCardAreas (collection-style, so hover shows each joker's own tooltip). One theme per page; pagers swap thetab_contentsUIBox 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 (wrappedcreate_UIBox_HUD, injected into thebutton_areanode under Run Info/Options); the pagers work there because vanillacreate_tabsalso names its bodytab_contents. - Discovery:
CardArea:emplacehook callsJCA.check_discoverieswhen a joker lands inG.jokers; famous pairs fielded together persist inconfig.discovered(saved immediately viaSMODS.save_mod_config) and per-run inG.GAME.jca_run_combos. Undiscovered pairs render face-down with a???caption in the Combos tab; blurb replaces it once fielded. Themes (everytheme_infotag) persist inconfig.themes_foundonce two DIFFERENT jokers connect the tag (giver + wanter; a joker carrying both sides needs a second joker). One toast per emplacement, famous pair outranking theme. Badge/progress render intheme_tab_def; toast names come fromJCA.theme_names(filled after THEME_TABS, used only at gameplay time). - Post-run recap: wraps
create_UIBox_game_over/create_UIBox_winand inserts a "Combos fielded this run" row (fromG.GAME.jca_run_combos) relative to vanilla button ids (from_game_over;from_game_wonorwin_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 fromTAG_LABEL.synergies.luaalso exportstheme_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.
- Scoring:
config.lua— default settings (pulse,owned_tooltip,threshold). Steamodded loads and persists this table itself; read it only viaSMODS.current_mod.config(exposed asJCA.config).
Conventions and gotchas
- Cluster tags (hand types
pair/straight/flush/…, suits,planet,spectral_gen): put the same tag in bothgivesandwantsof every member so members mutually reinforce (2+2 = 4). Pure enablers (e.g. Four Fingers, Smeared, Pareidolia) onlygive; pure payoffs onlywant. - Every
wantstag 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 eachPAIRSentry), else the firstTAG_ORDERmatch phrased fromTAG_TEXT(give/want/both variants), else the generic mult×xmult line. Every non-engine tag (anything besides mult/chips/xmult) MUST have aTAG_TEXTentry; newPAIRSentries 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.namefallback. - Anti-synergies never affect scores. Known traps are warning-only data:
CLASHES(pairwise, red "Clashes -" tooltip line vs owned jokers) andCAUTIONS(per-joker, buy-area "Caution:" line) insynergies.lua, same ~34-char blurb budget asPAIRS. Learning mode keeps warnings visible. - The UI hooks are the only untested-in-game surface; if a tooltip regression is
reported, suspect the
aut.infoentry format first and check the lovely log.