Viewing old revision of Module:Trade_Items
You are viewing an old revision of this page from 3/29/2026, 4:40:09 PM.
View latest versionconst Utils = await require("Utils");
const Wiki = await require("Wiki");
const Game = await require("Game");
const { latest: latestVersion } = await requireData("Versions");
const { TooltipBuilder } = await require("Tooltip_Builder");
const { InfoboxBuilder } = await require("Infobox_Builder");
function buildTradeItemTooltipHtml(itemData, itemId) {
const tooltip = new TooltipBuilder(`TradeItem: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.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>Stack Size: ${itemData.stack?.max ?? "N/A"}</div>`);
if (itemData.value?.vendor) {
tooltip.addLine(`<div>Buy Price: ${itemData.value.vendor} Crowns</div>`);
tooltip.addLine(`<div>Sell Price: ${Game.getSellPrice(itemData.value.vendor)} Crowns</div>`);
}
return tooltip.build();
}
async function renderTable(items, version, notes = {}) {
let wikitext = "";
let tooltips = "";
const notesAvailable = !!Object.keys(notes).length;
wikitext += '{| class="wiki-table" style="width: auto;"\n';
wikitext += `! colspan=${notesAvailable ? "4" : "3"} | :Version: ${Wiki.versionSelector(version, "Trade_Items", "wikiTable")}\n`;
wikitext += '|-\n';
wikitext += `! Name !! Rarity !! Max Stack${notesAvailable ? " !! Notes" : ""}\n`;
for (const id in items) {
const item = items[id];
const itemId = item.id || id;
wikitext += '|-\n';
const tooltipId = `TradeItem:Item:${itemId}`;
// Link only inside table
wikitext += `| <span data-tooltip-id="${tooltipId}">${availablePages.has(`File:${item.id}_icon.png`) ? `[[File:${item.id}_icon.png|16px|inline|alt=${item.name}|link=Trade Items/${item.name}]]` : ""}[[Trade Items/${item.name}|${item.name}]]</span>\n`;
// Collect tooltip block separately
tooltips += buildTradeItemTooltipHtml(item, itemId);
// Rarity
wikitext += `| ${item.rarity ? Wiki.wikiRarity(item.rarity) : "N/A"}\n`;
// Max Stack
wikitext += `| ${item.stack?.max ?? "N/A"}\n`;
if (notesAvailable) {
wikitext += `| ${notes[item.name] ?? ""}\n`;
}
}
wikitext += '|}\n';
wikitext += tooltips;
return wikitext;
}
async function renderNavbox(items, version) {
let wikitext = '';
let tooltips = '';
const versionTooltip = new TooltipBuilder(`TradeItem:Navbox:Version:${version.replace(/\./g, '_')}`);
versionTooltip.addLine(
`Data version: '''${version}''' <span class="${version === latestVersion ? 'latest' : 'outdated'}">(${version === latestVersion ? 'latest' : 'outdated'})</span>`
);
wikitext += `{| class="wiki-table navbox"\n`;
wikitext += `! :Version: ${Wiki.versionCode(version, versionTooltip)}\n`;
wikitext += `|-\n`;
wikitext += `! Trade Items Navigation\n`;
wikitext += `|-\n| `;
const itemLinks = [];
for (const id in items) {
const item = items[id];
const itemId = item.id || id;
const tooltipId = `TradeItem:Item:${itemId}`;
// ✅ Inline span only
itemLinks.push(
`<span data-tooltip-id="${tooltipId}">${availablePages.has(`File:${item.id}_icon.png`) ? `[[File:${item.id}_icon.png|16px|inline|alt=${item.name}|link=Trade Items/${item.name}]]` : ""}[[Trade Items/${item.name}|${item.name}]]</span>`
);
// ✅ Collect tooltip block separately
tooltips += buildTradeItemTooltipHtml(item, itemId);
}
wikitext += itemLinks.join(' • ') + '\n';
wikitext += `|}\n`;
// ✅ Append tooltip blocks AFTER navbox
wikitext += tooltips;
return wikitext;
}
async function wikiTable(props) {
const args = Utils.resolveArgs(props);
const selectedVersion = args["version"] || args[0];
const notes = args["notes"];
const { data, version } = await Utils.resolveData("Trade_Items", false, selectedVersion);
if (Utils.isModuleEmpty(data)) {
return `<div id="Trade_Items-wikiTable">⚠️ Trade item data is unavailable for version ${version}.${Wiki.versionSelector(version, "Trade_Items", "wikiTable")}</div>`;
}
const parsedNotes = notes?.length
? notes.split(",").reduce((acc, pair) => {
const [key, ...rest] = pair.split(":");
if (!key) return acc;
acc[key.trim()] = rest.join(":").trim();
return acc;
}, {})
: notes;
const sortedData = Utils.sortObjectByKey(data);
const table = await renderTable(sortedData, version, parsedNotes || {});
return `<div id="Trade_Items-wikiTable">${table}</div>`;
};
async function wikiNavbox() {
const { data, version } = await Utils.resolveData("Trade_Items", false);
if (Utils.isModuleEmpty(data)) {
return '⚠️ Trade items data is unavailable for all known versions.';
}
const sortedData = Utils.sortObjectByKey(data);
let output = '';
output += await renderNavbox(sortedData, version);
return output;
};
async function wikiTooltip(props) {
const args = Utils.resolveArgs(props);
const itemArg = args["item"] || args[0];
if (!itemArg) return "";
frame.tradeItemsCache ??= {};
frame.tradeItemsCache.tooltip ??= {};
const itemId = itemArg
.toLowerCase()
.replace(/\s+|\'/g, '_')
.split("/")
.pop();
const cacheKey = `TradeItem:${itemId}`;
if (frame.tradeItemsCache.tooltip[cacheKey]) {
return frame.tradeItemsCache.tooltip[cacheKey];
}
const { data } = await Utils.resolveData("Trade_Items", false);
if (Utils.isModuleEmpty(data)) {
return "⚠️ Trade items data are unavailable.";
}
const itemData = data[itemId];
if (!itemData) {
return `⚠️ Trade item '${itemArg}' not found.`;
}
const tooltipHtml = buildTradeItemTooltipHtml(itemData, itemId);
const linkHtml =
`<span data-tooltip-id="TradeItem:Item:${itemId}">` +
`[[Trade Items/${itemData.name}|${itemData.name}]]` +
`</span>`;
const result = linkHtml + tooltipHtml;
frame.tradeItemsCache.tooltip[cacheKey] = result;
return result;
}
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("Trade_Items", false, selectedVersion);
if (Utils.isModuleEmpty(data)) {
return `⚠️ Trade items data is unavailable for version ${selectedVersion}.${Wiki.versionSelector(version, "Trade_Items", "wikiInfobox", [itemArg])}`;
}
const itemId = itemArg?.toLowerCase()?.replace(/\s+|\'/g, '_').split("/").pop();
const itemData = data[itemId];
if (!itemData) {
return `⚠️ Trade item '${itemArg}' not found in data version ${version}.`;
}
const infobox = new InfoboxBuilder();
infobox.setTitle((itemData.name || itemArg) + Wiki.versionSelector(version, "Trade_Items", "wikiInfobox", [itemArg]));
infobox.setSubtitle(["Trade Item", ...itemData.tags].join(" "));
infobox.setRarity(itemData.rarity);
infobox.setImage(`[[File:${itemData.id}_icon.png|32px|${itemData.name || itemArg}]]`);
if (itemData.description) {
infobox.setDescription(itemData.description.replace(/\n{2,}/g, "<br>").replace(/<color=(.*?)>(.*?)<\/color>/g, "<span style='color:$1;'>$2</span>"));
}
if (itemData.stack?.max) {
infobox.startSection("General").addRow("Max Stack Size", itemData.stack.max);
}
if (itemData.value?.vendor) {
infobox.startSection("Price").addRow("Buy Price", itemData.value.vendor).addRow("Sell Price", Game.getSellPrice(itemData.value.vendor));
}
const itemRarityData = Game.getRarityLabel(itemData.rarity);
const ogImageBlock = `{{#og_image:
| layout = card
| title = ${itemData.name}
| subtitle = Trade Item
| image = File:${itemId}_icon.png
| primary = ${itemRarityData.name}
| secondary = Max Stack: ${itemData.stack?.max || "N/A"}, Buy Price: ${itemData.value.vendor}, Sell Price: ${Game.getSellPrice(itemData.value.vendor)}
| footer = ${itemData.description.replace(/\n+/g, " ").replace(/<.*?>/g, "").trim().slice(0, 160)}
| accent = ${itemRarityData.color}
| branding = ATLYSS TechPendium
}}`;
return `{{#og:|title=${itemData.name}}}${ogImageBlock}<div id="Trade_Items-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: items, version } = await Utils.resolveData("Trade_Items", false, selectedVersion);
if (Utils.isModuleEmpty(items)) {
return `<div id="Trade_Items-wikiDropSources">⚠️ Trade item data unavailable for ${version}. ${Wiki.versionSelector(version, "Trade_Items", "wikiDropSources", [itemArg])}</div>`;
}
const itemId = itemArg?.toLowerCase()?.replace(/\s+|\'/g, "_").split("/").pop();
const itemData = items[itemId];
if (!itemData) {
return `<div id="Trade_Items-wikiDropSources">⚠️ Trade item '${itemArg}' not found in ${version}. ${Wiki.versionSelector(version, "Trade_Items", "wikiDropSources", [itemArg])}</div>`;
}
const dropSources = [];
function resolveLootTableName(rawName) {
if (!rawName) return { name: rawName, difficulty: null };
let name = rawName.replace(/^lootTable_/i, "");
// Extract difficulty
let difficulty = null;
const diffMatch = name.match(/\b(easy|normal|hard)\b/i);
if (diffMatch) {
difficulty = diffMatch[1].toUpperCase();
name = name.replace(diffMatch[0], "");
}
// Extract tier
let tier = "";
const tierMatch = name.match(/tier(\d+)/i);
if (tierMatch) {
tier = `Tier ${tierMatch[1]}`;
name = name.replace(/tier\d+/i, "");
}
// Fix level formatting
name = name.replace(/lv(\d+)/i, "Lv $1");
// CamelCase → spaced
name = name.replace(/([a-z])([A-Z])/g, "$1 $2");
// Underscores → spaces
name = name.replace(/_/g, " ");
// Normalize spacing early
name = name.replace(/\s+/g, " ").trim();
// Fix known spelling mistakes (data normalization layer)
const corrections = {
"Catcombs": "Catacombs"
};
for (const wrong in corrections) {
const regex = new RegExp(`\\b${wrong}\\b`, "gi");
name = name.replace(regex, corrections[wrong]);
}
// Title Case
name = name.replace(/\b\w/g, c => c.toUpperCase());
// Append tier only if valid
if (tier) name += ` (${tier})`;
// FINAL SAFETY: remove empty parentheses
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>`;
}
// === ENEMIES (Game module calculation) ===
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}`
});
}
}
// === LOOT TABLES (Game module calculation) ===
const { data: lootTables } = await Utils.resolveData("Loot_Tables", false, version);
if (!Utils.isModuleEmpty(lootTables)) {
for (const tableId in lootTables) {
const table = lootTables[tableId];
// Exclude gambling tables
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="Trade_Items-wikiDropSources">No known drops in ${version}. ${Wiki.versionSelector(version, "Trade_Items", "wikiDropSources", [itemArg])}</div>`;
}
// Group
const grouped = {};
for (const entry of dropSources) {
if (!grouped[entry.category]) grouped[entry.category] = [];
grouped[entry.category].push(entry);
}
// Sort each category
for (const key in grouped) {
grouped[key].sort((a, b) => b.effectiveChance - a.effectiveChance);
}
// === RENDER ===
let wikitext = `{| class="wiki-table" style="width:auto;"\n`;
wikitext += `! colspan="6" | :Version: ${Wiki.versionSelector(version, "Trade_Items", "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="Trade_Items-wikiDropSources">${wikitext}</div>`;
}
async function wikiShopSources(props) {
const args = Utils.resolveArgs(props);
const itemArg = args["item"] || args[0];
const selectedVersion = args["version"] || args[1];
const { data: items, version } = await Utils.resolveData("Trade_Items", false, selectedVersion);
if (Utils.isModuleEmpty(items)) {
return `<div id="Trade_Items-wikiShopSources">⚠️ Trade item data unavailable for ${version}. ${Wiki.versionSelector(version, "Trade_Items", "wikiShopSources", [itemArg])}</div>`;
}
const itemId = itemArg?.toLowerCase()?.replace(/\s+|\'/g, "_").split("/").pop();
const itemData = items[itemId];
if (!itemData) {
return `<div id="Trade_Items-wikiShopSources">⚠️ Trade item '${itemArg}' not found in ${version}. ${Wiki.versionSelector(version, "Trade_Items", "wikiShopSources", [itemArg])}</div>`;
}
const shopSources = [];
// ==================================================
// HELPER: Parse Gambling Table Name
// Pattern: loottable_<npc>_<amount>cost_tierXX
// ==================================================
function parseGamblingTable(rawName) {
if (!rawName) return null;
let name = rawName.replace(/^lootTable_/i, "");
const tierMatch = name.match(/tier(\d+)/i);
const tier = tierMatch ? `Tier ${parseInt(tierMatch[1], 10)}` : null;
const costMatch = name.match(/_(\d+)cost/i);
const cost = costMatch ? parseInt(costMatch[1], 10) : null;
const npcMatch = name.match(/^(.+?)_\d+cost/i);
const npcRaw = npcMatch ? npcMatch[1] : null;
if (!npcRaw || cost === null) return null;
const npcName = npcRaw
.replace(/_/g, " ")
.replace(/\b\w/g, c => c.toUpperCase());
return {
npcName,
tier,
price: cost
};
}
// ================================
// SHOPS MODULE
// ================================
const { data: shops } = await Utils.resolveData("Shops", false, version);
if (!Utils.isModuleEmpty(shops)) {
for (const shopId in shops) {
const shop = shops[shopId];
if (!shop?.tables?.length) continue;
for (const table of shop.tables) {
if (!table?.items?.length) continue;
for (const entry of table.items) {
if (entry.itemGuid !== itemData.guid) continue;
let price = null;
let currency = null;
if (entry.specialStoreCost && entry.specialStoreCostQuantity) {
price = entry.specialStoreCostQuantity;
currency = Object.values(items).find(
i => i.guid === entry.specialStoreCost
);
}
if (!price && itemData.value?.vendor && !entry.isGambleSlot) {
price = itemData.value.vendor;
currency = { name: "Crowns", id: "gold" };
}
shopSources.push({
category: entry.isGambleSlot ? "Gambling" : "Direct Sale",
shopName: shop.name,
npcName: shop.shopOwner?.name,
levelRequirement: table.levelRequirement ?? null,
price,
currency,
chance: null
});
}
}
}
}
// =================================
// LOOT_TABLES MODULE (Pure Gambling)
// =================================
const { data: lootTables } = await Utils.resolveData("Loot_Tables", false, version);
if (!Utils.isModuleEmpty(lootTables)) {
for (const tableId in lootTables) {
const table = lootTables[tableId];
if (!table?.drops?.length) continue;
if (!/cost/i.test(table.name) || !/tier\d+/i.test(table.name)) continue;
const entry = table.drops.find(d => d.guid === itemData.guid);
if (!entry) continue;
const parsed = parseGamblingTable(table.name);
if (!parsed) continue;
shopSources.push({
category: "Gambling",
shopName: parsed.npcName,
npcName: parsed.npcName,
levelRequirement: parsed.tier,
price: parsed.price,
currency: { name: "Crowns", id: "gold" },
chance: entry.dropChance ?? null
});
}
}
if (!shopSources.length) {
return `<div id="Trade_Items-wikiShopSources">No known shop sources in ${version}. ${Wiki.versionSelector(version, "Trade_Items", "wikiShopSources", [itemArg])}</div>`;
}
// ================================
// GROUP
// ================================
const grouped = {};
for (const entry of shopSources) {
if (!grouped[entry.category]) grouped[entry.category] = [];
grouped[entry.category].push(entry);
}
// ================================
// SORTING
// ================================
for (const category in grouped) {
if (category === "Gambling") {
// Highest chance first
grouped[category].sort((a, b) => {
const chanceA = a.chance ?? 0;
const chanceB = b.chance ?? 0;
return chanceB - chanceA;
});
} else {
// Direct Sale: level → price
grouped[category].sort((a, b) => {
const lvlA = parseInt(a.levelRequirement) || 0;
const lvlB = parseInt(b.levelRequirement) || 0;
if (lvlA !== lvlB) return lvlA - lvlB;
return (a.price ?? 0) - (b.price ?? 0);
});
}
}
// ================================
// RENDER
// ================================
let wikitext = `{| class="wiki-table" style="width:auto;"\n`;
wikitext += `! colspan="5" | :Version: ${Wiki.versionSelector(version, "Trade_Items", "wikiShopSources", [itemArg])}\n`;
for (const category in grouped) {
wikitext += "|-\n";
wikitext += `! colspan="5" | ${category}\n`;
wikitext += "|-\n";
wikitext += "! Shop !! Vendor !! Level/Tier !! Price !! Chance\n";
for (const source of grouped[category]) {
let priceText = "-";
if (source.price && source.currency) {
if (source.currency.id === "gold") {
priceText = `${source.price} ${source.currency.name}`;
} else {
priceText = `${source.price} × [[Trade Items/${source.currency.name}|${source.currency.name}]]`;
}
}
let chanceText = "—";
if (source.chance !== null && source.chance !== undefined) {
chanceText = `${(source.chance * 100).toFixed(2)}%`;
}
const shopLink = `[[Shops/${source.shopName}|${source.shopName}]]`;
const npcLink = source.npcName
? `[[NPCs/${source.npcName}|${source.npcName}]]`
: "—";
wikitext += "|-\n";
wikitext += `| ${shopLink}`;
wikitext += ` || ${npcLink}`;
wikitext += ` || ${source.levelRequirement ?? "-"}`;
wikitext += ` || ${priceText}`;
wikitext += ` || ${chanceText}\n`;
}
}
wikitext += "|}";
return `<div id="Trade_Items-wikiShopSources">${wikitext}</div>`;
}
async function wikiQuestSources(props) {
const args = Utils.resolveArgs(props);
const itemArg = args["item"] || args[0];
const selectedVersion = args["version"] || args[1];
const { data: items, version } = await Utils.resolveData("Trade_Items", false, selectedVersion);
if (Utils.isModuleEmpty(items)) {
return `<div id="Trade_Items-wikiQuestSources">⚠️ Trade item data is unavailable for version ${version}. ${Wiki.versionSelector(version, "Trade_Items", "wikiQuestSources", [itemArg])}</div>`;
}
const itemId = itemArg?.toLowerCase()?.replace(/\s+|\'/g, '_').split("/").pop();
const itemData = items[itemId];
if (!itemData) {
return `<div id="Trade_Items-wikiQuestSources">⚠️ Trade item '${itemArg}' not found in data version ${version}. ${Wiki.versionSelector(version, "Trade_Items", "wikiQuestSources", [itemArg])}</div>`;
}
const { data: quests } = await Utils.resolveData("Quests", false, version);
if (Utils.isModuleEmpty(quests)) {
return `<div id="Trade_Items-wikiQuestSources">⚠️ Quest data is unavailable for version ${version}. ${Wiki.versionSelector(version, "Trade_Items", "wikiQuestSources", [itemArg])}</div>`;
}
const questSources = [];
for (const questId in quests) {
const quest = quests[questId];
if (!quest?.rewards?.items?.length) continue;
const rewardEntry = quest.rewards.items.find(r => r?.guid === itemData.guid);
if (!rewardEntry) continue;
questSources.push({
questName: quest.name,
questLevel: quest.level ?? null,
quantity: rewardEntry.quantity ?? 0,
questId
});
}
if (!questSources.length) {
return `<div id="Trade_Items-wikiQuestSources">No known quest rewards in version ${version}. ${Wiki.versionSelector(version, "Trade_Items", "wikiQuestSources", [itemArg])}</div>`;
}
// Sort by level ascending, then quest name
questSources.sort((a, b) => {
const lvlA = a.questLevel ?? 0;
const lvlB = b.questLevel ?? 0;
if (lvlA !== lvlB) return lvlA - lvlB;
return a.questName.localeCompare(b.questName);
});
let wikitext = `{| class="wiki-table" style="width:auto;"\n`;
wikitext += `! colspan="3" | :Version: ${Wiki.versionSelector(version, "Trade_Items", "wikiQuestSources", [itemArg])}\n`;
wikitext += "|-\n";
wikitext += "! Quest !! Level !! Reward Quantity\n";
for (const source of questSources) {
const questLink = `[[Quests/${source.questName}|${source.questName}]]`;
wikitext += "|-\n";
wikitext += `| ${questLink} || ${source.questLevel ?? "-"} || ${source.quantity}\n`;
}
wikitext += "|}\n";
return `<div id="Trade_Items-wikiQuestSources">${wikitext}</div>`;
}
async function wikiEnchantEquipment(props) {
const args = Utils.resolveArgs(props);
const itemArg = args["item"] || args[0];
const selectedVersion = args["version"] || args[1];
const { data: tradeItems, version } = await Utils.resolveData("Trade_Items", false, selectedVersion);
if (Utils.isModuleEmpty(tradeItems)) {
return `<div id="Trade_Items-wikiEnchantEquipment">⚠️ Trade item data is unavailable for version ${version}. ${Wiki.versionSelector(version, "Trade_Items", "wikiEnchantEquipment", [itemArg])}</div>`;
}
const itemId = itemArg?.toLowerCase()?.replace(/\s+|\'/g, '_').split("/").pop();
const itemData = tradeItems[itemId];
if (!itemData) {
return `<div id="Trade_Items-wikiEnchantEquipment">⚠️ Trade item '${itemArg}' not found in data version ${version}. ${Wiki.versionSelector(version, "Trade_Items", "wikiEnchantEquipment", [itemArg])}</div>`;
}
const { data: equipmentData } = await Utils.resolveData("Equipment", false, version);
if (Utils.isModuleEmpty(equipmentData)) {
return `<div id="Trade_Items-wikiEnchantEquipment">⚠️ Equipment data is unavailable for version ${version}. ${Wiki.versionSelector(version, "Trade_Items", "wikiEnchantEquipment", [itemArg])}</div>`;
}
const sources = [];
for (const eqId in equipmentData) {
const eq = equipmentData[eqId];
if (!eq?.modifiers?.cost?.item) continue;
if (eq?.rarity == "exotic") continue;
if (eq.modifiers.cost.item !== itemData.id && eq.modifiers.cost.item !== itemData.guid) continue;
sources.push({
equipmentName: eq.name,
quantity: eq.modifiers.cost.quantity ?? 1,
equipmentId: eq.id
});
}
if (!sources.length) {
return `<div id="Trade_Items-wikiEnchantEquipment">No equipment uses this trade item as enchanting material in version ${version}. ${Wiki.versionSelector(version, "Trade_Items", "wikiEnchantEquipment", [itemArg])}</div>`;
}
// Sort by equipment name
sources.sort((a, b) => a.equipmentName.localeCompare(b.equipmentName));
let wikitext = `{| class="wiki-table" style="width:auto;"\n`;
wikitext += `! colspan="2" | :Version: ${Wiki.versionSelector(version, "Trade_Items", "wikiEnchantEquipment", [itemArg])}\n`;
wikitext += "|-\n";
wikitext += "! Equipment !! Quantity Required\n";
for (const source of sources) {
const equipmentLink = `[[Equipment/${source.equipmentName}|${source.equipmentName}]]`;
wikitext += "|-\n";
wikitext += `| ${equipmentLink} || ${source.quantity}\n`;
}
wikitext += "|}\n";
return `<div id="Trade_Items-wikiEnchantEquipment">${wikitext}</div>`;
}
function diffObjects(oldObj, newObj, basePath = "") {
const changes = [];
const isObject = (val) =>
val && typeof val === "object" && !Array.isArray(val);
const allKeys = new Set([
...Object.keys(oldObj || {}),
...Object.keys(newObj || {})
]);
for (const key of allKeys) {
const oldValue = oldObj?.[key];
const newValue = newObj?.[key];
const path = basePath ? `${basePath}.${key}` : key;
if (isObject(oldValue) && isObject(newValue)) {
changes.push(...diffObjects(oldValue, newValue, path));
continue;
}
if (JSON.stringify(oldValue) !== JSON.stringify(newValue)) {
changes.push({
field: path,
oldValue,
newValue
});
}
}
return changes;
}
async function wikiQuestUse(props) {
const args = Utils.resolveArgs(props);
const itemArg = args["item"] || args[0];
const selectedVersion = args["version"] || args[1];
const { data: items, version } = await Utils.resolveData("Trade_Items", false, selectedVersion);
if (Utils.isModuleEmpty(items)) {
return `<div id="Trade_Items-wikiQuestUse">⚠️ Trade item data is unavailable for version ${version}. ${Wiki.versionSelector(version, "Trade_Items", "wikiQuestUse", [itemArg])}</div>`;
}
const itemId = itemArg?.toLowerCase()?.replace(/\s+|\'/g, '_').split("/").pop();
const itemData = items[itemId];
if (!itemData) {
return `<div id="Trade_Items-wikiQuestUse">⚠️ Trade item '${itemArg}' not found in data version ${version}. ${Wiki.versionSelector(version, "Trade_Items", "wikiQuestUse", [itemArg])}</div>`;
}
const { data: quests } = await Utils.resolveData("Quests", false, version);
if (Utils.isModuleEmpty(quests)) {
return `<div id="Trade_Items-wikiQuestUse">⚠️ Quest data is unavailable for version ${version}. ${Wiki.versionSelector(version, "Trade_Items", "wikiQuestUse", [itemArg])}</div>`;
}
const questUses = [];
for (const questId in quests) {
const quest = quests[questId];
if (!quest?.objectives?.items?.length) continue;
const objectiveEntry = quest.objectives.items.find(o => o?.guid === itemData.guid);
if (!objectiveEntry) continue;
questUses.push({
questName: quest.name,
questLevel: quest.level ?? null,
quantity: objectiveEntry.count ?? 0,
});
}
if (!questUses.length) {
return `<div id="Trade_Items-wikiQuestUse">No known quest uses in version ${version}. ${Wiki.versionSelector(version, "Trade_Items", "wikiQuestUse", [itemArg])}</div>`;
}
// Sort by level ascending, then quest name
questUses.sort((a, b) => {
const lvlA = a.questLevel ?? 0;
const lvlB = b.questLevel ?? 0;
if (lvlA !== lvlB) return lvlA - lvlB;
return a.questName.localeCompare(b.questName);
});
let wikitext = `{| class="wiki-table" style="width:auto;"\n`;
wikitext += `! colspan="3" | :Version: ${Wiki.versionSelector(version, "Trade_Items", "wikiQuestUse", [itemArg])}\n`;
wikitext += "|-\n";
wikitext += "! Quest !! Level !! Quantity\n";
for (const use of questUses) {
const questLink = `[[Quests/${use.questName}|${use.questName}]]`;
wikitext += "|-\n";
wikitext += `| ${questLink} || ${use.questLevel ?? "-"} || ${use.quantity}\n`;
}
wikitext += "|}\n";
return `<div id="Trade_Items-wikiQuestUse">${wikitext}</div>`;
}
async function wikiChanges(props) {
const args = Utils.resolveArgs(props);
const itemArg = args["item"] || args[0];
const selectedVersion = args["version"] || args[1];
const { latest: latestAlias, versions: availableVersions } = await requireData("Versions");
let resolvedVersion;
if (!selectedVersion || selectedVersion === latestAlias) {
resolvedVersion = availableVersions[0]?.id;
} else {
resolvedVersion = selectedVersion;
}
const { data: currentData } = await Utils.resolveData("Trade_Items", false, resolvedVersion);
if (Utils.isModuleEmpty(currentData)) {
return `<div id="Trade_Items-wikiChanges">⚠️ Trade item data unavailable for version ${resolvedVersion}. ${Wiki.versionSelector(resolvedVersion, "Trade_Items", "wikiChanges", [itemArg])}</div>`;
}
const itemId = itemArg?.toLowerCase()?.replace(/\s+|\'/g, '_').split("/").pop();
const currentItem = currentData[itemId];
if (!currentItem) {
return `<div id="Trade_Items-wikiChanges">⚠️ Trade item '${itemArg}' not found in version ${resolvedVersion}. ${Wiki.versionSelector(resolvedVersion, "Trade_Items", "wikiChanges", [itemArg])}</div>`;
}
const currentIndex = availableVersions.findIndex(v => v.id === resolvedVersion);
const previousVersion = currentIndex >= 0 ? availableVersions[currentIndex + 1]?.id : null;
if (!previousVersion) {
return `<div id="Trade_Items-wikiChanges">No previous version available to compare. ${Wiki.versionSelector(resolvedVersion, "Trade_Items", "wikiChanges", [itemArg])}</div>`;
}
const { data: previousData } = await Utils.resolveData("Trade_Items", false, previousVersion);
const previousItem = previousData?.[itemId];
if (!previousItem) {
return `<div id="Trade_Items-wikiChanges">
{| class="wiki-table" style="width:auto;"
! ${currentItem.name} was '''added''' in ${resolvedVersion} ${Wiki.versionSelector(resolvedVersion, "Trade_Items", "wikiChanges", [itemArg])}
|}
</div>`;
}
// -------------------------
// DIFF
// -------------------------
const ignoredPrefixes = ["icon", "guid", "id"];
const rawChanges = diffObjects(previousItem, currentItem)
.filter(change =>
!ignoredPrefixes.some(prefix =>
change.field.startsWith(prefix)
)
);
if (!rawChanges.length) {
return `<div id="Trade_Items-wikiChanges">No gameplay changes detected between ${previousVersion} and ${resolvedVersion}. ${Wiki.versionSelector(resolvedVersion, "Trade_Items", "wikiChanges", [itemArg])}</div>`;
}
// -------------------------
// GROUP CHANGES
// -------------------------
const sections = {
General: [],
Stats: [],
Value: [],
Modifiers: [],
Other: []
};
for (const change of rawChanges) {
if (change.field.startsWith("stats")) {
sections.Stats.push(change);
} else if (change.field.startsWith("buyPrice") || change.field.startsWith("sellPrice")) {
sections.Value.push(change);
} else if (change.field.startsWith("modifiers")) {
sections.Modifiers.push(change);
} else if (["name", "description", "rarity", "stackSize"].some(f => change.field.startsWith(f))) {
sections.General.push(change);
} else {
sections.Other.push(change);
}
}
// -------------------------
// FORMAT HELPERS
// -------------------------
const formatValue = (val) => {
if (val === undefined) return "—";
if (typeof val === "number") return val;
if (typeof val === "string") return `"${val}"`;
return `<code>${JSON.stringify(val)}</code>`;
};
const formatChangeRow = (change) => {
const { field, oldValue, newValue } = change;
let displayOld = formatValue(oldValue);
let displayNew = formatValue(newValue);
let indicator = "";
if (oldValue === undefined) {
indicator = "🟢 Added";
} else if (newValue === undefined) {
indicator = "🔴 Removed";
} else if (typeof oldValue === "number" && typeof newValue === "number") {
const diff = newValue - oldValue;
if (diff > 0) indicator = `▲ +${diff}`;
else if (diff < 0) indicator = `▼ ${diff}`;
}
return `|-\n| ${field} || ${displayOld} || ${displayNew} || ${indicator}\n`;
};
// -------------------------
// BUILD WIKITABLE
// -------------------------
let wikitext = `{| class="wiki-table" style="width:auto;"\n`;
wikitext += `! colspan="4" | Changes for ${currentItem.name} in ${resolvedVersion} (compared to ${previousVersion}) ${Wiki.versionSelector(resolvedVersion, "Trade_Items", "wikiChanges", [itemArg])}\n`;
wikitext += "|-\n";
wikitext += "! Field !! Previous !! New !! Change\n";
for (const sectionName in sections) {
const section = sections[sectionName];
if (!section.length) continue;
wikitext += `|-\n| colspan="4" style="background:#222; font-weight:bold;" | ${sectionName}\n`;
section.sort((a, b) => a.field.localeCompare(b.field));
for (const change of section) {
wikitext += formatChangeRow(change);
}
}
wikitext += "|}\n";
return `<div id="Trade_Items-wikiChanges">${wikitext}</div>`;
}
exports = {
wikiTable,
wikiNavbox,
wikiTooltip,
wikiInfobox,
wikiDropSources,
wikiShopSources,
wikiQuestSources,
wikiEnchantEquipment,
wikiQuestUse,
wikiChanges,
}