diff --git a/CHANGELOG.md b/CHANGELOG.md index aead5c8..4c4dda4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/main.lua b/main.lua index af11cb6..612d49a 100644 --- a/main.lua +++ b/main.lua @@ -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