Jump to content

MediaWiki:Common.js: Difference between revisions

From Utopia Game
No edit summary
Tag: Reverted
floating particles putting back in
 
(39 intermediate revisions by 2 users not shown)
Line 1: Line 1:
/* Any JavaScript here will be loaded for all users on every page load. */
/* Any JavaScript here will be loaded for all users on every page load. */
(function () {
  'use strict';


// Tab functionality
  /* =========================================================
$(document).ready(function() {
    CONFIG
    // Initialize tabs - show first tab by default
    ========================================================= */
     $('.wiki-tabs-container').each(function() {
  const DISCORD_URL = 'https://discord.gg/t2Rp2dRvze';
        var $container = $(this);
 
        var $firstButton = $container.find('.wiki-tab-button').first();
  // Countdown target: Sat, 18 Apr 2026 00:00 UTC
        var firstTabId = $firstButton.data('tab');
  const COUNTDOWN_TARGET_UTC = Date.parse('2026-04-18T00:00:00Z');
       
 
        $firstButton.addClass('active');
  // Clock timezone (same for everyone)
        $container.find('#' + firstTabId).addClass('active').show();
  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();
     });
     });
   
    // Tab click handler
    $('.wiki-tab-button').on('click', function() {
        var $button = $(this);
        var tabId = $button.data('tab');
        var $container = $button.closest('.wiki-tabs-container');
       
        // Remove active class from all buttons and panes in this container
        $container.find('.wiki-tab-button').removeClass('active');
        $container.find('.wiki-tab-pane').removeClass('active').hide();
       
        // Add active class to clicked button and corresponding pane
        $button.addClass('active');
        $container.find('#' + tabId).addClass('active').show();
    });
});


