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
}