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:
for a rightmost Blueprint the right-hand slot is nil and the idiom falls
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
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
+60
View File
@@ -220,6 +220,30 @@ function JCA.copy_source(card)
return nil, 'none', chain
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
-- 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
@@ -633,6 +657,42 @@ local function tooltip_rows(card)
local caution = in_buy_area(card) and JCA.cautions[key]
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,
-- never part of the score; capped at two lines so multi-suit jokers do
-- 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
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
-- the board: copyable (blueprint_compat for TRUTH, not debuffed, not another
-- copier), preferring jokers the db marks as prime copy targets.