ATLYSS TechPendium

Module:Consumables

Viewing old revision of Module:Consumables

You are viewing an old revision of this page from 3/30/2026, 12:52:44 PM.

View latest version
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 { latest: latestVersion } = await requireData("Versions");
const { TooltipBuilder } = await require("Tooltip_Builder");
const { InfoboxBuilder } = await require("Infobox_Builder");

async function buildConsumableTooltipHtml(itemData, itemId, version = latestVersion) {
    const tooltip = new TooltipBuilder(`Consumable: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>`);

    switch (itemData.category) {
        case "classtome":
            const { data: classData } = await Utils.resolveData("Classes", false, version);

            const classInfo = Utils.findByProperty(classData, "guid", itemData.unlocks?.classGuid);

            if (itemData.unlocks?.tierIndex) {
                tooltip.addLine(`<div>Unlocks Class: ${classInfo?.name ?? itemData.unlocks?.classGuid ?? "N/A"} (Tier: ${classInfo?.tiers[itemData.unlocks.tierIndex - 1]?.name ?? "N/A"})</div>`);
            } else {
                tooltip.addLine(`<div>Unlocks Class: ${classInfo?.name ?? itemData.unlocks?.classGuid ?? "N/A"}</div>`);
            }

            tooltip.addLine(`<div>Level Requirement: ${itemData.levelRequirement ?? "N/A"}</div>`);
            break;
        case "dye":
            tooltip.addLine(`<div>Buy Price: ${itemData.buyPrice} Crowns</div>`);
            tooltip.addLine(`<div>Sell Price: ${Game.getSellPrice(itemData.buyPrice)} Crowns</div>`);
            tooltip.addLine(`<span>Dye Color:</span><span class="dye-box" style="background-color: ${itemId.replace("_dye", "")};"></span>`);
            break;
        case "skillscroll":
            const { data: skillData } = await Utils.resolveData("Skills", false, version);
            const skillInfo = Utils.findByProperty(skillData, "guid", itemData.unlocks?.skillGuid);

            tooltip.addLine(`<div>Buy Price: ${itemData.buyPrice} Crowns</div>`);
            tooltip.addLine(`<div>Sell Price: ${Game.getSellPrice(itemData.buyPrice)} Crowns</div>`);
            tooltip.addLine(`<div>Unlocks Skill: ${skillInfo?.name ?? itemData.unlocks?.skillGuid ?? "N/A"}</div>`);
            tooltip.addLine(`<div>Level Requirement: ${itemData.levelRequirement ?? "N/A"}</div>`);
            break;
        case "instant":
            tooltip.addLine(`<div>Buy Price: ${itemData.buyPrice} Crowns</div>`);
            tooltip.addLine(`<div>Sell Price: ${Game.getSellPrice(itemData.buyPrice)} Crowns</div>`);
            
            if (itemData.effects.instant?.health) {
                tooltip.addLine(`<div>Restores Health: ${itemData.effects.instant.health}</div>`);
            }

            if (itemData.effects.instant?.mana) {
                tooltip.addLine(`<div>Restores Mana: ${itemData.effects.instant.mana}</div>`);
            }

            if (itemData.effects.instant?.stamina) {
                tooltip.addLine(`<div>Restores Stamina: ${itemData.effects.instant.stamina}</div>`);
            }

            if (itemData.effects.instant?.experience) {
                tooltip.addLine(`<div>Restores Experience: ${itemData.effects.instant.experience}</div>`);
            }

            if (itemData.effects.instant?.resetSkillPoints) {
                tooltip.addLine(`<div>Resets Skill Points</div>`);
            }

            if (itemData.effects.instant?.resetAttributePoints) {
                tooltip.addLine(`<div>Resets Attribute Points</div>`);
            }
            break;
        case "status":
            tooltip.addLine(`<div>Buy Price: ${itemData.buyPrice} Crowns</div>`);
            tooltip.addLine(`<div>Sell Price: ${Game.getSellPrice(itemData.buyPrice)} Crowns</div>`);

            if (itemData.effects.statusCondition?.conditionGuid) {
                const { data: conditionData } = await Utils.resolveData("Conditions", false, version);
                const conditionInfo = Utils.findByProperty(conditionData, "guid", itemData.effects.statusCondition.conditionGuid);

                tooltip.addLine(`<div>Applies Status Condition: ${conditionInfo?.name ?? itemData.effects.statusCondition.conditionGuid ?? "N/A"}</div>`);
            }
            break;
        case "tool":
            tooltip.addLine(`<div>Buy Price: ${itemData.buyPrice} Crowns</div>`);
            tooltip.addLine(`<div>Sell Price: ${Game.getSellPrice(itemData.buyPrice)} Crowns</div>`);
            break;
    }

    return tooltip.build();
}

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

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

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

    const sortedData = Utils.sortObjectByKey(data);

    let table = "";
    let tooltips = "";

    table += `{| class="wiki-table" style="width:auto;"\n`;
    table += `! colspan="5" | :Version: ${Wiki.versionSelector(version, "Consumables", "wikiTable")}\n`;
    table += `|-\n`;
    table += `! Name !! Rarity !! Max Stack !! Buy Price !! Sell Price\n`;

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

        table += `|-\n`;

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

        table += `| <span data-tooltip-id="${tooltipId}">${availablePages.has(`File:${item.id}_icon.png`) ? `[[File:${item.id}_icon.png|16px|inline|alt=${item.name}|link=Consumables/${item.name}]]` : ""}[[Consumables/${item.name}|${item.name}]]</span>\n`;
        table += `| ${Wiki.wikiRarity(item.rarity)}\n`;
        table += `| ${item.stack?.max ?? "N/A"}\n`;
        
        if (item.buyPrice != undefined) {
            table += `| ${item.buyPrice} Crowns\n`;
            table += `| ${Game.getSellPrice(item.buyPrice)} Crowns\n`;
        } else {
            table += `| N/A\n`;
            table += `| N/A\n`;
        }

        if (!Cache.has("ConsumableTooltips", tooltipId)) {
            Cache.set("ConsumableTooltips", tooltipId, await buildConsumableTooltipHtml(item, itemId, version));
            tooltips += Cache.get("ConsumableTooltips", tooltipId);
        }
    }

    table += `|}\n`;

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

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

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

    const sortedData = Utils.sortObjectByKey(data);

    let navbox = "";
    let tooltips = "";

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

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

    const itemLinks = [];

    for (const id in sortedData) {
        const item = sortedData[id];
        const itemId = item.id || id;
        const tooltipId = `Consumable:Item:${itemId}`;

        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=Consumables/${item.name}]]` : ""}[[Consumables/${item.name}|${item.name}]]</span>`);

        if (!Cache.has("ConsumableTooltips", tooltipId)) {
            Cache.set("ConsumableTooltips", tooltipId, await buildConsumableTooltipHtml(item, itemId, version));
            tooltips += Cache.get("ConsumableTooltips", tooltipId);
        }
    }

    navbox += itemLinks.join(" • ") + `\n`;
    navbox += `|}\n`;

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

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

    const itemId = itemArg.toLowerCase().replace(/\s+|\'/g, '_').split("/").pop();

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

    if (Cache.has("ConsumableTooltips", tooltipId)) {
        return Cache.get("ConsumableTooltips", tooltipId);
    }

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

    if (Utils.isModuleEmpty(data)) {
        return `⚠️ Consumable data is unavailable.`;
    }

    const itemData = data[itemId];

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

    const tooltipHtml = await buildConsumableTooltipHtml(itemData, itemId);

    const linkHtml = 
        `<span data-tooltip-id="${tooltipId}">` +
        `[[Consumables/${itemData.name}|${itemData.name}]]` +
        `</span>`;

    const result = linkHtml + tooltipHtml;

    Cache.set("ConsumableTooltips", tooltipId, 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("Consumables", false, selectedVersion);

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

    const itemId = itemArg?.toLowerCase().replace(/\s+|'/g, '_').split("/").pop();
    const itemData = data[itemId];

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

    const infobox = new InfoboxBuilder();

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

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

    const subtitleParts = ["Consumable"];
    if (itemData.category) {
        subtitleParts.push(humanize(itemData.category));
    }
    const subtitle = subtitleParts.join(" · ");
    infobox.setSubtitle(subtitle);
    infobox.setRarity(itemData.rarity);
    infobox.setImage(`[[File:${itemData.id || itemId}_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>"));
    }

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

    if (itemData.category) {
        general.addRow("Category", humanize(itemData.category));
    }

    if (itemData.stack?.max) {
        general.addRow("Max Stack Size", itemData.stack.max);
    }

    if (itemData.levelRequirement != null) {
        general.addRow("Level Requirement", itemData.levelRequirement);
    }

    switch (itemData.category) {
        case "classtome": {
            const { data: classData } = await Utils.resolveData("Classes", false, version);
            const classInfo = Utils.findByProperty(classData, "guid", itemData.unlocks?.classGuid);
            const className = classInfo?.name || itemData.unlocks?.classGuid || "N/A";
            const tierName = itemData.unlocks?.tierIndex ? classInfo?.tiers?.[itemData.unlocks.tierIndex - 1]?.name : null;
            general.addRow("Unlocks", tierName ? `${className} (Tier: ${tierName})` : className);
            break;
        }
        case "skillscroll": {
            const { data: skillData } = await Utils.resolveData("Skills", false, version);
            const skillInfo = Utils.findByProperty(skillData, "guid", itemData.unlocks?.skillGuid);
            general.addRow("Unlocks", skillInfo?.name || itemData.unlocks?.skillGuid || "N/A");
            break;
        }
        case "dye": {
            const colorCode = itemId.replace(/_dye$/i, "");
            general.addRow("Dye Color", `<span style="display:inline-block;width:18px;height:18px;border:1px solid #999;background-color:${colorCode};vertical-align:middle;margin-right:6px"></span>${colorCode}`);
            break;
        }
        case "instant": {
            if (itemData.effects?.instant) {
                const instant = itemData.effects.instant;
                if (instant.health != null) general.addRow("Restores Health", instant.health);
                if (instant.mana != null) general.addRow("Restores Mana", instant.mana);
                if (instant.stamina != null) general.addRow("Restores Stamina", instant.stamina);
                if (instant.experience != null) general.addRow("Restores Experience", instant.experience);
                if (instant.resetSkillPoints) general.addRow("Resets", "Skill Points");
                if (instant.resetAttributePoints) general.addRow("Resets", "Attribute Points");
            }
            break;
        }
        case "status": {
            if (itemData.effects?.statusCondition?.conditionGuid) {
                const { data: conditionData } = await Utils.resolveData("Conditions", false, version);
                const conditionInfo = Utils.findByProperty(conditionData, "guid", itemData.effects.statusCondition.conditionGuid);
                general.addRow("Applies Status", conditionInfo?.name || itemData.effects.statusCondition.conditionGuid);
            }
            break;
        }
        case "tool": {
            general.addRow("Tool Type", humanize(itemData.subtype || "Tool"));
            break;
        }
    }

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

    const itemRarityData = Game.getRarityLabel(itemData.rarity);
    const title = itemData.name || itemArg;
    const buyPrice = itemData.buyPrice != null ? `${itemData.buyPrice} Crowns` : "N/A";
    const sellPrice = itemData.buyPrice != null ? `${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 = ${humanize(itemData.category) || "Consumable"}
     | secondary = Max Stack: ${itemData.stack?.max || "N/A"}, 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="Consumables-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("Consumables", false, selectedVersion);

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

    const itemId = itemArg?.toLowerCase().replace(/\s+|\'/g, "_").split("/").pop();
    const itemData = items[itemId];

    if (!itemData) {
        return `<div id="Consumables-wikiDropSources">⚠️ Consumable '${itemArg}' not found in ${version}. ${Wiki.versionSelector(version, "Consumables", "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="Consumables-wikiDropSources">No known drops in ${version}. ${Wiki.versionSelector(version, "Consumables", "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, "Consumables", "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="Consumables-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("Consumables", false, selectedVersion);

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

    const itemId = itemArg?.toLowerCase().replace(/\s+|\'/g, "_").split("/").pop();
    const itemData = items[itemId];

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

    const shopSources = [];

    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
        };
    }

    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) {
                        price = entry.specialStoreCostQuantity;
                        currency = Object.values(items).find(i => i.guid === entry.specialStoreCost);
                    } else if (entry.useDedicatedItemValue) {
                        price = entry.dedicatedItemValue;
                        currency = { name: "Crowns", id: "gold" };
                    } else if (itemData.buyPrice != null && !entry.isGambleSlot) {
                        price = itemData.buyPrice;
                        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
                    });
                }
            }
        }
    }

    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="Consumables-wikiShopSources">No known shop sources in ${version}. ${Wiki.versionSelector(version, "Consumables", "wikiShopSources", [itemArg])}</div>`;
    }

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

    for (const category in grouped) {
        if (category === "Gambling") {
            grouped[category].sort((a, b) => {
                const chanceA = a.chance ?? 0;
                const chanceB = b.chance ?? 0;
                return chanceB - chanceA;
            });
        } else {
            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);
            });
        }
    }

    let wikitext = `{| class="wiki-table" style="width:auto;"\n`;
    wikitext += `! colspan="5" | :Version: ${Wiki.versionSelector(version, "Consumables", "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} × [[Consumables/${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="Consumables-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("Consumables", false, selectedVersion);

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

    const itemId = itemArg?.toLowerCase().replace(/\s+|\'/g, "_").split("/").pop();
    const itemData = items[itemId];

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

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

    if (Utils.isModuleEmpty(quests)) {
        return `<div id="Consumables-wikiQuestSources">⚠️ Quest data is unavailable for version ${version}. ${Wiki.versionSelector(version, "Consumables", "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="Consumables-wikiQuestSources">No known quest rewards in version ${version}. ${Wiki.versionSelector(version, "Consumables", "wikiQuestSources", [itemArg])}</div>`;
    }

    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, "Consumables", "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 += "|}";

    return `<div id="Consumables-wikiQuestSources">${wikitext}</div>`;
}

exports = {
    wikiTable,
    wikiNavbox,
    wikiTooltip,
    wikiInfobox,
    wikiDropSources,
    wikiShopSources,
    wikiQuestSources
}
Last Edited by LiveGobe on 3/30/2026, 12:52:44 PM

This page categories: