<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="de">
	<id>https://firestone-idle-rpg.de/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Webmaster</id>
	<title>Firestone Idle RPG Wiki - Benutzerbeiträge [de]</title>
	<link rel="self" type="application/atom+xml" href="https://firestone-idle-rpg.de/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Webmaster"/>
	<link rel="alternate" type="text/html" href="https://firestone-idle-rpg.de/wiki/Spezial:Beitr%C3%A4ge/Webmaster"/>
	<updated>2026-06-15T18:44:20Z</updated>
	<subtitle>Benutzerbeiträge</subtitle>
	<generator>MediaWiki 1.44.2</generator>
	<entry>
		<id>https://firestone-idle-rpg.de/index.php?title=MediaWiki:Common.js&amp;diff=3780</id>
		<title>MediaWiki:Common.js</title>
		<link rel="alternate" type="text/html" href="https://firestone-idle-rpg.de/index.php?title=MediaWiki:Common.js&amp;diff=3780"/>
		<updated>2025-10-30T11:08:34Z</updated>

		<summary type="html">&lt;p&gt;Webmaster: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* Das folgende JavaScript wird für alle Benutzer geladen. */&lt;br /&gt;
/* ==== Sidebar: klickbare Kopfzeile + Gruppen mit Auf/Zu + Persistenz (Vector-2022 &amp;amp; Drawer) ==== */&lt;br /&gt;
(function () {&lt;br /&gt;
  &#039;use strict&#039;;&lt;br /&gt;
  if ( mw.config.get(&#039;skin&#039;) !== &#039;vector-2022&#039; ) return;&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Persistenz ---------- */&lt;br /&gt;
  var STORAGE_KEY = &#039;kr-sb-state:v3:&#039; + ( mw.config.get(&#039;wgUserName&#039;) || &#039;anon&#039; );&lt;br /&gt;
  function loadState() {&lt;br /&gt;
    try { return JSON.parse( localStorage.getItem(STORAGE_KEY) || &#039;{}&#039; ); }&lt;br /&gt;
    catch (e) { return {}; }&lt;br /&gt;
  }&lt;br /&gt;
  function saveState(state) {&lt;br /&gt;
    try { localStorage.setItem( STORAGE_KEY, JSON.stringify(state) ); }&lt;br /&gt;
    catch (e) {}&lt;br /&gt;
  }&lt;br /&gt;
  var STATE = loadState();&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Utils ---------- */&lt;br /&gt;
  function extractDirective(a) {&lt;br /&gt;
    if (!a) return null;&lt;br /&gt;
    var href = a.getAttribute(&#039;href&#039;) || &#039;&#039;;&lt;br /&gt;
    try { href = new URL(href, location.href).hash || &#039;&#039;; } catch (e) {}&lt;br /&gt;
    var s = (href || &#039;&#039;).trim().toLowerCase();&lt;br /&gt;
    if (s.includes(&#039;#group:&#039;)) return { kind: &#039;group&#039;, value: decodeURIComponent(s.split(&#039;#group:&#039;).pop()) };&lt;br /&gt;
    if (s.includes(&#039;#link:&#039;))  return { kind: &#039;link&#039;,  value: decodeURIComponent(s.split(&#039;#link:&#039;).pop())  };&lt;br /&gt;
    return null;&lt;br /&gt;
  }&lt;br /&gt;
  function stripDirectiveHash(a) {&lt;br /&gt;
    if (!a) return;&lt;br /&gt;
    a.setAttribute(&#039;href&#039;, (a.getAttribute(&#039;href&#039;) || &#039;&#039;).replace(/#(?:link|group):[^#]*$/i, &#039;&#039;) );&lt;br /&gt;
  }&lt;br /&gt;
  function hasRealHref(a) {&lt;br /&gt;
    if (!a) return false;&lt;br /&gt;
    return !/^#/.test( a.getAttribute(&#039;href&#039;) || &#039;&#039; );&lt;br /&gt;
  }&lt;br /&gt;
  function normId(s) {&lt;br /&gt;
    return String(s || &#039;&#039;).trim().toLowerCase()&lt;br /&gt;
      .replace(/\s+/g, &#039;-&#039;).replace(/[^\w\-:.|]/g, &#039;&#039;);&lt;br /&gt;
  }&lt;br /&gt;
  function toggleSet(li, arrow, expanded) {&lt;br /&gt;
    if (expanded) { li.classList.remove(&#039;is-collapsed&#039;); arrow.setAttribute(&#039;aria-expanded&#039;,&#039;true&#039;); }&lt;br /&gt;
    else          { li.classList.add(&#039;is-collapsed&#039;);    arrow.setAttribute(&#039;aria-expanded&#039;,&#039;false&#039;); }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Kopf + ID ---------- */&lt;br /&gt;
  function makeHead(li, keepLink, containerUL, labelOverride) {&lt;br /&gt;
    var a = li.querySelector(&#039;:scope &amp;gt; a&#039;);&lt;br /&gt;
&lt;br /&gt;
    var head = document.createElement(&#039;div&#039;);&lt;br /&gt;
    head.className = &#039;kr-head has-toggle&#039;; // ganze Kopfzeile darf toggeln (außer Link)&lt;br /&gt;
&lt;br /&gt;
    var btn = document.createElement(&#039;button&#039;);&lt;br /&gt;
    btn.type = &#039;button&#039;;&lt;br /&gt;
    btn.className = &#039;kr-arrow&#039;;&lt;br /&gt;
    btn.setAttribute(&#039;aria-expanded&#039;, &#039;false&#039;);&lt;br /&gt;
    head.appendChild(btn);&lt;br /&gt;
&lt;br /&gt;
    if (keepLink &amp;amp;&amp;amp; a) { stripDirectiveHash(a); head.appendChild(a); }&lt;br /&gt;
    else {&lt;br /&gt;
      var span = document.createElement(&#039;span&#039;);&lt;br /&gt;
      span.className = &#039;kr-title&#039;;&lt;br /&gt;
      span.textContent = labelOverride || (a &amp;amp;&amp;amp; a.textContent) || &#039;&#039;;&lt;br /&gt;
      head.appendChild(span);&lt;br /&gt;
      if (a) a.remove();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    li.insertBefore(head, li.firstChild);&lt;br /&gt;
&lt;br /&gt;
    // stabile ID für Persistenz&lt;br /&gt;
    var portlet = containerUL.closest(&#039;.vector-menu&#039;);&lt;br /&gt;
    var pid = (portlet &amp;amp;&amp;amp; (portlet.id || portlet.getAttribute(&#039;id&#039;))) || &#039;portlet&#039;;&lt;br /&gt;
    var labelText = (labelOverride || (head.querySelector(&#039;a, .kr-title&#039;) || {}).textContent || &#039;&#039;).trim();&lt;br /&gt;
    li.dataset.krId = normId( pid + &#039;::&#039; + labelText );&lt;br /&gt;
&lt;br /&gt;
    // Default zu; gespeicherten Zustand anwenden&lt;br /&gt;
    toggleSet(li, btn, false);&lt;br /&gt;
    var st = STATE[ li.dataset.krId ];&lt;br /&gt;
    if (st === 1) toggleSet(li, btn, true);&lt;br /&gt;
&lt;br /&gt;
    return head;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Toggle-Handler ---------- */&lt;br /&gt;
  function handleArrowClick(btn) {&lt;br /&gt;
    var li = btn.closest(&#039;li.kr-group, li.kr-top&#039;);&lt;br /&gt;
    if (!li) return;&lt;br /&gt;
&lt;br /&gt;
    var willExpand = li.classList.contains(&#039;is-collapsed&#039;);&lt;br /&gt;
    toggleSet(li, btn, willExpand);&lt;br /&gt;
&lt;br /&gt;
    var id = li.dataset.krId;&lt;br /&gt;
    if (id) {&lt;br /&gt;
      STATE[id] = willExpand ? 1 : 0;&lt;br /&gt;
      saveState(STATE);&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* Globale Delegation: Pfeil klickt immer; Kopfzeile klickt nur, wenn NICHT im Link */&lt;br /&gt;
  document.addEventListener(&#039;click&#039;, function (e) {&lt;br /&gt;
    var arrow = e.target &amp;amp;&amp;amp; e.target.closest &amp;amp;&amp;amp; e.target.closest(&#039;.kr-arrow&#039;);&lt;br /&gt;
    if (arrow) {&lt;br /&gt;
      e.preventDefault();&lt;br /&gt;
      handleArrowClick(arrow);&lt;br /&gt;
      return;&lt;br /&gt;
    }&lt;br /&gt;
    var head = e.target &amp;amp;&amp;amp; e.target.closest &amp;amp;&amp;amp; e.target.closest(&#039;.kr-head&#039;);&lt;br /&gt;
    if (head &amp;amp;&amp;amp; !e.target.closest(&#039;.kr-head a&#039;)) {&lt;br /&gt;
      e.preventDefault(); // freie Fläche der Kopfzeile&lt;br /&gt;
      var btn = head.querySelector(&#039;.kr-arrow&#039;);&lt;br /&gt;
      if (btn) handleArrowClick(btn);&lt;br /&gt;
    }&lt;br /&gt;
  }, true); // capture=true, damit nichts dazwischenfunkt&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Aufbau der Gruppen ---------- */&lt;br /&gt;
  function buildGroups(ul) {&lt;br /&gt;
    var lis = Array.from( ul.querySelectorAll(&#039;:scope &amp;gt; li.mw-list-item&#039;) );&lt;br /&gt;
    var current = null;&lt;br /&gt;
&lt;br /&gt;
    lis.forEach(function (li) {&lt;br /&gt;
      var a = li.querySelector(&#039;:scope &amp;gt; a&#039;);&lt;br /&gt;
      var d = extractDirective(a);&lt;br /&gt;
&lt;br /&gt;
      if (d &amp;amp;&amp;amp; d.kind === &#039;group&#039;) {&lt;br /&gt;
        var clickable = hasRealHref(a);&lt;br /&gt;
        li.classList.add(&#039;kr-group&#039;);&lt;br /&gt;
        makeHead(li, clickable, ul, (a &amp;amp;&amp;amp; a.textContent) || d.value || &#039;&#039;);&lt;br /&gt;
        if (!clickable &amp;amp;&amp;amp; a) a.remove(); else if (clickable) stripDirectiveHash(a);&lt;br /&gt;
&lt;br /&gt;
        var sub = document.createElement(&#039;ul&#039;);&lt;br /&gt;
        sub.className = &#039;kr-sub&#039;;&lt;br /&gt;
        li.appendChild(sub);&lt;br /&gt;
        current = sub;&lt;br /&gt;
        return;&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      if (current &amp;amp;&amp;amp; !(d &amp;amp;&amp;amp; (d.kind === &#039;group&#039; || d.kind === &#039;link&#039;))) {&lt;br /&gt;
        current.appendChild(li);&lt;br /&gt;
      }&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Portlets verarbeiten ---------- */&lt;br /&gt;
  function buildPortlets(root) {&lt;br /&gt;
    var lists = root.querySelectorAll(&#039;.vector-menu .vector-menu-content-list&#039;);&lt;br /&gt;
    lists.forEach(function (ul) {&lt;br /&gt;
      if (ul.dataset.krSbDone) return;&lt;br /&gt;
      ul.dataset.krSbDone = &#039;1&#039;;&lt;br /&gt;
&lt;br /&gt;
      var items  = Array.from( ul.querySelectorAll(&#039;:scope &amp;gt; li.mw-list-item&#039;) );&lt;br /&gt;
      var topIdx = items.findIndex(function (li) {&lt;br /&gt;
        var a = li.querySelector(&#039;:scope &amp;gt; a&#039;);&lt;br /&gt;
        var d = extractDirective(a);&lt;br /&gt;
        return d &amp;amp;&amp;amp; d.kind === &#039;link&#039;;&lt;br /&gt;
      });&lt;br /&gt;
&lt;br /&gt;
      if (topIdx &amp;gt;= 0) {&lt;br /&gt;
        var top = items[topIdx];&lt;br /&gt;
        top.classList.add(&#039;kr-top&#039;);&lt;br /&gt;
        makeHead(top, true, ul);&lt;br /&gt;
        stripDirectiveHash( top.querySelector(&#039;:scope &amp;gt; a&#039;) );&lt;br /&gt;
&lt;br /&gt;
        var sub = document.createElement(&#039;ul&#039;);&lt;br /&gt;
        sub.className = &#039;kr-sub&#039;;&lt;br /&gt;
        top.appendChild(sub);&lt;br /&gt;
&lt;br /&gt;
        for (var i = topIdx + 1; i &amp;lt; items.length; i++) sub.appendChild(items[i]);&lt;br /&gt;
        buildGroups(sub);&lt;br /&gt;
      } else {&lt;br /&gt;
        buildGroups(ul);&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      // Direktiven aus hrefs entfernen; Vector-Portlet-Heading ausblenden&lt;br /&gt;
      ul.querySelectorAll(&#039;a[href*=&amp;quot;#link:&amp;quot;], a[href*=&amp;quot;#group:&amp;quot;]&#039;).forEach(stripDirectiveHash);&lt;br /&gt;
      var portlet = ul.closest(&#039;.vector-menu&#039;);&lt;br /&gt;
      var heading = portlet &amp;amp;&amp;amp; portlet.querySelector(&#039;:scope &amp;gt; .vector-menu-heading&#039;);&lt;br /&gt;
      if (heading) heading.remove();&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Bootstrapping ---------- */&lt;br /&gt;
  function processRoot(root) {&lt;br /&gt;
    if (!root || root.dataset.krSbRootDone) return;&lt;br /&gt;
    root.dataset.krSbRootDone = &#039;1&#039;;&lt;br /&gt;
    root.classList.add(&#039;kr-sb&#039;);&lt;br /&gt;
    buildPortlets(root);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function init() {&lt;br /&gt;
    document.querySelectorAll(&#039;#mw-panel, #vector-main-menu, #vector-main-menu-pinned, .vector-drawer&#039;)&lt;br /&gt;
      .forEach(processRoot);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  init();&lt;br /&gt;
  mw.hook(&#039;wikipage.content&#039;).add(init);&lt;br /&gt;
&lt;br /&gt;
  // Drawer/DOM-Änderungen beobachten&lt;br /&gt;
  var mo = new MutationObserver(function (muts) {&lt;br /&gt;
    muts.forEach(function (m) {&lt;br /&gt;
      m.addedNodes &amp;amp;&amp;amp; m.addedNodes.forEach(function (n) {&lt;br /&gt;
        if (!(n instanceof Element)) return;&lt;br /&gt;
        if (n.matches(&#039;#vector-main-menu, #vector-main-menu-pinned, .vector-drawer, #mw-panel&#039;)) processRoot(n);&lt;br /&gt;
        n.querySelectorAll &amp;amp;&amp;amp; n.querySelectorAll(&#039;#vector-main-menu, #vector-main-menu-pinned, .vector-drawer, #mw-panel&#039;)&lt;br /&gt;
          .forEach(processRoot);&lt;br /&gt;
      });&lt;br /&gt;
    });&lt;br /&gt;
  });&lt;br /&gt;
  mo.observe(document.body, { childList: true, subtree: true });&lt;br /&gt;
})();&lt;br /&gt;
&lt;br /&gt;
/* TabberNeue: Anker-Scroll unter Vector-Header + robustes „Pinning“ */&lt;br /&gt;
(function () {&lt;br /&gt;
  /* reale Sticky-Höhe (+0.5rem Luft) – korrekt für Gäste */&lt;br /&gt;
  function stickyOffsetPx() {&lt;br /&gt;
    var air = Math.round(parseFloat(getComputedStyle(document.documentElement).fontSize) * 0.5);&lt;br /&gt;
&lt;br /&gt;
    // eingeloggt zuverlässig erkennen (Logout-Link existiert nur dann)&lt;br /&gt;
    var isLoggedIn = !!document.getElementById(&#039;pt-logout&#039;);&lt;br /&gt;
&lt;br /&gt;
    // Vector 2022 Header-Container&lt;br /&gt;
    var hdr = document.querySelector(&#039;.vector-sticky-header-container, #vector-sticky-header&#039;);&lt;br /&gt;
    if (!isLoggedIn || !hdr) return air;            // Gäste: nur Luft&lt;br /&gt;
&lt;br /&gt;
    // nur zählen, wenn der Header wirklich sichtbar/aktiv ist&lt;br /&gt;
    var cs = getComputedStyle(hdr);&lt;br /&gt;
    var op = parseFloat(cs.opacity || &#039;1&#039;);&lt;br /&gt;
    var r  = hdr.getBoundingClientRect();&lt;br /&gt;
    var visible = op &amp;gt; 0.01 &amp;amp;&amp;amp; r.top &amp;lt;= 0 &amp;amp;&amp;amp; r.bottom &amp;gt; 0;&lt;br /&gt;
&lt;br /&gt;
    var h = visible ? Math.round(r.height) : 0;&lt;br /&gt;
    if (!h &amp;amp;&amp;amp; isLoggedIn) h = 50;                   // letzte Rettung nur für eingeloggte&lt;br /&gt;
    return h + air;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* oberste Tabber-Leiste finden */&lt;br /&gt;
  function headerForPanel(panel) {&lt;br /&gt;
    if (!panel) return null;&lt;br /&gt;
    var top = panel.closest(&#039;.tabber&#039;);&lt;br /&gt;
    for (var cur = top; cur &amp;amp;&amp;amp; cur.parentElement;) {&lt;br /&gt;
      var up = cur.parentElement.closest(&#039;.tabber&#039;);&lt;br /&gt;
      if (!up) break;&lt;br /&gt;
      top = up; cur = up;&lt;br /&gt;
    }&lt;br /&gt;
    return top ? top.querySelector(&#039;:scope &amp;gt; .tabber__header&#039;) : null;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* Panel/Tabber, die wir beobachten wollen */&lt;br /&gt;
  function getObserved(tabber) {&lt;br /&gt;
    var section = tabber &amp;amp;&amp;amp; tabber.querySelector(&#039;:scope &amp;gt; .tabber__section&#039;);&lt;br /&gt;
    return { tabber: tabber, section: section };&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* „Pinne“ den Viewport hart auf header/panel; löse erst, wenn stabil */&lt;br /&gt;
  function pinScrollToTarget(panelOrHeader) {&lt;br /&gt;
    var target = panelOrHeader;&lt;br /&gt;
    if (!target) return;&lt;br /&gt;
&lt;br /&gt;
    var tabber = target.closest(&#039;.tabber&#039;);&lt;br /&gt;
    var obs = getObserved(tabber);&lt;br /&gt;
&lt;br /&gt;
    /* Freeze: Höhe zum Start lesen und auf dem Container festhalten */&lt;br /&gt;
    if (tabber &amp;amp;&amp;amp; obs.section) {&lt;br /&gt;
      var h0 = Math.ceil(obs.section.getBoundingClientRect().height);&lt;br /&gt;
      tabber.style.setProperty(&#039;--tabber-freeze-h&#039;, h0 + &#039;px&#039;);&lt;br /&gt;
      tabber.classList.add(&#039;tabber-is-switching&#039;);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /* Smooth-Scrolling global aus, damit nichts „nachfedert“ */&lt;br /&gt;
    document.documentElement.classList.add(&#039;fs-scroll-now&#039;);&lt;br /&gt;
&lt;br /&gt;
    /* Fremde scrollIntoView()-Aufrufe währenddessen blocken */&lt;br /&gt;
    var origScrollIntoView = Element.prototype.scrollIntoView;&lt;br /&gt;
    Element.prototype.scrollIntoView = function(){ /* noop while pinned */ };&lt;br /&gt;
&lt;br /&gt;
    /* Beobachte Größenänderungen → Stability Counter zurücksetzen */&lt;br /&gt;
    var stable = 0, lastH = -1, raf = 0;&lt;br /&gt;
    var ro = null;&lt;br /&gt;
    if (obs.section &amp;amp;&amp;amp; window.ResizeObserver) {&lt;br /&gt;
      ro = new ResizeObserver(function(){ stable = 0; });&lt;br /&gt;
      try { ro.observe(obs.section); } catch(e){}&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    var MAX_MS = 3000;&lt;br /&gt;
    var t0 = performance.now();&lt;br /&gt;
&lt;br /&gt;
    function wantY() {&lt;br /&gt;
      return target.getBoundingClientRect().top + window.scrollY - stickyOffsetPx();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function release() {&lt;br /&gt;
      if (ro) ro.disconnect();&lt;br /&gt;
      Element.prototype.scrollIntoView = origScrollIntoView;&lt;br /&gt;
      document.documentElement.classList.remove(&#039;fs-scroll-now&#039;);&lt;br /&gt;
      if (tabber) {&lt;br /&gt;
        tabber.classList.remove(&#039;tabber-is-switching&#039;);&lt;br /&gt;
        tabber.style.removeProperty(&#039;--tabber-freeze-h&#039;);&lt;br /&gt;
      }&lt;br /&gt;
      cancelAnimationFrame(raf);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function tick() {&lt;br /&gt;
      var y = wantY();&lt;br /&gt;
      if (Math.abs(window.scrollY - y) &amp;gt; 1) {&lt;br /&gt;
        /* Hart auf die Ziel-Position setzen */&lt;br /&gt;
        window.scrollTo(0, y);&lt;br /&gt;
        stable = 0; /* wir mussten korrigieren → noch nicht stabil */&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      /* aktuelles Maß nehmen (Section bevorzugt, sonst Tabber) */&lt;br /&gt;
      var hNow = 0;&lt;br /&gt;
      if (obs.section) hNow = obs.section.getBoundingClientRect().height;&lt;br /&gt;
      else if (tabber)  hNow = tabber.getBoundingClientRect().height;&lt;br /&gt;
&lt;br /&gt;
      if (Math.abs(hNow - lastH) &amp;lt; 1 &amp;amp;&amp;amp; Math.abs(window.scrollY - y) &amp;lt;= 1) {&lt;br /&gt;
        stable++;&lt;br /&gt;
      } else {&lt;br /&gt;
        stable = 0;&lt;br /&gt;
        lastH = hNow;&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      /* 12 stabile Frames ≈ ~200ms bei 60fps */&lt;br /&gt;
      if (stable &amp;gt;= 12 || (performance.now() - t0) &amp;gt; MAX_MS) {&lt;br /&gt;
        release();&lt;br /&gt;
        return;&lt;br /&gt;
      }&lt;br /&gt;
      raf = requestAnimationFrame(tick);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /* sofort starten */&lt;br /&gt;
    raf = requestAnimationFrame(tick);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function scrollToTab(hashOrId) {&lt;br /&gt;
    var name = (hashOrId || &#039;&#039;).replace(/^#/, &#039;&#039;);&lt;br /&gt;
    if (!/^tabber-/.test(name)) return;&lt;br /&gt;
&lt;br /&gt;
    var panel  = document.getElementById(name);&lt;br /&gt;
    if (!panel) return;&lt;br /&gt;
&lt;br /&gt;
    var header = headerForPanel(panel) || panel;&lt;br /&gt;
    pinScrollToTarget(header);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* 1) Direkt mit Hash geladen */&lt;br /&gt;
  if (/^#tabber-/.test(location.hash)) {&lt;br /&gt;
    setTimeout(function(){ scrollToTab(location.hash); }, 60);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* 2) Hashwechsel */&lt;br /&gt;
  window.addEventListener(&#039;hashchange&#039;, function () {&lt;br /&gt;
    if (/^#tabber-/.test(location.hash)) scrollToTab(location.hash);&lt;br /&gt;
  });&lt;br /&gt;
&lt;br /&gt;
  /* 3) Klicks (TabberNeue nutzt History API, verhindert default) */&lt;br /&gt;
  document.addEventListener(&#039;click&#039;, function (e) {&lt;br /&gt;
    var a = e.target &amp;amp;&amp;amp; e.target.closest(&#039;a[href^=&amp;quot;#tabber-&amp;quot;], button[aria-controls^=&amp;quot;tabber-&amp;quot;]&#039;);&lt;br /&gt;
    if (!a) return;&lt;br /&gt;
    var id = a.getAttribute(&#039;href&#039;) || (&#039;#&#039; + a.getAttribute(&#039;aria-controls&#039;));&lt;br /&gt;
    setTimeout(function(){ scrollToTab(id); }, 60);&lt;br /&gt;
  }, true);&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Webmaster</name></author>
	</entry>
	<entry>
		<id>https://firestone-idle-rpg.de/index.php?title=MediaWiki:Common.js&amp;diff=3779</id>
		<title>MediaWiki:Common.js</title>
		<link rel="alternate" type="text/html" href="https://firestone-idle-rpg.de/index.php?title=MediaWiki:Common.js&amp;diff=3779"/>
		<updated>2025-10-30T07:56:30Z</updated>

		<summary type="html">&lt;p&gt;Webmaster: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* Das folgende JavaScript wird für alle Benutzer geladen. */&lt;br /&gt;
/* ==== Sidebar: klickbare Kopfzeile + Gruppen mit Auf/Zu + Persistenz (Vector-2022 &amp;amp; Drawer) ==== */&lt;br /&gt;
(function () {&lt;br /&gt;
  &#039;use strict&#039;;&lt;br /&gt;
  if ( mw.config.get(&#039;skin&#039;) !== &#039;vector-2022&#039; ) return;&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Persistenz ---------- */&lt;br /&gt;
  var STORAGE_KEY = &#039;kr-sb-state:v3:&#039; + ( mw.config.get(&#039;wgUserName&#039;) || &#039;anon&#039; );&lt;br /&gt;
  function loadState() {&lt;br /&gt;
    try { return JSON.parse( localStorage.getItem(STORAGE_KEY) || &#039;{}&#039; ); }&lt;br /&gt;
    catch (e) { return {}; }&lt;br /&gt;
  }&lt;br /&gt;
  function saveState(state) {&lt;br /&gt;
    try { localStorage.setItem( STORAGE_KEY, JSON.stringify(state) ); }&lt;br /&gt;
    catch (e) {}&lt;br /&gt;
  }&lt;br /&gt;
  var STATE = loadState();&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Utils ---------- */&lt;br /&gt;
  function extractDirective(a) {&lt;br /&gt;
    if (!a) return null;&lt;br /&gt;
    var href = a.getAttribute(&#039;href&#039;) || &#039;&#039;;&lt;br /&gt;
    try { href = new URL(href, location.href).hash || &#039;&#039;; } catch (e) {}&lt;br /&gt;
    var s = (href || &#039;&#039;).trim().toLowerCase();&lt;br /&gt;
    if (s.includes(&#039;#group:&#039;)) return { kind: &#039;group&#039;, value: decodeURIComponent(s.split(&#039;#group:&#039;).pop()) };&lt;br /&gt;
    if (s.includes(&#039;#link:&#039;))  return { kind: &#039;link&#039;,  value: decodeURIComponent(s.split(&#039;#link:&#039;).pop())  };&lt;br /&gt;
    return null;&lt;br /&gt;
  }&lt;br /&gt;
  function stripDirectiveHash(a) {&lt;br /&gt;
    if (!a) return;&lt;br /&gt;
    a.setAttribute(&#039;href&#039;, (a.getAttribute(&#039;href&#039;) || &#039;&#039;).replace(/#(?:link|group):[^#]*$/i, &#039;&#039;) );&lt;br /&gt;
  }&lt;br /&gt;
  function hasRealHref(a) {&lt;br /&gt;
    if (!a) return false;&lt;br /&gt;
    return !/^#/.test( a.getAttribute(&#039;href&#039;) || &#039;&#039; );&lt;br /&gt;
  }&lt;br /&gt;
  function normId(s) {&lt;br /&gt;
    return String(s || &#039;&#039;).trim().toLowerCase()&lt;br /&gt;
      .replace(/\s+/g, &#039;-&#039;).replace(/[^\w\-:.|]/g, &#039;&#039;);&lt;br /&gt;
  }&lt;br /&gt;
  function toggleSet(li, arrow, expanded) {&lt;br /&gt;
    if (expanded) { li.classList.remove(&#039;is-collapsed&#039;); arrow.setAttribute(&#039;aria-expanded&#039;,&#039;true&#039;); }&lt;br /&gt;
    else          { li.classList.add(&#039;is-collapsed&#039;);    arrow.setAttribute(&#039;aria-expanded&#039;,&#039;false&#039;); }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Kopf + ID ---------- */&lt;br /&gt;
  function makeHead(li, keepLink, containerUL, labelOverride) {&lt;br /&gt;
    var a = li.querySelector(&#039;:scope &amp;gt; a&#039;);&lt;br /&gt;
&lt;br /&gt;
    var head = document.createElement(&#039;div&#039;);&lt;br /&gt;
    head.className = &#039;kr-head has-toggle&#039;; // ganze Kopfzeile darf toggeln (außer Link)&lt;br /&gt;
&lt;br /&gt;
    var btn = document.createElement(&#039;button&#039;);&lt;br /&gt;
    btn.type = &#039;button&#039;;&lt;br /&gt;
    btn.className = &#039;kr-arrow&#039;;&lt;br /&gt;
    btn.setAttribute(&#039;aria-expanded&#039;, &#039;false&#039;);&lt;br /&gt;
    head.appendChild(btn);&lt;br /&gt;
&lt;br /&gt;
    if (keepLink &amp;amp;&amp;amp; a) { stripDirectiveHash(a); head.appendChild(a); }&lt;br /&gt;
    else {&lt;br /&gt;
      var span = document.createElement(&#039;span&#039;);&lt;br /&gt;
      span.className = &#039;kr-title&#039;;&lt;br /&gt;
      span.textContent = labelOverride || (a &amp;amp;&amp;amp; a.textContent) || &#039;&#039;;&lt;br /&gt;
      head.appendChild(span);&lt;br /&gt;
      if (a) a.remove();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    li.insertBefore(head, li.firstChild);&lt;br /&gt;
&lt;br /&gt;
    // stabile ID für Persistenz&lt;br /&gt;
    var portlet = containerUL.closest(&#039;.vector-menu&#039;);&lt;br /&gt;
    var pid = (portlet &amp;amp;&amp;amp; (portlet.id || portlet.getAttribute(&#039;id&#039;))) || &#039;portlet&#039;;&lt;br /&gt;
    var labelText = (labelOverride || (head.querySelector(&#039;a, .kr-title&#039;) || {}).textContent || &#039;&#039;).trim();&lt;br /&gt;
    li.dataset.krId = normId( pid + &#039;::&#039; + labelText );&lt;br /&gt;
&lt;br /&gt;
    // Default zu; gespeicherten Zustand anwenden&lt;br /&gt;
    toggleSet(li, btn, false);&lt;br /&gt;
    var st = STATE[ li.dataset.krId ];&lt;br /&gt;
    if (st === 1) toggleSet(li, btn, true);&lt;br /&gt;
&lt;br /&gt;
    return head;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Toggle-Handler ---------- */&lt;br /&gt;
  function handleArrowClick(btn) {&lt;br /&gt;
    var li = btn.closest(&#039;li.kr-group, li.kr-top&#039;);&lt;br /&gt;
    if (!li) return;&lt;br /&gt;
&lt;br /&gt;
    var willExpand = li.classList.contains(&#039;is-collapsed&#039;);&lt;br /&gt;
    toggleSet(li, btn, willExpand);&lt;br /&gt;
&lt;br /&gt;
    var id = li.dataset.krId;&lt;br /&gt;
    if (id) {&lt;br /&gt;
      STATE[id] = willExpand ? 1 : 0;&lt;br /&gt;
      saveState(STATE);&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* Globale Delegation: Pfeil klickt immer; Kopfzeile klickt nur, wenn NICHT im Link */&lt;br /&gt;
  document.addEventListener(&#039;click&#039;, function (e) {&lt;br /&gt;
    var arrow = e.target &amp;amp;&amp;amp; e.target.closest &amp;amp;&amp;amp; e.target.closest(&#039;.kr-arrow&#039;);&lt;br /&gt;
    if (arrow) {&lt;br /&gt;
      e.preventDefault();&lt;br /&gt;
      handleArrowClick(arrow);&lt;br /&gt;
      return;&lt;br /&gt;
    }&lt;br /&gt;
    var head = e.target &amp;amp;&amp;amp; e.target.closest &amp;amp;&amp;amp; e.target.closest(&#039;.kr-head&#039;);&lt;br /&gt;
    if (head &amp;amp;&amp;amp; !e.target.closest(&#039;.kr-head a&#039;)) {&lt;br /&gt;
      e.preventDefault(); // freie Fläche der Kopfzeile&lt;br /&gt;
      var btn = head.querySelector(&#039;.kr-arrow&#039;);&lt;br /&gt;
      if (btn) handleArrowClick(btn);&lt;br /&gt;
    }&lt;br /&gt;
  }, true); // capture=true, damit nichts dazwischenfunkt&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Aufbau der Gruppen ---------- */&lt;br /&gt;
  function buildGroups(ul) {&lt;br /&gt;
    var lis = Array.from( ul.querySelectorAll(&#039;:scope &amp;gt; li.mw-list-item&#039;) );&lt;br /&gt;
    var current = null;&lt;br /&gt;
&lt;br /&gt;
    lis.forEach(function (li) {&lt;br /&gt;
      var a = li.querySelector(&#039;:scope &amp;gt; a&#039;);&lt;br /&gt;
      var d = extractDirective(a);&lt;br /&gt;
&lt;br /&gt;
      if (d &amp;amp;&amp;amp; d.kind === &#039;group&#039;) {&lt;br /&gt;
        var clickable = hasRealHref(a);&lt;br /&gt;
        li.classList.add(&#039;kr-group&#039;);&lt;br /&gt;
        makeHead(li, clickable, ul, (a &amp;amp;&amp;amp; a.textContent) || d.value || &#039;&#039;);&lt;br /&gt;
        if (!clickable &amp;amp;&amp;amp; a) a.remove(); else if (clickable) stripDirectiveHash(a);&lt;br /&gt;
&lt;br /&gt;
        var sub = document.createElement(&#039;ul&#039;);&lt;br /&gt;
        sub.className = &#039;kr-sub&#039;;&lt;br /&gt;
        li.appendChild(sub);&lt;br /&gt;
        current = sub;&lt;br /&gt;
        return;&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      if (current &amp;amp;&amp;amp; !(d &amp;amp;&amp;amp; (d.kind === &#039;group&#039; || d.kind === &#039;link&#039;))) {&lt;br /&gt;
        current.appendChild(li);&lt;br /&gt;
      }&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Portlets verarbeiten ---------- */&lt;br /&gt;
  function buildPortlets(root) {&lt;br /&gt;
    var lists = root.querySelectorAll(&#039;.vector-menu .vector-menu-content-list&#039;);&lt;br /&gt;
    lists.forEach(function (ul) {&lt;br /&gt;
      if (ul.dataset.krSbDone) return;&lt;br /&gt;
      ul.dataset.krSbDone = &#039;1&#039;;&lt;br /&gt;
&lt;br /&gt;
      var items  = Array.from( ul.querySelectorAll(&#039;:scope &amp;gt; li.mw-list-item&#039;) );&lt;br /&gt;
      var topIdx = items.findIndex(function (li) {&lt;br /&gt;
        var a = li.querySelector(&#039;:scope &amp;gt; a&#039;);&lt;br /&gt;
        var d = extractDirective(a);&lt;br /&gt;
        return d &amp;amp;&amp;amp; d.kind === &#039;link&#039;;&lt;br /&gt;
      });&lt;br /&gt;
&lt;br /&gt;
      if (topIdx &amp;gt;= 0) {&lt;br /&gt;
        var top = items[topIdx];&lt;br /&gt;
        top.classList.add(&#039;kr-top&#039;);&lt;br /&gt;
        makeHead(top, true, ul);&lt;br /&gt;
        stripDirectiveHash( top.querySelector(&#039;:scope &amp;gt; a&#039;) );&lt;br /&gt;
&lt;br /&gt;
        var sub = document.createElement(&#039;ul&#039;);&lt;br /&gt;
        sub.className = &#039;kr-sub&#039;;&lt;br /&gt;
        top.appendChild(sub);&lt;br /&gt;
&lt;br /&gt;
        for (var i = topIdx + 1; i &amp;lt; items.length; i++) sub.appendChild(items[i]);&lt;br /&gt;
        buildGroups(sub);&lt;br /&gt;
      } else {&lt;br /&gt;
        buildGroups(ul);&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      // Direktiven aus hrefs entfernen; Vector-Portlet-Heading ausblenden&lt;br /&gt;
      ul.querySelectorAll(&#039;a[href*=&amp;quot;#link:&amp;quot;], a[href*=&amp;quot;#group:&amp;quot;]&#039;).forEach(stripDirectiveHash);&lt;br /&gt;
      var portlet = ul.closest(&#039;.vector-menu&#039;);&lt;br /&gt;
      var heading = portlet &amp;amp;&amp;amp; portlet.querySelector(&#039;:scope &amp;gt; .vector-menu-heading&#039;);&lt;br /&gt;
      if (heading) heading.remove();&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Bootstrapping ---------- */&lt;br /&gt;
  function processRoot(root) {&lt;br /&gt;
    if (!root || root.dataset.krSbRootDone) return;&lt;br /&gt;
    root.dataset.krSbRootDone = &#039;1&#039;;&lt;br /&gt;
    root.classList.add(&#039;kr-sb&#039;);&lt;br /&gt;
    buildPortlets(root);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function init() {&lt;br /&gt;
    document.querySelectorAll(&#039;#mw-panel, #vector-main-menu, #vector-main-menu-pinned, .vector-drawer&#039;)&lt;br /&gt;
      .forEach(processRoot);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  init();&lt;br /&gt;
  mw.hook(&#039;wikipage.content&#039;).add(init);&lt;br /&gt;
&lt;br /&gt;
  // Drawer/DOM-Änderungen beobachten&lt;br /&gt;
  var mo = new MutationObserver(function (muts) {&lt;br /&gt;
    muts.forEach(function (m) {&lt;br /&gt;
      m.addedNodes &amp;amp;&amp;amp; m.addedNodes.forEach(function (n) {&lt;br /&gt;
        if (!(n instanceof Element)) return;&lt;br /&gt;
        if (n.matches(&#039;#vector-main-menu, #vector-main-menu-pinned, .vector-drawer, #mw-panel&#039;)) processRoot(n);&lt;br /&gt;
        n.querySelectorAll &amp;amp;&amp;amp; n.querySelectorAll(&#039;#vector-main-menu, #vector-main-menu-pinned, .vector-drawer, #mw-panel&#039;)&lt;br /&gt;
          .forEach(processRoot);&lt;br /&gt;
      });&lt;br /&gt;
    });&lt;br /&gt;
  });&lt;br /&gt;
  mo.observe(document.body, { childList: true, subtree: true });&lt;br /&gt;
})();&lt;br /&gt;
&lt;br /&gt;
/* TabberNeue: Anker-Scroll unter Vector-Header + robustes „Pinning“ */&lt;br /&gt;
(function () {&lt;br /&gt;
  /* reale Sticky-Höhe (+0.5rem Luft) */&lt;br /&gt;
  function stickyOffsetPx() {&lt;br /&gt;
    var hdr = document.querySelector(&#039;.vector-sticky-header-container&#039;) &lt;br /&gt;
           || document.getElementById(&#039;vector-sticky-header&#039;);&lt;br /&gt;
    var h = 0;&lt;br /&gt;
    if (hdr) {&lt;br /&gt;
      var r = hdr.getBoundingClientRect();&lt;br /&gt;
      if (r.top &amp;lt;= 0 &amp;amp;&amp;amp; r.bottom &amp;gt; 0) h = Math.round(r.height);&lt;br /&gt;
    }&lt;br /&gt;
    if (!h) h = 50; // Fallback&lt;br /&gt;
    var air = Math.round(parseFloat(getComputedStyle(document.documentElement).fontSize) * 0.5);&lt;br /&gt;
    return h + air;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* oberste Tabber-Leiste finden */&lt;br /&gt;
  function headerForPanel(panel) {&lt;br /&gt;
    if (!panel) return null;&lt;br /&gt;
    var top = panel.closest(&#039;.tabber&#039;);&lt;br /&gt;
    for (var cur = top; cur &amp;amp;&amp;amp; cur.parentElement;) {&lt;br /&gt;
      var up = cur.parentElement.closest(&#039;.tabber&#039;);&lt;br /&gt;
      if (!up) break;&lt;br /&gt;
      top = up; cur = up;&lt;br /&gt;
    }&lt;br /&gt;
    return top ? top.querySelector(&#039;:scope &amp;gt; .tabber__header&#039;) : null;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* Panel/Tabber, die wir beobachten wollen */&lt;br /&gt;
  function getObserved(tabber) {&lt;br /&gt;
    var section = tabber &amp;amp;&amp;amp; tabber.querySelector(&#039;:scope &amp;gt; .tabber__section&#039;);&lt;br /&gt;
    return { tabber: tabber, section: section };&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* „Pinne“ den Viewport hart auf header/panel; löse erst, wenn stabil */&lt;br /&gt;
  function pinScrollToTarget(panelOrHeader) {&lt;br /&gt;
    var target = panelOrHeader;&lt;br /&gt;
    if (!target) return;&lt;br /&gt;
&lt;br /&gt;
    var tabber = target.closest(&#039;.tabber&#039;);&lt;br /&gt;
    var obs = getObserved(tabber);&lt;br /&gt;
&lt;br /&gt;
    /* Freeze: Höhe zum Start lesen und auf dem Container festhalten */&lt;br /&gt;
    if (tabber &amp;amp;&amp;amp; obs.section) {&lt;br /&gt;
      var h0 = Math.ceil(obs.section.getBoundingClientRect().height);&lt;br /&gt;
      tabber.style.setProperty(&#039;--tabber-freeze-h&#039;, h0 + &#039;px&#039;);&lt;br /&gt;
      tabber.classList.add(&#039;tabber-is-switching&#039;);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /* Smooth-Scrolling global aus, damit nichts „nachfedert“ */&lt;br /&gt;
    document.documentElement.classList.add(&#039;fs-scroll-now&#039;);&lt;br /&gt;
&lt;br /&gt;
    /* Fremde scrollIntoView()-Aufrufe währenddessen blocken */&lt;br /&gt;
    var origScrollIntoView = Element.prototype.scrollIntoView;&lt;br /&gt;
    Element.prototype.scrollIntoView = function(){ /* noop while pinned */ };&lt;br /&gt;
&lt;br /&gt;
    /* Beobachte Größenänderungen → Stability Counter zurücksetzen */&lt;br /&gt;
    var stable = 0, lastH = -1, raf = 0;&lt;br /&gt;
    var ro = null;&lt;br /&gt;
    if (obs.section &amp;amp;&amp;amp; window.ResizeObserver) {&lt;br /&gt;
      ro = new ResizeObserver(function(){ stable = 0; });&lt;br /&gt;
      try { ro.observe(obs.section); } catch(e){}&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    var MAX_MS = 3000;&lt;br /&gt;
    var t0 = performance.now();&lt;br /&gt;
&lt;br /&gt;
    function wantY() {&lt;br /&gt;
      return target.getBoundingClientRect().top + window.scrollY - stickyOffsetPx();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function release() {&lt;br /&gt;
      if (ro) ro.disconnect();&lt;br /&gt;
      Element.prototype.scrollIntoView = origScrollIntoView;&lt;br /&gt;
      document.documentElement.classList.remove(&#039;fs-scroll-now&#039;);&lt;br /&gt;
      if (tabber) {&lt;br /&gt;
        tabber.classList.remove(&#039;tabber-is-switching&#039;);&lt;br /&gt;
        tabber.style.removeProperty(&#039;--tabber-freeze-h&#039;);&lt;br /&gt;
      }&lt;br /&gt;
      cancelAnimationFrame(raf);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function tick() {&lt;br /&gt;
      var y = wantY();&lt;br /&gt;
      if (Math.abs(window.scrollY - y) &amp;gt; 1) {&lt;br /&gt;
        /* Hart auf die Ziel-Position setzen */&lt;br /&gt;
        window.scrollTo(0, y);&lt;br /&gt;
        stable = 0; /* wir mussten korrigieren → noch nicht stabil */&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      /* aktuelles Maß nehmen (Section bevorzugt, sonst Tabber) */&lt;br /&gt;
      var hNow = 0;&lt;br /&gt;
      if (obs.section) hNow = obs.section.getBoundingClientRect().height;&lt;br /&gt;
      else if (tabber)  hNow = tabber.getBoundingClientRect().height;&lt;br /&gt;
&lt;br /&gt;
      if (Math.abs(hNow - lastH) &amp;lt; 1 &amp;amp;&amp;amp; Math.abs(window.scrollY - y) &amp;lt;= 1) {&lt;br /&gt;
        stable++;&lt;br /&gt;
      } else {&lt;br /&gt;
        stable = 0;&lt;br /&gt;
        lastH = hNow;&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      /* 12 stabile Frames ≈ ~200ms bei 60fps */&lt;br /&gt;
      if (stable &amp;gt;= 12 || (performance.now() - t0) &amp;gt; MAX_MS) {&lt;br /&gt;
        release();&lt;br /&gt;
        return;&lt;br /&gt;
      }&lt;br /&gt;
      raf = requestAnimationFrame(tick);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /* sofort starten */&lt;br /&gt;
    raf = requestAnimationFrame(tick);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function scrollToTab(hashOrId) {&lt;br /&gt;
    var name = (hashOrId || &#039;&#039;).replace(/^#/, &#039;&#039;);&lt;br /&gt;
    if (!/^tabber-/.test(name)) return;&lt;br /&gt;
&lt;br /&gt;
    var panel  = document.getElementById(name);&lt;br /&gt;
    if (!panel) return;&lt;br /&gt;
&lt;br /&gt;
    var header = headerForPanel(panel) || panel;&lt;br /&gt;
    pinScrollToTarget(header);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* 1) Direkt mit Hash geladen */&lt;br /&gt;
  if (/^#tabber-/.test(location.hash)) {&lt;br /&gt;
    setTimeout(function(){ scrollToTab(location.hash); }, 60);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* 2) Hashwechsel */&lt;br /&gt;
  window.addEventListener(&#039;hashchange&#039;, function () {&lt;br /&gt;
    if (/^#tabber-/.test(location.hash)) scrollToTab(location.hash);&lt;br /&gt;
  });&lt;br /&gt;
&lt;br /&gt;
  /* 3) Klicks (TabberNeue nutzt History API, verhindert default) */&lt;br /&gt;
  document.addEventListener(&#039;click&#039;, function (e) {&lt;br /&gt;
    var a = e.target &amp;amp;&amp;amp; e.target.closest(&#039;a[href^=&amp;quot;#tabber-&amp;quot;], button[aria-controls^=&amp;quot;tabber-&amp;quot;]&#039;);&lt;br /&gt;
    if (!a) return;&lt;br /&gt;
    var id = a.getAttribute(&#039;href&#039;) || (&#039;#&#039; + a.getAttribute(&#039;aria-controls&#039;));&lt;br /&gt;
    setTimeout(function(){ scrollToTab(id); }, 60);&lt;br /&gt;
  }, true);&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Webmaster</name></author>
	</entry>
	<entry>
		<id>https://firestone-idle-rpg.de/index.php?title=MediaWiki:Common.js&amp;diff=3778</id>
		<title>MediaWiki:Common.js</title>
		<link rel="alternate" type="text/html" href="https://firestone-idle-rpg.de/index.php?title=MediaWiki:Common.js&amp;diff=3778"/>
		<updated>2025-10-30T07:50:38Z</updated>

		<summary type="html">&lt;p&gt;Webmaster: Änderung 3777 von Webmaster (Diskussion) rückgängig gemacht.&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* Das folgende JavaScript wird für alle Benutzer geladen. */&lt;br /&gt;
/* ==== Sidebar: klickbare Kopfzeile + Gruppen mit Auf/Zu + Persistenz (Vector-2022 &amp;amp; Drawer) ==== */&lt;br /&gt;
(function () {&lt;br /&gt;
  &#039;use strict&#039;;&lt;br /&gt;
  if ( mw.config.get(&#039;skin&#039;) !== &#039;vector-2022&#039; ) return;&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Persistenz ---------- */&lt;br /&gt;
  var STORAGE_KEY = &#039;kr-sb-state:v3:&#039; + ( mw.config.get(&#039;wgUserName&#039;) || &#039;anon&#039; );&lt;br /&gt;
  function loadState() {&lt;br /&gt;
    try { return JSON.parse( localStorage.getItem(STORAGE_KEY) || &#039;{}&#039; ); }&lt;br /&gt;
    catch (e) { return {}; }&lt;br /&gt;
  }&lt;br /&gt;
  function saveState(state) {&lt;br /&gt;
    try { localStorage.setItem( STORAGE_KEY, JSON.stringify(state) ); }&lt;br /&gt;
    catch (e) {}&lt;br /&gt;
  }&lt;br /&gt;
  var STATE = loadState();&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Utils ---------- */&lt;br /&gt;
  function extractDirective(a) {&lt;br /&gt;
    if (!a) return null;&lt;br /&gt;
    var href = a.getAttribute(&#039;href&#039;) || &#039;&#039;;&lt;br /&gt;
    try { href = new URL(href, location.href).hash || &#039;&#039;; } catch (e) {}&lt;br /&gt;
    var s = (href || &#039;&#039;).trim().toLowerCase();&lt;br /&gt;
    if (s.includes(&#039;#group:&#039;)) return { kind: &#039;group&#039;, value: decodeURIComponent(s.split(&#039;#group:&#039;).pop()) };&lt;br /&gt;
    if (s.includes(&#039;#link:&#039;))  return { kind: &#039;link&#039;,  value: decodeURIComponent(s.split(&#039;#link:&#039;).pop())  };&lt;br /&gt;
    return null;&lt;br /&gt;
  }&lt;br /&gt;
  function stripDirectiveHash(a) {&lt;br /&gt;
    if (!a) return;&lt;br /&gt;
    a.setAttribute(&#039;href&#039;, (a.getAttribute(&#039;href&#039;) || &#039;&#039;).replace(/#(?:link|group):[^#]*$/i, &#039;&#039;) );&lt;br /&gt;
  }&lt;br /&gt;
  function hasRealHref(a) {&lt;br /&gt;
    if (!a) return false;&lt;br /&gt;
    return !/^#/.test( a.getAttribute(&#039;href&#039;) || &#039;&#039; );&lt;br /&gt;
  }&lt;br /&gt;
  function normId(s) {&lt;br /&gt;
    return String(s || &#039;&#039;).trim().toLowerCase()&lt;br /&gt;
      .replace(/\s+/g, &#039;-&#039;).replace(/[^\w\-:.|]/g, &#039;&#039;);&lt;br /&gt;
  }&lt;br /&gt;
  function toggleSet(li, arrow, expanded) {&lt;br /&gt;
    if (expanded) { li.classList.remove(&#039;is-collapsed&#039;); arrow.setAttribute(&#039;aria-expanded&#039;,&#039;true&#039;); }&lt;br /&gt;
    else          { li.classList.add(&#039;is-collapsed&#039;);    arrow.setAttribute(&#039;aria-expanded&#039;,&#039;false&#039;); }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Kopf + ID ---------- */&lt;br /&gt;
  function makeHead(li, keepLink, containerUL, labelOverride) {&lt;br /&gt;
    var a = li.querySelector(&#039;:scope &amp;gt; a&#039;);&lt;br /&gt;
&lt;br /&gt;
    var head = document.createElement(&#039;div&#039;);&lt;br /&gt;
    head.className = &#039;kr-head has-toggle&#039;; // ganze Kopfzeile darf toggeln (außer Link)&lt;br /&gt;
&lt;br /&gt;
    var btn = document.createElement(&#039;button&#039;);&lt;br /&gt;
    btn.type = &#039;button&#039;;&lt;br /&gt;
    btn.className = &#039;kr-arrow&#039;;&lt;br /&gt;
    btn.setAttribute(&#039;aria-expanded&#039;, &#039;false&#039;);&lt;br /&gt;
    head.appendChild(btn);&lt;br /&gt;
&lt;br /&gt;
    if (keepLink &amp;amp;&amp;amp; a) { stripDirectiveHash(a); head.appendChild(a); }&lt;br /&gt;
    else {&lt;br /&gt;
      var span = document.createElement(&#039;span&#039;);&lt;br /&gt;
      span.className = &#039;kr-title&#039;;&lt;br /&gt;
      span.textContent = labelOverride || (a &amp;amp;&amp;amp; a.textContent) || &#039;&#039;;&lt;br /&gt;
      head.appendChild(span);&lt;br /&gt;
      if (a) a.remove();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    li.insertBefore(head, li.firstChild);&lt;br /&gt;
&lt;br /&gt;
    // stabile ID für Persistenz&lt;br /&gt;
    var portlet = containerUL.closest(&#039;.vector-menu&#039;);&lt;br /&gt;
    var pid = (portlet &amp;amp;&amp;amp; (portlet.id || portlet.getAttribute(&#039;id&#039;))) || &#039;portlet&#039;;&lt;br /&gt;
    var labelText = (labelOverride || (head.querySelector(&#039;a, .kr-title&#039;) || {}).textContent || &#039;&#039;).trim();&lt;br /&gt;
    li.dataset.krId = normId( pid + &#039;::&#039; + labelText );&lt;br /&gt;
&lt;br /&gt;
    // Default zu; gespeicherten Zustand anwenden&lt;br /&gt;
    toggleSet(li, btn, false);&lt;br /&gt;
    var st = STATE[ li.dataset.krId ];&lt;br /&gt;
    if (st === 1) toggleSet(li, btn, true);&lt;br /&gt;
&lt;br /&gt;
    return head;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Toggle-Handler ---------- */&lt;br /&gt;
  function handleArrowClick(btn) {&lt;br /&gt;
    var li = btn.closest(&#039;li.kr-group, li.kr-top&#039;);&lt;br /&gt;
    if (!li) return;&lt;br /&gt;
&lt;br /&gt;
    var willExpand = li.classList.contains(&#039;is-collapsed&#039;);&lt;br /&gt;
    toggleSet(li, btn, willExpand);&lt;br /&gt;
&lt;br /&gt;
    var id = li.dataset.krId;&lt;br /&gt;
    if (id) {&lt;br /&gt;
      STATE[id] = willExpand ? 1 : 0;&lt;br /&gt;
      saveState(STATE);&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* Globale Delegation: Pfeil klickt immer; Kopfzeile klickt nur, wenn NICHT im Link */&lt;br /&gt;
  document.addEventListener(&#039;click&#039;, function (e) {&lt;br /&gt;
    var arrow = e.target &amp;amp;&amp;amp; e.target.closest &amp;amp;&amp;amp; e.target.closest(&#039;.kr-arrow&#039;);&lt;br /&gt;
    if (arrow) {&lt;br /&gt;
      e.preventDefault();&lt;br /&gt;
      handleArrowClick(arrow);&lt;br /&gt;
      return;&lt;br /&gt;
    }&lt;br /&gt;
    var head = e.target &amp;amp;&amp;amp; e.target.closest &amp;amp;&amp;amp; e.target.closest(&#039;.kr-head&#039;);&lt;br /&gt;
    if (head &amp;amp;&amp;amp; !e.target.closest(&#039;.kr-head a&#039;)) {&lt;br /&gt;
      e.preventDefault(); // freie Fläche der Kopfzeile&lt;br /&gt;
      var btn = head.querySelector(&#039;.kr-arrow&#039;);&lt;br /&gt;
      if (btn) handleArrowClick(btn);&lt;br /&gt;
    }&lt;br /&gt;
  }, true); // capture=true, damit nichts dazwischenfunkt&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Aufbau der Gruppen ---------- */&lt;br /&gt;
  function buildGroups(ul) {&lt;br /&gt;
    var lis = Array.from( ul.querySelectorAll(&#039;:scope &amp;gt; li.mw-list-item&#039;) );&lt;br /&gt;
    var current = null;&lt;br /&gt;
&lt;br /&gt;
    lis.forEach(function (li) {&lt;br /&gt;
      var a = li.querySelector(&#039;:scope &amp;gt; a&#039;);&lt;br /&gt;
      var d = extractDirective(a);&lt;br /&gt;
&lt;br /&gt;
      if (d &amp;amp;&amp;amp; d.kind === &#039;group&#039;) {&lt;br /&gt;
        var clickable = hasRealHref(a);&lt;br /&gt;
        li.classList.add(&#039;kr-group&#039;);&lt;br /&gt;
        makeHead(li, clickable, ul, (a &amp;amp;&amp;amp; a.textContent) || d.value || &#039;&#039;);&lt;br /&gt;
        if (!clickable &amp;amp;&amp;amp; a) a.remove(); else if (clickable) stripDirectiveHash(a);&lt;br /&gt;
&lt;br /&gt;
        var sub = document.createElement(&#039;ul&#039;);&lt;br /&gt;
        sub.className = &#039;kr-sub&#039;;&lt;br /&gt;
        li.appendChild(sub);&lt;br /&gt;
        current = sub;&lt;br /&gt;
        return;&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      if (current &amp;amp;&amp;amp; !(d &amp;amp;&amp;amp; (d.kind === &#039;group&#039; || d.kind === &#039;link&#039;))) {&lt;br /&gt;
        current.appendChild(li);&lt;br /&gt;
      }&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Portlets verarbeiten ---------- */&lt;br /&gt;
  function buildPortlets(root) {&lt;br /&gt;
    var lists = root.querySelectorAll(&#039;.vector-menu .vector-menu-content-list&#039;);&lt;br /&gt;
    lists.forEach(function (ul) {&lt;br /&gt;
      if (ul.dataset.krSbDone) return;&lt;br /&gt;
      ul.dataset.krSbDone = &#039;1&#039;;&lt;br /&gt;
&lt;br /&gt;
      var items  = Array.from( ul.querySelectorAll(&#039;:scope &amp;gt; li.mw-list-item&#039;) );&lt;br /&gt;
      var topIdx = items.findIndex(function (li) {&lt;br /&gt;
        var a = li.querySelector(&#039;:scope &amp;gt; a&#039;);&lt;br /&gt;
        var d = extractDirective(a);&lt;br /&gt;
        return d &amp;amp;&amp;amp; d.kind === &#039;link&#039;;&lt;br /&gt;
      });&lt;br /&gt;
&lt;br /&gt;
      if (topIdx &amp;gt;= 0) {&lt;br /&gt;
        var top = items[topIdx];&lt;br /&gt;
        top.classList.add(&#039;kr-top&#039;);&lt;br /&gt;
        makeHead(top, true, ul);&lt;br /&gt;
        stripDirectiveHash( top.querySelector(&#039;:scope &amp;gt; a&#039;) );&lt;br /&gt;
&lt;br /&gt;
        var sub = document.createElement(&#039;ul&#039;);&lt;br /&gt;
        sub.className = &#039;kr-sub&#039;;&lt;br /&gt;
        top.appendChild(sub);&lt;br /&gt;
&lt;br /&gt;
        for (var i = topIdx + 1; i &amp;lt; items.length; i++) sub.appendChild(items[i]);&lt;br /&gt;
        buildGroups(sub);&lt;br /&gt;
      } else {&lt;br /&gt;
        buildGroups(ul);&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      // Direktiven aus hrefs entfernen; Vector-Portlet-Heading ausblenden&lt;br /&gt;
      ul.querySelectorAll(&#039;a[href*=&amp;quot;#link:&amp;quot;], a[href*=&amp;quot;#group:&amp;quot;]&#039;).forEach(stripDirectiveHash);&lt;br /&gt;
      var portlet = ul.closest(&#039;.vector-menu&#039;);&lt;br /&gt;
      var heading = portlet &amp;amp;&amp;amp; portlet.querySelector(&#039;:scope &amp;gt; .vector-menu-heading&#039;);&lt;br /&gt;
      if (heading) heading.remove();&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Bootstrapping ---------- */&lt;br /&gt;
  function processRoot(root) {&lt;br /&gt;
    if (!root || root.dataset.krSbRootDone) return;&lt;br /&gt;
    root.dataset.krSbRootDone = &#039;1&#039;;&lt;br /&gt;
    root.classList.add(&#039;kr-sb&#039;);&lt;br /&gt;
    buildPortlets(root);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function init() {&lt;br /&gt;
    document.querySelectorAll(&#039;#mw-panel, #vector-main-menu, #vector-main-menu-pinned, .vector-drawer&#039;)&lt;br /&gt;
      .forEach(processRoot);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  init();&lt;br /&gt;
  mw.hook(&#039;wikipage.content&#039;).add(init);&lt;br /&gt;
&lt;br /&gt;
  // Drawer/DOM-Änderungen beobachten&lt;br /&gt;
  var mo = new MutationObserver(function (muts) {&lt;br /&gt;
    muts.forEach(function (m) {&lt;br /&gt;
      m.addedNodes &amp;amp;&amp;amp; m.addedNodes.forEach(function (n) {&lt;br /&gt;
        if (!(n instanceof Element)) return;&lt;br /&gt;
        if (n.matches(&#039;#vector-main-menu, #vector-main-menu-pinned, .vector-drawer, #mw-panel&#039;)) processRoot(n);&lt;br /&gt;
        n.querySelectorAll &amp;amp;&amp;amp; n.querySelectorAll(&#039;#vector-main-menu, #vector-main-menu-pinned, .vector-drawer, #mw-panel&#039;)&lt;br /&gt;
          .forEach(processRoot);&lt;br /&gt;
      });&lt;br /&gt;
    });&lt;br /&gt;
  });&lt;br /&gt;
  mo.observe(document.body, { childList: true, subtree: true });&lt;br /&gt;
})();&lt;br /&gt;
&lt;br /&gt;
/* TabberNeue: Anker-Scroll unter Vector-Header + robustes „Pinning“ */&lt;br /&gt;
(function () {&lt;br /&gt;
  /* reale Sticky-Höhe (+0.5rem Luft) */&lt;br /&gt;
  function stickyOffsetPx() {&lt;br /&gt;
    var hdr = document.querySelector(&#039;.vector-sticky-header-container&#039;) &lt;br /&gt;
           || document.getElementById(&#039;vector-sticky-header&#039;);&lt;br /&gt;
    var h = 0;&lt;br /&gt;
    if (hdr) {&lt;br /&gt;
      var r = hdr.getBoundingClientRect();&lt;br /&gt;
      if (r.top &amp;lt;= 0 &amp;amp;&amp;amp; r.bottom &amp;gt; 0) h = Math.round(r.height);&lt;br /&gt;
    }&lt;br /&gt;
    if (!h) h = 56; // Fallback&lt;br /&gt;
    var air = Math.round(parseFloat(getComputedStyle(document.documentElement).fontSize) * 0.5);&lt;br /&gt;
    return h + air;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* oberste Tabber-Leiste finden */&lt;br /&gt;
  function headerForPanel(panel) {&lt;br /&gt;
    if (!panel) return null;&lt;br /&gt;
    var top = panel.closest(&#039;.tabber&#039;);&lt;br /&gt;
    for (var cur = top; cur &amp;amp;&amp;amp; cur.parentElement;) {&lt;br /&gt;
      var up = cur.parentElement.closest(&#039;.tabber&#039;);&lt;br /&gt;
      if (!up) break;&lt;br /&gt;
      top = up; cur = up;&lt;br /&gt;
    }&lt;br /&gt;
    return top ? top.querySelector(&#039;:scope &amp;gt; .tabber__header&#039;) : null;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* Panel/Tabber, die wir beobachten wollen */&lt;br /&gt;
  function getObserved(tabber) {&lt;br /&gt;
    var section = tabber &amp;amp;&amp;amp; tabber.querySelector(&#039;:scope &amp;gt; .tabber__section&#039;);&lt;br /&gt;
    return { tabber: tabber, section: section };&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* „Pinne“ den Viewport hart auf header/panel; löse erst, wenn stabil */&lt;br /&gt;
  function pinScrollToTarget(panelOrHeader) {&lt;br /&gt;
    var target = panelOrHeader;&lt;br /&gt;
    if (!target) return;&lt;br /&gt;
&lt;br /&gt;
    var tabber = target.closest(&#039;.tabber&#039;);&lt;br /&gt;
    var obs = getObserved(tabber);&lt;br /&gt;
&lt;br /&gt;
    /* Freeze: Höhe zum Start lesen und auf dem Container festhalten */&lt;br /&gt;
    if (tabber &amp;amp;&amp;amp; obs.section) {&lt;br /&gt;
      var h0 = Math.ceil(obs.section.getBoundingClientRect().height);&lt;br /&gt;
      tabber.style.setProperty(&#039;--tabber-freeze-h&#039;, h0 + &#039;px&#039;);&lt;br /&gt;
      tabber.classList.add(&#039;tabber-is-switching&#039;);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /* Smooth-Scrolling global aus, damit nichts „nachfedert“ */&lt;br /&gt;
    document.documentElement.classList.add(&#039;fs-scroll-now&#039;);&lt;br /&gt;
&lt;br /&gt;
    /* Fremde scrollIntoView()-Aufrufe währenddessen blocken */&lt;br /&gt;
    var origScrollIntoView = Element.prototype.scrollIntoView;&lt;br /&gt;
    Element.prototype.scrollIntoView = function(){ /* noop while pinned */ };&lt;br /&gt;
&lt;br /&gt;
    /* Beobachte Größenänderungen → Stability Counter zurücksetzen */&lt;br /&gt;
    var stable = 0, lastH = -1, raf = 0;&lt;br /&gt;
    var ro = null;&lt;br /&gt;
    if (obs.section &amp;amp;&amp;amp; window.ResizeObserver) {&lt;br /&gt;
      ro = new ResizeObserver(function(){ stable = 0; });&lt;br /&gt;
      try { ro.observe(obs.section); } catch(e){}&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    var MAX_MS = 3000;&lt;br /&gt;
    var t0 = performance.now();&lt;br /&gt;
&lt;br /&gt;
    function wantY() {&lt;br /&gt;
      return target.getBoundingClientRect().top + window.scrollY - stickyOffsetPx();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function release() {&lt;br /&gt;
      if (ro) ro.disconnect();&lt;br /&gt;
      Element.prototype.scrollIntoView = origScrollIntoView;&lt;br /&gt;
      document.documentElement.classList.remove(&#039;fs-scroll-now&#039;);&lt;br /&gt;
      if (tabber) {&lt;br /&gt;
        tabber.classList.remove(&#039;tabber-is-switching&#039;);&lt;br /&gt;
        tabber.style.removeProperty(&#039;--tabber-freeze-h&#039;);&lt;br /&gt;
      }&lt;br /&gt;
      cancelAnimationFrame(raf);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function tick() {&lt;br /&gt;
      var y = wantY();&lt;br /&gt;
      if (Math.abs(window.scrollY - y) &amp;gt; 1) {&lt;br /&gt;
        /* Hart auf die Ziel-Position setzen */&lt;br /&gt;
        window.scrollTo(0, y);&lt;br /&gt;
        stable = 0; /* wir mussten korrigieren → noch nicht stabil */&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      /* aktuelles Maß nehmen (Section bevorzugt, sonst Tabber) */&lt;br /&gt;
      var hNow = 0;&lt;br /&gt;
      if (obs.section) hNow = obs.section.getBoundingClientRect().height;&lt;br /&gt;
      else if (tabber)  hNow = tabber.getBoundingClientRect().height;&lt;br /&gt;
&lt;br /&gt;
      if (Math.abs(hNow - lastH) &amp;lt; 1 &amp;amp;&amp;amp; Math.abs(window.scrollY - y) &amp;lt;= 1) {&lt;br /&gt;
        stable++;&lt;br /&gt;
      } else {&lt;br /&gt;
        stable = 0;&lt;br /&gt;
        lastH = hNow;&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      /* 12 stabile Frames ≈ ~200ms bei 60fps */&lt;br /&gt;
      if (stable &amp;gt;= 12 || (performance.now() - t0) &amp;gt; MAX_MS) {&lt;br /&gt;
        release();&lt;br /&gt;
        return;&lt;br /&gt;
      }&lt;br /&gt;
      raf = requestAnimationFrame(tick);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /* sofort starten */&lt;br /&gt;
    raf = requestAnimationFrame(tick);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function scrollToTab(hashOrId) {&lt;br /&gt;
    var name = (hashOrId || &#039;&#039;).replace(/^#/, &#039;&#039;);&lt;br /&gt;
    if (!/^tabber-/.test(name)) return;&lt;br /&gt;
&lt;br /&gt;
    var panel  = document.getElementById(name);&lt;br /&gt;
    if (!panel) return;&lt;br /&gt;
&lt;br /&gt;
    var header = headerForPanel(panel) || panel;&lt;br /&gt;
    pinScrollToTarget(header);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* 1) Direkt mit Hash geladen */&lt;br /&gt;
  if (/^#tabber-/.test(location.hash)) {&lt;br /&gt;
    setTimeout(function(){ scrollToTab(location.hash); }, 60);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* 2) Hashwechsel */&lt;br /&gt;
  window.addEventListener(&#039;hashchange&#039;, function () {&lt;br /&gt;
    if (/^#tabber-/.test(location.hash)) scrollToTab(location.hash);&lt;br /&gt;
  });&lt;br /&gt;
&lt;br /&gt;
  /* 3) Klicks (TabberNeue nutzt History API, verhindert default) */&lt;br /&gt;
  document.addEventListener(&#039;click&#039;, function (e) {&lt;br /&gt;
    var a = e.target &amp;amp;&amp;amp; e.target.closest(&#039;a[href^=&amp;quot;#tabber-&amp;quot;], button[aria-controls^=&amp;quot;tabber-&amp;quot;]&#039;);&lt;br /&gt;
    if (!a) return;&lt;br /&gt;
    var id = a.getAttribute(&#039;href&#039;) || (&#039;#&#039; + a.getAttribute(&#039;aria-controls&#039;));&lt;br /&gt;
    setTimeout(function(){ scrollToTab(id); }, 60);&lt;br /&gt;
  }, true);&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Webmaster</name></author>
	</entry>
	<entry>
		<id>https://firestone-idle-rpg.de/index.php?title=MediaWiki:Common.js&amp;diff=3777</id>
		<title>MediaWiki:Common.js</title>
		<link rel="alternate" type="text/html" href="https://firestone-idle-rpg.de/index.php?title=MediaWiki:Common.js&amp;diff=3777"/>
		<updated>2025-10-30T03:37:53Z</updated>

		<summary type="html">&lt;p&gt;Webmaster: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* Das folgende JavaScript wird für alle Benutzer geladen. */&lt;br /&gt;
/* ==== Sidebar: klickbare Kopfzeile + Gruppen mit Auf/Zu + Persistenz (Vector-2022 &amp;amp; Drawer) ==== */&lt;br /&gt;
(function () {&lt;br /&gt;
  &#039;use strict&#039;;&lt;br /&gt;
  if ( mw.config.get(&#039;skin&#039;) !== &#039;vector-2022&#039; ) return;&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Persistenz ---------- */&lt;br /&gt;
  var STORAGE_KEY = &#039;kr-sb-state:v3:&#039; + ( mw.config.get(&#039;wgUserName&#039;) || &#039;anon&#039; );&lt;br /&gt;
  function loadState() {&lt;br /&gt;
    try { return JSON.parse( localStorage.getItem(STORAGE_KEY) || &#039;{}&#039; ); }&lt;br /&gt;
    catch (e) { return {}; }&lt;br /&gt;
  }&lt;br /&gt;
  function saveState(state) {&lt;br /&gt;
    try { localStorage.setItem( STORAGE_KEY, JSON.stringify(state) ); }&lt;br /&gt;
    catch (e) {}&lt;br /&gt;
  }&lt;br /&gt;
  var STATE = loadState();&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Utils ---------- */&lt;br /&gt;
  function extractDirective(a) {&lt;br /&gt;
    if (!a) return null;&lt;br /&gt;
    var href = a.getAttribute(&#039;href&#039;) || &#039;&#039;;&lt;br /&gt;
    try { href = new URL(href, location.href).hash || &#039;&#039;; } catch (e) {}&lt;br /&gt;
    var s = (href || &#039;&#039;).trim().toLowerCase();&lt;br /&gt;
    if (s.includes(&#039;#group:&#039;)) return { kind: &#039;group&#039;, value: decodeURIComponent(s.split(&#039;#group:&#039;).pop()) };&lt;br /&gt;
    if (s.includes(&#039;#link:&#039;))  return { kind: &#039;link&#039;,  value: decodeURIComponent(s.split(&#039;#link:&#039;).pop())  };&lt;br /&gt;
    return null;&lt;br /&gt;
  }&lt;br /&gt;
  function stripDirectiveHash(a) {&lt;br /&gt;
    if (!a) return;&lt;br /&gt;
    a.setAttribute(&#039;href&#039;, (a.getAttribute(&#039;href&#039;) || &#039;&#039;).replace(/#(?:link|group):[^#]*$/i, &#039;&#039;) );&lt;br /&gt;
  }&lt;br /&gt;
  function hasRealHref(a) {&lt;br /&gt;
    if (!a) return false;&lt;br /&gt;
    return !/^#/.test( a.getAttribute(&#039;href&#039;) || &#039;&#039; );&lt;br /&gt;
  }&lt;br /&gt;
  function normId(s) {&lt;br /&gt;
    return String(s || &#039;&#039;).trim().toLowerCase()&lt;br /&gt;
      .replace(/\s+/g, &#039;-&#039;).replace(/[^\w\-:.|]/g, &#039;&#039;);&lt;br /&gt;
  }&lt;br /&gt;
  function toggleSet(li, arrow, expanded) {&lt;br /&gt;
    if (expanded) { li.classList.remove(&#039;is-collapsed&#039;); arrow.setAttribute(&#039;aria-expanded&#039;,&#039;true&#039;); }&lt;br /&gt;
    else          { li.classList.add(&#039;is-collapsed&#039;);    arrow.setAttribute(&#039;aria-expanded&#039;,&#039;false&#039;); }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Kopf + ID ---------- */&lt;br /&gt;
  function makeHead(li, keepLink, containerUL, labelOverride) {&lt;br /&gt;
    var a = li.querySelector(&#039;:scope &amp;gt; a&#039;);&lt;br /&gt;
&lt;br /&gt;
    var head = document.createElement(&#039;div&#039;);&lt;br /&gt;
    head.className = &#039;kr-head has-toggle&#039;; // ganze Kopfzeile darf toggeln (außer Link)&lt;br /&gt;
&lt;br /&gt;
    var btn = document.createElement(&#039;button&#039;);&lt;br /&gt;
    btn.type = &#039;button&#039;;&lt;br /&gt;
    btn.className = &#039;kr-arrow&#039;;&lt;br /&gt;
    btn.setAttribute(&#039;aria-expanded&#039;, &#039;false&#039;);&lt;br /&gt;
    head.appendChild(btn);&lt;br /&gt;
&lt;br /&gt;
    if (keepLink &amp;amp;&amp;amp; a) { stripDirectiveHash(a); head.appendChild(a); }&lt;br /&gt;
    else {&lt;br /&gt;
      var span = document.createElement(&#039;span&#039;);&lt;br /&gt;
      span.className = &#039;kr-title&#039;;&lt;br /&gt;
      span.textContent = labelOverride || (a &amp;amp;&amp;amp; a.textContent) || &#039;&#039;;&lt;br /&gt;
      head.appendChild(span);&lt;br /&gt;
      if (a) a.remove();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    li.insertBefore(head, li.firstChild);&lt;br /&gt;
&lt;br /&gt;
    // stabile ID für Persistenz&lt;br /&gt;
    var portlet = containerUL.closest(&#039;.vector-menu&#039;);&lt;br /&gt;
    var pid = (portlet &amp;amp;&amp;amp; (portlet.id || portlet.getAttribute(&#039;id&#039;))) || &#039;portlet&#039;;&lt;br /&gt;
    var labelText = (labelOverride || (head.querySelector(&#039;a, .kr-title&#039;) || {}).textContent || &#039;&#039;).trim();&lt;br /&gt;
    li.dataset.krId = normId( pid + &#039;::&#039; + labelText );&lt;br /&gt;
&lt;br /&gt;
    // Default zu; gespeicherten Zustand anwenden&lt;br /&gt;
    toggleSet(li, btn, false);&lt;br /&gt;
    var st = STATE[ li.dataset.krId ];&lt;br /&gt;
    if (st === 1) toggleSet(li, btn, true);&lt;br /&gt;
&lt;br /&gt;
    return head;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Toggle-Handler ---------- */&lt;br /&gt;
  function handleArrowClick(btn) {&lt;br /&gt;
    var li = btn.closest(&#039;li.kr-group, li.kr-top&#039;);&lt;br /&gt;
    if (!li) return;&lt;br /&gt;
&lt;br /&gt;
    var willExpand = li.classList.contains(&#039;is-collapsed&#039;);&lt;br /&gt;
    toggleSet(li, btn, willExpand);&lt;br /&gt;
&lt;br /&gt;
    var id = li.dataset.krId;&lt;br /&gt;
    if (id) {&lt;br /&gt;
      STATE[id] = willExpand ? 1 : 0;&lt;br /&gt;
      saveState(STATE);&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* Globale Delegation: Pfeil klickt immer; Kopfzeile klickt nur, wenn NICHT im Link */&lt;br /&gt;
  document.addEventListener(&#039;click&#039;, function (e) {&lt;br /&gt;
    var arrow = e.target &amp;amp;&amp;amp; e.target.closest &amp;amp;&amp;amp; e.target.closest(&#039;.kr-arrow&#039;);&lt;br /&gt;
    if (arrow) {&lt;br /&gt;
      e.preventDefault();&lt;br /&gt;
      handleArrowClick(arrow);&lt;br /&gt;
      return;&lt;br /&gt;
    }&lt;br /&gt;
    var head = e.target &amp;amp;&amp;amp; e.target.closest &amp;amp;&amp;amp; e.target.closest(&#039;.kr-head&#039;);&lt;br /&gt;
    if (head &amp;amp;&amp;amp; !e.target.closest(&#039;.kr-head a&#039;)) {&lt;br /&gt;
      e.preventDefault(); // freie Fläche der Kopfzeile&lt;br /&gt;
      var btn = head.querySelector(&#039;.kr-arrow&#039;);&lt;br /&gt;
      if (btn) handleArrowClick(btn);&lt;br /&gt;
    }&lt;br /&gt;
  }, true); // capture=true, damit nichts dazwischenfunkt&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Aufbau der Gruppen ---------- */&lt;br /&gt;
  function buildGroups(ul) {&lt;br /&gt;
    var lis = Array.from( ul.querySelectorAll(&#039;:scope &amp;gt; li.mw-list-item&#039;) );&lt;br /&gt;
    var current = null;&lt;br /&gt;
&lt;br /&gt;
    lis.forEach(function (li) {&lt;br /&gt;
      var a = li.querySelector(&#039;:scope &amp;gt; a&#039;);&lt;br /&gt;
      var d = extractDirective(a);&lt;br /&gt;
&lt;br /&gt;
      if (d &amp;amp;&amp;amp; d.kind === &#039;group&#039;) {&lt;br /&gt;
        var clickable = hasRealHref(a);&lt;br /&gt;
        li.classList.add(&#039;kr-group&#039;);&lt;br /&gt;
        makeHead(li, clickable, ul, (a &amp;amp;&amp;amp; a.textContent) || d.value || &#039;&#039;);&lt;br /&gt;
        if (!clickable &amp;amp;&amp;amp; a) a.remove(); else if (clickable) stripDirectiveHash(a);&lt;br /&gt;
&lt;br /&gt;
        var sub = document.createElement(&#039;ul&#039;);&lt;br /&gt;
        sub.className = &#039;kr-sub&#039;;&lt;br /&gt;
        li.appendChild(sub);&lt;br /&gt;
        current = sub;&lt;br /&gt;
        return;&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      if (current &amp;amp;&amp;amp; !(d &amp;amp;&amp;amp; (d.kind === &#039;group&#039; || d.kind === &#039;link&#039;))) {&lt;br /&gt;
        current.appendChild(li);&lt;br /&gt;
      }&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Portlets verarbeiten ---------- */&lt;br /&gt;
  function buildPortlets(root) {&lt;br /&gt;
    var lists = root.querySelectorAll(&#039;.vector-menu .vector-menu-content-list&#039;);&lt;br /&gt;
    lists.forEach(function (ul) {&lt;br /&gt;
      if (ul.dataset.krSbDone) return;&lt;br /&gt;
      ul.dataset.krSbDone = &#039;1&#039;;&lt;br /&gt;
&lt;br /&gt;
      var items  = Array.from( ul.querySelectorAll(&#039;:scope &amp;gt; li.mw-list-item&#039;) );&lt;br /&gt;
      var topIdx = items.findIndex(function (li) {&lt;br /&gt;
        var a = li.querySelector(&#039;:scope &amp;gt; a&#039;);&lt;br /&gt;
        var d = extractDirective(a);&lt;br /&gt;
        return d &amp;amp;&amp;amp; d.kind === &#039;link&#039;;&lt;br /&gt;
      });&lt;br /&gt;
&lt;br /&gt;
      if (topIdx &amp;gt;= 0) {&lt;br /&gt;
        var top = items[topIdx];&lt;br /&gt;
        top.classList.add(&#039;kr-top&#039;);&lt;br /&gt;
        makeHead(top, true, ul);&lt;br /&gt;
        stripDirectiveHash( top.querySelector(&#039;:scope &amp;gt; a&#039;) );&lt;br /&gt;
&lt;br /&gt;
        var sub = document.createElement(&#039;ul&#039;);&lt;br /&gt;
        sub.className = &#039;kr-sub&#039;;&lt;br /&gt;
        top.appendChild(sub);&lt;br /&gt;
&lt;br /&gt;
        for (var i = topIdx + 1; i &amp;lt; items.length; i++) sub.appendChild(items[i]);&lt;br /&gt;
        buildGroups(sub);&lt;br /&gt;
      } else {&lt;br /&gt;
        buildGroups(ul);&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      // Direktiven aus hrefs entfernen; Vector-Portlet-Heading ausblenden&lt;br /&gt;
      ul.querySelectorAll(&#039;a[href*=&amp;quot;#link:&amp;quot;], a[href*=&amp;quot;#group:&amp;quot;]&#039;).forEach(stripDirectiveHash);&lt;br /&gt;
      var portlet = ul.closest(&#039;.vector-menu&#039;);&lt;br /&gt;
      var heading = portlet &amp;amp;&amp;amp; portlet.querySelector(&#039;:scope &amp;gt; .vector-menu-heading&#039;);&lt;br /&gt;
      if (heading) heading.remove();&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Bootstrapping ---------- */&lt;br /&gt;
  function processRoot(root) {&lt;br /&gt;
    if (!root || root.dataset.krSbRootDone) return;&lt;br /&gt;
    root.dataset.krSbRootDone = &#039;1&#039;;&lt;br /&gt;
    root.classList.add(&#039;kr-sb&#039;);&lt;br /&gt;
    buildPortlets(root);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function init() {&lt;br /&gt;
    document.querySelectorAll(&#039;#mw-panel, #vector-main-menu, #vector-main-menu-pinned, .vector-drawer&#039;)&lt;br /&gt;
      .forEach(processRoot);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  init();&lt;br /&gt;
  mw.hook(&#039;wikipage.content&#039;).add(init);&lt;br /&gt;
&lt;br /&gt;
  // Drawer/DOM-Änderungen beobachten&lt;br /&gt;
  var mo = new MutationObserver(function (muts) {&lt;br /&gt;
    muts.forEach(function (m) {&lt;br /&gt;
      m.addedNodes &amp;amp;&amp;amp; m.addedNodes.forEach(function (n) {&lt;br /&gt;
        if (!(n instanceof Element)) return;&lt;br /&gt;
        if (n.matches(&#039;#vector-main-menu, #vector-main-menu-pinned, .vector-drawer, #mw-panel&#039;)) processRoot(n);&lt;br /&gt;
        n.querySelectorAll &amp;amp;&amp;amp; n.querySelectorAll(&#039;#vector-main-menu, #vector-main-menu-pinned, .vector-drawer, #mw-panel&#039;)&lt;br /&gt;
          .forEach(processRoot);&lt;br /&gt;
      });&lt;br /&gt;
    });&lt;br /&gt;
  });&lt;br /&gt;
  mo.observe(document.body, { childList: true, subtree: true });&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Webmaster</name></author>
	</entry>
	<entry>
		<id>https://firestone-idle-rpg.de/index.php?title=MediaWiki:Common.js&amp;diff=3776</id>
		<title>MediaWiki:Common.js</title>
		<link rel="alternate" type="text/html" href="https://firestone-idle-rpg.de/index.php?title=MediaWiki:Common.js&amp;diff=3776"/>
		<updated>2025-10-30T03:11:32Z</updated>

		<summary type="html">&lt;p&gt;Webmaster: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* Das folgende JavaScript wird für alle Benutzer geladen. */&lt;br /&gt;
/* ==== Sidebar: klickbare Kopfzeile + Gruppen mit Auf/Zu + Persistenz (Vector-2022 &amp;amp; Drawer) ==== */&lt;br /&gt;
(function () {&lt;br /&gt;
  &#039;use strict&#039;;&lt;br /&gt;
  if ( mw.config.get(&#039;skin&#039;) !== &#039;vector-2022&#039; ) return;&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Persistenz ---------- */&lt;br /&gt;
  var STORAGE_KEY = &#039;kr-sb-state:v3:&#039; + ( mw.config.get(&#039;wgUserName&#039;) || &#039;anon&#039; );&lt;br /&gt;
  function loadState() {&lt;br /&gt;
    try { return JSON.parse( localStorage.getItem(STORAGE_KEY) || &#039;{}&#039; ); }&lt;br /&gt;
    catch (e) { return {}; }&lt;br /&gt;
  }&lt;br /&gt;
  function saveState(state) {&lt;br /&gt;
    try { localStorage.setItem( STORAGE_KEY, JSON.stringify(state) ); }&lt;br /&gt;
    catch (e) {}&lt;br /&gt;
  }&lt;br /&gt;
  var STATE = loadState();&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Utils ---------- */&lt;br /&gt;
  function extractDirective(a) {&lt;br /&gt;
    if (!a) return null;&lt;br /&gt;
    var href = a.getAttribute(&#039;href&#039;) || &#039;&#039;;&lt;br /&gt;
    try { href = new URL(href, location.href).hash || &#039;&#039;; } catch (e) {}&lt;br /&gt;
    var s = (href || &#039;&#039;).trim().toLowerCase();&lt;br /&gt;
    if (s.includes(&#039;#group:&#039;)) return { kind: &#039;group&#039;, value: decodeURIComponent(s.split(&#039;#group:&#039;).pop()) };&lt;br /&gt;
    if (s.includes(&#039;#link:&#039;))  return { kind: &#039;link&#039;,  value: decodeURIComponent(s.split(&#039;#link:&#039;).pop())  };&lt;br /&gt;
    return null;&lt;br /&gt;
  }&lt;br /&gt;
  function stripDirectiveHash(a) {&lt;br /&gt;
    if (!a) return;&lt;br /&gt;
    a.setAttribute(&#039;href&#039;, (a.getAttribute(&#039;href&#039;) || &#039;&#039;).replace(/#(?:link|group):[^#]*$/i, &#039;&#039;) );&lt;br /&gt;
  }&lt;br /&gt;
  function hasRealHref(a) {&lt;br /&gt;
    if (!a) return false;&lt;br /&gt;
    return !/^#/.test( a.getAttribute(&#039;href&#039;) || &#039;&#039; );&lt;br /&gt;
  }&lt;br /&gt;
  function normId(s) {&lt;br /&gt;
    return String(s || &#039;&#039;).trim().toLowerCase()&lt;br /&gt;
      .replace(/\s+/g, &#039;-&#039;).replace(/[^\w\-:.|]/g, &#039;&#039;);&lt;br /&gt;
  }&lt;br /&gt;
  function toggleSet(li, arrow, expanded) {&lt;br /&gt;
    if (expanded) { li.classList.remove(&#039;is-collapsed&#039;); arrow.setAttribute(&#039;aria-expanded&#039;,&#039;true&#039;); }&lt;br /&gt;
    else          { li.classList.add(&#039;is-collapsed&#039;);    arrow.setAttribute(&#039;aria-expanded&#039;,&#039;false&#039;); }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Kopf + ID ---------- */&lt;br /&gt;
  function makeHead(li, keepLink, containerUL, labelOverride) {&lt;br /&gt;
    var a = li.querySelector(&#039;:scope &amp;gt; a&#039;);&lt;br /&gt;
&lt;br /&gt;
    var head = document.createElement(&#039;div&#039;);&lt;br /&gt;
    head.className = &#039;kr-head has-toggle&#039;; // ganze Kopfzeile darf toggeln (außer Link)&lt;br /&gt;
&lt;br /&gt;
    var btn = document.createElement(&#039;button&#039;);&lt;br /&gt;
    btn.type = &#039;button&#039;;&lt;br /&gt;
    btn.className = &#039;kr-arrow&#039;;&lt;br /&gt;
    btn.setAttribute(&#039;aria-expanded&#039;, &#039;false&#039;);&lt;br /&gt;
    head.appendChild(btn);&lt;br /&gt;
&lt;br /&gt;
    if (keepLink &amp;amp;&amp;amp; a) { stripDirectiveHash(a); head.appendChild(a); }&lt;br /&gt;
    else {&lt;br /&gt;
      var span = document.createElement(&#039;span&#039;);&lt;br /&gt;
      span.className = &#039;kr-title&#039;;&lt;br /&gt;
      span.textContent = labelOverride || (a &amp;amp;&amp;amp; a.textContent) || &#039;&#039;;&lt;br /&gt;
      head.appendChild(span);&lt;br /&gt;
      if (a) a.remove();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    li.insertBefore(head, li.firstChild);&lt;br /&gt;
&lt;br /&gt;
    // stabile ID für Persistenz&lt;br /&gt;
    var portlet = containerUL.closest(&#039;.vector-menu&#039;);&lt;br /&gt;
    var pid = (portlet &amp;amp;&amp;amp; (portlet.id || portlet.getAttribute(&#039;id&#039;))) || &#039;portlet&#039;;&lt;br /&gt;
    var labelText = (labelOverride || (head.querySelector(&#039;a, .kr-title&#039;) || {}).textContent || &#039;&#039;).trim();&lt;br /&gt;
    li.dataset.krId = normId( pid + &#039;::&#039; + labelText );&lt;br /&gt;
&lt;br /&gt;
    // Default zu; gespeicherten Zustand anwenden&lt;br /&gt;
    toggleSet(li, btn, false);&lt;br /&gt;
    var st = STATE[ li.dataset.krId ];&lt;br /&gt;
    if (st === 1) toggleSet(li, btn, true);&lt;br /&gt;
&lt;br /&gt;
    return head;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Toggle-Handler ---------- */&lt;br /&gt;
  function handleArrowClick(btn) {&lt;br /&gt;
    var li = btn.closest(&#039;li.kr-group, li.kr-top&#039;);&lt;br /&gt;
    if (!li) return;&lt;br /&gt;
&lt;br /&gt;
    var willExpand = li.classList.contains(&#039;is-collapsed&#039;);&lt;br /&gt;
    toggleSet(li, btn, willExpand);&lt;br /&gt;
&lt;br /&gt;
    var id = li.dataset.krId;&lt;br /&gt;
    if (id) {&lt;br /&gt;
      STATE[id] = willExpand ? 1 : 0;&lt;br /&gt;
      saveState(STATE);&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* Globale Delegation: Pfeil klickt immer; Kopfzeile klickt nur, wenn NICHT im Link */&lt;br /&gt;
  document.addEventListener(&#039;click&#039;, function (e) {&lt;br /&gt;
    var arrow = e.target &amp;amp;&amp;amp; e.target.closest &amp;amp;&amp;amp; e.target.closest(&#039;.kr-arrow&#039;);&lt;br /&gt;
    if (arrow) {&lt;br /&gt;
      e.preventDefault();&lt;br /&gt;
      handleArrowClick(arrow);&lt;br /&gt;
      return;&lt;br /&gt;
    }&lt;br /&gt;
    var head = e.target &amp;amp;&amp;amp; e.target.closest &amp;amp;&amp;amp; e.target.closest(&#039;.kr-head&#039;);&lt;br /&gt;
    if (head &amp;amp;&amp;amp; !e.target.closest(&#039;.kr-head a&#039;)) {&lt;br /&gt;
      e.preventDefault(); // freie Fläche der Kopfzeile&lt;br /&gt;
      var btn = head.querySelector(&#039;.kr-arrow&#039;);&lt;br /&gt;
      if (btn) handleArrowClick(btn);&lt;br /&gt;
    }&lt;br /&gt;
  }, true); // capture=true, damit nichts dazwischenfunkt&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Aufbau der Gruppen ---------- */&lt;br /&gt;
  function buildGroups(ul) {&lt;br /&gt;
    var lis = Array.from( ul.querySelectorAll(&#039;:scope &amp;gt; li.mw-list-item&#039;) );&lt;br /&gt;
    var current = null;&lt;br /&gt;
&lt;br /&gt;
    lis.forEach(function (li) {&lt;br /&gt;
      var a = li.querySelector(&#039;:scope &amp;gt; a&#039;);&lt;br /&gt;
      var d = extractDirective(a);&lt;br /&gt;
&lt;br /&gt;
      if (d &amp;amp;&amp;amp; d.kind === &#039;group&#039;) {&lt;br /&gt;
        var clickable = hasRealHref(a);&lt;br /&gt;
        li.classList.add(&#039;kr-group&#039;);&lt;br /&gt;
        makeHead(li, clickable, ul, (a &amp;amp;&amp;amp; a.textContent) || d.value || &#039;&#039;);&lt;br /&gt;
        if (!clickable &amp;amp;&amp;amp; a) a.remove(); else if (clickable) stripDirectiveHash(a);&lt;br /&gt;
&lt;br /&gt;
        var sub = document.createElement(&#039;ul&#039;);&lt;br /&gt;
        sub.className = &#039;kr-sub&#039;;&lt;br /&gt;
        li.appendChild(sub);&lt;br /&gt;
        current = sub;&lt;br /&gt;
        return;&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      if (current &amp;amp;&amp;amp; !(d &amp;amp;&amp;amp; (d.kind === &#039;group&#039; || d.kind === &#039;link&#039;))) {&lt;br /&gt;
        current.appendChild(li);&lt;br /&gt;
      }&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Portlets verarbeiten ---------- */&lt;br /&gt;
  function buildPortlets(root) {&lt;br /&gt;
    var lists = root.querySelectorAll(&#039;.vector-menu .vector-menu-content-list&#039;);&lt;br /&gt;
    lists.forEach(function (ul) {&lt;br /&gt;
      if (ul.dataset.krSbDone) return;&lt;br /&gt;
      ul.dataset.krSbDone = &#039;1&#039;;&lt;br /&gt;
&lt;br /&gt;
      var items  = Array.from( ul.querySelectorAll(&#039;:scope &amp;gt; li.mw-list-item&#039;) );&lt;br /&gt;
      var topIdx = items.findIndex(function (li) {&lt;br /&gt;
        var a = li.querySelector(&#039;:scope &amp;gt; a&#039;);&lt;br /&gt;
        var d = extractDirective(a);&lt;br /&gt;
        return d &amp;amp;&amp;amp; d.kind === &#039;link&#039;;&lt;br /&gt;
      });&lt;br /&gt;
&lt;br /&gt;
      if (topIdx &amp;gt;= 0) {&lt;br /&gt;
        var top = items[topIdx];&lt;br /&gt;
        top.classList.add(&#039;kr-top&#039;);&lt;br /&gt;
        makeHead(top, true, ul);&lt;br /&gt;
        stripDirectiveHash( top.querySelector(&#039;:scope &amp;gt; a&#039;) );&lt;br /&gt;
&lt;br /&gt;
        var sub = document.createElement(&#039;ul&#039;);&lt;br /&gt;
        sub.className = &#039;kr-sub&#039;;&lt;br /&gt;
        top.appendChild(sub);&lt;br /&gt;
&lt;br /&gt;
        for (var i = topIdx + 1; i &amp;lt; items.length; i++) sub.appendChild(items[i]);&lt;br /&gt;
        buildGroups(sub);&lt;br /&gt;
      } else {&lt;br /&gt;
        buildGroups(ul);&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      // Direktiven aus hrefs entfernen; Vector-Portlet-Heading ausblenden&lt;br /&gt;
      ul.querySelectorAll(&#039;a[href*=&amp;quot;#link:&amp;quot;], a[href*=&amp;quot;#group:&amp;quot;]&#039;).forEach(stripDirectiveHash);&lt;br /&gt;
      var portlet = ul.closest(&#039;.vector-menu&#039;);&lt;br /&gt;
      var heading = portlet &amp;amp;&amp;amp; portlet.querySelector(&#039;:scope &amp;gt; .vector-menu-heading&#039;);&lt;br /&gt;
      if (heading) heading.remove();&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Bootstrapping ---------- */&lt;br /&gt;
  function processRoot(root) {&lt;br /&gt;
    if (!root || root.dataset.krSbRootDone) return;&lt;br /&gt;
    root.dataset.krSbRootDone = &#039;1&#039;;&lt;br /&gt;
    root.classList.add(&#039;kr-sb&#039;);&lt;br /&gt;
    buildPortlets(root);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function init() {&lt;br /&gt;
    document.querySelectorAll(&#039;#mw-panel, #vector-main-menu, #vector-main-menu-pinned, .vector-drawer&#039;)&lt;br /&gt;
      .forEach(processRoot);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  init();&lt;br /&gt;
  mw.hook(&#039;wikipage.content&#039;).add(init);&lt;br /&gt;
&lt;br /&gt;
  // Drawer/DOM-Änderungen beobachten&lt;br /&gt;
  var mo = new MutationObserver(function (muts) {&lt;br /&gt;
    muts.forEach(function (m) {&lt;br /&gt;
      m.addedNodes &amp;amp;&amp;amp; m.addedNodes.forEach(function (n) {&lt;br /&gt;
        if (!(n instanceof Element)) return;&lt;br /&gt;
        if (n.matches(&#039;#vector-main-menu, #vector-main-menu-pinned, .vector-drawer, #mw-panel&#039;)) processRoot(n);&lt;br /&gt;
        n.querySelectorAll &amp;amp;&amp;amp; n.querySelectorAll(&#039;#vector-main-menu, #vector-main-menu-pinned, .vector-drawer, #mw-panel&#039;)&lt;br /&gt;
          .forEach(processRoot);&lt;br /&gt;
      });&lt;br /&gt;
    });&lt;br /&gt;
  });&lt;br /&gt;
  mo.observe(document.body, { childList: true, subtree: true });&lt;br /&gt;
})();&lt;br /&gt;
&lt;br /&gt;
/* TabberNeue: Anker-Scroll unter Vector-Header + robustes „Pinning“ */&lt;br /&gt;
(function () {&lt;br /&gt;
  /* reale Sticky-Höhe (+0.5rem Luft) */&lt;br /&gt;
  function stickyOffsetPx() {&lt;br /&gt;
    var hdr = document.querySelector(&#039;.vector-sticky-header-container&#039;) &lt;br /&gt;
           || document.getElementById(&#039;vector-sticky-header&#039;);&lt;br /&gt;
    var h = 0;&lt;br /&gt;
    if (hdr) {&lt;br /&gt;
      var r = hdr.getBoundingClientRect();&lt;br /&gt;
      if (r.top &amp;lt;= 0 &amp;amp;&amp;amp; r.bottom &amp;gt; 0) h = Math.round(r.height);&lt;br /&gt;
    }&lt;br /&gt;
    if (!h) h = 56; // Fallback&lt;br /&gt;
    var air = Math.round(parseFloat(getComputedStyle(document.documentElement).fontSize) * 0.5);&lt;br /&gt;
    return h + air;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* oberste Tabber-Leiste finden */&lt;br /&gt;
  function headerForPanel(panel) {&lt;br /&gt;
    if (!panel) return null;&lt;br /&gt;
    var top = panel.closest(&#039;.tabber&#039;);&lt;br /&gt;
    for (var cur = top; cur &amp;amp;&amp;amp; cur.parentElement;) {&lt;br /&gt;
      var up = cur.parentElement.closest(&#039;.tabber&#039;);&lt;br /&gt;
      if (!up) break;&lt;br /&gt;
      top = up; cur = up;&lt;br /&gt;
    }&lt;br /&gt;
    return top ? top.querySelector(&#039;:scope &amp;gt; .tabber__header&#039;) : null;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* Panel/Tabber, die wir beobachten wollen */&lt;br /&gt;
  function getObserved(tabber) {&lt;br /&gt;
    var section = tabber &amp;amp;&amp;amp; tabber.querySelector(&#039;:scope &amp;gt; .tabber__section&#039;);&lt;br /&gt;
    return { tabber: tabber, section: section };&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* „Pinne“ den Viewport hart auf header/panel; löse erst, wenn stabil */&lt;br /&gt;
  function pinScrollToTarget(panelOrHeader) {&lt;br /&gt;
    var target = panelOrHeader;&lt;br /&gt;
    if (!target) return;&lt;br /&gt;
&lt;br /&gt;
    var tabber = target.closest(&#039;.tabber&#039;);&lt;br /&gt;
    var obs = getObserved(tabber);&lt;br /&gt;
&lt;br /&gt;
    /* Freeze: Höhe zum Start lesen und auf dem Container festhalten */&lt;br /&gt;
    if (tabber &amp;amp;&amp;amp; obs.section) {&lt;br /&gt;
      var h0 = Math.ceil(obs.section.getBoundingClientRect().height);&lt;br /&gt;
      tabber.style.setProperty(&#039;--tabber-freeze-h&#039;, h0 + &#039;px&#039;);&lt;br /&gt;
      tabber.classList.add(&#039;tabber-is-switching&#039;);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /* Smooth-Scrolling global aus, damit nichts „nachfedert“ */&lt;br /&gt;
    document.documentElement.classList.add(&#039;fs-scroll-now&#039;);&lt;br /&gt;
&lt;br /&gt;
    /* Fremde scrollIntoView()-Aufrufe währenddessen blocken */&lt;br /&gt;
    var origScrollIntoView = Element.prototype.scrollIntoView;&lt;br /&gt;
    Element.prototype.scrollIntoView = function(){ /* noop while pinned */ };&lt;br /&gt;
&lt;br /&gt;
    /* Beobachte Größenänderungen → Stability Counter zurücksetzen */&lt;br /&gt;
    var stable = 0, lastH = -1, raf = 0;&lt;br /&gt;
    var ro = null;&lt;br /&gt;
    if (obs.section &amp;amp;&amp;amp; window.ResizeObserver) {&lt;br /&gt;
      ro = new ResizeObserver(function(){ stable = 0; });&lt;br /&gt;
      try { ro.observe(obs.section); } catch(e){}&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    var MAX_MS = 3000;&lt;br /&gt;
    var t0 = performance.now();&lt;br /&gt;
&lt;br /&gt;
    function wantY() {&lt;br /&gt;
      return target.getBoundingClientRect().top + window.scrollY - stickyOffsetPx();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function release() {&lt;br /&gt;
      if (ro) ro.disconnect();&lt;br /&gt;
      Element.prototype.scrollIntoView = origScrollIntoView;&lt;br /&gt;
      document.documentElement.classList.remove(&#039;fs-scroll-now&#039;);&lt;br /&gt;
      if (tabber) {&lt;br /&gt;
        tabber.classList.remove(&#039;tabber-is-switching&#039;);&lt;br /&gt;
        tabber.style.removeProperty(&#039;--tabber-freeze-h&#039;);&lt;br /&gt;
      }&lt;br /&gt;
      cancelAnimationFrame(raf);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function tick() {&lt;br /&gt;
      var y = wantY();&lt;br /&gt;
      if (Math.abs(window.scrollY - y) &amp;gt; 1) {&lt;br /&gt;
        /* Hart auf die Ziel-Position setzen */&lt;br /&gt;
        window.scrollTo(0, y);&lt;br /&gt;
        stable = 0; /* wir mussten korrigieren → noch nicht stabil */&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      /* aktuelles Maß nehmen (Section bevorzugt, sonst Tabber) */&lt;br /&gt;
      var hNow = 0;&lt;br /&gt;
      if (obs.section) hNow = obs.section.getBoundingClientRect().height;&lt;br /&gt;
      else if (tabber)  hNow = tabber.getBoundingClientRect().height;&lt;br /&gt;
&lt;br /&gt;
      if (Math.abs(hNow - lastH) &amp;lt; 1 &amp;amp;&amp;amp; Math.abs(window.scrollY - y) &amp;lt;= 1) {&lt;br /&gt;
        stable++;&lt;br /&gt;
      } else {&lt;br /&gt;
        stable = 0;&lt;br /&gt;
        lastH = hNow;&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      /* 12 stabile Frames ≈ ~200ms bei 60fps */&lt;br /&gt;
      if (stable &amp;gt;= 12 || (performance.now() - t0) &amp;gt; MAX_MS) {&lt;br /&gt;
        release();&lt;br /&gt;
        return;&lt;br /&gt;
      }&lt;br /&gt;
      raf = requestAnimationFrame(tick);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /* sofort starten */&lt;br /&gt;
    raf = requestAnimationFrame(tick);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function scrollToTab(hashOrId) {&lt;br /&gt;
    var name = (hashOrId || &#039;&#039;).replace(/^#/, &#039;&#039;);&lt;br /&gt;
    if (!/^tabber-/.test(name)) return;&lt;br /&gt;
&lt;br /&gt;
    var panel  = document.getElementById(name);&lt;br /&gt;
    if (!panel) return;&lt;br /&gt;
&lt;br /&gt;
    var header = headerForPanel(panel) || panel;&lt;br /&gt;
    pinScrollToTarget(header);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* 1) Direkt mit Hash geladen */&lt;br /&gt;
  if (/^#tabber-/.test(location.hash)) {&lt;br /&gt;
    setTimeout(function(){ scrollToTab(location.hash); }, 60);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* 2) Hashwechsel */&lt;br /&gt;
  window.addEventListener(&#039;hashchange&#039;, function () {&lt;br /&gt;
    if (/^#tabber-/.test(location.hash)) scrollToTab(location.hash);&lt;br /&gt;
  });&lt;br /&gt;
&lt;br /&gt;
  /* 3) Klicks (TabberNeue nutzt History API, verhindert default) */&lt;br /&gt;
  document.addEventListener(&#039;click&#039;, function (e) {&lt;br /&gt;
    var a = e.target &amp;amp;&amp;amp; e.target.closest(&#039;a[href^=&amp;quot;#tabber-&amp;quot;], button[aria-controls^=&amp;quot;tabber-&amp;quot;]&#039;);&lt;br /&gt;
    if (!a) return;&lt;br /&gt;
    var id = a.getAttribute(&#039;href&#039;) || (&#039;#&#039; + a.getAttribute(&#039;aria-controls&#039;));&lt;br /&gt;
    setTimeout(function(){ scrollToTab(id); }, 60);&lt;br /&gt;
  }, true);&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Webmaster</name></author>
	</entry>
	<entry>
		<id>https://firestone-idle-rpg.de/index.php?title=MediaWiki:Common.js&amp;diff=3775</id>
		<title>MediaWiki:Common.js</title>
		<link rel="alternate" type="text/html" href="https://firestone-idle-rpg.de/index.php?title=MediaWiki:Common.js&amp;diff=3775"/>
		<updated>2025-10-30T02:42:21Z</updated>

		<summary type="html">&lt;p&gt;Webmaster: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* Das folgende JavaScript wird für alle Benutzer geladen. */&lt;br /&gt;
/* ==== Sidebar: klickbare Kopfzeile + Gruppen mit Auf/Zu + Persistenz (Vector-2022 &amp;amp; Drawer) ==== */&lt;br /&gt;
(function () {&lt;br /&gt;
  &#039;use strict&#039;;&lt;br /&gt;
  if ( mw.config.get(&#039;skin&#039;) !== &#039;vector-2022&#039; ) return;&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Persistenz ---------- */&lt;br /&gt;
  var STORAGE_KEY = &#039;kr-sb-state:v3:&#039; + ( mw.config.get(&#039;wgUserName&#039;) || &#039;anon&#039; );&lt;br /&gt;
  function loadState() {&lt;br /&gt;
    try { return JSON.parse( localStorage.getItem(STORAGE_KEY) || &#039;{}&#039; ); }&lt;br /&gt;
    catch (e) { return {}; }&lt;br /&gt;
  }&lt;br /&gt;
  function saveState(state) {&lt;br /&gt;
    try { localStorage.setItem( STORAGE_KEY, JSON.stringify(state) ); }&lt;br /&gt;
    catch (e) {}&lt;br /&gt;
  }&lt;br /&gt;
  var STATE = loadState();&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Utils ---------- */&lt;br /&gt;
  function extractDirective(a) {&lt;br /&gt;
    if (!a) return null;&lt;br /&gt;
    var href = a.getAttribute(&#039;href&#039;) || &#039;&#039;;&lt;br /&gt;
    try { href = new URL(href, location.href).hash || &#039;&#039;; } catch (e) {}&lt;br /&gt;
    var s = (href || &#039;&#039;).trim().toLowerCase();&lt;br /&gt;
    if (s.includes(&#039;#group:&#039;)) return { kind: &#039;group&#039;, value: decodeURIComponent(s.split(&#039;#group:&#039;).pop()) };&lt;br /&gt;
    if (s.includes(&#039;#link:&#039;))  return { kind: &#039;link&#039;,  value: decodeURIComponent(s.split(&#039;#link:&#039;).pop())  };&lt;br /&gt;
    return null;&lt;br /&gt;
  }&lt;br /&gt;
  function stripDirectiveHash(a) {&lt;br /&gt;
    if (!a) return;&lt;br /&gt;
    a.setAttribute(&#039;href&#039;, (a.getAttribute(&#039;href&#039;) || &#039;&#039;).replace(/#(?:link|group):[^#]*$/i, &#039;&#039;) );&lt;br /&gt;
  }&lt;br /&gt;
  function hasRealHref(a) {&lt;br /&gt;
    if (!a) return false;&lt;br /&gt;
    return !/^#/.test( a.getAttribute(&#039;href&#039;) || &#039;&#039; );&lt;br /&gt;
  }&lt;br /&gt;
  function normId(s) {&lt;br /&gt;
    return String(s || &#039;&#039;).trim().toLowerCase()&lt;br /&gt;
      .replace(/\s+/g, &#039;-&#039;).replace(/[^\w\-:.|]/g, &#039;&#039;);&lt;br /&gt;
  }&lt;br /&gt;
  function toggleSet(li, arrow, expanded) {&lt;br /&gt;
    if (expanded) { li.classList.remove(&#039;is-collapsed&#039;); arrow.setAttribute(&#039;aria-expanded&#039;,&#039;true&#039;); }&lt;br /&gt;
    else          { li.classList.add(&#039;is-collapsed&#039;);    arrow.setAttribute(&#039;aria-expanded&#039;,&#039;false&#039;); }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Kopf + ID ---------- */&lt;br /&gt;
  function makeHead(li, keepLink, containerUL, labelOverride) {&lt;br /&gt;
    var a = li.querySelector(&#039;:scope &amp;gt; a&#039;);&lt;br /&gt;
&lt;br /&gt;
    var head = document.createElement(&#039;div&#039;);&lt;br /&gt;
    head.className = &#039;kr-head has-toggle&#039;; // ganze Kopfzeile darf toggeln (außer Link)&lt;br /&gt;
&lt;br /&gt;
    var btn = document.createElement(&#039;button&#039;);&lt;br /&gt;
    btn.type = &#039;button&#039;;&lt;br /&gt;
    btn.className = &#039;kr-arrow&#039;;&lt;br /&gt;
    btn.setAttribute(&#039;aria-expanded&#039;, &#039;false&#039;);&lt;br /&gt;
    head.appendChild(btn);&lt;br /&gt;
&lt;br /&gt;
    if (keepLink &amp;amp;&amp;amp; a) { stripDirectiveHash(a); head.appendChild(a); }&lt;br /&gt;
    else {&lt;br /&gt;
      var span = document.createElement(&#039;span&#039;);&lt;br /&gt;
      span.className = &#039;kr-title&#039;;&lt;br /&gt;
      span.textContent = labelOverride || (a &amp;amp;&amp;amp; a.textContent) || &#039;&#039;;&lt;br /&gt;
      head.appendChild(span);&lt;br /&gt;
      if (a) a.remove();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    li.insertBefore(head, li.firstChild);&lt;br /&gt;
&lt;br /&gt;
    // stabile ID für Persistenz&lt;br /&gt;
    var portlet = containerUL.closest(&#039;.vector-menu&#039;);&lt;br /&gt;
    var pid = (portlet &amp;amp;&amp;amp; (portlet.id || portlet.getAttribute(&#039;id&#039;))) || &#039;portlet&#039;;&lt;br /&gt;
    var labelText = (labelOverride || (head.querySelector(&#039;a, .kr-title&#039;) || {}).textContent || &#039;&#039;).trim();&lt;br /&gt;
    li.dataset.krId = normId( pid + &#039;::&#039; + labelText );&lt;br /&gt;
&lt;br /&gt;
    // Default zu; gespeicherten Zustand anwenden&lt;br /&gt;
    toggleSet(li, btn, false);&lt;br /&gt;
    var st = STATE[ li.dataset.krId ];&lt;br /&gt;
    if (st === 1) toggleSet(li, btn, true);&lt;br /&gt;
&lt;br /&gt;
    return head;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Toggle-Handler ---------- */&lt;br /&gt;
  function handleArrowClick(btn) {&lt;br /&gt;
    var li = btn.closest(&#039;li.kr-group, li.kr-top&#039;);&lt;br /&gt;
    if (!li) return;&lt;br /&gt;
&lt;br /&gt;
    var willExpand = li.classList.contains(&#039;is-collapsed&#039;);&lt;br /&gt;
    toggleSet(li, btn, willExpand);&lt;br /&gt;
&lt;br /&gt;
    var id = li.dataset.krId;&lt;br /&gt;
    if (id) {&lt;br /&gt;
      STATE[id] = willExpand ? 1 : 0;&lt;br /&gt;
      saveState(STATE);&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* Globale Delegation: Pfeil klickt immer; Kopfzeile klickt nur, wenn NICHT im Link */&lt;br /&gt;
  document.addEventListener(&#039;click&#039;, function (e) {&lt;br /&gt;
    var arrow = e.target &amp;amp;&amp;amp; e.target.closest &amp;amp;&amp;amp; e.target.closest(&#039;.kr-arrow&#039;);&lt;br /&gt;
    if (arrow) {&lt;br /&gt;
      e.preventDefault();&lt;br /&gt;
      handleArrowClick(arrow);&lt;br /&gt;
      return;&lt;br /&gt;
    }&lt;br /&gt;
    var head = e.target &amp;amp;&amp;amp; e.target.closest &amp;amp;&amp;amp; e.target.closest(&#039;.kr-head&#039;);&lt;br /&gt;
    if (head &amp;amp;&amp;amp; !e.target.closest(&#039;.kr-head a&#039;)) {&lt;br /&gt;
      e.preventDefault(); // freie Fläche der Kopfzeile&lt;br /&gt;
      var btn = head.querySelector(&#039;.kr-arrow&#039;);&lt;br /&gt;
      if (btn) handleArrowClick(btn);&lt;br /&gt;
    }&lt;br /&gt;
  }, true); // capture=true, damit nichts dazwischenfunkt&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Aufbau der Gruppen ---------- */&lt;br /&gt;
  function buildGroups(ul) {&lt;br /&gt;
    var lis = Array.from( ul.querySelectorAll(&#039;:scope &amp;gt; li.mw-list-item&#039;) );&lt;br /&gt;
    var current = null;&lt;br /&gt;
&lt;br /&gt;
    lis.forEach(function (li) {&lt;br /&gt;
      var a = li.querySelector(&#039;:scope &amp;gt; a&#039;);&lt;br /&gt;
      var d = extractDirective(a);&lt;br /&gt;
&lt;br /&gt;
      if (d &amp;amp;&amp;amp; d.kind === &#039;group&#039;) {&lt;br /&gt;
        var clickable = hasRealHref(a);&lt;br /&gt;
        li.classList.add(&#039;kr-group&#039;);&lt;br /&gt;
        makeHead(li, clickable, ul, (a &amp;amp;&amp;amp; a.textContent) || d.value || &#039;&#039;);&lt;br /&gt;
        if (!clickable &amp;amp;&amp;amp; a) a.remove(); else if (clickable) stripDirectiveHash(a);&lt;br /&gt;
&lt;br /&gt;
        var sub = document.createElement(&#039;ul&#039;);&lt;br /&gt;
        sub.className = &#039;kr-sub&#039;;&lt;br /&gt;
        li.appendChild(sub);&lt;br /&gt;
        current = sub;&lt;br /&gt;
        return;&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      if (current &amp;amp;&amp;amp; !(d &amp;amp;&amp;amp; (d.kind === &#039;group&#039; || d.kind === &#039;link&#039;))) {&lt;br /&gt;
        current.appendChild(li);&lt;br /&gt;
      }&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Portlets verarbeiten ---------- */&lt;br /&gt;
  function buildPortlets(root) {&lt;br /&gt;
    var lists = root.querySelectorAll(&#039;.vector-menu .vector-menu-content-list&#039;);&lt;br /&gt;
    lists.forEach(function (ul) {&lt;br /&gt;
      if (ul.dataset.krSbDone) return;&lt;br /&gt;
      ul.dataset.krSbDone = &#039;1&#039;;&lt;br /&gt;
&lt;br /&gt;
      var items  = Array.from( ul.querySelectorAll(&#039;:scope &amp;gt; li.mw-list-item&#039;) );&lt;br /&gt;
      var topIdx = items.findIndex(function (li) {&lt;br /&gt;
        var a = li.querySelector(&#039;:scope &amp;gt; a&#039;);&lt;br /&gt;
        var d = extractDirective(a);&lt;br /&gt;
        return d &amp;amp;&amp;amp; d.kind === &#039;link&#039;;&lt;br /&gt;
      });&lt;br /&gt;
&lt;br /&gt;
      if (topIdx &amp;gt;= 0) {&lt;br /&gt;
        var top = items[topIdx];&lt;br /&gt;
        top.classList.add(&#039;kr-top&#039;);&lt;br /&gt;
        makeHead(top, true, ul);&lt;br /&gt;
        stripDirectiveHash( top.querySelector(&#039;:scope &amp;gt; a&#039;) );&lt;br /&gt;
&lt;br /&gt;
        var sub = document.createElement(&#039;ul&#039;);&lt;br /&gt;
        sub.className = &#039;kr-sub&#039;;&lt;br /&gt;
        top.appendChild(sub);&lt;br /&gt;
&lt;br /&gt;
        for (var i = topIdx + 1; i &amp;lt; items.length; i++) sub.appendChild(items[i]);&lt;br /&gt;
        buildGroups(sub);&lt;br /&gt;
      } else {&lt;br /&gt;
        buildGroups(ul);&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      // Direktiven aus hrefs entfernen; Vector-Portlet-Heading ausblenden&lt;br /&gt;
      ul.querySelectorAll(&#039;a[href*=&amp;quot;#link:&amp;quot;], a[href*=&amp;quot;#group:&amp;quot;]&#039;).forEach(stripDirectiveHash);&lt;br /&gt;
      var portlet = ul.closest(&#039;.vector-menu&#039;);&lt;br /&gt;
      var heading = portlet &amp;amp;&amp;amp; portlet.querySelector(&#039;:scope &amp;gt; .vector-menu-heading&#039;);&lt;br /&gt;
      if (heading) heading.remove();&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Bootstrapping ---------- */&lt;br /&gt;
  function processRoot(root) {&lt;br /&gt;
    if (!root || root.dataset.krSbRootDone) return;&lt;br /&gt;
    root.dataset.krSbRootDone = &#039;1&#039;;&lt;br /&gt;
    root.classList.add(&#039;kr-sb&#039;);&lt;br /&gt;
    buildPortlets(root);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function init() {&lt;br /&gt;
    document.querySelectorAll(&#039;#mw-panel, #vector-main-menu, #vector-main-menu-pinned, .vector-drawer&#039;)&lt;br /&gt;
      .forEach(processRoot);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  init();&lt;br /&gt;
  mw.hook(&#039;wikipage.content&#039;).add(init);&lt;br /&gt;
&lt;br /&gt;
  // Drawer/DOM-Änderungen beobachten&lt;br /&gt;
  var mo = new MutationObserver(function (muts) {&lt;br /&gt;
    muts.forEach(function (m) {&lt;br /&gt;
      m.addedNodes &amp;amp;&amp;amp; m.addedNodes.forEach(function (n) {&lt;br /&gt;
        if (!(n instanceof Element)) return;&lt;br /&gt;
        if (n.matches(&#039;#vector-main-menu, #vector-main-menu-pinned, .vector-drawer, #mw-panel&#039;)) processRoot(n);&lt;br /&gt;
        n.querySelectorAll &amp;amp;&amp;amp; n.querySelectorAll(&#039;#vector-main-menu, #vector-main-menu-pinned, .vector-drawer, #mw-panel&#039;)&lt;br /&gt;
          .forEach(processRoot);&lt;br /&gt;
      });&lt;br /&gt;
    });&lt;br /&gt;
  });&lt;br /&gt;
  mo.observe(document.body, { childList: true, subtree: true });&lt;br /&gt;
})();&lt;br /&gt;
&lt;br /&gt;
/* TabberNeue: Anker-Scroll unter Vector-Header + Freeze + Pinning */&lt;br /&gt;
(function () {&lt;br /&gt;
  &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
  /* --- Sticky-Offset (+0.5rem Luft) --- */&lt;br /&gt;
  function stickyOffsetPx() {&lt;br /&gt;
    var hdr = document.querySelector(&#039;.vector-sticky-header-container&#039;);&lt;br /&gt;
    var h = hdr ? Math.round(hdr.getBoundingClientRect().height) : 56;&lt;br /&gt;
    var air = Math.round(parseFloat(getComputedStyle(document.documentElement).fontSize) * 0.5);&lt;br /&gt;
    return h + air;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* --- oberste Tabber-Leiste zu einem Panel --- */&lt;br /&gt;
  function headerForPanel(panel) {&lt;br /&gt;
    if (!panel) return null;&lt;br /&gt;
    var top = panel.closest(&#039;.tabber&#039;);&lt;br /&gt;
    for (var cur = top; cur &amp;amp;&amp;amp; cur.parentElement; ) {&lt;br /&gt;
      var up = cur.parentElement.closest(&#039;.tabber&#039;);&lt;br /&gt;
      if (!up) break;&lt;br /&gt;
      top = up; cur = up;&lt;br /&gt;
    }&lt;br /&gt;
    return top ? top.querySelector(&#039;:scope &amp;gt; .tabber__header&#039;) : null;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* --- hartes Pinning: hält ScrollY auf gewünschtem Wert --- */&lt;br /&gt;
  function startPin(targetY) {&lt;br /&gt;
    var on = true;&lt;br /&gt;
    function lock() {&lt;br /&gt;
      if (!on) return;&lt;br /&gt;
      if (Math.abs(window.scrollY - targetY) &amp;gt; 1) window.scrollTo(0, targetY);&lt;br /&gt;
      requestAnimationFrame(lock);&lt;br /&gt;
    }&lt;br /&gt;
    document.documentElement.classList.add(&#039;fs-scroll-now&#039;);&lt;br /&gt;
    requestAnimationFrame(lock);&lt;br /&gt;
    return function stopPin(){ on = false; document.documentElement.classList.remove(&#039;fs-scroll-now&#039;); };&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* --- Freeze: min-height stabil + Scroll-Anchor neutralisieren --- */&lt;br /&gt;
  function freezeTabber(tabber) {&lt;br /&gt;
    var section = tabber &amp;amp;&amp;amp; tabber.querySelector(&#039;:scope &amp;gt; .tabber__section&#039;);&lt;br /&gt;
    if (!section) return function(){};&lt;br /&gt;
    var h = Math.max(0, Math.round(section.getBoundingClientRect().height));&lt;br /&gt;
    tabber.style.setProperty(&#039;--tabber-freeze-h&#039;, h + &#039;px&#039;);&lt;br /&gt;
    tabber.classList.add(&#039;tabber-is-switching&#039;);&lt;br /&gt;
    var oldOA = section.style.overflowAnchor;&lt;br /&gt;
    section.style.overflowAnchor = &#039;none&#039;;&lt;br /&gt;
    return function unfreeze(){&lt;br /&gt;
      tabber.classList.remove(&#039;tabber-is-switching&#039;);&lt;br /&gt;
      tabber.style.removeProperty(&#039;--tabber-freeze-h&#039;);&lt;br /&gt;
      section.style.overflowAnchor = oldOA || &#039;&#039;;&lt;br /&gt;
    };&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* --- Warte bis Panel „ruhig“ ist (keine Mutationen + Höhe stabil) --- */&lt;br /&gt;
  function waitStable(panel) {&lt;br /&gt;
    var section = panel &amp;amp;&amp;amp; panel.closest(&#039;.tabber&#039;)?.querySelector(&#039;:scope &amp;gt; .tabber__section&#039;);&lt;br /&gt;
    if (!section) return Promise.resolve();&lt;br /&gt;
&lt;br /&gt;
    var lastH = Math.round(section.getBoundingClientRect().height);&lt;br /&gt;
    var stableFrames = 0, lastMutation = performance.now();&lt;br /&gt;
&lt;br /&gt;
    var mo = new MutationObserver(function(){ lastMutation = performance.now(); });&lt;br /&gt;
    mo.observe(section, {subtree:true, childList:true, attributes:true, characterData:true});&lt;br /&gt;
&lt;br /&gt;
    return new Promise(function(resolve){&lt;br /&gt;
      (function loop(){&lt;br /&gt;
        var h = Math.round(section.getBoundingClientRect().height);&lt;br /&gt;
        stableFrames = (Math.abs(h - lastH) &amp;lt; 1) ? (stableFrames + 1) : 0;&lt;br /&gt;
        lastH = h;&lt;br /&gt;
        var quiet = (performance.now() - lastMutation) &amp;gt; 300;   // 300ms ohne Mutation&lt;br /&gt;
        if (stableFrames &amp;gt;= 12 &amp;amp;&amp;amp; quiet) {                      // ~200ms höhenstabil&lt;br /&gt;
          mo.disconnect(); resolve(); return;&lt;br /&gt;
        }&lt;br /&gt;
        requestAnimationFrame(loop);&lt;br /&gt;
      })();&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* --- Tabber internes scrollIntoView neutralisieren --- */&lt;br /&gt;
  var _siv = Element.prototype.scrollIntoView;&lt;br /&gt;
  Element.prototype.scrollIntoView = function(arg){&lt;br /&gt;
    try{ if (this.closest &amp;amp;&amp;amp; this.closest(&#039;.tabber&#039;)) return; }catch(e){}&lt;br /&gt;
    return _siv.call(this, arg);&lt;br /&gt;
  };&lt;br /&gt;
&lt;br /&gt;
  /* --- Zielposition berechnen + springen --- */&lt;br /&gt;
  function jumpTo(id, behavior) {&lt;br /&gt;
    var name = (id || &#039;&#039;).replace(/^#/, &#039;&#039;);&lt;br /&gt;
    if (!/^tabber-/.test(name)) return;&lt;br /&gt;
    var panel = document.getElementById(name); if (!panel) return;&lt;br /&gt;
    var head  = headerForPanel(panel) || panel;&lt;br /&gt;
    var y = head.getBoundingClientRect().top + window.scrollY - stickyOffsetPx();&lt;br /&gt;
    window.scrollTo({ top: y, behavior: behavior || &#039;auto&#039; });&lt;br /&gt;
    return { panel: panel, y: y };&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* --- Hauptlogik: Freeze + Pinning bis Stabilität, dann finaler Feinsprung --- */&lt;br /&gt;
  async function smartJump(id) {&lt;br /&gt;
    var j = jumpTo(id, &#039;auto&#039;); if (!j) return;&lt;br /&gt;
    var tabber = j.panel.closest(&#039;.tabber&#039;);&lt;br /&gt;
    var unfreeze = tabber ? freezeTabber(tabber) : function(){};&lt;br /&gt;
    var stop = startPin(j.y);&lt;br /&gt;
&lt;br /&gt;
    // solange DOM wackelt, gepinnt lassen&lt;br /&gt;
    await waitStable(j.panel);&lt;br /&gt;
&lt;br /&gt;
    // final: Pin lösen, Freeze lösen, feiner Smooth-Jump&lt;br /&gt;
    stop(); unfreeze();&lt;br /&gt;
    jumpTo(id, &#039;smooth&#039;);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* --- Hash beim Laden --- */&lt;br /&gt;
  if (/^#tabber-/.test(location.hash)) setTimeout(function(){ smartJump(location.hash); }, 0);&lt;br /&gt;
&lt;br /&gt;
  /* --- Hashwechsel --- */&lt;br /&gt;
  window.addEventListener(&#039;hashchange&#039;, function () {&lt;br /&gt;
    if (/^#tabber-/.test(location.hash)) smartJump(location.hash);&lt;br /&gt;
  });&lt;br /&gt;
&lt;br /&gt;
  /* --- Klicks auf Tabs (Capture, ohne preventDefault) --- */&lt;br /&gt;
  document.addEventListener(&#039;click&#039;, function (e) {&lt;br /&gt;
    var a = e.target &amp;amp;&amp;amp; e.target.closest &amp;amp;&amp;amp; e.target.closest(&#039;a[href^=&amp;quot;#tabber-&amp;quot;], button[aria-controls^=&amp;quot;tabber-&amp;quot;]&#039;);&lt;br /&gt;
    if (!a) return;&lt;br /&gt;
    var id = a.getAttribute(&#039;href&#039;) || (&#039;#&#039; + a.getAttribute(&#039;aria-controls&#039;));&lt;br /&gt;
    smartJump(id);&lt;br /&gt;
  }, true);&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Webmaster</name></author>
	</entry>
	<entry>
		<id>https://firestone-idle-rpg.de/index.php?title=MediaWiki:Common.js&amp;diff=3774</id>
		<title>MediaWiki:Common.js</title>
		<link rel="alternate" type="text/html" href="https://firestone-idle-rpg.de/index.php?title=MediaWiki:Common.js&amp;diff=3774"/>
		<updated>2025-10-30T02:23:42Z</updated>

		<summary type="html">&lt;p&gt;Webmaster: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* Das folgende JavaScript wird für alle Benutzer geladen. */&lt;br /&gt;
/* ==== Sidebar: klickbare Kopfzeile + Gruppen mit Auf/Zu + Persistenz (Vector-2022 &amp;amp; Drawer) ==== */&lt;br /&gt;
(function () {&lt;br /&gt;
  &#039;use strict&#039;;&lt;br /&gt;
  if ( mw.config.get(&#039;skin&#039;) !== &#039;vector-2022&#039; ) return;&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Persistenz ---------- */&lt;br /&gt;
  var STORAGE_KEY = &#039;kr-sb-state:v3:&#039; + ( mw.config.get(&#039;wgUserName&#039;) || &#039;anon&#039; );&lt;br /&gt;
  function loadState() {&lt;br /&gt;
    try { return JSON.parse( localStorage.getItem(STORAGE_KEY) || &#039;{}&#039; ); }&lt;br /&gt;
    catch (e) { return {}; }&lt;br /&gt;
  }&lt;br /&gt;
  function saveState(state) {&lt;br /&gt;
    try { localStorage.setItem( STORAGE_KEY, JSON.stringify(state) ); }&lt;br /&gt;
    catch (e) {}&lt;br /&gt;
  }&lt;br /&gt;
  var STATE = loadState();&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Utils ---------- */&lt;br /&gt;
  function extractDirective(a) {&lt;br /&gt;
    if (!a) return null;&lt;br /&gt;
    var href = a.getAttribute(&#039;href&#039;) || &#039;&#039;;&lt;br /&gt;
    try { href = new URL(href, location.href).hash || &#039;&#039;; } catch (e) {}&lt;br /&gt;
    var s = (href || &#039;&#039;).trim().toLowerCase();&lt;br /&gt;
    if (s.includes(&#039;#group:&#039;)) return { kind: &#039;group&#039;, value: decodeURIComponent(s.split(&#039;#group:&#039;).pop()) };&lt;br /&gt;
    if (s.includes(&#039;#link:&#039;))  return { kind: &#039;link&#039;,  value: decodeURIComponent(s.split(&#039;#link:&#039;).pop())  };&lt;br /&gt;
    return null;&lt;br /&gt;
  }&lt;br /&gt;
  function stripDirectiveHash(a) {&lt;br /&gt;
    if (!a) return;&lt;br /&gt;
    a.setAttribute(&#039;href&#039;, (a.getAttribute(&#039;href&#039;) || &#039;&#039;).replace(/#(?:link|group):[^#]*$/i, &#039;&#039;) );&lt;br /&gt;
  }&lt;br /&gt;
  function hasRealHref(a) {&lt;br /&gt;
    if (!a) return false;&lt;br /&gt;
    return !/^#/.test( a.getAttribute(&#039;href&#039;) || &#039;&#039; );&lt;br /&gt;
  }&lt;br /&gt;
  function normId(s) {&lt;br /&gt;
    return String(s || &#039;&#039;).trim().toLowerCase()&lt;br /&gt;
      .replace(/\s+/g, &#039;-&#039;).replace(/[^\w\-:.|]/g, &#039;&#039;);&lt;br /&gt;
  }&lt;br /&gt;
  function toggleSet(li, arrow, expanded) {&lt;br /&gt;
    if (expanded) { li.classList.remove(&#039;is-collapsed&#039;); arrow.setAttribute(&#039;aria-expanded&#039;,&#039;true&#039;); }&lt;br /&gt;
    else          { li.classList.add(&#039;is-collapsed&#039;);    arrow.setAttribute(&#039;aria-expanded&#039;,&#039;false&#039;); }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Kopf + ID ---------- */&lt;br /&gt;
  function makeHead(li, keepLink, containerUL, labelOverride) {&lt;br /&gt;
    var a = li.querySelector(&#039;:scope &amp;gt; a&#039;);&lt;br /&gt;
&lt;br /&gt;
    var head = document.createElement(&#039;div&#039;);&lt;br /&gt;
    head.className = &#039;kr-head has-toggle&#039;; // ganze Kopfzeile darf toggeln (außer Link)&lt;br /&gt;
&lt;br /&gt;
    var btn = document.createElement(&#039;button&#039;);&lt;br /&gt;
    btn.type = &#039;button&#039;;&lt;br /&gt;
    btn.className = &#039;kr-arrow&#039;;&lt;br /&gt;
    btn.setAttribute(&#039;aria-expanded&#039;, &#039;false&#039;);&lt;br /&gt;
    head.appendChild(btn);&lt;br /&gt;
&lt;br /&gt;
    if (keepLink &amp;amp;&amp;amp; a) { stripDirectiveHash(a); head.appendChild(a); }&lt;br /&gt;
    else {&lt;br /&gt;
      var span = document.createElement(&#039;span&#039;);&lt;br /&gt;
      span.className = &#039;kr-title&#039;;&lt;br /&gt;
      span.textContent = labelOverride || (a &amp;amp;&amp;amp; a.textContent) || &#039;&#039;;&lt;br /&gt;
      head.appendChild(span);&lt;br /&gt;
      if (a) a.remove();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    li.insertBefore(head, li.firstChild);&lt;br /&gt;
&lt;br /&gt;
    // stabile ID für Persistenz&lt;br /&gt;
    var portlet = containerUL.closest(&#039;.vector-menu&#039;);&lt;br /&gt;
    var pid = (portlet &amp;amp;&amp;amp; (portlet.id || portlet.getAttribute(&#039;id&#039;))) || &#039;portlet&#039;;&lt;br /&gt;
    var labelText = (labelOverride || (head.querySelector(&#039;a, .kr-title&#039;) || {}).textContent || &#039;&#039;).trim();&lt;br /&gt;
    li.dataset.krId = normId( pid + &#039;::&#039; + labelText );&lt;br /&gt;
&lt;br /&gt;
    // Default zu; gespeicherten Zustand anwenden&lt;br /&gt;
    toggleSet(li, btn, false);&lt;br /&gt;
    var st = STATE[ li.dataset.krId ];&lt;br /&gt;
    if (st === 1) toggleSet(li, btn, true);&lt;br /&gt;
&lt;br /&gt;
    return head;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Toggle-Handler ---------- */&lt;br /&gt;
  function handleArrowClick(btn) {&lt;br /&gt;
    var li = btn.closest(&#039;li.kr-group, li.kr-top&#039;);&lt;br /&gt;
    if (!li) return;&lt;br /&gt;
&lt;br /&gt;
    var willExpand = li.classList.contains(&#039;is-collapsed&#039;);&lt;br /&gt;
    toggleSet(li, btn, willExpand);&lt;br /&gt;
&lt;br /&gt;
    var id = li.dataset.krId;&lt;br /&gt;
    if (id) {&lt;br /&gt;
      STATE[id] = willExpand ? 1 : 0;&lt;br /&gt;
      saveState(STATE);&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* Globale Delegation: Pfeil klickt immer; Kopfzeile klickt nur, wenn NICHT im Link */&lt;br /&gt;
  document.addEventListener(&#039;click&#039;, function (e) {&lt;br /&gt;
    var arrow = e.target &amp;amp;&amp;amp; e.target.closest &amp;amp;&amp;amp; e.target.closest(&#039;.kr-arrow&#039;);&lt;br /&gt;
    if (arrow) {&lt;br /&gt;
      e.preventDefault();&lt;br /&gt;
      handleArrowClick(arrow);&lt;br /&gt;
      return;&lt;br /&gt;
    }&lt;br /&gt;
    var head = e.target &amp;amp;&amp;amp; e.target.closest &amp;amp;&amp;amp; e.target.closest(&#039;.kr-head&#039;);&lt;br /&gt;
    if (head &amp;amp;&amp;amp; !e.target.closest(&#039;.kr-head a&#039;)) {&lt;br /&gt;
      e.preventDefault(); // freie Fläche der Kopfzeile&lt;br /&gt;
      var btn = head.querySelector(&#039;.kr-arrow&#039;);&lt;br /&gt;
      if (btn) handleArrowClick(btn);&lt;br /&gt;
    }&lt;br /&gt;
  }, true); // capture=true, damit nichts dazwischenfunkt&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Aufbau der Gruppen ---------- */&lt;br /&gt;
  function buildGroups(ul) {&lt;br /&gt;
    var lis = Array.from( ul.querySelectorAll(&#039;:scope &amp;gt; li.mw-list-item&#039;) );&lt;br /&gt;
    var current = null;&lt;br /&gt;
&lt;br /&gt;
    lis.forEach(function (li) {&lt;br /&gt;
      var a = li.querySelector(&#039;:scope &amp;gt; a&#039;);&lt;br /&gt;
      var d = extractDirective(a);&lt;br /&gt;
&lt;br /&gt;
      if (d &amp;amp;&amp;amp; d.kind === &#039;group&#039;) {&lt;br /&gt;
        var clickable = hasRealHref(a);&lt;br /&gt;
        li.classList.add(&#039;kr-group&#039;);&lt;br /&gt;
        makeHead(li, clickable, ul, (a &amp;amp;&amp;amp; a.textContent) || d.value || &#039;&#039;);&lt;br /&gt;
        if (!clickable &amp;amp;&amp;amp; a) a.remove(); else if (clickable) stripDirectiveHash(a);&lt;br /&gt;
&lt;br /&gt;
        var sub = document.createElement(&#039;ul&#039;);&lt;br /&gt;
        sub.className = &#039;kr-sub&#039;;&lt;br /&gt;
        li.appendChild(sub);&lt;br /&gt;
        current = sub;&lt;br /&gt;
        return;&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      if (current &amp;amp;&amp;amp; !(d &amp;amp;&amp;amp; (d.kind === &#039;group&#039; || d.kind === &#039;link&#039;))) {&lt;br /&gt;
        current.appendChild(li);&lt;br /&gt;
      }&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Portlets verarbeiten ---------- */&lt;br /&gt;
  function buildPortlets(root) {&lt;br /&gt;
    var lists = root.querySelectorAll(&#039;.vector-menu .vector-menu-content-list&#039;);&lt;br /&gt;
    lists.forEach(function (ul) {&lt;br /&gt;
      if (ul.dataset.krSbDone) return;&lt;br /&gt;
      ul.dataset.krSbDone = &#039;1&#039;;&lt;br /&gt;
&lt;br /&gt;
      var items  = Array.from( ul.querySelectorAll(&#039;:scope &amp;gt; li.mw-list-item&#039;) );&lt;br /&gt;
      var topIdx = items.findIndex(function (li) {&lt;br /&gt;
        var a = li.querySelector(&#039;:scope &amp;gt; a&#039;);&lt;br /&gt;
        var d = extractDirective(a);&lt;br /&gt;
        return d &amp;amp;&amp;amp; d.kind === &#039;link&#039;;&lt;br /&gt;
      });&lt;br /&gt;
&lt;br /&gt;
      if (topIdx &amp;gt;= 0) {&lt;br /&gt;
        var top = items[topIdx];&lt;br /&gt;
        top.classList.add(&#039;kr-top&#039;);&lt;br /&gt;
        makeHead(top, true, ul);&lt;br /&gt;
        stripDirectiveHash( top.querySelector(&#039;:scope &amp;gt; a&#039;) );&lt;br /&gt;
&lt;br /&gt;
        var sub = document.createElement(&#039;ul&#039;);&lt;br /&gt;
        sub.className = &#039;kr-sub&#039;;&lt;br /&gt;
        top.appendChild(sub);&lt;br /&gt;
&lt;br /&gt;
        for (var i = topIdx + 1; i &amp;lt; items.length; i++) sub.appendChild(items[i]);&lt;br /&gt;
        buildGroups(sub);&lt;br /&gt;
      } else {&lt;br /&gt;
        buildGroups(ul);&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      // Direktiven aus hrefs entfernen; Vector-Portlet-Heading ausblenden&lt;br /&gt;
      ul.querySelectorAll(&#039;a[href*=&amp;quot;#link:&amp;quot;], a[href*=&amp;quot;#group:&amp;quot;]&#039;).forEach(stripDirectiveHash);&lt;br /&gt;
      var portlet = ul.closest(&#039;.vector-menu&#039;);&lt;br /&gt;
      var heading = portlet &amp;amp;&amp;amp; portlet.querySelector(&#039;:scope &amp;gt; .vector-menu-heading&#039;);&lt;br /&gt;
      if (heading) heading.remove();&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Bootstrapping ---------- */&lt;br /&gt;
  function processRoot(root) {&lt;br /&gt;
    if (!root || root.dataset.krSbRootDone) return;&lt;br /&gt;
    root.dataset.krSbRootDone = &#039;1&#039;;&lt;br /&gt;
    root.classList.add(&#039;kr-sb&#039;);&lt;br /&gt;
    buildPortlets(root);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function init() {&lt;br /&gt;
    document.querySelectorAll(&#039;#mw-panel, #vector-main-menu, #vector-main-menu-pinned, .vector-drawer&#039;)&lt;br /&gt;
      .forEach(processRoot);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  init();&lt;br /&gt;
  mw.hook(&#039;wikipage.content&#039;).add(init);&lt;br /&gt;
&lt;br /&gt;
  // Drawer/DOM-Änderungen beobachten&lt;br /&gt;
  var mo = new MutationObserver(function (muts) {&lt;br /&gt;
    muts.forEach(function (m) {&lt;br /&gt;
      m.addedNodes &amp;amp;&amp;amp; m.addedNodes.forEach(function (n) {&lt;br /&gt;
        if (!(n instanceof Element)) return;&lt;br /&gt;
        if (n.matches(&#039;#vector-main-menu, #vector-main-menu-pinned, .vector-drawer, #mw-panel&#039;)) processRoot(n);&lt;br /&gt;
        n.querySelectorAll &amp;amp;&amp;amp; n.querySelectorAll(&#039;#vector-main-menu, #vector-main-menu-pinned, .vector-drawer, #mw-panel&#039;)&lt;br /&gt;
          .forEach(processRoot);&lt;br /&gt;
      });&lt;br /&gt;
    });&lt;br /&gt;
  });&lt;br /&gt;
  mo.observe(document.body, { childList: true, subtree: true });&lt;br /&gt;
})();&lt;br /&gt;
&lt;br /&gt;
/* TabberNeue: Anker-Scroll unter Vector-Header + Freeze bei Transclusion */&lt;br /&gt;
(function () {&lt;br /&gt;
  &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
  /* -------- Sticky-Header-Höhe (+0.5rem Luft) ---------- */&lt;br /&gt;
  function stickyOffsetPx() {&lt;br /&gt;
    var hdr = document.querySelector(&#039;.vector-sticky-header-container&#039;);&lt;br /&gt;
    var h = hdr ? Math.round(hdr.getBoundingClientRect().height) : 56;&lt;br /&gt;
    var air = Math.round(parseFloat(getComputedStyle(document.documentElement).fontSize) * 0.5);&lt;br /&gt;
    return h + air;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* -------- Oberste Tabber-Leiste für ein Panel ---------- */&lt;br /&gt;
  function headerForPanel(panel) {&lt;br /&gt;
    if (!panel) return null;&lt;br /&gt;
    var top = panel.closest(&#039;.tabber&#039;);&lt;br /&gt;
    for (var up, cur = top; cur &amp;amp;&amp;amp; cur.parentElement; ) {&lt;br /&gt;
      up = cur.parentElement.closest(&#039;.tabber&#039;);&lt;br /&gt;
      if (!up) break;&lt;br /&gt;
      top = up; cur = up;&lt;br /&gt;
    }&lt;br /&gt;
    return top ? top.querySelector(&#039;:scope &amp;gt; .tabber__header&#039;) : null;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* -------- Freeze: hält die Layout-Höhe stabil ---------- */&lt;br /&gt;
  function freezeTabber(tabber) {&lt;br /&gt;
    var section = tabber &amp;amp;&amp;amp; tabber.querySelector(&#039;:scope &amp;gt; .tabber__section&#039;);&lt;br /&gt;
    if (!section) return function(){};&lt;br /&gt;
&lt;br /&gt;
    var h = Math.max(0, Math.round(section.getBoundingClientRect().height));&lt;br /&gt;
    tabber.style.setProperty(&#039;--tabber-freeze-h&#039;, h + &#039;px&#039;);&lt;br /&gt;
    tabber.classList.add(&#039;tabber-is-switching&#039;);&lt;br /&gt;
&lt;br /&gt;
    var closed = false, stable = 0, last = h;&lt;br /&gt;
    function tick() {&lt;br /&gt;
      var cur = Math.round(section.getBoundingClientRect().height);&lt;br /&gt;
      if (Math.abs(cur - last) &amp;lt; 1) stable++; else { stable = 0; last = cur; }&lt;br /&gt;
      if (!closed) requestAnimationFrame(tick);&lt;br /&gt;
    }&lt;br /&gt;
    requestAnimationFrame(tick);&lt;br /&gt;
&lt;br /&gt;
    return function unfreeze() {&lt;br /&gt;
      if (closed) return; closed = true;&lt;br /&gt;
      tabber.classList.remove(&#039;tabber-is-switching&#039;);&lt;br /&gt;
      tabber.style.removeProperty(&#039;--tabber-freeze-h&#039;);&lt;br /&gt;
    };&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* -------- Tabber-eigenes ScrollIntoView neutralisieren ---------- */&lt;br /&gt;
  var _siv = Element.prototype.scrollIntoView;&lt;br /&gt;
  Element.prototype.scrollIntoView = function(arg){&lt;br /&gt;
    try { if (this.closest &amp;amp;&amp;amp; this.closest(&#039;.tabber&#039;)) return; } catch(e){}&lt;br /&gt;
    return _siv.call(this, arg);&lt;br /&gt;
  };&lt;br /&gt;
&lt;br /&gt;
  /* -------- Sprung rechnen ---------- */&lt;br /&gt;
  function jumpTo(id, behavior) {&lt;br /&gt;
    var name = (id || &#039;&#039;).replace(/^#/, &#039;&#039;);&lt;br /&gt;
    if (!/^tabber-/.test(name)) return;&lt;br /&gt;
    var panel  = document.getElementById(name); if (!panel) return;&lt;br /&gt;
    var header = headerForPanel(panel) || panel;&lt;br /&gt;
    var y = header.getBoundingClientRect().top + window.scrollY - stickyOffsetPx();&lt;br /&gt;
    window.scrollTo({ top: y, behavior: behavior || &#039;auto&#039; });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* -------- „smarter“ Sprung inkl. Freeze &amp;amp; Nachjustierung ---------- */&lt;br /&gt;
  function smartJump(id) {&lt;br /&gt;
    var name = (id || &#039;&#039;).replace(/^#/, &#039;&#039;);&lt;br /&gt;
    if (!/^tabber-/.test(name)) return;&lt;br /&gt;
&lt;br /&gt;
    var panel  = document.getElementById(name); if (!panel) return;&lt;br /&gt;
    var tabber = panel.closest(&#039;.tabber&#039;);&lt;br /&gt;
    var unfreeze = tabber ? freezeTabber(tabber) : function(){};&lt;br /&gt;
&lt;br /&gt;
    // CSS-smooth-scroll kurz aus -&amp;gt; sofortige, verlässliche Positionierung&lt;br /&gt;
    document.documentElement.classList.add(&#039;fs-scroll-now&#039;);&lt;br /&gt;
    jumpTo(id, &#039;auto&#039;); // Sofort (vor Tabber-Umschaltung)&lt;br /&gt;
&lt;br /&gt;
    // Während des Umschaltens „festklemmen“ und mehrfach nachführen,&lt;br /&gt;
    // danach glatt ausblenden und final sauber positionieren.&lt;br /&gt;
    var frames = 0;&lt;br /&gt;
    (function pinLoop(){&lt;br /&gt;
      frames++;&lt;br /&gt;
      jumpTo(id, &#039;auto&#039;);&lt;br /&gt;
      if (frames &amp;lt; 15) { requestAnimationFrame(pinLoop); return; }&lt;br /&gt;
&lt;br /&gt;
      document.documentElement.classList.remove(&#039;fs-scroll-now&#039;);&lt;br /&gt;
      unfreeze();&lt;br /&gt;
      jumpTo(id, &#039;smooth&#039;); // feines Nachjustieren&lt;br /&gt;
    })();&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* -------- Hash beim Laden ---------- */&lt;br /&gt;
  if (/^#tabber-/.test(location.hash)) {&lt;br /&gt;
    // ganz kurz warten, bis TabberNeue ARIA/hidden gesetzt hat&lt;br /&gt;
    setTimeout(function(){ smartJump(location.hash); }, 0);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* -------- Hash-Wechsel ---------- */&lt;br /&gt;
  window.addEventListener(&#039;hashchange&#039;, function () {&lt;br /&gt;
    if (/^#tabber-/.test(location.hash)) smartJump(location.hash);&lt;br /&gt;
  });&lt;br /&gt;
&lt;br /&gt;
  /* -------- Klicks auf Tabs (Capture, ohne preventDefault) ---------- */&lt;br /&gt;
  document.addEventListener(&#039;click&#039;, function (e) {&lt;br /&gt;
    var a = e.target &amp;amp;&amp;amp; e.target.closest &amp;amp;&amp;amp; e.target.closest(&#039;a[href^=&amp;quot;#tabber-&amp;quot;], button[aria-controls^=&amp;quot;tabber-&amp;quot;]&#039;);&lt;br /&gt;
    if (!a) return;&lt;br /&gt;
    var id = a.getAttribute(&#039;href&#039;) || (&#039;#&#039; + a.getAttribute(&#039;aria-controls&#039;));&lt;br /&gt;
    smartJump(id); // vor TabberNeue handeln&lt;br /&gt;
  }, true);&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Webmaster</name></author>
	</entry>
	<entry>
		<id>https://firestone-idle-rpg.de/index.php?title=MediaWiki:Common.js&amp;diff=3773</id>
		<title>MediaWiki:Common.js</title>
		<link rel="alternate" type="text/html" href="https://firestone-idle-rpg.de/index.php?title=MediaWiki:Common.js&amp;diff=3773"/>
		<updated>2025-10-30T02:03:50Z</updated>

		<summary type="html">&lt;p&gt;Webmaster: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* Das folgende JavaScript wird für alle Benutzer geladen. */&lt;br /&gt;
/* ==== Sidebar: klickbare Kopfzeile + Gruppen mit Auf/Zu + Persistenz (Vector-2022 &amp;amp; Drawer) ==== */&lt;br /&gt;
(function () {&lt;br /&gt;
  &#039;use strict&#039;;&lt;br /&gt;
  if ( mw.config.get(&#039;skin&#039;) !== &#039;vector-2022&#039; ) return;&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Persistenz ---------- */&lt;br /&gt;
  var STORAGE_KEY = &#039;kr-sb-state:v3:&#039; + ( mw.config.get(&#039;wgUserName&#039;) || &#039;anon&#039; );&lt;br /&gt;
  function loadState() {&lt;br /&gt;
    try { return JSON.parse( localStorage.getItem(STORAGE_KEY) || &#039;{}&#039; ); }&lt;br /&gt;
    catch (e) { return {}; }&lt;br /&gt;
  }&lt;br /&gt;
  function saveState(state) {&lt;br /&gt;
    try { localStorage.setItem( STORAGE_KEY, JSON.stringify(state) ); }&lt;br /&gt;
    catch (e) {}&lt;br /&gt;
  }&lt;br /&gt;
  var STATE = loadState();&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Utils ---------- */&lt;br /&gt;
  function extractDirective(a) {&lt;br /&gt;
    if (!a) return null;&lt;br /&gt;
    var href = a.getAttribute(&#039;href&#039;) || &#039;&#039;;&lt;br /&gt;
    try { href = new URL(href, location.href).hash || &#039;&#039;; } catch (e) {}&lt;br /&gt;
    var s = (href || &#039;&#039;).trim().toLowerCase();&lt;br /&gt;
    if (s.includes(&#039;#group:&#039;)) return { kind: &#039;group&#039;, value: decodeURIComponent(s.split(&#039;#group:&#039;).pop()) };&lt;br /&gt;
    if (s.includes(&#039;#link:&#039;))  return { kind: &#039;link&#039;,  value: decodeURIComponent(s.split(&#039;#link:&#039;).pop())  };&lt;br /&gt;
    return null;&lt;br /&gt;
  }&lt;br /&gt;
  function stripDirectiveHash(a) {&lt;br /&gt;
    if (!a) return;&lt;br /&gt;
    a.setAttribute(&#039;href&#039;, (a.getAttribute(&#039;href&#039;) || &#039;&#039;).replace(/#(?:link|group):[^#]*$/i, &#039;&#039;) );&lt;br /&gt;
  }&lt;br /&gt;
  function hasRealHref(a) {&lt;br /&gt;
    if (!a) return false;&lt;br /&gt;
    return !/^#/.test( a.getAttribute(&#039;href&#039;) || &#039;&#039; );&lt;br /&gt;
  }&lt;br /&gt;
  function normId(s) {&lt;br /&gt;
    return String(s || &#039;&#039;).trim().toLowerCase()&lt;br /&gt;
      .replace(/\s+/g, &#039;-&#039;).replace(/[^\w\-:.|]/g, &#039;&#039;);&lt;br /&gt;
  }&lt;br /&gt;
  function toggleSet(li, arrow, expanded) {&lt;br /&gt;
    if (expanded) { li.classList.remove(&#039;is-collapsed&#039;); arrow.setAttribute(&#039;aria-expanded&#039;,&#039;true&#039;); }&lt;br /&gt;
    else          { li.classList.add(&#039;is-collapsed&#039;);    arrow.setAttribute(&#039;aria-expanded&#039;,&#039;false&#039;); }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Kopf + ID ---------- */&lt;br /&gt;
  function makeHead(li, keepLink, containerUL, labelOverride) {&lt;br /&gt;
    var a = li.querySelector(&#039;:scope &amp;gt; a&#039;);&lt;br /&gt;
&lt;br /&gt;
    var head = document.createElement(&#039;div&#039;);&lt;br /&gt;
    head.className = &#039;kr-head has-toggle&#039;; // ganze Kopfzeile darf toggeln (außer Link)&lt;br /&gt;
&lt;br /&gt;
    var btn = document.createElement(&#039;button&#039;);&lt;br /&gt;
    btn.type = &#039;button&#039;;&lt;br /&gt;
    btn.className = &#039;kr-arrow&#039;;&lt;br /&gt;
    btn.setAttribute(&#039;aria-expanded&#039;, &#039;false&#039;);&lt;br /&gt;
    head.appendChild(btn);&lt;br /&gt;
&lt;br /&gt;
    if (keepLink &amp;amp;&amp;amp; a) { stripDirectiveHash(a); head.appendChild(a); }&lt;br /&gt;
    else {&lt;br /&gt;
      var span = document.createElement(&#039;span&#039;);&lt;br /&gt;
      span.className = &#039;kr-title&#039;;&lt;br /&gt;
      span.textContent = labelOverride || (a &amp;amp;&amp;amp; a.textContent) || &#039;&#039;;&lt;br /&gt;
      head.appendChild(span);&lt;br /&gt;
      if (a) a.remove();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    li.insertBefore(head, li.firstChild);&lt;br /&gt;
&lt;br /&gt;
    // stabile ID für Persistenz&lt;br /&gt;
    var portlet = containerUL.closest(&#039;.vector-menu&#039;);&lt;br /&gt;
    var pid = (portlet &amp;amp;&amp;amp; (portlet.id || portlet.getAttribute(&#039;id&#039;))) || &#039;portlet&#039;;&lt;br /&gt;
    var labelText = (labelOverride || (head.querySelector(&#039;a, .kr-title&#039;) || {}).textContent || &#039;&#039;).trim();&lt;br /&gt;
    li.dataset.krId = normId( pid + &#039;::&#039; + labelText );&lt;br /&gt;
&lt;br /&gt;
    // Default zu; gespeicherten Zustand anwenden&lt;br /&gt;
    toggleSet(li, btn, false);&lt;br /&gt;
    var st = STATE[ li.dataset.krId ];&lt;br /&gt;
    if (st === 1) toggleSet(li, btn, true);&lt;br /&gt;
&lt;br /&gt;
    return head;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Toggle-Handler ---------- */&lt;br /&gt;
  function handleArrowClick(btn) {&lt;br /&gt;
    var li = btn.closest(&#039;li.kr-group, li.kr-top&#039;);&lt;br /&gt;
    if (!li) return;&lt;br /&gt;
&lt;br /&gt;
    var willExpand = li.classList.contains(&#039;is-collapsed&#039;);&lt;br /&gt;
    toggleSet(li, btn, willExpand);&lt;br /&gt;
&lt;br /&gt;
    var id = li.dataset.krId;&lt;br /&gt;
    if (id) {&lt;br /&gt;
      STATE[id] = willExpand ? 1 : 0;&lt;br /&gt;
      saveState(STATE);&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* Globale Delegation: Pfeil klickt immer; Kopfzeile klickt nur, wenn NICHT im Link */&lt;br /&gt;
  document.addEventListener(&#039;click&#039;, function (e) {&lt;br /&gt;
    var arrow = e.target &amp;amp;&amp;amp; e.target.closest &amp;amp;&amp;amp; e.target.closest(&#039;.kr-arrow&#039;);&lt;br /&gt;
    if (arrow) {&lt;br /&gt;
      e.preventDefault();&lt;br /&gt;
      handleArrowClick(arrow);&lt;br /&gt;
      return;&lt;br /&gt;
    }&lt;br /&gt;
    var head = e.target &amp;amp;&amp;amp; e.target.closest &amp;amp;&amp;amp; e.target.closest(&#039;.kr-head&#039;);&lt;br /&gt;
    if (head &amp;amp;&amp;amp; !e.target.closest(&#039;.kr-head a&#039;)) {&lt;br /&gt;
      e.preventDefault(); // freie Fläche der Kopfzeile&lt;br /&gt;
      var btn = head.querySelector(&#039;.kr-arrow&#039;);&lt;br /&gt;
      if (btn) handleArrowClick(btn);&lt;br /&gt;
    }&lt;br /&gt;
  }, true); // capture=true, damit nichts dazwischenfunkt&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Aufbau der Gruppen ---------- */&lt;br /&gt;
  function buildGroups(ul) {&lt;br /&gt;
    var lis = Array.from( ul.querySelectorAll(&#039;:scope &amp;gt; li.mw-list-item&#039;) );&lt;br /&gt;
    var current = null;&lt;br /&gt;
&lt;br /&gt;
    lis.forEach(function (li) {&lt;br /&gt;
      var a = li.querySelector(&#039;:scope &amp;gt; a&#039;);&lt;br /&gt;
      var d = extractDirective(a);&lt;br /&gt;
&lt;br /&gt;
      if (d &amp;amp;&amp;amp; d.kind === &#039;group&#039;) {&lt;br /&gt;
        var clickable = hasRealHref(a);&lt;br /&gt;
        li.classList.add(&#039;kr-group&#039;);&lt;br /&gt;
        makeHead(li, clickable, ul, (a &amp;amp;&amp;amp; a.textContent) || d.value || &#039;&#039;);&lt;br /&gt;
        if (!clickable &amp;amp;&amp;amp; a) a.remove(); else if (clickable) stripDirectiveHash(a);&lt;br /&gt;
&lt;br /&gt;
        var sub = document.createElement(&#039;ul&#039;);&lt;br /&gt;
        sub.className = &#039;kr-sub&#039;;&lt;br /&gt;
        li.appendChild(sub);&lt;br /&gt;
        current = sub;&lt;br /&gt;
        return;&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      if (current &amp;amp;&amp;amp; !(d &amp;amp;&amp;amp; (d.kind === &#039;group&#039; || d.kind === &#039;link&#039;))) {&lt;br /&gt;
        current.appendChild(li);&lt;br /&gt;
      }&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Portlets verarbeiten ---------- */&lt;br /&gt;
  function buildPortlets(root) {&lt;br /&gt;
    var lists = root.querySelectorAll(&#039;.vector-menu .vector-menu-content-list&#039;);&lt;br /&gt;
    lists.forEach(function (ul) {&lt;br /&gt;
      if (ul.dataset.krSbDone) return;&lt;br /&gt;
      ul.dataset.krSbDone = &#039;1&#039;;&lt;br /&gt;
&lt;br /&gt;
      var items  = Array.from( ul.querySelectorAll(&#039;:scope &amp;gt; li.mw-list-item&#039;) );&lt;br /&gt;
      var topIdx = items.findIndex(function (li) {&lt;br /&gt;
        var a = li.querySelector(&#039;:scope &amp;gt; a&#039;);&lt;br /&gt;
        var d = extractDirective(a);&lt;br /&gt;
        return d &amp;amp;&amp;amp; d.kind === &#039;link&#039;;&lt;br /&gt;
      });&lt;br /&gt;
&lt;br /&gt;
      if (topIdx &amp;gt;= 0) {&lt;br /&gt;
        var top = items[topIdx];&lt;br /&gt;
        top.classList.add(&#039;kr-top&#039;);&lt;br /&gt;
        makeHead(top, true, ul);&lt;br /&gt;
        stripDirectiveHash( top.querySelector(&#039;:scope &amp;gt; a&#039;) );&lt;br /&gt;
&lt;br /&gt;
        var sub = document.createElement(&#039;ul&#039;);&lt;br /&gt;
        sub.className = &#039;kr-sub&#039;;&lt;br /&gt;
        top.appendChild(sub);&lt;br /&gt;
&lt;br /&gt;
        for (var i = topIdx + 1; i &amp;lt; items.length; i++) sub.appendChild(items[i]);&lt;br /&gt;
        buildGroups(sub);&lt;br /&gt;
      } else {&lt;br /&gt;
        buildGroups(ul);&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      // Direktiven aus hrefs entfernen; Vector-Portlet-Heading ausblenden&lt;br /&gt;
      ul.querySelectorAll(&#039;a[href*=&amp;quot;#link:&amp;quot;], a[href*=&amp;quot;#group:&amp;quot;]&#039;).forEach(stripDirectiveHash);&lt;br /&gt;
      var portlet = ul.closest(&#039;.vector-menu&#039;);&lt;br /&gt;
      var heading = portlet &amp;amp;&amp;amp; portlet.querySelector(&#039;:scope &amp;gt; .vector-menu-heading&#039;);&lt;br /&gt;
      if (heading) heading.remove();&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Bootstrapping ---------- */&lt;br /&gt;
  function processRoot(root) {&lt;br /&gt;
    if (!root || root.dataset.krSbRootDone) return;&lt;br /&gt;
    root.dataset.krSbRootDone = &#039;1&#039;;&lt;br /&gt;
    root.classList.add(&#039;kr-sb&#039;);&lt;br /&gt;
    buildPortlets(root);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function init() {&lt;br /&gt;
    document.querySelectorAll(&#039;#mw-panel, #vector-main-menu, #vector-main-menu-pinned, .vector-drawer&#039;)&lt;br /&gt;
      .forEach(processRoot);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  init();&lt;br /&gt;
  mw.hook(&#039;wikipage.content&#039;).add(init);&lt;br /&gt;
&lt;br /&gt;
  // Drawer/DOM-Änderungen beobachten&lt;br /&gt;
  var mo = new MutationObserver(function (muts) {&lt;br /&gt;
    muts.forEach(function (m) {&lt;br /&gt;
      m.addedNodes &amp;amp;&amp;amp; m.addedNodes.forEach(function (n) {&lt;br /&gt;
        if (!(n instanceof Element)) return;&lt;br /&gt;
        if (n.matches(&#039;#vector-main-menu, #vector-main-menu-pinned, .vector-drawer, #mw-panel&#039;)) processRoot(n);&lt;br /&gt;
        n.querySelectorAll &amp;amp;&amp;amp; n.querySelectorAll(&#039;#vector-main-menu, #vector-main-menu-pinned, .vector-drawer, #mw-panel&#039;)&lt;br /&gt;
          .forEach(processRoot);&lt;br /&gt;
      });&lt;br /&gt;
    });&lt;br /&gt;
  });&lt;br /&gt;
  mo.observe(document.body, { childList: true, subtree: true });&lt;br /&gt;
})();&lt;br /&gt;
&lt;br /&gt;
/* TabberNeue: Anchor-Lock wie früher #TabberStart (ohne echtes Anker-Element) */&lt;br /&gt;
(function () {&lt;br /&gt;
  function stickyOffsetPx() {&lt;br /&gt;
    var hdr = document.querySelector(&#039;.vector-sticky-header-container&#039;);&lt;br /&gt;
    var h = hdr ? Math.round(hdr.getBoundingClientRect().height) : 0;&lt;br /&gt;
    var air = Math.round(parseFloat(getComputedStyle(document.documentElement).fontSize) * 0.5);&lt;br /&gt;
    return h + air;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function headerForPanel(panel) {&lt;br /&gt;
    if (!panel) return null;&lt;br /&gt;
    var top = panel.closest(&#039;.tabber&#039;);&lt;br /&gt;
    for (var cur = top; cur &amp;amp;&amp;amp; cur.parentElement; ) {&lt;br /&gt;
      var up = cur.parentElement.closest(&#039;.tabber&#039;);&lt;br /&gt;
      if (!up) break;&lt;br /&gt;
      top = up; cur = up;&lt;br /&gt;
    }&lt;br /&gt;
    return top ? top.querySelector(&#039;:scope &amp;gt; .tabber__header&#039;) : null;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // Scroll an Y „anklammern“ und für ms Millisekunden festhalten&lt;br /&gt;
  function lockScroll(y, ms) {&lt;br /&gt;
    var until = performance.now() + (ms || 900);&lt;br /&gt;
    var active = true;&lt;br /&gt;
&lt;br /&gt;
    function clamp() {&lt;br /&gt;
      if (!active) return;&lt;br /&gt;
      window.scrollTo(0, y);&lt;br /&gt;
      if (performance.now() &amp;lt; until) requestAnimationFrame(clamp);&lt;br /&gt;
    }&lt;br /&gt;
    function onScroll() {&lt;br /&gt;
      if (!active) return;&lt;br /&gt;
      if (Math.abs(window.scrollY - y) &amp;gt; 1) window.scrollTo(0, y);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    window.addEventListener(&#039;scroll&#039;, onScroll, { passive: true });&lt;br /&gt;
    requestAnimationFrame(clamp);&lt;br /&gt;
&lt;br /&gt;
    return function release() {&lt;br /&gt;
      active = false;&lt;br /&gt;
      window.removeEventListener(&#039;scroll&#039;, onScroll);&lt;br /&gt;
    };&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // Warten bis die Tabber-Höhe einige Frames stabil bleibt&lt;br /&gt;
  function waitStableHeight(tabber, frames) {&lt;br /&gt;
    var sec = tabber &amp;amp;&amp;amp; tabber.querySelector(&#039;:scope &amp;gt; .tabber__section&#039;);&lt;br /&gt;
    if (!sec) return Promise.resolve();&lt;br /&gt;
    var last = sec.getBoundingClientRect().height, ok = 0, need = frames || 8;&lt;br /&gt;
    return new Promise(function (resolve) {&lt;br /&gt;
      function step() {&lt;br /&gt;
        var h = sec.getBoundingClientRect().height;&lt;br /&gt;
        ok = (Math.abs(h - last) &amp;lt; 1) ? ok + 1 : 0;&lt;br /&gt;
        last = h;&lt;br /&gt;
        if (ok &amp;gt;= need) return resolve();&lt;br /&gt;
        requestAnimationFrame(step);&lt;br /&gt;
      }&lt;br /&gt;
      requestAnimationFrame(step);&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function jumpToTab(id) {&lt;br /&gt;
    var name = (id || &#039;&#039;).replace(/^#/, &#039;&#039;);&lt;br /&gt;
    if (!/^tabber-/.test(name)) return;&lt;br /&gt;
&lt;br /&gt;
    var panel  = document.getElementById(name);&lt;br /&gt;
    if (!panel) return;&lt;br /&gt;
&lt;br /&gt;
    var tabber = panel.closest(&#039;.tabber&#039;);&lt;br /&gt;
    var header = headerForPanel(panel);&lt;br /&gt;
    var anchor = header || panel;&lt;br /&gt;
&lt;br /&gt;
    var y = anchor.getBoundingClientRect().top + window.scrollY - stickyOffsetPx();&lt;br /&gt;
&lt;br /&gt;
    // 1) Sofortiger Sprung, dann Scroll „anklammern“&lt;br /&gt;
    window.scrollTo(0, y);&lt;br /&gt;
    var release = lockScroll(y, 1100);            // ggf. 900–1500 ms anpassen&lt;br /&gt;
    if (tabber) tabber.classList.add(&#039;tabber-is-switching&#039;); // nutzt deine vorhandene CSS-Regel&lt;br /&gt;
&lt;br /&gt;
    // 2) Nach Stabilisierung lösen + fein nachjustieren&lt;br /&gt;
    waitStableHeight(tabber).then(function () {&lt;br /&gt;
      release();&lt;br /&gt;
      if (tabber) tabber.classList.remove(&#039;tabber-is-switching&#039;);&lt;br /&gt;
      var y2 = (header || panel).getBoundingClientRect().top + window.scrollY - stickyOffsetPx();&lt;br /&gt;
      if (Math.abs(y2 - y) &amp;gt; 1) window.scrollTo({ top: y2, behavior: &#039;smooth&#039; });&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // Direktaufruf mit Hash&lt;br /&gt;
  if (/^#tabber-/.test(location.hash)) {&lt;br /&gt;
    setTimeout(function () { jumpToTab(location.hash); }, 60);&lt;br /&gt;
  }&lt;br /&gt;
  // Hashwechsel (falls gesetzt)&lt;br /&gt;
  window.addEventListener(&#039;hashchange&#039;, function () {&lt;br /&gt;
    if (/^#tabber-/.test(location.hash)) jumpToTab(location.hash);&lt;br /&gt;
  });&lt;br /&gt;
  // Tab-Klicks (TabberNeue nutzt History-API → kurzer Delay)&lt;br /&gt;
  document.addEventListener(&#039;click&#039;, function (e) {&lt;br /&gt;
    var a = e.target &amp;amp;&amp;amp; e.target.closest(&#039;a[href^=&amp;quot;#tabber-&amp;quot;], button[aria-controls^=&amp;quot;tabber-&amp;quot;]&#039;);&lt;br /&gt;
    if (!a) return;&lt;br /&gt;
    var id = a.getAttribute(&#039;href&#039;) || (&#039;#&#039; + a.getAttribute(&#039;aria-controls&#039;));&lt;br /&gt;
    setTimeout(function () { jumpToTab(id); }, 60);&lt;br /&gt;
  }, true);&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Webmaster</name></author>
	</entry>
	<entry>
		<id>https://firestone-idle-rpg.de/index.php?title=MediaWiki:Common.js&amp;diff=3772</id>
		<title>MediaWiki:Common.js</title>
		<link rel="alternate" type="text/html" href="https://firestone-idle-rpg.de/index.php?title=MediaWiki:Common.js&amp;diff=3772"/>
		<updated>2025-10-30T01:33:48Z</updated>

		<summary type="html">&lt;p&gt;Webmaster: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* Das folgende JavaScript wird für alle Benutzer geladen. */&lt;br /&gt;
/* ==== Sidebar: klickbare Kopfzeile + Gruppen mit Auf/Zu + Persistenz (Vector-2022 &amp;amp; Drawer) ==== */&lt;br /&gt;
(function () {&lt;br /&gt;
  &#039;use strict&#039;;&lt;br /&gt;
  if ( mw.config.get(&#039;skin&#039;) !== &#039;vector-2022&#039; ) return;&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Persistenz ---------- */&lt;br /&gt;
  var STORAGE_KEY = &#039;kr-sb-state:v3:&#039; + ( mw.config.get(&#039;wgUserName&#039;) || &#039;anon&#039; );&lt;br /&gt;
  function loadState() {&lt;br /&gt;
    try { return JSON.parse( localStorage.getItem(STORAGE_KEY) || &#039;{}&#039; ); }&lt;br /&gt;
    catch (e) { return {}; }&lt;br /&gt;
  }&lt;br /&gt;
  function saveState(state) {&lt;br /&gt;
    try { localStorage.setItem( STORAGE_KEY, JSON.stringify(state) ); }&lt;br /&gt;
    catch (e) {}&lt;br /&gt;
  }&lt;br /&gt;
  var STATE = loadState();&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Utils ---------- */&lt;br /&gt;
  function extractDirective(a) {&lt;br /&gt;
    if (!a) return null;&lt;br /&gt;
    var href = a.getAttribute(&#039;href&#039;) || &#039;&#039;;&lt;br /&gt;
    try { href = new URL(href, location.href).hash || &#039;&#039;; } catch (e) {}&lt;br /&gt;
    var s = (href || &#039;&#039;).trim().toLowerCase();&lt;br /&gt;
    if (s.includes(&#039;#group:&#039;)) return { kind: &#039;group&#039;, value: decodeURIComponent(s.split(&#039;#group:&#039;).pop()) };&lt;br /&gt;
    if (s.includes(&#039;#link:&#039;))  return { kind: &#039;link&#039;,  value: decodeURIComponent(s.split(&#039;#link:&#039;).pop())  };&lt;br /&gt;
    return null;&lt;br /&gt;
  }&lt;br /&gt;
  function stripDirectiveHash(a) {&lt;br /&gt;
    if (!a) return;&lt;br /&gt;
    a.setAttribute(&#039;href&#039;, (a.getAttribute(&#039;href&#039;) || &#039;&#039;).replace(/#(?:link|group):[^#]*$/i, &#039;&#039;) );&lt;br /&gt;
  }&lt;br /&gt;
  function hasRealHref(a) {&lt;br /&gt;
    if (!a) return false;&lt;br /&gt;
    return !/^#/.test( a.getAttribute(&#039;href&#039;) || &#039;&#039; );&lt;br /&gt;
  }&lt;br /&gt;
  function normId(s) {&lt;br /&gt;
    return String(s || &#039;&#039;).trim().toLowerCase()&lt;br /&gt;
      .replace(/\s+/g, &#039;-&#039;).replace(/[^\w\-:.|]/g, &#039;&#039;);&lt;br /&gt;
  }&lt;br /&gt;
  function toggleSet(li, arrow, expanded) {&lt;br /&gt;
    if (expanded) { li.classList.remove(&#039;is-collapsed&#039;); arrow.setAttribute(&#039;aria-expanded&#039;,&#039;true&#039;); }&lt;br /&gt;
    else          { li.classList.add(&#039;is-collapsed&#039;);    arrow.setAttribute(&#039;aria-expanded&#039;,&#039;false&#039;); }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Kopf + ID ---------- */&lt;br /&gt;
  function makeHead(li, keepLink, containerUL, labelOverride) {&lt;br /&gt;
    var a = li.querySelector(&#039;:scope &amp;gt; a&#039;);&lt;br /&gt;
&lt;br /&gt;
    var head = document.createElement(&#039;div&#039;);&lt;br /&gt;
    head.className = &#039;kr-head has-toggle&#039;; // ganze Kopfzeile darf toggeln (außer Link)&lt;br /&gt;
&lt;br /&gt;
    var btn = document.createElement(&#039;button&#039;);&lt;br /&gt;
    btn.type = &#039;button&#039;;&lt;br /&gt;
    btn.className = &#039;kr-arrow&#039;;&lt;br /&gt;
    btn.setAttribute(&#039;aria-expanded&#039;, &#039;false&#039;);&lt;br /&gt;
    head.appendChild(btn);&lt;br /&gt;
&lt;br /&gt;
    if (keepLink &amp;amp;&amp;amp; a) { stripDirectiveHash(a); head.appendChild(a); }&lt;br /&gt;
    else {&lt;br /&gt;
      var span = document.createElement(&#039;span&#039;);&lt;br /&gt;
      span.className = &#039;kr-title&#039;;&lt;br /&gt;
      span.textContent = labelOverride || (a &amp;amp;&amp;amp; a.textContent) || &#039;&#039;;&lt;br /&gt;
      head.appendChild(span);&lt;br /&gt;
      if (a) a.remove();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    li.insertBefore(head, li.firstChild);&lt;br /&gt;
&lt;br /&gt;
    // stabile ID für Persistenz&lt;br /&gt;
    var portlet = containerUL.closest(&#039;.vector-menu&#039;);&lt;br /&gt;
    var pid = (portlet &amp;amp;&amp;amp; (portlet.id || portlet.getAttribute(&#039;id&#039;))) || &#039;portlet&#039;;&lt;br /&gt;
    var labelText = (labelOverride || (head.querySelector(&#039;a, .kr-title&#039;) || {}).textContent || &#039;&#039;).trim();&lt;br /&gt;
    li.dataset.krId = normId( pid + &#039;::&#039; + labelText );&lt;br /&gt;
&lt;br /&gt;
    // Default zu; gespeicherten Zustand anwenden&lt;br /&gt;
    toggleSet(li, btn, false);&lt;br /&gt;
    var st = STATE[ li.dataset.krId ];&lt;br /&gt;
    if (st === 1) toggleSet(li, btn, true);&lt;br /&gt;
&lt;br /&gt;
    return head;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Toggle-Handler ---------- */&lt;br /&gt;
  function handleArrowClick(btn) {&lt;br /&gt;
    var li = btn.closest(&#039;li.kr-group, li.kr-top&#039;);&lt;br /&gt;
    if (!li) return;&lt;br /&gt;
&lt;br /&gt;
    var willExpand = li.classList.contains(&#039;is-collapsed&#039;);&lt;br /&gt;
    toggleSet(li, btn, willExpand);&lt;br /&gt;
&lt;br /&gt;
    var id = li.dataset.krId;&lt;br /&gt;
    if (id) {&lt;br /&gt;
      STATE[id] = willExpand ? 1 : 0;&lt;br /&gt;
      saveState(STATE);&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* Globale Delegation: Pfeil klickt immer; Kopfzeile klickt nur, wenn NICHT im Link */&lt;br /&gt;
  document.addEventListener(&#039;click&#039;, function (e) {&lt;br /&gt;
    var arrow = e.target &amp;amp;&amp;amp; e.target.closest &amp;amp;&amp;amp; e.target.closest(&#039;.kr-arrow&#039;);&lt;br /&gt;
    if (arrow) {&lt;br /&gt;
      e.preventDefault();&lt;br /&gt;
      handleArrowClick(arrow);&lt;br /&gt;
      return;&lt;br /&gt;
    }&lt;br /&gt;
    var head = e.target &amp;amp;&amp;amp; e.target.closest &amp;amp;&amp;amp; e.target.closest(&#039;.kr-head&#039;);&lt;br /&gt;
    if (head &amp;amp;&amp;amp; !e.target.closest(&#039;.kr-head a&#039;)) {&lt;br /&gt;
      e.preventDefault(); // freie Fläche der Kopfzeile&lt;br /&gt;
      var btn = head.querySelector(&#039;.kr-arrow&#039;);&lt;br /&gt;
      if (btn) handleArrowClick(btn);&lt;br /&gt;
    }&lt;br /&gt;
  }, true); // capture=true, damit nichts dazwischenfunkt&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Aufbau der Gruppen ---------- */&lt;br /&gt;
  function buildGroups(ul) {&lt;br /&gt;
    var lis = Array.from( ul.querySelectorAll(&#039;:scope &amp;gt; li.mw-list-item&#039;) );&lt;br /&gt;
    var current = null;&lt;br /&gt;
&lt;br /&gt;
    lis.forEach(function (li) {&lt;br /&gt;
      var a = li.querySelector(&#039;:scope &amp;gt; a&#039;);&lt;br /&gt;
      var d = extractDirective(a);&lt;br /&gt;
&lt;br /&gt;
      if (d &amp;amp;&amp;amp; d.kind === &#039;group&#039;) {&lt;br /&gt;
        var clickable = hasRealHref(a);&lt;br /&gt;
        li.classList.add(&#039;kr-group&#039;);&lt;br /&gt;
        makeHead(li, clickable, ul, (a &amp;amp;&amp;amp; a.textContent) || d.value || &#039;&#039;);&lt;br /&gt;
        if (!clickable &amp;amp;&amp;amp; a) a.remove(); else if (clickable) stripDirectiveHash(a);&lt;br /&gt;
&lt;br /&gt;
        var sub = document.createElement(&#039;ul&#039;);&lt;br /&gt;
        sub.className = &#039;kr-sub&#039;;&lt;br /&gt;
        li.appendChild(sub);&lt;br /&gt;
        current = sub;&lt;br /&gt;
        return;&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      if (current &amp;amp;&amp;amp; !(d &amp;amp;&amp;amp; (d.kind === &#039;group&#039; || d.kind === &#039;link&#039;))) {&lt;br /&gt;
        current.appendChild(li);&lt;br /&gt;
      }&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Portlets verarbeiten ---------- */&lt;br /&gt;
  function buildPortlets(root) {&lt;br /&gt;
    var lists = root.querySelectorAll(&#039;.vector-menu .vector-menu-content-list&#039;);&lt;br /&gt;
    lists.forEach(function (ul) {&lt;br /&gt;
      if (ul.dataset.krSbDone) return;&lt;br /&gt;
      ul.dataset.krSbDone = &#039;1&#039;;&lt;br /&gt;
&lt;br /&gt;
      var items  = Array.from( ul.querySelectorAll(&#039;:scope &amp;gt; li.mw-list-item&#039;) );&lt;br /&gt;
      var topIdx = items.findIndex(function (li) {&lt;br /&gt;
        var a = li.querySelector(&#039;:scope &amp;gt; a&#039;);&lt;br /&gt;
        var d = extractDirective(a);&lt;br /&gt;
        return d &amp;amp;&amp;amp; d.kind === &#039;link&#039;;&lt;br /&gt;
      });&lt;br /&gt;
&lt;br /&gt;
      if (topIdx &amp;gt;= 0) {&lt;br /&gt;
        var top = items[topIdx];&lt;br /&gt;
        top.classList.add(&#039;kr-top&#039;);&lt;br /&gt;
        makeHead(top, true, ul);&lt;br /&gt;
        stripDirectiveHash( top.querySelector(&#039;:scope &amp;gt; a&#039;) );&lt;br /&gt;
&lt;br /&gt;
        var sub = document.createElement(&#039;ul&#039;);&lt;br /&gt;
        sub.className = &#039;kr-sub&#039;;&lt;br /&gt;
        top.appendChild(sub);&lt;br /&gt;
&lt;br /&gt;
        for (var i = topIdx + 1; i &amp;lt; items.length; i++) sub.appendChild(items[i]);&lt;br /&gt;
        buildGroups(sub);&lt;br /&gt;
      } else {&lt;br /&gt;
        buildGroups(ul);&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      // Direktiven aus hrefs entfernen; Vector-Portlet-Heading ausblenden&lt;br /&gt;
      ul.querySelectorAll(&#039;a[href*=&amp;quot;#link:&amp;quot;], a[href*=&amp;quot;#group:&amp;quot;]&#039;).forEach(stripDirectiveHash);&lt;br /&gt;
      var portlet = ul.closest(&#039;.vector-menu&#039;);&lt;br /&gt;
      var heading = portlet &amp;amp;&amp;amp; portlet.querySelector(&#039;:scope &amp;gt; .vector-menu-heading&#039;);&lt;br /&gt;
      if (heading) heading.remove();&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Bootstrapping ---------- */&lt;br /&gt;
  function processRoot(root) {&lt;br /&gt;
    if (!root || root.dataset.krSbRootDone) return;&lt;br /&gt;
    root.dataset.krSbRootDone = &#039;1&#039;;&lt;br /&gt;
    root.classList.add(&#039;kr-sb&#039;);&lt;br /&gt;
    buildPortlets(root);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function init() {&lt;br /&gt;
    document.querySelectorAll(&#039;#mw-panel, #vector-main-menu, #vector-main-menu-pinned, .vector-drawer&#039;)&lt;br /&gt;
      .forEach(processRoot);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  init();&lt;br /&gt;
  mw.hook(&#039;wikipage.content&#039;).add(init);&lt;br /&gt;
&lt;br /&gt;
  // Drawer/DOM-Änderungen beobachten&lt;br /&gt;
  var mo = new MutationObserver(function (muts) {&lt;br /&gt;
    muts.forEach(function (m) {&lt;br /&gt;
      m.addedNodes &amp;amp;&amp;amp; m.addedNodes.forEach(function (n) {&lt;br /&gt;
        if (!(n instanceof Element)) return;&lt;br /&gt;
        if (n.matches(&#039;#vector-main-menu, #vector-main-menu-pinned, .vector-drawer, #mw-panel&#039;)) processRoot(n);&lt;br /&gt;
        n.querySelectorAll &amp;amp;&amp;amp; n.querySelectorAll(&#039;#vector-main-menu, #vector-main-menu-pinned, .vector-drawer, #mw-panel&#039;)&lt;br /&gt;
          .forEach(processRoot);&lt;br /&gt;
      });&lt;br /&gt;
    });&lt;br /&gt;
  });&lt;br /&gt;
  mo.observe(document.body, { childList: true, subtree: true });&lt;br /&gt;
})();&lt;br /&gt;
&lt;br /&gt;
/* TabberNeue: Anker-Scroll unter Vector-Header + Freeze bei Transclusion */&lt;br /&gt;
(function () {&lt;br /&gt;
  // reale Sticky-Höhe ermitteln (+0.5rem Luft)&lt;br /&gt;
  function stickyOffsetPx() {&lt;br /&gt;
    var hdr = document.querySelector(&#039;.vector-sticky-header-container&#039;);&lt;br /&gt;
    var h = hdr ? Math.round(hdr.getBoundingClientRect().height) : 0;&lt;br /&gt;
    var air = Math.round(parseFloat(getComputedStyle(document.documentElement).fontSize) * 0.5);&lt;br /&gt;
    return h + air;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // oberste Tabber-Leiste für ein Panel finden&lt;br /&gt;
  function headerForPanel(panel) {&lt;br /&gt;
    if (!panel) return null;&lt;br /&gt;
    var top = panel.closest(&#039;.tabber&#039;);&lt;br /&gt;
    for (var cur = top; cur &amp;amp;&amp;amp; cur.parentElement; ) {&lt;br /&gt;
      var up = cur.parentElement.closest(&#039;.tabber&#039;);&lt;br /&gt;
      if (!up) break;&lt;br /&gt;
      top = up; cur = up;&lt;br /&gt;
    }&lt;br /&gt;
    return top ? top.querySelector(&#039;:scope &amp;gt; .tabber__header&#039;) : null;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // Tabber für die Dauer des Umschaltens/Ladens “einfrieren”&lt;br /&gt;
  function freezeTabber(tabber) {&lt;br /&gt;
    var section = tabber &amp;amp;&amp;amp; tabber.querySelector(&#039;:scope &amp;gt; .tabber__section&#039;);&lt;br /&gt;
    if (!section) return Promise.resolve();&lt;br /&gt;
&lt;br /&gt;
    var h = section.getBoundingClientRect().height;&lt;br /&gt;
    tabber.style.setProperty(&#039;--tabber-freeze-h&#039;, h + &#039;px&#039;);&lt;br /&gt;
    tabber.classList.add(&#039;tabber-is-switching&#039;);&lt;br /&gt;
&lt;br /&gt;
    return new Promise(function (resolve) {&lt;br /&gt;
      var last = h, stable = 0;&lt;br /&gt;
      function tick() {&lt;br /&gt;
        var cur = section.getBoundingClientRect().height;&lt;br /&gt;
        if (Math.abs(cur - last) &amp;lt; 1) stable++; else { stable = 0; last = cur; }&lt;br /&gt;
        if (stable &amp;gt;= 8) { // ~8 Frames stabil&lt;br /&gt;
          tabber.classList.remove(&#039;tabber-is-switching&#039;);&lt;br /&gt;
          tabber.style.removeProperty(&#039;--tabber-freeze-h&#039;);&lt;br /&gt;
          resolve();&lt;br /&gt;
          return;&lt;br /&gt;
        }&lt;br /&gt;
        requestAnimationFrame(tick);&lt;br /&gt;
      }&lt;br /&gt;
      requestAnimationFrame(tick);&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function scrollToTab(id) {&lt;br /&gt;
    var name = (id || &#039;&#039;).replace(/^#/, &#039;&#039;);&lt;br /&gt;
    if (!/^tabber-/.test(name)) return;&lt;br /&gt;
&lt;br /&gt;
    var panel  = document.getElementById(name);&lt;br /&gt;
    var tabber = panel &amp;amp;&amp;amp; panel.closest(&#039;.tabber&#039;);&lt;br /&gt;
    var header = panel &amp;amp;&amp;amp; headerForPanel(panel);&lt;br /&gt;
    if (!panel) return;&lt;br /&gt;
&lt;br /&gt;
    var doFreeze = tabber ? freezeTabber(tabber) : Promise.resolve();&lt;br /&gt;
&lt;br /&gt;
    var y1 = (header || panel).getBoundingClientRect().top + window.scrollY - stickyOffsetPx();&lt;br /&gt;
    /* 1. Sprung: sofort, ohne Smooth (verhindert sichtbares „Zurückfedern“) */&lt;br /&gt;
    window.scrollTo({ top: y1 });&lt;br /&gt;
&lt;br /&gt;
    doFreeze.then(function () {&lt;br /&gt;
      var y2 = (header || panel).getBoundingClientRect().top + window.scrollY - stickyOffsetPx();&lt;br /&gt;
      /* 2. Feinkorrektur: Smooth, wenn sich was verändert hat */&lt;br /&gt;
      if (Math.abs(y2 - y1) &amp;gt; 1) {&lt;br /&gt;
        window.scrollTo({ top: y2, behavior: &#039;smooth&#039; });&lt;br /&gt;
      }&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // 1) Direkt mit Hash geladen&lt;br /&gt;
  if (/^#tabber-/.test(location.hash)) {&lt;br /&gt;
    setTimeout(function () { scrollToTab(location.hash); }, 60);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // 2) Hashwechsel&lt;br /&gt;
  window.addEventListener(&#039;hashchange&#039;, function () {&lt;br /&gt;
    if (/^#tabber-/.test(location.hash)) scrollToTab(location.hash);&lt;br /&gt;
  });&lt;br /&gt;
&lt;br /&gt;
  // 3) Klicks (TabberNeue verhindert default / nutzt History API)&lt;br /&gt;
  document.addEventListener(&#039;click&#039;, function (e) {&lt;br /&gt;
    var a = e.target &amp;amp;&amp;amp; e.target.closest(&#039;a[href^=&amp;quot;#tabber-&amp;quot;], button[aria-controls^=&amp;quot;tabber-&amp;quot;]&#039;);&lt;br /&gt;
    if (!a) return;&lt;br /&gt;
    var id = a.getAttribute(&#039;href&#039;) || (&#039;#&#039; + a.getAttribute(&#039;aria-controls&#039;));&lt;br /&gt;
    // ganz kurz warten, bis TabberNeue den Wechsel gestartet hat&lt;br /&gt;
    setTimeout(function () { scrollToTab(id); }, 60);&lt;br /&gt;
  }, true);&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Webmaster</name></author>
	</entry>
	<entry>
		<id>https://firestone-idle-rpg.de/index.php?title=MediaWiki:Common.js&amp;diff=3771</id>
		<title>MediaWiki:Common.js</title>
		<link rel="alternate" type="text/html" href="https://firestone-idle-rpg.de/index.php?title=MediaWiki:Common.js&amp;diff=3771"/>
		<updated>2025-10-29T10:08:18Z</updated>

		<summary type="html">&lt;p&gt;Webmaster: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* Das folgende JavaScript wird für alle Benutzer geladen. */&lt;br /&gt;
/* ==== Sidebar: klickbare Kopfzeile + Gruppen mit Auf/Zu + Persistenz (Vector-2022 &amp;amp; Drawer) ==== */&lt;br /&gt;
(function () {&lt;br /&gt;
  &#039;use strict&#039;;&lt;br /&gt;
  if ( mw.config.get(&#039;skin&#039;) !== &#039;vector-2022&#039; ) return;&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Persistenz ---------- */&lt;br /&gt;
  var STORAGE_KEY = &#039;kr-sb-state:v3:&#039; + ( mw.config.get(&#039;wgUserName&#039;) || &#039;anon&#039; );&lt;br /&gt;
  function loadState() {&lt;br /&gt;
    try { return JSON.parse( localStorage.getItem(STORAGE_KEY) || &#039;{}&#039; ); }&lt;br /&gt;
    catch (e) { return {}; }&lt;br /&gt;
  }&lt;br /&gt;
  function saveState(state) {&lt;br /&gt;
    try { localStorage.setItem( STORAGE_KEY, JSON.stringify(state) ); }&lt;br /&gt;
    catch (e) {}&lt;br /&gt;
  }&lt;br /&gt;
  var STATE = loadState();&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Utils ---------- */&lt;br /&gt;
  function extractDirective(a) {&lt;br /&gt;
    if (!a) return null;&lt;br /&gt;
    var href = a.getAttribute(&#039;href&#039;) || &#039;&#039;;&lt;br /&gt;
    try { href = new URL(href, location.href).hash || &#039;&#039;; } catch (e) {}&lt;br /&gt;
    var s = (href || &#039;&#039;).trim().toLowerCase();&lt;br /&gt;
    if (s.includes(&#039;#group:&#039;)) return { kind: &#039;group&#039;, value: decodeURIComponent(s.split(&#039;#group:&#039;).pop()) };&lt;br /&gt;
    if (s.includes(&#039;#link:&#039;))  return { kind: &#039;link&#039;,  value: decodeURIComponent(s.split(&#039;#link:&#039;).pop())  };&lt;br /&gt;
    return null;&lt;br /&gt;
  }&lt;br /&gt;
  function stripDirectiveHash(a) {&lt;br /&gt;
    if (!a) return;&lt;br /&gt;
    a.setAttribute(&#039;href&#039;, (a.getAttribute(&#039;href&#039;) || &#039;&#039;).replace(/#(?:link|group):[^#]*$/i, &#039;&#039;) );&lt;br /&gt;
  }&lt;br /&gt;
  function hasRealHref(a) {&lt;br /&gt;
    if (!a) return false;&lt;br /&gt;
    return !/^#/.test( a.getAttribute(&#039;href&#039;) || &#039;&#039; );&lt;br /&gt;
  }&lt;br /&gt;
  function normId(s) {&lt;br /&gt;
    return String(s || &#039;&#039;).trim().toLowerCase()&lt;br /&gt;
      .replace(/\s+/g, &#039;-&#039;).replace(/[^\w\-:.|]/g, &#039;&#039;);&lt;br /&gt;
  }&lt;br /&gt;
  function toggleSet(li, arrow, expanded) {&lt;br /&gt;
    if (expanded) { li.classList.remove(&#039;is-collapsed&#039;); arrow.setAttribute(&#039;aria-expanded&#039;,&#039;true&#039;); }&lt;br /&gt;
    else          { li.classList.add(&#039;is-collapsed&#039;);    arrow.setAttribute(&#039;aria-expanded&#039;,&#039;false&#039;); }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Kopf + ID ---------- */&lt;br /&gt;
  function makeHead(li, keepLink, containerUL, labelOverride) {&lt;br /&gt;
    var a = li.querySelector(&#039;:scope &amp;gt; a&#039;);&lt;br /&gt;
&lt;br /&gt;
    var head = document.createElement(&#039;div&#039;);&lt;br /&gt;
    head.className = &#039;kr-head has-toggle&#039;; // ganze Kopfzeile darf toggeln (außer Link)&lt;br /&gt;
&lt;br /&gt;
    var btn = document.createElement(&#039;button&#039;);&lt;br /&gt;
    btn.type = &#039;button&#039;;&lt;br /&gt;
    btn.className = &#039;kr-arrow&#039;;&lt;br /&gt;
    btn.setAttribute(&#039;aria-expanded&#039;, &#039;false&#039;);&lt;br /&gt;
    head.appendChild(btn);&lt;br /&gt;
&lt;br /&gt;
    if (keepLink &amp;amp;&amp;amp; a) { stripDirectiveHash(a); head.appendChild(a); }&lt;br /&gt;
    else {&lt;br /&gt;
      var span = document.createElement(&#039;span&#039;);&lt;br /&gt;
      span.className = &#039;kr-title&#039;;&lt;br /&gt;
      span.textContent = labelOverride || (a &amp;amp;&amp;amp; a.textContent) || &#039;&#039;;&lt;br /&gt;
      head.appendChild(span);&lt;br /&gt;
      if (a) a.remove();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    li.insertBefore(head, li.firstChild);&lt;br /&gt;
&lt;br /&gt;
    // stabile ID für Persistenz&lt;br /&gt;
    var portlet = containerUL.closest(&#039;.vector-menu&#039;);&lt;br /&gt;
    var pid = (portlet &amp;amp;&amp;amp; (portlet.id || portlet.getAttribute(&#039;id&#039;))) || &#039;portlet&#039;;&lt;br /&gt;
    var labelText = (labelOverride || (head.querySelector(&#039;a, .kr-title&#039;) || {}).textContent || &#039;&#039;).trim();&lt;br /&gt;
    li.dataset.krId = normId( pid + &#039;::&#039; + labelText );&lt;br /&gt;
&lt;br /&gt;
    // Default zu; gespeicherten Zustand anwenden&lt;br /&gt;
    toggleSet(li, btn, false);&lt;br /&gt;
    var st = STATE[ li.dataset.krId ];&lt;br /&gt;
    if (st === 1) toggleSet(li, btn, true);&lt;br /&gt;
&lt;br /&gt;
    return head;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Toggle-Handler ---------- */&lt;br /&gt;
  function handleArrowClick(btn) {&lt;br /&gt;
    var li = btn.closest(&#039;li.kr-group, li.kr-top&#039;);&lt;br /&gt;
    if (!li) return;&lt;br /&gt;
&lt;br /&gt;
    var willExpand = li.classList.contains(&#039;is-collapsed&#039;);&lt;br /&gt;
    toggleSet(li, btn, willExpand);&lt;br /&gt;
&lt;br /&gt;
    var id = li.dataset.krId;&lt;br /&gt;
    if (id) {&lt;br /&gt;
      STATE[id] = willExpand ? 1 : 0;&lt;br /&gt;
      saveState(STATE);&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* Globale Delegation: Pfeil klickt immer; Kopfzeile klickt nur, wenn NICHT im Link */&lt;br /&gt;
  document.addEventListener(&#039;click&#039;, function (e) {&lt;br /&gt;
    var arrow = e.target &amp;amp;&amp;amp; e.target.closest &amp;amp;&amp;amp; e.target.closest(&#039;.kr-arrow&#039;);&lt;br /&gt;
    if (arrow) {&lt;br /&gt;
      e.preventDefault();&lt;br /&gt;
      handleArrowClick(arrow);&lt;br /&gt;
      return;&lt;br /&gt;
    }&lt;br /&gt;
    var head = e.target &amp;amp;&amp;amp; e.target.closest &amp;amp;&amp;amp; e.target.closest(&#039;.kr-head&#039;);&lt;br /&gt;
    if (head &amp;amp;&amp;amp; !e.target.closest(&#039;.kr-head a&#039;)) {&lt;br /&gt;
      e.preventDefault(); // freie Fläche der Kopfzeile&lt;br /&gt;
      var btn = head.querySelector(&#039;.kr-arrow&#039;);&lt;br /&gt;
      if (btn) handleArrowClick(btn);&lt;br /&gt;
    }&lt;br /&gt;
  }, true); // capture=true, damit nichts dazwischenfunkt&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Aufbau der Gruppen ---------- */&lt;br /&gt;
  function buildGroups(ul) {&lt;br /&gt;
    var lis = Array.from( ul.querySelectorAll(&#039;:scope &amp;gt; li.mw-list-item&#039;) );&lt;br /&gt;
    var current = null;&lt;br /&gt;
&lt;br /&gt;
    lis.forEach(function (li) {&lt;br /&gt;
      var a = li.querySelector(&#039;:scope &amp;gt; a&#039;);&lt;br /&gt;
      var d = extractDirective(a);&lt;br /&gt;
&lt;br /&gt;
      if (d &amp;amp;&amp;amp; d.kind === &#039;group&#039;) {&lt;br /&gt;
        var clickable = hasRealHref(a);&lt;br /&gt;
        li.classList.add(&#039;kr-group&#039;);&lt;br /&gt;
        makeHead(li, clickable, ul, (a &amp;amp;&amp;amp; a.textContent) || d.value || &#039;&#039;);&lt;br /&gt;
        if (!clickable &amp;amp;&amp;amp; a) a.remove(); else if (clickable) stripDirectiveHash(a);&lt;br /&gt;
&lt;br /&gt;
        var sub = document.createElement(&#039;ul&#039;);&lt;br /&gt;
        sub.className = &#039;kr-sub&#039;;&lt;br /&gt;
        li.appendChild(sub);&lt;br /&gt;
        current = sub;&lt;br /&gt;
        return;&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      if (current &amp;amp;&amp;amp; !(d &amp;amp;&amp;amp; (d.kind === &#039;group&#039; || d.kind === &#039;link&#039;))) {&lt;br /&gt;
        current.appendChild(li);&lt;br /&gt;
      }&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Portlets verarbeiten ---------- */&lt;br /&gt;
  function buildPortlets(root) {&lt;br /&gt;
    var lists = root.querySelectorAll(&#039;.vector-menu .vector-menu-content-list&#039;);&lt;br /&gt;
    lists.forEach(function (ul) {&lt;br /&gt;
      if (ul.dataset.krSbDone) return;&lt;br /&gt;
      ul.dataset.krSbDone = &#039;1&#039;;&lt;br /&gt;
&lt;br /&gt;
      var items  = Array.from( ul.querySelectorAll(&#039;:scope &amp;gt; li.mw-list-item&#039;) );&lt;br /&gt;
      var topIdx = items.findIndex(function (li) {&lt;br /&gt;
        var a = li.querySelector(&#039;:scope &amp;gt; a&#039;);&lt;br /&gt;
        var d = extractDirective(a);&lt;br /&gt;
        return d &amp;amp;&amp;amp; d.kind === &#039;link&#039;;&lt;br /&gt;
      });&lt;br /&gt;
&lt;br /&gt;
      if (topIdx &amp;gt;= 0) {&lt;br /&gt;
        var top = items[topIdx];&lt;br /&gt;
        top.classList.add(&#039;kr-top&#039;);&lt;br /&gt;
        makeHead(top, true, ul);&lt;br /&gt;
        stripDirectiveHash( top.querySelector(&#039;:scope &amp;gt; a&#039;) );&lt;br /&gt;
&lt;br /&gt;
        var sub = document.createElement(&#039;ul&#039;);&lt;br /&gt;
        sub.className = &#039;kr-sub&#039;;&lt;br /&gt;
        top.appendChild(sub);&lt;br /&gt;
&lt;br /&gt;
        for (var i = topIdx + 1; i &amp;lt; items.length; i++) sub.appendChild(items[i]);&lt;br /&gt;
        buildGroups(sub);&lt;br /&gt;
      } else {&lt;br /&gt;
        buildGroups(ul);&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      // Direktiven aus hrefs entfernen; Vector-Portlet-Heading ausblenden&lt;br /&gt;
      ul.querySelectorAll(&#039;a[href*=&amp;quot;#link:&amp;quot;], a[href*=&amp;quot;#group:&amp;quot;]&#039;).forEach(stripDirectiveHash);&lt;br /&gt;
      var portlet = ul.closest(&#039;.vector-menu&#039;);&lt;br /&gt;
      var heading = portlet &amp;amp;&amp;amp; portlet.querySelector(&#039;:scope &amp;gt; .vector-menu-heading&#039;);&lt;br /&gt;
      if (heading) heading.remove();&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Bootstrapping ---------- */&lt;br /&gt;
  function processRoot(root) {&lt;br /&gt;
    if (!root || root.dataset.krSbRootDone) return;&lt;br /&gt;
    root.dataset.krSbRootDone = &#039;1&#039;;&lt;br /&gt;
    root.classList.add(&#039;kr-sb&#039;);&lt;br /&gt;
    buildPortlets(root);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function init() {&lt;br /&gt;
    document.querySelectorAll(&#039;#mw-panel, #vector-main-menu, #vector-main-menu-pinned, .vector-drawer&#039;)&lt;br /&gt;
      .forEach(processRoot);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  init();&lt;br /&gt;
  mw.hook(&#039;wikipage.content&#039;).add(init);&lt;br /&gt;
&lt;br /&gt;
  // Drawer/DOM-Änderungen beobachten&lt;br /&gt;
  var mo = new MutationObserver(function (muts) {&lt;br /&gt;
    muts.forEach(function (m) {&lt;br /&gt;
      m.addedNodes &amp;amp;&amp;amp; m.addedNodes.forEach(function (n) {&lt;br /&gt;
        if (!(n instanceof Element)) return;&lt;br /&gt;
        if (n.matches(&#039;#vector-main-menu, #vector-main-menu-pinned, .vector-drawer, #mw-panel&#039;)) processRoot(n);&lt;br /&gt;
        n.querySelectorAll &amp;amp;&amp;amp; n.querySelectorAll(&#039;#vector-main-menu, #vector-main-menu-pinned, .vector-drawer, #mw-panel&#039;)&lt;br /&gt;
          .forEach(processRoot);&lt;br /&gt;
      });&lt;br /&gt;
    });&lt;br /&gt;
  });&lt;br /&gt;
  mo.observe(document.body, { childList: true, subtree: true });&lt;br /&gt;
})();&lt;br /&gt;
&lt;br /&gt;
/* TabberNeue: Anker-Scroll unter Vector-Header + Freeze bei Transclusion */&lt;br /&gt;
(function () {&lt;br /&gt;
  // reale Sticky-Höhe ermitteln (+0.5rem Luft)&lt;br /&gt;
  function stickyOffsetPx() {&lt;br /&gt;
    var hdr = document.querySelector(&#039;.vector-sticky-header-container&#039;);&lt;br /&gt;
    var h = hdr ? Math.round(hdr.getBoundingClientRect().height) : 0;&lt;br /&gt;
    var air = Math.round(parseFloat(getComputedStyle(document.documentElement).fontSize) * 0.5);&lt;br /&gt;
    return h + air;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // oberste Tabber-Leiste für ein Panel finden&lt;br /&gt;
  function headerForPanel(panel) {&lt;br /&gt;
    if (!panel) return null;&lt;br /&gt;
    var top = panel.closest(&#039;.tabber&#039;);&lt;br /&gt;
    for (var cur = top; cur &amp;amp;&amp;amp; cur.parentElement; ) {&lt;br /&gt;
      var up = cur.parentElement.closest(&#039;.tabber&#039;);&lt;br /&gt;
      if (!up) break;&lt;br /&gt;
      top = up; cur = up;&lt;br /&gt;
    }&lt;br /&gt;
    return top ? top.querySelector(&#039;:scope &amp;gt; .tabber__header&#039;) : null;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // Tabber für die Dauer des Umschaltens/Ladens “einfrieren”&lt;br /&gt;
  function freezeTabber(tabber) {&lt;br /&gt;
    var section = tabber &amp;amp;&amp;amp; tabber.querySelector(&#039;:scope &amp;gt; .tabber__section&#039;);&lt;br /&gt;
    if (!section) return Promise.resolve();&lt;br /&gt;
&lt;br /&gt;
    var h = section.getBoundingClientRect().height;&lt;br /&gt;
    tabber.style.setProperty(&#039;--tabber-freeze-h&#039;, h + &#039;px&#039;);&lt;br /&gt;
    tabber.classList.add(&#039;tabber-is-switching&#039;);&lt;br /&gt;
&lt;br /&gt;
    return new Promise(function (resolve) {&lt;br /&gt;
      var last = h, stable = 0;&lt;br /&gt;
      function tick() {&lt;br /&gt;
        var cur = section.getBoundingClientRect().height;&lt;br /&gt;
        if (Math.abs(cur - last) &amp;lt; 1) stable++; else { stable = 0; last = cur; }&lt;br /&gt;
        if (stable &amp;gt;= 8) { // ~8 Frames stabil&lt;br /&gt;
          tabber.classList.remove(&#039;tabber-is-switching&#039;);&lt;br /&gt;
          tabber.style.removeProperty(&#039;--tabber-freeze-h&#039;);&lt;br /&gt;
          resolve();&lt;br /&gt;
          return;&lt;br /&gt;
        }&lt;br /&gt;
        requestAnimationFrame(tick);&lt;br /&gt;
      }&lt;br /&gt;
      requestAnimationFrame(tick);&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function scrollToTab(id) {&lt;br /&gt;
    var name = (id || &#039;&#039;).replace(/^#/, &#039;&#039;);&lt;br /&gt;
    if (!/^tabber-/.test(name)) return;&lt;br /&gt;
&lt;br /&gt;
    var panel  = document.getElementById(name);&lt;br /&gt;
    var tabber = panel &amp;amp;&amp;amp; panel.closest(&#039;.tabber&#039;);&lt;br /&gt;
    var header = panel &amp;amp;&amp;amp; headerForPanel(panel);&lt;br /&gt;
    if (!panel) return;&lt;br /&gt;
&lt;br /&gt;
    // vor dem Scroll einfrieren (verhindert “Hochspringen” während Transclusion)&lt;br /&gt;
    var doFreeze = tabber ? freezeTabber(tabber) : Promise.resolve();&lt;br /&gt;
&lt;br /&gt;
    // erste Position (sofort), dann nach Stabilisierung fein nachjustieren&lt;br /&gt;
    var y1 = (header || panel).getBoundingClientRect().top + window.scrollY - stickyOffsetPx();&lt;br /&gt;
    window.scrollTo({ top: y1, behavior: &#039;smooth&#039; });&lt;br /&gt;
&lt;br /&gt;
    doFreeze.then(function () {&lt;br /&gt;
      var y2 = (header || panel).getBoundingClientRect().top + window.scrollY - stickyOffsetPx();&lt;br /&gt;
      window.scrollTo({ top: y2, behavior: &#039;smooth&#039; });&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // 1) Direkt mit Hash geladen&lt;br /&gt;
  if (/^#tabber-/.test(location.hash)) {&lt;br /&gt;
    setTimeout(function () { scrollToTab(location.hash); }, 60);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // 2) Hashwechsel&lt;br /&gt;
  window.addEventListener(&#039;hashchange&#039;, function () {&lt;br /&gt;
    if (/^#tabber-/.test(location.hash)) scrollToTab(location.hash);&lt;br /&gt;
  });&lt;br /&gt;
&lt;br /&gt;
  // 3) Klicks (TabberNeue verhindert default / nutzt History API)&lt;br /&gt;
  document.addEventListener(&#039;click&#039;, function (e) {&lt;br /&gt;
    var a = e.target &amp;amp;&amp;amp; e.target.closest(&#039;a[href^=&amp;quot;#tabber-&amp;quot;], button[aria-controls^=&amp;quot;tabber-&amp;quot;]&#039;);&lt;br /&gt;
    if (!a) return;&lt;br /&gt;
    var id = a.getAttribute(&#039;href&#039;) || (&#039;#&#039; + a.getAttribute(&#039;aria-controls&#039;));&lt;br /&gt;
    // ganz kurz warten, bis TabberNeue den Wechsel gestartet hat&lt;br /&gt;
    setTimeout(function () { scrollToTab(id); }, 60);&lt;br /&gt;
  }, true);&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Webmaster</name></author>
	</entry>
	<entry>
		<id>https://firestone-idle-rpg.de/index.php?title=MediaWiki:Common.js&amp;diff=3770</id>
		<title>MediaWiki:Common.js</title>
		<link rel="alternate" type="text/html" href="https://firestone-idle-rpg.de/index.php?title=MediaWiki:Common.js&amp;diff=3770"/>
		<updated>2025-10-29T09:31:18Z</updated>

		<summary type="html">&lt;p&gt;Webmaster: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* Das folgende JavaScript wird für alle Benutzer geladen. */&lt;br /&gt;
/* ==== Sidebar: klickbare Kopfzeile + Gruppen mit Auf/Zu + Persistenz (Vector-2022 &amp;amp; Drawer) ==== */&lt;br /&gt;
(function () {&lt;br /&gt;
  &#039;use strict&#039;;&lt;br /&gt;
  if ( mw.config.get(&#039;skin&#039;) !== &#039;vector-2022&#039; ) return;&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Persistenz ---------- */&lt;br /&gt;
  var STORAGE_KEY = &#039;kr-sb-state:v3:&#039; + ( mw.config.get(&#039;wgUserName&#039;) || &#039;anon&#039; );&lt;br /&gt;
  function loadState() {&lt;br /&gt;
    try { return JSON.parse( localStorage.getItem(STORAGE_KEY) || &#039;{}&#039; ); }&lt;br /&gt;
    catch (e) { return {}; }&lt;br /&gt;
  }&lt;br /&gt;
  function saveState(state) {&lt;br /&gt;
    try { localStorage.setItem( STORAGE_KEY, JSON.stringify(state) ); }&lt;br /&gt;
    catch (e) {}&lt;br /&gt;
  }&lt;br /&gt;
  var STATE = loadState();&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Utils ---------- */&lt;br /&gt;
  function extractDirective(a) {&lt;br /&gt;
    if (!a) return null;&lt;br /&gt;
    var href = a.getAttribute(&#039;href&#039;) || &#039;&#039;;&lt;br /&gt;
    try { href = new URL(href, location.href).hash || &#039;&#039;; } catch (e) {}&lt;br /&gt;
    var s = (href || &#039;&#039;).trim().toLowerCase();&lt;br /&gt;
    if (s.includes(&#039;#group:&#039;)) return { kind: &#039;group&#039;, value: decodeURIComponent(s.split(&#039;#group:&#039;).pop()) };&lt;br /&gt;
    if (s.includes(&#039;#link:&#039;))  return { kind: &#039;link&#039;,  value: decodeURIComponent(s.split(&#039;#link:&#039;).pop())  };&lt;br /&gt;
    return null;&lt;br /&gt;
  }&lt;br /&gt;
  function stripDirectiveHash(a) {&lt;br /&gt;
    if (!a) return;&lt;br /&gt;
    a.setAttribute(&#039;href&#039;, (a.getAttribute(&#039;href&#039;) || &#039;&#039;).replace(/#(?:link|group):[^#]*$/i, &#039;&#039;) );&lt;br /&gt;
  }&lt;br /&gt;
  function hasRealHref(a) {&lt;br /&gt;
    if (!a) return false;&lt;br /&gt;
    return !/^#/.test( a.getAttribute(&#039;href&#039;) || &#039;&#039; );&lt;br /&gt;
  }&lt;br /&gt;
  function normId(s) {&lt;br /&gt;
    return String(s || &#039;&#039;).trim().toLowerCase()&lt;br /&gt;
      .replace(/\s+/g, &#039;-&#039;).replace(/[^\w\-:.|]/g, &#039;&#039;);&lt;br /&gt;
  }&lt;br /&gt;
  function toggleSet(li, arrow, expanded) {&lt;br /&gt;
    if (expanded) { li.classList.remove(&#039;is-collapsed&#039;); arrow.setAttribute(&#039;aria-expanded&#039;,&#039;true&#039;); }&lt;br /&gt;
    else          { li.classList.add(&#039;is-collapsed&#039;);    arrow.setAttribute(&#039;aria-expanded&#039;,&#039;false&#039;); }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Kopf + ID ---------- */&lt;br /&gt;
  function makeHead(li, keepLink, containerUL, labelOverride) {&lt;br /&gt;
    var a = li.querySelector(&#039;:scope &amp;gt; a&#039;);&lt;br /&gt;
&lt;br /&gt;
    var head = document.createElement(&#039;div&#039;);&lt;br /&gt;
    head.className = &#039;kr-head has-toggle&#039;; // ganze Kopfzeile darf toggeln (außer Link)&lt;br /&gt;
&lt;br /&gt;
    var btn = document.createElement(&#039;button&#039;);&lt;br /&gt;
    btn.type = &#039;button&#039;;&lt;br /&gt;
    btn.className = &#039;kr-arrow&#039;;&lt;br /&gt;
    btn.setAttribute(&#039;aria-expanded&#039;, &#039;false&#039;);&lt;br /&gt;
    head.appendChild(btn);&lt;br /&gt;
&lt;br /&gt;
    if (keepLink &amp;amp;&amp;amp; a) { stripDirectiveHash(a); head.appendChild(a); }&lt;br /&gt;
    else {&lt;br /&gt;
      var span = document.createElement(&#039;span&#039;);&lt;br /&gt;
      span.className = &#039;kr-title&#039;;&lt;br /&gt;
      span.textContent = labelOverride || (a &amp;amp;&amp;amp; a.textContent) || &#039;&#039;;&lt;br /&gt;
      head.appendChild(span);&lt;br /&gt;
      if (a) a.remove();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    li.insertBefore(head, li.firstChild);&lt;br /&gt;
&lt;br /&gt;
    // stabile ID für Persistenz&lt;br /&gt;
    var portlet = containerUL.closest(&#039;.vector-menu&#039;);&lt;br /&gt;
    var pid = (portlet &amp;amp;&amp;amp; (portlet.id || portlet.getAttribute(&#039;id&#039;))) || &#039;portlet&#039;;&lt;br /&gt;
    var labelText = (labelOverride || (head.querySelector(&#039;a, .kr-title&#039;) || {}).textContent || &#039;&#039;).trim();&lt;br /&gt;
    li.dataset.krId = normId( pid + &#039;::&#039; + labelText );&lt;br /&gt;
&lt;br /&gt;
    // Default zu; gespeicherten Zustand anwenden&lt;br /&gt;
    toggleSet(li, btn, false);&lt;br /&gt;
    var st = STATE[ li.dataset.krId ];&lt;br /&gt;
    if (st === 1) toggleSet(li, btn, true);&lt;br /&gt;
&lt;br /&gt;
    return head;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Toggle-Handler ---------- */&lt;br /&gt;
  function handleArrowClick(btn) {&lt;br /&gt;
    var li = btn.closest(&#039;li.kr-group, li.kr-top&#039;);&lt;br /&gt;
    if (!li) return;&lt;br /&gt;
&lt;br /&gt;
    var willExpand = li.classList.contains(&#039;is-collapsed&#039;);&lt;br /&gt;
    toggleSet(li, btn, willExpand);&lt;br /&gt;
&lt;br /&gt;
    var id = li.dataset.krId;&lt;br /&gt;
    if (id) {&lt;br /&gt;
      STATE[id] = willExpand ? 1 : 0;&lt;br /&gt;
      saveState(STATE);&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* Globale Delegation: Pfeil klickt immer; Kopfzeile klickt nur, wenn NICHT im Link */&lt;br /&gt;
  document.addEventListener(&#039;click&#039;, function (e) {&lt;br /&gt;
    var arrow = e.target &amp;amp;&amp;amp; e.target.closest &amp;amp;&amp;amp; e.target.closest(&#039;.kr-arrow&#039;);&lt;br /&gt;
    if (arrow) {&lt;br /&gt;
      e.preventDefault();&lt;br /&gt;
      handleArrowClick(arrow);&lt;br /&gt;
      return;&lt;br /&gt;
    }&lt;br /&gt;
    var head = e.target &amp;amp;&amp;amp; e.target.closest &amp;amp;&amp;amp; e.target.closest(&#039;.kr-head&#039;);&lt;br /&gt;
    if (head &amp;amp;&amp;amp; !e.target.closest(&#039;.kr-head a&#039;)) {&lt;br /&gt;
      e.preventDefault(); // freie Fläche der Kopfzeile&lt;br /&gt;
      var btn = head.querySelector(&#039;.kr-arrow&#039;);&lt;br /&gt;
      if (btn) handleArrowClick(btn);&lt;br /&gt;
    }&lt;br /&gt;
  }, true); // capture=true, damit nichts dazwischenfunkt&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Aufbau der Gruppen ---------- */&lt;br /&gt;
  function buildGroups(ul) {&lt;br /&gt;
    var lis = Array.from( ul.querySelectorAll(&#039;:scope &amp;gt; li.mw-list-item&#039;) );&lt;br /&gt;
    var current = null;&lt;br /&gt;
&lt;br /&gt;
    lis.forEach(function (li) {&lt;br /&gt;
      var a = li.querySelector(&#039;:scope &amp;gt; a&#039;);&lt;br /&gt;
      var d = extractDirective(a);&lt;br /&gt;
&lt;br /&gt;
      if (d &amp;amp;&amp;amp; d.kind === &#039;group&#039;) {&lt;br /&gt;
        var clickable = hasRealHref(a);&lt;br /&gt;
        li.classList.add(&#039;kr-group&#039;);&lt;br /&gt;
        makeHead(li, clickable, ul, (a &amp;amp;&amp;amp; a.textContent) || d.value || &#039;&#039;);&lt;br /&gt;
        if (!clickable &amp;amp;&amp;amp; a) a.remove(); else if (clickable) stripDirectiveHash(a);&lt;br /&gt;
&lt;br /&gt;
        var sub = document.createElement(&#039;ul&#039;);&lt;br /&gt;
        sub.className = &#039;kr-sub&#039;;&lt;br /&gt;
        li.appendChild(sub);&lt;br /&gt;
        current = sub;&lt;br /&gt;
        return;&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      if (current &amp;amp;&amp;amp; !(d &amp;amp;&amp;amp; (d.kind === &#039;group&#039; || d.kind === &#039;link&#039;))) {&lt;br /&gt;
        current.appendChild(li);&lt;br /&gt;
      }&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Portlets verarbeiten ---------- */&lt;br /&gt;
  function buildPortlets(root) {&lt;br /&gt;
    var lists = root.querySelectorAll(&#039;.vector-menu .vector-menu-content-list&#039;);&lt;br /&gt;
    lists.forEach(function (ul) {&lt;br /&gt;
      if (ul.dataset.krSbDone) return;&lt;br /&gt;
      ul.dataset.krSbDone = &#039;1&#039;;&lt;br /&gt;
&lt;br /&gt;
      var items  = Array.from( ul.querySelectorAll(&#039;:scope &amp;gt; li.mw-list-item&#039;) );&lt;br /&gt;
      var topIdx = items.findIndex(function (li) {&lt;br /&gt;
        var a = li.querySelector(&#039;:scope &amp;gt; a&#039;);&lt;br /&gt;
        var d = extractDirective(a);&lt;br /&gt;
        return d &amp;amp;&amp;amp; d.kind === &#039;link&#039;;&lt;br /&gt;
      });&lt;br /&gt;
&lt;br /&gt;
      if (topIdx &amp;gt;= 0) {&lt;br /&gt;
        var top = items[topIdx];&lt;br /&gt;
        top.classList.add(&#039;kr-top&#039;);&lt;br /&gt;
        makeHead(top, true, ul);&lt;br /&gt;
        stripDirectiveHash( top.querySelector(&#039;:scope &amp;gt; a&#039;) );&lt;br /&gt;
&lt;br /&gt;
        var sub = document.createElement(&#039;ul&#039;);&lt;br /&gt;
        sub.className = &#039;kr-sub&#039;;&lt;br /&gt;
        top.appendChild(sub);&lt;br /&gt;
&lt;br /&gt;
        for (var i = topIdx + 1; i &amp;lt; items.length; i++) sub.appendChild(items[i]);&lt;br /&gt;
        buildGroups(sub);&lt;br /&gt;
      } else {&lt;br /&gt;
        buildGroups(ul);&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      // Direktiven aus hrefs entfernen; Vector-Portlet-Heading ausblenden&lt;br /&gt;
      ul.querySelectorAll(&#039;a[href*=&amp;quot;#link:&amp;quot;], a[href*=&amp;quot;#group:&amp;quot;]&#039;).forEach(stripDirectiveHash);&lt;br /&gt;
      var portlet = ul.closest(&#039;.vector-menu&#039;);&lt;br /&gt;
      var heading = portlet &amp;amp;&amp;amp; portlet.querySelector(&#039;:scope &amp;gt; .vector-menu-heading&#039;);&lt;br /&gt;
      if (heading) heading.remove();&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Bootstrapping ---------- */&lt;br /&gt;
  function processRoot(root) {&lt;br /&gt;
    if (!root || root.dataset.krSbRootDone) return;&lt;br /&gt;
    root.dataset.krSbRootDone = &#039;1&#039;;&lt;br /&gt;
    root.classList.add(&#039;kr-sb&#039;);&lt;br /&gt;
    buildPortlets(root);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function init() {&lt;br /&gt;
    document.querySelectorAll(&#039;#mw-panel, #vector-main-menu, #vector-main-menu-pinned, .vector-drawer&#039;)&lt;br /&gt;
      .forEach(processRoot);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  init();&lt;br /&gt;
  mw.hook(&#039;wikipage.content&#039;).add(init);&lt;br /&gt;
&lt;br /&gt;
  // Drawer/DOM-Änderungen beobachten&lt;br /&gt;
  var mo = new MutationObserver(function (muts) {&lt;br /&gt;
    muts.forEach(function (m) {&lt;br /&gt;
      m.addedNodes &amp;amp;&amp;amp; m.addedNodes.forEach(function (n) {&lt;br /&gt;
        if (!(n instanceof Element)) return;&lt;br /&gt;
        if (n.matches(&#039;#vector-main-menu, #vector-main-menu-pinned, .vector-drawer, #mw-panel&#039;)) processRoot(n);&lt;br /&gt;
        n.querySelectorAll &amp;amp;&amp;amp; n.querySelectorAll(&#039;#vector-main-menu, #vector-main-menu-pinned, .vector-drawer, #mw-panel&#039;)&lt;br /&gt;
          .forEach(processRoot);&lt;br /&gt;
      });&lt;br /&gt;
    });&lt;br /&gt;
  });&lt;br /&gt;
  mo.observe(document.body, { childList: true, subtree: true });&lt;br /&gt;
})();&lt;br /&gt;
&lt;br /&gt;
/* TabberNeue: Anker-Sprung unter Vector-2022 Sticky-Header reparieren */&lt;br /&gt;
(function () {&lt;br /&gt;
  &#039;use strict&#039;;&lt;br /&gt;
  if (mw.config.get(&#039;skin&#039;) !== &#039;vector-2022&#039;) return;&lt;br /&gt;
&lt;br /&gt;
  function stickyOffsetPx() {&lt;br /&gt;
    var cs = getComputedStyle(document.documentElement);&lt;br /&gt;
    var sticky = parseFloat(cs.getPropertyValue(&#039;--sticky-offset&#039;)) || 50; // Vector setzt das&lt;br /&gt;
    var air = parseFloat(cs.fontSize) * 0.5; // +0.5rem Luft&lt;br /&gt;
    return Math.round(sticky + air);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function topHeaderForPanel(panel) {&lt;br /&gt;
    if (!panel) return null;&lt;br /&gt;
    var topTabber = panel.closest(&#039;.tabber&#039;);&lt;br /&gt;
    var cur = topTabber;&lt;br /&gt;
    while (cur &amp;amp;&amp;amp; cur.parentElement) {&lt;br /&gt;
      var up = cur.parentElement.closest(&#039;.tabber&#039;);&lt;br /&gt;
      if (!up) break;&lt;br /&gt;
      topTabber = up; cur = up;&lt;br /&gt;
    }&lt;br /&gt;
    return topTabber ? topTabber.querySelector(&#039;:scope &amp;gt; .tabber__header&#039;) : null;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // wartet kurz, bis TabberNeue ARIA/hidden umgeschaltet hat&lt;br /&gt;
  function defer(fn) {&lt;br /&gt;
    requestAnimationFrame(function () {&lt;br /&gt;
      setTimeout(fn, 40);&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function scrollToTab(hashOrId) {&lt;br /&gt;
    var name = String(hashOrId || &#039;&#039;).replace(/^#/, &#039;&#039;);&lt;br /&gt;
    if (!/^tabber-/.test(name)) return;&lt;br /&gt;
&lt;br /&gt;
    var panel = document.getElementById(name);&lt;br /&gt;
    if (!panel) return;&lt;br /&gt;
&lt;br /&gt;
    var target = topHeaderForPanel(panel) || panel;&lt;br /&gt;
    var y = target.getBoundingClientRect().top + window.scrollY - stickyOffsetPx();&lt;br /&gt;
    window.scrollTo({ top: y, behavior: &#039;smooth&#039; });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function setup() {&lt;br /&gt;
    // Direktaufruf mit Hash (Reload / externer Link)&lt;br /&gt;
    if (/^#tabber-/.test(location.hash)) {&lt;br /&gt;
      defer(function () { scrollToTab(location.hash); });&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // hashchange (falls irgendwo doch auf Hash umgestellt wird)&lt;br /&gt;
    window.addEventListener(&#039;hashchange&#039;, function () {&lt;br /&gt;
      if (/^#tabber-/.test(location.hash)) defer(function () { scrollToTab(location.hash); });&lt;br /&gt;
    });&lt;br /&gt;
&lt;br /&gt;
    // Tab-Klicks: TabberNeue verhindert default + nutzt replaceState → wir scrollen danach&lt;br /&gt;
    document.addEventListener(&#039;click&#039;, function (e) {&lt;br /&gt;
      var a = e.target &amp;amp;&amp;amp; e.target.closest(&#039;a[href^=&amp;quot;#tabber-&amp;quot;], button[aria-controls^=&amp;quot;tabber-&amp;quot;]&#039;);&lt;br /&gt;
      if (!a) return;&lt;br /&gt;
      var id = a.getAttribute(&#039;href&#039;) || (&#039;#&#039; + a.getAttribute(&#039;aria-controls&#039;));&lt;br /&gt;
      defer(function () { scrollToTab(id); });&lt;br /&gt;
    }, true);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // auch bei Ajax-Navigationswechseln sicher initialisieren&lt;br /&gt;
  mw.hook(&#039;wikipage.content&#039;).add(setup);&lt;br /&gt;
  // und einmal sofort (Initialaufruf)&lt;br /&gt;
  setup();&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Webmaster</name></author>
	</entry>
	<entry>
		<id>https://firestone-idle-rpg.de/index.php?title=MediaWiki:Common.js&amp;diff=3769</id>
		<title>MediaWiki:Common.js</title>
		<link rel="alternate" type="text/html" href="https://firestone-idle-rpg.de/index.php?title=MediaWiki:Common.js&amp;diff=3769"/>
		<updated>2025-10-29T09:17:04Z</updated>

		<summary type="html">&lt;p&gt;Webmaster: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* Das folgende JavaScript wird für alle Benutzer geladen. */&lt;br /&gt;
/* ==== Sidebar: klickbare Kopfzeile + Gruppen mit Auf/Zu + Persistenz (Vector-2022 &amp;amp; Drawer) ==== */&lt;br /&gt;
(function () {&lt;br /&gt;
  &#039;use strict&#039;;&lt;br /&gt;
  if ( mw.config.get(&#039;skin&#039;) !== &#039;vector-2022&#039; ) return;&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Persistenz ---------- */&lt;br /&gt;
  var STORAGE_KEY = &#039;kr-sb-state:v3:&#039; + ( mw.config.get(&#039;wgUserName&#039;) || &#039;anon&#039; );&lt;br /&gt;
  function loadState() {&lt;br /&gt;
    try { return JSON.parse( localStorage.getItem(STORAGE_KEY) || &#039;{}&#039; ); }&lt;br /&gt;
    catch (e) { return {}; }&lt;br /&gt;
  }&lt;br /&gt;
  function saveState(state) {&lt;br /&gt;
    try { localStorage.setItem( STORAGE_KEY, JSON.stringify(state) ); }&lt;br /&gt;
    catch (e) {}&lt;br /&gt;
  }&lt;br /&gt;
  var STATE = loadState();&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Utils ---------- */&lt;br /&gt;
  function extractDirective(a) {&lt;br /&gt;
    if (!a) return null;&lt;br /&gt;
    var href = a.getAttribute(&#039;href&#039;) || &#039;&#039;;&lt;br /&gt;
    try { href = new URL(href, location.href).hash || &#039;&#039;; } catch (e) {}&lt;br /&gt;
    var s = (href || &#039;&#039;).trim().toLowerCase();&lt;br /&gt;
    if (s.includes(&#039;#group:&#039;)) return { kind: &#039;group&#039;, value: decodeURIComponent(s.split(&#039;#group:&#039;).pop()) };&lt;br /&gt;
    if (s.includes(&#039;#link:&#039;))  return { kind: &#039;link&#039;,  value: decodeURIComponent(s.split(&#039;#link:&#039;).pop())  };&lt;br /&gt;
    return null;&lt;br /&gt;
  }&lt;br /&gt;
  function stripDirectiveHash(a) {&lt;br /&gt;
    if (!a) return;&lt;br /&gt;
    a.setAttribute(&#039;href&#039;, (a.getAttribute(&#039;href&#039;) || &#039;&#039;).replace(/#(?:link|group):[^#]*$/i, &#039;&#039;) );&lt;br /&gt;
  }&lt;br /&gt;
  function hasRealHref(a) {&lt;br /&gt;
    if (!a) return false;&lt;br /&gt;
    return !/^#/.test( a.getAttribute(&#039;href&#039;) || &#039;&#039; );&lt;br /&gt;
  }&lt;br /&gt;
  function normId(s) {&lt;br /&gt;
    return String(s || &#039;&#039;).trim().toLowerCase()&lt;br /&gt;
      .replace(/\s+/g, &#039;-&#039;).replace(/[^\w\-:.|]/g, &#039;&#039;);&lt;br /&gt;
  }&lt;br /&gt;
  function toggleSet(li, arrow, expanded) {&lt;br /&gt;
    if (expanded) { li.classList.remove(&#039;is-collapsed&#039;); arrow.setAttribute(&#039;aria-expanded&#039;,&#039;true&#039;); }&lt;br /&gt;
    else          { li.classList.add(&#039;is-collapsed&#039;);    arrow.setAttribute(&#039;aria-expanded&#039;,&#039;false&#039;); }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Kopf + ID ---------- */&lt;br /&gt;
  function makeHead(li, keepLink, containerUL, labelOverride) {&lt;br /&gt;
    var a = li.querySelector(&#039;:scope &amp;gt; a&#039;);&lt;br /&gt;
&lt;br /&gt;
    var head = document.createElement(&#039;div&#039;);&lt;br /&gt;
    head.className = &#039;kr-head has-toggle&#039;; // ganze Kopfzeile darf toggeln (außer Link)&lt;br /&gt;
&lt;br /&gt;
    var btn = document.createElement(&#039;button&#039;);&lt;br /&gt;
    btn.type = &#039;button&#039;;&lt;br /&gt;
    btn.className = &#039;kr-arrow&#039;;&lt;br /&gt;
    btn.setAttribute(&#039;aria-expanded&#039;, &#039;false&#039;);&lt;br /&gt;
    head.appendChild(btn);&lt;br /&gt;
&lt;br /&gt;
    if (keepLink &amp;amp;&amp;amp; a) { stripDirectiveHash(a); head.appendChild(a); }&lt;br /&gt;
    else {&lt;br /&gt;
      var span = document.createElement(&#039;span&#039;);&lt;br /&gt;
      span.className = &#039;kr-title&#039;;&lt;br /&gt;
      span.textContent = labelOverride || (a &amp;amp;&amp;amp; a.textContent) || &#039;&#039;;&lt;br /&gt;
      head.appendChild(span);&lt;br /&gt;
      if (a) a.remove();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    li.insertBefore(head, li.firstChild);&lt;br /&gt;
&lt;br /&gt;
    // stabile ID für Persistenz&lt;br /&gt;
    var portlet = containerUL.closest(&#039;.vector-menu&#039;);&lt;br /&gt;
    var pid = (portlet &amp;amp;&amp;amp; (portlet.id || portlet.getAttribute(&#039;id&#039;))) || &#039;portlet&#039;;&lt;br /&gt;
    var labelText = (labelOverride || (head.querySelector(&#039;a, .kr-title&#039;) || {}).textContent || &#039;&#039;).trim();&lt;br /&gt;
    li.dataset.krId = normId( pid + &#039;::&#039; + labelText );&lt;br /&gt;
&lt;br /&gt;
    // Default zu; gespeicherten Zustand anwenden&lt;br /&gt;
    toggleSet(li, btn, false);&lt;br /&gt;
    var st = STATE[ li.dataset.krId ];&lt;br /&gt;
    if (st === 1) toggleSet(li, btn, true);&lt;br /&gt;
&lt;br /&gt;
    return head;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Toggle-Handler ---------- */&lt;br /&gt;
  function handleArrowClick(btn) {&lt;br /&gt;
    var li = btn.closest(&#039;li.kr-group, li.kr-top&#039;);&lt;br /&gt;
    if (!li) return;&lt;br /&gt;
&lt;br /&gt;
    var willExpand = li.classList.contains(&#039;is-collapsed&#039;);&lt;br /&gt;
    toggleSet(li, btn, willExpand);&lt;br /&gt;
&lt;br /&gt;
    var id = li.dataset.krId;&lt;br /&gt;
    if (id) {&lt;br /&gt;
      STATE[id] = willExpand ? 1 : 0;&lt;br /&gt;
      saveState(STATE);&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* Globale Delegation: Pfeil klickt immer; Kopfzeile klickt nur, wenn NICHT im Link */&lt;br /&gt;
  document.addEventListener(&#039;click&#039;, function (e) {&lt;br /&gt;
    var arrow = e.target &amp;amp;&amp;amp; e.target.closest &amp;amp;&amp;amp; e.target.closest(&#039;.kr-arrow&#039;);&lt;br /&gt;
    if (arrow) {&lt;br /&gt;
      e.preventDefault();&lt;br /&gt;
      handleArrowClick(arrow);&lt;br /&gt;
      return;&lt;br /&gt;
    }&lt;br /&gt;
    var head = e.target &amp;amp;&amp;amp; e.target.closest &amp;amp;&amp;amp; e.target.closest(&#039;.kr-head&#039;);&lt;br /&gt;
    if (head &amp;amp;&amp;amp; !e.target.closest(&#039;.kr-head a&#039;)) {&lt;br /&gt;
      e.preventDefault(); // freie Fläche der Kopfzeile&lt;br /&gt;
      var btn = head.querySelector(&#039;.kr-arrow&#039;);&lt;br /&gt;
      if (btn) handleArrowClick(btn);&lt;br /&gt;
    }&lt;br /&gt;
  }, true); // capture=true, damit nichts dazwischenfunkt&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Aufbau der Gruppen ---------- */&lt;br /&gt;
  function buildGroups(ul) {&lt;br /&gt;
    var lis = Array.from( ul.querySelectorAll(&#039;:scope &amp;gt; li.mw-list-item&#039;) );&lt;br /&gt;
    var current = null;&lt;br /&gt;
&lt;br /&gt;
    lis.forEach(function (li) {&lt;br /&gt;
      var a = li.querySelector(&#039;:scope &amp;gt; a&#039;);&lt;br /&gt;
      var d = extractDirective(a);&lt;br /&gt;
&lt;br /&gt;
      if (d &amp;amp;&amp;amp; d.kind === &#039;group&#039;) {&lt;br /&gt;
        var clickable = hasRealHref(a);&lt;br /&gt;
        li.classList.add(&#039;kr-group&#039;);&lt;br /&gt;
        makeHead(li, clickable, ul, (a &amp;amp;&amp;amp; a.textContent) || d.value || &#039;&#039;);&lt;br /&gt;
        if (!clickable &amp;amp;&amp;amp; a) a.remove(); else if (clickable) stripDirectiveHash(a);&lt;br /&gt;
&lt;br /&gt;
        var sub = document.createElement(&#039;ul&#039;);&lt;br /&gt;
        sub.className = &#039;kr-sub&#039;;&lt;br /&gt;
        li.appendChild(sub);&lt;br /&gt;
        current = sub;&lt;br /&gt;
        return;&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      if (current &amp;amp;&amp;amp; !(d &amp;amp;&amp;amp; (d.kind === &#039;group&#039; || d.kind === &#039;link&#039;))) {&lt;br /&gt;
        current.appendChild(li);&lt;br /&gt;
      }&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Portlets verarbeiten ---------- */&lt;br /&gt;
  function buildPortlets(root) {&lt;br /&gt;
    var lists = root.querySelectorAll(&#039;.vector-menu .vector-menu-content-list&#039;);&lt;br /&gt;
    lists.forEach(function (ul) {&lt;br /&gt;
      if (ul.dataset.krSbDone) return;&lt;br /&gt;
      ul.dataset.krSbDone = &#039;1&#039;;&lt;br /&gt;
&lt;br /&gt;
      var items  = Array.from( ul.querySelectorAll(&#039;:scope &amp;gt; li.mw-list-item&#039;) );&lt;br /&gt;
      var topIdx = items.findIndex(function (li) {&lt;br /&gt;
        var a = li.querySelector(&#039;:scope &amp;gt; a&#039;);&lt;br /&gt;
        var d = extractDirective(a);&lt;br /&gt;
        return d &amp;amp;&amp;amp; d.kind === &#039;link&#039;;&lt;br /&gt;
      });&lt;br /&gt;
&lt;br /&gt;
      if (topIdx &amp;gt;= 0) {&lt;br /&gt;
        var top = items[topIdx];&lt;br /&gt;
        top.classList.add(&#039;kr-top&#039;);&lt;br /&gt;
        makeHead(top, true, ul);&lt;br /&gt;
        stripDirectiveHash( top.querySelector(&#039;:scope &amp;gt; a&#039;) );&lt;br /&gt;
&lt;br /&gt;
        var sub = document.createElement(&#039;ul&#039;);&lt;br /&gt;
        sub.className = &#039;kr-sub&#039;;&lt;br /&gt;
        top.appendChild(sub);&lt;br /&gt;
&lt;br /&gt;
        for (var i = topIdx + 1; i &amp;lt; items.length; i++) sub.appendChild(items[i]);&lt;br /&gt;
        buildGroups(sub);&lt;br /&gt;
      } else {&lt;br /&gt;
        buildGroups(ul);&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      // Direktiven aus hrefs entfernen; Vector-Portlet-Heading ausblenden&lt;br /&gt;
      ul.querySelectorAll(&#039;a[href*=&amp;quot;#link:&amp;quot;], a[href*=&amp;quot;#group:&amp;quot;]&#039;).forEach(stripDirectiveHash);&lt;br /&gt;
      var portlet = ul.closest(&#039;.vector-menu&#039;);&lt;br /&gt;
      var heading = portlet &amp;amp;&amp;amp; portlet.querySelector(&#039;:scope &amp;gt; .vector-menu-heading&#039;);&lt;br /&gt;
      if (heading) heading.remove();&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* ---------- Bootstrapping ---------- */&lt;br /&gt;
  function processRoot(root) {&lt;br /&gt;
    if (!root || root.dataset.krSbRootDone) return;&lt;br /&gt;
    root.dataset.krSbRootDone = &#039;1&#039;;&lt;br /&gt;
    root.classList.add(&#039;kr-sb&#039;);&lt;br /&gt;
    buildPortlets(root);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function init() {&lt;br /&gt;
    document.querySelectorAll(&#039;#mw-panel, #vector-main-menu, #vector-main-menu-pinned, .vector-drawer&#039;)&lt;br /&gt;
      .forEach(processRoot);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  init();&lt;br /&gt;
  mw.hook(&#039;wikipage.content&#039;).add(init);&lt;br /&gt;
&lt;br /&gt;
  // Drawer/DOM-Änderungen beobachten&lt;br /&gt;
  var mo = new MutationObserver(function (muts) {&lt;br /&gt;
    muts.forEach(function (m) {&lt;br /&gt;
      m.addedNodes &amp;amp;&amp;amp; m.addedNodes.forEach(function (n) {&lt;br /&gt;
        if (!(n instanceof Element)) return;&lt;br /&gt;
        if (n.matches(&#039;#vector-main-menu, #vector-main-menu-pinned, .vector-drawer, #mw-panel&#039;)) processRoot(n);&lt;br /&gt;
        n.querySelectorAll &amp;amp;&amp;amp; n.querySelectorAll(&#039;#vector-main-menu, #vector-main-menu-pinned, .vector-drawer, #mw-panel&#039;)&lt;br /&gt;
          .forEach(processRoot);&lt;br /&gt;
      });&lt;br /&gt;
    });&lt;br /&gt;
  });&lt;br /&gt;
  mo.observe(document.body, { childList: true, subtree: true });&lt;br /&gt;
&lt;br /&gt;
/* TabberNeue: restore anchor jump under Vector sticky header */&lt;br /&gt;
  // Sticky-Offset aus Vector (fällt zurück auf 50px) + kleine Luft (0.5rem)&lt;br /&gt;
  function stickyOffsetPx() {&lt;br /&gt;
    var cs = getComputedStyle(document.documentElement);&lt;br /&gt;
    var sticky = parseFloat(cs.getPropertyValue(&#039;--sticky-offset&#039;)) || 50;&lt;br /&gt;
    var air = parseFloat(cs.fontSize) * 0.5; // 0.5rem&lt;br /&gt;
    return Math.round(sticky + air);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // Für ein Panel #tabber-… die passende Header-Zeile finden.&lt;br /&gt;
  // Wir nehmen bewusst die OBERSTE Tabber-Leiste (bei verschachtelten Tabbern),&lt;br /&gt;
  // damit beide Leisten sichtbar bleiben.&lt;br /&gt;
  function headerForPanel(panel) {&lt;br /&gt;
    if (!panel) return null;&lt;br /&gt;
    var topTabber = panel.closest(&#039;.tabber&#039;);&lt;br /&gt;
    var cur = topTabber;&lt;br /&gt;
    while (cur &amp;amp;&amp;amp; cur.parentElement) {&lt;br /&gt;
      var up = cur.parentElement.closest(&#039;.tabber&#039;);&lt;br /&gt;
      if (!up) break;&lt;br /&gt;
      topTabber = up; cur = up;&lt;br /&gt;
    }&lt;br /&gt;
    return topTabber ? topTabber.querySelector(&#039;:scope &amp;gt; .tabber__header&#039;) : null;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function scrollToTab(id) {&lt;br /&gt;
    var name = (id || &#039;&#039;).replace(/^#/, &#039;&#039;);&lt;br /&gt;
    if (!/^tabber-/.test(name)) return;&lt;br /&gt;
    var panel = document.getElementById(name);&lt;br /&gt;
    if (!panel) return;&lt;br /&gt;
&lt;br /&gt;
    var target = headerForPanel(panel) || panel;&lt;br /&gt;
    var y = target.getBoundingClientRect().top + window.scrollY - stickyOffsetPx();&lt;br /&gt;
    window.scrollTo({ top: y, behavior: &#039;smooth&#039; });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // 1) Direktaufruf mit Hash (Seitenaufruf/Reload)&lt;br /&gt;
  if (/^#tabber-/.test(location.hash)) {&lt;br /&gt;
    // kurz warten, bis TabberNeue ARIA/hidden gesetzt hat&lt;br /&gt;
    setTimeout(function () { scrollToTab(location.hash); }, 60);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // 2) Hashwechsel (falls woanders doch pushState→hashchange genutzt wird)&lt;br /&gt;
  window.addEventListener(&#039;hashchange&#039;, function () {&lt;br /&gt;
    if (/^#tabber-/.test(location.hash)) scrollToTab(location.hash);&lt;br /&gt;
  });&lt;br /&gt;
&lt;br /&gt;
  // 3) Tab-Klicks: TabberNeue verhindert default + nutzt replaceState.&lt;br /&gt;
  // Wir haken uns nach dem Klick kurz später ein und scrollen.&lt;br /&gt;
  document.addEventListener(&#039;click&#039;, function (e) {&lt;br /&gt;
    var a = e.target &amp;amp;&amp;amp; e.target.closest(&#039;a[href^=&amp;quot;#tabber-&amp;quot;], button[aria-controls^=&amp;quot;tabber-&amp;quot;]&#039;);&lt;br /&gt;
    if (!a) return;&lt;br /&gt;
    var id = a.getAttribute(&#039;href&#039;) || (&#039;#&#039; + a.getAttribute(&#039;aria-controls&#039;));&lt;br /&gt;
    setTimeout(function () { scrollToTab(id); }, 60);&lt;br /&gt;
  }, true);&lt;br /&gt;
&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Webmaster</name></author>
	</entry>
	<entry>
		<id>https://firestone-idle-rpg.de/index.php?title=Modul:Firestone&amp;diff=3768</id>
		<title>Modul:Firestone</title>
		<link rel="alternate" type="text/html" href="https://firestone-idle-rpg.de/index.php?title=Modul:Firestone&amp;diff=3768"/>
		<updated>2025-10-28T12:53:32Z</updated>

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

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

		<summary type="html">&lt;p&gt;Webmaster: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;-- Modul:Firestone&lt;br /&gt;
local M = {}&lt;br /&gt;
&lt;br /&gt;
-- ========== kleine Helfer ==========&lt;br /&gt;
local COL_OFFER, COL_PRICE, COL_LIMIT = &amp;quot;50%&amp;quot;, &amp;quot;25%&amp;quot;, &amp;quot;25%&amp;quot;&lt;br /&gt;
local DAY = 86400&lt;br /&gt;
local function parse_iso_date(s)&lt;br /&gt;
  local y,m,d = tostring(s or &amp;quot;&amp;quot;):match(&amp;quot;^(%d%d%d%d)%-(%d%d)%-(%d%d)$&amp;quot;)&lt;br /&gt;
  if not y then return nil end&lt;br /&gt;
  return os.time{ year=tonumber(y), month=tonumber(m), day=tonumber(d), hour=12 }&lt;br /&gt;
end&lt;br /&gt;
local function ts_to_mw(ts) return os.date(&amp;quot;!%Y%m%d%H%M%S&amp;quot;, ts) end&lt;br /&gt;
local function format_date_de(v)&lt;br /&gt;
  local ts = (type(v)==&amp;quot;number&amp;quot;) and v or parse_iso_date(v)&lt;br /&gt;
  if not ts then return &amp;quot;&amp;quot; end&lt;br /&gt;
  local lang = mw.getContentLanguage()&lt;br /&gt;
  return lang:formatDate(&amp;quot;d.m.Y&amp;quot;, ts_to_mw(ts))&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function isempty(v) return v == nil or v == &amp;quot;&amp;quot; end&lt;br /&gt;
local function norm(s)&lt;br /&gt;
  if s == nil then return &amp;quot;&amp;quot; end&lt;br /&gt;
  s = mw.text.trim(tostring(s))&lt;br /&gt;
  return mw.ustring.lower(s):gsub(&amp;quot;%s+&amp;quot;,&amp;quot; &amp;quot;)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- bevorzugt Parent-Args (Vorlagen)&lt;br /&gt;
local function getArgs(frame)&lt;br /&gt;
  local p = frame:getParent()&lt;br /&gt;
  local src = (p and p.args) or frame.args or {}&lt;br /&gt;
  local a = {}&lt;br /&gt;
  for k,v in pairs(src) do&lt;br /&gt;
    if v ~= nil and v ~= &amp;quot;&amp;quot; then a[k] = mw.text.trim(tostring(v)) end&lt;br /&gt;
  end&lt;br /&gt;
  return a&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function has_items(t)&lt;br /&gt;
  if type(t) ~= &amp;quot;table&amp;quot; then return false end&lt;br /&gt;
  for _ in pairs(t) do return true end&lt;br /&gt;
  return false&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function pick_key(tbl, key)&lt;br /&gt;
  if type(tbl) ~= &amp;quot;table&amp;quot; then return nil end&lt;br /&gt;
  if tbl[key] ~= nil then return key end&lt;br /&gt;
  local k2 = tostring(key)&lt;br /&gt;
  if tbl[k2] ~= nil then return k2 end&lt;br /&gt;
  local kn = norm(k2)&lt;br /&gt;
  for k,_ in pairs(tbl) do if norm(k) == kn then return k end end&lt;br /&gt;
  return nil&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function deep_get(tbl, path)&lt;br /&gt;
  local cur = tbl&lt;br /&gt;
  for _,k in ipairs(path) do&lt;br /&gt;
    if type(cur) ~= &amp;quot;table&amp;quot; then return nil end&lt;br /&gt;
    local real = pick_key(cur, k); if not real then return nil end&lt;br /&gt;
    cur = cur[real]&lt;br /&gt;
  end&lt;br /&gt;
  return cur&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function fileTag(file, opts) -- [[Datei:...]]&lt;br /&gt;
  if isempty(file) then return &amp;quot;&amp;quot; end&lt;br /&gt;
  local parts = { &amp;quot;Datei:&amp;quot;..file }&lt;br /&gt;
  if opts and opts.size  then table.insert(parts, opts.size)  end&lt;br /&gt;
  if opts and opts.param then table.insert(parts, opts.param) end -- z.B. &amp;quot;thumb|Text&amp;quot;&lt;br /&gt;
  return string.format(&amp;quot;[[%s]]&amp;quot;, table.concat(parts, &amp;quot;|&amp;quot;))&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function pretty_range(s)&lt;br /&gt;
  if not s or s==&amp;quot;&amp;quot; then return &amp;quot;&amp;quot; end&lt;br /&gt;
  s = tostring(s)&lt;br /&gt;
  if s:sub(-1) == &amp;quot;-&amp;quot; then return s:sub(1, -2) .. &amp;quot;+&amp;quot; end&lt;br /&gt;
  return s:gsub(&amp;quot;%-&amp;quot;,&amp;quot;–&amp;quot;)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- ========== Lazy-Loads ==========&lt;br /&gt;
local I18N, EVENTS, HEROES&lt;br /&gt;
local function i18n()&lt;br /&gt;
  if I18N then return I18N end&lt;br /&gt;
  for _,t in ipairs{ &amp;quot;Modul:HeroI18n&amp;quot;, &amp;quot;Module:HeroI18n&amp;quot; } do&lt;br /&gt;
    local ok, data = pcall(mw.loadData, t) ; if ok and type(data)==&amp;quot;table&amp;quot; then I18N=data; return I18N end&lt;br /&gt;
    local ok2, mod = pcall(require, t)      ; if ok2 and type(mod) ==&amp;quot;table&amp;quot; then I18N=mod ; return I18N end&lt;br /&gt;
  end&lt;br /&gt;
  I18N = { i18n={}, events={ names={}, sections={default={}}, phrases={}, links={}, unlocks={} } }&lt;br /&gt;
  return I18N&lt;br /&gt;
end&lt;br /&gt;
local function events()&lt;br /&gt;
  if EVENTS then return EVENTS end&lt;br /&gt;
  for _,t in ipairs{ &amp;quot;Modul:EventData&amp;quot;, &amp;quot;Module:EventData&amp;quot; } do&lt;br /&gt;
    local ok, data = pcall(mw.loadData, t) ; if ok and type(data)==&amp;quot;table&amp;quot; and type(data.events)==&amp;quot;table&amp;quot; then EVENTS=data.events; return EVENTS end&lt;br /&gt;
    local ok2, mod = pcall(require, t)     ; if ok2 and type(mod) ==&amp;quot;table&amp;quot; and type(mod.events)==&amp;quot;table&amp;quot; then EVENTS=mod.events ; return EVENTS end&lt;br /&gt;
  end&lt;br /&gt;
  EVENTS = {}; return EVENTS&lt;br /&gt;
end&lt;br /&gt;
local function heroes()&lt;br /&gt;
  if HEROES then return HEROES end&lt;br /&gt;
  for _,t in ipairs{ &amp;quot;Modul:HeroData&amp;quot;, &amp;quot;Module:HeroData&amp;quot; } do&lt;br /&gt;
    local ok, data = pcall(mw.loadData, t) ; if ok and type(data)==&amp;quot;table&amp;quot; and type(data.heroes)==&amp;quot;table&amp;quot; then HEROES=data.heroes; return HEROES end&lt;br /&gt;
    local ok2, mod = pcall(require, t)     ; if ok2 and type(mod) ==&amp;quot;table&amp;quot; and type(mod.heroes)==&amp;quot;table&amp;quot; then HEROES=mod.heroes ; return HEROES end&lt;br /&gt;
  end&lt;br /&gt;
  HEROES = {}; return HEROES&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- ========== i18n / Labels ==========&lt;br /&gt;
local function ev_label(evkey, which)&lt;br /&gt;
  local E = i18n().events or {}&lt;br /&gt;
  local spec = (E.sections and E.sections[norm(evkey)]) or {}&lt;br /&gt;
  local def  = (E.sections and E.sections.default) or {}&lt;br /&gt;
  return (spec[which] or def[which] or &amp;quot;&amp;quot;)&lt;br /&gt;
end&lt;br /&gt;
local function ev_name(evkey)&lt;br /&gt;
  local names = (i18n().events or {}).names or {}&lt;br /&gt;
  return names[norm(evkey)] or evkey&lt;br /&gt;
end&lt;br /&gt;
local function header_labels(evkey)&lt;br /&gt;
  return ev_label(evkey, &amp;quot;offer&amp;quot;), ev_label(evkey, &amp;quot;price&amp;quot;), ev_label(evkey, &amp;quot;limit&amp;quot;)&lt;br /&gt;
end&lt;br /&gt;
local function autotr(key_name, value)&lt;br /&gt;
  if type(value) ~= &amp;quot;string&amp;quot; then return value end&lt;br /&gt;
  local k = norm(key_name)&lt;br /&gt;
&lt;br /&gt;
  -- 1) Standardwortschatz (falls vorhanden)&lt;br /&gt;
  local m1 = (i18n().i18n or {})[k]&lt;br /&gt;
  local v  = m1 and m1[norm(value)]&lt;br /&gt;
  if v then return v end&lt;br /&gt;
&lt;br /&gt;
  -- 2) Spezialfall Event-Typen: i18n().events.infobox.type&lt;br /&gt;
  if k == &amp;quot;type&amp;quot; then&lt;br /&gt;
    local m2 = (((i18n().events or {}).infobox) or {}).type or {}&lt;br /&gt;
    return m2[norm(value)] or value&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  return value&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function ev_notice_icon_wikitext(ev)&lt;br /&gt;
  local file = ev.currency and ev.currency.icon&lt;br /&gt;
  if not file or file == &amp;quot;&amp;quot; then return &amp;quot;&amp;quot; end&lt;br /&gt;
  local img = fileTag(file, { size = &amp;quot;24px&amp;quot;, param = &amp;quot;frameless|link=&amp;quot; })&lt;br /&gt;
  return &#039;&amp;lt;span class=&amp;quot;fs-ic&amp;quot;&amp;gt;&#039; .. img .. &#039;&amp;lt;/span&amp;gt;&#039;&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function ev_pagelink(ev, evkey, label)&lt;br /&gt;
  local page = ev.page&lt;br /&gt;
  if page and page ~= &amp;quot;&amp;quot; then&lt;br /&gt;
    return string.format(&amp;quot;[[%s|%s]]&amp;quot;, page, label)&lt;br /&gt;
  end&lt;br /&gt;
  return label&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
-- ========== Nummern + &amp;lt;ref&amp;gt; (voll i18n-gesteuert) ==========&lt;br /&gt;
local function parse_sci(v)&lt;br /&gt;
  if type(v) == &amp;quot;number&amp;quot; then return v end&lt;br /&gt;
  if type(v) == &amp;quot;string&amp;quot; then&lt;br /&gt;
    local a,e = v:match(&amp;quot;^%s*([%d%.]+)[eE]([%+%-]?%d+)%s*$&amp;quot;)&lt;br /&gt;
    if a and e then return (tonumber(a) or 0) * 10^(tonumber(e) or 0) end&lt;br /&gt;
    local n = tonumber(v); if n then return n end&lt;br /&gt;
  end&lt;br /&gt;
  return nil&lt;br /&gt;
end&lt;br /&gt;
local function i18n_big_units()&lt;br /&gt;
  local N = (i18n().numbers or {})&lt;br /&gt;
  return {&lt;br /&gt;
    {1e12, N.trillion_sing,  N.trillion_plur},&lt;br /&gt;
    {1e9,  N.billion_sing,   N.billion_plur},&lt;br /&gt;
    {1e6,  N.million_sing,   N.million_plur},&lt;br /&gt;
    {1e3,  N.thousand_sing,  N.thousand_plur},&lt;br /&gt;
  }&lt;br /&gt;
end&lt;br /&gt;
local function format_big(v)&lt;br /&gt;
  local n = parse_sci(v); if not n then return tostring(v or &amp;quot;&amp;quot;) end&lt;br /&gt;
  for _,u in ipairs(i18n_big_units()) do&lt;br /&gt;
    local base, sing, plur = u[1], u[2], u[3]&lt;br /&gt;
    if base and sing and plur and n &amp;gt;= base and math.floor(n % base) == 0 then&lt;br /&gt;
      local q = math.floor(n / base)&lt;br /&gt;
      return string.format(&amp;quot;%d %s&amp;quot;, q, (q==1) and sing or plur)&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
  return tostring(n)&lt;br /&gt;
end&lt;br /&gt;
local function ref_gear_power(v)&lt;br /&gt;
  if isempty(v) then return &amp;quot;&amp;quot; end&lt;br /&gt;
  local phr = (((i18n().events or {}).phrases) or {}).req_gear_power&lt;br /&gt;
  if isempty(phr) then return &amp;quot;&amp;quot; end&lt;br /&gt;
  local name = &amp;quot;gear-&amp;quot; .. tostring(v):gsub(&amp;quot;%s&amp;quot;,&amp;quot;&amp;quot;)&lt;br /&gt;
  return string.format(&#039;&amp;lt;ref name=&amp;quot;%s&amp;quot;&amp;gt;%s&amp;lt;/ref&amp;gt;&#039;, name, (phr:gsub(&amp;quot;$1&amp;quot;, format_big(v))))&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- ========== Event-Renderer ==========&lt;br /&gt;
local function htag(level, text)&lt;br /&gt;
  if isempty(text) then return &amp;quot;&amp;quot; end&lt;br /&gt;
  local eq = level==3 and &amp;quot;===&amp;quot; or &amp;quot;==&amp;quot;&lt;br /&gt;
  return string.format(&amp;quot;%s %s %s&amp;quot;, eq, text, eq)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function event_file(ev, evkey, which)&lt;br /&gt;
  if which==&amp;quot;deco&amp;quot; then&lt;br /&gt;
    local file   = ev.images and ev.images.deco or &amp;quot;&amp;quot;&lt;br /&gt;
    local legend = ev_label(evkey, &amp;quot;deco&amp;quot;)&lt;br /&gt;
    return fileTag(file, { param = &amp;quot;thumb|&amp;quot;..legend })&lt;br /&gt;
  elseif which==&amp;quot;fullname&amp;quot; then&lt;br /&gt;
    local file   = ev.images and ev.images.fullname or &amp;quot;&amp;quot;&lt;br /&gt;
    local legend = ev_label(evkey, &amp;quot;fullname&amp;quot;)&lt;br /&gt;
    return fileTag(file, { param = &amp;quot;thumb|&amp;quot;..legend })&lt;br /&gt;
  elseif which==&amp;quot;ex_shop&amp;quot; then&lt;br /&gt;
    local file   = ev.images and ev.images.ex_shop or &amp;quot;&amp;quot;&lt;br /&gt;
    local legend = ev_label(evkey, &amp;quot;ex_shop&amp;quot;)&lt;br /&gt;
    return fileTag(file, { param = &amp;quot;thumb|&amp;quot;..legend })&lt;br /&gt;
  elseif which==&amp;quot;banner&amp;quot; then&lt;br /&gt;
    return fileTag(ev.banner, { param=&amp;quot;thumb&amp;quot; })&lt;br /&gt;
  end&lt;br /&gt;
  return &amp;quot;&amp;quot;&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function event_link(ev, evkey, which)&lt;br /&gt;
  if which==&amp;quot;type&amp;quot; then&lt;br /&gt;
    local E = i18n().events or {}&lt;br /&gt;
    local page  = (E.links and E.links.type and E.links.type[ev.type or &amp;quot;&amp;quot;])&lt;br /&gt;
                  or (E.links and E.links.type) or &amp;quot;Calendar Events&amp;quot;&lt;br /&gt;
    local map   = (E.infobox and E.infobox.type) or {}&lt;br /&gt;
    local label = map[norm(ev.type or &amp;quot;&amp;quot;)] or (ev.type or &amp;quot;&amp;quot;)&lt;br /&gt;
    return string.format(&amp;quot;[[%s|%s]]&amp;quot;, page, label)&lt;br /&gt;
  end&lt;br /&gt;
  return &amp;quot;&amp;quot;&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function event_history(ev, evkey, args)&lt;br /&gt;
  local H = ev.history or {}; if not has_items(H) then return &amp;quot;&amp;quot; end&lt;br /&gt;
&lt;br /&gt;
  local phrases  = ((i18n().events or {}).phrases or {})&lt;br /&gt;
  local alt_gp   = phrases.alt_platform or &amp;quot;auf Google Play&amp;quot;&lt;br /&gt;
&lt;br /&gt;
  local function fmt_range_or_raw(s)&lt;br /&gt;
    if not s or s == &amp;quot;&amp;quot; then return &amp;quot;&amp;quot; end&lt;br /&gt;
    local a,b = tostring(s):match(&amp;quot;^(%d%d%d%d%-%d%d%-%d%d)%s+bis%s+(%d%d%d%d%-%d%d%-%d%d)$&amp;quot;)&lt;br /&gt;
    if a and b then&lt;br /&gt;
      return string.format(&amp;quot;%s bis %s&amp;quot;, format_date_de(a), format_date_de(b))&lt;br /&gt;
    end&lt;br /&gt;
    local ts = parse_iso_date(s)&lt;br /&gt;
    return ts and format_date_de(ts) or s&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  local lines = {}&lt;br /&gt;
  for _,h in ipairs(H) do&lt;br /&gt;
    local a = format_date_de(h.start)&lt;br /&gt;
    local b = format_date_de(h[&amp;quot;end&amp;quot;] or h.end_date)&lt;br /&gt;
    local line = string.format(&amp;quot;* %s: %s bis %s&amp;quot;, tostring(h.year or &amp;quot;&amp;quot;), a, b)&lt;br /&gt;
    if h.alt_platform then&lt;br /&gt;
      line = line .. string.format(&amp;quot; (%s %s)&amp;quot;, fmt_range_or_raw(h.alt_platform), alt_gp)&lt;br /&gt;
    end&lt;br /&gt;
    lines[#lines+1] = line&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function item_label(key, amount)&lt;br /&gt;
  local I = (i18n().items or {})[key] or {}&lt;br /&gt;
  local n = tonumber(amount or 1) or 1&lt;br /&gt;
  local txt = (n == 1) and (I.sing or key) or (I.plur or (I.sing or key))&lt;br /&gt;
  local disp = I.link and string.format(&amp;quot;[[%s|%s]]&amp;quot;, I.link, txt) or txt&lt;br /&gt;
  return string.format(&amp;quot;%d %s&amp;quot;, n, disp)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- references/render&lt;br /&gt;
local FRAME = nil&lt;br /&gt;
local function tabber_render_from_content(content)&lt;br /&gt;
  if FRAME and FRAME.extensionTag then&lt;br /&gt;
    return FRAME:extensionTag(&#039;tabber&#039;, content)&lt;br /&gt;
  end&lt;br /&gt;
  return &amp;quot;&amp;lt;tabber&amp;gt;\n&amp;quot; .. content .. &amp;quot;\n&amp;lt;/tabber&amp;gt;&amp;quot;&lt;br /&gt;
end&lt;br /&gt;
local function references_tag()&lt;br /&gt;
  if FRAME and FRAME.extensionTag then&lt;br /&gt;
    return FRAME:extensionTag(&#039;references&#039;, &#039;&#039;)&lt;br /&gt;
  end&lt;br /&gt;
  return &#039;&amp;lt;references /&amp;gt;&#039;&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- innerer Tab-Inhalt: je Range&lt;br /&gt;
local function build_tabber_content_for(list)&lt;br /&gt;
  local out = {}&lt;br /&gt;
  for _,it in ipairs(list) do&lt;br /&gt;
    local label   = pretty_range(it.range or &amp;quot;&amp;quot;)&lt;br /&gt;
    local note    = ref_gear_power(it.req_gear_power)&lt;br /&gt;
    local content = string.format(&lt;br /&gt;
      &amp;quot;%s %s%s&amp;quot;,&lt;br /&gt;
      fileTag(it.icon or &amp;quot;&amp;quot;, { size=&amp;quot;30px&amp;quot; }),&lt;br /&gt;
      (it.item and item_label(it.item, it.amount or 1)) or (it.title or &amp;quot;&amp;quot;),&lt;br /&gt;
      note&lt;br /&gt;
    )&lt;br /&gt;
    out[#out+1] = &amp;quot;|-|&amp;quot; .. label .. &amp;quot;=\n&amp;quot; .. content&lt;br /&gt;
  end&lt;br /&gt;
  return table.concat(out, &amp;quot;\n&amp;quot;)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Gruppen-Label aus i18n&lt;br /&gt;
local function group_label(kind)&lt;br /&gt;
  local G = ((i18n().events or {}).group_labels or {})&lt;br /&gt;
  return G[kind] or kind&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function event_avatars(ev, evkey)&lt;br /&gt;
  local AV = ev.avatars&lt;br /&gt;
  if not has_items(AV) then return &amp;quot;&amp;quot; end&lt;br /&gt;
&lt;br /&gt;
  local token_icon = (ev.currency or {}).icon or &amp;quot;&amp;quot;&lt;br /&gt;
  local token_name = ev_label(evkey, &amp;quot;currencies&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
  local years, out = {}, {}&lt;br /&gt;
  for y,_ in pairs(AV) do table.insert(years, tonumber(y) or y) end&lt;br /&gt;
  table.sort(years, function(a,b) return tostring(a) &amp;lt; tostring(b) end)&lt;br /&gt;
&lt;br /&gt;
  for _,year in ipairs(years) do&lt;br /&gt;
    local list = AV[year] or AV[tostring(year)]&lt;br /&gt;
    if has_items(list) then&lt;br /&gt;
      out[#out+1] = (&amp;quot;==== %s ====&amp;quot;):format(year)&lt;br /&gt;
      out[#out+1] = &#039;{| class=&amp;quot;article-table&amp;quot; style=&amp;quot;font-size:14px;&amp;quot;&#039;&lt;br /&gt;
      out[#out+1] = &#039;! colspan=&amp;quot;2&amp;quot; | Avatar\n! Preis&#039;&lt;br /&gt;
      for _,it in ipairs(list) do&lt;br /&gt;
        out[#out+1] = &#039;|-&#039;&lt;br /&gt;
        out[#out+1] = string.format(&#039;|%s || %s || %s %s %s&#039;,&lt;br /&gt;
          fileTag(it.file, { size=&amp;quot;50px&amp;quot; }),&lt;br /&gt;
          it.title or &amp;quot;&amp;quot;,&lt;br /&gt;
          fileTag(token_icon, { size=&amp;quot;25px&amp;quot; }),&lt;br /&gt;
          tostring(it.cost or 0),&lt;br /&gt;
          token_name&lt;br /&gt;
        )&lt;br /&gt;
      end&lt;br /&gt;
      out[#out+1] = &#039;|}&#039;&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
  return (#out &amp;gt; 0) and table.concat(out, &amp;quot;\n&amp;quot;) or &amp;quot;&amp;quot;&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Austausch-Tabelle (äußerer Tab: Gruppen → innerer Tab: Ranges)&lt;br /&gt;
local function event_exchange_tabs(ev, evkey)&lt;br /&gt;
  local ex = ev.exchange or {}&lt;br /&gt;
  local token_icon = (ev.currency or {}).icon or &amp;quot;&amp;quot;&lt;br /&gt;
  local token_name = ev_label(evkey, &amp;quot;currencies&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
  if not (has_items(ex.chests_by_level)&lt;br /&gt;
       or has_items(ex.chests_by_stars)&lt;br /&gt;
       or has_items(ex.chests_by_oracle)&lt;br /&gt;
       or has_items(ex.currencies)) then&lt;br /&gt;
    return &amp;quot;&amp;quot;&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  local outer_parts = {}&lt;br /&gt;
  local function add_group(kind, list)&lt;br /&gt;
    if not has_items(list) then return end&lt;br /&gt;
    local inner_content = build_tabber_content_for(list)&lt;br /&gt;
    local inner_tabber  = tabber_render_from_content(inner_content)&lt;br /&gt;
    outer_parts[#outer_parts+1] = &amp;quot;|-|&amp;quot; .. group_label(kind) .. &amp;quot;=\n&amp;quot; .. inner_tabber&lt;br /&gt;
  end&lt;br /&gt;
  add_group(&amp;quot;level&amp;quot;,  ex.chests_by_level)&lt;br /&gt;
  add_group(&amp;quot;stars&amp;quot;,  ex.chests_by_stars)&lt;br /&gt;
  add_group(&amp;quot;oracle&amp;quot;, ex.chests_by_oracle)&lt;br /&gt;
&lt;br /&gt;
  local left_tabber = tabber_render_from_content(table.concat(outer_parts, &amp;quot;\n&amp;quot;))&lt;br /&gt;
&lt;br /&gt;
  -- Preis/Limit (erste gefundene Gruppe)&lt;br /&gt;
  local function pick_price_limit()&lt;br /&gt;
    if has_items(ex.chests_by_level)  then return ex.chests_by_level[1].price or 0,  ex.chests_by_level[1].limit or &amp;quot;&amp;quot;  end&lt;br /&gt;
    if has_items(ex.chests_by_stars)  then return ex.chests_by_stars[1].price or 0,  ex.chests_by_stars[1].limit or &amp;quot;&amp;quot;  end&lt;br /&gt;
    if has_items(ex.chests_by_oracle) then return ex.chests_by_oracle[1].price or 0, ex.chests_by_oracle[1].limit or &amp;quot;&amp;quot; end&lt;br /&gt;
    return 0, &amp;quot;&amp;quot;&lt;br /&gt;
  end&lt;br /&gt;
  local price, limit = pick_price_limit()&lt;br /&gt;
&lt;br /&gt;
  local t = {}&lt;br /&gt;
  local H_offer, H_price, H_limit = header_labels(evkey)&lt;br /&gt;
  t[#t+1] = &#039;{| class=&amp;quot;article-table fs-exchange&amp;quot; style=&amp;quot;font-size:14px;&amp;quot;&#039;&lt;br /&gt;
  t[#t+1] = string.format(&lt;br /&gt;
    &#039;! style=&amp;quot;width:%s;&amp;quot; | %s !! style=&amp;quot;width:%s;&amp;quot; | %s !! style=&amp;quot;width:%s;&amp;quot; | %s&#039;,&lt;br /&gt;
    COL_OFFER, H_offer, COL_PRICE, H_price, COL_LIMIT, H_limit&lt;br /&gt;
  )&lt;br /&gt;
  t[#t+1] = &#039;|-&#039;&lt;br /&gt;
  t[#t+1] = &#039;|&#039; .. left_tabber .. string.format(&lt;br /&gt;
    &#039; || style=&amp;quot;text-align:center;&amp;quot; | %s %s %s || style=&amp;quot;text-align:center;&amp;quot; | %s&#039;,&lt;br /&gt;
    fileTag(token_icon, { size=&amp;quot;25px&amp;quot; }),&lt;br /&gt;
    tostring(price), token_name,&lt;br /&gt;
    tostring(limit)&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
  if has_items(ex.currencies) then&lt;br /&gt;
    for _,it in ipairs(ex.currencies) do&lt;br /&gt;
      local offer = string.format(&#039;%s %s&#039;,&lt;br /&gt;
        fileTag(it.icon or &amp;quot;&amp;quot;, { size=&amp;quot;30px&amp;quot; }),&lt;br /&gt;
        item_label(it.item, it.amount or 1)&lt;br /&gt;
      )&lt;br /&gt;
      t[#t+1] = &#039;|-&#039;&lt;br /&gt;
      t[#t+1] = string.format(&lt;br /&gt;
        &#039;| %s || style=&amp;quot;text-align:center;&amp;quot; | %s %s %s || style=&amp;quot;text-align:center;&amp;quot; | %s&#039;,&lt;br /&gt;
        offer,&lt;br /&gt;
        fileTag(token_icon, { size=&amp;quot;25px&amp;quot; }),&lt;br /&gt;
        tostring(it.price or 0), token_name,&lt;br /&gt;
        tostring(it.limit or &amp;quot;&amp;quot;)&lt;br /&gt;
      )&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  t[#t+1] = &#039;|}&#039;&lt;br /&gt;
  t[#t+1] = references_tag()&lt;br /&gt;
  return &#039;\n&#039; .. table.concat(t, &#039;\n&#039;)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function event_offers(ev, evkey, which)&lt;br /&gt;
  local list = (ev.shop or {})[which]&lt;br /&gt;
  if not has_items(list) then return &amp;quot;&amp;quot; end&lt;br /&gt;
  local token_icon = (ev.currency or {}).icon or &amp;quot;&amp;quot;&lt;br /&gt;
  local token_name = ev_label(evkey, &amp;quot;currencies&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
  local t = {}&lt;br /&gt;
  t[#t+1] = &#039;{| class=&amp;quot;article-table&amp;quot; style=&amp;quot;font-size:14px;&amp;quot;&#039;&lt;br /&gt;
  t[#t+1] = &amp;quot;! Angebot !! Menge&amp;quot;&lt;br /&gt;
  for _,it in ipairs(list) do&lt;br /&gt;
    t[#t+1] = &amp;quot;|-&amp;quot;&lt;br /&gt;
    t[#t+1] = string.format(&amp;quot;| &#039;&#039;%s&#039;&#039;&amp;lt;br /&amp;gt;%s || %s %s %s&amp;quot;,&lt;br /&gt;
      it.name or &amp;quot;&amp;quot;,&lt;br /&gt;
      fileTag(it.image, { size=&amp;quot;x50px&amp;quot; }),&lt;br /&gt;
      fileTag(token_icon, { size=&amp;quot;30px&amp;quot; }),&lt;br /&gt;
      tostring(it.token or 0),&lt;br /&gt;
      token_name&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
  t[#t+1] = &amp;quot;|}&amp;quot;&lt;br /&gt;
  return table.concat(t, &amp;quot;\n&amp;quot;)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- ========== SiteNotice-Support (Datum/Auto-Pick) ==========&lt;br /&gt;
local function event_window(ev)&lt;br /&gt;
  local S = ev.schedule or ev&lt;br /&gt;
  local s = S.start_date; if isempty(s) then return nil end&lt;br /&gt;
&lt;br /&gt;
  local st = parse_iso_date(s); if not st then return nil end&lt;br /&gt;
&lt;br /&gt;
  local en_excl, en_incl&lt;br /&gt;
  if not isempty(S.end_date) then&lt;br /&gt;
    -- end_date wird als inklusives Kalenderdatum verstanden&lt;br /&gt;
    en_incl = parse_iso_date(S.end_date)&lt;br /&gt;
    if not en_incl then return st, nil, nil end&lt;br /&gt;
    en_excl = en_incl + DAY&lt;br /&gt;
  else&lt;br /&gt;
    -- Dauer in Tagen (Standard 14), exklusives Ende = st + d*DAY&lt;br /&gt;
    local d = tonumber(S.duration_days) or 14&lt;br /&gt;
    en_excl = st + d * DAY&lt;br /&gt;
    en_incl = en_excl - DAY&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  return st, en_excl, en_incl&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function pick_notice_eventkey()&lt;br /&gt;
  local cfg = ((i18n().events or {}).notice or {})&lt;br /&gt;
  local pre  = tonumber(cfg.pre_days)  or 7&lt;br /&gt;
  local post = tonumber(cfg.post_days) or 3&lt;br /&gt;
  local now = os.time()&lt;br /&gt;
&lt;br /&gt;
  local running, upcoming, ended_recent = {}, {}, {}&lt;br /&gt;
&lt;br /&gt;
  for k, ev in pairs(events()) do&lt;br /&gt;
    local st, en_excl = event_window(ev)&lt;br /&gt;
    if st and en_excl then&lt;br /&gt;
      if now &amp;gt;= st and now &amp;lt; en_excl then&lt;br /&gt;
        table.insert(running, { k=k, st=st, en=en_excl })&lt;br /&gt;
      elseif now &amp;gt;= st - pre*DAY and now &amp;lt; st then&lt;br /&gt;
        table.insert(upcoming, { k=k, st=st, en=en_excl })&lt;br /&gt;
      elseif now &amp;gt;= en_excl and now &amp;lt; en_excl + post*DAY then&lt;br /&gt;
        table.insert(ended_recent, { k=k, st=st, en=en_excl })&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  local function by_end(a,b)   return a.en &amp;lt; b.en end&lt;br /&gt;
  local function by_start(a,b) return a.st &amp;lt; b.st end&lt;br /&gt;
  if #running      &amp;gt; 0 then table.sort(running,      by_end)   ; return running[1].k      end&lt;br /&gt;
  if #upcoming     &amp;gt; 0 then table.sort(upcoming,     by_start) ; return upcoming[1].k     end&lt;br /&gt;
  if #ended_recent &amp;gt; 0 then table.sort(ended_recent, by_end)   ; return ended_recent[1].k end&lt;br /&gt;
  return nil&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function event_notice(ev, evkey)&lt;br /&gt;
  local phrases = (((i18n().events or {}).phrases) or {})&lt;br /&gt;
  local PH_RUNNING = phrases.event_notice   -- &amp;quot;Das $1 läuft von $2 bis $3&amp;quot;&lt;br /&gt;
  local PH_ENDED   = phrases.event_ended    -- &amp;quot;Das $1 ist beendet. Du kannst verbleibende $4 noch im $5 eintauschen.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
  -- Ohne Texte kein Hinweis&lt;br /&gt;
  if isempty(PH_RUNNING) then return &amp;quot;&amp;quot; end&lt;br /&gt;
&lt;br /&gt;
  local st, en_excl, en_incl = event_window(ev); if not st or not en_excl then return &amp;quot;&amp;quot; end&lt;br /&gt;
&lt;br /&gt;
  local cfg  = ((i18n().events or {}).notice or {})&lt;br /&gt;
  local pre  = tonumber(cfg.pre_days)  or 7   -- z. B. 7 Tage vorher&lt;br /&gt;
  local post = tonumber(cfg.post_days) or 3   -- z. B. 3 Tage danach&lt;br /&gt;
  local now  = os.time()&lt;br /&gt;
&lt;br /&gt;
  local show_from = st - pre  * DAY&lt;br /&gt;
  local show_till = en_excl + post * DAY&lt;br /&gt;
  if now &amp;lt; show_from or now &amp;gt;= show_till then&lt;br /&gt;
    return &amp;quot;&amp;quot;&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  -- Anzeigename (aus i18n sections oder names)&lt;br /&gt;
  local name_label = ev_label(evkey, &amp;quot;eventname&amp;quot;)&lt;br /&gt;
  if isempty(name_label) then name_label = ev_name(evkey) end&lt;br /&gt;
&lt;br /&gt;
  -- Verlinken NUR wenn ev.page existiert&lt;br /&gt;
  local name = ev_pagelink(ev, evkey, name_label)&lt;br /&gt;
&lt;br /&gt;
  -- Icon aus EventData → currency.icon&lt;br /&gt;
  local ic = ev_notice_icon_wikitext(ev)  -- &amp;quot;&amp;quot; wenn kein currency.icon&lt;br /&gt;
&lt;br /&gt;
  local text&lt;br /&gt;
  if now &amp;lt; en_excl then&lt;br /&gt;
    -- läuft / startet bald&lt;br /&gt;
    text = PH_RUNNING&lt;br /&gt;
      :gsub(&amp;quot;$1&amp;quot;, name)&lt;br /&gt;
      :gsub(&amp;quot;$2&amp;quot;, format_date_de(st))&lt;br /&gt;
      :gsub(&amp;quot;$3&amp;quot;, format_date_de(en_excl))&lt;br /&gt;
  else&lt;br /&gt;
    -- beendet (bis zu post_days danach)&lt;br /&gt;
    if isempty(PH_ENDED) then return &amp;quot;&amp;quot; end&lt;br /&gt;
    -- Labels aus i18n.sections (wie gehabt)&lt;br /&gt;
    local curr = ev_label(evkey, &amp;quot;currencies&amp;quot;)&lt;br /&gt;
    local shop = ev_label(evkey, &amp;quot;ex_shop&amp;quot;)&lt;br /&gt;
    if isempty(curr) or isempty(shop) then return &amp;quot;&amp;quot; end&lt;br /&gt;
    text = PH_ENDED&lt;br /&gt;
      :gsub(&amp;quot;$1&amp;quot;, name)&lt;br /&gt;
      :gsub(&amp;quot;$4&amp;quot;, curr)&lt;br /&gt;
      :gsub(&amp;quot;$5&amp;quot;, shop)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  local prefix = (ic ~= &amp;quot;&amp;quot; and (ic .. &amp;quot; &amp;quot;) or &amp;quot;&amp;quot;)&lt;br /&gt;
  local suffix = (ic ~= &amp;quot;&amp;quot; and (&amp;quot; &amp;quot; .. ic) or &amp;quot;&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
  return string.format(&lt;br /&gt;
    &#039;&amp;lt;div class=&amp;quot;fs-notice&amp;quot; data-ev=&amp;quot;%s&amp;quot;&amp;gt;%s%s%s&amp;lt;/div&amp;gt;&#039;,&lt;br /&gt;
    mw.text.encode(evkey),&lt;br /&gt;
    prefix, text, suffix&lt;br /&gt;
  )&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- ========== Dispatcher ==========&lt;br /&gt;
local function get_event(evkey)&lt;br /&gt;
  local evs = events()&lt;br /&gt;
  local real = pick_key(evs, evkey)&lt;br /&gt;
  return real and evs[real] or nil, real or evkey&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function dispatch_event(args)&lt;br /&gt;
  local evkey = args[2]&lt;br /&gt;
&lt;br /&gt;
  -- {{Firestone|Event|notice}}  → automatisch wählen&lt;br /&gt;
  if evkey and norm(evkey) == &amp;quot;notice&amp;quot; then&lt;br /&gt;
    local pick = pick_notice_eventkey()&lt;br /&gt;
    if not pick then return &amp;quot;&amp;quot; end&lt;br /&gt;
    local ev, r = get_event(pick); if not ev then return &amp;quot;&amp;quot; end&lt;br /&gt;
    return event_notice(ev, r)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  -- normaler Weg&lt;br /&gt;
  if isempty(evkey) then return &amp;quot;&amp;quot; end&lt;br /&gt;
  local ev, realkey = get_event(evkey); if not ev then return &amp;quot;&amp;quot; end&lt;br /&gt;
&lt;br /&gt;
  local a3, a4 = args[3], args[4]&lt;br /&gt;
  if isempty(a3) then return ev_name(realkey) end&lt;br /&gt;
&lt;br /&gt;
  if a3==&amp;quot;notice&amp;quot;  then return event_notice(ev, realkey)&lt;br /&gt;
  elseif a3==&amp;quot;file&amp;quot;   then return event_file(ev, realkey, a4 or &amp;quot;&amp;quot;)&lt;br /&gt;
  elseif a3==&amp;quot;link&amp;quot;   then return event_link(ev, realkey, a4 or &amp;quot;&amp;quot;)&lt;br /&gt;
  elseif a3==&amp;quot;h2&amp;quot;     then return htag(2, ev_label(realkey, a4 or &amp;quot;&amp;quot;))&lt;br /&gt;
  elseif a3==&amp;quot;h3&amp;quot;     then return htag(3, ev_label(realkey, a4 or &amp;quot;&amp;quot;))&lt;br /&gt;
  elseif a3==&amp;quot;history&amp;quot;  then return event_history(ev, realkey, a)&lt;br /&gt;
  elseif a3==&amp;quot;avatars&amp;quot;  then return event_avatars(ev, realkey)&lt;br /&gt;
  elseif a3==&amp;quot;exchange&amp;quot; then return event_exchange_tabs(ev, realkey)&lt;br /&gt;
  elseif a3==&amp;quot;offers&amp;quot;   then&lt;br /&gt;
    if a4==&amp;quot;start&amp;quot; then return event_offers(ev, realkey, &amp;quot;initial&amp;quot;)&lt;br /&gt;
    elseif a4==&amp;quot;more&amp;quot; then return event_offers(ev, realkey, &amp;quot;after&amp;quot;) end&lt;br /&gt;
    return &amp;quot;&amp;quot;&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  -- Sonderfälle für Währungsnamen aus i18n.sections&lt;br /&gt;
  if a3 == &amp;quot;currency&amp;quot;  then return ev_label(realkey, &amp;quot;currency&amp;quot;)  end&lt;br /&gt;
  if a3 == &amp;quot;currencies&amp;quot; then return ev_label(realkey, &amp;quot;currencies&amp;quot;) end&lt;br /&gt;
&lt;br /&gt;
  -- generischer Pfad in EventData&lt;br /&gt;
  local path, i = {}, 3&lt;br /&gt;
  while args[i] do table.insert(path, args[i]); i=i+1 end&lt;br /&gt;
  local v = deep_get(ev, path)&lt;br /&gt;
  if v ~= nil then&lt;br /&gt;
    local last = path[#path] and tostring(path[#path]) or &amp;quot;&amp;quot;&lt;br /&gt;
    v = autotr(last, v)&lt;br /&gt;
    return tostring(v)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  -- Fallback auf Labels/Name/Unlocks&lt;br /&gt;
  local last = norm(path[#path] or a3)&lt;br /&gt;
  if last==&amp;quot;eventname&amp;quot; or last==&amp;quot;name&amp;quot; then return ev_name(realkey) end&lt;br /&gt;
  local lbl = ev_label(realkey, last); if not isempty(lbl) then return lbl end&lt;br /&gt;
  local U = ((i18n().events or {}).unlocks or {})[last]&lt;br /&gt;
  if U then return U end&lt;br /&gt;
&lt;br /&gt;
  return &amp;quot;&amp;quot;&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function dispatch_hero(args)&lt;br /&gt;
  local name = args[2]; if isempty(name) then return &amp;quot;&amp;quot; end&lt;br /&gt;
  local H = heroes()&lt;br /&gt;
  local real = pick_key(H, name); if not real then return &amp;quot;&amp;quot; end&lt;br /&gt;
  local h = H[real]&lt;br /&gt;
  local path, i = {}, 3&lt;br /&gt;
  while args[i] do table.insert(path, args[i]); i=i+1 end&lt;br /&gt;
  local v = deep_get(h, path)&lt;br /&gt;
  local last = path[#path] and tostring(path[#path]) or &amp;quot;&amp;quot;&lt;br /&gt;
  v = autotr(last, v)&lt;br /&gt;
  return tostring(v or &amp;quot;&amp;quot;)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- ========== main ==========&lt;br /&gt;
function M.main(frame)&lt;br /&gt;
  FRAME = frame&lt;br /&gt;
  local a = getArgs(frame)&lt;br /&gt;
  for i=1,10 do a[i] = a[i] and tostring(a[i]) or nil end&lt;br /&gt;
  local dom = a[1] and norm(a[1]) or &amp;quot;&amp;quot;&lt;br /&gt;
  if dom==&amp;quot;event&amp;quot; then&lt;br /&gt;
    return dispatch_event(a)&lt;br /&gt;
  elseif dom==&amp;quot;hero&amp;quot; then&lt;br /&gt;
    return dispatch_hero(a)&lt;br /&gt;
  else&lt;br /&gt;
    return &amp;quot;&amp;quot;&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
return M&lt;/div&gt;</summary>
		<author><name>Webmaster</name></author>
	</entry>
	<entry>
		<id>https://firestone-idle-rpg.de/index.php?title=Vorlage:EventPage&amp;diff=3765</id>
		<title>Vorlage:EventPage</title>
		<link rel="alternate" type="text/html" href="https://firestone-idle-rpg.de/index.php?title=Vorlage:EventPage&amp;diff=3765"/>
		<updated>2025-10-28T11:59:53Z</updated>

		<summary type="html">&lt;p&gt;Webmaster: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox/Event&lt;br /&gt;
|banner={{Firestone|Event|{{{name|}}}|banner}}&lt;br /&gt;
|caption=&lt;br /&gt;
|type={{Firestone|Event|{{{name|}}}|type}}&lt;br /&gt;
|date={{Firestone|Event|{{{name|}}}|month}}&lt;br /&gt;
|duration={{Firestone|Event|{{{name|}}}|duration}}&lt;br /&gt;
|unlocksat={{Firestone|Event|{{{name|}}}|character_level_10}}}}&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|file|deco}}&lt;br /&gt;
Das {{Firestone|Event|{{{name|}}}|eventname}} ist ein {{Firestone|Event|{{{name|}}}|type}}, das im {{Firestone|Event|{{{name|}}}|month}} startet und {{Firestone|Event|{{{name|}}}|duration}} dauert. &lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;overflow:auto;&amp;quot;&amp;gt;Frühere Ausgaben waren:&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;{{Firestone|Event|{{{name|}}}|history}}&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In diesem Event lässt {{Firestone|Event|{{{name|}}}|fullname}}, {{Firestone|Event|{{{name|}}}|currencies}} auf dem Schlachtfeld fallen. Diese können anschließend in der {{Firestone|Event|{{{name|}}}|ex_shop}} gegen [[Gear Chests|Ausrüstungstruhen]], [[Jewel Chests|Juwelentruhen]], bestimmte [[currencies|Währungen]] und einzigartige [[avatar|Avatare]] eingetauscht werden. Zusätzlich ist die Stadt mit Halloween-Dekoration geschmückt. &lt;br /&gt;
&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|h2|currencies}}&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|file|fullname}}&lt;br /&gt;
&lt;br /&gt;
Während des Events lässt {{Firestone|Event|{{{name|}}}|fullname}}, {{Firestone|Event|{{{name|}}}|currencies}} auf dem Schlachtfeld fallen – mit einer Rate von 1 {{Firestone|Event|{{{name|}}}|currency}} alle 28,8 Sekunden. Die {{Firestone|Event|{{{name|}}}|currencies}} werden automatisch eingesammelt, was zu einem fixen Zuwachs von 3.000 {{Firestone|Event|{{{name|}}}|currencies}} pro Tag führt. {{Firestone|Event|{{{name|}}}|currencies}} werden auch gesammelt, während du offline bist, und werden dir gutgeschrieben, wenn {{Firestone|Event|{{{name|}}}|shortname}} dich nach dem erneuten Einloggen das erste Mal besucht. Offline-Gewinne sind jedoch auf 24 Stunden begrenzt.&lt;br /&gt;
&lt;br /&gt;
Wenn du einen Einkauf im {{Firestone|Event|{{{name|}}}|shopname}} tätigst (siehe unten), erhöht sich die Drop-Rate für den Rest des Events auf 2 {{Firestone|Event|{{{name|}}}|currencies}} alle 28,8 Sekunden.&lt;br /&gt;
&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|h2|ex_shop}}&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|file|ex_shop}}&lt;br /&gt;
&lt;br /&gt;
Die {{Firestone|Event|{{{name|}}}|ex_shop}} ersetzt den Brunnen in der Stadt und ist nur während des Halloween-Events verfügbar. Nach Ende des Events verschwindet die {{Firestone|Event|{{{name|}}}|ex_shop}} wieder und deine verbleibenden {{Firestone|Event|{{{name|}}}|currencies}} gehen verloren. Die {{Firestone|Event|{{{name|}}}|ex_shop}} besteht aus den folgenden drei Bereichen.&lt;br /&gt;
&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|h3|currency_exchange}}&lt;br /&gt;
&lt;br /&gt;
Im {{Firestone|Event|{{{name|}}}|currency_exchange}} kannst du {{Firestone|Event|{{{name|}}}|currencies}} gegen Truhen und [[currencies|Währungen]] eintauschen. Die Art der [[Gear Chests|Ausrüstungstruhe]] hängt von deiner [[character level|Charakterstufe]] ab und die Art der [[Jewel Chests|Juwelentruhe]] von der Anzahl deiner [[campaign stars|Kampagnensterne]]. Die Anzahl der Käufe ist für jeden angebotenen Truhen- und Währungstyp begrenzt.&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|exchange}}&lt;br /&gt;
&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|h3|avatar_exchange}}&lt;br /&gt;
&lt;br /&gt;
Bei »{{Firestone|Event|{{{name|}}}|avatar_exchange}}« kannst du {{Firestone|Event|{{{name|}}}|currencies}} gegen einzigartige [[avatar|Avatare]] eintauschen, die nur während des {{Firestone|Event|{{{name|}}}|name}} erhältlich sind.&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|avatars}}&lt;br /&gt;
&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|h3|shop}}&lt;br /&gt;
&lt;br /&gt;
Im {{Firestone|Event|{{{name|}}}|shopname}} kannst du {{Firestone|Event|{{{name|}}}|currencies}} mit [[Platforms|Plattform]]-Währung kaufen. Jeder Kauf im {{Firestone|Event|{{{name|}}}|shopname}} erhöht die Drop-Rate der {{Firestone|Event|{{{name|}}}|currencies}} auf dem Schlachtfeld für den Rest des Events auf 2 {{Firestone|Event|{{{name|}}}|currencies}} alle 28,8 Sekunden. Jedes Angebot kann nur einmal erworben werden.&lt;br /&gt;
&lt;br /&gt;
Anfangs werden die folgenden 3 Angebote angezeigt:&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|offers|start}}&lt;br /&gt;
&lt;br /&gt;
Sobald die Angebote 1–3 alle gekauft wurden, erscheinen die folgenden Angebote:&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|offers|more}}&lt;br /&gt;
&lt;br /&gt;
{{Navigation}}&lt;br /&gt;
[[Category:Browse]]&lt;br /&gt;
[[Category:Events]]&lt;/div&gt;</summary>
		<author><name>Webmaster</name></author>
	</entry>
	<entry>
		<id>https://firestone-idle-rpg.de/index.php?title=Vorlage:EventPage&amp;diff=3764</id>
		<title>Vorlage:EventPage</title>
		<link rel="alternate" type="text/html" href="https://firestone-idle-rpg.de/index.php?title=Vorlage:EventPage&amp;diff=3764"/>
		<updated>2025-10-28T11:57:37Z</updated>

		<summary type="html">&lt;p&gt;Webmaster: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox/Event&lt;br /&gt;
|banner={{Firestone|Event|{{{name|}}}|banner}}&lt;br /&gt;
|caption=&lt;br /&gt;
|type={{Firestone|Event|{{{name|}}}|type}}&lt;br /&gt;
|date={{Firestone|Event|{{{name|}}}|month}}&lt;br /&gt;
|duration={{Firestone|Event|{{{name|}}}|duration}}&lt;br /&gt;
|unlocksat={{Firestone|Event|{{{name|}}}|character_level_10}}}}&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|file|deco}}&lt;br /&gt;
Das {{Firestone|Event|{{{name|}}}|eventname}} ist ein {{Firestone|Event|{{{name|}}}|type}}, das im {{Firestone|Event|{{{name|}}}|month}} startet und {{Firestone|Event|{{{name|}}}|duration}} dauert. &amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;overflow:auto;&amp;quot;&amp;gt;&lt;br /&gt;
Frühere Ausgaben waren:&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;{{Firestone|Event|{{{name|}}}|history}}&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In diesem Event lässt {{Firestone|Event|{{{name|}}}|fullname}}, {{Firestone|Event|{{{name|}}}|currencies}} auf dem Schlachtfeld fallen. Diese können anschließend in der {{Firestone|Event|{{{name|}}}|ex_shop}} gegen [[Gear Chests|Ausrüstungstruhen]], [[Jewel Chests|Juwelentruhen]], bestimmte [[currencies|Währungen]] und einzigartige [[avatar|Avatare]] eingetauscht werden. Zusätzlich ist die Stadt mit Halloween-Dekoration geschmückt. &lt;br /&gt;
&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|h2|currencies}}&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|file|fullname}}&lt;br /&gt;
&lt;br /&gt;
Während des Events lässt {{Firestone|Event|{{{name|}}}|fullname}}, {{Firestone|Event|{{{name|}}}|currencies}} auf dem Schlachtfeld fallen – mit einer Rate von 1 {{Firestone|Event|{{{name|}}}|currency}} alle 28,8 Sekunden. Die {{Firestone|Event|{{{name|}}}|currencies}} werden automatisch eingesammelt, was zu einem fixen Zuwachs von 3.000 {{Firestone|Event|{{{name|}}}|currencies}} pro Tag führt. {{Firestone|Event|{{{name|}}}|currencies}} werden auch gesammelt, während du offline bist, und werden dir gutgeschrieben, wenn {{Firestone|Event|{{{name|}}}|shortname}} dich nach dem erneuten Einloggen das erste Mal besucht. Offline-Gewinne sind jedoch auf 24 Stunden begrenzt.&lt;br /&gt;
&lt;br /&gt;
Wenn du einen Einkauf im {{Firestone|Event|{{{name|}}}|shopname}} tätigst (siehe unten), erhöht sich die Drop-Rate für den Rest des Events auf 2 {{Firestone|Event|{{{name|}}}|currencies}} alle 28,8 Sekunden.&lt;br /&gt;
&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|h2|ex_shop}}&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|file|ex_shop}}&lt;br /&gt;
&lt;br /&gt;
Die {{Firestone|Event|{{{name|}}}|ex_shop}} ersetzt den Brunnen in der Stadt und ist nur während des Halloween-Events verfügbar. Nach Ende des Events verschwindet die {{Firestone|Event|{{{name|}}}|ex_shop}} wieder und deine verbleibenden {{Firestone|Event|{{{name|}}}|currencies}} gehen verloren. Die {{Firestone|Event|{{{name|}}}|ex_shop}} besteht aus den folgenden drei Bereichen.&lt;br /&gt;
&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|h3|currency_exchange}}&lt;br /&gt;
&lt;br /&gt;
Im {{Firestone|Event|{{{name|}}}|currency_exchange}} kannst du {{Firestone|Event|{{{name|}}}|currencies}} gegen Truhen und [[currencies|Währungen]] eintauschen. Die Art der [[Gear Chests|Ausrüstungstruhe]] hängt von deiner [[character level|Charakterstufe]] ab und die Art der [[Jewel Chests|Juwelentruhe]] von der Anzahl deiner [[campaign stars|Kampagnensterne]]. Die Anzahl der Käufe ist für jeden angebotenen Truhen- und Währungstyp begrenzt.&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|exchange}}&lt;br /&gt;
&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|h3|avatar_exchange}}&lt;br /&gt;
&lt;br /&gt;
Bei »{{Firestone|Event|{{{name|}}}|avatar_exchange}}« kannst du {{Firestone|Event|{{{name|}}}|currencies}} gegen einzigartige [[avatar|Avatare]] eintauschen, die nur während des {{Firestone|Event|{{{name|}}}|name}} erhältlich sind.&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|avatars}}&lt;br /&gt;
&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|h3|shop}}&lt;br /&gt;
&lt;br /&gt;
Im {{Firestone|Event|{{{name|}}}|shopname}} kannst du {{Firestone|Event|{{{name|}}}|currencies}} mit [[Platforms|Plattform]]-Währung kaufen. Jeder Kauf im {{Firestone|Event|{{{name|}}}|shopname}} erhöht die Drop-Rate der {{Firestone|Event|{{{name|}}}|currencies}} auf dem Schlachtfeld für den Rest des Events auf 2 {{Firestone|Event|{{{name|}}}|currencies}} alle 28,8 Sekunden. Jedes Angebot kann nur einmal erworben werden.&lt;br /&gt;
&lt;br /&gt;
Anfangs werden die folgenden 3 Angebote angezeigt:&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|offers|start}}&lt;br /&gt;
&lt;br /&gt;
Sobald die Angebote 1–3 alle gekauft wurden, erscheinen die folgenden Angebote:&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|offers|more}}&lt;br /&gt;
&lt;br /&gt;
{{Navigation}}&lt;br /&gt;
[[Category:Browse]]&lt;br /&gt;
[[Category:Events]]&lt;/div&gt;</summary>
		<author><name>Webmaster</name></author>
	</entry>
	<entry>
		<id>https://firestone-idle-rpg.de/index.php?title=Vorlage:EventPage&amp;diff=3763</id>
		<title>Vorlage:EventPage</title>
		<link rel="alternate" type="text/html" href="https://firestone-idle-rpg.de/index.php?title=Vorlage:EventPage&amp;diff=3763"/>
		<updated>2025-10-28T11:48:27Z</updated>

		<summary type="html">&lt;p&gt;Webmaster: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox/Event&lt;br /&gt;
|banner={{Firestone|Event|{{{name|}}}|banner}}&lt;br /&gt;
|caption=&lt;br /&gt;
|type={{Firestone|Event|{{{name|}}}|type}}&lt;br /&gt;
|date={{Firestone|Event|{{{name|}}}|month}}&lt;br /&gt;
|duration={{Firestone|Event|{{{name|}}}|duration}}&lt;br /&gt;
|unlocksat={{Firestone|Event|{{{name|}}}|character_level_10}}}}&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|file|deco}}&lt;br /&gt;
Das {{Firestone|Event|{{{name|}}}|eventname}} ist ein {{Firestone|Event|{{{name|}}}|type}}, das im {{Firestone|Event|{{{name|}}}|month}} startet und {{Firestone|Event|{{{name|}}}|duration}} dauert. Frühere Ausgaben waren: &amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width:400px; overflow:auto;&amp;quot;&amp;gt;&lt;br /&gt;
This text is not collapsible; but the next is collapsible and hidden by default:&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;{{Lorem}}&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|history|id=fs-hist-{{{name|}}}}}&lt;br /&gt;
&lt;br /&gt;
In diesem Event lässt {{Firestone|Event|{{{name|}}}|fullname}}, {{Firestone|Event|{{{name|}}}|currencies}} auf dem Schlachtfeld fallen. Diese können anschließend in der {{Firestone|Event|{{{name|}}}|ex_shop}} gegen [[Gear Chests|Ausrüstungstruhen]], [[Jewel Chests|Juwelentruhen]], bestimmte [[currencies|Währungen]] und einzigartige [[avatar|Avatare]] eingetauscht werden. Zusätzlich ist die Stadt mit Halloween-Dekoration geschmückt. &lt;br /&gt;
&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|h2|currencies}}&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|file|fullname}}&lt;br /&gt;
&lt;br /&gt;
Während des Events lässt {{Firestone|Event|{{{name|}}}|fullname}}, {{Firestone|Event|{{{name|}}}|currencies}} auf dem Schlachtfeld fallen – mit einer Rate von 1 {{Firestone|Event|{{{name|}}}|currency}} alle 28,8 Sekunden. Die {{Firestone|Event|{{{name|}}}|currencies}} werden automatisch eingesammelt, was zu einem fixen Zuwachs von 3.000 {{Firestone|Event|{{{name|}}}|currencies}} pro Tag führt. {{Firestone|Event|{{{name|}}}|currencies}} werden auch gesammelt, während du offline bist, und werden dir gutgeschrieben, wenn {{Firestone|Event|{{{name|}}}|shortname}} dich nach dem erneuten Einloggen das erste Mal besucht. Offline-Gewinne sind jedoch auf 24 Stunden begrenzt.&lt;br /&gt;
&lt;br /&gt;
Wenn du einen Einkauf im {{Firestone|Event|{{{name|}}}|shopname}} tätigst (siehe unten), erhöht sich die Drop-Rate für den Rest des Events auf 2 {{Firestone|Event|{{{name|}}}|currencies}} alle 28,8 Sekunden.&lt;br /&gt;
&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|h2|ex_shop}}&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|file|ex_shop}}&lt;br /&gt;
&lt;br /&gt;
Die {{Firestone|Event|{{{name|}}}|ex_shop}} ersetzt den Brunnen in der Stadt und ist nur während des Halloween-Events verfügbar. Nach Ende des Events verschwindet die {{Firestone|Event|{{{name|}}}|ex_shop}} wieder und deine verbleibenden {{Firestone|Event|{{{name|}}}|currencies}} gehen verloren. Die {{Firestone|Event|{{{name|}}}|ex_shop}} besteht aus den folgenden drei Bereichen.&lt;br /&gt;
&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|h3|currency_exchange}}&lt;br /&gt;
&lt;br /&gt;
Im {{Firestone|Event|{{{name|}}}|currency_exchange}} kannst du {{Firestone|Event|{{{name|}}}|currencies}} gegen Truhen und [[currencies|Währungen]] eintauschen. Die Art der [[Gear Chests|Ausrüstungstruhe]] hängt von deiner [[character level|Charakterstufe]] ab und die Art der [[Jewel Chests|Juwelentruhe]] von der Anzahl deiner [[campaign stars|Kampagnensterne]]. Die Anzahl der Käufe ist für jeden angebotenen Truhen- und Währungstyp begrenzt.&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|exchange}}&lt;br /&gt;
&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|h3|avatar_exchange}}&lt;br /&gt;
&lt;br /&gt;
Bei »{{Firestone|Event|{{{name|}}}|avatar_exchange}}« kannst du {{Firestone|Event|{{{name|}}}|currencies}} gegen einzigartige [[avatar|Avatare]] eintauschen, die nur während des {{Firestone|Event|{{{name|}}}|name}} erhältlich sind.&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|avatars}}&lt;br /&gt;
&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|h3|shop}}&lt;br /&gt;
&lt;br /&gt;
Im {{Firestone|Event|{{{name|}}}|shopname}} kannst du {{Firestone|Event|{{{name|}}}|currencies}} mit [[Platforms|Plattform]]-Währung kaufen. Jeder Kauf im {{Firestone|Event|{{{name|}}}|shopname}} erhöht die Drop-Rate der {{Firestone|Event|{{{name|}}}|currencies}} auf dem Schlachtfeld für den Rest des Events auf 2 {{Firestone|Event|{{{name|}}}|currencies}} alle 28,8 Sekunden. Jedes Angebot kann nur einmal erworben werden.&lt;br /&gt;
&lt;br /&gt;
Anfangs werden die folgenden 3 Angebote angezeigt:&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|offers|start}}&lt;br /&gt;
&lt;br /&gt;
Sobald die Angebote 1–3 alle gekauft wurden, erscheinen die folgenden Angebote:&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|offers|more}}&lt;br /&gt;
&lt;br /&gt;
{{Navigation}}&lt;br /&gt;
[[Category:Browse]]&lt;br /&gt;
[[Category:Events]]&lt;/div&gt;</summary>
		<author><name>Webmaster</name></author>
	</entry>
	<entry>
		<id>https://firestone-idle-rpg.de/index.php?title=Vorlage:EventPage&amp;diff=3762</id>
		<title>Vorlage:EventPage</title>
		<link rel="alternate" type="text/html" href="https://firestone-idle-rpg.de/index.php?title=Vorlage:EventPage&amp;diff=3762"/>
		<updated>2025-10-28T11:46:42Z</updated>

		<summary type="html">&lt;p&gt;Webmaster: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox/Event&lt;br /&gt;
|banner={{Firestone|Event|{{{name|}}}|banner}}&lt;br /&gt;
|caption=&lt;br /&gt;
|type={{Firestone|Event|{{{name|}}}|type}}&lt;br /&gt;
|date={{Firestone|Event|{{{name|}}}|month}}&lt;br /&gt;
|duration={{Firestone|Event|{{{name|}}}|duration}}&lt;br /&gt;
|unlocksat={{Firestone|Event|{{{name|}}}|character_level_10}}}}&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|file|deco}}&lt;br /&gt;
Das {{Firestone|Event|{{{name|}}}|eventname}} ist ein {{Firestone|Event|{{{name|}}}|type}}, das im {{Firestone|Event|{{{name|}}}|month}} startet und {{Firestone|Event|{{{name|}}}|duration}} dauert. Frühere Ausgaben waren: &amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
This text is not collapsible; but the next is collapsible and hidden by default:&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;{{Lorem}}&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|history|id=fs-hist-{{{name|}}}}}&lt;br /&gt;
&lt;br /&gt;
In diesem Event lässt {{Firestone|Event|{{{name|}}}|fullname}}, {{Firestone|Event|{{{name|}}}|currencies}} auf dem Schlachtfeld fallen. Diese können anschließend in der {{Firestone|Event|{{{name|}}}|ex_shop}} gegen [[Gear Chests|Ausrüstungstruhen]], [[Jewel Chests|Juwelentruhen]], bestimmte [[currencies|Währungen]] und einzigartige [[avatar|Avatare]] eingetauscht werden. Zusätzlich ist die Stadt mit Halloween-Dekoration geschmückt. &lt;br /&gt;
&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|h2|currencies}}&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|file|fullname}}&lt;br /&gt;
&lt;br /&gt;
Während des Events lässt {{Firestone|Event|{{{name|}}}|fullname}}, {{Firestone|Event|{{{name|}}}|currencies}} auf dem Schlachtfeld fallen – mit einer Rate von 1 {{Firestone|Event|{{{name|}}}|currency}} alle 28,8 Sekunden. Die {{Firestone|Event|{{{name|}}}|currencies}} werden automatisch eingesammelt, was zu einem fixen Zuwachs von 3.000 {{Firestone|Event|{{{name|}}}|currencies}} pro Tag führt. {{Firestone|Event|{{{name|}}}|currencies}} werden auch gesammelt, während du offline bist, und werden dir gutgeschrieben, wenn {{Firestone|Event|{{{name|}}}|shortname}} dich nach dem erneuten Einloggen das erste Mal besucht. Offline-Gewinne sind jedoch auf 24 Stunden begrenzt.&lt;br /&gt;
&lt;br /&gt;
Wenn du einen Einkauf im {{Firestone|Event|{{{name|}}}|shopname}} tätigst (siehe unten), erhöht sich die Drop-Rate für den Rest des Events auf 2 {{Firestone|Event|{{{name|}}}|currencies}} alle 28,8 Sekunden.&lt;br /&gt;
&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|h2|ex_shop}}&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|file|ex_shop}}&lt;br /&gt;
&lt;br /&gt;
Die {{Firestone|Event|{{{name|}}}|ex_shop}} ersetzt den Brunnen in der Stadt und ist nur während des Halloween-Events verfügbar. Nach Ende des Events verschwindet die {{Firestone|Event|{{{name|}}}|ex_shop}} wieder und deine verbleibenden {{Firestone|Event|{{{name|}}}|currencies}} gehen verloren. Die {{Firestone|Event|{{{name|}}}|ex_shop}} besteht aus den folgenden drei Bereichen.&lt;br /&gt;
&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|h3|currency_exchange}}&lt;br /&gt;
&lt;br /&gt;
Im {{Firestone|Event|{{{name|}}}|currency_exchange}} kannst du {{Firestone|Event|{{{name|}}}|currencies}} gegen Truhen und [[currencies|Währungen]] eintauschen. Die Art der [[Gear Chests|Ausrüstungstruhe]] hängt von deiner [[character level|Charakterstufe]] ab und die Art der [[Jewel Chests|Juwelentruhe]] von der Anzahl deiner [[campaign stars|Kampagnensterne]]. Die Anzahl der Käufe ist für jeden angebotenen Truhen- und Währungstyp begrenzt.&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|exchange}}&lt;br /&gt;
&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|h3|avatar_exchange}}&lt;br /&gt;
&lt;br /&gt;
Bei »{{Firestone|Event|{{{name|}}}|avatar_exchange}}« kannst du {{Firestone|Event|{{{name|}}}|currencies}} gegen einzigartige [[avatar|Avatare]] eintauschen, die nur während des {{Firestone|Event|{{{name|}}}|name}} erhältlich sind.&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|avatars}}&lt;br /&gt;
&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|h3|shop}}&lt;br /&gt;
&lt;br /&gt;
Im {{Firestone|Event|{{{name|}}}|shopname}} kannst du {{Firestone|Event|{{{name|}}}|currencies}} mit [[Platforms|Plattform]]-Währung kaufen. Jeder Kauf im {{Firestone|Event|{{{name|}}}|shopname}} erhöht die Drop-Rate der {{Firestone|Event|{{{name|}}}|currencies}} auf dem Schlachtfeld für den Rest des Events auf 2 {{Firestone|Event|{{{name|}}}|currencies}} alle 28,8 Sekunden. Jedes Angebot kann nur einmal erworben werden.&lt;br /&gt;
&lt;br /&gt;
Anfangs werden die folgenden 3 Angebote angezeigt:&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|offers|start}}&lt;br /&gt;
&lt;br /&gt;
Sobald die Angebote 1–3 alle gekauft wurden, erscheinen die folgenden Angebote:&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|offers|more}}&lt;br /&gt;
&lt;br /&gt;
{{Navigation}}&lt;br /&gt;
[[Category:Browse]]&lt;br /&gt;
[[Category:Events]]&lt;/div&gt;</summary>
		<author><name>Webmaster</name></author>
	</entry>
	<entry>
		<id>https://firestone-idle-rpg.de/index.php?title=Vorlage:EventPage&amp;diff=3761</id>
		<title>Vorlage:EventPage</title>
		<link rel="alternate" type="text/html" href="https://firestone-idle-rpg.de/index.php?title=Vorlage:EventPage&amp;diff=3761"/>
		<updated>2025-10-28T10:57:03Z</updated>

		<summary type="html">&lt;p&gt;Webmaster: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox/Event&lt;br /&gt;
|banner={{Firestone|Event|{{{name|}}}|banner}}&lt;br /&gt;
|caption=&lt;br /&gt;
|type={{Firestone|Event|{{{name|}}}|type}}&lt;br /&gt;
|date={{Firestone|Event|{{{name|}}}|month}}&lt;br /&gt;
|duration={{Firestone|Event|{{{name|}}}|duration}}&lt;br /&gt;
|unlocksat={{Firestone|Event|{{{name|}}}|character_level_10}}}}&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|file|deco}}&lt;br /&gt;
Das {{Firestone|Event|{{{name|}}}|eventname}} ist ein {{Firestone|Event|{{{name|}}}|type}}, das im {{Firestone|Event|{{{name|}}}|month}} startet und {{Firestone|Event|{{{name|}}}|duration}} dauert. Frühere Ausgaben waren: {{#tag:span|Anzeigen&lt;br /&gt;
 | class=&amp;quot;mw-collapsible-toggle fs-toggle&amp;quot;&lt;br /&gt;
 | data-mw-collapsible-target=&amp;quot;#fs-hist-{{{name|}}}&amp;quot;&lt;br /&gt;
 | data-expandtext=&amp;quot;Anzeigen&amp;quot;&lt;br /&gt;
 | data-collapsetext=&amp;quot;Verbergen&amp;quot;&lt;br /&gt;
}}&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|history|id=fs-hist-{{{name|}}}}}&lt;br /&gt;
&lt;br /&gt;
In diesem Event lässt {{Firestone|Event|{{{name|}}}|fullname}}, {{Firestone|Event|{{{name|}}}|currencies}} auf dem Schlachtfeld fallen. Diese können anschließend in der {{Firestone|Event|{{{name|}}}|ex_shop}} gegen [[Gear Chests|Ausrüstungstruhen]], [[Jewel Chests|Juwelentruhen]], bestimmte [[currencies|Währungen]] und einzigartige [[avatar|Avatare]] eingetauscht werden. Zusätzlich ist die Stadt mit Halloween-Dekoration geschmückt. &lt;br /&gt;
&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|h2|currencies}}&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|file|fullname}}&lt;br /&gt;
&lt;br /&gt;
Während des Events lässt {{Firestone|Event|{{{name|}}}|fullname}}, {{Firestone|Event|{{{name|}}}|currencies}} auf dem Schlachtfeld fallen – mit einer Rate von 1 {{Firestone|Event|{{{name|}}}|currency}} alle 28,8 Sekunden. Die {{Firestone|Event|{{{name|}}}|currencies}} werden automatisch eingesammelt, was zu einem fixen Zuwachs von 3.000 {{Firestone|Event|{{{name|}}}|currencies}} pro Tag führt. {{Firestone|Event|{{{name|}}}|currencies}} werden auch gesammelt, während du offline bist, und werden dir gutgeschrieben, wenn {{Firestone|Event|{{{name|}}}|shortname}} dich nach dem erneuten Einloggen das erste Mal besucht. Offline-Gewinne sind jedoch auf 24 Stunden begrenzt.&lt;br /&gt;
&lt;br /&gt;
Wenn du einen Einkauf im {{Firestone|Event|{{{name|}}}|shopname}} tätigst (siehe unten), erhöht sich die Drop-Rate für den Rest des Events auf 2 {{Firestone|Event|{{{name|}}}|currencies}} alle 28,8 Sekunden.&lt;br /&gt;
&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|h2|ex_shop}}&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|file|ex_shop}}&lt;br /&gt;
&lt;br /&gt;
Die {{Firestone|Event|{{{name|}}}|ex_shop}} ersetzt den Brunnen in der Stadt und ist nur während des Halloween-Events verfügbar. Nach Ende des Events verschwindet die {{Firestone|Event|{{{name|}}}|ex_shop}} wieder und deine verbleibenden {{Firestone|Event|{{{name|}}}|currencies}} gehen verloren. Die {{Firestone|Event|{{{name|}}}|ex_shop}} besteht aus den folgenden drei Bereichen.&lt;br /&gt;
&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|h3|currency_exchange}}&lt;br /&gt;
&lt;br /&gt;
Im {{Firestone|Event|{{{name|}}}|currency_exchange}} kannst du {{Firestone|Event|{{{name|}}}|currencies}} gegen Truhen und [[currencies|Währungen]] eintauschen. Die Art der [[Gear Chests|Ausrüstungstruhe]] hängt von deiner [[character level|Charakterstufe]] ab und die Art der [[Jewel Chests|Juwelentruhe]] von der Anzahl deiner [[campaign stars|Kampagnensterne]]. Die Anzahl der Käufe ist für jeden angebotenen Truhen- und Währungstyp begrenzt.&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|exchange}}&lt;br /&gt;
&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|h3|avatar_exchange}}&lt;br /&gt;
&lt;br /&gt;
Bei »{{Firestone|Event|{{{name|}}}|avatar_exchange}}« kannst du {{Firestone|Event|{{{name|}}}|currencies}} gegen einzigartige [[avatar|Avatare]] eintauschen, die nur während des {{Firestone|Event|{{{name|}}}|name}} erhältlich sind.&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|avatars}}&lt;br /&gt;
&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|h3|shop}}&lt;br /&gt;
&lt;br /&gt;
Im {{Firestone|Event|{{{name|}}}|shopname}} kannst du {{Firestone|Event|{{{name|}}}|currencies}} mit [[Platforms|Plattform]]-Währung kaufen. Jeder Kauf im {{Firestone|Event|{{{name|}}}|shopname}} erhöht die Drop-Rate der {{Firestone|Event|{{{name|}}}|currencies}} auf dem Schlachtfeld für den Rest des Events auf 2 {{Firestone|Event|{{{name|}}}|currencies}} alle 28,8 Sekunden. Jedes Angebot kann nur einmal erworben werden.&lt;br /&gt;
&lt;br /&gt;
Anfangs werden die folgenden 3 Angebote angezeigt:&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|offers|start}}&lt;br /&gt;
&lt;br /&gt;
Sobald die Angebote 1–3 alle gekauft wurden, erscheinen die folgenden Angebote:&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|offers|more}}&lt;br /&gt;
&lt;br /&gt;
{{Navigation}}&lt;br /&gt;
[[Category:Browse]]&lt;br /&gt;
[[Category:Events]]&lt;/div&gt;</summary>
		<author><name>Webmaster</name></author>
	</entry>
	<entry>
		<id>https://firestone-idle-rpg.de/index.php?title=Modul:Firestone&amp;diff=3760</id>
		<title>Modul:Firestone</title>
		<link rel="alternate" type="text/html" href="https://firestone-idle-rpg.de/index.php?title=Modul:Firestone&amp;diff=3760"/>
		<updated>2025-10-28T10:55:39Z</updated>

		<summary type="html">&lt;p&gt;Webmaster: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;-- Modul:Firestone&lt;br /&gt;
local M = {}&lt;br /&gt;
&lt;br /&gt;
-- ========== kleine Helfer ==========&lt;br /&gt;
local COL_OFFER, COL_PRICE, COL_LIMIT = &amp;quot;50%&amp;quot;, &amp;quot;25%&amp;quot;, &amp;quot;25%&amp;quot;&lt;br /&gt;
local DAY = 86400&lt;br /&gt;
local function parse_iso_date(s)&lt;br /&gt;
  local y,m,d = tostring(s or &amp;quot;&amp;quot;):match(&amp;quot;^(%d%d%d%d)%-(%d%d)%-(%d%d)$&amp;quot;)&lt;br /&gt;
  if not y then return nil end&lt;br /&gt;
  return os.time{ year=tonumber(y), month=tonumber(m), day=tonumber(d), hour=12 }&lt;br /&gt;
end&lt;br /&gt;
local function ts_to_mw(ts) return os.date(&amp;quot;!%Y%m%d%H%M%S&amp;quot;, ts) end&lt;br /&gt;
local function format_date_de(v)&lt;br /&gt;
  local ts = (type(v)==&amp;quot;number&amp;quot;) and v or parse_iso_date(v)&lt;br /&gt;
  if not ts then return &amp;quot;&amp;quot; end&lt;br /&gt;
  local lang = mw.getContentLanguage()&lt;br /&gt;
  return lang:formatDate(&amp;quot;d.m.Y&amp;quot;, ts_to_mw(ts))&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function isempty(v) return v == nil or v == &amp;quot;&amp;quot; end&lt;br /&gt;
local function norm(s)&lt;br /&gt;
  if s == nil then return &amp;quot;&amp;quot; end&lt;br /&gt;
  s = mw.text.trim(tostring(s))&lt;br /&gt;
  return mw.ustring.lower(s):gsub(&amp;quot;%s+&amp;quot;,&amp;quot; &amp;quot;)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- bevorzugt Parent-Args (Vorlagen)&lt;br /&gt;
local function getArgs(frame)&lt;br /&gt;
  local p = frame:getParent()&lt;br /&gt;
  local src = (p and p.args) or frame.args or {}&lt;br /&gt;
  local a = {}&lt;br /&gt;
  for k,v in pairs(src) do&lt;br /&gt;
    if v ~= nil and v ~= &amp;quot;&amp;quot; then a[k] = mw.text.trim(tostring(v)) end&lt;br /&gt;
  end&lt;br /&gt;
  return a&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function has_items(t)&lt;br /&gt;
  if type(t) ~= &amp;quot;table&amp;quot; then return false end&lt;br /&gt;
  for _ in pairs(t) do return true end&lt;br /&gt;
  return false&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function pick_key(tbl, key)&lt;br /&gt;
  if type(tbl) ~= &amp;quot;table&amp;quot; then return nil end&lt;br /&gt;
  if tbl[key] ~= nil then return key end&lt;br /&gt;
  local k2 = tostring(key)&lt;br /&gt;
  if tbl[k2] ~= nil then return k2 end&lt;br /&gt;
  local kn = norm(k2)&lt;br /&gt;
  for k,_ in pairs(tbl) do if norm(k) == kn then return k end end&lt;br /&gt;
  return nil&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function deep_get(tbl, path)&lt;br /&gt;
  local cur = tbl&lt;br /&gt;
  for _,k in ipairs(path) do&lt;br /&gt;
    if type(cur) ~= &amp;quot;table&amp;quot; then return nil end&lt;br /&gt;
    local real = pick_key(cur, k); if not real then return nil end&lt;br /&gt;
    cur = cur[real]&lt;br /&gt;
  end&lt;br /&gt;
  return cur&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function fileTag(file, opts) -- [[Datei:...]]&lt;br /&gt;
  if isempty(file) then return &amp;quot;&amp;quot; end&lt;br /&gt;
  local parts = { &amp;quot;Datei:&amp;quot;..file }&lt;br /&gt;
  if opts and opts.size  then table.insert(parts, opts.size)  end&lt;br /&gt;
  if opts and opts.param then table.insert(parts, opts.param) end -- z.B. &amp;quot;thumb|Text&amp;quot;&lt;br /&gt;
  return string.format(&amp;quot;[[%s]]&amp;quot;, table.concat(parts, &amp;quot;|&amp;quot;))&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function pretty_range(s)&lt;br /&gt;
  if not s or s==&amp;quot;&amp;quot; then return &amp;quot;&amp;quot; end&lt;br /&gt;
  s = tostring(s)&lt;br /&gt;
  if s:sub(-1) == &amp;quot;-&amp;quot; then return s:sub(1, -2) .. &amp;quot;+&amp;quot; end&lt;br /&gt;
  return s:gsub(&amp;quot;%-&amp;quot;,&amp;quot;–&amp;quot;)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- ========== Lazy-Loads ==========&lt;br /&gt;
local I18N, EVENTS, HEROES&lt;br /&gt;
local function i18n()&lt;br /&gt;
  if I18N then return I18N end&lt;br /&gt;
  for _,t in ipairs{ &amp;quot;Modul:HeroI18n&amp;quot;, &amp;quot;Module:HeroI18n&amp;quot; } do&lt;br /&gt;
    local ok, data = pcall(mw.loadData, t) ; if ok and type(data)==&amp;quot;table&amp;quot; then I18N=data; return I18N end&lt;br /&gt;
    local ok2, mod = pcall(require, t)      ; if ok2 and type(mod) ==&amp;quot;table&amp;quot; then I18N=mod ; return I18N end&lt;br /&gt;
  end&lt;br /&gt;
  I18N = { i18n={}, events={ names={}, sections={default={}}, phrases={}, links={}, unlocks={} } }&lt;br /&gt;
  return I18N&lt;br /&gt;
end&lt;br /&gt;
local function events()&lt;br /&gt;
  if EVENTS then return EVENTS end&lt;br /&gt;
  for _,t in ipairs{ &amp;quot;Modul:EventData&amp;quot;, &amp;quot;Module:EventData&amp;quot; } do&lt;br /&gt;
    local ok, data = pcall(mw.loadData, t) ; if ok and type(data)==&amp;quot;table&amp;quot; and type(data.events)==&amp;quot;table&amp;quot; then EVENTS=data.events; return EVENTS end&lt;br /&gt;
    local ok2, mod = pcall(require, t)     ; if ok2 and type(mod) ==&amp;quot;table&amp;quot; and type(mod.events)==&amp;quot;table&amp;quot; then EVENTS=mod.events ; return EVENTS end&lt;br /&gt;
  end&lt;br /&gt;
  EVENTS = {}; return EVENTS&lt;br /&gt;
end&lt;br /&gt;
local function heroes()&lt;br /&gt;
  if HEROES then return HEROES end&lt;br /&gt;
  for _,t in ipairs{ &amp;quot;Modul:HeroData&amp;quot;, &amp;quot;Module:HeroData&amp;quot; } do&lt;br /&gt;
    local ok, data = pcall(mw.loadData, t) ; if ok and type(data)==&amp;quot;table&amp;quot; and type(data.heroes)==&amp;quot;table&amp;quot; then HEROES=data.heroes; return HEROES end&lt;br /&gt;
    local ok2, mod = pcall(require, t)     ; if ok2 and type(mod) ==&amp;quot;table&amp;quot; and type(mod.heroes)==&amp;quot;table&amp;quot; then HEROES=mod.heroes ; return HEROES end&lt;br /&gt;
  end&lt;br /&gt;
  HEROES = {}; return HEROES&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- ========== i18n / Labels ==========&lt;br /&gt;
local function ev_label(evkey, which)&lt;br /&gt;
  local E = i18n().events or {}&lt;br /&gt;
  local spec = (E.sections and E.sections[norm(evkey)]) or {}&lt;br /&gt;
  local def  = (E.sections and E.sections.default) or {}&lt;br /&gt;
  return (spec[which] or def[which] or &amp;quot;&amp;quot;)&lt;br /&gt;
end&lt;br /&gt;
local function ev_name(evkey)&lt;br /&gt;
  local names = (i18n().events or {}).names or {}&lt;br /&gt;
  return names[norm(evkey)] or evkey&lt;br /&gt;
end&lt;br /&gt;
local function header_labels(evkey)&lt;br /&gt;
  return ev_label(evkey, &amp;quot;offer&amp;quot;), ev_label(evkey, &amp;quot;price&amp;quot;), ev_label(evkey, &amp;quot;limit&amp;quot;)&lt;br /&gt;
end&lt;br /&gt;
local function autotr(key_name, value)&lt;br /&gt;
  if type(value) ~= &amp;quot;string&amp;quot; then return value end&lt;br /&gt;
  local k = norm(key_name)&lt;br /&gt;
&lt;br /&gt;
  -- 1) Standardwortschatz (falls vorhanden)&lt;br /&gt;
  local m1 = (i18n().i18n or {})[k]&lt;br /&gt;
  local v  = m1 and m1[norm(value)]&lt;br /&gt;
  if v then return v end&lt;br /&gt;
&lt;br /&gt;
  -- 2) Spezialfall Event-Typen: i18n().events.infobox.type&lt;br /&gt;
  if k == &amp;quot;type&amp;quot; then&lt;br /&gt;
    local m2 = (((i18n().events or {}).infobox) or {}).type or {}&lt;br /&gt;
    return m2[norm(value)] or value&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  return value&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function ev_notice_icon_wikitext(ev)&lt;br /&gt;
  local file = ev.currency and ev.currency.icon&lt;br /&gt;
  if not file or file == &amp;quot;&amp;quot; then return &amp;quot;&amp;quot; end&lt;br /&gt;
  local img = fileTag(file, { size = &amp;quot;24px&amp;quot;, param = &amp;quot;frameless|link=&amp;quot; })&lt;br /&gt;
  return &#039;&amp;lt;span class=&amp;quot;fs-ic&amp;quot;&amp;gt;&#039; .. img .. &#039;&amp;lt;/span&amp;gt;&#039;&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function ev_pagelink(ev, evkey, label)&lt;br /&gt;
  local page = ev.page&lt;br /&gt;
  if page and page ~= &amp;quot;&amp;quot; then&lt;br /&gt;
    return string.format(&amp;quot;[[%s|%s]]&amp;quot;, page, label)&lt;br /&gt;
  end&lt;br /&gt;
  return label&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
-- ========== Nummern + &amp;lt;ref&amp;gt; (voll i18n-gesteuert) ==========&lt;br /&gt;
local function parse_sci(v)&lt;br /&gt;
  if type(v) == &amp;quot;number&amp;quot; then return v end&lt;br /&gt;
  if type(v) == &amp;quot;string&amp;quot; then&lt;br /&gt;
    local a,e = v:match(&amp;quot;^%s*([%d%.]+)[eE]([%+%-]?%d+)%s*$&amp;quot;)&lt;br /&gt;
    if a and e then return (tonumber(a) or 0) * 10^(tonumber(e) or 0) end&lt;br /&gt;
    local n = tonumber(v); if n then return n end&lt;br /&gt;
  end&lt;br /&gt;
  return nil&lt;br /&gt;
end&lt;br /&gt;
local function i18n_big_units()&lt;br /&gt;
  local N = (i18n().numbers or {})&lt;br /&gt;
  return {&lt;br /&gt;
    {1e12, N.trillion_sing,  N.trillion_plur},&lt;br /&gt;
    {1e9,  N.billion_sing,   N.billion_plur},&lt;br /&gt;
    {1e6,  N.million_sing,   N.million_plur},&lt;br /&gt;
    {1e3,  N.thousand_sing,  N.thousand_plur},&lt;br /&gt;
  }&lt;br /&gt;
end&lt;br /&gt;
local function format_big(v)&lt;br /&gt;
  local n = parse_sci(v); if not n then return tostring(v or &amp;quot;&amp;quot;) end&lt;br /&gt;
  for _,u in ipairs(i18n_big_units()) do&lt;br /&gt;
    local base, sing, plur = u[1], u[2], u[3]&lt;br /&gt;
    if base and sing and plur and n &amp;gt;= base and math.floor(n % base) == 0 then&lt;br /&gt;
      local q = math.floor(n / base)&lt;br /&gt;
      return string.format(&amp;quot;%d %s&amp;quot;, q, (q==1) and sing or plur)&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
  return tostring(n)&lt;br /&gt;
end&lt;br /&gt;
local function ref_gear_power(v)&lt;br /&gt;
  if isempty(v) then return &amp;quot;&amp;quot; end&lt;br /&gt;
  local phr = (((i18n().events or {}).phrases) or {}).req_gear_power&lt;br /&gt;
  if isempty(phr) then return &amp;quot;&amp;quot; end&lt;br /&gt;
  local name = &amp;quot;gear-&amp;quot; .. tostring(v):gsub(&amp;quot;%s&amp;quot;,&amp;quot;&amp;quot;)&lt;br /&gt;
  return string.format(&#039;&amp;lt;ref name=&amp;quot;%s&amp;quot;&amp;gt;%s&amp;lt;/ref&amp;gt;&#039;, name, (phr:gsub(&amp;quot;$1&amp;quot;, format_big(v))))&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- ========== Event-Renderer ==========&lt;br /&gt;
local function htag(level, text)&lt;br /&gt;
  if isempty(text) then return &amp;quot;&amp;quot; end&lt;br /&gt;
  local eq = level==3 and &amp;quot;===&amp;quot; or &amp;quot;==&amp;quot;&lt;br /&gt;
  return string.format(&amp;quot;%s %s %s&amp;quot;, eq, text, eq)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function event_file(ev, evkey, which)&lt;br /&gt;
  if which==&amp;quot;deco&amp;quot; then&lt;br /&gt;
    local file   = ev.images and ev.images.deco or &amp;quot;&amp;quot;&lt;br /&gt;
    local legend = ev_label(evkey, &amp;quot;deco&amp;quot;)&lt;br /&gt;
    return fileTag(file, { param = &amp;quot;thumb|&amp;quot;..legend })&lt;br /&gt;
  elseif which==&amp;quot;fullname&amp;quot; then&lt;br /&gt;
    local file   = ev.images and ev.images.fullname or &amp;quot;&amp;quot;&lt;br /&gt;
    local legend = ev_label(evkey, &amp;quot;fullname&amp;quot;)&lt;br /&gt;
    return fileTag(file, { param = &amp;quot;thumb|&amp;quot;..legend })&lt;br /&gt;
  elseif which==&amp;quot;ex_shop&amp;quot; then&lt;br /&gt;
    local file   = ev.images and ev.images.ex_shop or &amp;quot;&amp;quot;&lt;br /&gt;
    local legend = ev_label(evkey, &amp;quot;ex_shop&amp;quot;)&lt;br /&gt;
    return fileTag(file, { param = &amp;quot;thumb|&amp;quot;..legend })&lt;br /&gt;
  elseif which==&amp;quot;banner&amp;quot; then&lt;br /&gt;
    return fileTag(ev.banner, { param=&amp;quot;thumb&amp;quot; })&lt;br /&gt;
  end&lt;br /&gt;
  return &amp;quot;&amp;quot;&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function event_link(ev, evkey, which)&lt;br /&gt;
  if which==&amp;quot;type&amp;quot; then&lt;br /&gt;
    local E = i18n().events or {}&lt;br /&gt;
    local page  = (E.links and E.links.type and E.links.type[ev.type or &amp;quot;&amp;quot;])&lt;br /&gt;
                  or (E.links and E.links.type) or &amp;quot;Calendar Events&amp;quot;&lt;br /&gt;
    local map   = (E.infobox and E.infobox.type) or {}&lt;br /&gt;
    local label = map[norm(ev.type or &amp;quot;&amp;quot;)] or (ev.type or &amp;quot;&amp;quot;)&lt;br /&gt;
    return string.format(&amp;quot;[[%s|%s]]&amp;quot;, page, label)&lt;br /&gt;
  end&lt;br /&gt;
  return &amp;quot;&amp;quot;&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function event_history(ev, evkey, args)&lt;br /&gt;
  local H = ev.history or {}; if not has_items(H) then return &amp;quot;&amp;quot; end&lt;br /&gt;
&lt;br /&gt;
  local phrases  = ((i18n().events or {}).phrases or {})&lt;br /&gt;
  local alt_gp   = phrases.alt_platform or &amp;quot;auf Google Play&amp;quot;&lt;br /&gt;
  local showTxt  = phrases.toggle_show  or &amp;quot;Anzeigen&amp;quot;&lt;br /&gt;
  local hideTxt  = phrases.toggle_hide  or &amp;quot;Verbergen&amp;quot;&lt;br /&gt;
&lt;br /&gt;
  local function fmt_range_or_raw(s)&lt;br /&gt;
    if not s or s == &amp;quot;&amp;quot; then return &amp;quot;&amp;quot; end&lt;br /&gt;
    local a,b = tostring(s):match(&amp;quot;^(%d%d%d%d%-%d%d%-%d%d)%s+bis%s+(%d%d%d%d%-%d%d%-%d%d)$&amp;quot;)&lt;br /&gt;
    if a and b then&lt;br /&gt;
      return string.format(&amp;quot;%s bis %s&amp;quot;, format_date_de(a), format_date_de(b))&lt;br /&gt;
    end&lt;br /&gt;
    local ts = parse_iso_date(s)&lt;br /&gt;
    return ts and format_date_de(ts) or s&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  local lines = {}&lt;br /&gt;
  for _,h in ipairs(H) do&lt;br /&gt;
    local a = format_date_de(h.start)&lt;br /&gt;
    local b = format_date_de(h[&amp;quot;end&amp;quot;] or h.end_date)&lt;br /&gt;
    local line = string.format(&amp;quot;* %s: %s bis %s&amp;quot;, tostring(h.year or &amp;quot;&amp;quot;), a, b)&lt;br /&gt;
    if h.alt_platform then&lt;br /&gt;
      line = line .. string.format(&amp;quot; (%s %s)&amp;quot;, fmt_range_or_raw(h.alt_platform), alt_gp)&lt;br /&gt;
    end&lt;br /&gt;
    lines[#lines+1] = line&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  -- vorhersagbare/überschreibbare ID für den externen Toggle in der Vorlage&lt;br /&gt;
  local id = (args and args.id) or (&amp;quot;fs-hist-&amp;quot; .. mw.ustring.gsub(tostring(evkey), &amp;quot;[^%w%-%_]&amp;quot;, &amp;quot;-&amp;quot;))&lt;br /&gt;
&lt;br /&gt;
  -- Wrapper (.fs-history-wrap) + eigentliche Collapsible-Box (.fs-history)&lt;br /&gt;
  return string.format(&lt;br /&gt;
    &#039;&amp;lt;div class=&amp;quot;fs-history-wrap&amp;quot;&amp;gt;\n&#039; ..&lt;br /&gt;
      &#039;&amp;lt;div id=&amp;quot;%s&amp;quot; class=&amp;quot;mw-collapsible mw-collapsed fs-history&amp;quot; data-expandtext=&amp;quot;%s&amp;quot; data-collapsetext=&amp;quot;%s&amp;quot;&amp;gt;\n%s\n&amp;lt;/div&amp;gt;\n&#039; ..&lt;br /&gt;
    &#039;&amp;lt;/div&amp;gt;&#039;,&lt;br /&gt;
    id, mw.text.encode(showTxt), mw.text.encode(hideTxt), table.concat(lines, &amp;quot;\n&amp;quot;)&lt;br /&gt;
  )&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function item_label(key, amount)&lt;br /&gt;
  local I = (i18n().items or {})[key] or {}&lt;br /&gt;
  local n = tonumber(amount or 1) or 1&lt;br /&gt;
  local txt = (n == 1) and (I.sing or key) or (I.plur or (I.sing or key))&lt;br /&gt;
  local disp = I.link and string.format(&amp;quot;[[%s|%s]]&amp;quot;, I.link, txt) or txt&lt;br /&gt;
  return string.format(&amp;quot;%d %s&amp;quot;, n, disp)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- references/render&lt;br /&gt;
local FRAME = nil&lt;br /&gt;
local function tabber_render_from_content(content)&lt;br /&gt;
  if FRAME and FRAME.extensionTag then&lt;br /&gt;
    return FRAME:extensionTag(&#039;tabber&#039;, content)&lt;br /&gt;
  end&lt;br /&gt;
  return &amp;quot;&amp;lt;tabber&amp;gt;\n&amp;quot; .. content .. &amp;quot;\n&amp;lt;/tabber&amp;gt;&amp;quot;&lt;br /&gt;
end&lt;br /&gt;
local function references_tag()&lt;br /&gt;
  if FRAME and FRAME.extensionTag then&lt;br /&gt;
    return FRAME:extensionTag(&#039;references&#039;, &#039;&#039;)&lt;br /&gt;
  end&lt;br /&gt;
  return &#039;&amp;lt;references /&amp;gt;&#039;&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- innerer Tab-Inhalt: je Range&lt;br /&gt;
local function build_tabber_content_for(list)&lt;br /&gt;
  local out = {}&lt;br /&gt;
  for _,it in ipairs(list) do&lt;br /&gt;
    local label   = pretty_range(it.range or &amp;quot;&amp;quot;)&lt;br /&gt;
    local note    = ref_gear_power(it.req_gear_power)&lt;br /&gt;
    local content = string.format(&lt;br /&gt;
      &amp;quot;%s %s%s&amp;quot;,&lt;br /&gt;
      fileTag(it.icon or &amp;quot;&amp;quot;, { size=&amp;quot;30px&amp;quot; }),&lt;br /&gt;
      (it.item and item_label(it.item, it.amount or 1)) or (it.title or &amp;quot;&amp;quot;),&lt;br /&gt;
      note&lt;br /&gt;
    )&lt;br /&gt;
    out[#out+1] = &amp;quot;|-|&amp;quot; .. label .. &amp;quot;=\n&amp;quot; .. content&lt;br /&gt;
  end&lt;br /&gt;
  return table.concat(out, &amp;quot;\n&amp;quot;)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Gruppen-Label aus i18n&lt;br /&gt;
local function group_label(kind)&lt;br /&gt;
  local G = ((i18n().events or {}).group_labels or {})&lt;br /&gt;
  return G[kind] or kind&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function event_avatars(ev, evkey)&lt;br /&gt;
  local AV = ev.avatars&lt;br /&gt;
  if not has_items(AV) then return &amp;quot;&amp;quot; end&lt;br /&gt;
&lt;br /&gt;
  local token_icon = (ev.currency or {}).icon or &amp;quot;&amp;quot;&lt;br /&gt;
  local token_name = ev_label(evkey, &amp;quot;currencies&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
  local years, out = {}, {}&lt;br /&gt;
  for y,_ in pairs(AV) do table.insert(years, tonumber(y) or y) end&lt;br /&gt;
  table.sort(years, function(a,b) return tostring(a) &amp;lt; tostring(b) end)&lt;br /&gt;
&lt;br /&gt;
  for _,year in ipairs(years) do&lt;br /&gt;
    local list = AV[year] or AV[tostring(year)]&lt;br /&gt;
    if has_items(list) then&lt;br /&gt;
      out[#out+1] = (&amp;quot;==== %s ====&amp;quot;):format(year)&lt;br /&gt;
      out[#out+1] = &#039;{| class=&amp;quot;article-table&amp;quot; style=&amp;quot;font-size:14px;&amp;quot;&#039;&lt;br /&gt;
      out[#out+1] = &#039;! colspan=&amp;quot;2&amp;quot; | Avatar\n! Preis&#039;&lt;br /&gt;
      for _,it in ipairs(list) do&lt;br /&gt;
        out[#out+1] = &#039;|-&#039;&lt;br /&gt;
        out[#out+1] = string.format(&#039;|%s || %s || %s %s %s&#039;,&lt;br /&gt;
          fileTag(it.file, { size=&amp;quot;50px&amp;quot; }),&lt;br /&gt;
          it.title or &amp;quot;&amp;quot;,&lt;br /&gt;
          fileTag(token_icon, { size=&amp;quot;25px&amp;quot; }),&lt;br /&gt;
          tostring(it.cost or 0),&lt;br /&gt;
          token_name&lt;br /&gt;
        )&lt;br /&gt;
      end&lt;br /&gt;
      out[#out+1] = &#039;|}&#039;&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
  return (#out &amp;gt; 0) and table.concat(out, &amp;quot;\n&amp;quot;) or &amp;quot;&amp;quot;&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Austausch-Tabelle (äußerer Tab: Gruppen → innerer Tab: Ranges)&lt;br /&gt;
local function event_exchange_tabs(ev, evkey)&lt;br /&gt;
  local ex = ev.exchange or {}&lt;br /&gt;
  local token_icon = (ev.currency or {}).icon or &amp;quot;&amp;quot;&lt;br /&gt;
  local token_name = ev_label(evkey, &amp;quot;currencies&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
  if not (has_items(ex.chests_by_level)&lt;br /&gt;
       or has_items(ex.chests_by_stars)&lt;br /&gt;
       or has_items(ex.chests_by_oracle)&lt;br /&gt;
       or has_items(ex.currencies)) then&lt;br /&gt;
    return &amp;quot;&amp;quot;&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  local outer_parts = {}&lt;br /&gt;
  local function add_group(kind, list)&lt;br /&gt;
    if not has_items(list) then return end&lt;br /&gt;
    local inner_content = build_tabber_content_for(list)&lt;br /&gt;
    local inner_tabber  = tabber_render_from_content(inner_content)&lt;br /&gt;
    outer_parts[#outer_parts+1] = &amp;quot;|-|&amp;quot; .. group_label(kind) .. &amp;quot;=\n&amp;quot; .. inner_tabber&lt;br /&gt;
  end&lt;br /&gt;
  add_group(&amp;quot;level&amp;quot;,  ex.chests_by_level)&lt;br /&gt;
  add_group(&amp;quot;stars&amp;quot;,  ex.chests_by_stars)&lt;br /&gt;
  add_group(&amp;quot;oracle&amp;quot;, ex.chests_by_oracle)&lt;br /&gt;
&lt;br /&gt;
  local left_tabber = tabber_render_from_content(table.concat(outer_parts, &amp;quot;\n&amp;quot;))&lt;br /&gt;
&lt;br /&gt;
  -- Preis/Limit (erste gefundene Gruppe)&lt;br /&gt;
  local function pick_price_limit()&lt;br /&gt;
    if has_items(ex.chests_by_level)  then return ex.chests_by_level[1].price or 0,  ex.chests_by_level[1].limit or &amp;quot;&amp;quot;  end&lt;br /&gt;
    if has_items(ex.chests_by_stars)  then return ex.chests_by_stars[1].price or 0,  ex.chests_by_stars[1].limit or &amp;quot;&amp;quot;  end&lt;br /&gt;
    if has_items(ex.chests_by_oracle) then return ex.chests_by_oracle[1].price or 0, ex.chests_by_oracle[1].limit or &amp;quot;&amp;quot; end&lt;br /&gt;
    return 0, &amp;quot;&amp;quot;&lt;br /&gt;
  end&lt;br /&gt;
  local price, limit = pick_price_limit()&lt;br /&gt;
&lt;br /&gt;
  local t = {}&lt;br /&gt;
  local H_offer, H_price, H_limit = header_labels(evkey)&lt;br /&gt;
  t[#t+1] = &#039;{| class=&amp;quot;article-table fs-exchange&amp;quot; style=&amp;quot;font-size:14px;&amp;quot;&#039;&lt;br /&gt;
  t[#t+1] = string.format(&lt;br /&gt;
    &#039;! style=&amp;quot;width:%s;&amp;quot; | %s !! style=&amp;quot;width:%s;&amp;quot; | %s !! style=&amp;quot;width:%s;&amp;quot; | %s&#039;,&lt;br /&gt;
    COL_OFFER, H_offer, COL_PRICE, H_price, COL_LIMIT, H_limit&lt;br /&gt;
  )&lt;br /&gt;
  t[#t+1] = &#039;|-&#039;&lt;br /&gt;
  t[#t+1] = &#039;|&#039; .. left_tabber .. string.format(&lt;br /&gt;
    &#039; || style=&amp;quot;text-align:center;&amp;quot; | %s %s %s || style=&amp;quot;text-align:center;&amp;quot; | %s&#039;,&lt;br /&gt;
    fileTag(token_icon, { size=&amp;quot;25px&amp;quot; }),&lt;br /&gt;
    tostring(price), token_name,&lt;br /&gt;
    tostring(limit)&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
  if has_items(ex.currencies) then&lt;br /&gt;
    for _,it in ipairs(ex.currencies) do&lt;br /&gt;
      local offer = string.format(&#039;%s %s&#039;,&lt;br /&gt;
        fileTag(it.icon or &amp;quot;&amp;quot;, { size=&amp;quot;30px&amp;quot; }),&lt;br /&gt;
        item_label(it.item, it.amount or 1)&lt;br /&gt;
      )&lt;br /&gt;
      t[#t+1] = &#039;|-&#039;&lt;br /&gt;
      t[#t+1] = string.format(&lt;br /&gt;
        &#039;| %s || style=&amp;quot;text-align:center;&amp;quot; | %s %s %s || style=&amp;quot;text-align:center;&amp;quot; | %s&#039;,&lt;br /&gt;
        offer,&lt;br /&gt;
        fileTag(token_icon, { size=&amp;quot;25px&amp;quot; }),&lt;br /&gt;
        tostring(it.price or 0), token_name,&lt;br /&gt;
        tostring(it.limit or &amp;quot;&amp;quot;)&lt;br /&gt;
      )&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  t[#t+1] = &#039;|}&#039;&lt;br /&gt;
  t[#t+1] = references_tag()&lt;br /&gt;
  return &#039;\n&#039; .. table.concat(t, &#039;\n&#039;)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function event_offers(ev, evkey, which)&lt;br /&gt;
  local list = (ev.shop or {})[which]&lt;br /&gt;
  if not has_items(list) then return &amp;quot;&amp;quot; end&lt;br /&gt;
  local token_icon = (ev.currency or {}).icon or &amp;quot;&amp;quot;&lt;br /&gt;
  local token_name = ev_label(evkey, &amp;quot;currencies&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
  local t = {}&lt;br /&gt;
  t[#t+1] = &#039;{| class=&amp;quot;article-table&amp;quot; style=&amp;quot;font-size:14px;&amp;quot;&#039;&lt;br /&gt;
  t[#t+1] = &amp;quot;! Angebot !! Menge&amp;quot;&lt;br /&gt;
  for _,it in ipairs(list) do&lt;br /&gt;
    t[#t+1] = &amp;quot;|-&amp;quot;&lt;br /&gt;
    t[#t+1] = string.format(&amp;quot;| &#039;&#039;%s&#039;&#039;&amp;lt;br /&amp;gt;%s || %s %s %s&amp;quot;,&lt;br /&gt;
      it.name or &amp;quot;&amp;quot;,&lt;br /&gt;
      fileTag(it.image, { size=&amp;quot;x50px&amp;quot; }),&lt;br /&gt;
      fileTag(token_icon, { size=&amp;quot;30px&amp;quot; }),&lt;br /&gt;
      tostring(it.token or 0),&lt;br /&gt;
      token_name&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
  t[#t+1] = &amp;quot;|}&amp;quot;&lt;br /&gt;
  return table.concat(t, &amp;quot;\n&amp;quot;)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- ========== SiteNotice-Support (Datum/Auto-Pick) ==========&lt;br /&gt;
local function event_window(ev)&lt;br /&gt;
  local S = ev.schedule or ev&lt;br /&gt;
  local s = S.start_date; if isempty(s) then return nil end&lt;br /&gt;
&lt;br /&gt;
  local st = parse_iso_date(s); if not st then return nil end&lt;br /&gt;
&lt;br /&gt;
  local en_excl, en_incl&lt;br /&gt;
  if not isempty(S.end_date) then&lt;br /&gt;
    -- end_date wird als inklusives Kalenderdatum verstanden&lt;br /&gt;
    en_incl = parse_iso_date(S.end_date)&lt;br /&gt;
    if not en_incl then return st, nil, nil end&lt;br /&gt;
    en_excl = en_incl + DAY&lt;br /&gt;
  else&lt;br /&gt;
    -- Dauer in Tagen (Standard 14), exklusives Ende = st + d*DAY&lt;br /&gt;
    local d = tonumber(S.duration_days) or 14&lt;br /&gt;
    en_excl = st + d * DAY&lt;br /&gt;
    en_incl = en_excl - DAY&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  return st, en_excl, en_incl&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function pick_notice_eventkey()&lt;br /&gt;
  local cfg = ((i18n().events or {}).notice or {})&lt;br /&gt;
  local pre  = tonumber(cfg.pre_days)  or 7&lt;br /&gt;
  local post = tonumber(cfg.post_days) or 3&lt;br /&gt;
  local now = os.time()&lt;br /&gt;
&lt;br /&gt;
  local running, upcoming, ended_recent = {}, {}, {}&lt;br /&gt;
&lt;br /&gt;
  for k, ev in pairs(events()) do&lt;br /&gt;
    local st, en_excl = event_window(ev)&lt;br /&gt;
    if st and en_excl then&lt;br /&gt;
      if now &amp;gt;= st and now &amp;lt; en_excl then&lt;br /&gt;
        table.insert(running, { k=k, st=st, en=en_excl })&lt;br /&gt;
      elseif now &amp;gt;= st - pre*DAY and now &amp;lt; st then&lt;br /&gt;
        table.insert(upcoming, { k=k, st=st, en=en_excl })&lt;br /&gt;
      elseif now &amp;gt;= en_excl and now &amp;lt; en_excl + post*DAY then&lt;br /&gt;
        table.insert(ended_recent, { k=k, st=st, en=en_excl })&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  local function by_end(a,b)   return a.en &amp;lt; b.en end&lt;br /&gt;
  local function by_start(a,b) return a.st &amp;lt; b.st end&lt;br /&gt;
  if #running      &amp;gt; 0 then table.sort(running,      by_end)   ; return running[1].k      end&lt;br /&gt;
  if #upcoming     &amp;gt; 0 then table.sort(upcoming,     by_start) ; return upcoming[1].k     end&lt;br /&gt;
  if #ended_recent &amp;gt; 0 then table.sort(ended_recent, by_end)   ; return ended_recent[1].k end&lt;br /&gt;
  return nil&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function event_notice(ev, evkey)&lt;br /&gt;
  local phrases = (((i18n().events or {}).phrases) or {})&lt;br /&gt;
  local PH_RUNNING = phrases.event_notice   -- &amp;quot;Das $1 läuft von $2 bis $3&amp;quot;&lt;br /&gt;
  local PH_ENDED   = phrases.event_ended    -- &amp;quot;Das $1 ist beendet. Du kannst verbleibende $4 noch im $5 eintauschen.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
  -- Ohne Texte kein Hinweis&lt;br /&gt;
  if isempty(PH_RUNNING) then return &amp;quot;&amp;quot; end&lt;br /&gt;
&lt;br /&gt;
  local st, en_excl, en_incl = event_window(ev); if not st or not en_excl then return &amp;quot;&amp;quot; end&lt;br /&gt;
&lt;br /&gt;
  local cfg  = ((i18n().events or {}).notice or {})&lt;br /&gt;
  local pre  = tonumber(cfg.pre_days)  or 7   -- z. B. 7 Tage vorher&lt;br /&gt;
  local post = tonumber(cfg.post_days) or 3   -- z. B. 3 Tage danach&lt;br /&gt;
  local now  = os.time()&lt;br /&gt;
&lt;br /&gt;
  local show_from = st - pre  * DAY&lt;br /&gt;
  local show_till = en_excl + post * DAY&lt;br /&gt;
  if now &amp;lt; show_from or now &amp;gt;= show_till then&lt;br /&gt;
    return &amp;quot;&amp;quot;&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  -- Anzeigename (aus i18n sections oder names)&lt;br /&gt;
  local name_label = ev_label(evkey, &amp;quot;eventname&amp;quot;)&lt;br /&gt;
  if isempty(name_label) then name_label = ev_name(evkey) end&lt;br /&gt;
&lt;br /&gt;
  -- Verlinken NUR wenn ev.page existiert&lt;br /&gt;
  local name = ev_pagelink(ev, evkey, name_label)&lt;br /&gt;
&lt;br /&gt;
  -- Icon aus EventData → currency.icon&lt;br /&gt;
  local ic = ev_notice_icon_wikitext(ev)  -- &amp;quot;&amp;quot; wenn kein currency.icon&lt;br /&gt;
&lt;br /&gt;
  local text&lt;br /&gt;
  if now &amp;lt; en_excl then&lt;br /&gt;
    -- läuft / startet bald&lt;br /&gt;
    text = PH_RUNNING&lt;br /&gt;
      :gsub(&amp;quot;$1&amp;quot;, name)&lt;br /&gt;
      :gsub(&amp;quot;$2&amp;quot;, format_date_de(st))&lt;br /&gt;
      :gsub(&amp;quot;$3&amp;quot;, format_date_de(en_excl))&lt;br /&gt;
  else&lt;br /&gt;
    -- beendet (bis zu post_days danach)&lt;br /&gt;
    if isempty(PH_ENDED) then return &amp;quot;&amp;quot; end&lt;br /&gt;
    -- Labels aus i18n.sections (wie gehabt)&lt;br /&gt;
    local curr = ev_label(evkey, &amp;quot;currencies&amp;quot;)&lt;br /&gt;
    local shop = ev_label(evkey, &amp;quot;ex_shop&amp;quot;)&lt;br /&gt;
    if isempty(curr) or isempty(shop) then return &amp;quot;&amp;quot; end&lt;br /&gt;
    text = PH_ENDED&lt;br /&gt;
      :gsub(&amp;quot;$1&amp;quot;, name)&lt;br /&gt;
      :gsub(&amp;quot;$4&amp;quot;, curr)&lt;br /&gt;
      :gsub(&amp;quot;$5&amp;quot;, shop)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  local prefix = (ic ~= &amp;quot;&amp;quot; and (ic .. &amp;quot; &amp;quot;) or &amp;quot;&amp;quot;)&lt;br /&gt;
  local suffix = (ic ~= &amp;quot;&amp;quot; and (&amp;quot; &amp;quot; .. ic) or &amp;quot;&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
  return string.format(&lt;br /&gt;
    &#039;&amp;lt;div class=&amp;quot;fs-notice&amp;quot; data-ev=&amp;quot;%s&amp;quot;&amp;gt;%s%s%s&amp;lt;/div&amp;gt;&#039;,&lt;br /&gt;
    mw.text.encode(evkey),&lt;br /&gt;
    prefix, text, suffix&lt;br /&gt;
  )&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- ========== Dispatcher ==========&lt;br /&gt;
local function get_event(evkey)&lt;br /&gt;
  local evs = events()&lt;br /&gt;
  local real = pick_key(evs, evkey)&lt;br /&gt;
  return real and evs[real] or nil, real or evkey&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function dispatch_event(args)&lt;br /&gt;
  local evkey = args[2]&lt;br /&gt;
&lt;br /&gt;
  -- {{Firestone|Event|notice}}  → automatisch wählen&lt;br /&gt;
  if evkey and norm(evkey) == &amp;quot;notice&amp;quot; then&lt;br /&gt;
    local pick = pick_notice_eventkey()&lt;br /&gt;
    if not pick then return &amp;quot;&amp;quot; end&lt;br /&gt;
    local ev, r = get_event(pick); if not ev then return &amp;quot;&amp;quot; end&lt;br /&gt;
    return event_notice(ev, r)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  -- normaler Weg&lt;br /&gt;
  if isempty(evkey) then return &amp;quot;&amp;quot; end&lt;br /&gt;
  local ev, realkey = get_event(evkey); if not ev then return &amp;quot;&amp;quot; end&lt;br /&gt;
&lt;br /&gt;
  local a3, a4 = args[3], args[4]&lt;br /&gt;
  if isempty(a3) then return ev_name(realkey) end&lt;br /&gt;
&lt;br /&gt;
  if a3==&amp;quot;notice&amp;quot;  then return event_notice(ev, realkey)&lt;br /&gt;
  elseif a3==&amp;quot;file&amp;quot;   then return event_file(ev, realkey, a4 or &amp;quot;&amp;quot;)&lt;br /&gt;
  elseif a3==&amp;quot;link&amp;quot;   then return event_link(ev, realkey, a4 or &amp;quot;&amp;quot;)&lt;br /&gt;
  elseif a3==&amp;quot;h2&amp;quot;     then return htag(2, ev_label(realkey, a4 or &amp;quot;&amp;quot;))&lt;br /&gt;
  elseif a3==&amp;quot;h3&amp;quot;     then return htag(3, ev_label(realkey, a4 or &amp;quot;&amp;quot;))&lt;br /&gt;
  elseif a3==&amp;quot;history&amp;quot;  then return event_history(ev, realkey, a)&lt;br /&gt;
  elseif a3==&amp;quot;avatars&amp;quot;  then return event_avatars(ev, realkey)&lt;br /&gt;
  elseif a3==&amp;quot;exchange&amp;quot; then return event_exchange_tabs(ev, realkey)&lt;br /&gt;
  elseif a3==&amp;quot;offers&amp;quot;   then&lt;br /&gt;
    if a4==&amp;quot;start&amp;quot; then return event_offers(ev, realkey, &amp;quot;initial&amp;quot;)&lt;br /&gt;
    elseif a4==&amp;quot;more&amp;quot; then return event_offers(ev, realkey, &amp;quot;after&amp;quot;) end&lt;br /&gt;
    return &amp;quot;&amp;quot;&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  -- Sonderfälle für Währungsnamen aus i18n.sections&lt;br /&gt;
  if a3 == &amp;quot;currency&amp;quot;  then return ev_label(realkey, &amp;quot;currency&amp;quot;)  end&lt;br /&gt;
  if a3 == &amp;quot;currencies&amp;quot; then return ev_label(realkey, &amp;quot;currencies&amp;quot;) end&lt;br /&gt;
&lt;br /&gt;
  -- generischer Pfad in EventData&lt;br /&gt;
  local path, i = {}, 3&lt;br /&gt;
  while args[i] do table.insert(path, args[i]); i=i+1 end&lt;br /&gt;
  local v = deep_get(ev, path)&lt;br /&gt;
  if v ~= nil then&lt;br /&gt;
    local last = path[#path] and tostring(path[#path]) or &amp;quot;&amp;quot;&lt;br /&gt;
    v = autotr(last, v)&lt;br /&gt;
    return tostring(v)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  -- Fallback auf Labels/Name/Unlocks&lt;br /&gt;
  local last = norm(path[#path] or a3)&lt;br /&gt;
  if last==&amp;quot;eventname&amp;quot; or last==&amp;quot;name&amp;quot; then return ev_name(realkey) end&lt;br /&gt;
  local lbl = ev_label(realkey, last); if not isempty(lbl) then return lbl end&lt;br /&gt;
  local U = ((i18n().events or {}).unlocks or {})[last]&lt;br /&gt;
  if U then return U end&lt;br /&gt;
&lt;br /&gt;
  return &amp;quot;&amp;quot;&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function dispatch_hero(args)&lt;br /&gt;
  local name = args[2]; if isempty(name) then return &amp;quot;&amp;quot; end&lt;br /&gt;
  local H = heroes()&lt;br /&gt;
  local real = pick_key(H, name); if not real then return &amp;quot;&amp;quot; end&lt;br /&gt;
  local h = H[real]&lt;br /&gt;
  local path, i = {}, 3&lt;br /&gt;
  while args[i] do table.insert(path, args[i]); i=i+1 end&lt;br /&gt;
  local v = deep_get(h, path)&lt;br /&gt;
  local last = path[#path] and tostring(path[#path]) or &amp;quot;&amp;quot;&lt;br /&gt;
  v = autotr(last, v)&lt;br /&gt;
  return tostring(v or &amp;quot;&amp;quot;)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- ========== main ==========&lt;br /&gt;
function M.main(frame)&lt;br /&gt;
  FRAME = frame&lt;br /&gt;
  local a = getArgs(frame)&lt;br /&gt;
  for i=1,10 do a[i] = a[i] and tostring(a[i]) or nil end&lt;br /&gt;
  local dom = a[1] and norm(a[1]) or &amp;quot;&amp;quot;&lt;br /&gt;
  if dom==&amp;quot;event&amp;quot; then&lt;br /&gt;
    return dispatch_event(a)&lt;br /&gt;
  elseif dom==&amp;quot;hero&amp;quot; then&lt;br /&gt;
    return dispatch_hero(a)&lt;br /&gt;
  else&lt;br /&gt;
    return &amp;quot;&amp;quot;&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
return M&lt;/div&gt;</summary>
		<author><name>Webmaster</name></author>
	</entry>
	<entry>
		<id>https://firestone-idle-rpg.de/index.php?title=Vorlage:EventPage&amp;diff=3759</id>
		<title>Vorlage:EventPage</title>
		<link rel="alternate" type="text/html" href="https://firestone-idle-rpg.de/index.php?title=Vorlage:EventPage&amp;diff=3759"/>
		<updated>2025-10-28T10:52:31Z</updated>

		<summary type="html">&lt;p&gt;Webmaster: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox/Event&lt;br /&gt;
|banner={{Firestone|Event|{{{name|}}}|banner}}&lt;br /&gt;
|caption=&lt;br /&gt;
|type={{Firestone|Event|{{{name|}}}|type}}&lt;br /&gt;
|date={{Firestone|Event|{{{name|}}}|month}}&lt;br /&gt;
|duration={{Firestone|Event|{{{name|}}}|duration}}&lt;br /&gt;
|unlocksat={{Firestone|Event|{{{name|}}}|character_level_10}}}}&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|file|deco}}&lt;br /&gt;
Das {{Firestone|Event|{{{name|}}}|eventname}} ist ein {{Firestone|Event|{{{name|}}}|type}}, das im {{Firestone|Event|{{{name|}}}|month}} startet und {{Firestone|Event|{{{name|}}}|duration}} dauert. Frühere Ausgaben waren: {{#tag:a|Anzeigen&lt;br /&gt;
 | class=&amp;quot;mw-collapsible-toggle fs-toggle&amp;quot;&lt;br /&gt;
 | data-mw-collapsible-target=&amp;quot;#fs-hist-{{{name|}}}&amp;quot;&lt;br /&gt;
 | data-expandtext=&amp;quot;Anzeigen&amp;quot;&lt;br /&gt;
 | data-collapsetext=&amp;quot;Verbergen&amp;quot;&lt;br /&gt;
}}&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|history|id=fs-hist-{{{name|}}}}}&lt;br /&gt;
&lt;br /&gt;
In diesem Event lässt {{Firestone|Event|{{{name|}}}|fullname}}, {{Firestone|Event|{{{name|}}}|currencies}} auf dem Schlachtfeld fallen. Diese können anschließend in der {{Firestone|Event|{{{name|}}}|ex_shop}} gegen [[Gear Chests|Ausrüstungstruhen]], [[Jewel Chests|Juwelentruhen]], bestimmte [[currencies|Währungen]] und einzigartige [[avatar|Avatare]] eingetauscht werden. Zusätzlich ist die Stadt mit Halloween-Dekoration geschmückt. &lt;br /&gt;
&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|h2|currencies}}&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|file|fullname}}&lt;br /&gt;
&lt;br /&gt;
Während des Events lässt {{Firestone|Event|{{{name|}}}|fullname}}, {{Firestone|Event|{{{name|}}}|currencies}} auf dem Schlachtfeld fallen – mit einer Rate von 1 {{Firestone|Event|{{{name|}}}|currency}} alle 28,8 Sekunden. Die {{Firestone|Event|{{{name|}}}|currencies}} werden automatisch eingesammelt, was zu einem fixen Zuwachs von 3.000 {{Firestone|Event|{{{name|}}}|currencies}} pro Tag führt. {{Firestone|Event|{{{name|}}}|currencies}} werden auch gesammelt, während du offline bist, und werden dir gutgeschrieben, wenn {{Firestone|Event|{{{name|}}}|shortname}} dich nach dem erneuten Einloggen das erste Mal besucht. Offline-Gewinne sind jedoch auf 24 Stunden begrenzt.&lt;br /&gt;
&lt;br /&gt;
Wenn du einen Einkauf im {{Firestone|Event|{{{name|}}}|shopname}} tätigst (siehe unten), erhöht sich die Drop-Rate für den Rest des Events auf 2 {{Firestone|Event|{{{name|}}}|currencies}} alle 28,8 Sekunden.&lt;br /&gt;
&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|h2|ex_shop}}&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|file|ex_shop}}&lt;br /&gt;
&lt;br /&gt;
Die {{Firestone|Event|{{{name|}}}|ex_shop}} ersetzt den Brunnen in der Stadt und ist nur während des Halloween-Events verfügbar. Nach Ende des Events verschwindet die {{Firestone|Event|{{{name|}}}|ex_shop}} wieder und deine verbleibenden {{Firestone|Event|{{{name|}}}|currencies}} gehen verloren. Die {{Firestone|Event|{{{name|}}}|ex_shop}} besteht aus den folgenden drei Bereichen.&lt;br /&gt;
&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|h3|currency_exchange}}&lt;br /&gt;
&lt;br /&gt;
Im {{Firestone|Event|{{{name|}}}|currency_exchange}} kannst du {{Firestone|Event|{{{name|}}}|currencies}} gegen Truhen und [[currencies|Währungen]] eintauschen. Die Art der [[Gear Chests|Ausrüstungstruhe]] hängt von deiner [[character level|Charakterstufe]] ab und die Art der [[Jewel Chests|Juwelentruhe]] von der Anzahl deiner [[campaign stars|Kampagnensterne]]. Die Anzahl der Käufe ist für jeden angebotenen Truhen- und Währungstyp begrenzt.&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|exchange}}&lt;br /&gt;
&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|h3|avatar_exchange}}&lt;br /&gt;
&lt;br /&gt;
Bei »{{Firestone|Event|{{{name|}}}|avatar_exchange}}« kannst du {{Firestone|Event|{{{name|}}}|currencies}} gegen einzigartige [[avatar|Avatare]] eintauschen, die nur während des {{Firestone|Event|{{{name|}}}|name}} erhältlich sind.&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|avatars}}&lt;br /&gt;
&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|h3|shop}}&lt;br /&gt;
&lt;br /&gt;
Im {{Firestone|Event|{{{name|}}}|shopname}} kannst du {{Firestone|Event|{{{name|}}}|currencies}} mit [[Platforms|Plattform]]-Währung kaufen. Jeder Kauf im {{Firestone|Event|{{{name|}}}|shopname}} erhöht die Drop-Rate der {{Firestone|Event|{{{name|}}}|currencies}} auf dem Schlachtfeld für den Rest des Events auf 2 {{Firestone|Event|{{{name|}}}|currencies}} alle 28,8 Sekunden. Jedes Angebot kann nur einmal erworben werden.&lt;br /&gt;
&lt;br /&gt;
Anfangs werden die folgenden 3 Angebote angezeigt:&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|offers|start}}&lt;br /&gt;
&lt;br /&gt;
Sobald die Angebote 1–3 alle gekauft wurden, erscheinen die folgenden Angebote:&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|offers|more}}&lt;br /&gt;
&lt;br /&gt;
{{Navigation}}&lt;br /&gt;
[[Category:Browse]]&lt;br /&gt;
[[Category:Events]]&lt;/div&gt;</summary>
		<author><name>Webmaster</name></author>
	</entry>
	<entry>
		<id>https://firestone-idle-rpg.de/index.php?title=Vorlage:EventPage&amp;diff=3758</id>
		<title>Vorlage:EventPage</title>
		<link rel="alternate" type="text/html" href="https://firestone-idle-rpg.de/index.php?title=Vorlage:EventPage&amp;diff=3758"/>
		<updated>2025-10-28T09:58:27Z</updated>

		<summary type="html">&lt;p&gt;Webmaster: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox/Event&lt;br /&gt;
|banner={{Firestone|Event|{{{name|}}}|banner}}&lt;br /&gt;
|caption=&lt;br /&gt;
|type={{Firestone|Event|{{{name|}}}|type}}&lt;br /&gt;
|date={{Firestone|Event|{{{name|}}}|month}}&lt;br /&gt;
|duration={{Firestone|Event|{{{name|}}}|duration}}&lt;br /&gt;
|unlocksat={{Firestone|Event|{{{name|}}}|character_level_10}}}}&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|file|deco}}&lt;br /&gt;
Das {{Firestone|Event|{{{name|}}}|eventname}} ist ein {{Firestone|Event|{{{name|}}}|type}}, das im {{Firestone|Event|{{{name|}}}|month}} startet und {{Firestone|Event|{{{name|}}}|duration}} dauert. Frühere Ausgaben waren: {{#tag:span||class=mw-collapsible-toggle fs-toggle|data-mw-collapsible-target=#fs-hist-{{{name|}}}}}&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|history|id=fs-hist-{{{name|}}}}}&lt;br /&gt;
&lt;br /&gt;
In diesem Event lässt {{Firestone|Event|{{{name|}}}|fullname}}, {{Firestone|Event|{{{name|}}}|currencies}} auf dem Schlachtfeld fallen. Diese können anschließend in der {{Firestone|Event|{{{name|}}}|ex_shop}} gegen [[Gear Chests|Ausrüstungstruhen]], [[Jewel Chests|Juwelentruhen]], bestimmte [[currencies|Währungen]] und einzigartige [[avatar|Avatare]] eingetauscht werden. Zusätzlich ist die Stadt mit Halloween-Dekoration geschmückt. &lt;br /&gt;
&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|h2|currencies}}&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|file|fullname}}&lt;br /&gt;
&lt;br /&gt;
Während des Events lässt {{Firestone|Event|{{{name|}}}|fullname}}, {{Firestone|Event|{{{name|}}}|currencies}} auf dem Schlachtfeld fallen – mit einer Rate von 1 {{Firestone|Event|{{{name|}}}|currency}} alle 28,8 Sekunden. Die {{Firestone|Event|{{{name|}}}|currencies}} werden automatisch eingesammelt, was zu einem fixen Zuwachs von 3.000 {{Firestone|Event|{{{name|}}}|currencies}} pro Tag führt. {{Firestone|Event|{{{name|}}}|currencies}} werden auch gesammelt, während du offline bist, und werden dir gutgeschrieben, wenn {{Firestone|Event|{{{name|}}}|shortname}} dich nach dem erneuten Einloggen das erste Mal besucht. Offline-Gewinne sind jedoch auf 24 Stunden begrenzt.&lt;br /&gt;
&lt;br /&gt;
Wenn du einen Einkauf im {{Firestone|Event|{{{name|}}}|shopname}} tätigst (siehe unten), erhöht sich die Drop-Rate für den Rest des Events auf 2 {{Firestone|Event|{{{name|}}}|currencies}} alle 28,8 Sekunden.&lt;br /&gt;
&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|h2|ex_shop}}&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|file|ex_shop}}&lt;br /&gt;
&lt;br /&gt;
Die {{Firestone|Event|{{{name|}}}|ex_shop}} ersetzt den Brunnen in der Stadt und ist nur während des Halloween-Events verfügbar. Nach Ende des Events verschwindet die {{Firestone|Event|{{{name|}}}|ex_shop}} wieder und deine verbleibenden {{Firestone|Event|{{{name|}}}|currencies}} gehen verloren. Die {{Firestone|Event|{{{name|}}}|ex_shop}} besteht aus den folgenden drei Bereichen.&lt;br /&gt;
&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|h3|currency_exchange}}&lt;br /&gt;
&lt;br /&gt;
Im {{Firestone|Event|{{{name|}}}|currency_exchange}} kannst du {{Firestone|Event|{{{name|}}}|currencies}} gegen Truhen und [[currencies|Währungen]] eintauschen. Die Art der [[Gear Chests|Ausrüstungstruhe]] hängt von deiner [[character level|Charakterstufe]] ab und die Art der [[Jewel Chests|Juwelentruhe]] von der Anzahl deiner [[campaign stars|Kampagnensterne]]. Die Anzahl der Käufe ist für jeden angebotenen Truhen- und Währungstyp begrenzt.&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|exchange}}&lt;br /&gt;
&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|h3|avatar_exchange}}&lt;br /&gt;
&lt;br /&gt;
Bei »{{Firestone|Event|{{{name|}}}|avatar_exchange}}« kannst du {{Firestone|Event|{{{name|}}}|currencies}} gegen einzigartige [[avatar|Avatare]] eintauschen, die nur während des {{Firestone|Event|{{{name|}}}|name}} erhältlich sind.&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|avatars}}&lt;br /&gt;
&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|h3|shop}}&lt;br /&gt;
&lt;br /&gt;
Im {{Firestone|Event|{{{name|}}}|shopname}} kannst du {{Firestone|Event|{{{name|}}}|currencies}} mit [[Platforms|Plattform]]-Währung kaufen. Jeder Kauf im {{Firestone|Event|{{{name|}}}|shopname}} erhöht die Drop-Rate der {{Firestone|Event|{{{name|}}}|currencies}} auf dem Schlachtfeld für den Rest des Events auf 2 {{Firestone|Event|{{{name|}}}|currencies}} alle 28,8 Sekunden. Jedes Angebot kann nur einmal erworben werden.&lt;br /&gt;
&lt;br /&gt;
Anfangs werden die folgenden 3 Angebote angezeigt:&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|offers|start}}&lt;br /&gt;
&lt;br /&gt;
Sobald die Angebote 1–3 alle gekauft wurden, erscheinen die folgenden Angebote:&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|offers|more}}&lt;br /&gt;
&lt;br /&gt;
{{Navigation}}&lt;br /&gt;
[[Category:Browse]]&lt;br /&gt;
[[Category:Events]]&lt;/div&gt;</summary>
		<author><name>Webmaster</name></author>
	</entry>
	<entry>
		<id>https://firestone-idle-rpg.de/index.php?title=Modul:Firestone&amp;diff=3757</id>
		<title>Modul:Firestone</title>
		<link rel="alternate" type="text/html" href="https://firestone-idle-rpg.de/index.php?title=Modul:Firestone&amp;diff=3757"/>
		<updated>2025-10-28T09:31:33Z</updated>

		<summary type="html">&lt;p&gt;Webmaster: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;-- Modul:Firestone&lt;br /&gt;
local M = {}&lt;br /&gt;
&lt;br /&gt;
-- ========== kleine Helfer ==========&lt;br /&gt;
local COL_OFFER, COL_PRICE, COL_LIMIT = &amp;quot;50%&amp;quot;, &amp;quot;25%&amp;quot;, &amp;quot;25%&amp;quot;&lt;br /&gt;
local DAY = 86400&lt;br /&gt;
local function parse_iso_date(s)&lt;br /&gt;
  local y,m,d = tostring(s or &amp;quot;&amp;quot;):match(&amp;quot;^(%d%d%d%d)%-(%d%d)%-(%d%d)$&amp;quot;)&lt;br /&gt;
  if not y then return nil end&lt;br /&gt;
  return os.time{ year=tonumber(y), month=tonumber(m), day=tonumber(d), hour=12 }&lt;br /&gt;
end&lt;br /&gt;
local function ts_to_mw(ts) return os.date(&amp;quot;!%Y%m%d%H%M%S&amp;quot;, ts) end&lt;br /&gt;
local function format_date_de(v)&lt;br /&gt;
  local ts = (type(v)==&amp;quot;number&amp;quot;) and v or parse_iso_date(v)&lt;br /&gt;
  if not ts then return &amp;quot;&amp;quot; end&lt;br /&gt;
  local lang = mw.getContentLanguage()&lt;br /&gt;
  return lang:formatDate(&amp;quot;d.m.Y&amp;quot;, ts_to_mw(ts))&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function isempty(v) return v == nil or v == &amp;quot;&amp;quot; end&lt;br /&gt;
local function norm(s)&lt;br /&gt;
  if s == nil then return &amp;quot;&amp;quot; end&lt;br /&gt;
  s = mw.text.trim(tostring(s))&lt;br /&gt;
  return mw.ustring.lower(s):gsub(&amp;quot;%s+&amp;quot;,&amp;quot; &amp;quot;)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- bevorzugt Parent-Args (Vorlagen)&lt;br /&gt;
local function getArgs(frame)&lt;br /&gt;
  local p = frame:getParent()&lt;br /&gt;
  local src = (p and p.args) or frame.args or {}&lt;br /&gt;
  local a = {}&lt;br /&gt;
  for k,v in pairs(src) do&lt;br /&gt;
    if v ~= nil and v ~= &amp;quot;&amp;quot; then a[k] = mw.text.trim(tostring(v)) end&lt;br /&gt;
  end&lt;br /&gt;
  return a&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function has_items(t)&lt;br /&gt;
  if type(t) ~= &amp;quot;table&amp;quot; then return false end&lt;br /&gt;
  for _ in pairs(t) do return true end&lt;br /&gt;
  return false&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function pick_key(tbl, key)&lt;br /&gt;
  if type(tbl) ~= &amp;quot;table&amp;quot; then return nil end&lt;br /&gt;
  if tbl[key] ~= nil then return key end&lt;br /&gt;
  local k2 = tostring(key)&lt;br /&gt;
  if tbl[k2] ~= nil then return k2 end&lt;br /&gt;
  local kn = norm(k2)&lt;br /&gt;
  for k,_ in pairs(tbl) do if norm(k) == kn then return k end end&lt;br /&gt;
  return nil&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function deep_get(tbl, path)&lt;br /&gt;
  local cur = tbl&lt;br /&gt;
  for _,k in ipairs(path) do&lt;br /&gt;
    if type(cur) ~= &amp;quot;table&amp;quot; then return nil end&lt;br /&gt;
    local real = pick_key(cur, k); if not real then return nil end&lt;br /&gt;
    cur = cur[real]&lt;br /&gt;
  end&lt;br /&gt;
  return cur&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function fileTag(file, opts) -- [[Datei:...]]&lt;br /&gt;
  if isempty(file) then return &amp;quot;&amp;quot; end&lt;br /&gt;
  local parts = { &amp;quot;Datei:&amp;quot;..file }&lt;br /&gt;
  if opts and opts.size  then table.insert(parts, opts.size)  end&lt;br /&gt;
  if opts and opts.param then table.insert(parts, opts.param) end -- z.B. &amp;quot;thumb|Text&amp;quot;&lt;br /&gt;
  return string.format(&amp;quot;[[%s]]&amp;quot;, table.concat(parts, &amp;quot;|&amp;quot;))&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function pretty_range(s)&lt;br /&gt;
  if not s or s==&amp;quot;&amp;quot; then return &amp;quot;&amp;quot; end&lt;br /&gt;
  s = tostring(s)&lt;br /&gt;
  if s:sub(-1) == &amp;quot;-&amp;quot; then return s:sub(1, -2) .. &amp;quot;+&amp;quot; end&lt;br /&gt;
  return s:gsub(&amp;quot;%-&amp;quot;,&amp;quot;–&amp;quot;)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- ========== Lazy-Loads ==========&lt;br /&gt;
local I18N, EVENTS, HEROES&lt;br /&gt;
local function i18n()&lt;br /&gt;
  if I18N then return I18N end&lt;br /&gt;
  for _,t in ipairs{ &amp;quot;Modul:HeroI18n&amp;quot;, &amp;quot;Module:HeroI18n&amp;quot; } do&lt;br /&gt;
    local ok, data = pcall(mw.loadData, t) ; if ok and type(data)==&amp;quot;table&amp;quot; then I18N=data; return I18N end&lt;br /&gt;
    local ok2, mod = pcall(require, t)      ; if ok2 and type(mod) ==&amp;quot;table&amp;quot; then I18N=mod ; return I18N end&lt;br /&gt;
  end&lt;br /&gt;
  I18N = { i18n={}, events={ names={}, sections={default={}}, phrases={}, links={}, unlocks={} } }&lt;br /&gt;
  return I18N&lt;br /&gt;
end&lt;br /&gt;
local function events()&lt;br /&gt;
  if EVENTS then return EVENTS end&lt;br /&gt;
  for _,t in ipairs{ &amp;quot;Modul:EventData&amp;quot;, &amp;quot;Module:EventData&amp;quot; } do&lt;br /&gt;
    local ok, data = pcall(mw.loadData, t) ; if ok and type(data)==&amp;quot;table&amp;quot; and type(data.events)==&amp;quot;table&amp;quot; then EVENTS=data.events; return EVENTS end&lt;br /&gt;
    local ok2, mod = pcall(require, t)     ; if ok2 and type(mod) ==&amp;quot;table&amp;quot; and type(mod.events)==&amp;quot;table&amp;quot; then EVENTS=mod.events ; return EVENTS end&lt;br /&gt;
  end&lt;br /&gt;
  EVENTS = {}; return EVENTS&lt;br /&gt;
end&lt;br /&gt;
local function heroes()&lt;br /&gt;
  if HEROES then return HEROES end&lt;br /&gt;
  for _,t in ipairs{ &amp;quot;Modul:HeroData&amp;quot;, &amp;quot;Module:HeroData&amp;quot; } do&lt;br /&gt;
    local ok, data = pcall(mw.loadData, t) ; if ok and type(data)==&amp;quot;table&amp;quot; and type(data.heroes)==&amp;quot;table&amp;quot; then HEROES=data.heroes; return HEROES end&lt;br /&gt;
    local ok2, mod = pcall(require, t)     ; if ok2 and type(mod) ==&amp;quot;table&amp;quot; and type(mod.heroes)==&amp;quot;table&amp;quot; then HEROES=mod.heroes ; return HEROES end&lt;br /&gt;
  end&lt;br /&gt;
  HEROES = {}; return HEROES&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- ========== i18n / Labels ==========&lt;br /&gt;
local function ev_label(evkey, which)&lt;br /&gt;
  local E = i18n().events or {}&lt;br /&gt;
  local spec = (E.sections and E.sections[norm(evkey)]) or {}&lt;br /&gt;
  local def  = (E.sections and E.sections.default) or {}&lt;br /&gt;
  return (spec[which] or def[which] or &amp;quot;&amp;quot;)&lt;br /&gt;
end&lt;br /&gt;
local function ev_name(evkey)&lt;br /&gt;
  local names = (i18n().events or {}).names or {}&lt;br /&gt;
  return names[norm(evkey)] or evkey&lt;br /&gt;
end&lt;br /&gt;
local function header_labels(evkey)&lt;br /&gt;
  return ev_label(evkey, &amp;quot;offer&amp;quot;), ev_label(evkey, &amp;quot;price&amp;quot;), ev_label(evkey, &amp;quot;limit&amp;quot;)&lt;br /&gt;
end&lt;br /&gt;
local function autotr(key_name, value)&lt;br /&gt;
  if type(value) ~= &amp;quot;string&amp;quot; then return value end&lt;br /&gt;
  local k = norm(key_name)&lt;br /&gt;
&lt;br /&gt;
  -- 1) Standardwortschatz (falls vorhanden)&lt;br /&gt;
  local m1 = (i18n().i18n or {})[k]&lt;br /&gt;
  local v  = m1 and m1[norm(value)]&lt;br /&gt;
  if v then return v end&lt;br /&gt;
&lt;br /&gt;
  -- 2) Spezialfall Event-Typen: i18n().events.infobox.type&lt;br /&gt;
  if k == &amp;quot;type&amp;quot; then&lt;br /&gt;
    local m2 = (((i18n().events or {}).infobox) or {}).type or {}&lt;br /&gt;
    return m2[norm(value)] or value&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  return value&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function ev_notice_icon_wikitext(ev)&lt;br /&gt;
  local file = ev.currency and ev.currency.icon&lt;br /&gt;
  if not file or file == &amp;quot;&amp;quot; then return &amp;quot;&amp;quot; end&lt;br /&gt;
  local img = fileTag(file, { size = &amp;quot;24px&amp;quot;, param = &amp;quot;frameless|link=&amp;quot; })&lt;br /&gt;
  return &#039;&amp;lt;span class=&amp;quot;fs-ic&amp;quot;&amp;gt;&#039; .. img .. &#039;&amp;lt;/span&amp;gt;&#039;&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function ev_pagelink(ev, evkey, label)&lt;br /&gt;
  local page = ev.page&lt;br /&gt;
  if page and page ~= &amp;quot;&amp;quot; then&lt;br /&gt;
    return string.format(&amp;quot;[[%s|%s]]&amp;quot;, page, label)&lt;br /&gt;
  end&lt;br /&gt;
  return label&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
-- ========== Nummern + &amp;lt;ref&amp;gt; (voll i18n-gesteuert) ==========&lt;br /&gt;
local function parse_sci(v)&lt;br /&gt;
  if type(v) == &amp;quot;number&amp;quot; then return v end&lt;br /&gt;
  if type(v) == &amp;quot;string&amp;quot; then&lt;br /&gt;
    local a,e = v:match(&amp;quot;^%s*([%d%.]+)[eE]([%+%-]?%d+)%s*$&amp;quot;)&lt;br /&gt;
    if a and e then return (tonumber(a) or 0) * 10^(tonumber(e) or 0) end&lt;br /&gt;
    local n = tonumber(v); if n then return n end&lt;br /&gt;
  end&lt;br /&gt;
  return nil&lt;br /&gt;
end&lt;br /&gt;
local function i18n_big_units()&lt;br /&gt;
  local N = (i18n().numbers or {})&lt;br /&gt;
  return {&lt;br /&gt;
    {1e12, N.trillion_sing,  N.trillion_plur},&lt;br /&gt;
    {1e9,  N.billion_sing,   N.billion_plur},&lt;br /&gt;
    {1e6,  N.million_sing,   N.million_plur},&lt;br /&gt;
    {1e3,  N.thousand_sing,  N.thousand_plur},&lt;br /&gt;
  }&lt;br /&gt;
end&lt;br /&gt;
local function format_big(v)&lt;br /&gt;
  local n = parse_sci(v); if not n then return tostring(v or &amp;quot;&amp;quot;) end&lt;br /&gt;
  for _,u in ipairs(i18n_big_units()) do&lt;br /&gt;
    local base, sing, plur = u[1], u[2], u[3]&lt;br /&gt;
    if base and sing and plur and n &amp;gt;= base and math.floor(n % base) == 0 then&lt;br /&gt;
      local q = math.floor(n / base)&lt;br /&gt;
      return string.format(&amp;quot;%d %s&amp;quot;, q, (q==1) and sing or plur)&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
  return tostring(n)&lt;br /&gt;
end&lt;br /&gt;
local function ref_gear_power(v)&lt;br /&gt;
  if isempty(v) then return &amp;quot;&amp;quot; end&lt;br /&gt;
  local phr = (((i18n().events or {}).phrases) or {}).req_gear_power&lt;br /&gt;
  if isempty(phr) then return &amp;quot;&amp;quot; end&lt;br /&gt;
  local name = &amp;quot;gear-&amp;quot; .. tostring(v):gsub(&amp;quot;%s&amp;quot;,&amp;quot;&amp;quot;)&lt;br /&gt;
  return string.format(&#039;&amp;lt;ref name=&amp;quot;%s&amp;quot;&amp;gt;%s&amp;lt;/ref&amp;gt;&#039;, name, (phr:gsub(&amp;quot;$1&amp;quot;, format_big(v))))&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- ========== Event-Renderer ==========&lt;br /&gt;
local function htag(level, text)&lt;br /&gt;
  if isempty(text) then return &amp;quot;&amp;quot; end&lt;br /&gt;
  local eq = level==3 and &amp;quot;===&amp;quot; or &amp;quot;==&amp;quot;&lt;br /&gt;
  return string.format(&amp;quot;%s %s %s&amp;quot;, eq, text, eq)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function event_file(ev, evkey, which)&lt;br /&gt;
  if which==&amp;quot;deco&amp;quot; then&lt;br /&gt;
    local file   = ev.images and ev.images.deco or &amp;quot;&amp;quot;&lt;br /&gt;
    local legend = ev_label(evkey, &amp;quot;deco&amp;quot;)&lt;br /&gt;
    return fileTag(file, { param = &amp;quot;thumb|&amp;quot;..legend })&lt;br /&gt;
  elseif which==&amp;quot;fullname&amp;quot; then&lt;br /&gt;
    local file   = ev.images and ev.images.fullname or &amp;quot;&amp;quot;&lt;br /&gt;
    local legend = ev_label(evkey, &amp;quot;fullname&amp;quot;)&lt;br /&gt;
    return fileTag(file, { param = &amp;quot;thumb|&amp;quot;..legend })&lt;br /&gt;
  elseif which==&amp;quot;ex_shop&amp;quot; then&lt;br /&gt;
    local file   = ev.images and ev.images.ex_shop or &amp;quot;&amp;quot;&lt;br /&gt;
    local legend = ev_label(evkey, &amp;quot;ex_shop&amp;quot;)&lt;br /&gt;
    return fileTag(file, { param = &amp;quot;thumb|&amp;quot;..legend })&lt;br /&gt;
  elseif which==&amp;quot;banner&amp;quot; then&lt;br /&gt;
    return fileTag(ev.banner, { param=&amp;quot;thumb&amp;quot; })&lt;br /&gt;
  end&lt;br /&gt;
  return &amp;quot;&amp;quot;&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function event_link(ev, evkey, which)&lt;br /&gt;
  if which==&amp;quot;type&amp;quot; then&lt;br /&gt;
    local E = i18n().events or {}&lt;br /&gt;
    local page  = (E.links and E.links.type and E.links.type[ev.type or &amp;quot;&amp;quot;])&lt;br /&gt;
                  or (E.links and E.links.type) or &amp;quot;Calendar Events&amp;quot;&lt;br /&gt;
    local map   = (E.infobox and E.infobox.type) or {}&lt;br /&gt;
    local label = map[norm(ev.type or &amp;quot;&amp;quot;)] or (ev.type or &amp;quot;&amp;quot;)&lt;br /&gt;
    return string.format(&amp;quot;[[%s|%s]]&amp;quot;, page, label)&lt;br /&gt;
  end&lt;br /&gt;
  return &amp;quot;&amp;quot;&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function event_history(ev, evkey, args)&lt;br /&gt;
  local H = ev.history or {}; if not has_items(H) then return &amp;quot;&amp;quot; end&lt;br /&gt;
&lt;br /&gt;
  local phrases = ((i18n().events or {}).phrases or {})&lt;br /&gt;
  local alt_gp  = phrases.alt_platform or &amp;quot;auf Google Play&amp;quot;&lt;br /&gt;
  local showTxt = phrases.toggle_show  or &amp;quot;Anzeigen&amp;quot;&lt;br /&gt;
  local hideTxt = phrases.toggle_hide  or &amp;quot;Verbergen&amp;quot;&lt;br /&gt;
&lt;br /&gt;
  local function fmt_range_or_raw(s)&lt;br /&gt;
    if not s or s == &amp;quot;&amp;quot; then return &amp;quot;&amp;quot; end&lt;br /&gt;
    local a,b = tostring(s):match(&amp;quot;^(%d%d%d%d%-%d%d%-%d%d)%s+bis%s+(%d%d%d%d%-%d%d%-%d%d)$&amp;quot;)&lt;br /&gt;
    if a and b then&lt;br /&gt;
      return string.format(&amp;quot;%s bis %s&amp;quot;, format_date_de(a), format_date_de(b))&lt;br /&gt;
    end&lt;br /&gt;
    local ts = parse_iso_date(s)&lt;br /&gt;
    return ts and format_date_de(ts) or s&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  local lines = {}&lt;br /&gt;
  for _,h in ipairs(H) do&lt;br /&gt;
    local a = format_date_de(h.start)&lt;br /&gt;
    local b = format_date_de(h[&amp;quot;end&amp;quot;] or h.end_date)&lt;br /&gt;
    local line = string.format(&amp;quot;* %s: %s bis %s&amp;quot;, tostring(h.year or &amp;quot;&amp;quot;), a, b)&lt;br /&gt;
    if h.alt_platform then&lt;br /&gt;
      line = line .. string.format(&amp;quot; (%s %s)&amp;quot;, fmt_range_or_raw(h.alt_platform), alt_gp)&lt;br /&gt;
    end&lt;br /&gt;
    lines[#lines+1] = line&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  -- vorhersagbare/überschreibbare ID für den Toggle&lt;br /&gt;
  local id = (args and args.id) or (&amp;quot;fs-hist-&amp;quot; .. mw.ustring.gsub(tostring(evkey), &amp;quot;[^%w%-%_]&amp;quot;, &amp;quot;-&amp;quot;))&lt;br /&gt;
&lt;br /&gt;
  -- standardmäßig eingeklappt; Toggle-Texte können via phrases.* gesetzt werden&lt;br /&gt;
  return string.format(&lt;br /&gt;
    &#039;&amp;lt;div id=&amp;quot;%s&amp;quot; class=&amp;quot;mw-collapsible mw-collapsed fs-history&amp;quot; data-expandtext=&amp;quot;%s&amp;quot; data-collapsetext=&amp;quot;%s&amp;quot;&amp;gt;\n%s\n&amp;lt;/div&amp;gt;&#039;,&lt;br /&gt;
    id, mw.text.encode(showTxt), mw.text.encode(hideTxt), table.concat(lines, &amp;quot;\n&amp;quot;)&lt;br /&gt;
  )&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function item_label(key, amount)&lt;br /&gt;
  local I = (i18n().items or {})[key] or {}&lt;br /&gt;
  local n = tonumber(amount or 1) or 1&lt;br /&gt;
  local txt = (n == 1) and (I.sing or key) or (I.plur or (I.sing or key))&lt;br /&gt;
  local disp = I.link and string.format(&amp;quot;[[%s|%s]]&amp;quot;, I.link, txt) or txt&lt;br /&gt;
  return string.format(&amp;quot;%d %s&amp;quot;, n, disp)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- references/render&lt;br /&gt;
local FRAME = nil&lt;br /&gt;
local function tabber_render_from_content(content)&lt;br /&gt;
  if FRAME and FRAME.extensionTag then&lt;br /&gt;
    return FRAME:extensionTag(&#039;tabber&#039;, content)&lt;br /&gt;
  end&lt;br /&gt;
  return &amp;quot;&amp;lt;tabber&amp;gt;\n&amp;quot; .. content .. &amp;quot;\n&amp;lt;/tabber&amp;gt;&amp;quot;&lt;br /&gt;
end&lt;br /&gt;
local function references_tag()&lt;br /&gt;
  if FRAME and FRAME.extensionTag then&lt;br /&gt;
    return FRAME:extensionTag(&#039;references&#039;, &#039;&#039;)&lt;br /&gt;
  end&lt;br /&gt;
  return &#039;&amp;lt;references /&amp;gt;&#039;&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- innerer Tab-Inhalt: je Range&lt;br /&gt;
local function build_tabber_content_for(list)&lt;br /&gt;
  local out = {}&lt;br /&gt;
  for _,it in ipairs(list) do&lt;br /&gt;
    local label   = pretty_range(it.range or &amp;quot;&amp;quot;)&lt;br /&gt;
    local note    = ref_gear_power(it.req_gear_power)&lt;br /&gt;
    local content = string.format(&lt;br /&gt;
      &amp;quot;%s %s%s&amp;quot;,&lt;br /&gt;
      fileTag(it.icon or &amp;quot;&amp;quot;, { size=&amp;quot;30px&amp;quot; }),&lt;br /&gt;
      (it.item and item_label(it.item, it.amount or 1)) or (it.title or &amp;quot;&amp;quot;),&lt;br /&gt;
      note&lt;br /&gt;
    )&lt;br /&gt;
    out[#out+1] = &amp;quot;|-|&amp;quot; .. label .. &amp;quot;=\n&amp;quot; .. content&lt;br /&gt;
  end&lt;br /&gt;
  return table.concat(out, &amp;quot;\n&amp;quot;)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Gruppen-Label aus i18n&lt;br /&gt;
local function group_label(kind)&lt;br /&gt;
  local G = ((i18n().events or {}).group_labels or {})&lt;br /&gt;
  return G[kind] or kind&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function event_avatars(ev, evkey)&lt;br /&gt;
  local AV = ev.avatars&lt;br /&gt;
  if not has_items(AV) then return &amp;quot;&amp;quot; end&lt;br /&gt;
&lt;br /&gt;
  local token_icon = (ev.currency or {}).icon or &amp;quot;&amp;quot;&lt;br /&gt;
  local token_name = ev_label(evkey, &amp;quot;currencies&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
  local years, out = {}, {}&lt;br /&gt;
  for y,_ in pairs(AV) do table.insert(years, tonumber(y) or y) end&lt;br /&gt;
  table.sort(years, function(a,b) return tostring(a) &amp;lt; tostring(b) end)&lt;br /&gt;
&lt;br /&gt;
  for _,year in ipairs(years) do&lt;br /&gt;
    local list = AV[year] or AV[tostring(year)]&lt;br /&gt;
    if has_items(list) then&lt;br /&gt;
      out[#out+1] = (&amp;quot;==== %s ====&amp;quot;):format(year)&lt;br /&gt;
      out[#out+1] = &#039;{| class=&amp;quot;article-table&amp;quot; style=&amp;quot;font-size:14px;&amp;quot;&#039;&lt;br /&gt;
      out[#out+1] = &#039;! colspan=&amp;quot;2&amp;quot; | Avatar\n! Preis&#039;&lt;br /&gt;
      for _,it in ipairs(list) do&lt;br /&gt;
        out[#out+1] = &#039;|-&#039;&lt;br /&gt;
        out[#out+1] = string.format(&#039;|%s || %s || %s %s %s&#039;,&lt;br /&gt;
          fileTag(it.file, { size=&amp;quot;50px&amp;quot; }),&lt;br /&gt;
          it.title or &amp;quot;&amp;quot;,&lt;br /&gt;
          fileTag(token_icon, { size=&amp;quot;25px&amp;quot; }),&lt;br /&gt;
          tostring(it.cost or 0),&lt;br /&gt;
          token_name&lt;br /&gt;
        )&lt;br /&gt;
      end&lt;br /&gt;
      out[#out+1] = &#039;|}&#039;&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
  return (#out &amp;gt; 0) and table.concat(out, &amp;quot;\n&amp;quot;) or &amp;quot;&amp;quot;&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Austausch-Tabelle (äußerer Tab: Gruppen → innerer Tab: Ranges)&lt;br /&gt;
local function event_exchange_tabs(ev, evkey)&lt;br /&gt;
  local ex = ev.exchange or {}&lt;br /&gt;
  local token_icon = (ev.currency or {}).icon or &amp;quot;&amp;quot;&lt;br /&gt;
  local token_name = ev_label(evkey, &amp;quot;currencies&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
  if not (has_items(ex.chests_by_level)&lt;br /&gt;
       or has_items(ex.chests_by_stars)&lt;br /&gt;
       or has_items(ex.chests_by_oracle)&lt;br /&gt;
       or has_items(ex.currencies)) then&lt;br /&gt;
    return &amp;quot;&amp;quot;&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  local outer_parts = {}&lt;br /&gt;
  local function add_group(kind, list)&lt;br /&gt;
    if not has_items(list) then return end&lt;br /&gt;
    local inner_content = build_tabber_content_for(list)&lt;br /&gt;
    local inner_tabber  = tabber_render_from_content(inner_content)&lt;br /&gt;
    outer_parts[#outer_parts+1] = &amp;quot;|-|&amp;quot; .. group_label(kind) .. &amp;quot;=\n&amp;quot; .. inner_tabber&lt;br /&gt;
  end&lt;br /&gt;
  add_group(&amp;quot;level&amp;quot;,  ex.chests_by_level)&lt;br /&gt;
  add_group(&amp;quot;stars&amp;quot;,  ex.chests_by_stars)&lt;br /&gt;
  add_group(&amp;quot;oracle&amp;quot;, ex.chests_by_oracle)&lt;br /&gt;
&lt;br /&gt;
  local left_tabber = tabber_render_from_content(table.concat(outer_parts, &amp;quot;\n&amp;quot;))&lt;br /&gt;
&lt;br /&gt;
  -- Preis/Limit (erste gefundene Gruppe)&lt;br /&gt;
  local function pick_price_limit()&lt;br /&gt;
    if has_items(ex.chests_by_level)  then return ex.chests_by_level[1].price or 0,  ex.chests_by_level[1].limit or &amp;quot;&amp;quot;  end&lt;br /&gt;
    if has_items(ex.chests_by_stars)  then return ex.chests_by_stars[1].price or 0,  ex.chests_by_stars[1].limit or &amp;quot;&amp;quot;  end&lt;br /&gt;
    if has_items(ex.chests_by_oracle) then return ex.chests_by_oracle[1].price or 0, ex.chests_by_oracle[1].limit or &amp;quot;&amp;quot; end&lt;br /&gt;
    return 0, &amp;quot;&amp;quot;&lt;br /&gt;
  end&lt;br /&gt;
  local price, limit = pick_price_limit()&lt;br /&gt;
&lt;br /&gt;
  local t = {}&lt;br /&gt;
  local H_offer, H_price, H_limit = header_labels(evkey)&lt;br /&gt;
  t[#t+1] = &#039;{| class=&amp;quot;article-table fs-exchange&amp;quot; style=&amp;quot;font-size:14px;&amp;quot;&#039;&lt;br /&gt;
  t[#t+1] = string.format(&lt;br /&gt;
    &#039;! style=&amp;quot;width:%s;&amp;quot; | %s !! style=&amp;quot;width:%s;&amp;quot; | %s !! style=&amp;quot;width:%s;&amp;quot; | %s&#039;,&lt;br /&gt;
    COL_OFFER, H_offer, COL_PRICE, H_price, COL_LIMIT, H_limit&lt;br /&gt;
  )&lt;br /&gt;
  t[#t+1] = &#039;|-&#039;&lt;br /&gt;
  t[#t+1] = &#039;|&#039; .. left_tabber .. string.format(&lt;br /&gt;
    &#039; || style=&amp;quot;text-align:center;&amp;quot; | %s %s %s || style=&amp;quot;text-align:center;&amp;quot; | %s&#039;,&lt;br /&gt;
    fileTag(token_icon, { size=&amp;quot;25px&amp;quot; }),&lt;br /&gt;
    tostring(price), token_name,&lt;br /&gt;
    tostring(limit)&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
  if has_items(ex.currencies) then&lt;br /&gt;
    for _,it in ipairs(ex.currencies) do&lt;br /&gt;
      local offer = string.format(&#039;%s %s&#039;,&lt;br /&gt;
        fileTag(it.icon or &amp;quot;&amp;quot;, { size=&amp;quot;30px&amp;quot; }),&lt;br /&gt;
        item_label(it.item, it.amount or 1)&lt;br /&gt;
      )&lt;br /&gt;
      t[#t+1] = &#039;|-&#039;&lt;br /&gt;
      t[#t+1] = string.format(&lt;br /&gt;
        &#039;| %s || style=&amp;quot;text-align:center;&amp;quot; | %s %s %s || style=&amp;quot;text-align:center;&amp;quot; | %s&#039;,&lt;br /&gt;
        offer,&lt;br /&gt;
        fileTag(token_icon, { size=&amp;quot;25px&amp;quot; }),&lt;br /&gt;
        tostring(it.price or 0), token_name,&lt;br /&gt;
        tostring(it.limit or &amp;quot;&amp;quot;)&lt;br /&gt;
      )&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  t[#t+1] = &#039;|}&#039;&lt;br /&gt;
  t[#t+1] = references_tag()&lt;br /&gt;
  return &#039;\n&#039; .. table.concat(t, &#039;\n&#039;)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function event_offers(ev, evkey, which)&lt;br /&gt;
  local list = (ev.shop or {})[which]&lt;br /&gt;
  if not has_items(list) then return &amp;quot;&amp;quot; end&lt;br /&gt;
  local token_icon = (ev.currency or {}).icon or &amp;quot;&amp;quot;&lt;br /&gt;
  local token_name = ev_label(evkey, &amp;quot;currencies&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
  local t = {}&lt;br /&gt;
  t[#t+1] = &#039;{| class=&amp;quot;article-table&amp;quot; style=&amp;quot;font-size:14px;&amp;quot;&#039;&lt;br /&gt;
  t[#t+1] = &amp;quot;! Angebot !! Menge&amp;quot;&lt;br /&gt;
  for _,it in ipairs(list) do&lt;br /&gt;
    t[#t+1] = &amp;quot;|-&amp;quot;&lt;br /&gt;
    t[#t+1] = string.format(&amp;quot;| &#039;&#039;%s&#039;&#039;&amp;lt;br /&amp;gt;%s || %s %s %s&amp;quot;,&lt;br /&gt;
      it.name or &amp;quot;&amp;quot;,&lt;br /&gt;
      fileTag(it.image, { size=&amp;quot;x50px&amp;quot; }),&lt;br /&gt;
      fileTag(token_icon, { size=&amp;quot;30px&amp;quot; }),&lt;br /&gt;
      tostring(it.token or 0),&lt;br /&gt;
      token_name&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
  t[#t+1] = &amp;quot;|}&amp;quot;&lt;br /&gt;
  return table.concat(t, &amp;quot;\n&amp;quot;)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- ========== SiteNotice-Support (Datum/Auto-Pick) ==========&lt;br /&gt;
local function event_window(ev)&lt;br /&gt;
  local S = ev.schedule or ev&lt;br /&gt;
  local s = S.start_date; if isempty(s) then return nil end&lt;br /&gt;
&lt;br /&gt;
  local st = parse_iso_date(s); if not st then return nil end&lt;br /&gt;
&lt;br /&gt;
  local en_excl, en_incl&lt;br /&gt;
  if not isempty(S.end_date) then&lt;br /&gt;
    -- end_date wird als inklusives Kalenderdatum verstanden&lt;br /&gt;
    en_incl = parse_iso_date(S.end_date)&lt;br /&gt;
    if not en_incl then return st, nil, nil end&lt;br /&gt;
    en_excl = en_incl + DAY&lt;br /&gt;
  else&lt;br /&gt;
    -- Dauer in Tagen (Standard 14), exklusives Ende = st + d*DAY&lt;br /&gt;
    local d = tonumber(S.duration_days) or 14&lt;br /&gt;
    en_excl = st + d * DAY&lt;br /&gt;
    en_incl = en_excl - DAY&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  return st, en_excl, en_incl&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function pick_notice_eventkey()&lt;br /&gt;
  local cfg = ((i18n().events or {}).notice or {})&lt;br /&gt;
  local pre  = tonumber(cfg.pre_days)  or 7&lt;br /&gt;
  local post = tonumber(cfg.post_days) or 3&lt;br /&gt;
  local now = os.time()&lt;br /&gt;
&lt;br /&gt;
  local running, upcoming, ended_recent = {}, {}, {}&lt;br /&gt;
&lt;br /&gt;
  for k, ev in pairs(events()) do&lt;br /&gt;
    local st, en_excl = event_window(ev)&lt;br /&gt;
    if st and en_excl then&lt;br /&gt;
      if now &amp;gt;= st and now &amp;lt; en_excl then&lt;br /&gt;
        table.insert(running, { k=k, st=st, en=en_excl })&lt;br /&gt;
      elseif now &amp;gt;= st - pre*DAY and now &amp;lt; st then&lt;br /&gt;
        table.insert(upcoming, { k=k, st=st, en=en_excl })&lt;br /&gt;
      elseif now &amp;gt;= en_excl and now &amp;lt; en_excl + post*DAY then&lt;br /&gt;
        table.insert(ended_recent, { k=k, st=st, en=en_excl })&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  local function by_end(a,b)   return a.en &amp;lt; b.en end&lt;br /&gt;
  local function by_start(a,b) return a.st &amp;lt; b.st end&lt;br /&gt;
  if #running      &amp;gt; 0 then table.sort(running,      by_end)   ; return running[1].k      end&lt;br /&gt;
  if #upcoming     &amp;gt; 0 then table.sort(upcoming,     by_start) ; return upcoming[1].k     end&lt;br /&gt;
  if #ended_recent &amp;gt; 0 then table.sort(ended_recent, by_end)   ; return ended_recent[1].k end&lt;br /&gt;
  return nil&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function event_notice(ev, evkey)&lt;br /&gt;
  local phrases = (((i18n().events or {}).phrases) or {})&lt;br /&gt;
  local PH_RUNNING = phrases.event_notice   -- &amp;quot;Das $1 läuft von $2 bis $3&amp;quot;&lt;br /&gt;
  local PH_ENDED   = phrases.event_ended    -- &amp;quot;Das $1 ist beendet. Du kannst verbleibende $4 noch im $5 eintauschen.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
  -- Ohne Texte kein Hinweis&lt;br /&gt;
  if isempty(PH_RUNNING) then return &amp;quot;&amp;quot; end&lt;br /&gt;
&lt;br /&gt;
  local st, en_excl, en_incl = event_window(ev); if not st or not en_excl then return &amp;quot;&amp;quot; end&lt;br /&gt;
&lt;br /&gt;
  local cfg  = ((i18n().events or {}).notice or {})&lt;br /&gt;
  local pre  = tonumber(cfg.pre_days)  or 7   -- z. B. 7 Tage vorher&lt;br /&gt;
  local post = tonumber(cfg.post_days) or 3   -- z. B. 3 Tage danach&lt;br /&gt;
  local now  = os.time()&lt;br /&gt;
&lt;br /&gt;
  local show_from = st - pre  * DAY&lt;br /&gt;
  local show_till = en_excl + post * DAY&lt;br /&gt;
  if now &amp;lt; show_from or now &amp;gt;= show_till then&lt;br /&gt;
    return &amp;quot;&amp;quot;&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  -- Anzeigename (aus i18n sections oder names)&lt;br /&gt;
  local name_label = ev_label(evkey, &amp;quot;eventname&amp;quot;)&lt;br /&gt;
  if isempty(name_label) then name_label = ev_name(evkey) end&lt;br /&gt;
&lt;br /&gt;
  -- Verlinken NUR wenn ev.page existiert&lt;br /&gt;
  local name = ev_pagelink(ev, evkey, name_label)&lt;br /&gt;
&lt;br /&gt;
  -- Icon aus EventData → currency.icon&lt;br /&gt;
  local ic = ev_notice_icon_wikitext(ev)  -- &amp;quot;&amp;quot; wenn kein currency.icon&lt;br /&gt;
&lt;br /&gt;
  local text&lt;br /&gt;
  if now &amp;lt; en_excl then&lt;br /&gt;
    -- läuft / startet bald&lt;br /&gt;
    text = PH_RUNNING&lt;br /&gt;
      :gsub(&amp;quot;$1&amp;quot;, name)&lt;br /&gt;
      :gsub(&amp;quot;$2&amp;quot;, format_date_de(st))&lt;br /&gt;
      :gsub(&amp;quot;$3&amp;quot;, format_date_de(en_excl))&lt;br /&gt;
  else&lt;br /&gt;
    -- beendet (bis zu post_days danach)&lt;br /&gt;
    if isempty(PH_ENDED) then return &amp;quot;&amp;quot; end&lt;br /&gt;
    -- Labels aus i18n.sections (wie gehabt)&lt;br /&gt;
    local curr = ev_label(evkey, &amp;quot;currencies&amp;quot;)&lt;br /&gt;
    local shop = ev_label(evkey, &amp;quot;ex_shop&amp;quot;)&lt;br /&gt;
    if isempty(curr) or isempty(shop) then return &amp;quot;&amp;quot; end&lt;br /&gt;
    text = PH_ENDED&lt;br /&gt;
      :gsub(&amp;quot;$1&amp;quot;, name)&lt;br /&gt;
      :gsub(&amp;quot;$4&amp;quot;, curr)&lt;br /&gt;
      :gsub(&amp;quot;$5&amp;quot;, shop)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  local prefix = (ic ~= &amp;quot;&amp;quot; and (ic .. &amp;quot; &amp;quot;) or &amp;quot;&amp;quot;)&lt;br /&gt;
  local suffix = (ic ~= &amp;quot;&amp;quot; and (&amp;quot; &amp;quot; .. ic) or &amp;quot;&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
  return string.format(&lt;br /&gt;
    &#039;&amp;lt;div class=&amp;quot;fs-notice&amp;quot; data-ev=&amp;quot;%s&amp;quot;&amp;gt;%s%s%s&amp;lt;/div&amp;gt;&#039;,&lt;br /&gt;
    mw.text.encode(evkey),&lt;br /&gt;
    prefix, text, suffix&lt;br /&gt;
  )&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- ========== Dispatcher ==========&lt;br /&gt;
local function get_event(evkey)&lt;br /&gt;
  local evs = events()&lt;br /&gt;
  local real = pick_key(evs, evkey)&lt;br /&gt;
  return real and evs[real] or nil, real or evkey&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function dispatch_event(args)&lt;br /&gt;
  local evkey = args[2]&lt;br /&gt;
&lt;br /&gt;
  -- {{Firestone|Event|notice}}  → automatisch wählen&lt;br /&gt;
  if evkey and norm(evkey) == &amp;quot;notice&amp;quot; then&lt;br /&gt;
    local pick = pick_notice_eventkey()&lt;br /&gt;
    if not pick then return &amp;quot;&amp;quot; end&lt;br /&gt;
    local ev, r = get_event(pick); if not ev then return &amp;quot;&amp;quot; end&lt;br /&gt;
    return event_notice(ev, r)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  -- normaler Weg&lt;br /&gt;
  if isempty(evkey) then return &amp;quot;&amp;quot; end&lt;br /&gt;
  local ev, realkey = get_event(evkey); if not ev then return &amp;quot;&amp;quot; end&lt;br /&gt;
&lt;br /&gt;
  local a3, a4 = args[3], args[4]&lt;br /&gt;
  if isempty(a3) then return ev_name(realkey) end&lt;br /&gt;
&lt;br /&gt;
  if a3==&amp;quot;notice&amp;quot;  then return event_notice(ev, realkey)&lt;br /&gt;
  elseif a3==&amp;quot;file&amp;quot;   then return event_file(ev, realkey, a4 or &amp;quot;&amp;quot;)&lt;br /&gt;
  elseif a3==&amp;quot;link&amp;quot;   then return event_link(ev, realkey, a4 or &amp;quot;&amp;quot;)&lt;br /&gt;
  elseif a3==&amp;quot;h2&amp;quot;     then return htag(2, ev_label(realkey, a4 or &amp;quot;&amp;quot;))&lt;br /&gt;
  elseif a3==&amp;quot;h3&amp;quot;     then return htag(3, ev_label(realkey, a4 or &amp;quot;&amp;quot;))&lt;br /&gt;
  elseif a3==&amp;quot;history&amp;quot;  then return event_history(ev, realkey, a)&lt;br /&gt;
  elseif a3==&amp;quot;avatars&amp;quot;  then return event_avatars(ev, realkey)&lt;br /&gt;
  elseif a3==&amp;quot;exchange&amp;quot; then return event_exchange_tabs(ev, realkey)&lt;br /&gt;
  elseif a3==&amp;quot;offers&amp;quot;   then&lt;br /&gt;
    if a4==&amp;quot;start&amp;quot; then return event_offers(ev, realkey, &amp;quot;initial&amp;quot;)&lt;br /&gt;
    elseif a4==&amp;quot;more&amp;quot; then return event_offers(ev, realkey, &amp;quot;after&amp;quot;) end&lt;br /&gt;
    return &amp;quot;&amp;quot;&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  -- Sonderfälle für Währungsnamen aus i18n.sections&lt;br /&gt;
  if a3 == &amp;quot;currency&amp;quot;  then return ev_label(realkey, &amp;quot;currency&amp;quot;)  end&lt;br /&gt;
  if a3 == &amp;quot;currencies&amp;quot; then return ev_label(realkey, &amp;quot;currencies&amp;quot;) end&lt;br /&gt;
&lt;br /&gt;
  -- generischer Pfad in EventData&lt;br /&gt;
  local path, i = {}, 3&lt;br /&gt;
  while args[i] do table.insert(path, args[i]); i=i+1 end&lt;br /&gt;
  local v = deep_get(ev, path)&lt;br /&gt;
  if v ~= nil then&lt;br /&gt;
    local last = path[#path] and tostring(path[#path]) or &amp;quot;&amp;quot;&lt;br /&gt;
    v = autotr(last, v)&lt;br /&gt;
    return tostring(v)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  -- Fallback auf Labels/Name/Unlocks&lt;br /&gt;
  local last = norm(path[#path] or a3)&lt;br /&gt;
  if last==&amp;quot;eventname&amp;quot; or last==&amp;quot;name&amp;quot; then return ev_name(realkey) end&lt;br /&gt;
  local lbl = ev_label(realkey, last); if not isempty(lbl) then return lbl end&lt;br /&gt;
  local U = ((i18n().events or {}).unlocks or {})[last]&lt;br /&gt;
  if U then return U end&lt;br /&gt;
&lt;br /&gt;
  return &amp;quot;&amp;quot;&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function dispatch_hero(args)&lt;br /&gt;
  local name = args[2]; if isempty(name) then return &amp;quot;&amp;quot; end&lt;br /&gt;
  local H = heroes()&lt;br /&gt;
  local real = pick_key(H, name); if not real then return &amp;quot;&amp;quot; end&lt;br /&gt;
  local h = H[real]&lt;br /&gt;
  local path, i = {}, 3&lt;br /&gt;
  while args[i] do table.insert(path, args[i]); i=i+1 end&lt;br /&gt;
  local v = deep_get(h, path)&lt;br /&gt;
  local last = path[#path] and tostring(path[#path]) or &amp;quot;&amp;quot;&lt;br /&gt;
  v = autotr(last, v)&lt;br /&gt;
  return tostring(v or &amp;quot;&amp;quot;)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- ========== main ==========&lt;br /&gt;
function M.main(frame)&lt;br /&gt;
  FRAME = frame&lt;br /&gt;
  local a = getArgs(frame)&lt;br /&gt;
  for i=1,10 do a[i] = a[i] and tostring(a[i]) or nil end&lt;br /&gt;
  local dom = a[1] and norm(a[1]) or &amp;quot;&amp;quot;&lt;br /&gt;
  if dom==&amp;quot;event&amp;quot; then&lt;br /&gt;
    return dispatch_event(a)&lt;br /&gt;
  elseif dom==&amp;quot;hero&amp;quot; then&lt;br /&gt;
    return dispatch_hero(a)&lt;br /&gt;
  else&lt;br /&gt;
    return &amp;quot;&amp;quot;&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
return M&lt;/div&gt;</summary>
		<author><name>Webmaster</name></author>
	</entry>
	<entry>
		<id>https://firestone-idle-rpg.de/index.php?title=Modul:Firestone&amp;diff=3756</id>
		<title>Modul:Firestone</title>
		<link rel="alternate" type="text/html" href="https://firestone-idle-rpg.de/index.php?title=Modul:Firestone&amp;diff=3756"/>
		<updated>2025-10-28T09:28:55Z</updated>

		<summary type="html">&lt;p&gt;Webmaster: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;-- Modul:Firestone&lt;br /&gt;
local M = {}&lt;br /&gt;
&lt;br /&gt;
-- ========== kleine Helfer ==========&lt;br /&gt;
local COL_OFFER, COL_PRICE, COL_LIMIT = &amp;quot;50%&amp;quot;, &amp;quot;25%&amp;quot;, &amp;quot;25%&amp;quot;&lt;br /&gt;
local DAY = 86400&lt;br /&gt;
local function parse_iso_date(s)&lt;br /&gt;
  local y,m,d = tostring(s or &amp;quot;&amp;quot;):match(&amp;quot;^(%d%d%d%d)%-(%d%d)%-(%d%d)$&amp;quot;)&lt;br /&gt;
  if not y then return nil end&lt;br /&gt;
  return os.time{ year=tonumber(y), month=tonumber(m), day=tonumber(d), hour=12 }&lt;br /&gt;
end&lt;br /&gt;
local function ts_to_mw(ts) return os.date(&amp;quot;!%Y%m%d%H%M%S&amp;quot;, ts) end&lt;br /&gt;
local function format_date_de(v)&lt;br /&gt;
  local ts = (type(v)==&amp;quot;number&amp;quot;) and v or parse_iso_date(v)&lt;br /&gt;
  if not ts then return &amp;quot;&amp;quot; end&lt;br /&gt;
  local lang = mw.getContentLanguage()&lt;br /&gt;
  return lang:formatDate(&amp;quot;d.m.Y&amp;quot;, ts_to_mw(ts))&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function isempty(v) return v == nil or v == &amp;quot;&amp;quot; end&lt;br /&gt;
local function norm(s)&lt;br /&gt;
  if s == nil then return &amp;quot;&amp;quot; end&lt;br /&gt;
  s = mw.text.trim(tostring(s))&lt;br /&gt;
  return mw.ustring.lower(s):gsub(&amp;quot;%s+&amp;quot;,&amp;quot; &amp;quot;)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- bevorzugt Parent-Args (Vorlagen)&lt;br /&gt;
local function getArgs(frame)&lt;br /&gt;
  local p = frame:getParent()&lt;br /&gt;
  local src = (p and p.args) or frame.args or {}&lt;br /&gt;
  local a = {}&lt;br /&gt;
  for k,v in pairs(src) do&lt;br /&gt;
    if v ~= nil and v ~= &amp;quot;&amp;quot; then a[k] = mw.text.trim(tostring(v)) end&lt;br /&gt;
  end&lt;br /&gt;
  return a&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function has_items(t)&lt;br /&gt;
  if type(t) ~= &amp;quot;table&amp;quot; then return false end&lt;br /&gt;
  for _ in pairs(t) do return true end&lt;br /&gt;
  return false&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function pick_key(tbl, key)&lt;br /&gt;
  if type(tbl) ~= &amp;quot;table&amp;quot; then return nil end&lt;br /&gt;
  if tbl[key] ~= nil then return key end&lt;br /&gt;
  local k2 = tostring(key)&lt;br /&gt;
  if tbl[k2] ~= nil then return k2 end&lt;br /&gt;
  local kn = norm(k2)&lt;br /&gt;
  for k,_ in pairs(tbl) do if norm(k) == kn then return k end end&lt;br /&gt;
  return nil&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function deep_get(tbl, path)&lt;br /&gt;
  local cur = tbl&lt;br /&gt;
  for _,k in ipairs(path) do&lt;br /&gt;
    if type(cur) ~= &amp;quot;table&amp;quot; then return nil end&lt;br /&gt;
    local real = pick_key(cur, k); if not real then return nil end&lt;br /&gt;
    cur = cur[real]&lt;br /&gt;
  end&lt;br /&gt;
  return cur&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function fileTag(file, opts) -- [[Datei:...]]&lt;br /&gt;
  if isempty(file) then return &amp;quot;&amp;quot; end&lt;br /&gt;
  local parts = { &amp;quot;Datei:&amp;quot;..file }&lt;br /&gt;
  if opts and opts.size  then table.insert(parts, opts.size)  end&lt;br /&gt;
  if opts and opts.param then table.insert(parts, opts.param) end -- z.B. &amp;quot;thumb|Text&amp;quot;&lt;br /&gt;
  return string.format(&amp;quot;[[%s]]&amp;quot;, table.concat(parts, &amp;quot;|&amp;quot;))&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function pretty_range(s)&lt;br /&gt;
  if not s or s==&amp;quot;&amp;quot; then return &amp;quot;&amp;quot; end&lt;br /&gt;
  s = tostring(s)&lt;br /&gt;
  if s:sub(-1) == &amp;quot;-&amp;quot; then return s:sub(1, -2) .. &amp;quot;+&amp;quot; end&lt;br /&gt;
  return s:gsub(&amp;quot;%-&amp;quot;,&amp;quot;–&amp;quot;)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- ========== Lazy-Loads ==========&lt;br /&gt;
local I18N, EVENTS, HEROES&lt;br /&gt;
local function i18n()&lt;br /&gt;
  if I18N then return I18N end&lt;br /&gt;
  for _,t in ipairs{ &amp;quot;Modul:HeroI18n&amp;quot;, &amp;quot;Module:HeroI18n&amp;quot; } do&lt;br /&gt;
    local ok, data = pcall(mw.loadData, t) ; if ok and type(data)==&amp;quot;table&amp;quot; then I18N=data; return I18N end&lt;br /&gt;
    local ok2, mod = pcall(require, t)      ; if ok2 and type(mod) ==&amp;quot;table&amp;quot; then I18N=mod ; return I18N end&lt;br /&gt;
  end&lt;br /&gt;
  I18N = { i18n={}, events={ names={}, sections={default={}}, phrases={}, links={}, unlocks={} } }&lt;br /&gt;
  return I18N&lt;br /&gt;
end&lt;br /&gt;
local function events()&lt;br /&gt;
  if EVENTS then return EVENTS end&lt;br /&gt;
  for _,t in ipairs{ &amp;quot;Modul:EventData&amp;quot;, &amp;quot;Module:EventData&amp;quot; } do&lt;br /&gt;
    local ok, data = pcall(mw.loadData, t) ; if ok and type(data)==&amp;quot;table&amp;quot; and type(data.events)==&amp;quot;table&amp;quot; then EVENTS=data.events; return EVENTS end&lt;br /&gt;
    local ok2, mod = pcall(require, t)     ; if ok2 and type(mod) ==&amp;quot;table&amp;quot; and type(mod.events)==&amp;quot;table&amp;quot; then EVENTS=mod.events ; return EVENTS end&lt;br /&gt;
  end&lt;br /&gt;
  EVENTS = {}; return EVENTS&lt;br /&gt;
end&lt;br /&gt;
local function heroes()&lt;br /&gt;
  if HEROES then return HEROES end&lt;br /&gt;
  for _,t in ipairs{ &amp;quot;Modul:HeroData&amp;quot;, &amp;quot;Module:HeroData&amp;quot; } do&lt;br /&gt;
    local ok, data = pcall(mw.loadData, t) ; if ok and type(data)==&amp;quot;table&amp;quot; and type(data.heroes)==&amp;quot;table&amp;quot; then HEROES=data.heroes; return HEROES end&lt;br /&gt;
    local ok2, mod = pcall(require, t)     ; if ok2 and type(mod) ==&amp;quot;table&amp;quot; and type(mod.heroes)==&amp;quot;table&amp;quot; then HEROES=mod.heroes ; return HEROES end&lt;br /&gt;
  end&lt;br /&gt;
  HEROES = {}; return HEROES&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- ========== i18n / Labels ==========&lt;br /&gt;
local function ev_label(evkey, which)&lt;br /&gt;
  local E = i18n().events or {}&lt;br /&gt;
  local spec = (E.sections and E.sections[norm(evkey)]) or {}&lt;br /&gt;
  local def  = (E.sections and E.sections.default) or {}&lt;br /&gt;
  return (spec[which] or def[which] or &amp;quot;&amp;quot;)&lt;br /&gt;
end&lt;br /&gt;
local function ev_name(evkey)&lt;br /&gt;
  local names = (i18n().events or {}).names or {}&lt;br /&gt;
  return names[norm(evkey)] or evkey&lt;br /&gt;
end&lt;br /&gt;
local function header_labels(evkey)&lt;br /&gt;
  return ev_label(evkey, &amp;quot;offer&amp;quot;), ev_label(evkey, &amp;quot;price&amp;quot;), ev_label(evkey, &amp;quot;limit&amp;quot;)&lt;br /&gt;
end&lt;br /&gt;
local function autotr(key_name, value)&lt;br /&gt;
  if type(value) ~= &amp;quot;string&amp;quot; then return value end&lt;br /&gt;
  local k = norm(key_name)&lt;br /&gt;
&lt;br /&gt;
  -- 1) Standardwortschatz (falls vorhanden)&lt;br /&gt;
  local m1 = (i18n().i18n or {})[k]&lt;br /&gt;
  local v  = m1 and m1[norm(value)]&lt;br /&gt;
  if v then return v end&lt;br /&gt;
&lt;br /&gt;
  -- 2) Spezialfall Event-Typen: i18n().events.infobox.type&lt;br /&gt;
  if k == &amp;quot;type&amp;quot; then&lt;br /&gt;
    local m2 = (((i18n().events or {}).infobox) or {}).type or {}&lt;br /&gt;
    return m2[norm(value)] or value&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  return value&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function ev_notice_icon_wikitext(ev)&lt;br /&gt;
  local file = ev.currency and ev.currency.icon&lt;br /&gt;
  if not file or file == &amp;quot;&amp;quot; then return &amp;quot;&amp;quot; end&lt;br /&gt;
  local img = fileTag(file, { size = &amp;quot;24px&amp;quot;, param = &amp;quot;frameless|link=&amp;quot; })&lt;br /&gt;
  return &#039;&amp;lt;span class=&amp;quot;fs-ic&amp;quot;&amp;gt;&#039; .. img .. &#039;&amp;lt;/span&amp;gt;&#039;&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function ev_pagelink(ev, evkey, label)&lt;br /&gt;
  local page = ev.page&lt;br /&gt;
  if page and page ~= &amp;quot;&amp;quot; then&lt;br /&gt;
    return string.format(&amp;quot;[[%s|%s]]&amp;quot;, page, label)&lt;br /&gt;
  end&lt;br /&gt;
  return label&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
-- ========== Nummern + &amp;lt;ref&amp;gt; (voll i18n-gesteuert) ==========&lt;br /&gt;
local function parse_sci(v)&lt;br /&gt;
  if type(v) == &amp;quot;number&amp;quot; then return v end&lt;br /&gt;
  if type(v) == &amp;quot;string&amp;quot; then&lt;br /&gt;
    local a,e = v:match(&amp;quot;^%s*([%d%.]+)[eE]([%+%-]?%d+)%s*$&amp;quot;)&lt;br /&gt;
    if a and e then return (tonumber(a) or 0) * 10^(tonumber(e) or 0) end&lt;br /&gt;
    local n = tonumber(v); if n then return n end&lt;br /&gt;
  end&lt;br /&gt;
  return nil&lt;br /&gt;
end&lt;br /&gt;
local function i18n_big_units()&lt;br /&gt;
  local N = (i18n().numbers or {})&lt;br /&gt;
  return {&lt;br /&gt;
    {1e12, N.trillion_sing,  N.trillion_plur},&lt;br /&gt;
    {1e9,  N.billion_sing,   N.billion_plur},&lt;br /&gt;
    {1e6,  N.million_sing,   N.million_plur},&lt;br /&gt;
    {1e3,  N.thousand_sing,  N.thousand_plur},&lt;br /&gt;
  }&lt;br /&gt;
end&lt;br /&gt;
local function format_big(v)&lt;br /&gt;
  local n = parse_sci(v); if not n then return tostring(v or &amp;quot;&amp;quot;) end&lt;br /&gt;
  for _,u in ipairs(i18n_big_units()) do&lt;br /&gt;
    local base, sing, plur = u[1], u[2], u[3]&lt;br /&gt;
    if base and sing and plur and n &amp;gt;= base and math.floor(n % base) == 0 then&lt;br /&gt;
      local q = math.floor(n / base)&lt;br /&gt;
      return string.format(&amp;quot;%d %s&amp;quot;, q, (q==1) and sing or plur)&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
  return tostring(n)&lt;br /&gt;
end&lt;br /&gt;
local function ref_gear_power(v)&lt;br /&gt;
  if isempty(v) then return &amp;quot;&amp;quot; end&lt;br /&gt;
  local phr = (((i18n().events or {}).phrases) or {}).req_gear_power&lt;br /&gt;
  if isempty(phr) then return &amp;quot;&amp;quot; end&lt;br /&gt;
  local name = &amp;quot;gear-&amp;quot; .. tostring(v):gsub(&amp;quot;%s&amp;quot;,&amp;quot;&amp;quot;)&lt;br /&gt;
  return string.format(&#039;&amp;lt;ref name=&amp;quot;%s&amp;quot;&amp;gt;%s&amp;lt;/ref&amp;gt;&#039;, name, (phr:gsub(&amp;quot;$1&amp;quot;, format_big(v))))&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- ========== Event-Renderer ==========&lt;br /&gt;
local function htag(level, text)&lt;br /&gt;
  if isempty(text) then return &amp;quot;&amp;quot; end&lt;br /&gt;
  local eq = level==3 and &amp;quot;===&amp;quot; or &amp;quot;==&amp;quot;&lt;br /&gt;
  return string.format(&amp;quot;%s %s %s&amp;quot;, eq, text, eq)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function event_file(ev, evkey, which)&lt;br /&gt;
  if which==&amp;quot;deco&amp;quot; then&lt;br /&gt;
    local file   = ev.images and ev.images.deco or &amp;quot;&amp;quot;&lt;br /&gt;
    local legend = ev_label(evkey, &amp;quot;deco&amp;quot;)&lt;br /&gt;
    return fileTag(file, { param = &amp;quot;thumb|&amp;quot;..legend })&lt;br /&gt;
  elseif which==&amp;quot;fullname&amp;quot; then&lt;br /&gt;
    local file   = ev.images and ev.images.fullname or &amp;quot;&amp;quot;&lt;br /&gt;
    local legend = ev_label(evkey, &amp;quot;fullname&amp;quot;)&lt;br /&gt;
    return fileTag(file, { param = &amp;quot;thumb|&amp;quot;..legend })&lt;br /&gt;
  elseif which==&amp;quot;ex_shop&amp;quot; then&lt;br /&gt;
    local file   = ev.images and ev.images.ex_shop or &amp;quot;&amp;quot;&lt;br /&gt;
    local legend = ev_label(evkey, &amp;quot;ex_shop&amp;quot;)&lt;br /&gt;
    return fileTag(file, { param = &amp;quot;thumb|&amp;quot;..legend })&lt;br /&gt;
  elseif which==&amp;quot;banner&amp;quot; then&lt;br /&gt;
    return fileTag(ev.banner, { param=&amp;quot;thumb&amp;quot; })&lt;br /&gt;
  end&lt;br /&gt;
  return &amp;quot;&amp;quot;&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function event_link(ev, evkey, which)&lt;br /&gt;
  if which==&amp;quot;type&amp;quot; then&lt;br /&gt;
    local E = i18n().events or {}&lt;br /&gt;
    local page  = (E.links and E.links.type and E.links.type[ev.type or &amp;quot;&amp;quot;])&lt;br /&gt;
                  or (E.links and E.links.type) or &amp;quot;Calendar Events&amp;quot;&lt;br /&gt;
    local map   = (E.infobox and E.infobox.type) or {}&lt;br /&gt;
    local label = map[norm(ev.type or &amp;quot;&amp;quot;)] or (ev.type or &amp;quot;&amp;quot;)&lt;br /&gt;
    return string.format(&amp;quot;[[%s|%s]]&amp;quot;, page, label)&lt;br /&gt;
  end&lt;br /&gt;
  return &amp;quot;&amp;quot;&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function event_history(ev, evkey, args)&lt;br /&gt;
  local H = ev.history or {}; if not has_items(H) then return &amp;quot;&amp;quot; end&lt;br /&gt;
&lt;br /&gt;
  local phrases = ((i18n().events or {}).phrases or {})&lt;br /&gt;
  local alt_gp  = phrases.alt_platform or &amp;quot;auf Google Play&amp;quot;&lt;br /&gt;
  local showTxt = phrases.toggle_show  or &amp;quot;Anzeigen&amp;quot;&lt;br /&gt;
  local hideTxt = phrases.toggle_hide  or &amp;quot;Verbergen&amp;quot;&lt;br /&gt;
&lt;br /&gt;
  local function fmt_range_or_raw(s)&lt;br /&gt;
    if not s or s == &amp;quot;&amp;quot; then return &amp;quot;&amp;quot; end&lt;br /&gt;
    local a,b = tostring(s):match(&amp;quot;^(%d%d%d%d%-%d%d%-%d%d)%s+bis%s+(%d%d%d%d%-%d%d%-%d%d)$&amp;quot;)&lt;br /&gt;
    if a and b then&lt;br /&gt;
      return string.format(&amp;quot;%s bis %s&amp;quot;, format_date_de(a), format_date_de(b))&lt;br /&gt;
    end&lt;br /&gt;
    local ts = parse_iso_date(s)&lt;br /&gt;
    return ts and format_date_de(ts) or s&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  local lines = {}&lt;br /&gt;
  for _,h in ipairs(H) do&lt;br /&gt;
    local a = format_date_de(h.start)&lt;br /&gt;
    local b = format_date_de(h[&amp;quot;end&amp;quot;] or h.end_date)&lt;br /&gt;
    local line = string.format(&amp;quot;* %s: %s bis %s&amp;quot;, tostring(h.year or &amp;quot;&amp;quot;), a, b)&lt;br /&gt;
    if h.alt_platform then&lt;br /&gt;
      line = line .. string.format(&amp;quot; (%s %s)&amp;quot;, fmt_range_or_raw(h.alt_platform), alt_gp)&lt;br /&gt;
    end&lt;br /&gt;
    lines[#lines+1] = line&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  -- vorhersagbare/überschreibbare ID für den Toggle&lt;br /&gt;
  local id = (args and args.id) or (&amp;quot;fs-hist-&amp;quot; .. mw.ustring.gsub(tostring(evkey), &amp;quot;[^%w%-%_]&amp;quot;, &amp;quot;-&amp;quot;))&lt;br /&gt;
&lt;br /&gt;
  -- standardmäßig eingeklappt; Toggle-Texte können via phrases.* gesetzt werden&lt;br /&gt;
  return string.format(&lt;br /&gt;
    &#039;&amp;lt;span id=&amp;quot;%s&amp;quot; class=&amp;quot;mw-collapsible mw-collapsed&amp;quot; data-expandtext=&amp;quot;%s&amp;quot; data-collapsetext=&amp;quot;%s&amp;quot;&amp;gt;\n%s\n&amp;lt;/span&amp;gt;&#039;,&lt;br /&gt;
    id, mw.text.encode(showTxt), mw.text.encode(hideTxt), table.concat(lines, &amp;quot;\n&amp;quot;)&lt;br /&gt;
  )&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function item_label(key, amount)&lt;br /&gt;
  local I = (i18n().items or {})[key] or {}&lt;br /&gt;
  local n = tonumber(amount or 1) or 1&lt;br /&gt;
  local txt = (n == 1) and (I.sing or key) or (I.plur or (I.sing or key))&lt;br /&gt;
  local disp = I.link and string.format(&amp;quot;[[%s|%s]]&amp;quot;, I.link, txt) or txt&lt;br /&gt;
  return string.format(&amp;quot;%d %s&amp;quot;, n, disp)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- references/render&lt;br /&gt;
local FRAME = nil&lt;br /&gt;
local function tabber_render_from_content(content)&lt;br /&gt;
  if FRAME and FRAME.extensionTag then&lt;br /&gt;
    return FRAME:extensionTag(&#039;tabber&#039;, content)&lt;br /&gt;
  end&lt;br /&gt;
  return &amp;quot;&amp;lt;tabber&amp;gt;\n&amp;quot; .. content .. &amp;quot;\n&amp;lt;/tabber&amp;gt;&amp;quot;&lt;br /&gt;
end&lt;br /&gt;
local function references_tag()&lt;br /&gt;
  if FRAME and FRAME.extensionTag then&lt;br /&gt;
    return FRAME:extensionTag(&#039;references&#039;, &#039;&#039;)&lt;br /&gt;
  end&lt;br /&gt;
  return &#039;&amp;lt;references /&amp;gt;&#039;&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- innerer Tab-Inhalt: je Range&lt;br /&gt;
local function build_tabber_content_for(list)&lt;br /&gt;
  local out = {}&lt;br /&gt;
  for _,it in ipairs(list) do&lt;br /&gt;
    local label   = pretty_range(it.range or &amp;quot;&amp;quot;)&lt;br /&gt;
    local note    = ref_gear_power(it.req_gear_power)&lt;br /&gt;
    local content = string.format(&lt;br /&gt;
      &amp;quot;%s %s%s&amp;quot;,&lt;br /&gt;
      fileTag(it.icon or &amp;quot;&amp;quot;, { size=&amp;quot;30px&amp;quot; }),&lt;br /&gt;
      (it.item and item_label(it.item, it.amount or 1)) or (it.title or &amp;quot;&amp;quot;),&lt;br /&gt;
      note&lt;br /&gt;
    )&lt;br /&gt;
    out[#out+1] = &amp;quot;|-|&amp;quot; .. label .. &amp;quot;=\n&amp;quot; .. content&lt;br /&gt;
  end&lt;br /&gt;
  return table.concat(out, &amp;quot;\n&amp;quot;)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Gruppen-Label aus i18n&lt;br /&gt;
local function group_label(kind)&lt;br /&gt;
  local G = ((i18n().events or {}).group_labels or {})&lt;br /&gt;
  return G[kind] or kind&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function event_avatars(ev, evkey)&lt;br /&gt;
  local AV = ev.avatars&lt;br /&gt;
  if not has_items(AV) then return &amp;quot;&amp;quot; end&lt;br /&gt;
&lt;br /&gt;
  local token_icon = (ev.currency or {}).icon or &amp;quot;&amp;quot;&lt;br /&gt;
  local token_name = ev_label(evkey, &amp;quot;currencies&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
  local years, out = {}, {}&lt;br /&gt;
  for y,_ in pairs(AV) do table.insert(years, tonumber(y) or y) end&lt;br /&gt;
  table.sort(years, function(a,b) return tostring(a) &amp;lt; tostring(b) end)&lt;br /&gt;
&lt;br /&gt;
  for _,year in ipairs(years) do&lt;br /&gt;
    local list = AV[year] or AV[tostring(year)]&lt;br /&gt;
    if has_items(list) then&lt;br /&gt;
      out[#out+1] = (&amp;quot;==== %s ====&amp;quot;):format(year)&lt;br /&gt;
      out[#out+1] = &#039;{| class=&amp;quot;article-table&amp;quot; style=&amp;quot;font-size:14px;&amp;quot;&#039;&lt;br /&gt;
      out[#out+1] = &#039;! colspan=&amp;quot;2&amp;quot; | Avatar\n! Preis&#039;&lt;br /&gt;
      for _,it in ipairs(list) do&lt;br /&gt;
        out[#out+1] = &#039;|-&#039;&lt;br /&gt;
        out[#out+1] = string.format(&#039;|%s || %s || %s %s %s&#039;,&lt;br /&gt;
          fileTag(it.file, { size=&amp;quot;50px&amp;quot; }),&lt;br /&gt;
          it.title or &amp;quot;&amp;quot;,&lt;br /&gt;
          fileTag(token_icon, { size=&amp;quot;25px&amp;quot; }),&lt;br /&gt;
          tostring(it.cost or 0),&lt;br /&gt;
          token_name&lt;br /&gt;
        )&lt;br /&gt;
      end&lt;br /&gt;
      out[#out+1] = &#039;|}&#039;&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
  return (#out &amp;gt; 0) and table.concat(out, &amp;quot;\n&amp;quot;) or &amp;quot;&amp;quot;&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Austausch-Tabelle (äußerer Tab: Gruppen → innerer Tab: Ranges)&lt;br /&gt;
local function event_exchange_tabs(ev, evkey)&lt;br /&gt;
  local ex = ev.exchange or {}&lt;br /&gt;
  local token_icon = (ev.currency or {}).icon or &amp;quot;&amp;quot;&lt;br /&gt;
  local token_name = ev_label(evkey, &amp;quot;currencies&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
  if not (has_items(ex.chests_by_level)&lt;br /&gt;
       or has_items(ex.chests_by_stars)&lt;br /&gt;
       or has_items(ex.chests_by_oracle)&lt;br /&gt;
       or has_items(ex.currencies)) then&lt;br /&gt;
    return &amp;quot;&amp;quot;&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  local outer_parts = {}&lt;br /&gt;
  local function add_group(kind, list)&lt;br /&gt;
    if not has_items(list) then return end&lt;br /&gt;
    local inner_content = build_tabber_content_for(list)&lt;br /&gt;
    local inner_tabber  = tabber_render_from_content(inner_content)&lt;br /&gt;
    outer_parts[#outer_parts+1] = &amp;quot;|-|&amp;quot; .. group_label(kind) .. &amp;quot;=\n&amp;quot; .. inner_tabber&lt;br /&gt;
  end&lt;br /&gt;
  add_group(&amp;quot;level&amp;quot;,  ex.chests_by_level)&lt;br /&gt;
  add_group(&amp;quot;stars&amp;quot;,  ex.chests_by_stars)&lt;br /&gt;
  add_group(&amp;quot;oracle&amp;quot;, ex.chests_by_oracle)&lt;br /&gt;
&lt;br /&gt;
  local left_tabber = tabber_render_from_content(table.concat(outer_parts, &amp;quot;\n&amp;quot;))&lt;br /&gt;
&lt;br /&gt;
  -- Preis/Limit (erste gefundene Gruppe)&lt;br /&gt;
  local function pick_price_limit()&lt;br /&gt;
    if has_items(ex.chests_by_level)  then return ex.chests_by_level[1].price or 0,  ex.chests_by_level[1].limit or &amp;quot;&amp;quot;  end&lt;br /&gt;
    if has_items(ex.chests_by_stars)  then return ex.chests_by_stars[1].price or 0,  ex.chests_by_stars[1].limit or &amp;quot;&amp;quot;  end&lt;br /&gt;
    if has_items(ex.chests_by_oracle) then return ex.chests_by_oracle[1].price or 0, ex.chests_by_oracle[1].limit or &amp;quot;&amp;quot; end&lt;br /&gt;
    return 0, &amp;quot;&amp;quot;&lt;br /&gt;
  end&lt;br /&gt;
  local price, limit = pick_price_limit()&lt;br /&gt;
&lt;br /&gt;
  local t = {}&lt;br /&gt;
  local H_offer, H_price, H_limit = header_labels(evkey)&lt;br /&gt;
  t[#t+1] = &#039;{| class=&amp;quot;article-table fs-exchange&amp;quot; style=&amp;quot;font-size:14px;&amp;quot;&#039;&lt;br /&gt;
  t[#t+1] = string.format(&lt;br /&gt;
    &#039;! style=&amp;quot;width:%s;&amp;quot; | %s !! style=&amp;quot;width:%s;&amp;quot; | %s !! style=&amp;quot;width:%s;&amp;quot; | %s&#039;,&lt;br /&gt;
    COL_OFFER, H_offer, COL_PRICE, H_price, COL_LIMIT, H_limit&lt;br /&gt;
  )&lt;br /&gt;
  t[#t+1] = &#039;|-&#039;&lt;br /&gt;
  t[#t+1] = &#039;|&#039; .. left_tabber .. string.format(&lt;br /&gt;
    &#039; || style=&amp;quot;text-align:center;&amp;quot; | %s %s %s || style=&amp;quot;text-align:center;&amp;quot; | %s&#039;,&lt;br /&gt;
    fileTag(token_icon, { size=&amp;quot;25px&amp;quot; }),&lt;br /&gt;
    tostring(price), token_name,&lt;br /&gt;
    tostring(limit)&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
  if has_items(ex.currencies) then&lt;br /&gt;
    for _,it in ipairs(ex.currencies) do&lt;br /&gt;
      local offer = string.format(&#039;%s %s&#039;,&lt;br /&gt;
        fileTag(it.icon or &amp;quot;&amp;quot;, { size=&amp;quot;30px&amp;quot; }),&lt;br /&gt;
        item_label(it.item, it.amount or 1)&lt;br /&gt;
      )&lt;br /&gt;
      t[#t+1] = &#039;|-&#039;&lt;br /&gt;
      t[#t+1] = string.format(&lt;br /&gt;
        &#039;| %s || style=&amp;quot;text-align:center;&amp;quot; | %s %s %s || style=&amp;quot;text-align:center;&amp;quot; | %s&#039;,&lt;br /&gt;
        offer,&lt;br /&gt;
        fileTag(token_icon, { size=&amp;quot;25px&amp;quot; }),&lt;br /&gt;
        tostring(it.price or 0), token_name,&lt;br /&gt;
        tostring(it.limit or &amp;quot;&amp;quot;)&lt;br /&gt;
      )&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  t[#t+1] = &#039;|}&#039;&lt;br /&gt;
  t[#t+1] = references_tag()&lt;br /&gt;
  return &#039;\n&#039; .. table.concat(t, &#039;\n&#039;)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function event_offers(ev, evkey, which)&lt;br /&gt;
  local list = (ev.shop or {})[which]&lt;br /&gt;
  if not has_items(list) then return &amp;quot;&amp;quot; end&lt;br /&gt;
  local token_icon = (ev.currency or {}).icon or &amp;quot;&amp;quot;&lt;br /&gt;
  local token_name = ev_label(evkey, &amp;quot;currencies&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
  local t = {}&lt;br /&gt;
  t[#t+1] = &#039;{| class=&amp;quot;article-table&amp;quot; style=&amp;quot;font-size:14px;&amp;quot;&#039;&lt;br /&gt;
  t[#t+1] = &amp;quot;! Angebot !! Menge&amp;quot;&lt;br /&gt;
  for _,it in ipairs(list) do&lt;br /&gt;
    t[#t+1] = &amp;quot;|-&amp;quot;&lt;br /&gt;
    t[#t+1] = string.format(&amp;quot;| &#039;&#039;%s&#039;&#039;&amp;lt;br /&amp;gt;%s || %s %s %s&amp;quot;,&lt;br /&gt;
      it.name or &amp;quot;&amp;quot;,&lt;br /&gt;
      fileTag(it.image, { size=&amp;quot;x50px&amp;quot; }),&lt;br /&gt;
      fileTag(token_icon, { size=&amp;quot;30px&amp;quot; }),&lt;br /&gt;
      tostring(it.token or 0),&lt;br /&gt;
      token_name&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
  t[#t+1] = &amp;quot;|}&amp;quot;&lt;br /&gt;
  return table.concat(t, &amp;quot;\n&amp;quot;)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- ========== SiteNotice-Support (Datum/Auto-Pick) ==========&lt;br /&gt;
local function event_window(ev)&lt;br /&gt;
  local S = ev.schedule or ev&lt;br /&gt;
  local s = S.start_date; if isempty(s) then return nil end&lt;br /&gt;
&lt;br /&gt;
  local st = parse_iso_date(s); if not st then return nil end&lt;br /&gt;
&lt;br /&gt;
  local en_excl, en_incl&lt;br /&gt;
  if not isempty(S.end_date) then&lt;br /&gt;
    -- end_date wird als inklusives Kalenderdatum verstanden&lt;br /&gt;
    en_incl = parse_iso_date(S.end_date)&lt;br /&gt;
    if not en_incl then return st, nil, nil end&lt;br /&gt;
    en_excl = en_incl + DAY&lt;br /&gt;
  else&lt;br /&gt;
    -- Dauer in Tagen (Standard 14), exklusives Ende = st + d*DAY&lt;br /&gt;
    local d = tonumber(S.duration_days) or 14&lt;br /&gt;
    en_excl = st + d * DAY&lt;br /&gt;
    en_incl = en_excl - DAY&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  return st, en_excl, en_incl&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function pick_notice_eventkey()&lt;br /&gt;
  local cfg = ((i18n().events or {}).notice or {})&lt;br /&gt;
  local pre  = tonumber(cfg.pre_days)  or 7&lt;br /&gt;
  local post = tonumber(cfg.post_days) or 3&lt;br /&gt;
  local now = os.time()&lt;br /&gt;
&lt;br /&gt;
  local running, upcoming, ended_recent = {}, {}, {}&lt;br /&gt;
&lt;br /&gt;
  for k, ev in pairs(events()) do&lt;br /&gt;
    local st, en_excl = event_window(ev)&lt;br /&gt;
    if st and en_excl then&lt;br /&gt;
      if now &amp;gt;= st and now &amp;lt; en_excl then&lt;br /&gt;
        table.insert(running, { k=k, st=st, en=en_excl })&lt;br /&gt;
      elseif now &amp;gt;= st - pre*DAY and now &amp;lt; st then&lt;br /&gt;
        table.insert(upcoming, { k=k, st=st, en=en_excl })&lt;br /&gt;
      elseif now &amp;gt;= en_excl and now &amp;lt; en_excl + post*DAY then&lt;br /&gt;
        table.insert(ended_recent, { k=k, st=st, en=en_excl })&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  local function by_end(a,b)   return a.en &amp;lt; b.en end&lt;br /&gt;
  local function by_start(a,b) return a.st &amp;lt; b.st end&lt;br /&gt;
  if #running      &amp;gt; 0 then table.sort(running,      by_end)   ; return running[1].k      end&lt;br /&gt;
  if #upcoming     &amp;gt; 0 then table.sort(upcoming,     by_start) ; return upcoming[1].k     end&lt;br /&gt;
  if #ended_recent &amp;gt; 0 then table.sort(ended_recent, by_end)   ; return ended_recent[1].k end&lt;br /&gt;
  return nil&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function event_notice(ev, evkey)&lt;br /&gt;
  local phrases = (((i18n().events or {}).phrases) or {})&lt;br /&gt;
  local PH_RUNNING = phrases.event_notice   -- &amp;quot;Das $1 läuft von $2 bis $3&amp;quot;&lt;br /&gt;
  local PH_ENDED   = phrases.event_ended    -- &amp;quot;Das $1 ist beendet. Du kannst verbleibende $4 noch im $5 eintauschen.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
  -- Ohne Texte kein Hinweis&lt;br /&gt;
  if isempty(PH_RUNNING) then return &amp;quot;&amp;quot; end&lt;br /&gt;
&lt;br /&gt;
  local st, en_excl, en_incl = event_window(ev); if not st or not en_excl then return &amp;quot;&amp;quot; end&lt;br /&gt;
&lt;br /&gt;
  local cfg  = ((i18n().events or {}).notice or {})&lt;br /&gt;
  local pre  = tonumber(cfg.pre_days)  or 7   -- z. B. 7 Tage vorher&lt;br /&gt;
  local post = tonumber(cfg.post_days) or 3   -- z. B. 3 Tage danach&lt;br /&gt;
  local now  = os.time()&lt;br /&gt;
&lt;br /&gt;
  local show_from = st - pre  * DAY&lt;br /&gt;
  local show_till = en_excl + post * DAY&lt;br /&gt;
  if now &amp;lt; show_from or now &amp;gt;= show_till then&lt;br /&gt;
    return &amp;quot;&amp;quot;&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  -- Anzeigename (aus i18n sections oder names)&lt;br /&gt;
  local name_label = ev_label(evkey, &amp;quot;eventname&amp;quot;)&lt;br /&gt;
  if isempty(name_label) then name_label = ev_name(evkey) end&lt;br /&gt;
&lt;br /&gt;
  -- Verlinken NUR wenn ev.page existiert&lt;br /&gt;
  local name = ev_pagelink(ev, evkey, name_label)&lt;br /&gt;
&lt;br /&gt;
  -- Icon aus EventData → currency.icon&lt;br /&gt;
  local ic = ev_notice_icon_wikitext(ev)  -- &amp;quot;&amp;quot; wenn kein currency.icon&lt;br /&gt;
&lt;br /&gt;
  local text&lt;br /&gt;
  if now &amp;lt; en_excl then&lt;br /&gt;
    -- läuft / startet bald&lt;br /&gt;
    text = PH_RUNNING&lt;br /&gt;
      :gsub(&amp;quot;$1&amp;quot;, name)&lt;br /&gt;
      :gsub(&amp;quot;$2&amp;quot;, format_date_de(st))&lt;br /&gt;
      :gsub(&amp;quot;$3&amp;quot;, format_date_de(en_excl))&lt;br /&gt;
  else&lt;br /&gt;
    -- beendet (bis zu post_days danach)&lt;br /&gt;
    if isempty(PH_ENDED) then return &amp;quot;&amp;quot; end&lt;br /&gt;
    -- Labels aus i18n.sections (wie gehabt)&lt;br /&gt;
    local curr = ev_label(evkey, &amp;quot;currencies&amp;quot;)&lt;br /&gt;
    local shop = ev_label(evkey, &amp;quot;ex_shop&amp;quot;)&lt;br /&gt;
    if isempty(curr) or isempty(shop) then return &amp;quot;&amp;quot; end&lt;br /&gt;
    text = PH_ENDED&lt;br /&gt;
      :gsub(&amp;quot;$1&amp;quot;, name)&lt;br /&gt;
      :gsub(&amp;quot;$4&amp;quot;, curr)&lt;br /&gt;
      :gsub(&amp;quot;$5&amp;quot;, shop)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  local prefix = (ic ~= &amp;quot;&amp;quot; and (ic .. &amp;quot; &amp;quot;) or &amp;quot;&amp;quot;)&lt;br /&gt;
  local suffix = (ic ~= &amp;quot;&amp;quot; and (&amp;quot; &amp;quot; .. ic) or &amp;quot;&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
  return string.format(&lt;br /&gt;
    &#039;&amp;lt;div class=&amp;quot;fs-notice&amp;quot; data-ev=&amp;quot;%s&amp;quot;&amp;gt;%s%s%s&amp;lt;/div&amp;gt;&#039;,&lt;br /&gt;
    mw.text.encode(evkey),&lt;br /&gt;
    prefix, text, suffix&lt;br /&gt;
  )&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- ========== Dispatcher ==========&lt;br /&gt;
local function get_event(evkey)&lt;br /&gt;
  local evs = events()&lt;br /&gt;
  local real = pick_key(evs, evkey)&lt;br /&gt;
  return real and evs[real] or nil, real or evkey&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function dispatch_event(args)&lt;br /&gt;
  local evkey = args[2]&lt;br /&gt;
&lt;br /&gt;
  -- {{Firestone|Event|notice}}  → automatisch wählen&lt;br /&gt;
  if evkey and norm(evkey) == &amp;quot;notice&amp;quot; then&lt;br /&gt;
    local pick = pick_notice_eventkey()&lt;br /&gt;
    if not pick then return &amp;quot;&amp;quot; end&lt;br /&gt;
    local ev, r = get_event(pick); if not ev then return &amp;quot;&amp;quot; end&lt;br /&gt;
    return event_notice(ev, r)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  -- normaler Weg&lt;br /&gt;
  if isempty(evkey) then return &amp;quot;&amp;quot; end&lt;br /&gt;
  local ev, realkey = get_event(evkey); if not ev then return &amp;quot;&amp;quot; end&lt;br /&gt;
&lt;br /&gt;
  local a3, a4 = args[3], args[4]&lt;br /&gt;
  if isempty(a3) then return ev_name(realkey) end&lt;br /&gt;
&lt;br /&gt;
  if a3==&amp;quot;notice&amp;quot;  then return event_notice(ev, realkey)&lt;br /&gt;
  elseif a3==&amp;quot;file&amp;quot;   then return event_file(ev, realkey, a4 or &amp;quot;&amp;quot;)&lt;br /&gt;
  elseif a3==&amp;quot;link&amp;quot;   then return event_link(ev, realkey, a4 or &amp;quot;&amp;quot;)&lt;br /&gt;
  elseif a3==&amp;quot;h2&amp;quot;     then return htag(2, ev_label(realkey, a4 or &amp;quot;&amp;quot;))&lt;br /&gt;
  elseif a3==&amp;quot;h3&amp;quot;     then return htag(3, ev_label(realkey, a4 or &amp;quot;&amp;quot;))&lt;br /&gt;
  elseif a3==&amp;quot;history&amp;quot;  then return event_history(ev, realkey, a)&lt;br /&gt;
  elseif a3==&amp;quot;avatars&amp;quot;  then return event_avatars(ev, realkey)&lt;br /&gt;
  elseif a3==&amp;quot;exchange&amp;quot; then return event_exchange_tabs(ev, realkey)&lt;br /&gt;
  elseif a3==&amp;quot;offers&amp;quot;   then&lt;br /&gt;
    if a4==&amp;quot;start&amp;quot; then return event_offers(ev, realkey, &amp;quot;initial&amp;quot;)&lt;br /&gt;
    elseif a4==&amp;quot;more&amp;quot; then return event_offers(ev, realkey, &amp;quot;after&amp;quot;) end&lt;br /&gt;
    return &amp;quot;&amp;quot;&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  -- Sonderfälle für Währungsnamen aus i18n.sections&lt;br /&gt;
  if a3 == &amp;quot;currency&amp;quot;  then return ev_label(realkey, &amp;quot;currency&amp;quot;)  end&lt;br /&gt;
  if a3 == &amp;quot;currencies&amp;quot; then return ev_label(realkey, &amp;quot;currencies&amp;quot;) end&lt;br /&gt;
&lt;br /&gt;
  -- generischer Pfad in EventData&lt;br /&gt;
  local path, i = {}, 3&lt;br /&gt;
  while args[i] do table.insert(path, args[i]); i=i+1 end&lt;br /&gt;
  local v = deep_get(ev, path)&lt;br /&gt;
  if v ~= nil then&lt;br /&gt;
    local last = path[#path] and tostring(path[#path]) or &amp;quot;&amp;quot;&lt;br /&gt;
    v = autotr(last, v)&lt;br /&gt;
    return tostring(v)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  -- Fallback auf Labels/Name/Unlocks&lt;br /&gt;
  local last = norm(path[#path] or a3)&lt;br /&gt;
  if last==&amp;quot;eventname&amp;quot; or last==&amp;quot;name&amp;quot; then return ev_name(realkey) end&lt;br /&gt;
  local lbl = ev_label(realkey, last); if not isempty(lbl) then return lbl end&lt;br /&gt;
  local U = ((i18n().events or {}).unlocks or {})[last]&lt;br /&gt;
  if U then return U end&lt;br /&gt;
&lt;br /&gt;
  return &amp;quot;&amp;quot;&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function dispatch_hero(args)&lt;br /&gt;
  local name = args[2]; if isempty(name) then return &amp;quot;&amp;quot; end&lt;br /&gt;
  local H = heroes()&lt;br /&gt;
  local real = pick_key(H, name); if not real then return &amp;quot;&amp;quot; end&lt;br /&gt;
  local h = H[real]&lt;br /&gt;
  local path, i = {}, 3&lt;br /&gt;
  while args[i] do table.insert(path, args[i]); i=i+1 end&lt;br /&gt;
  local v = deep_get(h, path)&lt;br /&gt;
  local last = path[#path] and tostring(path[#path]) or &amp;quot;&amp;quot;&lt;br /&gt;
  v = autotr(last, v)&lt;br /&gt;
  return tostring(v or &amp;quot;&amp;quot;)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- ========== main ==========&lt;br /&gt;
function M.main(frame)&lt;br /&gt;
  FRAME = frame&lt;br /&gt;
  local a = getArgs(frame)&lt;br /&gt;
  for i=1,10 do a[i] = a[i] and tostring(a[i]) or nil end&lt;br /&gt;
  local dom = a[1] and norm(a[1]) or &amp;quot;&amp;quot;&lt;br /&gt;
  if dom==&amp;quot;event&amp;quot; then&lt;br /&gt;
    return dispatch_event(a)&lt;br /&gt;
  elseif dom==&amp;quot;hero&amp;quot; then&lt;br /&gt;
    return dispatch_hero(a)&lt;br /&gt;
  else&lt;br /&gt;
    return &amp;quot;&amp;quot;&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
return M&lt;/div&gt;</summary>
		<author><name>Webmaster</name></author>
	</entry>
	<entry>
		<id>https://firestone-idle-rpg.de/index.php?title=Vorlage:EventPage&amp;diff=3755</id>
		<title>Vorlage:EventPage</title>
		<link rel="alternate" type="text/html" href="https://firestone-idle-rpg.de/index.php?title=Vorlage:EventPage&amp;diff=3755"/>
		<updated>2025-10-28T09:16:35Z</updated>

		<summary type="html">&lt;p&gt;Webmaster: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox/Event&lt;br /&gt;
|banner={{Firestone|Event|{{{name|}}}|banner}}&lt;br /&gt;
|caption=&lt;br /&gt;
|type={{Firestone|Event|{{{name|}}}|type}}&lt;br /&gt;
|date={{Firestone|Event|{{{name|}}}|month}}&lt;br /&gt;
|duration={{Firestone|Event|{{{name|}}}|duration}}&lt;br /&gt;
|unlocksat={{Firestone|Event|{{{name|}}}|character_level_10}}}}&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|file|deco}}&lt;br /&gt;
Das {{Firestone|Event|{{{name|}}}|eventname}} ist ein {{Firestone|Event|{{{name|}}}|type}}, das im {{Firestone|Event|{{{name|}}}|month}} startet und {{Firestone|Event|{{{name|}}}|duration}} dauert. Frühere Ausgaben waren: {{#tag:span||class=mw-collapsible-toggle|data-mw-collapsible-target=#fs-hist-{{{name|}}}}}&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|history|id=fs-hist-{{{name|}}}}}&lt;br /&gt;
&lt;br /&gt;
In diesem Event lässt {{Firestone|Event|{{{name|}}}|fullname}}, {{Firestone|Event|{{{name|}}}|currencies}} auf dem Schlachtfeld fallen. Diese können anschließend in der {{Firestone|Event|{{{name|}}}|ex_shop}} gegen [[Gear Chests|Ausrüstungstruhen]], [[Jewel Chests|Juwelentruhen]], bestimmte [[currencies|Währungen]] und einzigartige [[avatar|Avatare]] eingetauscht werden. Zusätzlich ist die Stadt mit Halloween-Dekoration geschmückt. &lt;br /&gt;
&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|h2|currencies}}&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|file|fullname}}&lt;br /&gt;
&lt;br /&gt;
Während des Events lässt {{Firestone|Event|{{{name|}}}|fullname}}, {{Firestone|Event|{{{name|}}}|currencies}} auf dem Schlachtfeld fallen – mit einer Rate von 1 {{Firestone|Event|{{{name|}}}|currency}} alle 28,8 Sekunden. Die {{Firestone|Event|{{{name|}}}|currencies}} werden automatisch eingesammelt, was zu einem fixen Zuwachs von 3.000 {{Firestone|Event|{{{name|}}}|currencies}} pro Tag führt. {{Firestone|Event|{{{name|}}}|currencies}} werden auch gesammelt, während du offline bist, und werden dir gutgeschrieben, wenn {{Firestone|Event|{{{name|}}}|shortname}} dich nach dem erneuten Einloggen das erste Mal besucht. Offline-Gewinne sind jedoch auf 24 Stunden begrenzt.&lt;br /&gt;
&lt;br /&gt;
Wenn du einen Einkauf im {{Firestone|Event|{{{name|}}}|shopname}} tätigst (siehe unten), erhöht sich die Drop-Rate für den Rest des Events auf 2 {{Firestone|Event|{{{name|}}}|currencies}} alle 28,8 Sekunden.&lt;br /&gt;
&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|h2|ex_shop}}&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|file|ex_shop}}&lt;br /&gt;
&lt;br /&gt;
Die {{Firestone|Event|{{{name|}}}|ex_shop}} ersetzt den Brunnen in der Stadt und ist nur während des Halloween-Events verfügbar. Nach Ende des Events verschwindet die {{Firestone|Event|{{{name|}}}|ex_shop}} wieder und deine verbleibenden {{Firestone|Event|{{{name|}}}|currencies}} gehen verloren. Die {{Firestone|Event|{{{name|}}}|ex_shop}} besteht aus den folgenden drei Bereichen.&lt;br /&gt;
&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|h3|currency_exchange}}&lt;br /&gt;
&lt;br /&gt;
Im {{Firestone|Event|{{{name|}}}|currency_exchange}} kannst du {{Firestone|Event|{{{name|}}}|currencies}} gegen Truhen und [[currencies|Währungen]] eintauschen. Die Art der [[Gear Chests|Ausrüstungstruhe]] hängt von deiner [[character level|Charakterstufe]] ab und die Art der [[Jewel Chests|Juwelentruhe]] von der Anzahl deiner [[campaign stars|Kampagnensterne]]. Die Anzahl der Käufe ist für jeden angebotenen Truhen- und Währungstyp begrenzt.&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|exchange}}&lt;br /&gt;
&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|h3|avatar_exchange}}&lt;br /&gt;
&lt;br /&gt;
Bei »{{Firestone|Event|{{{name|}}}|avatar_exchange}}« kannst du {{Firestone|Event|{{{name|}}}|currencies}} gegen einzigartige [[avatar|Avatare]] eintauschen, die nur während des {{Firestone|Event|{{{name|}}}|name}} erhältlich sind.&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|avatars}}&lt;br /&gt;
&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|h3|shop}}&lt;br /&gt;
&lt;br /&gt;
Im {{Firestone|Event|{{{name|}}}|shopname}} kannst du {{Firestone|Event|{{{name|}}}|currencies}} mit [[Platforms|Plattform]]-Währung kaufen. Jeder Kauf im {{Firestone|Event|{{{name|}}}|shopname}} erhöht die Drop-Rate der {{Firestone|Event|{{{name|}}}|currencies}} auf dem Schlachtfeld für den Rest des Events auf 2 {{Firestone|Event|{{{name|}}}|currencies}} alle 28,8 Sekunden. Jedes Angebot kann nur einmal erworben werden.&lt;br /&gt;
&lt;br /&gt;
Anfangs werden die folgenden 3 Angebote angezeigt:&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|offers|start}}&lt;br /&gt;
&lt;br /&gt;
Sobald die Angebote 1–3 alle gekauft wurden, erscheinen die folgenden Angebote:&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|offers|more}}&lt;br /&gt;
&lt;br /&gt;
{{Navigation}}&lt;br /&gt;
[[Category:Browse]]&lt;br /&gt;
[[Category:Events]]&lt;/div&gt;</summary>
		<author><name>Webmaster</name></author>
	</entry>
	<entry>
		<id>https://firestone-idle-rpg.de/index.php?title=Vorlage:EventPage&amp;diff=3754</id>
		<title>Vorlage:EventPage</title>
		<link rel="alternate" type="text/html" href="https://firestone-idle-rpg.de/index.php?title=Vorlage:EventPage&amp;diff=3754"/>
		<updated>2025-10-28T07:52:34Z</updated>

		<summary type="html">&lt;p&gt;Webmaster: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox/Event&lt;br /&gt;
|banner={{Firestone|Event|{{{name|}}}|banner}}&lt;br /&gt;
|caption=&lt;br /&gt;
|type={{Firestone|Event|{{{name|}}}|type}}&lt;br /&gt;
|date={{Firestone|Event|{{{name|}}}|month}}&lt;br /&gt;
|duration={{Firestone|Event|{{{name|}}}|duration}}&lt;br /&gt;
|unlocksat={{Firestone|Event|{{{name|}}}|character_level_10}}}}&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|file|deco}}&lt;br /&gt;
Das {{Firestone|Event|{{{name|}}}|eventname}} ist ein {{Firestone|Event|{{{name|}}}|type}}, das im {{Firestone|Event|{{{name|}}}|month}} startet und {{Firestone|Event|{{{name|}}}|duration}} dauert. Frühere Ausgaben waren: {{#tag:a||class=mw-collapsible-toggle|data-mw-collapsible-target=#fs-hist-{{{name|}}}}}&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|history|id=fs-hist-{{{name|}}}}}&lt;br /&gt;
&lt;br /&gt;
In diesem Event lässt {{Firestone|Event|{{{name|}}}|fullname}}, {{Firestone|Event|{{{name|}}}|currencies}} auf dem Schlachtfeld fallen. Diese können anschließend in der {{Firestone|Event|{{{name|}}}|ex_shop}} gegen [[Gear Chests|Ausrüstungstruhen]], [[Jewel Chests|Juwelentruhen]], bestimmte [[currencies|Währungen]] und einzigartige [[avatar|Avatare]] eingetauscht werden. Zusätzlich ist die Stadt mit Halloween-Dekoration geschmückt. &lt;br /&gt;
&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|h2|currencies}}&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|file|fullname}}&lt;br /&gt;
&lt;br /&gt;
Während des Events lässt {{Firestone|Event|{{{name|}}}|fullname}}, {{Firestone|Event|{{{name|}}}|currencies}} auf dem Schlachtfeld fallen – mit einer Rate von 1 {{Firestone|Event|{{{name|}}}|currency}} alle 28,8 Sekunden. Die {{Firestone|Event|{{{name|}}}|currencies}} werden automatisch eingesammelt, was zu einem fixen Zuwachs von 3.000 {{Firestone|Event|{{{name|}}}|currencies}} pro Tag führt. {{Firestone|Event|{{{name|}}}|currencies}} werden auch gesammelt, während du offline bist, und werden dir gutgeschrieben, wenn {{Firestone|Event|{{{name|}}}|shortname}} dich nach dem erneuten Einloggen das erste Mal besucht. Offline-Gewinne sind jedoch auf 24 Stunden begrenzt.&lt;br /&gt;
&lt;br /&gt;
Wenn du einen Einkauf im {{Firestone|Event|{{{name|}}}|shopname}} tätigst (siehe unten), erhöht sich die Drop-Rate für den Rest des Events auf 2 {{Firestone|Event|{{{name|}}}|currencies}} alle 28,8 Sekunden.&lt;br /&gt;
&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|h2|ex_shop}}&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|file|ex_shop}}&lt;br /&gt;
&lt;br /&gt;
Die {{Firestone|Event|{{{name|}}}|ex_shop}} ersetzt den Brunnen in der Stadt und ist nur während des Halloween-Events verfügbar. Nach Ende des Events verschwindet die {{Firestone|Event|{{{name|}}}|ex_shop}} wieder und deine verbleibenden {{Firestone|Event|{{{name|}}}|currencies}} gehen verloren. Die {{Firestone|Event|{{{name|}}}|ex_shop}} besteht aus den folgenden drei Bereichen.&lt;br /&gt;
&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|h3|currency_exchange}}&lt;br /&gt;
&lt;br /&gt;
Im {{Firestone|Event|{{{name|}}}|currency_exchange}} kannst du {{Firestone|Event|{{{name|}}}|currencies}} gegen Truhen und [[currencies|Währungen]] eintauschen. Die Art der [[Gear Chests|Ausrüstungstruhe]] hängt von deiner [[character level|Charakterstufe]] ab und die Art der [[Jewel Chests|Juwelentruhe]] von der Anzahl deiner [[campaign stars|Kampagnensterne]]. Die Anzahl der Käufe ist für jeden angebotenen Truhen- und Währungstyp begrenzt.&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|exchange}}&lt;br /&gt;
&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|h3|avatar_exchange}}&lt;br /&gt;
&lt;br /&gt;
Bei »{{Firestone|Event|{{{name|}}}|avatar_exchange}}« kannst du {{Firestone|Event|{{{name|}}}|currencies}} gegen einzigartige [[avatar|Avatare]] eintauschen, die nur während des {{Firestone|Event|{{{name|}}}|name}} erhältlich sind.&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|avatars}}&lt;br /&gt;
&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|h3|shop}}&lt;br /&gt;
&lt;br /&gt;
Im {{Firestone|Event|{{{name|}}}|shopname}} kannst du {{Firestone|Event|{{{name|}}}|currencies}} mit [[Platforms|Plattform]]-Währung kaufen. Jeder Kauf im {{Firestone|Event|{{{name|}}}|shopname}} erhöht die Drop-Rate der {{Firestone|Event|{{{name|}}}|currencies}} auf dem Schlachtfeld für den Rest des Events auf 2 {{Firestone|Event|{{{name|}}}|currencies}} alle 28,8 Sekunden. Jedes Angebot kann nur einmal erworben werden.&lt;br /&gt;
&lt;br /&gt;
Anfangs werden die folgenden 3 Angebote angezeigt:&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|offers|start}}&lt;br /&gt;
&lt;br /&gt;
Sobald die Angebote 1–3 alle gekauft wurden, erscheinen die folgenden Angebote:&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|offers|more}}&lt;br /&gt;
&lt;br /&gt;
{{Navigation}}&lt;br /&gt;
[[Category:Browse]]&lt;br /&gt;
[[Category:Events]]&lt;/div&gt;</summary>
		<author><name>Webmaster</name></author>
	</entry>
	<entry>
		<id>https://firestone-idle-rpg.de/index.php?title=Vorlage:EventPage&amp;diff=3753</id>
		<title>Vorlage:EventPage</title>
		<link rel="alternate" type="text/html" href="https://firestone-idle-rpg.de/index.php?title=Vorlage:EventPage&amp;diff=3753"/>
		<updated>2025-10-28T07:35:35Z</updated>

		<summary type="html">&lt;p&gt;Webmaster: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox/Event&lt;br /&gt;
|banner={{Firestone|Event|{{{name|}}}|banner}}&lt;br /&gt;
|caption=&lt;br /&gt;
|type={{Firestone|Event|{{{name|}}}|type}}&lt;br /&gt;
|date={{Firestone|Event|{{{name|}}}|month}}&lt;br /&gt;
|duration={{Firestone|Event|{{{name|}}}|duration}}&lt;br /&gt;
|unlocksat={{Firestone|Event|{{{name|}}}|character_level_10}}}}&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|file|deco}}&lt;br /&gt;
Das {{Firestone|Event|{{{name|}}}|eventname}} ist ein {{Firestone|Event|{{{name|}}}|type}}, das im {{Firestone|Event|{{{name|}}}|month}} startet und {{Firestone|Event|{{{name|}}}|duration}} dauert. Frühere Ausgaben waren: &amp;lt;a class=&amp;quot;mw-collapsible-toggle&amp;quot; data-mw-collapsible-target=&amp;quot;#fs-hist-{{{name|}}}&amp;quot;&amp;gt;&amp;lt;/a&amp;gt;&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|history|id=fs-hist-{{{name|}}}}}&lt;br /&gt;
&lt;br /&gt;
In diesem Event lässt {{Firestone|Event|{{{name|}}}|fullname}}, {{Firestone|Event|{{{name|}}}|currencies}} auf dem Schlachtfeld fallen. Diese können anschließend in der {{Firestone|Event|{{{name|}}}|ex_shop}} gegen [[Gear Chests|Ausrüstungstruhen]], [[Jewel Chests|Juwelentruhen]], bestimmte [[currencies|Währungen]] und einzigartige [[avatar|Avatare]] eingetauscht werden. Zusätzlich ist die Stadt mit Halloween-Dekoration geschmückt. &lt;br /&gt;
&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|h2|currencies}}&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|file|fullname}}&lt;br /&gt;
&lt;br /&gt;
Während des Events lässt {{Firestone|Event|{{{name|}}}|fullname}}, {{Firestone|Event|{{{name|}}}|currencies}} auf dem Schlachtfeld fallen – mit einer Rate von 1 {{Firestone|Event|{{{name|}}}|currency}} alle 28,8 Sekunden. Die {{Firestone|Event|{{{name|}}}|currencies}} werden automatisch eingesammelt, was zu einem fixen Zuwachs von 3.000 {{Firestone|Event|{{{name|}}}|currencies}} pro Tag führt. {{Firestone|Event|{{{name|}}}|currencies}} werden auch gesammelt, während du offline bist, und werden dir gutgeschrieben, wenn {{Firestone|Event|{{{name|}}}|shortname}} dich nach dem erneuten Einloggen das erste Mal besucht. Offline-Gewinne sind jedoch auf 24 Stunden begrenzt.&lt;br /&gt;
&lt;br /&gt;
Wenn du einen Einkauf im {{Firestone|Event|{{{name|}}}|shopname}} tätigst (siehe unten), erhöht sich die Drop-Rate für den Rest des Events auf 2 {{Firestone|Event|{{{name|}}}|currencies}} alle 28,8 Sekunden.&lt;br /&gt;
&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|h2|ex_shop}}&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|file|ex_shop}}&lt;br /&gt;
&lt;br /&gt;
Die {{Firestone|Event|{{{name|}}}|ex_shop}} ersetzt den Brunnen in der Stadt und ist nur während des Halloween-Events verfügbar. Nach Ende des Events verschwindet die {{Firestone|Event|{{{name|}}}|ex_shop}} wieder und deine verbleibenden {{Firestone|Event|{{{name|}}}|currencies}} gehen verloren. Die {{Firestone|Event|{{{name|}}}|ex_shop}} besteht aus den folgenden drei Bereichen.&lt;br /&gt;
&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|h3|currency_exchange}}&lt;br /&gt;
&lt;br /&gt;
Im {{Firestone|Event|{{{name|}}}|currency_exchange}} kannst du {{Firestone|Event|{{{name|}}}|currencies}} gegen Truhen und [[currencies|Währungen]] eintauschen. Die Art der [[Gear Chests|Ausrüstungstruhe]] hängt von deiner [[character level|Charakterstufe]] ab und die Art der [[Jewel Chests|Juwelentruhe]] von der Anzahl deiner [[campaign stars|Kampagnensterne]]. Die Anzahl der Käufe ist für jeden angebotenen Truhen- und Währungstyp begrenzt.&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|exchange}}&lt;br /&gt;
&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|h3|avatar_exchange}}&lt;br /&gt;
&lt;br /&gt;
Bei »{{Firestone|Event|{{{name|}}}|avatar_exchange}}« kannst du {{Firestone|Event|{{{name|}}}|currencies}} gegen einzigartige [[avatar|Avatare]] eintauschen, die nur während des {{Firestone|Event|{{{name|}}}|name}} erhältlich sind.&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|avatars}}&lt;br /&gt;
&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|h3|shop}}&lt;br /&gt;
&lt;br /&gt;
Im {{Firestone|Event|{{{name|}}}|shopname}} kannst du {{Firestone|Event|{{{name|}}}|currencies}} mit [[Platforms|Plattform]]-Währung kaufen. Jeder Kauf im {{Firestone|Event|{{{name|}}}|shopname}} erhöht die Drop-Rate der {{Firestone|Event|{{{name|}}}|currencies}} auf dem Schlachtfeld für den Rest des Events auf 2 {{Firestone|Event|{{{name|}}}|currencies}} alle 28,8 Sekunden. Jedes Angebot kann nur einmal erworben werden.&lt;br /&gt;
&lt;br /&gt;
Anfangs werden die folgenden 3 Angebote angezeigt:&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|offers|start}}&lt;br /&gt;
&lt;br /&gt;
Sobald die Angebote 1–3 alle gekauft wurden, erscheinen die folgenden Angebote:&lt;br /&gt;
{{Firestone|Event|{{{name|}}}|offers|more}}&lt;br /&gt;
&lt;br /&gt;
{{Navigation}}&lt;br /&gt;
[[Category:Browse]]&lt;br /&gt;
[[Category:Events]]&lt;/div&gt;</summary>
		<author><name>Webmaster</name></author>
	</entry>
	<entry>
		<id>https://firestone-idle-rpg.de/index.php?title=Modul:Firestone&amp;diff=3752</id>
		<title>Modul:Firestone</title>
		<link rel="alternate" type="text/html" href="https://firestone-idle-rpg.de/index.php?title=Modul:Firestone&amp;diff=3752"/>
		<updated>2025-10-28T07:34:00Z</updated>

		<summary type="html">&lt;p&gt;Webmaster: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;-- Modul:Firestone&lt;br /&gt;
local M = {}&lt;br /&gt;
&lt;br /&gt;
-- ========== kleine Helfer ==========&lt;br /&gt;
local COL_OFFER, COL_PRICE, COL_LIMIT = &amp;quot;50%&amp;quot;, &amp;quot;25%&amp;quot;, &amp;quot;25%&amp;quot;&lt;br /&gt;
local DAY = 86400&lt;br /&gt;
local function parse_iso_date(s)&lt;br /&gt;
  local y,m,d = tostring(s or &amp;quot;&amp;quot;):match(&amp;quot;^(%d%d%d%d)%-(%d%d)%-(%d%d)$&amp;quot;)&lt;br /&gt;
  if not y then return nil end&lt;br /&gt;
  return os.time{ year=tonumber(y), month=tonumber(m), day=tonumber(d), hour=12 }&lt;br /&gt;
end&lt;br /&gt;
local function ts_to_mw(ts) return os.date(&amp;quot;!%Y%m%d%H%M%S&amp;quot;, ts) end&lt;br /&gt;
local function format_date_de(v)&lt;br /&gt;
  local ts = (type(v)==&amp;quot;number&amp;quot;) and v or parse_iso_date(v)&lt;br /&gt;
  if not ts then return &amp;quot;&amp;quot; end&lt;br /&gt;
  local lang = mw.getContentLanguage()&lt;br /&gt;
  return lang:formatDate(&amp;quot;d.m.Y&amp;quot;, ts_to_mw(ts))&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function isempty(v) return v == nil or v == &amp;quot;&amp;quot; end&lt;br /&gt;
local function norm(s)&lt;br /&gt;
  if s == nil then return &amp;quot;&amp;quot; end&lt;br /&gt;
  s = mw.text.trim(tostring(s))&lt;br /&gt;
  return mw.ustring.lower(s):gsub(&amp;quot;%s+&amp;quot;,&amp;quot; &amp;quot;)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- bevorzugt Parent-Args (Vorlagen)&lt;br /&gt;
local function getArgs(frame)&lt;br /&gt;
  local p = frame:getParent()&lt;br /&gt;
  local src = (p and p.args) or frame.args or {}&lt;br /&gt;
  local a = {}&lt;br /&gt;
  for k,v in pairs(src) do&lt;br /&gt;
    if v ~= nil and v ~= &amp;quot;&amp;quot; then a[k] = mw.text.trim(tostring(v)) end&lt;br /&gt;
  end&lt;br /&gt;
  return a&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function has_items(t)&lt;br /&gt;
  if type(t) ~= &amp;quot;table&amp;quot; then return false end&lt;br /&gt;
  for _ in pairs(t) do return true end&lt;br /&gt;
  return false&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function pick_key(tbl, key)&lt;br /&gt;
  if type(tbl) ~= &amp;quot;table&amp;quot; then return nil end&lt;br /&gt;
  if tbl[key] ~= nil then return key end&lt;br /&gt;
  local k2 = tostring(key)&lt;br /&gt;
  if tbl[k2] ~= nil then return k2 end&lt;br /&gt;
  local kn = norm(k2)&lt;br /&gt;
  for k,_ in pairs(tbl) do if norm(k) == kn then return k end end&lt;br /&gt;
  return nil&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function deep_get(tbl, path)&lt;br /&gt;
  local cur = tbl&lt;br /&gt;
  for _,k in ipairs(path) do&lt;br /&gt;
    if type(cur) ~= &amp;quot;table&amp;quot; then return nil end&lt;br /&gt;
    local real = pick_key(cur, k); if not real then return nil end&lt;br /&gt;
    cur = cur[real]&lt;br /&gt;
  end&lt;br /&gt;
  return cur&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function fileTag(file, opts) -- [[Datei:...]]&lt;br /&gt;
  if isempty(file) then return &amp;quot;&amp;quot; end&lt;br /&gt;
  local parts = { &amp;quot;Datei:&amp;quot;..file }&lt;br /&gt;
  if opts and opts.size  then table.insert(parts, opts.size)  end&lt;br /&gt;
  if opts and opts.param then table.insert(parts, opts.param) end -- z.B. &amp;quot;thumb|Text&amp;quot;&lt;br /&gt;
  return string.format(&amp;quot;[[%s]]&amp;quot;, table.concat(parts, &amp;quot;|&amp;quot;))&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function pretty_range(s)&lt;br /&gt;
  if not s or s==&amp;quot;&amp;quot; then return &amp;quot;&amp;quot; end&lt;br /&gt;
  s = tostring(s)&lt;br /&gt;
  if s:sub(-1) == &amp;quot;-&amp;quot; then return s:sub(1, -2) .. &amp;quot;+&amp;quot; end&lt;br /&gt;
  return s:gsub(&amp;quot;%-&amp;quot;,&amp;quot;–&amp;quot;)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- ========== Lazy-Loads ==========&lt;br /&gt;
local I18N, EVENTS, HEROES&lt;br /&gt;
local function i18n()&lt;br /&gt;
  if I18N then return I18N end&lt;br /&gt;
  for _,t in ipairs{ &amp;quot;Modul:HeroI18n&amp;quot;, &amp;quot;Module:HeroI18n&amp;quot; } do&lt;br /&gt;
    local ok, data = pcall(mw.loadData, t) ; if ok and type(data)==&amp;quot;table&amp;quot; then I18N=data; return I18N end&lt;br /&gt;
    local ok2, mod = pcall(require, t)      ; if ok2 and type(mod) ==&amp;quot;table&amp;quot; then I18N=mod ; return I18N end&lt;br /&gt;
  end&lt;br /&gt;
  I18N = { i18n={}, events={ names={}, sections={default={}}, phrases={}, links={}, unlocks={} } }&lt;br /&gt;
  return I18N&lt;br /&gt;
end&lt;br /&gt;
local function events()&lt;br /&gt;
  if EVENTS then return EVENTS end&lt;br /&gt;
  for _,t in ipairs{ &amp;quot;Modul:EventData&amp;quot;, &amp;quot;Module:EventData&amp;quot; } do&lt;br /&gt;
    local ok, data = pcall(mw.loadData, t) ; if ok and type(data)==&amp;quot;table&amp;quot; and type(data.events)==&amp;quot;table&amp;quot; then EVENTS=data.events; return EVENTS end&lt;br /&gt;
    local ok2, mod = pcall(require, t)     ; if ok2 and type(mod) ==&amp;quot;table&amp;quot; and type(mod.events)==&amp;quot;table&amp;quot; then EVENTS=mod.events ; return EVENTS end&lt;br /&gt;
  end&lt;br /&gt;
  EVENTS = {}; return EVENTS&lt;br /&gt;
end&lt;br /&gt;
local function heroes()&lt;br /&gt;
  if HEROES then return HEROES end&lt;br /&gt;
  for _,t in ipairs{ &amp;quot;Modul:HeroData&amp;quot;, &amp;quot;Module:HeroData&amp;quot; } do&lt;br /&gt;
    local ok, data = pcall(mw.loadData, t) ; if ok and type(data)==&amp;quot;table&amp;quot; and type(data.heroes)==&amp;quot;table&amp;quot; then HEROES=data.heroes; return HEROES end&lt;br /&gt;
    local ok2, mod = pcall(require, t)     ; if ok2 and type(mod) ==&amp;quot;table&amp;quot; and type(mod.heroes)==&amp;quot;table&amp;quot; then HEROES=mod.heroes ; return HEROES end&lt;br /&gt;
  end&lt;br /&gt;
  HEROES = {}; return HEROES&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- ========== i18n / Labels ==========&lt;br /&gt;
local function ev_label(evkey, which)&lt;br /&gt;
  local E = i18n().events or {}&lt;br /&gt;
  local spec = (E.sections and E.sections[norm(evkey)]) or {}&lt;br /&gt;
  local def  = (E.sections and E.sections.default) or {}&lt;br /&gt;
  return (spec[which] or def[which] or &amp;quot;&amp;quot;)&lt;br /&gt;
end&lt;br /&gt;
local function ev_name(evkey)&lt;br /&gt;
  local names = (i18n().events or {}).names or {}&lt;br /&gt;
  return names[norm(evkey)] or evkey&lt;br /&gt;
end&lt;br /&gt;
local function header_labels(evkey)&lt;br /&gt;
  return ev_label(evkey, &amp;quot;offer&amp;quot;), ev_label(evkey, &amp;quot;price&amp;quot;), ev_label(evkey, &amp;quot;limit&amp;quot;)&lt;br /&gt;
end&lt;br /&gt;
local function autotr(key_name, value)&lt;br /&gt;
  if type(value) ~= &amp;quot;string&amp;quot; then return value end&lt;br /&gt;
  local k = norm(key_name)&lt;br /&gt;
&lt;br /&gt;
  -- 1) Standardwortschatz (falls vorhanden)&lt;br /&gt;
  local m1 = (i18n().i18n or {})[k]&lt;br /&gt;
  local v  = m1 and m1[norm(value)]&lt;br /&gt;
  if v then return v end&lt;br /&gt;
&lt;br /&gt;
  -- 2) Spezialfall Event-Typen: i18n().events.infobox.type&lt;br /&gt;
  if k == &amp;quot;type&amp;quot; then&lt;br /&gt;
    local m2 = (((i18n().events or {}).infobox) or {}).type or {}&lt;br /&gt;
    return m2[norm(value)] or value&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  return value&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function ev_notice_icon_wikitext(ev)&lt;br /&gt;
  local file = ev.currency and ev.currency.icon&lt;br /&gt;
  if not file or file == &amp;quot;&amp;quot; then return &amp;quot;&amp;quot; end&lt;br /&gt;
  local img = fileTag(file, { size = &amp;quot;24px&amp;quot;, param = &amp;quot;frameless|link=&amp;quot; })&lt;br /&gt;
  return &#039;&amp;lt;span class=&amp;quot;fs-ic&amp;quot;&amp;gt;&#039; .. img .. &#039;&amp;lt;/span&amp;gt;&#039;&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function ev_pagelink(ev, evkey, label)&lt;br /&gt;
  local page = ev.page&lt;br /&gt;
  if page and page ~= &amp;quot;&amp;quot; then&lt;br /&gt;
    return string.format(&amp;quot;[[%s|%s]]&amp;quot;, page, label)&lt;br /&gt;
  end&lt;br /&gt;
  return label&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
-- ========== Nummern + &amp;lt;ref&amp;gt; (voll i18n-gesteuert) ==========&lt;br /&gt;
local function parse_sci(v)&lt;br /&gt;
  if type(v) == &amp;quot;number&amp;quot; then return v end&lt;br /&gt;
  if type(v) == &amp;quot;string&amp;quot; then&lt;br /&gt;
    local a,e = v:match(&amp;quot;^%s*([%d%.]+)[eE]([%+%-]?%d+)%s*$&amp;quot;)&lt;br /&gt;
    if a and e then return (tonumber(a) or 0) * 10^(tonumber(e) or 0) end&lt;br /&gt;
    local n = tonumber(v); if n then return n end&lt;br /&gt;
  end&lt;br /&gt;
  return nil&lt;br /&gt;
end&lt;br /&gt;
local function i18n_big_units()&lt;br /&gt;
  local N = (i18n().numbers or {})&lt;br /&gt;
  return {&lt;br /&gt;
    {1e12, N.trillion_sing,  N.trillion_plur},&lt;br /&gt;
    {1e9,  N.billion_sing,   N.billion_plur},&lt;br /&gt;
    {1e6,  N.million_sing,   N.million_plur},&lt;br /&gt;
    {1e3,  N.thousand_sing,  N.thousand_plur},&lt;br /&gt;
  }&lt;br /&gt;
end&lt;br /&gt;
local function format_big(v)&lt;br /&gt;
  local n = parse_sci(v); if not n then return tostring(v or &amp;quot;&amp;quot;) end&lt;br /&gt;
  for _,u in ipairs(i18n_big_units()) do&lt;br /&gt;
    local base, sing, plur = u[1], u[2], u[3]&lt;br /&gt;
    if base and sing and plur and n &amp;gt;= base and math.floor(n % base) == 0 then&lt;br /&gt;
      local q = math.floor(n / base)&lt;br /&gt;
      return string.format(&amp;quot;%d %s&amp;quot;, q, (q==1) and sing or plur)&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
  return tostring(n)&lt;br /&gt;
end&lt;br /&gt;
local function ref_gear_power(v)&lt;br /&gt;
  if isempty(v) then return &amp;quot;&amp;quot; end&lt;br /&gt;
  local phr = (((i18n().events or {}).phrases) or {}).req_gear_power&lt;br /&gt;
  if isempty(phr) then return &amp;quot;&amp;quot; end&lt;br /&gt;
  local name = &amp;quot;gear-&amp;quot; .. tostring(v):gsub(&amp;quot;%s&amp;quot;,&amp;quot;&amp;quot;)&lt;br /&gt;
  return string.format(&#039;&amp;lt;ref name=&amp;quot;%s&amp;quot;&amp;gt;%s&amp;lt;/ref&amp;gt;&#039;, name, (phr:gsub(&amp;quot;$1&amp;quot;, format_big(v))))&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- ========== Event-Renderer ==========&lt;br /&gt;
local function htag(level, text)&lt;br /&gt;
  if isempty(text) then return &amp;quot;&amp;quot; end&lt;br /&gt;
  local eq = level==3 and &amp;quot;===&amp;quot; or &amp;quot;==&amp;quot;&lt;br /&gt;
  return string.format(&amp;quot;%s %s %s&amp;quot;, eq, text, eq)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function event_file(ev, evkey, which)&lt;br /&gt;
  if which==&amp;quot;deco&amp;quot; then&lt;br /&gt;
    local file   = ev.images and ev.images.deco or &amp;quot;&amp;quot;&lt;br /&gt;
    local legend = ev_label(evkey, &amp;quot;deco&amp;quot;)&lt;br /&gt;
    return fileTag(file, { param = &amp;quot;thumb|&amp;quot;..legend })&lt;br /&gt;
  elseif which==&amp;quot;fullname&amp;quot; then&lt;br /&gt;
    local file   = ev.images and ev.images.fullname or &amp;quot;&amp;quot;&lt;br /&gt;
    local legend = ev_label(evkey, &amp;quot;fullname&amp;quot;)&lt;br /&gt;
    return fileTag(file, { param = &amp;quot;thumb|&amp;quot;..legend })&lt;br /&gt;
  elseif which==&amp;quot;ex_shop&amp;quot; then&lt;br /&gt;
    local file   = ev.images and ev.images.ex_shop or &amp;quot;&amp;quot;&lt;br /&gt;
    local legend = ev_label(evkey, &amp;quot;ex_shop&amp;quot;)&lt;br /&gt;
    return fileTag(file, { param = &amp;quot;thumb|&amp;quot;..legend })&lt;br /&gt;
  elseif which==&amp;quot;banner&amp;quot; then&lt;br /&gt;
    return fileTag(ev.banner, { param=&amp;quot;thumb&amp;quot; })&lt;br /&gt;
  end&lt;br /&gt;
  return &amp;quot;&amp;quot;&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function event_link(ev, evkey, which)&lt;br /&gt;
  if which==&amp;quot;type&amp;quot; then&lt;br /&gt;
    local E = i18n().events or {}&lt;br /&gt;
    local page  = (E.links and E.links.type and E.links.type[ev.type or &amp;quot;&amp;quot;])&lt;br /&gt;
                  or (E.links and E.links.type) or &amp;quot;Calendar Events&amp;quot;&lt;br /&gt;
    local map   = (E.infobox and E.infobox.type) or {}&lt;br /&gt;
    local label = map[norm(ev.type or &amp;quot;&amp;quot;)] or (ev.type or &amp;quot;&amp;quot;)&lt;br /&gt;
    return string.format(&amp;quot;[[%s|%s]]&amp;quot;, page, label)&lt;br /&gt;
  end&lt;br /&gt;
  return &amp;quot;&amp;quot;&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function event_history(ev, evkey, args)&lt;br /&gt;
  local H = ev.history or {}; if not has_items(H) then return &amp;quot;&amp;quot; end&lt;br /&gt;
&lt;br /&gt;
  local phrases = ((i18n().events or {}).phrases or {})&lt;br /&gt;
  local alt_gp  = phrases.alt_platform or &amp;quot;auf Google Play&amp;quot;&lt;br /&gt;
  local showTxt = phrases.toggle_show  or &amp;quot;Anzeigen&amp;quot;&lt;br /&gt;
  local hideTxt = phrases.toggle_hide  or &amp;quot;Verbergen&amp;quot;&lt;br /&gt;
&lt;br /&gt;
  local function fmt_range_or_raw(s)&lt;br /&gt;
    if not s or s == &amp;quot;&amp;quot; then return &amp;quot;&amp;quot; end&lt;br /&gt;
    local a,b = tostring(s):match(&amp;quot;^(%d%d%d%d%-%d%d%-%d%d)%s+bis%s+(%d%d%d%d%-%d%d%-%d%d)$&amp;quot;)&lt;br /&gt;
    if a and b then&lt;br /&gt;
      return string.format(&amp;quot;%s bis %s&amp;quot;, format_date_de(a), format_date_de(b))&lt;br /&gt;
    end&lt;br /&gt;
    local ts = parse_iso_date(s)&lt;br /&gt;
    return ts and format_date_de(ts) or s&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  local lines = {}&lt;br /&gt;
  for _,h in ipairs(H) do&lt;br /&gt;
    local a = format_date_de(h.start)&lt;br /&gt;
    local b = format_date_de(h[&amp;quot;end&amp;quot;] or h.end_date)&lt;br /&gt;
    local line = string.format(&amp;quot;* %s: %s bis %s&amp;quot;, tostring(h.year or &amp;quot;&amp;quot;), a, b)&lt;br /&gt;
    if h.alt_platform then&lt;br /&gt;
      line = line .. string.format(&amp;quot; (%s %s)&amp;quot;, fmt_range_or_raw(h.alt_platform), alt_gp)&lt;br /&gt;
    end&lt;br /&gt;
    lines[#lines+1] = line&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  -- vorhersagbare/überschreibbare ID für den Toggle&lt;br /&gt;
  local id = (args and args.id) or (&amp;quot;fs-hist-&amp;quot; .. mw.ustring.gsub(tostring(evkey), &amp;quot;[^%w%-%_]&amp;quot;, &amp;quot;-&amp;quot;))&lt;br /&gt;
&lt;br /&gt;
  -- standardmäßig eingeklappt; Toggle-Texte können via phrases.* gesetzt werden&lt;br /&gt;
  return string.format(&lt;br /&gt;
    &#039;&amp;lt;div id=&amp;quot;%s&amp;quot; class=&amp;quot;mw-collapsible mw-collapsed&amp;quot; data-expandtext=&amp;quot;%s&amp;quot; data-collapsetext=&amp;quot;%s&amp;quot;&amp;gt;\n%s\n&amp;lt;/div&amp;gt;&#039;,&lt;br /&gt;
    id, mw.text.encode(showTxt), mw.text.encode(hideTxt), table.concat(lines, &amp;quot;\n&amp;quot;)&lt;br /&gt;
  )&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function item_label(key, amount)&lt;br /&gt;
  local I = (i18n().items or {})[key] or {}&lt;br /&gt;
  local n = tonumber(amount or 1) or 1&lt;br /&gt;
  local txt = (n == 1) and (I.sing or key) or (I.plur or (I.sing or key))&lt;br /&gt;
  local disp = I.link and string.format(&amp;quot;[[%s|%s]]&amp;quot;, I.link, txt) or txt&lt;br /&gt;
  return string.format(&amp;quot;%d %s&amp;quot;, n, disp)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- references/render&lt;br /&gt;
local FRAME = nil&lt;br /&gt;
local function tabber_render_from_content(content)&lt;br /&gt;
  if FRAME and FRAME.extensionTag then&lt;br /&gt;
    return FRAME:extensionTag(&#039;tabber&#039;, content)&lt;br /&gt;
  end&lt;br /&gt;
  return &amp;quot;&amp;lt;tabber&amp;gt;\n&amp;quot; .. content .. &amp;quot;\n&amp;lt;/tabber&amp;gt;&amp;quot;&lt;br /&gt;
end&lt;br /&gt;
local function references_tag()&lt;br /&gt;
  if FRAME and FRAME.extensionTag then&lt;br /&gt;
    return FRAME:extensionTag(&#039;references&#039;, &#039;&#039;)&lt;br /&gt;
  end&lt;br /&gt;
  return &#039;&amp;lt;references /&amp;gt;&#039;&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- innerer Tab-Inhalt: je Range&lt;br /&gt;
local function build_tabber_content_for(list)&lt;br /&gt;
  local out = {}&lt;br /&gt;
  for _,it in ipairs(list) do&lt;br /&gt;
    local label   = pretty_range(it.range or &amp;quot;&amp;quot;)&lt;br /&gt;
    local note    = ref_gear_power(it.req_gear_power)&lt;br /&gt;
    local content = string.format(&lt;br /&gt;
      &amp;quot;%s %s%s&amp;quot;,&lt;br /&gt;
      fileTag(it.icon or &amp;quot;&amp;quot;, { size=&amp;quot;30px&amp;quot; }),&lt;br /&gt;
      (it.item and item_label(it.item, it.amount or 1)) or (it.title or &amp;quot;&amp;quot;),&lt;br /&gt;
      note&lt;br /&gt;
    )&lt;br /&gt;
    out[#out+1] = &amp;quot;|-|&amp;quot; .. label .. &amp;quot;=\n&amp;quot; .. content&lt;br /&gt;
  end&lt;br /&gt;
  return table.concat(out, &amp;quot;\n&amp;quot;)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Gruppen-Label aus i18n&lt;br /&gt;
local function group_label(kind)&lt;br /&gt;
  local G = ((i18n().events or {}).group_labels or {})&lt;br /&gt;
  return G[kind] or kind&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function event_avatars(ev, evkey)&lt;br /&gt;
  local AV = ev.avatars&lt;br /&gt;
  if not has_items(AV) then return &amp;quot;&amp;quot; end&lt;br /&gt;
&lt;br /&gt;
  local token_icon = (ev.currency or {}).icon or &amp;quot;&amp;quot;&lt;br /&gt;
  local token_name = ev_label(evkey, &amp;quot;currencies&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
  local years, out = {}, {}&lt;br /&gt;
  for y,_ in pairs(AV) do table.insert(years, tonumber(y) or y) end&lt;br /&gt;
  table.sort(years, function(a,b) return tostring(a) &amp;lt; tostring(b) end)&lt;br /&gt;
&lt;br /&gt;
  for _,year in ipairs(years) do&lt;br /&gt;
    local list = AV[year] or AV[tostring(year)]&lt;br /&gt;
    if has_items(list) then&lt;br /&gt;
      out[#out+1] = (&amp;quot;==== %s ====&amp;quot;):format(year)&lt;br /&gt;
      out[#out+1] = &#039;{| class=&amp;quot;article-table&amp;quot; style=&amp;quot;font-size:14px;&amp;quot;&#039;&lt;br /&gt;
      out[#out+1] = &#039;! colspan=&amp;quot;2&amp;quot; | Avatar\n! Preis&#039;&lt;br /&gt;
      for _,it in ipairs(list) do&lt;br /&gt;
        out[#out+1] = &#039;|-&#039;&lt;br /&gt;
        out[#out+1] = string.format(&#039;|%s || %s || %s %s %s&#039;,&lt;br /&gt;
          fileTag(it.file, { size=&amp;quot;50px&amp;quot; }),&lt;br /&gt;
          it.title or &amp;quot;&amp;quot;,&lt;br /&gt;
          fileTag(token_icon, { size=&amp;quot;25px&amp;quot; }),&lt;br /&gt;
          tostring(it.cost or 0),&lt;br /&gt;
          token_name&lt;br /&gt;
        )&lt;br /&gt;
      end&lt;br /&gt;
      out[#out+1] = &#039;|}&#039;&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
  return (#out &amp;gt; 0) and table.concat(out, &amp;quot;\n&amp;quot;) or &amp;quot;&amp;quot;&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Austausch-Tabelle (äußerer Tab: Gruppen → innerer Tab: Ranges)&lt;br /&gt;
local function event_exchange_tabs(ev, evkey)&lt;br /&gt;
  local ex = ev.exchange or {}&lt;br /&gt;
  local token_icon = (ev.currency or {}).icon or &amp;quot;&amp;quot;&lt;br /&gt;
  local token_name = ev_label(evkey, &amp;quot;currencies&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
  if not (has_items(ex.chests_by_level)&lt;br /&gt;
       or has_items(ex.chests_by_stars)&lt;br /&gt;
       or has_items(ex.chests_by_oracle)&lt;br /&gt;
       or has_items(ex.currencies)) then&lt;br /&gt;
    return &amp;quot;&amp;quot;&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  local outer_parts = {}&lt;br /&gt;
  local function add_group(kind, list)&lt;br /&gt;
    if not has_items(list) then return end&lt;br /&gt;
    local inner_content = build_tabber_content_for(list)&lt;br /&gt;
    local inner_tabber  = tabber_render_from_content(inner_content)&lt;br /&gt;
    outer_parts[#outer_parts+1] = &amp;quot;|-|&amp;quot; .. group_label(kind) .. &amp;quot;=\n&amp;quot; .. inner_tabber&lt;br /&gt;
  end&lt;br /&gt;
  add_group(&amp;quot;level&amp;quot;,  ex.chests_by_level)&lt;br /&gt;
  add_group(&amp;quot;stars&amp;quot;,  ex.chests_by_stars)&lt;br /&gt;
  add_group(&amp;quot;oracle&amp;quot;, ex.chests_by_oracle)&lt;br /&gt;
&lt;br /&gt;
  local left_tabber = tabber_render_from_content(table.concat(outer_parts, &amp;quot;\n&amp;quot;))&lt;br /&gt;
&lt;br /&gt;
  -- Preis/Limit (erste gefundene Gruppe)&lt;br /&gt;
  local function pick_price_limit()&lt;br /&gt;
    if has_items(ex.chests_by_level)  then return ex.chests_by_level[1].price or 0,  ex.chests_by_level[1].limit or &amp;quot;&amp;quot;  end&lt;br /&gt;
    if has_items(ex.chests_by_stars)  then return ex.chests_by_stars[1].price or 0,  ex.chests_by_stars[1].limit or &amp;quot;&amp;quot;  end&lt;br /&gt;
    if has_items(ex.chests_by_oracle) then return ex.chests_by_oracle[1].price or 0, ex.chests_by_oracle[1].limit or &amp;quot;&amp;quot; end&lt;br /&gt;
    return 0, &amp;quot;&amp;quot;&lt;br /&gt;
  end&lt;br /&gt;
  local price, limit = pick_price_limit()&lt;br /&gt;
&lt;br /&gt;
  local t = {}&lt;br /&gt;
  local H_offer, H_price, H_limit = header_labels(evkey)&lt;br /&gt;
  t[#t+1] = &#039;{| class=&amp;quot;article-table fs-exchange&amp;quot; style=&amp;quot;font-size:14px;&amp;quot;&#039;&lt;br /&gt;
  t[#t+1] = string.format(&lt;br /&gt;
    &#039;! style=&amp;quot;width:%s;&amp;quot; | %s !! style=&amp;quot;width:%s;&amp;quot; | %s !! style=&amp;quot;width:%s;&amp;quot; | %s&#039;,&lt;br /&gt;
    COL_OFFER, H_offer, COL_PRICE, H_price, COL_LIMIT, H_limit&lt;br /&gt;
  )&lt;br /&gt;
  t[#t+1] = &#039;|-&#039;&lt;br /&gt;
  t[#t+1] = &#039;|&#039; .. left_tabber .. string.format(&lt;br /&gt;
    &#039; || style=&amp;quot;text-align:center;&amp;quot; | %s %s %s || style=&amp;quot;text-align:center;&amp;quot; | %s&#039;,&lt;br /&gt;
    fileTag(token_icon, { size=&amp;quot;25px&amp;quot; }),&lt;br /&gt;
    tostring(price), token_name,&lt;br /&gt;
    tostring(limit)&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
  if has_items(ex.currencies) then&lt;br /&gt;
    for _,it in ipairs(ex.currencies) do&lt;br /&gt;
      local offer = string.format(&#039;%s %s&#039;,&lt;br /&gt;
        fileTag(it.icon or &amp;quot;&amp;quot;, { size=&amp;quot;30px&amp;quot; }),&lt;br /&gt;
        item_label(it.item, it.amount or 1)&lt;br /&gt;
      )&lt;br /&gt;
      t[#t+1] = &#039;|-&#039;&lt;br /&gt;
      t[#t+1] = string.format(&lt;br /&gt;
        &#039;| %s || style=&amp;quot;text-align:center;&amp;quot; | %s %s %s || style=&amp;quot;text-align:center;&amp;quot; | %s&#039;,&lt;br /&gt;
        offer,&lt;br /&gt;
        fileTag(token_icon, { size=&amp;quot;25px&amp;quot; }),&lt;br /&gt;
        tostring(it.price or 0), token_name,&lt;br /&gt;
        tostring(it.limit or &amp;quot;&amp;quot;)&lt;br /&gt;
      )&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  t[#t+1] = &#039;|}&#039;&lt;br /&gt;
  t[#t+1] = references_tag()&lt;br /&gt;
  return &#039;\n&#039; .. table.concat(t, &#039;\n&#039;)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function event_offers(ev, evkey, which)&lt;br /&gt;
  local list = (ev.shop or {})[which]&lt;br /&gt;
  if not has_items(list) then return &amp;quot;&amp;quot; end&lt;br /&gt;
  local token_icon = (ev.currency or {}).icon or &amp;quot;&amp;quot;&lt;br /&gt;
  local token_name = ev_label(evkey, &amp;quot;currencies&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
  local t = {}&lt;br /&gt;
  t[#t+1] = &#039;{| class=&amp;quot;article-table&amp;quot; style=&amp;quot;font-size:14px;&amp;quot;&#039;&lt;br /&gt;
  t[#t+1] = &amp;quot;! Angebot !! Menge&amp;quot;&lt;br /&gt;
  for _,it in ipairs(list) do&lt;br /&gt;
    t[#t+1] = &amp;quot;|-&amp;quot;&lt;br /&gt;
    t[#t+1] = string.format(&amp;quot;| &#039;&#039;%s&#039;&#039;&amp;lt;br /&amp;gt;%s || %s %s %s&amp;quot;,&lt;br /&gt;
      it.name or &amp;quot;&amp;quot;,&lt;br /&gt;
      fileTag(it.image, { size=&amp;quot;x50px&amp;quot; }),&lt;br /&gt;
      fileTag(token_icon, { size=&amp;quot;30px&amp;quot; }),&lt;br /&gt;
      tostring(it.token or 0),&lt;br /&gt;
      token_name&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
  t[#t+1] = &amp;quot;|}&amp;quot;&lt;br /&gt;
  return table.concat(t, &amp;quot;\n&amp;quot;)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- ========== SiteNotice-Support (Datum/Auto-Pick) ==========&lt;br /&gt;
local function event_window(ev)&lt;br /&gt;
  local S = ev.schedule or ev&lt;br /&gt;
  local s = S.start_date; if isempty(s) then return nil end&lt;br /&gt;
&lt;br /&gt;
  local st = parse_iso_date(s); if not st then return nil end&lt;br /&gt;
&lt;br /&gt;
  local en_excl, en_incl&lt;br /&gt;
  if not isempty(S.end_date) then&lt;br /&gt;
    -- end_date wird als inklusives Kalenderdatum verstanden&lt;br /&gt;
    en_incl = parse_iso_date(S.end_date)&lt;br /&gt;
    if not en_incl then return st, nil, nil end&lt;br /&gt;
    en_excl = en_incl + DAY&lt;br /&gt;
  else&lt;br /&gt;
    -- Dauer in Tagen (Standard 14), exklusives Ende = st + d*DAY&lt;br /&gt;
    local d = tonumber(S.duration_days) or 14&lt;br /&gt;
    en_excl = st + d * DAY&lt;br /&gt;
    en_incl = en_excl - DAY&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  return st, en_excl, en_incl&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function pick_notice_eventkey()&lt;br /&gt;
  local cfg = ((i18n().events or {}).notice or {})&lt;br /&gt;
  local pre  = tonumber(cfg.pre_days)  or 7&lt;br /&gt;
  local post = tonumber(cfg.post_days) or 3&lt;br /&gt;
  local now = os.time()&lt;br /&gt;
&lt;br /&gt;
  local running, upcoming, ended_recent = {}, {}, {}&lt;br /&gt;
&lt;br /&gt;
  for k, ev in pairs(events()) do&lt;br /&gt;
    local st, en_excl = event_window(ev)&lt;br /&gt;
    if st and en_excl then&lt;br /&gt;
      if now &amp;gt;= st and now &amp;lt; en_excl then&lt;br /&gt;
        table.insert(running, { k=k, st=st, en=en_excl })&lt;br /&gt;
      elseif now &amp;gt;= st - pre*DAY and now &amp;lt; st then&lt;br /&gt;
        table.insert(upcoming, { k=k, st=st, en=en_excl })&lt;br /&gt;
      elseif now &amp;gt;= en_excl and now &amp;lt; en_excl + post*DAY then&lt;br /&gt;
        table.insert(ended_recent, { k=k, st=st, en=en_excl })&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  local function by_end(a,b)   return a.en &amp;lt; b.en end&lt;br /&gt;
  local function by_start(a,b) return a.st &amp;lt; b.st end&lt;br /&gt;
  if #running      &amp;gt; 0 then table.sort(running,      by_end)   ; return running[1].k      end&lt;br /&gt;
  if #upcoming     &amp;gt; 0 then table.sort(upcoming,     by_start) ; return upcoming[1].k     end&lt;br /&gt;
  if #ended_recent &amp;gt; 0 then table.sort(ended_recent, by_end)   ; return ended_recent[1].k end&lt;br /&gt;
  return nil&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function event_notice(ev, evkey)&lt;br /&gt;
  local phrases = (((i18n().events or {}).phrases) or {})&lt;br /&gt;
  local PH_RUNNING = phrases.event_notice   -- &amp;quot;Das $1 läuft von $2 bis $3&amp;quot;&lt;br /&gt;
  local PH_ENDED   = phrases.event_ended    -- &amp;quot;Das $1 ist beendet. Du kannst verbleibende $4 noch im $5 eintauschen.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
  -- Ohne Texte kein Hinweis&lt;br /&gt;
  if isempty(PH_RUNNING) then return &amp;quot;&amp;quot; end&lt;br /&gt;
&lt;br /&gt;
  local st, en_excl, en_incl = event_window(ev); if not st or not en_excl then return &amp;quot;&amp;quot; end&lt;br /&gt;
&lt;br /&gt;
  local cfg  = ((i18n().events or {}).notice or {})&lt;br /&gt;
  local pre  = tonumber(cfg.pre_days)  or 7   -- z. B. 7 Tage vorher&lt;br /&gt;
  local post = tonumber(cfg.post_days) or 3   -- z. B. 3 Tage danach&lt;br /&gt;
  local now  = os.time()&lt;br /&gt;
&lt;br /&gt;
  local show_from = st - pre  * DAY&lt;br /&gt;
  local show_till = en_excl + post * DAY&lt;br /&gt;
  if now &amp;lt; show_from or now &amp;gt;= show_till then&lt;br /&gt;
    return &amp;quot;&amp;quot;&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  -- Anzeigename (aus i18n sections oder names)&lt;br /&gt;
  local name_label = ev_label(evkey, &amp;quot;eventname&amp;quot;)&lt;br /&gt;
  if isempty(name_label) then name_label = ev_name(evkey) end&lt;br /&gt;
&lt;br /&gt;
  -- Verlinken NUR wenn ev.page existiert&lt;br /&gt;
  local name = ev_pagelink(ev, evkey, name_label)&lt;br /&gt;
&lt;br /&gt;
  -- Icon aus EventData → currency.icon&lt;br /&gt;
  local ic = ev_notice_icon_wikitext(ev)  -- &amp;quot;&amp;quot; wenn kein currency.icon&lt;br /&gt;
&lt;br /&gt;
  local text&lt;br /&gt;
  if now &amp;lt; en_excl then&lt;br /&gt;
    -- läuft / startet bald&lt;br /&gt;
    text = PH_RUNNING&lt;br /&gt;
      :gsub(&amp;quot;$1&amp;quot;, name)&lt;br /&gt;
      :gsub(&amp;quot;$2&amp;quot;, format_date_de(st))&lt;br /&gt;
      :gsub(&amp;quot;$3&amp;quot;, format_date_de(en_excl))&lt;br /&gt;
  else&lt;br /&gt;
    -- beendet (bis zu post_days danach)&lt;br /&gt;
    if isempty(PH_ENDED) then return &amp;quot;&amp;quot; end&lt;br /&gt;
    -- Labels aus i18n.sections (wie gehabt)&lt;br /&gt;
    local curr = ev_label(evkey, &amp;quot;currencies&amp;quot;)&lt;br /&gt;
    local shop = ev_label(evkey, &amp;quot;ex_shop&amp;quot;)&lt;br /&gt;
    if isempty(curr) or isempty(shop) then return &amp;quot;&amp;quot; end&lt;br /&gt;
    text = PH_ENDED&lt;br /&gt;
      :gsub(&amp;quot;$1&amp;quot;, name)&lt;br /&gt;
      :gsub(&amp;quot;$4&amp;quot;, curr)&lt;br /&gt;
      :gsub(&amp;quot;$5&amp;quot;, shop)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  local prefix = (ic ~= &amp;quot;&amp;quot; and (ic .. &amp;quot; &amp;quot;) or &amp;quot;&amp;quot;)&lt;br /&gt;
  local suffix = (ic ~= &amp;quot;&amp;quot; and (&amp;quot; &amp;quot; .. ic) or &amp;quot;&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
  return string.format(&lt;br /&gt;
    &#039;&amp;lt;div class=&amp;quot;fs-notice&amp;quot; data-ev=&amp;quot;%s&amp;quot;&amp;gt;%s%s%s&amp;lt;/div&amp;gt;&#039;,&lt;br /&gt;
    mw.text.encode(evkey),&lt;br /&gt;
    prefix, text, suffix&lt;br /&gt;
  )&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- ========== Dispatcher ==========&lt;br /&gt;
local function get_event(evkey)&lt;br /&gt;
  local evs = events()&lt;br /&gt;
  local real = pick_key(evs, evkey)&lt;br /&gt;
  return real and evs[real] or nil, real or evkey&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function dispatch_event(args)&lt;br /&gt;
  local evkey = args[2]&lt;br /&gt;
&lt;br /&gt;
  -- {{Firestone|Event|notice}}  → automatisch wählen&lt;br /&gt;
  if evkey and norm(evkey) == &amp;quot;notice&amp;quot; then&lt;br /&gt;
    local pick = pick_notice_eventkey()&lt;br /&gt;
    if not pick then return &amp;quot;&amp;quot; end&lt;br /&gt;
    local ev, r = get_event(pick); if not ev then return &amp;quot;&amp;quot; end&lt;br /&gt;
    return event_notice(ev, r)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  -- normaler Weg&lt;br /&gt;
  if isempty(evkey) then return &amp;quot;&amp;quot; end&lt;br /&gt;
  local ev, realkey = get_event(evkey); if not ev then return &amp;quot;&amp;quot; end&lt;br /&gt;
&lt;br /&gt;
  local a3, a4 = args[3], args[4]&lt;br /&gt;
  if isempty(a3) then return ev_name(realkey) end&lt;br /&gt;
&lt;br /&gt;
  if a3==&amp;quot;notice&amp;quot;  then return event_notice(ev, realkey)&lt;br /&gt;
  elseif a3==&amp;quot;file&amp;quot;   then return event_file(ev, realkey, a4 or &amp;quot;&amp;quot;)&lt;br /&gt;
  elseif a3==&amp;quot;link&amp;quot;   then return event_link(ev, realkey, a4 or &amp;quot;&amp;quot;)&lt;br /&gt;
  elseif a3==&amp;quot;h2&amp;quot;     then return htag(2, ev_label(realkey, a4 or &amp;quot;&amp;quot;))&lt;br /&gt;
  elseif a3==&amp;quot;h3&amp;quot;     then return htag(3, ev_label(realkey, a4 or &amp;quot;&amp;quot;))&lt;br /&gt;
  elseif a3==&amp;quot;history&amp;quot;  then return event_history(ev, realkey, a)&lt;br /&gt;
  elseif a3==&amp;quot;avatars&amp;quot;  then return event_avatars(ev, realkey)&lt;br /&gt;
  elseif a3==&amp;quot;exchange&amp;quot; then return event_exchange_tabs(ev, realkey)&lt;br /&gt;
  elseif a3==&amp;quot;offers&amp;quot;   then&lt;br /&gt;
    if a4==&amp;quot;start&amp;quot; then return event_offers(ev, realkey, &amp;quot;initial&amp;quot;)&lt;br /&gt;
    elseif a4==&amp;quot;more&amp;quot; then return event_offers(ev, realkey, &amp;quot;after&amp;quot;) end&lt;br /&gt;
    return &amp;quot;&amp;quot;&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  -- Sonderfälle für Währungsnamen aus i18n.sections&lt;br /&gt;
  if a3 == &amp;quot;currency&amp;quot;  then return ev_label(realkey, &amp;quot;currency&amp;quot;)  end&lt;br /&gt;
  if a3 == &amp;quot;currencies&amp;quot; then return ev_label(realkey, &amp;quot;currencies&amp;quot;) end&lt;br /&gt;
&lt;br /&gt;
  -- generischer Pfad in EventData&lt;br /&gt;
  local path, i = {}, 3&lt;br /&gt;
  while args[i] do table.insert(path, args[i]); i=i+1 end&lt;br /&gt;
  local v = deep_get(ev, path)&lt;br /&gt;
  if v ~= nil then&lt;br /&gt;
    local last = path[#path] and tostring(path[#path]) or &amp;quot;&amp;quot;&lt;br /&gt;
    v = autotr(last, v)&lt;br /&gt;
    return tostring(v)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  -- Fallback auf Labels/Name/Unlocks&lt;br /&gt;
  local last = norm(path[#path] or a3)&lt;br /&gt;
  if last==&amp;quot;eventname&amp;quot; or last==&amp;quot;name&amp;quot; then return ev_name(realkey) end&lt;br /&gt;
  local lbl = ev_label(realkey, last); if not isempty(lbl) then return lbl end&lt;br /&gt;
  local U = ((i18n().events or {}).unlocks or {})[last]&lt;br /&gt;
  if U then return U end&lt;br /&gt;
&lt;br /&gt;
  return &amp;quot;&amp;quot;&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function dispatch_hero(args)&lt;br /&gt;
  local name = args[2]; if isempty(name) then return &amp;quot;&amp;quot; end&lt;br /&gt;
  local H = heroes()&lt;br /&gt;
  local real = pick_key(H, name); if not real then return &amp;quot;&amp;quot; end&lt;br /&gt;
  local h = H[real]&lt;br /&gt;
  local path, i = {}, 3&lt;br /&gt;
  while args[i] do table.insert(path, args[i]); i=i+1 end&lt;br /&gt;
  local v = deep_get(h, path)&lt;br /&gt;
  local last = path[#path] and tostring(path[#path]) or &amp;quot;&amp;quot;&lt;br /&gt;
  v = autotr(last, v)&lt;br /&gt;
  return tostring(v or &amp;quot;&amp;quot;)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- ========== main ==========&lt;br /&gt;
function M.main(frame)&lt;br /&gt;
  FRAME = frame&lt;br /&gt;
  local a = getArgs(frame)&lt;br /&gt;
  for i=1,10 do a[i] = a[i] and tostring(a[i]) or nil end&lt;br /&gt;
  local dom = a[1] and norm(a[1]) or &amp;quot;&amp;quot;&lt;br /&gt;
  if dom==&amp;quot;event&amp;quot; then&lt;br /&gt;
    return dispatch_event(a)&lt;br /&gt;
  elseif dom==&amp;quot;hero&amp;quot; then&lt;br /&gt;
    return dispatch_hero(a)&lt;br /&gt;
  else&lt;br /&gt;
    return &amp;quot;&amp;quot;&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
return M&lt;/div&gt;</summary>
		<author><name>Webmaster</name></author>
	</entry>
	<entry>
		<id>https://firestone-idle-rpg.de/index.php?title=Modul:Firestone&amp;diff=3751</id>
		<title>Modul:Firestone</title>
		<link rel="alternate" type="text/html" href="https://firestone-idle-rpg.de/index.php?title=Modul:Firestone&amp;diff=3751"/>
		<updated>2025-10-28T03:01:26Z</updated>

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

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

		<summary type="html">&lt;p&gt;Webmaster: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;-- Modul:EventData&lt;br /&gt;
local E = {}&lt;br /&gt;
&lt;br /&gt;
E.events = {&lt;br /&gt;
  halloween = {&lt;br /&gt;
    banner   = &amp;quot;EventHalloween.png&amp;quot;,&lt;br /&gt;
    type     = &amp;quot;calendar_event&amp;quot;,&lt;br /&gt;
    page = &amp;quot;Halloween_Event&amp;quot;,&lt;br /&gt;
    month    = &amp;quot;October&amp;quot;,          -- wird per i18n → „Oktober“&lt;br /&gt;
    start_date    = &amp;quot;2025-10-24&amp;quot;,&lt;br /&gt;
    duration = &amp;quot;2 weeks&amp;quot;,          -- i18n → „2 Wochen“&lt;br /&gt;
&lt;br /&gt;
    -- Bilder, die du in der Vorlage über {{Firestone|Event|NAME|file|…}} benutzt&lt;br /&gt;
    images = {&lt;br /&gt;
      deco     = &amp;quot;HalloweenDecorations.png&amp;quot;,&lt;br /&gt;
      fullname = &amp;quot;Maxwell.png&amp;quot;,&lt;br /&gt;
      ex_shop  = &amp;quot;WitchsHut.png&amp;quot;,&lt;br /&gt;
    },&lt;br /&gt;
&lt;br /&gt;
    -- Event-&amp;quot;Währung&amp;quot;&lt;br /&gt;
    currency = { key=&amp;quot;pumpkin&amp;quot;, icon=&amp;quot;CurrencyPumpkin.png&amp;quot; },&lt;br /&gt;
&lt;br /&gt;
    -- Shop-Angebote&lt;br /&gt;
    shop = {&lt;br /&gt;
      initial = {&lt;br /&gt;
        { name=&amp;quot;Group of Pumpkins&amp;quot;,   image=&amp;quot;PumpkinGroup.png&amp;quot;,   token=2700  },&lt;br /&gt;
        { name=&amp;quot;Hat of Pumpkins&amp;quot;,     image=&amp;quot;PumpkinHat.png&amp;quot;,     token=6000  },&lt;br /&gt;
        { name=&amp;quot;Bucket of Pumpkins&amp;quot;,  image=&amp;quot;PumpkinBucket.png&amp;quot;,  token=16500 },&lt;br /&gt;
      },&lt;br /&gt;
      after = {&lt;br /&gt;
        { name=&amp;quot;Cart of Pumpkins&amp;quot;,     image=&amp;quot;PumpkinBarrow.png&amp;quot;,   token=34500  },&lt;br /&gt;
        { name=&amp;quot;Cauldron of Pumpkins&amp;quot;, image=&amp;quot;PumpkinCauldron.png&amp;quot;, token=72000  },&lt;br /&gt;
        { name=&amp;quot;Coffin of Pumpkins&amp;quot;,   image=&amp;quot;PumpkinCoffin.png&amp;quot;,   token=187500 },&lt;br /&gt;
        { name=&amp;quot;Carriage of Pumpkins&amp;quot;, image=&amp;quot;PumpkinCoach.png&amp;quot;,    token=390000 },&lt;br /&gt;
      },&lt;br /&gt;
    },&lt;br /&gt;
&lt;br /&gt;
    -- Austausch (verkürzte, saubere Struktur – du kannst jederzeit erweitern)&lt;br /&gt;
    exchange = {&lt;br /&gt;
      chests_by_level = {&lt;br /&gt;
        { range=&amp;quot;1-64&amp;quot;,    icon=&amp;quot;RareChest.png&amp;quot;,      item=&amp;quot;rare_chest&amp;quot;,      amount=1, price=2500, limit=25 },&lt;br /&gt;
        { range=&amp;quot;65-129&amp;quot;,  icon=&amp;quot;EpicChest.png&amp;quot;,      item=&amp;quot;epic_chest&amp;quot;,      amount=1, price=2500, limit=25 },&lt;br /&gt;
        { range=&amp;quot;130-299&amp;quot;, icon=&amp;quot;LegendaryChest.png&amp;quot;, item=&amp;quot;legendary_chest&amp;quot;, amount=1, price=2500, limit=25 },&lt;br /&gt;
        { range=&amp;quot;300-&amp;quot;,    icon=&amp;quot;MythicChest.png&amp;quot;,    item=&amp;quot;mythic_chest&amp;quot;,    amount=1, price=2500, limit=25, req_gear_power = &amp;quot;5e9&amp;quot; },&lt;br /&gt;
      },&lt;br /&gt;
      chests_by_stars = {&lt;br /&gt;
        { range=&amp;quot;1-99&amp;quot;,    icon=&amp;quot;GoldChest.png&amp;quot;,     item=&amp;quot;golden_chest&amp;quot;,   amount=1, price=2500, limit=25 },&lt;br /&gt;
        { range=&amp;quot;100-144&amp;quot;, icon=&amp;quot;DiamondChest.png&amp;quot;,  item=&amp;quot;diamond_chest&amp;quot;,  amount=1, price=2500, limit=25 },&lt;br /&gt;
        { range=&amp;quot;145-189&amp;quot;, icon=&amp;quot;OpalChest.png&amp;quot;,     item=&amp;quot;opal_chest&amp;quot;,     amount=1, price=2500, limit=25 },&lt;br /&gt;
        { range=&amp;quot;190-&amp;quot;,    icon=&amp;quot;EmeraldChest.png&amp;quot;,  item=&amp;quot;emerald_chest&amp;quot;,  amount=1, price=2500, limit=25 },&lt;br /&gt;
      },&lt;br /&gt;
      chests_by_oracle = {&lt;br /&gt;
        { range=&amp;quot;1-97&amp;quot;,    icon=&amp;quot;SolarChest.png&amp;quot;,    item=&amp;quot;solar_chest&amp;quot;,    amount=1, price=2500, limit=25 },&lt;br /&gt;
        { range=&amp;quot;98-148&amp;quot;,  icon=&amp;quot;NebulaChest.png&amp;quot;,   item=&amp;quot;nebula_chest&amp;quot;,   amount=1, price=2500, limit=25 },&lt;br /&gt;
        { range=&amp;quot;149-200&amp;quot;, icon=&amp;quot;CosmicChest.png&amp;quot;,   item=&amp;quot;cosmic_chest&amp;quot;,   amount=1, price=2500, limit=25 },&lt;br /&gt;
        { range=&amp;quot;201-&amp;quot;,    icon=&amp;quot;GalaxyChest.png&amp;quot;,   item=&amp;quot;galaxy_chest&amp;quot;,   amount=1, price=2500, limit=25 },&lt;br /&gt;
      },&lt;br /&gt;
      currencies = {&lt;br /&gt;
        { icon=&amp;quot;CurrencyExoticCoin.png&amp;quot;, item=&amp;quot;exotic_coins&amp;quot;,  amount=500,  price=1250, limit=50  },&lt;br /&gt;
        { icon=&amp;quot;CurrencyPickaxe.png&amp;quot;,    item=&amp;quot;pickaxe&amp;quot;,       amount=1,    price=375,  limit=70  },&lt;br /&gt;
        { icon=&amp;quot;CurrencyStrangeDust.png&amp;quot;,item=&amp;quot;strange_dust&amp;quot;,  amount=20,   price=500,  limit=300 },&lt;br /&gt;
        { icon=&amp;quot;CurrencyHonor.png&amp;quot;,      item=&amp;quot;honor&amp;quot;,         amount=5,    price=1500, limit=40  },&lt;br /&gt;
        { icon=&amp;quot;CurrencyBeer.png&amp;quot;,       item=&amp;quot;beer&amp;quot;,          amount=500,  price=1250, limit=150 },&lt;br /&gt;
        { icon=&amp;quot;CurrencyMeteorite.png&amp;quot;,  item=&amp;quot;meteorite&amp;quot;,     amount=150,  price=1500, limit=50  },&lt;br /&gt;
        { icon=&amp;quot;CurrencyGoldenKeys.png&amp;quot;, item=&amp;quot;golden_key&amp;quot;,    amount=1,    price=3000, limit=25  },&lt;br /&gt;
      },&lt;br /&gt;
    },&lt;br /&gt;
&lt;br /&gt;
    avatars = {&lt;br /&gt;
      [2022] = {&lt;br /&gt;
        { file=&amp;quot;AvatarMaxwell.png&amp;quot;,  title=&amp;quot;Maxwell, the Pumpkin Head&amp;quot;, cost=21000 },&lt;br /&gt;
        { file=&amp;quot;AvatarBilly.png&amp;quot;,    title=&amp;quot;Billy, the Happy Ghost&amp;quot;,    cost=21000 },&lt;br /&gt;
        { file=&amp;quot;AvatarYvonne.png&amp;quot;,   title=&amp;quot;Yvonne, The Trickster&amp;quot;,     cost=21000 },&lt;br /&gt;
        { file=&amp;quot;AvatarLucien.png&amp;quot;,   title=&amp;quot;Lucien, The Vampire&amp;quot;,       cost=21000 },&lt;br /&gt;
      },&lt;br /&gt;
      [2023] = {&lt;br /&gt;
        { file=&amp;quot;AvatarMaxwell.png&amp;quot;,  title=&amp;quot;Maxwell, the Pumpkin Head&amp;quot;, cost=21000 },&lt;br /&gt;
        { file=&amp;quot;AvatarEmberlyn.png&amp;quot;, title=&amp;quot;Emberlyn, the Bowian Witch&amp;quot;,cost=21000 },&lt;br /&gt;
        { file=&amp;quot;AvatarSpindle.png&amp;quot;,  title=&amp;quot;Spindle, the Crafty Weaver&amp;quot;,cost=21000 },&lt;br /&gt;
        { file=&amp;quot;AvatarTrashy.png&amp;quot;,   title=&amp;quot;Trashy, the Wacky Bin&amp;quot;,     cost=21000 },&lt;br /&gt;
      },&lt;br /&gt;
      [2024] = {&lt;br /&gt;
        { file=&amp;quot;AvatarMaxwell.png&amp;quot;,  title=&amp;quot;Maxwell, the Pumpkin Head&amp;quot;, cost=21000 },&lt;br /&gt;
        { file=&amp;quot;AvatarCatrina.png&amp;quot;,  title=&amp;quot;Catrina, the Undying Beauty&amp;quot;, cost=21000 },&lt;br /&gt;
        { file=&amp;quot;AvatarBaron.png&amp;quot;,    title=&amp;quot;Baron, the Voodoo Master&amp;quot;,    cost=21000 },&lt;br /&gt;
        { file=&amp;quot;AvatarSonar.png&amp;quot;,    title=&amp;quot;Sonar, the Nocturnal Messenger&amp;quot;, cost=21000 },&lt;br /&gt;
      },&lt;br /&gt;
    },&lt;br /&gt;
&lt;br /&gt;
    history = {&lt;br /&gt;
      { year=2022, start=&amp;quot;2022-10-24&amp;quot;, [&amp;quot;end&amp;quot;]=&amp;quot;2022-11-07&amp;quot; },&lt;br /&gt;
      { year=2023, start=&amp;quot;2023-10-20&amp;quot;, [&amp;quot;end&amp;quot;]=&amp;quot;2023-11-03&amp;quot;, alt_platform=&amp;quot;2023-10-24 bis 2023-11-07&amp;quot; },&lt;br /&gt;
      { year=2024, start=&amp;quot;2024-10-25&amp;quot;, [&amp;quot;end&amp;quot;]=&amp;quot;2024-11-08&amp;quot; },&lt;br /&gt;
    },&lt;br /&gt;
  },&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
return E&lt;/div&gt;</summary>
		<author><name>Webmaster</name></author>
	</entry>
	<entry>
		<id>https://firestone-idle-rpg.de/index.php?title=Modul:EventData&amp;diff=3748</id>
		<title>Modul:EventData</title>
		<link rel="alternate" type="text/html" href="https://firestone-idle-rpg.de/index.php?title=Modul:EventData&amp;diff=3748"/>
		<updated>2025-10-28T01:03:41Z</updated>

		<summary type="html">&lt;p&gt;Webmaster: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;-- Modul:EventData&lt;br /&gt;
local E = {}&lt;br /&gt;
&lt;br /&gt;
E.events = {&lt;br /&gt;
  halloween = {&lt;br /&gt;
    banner   = &amp;quot;EventHalloween.png&amp;quot;,&lt;br /&gt;
    type     = &amp;quot;calendar_event&amp;quot;,&lt;br /&gt;
    page = &amp;quot;Halloween_Event&amp;quot;,&lt;br /&gt;
    month    = &amp;quot;October&amp;quot;,          -- wird per i18n → „Oktober“&lt;br /&gt;
    start_date    = &amp;quot;2025-10-13&amp;quot;,&lt;br /&gt;
    duration = &amp;quot;2 weeks&amp;quot;,          -- i18n → „2 Wochen“&lt;br /&gt;
&lt;br /&gt;
    -- Bilder, die du in der Vorlage über {{Firestone|Event|NAME|file|…}} benutzt&lt;br /&gt;
    images = {&lt;br /&gt;
      deco     = &amp;quot;HalloweenDecorations.png&amp;quot;,&lt;br /&gt;
      fullname = &amp;quot;Maxwell.png&amp;quot;,&lt;br /&gt;
      ex_shop  = &amp;quot;WitchsHut.png&amp;quot;,&lt;br /&gt;
    },&lt;br /&gt;
&lt;br /&gt;
    -- Event-&amp;quot;Währung&amp;quot;&lt;br /&gt;
    currency = { key=&amp;quot;pumpkin&amp;quot;, icon=&amp;quot;CurrencyPumpkin.png&amp;quot; },&lt;br /&gt;
&lt;br /&gt;
    -- Shop-Angebote&lt;br /&gt;
    shop = {&lt;br /&gt;
      initial = {&lt;br /&gt;
        { name=&amp;quot;Group of Pumpkins&amp;quot;,   image=&amp;quot;PumpkinGroup.png&amp;quot;,   token=2700  },&lt;br /&gt;
        { name=&amp;quot;Hat of Pumpkins&amp;quot;,     image=&amp;quot;PumpkinHat.png&amp;quot;,     token=6000  },&lt;br /&gt;
        { name=&amp;quot;Bucket of Pumpkins&amp;quot;,  image=&amp;quot;PumpkinBucket.png&amp;quot;,  token=16500 },&lt;br /&gt;
      },&lt;br /&gt;
      after = {&lt;br /&gt;
        { name=&amp;quot;Cart of Pumpkins&amp;quot;,     image=&amp;quot;PumpkinBarrow.png&amp;quot;,   token=34500  },&lt;br /&gt;
        { name=&amp;quot;Cauldron of Pumpkins&amp;quot;, image=&amp;quot;PumpkinCauldron.png&amp;quot;, token=72000  },&lt;br /&gt;
        { name=&amp;quot;Coffin of Pumpkins&amp;quot;,   image=&amp;quot;PumpkinCoffin.png&amp;quot;,   token=187500 },&lt;br /&gt;
        { name=&amp;quot;Carriage of Pumpkins&amp;quot;, image=&amp;quot;PumpkinCoach.png&amp;quot;,    token=390000 },&lt;br /&gt;
      },&lt;br /&gt;
    },&lt;br /&gt;
&lt;br /&gt;
    -- Austausch (verkürzte, saubere Struktur – du kannst jederzeit erweitern)&lt;br /&gt;
    exchange = {&lt;br /&gt;
      chests_by_level = {&lt;br /&gt;
        { range=&amp;quot;1-64&amp;quot;,    icon=&amp;quot;RareChest.png&amp;quot;,      item=&amp;quot;rare_chest&amp;quot;,      amount=1, price=2500, limit=25 },&lt;br /&gt;
        { range=&amp;quot;65-129&amp;quot;,  icon=&amp;quot;EpicChest.png&amp;quot;,      item=&amp;quot;epic_chest&amp;quot;,      amount=1, price=2500, limit=25 },&lt;br /&gt;
        { range=&amp;quot;130-299&amp;quot;, icon=&amp;quot;LegendaryChest.png&amp;quot;, item=&amp;quot;legendary_chest&amp;quot;, amount=1, price=2500, limit=25 },&lt;br /&gt;
        { range=&amp;quot;300-&amp;quot;,    icon=&amp;quot;MythicChest.png&amp;quot;,    item=&amp;quot;mythic_chest&amp;quot;,    amount=1, price=2500, limit=25, req_gear_power = &amp;quot;5e9&amp;quot; },&lt;br /&gt;
      },&lt;br /&gt;
      chests_by_stars = {&lt;br /&gt;
        { range=&amp;quot;1-99&amp;quot;,    icon=&amp;quot;GoldChest.png&amp;quot;,     item=&amp;quot;golden_chest&amp;quot;,   amount=1, price=2500, limit=25 },&lt;br /&gt;
        { range=&amp;quot;100-144&amp;quot;, icon=&amp;quot;DiamondChest.png&amp;quot;,  item=&amp;quot;diamond_chest&amp;quot;,  amount=1, price=2500, limit=25 },&lt;br /&gt;
        { range=&amp;quot;145-189&amp;quot;, icon=&amp;quot;OpalChest.png&amp;quot;,     item=&amp;quot;opal_chest&amp;quot;,     amount=1, price=2500, limit=25 },&lt;br /&gt;
        { range=&amp;quot;190-&amp;quot;,    icon=&amp;quot;EmeraldChest.png&amp;quot;,  item=&amp;quot;emerald_chest&amp;quot;,  amount=1, price=2500, limit=25 },&lt;br /&gt;
      },&lt;br /&gt;
      chests_by_oracle = {&lt;br /&gt;
        { range=&amp;quot;1-97&amp;quot;,    icon=&amp;quot;SolarChest.png&amp;quot;,    item=&amp;quot;solar_chest&amp;quot;,    amount=1, price=2500, limit=25 },&lt;br /&gt;
        { range=&amp;quot;98-148&amp;quot;,  icon=&amp;quot;NebulaChest.png&amp;quot;,   item=&amp;quot;nebula_chest&amp;quot;,   amount=1, price=2500, limit=25 },&lt;br /&gt;
        { range=&amp;quot;149-200&amp;quot;, icon=&amp;quot;CosmicChest.png&amp;quot;,   item=&amp;quot;cosmic_chest&amp;quot;,   amount=1, price=2500, limit=25 },&lt;br /&gt;
        { range=&amp;quot;201-&amp;quot;,    icon=&amp;quot;GalaxyChest.png&amp;quot;,   item=&amp;quot;galaxy_chest&amp;quot;,   amount=1, price=2500, limit=25 },&lt;br /&gt;
      },&lt;br /&gt;
      currencies = {&lt;br /&gt;
        { icon=&amp;quot;CurrencyExoticCoin.png&amp;quot;, item=&amp;quot;exotic_coins&amp;quot;,  amount=500,  price=1250, limit=50  },&lt;br /&gt;
        { icon=&amp;quot;CurrencyPickaxe.png&amp;quot;,    item=&amp;quot;pickaxe&amp;quot;,       amount=1,    price=375,  limit=70  },&lt;br /&gt;
        { icon=&amp;quot;CurrencyStrangeDust.png&amp;quot;,item=&amp;quot;strange_dust&amp;quot;,  amount=20,   price=500,  limit=300 },&lt;br /&gt;
        { icon=&amp;quot;CurrencyHonor.png&amp;quot;,      item=&amp;quot;honor&amp;quot;,         amount=5,    price=1500, limit=40  },&lt;br /&gt;
        { icon=&amp;quot;CurrencyBeer.png&amp;quot;,       item=&amp;quot;beer&amp;quot;,          amount=500,  price=1250, limit=150 },&lt;br /&gt;
        { icon=&amp;quot;CurrencyMeteorite.png&amp;quot;,  item=&amp;quot;meteorite&amp;quot;,     amount=150,  price=1500, limit=50  },&lt;br /&gt;
        { icon=&amp;quot;CurrencyGoldenKeys.png&amp;quot;, item=&amp;quot;golden_key&amp;quot;,    amount=1,    price=3000, limit=25  },&lt;br /&gt;
      },&lt;br /&gt;
    },&lt;br /&gt;
&lt;br /&gt;
    avatars = {&lt;br /&gt;
      [2022] = {&lt;br /&gt;
        { file=&amp;quot;AvatarMaxwell.png&amp;quot;,  title=&amp;quot;Maxwell, the Pumpkin Head&amp;quot;, cost=21000 },&lt;br /&gt;
        { file=&amp;quot;AvatarBilly.png&amp;quot;,    title=&amp;quot;Billy, the Happy Ghost&amp;quot;,    cost=21000 },&lt;br /&gt;
        { file=&amp;quot;AvatarYvonne.png&amp;quot;,   title=&amp;quot;Yvonne, The Trickster&amp;quot;,     cost=21000 },&lt;br /&gt;
        { file=&amp;quot;AvatarLucien.png&amp;quot;,   title=&amp;quot;Lucien, The Vampire&amp;quot;,       cost=21000 },&lt;br /&gt;
      },&lt;br /&gt;
      [2023] = {&lt;br /&gt;
        { file=&amp;quot;AvatarMaxwell.png&amp;quot;,  title=&amp;quot;Maxwell, the Pumpkin Head&amp;quot;, cost=21000 },&lt;br /&gt;
        { file=&amp;quot;AvatarEmberlyn.png&amp;quot;, title=&amp;quot;Emberlyn, the Bowian Witch&amp;quot;,cost=21000 },&lt;br /&gt;
        { file=&amp;quot;AvatarSpindle.png&amp;quot;,  title=&amp;quot;Spindle, the Crafty Weaver&amp;quot;,cost=21000 },&lt;br /&gt;
        { file=&amp;quot;AvatarTrashy.png&amp;quot;,   title=&amp;quot;Trashy, the Wacky Bin&amp;quot;,     cost=21000 },&lt;br /&gt;
      },&lt;br /&gt;
      [2024] = {&lt;br /&gt;
        { file=&amp;quot;AvatarMaxwell.png&amp;quot;,  title=&amp;quot;Maxwell, the Pumpkin Head&amp;quot;, cost=21000 },&lt;br /&gt;
        { file=&amp;quot;AvatarCatrina.png&amp;quot;,  title=&amp;quot;Catrina, the Undying Beauty&amp;quot;, cost=21000 },&lt;br /&gt;
        { file=&amp;quot;AvatarBaron.png&amp;quot;,    title=&amp;quot;Baron, the Voodoo Master&amp;quot;,    cost=21000 },&lt;br /&gt;
        { file=&amp;quot;AvatarSonar.png&amp;quot;,    title=&amp;quot;Sonar, the Nocturnal Messenger&amp;quot;, cost=21000 },&lt;br /&gt;
      },&lt;br /&gt;
    },&lt;br /&gt;
&lt;br /&gt;
    history = {&lt;br /&gt;
      { year=2022, start=&amp;quot;2022-10-24&amp;quot;, [&amp;quot;end&amp;quot;]=&amp;quot;2022-11-07&amp;quot; },&lt;br /&gt;
      { year=2023, start=&amp;quot;2023-10-20&amp;quot;, [&amp;quot;end&amp;quot;]=&amp;quot;2023-11-03&amp;quot;, alt_platform=&amp;quot;2023-10-24 bis 2023-11-07&amp;quot; },&lt;br /&gt;
      { year=2024, start=&amp;quot;2024-10-25&amp;quot;, [&amp;quot;end&amp;quot;]=&amp;quot;2024-11-08&amp;quot; },&lt;br /&gt;
    },&lt;br /&gt;
  },&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
return E&lt;/div&gt;</summary>
		<author><name>Webmaster</name></author>
	</entry>
	<entry>
		<id>https://firestone-idle-rpg.de/index.php?title=Modul:EventData&amp;diff=3747</id>
		<title>Modul:EventData</title>
		<link rel="alternate" type="text/html" href="https://firestone-idle-rpg.de/index.php?title=Modul:EventData&amp;diff=3747"/>
		<updated>2025-10-28T01:02:21Z</updated>

		<summary type="html">&lt;p&gt;Webmaster: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;-- Modul:EventData&lt;br /&gt;
local E = {}&lt;br /&gt;
&lt;br /&gt;
E.events = {&lt;br /&gt;
  halloween = {&lt;br /&gt;
    banner   = &amp;quot;EventHalloween.png&amp;quot;,&lt;br /&gt;
    type     = &amp;quot;calendar_event&amp;quot;,&lt;br /&gt;
    page = &amp;quot;Halloween_Event&amp;quot;,&lt;br /&gt;
    month    = &amp;quot;October&amp;quot;,          -- wird per i18n → „Oktober“&lt;br /&gt;
    start_date    = &amp;quot;2025-10-14&amp;quot;,&lt;br /&gt;
    duration = &amp;quot;2 weeks&amp;quot;,          -- i18n → „2 Wochen“&lt;br /&gt;
&lt;br /&gt;
    -- Bilder, die du in der Vorlage über {{Firestone|Event|NAME|file|…}} benutzt&lt;br /&gt;
    images = {&lt;br /&gt;
      deco     = &amp;quot;HalloweenDecorations.png&amp;quot;,&lt;br /&gt;
      fullname = &amp;quot;Maxwell.png&amp;quot;,&lt;br /&gt;
      ex_shop  = &amp;quot;WitchsHut.png&amp;quot;,&lt;br /&gt;
    },&lt;br /&gt;
&lt;br /&gt;
    -- Event-&amp;quot;Währung&amp;quot;&lt;br /&gt;
    currency = { key=&amp;quot;pumpkin&amp;quot;, icon=&amp;quot;CurrencyPumpkin.png&amp;quot; },&lt;br /&gt;
&lt;br /&gt;
    -- Shop-Angebote&lt;br /&gt;
    shop = {&lt;br /&gt;
      initial = {&lt;br /&gt;
        { name=&amp;quot;Group of Pumpkins&amp;quot;,   image=&amp;quot;PumpkinGroup.png&amp;quot;,   token=2700  },&lt;br /&gt;
        { name=&amp;quot;Hat of Pumpkins&amp;quot;,     image=&amp;quot;PumpkinHat.png&amp;quot;,     token=6000  },&lt;br /&gt;
        { name=&amp;quot;Bucket of Pumpkins&amp;quot;,  image=&amp;quot;PumpkinBucket.png&amp;quot;,  token=16500 },&lt;br /&gt;
      },&lt;br /&gt;
      after = {&lt;br /&gt;
        { name=&amp;quot;Cart of Pumpkins&amp;quot;,     image=&amp;quot;PumpkinBarrow.png&amp;quot;,   token=34500  },&lt;br /&gt;
        { name=&amp;quot;Cauldron of Pumpkins&amp;quot;, image=&amp;quot;PumpkinCauldron.png&amp;quot;, token=72000  },&lt;br /&gt;
        { name=&amp;quot;Coffin of Pumpkins&amp;quot;,   image=&amp;quot;PumpkinCoffin.png&amp;quot;,   token=187500 },&lt;br /&gt;
        { name=&amp;quot;Carriage of Pumpkins&amp;quot;, image=&amp;quot;PumpkinCoach.png&amp;quot;,    token=390000 },&lt;br /&gt;
      },&lt;br /&gt;
    },&lt;br /&gt;
&lt;br /&gt;
    -- Austausch (verkürzte, saubere Struktur – du kannst jederzeit erweitern)&lt;br /&gt;
    exchange = {&lt;br /&gt;
      chests_by_level = {&lt;br /&gt;
        { range=&amp;quot;1-64&amp;quot;,    icon=&amp;quot;RareChest.png&amp;quot;,      item=&amp;quot;rare_chest&amp;quot;,      amount=1, price=2500, limit=25 },&lt;br /&gt;
        { range=&amp;quot;65-129&amp;quot;,  icon=&amp;quot;EpicChest.png&amp;quot;,      item=&amp;quot;epic_chest&amp;quot;,      amount=1, price=2500, limit=25 },&lt;br /&gt;
        { range=&amp;quot;130-299&amp;quot;, icon=&amp;quot;LegendaryChest.png&amp;quot;, item=&amp;quot;legendary_chest&amp;quot;, amount=1, price=2500, limit=25 },&lt;br /&gt;
        { range=&amp;quot;300-&amp;quot;,    icon=&amp;quot;MythicChest.png&amp;quot;,    item=&amp;quot;mythic_chest&amp;quot;,    amount=1, price=2500, limit=25, req_gear_power = &amp;quot;5e9&amp;quot; },&lt;br /&gt;
      },&lt;br /&gt;
      chests_by_stars = {&lt;br /&gt;
        { range=&amp;quot;1-99&amp;quot;,    icon=&amp;quot;GoldChest.png&amp;quot;,     item=&amp;quot;golden_chest&amp;quot;,   amount=1, price=2500, limit=25 },&lt;br /&gt;
        { range=&amp;quot;100-144&amp;quot;, icon=&amp;quot;DiamondChest.png&amp;quot;,  item=&amp;quot;diamond_chest&amp;quot;,  amount=1, price=2500, limit=25 },&lt;br /&gt;
        { range=&amp;quot;145-189&amp;quot;, icon=&amp;quot;OpalChest.png&amp;quot;,     item=&amp;quot;opal_chest&amp;quot;,     amount=1, price=2500, limit=25 },&lt;br /&gt;
        { range=&amp;quot;190-&amp;quot;,    icon=&amp;quot;EmeraldChest.png&amp;quot;,  item=&amp;quot;emerald_chest&amp;quot;,  amount=1, price=2500, limit=25 },&lt;br /&gt;
      },&lt;br /&gt;
      chests_by_oracle = {&lt;br /&gt;
        { range=&amp;quot;1-97&amp;quot;,    icon=&amp;quot;SolarChest.png&amp;quot;,    item=&amp;quot;solar_chest&amp;quot;,    amount=1, price=2500, limit=25 },&lt;br /&gt;
        { range=&amp;quot;98-148&amp;quot;,  icon=&amp;quot;NebulaChest.png&amp;quot;,   item=&amp;quot;nebula_chest&amp;quot;,   amount=1, price=2500, limit=25 },&lt;br /&gt;
        { range=&amp;quot;149-200&amp;quot;, icon=&amp;quot;CosmicChest.png&amp;quot;,   item=&amp;quot;cosmic_chest&amp;quot;,   amount=1, price=2500, limit=25 },&lt;br /&gt;
        { range=&amp;quot;201-&amp;quot;,    icon=&amp;quot;GalaxyChest.png&amp;quot;,   item=&amp;quot;galaxy_chest&amp;quot;,   amount=1, price=2500, limit=25 },&lt;br /&gt;
      },&lt;br /&gt;
      currencies = {&lt;br /&gt;
        { icon=&amp;quot;CurrencyExoticCoin.png&amp;quot;, item=&amp;quot;exotic_coins&amp;quot;,  amount=500,  price=1250, limit=50  },&lt;br /&gt;
        { icon=&amp;quot;CurrencyPickaxe.png&amp;quot;,    item=&amp;quot;pickaxe&amp;quot;,       amount=1,    price=375,  limit=70  },&lt;br /&gt;
        { icon=&amp;quot;CurrencyStrangeDust.png&amp;quot;,item=&amp;quot;strange_dust&amp;quot;,  amount=20,   price=500,  limit=300 },&lt;br /&gt;
        { icon=&amp;quot;CurrencyHonor.png&amp;quot;,      item=&amp;quot;honor&amp;quot;,         amount=5,    price=1500, limit=40  },&lt;br /&gt;
        { icon=&amp;quot;CurrencyBeer.png&amp;quot;,       item=&amp;quot;beer&amp;quot;,          amount=500,  price=1250, limit=150 },&lt;br /&gt;
        { icon=&amp;quot;CurrencyMeteorite.png&amp;quot;,  item=&amp;quot;meteorite&amp;quot;,     amount=150,  price=1500, limit=50  },&lt;br /&gt;
        { icon=&amp;quot;CurrencyGoldenKeys.png&amp;quot;, item=&amp;quot;golden_key&amp;quot;,    amount=1,    price=3000, limit=25  },&lt;br /&gt;
      },&lt;br /&gt;
    },&lt;br /&gt;
&lt;br /&gt;
    avatars = {&lt;br /&gt;
      [2022] = {&lt;br /&gt;
        { file=&amp;quot;AvatarMaxwell.png&amp;quot;,  title=&amp;quot;Maxwell, the Pumpkin Head&amp;quot;, cost=21000 },&lt;br /&gt;
        { file=&amp;quot;AvatarBilly.png&amp;quot;,    title=&amp;quot;Billy, the Happy Ghost&amp;quot;,    cost=21000 },&lt;br /&gt;
        { file=&amp;quot;AvatarYvonne.png&amp;quot;,   title=&amp;quot;Yvonne, The Trickster&amp;quot;,     cost=21000 },&lt;br /&gt;
        { file=&amp;quot;AvatarLucien.png&amp;quot;,   title=&amp;quot;Lucien, The Vampire&amp;quot;,       cost=21000 },&lt;br /&gt;
      },&lt;br /&gt;
      [2023] = {&lt;br /&gt;
        { file=&amp;quot;AvatarMaxwell.png&amp;quot;,  title=&amp;quot;Maxwell, the Pumpkin Head&amp;quot;, cost=21000 },&lt;br /&gt;
        { file=&amp;quot;AvatarEmberlyn.png&amp;quot;, title=&amp;quot;Emberlyn, the Bowian Witch&amp;quot;,cost=21000 },&lt;br /&gt;
        { file=&amp;quot;AvatarSpindle.png&amp;quot;,  title=&amp;quot;Spindle, the Crafty Weaver&amp;quot;,cost=21000 },&lt;br /&gt;
        { file=&amp;quot;AvatarTrashy.png&amp;quot;,   title=&amp;quot;Trashy, the Wacky Bin&amp;quot;,     cost=21000 },&lt;br /&gt;
      },&lt;br /&gt;
      [2024] = {&lt;br /&gt;
        { file=&amp;quot;AvatarMaxwell.png&amp;quot;,  title=&amp;quot;Maxwell, the Pumpkin Head&amp;quot;, cost=21000 },&lt;br /&gt;
        { file=&amp;quot;AvatarCatrina.png&amp;quot;,  title=&amp;quot;Catrina, the Undying Beauty&amp;quot;, cost=21000 },&lt;br /&gt;
        { file=&amp;quot;AvatarBaron.png&amp;quot;,    title=&amp;quot;Baron, the Voodoo Master&amp;quot;,    cost=21000 },&lt;br /&gt;
        { file=&amp;quot;AvatarSonar.png&amp;quot;,    title=&amp;quot;Sonar, the Nocturnal Messenger&amp;quot;, cost=21000 },&lt;br /&gt;
      },&lt;br /&gt;
    },&lt;br /&gt;
&lt;br /&gt;
    history = {&lt;br /&gt;
      { year=2022, start=&amp;quot;2022-10-24&amp;quot;, [&amp;quot;end&amp;quot;]=&amp;quot;2022-11-07&amp;quot; },&lt;br /&gt;
      { year=2023, start=&amp;quot;2023-10-20&amp;quot;, [&amp;quot;end&amp;quot;]=&amp;quot;2023-11-03&amp;quot;, alt_platform=&amp;quot;2023-10-24 bis 2023-11-07&amp;quot; },&lt;br /&gt;
      { year=2024, start=&amp;quot;2024-10-25&amp;quot;, [&amp;quot;end&amp;quot;]=&amp;quot;2024-11-08&amp;quot; },&lt;br /&gt;
    },&lt;br /&gt;
  },&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
return E&lt;/div&gt;</summary>
		<author><name>Webmaster</name></author>
	</entry>
	<entry>
		<id>https://firestone-idle-rpg.de/index.php?title=Modul:HeroI18n&amp;diff=3746</id>
		<title>Modul:HeroI18n</title>
		<link rel="alternate" type="text/html" href="https://firestone-idle-rpg.de/index.php?title=Modul:HeroI18n&amp;diff=3746"/>
		<updated>2025-10-28T01:01:48Z</updated>

		<summary type="html">&lt;p&gt;Webmaster: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;local M = {}&lt;br /&gt;
M.i18n = M.i18n or {}&lt;br /&gt;
&lt;br /&gt;
-- Events: Anzeigenamen, Labels, Infobox-Texte, Links, Einheiten&lt;br /&gt;
M.events = {&lt;br /&gt;
  -- Anzeigenamen je Event-Key&lt;br /&gt;
  names = {&lt;br /&gt;
    halloween = &amp;quot;Halloween-Event&amp;quot;,&lt;br /&gt;
    -- christmas = &amp;quot;Weihnachts-Event&amp;quot;, ...&lt;br /&gt;
    -- valentin = &amp;quot;Valentins-Event&amp;quot;, ...&lt;br /&gt;
  },&lt;br /&gt;
&lt;br /&gt;
  group_labels = {&lt;br /&gt;
    level  = &amp;quot;Charakterlevel&amp;quot;,&lt;br /&gt;
    stars  = &amp;quot;Kampagnensterne&amp;quot;,&lt;br /&gt;
    oracle = &amp;quot;Orakellevel&amp;quot;,&lt;br /&gt;
  },&lt;br /&gt;
&lt;br /&gt;
  unlocks = {&lt;br /&gt;
    character_level_10 = &amp;quot;Level 10&amp;quot;,&lt;br /&gt;
    -- optional: character_level_20 = &amp;quot;Level 20&amp;quot;, usw.&lt;br /&gt;
  },&lt;br /&gt;
&lt;br /&gt;
  phrases = {&lt;br /&gt;
    alt_platform = &amp;quot;auf Google Play&amp;quot;,&lt;br /&gt;
    req_gear_power = &amp;quot;Setzt außerdem $1 Ausrüstungsmacht voraus.&amp;quot;,&lt;br /&gt;
    event_notice = &amp;quot;Das $1 findet vom $2 bis $3 statt.&amp;quot;,&lt;br /&gt;
    event_ended  = &amp;quot;Das $1 ist beendet. Du kannst verbleibende $4 noch im $5 eintauschen.&amp;quot;&lt;br /&gt;
  },&lt;br /&gt;
&lt;br /&gt;
  -- Labels/Bezeichner pro Event (override), plus Defaults&lt;br /&gt;
  sections = {&lt;br /&gt;
    default = {&lt;br /&gt;
      name = &amp;quot;Unbekanntes Event&amp;quot;,&lt;br /&gt;
      eventname = &amp;quot;Unbekanntes Event&amp;quot;,&lt;br /&gt;
      currencies = &amp;quot;Unbekannte Währungen&amp;quot;,&lt;br /&gt;
      currency   = &amp;quot;Unbekannte Währung&amp;quot;,&lt;br /&gt;
      ex_shop    = &amp;quot;Unbekannter Tauschshop&amp;quot;,&lt;br /&gt;
      currency_exchange = &amp;quot;Unbekannter Währungstausch&amp;quot;,&lt;br /&gt;
      avatar_exchange  = &amp;quot;Unbekannte Avatare&amp;quot;,&lt;br /&gt;
      shop       = &amp;quot;Unbekannter Eventshop&amp;quot;,&lt;br /&gt;
      fullname   = &amp;quot;Unbekannter voller Name&amp;quot;,&lt;br /&gt;
      shortname  = &amp;quot;Unbekannter Name&amp;quot;,&lt;br /&gt;
      deco       = &amp;quot;Unbekannte Deko in Irongard&amp;quot;,&lt;br /&gt;
      offer = &amp;quot;Angebot&amp;quot;,&lt;br /&gt;
      price = &amp;quot;Preis&amp;quot;,&lt;br /&gt;
      limit = &amp;quot;Limit&amp;quot;, &lt;br /&gt;
    },&lt;br /&gt;
    halloween = {&lt;br /&gt;
      name = &amp;quot;Halloween-Event&amp;quot;,&lt;br /&gt;
      eventname = &amp;quot;Halloween-Event (Süßes oder Saures)&amp;quot;,&lt;br /&gt;
      currencies = &amp;quot;Kürbisse&amp;quot;,&lt;br /&gt;
      currency   = &amp;quot;Kürbis&amp;quot;,&lt;br /&gt;
      ex_shop    = &amp;quot;Hexenhütte&amp;quot;,&lt;br /&gt;
      currency_exchange = &amp;quot;Kürbistausch&amp;quot;,&lt;br /&gt;
      avatar_exchange  = &amp;quot;Schauerliche Avatare&amp;quot;,&lt;br /&gt;
      shop       = &amp;quot;Kürbishop&amp;quot;,&lt;br /&gt;
      shopname   = &amp;quot;Kürbishop&amp;quot;,&lt;br /&gt;
      fullname   = &amp;quot;Maxwell, der Kürbiskopf&amp;quot;,&lt;br /&gt;
      shortname  = &amp;quot;Maxwell&amp;quot;,&lt;br /&gt;
      deco       = &amp;quot;Kürbisse-Deko in Irongard&amp;quot;,&lt;br /&gt;
    },&lt;br /&gt;
  },&lt;br /&gt;
&lt;br /&gt;
  -- Infobox-Labels/Werte-Übersetzungen&lt;br /&gt;
  infobox = {&lt;br /&gt;
    type = {&lt;br /&gt;
      calendar_event = &amp;quot;Kalenderevent&amp;quot;,&lt;br /&gt;
    },&lt;br /&gt;
    -- Format für Freischaltung aus &amp;quot;character_level_10&amp;quot;&lt;br /&gt;
    unlocks_format = &amp;quot;Level $1&amp;quot;,&lt;br /&gt;
  },&lt;br /&gt;
&lt;br /&gt;
  -- Links (Zielseiten) für bestimmte Typen&lt;br /&gt;
  links = {&lt;br /&gt;
    type = {&lt;br /&gt;
      calendar_event = &amp;quot;Calendar Events&amp;quot;, -- Zielseite im Wiki&lt;br /&gt;
    }&lt;br /&gt;
  },&lt;br /&gt;
&lt;br /&gt;
  -- Einheiten/Formatierungen&lt;br /&gt;
  units = {&lt;br /&gt;
    week_singular = &amp;quot;Woche&amp;quot;,&lt;br /&gt;
    week_plural   = &amp;quot;Wochen&amp;quot;,&lt;br /&gt;
  },&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
M.numbers = {&lt;br /&gt;
  thousand_sing = &amp;quot;Tausend&amp;quot;,  thousand_plur = &amp;quot;Tausend&amp;quot;,&lt;br /&gt;
  million_sing  = &amp;quot;Million&amp;quot;,  million_plur  = &amp;quot;Millionen&amp;quot;,&lt;br /&gt;
  billion_sing  = &amp;quot;Milliarde&amp;quot;,billion_plur  = &amp;quot;Milliarden&amp;quot;,&lt;br /&gt;
  trillion_sing = &amp;quot;Billion&amp;quot;,  trillion_plur = &amp;quot;Billionen&amp;quot;,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
M.items = {&lt;br /&gt;
  rare_chest      = { sing=&amp;quot;Seltene Truhe&amp;quot;,      plur=&amp;quot;Seltene Truhen&amp;quot;,      link=&amp;quot;Rare Chest&amp;quot; },&lt;br /&gt;
  epic_chest      = { sing=&amp;quot;Epische Truhe&amp;quot;,      plur=&amp;quot;Epische Truhen&amp;quot;,      link=&amp;quot;Epic Chest&amp;quot; },&lt;br /&gt;
  legendary_chest = { sing=&amp;quot;Legendäre Truhe&amp;quot;,    plur=&amp;quot;Legendäre Truhen&amp;quot;,    link=&amp;quot;Legendary Chest&amp;quot; },&lt;br /&gt;
  mythic_chest    = { sing=&amp;quot;Mystische Truhe&amp;quot;,    plur=&amp;quot;Mystische Truhen&amp;quot;,    link=&amp;quot;Mythic Chest&amp;quot; },&lt;br /&gt;
&lt;br /&gt;
  golden_chest    = { sing=&amp;quot;Goldene Truhe&amp;quot;,      plur=&amp;quot;Goldene Truhen&amp;quot;,      link=&amp;quot;Golden Chest&amp;quot; },&lt;br /&gt;
  diamond_chest   = { sing=&amp;quot;Diamanttruhe&amp;quot;,       plur=&amp;quot;Diamanttruhen&amp;quot;,       link=&amp;quot;Diamond Chest&amp;quot; },&lt;br /&gt;
  opal_chest      = { sing=&amp;quot;Opaltruhe&amp;quot;,          plur=&amp;quot;Opaltruhen&amp;quot;,          link=&amp;quot;Opal Chest&amp;quot; },&lt;br /&gt;
  emerald_chest   = { sing=&amp;quot;Smaragdtruhe&amp;quot;,       plur=&amp;quot;Smaragdtruhen&amp;quot;,       link=&amp;quot;Emerald Chest&amp;quot; },&lt;br /&gt;
&lt;br /&gt;
  solar_chest     = { sing=&amp;quot;Solartruhe&amp;quot;,         plur=&amp;quot;Solartruhen&amp;quot;,         link=&amp;quot;Solar Chest&amp;quot; },&lt;br /&gt;
  nebula_chest    = { sing=&amp;quot;Nebeltruhe&amp;quot;,         plur=&amp;quot;Nebeltruhen&amp;quot;,         link=&amp;quot;Nebula Chest&amp;quot; },&lt;br /&gt;
  cosmic_chest    = { sing=&amp;quot;Kosmische Truhe&amp;quot;,    plur=&amp;quot;Kosmische Truhen&amp;quot;,    link=&amp;quot;Cosmic Chest&amp;quot; },&lt;br /&gt;
  galaxy_chest    = { sing=&amp;quot;Galaxietruhe&amp;quot;,       plur=&amp;quot;Galaxietruhen&amp;quot;,       link=&amp;quot;Galaxy Chest&amp;quot; },&lt;br /&gt;
&lt;br /&gt;
  exotic_coins    = { sing=&amp;quot;Exotische Münze&amp;quot;,    plur=&amp;quot;Exotische Münzen&amp;quot;,    link=&amp;quot;Exotic Coins&amp;quot; },&lt;br /&gt;
  pickaxe         = { sing=&amp;quot;Spitzhacke&amp;quot;,         plur=&amp;quot;Spitzhacken&amp;quot;,         link=&amp;quot;pickaxes&amp;quot; },&lt;br /&gt;
  strange_dust    = { sing=&amp;quot;Seltsamer Staub&amp;quot;,    plur=&amp;quot;Seltener Staub&amp;quot;,      link=&amp;quot;Strange Dust&amp;quot; },&lt;br /&gt;
  honor           = { sing=&amp;quot;Ehre&amp;quot;,               plur=&amp;quot;Ehre&amp;quot;,                link=&amp;quot;honor&amp;quot; },&lt;br /&gt;
  beer            = { sing=&amp;quot;Bier&amp;quot;,               plur=&amp;quot;Biere&amp;quot;,               link=&amp;quot;beer&amp;quot; },&lt;br /&gt;
  meteorite       = { sing=&amp;quot;Meteorit&amp;quot;,           plur=&amp;quot;Meteoriten&amp;quot;,          link=&amp;quot;meteorites&amp;quot; },&lt;br /&gt;
  golden_key      = { sing=&amp;quot;Goldener Schlüssel&amp;quot;, plur=&amp;quot;Goldene Schlüssel&amp;quot;,   link=&amp;quot;Golden Keys&amp;quot; },&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
M.ui = {&lt;br /&gt;
  default_skin = &amp;quot;Standard&amp;quot;,&lt;br /&gt;
&lt;br /&gt;
  headers = {&lt;br /&gt;
    main  = &amp;quot;Hauptattribute&amp;quot;,&lt;br /&gt;
    adv   = &amp;quot;Fortschrittsattribute&amp;quot;,&lt;br /&gt;
    chaos = &amp;quot;Chaos-Attribute&amp;quot;,&lt;br /&gt;
  },&lt;br /&gt;
&lt;br /&gt;
  basics = {&lt;br /&gt;
    class          = &amp;quot;Klasse:&amp;quot;,&lt;br /&gt;
    specialization = &amp;quot;Spezialisierung:&amp;quot;,&lt;br /&gt;
    attackstyle    = &amp;quot;Angriffsart:&amp;quot;,&lt;br /&gt;
    resource       = &amp;quot;Ressource:&amp;quot;,&lt;br /&gt;
  },&lt;br /&gt;
&lt;br /&gt;
  basics_override = {&lt;br /&gt;
    guardian = { class = &amp;quot;Typ:&amp;quot; },&lt;br /&gt;
  },&lt;br /&gt;
&lt;br /&gt;
  labels = {&lt;br /&gt;
    default  = { awakening = &amp;quot;Erweckungsbonus&amp;quot; },&lt;br /&gt;
    guardian = { awakening = &amp;quot;Wächteraura&amp;quot; },&lt;br /&gt;
    wm       = { awakening = &amp;quot;Meilenstein-Effekt&amp;quot;, ability = &amp;quot;Fähigkeit&amp;quot; },&lt;br /&gt;
  },&lt;br /&gt;
&lt;br /&gt;
  unlock = {&lt;br /&gt;
    at_stage    = &amp;quot;Freischaltung bei Abschnitt: $1&amp;quot;,&lt;br /&gt;
    pirate_ship = &amp;quot;Wird auf dem Piratenschiff freigeschaltet.&amp;quot;,&lt;br /&gt;
  },&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
M.i18n = {&lt;br /&gt;
  class = { warrior=&amp;quot;Krieger&amp;quot;, paladin=&amp;quot;Paladin&amp;quot;, rouge=&amp;quot;Schurke&amp;quot;,&lt;br /&gt;
            mage=&amp;quot;Magier&amp;quot;, druid=&amp;quot;Druide&amp;quot;, ranger=&amp;quot;Waldläufer&amp;quot; },&lt;br /&gt;
  attackstyle = { melee=&amp;quot;Nahkampf&amp;quot;, ranged=&amp;quot;Fernkampf&amp;quot;, spellcaster=&amp;quot;Zauberwirker&amp;quot; },&lt;br /&gt;
  specialization = { healer=&amp;quot;Heiler&amp;quot;, damage=&amp;quot;Schaden&amp;quot;, tank=&amp;quot;Tank&amp;quot; },&lt;br /&gt;
  resource = { mana=&amp;quot;Mana&amp;quot;, rage=&amp;quot;Wut&amp;quot;, energy=&amp;quot;Energie&amp;quot; },&lt;br /&gt;
&lt;br /&gt;
  awakening_bonus = {&lt;br /&gt;
    [&amp;quot;raining gold&amp;quot;]        = &amp;quot;Goldregen&amp;quot;,&lt;br /&gt;
    [&amp;quot;all main attributes&amp;quot;] = &amp;quot;Alle Hauptattribute&amp;quot;,&lt;br /&gt;
    prestigious             = &amp;quot;Prestigevoll&amp;quot;,&lt;br /&gt;
    redemption              = &amp;quot;Redemption&amp;quot;,&lt;br /&gt;
  },&lt;br /&gt;
&lt;br /&gt;
  month = {&lt;br /&gt;
    january=&amp;quot;Januar&amp;quot;, february=&amp;quot;Februar&amp;quot;, march=&amp;quot;März&amp;quot;, april=&amp;quot;April&amp;quot;, may=&amp;quot;Mai&amp;quot;, june=&amp;quot;Juni&amp;quot;,&lt;br /&gt;
    july=&amp;quot;Juli&amp;quot;, august=&amp;quot;August&amp;quot;, september=&amp;quot;September&amp;quot;, october=&amp;quot;Oktober&amp;quot;, november=&amp;quot;November&amp;quot;, december=&amp;quot;Dezember&amp;quot;,&lt;br /&gt;
  },&lt;br /&gt;
  duration = {&lt;br /&gt;
    [&amp;quot;2 weeks&amp;quot;]=&amp;quot;2 Wochen&amp;quot;&lt;br /&gt;
  },&lt;br /&gt;
  currency = {&lt;br /&gt;
    gold=&amp;quot;Gold&amp;quot;, gems=&amp;quot;Edelsteine&amp;quot;, energy=&amp;quot;Energie&amp;quot;, pumpkin=&amp;quot;Kürbis&amp;quot;, gift=&amp;quot;Geschenk&amp;quot;,&lt;br /&gt;
  },&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
M.aw_desc = {&lt;br /&gt;
  [&amp;quot;raining gold&amp;quot;]        = &amp;quot;Erhöht deinen Goldgewinn aus Kämpfen und Belohnungen.&amp;quot;,&lt;br /&gt;
  [&amp;quot;all main attributes&amp;quot;] = &amp;quot;Erhöht dauerhaft Schaden, Gesundheit und Rüstung.&amp;quot;,&lt;br /&gt;
  prestigious             = &amp;quot;Verbessert Prestige-Belohnungen und Effizienz beim Prestigen.&amp;quot;,&lt;br /&gt;
  redemption              = &amp;quot;Erhöht deinen Goldgewinn aus Kämpfen und Belohnungen.&amp;quot;,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
return M&lt;/div&gt;</summary>
		<author><name>Webmaster</name></author>
	</entry>
	<entry>
		<id>https://firestone-idle-rpg.de/index.php?title=Modul:EventData&amp;diff=3745</id>
		<title>Modul:EventData</title>
		<link rel="alternate" type="text/html" href="https://firestone-idle-rpg.de/index.php?title=Modul:EventData&amp;diff=3745"/>
		<updated>2025-10-28T00:49:58Z</updated>

		<summary type="html">&lt;p&gt;Webmaster: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;-- Modul:EventData&lt;br /&gt;
local E = {}&lt;br /&gt;
&lt;br /&gt;
E.events = {&lt;br /&gt;
  halloween = {&lt;br /&gt;
    banner   = &amp;quot;EventHalloween.png&amp;quot;,&lt;br /&gt;
    type     = &amp;quot;calendar_event&amp;quot;,&lt;br /&gt;
    page = &amp;quot;Halloween_Event&amp;quot;,&lt;br /&gt;
    month    = &amp;quot;October&amp;quot;,          -- wird per i18n → „Oktober“&lt;br /&gt;
    start_date    = &amp;quot;2025-10-15&amp;quot;,&lt;br /&gt;
    duration = &amp;quot;2 weeks&amp;quot;,          -- i18n → „2 Wochen“&lt;br /&gt;
&lt;br /&gt;
    -- Bilder, die du in der Vorlage über {{Firestone|Event|NAME|file|…}} benutzt&lt;br /&gt;
    images = {&lt;br /&gt;
      deco     = &amp;quot;HalloweenDecorations.png&amp;quot;,&lt;br /&gt;
      fullname = &amp;quot;Maxwell.png&amp;quot;,&lt;br /&gt;
      ex_shop  = &amp;quot;WitchsHut.png&amp;quot;,&lt;br /&gt;
    },&lt;br /&gt;
&lt;br /&gt;
    -- Event-&amp;quot;Währung&amp;quot;&lt;br /&gt;
    currency = { key=&amp;quot;pumpkin&amp;quot;, icon=&amp;quot;CurrencyPumpkin.png&amp;quot; },&lt;br /&gt;
&lt;br /&gt;
    -- Shop-Angebote&lt;br /&gt;
    shop = {&lt;br /&gt;
      initial = {&lt;br /&gt;
        { name=&amp;quot;Group of Pumpkins&amp;quot;,   image=&amp;quot;PumpkinGroup.png&amp;quot;,   token=2700  },&lt;br /&gt;
        { name=&amp;quot;Hat of Pumpkins&amp;quot;,     image=&amp;quot;PumpkinHat.png&amp;quot;,     token=6000  },&lt;br /&gt;
        { name=&amp;quot;Bucket of Pumpkins&amp;quot;,  image=&amp;quot;PumpkinBucket.png&amp;quot;,  token=16500 },&lt;br /&gt;
      },&lt;br /&gt;
      after = {&lt;br /&gt;
        { name=&amp;quot;Cart of Pumpkins&amp;quot;,     image=&amp;quot;PumpkinBarrow.png&amp;quot;,   token=34500  },&lt;br /&gt;
        { name=&amp;quot;Cauldron of Pumpkins&amp;quot;, image=&amp;quot;PumpkinCauldron.png&amp;quot;, token=72000  },&lt;br /&gt;
        { name=&amp;quot;Coffin of Pumpkins&amp;quot;,   image=&amp;quot;PumpkinCoffin.png&amp;quot;,   token=187500 },&lt;br /&gt;
        { name=&amp;quot;Carriage of Pumpkins&amp;quot;, image=&amp;quot;PumpkinCoach.png&amp;quot;,    token=390000 },&lt;br /&gt;
      },&lt;br /&gt;
    },&lt;br /&gt;
&lt;br /&gt;
    -- Austausch (verkürzte, saubere Struktur – du kannst jederzeit erweitern)&lt;br /&gt;
    exchange = {&lt;br /&gt;
      chests_by_level = {&lt;br /&gt;
        { range=&amp;quot;1-64&amp;quot;,    icon=&amp;quot;RareChest.png&amp;quot;,      item=&amp;quot;rare_chest&amp;quot;,      amount=1, price=2500, limit=25 },&lt;br /&gt;
        { range=&amp;quot;65-129&amp;quot;,  icon=&amp;quot;EpicChest.png&amp;quot;,      item=&amp;quot;epic_chest&amp;quot;,      amount=1, price=2500, limit=25 },&lt;br /&gt;
        { range=&amp;quot;130-299&amp;quot;, icon=&amp;quot;LegendaryChest.png&amp;quot;, item=&amp;quot;legendary_chest&amp;quot;, amount=1, price=2500, limit=25 },&lt;br /&gt;
        { range=&amp;quot;300-&amp;quot;,    icon=&amp;quot;MythicChest.png&amp;quot;,    item=&amp;quot;mythic_chest&amp;quot;,    amount=1, price=2500, limit=25, req_gear_power = &amp;quot;5e9&amp;quot; },&lt;br /&gt;
      },&lt;br /&gt;
      chests_by_stars = {&lt;br /&gt;
        { range=&amp;quot;1-99&amp;quot;,    icon=&amp;quot;GoldChest.png&amp;quot;,     item=&amp;quot;golden_chest&amp;quot;,   amount=1, price=2500, limit=25 },&lt;br /&gt;
        { range=&amp;quot;100-144&amp;quot;, icon=&amp;quot;DiamondChest.png&amp;quot;,  item=&amp;quot;diamond_chest&amp;quot;,  amount=1, price=2500, limit=25 },&lt;br /&gt;
        { range=&amp;quot;145-189&amp;quot;, icon=&amp;quot;OpalChest.png&amp;quot;,     item=&amp;quot;opal_chest&amp;quot;,     amount=1, price=2500, limit=25 },&lt;br /&gt;
        { range=&amp;quot;190-&amp;quot;,    icon=&amp;quot;EmeraldChest.png&amp;quot;,  item=&amp;quot;emerald_chest&amp;quot;,  amount=1, price=2500, limit=25 },&lt;br /&gt;
      },&lt;br /&gt;
      chests_by_oracle = {&lt;br /&gt;
        { range=&amp;quot;1-97&amp;quot;,    icon=&amp;quot;SolarChest.png&amp;quot;,    item=&amp;quot;solar_chest&amp;quot;,    amount=1, price=2500, limit=25 },&lt;br /&gt;
        { range=&amp;quot;98-148&amp;quot;,  icon=&amp;quot;NebulaChest.png&amp;quot;,   item=&amp;quot;nebula_chest&amp;quot;,   amount=1, price=2500, limit=25 },&lt;br /&gt;
        { range=&amp;quot;149-200&amp;quot;, icon=&amp;quot;CosmicChest.png&amp;quot;,   item=&amp;quot;cosmic_chest&amp;quot;,   amount=1, price=2500, limit=25 },&lt;br /&gt;
        { range=&amp;quot;201-&amp;quot;,    icon=&amp;quot;GalaxyChest.png&amp;quot;,   item=&amp;quot;galaxy_chest&amp;quot;,   amount=1, price=2500, limit=25 },&lt;br /&gt;
      },&lt;br /&gt;
      currencies = {&lt;br /&gt;
        { icon=&amp;quot;CurrencyExoticCoin.png&amp;quot;, item=&amp;quot;exotic_coins&amp;quot;,  amount=500,  price=1250, limit=50  },&lt;br /&gt;
        { icon=&amp;quot;CurrencyPickaxe.png&amp;quot;,    item=&amp;quot;pickaxe&amp;quot;,       amount=1,    price=375,  limit=70  },&lt;br /&gt;
        { icon=&amp;quot;CurrencyStrangeDust.png&amp;quot;,item=&amp;quot;strange_dust&amp;quot;,  amount=20,   price=500,  limit=300 },&lt;br /&gt;
        { icon=&amp;quot;CurrencyHonor.png&amp;quot;,      item=&amp;quot;honor&amp;quot;,         amount=5,    price=1500, limit=40  },&lt;br /&gt;
        { icon=&amp;quot;CurrencyBeer.png&amp;quot;,       item=&amp;quot;beer&amp;quot;,          amount=500,  price=1250, limit=150 },&lt;br /&gt;
        { icon=&amp;quot;CurrencyMeteorite.png&amp;quot;,  item=&amp;quot;meteorite&amp;quot;,     amount=150,  price=1500, limit=50  },&lt;br /&gt;
        { icon=&amp;quot;CurrencyGoldenKeys.png&amp;quot;, item=&amp;quot;golden_key&amp;quot;,    amount=1,    price=3000, limit=25  },&lt;br /&gt;
      },&lt;br /&gt;
    },&lt;br /&gt;
&lt;br /&gt;
    avatars = {&lt;br /&gt;
      [2022] = {&lt;br /&gt;
        { file=&amp;quot;AvatarMaxwell.png&amp;quot;,  title=&amp;quot;Maxwell, the Pumpkin Head&amp;quot;, cost=21000 },&lt;br /&gt;
        { file=&amp;quot;AvatarBilly.png&amp;quot;,    title=&amp;quot;Billy, the Happy Ghost&amp;quot;,    cost=21000 },&lt;br /&gt;
        { file=&amp;quot;AvatarYvonne.png&amp;quot;,   title=&amp;quot;Yvonne, The Trickster&amp;quot;,     cost=21000 },&lt;br /&gt;
        { file=&amp;quot;AvatarLucien.png&amp;quot;,   title=&amp;quot;Lucien, The Vampire&amp;quot;,       cost=21000 },&lt;br /&gt;
      },&lt;br /&gt;
      [2023] = {&lt;br /&gt;
        { file=&amp;quot;AvatarMaxwell.png&amp;quot;,  title=&amp;quot;Maxwell, the Pumpkin Head&amp;quot;, cost=21000 },&lt;br /&gt;
        { file=&amp;quot;AvatarEmberlyn.png&amp;quot;, title=&amp;quot;Emberlyn, the Bowian Witch&amp;quot;,cost=21000 },&lt;br /&gt;
        { file=&amp;quot;AvatarSpindle.png&amp;quot;,  title=&amp;quot;Spindle, the Crafty Weaver&amp;quot;,cost=21000 },&lt;br /&gt;
        { file=&amp;quot;AvatarTrashy.png&amp;quot;,   title=&amp;quot;Trashy, the Wacky Bin&amp;quot;,     cost=21000 },&lt;br /&gt;
      },&lt;br /&gt;
      [2024] = {&lt;br /&gt;
        { file=&amp;quot;AvatarMaxwell.png&amp;quot;,  title=&amp;quot;Maxwell, the Pumpkin Head&amp;quot;, cost=21000 },&lt;br /&gt;
        { file=&amp;quot;AvatarCatrina.png&amp;quot;,  title=&amp;quot;Catrina, the Undying Beauty&amp;quot;, cost=21000 },&lt;br /&gt;
        { file=&amp;quot;AvatarBaron.png&amp;quot;,    title=&amp;quot;Baron, the Voodoo Master&amp;quot;,    cost=21000 },&lt;br /&gt;
        { file=&amp;quot;AvatarSonar.png&amp;quot;,    title=&amp;quot;Sonar, the Nocturnal Messenger&amp;quot;, cost=21000 },&lt;br /&gt;
      },&lt;br /&gt;
    },&lt;br /&gt;
&lt;br /&gt;
    history = {&lt;br /&gt;
      { year=2022, start=&amp;quot;2022-10-24&amp;quot;, [&amp;quot;end&amp;quot;]=&amp;quot;2022-11-07&amp;quot; },&lt;br /&gt;
      { year=2023, start=&amp;quot;2023-10-20&amp;quot;, [&amp;quot;end&amp;quot;]=&amp;quot;2023-11-03&amp;quot;, alt_platform=&amp;quot;2023-10-24 bis 2023-11-07&amp;quot; },&lt;br /&gt;
      { year=2024, start=&amp;quot;2024-10-25&amp;quot;, [&amp;quot;end&amp;quot;]=&amp;quot;2024-11-08&amp;quot; },&lt;br /&gt;
    },&lt;br /&gt;
  },&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
return E&lt;/div&gt;</summary>
		<author><name>Webmaster</name></author>
	</entry>
	<entry>
		<id>https://firestone-idle-rpg.de/index.php?title=Modul:Link/Doku&amp;diff=3744</id>
		<title>Modul:Link/Doku</title>
		<link rel="alternate" type="text/html" href="https://firestone-idle-rpg.de/index.php?title=Modul:Link/Doku&amp;diff=3744"/>
		<updated>2025-10-28T00:28:18Z</updated>

		<summary type="html">&lt;p&gt;Webmaster: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Erzeugt interne Links mit Hover-Tooltip (Icon, Titel, Kurzbeschreibung) aus &#039;&#039;&#039;[[Modul:LinkData]]&#039;&#039;&#039;.  &lt;br /&gt;
Optional kann eine „Detailansicht“ (Wikitable) ausgegeben werden.&lt;br /&gt;
&lt;br /&gt;
== Grundsyntax ==&lt;br /&gt;
&lt;br /&gt;
; Tooltip-Link&lt;br /&gt;
: &amp;lt;code&amp;gt;&amp;lt;nowiki&amp;gt;{{Link|Ziel}}&amp;lt;/nowiki&amp;gt;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
; Tooltip-Link mit eigenem Anzeigenamen&lt;br /&gt;
: &amp;lt;code&amp;gt;&amp;lt;nowiki&amp;gt;{{Link|Ziel|Anzeigename}}&amp;lt;/nowiki&amp;gt;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
; Detailansicht (Tabelle) statt Link&lt;br /&gt;
: &amp;lt;code&amp;gt;&amp;lt;nowiki&amp;gt;{{Link|Ziel|view}}&amp;lt;/nowiki&amp;gt;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Parameter ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Param !! Name !! Typ/Standard !! Beschreibung&lt;br /&gt;
|-&lt;br /&gt;
| 1 || Ziel || Pflicht || Seitentitel oder „Titel#Abschnitt“. Wird als Linkziel verwendet.&lt;br /&gt;
|-&lt;br /&gt;
| 2 || Anzeigename / &amp;lt;code&amp;gt;view&amp;lt;/code&amp;gt; / Kurz-Suffix || optional ||&lt;br /&gt;
* leer → Anzeigename = &amp;lt;code&amp;gt;title&amp;lt;/code&amp;gt; aus LinkData&lt;br /&gt;
* &amp;lt;code&amp;gt;view&amp;lt;/code&amp;gt; → gibt die Tabelle aus&lt;br /&gt;
* &amp;lt;code&amp;gt;+…&amp;lt;/code&amp;gt; → Kurz-Suffix für Plural etc. (siehe unten)&lt;br /&gt;
|-&lt;br /&gt;
| s || &amp;lt;code&amp;gt;s&amp;lt;/code&amp;gt; (Suffix) || optional || Hängt Text an den Anzeigenamen aus LinkData an (z. B. &amp;lt;code&amp;gt;s=e&amp;lt;/code&amp;gt; → „Biere“).&lt;br /&gt;
|-&lt;br /&gt;
| key || &amp;lt;code&amp;gt;key&amp;lt;/code&amp;gt; || optional || Erzwingt den Datensatz-Schlüssel für LinkData (falls Ziel und &amp;lt;code&amp;gt;title&amp;lt;/code&amp;gt; nicht übereinstimmen).&lt;br /&gt;
|-&lt;br /&gt;
| limit || &amp;lt;code&amp;gt;limit&amp;lt;/code&amp;gt; || Zahl (220) || Maximale Länge der Tooltip-Beschreibung; bei Überschreitung wird abgeschnitten (…).&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Verhalten ==&lt;br /&gt;
&lt;br /&gt;
=== Anzeigename ===&lt;br /&gt;
* Standardmäßig wird als Anzeigename der &#039;&#039;&#039;deutsche Titel&#039;&#039;&#039; aus [[Modul:LinkData]] verwendet (Feld &amp;lt;code&amp;gt;title&amp;lt;/code&amp;gt;).&lt;br /&gt;
* Eigener Anzeigename (2. Parameter) überschreibt den Titel.&lt;br /&gt;
* Groß-/Kleinschreibung bleibt &#039;&#039;&#039;genau&#039;&#039;&#039; wie im Wikitext/LinkData.&lt;br /&gt;
&lt;br /&gt;
=== Plural / LinkTrail ===&lt;br /&gt;
MediaWikis LinkTrail funktioniert nur bei &#039;&#039;&#039;direkt geschriebenen&#039;&#039;&#039; Links, nicht bei Vorlagen. Dafür gibt es zwei Wege:&lt;br /&gt;
&lt;br /&gt;
# &#039;&#039;&#039;Suffix-Kurzform&#039;&#039;&#039; im 2. Parameter:  &lt;br /&gt;
: &amp;lt;code&amp;gt;&amp;lt;nowiki&amp;gt;{{Link|Bier|+e}}&amp;lt;/nowiki&amp;gt;&amp;lt;/code&amp;gt; → &amp;lt;code&amp;gt;{{Link|Bier|+e}}&amp;lt;/code&amp;gt;  &lt;br /&gt;
: &amp;lt;code&amp;gt;&amp;lt;nowiki&amp;gt;{{Link|Bier|+en}}&amp;lt;/nowiki&amp;gt;&amp;lt;/code&amp;gt; → &amp;lt;code&amp;gt;{{Link|Bier|+en}}&amp;lt;/code&amp;gt;&lt;br /&gt;
# &#039;&#039;&#039;Expliziter Suffix&#039;&#039;&#039; über &amp;lt;code&amp;gt;s=&amp;lt;/code&amp;gt;:  &lt;br /&gt;
: &amp;lt;code&amp;gt;&amp;lt;nowiki&amp;gt;{{Link|Bier|s=e}}&amp;lt;/nowiki&amp;gt;&amp;lt;/code&amp;gt; → &amp;lt;code&amp;gt;{{Link|Bier|s=e}}&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Hinweise:&#039;&#039;&#039;&lt;br /&gt;
* Sobald ein eigener Anzeigename gesetzt ist, wird &amp;lt;code&amp;gt;s=&amp;lt;/code&amp;gt; ignoriert.&lt;br /&gt;
* Bei „unpiped“ Links (also wirklich &amp;lt;code&amp;gt;[[Bier]]e&amp;lt;/code&amp;gt;) greift LinkTrail – das ist mit Vorlagen technisch &#039;&#039;&#039;nicht&#039;&#039;&#039; möglich. Nutze daher &amp;lt;code&amp;gt;s=&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;+…&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Anker/Abschnitte ===&lt;br /&gt;
* Bei Zielen mit &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; (z. B. &amp;lt;code&amp;gt;Währungen#Bier&amp;lt;/code&amp;gt;) wird &#039;&#039;&#039;immer&#039;&#039;&#039; ein gepipter Link erzeugt (kein LinkTrail / ToolTip möglich).&lt;br /&gt;
* Die Detailansicht (&amp;lt;code&amp;gt;view&amp;lt;/code&amp;gt;) setzt die Tabellen-&amp;lt;code&amp;gt;ID&amp;lt;/code&amp;gt; anhand des Titels (Leerzeichen → Unterstrich).&lt;br /&gt;
&lt;br /&gt;
=== Tooltip ===&lt;br /&gt;
* Holt &amp;lt;code&amp;gt;title&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;icon&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;desc&amp;lt;/code&amp;gt; aus [[Modul:LinkData]].&lt;br /&gt;
* Links in der Beschreibung werden im Hover-Tooltip zu &#039;&#039;&#039;normalem Text&#039;&#039;&#039; neutralisiert.&lt;br /&gt;
* Zeilenumbrüche werden unterstützt; lange Texte werden nach &amp;lt;code&amp;gt;limit&amp;lt;/code&amp;gt; Zeichen abgeschnitten.&lt;br /&gt;
&lt;br /&gt;
=== Fallbacks ===&lt;br /&gt;
* Gibt es keinen Datensatz in [[Modul:LinkData|LinkData]], wird ein normaler Link ohne Tooltip ausgegeben.&lt;br /&gt;
&lt;br /&gt;
== Beispiele ==&lt;br /&gt;
&lt;br /&gt;
; Standard&lt;br /&gt;
: &amp;lt;code&amp;gt;&amp;lt;nowiki&amp;gt;{{Link|Bier}}&amp;lt;/nowiki&amp;gt;&amp;lt;/code&amp;gt; → {{Link|Bier}}&lt;br /&gt;
&lt;br /&gt;
; Eigener Anzeigename&lt;br /&gt;
: &amp;lt;code&amp;gt;&amp;lt;nowiki&amp;gt;{{Link|Währungen#Bier|Bier}}&amp;lt;/nowiki&amp;gt;&amp;lt;/code&amp;gt; → {{Link|Währungen#Bier|Bier}}&lt;br /&gt;
&lt;br /&gt;
; Plural via Suffix (Kurzform)&lt;br /&gt;
: &amp;lt;code&amp;gt;&amp;lt;nowiki&amp;gt;{{Link|Bier|+e}}&amp;lt;/nowiki&amp;gt;&amp;lt;/code&amp;gt; → {{Link|Bier|+e}}&lt;br /&gt;
&lt;br /&gt;
; Plural via &amp;lt;code&amp;gt;s=&amp;lt;/code&amp;gt;&lt;br /&gt;
: &amp;lt;code&amp;gt;&amp;lt;nowiki&amp;gt;{{Link|Bier|s=en}}&amp;lt;/nowiki&amp;gt;&amp;lt;/code&amp;gt; → {{Link|Bier|s=en}}&lt;br /&gt;
&lt;br /&gt;
; Detailansicht (Wikitable)&lt;br /&gt;
: &amp;lt;code&amp;gt;&amp;lt;nowiki&amp;gt;{{Link|Bier|view}}&amp;lt;/nowiki&amp;gt;&amp;lt;/code&amp;gt; → {{Link|Bier|view}}&lt;br /&gt;
&lt;br /&gt;
; Limit anpassen&lt;br /&gt;
: &amp;lt;code&amp;gt;&amp;lt;nowiki&amp;gt;{{Link|Bier|limit=50}}&amp;lt;/nowiki&amp;gt;&amp;lt;/code&amp;gt; → {{Link|Bier|limit=50}}&lt;br /&gt;
&lt;br /&gt;
; Abweichender Datensatz (engl. key)&lt;br /&gt;
: &amp;lt;code&amp;gt;&amp;lt;nowiki&amp;gt;{{Link|Währungen|key=Beer}}&amp;lt;/nowiki&amp;gt;&amp;lt;/code&amp;gt; → {{Link|Währungen|key=Beer}}&lt;/div&gt;</summary>
		<author><name>Webmaster</name></author>
	</entry>
	<entry>
		<id>https://firestone-idle-rpg.de/index.php?title=Modul:Link/Doku&amp;diff=3743</id>
		<title>Modul:Link/Doku</title>
		<link rel="alternate" type="text/html" href="https://firestone-idle-rpg.de/index.php?title=Modul:Link/Doku&amp;diff=3743"/>
		<updated>2025-10-28T00:24:36Z</updated>

		<summary type="html">&lt;p&gt;Webmaster: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Erzeugt interne Links mit Hover-Tooltip (Icon, Titel, Kurzbeschreibung) aus &#039;&#039;&#039;[[Modul:LinkData]]&#039;&#039;&#039;.  &lt;br /&gt;
Optional kann eine „Detailansicht“ (Wikitable) ausgegeben werden.&lt;br /&gt;
&lt;br /&gt;
== Grundsyntax ==&lt;br /&gt;
&lt;br /&gt;
; Tooltip-Link&lt;br /&gt;
: &amp;lt;code&amp;gt;&amp;lt;nowiki&amp;gt;{{Link|Ziel}}&amp;lt;/nowiki&amp;gt;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
; Tooltip-Link mit eigenem Anzeigenamen&lt;br /&gt;
: &amp;lt;code&amp;gt;&amp;lt;nowiki&amp;gt;{{Link|Ziel|Anzeigename}}&amp;lt;/nowiki&amp;gt;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
; Detailansicht (Tabelle) statt Link&lt;br /&gt;
: &amp;lt;code&amp;gt;&amp;lt;nowiki&amp;gt;{{Link|Ziel|view}}&amp;lt;/nowiki&amp;gt;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Parameter ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Param !! Name !! Typ/Standard !! Beschreibung&lt;br /&gt;
|-&lt;br /&gt;
| 1 || Ziel || Pflicht || Seitentitel oder „Titel#Abschnitt“. Wird als Linkziel verwendet.&lt;br /&gt;
|-&lt;br /&gt;
| 2 || Anzeigename / &amp;lt;code&amp;gt;view&amp;lt;/code&amp;gt; / Kurz-Suffix || optional ||&lt;br /&gt;
* leer → Anzeigename = &amp;lt;code&amp;gt;title&amp;lt;/code&amp;gt; aus LinkData&lt;br /&gt;
* &amp;lt;code&amp;gt;view&amp;lt;/code&amp;gt; → gibt die Tabelle aus&lt;br /&gt;
* &amp;lt;code&amp;gt;+…&amp;lt;/code&amp;gt; → Kurz-Suffix für Plural etc. (siehe unten)&lt;br /&gt;
|-&lt;br /&gt;
| s || &amp;lt;code&amp;gt;s&amp;lt;/code&amp;gt; (Suffix) || optional || Hängt Text an den Anzeigenamen aus LinkData an (z. B. &amp;lt;code&amp;gt;s=e&amp;lt;/code&amp;gt; → „Biere“).&lt;br /&gt;
|-&lt;br /&gt;
| key || &amp;lt;code&amp;gt;key&amp;lt;/code&amp;gt; || optional || Erzwingt den Datensatz-Schlüssel für LinkData (falls Ziel und &amp;lt;code&amp;gt;title&amp;lt;/code&amp;gt; nicht übereinstimmen).&lt;br /&gt;
|-&lt;br /&gt;
| limit || &amp;lt;code&amp;gt;limit&amp;lt;/code&amp;gt; || Zahl (220) || Maximale Länge der Tooltip-Beschreibung; bei Überschreitung wird abgeschnitten (…).&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Verhalten ==&lt;br /&gt;
&lt;br /&gt;
=== Anzeigename ===&lt;br /&gt;
* Standardmäßig wird als Anzeigename der &#039;&#039;&#039;deutsche Titel&#039;&#039;&#039; aus [[Modul:LinkData]] verwendet (Feld &amp;lt;code&amp;gt;title&amp;lt;/code&amp;gt;).&lt;br /&gt;
* Eigener Anzeigename (2. Parameter) überschreibt den Titel.&lt;br /&gt;
* Groß-/Kleinschreibung bleibt &#039;&#039;&#039;genau&#039;&#039;&#039; wie im Wikitext/LinkData.&lt;br /&gt;
&lt;br /&gt;
=== Plural / LinkTrail ===&lt;br /&gt;
MediaWikis LinkTrail funktioniert nur bei &#039;&#039;&#039;direkt geschriebenen&#039;&#039;&#039; Links, nicht bei Vorlagen. Dafür gibt es zwei Wege:&lt;br /&gt;
&lt;br /&gt;
# &#039;&#039;&#039;Suffix-Kurzform&#039;&#039;&#039; im 2. Parameter:  &lt;br /&gt;
: &amp;lt;code&amp;gt;&amp;lt;nowiki&amp;gt;{{Link|Bier|+e}}&amp;lt;/nowiki&amp;gt;&amp;lt;/code&amp;gt; → &amp;lt;code&amp;gt;{{Link|Bier|+e}}&amp;lt;/code&amp;gt;  &lt;br /&gt;
: &amp;lt;code&amp;gt;&amp;lt;nowiki&amp;gt;{{Link|Bier|+en}}&amp;lt;/nowiki&amp;gt;&amp;lt;/code&amp;gt; → &amp;lt;code&amp;gt;{{Link|Bier|+en}}&amp;lt;/code&amp;gt;&lt;br /&gt;
# &#039;&#039;&#039;Expliziter Suffix&#039;&#039;&#039; über &amp;lt;code&amp;gt;s=&amp;lt;/code&amp;gt;:  &lt;br /&gt;
: &amp;lt;code&amp;gt;&amp;lt;nowiki&amp;gt;{{Link|Bier|s=e}}&amp;lt;/nowiki&amp;gt;&amp;lt;/code&amp;gt; → &amp;lt;code&amp;gt;{{Link|Bier|s=e}}&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Hinweise:&#039;&#039;&#039;&lt;br /&gt;
* Sobald ein eigener Anzeigename gesetzt ist, wird &amp;lt;code&amp;gt;s=&amp;lt;/code&amp;gt; ignoriert.&lt;br /&gt;
* Bei „unpiped“ Links (also wirklich &amp;lt;code&amp;gt;[[Bier]]e&amp;lt;/code&amp;gt;) greift LinkTrail – das ist mit Vorlagen technisch &#039;&#039;&#039;nicht&#039;&#039;&#039; möglich. Nutze daher &amp;lt;code&amp;gt;s=&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;+…&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Anker/Abschnitte ===&lt;br /&gt;
* Bei Zielen mit &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; (z. B. &amp;lt;code&amp;gt;Währungen#Bier&amp;lt;/code&amp;gt;) wird &#039;&#039;&#039;immer&#039;&#039;&#039; ein gepipter Link erzeugt (kein LinkTrail / ToolTip möglich).&lt;br /&gt;
* Die Detailansicht (&amp;lt;code&amp;gt;view&amp;lt;/code&amp;gt;) setzt die Tabellen-&amp;lt;code&amp;gt;ID&amp;lt;/code&amp;gt; anhand des Titels (Leerzeichen → Unterstrich).&lt;br /&gt;
&lt;br /&gt;
=== Tooltip ===&lt;br /&gt;
* Holt &amp;lt;code&amp;gt;title&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;icon&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;desc&amp;lt;/code&amp;gt; aus [[Modul:LinkData]].&lt;br /&gt;
* Links in der Beschreibung werden im Hover-Tooltip zu &#039;&#039;&#039;normalem Text&#039;&#039;&#039; neutralisiert.&lt;br /&gt;
* Zeilenumbrüche werden unterstützt; lange Texte werden nach &amp;lt;code&amp;gt;limit&amp;lt;/code&amp;gt; Zeichen abgeschnitten.&lt;br /&gt;
&lt;br /&gt;
=== Fallbacks ===&lt;br /&gt;
* Gibt es keinen Datensatz in [[Modul:LinkData|LinkData]], wird ein normaler Link ohne Tooltip ausgegeben.&lt;br /&gt;
&lt;br /&gt;
== Beispiele ==&lt;br /&gt;
&lt;br /&gt;
; Standard&lt;br /&gt;
: &amp;lt;code&amp;gt;&amp;lt;nowiki&amp;gt;{{Link|Bier}}&amp;lt;/nowiki&amp;gt;&amp;lt;/code&amp;gt; → {{Link|Bier}}&lt;br /&gt;
&lt;br /&gt;
; Eigener Anzeigename&lt;br /&gt;
: &amp;lt;code&amp;gt;&amp;lt;nowiki&amp;gt;{{Link|Währungen#Bier|Bier}}&amp;lt;/nowiki&amp;gt;&amp;lt;/code&amp;gt; → {{Link|Währungen#Bier|Bier}}&lt;br /&gt;
&lt;br /&gt;
; Plural via Suffix (Kurzform)&lt;br /&gt;
: &amp;lt;code&amp;gt;&amp;lt;nowiki&amp;gt;{{Link|Bier|+e}}&amp;lt;/nowiki&amp;gt;&amp;lt;/code&amp;gt; → {{Link|Bier|+e}}&lt;br /&gt;
&lt;br /&gt;
; Plural via &amp;lt;code&amp;gt;s=&amp;lt;/code&amp;gt;&lt;br /&gt;
: &amp;lt;code&amp;gt;&amp;lt;nowiki&amp;gt;{{Link|Bier|s=en}}&amp;lt;/nowiki&amp;gt;&amp;lt;/code&amp;gt; → {{Link|Bier|s=en}}&lt;br /&gt;
&lt;br /&gt;
; Detailansicht (Wikitable)&lt;br /&gt;
: &amp;lt;code&amp;gt;&amp;lt;nowiki&amp;gt;{{Link|Bier|view}}&amp;lt;/nowiki&amp;gt;&amp;lt;/code&amp;gt; → {{Link|Bier|view}}&lt;br /&gt;
&lt;br /&gt;
; Limit anpassen&lt;br /&gt;
: &amp;lt;code&amp;gt;&amp;lt;nowiki&amp;gt;{{Link|Bier|limit=50}}&amp;lt;/nowiki&amp;gt;&amp;lt;/code&amp;gt; → {{Link|Bier|limit=50}}&lt;br /&gt;
&lt;br /&gt;
; Abweichender Datensatz (key)&lt;br /&gt;
: &amp;lt;code&amp;gt;&amp;lt;nowiki&amp;gt;{{Link|Währungen|key=Beer}}&amp;lt;/nowiki&amp;gt;&amp;lt;/code&amp;gt; → {{Link|Währungen|key=Beer}}&lt;/div&gt;</summary>
		<author><name>Webmaster</name></author>
	</entry>
	<entry>
		<id>https://firestone-idle-rpg.de/index.php?title=Modul:Link/Doku&amp;diff=3742</id>
		<title>Modul:Link/Doku</title>
		<link rel="alternate" type="text/html" href="https://firestone-idle-rpg.de/index.php?title=Modul:Link/Doku&amp;diff=3742"/>
		<updated>2025-10-27T10:11:15Z</updated>

		<summary type="html">&lt;p&gt;Webmaster: Die Seite wurde neu angelegt: „&amp;lt;noinclude&amp;gt;{{Dokumentation}}&amp;lt;/noinclude&amp;gt; = Modul:Link – Doku =  Erzeugt interne Links mit Hover-Tooltip (Icon, Titel, Kurzbeschreibung) aus &amp;#039;&amp;#039;&amp;#039;Modul:LinkData&amp;#039;&amp;#039;&amp;#039;.   Optional kann eine „Detailansicht“ (Wikitable) ausgegeben werden.  == Grundsyntax ==  ; Tooltip-Link : &amp;lt;code&amp;gt;&amp;lt;nowiki&amp;gt;{{Link|Ziel}}&amp;lt;/nowiki&amp;gt;&amp;lt;/code&amp;gt;  ; Tooltip-Link mit eigenem Anzeigenamen : &amp;lt;code&amp;gt;&amp;lt;nowiki&amp;gt;{{Link|Ziel|Anzeigename}}&amp;lt;/nowiki&amp;gt;&amp;lt;/code&amp;gt;  ; Detailansicht (Tabelle) statt Link :…“&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;noinclude&amp;gt;{{Dokumentation}}&amp;lt;/noinclude&amp;gt;&lt;br /&gt;
= Modul:Link – Doku =&lt;br /&gt;
&lt;br /&gt;
Erzeugt interne Links mit Hover-Tooltip (Icon, Titel, Kurzbeschreibung) aus &#039;&#039;&#039;[[Modul:LinkData]]&#039;&#039;&#039;.  &lt;br /&gt;
Optional kann eine „Detailansicht“ (Wikitable) ausgegeben werden.&lt;br /&gt;
&lt;br /&gt;
== Grundsyntax ==&lt;br /&gt;
&lt;br /&gt;
; Tooltip-Link&lt;br /&gt;
: &amp;lt;code&amp;gt;&amp;lt;nowiki&amp;gt;{{Link|Ziel}}&amp;lt;/nowiki&amp;gt;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
; Tooltip-Link mit eigenem Anzeigenamen&lt;br /&gt;
: &amp;lt;code&amp;gt;&amp;lt;nowiki&amp;gt;{{Link|Ziel|Anzeigename}}&amp;lt;/nowiki&amp;gt;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
; Detailansicht (Tabelle) statt Link&lt;br /&gt;
: &amp;lt;code&amp;gt;&amp;lt;nowiki&amp;gt;{{Link|Ziel|view}}&amp;lt;/nowiki&amp;gt;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Parameter ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Param !! Name !! Typ/Standard !! Beschreibung&lt;br /&gt;
|-&lt;br /&gt;
| 1 || Ziel || Pflicht || Seitentitel oder „Titel#Abschnitt“. Wird als Linkziel verwendet.&lt;br /&gt;
|-&lt;br /&gt;
| 2 || Anzeigename / &amp;lt;code&amp;gt;view&amp;lt;/code&amp;gt; / Kurz-Suffix || optional ||&lt;br /&gt;
* leer → Anzeigename = &amp;lt;code&amp;gt;title&amp;lt;/code&amp;gt; aus LinkData&lt;br /&gt;
* &amp;lt;code&amp;gt;view&amp;lt;/code&amp;gt; → gibt die Tabelle aus&lt;br /&gt;
* &amp;lt;code&amp;gt;+…&amp;lt;/code&amp;gt; → Kurz-Suffix für Plural etc. (siehe unten)&lt;br /&gt;
|-&lt;br /&gt;
| s || &amp;lt;code&amp;gt;s&amp;lt;/code&amp;gt; (Suffix) || optional || Hängt Text an den Anzeigenamen aus LinkData an (z. B. &amp;lt;code&amp;gt;s=e&amp;lt;/code&amp;gt; → „Biere“).&lt;br /&gt;
|-&lt;br /&gt;
| key || &amp;lt;code&amp;gt;key&amp;lt;/code&amp;gt; || optional || Erzwingt den Datensatz-Schlüssel für LinkData (falls Ziel und &amp;lt;code&amp;gt;title&amp;lt;/code&amp;gt; nicht übereinstimmen).&lt;br /&gt;
|-&lt;br /&gt;
| limit || &amp;lt;code&amp;gt;limit&amp;lt;/code&amp;gt; || Zahl (220) || Maximale Länge der Tooltip-Beschreibung; bei Überschreitung wird abgeschnitten (…).&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Verhalten ==&lt;br /&gt;
&lt;br /&gt;
=== Anzeigename ===&lt;br /&gt;
* Standardmäßig wird als Anzeigename der &#039;&#039;&#039;deutsche Titel&#039;&#039;&#039; aus [[Modul:LinkData]] verwendet (Feld &amp;lt;code&amp;gt;title&amp;lt;/code&amp;gt;).&lt;br /&gt;
* Eigener Anzeigename (2. Parameter) überschreibt den Titel.&lt;br /&gt;
* Groß-/Kleinschreibung bleibt &#039;&#039;&#039;genau&#039;&#039;&#039; wie im Wikitext/LinkData.&lt;br /&gt;
&lt;br /&gt;
=== Plural / LinkTrail ===&lt;br /&gt;
MediaWikis LinkTrail funktioniert nur bei &#039;&#039;&#039;direkt geschriebenen&#039;&#039;&#039; Links, nicht bei Vorlagen. Dafür gibt es zwei Wege:&lt;br /&gt;
&lt;br /&gt;
# &#039;&#039;&#039;Suffix-Kurzform&#039;&#039;&#039; im 2. Parameter:  &lt;br /&gt;
: &amp;lt;code&amp;gt;&amp;lt;nowiki&amp;gt;{{Link|Bier|+e}}&amp;lt;/nowiki&amp;gt;&amp;lt;/code&amp;gt; → &amp;lt;code&amp;gt;{{Link|Bier|+e}}&amp;lt;/code&amp;gt;  &lt;br /&gt;
: &amp;lt;code&amp;gt;&amp;lt;nowiki&amp;gt;{{Link|Bier|+en}}&amp;lt;/nowiki&amp;gt;&amp;lt;/code&amp;gt; → &amp;lt;code&amp;gt;{{Link|Bier|+en}}&amp;lt;/code&amp;gt;&lt;br /&gt;
# &#039;&#039;&#039;Expliziter Suffix&#039;&#039;&#039; über &amp;lt;code&amp;gt;s=&amp;lt;/code&amp;gt;:  &lt;br /&gt;
: &amp;lt;code&amp;gt;&amp;lt;nowiki&amp;gt;{{Link|Bier|s=e}}&amp;lt;/nowiki&amp;gt;&amp;lt;/code&amp;gt; → &amp;lt;code&amp;gt;{{Link|Bier|s=e}}&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Hinweise:&#039;&#039;&#039;&lt;br /&gt;
* Sobald ein eigener Anzeigename gesetzt ist, wird &amp;lt;code&amp;gt;s=&amp;lt;/code&amp;gt; ignoriert.&lt;br /&gt;
* Bei „unpiped“ Links (also wirklich &amp;lt;code&amp;gt;[[Bier]]e&amp;lt;/code&amp;gt;) greift LinkTrail – das ist mit Vorlagen technisch &#039;&#039;&#039;nicht&#039;&#039;&#039; möglich. Nutze daher &amp;lt;code&amp;gt;s=&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;+…&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Anker/Abschnitte ===&lt;br /&gt;
* Bei Zielen mit &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; (z. B. &amp;lt;code&amp;gt;Währungen#Bier&amp;lt;/code&amp;gt;) wird &#039;&#039;&#039;immer&#039;&#039;&#039; ein gepipter Link erzeugt (kein LinkTrail / ToolTip möglich).&lt;br /&gt;
* Die Detailansicht (&amp;lt;code&amp;gt;view&amp;lt;/code&amp;gt;) setzt die Tabellen-&amp;lt;code&amp;gt;ID&amp;lt;/code&amp;gt; anhand des Titels (Leerzeichen → Unterstrich).&lt;br /&gt;
&lt;br /&gt;
=== Tooltip ===&lt;br /&gt;
* Holt &amp;lt;code&amp;gt;title&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;icon&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;desc&amp;lt;/code&amp;gt; aus [[Modul:LinkData]].&lt;br /&gt;
* Links in der Beschreibung werden im Hover-Tooltip zu &#039;&#039;&#039;normalem Text&#039;&#039;&#039; neutralisiert.&lt;br /&gt;
* Zeilenumbrüche werden unterstützt; lange Texte werden nach &amp;lt;code&amp;gt;limit&amp;lt;/code&amp;gt; Zeichen abgeschnitten.&lt;br /&gt;
&lt;br /&gt;
=== Fallbacks ===&lt;br /&gt;
* Gibt es keinen Datensatz in [[Modul:LinkData|LinkData]], wird ein normaler Link ohne Tooltip ausgegeben.&lt;br /&gt;
&lt;br /&gt;
== Beispiele ==&lt;br /&gt;
&lt;br /&gt;
; Standard&lt;br /&gt;
: &amp;lt;code&amp;gt;&amp;lt;nowiki&amp;gt;{{Link|Bier}}&amp;lt;/nowiki&amp;gt;&amp;lt;/code&amp;gt; → {{Link|Bier}}&lt;br /&gt;
&lt;br /&gt;
; Eigener Anzeigename&lt;br /&gt;
: &amp;lt;code&amp;gt;&amp;lt;nowiki&amp;gt;{{Link|Währungen#Bier|Bier}}&amp;lt;/nowiki&amp;gt;&amp;lt;/code&amp;gt; → {{Link|Währungen#Bier|Bier}}&lt;br /&gt;
&lt;br /&gt;
; Plural via Suffix (Kurzform)&lt;br /&gt;
: &amp;lt;code&amp;gt;&amp;lt;nowiki&amp;gt;{{Link|Bier|+e}}&amp;lt;/nowiki&amp;gt;&amp;lt;/code&amp;gt; → {{Link|Bier|+e}}&lt;br /&gt;
&lt;br /&gt;
; Plural via &amp;lt;code&amp;gt;s=&amp;lt;/code&amp;gt;&lt;br /&gt;
: &amp;lt;code&amp;gt;&amp;lt;nowiki&amp;gt;{{Link|Bier|s=en}}&amp;lt;/nowiki&amp;gt;&amp;lt;/code&amp;gt; → {{Link|Bier|s=en}}&lt;br /&gt;
&lt;br /&gt;
; Detailansicht (Wikitable)&lt;br /&gt;
: &amp;lt;code&amp;gt;&amp;lt;nowiki&amp;gt;{{Link|Bier|view}}&amp;lt;/nowiki&amp;gt;&amp;lt;/code&amp;gt; → {{Link|Bier|view}}&lt;br /&gt;
&lt;br /&gt;
; Limit anpassen&lt;br /&gt;
: &amp;lt;code&amp;gt;&amp;lt;nowiki&amp;gt;{{Link|Bier|limit=40}}&amp;lt;/nowiki&amp;gt;&amp;lt;/code&amp;gt; → {{Link|Bier|limit=40}}&lt;br /&gt;
&lt;br /&gt;
; Abweichender Datensatz (key)&lt;br /&gt;
: &amp;lt;code&amp;gt;&amp;lt;nowiki&amp;gt;{{Link|Währungen|key=Beer}}&amp;lt;/nowiki&amp;gt;&amp;lt;/code&amp;gt; → {{Link|Währungen|key=Beer}}&lt;br /&gt;
&lt;br /&gt;
&amp;lt;noinclude&amp;gt;[[Kategorie:Lua-Module|Link]]&amp;lt;/noinclude&amp;gt;&lt;/div&gt;</summary>
		<author><name>Webmaster</name></author>
	</entry>
	<entry>
		<id>https://firestone-idle-rpg.de/index.php?title=HSG/Info&amp;diff=3741</id>
		<title>HSG/Info</title>
		<link rel="alternate" type="text/html" href="https://firestone-idle-rpg.de/index.php?title=HSG/Info&amp;diff=3741"/>
		<updated>2025-10-27T08:28:57Z</updated>

		<summary type="html">&lt;p&gt;Webmaster: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{DISPLAYTITLE:Helden / Söldner / Götter – Informationen}}&lt;br /&gt;
Wir haben viel Text,&lt;br /&gt;
damit die liste weiter unten ist.&lt;br /&gt;
&lt;br /&gt;
Schauen wir mal.&lt;br /&gt;
&lt;br /&gt;
ToolTip:&lt;br /&gt;
{{Link|Gold}}&lt;br /&gt;
{{Link|Bier|+e}}&lt;br /&gt;
&lt;br /&gt;
Anzeige:&lt;br /&gt;
{{Link|Gold|view}}&lt;br /&gt;
&lt;br /&gt;
Ob es funktioniert.&lt;br /&gt;
&lt;br /&gt;
Lass uns viel Text schreiben&lt;br /&gt;
&lt;br /&gt;
Nur so können wir es prüfen.&lt;br /&gt;
&lt;br /&gt;
{{HeroCard|name=Talia}}&lt;br /&gt;
{{HeroCard|name=Burt}}&lt;br /&gt;
{{HeroCard|name=Vermilion}}&lt;br /&gt;
{{HeroCard|name=Aegis}}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Okay, wo springt er hin, wenn wir auf die Tabs klicken.&lt;br /&gt;
Schauen wir mal&lt;br /&gt;
Lalala&lt;br /&gt;
Usw. das sollte langsam reichen.&lt;br /&gt;
&lt;br /&gt;
Denke ich.&lt;br /&gt;
&amp;lt;tabbertransclude&amp;gt;&lt;br /&gt;
Talia|Talia&lt;br /&gt;
Boris|Boris&lt;br /&gt;
Asmondai|Asmondai&lt;br /&gt;
&amp;lt;/tabbertransclude&amp;gt;&lt;/div&gt;</summary>
		<author><name>Webmaster</name></author>
	</entry>
	<entry>
		<id>https://firestone-idle-rpg.de/index.php?title=Gold&amp;diff=3740</id>
		<title>Gold</title>
		<link rel="alternate" type="text/html" href="https://firestone-idle-rpg.de/index.php?title=Gold&amp;diff=3740"/>
		<updated>2025-10-27T08:28:00Z</updated>

		<summary type="html">&lt;p&gt;Webmaster: Weiterleitungsziel von Currencies#Gold nach Währungen#Gold geändert&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;#REDIRECT [[Währungen#Gold]]&lt;/div&gt;</summary>
		<author><name>Webmaster</name></author>
	</entry>
	<entry>
		<id>https://firestone-idle-rpg.de/index.php?title=W%C3%A4hrungen&amp;diff=3739</id>
		<title>Währungen</title>
		<link rel="alternate" type="text/html" href="https://firestone-idle-rpg.de/index.php?title=W%C3%A4hrungen&amp;diff=3739"/>
		<updated>2025-10-27T08:25:42Z</updated>

		<summary type="html">&lt;p&gt;Webmaster: Webmaster verschob die Seite Currencies nach Währungen, ohne dabei eine Weiterleitung anzulegen&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Link|Gold|view}}&lt;br /&gt;
&lt;br /&gt;
{{Link|Firestones|view}}&lt;br /&gt;
{{Link|Gems|view}}&lt;br /&gt;
{{Link|Void Crystals|view}}&lt;br /&gt;
{{Link|Meteorites|view}}&lt;br /&gt;
{{Link|Talent Points|view}}&lt;br /&gt;
{{Link|Honor|view}}&lt;br /&gt;
{{Link|Strange Dust|view}}&lt;br /&gt;
{{Link|Glory|view}}&lt;br /&gt;
{{Link|Contracts|view}}&lt;br /&gt;
{{Link|Guild Coins|view}}&lt;br /&gt;
{{Link|Expedition Tokens|view}}&lt;br /&gt;
{{Link|Beer|view}}&lt;br /&gt;
{{Link|Game Tokens|view}}&lt;br /&gt;
{{Link|Luck|view}}&lt;br /&gt;
{{Link|Golden Keys|view}}&lt;br /&gt;
{{Link|Prestige Tokens|view}}&lt;br /&gt;
{{Link|Exotic Coins|view}}&lt;br /&gt;
{{Link|Arcane Crystals|view}}&lt;br /&gt;
{{Link|Ethereal Shards|view}}&lt;br /&gt;
{{Link|Blueprints|view}}&lt;br /&gt;
{{Link|Tools|view}}&lt;br /&gt;
{{Link|Emblems of Valor|view}}&lt;br /&gt;
{{Link|Elixirs of Life|view}}&lt;br /&gt;
{{Link|Emblems of Courage|view}}&lt;br /&gt;
{{Link|Pickaxes|view}}&lt;br /&gt;
{{Link|Nobles Token|view}}&lt;br /&gt;
{{Link|Pharaohs Tokens|view}}&lt;br /&gt;
{{Link|Ancient Coins|view}}&lt;br /&gt;
{{Link|Scarabs|view}}&lt;br /&gt;
{{Link|Soul Embers|view}}&lt;br /&gt;
{{Link|Cobra Keys|view}}&lt;br /&gt;
{{Link|Twilight Hourglasses|view}}&lt;br /&gt;
{{Link|Moon Stones|view}}&lt;br /&gt;
{{Link|Eclipse Stones|view}}&lt;br /&gt;
{{Link|Orbs of Light|view}}&lt;br /&gt;
{{Link|Dark Runes|view}}&lt;br /&gt;
{{Link|Dragon Blood|view}}&lt;br /&gt;
{{Link|Soul Shards|view}}&lt;br /&gt;
{{Link|Virtue|view}}&lt;br /&gt;
{{Link|Emblems of Brotherhood|view}}&lt;br /&gt;
{{Link|Star Essence|view}}&lt;/div&gt;</summary>
		<author><name>Webmaster</name></author>
	</entry>
	<entry>
		<id>https://firestone-idle-rpg.de/index.php?title=Modul:Link&amp;diff=3738</id>
		<title>Modul:Link</title>
		<link rel="alternate" type="text/html" href="https://firestone-idle-rpg.de/index.php?title=Modul:Link&amp;diff=3738"/>
		<updated>2025-10-27T08:08:45Z</updated>

		<summary type="html">&lt;p&gt;Webmaster: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;local p = {}&lt;br /&gt;
&lt;br /&gt;
local data = mw.loadData(&#039;Modul:LinkData&#039;)&lt;br /&gt;
local US = mw.ustring&lt;br /&gt;
local DEFAULT_LIMIT = 220&lt;br /&gt;
&lt;br /&gt;
local function trim(s) return type(s)==&#039;string&#039; and (s:gsub(&#039;^%s+&#039;,&#039;&#039;):gsub(&#039;%s+$&#039;,&#039;&#039;)) or &#039;&#039; end&lt;br /&gt;
local function normalize_key(s)&lt;br /&gt;
  s = US.lower(trim(s or &#039;&#039;)):gsub(&#039;_&#039;,&#039; &#039;):gsub(&#039;%s+&#039;,&#039; &#039;)&lt;br /&gt;
  return s&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Info per Key, deutschem Titel oder Alias finden&lt;br /&gt;
local function find_info(term)&lt;br /&gt;
  local key = normalize_key(term)&lt;br /&gt;
  -- 1) direkter Key&lt;br /&gt;
  local bykey = data[key] or data[key:gsub(&#039; &#039;,&#039;_&#039;)]&lt;br /&gt;
  if bykey then return bykey end&lt;br /&gt;
  -- 2) Titel oder Aliases&lt;br /&gt;
  for _, entry in pairs(data) do&lt;br /&gt;
    if entry.title and normalize_key(entry.title) == key then&lt;br /&gt;
      return entry&lt;br /&gt;
    end&lt;br /&gt;
    if entry.aliases then&lt;br /&gt;
      for _, a in ipairs(entry.aliases) do&lt;br /&gt;
        if normalize_key(a) == key then return entry end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
  return nil&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function strip_links(txt)&lt;br /&gt;
  txt = txt or &#039;&#039;&lt;br /&gt;
  txt = txt:gsub(&#039;%[%[([^%]|%]]-)|([^\]]-)%]%]&#039;, &#039;%2&#039;)&lt;br /&gt;
  txt = txt:gsub(&#039;%[%[([^%]]-)%]%]&#039;, function(inner) return inner:gsub(&#039;_&#039;,&#039; &#039;) end)&lt;br /&gt;
  txt = txt:gsub(&#039;%[(https?://[^%s%]]+)%s+([^\]]-)%]&#039;, &#039;%2&#039;)&lt;br /&gt;
  txt = txt:gsub(&#039;%[(https?://[^%]]+)%]&#039;, &#039;%1&#039;)&lt;br /&gt;
  return txt&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function shorten(txt, limit)&lt;br /&gt;
  limit = tonumber(limit) or DEFAULT_LIMIT&lt;br /&gt;
  local len = US.len(txt or &#039;&#039;)&lt;br /&gt;
  if len &amp;lt;= limit then return txt end&lt;br /&gt;
  local cut = US.sub(txt, 1, limit)&lt;br /&gt;
  local lastSpace = cut:match(&#039;^.*()%s&#039;)&lt;br /&gt;
  if lastSpace and lastSpace &amp;gt; 15 then cut = US.sub(cut, 1, lastSpace - 1) end&lt;br /&gt;
  return cut .. &#039;…&#039;&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function htmlize_plain(txt)&lt;br /&gt;
  txt = mw.text.encode(tostring(txt or &#039;&#039;))&lt;br /&gt;
  return txt:gsub(&#039;\n&#039;,&#039;&amp;lt;br/&amp;gt;&#039;)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Angepasst: allow_trail lässt unpiped [[Ziel]] zu (für MediaWiki-LinkTrail)&lt;br /&gt;
local function make_link(target, display, allow_trail)&lt;br /&gt;
  target  = trim(target)&lt;br /&gt;
  display = trim(display or &#039;&#039;)&lt;br /&gt;
  if allow_trail and not target:find(&#039;#&#039;) then&lt;br /&gt;
    return &#039;[[&#039; .. target .. &#039;]]&#039;&lt;br /&gt;
  end&lt;br /&gt;
  return (display ~= &#039;&#039; and (&#039;[[&#039;..target..&#039;|&#039;..display..&#039;]]&#039;) or (&#039;[[&#039;..target..&#039;]]&#039;))&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function render_hover(link_wt, info, limit)&lt;br /&gt;
  if not info or (not info.title and not info.desc and not info.icon) then&lt;br /&gt;
    return link_wt&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  local wrap   = mw.html.create(&#039;span&#039;):addClass(&#039;fs-tt&#039;)&lt;br /&gt;
  wrap:wikitext(link_wt)&lt;br /&gt;
&lt;br /&gt;
  local content = mw.html.create(&#039;span&#039;):addClass(&#039;tt-content&#039;)&lt;br /&gt;
  local box     = mw.html.create(&#039;span&#039;):addClass(&#039;tt-box&#039;)&lt;br /&gt;
&lt;br /&gt;
  -- Icon (ohne Link)&lt;br /&gt;
  if info.icon and info.icon ~= &#039;&#039; then&lt;br /&gt;
    box:wikitext(&#039;[[File:&#039; .. info.icon .. &#039;|48x48px|link=]]&#039;)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  -- Titel + Beschreibung (gekürzt &amp;amp; link-bereinigt)&lt;br /&gt;
  local title = info.title or &#039;&#039;&lt;br /&gt;
  local desc  = info.desc  or &#039;&#039;&lt;br /&gt;
  desc = shorten(strip_links(desc), limit)&lt;br /&gt;
&lt;br /&gt;
  local function esc(s)&lt;br /&gt;
    s = tostring(s or &#039;&#039;)&lt;br /&gt;
    s = mw.text.encode(s)&lt;br /&gt;
    s = s:gsub(&#039;\n&#039;,&#039;&amp;lt;br/&amp;gt;&#039;)&lt;br /&gt;
    return s&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  local text_html = table.concat({&lt;br /&gt;
    &#039;&amp;lt;span class=&amp;quot;tt-text&amp;quot;&amp;gt;&#039;,&lt;br /&gt;
      &#039;&amp;lt;span class=&amp;quot;tt-title&amp;quot;&amp;gt;&#039;, esc(title), &#039;&amp;lt;/span&amp;gt;&#039;,&lt;br /&gt;
      &#039;&amp;lt;span class=&amp;quot;tt-desc&amp;quot;&amp;gt;&#039;,  esc(desc),  &#039;&amp;lt;/span&amp;gt;&#039;,&lt;br /&gt;
    &#039;&amp;lt;/span&amp;gt;&#039;&lt;br /&gt;
  })&lt;br /&gt;
&lt;br /&gt;
  box:wikitext(text_html)&lt;br /&gt;
  content:node(box)&lt;br /&gt;
  wrap:node(content)&lt;br /&gt;
  return tostring(wrap)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Helper: Anchor-ID nur aus dem Title, Spaces -&amp;gt; &amp;quot;_&amp;quot;&lt;br /&gt;
local function anchor_id_from_title(title)&lt;br /&gt;
  local t = mw.text.trim(title or &#039;&#039;)&lt;br /&gt;
  if t == &#039;&#039; then return nil end&lt;br /&gt;
  return mw.ustring.gsub(t, &#039;%s+&#039;, &#039;_&#039;)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- VIEW: wikitable mit id + width=&amp;quot;100%&amp;quot;&lt;br /&gt;
local function render_view(info)&lt;br /&gt;
  local title = (info and info.title) or &#039;&#039;&lt;br /&gt;
  local desc  = (info and info.desc)  or &#039;&#039;&lt;br /&gt;
  local icon  = (info and info.icon)  or &#039;&#039;&lt;br /&gt;
  local id    = anchor_id_from_title(title)&lt;br /&gt;
&lt;br /&gt;
  local tbl = mw.html.create(&#039;table&#039;)&lt;br /&gt;
    :addClass(&#039;wikitable&#039;)&lt;br /&gt;
    :attr(&#039;width&#039;, &#039;100%&#039;)&lt;br /&gt;
&lt;br /&gt;
  if id then&lt;br /&gt;
    tbl:attr(&#039;id&#039;, id)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  local trh = tbl:tag(&#039;tr&#039;)&lt;br /&gt;
  trh:tag(&#039;th&#039;):attr(&#039;colspan&#039;,&#039;2&#039;):wikitext(title ~= &#039;&#039; and title or &#039;&amp;amp;nbsp;&#039;)&lt;br /&gt;
&lt;br /&gt;
  local tr = tbl:tag(&#039;tr&#039;)&lt;br /&gt;
  local tdIcon = tr:tag(&#039;td&#039;):attr(&#039;width&#039;,&#039;110&#039;):attr(&#039;align&#039;,&#039;center&#039;)&lt;br /&gt;
  if icon ~= &#039;&#039; then&lt;br /&gt;
    tdIcon:wikitext(&#039;[[File:&#039;..icon..&#039;|x64px|link=]]&#039;)&lt;br /&gt;
  else&lt;br /&gt;
    tdIcon:wikitext(&#039;&amp;amp;nbsp;&#039;)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  tr:tag(&#039;td&#039;):wikitext(desc or &#039;&#039;)&lt;br /&gt;
  return tostring(tbl)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
function p.main(frame)&lt;br /&gt;
  local parent = frame:getParent()&lt;br /&gt;
  local args = parent and parent.args or frame.args&lt;br /&gt;
&lt;br /&gt;
  local target = trim(args[1] or &#039;&#039;)&lt;br /&gt;
  if target == &#039;&#039; then return &#039;&#039; end&lt;br /&gt;
&lt;br /&gt;
  local p2 = trim(args[2] or &#039;&#039;)&lt;br /&gt;
  local mode_view = (US.lower(p2) == &#039;view&#039;)&lt;br /&gt;
&lt;br /&gt;
  local key = trim(args.key or (mode_view and target or (p2 ~= &#039;&#039; and p2 or target)))&lt;br /&gt;
  local info = data[normalize_key(key)] or data[normalize_key(key):gsub(&#039; &#039;,&#039;_&#039;)]&lt;br /&gt;
  if not info then&lt;br /&gt;
    -- per deutschem Titel / evtl. Aliases suchen&lt;br /&gt;
    local function find_info(term)&lt;br /&gt;
      local k = normalize_key(term)&lt;br /&gt;
      local bykey = data[k] or data[k:gsub(&#039; &#039;,&#039;_&#039;)]&lt;br /&gt;
      if bykey then return bykey end&lt;br /&gt;
      for _, entry in pairs(data) do&lt;br /&gt;
        if entry.title and normalize_key(entry.title) == k then return entry end&lt;br /&gt;
        if entry.aliases then&lt;br /&gt;
          for _,a in ipairs(entry.aliases) do&lt;br /&gt;
            if normalize_key(a) == k then return entry end&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
      return nil&lt;br /&gt;
    end&lt;br /&gt;
    info = find_info(target)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  local limit = tonumber(args.limit or &#039;&#039;) or DEFAULT_LIMIT&lt;br /&gt;
&lt;br /&gt;
  if mode_view then&lt;br /&gt;
    return render_view(info or {})&lt;br /&gt;
  else&lt;br /&gt;
    -- 1) expliziter Anzeigename (2. Param, sofern nicht &#039;view&#039;)&lt;br /&gt;
    local explicit_display = (p2 ~= &#039;&#039; and US.lower(p2) ~= &#039;view&#039;) and p2 or nil&lt;br /&gt;
&lt;br /&gt;
    -- 2) Suffix-Unterstützung: |s=… oder Kurzform |+… im 2. Param&lt;br /&gt;
    local suffix = trim(args.s or &#039;&#039;)&lt;br /&gt;
    if suffix == &#039;&#039; and explicit_display and explicit_display:sub(1,1) == &#039;+&#039; then&lt;br /&gt;
      suffix = explicit_display:sub(2)&lt;br /&gt;
      explicit_display = nil&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    local baseTitle = (info and info.title) or target&lt;br /&gt;
    local display = explicit_display or (suffix ~= &#039;&#039; and (baseTitle .. suffix)) or (info and info.title) or nil&lt;br /&gt;
&lt;br /&gt;
    -- LinkTrail erlauben, wenn kein eigener Display gesetzt wurde&lt;br /&gt;
    -- und target == Titel (ohne Anker)&lt;br /&gt;
    local allow_trail = (explicit_display == nil and suffix == &#039;&#039;)&lt;br /&gt;
      and info and info.title&lt;br /&gt;
      and (normalize_key(target) == normalize_key(info.title))&lt;br /&gt;
      and not target:find(&#039;#&#039;)&lt;br /&gt;
&lt;br /&gt;
    local link_wt = make_link(target, display, allow_trail)&lt;br /&gt;
    return render_hover(link_wt, info or {}, limit)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
return p&lt;/div&gt;</summary>
		<author><name>Webmaster</name></author>
	</entry>
	<entry>
		<id>https://firestone-idle-rpg.de/index.php?title=Bier&amp;diff=3737</id>
		<title>Bier</title>
		<link rel="alternate" type="text/html" href="https://firestone-idle-rpg.de/index.php?title=Bier&amp;diff=3737"/>
		<updated>2025-10-27T07:15:35Z</updated>

		<summary type="html">&lt;p&gt;Webmaster: Weiterleitung nach Währungen#Bier erstellt&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;#REDIRECT [[Währungen#Bier]]&lt;/div&gt;</summary>
		<author><name>Webmaster</name></author>
	</entry>
	<entry>
		<id>https://firestone-idle-rpg.de/index.php?title=HSG/Info&amp;diff=3736</id>
		<title>HSG/Info</title>
		<link rel="alternate" type="text/html" href="https://firestone-idle-rpg.de/index.php?title=HSG/Info&amp;diff=3736"/>
		<updated>2025-10-27T07:14:24Z</updated>

		<summary type="html">&lt;p&gt;Webmaster: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{DISPLAYTITLE:Helden / Söldner / Götter – Informationen}}&lt;br /&gt;
Wir haben viel Text,&lt;br /&gt;
damit die liste weiter unten ist.&lt;br /&gt;
&lt;br /&gt;
Schauen wir mal.&lt;br /&gt;
&lt;br /&gt;
ToolTip:&lt;br /&gt;
{{Link|Gold}}&lt;br /&gt;
{{Link|Bier}}e&lt;br /&gt;
&lt;br /&gt;
Anzeige:&lt;br /&gt;
{{Link|Gold|view}}&lt;br /&gt;
&lt;br /&gt;
Ob es funktioniert.&lt;br /&gt;
&lt;br /&gt;
Lass uns viel Text schreiben&lt;br /&gt;
&lt;br /&gt;
Nur so können wir es prüfen.&lt;br /&gt;
&lt;br /&gt;
{{HeroCard|name=Talia}}&lt;br /&gt;
{{HeroCard|name=Burt}}&lt;br /&gt;
{{HeroCard|name=Vermilion}}&lt;br /&gt;
{{HeroCard|name=Aegis}}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Okay, wo springt er hin, wenn wir auf die Tabs klicken.&lt;br /&gt;
Schauen wir mal&lt;br /&gt;
Lalala&lt;br /&gt;
Usw. das sollte langsam reichen.&lt;br /&gt;
&lt;br /&gt;
Denke ich.&lt;br /&gt;
&amp;lt;tabbertransclude&amp;gt;&lt;br /&gt;
Talia|Talia&lt;br /&gt;
Boris|Boris&lt;br /&gt;
Asmondai|Asmondai&lt;br /&gt;
&amp;lt;/tabbertransclude&amp;gt;&lt;/div&gt;</summary>
		<author><name>Webmaster</name></author>
	</entry>
	<entry>
		<id>https://firestone-idle-rpg.de/index.php?title=HSG/Info&amp;diff=3735</id>
		<title>HSG/Info</title>
		<link rel="alternate" type="text/html" href="https://firestone-idle-rpg.de/index.php?title=HSG/Info&amp;diff=3735"/>
		<updated>2025-10-27T07:14:03Z</updated>

		<summary type="html">&lt;p&gt;Webmaster: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{DISPLAYTITLE:Helden / Söldner / Götter – Informationen}}&lt;br /&gt;
Wir haben viel Text,&lt;br /&gt;
damit die liste weiter unten ist.&lt;br /&gt;
&lt;br /&gt;
Schauen wir mal.&lt;br /&gt;
&lt;br /&gt;
ToolTip:&lt;br /&gt;
{{Link|Gold}}&lt;br /&gt;
{{Link|Beer}}e&lt;br /&gt;
&lt;br /&gt;
Anzeige:&lt;br /&gt;
{{Link|Gold|view}}&lt;br /&gt;
&lt;br /&gt;
Ob es funktioniert.&lt;br /&gt;
&lt;br /&gt;
Lass uns viel Text schreiben&lt;br /&gt;
&lt;br /&gt;
Nur so können wir es prüfen.&lt;br /&gt;
&lt;br /&gt;
{{HeroCard|name=Talia}}&lt;br /&gt;
{{HeroCard|name=Burt}}&lt;br /&gt;
{{HeroCard|name=Vermilion}}&lt;br /&gt;
{{HeroCard|name=Aegis}}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Okay, wo springt er hin, wenn wir auf die Tabs klicken.&lt;br /&gt;
Schauen wir mal&lt;br /&gt;
Lalala&lt;br /&gt;
Usw. das sollte langsam reichen.&lt;br /&gt;
&lt;br /&gt;
Denke ich.&lt;br /&gt;
&amp;lt;tabbertransclude&amp;gt;&lt;br /&gt;
Talia|Talia&lt;br /&gt;
Boris|Boris&lt;br /&gt;
Asmondai|Asmondai&lt;br /&gt;
&amp;lt;/tabbertransclude&amp;gt;&lt;/div&gt;</summary>
		<author><name>Webmaster</name></author>
	</entry>
	<entry>
		<id>https://firestone-idle-rpg.de/index.php?title=Modul:Link&amp;diff=3734</id>
		<title>Modul:Link</title>
		<link rel="alternate" type="text/html" href="https://firestone-idle-rpg.de/index.php?title=Modul:Link&amp;diff=3734"/>
		<updated>2025-10-27T07:13:18Z</updated>

		<summary type="html">&lt;p&gt;Webmaster: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;local p = {}&lt;br /&gt;
&lt;br /&gt;
local data = mw.loadData(&#039;Modul:LinkData&#039;)&lt;br /&gt;
local US = mw.ustring&lt;br /&gt;
local DEFAULT_LIMIT = 220&lt;br /&gt;
&lt;br /&gt;
local function trim(s) return type(s)==&#039;string&#039; and (s:gsub(&#039;^%s+&#039;,&#039;&#039;):gsub(&#039;%s+$&#039;,&#039;&#039;)) or &#039;&#039; end&lt;br /&gt;
local function normalize_key(s)&lt;br /&gt;
  s = US.lower(trim(s or &#039;&#039;)):gsub(&#039;_&#039;,&#039; &#039;):gsub(&#039;%s+&#039;,&#039; &#039;)&lt;br /&gt;
  return s&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Info per Key, deutschem Titel oder Alias finden&lt;br /&gt;
local function find_info(term)&lt;br /&gt;
  local key = normalize_key(term)&lt;br /&gt;
  -- 1) direkter Key&lt;br /&gt;
  local bykey = data[key] or data[key:gsub(&#039; &#039;,&#039;_&#039;)]&lt;br /&gt;
  if bykey then return bykey end&lt;br /&gt;
  -- 2) Titel oder Aliases&lt;br /&gt;
  for _, entry in pairs(data) do&lt;br /&gt;
    if entry.title and normalize_key(entry.title) == key then&lt;br /&gt;
      return entry&lt;br /&gt;
    end&lt;br /&gt;
    if entry.aliases then&lt;br /&gt;
      for _, a in ipairs(entry.aliases) do&lt;br /&gt;
        if normalize_key(a) == key then return entry end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
  return nil&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function strip_links(txt)&lt;br /&gt;
  txt = txt or &#039;&#039;&lt;br /&gt;
  txt = txt:gsub(&#039;%[%[([^%]|%]]-)|([^\]]-)%]%]&#039;, &#039;%2&#039;)&lt;br /&gt;
  txt = txt:gsub(&#039;%[%[([^%]]-)%]%]&#039;, function(inner) return inner:gsub(&#039;_&#039;,&#039; &#039;) end)&lt;br /&gt;
  txt = txt:gsub(&#039;%[(https?://[^%s%]]+)%s+([^\]]-)%]&#039;, &#039;%2&#039;)&lt;br /&gt;
  txt = txt:gsub(&#039;%[(https?://[^%]]+)%]&#039;, &#039;%1&#039;)&lt;br /&gt;
  return txt&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function shorten(txt, limit)&lt;br /&gt;
  limit = tonumber(limit) or DEFAULT_LIMIT&lt;br /&gt;
  local len = US.len(txt or &#039;&#039;)&lt;br /&gt;
  if len &amp;lt;= limit then return txt end&lt;br /&gt;
  local cut = US.sub(txt, 1, limit)&lt;br /&gt;
  local lastSpace = cut:match(&#039;^.*()%s&#039;)&lt;br /&gt;
  if lastSpace and lastSpace &amp;gt; 15 then cut = US.sub(cut, 1, lastSpace - 1) end&lt;br /&gt;
  return cut .. &#039;…&#039;&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function htmlize_plain(txt)&lt;br /&gt;
  txt = mw.text.encode(tostring(txt or &#039;&#039;))&lt;br /&gt;
  return txt:gsub(&#039;\n&#039;,&#039;&amp;lt;br/&amp;gt;&#039;)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Angepasst: allow_trail lässt unpiped [[Ziel]] zu (für MediaWiki-LinkTrail)&lt;br /&gt;
local function make_link(target, display, allow_trail)&lt;br /&gt;
  target  = trim(target)&lt;br /&gt;
  display = trim(display or &#039;&#039;)&lt;br /&gt;
  if allow_trail and not target:find(&#039;#&#039;) then&lt;br /&gt;
    return &#039;[[&#039; .. target .. &#039;]]&#039;&lt;br /&gt;
  end&lt;br /&gt;
  return (display ~= &#039;&#039; and (&#039;[[&#039;..target..&#039;|&#039;..display..&#039;]]&#039;) or (&#039;[[&#039;..target..&#039;]]&#039;))&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function render_hover(link_wt, info, limit)&lt;br /&gt;
  if not info or (not info.title and not info.desc and not info.icon) then&lt;br /&gt;
    return link_wt&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  local wrap   = mw.html.create(&#039;span&#039;):addClass(&#039;fs-tt&#039;)&lt;br /&gt;
  wrap:wikitext(link_wt)&lt;br /&gt;
&lt;br /&gt;
  local content = mw.html.create(&#039;span&#039;):addClass(&#039;tt-content&#039;)&lt;br /&gt;
  local box     = mw.html.create(&#039;span&#039;):addClass(&#039;tt-box&#039;)&lt;br /&gt;
&lt;br /&gt;
  -- Icon (ohne Link)&lt;br /&gt;
  if info.icon and info.icon ~= &#039;&#039; then&lt;br /&gt;
    box:wikitext(&#039;[[File:&#039; .. info.icon .. &#039;|48x48px|link=]]&#039;)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  -- Titel + Beschreibung (gekürzt &amp;amp; link-bereinigt)&lt;br /&gt;
  local title = info.title or &#039;&#039;&lt;br /&gt;
  local desc  = info.desc  or &#039;&#039;&lt;br /&gt;
  desc = shorten(strip_links(desc), limit)&lt;br /&gt;
&lt;br /&gt;
  local function esc(s)&lt;br /&gt;
    s = tostring(s or &#039;&#039;)&lt;br /&gt;
    s = mw.text.encode(s)&lt;br /&gt;
    s = s:gsub(&#039;\n&#039;,&#039;&amp;lt;br/&amp;gt;&#039;)&lt;br /&gt;
    return s&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  local text_html = table.concat({&lt;br /&gt;
    &#039;&amp;lt;span class=&amp;quot;tt-text&amp;quot;&amp;gt;&#039;,&lt;br /&gt;
      &#039;&amp;lt;span class=&amp;quot;tt-title&amp;quot;&amp;gt;&#039;, esc(title), &#039;&amp;lt;/span&amp;gt;&#039;,&lt;br /&gt;
      &#039;&amp;lt;span class=&amp;quot;tt-desc&amp;quot;&amp;gt;&#039;,  esc(desc),  &#039;&amp;lt;/span&amp;gt;&#039;,&lt;br /&gt;
    &#039;&amp;lt;/span&amp;gt;&#039;&lt;br /&gt;
  })&lt;br /&gt;
&lt;br /&gt;
  box:wikitext(text_html)&lt;br /&gt;
  content:node(box)&lt;br /&gt;
  wrap:node(content)&lt;br /&gt;
  return tostring(wrap)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Helper: Anchor-ID nur aus dem Title, Spaces -&amp;gt; &amp;quot;_&amp;quot;&lt;br /&gt;
local function anchor_id_from_title(title)&lt;br /&gt;
  local t = mw.text.trim(title or &#039;&#039;)&lt;br /&gt;
  if t == &#039;&#039; then return nil end&lt;br /&gt;
  return mw.ustring.gsub(t, &#039;%s+&#039;, &#039;_&#039;)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- VIEW: wikitable mit id + width=&amp;quot;100%&amp;quot;&lt;br /&gt;
local function render_view(info)&lt;br /&gt;
  local title = (info and info.title) or &#039;&#039;&lt;br /&gt;
  local desc  = (info and info.desc)  or &#039;&#039;&lt;br /&gt;
  local icon  = (info and info.icon)  or &#039;&#039;&lt;br /&gt;
  local id    = anchor_id_from_title(title)&lt;br /&gt;
&lt;br /&gt;
  local tbl = mw.html.create(&#039;table&#039;)&lt;br /&gt;
    :addClass(&#039;wikitable&#039;)&lt;br /&gt;
    :attr(&#039;width&#039;, &#039;100%&#039;)&lt;br /&gt;
&lt;br /&gt;
  if id then&lt;br /&gt;
    tbl:attr(&#039;id&#039;, id)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  local trh = tbl:tag(&#039;tr&#039;)&lt;br /&gt;
  trh:tag(&#039;th&#039;):attr(&#039;colspan&#039;,&#039;2&#039;):wikitext(title ~= &#039;&#039; and title or &#039;&amp;amp;nbsp;&#039;)&lt;br /&gt;
&lt;br /&gt;
  local tr = tbl:tag(&#039;tr&#039;)&lt;br /&gt;
  local tdIcon = tr:tag(&#039;td&#039;):attr(&#039;width&#039;,&#039;110&#039;):attr(&#039;align&#039;,&#039;center&#039;)&lt;br /&gt;
  if icon ~= &#039;&#039; then&lt;br /&gt;
    tdIcon:wikitext(&#039;[[File:&#039;..icon..&#039;|x64px|link=]]&#039;)&lt;br /&gt;
  else&lt;br /&gt;
    tdIcon:wikitext(&#039;&amp;amp;nbsp;&#039;)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  tr:tag(&#039;td&#039;):wikitext(desc or &#039;&#039;)&lt;br /&gt;
  return tostring(tbl)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
function p.main(frame)&lt;br /&gt;
  local parent = frame:getParent()&lt;br /&gt;
  local args = parent and parent.args or frame.args&lt;br /&gt;
&lt;br /&gt;
  local target = trim(args[1] or &#039;&#039;)&lt;br /&gt;
  if target == &#039;&#039; then return &#039;&#039; end&lt;br /&gt;
&lt;br /&gt;
  local p2 = trim(args[2] or &#039;&#039;)&lt;br /&gt;
  local mode_view = (US.lower(p2) == &#039;view&#039;)&lt;br /&gt;
&lt;br /&gt;
  -- Lookup: erst via |key= / alter Logik, dann per Titel/Key-Fallback&lt;br /&gt;
  local key = trim(args.key or (mode_view and target or (p2 ~= &#039;&#039; and p2 or target)))&lt;br /&gt;
  local info = data[normalize_key(key)] or data[normalize_key(key):gsub(&#039; &#039;,&#039;_&#039;)]&lt;br /&gt;
  if not info then&lt;br /&gt;
    info = find_info(target)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  local limit = tonumber(args.limit or &#039;&#039;) or DEFAULT_LIMIT&lt;br /&gt;
&lt;br /&gt;
  if mode_view then&lt;br /&gt;
    return render_view(info or {})&lt;br /&gt;
  else&lt;br /&gt;
    -- Displaytext:&lt;br /&gt;
    -- 1) explizit (2. Param, sofern nicht &#039;view&#039;)&lt;br /&gt;
    -- 2) sonst Titel aus Data&lt;br /&gt;
    -- 3) Fallback: nil -&amp;gt; [[target]] (zeigt Zieltext)&lt;br /&gt;
    local explicit_display = (p2 ~= &#039;&#039; and US.lower(p2) ~= &#039;view&#039;) and p2 or nil&lt;br /&gt;
    local display = explicit_display or (info and info.title) or nil&lt;br /&gt;
&lt;br /&gt;
    -- LinkTrail nur, wenn:&lt;br /&gt;
    -- - KEIN eigener Anzeigename übergeben wurde&lt;br /&gt;
    -- - target entspricht dem (deutschen) Titel&lt;br /&gt;
    -- - kein Anker (#)&lt;br /&gt;
    local allow_trail = (explicit_display == nil)&lt;br /&gt;
      and info and info.title&lt;br /&gt;
      and (normalize_key(target) == normalize_key(info.title))&lt;br /&gt;
      and not target:find(&#039;#&#039;)&lt;br /&gt;
&lt;br /&gt;
    local link_wt = make_link(target, display, allow_trail)&lt;br /&gt;
    return render_hover(link_wt, info or {}, limit)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
return p&lt;/div&gt;</summary>
		<author><name>Webmaster</name></author>
	</entry>
	<entry>
		<id>https://firestone-idle-rpg.de/index.php?title=HSG/Info&amp;diff=3733</id>
		<title>HSG/Info</title>
		<link rel="alternate" type="text/html" href="https://firestone-idle-rpg.de/index.php?title=HSG/Info&amp;diff=3733"/>
		<updated>2025-10-27T03:13:04Z</updated>

		<summary type="html">&lt;p&gt;Webmaster: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{DISPLAYTITLE:Helden / Söldner / Götter – Informationen}}&lt;br /&gt;
Wir haben viel Text,&lt;br /&gt;
damit die liste weiter unten ist.&lt;br /&gt;
&lt;br /&gt;
Schauen wir mal.&lt;br /&gt;
&lt;br /&gt;
ToolTip:&lt;br /&gt;
{{Link|Gold}}&lt;br /&gt;
{{Link|Beer}}s&lt;br /&gt;
&lt;br /&gt;
Anzeige:&lt;br /&gt;
{{Link|Gold|view}}&lt;br /&gt;
&lt;br /&gt;
Ob es funktioniert.&lt;br /&gt;
&lt;br /&gt;
Lass uns viel Text schreiben&lt;br /&gt;
&lt;br /&gt;
Nur so können wir es prüfen.&lt;br /&gt;
&lt;br /&gt;
{{HeroCard|name=Talia}}&lt;br /&gt;
{{HeroCard|name=Burt}}&lt;br /&gt;
{{HeroCard|name=Vermilion}}&lt;br /&gt;
{{HeroCard|name=Aegis}}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Okay, wo springt er hin, wenn wir auf die Tabs klicken.&lt;br /&gt;
Schauen wir mal&lt;br /&gt;
Lalala&lt;br /&gt;
Usw. das sollte langsam reichen.&lt;br /&gt;
&lt;br /&gt;
Denke ich.&lt;br /&gt;
&amp;lt;tabbertransclude&amp;gt;&lt;br /&gt;
Talia|Talia&lt;br /&gt;
Boris|Boris&lt;br /&gt;
Asmondai|Asmondai&lt;br /&gt;
&amp;lt;/tabbertransclude&amp;gt;&lt;/div&gt;</summary>
		<author><name>Webmaster</name></author>
	</entry>
	<entry>
		<id>https://firestone-idle-rpg.de/index.php?title=HSG/Info&amp;diff=3732</id>
		<title>HSG/Info</title>
		<link rel="alternate" type="text/html" href="https://firestone-idle-rpg.de/index.php?title=HSG/Info&amp;diff=3732"/>
		<updated>2025-10-27T03:12:19Z</updated>

		<summary type="html">&lt;p&gt;Webmaster: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{DISPLAYTITLE:Helden / Söldner / Götter – Informationen}}&lt;br /&gt;
Wir haben viel Text,&lt;br /&gt;
damit die liste weiter unten ist.&lt;br /&gt;
&lt;br /&gt;
Schauen wir mal.&lt;br /&gt;
&lt;br /&gt;
ToolTip:&lt;br /&gt;
{{Link|Gold}}&lt;br /&gt;
{{Link|Beer}}e&lt;br /&gt;
&lt;br /&gt;
Anzeige:&lt;br /&gt;
{{Link|Gold|view}}&lt;br /&gt;
&lt;br /&gt;
Ob es funktioniert.&lt;br /&gt;
&lt;br /&gt;
Lass uns viel Text schreiben&lt;br /&gt;
&lt;br /&gt;
Nur so können wir es prüfen.&lt;br /&gt;
&lt;br /&gt;
{{HeroCard|name=Talia}}&lt;br /&gt;
{{HeroCard|name=Burt}}&lt;br /&gt;
{{HeroCard|name=Vermilion}}&lt;br /&gt;
{{HeroCard|name=Aegis}}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Okay, wo springt er hin, wenn wir auf die Tabs klicken.&lt;br /&gt;
Schauen wir mal&lt;br /&gt;
Lalala&lt;br /&gt;
Usw. das sollte langsam reichen.&lt;br /&gt;
&lt;br /&gt;
Denke ich.&lt;br /&gt;
&amp;lt;tabbertransclude&amp;gt;&lt;br /&gt;
Talia|Talia&lt;br /&gt;
Boris|Boris&lt;br /&gt;
Asmondai|Asmondai&lt;br /&gt;
&amp;lt;/tabbertransclude&amp;gt;&lt;/div&gt;</summary>
		<author><name>Webmaster</name></author>
	</entry>
	<entry>
		<id>https://firestone-idle-rpg.de/index.php?title=HSG/Info&amp;diff=3731</id>
		<title>HSG/Info</title>
		<link rel="alternate" type="text/html" href="https://firestone-idle-rpg.de/index.php?title=HSG/Info&amp;diff=3731"/>
		<updated>2025-10-27T03:11:31Z</updated>

		<summary type="html">&lt;p&gt;Webmaster: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{DISPLAYTITLE:Helden / Söldner / Götter – Informationen}}&lt;br /&gt;
Wir haben viel Text,&lt;br /&gt;
damit die liste weiter unten ist.&lt;br /&gt;
&lt;br /&gt;
Schauen wir mal.&lt;br /&gt;
&lt;br /&gt;
ToolTip:&lt;br /&gt;
{{Link|Gold}}&lt;br /&gt;
{{Link|Bier}}e&lt;br /&gt;
&lt;br /&gt;
Anzeige:&lt;br /&gt;
{{Link|Gold|view}}&lt;br /&gt;
&lt;br /&gt;
Ob es funktioniert.&lt;br /&gt;
&lt;br /&gt;
Lass uns viel Text schreiben&lt;br /&gt;
&lt;br /&gt;
Nur so können wir es prüfen.&lt;br /&gt;
&lt;br /&gt;
{{HeroCard|name=Talia}}&lt;br /&gt;
{{HeroCard|name=Burt}}&lt;br /&gt;
{{HeroCard|name=Vermilion}}&lt;br /&gt;
{{HeroCard|name=Aegis}}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Okay, wo springt er hin, wenn wir auf die Tabs klicken.&lt;br /&gt;
Schauen wir mal&lt;br /&gt;
Lalala&lt;br /&gt;
Usw. das sollte langsam reichen.&lt;br /&gt;
&lt;br /&gt;
Denke ich.&lt;br /&gt;
&amp;lt;tabbertransclude&amp;gt;&lt;br /&gt;
Talia|Talia&lt;br /&gt;
Boris|Boris&lt;br /&gt;
Asmondai|Asmondai&lt;br /&gt;
&amp;lt;/tabbertransclude&amp;gt;&lt;/div&gt;</summary>
		<author><name>Webmaster</name></author>
	</entry>
</feed>