Greasy Fork is available in English.

UX Improvements

Add many UI improvements and additions

Fra 21.03.2024. Se den seneste versjonen.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

  1. // ==UserScript==
  2. // @name UX Improvements
  3. // @author commander
  4. // @description Add many UI improvements and additions
  5. // @namespace https://github.com/asger-finding/tanktrouble-userscripts
  6. // @version 0.0.3
  7. // @license GPL-3.0
  8. // @match *://*.tanktrouble.com/*
  9. // @exclude *://classic.tanktrouble.com/
  10. // @run-at document-end
  11. // @grant GM_addStyle
  12. // @require https://update.greatest.deepsurf.us/scripts/482092/1309109/TankTrouble%20Development%20Library.js
  13. // @noframes
  14. // ==/UserScript==
  15.  
  16. // TODO: Search in the forum (searxng api?)
  17. // TODO: Button to render high-res tanks no outline in TankInfoBox
  18. // TODO: Minimum game quality setting
  19. // TODO: Lobby games carousel
  20. // TODO: control switcher
  21.  
  22. const ranges = {
  23. years: 3600 * 24 * 365,
  24. months: (365 * 3600 * 24) / 12,
  25. weeks: 3600 * 24 * 7,
  26. days: 3600 * 24,
  27. hours: 3600,
  28. minutes: 60,
  29. seconds: 1
  30. };
  31.  
  32. /**
  33. * Format a timestamp to relative time ago from now
  34. * @param date Date object
  35. * @returns Time ago
  36. */
  37. const timeAgo = date => {
  38. const formatter = new Intl.RelativeTimeFormat('en');
  39. const secondsElapsed = (date.getTime() - Date.now()) / 1000;
  40.  
  41. for (const key in ranges) {
  42. if (ranges[key] < Math.abs(secondsElapsed)) {
  43. const delta = secondsElapsed / ranges[key];
  44. return formatter.format(Math.ceil(delta), key);
  45. }
  46. }
  47.  
  48. return 'now';
  49. };
  50.  
  51. GM_addStyle(`
  52. player-name {
  53. width: 150px;
  54. height: 20px;
  55. left: -5px;
  56. top: -12px;
  57. position: relative;
  58. display: block;
  59. }`);
  60.  
  61. // Wide "premium" screen
  62. GM_addStyle(`
  63. #content {
  64. max-width: 1884px !important;
  65. width: calc(100%) !important;
  66. }
  67.  
  68. .horizontalAdSlot,
  69. .verticalAdSlot,
  70. #leftBanner,
  71. #rightBanner,
  72. #topBanner {
  73. display: none !important;
  74. }
  75. `);
  76.  
  77. if (!customElements.get('player-name')) {
  78. customElements.define('player-name',
  79.  
  80. /**
  81. * Custom HTML element that renders a TankTrouble-style player name
  82. * from the username, width and height attribute
  83. */
  84. class PlayerName extends HTMLElement {
  85.  
  86. /**
  87. * Initialize the player name element
  88. */
  89. constructor() {
  90. super();
  91.  
  92. const shadow = this.attachShadow({ mode: 'closed' });
  93.  
  94. this.username = this.getAttribute('username') || 'Scrapped';
  95. this.width = this.getAttribute('width') || '150';
  96. this.height = this.getAttribute('height') || '25';
  97.  
  98. // create the internal implementation
  99. this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
  100. this.svg.setAttribute('width', this.width);
  101. this.svg.setAttribute('height', this.height);
  102.  
  103. this.name = document.createElementNS('http://www.w3.org/2000/svg', 'text');
  104. this.name.setAttribute('x', '50%');
  105. this.name.setAttribute('y', '0');
  106. this.name.setAttribute('text-anchor', 'middle');
  107. this.name.setAttribute('dominant-baseline', 'text-before-edge');
  108. this.name.setAttribute('font-family', 'TankTrouble');
  109. this.name.setAttribute('font-weight', 'normal');
  110. this.name.setAttribute('font-size', '16');
  111. this.name.setAttribute('fill', 'white');
  112. this.name.setAttribute('stroke', 'black');
  113. this.name.setAttribute('stroke-line-join', 'round');
  114. this.name.setAttribute('stroke-width', '2');
  115. this.name.setAttribute('paint-order', 'stroke');
  116. this.name.textContent = this.username;
  117.  
  118. this.svg.appendChild(this.name);
  119.  
  120. shadow.appendChild(this.svg);
  121. }
  122.  
  123. /**
  124. * Scale the username SVG text when it's in the DOM.
  125. *
  126. * Bounding boxes will first be calculated right when
  127. * it can be rendered.
  128. */
  129. connectedCallback() {
  130. const nameWidth = this.name.getComputedTextLength();
  131. if (nameWidth > this.width) {
  132. // Scale text down to match svg size
  133. const newSize = Math.floor((this.width / nameWidth) * 100);
  134. this.name.setAttribute('font-size', `${ newSize }%`);
  135. }
  136. }
  137.  
  138. });
  139. }
  140.  
  141. (() => {
  142. /**
  143. * Patch a sprite that doesn't have a .log bound to it
  144. * @param spriteName Name of the sprite in the DOM
  145. * @returns Function wrapper
  146. */
  147. const bindLogToSprite = spriteName => {
  148. const Sprite = Reflect.get(unsafeWindow, spriteName);
  149. if (!Sprite) throw new Error('No sprite in window with name', spriteName);
  150.  
  151. return function(...args) {
  152. const sprite = new Sprite(...args);
  153.  
  154. sprite.log = Log.create(spriteName);
  155.  
  156. return sprite;
  157. };
  158. };
  159.  
  160. Reflect.set(unsafeWindow, 'UIDiamondSprite', bindLogToSprite('UIDiamondSprite'));
  161. Reflect.set(unsafeWindow, 'UIGoldSprite', bindLogToSprite('UIGoldSprite'));
  162. })();
  163.  
  164. (() => {
  165. GM_addStyle(`
  166. @keyframes highlight-thread {
  167. 50% {
  168. border: #a0e900 2px solid;
  169. background-color: #dcffcc;
  170. }
  171. }
  172. .forum .thread.highlight .bubble,
  173. .forum .reply.highlight .bubble {
  174. animation: .5s ease-in 0.3s 2 alternate highlight-thread;
  175. }
  176. .forum .tanks {
  177. position: absolute;
  178. }
  179. .forum .reply.left .tanks {
  180. left: 0;
  181. }
  182. .forum .reply.right .tanks {
  183. right: 0;
  184. }
  185. .forum .tanks.tankCount2 {
  186. transform: scale(0.8);
  187. }
  188. .forum .tanks.tankCount3 {
  189. transform: scale(0.6);
  190. }
  191. .forum .tank.coCreator1 {
  192. position: absolute;
  193. transform: translate(-55px, 0px);
  194. }
  195. .forum .tank.coCreator2 {
  196. position: absolute;
  197. transform: translate(-110px, 0px);
  198. }
  199. .forum .reply.right .tank.coCreator1 {
  200. position: absolute;
  201. transform: translate(55px, 0px);
  202. }
  203. .forum .reply.right .tank.coCreator2 {
  204. position: absolute;
  205. transform: translate(110px, 0px);
  206. }
  207. .forum .share img {
  208. display: none;
  209. }
  210. .forum .thread .share:not(:active) .standard,
  211. .forum .thread .share:active .active,
  212. .forum .reply .share:not(:active) .standard,
  213. .forum .reply .share:active .active {
  214. display: inherit;
  215. }
  216. `);
  217.  
  218. // The jquery SVG plugin does not support the newer paint-order attribute
  219. $.svg._attrNames.paintOrder = 'paint-order';
  220.  
  221. /**
  222. * Add tank previews for all thread creators, not just the primary creator
  223. * @param threadOrReply Post data
  224. * @param threadOrReplyElement Parsed post element
  225. */
  226. const insertMultipleCreators = (threadOrReply, threadOrReplyElement) => {
  227. // Remove original tank preview
  228. threadOrReplyElement.find('.tank').remove();
  229.  
  230. const creators = {
  231. ...{ creator: threadOrReply.creator },
  232. ...threadOrReply.coCreator1 && { coCreator1: threadOrReply.coCreator1 },
  233. ...threadOrReply.coCreator2 && { coCreator2: threadOrReply.coCreator2 }
  234. };
  235. const creatorsContainer = $('<div/>')
  236. .addClass(`tanks tankCount${Object.keys(creators).length}`)
  237. .insertBefore(threadOrReplyElement.find('.container'));
  238.  
  239. // Render all creator tanks in canvas
  240. for (const [creatorType, playerId] of Object.entries(creators)) {
  241. const wrapper = document.createElement('div');
  242. wrapper.classList.add('tank', creatorType);
  243.  
  244. const canvas = document.createElement('canvas');
  245. canvas.width = UIConstants.TANK_ICON_WIDTH_SMALL;
  246. canvas.height = UIConstants.TANK_ICON_HEIGHT_SMALL;
  247. canvas.style.width = `${UIConstants.TANK_ICON_RESOLUTIONS[UIConstants.TANK_ICON_SIZES.SMALL] }px`;
  248. canvas.style.height = `${UIConstants.TANK_ICON_RESOLUTIONS[UIConstants.TANK_ICON_SIZES.SMALL] * 0.6 }px`;
  249. canvas.addEventListener('mouseup', () => {
  250. const rect = canvas.getBoundingClientRect();
  251. const win = canvas.ownerDocument.defaultView;
  252.  
  253. const top = rect.top + win.scrollY;
  254. const left = rect.left + win.scrollX;
  255.  
  256. TankTrouble.TankInfoBox.show(left + (canvas.clientWidth / 2), top + (canvas.clientHeight / 2), playerId, canvas.clientWidth / 2, canvas.clientHeight / 4);
  257. });
  258. UITankIcon.loadPlayerTankIcon(canvas, UIConstants.TANK_ICON_SIZES.SMALL, playerId);
  259.  
  260. wrapper.append(canvas);
  261. creatorsContainer.append(wrapper);
  262. }
  263.  
  264. // Render name of primary creator
  265. Backend.getInstance().getPlayerDetails(result => {
  266. const username = typeof result === 'object' ? Utils.maskUnapprovedUsername(result) : 'Scrapped';
  267. const width = UIConstants.TANK_ICON_RESOLUTIONS[UIConstants.TANK_ICON_SIZES.SMALL] + 10;
  268. const height = 25;
  269.  
  270. const playerName = $(`<player-name username="${ username }" width="${ width }" height="${ height }"></player-name>`);
  271. creatorsContainer.find('.tank.creator').append(playerName);
  272. }, () => {}, () => {}, creators.creator, Caches.getPlayerDetailsCache());
  273. };
  274.  
  275. /**
  276. * Scroll a post into view if it's not already
  277. * and highlight it once in view
  278. * @param threadOrReply Parsed post element
  279. */
  280. const highlightThreadOrReply = threadOrReply => {
  281. const observer = new IntersectionObserver(entries => {
  282. const [entry] = entries;
  283. const inView = entry.isIntersecting;
  284.  
  285. threadOrReply[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
  286. if (inView) {
  287. threadOrReply.addClass('highlight');
  288.  
  289. observer.disconnect();
  290. }
  291. });
  292.  
  293. observer.observe(threadOrReply[0]);
  294. };
  295.  
  296. /**
  297. * Insert a share button to the thread or reply that copies the link to the post to clipboard
  298. * @param threadOrReply Post data
  299. * @param threadOrReplyElement Parsed post element
  300. */
  301. const addShare = (threadOrReply, threadOrReplyElement) => {
  302. const isReply = Boolean(threadOrReply.threadId);
  303.  
  304. const url = new URL(window.location.href);
  305. const wasWindowOpenedFromPostShare = url.searchParams.get('ref') === 'share';
  306. if (wasWindowOpenedFromPostShare && isReply) {
  307. const urlReplyId = Number(url.searchParams.get('id'));
  308. if (urlReplyId === threadOrReply.id) highlightThreadOrReply(threadOrReplyElement);
  309. }
  310.  
  311. const likeAction = threadOrReplyElement.find('.action.like');
  312.  
  313. let shareAction = $('<div class="action share"></div>');
  314. const shareActionStandardImage = $('<img class="standard" src="https://i.imgur.com/emJXwew.png" srcset="https://i.imgur.com/UF4gXBk.png 2x"/>');
  315. const shareActionActiveImage = $('<img class="active" src="https://i.imgur.com/pNQ0Aja.png" srcset="https://i.imgur.com/Ti3IplV.png 2x"/>');
  316.  
  317. shareAction.append([shareActionStandardImage, shareActionActiveImage]);
  318. likeAction.after(shareAction);
  319.  
  320. // Replies have a duplicate actions container for
  321. // both right and left-facing replies.
  322. // So when the share button is appended, there may be multiple
  323. // and so we need to realize those instances as well
  324. shareAction = threadOrReplyElement.find('.action.share');
  325.  
  326. shareAction.tooltipster({
  327. position: 'top',
  328. offsetY: 5,
  329.  
  330. /** Reset tooltipster when mouse leaves */
  331. functionAfter: () => {
  332. shareAction.tooltipster('content', 'Copy link to clipboard');
  333. }
  334. });
  335. shareAction.tooltipster('content', 'Copy link to clipboard');
  336.  
  337. shareAction.on('mouseup', () => {
  338. const urlConstruct = new URL('/forum', window.location.origin);
  339.  
  340. if (isReply) {
  341. urlConstruct.searchParams.set('id', threadOrReply.id);
  342. urlConstruct.searchParams.set('threadId', threadOrReply.threadId);
  343. } else {
  344. urlConstruct.searchParams.set('threadId', threadOrReply.id);
  345. }
  346.  
  347. urlConstruct.searchParams.set('ref', 'share');
  348.  
  349. ClipboardManager.copy(urlConstruct.href);
  350.  
  351. shareAction.tooltipster('content', 'Copied!');
  352. });
  353. };
  354.  
  355. /**
  356. * Add text to details that shows when a post was last edited
  357. * @param threadOrReply Post data
  358. * @param threadOrReplyElement Parsed post element
  359. */
  360. const addLastEdited = (threadOrReply, threadOrReplyElement) => {
  361. const { created, latestEdit } = threadOrReply;
  362.  
  363. if (latestEdit) {
  364. const details = threadOrReplyElement.find('.bubble .details');
  365. const detailsText = details.text();
  366. const replyIndex = detailsText.indexOf('-');
  367. const lastReply = replyIndex !== -1
  368. ? ` - ${ detailsText.slice(replyIndex + 1).trim()}`
  369. : '';
  370.  
  371. // We remake creation time since the timeAgo
  372. // function estimates months slightly off
  373. // which may result in instances where the
  374. // edited happened longer ago than the thread
  375. // creation date
  376. const createdAgo = timeAgo(new Date(created * 1000));
  377. const editedAgo = `, edited ${ timeAgo(new Date(latestEdit * 1000)) }`;
  378.  
  379. details.text(`Created ${createdAgo}${editedAgo}${lastReply}`);
  380. }
  381. };
  382.  
  383. /**
  384. * Add anchor tags to links in posts
  385. * @param _threadOrReply Post data
  386. * @param threadOrReplyElement Parsed post element
  387. */
  388. const addHyperlinks = (_threadOrReply, threadOrReplyElement) => {
  389. const threadOrReplyContent = threadOrReplyElement.find('.bubble .content');
  390.  
  391. if (threadOrReplyContent.length) {
  392. const urlRegex = /(?<_>https?:\/\/[\w\-_]+(?:\.[\w\-_]+)+(?:[\w\-.,@?^=%&amp;:/~+#]*[\w\-@?^=%&amp;/~+#])?)/gu;
  393. const messageWithLinks = threadOrReplyContent.html().replace(urlRegex, '<a href="$1" target="_blank">$1</a>');
  394. threadOrReplyContent.html(messageWithLinks);
  395. }
  396. };
  397.  
  398. /**
  399. * Add extra features to a thread or reply
  400. * @param threadOrReply Post data
  401. * @param threadOrReplyElement
  402. */
  403. const addFeaturesToThreadOrReply = (threadOrReply, threadOrReplyElement) => {
  404. insertMultipleCreators(threadOrReply, threadOrReplyElement);
  405. addLastEdited(threadOrReply, threadOrReplyElement);
  406. addShare(threadOrReply, threadOrReplyElement);
  407. addHyperlinks(threadOrReply, threadOrReplyElement);
  408. };
  409.  
  410. /**
  411. *
  412. * @param threadOrReply
  413. */
  414. const handleThreadOrReply = threadOrReply => {
  415. if (threadOrReply === null) return;
  416.  
  417. const [key] = Object.keys(threadOrReply.html);
  418. const html = threadOrReply.html[key];
  419.  
  420. if (typeof html === 'string') {
  421. const threadOrReplyElement = $($.parseHTML(html));
  422.  
  423. addFeaturesToThreadOrReply(threadOrReply, threadOrReplyElement);
  424. threadOrReply.html[key] = threadOrReplyElement;
  425. threadOrReply.html.backup = html;
  426. } else if (html instanceof $) {
  427. // For some reason, the post breaks if it's already
  428. // been parsed through here. Therefore, we pull
  429. // from the backup html we set, and re-apply the changes
  430. const threadOrReplyElement = $($.parseHTML(threadOrReply.html.backup));
  431.  
  432. addFeaturesToThreadOrReply(threadOrReply, threadOrReplyElement);
  433. threadOrReply.html[key] = threadOrReplyElement;
  434. }
  435. };
  436.  
  437. const threadListChanged = ForumView.getMethod('threadListChanged');
  438. ForumView.method('threadListChanged', function(...args) {
  439. const threadList = args.shift();
  440. for (const thread of threadList) handleThreadOrReply(thread);
  441.  
  442. const result = threadListChanged.apply(this, [threadList, ...args]);
  443. return result;
  444. });
  445.  
  446. const replyListChanged = ForumView.getMethod('replyListChanged');
  447. ForumView.method('replyListChanged', function(...args) {
  448. const replyList = args.shift();
  449. for (const thread of replyList) handleThreadOrReply(thread);
  450.  
  451. const result = replyListChanged.apply(this, [replyList, ...args]);
  452. return result;
  453. });
  454.  
  455. const getSelectedThread = ForumModel.getMethod('getSelectedThread');
  456. ForumModel.method('getSelectedThread', function(...args) {
  457. const result = getSelectedThread.apply(this, [...args]);
  458.  
  459. handleThreadOrReply(result);
  460.  
  461. return result;
  462. });
  463. })();
  464.  
  465. (() => {
  466. Loader.interceptFunction(TankTrouble.AccountOverlay, '_initialize', (original, ...args) => {
  467. original(...args);
  468.  
  469. TankTrouble.AccountOverlay.accountCreatedText = $('<div></div>');
  470. TankTrouble.AccountOverlay.accountCreatedText.insertAfter(TankTrouble.AccountOverlay.accountHeadline);
  471. });
  472.  
  473. Loader.interceptFunction(TankTrouble.AccountOverlay, 'show', (original, ...args) => {
  474. original(...args);
  475.  
  476. Backend.getInstance().getPlayerDetails(result => {
  477. if (typeof result === 'object') {
  478. const created = new Date(result.getCreated() * 1000);
  479. const formatted = new Intl.DateTimeFormat('en-GB', { dateStyle: 'full' }).format(created);
  480.  
  481. TankTrouble.AccountOverlay.accountCreatedText.text(`Created: ${formatted} (${timeAgo(created)})`);
  482. }
  483. }, () => {}, () => {}, TankTrouble.AccountOverlay.playerId, Caches.getPlayerDetailsCache());
  484. });
  485. })();
  486.  
  487. (() => {
  488. /**
  489. * Determine player's admin state
  490. * @param playerDetails Player details
  491. * @returns -1 for retired admin, 0 for non-admin, 1 for admin
  492. */
  493. const getAdminState = playerDetails => {
  494. const isAdmin = playerDetails.getGmLevel() >= UIConstants.ADMIN_LEVEL_PLAYER_LOOKUP;
  495.  
  496. if (isAdmin) return 1;
  497. else if (TankTrouble.WallOfFame.admins.includes(playerDetails.getUsername())) return -1;
  498. return 0;
  499. };
  500.  
  501. /**
  502. * Prepend admin details to username
  503. * @param usernameParts Transformable array for the username
  504. * @param playerDetails Player details
  505. * @returns Mutated username parts
  506. */
  507. const maskUsernameByAdminState = (usernameParts, playerDetails) => {
  508. const adminState = getAdminState(playerDetails);
  509.  
  510. if (adminState === 1) usernameParts.unshift(`(GM${ playerDetails.getGmLevel() }) `);
  511. else if (adminState === -1) usernameParts.unshift('(Retd.) ');
  512.  
  513. return usernameParts;
  514. };
  515.  
  516. /**
  517. * Mask username if not yet approved
  518. * If the user or an admin is logged in
  519. * locally, then still show the username
  520. * @param usernameParts Transformable array for the username
  521. * @param playerDetails Player details
  522. * @returns Mutated username parts
  523. */
  524. const maskUnapprovedUsername = (usernameParts, playerDetails) => {
  525. if (!playerDetails.getUsernameApproved()) {
  526. const playerLoggedIn = Users.isAnyUser(playerDetails.getPlayerId());
  527. const anyAdminLoggedIn = Users.getHighestGmLevel() >= UIConstants.ADMIN_LEVEL_PLAYER_LOOKUP;
  528.  
  529. if (playerLoggedIn || anyAdminLoggedIn) {
  530. usernameParts.unshift('× ');
  531. usernameParts.push(playerDetails.getUsername(), ' ×');
  532. } else {
  533. usernameParts.length = 0;
  534. usernameParts.push('× × ×');
  535. }
  536. } else {
  537. usernameParts.push(playerDetails.getUsername());
  538. }
  539.  
  540. return usernameParts;
  541. };
  542.  
  543. /**
  544. * Transforms the player's username
  545. * depending on parameters admin and username approved
  546. * @param playerDetails Player details
  547. * @returns New username
  548. */
  549. const transformUsername = playerDetails => {
  550. const usernameParts = [];
  551.  
  552. maskUnapprovedUsername(usernameParts, playerDetails);
  553. maskUsernameByAdminState(usernameParts, playerDetails);
  554.  
  555. return usernameParts.join('');
  556. };
  557.  
  558. Utils.classMethod('maskUnapprovedUsername', playerDetails => transformUsername(playerDetails));
  559. })();
  560.  
  561. (() => {
  562. GM_addStyle(`
  563. .walletIcon {
  564. object-fit: contain;
  565. margin-right: 6px;
  566. }
  567. `);
  568.  
  569. Loader.interceptFunction(TankTrouble.VirtualShopOverlay, '_initialize', (original, ...args) => {
  570. original(...args);
  571.  
  572. // Initialize wallet elements
  573. TankTrouble.VirtualShopOverlay.walletGold = $("<div><button class='medium disabled' style='display: flex;'>Loading ...</button></div>");
  574. TankTrouble.VirtualShopOverlay.walletDiamonds = $("<div><button class='medium disabled' style='display: flex;'>Loading ...</button></div>");
  575. TankTrouble.VirtualShopOverlay.navigation.append([TankTrouble.VirtualShopOverlay.walletGold, TankTrouble.VirtualShopOverlay.walletDiamonds]);
  576. });
  577.  
  578. Loader.interceptFunction(TankTrouble.VirtualShopOverlay, 'show', (original, ...args) => {
  579. original(...args);
  580.  
  581. const [params] = args;
  582. Backend.getInstance().getCurrency(result => {
  583. if (typeof result === 'object') {
  584. // Set wallet currency from result
  585. const goldButton = TankTrouble.VirtualShopOverlay.walletGold.find('button').empty();
  586. const diamondsButton = TankTrouble.VirtualShopOverlay.walletDiamonds.find('button').empty();
  587.  
  588. Utils.addImageWithClasses(goldButton, 'walletIcon', 'assets/images/virtualShop/gold.png');
  589. goldButton.append(result.getGold());
  590. Utils.addImageWithClasses(diamondsButton, 'walletIcon', 'assets/images/virtualShop/diamond.png');
  591. diamondsButton.append(result.getDiamonds());
  592. }
  593. }, () => {}, () => {}, params.playerId, Caches.getCurrencyCache());
  594. });
  595. })();
  596.  
  597. (() => {
  598. Loader.interceptFunction(TankTrouble.TankInfoBox, '_initialize', (original, ...args) => {
  599. original(...args);
  600.  
  601. // Initialize death info elements
  602. TankTrouble.TankInfoBox.infoDeathsDiv = $('<tr/>');
  603. TankTrouble.TankInfoBox.infoDeathsIcon = $('<img class="statsIcon" src="https://i.imgur.com/PMAUKdq.png" srcset="https://i.imgur.com/vEjIwA4.png 2x"/>');
  604. TankTrouble.TankInfoBox.infoDeaths = $('<div/>');
  605.  
  606. // Align to center
  607. TankTrouble.TankInfoBox.infoDeathsDiv.css({
  608. display: 'flex',
  609. 'align-items': 'center',
  610. margin: '0 auto',
  611. width: 'fit-content'
  612. });
  613.  
  614. TankTrouble.TankInfoBox.infoDeathsDiv.tooltipster({
  615. position: 'left',
  616. offsetX: 5
  617. });
  618.  
  619. TankTrouble.TankInfoBox.infoDeathsDiv.append(TankTrouble.TankInfoBox.infoDeathsIcon);
  620. TankTrouble.TankInfoBox.infoDeathsDiv.append(TankTrouble.TankInfoBox.infoDeaths);
  621. TankTrouble.TankInfoBox.infoDeathsDiv.insertAfter(TankTrouble.TankInfoBox.infoTable);
  622.  
  623. TankTrouble.TankInfoBox.infoDeaths.svg({
  624. settings: {
  625. width: UIConstants.TANK_INFO_MAX_NUMBER_WIDTH,
  626. height: 34
  627. }
  628. });
  629. TankTrouble.TankInfoBox.infoDeathsSvg = TankTrouble.TankInfoBox.infoDeaths.svg('get');
  630. });
  631.  
  632. Loader.interceptFunction(TankTrouble.TankInfoBox, 'show', (original, ...args) => {
  633. original(...args);
  634.  
  635. TankTrouble.TankInfoBox.infoDeathsDiv.tooltipster('content', 'Deaths');
  636. TankTrouble.TankInfoBox.infoDeathsSvg.clear();
  637.  
  638. const [,, playerId] = args;
  639.  
  640. Backend.getInstance().getPlayerDetails(result => {
  641. const deaths = typeof result === 'object' ? result.getDeaths() : 'N/A';
  642.  
  643. const deathsText = TankTrouble.TankInfoBox.infoDeathsSvg.text(1, 22, deaths.toString(), {
  644. textAnchor: 'start',
  645. fontFamily: 'Arial Black',
  646. fontSize: 14,
  647. fill: 'white',
  648. stroke: 'black',
  649. strokeLineJoin: 'round',
  650. strokeWidth: 3,
  651. letterSpacing: 1,
  652. paintOrder: 'stroke'
  653. });
  654. const deathsLength = Utils.measureSVGText(deaths.toString(), {
  655. fontFamily: 'Arial Black',
  656. fontSize: 14
  657. });
  658.  
  659. scaleAndTranslate = Utils.getSVGScaleAndTranslateToFit(UIConstants.TANK_INFO_MAX_NUMBER_WIDTH, deathsLength + 7, 34, 'left');
  660. TankTrouble.TankInfoBox.infoDeathsSvg.configure(deathsText, { transform: scaleAndTranslate });
  661. }, () => {}, () => {}, playerId, Caches.getPlayerDetailsCache());
  662. });
  663. })();