The pixel-measured wrap passed the headless suite (which stubs text_width) but failed three assertions under the real engine in the smoke harness: - "Looking for:"/"Offers:" hints and the two-tone combo rows render at scale 0.3 but the width budget -- and every other row -- is measured at 0.32, so a row that fit at 0.3 slipped over the budget once measured at 0.32 and clipped. Render those rows small as before, but bound their wrap at the 0.32 reference: a 0.3 row is narrower still, so it can only wrap early, never clip. - wrap_measured word-split multi-word joker names, so a fusion's result read as "Fusion ready: Diamond" / "Bard ($12)" -- two jokers, not one. atomic() fuses a name or multi-word label into one unbreakable wrap token (spaces -> a \1 sentinel, restored for measurement and output); applied to every joker name and tag label that flows through a wrapped row. Smoke: 20/20 against the real Card:generate_UIBox_ability_table pipeline and the real Fusion Jokers recipe table. Headless: 947 pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,12 @@
|
||||
Settings) instead of a button in the run HUD, which keeps the play screen
|
||||
clear. Same overlay, same tabs; Mods > Combo Advisor is unchanged.
|
||||
|
||||
### Fixed
|
||||
- The Combo Advisor hover tooltip no longer runs off the edge of the screen:
|
||||
every advice row is measured against the real font and wrapped to the card's
|
||||
gutter, and a wrapped line never splits a joker name ("Diamond Bard" stays
|
||||
whole rather than breaking across two rows).
|
||||
|
||||
### Fusion Jokers integration
|
||||
- When [Fusion Jokers](https://github.com/wingedcatgirl/Fusion-Jokers) is
|
||||
installed, hovering any joker that is a fusion component names the fusion it
|
||||
|
||||
@@ -603,6 +603,16 @@ function JCA.row_width_budget()
|
||||
return (type(room) == 'number' and room > 0 and room or 20) * ROW_WIDTH_FRACTION
|
||||
end
|
||||
|
||||
-- A joker name or multi-word label must never wrap mid-phrase -- "Fusion ready:
|
||||
-- Diamond" over one row and "Bard" over the next reads as two jokers. atomic()
|
||||
-- fuses a phrase's spaces into a sentinel that %S+ tokenisation treats as one
|
||||
-- word; wrap_measured restores them for both measurement and output. \1 never
|
||||
-- occurs in localized joker text. ONLY apply to text that goes through
|
||||
-- wrap_measured (text_row) -- a sentinel placed straight into a UIT node would
|
||||
-- render as tofu (labelled_row's label node does NOT wrap, so it stays plain).
|
||||
local NBSP = '\1'
|
||||
local function atomic(s) return (tostring(s):gsub(' ', NBSP)) end
|
||||
|
||||
-- Word-wrap `str` to the measured budget, returning finished lines with the
|
||||
-- hanging `indent` already applied to continuations.
|
||||
--
|
||||
@@ -621,7 +631,9 @@ function JCA.wrap_measured(str, scale, budget, indent)
|
||||
for word in tostring(str):gmatch('%S+') do
|
||||
local nxt = line and (line .. ' ' .. word) or word
|
||||
local limit = #lines == 0 and budget or budget - pad
|
||||
if line and JCA.text_width(nxt, scale) > limit then
|
||||
-- Measure with sentinels restored to real spaces: an atomic() phrase is
|
||||
-- one %S+ token but its true width is the width of the spaced text.
|
||||
if line and JCA.text_width((nxt:gsub(NBSP, ' ')), scale) > limit then
|
||||
lines[#lines + 1] = line
|
||||
line = word
|
||||
else
|
||||
@@ -631,6 +643,7 @@ function JCA.wrap_measured(str, scale, budget, indent)
|
||||
if line then lines[#lines + 1] = line end
|
||||
if #lines == 0 then return {''} end
|
||||
for i = 2, #lines do lines[i] = indent .. lines[i] end
|
||||
for i = 1, #lines do lines[i] = (lines[i]:gsub(NBSP, ' ')) end
|
||||
return lines
|
||||
end
|
||||
|
||||
@@ -774,9 +787,15 @@ local function tooltip_rows(card)
|
||||
-- body wraps beneath it, which keeps the colour split readable instead of
|
||||
-- splitting a coloured phrase mid-word across rows.
|
||||
local function labelled_row(label, label_colour, body)
|
||||
-- Render small so "name: reason" fits one row more often, but bound the
|
||||
-- width at the SAME 0.32*S reference the budget and every other row are
|
||||
-- measured against -- a row drawn at 0.3 is narrower still, so this can
|
||||
-- only ever wrap early, never clip. (Measuring the wrap at 0.3 let a
|
||||
-- body line that fit at 0.3 slip over the budget once measured at 0.32.)
|
||||
local scale = 0.3 * S
|
||||
local ref = 0.32 * S
|
||||
local budget = JCA.row_width_budget()
|
||||
if JCA.text_width(label .. body, scale) <= budget then
|
||||
if JCA.text_width(label .. body, ref) <= budget then
|
||||
rows[#rows + 1] = {
|
||||
{n = G.UIT.T, config = {text = label, colour = label_colour, scale = scale}},
|
||||
{n = G.UIT.T, config = {text = body,
|
||||
@@ -788,8 +807,8 @@ local function tooltip_rows(card)
|
||||
text = label, colour = label_colour, scale = scale}}}
|
||||
-- The label owns the line above, so EVERY body line is indented here,
|
||||
-- not just continuations -- reserve the indent for all of them.
|
||||
local pad = JCA.text_width(' ', scale)
|
||||
for _, line in ipairs(JCA.wrap_measured(body, scale, budget - pad, '')) do
|
||||
local pad = JCA.text_width(' ', ref)
|
||||
for _, line in ipairs(JCA.wrap_measured(body, ref, budget - pad, '')) do
|
||||
rows[#rows + 1] = {{n = G.UIT.T, config = {text = ' ' .. line,
|
||||
colour = G.C.UI.TEXT_DARK, scale = scale}}}
|
||||
end
|
||||
@@ -807,10 +826,13 @@ local function tooltip_rows(card)
|
||||
end
|
||||
if #labels > 0 then
|
||||
-- One sentence, measured-wrapped, instead of a guessed
|
||||
-- character count: text_row already bounds the width.
|
||||
-- character count: text_row already bounds the width. Rendered at
|
||||
-- the default scale (like the "No synergy" line above it) so its
|
||||
-- wrap holds under the same budget every other row is measured by;
|
||||
-- multi-word labels stay whole so "card enhancing" never splits.
|
||||
local names = {}
|
||||
for _, l in ipairs(labels) do names[#names + 1] = l.name end
|
||||
text_row(prefix .. table.concat(names, ', '), nil, 0.3 * S)
|
||||
for _, l in ipairs(labels) do names[#names + 1] = atomic(l.name) end
|
||||
text_row(prefix .. table.concat(names, ', '))
|
||||
end
|
||||
end
|
||||
else
|
||||
@@ -827,7 +849,7 @@ local function tooltip_rows(card)
|
||||
end
|
||||
if #partners > shown then
|
||||
local rest = {}
|
||||
for i = shown + 1, #partners do rest[#rest + 1] = partners[i].name end
|
||||
for i = shown + 1, #partners do rest[#rest + 1] = atomic(partners[i].name) end
|
||||
text_row('also: ' .. table.concat(rest, ', '), G.C.UI.TEXT_DARK)
|
||||
end
|
||||
if in_buy_area(card) and total >= JCA.config.threshold
|
||||
@@ -873,9 +895,9 @@ local function tooltip_rows(card)
|
||||
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) .. ' -> '
|
||||
via = via .. atomic(name_of(link.config.center.key)) .. ' -> '
|
||||
end
|
||||
local tname = target and name_of(target.config.center.key)
|
||||
local tname = target and atomic(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
|
||||
@@ -901,12 +923,13 @@ local function tooltip_rows(card)
|
||||
local ok, plans = pcall(JCA.fusion_plans, card)
|
||||
for i = 1, (ok and math.min(2, #plans) or 0) do
|
||||
local p = plans[i]
|
||||
local rname = name_of(p.result)
|
||||
local rname = atomic(name_of(p.result))
|
||||
local price = p.cost and (' ($' .. p.cost .. ')') or ''
|
||||
if not p.ready then
|
||||
local want = {}
|
||||
for _, m in ipairs(p.missing) do
|
||||
want[#want + 1] = (m.n > 1 and (m.n .. 'x ') or '') .. name_of(m.key)
|
||||
want[#want + 1] = atomic(
|
||||
(m.n > 1 and (m.n .. 'x ') or '') .. name_of(m.key))
|
||||
end
|
||||
text_row(('Fuses into %s%s - still needs %s')
|
||||
:format(rname, price, table.concat(want, ', ')))
|
||||
@@ -924,7 +947,7 @@ local function tooltip_rows(card)
|
||||
end
|
||||
if #p.loses > 0 then
|
||||
local names = {}
|
||||
for _, k in ipairs(p.loses) do names[#names + 1] = name_of(k) end
|
||||
for _, k in ipairs(p.loses) do names[#names + 1] = atomic(name_of(k)) end
|
||||
text_row('Fusing drops combos with: ' .. table.concat(names, ', '),
|
||||
G.C.RED)
|
||||
end
|
||||
@@ -1018,9 +1041,9 @@ local function tooltip_rows(card)
|
||||
-- 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) .. ' -> '
|
||||
via = via .. atomic(name_of(link.config.center.key)) .. ' -> '
|
||||
end
|
||||
local tname = target and name_of(target.config.center.key)
|
||||
local tname = target and atomic(name_of(target.config.center.key))
|
||||
|
||||
if status == 'ok' then
|
||||
text_row('Copying ' .. via .. tname, G.C.GREEN)
|
||||
@@ -1039,7 +1062,7 @@ local function tooltip_rows(card)
|
||||
if status ~= 'ok' then
|
||||
local best = JCA.best_copy_target(card)
|
||||
if best then
|
||||
local bname = name_of(best.config.center.key)
|
||||
local bname = atomic(name_of(best.config.center.key))
|
||||
text_row(key == 'j_brainstorm'
|
||||
and ('Fix: make ' .. bname .. ' your leftmost joker')
|
||||
or ('Fix: slot this just left of ' .. bname), G.C.GREEN)
|
||||
@@ -1051,7 +1074,7 @@ local function tooltip_rows(card)
|
||||
-- next blind — worth shouting about while there is still time to reorder.
|
||||
local victim, safe = JCA.dagger_victim(card)
|
||||
if victim then
|
||||
local vname = name_of(victim.config.center.key)
|
||||
local vname = atomic(name_of(victim.config.center.key))
|
||||
if safe then
|
||||
text_row(vname .. ' is Eternal - the blade cannot eat it', G.C.GREEN)
|
||||
else
|
||||
@@ -1099,7 +1122,7 @@ local function consumable_rows(card)
|
||||
end
|
||||
if #fed > 0 then
|
||||
local names = {}
|
||||
for _, f in ipairs(fed) do names[#names + 1] = f.name end
|
||||
for _, f in ipairs(fed) do names[#names + 1] = atomic(f.name) end
|
||||
text_row('Feeds: ' .. table.concat(names, ', '))
|
||||
end
|
||||
-- A Standard Pack permanently adds playing cards, and Erosion is paid per
|
||||
|
||||
Reference in New Issue
Block a user