// ===============================
    $('.wiki-tab-button').on('click', function () {
// Add Custom Sticky Header Link
      const $button = $(this);
// ===============================
      const tabId = $button.data('tab');
mw.hook('wikipage.content').add(function () {
      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();


    const icons = document.querySelector('.vector-sticky-header-icons');
      $button.addClass('active');
     if (!icons) return;
      $container.find('#' + tabId).addClass('active').show();
     });
  });


    // Prevent duplicate button on navigation
  /* =========================================================
     if (document.getElementById('custom-sticky-link')) return;
    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');
     const link = document.createElement('a');
     link.id = 'custom-sticky-link';
     link.id = id;
     link.href = 'https://discord.gg/t2Rp2dRvze'; // CHANGE THIS
     link.href = DISCORD_URL;
     link.target = '_blank';
     link.target = '_blank';
     link.className = 'cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet';
     link.className =
     link.textContent = 'Join Us on Discord!'; // CHANGE TEXT
      'cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet sticky-discord-link';
    link.style.marginLeft = '10px';
     link.textContent = 'Join Us on Discord!';
     link.style.fontWeight = 'bold';
     link.style.fontWeight = 'bold';


     icons.prepend(link); // use appendChild() if you want it at the end
     container.prepend(link);
});
    return link;
  }


// ===============================
  // Sticky header: insert our widget bar inside sticky icons row
// Sticky Header Countdown Timer (Vector 2022) - to Apr 18, 2026 00:00 UTC
  function getStickyBar() {
// ===============================
mw.hook('wikipage.content').add(function () {
     const icons = document.querySelector('.vector-sticky-header-icons');
     const icons = document.querySelector('.vector-sticky-header-icons');
     if (!icons) return;
     if (!icons) return null;


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


     // Target: Sat, 18 Apr 2026 at 00:00 GMT+00:00 (UTC)
     // Keep Discord where it was (far-left in sticky icons)
     const target = new Date('2026-04-18T00:00:00Z');
     const discord = ensureDiscordLink(icons, 'custom-sticky-link');


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


     const label = document.createElement('span');
     // Put widgets immediately AFTER Discord (so Discord stays at the left)
     label.className = 'sticky-countdown__label';
    if (discord && discord.parentNode === icons) {
     label.textContent = 'Ends in:';
      discord.insertAdjacentElement('afterend', bar);
     } else {
      icons.prepend(bar);
     }


     const value = document.createElement('span');
     return bar;
    value.className = 'sticky-countdown__value';
  }
    value.textContent = '--:--:--';


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


     // Put it before the default icons (Talk/History/Edit)
     const headerEnd = header.querySelector('.vector-header-end');
     icons.prepend(wrap);
     if (!headerEnd) return null;


     const pad = (n) => String(n).padStart(2, '0');
     let bar = document.getElementById('main-time-widgets');
    if (bar) return bar;


     const formatRemaining = (ms) => {
     bar = document.createElement('span');
        if (ms <= 0) return '00:00:00';
    bar.id = 'main-time-widgets';
        const totalSeconds = Math.floor(ms / 1000);
    bar.className = 'time-widgets';


        const days = Math.floor(totalSeconds / 86400);
    headerEnd.prepend(bar);
        const hours = Math.floor((totalSeconds % 86400) / 3600);
    ensureDiscordLink(bar, 'main-discord-link');
        const minutes = Math.floor((totalSeconds % 3600) / 60);
        const seconds = totalSeconds % 60;


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


    const update = () => {
  function ensureWidget(bar, role, labelText) {
        const now = new Date();
    if (!bar) return;
        const diff = target.getTime() - now.getTime();
    if (bar.querySelector(`[data-role="${role}"]`)) return;
        value.textContent = formatRemaining(diff);
        wrap.classList.toggle('is-expired', diff <= 0);
    };


     update();
     const wrap = document.createElement('span');
     const timerId = window.setInterval(update, 1000);
     wrap.className = 'header-widget';


     // Cleanup if element removed (rare, but safe)
     if (labelText) {
    const observer = new MutationObserver(() => {
      const label = document.createElement('span');
        if (!document.getElementById('sticky-countdown')) {
      label.className = 'header-widget__label';
            clearInterval(timerId);
      label.textContent = labelText;
            observer.disconnect();
      wrap.appendChild(label);
        }
    }
    });
    observer.observe(document.body, { childList: true, subtree: true });
});


// ===============================
    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);
// Sticky Header Game Time
    const days = Math.floor(totalSeconds / 86400);
// Display: "Mar 1 YR1" (anchored at install time)
    const hours = Math.floor((totalSeconds % 86400) / 3600);
// Rules:
     const minutes = Math.floor((totalSeconds % 3600) / 60);
// - Each real hour = +1 in-game day number
     const seconds = totalSeconds % 60;
// - Each 24 hours = +1 in-game month (Jan..Jul via 7-day cycle)
// - Each 7 months/days = +1 in-game year
// ===============================
mw.hook('wikipage.content').add(function () {
     const icons = document.querySelector('.vector-sticky-header-icons');
     if (!icons) return;


     if (document.getElementById('sticky-game-time')) return;
     if (days > 0) return `${days}d ${pad2(hours)}:${pad2(minutes)}:${pad2(seconds)}`;
    return `${pad2(hours)}:${pad2(minutes)}:${pad2(seconds)}`;
  }


     const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul'];
  function computeGameString(nowMs) {
     const hoursPassed = Math.floor((nowMs - GAME_ANCHOR_REAL_UTC) / 3600000);


     // --------- ANCHOR: "right now" is Mar 1 YR1 ----------
     // Day increments every hour (1..24)
     const ANCHOR_MONTH_INDEX = 2; // Mar (Jan=0)
     const dayIndex = (GAME_ANCHOR_DAY - 1) + hoursPassed;
    const ANCHOR_DAY = 1;         // Day 1
     const dayNumber = ((dayIndex % 24) + 24) % 24 + 1;
     const ANCHOR_YEAR = 1;       // YR1


     // Optional timezone shift (0 = UTC). If you want server/local behavior, tell me your target TZ.
     // Month increments every 24 hours (Jan..Jul cycle)
     const GAME_TZ_OFFSET_HOURS = 0;
    const daysPassed = Math.floor(dayIndex / 24);
    const totalMonthIndex = GAME_ANCHOR_MONTH_INDEX + daysPassed;
     const monthIndex = ((totalMonthIndex % 7) + 7) % 7;


     const getShiftedNow = () => new Date(Date.now() + GAME_TZ_OFFSET_HOURS * 3600000);
    // Year increments every 7 real days
     const yearsPassed = Math.floor(totalMonthIndex / 7);
    const year = GAME_ANCHOR_YEAR + yearsPassed;


     // Anchor to the current real-hour boundary so changes happen cleanly on the hour
     return `Current Game Date: ${GAME_MONTHS[monthIndex]} ${dayNumber} YR${year}`;
    const now = getShiftedNow();
  }
    const anchorRealHour = new Date(Date.UTC(
        now.getUTCFullYear(),
        now.getUTCMonth(),
        now.getUTCDate(),
        now.getUTCHours(), 0, 0, 0
    ));


    // Build UI pill
  const clockFmt = new Intl.DateTimeFormat('en-GB', {
    const wrap = document.createElement('span');
    timeZone: CLOCK_TIMEZONE,
     wrap.id = 'sticky-game-time';
     hour: '2-digit',
     wrap.className = 'sticky-game-time';
    minute: '2-digit',
     second: '2-digit',
    hour12: false
  });


     const value = document.createElement('span');
  function updateAll() {
    value.className = 'sticky-game-time__value';
     const now = Date.now();


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


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


        // Total whole hours passed since anchor
    document.querySelectorAll('[data-role="game"]').forEach((el) => {
        const hoursPassed = Math.floor((t.getTime() - anchorRealHour.getTime()) / 3600000);
      if (shouldWarn) {
        el.textContent = `TICK SOON (${minutesLeft}m)`;
        el.classList.add('pulse-red');
      } else {
        el.textContent = computeGameString(now);
        el.classList.remove('pulse-red');
      }
    });


        // Day number advances each hour starting from ANCHOR_DAY
    // --- COUNTDOWN ---
        const dayIndexFromAnchor = (ANCHOR_DAY - 1) + hoursPassed; // 0-based
    document.querySelectorAll('[data-role="countdown"]').forEach((el) => {
        const dayNumber = (dayIndexFromAnchor % 24) + 1;
      el.textContent = formatRemaining(COUNTDOWN_TARGET_UTC - now);
 
    });
        // Every 24 hours, advance the "month/day-of-week" cycle
  }
        const daysPassed = Math.floor(dayIndexFromAnchor / 24); // 0..∞


        // Total "day-of-week" index (0..6) across years
  /* =========================================================
        const totalDayOfWeekIndex =
    Build + Boot
            (ANCHOR_YEAR - 1) * 7 + ANCHOR_MONTH_INDEX + daysPassed;
    ========================================================= */
  function buildBarsIfPossible() {
    const stickyBar = getStickyBar();
    const mainBar = getMainBar();


        const year = Math.floor(totalDayOfWeekIndex / 7) + 1;
    if (stickyBar) {
        const month = months[((totalDayOfWeekIndex % 7) + 7) % 7];
      ensureWidget(stickyBar, 'clock', 'UTC:');
 
      ensureWidget(stickyBar, 'game', null);
        return `${month} ${dayNumber} YR${year}`;
      ensureWidget(stickyBar, 'countdown', 'Age ends in:');
     }
     }


     function update() {
     if (mainBar) {
        value.textContent = computeGameString();
      ensureWidget(mainBar, 'clock', 'UTC:');
      ensureWidget(mainBar, 'game', null);
      ensureWidget(mainBar, 'countdown', 'Age ends in:');
     }
     }
  }


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


    // Update exactly on the next hour boundary, then every hour
  function initAll() {
     const now2 = getShiftedNow();
     buildBarsIfPossible();
     const msUntilNextHour =
     updateAll();
        3600000 - (now2.getUTCMinutes() * 60000 + now2.getUTCSeconds() * 1000 + now2.getUTCMilliseconds());
    startTickerOnce();
  }


    setTimeout(function () {
  $(initAll);
        update();
  mw.hook('wikipage.content').add(initAll);
        setInterval(update, 3600000);
  mw.hook('skin.ready').add(initAll);
    }, msUntilNextHour);
})();
});


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


