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:
funman300
2026-07-14 12:50:47 -07:00
parent 8ce5619273
commit de67970a15
3 changed files with 165 additions and 49 deletions
+52 -3
View File
@@ -295,12 +295,61 @@ eq(JCA.copy_source(G.jokers.cards[1]), nil, 'a non-copier has no copy source')
-- and yield nothing at all when copied.
local uncopyable = field('j_blueprint', 'j_splash')
uncopyable[2].config.center.blueprint_compat = false
local target, copyable = JCA.copy_source(uncopyable[1])
local target, status = JCA.copy_source(uncopyable[1])
eq(target, uncopyable[2], 'it still reports what sits in the copied slot')
eq(copyable, false, 'and flags that vanilla will not copy it (blueprint_compat)')
eq(status, 'incompatible', 'and flags that vanilla will not copy it (blueprint_compat)')
-- Vanilla tests the flag for TRUTH, so a modded joker that omits it is NOT
-- copyable. `~= false` would wrongly call this one fine.
local modded = field('j_blueprint', 'j_baron')
modded[2].config.center.blueprint_compat = nil
eq(select(2, JCA.copy_source(modded[1])), 'incompatible',
'a joker with no blueprint_compat flag at all is treated as uncopyable')
local fine = field('j_blueprint', 'j_baron')
eq(select(2, JCA.copy_source(fine[1])), true, 'a copyable target reports as copyable')
fine[2].config.center.blueprint_compat = true
eq(select(2, JCA.copy_source(fine[1])), 'ok', 'a copyable target reports as ok')
-- A debuffed joker is copied as nothing (SMODS.blueprint_effect bails on it).
local debuffed = field('j_blueprint', 'j_baron')
debuffed[2].config.center.blueprint_compat = true
debuffed[2].debuff = true
eq(select(2, JCA.copy_source(debuffed[1])), 'debuffed',
'copying a debuffed joker yields nothing')
-- Copiers CHAIN: Blueprint -> Blueprint -> Baron really does give two Barons, so
-- the answer must be Baron, not "a Blueprint".
local chained = field('j_blueprint', 'j_blueprint', 'j_baron')
for _, c in ipairs(chained) do c.config.center.blueprint_compat = true end
local final, st, via = JCA.copy_source(chained[1])
eq(final, chained[3], 'the chain resolves to the joker actually copied')
eq(st, 'ok', 'and reports it as live')
eq(#via, 1, 'and records the copier walked through on the way')
eq(via[1], chained[2], 'which is the middle Blueprint')
-- A copier ring copies nothing, and must not hang.
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')
--------------------------------------------------------------------------------
section('Engine: the sell advisor respects what you can actually sell')
--------------------------------------------------------------------------------
-- Eternal jokers CANNOT be sold (Card:can_sell_card, card.lua:1993). Telling the
-- player to sell one is advice they are not allowed to take.
--
-- 8 Ball scores 0 here and Scary Face 1, so 8 Ball is the natural cut -- until it
-- turns out to be Eternal, and the advice has to move to the next one down.
local et = field('j_8_ball', 'j_scary_face', 'j_baron', 'j_mime')
G.jokers.config.card_limit = 4
eq(JCA.weakest_link(), et[1], 'normally it flags the lowest-scoring joker')
et[1].ability = {eternal = true}
local pick = JCA.weakest_link()
ok(pick ~= et[1], 'but never an Eternal one, because it cannot be sold',
pick and pick.config.center.key or 'nil')
eq(pick, et[2], 'it falls through to the next sellable candidate instead')
--------------------------------------------------------------------------------
section('Engine: Ceremonial Dagger names its victim')