Zum Inhalt springen
Das Halloween-Event (Süßes oder Saures) findet vom 24.10.2025 bis 07.11.2025 statt.

Modul:Firestone: Unterschied zwischen den Versionen

Aus Firestone Idle RPG Wiki
Keine Bearbeitungszusammenfassung
Keine Bearbeitungszusammenfassung
Zeile 4: Zeile 4:
-- ===== 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) if s==nil then return "" end s=mw.text.trim(tostring(s)); return mw.ustring.lower(s):gsub("%s+"," ") 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
 
local function getArgs(frame)
local function getArgs(frame)
   local a = {}
  -- bevorzugt die Parameter der Vorlage (Elternframe)
  local p = frame:getParent()
   local a, p = {}, frame:getParent()
   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
   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); if tbl[k2]~=nil then return k2 end
   local k2 = tostring(key)
   local kn = norm(k2); for k,_ in pairs(tbl) do if norm(k)==kn then return k end end
  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
   return nil
end
end
local function deep_get(tbl, path)
local function deep_get(tbl, path)
   local cur = tbl
   local cur = tbl
Zeile 28: Zeile 37:
   return cur
   return cur
end
end
local function fileWikitext(file, size)
 
local function fileWikitext(file, opts)
   if isempty(file) then return "" end
   if isempty(file) then return "" end
   local parts = {"Datei:"..file}; if not isempty(size) then table.insert(parts, size) end
   local parts = {"File:"..file}
  if opts and opts.size  then table.insert(parts, opts.size) end
  if opts and opts.link  then table.insert(parts, "link="..opts.link) end
  if opts and opts.class then table.insert(parts, "class="..opts.class) end
  if opts and opts.thumb then
    table.insert(parts, "thumb")
    if opts.caption and opts.caption~="" then table.insert(parts, opts.caption) end
  end
   return string.format("[[%s]]", table.concat(parts, "|"))
   return string.format("[[%s]]", table.concat(parts, "|"))
end
local function heading(level, text)
  if isempty(text) then return "" end
  local marks = (level==3) and "===" or "=="
  return string.format("%s %s %s", marks, text, marks)
end
end


-- ===== lazy loads =====
-- ===== lazy loads =====
local HEROES, EVENTS, I18N
local HEROES, EVENTS, I18N
local function heroes()
local function heroes()
   if HEROES then return HEROES end
   if HEROES then return HEROES end
   for _,t in ipairs{ "Module:HeroData","Modul:HeroData" } do
   for _,t in ipairs{ "Module:HeroData","Modul:HeroData" } do
     local ok,d = pcall(mw.loadData,t); if ok and type(d)=="table" and type(d.heroes)=="table" then HEROES=d.heroes;return HEROES end
     local ok, data = pcall(mw.loadData, t)
     local ok2,m=pcall(require,t); if ok2 and type(m)=="table" and type(m.heroes)=="table" then HEROES=m.heroes;return HEROES end
    if ok and type(data)=="table" and type(data.heroes)=="table" then HEROES=data.heroes; return HEROES end
   end; HEROES={}; return HEROES
     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
end
local function events()
local function events()
   if EVENTS then return EVENTS end
   if EVENTS then return EVENTS end
   for _,t in ipairs{ "Module:EventData","Modul:EventData" } do
   for _,t in ipairs{ "Module:EventData","Modul:EventData" } do
     local ok,d=pcall(mw.loadData,t); if ok and type(d)=="table" and type(d.events)=="table" then EVENTS=d.events;return EVENTS end
     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
   end
   end
   for _,t in ipairs{ "Module:EventData","Modul:EventData" } do
   for _,t in ipairs{ "Module:EventData","Modul:EventData" } do
     local ok,m=pcall(require,t); if ok and type(m)=="table" and type(m.events)=="table" then EVENTS=m.events;return EVENTS end
     local ok, mod = pcall(require, t)
   end; EVENTS={}; return EVENTS
    if ok and type(mod)=="table" and type(mod.events)=="table" then EVENTS=mod.events; return EVENTS end
   end
  EVENTS = {}; return EVENTS
end
end
local function i18n()
local function i18n()
   if I18N then return I18N end
   if I18N then return I18N end
   for _,t in ipairs{ "Module:HeroI18n","Modul:HeroI18n" } do
   for _,t in ipairs{ "Module:HeroI18n","Modul:HeroI18n" } do
     local ok,d=pcall(mw.loadData,t); if ok and type(d)=="table" then I18N=d;return I18N end
     local ok, data = pcall(mw.loadData, t)
     local ok2,m=pcall(require,t);    if ok2 and type(m)=="table" then I18N=m;return I18N end
    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
   end
   I18N={ i18n={}, events={names={},sections={},infobox={},phrases={}} }; return I18N
   I18N = { i18n={}, events={names={},sections={},infobox={},links={},units={}} }
  return I18N
end
 
-- ===== translations/formatters =====
local function tr(kind, val)
  if isempty(val) then return val end
  local map = (i18n().i18n or {})[kind] or {}
  return map[norm(val)] or val
end
 
local function tr_month(val)
  if isempty(val) then return val end
  return tr("month", val)
end
 
local function tr_event_name(evkey)
  local names = (i18n().events or {}).names or {}
  return names[norm(evkey)] or evkey
end
end


-- ===== translations & helpers =====
local function tr(kind, v) if isempty(v) then return v end local map=(i18n().i18n or {})[kind] or {}; return map[norm(v)] or v end
local function tr_currency(k) return tr("currency",k) end
local function tr_event_name(k) local names=(i18n().events or {}).names or {}; return names[norm(k)] or k end
local function section_label(evkey, which)
local function section_label(evkey, which)
   local E=i18n().events or {}; local ev=(E.sections and E.sections[norm(evkey)]) or {}; local def=(E.sections and E.sections.default) or {}
   local E = i18n().events or {}
  local ev = (E.sections and E.sections[norm(evkey)]) or {}
  local def= (E.sections and E.sections.default) or {}
   return (ev[which] or def[which] or "")
   return (ev[which] or def[which] or "")
end
end
local function month_tr(s) local map=((i18n().i18n or {}).month) or {}; return map[norm(s or "")] or (s or "") end
 
local function format_unlocked(raw)
local function type_display(t)
  local P=(i18n().events or {}).phrases or {}; local tpl=P.character_level or "Level $1"
  local map = ((i18n().events or {}).infobox or {}).type or {}
  local n=tostring(raw or ""):match("^character_level_(%d+)$"); if n then return (tpl:gsub("%$1",n)) end
  return map[norm(t or "")] or (t or "")
  return raw or ""
end
end
local function resolve_event(key) local evs=events(); local real=pick_key(evs,key); return real and evs[real] or nil end


-- Drop-Block rendern
local function type_link(t)
local function render_drop(ev)
   local L = ((i18n().events or {}).links or {}).type or {}
   local D=ev.drop or {}; local P=(i18n().events or {}).phrases or {}; local token_key=ev.token and ev.token.key or ""; local lines={}
   local target = L[norm(t or "")] or ""
   if D.base and D.base.amount and D.base.period then
  local disp  = type_display(t)
    table.insert(lines, "* "..(P.drop_rate_base or "Drop-Rate: $1 $2 alle $3 (Basis).")
  if target~="" then
      :gsub("%$1",tostring(D.base.amount)):gsub("%$2",tr_currency(token_key)):gsub("%$3",tostring(D.base.period)))
    return string.format("[[%s|%s]]", target, disp)
  else
    return disp
   end
   end
   if D.boost and D.boost.amount and D.boost.period then
end
    table.insert(lines, "* "..(P.drop_rate_boost or "Drop-Rate: $1 $2 alle $3 (nach Kauf).")
 
      :gsub("%$1",tostring(D.boost.amount)):gsub("%$2",tr_currency(token_key)):gsub("%$3",tostring(D.boost.period)))
local function fmt_duration(s)
   if isempty(s) then return s end
  local U = (i18n().events or {}).units or {}
  local singular = U.week_singular or "Woche"
  local plural  = U.week_plural  or "Wochen"
  -- "2 weeks" / "1 week"
  local n = s:match("^(%d+)%s+weeks?$")
  if n then
    local num = tonumber(n) or 0
    return string.format("%d %s", num, (num==1 and singular or plural))
   end
   end
   if D.base_daily_gain then
   return s
    table.insert(lines, "* "..(P.fixed_daily_gain or "Fester Zuwachs: $1 $2 pro Tag.")
end
      :gsub("%$1",tostring(D.base_daily_gain)):gsub("%$2",tr_currency(token_key)))
 
local function fmt_unlocks(v)
  -- "character_level_10" -> "Level 10"
  if isempty(v) then return v end
  local n = tostring(v):match("^character_level_(%d+)$")
  if n then
    local fmt = (((i18n().events or {}).infobox or {}).unlocks_format) or "Level $1"
    return (fmt:gsub("%$1", n))
   end
   end
   if D.offline_cap then
  return v
     table.insert(lines, "* "..(P.offline_cap or "Offline-Gewinne sind auf $1 begrenzt.")
end
       :gsub("%$1",tostring(D.offline_cap)))
 
