diff --git a/src/client/HelpModal.ts b/src/client/HelpModal.ts
index 0dec60f7f..e49513845 100644
--- a/src/client/HelpModal.ts
+++ b/src/client/HelpModal.ts
@@ -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`
-
-
-
-
-
×
+
+
+
Hotkeys
+
+
+
+ Key
+ Action
+
+
+
+
+ Space
+ Alternate view (terrain/countries)
+
+
+ Shift + left click
+ Attack (when left click is set to open menu)
+
+
+ Ctrl + left click
+ Open build menu
+
+
+ C
+ Center camera on player
+
+
+ Q / E
+ Zoom out/in
+
+
+ W / A / S / D
+ Move camera
+
+
+ 1 / 2
+ Decrease/Increase attack ratio
+
+
+ Shift + scroll down / scroll up
+ Decrease/Increase attack ratio
+
+
+ ALT + R
+ Reset graphics
+
+
+
+
+
+
+ Game UI
+
-
Hotkeys
-
-
-
- Key
- Action
-
-
-
-
- Space
- Alternate view (terrain/countries)
-
-
- Shift + left click
- Attack (when left click is set to open menu)
-
-
- Ctrl + left click
- Open build menu
-
-
- C
- Center camera on player
-
-
- Q / E
- Zoom out/in
-
-
- W / A / S / D
- Move camera
-
-
- 1 / 2
- Decrease/Increase attack ratio
-
-
- Shift + scroll down / scroll up
- Decrease/Increase attack ratio
-
-
- ALT + R
- Reset graphics
-
-
-
+
Leaderboard
+
-
-
-
-
Game UI
-
-
-
Leaderboard
-
-
-
-
Shows the top players of the game and their names, % owned land and gold.
-
-
-
-
-
-
-
-
Control panel
-
-
-
-
The control panel contains the following elements:
-
- Pop - The amount of units you have, your max population and the rate at which you gain them.
- Gold - The amount of gold you have and the rate at which you gain it.
- 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.
- Attack ratio - The amount of troops that will be used when you attack. You can adjust the attack ratio using the slider.
-
-
-
-
-
-
-
-
-
Options
-
-
-
-
The following elements can be found inside:
-
- Pause/Unpause the game - Only available in single player mode.
- Timer - Time passed since the start of the game.
- Exit button.
- Settings - Open the settings menu. Inside you can toggle the Alternate View, Dark Mode, Emojis and action on left click.
-
-
-
-
-
-
-
Radial menu
-
-
-
-
-
Right clicking (or touch on mobile) opens the radial menu. From there you can:
-
-
- Open the build menu.
-
- - Open the Info menu.
-
- Send a boat to attack at the selected location (only available if you have access to water).
-
- Close the menu.
-
-
-
-
-
-
-
Info menu
-
-
-
Enemy info panel
-
-
-
-
- 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:
-
-
-
- Place a target mark on the player, marking it for all allies, used to coordinate attacks.
-
- Send an alliance request to the player. Allies can share resources and troops, but can't attack each other.
-
- Send an emoji to the player.
-
-
-
-
-
-
-
-
-
Ally info panel
-
-
-
-
- When you ally with a player, the following new icons become available:
-
-
-
- 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.
-
- 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.
-
-
-
-
-
-
-
-
-
Build menu
-
-
-
- Name
- Icon
- Description
-
-
-
-
- City
-
-
- Increases your max population. Useful when you can't
- expand your territory or you're about to hit your
- population limit.
-
-
-
- Defense Post
-
-
- Increases defenses around nearby borders. Attacks from
- enemies are slower and have more casualties.
-
-
-
- Port
-
-
- 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.
-
-
-
- Warship
-
-
- 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.
-
-
-
- Missile Silo
-
- Allows launching missiles.
-
-
- SAM Launcher
-
- 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.
-
-
- Atom Bomb
-
- 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.
-
-
- Hydrogen Bomb
-
- Large explosive bomb. Spawns from the nearest Missile Silo and lands in the area you first clicked to build it.
-
-
- MIRV
-
- 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.
-
-
-
-
-
-
-
-
-
Player icons
-
Examples of some of the ingame icons you will encounter and what they mean:
-
-
-
Crown - This is the number 1 player in the leaderboard
-
-
-
-
-
Crossed swords - Traitor. This player attacked an ally.
-
-
-
-
-
Handshake - Ally. This player is your ally.
-
-
+
Shows the top players of the game and their names, % owned land and gold.
-
-
+
+
+
+
+
+
Control panel
+
+
+
+
The control panel contains the following elements:
+
+ Pop - The amount of units you have, your max population and the rate at which you gain them.
+ Gold - The amount of gold you have and the rate at which you gain it.
+ 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.
+ Attack ratio - The amount of troops that will be used when you attack. You can adjust the attack ratio using the slider.
+
+
+
+
+
+
+
+
+
Options
+
+
+
+
The following elements can be found inside:
+
+ Pause/Unpause the game - Only available in single player mode.
+ Timer - Time passed since the start of the game.
+ Exit button.
+ Settings - Open the settings menu. Inside you can toggle the Alternate View, Dark Mode, Emojis and action on left click.
+
+
+
+
+
+
+
Radial menu
+
+
+
+
+
Right clicking (or touch on mobile) opens the radial menu. From there you can:
+
+
- Open the build menu.
+
+ - Open the Info menu.
+
- Send a boat to attack at the selected location (only available if you have access to water).
+
- Close the menu.
+
+
+
+
+
+
+
+
Info menu
+
+
+
Enemy info panel
+
+
+
+
+ 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:
+
+
+
- Place a target mark on the player, marking it for all allies, used to coordinate attacks.
+
- Send an alliance request to the player. Allies can share resources and troops, but can't attack each other.
+
- Send an emoji to the player.
+
+
+
+
+
+
+
+
+
Ally info panel
+
+
+
+
+ When you ally with a player, the following new icons become available:
+
+
+
- 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.
+
- 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.
+
+
+
+
+
+
+
+
+
Build menu
+
+
+
+ Name
+ Icon
+ Description
+
+
+
+
+ City
+
+
+ Increases your max population. Useful when you can't
+ expand your territory or you're about to hit your
+ population limit.
+
+
+
+ Defense Post
+
+
+ Increases defenses around nearby borders. Attacks from
+ enemies are slower and have more casualties.
+
+
+
+ Port
+
+
+ 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.
+
+
+
+ Warship
+
+
+ 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.
+
+
+
+ Missile Silo
+
+ Allows launching missiles.
+
+
+ SAM Launcher
+
+ 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.
+
+
+ Atom Bomb
+
+ 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.
+
+
+ Hydrogen Bomb
+
+ Large explosive bomb. Spawns from the nearest Missile Silo and lands in the area you first clicked to build it.
+
+
+ MIRV
+
+ 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.
+
+
+
+
+
+
+
+
+
Player icons
+
Examples of some of the ingame icons you will encounter and what they mean:
+
+
+
Crown - This is the number 1 player in the leaderboard
+
+
+
+
+
Crossed swords - Traitor. This player attacked an ally.
+
+
+
+
+
Handshake - Ally. This player is your ally.
+
+
+
+
+
`;
}
public open() {
- this.isModalOpen = true;
+ this.modalEl?.open();
}
public close() {
- this.isModalOpen = false;
- console.log("closing modal");
+ this.modalEl?.close();
}
}
diff --git a/src/client/HostLobbyModal.ts b/src/client/HostLobbyModal.ts
index 10fdc614a..b59b59608 100644
--- a/src/client/HostLobbyModal.ts
+++ b/src/client/HostLobbyModal.ts
@@ -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`
-
-
-
-
×
-
-
Private Lobby
-
-
- ${this.lobbyId}
- ${this.copySuccess
- ? html`✓ `
- : html`
-
-
-
- `}
-
-
-
-
-
-
-
Map
-
- ${Object.entries(GameMapType)
- .filter(([key]) => isNaN(Number(key)))
- .map(
- ([key, value]) => html`
-
this.handleMapSelection(value)}>
-
-
- `,
- )}
-
-
-
-
-
Random
-
-
-
-
-
-
-
Difficulty
-
- ${Object.entries(Difficulty)
- .filter(([key]) => isNaN(Number(key)))
- .map(
- ([key, value]) => html`
-
this.handleDifficultySelection(value)}
- >
-
-
- ${DifficultyDescription[key]}
-
-
- `,
- )}
-
-
-
-
-
-
Options
-
-
-
-
- Bots: ${this.bots == 0 ? "Disabled" : this.bots}
-
-
-
-
-
-
- Disable Nations
-
-
-
-
-
- Instant build
-
-
-
-
-
- Infinite gold
-
-
-
-
-
- Infinite troops
-
-
-
-
-
- Disable Nukes
-
-
-
-
-
-
-
- ${this.players.length}
- ${this.players.length === 1 ? "Player" : "Players"}
-
-
-
- ${this.players.map(
- (player) => html`${player} `,
- )}
-
-
-
+
+
- ${this.players.length === 1
- ? "Waiting for players..."
- : "Start Game"}
+ ${this.lobbyId}
+ ${this.copySuccess
+ ? html`✓ `
+ : html`
+
+
+
+ `}
-
+
+
+
+
Map
+
+ ${Object.entries(GameMapType)
+ .filter(([key]) => isNaN(Number(key)))
+ .map(
+ ([key, value]) => html`
+
this.handleMapSelection(value)}>
+
+
+ `,
+ )}
+
+
+
+
+
Random
+
+
+
+
+
+
+
Difficulty
+
+ ${Object.entries(Difficulty)
+ .filter(([key]) => isNaN(Number(key)))
+ .map(
+ ([key, value]) => html`
+
this.handleDifficultySelection(value)}
+ >
+
+
+ ${DifficultyDescription[key]}
+
+
+ `,
+ )}
+
+
+
+
+
+
Options
+
+
+
+
+ Bots: ${this.bots == 0 ? "Disabled" : this.bots}
+
+
+
+
+
+
+ Disable Nations
+
+
+
+
+
+ Instant build
+
+
+
+
+
+ Infinite gold
+
+
+
+
+
+ Infinite troops
+
+
+
+
+ Disable Nukes
+
+
+
+
+
+
+
+ ${this.players.length}
+ ${this.players.length === 1 ? "Player" : "Players"}
+
+
+
+ ${this.players.map(
+ (player) => html`${player} `,
+ )}
+
+
+
+
+
+
+
+
`;
}
+ 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();
diff --git a/src/client/JoinPrivateLobbyModal.ts b/src/client/JoinPrivateLobbyModal.ts
index ffd4268de..1afd669f4 100644
--- a/src/client/JoinPrivateLobbyModal.ts
+++ b/src/client/JoinPrivateLobbyModal.ts
@@ -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`
-
-
-
-
×
-
Join Private Lobby
-
-
-
+
-
- ${this.message}
-
-
-
- ${this.hasJoined && this.players.length > 0
- ? html`
-
- ${this.players.length}
- ${this.players.length === 1 ? "Player" : "Players"}
-
+
+
+
+
+
+ ${this.message}
+
+
+ ${this.hasJoined && this.players.length > 0
+ ? html`
+
+ ${this.players.length}
+ ${this.players.length === 1 ? "Player" : "Players"}
+
-
- ${this.players.map(
- (player) =>
- html`${player} `,
- )}
-
-
`
- : ""}
-
- ${!this.hasJoined
- ? html`
- Join Lobby
- `
+
+ ${this.players.map(
+ (player) => html`${player} `,
+ )}
+
+
`
: ""}
-
+
+ ${!this.hasJoined
+ ? html` `
+ : ""}
+
+
`;
}
- 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;
diff --git a/src/client/Main.ts b/src/client/Main.ts
index b77129bc3..48354c68d 100644
--- a/src/client/Main.ts
+++ b/src/client/Main.ts
@@ -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";
diff --git a/src/client/SinglePlayerModal.ts b/src/client/SinglePlayerModal.ts
index 528c0dab2..7d9a5b59c 100644
--- a/src/client/SinglePlayerModal.ts
+++ b/src/client/SinglePlayerModal.ts
@@ -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`
-
-
-
-
×
-
-
Single Player
-
-
-
-
-
Map
-
- ${Object.entries(GameMapType)
- .filter(([key]) => isNaN(Number(key)))
- .map(
- ([key, value]) => html`
-
-
-
- `,
- )}
-
-
-
-
-
Random
+
+
+
+
+
Map
+
+ ${Object.entries(GameMapType)
+ .filter(([key]) => isNaN(Number(key)))
+ .map(
+ ([key, value]) => html`
+
+
+
+ `,
+ )}
+
+
+
-
-
-
-
-
-
Difficulty
-
- ${Object.entries(Difficulty)
- .filter(([key]) => isNaN(Number(key)))
- .map(
- ([key, value]) => html`
-
this.handleDifficultySelection(value)}
- >
-
-
- ${DifficultyDescription[key]}
-
-
- `,
- )}
-
-
-
-
-
-
Options
-
-
-
-
- Bots: ${this.bots == 0 ? "Disabled" : this.bots}
-
-
-
-
-
-
- Disable Nations
-
-
-
-
- Instant build
-
-
-
-
-
- Infinite gold
-
-
-
-
-
- Infinite troops
-
-
-
-
-
- Disable Nukes
-
+
Random
-
- Start Game
-
+
+
+
Difficulty
+
+ ${Object.entries(Difficulty)
+ .filter(([key]) => isNaN(Number(key)))
+ .map(
+ ([key, value]) => html`
+
this.handleDifficultySelection(value)}
+ >
+
+
+ ${DifficultyDescription[key]}
+
+
+ `,
+ )}
+
+
+
+
+
+
Options
+
+
+
+
+ Bots: ${this.bots == 0 ? "Disabled" : this.bots}
+
+
+
+
+
+
+ Disable Nations
+
+
+
+
+ Instant build
+
+
+
+
+
+ Infinite gold
+
+
+
+
+
+ Infinite troops
+
+
+
+
+
+ Disable Nukes
+
+
+
-
+
+
+
`;
}
+ 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);
}
diff --git a/src/client/components/baseComponents/Button.ts b/src/client/components/baseComponents/Button.ts
new file mode 100644
index 000000000..efb44af13
--- /dev/null
+++ b/src/client/components/baseComponents/Button.ts
@@ -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`
+
+ ${this.title}
+
+ `;
+ }
+}
diff --git a/src/client/components/baseComponents/Modal.ts b/src/client/components/baseComponents/Modal.ts
new file mode 100644
index 000000000..5b762129a
--- /dev/null
+++ b/src/client/components/baseComponents/Modal.ts
@@ -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`
+
+ `
+ : html``}
+ `;
+ }
+}
diff --git a/src/client/index.html b/src/client/index.html
index 6ede5b98f..22a7fce4e 100644
--- a/src/client/index.html
+++ b/src/client/index.html
@@ -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 @@
-
-
+
+
-
-
+
-
-
+
-
-
-
+
- Create Lobby
-
-
+
- Join Lobby
-
+ title="Join Lobby"
+ translationKey="main.join_lobby"
+ block
+ secondary
+ >
-
-
- Single Player
-
-
+
-
-
- Instructions
-
-
-
+
+
{
+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