Melvor Summoning Simulator

Melvor Summoning Simulator, see examples to run it.

// ==UserScript==
// @name		Melvor Summoning Simulator
// @namespace	http://tampermonkey.net/
// @version		0.0.4
// @description	Melvor Summoning Simulator, see examples to run it.
// @grant		none
// @author		GMiclotte
// @include		https://melvoridle.com/*
// @include		https://*.melvoridle.com/*
// @exclude		https://melvoridle.com/index.php
// @exclude		https://*.melvoridle.com/index.php
// @exclude		https://wiki.melvoridle.com/*
// @exclude		https://*.wiki.melvoridle.com/*
// @inject-into page
// @noframes
// @grant		none
// ==/UserScript==

((main) => {
    const script = document.createElement('script');
    script.textContent = `try { (${main})(); } catch (e) { console.log(e); }`;
    document.body.appendChild(script).parentNode.removeChild(script);
})(() => {

    class SumSim {
        constructor(debug = false) {
            this.debugFlag = debug;
            this.current = undefined;
        }

        debug(...args) {
            if (this.debugFlag) {
                console.log(...args);
            }
        }

        log(...args) {
            console.log(...args);
        }

        getInitial() {
            return {
                startXP: 0,
                startMasteryXP: 0,
                startMasteryLevels: MILESTONES.Summoning.length - 1,
                startPoolXP: 0,
                // accumulator for costs of different iterations
                endCosts: new Costs(),
            };
        }

        getUnlockedItems(level) {
            let count = 0;
            const milestones = MILESTONES.Summoning;
            for (let i = 0; i < milestones.length; i++) {
                if (level >= milestones[i].level) count++;
                else break;
            }
            return count;
        }

        baseMasteryXpToAdd(unlockedItems, masteryLevel, currentTotalMastery) {
            //XP per Action = ((Total unlocked items for skill * current total Mastery level for skill / total Mastery level of skill) + (Mastery level of action * (total items in skill / 10))) * time per action (seconds) / 2
            this.debug('term1', unlockedItems, currentTotalMastery, getTotalMasteryLevelForSkill(Skills.Summoning));
            const term1 = unlockedItems * currentTotalMastery / getTotalMasteryLevelForSkill(Skills.Summoning);
            this.debug('term2', masteryLevel, getTotalItemsInSkill(Skills.Summoning));
            const term2 = masteryLevel * getTotalItemsInSkill(Skills.Summoning) / 10;
            const timeFactor = game.summoning.masteryModifiedInterval / 1000 / 2;
            this.debug('base mastery xp', term1, term2, timeFactor);
            return (term1 + term2) * timeFactor;
        }

        masteryXpModifier(pool) {
            let xpModifier = 0;
            if (pool >= masteryCheckpoints[0]) {
                xpModifier += 5;
            }
            if (getMasteryPoolProgress(CONSTANTS.skill.Firemaking) >= masteryCheckpoints[3]) {
                xpModifier += 5;
            }
            for (let i = 0; i < MASTERY[CONSTANTS.skill.Firemaking].xp.length; i++) {
                if (getMasteryLevel(CONSTANTS.skill.Firemaking, i) >= 99) xpModifier += 0.25;
            }
            xpModifier += playerModifiers.increasedGlobalMasteryXP - playerModifiers.decreasedGlobalMasteryXP;
            xpModifier += getTotalFromModifierArray("increasedMasteryXP", Skills.Summoning) - getTotalFromModifierArray("decreasedMasteryXP", Skills.Summoning);
            return xpModifier;
        }

        masteryXpFinal(base, xpModifier) {
            return Math.max(applyModifier(base, xpModifier), 1);
        }

        addNonShardCosts(recipe, altID, costs) {
            const itemID = recipe.nonShardItemCosts[altID];
            const item = items[itemID];
            const salePrice = Math.max(20, item.sellsFor);
            const itemValueRequired = Summoning.recipeGPCost * (1 - this.getNonShardCostReductionModifier(recipe) / 100);
            const qtyToAdd = Math.max(1, Math.floor(itemValueRequired / salePrice));
            costs.addItem(itemID, qtyToAdd);
        }

        isPoolTierActive(tier) {
            return this.current.currentPool >= masteryCheckpoints[tier];
        }

        modifyItemCost(itemID, quantity, recipe) {
            const masteryLevel = this.current.currentMastery;
            const item = items[itemID];
            if (item.type === 'Shard') {
                // Level 50 Mastery: +1 Shard Cost Reduction
                if (masteryLevel >= 50)
                    quantity--;
                // Level 99 Mastery: +1 Shard Cost Reduction
                if (masteryLevel >= 99)
                    quantity--;
                // Generic Shard Cost Decrease modifier
                quantity += player.modifiers.increasedSummoningShardCost - player.modifiers.decreasedSummoningShardCost;
                // Tier 2 Mastery Pool: +1 Shard Cost Reduction for Tier 1 and Tier 2 Tablets
                if ((recipe.tier === 1 || recipe.tier === 2) && this.isPoolTierActive(1))
                    quantity--;
                // Tier 4 Mastery Pool: +1 Shard Cost Reduction for Tier 3 Tablets
                if (recipe.tier === 3 && this.isPoolTierActive(2))
                    quantity--;
            }
            return Math.max(1, quantity);
        }

        modifyGPCost(recipe) {
            let gpCost = recipe.gpCost;
            gpCost *= 1 - this.getNonShardCostReductionModifier(recipe) / 100;
            return Math.max(1, Math.floor(gpCost));
        }

        modifySCCost(recipe) {
            let scCost = recipe.scCost;
            scCost *= 1 - this.getNonShardCostReductionModifier(recipe) / 100;
            return Math.max(1, Math.floor(scCost));
        }

        getNonShardCostReductionModifier(recipe) {
            const masteryLevel = this.current.currentMastery;
            let modifier = 0;
            // Non-Shard Cost reduction that scales with mastery level
            modifier += Math.floor(masteryLevel / 10) * 5;
            // Level 99 Mastery: +5% Non Shard Cost Reduction
            if (masteryLevel >= 99)
                modifier += 5;
            return modifier;
        }

        superGetRecipeCosts(recipe) {
            const costs = new Costs();
            recipe.itemCosts.forEach(({id, qty}) => {
                qty = this.modifyItemCost(id, qty, recipe);
                if (qty > 0)
                    costs.addItem(id, qty);
            });
            if (recipe.gpCost > 0)
                costs.addGP(this.modifyGPCost(recipe));
            if (recipe.scCost > 0)
                costs.addSlayerCoins(this.modifySCCost(recipe));
            return costs;
        }

        getAltRecipeCosts(recipe, altID) {
            const costs = this.superGetRecipeCosts(recipe);
            if (recipe.nonShardItemCosts.length > 0)
                this.addNonShardCosts(recipe, altID, costs);
            return costs;
        }

        getPreservationChance(chance = 0) {
            // Tier 3 Mastery Pool: +10% Resource Preservation chance
            if (this.isPoolTierActive(2))
                chance += 10;
            return this.superGetPreservationChance(chance);
        }

        superGetPreservationChance(chance) {
            chance += player.modifiers.increasedGlobalPreservationChance - player.modifiers.decreasedGlobalPreservationChance;
            chance += player.modifiers.getSkillModifierValue('increasedSkillPreservationChance', Skills.Summoning);
            chance -= player.modifiers.getSkillModifierValue('decreasedSkillPreservationChance', Skills.Summoning);
            return Math.min(chance, 80);
        }

        addCosts(source, target, amount = 1) {
            target.addGP(source._gp * amount);
            target.addSlayerCoins(source._sc * amount);
            source._items.forEach((amt, id) => {
                target.addItem(id, amt * amount);
            });
            return target;
        }

        plan(current, targetLevel, summonID, actionInterval, altID = 0, quiet = false) {
            // these values are used and updated in each iteration
            current.currentCosts = new Costs();
            current.currentMastery = Math.min(99, exp.xp_to_level(current.startMasteryXP) - 1);
            current.currentPool = current.endPool ?? 0;
            // set this.current
            this.current = current;
            const targetXP = exp.level_to_xp(targetLevel) + 1e-5;
            this.debug('target xp', targetXP);
            const mark = Summoning.marks[summonID];
            const consumptionXP = Summoning.getTabletConsumptionXP(summonID, actionInterval);
            const otherMasteryLevels = current.startMasteryLevels - (exp.xp_to_level(current.startMasteryXP) - 1);
            let xp = current.startXP;
            let masteryXP = current.startMasteryXP;
            let poolXP = current.startPoolXP;
            const maxPool = getMasteryPoolTotalXP(Skills.Summoning);
            let totalActions = 0;
            let totalTablets = 0;
            while (xp < targetXP) {
                const level = Math.min(99, exp.xp_to_level(xp) - 1);
                this.debug('level', level);
                // determine actions to mastery change
                const masteryLevel = Math.min(99, exp.xp_to_level(masteryXP) - 1);
                const masteryPerAction = this.masteryXpFinal(
                    this.baseMasteryXpToAdd(this.getUnlockedItems(level), masteryLevel, masteryLevel + otherMasteryLevels),
                    this.masteryXpModifier(poolXP / maxPool * 100),
                )
                const actionsToMasteryLevel = masteryLevel >= 99 ? Infinity : (exp.level_to_xp(masteryLevel + 1) + 1e-5 - masteryXP) / masteryPerAction;
                // determine actions to pool change
                const poolPerAction = masteryPerAction / (level >= 99 ? 2 : 4);
                const actionsToPoolCheckpoint = ([.10, .25, .50, .95, Infinity].find(x => x > poolXP / maxPool) * maxPool - poolXP) / poolPerAction;
                // compute tablets per action
                let tabletsPerAction = mark.baseQuantity;
                if (masteryLevel >= 99) {
                    tabletsPerAction += 10;
                }
                if (poolXP / maxPool > .95) {
                    tabletsPerAction += 10;
                }
                tabletsPerAction += player.modifiers.increasedSummoningCreationCharges - player.modifiers.decreasedSummoningCreationCharges;
                // compute xp per action
                let xpMultiplier = 1;
                xpMultiplier += getTotalFromModifierArray("increasedSkillXP", Skills.Summoning) / 100;
                xpMultiplier -= getTotalFromModifierArray("decreasedSkillXP", Skills.Summoning) / 100;
                xpMultiplier += (playerModifiers.increasedGlobalSkillXP - playerModifiers.decreasedGlobalSkillXP) / 100;
                const xpPerAction = xpMultiplier * (mark.baseXP + tabletsPerAction * consumptionXP);
                // compute actions to target
                const actionsToTarget = (targetXP - xp) / xpPerAction;
                // compute total actions
                this.debug('actions', actionsToTarget, actionsToMasteryLevel, actionsToPoolCheckpoint)
                const actions = Math.ceil(Math.min(actionsToTarget, actionsToMasteryLevel, actionsToPoolCheckpoint));
                totalActions += actions;
                totalTablets += actions * tabletsPerAction;
                // compute rewards for this iteration
                this.debug('rewards', xpPerAction, masteryPerAction, poolPerAction);
                const rewardXP = actions * xpPerAction;
                const rewardMastery = actions * masteryPerAction;
                const rewardPool = actions * poolPerAction;
                // add rewards to running totals
                xp += rewardXP;
                masteryXP += rewardMastery;
                poolXP += rewardPool;
                this.debug(`${xp} (+${rewardXP})xp; ${masteryXP} (+${rewardMastery}) mxp; ${poolXP} (+${rewardPool}) pool`);
                this.debug(`${exp.xp_to_level(xp) - 1} level; ${exp.xp_to_level(masteryXP) - 1} mastery; ${100.0 * poolXP / maxPool}% pool`);
                // compute costs for this iteration
                const costs = this.getAltRecipeCosts(mark, altID);
                // add costs to running totals
                this.addCosts(costs, current.currentCosts, Math.ceil(actions * (1 - this.getPreservationChance() / 100)));
                // these values should be updated, they are used in this iteration
                current.currentMastery = Math.min(99, exp.xp_to_level(masteryXP) - 1);
                current.currentPool = 100.0 * poolXP / maxPool;
            }
            if (!quiet) {
                this.log(`${totalActions} actions; ${totalTablets} tablets; ${xp} xp; ${masteryXP} mxp; ${poolXP} pool`);
                this.log(`costs:`)
                current.currentCosts._items.forEach((amt, id) => this.log(`${amt} ${Items[id]}`))
                if (current.currentCosts._gp > 0) {
                    this.log(`${current.currentCosts._gp} gp`)
                }
                if (current.currentCosts._sc > 0) {
                    this.log(`${current.currentCosts._sc} sc`);
                }
                this.log(`${exp.xp_to_level(xp) - 1} level; ${Math.min(99, exp.xp_to_level(masteryXP) - 1)} mastery; ${100.0 * poolXP / maxPool}% pool`);
            }
            // check if we used a new type of tablet
            const tabletTypesUsed = current.tabletTypesUsed ?? {};
            if (consumptionXP > 0) {
                tabletTypesUsed[mark.itemID] = true;
            }
            // clear this.current and return current
            current = {
                // these values are used by next iteration
                startXP: xp,
                startMasteryXP: 0, // this is set to 0 since typically we continue with a different action
                startMasteryLevels: otherMasteryLevels + Math.min(99, exp.xp_to_level(masteryXP) - 1),
                startPoolXP: poolXP,
                currentCosts: new Costs(),
                // additional values in case we want to log something
                endCosts: this.addCosts(current.currentCosts, current.endCosts),
                endLevel: exp.xp_to_level(xp) - 1,
                endMasteryXP: masteryXP,
                endMastery: Math.min(99, exp.xp_to_level(masteryXP) - 1),
                endPool: 100.0 * poolXP / maxPool,
                endActions: totalActions,
                endTablets: totalTablets,
                tabletTypesUsed: tabletTypesUsed,
            }
            this.current = undefined;
            this.log(' ');
            return current;
        }

        processPlanOutcome(current) {
            this.log(' ');
            this.log(`total raw costs:`);
            current.endCosts._items.forEach((amt, id) => this.log(`${amt} ${Items[id]}`))
            if (current.endCosts._gp > 0) {
                this.log(`${current.endCosts._gp} gp`)
            }
            if (current.endCosts._sc > 0) {
                this.log(`${current.endCosts._sc} sc`);
            }
            this.log(' ');

            let gpCost = current.endCosts._gp;
            current.endCosts._items.forEach((qty, x) => {
                if (items[x].category === 'Summoning' && items[x].type === 'Shard') {
                    gpCost += items[x].buysFor * qty;
                }
            });
            this.log(`actual gp cost: ${gpCost}`);

            let newItems = 0;
            Object.getOwnPropertyNames(current.tabletTypesUsed).forEach(x => {
                x = Number(x);
                if (game.stats.Items.get(x, ItemStats.TimesFound) === 0) {
                    newItems++;
                }
            });
            current.endCosts._items.forEach((_, x) => {
                x = Number(x);
                if (game.stats.Items.get(x, ItemStats.TimesFound) === 0) {
                    newItems++;
                }
            });
            this.log(`new items required: ${newItems}`);

            const masteryLevelsGained = current.startMasteryLevels - (MILESTONES.Summoning.length - 1);
            this.log(`mastery level gain: ${masteryLevelsGained}`);
        }
    }

    window.sumSim = new SumSim(false);

    // note: SumSim assumes you use all tablets immediately upon creation

    // scenario 1
    // Ent to 5, 1300ms wc actions (dragon axe, normal tree, mastery = 99, no other speed modifiers)
    // Mole to 99, 2200ms mining actions (mithril pickaxe, >50% pool)
    sumSim.example1 = () => {
        let current = sumSim.getInitial();
        current = sumSim.plan(current, 5, Summons.Ent, 1300, 0);
        current.startMasteryXP = 0; // different action, so reset mastery xp
        current = sumSim.plan(current, 99, Summons.Mole, 2200, 0);
        sumSim.processPlanOutcome(current);
        return current;
    }

    // scenario 2
    // Ent to 5, don't use tablets ( == 0ms wc actions)
    // Mole to 99, 2200ms mining actions (mithril pickaxe, >50% pool)
    sumSim.example2 = () => {
        let current = sumSim.getInitial();
        current = sumSim.plan(current, 5, Summons.Ent, 0, 0);
        current.startMasteryXP = 0; // different action, so reset mastery xp
        current = sumSim.plan(current, 99, Summons.Mole, 2200, 0);
        sumSim.processPlanOutcome(current);
        return current;
    }

    // scenario 3
    // Ent to 45, 1300ms wc actions (dragon axe, normal tree, mastery = 99, no other speed modifiers)
    // Leprechaun to 99, 2600ms thieving actions
    sumSim.example3 = () => {
        let current = sumSim.getInitial();
        current = sumSim.plan(current, 45, Summons.Ent, 1300, 0);
        current.startMasteryXP = 0; // different action, so reset mastery xp
        current = sumSim.plan(current, 99, Summons.Leprechaun, 2600, 0);
        sumSim.processPlanOutcome(current);
        return current;
    }

    // scenario 4
    // Ent to 15, 1300ms wc actions (dragon axe, normal tree, mastery = 99, no other speed modifiers)
    // Octopus to 99, 6000ms fishing actions (bronze fishing rod)
    sumSim.example4 = () => {
        let current = sumSim.getInitial();
        current = sumSim.plan(current, 45, Summons.Ent, 1300, 0);
        current.startMasteryXP = 0; // different action, so reset mastery xp
        current = sumSim.plan(current, 99, Summons.Octopus, 6000, 0);
        sumSim.processPlanOutcome(current);
        return current;
    }

    // scenario 5
    // Ent to 99, 1300ms wc actions (dragon axe, normal tree, mastery = 99, no other speed modifiers)
    sumSim.example5 = () => {
        let current = sumSim.getInitial();
        current = sumSim.plan(current, 99, Summons.Ent, 1300, 0);
        sumSim.processPlanOutcome(current);
        return current;
    }

    // scenario 6
    // Golbin to 99, 3000ms combat actions (fixed)
    sumSim.example6 = () => {
        let current = sumSim.getInitial();
        current = sumSim.plan(current, 99, Summons.GolbinThief, 3000, 0);
        sumSim.processPlanOutcome(current);
        return current;
    }

    // scenario 7
    // Ent to 99, no tablets
    sumSim.fuckit = () => {
        let current = sumSim.getInitial();
        current = sumSim.plan(current, 99, Summons.Ent, 0, 0);
        sumSim.processPlanOutcome(current);
        return current;
    }

});