-- ===== event helpers =====
local function resolve_event(key)
  local evs = events()
  local real = pick_key(evs, key)
  return real and evs[real] or nil
end
 
local function render_history(ev)
  local H = ev.history or {}
   if #H==0 then return "" end
  local lines = {}
  for _,h in ipairs(H) do
     local base = string.format("* %s: %s bis %s", tostring(h.year or ""), tostring(h.start or ""), tostring(h["end"] or ""))
    if h.alt_platform and h.alt_platform~="" then
       base = base .. string.format(" (%s auf Google Play)", tostring(h.alt_platform))
    end
    table.insert(lines, base)
   end
   end
   return table.concat(lines,"\n")
   return table.concat(lines, "\n")
end
end


-- Avatartabellen
local function render_avatars_all(ev, evkey)
local function render_avatar_year(ev, year)
   local AV = ev.avatars or {}
   local list = ev.avatars and ev.avatars[year]; if type(list)~="table" then return "" end
  -- Jahre sortieren
   local token_key=ev.token and ev.token.key or ""; local token_icon=ev.token and ev.token.icon or ""
  local years = {}
   local out={}
  for y,_ in pairs(AV) do table.insert(years, y) end
  table.insert(out, '{| class="article-table" style="font-size:14px;"')
  table.sort(years)
  table.insert(out, '! colspan="2" | Avatar\n! Preis')
  local out = {}
  for _,it in ipairs(list) do
   local token_key = ev.token and ev.token.key or ""
    table.insert(out, '|-')
  local token_icon= ev.token and ev.token.icon or ""
    table.insert(out, string.format('|%s || %s || %s %s %s',
   local tokens_lbl= section_label(evkey, "tokens")
      fileWikitext(it.file,"50px"), it.title or "",
 
      fileWikitext(token_icon,"25px"), tostring(it.cost or 0), tr_currency(token_key)))
  for _,year in ipairs(years) do
    local list = AV[year]
    table.insert(out, string.format("==== %d ====", year))
    table.insert(out, '{| class="article-table" style="font-size: 14px;"')
    table.insert(out, '!colspan="2" | Avatar\n!Preis')
    for _,it in ipairs(list) do
      table.insert(out, "|-")
      table.insert(out,
        string.format("|[[File:%s|50px]] || %s || [[File:%s|25px]] %s %s",
          tostring(it.file or ""),
          tostring(it.title or ""),
          tostring(token_icon or ""),
          tostring(it.cost or 0),
          tokens_lbl
        )
      )
    end
    table.insert(out, "|}")
    table.insert(out, "") -- Leerzeile
   end
   end
  table.insert(out, '|}')
   return table.concat(out, "\n")
   return table.concat(out, "\n")
end
end


local function render_avatars_all(ev)
local function render_exchange(ev, evkey)
   local A = ev.avatars or {}; local years={}
   local tok_icon = ev.token and ev.token.icon or ""
   for y,_ in pairs(A) do table.insert(years, tonumber(y) or y) end
  local tokens  = section_label(evkey, "tokens")
   table.sort(years)
 
   local out={}
  local rows = {}
   for _,y in ipairs(years) do
  local function push_header()
    table.insert(out, string.format("==== %s ====\n%s", tostring(y), render_avatar_year(ev, y)))
    table.insert(rows, '{| class="article-table" style="font-size: 14px;"')
    table.insert(rows, "!Angebot\n!Preis\n!Limit")
  end
 
  local function push_row_offer(range, icon, title, price, limit)
    table.insert(rows, "|-")
    table.insert(rows, string.format("|%s: [[File:%s|30px]] 1 [[%s|%s]] || [[File:%s|25px]] %s %s || %s",
      range, icon, title, title, tok_icon, tostring(price or 0), tokens, tostring(limit or "")
    ))
  end
 
  local function push_row_currency(icon, amount, title, price, limit)
    table.insert(rows, "|-")
    table.insert(rows, string.format("|[[File:%s|30px]] %s [[%s|%s]] || [[File:%s|25px]] %s %s || %s",
      icon, tostring(amount or 0), title, title, tok_icon, tostring(price or 0), tokens, tostring(limit or "")
    ))
  end
 
  push_header()
  for _,it in ipairs(ev.exchange.chests_by_level or {}) do
    push_row_offer("Stufe "..it.range, it.icon, it.title, it.price, it.limit)
  end
   for _,it in ipairs(ev.exchange.chests_by_stars or {}) do
    push_row_offer("Sterne "..it.range, it.icon, it.title, it.price, it.limit)
  end
  for _,it in ipairs(ev.exchange.chests_by_oracle or {}) do
    push_row_offer("Orakel "..it.range, it.icon, it.title, it.price, it.limit)
  end
  for _,it in ipairs(ev.exchange.currencies or {}) do
    push_row_currency(it.icon, it.amount, it.title, it.price, it.limit)
  end
  table.insert(rows, "|}")
  -- Fußnote (optional)
  local gp_req = deep_get(ev, {"exchange","notes","gp_req"})
  if gp_req and gp_req~="" then
    table.insert(rows, "*erfordert außerdem mehr als "..gp_req.." gesamte [[Gear Power|Ausrüstungsstärke]]")
  end
   return table.concat(rows, "\n")
