Modul:Firestone: Unterschied zwischen den Versionen
Keine Bearbeitungszusammenfassung |
Keine Bearbeitungszusammenfassung |
||
| (18 dazwischenliegende Versionen desselben Benutzers werden nicht angezeigt) | |||
| Zeile 1: | Zeile 1: | ||
-- Modul:Firestone | -- Modul:Firestone | ||
local M = {} | local M = {} | ||
-- | |||
-- ========== kleine Helfer ========== | |||
local COL_OFFER, COL_PRICE, COL_LIMIT = "50%", "25%", "25%" | local COL_OFFER, COL_PRICE, COL_LIMIT = "50%", "25%", "25%" | ||
local DAY = 86400 | |||
local function parse_iso_date(s) | |||
local y,m,d = tostring(s or ""):match("^(%d%d%d%d)%-(%d%d)%-(%d%d)$") | |||
if not y then return nil end | |||
return os.time{ year=tonumber(y), month=tonumber(m), day=tonumber(d), hour=12 } | |||
end | |||
local function ts_to_mw(ts) return os.date("!%Y%m%d%H%M%S", ts) end | |||
local function format_date_de(v) | |||
local ts = (type(v)=="number") and v or parse_iso_date(v) | |||
if not ts then return "" end | |||
local lang = mw.getContentLanguage() | |||
return lang:formatDate("d.m.Y", ts_to_mw(ts)) | |||
end | |||
local function isempty(v) return v == nil or v == "" end | local function isempty(v) return v == nil or v == "" end | ||
local function norm(s) if s==nil then return "" end s=mw.text.trim(tostring(s)) | local function norm(s) | ||
if s == nil then return "" end | |||
s = mw.text.trim(tostring(s)) | |||
return mw.ustring.lower(s):gsub("%s+"," ") | |||
end | |||
-- bevorzugt Parent-Args ( | -- bevorzugt Parent-Args (Vorlagen) | ||
local function getArgs(frame) | local function getArgs(frame) | ||
local p = frame:getParent() | local p = frame:getParent() | ||
local src = (p and p.args) or frame.args or {} | local src = (p and p.args) or frame.args or {} | ||
local a = {} | local a = {} | ||
for k,v in pairs(src) do if v ~= nil and v ~= "" then a[k] = mw.text.trim(tostring(v)) end end | for k,v in pairs(src) do | ||
if v ~= nil and v ~= "" then a[k] = mw.text.trim(tostring(v)) end | |||
end | |||
return a | return a | ||
end | |||
local function has_items(t) | |||
if type(t) ~= "table" then return false end | |||
for _ in pairs(t) do return true end | |||
return false | |||
end | end | ||
| Zeile 40: | Zeile 65: | ||
if isempty(file) then return "" end | if isempty(file) then return "" end | ||
local parts = { "Datei:"..file } | local parts = { "Datei:"..file } | ||
if opts and opts.size | if opts and opts.size then table.insert(parts, opts.size) end | ||
if opts and opts.param | if opts and opts.param then table.insert(parts, opts.param) end -- z.B. "thumb|Text" | ||
return string.format("[[%s]]", table.concat(parts, "|")) | return string.format("[[%s]]", table.concat(parts, "|")) | ||
end | end | ||
local function | local function pretty_range(s) | ||
if | if not s or s=="" then return "" end | ||
s = tostring(s) | |||
return | if s:sub(-1) == "-" then return s:sub(1, -2) .. "+" end | ||
return s:gsub("%-","–") | |||
end | end | ||
-- ========= | -- ========== Lazy-Loads ========== | ||
local I18N, EVENTS, HEROES | local I18N, EVENTS, HEROES | ||
local function i18n() | local function i18n() | ||
| Zeile 59: | Zeile 85: | ||
local ok2, mod = pcall(require, t) ; if ok2 and type(mod) =="table" then I18N=mod ; return I18N end | local ok2, mod = pcall(require, t) ; if ok2 and type(mod) =="table" then I18N=mod ; return I18N end | ||
end | end | ||
I18N = { i18n={}, events={ names={}, sections={default={}}, | I18N = { i18n={}, events={ names={}, sections={default={}}, phrases={}, links={}, unlocks={} } } | ||
return I18N | return I18N | ||
end | end | ||
| Zeile 79: | Zeile 105: | ||
end | end | ||
-- ========= i18n | -- ========== i18n / Labels ========== | ||
local function ev_label(evkey, which) | local function ev_label(evkey, which) | ||
local E = i18n().events or {} | local E = i18n().events or {} | ||
| Zeile 90: | Zeile 116: | ||
return names[norm(evkey)] or evkey | return names[norm(evkey)] or evkey | ||
end | end | ||
local function header_labels(evkey) | local function header_labels(evkey) | ||
return ev_label(evkey, "offer"), ev_label(evkey, "price"), ev_label(evkey, "limit") | return ev_label(evkey, "offer"), ev_label(evkey, "price"), ev_label(evkey, "limit") | ||
end | end | ||
local function autotr(key_name, value) | local function autotr(key_name, value) | ||
if type(value) ~= "string" then return value end | if type(value) ~= "string" then return value end | ||
local | local k = norm(key_name) | ||
return ( | |||
-- 1) Standardwortschatz (falls vorhanden) | |||
local m1 = (i18n().i18n or {})[k] | |||
local v = m1 and m1[norm(value)] | |||
if v then return v end | |||
-- 2) Spezialfall Event-Typen: i18n().events.infobox.type | |||
if k == "type" then | |||
local m2 = (((i18n().events or {}).infobox) or {}).type or {} | |||
return m2[norm(value)] or value | |||
end | |||
return value | |||
end | |||
local function ev_notice_icon_wikitext(ev) | |||
local file = ev.currency and ev.currency.icon | |||
if not file or file == "" then return "" end | |||
local img = fileTag(file, { size = "24px", param = "frameless|link=" }) | |||
return '<span class="fs-ic">' .. img .. '</span>' | |||
end | end | ||
-- ========= Event | local function ev_pagelink(ev, evkey, label) | ||
local page = ev.page | |||
if page and page ~= "" then | |||
return string.format("[[%s|%s]]", page, label) | |||
end | |||
return label | |||
end | |||
-- ========== Nummern + <ref> (voll i18n-gesteuert) ========== | |||
local function parse_sci(v) | |||
if type(v) == "number" then return v end | |||
if type(v) == "string" then | |||
local a,e = v:match("^%s*([%d%.]+)[eE]([%+%-]?%d+)%s*$") | |||
if a and e then return (tonumber(a) or 0) * 10^(tonumber(e) or 0) end | |||
local n = tonumber(v); if n then return n end | |||
end | |||
return nil | |||
end | |||
local function i18n_big_units() | |||
local N = (i18n().numbers or {}) | |||
return { | |||
{1e12, N.trillion_sing, N.trillion_plur}, | |||
{1e9, N.billion_sing, N.billion_plur}, | |||
{1e6, N.million_sing, N.million_plur}, | |||
{1e3, N.thousand_sing, N.thousand_plur}, | |||
} | |||
end | |||
local function format_big(v) | |||
local n = parse_sci(v); if not n then return tostring(v or "") end | |||
for _,u in ipairs(i18n_big_units()) do | |||
local base, sing, plur = u[1], u[2], u[3] | |||
if base and sing and plur and n >= base and math.floor(n % base) == 0 then | |||
local q = math.floor(n / base) | |||
return string.format("%d %s", q, (q==1) and sing or plur) | |||
end | |||
end | |||
return tostring(n) | |||
end | |||
local function ref_gear_power(v) | |||
if isempty(v) then return "" end | |||
local phr = (((i18n().events or {}).phrases) or {}).req_gear_power | |||
if isempty(phr) then return "" end | |||
local name = "gear-" .. tostring(v):gsub("%s","") | |||
return string.format('<ref name="%s">%s</ref>', name, (phr:gsub("$1", format_big(v)))) | |||
end | |||
-- ========== Event-Renderer ========== | |||
local function htag(level, text) | local function htag(level, text) | ||
if isempty(text) then return "" end | if isempty(text) then return "" end | ||
| Zeile 132: | Zeile 220: | ||
if which=="type" then | if which=="type" then | ||
local E = i18n().events or {} | local E = i18n().events or {} | ||
local page = (E.links and E.links.type and E.links.type[ev.type or ""]) or (E.links and E.links.type) or "Calendar Events" | local page = (E.links and E.links.type and E.links.type[ev.type or ""]) | ||
local map | or (E.links and E.links.type) or "Calendar Events" | ||
local label= map[norm(ev.type or "")] or (ev.type or "") | local map = (E.infobox and E.infobox.type) or {} | ||
local label = map[norm(ev.type or "")] or (ev.type or "") | |||
return string.format("[[%s|%s]]", page, label) | return string.format("[[%s|%s]]", page, label) | ||
end | end | ||
| Zeile 141: | Zeile 230: | ||
local function event_history(ev) | local function event_history(ev) | ||
local H = ev.history or {} | local H = ev.history or {} | ||
if not has_items(H) then return "" end | |||
local alt_gp = ((i18n().events or {}).phrases or {}).alt_platform or "auf Google Play" | local alt_gp = ((i18n().events or {}).phrases or {}).alt_platform or "auf Google Play" | ||
local | |||
local function fmt_range_de(s1, s2) | |||
return string.format("%s bis %s", format_date_de(s1), format_date_de(s2)) | |||
if | end | ||
-- "2023-10-24 bis 2023-11-07" → "24.10.2023 bis 07.11.2023" | |||
local function fmt_alt_platform(val) | |||
local y1,m1,d1,y2,m2,d2 = | |||
tostring(val or ""):match("^%s*(%d%d%d%d)%-(%d%d)%-(%d%d)%s+bis%s+(%d%d%d%d)%-(%d%d)%-(%d%d)%s*$") | |||
if y1 then | |||
return fmt_range_de(y1.."-"..m1.."-"..d1, y2.."-"..m2.."-"..d2) | |||
end | |||
-- falls kein ISO-Bereich erkannt wurde: unverändert ausgeben | |||
return tostring(val or "") | |||
end | end | ||
local out = {} | |||
local | for _,h in ipairs(H) do | ||
local year = tostring(h.year or "") | |||
local s = h.start or "" | |||
local e = h["end"] or "" | |||
-- Start/Ende immer DE-formatiert | |||
local line = string.format("* %s: %s", year, fmt_range_de(s, e)) | |||
-- optionaler Alternativ-Bereich (auch DE-formatiert) | |||
if h.alt_platform then | |||
if | line = line .. string.format(" (%s %s)", fmt_alt_platform(h.alt_platform), alt_gp) | ||
end | end | ||
table.insert(out, line) | |||
end | end | ||
return | return table.concat(out, "\n") | ||
end | end | ||
| Zeile 199: | Zeile 277: | ||
end | end | ||
-- references/render | |||
local FRAME = nil | |||
local function tabber_render_from_content(content) | |||
if FRAME and FRAME.extensionTag then | |||
return FRAME:extensionTag('tabber', content) | |||
local function | |||
if | |||
end | end | ||
return | return "<tabber>\n" .. content .. "\n</tabber>" | ||
end | end | ||
local function references_tag() | |||
if FRAME and FRAME.extensionTag then | |||
return FRAME:extensionTag('references', '') | |||
local function | |||
end | end | ||
return | return '<references />' | ||
end | end | ||
-- | -- innerer Tab-Inhalt: je Range | ||
local function build_tabber_content_for(list) | |||
local function build_tabber_content_for(list | |||
local out = {} | local out = {} | ||
for _,it in ipairs(list) do | for _,it in ipairs(list) do | ||
local label = pretty_range(it.range or "") | local label = pretty_range(it.range or "") | ||
local note = ref_gear_power(it.req_gear_power | local note = ref_gear_power(it.req_gear_power) | ||
local content = string.format( | local content = string.format( | ||
"%s %s", | "%s %s%s", | ||
fileTag(it.icon or "", { size="30px" }), | fileTag(it.icon or "", { size="30px" }), | ||
(it.item and item_label(it.item, it.amount or 1)) or (it.title or ""), | (it.item and item_label(it.item, it.amount or 1)) or (it.title or ""), | ||
| Zeile 274: | Zeile 309: | ||
end | end | ||
-- Gruppen-Label aus i18n | |||
-- | local function group_label(kind) | ||
local function | local G = ((i18n().events or {}).group_labels or {}) | ||
return G[kind] or kind | |||
return | |||
end | end | ||
local function | -- Baut die Avatar-Tabelle für einen Jahrgang | ||
local function build_avatar_table(list, token_icon, token_name) | |||
local t = {} | |||
-- | t[#t+1] = '{| class="article-table" style="font-size:14px;"' | ||
t[#t+1] = '! colspan="2" | Avatar\n! Preis' | |||
for _,it in ipairs(list) do | for _,it in ipairs(list) do | ||
t[#t+1] = '|-' | |||
t[#t+1] = string.format( | |||
'| %s || %s || %s %s %s', | |||
fileTag(it.file, { size="50px" }), | |||
it.title or "", | |||
(it. | fileTag(token_icon, { size="25px" }), | ||
tostring(it.cost or 0), | |||
token_name | |||
) | ) | ||
end | end | ||
t[#t+1] = '|}' | |||
return table.concat(t, "\n") | |||
end | |||
-- Avatare als TabberNeue: ein Tab pro Jahr | |||
local | local function event_avatars(ev, evkey) | ||
local AV = ev.avatars | |||
if not has_items(AV) then return "" end | |||
local | local token_icon = (ev.currency or {}).icon or "" | ||
local token_name = ev_label(evkey, "currencies") | |||
-- Jahre einsammeln und sortieren (aufsteigend) | |||
local years = {} | |||
for y,_ in pairs(AV) do years[#years+1] = tostring(y) end | |||
table.sort(years, function(a,b) return a > b end) | |||
-- Hinweis: Wenn du das neueste Jahr zuerst willst: | |||
-- table.sort(years, function(a,b) return a > b end) | |||
-- | -- Tab-Inhalt aufbauen | ||
local parts = {} | |||
for _,year in ipairs(years) do | |||
local list = AV[year] or AV[tonumber(year)] | |||
if has_items(list) then | |||
local table_markup = build_avatar_table(list, token_icon, token_name) | |||
parts[#parts+1] = "|-|" .. year .. "=\n" .. table_markup | |||
end | end | ||
end | end | ||
-- Als <tabber> rendern (nutzt deine tabber_render_from_content) | |||
-- | return tabber_render_from_content(table.concat(parts, "\n")) | ||
return | |||
end | end | ||
-- | -- Austausch-Tabelle (äußerer Tab: Gruppen → innerer Tab: Ranges) | ||
local function event_exchange_tabs(ev, evkey) | local function event_exchange_tabs(ev, evkey) | ||
local ex = ev.exchange or {} | local ex = ev.exchange or {} | ||
| Zeile 343: | Zeile 371: | ||
if not (has_items(ex.chests_by_level) | if not (has_items(ex.chests_by_level) | ||
or has_items(ex.chests_by_stars) | |||
or has_items(ex.chests_by_oracle) | |||
or has_items(ex.currencies)) then | |||
return "" | return "" | ||
end | end | ||
local outer_parts = {} | local outer_parts = {} | ||
local function add_group(kind, list) | local function add_group(kind, list) | ||
if not has_items(list) then return end | if not has_items(list) then return end | ||
local inner_content = build_tabber_content_for(list) | |||
local inner_content = build_tabber_content_for(list | local inner_tabber = tabber_render_from_content(inner_content) | ||
local inner_tabber = tabber_render_from_content(inner_content | |||
outer_parts[#outer_parts+1] = "|-|" .. group_label(kind) .. "=\n" .. inner_tabber | outer_parts[#outer_parts+1] = "|-|" .. group_label(kind) .. "=\n" .. inner_tabber | ||
end | end | ||
add_group("level", ex.chests_by_level) | add_group("level", ex.chests_by_level) | ||
add_group("stars", ex.chests_by_stars) | add_group("stars", ex.chests_by_stars) | ||
add_group("oracle", ex.chests_by_oracle) | add_group("oracle", ex.chests_by_oracle) | ||
local left_tabber = tabber_render_from_content(table.concat(outer_parts, "\n")) | local left_tabber = tabber_render_from_content(table.concat(outer_parts, "\n")) | ||
-- Preis/Limit | -- Preis/Limit (erste gefundene Gruppe) | ||
local function pick_price_limit() | local function pick_price_limit() | ||
if has_items(ex.chests_by_level) then return ex.chests_by_level[1].price or 0, ex.chests_by_level[1].limit or "" end | if has_items(ex.chests_by_level) then return ex.chests_by_level[1].price or 0, ex.chests_by_level[1].limit or "" end | ||
| Zeile 378: | Zeile 399: | ||
local price, limit = pick_price_limit() | local price, limit = pick_price_limit() | ||
local t = {} | local t = {} | ||
local H_offer, H_price, H_limit = header_labels(evkey) | local H_offer, H_price, H_limit = header_labels(evkey) | ||
t[#t+1] = '{| class="article-table fs-exchange" style="font-size:14px;"' | |||
t[#t+1] = '{| class="article-table" style="font-size:14px;"' | |||
t[#t+1] = string.format( | t[#t+1] = string.format( | ||
'! style="width:%s;" | %s !! style="width:%s;" | %s !! style="width:%s;" | %s', | '! style="width:%s;" | %s !! style="width:%s;" | %s !! style="width:%s;" | %s', | ||
COL_OFFER, H_offer, COL_PRICE, H_price, COL_LIMIT, H_limit | COL_OFFER, H_offer, COL_PRICE, H_price, COL_LIMIT, H_limit | ||
) | ) | ||
t[#t+1] = '|-' | t[#t+1] = '|-' | ||
t[#t+1] = '|' .. left_tabber .. string.format( | t[#t+1] = '|' .. left_tabber .. string.format( | ||
| Zeile 396: | Zeile 414: | ||
) | ) | ||
if has_items(ex.currencies) then | if has_items(ex.currencies) then | ||
for _,it in ipairs(ex.currencies) do | for _,it in ipairs(ex.currencies) do | ||
| Zeile 415: | Zeile 432: | ||
t[#t+1] = '|}' | t[#t+1] = '|}' | ||
t[#t+1] = | t[#t+1] = references_tag() | ||
return '\n' .. table.concat(t, '\n') | return '\n' .. table.concat(t, '\n') | ||
end | end | ||
| Zeile 443: | Zeile 459: | ||
end | end | ||
-- ========= | -- ========== SiteNotice-Support (Datum/Auto-Pick) ========== | ||
local function event_window(ev) | |||
local S = ev.schedule or ev | |||
local s = S.start_date; if isempty(s) then return nil end | |||
local st = parse_iso_date(s); if not st then return nil end | |||
local en_excl, en_incl | |||
if not isempty(S.end_date) then | |||
-- end_date wird als inklusives Kalenderdatum verstanden | |||
en_incl = parse_iso_date(S.end_date) | |||
if not en_incl then return st, nil, nil end | |||
en_excl = en_incl + DAY | |||
else | |||
-- Dauer in Tagen (Standard 14), exklusives Ende = st + d*DAY | |||
local d = tonumber(S.duration_days) or 14 | |||
en_excl = st + d * DAY | |||
en_incl = en_excl - DAY | |||
end | |||
return st, en_excl, en_incl | |||
end | |||
local function pick_notice_eventkey() | |||
local cfg = ((i18n().events or {}).notice or {}) | |||
local pre = tonumber(cfg.pre_days) or 7 | |||
local post = tonumber(cfg.post_days) or 3 | |||
local now = os.time() | |||
local running, upcoming, ended_recent = {}, {}, {} | |||
for k, ev in pairs(events()) do | |||
local st, en_excl = event_window(ev) | |||
if st and en_excl then | |||
if now >= st and now < en_excl then | |||
table.insert(running, { k=k, st=st, en=en_excl }) | |||
elseif now >= st - pre*DAY and now < st then | |||
table.insert(upcoming, { k=k, st=st, en=en_excl }) | |||
elseif now >= en_excl and now < en_excl + post*DAY then | |||
table.insert(ended_recent, { k=k, st=st, en=en_excl }) | |||
end | |||
end | |||
end | |||
local function by_end(a,b) return a.en < b.en end | |||
local function by_start(a,b) return a.st < b.st end | |||
if #running > 0 then table.sort(running, by_end) ; return running[1].k end | |||
if #upcoming > 0 then table.sort(upcoming, by_start) ; return upcoming[1].k end | |||
if #ended_recent > 0 then table.sort(ended_recent, by_end) ; return ended_recent[1].k end | |||
return nil | |||
end | |||
local function event_notice(ev, evkey) | |||
local phrases = (((i18n().events or {}).phrases) or {}) | |||
local PH_RUNNING = phrases.event_notice -- "Das $1 läuft von $2 bis $3" | |||
local PH_ENDED = phrases.event_ended -- "Das $1 ist beendet. Du kannst verbleibende $4 noch im $5 eintauschen." | |||
-- Ohne Texte kein Hinweis | |||
if isempty(PH_RUNNING) then return "" end | |||
local st, en_excl, en_incl = event_window(ev); if not st or not en_excl then return "" end | |||
local cfg = ((i18n().events or {}).notice or {}) | |||
local pre = tonumber(cfg.pre_days) or 7 -- z. B. 7 Tage vorher | |||
local post = tonumber(cfg.post_days) or 3 -- z. B. 3 Tage danach | |||
local now = os.time() | |||
local show_from = st - pre * DAY | |||
local show_till = en_excl + post * DAY | |||
if now < show_from or now >= show_till then | |||
return "" | |||
end | |||
-- Anzeigename (aus i18n sections oder names) | |||
local name_label = ev_label(evkey, "eventname") | |||
if isempty(name_label) then name_label = ev_name(evkey) end | |||
-- Verlinken NUR wenn ev.page existiert | |||
local name = ev_pagelink(ev, evkey, name_label) | |||
-- Icon aus EventData → currency.icon | |||
local ic = ev_notice_icon_wikitext(ev) -- "" wenn kein currency.icon | |||
local text | |||
if now < en_excl then | |||
-- läuft / startet bald | |||
text = PH_RUNNING | |||
:gsub("$1", name) | |||
:gsub("$2", format_date_de(st)) | |||
:gsub("$3", format_date_de(en_excl)) | |||
else | |||
-- beendet (bis zu post_days danach) | |||
if isempty(PH_ENDED) then return "" end | |||
-- Labels aus i18n.sections (wie gehabt) | |||
local curr = ev_label(evkey, "currencies") | |||
local shop = ev_label(evkey, "ex_shop") | |||
if isempty(curr) or isempty(shop) then return "" end | |||
text = PH_ENDED | |||
:gsub("$1", name) | |||
:gsub("$4", curr) | |||
:gsub("$5", shop) | |||
end | |||
local prefix = (ic ~= "" and (ic .. " ") or "") | |||
local suffix = (ic ~= "" and (" " .. ic) or "") | |||
return string.format( | |||
'<div class="fs-notice" data-ev="%s">%s%s%s</div>', | |||
mw.text.encode(evkey), | |||
prefix, text, suffix | |||
) | |||
end | |||
-- ========== Dispatcher ========== | |||
local function get_event(evkey) | local function get_event(evkey) | ||
local evs = events() | local evs = events() | ||
| Zeile 451: | Zeile 580: | ||
local function dispatch_event(args) | local function dispatch_event(args) | ||
local evkey = args[2]; if isempty(evkey) then return "" end | local evkey = args[2] | ||
-- {{Firestone|Event|notice}} → automatisch wählen | |||
if evkey and norm(evkey) == "notice" then | |||
local pick = pick_notice_eventkey() | |||
if not pick then return "" end | |||
local ev, r = get_event(pick); if not ev then return "" end | |||
return event_notice(ev, r) | |||
end | |||
-- normaler Weg | |||
if isempty(evkey) then return "" end | |||
local ev, realkey = get_event(evkey); if not ev then return "" end | local ev, realkey = get_event(evkey); if not ev then return "" end | ||
local a3, a4 = args[3], args[4] | local a3, a4 = args[3], args[4] | ||
if isempty(a3) then return ev_name(realkey) end | if isempty(a3) then return ev_name(realkey) end | ||
if a3=="notice" then return event_notice(ev, realkey) | |||
elseif a3=="file" then return event_file(ev, realkey, a4 or "") | |||
elseif a3=="link" then return event_link(ev, realkey, a4 or "") | elseif a3=="link" then return event_link(ev, realkey, a4 or "") | ||
elseif a3=="h2" | elseif a3=="h2" then return htag(2, ev_label(realkey, a4 or "")) | ||
elseif a3=="h3" | elseif a3=="h3" then return htag(3, ev_label(realkey, a4 or "")) | ||
elseif a3=="history" then return event_history(ev) | elseif a3=="history" then return event_history(ev, realkey, a) | ||
elseif a3=="avatars" then return event_avatars(ev, realkey) | elseif a3=="avatars" then return event_avatars(ev, realkey) | ||
elseif a3=="exchange" then return event_exchange_tabs(ev, realkey) | elseif a3=="exchange" then return event_exchange_tabs(ev, realkey) | ||
| Zeile 471: | Zeile 611: | ||
end | end | ||
-- | -- Sonderfälle für Währungsnamen aus i18n.sections | ||
if a3 == "currency" then return ev_label(realkey, "currency") end | |||
if a3 == "currencies" then return ev_label(realkey, "currencies") end | |||
-- generischer Pfad in EventData | |||
local path, i = {}, 3 | local path, i = {}, 3 | ||
while args[i] do table.insert(path, args[i]); i=i+1 end | while args[i] do table.insert(path, args[i]); i=i+1 end | ||
| Zeile 481: | Zeile 625: | ||
end | end | ||
-- Fallback | -- Fallback auf Labels/Name/Unlocks | ||
local last = norm(path[#path] or a3) | local last = norm(path[#path] or a3) | ||
if last=="eventname" or last=="name" then return ev_name(realkey) end | if last=="eventname" or last=="name" then return ev_name(realkey) end | ||
local lbl = ev_label(realkey, last); if not isempty(lbl) then return lbl end | local lbl = ev_label(realkey, last); if not isempty(lbl) then return lbl end | ||
local U = ((i18n().events or {}).unlocks or {})[last] | local U = ((i18n().events or {}).unlocks or {})[last] | ||
| Zeile 492: | Zeile 635: | ||
end | end | ||
local function dispatch_hero(args) | local function dispatch_hero(args) | ||
local name = args[2]; if isempty(name) then return "" end | local name = args[2]; if isempty(name) then return "" end | ||
| Zeile 506: | Zeile 648: | ||
end | end | ||
-- ========= main ========= | -- ========== main ========== | ||
function M.main(frame) | function M.main(frame) | ||
FRAME = frame | FRAME = frame | ||
local a = getArgs(frame) | local a = getArgs(frame) | ||
for i=1,10 do a[i] = a[i] and tostring(a[i]) or nil end | for i=1,10 do a[i] = a[i] and tostring(a[i]) or nil end | ||
local dom = a[1] and norm(a[1]) or "" | local dom = a[1] and norm(a[1]) or "" | ||
if dom=="event" then return dispatch_event(a) | if dom=="event" then | ||
elseif dom=="hero" then return dispatch_hero(a) | return dispatch_event(a) | ||
else return "" end | elseif dom=="hero" then | ||
return dispatch_hero(a) | |||
else | |||
return "" | |||
end | |||
end | end | ||
return M | return M | ||
Aktuelle Version vom 28. Oktober 2025, 13:53 Uhr
Die Dokumentation für dieses Modul kann unter Modul:Firestone/Doku erstellt werden
-- Modul:Firestone
local M = {}
-- ========== kleine Helfer ==========
local COL_OFFER, COL_PRICE, COL_LIMIT = "50%", "25%", "25%"
local DAY = 86400
local function parse_iso_date(s)
local y,m,d = tostring(s or ""):match("^(%d%d%d%d)%-(%d%d)%-(%d%d)$")
if not y then return nil end
return os.time{ year=tonumber(y), month=tonumber(m), day=tonumber(d), hour=12 }
end
local function ts_to_mw(ts) return os.date("!%Y%m%d%H%M%S", ts) end
local function format_date_de(v)
local ts = (type(v)=="number") and v or parse_iso_date(v)
if not ts then return "" end
local lang = mw.getContentLanguage()
return lang:formatDate("d.m.Y", ts_to_mw(ts))
end
local function isempty(v) return v == nil or v == "" end
local function norm(s)
if s == nil then return "" end
s = mw.text.trim(tostring(s))
return mw.ustring.lower(s):gsub("%s+"," ")
end
-- bevorzugt Parent-Args (Vorlagen)
local function getArgs(frame)
local p = frame:getParent()
local src = (p and p.args) or frame.args or {}
local a = {}
for k,v in pairs(src) do
if v ~= nil and v ~= "" then a[k] = mw.text.trim(tostring(v)) end
end
return a
end
local function has_items(t)
if type(t) ~= "table" then return false end
for _ in pairs(t) do return true end
return false
end
local function pick_key(tbl, key)
if type(tbl) ~= "table" then return nil end
if tbl[key] ~= nil then return key end
local k2 = tostring(key)
if tbl[k2] ~= nil then return k2 end
local kn = norm(k2)
for k,_ in pairs(tbl) do if norm(k) == kn then return k end end
return nil
end
local function deep_get(tbl, path)
local cur = tbl
for _,k in ipairs(path) do
if type(cur) ~= "table" then return nil end
local real = pick_key(cur, k); if not real then return nil end
cur = cur[real]
end
return cur
end
local function fileTag(file, opts) -- [[Datei:...]]
if isempty(file) then return "" end
local parts = { "Datei:"..file }
if opts and opts.size then table.insert(parts, opts.size) end
if opts and opts.param then table.insert(parts, opts.param) end -- z.B. "thumb|Text"
return string.format("[[%s]]", table.concat(parts, "|"))
end
local function pretty_range(s)
if not s or s=="" then return "" end
s = tostring(s)
if s:sub(-1) == "-" then return s:sub(1, -2) .. "+" end
return s:gsub("%-","–")
end
-- ========== Lazy-Loads ==========
local I18N, EVENTS, HEROES
local function i18n()
if I18N then return I18N end
for _,t in ipairs{ "Modul:HeroI18n", "Module:HeroI18n" } do
local ok, data = pcall(mw.loadData, t) ; if ok and type(data)=="table" then I18N=data; return I18N end
local ok2, mod = pcall(require, t) ; if ok2 and type(mod) =="table" then I18N=mod ; return I18N end
end
I18N = { i18n={}, events={ names={}, sections={default={}}, phrases={}, links={}, unlocks={} } }
return I18N
end
local function events()
if EVENTS then return EVENTS end
for _,t in ipairs{ "Modul:EventData", "Module:EventData" } do
local ok, data = pcall(mw.loadData, t) ; if ok and type(data)=="table" and type(data.events)=="table" then EVENTS=data.events; return EVENTS end
local ok2, mod = pcall(require, t) ; if ok2 and type(mod) =="table" and type(mod.events)=="table" then EVENTS=mod.events ; return EVENTS end
end
EVENTS = {}; return EVENTS
end
local function heroes()
if HEROES then return HEROES end
for _,t in ipairs{ "Modul:HeroData", "Module:HeroData" } do
local ok, data = pcall(mw.loadData, t) ; if ok and type(data)=="table" and type(data.heroes)=="table" then HEROES=data.heroes; return HEROES end
local ok2, mod = pcall(require, t) ; if ok2 and type(mod) =="table" and type(mod.heroes)=="table" then HEROES=mod.heroes ; return HEROES end
end
HEROES = {}; return HEROES
end
-- ========== i18n / Labels ==========
local function ev_label(evkey, which)
local E = i18n().events or {}
local spec = (E.sections and E.sections[norm(evkey)]) or {}
local def = (E.sections and E.sections.default) or {}
return (spec[which] or def[which] or "")
end
local function ev_name(evkey)
local names = (i18n().events or {}).names or {}
return names[norm(evkey)] or evkey
end
local function header_labels(evkey)
return ev_label(evkey, "offer"), ev_label(evkey, "price"), ev_label(evkey, "limit")
end
local function autotr(key_name, value)
if type(value) ~= "string" then return value end
local k = norm(key_name)
-- 1) Standardwortschatz (falls vorhanden)
local m1 = (i18n().i18n or {})[k]
local v = m1 and m1[norm(value)]
if v then return v end
-- 2) Spezialfall Event-Typen: i18n().events.infobox.type
if k == "type" then
local m2 = (((i18n().events or {}).infobox) or {}).type or {}
return m2[norm(value)] or value
end
return value
end
local function ev_notice_icon_wikitext(ev)
local file = ev.currency and ev.currency.icon
if not file or file == "" then return "" end
local img = fileTag(file, { size = "24px", param = "frameless|link=" })
return '<span class="fs-ic">' .. img .. '</span>'
end
local function ev_pagelink(ev, evkey, label)
local page = ev.page
if page and page ~= "" then
return string.format("[[%s|%s]]", page, label)
end
return label
end
-- ========== Nummern + <ref> (voll i18n-gesteuert) ==========
local function parse_sci(v)
if type(v) == "number" then return v end
if type(v) == "string" then
local a,e = v:match("^%s*([%d%.]+)[eE]([%+%-]?%d+)%s*$")
if a and e then return (tonumber(a) or 0) * 10^(tonumber(e) or 0) end
local n = tonumber(v); if n then return n end
end
return nil
end
local function i18n_big_units()
local N = (i18n().numbers or {})
return {
{1e12, N.trillion_sing, N.trillion_plur},
{1e9, N.billion_sing, N.billion_plur},
{1e6, N.million_sing, N.million_plur},
{1e3, N.thousand_sing, N.thousand_plur},
}
end
local function format_big(v)
local n = parse_sci(v); if not n then return tostring(v or "") end
for _,u in ipairs(i18n_big_units()) do
local base, sing, plur = u[1], u[2], u[3]
if base and sing and plur and n >= base and math.floor(n % base) == 0 then
local q = math.floor(n / base)
return string.format("%d %s", q, (q==1) and sing or plur)
end
end
return tostring(n)
end
local function ref_gear_power(v)
if isempty(v) then return "" end
local phr = (((i18n().events or {}).phrases) or {}).req_gear_power
if isempty(phr) then return "" end
local name = "gear-" .. tostring(v):gsub("%s","")
return string.format('<ref name="%s">%s</ref>', name, (phr:gsub("$1", format_big(v))))
end
-- ========== Event-Renderer ==========
local function htag(level, text)
if isempty(text) then return "" end
local eq = level==3 and "===" or "=="
return string.format("%s %s %s", eq, text, eq)
end
local function event_file(ev, evkey, which)
if which=="deco" then
local file = ev.images and ev.images.deco or ""
local legend = ev_label(evkey, "deco")
return fileTag(file, { param = "thumb|"..legend })
elseif which=="fullname" then
local file = ev.images and ev.images.fullname or ""
local legend = ev_label(evkey, "fullname")
return fileTag(file, { param = "thumb|"..legend })
elseif which=="ex_shop" then
local file = ev.images and ev.images.ex_shop or ""
local legend = ev_label(evkey, "ex_shop")
return fileTag(file, { param = "thumb|"..legend })
elseif which=="banner" then
return fileTag(ev.banner, { param="thumb" })
end
return ""
end
local function event_link(ev, evkey, which)
if which=="type" then
local E = i18n().events or {}
local page = (E.links and E.links.type and E.links.type[ev.type or ""])
or (E.links and E.links.type) or "Calendar Events"
local map = (E.infobox and E.infobox.type) or {}
local label = map[norm(ev.type or "")] or (ev.type or "")
return string.format("[[%s|%s]]", page, label)
end
return ""
end
local function event_history(ev)
local H = ev.history or {}
if not has_items(H) then return "" end
local alt_gp = ((i18n().events or {}).phrases or {}).alt_platform or "auf Google Play"
local function fmt_range_de(s1, s2)
return string.format("%s bis %s", format_date_de(s1), format_date_de(s2))
end
-- "2023-10-24 bis 2023-11-07" → "24.10.2023 bis 07.11.2023"
local function fmt_alt_platform(val)
local y1,m1,d1,y2,m2,d2 =
tostring(val or ""):match("^%s*(%d%d%d%d)%-(%d%d)%-(%d%d)%s+bis%s+(%d%d%d%d)%-(%d%d)%-(%d%d)%s*$")
if y1 then
return fmt_range_de(y1.."-"..m1.."-"..d1, y2.."-"..m2.."-"..d2)
end
-- falls kein ISO-Bereich erkannt wurde: unverändert ausgeben
return tostring(val or "")
end
local out = {}
for _,h in ipairs(H) do
local year = tostring(h.year or "")
local s = h.start or ""
local e = h["end"] or ""
-- Start/Ende immer DE-formatiert
local line = string.format("* %s: %s", year, fmt_range_de(s, e))
-- optionaler Alternativ-Bereich (auch DE-formatiert)
if h.alt_platform then
line = line .. string.format(" (%s %s)", fmt_alt_platform(h.alt_platform), alt_gp)
end
table.insert(out, line)
end
return table.concat(out, "\n")
end
local function item_label(key, amount)
local I = (i18n().items or {})[key] or {}
local n = tonumber(amount or 1) or 1
local txt = (n == 1) and (I.sing or key) or (I.plur or (I.sing or key))
local disp = I.link and string.format("[[%s|%s]]", I.link, txt) or txt
return string.format("%d %s", n, disp)
end
-- references/render
local FRAME = nil
local function tabber_render_from_content(content)
if FRAME and FRAME.extensionTag then
return FRAME:extensionTag('tabber', content)
end
return "<tabber>\n" .. content .. "\n</tabber>"
end
local function references_tag()
if FRAME and FRAME.extensionTag then
return FRAME:extensionTag('references', '')
end
return '<references />'
end
-- innerer Tab-Inhalt: je Range
local function build_tabber_content_for(list)
local out = {}
for _,it in ipairs(list) do
local label = pretty_range(it.range or "")
local note = ref_gear_power(it.req_gear_power)
local content = string.format(
"%s %s%s",
fileTag(it.icon or "", { size="30px" }),
(it.item and item_label(it.item, it.amount or 1)) or (it.title or ""),
note
)
out[#out+1] = "|-|" .. label .. "=\n" .. content
end
return table.concat(out, "\n")
end
-- Gruppen-Label aus i18n
local function group_label(kind)
local G = ((i18n().events or {}).group_labels or {})
return G[kind] or kind
end
-- Baut die Avatar-Tabelle für einen Jahrgang
local function build_avatar_table(list, token_icon, token_name)
local t = {}
t[#t+1] = '{| class="article-table" style="font-size:14px;"'
t[#t+1] = '! colspan="2" | Avatar\n! Preis'
for _,it in ipairs(list) do
t[#t+1] = '|-'
t[#t+1] = string.format(
'| %s || %s || %s %s %s',
fileTag(it.file, { size="50px" }),
it.title or "",
fileTag(token_icon, { size="25px" }),
tostring(it.cost or 0),
token_name
)
end
t[#t+1] = '|}'
return table.concat(t, "\n")
end
-- Avatare als TabberNeue: ein Tab pro Jahr
local function event_avatars(ev, evkey)
local AV = ev.avatars
if not has_items(AV) then return "" end
local token_icon = (ev.currency or {}).icon or ""
local token_name = ev_label(evkey, "currencies")
-- Jahre einsammeln und sortieren (aufsteigend)
local years = {}
for y,_ in pairs(AV) do years[#years+1] = tostring(y) end
table.sort(years, function(a,b) return a > b end)
-- Hinweis: Wenn du das neueste Jahr zuerst willst:
-- table.sort(years, function(a,b) return a > b end)
-- Tab-Inhalt aufbauen
local parts = {}
for _,year in ipairs(years) do
local list = AV[year] or AV[tonumber(year)]
if has_items(list) then
local table_markup = build_avatar_table(list, token_icon, token_name)
parts[#parts+1] = "|-|" .. year .. "=\n" .. table_markup
end
end
-- Als <tabber> rendern (nutzt deine tabber_render_from_content)
return tabber_render_from_content(table.concat(parts, "\n"))
end
-- Austausch-Tabelle (äußerer Tab: Gruppen → innerer Tab: Ranges)
local function event_exchange_tabs(ev, evkey)
local ex = ev.exchange or {}
local token_icon = (ev.currency or {}).icon or ""
local token_name = ev_label(evkey, "currencies")
if not (has_items(ex.chests_by_level)
or has_items(ex.chests_by_stars)
or has_items(ex.chests_by_oracle)
or has_items(ex.currencies)) then
return ""
end
local outer_parts = {}
local function add_group(kind, list)
if not has_items(list) then return end
local inner_content = build_tabber_content_for(list)
local inner_tabber = tabber_render_from_content(inner_content)
outer_parts[#outer_parts+1] = "|-|" .. group_label(kind) .. "=\n" .. inner_tabber
end
add_group("level", ex.chests_by_level)
add_group("stars", ex.chests_by_stars)
add_group("oracle", ex.chests_by_oracle)
local left_tabber = tabber_render_from_content(table.concat(outer_parts, "\n"))
-- Preis/Limit (erste gefundene Gruppe)
local function pick_price_limit()
if has_items(ex.chests_by_level) then return ex.chests_by_level[1].price or 0, ex.chests_by_level[1].limit or "" end
if has_items(ex.chests_by_stars) then return ex.chests_by_stars[1].price or 0, ex.chests_by_stars[1].limit or "" end
if has_items(ex.chests_by_oracle) then return ex.chests_by_oracle[1].price or 0, ex.chests_by_oracle[1].limit or "" end
return 0, ""
end
local price, limit = pick_price_limit()
local t = {}
local H_offer, H_price, H_limit = header_labels(evkey)
t[#t+1] = '{| class="article-table fs-exchange" style="font-size:14px;"'
t[#t+1] = string.format(
'! style="width:%s;" | %s !! style="width:%s;" | %s !! style="width:%s;" | %s',
COL_OFFER, H_offer, COL_PRICE, H_price, COL_LIMIT, H_limit
)
t[#t+1] = '|-'
t[#t+1] = '|' .. left_tabber .. string.format(
' || style="text-align:center;" | %s %s %s || style="text-align:center;" | %s',
fileTag(token_icon, { size="25px" }),
tostring(price), token_name,
tostring(limit)
)
if has_items(ex.currencies) then
for _,it in ipairs(ex.currencies) do
local offer = string.format('%s %s',
fileTag(it.icon or "", { size="30px" }),
item_label(it.item, it.amount or 1)
)
t[#t+1] = '|-'
t[#t+1] = string.format(
'| %s || style="text-align:center;" | %s %s %s || style="text-align:center;" | %s',
offer,
fileTag(token_icon, { size="25px" }),
tostring(it.price or 0), token_name,
tostring(it.limit or "")
)
end
end
t[#t+1] = '|}'
t[#t+1] = references_tag()
return '\n' .. table.concat(t, '\n')
end
local function event_offers(ev, evkey, which)
local list = (ev.shop or {})[which]
if not has_items(list) then return "" end
local token_icon = (ev.currency or {}).icon or ""
local token_name = ev_label(evkey, "currencies")
local t = {}
t[#t+1] = '{| class="article-table" style="font-size:14px;"'
t[#t+1] = "! Angebot !! Menge"
for _,it in ipairs(list) do
t[#t+1] = "|-"
t[#t+1] = string.format("| ''%s''<br />%s || %s %s %s",
it.name or "",
fileTag(it.image, { size="x50px" }),
fileTag(token_icon, { size="30px" }),
tostring(it.token or 0),
token_name
)
end
t[#t+1] = "|}"
return table.concat(t, "\n")
end
-- ========== SiteNotice-Support (Datum/Auto-Pick) ==========
local function event_window(ev)
local S = ev.schedule or ev
local s = S.start_date; if isempty(s) then return nil end
local st = parse_iso_date(s); if not st then return nil end
local en_excl, en_incl
if not isempty(S.end_date) then
-- end_date wird als inklusives Kalenderdatum verstanden
en_incl = parse_iso_date(S.end_date)
if not en_incl then return st, nil, nil end
en_excl = en_incl + DAY
else
-- Dauer in Tagen (Standard 14), exklusives Ende = st + d*DAY
local d = tonumber(S.duration_days) or 14
en_excl = st + d * DAY
en_incl = en_excl - DAY
end
return st, en_excl, en_incl
end
local function pick_notice_eventkey()
local cfg = ((i18n().events or {}).notice or {})
local pre = tonumber(cfg.pre_days) or 7
local post = tonumber(cfg.post_days) or 3
local now = os.time()
local running, upcoming, ended_recent = {}, {}, {}
for k, ev in pairs(events()) do
local st, en_excl = event_window(ev)
if st and en_excl then
if now >= st and now < en_excl then
table.insert(running, { k=k, st=st, en=en_excl })
elseif now >= st - pre*DAY and now < st then
table.insert(upcoming, { k=k, st=st, en=en_excl })
elseif now >= en_excl and now < en_excl + post*DAY then
table.insert(ended_recent, { k=k, st=st, en=en_excl })
end
end
end
local function by_end(a,b) return a.en < b.en end
local function by_start(a,b) return a.st < b.st end
if #running > 0 then table.sort(running, by_end) ; return running[1].k end
if #upcoming > 0 then table.sort(upcoming, by_start) ; return upcoming[1].k end
if #ended_recent > 0 then table.sort(ended_recent, by_end) ; return ended_recent[1].k end
return nil
end
local function event_notice(ev, evkey)
local phrases = (((i18n().events or {}).phrases) or {})
local PH_RUNNING = phrases.event_notice -- "Das $1 läuft von $2 bis $3"
local PH_ENDED = phrases.event_ended -- "Das $1 ist beendet. Du kannst verbleibende $4 noch im $5 eintauschen."
-- Ohne Texte kein Hinweis
if isempty(PH_RUNNING) then return "" end
local st, en_excl, en_incl = event_window(ev); if not st or not en_excl then return "" end
local cfg = ((i18n().events or {}).notice or {})
local pre = tonumber(cfg.pre_days) or 7 -- z. B. 7 Tage vorher
local post = tonumber(cfg.post_days) or 3 -- z. B. 3 Tage danach
local now = os.time()
local show_from = st - pre * DAY
local show_till = en_excl + post * DAY
if now < show_from or now >= show_till then
return ""
end
-- Anzeigename (aus i18n sections oder names)
local name_label = ev_label(evkey, "eventname")
if isempty(name_label) then name_label = ev_name(evkey) end
-- Verlinken NUR wenn ev.page existiert
local name = ev_pagelink(ev, evkey, name_label)
-- Icon aus EventData → currency.icon
local ic = ev_notice_icon_wikitext(ev) -- "" wenn kein currency.icon
local text
if now < en_excl then
-- läuft / startet bald
text = PH_RUNNING
:gsub("$1", name)
:gsub("$2", format_date_de(st))
:gsub("$3", format_date_de(en_excl))
else
-- beendet (bis zu post_days danach)
if isempty(PH_ENDED) then return "" end
-- Labels aus i18n.sections (wie gehabt)
local curr = ev_label(evkey, "currencies")
local shop = ev_label(evkey, "ex_shop")
if isempty(curr) or isempty(shop) then return "" end
text = PH_ENDED
:gsub("$1", name)
:gsub("$4", curr)
:gsub("$5", shop)
end
local prefix = (ic ~= "" and (ic .. " ") or "")
local suffix = (ic ~= "" and (" " .. ic) or "")
return string.format(
'<div class="fs-notice" data-ev="%s">%s%s%s</div>',
mw.text.encode(evkey),
prefix, text, suffix
)
end
-- ========== Dispatcher ==========
local function get_event(evkey)
local evs = events()
local real = pick_key(evs, evkey)
return real and evs[real] or nil, real or evkey
end
local function dispatch_event(args)
local evkey = args[2]
-- {{Firestone|Event|notice}} → automatisch wählen
if evkey and norm(evkey) == "notice" then
local pick = pick_notice_eventkey()
if not pick then return "" end
local ev, r = get_event(pick); if not ev then return "" end
return event_notice(ev, r)
end
-- normaler Weg
if isempty(evkey) then return "" end
local ev, realkey = get_event(evkey); if not ev then return "" end
local a3, a4 = args[3], args[4]
if isempty(a3) then return ev_name(realkey) end
if a3=="notice" then return event_notice(ev, realkey)
elseif a3=="file" then return event_file(ev, realkey, a4 or "")
elseif a3=="link" then return event_link(ev, realkey, a4 or "")
elseif a3=="h2" then return htag(2, ev_label(realkey, a4 or ""))
elseif a3=="h3" then return htag(3, ev_label(realkey, a4 or ""))
elseif a3=="history" then return event_history(ev, realkey, a)
elseif a3=="avatars" then return event_avatars(ev, realkey)
elseif a3=="exchange" then return event_exchange_tabs(ev, realkey)
elseif a3=="offers" then
if a4=="start" then return event_offers(ev, realkey, "initial")
elseif a4=="more" then return event_offers(ev, realkey, "after") end
return ""
end
-- Sonderfälle für Währungsnamen aus i18n.sections
if a3 == "currency" then return ev_label(realkey, "currency") end
if a3 == "currencies" then return ev_label(realkey, "currencies") end
-- generischer Pfad in EventData
local path, i = {}, 3
while args[i] do table.insert(path, args[i]); i=i+1 end
local v = deep_get(ev, path)
if v ~= nil then
local last = path[#path] and tostring(path[#path]) or ""
v = autotr(last, v)
return tostring(v)
end
-- Fallback auf Labels/Name/Unlocks
local last = norm(path[#path] or a3)
if last=="eventname" or last=="name" then return ev_name(realkey) end
local lbl = ev_label(realkey, last); if not isempty(lbl) then return lbl end
local U = ((i18n().events or {}).unlocks or {})[last]
if U then return U end
return ""
end
local function dispatch_hero(args)
local name = args[2]; if isempty(name) then return "" end
local H = heroes()
local real = pick_key(H, name); if not real then return "" end
local h = H[real]
local path, i = {}, 3
while args[i] do table.insert(path, args[i]); i=i+1 end
local v = deep_get(h, path)
local last = path[#path] and tostring(path[#path]) or ""
v = autotr(last, v)
return tostring(v or "")
end
-- ========== main ==========
function M.main(frame)
FRAME = frame
local a = getArgs(frame)
for i=1,10 do a[i] = a[i] and tostring(a[i]) or nil end
local dom = a[1] and norm(a[1]) or ""
if dom=="event" then
return dispatch_event(a)
elseif dom=="hero" then
return dispatch_hero(a)
else
return ""
end
end
return M