UX Improvements

Add many UI improvements and additions

当前为 2024-01-21 提交的版本,查看 最新版本

  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. (() => {
  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. if (!inView) {
  215. threadOrReply[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
  216. } else {
  217. threadOrReply.addClass('highlight');
  218.  
  219. observer.disconnect();
  220. }
  221. });
  222.  
  223. observer.observe(threadOrReply[0]);
  224. };
  225.  
  226. /**
  227. * Insert a share button to the thread or reply that copies the link to the post to clipboard
  228. * @param threadOrReply Post data
  229. * @param threadOrReplyElement Parsed post element
  230. */
  231. const addShare = (threadOrReply, threadOrReplyElement) => {
  232. const isReply = Boolean(threadOrReply.threadId);
  233.  
  234. const url = new URL(window.location.href);
  235. const wasWindowOpenedFromPostShare = url.searchParams.get('ref') === 'share';
  236. if (wasWindowOpenedFromPostShare && isReply) {
  237. const urlReplyId = Number(url.searchParams.get('id'));
  238. if (urlReplyId === threadOrReply.id) highlightThreadOrReply(threadOrReplyElement);
  239. }
  240.  
  241. const likeAction = threadOrReplyElement.find('.action.like');
  242.  
  243. let shareAction = $('<div class="action share"></div>');
  244. const shareActionStandardImage = $('<img class="standard" src="https://i.imgur.com/emJXwew.png" srcset="https://i.imgur.com/UF4gXBk.png 2x"/>');
  245. const shareActionActiveImage = $('<img class="active" src="https://i.imgur.com/pNQ0Aja.png" srcset="https://i.imgur.com/Ti3IplV.png 2x"/>');
  246.  
  247. shareAction.append([shareActionStandardImage, shareActionActiveImage]);
  248. likeAction.after(shareAction);
  249.  
  250. // Replies have a duplicate actions container for
  251. // both right and left-facing replies.
  252. // So when the share button is appended, there may be multiple
  253. // and so we need to realize those instances as well
  254. shareAction = threadOrReplyElement.find('.action.share');
  255.  
  256. shareAction.tooltipster({
  257. position: 'top',
  258. offsetY: 5,
  259.  
  260. /** Reset tooltipster when mouse leaves */
  261. functionAfter: () => {
  262. shareAction.tooltipster('content', 'Copy link to clipboard');
  263. }
  264. });
  265. shareAction.tooltipster('content', 'Copy link to clipboard');
  266.  
  267. shareAction.on('mouseup', () => {
  268. const urlConstruct = new URL('/forum', window.location.origin);
  269.  
  270. if (isReply) {
  271. urlConstruct.searchParams.set('id', threadOrReply.id);
  272. urlConstruct.searchParams.set('threadId', threadOrReply.threadId);
  273. } else {
  274. urlConstruct.searchParams.set('threadId', threadOrReply.id);
  275. }
  276.  
  277. urlConstruct.searchParams.set('ref', 'share');
  278.  
  279. ClipboardManager.copy(urlConstruct.href);
  280.  
  281. shareAction.tooltipster('content', 'Copied!');
  282. });
  283. };
  284.  
  285. /**
  286. * Add text to details that shows when a post was last edited
  287. * @param threadOrReply Post data
  288. * @param threadOrReplyElement Parsed post element
  289. */
  290. const addLastEdited = (threadOrReply, threadOrReplyElement) => {
  291. const { created, latestEdit } = threadOrReply;
  292.  
  293. if (latestEdit) {
  294. const details = threadOrReplyElement.find('.bubble .details');
  295. const detailsText = details.text();
  296. const replyIndex = detailsText.indexOf('-');
  297. const lastReply = replyIndex !== -1
  298. ? ` - ${ detailsText.slice(replyIndex + 1).trim()}`
  299. : '';
  300.  
  301. // We remake creation time since the timeAgo
  302. // function estimates months slightly off
  303. // which may result in instances where the
  304. // edited happened longer ago than the thread
  305. // creation date
  306. const createdAgo = timeAgo(new Date(created * 1000));
  307. const editedAgo = `, edited ${ timeAgo(new Date(latestEdit * 1000)) }`;
  308.  
  309. details.text(`Created ${createdAgo}${editedAgo}${lastReply}`);
  310. }
  311. };
  312.  
  313. /**
  314. * Add anchor tags to links in posts
  315. * @param _threadOrReply Post data
  316. * @param threadOrReplyElement Parsed post element
  317. */
  318. const addHyperlinks = (_threadOrReply, threadOrReplyElement) => {
  319. const threadOrReplyContent = threadOrReplyElement.find('.bubble .content');
  320.  
  321. if (threadOrReplyContent.length) {
  322. const urlRegex = /(?<_>https?:\/\/[\w\-_]+(?:\.[\w\-_]+)+(?:[\w\-.,@?^=%&amp;:/~+#]*[\w\-@?^=%&amp;/~+#])?)/gu;
  323. const messageWithLinks = threadOrReplyContent.html().replace(urlRegex, '<a href="$1" target="_blank">$1</a>');
  324. threadOrReplyContent.html(messageWithLinks);
  325. }
  326. };
  327.  
  328. /**
  329. * Add extra features to a thread or reply
  330. * @param threadOrReply Post data
  331. * @param threadOrReplyElement
  332. */
  333. const addFeaturesToThreadOrReply = (threadOrReply, threadOrReplyElement) => {
  334. insertMultipleCreators(threadOrReply, threadOrReplyElement);
  335. addLastEdited(threadOrReply, threadOrReplyElement);
  336. addShare(threadOrReply, threadOrReplyElement);
  337. addHyperlinks(threadOrReply, threadOrReplyElement);
  338. };
  339.  
  340. /**
  341. *
  342. * @param threadOrReply
  343. */
  344. const handleThreadOrReply = threadOrReply => {
  345. if (threadOrReply === null) return;
  346.  
  347. const [key] = Object.keys(threadOrReply.html);
  348. const html = threadOrReply.html[key];
  349.  
  350. if (typeof html === 'string') {
  351. const threadOrReplyElement = $($.parseHTML(html));
  352.  
  353. addFeaturesToThreadOrReply(threadOrReply, threadOrReplyElement);
  354. threadOrReply.html[key] = threadOrReplyElement;
  355. threadOrReply.html.backup = html;
  356. } else if (html instanceof $) {
  357. // For some reason, the post breaks if it's already
  358. // been parsed through here. Therefore, we pull
  359. // from the backup html we set, and re-apply the changes
  360. const threadOrReplyElement = $($.parseHTML(threadOrReply.html.backup));
  361.  
  362. addFeaturesToThreadOrReply(threadOrReply, threadOrReplyElement);
  363. threadOrReply.html[key] = threadOrReplyElement;
  364. }
  365. };
  366.  
  367. const threadListChanged = ForumView.getMethod('threadListChanged');
  368. ForumView.method('threadListChanged', function(...args) {
  369. const threadList = args.shift();
  370. for (const thread of threadList) handleThreadOrReply(thread);
  371.  
  372. const result = threadListChanged.apply(this, [threadList, ...args]);
  373. return result;
  374. });
  375.  
  376. const replyListChanged = ForumView.getMethod('replyListChanged');
  377. ForumView.method('replyListChanged', function(...args) {
  378. const replyList = args.shift();
  379. for (const thread of replyList) handleThreadOrReply(thread);
  380.  
  381. const result = replyListChanged.apply(this, [replyList, ...args]);
  382. return result;
  383. });
  384.  
  385. const getSelectedThread = ForumModel.getMethod('getSelectedThread');
  386. ForumModel.method('getSelectedThread', function(...args) {
  387. const result = getSelectedThread.apply(this, [...args]);
  388.  
  389. handleThreadOrReply(result);
  390.  
  391. return result;
  392. });
  393. })();
  394.  
  395. (() => {
  396. Loader.interceptFunction(TankTrouble.AccountOverlay, '_initialize', (original, ...args) => {
  397. original(...args);
  398.  
  399. TankTrouble.AccountOverlay.accountCreatedText = $('<div></div>');
  400. TankTrouble.AccountOverlay.accountCreatedText.insertAfter(TankTrouble.AccountOverlay.accountHeadline);
  401. });
  402.  
  403. Loader.interceptFunction(TankTrouble.AccountOverlay, 'show', (original, ...args) => {
  404. original(...args);
  405.  
  406. Backend.getInstance().getPlayerDetails(result => {
  407. if (typeof result === 'object') {
  408. const created = new Date(result.getCreated() * 1000);
  409. const formatted = new Intl.DateTimeFormat('en-GB', { dateStyle: 'full' }).format(created);
  410.  
  411. TankTrouble.AccountOverlay.accountCreatedText.text(`Created: ${formatted} (${timeAgo(created)})`);
  412. }
  413. }, () => {}, () => {}, TankTrouble.AccountOverlay.playerId, Caches.getPlayerDetailsCache());
  414. });
  415. })();
  416.  
  417. (() => {
  418. /**
  419. * Determine player's admin state
  420. * @param playerDetails Player details
  421. * @returns -1 for retired admin, 0 for non-admin, 1 for admin
  422. */
  423. const getAdminState = playerDetails => {
  424. const isAdmin = playerDetails.getGmLevel() >= UIConstants.ADMIN_LEVEL_PLAYER_LOOKUP;
  425.  
  426. if (isAdmin) return 1;
  427. else if (TankTrouble.WallOfFame.admins.includes(playerDetails.getUsername())) return -1;
  428. return 0;
  429. };
  430.  
  431. /**
  432. * Prepend admin details to username
  433. * @param usernameParts Transformable array for the username
  434. * @param playerDetails Player details
  435. * @returns Mutated username parts
  436. */
  437. const maskUsernameByAdminState = (usernameParts, playerDetails) => {
  438. const adminState = getAdminState(playerDetails);
  439.  
  440. if (adminState === 1) usernameParts.unshift(`(GM${ playerDetails.getGmLevel() }) `);
  441. else if (adminState === -1) usernameParts.unshift('(Retd.) ');
  442.  
  443. return usernameParts;
  444. };
  445.  
  446. /**
  447. * Mask username if not yet approved
  448. * If the user or an admin is logged in
  449. * locally, then still show the username
  450. * @param usernameParts Transformable array for the username
  451. * @param playerDetails Player details
  452. * @returns Mutated username parts
  453. */
  454. const maskUnapprovedUsername = (usernameParts, playerDetails) => {
  455. if (!playerDetails.getUsernameApproved()) {
  456. const playerLoggedIn = Users.isAnyUser(playerDetails.getPlayerId());
  457. const anyAdminLoggedIn = Users.getHighestGmLevel() >= UIConstants.ADMIN_LEVEL_PLAYER_LOOKUP;
  458.  
  459. if (playerLoggedIn || anyAdminLoggedIn) {
  460. usernameParts.unshift('× ');
  461. usernameParts.push(playerDetails.getUsername(), ' ×');
  462. } else {
  463. usernameParts.length = 0;
  464. usernameParts.push('× × ×');
  465. }
  466. } else {
  467. usernameParts.push(playerDetails.getUsername());
  468. }
  469.  
  470. return usernameParts;
  471. };
  472.  
  473. /**
  474. * Transforms the player's username
  475. * depending on parameters admin and username approved
  476. * @param playerDetails Player details
  477. * @returns New username
  478. */
  479. const transformUsername = playerDetails => {
  480. const usernameParts = [];
  481.  
  482. maskUnapprovedUsername(usernameParts, playerDetails);
  483. maskUsernameByAdminState(usernameParts, playerDetails);
  484.  
  485. return usernameParts.join('');
  486. };
  487.  
  488. Utils.classMethod('maskUnapprovedUsername', playerDetails => transformUsername(playerDetails));
  489. })();
  490.  
  491. (() => {
  492. GM_addStyle(`
  493. .walletIcon {
  494. object-fit: contain;
  495. margin-right: 6px;
  496. }
  497. `);
  498.  
  499. Loader.interceptFunction(TankTrouble.VirtualShopOverlay, '_initialize', (original, ...args) => {
  500. original(...args);
  501.  
  502. // Initialize wallet elements
  503. TankTrouble.VirtualShopOverlay.walletGold = $("<div><button class='medium disabled' style='display: flex;'>Loading ...</button></div>");
  504. TankTrouble.VirtualShopOverlay.walletDiamonds = $("<div><button class='medium disabled' style='display: flex;'>Loading ...</button></div>");
  505. TankTrouble.VirtualShopOverlay.navigation.append([TankTrouble.VirtualShopOverlay.walletGold, TankTrouble.VirtualShopOverlay.walletDiamonds]);
  506. });
  507.  
  508. Loader.interceptFunction(TankTrouble.VirtualShopOverlay, 'show', (original, ...args) => {
  509. original(...args);
  510.  
  511. const [params] = args;
  512. Backend.getInstance().getCurrency(result => {
  513. if (typeof result === 'object') {
  514. // Set wallet currency from result
  515. const goldButton = TankTrouble.VirtualShopOverlay.walletGold.find('button').empty();
  516. const diamondsButton = TankTrouble.VirtualShopOverlay.walletDiamonds.find('button').empty();
  517.  
  518. Utils.addImageWithClasses(goldButton, 'walletIcon', 'assets/images/virtualShop/gold.png');
  519. goldButton.append(result.getGold());
  520. Utils.addImageWithClasses(diamondsButton, 'walletIcon', 'assets/images/virtualShop/diamond.png');
  521. diamondsButton.append(result.getDiamonds());
  522. }
  523. }, () => {}, () => {}, params.playerId, Caches.getCurrencyCache());
  524. });
  525. })();
  526.  
  527. (() => {
  528. Loader.interceptFunction(TankTrouble.TankInfoBox, '_initialize', (original, ...args) => {
  529. original(...args);
  530.  
  531. // Initialize death info elements
  532. TankTrouble.TankInfoBox.infoDeathsDiv = $('<tr/>');
  533. TankTrouble.TankInfoBox.infoDeathsIcon = $('<img class="statsIcon" src="https://i.imgur.com/PMAUKdq.png" srcset="https://i.imgur.com/vEjIwA4.png 2x"/>');
  534. TankTrouble.TankInfoBox.infoDeaths = $('<div/>');
  535.  
  536. // Align to center
  537. TankTrouble.TankInfoBox.infoDeathsDiv.css({
  538. display: 'flex',
  539. 'align-items': 'center',
  540. margin: '0 auto',
  541. width: 'fit-content'
  542. });
  543.  
  544. TankTrouble.TankInfoBox.infoDeathsDiv.tooltipster({
  545. position: 'left',
  546. offsetX: 5
  547. });
  548.  
  549. TankTrouble.TankInfoBox.infoDeathsDiv.append(TankTrouble.TankInfoBox.infoDeathsIcon);
  550. TankTrouble.TankInfoBox.infoDeathsDiv.append(TankTrouble.TankInfoBox.infoDeaths);
  551. TankTrouble.TankInfoBox.infoDeathsDiv.insertAfter(TankTrouble.TankInfoBox.infoTable);
  552.  
  553. TankTrouble.TankInfoBox.infoDeaths.svg({
  554. settings: {
  555. width: UIConstants.TANK_INFO_MAX_NUMBER_WIDTH,
  556. height: 34
  557. }
  558. });
  559. TankTrouble.TankInfoBox.infoDeathsSvg = TankTrouble.TankInfoBox.infoDeaths.svg('get');
  560. });
  561.  
  562. Loader.interceptFunction(TankTrouble.TankInfoBox, 'show', (original, ...args) => {
  563. original(...args);
  564.  
  565. TankTrouble.TankInfoBox.infoDeathsDiv.tooltipster('content', 'Deaths');
  566. TankTrouble.TankInfoBox.infoDeathsSvg.clear();
  567.  
  568. const [,, playerId] = args;
  569.  
  570. Backend.getInstance().getPlayerDetails(result => {
  571. const deaths = typeof result === 'object' ? result.getDeaths() : 'N/A';
  572.  
  573. const deathsText = TankTrouble.TankInfoBox.infoDeathsSvg.text(1, 22, deaths.toString(), {
  574. textAnchor: 'start',
  575. fontFamily: 'Arial Black',
  576. fontSize: 14,
  577. fill: 'white',
  578. stroke: 'black',
  579. strokeLineJoin: 'round',
  580. strokeWidth: 3,
  581. letterSpacing: 1,
  582. paintOrder: 'stroke'
  583. });
  584. const deathsLength = Utils.measureSVGText(deaths.toString(), {
  585. fontFamily: 'Arial Black',
  586. fontSize: 14
  587. });
  588.  
  589. scaleAndTranslate = Utils.getSVGScaleAndTranslateToFit(UIConstants.TANK_INFO_MAX_NUMBER_WIDTH, deathsLength + 7, 34, 'left');
  590. TankTrouble.TankInfoBox.infoDeathsSvg.configure(deathsText, { transform: scaleAndTranslate });
  591. }, () => {}, () => {}, playerId, Caches.getPlayerDetailsCache());
  592. });
  593. })();