// ===============================
  const ctx = canvas.getContext('2d');
// Sticky Header Clock (timezone-stable)
  const particles = [];
// Shows the same time for everyone by forcing a timezone (UTC by default)
  const COUNT = 80;
// ===============================
mw.hook('wikipage.content').add(function () {
    const icons = document.querySelector('.vector-sticky-header-icons');
    if (!icons) return;


     if (document.getElementById('sticky-clock')) return;
  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
  ];


    // Choose the clock timezone:
  function resize() {
    // - "Etc/UTC" for UTC (same for all users)
     canvas.width  = window.innerWidth;
     // - Or "America/New_York" (same for all users, displayed in NY time)
     canvas.height = window.innerHeight;
     const CLOCK_TIMEZONE = 'Etc/UTC'; // <-- change if you want a specific zone
  }


    const wrap = document.createElement('span');
  function randomBetween(a, b) {
     wrap.id = 'sticky-clock';
     return a + Math.random() * (b - a);
    wrap.className = 'sticky-clock';
  }


     const label = document.createElement('span');
  function createParticle() {
    label.className = 'sticky-clock__label';
     return {
    label.textContent = (CLOCK_TIMEZONE === 'Etc/UTC') ? 'UTC:' : 'Time:';
      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
    };
  }


     const value = document.createElement('span');
  for (let i = 0; i < COUNT; i++) {
     value.className = 'sticky-clock__value';
     const p = createParticle();
    p.y = randomBetween(0, canvas.height); // spread on init
     p.opacity = randomBetween(0, 1);
    particles.push(p);
  }


    wrap.appendChild(label);
  function draw() {
     wrap.appendChild(value);
     ctx.clearRect(0, 0, canvas.width, canvas.height);


     // Put it near the front (use appendChild to put it at the end)
     particles.forEach((p, i) => {
    icons.prepend(wrap);
      // 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);
      }


    const fmt = new Intl.DateTimeFormat('en-GB', {
      p.life += 0.001;
        timeZone: CLOCK_TIMEZONE,
      p.y    -= p.speedY;
        hour: '2-digit',
      p.x    += p.speedX;
        minute: '2-digit',
        second: '2-digit',
        hour12: false
    });


    function updateClock() {
      // Reset when off screen or fully faded
         value.textContent = fmt.format(new Date());
      if (p.y < -10 || p.life > 1.2) {
    }
         particles[i] = createParticle();
        return;
      }


    updateClock();
      ctx.beginPath();
    setInterval(updateClock, 1000);
      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;
* Utopia Wiki Header Animations - MediaWiki Compatible
    requestAnimationFrame(draw);
* Direct targeting approach for MediaWiki 1.45+
  }
*/


$(document).ready(function() {
  resize();
    'use strict';
  draw();
   
  window.addEventListener('resize', resize);
    // Find the image first as our anchor point
})();
    var $logo = $('img[src*="Utopiawiki"]').first();
   
    if ($logo.length === 0) {
        console.log('Utopia logo not found');
        return;
    }
   
    console.log('Utopia animations: Logo found, initializing...');
   
    // Find the container - go up until we find the div with padding
    var $container = $logo.closest('div[style*="padding"]');
   
    if ($container.length === 0) {
        // Try alternative: find parent div
        $container = $logo.parent().parent();
    }
   
    console.log('Container found:', $container.length);
   
    // Wrap everything in our animation container if not already wrapped
    if (!$container.hasClass('utopia-header-container')) {
        $container.addClass('utopia-header-container');
        $container.css({
            'position': 'relative',
            'overflow': 'hidden',
            'background': 'linear-gradient(135deg, rgba(15, 15, 35, 0.95) 0%, rgba(30, 30, 60, 0.95) 100%)',
            'border-radius': '12px',
            'box-shadow': '0 8px 32px rgba(0, 0, 0, 0.4)',
            'padding': '3em 2em'
        });
    }
   
    // Style the logo
    $logo.addClass('utopia-logo').css({
        'filter': 'drop-shadow(0 4px 12px rgba(255, 195, 0, 0.3))',
        'transition': 'transform 0.3s ease, filter 0.3s ease'
    });
   
    // Find and style text divs
    $container.find('div').each(function() {
        var $div = $(this);
        var text = $div.text().trim();
       
        if (text.includes('Comprehensive Guide')) {
            $div.addClass('utopia-subtitle');
            console.log('Added subtitle class');
        } else if (text.includes('Welcome to')) {
            $div.addClass('utopia-welcome');
            console.log('Added welcome class');
        } else if (text.includes('Age')) {
            $div.addClass('utopia-ages');
            console.log('Added ages class');
        }
    });
   
    // Style all links in the ages section
    $container.find('a').each(function(index) {
        var $link = $(this);
        $link.addClass('age-link');
       
        // First link is current
        if (index === 0) {
            $link.addClass('age-current');
        }
       
        // Wrap in a styled container for better effect
        $link.css({
            'display': 'inline-block',
            'padding': '0.8em 1.5em',
            'margin': '0.5em',
            'background': 'linear-gradient(135deg, rgba(255, 195, 0, 0.1) 0%, rgba(255, 195, 0, 0.05) 100%)',
            'border': '2px solid rgba(255, 195, 0, 0.3)',
            'border-radius': '8px',
            'transition': 'all 0.3s ease',
            'position': 'relative'
        });
    });
   
    // Add hover effects to links
    $container.on('mouseenter', '.age-link', function() {
        $(this).css({
            'transform': 'translateY(-4px)',
            'border-color': 'rgba(255, 195, 0, 0.6)',
            'background': 'linear-gradient(135deg, rgba(255, 195, 0, 0.2) 0%, rgba(255, 195, 0, 0.1) 100%)',
            'box-shadow': '0 8px 24px rgba(255, 195, 0, 0.3)'
        });
       
        $(this).find('span').css({
            'text-shadow': '0 0 12px rgba(255, 195, 0, 0.8)'
        });
    }).on('mouseleave', '.age-link', function() {
        $(this).css({
            'transform': 'translateY(0)',
            'border-color': 'rgba(255, 195, 0, 0.3)',
            'background': 'linear-gradient(135deg, rgba(255, 195, 0, 0.1) 0%, rgba(255, 195, 0, 0.05) 100%)',
            'box-shadow': 'none'
        });
       
        $(this).find('span').css({
            'text-shadow': 'none'
        });
    });
   
    // Add floating particles
    addFloatingParticles($container);
   
    // Add parallax effect to logo
    addParallaxEffect($container, $logo);
   
    // Add animations
    animateElements($container);
   
    console.log('Utopia animations initialized successfully!');
   
    function addFloatingParticles($container) {
        // Create particle container
        var $particleContainer = $('<div class="utopia-particles"></div>').css({
            'position': 'absolute',
            'top': '0',
            'left': '0',
            'width': '100%',
            'height': '100%',
            'pointer-events': 'none',
            'overflow': 'hidden',
            'z-index': '1'
        });
       
        $container.prepend($particleContainer);
       
        // Create particles
        for (var i = 0; i < 15; i++) {
            createParticle($particleContainer);
        }
    }
   
    function createParticle($container) {
        var size = Math.random() * 3 + 1;
        var duration = Math.random() * 15 + 10;
        var delay = Math.random() * 5;
        var startX = Math.random() * 100;
        var endX = startX + (Math.random() * 100 - 50);
       
        var $particle = $('<div class="particle"></div>').css({
            'position': 'absolute',
            'width': size + 'px',
            'height': size + 'px',
            'background': 'rgba(255, 195, 0, ' + (Math.random() * 0.5 + 0.2) + ')',
            'border-radius': '50%',
            'left': startX + '%',
            'bottom': '-10px',
            'box-shadow': '0 0 ' + (size * 2) + 'px rgba(255, 195, 0, 0.5)',
            'opacity': '0'
        });
       
        $container.append($particle);
       
        // Animate particle
        setTimeout(function() {
            animateParticle($particle, duration, endX);
        }, delay * 1000);
    }
   
    function animateParticle($particle, duration, endX) {
        $particle.css({
            'transition': 'all ' + duration + 's linear',
            'transform': 'translateY(-100vh) translateX(' + endX + 'px) rotate(360deg)',
            'opacity': '1'
        });
       
        // Restart animation
        setTimeout(function() {
            $particle.css({
                'transition': 'none',
                'transform': 'translateY(0) translateX(0) rotate(0deg)',
                'opacity': '0'
            });
           
            setTimeout(function() {
                animateParticle($particle, duration, endX);
            }, 100);
        }, duration * 1000);
    }
   
    function addParallaxEffect($container, $logo) {
        var mouseX = 0;
        var mouseY = 0;
        var currentX = 0;
        var currentY = 0;
       
        $container.on('mousemove', function(e) {
            var offset = $container.offset();
            var width = $container.width();
            var height = $container.height();
           
            mouseX = (e.pageX - offset.left - width / 2) / width;
            mouseY = (e.pageY - offset.top - height / 2) / height;
        });
       
        $container.on('mouseleave', function() {
            mouseX = 0;
            mouseY = 0;
        });
       
        function animate() {
            currentX += (mouseX - currentX) * 0.1;
            currentY += (mouseY - currentY) * 0.1;
           
            $logo.css('transform', 'translate(' + (currentX * 20) + 'px, ' + (currentY * 20) + 'px)');
           
            requestAnimationFrame(animate);
        }
       
        animate();
    }
   
    function animateElements($container) {
        // Animate subtitle
        var $subtitle = $container.find('.utopia-subtitle');
        if ($subtitle.length) {
            $subtitle.css({
                'opacity': '0',
                'transform': 'translateY(30px)'
            });
           
            setTimeout(function() {
                $subtitle.css({
                    'transition': 'all 1s ease-out',
                    'opacity': '1',
                    'transform': 'translateY(0)',
                    'text-shadow': '0 2px 8px rgba(255, 255, 255, 0.2)'
                });
            }, 300);
        }
       
        // Animate welcome
        var $welcome = $container.find('.utopia-welcome');
        if ($welcome.length) {
            $welcome.css({
                'opacity': '0',
                'transform': 'translateY(30px)'
            });
           
            setTimeout(function() {
                $welcome.css({
                    'transition': 'all 1.2s ease-out',
                    'opacity': '1',
                    'transform': 'translateY(0)'
                });
               
                // Animate the bold Utopia text
                $welcome.find('b, strong').css({
                    'color': '#FFC300',
                    'text-shadow': '0 0 10px rgba(255, 195, 0, 0.4)'
                });
            }, 600);
        }
       
        // Animate ages
        var $ages = $container.find('.utopia-ages');
        if ($ages.length) {
            $ages.css({
                'opacity': '0',
                'transform': 'translateY(30px)'
            });
           
            setTimeout(function() {
                $ages.css({
                    'transition': 'all 1.4s ease-out',
                    'opacity': '1',
                    'transform': 'translateY(0)'
                });
            }, 900);
        }
       
        // Float animation for logo
        floatLogo($container.find('.utopia-logo'));
       
        // Pulse animation for current age
        pulseCurrentAge($container.find('.age-current'));
    }
   
    function floatLogo($logo) {
        var up = true;
       
        setInterval(function() {
            if (up) {
                $logo.css('transform', 'translateY(-15px)');
            } else {
                $logo.css('transform', 'translateY(0)');
            }
            up = !up;
        }, 3000);
       
        $logo.css('transition', 'transform 3s ease-in-out');
    }
   
    function pulseCurrentAge($current) {
        if ($current.length === 0) return;
       
        setInterval(function() {
            $current.css('box-shadow', '0 0 25px rgba(255, 195, 0, 0.6), 0 0 35px rgba(255, 195, 0, 0.3)');
           
            setTimeout(function() {
                $current.css('box-shadow', '0 0 15px rgba(255, 195, 0, 0.3)');
            }, 1000);
        }, 2000);
       
        $current.css('transition', 'box-shadow 1s ease-in-out');
    }
});

Latest revision as of 03: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);
})();