Jump to content

MediaWiki:Common.js: Difference between revisions

From Utopia Game
No edit summary
floating particles putting back in
 
(2 intermediate revisions by one other user not shown)
Line 21: Line 21:
   const GAME_ANCHOR_YEAR = 2;        // YR2
   const GAME_ANCHOR_YEAR = 2;        // YR2
   const GAME_MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul'];
   const GAME_MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul'];
  // Tick warning threshold
  const TICK_SOON_MINUTES = 5;


   const pad2 = (n) => String(n).padStart(2, '0');
   const pad2 = (n) => String(n).padStart(2, '0');
Line 32: Line 35:
       const $firstButton = $container.find('.wiki-tab-button').first();
       const $firstButton = $container.find('.wiki-tab-button').first();
       const firstTabId = $firstButton.data('tab');
       const firstTabId = $firstButton.data('tab');
      if (!firstTabId) return;


       $firstButton.addClass('active');
       $firstButton.addClass('active');
Line 40: Line 44:
       const $button = $(this);
       const $button = $(this);
       const tabId = $button.data('tab');
       const tabId = $button.data('tab');
      if (!tabId) return;
       const $container = $button.closest('.wiki-tabs-container');
       const $container = $button.closest('.wiki-tabs-container');


Line 119: Line 125:
   function ensureWidget(bar, role, labelText) {
   function ensureWidget(bar, role, labelText) {
     if (!bar) return;
     if (!bar) return;
    // already present?
     if (bar.querySelector(`[data-role="${role}"]`)) return;
     if (bar.querySelector(`[data-role="${role}"]`)) return;


     const wrap = document.createElement('span');
     const wrap = document.createElement('span');
     wrap.className = 'header-widget';
     wrap.className = 'header-widget';
    wrap.dataset.widget = role;


     if (labelText) {
     if (labelText) {
Line 186: Line 189:


   function updateAll() {
   function updateAll() {
  const now = Date.now();
    const now = Date.now();


  // --- CLOCK ---
    // --- CLOCK ---
  document.querySelectorAll('[data-role="clock"]').forEach((el) => {
    document.querySelectorAll('[data-role="clock"]').forEach((el) => {
    el.textContent = clockFmt.format(new Date(now));
      el.textContent = clockFmt.format(new Date(now));
  });
    });


     // --- GAME DATE (Tick warning 45 minutes before the hour) ---
     // --- GAME DATE (Tick warning 5 minutes before the hour) ---
  const msUntilNextHour = 3600000 - (now % 3600000);
    const msUntilNextHour = 3600000 - (now % 3600000);
  const thresholdMs = 45 * 60 * 1000;
    const thresholdMs = TICK_SOON_MINUTES * 60 * 1000;
  const shouldWarn = msUntilNextHour > 0 && msUntilNextHour <= thresholdMs;
    const shouldWarn = msUntilNextHour > 0 && msUntilNextHour <= thresholdMs;
    const minutesLeft = Math.ceil(msUntilNextHour / 60000);


  const minutesLeft = Math.ceil(msUntilNextHour / 60000);
    document.querySelectorAll('[data-role="game"]').forEach((el) => {
      if (shouldWarn) {
        el.textContent = `TICK SOON (${minutesLeft}m)`;
        el.classList.add('pulse-red');
      } else {
        el.textContent = computeGameString(now);
        el.classList.remove('pulse-red');
      }
    });


  document.querySelectorAll('[data-role="game"]').forEach((el) => {
    // --- COUNTDOWN ---
    if (shouldWarn) {
    document.querySelectorAll('[data-role="countdown"]').forEach((el) => {
       el.textContent = `TICK SOON (${minutesLeft}m)`;
       el.textContent = formatRemaining(COUNTDOWN_TARGET_UTC - now);
      el.classList.add("pulse-red");
    } else {
      el.textContent = computeGameString(now);
      el.classList.remove("pulse-red");
     });
     });
 
   }
 
   // --- COUNTDOWN ---
  document.querySelectorAll('[data-role="countdown"]').forEach((el) => {
    el.textContent = formatRemaining(COUNTDOWN_TARGET_UTC - now);
  });
}
 
 
 
  // --- COUNTDOWN ---
  document.querySelectorAll('[data-role="countdown"]').forEach((el) => {
    el.textContent = formatRemaining(COUNTDOWN_TARGET_UTC - now);
  });
}


   /* =========================================================
   /* =========================================================
     Build + Boot (FIXED)
     Build + Boot
     ========================================================= */
     ========================================================= */
   function buildBarsIfPossible() {
   function buildBarsIfPossible() {
Line 231: Line 225:
     const mainBar = getMainBar();
     const mainBar = getMainBar();


    // Sticky: Discord is separate (left), widgets in bar
     if (stickyBar) {
     if (stickyBar) {
       ensureWidget(stickyBar, 'clock', 'UTC:');
       ensureWidget(stickyBar, 'clock', 'UTC:');
Line 238: Line 231:
     }
     }


    // Main: Discord is inside the bar (first), then widgets
     if (mainBar) {
     if (mainBar) {
       ensureWidget(mainBar, 'clock', 'UTC:');
       ensureWidget(mainBar, 'clock', 'UTC:');
Line 244: Line 236:
       ensureWidget(mainBar, 'countdown', 'Age ends in:');
       ensureWidget(mainBar, 'countdown', 'Age ends in:');
     }
     }
    updateAll();
   }
   }


   function startTickerOnce() {
   function startTickerOnce() {
     if (window.__timeWidgetsIntervalId) return; // already ticking
     if (window.__timeWidgetsIntervalId) return;
     updateAll();
     updateAll();
     window.__timeWidgetsIntervalId = setInterval(updateAll, 1000);
     window.__timeWidgetsIntervalId = setInterval(updateAll, 1000);
Line 256: Line 246:
   function initAll() {
   function initAll() {
     buildBarsIfPossible();
     buildBarsIfPossible();
    updateAll();
     startTickerOnce();
     startTickerOnce();
   }
   }


  // Initial page load
   $(initAll);
   $(initAll);
  mw.hook('wikipage.content').add(initAll);
  mw.hook('skin.ready').add(initAll);
})();


  // Vector 2022 navigation/content swaps
// Floating particles
   mw.hook('wikipage.content').add(initAll);
(function () {
   mw.hook('skin.ready').add(initAll); // harmless extra safety
   const canvas = document.createElement('canvas');
  canvas.id = 'particle-canvas';
  document.body.prepend(canvas);
 
  const ctx = canvas.getContext('2d');
   const particles = [];
  const COUNT = 80;
 
  const COLORS = [
    'rgba(0, 53, 102,  0.9)',  // #003566
    'rgba(0, 29,  61,  0.8)',  // #001d3d
    'rgba(0,  8,  20,  0.7)',  // #000814
    'rgba(255, 255, 255, 0.15)', // subtle white glint
  ];
 
  function resize() {
    canvas.width  = window.innerWidth;
    canvas.height = window.innerHeight;
  }
 
  function randomBetween(a, b) {
    return a + Math.random() * (b - a);
  }
 
  function createParticle() {
    return {
      x:      randomBetween(0, canvas.width),
      y:      randomBetween(canvas.height * 0.2, canvas.height),
      radius:  randomBetween(1.5, 5),
      color:  COLORS[Math.floor(Math.random() * COLORS.length)],
      speedY:  randomBetween(0.2, 0.7),
      speedX:  randomBetween(-0.2, 0.2),
      opacity: 0,
      fadeIn:  randomBetween(0.003, 0.008),
      life:    randomBetween(0.4, 1),  // 0–1, fades out after 0.8
    };
  }
 
  for (let i = 0; i < COUNT; i++) {
    const p = createParticle();
    p.y = randomBetween(0, canvas.height); // spread on init
    p.opacity = randomBetween(0, 1);
    particles.push(p);
  }
 
  function draw() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
 
    particles.forEach((p, i) => {
      // Fade in / out
      if (p.life < 0.8) {
        p.opacity = Math.min(1, p.opacity + p.fadeIn);
      } else {
        p.opacity = Math.max(0, p.opacity - p.fadeIn * 0.5);
      }
 
      p.life += 0.001;
      p.y    -= p.speedY;
      p.x    += p.speedX;
 
      // Reset when off screen or fully faded
      if (p.y < -10 || p.life > 1.2) {
        particles[i] = createParticle();
        return;
      }
 
      ctx.beginPath();
      ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
      ctx.globalAlpha = p.opacity;
      ctx.fillStyle  = p.color;
      ctx.fill();
 
      // Soft glow
      ctx.shadowBlur  = 8;
      ctx.shadowColor = p.color;
      ctx.fill();
      ctx.shadowBlur  = 0;
    });
 
    ctx.globalAlpha = 1;
    requestAnimationFrame(draw);
  }


  resize();
  draw();
  window.addEventListener('resize', resize);
})();
})();

Latest revision as of 02:55, 21 February 2026

/* Any JavaScript here will be loaded for all users on every page load. */
(function () {
  'use strict';

  /* =========================================================
     CONFIG
     ========================================================= */
  const DISCORD_URL = 'https://discord.gg/t2Rp2dRvze';

  // Countdown target: Sat, 18 Apr 2026 00:00 UTC
  const COUNTDOWN_TARGET_UTC = Date.parse('2026-04-18T00:00:00Z');

  // Clock timezone (same for everyone)
  const CLOCK_TIMEZONE = 'Etc/UTC';

  // Game time anchor (real UTC -> game time)
  // At 2026-02-19 14:00:00 UTC, game time was Jan 21 YR2
  const GAME_ANCHOR_REAL_UTC = Date.parse('2026-02-19T14:00:00Z');
  const GAME_ANCHOR_MONTH_INDEX = 0; // Jan
  const GAME_ANCHOR_DAY = 21;        // 1..24
  const GAME_ANCHOR_YEAR = 2;        // YR2
  const GAME_MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul'];

  // Tick warning threshold
  const TICK_SOON_MINUTES = 5;

  const pad2 = (n) => String(n).padStart(2, '0');

  /* =========================================================
     Tabs (your existing code)
     ========================================================= */
  $(function () {
    $('.wiki-tabs-container').each(function () {
      const $container = $(this);
      const $firstButton = $container.find('.wiki-tab-button').first();
      const firstTabId = $firstButton.data('tab');
      if (!firstTabId) return;

      $firstButton.addClass('active');
      $container.find('#' + firstTabId).addClass('active').show();
    });

    $('.wiki-tab-button').on('click', function () {
      const $button = $(this);
      const tabId = $button.data('tab');
      if (!tabId) return;

      const $container = $button.closest('.wiki-tabs-container');

      $container.find('.wiki-tab-button').removeClass('active');
      $container.find('.wiki-tab-pane').removeClass('active').hide();

      $button.addClass('active');
      $container.find('#' + tabId).addClass('active').show();
    });
  });

  /* =========================================================
     Helpers: find/create containers in BOTH headers
     ========================================================= */
  function ensureDiscordLink(container, id) {
    if (!container) return null;
    const existing = document.getElementById(id);
    if (existing) return existing;

    const link = document.createElement('a');
    link.id = id;
    link.href = DISCORD_URL;
    link.target = '_blank';
    link.className =
      'cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet sticky-discord-link';
    link.textContent = 'Join Us on Discord!';
    link.style.fontWeight = 'bold';

    container.prepend(link);
    return link;
  }

  // Sticky header: insert our widget bar inside sticky icons row
  function getStickyBar() {
    const icons = document.querySelector('.vector-sticky-header-icons');
    if (!icons) return null;

    let bar = document.getElementById('sticky-time-widgets');
    if (bar) return bar;

    // Keep Discord where it was (far-left in sticky icons)
    const discord = ensureDiscordLink(icons, 'custom-sticky-link');

    bar = document.createElement('span');
    bar.id = 'sticky-time-widgets';
    bar.className = 'time-widgets';

    // Put widgets immediately AFTER Discord (so Discord stays at the left)
    if (discord && discord.parentNode === icons) {
      discord.insertAdjacentElement('afterend', bar);
    } else {
      icons.prepend(bar);
    }

    return bar;
  }

  // Main header: put our widgets in header end area
  function getMainBar() {
    const header = document.querySelector('.vector-header');
    if (!header) return null;

    const headerEnd = header.querySelector('.vector-header-end');
    if (!headerEnd) return null;

    let bar = document.getElementById('main-time-widgets');
    if (bar) return bar;

    bar = document.createElement('span');
    bar.id = 'main-time-widgets';
    bar.className = 'time-widgets';

    headerEnd.prepend(bar);
    ensureDiscordLink(bar, 'main-discord-link');

    return bar;
  }

  function ensureWidget(bar, role, labelText) {
    if (!bar) return;
    if (bar.querySelector(`[data-role="${role}"]`)) return;

    const wrap = document.createElement('span');
    wrap.className = 'header-widget';

    if (labelText) {
      const label = document.createElement('span');
      label.className = 'header-widget__label';
      label.textContent = labelText;
      wrap.appendChild(label);
    }

    const value = document.createElement('span');
    value.className = 'header-widget__value';
    value.dataset.role = role;

    wrap.appendChild(value);
    bar.appendChild(wrap);
  }

  /* =========================================================
     Calculations / formatting
     ========================================================= */
  function formatRemaining(ms) {
    if (ms <= 0) return '00:00:00';

    const totalSeconds = Math.floor(ms / 1000);
    const days = Math.floor(totalSeconds / 86400);
    const hours = Math.floor((totalSeconds % 86400) / 3600);
    const minutes = Math.floor((totalSeconds % 3600) / 60);
    const seconds = totalSeconds % 60;

    if (days > 0) return `${days}d ${pad2(hours)}:${pad2(minutes)}:${pad2(seconds)}`;
    return `${pad2(hours)}:${pad2(minutes)}:${pad2(seconds)}`;
  }

  function computeGameString(nowMs) {
    const hoursPassed = Math.floor((nowMs - GAME_ANCHOR_REAL_UTC) / 3600000);

    // Day increments every hour (1..24)
    const dayIndex = (GAME_ANCHOR_DAY - 1) + hoursPassed;
    const dayNumber = ((dayIndex % 24) + 24) % 24 + 1;

    // Month increments every 24 hours (Jan..Jul cycle)
    const daysPassed = Math.floor(dayIndex / 24);
    const totalMonthIndex = GAME_ANCHOR_MONTH_INDEX + daysPassed;
    const monthIndex = ((totalMonthIndex % 7) + 7) % 7;

    // Year increments every 7 real days
    const yearsPassed = Math.floor(totalMonthIndex / 7);
    const year = GAME_ANCHOR_YEAR + yearsPassed;

    return `Current Game Date: ${GAME_MONTHS[monthIndex]} ${dayNumber} YR${year}`;
  }

  const clockFmt = new Intl.DateTimeFormat('en-GB', {
    timeZone: CLOCK_TIMEZONE,
    hour: '2-digit',
    minute: '2-digit',
    second: '2-digit',
    hour12: false
  });

  function updateAll() {
    const now = Date.now();

    // --- CLOCK ---
    document.querySelectorAll('[data-role="clock"]').forEach((el) => {
      el.textContent = clockFmt.format(new Date(now));
    });

    // --- GAME DATE (Tick warning 5 minutes before the hour) ---
    const msUntilNextHour = 3600000 - (now % 3600000);
    const thresholdMs = TICK_SOON_MINUTES * 60 * 1000;
    const shouldWarn = msUntilNextHour > 0 && msUntilNextHour <= thresholdMs;
    const minutesLeft = Math.ceil(msUntilNextHour / 60000);

    document.querySelectorAll('[data-role="game"]').forEach((el) => {
      if (shouldWarn) {
        el.textContent = `TICK SOON (${minutesLeft}m)`;
        el.classList.add('pulse-red');
      } else {
        el.textContent = computeGameString(now);
        el.classList.remove('pulse-red');
      }
    });

    // --- COUNTDOWN ---
    document.querySelectorAll('[data-role="countdown"]').forEach((el) => {
      el.textContent = formatRemaining(COUNTDOWN_TARGET_UTC - now);
    });
  }

  /* =========================================================
     Build + Boot
     ========================================================= */
  function buildBarsIfPossible() {
    const stickyBar = getStickyBar();
    const mainBar = getMainBar();

    if (stickyBar) {
      ensureWidget(stickyBar, 'clock', 'UTC:');
      ensureWidget(stickyBar, 'game', null);
      ensureWidget(stickyBar, 'countdown', 'Age ends in:');
    }

    if (mainBar) {
      ensureWidget(mainBar, 'clock', 'UTC:');
      ensureWidget(mainBar, 'game', null);
      ensureWidget(mainBar, 'countdown', 'Age ends in:');
    }
  }

  function startTickerOnce() {
    if (window.__timeWidgetsIntervalId) return;
    updateAll();
    window.__timeWidgetsIntervalId = setInterval(updateAll, 1000);
  }

  function initAll() {
    buildBarsIfPossible();
    updateAll();
    startTickerOnce();
  }

  $(initAll);
  mw.hook('wikipage.content').add(initAll);
  mw.hook('skin.ready').add(initAll);
})();

// Floating particles
(function () {
  const canvas = document.createElement('canvas');
  canvas.id = 'particle-canvas';
  document.body.prepend(canvas);

  const ctx = canvas.getContext('2d');
  const particles = [];
  const COUNT = 80;

  const COLORS = [
    'rgba(0, 53, 102,  0.9)',   // #003566
    'rgba(0, 29,  61,  0.8)',   // #001d3d
    'rgba(0,  8,  20,  0.7)',   // #000814
    'rgba(255, 255, 255, 0.15)', // subtle white glint
  ];

  function resize() {
    canvas.width  = window.innerWidth;
    canvas.height = window.innerHeight;
  }

  function randomBetween(a, b) {
    return a + Math.random() * (b - a);
  }

  function createParticle() {
    return {
      x:       randomBetween(0, canvas.width),
      y:       randomBetween(canvas.height * 0.2, canvas.height),
      radius:  randomBetween(1.5, 5),
      color:   COLORS[Math.floor(Math.random() * COLORS.length)],
      speedY:  randomBetween(0.2, 0.7),
      speedX:  randomBetween(-0.2, 0.2),
      opacity: 0,
      fadeIn:  randomBetween(0.003, 0.008),
      life:    randomBetween(0.4, 1),   // 0–1, fades out after 0.8
    };
  }

  for (let i = 0; i < COUNT; i++) {
    const p = createParticle();
    p.y = randomBetween(0, canvas.height); // spread on init
    p.opacity = randomBetween(0, 1);
    particles.push(p);
  }

  function draw() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    particles.forEach((p, i) => {
      // Fade in / out
      if (p.life < 0.8) {
        p.opacity = Math.min(1, p.opacity + p.fadeIn);
      } else {
        p.opacity = Math.max(0, p.opacity - p.fadeIn * 0.5);
      }

      p.life += 0.001;
      p.y    -= p.speedY;
      p.x    += p.speedX;

      // Reset when off screen or fully faded
      if (p.y < -10 || p.life > 1.2) {
        particles[i] = createParticle();
        return;
      }

      ctx.beginPath();
      ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
      ctx.globalAlpha = p.opacity;
      ctx.fillStyle   = p.color;
      ctx.fill();

      // Soft glow
      ctx.shadowBlur  = 8;
      ctx.shadowColor = p.color;
      ctx.fill();
      ctx.shadowBlur  = 0;
    });

    ctx.globalAlpha = 1;
    requestAnimationFrame(draw);
  }

  resize();
  draw();
  window.addEventListener('resize', resize);
})();