From 21995589dfae6404f38193c26ec1b2953632ffb1 Mon Sep 17 00:00:00 2001 From: funman300 Date: Tue, 14 Jul 2026 15:05:02 -0700 Subject: [PATCH] Answer for shop copiers before the player pays MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- CLAUDE.md | 7 +++++++ main.lua | 60 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ test.lua | 32 +++++++++++++++++++++++++++++ 3 files changed, 99 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 3b7cf4d..1b06e76 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/main.lua b/main.lua index 42903da..58dba6b 100644 --- a/main.lua +++ b/main.lua @@ -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. diff --git a/test.lua b/test.lua index 5ca6420..e296209 100644 --- a/test.lua +++ b/test.lua @@ -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.