Modul:Firestone: Unterschied zwischen den Versionen
Keine Bearbeitungszusammenfassung  | 
				Keine Bearbeitungszusammenfassung  | 
				||
| Zeile 2: | Zeile 2: | ||
local M = {}  | local M = {}  | ||
-- ===== helpers =====  | -- ========= helpers =========  | ||
local function isempty(v) return v==nil or v=="" end  | local function isempty(v) return v == nil or v == "" end  | ||
local function norm(s)  | local function norm(s)  | ||
   if s==nil then return "" end  |    if s == nil then return "" end  | ||
   s = mw.text.trim(tostring(s))  |    s = mw.text.trim(tostring(s))  | ||
   return mw.ustring.lower(s):gsub("%s+"," ")  |    return mw.ustring.lower(s):gsub("%s+"," ")  | ||
end  | end  | ||
-- bevorzugt Parent-Args (typisch bei Vorlagen)  | |||
local function getArgs(frame)  | local function getArgs(frame)  | ||
   local p = frame:getParent()  | |||
   local   | |||
   local src = (p and p.args) or frame.args or {}  |    local src = (p and p.args) or frame.args or {}  | ||
   for k,v in pairs(src) do if v~=nil and v~="" then a[k]=mw.text.trim(tostring(v)) end end  |   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  |    return a  | ||
end  | end  | ||
local function pick_key(tbl, key)  | local function pick_key(tbl, key)  | ||
   if type(tbl)~="table" then return nil end  |    if type(tbl) ~= "table" then return nil end  | ||
   if tbl[key]~=nil then return key end  |    if tbl[key] ~= nil then return key end  | ||
   local k2 = tostring(key)  |    local k2 = tostring(key)  | ||
   if tbl[k2]~=nil then return k2 end  |    if tbl[k2] ~= nil then return k2 end  | ||
   local kn = norm(k2)  |    local kn = norm(k2)  | ||
   for k,_ in pairs(tbl) do if norm(k)==kn then return k end end  |    for k,_ in pairs(tbl) do if norm(k) == kn then return k end end  | ||
   return nil  |    return nil  | ||
end  | end  | ||
| Zeile 31: | Zeile 32: | ||
   local cur = tbl  |    local cur = tbl  | ||
   for _,k in ipairs(path) do  |    for _,k in ipairs(path) do  | ||
     if type(cur)~="table" then return nil end  |      if type(cur) ~= "table" then return nil end  | ||
     local real = pick_key(cur, k); if not real then return nil end  |      local real = pick_key(cur, k); if not real then return nil end  | ||
     cur = cur[real]  |      cur = cur[real]  | ||
