Answer the fuse button: advise on fusions when Fusion Jokers is installed
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>
This commit is contained in:
@@ -124,6 +124,30 @@ JokerDisplay = {Definitions = {
|
||||
}}
|
||||
TMJ = {SEARCH_FIELD_FUNCS = {}}
|
||||
|
||||
-- Fusion Jokers (priority -10000, so it too is loaded before us). Its recipe
|
||||
-- table is an array with the reverse index hung off the same table. Three
|
||||
-- recipes, shaped like the real ones: a plain two-component fusion, a second
|
||||
-- recipe sharing a component (so one joker can feed two fusions), and a
|
||||
-- repeated-component recipe -- their own debug fusion needs 3x Joker, so
|
||||
-- counting by quantity rather than presence is a real requirement, not a
|
||||
-- hypothetical.
|
||||
FusionJokers = {fusions = {
|
||||
{jokers = {{name = 'j_greedy_joker'}, {name = 'j_rough_gem'}},
|
||||
result_joker = 'j_fuse_diamond_bard', cost = 12},
|
||||
{jokers = {{name = 'j_greedy_joker'}, {name = 'j_blackboard'}},
|
||||
result_joker = 'j_fuse_test_two', cost = 4},
|
||||
{jokers = {{name = 'j_joker'}, {name = 'j_joker'}, {name = 'j_joker'}},
|
||||
result_joker = 'j_fuse_three_jimbos', cost = 2},
|
||||
}}
|
||||
FusionJokers.fusions.ingredience = {}
|
||||
for _, f in ipairs(FusionJokers.fusions) do
|
||||
for _, c in ipairs(f.jokers) do
|
||||
FusionJokers.fusions.ingredience[c.name] =
|
||||
FusionJokers.fusions.ingredience[c.name] or {}
|
||||
FusionJokers.fusions.ingredience[c.name][f.result_joker] = true
|
||||
end
|
||||
end
|
||||
|
||||
local data = dofile('synergies.lua')
|
||||
dofile('main.lua')
|
||||
|
||||
@@ -851,6 +875,126 @@ 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('Integrations: Fusion Jokers recipes, cost and what fusing costs you')
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
eq(JCA.integrations.fusionjokers, 3, 'every stub recipe was indexed')
|
||||
|
||||
-- A shop card is priced as if already bought: the question in the shop is
|
||||
-- "what does buying this unlock", not "what do I have".
|
||||
local function shop_card(key)
|
||||
return {config = {center = {key = key, set = 'Joker'}}}
|
||||
end
|
||||
local function plan_for(card, result)
|
||||
for _, p in ipairs(JCA.fusion_plans(card)) do
|
||||
if p.result == result then return p end
|
||||
end
|
||||
end
|
||||
|
||||
G.GAME.dollars = 20
|
||||
|
||||
-- A joker in no recipe stays silent, and so does an unknown modded one.
|
||||
field('j_baron', 'j_mime')
|
||||
eq(#JCA.fusion_plans(G.jokers.cards[1]), 0, 'a joker in no recipe has no plans')
|
||||
eq(#JCA.fusion_plans(shop_card('j_totally_modded')), 0,
|
||||
'an unknown modded joker has no plans, not an error')
|
||||
|
||||
-- Owned, with the partner component missing.
|
||||
field('j_greedy_joker', 'j_baron')
|
||||
local p = plan_for(G.jokers.cards[1], 'j_fuse_diamond_bard')
|
||||
ok(p and not p.ready, 'a half-assembled fusion is not ready')
|
||||
eq(p and #p.missing, 1, 'and names exactly the component still missing')
|
||||
eq(p and p.missing[1].key, 'j_rough_gem', 'which is Rough Gem')
|
||||
eq(p and p.missing[1].n, 1, 'one copy short')
|
||||
eq(p and #p.loses, 0, 'an unready fusion claims no losses')
|
||||
|
||||
-- Both components owned: ready, priced, affordable at $20.
|
||||
field('j_greedy_joker', 'j_rough_gem')
|
||||
p = plan_for(G.jokers.cards[1], 'j_fuse_diamond_bard')
|
||||
ok(p and p.ready, 'both components fielded makes the fusion ready')
|
||||
eq(p and p.cost, 12, 'at the recipe cost')
|
||||
eq(p and p.affordable, true, '$20 covers a $12 fusion')
|
||||
G.GAME.dollars = 5
|
||||
p = plan_for(G.jokers.cards[1], 'j_fuse_diamond_bard')
|
||||
eq(p and p.affordable, false, 'but $5 does not')
|
||||
G.GAME.dollars = 20
|
||||
|
||||
-- The whole point of the integration: fusing EATS the components, so the
|
||||
-- combos they hold with the rest of the board die with them. Smeared feeds
|
||||
-- both Diamond jokers their suit_flex, and survives the fusion.
|
||||
field('j_greedy_joker', 'j_rough_gem', 'j_smeared')
|
||||
p = plan_for(G.jokers.cards[1], 'j_fuse_diamond_bard')
|
||||
eq(p and #p.loses, 1, 'fusing reports the combo it would cost you')
|
||||
eq(p and p.loses[1], 'j_smeared', 'naming the surviving partner left stranded')
|
||||
|
||||
-- A component of the fusion is not "lost" -- it is being spent, not stranded.
|
||||
field('j_greedy_joker', 'j_rough_gem')
|
||||
p = plan_for(G.jokers.cards[1], 'j_fuse_diamond_bard')
|
||||
eq(p and #p.loses, 0, 'the other component is spent, not counted as a loss')
|
||||
|
||||
-- In the shop, the card counts as bought.
|
||||
field('j_rough_gem')
|
||||
p = plan_for(shop_card('j_greedy_joker'), 'j_fuse_diamond_bard')
|
||||
ok(p and p.ready, 'a shop card completes a recipe as if already bought')
|
||||
eq(#JCA.fusion_plans(G.jokers.cards[1]), 1,
|
||||
'while the owned half alone still reads as unready')
|
||||
ok(not JCA.fusion_plans(G.jokers.cards[1])[1].ready, '(and says so)')
|
||||
|
||||
-- Repeated components are counted by QUANTITY, not presence: their own debug
|
||||
-- recipe needs three Jokers, and owning one is not owning three.
|
||||
field('j_joker')
|
||||
p = plan_for(G.jokers.cards[1], 'j_fuse_three_jimbos')
|
||||
ok(p and not p.ready, 'one Joker does not satisfy a 3x Joker recipe')
|
||||
eq(p and p.missing[1].n, 2, 'and the shortfall is the missing two')
|
||||
field('j_joker', 'j_joker', 'j_joker')
|
||||
p = plan_for(G.jokers.cards[1], 'j_fuse_three_jimbos')
|
||||
ok(p and p.ready, 'three Jokers do satisfy it')
|
||||
|
||||
-- Discounts, mirroring get_card_fusion: flat then percentage, floored, min $1.
|
||||
field('j_greedy_joker', 'j_rough_gem')
|
||||
G.GAME.fujo_fusion_discount = {j_fuse_diamond_bard = 5}
|
||||
eq(plan_for(G.jokers.cards[1], 'j_fuse_diamond_bard').cost, 7,
|
||||
'a per-fusion flat discount comes off the price')
|
||||
G.GAME.fujo_fusion_discount.universal = 2
|
||||
eq(plan_for(G.jokers.cards[1], 'j_fuse_diamond_bard').cost, 5,
|
||||
'and stacks with the universal one')
|
||||
G.GAME.fujo_fusion_discountpercent = {universal = 0.5}
|
||||
eq(plan_for(G.jokers.cards[1], 'j_fuse_diamond_bard').cost, 2,
|
||||
'percentages apply after, and round down')
|
||||
G.GAME.fujo_fusion_discount.universal = 100
|
||||
eq(plan_for(G.jokers.cards[1], 'j_fuse_diamond_bard').cost, 1,
|
||||
'a fusion never costs less than $1')
|
||||
G.GAME.fujo_fusion_discount, G.GAME.fujo_fusion_discountpercent = nil, nil
|
||||
|
||||
-- Ready fusions sort ahead of unready ones, so the two lines the tooltip has
|
||||
-- room for are the two the player can act on.
|
||||
field('j_greedy_joker', 'j_blackboard')
|
||||
local plans = JCA.fusion_plans(G.jokers.cards[1])
|
||||
eq(#plans, 2, 'a joker feeding two recipes gets a plan for each')
|
||||
eq(plans[1].result, 'j_fuse_test_two', 'the ready fusion sorts first')
|
||||
ok(not plans[2].ready, 'the unready one follows')
|
||||
|
||||
-- A third-party recipe can gate itself; a predicate that says no, or throws,
|
||||
-- must never produce a false "ready".
|
||||
FusionJokers.fusions[2].requirement = function() return false end
|
||||
ok(not plan_for(G.jokers.cards[1], 'j_fuse_test_two').ready,
|
||||
'a requirement returning false blocks readiness')
|
||||
FusionJokers.fusions[2].requirement = function() error('someone elses bug') end
|
||||
ok(not plan_for(G.jokers.cards[1], 'j_fuse_test_two').ready,
|
||||
'and a requirement that throws is treated as unmet, not a crash')
|
||||
FusionJokers.fusions[2].requirement = nil
|
||||
|
||||
-- No Fusion Jokers installed: silence, exactly like an unknown joker.
|
||||
do
|
||||
local real = FusionJokers
|
||||
FusionJokers = nil
|
||||
eq(dofile('integrations.lua').fusionjokers(), 0,
|
||||
'a missing Fusion Jokers install patches nothing and does not error')
|
||||
FusionJokers = real
|
||||
end
|
||||
G.GAME.dollars = nil
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
section('UI: catalog pages build and animate')
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user