Jump to content

MediaWiki:Common.js: Difference between revisions

From Utopia Game
No edit summary
Tag: Reverted
No edit summary
 
(6 intermediate revisions by 2 users not shown)
Line 124: Line 124:
// ===============================
// ===============================
// Sticky Header Game Time
// Sticky Header Game Time
// Display: "Mar 1 YR1" (anchored at install time)
// Display: "Mar 1 YR1"
// Rules:
// Rules:
// - Each real hour = +1 in-game day number
// - Each real hour = +1 in-game day (1..24)
// - Each 24 hours = +1 in-game month (Jan..Jul via 7-day cycle)
// - Each 24 hours = +1 in-game month (Jan..Jul cycle)
// - Each 7 months/days = +1 in-game year
// - Each 7 days = +1 in-game year
// ===============================
// ===============================
mw.hook('wikipage.content').add(function () {
mw.hook('wikipage.content').add(function () {
Line 138: Line 138:
     const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul'];
     const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul'];


     // --------- ANCHOR: "right now" is Mar 1 YR1 ----------
     // --------- ANCHOR: right now is Mar 1 YR1 ----------
     const ANCHOR_MONTH_INDEX = 2; // Mar (Jan=0)
     const ANCHOR_MONTH_INDEX = 5; // Mar (Jan=0)
     const ANCHOR_DAY = 1;        // Day 1
     const ANCHOR_DAY = 14;        // 1..24
     const ANCHOR_YEAR = 1;        // YR1
     const ANCHOR_YEAR = 1;        // YR1


     // Optional timezone shift (0 = UTC). If you want server/local behavior, tell me your target TZ.
     // If you want the game to follow a fixed timezone for everyone:
    // 0 = UTC. (Use 0 unless you KNOW you want another fixed offset.)
     const GAME_TZ_OFFSET_HOURS = 0;
     const GAME_TZ_OFFSET_HOURS = 0;


     const getShiftedNow = () => new Date(Date.now() + GAME_TZ_OFFSET_HOURS * 3600000);
     const getShiftedNow = () => new Date(Date.now() + GAME_TZ_OFFSET_HOURS * 3600000);


     // Anchor to the current real-hour boundary so changes happen cleanly on the hour
     // Anchor to the current hour boundary (so it flips cleanly each hour)
     const now = getShiftedNow();
     const now = getShiftedNow();
     const anchorRealHour = new Date(Date.UTC(
     const anchorRealHour = new Date(Date.UTC(
Line 157: Line 158:
     ));
     ));


     // Build UI pill
     // UI pill
     const wrap = document.createElement('span');
     const wrap = document.createElement('span');
     wrap.id = 'sticky-game-time';
     wrap.id = 'sticky-game-time';
Line 166: Line 167:


     wrap.appendChild(value);
     wrap.appendChild(value);
    // Put it before the default icons
     icons.prepend(wrap);
     icons.prepend(wrap);


Line 171: Line 174:
         const t = getShiftedNow();
         const t = getShiftedNow();


         // Total whole hours passed since anchor
         // Whole hours passed since we declared "Mar 1 YR1"
         const hoursPassed = Math.floor((t.getTime() - anchorRealHour.getTime()) / 3600000);
         const hoursPassed = Math.floor((t.getTime() - anchorRealHour.getTime()) / 3600000);


         // Day number advances each hour starting from ANCHOR_DAY
         // Day increments each hour
         const dayIndexFromAnchor = (ANCHOR_DAY - 1) + hoursPassed; // 0-based
         const dayIndex = (ANCHOR_DAY - 1) + hoursPassed; // 0-based
         const dayNumber = (dayIndexFromAnchor % 24) + 1;
         const dayNumber = (dayIndex % 24) + 1;           // 1..24


         // Every 24 hours, advance the "month/day-of-week" cycle
         // Month increments each 24 hours
         const daysPassed = Math.floor(dayIndexFromAnchor / 24); // 0..∞
         const daysPassed = Math.floor(dayIndex / 24);   // 0..∞


         // Total "day-of-week" index (0..6) across years
         // Month+Year cycle is 7 days long
         const totalDayOfWeekIndex =
         const totalMonthIndex = (ANCHOR_YEAR - 1) * 7 + ANCHOR_MONTH_INDEX + daysPassed;
            (ANCHOR_YEAR - 1) * 7 + ANCHOR_MONTH_INDEX + daysPassed;


         const year = Math.floor(totalDayOfWeekIndex / 7) + 1;
         const year = Math.floor(totalMonthIndex / 7) + 1;
         const month = months[((totalDayOfWeekIndex % 7) + 7) % 7];
         const month = months[((totalMonthIndex % 7) + 7) % 7];


         return `${month} ${dayNumber} YR${year}`;
         return `${month} ${dayNumber} YR${year}`;
Line 195: Line 197:
     }
     }


     // Update now
     // Initial update
     update();
     update();


     // Update exactly on the next hour boundary, then every hour
     // Update exactly on next hour, then every hour
     const now2 = getShiftedNow();
     const now2 = getShiftedNow();
     const msUntilNextHour =
     const msUntilNextHour =
Line 257: Line 259:
     setInterval(updateClock, 1000);
     setInterval(updateClock, 1000);
});
});
/**
* UTOPIA WIKI HEADER - GAMING ELITE ANIMATIONS
* Sleek, professional animations for hardcore gamers
*/
(function() {
    'use strict';
   
    // Configuration
    const CONFIG = {
        particleCount: 30,
        parallaxStrength: 15,
        colors: {
            primary: '#ffc300',
            secondary: '#ffd60a',
            dark: '#000814'
        }
    };
   
    // Initialize when DOM is ready
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initUtopiaHeader);
    } else {
        initUtopiaHeader();
    }
   
    function initUtopiaHeader() {
        const header = findHeaderSection();
        if (!header) {
            console.warn('Utopia header section not found');
            return;
        }
       
        // Check for reduced motion preference
        const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
       
        if (!prefersReducedMotion) {
            initParticleSystem(header);
            initParallaxEffect(header);
            initMouseTrailEffect(header);
            initCornerAccents(header);
        }
       
        initHoverEffects(header);
        initScrollReveal(header);
    }
   
    function findHeaderSection() {
        // Look for the header section with class
        let header = document.querySelector('.utopia-header-section');
       
        // Fallback: find by image
        if (!header) {
            const logo = document.querySelector('img[alt*="Utopiawiki"], img[src*="Utopiawiki"]');
            if (logo) {
                header = logo.closest('div[style*="padding"]')?.parentElement;
            }
        }
       
        return header;
    }
   
    /**
    * Particle System - Floating tech particles
    */
    function initParticleSystem(header) {
        const canvas = document.createElement('canvas');
        canvas.style.cssText = `
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            pointer-events: none;
            z-index: 1;
        `;
        header.style.position = 'relative';
        header.insertBefore(canvas, header.firstChild);
       
        const ctx = canvas.getContext('2d');
        let particles = [];
        let animationId;
       
        function resize() {
            canvas.width = header.offsetWidth;
            canvas.height = header.offsetHeight;
            initParticles();
        }
       
        function initParticles() {
            particles = [];
            for (let i = 0; i < CONFIG.particleCount; i++) {
                particles.push(createParticle());
            }
        }
       
        function createParticle() {
            return {
                x: Math.random() * canvas.width,
                y: canvas.height + Math.random() * 100,
                size: Math.random() * 2 + 1,
                speedY: Math.random() * 0.5 + 0.3,
                speedX: (Math.random() - 0.5) * 0.3,
                opacity: Math.random() * 0.5 + 0.3,
                hue: Math.random() > 0.7 ? 45 : 42 // Gold hues
            };
        }
       
        function updateParticles() {
            particles.forEach((particle, index) => {
                particle.y -= particle.speedY;
                particle.x += particle.speedX;
               
                // Reset particle when it goes off screen
                if (particle.y < -10 || particle.x < -10 || particle.x > canvas.width + 10) {
                    particles[index] = createParticle();
                }
            });
        }
       
        function drawParticles() {
            ctx.clearRect(0, 0, canvas.width, canvas.height);
           
            particles.forEach(particle => {
                ctx.beginPath();
                ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2);
                ctx.fillStyle = `hsla(${particle.hue}, 100%, 50%, ${particle.opacity})`;
                ctx.shadowBlur = 8;
                ctx.shadowColor = `hsla(${particle.hue}, 100%, 50%, 0.8)`;
                ctx.fill();
                ctx.shadowBlur = 0;
            });
        }
       
        function animate() {
            updateParticles();
            drawParticles();
            animationId = requestAnimationFrame(animate);
        }
       
        resize();
        animate();
       
        window.addEventListener('resize', resize);
       
        // Cleanup on navigation
        const observer = new MutationObserver(() => {
            if (!document.body.contains(canvas)) {
                cancelAnimationFrame(animationId);
                observer.disconnect();
            }
        });
        observer.observe(document.body, { childList: true, subtree: true });
    }
   
    /**
    * Parallax Effect - Logo follows mouse
    */
    function initParallaxEffect(header) {
        const logo = header.querySelector('img[alt*="Utopiawiki"], img[src*="Utopiawiki"]');
        if (!logo) return;
       
        header.setAttribute('data-parallax', 'true');
       
        let mouseX = 0;
        let mouseY = 0;
        let currentX = 0;
        let currentY = 0;
       
        header.addEventListener('mousemove', (e) => {
            const rect = header.getBoundingClientRect();
            mouseX = (e.clientX - rect.left - rect.width / 2) / rect.width;
            mouseY = (e.clientY - rect.top - rect.height / 2) / rect.height;
        });
       
        header.addEventListener('mouseleave', () => {
            mouseX = 0;
            mouseY = 0;
        });
       
        function animate() {
            currentX += (mouseX - currentX) * 0.1;
            currentY += (mouseY - currentY) * 0.1;
           
            const offsetX = currentX * CONFIG.parallaxStrength;
            const offsetY = currentY * CONFIG.parallaxStrength;
           
            logo.style.transform = `translate(${offsetX}px, ${offsetY}px)`;
           
            requestAnimationFrame(animate);
        }
       
        animate();
    }
   
    /**
    * Mouse Trail Effect - Subtle glow follows cursor
    */
    function initMouseTrailEffect(header) {
        const trail = document.createElement('div');
        trail.style.cssText = `
            position: absolute;
            width: 200px;
            height: 200px;
            border-radius: 50%;
            background: radial-gradient(circle, rgba(255, 195, 0, 0.15) 0%, transparent 70%);
            pointer-events: none;
            z-index: 0;
            transition: opacity 0.3s ease;
            opacity: 0;
        `;
        header.appendChild(trail);
       
        let mouseX = 0;
        let mouseY = 0;
        let currentX = 0;
        let currentY = 0;
       
        header.addEventListener('mouseenter', () => {
            trail.style.opacity = '1';
        });
       
        header.addEventListener('mouseleave', () => {
            trail.style.opacity = '0';
        });
       
        header.addEventListener('mousemove', (e) => {
            const rect = header.getBoundingClientRect();
            mouseX = e.clientX - rect.left;
            mouseY = e.clientY - rect.top;
        });
       
        function animate() {
            currentX += (mouseX - currentX) * 0.15;
            currentY += (mouseY - currentY) * 0.15;
           
            trail.style.left = (currentX - 100) + 'px';
            trail.style.top = (currentY - 100) + 'px';
           
            requestAnimationFrame(animate);
        }
       
        animate();
    }
   
    /**
    * Corner Accents - Animated corner decorations
    */
    function initCornerAccents(header) {
        const corners = ['top-left', 'top-right', 'bottom-left', 'bottom-right'];
       
        corners.forEach((corner, index) => {
            const accent = document.createElement('div');
            const [vertical, horizontal] = corner.split('-');
           
            accent.style.cssText = `
                position: absolute;
                ${vertical}: 0;
                ${horizontal}: 0;
                width: 40px;
                height: 40px;
                border-${vertical}: 2px solid rgba(255, 195, 0, 0.3);
                border-${horizontal}: 2px solid rgba(255, 195, 0, 0.3);
                z-index: 3;
                animation: cornerPulse ${2 + index * 0.3}s ease-in-out infinite;
                animation-delay: ${index * 0.2}s;
            `;
           
            header.appendChild(accent);
        });
       
        // Add corner pulse animation
        if (!document.getElementById('utopia-corner-styles')) {
            const style = document.createElement('style');
            style.id = 'utopia-corner-styles';
            style.textContent = `
                @keyframes cornerPulse {
                    0%, 100% {
                        opacity: 0.3;
                        transform: scale(1);
                    }
                    50% {
                        opacity: 0.6;
                        transform: scale(1.1);
                    }
                }
            `;
            document.head.appendChild(style);
        }
    }
   
    /**
    * Hover Effects - Enhanced age link interactions
    */
    function initHoverEffects(header) {
        const links = header.querySelectorAll('a');
       
        links.forEach(link => {
            // Add ripple effect on click
            link.addEventListener('click', function(e) {
                const ripple = document.createElement('div');
                const rect = this.getBoundingClientRect();
                const size = Math.max(rect.width, rect.height);
                const x = e.clientX - rect.left - size / 2;
                const y = e.clientY - rect.top - size / 2;
               
                ripple.style.cssText = `
                    position: absolute;
                    left: ${x}px;
                    top: ${y}px;
                    width: ${size}px;
                    height: ${size}px;
                    border-radius: 50%;
                    background: rgba(255, 213, 10, 0.4);
                    pointer-events: none;
                    transform: scale(0);
                    animation: rippleEffect 0.6s ease-out;
                `;
               
                this.style.position = 'relative';
                this.style.overflow = 'hidden';
                this.appendChild(ripple);
               
                setTimeout(() => ripple.remove(), 600);
            });
        });
       
        // Add ripple animation if not exists
        if (!document.getElementById('utopia-ripple-styles')) {
            const style = document.createElement('style');
            style.id = 'utopia-ripple-styles';
            style.textContent = `
                @keyframes rippleEffect {
                    to {
                        transform: scale(2);
                        opacity: 0;
                    }
                }
            `;
            document.head.appendChild(style);
        }
    }
   
    /**
    * Scroll Reveal - Fade in elements on scroll
    */
    function initScrollReveal(header) {
        const observer = new IntersectionObserver((entries) => {
            entries.forEach(entry => {
                if (entry.isIntersecting) {
                    entry.target.style.opacity = '1';
                    entry.target.style.transform = 'translateY(0)';
                }
            });
        }, {
            threshold: 0.1,
            rootMargin: '0px 0px -50px 0px'
        });
       
        observer.observe(header);
    }
   
})();