| Zeile 38: | Zeile 39: | ||
end  | end  | ||
local function   | local function fileTag(file, opts) -- [[File:...]]  | ||
   if isempty(file) then return "" end  |    if isempty(file) then return "" end  | ||
   local parts = {"  |    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.  |    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  | ||
-- ========= 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  | |||
local   | |||
local function   | |||
   if   | |||
   for _,t in ipairs{ "  | |||
     local ok, data = pcall(mw.loadData, t)  | |||
     local ok2, mod = pcall(require, t)  | |||
   end  |    end  | ||
   I18N = { i18n={}, events={ names={}, sections={default={}}, infobox={}, links={}, phrases={}, unlocks={} } }  | |||
  return I18N  | |||
end  | end  | ||
local function events()  | local function events()  | ||
   if EVENTS then return EVENTS end  |    if EVENTS then return EVENTS end  | ||
   for _,t in ipairs{ "  |    for _,t in ipairs{ "Modul:EventData", "Module:EventData" } do  | ||
     local ok, data = pcall(mw.loadData, t)  |      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  | |||
     local   | |||
     if   | |||
   end  |    end  | ||
   EVENTS = {}; return EVENTS  |    EVENTS = {}; return EVENTS  | ||
end  | end  | ||
local function heroes()  | |||
local function   |    if HEROES then return HEROES end  | ||
   if   |    for _,t in ipairs{ "Modul:HeroData", "Module:HeroData" } do  | ||
   for _,t in ipairs{ "  |      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 ok, data = pcall(mw.loadData, t)  |      local ok2, mod = pcall(require, t)     ; if ok2 and type(mod) =="table" and type(mod.heroes)=="table" then HEROES=mod.heroes ; return HEROES end  | ||
     local ok2, mod = pcall(require, t)  | |||
     if ok2 and type(mod)=="table" then   | |||
   end  |    end  | ||
   HEROES = {}; return HEROES  | |||
end  | end  | ||
-- =====   | -- ========= i18n helpers =========  | ||
local function   | local function tr_simple(kind, key)  | ||
   if isempty(  |    if isempty(key) then return "" end  | ||
   local map = (i18n().i18n or {})[kind] or {}  |    local map = (i18n().i18n or {})[kind] or {}  | ||
   return map[norm(  |    return map[norm(key)] or key  | ||
end  | end  | ||
local function tr_month(  | local function tr_month(m) return tr_simple("month", m) end  | ||
local function tr_duration(d) return tr_simple("duration", d) end  | |||
   return   | |||
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  | end  | ||
local function   | local function ev_name(evkey)  | ||
   local names = (i18n().events or {}).names or {}  |    local names = (i18n().events or {}).names or {}  | ||
   return names[norm(evkey)] or evkey  |    return names[norm(evkey)] or evkey  | ||
end  | end  | ||
local function   | -- ========= event core =========  | ||
   local   | local function get_event(evkey)  | ||
   local   |    local evs = events()  | ||
   local real = pick_key(evs, evkey)  | |||
   return real and evs[real] or nil, real or evkey  | |||
end  | end  | ||
local function   | -- plain field with built-in translations for exactly the keys,  | ||
   local map = ((i18n().events or {}).infobox or {}).type or {}  | -- die du in der Vorlage verwendest  | ||
local function event_field(ev, key, evkey)  | |||
   if key == "banner" then return ev.banner or ""  | |||
  elseif key == "type" then  | |||
    local t = ev.type or ""  | |||
    local map = ((i18n().events or {}).infobox or {}).type or {}  | |||
    return map[norm(t)] or t  | |||
  elseif key == "month" then  | |||
    return tr_month(ev.month or ev.date or "")  | |||
     return   |   elseif key == "duration" then  | ||
    return tr_duration(ev.duration or "")  | |||
   elseif key == "character_level_10" then  | |||
    return ((i18n().events or {}).unlocks or {}).character_level_10 or "Level 10"  | |||
   elseif key == "currency" then  | |||
    return ev_label(evkey, "currency")  | |||
  elseif key == "currencies" then  | |||
    return ev_label(evkey, "currencies")  | |||
   elseif key == "ex_shop" or key=="shopname" or key=="fullname" or key=="shortname"  | |||
     or key=="currency_exchange" or key=="avatar_exchange" or key=="name" or key=="eventname" then  | |||
    local k = key == "eventname" and "name" or key  | |||
     return ev_label(evkey, k)  | |||
   else  |    else  | ||
     return   |     -- generischer Pfad (falls du mal Werte direkt aus EventData brauchst)  | ||
     return tostring(ev[key] or "")  | |||
   end  |    end  | ||
end  | end  | ||
local function   | -- ==/=== Überschriften ==  | ||
   if isempty(  | local function htag(level, text)  | ||
   local   |    if isempty(text) then return "" end  | ||
   local eq = level==3 and "===" or "=="  | |||
   return string.format("%s %s %s", eq, text, eq)  | |||
end  | end  | ||
local function   | -- [[File:...|thumb|...]]  | ||
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")  | |||
     local   |     return fileTag(file, { param = "thumb|"..legend })  | ||
     return (  |    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=="banner" then  | |||
     return fileTag(ev.banner, { param="thumb" })  | |||
   end  |    end  | ||
   return   |    return ""  | ||
end  | end  | ||
--   | -- Links, z.B. [[Calendar Events|Kalenderevent]]  | ||
local function   | local function event_link(ev, evkey, which)  | ||
   local   |    if which=="type" then  | ||
    local page = ((i18n().events or {}).links or {}).type or "Calendar Events"  | |||
    local label= event_field(ev, "type", evkey)  | |||
    return string.format("[[%s|%s]]", page, label)  | |||
  end  | |||
  return ""  | |||
end  | end  | ||
local function   | -- History-List bullets  | ||
   local H = ev.history or {}  | local function event_history(ev)  | ||
   local H = ev.history or {}; if #H==0 then return "" end  | |||
   local   |    local alt_gp = ((i18n().events or {}).phrases or {}).alt_platform or "auf Google Play"  | ||
  local t = {}  | |||
   for _,h in ipairs(H) do  |    for _,h in ipairs(H) do  | ||
     local   |      local line = string.format("* %s: %s bis %s", tostring(h.year or ""), tostring(h.start or ""), tostring(h["end"] or ""))  | ||
     if h.alt_platform   |      if h.alt_platform then  | ||
       line = line .. string.format(" (%s %s)", tostring(h.alt_platform), alt_gp)  | |||
     end  |      end  | ||
     table.insert(  |      table.insert(t, line)  | ||
   end  |    end  | ||
   return table.concat(  |    return table.concat(t, "\n")  | ||
end  | end  | ||
local function   | -- Avatars: alle Jahre in einem Rutsch  | ||
local function event_avatars(ev, evkey)  | |||
   local AV = ev.avatars or {}  |    local AV = ev.avatars or {}  | ||
   local token_icon = ev.currency and ev.currency.icon or ""  | |||
  local token_name = event_field(ev, "currencies", evkey)  | |||
  local out = {}  | |||
   local years = {}  |    local years = {}  | ||
   for y,_ in pairs(AV) do table.insert(years, y) end  |    for y,_ in pairs(AV) do table.insert(years, y) end  | ||
   table.sort(years)  |    table.sort(years)  | ||
   for _,year in ipairs(years) do  |    for _,year in ipairs(years) do  | ||
     table.insert(out, string.format("==== %s ====", year))  | |||
     table.insert(out, string.format("==== %  |      table.insert(out, '{| class="article-table" style="font-size:14px;"')  | ||
     table.insert(out, '{| class="article-table" style="font-size: 14px;"')  |      table.insert(out, '! colspan="2" | Avatar\n! Preis')  | ||
     table.insert(out, '!colspan="2" | Avatar\n!Preis')  |      for _,it in ipairs(AV[year]) do  | ||
     for _,it in ipairs(  | |||
       table.insert(out, "|-")  |        table.insert(out, "|-")  | ||
       table.insert(out,  |        table.insert(out, 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  |      end  | ||
     table.insert(out, "|}")  |      table.insert(out, "|}")  | ||
   end  |    end  | ||
   return table.concat(out, "\n")  |    return table.concat(out, "\n")  | ||
end  | end  | ||
local function   | -- Exchange-Tabelle (kurz & robust)  | ||
   local   | local function event_exchange(ev, evkey)  | ||
   local   |    local ex = ev.exchange or {}  | ||
  local token = ev.currency or {}  | |||
  local token_icon = token.icon or ""  | |||
   local token_name = event_field(ev, "currencies", evkey)  | |||
   local   |    local function table_for(list, header)  | ||
    if type(list) ~= "table" or #list==0 then return "" end  | |||
     table.insert(  |     local t = {}  | ||
     table.insert(  |      table.insert(t, '{| class="article-table" style="font-size:14px;"')  | ||
     table.insert(t, "! "..header.." !! Preis !! Limit")  | |||
    for _,it in ipairs(list) do  | |||
      table.insert(t, "|-")  | |||
      local offer = string.format("[[Datei:%s|30px]] %s", it.icon or "", it.title or "")  | |||
      if it.range then offer = "level "..it.range..":  "..offer end  | |||
      table.insert(t, string.format("| %s || %s %s %s || %s",  | |||
        offer,  | |||
        fileTag(token_icon, { size="25px" }),  | |||
        tostring(it.price or 0),  | |||
        token_name,  | |||
        tostring(it.limit or "")  | |||
      ))  | |||
    end  | |||
    table.insert(t, "|}")  | |||
    return table.concat(t, "\n")  | |||
   end  |    end  | ||
   local   |    local parts = {}  | ||
  if ex.chests_by_level then table.insert(parts, table_for(ex.chests_by_level, "Angebot")) end  | |||
   if ex.chests_by_stars then table.insert(parts, table_for(ex.chests_by_stars, "Angebot")) end  | |||
   if ex.chests_by_oracle then table.insert(parts, table_for(ex.chests_by_oracle, "Angebot")) end  | |||
   if ex.currencies then table.insert(parts, table_for(ex.currencies, "Angebot")) end  | |||
   return table.concat(parts, "\n")  | |||
   return table.concat(  | |||
end  | end  | ||
local function   | -- Shop-Angebote  | ||
   local list =   | local function event_offers(ev, evkey, which)  | ||
   if type(list)~="table" or #list==0 then return "" end  |    local list = (ev.shop or {})[which]  | ||
   local   |    if type(list) ~= "table" or #list==0 then return "" end  | ||
   local token_icon = ev.currency and ev.currency.icon or ""  | |||
  local token_name = event_field(ev, "currencies", evkey)  | |||
   local t = {}  | |||
   table.insert(t, '{| class="article-table" style="font-size:14px;"')  | |||
  table.insert(t, "! colspan=\"2\" style=\"text-align:center\" | ")  | |||
  table.insert(t, "|-")  | |||
  for _,it in ipairs(list) do  | |||
    table.insert(t, string.format("| %s || %s %s %s",  | |||
      "''"..(it.name or "").."''<br />"..fileTag(it.image, { size="x50px" }),  | |||
      fileTag(token_icon, { size="30px" }),  | |||
      tostring(it.token or 0),  | |||
      token_name  | |||
    ))  | |||
    table.insert(t, "|-")  | |||
   end  |    end  | ||
   return   |   table.insert(t, "|}")  | ||
   return table.concat(t, "\n")  | |||
end  | end  | ||
-- =====  | -- ===== main dispatcher for Event =====  | ||
local function   | local function dispatch_event(args)  | ||
   local   |    local evkey = args[2]; if isempty(evkey) then return "" end  | ||
   local ev =   |    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) or a3=="eventname" then  | |||
   if isempty(a3)   |      return ev_name(realkey)  | ||
     return   | |||
   end  |    end  | ||
   --   |    -- plain fields & labels  | ||
   if a3=="  |    if a3=="banner" or a3=="type" or a3=="month" or a3=="duration"  | ||
     return   |      or a3=="character_level_10"  | ||
     or a3=="currency" or a3=="currencies"  | |||
     or a3=="ex_shop" or a3=="fullname" or a3=="shortname"  | |||
     or a3=="shopname" or a3=="currency_exchange" or a3=="avatar_exchange"  | |||
     or a3=="name" then  | |||
     return event_field(ev, a3, realkey)  | |||
   end  |    end  | ||
   --   |    -- file / link / headings  | ||
   if a3=="link"   |    if a3=="file" then return event_file(ev, realkey, a4 or "")  | ||
  elseif a3=="link" then return event_link(ev, realkey, a4 or "")  | |||
   end  |   elseif a3=="h2"  then return htag(2, ev_label(realkey, a4 or ""))  | ||
   elseif a3=="h3"  then return htag(3, ev_label(realkey, a4 or "")) end  | |||
   --   |    -- composed blocks  | ||
   if a3=="history" then  |    if a3=="history" then return event_history(ev)  | ||
   elseif a3=="avatars" then return event_avatars(ev, realkey)  | |||
   elseif a3=="avatars" then  |    elseif a3=="exchange" then return event_exchange(ev, realkey)  | ||
   elseif a3=="offers" and (a4=="start" or a4=="more") then  | |||
   elseif a3=="exchange" then  |      return event_offers(ev, realkey, (a4=="start") and "initial" or "after")  | ||
   elseif a3=="offers"   | |||
     return   | |||
   end  |    end  | ||
   return ""  | |||
end  | |||
-- ===== hero (nur für Vollständigkeit deiner Wünsche) =====  | |||
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  |    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  | ||
   local v = deep_get(  |    local v = deep_get(h, path)  | ||
   return tostring(v or "")  |    return tostring(v or "")  | ||
end  | end  | ||
| Zeile 398: | Zeile 322: | ||
function M.main(frame)  | function M.main(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,  | |||
   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)  | ||
   else return "" end  | |||
   else  | |||
end  | end  | ||
return M  | return M  | ||
Version vom 20. Oktober 2025, 18:55 Uhr
Die Dokumentation für dieses Modul kann unter Modul:Firestone/Doku erstellt werden
-- Modul:Firestone
local M = {}
-- ========= helpers =========
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 (typisch bei 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 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) -- [[File:...]]
  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
-- ========= 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={}}, infobox={}, links={}, phrases={}, 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 helpers =========
local function tr_simple(kind, key)
  if isempty(key) then return "" end
  local map = (i18n().i18n or {})[kind] or {}
  return map[norm(key)] or key
end
local function tr_month(m) return tr_simple("month", m) end
local function tr_duration(d) return tr_simple("duration", d) end
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
-- ========= event core =========
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
-- plain field with built-in translations for exactly the keys,
-- die du in der Vorlage verwendest
local function event_field(ev, key, evkey)
  if key == "banner" then return ev.banner or ""
  elseif key == "type" then
    local t = ev.type or ""
    local map = ((i18n().events or {}).infobox or {}).type or {}
    return map[norm(t)] or t
  elseif key == "month" then
    return tr_month(ev.month or ev.date or "")
  elseif key == "duration" then
    return tr_duration(ev.duration or "")
  elseif key == "character_level_10" then
    return ((i18n().events or {}).unlocks or {}).character_level_10 or "Level 10"
  elseif key == "currency" then
    return ev_label(evkey, "currency")
  elseif key == "currencies" then
    return ev_label(evkey, "currencies")
  elseif key == "ex_shop" or key=="shopname" or key=="fullname" or key=="shortname"
     or key=="currency_exchange" or key=="avatar_exchange" or key=="name" or key=="eventname" then
    local k = key == "eventname" and "name" or key
    return ev_label(evkey, k)
  else
    -- generischer Pfad (falls du mal Werte direkt aus EventData brauchst)
    return tostring(ev[key] or "")
  end
end
-- ==/=== Überschriften ==
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
-- [[File:...|thumb|...]]
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=="banner" then
    return fileTag(ev.banner, { param="thumb" })
  end
  return ""
end
-- Links, z.B. [[Calendar Events|Kalenderevent]]
local function event_link(ev, evkey, which)
  if which=="type" then
    local page = ((i18n().events or {}).links or {}).type or "Calendar Events"
    local label= event_field(ev, "type", evkey)
    return string.format("[[%s|%s]]", page, label)
  end
  return ""
end
-- History-List bullets
local function event_history(ev)
  local H = ev.history or {}; if #H==0 then return "" end
  local alt_gp = ((i18n().events or {}).phrases or {}).alt_platform or "auf Google Play"
  local t = {}
  for _,h in ipairs(H) do
    local line = string.format("* %s: %s bis %s", tostring(h.year or ""), tostring(h.start or ""), tostring(h["end"] or ""))
    if h.alt_platform then
      line = line .. string.format(" (%s %s)", tostring(h.alt_platform), alt_gp)
    end
    table.insert(t, line)
  end
  return table.concat(t, "\n")
end
-- Avatars: alle Jahre in einem Rutsch
local function event_avatars(ev, evkey)
  local AV = ev.avatars or {}
  local token_icon = ev.currency and ev.currency.icon or ""
  local token_name = event_field(ev, "currencies", evkey)
  local out = {}
  local years = {}
  for y,_ in pairs(AV) do table.insert(years, y) end
  table.sort(years)
  for _,year in ipairs(years) do
    table.insert(out, string.format("==== %s ====", year))
    table.insert(out, '{| class="article-table" style="font-size:14px;"')
    table.insert(out, '! colspan="2" | Avatar\n! Preis')
    for _,it in ipairs(AV[year]) do
      table.insert(out, "|-")
      table.insert(out, 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
    table.insert(out, "|}")
  end
  return table.concat(out, "\n")
end
-- Exchange-Tabelle (kurz & robust)
local function event_exchange(ev, evkey)
  local ex = ev.exchange or {}
  local token = ev.currency or {}
  local token_icon = token.icon or ""
  local token_name = event_field(ev, "currencies", evkey)
  local function table_for(list, header)
    if type(list) ~= "table" or #list==0 then return "" end
    local t = {}
    table.insert(t, '{| class="article-table" style="font-size:14px;"')
    table.insert(t, "! "..header.." !! Preis !! Limit")
    for _,it in ipairs(list) do
      table.insert(t, "|-")
      local offer = string.format("[[Datei:%s|30px]] %s", it.icon or "", it.title or "")
      if it.range then offer = "level "..it.range..":  "..offer end
      table.insert(t, string.format("| %s || %s %s %s || %s",
        offer,
        fileTag(token_icon, { size="25px" }),
        tostring(it.price or 0),
        token_name,
        tostring(it.limit or "")
      ))
    end
    table.insert(t, "|}")
    return table.concat(t, "\n")
  end
  local parts = {}
  if ex.chests_by_level then table.insert(parts, table_for(ex.chests_by_level, "Angebot")) end
  if ex.chests_by_stars then table.insert(parts, table_for(ex.chests_by_stars, "Angebot")) end
  if ex.chests_by_oracle then table.insert(parts, table_for(ex.chests_by_oracle, "Angebot")) end
  if ex.currencies then table.insert(parts, table_for(ex.currencies, "Angebot")) end
  return table.concat(parts, "\n")
end
-- Shop-Angebote
local function event_offers(ev, evkey, which)
  local list = (ev.shop or {})[which]
  if type(list) ~= "table" or #list==0 then return "" end
  local token_icon = ev.currency and ev.currency.icon or ""
  local token_name = event_field(ev, "currencies", evkey)
  local t = {}
  table.insert(t, '{| class="article-table" style="font-size:14px;"')
  table.insert(t, "! colspan=\"2\" style=\"text-align:center\" | ")
  table.insert(t, "|-")
  for _,it in ipairs(list) do
    table.insert(t, string.format("| %s || %s %s %s",
      "''"..(it.name or "").."''<br />"..fileTag(it.image, { size="x50px" }),
      fileTag(token_icon, { size="30px" }),
      tostring(it.token or 0),
      token_name
    ))
    table.insert(t, "|-")
  end
  table.insert(t, "|}")
  return table.concat(t, "\n")
end
-- ===== main dispatcher for Event =====
local function dispatch_event(args)
  local evkey = args[2]; 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) or a3=="eventname" then
    return ev_name(realkey)
  end
  -- plain fields & labels
  if a3=="banner" or a3=="type" or a3=="month" or a3=="duration"
     or a3=="character_level_10"
     or a3=="currency" or a3=="currencies"
     or a3=="ex_shop" or a3=="fullname" or a3=="shortname"
     or a3=="shopname" or a3=="currency_exchange" or a3=="avatar_exchange"
     or a3=="name" then
    return event_field(ev, a3, realkey)
  end
  -- file / link / headings
  if 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 "")) end
  -- composed blocks
  if a3=="history" then return event_history(ev)
  elseif a3=="avatars" then return event_avatars(ev, realkey)
  elseif a3=="exchange" then return event_exchange(ev, realkey)
  elseif a3=="offers" and (a4=="start" or a4=="more") then
    return event_offers(ev, realkey, (a4=="start") and "initial" or "after")
  end
  return ""
end
-- ===== hero (nur für Vollständigkeit deiner Wünsche) =====
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)
  return tostring(v or "")
end
-- ===== main =====
function M.main(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