MediaWiki:Common.js
Appearance
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/* 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'];
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');
$firstButton.addClass('active');
$container.find('#' + firstTabId).addClass('active').show();
});
$('.wiki-tab-button').on('click', function () {
const $button = $(this);
const tabId = $button.data('tab');
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;
// already present?
if (bar.querySelector(`[data-role="${role}"]`)) return;
const wrap = document.createElement('span');
wrap.className = 'header-widget';
wrap.dataset.widget = role;
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() {
// ---- Pulse logic (45 minutes before the tick) ----
const msUntilNextHour = 3600000 - (now % 3600000);
const thresholdMs = 45 * 60 * 1000;
const shouldWarn = msUntilNextHour > 0 && msUntilNextHour <= thresholdMs;
document.querySelectorAll('[data-role="game"]').forEach((el) => {
if (shouldWarn) {
el.textContent = "TICK SOON";
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 (FIXED)
========================================================= */
function buildBarsIfPossible() {
const stickyBar = getStickyBar();
const mainBar = getMainBar();
// Sticky: Discord is separate (left), widgets in bar
if (stickyBar) {
ensureWidget(stickyBar, 'clock', 'UTC:');
ensureWidget(stickyBar, 'game', null);
ensureWidget(stickyBar, 'countdown', 'Age ends in:');
}
// Main: Discord is inside the bar (first), then widgets
if (mainBar) {
ensureWidget(mainBar, 'clock', 'UTC:');
ensureWidget(mainBar, 'game', null);
ensureWidget(mainBar, 'countdown', 'Age ends in:');
}
updateAll();
}
function startTickerOnce() {
if (window.__timeWidgetsIntervalId) return; // already ticking
updateAll();
window.__timeWidgetsIntervalId = setInterval(updateAll, 1000);
}
function initAll() {
buildBarsIfPossible();
startTickerOnce();
}
// Initial page load
$(initAll);
// Vector 2022 navigation/content swaps
mw.hook('wikipage.content').add(initAll);
mw.hook('skin.ready').add(initAll); // harmless extra safety
})();