Create base components button, modal .. (#331)

Create base components with shared styles, as start of make ui better.
For now shoul look same but underhood new copoments are used.

This should be first PR that handle this and many more comes. I am in
rush due conflict with other ppl, but should work as i tested.

Testing again and look at structure

Main goal i have global css not scope in component die loading times and
size of elements. (Modal due nature of lit and shadow dom is exception,
maybe later find better way).

Documenting the components will happen later as base components
establish their usage.
This commit is contained in:
Mittanicz
2025-03-24 18:34:27 +01:00
committed by GitHub
parent f7c6b0d585
commit 166ef92970
11 changed files with 1544 additions and 1751 deletions
+263 -473
View File
@@ -1,500 +1,290 @@
import { LitElement, css, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { LitElement, html } from "lit";
import { customElement, query } from "lit/decorators.js";
import "./components/Difficulties";
import "./components/Maps";
@customElement("help-modal")
export class HelpModal extends LitElement {
@state() private isModalOpen = false;
// Added #helpModal infront of everything to prevent leaks of css in other elements outside this one
private styles = css`
.radial-menu-image {
width: 211px;
height: 200px;
}
#helpModal.modal-overlay {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
overflow-y: auto;
display: flex;
align-items: center;
justify-content: center;
}
#helpModal .modal-content {
background-color: rgb(35 35 35 / 0.8);
-webkit-backdrop-filter: blur(12px);
backdrop-filter: blur(12px);
color: white;
padding: 20px;
border-radius: 8px;
width: 80%;
max-width: 1280px;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 0 40px rgba(0, 0, 0, 0.5);
border: 1px solid rgba(255, 255, 255, 0.2);
backdrop-filter: blur(8px);
position: relative;
}
#helpModal .title {
font-size: 28px;
color: #fff;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
padding: 0 0 0 20px;
}
#helpModal .close {
position: sticky;
top: 0px;
right: 0px;
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
#helpModal .close:hover,
#helpModal .close:focus {
color: white;
text-decoration: none;
cursor: pointer;
}
#helpModal table {
border-collapse: collapse;
}
#helpModal table,
#helpModal table th,
#helpModal table td {
border: 1px solid rgb(255 255 255 / 0.2);
}
#helpModal table th,
#helpModal table td {
padding: 8px 16px;
}
#helpModal table td:first-of-type {
text-align: center;
}
#helpModal .icon {
background-color: white;
width: 32px;
height: 32px;
}
#helpModal .city-icon {
-webkit-mask: url(/images/CityIconWhite.svg) no-repeat center / cover;
mask: url(/images/CityIconWhite.svg) no-repeat center / cover;
}
#helpModal .defense-post-icon {
-webkit-mask: url(/images/ShieldIconWhite.svg) no-repeat center / cover;
mask: url(/images/ShieldIconWhite.svg) no-repeat center / cover;
}
#helpModal .port-icon {
-webkit-mask: url(/images/PortIcon.svg) no-repeat center / cover;
mask: url(/images/PortIcon.svg) no-repeat center / cover;
}
#helpModal .warship-icon {
-webkit-mask: url(/images/BattleshipIconWhite.svg) no-repeat center /
cover;
mask: url(/images/BattleshipIconWhite.svg) no-repeat center / cover;
}
#helpModal .missile-silo-icon {
-webkit-mask: url(/images/MissileSiloIconWhite.svg) no-repeat center /
cover;
mask: url(/images/MissileSiloIconWhite.svg) no-repeat center / cover;
}
#helpModal .sam-launcher-icon {
-webkit-mask: url(/images/SamLauncherIconWhite.svg) no-repeat center /
cover;
mask: url(/images/SamLauncherIconWhite.svg) no-repeat center / cover;
}
#helpModal .atom-bomb-icon {
-webkit-mask: url(/images/NukeIconWhite.svg) no-repeat center / cover;
mask: url(/images/NukeIconWhite.svg) no-repeat center / cover;
}
#helpModal .hydrogen-bomb-icon {
-webkit-mask: url(/images/MushroomCloudIconWhite.svg) no-repeat center /
cover;
mask: url(/images/MushroomCloudIconWhite.svg) no-repeat center / cover;
}
#helpModal .mirv-icon {
-webkit-mask: url(/images/MIRVIcon.svg) no-repeat center / cover;
mask: url(/images/MIRVIcon.svg) no-repeat center / cover;
}
#helpModal .target-icon {
-webkit-mask: url(/images/TargetIcon.svg) no-repeat center / cover;
mask: url(/images/TargetIcon.svg) no-repeat center / cover;
}
#helpModal .alliance-icon {
-webkit-mask: url(/images/AllianceIconWhite.svg) no-repeat center / cover;
mask: url(/images/AllianceIconWhite.svg) no-repeat center / cover;
}
#helpModal .emoji-icon {
-webkit-mask: url(/images/EmojiIconWhite.svg) no-repeat center / cover;
mask: url(/images/EmojiIconWhite.svg) no-repeat center / cover;
}
#helpModal .betray-icon {
-webkit-mask: url(/images/TraitorIconWhite.svg) no-repeat center / cover;
mask: url(/images/TraitorIconWhite.svg) no-repeat center / cover;
}
#helpModal .donate-icon {
-webkit-mask: url(/images/DonateIconWhite.svg) no-repeat center / cover;
mask: url(/images/DonateIconWhite.svg) no-repeat center / cover;
}
#helpModal .build-icon {
-webkit-mask: url(/images/BuildIconWhite.svg) no-repeat center / cover;
mask: url(/images/BuildIconWhite.svg) no-repeat center / cover;
}
#helpModal .info-icon {
-webkit-mask: url(/images/InfoIcon.svg) no-repeat center / cover;
mask: url(/images/InfoIcon.svg) no-repeat center / cover;
}
#helpModal .boat-icon {
-webkit-mask: url(/images/BoatIcon.svg) no-repeat center / cover;
mask: url(/images/BoatIcon.svg) no-repeat center / cover;
}
#helpModal .cancel-icon {
-webkit-mask: url(/images/XIcon.svg) no-repeat center / cover;
mask: url(/images/XIcon.svg) no-repeat center / cover;
}
@media screen and (max-width: 768px) {
#helpModal .modal-content {
max-height: 90vh;
max-width: 100vw;
width: 100%;
}
`;
@query("o-modal") private modalEl!: HTMLElement & {
open: () => void;
close: () => void;
};
createRenderRoot() {
// Disable shadow DOM to allow Tailwind classes to work
return this;
}
render() {
return html`
<style>
${this.styles}
</style>
<div
id="helpModal"
class="modal-overlay"
style="display: ${this.isModalOpen ? "flex" : "none"}"
>
<div
class="absolute left-0 top-0 w-full h-full ${
this.isModalOpen ? "" : "hidden"
}"
@click=${this.close}
></div>
<div class="modal-content">
<span class="close" @click=${this.close}>&times;</span>
<o-modal id="helpModal" title="Instructions" translationKey="main.instructions">
<div class="flex flex-col items-center">
<div data-i18n="help_modal.hotkeys" class="text-center text-2xl font-bold mb-4">Hotkeys</div>
<table>
<thead>
<tr>
<th data-i18n="help_modal.table_key">Key</th>
<th data-i18n="help_modal.table_action">Action</th>
</tr>
</thead>
<tbody class="text-left">
<tr>
<td>Space</td>
<td data-i18n="help_modal.action_alt_view">Alternate view (terrain/countries)</td>
</tr>
<tr>
<td>Shift + left click</td>
<td data-i18n="help_modal.action_attack_altclick">Attack (when left click is set to open menu)</td>
</tr>
<tr>
<td>Ctrl + left click</td>
<td data-i18n="help_modal.action_build">Open build menu</td>
</tr>
<tr>
<td>C</td>
<td data-i18n="help_modal.action_center">Center camera on player</td>
</tr>
<tr>
<td>Q / E</td>
<td data-i18n="help_modal.action_zoom">Zoom out/in</td>
</tr>
<tr>
<td>W / A / S / D</td>
<td data-i18n="help_modal.action_move_camera">Move camera</td>
</tr>
<tr>
<td>1 / 2</td>
<td data-i18n="help_modal.action_ratio_change">Decrease/Increase attack ratio</td>
</tr>
<tr>
<td>Shift + scroll down / scroll up</td>
<td data-i18n="help_modal.action_ratio_change">Decrease/Increase attack ratio</td>
</tr>
<tr>
<td>ALT + R</td>
<td data-i18n="help_modal.action_reset_gfx">Reset graphics</td>
</tr>
</tbody>
</table>
</div>
<hr class="mt-6 mb-4">
<div data-i18n="help_modal.ui_section" class="text-2xl font-bold text-center mb-4">Game UI</div>
<div class="flex flex-col md:flex-row gap-4">
<div class="flex flex-col items-center">
<div data-i18n="help_modal.hotkeys" class="text-center text-2xl font-bold mb-4">Hotkeys</div>
<table>
<thead>
<tr>
<th data-i18n="help_modal.table_key">Key</th>
<th data-i18n="help_modal.table_action">Action</th>
</tr>
</thead>
<tbody class="text-left">
<tr>
<td>Space</td>
<td data-i18n="help_modal.action_alt_view">Alternate view (terrain/countries)</td>
</tr>
<tr>
<td>Shift + left click</td>
<td data-i18n="help_modal.action_attack_altclick">Attack (when left click is set to open menu)</td>
</tr>
<tr>
<td>Ctrl + left click</td>
<td data-i18n="help_modal.action_build">Open build menu</td>
</tr>
<tr>
<td>C</td>
<td data-i18n="help_modal.action_center">Center camera on player</td>
</tr>
<tr>
<td>Q / E</td>
<td data-i18n="help_modal.action_zoom">Zoom out/in</td>
</tr>
<tr>
<td>W / A / S / D</td>
<td data-i18n="help_modal.action_move_camera">Move camera</td>
</tr>
<tr>
<td>1 / 2</td>
<td data-i18n="help_modal.action_ratio_change">Decrease/Increase attack ratio</td>
</tr>
<tr>
<td>Shift + scroll down / scroll up</td>
<td data-i18n="help_modal.action_ratio_change">Decrease/Increase attack ratio</td>
</tr>
<tr>
<td>ALT + R</td>
<td data-i18n="help_modal.action_reset_gfx">Reset graphics</td>
</tr>
</tbody>
</table>
<div data-i18n="help_modal.ui_leaderboard" class="text-gray-300">Leaderboard</div>
<img src="/images/helpModal/leaderboard.png" alt="Leaderboard" title="Leaderboard" />
</div>
<hr class="mt-6 mb-4">
<div data-i18n="help_modal.ui_section" class="text-2xl font-bold text-center mb-4">Game UI</div>
<div class="flex flex-col md:flex-row gap-4">
<div class="flex flex-col items-center">
<div data-i18n="help_modal.ui_leaderboard" class="text-gray-300">Leaderboard</div>
<img src="/images/helpModal/leaderboard.png" alt="Leaderboard" title="Leaderboard" />
</div>
<div>
<p data-i18n="help_modal.ui_leaderboard_desc">Shows the top players of the game and their names, % owned land and gold.</p>
</div>
</div>
<hr class="mt-6 mb-4">
<div class="flex flex-col md:flex-row gap-4">
<div class="flex flex-col items-center w-full md:w-[80%]">
<div data-i18n="help_modal.ui_control" class="text-gray-300">Control panel</div>
<img src="/images/helpModal/controlPanel.png" alt="Control panel" title="Control panel" />
</div>
<div>
<p data-i18n="help_modal.ui_control_desc" class="mb-4">The control panel contains the following elements:</p>
<ul>
<li data-i18n="help_modal.ui_pop" class="mb-4">Pop - The amount of units you have, your max population and the rate at which you gain them.</li>
<li data-i18n="help_modal.ui_gold" class="mb-4">Gold - The amount of gold you have and the rate at which you gain it.</li>
<li data-i18n="help_modal.ui_troops_workers" class="mb-4">Troops and Workers - The amount of allocated troops and workers. Troops are used to attack or defend against attacks. Workers are used to generate gold. You can adjust the number of troops and workers using the slider.</li>
<li data-i18n="help_modal.ui_attack_ratio" class="mb-4">Attack ratio - The amount of troops that will be used when you attack. You can adjust the attack ratio using the slider.</li>
</ul>
</div>
</div>
<hr class="mt-6 mb-4">
<div class="flex flex-col md:flex-row gap-4">
<div class="flex flex-col items-center">
<div data-i18n="help_modal.ui_options" class="text-gray-300">Options</div>
<img src="/images/helpModal/options.png" alt="Options" title="Options" />
</div>
<div>
<p data-i18n="help_modal.ui_options_desc" class="mb-4">The following elements can be found inside:</p>
<ul>
<li data-i18n="help_modal.option_pause" class="mb-4">Pause/Unpause the game - Only available in single player mode.</li>
<li data-i18n="help_modal.option_timer" class="mb-4">Timer - Time passed since the start of the game.</li>
<li data-i18n="help_modal.option_exit" class="mb-4">Exit button.</li>
<li data-i18n="help_modal.option_settings" class="mb-4">Settings - Open the settings menu. Inside you can toggle the Alternate View, Dark Mode, Emojis and action on left click.</li>
</ul>
</div>
</div>
<hr class="mt-6 mb-4">
<div data-i18n="radial_title" class="text-2xl font-bold text-center">Radial menu</div>
<div class="flex flex-col md:flex-row gap-4">
<img src="/images/helpModal/radialMenu.png" alt="Radial menu" title="Radial menu", class="radial-menu-image" />
<div>
<p data-i18n="help_modal.radial_desc" class="mb-4">Right clicking (or touch on mobile) opens the radial menu. From there you can:</p>
<ul>
<li class="mb-4"><div class="inline-block icon build-icon"></div><span data-i18n="help_modal.radial_build"> - Open the build menu.</span></li>
<li class="mb-4">
<img src="/images/InfoIcon.svg" class="inline-block icon" style="fill: white; background: transparent;"/><span data-i18n="help_modal.radial_info"> - Open the Info menu.</span></li>
<li class="mb-4"><div class="inline-block icon boat-icon"></div><span data-i18n="help_modal.radial_boat"> - Send a boat to attack at the selected location (only available if you have access to water).</span></li>
<li class="mb-4"><div class="inline-block icon cancel-icon"></div><span data-i18n="help_modal.radial_close"> - Close the menu.</span></li>
</ul>
</div>
</div>
<hr class="mt-6 mb-4">
<div>
<div data-i18n="help_modal.info_title" class="text-2xl font-bold text-center">Info menu</div>
<div class="flex flex-col md:flex-row gap-4 mt-2">
<div class="flex flex-col items-center w-full md:w-[80%]">
<div data-i18n="help_modal.info_enemy_panel" class="text-gray-300">Enemy info panel</div>
<img src="/images/helpModal/infoMenu.png" alt="Enemy info panel" title="Enemy info panel" />
</div>
<div class="pt-4">
<p data-i18n="help_modal.info_enemy_desc" class="mb-4">
Contains information such for the selected player name, gold, troops, and if the player is a traitor. Traitor is a player who betrayed and attacked a player who was in an alliance with them. The icons below represent the following interactions:
</p>
<ul>
<li class="mb-4"><div class="inline-block icon target-icon"></div><span data-i18n="help_modal.info_target"> - Place a target mark on the player, marking it for all allies, used to coordinate attacks.</span></li>
<li class="mb-4"><div class="inline-block icon alliance-icon"></div><span data-i18n="help_modal.info_alliance"> - Send an alliance request to the player. Allies can share resources and troops, but can't attack each other.</span></li>
<li><div class="inline-block icon emoji-icon"></div><span data-i18n="help_modal.info_emoji"> - Send an emoji to the player.</span></li>
</ul>
</div>
</div>
<hr class="mt-6 mb-4">
<div class="flex flex-col md:flex-row gap-4">
<div class="flex flex-col items-center w-full md:w-[62%]">
<div data-i18n="help_modal.info_ally_panel" class="text-gray-300">Ally info panel</div>
<img src="/images/helpModal/infoMenuAlly.png" alt="Ally info panel" title="Ally info panel" />
</div>
<div class="pt-4">
<p data-i18n="help_modal.info_ally_desc" class="mb-4">
When you ally with a player, the following new icons become available:
</p>
<ul>
<li class="mb-4"><div class="inline-block icon betray-icon"></div><span data-i18n="help_modal.ally_betray"> - Betray your ally, ending the alliance. You will now have a permanent icon stuck next to your name. Bots are less likely to ally with you and players will think twice before doing so.</span></li>
<li class="mb-4"><div class="inline-block icon donate-icon"></div><span data-i18n="help_modal.ally_donate"> - Donate some of your troops to your ally. Used when they're low on troops and are being attacked, or when they need that extra power to crush an enemy.</span></li>
</ul>
</div>
</div>
</div>
<hr class="mt-6 mb-4">
<div>
<div data-i18n="help_modal.build_menu_title" class="text-2xl font-bold mb-4 text-center">Build menu</div>
<table>
<thead>
<tr>
<th data-i18n="help_modal.build_name">Name</th>
<th data-i18n="help_modal.build_icon">Icon</th>
<th data-i18n="help_modal.build_desc">Description</th>
</tr>
</thead>
<tbody class="text-left">
<tr>
<td data-i18n="help_modal.build_city">City</td>
<td><div class="icon city-icon"></div></td>
<td data-i18n="help_modal.build_city_desc">
Increases your max population. Useful when you can't
expand your territory or you're about to hit your
population limit.
</td>
</tr>
<tr>
<td data-i18n="help_modal.build_defense">Defense Post</td>
<td><div class="icon defense-post-icon"></div></td>
<td data-i18n="help_modal.build_defense_desc">
Increases defenses around nearby borders. Attacks from
enemies are slower and have more casualties.
</td>
</tr>
<tr>
<td data-i18n="help_modal.build_port">Port</td>
<td><div class="icon port-icon"></div></td>
<td data-i18n="help_modal.build_port_desc">
Automatically sends trade ships between ports of your
country and other countries (except if you clicked "stop
trade" on them or they clicked "stop trade on you"), giving
gold to both sides. Allows building Battleships. Can only
be built near water.
</td>
</tr>
<tr>
<td data-i18n="help_modal.build_warship">Warship</td>
<td><div class="icon warship-icon"></div></td>
<td data-i18n="help_modal.build_warship_desc">
Patrols in an area, capturing trade ships and destroying
enemy Warships and Boats. Spawns from the nearest Port and
patrols the area you first clicked to build it.
</td>
</tr>
<tr>
<td data-i18n="help_modal.build_silo">Missile Silo</td>
<td><div class="icon missile-silo-icon"></div></td>
<td data-i18n="help_modal.build_silo_desc">Allows launching missiles.</td>
</tr>
<tr>
<td data-i18n="help_modal.build_sam">SAM Launcher</td>
<td><div class="icon sam-launcher-icon"></div></td>
<td data-i18n="help_modal.build_sam_desc">Has a 75% chance to intercept enemy missiles in it's 100 pixel range.
The SAM has a 7.5 second cooldown and can not intercept MIRVs.</td>
</tr>
<tr>
<td data-i18n="help_modal.build_atom">Atom Bomb</td>
<td><div class="icon atom-bomb-icon"></div></td></td>
<td data-i18n="help_modal.build_atom_desc">Small explosive bomb that destroys territory, buildings, ships and boats. Spawns from the nearest Missile Silo and lands in the area you first clicked to build it.</td>
</tr>
<tr>
<td data-i18n="help_modal.build_hydrogen">Hydrogen Bomb</td>
<td><div class="icon hydrogen-bomb-icon"></div></td></td>
<td data-i18n="help_modal.build_hydrogen_desc">Large explosive bomb. Spawns from the nearest Missile Silo and lands in the area you first clicked to build it.</td>
</tr>
<tr>
<td data-i18n="help_modal.build_mirv">MIRV</td>
<td><div class="icon mirv-icon"></div></td>
<td data-i18n="help_modal.build_mirv_desc">The most powerful bomb in the game. Splits up into smaller bombs that will cover a huge range of territory. Only damages the player that you first clicked on to build it. Spawns from the nearest Missile Silo and lands in the area you first clicked to build it.</td>
</tr>
</tbody>
</table>
</div>
<hr class="mt-6 mb-4">
<div>
<div data-i18n="help_modal.player_icons" class="text-2xl font-bold text-center">Player icons</div>
<p data-i18n="help_modal.icon_desc">Examples of some of the ingame icons you will encounter and what they mean:</p>
<div class="flex flex-col md:flex-row gap-4 mt-2">
<div class="flex flex-col items-center">
<div data-i18n="help_modal.icon_crown" class="text-gray-300">Crown - This is the number 1 player in the leaderboard</div>
<img src="/images/helpModal/number1.png" alt="Number 1 player" title="Number 1 player" />
</div>
<div class="flex flex-col items-center">
<div data-i18n="help_modal.icon_traitor" class="text-gray-300">Crossed swords - Traitor. This player attacked an ally.</div>
<img src="/images/helpModal/traitor.png" alt="Traitor" title="Traitor" />
</div>
<div class="flex flex-col items-center">
<div data-i18n="help_modal.icon_ally" class="text-gray-300">Handshake - Ally. This player is your ally.</div>
<img src="/images/helpModal/ally.png" alt="Ally" title="Ally" />
</div>
<p data-i18n="help_modal.ui_leaderboard_desc">Shows the top players of the game and their names, % owned land and gold.</p>
</div>
</div>
</div>
</div>
<hr class="mt-6 mb-4">
<div class="flex flex-col md:flex-row gap-4">
<div class="flex flex-col items-center w-full md:w-[80%]">
<div data-i18n="help_modal.ui_control" class="text-gray-300">Control panel</div>
<img src="/images/helpModal/controlPanel.png" alt="Control panel" title="Control panel" />
</div>
<div>
<p data-i18n="help_modal.ui_control_desc" class="mb-4">The control panel contains the following elements:</p>
<ul>
<li data-i18n="help_modal.ui_pop" class="mb-4">Pop - The amount of units you have, your max population and the rate at which you gain them.</li>
<li data-i18n="help_modal.ui_gold" class="mb-4">Gold - The amount of gold you have and the rate at which you gain it.</li>
<li data-i18n="help_modal.ui_troops_workers" class="mb-4">Troops and Workers - The amount of allocated troops and workers. Troops are used to attack or defend against attacks. Workers are used to generate gold. You can adjust the number of troops and workers using the slider.</li>
<li data-i18n="help_modal.ui_attack_ratio" class="mb-4">Attack ratio - The amount of troops that will be used when you attack. You can adjust the attack ratio using the slider.</li>
</ul>
</div>
</div>
<hr class="mt-6 mb-4">
<div class="flex flex-col md:flex-row gap-4">
<div class="flex flex-col items-center">
<div data-i18n="help_modal.ui_options" class="text-gray-300">Options</div>
<img src="/images/helpModal/options.png" alt="Options" title="Options" />
</div>
<div>
<p data-i18n="help_modal.ui_options_desc" class="mb-4">The following elements can be found inside:</p>
<ul>
<li data-i18n="help_modal.option_pause" class="mb-4">Pause/Unpause the game - Only available in single player mode.</li>
<li data-i18n="help_modal.option_timer" class="mb-4">Timer - Time passed since the start of the game.</li>
<li data-i18n="help_modal.option_exit" class="mb-4">Exit button.</li>
<li data-i18n="help_modal.option_settings" class="mb-4">Settings - Open the settings menu. Inside you can toggle the Alternate View, Dark Mode, Emojis and action on left click.</li>
</ul>
</div>
</div>
<hr class="mt-6 mb-4">
<div data-i18n="radial_title" class="text-2xl font-bold text-center">Radial menu</div>
<div class="flex flex-col md:flex-row gap-4">
<img src="/images/helpModal/radialMenu.png" alt="Radial menu" title="Radial menu", class="radial-menu-image" />
<div>
<p data-i18n="help_modal.radial_desc" class="mb-4">Right clicking (or touch on mobile) opens the radial menu. From there you can:</p>
<ul>
<li class="mb-4"><div class="inline-block icon build-icon"></div><span data-i18n="help_modal.radial_build"> - Open the build menu.</span></li>
<li class="mb-4">
<img src="/images/InfoIcon.svg" class="inline-block icon" style="fill: white; background: transparent;"/><span data-i18n="help_modal.radial_info"> - Open the Info menu.</span></li>
<li class="mb-4"><div class="inline-block icon boat-icon"></div><span data-i18n="help_modal.radial_boat"> - Send a boat to attack at the selected location (only available if you have access to water).</span></li>
<li class="mb-4"><div class="inline-block icon cancel-icon"></div><span data-i18n="help_modal.radial_close"> - Close the menu.</span></li>
</ul>
</div>
</div>
<hr class="mt-6 mb-4">
<div>
<div data-i18n="help_modal.info_title" class="text-2xl font-bold text-center">Info menu</div>
<div class="flex flex-col md:flex-row gap-4 mt-2">
<div class="flex flex-col items-center w-full md:w-[80%]">
<div data-i18n="help_modal.info_enemy_panel" class="text-gray-300">Enemy info panel</div>
<img src="/images/helpModal/infoMenu.png" alt="Enemy info panel" title="Enemy info panel" />
</div>
<div class="pt-4">
<p data-i18n="help_modal.info_enemy_desc" class="mb-4">
Contains information such for the selected player name, gold, troops, and if the player is a traitor. Traitor is a player who betrayed and attacked a player who was in an alliance with them. The icons below represent the following interactions:
</p>
<ul>
<li class="mb-4"><div class="inline-block icon target-icon"></div><span data-i18n="help_modal.info_target"> - Place a target mark on the player, marking it for all allies, used to coordinate attacks.</span></li>
<li class="mb-4"><div class="inline-block icon alliance-icon"></div><span data-i18n="help_modal.info_alliance"> - Send an alliance request to the player. Allies can share resources and troops, but can't attack each other.</span></li>
<li><div class="inline-block icon emoji-icon"></div><span data-i18n="help_modal.info_emoji"> - Send an emoji to the player.</span></li>
</ul>
</div>
</div>
<hr class="mt-6 mb-4">
<div class="flex flex-col md:flex-row gap-4">
<div class="flex flex-col items-center w-full md:w-[62%]">
<div data-i18n="help_modal.info_ally_panel" class="text-gray-300">Ally info panel</div>
<img src="/images/helpModal/infoMenuAlly.png" alt="Ally info panel" title="Ally info panel" />
</div>
<div class="pt-4">
<p data-i18n="help_modal.info_ally_desc" class="mb-4">
When you ally with a player, the following new icons become available:
</p>
<ul>
<li class="mb-4"><div class="inline-block icon betray-icon"></div><span data-i18n="help_modal.ally_betray"> - Betray your ally, ending the alliance. You will now have a permanent icon stuck next to your name. Bots are less likely to ally with you and players will think twice before doing so.</span></li>
<li class="mb-4"><div class="inline-block icon donate-icon"></div><span data-i18n="help_modal.ally_donate"> - Donate some of your troops to your ally. Used when they're low on troops and are being attacked, or when they need that extra power to crush an enemy.</span></li>
</ul>
</div>
</div>
</div>
<hr class="mt-6 mb-4">
<div>
<div data-i18n="help_modal.build_menu_title" class="text-2xl font-bold mb-4 text-center">Build menu</div>
<table>
<thead>
<tr>
<th data-i18n="help_modal.build_name">Name</th>
<th data-i18n="help_modal.build_icon">Icon</th>
<th data-i18n="help_modal.build_desc">Description</th>
</tr>
</thead>
<tbody class="text-left">
<tr>
<td data-i18n="help_modal.build_city">City</td>
<td><div class="icon city-icon"></div></td>
<td data-i18n="help_modal.build_city_desc">
Increases your max population. Useful when you can't
expand your territory or you're about to hit your
population limit.
</td>
</tr>
<tr>
<td data-i18n="help_modal.build_defense">Defense Post</td>
<td><div class="icon defense-post-icon"></div></td>
<td data-i18n="help_modal.build_defense_desc">
Increases defenses around nearby borders. Attacks from
enemies are slower and have more casualties.
</td>
</tr>
<tr>
<td data-i18n="help_modal.build_port">Port</td>
<td><div class="icon port-icon"></div></td>
<td data-i18n="help_modal.build_port_desc">
Automatically sends trade ships between ports of your
country and other countries (except if you clicked "stop
trade" on them or they clicked "stop trade on you"), giving
gold to both sides. Allows building Battleships. Can only
be built near water.
</td>
</tr>
<tr>
<td data-i18n="help_modal.build_warship">Warship</td>
<td><div class="icon warship-icon"></div></td>
<td data-i18n="help_modal.build_warship_desc">
Patrols in an area, capturing trade ships and destroying
enemy Warships and Boats. Spawns from the nearest Port and
patrols the area you first clicked to build it.
</td>
</tr>
<tr>
<td data-i18n="help_modal.build_silo">Missile Silo</td>
<td><div class="icon missile-silo-icon"></div></td>
<td data-i18n="help_modal.build_silo_desc">Allows launching missiles.</td>
</tr>
<tr>
<td data-i18n="help_modal.build_sam">SAM Launcher</td>
<td><div class="icon sam-launcher-icon"></div></td>
<td data-i18n="help_modal.build_sam_desc">Has a 75% chance to intercept enemy missiles in it's 100 pixel range.
The SAM has a 7.5 second cooldown and can not intercept MIRVs.</td>
</tr>
<tr>
<td data-i18n="help_modal.build_atom">Atom Bomb</td>
<td><div class="icon atom-bomb-icon"></div></td></td>
<td data-i18n="help_modal.build_atom_desc">Small explosive bomb that destroys territory, buildings, ships and boats. Spawns from the nearest Missile Silo and lands in the area you first clicked to build it.</td>
</tr>
<tr>
<td data-i18n="help_modal.build_hydrogen">Hydrogen Bomb</td>
<td><div class="icon hydrogen-bomb-icon"></div></td></td>
<td data-i18n="help_modal.build_hydrogen_desc">Large explosive bomb. Spawns from the nearest Missile Silo and lands in the area you first clicked to build it.</td>
</tr>
<tr>
<td data-i18n="help_modal.build_mirv">MIRV</td>
<td><div class="icon mirv-icon"></div></td>
<td data-i18n="help_modal.build_mirv_desc">The most powerful bomb in the game. Splits up into smaller bombs that will cover a huge range of territory. Only damages the player that you first clicked on to build it. Spawns from the nearest Missile Silo and lands in the area you first clicked to build it.</td>
</tr>
</tbody>
</table>
</div>
<hr class="mt-6 mb-4">
<div>
<div data-i18n="help_modal.player_icons" class="text-2xl font-bold text-center">Player icons</div>
<p data-i18n="help_modal.icon_desc">Examples of some of the ingame icons you will encounter and what they mean:</p>
<div class="flex flex-col md:flex-row gap-4 mt-2">
<div class="flex flex-col items-center">
<div data-i18n="help_modal.icon_crown" class="text-gray-300">Crown - This is the number 1 player in the leaderboard</div>
<img src="/images/helpModal/number1.png" alt="Number 1 player" title="Number 1 player" />
</div>
<div class="flex flex-col items-center">
<div data-i18n="help_modal.icon_traitor" class="text-gray-300">Crossed swords - Traitor. This player attacked an ally.</div>
<img src="/images/helpModal/traitor.png" alt="Traitor" title="Traitor" />
</div>
<div class="flex flex-col items-center">
<div data-i18n="help_modal.icon_ally" class="text-gray-300">Handshake - Ally. This player is your ally.</div>
<img src="/images/helpModal/ally.png" alt="Ally" title="Ally" />
</div>
</div>
</div>
</o-modal>
`;
}
public open() {
this.isModalOpen = true;
this.modalEl?.open();
}
public close() {
this.isModalOpen = false;
console.log("closing modal");
this.modalEl?.close();
}
}
+219 -517
View File
@@ -1,20 +1,23 @@
import { LitElement, html, css } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { LitElement, html } from "lit";
import { customElement, query, property, state } from "lit/decorators.js";
import { Difficulty, GameMapType, GameType } from "../core/game/Game";
import { GameConfig, GameInfo } from "../core/Schemas";
import { consolex } from "../core/Consolex";
import "./components/Difficulties";
import "./components/baseComponents/Modal";
import { DifficultyDescription } from "./components/Difficulties";
import "./components/Maps";
import randomMap from "../../resources/images/RandomMap.png";
import { generateID } from "../core/Util";
import { getServerConfigFromClient } from "../core/configuration/ConfigLoader";
import { getConfig } from "../core/configuration/ConfigLoader";
import { JoinLobbyEvent } from "./Main";
@customElement("host-lobby-modal")
export class HostLobbyModal extends LitElement {
@state() private isModalOpen = false;
@query("o-modal") private modalEl!: HTMLElement & {
open: () => void;
close: () => void;
};
@state() private selectedMap: GameMapType = GameMapType.World;
@state() private selectedDifficulty: Difficulty = Difficulty.Medium;
@state() private disableNPCs = false;
@@ -32,526 +35,222 @@ export class HostLobbyModal extends LitElement {
// Add a new timer for debouncing bot changes
private botsUpdateTimer: number | null = null;
static styles = css`
.modal-overlay {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
overflow-y: auto;
display: flex;
align-items: center;
justify-content: center;
}
.modal-content {
background-color: rgb(35 35 35 / 0.8);
-webkit-backdrop-filter: blur(12px);
backdrop-filter: blur(12px);
color: white;
padding: 20px;
border-radius: 8px;
width: 80%;
max-width: 1280px;
max-height: 80vh;
overflow-y: auto;
text-align: center;
box-shadow: 0 0 40px rgba(0, 0, 0, 0.5);
border: 1px solid rgba(255, 255, 255, 0.2);
backdrop-filter: blur(8px);
position: relative;
}
/* Add custom scrollbar styles */
.modal-content::-webkit-scrollbar {
width: 8px;
}
.modal-content::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.1);
border-radius: 4px;
}
.modal-content::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 4px;
}
.modal-content::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}
.title {
font-size: 28px;
color: #fff;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
padding: 0 0 0 20px;
}
.close {
position: sticky;
top: 0px;
right: 0px;
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
.close:hover,
.close:focus {
color: white;
text-decoration: none;
cursor: pointer;
}
.start-game-button {
width: 100%;
max-width: 300px;
padding: 15px 20px;
font-size: 16px;
cursor: pointer;
background-color: #007bff;
color: white;
border: none;
border-radius: 8px;
transition: background-color 0.3s;
display: inline-block;
margin: 0 0 20px 0;
}
.start-game-button:not(:disabled):hover {
background-color: #0056b3;
}
.start-game-button:disabled {
background: linear-gradient(to right, #4a4a4a, #3d3d3d);
opacity: 0.7;
cursor: not-allowed;
}
.options-layout {
display: grid;
grid-template-columns: 1fr;
gap: 24px;
margin: 24px 0;
}
.options-section {
background: rgba(0, 0, 0, 0.2);
padding: 12px 24px 24px 24px;
border-radius: 12px;
}
.option-title {
margin: 0 0 16px 0;
font-size: 20px;
color: #fff;
text-align: center;
}
.option-cards {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
gap: 16px;
}
.option-card {
width: 100%;
min-width: 100px;
max-width: 120px;
padding: 4px 4px 0 4px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
background: rgba(30, 30, 30, 0.95);
border: 2px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
cursor: pointer;
transition: all 0.2s ease-in-out;
}
.option-card:hover {
transform: translateY(-2px);
border-color: rgba(255, 255, 255, 0.3);
background: rgba(40, 40, 40, 0.95);
}
.option-card.selected {
border-color: #4a9eff;
background: rgba(74, 158, 255, 0.1);
}
.option-card-title {
font-size: 14px;
color: #aaa;
text-align: center;
margin: 0 0 4px 0;
}
.option-image {
width: 100%;
aspect-ratio: 4/2;
color: #aaa;
transition: transform 0.2s ease-in-out;
border-radius: 8px;
background-color: rgba(255, 255, 255, 0.1);
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
}
.option-card input[type="checkbox"] {
display: none;
}
label.option-card:hover {
transform: none;
}
.checkbox-icon {
width: 16px;
height: 16px;
border: 2px solid #aaa;
border-radius: 6px;
margin: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease-in-out;
}
.option-card.selected .checkbox-icon {
border-color: #4a9eff;
background: #4a9eff;
}
.option-card.selected .checkbox-icon::after {
content: "✓";
color: white;
}
/* HostLobbyModal css */
.clipboard-icon {
display: flex;
align-items: center;
justify-content: center;
color: #fff;
}
.copy-success {
position: relative;
transform: translateY(-10px);
color: green;
font-size: 14px;
margin-top: 5px;
}
.copy-success-icon {
width: 18px;
height: 18px;
color: #4caf50;
}
.lobby-id-box {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
margin: 8px 0px 0px 0px;
}
.lobby-id-button {
display: flex;
align-items: center;
gap: 8px;
background: rgba(0, 0, 0, 0.2);
padding: 8px 16px;
border-radius: 6px;
border: 1px solid rgba(255, 255, 255, 0.1);
cursor: pointer;
transition: all 0.2s ease-in-out;
}
.lobby-id-button:hover {
background: rgba(0, 0, 0, 0.3);
border-color: rgba(255, 255, 255, 0.2);
transform: translateY(-1px);
}
.lobby-id {
font-size: 14px;
color: #fff;
text-align: center;
}
.players-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: center;
padding: 0 16px;
}
.player-tag {
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.1);
padding: 4px 16px;
border-radius: 16px;
font-size: 14px;
color: #fff;
border: 1px solid rgba(255, 255, 255, 0.2);
}
#bots-count {
width: 80%;
}
@media screen and (max-width: 768px) {
.modal-content {
max-height: calc(90vh - 42px);
max-width: 100vw;
width: 100%;
}
`;
render() {
return html`
<div
class="modal-overlay"
style="display: ${this.isModalOpen ? "flex" : "none"}"
>
<div
style="position: absolute; left: 0px; top: 0px; width: 100%; height: 100%;"
class="${this.isModalOpen ? "" : "hidden"}"
@click=${this.close}
></div>
<div class="modal-content">
<span class="close" @click=${this.close}>&times;</span>
<div class="title">Private Lobby</div>
<div class="lobby-id-box">
<button
class="lobby-id-button"
@click=${this.copyToClipboard}
?disabled=${this.copySuccess}
>
<span class="lobby-id">${this.lobbyId}</span>
${this.copySuccess
? html`<span class="copy-success-icon">✓</span>`
: html`
<svg
class="clipboard-icon"
stroke="currentColor"
fill="currentColor"
stroke-width="0"
viewBox="0 0 512 512"
height="18px"
width="18px"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M296 48H176.5C154.4 48 136 65.4 136 87.5V96h-7.5C106.4 96 88 113.4 88 135.5v288c0 22.1 18.4 40.5 40.5 40.5h208c22.1 0 39.5-18.4 39.5-40.5V416h8.5c22.1 0 39.5-18.4 39.5-40.5V176L296 48zm0 44.6l83.4 83.4H296V92.6zm48 330.9c0 4.7-3.4 8.5-7.5 8.5h-208c-4.4 0-8.5-4.1-8.5-8.5v-288c0-4.1 3.8-7.5 8.5-7.5h7.5v255.5c0 22.1 10.4 32.5 32.5 32.5H344v7.5zm48-48c0 4.7-3.4 8.5-7.5 8.5h-208c-4.4 0-8.5-4.1-8.5-8.5v-288c0-4.1 3.8-7.5 8.5-7.5H264v128h128v167.5z"
></path>
</svg>
`}
</button>
</div>
<div class="options-layout">
<!-- Map Selection -->
<div class="options-section">
<div class="option-title">Map</div>
<div class="option-cards">
${Object.entries(GameMapType)
.filter(([key]) => isNaN(Number(key)))
.map(
([key, value]) => html`
<div @click=${() => this.handleMapSelection(value)}>
<map-display
.mapKey=${key}
.selected=${!this.useRandomMap &&
this.selectedMap === value}
></map-display>
</div>
`,
)}
<div
class="option-card random-map ${this.useRandomMap
? "selected"
: ""}"
@click=${this.handleRandomMapToggle}
>
<div class="option-image">
<img
src=${randomMap}
alt="Random Map"
style="width:100%; aspect-ratio: 4/2; object-fit:cover; border-radius:8px;"
/>
</div>
<div class="option-card-title">Random</div>
</div>
</div>
</div>
<!-- Difficulty Selection -->
<div class="options-section">
<div class="option-title">Difficulty</div>
<div class="option-cards">
${Object.entries(Difficulty)
.filter(([key]) => isNaN(Number(key)))
.map(
([key, value]) => html`
<div
class="option-card ${this.selectedDifficulty === value
? "selected"
: ""}"
@click=${() => this.handleDifficultySelection(value)}
>
<difficulty-display
.difficultyKey=${key}
></difficulty-display>
<p class="option-card-title">
${DifficultyDescription[key]}
</p>
</div>
`,
)}
</div>
</div>
<!-- Game Options -->
<div class="options-section">
<div class="option-title">Options</div>
<div class="option-cards">
<label for="bots-count" class="option-card">
<input
type="range"
id="bots-count"
min="0"
max="400"
step="1"
@input=${this.handleBotsChange}
@change=${this.handleBotsChange}
.value="${String(this.bots)}"
/>
<div class="option-card-title">
Bots: ${this.bots == 0 ? "Disabled" : this.bots}
</div>
</label>
<label
for="disable-npcs"
class="option-card ${this.disableNPCs ? "selected" : ""}"
>
<div class="checkbox-icon"></div>
<input
type="checkbox"
id="disable-npcs"
@change=${this.handleDisableNPCsChange}
.checked=${this.disableNPCs}
/>
<div class="option-card-title">Disable Nations</div>
</label>
<label
for="instant-build"
class="option-card ${this.instantBuild ? "selected" : ""}"
>
<div class="checkbox-icon"></div>
<input
type="checkbox"
id="instant-build"
@change=${this.handleInstantBuildChange}
.checked=${this.instantBuild}
/>
<div class="option-card-title">Instant build</div>
</label>
<label
for="infinite-gold"
class="option-card ${this.infiniteGold ? "selected" : ""}"
>
<div class="checkbox-icon"></div>
<input
type="checkbox"
id="infinite-gold"
@change=${this.handleInfiniteGoldChange}
.checked=${this.infiniteGold}
/>
<div class="option-card-title">Infinite gold</div>
</label>
<label
for="infinite-troops"
class="option-card ${this.infiniteTroops ? "selected" : ""}"
>
<div class="checkbox-icon"></div>
<input
type="checkbox"
id="infinite-troops"
@change=${this.handleInfiniteTroopsChange}
.checked=${this.infiniteTroops}
/>
<div class="option-card-title">Infinite troops</div>
</label>
<label
for="disable-nukes"
class="option-card ${this.disableNukes ? "selected" : ""}"
>
<div class="checkbox-icon"></div>
<input
type="checkbox"
id="disable-nukes"
@change=${this.handleDisableNukesChange}
.checked=${this.disableNukes}
/>
<div class="option-card-title">Disable Nukes</div>
</label>
</div>
</div>
<!-- Lobby Selection -->
<div class="options-section">
<div class="option-title">
${this.players.length}
${this.players.length === 1 ? "Player" : "Players"}
</div>
<div class="players-list">
${this.players.map(
(player) => html`<span class="player-tag">${player}</span>`,
)}
</div>
</div>
</div>
<o-modal title="Private lobby">
<div class="lobby-id-box">
<button
@click=${this.startGame}
?disabled=${this.players.length < 2}
class="start-game-button"
class="lobby-id-button"
@click=${this.copyToClipboard}
?disabled=${this.copySuccess}
>
${this.players.length === 1
? "Waiting for players..."
: "Start Game"}
<span class="lobby-id">${this.lobbyId}</span>
${this.copySuccess
? html`<span class="copy-success-icon">✓</span>`
: html`
<svg
class="clipboard-icon"
stroke="currentColor"
fill="currentColor"
stroke-width="0"
viewBox="0 0 512 512"
height="18px"
width="18px"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M296 48H176.5C154.4 48 136 65.4 136 87.5V96h-7.5C106.4 96 88 113.4 88 135.5v288c0 22.1 18.4 40.5 40.5 40.5h208c22.1 0 39.5-18.4 39.5-40.5V416h8.5c22.1 0 39.5-18.4 39.5-40.5V176L296 48zm0 44.6l83.4 83.4H296V92.6zm48 330.9c0 4.7-3.4 8.5-7.5 8.5h-208c-4.4 0-8.5-4.1-8.5-8.5v-288c0-4.1 3.8-7.5 8.5-7.5h7.5v255.5c0 22.1 10.4 32.5 32.5 32.5H344v7.5zm48-48c0 4.7-3.4 8.5-7.5 8.5h-208c-4.4 0-8.5-4.1-8.5-8.5v-288c0-4.1 3.8-7.5 8.5-7.5H264v128h128v167.5z"
></path>
</svg>
`}
</button>
</div>
</div>
<div class="options-layout">
<!-- Map Selection -->
<div class="options-section">
<div class="option-title">Map</div>
<div class="option-cards">
${Object.entries(GameMapType)
.filter(([key]) => isNaN(Number(key)))
.map(
([key, value]) => html`
<div @click=${() => this.handleMapSelection(value)}>
<map-display
.mapKey=${key}
.selected=${!this.useRandomMap &&
this.selectedMap === value}
></map-display>
</div>
`,
)}
<div
class="option-card random-map ${this.useRandomMap
? "selected"
: ""}"
@click=${this.handleRandomMapToggle}
>
<div class="option-image">
<img
src=${randomMap}
alt="Random Map"
style="width:100%; aspect-ratio: 4/2; object-fit:cover; border-radius:8px;"
/>
</div>
<div class="option-card-title">Random</div>
</div>
</div>
</div>
<!-- Difficulty Selection -->
<div class="options-section">
<div class="option-title">Difficulty</div>
<div class="option-cards">
${Object.entries(Difficulty)
.filter(([key]) => isNaN(Number(key)))
.map(
([key, value]) => html`
<div
class="option-card ${this.selectedDifficulty === value
? "selected"
: ""}"
@click=${() => this.handleDifficultySelection(value)}
>
<difficulty-display
.difficultyKey=${key}
></difficulty-display>
<p class="option-card-title">
${DifficultyDescription[key]}
</p>
</div>
`,
)}
</div>
</div>
<!-- Game Options -->
<div class="options-section">
<div class="option-title">Options</div>
<div class="option-cards">
<label for="private-lobby-bots-count" class="option-card">
<input
type="range"
id="private-lobby-bots-count"
min="0"
max="400"
step="1"
@input=${this.handleBotsChange}
@change=${this.handleBotsChange}
.value="${String(this.bots)}"
/>
<div class="option-card-title">
Bots: ${this.bots == 0 ? "Disabled" : this.bots}
</div>
</label>
<label
for="private-lobby-disable-npcd"
class="option-card ${this.disableNPCs ? "selected" : ""}"
>
<div class="checkbox-icon"></div>
<input
type="checkbox"
id="private-lobby-disable-npcd"
@change=${this.handleDisableNPCsChange}
.checked=${this.disableNPCs}
/>
<div class="option-card-title">Disable Nations</div>
</label>
<label
for="private-lobby-instant-build"
class="option-card ${this.instantBuild ? "selected" : ""}"
>
<div class="checkbox-icon"></div>
<input
type="checkbox"
id="private-lobby-instant-build"
@change=${this.handleInstantBuildChange}
.checked=${this.instantBuild}
/>
<div class="option-card-title">Instant build</div>
</label>
<label
for="private-lobby-infinite-gold"
class="option-card ${this.infiniteGold ? "selected" : ""}"
>
<div class="checkbox-icon"></div>
<input
type="checkbox"
id="private-lobby-infinite-gold"
@change=${this.handleInfiniteGoldChange}
.checked=${this.infiniteGold}
/>
<div class="option-card-title">Infinite gold</div>
</label>
<label
for="private-lobby-infinite-troops"
class="option-card ${this.infiniteTroops ? "selected" : ""}"
>
<div class="checkbox-icon"></div>
<input
type="checkbox"
id="private-lobby-infinite-troops"
@change=${this.handleInfiniteTroopsChange}
.checked=${this.infiniteTroops}
/>
<div class="option-card-title">Infinite troops</div>
</label>
<label
for="private-lobby-disable-nukes"
class="option-card ${this.disableNukes ? "selected" : ""}"
>
<div class="checkbox-icon"></div>
<input
type="checkbox"
id="disable-nukes"
@change=${this.handleDisableNukesChange}
.checked=${this.disableNukes}
/>
<div class="option-card-title">Disable Nukes</div>
</label>
</div>
</div>
<!-- Lobby Selection -->
<div class="options-section">
<div class="option-title">
${this.players.length}
${this.players.length === 1 ? "Player" : "Players"}
</div>
<div class="players-list">
${this.players.map(
(player) => html`<span class="player-tag">${player}</span>`,
)}
</div>
</div>
</div>
<div class="flex justify-center">
<o-button
.title=${this.players.length === 1
? "Waiting for players..."
: "Start Game"}
?disable=${this.players.length < 2}
@click=${this.startGame}
block
>
</o-button>
</div>
</o-modal>
`;
}
createRenderRoot() {
return this;
}
public open() {
createLobby()
.then((lobby) => {
@@ -569,12 +268,12 @@ export class HostLobbyModal extends LitElement {
}),
);
});
this.isModalOpen = true;
this.modalEl?.open();
this.playersInterval = setInterval(() => this.pollPlayers(), 1000);
}
public close() {
this.isModalOpen = false;
this.modalEl?.close();
this.copySuccess = false;
if (this.playersInterval) {
clearInterval(this.playersInterval);
@@ -597,6 +296,7 @@ export class HostLobbyModal extends LitElement {
this.useRandomMap = false;
this.putGameConfig();
}
private async handleDifficultySelection(value: Difficulty) {
this.selectedDifficulty = value;
this.putGameConfig();
@@ -628,10 +328,12 @@ export class HostLobbyModal extends LitElement {
this.instantBuild = Boolean((e.target as HTMLInputElement).checked);
this.putGameConfig();
}
private handleInfiniteGoldChange(e: Event) {
this.infiniteGold = Boolean((e.target as HTMLInputElement).checked);
this.putGameConfig();
}
private handleInfiniteTroopsChange(e: Event) {
this.infiniteTroops = Boolean((e.target as HTMLInputElement).checked);
this.putGameConfig();
+66 -284
View File
@@ -1,311 +1,93 @@
import { LitElement, css, html } from "lit";
import { customElement, query, state } from "lit/decorators.js";
import { consolex } from "../core/Consolex";
import { GameMapType, GameType } from "../core/game/Game";
import { GameInfo, GameRecord } from "../core/Schemas";
import { getServerConfigFromClient } from "../core/configuration/ConfigLoader";
import { JoinLobbyEvent } from "./Main";
import "./components/baseComponents/Modal";
import "./components/baseComponents/Button";
@customElement("join-private-lobby-modal")
export class JoinPrivateLobbyModal extends LitElement {
@state() private isModalOpen = false;
@state() private message: string = "";
@query("o-modal") private modalEl!: HTMLElement & {
open: () => void;
close: () => void;
};
@query("#lobbyIdInput") private lobbyIdInput!: HTMLInputElement;
@state() private message: string = "";
@state() private hasJoined = false;
@state() private players: string[] = [];
private playersInterval = null;
static styles = css`
.modal-overlay {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
overflow-y: auto;
display: flex;
align-items: center;
justify-content: center;
}
.modal-content {
background-color: rgb(35 35 35 / 0.8);
-webkit-backdrop-filter: blur(12px);
backdrop-filter: blur(12px);
color: white;
padding: 20px;
border-radius: 8px;
width: 80%;
max-width: 500px;
max-height: 80vh;
overflow-y: auto;
text-align: center;
box-shadow: 0 0 40px rgba(0, 0, 0, 0.5);
border: 1px solid rgba(255, 255, 255, 0.2);
backdrop-filter: blur(8px);
position: relative;
}
/* Add custom scrollbar styles */
.modal-content::-webkit-scrollbar {
width: 8px;
}
.modal-content::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.1);
border-radius: 4px;
}
.modal-content::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 4px;
}
.modal-content::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}
.title {
font-size: 28px;
color: #fff;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
padding: 0 0 0 20px;
}
.close {
position: sticky;
top: 0px;
right: 0px;
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
.close:hover,
.close:focus {
color: white;
text-decoration: none;
cursor: pointer;
}
.start-game-button {
width: 100%;
max-width: 300px;
padding: 15px 20px;
font-size: 16px;
cursor: pointer;
background-color: #007bff;
color: white;
border: none;
border-radius: 8px;
transition: background-color 0.3s;
display: inline-block;
margin: 0 0 20px 0;
}
.start-game-button:not(:disabled):hover {
background-color: #0056b3;
}
.start-game-button:disabled {
background: linear-gradient(to right, #4a4a4a, #3d3d3d);
opacity: 0.7;
cursor: not-allowed;
}
/* JoinPrivateLobbyModal css */
.message-area {
margin-top: 10px;
padding: 10px;
border-radius: 4px;
font-size: 14px;
transition: opacity 0.3s ease;
opacity: 0;
height: 0;
overflow: hidden;
}
.message-area.show {
opacity: 1;
height: auto;
margin-bottom: 10px;
}
.message-area.error {
background-color: #ffebee;
color: #c62828;
}
.message-area.success {
background-color: #e8f5e9;
color: #2e7d32;
}
.lobby-id-box {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
margin: 40px 0px 0px 0px;
}
.lobby-id-box input {
flex-grow: 1;
max-width: 200px;
padding: 10px;
font-size: 16px;
border: 1px solid #ccc;
border-radius: 8px;
}
.lobby-id-paste-button {
display: flex;
align-items: center;
background: rgba(0, 0, 0, 0.2);
padding: 10px 16px;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.1);
cursor: pointer;
transition: all 0.2s ease-in-out;
}
.lobby-id-paste-button:hover {
background: rgba(0, 0, 0, 0.3);
border-color: rgba(255, 255, 255, 0.2);
transform: translateY(-1px);
}
.lobby-id-paste-button-icon {
display: flex;
align-items: center;
justify-content: center;
color: #fff;
}
.options-section {
background: rgba(0, 0, 0, 0.2);
padding: 12px 24px 24px 24px;
border-radius: 12px;
}
.option-title {
margin: 0 0 16px 0;
font-size: 20px;
color: #fff;
text-align: center;
}
.players-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: center;
padding: 0 16px;
}
.player-tag {
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.1);
padding: 4px 16px;
border-radius: 16px;
font-size: 14px;
color: #fff;
border: 1px solid rgba(255, 255, 255, 0.2);
}
@media screen and (max-width: 768px) {
.modal-content {
max-height: calc(90vh - 42px);
max-width: 100vw;
width: 100%;
}
`;
render() {
return html`
<div
class="modal-overlay"
style="display: ${this.isModalOpen ? "flex" : "none"}"
>
<div
style="position: absolute; left: 0px; top: 0px; width: 100%; height: 100%;"
class="${this.isModalOpen ? "" : "hidden"}"
@click=${this.close}
></div>
<div class="modal-content">
<span class="close" @click=${this.closeAndLeave}>&times;</span>
<div class="title">Join Private Lobby</div>
<div class="lobby-id-box">
<input
type="text"
id="lobbyIdInput"
placeholder="Enter Lobby ID"
@keyup=${this.handleChange}
/>
<button
@click=${this.pasteFromClipboard}
class="lobby-id-paste-button"
<o-modal title="Join Private Lobby">
<div class="lobby-id-box">
<input
type="text"
id="lobbyIdInput"
placeholder="Enter Lobby ID"
@keyup=${this.handleChange}
/>
<button
@click=${this.pasteFromClipboard}
class="lobby-id-paste-button"
>
<svg
class="lobby-id-paste-button-icon"
stroke="currentColor"
fill="currentColor"
stroke-width="0"
viewBox="0 0 32 32"
height="18px"
width="18px"
xmlns="http://www.w3.org/2000/svg"
>
<svg
class="lobby-id-paste-button-icon"
stroke="currentColor"
fill="currentColor"
stroke-width="0"
viewBox="0 0 32 32"
height="18px"
width="18px"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M 15 3 C 13.742188 3 12.847656 3.890625 12.40625 5 L 5 5 L 5 28 L 13 28 L 13 30 L 27 30 L 27 14 L 25 14 L 25 5 L 17.59375 5 C 17.152344 3.890625 16.257813 3 15 3 Z M 15 5 C 15.554688 5 16 5.445313 16 6 L 16 7 L 19 7 L 19 9 L 11 9 L 11 7 L 14 7 L 14 6 C 14 5.445313 14.445313 5 15 5 Z M 7 7 L 9 7 L 9 11 L 21 11 L 21 7 L 23 7 L 23 14 L 13 14 L 13 26 L 7 26 Z M 15 16 L 25 16 L 25 28 L 15 28 Z"
></path>
</svg>
</button>
</div>
<div class="message-area ${this.message ? "show" : ""}">
${this.message}
</div>
<div class="options-layout">
<!-- Lobby Selection -->
${this.hasJoined && this.players.length > 0
? html`<div class="options-section">
<div class="option-title">
${this.players.length}
${this.players.length === 1 ? "Player" : "Players"}
</div>
<path
d="M 15 3 C 13.742188 3 12.847656 3.890625 12.40625 5 L 5 5 L 5 28 L 13 28 L 13 30 L 27 30 L 27 14 L 25 14 L 25 5 L 17.59375 5 C 17.152344 3.890625 16.257813 3 15 3 Z M 15 5 C 15.554688 5 16 5.445313 16 6 L 16 7 L 19 7 L 19 9 L 11 9 L 11 7 L 14 7 L 14 6 C 14 5.445313 14.445313 5 15 5 Z M 7 7 L 9 7 L 9 11 L 21 11 L 21 7 L 23 7 L 23 14 L 13 14 L 13 26 L 7 26 Z M 15 16 L 25 16 L 25 28 L 15 28 Z"
></path>
</svg>
</button>
</div>
<div class="message-area ${this.message ? "show" : ""}">
${this.message}
</div>
<div class="options-layout">
${this.hasJoined && this.players.length > 0
? html` <div class="options-section">
<div class="option-title">
${this.players.length}
${this.players.length === 1 ? "Player" : "Players"}
</div>
<div class="players-list">
${this.players.map(
(player) =>
html`<span class="player-tag">${player}</span>`,
)}
</div>
</div>`
: ""}
</div>
${!this.hasJoined
? html`<button class="start-game-button" @click=${this.joinLobby}>
Join Lobby
</button>`
<div class="players-list">
${this.players.map(
(player) => html`<span class="player-tag">${player}</span>`,
)}
</div>
</div>`
: ""}
</div>
</div>
<div class="flex justify-center">
${!this.hasJoined
? html` <o-button
title="Join Lobby"
block
@click=${this.joinLobby}
></o-button>`
: ""}
</div>
</o-modal>
`;
}
public open(id: string = "") {
this.isModalOpen = true;
createRenderRoot() {
return this; // light DOM
}
public open(id: string = "") {
this.modalEl?.open();
if (id) {
this.setLobbyId(id);
this.joinLobby();
@@ -313,8 +95,8 @@ export class JoinPrivateLobbyModal extends LitElement {
}
public close() {
this.isModalOpen = false;
this.lobbyIdInput.value = null;
this.modalEl?.close();
if (this.playersInterval) {
clearInterval(this.playersInterval);
this.playersInterval = null;
+2
View File
@@ -1,6 +1,8 @@
import { ClientGameRunner, joinLobby } from "./ClientGameRunner";
import favicon from "../../resources/images/Favicon.svg";
import "./PublicLobby";
import "./components/baseComponents/Button";
import "./components/baseComponents/Modal";
import "./UsernameInput";
import "./styles.css";
import { UsernameInput } from "./UsernameInput";
+178 -409
View File
@@ -1,9 +1,11 @@
import { LitElement, html, css } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { LitElement, html } from "lit";
import { customElement, query, state } from "lit/decorators.js";
import { Difficulty, GameMapType, GameType } from "../core/game/Game";
import { generateID as generateID } from "../core/Util";
import { consolex } from "../core/Consolex";
import "./components/Difficulties";
import "./components/baseComponents/Modal";
import "./components/baseComponents/Button";
import { DifficultyDescription } from "./components/Difficulties";
import "./components/Maps";
import randomMap from "../../resources/images/RandomMap.png";
@@ -12,7 +14,10 @@ import { JoinLobbyEvent } from "./Main";
@customElement("single-player-modal")
export class SinglePlayerModal extends LitElement {
@state() private isModalOpen = false;
@query("o-modal") private modalEl!: HTMLElement & {
open: () => void;
close: () => void;
};
@state() private selectedMap: GameMapType = GameMapType.World;
@state() private selectedDifficulty: Difficulty = Difficulty.Medium;
@state() private disableNPCs: boolean = false;
@@ -23,433 +28,193 @@ export class SinglePlayerModal extends LitElement {
@state() private instantBuild: boolean = false;
@state() private useRandomMap: boolean = false;
static styles = css`
.modal-overlay {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
overflow-y: auto;
display: flex;
align-items: center;
justify-content: center;
}
.modal-content {
background-color: rgb(35 35 35 / 0.8);
-webkit-backdrop-filter: blur(12px);
backdrop-filter: blur(12px);
color: white;
padding: 20px;
border-radius: 8px;
width: 80%;
max-width: 1280px;
max-height: 80vh;
overflow-y: auto;
text-align: center;
box-shadow: 0 0 40px rgba(0, 0, 0, 0.5);
border: 1px solid rgba(255, 255, 255, 0.2);
backdrop-filter: blur(8px);
position: relative;
}
/* Add custom scrollbar styles */
.modal-content::-webkit-scrollbar {
width: 8px;
}
.modal-content::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.1);
border-radius: 4px;
}
.modal-content::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 4px;
}
.modal-content::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}
.title {
font-size: 28px;
color: #fff;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
padding: 0 0 0 20px;
}
.close {
position: sticky;
top: 0px;
right: 0px;
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
.close:hover,
.close:focus {
color: white;
text-decoration: none;
cursor: pointer;
}
.start-game-button {
width: 100%;
max-width: 300px;
padding: 15px 20px;
font-size: 16px;
cursor: pointer;
background-color: #007bff;
color: white;
border: none;
border-radius: 8px;
transition: background-color 0.3s;
display: inline-block;
margin: 0 0 20px 0;
}
.start-game-button:not(:disabled):hover {
background-color: #0056b3;
}
.start-game-button:disabled {
background: linear-gradient(to right, #4a4a4a, #3d3d3d);
opacity: 0.7;
cursor: not-allowed;
}
.options-layout {
display: grid;
grid-template-columns: 1fr;
gap: 24px;
margin: 24px 0;
}
.options-section {
background: rgba(0, 0, 0, 0.2);
padding: 12px 24px 24px 24px;
border-radius: 12px;
}
.option-title {
margin: 0 0 16px 0;
font-size: 20px;
color: #fff;
text-align: center;
}
.option-cards {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
gap: 16px;
}
.option-card {
width: 100%;
min-width: 100px;
max-width: 120px;
padding: 4px 4px 0 4px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
background: rgba(30, 30, 30, 0.95);
border: 2px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
cursor: pointer;
transition: all 0.2s ease-in-out;
}
.option-card:hover {
transform: translateY(-2px);
border-color: rgba(255, 255, 255, 0.3);
background: rgba(40, 40, 40, 0.95);
}
.option-card.selected {
border-color: #4a9eff;
background: rgba(74, 158, 255, 0.1);
}
.option-card-title {
font-size: 14px;
color: #aaa;
text-align: center;
margin: 0 0 4px 0;
}
.option-image {
width: 100%;
aspect-ratio: 4/2;
color: #aaa;
transition: transform 0.2s ease-in-out;
border-radius: 8px;
background-color: rgba(255, 255, 255, 0.1);
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
}
.option-card input[type="checkbox"] {
display: none;
}
label.option-card:hover {
transform: none;
}
.checkbox-icon {
width: 16px;
height: 16px;
border: 2px solid #aaa;
border-radius: 6px;
margin: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease-in-out;
}
.option-card.selected .checkbox-icon {
border-color: #4a9eff;
background: #4a9eff;
}
.option-card.selected .checkbox-icon::after {
content: "✓";
color: white;
}
#bots-count {
width: 80%;
}
.random-map {
border: 2px solid rgba(255, 255, 255, 0.1);
background: rgba(30, 30, 30, 0.95);
}
.random-map.selected {
border: 2px solid '@4a9eff'
background: 'rgba(74, 158, 255, 0.1)'
}
@media screen and (max-width: 768px) {
.modal-content {
max-height: calc(90vh - 42px);
max-width: 100vw;
width: 100%;
}
}
`;
render() {
return html`
<div
class="modal-overlay"
style="display: ${this.isModalOpen ? "flex" : "none"}"
>
<div
style="position: absolute; left: 0px; top: 0px; width: 100%; height: 100%;"
class="${this.isModalOpen ? "" : "hidden"}"
@click=${this.close}
></div>
<div class="modal-content">
<span class="close" @click=${this.close}>&times;</span>
<div class="title">Single Player</div>
<div class="options-layout">
<!-- Map Selection -->
<div class="options-section">
<div class="option-title">Map</div>
<div class="option-cards">
${Object.entries(GameMapType)
.filter(([key]) => isNaN(Number(key)))
.map(
([key, value]) => html`
<div
@click=${function () {
this.handleMapSelection(value);
}}
>
<map-display
.mapKey=${key}
.selected=${!this.useRandomMap &&
this.selectedMap === value}
></map-display>
</div>
`,
)}
<div
class="option-card random-map ${this.useRandomMap
? "selected"
: ""}"
@click=${this.handleRandomMapToggle}
>
<div class="option-image">
<img
src=${randomMap}
alt="Random Map"
style="width:100%; aspect-ratio: 4/2; object-fit:cover; border-radius:8px;"
/>
</div>
<div class="option-card-title">Random</div>
<o-modal title="Single Player">
<div class="options-layout">
<!-- Map Selection -->
<div class="options-section">
<div class="option-title">Map</div>
<div class="option-cards">
${Object.entries(GameMapType)
.filter(([key]) => isNaN(Number(key)))
.map(
([key, value]) => html`
<div
@click=${function () {
this.handleMapSelection(value);
}}
>
<map-display
.mapKey=${key}
.selected=${!this.useRandomMap &&
this.selectedMap === value}
></map-display>
</div>
`,
)}
<div
class="option-card random-map ${this.useRandomMap
? "selected"
: ""}"
@click=${this.handleRandomMapToggle}
>
<div class="option-image">
<img
src=${randomMap}
alt="Random Map"
style="width:100%; aspect-ratio: 4/2; object-fit:cover; border-radius:8px;"
/>
</div>
</div>
</div>
<!-- Difficulty Selection -->
<div class="options-section">
<div class="option-title">Difficulty</div>
<div class="option-cards">
${Object.entries(Difficulty)
.filter(([key]) => isNaN(Number(key)))
.map(
([key, value]) => html`
<div
class="option-card ${this.selectedDifficulty === value
? "selected"
: ""}"
@click=${() => this.handleDifficultySelection(value)}
>
<difficulty-display
.difficultyKey=${key}
></difficulty-display>
<p class="option-card-title">
${DifficultyDescription[key]}
</p>
</div>
`,
)}
</div>
</div>
<!-- Game Options -->
<div class="options-section">
<div class="option-title">Options</div>
<div class="option-cards">
<label for="bots-count" class="option-card">
<input
type="range"
id="bots-count"
min="0"
max="400"
step="1"
@input=${this.handleBotsChange}
@change=${this.handleBotsChange}
.value="${this.bots}"
/>
<div class="option-card-title">
Bots: ${this.bots == 0 ? "Disabled" : this.bots}
</div>
</label>
<label
for="disable-npcs"
class="option-card ${this.disableNPCs ? "selected" : ""}"
>
<div class="checkbox-icon"></div>
<input
type="checkbox"
id="disable-npcs"
@change=${this.handleDisableNPCsChange}
.checked=${this.disableNPCs}
/>
<div class="option-card-title">Disable Nations</div>
</label>
<label
for="instant-build"
class="option-card ${this.instantBuild ? "selected" : ""}"
>
<div class="checkbox-icon"></div>
<input
type="checkbox"
id="instant-build"
@change=${this.handleInstantBuildChange}
.checked=${this.instantBuild}
/>
<div class="option-card-title">Instant build</div>
</label>
<label
for="infinite-gold"
class="option-card ${this.infiniteGold ? "selected" : ""}"
>
<div class="checkbox-icon"></div>
<input
type="checkbox"
id="infinite-gold"
@change=${this.handleInfiniteGoldChange}
.checked=${this.infiniteGold}
/>
<div class="option-card-title">Infinite gold</div>
</label>
<label
for="infinite-troops"
class="option-card ${this.infiniteTroops ? "selected" : ""}"
>
<div class="checkbox-icon"></div>
<input
type="checkbox"
id="infinite-troops"
@change=${this.handleInfiniteTroopsChange}
.checked=${this.infiniteTroops}
/>
<div class="option-card-title">Infinite troops</div>
</label>
<label
for="disable-nukes"
class="option-card ${this.disableNukes ? "selected" : ""}"
>
<div class="checkbox-icon"></div>
<input
type="checkbox"
id="disable-nukes"
@change=${this.handleDisableNukesChange}
.checked=${this.disableNukes}
/>
<div class="option-card-title">Disable Nukes</div>
</label>
<div class="option-card-title">Random</div>
</div>
</div>
</div>
<button @click=${this.startGame} class="start-game-button">
Start Game
</button>
<!-- Difficulty Selection -->
<div class="options-section">
<div class="option-title">Difficulty</div>
<div class="option-cards">
${Object.entries(Difficulty)
.filter(([key]) => isNaN(Number(key)))
.map(
([key, value]) => html`
<div
class="option-card ${this.selectedDifficulty === value
? "selected"
: ""}"
@click=${() => this.handleDifficultySelection(value)}
>
<difficulty-display
.difficultyKey=${key}
></difficulty-display>
<p class="option-card-title">
${DifficultyDescription[key]}
</p>
</div>
`,
)}
</div>
</div>
<!-- Game Options -->
<div class="options-section">
<div class="option-title">Options</div>
<div class="option-cards">
<label for="bots-count" class="option-card">
<input
type="range"
id="bots-count"
min="0"
max="400"
step="1"
@input=${this.handleBotsChange}
@change=${this.handleBotsChange}
.value="${this.bots}"
/>
<div class="option-card-title">
Bots: ${this.bots == 0 ? "Disabled" : this.bots}
</div>
</label>
<label
for="disable-npcs"
class="option-card ${this.disableNPCs ? "selected" : ""}"
>
<div class="checkbox-icon"></div>
<input
type="checkbox"
id="disable-npcs"
@change=${this.handleDisableNPCsChange}
.checked=${this.disableNPCs}
/>
<div class="option-card-title">Disable Nations</div>
</label>
<label
for="instant-build"
class="option-card ${this.instantBuild ? "selected" : ""}"
>
<div class="checkbox-icon"></div>
<input
type="checkbox"
id="instant-build"
@change=${this.handleInstantBuildChange}
.checked=${this.instantBuild}
/>
<div class="option-card-title">Instant build</div>
</label>
<label
for="infinite-gold"
class="option-card ${this.infiniteGold ? "selected" : ""}"
>
<div class="checkbox-icon"></div>
<input
type="checkbox"
id="infinite-gold"
@change=${this.handleInfiniteGoldChange}
.checked=${this.infiniteGold}
/>
<div class="option-card-title">Infinite gold</div>
</label>
<label
for="infinite-troops"
class="option-card ${this.infiniteTroops ? "selected" : ""}"
>
<div class="checkbox-icon"></div>
<input
type="checkbox"
id="infinite-troops"
@change=${this.handleInfiniteTroopsChange}
.checked=${this.infiniteTroops}
/>
<div class="option-card-title">Infinite troops</div>
</label>
<label
for="disable-nukes"
class="option-card ${this.disableNukes ? "selected" : ""}"
>
<div class="checkbox-icon"></div>
<input
type="checkbox"
id="disable-nukes"
@change=${this.handleDisableNukesChange}
.checked=${this.disableNukes}
/>
<div class="option-card-title">Disable Nukes</div>
</label>
</div>
</div>
</div>
</div>
<o-button
title="Start Game"
@click=${this.startGame}
blockDesktop
></o-button>
</o-modal>
`;
}
createRenderRoot() {
return this; // light DOM
}
public open() {
this.isModalOpen = true;
this.modalEl?.open();
this.useRandomMap = false;
}
public close() {
this.isModalOpen = false;
this.modalEl?.close();
}
private handleRandomMapToggle() {
this.useRandomMap = true;
}
private handleMapSelection(value: GameMapType) {
this.selectedMap = value;
this.useRandomMap = false;
@@ -458,6 +223,7 @@ export class SinglePlayerModal extends LitElement {
private handleDifficultySelection(value: Difficulty) {
this.selectedDifficulty = value;
}
private handleBotsChange(e: Event) {
const value = parseInt((e.target as HTMLInputElement).value);
if (isNaN(value) || value < 0 || value > 400) {
@@ -465,9 +231,11 @@ export class SinglePlayerModal extends LitElement {
}
this.bots = value;
}
private handleInstantBuildChange(e: Event) {
this.instantBuild = Boolean((e.target as HTMLInputElement).checked);
}
private handleInfiniteGoldChange(e: Event) {
this.infiniteGold = Boolean((e.target as HTMLInputElement).checked);
}
@@ -479,6 +247,7 @@ export class SinglePlayerModal extends LitElement {
private handleDisableNPCsChange(e: Event) {
this.disableNPCs = Boolean((e.target as HTMLInputElement).checked);
}
private handleDisableNukesChange(e: Event) {
this.disableNukes = Boolean((e.target as HTMLInputElement).checked);
}
@@ -0,0 +1,35 @@
import { LitElement, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { classMap } from "lit/directives/class-map.js";
@customElement("o-button")
export class OButton extends LitElement {
@property({ type: String }) title = "";
@property({ type: String }) translationKey = "";
@property({ type: Boolean }) secondary = false;
@property({ type: Boolean }) block = false;
@property({ type: Boolean }) blockDesktop = false;
@property({ type: Boolean }) disable = false;
createRenderRoot() {
return this;
}
render() {
return html`
<button
data-i18n="${this.translationKey}"
class=${classMap({
"c-button": true,
"c-button--block": this.block,
"c-button--blockDesktop": this.blockDesktop,
"c-button--secondary": this.secondary,
"c-button--disabled": this.disable,
})}
?disabled=${this.disable}
>
${this.title}
</button>
`;
}
}
@@ -0,0 +1,93 @@
import { LitElement, html, css } from "lit";
import { customElement, property, state } from "lit/decorators.js";
@customElement("o-modal")
export class OModal extends LitElement {
@state() public isModalOpen = false;
@property({ type: String }) title = "";
@property({ type: String }) translationKey = "";
static styles = css`
.c-modal {
position: fixed;
padding: 1rem;
z-index: 1000;
left: 0;
bottom: 0;
right: 0;
top: 0;
background-color: rgba(0, 0, 0, 0.5);
overflow-y: auto;
display: flex;
align-items: center;
justify-content: center;
}
.c-modal__wrapper {
background: #23232382;
border-radius: 8px;
min-width: 340px;
max-width: 860px;
}
.c-modal__header {
position: relative;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
font-size: 18px;
background: #000000a1;
text-align: center;
color: #fff;
padding: 1rem 2.4rem 1rem 1.4rem;
}
.c-modal__close {
cursor: pointer;
position: absolute;
right: 1rem;
top: 1rem;
}
.c-modal__content {
position: relative;
color: #fff;
padding: 1.4rem;
max-height: 60dvh;
overflow-y: scroll;
backdrop-filter: blur(8px);
}
`;
public open() {
this.isModalOpen = true;
}
public close() {
this.isModalOpen = false;
this.dispatchEvent(
new CustomEvent("modal-close", { bubbles: true, composed: true }),
);
}
render() {
return html`
${this.isModalOpen
? html`
<aside class="c-modal">
<div class="c-modal__wrapper">
<header
class="c-modal__header"
data-i18n="${this.translationKey}"
>
${this.title}
<div class="c-modal__close" @click=${this.close}>X</div>
</header>
<section class="c-modal__content">
<slot></slot>
</section>
</div>
</aside>
`
: html``}
`;
}
}
+46 -49
View File
@@ -48,10 +48,25 @@
/* Critical styles to prevent layout shift */
.container {
display: flex;
padding: 1rem;
max-width: 540px !important;
flex-direction: column;
gap: 1rem;
opacity: 1;
transition: opacity 0.3s ease-in-out;
}
.container__row {
display: flex;
gap: 1rem;
align-items: center;
}
.container__row--equal > * {
flex: 1 1 100%;
}
.logo-glow {
fill: url(#logo-gradient);
filter: drop-shadow(1px 1px 0px rgb(255, 255, 255))
@@ -230,15 +245,12 @@
<!-- Main container with responsive padding -->
<div class="flex justify-center items-center flex-grow">
<div class="container px-4 sm:px-6 lg:px-8 py-4 sm:py-6 lg:py-8">
<div
class="flex gap-1 items-center max-w-sm sm:max-w-md lg:max-w-lg xl:max-w-xl mx-auto p-2 pb-4"
>
<div class="container">
<div class="container__row">
<flag-input class="w-[20%] md:w-[15%]"></flag-input>
<username-input class="w-full"></username-input>
</div>
<div class="max-w-sm sm:max-w-md lg:max-w-lg xl:max-w-xl mx-auto mt-4">
<div>
<a
target="_blank"
href="https://discord.gg/k22YrnAzGp"
@@ -252,56 +264,41 @@
<span data-i18n="main.join_discord"> Join the Discord! </span>
</a>
</div>
<div class="max-w-sm sm:max-w-md lg:max-w-lg xl:max-w-xl mx-auto mt-4">
<div>
<public-lobby class="w-full"></public-lobby>
</div>
<div
class="pt-4 flex gap-4 sm:gap-6 lg:gap-8 max-w-sm sm:max-w-md lg:max-w-lg xl:max-w-xl mx-auto"
>
<button
<div class="container__row container__row--equal">
<o-button
id="host-lobby-button"
data-i18n="main.create_lobby"
class="bg-blue-100 hover:bg-blue-200 text-blue-900 p-3 sm:p-4 lg:p-5 font-medium text-sm sm:text-base lg:text-lg rounded-md w-full border-none cursor-pointer transition-colors duration-300"
>
Create Lobby
</button>
<button
title="Create Lobby"
translationKey="main.create_lobby"
block
secondary
></o-button>
<o-button
id="join-private-lobby-button"
data-i18n="main.join_lobby"
class="bg-blue-100 hover:bg-blue-200 text-blue-900 p-3 sm:p-4 lg:p-5 font-medium text-sm sm:text-base lg:text-lg rounded-md w-full border-none cursor-pointer transition-colors duration-300"
>
Join Lobby
</button>
title="Join Lobby"
translationKey="main.join_lobby"
block
secondary
></o-button>
</div>
<div
class="max-w-sm sm:max-w-md lg:max-w-lg xl:max-w-xl mx-auto mt-4 sm:mt-6 lg:mt-8"
>
<button
id="single-player"
data-i18n="main.single_player"
class="w-full bg-blue-600 hover:bg-blue-700 text-white p-3 sm:p-4 lg:p-5 font-bold text-lg sm:text-xl lg:text-2xl rounded-lg border-none cursor-pointer transition-colors duration-300"
>
Single Player
</button>
</div>
<o-button
id="single-player"
title="Single Player"
translationKey="main.single_player"
block
></o-button>
<div
class="pt-4 flex gap-4 sm:gap-6 lg:gap-8 max-w-sm sm:max-w-md lg:max-w-lg xl:max-w-xl mx-auto"
>
<button
id="help-button"
data-i18n="main.instructions"
class="w-25 bg-blue-100 hover:bg-blue-200 text-blue-900 p-3 sm:p-4 lg:p-5 font-medium text-sm sm:text-base lg:text-lg rounded-md w-full border-none cursor-pointer transition-colors duration-300"
>
Instructions
</button>
</div>
<div
class="pt-4 flex gap-4 sm:gap-6 lg:gap-8 max-w-sm sm:max-w-md lg:max-w-lg xl:max-w-xl mx-auto"
>
<o-button
id="help-button"
title="Instructions"
translationKey="main.instructions"
block
secondary
></o-button>
<div class="container__row">
<select
id="lang-selector"
class="text-center appearance-none w-full bg-blue-100 hover:bg-blue-200 text-blue-900 p-3 sm:p-4 lg:p-5 font-medium text-sm sm:text-base lg:text-lg rounded-md border-none cursor-pointer transition-colors duration-300"
+562
View File
@@ -26,3 +26,565 @@
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}
.start-game-button {
width: 100%;
max-width: 300px;
padding: 15px 20px;
font-size: 16px;
cursor: pointer;
background-color: #007bff;
color: white;
border: none;
border-radius: 8px;
transition: background-color 0.3s;
display: inline-block;
margin: 0 0 20px 0;
}
.start-game-button:not(:disabled):hover {
background-color: #0056b3;
}
.start-game-button:disabled {
background: linear-gradient(to right, #4a4a4a, #3d3d3d);
opacity: 0.7;
cursor: not-allowed;
}
.options-layout {
display: grid;
grid-template-columns: 1fr;
gap: 24px;
margin: 24px 0;
}
.options-section {
background: rgba(0, 0, 0, 0.2);
padding: 12px 24px 24px 24px;
border-radius: 12px;
}
.option-title {
margin: 0 0 16px 0;
font-size: 20px;
color: #fff;
text-align: center;
}
.option-cards {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
gap: 16px;
}
.option-card {
width: 100%;
min-width: 100px;
max-width: 120px;
padding: 4px 4px 0 4px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
background: rgba(30, 30, 30, 0.95);
border: 2px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
cursor: pointer;
transition: all 0.2s ease-in-out;
}
.option-card:hover {
transform: translateY(-2px);
border-color: rgba(255, 255, 255, 0.3);
background: rgba(40, 40, 40, 0.95);
}
.option-card.selected {
border-color: #4a9eff;
background: rgba(74, 158, 255, 0.1);
}
.option-card-title {
font-size: 14px;
color: #aaa;
text-align: center;
margin: 0 0 4px 0;
}
.option-image {
width: 100%;
aspect-ratio: 4/2;
color: #aaa;
transition: transform 0.2s ease-in-out;
border-radius: 8px;
background-color: rgba(255, 255, 255, 0.1);
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
}
.option-card input[type="checkbox"] {
display: none;
}
label.option-card:hover {
transform: none;
}
.checkbox-icon {
width: 16px;
height: 16px;
border: 2px solid #aaa;
border-radius: 6px;
margin: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease-in-out;
}
.option-card.selected .checkbox-icon {
border-color: #4a9eff;
background: #4a9eff;
}
.option-card.selected .checkbox-icon::after {
content: "✓";
color: white;
}
/* HostLobbyModal css */
.clipboard-icon {
display: flex;
align-items: center;
justify-content: center;
color: #fff;
}
.copy-success {
position: relative;
color: green;
font-size: 14px;
margin-top: 5px;
}
.copy-success-icon {
width: 18px;
height: 18px;
color: #4caf50;
}
.lobby-id-box {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
margin: 8px 0px 0px 0px;
}
.lobby-id-button {
display: flex;
align-items: center;
gap: 8px;
background: rgba(0, 0, 0, 0.2);
padding: 8px 16px;
border-radius: 6px;
border: 1px solid rgba(255, 255, 255, 0.1);
cursor: pointer;
transition: all 0.2s ease-in-out;
}
.lobby-id-button:hover {
background: rgba(0, 0, 0, 0.3);
border-color: rgba(255, 255, 255, 0.2);
transform: translateY(-1px);
}
.lobby-id {
font-size: 14px;
color: #fff;
text-align: center;
}
.players-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: center;
padding: 0 16px;
}
.player-tag {
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.1);
padding: 4px 16px;
border-radius: 16px;
font-size: 14px;
color: #fff;
border: 1px solid rgba(255, 255, 255, 0.2);
}
#bots-count,
#private-lobby-bots-count {
width: 80%;
height: 16px;
}
#bots-count::-webkit-slider-runnable-track,
#private-lobby-bots-count::-webkit-slider-runnable-track {
background: #0075ff;
height: 8px;
}
#bots-count::-webkit-slider-thumb,
#private-lobby-bots-count::-webkit-slider-thumb {
background: #0075ff;
border-color: #0075ff;
position: relative;
top: -3px;
}
.random-map {
border: 2px solid rgba(255, 255, 255, 0.1);
background: rgba(30, 30, 30, 0.95);
}
.random-map.selected {
border: 2px solid #4a9eff;
background: rgba(74, 158, 255, 0.1);
}
.radial-menu-image {
width: 211px;
height: 200px;
}
#helpModal table {
border-collapse: collapse;
}
#helpModal table,
#helpModal table th,
#helpModal table td {
border: 1px solid rgb(255 255 255 / 0.2);
}
#helpModal table th,
#helpModal table td {
padding: 8px 16px;
}
#helpModal table td:first-of-type {
text-align: center;
}
#helpModal .icon {
background-color: white;
width: 32px;
height: 32px;
}
#helpModal .city-icon {
mask: url("../../resources/images/CityIconWhite.svg") no-repeat center / cover;
}
#helpModal .defense-post-icon {
mask: url("../../resources/images/ShieldIconWhite.svg") no-repeat center /
cover;
}
#helpModal .port-icon {
mask: url("../../resources/images/PortIcon.svg") no-repeat center / cover;
}
#helpModal .warship-icon {
mask: url("../../resources/images/BattleshipIconWhite.svg") no-repeat center /
cover;
}
#helpModal .missile-silo-icon {
mask: url("../../resources/images/MissileSiloIconWhite.svg") no-repeat
center / cover;
}
#helpModal .atom-bomb-icon {
mask: url("../../resources/images/NukeIconWhite.svg") no-repeat center / cover;
}
#helpModal .hydrogen-bomb-icon {
mask: url("../../resources/images/MushroomCloudIconWhite.svg") no-repeat
center / cover;
}
#helpModal .mirv-icon {
mask: url("../../resources/images/MIRVIcon.svg") no-repeat center / cover;
}
#helpModal .target-icon {
mask: url("../../resources/images/TargetIcon.svg") no-repeat center / cover;
}
#helpModal .alliance-icon {
mask: url("../../resources/images/AllianceIconWhite.svg") no-repeat center /
cover;
}
#helpModal .emoji-icon {
mask: url("../../resources/images/EmojiIconWhite.svg") no-repeat center /
cover;
}
#helpModal .betray-icon {
mask: url("../../resources/images/TraitorIconWhite.svg") no-repeat center /
cover;
}
#helpModal .donate-icon {
mask: url("../../resources/images/DonateIconWhite.svg") no-repeat center /
cover;
}
#helpModal .build-icon {
mask: url("../../resources/images/BuildIconWhite.svg") no-repeat center /
cover;
}
#helpModal .info-icon {
mask: url("../../resources/images/InfoIcon.svg") no-repeat center / cover;
}
#helpModal .boat-icon {
mask: url("../../resources/images/BoatIcon.svg") no-repeat center / cover;
}
#helpModal .cancel-icon {
mask: url("../../resources/images/XIcon.svg") no-repeat center / cover;
}
@media screen and (max-width: 768px) {
#helpModal .modal-content {
max-height: 90vh;
max-width: 100vw;
width: 100%;
}
}
.message-area {
margin-top: 10px;
padding: 10px;
border-radius: 4px;
font-size: 14px;
transition: opacity 0.3s ease;
opacity: 0;
height: 0;
overflow: hidden;
}
.message-area.show {
opacity: 1;
height: auto;
margin-bottom: 10px;
}
.message-area.error {
background-color: #ffebee;
color: #c62828;
}
.message-area.success {
background-color: #e8f5e9;
color: #2e7d32;
}
.lobby-id-box {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
margin: 40px 0px 0px 0px;
}
.lobby-id-box input {
flex-grow: 1;
max-width: 200px;
padding: 10px;
font-size: 16px;
border: 1px solid #ccc;
border-radius: 8px;
}
.lobby-id-paste-button {
display: flex;
align-items: center;
background: rgba(0, 0, 0, 0.2);
padding: 10px 16px;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.1);
cursor: pointer;
transition: all 0.2s ease-in-out;
}
.lobby-id-paste-button:hover {
background: rgba(0, 0, 0, 0.3);
border-color: rgba(255, 255, 255, 0.2);
transform: translateY(-1px);
}
.lobby-id-paste-button-icon {
display: flex;
align-items: center;
justify-content: center;
color: #fff;
}
.options-section {
background: rgba(0, 0, 0, 0.2);
padding: 12px 24px 24px 24px;
border-radius: 12px;
}
.option-title {
margin: 0 0 16px 0;
font-size: 20px;
color: #fff;
text-align: center;
}
.players-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: center;
padding: 0 16px;
}
.player-tag {
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.1);
padding: 4px 16px;
border-radius: 16px;
font-size: 14px;
color: #fff;
border: 1px solid rgba(255, 255, 255, 0.2);
}
/*Base styles*/
.c-button {
background: #2563eb;
color: #fff;
cursor: pointer;
outline: none;
display: inline-block;
font-size: 16px;
border: 1px solid transparent;
text-align: center;
padding: 0.8rem 1rem;
border-radius: 8px;
@media (min-width: 1024px) {
font-size: 18px;
}
}
.c-button:hover,
.c-button:active,
.c-button:focus {
background: #1d4ed8;
}
.c-button:disabled {
background: linear-gradient(to right, rgb(74, 74, 74), rgb(61, 61, 61));
opacity: 0.7;
cursor: not-allowed;
}
.c-button--secondary {
background: #dbeafe;
color: #202020;
}
.c-button--secondary:hover,
.c-button--secondary:active,
.c-button--secondary:focus {
background: #bfdbfe;
}
.c-button--block {
display: block;
width: 100%;
}
.c-button--blockDesktop {
display: block;
width: 100%;
@media (min-width: 1024px) {
width: auto;
margin: 0 auto;
}
}
.c-modal {
position: fixed;
padding: 1rem;
z-index: 1000;
left: 0;
bottom: 0;
right: 0;
top: 0;
background-color: rgba(0, 0, 0, 0.5);
overflow-y: auto;
display: flex;
align-items: center;
justify-content: center;
}
.c-modal__wrapper {
background: #23232382;
border-radius: 8px;
min-width: 340px;
max-width: 860px;
}
.c-modal__header {
position: relative;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
font-size: 18px;
background: #000000a1;
text-align: center;
color: #fff;
padding: 1rem 2.4rem 1rem 1.4rem;
}
.c-modal__close {
cursor: pointer;
position: absolute;
right: 1rem;
top: 1rem;
}
.c-modal__content {
position: relative;
color: #fff;
padding: 1.4rem;
max-height: 60dvh;
overflow-y: scroll;
backdrop-filter: blur(8px);
}
/*This will be removed in future*/
o-modal o-button {
@media (min-width: 1024px) {
margin: 0 auto;
display: block;
text-align: center;
}
}
+1
View File
@@ -3,6 +3,7 @@
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "node",
"experimentalDecorators": true,
"types": ["jest", "node"]
},
"include": ["tests/**/*"]
+79 -19
View File
@@ -1,5 +1,6 @@
import path from "path";
import { fileURLToPath } from "url";
import fs from "fs/promises";
import HtmlWebpackPlugin from "html-webpack-plugin";
import webpack from "webpack";
import CopyPlugin from "copy-webpack-plugin";
@@ -7,7 +8,41 @@ import CopyPlugin from "copy-webpack-plugin";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export default (env, argv) => {
async function checkResourcesCopied(sourceDir, targetDir) {
async function checkDir(source, target) {
let items;
try {
items = await fs.readdir(source, { withFileTypes: true });
} catch (error) {
console.error(`Error reading directory ${source}:`, error);
return false;
}
for (const item of items) {
const sourcePath = path.join(source, item.name);
const targetPath = path.join(target, item.name);
if (item.isDirectory()) {
try {
await fs.access(targetPath);
} catch (error) {
// Target directory does not exist.
return false;
}
const exists = await checkDir(sourcePath, targetPath);
if (!exists) return false;
} else if (item.isFile()) {
try {
await fs.access(targetPath);
} catch (error) {
// Target file does not exist.
return false;
}
}
}
return true;
}
return checkDir(sourceDir, targetDir);
}
export default async (env, argv) => {
const isProduction = argv.mode === "production";
return {
@@ -16,7 +51,7 @@ export default (env, argv) => {
publicPath: "/",
filename: "js/[name].[contenthash].js", // Added content hash
path: path.resolve(__dirname, "static"),
clean: true,
clean: isProduction,
},
module: {
rules: [
@@ -119,23 +154,48 @@ export default (env, argv) => {
new webpack.DefinePlugin({
"process.env.GAME_ENV": JSON.stringify(isProduction ? "prod" : "dev"),
}),
new CopyPlugin({
patterns: [
{
from: "resources",
to: ".", // Copy to the output directory (static)
// Add content hashing to copied files
transform: function (content, path) {
return content; // Return unmodified content
},
// Don't hash HTML files from resources
noErrorOnMissing: true,
},
],
options: {
concurrency: 100,
},
}),
...(await (async () => {
if (isProduction) {
return [
new CopyPlugin({
patterns: [
{
from: "resources",
to: ".",
noErrorOnMissing: true,
},
],
options: { concurrency: 100 },
}),
];
} else {
const resourcesDir = path.resolve(__dirname, "resources");
const targetDir = path.resolve(__dirname, "static");
const allExist = await checkResourcesCopied(resourcesDir, targetDir);
if (allExist) {
console.log(
"[CopyPlugin] Skipped: All resources already exist in static/.",
);
return []; // Skip CopyPlugin if all resources are present.
} else {
console.log(
"[CopyPlugin] Copying missing resources to static/ ...",
);
return [
new CopyPlugin({
patterns: [
{
from: "resources",
to: ".",
noErrorOnMissing: true,
},
],
options: { concurrency: 100 },
}),
];
}
}
})()),
],
optimization: {
// Add optimization configuration for better caching