UX Improvements

Add many UI improvements and additions

当前为 2024-02-03 提交的版本,查看 最新版本

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