AutoBSC++

Auto completes Brawl Stars Championship live stream events

  1. // ==UserScript==
  2. // @name AutoBSC++
  3. // @namespace https://github.com/LaptopCat
  4. // @homepageURL https://github.com/LaptopCat/AutoBSC
  5. // @supportURL https://github.com/LaptopCat/AutoBSC/issues
  6. // @license MIT
  7. // @version 0.2.2
  8. // @description Auto completes Brawl Stars Championship live stream events
  9. // @author laptopcat
  10. // @match https://event.supercell.com/brawlstars/*
  11. // @icon https://event.supercell.com/brawlstars/page-icon.ico
  12. // @grant none
  13. // ==/UserScript==
  14.  
  15. function load(key, def) {
  16. let res = localStorage.getItem("autobsc-" + key)
  17.  
  18. if (res === null) {
  19. store(key, def)
  20. return def
  21. } else {
  22. return JSON.parse(res)
  23. }
  24. }
  25.  
  26. function store(key, val) {
  27. localStorage.setItem("autobsc-"+key, JSON.stringify(val))
  28. }
  29.  
  30. // ==================== Begin AutoBSC Configuration ====================
  31. // This is the default configuration
  32. // load(config_key, default_value)
  33. // Do not change the config key unless you know what you are doing
  34.  
  35. // Auto send cheer, +5 points
  36. let cheerEnabled = load("cheer", true);
  37.  
  38. // Auto send poll (choosing MVP), always choose the first option, +100 points
  39. let pollEnabled = load("poll", true);
  40.  
  41. // Auto send quiz, always choose correct option
  42. let quizEnabled = load("quiz", true);
  43.  
  44. // Auto send match prediction
  45. let matchPredictionEnabled = load("matchPrediction", false);
  46.  
  47. // Team selection strategy
  48. // Can be 1 (select first team), 2 (select second team), rand (select random), maj (follow majority)
  49. // This setting will only be used if match prediction is enabled
  50. let matchPredictionStrategy = load("predictionStrategy", "maj")
  51.  
  52. // Auto collect lootdrops (randomly appearing 10 point drops)
  53. let dropEnabled = load("drop", true);
  54.  
  55. // Auto collect sliders
  56. let sliderEnabled = load("slider", true);
  57.  
  58. // Log events (such as sending cheer) to the feed
  59. let feedLoggingEnabled = load("feedLogging", true);
  60.  
  61. // Remove cheer graphics (improves performance? haven't tested but pretty sure it does)
  62. let lowDetail = load("lowDetail", false);
  63.  
  64. // Debug logging of websocket messages to console
  65. let debug = false;
  66.  
  67. // ===================== End AutoBSC Configuration =====================
  68.  
  69. let feed;
  70.  
  71. function log(msg) {
  72. if (!feedLoggingEnabled) {
  73. return
  74. }
  75. if (!feed) {
  76. feed = document.getElementsByClassName("Feed__content")[0];
  77. if (!feed) {return}
  78. }
  79. feed.children[feed.children.length - 2].insertAdjacentHTML("afterend", `<div data-v-3dcc93da="" data-v-8a7cf7d7="" class="Container" style="translate: none; rotate: none; scale: none; transform: translate(0px);">
  80. <div data-v-de4b4abb="" data-v-3dcc93da="" class="BaseCard BaseCard--rmedium">
  81. <div data-v-3dcc93da="" class="ContentCard ContentCard--disabled ContentCard--inactive ContentCard--isFullWidth ContentCard--isCelebration">
  82. <div data-v-3dcc93da="" class="ContentCard__celebration">
  83. <div data-v-3dcc93da="" class="ContentCard__celebration__background"></div>
  84. <div data-v-3dcc93da="" class="ContentCard__celebration__bottomContainer"></div>
  85. <div data-v-8a7cf7d7="" class="RewardCard">
  86. <div data-v-8a7cf7d7="" class="RewardCard__rewardContainer">
  87. <div data-v-8a7cf7d7="" class="RewardCard__infoContainer">
  88. <div data-v-8a7cf7d7="" class="RewardCard__textContainer">
  89. <div data-v-8a7cf7d7="" class="RewardCard__textContainer__title">${msg}</div>
  90. </div>
  91. </div>
  92. </div>
  93. </div>
  94. </div>
  95. </div>
  96. </div>
  97. </div>`)
  98. };
  99.  
  100. function purge(elements) {
  101. for (let elem of elements) {
  102. try {
  103. elem.remove()
  104. } catch (e) {
  105. console.warn("[AutoBSC] Failed to remove element", elem, e)
  106. }
  107. }
  108. }
  109.  
  110. // The rest of the code is not recommended to modify unless you know what you are doing
  111. (function() {
  112. "use strict";
  113.  
  114. let loaded = false;
  115. let conn
  116. let matchpredblue
  117. let matchpredred
  118. let predictions
  119.  
  120. let lastCheerId = "";
  121. let lastPollId = "";
  122. let lastQuizId = "";
  123. let lastDropId = "";
  124. let lastMatchPredictionId = "";
  125. let lastSliderId = "";
  126.  
  127. const OriginalWebSocket = window.WebSocket;
  128.  
  129. class PatchedWebSocket extends OriginalWebSocket {
  130. constructor(...args) {
  131. super(...args);
  132.  
  133. const originalGet = Object.getOwnPropertyDescriptor(OriginalWebSocket.prototype, "onmessage").get;
  134.  
  135. const originalSet = Object.getOwnPropertyDescriptor(OriginalWebSocket.prototype, "onmessage").set;
  136.  
  137. Object.defineProperty(this, "onmessage", {
  138. configurable: true,
  139. enumerable: true,
  140. get() {
  141. return originalGet.call(this);
  142. },
  143. set(newOnMessage) {
  144. const onMessage = (event) => {
  145. parse(event.data, this);
  146. newOnMessage(event);
  147. };
  148. originalSet.call(this, onMessage);
  149. },
  150. });
  151.  
  152. const originalSend = this.send;
  153.  
  154. this.send = function(data) {
  155. if (debug) {
  156. const parsed = JSON.parse(data);
  157.  
  158. console.log("[AutoBSC] Sending message:", data, parsed);
  159. }
  160. originalSend.call(this, data);
  161. };
  162. }
  163. }
  164.  
  165. window.WebSocket = PatchedWebSocket;
  166.  
  167. function parse(data, ws) {
  168. const msg = JSON.parse(data)
  169. if (debug) {
  170. console.log("[AutoBSC] Received message:", msg, data);
  171. }
  172.  
  173. msg.forEach(event => {
  174. const messageType = event.messageType;
  175. if (messageType === "global_state" && !loaded) {
  176. setupAutoBsc();
  177. }
  178.  
  179. if (messageType === "cheer") {
  180. if (conn) {
  181. conn.textContent = event.payload.connectedClients
  182. }
  183.  
  184. if (lowDetail) {
  185. purge(document.getElementsByClassName("Cheer__gradient"))
  186. purge(document.getElementsByClassName("Cheer__canvas"))
  187. }
  188.  
  189. if (cheerEnabled && event.payload.typeId !== lastCheerId) {
  190. log("Sending cheer");
  191.  
  192. setTimeout(() => {
  193. for (let btn of document.getElementsByClassName("cheer-btn-container__cheer-btn")) {
  194. btn.click()
  195. }
  196. }, 500)
  197. lastCheerId = event.payload.typeId
  198. }
  199. }
  200.  
  201. if (messageType === "poll" && pollEnabled) {
  202. if (event.payload.typeId !== lastPollId) {
  203. log("Sending poll");
  204.  
  205. setTimeout(() => {
  206. try {
  207. for (let que of document.getElementsByClassName("MultiChoiceQuestionCard")) {
  208. que.getElementsByTagName("button")[0].click()
  209. }
  210. } catch (e) {
  211. console.error("[AutoBSC]", e)
  212. }
  213. }, 3500);
  214. lastPollId = event.payload.typeId;
  215. }
  216. }
  217.  
  218. if (messageType === "quiz" && quizEnabled) {
  219. if (event.payload.typeId !== lastQuizId) {
  220. log("Sending quiz");
  221.  
  222. setTimeout(() => {
  223. for (let que of document.getElementsByClassName("BaseCard")) {
  224. try {
  225. if (que.getElementsByClassName("Points__correctAnswer").length === 0) {
  226. continue
  227. }
  228.  
  229. que.getElementsByClassName("MultiChoiceQuestionCard__button")[event.payload.correctAnswer.alternative].click()
  230. } catch (e) {
  231. console.error("[AutoBSC]", e)
  232. }
  233. }
  234. }, 3500);
  235. lastQuizId = event.payload.typeId;
  236. }
  237. }
  238.  
  239. if (messageType === "match_prediction") {
  240. predictions = event.payload.answers
  241. if (matchpredblue) {
  242. matchpredblue.textContent = predictions["0"]
  243. }
  244. if (matchpredred) {
  245. matchpredred.textContent = predictions["1"]
  246. }
  247. if (matchPredictionEnabled && event.payload.typeId !== lastMatchPredictionId) {
  248. log("Sending match prediction");
  249. let team = 0
  250. setTimeout(() => {
  251. switch (matchPredictionStrategy) {
  252. case "2":
  253. team = 1
  254. break
  255. case "rand":
  256. team = Math.floor(Math.random() * 2)
  257. break
  258. case "maj":
  259. if (predictions["0"] > predictions["1"]) {
  260. team = 0
  261. } else {
  262. team = 1
  263. }
  264. break
  265. default:
  266. break
  267. }
  268. log(`Placing prediction for ${team === 0 ? "blue" : "red"}`)
  269. for (let a of document.getElementsByClassName("MatchPredictionQuestionCard__buttonGroup")) {
  270. try {
  271. a.getElementsByTagName("button")[team].click()
  272. } catch (e) {
  273. console.error("[AutoBSC]", e)
  274. }
  275. }
  276. }, 10000);
  277.  
  278. lastMatchPredictionId = event.payload.typeId
  279. }
  280. }
  281.  
  282. if (messageType === "loot_drop" && dropEnabled) {
  283. if (event.payload.typeId !== lastDropId) {
  284. log("Collecting loot drop")
  285.  
  286. setTimeout(() => {
  287. for (let drop of document.getElementsByClassName("LootDropCard")) {
  288. try {
  289. drop.getElementsByClassName("RectangleButton")[0].click()
  290. } catch (e) {
  291. console.error("[AutoBSC]", e)
  292. }
  293. }
  294. lastDropId = event.payload.typeId
  295. }, 2000)
  296. }
  297. }
  298.  
  299. if (messageType === "slider" && sliderEnabled) {
  300. if (event.payload.typeId !== lastSliderId) {
  301. log("Collecting slider")
  302.  
  303. setTimeout(() => {
  304. for (let drop of document.getElementsByClassName("SliderQuestionCard")) {
  305. try {
  306. let elem = drop.getElementsByTagName("input")[0]
  307. elem.value = "100"
  308. elem.dispatchEvent(new InputEvent("input"))
  309. elem.dispatchEvent(new Event("change"))
  310. } catch (e) {
  311. console.error("[AutoBSC]", e)
  312. }
  313. }
  314. lastSliderId = event.payload.typeId
  315. }, 2000)
  316. }
  317. }
  318. })
  319. }
  320.  
  321. function setupAutoBsc() {
  322. loaded = true;
  323.  
  324. console.log("[AutoBSC] AutoBSC loaded");
  325.  
  326. const interval = setInterval(() => {
  327. const div = document.getElementsByClassName("Feed__content")[0];
  328. if (div) {
  329. div.insertAdjacentHTML("afterbegin", loadedMessageHtml);
  330. clearInterval(interval);
  331. }
  332. }, 500);
  333.  
  334. const reconnectButtonContainer = document.querySelector("#__layout > div > div:nth-child(5)");
  335. const reconnectButton = document.querySelector(
  336. "#__layout > div > div:nth-child(5) > div > div > div > div.baseModal__scroll > div > div > button > div.RectangleButton.RectangleButton--cta > div > div"
  337. );
  338.  
  339. setInterval(() => {
  340. if (reconnectButtonContainer.style.display !== "none") {
  341. console.log("[AutoBSC] Reconnecting");
  342. reconnectButton.click();
  343. }
  344. }, 1000);
  345.  
  346. document.body.insertAdjacentHTML("afterbegin", `
  347. <style>
  348. #autobsc-overlay > details[open] {
  349. width: 20rem;
  350. }
  351.  
  352. .autobsc-config-container {
  353. width: 15rem;
  354. padding-bottom: 0.15rem;
  355. }
  356.  
  357. .autobsc-config-container > input[type=checkbox] {
  358. float: right;
  359. position: relative;
  360. top: 0.15rem;
  361. }
  362.  
  363. .Video__InteractionBlocker, .VideoCover.VideoCover--hidden {
  364. all: unset !important;
  365. display: none;
  366. }
  367. </style>
  368. <div id="autobsc-overlay" style="position: absolute; top: 20%; z-index: 99999999; background: antiquewhite">
  369. <details>
  370. <summary style="list-style: none;" id="autobsc-overlayheader" onclick="if (getAttribute('drag') === '') event.preventDefault()">
  371. <div style="padding: 1rem;">AutoBSC++</div>
  372. </summary>
  373.  
  374. <div style="display: grid; justify-content: center; margin-bottom: .5rem;">
  375. <div>
  376. <div style="margin-bottom: .5rem">
  377. <h1>Data</h1>
  378. Connected: <span id="autobsc-connected">unknown</span>
  379. </div>
  380.  
  381. <div style="margin-bottom: .5rem;">
  382. <h3>Predictions</h3>
  383. Blue: <span id="autobsc-pick-blue">unknown</span><br>
  384. Red: <span id="autobsc-pick-red">unknown</span>
  385. </div>
  386.  
  387. <h1>Config</h1>
  388. <div class="autobsc-config-container">Autocheer <input type="checkbox" id="autobsc-cheer"></div>
  389.  
  390. <div class="autobsc-config-container">Answer polls <input type="checkbox" id="autobsc-poll"></div>
  391.  
  392. <div class="autobsc-config-container">Answer quiz <input type="checkbox" id="autobsc-quiz"></div>
  393.  
  394. <div class="autobsc-config-container">Answer slider <input type="checkbox" id="autobsc-slider"></div>
  395.  
  396. <div class="autobsc-config-container">Collect lootdrop <input type="checkbox" id="autobsc-lootdrop"></div>
  397.  
  398. <div class="autobsc-config-container">Autopredict <input type="checkbox" id="autobsc-predict"></div>
  399.  
  400. <div class="autobsc-config-container">Autopredict strategy <select style="width: 3.825rem;" id="autobsc-predict-strat">
  401. <option value="1">Blue</option>
  402. <option value="2">Red</option>
  403. <option value="rand">Random</option>
  404. <option value="maj">Follow majority</option>
  405. </select></div>
  406.  
  407. <div class="autobsc-config-container">Feed logging <input type="checkbox" id="autobsc-feedlogging"></div>
  408. <div class="autobsc-config-container">Low Detail Mode <input type="checkbox" id="autobsc-lowdetail"></div>
  409.  
  410. <button style="background-color: red; border: none; color: white;" onclick='if (confirm("Are you sure? You will only be able to open the overlay again by reloading the page")) document.getElementById("autobsc-overlay").remove()'>Destroy overlay</button>
  411.  
  412. </div>
  413. </div>
  414. </details></div>
  415. `)
  416. dragElement(document.getElementById("autobsc-overlay"))
  417.  
  418. const elems = {
  419. cheer: document.getElementById("autobsc-cheer"),
  420. poll: document.getElementById("autobsc-poll"),
  421. quiz: document.getElementById("autobsc-quiz"),
  422. slider: document.getElementById("autobsc-slider"),
  423. lootdrop: document.getElementById("autobsc-lootdrop"),
  424. predict: document.getElementById("autobsc-predict"),
  425. predictstrat: document.getElementById("autobsc-predict-strat"),
  426. feedlogging: document.getElementById("autobsc-feedlogging"),
  427. lowdetail: document.getElementById("autobsc-lowdetail")
  428. }
  429.  
  430. elems.cheer.checked = cheerEnabled
  431. elems.poll.checked = pollEnabled
  432. elems.quiz.checked = quizEnabled
  433. elems.slider.checked = sliderEnabled
  434. elems.predict.checked = matchPredictionEnabled
  435. elems.lootdrop.checked = dropEnabled
  436. elems.feedlogging.checked = feedLoggingEnabled
  437.  
  438. elems.predictstrat.value = matchPredictionStrategy
  439. elems.lowdetail.checked = lowDetail
  440.  
  441. elems.cheer.onchange = function(e) {
  442. cheerEnabled = e.target.checked
  443. store("cheer", cheerEnabled)
  444. }
  445. elems.poll.onchange = function(e) {
  446. pollEnabled = e.target.checked
  447. store("poll", pollEnabled)
  448. }
  449. elems.quiz.onchange = function(e) {
  450. quizEnabled = e.target.checked
  451. store("quiz", quizEnabled)
  452. }
  453.  
  454. elems.slider.onchange = function(e) {
  455. sliderEnabled = e.target.checked
  456. store("slider", sliderEnabled)
  457. }
  458. elems.predict.onchange = function(e) {
  459. matchPredictionEnabled = e.target.checked
  460. store("matchPrediction", matchPredictionEnabled)
  461. }
  462. elems.lootdrop.onchange = function(e) {
  463. dropEnabled = e.target.checked
  464. store("drop", dropEnabled)
  465. }
  466. elems.feedlogging.onchange = function(e) {
  467. feedLoggingEnabled = e.target.checked
  468. store("feedLogging", feedLoggingEnabled)
  469. }
  470.  
  471. elems.predictstrat.onchange = function(e) {
  472. matchPredictionStrategy = e.target.value
  473. store("predictionStrategy", matchPredictionStrategy)
  474. }
  475.  
  476. elems.lowdetail.onchange = function(e) {
  477. lowDetail = e.target.checked
  478. store("lowDetail", lowDetail)
  479. if (!lowDetail) {
  480. return
  481. }
  482. purge(document.getElementsByClassName("Cheer__gradient"))
  483. purge(document.getElementsByClassName("Cheer__canvas"))
  484. }
  485.  
  486. conn = document.getElementById("autobsc-connected")
  487. matchpredblue = document.getElementById("autobsc-pick-blue")
  488. matchpredred = document.getElementById("autobsc-pick-red")
  489. }
  490.  
  491. const loadedMessageHtml = `<div data-v-3dcc93da="" data-v-8a7cf7d7="" class="Container Container--extraTopMargin" style="translate: none; rotate: none; scale: none; transform: translate(0px);">
  492. <div data-v-de4b4abb="" data-v-3dcc93da="" class="BaseCard BaseCard--rmedium">
  493. <div data-v-3dcc93da="" class="ContentCard ContentCard--disabled ContentCard--inactive ContentCard--isFullWidth ContentCard--isCelebration">
  494. <div data-v-3dcc93da="" class="ContentCard__celebration">
  495. <div data-v-3dcc93da="" class="ContentCard__celebration__background"></div>
  496. <div data-v-3dcc93da="" class="ContentCard__celebration__bottomContainer"></div>
  497. <div data-v-8a7cf7d7="" class="RewardCard">
  498. <div data-v-8a7cf7d7="" class="RewardCard__rewardContainer">
  499. <div data-v-8a7cf7d7="" class="RewardCard__reward">
  500. <picture data-v-afed0133="" data-v-8a7cf7d7="" class="cms-image cms-image--fullWidth cms-image--loaded cms-image--fullWidth"><img data-v-afed0133="" src="https://event.supercell.com/brawlstars/assets/rewards/images/emoji_starr.svg" class="cms-image cms-image--fullWidth cms-image--loaded cms-image--fullWidth"></picture>
  501. </div>
  502. <div data-v-8a7cf7d7="" class="RewardCard__infoContainer">
  503. <div data-v-8a7cf7d7="" class="RewardCard__textContainer">
  504. <div data-v-8a7cf7d7="" class="RewardCard__textContainer__title">AutoBSC++ loaded</div>
  505. <div data-v-8a7cf7d7="" class="RewardCard__textContainer__subTitle">made by laptopcat (based on AutoBSC by catme0w)</div>
  506. </div>
  507. </div>
  508. </div>
  509. </div>
  510. </div>
  511. </div>
  512. </div>
  513. </div>`;
  514. })();
  515.  
  516. function dragElement(elmnt) {
  517. var pos1 = 0,
  518. pos2 = 0,
  519. pos3 = 0,
  520. pos4 = 0;
  521.  
  522. let dragger = document.getElementById(elmnt.id + "header") ?? elmnt
  523. dragger.onmousedown = dragMouseDown
  524.  
  525. function dragMouseDown(e) {
  526. e.preventDefault();
  527. // get the mouse cursor position at startup:
  528. pos3 = e.clientX;
  529. pos4 = e.clientY;
  530. document.onmouseup = closeDragElement;
  531. // call a function whenever the cursor moves:
  532. document.onmousemove = elementDrag;
  533. }
  534.  
  535. function elementDrag(e) {
  536. dragger.setAttribute("drag", "")
  537. e.preventDefault();
  538. // calculate the new cursor position:
  539. pos1 = pos3 - e.clientX;
  540. pos2 = pos4 - e.clientY;
  541. pos3 = e.clientX;
  542. pos4 = e.clientY;
  543. // set the element's new position:
  544. elmnt.style.top = (elmnt.offsetTop - pos2) + "px";
  545. elmnt.style.left = (elmnt.offsetLeft - pos1) + "px";
  546. }
  547.  
  548. function closeDragElement() {
  549. setTimeout(() => dragger.removeAttribute("drag"), 100)
  550. document.onmouseup = null
  551. document.onmousemove = null
  552. }
  553. }