<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://utopiawiki.com/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Sonja+Phone</id>
	<title>Utopia Game - User contributions [en]</title>
	<link rel="self" type="application/atom+xml" href="https://utopiawiki.com/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Sonja+Phone"/>
	<link rel="alternate" type="text/html" href="https://utopiawiki.com/index.php/Special:Contributions/Sonja_Phone"/>
	<updated>2026-06-13T12:51:35Z</updated>
	<subtitle>User contributions</subtitle>
	<generator>MediaWiki 1.45.1</generator>
	<entry>
		<id>https://utopiawiki.com/index.php?title=MediaWiki:Common.js&amp;diff=1081</id>
		<title>MediaWiki:Common.js</title>
		<link rel="alternate" type="text/html" href="https://utopiawiki.com/index.php?title=MediaWiki:Common.js&amp;diff=1081"/>
		<updated>2026-06-08T01:57:25Z</updated>

		<summary type="html">&lt;p&gt;Sonja Phone: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* Any JavaScript here will be loaded for all users on every page load. */&lt;br /&gt;
(function () {&lt;br /&gt;
  &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
  /* =========================================================&lt;br /&gt;
     CONFIG&lt;br /&gt;
     ========================================================= */&lt;br /&gt;
  const DISCORD_URL = &#039;https://discord.gg/t2Rp2dRvze&#039;;&lt;br /&gt;
&lt;br /&gt;
  // Countdown target: Sat, 18 Apr 2026 00:00 UTC&lt;br /&gt;
  const COUNTDOWN_TARGET_UTC = Date.parse(&#039;2026-07-11T00:00:00Z&#039;);&lt;br /&gt;
&lt;br /&gt;
  // Clock timezone (same for everyone)&lt;br /&gt;
  const CLOCK_TIMEZONE = &#039;Etc/UTC&#039;;&lt;br /&gt;
&lt;br /&gt;
const AGE_START_UTC = Date.parse(&#039;2026-04-28T18:00:00Z&#039;);&lt;br /&gt;
&lt;br /&gt;
  // Game time anchor (real UTC -&amp;gt; game time)&lt;br /&gt;
  // At 2026-04-28 14:00:00 UTC, game time was Jan 1 YR0&lt;br /&gt;
  const GAME_ANCHOR_REAL_UTC = Date.parse(&#039;2026-04-28T18:00:00Z&#039;);&lt;br /&gt;
  const GAME_ANCHOR_MONTH_INDEX = 0; // Jan&lt;br /&gt;
  const GAME_ANCHOR_DAY = 1;        // 1..24&lt;br /&gt;
  const GAME_ANCHOR_YEAR = 0;        // YR0&lt;br /&gt;
  const GAME_MONTHS = [&#039;Jan&#039;, &#039;Feb&#039;, &#039;Mar&#039;, &#039;Apr&#039;, &#039;May&#039;, &#039;Jun&#039;, &#039;Jul&#039;];&lt;br /&gt;
&lt;br /&gt;
  // Tick warning threshold&lt;br /&gt;
  const TICK_SOON_MINUTES = 5;&lt;br /&gt;
&lt;br /&gt;
  const pad2 = (n) =&amp;gt; String(n).padStart(2, &#039;0&#039;);&lt;br /&gt;
&lt;br /&gt;
  /* =========================================================&lt;br /&gt;
     Tabs (your existing code)&lt;br /&gt;
     ========================================================= */&lt;br /&gt;
  $(function () {&lt;br /&gt;
    $(&#039;.wiki-tabs-container&#039;).each(function () {&lt;br /&gt;
      const $container = $(this);&lt;br /&gt;
      const $firstButton = $container.find(&#039;.wiki-tab-button&#039;).first();&lt;br /&gt;
      const firstTabId = $firstButton.data(&#039;tab&#039;);&lt;br /&gt;
      if (!firstTabId) return;&lt;br /&gt;
&lt;br /&gt;
      $firstButton.addClass(&#039;active&#039;);&lt;br /&gt;
      $container.find(&#039;#&#039; + firstTabId).addClass(&#039;active&#039;).show();&lt;br /&gt;
    });&lt;br /&gt;
&lt;br /&gt;
    $(&#039;.wiki-tab-button&#039;).on(&#039;click&#039;, function () {&lt;br /&gt;
      const $button = $(this);&lt;br /&gt;
      const tabId = $button.data(&#039;tab&#039;);&lt;br /&gt;
      if (!tabId) return;&lt;br /&gt;
&lt;br /&gt;
      const $container = $button.closest(&#039;.wiki-tabs-container&#039;);&lt;br /&gt;
&lt;br /&gt;
      $container.find(&#039;.wiki-tab-button&#039;).removeClass(&#039;active&#039;);&lt;br /&gt;
      $container.find(&#039;.wiki-tab-pane&#039;).removeClass(&#039;active&#039;).hide();&lt;br /&gt;
&lt;br /&gt;
      $button.addClass(&#039;active&#039;);&lt;br /&gt;
      $container.find(&#039;#&#039; + tabId).addClass(&#039;active&#039;).show();&lt;br /&gt;
    });&lt;br /&gt;
  });&lt;br /&gt;
&lt;br /&gt;
  /* =========================================================&lt;br /&gt;
     Helpers: find/create containers in BOTH headers&lt;br /&gt;
     ========================================================= */&lt;br /&gt;
  function ensureDiscordLink(container, id) {&lt;br /&gt;
    if (!container) return null;&lt;br /&gt;
    const existing = document.getElementById(id);&lt;br /&gt;
    if (existing) return existing;&lt;br /&gt;
&lt;br /&gt;
    const link = document.createElement(&#039;a&#039;);&lt;br /&gt;
    link.id = id;&lt;br /&gt;
    link.href = DISCORD_URL;&lt;br /&gt;
    link.target = &#039;_blank&#039;;&lt;br /&gt;
    link.className =&lt;br /&gt;
      &#039;cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet sticky-discord-link&#039;;&lt;br /&gt;
    link.textContent = &#039;Join Us on Discord!&#039;;&lt;br /&gt;
    link.style.fontWeight = &#039;bold&#039;;&lt;br /&gt;
&lt;br /&gt;
    container.prepend(link);&lt;br /&gt;
    return link;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // Sticky header: insert our widget bar inside sticky icons row&lt;br /&gt;
  function getStickyBar() {&lt;br /&gt;
    const icons = document.querySelector(&#039;.vector-sticky-header-icons&#039;);&lt;br /&gt;
    if (!icons) return null;&lt;br /&gt;
&lt;br /&gt;
    let bar = document.getElementById(&#039;sticky-time-widgets&#039;);&lt;br /&gt;
    if (bar) return bar;&lt;br /&gt;
&lt;br /&gt;
    // Keep Discord where it was (far-left in sticky icons)&lt;br /&gt;
    const discord = ensureDiscordLink(icons, &#039;custom-sticky-link&#039;);&lt;br /&gt;
&lt;br /&gt;
    bar = document.createElement(&#039;span&#039;);&lt;br /&gt;
    bar.id = &#039;sticky-time-widgets&#039;;&lt;br /&gt;
    bar.className = &#039;time-widgets&#039;;&lt;br /&gt;
&lt;br /&gt;
    // Put widgets immediately AFTER Discord (so Discord stays at the left)&lt;br /&gt;
    if (discord &amp;amp;&amp;amp; discord.parentNode === icons) {&lt;br /&gt;
      discord.insertAdjacentElement(&#039;afterend&#039;, bar);&lt;br /&gt;
    } else {&lt;br /&gt;
      icons.prepend(bar);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    return bar;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // Main header: put our widgets in header end area&lt;br /&gt;
  function getMainBar() {&lt;br /&gt;
    const header = document.querySelector(&#039;.vector-header&#039;);&lt;br /&gt;
    if (!header) return null;&lt;br /&gt;
&lt;br /&gt;
    const headerEnd = header.querySelector(&#039;.vector-header-end&#039;);&lt;br /&gt;
    if (!headerEnd) return null;&lt;br /&gt;
&lt;br /&gt;
    let bar = document.getElementById(&#039;main-time-widgets&#039;);&lt;br /&gt;
    if (bar) return bar;&lt;br /&gt;
&lt;br /&gt;
    bar = document.createElement(&#039;span&#039;);&lt;br /&gt;
    bar.id = &#039;main-time-widgets&#039;;&lt;br /&gt;
    bar.className = &#039;time-widgets&#039;;&lt;br /&gt;
&lt;br /&gt;
    headerEnd.prepend(bar);&lt;br /&gt;
    ensureDiscordLink(bar, &#039;main-discord-link&#039;);&lt;br /&gt;
&lt;br /&gt;
    return bar;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function ensureWidget(bar, role, labelText) {&lt;br /&gt;
    if (!bar) return;&lt;br /&gt;
    if (bar.querySelector(`[data-role=&amp;quot;${role}&amp;quot;]`)) return;&lt;br /&gt;
&lt;br /&gt;
    const wrap = document.createElement(&#039;span&#039;);&lt;br /&gt;
    wrap.className = &#039;header-widget&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (labelText) {&lt;br /&gt;
      const label = document.createElement(&#039;span&#039;);&lt;br /&gt;
      label.className = &#039;header-widget__label&#039;;&lt;br /&gt;
      label.textContent = labelText;&lt;br /&gt;
      wrap.appendChild(label);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    const value = document.createElement(&#039;span&#039;);&lt;br /&gt;
    value.className = &#039;header-widget__value&#039;;&lt;br /&gt;
    value.dataset.role = role;&lt;br /&gt;
&lt;br /&gt;
    wrap.appendChild(value);&lt;br /&gt;
    bar.appendChild(wrap);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* =========================================================&lt;br /&gt;
     Calculations / formatting&lt;br /&gt;
     ========================================================= */&lt;br /&gt;
  function formatRemaining(ms) {&lt;br /&gt;
    if (ms &amp;lt;= 0) return &#039;00:00:00&#039;;&lt;br /&gt;
&lt;br /&gt;
    const totalSeconds = Math.floor(ms / 1000);&lt;br /&gt;
    const days = Math.floor(totalSeconds / 86400);&lt;br /&gt;
    const hours = Math.floor((totalSeconds % 86400) / 3600);&lt;br /&gt;
    const minutes = Math.floor((totalSeconds % 3600) / 60);&lt;br /&gt;
    const seconds = totalSeconds % 60;&lt;br /&gt;
&lt;br /&gt;
    if (days &amp;gt; 0) return `${days}d ${pad2(hours)}:${pad2(minutes)}:${pad2(seconds)}`;&lt;br /&gt;
    return `${pad2(hours)}:${pad2(minutes)}:${pad2(seconds)}`;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function computeGameString(nowMs) {&lt;br /&gt;
    const hoursPassed = Math.floor((nowMs - GAME_ANCHOR_REAL_UTC) / 3600000);&lt;br /&gt;
&lt;br /&gt;
    // Day increments every hour (1..24)&lt;br /&gt;
    const dayIndex = (GAME_ANCHOR_DAY - 1) + hoursPassed;&lt;br /&gt;
    const dayNumber = ((dayIndex % 24) + 24) % 24 + 1;&lt;br /&gt;
&lt;br /&gt;
    // Month increments every 24 hours (Jan..Jul cycle)&lt;br /&gt;
    const daysPassed = Math.floor(dayIndex / 24);&lt;br /&gt;
    const totalMonthIndex = GAME_ANCHOR_MONTH_INDEX + daysPassed;&lt;br /&gt;
    const monthIndex = ((totalMonthIndex % 7) + 7) % 7;&lt;br /&gt;
&lt;br /&gt;
    // Year increments every 7 real days&lt;br /&gt;
    const yearsPassed = Math.floor(totalMonthIndex / 7);&lt;br /&gt;
    const year = GAME_ANCHOR_YEAR + yearsPassed;&lt;br /&gt;
&lt;br /&gt;
    return `Current Game Date: ${GAME_MONTHS[monthIndex]} ${dayNumber} YR${year}`;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  const clockFmt = new Intl.DateTimeFormat(&#039;en-GB&#039;, {&lt;br /&gt;
    timeZone: CLOCK_TIMEZONE,&lt;br /&gt;
    hour: &#039;2-digit&#039;,&lt;br /&gt;
    minute: &#039;2-digit&#039;,&lt;br /&gt;
    second: &#039;2-digit&#039;,&lt;br /&gt;
    hour12: false&lt;br /&gt;
  });&lt;br /&gt;
&lt;br /&gt;
  function updateAll() {&lt;br /&gt;
    const now = Date.now();&lt;br /&gt;
&lt;br /&gt;
    // --- CLOCK ---&lt;br /&gt;
    document.querySelectorAll(&#039;[data-role=&amp;quot;clock&amp;quot;]&#039;).forEach((el) =&amp;gt; {&lt;br /&gt;
      el.textContent = clockFmt.format(new Date(now));&lt;br /&gt;
    });&lt;br /&gt;
&lt;br /&gt;
    // --- GAME DATE ---&lt;br /&gt;
const msUntilNextHour = 3600000 - (now % 3600000);&lt;br /&gt;
const thresholdMs = TICK_SOON_MINUTES * 60 * 1000;&lt;br /&gt;
const shouldWarn = msUntilNextHour &amp;gt; 0 &amp;amp;&amp;amp; msUntilNextHour &amp;lt;= thresholdMs;&lt;br /&gt;
const minutesLeft = Math.ceil(msUntilNextHour / 60000);&lt;br /&gt;
&lt;br /&gt;
document.querySelectorAll(&#039;[data-role=&amp;quot;game&amp;quot;]&#039;).forEach((el) =&amp;gt; {&lt;br /&gt;
  if (now &amp;lt; AGE_START_UTC) {&lt;br /&gt;
    el.textContent = &#039;PRE AGE&#039;;&lt;br /&gt;
    el.classList.remove(&#039;pulse-red&#039;);&lt;br /&gt;
  } else if (shouldWarn) {&lt;br /&gt;
    el.textContent = `TICK SOON (${minutesLeft}m)`;&lt;br /&gt;
    el.classList.add(&#039;pulse-red&#039;);&lt;br /&gt;
  } else {&lt;br /&gt;
    el.textContent = computeGameString(now);&lt;br /&gt;
    el.classList.remove(&#039;pulse-red&#039;);&lt;br /&gt;
  }&lt;br /&gt;
});&lt;br /&gt;
&lt;br /&gt;
    // --- COUNTDOWN ---&lt;br /&gt;
    document.querySelectorAll(&#039;[data-role=&amp;quot;countdown&amp;quot;]&#039;).forEach((el) =&amp;gt; {&lt;br /&gt;
      el.textContent = formatRemaining(COUNTDOWN_TARGET_UTC - now);&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  /* =========================================================&lt;br /&gt;
     Build + Boot&lt;br /&gt;
     ========================================================= */&lt;br /&gt;
  function buildBarsIfPossible() {&lt;br /&gt;
    const stickyBar = getStickyBar();&lt;br /&gt;
    const mainBar = getMainBar();&lt;br /&gt;
&lt;br /&gt;
    if (stickyBar) {&lt;br /&gt;
      ensureWidget(stickyBar, &#039;clock&#039;, &#039;UTC:&#039;);&lt;br /&gt;
      ensureWidget(stickyBar, &#039;game&#039;, null);&lt;br /&gt;
      ensureWidget(stickyBar, &#039;countdown&#039;, &#039;Age ends in:&#039;);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    if (mainBar) {&lt;br /&gt;
      ensureWidget(mainBar, &#039;clock&#039;, &#039;UTC:&#039;);&lt;br /&gt;
      ensureWidget(mainBar, &#039;game&#039;, null);&lt;br /&gt;
      ensureWidget(mainBar, &#039;countdown&#039;, &#039;Age ends in:&#039;);&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function startTickerOnce() {&lt;br /&gt;
    if (window.__timeWidgetsIntervalId) return;&lt;br /&gt;
    updateAll();&lt;br /&gt;
    window.__timeWidgetsIntervalId = setInterval(updateAll, 1000);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function initAll() {&lt;br /&gt;
    buildBarsIfPossible();&lt;br /&gt;
    updateAll();&lt;br /&gt;
    startTickerOnce();&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  $(initAll);&lt;br /&gt;
  mw.hook(&#039;wikipage.content&#039;).add(initAll);&lt;br /&gt;
  mw.hook(&#039;skin.ready&#039;).add(initAll);&lt;br /&gt;
})();&lt;br /&gt;
&lt;br /&gt;
// Floating particles&lt;br /&gt;
(function () {&lt;br /&gt;
  const canvas = document.createElement(&#039;canvas&#039;);&lt;br /&gt;
  canvas.id = &#039;particle-canvas&#039;;&lt;br /&gt;
  document.body.prepend(canvas);&lt;br /&gt;
&lt;br /&gt;
  const ctx = canvas.getContext(&#039;2d&#039;);&lt;br /&gt;
  const particles = [];&lt;br /&gt;
  const COUNT = 80;&lt;br /&gt;
&lt;br /&gt;
  const COLORS = [&lt;br /&gt;
    &#039;rgba(0, 53, 102,  0.9)&#039;,   // #003566&lt;br /&gt;
    &#039;rgba(0, 29,  61,  0.8)&#039;,   // #001d3d&lt;br /&gt;
    &#039;rgba(0,  8,  20,  0.7)&#039;,   // #000814&lt;br /&gt;
    &#039;rgba(255, 255, 255, 0.15)&#039;, // subtle white glint&lt;br /&gt;
  ];&lt;br /&gt;
&lt;br /&gt;
  function resize() {&lt;br /&gt;
    canvas.width  = window.innerWidth;&lt;br /&gt;
    canvas.height = window.innerHeight;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function randomBetween(a, b) {&lt;br /&gt;
    return a + Math.random() * (b - a);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function createParticle() {&lt;br /&gt;
    return {&lt;br /&gt;
      x:       randomBetween(0, canvas.width),&lt;br /&gt;
      y:       randomBetween(canvas.height * 0.2, canvas.height),&lt;br /&gt;
      radius:  randomBetween(1.5, 5),&lt;br /&gt;
      color:   COLORS[Math.floor(Math.random() * COLORS.length)],&lt;br /&gt;
      speedY:  randomBetween(0.2, 0.7),&lt;br /&gt;
      speedX:  randomBetween(-0.2, 0.2),&lt;br /&gt;
      opacity: 0,&lt;br /&gt;
      fadeIn:  randomBetween(0.003, 0.008),&lt;br /&gt;
      life:    randomBetween(0.4, 1),   // 0–1, fades out after 0.8&lt;br /&gt;
    };&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  for (let i = 0; i &amp;lt; COUNT; i++) {&lt;br /&gt;
    const p = createParticle();&lt;br /&gt;
    p.y = randomBetween(0, canvas.height); // spread on init&lt;br /&gt;
    p.opacity = randomBetween(0, 1);&lt;br /&gt;
    particles.push(p);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function draw() {&lt;br /&gt;
    ctx.clearRect(0, 0, canvas.width, canvas.height);&lt;br /&gt;
&lt;br /&gt;
    particles.forEach((p, i) =&amp;gt; {&lt;br /&gt;
      // Fade in / out&lt;br /&gt;
      if (p.life &amp;lt; 0.8) {&lt;br /&gt;
        p.opacity = Math.min(1, p.opacity + p.fadeIn);&lt;br /&gt;
      } else {&lt;br /&gt;
        p.opacity = Math.max(0, p.opacity - p.fadeIn * 0.5);&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      p.life += 0.001;&lt;br /&gt;
      p.y    -= p.speedY;&lt;br /&gt;
      p.x    += p.speedX;&lt;br /&gt;
&lt;br /&gt;
      // Reset when off screen or fully faded&lt;br /&gt;
      if (p.y &amp;lt; -10 || p.life &amp;gt; 1.2) {&lt;br /&gt;
        particles[i] = createParticle();&lt;br /&gt;
        return;&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      ctx.beginPath();&lt;br /&gt;
      ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);&lt;br /&gt;
      ctx.globalAlpha = p.opacity;&lt;br /&gt;
      ctx.fillStyle   = p.color;&lt;br /&gt;
      ctx.fill();&lt;br /&gt;
&lt;br /&gt;
      // Soft glow&lt;br /&gt;
      ctx.shadowBlur  = 8;&lt;br /&gt;
      ctx.shadowColor = p.color;&lt;br /&gt;
      ctx.fill();&lt;br /&gt;
      ctx.shadowBlur  = 0;&lt;br /&gt;
    });&lt;br /&gt;
&lt;br /&gt;
    ctx.globalAlpha = 1;&lt;br /&gt;
    requestAnimationFrame(draw);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  resize();&lt;br /&gt;
  draw();&lt;br /&gt;
  window.addEventListener(&#039;resize&#039;, resize);&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Sonja Phone</name></author>
	</entry>
</feed>