end
 
local function render_offers(ev, kind, evkey)
  local list = (kind=="start") and ((ev.shop or {}).initial) or ((ev.shop or {}).after)
  if type(list)~="table" or #list==0 then return "" end
  local tokens = section_label(evkey, "tokens")
 
   if kind=="start" then
    -- 3 Spalten wie im Original
    local out = {
      '{| class="article-table" style="font-size: 14px;"',
      '! colspan="2" style="text-align:center" | '..(list[1].name or "")..'<br />[[File:'..(list[1].image or "")..'|x50px]]',
      '! colspan="2" style="text-align:center" | '..(list[2].name or "")..'<br />[[File:'..(list[2].image or "")..'|x50px]]',
      '! colspan="2" style="text-align:center" | '..(list[3].name or "")..'<br />[[File:'..(list[3].image or "")..'|x50px]]',
      '|-',
      '[[File:CurrencyPumpkin.png|30px]] || '..tostring(list[1].token or 0).." "..tokens,
      '[[File:CurrencyPumpkin.png|30px]] || '..tostring(list[2].token or 0).." "..tokens,
      '[[File:CurrencyPumpkin.png|30px]] || '..tostring(list[3].token or 0).." "..tokens,
      '|-',
      'Preis: || *',
      'Preis: || *',
      'Preis: || *',
      '|}',
      '&nbsp;* der Preis hängt von deiner Plattform ab',
    }
    return table.concat(out, "\n")
   else
    -- 4 Spalten wie im Original
    local headers = {}
    table.insert(headers, '{| class="article-table" style="font-size: 14px;"')
    table.insert(headers, "|-")
    for i=1,#list do
      local it=list[i]
      table.insert(headers, string.format('! colspan="2" style="text-align:center" | %s<br />[[File:%s|x50px]]', it.name or "", it.image or ""))
    end
    table.insert(headers, "|-")
    local prices = {}
    for i=1,#list do
      local it=list[i]
      table.insert(prices, string.format('[[File:CurrencyPumpkin.png|30px]] || %s %s', tostring(it.token or 0), tokens))
    end
    local foot = {}
    for i=1,#list do table.insert(foot, "Preis: || *") end
    table.insert(headers, table.concat(prices, "\n"))
    table.insert(headers, "|-")
    table.insert(headers, table.concat(foot, "\n"))
    table.insert(headers, "|}")
    table.insert(headers, "&nbsp;* der Preis hängt von deiner Plattform ab")
    return table.concat(headers, "\n")
   end
   end
  return table.concat(out, "\n")
end
end


-- History
local function render_file(evkey, which, ev)
local function render_history(ev)
   ev = ev or {}
   local H=ev.history or {}; if #H==0 then return "" end
   local imgs = ev.images or {}
   local P=(i18n().events or {}).phrases or {}; local alt_on_gp=P.alt_platform or "auf Google Play"
   local it = imgs[which] or {}
   local lines={}
   local caption
   for _,h in ipairs(H) do
  if which=="deco" then caption = section_label(evkey, "deco_caption")
    local base = string.format("* %s: %s bis %s", tostring(h.year or ""), tostring(h.start or ""), tostring(h['end'] or ""))
  elseif which=="ex_shop" then caption = section_label(evkey, "hub")
    if h.alt_platform then base = base .. string.format(" (%s %s)", tostring(h.alt_platform), alt_on_gp) end
  elseif which=="fullname" then caption = section_label(evkey, "fullname")
    table.insert(lines, base)
   end
   end
   return table.concat(lines, "\n")
   return fileWikitext(it.file, {thumb=true, caption=caption})
end
end


