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
No edit summary
No edit summary
Line 9: Line 9:


mw.hook('wikipage.content').add(function () {
mw.hook('wikipage.content').add(function () {
  /* ===========================
    HELPERS
    =========================== */


   function parseTime(t) {
   function parseTime(t) {
Line 15: Line 19:
     return m * 60 + s;
     return m * 60 + s;
   }
   }
  function getPoints(rank) {
    const scoring = {
      1:25,2:20,3:17,4:15,5:13,
      6:11,7:10,8:9,9:8,10:7,
      11:6,12:5,13:4,14:3,15:2
    };
    return scoring[rank] || 1;
  }
  /* ===========================
    MAIN TABLE
    =========================== */


   const table = document.getElementById("leaderboardTable");
   const table = document.getElementById("leaderboardTable");
Line 24: Line 42:
     =========================== */
     =========================== */


   const wrapper = document.createElement("div");
   const filterWrapper = document.createElement("div");
   wrapper.style.marginBottom = "20px";
   filterWrapper.style.marginBottom = "20px";


   const label = document.createElement("h3");
   const filterTitle = document.createElement("h3");
   label.innerText = "🔍 Filter Runs";
   filterTitle.innerText = "🔍 Filter Runs";
   wrapper.appendChild(label);
   filterWrapper.appendChild(filterTitle);


   const dungeonSelect = document.createElement("select");
   const dungeonSelect = document.createElement("select");
   dungeonSelect.style.marginRight = "20px";
   dungeonSelect.style.marginRight = "15px";


   [
   [
Line 45: Line 63:
     "Hall of Daggers"
     "Hall of Daggers"
   ].forEach(name => {
   ].forEach(name => {
     const option = document.createElement("option");
     const opt = document.createElement("option");
     option.value = name;
     opt.value = name;
     option.textContent = name;
     opt.textContent = name;
     dungeonSelect.appendChild(option);
     dungeonSelect.appendChild(opt);
   });
   });


   wrapper.appendChild(document.createTextNode("Dungeon: "));
   filterWrapper.appendChild(document.createTextNode("Dungeon: "));
   wrapper.appendChild(dungeonSelect);
   filterWrapper.appendChild(dungeonSelect);


   const playerInput = document.createElement("input");
   const playerInput = document.createElement("input");
   playerInput.placeholder = "Search player...";
   playerInput.placeholder = "Search player...";
   playerInput.style.marginLeft = "15px";
   playerInput.style.marginLeft = "10px";
   wrapper.appendChild(playerInput);
   filterWrapper.appendChild(playerInput);




Line 65: Line 83:


   function sortAndRank() {
   function sortAndRank() {
     const rows = Array.from(table.querySelectorAll("tr")).slice(1);
     const rows = Array.from(table.querySelectorAll("tr")).slice(1);


     rows.sort((a,b) => {
     rows.sort((a,b) => {
      if (a.cells.length < 5 || b.cells.length < 5) return 0;
       const dA = a.cells[1].innerText.trim();
       const dA = a.cells[1].innerText.trim();
       const dB = b.cells[1].innerText.trim();
       const dB = b.cells[1].innerText.trim();
       if (dA !== dB) return dA.localeCompare(dB);
       if (dA !== dB) return dA.localeCompare(dB);
       return parseTime(a.cells[4].innerText) - parseTime(b.cells[4].innerText);
       return parseTime(a.cells[4].innerText) - parseTime(b.cells[4].innerText);
     });
     });


     rows.forEach(row => table.appendChild(row));
     rows.forEach(r => table.appendChild(r));


     let currentDungeon = "";
     let currentDungeon = "";
Line 82: Line 106:


     rows.forEach(row => {
     rows.forEach(row => {
      if (row.cells.length < 5) return;
       const dungeon = row.cells[1].innerText.trim();
       const dungeon = row.cells[1].innerText.trim();
       const time = parseTime(row.cells[4].innerText);
       const time = parseTime(row.cells[4].innerText);
Line 97: Line 124:


       row.cells[0].innerText = rank;
       row.cells[0].innerText = rank;
     });
     });
   }
   }
Line 102: Line 130:


   /* ===========================
   /* ===========================
     BUILD WR + COMPETITION
     BUILD TOP SECTION
     =========================== */
     =========================== */


Line 112: Line 140:
     const records = {};
     const records = {};


     function getPoints(rank) {
     rows.forEach(row => {
      const scoring = {
        1:25,2:20,3:17,4:15,5:13,
        6:11,7:10,8:9,9:8,10:7,
        11:6,12:5,13:4,14:3,15:2
      };
      return scoring[rank] || 1;
    }


    rows.forEach(row => {
      if (row.cells.length < 5) return;


       const dungeon = row.cells[1].innerText.trim();
       const dungeon = row.cells[1]?.innerText?.trim();
       const player = row.cells[2].innerText.trim();
       const player = row.cells[2]?.innerText?.trim();
       const time = row.cells[4].innerText.trim();
       const time = row.cells[4]?.innerText?.trim();
       const rank = parseInt(row.cells[0].innerText);
       const rank = parseInt(row.cells[0]?.innerText);


       if (!player || isNaN(rank)) return;
       if (!dungeon || !player || !time || isNaN(rank)) return;


       // points
       /* POINTS */
       playerPoints[player] = (playerPoints[player] || 0) + getPoints(rank);
       playerPoints[player] = (playerPoints[player] || 0) + getPoints(rank);


       // WR
       /* WORLD RECORDS */
       if (!records[dungeon]) {
       if (!records[dungeon]) {
         records[dungeon] = { time, players: [player] };
         records[dungeon] = { time, players: [player] };
Line 143: Line 164:




     /* ---------- container ---------- */
     /* REMOVE OLD */
 
     const existing = document.getElementById("topSection");
     const existing = document.getElementById("topSection");
     if (existing) existing.remove();
     if (existing) existing.remove();


    /* FLEX CONTAINER */
     const top = document.createElement("div");
     const top = document.createElement("div");
     top.id = "topSection";
     top.id = "topSection";
     top.style.display = "flex";
     top.style.display = "flex";
     top.style.gap = "30px";
     top.style.gap = "30px";
    top.style.alignItems = "flex-start";
     top.style.marginBottom = "25px";
     top.style.marginBottom = "25px";




     /* ---------- COMPETITION ---------- */
     /* ===========================
      COMPETITION
      =========================== */


     const compBox = document.createElement("div");
     const compBox = document.createElement("div");
Line 182: Line 204:
       .sort((a,b)=>b.points-a.points);
       .sort((a,b)=>b.points-a.points);


     let prev = null, rank = 0, pos = 0;
     let prevPoints = null;
    let rank = 0;
    let position = 0;


     leaderboard.forEach(e=>{
     leaderboard.forEach(entry => {
      pos++;
      if (e.points !== prev) rank = pos;
      prev = e.points;


       const r = document.createElement("tr");
       position++;
      if (entry.points !== prevPoints) rank = position;
      prevPoints = entry.points;


       let medal = rank===1?"🥇 1":rank===2?"🥈 2":rank===3?"🥉 3":rank;
       const medal =
        rank===1?"🥇 1":
        rank===2?"🥈 2":
        rank===3?"🥉 3":
        rank;


       r.innerHTML = `
       const row = document.createElement("tr");
      row.innerHTML = `
         <td>${medal}</td>
         <td>${medal}</td>
         <td>${e.player}</td>
         <td>${entry.player}</td>
         <td>${e.points}</td>
         <td>${entry.points}</td>
       `;
       `;
       compTable.appendChild(r);
       compTable.appendChild(row);
     });
     });


Line 204: Line 232:




     /* ---------- WORLD RECORDS ---------- */
     /* ===========================
      WORLD RECORDS
      =========================== */


     const wrBox = document.createElement("div");
     const wrBox = document.createElement("div");
Line 225: Line 255:
     `;
     `;


     Object.entries(records).forEach(([d, data])=>{
     Object.entries(records).forEach(([d, data]) => {
       const r = document.createElement("tr");
       const row = document.createElement("tr");
       r.innerHTML = `
       row.innerHTML = `
         <td>${d}</td>
         <td>${d}</td>
         <td>${data.time}</td>
         <td>${data.time}</td>
         <td>${data.players.join(", ")}</td>
         <td>${data.players.join(", ")}</td>
       `;
       `;
       wrTable.appendChild(r);
       wrTable.appendChild(row);
     });
     });


Line 238: Line 268:




     /* ---------- combine ---------- */
     /* COMBINE */
 
     top.appendChild(compBox);
     top.appendChild(compBox);
     top.appendChild(wrBox);
     top.appendChild(wrBox);
Line 245: Line 274:
     table.parentNode.insertBefore(top, table);
     table.parentNode.insertBefore(top, table);


     // filters under both
     /* FILTER BELOW */
     table.parentNode.insertBefore(wrapper, table);
     table.parentNode.insertBefore(filterWrapper, table);
   }
   }




   /* ===========================
   /* ===========================
     FILTER
     FILTER LOGIC
     =========================== */
     =========================== */


   function filterTable() {
   function filterTable() {
    const d = dungeonSelect.value;
    const p = playerInput.value.toLowerCase();


     Array.from(table.querySelectorAll("tr")).slice(1).forEach(row=>{
     const dungeon = dungeonSelect.value;
       const dungeon = row.cells[1].innerText.trim();
    const player = playerInput.value.toLowerCase();
       const player = row.cells[2].innerText.toLowerCase();
 
    const rows = Array.from(table.querySelectorAll("tr")).slice(1);
 
    rows.forEach(row => {
 
      if (row.cells.length < 5) return;
 
       const d = row.cells[1].innerText.trim();
       const p = row.cells[2].innerText.toLowerCase();


       row.style.display =
       row.style.display =
         (d==="All"||dungeon===d) && player.includes(p) ? "" : "none";
         (dungeon === "All" || d === dungeon) &&
        p.includes(player)
          ? ""
          : "none";
     });
     });
   }
   }

Revision as of 15:00, 18 March 2026

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

mw.hook('wikipage.content').add(function ($content) {
  $content.find('.spoiler').on('click', function () {
    $(this).toggleClass('revealed');
  });
});


mw.hook('wikipage.content').add(function () {

  /* ===========================
     HELPERS
     =========================== */

  function parseTime(t) {
    if (!t || !t.includes(":")) return 999999;
    const [m, s] = t.trim().split(":").map(Number);
    return m * 60 + s;
  }

  function getPoints(rank) {
    const scoring = {
      1:25,2:20,3:17,4:15,5:13,
      6:11,7:10,8:9,9:8,10:7,
      11:6,12:5,13:4,14:3,15:2
    };
    return scoring[rank] || 1;
  }


  /* ===========================
     MAIN TABLE
     =========================== */

  const table = document.getElementById("leaderboardTable");
  if (!table) return;


  /* ===========================
     FILTER UI
     =========================== */

  const filterWrapper = document.createElement("div");
  filterWrapper.style.marginBottom = "20px";

  const filterTitle = document.createElement("h3");
  filterTitle.innerText = "🔍 Filter Runs";
  filterWrapper.appendChild(filterTitle);

  const dungeonSelect = document.createElement("select");
  dungeonSelect.style.marginRight = "15px";

  [
    "All",
    "Wolf's Den","Wolf's Den II","Wolf's Den III",
    "The Red Nest","For The Eggs",
    "Scarlet Revenge","Scarlet Revenge II",
    "Promised One","Promised One II","Promised One III",
    "Lost and Found","The Hostage","Cactus On Fire",
    "Deep Echoes","Deep Echoes II","Deep Echoes III",
    "King's Hoard","King's Hoard II",
    "Hall of Daggers"
  ].forEach(name => {
    const opt = document.createElement("option");
    opt.value = name;
    opt.textContent = name;
    dungeonSelect.appendChild(opt);
  });

  filterWrapper.appendChild(document.createTextNode("Dungeon: "));
  filterWrapper.appendChild(dungeonSelect);

  const playerInput = document.createElement("input");
  playerInput.placeholder = "Search player...";
  playerInput.style.marginLeft = "10px";
  filterWrapper.appendChild(playerInput);


  /* ===========================
     SORT + RANK
     =========================== */

  function sortAndRank() {

    const rows = Array.from(table.querySelectorAll("tr")).slice(1);

    rows.sort((a,b) => {

      if (a.cells.length < 5 || b.cells.length < 5) return 0;

      const dA = a.cells[1].innerText.trim();
      const dB = b.cells[1].innerText.trim();

      if (dA !== dB) return dA.localeCompare(dB);

      return parseTime(a.cells[4].innerText) - parseTime(b.cells[4].innerText);
    });

    rows.forEach(r => table.appendChild(r));

    let currentDungeon = "";
    let position = 0;
    let rank = 0;
    let prevTime = null;

    rows.forEach(row => {

      if (row.cells.length < 5) return;

      const dungeon = row.cells[1].innerText.trim();
      const time = parseTime(row.cells[4].innerText);

      if (dungeon !== currentDungeon) {
        currentDungeon = dungeon;
        position = 1;
        rank = 1;
        prevTime = time;
      } else {
        position++;
        if (time !== prevTime) rank = position;
        prevTime = time;
      }

      row.cells[0].innerText = rank;

    });
  }


  /* ===========================
     BUILD TOP SECTION
     =========================== */

  function buildTopSection() {

    const rows = Array.from(table.querySelectorAll("tr")).slice(1);

    const playerPoints = {};
    const records = {};

    rows.forEach(row => {

      if (row.cells.length < 5) return;

      const dungeon = row.cells[1]?.innerText?.trim();
      const player = row.cells[2]?.innerText?.trim();
      const time = row.cells[4]?.innerText?.trim();
      const rank = parseInt(row.cells[0]?.innerText);

      if (!dungeon || !player || !time || isNaN(rank)) return;

      /* POINTS */
      playerPoints[player] = (playerPoints[player] || 0) + getPoints(rank);

      /* WORLD RECORDS */
      if (!records[dungeon]) {
        records[dungeon] = { time, players: [player] };
      } else if (time === records[dungeon].time) {
        records[dungeon].players.push(player);
      }

    });


    /* REMOVE OLD */
    const existing = document.getElementById("topSection");
    if (existing) existing.remove();


    /* FLEX CONTAINER */
    const top = document.createElement("div");
    top.id = "topSection";
    top.style.display = "flex";
    top.style.gap = "30px";
    top.style.marginBottom = "25px";


    /* ===========================
       COMPETITION
       =========================== */

    const compBox = document.createElement("div");
    compBox.style.flex = "1";

    const compTitle = document.createElement("h2");
    compTitle.innerText = "🏆 Competition Leaderboard";
    compBox.appendChild(compTitle);

    const compTable = document.createElement("table");
    compTable.className = "wikitable";
    compTable.style.width = "100%";

    compTable.innerHTML = `
      <tr>
        <th>Rank</th>
        <th>Player</th>
        <th>Points</th>
      </tr>
    `;

    const leaderboard = Object.entries(playerPoints)
      .map(([player, points]) => ({player, points}))
      .sort((a,b)=>b.points-a.points);

    let prevPoints = null;
    let rank = 0;
    let position = 0;

    leaderboard.forEach(entry => {

      position++;
      if (entry.points !== prevPoints) rank = position;
      prevPoints = entry.points;

      const medal =
        rank===1?"🥇 1":
        rank===2?"🥈 2":
        rank===3?"🥉 3":
        rank;

      const row = document.createElement("tr");
      row.innerHTML = `
        <td>${medal}</td>
        <td>${entry.player}</td>
        <td>${entry.points}</td>
      `;
      compTable.appendChild(row);
    });

    compBox.appendChild(compTable);


    /* ===========================
       WORLD RECORDS
       =========================== */

    const wrBox = document.createElement("div");
    wrBox.style.flex = "1";

    const wrTitle = document.createElement("h2");
    wrTitle.innerText = "🏆 Dungeon World Records";
    wrBox.appendChild(wrTitle);

    const wrTable = document.createElement("table");
    wrTable.className = "wikitable";
    wrTable.style.width = "100%";

    wrTable.innerHTML = `
      <tr>
        <th>Dungeon</th>
        <th>Time</th>
        <th>Player(s)</th>
      </tr>
    `;

    Object.entries(records).forEach(([d, data]) => {
      const row = document.createElement("tr");
      row.innerHTML = `
        <td>${d}</td>
        <td>${data.time}</td>
        <td>${data.players.join(", ")}</td>
      `;
      wrTable.appendChild(row);
    });

    wrBox.appendChild(wrTable);


    /* COMBINE */
    top.appendChild(compBox);
    top.appendChild(wrBox);

    table.parentNode.insertBefore(top, table);

    /* FILTER BELOW */
    table.parentNode.insertBefore(filterWrapper, table);
  }


  /* ===========================
     FILTER LOGIC
     =========================== */

  function filterTable() {

    const dungeon = dungeonSelect.value;
    const player = playerInput.value.toLowerCase();

    const rows = Array.from(table.querySelectorAll("tr")).slice(1);

    rows.forEach(row => {

      if (row.cells.length < 5) return;

      const d = row.cells[1].innerText.trim();
      const p = row.cells[2].innerText.toLowerCase();

      row.style.display =
        (dungeon === "All" || d === dungeon) &&
        p.includes(player)
          ? ""
          : "none";
    });
  }

  dungeonSelect.addEventListener("change", filterTable);
  playerInput.addEventListener("input", filterTable);


  /* ===========================
     INIT
     =========================== */

  sortAndRank();
  buildTopSection();

});