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
![Snímek obrazovky 2025-06-21 v 8 13 46](https://github.c
![Snímek obrazovky 2025-06-21 v 8 13
35](https://github.com/user-attachments/assets/e166ee1b-6173-48f5-8e2d-598d796a7e2d)
om/user-attachments/assets/3a0edbef-e621-4fc4-b6b7-c1ed
![Snímek obrazovky 2025-06-21 v 8 13
20](https://github.com/user-attachments/assets/1214ab61-323c-4317-8722-eaa4b932a60c)
8f9a8219)
![Snímek obrazovky 2025-06-21 v 8 10
04](https://github.com/user-attachments/assets/374fe15a-bfad-4469-9950-3ec631b6e2d3)

Closes #1165

---------

Co-authored-by: Scott Anderson <scottanderson@users.noreply.github.com>
Co-authored-by: evanpelle <evanpelle@gmail.com>
This commit is contained in:
DiesselOne
2025-07-01 04:49:42 +02:00
committed by GitHub
parent 91adf1fa8b
commit 4e48eba910
55 changed files with 795 additions and 715 deletions
+1
View File
@@ -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

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 397 B

+1
View File
@@ -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

+1
View File
@@ -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

+1
View File
@@ -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

+1
View File
@@ -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

+1
View File
@@ -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

+1
View File
@@ -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

+1
View File
@@ -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

+1
View File
@@ -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

Binary file not shown.

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

+1
View File
@@ -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

+1
View File
@@ -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

+4
View File
@@ -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

+1
View File
@@ -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

+4
View File
@@ -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

+1 -1
View File
@@ -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%)",
+1 -1
View File
@@ -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%)",
+1 -1
View File
@@ -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 (1100%)",
+6 -6
View File
@@ -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 sites 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 (1100%)",
+1 -1
View File
@@ -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 (1100%)",
+1 -1
View File
@@ -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 (1100%)",
+1 -1
View File
@@ -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 (1100%)",
+1 -1
View File
@@ -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%)",
+1 -1
View File
@@ -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 (1100%)",
+1 -1
View File
@@ -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%)",
+1 -1
View File
@@ -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 (1100%)",
+1 -1
View File
@@ -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%)",
+1 -1
View File
@@ -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)",
+1 -1
View File
@@ -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%)",
+1 -1
View File
@@ -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) => {
+18 -15
View File
@@ -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>
`;
}
}
+12 -77
View File
@@ -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
+26 -12
View File
@@ -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>
`;
}
}
+471
View File
@@ -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>
`;
}
}
+2 -2
View File
@@ -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) : ""}
+35 -77
View File
@@ -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>
`;
}
}
+23 -12
View File
@@ -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;
-93
View File
@@ -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>
`;
}
}
+4 -7
View File
@@ -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>
+8
View File
@@ -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;
+7
View File
@@ -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);
}