Latest revision as of 02:28, 17 February 2026

/* Any JavaScript here will be loaded for all users on every page load. */

// Tab functionality
$(document).ready(function() {
    // Initialize tabs - show first tab by default
    $('.wiki-tabs-container').each(function() {
        var $container = $(this);
        var $firstButton = $container.find('.wiki-tab-button').first();
        var firstTabId = $firstButton.data('tab');
        
        $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();
    });
});

// ===============================
// Add Custom Sticky Header Link
// ===============================
mw.hook('wikipage.content').add(function () {

    const icons = document.querySelector('.vector-sticky-header-icons');
    if (!icons) return;

    // Prevent duplicate button on navigation
    if (document.getElementById('custom-sticky-link')) return;

    const link = document.createElement('a');
    link.id = 'custom-sticky-link';
    link.href = 'https://discord.gg/t2Rp2dRvze'; // CHANGE THIS
    link.target = '_blank';
    link.className = 'cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet';
    link.textContent = 'Join Us on Discord!'; // CHANGE TEXT
    link.style.marginLeft = '10px';
    link.style.fontWeight = 'bold';

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

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

    // Prevent duplicates
    if (document.getElementById('sticky-countdown')) return;

    // Target: Sat, 18 Apr 2026 at 00:00 GMT+00:00 (UTC)
    const target = new Date('2026-04-18T00:00:00Z');

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

    const label = document.createElement('span');
    label.className = 'sticky-countdown__label';
    label.textContent = 'Ends in:';

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

    wrap.appendChild(label);
    wrap.appendChild(value);

    // Put it before the default icons (Talk/History/Edit)
    icons.prepend(wrap);

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

    const 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 ${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
        return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
    };

    const update = () => {
        const now = new Date();
        const diff = target.getTime() - now.getTime();
        value.textContent = formatRemaining(diff);
        wrap.classList.toggle('is-expired', diff <= 0);
    };

    update();
    const timerId = window.setInterval(update, 1000);

    // Cleanup if element removed (rare, but safe)
    const observer = new MutationObserver(() => {
        if (!document.getElementById('sticky-countdown')) {
            clearInterval(timerId);
            observer.disconnect();
        }
    });
    observer.observe(document.body, { childList: true, subtree: true });
});

