UX Improvements

Many refactors and additions to the TankTrouble interface

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