UX Improvements

Add many UI improvements and additions

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