UX Improvements

Add many UI improvements and additions

As of 2024-01-22. See the latest version.

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