Better In Game UI (#1243)
## Description: Top Bar Refactor – UI & UX Improvement Proposal This update overhauls the top game bar to improve clarity, responsiveness, and overall user experience across desktop and mobile. It consolidates player resources (e.g., building counts), integrates game controls (pause, settings, time), and enhances visual contrast. Key changes: Redesigned top bar with player data and game options. Team color indicator bar (team games only). Countdown bar during "Choose Starting Position" phase. Removed redundant info (e.g., troop/worker counts shown elsewhere). Inspired by strategy games like Travian Legends, this refactor offers a cleaner and more intuitive layout, especially for smaller screens. ⚠️ Note: This is a large change and likely contains visual or functional bugs I can’t currently spot due to fatigue. Thorough testing is required before approval. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [ ] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors ## Please put your Discord username so you can be contacted if a bug or regression is found: Diessel  om/user-attachments/assets/3a0edbef-e621-4fc4-b6b7-c1ed  8f9a8219)  Closes #1165 --------- Co-authored-by: Scott Anderson <scottanderson@users.noreply.github.com> Co-authored-by: evanpelle <evanpelle@gmail.com>
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#fefffe" d="M12 3a9 9 0 1 0 9 9c0-.46-.04-.92-.1-1.36a5.39 5.39 0 0 1-4.4 2.26a5.403 5.403 0 0 1-3.14-9.8c-.44-.06-.9-.1-1.36-.1"/></svg>
|
||||
|
After Width: | Height: | Size: 233 B |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 397 B |
@@ -0,0 +1 @@
|
||||
<svg class="svg-icon" style="vertical-align: middle;fill: currentColor;overflow: hidden; shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#fefffe" d="M19 3H5c-1.11 0-2 .89-2 2v4h2V5h14v14H5v-4H3v4a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2m-8.92 12.58L11.5 17l5-5l-5-5l-1.42 1.41L12.67 11H3v2h9.67z"/></svg>
|
||||
|
After Width: | Height: | Size: 472 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 512 512"><path fill="#fefffe" d="m293.324 20.738l-5.256 162.94L141.478 60.45l47.157 112.16L67.86 91.446L189.37 221.64l-168.2-63.073l153.6 146.003l-113.885 2.92l160.02 77.09l-127.9 14.602L196.91 435.81l-39.666 10.676l124.395 30.37l-36.208 20.44h244.12v-91.835l-74.345-188.675l74.346 107.865v-44.025l-20.112-154.42l-17.323 84.563L403.12 55.193L377.33 141L343.295 35.92l6.68 106.877l-56.65-122.06zm12.133 116.823l73.764 121.56l15.376-30.233l38.836 141.656l-73.946-103.967l9.682 76.022l-77.018-20.64l102.518 91.044l-33.8 6.584l93.515 53.76l-202.012-61.412l64.49-21.9l-113.56-57.97l74.012 13.08L156.25 242.18l126.86 37.164l-19.96-60.555l69.795 44.25l-27.488-125.48z"/></svg>
|
||||
|
After Width: | Height: | Size: 746 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#fefffe" d="M12 8c-2.21 0-4 1.79-4 4s1.79 4 4 4s4-1.79 4-4s-1.79-4-4-4m-7 7H3v4c0 1.1.9 2 2 2h4v-2H5zM5 5h4V3H5c-1.1 0-2 .9-2 2v4h2zm14-2h-4v2h4v4h2V5c0-1.1-.9-2-2-2m0 16h-4v2h4c1.1 0 2-.9 2-2v-4h-2z"/></svg>
|
||||
|
After Width: | Height: | Size: 304 B |
@@ -0,0 +1 @@
|
||||
<svg class="svg-icon" style="width: 1em; height: 1em;vertical-align: middle;fill: currentColor;overflow: hidden; shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#fefffe" d="M11 7h2v2h-2zm0 4h2v6h-2zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10s10-4.48 10-10S17.52 2 12 2m0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8s8 3.59 8 8s-3.59 8-8 8"/></svg>
|
||||
|
After Width: | Height: | Size: 491 B |
@@ -0,0 +1 @@
|
||||
<svg class="svg-icon" style="width: 1em; height: 1em;vertical-align: middle;fill: currentColor;overflow: hidden; shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#fefffe" d="M12 22c5.5 0 10-4.5 10-10S17.5 2 12 2S2 6.5 2 12s4.5 10 10 10M11 7h2v2h-2zm3 10h-4v-2h1v-2h-1v-2h3v4h1z"/></svg>
|
||||
|
After Width: | Height: | Size: 447 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#fefffe" d="M16 11V3H8v6H2v12h20V11zm-6-6h4v14h-4zm-6 6h4v8H4zm16 8h-4v-6h4z"/></svg>
|
||||
|
After Width: | Height: | Size: 181 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#fefffe" d="M7.5 21H2V9h5.5zm7.25-18h-5.5v18h5.5zM22 11h-5.5v10H22z"/></svg>
|
||||
|
After Width: | Height: | Size: 172 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#fefffe" d="M13 1.07V9h7c0-4.08-3.05-7.44-7-7.93M4 15c0 4.42 3.58 8 8 8s8-3.58 8-8v-4H4zm7-13.93C7.05 1.56 4 4.92 4 9h7z"/></svg>
|
||||
|
After Width: | Height: | Size: 225 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 512 512"><path fill="#fefffe" d="M255.063 21c-46.697 0-88.406 27.674-117.844 70.656c-29.44 42.982-47.25 101.566-47.25 166.094s17.81 123.112 47.25 166.094c29.437 42.982 71.146 70.656 117.843 70.656c46.696 0 88.405-27.674 117.843-70.656c29.44-42.982 47.25-101.567 47.25-166.094c0-64.528-17.81-123.112-47.25-166.094C343.468 48.674 301.76 21 255.062 21zM396.28 200.344c3.365 18.28 5.19 37.527 5.19 57.406c0 18.535-1.594 36.522-4.533 53.688c-37.91 12.904-87.436 20.812-141.656 20.812c-54.45 0-104.125-8.235-142.186-21.313c-2.884-17.014-4.438-34.833-4.438-53.187c0-19.868 1.827-39.103 5.188-57.375c37.903 14.565 87.35 23.25 141.47 23.25c54.136 0 103.183-8.707 140.967-23.28zM177.157 241c-15.137-.162-30.97 3.458-47.375 10.313c14.562 51.423 87.08 42.483 102.157 10.156c-17.004-13.822-35.318-20.262-54.78-20.47zm155.75 0c-19.462.208-37.808 6.648-54.812 20.47c15.078 32.326 87.596 41.266 102.156-10.158c-16.405-6.854-32.206-10.474-47.344-10.312"/></svg>
|
||||
|
After Width: | Height: | Size: 1021 B |
@@ -0,0 +1 @@
|
||||
<svg class="svg-icon" style="width: 1em; height: 1em;vertical-align: middle;fill: currentColor;overflow: hidden; shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#fefffe" d="M14 19h4V5h-4M6 19h4V5H6z"/></svg>
|
||||
|
After Width: | Height: | Size: 369 B |
@@ -0,0 +1 @@
|
||||
<svg class="svg-icon" style="width: 1em; height: 1em;vertical-align: middle;fill: currentColor;overflow: hidden; shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#fefffe" d="M8 5.14v14l11-7z"/></svg>
|
||||
|
After Width: | Height: | Size: 360 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#fefffe" d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4s-4 1.79-4 4s1.79 4 4 4m0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4"/></svg>
|
||||
|
After Width: | Height: | Size: 225 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#fefffe" d="M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6s-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8s-3.58-8-8-8"/></svg>
|
||||
|
After Width: | Height: | Size: 219 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#fefffe" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10s10-4.48 10-10S17.52 2 12 2m6 10c0 3.31-2.69 6-6 6s-6-2.69-6-6h2c0 2.21 1.79 4 4 4s4-1.79 4-4s-1.79-4-4-4v3L8 7l4-4v3c3.31 0 6 2.69 6 6"/></svg>
|
||||
|
After Width: | Height: | Size: 290 B |
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#fefffe" d="M16.5 13c-1.2 0-3.07.34-4.5 1c-1.43-.67-3.3-1-4.5-1C5.33 13 1 14.08 1 16.25V19h22v-2.75c0-2.17-4.33-3.25-6.5-3.25m-4 4.5h-10v-1.25c0-.54 2.56-1.75 5-1.75s5 1.21 5 1.75zm9 0H14v-1.25c0-.46-.2-.86-.52-1.22c.88-.3 1.96-.53 3.02-.53c2.44 0 5 1.21 5 1.75zM7.5 12c1.93 0 3.5-1.57 3.5-3.5S9.43 5 7.5 5S4 6.57 4 8.5S5.57 12 7.5 12m0-5.5c1.1 0 2 .9 2 2s-.9 2-2 2s-2-.9-2-2s.9-2 2-2m9 5.5c1.93 0 3.5-1.57 3.5-3.5S18.43 5 16.5 5S13 6.57 13 8.5s1.57 3.5 3.5 3.5m0-5.5c1.1 0 2 .9 2 2s-.9 2-2 2s-2-.9-2-2s.9-2 2-2"/></svg>
|
||||
|
After Width: | Height: | Size: 616 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#fefffe" d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5s-3 1.34-3 3s1.34 3 3 3m-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5S5 6.34 5 8s1.34 3 3 3m0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5m8 0c-.29 0-.62.02-.97.05c1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5"/></svg>
|
||||
|
After Width: | Height: | Size: 388 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 512 512"><path fill="#fefffe" d="M249.28 19.188v.25c-18.114 38.634-45.065 72.36-77.686 102.937l37.72-3.938l-51.345 65.032l24.81-7.907l-33.624 54.875l16.53 9.843l-65.25 92.157l36.095.188l-51.686 83.594l63.562-8.126l12 32.094l66.438-25.282L215.5 493.28h52.938l-6.532-68.217l38.188 16.406l10.187-24.783l44.283 20.97l56.406-20.75l-37.064-64.094l-12.437-2.282l6.78 17.19l7.844 19.905l-19.938-7.78l-50.906-19.908v35.751l-14.156-8.594l-69.375-42l-21.595 21.25l-18.03 17.75l2.155-25.22l2.125-24.655l18.188 1.56l9.218-9.092l5.19-5.094l6.218 3.75l61.375 37.156v-29.906l12.75 4.97l43.718 17.092l-5.092-12.906l-6.157-15.656l16.533 3.03l45.468 8.345l-34.53-38.94l-23.625 14.033l-6.688 3.968l-5.125-5.874l-14.28-16.437l.218 1.217l-18.406 3.22l-5.97-34.313l-5.75-33.063l22 25.345l31.188 35.875l43.907-26.03c-24.67-19.543-39.507-33.87-49.658-48.814l.813 12.656l1.97 31l-18.75-24.75l-34.47-45.437l-22.25 46.813l-13.844 29.125l-3.843-32.032l-3.5-28.843l16.532-1.968l16.624-34.97l6.594-13.875l9.28 12.22l25 32.936l-.75-11.53l-.906-14.28l13.47 4.936L341.81 188l-26.125-35.156l-55.843-28.875l-8.938 20.218l-9.656 21.937l-7.72-22.688l-7.468-21.875l16.97-5.78l3.718-8.438l4-9.125l8.844 4.593l49.375 25.53l16.467-5.562c-43.42-34.31-64.63-68.886-76.156-103.593z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,4 @@
|
||||
<svg class="svg-icon" style="width: 1em; height: 1em;vertical-align: middle;fill: currentColor;overflow: hidden; shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path fill="#fefffe"
|
||||
d="M4.636,2.1C4.325,1.011,5.18-.06,6.311,.003l6.002,.51c.831,.046,1.388,.873,1.118,1.661l-.319,.826H5.115l-.479-.9Zm5.163,18.9h4.201v3H5v-5.884l-2.145,4.527L.145,21.357l3.688-7.784c.74-1.563,2.336-2.573,4.066-2.573h5.102v5.853l-3.201,4.147Zm-1.799-2.794l3.368-4.206h-3.368v4.206Zm1-9.206c2.209,0,4-1.791,4-4H5c0,2.209,1.791,4,4,4Zm10.005,4.836c2.382-.504,4.995-2.225,4.995-5.836h-3c0,1.679-1.037,2.417-2,2.743V0h-3V1h-1V3h1v6h-1v9h1v6h5v-2l-.977-4h2.977v-3h-3.711l-.285-1.164Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 825 B |
@@ -0,0 +1 @@
|
||||
<svg class="svg-icon" style="width: 1em; height: 1em;vertical-align: middle;fill: currentColor;overflow: hidden;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path fill="#fefffe" d="M136.48 27.746c-2.108.024-4.174.152-6.242.272c42.927 23.035 87.233 59.434 121.902 96.57c8.66 9.276 12.358 18.765 16.371 27.44c2.486 5.37 5.173 10.658 9.297 16.37l30.65-26.373c-3.067-5.031-5.213-10.567-6.044-16.386c-.919-6.432.09-13.283 3.039-19.48c-32.92-24.035-68.653-47.25-102.75-62.026c-22.779-9.871-44.547-15.843-64.1-16.371q-1.07-.029-2.123-.016m226.463 99.256c-2.825 0-5.562.505-8.092 1.293l28.91 28.91c.788-2.53 1.293-5.266 1.293-8.092c0-6.485-2.314-12.726-5.85-16.262s-9.776-5.85-16.261-5.85zm-23.088 11.754l-21.89 18.836c9.951-2.533 20.985.059 28.712 7.787s10.32 18.761 7.788 28.713l18.835-21.89zm-14.39 35.78c-3.053 0-6.104 1.189-8.485 3.57c-4.762 4.761-4.762 12.208 0 16.97s12.21 4.762 16.97 0c4.763-4.762 4.763-12.209 0-16.97c-2.38-2.381-5.433-3.57-8.485-3.57m-27.582.335l-9.846 8.47l-5.352 46.03l46.03-5.352l8.468-9.841c-10.923 4.588-24.09 2.467-32.931-6.373c-8.842-8.842-10.959-22.01-6.37-32.934zm72.148 28.727l-26.373 30.65c5.712 4.124 11 6.812 16.371 9.297c8.674 4.013 18.163 7.711 27.44 16.37c37.136 34.67 73.534 78.977 96.57 121.903c1.254-21.638-4.803-46.36-16.115-72.465c-14.776-34.097-37.992-69.829-62.026-102.75c-6.198 2.95-13.049 3.958-19.48 3.04c-5.82-.832-11.355-2.978-16.387-6.045m-103.375 7.79l-28.398 26.588L274.08 273.8l26.588-28.399l-38.489 4.477zm-41.545 38.897l-11.686 10.941l37.405 37.405l10.941-11.686zm-24.832 23.252l-90.564 84.797l44.007 44.008l84.797-90.565zM96.566 370.643l-21.91 20.515l46.242 46.242l20.516-21.91zm-28.09 39.79l-5.656 16.971l21.832 21.832l16.97-5.656zm-18.789 29.295l-18.49 9.686a106 106 0 0 0-2.746 13.676c-.608 4.548-.852 9.29-.469 12.92s1.496 5.735 1.912 6.152c.417.417 2.523 1.53 6.153 1.912c3.63.383 8.372.139 12.92-.469a106 106 0 0 0 13.675-2.746l9.686-18.49z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,4 @@
|
||||
<svg class="svg-icon" style="width: 1em; height: 1em;vertical-align: middle;fill: currentColor;overflow: hidden; shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path fill="#fefffe"
|
||||
d="m21 11v3.078c0 1.103-.897 2-2 2s-2-.897-2-2v-3.078c-1.764.772-3 2.529-3 4.578s1.236 3.806 3 4.578v3.844h4v-3.844c1.764-.772 3-2.529 3-4.578s-1.236-3.806-3-4.578zm-19-2.023h12c.005 3.293-2.689 6.004-6 6-3.311.004-6.005-2.707-6-6zm13 15.023h-15v-3c0-2.761 2.239-5 5-5h7.026c.13 2.149 1.232 4.104 2.974 5.317zm0-17.023h-14v-2h1.023c.239-2.634 2.343-4.738 4.977-4.977v3.977h2v-3.977c2.634.239 4.738 2.343 4.977 4.977h1.023z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 771 B |
@@ -214,7 +214,7 @@
|
||||
"emojis_desc": "تبديل ما إذا كانت الرموز التعبيرية تظهر في اللعبة",
|
||||
"anonymous_names_label": "🥷 الأسماء المخفية",
|
||||
"anonymous_names_desc": "إخفاء أسماء اللاعبين الحقيقية باستخدام أسماء عشوائية على شاشتك",
|
||||
"left_click_label": "🖱️ النقر الأيسر لفتح القائمة",
|
||||
"left_click_label": " النقر الأيسر لفتح القائمة",
|
||||
"left_click_desc": "عند التفعيل، النقر الأيسر يفتح القائمة وزر السيف للهجوم. عند التعطيل، النقر الأيسر يهاجم مباشرة",
|
||||
"attack_ratio_label": "⚔️ نسبة الهجوم",
|
||||
"attack_ratio_desc": "نسبة القوات المُرسلة في الهجوم (1–100%)",
|
||||
|
||||
@@ -230,7 +230,7 @@
|
||||
"special_effects_desc": "Превключване на специалните ефекти. Деактивиране, за да се увеличи производителността",
|
||||
"anonymous_names_label": "🥷 Скрити имена",
|
||||
"anonymous_names_desc": "Скриване на истинските имена на играчите с произволни такива на екрана ти.",
|
||||
"left_click_label": "🖱️ Щтракване на ляв бутон, за да се отвори менюто",
|
||||
"left_click_label": "Щтракване на ляв бутон, за да се отвори менюто",
|
||||
"left_click_desc": "Когато е ВКЛЮЧЕНО, щракването с ляв бутон отваря менюто и атаките се извършват чрез бутона на меч. Когато е ИЗКЛЮЧЕНО, щракването с ляв бутон атакува директно.",
|
||||
"attack_ratio_label": "⚔️ Съотношение на атака",
|
||||
"attack_ratio_desc": "Какъв процент от Вашите войници да се изпратят в атака (1–100%)",
|
||||
|
||||
@@ -230,7 +230,7 @@
|
||||
"special_effects_desc": "Určuje zda jsou zapnuté speciální efekty. Deaktivuj pro zlepšení výkonu",
|
||||
"anonymous_names_label": "🥷 Skrýt jména",
|
||||
"anonymous_names_desc": "Skrýt skutečná jména hráčů a nahradit je náhodnými názvy na vaší obrazovce.",
|
||||
"left_click_label": "🖱️ Levým tlačítkem myši Otevřít Menu",
|
||||
"left_click_label": "Levým tlačítkem myši Otevřít Menu",
|
||||
"left_click_desc": "Pokud je zapnuto otevře \"Levé tlačítko\" myši menu a \"Útok\" tlačítko zaútočí. Pokud je vypnuto, útočí se přímo levým tlačítkem myši.",
|
||||
"attack_ratio_label": "⚔️ Poměr útoku",
|
||||
"attack_ratio_desc": "Jaké procento tvých vojáků poslat při útoku (1–100%)",
|
||||
|
||||
@@ -247,17 +247,17 @@
|
||||
"title": "User Settings",
|
||||
"tab_basic": "Basic Settings",
|
||||
"tab_keybinds": "Keybinds",
|
||||
"dark_mode_label": "🌙 Dark Mode",
|
||||
"dark_mode_label": "Dark Mode",
|
||||
"dark_mode_desc": "Toggle the site’s appearance between light and dark themes",
|
||||
"emojis_label": "😊 Emojis",
|
||||
"emojis_label": "Emojis",
|
||||
"emojis_desc": "Toggle whether emojis are shown in game",
|
||||
"alert_frame_label": "🚨 Alert Frame",
|
||||
"alert_frame_label": "Alert Frame",
|
||||
"alert_frame_desc": "Toggle the alert frame. When enabled, the frame will be displayed when you are betrayed.",
|
||||
"special_effects_label": "💥 Special effects",
|
||||
"special_effects_label": "Special effects",
|
||||
"special_effects_desc": "Toggle special effects. Deactivate to improve performances",
|
||||
"anonymous_names_label": "🥷 Hidden Names",
|
||||
"anonymous_names_label": "Hidden Names",
|
||||
"anonymous_names_desc": "Hide real player names with random ones on your screen.",
|
||||
"left_click_label": "🖱️ Left Click to Open Menu",
|
||||
"left_click_label": "Left Click to Open Menu",
|
||||
"left_click_desc": "When ON, left-click opens menu and sword button attacks. When OFF, left-click attacks directly.",
|
||||
"attack_ratio_label": "⚔️ Attack Ratio",
|
||||
"attack_ratio_desc": "What percentage of your troops to send in an attack (1–100%)",
|
||||
|
||||
@@ -230,7 +230,7 @@
|
||||
"special_effects_desc": "Baskuli specialajn efektojn. Malaktivigu por plibonigi funkciadon",
|
||||
"anonymous_names_label": "🥷 Maskitaj Nomoj",
|
||||
"anonymous_names_desc": "Kaŝi verajn uzantnomojn kun hazardaj sur via ekrano.",
|
||||
"left_click_label": "🖱️Maldekstra alklako por malfermi menuon",
|
||||
"left_click_label": "Maldekstra alklako por malfermi menuon",
|
||||
"left_click_desc": "Kiam aktiviga, maldekstra alklako malfermas menuon kaj glava atakbutono. Kiam malaktiviga, maldekstra alklako atakas direkten.",
|
||||
"attack_ratio_label": "⚔️ Atakkvociento",
|
||||
"attack_ratio_desc": "Kian procenton de viaj trupoj sendi en atako (1–100%)",
|
||||
|
||||
@@ -230,7 +230,7 @@
|
||||
"special_effects_desc": "Activa los efectos especiales. Desactiva para mejorar el rendimiento",
|
||||
"anonymous_names_label": "🥷 Nombres ocultos",
|
||||
"anonymous_names_desc": "Oculta los nombres de jugadores reales con nombres aleatorios en tu pantalla.",
|
||||
"left_click_label": "🖱️ Clic izquierdo para abrir el menú",
|
||||
"left_click_label": "Clic izquierdo para abrir el menú",
|
||||
"left_click_desc": "Cuando está activado, el clic izquierdo abre el menú y el botón de espada ataca. Cuando está desactivado, el clic izquierdo ataca directamente.",
|
||||
"attack_ratio_label": "⚔️ Ratio de ataque",
|
||||
"attack_ratio_desc": "Porcentaje de tus tropas que se envían en un ataque (1–100%)",
|
||||
|
||||
@@ -230,7 +230,7 @@
|
||||
"special_effects_desc": "Activer/désactiver les effets spéciaux. Désactiver pour améliorer les performances",
|
||||
"anonymous_names_label": "🥷 Noms masqués",
|
||||
"anonymous_names_desc": "Cacher le vrai nom des joueurs avec des noms aléatoires sur votre écran.",
|
||||
"left_click_label": "🖱️ Clic gauche pour ouvrir le menu",
|
||||
"left_click_label": "Clic gauche pour ouvrir le menu",
|
||||
"left_click_desc": "Activé, un clic gauche ouvre le menu et le bouton épée d'attaque. Désactivé, un clic gauche attaque directement.",
|
||||
"attack_ratio_label": "⚔️ Ratio d'attaque",
|
||||
"attack_ratio_desc": "Quel pourcentage de vos troupes envoyer dans une attaque (1–100%)",
|
||||
|
||||
@@ -230,7 +230,7 @@
|
||||
"special_effects_desc": "מאפשר הצגה של אפקטים מיוחדים. בטל כדי לשפר את הביצועים של המשחק.",
|
||||
"anonymous_names_label": "🥷 שמות חבויים",
|
||||
"anonymous_names_desc": "החלף את שמות השחקנים האמיתיים עם שמות רנדומליים במסך שלך.",
|
||||
"left_click_label": "לחצן שמאלי לפתיחת תפריט 🖱️",
|
||||
"left_click_label": "לחצן שמאלי לפתיחת תפריט",
|
||||
"left_click_desc": "כאשר מופעל, הלחצן השמאלי פותח את התפריט וכפתור החרב תוקף. כאשר מכובה, הלחצן השמאלי תוקף ישירות.",
|
||||
"attack_ratio_label": "יחס תקיפה ⚔️",
|
||||
"attack_ratio_desc": "כמה אחוז מהחיילים שלך לשלוח בהתקפה (1-100%)",
|
||||
|
||||
@@ -230,7 +230,7 @@
|
||||
"special_effects_desc": "Attiva/disattiva gli effetti speciali. Disattiva per migliorare le prestazioni",
|
||||
"anonymous_names_label": "🥷 Nomi Nascosti",
|
||||
"anonymous_names_desc": "Nascondi i nomi dei giocatori reali sullo schermo con nomi casuali.",
|
||||
"left_click_label": "🖱️ Clic sinistro per aprire il menu",
|
||||
"left_click_label": "Clic sinistro per aprire il menu",
|
||||
"left_click_desc": "Quando attivo, il clic sinistro apre il menu e il pulsante di attacco. Quando non attivo, fare clic con il tasto sinistro attaccherà direttamente.",
|
||||
"attack_ratio_label": "⚔️ Rapporto Di Attacco",
|
||||
"attack_ratio_desc": "Quale percentuale delle tue truppe inviare in attacco (1–100%)",
|
||||
|
||||
@@ -230,7 +230,7 @@
|
||||
"special_effects_desc": "Speciale effecten aanzetten. Zet uit om de prestaties van het spel te verbeteren",
|
||||
"anonymous_names_label": "🥷 Verborgen namen",
|
||||
"anonymous_names_desc": "Vervang echte spelersnamen door willekeurige namen op je scherm.",
|
||||
"left_click_label": "🖱️ Linkermuisknop voor openen menu",
|
||||
"left_click_label": "Linkermuisknop voor openen menu",
|
||||
"left_click_desc": "Als AAN: linkermuisknop opent het Radiale menu met zwaard-aanvalsknop. Als UIT: linkermuisknop opent direct de aanval.",
|
||||
"attack_ratio_label": "⚔️ Aanvalsverhouding",
|
||||
"attack_ratio_desc": "Welk percentage van je troepen je bij een aanval stuurt (1-100%)",
|
||||
|
||||
@@ -214,7 +214,7 @@
|
||||
"emojis_desc": "Przełącz, czy emotki mają być wyświetlane w grze",
|
||||
"anonymous_names_label": "🥷 Ukryte Nazwy",
|
||||
"anonymous_names_desc": "Ukryj prawdziwe nazwy graczy, zastępując je losowymi na swoim ekranie.",
|
||||
"left_click_label": "🖱️ Kliknij lewym przyciskiem myszy aby otworzyć Menu",
|
||||
"left_click_label": "Kliknij lewym przyciskiem myszy aby otworzyć Menu",
|
||||
"left_click_desc": "Gdy WŁĄCZONE, lewy przycisk myszy otwiera menu, a przycisk z mieczem atakuje. Gdy WYŁĄCZONE, lewy przycisk myszy atakuje bezpośrednio.",
|
||||
"attack_ratio_label": "⚔️ Współczynnik ataku",
|
||||
"attack_ratio_desc": "Jaki procent swoich żołnierzy chcesz wysłać do ataku (1–100%)",
|
||||
|
||||
@@ -230,7 +230,7 @@
|
||||
"special_effects_desc": "Переключить специальные эффекты. Отключите для улучшения производительности",
|
||||
"anonymous_names_label": "🥷 Скрытые имена",
|
||||
"anonymous_names_desc": "Скрыть настоящие имена игроков и заменить их случайными.",
|
||||
"left_click_label": "🖱️ Открытие меню левой кнопкой мыши",
|
||||
"left_click_label": "Открытие меню левой кнопкой мыши",
|
||||
"left_click_desc": "ВКЛЮЧЕНО: щелчок левой кнопкой мыши открывает меню, атака совершается кнопкой с мечом. ВЫКЛЮЧЕНО: нажатие левой кнопкой мыши совершает атаку напрямую.",
|
||||
"attack_ratio_label": "⚔️ Соотношение атаки",
|
||||
"attack_ratio_desc": "Какой процент ваших войск отправлять в бой (1–100%)",
|
||||
|
||||
@@ -214,7 +214,7 @@
|
||||
"emojis_desc": "o ante e lon pi sitelen Emosi",
|
||||
"anonymous_names_label": "🥷 nimi len",
|
||||
"anonymous_names_desc": "o len e nimi lon pi jan musi kepeken nimi nasa lon musi sina",
|
||||
"left_click_label": "🖱️ ilo luka soto li open e lipu ken",
|
||||
"left_click_label": "ilo luka soto li open e lipu ken",
|
||||
"left_click_desc": "ni li lon la ilo luka soto li open e lipu ken la ilo luka utala li utala. ni li pini la ilo luka soto li utala taso.",
|
||||
"attack_ratio_label": "⚔️ nanpa utala",
|
||||
"attack_ratio_desc": "sina utala kepeken kulupu utala la o pana e kulupu suli ni (kipisi wan tawa wan ale)",
|
||||
|
||||
@@ -230,7 +230,7 @@
|
||||
"special_effects_desc": "Перемкнути спеціальні ефекти. Вимкніть для поліпшення продуктивності",
|
||||
"anonymous_names_label": "🥷 Приховані імена",
|
||||
"anonymous_names_desc": "Приховати справжні імена гравців і замінити їх випадковими.",
|
||||
"left_click_label": "🖱️ Відкриття меню лівою кнопкою миші",
|
||||
"left_click_label": "Відкриття меню лівою кнопкою миші",
|
||||
"left_click_desc": "УВІМКНЕНО: лівий клац відкриває меню, атака здійснюється кнопкою з мечем. ВИМКНЕНО: лівий клац здійснює атаку напряму.",
|
||||
"attack_ratio_label": "⚔️ Співвідношення атаки",
|
||||
"attack_ratio_desc": "Який відсоток ваших військ відправляти в напад (1–100%)",
|
||||
|
||||
@@ -366,7 +366,7 @@ class Client {
|
||||
"host-lobby-modal",
|
||||
"join-private-lobby-modal",
|
||||
"game-starting-modal",
|
||||
"top-bar",
|
||||
"game-top-bar",
|
||||
"help-modal",
|
||||
"user-setting",
|
||||
].forEach((tag) => {
|
||||
|
||||
@@ -14,6 +14,8 @@ import { EmojiTable } from "./layers/EmojiTable";
|
||||
import { EventsDisplay } from "./layers/EventsDisplay";
|
||||
import { FxLayer } from "./layers/FxLayer";
|
||||
import { GameLeftSidebar } from "./layers/GameLeftSidebar";
|
||||
import { GameRightSidebar } from "./layers/GameRightSidebar";
|
||||
import { GameTopBar } from "./layers/GameTopBar";
|
||||
import { GutterAdModal } from "./layers/GutterAdModal";
|
||||
import { HeadsUpMessage } from "./layers/HeadsUpMessage";
|
||||
import { Layer } from "./layers/Layer";
|
||||
@@ -21,7 +23,6 @@ import { Leaderboard } from "./layers/Leaderboard";
|
||||
import { MainRadialMenu } from "./layers/MainRadialMenu";
|
||||
import { MultiTabModal } from "./layers/MultiTabModal";
|
||||
import { NameLayer } from "./layers/NameLayer";
|
||||
import { OptionsMenu } from "./layers/OptionsMenu";
|
||||
import { PlayerInfoOverlay } from "./layers/PlayerInfoOverlay";
|
||||
import { PlayerPanel } from "./layers/PlayerPanel";
|
||||
import { RailroadLayer } from "./layers/RailroadLayer";
|
||||
@@ -33,7 +34,6 @@ import { StructureLayer } from "./layers/StructureLayer";
|
||||
import { TeamStats } from "./layers/TeamStats";
|
||||
import { TerrainLayer } from "./layers/TerrainLayer";
|
||||
import { TerritoryLayer } from "./layers/TerritoryLayer";
|
||||
import { TopBar } from "./layers/TopBar";
|
||||
import { UILayer } from "./layers/UILayer";
|
||||
import { UnitInfoModal } from "./layers/UnitInfoModal";
|
||||
import { UnitLayer } from "./layers/UnitLayer";
|
||||
@@ -135,25 +135,28 @@ export function createRenderer(
|
||||
winModal.eventBus = eventBus;
|
||||
winModal.game = game;
|
||||
|
||||
const optionsMenu = document.querySelector("options-menu") as OptionsMenu;
|
||||
if (!(optionsMenu instanceof OptionsMenu)) {
|
||||
console.error("options menu not found");
|
||||
}
|
||||
optionsMenu.eventBus = eventBus;
|
||||
optionsMenu.game = game;
|
||||
|
||||
const replayPanel = document.querySelector("replay-panel") as ReplayPanel;
|
||||
if (!(replayPanel instanceof ReplayPanel)) {
|
||||
console.error("ReplayPanel element not found in the DOM");
|
||||
console.error("replay panel not found");
|
||||
}
|
||||
replayPanel.eventBus = eventBus;
|
||||
replayPanel.game = game;
|
||||
|
||||
const topBar = document.querySelector("top-bar") as TopBar;
|
||||
if (!(topBar instanceof TopBar)) {
|
||||
const gameRightSidebar = document.querySelector(
|
||||
"game-right-sidebar",
|
||||
) as GameRightSidebar;
|
||||
if (!(gameRightSidebar instanceof GameRightSidebar)) {
|
||||
console.error("Game Right bar not found");
|
||||
}
|
||||
gameRightSidebar.game = game;
|
||||
gameRightSidebar.eventBus = eventBus;
|
||||
|
||||
const gameTopBar = document.querySelector("game-top-bar") as GameTopBar;
|
||||
if (!(gameTopBar instanceof GameTopBar)) {
|
||||
console.error("top bar not found");
|
||||
}
|
||||
topBar.game = game;
|
||||
gameTopBar.game = game;
|
||||
gameTopBar.eventBus = eventBus;
|
||||
|
||||
const playerPanel = document.querySelector("player-panel") as PlayerPanel;
|
||||
if (!(playerPanel instanceof PlayerPanel)) {
|
||||
@@ -248,13 +251,13 @@ export function createRenderer(
|
||||
new SpawnTimer(game, transformHandler),
|
||||
leaderboard,
|
||||
gameLeftSidebar,
|
||||
gameTopBar,
|
||||
gameRightSidebar,
|
||||
controlPanel,
|
||||
playerInfo,
|
||||
winModal,
|
||||
optionsMenu,
|
||||
replayPanel,
|
||||
teamStats,
|
||||
topBar,
|
||||
playerPanel,
|
||||
headsUpMessage,
|
||||
unitInfoModal,
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
@customElement("leaderboard-regular-icon")
|
||||
export class LeaderboardRegularIcon extends LitElement {
|
||||
@property({ type: String }) size = "24"; // Accepts "24", "32", etc.
|
||||
@property({ type: String }) color = "currentColor";
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
svg {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="${this.size}"
|
||||
height="${this.size}"
|
||||
viewBox="0 0 24 24"
|
||||
fill="${this.color}"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M4 19h4v-8H4zm6 0h4V5h-4zm6 0h4v-6h-4zM2 21V9h6V3h8v8h6v10z"
|
||||
/>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
@customElement("leaderboard-solid-icon")
|
||||
export class LeaderboardSolidIcon extends LitElement {
|
||||
@property({ type: String }) size = "24"; // Accepts "24", "32", etc.
|
||||
@property({ type: String }) color = "currentColor";
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
svg {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="${this.size}"
|
||||
height="${this.size}"
|
||||
viewBox="0 0 24 24"
|
||||
fill="${this.color}"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M2 21V9h5.5v12zm7.25 0V3h5.5v18zm7.25 0V11H22v10z"
|
||||
/>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
@customElement("team-regular-icon")
|
||||
export class TeamRegularIcon extends LitElement {
|
||||
@property({ type: String }) size = "24"; // Accepts "24", "32", etc.
|
||||
@property({ type: String }) color = "currentColor";
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
svg {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="${this.size}"
|
||||
height="${this.size}"
|
||||
viewBox="0 0 24 24"
|
||||
fill="${this.color}"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M9 13.75c-2.34 0-7 1.17-7 3.5V19h14v-1.75c0-2.33-4.66-3.5-7-3.5M4.34 17c.84-.58 2.87-1.25 4.66-1.25s3.82.67 4.66 1.25zM9 12c1.93 0 3.5-1.57 3.5-3.5S10.93 5 9 5S5.5 6.57 5.5 8.5S7.07 12 9 12m0-5c.83 0 1.5.67 1.5 1.5S9.83 10 9 10s-1.5-.67-1.5-1.5S8.17 7 9 7m7.04 6.81c1.16.84 1.96 1.96 1.96 3.44V19h4v-1.75c0-2.02-3.5-3.17-5.96-3.44M15 12c1.93 0 3.5-1.57 3.5-3.5S16.93 5 15 5c-.54 0-1.04.13-1.5.35c.63.89 1 1.98 1 3.15s-.37 2.26-1 3.15c.46.22.96.35 1.5.35"
|
||||
/>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
@customElement("team-solid-icon")
|
||||
export class TeamSolidIcon extends LitElement {
|
||||
@property({ type: String }) size = "24"; // Accepts "24", "32", etc.
|
||||
@property({ type: String }) color = "currentColor";
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
svg {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="${this.size}"
|
||||
height="${this.size}"
|
||||
viewBox="0 0 24 24"
|
||||
fill="${this.color}"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5s-3 1.34-3 3s1.34 3 3 3m-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5S5 6.34 5 8s1.34 3 3 3m0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5m8 0c-.29 0-.62.02-.97.05c1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5"
|
||||
/>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,11 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import factoryIcon from "../../../../resources/images/FactoryIconWhite.svg";
|
||||
import { translateText } from "../../../client/Utils";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
import { Gold, UnitType } from "../../../core/game/Game";
|
||||
import { GameView } from "../../../core/game/GameView";
|
||||
import { AttackRatioEvent } from "../../InputHandler";
|
||||
import { SendSetTargetTroopRatioEvent } from "../../Transport";
|
||||
import { renderNumber, renderTroops } from "../../Utils";
|
||||
import { renderTroops } from "../../Utils";
|
||||
import { UIState } from "../UIState";
|
||||
import { Layer } from "./Layer";
|
||||
|
||||
@@ -29,37 +27,13 @@ export class ControlPanel extends LitElement implements Layer {
|
||||
@state()
|
||||
private _population: number;
|
||||
|
||||
@state()
|
||||
private _maxPopulation: number;
|
||||
|
||||
@state()
|
||||
private popRate: number;
|
||||
|
||||
@state()
|
||||
private _troops: number;
|
||||
|
||||
@state()
|
||||
private _workers: number;
|
||||
|
||||
@state()
|
||||
private _isVisible = false;
|
||||
|
||||
@state()
|
||||
private _manpower: number = 0;
|
||||
|
||||
@state()
|
||||
private _gold: Gold;
|
||||
|
||||
@state()
|
||||
private _goldPerSecond: Gold;
|
||||
|
||||
@state()
|
||||
private _factories: number;
|
||||
|
||||
private _lastPopulationIncreaseRate: number;
|
||||
|
||||
private _popRateIsIncreasing: boolean = true;
|
||||
|
||||
private init_: boolean = false;
|
||||
|
||||
init() {
|
||||
@@ -118,22 +92,13 @@ export class ControlPanel extends LitElement implements Layer {
|
||||
|
||||
const popIncreaseRate = player.population() - this._population;
|
||||
if (this.game.ticks() % 5 === 0) {
|
||||
this._popRateIsIncreasing =
|
||||
popIncreaseRate >= this._lastPopulationIncreaseRate;
|
||||
this._lastPopulationIncreaseRate = popIncreaseRate;
|
||||
}
|
||||
|
||||
this._population = player.population();
|
||||
this._maxPopulation = this.game.config().maxPopulation(player);
|
||||
this._gold = player.gold();
|
||||
this._troops = player.troops();
|
||||
this._workers = player.workers();
|
||||
this.popRate = this.game.config().populationIncreaseRate(player) * 10;
|
||||
this._goldPerSecond = this.game.config().goldAdditionRate(player) * 10n;
|
||||
|
||||
this.currentTroopRatio = player.troops() / player.population();
|
||||
this.requestUpdate();
|
||||
this._factories = player.units(UnitType.Factory).length;
|
||||
}
|
||||
|
||||
onAttackRatioChange(newRatio: number) {
|
||||
@@ -209,51 +174,21 @@ export class ControlPanel extends LitElement implements Layer {
|
||||
</style>
|
||||
<div
|
||||
class="${this._isVisible
|
||||
? "w-[320px] text-sm lg:text-m bg-gray-800/70 p-2 pr-3 lg:p-4 shadow-lg lg:rounded-lg backdrop-blur"
|
||||
? "text-sm lg:text-m md:w-[320px] bg-gray-800/70 p-2 pr-3 lg:p-4 shadow-lg lg:rounded-lg backdrop-blur"
|
||||
: "hidden"}"
|
||||
@contextmenu=${(e) => e.preventDefault()}
|
||||
>
|
||||
<div class="hidden lg:block bg-black/30 text-white mb-4 p-2 rounded">
|
||||
<div class="flex justify-between mb-1">
|
||||
<span class="font-bold"
|
||||
>${translateText("control_panel.pop")}:</span
|
||||
>
|
||||
<span translate="no"
|
||||
>${renderTroops(this._population)} /
|
||||
${renderTroops(this._maxPopulation)}
|
||||
<span
|
||||
class="${this._popRateIsIncreasing
|
||||
? "text-green-500"
|
||||
: "text-yellow-500"}"
|
||||
translate="no"
|
||||
>(+${renderTroops(this.popRate)})</span
|
||||
></span
|
||||
>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="font-bold"
|
||||
>${translateText("control_panel.gold")}:</span
|
||||
>
|
||||
<span translate="no"
|
||||
>${renderNumber(this._gold)}
|
||||
(+${renderNumber(this._goldPerSecond)}
|
||||
${renderNumber(this._factories)}
|
||||
<img
|
||||
src="${factoryIcon}"
|
||||
style="display: inline"
|
||||
width="15"
|
||||
/>)</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative mb-4 lg:mb-4">
|
||||
<label class="block text-white mb-1" translate="no"
|
||||
>${translateText("control_panel.troops")}:
|
||||
<span translate="no">${renderTroops(this._troops)}</span> |
|
||||
${translateText("control_panel.workers")}:
|
||||
<span translate="no">${renderTroops(this._workers)}</span></label
|
||||
>
|
||||
<label class="flex justify-between text-white mb-1" translate="no">
|
||||
<span>
|
||||
${translateText("control_panel.troops")}:
|
||||
${(this.currentTroopRatio * 100).toFixed(0)}%
|
||||
</span>
|
||||
<span>
|
||||
${translateText("control_panel.workers")}:
|
||||
${((1 - this.currentTroopRatio) * 100).toFixed(0)}%
|
||||
</span>
|
||||
</label>
|
||||
<div class="relative h-8">
|
||||
<!-- Background track -->
|
||||
<div
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Colord } from "colord";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import leaderboardRegularIcon from "../../../../resources/images/LeaderboardIconRegularWhite.svg";
|
||||
import leaderboardSolidIcon from "../../../../resources/images/LeaderboardIconSolidWhite.svg";
|
||||
import teamRegularIcon from "../../../../resources/images/TeamIconRegularWhite.svg";
|
||||
import teamSolidIcon from "../../../../resources/images/TeamIconSolidWhite.svg";
|
||||
import { GameMode } from "../../../core/game/Game";
|
||||
import { GameView } from "../../../core/game/GameView";
|
||||
import { translateText } from "../../Utils";
|
||||
import "../icons/LeaderboardRegularIcon";
|
||||
import "../icons/LeaderboardSolidIcon";
|
||||
import "../icons/TeamRegularIcon";
|
||||
import "../icons/TeamSolidIcon";
|
||||
import { Layer } from "./Layer";
|
||||
|
||||
@customElement("game-left-sidebar")
|
||||
@@ -77,7 +77,7 @@ export class GameLeftSidebar extends LitElement implements Layer {
|
||||
render() {
|
||||
return html`
|
||||
<aside
|
||||
class=${`fixed top-[50px] lg:top-[10px] left-0 z-[1000] flex flex-col max-h-[calc(100vh-80px)] overflow-y-auto p-2 bg-slate-800/40 backdrop-blur-sm shadow-xs rounded-tr-lg rounded-br-lg transition-transform duration-300 ease-out transform ${
|
||||
class=${`fixed top-[90px] left-0 z-[1000] flex flex-col max-h-[calc(100vh-80px)] overflow-y-auto p-2 bg-slate-800/40 backdrop-blur-sm shadow-xs rounded-tr-lg rounded-br-lg transition-transform duration-300 ease-out transform ${
|
||||
this.isVisible ? "translate-x-0" : "-translate-x-full"
|
||||
}`}
|
||||
>
|
||||
@@ -94,11 +94,20 @@ export class GameLeftSidebar extends LitElement implements Layer {
|
||||
</div>
|
||||
`
|
||||
: null}
|
||||
<div class="flex items-center gap-2 space-x-2 text-white mb-2">
|
||||
<div
|
||||
class=${`flex items-center gap-2 space-x-2 text-white ${
|
||||
this.isLeaderboardShow || this.isTeamLeaderboardShow ? "mb-2" : ""
|
||||
}`}
|
||||
>
|
||||
<div class="w-6 h-6 cursor-pointer" @click=${this.toggleLeaderboard}>
|
||||
${this.isLeaderboardShow
|
||||
? html` <leaderboard-solid-icon></leaderboard-solid-icon>`
|
||||
: html` <leaderboard-regular-icon></leaderboard-regular-icon>`}
|
||||
<img
|
||||
src=${this.isLeaderboardShow
|
||||
? leaderboardSolidIcon
|
||||
: leaderboardRegularIcon}
|
||||
alt="treeIcon"
|
||||
width="20"
|
||||
height="20"
|
||||
/>
|
||||
</div>
|
||||
${this.isTeamGame
|
||||
? html`
|
||||
@@ -106,9 +115,14 @@ export class GameLeftSidebar extends LitElement implements Layer {
|
||||
class="w-6 h-6 cursor-pointer"
|
||||
@click=${this.toggleTeamLeaderboard}
|
||||
>
|
||||
${this.isTeamLeaderboardShow
|
||||
? html` <team-solid-icon></team-solid-icon>`
|
||||
: html` <team-regular-icon></team-regular-icon>`}
|
||||
<img
|
||||
src=${this.isTeamLeaderboardShow
|
||||
? teamSolidIcon
|
||||
: teamRegularIcon}
|
||||
alt="treeIcon"
|
||||
width="20"
|
||||
height="20"
|
||||
/>
|
||||
</div>
|
||||
`
|
||||
: null}
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import exitIcon from "../../../../resources/images/ExitIconWhite.svg";
|
||||
import pauseIcon from "../../../../resources/images/PauseIconWhite.svg";
|
||||
import playIcon from "../../../../resources/images/PlayIconWhite.svg";
|
||||
import replayRegularIcon from "../../../../resources/images/ReplayRegularIconWhite.svg";
|
||||
import replaySolidIcon from "../../../../resources/images/ReplaySolidIconWhite.svg";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
import { GameType } from "../../../core/game/Game";
|
||||
import { GameView } from "../../../core/game/GameView";
|
||||
import { PauseGameEvent } from "../../Transport";
|
||||
import { Layer } from "./Layer";
|
||||
|
||||
@customElement("game-right-sidebar")
|
||||
export class GameRightSidebar extends LitElement implements Layer {
|
||||
public game: GameView;
|
||||
public eventBus: EventBus;
|
||||
@state()
|
||||
private _isSinglePlayer: boolean = false;
|
||||
|
||||
@state()
|
||||
private _isReplayVisible: boolean = false;
|
||||
|
||||
@state()
|
||||
private _isVisible: boolean = true;
|
||||
|
||||
@state()
|
||||
private isPaused: boolean = false;
|
||||
|
||||
@state()
|
||||
private isExistButtonVisible: boolean = true;
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
init() {
|
||||
this._isSinglePlayer =
|
||||
this.game?.config()?.gameConfig()?.gameType === GameType.Singleplayer ||
|
||||
this.game.config().isReplay();
|
||||
this._isVisible = true;
|
||||
this.game.inSpawnPhase();
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
tick() {
|
||||
if (!this.game.inSpawnPhase()) {
|
||||
this.isExistButtonVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
private toggleReplayPanel(): void {
|
||||
this._isReplayVisible = !this._isReplayVisible;
|
||||
}
|
||||
|
||||
private onPauseButtonClick() {
|
||||
this.isPaused = !this.isPaused;
|
||||
this.eventBus.emit(new PauseGameEvent(this.isPaused));
|
||||
}
|
||||
|
||||
private onExitButtonClick() {
|
||||
const isAlive = this.game.myPlayer()?.isAlive();
|
||||
if (isAlive) {
|
||||
const isConfirmed = confirm("Are you sure you want to exit the game?");
|
||||
if (!isConfirmed) return;
|
||||
}
|
||||
// redirect to the home page
|
||||
window.location.href = "/";
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<aside
|
||||
class=${`fixed top-[90px] right-0 z-[1000] flex flex-col max-h-[calc(100vh-80px)] overflow-y-auto p-2 bg-slate-800/40 backdrop-blur-sm shadow-xs rounded-tl-lg rounded-bl-lg transition-transform duration-300 ease-out transform ${
|
||||
this._isVisible ? "translate-x-0" : "translate-x-full"
|
||||
}`}
|
||||
@contextmenu=${(e: Event) => e.preventDefault()}
|
||||
>
|
||||
<div
|
||||
class=${`flex justify-end items-center gap-2 text-white ${
|
||||
this._isReplayVisible ? "mb-2" : ""
|
||||
}`}
|
||||
>
|
||||
${this._isSinglePlayer || this.game?.config()?.isReplay()
|
||||
? html`
|
||||
<div
|
||||
class="w-6 h-6 cursor-pointer"
|
||||
@click=${this.toggleReplayPanel}
|
||||
>
|
||||
<img
|
||||
src=${this._isReplayVisible
|
||||
? replaySolidIcon
|
||||
: replayRegularIcon}
|
||||
alt="replay"
|
||||
width="20"
|
||||
height="20"
|
||||
style="vertical-align: middle;"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="w-6 h-6 cursor-pointer"
|
||||
@click=${this.onPauseButtonClick}
|
||||
>
|
||||
<img
|
||||
src=${this.isPaused ? playIcon : pauseIcon}
|
||||
alt="play/pause"
|
||||
width="20"
|
||||
height="20"
|
||||
style="vertical-align: middle;"
|
||||
/>
|
||||
</div>
|
||||
${this.isExistButtonVisible
|
||||
? html`
|
||||
<div
|
||||
class="w-6 h-6 cursor-pointer"
|
||||
@click=${this.onExitButtonClick}
|
||||
>
|
||||
<img
|
||||
src=${exitIcon}
|
||||
alt="exit"
|
||||
width="20"
|
||||
height="20"
|
||||
/>
|
||||
</div>
|
||||
`
|
||||
: null}
|
||||
`
|
||||
: null}
|
||||
</div>
|
||||
<div class="block lg:flex flex-wrap gap-2">
|
||||
<replay-panel
|
||||
.isSingleplayer="${this._isSinglePlayer}"
|
||||
.visible="${this._isReplayVisible}"
|
||||
></replay-panel>
|
||||
</div>
|
||||
</aside>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,471 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, query, state } from "lit/decorators.js";
|
||||
import cityIcon from "../../../../resources/images/CityIconWhite.svg";
|
||||
import darkModeIcon from "../../../../resources/images/DarkModeIconWhite.svg";
|
||||
import emojiIcon from "../../../../resources/images/EmojiIconWhite.svg";
|
||||
import exitIcon from "../../../../resources/images/ExitIconWhite.svg";
|
||||
import explosionIcon from "../../../../resources/images/ExplosionIconWhite.svg";
|
||||
import factoryIcon from "../../../../resources/images/FactoryIconWhite.svg";
|
||||
import goldCoinIcon from "../../../../resources/images/GoldCoinIcon.svg";
|
||||
import missileSiloIcon from "../../../../resources/images/MissileSiloUnit.png";
|
||||
import mouseIcon from "../../../../resources/images/MouseIconWhite.svg";
|
||||
import ninjaIcon from "../../../../resources/images/NinjaIconWhite.svg";
|
||||
import populationIcon from "../../../../resources/images/PopulationIconSolidWhite.svg";
|
||||
import portIcon from "../../../../resources/images/PortIcon.svg";
|
||||
import samLauncherIcon from "../../../../resources/images/SamLauncherUnitWhite.png";
|
||||
import settingsIcon from "../../../../resources/images/SettingIconWhite.svg";
|
||||
import defensePostIcon from "../../../../resources/images/ShieldIconWhite.svg";
|
||||
import treeIcon from "../../../../resources/images/TreeIconWhite.svg";
|
||||
import troopIcon from "../../../../resources/images/TroopIconWhite.svg";
|
||||
import workerIcon from "../../../../resources/images/WorkerIconWhite.svg";
|
||||
import { translateText } from "../../../client/Utils";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
import { UnitType } from "../../../core/game/Game";
|
||||
import { GameUpdateType } from "../../../core/game/GameUpdates";
|
||||
import { GameView } from "../../../core/game/GameView";
|
||||
import { UserSettings } from "../../../core/game/UserSettings";
|
||||
import { AlternateViewEvent, RefreshGraphicsEvent } from "../../InputHandler";
|
||||
import { renderNumber, renderTroops } from "../../Utils";
|
||||
import { Layer } from "./Layer";
|
||||
|
||||
@customElement("game-top-bar")
|
||||
export class GameTopBar extends LitElement implements Layer {
|
||||
public game: GameView;
|
||||
public eventBus: EventBus;
|
||||
private _userSettings: UserSettings = new UserSettings();
|
||||
private _population = 0;
|
||||
private _troops = 0;
|
||||
private _cities = 0;
|
||||
private _factories = 0;
|
||||
private _workers = 0;
|
||||
private _missileSilo = 0;
|
||||
private _port = 0;
|
||||
private _defensePost = 0;
|
||||
private _samLauncher = 0;
|
||||
private _lastPopulationIncreaseRate = 0;
|
||||
private _popRateIsIncreasing = false;
|
||||
private hasWinner = false;
|
||||
|
||||
@state()
|
||||
private showSettingsMenu = false;
|
||||
@state()
|
||||
private alternateView: boolean = false;
|
||||
|
||||
@state()
|
||||
private timer: number = 0;
|
||||
|
||||
@query(".settings-container")
|
||||
private settingsContainer!: HTMLElement;
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
init() {
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
tick() {
|
||||
this.updatePopulationIncrease();
|
||||
const player = this.game?.myPlayer();
|
||||
if (!player) return;
|
||||
this._troops = player.troops();
|
||||
this._workers = player.workers();
|
||||
this._cities = player.totalUnitLevels(UnitType.City);
|
||||
this._missileSilo = player.totalUnitLevels(UnitType.MissileSilo);
|
||||
this._port = player.totalUnitLevels(UnitType.Port);
|
||||
this._defensePost = player.totalUnitLevels(UnitType.DefensePost);
|
||||
this._samLauncher = player.totalUnitLevels(UnitType.SAMLauncher);
|
||||
this._factories = player.totalUnitLevels(UnitType.Factory);
|
||||
const updates = this.game.updatesSinceLastTick();
|
||||
if (updates) {
|
||||
this.hasWinner = this.hasWinner || updates[GameUpdateType.Win].length > 0;
|
||||
}
|
||||
if (this.game.inSpawnPhase()) {
|
||||
this.timer = 0;
|
||||
} else if (!this.hasWinner && this.game.ticks() % 10 === 0) {
|
||||
this.timer++;
|
||||
}
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
window.addEventListener("click", this.handleOutsideClick, true);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
window.removeEventListener("click", this.handleOutsideClick, true);
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
private handleOutsideClick = (event: MouseEvent) => {
|
||||
if (
|
||||
this.showSettingsMenu &&
|
||||
this.settingsContainer &&
|
||||
!this.settingsContainer.contains(event.target as Node)
|
||||
) {
|
||||
this.showSettingsMenu = false;
|
||||
}
|
||||
};
|
||||
|
||||
private onExitButtonClick() {
|
||||
const isAlive = this.game.myPlayer()?.isAlive();
|
||||
if (isAlive) {
|
||||
const isConfirmed = confirm("Are you sure you want to exit the game?");
|
||||
if (!isConfirmed) return;
|
||||
}
|
||||
// redirect to the home page
|
||||
window.location.href = "/";
|
||||
}
|
||||
|
||||
private onTerrainButtonClick() {
|
||||
this.alternateView = !this.alternateView;
|
||||
this.eventBus.emit(new AlternateViewEvent(this.alternateView));
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private onToggleEmojisButtonClick() {
|
||||
this._userSettings.toggleEmojis();
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private onToggleSpecialEffectsButtonClick() {
|
||||
this._userSettings.toggleFxLayer();
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private onToggleDarkModeButtonClick() {
|
||||
this._userSettings.toggleDarkMode();
|
||||
this.requestUpdate();
|
||||
this.eventBus.emit(new RefreshGraphicsEvent());
|
||||
}
|
||||
|
||||
private onToggleRandomNameModeButtonClick() {
|
||||
this._userSettings.toggleRandomName();
|
||||
}
|
||||
private onToggleLeftClickOpensMenu() {
|
||||
this._userSettings.toggleLeftClickOpenMenu();
|
||||
}
|
||||
|
||||
private toggleSettingsMenu() {
|
||||
this.showSettingsMenu = !this.showSettingsMenu;
|
||||
}
|
||||
|
||||
private updatePopulationIncrease() {
|
||||
const player = this.game?.myPlayer();
|
||||
if (player === null) return;
|
||||
const popIncreaseRate = player.population() - this._population;
|
||||
if (this.game.ticks() % 5 === 0) {
|
||||
this._popRateIsIncreasing =
|
||||
popIncreaseRate >= this._lastPopulationIncreaseRate;
|
||||
this._lastPopulationIncreaseRate = popIncreaseRate;
|
||||
}
|
||||
}
|
||||
|
||||
private secondsToHms = (d: number): string => {
|
||||
const h = Math.floor(d / 3600);
|
||||
const m = Math.floor((d % 3600) / 60);
|
||||
const s = Math.floor((d % 3600) % 60);
|
||||
let time = d === 0 ? "-" : `${s}s`;
|
||||
if (m > 0) time = `${m}m` + time;
|
||||
if (h > 0) time = `${h}h` + time;
|
||||
return time;
|
||||
};
|
||||
|
||||
render() {
|
||||
const myPlayer = this.game?.myPlayer();
|
||||
if (!this.game || !myPlayer || this.game.inSpawnPhase()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isAlt = this.game.config().isReplay();
|
||||
if (isAlt) {
|
||||
return html`
|
||||
<div
|
||||
class="fixed top-0 left-auto right-0 z-[1100] bg-slate-800/40 backdrop-blur-sm p-2 flex justify-center items-center"
|
||||
>
|
||||
<div
|
||||
class="w-[70px] h-8 lg:w-24 lg:h-10 border border-slate-400 p-0.5 text-xs md:text-sm lg:text-base flex items-center text-white px-1"
|
||||
>
|
||||
${this.secondsToHms(this.timer)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
const popRate = myPlayer
|
||||
? this.game.config().populationIncreaseRate(myPlayer) * 10
|
||||
: 0;
|
||||
const maxPop = myPlayer ? this.game.config().maxPopulation(myPlayer) : 0;
|
||||
const goldPerSecond = myPlayer
|
||||
? this.game.config().goldAdditionRate(myPlayer) * 10n
|
||||
: 0n;
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="fixed top-0 min-h-[50px] lg:min-h-[80px] z-[1100] flex flex-wrap bg-slate-800/40 backdrop-blur-sm shadow-xs text-white text-xs md:text-sm lg:text-base left-0 right-0 grid-cols-4 p-1 md:px-1.5 lg:px-4"
|
||||
>
|
||||
<div
|
||||
class="flex flex-1 basis-full justify-between items-center gap-1 w-full"
|
||||
>
|
||||
${myPlayer?.isAlive() && !this.game.inSpawnPhase()
|
||||
? html`
|
||||
<div class="overflow-x-auto hide-scrollbar flex-1 max-w-[85vw]">
|
||||
<div
|
||||
class="grid gap-1 grid-cols-[80px_100px_80px_minmax(80px,auto)] w-max md:gap-2 md:grid-cols-[90px_120px_90px_minmax(100px,auto)]"
|
||||
>
|
||||
<div
|
||||
class="flex flex-wrap gap-1 flex-col bg-slate-800/20 border border-slate-400 p-0.5 md:px-1 lg:px-2"
|
||||
>
|
||||
<div class="flex gap-2 items-center justify-between">
|
||||
<img
|
||||
src=${goldCoinIcon}
|
||||
alt="gold"
|
||||
width="20"
|
||||
height="20"
|
||||
style="vertical-align: middle;"
|
||||
/>
|
||||
+${renderNumber(goldPerSecond)}
|
||||
</div>
|
||||
<div>${renderNumber(myPlayer.gold())}</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-wrap gap-1 flex-col bg-slate-800/20 border border-slate-400 p-0.5 md:px-1 lg:px-2"
|
||||
>
|
||||
<div class="flex gap-2 items-center justify-between">
|
||||
<img
|
||||
src=${populationIcon}
|
||||
alt="population"
|
||||
width="20"
|
||||
height="20"
|
||||
style="vertical-align: middle;"
|
||||
/>
|
||||
<span
|
||||
class="${this._popRateIsIncreasing
|
||||
? "text-green-500"
|
||||
: "text-yellow-500"}"
|
||||
translate="no"
|
||||
>
|
||||
+${renderTroops(popRate)}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
${renderTroops(myPlayer.population())} /
|
||||
${renderTroops(maxPop)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex bg-slate-800/20 border border-slate-400 p-0.5 md:px-1 lg:px-2"
|
||||
>
|
||||
<div class="flex flex-col flex-grow gap-1 w-full ">
|
||||
<div class="flex gap-1">
|
||||
<img
|
||||
src=${troopIcon}
|
||||
alt="troops"
|
||||
width="20"
|
||||
height="20"
|
||||
style="vertical-align: middle;"
|
||||
/>
|
||||
${renderTroops(this._troops)}
|
||||
</div>
|
||||
<div class="flex gap-1">
|
||||
<img
|
||||
src=${workerIcon}
|
||||
alt="gold"
|
||||
width="20"
|
||||
height="20"
|
||||
style="vertical-align: middle;"
|
||||
/>
|
||||
${renderTroops(this._workers)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="grid grid-rows-1 auto-cols-max grid-flow-col gap-1 bg-slate-800/20 border border-slate-400 p-0.5 md:px-1 lg:px-2 md:gap-2"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<img
|
||||
src=${cityIcon}
|
||||
alt="gold"
|
||||
width="20"
|
||||
height="20"
|
||||
style="vertical-align: middle;"
|
||||
/>
|
||||
${renderNumber(this._cities)}
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<img
|
||||
src=${factoryIcon}
|
||||
alt="gold"
|
||||
width="20"
|
||||
height="20"
|
||||
style="vertical-align: middle;"
|
||||
/>
|
||||
${renderNumber(this._factories)}
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<img
|
||||
src=${portIcon}
|
||||
alt="gold"
|
||||
width="20"
|
||||
height="20"
|
||||
style="vertical-align: middle;"
|
||||
/>
|
||||
${renderNumber(this._port)}
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<img
|
||||
src=${defensePostIcon}
|
||||
alt="gold"
|
||||
width="20"
|
||||
height="20"
|
||||
style="vertical-align: middle;"
|
||||
/>
|
||||
${renderNumber(this._defensePost)}
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<img
|
||||
src=${missileSiloIcon}
|
||||
alt="gold"
|
||||
width="20"
|
||||
height="20"
|
||||
style="vertical-align: middle;"
|
||||
/>
|
||||
${renderNumber(this._missileSilo)}
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<img
|
||||
src=${samLauncherIcon}
|
||||
alt="gold"
|
||||
width="20"
|
||||
height="20"
|
||||
style="vertical-align: middle;"
|
||||
/>
|
||||
${renderNumber(this._samLauncher)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: html`<div></div>`}
|
||||
<div class="flex gap-1 items-center">
|
||||
<div
|
||||
class="w-[70px] h-8 lg:w-24 lg:h-10 border border-slate-400 p-0.5 text-xs md:text-sm lg:text-base flex items-center px-1"
|
||||
>
|
||||
${this.secondsToHms(this.timer)}
|
||||
</div>
|
||||
<div class="relative settings-container">
|
||||
<img
|
||||
class="cursor-pointer bg-slate-800/20 border border-slate-400 p-0.5"
|
||||
src=${settingsIcon}
|
||||
alt="settings"
|
||||
width="28"
|
||||
height="28"
|
||||
style="vertical-align: middle;"
|
||||
@click=${this.toggleSettingsMenu}
|
||||
/>
|
||||
${this.showSettingsMenu
|
||||
? html`
|
||||
<div
|
||||
class="absolute right-0 mt-1.5 bg-slate-700 border border-slate-500 rounded shadow-lg z-[1100] w-max min-w-[10rem] whitespace-nowrap"
|
||||
>
|
||||
<button
|
||||
class="flex gap-1 items-center w-full text-left px-2 py-1 hover:bg-slate-600 text-white text-sm"
|
||||
@click="${this.onTerrainButtonClick}"
|
||||
>
|
||||
<img
|
||||
src=${treeIcon}
|
||||
alt="treeIcon"
|
||||
width="20"
|
||||
height="20"
|
||||
/>
|
||||
Toggle Terrain ${this.alternateView ? "On" : "Off"}
|
||||
</button>
|
||||
<button
|
||||
class="flex gap-1 items-center w-full text-left px-2 py-1 hover:bg-slate-600 text-white text-sm"
|
||||
@click="${this.onToggleEmojisButtonClick}"
|
||||
>
|
||||
<img
|
||||
src=${emojiIcon}
|
||||
alt="emojiIcon"
|
||||
width="20"
|
||||
height="20"
|
||||
/>
|
||||
${translateText("user_setting.emojis_label")}
|
||||
${this._userSettings.emojis() ? "On" : "Off"}
|
||||
</button>
|
||||
<button
|
||||
class="flex gap-1 items-center w-full text-left px-2 py-1 hover:bg-slate-600 text-white text-sm"
|
||||
@click="${this.onToggleDarkModeButtonClick}"
|
||||
>
|
||||
<img
|
||||
src=${darkModeIcon}
|
||||
alt="darkModeIcon"
|
||||
width="20"
|
||||
height="20"
|
||||
/>
|
||||
${translateText("user_setting.dark_mode_label")}
|
||||
${this._userSettings.darkMode() ? "On" : "Off"}
|
||||
</button>
|
||||
<button
|
||||
class="flex gap-1 items-center w-full text-left px-2 py-1 hover:bg-slate-600 text-white text-sm"
|
||||
@click="${this.onToggleSpecialEffectsButtonClick}"
|
||||
>
|
||||
<img
|
||||
src=${explosionIcon}
|
||||
alt="onExitButtonClick"
|
||||
width="20"
|
||||
height="20"
|
||||
/>
|
||||
${translateText("user_setting.special_effects_label")}
|
||||
${this._userSettings.fxLayer() ? "On" : "Off"}
|
||||
</button>
|
||||
<button
|
||||
class="flex gap-1 items-center w-full text-left px-2 py-1 hover:bg-slate-600 text-white text-sm"
|
||||
@click="${this.onToggleRandomNameModeButtonClick}"
|
||||
>
|
||||
<img
|
||||
src=${ninjaIcon}
|
||||
alt="ninjaIcon"
|
||||
width="20"
|
||||
height="20"
|
||||
/>
|
||||
${translateText("user_setting.anonymous_names_label")}
|
||||
${this._userSettings.anonymousNames() ? "On" : "Off"}
|
||||
</button>
|
||||
<button
|
||||
class="flex gap-1 items-center w-full text-left px-2 py-1 hover:bg-slate-600 text-white text-sm"
|
||||
@click="${this.onToggleLeftClickOpensMenu}"
|
||||
>
|
||||
<img
|
||||
src=${mouseIcon}
|
||||
alt="mouseIcon"
|
||||
width="20"
|
||||
height="20"
|
||||
/>
|
||||
Left click
|
||||
${this._userSettings.leftClickOpensMenu()
|
||||
? "On"
|
||||
: "Off"}
|
||||
</button>
|
||||
<button
|
||||
class="flex gap-1 items-center w-full text-left px-2 py-1 hover:bg-slate-600 text-white text-sm"
|
||||
@click="${this.onExitButtonClick}"
|
||||
>
|
||||
<img
|
||||
src=${exitIcon}
|
||||
alt="exitIcon"
|
||||
width="20"
|
||||
height="20"
|
||||
/>
|
||||
Exit game
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
: null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -34,8 +34,8 @@ export class HeadsUpMessage extends LitElement implements Layer {
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="flex items-center
|
||||
w-full justify-evenly h-8 lg:h-10 top-0 lg:top-4 left-0 lg:left-4
|
||||
class="flex items-center relative
|
||||
w-full justify-evenly h-8 lg:h-10 md:top-[70px] left-0 lg:left-4
|
||||
bg-opacity-60 bg-gray-900 rounded-md lg:rounded-lg
|
||||
backdrop-blur-md text-white text-md lg:text-xl p-1 lg:p-2"
|
||||
@contextmenu=${(e) => e.preventDefault()}
|
||||
|
||||
@@ -352,11 +352,11 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="flex w-full z-50 flex-col"
|
||||
class="hidden lg:flex fixed top-[245px] right-0 w-full z-50 flex-col max-w-[180px]"
|
||||
@contextmenu=${(e) => e.preventDefault()}
|
||||
>
|
||||
<div
|
||||
class="bg-slate-800/40 backdrop-blur-sm shadow-xs rounded-lg shadow-lg backdrop-blur-sm transition-all duration-300 text-white text-lg md:text-base ${containerClasses}"
|
||||
class="bg-slate-800/40 backdrop-blur-sm shadow-xs rounded-lg shadow-lg transition-all duration-300 text-white text-lg md:text-base ${containerClasses}"
|
||||
>
|
||||
${this.player !== null ? this.renderPlayerInfo(this.player) : ""}
|
||||
${this.unit !== null ? this.renderUnitInfo(this.unit) : ""}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
import { GameType } from "../../../core/game/Game";
|
||||
import { GameView } from "../../../core/game/GameView";
|
||||
import { ReplaySpeedChangeEvent } from "../../InputHandler";
|
||||
import {
|
||||
@@ -16,27 +15,26 @@ export class ReplayPanel extends LitElement implements Layer {
|
||||
public game: GameView | undefined;
|
||||
public eventBus: EventBus | undefined;
|
||||
|
||||
@property({ type: Boolean })
|
||||
visible: boolean = false;
|
||||
|
||||
@state()
|
||||
private _replaySpeedMultiplier: number = defaultReplaySpeedMultiplier;
|
||||
private _isSinglePlayer: boolean = false;
|
||||
|
||||
@state()
|
||||
private _isVisible = false;
|
||||
@property({ type: Boolean })
|
||||
isSingleplayer = false;
|
||||
|
||||
init() {
|
||||
this._isSinglePlayer =
|
||||
this.game?.config().gameConfig().gameType === GameType.Singleplayer;
|
||||
if (this._isSinglePlayer) {
|
||||
this.setVisible(true);
|
||||
}
|
||||
createRenderRoot() {
|
||||
return this; // Enable Tailwind CSS
|
||||
}
|
||||
|
||||
tick() {
|
||||
if (!this._isVisible && this.game?.config().isReplay()) {
|
||||
this.setVisible(true);
|
||||
}
|
||||
init() {}
|
||||
|
||||
this.requestUpdate();
|
||||
tick() {
|
||||
if (!this.visible) return;
|
||||
if (this.game!.ticks() % 10 === 0) {
|
||||
this.requestUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
onReplaySpeedChange(value: ReplaySpeedMultiplier) {
|
||||
@@ -44,85 +42,45 @@ export class ReplayPanel extends LitElement implements Layer {
|
||||
this.eventBus?.emit(new ReplaySpeedChangeEvent(value));
|
||||
}
|
||||
|
||||
renderLayer(context: CanvasRenderingContext2D) {
|
||||
// Render any necessary canvas elements
|
||||
}
|
||||
|
||||
shouldTransform(): boolean {
|
||||
renderLayer(_ctx: CanvasRenderingContext2D) {}
|
||||
shouldTransform() {
|
||||
return false;
|
||||
}
|
||||
|
||||
setVisible(visible: boolean) {
|
||||
this._isVisible = visible;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this._isVisible) {
|
||||
return html``;
|
||||
}
|
||||
if (!this.visible) return html``;
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="bg-opacity-60 bg-gray-900 p-1 lg:p-2 rounded-es-sm lg:rounded-lg backdrop-blur-md"
|
||||
@contextmenu=${(e) => e.preventDefault()}
|
||||
@contextmenu=${(e: Event) => e.preventDefault()}
|
||||
>
|
||||
<label class="block mb-1 text-white" translate="no">
|
||||
${this._isSinglePlayer
|
||||
${this.isSingleplayer
|
||||
? translateText("replay_panel.game_speed")
|
||||
: translateText("replay_panel.replay_speed")}
|
||||
</label>
|
||||
<div class="grid grid-cols-2 gap-1">
|
||||
<button
|
||||
class="text-white font-bold py-0 rounded border transition ${this
|
||||
._replaySpeedMultiplier === ReplaySpeedMultiplier.slow
|
||||
? "bg-blue-500 border-gray-400"
|
||||
: "border-gray-500"}"
|
||||
@click=${() => {
|
||||
this.onReplaySpeedChange(ReplaySpeedMultiplier.slow);
|
||||
}}
|
||||
>
|
||||
×0.5
|
||||
</button>
|
||||
<button
|
||||
class="text-white font-bold py-0 rounded border transition ${this
|
||||
._replaySpeedMultiplier === ReplaySpeedMultiplier.normal
|
||||
? "bg-blue-500 border-gray-400"
|
||||
: "border-gray-500"}"
|
||||
@click=${() => {
|
||||
this.onReplaySpeedChange(ReplaySpeedMultiplier.normal);
|
||||
}}
|
||||
>
|
||||
×1
|
||||
</button>
|
||||
<button
|
||||
class="text-white font-bold py-0 rounded border transition ${this
|
||||
._replaySpeedMultiplier === ReplaySpeedMultiplier.fast
|
||||
? "bg-blue-500 border-gray-400"
|
||||
: "border-gray-500"}"
|
||||
@click=${() => {
|
||||
this.onReplaySpeedChange(ReplaySpeedMultiplier.fast);
|
||||
}}
|
||||
>
|
||||
×2
|
||||
</button>
|
||||
<button
|
||||
class="text-white font-bold py-0 rounded border transition ${this
|
||||
._replaySpeedMultiplier === ReplaySpeedMultiplier.fastest
|
||||
? "bg-blue-500 border-gray-400"
|
||||
: "border-gray-500"}"
|
||||
@click=${() => {
|
||||
this.onReplaySpeedChange(ReplaySpeedMultiplier.fastest);
|
||||
}}
|
||||
>
|
||||
max
|
||||
</button>
|
||||
${this.renderSpeedButton(ReplaySpeedMultiplier.slow, "×0.5")}
|
||||
${this.renderSpeedButton(ReplaySpeedMultiplier.normal, "×1")}
|
||||
${this.renderSpeedButton(ReplaySpeedMultiplier.fast, "×2")}
|
||||
${this.renderSpeedButton(ReplaySpeedMultiplier.fastest, "max")}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
createRenderRoot() {
|
||||
return this; // Disable shadow DOM to allow Tailwind styles
|
||||
private renderSpeedButton(value: ReplaySpeedMultiplier, label: string) {
|
||||
const isActive = this._replaySpeedMultiplier === value;
|
||||
return html`
|
||||
<button
|
||||
class="text-white font-bold py-0 rounded border transition ${isActive
|
||||
? "bg-blue-500 border-gray-400"
|
||||
: "border-gray-500"}"
|
||||
@click=${() => this.onReplaySpeedChange(value)}
|
||||
>
|
||||
${label}
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,11 @@ export class SpawnTimer implements Layer {
|
||||
|
||||
tick() {
|
||||
if (this.game.inSpawnPhase()) {
|
||||
this.ratios[0] =
|
||||
this.game.ticks() / this.game.config().numSpawnPhaseTurns();
|
||||
// During spawn phase, only one segment filling full width
|
||||
this.ratios = [
|
||||
this.game.ticks() / this.game.config().numSpawnPhaseTurns(),
|
||||
];
|
||||
this.colors = ["rgba(0, 128, 255, 0.7)"];
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -33,18 +36,17 @@ export class SpawnTimer implements Layer {
|
||||
const team = player.team();
|
||||
if (team === null) throw new Error("Team is null");
|
||||
const tiles = teamTiles.get(team) ?? 0;
|
||||
const sum = tiles + player.numTilesOwned();
|
||||
teamTiles.set(team, sum);
|
||||
teamTiles.set(team, tiles + player.numTilesOwned());
|
||||
}
|
||||
|
||||
const theme = this.game.config().theme();
|
||||
const total = sumIterator(teamTiles.values());
|
||||
if (total === 0) return;
|
||||
|
||||
for (const [team, count] of teamTiles) {
|
||||
const ratio = count / total;
|
||||
const color = theme.teamColor(team).toRgbString();
|
||||
this.ratios.push(ratio);
|
||||
this.colors.push(color);
|
||||
this.colors.push(theme.teamColor(team).toRgbString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,12 +55,23 @@ export class SpawnTimer implements Layer {
|
||||
}
|
||||
|
||||
renderLayer(context: CanvasRenderingContext2D) {
|
||||
if (this.ratios === null) return;
|
||||
if (this.ratios.length === 0) return;
|
||||
if (this.colors.length === 0) return;
|
||||
if (this.ratios.length === 0 || this.colors.length === 0) return;
|
||||
|
||||
const barHeight = 10;
|
||||
const barWidth = this.transformHandler.width();
|
||||
let yOffset: number;
|
||||
|
||||
if (this.game.inSpawnPhase()) {
|
||||
// At spawn time, draw at top
|
||||
yOffset = 0;
|
||||
} else if (this.game.config().gameConfig().gameMode === GameMode.Team) {
|
||||
// After spawn, only in team mode, offset based on screen width
|
||||
const screenW = window.innerWidth;
|
||||
yOffset = screenW > 1024 ? 80 : 58;
|
||||
} else {
|
||||
// Not spawn and not team mode: no bar
|
||||
return;
|
||||
}
|
||||
|
||||
let x = 0;
|
||||
let filledRatio = 0;
|
||||
@@ -67,7 +80,7 @@ export class SpawnTimer implements Layer {
|
||||
const segmentWidth = barWidth * ratio;
|
||||
|
||||
context.fillStyle = this.colors[i];
|
||||
context.fillRect(x, 0, segmentWidth, barHeight);
|
||||
context.fillRect(x, yOffset, segmentWidth, barHeight);
|
||||
|
||||
x += segmentWidth;
|
||||
filledRatio += ratio;
|
||||
@@ -76,8 +89,6 @@ export class SpawnTimer implements Layer {
|
||||
}
|
||||
|
||||
function sumIterator(values: MapIterator<number>) {
|
||||
// To use reduce, we'd need to allocate an array:
|
||||
// return Array.from(values).reduce((sum, v) => sum + v, 0);
|
||||
let total = 0;
|
||||
for (const value of values) {
|
||||
total += value;
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
import { translateText } from "../../../client/Utils";
|
||||
import { GameView } from "../../../core/game/GameView";
|
||||
import { renderNumber, renderTroops } from "../../Utils";
|
||||
import { Layer } from "./Layer";
|
||||
|
||||
@customElement("top-bar")
|
||||
export class TopBar extends LitElement implements Layer {
|
||||
public game: GameView;
|
||||
private isVisible = false;
|
||||
private _population = 0;
|
||||
private _lastPopulationIncreaseRate = 0;
|
||||
private _popRateIsIncreasing = false;
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
init() {
|
||||
this.isVisible = true;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
tick() {
|
||||
this.updatePopulationIncrease();
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private updatePopulationIncrease() {
|
||||
const player = this.game?.myPlayer();
|
||||
if (player === null) return;
|
||||
const popIncreaseRate = player.population() - this._population;
|
||||
if (this.game.ticks() % 5 === 0) {
|
||||
this._popRateIsIncreasing =
|
||||
popIncreaseRate >= this._lastPopulationIncreaseRate;
|
||||
this._lastPopulationIncreaseRate = popIncreaseRate;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.isVisible) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const myPlayer = this.game?.myPlayer();
|
||||
if (!myPlayer?.isAlive() || this.game?.inSpawnPhase()) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const popRate = this.game.config().populationIncreaseRate(myPlayer) * 10;
|
||||
const maxPop = this.game.config().maxPopulation(myPlayer);
|
||||
const goldPerSecond = this.game.config().goldAdditionRate(myPlayer) * 10n;
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="fixed top-0 z-50 bg-slate-800/40 backdrop-blur-sm shadow-xs text-white text-sm p-1 rounded-ee-sm lg:rounded grid grid-cols-1 sm:grid-cols-2 w-1/2 sm:w-2/3 md:w-1/2 lg:hidden"
|
||||
>
|
||||
<!-- Pop section (takes 2 columns on desktop) -->
|
||||
<div
|
||||
class="sm:col-span-1 flex items-center space-x-1 overflow-x-auto whitespace-nowrap"
|
||||
>
|
||||
<span class="font-bold shrink-0"
|
||||
>${translateText("control_panel.pop")}:</span
|
||||
>
|
||||
<span translate="no"
|
||||
>${renderTroops(myPlayer.population())} /
|
||||
${renderTroops(maxPop)}</span
|
||||
>
|
||||
<span
|
||||
translate="no"
|
||||
class="${this._popRateIsIncreasing
|
||||
? "text-green-500"
|
||||
: "text-yellow-500"}"
|
||||
>(+${renderTroops(popRate)})</span
|
||||
>
|
||||
</div>
|
||||
<!-- Gold section (takes 1 column on desktop) -->
|
||||
<div
|
||||
class="flex items-center space-x-2 overflow-x-auto whitespace-nowrap"
|
||||
>
|
||||
<span class="font-bold shrink-0"
|
||||
>${translateText("control_panel.gold")}:</span
|
||||
>
|
||||
<span translate="no"
|
||||
>${renderNumber(myPlayer.gold())}
|
||||
(+${renderNumber(goldPerSecond)})</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -265,7 +265,6 @@
|
||||
<div id="radialMenu" class="radial-menu"></div>
|
||||
<div class="flex gap-2 fixed right-[10px] top-[10px] z-50 flex-col">
|
||||
<options-menu></options-menu>
|
||||
<replay-panel></replay-panel>
|
||||
<player-info-overlay></player-info-overlay>
|
||||
</div>
|
||||
<div
|
||||
@@ -279,16 +278,13 @@
|
||||
style="position: fixed; pointer-events: none"
|
||||
>
|
||||
<div
|
||||
class="w-full sm:w-2/3 sm:fixed sm:right-0 sm:bottom-0 sm:flex flex-col items-end"
|
||||
class="w-full md:w-2/3 md:fixed sm:right-0 md:bottom-0 md:flex flex-col items-end"
|
||||
style="pointer-events: none"
|
||||
>
|
||||
<chat-display></chat-display>
|
||||
<events-display></events-display>
|
||||
</div>
|
||||
<div
|
||||
class="w-[320px] flex flex-col items-center"
|
||||
style="pointer-events: auto"
|
||||
>
|
||||
<div style="pointer-events: auto">
|
||||
<control-panel></control-panel>
|
||||
</div>
|
||||
</div>
|
||||
@@ -363,7 +359,8 @@
|
||||
<build-menu></build-menu>
|
||||
<win-modal></win-modal>
|
||||
<game-starting-modal></game-starting-modal>
|
||||
<top-bar></top-bar>
|
||||
<game-top-bar></game-top-bar>
|
||||
<game-right-sidebar></game-right-sidebar>
|
||||
|
||||
<player-panel></player-panel>
|
||||
<help-modal></help-modal>
|
||||
|
||||
@@ -37,6 +37,14 @@
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.hide-scrollbar {
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE/Edge */
|
||||
}
|
||||
.hide-scrollbar::-webkit-scrollbar {
|
||||
display: none; /* Chrome, Safari */
|
||||
}
|
||||
|
||||
.start-game-button {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
|
||||
@@ -264,10 +264,17 @@ export class PlayerView {
|
||||
targetTroopRatio(): number {
|
||||
return this.data.targetTroopRatio;
|
||||
}
|
||||
|
||||
troops(): number {
|
||||
return this.data.troops;
|
||||
}
|
||||
|
||||
totalUnitLevels(type: UnitType): number {
|
||||
return this.units(type)
|
||||
.map((unit) => unit.level())
|
||||
.reduce((a, b) => a + b, 0);
|
||||
}
|
||||
|
||||
isAlliedWith(other: PlayerView): boolean {
|
||||
return this.data.allies.some((n) => other.smallID() === n);
|
||||
}
|
||||
|
||||