您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds calculations to Bountiful Beanstalk HUD tooltips. Beanster quantities craftable with your resources, and the max possible loot & noise for your current/next room/zone and multiplier.
// ==UserScript== // @name MH - Bean Counter // @author squash // @namespace https://gf.qytechs.cn/users/918578 // @description Adds calculations to Bountiful Beanstalk HUD tooltips. Beanster quantities craftable with your resources, and the max possible loot & noise for your current/next room/zone and multiplier. // @match https://www.mousehuntgame.com/* // @match http://www.mousehuntgame.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=mousehuntgame.com // @grant none // @version 0.1.11 // ==/UserScript== (function () { 'use strict'; function init() { function calculateMaxCraftable(itemsOwned, recipe, ignoreItems = [], accounted = {}, itemsCrafted = {}, cheeseRecipes, settings) { let maxCanCraft = Infinity; let cost = {}; if (settings) { // Calculate craftable of each ingredient recursively for (const item of recipe.items) { if (item.type in cheeseRecipes) { let subRecipe = cheeseRecipes[item.type][settings.recipe[item.type] ? 'upsell' : 'vanilla']; let subCraftable = calculateMaxCraftable(itemsOwned, subRecipe, ignoreItems, accounted, itemsCrafted, cheeseRecipes, settings); itemsCrafted[item.type] = subCraftable.quantity; } } } for (const item of recipe.items) { let quantityOwned = itemsOwned[item.type]?.quantity_unformatted || 0; // If tracking items that COULD be crafted, add them to available ingredients if (item.type in itemsCrafted) { quantityOwned += itemsCrafted[item.type]; } let maxCanCraftThisIngredient = Math.floor(quantityOwned / item.required_quantity); if (!ignoreItems.includes(item.name)) { // || !settings) { maxCanCraft = Math.min(maxCanCraft, maxCanCraftThisIngredient); } } // Record crafting costs for (const item of recipe.items) { if (settings?.general.include_previous_costs) { // Calculate prior recipe costs based on maxCanCraft of current recipe let obj = {}; obj[item.type] = maxCanCraft * item.required_quantity; cost = sumProperties(cost, obj); cost = recursiveCosts(item, cost, cheeseRecipes, settings, itemsOwned); } else { cost[item.type] = maxCanCraft * item.required_quantity; } // accounted properties updated by reference? accounted[item.type] = itemsOwned[item.type].quantity_unformatted + (itemsCrafted[item.type] || 0); } return { name: recipe.action.name, quantity: maxCanCraft * recipe.action.result_quantity, cost: cost, accounted: accounted, }; } function recursiveCosts(item, cost, cheeseRecipes, settings, itemsOwned) { if (item.type in cheeseRecipes) { let quantity = cost[item.type]; // Use configured recipe let subRecipe = cheeseRecipes[item.type][settings.recipe[item.type] ? 'upsell' : 'vanilla']; // Remove already owned qty to not account for ingredients already spent cost[item.type] -= itemsOwned[item.type]?.quantity_unformatted || 0; let maxCanCraft = cost[item.type] / subRecipe.action.result_quantity; for (const subItem of subRecipe.items) { let obj = {}; obj[subItem.type] = maxCanCraft * subItem.required_quantity; cost = sumProperties(cost, obj); cost = recursiveCosts(subItem, cost, cheeseRecipes, settings, itemsOwned); } // Change cost back to actual qty of cheese needed cost[item.type] = quantity; } return cost; } function sumProperties(obj1, obj2) { const result = { ...obj1 }; // Copy object for (const key in obj2) { if (result.hasOwnProperty(key)) { result[key] += obj2[key]; } else { // If the key doesn't exist in result, add it result[key] = obj2[key]; } } return result; } function calculateAllCheeses(cheeseRecipes, itemsOwned, ignoreItems, settings) { let allTotals = {}; const cheeseTypes = Object.keys(cheeseRecipes); // 'beanster_cheese', 'lavish_beanster_cheese', 'royal_beanster_cheese' for (const cheeseType of cheeseTypes) { let recipeTotals = {}; let types = []; if (settings.recipe[cheeseType]) { types.push('upsell'); } else { types.push('vanilla'); } for (const recipeType of types) { let cheeseRecipe = cheeseRecipes[cheeseType][recipeType]; let craftingResult = calculateMaxCraftable(itemsOwned, cheeseRecipe, ignoreItems); recipeTotals[recipeType] = { as_is: craftingResult, }; recipeTotals[recipeType].with_crafted = calculateMaxCraftable(itemsOwned, cheeseRecipe, ignoreItems, {}, {}, cheeseRecipes, settings); } allTotals[cheeseType] = recipeTotals; } return allTotals; } function calculatePossibleLoot(data) { let totalLootMultiplier = data.loot_multipliers.total; let huntsPerRoom = 20; // Assuming a fixed number of hunts per room // Calculate total possible loot depending on in_castle status let currentRoomOrZone, nextRoomOrZone, huntsRemaining; if (data.in_castle) { currentRoomOrZone = data.castle.current_room; nextRoomOrZone = data.castle.next_room; huntsRemaining = data.castle.hunts_remaining; } else { currentRoomOrZone = data.beanstalk.current_zone; nextRoomOrZone = data.beanstalk.next_zone; huntsRemaining = data.beanstalk.hunts_remaining; } let nextRoomOrZoneMultiplier = (totalLootMultiplier / currentRoomOrZone.loot_multiplier) * nextRoomOrZone.loot_multiplier; return { currentRoomOrZoneLoot: { hunts: huntsRemaining, quantity: totalLootMultiplier * huntsRemaining, }, nextRoomOrZoneLoot: { hunts: huntsPerRoom, quantity: nextRoomOrZoneMultiplier * huntsPerRoom, }, }; } function calculateCatchesUntilMaxNoise(data) { const noisePerCatch = data.loot_multipliers.total; const remainingNoise = data.castle.max_noise_level - data.castle.noise_level; const catchesNeeded = Math.ceil(remainingNoise / noisePerCatch); const mayWake = catchesNeeded <= data.castle.hunts_remaining; const possibleNoise = data.castle.hunts_remaining * noisePerCatch; const noiseDiff = mayWake ? possibleNoise - remainingNoise : remainingNoise - possibleNoise; const catchesShort = mayWake ? 0 : catchesNeeded - data.castle.hunts_remaining; // Noise short, or noise extra let content = `${catchesNeeded} catches until full noise. `; if (mayWake) { content += `May wake giant with extra ${noiseDiff} noise.`; } else { content += `${catchesShort} catches (${noiseDiff} noise) short of waking giant.`; } if (remainingNoise < 0) { content = ``; } return content; } function renderCraftable(output) { let allRenders = []; const recipeTypeMap = { upsell: 'Magic Essence', vanilla: 'Standard', }; const calcTypeMap = { as_is: 'Current Ingredients', with_crafted: 'Craftable Ingredients', }; const bait = document.querySelectorAll('.headsUpDisplayBountifulBeanstalkView__baitCraftableContainer'); if (bait) { for (const el of bait) { let result = output[el.dataset.itemType]; let tooltip = el.querySelector('.mousehuntTooltip'); let extras = el.querySelector('.mousehuntTooltip__estimates') || document.createElement('div'); extras.classList = 'mousehuntTooltip__estimates'; let content = ''; for (const recipeType in result) { content += `<br><b>${recipeTypeMap[recipeType]}</b>`; for (const calcType in result[recipeType]) { content += `<br>${calcTypeMap[calcType]}<br>`; content += `<div style="padding-left: 1.5ch;"><b>${result[recipeType][calcType].quantity.toLocaleString('en-US')}</b> ${result[recipeType][calcType].name}</div>`; content += `<div style="padding-left: 1.5ch;">${renderCraftableCost(result[recipeType][calcType].cost, result[recipeType][calcType].accounted)}</div>`; } content += `<br>`; } allRenders.push(content); extras.innerHTML = ` --- ${content} `; tooltip.append(extras); } } return allRenders; } function renderTypeAsName(input) { // Remove undesired phrases input = input.replace(/_craft_item|_stat_item|_cheese/g, ''); // Replace underscores with spaces input = input.replace(/_/g, ' '); // Uppercase the first letter of every word input = input.replace(/\b\w/g, function (letter) { return letter.toUpperCase(); }); return input; } function renderCraftableCost(items, accounted) { let out = ''; for (const type in items) { let short = accounted[type] - items[type]; out += `<i>(${items[type].toLocaleString('en-US')} ${renderTypeAsName(type)}`; if (short < 0) { out += `<br><span style="color: red;">${short.toLocaleString('en-US')}</span>`; } out += `)</i><br>`; } return out; } function renderTooltip(el, content) { if (el) { let tooltip = el.querySelector('.mousehuntTooltip'); if (tooltip) { let extras = el.querySelector('.mousehuntTooltip__estimates') || document.createElement('div'); extras.classList = 'mousehuntTooltip__estimates'; extras.innerHTML = content; tooltip.append(extras); } } } function renderRoomLoot(loot, data) { let elCurrent, elNext; if (data.in_castle) { elCurrent = document.querySelector('.bountifulBeanstalkCastleView__plinthOverlay'); elNext = document.querySelector('.headsUpDisplayBountifulBeanstalkView__castleChevronContainer'); } else { elCurrent = document.querySelector('.bountifulBeanstalkClimbView__plinth'); //elCurrent.style.zIndex = 'auto'; //let chevron = document.querySelector('.bountifulBeanstalkClimbView__plinthChevron'); //chevron.style.zIndex = 'auto'; elNext = document.querySelector('.headsUpDisplayBountifulBeanstalkView__climbNextRoom'); } let contentCurrent = ''; let contentNext = ''; // Loot for current room/zone if (loot.currentRoomOrZoneLoot.quantity > 0) { contentCurrent += `<b>${loot.currentRoomOrZoneLoot.quantity}</b> each with ${loot.currentRoomOrZoneLoot.hunts} catches.`; } // Loot for next room/zone if (data.castle.is_boss_chase == false && loot.nextRoomOrZoneLoot.quantity > 0) { contentNext += `<b>${loot.nextRoomOrZoneLoot.quantity}</b> each with ${loot.nextRoomOrZoneLoot.hunts} catches.`; } renderTooltip(elCurrent, contentCurrent); renderTooltip(elNext, contentNext); } function getSettings() { const defaults = { general: { enable_loot_estimate: true, enable_noise_estimate: true, include_previous_costs: false, }, // Magic essence recipe? recipe: { beanster_cheese: true, lavish_beanster_cheese: true, leaping_lavish_beanster_cheese: true, royal_beanster_cheese: true, }, // Ignore/Assume unlimited ingredients? ignore: { Gold: true, 'Magic Essence': true, 'Beanster Cheese': false, 'Lavish Beanster Cheese': false, 'Golden Harp String': false, }, }; let settings = Object.assign({}, defaults); let storage = JSON.parse(localStorage.getItem('squash-beans')); if (storage) { Object.assign(settings.general, storage.general ?? {}); Object.assign(settings.recipe, storage.recipe ?? {}); Object.assign(settings.ignore, storage.ignore ?? {}); } return settings; } function setupSettingsDialog(renders) { // Configuration dialog const button = document.querySelector('.headsUpDisplayBountifulBeanstalkView__bean-counter-button') || document.createElement('a'); button.style = `position: absolute; z-index: 21; left: 160px; top: 15px; font-size: 18px; font-weight: bold; color: #fa822d; text-shadow: 1px 1px #000;`; button.innerText = '⚙'; button.classList = 'headsUpDisplayBountifulBeanstalkView__bean-counter-button'; button.title = 'Bean Counter HUD Settings'; button.onclick = () => { let dialog = new jsDialog(); dialog.setTemplate('ajax'); dialog.setIsModal(true); dialog.addToken('{*prefix*}', '<h2 class="title">Bean Counter HUD Settings</h2>'); let settings = getSettings(); // Save settings on checkbox change - also do interface refresh to update calculation preview let saveSettings = `localStorage.setItem('squash-beans', JSON.stringify(Array.from(document.querySelector('.jsDialog form').elements).filter(e=>e.type==='checkbox').reduce((d,e)=>(g=e.name.split('.'),d[g[0]]||(d[g[0]]={}),d[g[0]][g[1]]=e.checked,d),{}))); hg.utils.PageUtil.refresh();`; // Settings form let content = ``; content += `<form style="display: flex;">`; content += `<div style="padding: 1em;">`; content += `<h3>Use Magic Essence Recipe?</h3>`; for (const key in settings.recipe) { let label = key.replace(/_/g, ' ').replace(/\b\w/g, (match) => match.toUpperCase()); content += `<p><label><input type="checkbox" name="recipe.${key}" value="1" ${settings.recipe[key] ? 'checked' : ''} onchange="${saveSettings}"> ${label} </label></p>`; } content += `</div>`; content += `<div style="padding: 1em;">`; content += `<h3>Assume Unlimited Ingredient?</h3>`; for (const key in settings.ignore) { content += `<p><label><input type="checkbox" name="ignore.${key}" value="1" ${settings.ignore[key] ? 'checked' : ''} onchange="${saveSettings}"> ${key} </label></p>`; } content += `</div>`; content += `<div style="padding: 1em;">`; content += `<h3>General</h3>`; content += `<p><label><input type="checkbox" name="general.enable_loot_estimate" value="1" ${settings.general.enable_loot_estimate ? 'checked' : ''} onchange="${saveSettings}"> Estimate max room/zone loot? </label></p>`; content += `<p><label><input type="checkbox" name="general.enable_noise_estimate" value="1" ${settings.general.enable_noise_estimate ? 'checked' : ''} onchange="${saveSettings}"> Estimate noise generated? </label></p>`; content += `<p><label><input type="checkbox" name="general.include_previous_costs" value="1" ${settings.general.include_previous_costs ? 'checked' : ''} onchange="${saveSettings}"> Tally crafting costs from prior recipes? </label></p>`; content += `</div>`; content += `</form>`; // Calculation preview content += `<div class="headsUpDisplayBountifulBeanstalkView__bean-counter-dialog-preview" style="display: flex; border-top: 1px solid lightgrey;">`; for (const render of renders) { content += `<div style="padding: 1em;">${render}</div>`; } content += `</div>`; content += `<div style="padding: 1em;">`; content += `<p>Checking unlimited ingredient will behave as though you have an unlimited amount for any recipes that use it and determine the max-craftable based on the other ingredients. </p>`; content += `<p>Checking tally prior crafting costs will add up the costs of any uncrafted ingredients from prior recipes in addition to the current recipe. </p>`; content += `<p>Ingredient numbers shown in red indicate how much of that ingredient you're missing and cannot craft using one of the prior cheese recipes. </p>`; content += `</div>`; content += `</form>`; dialog.addToken('{*content*}', content); dialog.addToken('{*suffix*}', `<input class="jsDialogClose" type="button" value="Close">`); dialog.show(); }; document.querySelector('.headsUpDisplayBountifulBeanstalkView').append(button); } function update(data) { const itemsOwned = data.items; const cheeseRecipes = { beanster_cheese: data.beanster_recipe, lavish_beanster_cheese: data.lavish_beanster_recipe, leaping_lavish_beanster_cheese: data.leaping_lavish_beanster_recipe, royal_beanster_cheese: data.royal_beanster_recipe, }; let settings = getSettings(); let ignoreItems = Object.entries(settings.ignore) .filter(([key, value]) => value === true) .map(([key]) => key); //['Magic Essence', 'Gold', 'Beanster Cheese']; // Add room/zone loot to tooltips if (settings.general.enable_loot_estimate) { renderRoomLoot(calculatePossibleLoot(data), data); } // Add cheese crafting calculations to tooltips let renders = renderCraftable(calculateAllCheeses(cheeseRecipes, itemsOwned, ignoreItems, settings)); // Add projected noise to noise meter tooltip if (settings.general.enable_noise_estimate && data.in_castle && data.castle.is_boss_chase == false) { let el = document.querySelector('.bountifulBeanstalkCastleView__noiseMeter'); if (el) { let content = calculateCatchesUntilMaxNoise(data); renderTooltip(el, content); } } // Add settings dialog/button setupSettingsDialog(renders); // If dialog is open, update the calculation preview let dialogPreview = document.querySelector('.headsUpDisplayBountifulBeanstalkView__bean-counter-dialog-preview'); if (dialogPreview) { let content = ``; for (const render of renders) { content += `<div style="padding: 1em;">${render}</div>`; } dialogPreview.innerHTML = content; } } if (user?.environment_type == 'bountiful_beanstalk') { update(user.enviroment_atts); } eventRegistry.addEventListener( 'ajax_response', (response) => { if (response?.user?.environment_type == 'bountiful_beanstalk') { update(response.user.enviroment_atts); } }, null, false, 1 ); } if (typeof eventRegistry === 'undefined') { // Workaround for GM const script = document.createElement('script'); script.type = 'application/javascript'; script.textContent = '(' + init + ')();'; document.body.appendChild(script); } else { init(); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址