Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.
Revision as of 14:01, 27 July 2026 by Aurelianus (talk | contribs) (Power the live four-zodiac homepage cycle)

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/* Any JavaScript here will be loaded for all users on every page load. */

/* 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);
  });
}());