Stop giving advice the game will not let you follow
Four fixes, all traced to the running source. * The sell advisor could tell you to sell an ETERNAL joker, which Card:can_sell_card refuses outright (card.lua:1993). It now never names one, though they still count toward the best score -- they are part of the build you are keeping. Advice you cannot take is worse than none. * Copiers CHAIN. SMODS.blueprint_effect recurses (utils.lua:2269) and Blueprint's own blueprint_compat is true, so Blueprint -> Blueprint -> Baron really does give two Barons. The tooltip used to answer "Copying Blueprint", the one reply that helps nobody; it now walks the chain to the joker actually copied and shows the route. Capped at the board size like vanilla, and a Blueprint/Brainstorm ring resolves to nothing instead of hanging. * blueprint_compat is tested for TRUTH by vanilla (utils.lua:2254), not for `~= false` as I had it. A modded joker that simply omits the flag is NOT copyable, and this mod supports other mods' jokers by design, so saying otherwise was a lie about someone else's content. * A debuffed joker copies as nothing (same line), and a debuffed joker does nothing at all this blind -- both now say so. Scoring is left alone: a debuff is temporary and the advisor advises on the build, so hiding a debuffed partner from "Active combos" would misrepresent it. Also: never call SMODS.is_eternal() from an advisor path. It runs a full SMODS.calculate_context per call (utils.lua:3079) -- far too heavy for a hover -- and outside a live run it reported EVERY joker as Eternal, which silently disabled the sell advisor. The smoke harness caught that; the stub could not have. 721 pass locally, 12 in-game. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -159,36 +159,65 @@ end
|
||||
|
||||
-- Copy jokers are positional, and the board order is right there in
|
||||
-- G.jokers.cards -- so the advisor can name what a copier is ACTUALLY copying
|
||||
-- rather than guess. Blueprint copies the joker to its right (card.lua:2306);
|
||||
-- Brainstorm copies the leftmost one (card.lua:2322), which is nothing when it
|
||||
-- is itself leftmost.
|
||||
-- rather than guess. Blueprint copies the joker to its right (card.lua:4663);
|
||||
-- Brainstorm copies the leftmost one, which is nothing when it is itself leftmost.
|
||||
--
|
||||
-- Returns: the copied card (or nil), and whether vanilla will actually let it be
|
||||
-- copied -- 29 jokers carry `blueprint_compat = false` and yield nothing at all.
|
||||
-- Copiers CHAIN: Blueprint's own blueprint_compat is true, and
|
||||
-- SMODS.blueprint_effect recurses into the copied card (utils.lua:2269), so
|
||||
-- Blueprint -> Blueprint -> Baron really does give you two Barons. Answering
|
||||
-- "Copying Blueprint" would be the one reply that tells the player nothing, so we
|
||||
-- walk the chain to the joker whose ability actually gets copied.
|
||||
--
|
||||
-- Returns: target, status, chain
|
||||
-- target - the joker ultimately copied (nil when the chain yields nothing)
|
||||
-- status - 'ok' | 'none' | 'incompatible' | 'debuffed'
|
||||
-- chain - the copiers walked through on the way there (Blueprints in between)
|
||||
local COPIERS = {j_blueprint = 'right', j_brainstorm = 'leftmost'}
|
||||
|
||||
function JCA.copy_source(card)
|
||||
if not (G.jokers and G.jokers.cards and card.config and card.config.center) then
|
||||
return nil
|
||||
return nil, 'none', {}
|
||||
end
|
||||
local mode = COPIERS[card.config.center.key]
|
||||
if not mode then return nil end
|
||||
local cards, idx = G.jokers.cards, nil
|
||||
for i, c in ipairs(cards) do
|
||||
if c == card then idx = i break end
|
||||
end
|
||||
if not idx then return nil end
|
||||
if not COPIERS[card.config.center.key] then return nil, 'none', {} end
|
||||
|
||||
-- NOT the `a and b or c` idiom: for a rightmost Blueprint cards[idx+1] is nil,
|
||||
-- and the idiom would fall through to `or cards[1]` and confidently report it
|
||||
-- copying the LEFTMOST joker.
|
||||
local target
|
||||
if mode == 'right' then target = cards[idx + 1] else target = cards[1] end
|
||||
if not target or target == card then return nil end
|
||||
local center = target.config and target.config.center
|
||||
if not center then return nil end
|
||||
-- blueprint_compat is vanilla's own flag for "this joker cannot be copied".
|
||||
return target, center.blueprint_compat ~= false
|
||||
local cards = G.jokers.cards
|
||||
local chain, seen, current = {}, {}, card
|
||||
|
||||
-- Vanilla caps the recursion at the board size (utils.lua:2255); a
|
||||
-- Blueprint/Brainstorm ring would otherwise loop forever and yields nothing.
|
||||
for _ = 1, #cards + 1 do
|
||||
if seen[current] then return nil, 'none', chain end
|
||||
seen[current] = true
|
||||
|
||||
local mode = COPIERS[current.config.center.key]
|
||||
local idx
|
||||
for i, c in ipairs(cards) do
|
||||
if c == current then idx = i break end
|
||||
end
|
||||
if not idx then return nil, 'none', chain end
|
||||
|
||||
-- NOT the `a and b or c` idiom: for a rightmost Blueprint cards[idx+1] is
|
||||
-- nil, and the idiom would fall through to `or cards[1]` and confidently
|
||||
-- report it copying the LEFTMOST joker.
|
||||
local target
|
||||
if mode == 'right' then target = cards[idx + 1] else target = cards[1] end
|
||||
if not target or target == current then return nil, 'none', chain end
|
||||
|
||||
local center = target.config and target.config.center
|
||||
if not center then return nil, 'none', chain end
|
||||
|
||||
-- Vanilla tests blueprint_compat for TRUTH, not for `~= false`
|
||||
-- (utils.lua:2254). A modded joker that simply omits the flag is NOT
|
||||
-- copyable, and saying otherwise would be a lie about someone else's mod.
|
||||
if not center.blueprint_compat then return target, 'incompatible', chain end
|
||||
-- A debuffed joker is copied as nothing at all (same line).
|
||||
if target.debuff then return target, 'debuffed', chain end
|
||||
|
||||
if not COPIERS[center.key] then return target, 'ok', chain end
|
||||
chain[#chain + 1] = target -- another copier: follow it
|
||||
current = target
|
||||
end
|
||||
return nil, 'none', chain
|
||||
end
|
||||
|
||||
-- Ceremonial Dagger is positional too, and it is the dangerous one: when the
|
||||
@@ -211,12 +240,9 @@ function JCA.dagger_victim(card)
|
||||
local target = cards[idx + 1]
|
||||
if not target or not (target.config and target.config.center) then return nil end
|
||||
|
||||
local eternal = target.ability and target.ability.eternal
|
||||
if SMODS and SMODS.is_eternal then
|
||||
local ok, res = pcall(SMODS.is_eternal, target, card)
|
||||
if ok then eternal = res end
|
||||
end
|
||||
return target, not not eternal
|
||||
-- Same reasoning as JCA.is_eternal: read the sticker, never call
|
||||
-- SMODS.is_eternal() from an advisor path.
|
||||
return target, JCA.is_eternal(target)
|
||||
end
|
||||
|
||||
-- Sell advisor: on a full board, the owned joker contributing the least
|
||||
@@ -244,6 +270,20 @@ local function keep_score(card, cards)
|
||||
return total
|
||||
end
|
||||
|
||||
-- Eternal jokers CANNOT be sold: Card:can_sell_card bails on them outright
|
||||
-- (card.lua:1993). Advice you are not allowed to take is worse than no advice, so
|
||||
-- an Eternal joker is never the sell candidate -- but it still counts towards the
|
||||
-- best score, because it is genuinely part of the build you are keeping.
|
||||
-- Read the sticker directly, and do NOT call SMODS.is_eternal(): it runs a full
|
||||
-- SMODS.calculate_context on every call (utils.lua:3079), which is far too heavy
|
||||
-- for something a hover invokes once per joker -- and it does not answer honestly
|
||||
-- outside a live run. In the smoke harness it reported EVERY joker as Eternal,
|
||||
-- which silenced the sell advisor completely. `ability.eternal` is the same flag
|
||||
-- vanilla reads to draw the Eternal badge (card.lua:1141).
|
||||
function JCA.is_eternal(card)
|
||||
return not not (card.ability and card.ability.eternal)
|
||||
end
|
||||
|
||||
function JCA.weakest_link()
|
||||
if not (G.jokers and G.jokers.cards) then return nil end
|
||||
local cards = G.jokers.cards
|
||||
@@ -253,7 +293,7 @@ function JCA.weakest_link()
|
||||
for _, c in ipairs(cards) do
|
||||
if c.config and c.config.center and c.config.center.set == 'Joker' then
|
||||
local total = keep_score(c, cards)
|
||||
if not worst_total or total < worst_total then
|
||||
if not JCA.is_eternal(c) and (not worst_total or total < worst_total) then
|
||||
worst, worst_total = c, total
|
||||
end
|
||||
if not best_total or total > best_total then best_total = total end
|
||||
@@ -466,19 +506,33 @@ local function tooltip_rows(card)
|
||||
-- The board order decides it, so only an owned joker has an answer. These are
|
||||
-- explanations, not verdicts, so learning mode keeps them.
|
||||
if card.area == G.jokers then
|
||||
-- A debuffed joker does nothing at all this blind, whatever its combos say.
|
||||
if card.debuff then
|
||||
text_row('Debuffed - does nothing this blind', G.C.RED)
|
||||
end
|
||||
|
||||
-- Copy jokers. A Blueprint in the wrong slot is a dead card that looks fine.
|
||||
local target, copyable = JCA.copy_source(card)
|
||||
if not target then
|
||||
if COPIERS[key] then
|
||||
if COPIERS[key] then
|
||||
local target, status, chain = JCA.copy_source(card)
|
||||
-- Chained copiers: "Copying Blueprint -> Baron" beats "Copying
|
||||
-- Blueprint", which is the one answer that helps nobody.
|
||||
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('Copying ' .. via .. tname, G.C.GREEN)
|
||||
elseif status == 'incompatible' then
|
||||
text_row('Copying ' .. via .. tname .. ' - which cannot be copied', G.C.RED)
|
||||
elseif status == 'debuffed' then
|
||||
text_row('Copying ' .. via .. tname .. ' - debuffed, so nothing', G.C.RED)
|
||||
else
|
||||
text_row(key == 'j_brainstorm'
|
||||
and 'Copying nothing - it IS the leftmost joker'
|
||||
or 'Copying nothing - no joker to its right', G.C.RED)
|
||||
end
|
||||
elseif not copyable then
|
||||
text_row('Copying ' .. name_of(target.config.center.key)
|
||||
.. ' - which cannot be copied', G.C.RED)
|
||||
else
|
||||
text_row('Copying ' .. name_of(target.config.center.key), G.C.GREEN)
|
||||
end
|
||||
|
||||
-- Ceremonial Dagger. This one is about to eat a joker, permanently, at the
|
||||
|
||||
Reference in New Issue
Block a user