├── README.md └── upgrademanager.user.js /README.md: -------------------------------------------------------------------------------- 1 | # SteamMonsterAutoUpgradeManager 2 | An automatic upgrade manager for the 2015 Summer Steam Monster Minigame 3 | 4 | The core of this script is unashamedly ripped from [ensingm2's](https://github.com/ensingm2) [SteamMonsterGameScript](https://github.com/ensingm2/SteamMonsterGameScript), which got the code from [Meishuu's](https://www.reddit.com/user/Meishuu) [Upgrade Auto-buyer](https://www.reddit.com/r/SteamMonsterGame/comments/39o5fu/upgrade_autobuyer/). 5 | -------------------------------------------------------------------------------- /upgrademanager.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Monster Minigame AutoUpgrade 2.0 3 | // @namespace https://github.com/Pawsed/SteamMonsterAutoUpgradeManager/ 4 | // @version 2.0 5 | // @description An automatic upgrade manager for the 2015 Summer Steam Monster Minigame 6 | // @match *://steamcommunity.com/minigame/towerattack* 7 | // @match *://steamcommunity.com//minigame/towerattack* 8 | // @updateURL https://github.com/Pawsed/SteamMonsterAutoUpgradeManager/raw/master/upgrademanager.user.js 9 | // @downloadURL https://github.com/Pawsed/SteamMonsterAutoUpgradeManager/raw/master/upgrademanager.user.js 10 | // @grant none 11 | // ==/UserScript== 12 | // Automatically buy miscellaneous abilities? Medics is considered 13 | // essential and will be bought despite this setting. 14 | var autoBuyAbilities = false; 15 | 16 | // How many elements do you want to upgrade? If we decide to upgrade an 17 | // element, we'll try to always keep this many as close in levels as we 18 | // can, and ignore the rest. 19 | var elementalSpecializations = 0; 20 | 21 | // How frequent do we check to see if we can upgrade? 22 | var upgradeManagerPeriod = 500; 23 | var busyCount = 0; 24 | 25 | var survivalTime = 10; 26 | var autoUpgradeManager, upgradeManagerPrefilter; 27 | 28 | function startAutoUpgradeManager() { 29 | if (autoUpgradeManager) { 30 | console.log("UpgradeManager is already running!"); 31 | return; 32 | } 33 | 34 | /************ 35 | * SETTINGS * 36 | ************/ 37 | 38 | // Should we highlight the item we're going for next? 39 | var highlightNext = true; 40 | 41 | // Should we automatically by the next item? 42 | var autoBuyNext = true; 43 | 44 | // To estimate the overall boost in damage from upgrading an element, 45 | // we sort the elements from highest level to lowest, then multiply 46 | // each one's level by the number in the corresponding spot to get a 47 | // weighted average of their effects on your overall damage per click. 48 | // If you don't prioritize lanes that you're strongest against, this 49 | // will be [0.25, 0.25, 0.25, 0.25], giving each element an equal 50 | // scaling. However, this defaults to [0.4, 0.3, 0.2, 0.1] under the 51 | // assumption that you will spend much more time in lanes with your 52 | // strongest elements. 53 | var elementalCoefficients = [0.4, 0.3, 0.2, 0.1]; 54 | 55 | // To include passive DPS upgrades (Auto-fire, etc.) we have to scale 56 | // down their DPS boosts for an accurate comparison to clicking. This 57 | // is approximately how many clicks per second we should assume you are 58 | // consistently doing. If you have an autoclicker, this is easy to set. 59 | 60 | /*********** 61 | * GLOBALS * 62 | ***********/ 63 | var scene = g_Minigame.CurrentScene(); 64 | var waitingForUpdate = false; 65 | 66 | var next = { 67 | id: -1, 68 | cost: 0 69 | }; 70 | 71 | var necessary = [ 72 | { 73 | id: 11, 74 | level: 1 75 | } // Medics 76 | ]; 77 | 78 | var gAbilities = [ 79 | 11, // Medics 80 | 13, // Good Luck Charms 81 | 16, // Tactical Nuke 82 | 18, // Napalm 83 | 17, // Cluster Bomb 84 | 14, // Metal Detector 85 | 15, // Decrease Cooldowns 86 | 12 // Morale Booster 87 | ]; 88 | 89 | var gLuckyShot = 7; 90 | var gElementalUpgrades = [3, 4, 5, 6]; // Fire, Water, Earth, Air 91 | 92 | var gHealthUpgrades = []; 93 | var gAutoUpgrades = []; 94 | var gDamageUpgrades = []; 95 | 96 | Object.keys(scene.m_rgTuningData.upgrades) 97 | .sort(function(a, b) { 98 | return a - b; 99 | }) // why is default sort string comparison 100 | .forEach(function(id) { 101 | var upgrade = scene.m_rgTuningData.upgrades[id]; 102 | switch (upgrade.type) { 103 | case 0: 104 | gHealthUpgrades.push(+id); 105 | break; 106 | case 1: 107 | gAutoUpgrades.push(+id); 108 | break; 109 | case 2: 110 | gDamageUpgrades.push(+id); 111 | break; 112 | } 113 | }); 114 | 115 | /*********** 116 | * HELPERS * 117 | ***********/ 118 | var getElementals = (function() { 119 | var cache = false; 120 | return function(refresh) { 121 | if (!cache || refresh) { 122 | cache = gElementalUpgrades 123 | .map(function(id) { 124 | return { 125 | id: id, 126 | level: scene.GetUpgradeLevel(id) 127 | }; 128 | }) 129 | .sort(function(a, b) { 130 | return b.level - a.level; 131 | }); 132 | } 133 | return cache; 134 | }; 135 | })(); 136 | 137 | var getElementalCoefficient = function(elementals) { 138 | elementals = elementals || getElementals(); 139 | return scene.m_rgTuningData.upgrades[4].multiplier * 140 | elementals.reduce(function(sum, elemental, i) { 141 | return sum + elemental.level * elementalCoefficients[i]; 142 | }, 0); 143 | }; 144 | 145 | var canUpgrade = function(id) { 146 | // do we even have the upgrade? 147 | if (!scene.bHaveUpgrade(id)) return false; 148 | 149 | // does it have a required upgrade? 150 | var data = scene.m_rgTuningData.upgrades[id]; 151 | var required = data.required_upgrade; 152 | if (required !== undefined) { 153 | // is it at the required level to unlock? 154 | var level = data.required_upgrade_level || 1; 155 | return (level <= scene.GetUpgradeLevel(required)); 156 | } 157 | 158 | // otherwise, we're good to go! 159 | return true; 160 | }; 161 | 162 | var calculateUpgradeTree = function(id, level) { 163 | var data = scene.m_rgTuningData.upgrades[id]; 164 | var boost = 0; 165 | var cost = 0; 166 | var parent; 167 | 168 | var cur_level = scene.GetUpgradeLevel(id); 169 | if (level === undefined) level = cur_level + 1; 170 | 171 | // for each missing level, add boost and cost 172 | for (var level_diff = level - cur_level; level_diff > 0; level_diff--) { 173 | boost += data.multiplier; 174 | cost += data.cost * Math.pow(data.cost_exponential_base, level - level_diff); 175 | } 176 | 177 | // recurse for required upgrades` 178 | var required = data.required_upgrade; 179 | if (required !== undefined) { 180 | var parents = calculateUpgradeTree(required, data.required_upgrade_level || 1); 181 | if (parents.cost > 0) { 182 | boost += parents.boost; 183 | cost += parents.cost; 184 | parent = parents.required || required; 185 | } 186 | } 187 | 188 | return { 189 | boost: boost, 190 | cost: cost, 191 | required: parent 192 | }; 193 | }; 194 | 195 | var necessaryUpgrade = function() { 196 | var best = { 197 | id: -1, 198 | cost: 0 199 | }; 200 | var wanted, id; 201 | while (necessary.length > 0) { 202 | wanted = necessary[0]; 203 | id = wanted.id; 204 | if (scene.GetUpgradeLevel(id) < wanted.level) { 205 | best = { 206 | id: id, 207 | cost: scene.GetUpgradeCost(id) 208 | }; 209 | break; 210 | } 211 | necessary.shift(); 212 | } 213 | return best; 214 | }; 215 | 216 | var nextAbilityUpgrade = function() { 217 | var best = { 218 | id: -1, 219 | cost: 0 220 | }; 221 | if (autoBuyAbilities) { 222 | gAbilities.some(function(id) { 223 | if (canUpgrade(id) && scene.GetUpgradeLevel(id) < 1) { 224 | best = { 225 | id: id, 226 | cost: scene.GetUpgradeCost(id) 227 | }; 228 | return true; 229 | } 230 | }); 231 | } 232 | return best; 233 | }; 234 | 235 | var bestHealthUpgrade = function() { 236 | var best = { 237 | id: -1, 238 | cost: 0, 239 | hpg: 0 240 | }; 241 | var result, hpg; 242 | gHealthUpgrades.forEach(function(id) { 243 | result = calculateUpgradeTree(id); 244 | hpg = scene.m_rgTuningData.player.hp * result.boost / result.cost; 245 | if (hpg >= best.hpg) { 246 | if (result.required !== undefined) id = result.required; 247 | cost = scene.GetUpgradeCost(id); 248 | if (cost <= scene.m_rgPlayerData.gold || (best.cost === 0 || cost < best.cost)) { // TODO 249 | best = { 250 | id: id, 251 | cost: cost, 252 | hpg: hpg 253 | }; 254 | } 255 | } 256 | }); 257 | return best; 258 | }; 259 | 260 | var bestDamageUpgrade = function() { 261 | var best = { 262 | id: -1, 263 | cost: 0, 264 | dpg: 0 265 | }; 266 | var result, data, cost, dpg, boost; 267 | 268 | var dpc = scene.m_rgPlayerTechTree.damage_per_click; 269 | var base_dpc = scene.m_rgTuningData.player.damage_per_click; 270 | var critmult = scene.m_rgPlayerTechTree.damage_multiplier_crit; 271 | var unusedCritChance = getAbilityItemQuantity(18) * 0.01; // Take unused Crit items into account, since they will probably be applied soon 272 | var critrate = Math.min(scene.m_rgPlayerTechTree.crit_percentage + unusedCritChance, 1); 273 | var elementals = getElementals(); 274 | var elementalCoefficient = getElementalCoefficient(elementals); 275 | 276 | // check auto damage upgrades 277 | gAutoUpgrades.forEach(function(id) { 278 | result = calculateUpgradeTree(id); 279 | dpg = 0; 280 | /*if (dpg >= best.dpg) { 281 | if (result.required !== undefined) id = result.required; 282 | best = { 283 | id: id, 284 | cost: scene.GetUpgradeCost(id), 285 | dpg: dpg 286 | }; 287 | }*/ 288 | }); 289 | 290 | // check Lucky Shot 291 | if (canUpgrade(gLuckyShot)) { // lazy check because prereq is necessary upgrade 292 | data = scene.m_rgTuningData.upgrades[gLuckyShot]; 293 | boost = dpc * critrate * data.multiplier; 294 | cost = scene.GetUpgradeCost(gLuckyShot); 295 | dpg = boost / cost; 296 | if (dpg >= best.dpg) { 297 | best = { 298 | id: gLuckyShot, 299 | cost: cost, 300 | dpg: dpg 301 | }; 302 | } 303 | } 304 | 305 | // check click damage upgrades 306 | gDamageUpgrades.forEach(function(id) { 307 | result = calculateUpgradeTree(id); 308 | dpg = base_dpc * result.boost * (critrate * critmult + (1 - critrate) * elementalCoefficient) / result.cost; 309 | if (dpg >= best.dpg) { 310 | if (result.required !== undefined) id = result.required; 311 | best = { 312 | id: id, 313 | cost: scene.GetUpgradeCost(id), 314 | dpg: dpg 315 | }; 316 | } 317 | }); 318 | 319 | // check elementals 320 | data = scene.m_rgTuningData.upgrades[4]; 321 | var elementalLevels = elementals.reduce(function(sum, elemental) { 322 | return sum + elemental.level; 323 | }, 1); 324 | cost = data.cost * Math.pow(data.cost_exponential_base, elementalLevels); 325 | 326 | // - make new elementals array for testing 327 | var testElementals = elementals.map(function(elemental) { 328 | return { 329 | level: elemental.level 330 | }; 331 | }); 332 | if (elementalSpecializations != 0) { 333 | var upgradeLevel = testElementals[elementalSpecializations - 1].level; 334 | testElementals[elementalSpecializations - 1].level++; 335 | if (elementalSpecializations > 1) { 336 | // swap positions if upgraded elemental now has bigger level than (originally) next highest 337 | var prevElem = testElementals[elementalSpecializations - 2].level; 338 | if (prevElem <= upgradeLevel) { 339 | testElementals[elementalSpecializations - 2].level = upgradeLevel + 1; 340 | testElementals[elementalSpecializations - 1].level = prevElem; 341 | } 342 | } 343 | } 344 | 345 | // - calculate stats 346 | boost = dpc * (1 - critrate) * (getElementalCoefficient(testElementals) - elementalCoefficient); 347 | dpg = boost / cost; 348 | if (dpg > best.dpg) { // give base damage boosters priority 349 | // find all elements at upgradeLevel and randomly pick one 350 | var match = elementals.filter(function(elemental) { 351 | return elemental.level == upgradeLevel; 352 | }); 353 | match = match[Math.floor(Math.random() * match.length)].id; 354 | best = { 355 | id: match, 356 | cost: cost, 357 | dpg: dpg 358 | }; 359 | } 360 | 361 | return best; 362 | }; 363 | 364 | var timeToDie = (function() { 365 | var cache = false; 366 | return function(refresh) { 367 | if (cache === false || refresh) { 368 | var maxHp = scene.m_rgPlayerTechTree.max_hp; 369 | var enemyDps = scene.m_rgGameData.lanes.reduce(function(max, lane) { 370 | return Math.max(max, lane.enemies.reduce(function(sum, enemy) { 371 | return sum + enemy.dps; 372 | }, 0)); 373 | }, 0); 374 | cache = maxHp / (enemyDps || scene.m_rgGameData.level * 4); 375 | } 376 | return cache; 377 | }; 378 | })(); 379 | 380 | var updateNext = function() { 381 | next = necessaryUpgrade(); 382 | if (next.id === -1) { 383 | if (timeToDie() < survivalTime) { 384 | next = bestHealthUpgrade(); 385 | } 386 | 387 | else { 388 | var damage = bestDamageUpgrade(); 389 | var ability = nextAbilityUpgrade(); 390 | next = (damage.cost < ability.cost || ability.id === -1) ? damage : ability; 391 | } 392 | } 393 | if (next.id !== -1) { 394 | if (highlightNext) { 395 | $J('.next_upgrade').removeClass('next_upgrade'); 396 | $J(document.getElementById('upgr_' + next.id)).addClass('next_upgrade'); 397 | } 398 | console.log( 399 | '%cnext buy: %c%s %c(%s)', 'font-weight:bold', 'color:red', 400 | scene.m_rgTuningData.upgrades[next.id].name, 'color:red;font-style:italic', 401 | FormatNumberForDisplay(next.cost) 402 | ); 403 | } 404 | }; 405 | 406 | var hook = function(base, method, func) { 407 | var original = method + '_upgradeManager'; 408 | if (!base.prototype[original]) base.prototype[original] = base.prototype[method]; 409 | base.prototype[method] = function() { 410 | this[original].apply(this, arguments); 411 | func.apply(this, arguments); 412 | }; 413 | }; 414 | 415 | /******** 416 | * MAIN * 417 | ********/ 418 | // ---------- JS hooks ---------- 419 | hook(CSceneGame, 'TryUpgrade', function() { 420 | // if it's a valid try, we should reevaluate after the update 421 | if (this.m_bUpgradesBusy) { 422 | if (highlightNext) $J(document.body).addClass('upgrade_waiting'); 423 | next.id = -1; 424 | } 425 | }); 426 | 427 | hook(CSceneGame, 'ChangeLevel', function() { 428 | // recalculate enemy DPS to see if we can survive this level 429 | if (timeToDie(true) < survivalTime) updateNext(); 430 | }); 431 | 432 | upgradeManagerPrefilter = function(opts, origOpts, xhr) { 433 | if (/ChooseUpgrade/.test(opts.url)) { 434 | xhr 435 | .success(function() { 436 | // wait as short a delay as possible 437 | // then we re-run to figure out the next item to queue 438 | window.setTimeout(upgradeManager, 0); 439 | }) 440 | .fail(function() { 441 | // we're desynced. wait til data refresh 442 | // m_bUpgradesBusy was not set to false 443 | scene.m_bNeedTechTree = true; 444 | waitingForUpdate = true; 445 | }); 446 | } else if (/GetPlayerData/.test(opts.url)) { 447 | if (waitingForUpdate) { 448 | xhr.success(function(result) { 449 | var message = g_Server.m_protobuf_GetPlayerDataResponse.decode(result).toRaw(true, true); 450 | if (message.tech_tree) { 451 | // done waiting! no longer busy 452 | waitingForUpdate = false; 453 | scene.m_bUpgradesBusy = false; 454 | window.setTimeout(upgradeManager, 0); 455 | } 456 | }); 457 | } 458 | } 459 | }; 460 | 461 | // ---------- CSS ---------- 462 | $J(document.body).removeClass('upgrade_waiting'); 463 | $J('.next_upgrade').removeClass('next_upgrade'); 464 | if (highlightNext) { 465 | var cssPrefix = function(property, value) { 466 | return '-webkit-' + property + ': ' + value + '; ' + property + ': ' + value + ';'; 467 | }; 468 | 469 | var css = 470 | '.next_upgrade { ' + cssPrefix('filter', 'brightness(1.5) contrast(2)') + ' }\n' + 471 | '.next_upgrade.cantafford { ' + cssPrefix('filter', 'contrast(1.3)') + ' }\n' + 472 | '.next_upgrade .info .name, .next_upgrade.element_upgrade .level { color: #e1b21e; }\n' + 473 | '#upgrades .next_upgrade .link { ' + cssPrefix('filter', 'brightness(0.8) hue-rotate(120deg)') + ' }\n' + 474 | '#elements .next_upgrade .link { ' + cssPrefix('filter', 'hue-rotate(120deg)') + ' }\n' + 475 | '.next_upgrade .cost { ' + cssPrefix('filter', 'hue-rotate(-120deg)') + ' }\n' + 476 | '.upgrade_waiting .next_upgrade { ' + cssPrefix('animation', 'blink 1s infinite alternate') + ' }\n' + 477 | '@-webkit-keyframes blink { to { opacity: 0.5; } }\n' + 478 | '@keyframes blink { to { opacity: 0.5; } }'; 479 | 480 | var style = document.getElementById('upgradeManagerStyles'); 481 | if (!style) { 482 | style = document.createElement('style'); 483 | $J(style).attr('id', 'upgradeManagerStyles').appendTo('head'); 484 | } 485 | $J(style).html(css); 486 | } 487 | 488 | // ---------- Timer ---------- 489 | function upgradeManager() { 490 | scene = g_Minigame.CurrentScene(); 491 | 492 | // tried to buy upgrade and waiting for reply; don't do anything 493 | if (scene.m_bUpgradesBusy) { 494 | busyCount++; 495 | if(busyCount < 5){ 496 | return; 497 | } 498 | m_bUpgradesBusy = false; 499 | busyCount = 0; 500 | } 501 | 502 | // no item queued; refresh stats and queue next item 503 | if (next.id === -1) { 504 | if (highlightNext) $J(document.body).removeClass('upgrade_waiting'); 505 | getElementals(true); 506 | timeToDie(true); 507 | updateNext(); 508 | } 509 | 510 | // item queued; buy if we can afford it 511 | if (next.id !== -1 && autoBuyNext) { 512 | if (next.cost <= scene.m_rgPlayerData.gold) { 513 | var link = $J('.link', document.getElementById('upgr_' + next.id)).get(0); 514 | if (link) { 515 | scene.TryUpgrade(link); 516 | } else { 517 | console.error('failed to find upgrade'); 518 | } 519 | } 520 | } 521 | } 522 | 523 | autoUpgradeManager = setInterval(upgradeManager, upgradeManagerPeriod); 524 | 525 | console.log("autoUpgradeManager has been started."); 526 | } 527 | 528 | function getAbilityItemQuantity(abilityID) { 529 | for (var i = 0; i < g_Minigame.CurrentScene().m_rgPlayerTechTree.ability_items.length; ++i) { 530 | var abilityItem = g_Minigame.CurrentScene().m_rgPlayerTechTree.ability_items[i]; 531 | 532 | if (abilityItem.ability == abilityID) 533 | return abilityItem.quantity; 534 | } 535 | 536 | return 0; 537 | } 538 | 539 | function gameRunning() { 540 | return g_Minigame && g_Minigame.CurrentScene() && g_Minigame.CurrentScene().m_bRunning; 541 | } 542 | 543 | function tryStart() { 544 | if (!gameRunning()) { 545 | setTimeout(tryStart, 1000); 546 | } else { 547 | if (!upgradeManagerPrefilter) { 548 | // add prefilter on first run 549 | $J.ajaxPrefilter(function() { 550 | // this will be defined by the end of the script 551 | upgradeManagerPrefilter.apply(this, arguments); 552 | }); 553 | } 554 | startAutoUpgradeManager(); 555 | /*upgradeManager = startAutoUpgradeManager(); 556 | if (upgradeManagerTimer) window.clearTimeout(upgradeManagerTimer); 557 | var upgradeManagerTimer = window.setInterval(upgradeManager, 5000);*/ 558 | } 559 | } 560 | 561 | setTimeout(tryStart, 5000); 562 | --------------------------------------------------------------------------------