// ===============================



// ===============================
// Sticky Header Game Time
// Display: "Mar 1 YR1"
// Rules:
// - Each real hour = +1 in-game day (1..24)
// - Each 24 hours = +1 in-game month (Jan..Jul cycle)
// - Each 7 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;

    const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul'];

    // --------- ANCHOR: right now is Mar 1 YR1 ----------
    const ANCHOR_MONTH_INDEX = 5; // Mar (Jan=0)
    const ANCHOR_DAY = 14;         // 1..24
    const ANCHOR_YEAR = 1;        // YR1

    // If you want the game to follow a fixed timezone for everyone:
    // 0 = UTC. (Use 0 unless you KNOW you want another fixed offset.)
    const GAME_TZ_OFFSET_HOURS = 0;

    const getShiftedNow = () => new Date(Date.now() + GAME_TZ_OFFSET_HOURS * 3600000);

    // Anchor to the current hour boundary (so it flips cleanly each hour)
    const now = getShiftedNow();
    const anchorRealHour = new Date(Date.UTC(
        now.getUTCFullYear(),
        now.getUTCMonth(),
        now.getUTCDate(),
        now.getUTCHours(), 0, 0, 0
    ));

    // UI pill
    const wrap = document.createElement('span');
    wrap.id = 'sticky-game-time';
    wrap.className = 'sticky-game-time';

    const value = document.createElement('span');
    value.className = 'sticky-game-time__value';

    wrap.appendChild(value);

    // Put it before the default icons
    icons.prepend(wrap);

    function computeGameString() {
        const t = getShiftedNow();

        // Whole hours passed since we declared "Mar 1 YR1"
        const hoursPassed = Math.floor((t.getTime() - anchorRealHour.getTime()) / 3600000);

        // Day increments each hour
        const dayIndex = (ANCHOR_DAY - 1) + hoursPassed; // 0-based
        const dayNumber = (dayIndex % 24) + 1;           // 1..24

        // Month increments each 24 hours
        const daysPassed = Math.floor(dayIndex / 24);    // 0..∞

        // Month+Year cycle is 7 days long
        const totalMonthIndex = (ANCHOR_YEAR - 1) * 7 + ANCHOR_MONTH_INDEX + daysPassed;

        const year = Math.floor(totalMonthIndex / 7) + 1;
        const month = months[((totalMonthIndex % 7) + 7) % 7];

        return `${month} ${dayNumber} YR${year}`;
    }

    function update() {
        value.textContent = computeGameString();
    }

    // Initial update
    update();

    // Update exactly on next hour, then every hour
    const now2 = getShiftedNow();
    const msUntilNextHour =
        3600000 - (now2.getUTCMinutes() * 60000 + now2.getUTCSeconds() * 1000 + now2.getUTCMilliseconds());

    setTimeout(function () {
        update();
        setInterval(update, 3600000);
    }, msUntilNextHour);
});


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

    if (document.getElementById('sticky-clock')) return;

    // Choose the clock timezone:
    // - "Etc/UTC" for UTC (same for all users)
    // - Or "America/New_York" (same for all users, displayed in NY time)
    const CLOCK_TIMEZONE = 'Etc/UTC'; // <-- change if you want a specific zone

    const wrap = document.createElement('span');
    wrap.id = 'sticky-clock';
    wrap.className = 'sticky-clock';

    const label = document.createElement('span');
    label.className = 'sticky-clock__label';
    label.textContent = (CLOCK_TIMEZONE === 'Etc/UTC') ? 'UTC:' : 'Time:';

    const value = document.createElement('span');
    value.className = 'sticky-clock__value';

    wrap.appendChild(label);
    wrap.appendChild(value);

    // Put it near the front (use appendChild to put it at the end)
    icons.prepend(wrap);

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

    function updateClock() {
        value.textContent = fmt.format(new Date());
    }

    updateClock();
    setInterval(updateClock, 1000);
});