Module:ItemBox: Difference between revisions

From The Deadlock Wiki
Jump to navigation Jump to search
Vergir (talk | contribs)
Add variants() for stable per-item texture variety (replaces nth-child); add font-table regen recipe to header (with help from vergir-bot LLM)
Vergir (talk | contribs)
m comments update
Line 4: Line 4:
-- ============================================================================
-- ============================================================================
--  Retail Demo (bold) glyph advance widths, in em of the font size.
--  Retail Demo (bold) glyph advance widths, in em of the font size.
--  unitsPerEm = 1000. Measured from File:Retaildemo-bold.woff2
--  unitsPerEm = 1000. Measured from /images/d/d5/Retaildemo-bold.woff2
--  ( /images/d/d5/Retaildemo-bold.woff2 ), the bold face used by .itembox-name.
--   Table can be regenerated with fontforge / fonttools
--
-- THIS TABLE IS FONT-SPECIFIC. If that file is re-uploaded with different
--  metrics, name fitting drifts. Regenerate the table with:
--
--    curl -o rd.woff2 https://deadlock.wiki/images/d/d5/Retaildemo-bold.woff2
--    fontforge -lang=py -c 'import fontforge;f=fontforge.open("rd.woff2");f.generate("rd.ttf")'
--    python3 - << 'PY'
--    from fontTools.ttLib import TTFont; import string
--    f=TTFont("rd.ttf"); upm=f["head"].unitsPerEm; hm=f["hmtx"]; uni={}
--    for t in f["cmap"].tables:
--        if t.isUnicode():
--            for cp,gn in t.cmap.items(): uni.setdefault(cp,gn)
--    for c in string.ascii_uppercase+string.ascii_lowercase+string.digits+" ":
--        gn=uni.get(ord(c))
--        if gn: print(f'    ["{c}"]={hm[gn][0]/upm:.3f},')
--    PY
-- ============================================================================
-- ============================================================================
local WIDTH = {
local WIDTH = {
Line 120: Line 104:
end
end


-- Stable per-item texture variety, replacing position-dependent :nth-child.
-- User for stable per-item texture variety
-- A djb2 hash of the (language-independent) code name picks one of each of the
-- icon-mask / wear / paper-texture variants. Same item -> same look everywhere.
local function hash(s)
local function hash(s)
     local h = 5381
     local h = 5381

Revision as of 18:04, 20 July 2026

Documentation for this module may be created at Module:ItemBox/doc

local p = {}
local ustring = mw.ustring

-- ============================================================================
--  Retail Demo (bold) glyph advance widths, in em of the font size.
--  unitsPerEm = 1000. Measured from /images/d/d5/Retaildemo-bold.woff2
--   Table can be regenerated with fontforge / fonttools
-- ============================================================================
local WIDTH = {
    A=0.677, B=0.590, C=0.621, D=0.693, E=0.552, F=0.530, G=0.686, H=0.702,
    I=0.276, J=0.301, K=0.637, L=0.491, M=0.904, N=0.695, O=0.741, P=0.585,
    Q=0.740, R=0.628, S=0.555, T=0.555, U=0.694, V=0.668, W=0.913, X=0.667,
    Y=0.635, Z=0.666,
    a=0.573, b=0.608, c=0.473, d=0.604, e=0.570, f=0.382, g=0.558, h=0.608,
    i=0.269, j=0.266, k=0.584, l=0.268, m=0.907, n=0.602, o=0.611, p=0.604,
    q=0.605, r=0.392, s=0.496, t=0.404, u=0.602, v=0.549, w=0.776, x=0.571,
    y=0.550, z=0.561,
    ["0"]=0.662, ["1"]=0.312, ["2"]=0.560, ["3"]=0.566, ["4"]=0.612,
    ["5"]=0.562, ["6"]=0.601, ["7"]=0.520, ["8"]=0.583, ["9"]=0.598,
    [" "]=0.240,
}

-- Characters absent from Retail Demo (hyphen, apostrophe, ...) are drawn by the
-- browser in the fallback sans-serif; this is a reasonable stand-in width.
local FALLBACK = 0.60

-- Card geometry in px at the reference scale where 1em = 10px. The card is
-- scaled through font-size on .itembox (the |size= parameter) and the value
-- returned here is em, so it scales together with everything else.
local USABLE_W = 72    -- 80px card minus 4px padding on each side
local NAME_H   = 42    -- vertical room the wrapped name may occupy
local LINE_H   = 1.2   -- must match .itembox-name line-height
local MAX_PX   = 15    -- full size  (1.50em)
local MIN_PX   = 8     -- hard floor (0.80em)

local function charWidth(ch)
    local w = WIDTH[ch]
    if w then return w end
    local cp = ustring.codepoint(ch) or 0
    -- CJK / full-width ranges render roughly twice as wide as Latin
    if (cp >= 0x1100 and cp <= 0x115F) or (cp >= 0x2E80 and cp <= 0xA4CF)
        or (cp >= 0xAC00 and cp <= 0xD7A3) or (cp >= 0xF900 and cp <= 0xFAFF)
        or (cp >= 0xFF00 and cp <= 0xFF60) or (cp >= 0xFFE0 and cp <= 0xFFE6) then
        return 2.0
    end
    return FALLBACK
end

local function wordWidth(word)
    local total = 0
    for ch in ustring.gmatch(word, ".") do
        total = total + charWidth(ch)
    end
    return total
end

-- Greedy line count when filling USABLE_W at font size `px`. Close enough to
-- text-wrap:balance for sizing, since both keep every word intact.
local function lineCount(words, px)
    local space = charWidth(" ") * px
    local cur, lines = 0, 1
    for _, w in ipairs(words) do
        local ww = wordWidth(w) * px
        if cur == 0 then
            cur = ww
        elseif cur + space + ww <= USABLE_W then
            cur = cur + space + ww
        else
            lines = lines + 1
            cur = ww
        end
    end
    return lines
end

-- Returns the name font size as an em string (scales with the card).
function p.getFontSize(frame)
    local name = ustring.gsub(frame.args[1] or "", "^%s*(.-)%s*$", "%1")
    if name == "" then return string.format("%.2f", MAX_PX / 10) end

    local words = {}
    for w in ustring.gmatch(name, "%S+") do words[#words + 1] = w end

    local widest = 0
    for _, w in ipairs(words) do
        local ww = wordWidth(w)
        if ww > widest then widest = ww end
    end

    local px = USABLE_W / widest
    if px > MAX_PX then px = MAX_PX end

    while px > MIN_PX and lineCount(words, px) * LINE_H * px > NAME_H do
        px = px - 0.25
    end
    if px < MIN_PX then px = MIN_PX end

    local snapped = math.floor(px * 2 + 0.5) / 2
    if lineCount(words, snapped) * LINE_H * snapped > NAME_H then
        snapped = math.floor(px * 2) / 2
    end

    return string.format("%.2f", snapped / 10)
end

-- User for stable per-item texture variety
local function hash(s)
    local h = 5381
    for i = 1, #s do
        h = (h * 33 + s:byte(i)) % 2147483647
    end
    return h
end

function p.variants(frame)
    local h = hash(frame.args[1] or "")
    local icon = (h % 3) + 1
    local wear = (math.floor(h / 3) % 4) + 1
    local tex  = (math.floor(h / 12) % 3) + 1
    return string.format("itembox-icon-v%d itembox-wear-v%d itembox-texture-v%d",
        icon, wear, tex)
end

return p