// ==UserScript== // @name [ensingm2] Steam Monster Game Script // @namespace https://github.com/ensingm2/SteamMonsterGameScript // @description A Javascript automator for the 2015 Summer Steam Monster Minigame // @version 2.16 // @match http://steamcommunity.com/minigame/towerattack* // @match http://steamcommunity.com//minigame/towerattack* // @updateURL https://raw.githubusercontent.com/ensingm2/SteamMonsterGameScript/master/automator.user.js // @downloadURL https://raw.githubusercontent.com/ensingm2/SteamMonsterGameScript/master/automator.user.js // @require https://raw.githubusercontent.com/ensingm2/SteamMonsterGameScript/master/slaveWindows.js?ver=2_06 // @grant none // ==/UserScript== // Compiled and customized by https://github.com/ensingm2 // See a (hopefully) full list of contributors over at https://github.com/ensingm2/SteamMonsterGameScript#contributors // Custom variables var debug = false; var clicksPerSecond = g_TuningData.abilities[1].max_num_clicks; var autoClickerVariance = Math.floor(clicksPerSecond / 10); clicksPerSecond -= Math.ceil(autoClickerVariance / 2); var respawnCheckFreq = 5000; var targetSwapperFreq = 1000; var abilityUseCheckFreq = 2000; var itemUseCheckFreq = 5000; var seekHealingPercent = 20; var upgradeManagerFreq = 5000; var slowRenderingFreq = 1000; var autoBuyAbilities = false; var refreshDelay = 3600000; //Page refresh every 60min var spamStatBoosters = true; // Boss Nuke Variables var useNukeOnBossAbovePercent = 40; //Controls to sync us up with other scripts var CONTROL = { speedThreshold: 2000, // use gold rain every boss round after here rainingRounds: 100, // use gold rain every x rounds disableGoldRainLevels: 200 // min level to use gold rain on }; //item use variables var useMedicsAtPercent = 40; var useMedicsAtLanePercent = 70; var useMedicsAtLanePercentAliveReq = 30; var useNukeOnSpawnerAbovePercent = 75; var useMetalDetectorOnBossBelowPercent = 30; var useStealHealthAtPercent = 15; var useRainingGoldAbovePercent = 50; var useLikeNewAboveCooldown = 14220000; // Need to save at least 14220s of cooldowns(60% of max) var useResurrectToSaveCount = 150; // Use revive to save 150 people var minutesBufferForConsumableDump = 10; var survivalTime = 10; // check how long we would survive on a level and prioritize armour if needed // You shouldn't need to ever change this, you only push to server every 1s anyway var autoClickerFreq = 1000; // Internal variables, you shouldn't need to touch these var autoRespawner, autoClicker, autoTargetSwapper, autoTargetSwapperElementUpdate, autoAbilityUser, autoUpgradeManager, fpsThrottle, spammer; var elementUpdateRate = 60000; var autoUseConsumables = true; var userElementMultipliers = [1, 1, 1, 1]; var userMaxElementMultiiplier = 1; var swapReason; var lastLootLevel = 0; var lastLootCache = []; var ABILITIES = { FIRE_WEAPON: 1, CHANGE_LANE: 2, RESPAWN: 3, CHANGE_TARGET: 4, MORALE_BOOSTER: 5, GOOD_LUCK_CHARMS: 6, MEDICS: 7, METAL_DETECTOR: 8, DECREASE_COOLDOWNS: 9, TACTICAL_NUKE: 10, CLUSTER_BOMB: 11, NAPALM: 12, RESURRECTION: 13, CRIPPLE_SPAWNER: 14, CRIPPLE_MONSTER: 15, MAX_ELEMENTAL_DAMAGE: 16, RAINING_GOLD: 17, CRIT: 18, PUMPED_UP: 19, THROW_MONEY_AT_SCREEN: 20, GOD_MODE: 21, TREASURE: 22, STEAL_HEALTH: 23, REFLECT_DAMAGE: 24, FEELING_LUCKY: 25, WORMHOLE: 26, LIKE_NEW: 27 }; function startAllAutos() { startAutoRespawner(); startAutoClicker(); startAutoTargetSwapper(); startAutoAbilityUser(); startAutoItemUser(); startAutoUpgradeManager(); } function loadSettings() { if(WebStorage.GetLocal('autoClickerEnabled') === false) toggleAutoClicker(); if(WebStorage.GetLocal('autoTargetSwapperEnabled') === false) toggleAutoTargetSwapper(); if(WebStorage.GetLocal('autoAbilityUserEnabled') === false) toggleAutoAbilityUser(); if(WebStorage.GetLocal('autoConsumableUserEnabled') === false) toggleAutoItemUser(); if(WebStorage.GetLocal('autoUpgraderEnabled') === false) toggleAutoUpgradeManager(); if(WebStorage.GetLocal('particleSpamEnabled')){ spammer = setInterval(spamNoClick, 1000 / clicksPerSecond); updateToggle("particles", false); } if (WebStorage.GetLocal('fpsThrottleEnabled') === true) toggleFPS(); if(WebStorage.GetLocal('spamStatBoostersEnabled') === false) toggleSpamStatBoosters(); if(WebStorage.GetLocal('survivalTime') !== null) { survivalTime = WebStorage.GetLocal('survivalTime'); $J("#survival_time").html(survivalTime); } } function stopAllAutos() { stopAutoClicker(); stopAutoRespawner(); stopAutoTargetSwapper(); stopAutoAbilityUser(); stopAutoItemUser(); stopAutoUpgradeManager(); } //Keep trying to start every second till success var startAttempts = 0; var startAll = setInterval(function() { if (!gameRunning()) { //Don't refresh if we're waiting on game to start if (g_Minigame.m_CurrentScene.m_rgGameData.status != 1) { //Refresh if the game still isn't running after 15s if (startAttempts > 15) location.reload(); startAttempts++; } return; } clearInterval(startAll); startAllAutos(); initGUI(); loadSettings(); //Start leaderboard (if user is running userscript) if (typeof unsafeWindow != 'undefined') initLeaderboard(); if (typeof runMaster == 'function') { //Setup for slave windows if (location.search.match(/slave/)) runSlave(); else runMaster(); } //Keep Playing while minimized - http://www.reddit.com/r/SteamMonsterGame/comments/39yng9/keep_autoclicking_after_minimizingchanging_tabs/ setInterval(function(p) { return p.Tick = eval("(" + ("" + p.Tick).replace(/document\.(hidden|webkitHidden|mozHidden|msHidden)/g, !1) + ")"), function() { p = g_Minigame.m_CurrentScene, p && document.hidden && p.Tick(); }; }(CSceneGame.prototype), 1000); setTimeout(function() { //Try to reload every 15s var reloader = setInterval(function() { //No raining gold, treasure mob, boss, or miniboss var target = getTarget(); var reload = !currentLaneHasAbility(ABILITIES.RAINING_GOLD) && target.m_data.type != 4 && target.m_data.type != 2 && target.m_data.type != 3 && target.m_data.type !== false; if (reload) { clearInterval(reloader); location.reload(); } }, 15000); }, refreshDelay); }, 1000); //Expose functions if running in userscript if (typeof unsafeWindow != 'undefined') { // Variables unsafeWindow.debug = debug; unsafeWindow.clicksPerSecond = clicksPerSecond; unsafeWindow.autoClickerVariance = autoClickerVariance; unsafeWindow.respawnCheckFreq = respawnCheckFreq; unsafeWindow.targetSwapperFreq = targetSwapperFreq; unsafeWindow.abilityUseCheckFreq = abilityUseCheckFreq; unsafeWindow.itemUseCheckFreq = itemUseCheckFreq; unsafeWindow.seekHealingPercent = seekHealingPercent; unsafeWindow.upgradeManagerFreq = upgradeManagerFreq; unsafeWindow.autoBuyAbilities = autoBuyAbilities; unsafeWindow.fpsThrottle = fpsThrottle; //item use variables unsafeWindow.useMedicsAtPercent = useMedicsAtPercent; unsafeWindow.useMedicsAtLanePercent = useMedicsAtLanePercent; unsafeWindow.useMedicsAtLanePercentAliveReq = useMedicsAtLanePercentAliveReq; unsafeWindow.useNukeOnSpawnerAbovePercent = useNukeOnSpawnerAbovePercent; unsafeWindow.useMetalDetectorOnBossBelowPercent = useMetalDetectorOnBossBelowPercent; unsafeWindow.useStealHealthAtPercent = useStealHealthAtPercent; unsafeWindow.useRainingGoldAbovePercent = useRainingGoldAbovePercent; unsafeWindow.autoUseConsumables = autoUseConsumables; unsafeWindow.useResurrectToSaveCount = useResurrectToSaveCount; unsafeWindow.spamStatBoosters = spamStatBoosters; //Slave window variables unsafeWindow.slaveWindowUICleanup = slaveWindowUICleanup; unsafeWindow.slaveWindowPeriodicRestart = slaveWindowPeriodicRestart; unsafeWindow.slaveWindowPeriodicRestartInterval = slaveWindowPeriodicRestartInterval; //Boss nuke vars unsafeWindow.useNukeOnBossAbovePercent = useNukeOnBossAbovePercent; // Functions unsafeWindow.startAutoClicker = startAutoClicker; unsafeWindow.startAutoRespawner = startAutoRespawner; unsafeWindow.startAutoTargetSwapper = startAutoTargetSwapper; unsafeWindow.startAutoAbilityUser = startAutoAbilityUser; unsafeWindow.startAutoItemUser = startAutoItemUser; unsafeWindow.startAllAutos = startAllAutos; unsafeWindow.startAutoUpgradeManager = startAutoUpgradeManager; unsafeWindow.stopAutoClicker = stopAutoClicker; unsafeWindow.stopAutoRespawner = stopAutoRespawner; unsafeWindow.stopAutoTargetSwapper = stopAutoTargetSwapper; unsafeWindow.stopAutoAbilityUser = stopAutoAbilityUser; unsafeWindow.stopAutoItemUser = stopAutoItemUser; unsafeWindow.stopAutoUpgradeManager = stopAutoUpgradeManager; unsafeWindow.stopAllAutos = stopAllAutos; unsafeWindow.disableAutoNukes = disableAutoNukes; unsafeWindow.castAbility = castAbility; unsafeWindow.hasAbility = hasAbility; unsafeWindow.abilityIsUnlocked = abilityIsUnlocked; unsafeWindow.abilityCooldown = abilityCooldown; unsafeWindow.toggleAutoClicker = toggleAutoClicker; unsafeWindow.toggleAutoTargetSwapper = toggleAutoTargetSwapper; unsafeWindow.toggleAutoAbilityUser = toggleAutoAbilityUser; unsafeWindow.toggleAutoItemUser = toggleAutoItemUser; unsafeWindow.toggleAutoUpgradeManager = toggleAutoUpgradeManager; unsafeWindow.spamNoClick = spamNoClick; unsafeWindow.toggleSpammer = toggleSpammer; unsafeWindow.getTarget = getTarget; unsafeWindow.currentLaneHasAbility = currentLaneHasAbility; unsafeWindow.laneHasAbility = laneHasAbility; unsafeWindow.getMobTypePriority = getMobTypePriority; unsafeWindow.updateStats = updateStats; //Hacky way to let people change vars using userscript before I set up getter/setter fns tomorrow var varSetter = setInterval(function() { if (debug) console.log('updating options'); // Main vars debug = unsafeWindow.debug; clicksPerSecond = unsafeWindow.clicksPerSecond; autoClickerVariance = unsafeWindow.autoClickerVariance; respawnCheckFreq = unsafeWindow.respawnCheckFreq; targetSwapperFreq = unsafeWindow.targetSwapperFreq; abilityUseCheckFreq = unsafeWindow.abilityUseCheckFreq; itemUseCheckFreq = unsafeWindow.itemUseCheckFreq; seekHealingPercent = unsafeWindow.seekHealingPercent; upgradeManagerFreq = unsafeWindow.upgradeManagerFreq; autoBuyAbilities = unsafeWindow.autoBuyAbilities; fpsThrottle = unsafeWindow.fpsThrottle; //item use variables useMedicsAtPercent = unsafeWindow.useMedicsAtPercent; useMedicsAtLanePercent = unsafeWindow.useMedicsAtLanePercent; useMedicsAtLanePercentAliveReq = unsafeWindow.useMedicsAtLanePercentAliveReq; useNukeOnSpawnerAbovePercent = unsafeWindow.useNukeOnSpawnerAbovePercent; useMetalDetectorOnBossBelowPercent = unsafeWindow.useMetalDetectorOnBossBelowPercent; useStealHealthAtPercent = unsafeWindow.useStealHealthAtPercent; useRainingGoldAbovePercent = unsafeWindow.useRainingGoldAbovePercent; useResurrectToSaveCount = unsafeWindow.useResurrectToSaveCount; spamStatBoosters = unsafeWindow.spamStatBoosters; //Boss nuke vars useNukeOnBossAbovePercent = unsafeWindow.useNukeOnBossAbovePercent; }, 5000); //Add closure 'debug' getter and setter unsafeWindow.getDebug = function() { return debug; }; unsafeWindow.setDebug = function(state) { debug = state; }; } // ================ AUTO CLICKER ================ function startAutoClicker() { if (autoClicker) { console.log("Autoclicker is already running!"); return; } autoClicker = setInterval(function() { if (!gameRunning()) return; //Vary the number of clicks by up to the autoClickerVariance variable (plus or minus) var randomVariance = Math.floor(Math.random() * autoClickerVariance * 2) - (autoClickerVariance); var clicks = clicksPerSecond + randomVariance; // Set the variable to be sent to the server g_Minigame.m_CurrentScene.m_nClicks += clicks; // Anti-anti-clicker countermeasure g_msTickRate = 1100; // Update Gold Counter var nClickGoldPct = g_Minigame.m_CurrentScene.m_rgGameData.lanes[g_Minigame.m_CurrentScene.m_rgPlayerData.current_lane].active_player_ability_gold_per_click; var enemy = getTarget(); if (enemy && nClickGoldPct > 0 && enemy.m_data.hp > 0) { var nClickGold = enemy.m_data.gold * nClickGoldPct * g_Minigame.m_CurrentScene.m_nClicks; g_Minigame.m_CurrentScene.ClientOverride('player_data', 'gold', g_Minigame.m_CurrentScene.m_rgPlayerData.gold + nClickGold); g_Minigame.m_CurrentScene.ApplyClientOverrides('player_data', true); } //Clear out the crits var numCrits = g_Minigame.m_CurrentScene.m_rgStoredCrits.length; g_Minigame.m_CurrentScene.m_rgStoredCrits = []; if (debug) { if (numCrits > 1) console.log('Clicking ' + g_Minigame.m_CurrentScene.m_nClicks + ' times this second. (' + numCrits + ' crits).'); if (numCrits == 1) console.log('Clicking ' + g_Minigame.m_CurrentScene.m_nClicks + ' times this second. (1 crit).'); else console.log('Clicking ' + g_Minigame.m_CurrentScene.m_nClicks + ' times this second.'); //Calculate Damage done var damage = g_Minigame.m_CurrentScene.CalculateDamage(g_Minigame.m_CurrentScene.m_rgPlayerTechTree.damage_per_click * userMaxElementMultiiplier * g_Minigame.m_CurrentScene.m_nClicks); var damageStr = "(unknown)"; if (damage > 1000000000) damageStr = (damage / 1000000000) + "B"; else if (damage > 1000000) damageStr = (damage / 1000000) + "M"; else if (damage > 1000) damageStr = (damage / 1000) + "K"; console.log('We did roughly ' + damageStr + ' damage in the last second.'); } }, autoClickerFreq); console.log("autoClicker has been started."); } function stopAutoClicker() { if (autoClicker) { clearInterval(autoClicker); autoClicker = null; console.log("autoClicker has been stopped."); } else console.log("No autoClicker is running to stop."); } // ================ AUTO ABILITY ITEM USE ================ function startAutoAbilityUser() { if (autoAbilityUser) { console.log("autoAbilityUser is already running!"); return; } autoAbilityUser = setInterval(function() { if (debug) console.log("Checking if it's useful to use an ability."); var percentHPRemaining = g_Minigame.CurrentScene().m_rgPlayerData.hp / g_Minigame.CurrentScene().m_rgPlayerTechTree.max_hp * 100; var target = getTarget(); var currentLane = g_Minigame.m_CurrentScene.m_rgGameData.lanes[g_Minigame.CurrentScene().m_rgPlayerData.current_lane]; var lvl = getGameLevel(); // Use any consumables that you won't run out of before round end for (var key in ABILITIES) { if (ABILITIES.hasOwnProperty(key)) { var abilityID = ABILITIES[key]; //Only check consumables if (abilityID >= ABILITIES.RESURRECTION) { var ignoreBufferPeriod = (abilityID == ABILITIES.THROW_MONEY_AT_SCREEN); if(hasTimeLeftToUseConsumable(abilityID, ignoreBufferPeriod)) castAbility(abilityID); } } } // Wormholes -- use before wasting items on lanes if (hasAbility(ABILITIES.WORMHOLE) && autoUseConsumables) { if ((lvl % CONTROL.rainingRounds === 0 && lvl > CONTROL.speedThreshold) || hasTimeLeftToUseConsumable(ABILITIES.WORMHOLE, false)) { // Use wormhole as close to the end on every 100th level (causes a 10 level jump instead of a 1) if (debug) console.log("Casting Wormhole! Allons-y!!!"); castAbility(ABILITIES.WORMHOLE); } } // Spam permanent stat boosters if set if(spamStatBoosters){ // Crit if(getAbilityItemQuantity(18)) castAbility(18); // Pumped Up if(getAbilityItemQuantity(19)) castAbility(19); } // Abilities only used on targets if (target) { var targetPercentHPRemaining = target.m_data.hp / target.m_data.max_hp * 100; var laneDPS = g_Minigame.m_CurrentScene.m_rgLaneData[g_Minigame.CurrentScene().m_rgPlayerData.current_lane].friendly_dps; var timeToTargetDeath = target.m_data.hp / laneDPS; // First priority since it can use Decrease Cooldowns //Nuke bosses after the 1000th level and not every 200th level thereafter var nukeBosses = (g_Minigame.m_CurrentScene.m_nCurrentLevel + 1 >= CONTROL.speedThreshold) && ((g_Minigame.m_CurrentScene.m_nCurrentLevel + 1) % CONTROL.rainingRounds !== 0); var isBoss = (target.m_data.type == 2 || target.m_data.type === false); // Assume false is a boss //Use Decrease Cooldowns on bosses if(isBoss) if(targetPercentHPRemaining > 75 && !currentLaneHasAbility(9) && hasAbility(9)) castAbility(9); // Abilities only used when targeting Spawners (sub lvl 1000) or nuking bosses (above level 1k) if ((target.m_data.type === 0 && g_Minigame.m_CurrentScene.m_nCurrentLevel + 1 >= CONTROL.speedThreshold) || (isBoss && nukeBosses)) { // Morale Booster, Good Luck Charm, and Decrease Cooldowns var moraleBoosterReady = hasAbility(ABILITIES.MORALE_BOOSTER); var goodLuckCharmReady = hasAbility(ABILITIES.GOOD_LUCK_CHARMS); var critReady = (hasAbility(ABILITIES.CRIT) && autoUseConsumables); // Only use items on targets that are spawners and have nearly full health if (targetPercentHPRemaining >= 90 && autoUseConsumables && (hasAbility(ABILITIES.CRIPPLE_SPAWNER) || hasAbility(ABILITIES.CRIPPLE_MONSTER))) { // Check to see if Cripple Spawner and Cripple Monster items are ready to use if (hasAbility(ABILITIES.CRIPPLE_SPAWNER)) { castAbility(ABILITIES.CRIPPLE_SPAWNER); } else if (hasAbility(ABILITIES.CRIPPLE_MONSTER)) { castAbility(ABILITIES.CRIPPLE_MONSTER); } } else if (moraleBoosterReady || critReady || goodLuckCharmReady) { // If we have both we want to combo them var moraleBoosterUnlocked = abilityIsUnlocked(ABILITIES.MORALE_BOOSTER); var goodLuckCharmUnlocked = abilityIsUnlocked(ABILITIES.GOOD_LUCK_CHARMS); // "if Moral Booster isn't unlocked or Good Luck Charm isn't unlocked, or both are ready" if ((!moraleBoosterUnlocked && !critReady) || !goodLuckCharmUnlocked || ((moraleBoosterReady || critReady) && (goodLuckCharmReady || !goodLuckCharmUnlocked))) { var currentLaneHasCooldown = currentLaneHasAbility(ABILITIES.DECREASE_COOLDOWNS); // Only use on targets that are spawners and have nearly full health if (targetPercentHPRemaining >= 70 || (currentLaneHasCooldown && targetPercentHPRemaining >= 60)) { // Combo these with Decrease Cooldowns ability // If Decreased Cooldowns will be available soon, wait if ( currentLaneHasCooldown || // If current lane already has Decreased Cooldown, or hasAbility(ABILITIES.DECREASE_COOLDOWNS) || // If we have the ability ready !abilityIsUnlocked(ABILITIES.DECREASE_COOLDOWNS) || // if we haven't unlocked the ability yet, or (abilityCooldown(ABILITIES.DECREASE_COOLDOWNS) > 60) // if cooldown > 60 ) { if (hasAbility(ABILITIES.DECREASE_COOLDOWNS) && !currentLaneHasAbility(ABILITIES.DECREASE_COOLDOWNS)) { // Other abilities won't benifit if used at the same time if (debug) console.log('Triggering Decrease Cooldown!'); castAbility(ABILITIES.DECREASE_COOLDOWNS); } else { // Use these abilities next pass //Use crit if one's available if (critReady) { if (debug) console.log("Using Crit!"); castAbility(ABILITIES.CRIT); } else if (moraleBoosterReady) { if (debug) console.log("Casting Morale Booster!"); castAbility(ABILITIES.MORALE_BOOSTER); } if (goodLuckCharmReady) { if (debug) console.log("Casting Good Luck Charm!"); castAbility(ABILITIES.GOOD_LUCK_CHARMS); } } } } } } // Tactical Nuke if (hasAbility(ABILITIES.TACTICAL_NUKE) && (targetPercentHPRemaining >= useNukeOnSpawnerAbovePercent || (target.m_data.type == 2 && targetPercentHPRemaining >= useNukeOnBossAbovePercent))) { if (debug) console.log('Nuclear launch detected.'); castAbility(ABILITIES.TACTICAL_NUKE); } // Napalm else if (target.m_data.type === 0 && hasAbility(ABILITIES.NAPALM) && targetPercentHPRemaining >= useNukeOnSpawnerAbovePercent && currentLane.enemies.length >= 4) { if (debug) console.log('Triggering napalm!'); castAbility(ABILITIES.NAPALM); } // Cluster Bomb else if (target.m_data.type === 0 && hasAbility(ABILITIES.CLUSTER_BOMB) && targetPercentHPRemaining >= useNukeOnSpawnerAbovePercent && currentLane.enemies.length >= 4) { if (debug) console.log('Triggering cluster bomb!'); castAbility(ABILITIES.CLUSTER_BOMB); } // Boss Nuke Rounds if (isBoss) { // Max Elemental Damage if (hasAbility(ABILITIES.MAX_ELEMENTAL_DAMAGE) && autoUseConsumables && targetPercentHPRemaining > useNukeOnBossAbovePercent) { if (debug) console.log('Using Max Elemental Damage on boss.'); castAbility(ABILITIES.MAX_ELEMENTAL_DAMAGE); } // Reflect Damage if (hasAbility(ABILITIES.REFLECT_DAMAGE) && autoUseConsumables && targetPercentHPRemaining > useNukeOnBossAbovePercent) { if (debug) console.log('Using Reflect Damage on boss.'); castAbility(ABILITIES.REFLECT_DAMAGE); } } } //Use cases for bosses else if (!nukeBosses && isBoss) { if(!nukeBosses) { //Raining Gold if (hasAbility(ABILITIES.RAINING_GOLD) && autoUseConsumables && targetPercentHPRemaining > useRainingGoldAbovePercent && timeToTargetDeath > 30 && lvl > CONTROL.disableGoldRainLevels && (lvl <= CONTROL.speedThreshold || lvl % CONTROL.rainingRounds === 0)) { if (debug) console.log('Using Raining Gold on boss.'); castAbility(ABILITIES.RAINING_GOLD); } } } // Metal Detector var treasureReady = hasAbility(ABILITIES.TREASURE) && autoUseConsumables; if ((isBoss || target.m_data.type == 4) && timeToTargetDeath < 10) { if (hasAbility(ABILITIES.METAL_DETECTOR) || treasureReady) { if (treasureReady) { if (debug) console.log('Using Metal Detector via Treasure.'); castAbility(ABILITIES.TREASURE); } else { if (debug) console.log('Using Metal Detector.'); castAbility(ABILITIES.METAL_DETECTOR); } } } } //Estimate average player HP Percent in lane var laneTotalPctHP = 0; var laneTotalCount = 0; for (var i = 1; i < 10; i++) { var HPGuess = ((i - 1) * 10 + 5); laneTotalPctHP += HPGuess * currentLane.player_hp_buckets[i]; laneTotalCount += currentLane.player_hp_buckets[i]; } var avgLanePercentHP = laneTotalPctHP / laneTotalCount; var percentAlive = laneTotalCount / (laneTotalCount + currentLane.player_hp_buckets[0]) * 100; // Medics if ((percentHPRemaining <= useMedicsAtPercent || (avgLanePercentHP <= useMedicsAtLanePercent && percentAlive > useMedicsAtLanePercentAliveReq)) && !g_Minigame.m_CurrentScene.m_bIsDead) { if (debug) { if (percentHPRemaining <= useMedicsAtPercent) console.log("Health below threshold. Need medics!"); if (avgLanePercentHP <= useMedicsAtLanePercent && percentAlive > useMedicsAtLanePercentAliveReq) console.log("Average lane below threshold. Need medics!"); } // Only use if there isn't already a Medics active? var pumpedUpReady = hasAbility(ABILITIES.PUMPED_UP) && autoUseConsumables; var stealHealthReady = hasAbility(ABILITIES.STEAL_HEALTH) && autoUseConsumables; if ((hasAbility(ABILITIES.MEDICS) || pumpedUpReady) && currentLaneHasAbility(ABILITIES.MEDICS) < 2) { if (pumpedUpReady) { if (debug) console.log("Using Medics via Pumped Up!"); castAbility(ABILITIES.PUMPED_UP); } else { if (debug) console.log("Using Medics!"); castAbility(ABILITIES.MEDICS); } } else if (stealHealthReady && percentHPRemaining <= useMedicsAtPercent) { if (debug) console.log("Using Steal Health in place of Medics!"); castAbility(ABILITIES.STEAL_HEALTH); } else if (debug) console.log("No medics to unleash!"); } // Resurrect if (hasAbility(ABILITIES.RESURRECTION) && autoUseConsumables) { if (currentLane.player_hp_buckets[0] >= useResurrectToSaveCount) { if (debug) console.log('Using resurrection to save ' + currentLane.player_hp_buckets[0] + ' lane allies.'); castAbility(ABILITIES.RESURRECTION); } } // Like New if (hasAbility(ABILITIES.LIKE_NEW) && autoUseConsumables) { var totalCD = 0; for (i = 5; i <= 12; i++) { if (abilityIsUnlocked(i)) totalCD += abilityCooldown(i); } if (totalCD * 1000 >= useLikeNewAboveCooldown) { if (debug) console.log('Using like new to save a total of ' + totalCD + ' seconds of cooldown.'); castAbility(ABILITIES.LIKE_NEW); } } }, abilityUseCheckFreq); console.log("autoAbilityUser has been started."); } function startAutoItemUser() { autoUseConsumables = true; console.log("Automatic use of consumables has been enabled."); } function stopAutoAbilityUser() { if (autoAbilityUser) { clearInterval(autoAbilityUser); autoAbilityUser = null; console.log("autoAbilityUser has been stopped."); } else console.log("No autoAbilityUser is running to stop."); } function stopAutoItemUser() { autoUseConsumables = false; console.log("Automatic use of consumables has been disabled."); } function disableAutoNukes() { useNukeOnSpawnerAbovePercent = 200; console.log('Automatic nukes have been disabled'); } // ================ AUTO RESPAWNER ================ function startAutoRespawner() { if (autoRespawner) { console.log("autoRespawner is already running!"); return; } autoRespawner = setInterval(function() { if (debug) console.log('Checking if the player is dead.'); // Credit to /u/kolodz for base code. http://www.reddit.com/r/SteamMonsterGame/comments/39joz2/javascript_auto_respawn/ if (g_Minigame.m_CurrentScene.m_bIsDead) { if (debug) console.log('Player is dead. Respawning.'); RespawnPlayer(); } }, respawnCheckFreq); console.log("autoRespawner has been started."); } function stopAutoRespawner() { if (autoRespawner) { clearInterval(autoRespawner); autoRespawner = null; console.log("autoRespawner has been stopped."); } else console.log("No autoRespawner is running to stop."); } // ================ AUTO TARGET SWAPPER ================ function startAutoTargetSwapper() { if (autoTargetSwapper) { console.log("autoTargetSwapper is already running!"); return; } updateUserElementMultipliers(); autoTargetSwapperElementUpdate = setInterval(updateUserElementMultipliers, elementUpdateRate); autoTargetSwapper = setInterval(function() { if (debug) console.log('Looking for a new target.'); var currentTarget = getTarget(); g_Minigame.m_CurrentScene.m_rgEnemies.each(function(potentialTarget) { if (compareMobPriority(potentialTarget, currentTarget)) currentTarget = potentialTarget; }); //Switch to that target var oldTarget = getTarget(); if (currentTarget.m_data && oldTarget.m_data && currentTarget.m_data.id != oldTarget.m_data.id) { if (debug && swapReason !== null) { console.log(swapReason); swapReason = null; } if (g_Minigame.m_CurrentScene.m_rgPlayerData.current_lane != currentTarget.m_nLane) g_Minigame.m_CurrentScene.TryChangeLane(currentTarget.m_nLane); g_Minigame.m_CurrentScene.TryChangeTarget(currentTarget.m_nID); } //Move back to lane if still targetting else if (currentTarget.m_data && g_Minigame.m_CurrentScene.m_rgPlayerData.current_lane != currentTarget.m_nLane) { g_Minigame.m_CurrentScene.TryChangeLane(currentTarget.m_nLane); } }, targetSwapperFreq); console.log("autoTargetSwapper has been started."); } function stopAutoTargetSwapper() { if (autoTargetSwapper) { clearInterval(autoTargetSwapper); autoTargetSwapper = null; console.log("autoTargetSwapper has been stopped."); } else console.log("No autoTargetSwapper is running to stop."); } // ================ AUTO UPGRADE MANAGER ================ var upgradeManagerPrefilter; if (!upgradeManagerPrefilter) { // add prefilter on first run $J.ajaxPrefilter(function() { // this will be defined by the end of the script if (upgradeManagerPrefilter !== undefined) { upgradeManagerPrefilter.apply(this, arguments); } }); } function startAutoUpgradeManager() { if (autoUpgradeManager) { console.log("UpgradeManager is already running!"); return; } /************ * SETTINGS * ************/ // Should we highlight the item we're going for next? var highlightNext = true; // Should we automatically by the next item? var autoBuyNext = true; // How many elements do you want to upgrade? If we decide to upgrade an // element, we'll try to always keep this many as close in levels as we // can, and ignore the rest. var elementalSpecializations = 1; // To estimate the overall boost in damage from upgrading an element, // we sort the elements from highest level to lowest, then multiply // each one's level by the number in the corresponding spot to get a // weighted average of their effects on your overall damage per click. // If you don't prioritize lanes that you're strongest against, this // will be [0.25, 0.25, 0.25, 0.25], giving each element an equal // scaling. However, this defaults to [0.4, 0.3, 0.2, 0.1] under the // assumption that you will spend much more time in lanes with your // strongest elements. var elementalCoefficients = [0.4, 0.3, 0.2, 0.1]; // To include passive DPS upgrades (Auto-fire, etc.) we have to scale // down their DPS boosts for an accurate comparison to clicking. This // is approximately how many clicks per second we should assume you are // consistently doing. If you have an autoclicker, this is easy to set. var clickFrequency = clicksPerSecond + Math.ceil(autoClickerVariance / 2); /*********** * GLOBALS * ***********/ var scene = g_Minigame.CurrentScene(); var waitingForUpdate = false; var next = { id: -1, cost: 0 }; var necessary = [ { id: 0, level: 1 }, // Light Armor { id: 11, level: 1 }, // Medics { id: 2, level: 10 }, // Armor Piercing Round { id: 1, level: 10 }, // Auto-fire Cannon ]; var gAbilities = [ 11, // Medics 13, // Good Luck Charms 16, // Tactical Nuke 18, // Napalm 17, // Cluster Bomb 14, // Metal Detector 15, // Decrease Cooldowns 12, // Morale Booster ]; var gLuckyShot = 7; var gBossLoot = 19; var gElementalUpgrades = [3, 4, 5, 6]; // Fire, Water, Earth, Air var gHealthUpgrades = []; var gAutoUpgrades = []; var gDamageUpgrades = []; Object.keys(scene.m_rgTuningData.upgrades) .sort(function(a, b) { return a - b; }) // why is default sort string comparison .forEach(function(id) { var upgrade = scene.m_rgTuningData.upgrades[id]; switch (upgrade.type) { case 0: gHealthUpgrades.push(+id); break; case 1: gAutoUpgrades.push(+id); break; case 2: gDamageUpgrades.push(+id); break; } }); /*********** * HELPERS * ***********/ var getElementals = (function() { var cache = false; return function(refresh) { if (!cache || refresh) { cache = gElementalUpgrades .map(function(id) { return { id: id, level: scene.GetUpgradeLevel(id) }; }) .sort(function(a, b) { return b.level - a.level; }); } return cache; }; })(); var getElementalCoefficient = function(elementals) { elementals = elementals || getElementals(); return scene.m_rgTuningData.upgrades[4].multiplier * elementals.reduce(function(sum, elemental, i) { return sum + elemental.level * elementalCoefficients[i]; }, 0); }; var canUpgrade = function(id) { // do we even have the upgrade? if (!scene.bHaveUpgrade(id)) return false; // does it have a required upgrade? var data = scene.m_rgTuningData.upgrades[id]; var required = data.required_upgrade; if (required !== undefined) { // is it at the required level to unlock? var level = data.required_upgrade_level || 1; return (level <= scene.GetUpgradeLevel(required)); } // otherwise, we're good to go! return true; }; var calculateUpgradeTree = function(id, level) { var data = scene.m_rgTuningData.upgrades[id]; var boost = 0; var cost = 0; var parent; var cur_level = scene.GetUpgradeLevel(id); if (level === undefined) level = cur_level + 1; // for each missing level, add boost and cost for (var level_diff = level - cur_level; level_diff > 0; level_diff--) { boost += data.multiplier; cost += data.cost * Math.pow(data.cost_exponential_base, level - level_diff); } // recurse for required upgrades var required = data.required_upgrade; if (required !== undefined) { var parents = calculateUpgradeTree(required, data.required_upgrade_level || 1); if (parents.cost > 0) { boost += parents.boost; cost += parents.cost; parent = parents.required || required; } } return { boost: boost, cost: cost, required: parent }; }; var necessaryUpgrade = function() { var best = { id: -1, cost: 0 }; var wanted, id; while (necessary.length > 0) { wanted = necessary[0]; id = wanted.id; if (scene.GetUpgradeLevel(id) < wanted.level) { best = { id: id, cost: scene.GetUpgradeCost(id) }; break; } necessary.shift(); } return best; }; var nextAbilityUpgrade = function() { var best = { id: -1, cost: 0 }; if (autoBuyAbilities) { gAbilities.some(function(id) { if (canUpgrade(id) && scene.GetUpgradeLevel(id) < 1) { best = { id: id, cost: scene.GetUpgradeCost(id) }; return true; } }); } return best; }; var bestHealthUpgrade = function() { var best = { id: -1, cost: 0, hpg: 0 }; var result, hpg; gHealthUpgrades.forEach(function(id) { result = calculateUpgradeTree(id); hpg = scene.m_rgTuningData.player.hp * result.boost / result.cost; if (hpg >= best.hpg) { if (result.required !== undefined) id = result.required; cost = scene.GetUpgradeCost(id); if (cost <= scene.m_rgPlayerData.gold || (best.cost === 0 || cost < best.cost)) { // TODO best = { id: id, cost: cost, hpg: hpg }; } } }); return best; }; var bestDamageUpgrade = function() { var best = { id: -1, cost: 0, dpg: 0 }; var result, data, cost, dpg, boost; var dpc = scene.m_rgPlayerTechTree.damage_per_click; var base_dpc = scene.m_rgTuningData.player.damage_per_click; var critmult = scene.m_rgPlayerTechTree.damage_multiplier_crit; var unusedCritChance = getAbilityItemQuantity(18) * 0.01; // Take unused Crit items into account, since they will probably be applied soon var critrate = Math.min(scene.m_rgPlayerTechTree.crit_percentage + unusedCritChance, 1); var elementals = getElementals(); var elementalCoefficient = getElementalCoefficient(elementals); // check auto damage upgrades gAutoUpgrades.forEach(function(id) { result = calculateUpgradeTree(id); dpg = (scene.m_rgPlayerTechTree.base_dps * result.boost / clickFrequency) / result.cost; if (dpg >= best.dpg) { if (result.required !== undefined) id = result.required; best = { id: id, cost: scene.GetUpgradeCost(id), dpg: dpg }; } }); // check Lucky Shot if (canUpgrade(gLuckyShot)) { // lazy check because prereq is necessary upgrade data = scene.m_rgTuningData.upgrades[gLuckyShot]; boost = dpc * critrate * data.multiplier; cost = scene.GetUpgradeCost(gLuckyShot); dpg = boost / cost; if (dpg >= best.dpg) { best = { id: gLuckyShot, cost: cost, dpg: dpg }; } } // check click damage upgrades gDamageUpgrades.forEach(function(id) { result = calculateUpgradeTree(id); dpg = base_dpc * result.boost * (critrate * critmult + (1 - critrate) * elementalCoefficient) / result.cost; if (dpg >= best.dpg) { if (result.required !== undefined) id = result.required; best = { id: id, cost: scene.GetUpgradeCost(id), dpg: dpg }; } }); // check elementals data = scene.m_rgTuningData.upgrades[4]; var elementalLevels = elementals.reduce(function(sum, elemental) { return sum + elemental.level; }, 1); cost = data.cost * Math.pow(data.cost_exponential_base, elementalLevels); // - make new elementals array for testing var testElementals = elementals.map(function(elemental) { return { level: elemental.level }; }); var upgradeLevel = testElementals[elementalSpecializations - 1].level; testElementals[elementalSpecializations - 1].level++; if (elementalSpecializations > 1) { // swap positions if upgraded elemental now has bigger level than (originally) next highest var prevElem = testElementals[elementalSpecializations - 2].level; if (prevElem <= upgradeLevel) { testElementals[elementalSpecializations - 2].level = upgradeLevel + 1; testElementals[elementalSpecializations - 1].level = prevElem; } } // - calculate stats boost = dpc * (1 - critrate) * (getElementalCoefficient(testElementals) - elementalCoefficient); dpg = boost / cost; if (dpg > best.dpg) { // give base damage boosters priority // find all elements at upgradeLevel and randomly pick one var match = elementals.filter(function(elemental) { return elemental.level == upgradeLevel; }); match = match[Math.floor(Math.random() * match.length)].id; best = { id: match, cost: cost, dpg: dpg }; } // Boss Loot Upgrade var lootRate = g_Minigame.m_CurrentScene.m_rgPlayerTechTree.boss_loot_drop_percentage * 100; var levelTime = (g_Minigame.m_CurrentScene.m_rgGameData.timestamp - g_Minigame.m_CurrentScene.m_rgGameData.timestamp_game_start) / g_Minigame.m_CurrentScene.m_rgGameData.level; var lootCost = scene.GetUpgradeCost(gBossLoot); var lootEfficient = false; // Case1: drop rate < 50%: if cost is <10% that of the 'best upgrade', and room isn't moving slowly. // Case2: drop rate >=50%: if cost is <1% that of the 'best upgrade', and room isn't moving slowly. if ( (lootRate < 50 && lootCost < best.cost * 0.1 && lootCost < best.cost * 10 / levelTime) || (lootRate < 100 && lootCost < best.cost * 0.01 && lootCost < best.cost / levelTime) ) { lootEfficient = true; } if (canUpgrade(gBossLoot) && lootEfficient) { best = { id: gBossLoot, cost: lootCost, dpg: gBossLoot.cost }; } return best; }; var timeToDie = (function() { var cache = false; return function(refresh) { if (cache === false || refresh) { var maxHp = scene.m_rgPlayerTechTree.max_hp; var enemyDps = scene.m_rgGameData.lanes.reduce(function(max, lane) { return Math.max(max, lane.enemies.reduce(function(sum, enemy) { return sum + enemy.dps; }, 0)); }, 0); cache = maxHp / (enemyDps || scene.m_rgGameData.level * 4); } return cache; }; })(); var updateNext = function() { next = necessaryUpgrade(); if (next.id === -1) { if (timeToDie() < survivalTime) { next = bestHealthUpgrade(); } else { var damage = bestDamageUpgrade(); var ability = nextAbilityUpgrade(); next = (damage.cost < ability.cost || ability.id === -1) ? damage : ability; } } if (next.id !== -1) { if (highlightNext) { $J('.next_upgrade').removeClass('next_upgrade'); $J(document.getElementById('upgr_' + next.id)).addClass('next_upgrade'); } if (debug) { console.log( 'next buy:', scene.m_rgTuningData.upgrades[next.id].name, '(' + FormatNumberForDisplay(next.cost) + ')' ); } } }; var hook = function(base, method, func) { var original = method + '_upgradeManager'; if (!base.prototype[original]) base.prototype[original] = base.prototype[method]; base.prototype[method] = function() { this[original].apply(this, arguments); func.apply(this, arguments); }; }; /******** * MAIN * ********/ // ---------- JS hooks ---------- hook(CSceneGame, 'TryUpgrade', function() { // if it's a valid try, we should reevaluate after the update if (this.m_bUpgradesBusy) { if (highlightNext) $J(document.body).addClass('upgrade_waiting'); next.id = -1; } }); hook(CSceneGame, 'ChangeLevel', function() { // recalculate enemy DPS to see if we can survive this level if (timeToDie(true) < survivalTime) updateNext(); }); upgradeManagerPrefilter = function(opts, origOpts, xhr) { if (/ChooseUpgrade/.test(opts.url)) { xhr .success(function() { // wait as short a delay as possible // then we re-run to figure out the next item to queue window.setTimeout(upgradeManager, 0); }) .fail(function() { // we're desynced. wait til data refresh // m_bUpgradesBusy was not set to false scene.m_bNeedTechTree = true; waitingForUpdate = true; }); } else if (/GetPlayerData/.test(opts.url)) { if (waitingForUpdate) { xhr.success(function(result) { var message = g_Server.m_protobuf_GetPlayerDataResponse.decode(result).toRaw(true, true); if (message.tech_tree) { // done waiting! no longer busy waitingForUpdate = false; scene.m_bUpgradesBusy = false; window.setTimeout(upgradeManager, 0); } }); } } }; // ---------- CSS ---------- $J(document.body).removeClass('upgrade_waiting'); $J('.next_upgrade').removeClass('next_upgrade'); if (highlightNext) { var cssPrefix = function(property, value) { return '-webkit-' + property + ': ' + value + '; ' + property + ': ' + value + ';'; }; var css = '.next_upgrade { ' + cssPrefix('filter', 'brightness(1.5) contrast(2)') + ' }\n' + '.next_upgrade.cantafford { ' + cssPrefix('filter', 'contrast(1.3)') + ' }\n' + '.next_upgrade .info .name, .next_upgrade.element_upgrade .level { color: #e1b21e; }\n' + '#upgrades .next_upgrade .link { ' + cssPrefix('filter', 'brightness(0.8) hue-rotate(120deg)') + ' }\n' + '#elements .next_upgrade .link { ' + cssPrefix('filter', 'hue-rotate(120deg)') + ' }\n' + '.next_upgrade .cost { ' + cssPrefix('filter', 'hue-rotate(-120deg)') + ' }\n' + '.upgrade_waiting .next_upgrade { ' + cssPrefix('animation', 'blink 1s infinite alternate') + ' }\n' + '@-webkit-keyframes blink { to { opacity: 0.5; } }\n' + '@keyframes blink { to { opacity: 0.5; } }'; var style = document.getElementById('upgradeManagerStyles'); if (!style) { style = document.createElement('style'); $J(style).attr('id', 'upgradeManagerStyles').appendTo('head'); } $J(style).html(css); } // ---------- Timer ---------- function upgradeManager() { if (debug) console.log('Checking for worthwhile upgrades'); scene = g_Minigame.CurrentScene(); // tried to buy upgrade and waiting for reply; don't do anything if (scene.m_bUpgradesBusy) return; // no item queued; refresh stats and queue next item if (next.id === -1) { if (highlightNext) $J(document.body).removeClass('upgrade_waiting'); getElementals(true); timeToDie(true); updateNext(); } // item queued; buy if we can afford it if (next.id !== -1 && autoBuyNext) { if (next.cost <= scene.m_rgPlayerData.gold) { var link = $J('.link', document.getElementById('upgr_' + next.id)).get(0); if (link) { scene.TryUpgrade(link); } else { console.error('failed to find upgrade'); } } } } autoUpgradeManager = setInterval(upgradeManager, upgradeManagerFreq); console.log("autoUpgradeManager has been started."); } function stopAutoUpgradeManager() { if (autoUpgradeManager) { clearInterval(autoUpgradeManager); autoUpgradeManager = null; //Remove hooks function removeHook(base, method) { base.prototype[method] = (base.prototype[method + '_upgradeManager'] || base.prototype[method]); } removeHook(CSceneGame, 'TryUpgrade'); removeHook(CSceneGame, 'ChangeLevel'); //Clear the visual $J(document.body).removeClass('upgrade_waiting'); $J('.next_upgrade').removeClass('next_upgrade'); console.log("autoUpgradeManager has been stopped."); } else console.log("No autoUpgradeManager is running to stop."); } // ================ SLOW RENDERING ================ var gameOldRenderer = function() {}; function startFPSThrottle(){ if (fpsThrottle) { console.log("fpsThrottling is already running!"); return; } gameOldRenderer = g_Minigame.Render; var ticker = PIXI.ticker.shared; ticker.autoStart = false; ticker.stop(); g_Minigame.Render = function() {}; // Visual Display for peoples $J("#uicontainer").append('