Answer for shop copiers before the player pays

A purchase always lands in the rightmost slot (buy_from_shop emplaces at
the end, button_callbacks.lua:2279), which the two copiers experience in
opposite ways: a bought Blueprint arrives copying nothing, a bought
Brainstorm copies the leftmost joker immediately. The buy-area tooltip
now says which, by name — JCA.copy_source_if_bought resolves the arrival
target (chaining through a leftmost copier exactly like copy_source),
and Blueprint's line names the joker to slot it against after buying.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-07-14 15:05:02 -07:00
parent a2d16ae12f
commit 21995589df
3 changed files with 99 additions and 0 deletions
+7
View File
@@ -135,6 +135,13 @@ Three-layer split; `JCA` is the single global namespace:
nothing. Do not rebuild the neighbour lookup with the `a and b or c` idiom: nothing. Do not rebuild the neighbour lookup with the `a and b or c` idiom:
for a rightmost Blueprint the right-hand slot is nil and the idiom falls for a rightmost Blueprint the right-hand slot is nil and the idiom falls
through to the leftmost joker, confidently reporting the wrong card. through to the leftmost joker, confidently reporting the wrong card.
Shop copiers get the same treatment *before* purchase via
`JCA.copy_source_if_bought(key)`: a purchase always lands in the
**rightmost** slot (`buy_from_shop``G.jokers:emplace`,
`button_callbacks.lua:2279`; emplace appends, `cardarea.lua:53`), so a
bought Blueprint starts dead (buy-area tooltip names the joker to slot it
against) while a bought Brainstorm copies the leftmost joker immediately
(tooltip says which, chaining through a leftmost copier like copy_source).
- `JCA.is_eternal(card)` reads `card.ability.eternal` — the flag vanilla itself - `JCA.is_eternal(card)` reads `card.ability.eternal` — the flag vanilla itself
uses for the badge (`card.lua:1141`). **Never call `SMODS.is_eternal()` from uses for the badge (`card.lua:1141`). **Never call `SMODS.is_eternal()` from
an advisor path**: it runs a full `SMODS.calculate_context` per call an advisor path**: it runs a full `SMODS.calculate_context` per call
+60
View File
@@ -220,6 +220,30 @@ function JCA.copy_source(card)
return nil, 'none', chain return nil, 'none', chain
end end
-- What a copier still IN THE SHOP would copy if bought right now. A purchase
-- always lands in the RIGHTMOST slot (buy_from_shop hands the card to
-- G.jokers:emplace, button_callbacks.lua:2279, and emplace appends,
-- cardarea.lua:53) -- so a bought Blueprint arrives with no right-hand
-- neighbour and copies nothing until the player reorders, while a bought
-- Brainstorm reads the leftmost joker the moment it lands. Same returns as
-- copy_source; a leftmost joker that is itself a copier is followed through
-- copy_source so the chain resolves to the joker actually copied.
function JCA.copy_source_if_bought(key)
if not (COPIERS[key] and G.jokers and G.jokers.cards) then return nil, 'none', {} end
if COPIERS[key] == 'right' then return nil, 'none', {} end
local target = G.jokers.cards[1]
local center = target and target.config and target.config.center
if not center then return nil, 'none', {} end
if COPIERS[center.key] then
local final, status, chain = JCA.copy_source(target)
table.insert(chain, 1, target)
return final, status, chain
end
if not center.blueprint_compat then return target, 'incompatible', {} end
if target.debuff then return target, 'debuffed', {} end
return target, 'ok', {}
end
-- Ceremonial Dagger is positional too, and it is the dangerous one: when the -- Ceremonial Dagger is positional too, and it is the dangerous one: when the
-- blind is selected it DESTROYS the joker to its right and eats twice its sell -- blind is selected it DESTROYS the joker to its right and eats twice its sell
-- value as Mult (card.lua:2945). The card text says so; what it cannot tell you -- value as Mult (card.lua:2945). The card text says so; what it cannot tell you
@@ -633,6 +657,42 @@ local function tooltip_rows(card)
local caution = in_buy_area(card) and JCA.cautions[key] local caution = in_buy_area(card) and JCA.cautions[key]
if caution then text_row('Caution: ' .. caution, G.C.RED) end if caution then text_row('Caution: ' .. caution, G.C.RED) end
-- Copiers in the shop: a purchase always lands rightmost (see
-- copy_source_if_bought), so say what THIS board hands the copier the
-- moment it arrives -- by name, before the player pays. Explanations,
-- not verdicts, so learning mode keeps them.
if in_buy_area(card) and COPIERS[key] then
if key == 'j_blueprint' then
-- The caution above already says a rightmost Blueprint is dead;
-- add the one thing it cannot know: which joker to slot it against.
local best = JCA.best_copy_target(card)
if best then
text_row('Fix after buying: slot it just left of '
.. name_of(best.config.center.key), G.C.GREEN)
end
else
local target, status, chain = JCA.copy_source_if_bought(key)
local via = ''
for _, link in ipairs(chain) do
via = via .. name_of(link.config.center.key) .. ' -> '
end
local tname = target and name_of(target.config.center.key)
if status == 'ok' then
text_row('Will copy ' .. via .. tname .. ' as soon as it lands', G.C.GREEN)
elseif status == 'incompatible' then
text_row('Will copy ' .. via .. tname .. ' - which cannot be copied', G.C.RED)
elseif status == 'debuffed' then
text_row('Will copy ' .. via .. tname .. ' - debuffed, so nothing', G.C.RED)
else
local best = JCA.best_copy_target(card)
text_row(best
and ('Will copy nothing - make ' .. name_of(best.config.center.key)
.. ' your leftmost joker first')
or 'Will copy nothing - no copyable joker on your board', G.C.RED)
end
end
end
-- Run context: this deck, these hand levels. Facts that frame the tags, -- Run context: this deck, these hand levels. Facts that frame the tags,
-- never part of the score; capped at two lines so multi-suit jokers do -- never part of the score; capped at two lines so multi-suit jokers do
-- not flood the tooltip. Fixed iteration order keeps hovers stable. -- not flood the tooltip. Fixed iteration order keeps hovers stable.
+32
View File
@@ -352,6 +352,38 @@ local ring = field('j_brainstorm', 'j_blueprint')
for _, c in ipairs(ring) do c.config.center.blueprint_compat = true end for _, c in ipairs(ring) do c.config.center.blueprint_compat = true end
eq(JCA.copy_source(ring[2]), nil, 'a Blueprint/Brainstorm ring resolves to nothing') eq(JCA.copy_source(ring[2]), nil, 'a Blueprint/Brainstorm ring resolves to nothing')
-- A copier still in the SHOP is positional before it is even bought: a purchase
-- always lands in the rightmost slot (buy_from_shop emplaces at the end,
-- button_callbacks.lua:2279), so a bought Blueprint starts dead while a bought
-- Brainstorm reads the leftmost joker immediately.
field('j_baron', 'j_mime')
eq(JCA.copy_source_if_bought('j_blueprint'), nil,
'a bought Blueprint arrives rightmost, copying nothing')
eq(select(2, JCA.copy_source_if_bought('j_blueprint')), 'none',
'and reports the empty slot, not an error')
local arrival = field('j_baron', 'j_mime')
arrival[1].config.center.blueprint_compat = true
local bought, bought_status = JCA.copy_source_if_bought('j_brainstorm')
eq(bought, arrival[1], 'a bought Brainstorm copies the leftmost joker on arrival')
eq(bought_status, 'ok', 'and reports it as live')
-- The leftmost joker being a copier chains exactly like an owned Brainstorm:
-- the answer is the joker at the end, and the walked copier is recorded.
local shop_chain = field('j_blueprint', 'j_baron')
for _, c in ipairs(shop_chain) do c.config.center.blueprint_compat = true end
local final_b, st_b, via_b = JCA.copy_source_if_bought('j_brainstorm')
eq(final_b, shop_chain[2], 'a leftmost Blueprint chains to the joker it copies')
eq(st_b, 'ok', 'and reports it as live')
eq(#via_b, 1, 'and records the copier walked through')
eq(via_b[1], shop_chain[1], 'which is the leftmost Blueprint')
field('j_baron', 'j_mime')
G.jokers.cards = {}
eq(JCA.copy_source_if_bought('j_brainstorm'), nil,
'an empty board gives a bought Brainstorm nothing to copy')
eq(JCA.copy_source_if_bought('j_baron'), nil, 'a non-copier has no arrival answer')
-- A dead copier's fix is a reorder, so the advisor names the best target on -- A dead copier's fix is a reorder, so the advisor names the best target on
-- the board: copyable (blueprint_compat for TRUTH, not debuffed, not another -- the board: copyable (blueprint_compat for TRUTH, not debuffed, not another
-- copier), preferring jokers the db marks as prime copy targets. -- copier), preferring jokers the db marks as prime copy targets.