The pixel-measured wrap passed the headless suite (which stubs text_width) but
failed three assertions under the real engine in the smoke harness:
- "Looking for:"/"Offers:" hints and the two-tone combo rows render at scale
0.3 but the width budget -- and every other row -- is measured at 0.32, so a
row that fit at 0.3 slipped over the budget once measured at 0.32 and clipped.
Render those rows small as before, but bound their wrap at the 0.32 reference:
a 0.3 row is narrower still, so it can only wrap early, never clip.
- wrap_measured word-split multi-word joker names, so a fusion's result read as
"Fusion ready: Diamond" / "Bard ($12)" -- two jokers, not one. atomic() fuses
a name or multi-word label into one unbreakable wrap token (spaces -> a \1
sentinel, restored for measurement and output); applied to every joker name
and tag label that flows through a wrapped row.
Smoke: 20/20 against the real Card:generate_UIBox_ability_table pipeline and the
real Fusion Jokers recipe table. Headless: 947 pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The purple Combos button lived in the run HUD button_area; move it to a
"Combos" row in the pause menu (Escape > under Settings, before
restart_button) so the play screen stays clear. Same overlay, same tabs;
Mods > Combo Advisor is unchanged.
restart_button is an id vanilla only builds during a run, so the row never
reaches the main menu. Because the pause menu builds its node list as a table
constructor with conditional entries (an unseeded run leaves a hole at the
seed row's index), find_trail/insert_node walk and shift by explicit numeric
key rather than ipairs/table.insert.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Hovering a joker that is a fusion component now names the fusion it feeds,
its cost with discounts applied, and what is still missing. In the shop the
card is counted as if already bought, so it answers the buy-side question
("Buy to fuse into Diamond Bard ($12 to fuse)") instead of restating what
you already own.
The line worth having is the last one. Fusing CONSUMES its components --
fuse_card calls ingredient:remove() -- so every combo those jokers held with
the rest of the board dies with them, and the FUSE button cannot tell you
that. "Fusing drops combos with: Smeared" is computed from the surviving
board, and a component being spent is not counted as a loss.
Three things about their code shaped this:
* Card:get_card_fusion() would have answered most of it, but it drives its
price flicker with math.randomseed(love.timer.getTime() * 8). An advisor
reseeding Lua's RNG on every hover has no business doing that, so this
reads FusionJokers.fusions directly and mirrors their discount arithmetic
(flat then percentage, per-result and universal, floored, min $1).
* Recipes take repeated components -- their own debug fusion needs 3x Joker
-- so components are counted by quantity, not presence.
* Affordability is judged only on plain numbers: Fusion Jokers uses to_big,
and Talisman turns G.GAME.dollars into an object. Guessing wrong about
someone's money is worse than staying quiet, so the money line just
does not appear.
Ownership is board membership, not card.area, matching copy_source and
dagger_victim -- the area pointer answers differently for the same card
depending on who built it.
Verified against the shipped recipe table: all 15 resolve at the right cost,
and all 30 components are jokers the database already knows. Not installed
means silence, like every other integration.
918 tests pass on 5.4 and LuaJIT (31 new), and 18/18 in the real game.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Localization was assessed and deliberately deferred: the mod's teaching
text lives in data blurbs (synergies.lua), so translating only the UI
chrome would produce a half-English interface; a real pass needs the
blurb schema redesigned around loc keys first.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>