-- ===== feature: Event (einheitlich) =====
-- ====== EVENT dispatcher ======
-- {{Firestone|Event|halloween|...}}
local function fn_event(args)
local function fn_event(args)
   local key=args[2]; if isempty(key) then return "" end
   local key = args[2]; if isempty(key) then return "" end
   local ev=resolve_event(key); if not ev then return "" end
   local ev = resolve_event(key); if not ev then return "" end
   if not args[3] then return "" end
   local evkey = pick_key(events(), key) or key


   -- Labels/Neben-Namensräume
  local a3, a4 = args[3], args[4]
   if args[3]=="@infobox" and args[4] then
 
     local E=i18n().events or {}
  if isempty(a3) then return "" end
    local lbl=(E.infobox and E.infobox[args[4]]) or ""
 
     return tostring(lbl or "")
   -- Überschriften
   if a3=="h2" then
     return heading(2, section_label(evkey, a4 or ""))
  elseif a3=="h3" then
     return heading(3, section_label(evkey, a4 or ""))
   end
   end
   if args[3]=="@section" and args[4] then
 
     return section_label(key, args[4])
  -- Bilder
   if a3=="file" then
     return render_file(evkey, a4 or "", ev)
   end
   end
   if (args[3]=="@phrase" or args[3]=="@phrases") and args[4] then
 
     local P=(i18n().events or {}).phrases or {}
  -- Links
     return tostring(P[args[4]] or "")
   if a3=="link" and a4=="type" then
    return type_link(ev.type)
  end
 
  -- zusammengesetzte Renderings
  if a3=="history" then
     return render_history(ev)
  elseif a3=="avatars" then
    return render_avatars_all(ev, evkey)
  elseif a3=="exchange" then
     return render_exchange(ev, evkey)
  elseif a3=="offers" then
    local kind = args[4] -- "start" | "more"
    return render_offers(ev, kind, evkey)
   end
   end


   -- Normaler Pfad …
   -- einfache Felder/Labels
   local path, i = {}, 3
   if a3=="banner" then
   while args[i] do table.insert(path,args[i]); i=i+1 end
    return tostring(ev.banner or "")
   local last = norm(path[#path] or "")
   elseif a3=="type" then
    return type_display(ev.type)
  elseif a3=="month" or a3=="date" then
    return tr_month(ev.month or ev.date or "")
  elseif a3=="duration" then
    return fmt_duration(ev.duration)
   elseif a3=="unlocks_at" then
    return fmt_unlocks(ev.unlocks_at)
  elseif a3=="eventname" then
    return tr_event_name(evkey)
  end


   -- bequeme Aliasse
   -- Freiform-Schlüssel wie "character_level_10"
  if last=="month" then path[#path]="date"; last="date" end
   do
  if last=="name"  then return tr_event_name(key) or section_label(key,"name") end
     local maybe = fmt_unlocks(a3)
   if last=="history" then return render_history(ev) end
     if maybe ~= a3 then return maybe end
  if last=="avatars" then
     local year = tonumber(path[4] or "")
     if year then return render_avatar_year(ev, year) else return render_avatars_all(ev) end
   end
   end
  if last=="drop" then return render_drop(ev) end


  -- reine Label-Getter aus Sections (currencies, shopname, ex_shop, fullname, shortname, name …)
  local lbl = section_label(evkey, a3)
  if not isempty(lbl) then return lbl end
  -- Fallback auf tiefe Pfade: {{Firestone|Event|name|path|to|field}}
  local path, i = {}, 3
  while args[i] do table.insert(path, args[i]); i=i+1 end
   local v = deep_get(ev, path)
   local v = deep_get(ev, path)
  if last=="date" then
    v = month_tr(v)
  elseif last=="unlocks_at" then
    v = format_unlocked(v)
  elseif last=="type" then
    local map=((i18n().events or {}).infobox or {}).type or {}
    v = map[norm(v or "")] or (v or "")
  end
  if type(v)=="string" then v=mw.text.trim(v) end
   return tostring(v or "")
   return tostring(v or "")
end
end


-- ===== feature: currency / hero (bleibt) =====
-- ===== main =====
local function fn_currency(args) local key=args[2]; if isempty(key) then return "" end; return tostring(tr_currency(key) or "") end
function M.main(frame)
local function fn_hero(args)
   local a = getArgs(frame)
   local name=args[2]; if isempty(name) then return "" end
   -- numerische Parameter durchreichen
   local H=heroes(); local real=pick_key(H,name); if not real then return "" end
   for i=1,12 do a[i] = a[i] and tostring(a[i]) or nil end
   local hero=H[real]; if not hero then return "" end
   local dom = a[1] and norm(a[1]) or ""
  local path,i={},3; while args[i] do table.insert(path,args[i]); i=i+1 end
   local v=deep_get(hero,path); local last=norm(path[#path] or "")
  if last=="class" then return tr("class",v) or ""
  elseif last=="attackstyle" then return tr("attackstyle",v) or ""
  elseif last=="specialization" then return tr("specialization",v) or ""
  elseif last=="resource" then return tr("resource",v) or ""
  else return tostring(v or "") end
end


-- ===== main dispatcher =====
   if dom=="event" then
function M.main(frame)
     return fn_event(a)
  local a=getArgs(frame); for i=1,10 do a[i]=a[i] and tostring(a[i]) or nil end
   else
  local dom=a[1] and norm(a[1]) or ""
    return ""
   if dom=="event"    then return fn_event(a)
   end
   elseif dom=="currency" then return fn_currency(a)
  elseif dom=="hero"     then return fn_hero(a)
   else return "" end
end
end


return M
return M

Version vom 20. Oktober 2025, 04:32 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

local function getArgs(frame)
  -- bevorzugt die Parameter der Vorlage (Elternframe)
  local a, p = {}, frame:getParent()
  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
  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 fileWikitext(file, opts)
  if isempty(file) then return "" end
  local parts = {"File:"..file}
  if opts and opts.size  then table.insert(parts, opts.size) end
  if opts and opts.link  then table.insert(parts, "link="..opts.link) end
  if opts and opts.class then table.insert(parts, "class="..opts.class) end
  if opts and opts.thumb then
    table.insert(parts, "thumb")
    if opts.caption and opts.caption~="" then table.insert(parts, opts.caption) end
  end
  return string.format("[[%s]]", table.concat(parts, "|"))
end

local function heading(level, text)
  if isempty(text) then return "" end
  local marks = (level==3) and "===" or "=="
  return string.format("%s %s %s", marks, text, marks)
end

-- ===== lazy loads =====
local HEROES, EVENTS, I18N

local function heroes()
  if HEROES then return HEROES end
  for _,t in ipairs{ "Module:HeroData","Modul: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

local function events()
  if EVENTS then return EVENTS end
  for _,t in ipairs{ "Module:EventData","Modul: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
  end
  for _,t in ipairs{ "Module:EventData","Modul:EventData" } do
    local ok, mod = pcall(require, t)
    if ok and type(mod)=="table" and type(mod.events)=="table" then EVENTS=mod.events; return EVENTS end
  end
  EVENTS = {}; return EVENTS
end

local function i18n()
  if I18N then return I18N end
  for _,t in ipairs{ "Module:HeroI18n","Modul: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={},infobox={},links={},units={}} }
  return I18N
end

-- ===== translations/formatters =====
local function tr(kind, val)
  if isempty(val) then return val end
  local map = (i18n().i18n or {})[kind] or {}
  return map[norm(val)] or val
end

local function tr_month(val)
  if isempty(val) then return val end
  return tr("month", val)
end

local function tr_event_name(evkey)
  local names = (i18n().events or {}).names or {}
  return names[norm(evkey)] or evkey
end

local function section_label(evkey, which)
  local E = i18n().events or {}
  local ev = (E.sections and E.sections[norm(evkey)]) or {}
  local def= (E.sections and E.sections.default) or {}
  return (ev[which] or def[which] or "")
end

local function type_display(t)
  local map = ((i18n().events or {}).infobox or {}).type or {}
  return map[norm(t or "")] or (t or "")
end

local function type_link(t)
  local L = ((i18n().events or {}).links or {}).type or {}
  local target = L[norm(t or "")] or ""
  local disp   = type_display(t)
  if target~="" then
    return string.format("[[%s|%s]]", target, disp)
  else
    return disp
  end
end

local function fmt_duration(s)
  if isempty(s) then return s end
  local U = (i18n().events or {}).units or {}
  local singular = U.week_singular or "Woche"
  local plural   = U.week_plural   or "Wochen"
  -- "2 weeks" / "1 week"
  local n = s:match("^(%d+)%s+weeks?$")
  if n then
    local num = tonumber(n) or 0
    return string.format("%d %s", num, (num==1 and singular or plural))
  end
  return s
end

local function fmt_unlocks(v)
  -- "character_level_10" -> "Level 10"
  if isempty(v) then return v end
  local n = tostring(v):match("^character_level_(%d+)$")
  if n then
    local fmt = (((i18n().events or {}).infobox or {}).unlocks_format) or "Level $1"
    return (fmt:gsub("%$1", n))
  end
  return v
end

-- ===== event helpers =====
local function resolve_event(key)
  local evs = events()
  local real = pick_key(evs, key)
  return real and evs[real] or nil
end

local function render_history(ev)
  local H = ev.history or {}
  if #H==0 then return "" end
  local lines = {}
  for _,h in ipairs(H) do
    local base = string.format("* %s: %s bis %s", tostring(h.year or ""), tostring(h.start or ""), tostring(h["end"] or ""))
    if h.alt_platform and h.alt_platform~="" then
      base = base .. string.format(" (%s auf Google Play)", tostring(h.alt_platform))
    end
    table.insert(lines, base)
  end
  return table.concat(lines, "\n")
end

local function render_avatars_all(ev, evkey)
  local AV = ev.avatars or {}
  -- Jahre sortieren
  local years = {}
  for y,_ in pairs(AV) do table.insert(years, y) end
  table.sort(years)
  local out = {}
  local token_key = ev.token and ev.token.key or ""
  local token_icon= ev.token and ev.token.icon or ""
  local tokens_lbl= section_label(evkey, "tokens")

  for _,year in ipairs(years) do
    local list = AV[year]
    table.insert(out, string.format("==== %d ====", year))
    table.insert(out, '{| class="article-table" style="font-size: 14px;"')
    table.insert(out, '!colspan="2" | Avatar\n!Preis')
    for _,it in ipairs(list) do
      table.insert(out, "|-")
      table.insert(out,
        string.format("|[[File:%s|50px]] || %s || [[File:%s|25px]] %s %s",
          tostring(it.file or ""),
          tostring(it.title or ""),
          tostring(token_icon or ""),
          tostring(it.cost or 0),
          tokens_lbl
        )
      )
    end
    table.insert(out, "|}")
    table.insert(out, "") -- Leerzeile
  end
  return table.concat(out, "\n")
end

local function render_exchange(ev, evkey)
  local tok_icon = ev.token and ev.token.icon or ""
  local tokens   = section_label(evkey, "tokens")

  local rows = {}
  local function push_header()
    table.insert(rows, '{| class="article-table" style="font-size: 14px;"')
    table.insert(rows, "!Angebot\n!Preis\n!Limit")
  end

  local function push_row_offer(range, icon, title, price, limit)
    table.insert(rows, "|-")
    table.insert(rows, string.format("|%s: [[File:%s|30px]] 1 [[%s|%s]] || [[File:%s|25px]] %s %s || %s",
      range, icon, title, title, tok_icon, tostring(price or 0), tokens, tostring(limit or "")
    ))
  end

  local function push_row_currency(icon, amount, title, price, limit)
    table.insert(rows, "|-")
    table.insert(rows, string.format("|[[File:%s|30px]] %s [[%s|%s]] || [[File:%s|25px]] %s %s || %s",
      icon, tostring(amount or 0), title, title, tok_icon, tostring(price or 0), tokens, tostring(limit or "")
    ))
  end

  push_header()
  for _,it in ipairs(ev.exchange.chests_by_level or {}) do
    push_row_offer("Stufe "..it.range, it.icon, it.title, it.price, it.limit)
  end
  for _,it in ipairs(ev.exchange.chests_by_stars or {}) do
    push_row_offer("Sterne "..it.range, it.icon, it.title, it.price, it.limit)
  end
  for _,it in ipairs(ev.exchange.chests_by_oracle or {}) do
    push_row_offer("Orakel "..it.range, it.icon, it.title, it.price, it.limit)
  end
  for _,it in ipairs(ev.exchange.currencies or {}) do
    push_row_currency(it.icon, it.amount, it.title, it.price, it.limit)
  end
  table.insert(rows, "|}")
  -- Fußnote (optional)
  local gp_req = deep_get(ev, {"exchange","notes","gp_req"})
  if gp_req and gp_req~="" then
    table.insert(rows, "&ast;erfordert außerdem mehr als "..gp_req.." gesamte [[Gear Power|Ausrüstungsstärke]]")
  end
  return table.concat(rows, "\n")
end

local function render_offers(ev, kind, evkey)
  local list = (kind=="start") and ((ev.shop or {}).initial) or ((ev.shop or {}).after)
  if type(list)~="table" or #list==0 then return "" end
  local tokens = section_label(evkey, "tokens")

  if kind=="start" then
    -- 3 Spalten wie im Original
    local out = {
      '{| class="article-table" style="font-size: 14px;"',
      '! colspan="2" style="text-align:center" | '..(list[1].name or "")..'<br />[[File:'..(list[1].image or "")..'|x50px]]',
      '! colspan="2" style="text-align:center" | '..(list[2].name or "")..'<br />[[File:'..(list[2].image or "")..'|x50px]]',
      '! colspan="2" style="text-align:center" | '..(list[3].name or "")..'<br />[[File:'..(list[3].image or "")..'|x50px]]',
      '|-',
      '[[File:CurrencyPumpkin.png|30px]] || '..tostring(list[1].token or 0).." "..tokens,
      '[[File:CurrencyPumpkin.png|30px]] || '..tostring(list[2].token or 0).." "..tokens,
      '[[File:CurrencyPumpkin.png|30px]] || '..tostring(list[3].token or 0).." "..tokens,
      '|-',
      'Preis: || *',
      'Preis: || *',
      'Preis: || *',
      '|}',
      '&nbsp;* der Preis hängt von deiner Plattform ab',
    }
    return table.concat(out, "\n")
  else
    -- 4 Spalten wie im Original
    local headers = {}
    table.insert(headers, '{| class="article-table" style="font-size: 14px;"')
    table.insert(headers, "|-")
    for i=1,#list do
      local it=list[i]
      table.insert(headers, string.format('! colspan="2" style="text-align:center" | %s<br />[[File:%s|x50px]]', it.name or "", it.image or ""))
    end
    table.insert(headers, "|-")
    local prices = {}
    for i=1,#list do
      local it=list[i]
      table.insert(prices, string.format('[[File:CurrencyPumpkin.png|30px]] || %s %s', tostring(it.token or 0), tokens))
    end
    local foot = {}
    for i=1,#list do table.insert(foot, "Preis: || *") end
    table.insert(headers, table.concat(prices, "\n"))
    table.insert(headers, "|-")
    table.insert(headers, table.concat(foot, "\n"))
    table.insert(headers, "|}")
    table.insert(headers, "&nbsp;* der Preis hängt von deiner Plattform ab")
    return table.concat(headers, "\n")
  end
end

local function render_file(evkey, which, ev)
  ev = ev or {}
  local imgs = ev.images or {}
  local it = imgs[which] or {}
  local caption
  if which=="deco" then caption = section_label(evkey, "deco_caption")
  elseif which=="ex_shop" then caption = section_label(evkey, "hub")
  elseif which=="fullname" then caption = section_label(evkey, "fullname")
  end
  return fileWikitext(it.file, {thumb=true, caption=caption})
end

-- ====== EVENT dispatcher ======
local function fn_event(args)
  local key = args[2]; if isempty(key) then return "" end
  local ev = resolve_event(key); if not ev then return "" end
  local evkey = pick_key(events(), key) or key

  local a3, a4 = args[3], args[4]

  if isempty(a3) then return "" end

  -- Überschriften
  if a3=="h2" then
    return heading(2, section_label(evkey, a4 or ""))
  elseif a3=="h3" then
    return heading(3, section_label(evkey, a4 or ""))
  end

  -- Bilder
  if a3=="file" then
    return render_file(evkey, a4 or "", ev)
  end

  -- Links
  if a3=="link" and a4=="type" then
    return type_link(ev.type)
  end

  -- zusammengesetzte Renderings
  if a3=="history" then
    return render_history(ev)
  elseif a3=="avatars" then
    return render_avatars_all(ev, evkey)
  elseif a3=="exchange" then
    return render_exchange(ev, evkey)
  elseif a3=="offers" then
    local kind = args[4] -- "start" | "more"
    return render_offers(ev, kind, evkey)
  end

  -- einfache Felder/Labels
  if a3=="banner" then
    return tostring(ev.banner or "")
  elseif a3=="type" then
    return type_display(ev.type)
  elseif a3=="month" or a3=="date" then
    return tr_month(ev.month or ev.date or "")
  elseif a3=="duration" then
    return fmt_duration(ev.duration)
  elseif a3=="unlocks_at" then
    return fmt_unlocks(ev.unlocks_at)
  elseif a3=="eventname" then
    return tr_event_name(evkey)
  end

  -- Freiform-Schlüssel wie "character_level_10"
  do
    local maybe = fmt_unlocks(a3)
    if maybe ~= a3 then return maybe end
  end

  -- reine Label-Getter aus Sections (currencies, shopname, ex_shop, fullname, shortname, name …)
  local lbl = section_label(evkey, a3)
  if not isempty(lbl) then return lbl end

  -- Fallback auf tiefe Pfade: {{Firestone|Event|name|path|to|field}}
  local path, i = {}, 3
  while args[i] do table.insert(path, args[i]); i=i+1 end
  local v = deep_get(ev, path)
  return tostring(v or "")
end

-- ===== main =====
function M.main(frame)
  local a = getArgs(frame)
  -- numerische Parameter durchreichen
  for i=1,12 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 fn_event(a)
  else
    return ""
  end
end

return M