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
|
Settings) instead of a button in the run HUD, which keeps the play screen
|
||||||
clear. Same overlay, same tabs; Mods > Combo Advisor is unchanged.
|
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
|
### Fusion Jokers integration
|
||||||
- When [Fusion Jokers](https://github.com/wingedcatgirl/Fusion-Jokers) is
|
- When [Fusion Jokers](https://github.com/wingedcatgirl/Fusion-Jokers) is
|
||||||
installed, hovering any joker that is a fusion component names the fusion it
|
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
|
return (type(room) == 'number' and room > 0 and room or 20) * ROW_WIDTH_FRACTION
|
||||||
end
|
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
|
-- Word-wrap `str` to the measured budget, returning finished lines with the
|
||||||
-- hanging `indent` already applied to continuations.
|
-- 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
|
for word in tostring(str):gmatch('%S+') do
|
||||||
local nxt = line and (line .. ' ' .. word) or word
|
local nxt = line and (line .. ' ' .. word) or word
|
||||||
local limit = #lines == 0 and budget or budget - pad
|
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
|
lines[#lines + 1] = line
|
||||||
line = word
|
line = word
|
||||||
else
|
else
|
||||||
@@ -631,6 +643,7 @@ function JCA.wrap_measured(str, scale, budget, indent)
|
|||||||
if line then lines[#lines + 1] = line end
|
if line then lines[#lines + 1] = line end
|
||||||
if #lines == 0 then return {''} end
|
if #lines == 0 then return {''} end
|
||||||
for i = 2, #lines do lines[i] = indent .. lines[i] 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
|
return lines
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -774,9 +787,15 @@ local function tooltip_rows(card)
|
|||||||
-- body wraps beneath it, which keeps the colour split readable instead of
|
-- body wraps beneath it, which keeps the colour split readable instead of
|
||||||
-- splitting a coloured phrase mid-word across rows.
|
-- splitting a coloured phrase mid-word across rows.
|
||||||
local function labelled_row(label, label_colour, body)
|
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 scale = 0.3 * S
|
||||||
|
local ref = 0.32 * S
|
||||||
local budget = JCA.row_width_budget()
|
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] = {
|
rows[#rows + 1] = {
|
||||||
{n = G.UIT.T, config = {text = label, colour = label_colour, scale = scale}},
|
{n = G.UIT.T, config = {text = label, colour = label_colour, scale = scale}},
|
||||||
{n = G.UIT.T, config = {text = body,
|
{n = G.UIT.T, config = {text = body,
|
||||||
@@ -788,8 +807,8 @@ local function tooltip_rows(card)
|
|||||||
text = label, colour = label_colour, scale = scale}}}
|
text = label, colour = label_colour, scale = scale}}}
|
||||||
-- The label owns the line above, so EVERY body line is indented here,
|
-- The label owns the line above, so EVERY body line is indented here,
|
||||||
-- not just continuations -- reserve the indent for all of them.
|
-- not just continuations -- reserve the indent for all of them.
|
||||||
local pad = JCA.text_width(' ', scale)
|
local pad = JCA.text_width(' ', ref)
|
||||||
for _, line in ipairs(JCA.wrap_measured(body, scale, budget - pad, '')) do
|
for _, line in ipairs(JCA.wrap_measured(body, ref, budget - pad, '')) do
|
||||||
rows[#rows + 1] = {{n = G.UIT.T, config = {text = ' ' .. line,
|
rows[#rows + 1] = {{n = G.UIT.T, config = {text = ' ' .. line,
|
||||||
colour = G.C.UI.TEXT_DARK, scale = scale}}}
|
colour = G.C.UI.TEXT_DARK, scale = scale}}}
|
||||||
end
|
end
|
||||||
@@ -807,10 +826,13 @@ local function tooltip_rows(card)
|
|||||||
end
|
end
|
||||||
if #labels > 0 then
|
if #labels > 0 then
|
||||||
-- One sentence, measured-wrapped, instead of a guessed
|
-- 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 = {}
|
local names = {}
|
||||||
for _, l in ipairs(labels) do names[#names + 1] = l.name end
|
for _, l in ipairs(labels) do names[#names + 1] = atomic(l.name) end
|
||||||
text_row(prefix .. table.concat(names, ', '), nil, 0.3 * S)
|
text_row(prefix .. table.concat(names, ', '))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
@@ -827,7 +849,7 @@ local function tooltip_rows(card)
|
|||||||
end
|
end
|
||||||
if #partners > shown then
|
if #partners > shown then
|
||||||
local rest = {}
|
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)
|
text_row('also: ' .. table.concat(rest, ', '), G.C.UI.TEXT_DARK)
|
||||||
end
|
end
|
||||||
if in_buy_area(card) and total >= JCA.config.threshold
|
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 target, status, chain = JCA.copy_source_if_bought(key)
|
||||||
local via = ''
|
local via = ''
|
||||||
for _, link in ipairs(chain) do
|
for _, link in ipairs(chain) do
|
||||||
via = via .. name_of(link.config.center.key) .. ' -> '
|
via = via .. atomic(name_of(link.config.center.key)) .. ' -> '
|
||||||
end
|
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
|
if status == 'ok' then
|
||||||
text_row('Will copy ' .. via .. tname .. ' as soon as it lands', G.C.GREEN)
|
text_row('Will copy ' .. via .. tname .. ' as soon as it lands', G.C.GREEN)
|
||||||
elseif status == 'incompatible' then
|
elseif status == 'incompatible' then
|
||||||
@@ -901,12 +923,13 @@ local function tooltip_rows(card)
|
|||||||
local ok, plans = pcall(JCA.fusion_plans, card)
|
local ok, plans = pcall(JCA.fusion_plans, card)
|
||||||
for i = 1, (ok and math.min(2, #plans) or 0) do
|
for i = 1, (ok and math.min(2, #plans) or 0) do
|
||||||
local p = plans[i]
|
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 ''
|
local price = p.cost and (' ($' .. p.cost .. ')') or ''
|
||||||
if not p.ready then
|
if not p.ready then
|
||||||
local want = {}
|
local want = {}
|
||||||
for _, m in ipairs(p.missing) do
|
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
|
end
|
||||||
text_row(('Fuses into %s%s - still needs %s')
|
text_row(('Fuses into %s%s - still needs %s')
|
||||||
:format(rname, price, table.concat(want, ', ')))
|
:format(rname, price, table.concat(want, ', ')))
|
||||||
@@ -924,7 +947,7 @@ local function tooltip_rows(card)
|
|||||||
end
|
end
|
||||||
if #p.loses > 0 then
|
if #p.loses > 0 then
|
||||||
local names = {}
|
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, ', '),
|
text_row('Fusing drops combos with: ' .. table.concat(names, ', '),
|
||||||
G.C.RED)
|
G.C.RED)
|
||||||
end
|
end
|
||||||
@@ -1018,9 +1041,9 @@ local function tooltip_rows(card)
|
|||||||
-- Blueprint", which is the one answer that helps nobody.
|
-- Blueprint", which is the one answer that helps nobody.
|
||||||
local via = ''
|
local via = ''
|
||||||
for _, link in ipairs(chain) do
|
for _, link in ipairs(chain) do
|
||||||
via = via .. name_of(link.config.center.key) .. ' -> '
|
via = via .. atomic(name_of(link.config.center.key)) .. ' -> '
|
||||||
end
|
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
|
if status == 'ok' then
|
||||||
text_row('Copying ' .. via .. tname, G.C.GREEN)
|
text_row('Copying ' .. via .. tname, G.C.GREEN)
|
||||||
@@ -1039,7 +1062,7 @@ local function tooltip_rows(card)
|
|||||||
if status ~= 'ok' then
|
if status ~= 'ok' then
|
||||||
local best = JCA.best_copy_target(card)
|
local best = JCA.best_copy_target(card)
|
||||||
if best then
|
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'
|
text_row(key == 'j_brainstorm'
|
||||||
and ('Fix: make ' .. bname .. ' your leftmost joker')
|
and ('Fix: make ' .. bname .. ' your leftmost joker')
|
||||||
or ('Fix: slot this just left of ' .. bname), G.C.GREEN)
|
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.
|
-- next blind — worth shouting about while there is still time to reorder.
|
||||||
local victim, safe = JCA.dagger_victim(card)
|
local victim, safe = JCA.dagger_victim(card)
|
||||||
if victim then
|
if victim then
|
||||||
local vname = name_of(victim.config.center.key)
|
local vname = atomic(name_of(victim.config.center.key))
|
||||||
if safe then
|
if safe then
|
||||||
text_row(vname .. ' is Eternal - the blade cannot eat it', G.C.GREEN)
|
text_row(vname .. ' is Eternal - the blade cannot eat it', G.C.GREEN)
|
||||||
else
|
else
|
||||||
@@ -1099,7 +1122,7 @@ local function consumable_rows(card)
|
|||||||
end
|
end
|
||||||
if #fed > 0 then
|
if #fed > 0 then
|
||||||
local names = {}
|
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, ', '))
|
text_row('Feeds: ' .. table.concat(names, ', '))
|
||||||
end
|
end
|
||||||
-- A Standard Pack permanently adds playing cards, and Erosion is paid per
|
-- A Standard Pack permanently adds playing cards, and Erosion is paid per
|
||||||
|
|||||||
Reference in New Issue
Block a user