Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

MediaWiki:Common.js: Difference between revisions

MediaWiki interface page
Repair bulk save controls and refresh archive tables
Use native form submission safely after version history
 
(21 intermediate revisions by the same user 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. */ /* HarvestWiki dynamic Cargo archive filters */ (function () { 'use strict'; function initHerbLocationFilters(root) { var catalogs = []; if (root && root.matches && root.matches('.hw-herb-catalog')) { catalogs.push(root); } if (root && root.querySelectorAll) { catalogs = catalogs.concat(Array.prototype.slice.call(root.querySelectorAll('.hw-herb-catalog'))); } catalogs.forEach(function (catalog) { if (catalog.dataset.hwLocationFiltersReady === '1') { return; } var table = catalog.querySelector('table'); var filterBar = document.querySelector('.hw-herb-filter'); if (!table || !filterBar) { return; } var headers = Array.prototype.slice.call(table.querySelectorAll('thead th')); var locationIndex = headers.findIndex(function (header) { return header.textContent.trim().toLowerCase() === 'location'; }); if (locationIndex < 0) { return; } var rows = Array.prototype.slice.call(table.querySelectorAll('tbody tr')); var counts = {}; rows.forEach(function (row) { var cell = row.querySelectorAll('td')[locationIndex]; var location = cell ? cell.textContent.trim() : ''; row.dataset.hwLocation = location; if (location) { counts[location] = (counts[location] || 0) + 1; } }); var locations = Object.keys(counts).sort(); var enabled = new Set(locations); filterBar.innerHTML = ''; var showAll = document.createElement('button'); showAll.type = 'button'; showAll.className = 'hw-filter-chip hw-filter-chip--all'; showAll.textContent = 'Show all'; filterBar.appendChild(showAll); var locationButtons = {}; locations.forEach(function (location) { var button = document.createElement('button'); button.type = 'button'; button.className = 'hw-filter-chip is-active'; button.setAttribute('aria-pressed', 'true'); button.textContent = location + ' (' + counts[location] + ')'; button.addEventListener('click', function () { if (enabled.has(location)) { enabled.delete(location); } else { enabled.add(location); } applyFilters(); }); locationButtons[location] = button; filterBar.appendChild(button); }); function applyFilters() { rows.forEach(function (row) { row.hidden = !enabled.has(row.dataset.hwLocation); }); locations.forEach(function (location) { var active = enabled.has(location); locationButtons[location].classList.toggle('is-active', active); locationButtons[location].setAttribute('aria-pressed', active ? 'true' : 'false'); }); showAll.disabled = enabled.size === locations.length; showAll.classList.toggle('is-active', enabled.size === locations.length); } showAll.addEventListener('click', function () { enabled = new Set(locations); applyFilters(); }); catalog.dataset.hwLocationFiltersReady = '1'; applyFilters(); }); } mw.hook('wikipage.content').add(function ($content) { initHerbLocationFilters($content && $content[0] ? $content[0] : document); }); }()); /* HarvestWiki admin-only Herb form UI */ (function () { 'use strict'; var groups = mw.config.get('wgUserGroups') || []; var categories = mw.config.get('wgCategories') || []; if (groups.indexOf('sysop') !== -1 || categories.indexOf('Herbs') === -1) { return; } function hideHerbFormActions() { ['ca-formedit', 'ca-formcreate', 'ca-formedit-sticky-header', 'ca-formcreate-sticky-header'].forEach(function (id) { var element = document.getElementById(id); if (element) { element.hidden = true; } }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', hideHerbFormActions); } else { hideHerbFormActions(); } }()); /* HarvestWiki rarity-colored Herb and Crystal table rows */ (function () { 'use strict'; var rarities = { common: true, uncommon: true, rare: true, legendary: true, mythic: true, secret: true, mortal: true, refined: true, spirit: true }; function normalizeRarity(value) { var rarity = (value || '').trim().toLowerCase(); return rarities[rarity] ? rarity : ''; } function applyRarityColors(root) { var scope = root && root.querySelectorAll ? root : document; var tables = []; if (scope.matches && scope.matches('table')) { tables.push(scope); } tables = tables.concat(Array.prototype.slice.call(scope.querySelectorAll('table'))); tables.forEach(function (table) { var headerCells = Array.prototype.slice.call(table.querySelectorAll('tr:first-child th, tr:first-child td')); var itemIndex = headerCells.findIndex(function (cell) { var label = cell.textContent.trim().toLowerCase(); return label === 'herb' || label === 'crystal' || cell.classList.contains('field_Herb') || cell.classList.contains('field_Crystal'); }); var rarityIndex = headerCells.findIndex(function (cell) { return cell.textContent.trim().toLowerCase() === 'rarity' || cell.classList.contains('field_Rarity'); }); Array.prototype.slice.call(table.rows).forEach(function (row) { var cells = Array.prototype.slice.call(row.cells); var itemCell = row.querySelector('td.field_Herb, td.field_Crystal'); var rarityCell = row.querySelector('td.field_Rarity'); if (!itemCell && itemIndex >= 0) { itemCell = cells[itemIndex]; } if (!rarityCell && rarityIndex >= 0) { rarityCell = cells[rarityIndex]; } if (!itemCell || !rarityCell || itemCell.tagName !== 'TD') { return; } var rarity = normalizeRarity(rarityCell.textContent); if (!rarity) { return; } row.classList.add('hw-rarity-row'); row.dataset.hwRarity = rarity; itemCell.classList.add('hw-rarity-item'); rarityCell.classList.add('hw-rarity-label'); }); table.dataset.hwRarityColorsReady = '1'; }); } mw.hook('wikipage.content').add(function ($content) { applyRarityColors($content && $content[0] ? $content[0] : document); }); }()); /* HarvestWiki default Herb rarity order */ (function () { 'use strict'; var rarityRank = { common: 0, uncommon: 1, rare: 2, legendary: 3, mythic: 4, secret: 5 }; function sortHerbTables(root) { var scope = root && root.querySelectorAll ? root : document; var tables = []; if (scope.matches && scope.matches('table')) { tables.push(scope); } tables = tables.concat(Array.prototype.slice.call(scope.querySelectorAll('table'))); tables.forEach(function (table) { if (table.dataset.hwHerbRaritySorted === '1') { return; } var headers = Array.prototype.slice.call(table.querySelectorAll('thead th')); if (!headers.length && table.rows.length) { headers = Array.prototype.slice.call(table.rows[0].cells); } var herbIndex = headers.findIndex(function (header) { return header.textContent.trim().toLowerCase() === 'herb' || header.classList.contains('field_Herb'); }); var rarityIndex = headers.findIndex(function (header) { return header.textContent.trim().toLowerCase() === 'rarity' || header.classList.contains('field_Rarity'); }); if (herbIndex < 0 || rarityIndex < 0) { return; } Array.prototype.slice.call(table.tBodies).forEach(function (body) { var rows = Array.prototype.slice.call(body.rows).map(function (row, index) { var rarityCell = row.cells[rarityIndex]; var rarity = rarityCell ? rarityCell.textContent.trim().toLowerCase() : ''; return { row: row, index: index, rank: Object.prototype.hasOwnProperty.call(rarityRank, rarity) ? rarityRank[rarity] : 999 }; }); rows.sort(function (a, b) { return a.rank - b.rank || a.index - b.index; }); var fragment = document.createDocumentFragment(); rows.forEach(function (entry) { fragment.appendChild(entry.row); }); body.appendChild(fragment); }); table.dataset.hwHerbRaritySorted = '1'; }); } mw.hook('wikipage.content').add(function ($content) { sortHerbTables($content && $content[0] ? $content[0] : document); }); }()); /* HarvestWiki default Crystal rarity order */ (function () { 'use strict'; var crystalRarityRank = { mortal: 0, refined: 1, spirit: 2 }; function sortCrystalTables(root) { var scope = root && root.querySelectorAll ? root : document; var tables = []; if (scope.matches && scope.matches('table')) { tables.push(scope); } tables = tables.concat(Array.prototype.slice.call(scope.querySelectorAll('table'))); tables.forEach(function (table) { if (table.dataset.hwCrystalRaritySorted === '1') { return; } var headers = Array.prototype.slice.call(table.querySelectorAll('thead th')); if (!headers.length && table.rows.length) { headers = Array.prototype.slice.call(table.rows[0].cells); } var crystalIndex = headers.findIndex(function (header) { return header.textContent.trim().toLowerCase() === 'crystal' || header.classList.contains('field_Crystal'); }); var rarityIndex = headers.findIndex(function (header) { return header.textContent.trim().toLowerCase() === 'rarity' || header.classList.contains('field_Rarity'); }); if (crystalIndex < 0 || rarityIndex < 0) { return; } Array.prototype.slice.call(table.tBodies).forEach(function (body) { var rows = Array.prototype.slice.call(body.rows).map(function (row, index) { var rarityCell = row.cells[rarityIndex]; var rarity = rarityCell ? rarityCell.textContent.trim().toLowerCase() : ''; return { row: row, index: index, rank: Object.prototype.hasOwnProperty.call(crystalRarityRank, rarity) ? crystalRarityRank[rarity] : 999 }; }); rows.sort(function (a, b) { return a.rank - b.rank || a.index - b.index; }); var fragment = document.createDocumentFragment(); rows.forEach(function (entry) { fragment.appendChild(entry.row); }); body.appendChild(fragment); }); table.dataset.hwCrystalRaritySorted = '1'; }); } mw.hook('wikipage.content').add(function ($content) { sortCrystalTables($content && $content[0] ? $content[0] : document); }); }()); /* HarvestWiki admin-only Crystal form UI */ (function () { 'use strict'; var groups = mw.config.get('wgUserGroups') || []; var categories = mw.config.get('wgCategories') || []; if (groups.indexOf('sysop') !== -1 || categories.indexOf('Crystals') === -1) { return; } function hideCrystalFormActions() { ['ca-formedit', 'ca-formcreate', 'ca-formedit-sticky-header', 'ca-formcreate-sticky-header'].forEach(function (id) { var element = document.getElementById(id); if (element) { element.hidden = true; } }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', hideCrystalFormActions); } else { hideCrystalFormActions(); } }()); /* HarvestWiki admin-only Weather form UI */ (function () { 'use strict'; var groups = mw.config.get('wgUserGroups') || []; var categories = mw.config.get('wgCategories') || []; if (groups.indexOf('sysop') !== -1 || categories.indexOf('Weather') === -1) { return; } function hideWeatherFormActions() { ['ca-formedit', 'ca-formcreate', 'ca-formedit-sticky-header', 'ca-formcreate-sticky-header'].forEach(function (id) { var element = document.getElementById(id); if (element) { element.hidden = true; } }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', hideWeatherFormActions); } else { hideWeatherFormActions(); } }()); /* HarvestWiki four-zodiac celestial calendar */ (function () { 'use strict'; var anchor = Date.UTC(2026, 0, 1, 0, 0, 0); var seasonLength = 8 * 60 * 60 * 1000; var seasonNames = [ 'Year of the Dragon', 'Year of the Tiger', 'Year of the Phoenix', 'Year of the Tortoise' ]; function formatDuration(milliseconds) { var seconds = Math.max(0, Math.ceil(milliseconds / 1000)); var hours = Math.floor(seconds / 3600); var minutes = Math.floor((seconds % 3600) / 60); var remainder = seconds % 60; return String(hours).padStart(2, '0') + ':' + String(minutes).padStart(2, '0') + ':' + String(remainder).padStart(2, '0'); } function initZodiacCycles(root) { var widgets = []; if (root && root.matches && root.matches('[data-hw-zodiac-cycle]')) { widgets.push(root); } if (root && root.querySelectorAll) { widgets = widgets.concat(Array.prototype.slice.call(root.querySelectorAll('[data-hw-zodiac-cycle]'))); } widgets.forEach(function (widget) { if (widget.dataset.hwZodiacReady === '1') { return; } widget.dataset.hwZodiacReady = '1'; var currentLabel = widget.querySelector('[data-hw-zodiac-current]'); var countdown = widget.querySelector('[data-hw-zodiac-countdown]'); var progress = widget.querySelector('[data-hw-zodiac-progress]'); var seasons = Array.prototype.slice.call(widget.querySelectorAll('[data-hw-zodiac-season]')); function updateZodiacCycle() { var now = Date.now(); var step = Math.floor((now - anchor) / seasonLength); var currentIndex = ((step % seasonNames.length) + seasonNames.length) % seasonNames.length; var nextBoundary = anchor + ((step + 1) * seasonLength); var remaining = Math.max(0, nextBoundary - now); var elapsedInSeason = ((now - anchor) % seasonLength + seasonLength) % seasonLength; if (currentLabel) { currentLabel.textContent = seasonNames[currentIndex]; } if (countdown) { countdown.textContent = formatDuration(remaining); countdown.title = 'Changes at ' + new Date(nextBoundary).toLocaleString(); } if (progress) { progress.style.width = ((elapsedInSeason / seasonLength) * 100).toFixed(3) + '%'; } seasons.forEach(function (season, index) { var offset = (index - currentIndex + seasonNames.length) % seasonNames.length; var state = season.querySelector('[data-hw-zodiac-state]'); var startsIn = remaining + (Math.max(0, offset - 1) * seasonLength); var changeAt = offset === 0 ? nextBoundary : now + startsIn; season.classList.toggle('is-current', offset === 0); season.setAttribute('aria-current', offset === 0 ? 'true' : 'false'); season.title = (offset === 0 ? 'Ends ' : 'Begins ') + new Date(changeAt).toLocaleString(); if (state) { if (offset === 0) { state.textContent = 'Current · ends in ' + formatDuration(remaining); } else if (offset === 1) { state.textContent = 'Next · starts in ' + formatDuration(startsIn); } else { state.textContent = 'Starts in ' + formatDuration(startsIn); } } }); } updateZodiacCycle(); window.setInterval(updateZodiacCycle, 1000); }); } mw.hook('wikipage.content').add(function ($content) { initZodiacCycles($content && $content[0] ? $content[0] : document); }); }()); /* HarvestWiki multi-page editor controls and archive refresh */ (function () { 'use strict'; function initBulkEditors(root) { var grids = []; if (root && root.matches && root.matches('.pfSpreadsheet')) { grids.push(root); } if (root && root.querySelectorAll) { grids = grids.concat(Array.prototype.slice.call(root.querySelectorAll('.pfSpreadsheet'))); } grids.forEach(function (grid) { if (grid.dataset.hwBulkEditorReady === '1') { return; } grid.dataset.hwBulkEditorReady = '1'; var parameters = new URLSearchParams(window.location.search); var templateName = parameters.get('template') || grid.id.replace(/Grid$/, ''); var archivePage = templateName === 'Herb' ? 'Herbs' : (templateName === 'Crystal' ? 'Crystals' : null); var userGroups = mw.config.get('wgUserGroups') || []; var canDelete = archivePage && userGroups.indexOf('sysop') !== -1; function purgeArchive() { if (!archivePage || typeof mw.Api !== 'function') { return Promise.resolve(); } return new mw.Api().post({ action: 'purge', titles: archivePage, formatversion: 2 }); } function wireRowActions() { Array.prototype.slice.call(grid.querySelectorAll('a.save-changes')).forEach(function (link) { if (link.dataset.hwBulkSaveReady === '1') { return; } link.dataset.hwBulkSaveReady = '1'; link.setAttribute('title', 'Save this row'); link.setAttribute('aria-label', 'Save this row'); link.addEventListener('click', function () { link.classList.remove('is-saved'); link.classList.add('is-saving'); function refreshArchive() { purgeArchive().then(function () { link.classList.remove('is-saving'); link.classList.add('is-saved'); window.setTimeout(function () { link.classList.remove('is-saved'); }, 2200); }); } window.setTimeout(refreshArchive, 1600); window.setTimeout(refreshArchive, 4200); }); }); if (!canDelete) { return; } Array.prototype.slice.call(grid.querySelectorAll('tbody tr')).forEach(function (row) { var pageCell = row.querySelector('td[data-x="0"]'); var actionBox = row.querySelector('.save-or-cancel'); if (!pageCell || !actionBox || actionBox.querySelector('.hw-delete-row')) { return; } var pageTitle = pageCell.textContent.trim(); if (!pageTitle) { return; } var deleteButton = document.createElement('button'); deleteButton.type = 'button'; deleteButton.className = 'hw-delete-row'; deleteButton.textContent = 'Delete'; deleteButton.title = 'Delete page ' + pageTitle; deleteButton.setAttribute('aria-label', 'Delete page ' + pageTitle); deleteButton.addEventListener('click', function () { var confirmed = window.confirm( 'Permanently delete "' + pageTitle + '"?\n\n' + 'This removes its wiki page and all table data. This cannot be undone.' ); if (!confirmed) { return; } deleteButton.disabled = true; deleteButton.classList.add('is-deleting'); deleteButton.textContent = 'Deleting…'; new mw.Api().postWithToken('csrf', { action: 'delete', title: pageTitle, reason: 'Deleted from the multi-page editor', formatversion: 2 }).then(function () { return purgeArchive(); }).then(function () { deleteButton.textContent = 'Deleted'; window.setTimeout(function () { window.location.reload(); }, 700); }, function () { deleteButton.disabled = false; deleteButton.classList.remove('is-deleting'); deleteButton.textContent = 'Delete'; mw.notify('Could not delete "' + pageTitle + '". The record was not removed.', { type: 'error' }); }); }); actionBox.appendChild(deleteButton); }); } wireRowActions(); if (window.MutationObserver) { new MutationObserver(wireRowActions).observe(grid, { childList: true, subtree: true }); } if (!grid.querySelector('.hw-bulk-editor-note')) { var note = document.createElement('div'); note.className = 'hw-bulk-editor-note'; note.textContent = 'Scroll sideways to reach every field. Use Save at the right of each row.' + (archivePage ? ' Saved rows automatically refresh the ' + archivePage + ' index.' : '') + (canDelete ? ' Administrators can permanently remove a record with Delete.' : ''); grid.appendChild(note); } }); } mw.hook('wikipage.content').add(function ($content) { initBulkEditors($content && $content[0] ? $content[0] : document); }); }());


/* HarvestWiki dynamic Cargo archive filters */
/* HarvestWiki automatic Herb, Crystal, Mutation, Shovel, Weather, TOD, and Zodiac link colors */
(function () {
(function () {
   'use strict';
   'use strict';


   function initHerbLocationFilters(root) {
   var rarityColors = {
     var catalogs = [];
     common: '#e5e7e7',
     if (root && root.matches && root.matches('.hw-herb-catalog')) {
    uncommon: '#87ff97',
      catalogs.push(root);
    rare: '#6d90ff',
     }
    legendary: '#fff687',
    if (root && root.querySelectorAll) {
    mythic: '#ff79cb',
      catalogs = catalogs.concat(Array.prototype.slice.call(root.querySelectorAll('.hw-herb-catalog')));
    secret: '#12005c',
    mortal: '#cef1e8',
    refined: '#464747',
    spirit: '#00c3f2'
  };
 
  var mutationColors = {
    silver: '#eef1f1',
    golden: '#f4d05b',
    vitality: '#e96869',
    vampiric: '#de002b',
    icy: '#9ee7d8',
    frozen: '#6ee8ff',
    spirit: '#ffabf6',
    royal: '#ffec31',
    imperial: '#b600ff',
    century: '#5369ff',
    millenium: '#6c47ff',
    eon: '#7f15ff',
    sovereign: '#ff26a5',
    supreme: '#ed13ff'
  };
 
  var worldColors = {
    clear: '#f1cb4f',
    cloudy: '#949ba5',
    rain: '#60a5e2',
    storm: '#525964',
    thunderstorm: '#f8d652',
    'dao tempest': '#b9422f',
    sunrise: '#ef9d49',
    daytime: '#63ace0',
    sunset: '#df6447',
    night: '#abb6f9',
    'year of the dragon': '#ffb86f',
    'year of the tiger': '#f7f672',
    'year of the phoenix': '#f87b84',
    'year of the tortoise': '#4fc57d'
  };
 
  var shovelColors = {
    'basic shovel': '#ffffff',
    "journeyman's shovel": '#ffffff',
     'root breaker': '#ffffff',
    'steel spade': '#ffffff',
    'autumn leaf': '#ffffff',
    "king's destiny": '#ffffff'
  };
 
  var entityColors = {
    'ancient woe herb': rarityColors.rare,
    'breathless dawn herb': rarityColors.uncommon,
    'crimson sun grass': rarityColors.common,
    'fleeting dusk herb': rarityColors.uncommon,
    'frigid moon grass': rarityColors.common,
    'pursuing longevity herb': rarityColors.mythic,
    'sparkling joy herb': rarityColors.rare,
    'yang lotus': rarityColors.legendary,
    'yin lotus': rarityColors.legendary,
    'air crystal': rarityColors.mortal,
    'earth crystal': rarityColors.mortal,
    'flame crystal': rarityColors.mortal,
    'water crystal': rarityColors.mortal,
    'wood crystal': rarityColors.mortal,
    'magma crystal': rarityColors.refined,
    'mist crystal': rarityColors.refined,
    'longevity crystal': rarityColors.spirit,
    clear: worldColors.clear,
    cloudy: worldColors.cloudy,
    rain: worldColors.rain,
    storm: worldColors.storm,
    thunderstorm: worldColors.thunderstorm,
    'dao tempest': worldColors['dao tempest'],
    sunrise: worldColors.sunrise,
    daytime: worldColors.daytime,
    sunset: worldColors.sunset,
    night: worldColors.night,
    'year of the dragon': worldColors['year of the dragon'],
    'year of the tiger': worldColors['year of the tiger'],
    'year of the phoenix': worldColors['year of the phoenix'],
    'year of the tortoise': worldColors['year of the tortoise'],
    'basic shovel': shovelColors['basic shovel'],
    "journeyman's shovel": shovelColors["journeyman's shovel"],
    'root breaker': shovelColors['root breaker'],
    'steel spade': shovelColors['steel spade'],
    'autumn leaf': shovelColors['autumn leaf'],
     "king's destiny": shovelColors["king's destiny"]
  };
 
  function normalizeTitle(value) {
    return (value || '').replace(/_/g, ' ').replace(/\s+/g, ' ').trim().toLowerCase();
  }
 
  function titleFromLink(link) {
    var href = link.getAttribute('href') || '';
    if (!href || href.charAt(0) === '#') {
      return '';
     }
     }


     catalogs.forEach(function (catalog) {
     try {
       if (catalog.dataset.hwLocationFiltersReady === '1') {
      var url = new URL(href, window.location.origin);
         return;
       if (url.origin !== window.location.origin) {
         return '';
       }
       }


       var table = catalog.querySelector('table');
       var title = url.searchParams.get('title');
       var filterBar = document.querySelector('.hw-herb-filter');
       if (!title && url.pathname.indexOf('/index.php/') === 0) {
      if (!table || !filterBar) {
         title = decodeURIComponent(url.pathname.slice('/index.php/'.length));
         return;
       }
       }
      return normalizeTitle((title || '').split('#')[0]);
    } catch (error) {
      return '';
    }
  }


      var headers = Array.prototype.slice.call(table.querySelectorAll('thead th'));
  function cargoQuery(table, fields) {
      var locationIndex = headers.findIndex(function (header) {
    return mw.loader.using('mediawiki.api').then(function () {
        return header.textContent.trim().toLowerCase() === 'location';
      return new mw.Api().get({
        action: 'cargoquery',
        tables: table,
        fields: fields,
        limit: 500,
        formatversion: 2
       });
       });
       if (locationIndex < 0) {
    }).then(function (data) {
        return;
       return data && data.cargoquery ? data.cargoquery : [];
       }
    }, function () {
      return [];
    });
  }
 
  function buildColorMap() {
    return Promise.all([
      cargoQuery('Herbs', '_pageName=Page,Rarity'),
      cargoQuery('Crystals', '_pageName=Page,Rarity'),
      cargoQuery('Mutations', '_pageName=Page,Color'),
      cargoQuery('Weathers', '_pageName=Page,Color'),
      cargoQuery('Shovels', '_pageName=Page,Color')
    ]).then(function (groups) {
       var colorMap = Object.assign({}, entityColors);


       var rows = Array.prototype.slice.call(table.querySelectorAll('tbody tr'));
       groups[0].forEach(function (row) {
      var counts = {};
         var data = row.title || {};
      rows.forEach(function (row) {
         var color = rarityColors[normalizeTitle(data.Rarity)];
         var cell = row.querySelectorAll('td')[locationIndex];
         if (data.Page && color) {
         var location = cell ? cell.textContent.trim() : '';
           colorMap[normalizeTitle(data.Page)] = color;
        row.dataset.hwLocation = location;
         if (location) {
           counts[location] = (counts[location] || 0) + 1;
         }
         }
       });
       });


       var locations = Object.keys(counts).sort();
       groups[1].forEach(function (row) {
      var enabled = new Set(locations);
        var data = row.title || {};
      filterBar.innerHTML = '';
        var color = rarityColors[normalizeTitle(data.Rarity)];
        if (data.Page && color) {
          colorMap[normalizeTitle(data.Page)] = color;
        }
      });


       var showAll = document.createElement('button');
       groups[2].forEach(function (row) {
      showAll.type = 'button';
        var data = row.title || {};
      showAll.className = 'hw-filter-chip hw-filter-chip--all';
        if (data.Page && data.Color) {
      showAll.textContent = 'Show all';
          colorMap[normalizeTitle(data.Page)] = data.Color;
       filterBar.appendChild(showAll);
        }
       });


       var locationButtons = {};
       groups[3].forEach(function (row) {
      locations.forEach(function (location) {
         var data = row.title || {};
         var button = document.createElement('button');
         if (data.Page && data.Color) {
         button.type = 'button';
           colorMap[normalizeTitle(data.Page)] = data.Color;
        button.className = 'hw-filter-chip is-active';
         }
        button.setAttribute('aria-pressed', 'true');
        button.textContent = location + ' (' + counts[location] + ')';
        button.addEventListener('click', function () {
           if (enabled.has(location)) {
            enabled.delete(location);
          } else {
            enabled.add(location);
          }
          applyFilters();
         });
        locationButtons[location] = button;
        filterBar.appendChild(button);
       });
       });


       function applyFilters() {
       groups[4].forEach(function (row) {
        rows.forEach(function (row) {
        var data = row.title || {};
          row.hidden = !enabled.has(row.dataset.hwLocation);
         if (data.Page && data.Color) {
        });
           colorMap[normalizeTitle(data.Page)] = data.Color;
         locations.forEach(function (location) {
         }
           var active = enabled.has(location);
      });
          locationButtons[location].classList.toggle('is-active', active);
          locationButtons[location].setAttribute('aria-pressed', active ? 'true' : 'false');
         });
        showAll.disabled = enabled.size === locations.length;
        showAll.classList.toggle('is-active', enabled.size === locations.length);
      }


       showAll.addEventListener('click', function () {
       Object.keys(mutationColors).forEach(function (title) {
         enabled = new Set(locations);
         colorMap[title] = mutationColors[title];
        applyFilters();
       });
       });


       catalog.dataset.hwLocationFiltersReady = '1';
       return colorMap;
      applyFilters();
     });
     });
   }
   }


   mw.hook('wikipage.content').add(function ($content) {
   function applyLinkColors(root, colorMap) {
     initHerbLocationFilters($content && $content[0] ? $content[0] : document);
     var scope = root && root.querySelectorAll ? root : document;
  });
    var links = [];
}());
    if (scope.matches && scope.matches('a[href]')) {
      links.push(scope);
    }
    links = links.concat(Array.prototype.slice.call(scope.querySelectorAll('a[href]')));


/* HarvestWiki admin-only Herb form UI */
    links.forEach(function (link) {
(function () {
      var color = colorMap[titleFromLink(link)];
  'use strict';
      if (!color || link.classList.contains('new')) {
  var groups = mw.config.get('wgUserGroups') || [];
        return;
  var categories = mw.config.get('wgCategories') || [];
  if (groups.indexOf('sysop') !== -1 || categories.indexOf('Herbs') === -1) {
    return;
  }
  function hideHerbFormActions() {
    ['ca-formedit', 'ca-formcreate', 'ca-formedit-sticky-header', 'ca-formcreate-sticky-header'].forEach(function (id) {
      var element = document.getElementById(id);
      if (element) {
        element.hidden = true;
       }
       }
      link.classList.add('hw-colored-link');
      link.style.setProperty('--hw-link-color', color);
     });
     });
  }
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', hideHerbFormActions);
  } else {
    hideHerbFormActions();
  }
}());
/* HarvestWiki rarity-colored Herb and Crystal table rows */
(function () {
  'use strict';
  var rarities = {
    common: true,
    uncommon: true,
    rare: true,
    legendary: true,
    mythic: true,
    secret: true,
    mortal: true,
    refined: true,
    spirit: true
  };
  function normalizeRarity(value) {
    var rarity = (value || '').trim().toLowerCase();
    return rarities[rarity] ? rarity : '';
   }
   }


   function applyRarityColors(root) {
   function applyMutationRows(root, colorMap) {
     var scope = root && root.querySelectorAll ? root : document;
     var scope = root && root.querySelectorAll ? root : document;
     var tables = [];
     var tables = [];
     if (scope.matches && scope.matches('table')) {
     if (scope.matches && scope.matches('table')) {
       tables.push(scope);
       tables.push(scope);
Line 156: Line 224:


     tables.forEach(function (table) {
     tables.forEach(function (table) {
       var headerCells = Array.prototype.slice.call(table.querySelectorAll('tr:first-child th, tr:first-child td'));
       var headers = Array.prototype.slice.call(table.querySelectorAll('thead th'));
       var itemIndex = headerCells.findIndex(function (cell) {
       if (!headers.length && table.rows.length) {
        var label = cell.textContent.trim().toLowerCase();
         headers = Array.prototype.slice.call(table.rows[0].cells);
         return label === 'herb' || label === 'crystal' ||
       }
          cell.classList.contains('field_Herb') || cell.classList.contains('field_Crystal');
 
       });
       var mutationIndex = headers.findIndex(function (header) {
       var rarityIndex = headerCells.findIndex(function (cell) {
         return header.textContent.trim().toLowerCase() === 'mutation' ||
         return cell.textContent.trim().toLowerCase() === 'rarity' ||
           header.classList.contains('field_Mutation');
           cell.classList.contains('field_Rarity');
       });
       });
      if (mutationIndex < 0) {
        return;
      }


       Array.prototype.slice.call(table.rows).forEach(function (row) {
       Array.prototype.slice.call(table.tBodies).forEach(function (body) {
         var cells = Array.prototype.slice.call(row.cells);
         Array.prototype.slice.call(body.rows).forEach(function (row) {
        var itemCell = row.querySelector('td.field_Herb, td.field_Crystal');
          var mutationCell = row.cells[mutationIndex];
        var rarityCell = row.querySelector('td.field_Rarity');
          var mutationLink = mutationCell ? mutationCell.querySelector('a[href]') : null;
 
           var color = mutationLink ? colorMap[titleFromLink(mutationLink)] : '';
        if (!itemCell && itemIndex >= 0) {
          if (!mutationCell || !color) {
           itemCell = cells[itemIndex];
            return;
        }
          }
        if (!rarityCell && rarityIndex >= 0) {
           row.classList.add('hw-mutation-row');
          rarityCell = cells[rarityIndex];
          row.style.setProperty('--hw-mutation-row-color', color);
        }
          mutationCell.classList.add('hw-mutation-item');
        if (!itemCell || !rarityCell || itemCell.tagName !== 'TD') {
        });
          return;
        }
 
        var rarity = normalizeRarity(rarityCell.textContent);
        if (!rarity) {
           return;
        }
 
        row.classList.add('hw-rarity-row');
        row.dataset.hwRarity = rarity;
        itemCell.classList.add('hw-rarity-item');
        rarityCell.classList.add('hw-rarity-label');
       });
       });
      table.dataset.hwRarityColorsReady = '1';
     });
     });
   }
   }


   mw.hook('wikipage.content').add(function ($content) {
   function applyShovelRows(root, colorMap) {
    applyRarityColors($content && $content[0] ? $content[0] : document);
  });
}());
 
 
/* HarvestWiki default Herb rarity order */
(function () {
  'use strict';
 
  var rarityRank = {
    common: 0,
    uncommon: 1,
    rare: 2,
    legendary: 3,
    mythic: 4,
    secret: 5
  };
 
  function sortHerbTables(root) {
     var scope = root && root.querySelectorAll ? root : document;
     var scope = root && root.querySelectorAll ? root : document;
     var tables = [];
     var tables = [];
     if (scope.matches && scope.matches('table')) {
     if (scope.matches && scope.matches('table')) {
       tables.push(scope);
       tables.push(scope);
Line 226: Line 262:


     tables.forEach(function (table) {
     tables.forEach(function (table) {
      if (table.dataset.hwHerbRaritySorted === '1') {
        return;
      }
       var headers = Array.prototype.slice.call(table.querySelectorAll('thead th'));
       var headers = Array.prototype.slice.call(table.querySelectorAll('thead th'));
       if (!headers.length && table.rows.length) {
       if (!headers.length && table.rows.length) {
Line 235: Line 267:
       }
       }


       var herbIndex = headers.findIndex(function (header) {
       var shovelIndex = headers.findIndex(function (header) {
         return header.textContent.trim().toLowerCase() === 'herb' ||
         return header.textContent.trim().toLowerCase() === 'shovel' ||
           header.classList.contains('field_Herb');
           header.classList.contains('field_Shovel');
       });
       });
      var rarityIndex = headers.findIndex(function (header) {
       if (shovelIndex < 0) {
        return header.textContent.trim().toLowerCase() === 'rarity' ||
          header.classList.contains('field_Rarity');
      });
 
       if (herbIndex < 0 || rarityIndex < 0) {
         return;
         return;
       }
       }


       Array.prototype.slice.call(table.tBodies).forEach(function (body) {
       Array.prototype.slice.call(table.tBodies).forEach(function (body) {
         var rows = Array.prototype.slice.call(body.rows).map(function (row, index) {
         Array.prototype.slice.call(body.rows).forEach(function (row) {
           var rarityCell = row.cells[rarityIndex];
           var shovelCell = row.cells[shovelIndex];
           var rarity = rarityCell ? rarityCell.textContent.trim().toLowerCase() : '';
           var shovelLink = shovelCell ? shovelCell.querySelector('a[href]') : null;
           return {
           var color = shovelLink ? colorMap[titleFromLink(shovelLink)] : '';
            row: row,
           if (!shovelCell || !color) {
            index: index,
            return;
            rank: Object.prototype.hasOwnProperty.call(rarityRank, rarity) ? rarityRank[rarity] : 999
          }
           };
          row.classList.add('hw-shovel-row');
        });
          row.style.setProperty('--hw-shovel-row-color', color);
 
           shovelCell.classList.add('hw-shovel-item');
        rows.sort(function (a, b) {
          return a.rank - b.rank || a.index - b.index;
        });
 
        var fragment = document.createDocumentFragment();
        rows.forEach(function (entry) {
           fragment.appendChild(entry.row);
         });
         });
        body.appendChild(fragment);
       });
       });
      table.dataset.hwHerbRaritySorted = '1';
     });
     });
   }
   }
  var immediateColorMap = Object.assign({}, entityColors);
  Object.keys(mutationColors).forEach(function (title) {
    immediateColorMap[title] = mutationColors[title];
  });
  Object.keys(worldColors).forEach(function (title) {
    immediateColorMap[title] = worldColors[title];
  });
  Object.keys(shovelColors).forEach(function (title) {
    immediateColorMap[title] = shovelColors[title];
  });
  var colorMapPromise = buildColorMap();


   mw.hook('wikipage.content').add(function ($content) {
   mw.hook('wikipage.content').add(function ($content) {
     sortHerbTables($content && $content[0] ? $content[0] : document);
     var root = $content && $content[0] ? $content[0] : document;
    applyMutationRows(root, immediateColorMap);
    applyShovelRows(root, immediateColorMap);
    applyLinkColors(root, immediateColorMap);
    colorMapPromise.then(function (colorMap) {
      applyMutationRows(root, colorMap);
      applyShovelRows(root, colorMap);
      applyLinkColors(root, colorMap);
    });
   });
   });
}());
}());




/* HarvestWiki default Crystal rarity order */
/* HarvestWiki hierarchical Citizen popout menu */
(function () {
(function () {
   'use strict';
   'use strict';


   var crystalRarityRank = {
   var groups = [
     mortal: 0,
    {
    refined: 1,
      key: 'herb',
     spirit: 2
      title: 'Herb',
   };
      note: 'Harvesting & cultivation',
      glyph: '草',
      pages: ['Herbs', 'Mutations', 'Crystals', 'Shovels']
    },
     {
      key: 'world',
      title: 'World',
      note: 'Places, people & conditions',
      glyph: '界',
      pages: ['Weather', 'Locations', 'NPCs']
     }
   ];


   function sortCrystalTables(root) {
   function directLink(item) {
     var scope = root && root.querySelectorAll ? root : document;
     var children = item ? item.children : null;
     var tables = [];
     var index;
    if (!children) {
      return null;
    }
    for (index = 0; index < children.length; index += 1) {
      if (children[index].tagName === 'A') {
        return children[index];
      }
    }
    return null;
  }


     if (scope.matches && scope.matches('table')) {
  function linkTitle(link) {
       tables.push(scope);
    var path;
     if (!link) {
      return '';
    }
    try {
      path = new URL(link.href, location.href).pathname.split('/').pop() || '';
      return decodeURIComponent(path).replace(/_/g, ' ');
    } catch (error) {
       return (link.textContent || '').trim();
     }
     }
    tables = tables.concat(Array.prototype.slice.call(scope.querySelectorAll('table')));
  }


     tables.forEach(function (table) {
  function closeOtherGroups(root, keep) {
       if (table.dataset.hwCrystalRaritySorted === '1') {
     Array.prototype.slice.call(root.querySelectorAll('.hw-menu-group.is-open')).forEach(function (group) {
         return;
       if (group !== keep) {
        group.classList.remove('is-open');
         group.querySelector('.hw-menu-trigger').setAttribute('aria-expanded', 'false');
       }
       }
    });
  }
  function createGroup(config, items, root) {
    var group = document.createElement('li');
    var trigger = document.createElement('button');
    var glyph = document.createElement('span');
    var copy = document.createElement('span');
    var title = document.createElement('span');
    var note = document.createElement('span');
    var chevron = document.createElement('span');
    var submenu = document.createElement('ul');
    var closeTimer = null;


      var headers = Array.prototype.slice.call(table.querySelectorAll('thead th'));
    function cancelClose() {
       if (!headers.length && table.rows.length) {
       if (closeTimer !== null) {
         headers = Array.prototype.slice.call(table.rows[0].cells);
         window.clearTimeout(closeTimer);
        closeTimer = null;
       }
       }
    }
    function closeGroup() {
      group.classList.remove('is-open');
      trigger.setAttribute('aria-expanded', 'false');
    }
    function scheduleClose() {
      cancelClose();
      closeTimer = window.setTimeout(function () {
        closeTimer = null;
        if (!group.matches(':hover') && !group.contains(document.activeElement)) {
          closeGroup();
        }
      }, 240);
    }
    group.className = 'hw-menu-group hw-menu-group--' + config.key;
    trigger.type = 'button';
    trigger.className = 'hw-menu-trigger';
    trigger.setAttribute('aria-expanded', 'false');
    trigger.setAttribute('aria-haspopup', 'true');
    glyph.className = 'hw-menu-glyph';
    glyph.setAttribute('aria-hidden', 'true');
    glyph.textContent = config.glyph;
    copy.className = 'hw-menu-trigger-copy';
    title.className = 'hw-menu-trigger-title';
    title.textContent = config.title;
    note.className = 'hw-menu-trigger-note';
    note.textContent = config.note;
    copy.appendChild(title);
    copy.appendChild(note);
    chevron.className = 'hw-menu-chevron';
    chevron.setAttribute('aria-hidden', 'true');
    chevron.textContent = '›';
    trigger.appendChild(glyph);
    trigger.appendChild(copy);
    trigger.appendChild(chevron);
    submenu.className = 'hw-menu-submenu';
    submenu.setAttribute('aria-label', config.title + ' pages');
    items.forEach(function (item) {
      submenu.appendChild(item);
    });
    trigger.addEventListener('click', function () {
      var willOpen = !group.classList.contains('is-open');
      cancelClose();
      closeOtherGroups(root, group);
      group.classList.toggle('is-open', willOpen);
      trigger.setAttribute('aria-expanded', willOpen ? 'true' : 'false');
    });
    group.addEventListener('mouseenter', function () {
      cancelClose();
      closeOtherGroups(root, group);
      group.classList.add('is-open');
      trigger.setAttribute('aria-expanded', 'true');
    });


      var crystalIndex = headers.findIndex(function (header) {
    group.addEventListener('mouseleave', scheduleClose);
        return header.textContent.trim().toLowerCase() === 'crystal' ||
    submenu.addEventListener('mouseenter', cancelClose);
          header.classList.contains('field_Crystal');
    submenu.addEventListener('mouseleave', scheduleClose);
      });
      var rarityIndex = headers.findIndex(function (header) {
        return header.textContent.trim().toLowerCase() === 'rarity' ||
          header.classList.contains('field_Rarity');
      });


       if (crystalIndex < 0 || rarityIndex < 0) {
    group.addEventListener('focusout', function (event) {
         return;
       if (!group.contains(event.relatedTarget)) {
         cancelClose();
        closeGroup();
       }
       }
    });
    group.appendChild(trigger);
    group.appendChild(submenu);
    return group;
  }


      Array.prototype.slice.call(table.tBodies).forEach(function (body) {
  function enhanceMenu() {
        var rows = Array.prototype.slice.call(body.rows).map(function (row, index) {
    var portlet = document.querySelector('#p-HarvestWiki');
          var rarityCell = row.cells[rarityIndex];
    var list;
          var rarity = rarityCell ? rarityCell.textContent.trim().toLowerCase() : '';
    var items;
          return {
    var byTitle = {};
            row: row,
    var newList;
            index: index,
    var complete;
            rank: Object.prototype.hasOwnProperty.call(crystalRarityRank, rarity) ?
 
              crystalRarityRank[rarity] : 999
    if (!portlet || portlet.classList.contains('hw-menu-ready')) {
          };
      return;
        });
    }
 
    list = portlet.querySelector('.citizen-menu__content > ul');
    if (!list) {
      return;
    }
 
    items = Array.prototype.slice.call(list.children);
    items.forEach(function (item) {
      var link = directLink(item);
      var title = linkTitle(link);
      if (title) {
        byTitle[title.toLowerCase()] = item;
      }
    });


        rows.sort(function (a, b) {
    complete = groups.every(function (config) {
          return a.rank - b.rank || a.index - b.index;
      return config.pages.every(function (page) {
        });
        return Boolean(byTitle[page.toLowerCase()]);
      });
    });
    if (!complete) {
      return;
    }


        var fragment = document.createDocumentFragment();
    newList = document.createElement('ul');
        rows.forEach(function (entry) {
    newList.className = 'hw-menu-root';
          fragment.appendChild(entry.row);
    groups.forEach(function (config) {
        });
      var groupItems = config.pages.map(function (page) {
         body.appendChild(fragment);
         return byTitle[page.toLowerCase()];
       });
       });
      newList.appendChild(createGroup(config, groupItems, newList));
    });
    list.replaceWith(newList);
    portlet.classList.add('hw-menu-ready');
  }
  function initMenu() {
    enhanceMenu();
    new MutationObserver(enhanceMenu).observe(document.body, {
      childList: true,
      subtree: true
    });


       table.dataset.hwCrystalRaritySorted = '1';
    document.addEventListener('keydown', function (event) {
       var root;
      if (event.key !== 'Escape') {
        return;
      }
      root = document.querySelector('.hw-menu-root');
      if (root) {
        closeOtherGroups(root, null);
      }
     });
     });
   }
   }


   mw.hook('wikipage.content').add(function ($content) {
   if (document.readyState === 'loading') {
     sortCrystalTables($content && $content[0] ? $content[0] : document);
    document.addEventListener('DOMContentLoaded', initMenu, { once: true });
   });
  } else {
     initMenu();
   }
}());
}());




/* HarvestWiki admin-only Crystal form UI */
/* HarvestWiki automatic page release history */
(function () {
(function () {
   'use strict';
   'use strict';
   var groups = mw.config.get('wgUserGroups') || [];
 
  var categories = mw.config.get('wgCategories') || [];
   function addHistoryToNewPage() {
  if (groups.indexOf('sysop') !== -1 || categories.indexOf('Crystals') === -1) {
    var action = mw.config.get('wgAction');
     return;
    var articleId = Number(mw.config.get('wgArticleId') || 0);
    var namespace = Number(mw.config.get('wgNamespaceNumber'));
    var pageName = String(mw.config.get('wgPageName') || '');
    var textbox;
    var marker;
 
    if (action !== 'edit' || articleId !== 0 || namespace !== 0 || /\/Change_History$/.test(pageName)) {
      return;
    }
 
    textbox = document.getElementById('wpTextbox1');
    if (!textbox || /\{\{\s*Change History\b/i.test(textbox.value)) {
      return;
    }
 
    marker = '\n\n== Change History ==\n{{Change History|{{' + 'subst:Current version}}|{{' + 'subst:Current version page}}}}\n';
    textbox.value = textbox.value.replace(/\s+$/, '') + marker;
     textbox.dispatchEvent(new Event('input', { bubbles: true }));
   }
   }
   function hideCrystalFormActions() {
 
     ['ca-formedit', 'ca-formcreate', 'ca-formedit-sticky-header', 'ca-formcreate-sticky-header'].forEach(function (id) {
   function enableHistoryEditorTools() {
       var element = document.getElementById(id);
     var userName = mw.config.get('wgUserName');
       if (element) {
    var probablyEditable = mw.config.get('wgIsProbablyEditable');
         element.hidden = true;
    var releases;
 
    if (!userName || probablyEditable === false) {
      return;
    }
 
    releases = document.querySelectorAll('.hw-change-history-release');
    if (!releases.length) {
      return;
    }
 
    document.body.classList.add('hw-history-tools-enabled');
 
    releases.forEach(function (release) {
       var addLink = release.querySelector('.hw-history-add a, a.hw-history-add');
      var version = String(release.getAttribute('data-current-version') || '').trim();
      var series = String(release.getAttribute('data-current-series') || '').trim();
      var historyPage = String(release.getAttribute('data-history-page') || '').trim();
      var params;
 
       if (!addLink || !version || !series || !historyPage) {
         return;
       }
       }
      params = new URLSearchParams();
      params.set('title', historyPage);
      params.set('action', 'edit');
      params.set('section', 'new');
      params.set('preload', 'Template:Change History/entry');
      params.set('preloadtitle', 'Version ' + version);
      params.append('preloadparams[]', version);
      params.append('preloadparams[]', series);
      addLink.href = mw.util.wikiScript('index') + '?' + params.toString();
     });
     });
   }
   }
  function initVersionHistory() {
    addHistoryToNewPage();
    enableHistoryEditorTools();
    window.setTimeout(addHistoryToNewPage, 350);
    window.setTimeout(enableHistoryEditorTools, 350);
  }
  if (mw.hook) {
    mw.hook('wikipage.editform').add(function () {
      window.setTimeout(addHistoryToNewPage, 100);
    });
    mw.hook('wikipage.content').add(function () {
      window.setTimeout(enableHistoryEditorTools, 100);
    });
  }
   if (document.readyState === 'loading') {
   if (document.readyState === 'loading') {
     document.addEventListener('DOMContentLoaded', hideCrystalFormActions);
     document.addEventListener('DOMContentLoaded', initVersionHistory, { once: true });
   } else {
   } else {
     hideCrystalFormActions();
     initVersionHistory();
   }
   }
}());
}());


 
/* HarvestWiki version snapshots for source edits, Page Forms, and administrators */
/* HarvestWiki admin-only Weather form UI */
(function () {
(function () {
   'use strict';
   'use strict';
   var groups = mw.config.get('wgUserGroups') || [];
 
   var categories = mw.config.get('wgCategories') || [];
  var MARKER_KEY = 'hwVersionHistoryPending';
   if (groups.indexOf('sysop') !== -1 || categories.indexOf('Weather') === -1) {
  var ADMIN_PAGE = 'HarvestWiki:Version control';
     return;
  var AUTO_START = '<!-- HW-AUTO-START:';
 
  function api() {
    return new mw.Api();
  }
 
  function isSignedInEditor() {
    return Boolean(mw.config.get('wgUserName')) && mw.config.get('wgIsProbablyEditable') !== false;
  }
 
   function isVersionAdmin() {
    var groups = mw.config.get('wgUserGroups') || [];
    return groups.indexOf('sysop') !== -1 || groups.indexOf('interface-admin') !== -1 || groups.indexOf('bureaucrat') !== -1;
  }
 
  function normalTitle(title) {
    return String(title || '').replace(/_/g, ' ').trim();
  }
 
   function seriesFor(version) {
    var parts = String(version || '').split('.');
    return parts.length === 3 ? parts[0] + '.' + parts[1] : '';
  }
 
  function validVersion(version) {
    return /^\d+\.\d{2}\.\d+$/.test(String(version || '').trim());
  }
 
  function getPageText(title) {
    return api().get({
      action: 'query',
      prop: 'revisions',
      titles: title,
      rvprop: 'content',
      rvslots: 'main',
      formatversion: 2
    }).then(function (data) {
      var page = data && data.query && data.query.pages && data.query.pages[0];
      var revision = page && page.revisions && page.revisions[0];
      var slots = revision && revision.slots;
      return {
        exists: Boolean(page && !page.missing),
        text: slots && slots.main && typeof slots.main.content === 'string' ? slots.main.content : ''
      };
    });
  }
 
  function savePage(title, text, summary, createOnly) {
    var params = {
      action: 'edit',
      title: title,
      text: text,
      summary: summary,
      watchlist: 'nochange',
      formatversion: 2
    };
    if (createOnly) {
      params.createonly = 1;
    }
    return api().postWithToken('csrf', params);
  }
 
  function readCurrentVersion() {
    return Promise.all([
      getPageText('Template:Current version'),
      getPageText('Template:Current version page')
    ]).then(function (pages) {
      var versionMatch = pages[0].text.match(/\d+\.\d{2}\.\d+/);
      var seriesMatch = pages[1].text.match(/\d+\.\d{2}/);
      var version = versionMatch ? versionMatch[0] : '';
      var series = seriesMatch ? seriesMatch[0] : seriesFor(version);
      if (!validVersion(version) || !series) {
        throw new Error('The current version templates do not contain a valid x.xx.x version.');
      }
      return { version: version, series: series };
    });
  }
 
  function cleanFieldName(name) {
    var raw = String(name || '');
    var matches = raw.match(/\[([^\]]+)\]/g);
    var last;
    if (matches && matches.length) {
      last = matches[matches.length - 1].replace(/^\[|\]$/g, '');
    } else {
      last = raw;
    }
    last = last.replace(/^pf_free_text$/i, 'Article content')
      .replace(/^free_text$/i, 'Article content')
      .replace(/_/g, ' ')
      .replace(/\s+/g, ' ')
      .trim();
    return last;
  }
 
  function labelFor(key) {
    return String(key || '')
      .replace(/[_-]+/g, ' ')
      .replace(/\b\w/g, function (letter) { return letter.toUpperCase(); });
   }
 
  function normalizeValue(value) {
    return String(value == null ? '' : value).replace(/\r\n?/g, '\n').replace(/[ \t]+/g, ' ').trim();
  }
 
  function collectFormSnapshot(form) {
    var values = {};
    var ignored = /^(wp(EditToken|Starttime|Edittime|AutoSummary|Summary|Section|UltimateParam)|title|action|format|oldid)$/i;
    Array.prototype.forEach.call(form.querySelectorAll('input[name], select[name], textarea[name]'), function (field) {
      var type = String(field.type || '').toLowerCase();
      var name = String(field.name || '');
      var key;
      var value;
      if (!name || field.disabled || ignored.test(name) || /submit|button|reset|image|file/.test(type)) {
        return;
      }
      if ((type === 'checkbox' || type === 'radio') && !field.checked) {
        return;
      }
      if (type === 'hidden' && name.indexOf('[') === -1 && name !== 'pf_free_text') {
        return;
      }
      key = cleanFieldName(name);
      if (!key || /token|timestamp|summary/i.test(key)) {
        return;
      }
      if (field.tagName === 'SELECT' && field.multiple) {
        value = Array.prototype.filter.call(field.options, function (option) { return option.selected; })
          .map(function (option) { return option.value; }).join('; ');
      } else {
        value = field.value;
      }
      value = normalizeValue(value);
      if (Object.prototype.hasOwnProperty.call(values, key) && value && values[key] !== value) {
        values[key] = normalizeValue(values[key] + '; ' + value);
      } else if (!Object.prototype.hasOwnProperty.call(values, key) || value) {
        values[key] = value;
      }
    });
    return values;
  }
 
  function snapshotFromWikitext(text) {
    var source = String(text || '');
    var snapshot = {};
    var match;
    var paramPattern = /^\s*\|\s*([^=\n]+?)\s*=\s*([\s\S]*?)(?=^\s*\|\s*[^=\n]+?\s*=|^\s*}}|\z)/gm;
    var cleaned = source.replace(/==\s*Change History\s*==[\s\S]*$/i, '').trim();
    while ((match = paramPattern.exec(cleaned))) {
      snapshot[cleanFieldName(match[1])] = normalizeValue(match[2]);
    }
    if (!Object.keys(snapshot).length) {
      snapshot['Article content'] = normalizeValue(cleaned);
    }
    return snapshot;
  }
 
  function snapshotForForm(form) {
    var textbox = form.querySelector('#wpTextbox1, textarea[name="wpTextbox1"]');
    if (textbox && String(mw.config.get('wgAction')) === 'edit') {
      return snapshotFromWikitext(textbox.value);
    }
    return collectFormSnapshot(form);
  }
 
  function sameSnapshot(a, b) {
    var aKeys = Object.keys(a || {}).sort();
    var bKeys = Object.keys(b || {}).sort();
    if (aKeys.length !== bKeys.length) {
      return false;
    }
    return aKeys.every(function (key, index) {
      return key === bKeys[index] && normalizeValue(a[key]) === normalizeValue(b[key]);
    });
  }
 
  function encodeSnapshot(snapshot) {
    return btoa(unescape(encodeURIComponent(JSON.stringify(snapshot || {}))));
  }
 
  function decodeSnapshot(encoded) {
    try {
      return JSON.parse(decodeURIComponent(escape(atob(encoded))));
    } catch (error) {
      return {};
    }
  }
 
  function historyBlocks(text) {
    var blocks = [];
    var pattern = /<!-- HW-AUTO-START:([0-9]+\.[0-9]{2}\.[0-9]+) -->([\s\S]*?)<!-- HW-AUTO-END:\1 -->/g;
    var match;
    var snapshotMatch;
    while ((match = pattern.exec(String(text || '')))) {
      snapshotMatch = match[2].match(/<!-- HW-SNAPSHOT:([A-Za-z0-9+/=]+) -->/);
      blocks.push({
        version: match[1],
        full: match[0],
        index: match.index,
        created: /\*\s*Page created\./i.test(match[2]),
        snapshot: snapshotMatch ? decodeSnapshot(snapshotMatch[1]) : {}
      });
    }
    return blocks;
  }
 
  function shortValue(value) {
    var clean = normalizeValue(value);
    if (clean.length > 120) {
      clean = clean.slice(0, 117) + '...';
    }
     return clean;
   }
   }
   function hideWeatherFormActions() {
 
     ['ca-formedit', 'ca-formcreate', 'ca-formedit-sticky-header', 'ca-formcreate-sticky-header'].forEach(function (id) {
   function changeMessages(before, after) {
       var element = document.getElementById(id);
    var keys = {};
       if (element) {
    var messages = [];
         element.hidden = true;
    Object.keys(before || {}).forEach(function (key) { keys[key] = true; });
     Object.keys(after || {}).forEach(function (key) { keys[key] = true; });
    Object.keys(keys).sort().forEach(function (key) {
       var oldValue = normalizeValue((before || {})[key]);
      var newValue = normalizeValue((after || {})[key]);
      var label = labelFor(key);
      if (oldValue === newValue) {
        return;
      }
      if (!oldValue && newValue) {
        messages.push('Added ' + label + ': “' + shortValue(newValue) + '”.');
       } else if (oldValue && !newValue) {
         messages.push('Removed ' + label + ' (previously “' + shortValue(oldValue) + '”).');
      } else if (key === 'Article content') {
        messages.push('Updated the article content.');
      } else {
        messages.push('Changed ' + label + ' from “' + shortValue(oldValue) + '” to “' + shortValue(newValue) + '”.');
       }
       }
     });
     });
    if (messages.length > 12) {
      messages = messages.slice(0, 12).concat(['Updated ' + (messages.length - 12) + ' additional fields.']);
    }
    return messages;
  }
  function buildBlock(version, series, messages, snapshot) {
    return AUTO_START + version + ' -->\n' +
      '== Version ' + version + ' ==\n' +
      "'''{{Version link|" + version + '|' + series + "}}'''\n" +
      messages.map(function (message) { return '* ' + message; }).join('\n') + '\n' +
      '<!-- HW-SNAPSHOT:' + encodeSnapshot(snapshot) + ' -->\n' +
      '<!-- HW-AUTO-END:' + version + ' -->';
   }
   }
   if (document.readyState === 'loading') {
 
     document.addEventListener('DOMContentLoaded', hideWeatherFormActions);
   function upsertHistory(existingText, versionInfo, afterSnapshot, isNewPage, forceCreated) {
   } else {
    var text = String(existingText || '').trim();
     hideWeatherFormActions();
    var blocks = historyBlocks(text);
    var current = blocks.filter(function (block) { return block.version === versionInfo.version; })[0];
    var previous = blocks.filter(function (block) { return block.version !== versionInfo.version && Object.keys(block.snapshot).length; })[0];
    var beforeSnapshot = previous ? previous.snapshot : {};
    var messages;
    var replacement;
 
    if (current && current.created) {
      messages = ['Page created.'];
    } else if ((isNewPage || forceCreated) && !blocks.length) {
      messages = ['Page created.'];
    } else {
      messages = changeMessages(beforeSnapshot, afterSnapshot);
      if (!messages.length) {
        return { changed: false, text: text };
      }
    }
 
     replacement = buildBlock(versionInfo.version, versionInfo.series, messages, afterSnapshot);
    if (current) {
      return { changed: replacement !== current.full, text: text.replace(current.full, replacement) };
    }
    return { changed: true, text: replacement + (text ? '\n\n' + text : '') };
   }
 
  function historyMarkup(versionInfo) {
     return '\n\n== Change History ==\n{{Change History|' + versionInfo.version + '|' + versionInfo.series + '}}\n';
   }
   }
}());


  function ensureHistorySection(title, versionInfo) {
    return getPageText(title).then(function (page) {
      if (!page.exists || /{{\s*Change History\b/i.test(page.text)) {
        return false;
      }
      return savePage(title, page.text.replace(/\s+$/, '') + historyMarkup(versionInfo), 'Add version change history section').then(function () {
        return true;
      });
    });
  }


/* HarvestWiki four-zodiac celestial calendar */
  function recordAutomaticHistory(title, versionInfo, afterSnapshot, isNewPage, forceCreated) {
(function () {
    var historyTitle = title + '/Change History';
  'use strict';
    return getPageText(historyTitle).then(function (historyPage) {
      var updated = upsertHistory(historyPage.text, versionInfo, afterSnapshot, isNewPage, forceCreated);
      if (!updated.changed) {
        return false;
      }
      return savePage(historyTitle, updated.text + '\n', 'Record automatic change history for ' + versionInfo.version).then(function () {
        return true;
      });
    });
  }


   var anchor = Date.UTC(2026, 0, 1, 0, 0, 0);
   function setPendingEnsure(title, versionInfo) {
  var seasonLength = 8 * 60 * 60 * 1000;
    try {
  var seasonNames = [
      sessionStorage.setItem(MARKER_KEY, JSON.stringify({
    'Year of the Dragon',
        title: title,
    'Year of the Tiger',
        version: versionInfo.version,
    'Year of the Phoenix',
        series: versionInfo.series,
     'Year of the Tortoise'
        time: Date.now()
   ];
      }));
     } catch (error) {}
   }


   function formatDuration(milliseconds) {
   function completePendingEnsure() {
     var seconds = Math.max(0, Math.ceil(milliseconds / 1000));
     var raw;
     var hours = Math.floor(seconds / 3600);
    var pending;
     var minutes = Math.floor((seconds % 3600) / 60);
    try {
     var remainder = seconds % 60;
      raw = sessionStorage.getItem(MARKER_KEY);
     return String(hours).padStart(2, '0') + ':' +
      pending = raw ? JSON.parse(raw) : null;
      String(minutes).padStart(2, '0') + ':' +
    } catch (error) {
       String(remainder).padStart(2, '0');
      pending = null;
     }
    if (!pending || !pending.title || Date.now() - Number(pending.time || 0) > 300000) {
      return;
     }
    if (normalTitle(mw.config.get('wgPageName')) !== normalTitle(pending.title) || Number(mw.config.get('wgArticleId') || 0) === 0) {
      return;
     }
    sessionStorage.removeItem(MARKER_KEY);
     ensureHistorySection(pending.title, { version: pending.version, series: pending.series }).then(function (changed) {
      if (changed) {
        mw.notify('The page was saved and its change-history section was added.', { type: 'success' });
        window.setTimeout(function () { window.location.reload(); }, 700);
       }
    }).catch(function (error) {
      mw.notify('The page saved, but its change-history section could not be added: ' + error.message, { type: 'error' });
    });
   }
   }


   function initZodiacCycles(root) {
   function initTrackedForm() {
     var widgets = [];
     var namespace = Number(mw.config.get('wgNamespaceNumber'));
     if (root && root.matches && root.matches('[data-hw-zodiac-cycle]')) {
    var specialFormMatch = String(window.location.pathname || '').match(/\/Special:FormEdit\/[^/]+\/(.+)$/i);
       widgets.push(root);
    var form;
    var initialSnapshot;
    var isNewPage;
    var pageTitle;
 
     if (!mw.config.get('wgUserName') || (!specialFormMatch && namespace !== 0) || /\/Change_History$/.test(String(mw.config.get('wgPageName') || ''))) {
       return;
     }
     }
     if (root && root.querySelectorAll) {
 
      widgets = widgets.concat(Array.prototype.slice.call(root.querySelectorAll('[data-hw-zodiac-cycle]')));
     form = document.querySelector('form#pfForm, form#sfForm, form#editform');
    if (!form || form.dataset.hwVersionTrackingReady === '1') {
      return;
     }
     }


     widgets.forEach(function (widget) {
     form.dataset.hwVersionTrackingReady = '1';
       if (widget.dataset.hwZodiacReady === '1') {
    initialSnapshot = snapshotForForm(form);
    isNewPage = Boolean(specialFormMatch) || Number(mw.config.get('wgArticleId') || 0) === 0;
    pageTitle = specialFormMatch ? normalTitle(decodeURIComponent(specialFormMatch[1])) : normalTitle(mw.config.get('wgPageName'));
 
    form.addEventListener('submit', function (event) {
      var submitter = event.submitter;
      var outgoing;
      var submitName = submitter ? String(submitter.name || submitter.value || '') : '';
 
       if (form.dataset.hwVersionSubmitting === '1' || /preview|diff|changes|cancel/i.test(submitName)) {
         return;
         return;
       }
       }
      widget.dataset.hwZodiacReady = '1';


       var currentLabel = widget.querySelector('[data-hw-zodiac-current]');
       outgoing = snapshotForForm(form);
       var countdown = widget.querySelector('[data-hw-zodiac-countdown]');
       if (!isNewPage && sameSnapshot(initialSnapshot, outgoing)) {
      var progress = widget.querySelector('[data-hw-zodiac-progress]');
        return;
       var seasons = Array.prototype.slice.call(widget.querySelectorAll('[data-hw-zodiac-season]'));
       }


       function updateZodiacCycle() {
       event.preventDefault();
        var now = Date.now();
      form.dataset.hwVersionSubmitting = '1';
        var step = Math.floor((now - anchor) / seasonLength);
      mw.notify('Saving the page and recording its version history…');
        var currentIndex = ((step % seasonNames.length) + seasonNames.length) % seasonNames.length;
        var nextBoundary = anchor + ((step + 1) * seasonLength);
        var remaining = Math.max(0, nextBoundary - now);
        var elapsedInSeason = ((now - anchor) % seasonLength + seasonLength) % seasonLength;


         if (currentLabel) {
      readCurrentVersion().then(function (versionInfo) {
           currentLabel.textContent = seasonNames[currentIndex];
         return recordAutomaticHistory(pageTitle, versionInfo, outgoing, isNewPage, false).then(function () {
           setPendingEnsure(pageTitle, versionInfo);
        });
      }).catch(function (error) {
        mw.notify('The page will still be saved, but automatic history failed: ' + error.message, { type: 'warn' });
      }).then(function () {
        var saveField = form.querySelector('input[type="hidden"][name="wpSave"]');
        if (!saveField) {
          saveField = document.createElement('input');
          saveField.type = 'hidden';
          saveField.name = 'wpSave';
          saveField.value = 'Save page';
          form.appendChild(saveField);
         }
         }
         if (countdown) {
         HTMLFormElement.prototype.submit.call(form);
          countdown.textContent = formatDuration(remaining);
      });
          countdown.title = 'Changes at ' + new Date(nextBoundary).toLocaleString();
    }, true);
         }
  }
         if (progress) {
 
          progress.style.width = ((elapsedInSeason / seasonLength) * 100).toFixed(3) + '%';
  function allMainPages() {
    var results = [];
    function next(apcontinue) {
      var params = { action: 'query', list: 'allpages', apnamespace: 0, aplimit: 'max', formatversion: 2 };
      if (apcontinue) {
        params.apcontinue = apcontinue;
      }
      return api().get(params).then(function (data) {
        results = results.concat((data.query && data.query.allpages) || []);
        return data.continue && data.continue.apcontinue ? next(data.continue.apcontinue) : results;
      });
    }
    return next();
  }
 
  function backfillOne(title, versionInfo, forceCreated) {
    if (title === 'Change History' || /\/Change History$/.test(title)) {
      return Promise.resolve({ title: title, skipped: true });
    }
    return getPageText(title).then(function (page) {
      if (!page.exists) {
         return { title: title, skipped: true };
      }
      var snapshot = snapshotFromWikitext(page.text);
      return Promise.all([
         ensureHistorySection(title, versionInfo),
        recordAutomaticHistory(title, versionInfo, snapshot, false, forceCreated)
      ]).then(function () {
        return { title: title, skipped: false };
      });
    });
  }
 
  function runBackfill(versionInfo, forceCreated, progress) {
    return allMainPages().then(function (pages) {
      var queue = pages.map(function (page) { return normalTitle(page.title); });
      var done = 0;
      var failures = [];
      var workers = [];
 
      function worker() {
        var title = queue.shift();
        if (!title) {
          return Promise.resolve();
         }
         }
        return backfillOne(title, versionInfo, forceCreated).catch(function (error) {
          failures.push(title + ': ' + error.message);
        }).then(function () {
          done += 1;
          progress(done, pages.length, title, failures.length);
          return worker();
        });
      }
      for (var i = 0; i < 3; i += 1) {
        workers.push(worker());
      }
      return Promise.all(workers).then(function () {
        return { total: pages.length, failures: failures };
      });
    });
  }
  function currentTemplateText(version) {
    return '<includeonly>' + version + '</includeonly><noinclude>Sitewide current patch version. Change this through [[HarvestWiki:Version control]]. Version numbers use x.xx.x. [[Category:Version templates]]</noinclude>';
  }
  function currentSeriesText(series) {
    return '<includeonly>' + series + '</includeonly><noinclude>Parent release-series number for the current patch. Change this through [[HarvestWiki:Version control]]. [[Category:Version templates]]</noinclude>';
  }
  function releasePageText(versionInfo, title, date) {
    return '{{Version\n|version=' + versionInfo.series + '\n|title=' + title + '\n|release date=' + date + '\n|current patch=' + versionInfo.version + '\n}}\n\n== Overview ==\nDescribe the goals and scope of this release series.\n\n== Changelogs ==\n=== ' + versionInfo.version + ' ===\n' + title + ' — ' + date + '\n* Add release changes here.\n\n[[Category:Versions]]\n';
  }


        seasons.forEach(function (season, index) {
  function updateReleasePage(versionInfo, title, date) {
          var offset = (index - currentIndex + seasonNames.length) % seasonNames.length;
    var pageTitle = 'Version ' + versionInfo.series;
          var state = season.querySelector('[data-hw-zodiac-state]');
    return getPageText(pageTitle).then(function (page) {
          var startsIn = remaining + (Math.max(0, offset - 1) * seasonLength);
      var text;
          var changeAt = offset === 0 ? nextBoundary : now + startsIn;
      if (!page.exists) {
          season.classList.toggle('is-current', offset === 0);
        return savePage(pageTitle, releasePageText(versionInfo, title, date), 'Create release page for ' + versionInfo.version);
          season.setAttribute('aria-current', offset === 0 ? 'true' : 'false');
      }
          season.title = (offset === 0 ? 'Ends ' : 'Begins ') + new Date(changeAt).toLocaleString();
      text = page.text;
          if (state) {
      if (/\|\s*current patch\s*=/.test(text)) {
            if (offset === 0) {
        text = text.replace(/(\|\s*current patch\s*=\s*)[^\n|}]*/i, '$1' + versionInfo.version);
              state.textContent = 'Current · ends in ' + formatDuration(remaining);
      }
            } else if (offset === 1) {
      if (text.indexOf('=== ' + versionInfo.version + ' ===') === -1) {
              state.textContent = 'Next · starts in ' + formatDuration(startsIn);
        text = text.replace(/\s*\[\[Category:Versions\]\]\s*$/i, '') +
            } else {
          '\n\n=== ' + versionInfo.version + ' ===\n' + title + ' ' + date + '\n* Add release changes here.\n\n[[Category:Versions]]\n';
              state.textContent = 'Starts in ' + formatDuration(startsIn);
            }
          }
        });
       }
       }
      return savePage(pageTitle, text, 'Set current patch to ' + versionInfo.version);
    });
  }


       updateZodiacCycle();
  function applyVersion(version, title, date) {
       window.setInterval(updateZodiacCycle, 1000);
    var versionInfo = { version: version, series: seriesFor(version) };
    return Promise.all([
       savePage('Template:Current version', currentTemplateText(version), 'Set current version to ' + version),
       savePage('Template:Current version page', currentSeriesText(versionInfo.series), 'Set current version series to ' + versionInfo.series),
      updateReleasePage(versionInfo, title, date)
    ]).then(function () {
      return versionInfo;
     });
     });
   }
   }


   mw.hook('wikipage.content').add(function ($content) {
   function initVersionAdmin() {
     initZodiacCycles($content && $content[0] ? $content[0] : document);
     var root;
  });
    var status;
}());
    var versionInput;
    var titleInput;
    var dateInput;
    var applyButton;
    var backfillButton;
    var snapshotButton;


    if (normalTitle(mw.config.get('wgPageName')) !== ADMIN_PAGE) {
      return;
    }


/* HarvestWiki multi-page editor controls and archive refresh */
    root = document.getElementById('hw-version-control-root');
(function () {
    if (!root) {
  'use strict';
      return;
    }


  function initBulkEditors(root) {
     if (!isVersionAdmin()) {
    var grids = [];
      root.innerHTML = '<div class="hw-note">Administrator access is required.</div>';
     if (root && root.matches && root.matches('.pfSpreadsheet')) { grids.push(root); }
      return;
    if (root && root.querySelectorAll) {
      grids = grids.concat(Array.prototype.slice.call(root.querySelectorAll('.pfSpreadsheet')));
     }
     }


     grids.forEach(function (grid) {
     document.body.classList.add('hw-version-admin-authorized');
       if (grid.dataset.hwBulkEditorReady === '1') { return; }
    root.innerHTML = '<div class="hw-version-admin-card">' +
       grid.dataset.hwBulkEditorReady = '1';
      '<h2>Current release</h2>' +
       '<div class="hw-version-admin-grid">' +
      '<label>Version number <input id="hw-version-number" type="text" placeholder="0.01.0" pattern="\d+\.\d{2}\.\d+"></label>' +
      '<label>Release title <input id="hw-version-title" type="text" placeholder="Early Development"></label>' +
      '<label>Release date <input id="hw-version-date" type="text" placeholder="29 July 2026"></label>' +
      '</div>' +
      '<div class="hw-version-admin-actions"><button id="hw-version-apply" type="button">Apply version and create release page</button></div>' +
      '</div>' +
      '<div class="hw-version-admin-card"><h2>Page history tools</h2>' +
      '<p><button id="hw-version-backfill" type="button">Backfill untracked pages as created now</button></p>' +
      '<p><button id="hw-version-snapshot" type="button">Snapshot every page in the current version</button></p>' +
       '<p class="hw-version-admin-help">Backfill is for legacy pages. Snapshot creates a current-version baseline only where the page differs from its last stored snapshot.</p>' +
      '</div>' +
      '<div id="hw-version-status" class="hw-version-status" role="status" aria-live="polite"></div>';


      var parameters = new URLSearchParams(window.location.search);
    status = document.getElementById('hw-version-status');
      var templateName = parameters.get('template') || grid.id.replace(/Grid$/, '');
    versionInput = document.getElementById('hw-version-number');
      var archivePage = templateName === 'Herb' ? 'Herbs' :
    titleInput = document.getElementById('hw-version-title');
        (templateName === 'Crystal' ? 'Crystals' : null);
    dateInput = document.getElementById('hw-version-date');
      var saveLinks = Array.prototype.slice.call(grid.querySelectorAll('a.save-changes'));
    applyButton = document.getElementById('hw-version-apply');
    backfillButton = document.getElementById('hw-version-backfill');
    snapshotButton = document.getElementById('hw-version-snapshot');


      saveLinks.forEach(function (link) {
    readCurrentVersion().then(function (info) {
        link.setAttribute('title', 'Save this row');
      versionInput.value = info.version;
        link.setAttribute('aria-label', 'Save this row');
    });
        link.addEventListener('click', function () {
          link.classList.remove('is-saved');
          link.classList.add('is-saving');


          function refreshArchive() {
    function setBusy(busy) {
            if (!archivePage || typeof mw.Api !== 'function') { return; }
      applyButton.disabled = busy;
            new mw.Api().post({
      backfillButton.disabled = busy;
              action: 'purge',
      snapshotButton.disabled = busy;
              titles: archivePage,
    }
              formatversion: 2
            }).then(function () {
              link.classList.remove('is-saving');
              link.classList.add('is-saved');
              window.setTimeout(function () { link.classList.remove('is-saved'); }, 2200);
            });
          }


          window.setTimeout(refreshArchive, 1600);
    function progress(done, total, title, failures) {
          window.setTimeout(refreshArchive, 4200);
      status.textContent = 'Processed ' + done + ' of ' + total + ': ' + title + (failures ? ' (' + failures + ' failed)' : '');
        });
    }
      });


      if (!grid.querySelector('.hw-bulk-editor-note')) {
    applyButton.addEventListener('click', function () {
        var note = document.createElement('div');
      var version = versionInput.value.trim();
        note.className = 'hw-bulk-editor-note';
      var title = titleInput.value.trim();
         note.textContent = 'Scroll sideways to reach every field. Use Save at the right of each row.' +
      var date = dateInput.value.trim();
          (archivePage ? ' Saved rows automatically refresh the ' + archivePage + ' index.' : '');
      if (!validVersion(version) || !title || !date) {
         grid.appendChild(note);
         status.textContent = 'Enter a version in x.xx.x format, a title, and a release date.';
         return;
       }
       }
      setBusy(true);
      status.textContent = 'Applying ' + version + '…';
      applyVersion(version, title, date).then(function () {
        status.textContent = 'Current version is now ' + version + '. The release page is ready.';
      }).catch(function (error) {
        status.textContent = 'Version update failed: ' + error.message;
      }).then(function () { setBusy(false); });
    });
    backfillButton.addEventListener('click', function () {
      setBusy(true);
      status.textContent = 'Reading current version…';
      readCurrentVersion().then(function (info) {
        return runBackfill(info, true, progress);
      }).then(function (result) {
        status.textContent = 'Backfill complete: ' + result.total + ' pages checked, ' + result.failures.length + ' failed.';
      }).catch(function (error) {
        status.textContent = 'Backfill failed: ' + error.message;
      }).then(function () { setBusy(false); });
    });
    snapshotButton.addEventListener('click', function () {
      setBusy(true);
      status.textContent = 'Reading current version…';
      readCurrentVersion().then(function (info) {
        return runBackfill(info, false, progress);
      }).then(function (result) {
        status.textContent = 'Snapshot complete: ' + result.total + ' pages checked, ' + result.failures.length + ' failed.';
      }).catch(function (error) {
        status.textContent = 'Snapshot failed: ' + error.message;
      }).then(function () { setBusy(false); });
     });
     });
   }
   }


   mw.hook('wikipage.content').add(function ($content) {
   function initAutomaticVersioning() {
     initBulkEditors($content && $content[0] ? $content[0] : document);
    completePendingEnsure();
    initTrackedForm();
    initVersionAdmin();
    window.setTimeout(initTrackedForm, 500);
  }
 
  function startAutomaticVersioning() {
    if (mw.hook) {
      mw.hook('wikipage.editform').add(function () { window.setTimeout(initTrackedForm, 100); });
    }
 
     if (document.readyState === 'loading') {
      document.addEventListener('DOMContentLoaded', initAutomaticVersioning, { once: true });
    } else {
      initAutomaticVersioning();
    }
  }
 
  mw.loader.using(['mediawiki.api', 'mediawiki.util'], startAutomaticVersioning, function (error) {
    if (window.console && console.error) {
      console.error('HarvestWiki version tracking could not load:', error);
    }
   });
   });
}());
}());

Latest revision as of 15:51, 29 July 2026

/* Any JavaScript here will be loaded for all users on every page load. */ /* HarvestWiki dynamic Cargo archive filters */ (function () { 'use strict'; function initHerbLocationFilters(root) { var catalogs = []; if (root && root.matches && root.matches('.hw-herb-catalog')) { catalogs.push(root); } if (root && root.querySelectorAll) { catalogs = catalogs.concat(Array.prototype.slice.call(root.querySelectorAll('.hw-herb-catalog'))); } catalogs.forEach(function (catalog) { if (catalog.dataset.hwLocationFiltersReady === '1') { return; } var table = catalog.querySelector('table'); var filterBar = document.querySelector('.hw-herb-filter'); if (!table || !filterBar) { return; } var headers = Array.prototype.slice.call(table.querySelectorAll('thead th')); var locationIndex = headers.findIndex(function (header) { return header.textContent.trim().toLowerCase() === 'location'; }); if (locationIndex < 0) { return; } var rows = Array.prototype.slice.call(table.querySelectorAll('tbody tr')); var counts = {}; rows.forEach(function (row) { var cell = row.querySelectorAll('td')[locationIndex]; var location = cell ? cell.textContent.trim() : ''; row.dataset.hwLocation = location; if (location) { counts[location] = (counts[location] || 0) + 1; } }); var locations = Object.keys(counts).sort(); var enabled = new Set(locations); filterBar.innerHTML = ''; var showAll = document.createElement('button'); showAll.type = 'button'; showAll.className = 'hw-filter-chip hw-filter-chip--all'; showAll.textContent = 'Show all'; filterBar.appendChild(showAll); var locationButtons = {}; locations.forEach(function (location) { var button = document.createElement('button'); button.type = 'button'; button.className = 'hw-filter-chip is-active'; button.setAttribute('aria-pressed', 'true'); button.textContent = location + ' (' + counts[location] + ')'; button.addEventListener('click', function () { if (enabled.has(location)) { enabled.delete(location); } else { enabled.add(location); } applyFilters(); }); locationButtons[location] = button; filterBar.appendChild(button); }); function applyFilters() { rows.forEach(function (row) { row.hidden = !enabled.has(row.dataset.hwLocation); }); locations.forEach(function (location) { var active = enabled.has(location); locationButtons[location].classList.toggle('is-active', active); locationButtons[location].setAttribute('aria-pressed', active ? 'true' : 'false'); }); showAll.disabled = enabled.size === locations.length; showAll.classList.toggle('is-active', enabled.size === locations.length); } showAll.addEventListener('click', function () { enabled = new Set(locations); applyFilters(); }); catalog.dataset.hwLocationFiltersReady = '1'; applyFilters(); }); } mw.hook('wikipage.content').add(function ($content) { initHerbLocationFilters($content && $content[0] ? $content[0] : document); }); }()); /* HarvestWiki admin-only Herb form UI */ (function () { 'use strict'; var groups = mw.config.get('wgUserGroups') || []; var categories = mw.config.get('wgCategories') || []; if (groups.indexOf('sysop') !== -1 || categories.indexOf('Herbs') === -1) { return; } function hideHerbFormActions() { ['ca-formedit', 'ca-formcreate', 'ca-formedit-sticky-header', 'ca-formcreate-sticky-header'].forEach(function (id) { var element = document.getElementById(id); if (element) { element.hidden = true; } }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', hideHerbFormActions); } else { hideHerbFormActions(); } }()); /* HarvestWiki rarity-colored Herb and Crystal table rows */ (function () { 'use strict'; var rarities = { common: true, uncommon: true, rare: true, legendary: true, mythic: true, secret: true, mortal: true, refined: true, spirit: true }; function normalizeRarity(value) { var rarity = (value || '').trim().toLowerCase(); return rarities[rarity] ? rarity : ''; } function applyRarityColors(root) { var scope = root && root.querySelectorAll ? root : document; var tables = []; if (scope.matches && scope.matches('table')) { tables.push(scope); } tables = tables.concat(Array.prototype.slice.call(scope.querySelectorAll('table'))); tables.forEach(function (table) { var headerCells = Array.prototype.slice.call(table.querySelectorAll('tr:first-child th, tr:first-child td')); var itemIndex = headerCells.findIndex(function (cell) { var label = cell.textContent.trim().toLowerCase(); return label === 'herb' || label === 'crystal' || cell.classList.contains('field_Herb') || cell.classList.contains('field_Crystal'); }); var rarityIndex = headerCells.findIndex(function (cell) { return cell.textContent.trim().toLowerCase() === 'rarity' || cell.classList.contains('field_Rarity'); }); Array.prototype.slice.call(table.rows).forEach(function (row) { var cells = Array.prototype.slice.call(row.cells); var itemCell = row.querySelector('td.field_Herb, td.field_Crystal'); var rarityCell = row.querySelector('td.field_Rarity'); if (!itemCell && itemIndex >= 0) { itemCell = cells[itemIndex]; } if (!rarityCell && rarityIndex >= 0) { rarityCell = cells[rarityIndex]; } if (!itemCell || !rarityCell || itemCell.tagName !== 'TD') { return; } var rarity = normalizeRarity(rarityCell.textContent); if (!rarity) { return; } row.classList.add('hw-rarity-row'); row.dataset.hwRarity = rarity; itemCell.classList.add('hw-rarity-item'); rarityCell.classList.add('hw-rarity-label'); }); table.dataset.hwRarityColorsReady = '1'; }); } mw.hook('wikipage.content').add(function ($content) { applyRarityColors($content && $content[0] ? $content[0] : document); }); }()); /* HarvestWiki default Herb rarity order */ (function () { 'use strict'; var rarityRank = { common: 0, uncommon: 1, rare: 2, legendary: 3, mythic: 4, secret: 5 }; function sortHerbTables(root) { var scope = root && root.querySelectorAll ? root : document; var tables = []; if (scope.matches && scope.matches('table')) { tables.push(scope); } tables = tables.concat(Array.prototype.slice.call(scope.querySelectorAll('table'))); tables.forEach(function (table) { if (table.dataset.hwHerbRaritySorted === '1') { return; } var headers = Array.prototype.slice.call(table.querySelectorAll('thead th')); if (!headers.length && table.rows.length) { headers = Array.prototype.slice.call(table.rows[0].cells); } var herbIndex = headers.findIndex(function (header) { return header.textContent.trim().toLowerCase() === 'herb' || header.classList.contains('field_Herb'); }); var rarityIndex = headers.findIndex(function (header) { return header.textContent.trim().toLowerCase() === 'rarity' || header.classList.contains('field_Rarity'); }); if (herbIndex < 0 || rarityIndex < 0) { return; } Array.prototype.slice.call(table.tBodies).forEach(function (body) { var rows = Array.prototype.slice.call(body.rows).map(function (row, index) { var rarityCell = row.cells[rarityIndex]; var rarity = rarityCell ? rarityCell.textContent.trim().toLowerCase() : ''; return { row: row, index: index, rank: Object.prototype.hasOwnProperty.call(rarityRank, rarity) ? rarityRank[rarity] : 999 }; }); rows.sort(function (a, b) { return a.rank - b.rank || a.index - b.index; }); var fragment = document.createDocumentFragment(); rows.forEach(function (entry) { fragment.appendChild(entry.row); }); body.appendChild(fragment); }); table.dataset.hwHerbRaritySorted = '1'; }); } mw.hook('wikipage.content').add(function ($content) { sortHerbTables($content && $content[0] ? $content[0] : document); }); }()); /* HarvestWiki default Crystal rarity order */ (function () { 'use strict'; var crystalRarityRank = { mortal: 0, refined: 1, spirit: 2 }; function sortCrystalTables(root) { var scope = root && root.querySelectorAll ? root : document; var tables = []; if (scope.matches && scope.matches('table')) { tables.push(scope); } tables = tables.concat(Array.prototype.slice.call(scope.querySelectorAll('table'))); tables.forEach(function (table) { if (table.dataset.hwCrystalRaritySorted === '1') { return; } var headers = Array.prototype.slice.call(table.querySelectorAll('thead th')); if (!headers.length && table.rows.length) { headers = Array.prototype.slice.call(table.rows[0].cells); } var crystalIndex = headers.findIndex(function (header) { return header.textContent.trim().toLowerCase() === 'crystal' || header.classList.contains('field_Crystal'); }); var rarityIndex = headers.findIndex(function (header) { return header.textContent.trim().toLowerCase() === 'rarity' || header.classList.contains('field_Rarity'); }); if (crystalIndex < 0 || rarityIndex < 0) { return; } Array.prototype.slice.call(table.tBodies).forEach(function (body) { var rows = Array.prototype.slice.call(body.rows).map(function (row, index) { var rarityCell = row.cells[rarityIndex]; var rarity = rarityCell ? rarityCell.textContent.trim().toLowerCase() : ''; return { row: row, index: index, rank: Object.prototype.hasOwnProperty.call(crystalRarityRank, rarity) ? crystalRarityRank[rarity] : 999 }; }); rows.sort(function (a, b) { return a.rank - b.rank || a.index - b.index; }); var fragment = document.createDocumentFragment(); rows.forEach(function (entry) { fragment.appendChild(entry.row); }); body.appendChild(fragment); }); table.dataset.hwCrystalRaritySorted = '1'; }); } mw.hook('wikipage.content').add(function ($content) { sortCrystalTables($content && $content[0] ? $content[0] : document); }); }()); /* HarvestWiki admin-only Crystal form UI */ (function () { 'use strict'; var groups = mw.config.get('wgUserGroups') || []; var categories = mw.config.get('wgCategories') || []; if (groups.indexOf('sysop') !== -1 || categories.indexOf('Crystals') === -1) { return; } function hideCrystalFormActions() { ['ca-formedit', 'ca-formcreate', 'ca-formedit-sticky-header', 'ca-formcreate-sticky-header'].forEach(function (id) { var element = document.getElementById(id); if (element) { element.hidden = true; } }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', hideCrystalFormActions); } else { hideCrystalFormActions(); } }()); /* HarvestWiki admin-only Weather form UI */ (function () { 'use strict'; var groups = mw.config.get('wgUserGroups') || []; var categories = mw.config.get('wgCategories') || []; if (groups.indexOf('sysop') !== -1 || categories.indexOf('Weather') === -1) { return; } function hideWeatherFormActions() { ['ca-formedit', 'ca-formcreate', 'ca-formedit-sticky-header', 'ca-formcreate-sticky-header'].forEach(function (id) { var element = document.getElementById(id); if (element) { element.hidden = true; } }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', hideWeatherFormActions); } else { hideWeatherFormActions(); } }()); /* HarvestWiki four-zodiac celestial calendar */ (function () { 'use strict'; var anchor = Date.UTC(2026, 0, 1, 0, 0, 0); var seasonLength = 8 * 60 * 60 * 1000; var seasonNames = [ 'Year of the Dragon', 'Year of the Tiger', 'Year of the Phoenix', 'Year of the Tortoise' ]; function formatDuration(milliseconds) { var seconds = Math.max(0, Math.ceil(milliseconds / 1000)); var hours = Math.floor(seconds / 3600); var minutes = Math.floor((seconds % 3600) / 60); var remainder = seconds % 60; return String(hours).padStart(2, '0') + ':' + String(minutes).padStart(2, '0') + ':' + String(remainder).padStart(2, '0'); } function initZodiacCycles(root) { var widgets = []; if (root && root.matches && root.matches('[data-hw-zodiac-cycle]')) { widgets.push(root); } if (root && root.querySelectorAll) { widgets = widgets.concat(Array.prototype.slice.call(root.querySelectorAll('[data-hw-zodiac-cycle]'))); } widgets.forEach(function (widget) { if (widget.dataset.hwZodiacReady === '1') { return; } widget.dataset.hwZodiacReady = '1'; var currentLabel = widget.querySelector('[data-hw-zodiac-current]'); var countdown = widget.querySelector('[data-hw-zodiac-countdown]'); var progress = widget.querySelector('[data-hw-zodiac-progress]'); var seasons = Array.prototype.slice.call(widget.querySelectorAll('[data-hw-zodiac-season]')); function updateZodiacCycle() { var now = Date.now(); var step = Math.floor((now - anchor) / seasonLength); var currentIndex = ((step % seasonNames.length) + seasonNames.length) % seasonNames.length; var nextBoundary = anchor + ((step + 1) * seasonLength); var remaining = Math.max(0, nextBoundary - now); var elapsedInSeason = ((now - anchor) % seasonLength + seasonLength) % seasonLength; if (currentLabel) { currentLabel.textContent = seasonNames[currentIndex]; } if (countdown) { countdown.textContent = formatDuration(remaining); countdown.title = 'Changes at ' + new Date(nextBoundary).toLocaleString(); } if (progress) { progress.style.width = ((elapsedInSeason / seasonLength) * 100).toFixed(3) + '%'; } seasons.forEach(function (season, index) { var offset = (index - currentIndex + seasonNames.length) % seasonNames.length; var state = season.querySelector('[data-hw-zodiac-state]'); var startsIn = remaining + (Math.max(0, offset - 1) * seasonLength); var changeAt = offset === 0 ? nextBoundary : now + startsIn; season.classList.toggle('is-current', offset === 0); season.setAttribute('aria-current', offset === 0 ? 'true' : 'false'); season.title = (offset === 0 ? 'Ends ' : 'Begins ') + new Date(changeAt).toLocaleString(); if (state) { if (offset === 0) { state.textContent = 'Current · ends in ' + formatDuration(remaining); } else if (offset === 1) { state.textContent = 'Next · starts in ' + formatDuration(startsIn); } else { state.textContent = 'Starts in ' + formatDuration(startsIn); } } }); } updateZodiacCycle(); window.setInterval(updateZodiacCycle, 1000); }); } mw.hook('wikipage.content').add(function ($content) { initZodiacCycles($content && $content[0] ? $content[0] : document); }); }()); /* HarvestWiki multi-page editor controls and archive refresh */ (function () { 'use strict'; function initBulkEditors(root) { var grids = []; if (root && root.matches && root.matches('.pfSpreadsheet')) { grids.push(root); } if (root && root.querySelectorAll) { grids = grids.concat(Array.prototype.slice.call(root.querySelectorAll('.pfSpreadsheet'))); } grids.forEach(function (grid) { if (grid.dataset.hwBulkEditorReady === '1') { return; } grid.dataset.hwBulkEditorReady = '1'; var parameters = new URLSearchParams(window.location.search); var templateName = parameters.get('template') || grid.id.replace(/Grid$/, ''); var archivePage = templateName === 'Herb' ? 'Herbs' : (templateName === 'Crystal' ? 'Crystals' : null); var userGroups = mw.config.get('wgUserGroups') || []; var canDelete = archivePage && userGroups.indexOf('sysop') !== -1; function purgeArchive() { if (!archivePage || typeof mw.Api !== 'function') { return Promise.resolve(); } return new mw.Api().post({ action: 'purge', titles: archivePage, formatversion: 2 }); } function wireRowActions() { Array.prototype.slice.call(grid.querySelectorAll('a.save-changes')).forEach(function (link) { if (link.dataset.hwBulkSaveReady === '1') { return; } link.dataset.hwBulkSaveReady = '1'; link.setAttribute('title', 'Save this row'); link.setAttribute('aria-label', 'Save this row'); link.addEventListener('click', function () { link.classList.remove('is-saved'); link.classList.add('is-saving'); function refreshArchive() { purgeArchive().then(function () { link.classList.remove('is-saving'); link.classList.add('is-saved'); window.setTimeout(function () { link.classList.remove('is-saved'); }, 2200); }); } window.setTimeout(refreshArchive, 1600); window.setTimeout(refreshArchive, 4200); }); }); if (!canDelete) { return; } Array.prototype.slice.call(grid.querySelectorAll('tbody tr')).forEach(function (row) { var pageCell = row.querySelector('td[data-x="0"]'); var actionBox = row.querySelector('.save-or-cancel'); if (!pageCell || !actionBox || actionBox.querySelector('.hw-delete-row')) { return; } var pageTitle = pageCell.textContent.trim(); if (!pageTitle) { return; } var deleteButton = document.createElement('button'); deleteButton.type = 'button'; deleteButton.className = 'hw-delete-row'; deleteButton.textContent = 'Delete'; deleteButton.title = 'Delete page ' + pageTitle; deleteButton.setAttribute('aria-label', 'Delete page ' + pageTitle); deleteButton.addEventListener('click', function () { var confirmed = window.confirm( 'Permanently delete "' + pageTitle + '"?\n\n' + 'This removes its wiki page and all table data. This cannot be undone.' ); if (!confirmed) { return; } deleteButton.disabled = true; deleteButton.classList.add('is-deleting'); deleteButton.textContent = 'Deleting…'; new mw.Api().postWithToken('csrf', { action: 'delete', title: pageTitle, reason: 'Deleted from the multi-page editor', formatversion: 2 }).then(function () { return purgeArchive(); }).then(function () { deleteButton.textContent = 'Deleted'; window.setTimeout(function () { window.location.reload(); }, 700); }, function () { deleteButton.disabled = false; deleteButton.classList.remove('is-deleting'); deleteButton.textContent = 'Delete'; mw.notify('Could not delete "' + pageTitle + '". The record was not removed.', { type: 'error' }); }); }); actionBox.appendChild(deleteButton); }); } wireRowActions(); if (window.MutationObserver) { new MutationObserver(wireRowActions).observe(grid, { childList: true, subtree: true }); } if (!grid.querySelector('.hw-bulk-editor-note')) { var note = document.createElement('div'); note.className = 'hw-bulk-editor-note'; note.textContent = 'Scroll sideways to reach every field. Use Save at the right of each row.' + (archivePage ? ' Saved rows automatically refresh the ' + archivePage + ' index.' : '') + (canDelete ? ' Administrators can permanently remove a record with Delete.' : ''); grid.appendChild(note); } }); } mw.hook('wikipage.content').add(function ($content) { initBulkEditors($content && $content[0] ? $content[0] : document); }); }());

/* HarvestWiki automatic Herb, Crystal, Mutation, Shovel, Weather, TOD, and Zodiac link colors */
(function () {
  'use strict';

  var rarityColors = {
    common: '#e5e7e7',
    uncommon: '#87ff97',
    rare: '#6d90ff',
    legendary: '#fff687',
    mythic: '#ff79cb',
    secret: '#12005c',
    mortal: '#cef1e8',
    refined: '#464747',
    spirit: '#00c3f2'
  };

  var mutationColors = {
    silver: '#eef1f1',
    golden: '#f4d05b',
    vitality: '#e96869',
    vampiric: '#de002b',
    icy: '#9ee7d8',
    frozen: '#6ee8ff',
    spirit: '#ffabf6',
    royal: '#ffec31',
    imperial: '#b600ff',
    century: '#5369ff',
    millenium: '#6c47ff',
    eon: '#7f15ff',
    sovereign: '#ff26a5',
    supreme: '#ed13ff'
  };

  var worldColors = {
    clear: '#f1cb4f',
    cloudy: '#949ba5',
    rain: '#60a5e2',
    storm: '#525964',
    thunderstorm: '#f8d652',
    'dao tempest': '#b9422f',
    sunrise: '#ef9d49',
    daytime: '#63ace0',
    sunset: '#df6447',
    night: '#abb6f9',
    'year of the dragon': '#ffb86f',
    'year of the tiger': '#f7f672',
    'year of the phoenix': '#f87b84',
    'year of the tortoise': '#4fc57d'
  };

  var shovelColors = {
    'basic shovel': '#ffffff',
    "journeyman's shovel": '#ffffff',
    'root breaker': '#ffffff',
    'steel spade': '#ffffff',
    'autumn leaf': '#ffffff',
    "king's destiny": '#ffffff'
  };

  var entityColors = {
    'ancient woe herb': rarityColors.rare,
    'breathless dawn herb': rarityColors.uncommon,
    'crimson sun grass': rarityColors.common,
    'fleeting dusk herb': rarityColors.uncommon,
    'frigid moon grass': rarityColors.common,
    'pursuing longevity herb': rarityColors.mythic,
    'sparkling joy herb': rarityColors.rare,
    'yang lotus': rarityColors.legendary,
    'yin lotus': rarityColors.legendary,
    'air crystal': rarityColors.mortal,
    'earth crystal': rarityColors.mortal,
    'flame crystal': rarityColors.mortal,
    'water crystal': rarityColors.mortal,
    'wood crystal': rarityColors.mortal,
    'magma crystal': rarityColors.refined,
    'mist crystal': rarityColors.refined,
    'longevity crystal': rarityColors.spirit,
    clear: worldColors.clear,
    cloudy: worldColors.cloudy,
    rain: worldColors.rain,
    storm: worldColors.storm,
    thunderstorm: worldColors.thunderstorm,
    'dao tempest': worldColors['dao tempest'],
    sunrise: worldColors.sunrise,
    daytime: worldColors.daytime,
    sunset: worldColors.sunset,
    night: worldColors.night,
    'year of the dragon': worldColors['year of the dragon'],
    'year of the tiger': worldColors['year of the tiger'],
    'year of the phoenix': worldColors['year of the phoenix'],
    'year of the tortoise': worldColors['year of the tortoise'],
    'basic shovel': shovelColors['basic shovel'],
    "journeyman's shovel": shovelColors["journeyman's shovel"],
    'root breaker': shovelColors['root breaker'],
    'steel spade': shovelColors['steel spade'],
    'autumn leaf': shovelColors['autumn leaf'],
    "king's destiny": shovelColors["king's destiny"]
  };

  function normalizeTitle(value) {
    return (value || '').replace(/_/g, ' ').replace(/\s+/g, ' ').trim().toLowerCase();
  }

  function titleFromLink(link) {
    var href = link.getAttribute('href') || '';
    if (!href || href.charAt(0) === '#') {
      return '';
    }

    try {
      var url = new URL(href, window.location.origin);
      if (url.origin !== window.location.origin) {
        return '';
      }

      var title = url.searchParams.get('title');
      if (!title && url.pathname.indexOf('/index.php/') === 0) {
        title = decodeURIComponent(url.pathname.slice('/index.php/'.length));
      }
      return normalizeTitle((title || '').split('#')[0]);
    } catch (error) {
      return '';
    }
  }

  function cargoQuery(table, fields) {
    return mw.loader.using('mediawiki.api').then(function () {
      return new mw.Api().get({
        action: 'cargoquery',
        tables: table,
        fields: fields,
        limit: 500,
        formatversion: 2
      });
    }).then(function (data) {
      return data && data.cargoquery ? data.cargoquery : [];
    }, function () {
      return [];
    });
  }

  function buildColorMap() {
    return Promise.all([
      cargoQuery('Herbs', '_pageName=Page,Rarity'),
      cargoQuery('Crystals', '_pageName=Page,Rarity'),
      cargoQuery('Mutations', '_pageName=Page,Color'),
      cargoQuery('Weathers', '_pageName=Page,Color'),
      cargoQuery('Shovels', '_pageName=Page,Color')
    ]).then(function (groups) {
      var colorMap = Object.assign({}, entityColors);

      groups[0].forEach(function (row) {
        var data = row.title || {};
        var color = rarityColors[normalizeTitle(data.Rarity)];
        if (data.Page && color) {
          colorMap[normalizeTitle(data.Page)] = color;
        }
      });

      groups[1].forEach(function (row) {
        var data = row.title || {};
        var color = rarityColors[normalizeTitle(data.Rarity)];
        if (data.Page && color) {
          colorMap[normalizeTitle(data.Page)] = color;
        }
      });

      groups[2].forEach(function (row) {
        var data = row.title || {};
        if (data.Page && data.Color) {
          colorMap[normalizeTitle(data.Page)] = data.Color;
        }
      });

      groups[3].forEach(function (row) {
        var data = row.title || {};
        if (data.Page && data.Color) {
          colorMap[normalizeTitle(data.Page)] = data.Color;
        }
      });

      groups[4].forEach(function (row) {
        var data = row.title || {};
        if (data.Page && data.Color) {
          colorMap[normalizeTitle(data.Page)] = data.Color;
        }
      });

      Object.keys(mutationColors).forEach(function (title) {
        colorMap[title] = mutationColors[title];
      });

      return colorMap;
    });
  }

  function applyLinkColors(root, colorMap) {
    var scope = root && root.querySelectorAll ? root : document;
    var links = [];
    if (scope.matches && scope.matches('a[href]')) {
      links.push(scope);
    }
    links = links.concat(Array.prototype.slice.call(scope.querySelectorAll('a[href]')));

    links.forEach(function (link) {
      var color = colorMap[titleFromLink(link)];
      if (!color || link.classList.contains('new')) {
        return;
      }
      link.classList.add('hw-colored-link');
      link.style.setProperty('--hw-link-color', color);
    });
  }

  function applyMutationRows(root, colorMap) {
    var scope = root && root.querySelectorAll ? root : document;
    var tables = [];
    if (scope.matches && scope.matches('table')) {
      tables.push(scope);
    }
    tables = tables.concat(Array.prototype.slice.call(scope.querySelectorAll('table')));

    tables.forEach(function (table) {
      var headers = Array.prototype.slice.call(table.querySelectorAll('thead th'));
      if (!headers.length && table.rows.length) {
        headers = Array.prototype.slice.call(table.rows[0].cells);
      }

      var mutationIndex = headers.findIndex(function (header) {
        return header.textContent.trim().toLowerCase() === 'mutation' ||
          header.classList.contains('field_Mutation');
      });
      if (mutationIndex < 0) {
        return;
      }

      Array.prototype.slice.call(table.tBodies).forEach(function (body) {
        Array.prototype.slice.call(body.rows).forEach(function (row) {
          var mutationCell = row.cells[mutationIndex];
          var mutationLink = mutationCell ? mutationCell.querySelector('a[href]') : null;
          var color = mutationLink ? colorMap[titleFromLink(mutationLink)] : '';
          if (!mutationCell || !color) {
            return;
          }
          row.classList.add('hw-mutation-row');
          row.style.setProperty('--hw-mutation-row-color', color);
          mutationCell.classList.add('hw-mutation-item');
        });
      });
    });
  }

  function applyShovelRows(root, colorMap) {
    var scope = root && root.querySelectorAll ? root : document;
    var tables = [];
    if (scope.matches && scope.matches('table')) {
      tables.push(scope);
    }
    tables = tables.concat(Array.prototype.slice.call(scope.querySelectorAll('table')));

    tables.forEach(function (table) {
      var headers = Array.prototype.slice.call(table.querySelectorAll('thead th'));
      if (!headers.length && table.rows.length) {
        headers = Array.prototype.slice.call(table.rows[0].cells);
      }

      var shovelIndex = headers.findIndex(function (header) {
        return header.textContent.trim().toLowerCase() === 'shovel' ||
          header.classList.contains('field_Shovel');
      });
      if (shovelIndex < 0) {
        return;
      }

      Array.prototype.slice.call(table.tBodies).forEach(function (body) {
        Array.prototype.slice.call(body.rows).forEach(function (row) {
          var shovelCell = row.cells[shovelIndex];
          var shovelLink = shovelCell ? shovelCell.querySelector('a[href]') : null;
          var color = shovelLink ? colorMap[titleFromLink(shovelLink)] : '';
          if (!shovelCell || !color) {
            return;
          }
          row.classList.add('hw-shovel-row');
          row.style.setProperty('--hw-shovel-row-color', color);
          shovelCell.classList.add('hw-shovel-item');
        });
      });
    });
  }

  var immediateColorMap = Object.assign({}, entityColors);
  Object.keys(mutationColors).forEach(function (title) {
    immediateColorMap[title] = mutationColors[title];
  });
  Object.keys(worldColors).forEach(function (title) {
    immediateColorMap[title] = worldColors[title];
  });
  Object.keys(shovelColors).forEach(function (title) {
    immediateColorMap[title] = shovelColors[title];
  });
  var colorMapPromise = buildColorMap();

  mw.hook('wikipage.content').add(function ($content) {
    var root = $content && $content[0] ? $content[0] : document;
    applyMutationRows(root, immediateColorMap);
    applyShovelRows(root, immediateColorMap);
    applyLinkColors(root, immediateColorMap);
    colorMapPromise.then(function (colorMap) {
      applyMutationRows(root, colorMap);
      applyShovelRows(root, colorMap);
      applyLinkColors(root, colorMap);
    });
  });
}());


/* HarvestWiki hierarchical Citizen popout menu */
(function () {
  'use strict';

  var groups = [
    {
      key: 'herb',
      title: 'Herb',
      note: 'Harvesting & cultivation',
      glyph: '草',
      pages: ['Herbs', 'Mutations', 'Crystals', 'Shovels']
    },
    {
      key: 'world',
      title: 'World',
      note: 'Places, people & conditions',
      glyph: '界',
      pages: ['Weather', 'Locations', 'NPCs']
    }
  ];

  function directLink(item) {
    var children = item ? item.children : null;
    var index;
    if (!children) {
      return null;
    }
    for (index = 0; index < children.length; index += 1) {
      if (children[index].tagName === 'A') {
        return children[index];
      }
    }
    return null;
  }

  function linkTitle(link) {
    var path;
    if (!link) {
      return '';
    }
    try {
      path = new URL(link.href, location.href).pathname.split('/').pop() || '';
      return decodeURIComponent(path).replace(/_/g, ' ');
    } catch (error) {
      return (link.textContent || '').trim();
    }
  }

  function closeOtherGroups(root, keep) {
    Array.prototype.slice.call(root.querySelectorAll('.hw-menu-group.is-open')).forEach(function (group) {
      if (group !== keep) {
        group.classList.remove('is-open');
        group.querySelector('.hw-menu-trigger').setAttribute('aria-expanded', 'false');
      }
    });
  }

  function createGroup(config, items, root) {
    var group = document.createElement('li');
    var trigger = document.createElement('button');
    var glyph = document.createElement('span');
    var copy = document.createElement('span');
    var title = document.createElement('span');
    var note = document.createElement('span');
    var chevron = document.createElement('span');
    var submenu = document.createElement('ul');
    var closeTimer = null;

    function cancelClose() {
      if (closeTimer !== null) {
        window.clearTimeout(closeTimer);
        closeTimer = null;
      }
    }

    function closeGroup() {
      group.classList.remove('is-open');
      trigger.setAttribute('aria-expanded', 'false');
    }

    function scheduleClose() {
      cancelClose();
      closeTimer = window.setTimeout(function () {
        closeTimer = null;
        if (!group.matches(':hover') && !group.contains(document.activeElement)) {
          closeGroup();
        }
      }, 240);
    }

    group.className = 'hw-menu-group hw-menu-group--' + config.key;
    trigger.type = 'button';
    trigger.className = 'hw-menu-trigger';
    trigger.setAttribute('aria-expanded', 'false');
    trigger.setAttribute('aria-haspopup', 'true');

    glyph.className = 'hw-menu-glyph';
    glyph.setAttribute('aria-hidden', 'true');
    glyph.textContent = config.glyph;

    copy.className = 'hw-menu-trigger-copy';
    title.className = 'hw-menu-trigger-title';
    title.textContent = config.title;
    note.className = 'hw-menu-trigger-note';
    note.textContent = config.note;
    copy.appendChild(title);
    copy.appendChild(note);

    chevron.className = 'hw-menu-chevron';
    chevron.setAttribute('aria-hidden', 'true');
    chevron.textContent = '›';

    trigger.appendChild(glyph);
    trigger.appendChild(copy);
    trigger.appendChild(chevron);

    submenu.className = 'hw-menu-submenu';
    submenu.setAttribute('aria-label', config.title + ' pages');
    items.forEach(function (item) {
      submenu.appendChild(item);
    });

    trigger.addEventListener('click', function () {
      var willOpen = !group.classList.contains('is-open');
      cancelClose();
      closeOtherGroups(root, group);
      group.classList.toggle('is-open', willOpen);
      trigger.setAttribute('aria-expanded', willOpen ? 'true' : 'false');
    });

    group.addEventListener('mouseenter', function () {
      cancelClose();
      closeOtherGroups(root, group);
      group.classList.add('is-open');
      trigger.setAttribute('aria-expanded', 'true');
    });

    group.addEventListener('mouseleave', scheduleClose);
    submenu.addEventListener('mouseenter', cancelClose);
    submenu.addEventListener('mouseleave', scheduleClose);

    group.addEventListener('focusout', function (event) {
      if (!group.contains(event.relatedTarget)) {
        cancelClose();
        closeGroup();
      }
    });

    group.appendChild(trigger);
    group.appendChild(submenu);
    return group;
  }

  function enhanceMenu() {
    var portlet = document.querySelector('#p-HarvestWiki');
    var list;
    var items;
    var byTitle = {};
    var newList;
    var complete;

    if (!portlet || portlet.classList.contains('hw-menu-ready')) {
      return;
    }

    list = portlet.querySelector('.citizen-menu__content > ul');
    if (!list) {
      return;
    }

    items = Array.prototype.slice.call(list.children);
    items.forEach(function (item) {
      var link = directLink(item);
      var title = linkTitle(link);
      if (title) {
        byTitle[title.toLowerCase()] = item;
      }
    });

    complete = groups.every(function (config) {
      return config.pages.every(function (page) {
        return Boolean(byTitle[page.toLowerCase()]);
      });
    });
    if (!complete) {
      return;
    }

    newList = document.createElement('ul');
    newList.className = 'hw-menu-root';
    groups.forEach(function (config) {
      var groupItems = config.pages.map(function (page) {
        return byTitle[page.toLowerCase()];
      });
      newList.appendChild(createGroup(config, groupItems, newList));
    });

    list.replaceWith(newList);
    portlet.classList.add('hw-menu-ready');
  }

  function initMenu() {
    enhanceMenu();
    new MutationObserver(enhanceMenu).observe(document.body, {
      childList: true,
      subtree: true
    });

    document.addEventListener('keydown', function (event) {
      var root;
      if (event.key !== 'Escape') {
        return;
      }
      root = document.querySelector('.hw-menu-root');
      if (root) {
        closeOtherGroups(root, null);
      }
    });
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', initMenu, { once: true });
  } else {
    initMenu();
  }
}());


/* HarvestWiki automatic page release history */
(function () {
  'use strict';

  function addHistoryToNewPage() {
    var action = mw.config.get('wgAction');
    var articleId = Number(mw.config.get('wgArticleId') || 0);
    var namespace = Number(mw.config.get('wgNamespaceNumber'));
    var pageName = String(mw.config.get('wgPageName') || '');
    var textbox;
    var marker;

    if (action !== 'edit' || articleId !== 0 || namespace !== 0 || /\/Change_History$/.test(pageName)) {
      return;
    }

    textbox = document.getElementById('wpTextbox1');
    if (!textbox || /\{\{\s*Change History\b/i.test(textbox.value)) {
      return;
    }

    marker = '\n\n== Change History ==\n{{Change History|{{' + 'subst:Current version}}|{{' + 'subst:Current version page}}}}\n';
    textbox.value = textbox.value.replace(/\s+$/, '') + marker;
    textbox.dispatchEvent(new Event('input', { bubbles: true }));
  }

  function enableHistoryEditorTools() {
    var userName = mw.config.get('wgUserName');
    var probablyEditable = mw.config.get('wgIsProbablyEditable');
    var releases;

    if (!userName || probablyEditable === false) {
      return;
    }

    releases = document.querySelectorAll('.hw-change-history-release');
    if (!releases.length) {
      return;
    }

    document.body.classList.add('hw-history-tools-enabled');

    releases.forEach(function (release) {
      var addLink = release.querySelector('.hw-history-add a, a.hw-history-add');
      var version = String(release.getAttribute('data-current-version') || '').trim();
      var series = String(release.getAttribute('data-current-series') || '').trim();
      var historyPage = String(release.getAttribute('data-history-page') || '').trim();
      var params;

      if (!addLink || !version || !series || !historyPage) {
        return;
      }

      params = new URLSearchParams();
      params.set('title', historyPage);
      params.set('action', 'edit');
      params.set('section', 'new');
      params.set('preload', 'Template:Change History/entry');
      params.set('preloadtitle', 'Version ' + version);
      params.append('preloadparams[]', version);
      params.append('preloadparams[]', series);
      addLink.href = mw.util.wikiScript('index') + '?' + params.toString();
    });
  }

  function initVersionHistory() {
    addHistoryToNewPage();
    enableHistoryEditorTools();
    window.setTimeout(addHistoryToNewPage, 350);
    window.setTimeout(enableHistoryEditorTools, 350);
  }

  if (mw.hook) {
    mw.hook('wikipage.editform').add(function () {
      window.setTimeout(addHistoryToNewPage, 100);
    });
    mw.hook('wikipage.content').add(function () {
      window.setTimeout(enableHistoryEditorTools, 100);
    });
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', initVersionHistory, { once: true });
  } else {
    initVersionHistory();
  }
}());

/* HarvestWiki version snapshots for source edits, Page Forms, and administrators */
(function () {
  'use strict';

  var MARKER_KEY = 'hwVersionHistoryPending';
  var ADMIN_PAGE = 'HarvestWiki:Version control';
  var AUTO_START = '<!-- HW-AUTO-START:';

  function api() {
    return new mw.Api();
  }

  function isSignedInEditor() {
    return Boolean(mw.config.get('wgUserName')) && mw.config.get('wgIsProbablyEditable') !== false;
  }

  function isVersionAdmin() {
    var groups = mw.config.get('wgUserGroups') || [];
    return groups.indexOf('sysop') !== -1 || groups.indexOf('interface-admin') !== -1 || groups.indexOf('bureaucrat') !== -1;
  }

  function normalTitle(title) {
    return String(title || '').replace(/_/g, ' ').trim();
  }

  function seriesFor(version) {
    var parts = String(version || '').split('.');
    return parts.length === 3 ? parts[0] + '.' + parts[1] : '';
  }

  function validVersion(version) {
    return /^\d+\.\d{2}\.\d+$/.test(String(version || '').trim());
  }

  function getPageText(title) {
    return api().get({
      action: 'query',
      prop: 'revisions',
      titles: title,
      rvprop: 'content',
      rvslots: 'main',
      formatversion: 2
    }).then(function (data) {
      var page = data && data.query && data.query.pages && data.query.pages[0];
      var revision = page && page.revisions && page.revisions[0];
      var slots = revision && revision.slots;
      return {
        exists: Boolean(page && !page.missing),
        text: slots && slots.main && typeof slots.main.content === 'string' ? slots.main.content : ''
      };
    });
  }

  function savePage(title, text, summary, createOnly) {
    var params = {
      action: 'edit',
      title: title,
      text: text,
      summary: summary,
      watchlist: 'nochange',
      formatversion: 2
    };
    if (createOnly) {
      params.createonly = 1;
    }
    return api().postWithToken('csrf', params);
  }

  function readCurrentVersion() {
    return Promise.all([
      getPageText('Template:Current version'),
      getPageText('Template:Current version page')
    ]).then(function (pages) {
      var versionMatch = pages[0].text.match(/\d+\.\d{2}\.\d+/);
      var seriesMatch = pages[1].text.match(/\d+\.\d{2}/);
      var version = versionMatch ? versionMatch[0] : '';
      var series = seriesMatch ? seriesMatch[0] : seriesFor(version);
      if (!validVersion(version) || !series) {
        throw new Error('The current version templates do not contain a valid x.xx.x version.');
      }
      return { version: version, series: series };
    });
  }

  function cleanFieldName(name) {
    var raw = String(name || '');
    var matches = raw.match(/\[([^\]]+)\]/g);
    var last;
    if (matches && matches.length) {
      last = matches[matches.length - 1].replace(/^\[|\]$/g, '');
    } else {
      last = raw;
    }
    last = last.replace(/^pf_free_text$/i, 'Article content')
      .replace(/^free_text$/i, 'Article content')
      .replace(/_/g, ' ')
      .replace(/\s+/g, ' ')
      .trim();
    return last;
  }

  function labelFor(key) {
    return String(key || '')
      .replace(/[_-]+/g, ' ')
      .replace(/\b\w/g, function (letter) { return letter.toUpperCase(); });
  }

  function normalizeValue(value) {
    return String(value == null ? '' : value).replace(/\r\n?/g, '\n').replace(/[ \t]+/g, ' ').trim();
  }

  function collectFormSnapshot(form) {
    var values = {};
    var ignored = /^(wp(EditToken|Starttime|Edittime|AutoSummary|Summary|Section|UltimateParam)|title|action|format|oldid)$/i;
    Array.prototype.forEach.call(form.querySelectorAll('input[name], select[name], textarea[name]'), function (field) {
      var type = String(field.type || '').toLowerCase();
      var name = String(field.name || '');
      var key;
      var value;
      if (!name || field.disabled || ignored.test(name) || /submit|button|reset|image|file/.test(type)) {
        return;
      }
      if ((type === 'checkbox' || type === 'radio') && !field.checked) {
        return;
      }
      if (type === 'hidden' && name.indexOf('[') === -1 && name !== 'pf_free_text') {
        return;
      }
      key = cleanFieldName(name);
      if (!key || /token|timestamp|summary/i.test(key)) {
        return;
      }
      if (field.tagName === 'SELECT' && field.multiple) {
        value = Array.prototype.filter.call(field.options, function (option) { return option.selected; })
          .map(function (option) { return option.value; }).join('; ');
      } else {
        value = field.value;
      }
      value = normalizeValue(value);
      if (Object.prototype.hasOwnProperty.call(values, key) && value && values[key] !== value) {
        values[key] = normalizeValue(values[key] + '; ' + value);
      } else if (!Object.prototype.hasOwnProperty.call(values, key) || value) {
        values[key] = value;
      }
    });
    return values;
  }

  function snapshotFromWikitext(text) {
    var source = String(text || '');
    var snapshot = {};
    var match;
    var paramPattern = /^\s*\|\s*([^=\n]+?)\s*=\s*([\s\S]*?)(?=^\s*\|\s*[^=\n]+?\s*=|^\s*}}|\z)/gm;
    var cleaned = source.replace(/==\s*Change History\s*==[\s\S]*$/i, '').trim();
    while ((match = paramPattern.exec(cleaned))) {
      snapshot[cleanFieldName(match[1])] = normalizeValue(match[2]);
    }
    if (!Object.keys(snapshot).length) {
      snapshot['Article content'] = normalizeValue(cleaned);
    }
    return snapshot;
  }

  function snapshotForForm(form) {
    var textbox = form.querySelector('#wpTextbox1, textarea[name="wpTextbox1"]');
    if (textbox && String(mw.config.get('wgAction')) === 'edit') {
      return snapshotFromWikitext(textbox.value);
    }
    return collectFormSnapshot(form);
  }

  function sameSnapshot(a, b) {
    var aKeys = Object.keys(a || {}).sort();
    var bKeys = Object.keys(b || {}).sort();
    if (aKeys.length !== bKeys.length) {
      return false;
    }
    return aKeys.every(function (key, index) {
      return key === bKeys[index] && normalizeValue(a[key]) === normalizeValue(b[key]);
    });
  }

  function encodeSnapshot(snapshot) {
    return btoa(unescape(encodeURIComponent(JSON.stringify(snapshot || {}))));
  }

  function decodeSnapshot(encoded) {
    try {
      return JSON.parse(decodeURIComponent(escape(atob(encoded))));
    } catch (error) {
      return {};
    }
  }

  function historyBlocks(text) {
    var blocks = [];
    var pattern = /<!-- HW-AUTO-START:([0-9]+\.[0-9]{2}\.[0-9]+) -->([\s\S]*?)<!-- HW-AUTO-END:\1 -->/g;
    var match;
    var snapshotMatch;
    while ((match = pattern.exec(String(text || '')))) {
      snapshotMatch = match[2].match(/<!-- HW-SNAPSHOT:([A-Za-z0-9+/=]+) -->/);
      blocks.push({
        version: match[1],
        full: match[0],
        index: match.index,
        created: /\*\s*Page created\./i.test(match[2]),
        snapshot: snapshotMatch ? decodeSnapshot(snapshotMatch[1]) : {}
      });
    }
    return blocks;
  }

  function shortValue(value) {
    var clean = normalizeValue(value);
    if (clean.length > 120) {
      clean = clean.slice(0, 117) + '...';
    }
    return clean;
  }

  function changeMessages(before, after) {
    var keys = {};
    var messages = [];
    Object.keys(before || {}).forEach(function (key) { keys[key] = true; });
    Object.keys(after || {}).forEach(function (key) { keys[key] = true; });
    Object.keys(keys).sort().forEach(function (key) {
      var oldValue = normalizeValue((before || {})[key]);
      var newValue = normalizeValue((after || {})[key]);
      var label = labelFor(key);
      if (oldValue === newValue) {
        return;
      }
      if (!oldValue && newValue) {
        messages.push('Added ' + label + ': “' + shortValue(newValue) + '”.');
      } else if (oldValue && !newValue) {
        messages.push('Removed ' + label + ' (previously “' + shortValue(oldValue) + '”).');
      } else if (key === 'Article content') {
        messages.push('Updated the article content.');
      } else {
        messages.push('Changed ' + label + ' from “' + shortValue(oldValue) + '” to “' + shortValue(newValue) + '”.');
      }
    });
    if (messages.length > 12) {
      messages = messages.slice(0, 12).concat(['Updated ' + (messages.length - 12) + ' additional fields.']);
    }
    return messages;
  }

  function buildBlock(version, series, messages, snapshot) {
    return AUTO_START + version + ' -->\n' +
      '== Version ' + version + ' ==\n' +
      "'''{{Version link|" + version + '|' + series + "}}'''\n" +
      messages.map(function (message) { return '* ' + message; }).join('\n') + '\n' +
      '<!-- HW-SNAPSHOT:' + encodeSnapshot(snapshot) + ' -->\n' +
      '<!-- HW-AUTO-END:' + version + ' -->';
  }

  function upsertHistory(existingText, versionInfo, afterSnapshot, isNewPage, forceCreated) {
    var text = String(existingText || '').trim();
    var blocks = historyBlocks(text);
    var current = blocks.filter(function (block) { return block.version === versionInfo.version; })[0];
    var previous = blocks.filter(function (block) { return block.version !== versionInfo.version && Object.keys(block.snapshot).length; })[0];
    var beforeSnapshot = previous ? previous.snapshot : {};
    var messages;
    var replacement;

    if (current && current.created) {
      messages = ['Page created.'];
    } else if ((isNewPage || forceCreated) && !blocks.length) {
      messages = ['Page created.'];
    } else {
      messages = changeMessages(beforeSnapshot, afterSnapshot);
      if (!messages.length) {
        return { changed: false, text: text };
      }
    }

    replacement = buildBlock(versionInfo.version, versionInfo.series, messages, afterSnapshot);
    if (current) {
      return { changed: replacement !== current.full, text: text.replace(current.full, replacement) };
    }
    return { changed: true, text: replacement + (text ? '\n\n' + text : '') };
  }

  function historyMarkup(versionInfo) {
    return '\n\n== Change History ==\n{{Change History|' + versionInfo.version + '|' + versionInfo.series + '}}\n';
  }

  function ensureHistorySection(title, versionInfo) {
    return getPageText(title).then(function (page) {
      if (!page.exists || /{{\s*Change History\b/i.test(page.text)) {
        return false;
      }
      return savePage(title, page.text.replace(/\s+$/, '') + historyMarkup(versionInfo), 'Add version change history section').then(function () {
        return true;
      });
    });
  }

  function recordAutomaticHistory(title, versionInfo, afterSnapshot, isNewPage, forceCreated) {
    var historyTitle = title + '/Change History';
    return getPageText(historyTitle).then(function (historyPage) {
      var updated = upsertHistory(historyPage.text, versionInfo, afterSnapshot, isNewPage, forceCreated);
      if (!updated.changed) {
        return false;
      }
      return savePage(historyTitle, updated.text + '\n', 'Record automatic change history for ' + versionInfo.version).then(function () {
        return true;
      });
    });
  }

  function setPendingEnsure(title, versionInfo) {
    try {
      sessionStorage.setItem(MARKER_KEY, JSON.stringify({
        title: title,
        version: versionInfo.version,
        series: versionInfo.series,
        time: Date.now()
      }));
    } catch (error) {}
  }

  function completePendingEnsure() {
    var raw;
    var pending;
    try {
      raw = sessionStorage.getItem(MARKER_KEY);
      pending = raw ? JSON.parse(raw) : null;
    } catch (error) {
      pending = null;
    }
    if (!pending || !pending.title || Date.now() - Number(pending.time || 0) > 300000) {
      return;
    }
    if (normalTitle(mw.config.get('wgPageName')) !== normalTitle(pending.title) || Number(mw.config.get('wgArticleId') || 0) === 0) {
      return;
    }
    sessionStorage.removeItem(MARKER_KEY);
    ensureHistorySection(pending.title, { version: pending.version, series: pending.series }).then(function (changed) {
      if (changed) {
        mw.notify('The page was saved and its change-history section was added.', { type: 'success' });
        window.setTimeout(function () { window.location.reload(); }, 700);
      }
    }).catch(function (error) {
      mw.notify('The page saved, but its change-history section could not be added: ' + error.message, { type: 'error' });
    });
  }

  function initTrackedForm() {
    var namespace = Number(mw.config.get('wgNamespaceNumber'));
    var specialFormMatch = String(window.location.pathname || '').match(/\/Special:FormEdit\/[^/]+\/(.+)$/i);
    var form;
    var initialSnapshot;
    var isNewPage;
    var pageTitle;

    if (!mw.config.get('wgUserName') || (!specialFormMatch && namespace !== 0) || /\/Change_History$/.test(String(mw.config.get('wgPageName') || ''))) {
      return;
    }

    form = document.querySelector('form#pfForm, form#sfForm, form#editform');
    if (!form || form.dataset.hwVersionTrackingReady === '1') {
      return;
    }

    form.dataset.hwVersionTrackingReady = '1';
    initialSnapshot = snapshotForForm(form);
    isNewPage = Boolean(specialFormMatch) || Number(mw.config.get('wgArticleId') || 0) === 0;
    pageTitle = specialFormMatch ? normalTitle(decodeURIComponent(specialFormMatch[1])) : normalTitle(mw.config.get('wgPageName'));

    form.addEventListener('submit', function (event) {
      var submitter = event.submitter;
      var outgoing;
      var submitName = submitter ? String(submitter.name || submitter.value || '') : '';

      if (form.dataset.hwVersionSubmitting === '1' || /preview|diff|changes|cancel/i.test(submitName)) {
        return;
      }

      outgoing = snapshotForForm(form);
      if (!isNewPage && sameSnapshot(initialSnapshot, outgoing)) {
        return;
      }

      event.preventDefault();
      form.dataset.hwVersionSubmitting = '1';
      mw.notify('Saving the page and recording its version history…');

      readCurrentVersion().then(function (versionInfo) {
        return recordAutomaticHistory(pageTitle, versionInfo, outgoing, isNewPage, false).then(function () {
          setPendingEnsure(pageTitle, versionInfo);
        });
      }).catch(function (error) {
        mw.notify('The page will still be saved, but automatic history failed: ' + error.message, { type: 'warn' });
      }).then(function () {
        var saveField = form.querySelector('input[type="hidden"][name="wpSave"]');
        if (!saveField) {
          saveField = document.createElement('input');
          saveField.type = 'hidden';
          saveField.name = 'wpSave';
          saveField.value = 'Save page';
          form.appendChild(saveField);
        }
        HTMLFormElement.prototype.submit.call(form);
      });
    }, true);
  }

  function allMainPages() {
    var results = [];
    function next(apcontinue) {
      var params = { action: 'query', list: 'allpages', apnamespace: 0, aplimit: 'max', formatversion: 2 };
      if (apcontinue) {
        params.apcontinue = apcontinue;
      }
      return api().get(params).then(function (data) {
        results = results.concat((data.query && data.query.allpages) || []);
        return data.continue && data.continue.apcontinue ? next(data.continue.apcontinue) : results;
      });
    }
    return next();
  }

  function backfillOne(title, versionInfo, forceCreated) {
    if (title === 'Change History' || /\/Change History$/.test(title)) {
      return Promise.resolve({ title: title, skipped: true });
    }
    return getPageText(title).then(function (page) {
      if (!page.exists) {
        return { title: title, skipped: true };
      }
      var snapshot = snapshotFromWikitext(page.text);
      return Promise.all([
        ensureHistorySection(title, versionInfo),
        recordAutomaticHistory(title, versionInfo, snapshot, false, forceCreated)
      ]).then(function () {
        return { title: title, skipped: false };
      });
    });
  }

  function runBackfill(versionInfo, forceCreated, progress) {
    return allMainPages().then(function (pages) {
      var queue = pages.map(function (page) { return normalTitle(page.title); });
      var done = 0;
      var failures = [];
      var workers = [];

      function worker() {
        var title = queue.shift();
        if (!title) {
          return Promise.resolve();
        }
        return backfillOne(title, versionInfo, forceCreated).catch(function (error) {
          failures.push(title + ': ' + error.message);
        }).then(function () {
          done += 1;
          progress(done, pages.length, title, failures.length);
          return worker();
        });
      }

      for (var i = 0; i < 3; i += 1) {
        workers.push(worker());
      }
      return Promise.all(workers).then(function () {
        return { total: pages.length, failures: failures };
      });
    });
  }

  function currentTemplateText(version) {
    return '<includeonly>' + version + '</includeonly><noinclude>Sitewide current patch version. Change this through [[HarvestWiki:Version control]]. Version numbers use x.xx.x. [[Category:Version templates]]</noinclude>';
  }

  function currentSeriesText(series) {
    return '<includeonly>' + series + '</includeonly><noinclude>Parent release-series number for the current patch. Change this through [[HarvestWiki:Version control]]. [[Category:Version templates]]</noinclude>';
  }

  function releasePageText(versionInfo, title, date) {
    return '{{Version\n|version=' + versionInfo.series + '\n|title=' + title + '\n|release date=' + date + '\n|current patch=' + versionInfo.version + '\n}}\n\n== Overview ==\nDescribe the goals and scope of this release series.\n\n== Changelogs ==\n=== ' + versionInfo.version + ' ===\n' + title + ' — ' + date + '\n* Add release changes here.\n\n[[Category:Versions]]\n';
  }

  function updateReleasePage(versionInfo, title, date) {
    var pageTitle = 'Version ' + versionInfo.series;
    return getPageText(pageTitle).then(function (page) {
      var text;
      if (!page.exists) {
        return savePage(pageTitle, releasePageText(versionInfo, title, date), 'Create release page for ' + versionInfo.version);
      }
      text = page.text;
      if (/\|\s*current patch\s*=/.test(text)) {
        text = text.replace(/(\|\s*current patch\s*=\s*)[^\n|}]*/i, '$1' + versionInfo.version);
      }
      if (text.indexOf('=== ' + versionInfo.version + ' ===') === -1) {
        text = text.replace(/\s*\[\[Category:Versions\]\]\s*$/i, '') +
          '\n\n=== ' + versionInfo.version + ' ===\n' + title + ' — ' + date + '\n* Add release changes here.\n\n[[Category:Versions]]\n';
      }
      return savePage(pageTitle, text, 'Set current patch to ' + versionInfo.version);
    });
  }

  function applyVersion(version, title, date) {
    var versionInfo = { version: version, series: seriesFor(version) };
    return Promise.all([
      savePage('Template:Current version', currentTemplateText(version), 'Set current version to ' + version),
      savePage('Template:Current version page', currentSeriesText(versionInfo.series), 'Set current version series to ' + versionInfo.series),
      updateReleasePage(versionInfo, title, date)
    ]).then(function () {
      return versionInfo;
    });
  }

  function initVersionAdmin() {
    var root;
    var status;
    var versionInput;
    var titleInput;
    var dateInput;
    var applyButton;
    var backfillButton;
    var snapshotButton;

    if (normalTitle(mw.config.get('wgPageName')) !== ADMIN_PAGE) {
      return;
    }

    root = document.getElementById('hw-version-control-root');
    if (!root) {
      return;
    }

    if (!isVersionAdmin()) {
      root.innerHTML = '<div class="hw-note">Administrator access is required.</div>';
      return;
    }

    document.body.classList.add('hw-version-admin-authorized');
    root.innerHTML = '<div class="hw-version-admin-card">' +
      '<h2>Current release</h2>' +
      '<div class="hw-version-admin-grid">' +
      '<label>Version number <input id="hw-version-number" type="text" placeholder="0.01.0" pattern="\d+\.\d{2}\.\d+"></label>' +
      '<label>Release title <input id="hw-version-title" type="text" placeholder="Early Development"></label>' +
      '<label>Release date <input id="hw-version-date" type="text" placeholder="29 July 2026"></label>' +
      '</div>' +
      '<div class="hw-version-admin-actions"><button id="hw-version-apply" type="button">Apply version and create release page</button></div>' +
      '</div>' +
      '<div class="hw-version-admin-card"><h2>Page history tools</h2>' +
      '<p><button id="hw-version-backfill" type="button">Backfill untracked pages as created now</button></p>' +
      '<p><button id="hw-version-snapshot" type="button">Snapshot every page in the current version</button></p>' +
      '<p class="hw-version-admin-help">Backfill is for legacy pages. Snapshot creates a current-version baseline only where the page differs from its last stored snapshot.</p>' +
      '</div>' +
      '<div id="hw-version-status" class="hw-version-status" role="status" aria-live="polite"></div>';

    status = document.getElementById('hw-version-status');
    versionInput = document.getElementById('hw-version-number');
    titleInput = document.getElementById('hw-version-title');
    dateInput = document.getElementById('hw-version-date');
    applyButton = document.getElementById('hw-version-apply');
    backfillButton = document.getElementById('hw-version-backfill');
    snapshotButton = document.getElementById('hw-version-snapshot');

    readCurrentVersion().then(function (info) {
      versionInput.value = info.version;
    });

    function setBusy(busy) {
      applyButton.disabled = busy;
      backfillButton.disabled = busy;
      snapshotButton.disabled = busy;
    }

    function progress(done, total, title, failures) {
      status.textContent = 'Processed ' + done + ' of ' + total + ': ' + title + (failures ? ' (' + failures + ' failed)' : '');
    }

    applyButton.addEventListener('click', function () {
      var version = versionInput.value.trim();
      var title = titleInput.value.trim();
      var date = dateInput.value.trim();
      if (!validVersion(version) || !title || !date) {
        status.textContent = 'Enter a version in x.xx.x format, a title, and a release date.';
        return;
      }
      setBusy(true);
      status.textContent = 'Applying ' + version + '…';
      applyVersion(version, title, date).then(function () {
        status.textContent = 'Current version is now ' + version + '. The release page is ready.';
      }).catch(function (error) {
        status.textContent = 'Version update failed: ' + error.message;
      }).then(function () { setBusy(false); });
    });

    backfillButton.addEventListener('click', function () {
      setBusy(true);
      status.textContent = 'Reading current version…';
      readCurrentVersion().then(function (info) {
        return runBackfill(info, true, progress);
      }).then(function (result) {
        status.textContent = 'Backfill complete: ' + result.total + ' pages checked, ' + result.failures.length + ' failed.';
      }).catch(function (error) {
        status.textContent = 'Backfill failed: ' + error.message;
      }).then(function () { setBusy(false); });
    });

    snapshotButton.addEventListener('click', function () {
      setBusy(true);
      status.textContent = 'Reading current version…';
      readCurrentVersion().then(function (info) {
        return runBackfill(info, false, progress);
      }).then(function (result) {
        status.textContent = 'Snapshot complete: ' + result.total + ' pages checked, ' + result.failures.length + ' failed.';
      }).catch(function (error) {
        status.textContent = 'Snapshot failed: ' + error.message;
      }).then(function () { setBusy(false); });
    });
  }

  function initAutomaticVersioning() {
    completePendingEnsure();
    initTrackedForm();
    initVersionAdmin();
    window.setTimeout(initTrackedForm, 500);
  }

  function startAutomaticVersioning() {
    if (mw.hook) {
      mw.hook('wikipage.editform').add(function () { window.setTimeout(initTrackedForm, 100); });
    }

    if (document.readyState === 'loading') {
      document.addEventListener('DOMContentLoaded', initAutomaticVersioning, { once: true });
    } else {
      initAutomaticVersioning();
    }
  }

  mw.loader.using(['mediawiki.api', 'mediawiki.util'], startAutomaticVersioning, function (error) {
    if (window.console && console.error) {
      console.error('HarvestWiki version tracking could not load:', error);
    }
  });
}());