ATLYSS TechPendium

Module:Equipment

This Module contains Wiki logic and must be loaded using require keyword inside other logic modules.
Note that require supports only static imports.
This module can be {{#invoke}}-ed to render dynamic parts of the page.
const Utils = await require("Utils");
const Wiki = await require("Wiki");
const Game = await require("Game");
const Cache = await require("Cache");
const { wikiTooltip: wikiTradeItemTooltip } = await require("Trade_Items");
const { latest: latestVersion } = await requireData("Versions");
const { TooltipBuilder } = await require("Tooltip_Builder");
const { InfoboxBuilder } = await require("Infobox_Builder");

async function buildEquipmentTooltipHtml(itemData, itemId, version = latestVersion) {
    const tooltip = new TooltipBuilder(`Equipment:Item:${itemId}`);

    const name = itemData.name || itemId;
    const description = itemData.description || "";

    const sanitizedDescription = description
        .replace(/\n/g, " ")
        .replace(/<color=(.*?)>(.*?)<\/color>/g,
            "<span style='color:$1;'>$2</span>");

    tooltip.addLine(
        `<div style="display:flex;justify-content:space-between;">` +
        `<div>'''${name}'''</div>` +
        `<div>[[File:${itemData.type.charAt(0).toUpperCase() + itemData.type.slice(1)}_${itemData.id}_icon.png|26px|inline|alt=${name}]]</div>` +
        `</div>`
    );

    if (sanitizedDescription) {
        tooltip.addLine(`<div>${sanitizedDescription.replace(/\n/g, "<br>")}</div>`);
    }

    tooltip.addLine(`<div>Rarity: ${Wiki.wikiRarity(itemData.rarity)}</div>`);
    tooltip.addLine(`<div>Level Requirement: ${itemData.requirements.level || 0}</div>`);

    if (itemData.requirements.class) {
        const { data: classData } = await Utils.resolveData("Classes", false);

        const classInfo = Utils.findByProperty(classData, "guid", itemData.requirements.class);

        tooltip.addLine(`<div>Class Requirement: ${classInfo ? classInfo.name : "Unknown"}</div>`);
    }

    if (itemData.type === "weapon") {
        tooltip.addLine(`<div>Weapon Type: ${itemData.slot.charAt(0).toUpperCase() + itemData.slot.slice(1)}</div>`);
        tooltip.addLine(`<div>Element: ${itemData.element || "None"}</div>`);
        tooltip.addLine(`<div>Damage: ${itemData.damage.min}-${itemData.damage.max}</div>`);
    }

    const { data: statsData } = await Utils.resolveData("Stats", false);

    for (const statId in itemData.stats) {
        const statValue = itemData.stats[statId];
        const statInfo = statsData[statId];

        if (["stat.experience", "stat.crit_rate", "stat.magic_crit_rate", "stat.evasion"].includes(statId)) {
            tooltip.addLine(`<div>${statInfo ? statInfo.name : statId}: ${(statValue * 100).toFixed(2)}%</div>`);
        } else {
            tooltip.addLine(`<div>${statInfo ? statInfo.name : statId}: ${statValue}</div>`);
        }
    }

    return tooltip.build();
}

async function wikiTable(props) {
    const args = Utils.resolveArgs(props);
    const selectedVersion = args["version"] || args[0];
    const filterType = args["type"] || args[1];
    const filterSlot = args["slot"] || args[2];

    const { data, version } = await Utils.resolveData("Equipment", false, selectedVersion);

    if (Utils.isModuleEmpty(data)) {
        return `<div id="Equipment-wikiTable">⚠️ Equipment data is unavailable for version ${version}.${Wiki.versionSelector(version, "Equipment", "wikiTable", [`type=${filterType}`, `slot=${filterSlot}`])}</div>`;
    }

    const sortedData = Utils.sortObject(data, (a, b) => {
        const levelA = a.requirements.level || 0;
        const levelB = b.requirements.level || 0;

        if (levelA !== levelB) {
            return levelA - levelB;
        }

        return a.name.localeCompare(b.name);
    });
    const filteredData = Utils.filterObjectByProperties(sortedData, { type: filterType, slot: filterSlot });

    let table = "";

    table += `{| class="wiki-table" style="width:auto;"\n`;
    table += `! colspan="${filterType == "armor" ? 4 : filterType == "weapon" ? 5 : 3}" | :Version: ${Wiki.versionSelector(version, "Equipment", "wikiTable", [`type=${filterType}`, `slot=${filterSlot}`])}\n`;
    table += `|-\n`;
    table += `! Name !! Level !! Rarity ${filterType == "armor" ? "!! Class" : filterType == "weapon" ? "!! Element !! Damage" : ""}\n`;

    for (const id in filteredData) {
        const item = filteredData[id];
        const itemId = item.id;

        table += `|-\n`;

        const tooltipId = `Equipment:Item:${itemId}`;

        table += `| <span data-tooltip-id="${tooltipId}">${availablePages.has(`File:${item.type.charAt(0).toUpperCase() + item.type.slice(1)}_${item.id}_icon.png`) ? `[[File:${item.type.charAt(0).toUpperCase() + item.type.slice(1)}_${item.id}_icon.png|16px|inline|alt=${item.name}|link=${item.type.charAt(0).toUpperCase() + item.type.slice(1)}s/${item.name}]]` : ""}[[${item.type.charAt(0).toUpperCase() + item.type.slice(1)}s/${item.name}|${item.name}]]</span>\n`;
        table += `| ${item.requirements.level || 0}\n`;
        table += `| ${Wiki.wikiRarity(item.rarity)}\n`;

        if (item.type === "weapon") {
            table += `| ${item.element || "None"}\n`;
            table += `| ${item.damage.min}-${item.damage.max}\n`;
        } else if (item.type === "armor") {
            const { data: classData } = await Utils.resolveData("Classes", false);
            const classInfo = Utils.findByProperty(classData, "guid", item.requirements.class);
            table += `| ${classInfo ? classInfo.name : "Any"}\n`;
        }

        if (!Cache.has("EquipmentTooltips", tooltipId)) {
            Cache.set("EquipmentTooltips", tooltipId, await buildEquipmentTooltipHtml(item, itemId, version));
            frame.__footer = frame.__footer + String(Cache.get("EquipmentTooltips", tooltipId));
        }
    }

    table += `|}\n`;

    return `<div id="Equipment-wikiTable">${table}</div>`;
}

async function wikiNavbox() {
    const { data, version } = await Utils.resolveData("Equipment", false);

    if (Utils.isModuleEmpty(data)) {
        return `<div id="Equipment-wikiNavbox">⚠️ Equipment data is unavailable for version ${version}.${Wiki.versionSelector(version, "Equipment", "wikiNavbox")}</div>`;
    }

    const sortedData = Utils.sortObjectByKey(data);

    let navbox = "";

    const versionTooltip = new TooltipBuilder(`Equipment:Version:${version.replace(/\./g, '_')}`);
    versionTooltip.addLine(
        `Data version: '''${version}''' <span class="${version === latestVersion ? 'latest' : 'outdated'}">(${version === latestVersion ? 'latest' : 'outdated'})</span>`
    );

    const weapons = Utils.filterObjectByProperty(sortedData, "type", "weapon");
    const armors = Utils.filterObjectByProperty(sortedData, "type", "armor");

    const weaponsBySlot = {};
    for (const id in weapons) {
        const weapon = weapons[id];

        if (!weaponsBySlot[weapon.slot]) weaponsBySlot[weapon.slot] = [];
        weaponsBySlot[weapon.slot].push(weapon);
    }

    const armorsBySlot = {};
    for (const id in armors) {
        const armor = armors[id];
        if (!armorsBySlot[armor.slot]) armorsBySlot[armor.slot] = [];
        armorsBySlot[armor.slot].push(armor);
    }

    navbox += `{| class="wiki-table navbox"\n`;
    navbox += `! colspan="3" | :Version: ${Wiki.versionCode(version, versionTooltip)}\n`;
    navbox += `|-\n`;
    navbox += `! colspan="3" | Equipment Navigation\n`;
    navbox += `|-\n`;

    navbox += `! rowspan="${Object.keys(weaponsBySlot).length + 1}" | Weapons\n`;

    for (const slot in weaponsBySlot) {
        const slotWeapons = weaponsBySlot[slot];
        navbox += `|-\n`;
        navbox += `! ${slot.charAt(0).toUpperCase() + slot.slice(1)}\n`;

        const weaponLinks = [];

        for (const w of slotWeapons) {
            const itemId = w.id;
            const tooltipId = `Equipment:Item:${itemId}`;

            weaponLinks.push(`<span data-tooltip-id="${tooltipId}">${availablePages.has(`File:Weapon_${w.id}_icon.png`) ? `[[File:Weapon_${w.id}_icon.png|16px|inline|alt=${w.name}|link=Weapons/${w.name}]]` : ""}[[Weapons/${w.name}|${w.name}]]</span>`);

            if (!Cache.has("EquipmentTooltips", tooltipId)) {
                Cache.set("EquipmentTooltips", tooltipId, await buildEquipmentTooltipHtml(w, itemId, version));
                frame.__footer = frame.__footer + String(Cache.get("EquipmentTooltips", tooltipId));
            }
        }

        navbox += `| ${weaponLinks.join(" • ")}\n`;
    }

    navbox += `|-\n`;
    navbox += `! rowspan="${Object.keys(armorsBySlot).length + 1}" | Armors\n`;

    for (const slot in armorsBySlot) {
        const slotArmors = armorsBySlot[slot];
        navbox += `|-\n`;
        navbox += `! ${slot.charAt(0).toUpperCase() + slot.slice(1)}\n`;

        const armorLinks = [];

        for (const a of slotArmors) {
            const itemId = a.id;
            const tooltipId = `Equipment:Item:${itemId}`;

            armorLinks.push(`<span data-tooltip-id="${tooltipId}">${availablePages.has(`File:Armor_${a.id}_icon.png`) ? `[[File:Armor_${a.id}_icon.png|16px|inline|alt=${a.name}|link=Armors/${a.name}]]` : ""}[[Armors/${a.name}|${a.name}]]</span>`);

            if (!Cache.has("EquipmentTooltips", tooltipId)) {
                Cache.set("EquipmentTooltips", tooltipId, await buildEquipmentTooltipHtml(a, itemId, version));
                frame.__footer = frame.__footer + String(Cache.get("EquipmentTooltips", tooltipId));
            }
        }

        navbox += `| ${armorLinks.join(" • ")}\n`;
    }

    navbox += `|}`;

    return `<div id="Equipment-wikiNavbox">${navbox}</div>`;
}

async function wikiTooltip(props) {
    const args = Utils.resolveArgs(props);
    const itemArg = args["item"] || args[0];

    if (!itemArg) {
        return "⚠️ No equipment specified.";
    }

    const itemId = itemArg.split("/").filter(Boolean).findLast(p => !/^[a-z]{2}$/.test(p)).toLowerCase().replace(/\s+|'/g, "_");

    const tooltipId = `Equipment:Item:${itemId}`;

    const { data } = await Utils.resolveData("Equipment", false);

    if (Utils.isModuleEmpty(data)) {
        return "⚠️ Equipment data is unavailable.";
    }

    const itemData = data[itemId];

    if (!itemData) {
        return `⚠️ Equipment item '${itemId}' not found.`;
    }

    const isWeapon = itemData.type === "weapon";

    if (Cache.has("EquipmentTooltips", tooltipId)) {
        return `<span data-tooltip-id="${tooltipId}">[[${isWeapon ? "Weapons" : "Armors"}/${itemData.name}|${itemData.name}]]</span>`;
    }

    const tooltipHtml = await buildEquipmentTooltipHtml(itemData, itemId);

    if (!Cache.has("EquipmentTooltips", tooltipId)) {
        Cache.set("EquipmentTooltips", tooltipId, tooltipHtml);
        frame.__footer = frame.__footer + String(Cache.get("EquipmentTooltips", tooltipId));
    }

    return `<span data-tooltip-id="${tooltipId}">[[${isWeapon ? "Weapons" : "Armors"}/${itemData.name}|${itemData.name}]]</span>`;
}

async function wikiInfobox(props) {
    const args = Utils.resolveArgs(props);
    const itemArg = args["item"] || args[0];
    const selectedVersion = args["version"] || args[1];

    const { data, version } = await Utils.resolveData("Equipment", false, selectedVersion);

    if (Utils.isModuleEmpty(data)) {
        return `⚠️ Equipment data is unavailable for version ${selectedVersion}.${Wiki.versionSelector(version, "Equipment", "wikiInfobox", [itemArg])}`;
    }

    const itemId = itemArg.split("/").filter(Boolean).findLast(p => !/^[a-z]{2}$/.test(p)).toLowerCase().replace(/\s+|'/g, "_");
    const itemData = data[itemId];

    if (!itemData) {
        return `⚠️ Equipment item '${itemId}' not found in data version ${version}.`;
    }

    const infobox = new InfoboxBuilder();

    infobox.setTitle((itemData.name || itemId) + Wiki.versionSelector(version, "Equipment", "wikiInfobox", [itemArg]));

    const humanize = (value) =>
        value?.toString().replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());

    const subtitleParts = ["Equipment"];

    if (itemData.type) subtitleParts.push(humanize(itemData.type));
    if (itemData.slot) subtitleParts.push(humanize(itemData.slot));

    const subtitle = subtitleParts.join(" · ");

    infobox.setSubtitle(subtitle);
    infobox.setRarity(itemData.rarity);
    infobox.setImage(`[[File:${itemData.id || itemId}_icon.png|32px|${itemData.name || itemId}]]`);

    if (itemData.description) {
        infobox.setDescription(itemData.description
            .replace(/\n{2,}/g, "<br>")
            .replace(/<color=(.*?)>(.*?)<\/color>/g, "<span style='color:$1;'>$2</span>"));
    }

    const general = infobox.startSection("General");

    if (itemData.type) {
        general.addRow("Category", humanize(itemData.type));
        if (itemData.type === "weapon") general.addRow("Type", humanize(itemData.slot || "Unknown"));
        else general.addRow("Slot", humanize(itemData.slot || "Unknown"));
    }

    if (itemData.requirements) {
        if (itemData.requirements.level) general.addRow("Level Requirement", itemData.requirements.level);
        if (itemData.requirements.class) {
            const { data: classData } = await Utils.resolveData("Classes", false, version);
            const classInfo = Utils.findByProperty(classData, "guid", itemData.requirements.class);
            general.addRow("Class Requirement", classInfo?.name || "Unknown");
        }
    }

    if (itemData.damage) general.addRow("Damage", `${itemData.damage.min}-${itemData.damage.max}`);
    if (itemData.element) general.addRow("Element", humanize(itemData.element));

    if (itemData.buyPrice) {
        const priceSection = infobox.startSection("Price");
        priceSection.addRow("Buy Price", `${itemData.buyPrice} Crowns`);
        priceSection.addRow("Sell Price", `${Game.getSellPrice(itemData.buyPrice)} Crowns`);
    }

    if (itemData.modifiers) {
        const enchantingSection = infobox.startSection("Enchanting");
        enchantingSection.addRow("Currency Price", `${Game.getEnchantPrice(itemData.buyPrice)} Crowns`);

        if (itemData.modifiers.cost) {
            const enchantItemData = await Utils.resolveItemByGuid(itemData.modifiers.cost.item, { version });

            if (enchantItemData) {
                enchantingSection.addRow("Item Price", `${itemData.modifiers.cost.quantity}x ${await wikiTradeItemTooltip({ item: enchantItemData.id })}`);
            }
        }
    } else infobox.startSection("Enchanting Not Available");

    if (itemData.stats) {
        const { data: statsData } = await Utils.resolveData("Stats", false, version);
        const statsSection = infobox.startSection("Stats");
        const statEntries = Object.keys(itemData.stats).sort((a, b) => a.localeCompare(b)).forEach(statId => {
            const statValue = itemData.stats[statId];
            const statInfo = statsData[statId];
            const displayValue = ["stat.experience", "stat.crit_rate", "stat.magic_crit_rate", "stat.evasion"].includes(statId)
                ? `${(statValue * 100).toFixed(2)}%`
                : statValue;
            statsSection.addRow(statInfo ? statInfo.name : statId, displayValue);
        });
    }

    const itemRarityData = Game.getRarityLabel(itemData.rarity);
    const title = itemData.name || itemId;
    const damage = itemData.damage ? `${itemData.damage.min}-${itemData.damage.max}` : "N/A";
    const element = itemData.element ? humanize(itemData.element) : "None";
    const buyPrice = itemData.buyPrice ? `${itemData.buyPrice} Crowns` : "N/A";
    const sellPrice = itemData.buyPrice ? `${Game.getSellPrice(itemData.buyPrice)} Crowns` : "N/A";
    const cleanFooter = (itemData.description || "").replace(/\n+/g, " ").replace(/<.*?>/g, "").trim().slice(0, 160) || title;

    const ogImageBlock = `{{#og_image:
     | layout = card
     | title = ${title}
     | subtitle = ${subtitle}
     | image = File:${itemId}_icon.png
     | primary = ${itemRarityData.name}
     | secondary = ${itemData.type == "weapon" ? `Damage: ${damage}, Element: ${element},` : ""} Buy Price: ${buyPrice}, Sell Price: ${sellPrice}
     | footer = ${cleanFooter}
     | accent = ${itemRarityData.color}
     | branding = ATLYSS TechPendium
    }}`;

    return `{{#og:|type=summary_large_image|title=${title}}}${ogImageBlock}<div id="Equipment-wikiInfobox" style="float: ${infobox.float || "right"}">${infobox.build()}</div>`;
}

async function wikiDropSources(props) {
    const args = Utils.resolveArgs(props);
    const itemArg = args["item"] || args[0];
    const selectedVersion = args["version"] || args[1];

    const { data, version } = await Utils.resolveData("Equipment", false, selectedVersion);

    if (Utils.isModuleEmpty(data)) {
        return `<div id="Equipment-wikiDropSources">⚠️ Equipment data is unavailable for version ${selectedVersion}.${Wiki.versionSelector(version, "Equipment", "wikiDropSources", [itemArg])}</div>`;
    }

    const itemId = itemArg.split("/").filter(Boolean).findLast(p => !/^[a-z]{2}$/.test(p)).toLowerCase().replace(/\s+|'/g, "_");
    const itemData = data[itemId];

    if (!itemData) {
        return `<div id="Equipment-wikiDropSources">⚠️ Equipment item '${itemId}' not found in data version ${version}.${Wiki.versionSelector(version, "Equipment", "wikiDropSources", [itemArg])}</div>`;
    }

    const dropSources = [];

    function resolveLootTableName(rawName) {
        if (!rawName) return { name: rawName, difficulty: null };

        let name = rawName.replace(/^lootTable_/i, "");

        let difficulty = null;
        const diffMatch = name.match(/\b(easy|normal|hard)\b/i);
        if (diffMatch) {
            difficulty = diffMatch[1].toUpperCase();
            name = name.replace(diffMatch[0], "");
        }

        let tier = "";
        const tierMatch = name.match(/tier(\d+)/i);
        if (tierMatch) {
            tier = `Tier ${tierMatch[1]}`;
            name = name.replace(/tier\d+/i, "");
        }

        name = name.replace(/lv(\d+)/i, "Lv $1");
        name = name.replace(/([a-z])([A-Z])/g, "$1 $2");
        name = name.replace(/_/g, " ");
        name = name.replace(/\s+/g, " ").trim();

        const corrections = {
            "Catcombs": "Catacombs"
        };

        for (const wrong in corrections) {
            const regex = new RegExp(`\\b${wrong}\\b`, "gi");
            name = name.replace(regex, corrections[wrong]);
        }

        name = name.replace(/\b\w/g, c => c.toUpperCase());
        if (tier) name += ` (${tier})`;
        name = name.replace(/\(\s*\)/g, "").trim();

        return { name, difficulty };
    }

    function renderDifficultyBadge(difficulty) {
        if (!difficulty) return "";

        let color = "#888";
        if (difficulty === "HARD") color = "#c0392b";
        if (difficulty === "NORMAL") color = "#888";
        if (difficulty === "EASY") color = "#27ae60";

        return ` <span style="color:${color}; font-weight:bold;">${difficulty}</span>`;
    }

    const { data: creeps } = await Utils.resolveData("Creeps", false, version);

    if (!Utils.isModuleEmpty(creeps)) {
        for (const creepId in creeps) {
            const creep = creeps[creepId];
            const drop = await Game.calculateCreepDrops(creep, itemData.guid);
            if (!drop) continue;

            dropSources.push({
                category: "Enemy",
                name: creep.name,
                level: creep.level ?? "?",
                isElite: creep.isElite ?? false,
                rawChance: +(drop.dropChance * 100).toFixed(2),
                effectiveChance: +(drop.effectiveDrop * 100).toFixed(4),
                quantity: drop.quantityMin === drop.quantityMax
                    ? drop.quantityMin
                    : `${drop.quantityMin}-${drop.quantityMax}`
            });
        }
    }

    const { data: lootTables } = await Utils.resolveData("Loot_Tables", false, version);

    if (!Utils.isModuleEmpty(lootTables)) {
        for (const tableId in lootTables) {
            const table = lootTables[tableId];
            if (/cost/i.test(table.name) && /tier\d+/i.test(table.name)) continue;
            const drop = await Game.calculateLootTableDrops(table, itemData.guid);
            if (!drop) continue;

            dropSources.push({
                category: "Loot Table",
                name: table.name,
                rawChance: +(drop.dropChance * 100).toFixed(2),
                effectiveChance: +(drop.effectiveDrop * 100).toFixed(4),
                quantity: drop.quantityMin === drop.quantityMax
                    ? drop.quantityMin
                    : `${drop.quantityMin}-${drop.quantityMax}`
            });
        }
    }

    if (!dropSources.length) {
        return `<div id="Equipment-wikiDropSources">No known drops in ${version}. ${Wiki.versionSelector(version, "Equipment", "wikiDropSources", [itemArg])}</div>`;
    }

    const grouped = {};
    for (const entry of dropSources) {
        if (!grouped[entry.category]) grouped[entry.category] = [];
        grouped[entry.category].push(entry);
    }

    for (const key in grouped) {
        grouped[key].sort((a, b) => b.effectiveChance - a.effectiveChance);
    }

    let wikitext = `{| class="wiki-table" style="width:auto;"\n`;
    wikitext += `! colspan="6" | :Version: ${Wiki.versionSelector(version, "Equipment", "wikiDropSources", [itemArg])}\n`;

    for (const category in grouped) {
        wikitext += "|-\n";
        wikitext += `! colspan="6" | ${category} Drops\n`;

        if (category === "Enemy") {
            wikitext += "|-\n";
            wikitext += "! Source !! Level !! Elite !! Raw Chance !! Effective Chance !! Quantity\n";

            for (const source of grouped[category]) {
                wikitext += "|-\n";
                wikitext += `| [[Creeps/${source.name}|${source.name}]]`;
                wikitext += ` || ${source.level}`;
                wikitext += ` || ${source.isElite ? "Yes" : "No"}`;
                wikitext += ` || ${source.rawChance.toFixed(2)}%`;
                wikitext += ` || ${source.effectiveChance.toFixed(4)}%`;
                wikitext += ` || ${source.quantity}\n`;
            }
        } else {
            wikitext += "|-\n";
            wikitext += "! Source !! Raw Chance !! Effective Chance !! Quantity\n";

            for (const source of grouped[category]) {
                const resolved = resolveLootTableName(source.name);
                const badge = renderDifficultyBadge(resolved.difficulty);

                wikitext += "|-\n";
                wikitext += `| ${resolved.name}${badge}`;
                wikitext += ` || ${source.rawChance.toFixed(2)}%`;
                wikitext += ` || ${source.effectiveChance.toFixed(4)}%`;
                wikitext += ` || ${source.quantity}\n`;
            }
        }
    }

    wikitext += "|}";

    return `<div id="Equipment-wikiDropSources">${wikitext}</div>`;
}

exports = {
    wikiTable,
    wikiNavbox,
    wikiTooltip,
    wikiInfobox,
    wikiDropSources
}
Last Edited by LiveGobe on 6/16/2026, 5:12:32 PM

This page categories: