Respect the other two stickers: rentals bleed, perished jokers are dead

JCA.is_rental and JCA.is_perished read ability.rental and
ability.perishable/perish_tally, the same flags vanilla uses
(card.lua:684/676). The sell advisor now docks a rental 2 points (it
drains $3 every round, game.lua:1957) and treats a perished joker --
tally 0, debuffed permanently per card.lua:716 -- as dead weight that
outweighs any synergy score. Tooltips gain the mechanical facts:
countdown on ticking perishables, drain rate on rentals (shop phrasing
notes the $1 price), and 'Perished - it will never work again' instead
of the misleading 'does nothing this blind' on permanently dead cards.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-07-14 13:44:52 -07:00
parent 665bd9b396
commit ea8faac434
2 changed files with 79 additions and 2 deletions
+46 -2
View File
@@ -258,6 +258,13 @@ end
-- so it always breaks a tie against a clashing joker. -- so it always breaks a tie against a clashing joker.
local CLASH_PENALTY = 3 local CLASH_PENALTY = 3
-- Sticker weights, same philosophy: mechanical facts only. A rental bleeds
-- $3 every round, so between two equally-scoring jokers, cut the rental. A
-- PERISHED joker (perishable, tally at 0) is debuffed forever -- dead weight
-- at any synergy score, so its penalty dwarfs every possible total.
local RENTAL_PENALTY = 2
local PERISHED_PENALTY = 1000
local function keep_score(card, cards) local function keep_score(card, cards)
local _, total = JCA.partners_for(card) local _, total = JCA.partners_for(card)
local key = card.config.center.key local key = card.config.center.key
@@ -267,6 +274,8 @@ local function keep_score(card, cards)
total = total - CLASH_PENALTY total = total - CLASH_PENALTY
end end
end end
if JCA.is_rental(card) then total = total - RENTAL_PENALTY end
if JCA.is_perished(card) then total = total - PERISHED_PENALTY end
return total return total
end end
@@ -284,6 +293,22 @@ function JCA.is_eternal(card)
return not not (card.ability and card.ability.eternal) return not not (card.ability and card.ability.eternal)
end end
-- Rental and Perishable are the other two stickers the advisor must respect,
-- and both are plain ability flags like eternal (card.lua:684, 676). A rental
-- drains G.GAME.rental_rate every round (card.lua:2646; $3, game.lua:1957).
-- A perishable joker counts perish_tally down each round (card.lua:2652) and
-- once it hits 0 the card is debuffed PERMANENTLY (card.lua:716) -- not "this
-- blind", forever. Eternal and perishable are mutually exclusive (set_perishable
-- bails on eternals), so a perished joker is always legal to sell.
function JCA.is_rental(card)
return not not (card.ability and card.ability.rental)
end
function JCA.is_perished(card)
local a = card.ability
return not not (a and a.perishable and (a.perish_tally or 0) <= 0)
end
function JCA.weakest_link() function JCA.weakest_link()
if not (G.jokers and G.jokers.cards) then return nil end if not (G.jokers and G.jokers.cards) then return nil end
local cards = G.jokers.cards local cards = G.jokers.cards
@@ -567,13 +592,32 @@ 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
-- Sticker facts (any area -- higher stakes put stickers on shop jokers
-- too). Mechanical numbers from the game source, not judgements, so
-- learning mode keeps them.
if card.ability and card.ability.perishable and not JCA.is_perished(card) then
local n = card.ability.perish_tally or 0
text_row(('Perishable - %d round%s before it dies')
:format(n, n == 1 and '' or 's'), G.C.RED)
end
if JCA.is_rental(card) then
local rate = (G.GAME and G.GAME.rental_rate) or 3
text_row(in_buy_area(card)
and ('Rental - $1 now, but $' .. rate .. ' every round after')
or ('Rental - drains $' .. rate .. ' every round'), G.C.RED)
end
-- Positional jokers: say what this one is doing to its neighbour RIGHT NOW. -- Positional jokers: say what this one is doing to its neighbour RIGHT NOW.
-- The board order decides it, so only an owned joker has an answer. These are -- The board order decides it, so only an owned joker has an answer. These are
-- explanations, not verdicts, so learning mode keeps them. -- explanations, not verdicts, so learning mode keeps them.
if card.area == G.jokers then if card.area == G.jokers then
-- A debuffed joker does nothing at all this blind, whatever its combos say. -- A debuffed joker does nothing at all this blind, whatever its combos
-- say -- and a PERISHED one is debuffed for good (card.lua:716), which
-- deserves the honest tense.
if card.debuff then if card.debuff then
text_row('Debuffed - does nothing this blind', G.C.RED) text_row(JCA.is_perished(card)
and 'Perished - it will never work again'
or 'Debuffed - does nothing this blind', G.C.RED)
end end
-- Copy jokers. A Blueprint in the wrong slot is a dead card that looks fine. -- Copy jokers. A Blueprint in the wrong slot is a dead card that looks fine.
+33
View File
@@ -382,6 +382,39 @@ ok(pick ~= et[1], 'but never an Eternal one, because it cannot be sold',
pick and pick.config.center.key or 'nil') pick and pick.config.center.key or 'nil')
eq(pick, et[2], 'it falls through to the next sellable candidate instead') eq(pick, et[2], 'it falls through to the next sellable candidate instead')
--------------------------------------------------------------------------------
section('Engine: the sell advisor weighs rentals and perished jokers')
--------------------------------------------------------------------------------
-- ability.rental drains $3 a round (card.lua:2646, game.lua:1957); a perishable
-- joker whose tally ran out is debuffed FOREVER (card.lua:716). Both are flags
-- on card.ability, read the same way as eternal.
local plain = field('j_baron', 'j_joker')
eq(JCA.is_rental(plain[1]), false, 'no sticker reads as no rental')
plain[1].ability = {rental = true}
eq(JCA.is_rental(plain[1]), true, 'ability.rental reads as a rental')
plain[2].ability = {perishable = true, perish_tally = 3}
eq(JCA.is_perished(plain[2]), false, 'a ticking perishable has not perished yet')
plain[2].ability.perish_tally = 0
eq(JCA.is_perished(plain[2]), true, 'tally 0 means debuffed for good')
-- Three mutually inert jokers tie at 0, so nothing stands out to cut --
-- until one of them is a rental, which bleeds money every round.
local rent = field('j_joker', 'j_half', 'j_misprint')
G.jokers.config.card_limit = 3
eq(JCA.weakest_link(), nil, 'an all-tied board flags nobody')
rent[2].ability = {rental = true}
eq(JCA.weakest_link(), rent[2], 'the rental loses the tie: it drains $3 a round')
-- A perished joker is dead weight at ANY synergy score: it outweighs even a
-- famous-pair member against the board's zero-scorer.
local per = field('j_baron', 'j_mime', 'j_joker')
G.jokers.config.card_limit = 3
eq(JCA.weakest_link(), per[3], 'normally the zero-scorer is the cut')
per[2].ability = {perishable = true, perish_tally = 0}
eq(JCA.weakest_link(), per[2],
'but a perished joker is the cut at any score - it will never work again')
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
section('Engine: Ceremonial Dagger names its victim') section('Engine: Ceremonial Dagger names its victim')
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------