This commit is contained in:
NewHappyRabbit
2025-02-12 20:03:26 +02:00
80 changed files with 21588 additions and 19405 deletions
+15 -15
View File
@@ -64,13 +64,13 @@ export interface LobbyConfig {
export function joinLobby(
lobbyConfig: LobbyConfig,
onjoin: () => void
onjoin: () => void,
): () => void {
const eventBus = new EventBus();
initRemoteSender(eventBus);
consolex.log(
`joinging lobby: gameID: ${lobbyConfig.gameID}, clientID: ${lobbyConfig.clientID}, persistentID: ${lobbyConfig.persistentID}`
`joinging lobby: gameID: ${lobbyConfig.gameID}, clientID: ${lobbyConfig.clientID}, persistentID: ${lobbyConfig.persistentID}`,
);
const serverConfig = getServerConfig();
@@ -91,7 +91,7 @@ export function joinLobby(
lobbyConfig,
gameConfig,
eventBus,
serverConfig
serverConfig,
);
const onconnect = () => {
@@ -103,7 +103,7 @@ export function joinLobby(
consolex.log("lobby: game started");
onjoin();
createClientGame(lobbyConfig, message.config, eventBus, transport).then(
(r) => r.start()
(r) => r.start(),
);
}
};
@@ -118,7 +118,7 @@ export async function createClientGame(
lobbyConfig: LobbyConfig,
gameConfig: GameConfig,
eventBus: EventBus,
transport: Transport
transport: Transport,
): Promise<ClientGameRunner> {
const config = getConfig(gameConfig);
@@ -126,14 +126,14 @@ export async function createClientGame(
const worker = new WorkerClient(
lobbyConfig.gameID,
gameConfig,
lobbyConfig.clientID
lobbyConfig.clientID,
);
await worker.initialize();
const gameView = new GameView(
worker,
config,
gameMap.gameMap,
lobbyConfig.clientID
lobbyConfig.clientID,
);
consolex.log("going to init path finder");
@@ -143,11 +143,11 @@ export async function createClientGame(
canvas,
gameView,
eventBus,
lobbyConfig.clientID
lobbyConfig.clientID,
);
consolex.log(
`creating private game got difficulty: ${gameConfig.difficulty}`
`creating private game got difficulty: ${gameConfig.difficulty}`,
);
return new ClientGameRunner(
@@ -157,7 +157,7 @@ export async function createClientGame(
new InputHandler(canvas, eventBus),
transport,
worker,
gameView
gameView,
);
}
@@ -175,7 +175,7 @@ export class ClientGameRunner {
private input: InputHandler,
private transport: Transport,
private worker: WorkerClient,
private gameView: GameView
private gameView: GameView,
) {}
public start() {
@@ -223,7 +223,7 @@ export class ClientGameRunner {
}
if (this.turnsSeen != message.turn.turnNumber) {
consolex.error(
`got wrong turn have turns ${this.turnsSeen}, received turn ${message.turn.turnNumber}`
`got wrong turn have turns ${this.turnsSeen}, received turn ${message.turn.turnNumber}`,
);
} else {
this.worker.sendTurn(message.turn);
@@ -246,7 +246,7 @@ export class ClientGameRunner {
}
const cell = this.renderer.transformHandler.screenToWorldCoordinates(
event.x,
event.y
event.y,
);
if (!this.gameView.isValidCoord(cell.x, cell.y)) {
return;
@@ -276,8 +276,8 @@ export class ClientGameRunner {
this.eventBus.emit(
new SendAttackIntentEvent(
this.gameView.owner(tile).id(),
this.myPlayer.troops() * this.renderer.uiState.attackRatio
)
this.myPlayer.troops() * this.renderer.uiState.attackRatio,
),
);
}
});
+372 -119
View File
@@ -3,12 +3,15 @@ import { customElement, property, state } from "lit/decorators.js";
import { Difficulty, GameMapType, GameType } from "../core/game/Game";
import { Lobby } from "../core/Schemas";
import { consolex } from "../core/Consolex";
import "./components/Difficulties";
import { DifficultyDescription } from "./components/Difficulties";
import "./components/Maps";
@customElement("host-lobby-modal")
export class HostLobbyModal extends LitElement {
@state() private isModalOpen = false;
@state() private selectedMap: GameMapType = GameMapType.World;
@state() private selectedDiffculty: Difficulty = Difficulty.Medium;
@state() private selectedDifficulty: Difficulty = Difficulty.Medium;
@state() private disableNPCs = false;
@state() private disableBots = false;
@state() private creativeMode = false;
@@ -28,16 +31,57 @@ export class HostLobbyModal extends LitElement {
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: white;
margin: 15% auto;
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-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 {
@@ -50,55 +94,211 @@ export class HostLobbyModal extends LitElement {
.close:hover,
.close:focus {
color: black;
color: white;
text-decoration: none;
cursor: pointer;
}
button {
padding: 10px 20px;
.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: 4px;
border-radius: 8px;
transition: background-color 0.3s;
display: inline-block;
margin-top: 20px;
margin: 0 0 20px 0;
}
button:hover {
.start-game-button:not(:disabled):hover {
background-color: #0056b3;
}
select {
padding: 8px;
font-size: 16px;
margin-top: 10px;
width: 200px;
.start-game-button:disabled {
background: linear-gradient(to right, #4a4a4a, #3d3d3d);
opacity: 0.7;
cursor: not-allowed;
}
.lobby-id-container {
.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;
}
.clipboard-icon {
.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: opacity 0.3s;
transition: all 0.2s ease-in-out;
}
.clipboard-icon:hover {
opacity: 0.7;
.lobby-id-button:hover {
background: rgba(0, 0, 0, 0.3);
border-color: rgba(255, 255, 255, 0.2);
transform: translateY(-1px);
}
.copy-success {
color: green;
.lobby-id {
font-size: 14px;
margin-top: 5px;
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);
}
`;
@@ -106,97 +306,157 @@ export class HostLobbyModal extends LitElement {
return html`
<div
class="modal-overlay"
style="display: ${this.isModalOpen ? "block" : "none"}"
style="display: ${this.isModalOpen ? "flex" : "none"}"
>
<div class="modal-content">
<span class="close" @click=${this.close}>&times;</span>
<h2>Private Lobby</h2>
<div class="lobby-id-container">
<h3>Lobby ID: ${this.lobbyId}</h3>
<svg
<div class="title">Private Lobby</div>
<div class="lobby-id-box">
<button
class="lobby-id-button"
@click=${this.copyToClipboard}
class="clipboard-icon"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
?disabled=${this.copySuccess}
>
<path
d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"
></path>
<rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect>
</svg>
</div>
${this.copySuccess
? html`<p class="copy-success">Copied to clipboard!</p>`
: ""}
<div>
<label for="map-select">Map: </label>
<select id="map-select" @change=${this.handleMapChange}>
${Object.entries(GameMapType)
.filter(([key]) => isNaN(Number(key)))
.map(
([key, value]) => html`
<option
value=${value}
?selected=${this.selectedMap === value}
<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"
>
${key}
</option>
`,
<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.selectedMap === value}
></map-display>
</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="disable-bots"
class="option-card ${this.disableBots ? "selected" : ""}"
>
<div class="checkbox-icon"></div>
<input
type="checkbox"
id="disable-bots"
@change=${this.handleDisableBotsChange}
.checked=${this.disableBots}
/>
<div class="option-card-title">Disable 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 NPCs</div>
</label>
<label
for="creative-mode"
class="option-card ${this.creativeMode ? "selected" : ""}"
>
<div class="checkbox-icon"></div>
<input
type="checkbox"
id="creative-mode"
@change=${this.handleCreativeModeChange}
.checked=${this.creativeMode}
/>
<div class="option-card-title">Creative Mode</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>`,
)}
</select>
</div>
<div>
<label for="map-select">Difficulty: </label>
<select id="map-select" @change=${this.handleDifficultyChange}>
${Object.entries(Difficulty)
.filter(([key]) => isNaN(Number(key)))
.map(
([key, value]) => html`
<option
value=${value}
?selected=${this.selectedDiffculty === value}
>
${key}
</option>
`,
)}
</select>
</div>
<div>
<input
type="checkbox"
id="disable-bots"
@change=${this.handleDisableBotsChange}
/>
<label for="disable-bots">Disable Bots</label>
</div>
<div>
<input
type="checkbox"
id="disable-npcs"
@change=${this.handleDisableNPCsChange}
/>
<label for="disable-npcs">Disable NPCs</label>
</div>
<div>
<input
type="checkbox"
id="creative-mode"
@change=${this.handleCreativeModeChange}
/>
<label for="creative-mode">Creative mode</label>
</div>
<button @click=${this.startGame}>Start Game</button>
<div>
<p>Players: ${this.players.join(", ")}</p>
<p></p>
</div>
</div>
</div>
<button
@click=${this.startGame}
?disabled=${this.players.length < 2}
class="start-game-button"
>
${this.players.length === 1
? "Waiting for players..."
: "Start Game"}
</button>
</div>
</div>
`;
@@ -217,7 +477,7 @@ export class HostLobbyModal extends LitElement {
id: this.lobbyId,
},
map: this.selectedMap,
difficulty: this.selectedDiffculty,
difficulty: this.selectedDifficulty,
disableBots: this.disableBots,
disableNPCs: this.disableNPCs,
creativeMode: this.creativeMode,
@@ -240,19 +500,12 @@ export class HostLobbyModal extends LitElement {
}
}
private async handleMapChange(e: Event) {
this.selectedMap = String(
(e.target as HTMLSelectElement).value,
) as GameMapType;
consolex.log(`updating map to ${this.selectedMap}`);
private async handleMapSelection(value: GameMapType) {
this.selectedMap = value;
this.putGameConfig();
}
private async handleDifficultyChange(e: Event) {
this.selectedDiffculty = String(
(e.target as HTMLSelectElement).value,
) as Difficulty;
consolex.log(`updating difficulty to ${this.selectedDiffculty}`);
private async handleDifficultySelection(value: Difficulty) {
this.selectedDifficulty = value;
this.putGameConfig();
}
@@ -282,7 +535,7 @@ export class HostLobbyModal extends LitElement {
},
body: JSON.stringify({
gameMap: this.selectedMap,
difficulty: this.selectedDiffculty,
difficulty: this.selectedDifficulty,
disableBots: this.disableBots,
disableNPCs: this.disableNPCs,
creativeMode: this.creativeMode,
+83 -2
View File
@@ -72,6 +72,12 @@ export class InputHandler {
private alternateView = false;
private moveInterval: any = null;
private activeKeys = new Set<string>();
private readonly PAN_SPEED = 5;
private readonly ZOOM_SPEED = 10;
constructor(
private canvas: HTMLCanvasElement,
private eventBus: EventBus,
@@ -95,14 +101,67 @@ export class InputHandler {
});
this.pointers.clear();
// Initialize the combined movement interval
this.moveInterval = setInterval(() => {
let deltaX = 0;
let deltaY = 0;
// Handle both WASD and arrow keys
if (this.activeKeys.has("KeyW") || this.activeKeys.has("ArrowUp"))
deltaY += this.PAN_SPEED;
if (this.activeKeys.has("KeyS") || this.activeKeys.has("ArrowDown"))
deltaY -= this.PAN_SPEED;
if (this.activeKeys.has("KeyA") || this.activeKeys.has("ArrowLeft"))
deltaX += this.PAN_SPEED;
if (this.activeKeys.has("KeyD") || this.activeKeys.has("ArrowRight"))
deltaX -= this.PAN_SPEED;
if (deltaX !== 0 || deltaY !== 0) {
this.eventBus.emit(new DragEvent(deltaX, deltaY));
}
// Handle zooming
const screenCenterX = window.innerWidth / 2;
const screenCenterY = window.innerHeight / 2;
if (this.activeKeys.has("Minus")) {
this.eventBus.emit(
new ZoomEvent(screenCenterX, screenCenterY, this.ZOOM_SPEED),
);
}
if (this.activeKeys.has("Equal")) {
this.eventBus.emit(
new ZoomEvent(screenCenterX, screenCenterY, -this.ZOOM_SPEED),
);
}
}, 1);
window.addEventListener("keydown", (e) => {
if (e.code === "Space") {
e.preventDefault(); // Prevent page scrolling
e.preventDefault();
if (!this.alternateView) {
this.alternateView = true;
this.eventBus.emit(new AlternateViewEvent(true));
}
}
// Add all movement keys to activeKeys
if (
[
"KeyW",
"KeyA",
"KeyS",
"KeyD",
"ArrowUp",
"ArrowLeft",
"ArrowDown",
"ArrowRight",
"Minus",
"Equal",
].includes(e.code)
) {
this.activeKeys.add(e.code);
}
});
window.addEventListener("keyup", (e) => {
@@ -115,6 +174,24 @@ export class InputHandler {
e.preventDefault();
this.eventBus.emit(new RefreshGraphicsEvent());
}
// Remove all movement keys from activeKeys
if (
[
"KeyW",
"KeyA",
"KeyS",
"KeyD",
"ArrowUp",
"ArrowLeft",
"ArrowDown",
"ArrowRight",
"Minus",
"Equal",
].includes(e.code)
) {
this.activeKeys.delete(e.code);
}
});
}
@@ -193,7 +270,6 @@ export class InputHandler {
const pinchDelta = currentPinchDistance - this.lastPinchDistance;
if (Math.abs(pinchDelta) > 1) {
// Threshold to avoid tiny zoom adjustments
const zoomCenter = this.getPinchCenter();
this.eventBus.emit(
new ZoomEvent(zoomCenter.x, zoomCenter.y, -pinchDelta * 2),
@@ -222,4 +298,9 @@ export class InputHandler {
y: (pointerEvents[0].clientY + pointerEvents[1].clientY) / 2,
};
}
destroy() {
clearInterval(this.moveInterval);
this.activeKeys.clear();
}
}
+203 -32
View File
@@ -9,6 +9,9 @@ export class JoinPrivateLobbyModal extends LitElement {
@state() private message: string = "";
@query("#lobbyIdInput") private lobbyIdInput!: HTMLInputElement;
@state() private hasJoined = false;
@state() private players: string[] = [];
private playersInterval = null;
static styles = css`
.modal-overlay {
@@ -20,16 +23,58 @@ export class JoinPrivateLobbyModal extends LitElement {
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: white;
margin: 15% auto;
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 {
color: #aaa;
float: right;
@@ -37,47 +82,41 @@ export class JoinPrivateLobbyModal extends LitElement {
font-weight: bold;
cursor: pointer;
}
.close:hover,
.close:focus {
color: black;
color: white;
text-decoration: none;
cursor: pointer;
}
button {
padding: 10px 20px;
.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: 4px;
border-radius: 8px;
transition: background-color 0.3s;
display: inline-block;
margin: 0 0 20px 0;
}
button:hover {
.start-game-button:not(:disabled):hover {
background-color: #0056b3;
}
.lobby-id-container {
display: flex;
align-items: stretch;
justify-content: center;
gap: 10px;
margin: 20px 0;
}
.lobby-id-container input {
flex-grow: 1;
max-width: 200px;
padding: 10px;
font-size: 16px;
border: 1px solid #ccc;
border-radius: 4px;
}
.lobby-id-container button {
padding: 10px 15px;
}
.join-button {
margin-top: 10px;
.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;
@@ -104,26 +143,135 @@ export class JoinPrivateLobbyModal extends LitElement {
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);
}
`;
render() {
return html`
<div
class="modal-overlay"
style="display: ${this.isModalOpen ? "block" : "none"}"
style="display: ${this.isModalOpen ? "flex" : "none"}"
>
<div class="modal-content">
<span class="close" @click=${this.closeAndLeave}>&times;</span>
<h2>Join Private Lobby</h2>
<div class="lobby-id-container">
<div class="title">Join Private Lobby</div>
<div class="lobby-id-box">
<input type="text" id="lobbyIdInput" placeholder="Enter Lobby ID" />
<button @click=${this.pasteFromClipboard}>Paste</button>
<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"
>
<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>
<div class="players-list">
${this.players.map(
(player) =>
html`<span class="player-tag">${player}</span>`,
)}
</div>
</div>`
: ""}
</div>
${!this.hasJoined
? html`<button class="join-button" @click=${this.joinLobby}>
? html`<button class="start-game-button" @click=${this.joinLobby}>
Join Lobby
</button>`
: ""}
@@ -139,6 +287,10 @@ export class JoinPrivateLobbyModal extends LitElement {
public close() {
this.isModalOpen = false;
this.lobbyIdInput.value = null;
if (this.playersInterval) {
clearInterval(this.playersInterval);
this.playersInterval = null;
}
}
public closeAndLeave() {
@@ -192,6 +344,7 @@ export class JoinPrivateLobbyModal extends LitElement {
composed: true,
}),
);
this.playersInterval = setInterval(() => this.pollPlayers(), 1000);
} else {
this.message = "Lobby not found. Please check the ID and try again.";
}
@@ -201,4 +354,22 @@ export class JoinPrivateLobbyModal extends LitElement {
this.message = "An error occurred. Please try again.";
});
}
private async pollPlayers() {
if (!this.lobbyIdInput?.value) return;
fetch(`/lobby/${this.lobbyIdInput.value}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
})
.then((response) => response.json())
.then((data) => {
this.players = data.players.map((p) => p.username);
})
.catch((error) => {
consolex.error("Error polling players:", error);
});
}
}
+138 -149
View File
@@ -1,98 +1,90 @@
import { ClientGameRunner, joinLobby } from './ClientGameRunner';
import backgroundImage from '../../resources/images/EuropeBackground.svg';
import favicon from '../../resources/images/Favicon.svg';
import { ClientGameRunner, joinLobby } from "./ClientGameRunner";
import backgroundImage from "../../resources/images/EuropeBackground.svg";
import favicon from "../../resources/images/Favicon.svg";
import './PublicLobby';
import './UsernameInput';
import './styles.css';
import { UsernameInput } from './UsernameInput';
import { SinglePlayerModal } from './SinglePlayerModal';
import { HostLobbyModal as HostPrivateLobbyModal } from './HostLobbyModal';
import { JoinPrivateLobbyModal } from './JoinPrivateLobbyModal';
import { generateID } from '../core/Util';
import { generateCryptoRandomUUID } from './Utils';
import { consolex } from '../core/Consolex';
import './components/FlagInput';
import { FlagInput } from './components/FlagInput';
import "./PublicLobby";
import "./UsernameInput";
import "./styles.css";
import { UsernameInput } from "./UsernameInput";
import { SinglePlayerModal } from "./SinglePlayerModal";
import { HostLobbyModal as HostPrivateLobbyModal } from "./HostLobbyModal";
import { JoinPrivateLobbyModal } from "./JoinPrivateLobbyModal";
import { generateID } from "../core/Util";
import { generateCryptoRandomUUID } from "./Utils";
import { consolex } from "../core/Consolex";
import "./components/FlagInput";
import { FlagInput } from "./components/FlagInput";
import page from 'page';
class Client {
private gameStop: () => void;
private gameStop: () => void;
private usernameInput: UsernameInput | null = null;
private flagInput: FlagInput | null = null;
private usernameInput: UsernameInput | null = null;
private flagInput: FlagInput | null = null;
private joinModal: JoinPrivateLobbyModal;
constructor() {}
private joinModal: JoinPrivateLobbyModal;
constructor() {}
initialize(): void {
this.flagInput = document.querySelector('flag-input') as FlagInput;
if (!this.flagInput) {
consolex.warn('Flag input element not found');
}
initialize(): void {
this.flagInput = document.querySelector("flag-input") as FlagInput;
if (!this.flagInput) {
consolex.warn("Flag input element not found");
}
this.usernameInput = document.querySelector(
'username-input'
) as UsernameInput;
if (!this.usernameInput) {
consolex.warn('Username input element not found');
}
window.addEventListener('beforeunload', (event) => {
consolex.log('Browser is closing');
if (this.gameStop != null) {
this.gameStop();
}
});
this.usernameInput = document.querySelector(
"username-input",
) as UsernameInput;
if (!this.usernameInput) {
consolex.warn("Username input element not found");
}
window.addEventListener("beforeunload", (event) => {
consolex.log("Browser is closing");
if (this.gameStop != null) {
this.gameStop();
}
});
setFavicon();
document.addEventListener(
'join-lobby',
this.handleJoinLobby.bind(this)
);
document.addEventListener(
'leave-lobby',
this.handleLeaveLobby.bind(this)
);
document.addEventListener(
'single-player',
this.handleSinglePlayer.bind(this)
);
setFavicon();
document.addEventListener("join-lobby", this.handleJoinLobby.bind(this));
document.addEventListener("leave-lobby", this.handleLeaveLobby.bind(this));
document.addEventListener(
"single-player",
this.handleSinglePlayer.bind(this),
);
const spModal = document.querySelector(
'single-player-modal'
) as SinglePlayerModal;
spModal instanceof SinglePlayerModal;
document
.getElementById('single-player')
.addEventListener('click', () => {
if (this.usernameInput.isValid()) {
spModal.open();
}
});
const spModal = document.querySelector(
"single-player-modal",
) as SinglePlayerModal;
spModal instanceof SinglePlayerModal;
document.getElementById("single-player").addEventListener("click", () => {
if (this.usernameInput.isValid()) {
spModal.open();
}
});
const hostModal = document.querySelector(
'host-lobby-modal'
) as HostPrivateLobbyModal;
hostModal instanceof HostPrivateLobbyModal;
document
.getElementById('host-lobby-button')
.addEventListener('click', () => {
if (this.usernameInput.isValid()) {
hostModal.open();
}
});
const hostModal = document.querySelector(
"host-lobby-modal",
) as HostPrivateLobbyModal;
hostModal instanceof HostPrivateLobbyModal;
document
.getElementById("host-lobby-button")
.addEventListener("click", () => {
if (this.usernameInput.isValid()) {
hostModal.open();
}
});
this.joinModal = document.querySelector(
'join-private-lobby-modal'
) as JoinPrivateLobbyModal;
this.joinModal instanceof JoinPrivateLobbyModal;
document
.getElementById('join-private-lobby-button')
.addEventListener('click', () => {
if (this.usernameInput.isValid()) {
this.joinModal.open();
}
});
this.joinModal = document.querySelector(
"join-private-lobby-modal",
) as JoinPrivateLobbyModal;
this.joinModal instanceof JoinPrivateLobbyModal;
document
.getElementById("join-private-lobby-button")
.addEventListener("click", () => {
if (this.usernameInput.isValid()) {
this.joinModal.open();
}
});
page('/join/:id', (ctx) => {
// TODO: Implement logic for joining a lobby
@@ -101,88 +93,85 @@ class Client {
});
page();
}
}
private async handleJoinLobby(event: CustomEvent) {
const lobby = event.detail.lobby;
consolex.log(`joining lobby ${lobby.id}`);
if (this.gameStop != null) {
consolex.log('joining lobby, stopping existing game');
this.gameStop();
}
this.gameStop = joinLobby(
{
gameType: event.detail.gameType,
flag: (): string => this.flagInput.getCurrentFlag(),
playerName: (): string =>
this.usernameInput.getCurrentUsername(),
gameID: lobby.id,
persistentID: getPersistentIDFromCookie(),
playerID: generateID(),
clientID: generateID(),
map: event.detail.map,
difficulty: event.detail.difficulty,
disableBots: event.detail.disableBots,
disableNPCs: event.detail.disableNPCs,
creativeMode: event.detail.creativeMode,
},
() => this.joinModal.close()
);
}
private async handleJoinLobby(event: CustomEvent) {
const lobby = event.detail.lobby;
consolex.log(`joining lobby ${lobby.id}`);
if (this.gameStop != null) {
consolex.log("joining lobby, stopping existing game");
this.gameStop();
}
this.gameStop = joinLobby(
{
gameType: event.detail.gameType,
flag: (): string => this.flagInput.getCurrentFlag(),
playerName: (): string => this.usernameInput.getCurrentUsername(),
gameID: lobby.id,
persistentID: getPersistentIDFromCookie(),
playerID: generateID(),
clientID: generateID(),
map: event.detail.map,
difficulty: event.detail.difficulty,
disableBots: event.detail.disableBots,
disableNPCs: event.detail.disableNPCs,
creativeMode: event.detail.creativeMode,
},
() => this.joinModal.close(),
);
}
private async handleLeaveLobby(event: CustomEvent) {
if (this.gameStop == null) {
return;
}
consolex.log('leaving lobby, cancelling game');
this.gameStop();
this.gameStop = null;
}
private async handleLeaveLobby(event: CustomEvent) {
if (this.gameStop == null) {
return;
}
consolex.log("leaving lobby, cancelling game");
this.gameStop();
this.gameStop = null;
}
private async handleSinglePlayer(event: CustomEvent) {
alert('coming soon');
}
private async handleSinglePlayer(event: CustomEvent) {
alert("coming soon");
}
}
// Initialize the client when the DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
new Client().initialize();
document.addEventListener("DOMContentLoaded", () => {
new Client().initialize();
});
document.body.style.backgroundImage = `url(${backgroundImage})`;
function setFavicon(): void {
const link = document.createElement('link');
link.type = 'image/x-icon';
link.rel = 'shortcut icon';
link.href = favicon;
document.head.appendChild(link);
const link = document.createElement("link");
link.type = "image/x-icon";
link.rel = "shortcut icon";
link.href = favicon;
document.head.appendChild(link);
}
// WARNING: DO NOT EXPOSE THIS ID
export function getPersistentIDFromCookie(): string {
const COOKIE_NAME = 'player_persistent_id';
const COOKIE_NAME = "player_persistent_id";
// Try to get existing cookie
const cookies = document.cookie.split(';');
for (let cookie of cookies) {
const [cookieName, cookieValue] = cookie
.split('=')
.map((c) => c.trim());
if (cookieName === COOKIE_NAME) {
return cookieValue;
}
}
// Try to get existing cookie
const cookies = document.cookie.split(";");
for (let cookie of cookies) {
const [cookieName, cookieValue] = cookie.split("=").map((c) => c.trim());
if (cookieName === COOKIE_NAME) {
return cookieValue;
}
}
// If no cookie exists, create new ID and set cookie
const newID = generateCryptoRandomUUID();
document.cookie = [
`${COOKIE_NAME}=${newID}`,
`max-age=${5 * 365 * 24 * 60 * 60}`, // 5 years
'path=/',
'SameSite=Strict',
'Secure',
].join(';');
// If no cookie exists, create new ID and set cookie
const newID = generateCryptoRandomUUID();
document.cookie = [
`${COOKIE_NAME}=${newID}`,
`max-age=${5 * 365 * 24 * 60 * 60}`, // 5 years
"path=/",
"SameSite=Strict",
"Secure",
].join(";");
return newID;
return newID;
}
+3 -3
View File
@@ -20,7 +20,7 @@ export class PublicLobby extends LitElement {
this.fetchAndUpdateLobbies();
this.lobbiesInterval = window.setInterval(
() => this.fetchAndUpdateLobbies(),
1000
1000,
);
}
@@ -111,7 +111,7 @@ export class PublicLobby extends LitElement {
},
bubbles: true,
composed: true,
})
}),
);
} else {
this.dispatchEvent(
@@ -119,7 +119,7 @@ export class PublicLobby extends LitElement {
detail: { lobby: this.currLobby },
bubbles: true,
composed: true,
})
}),
);
this.currLobby = null;
}
+268 -84
View File
@@ -3,6 +3,9 @@ import { customElement, property, 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 { DifficultyDescription } from "./components/Difficulties";
import "./components/Maps";
@customElement("single-player-modal")
export class SinglePlayerModal extends LitElement {
@@ -23,16 +26,57 @@ export class SinglePlayerModal extends LitElement {
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: white;
margin: 15% auto;
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-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 {
@@ -45,33 +89,139 @@ export class SinglePlayerModal extends LitElement {
.close:hover,
.close:focus {
color: black;
color: white;
text-decoration: none;
cursor: pointer;
}
button {
padding: 10px 20px;
.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: 4px;
border-radius: 8px;
transition: background-color 0.3s;
display: inline-block;
margin-top: 20px;
margin: 0 0 20px 0;
}
button:hover {
.start-game-button:not(:disabled):hover {
background-color: #0056b3;
}
select {
padding: 8px;
font-size: 16px;
margin-top: 10px;
width: 200px;
.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;
}
`;
@@ -79,72 +229,111 @@ export class SinglePlayerModal extends LitElement {
return html`
<div
class="modal-overlay"
style="display: ${this.isModalOpen ? "block" : "none"}"
style="display: ${this.isModalOpen ? "flex" : "none"}"
>
<div class="modal-content">
<span class="close" @click=${this.close}>&times;</span>
<h2>Start Single Player Game</h2>
<div>
<label for="map-select">Map: </label>
<select id="map-select" @change=${this.handleMapChange}>
${Object.entries(GameMapType)
.filter(([key]) => isNaN(Number(key)))
.map(
([key, value]) => html`
<option
value=${value}
?selected=${this.selectedMap === value}
>
${value}
</option>
`,
)}
</select>
</div>
<div>
<label for="map-select">Difficulty: </label>
<select id="map-select" @change=${this.handleDifficultyChange}>
${Object.entries(Difficulty)
.filter(([key]) => isNaN(Number(key)))
.map(
([key, value]) => html`
<option
value=${value}
?selected=${this.selectedDifficulty === value}
>
${value}
</option>
`,
)}
</select>
</div>
<div>
<input
type="checkbox"
id="disable-bots"
@change=${this.handleDisableBotsChange}
/>
<label for="disable-bots">Disable Bots</label>
</div>
<div>
<input
type="checkbox"
id="disable-npcs"
@change=${this.handleDisableNPCsChange}
/>
<label for="disable-npcs">Disable NPCs</label>
<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=${() => this.handleMapSelection(value)}>
<map-display
.mapKey=${key}
.selected=${this.selectedMap === value}
></map-display>
</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="disable-bots"
class="option-card ${this.disableBots ? "selected" : ""}"
>
<div class="checkbox-icon"></div>
<input
type="checkbox"
id="disable-bots"
@change=${this.handleDisableBotsChange}
.checked=${this.disableBots}
/>
<div class="option-card-title">Disable 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 NPCs</div>
</label>
<label
for="creative-mode"
class="option-card ${this.creativeMode ? "selected" : ""}"
>
<div class="checkbox-icon"></div>
<input
type="checkbox"
id="creative-mode"
@change=${this.handleCreativeModeChange}
.checked=${this.creativeMode}
/>
<div class="option-card-title">Creative Mode</div>
</label>
</div>
</div>
</div>
<div>
<input
type="checkbox"
id="creative-mode"
@change=${this.handleCreativeModeChange}
/>
<label for="creative-mode">Creative mode</label>
</div>
<button @click=${this.startGame}>Start Game</button>
<button @click=${this.startGame} class="start-game-button">
Start Game
</button>
</div>
</div>
`;
@@ -157,16 +346,11 @@ export class SinglePlayerModal extends LitElement {
public close() {
this.isModalOpen = false;
}
private handleMapChange(e: Event) {
this.selectedMap = String(
(e.target as HTMLSelectElement).value,
) as GameMapType;
private handleMapSelection(value: GameMapType) {
this.selectedMap = value;
}
private handleDifficultyChange(e: Event) {
this.selectedDifficulty = String(
(e.target as HTMLSelectElement).value,
) as Difficulty;
private handleDifficultySelection(value: Difficulty) {
this.selectedDifficulty = value;
}
private handleDisableBotsChange(e: Event) {
this.disableBots = Boolean((e.target as HTMLInputElement).checked);
+1
View File
@@ -3,6 +3,7 @@ export function renderTroops(troops: number): string {
}
export function renderNumber(num: number) {
num = Math.max(num, 0);
let numStr = "";
if (num >= 10_000_000) {
numStr = (num / 1000000).toFixed(1) + "M";
+150
View File
@@ -0,0 +1,150 @@
import { LitElement, html, css } from "lit";
import { customElement, property } from "lit/decorators.js";
export enum DifficultyDescription {
Easy = "Relaxed",
Medium = "Balanced",
Hard = "Intense",
Impossible = "Challenging",
}
@customElement("difficulty-display")
export class DifficultyDisplay extends LitElement {
@property({ type: String }) difficultyKey = "";
static styles = css`
.difficulty-indicator {
display: flex;
justify-content: center;
align-items: center;
height: 40px;
gap: 6px;
margin: 4px 0 0 0;
}
.difficulty-skull {
width: 16px;
height: 16px;
opacity: 0.3;
transition: all 0.2s ease;
}
.difficulty-skull.big {
width: 40px;
height: 40px;
}
.difficulty-skull.active {
opacity: 1;
color: #ff3838;
filter: drop-shadow(0 0 4px rgba(255, 56, 56, 0.4));
transform: translateY(-1px);
}
:host(:hover) .difficulty-skull.active {
filter: drop-shadow(0 0 6px rgba(255, 56, 56, 0.6));
transform: translateY(-2px);
}
`;
private getDifficultyIcon(difficultyKey: string) {
const skull = html`<svg
stroke="currentColor"
fill="none"
stroke-width="2"
viewBox="0 0 24 24"
stroke-linecap="round"
stroke-linejoin="round"
height="100%"
width="100%"
xmlns="http://www.w3.org/2000/svg"
>
<path d="m12.5 17-.5-1-.5 1h1z"></path>
<path
d="M15 22a1 1 0 0 0 1-1v-1a2 2 0 0 0 1.56-3.25 8 8 0 1 0-11.12 0A2 2 0 0 0 8 20v1a1 1 0 0 0 1 1z"
></path>
<circle cx="15" cy="12" r="1"></circle>
<circle cx="9" cy="12" r="1"></circle>
</svg>`;
const burningSkull = html`<svg
stroke="currentColor"
fill="currentColor"
stroke-width="0"
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
height="100%"
width="100%"
>
<path
d="M268.725 389.28l3.74 28.7h-30.89l3.74-28.7a11.705 11.705 0 1 1 23.41 0zm33.84-71.83a29.5 29.5 0 1 0 29.5 29.5 29.5 29.5 0 0 0-29.51-29.5zm-94.4 0a29.5 29.5 0 1 0 29.5 29.5 29.5 29.5 0 0 0-29.51-29.5zm245.71-62c0 98.2-48.22 182.68-117.39 220.24-46 28.26-112.77 28.26-156.19 2.5-71.72-36.21-122.17-122.29-122.17-222.73 0-78.16 30.54-147.63 77.89-191.67 0 0-42.08 82.86 9.1 135-11.67-173.77 169.28-63 118-184 151.79 83.33 9.14 105 84.1 148.21 0 0 66.21 47 36.4-91.73 42.95 43.99 70.25 110.3 70.25 184.19zm-68.54 29.87c-2.45-65.49-54.88-119.59-120.26-124.07-3.06-.21-6.15-.31-9.16-.31a129.4 129.4 0 0 0-129.43 129.35 132.15 132.15 0 0 0 24.51 76v25a35 35 0 0 0 34.74 34.69h6.26v16.61a34.66 34.66 0 0 0 34.71 34.39h61.78a34.48 34.48 0 0 0 34.51-34.39v-16.61h5.38a34.89 34.89 0 0 0 34.62-34.75v-28a129.32 129.32 0 0 0 22.33-77.9z"
></path>
</svg>`;
const kingSkull = html`<svg
stroke="currentColor"
fill="currentColor"
stroke-width="0"
viewBox="0 0 512 512"
height="100%"
width="100%"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M92.406 13.02l-.164 156.353c3.064.507 6.208 1.38 9.39 2.627 36.496 14.306 74.214 22.435 111.864 25.473l43.402-60.416 42.317 58.906c36.808-4.127 72.566-12.502 105.967-24.09 3.754-1.302 7.368-2.18 10.818-2.6l1.523-156.252-75.82 95.552-34.084-95.55-53.724 103.74-53.722-103.74-35.442 95.55-72.32-95.55h-.006zm164.492 156.07l-28.636 39.86 28.634 39.86 28.637-39.86-28.635-39.86zM86.762 187.55c-2.173-.08-3.84.274-5.012.762-2.345.977-3.173 2.19-3.496 4.196-.645 4.01 2.825 14.35 23.03 21.36 41.7 14.468 84.262 23.748 126.778 26.833l-17.75-24.704c-38.773-3.285-77.69-11.775-115.5-26.596-3.197-1.253-5.877-1.77-8.05-1.85zm333.275.19c-2.156.052-5.048.512-8.728 1.79-33.582 11.65-69.487 20.215-106.523 24.646l-19.264 26.818c40.427-2.602 80.433-11.287 119.22-26.96 15.913-6.43 21.46-17.81 21.36-22.362-.052-2.276-.278-2.566-1.753-3.274-.738-.353-2.157-.71-4.313-.658zm-18.117 47.438c-42.5 15.87-86.26 23.856-130.262 25.117l-14.76 20.547-14.878-20.71c-44.985-1.745-89.98-10.23-133.905-24.306-12.78 28.51-18.94 61.14-19.603 93.44 37.52 17.497 62.135 39.817 75.556 64.63C177 417.8 179.282 443.62 174.184 467.98c7.72 5.007 16.126 9.144 24.98 12.432l5.557-47.89 18.563 2.154-5.935 51.156c9.57 2.21 19.443 3.53 29.377 3.982v-54.67h18.69v54.49c9.903-.638 19.705-2.128 29.155-4.484l-5.857-50.474 18.564-2.155 5.436 46.852c8.747-3.422 17.004-7.643 24.506-12.69-5.758-24.413-3.77-49.666 9.01-72.988 13.28-24.234 37.718-46 74.803-64.29-.62-33.526-6.687-66.122-19.113-94.23zm-266.733 47.006c34.602.23 68.407 12.236 101.358 36.867-46.604 33.147-129.794 34.372-108.29-36.755 2.315-.09 4.626-.127 6.933-.11zm242.825 0c2.307-.016 4.617.022 6.93.11 21.506 71.128-61.684 69.903-108.288 36.757 32.95-24.63 66.756-36.637 101.358-36.866zM255.164 332.14c11.77 21.725 19.193 43.452 25.367 65.178h-50.737c4.57-21.726 13.77-43.45 25.37-65.18z"
></path>
</svg>`;
const questionMark = html`<svg
stroke="currentColor"
fill="currentColor"
stroke-width="0"
viewBox="0 0 24 24"
height="100%"
width="100%"
xmlns="http://www.w3.org/2000/svg"
>
<path fill="none" d="M0 0h24v24H0z"></path>
<path
d="M11.07 12.85c.77-1.39 2.25-2.21 3.11-3.44.91-1.29.4-3.7-2.18-3.7-1.69 0-2.52 1.28-2.87 2.34L6.54 6.96C7.25 4.83 9.18 3 11.99 3c2.35 0 3.96 1.07 4.78 2.41.7 1.15 1.11 3.3.03 4.9-1.2 1.77-2.35 2.31-2.97 3.45-.25.46-.35.76-.35 2.24h-2.89c-.01-.78-.13-2.05.48-3.15zM14 20c0 1.1-.9 2-2 2s-2-.9-2-2 .9-2 2-2 2 .9 2 2z"
></path>
</svg>`;
switch (difficultyKey) {
case "Easy":
return html`
<div class="difficulty-skull active">${skull}</div>
<div class="difficulty-skull">${skull}</div>
<div class="difficulty-skull">${skull}</div>
`;
case "Medium":
return html`
<div class="difficulty-skull active">${skull}</div>
<div class="difficulty-skull active">${skull}</div>
<div class="difficulty-skull">${skull}</div>
`;
case "Hard":
return html`
<div class="difficulty-skull active">${skull}</div>
<div class="difficulty-skull active">${skull}</div>
<div class="difficulty-skull active">${skull}</div>
`;
case "Impossible":
return html`
<div class="difficulty-skull big active">${burningSkull}</div>
`;
default:
return html`<div class="difficulty-skull big active">
${questionMark}
</div>`;
}
}
render() {
return html`
<div class="difficulty-indicator">
${this.getDifficultyIcon(this.difficultyKey)}
</div>
`;
}
}
+2 -1
View File
@@ -49,6 +49,7 @@ export class FlagInput extends LitElement {
left: 0;
width: 560px;
height: 500px;
max-height: 50vh;
background-color: rgb(35 35 35 / 0.8);
-webkit-backdrop-filter: blur(12px);
backdrop-filter: blur(12px);
@@ -140,7 +141,7 @@ export class FlagInput extends LitElement {
detail: { flag: this.flag },
bubbles: true,
composed: true,
})
}),
);
}
+115
View File
@@ -0,0 +1,115 @@
import { LitElement, html, css } from "lit";
import { customElement, property } from "lit/decorators.js";
import { GameMapType } from "../../core/game/Game";
// Add map descriptions
export const MapDescription: Record<keyof typeof GameMapType, string> = {
World: "World",
Europe: "Europe",
Mena: "MENA",
NorthAmerica: "North America",
Oceania: "Oceania",
BlackSea: "Black Sea",
};
import world from "../../../resources/maps/WorldMap.png";
import oceania from "../../../resources/maps/Oceania.png";
import europe from "../../../resources/maps/Europe.png";
import mena from "../../../resources/maps/Mena.png";
import northAmerica from "../../../resources/maps/NorthAmerica.png";
import blackSea from "../../../resources/maps/BlackSea.png";
@customElement("map-display")
export class MapDisplay extends LitElement {
@property({ type: String }) mapKey = "";
@property({ type: Boolean }) selected = false;
static styles = css`
.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;
}
`;
private getMapsImage(map: GameMapType): string {
switch (map) {
case GameMapType.World:
return world;
case GameMapType.Oceania:
return oceania;
case GameMapType.Europe:
return europe;
case GameMapType.Mena:
return mena;
case GameMapType.NorthAmerica:
return northAmerica;
case GameMapType.BlackSea:
return blackSea;
default:
return "";
}
}
render() {
const mapValue = GameMapType[this.mapKey as keyof typeof GameMapType];
return html`
<div class="option-card ${this.selected ? "selected" : ""}">
${this.getMapsImage(mapValue)
? html`<img
src="${this.getMapsImage(mapValue)}"
alt="${this.mapKey}"
class="option-image"
/>`
: html`<div class="option-image">
<p>${this.mapKey}</p>
</div>`}
<div class="option-card-title">
${MapDescription[this.mapKey as keyof typeof GameMapType]}
</div>
</div>
`;
}
}
File diff suppressed because it is too large Load Diff
+8 -8
View File
@@ -29,7 +29,7 @@ export function createRenderer(
canvas: HTMLCanvasElement,
game: GameView,
eventBus: EventBus,
clientID: ClientID
clientID: ClientID,
): GameRenderer {
const transformHandler = new TransformHandler(game, eventBus, canvas);
@@ -65,7 +65,7 @@ export function createRenderer(
controlPanel.game = game;
const eventsDisplay = document.querySelector(
"events-display"
"events-display",
) as EventsDisplay;
if (!(eventsDisplay instanceof EventsDisplay)) {
consolex.error("events display not found");
@@ -75,7 +75,7 @@ export function createRenderer(
eventsDisplay.clientID = clientID;
const playerInfo = document.querySelector(
"player-info-overlay"
"player-info-overlay",
) as PlayerInfoOverlay;
if (!(playerInfo instanceof PlayerInfoOverlay)) {
consolex.error("player info overlay not found");
@@ -129,7 +129,7 @@ export function createRenderer(
buildMenu,
uiState,
playerInfo,
playerPanel
playerPanel,
),
new SpawnTimer(game, transformHandler),
leaderboard,
@@ -147,7 +147,7 @@ export function createRenderer(
canvas,
transformHandler,
uiState,
layers
layers,
);
}
@@ -160,7 +160,7 @@ export class GameRenderer {
private canvas: HTMLCanvasElement,
public transformHandler: TransformHandler,
public uiState: UIState,
private layers: Layer[]
private layers: Layer[],
) {
this.context = canvas.getContext("2d");
}
@@ -183,7 +183,7 @@ export class GameRenderer {
this.transformHandler = new TransformHandler(
this.game,
this.eventBus,
this.canvas
this.canvas,
);
requestAnimationFrame(() => this.renderGame());
@@ -229,7 +229,7 @@ export class GameRenderer {
const duration = performance.now() - start;
if (duration > 50) {
console.warn(
`tick ${this.game.ticks()} took ${duration}ms to render frame`
`tick ${this.game.ticks()} took ${duration}ms to render frame`,
);
}
}
+4 -4
View File
@@ -200,7 +200,7 @@ export class BuildMenu extends LitElement {
public onBuildSelected = (item: BuildItemDisplay) => {
this.eventBus.emit(
new BuildUnitIntentEvent(item.unitType, this.clickedCell)
new BuildUnitIntentEvent(item.unitType, this.clickedCell),
);
this.hideMenu();
};
@@ -235,7 +235,7 @@ export class BuildMenu extends LitElement {
? this.game
.unitInfo(item.unitType)
.cost(this.myPlayer)
: 0
: 0,
)}
<img
src=${goldCoinIcon}
@@ -246,10 +246,10 @@ export class BuildMenu extends LitElement {
/>
</span>
</button>
`
`,
)}
</div>
`
`,
)}
</div>
`;
+3 -3
View File
@@ -10,7 +10,7 @@ const emojiTable: string[][] = [
["😎", "❤️", "💰", "🤝", "🖕"],
["💥", "🆘", "🕊️", "➡️", "⬅️"],
["↙️", "↖️", "↗️", "⬆️", "↘️"],
["⬇️", "❓", "⏳", "☢️", "⚠️"]
["⬇️", "❓", "⏳", "☢️", "⚠️"],
];
@customElement("emoji-table")
@@ -110,10 +110,10 @@ export class EmojiTable extends LitElement {
>
${emoji}
</button>
`
`,
)}
</div>
`
`,
)}
</div>
`;
+90 -21
View File
@@ -1,8 +1,11 @@
import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { EventBus } from "../../../core/EventBus";
import { AllPlayers, MessageType } from "../../../core/game/Game";
import { DisplayMessageUpdate } from "../../../core/game/GameUpdates";
import { AllPlayers, MessageType, PlayerType } from "../../../core/game/Game";
import {
AttackUpdate,
DisplayMessageUpdate,
} from "../../../core/game/GameUpdates";
import { EmojiUpdate } from "../../../core/game/GameUpdates";
import { TargetPlayerUpdate } from "../../../core/game/GameUpdates";
import { AllianceExpiredUpdate } from "../../../core/game/GameUpdates";
@@ -16,6 +19,7 @@ import { SendAllianceReplyIntentEvent } from "../../Transport";
import { unsafeHTML } from "lit/directives/unsafe-html.js";
import { onlyImages, sanitize } from "../../../core/Util";
import { GameView, PlayerView } from "../../../core/game/GameView";
import { renderTroops } from "../../Utils";
interface Event {
description: string;
@@ -38,6 +42,8 @@ export class EventsDisplay extends LitElement implements Layer {
public clientID: ClientID;
private events: Event[] = [];
@state() private incomingAttacks: AttackUpdate[] = [];
@state() private outgoingAttacks: AttackUpdate[] = [];
private updateMap = new Map([
[GameUpdateType.DisplayEvent, (u) => this.onDisplayMessageEvent(u)],
@@ -54,6 +60,8 @@ export class EventsDisplay extends LitElement implements Layer {
constructor() {
super();
this.events = [];
this.incomingAttacks = [];
this.outgoingAttacks = [];
}
init() {}
@@ -85,12 +93,18 @@ export class EventsDisplay extends LitElement implements Layer {
if (!myPlayer) {
return;
}
myPlayer.incomingAttacks().forEach((a) => {
// console.log(`got incoming attack: ${JSON.stringify(a)}`);
});
myPlayer.outgoingAttacks().forEach((a) => {
// console.log(`got outgoing attack: ${JSON.stringify(a)}`);
// Update attacks
this.incomingAttacks = myPlayer.incomingAttacks().filter((a) => {
const t = (this.game.playerBySmallID(a.attackerID) as PlayerView).type();
return t != PlayerType.Bot;
});
this.outgoingAttacks = myPlayer
.outgoingAttacks()
.filter((a) => a.targetID != 0);
this.requestUpdate();
}
private addEvent(event: Event) {
@@ -136,10 +150,10 @@ export class EventsDisplay extends LitElement implements Layer {
}
const requestor = this.game.playerBySmallID(
update.requestorID
update.requestorID,
) as PlayerView;
const recipient = this.game.playerBySmallID(
update.recipientID
update.recipientID,
) as PlayerView;
this.addEvent({
@@ -150,7 +164,7 @@ export class EventsDisplay extends LitElement implements Layer {
className: "btn",
action: () =>
this.eventBus.emit(
new SendAllianceReplyIntentEvent(requestor, recipient, true)
new SendAllianceReplyIntentEvent(requestor, recipient, true),
),
},
{
@@ -158,7 +172,7 @@ export class EventsDisplay extends LitElement implements Layer {
className: "btn-info",
action: () =>
this.eventBus.emit(
new SendAllianceReplyIntentEvent(requestor, recipient, false)
new SendAllianceReplyIntentEvent(requestor, recipient, false),
),
},
],
@@ -167,7 +181,7 @@ export class EventsDisplay extends LitElement implements Layer {
createdAt: this.game.ticks(),
onDelete: () =>
this.eventBus.emit(
new SendAllianceReplyIntentEvent(requestor, recipient, false)
new SendAllianceReplyIntentEvent(requestor, recipient, false),
),
});
}
@@ -179,7 +193,7 @@ export class EventsDisplay extends LitElement implements Layer {
}
const recipient = this.game.playerBySmallID(
update.request.recipientID
update.request.recipientID,
) as PlayerView;
this.addEvent({
@@ -224,8 +238,8 @@ export class EventsDisplay extends LitElement implements Layer {
update.player1ID === myPlayer.smallID()
? update.player2ID
: update.player2ID === myPlayer.smallID()
? update.player1ID
: null;
? update.player1ID
: null;
const other = this.game.playerBySmallID(otherID) as PlayerView;
if (!other || !myPlayer.isAlive() || !other.isAlive()) return;
@@ -261,7 +275,7 @@ export class EventsDisplay extends LitElement implements Layer {
? AllPlayers
: this.game.playerBySmallID(update.emoji.recipientID);
const sender = this.game.playerBySmallID(
update.emoji.senderID
update.emoji.senderID,
) as PlayerView;
if (recipient == myPlayer) {
@@ -300,8 +314,62 @@ export class EventsDisplay extends LitElement implements Layer {
}
}
private renderAttacks() {
if (
this.incomingAttacks.length === 0 &&
this.outgoingAttacks.length === 0
) {
return html``;
}
return html`
${this.incomingAttacks.length > 0
? html`
<tr class="border-t border-gray-700">
<td class="lg:p-3 p-1 text-left text-red-400">
${this.incomingAttacks.map(
(attack) => html`
<div class="ml-2">
${renderTroops(attack.troops)}
${(
this.game.playerBySmallID(
attack.attackerID,
) as PlayerView
)?.name()}
</div>
`,
)}
</td>
</tr>
`
: ""}
${this.outgoingAttacks.length > 0
? html`
<tr class="border-t border-gray-700">
<td class="lg:p-3 p-1 text-left text-blue-400">
${this.outgoingAttacks.map(
(attack) => html`
<div class="ml-2">
${renderTroops(attack.troops)}
${(
this.game.playerBySmallID(attack.targetID) as PlayerView
)?.name()}
</div>
`,
)}
</td>
</tr>
`
: ""}
`;
}
render() {
if (this.events.length === 0) {
if (
this.events.length === 0 &&
this.incomingAttacks.length === 0 &&
this.outgoingAttacks.length === 0
) {
return html``;
}
@@ -317,7 +385,7 @@ export class EventsDisplay extends LitElement implements Layer {
(event, index) => html`
<tr
class="border-b border-opacity-0 ${this.getMessageTypeClasses(
event.type
event.type,
)}"
>
<td class="lg:p-3 p-1 text-left">
@@ -342,15 +410,16 @@ export class EventsDisplay extends LitElement implements Layer {
>
${btn.text}
</button>
`
`,
)}
</div>
`
: ""}
</td>
</tr>
`
`,
)}
${this.renderAttacks()}
</tbody>
</table>
</div>
@@ -358,6 +427,6 @@ export class EventsDisplay extends LitElement implements Layer {
}
createRenderRoot() {
return this; // Required for Tailwind classes to work with Lit
return this;
}
}
+2 -2
View File
@@ -80,7 +80,7 @@ export class Leaderboard extends LitElement implements Layer {
name: myPlayer.displayName(),
position: place,
score: formatPercentage(
myPlayer.numTilesOwned() / this.game.numLandTiles()
myPlayer.numTilesOwned() / this.game.numLandTiles(),
),
gold: renderNumber(myPlayer.gold()),
isMyPlayer: true,
@@ -205,7 +205,7 @@ export class Leaderboard extends LitElement implements Layer {
<td>${player.score}</td>
<td>${player.gold}</td>
</tr>
`
`,
)}
</tbody>
</table>
+42 -24
View File
@@ -26,7 +26,7 @@ class RenderInfo {
public lastRenderCalc: number,
public location: Cell,
public fontSize: number,
public element: HTMLElement
public element: HTMLElement,
) {}
}
@@ -50,7 +50,7 @@ export class NameLayer implements Layer {
private game: GameView,
private theme: Theme,
private transformHandler: TransformHandler,
private clientID: ClientID
private clientID: ClientID,
) {
this.traitorIconImage = new Image();
this.traitorIconImage.src = traitorIcon;
@@ -101,7 +101,13 @@ export class NameLayer implements Layer {
if (!this.seenPlayers.has(player)) {
this.seenPlayers.add(player);
this.renders.push(
new RenderInfo(player, 0, null, 0, this.createPlayerElement(player))
new RenderInfo(
player,
0,
null,
0,
this.createPlayerElement(player),
),
);
}
}
@@ -110,11 +116,11 @@ export class NameLayer implements Layer {
public renderLayer(mainContex: CanvasRenderingContext2D) {
const screenPosOld = this.transformHandler.worldToScreenCoordinates(
new Cell(0, 0)
new Cell(0, 0),
);
const screenPos = new Cell(
screenPosOld.x - window.innerWidth / 2,
screenPosOld.y - window.innerHeight / 2
screenPosOld.y - window.innerHeight / 2,
);
this.container.style.transform = `translate(${screenPos.x}px, ${screenPos.y}px) scale(${this.transformHandler.scale})`;
@@ -131,7 +137,7 @@ export class NameLayer implements Layer {
0,
0,
mainContex.canvas.width,
mainContex.canvas.height
mainContex.canvas.height,
);
}
@@ -143,10 +149,9 @@ export class NameLayer implements Layer {
element.style.alignItems = "center";
element.style.gap = "0px";
if (player.flag()) {
const flagImg = document.createElement("img");
flagImg.classList.add('player-flag');
flagImg.classList.add("player-flag");
flagImg.style.marginBottom = "-5%";
flagImg.style.opacity = '0.8';
flagImg.src = '/flags/' + sanitize(player.flag()) + '.svg';
@@ -157,8 +162,9 @@ export class NameLayer implements Layer {
}
const nameDiv = document.createElement("div");
nameDiv.classList.add('player-name');
nameDiv.innerHTML = (player.type() !== PlayerType.Human ? "🤖 " : '') + player.name();
nameDiv.classList.add("player-name");
nameDiv.innerHTML =
(player.type() !== PlayerType.Human ? "🤖 " : "") + player.name();
nameDiv.style.color = this.theme.playerInfoColor(player.id()).toHex();
nameDiv.style.fontFamily = this.theme.font();
nameDiv.style.whiteSpace = "nowrap";
@@ -167,9 +173,8 @@ export class NameLayer implements Layer {
nameDiv.style.zIndex = "3";
element.appendChild(nameDiv);
const troopsDiv = document.createElement("div");
troopsDiv.classList.add('player-troops');
troopsDiv.classList.add("player-troops");
troopsDiv.textContent = renderTroops(player.troops());
troopsDiv.style.color = this.theme.playerInfoColor(player.id()).toHex();
troopsDiv.style.fontFamily = this.theme.font();
@@ -178,7 +183,7 @@ export class NameLayer implements Layer {
element.appendChild(troopsDiv);
const iconsDiv = document.createElement("div");
iconsDiv.classList.add('player-icons');
iconsDiv.classList.add("player-icons");
iconsDiv.style.display = "flex";
iconsDiv.style.gap = "4px";
iconsDiv.style.justifyContent = "center";
@@ -189,6 +194,9 @@ export class NameLayer implements Layer {
iconsDiv.style.height = "100%";
element.appendChild(iconsDiv);
// Start off invisible so it doesn't flash at 0,0
element.style.display = "none";
this.container.appendChild(element);
return element;
}
@@ -203,7 +211,7 @@ export class NameLayer implements Layer {
const oldLocation = render.location;
render.location = new Cell(
render.player.nameLocation().x,
render.player.nameLocation().y
render.player.nameLocation().y,
);
// Calculate base size and scale
@@ -226,14 +234,20 @@ export class NameLayer implements Layer {
render.lastRenderCalc = now + this.rand.nextInt(0, 100);
// Update text sizes
const nameDiv = render.element.querySelector(".player-name") as HTMLDivElement;
const troopsDiv = render.element.querySelector(".player-troops") as HTMLDivElement;
const nameDiv = render.element.querySelector(
".player-name",
) as HTMLDivElement;
const troopsDiv = render.element.querySelector(
".player-troops",
) as HTMLDivElement;
nameDiv.style.fontSize = `${render.fontSize}px`;
troopsDiv.style.fontSize = `${render.fontSize}px`;
troopsDiv.textContent = renderTroops(render.player.troops());
// Handle icons
const iconsDiv = render.element.querySelector(".player-icons") as HTMLDivElement;
const iconsDiv = render.element.querySelector(
".player-icons",
) as HTMLDivElement;
const iconSize = Math.min(render.fontSize * 1.5, 48);
const myPlayer = this.getPlayer();
@@ -242,7 +256,7 @@ export class NameLayer implements Layer {
if (render.player === this.firstPlace) {
if (!existingCrown) {
iconsDiv.appendChild(
this.createIconElement(this.crownIconImage.src, iconSize, "crown")
this.createIconElement(this.crownIconImage.src, iconSize, "crown"),
);
}
} else if (existingCrown) {
@@ -254,7 +268,11 @@ export class NameLayer implements Layer {
if (render.player.isTraitor()) {
if (!existingTraitor) {
iconsDiv.appendChild(
this.createIconElement(this.traitorIconImage.src, iconSize, "traitor")
this.createIconElement(
this.traitorIconImage.src,
iconSize,
"traitor",
),
);
}
} else if (existingTraitor) {
@@ -269,8 +287,8 @@ export class NameLayer implements Layer {
this.createIconElement(
this.allianceIconImage.src,
iconSize,
"alliance"
)
"alliance",
),
);
}
} else if (existingAlliance) {
@@ -285,7 +303,7 @@ export class NameLayer implements Layer {
) {
if (!existingTarget) {
iconsDiv.appendChild(
this.createIconElement(this.targetIconImage.src, iconSize, "target")
this.createIconElement(this.targetIconImage.src, iconSize, "target"),
);
}
} else if (existingTarget) {
@@ -299,7 +317,7 @@ export class NameLayer implements Layer {
.filter(
(emoji) =>
emoji.recipientID == AllPlayers ||
emoji.recipientID == myPlayer?.smallID()
emoji.recipientID == myPlayer?.smallID(),
);
if (emojis.length > 0) {
@@ -332,7 +350,7 @@ export class NameLayer implements Layer {
private createIconElement(
src: string,
size: number,
id: string
id: string,
): HTMLImageElement {
const icon = document.createElement("img");
icon.src = src;
@@ -22,7 +22,7 @@ import { renderNumber, renderTroops } from "../../Utils";
function euclideanDistWorld(
coord: { x: number; y: number },
tileRef: TileRef,
game: GameView
game: GameView,
): number {
const x = game.x(tileRef);
const y = game.y(tileRef);
@@ -71,7 +71,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
init() {
this.eventBus.on(MouseMoveEvent, (e: MouseMoveEvent) =>
this.onMouseEvent(e)
this.onMouseEvent(e),
);
this._isActive = true;
}
+2 -2
View File
@@ -53,7 +53,7 @@ export class PlayerPanel extends LitElement implements Layer {
private handleAllianceClick(
e: Event,
myPlayer: PlayerView,
other: PlayerView
other: PlayerView,
) {
e.stopPropagation();
this.eventBus.emit(new SendAllianceRequestIntentEvent(myPlayer, other));
@@ -63,7 +63,7 @@ export class PlayerPanel extends LitElement implements Layer {
private handleBreakAllianceClick(
e: Event,
myPlayer: PlayerView,
other: PlayerView
other: PlayerView,
) {
e.stopPropagation();
this.eventBus.emit(new SendBreakAllianceIntentEvent(myPlayer, other));
+11 -11
View File
@@ -99,7 +99,7 @@ export class RadialMenu implements Layer {
private buildMenu: BuildMenu,
private uiState: UIState,
private playerInfoOverlay: PlayerInfoOverlay,
private playerPanel: PlayerPanel
private playerPanel: PlayerPanel,
) {}
init() {
@@ -108,7 +108,7 @@ export class RadialMenu implements Layer {
this.eventBus.on(ShowBuildMenuEvent, (e) => {
const clickedCell = this.transformHandler.screenToWorldCoordinates(
e.x,
e.y
e.y,
);
if (clickedCell == null) {
return;
@@ -144,7 +144,7 @@ export class RadialMenu implements Layer {
.append("g")
.attr(
"transform",
`translate(${this.menuSize / 2},${this.menuSize / 2})`
`translate(${this.menuSize / 2},${this.menuSize / 2})`,
);
const pie = d3
@@ -169,7 +169,7 @@ export class RadialMenu implements Layer {
.append("path")
.attr("d", arc)
.attr("fill", (d) =>
d.data.disabled ? this.disabledColor : d.data.color
d.data.disabled ? this.disabledColor : d.data.color,
)
.attr("stroke", "#ffffff")
.attr("stroke-width", "2")
@@ -294,7 +294,7 @@ export class RadialMenu implements Layer {
this.clickedCell = this.transformHandler.screenToWorldCoordinates(
event.x,
event.y
event.y,
);
if (!this.g.isValidCoord(this.clickedCell.x, this.clickedCell.y)) {
return;
@@ -323,7 +323,7 @@ export class RadialMenu implements Layer {
private handlePlayerActions(
myPlayer: PlayerView,
actions: PlayerActions,
tile: TileRef
tile: TileRef,
) {
this.activateMenuElement(Slot.Build, "#ebe250", buildIcon, () => {
this.buildMenu.showMenu(myPlayer, this.clickedCell);
@@ -341,8 +341,8 @@ export class RadialMenu implements Layer {
new SendBoatAttackIntentEvent(
this.g.owner(tile).id(),
this.clickedCell,
this.uiState.attackRatio * myPlayer.troops()
)
this.uiState.attackRatio * myPlayer.troops(),
),
);
});
}
@@ -394,8 +394,8 @@ export class RadialMenu implements Layer {
this.eventBus.emit(
new SendAttackIntentEvent(
this.g.owner(clicked).id(),
this.uiState.attackRatio * myPlayer.troops()
)
this.uiState.attackRatio * myPlayer.troops(),
),
);
}
}
@@ -406,7 +406,7 @@ export class RadialMenu implements Layer {
slot: Slot,
color: string,
icon: string,
action: () => void
action: () => void,
) {
const menuItem = this.menuItems.get(slot);
menuItem.action = action;
+16 -13
View File
@@ -50,7 +50,10 @@ export class StructureLayer implements Layer {
},
};
constructor(private game: GameView, private eventBus: EventBus) {
constructor(
private game: GameView,
private eventBus: EventBus,
) {
this.theme = game.config().theme();
this.loadIconData();
}
@@ -72,11 +75,11 @@ export class StructureLayer implements Layer {
0,
0,
tempCanvas.width,
tempCanvas.height
tempCanvas.height,
);
this.unitIcons.set(unitType, iconData);
console.log(
`icond data width height: ${iconData.width}, ${iconData.height}`
`icond data width height: ${iconData.width}, ${iconData.height}`,
);
};
});
@@ -89,9 +92,9 @@ export class StructureLayer implements Layer {
tick() {
this.game
.updatesSinceLastTick()
[GameUpdateType.Unit].forEach((u) =>
this.handleUnitRendering(this.game.unit(u.id))
);
[
GameUpdateType.Unit
].forEach((u) => this.handleUnitRendering(this.game.unit(u.id)));
}
init() {
@@ -113,7 +116,7 @@ export class StructureLayer implements Layer {
-this.game.width() / 2,
-this.game.height() / 2,
this.game.width(),
this.game.height()
this.game.height(),
);
}
@@ -133,7 +136,7 @@ export class StructureLayer implements Layer {
// Clear previous rendering
for (const tile of this.game.bfs(
unit.tile(),
euclDistFN(unit.tile(), config.borderRadius)
euclDistFN(unit.tile(), config.borderRadius),
)) {
this.clearCell(new Cell(this.game.x(tile), this.game.y(tile)));
}
@@ -145,27 +148,27 @@ export class StructureLayer implements Layer {
// Draw border and territory
for (const tile of this.game.bfs(
unit.tile(),
euclDistFN(unit.tile(), config.borderRadius)
euclDistFN(unit.tile(), config.borderRadius),
)) {
this.paintCell(
new Cell(this.game.x(tile), this.game.y(tile)),
unit.type() == UnitType.Construction
? underConstructionColor
: this.theme.borderColor(unit.owner().info()),
255
255,
);
}
for (const tile of this.game.bfs(
unit.tile(),
euclDistFN(unit.tile(), config.territoryRadius)
euclDistFN(unit.tile(), config.territoryRadius),
)) {
this.paintCell(
new Cell(this.game.x(tile), this.game.y(tile)),
unit.type() == UnitType.Construction
? underConstructionColor
: this.theme.territoryColor(unit.owner().info()),
130
130,
);
}
@@ -181,7 +184,7 @@ export class StructureLayer implements Layer {
startY: number,
width: number,
height: number,
unit: UnitView
unit: UnitView,
) {
let color = this.theme.borderColor(unit.owner().info());
if (unit.type() == UnitType.Construction) {
+14 -11
View File
@@ -50,7 +50,10 @@ export class TerritoryLayer implements Layer {
private refreshRate = 50;
private lastRefresh = 0;
constructor(private game: GameView, private eventBus: EventBus) {
constructor(
private game: GameView,
private eventBus: EventBus,
) {
this.theme = game.config().theme();
}
@@ -67,7 +70,7 @@ export class TerritoryLayer implements Layer {
this.game
.bfs(
tile,
manhattanDistFN(tile, this.game.config().defensePostRange())
manhattanDistFN(tile, this.game.config().defensePostRange()),
)
.forEach((t) => {
if (
@@ -91,7 +94,7 @@ export class TerritoryLayer implements Layer {
0,
0,
this.game.width(),
this.game.height()
this.game.height(),
);
const humans = this.game
.playerViews()
@@ -111,7 +114,7 @@ export class TerritoryLayer implements Layer {
this.paintHighlightCell(
new Cell(this.game.x(tile), this.game.y(tile)),
this.theme.spawnHighlightColor(),
255
255,
);
}
}
@@ -138,7 +141,7 @@ export class TerritoryLayer implements Layer {
0,
0,
this.game.width(),
this.game.height()
this.game.height(),
);
this.initImageData();
this.canvas.width = this.game.width();
@@ -185,7 +188,7 @@ export class TerritoryLayer implements Layer {
-this.game.width() / 2,
-this.game.height() / 2,
this.game.width(),
this.game.height()
this.game.height(),
);
if (this.game.inSpawnPhase()) {
context.drawImage(
@@ -193,7 +196,7 @@ export class TerritoryLayer implements Layer {
-this.game.width() / 2,
-this.game.height() / 2,
this.game.width(),
this.game.height()
this.game.height(),
);
}
}
@@ -224,7 +227,7 @@ export class TerritoryLayer implements Layer {
this.game.x(tile),
this.game.y(tile),
this.theme.falloutColor(),
150
150,
);
return;
}
@@ -241,14 +244,14 @@ export class TerritoryLayer implements Layer {
this.game.x(tile),
this.game.y(tile),
this.theme.defendedBorderColor(owner.info()),
255
255,
);
} else {
this.paintCell(
this.game.x(tile),
this.game.y(tile),
this.theme.borderColor(owner.info()),
255
255,
);
}
} else {
@@ -256,7 +259,7 @@ export class TerritoryLayer implements Layer {
this.game.x(tile),
this.game.y(tile),
this.theme.territoryColor(owner.info()),
150
150,
);
}
}
+4 -2
View File
@@ -26,16 +26,19 @@ export class TopBar extends LitElement implements Layer {
if (!this.isVisible) {
return html``;
}
const myPlayer = this.game?.myPlayer();
if (!myPlayer?.isAlive() || this.game?.inSpawnPhase()) {
return html``;
}
const popRate = this.game.config().populationIncreaseRate(myPlayer) * 10;
const maxPop = this.game.config().maxPopulation(myPlayer);
const goldPerSecond = this.game.config().goldAdditionRate(myPlayer) * 10;
return html`
<div
class="fixed top-0 z-50 bg-black/90 text-white text-sm p-1 rounded grid grid-cols-1 sm:grid-cols-2 w-1/2 sm:w-2/3 md:w-1/2 lg:hidden"
class="fixed top-0 z-50 bg-gray-800/70 text-white text-sm p-1 rounded grid grid-cols-1 sm:grid-cols-2 w-1/2 sm:w-2/3 md:w-1/2 lg:hidden backdrop-blur"
>
<!-- Pop section (takes 2 columns on desktop) -->
<div
@@ -48,7 +51,6 @@ export class TopBar extends LitElement implements Layer {
>
<span>(+${renderTroops(popRate)})</span>
</div>
<!-- Gold section (takes 1 column on desktop) -->
<div
class="flex items-center space-x-2 overflow-x-auto whitespace-nowrap"
+24 -24
View File
@@ -36,7 +36,7 @@ export class UnitLayer implements Layer {
constructor(
private game: GameView,
private eventBus: EventBus,
private clientID: ClientID
private clientID: ClientID,
) {
this.theme = game.config().theme();
}
@@ -65,7 +65,7 @@ export class UnitLayer implements Layer {
-this.game.width() / 2,
-this.game.height() / 2,
this.game.width(),
this.game.height()
this.game.height(),
);
}
@@ -131,7 +131,7 @@ export class UnitLayer implements Layer {
// Clear previous area
for (const t of this.game.bfs(
unit.lastTile(),
euclDistFN(unit.lastTile(), 6)
euclDistFN(unit.lastTile(), 6),
)) {
this.clearCell(this.game.x(t), this.game.y(t));
}
@@ -147,21 +147,21 @@ export class UnitLayer implements Layer {
this.game.y(t),
rel,
this.theme.territoryColor(unit.owner().info()),
255
255,
);
}
// Paint border
for (const t of this.game.bfs(
unit.tile(),
manhattanDistFN(unit.tile(), 4)
manhattanDistFN(unit.tile(), 4),
)) {
this.paintCell(
this.game.x(t),
this.game.y(t),
rel,
this.theme.borderColor(unit.owner().info()),
255
255,
);
}
@@ -172,7 +172,7 @@ export class UnitLayer implements Layer {
this.game.y(t),
rel,
this.theme.territoryColor(unit.owner().info()),
255
255,
);
}
}
@@ -198,14 +198,14 @@ export class UnitLayer implements Layer {
this.game.y(unit.tile()),
rel,
this.theme.borderColor(unit.owner().info()),
255
255,
);
this.paintCell(
this.game.x(unit.lastTile()),
this.game.y(unit.lastTile()),
rel,
this.theme.borderColor(unit.owner().info()),
255
255,
);
}
@@ -215,7 +215,7 @@ export class UnitLayer implements Layer {
// Clear previous area
for (const t of this.game.bfs(
unit.lastTile(),
euclDistFN(unit.lastTile(), 2)
euclDistFN(unit.lastTile(), 2),
)) {
this.clearCell(this.game.x(t), this.game.y(t));
}
@@ -228,7 +228,7 @@ export class UnitLayer implements Layer {
this.game.y(t),
rel,
this.theme.borderColor(unit.owner().info()),
255
255,
);
}
}
@@ -246,7 +246,7 @@ export class UnitLayer implements Layer {
this.game.y(unit.tile()),
rel,
this.theme.borderColor(unit.owner().info()),
255
255,
);
}
}
@@ -257,7 +257,7 @@ export class UnitLayer implements Layer {
// Clear previous area
for (const t of this.game.bfs(
unit.lastTile(),
euclDistFN(unit.lastTile(), 3)
euclDistFN(unit.lastTile(), 3),
)) {
this.clearCell(this.game.x(t), this.game.y(t));
}
@@ -266,28 +266,28 @@ export class UnitLayer implements Layer {
// Paint territory
for (const t of this.game.bfs(
unit.tile(),
manhattanDistFN(unit.tile(), 2)
manhattanDistFN(unit.tile(), 2),
)) {
this.paintCell(
this.game.x(t),
this.game.y(t),
rel,
this.theme.territoryColor(unit.owner().info()),
255
255,
);
}
// Paint border
for (const t of this.game.bfs(
unit.tile(),
manhattanDistFN(unit.tile(), 1)
manhattanDistFN(unit.tile(), 1),
)) {
this.paintCell(
this.game.x(t),
this.game.y(t),
rel,
this.theme.borderColor(unit.owner().info()),
255
255,
);
}
}
@@ -305,7 +305,7 @@ export class UnitLayer implements Layer {
// Clear previous area
for (const t of this.game.bfs(
unit.lastTile(),
manhattanDistFN(unit.lastTile(), 3)
manhattanDistFN(unit.lastTile(), 3),
)) {
this.clearCell(this.game.x(t), this.game.y(t));
}
@@ -318,35 +318,35 @@ export class UnitLayer implements Layer {
this.game.y(t),
rel,
this.theme.territoryColor(unit.owner().info()),
150
150,
);
}
// Paint border
for (const t of this.game.bfs(
unit.tile(),
manhattanDistFN(unit.tile(), 2)
manhattanDistFN(unit.tile(), 2),
)) {
this.paintCell(
this.game.x(t),
this.game.y(t),
rel,
this.theme.borderColor(unit.owner().info()),
255
255,
);
}
// Paint territory
for (const t of this.game.bfs(
unit.tile(),
manhattanDistFN(unit.tile(), 1)
manhattanDistFN(unit.tile(), 1),
)) {
this.paintCell(
this.game.x(t),
this.game.y(t),
rel,
this.theme.territoryColor(unit.owner().info()),
255
255,
);
}
} else {
@@ -362,7 +362,7 @@ export class UnitLayer implements Layer {
y: number,
relationship: Relationship,
color: Colord,
alpha: number
alpha: number,
) {
this.clearCell(x, y);
if (this.alternateView) {
+270 -162
View File
@@ -1,178 +1,286 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en" class="h-full">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>OpenFront (ALPHA)</title>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>OpenFront (ALPHA)</title>
<!-- Critical CSS to prevent FOUC -->
<style>
.preload * {
-webkit-transition: none !important;
-moz-transition: none !important;
-ms-transition: none !important;
-o-transition: none !important;
transition: none !important;
}
<!-- Preload critical assets -->
<link rel="preload" href="../../resources/images/OpenFrontLogo.svg" as="image" />
<link rel="preload" href="../../resources/images/DiscordIcon.svg" as="image" />
html {
visibility: visible;
opacity: 1;
}
<!-- Critical CSS to prevent FOUC -->
<style>
.preload * {
-webkit-transition: none !important;
-moz-transition: none !important;
-ms-transition: none !important;
-o-transition: none !important;
transition: none !important;
}
html.preload {
visibility: hidden;
opacity: 0;
}
html {
visibility: visible;
opacity: 1;
}
body::before {
content: "";
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
z-index: -1;
}
html.preload {
visibility: hidden;
opacity: 0;
}
/* Critical styles to prevent layout shift */
.container {
opacity: 1;
transition: opacity 0.3s ease-in-out;
}
body::before {
content: "";
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
z-index: -1;
}
.logo-glow {
fill: url(#logo-gradient);
filter: drop-shadow(1px 1px 0px rgb(255, 255, 255))
drop-shadow(-1px -1px 0px rgb(255, 255, 255))
drop-shadow(1px -1px 0px rgb(255, 255, 255))
drop-shadow(-1px 1px 0px rgb(255, 255, 255))
drop-shadow(3px 3px 0px rgb(255, 255, 255));
}
.logo-version {
color: #2563eb;
filter: drop-shadow(1px 1px 0px rgb(255, 255, 255))
drop-shadow(-1px -1px 0px rgb(255, 255, 255))
drop-shadow(1px -1px 0px rgb(255, 255, 255))
drop-shadow(-1px 1px 0px rgb(255, 255, 255));
}
</style>
/* Critical styles to prevent layout shift */
.container {
opacity: 1;
transition: opacity 0.3s ease-in-out;
}
</style>
<!-- Immediate execution to prevent FOUC -->
<script>
document.documentElement.className = "preload";
</script>
<!-- Immediate execution to prevent FOUC -->
<script>
document.documentElement.className = "preload";
</script>
<!-- Analytics -->
<script
async
src="https://www.googletagmanager.com/gtag/js?id=AW-16702609763"
></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag("js", new Date());
gtag("config", "AW-16702609763");
</script>
<script
async
src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-7035513310742290"
crossorigin="anonymous"
></script>
</head>
<!-- Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=AW-16702609763"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag("js", new Date());
gtag("config", "AW-16702609763");
</script>
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-7035513310742290"
crossorigin="anonymous"></script>
</head>
<body
class="h-full select-none font-sans min-h-screen bg-opacity-0 bg-cover bg-center bg-fixed transition-opacity duration-300 ease-in-out flex flex-col">
<!-- Main container with responsive padding -->
<!-- Logo section remains the same -->
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-4 sm:py-6 lg:py-8 flex-grow">
<div class="flex justify-center">
<img src="../../resources/images/OpenFrontLogo.png" alt="OpenFront.io" />
</div>
<div class="flex justify-center text-sm font-bold mt-[-10px] pb-6 md:pb-12">
v0.15.0
</div>
<div class="relative items-center max-w-sm sm:max-w-md lg:max-w-lg xl:max-w-xl mx-auto p-2 pb-4">
<flag-input></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 p-2">
<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 id="host-lobby-button"
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 id="join-private-lobby-button"
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>
</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"
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>
</div>
<!-- Game components -->
<div id="customMenu" class="mt-4 sm:mt-6 lg:mt-8">
<ul></ul>
</div>
<div id="app"></div>
<div id="radialMenu" class="radial-menu"></div>
<!-- Game modals and overlays -->
<single-player-modal></single-player-modal>
<host-lobby-modal></host-lobby-modal>
<join-private-lobby-modal></join-private-lobby-modal>
<emoji-table></emoji-table>
<leader-board></leader-board>
<build-menu></build-menu>
<win-modal></win-modal>
<top-bar></top-bar>
<player-panel></player-panel>
<div class="fixed right-0 top-0 z-50 flex flex-col w-32 sm:w-32 lg:w-48">
<options-menu></options-menu>
<player-info-overlay></player-info-overlay>
</div>
<div class="bottom-0 w-full flex-col-reverse sm:flex-row z-50" style="position: fixed; pointer-events: none">
<div class="w-full sm:w-2/3 sm:fixed sm:right-0 sm:bottom-0 sm:flex justify-end" style="pointer-events: auto">
<events-display></events-display>
</div>
<div class="w-full sm:w-1/3" style="pointer-events: auto">
<control-panel></control-panel>
</div>
</div>
<!-- Footer section -->
<div class="w-full bg-gray-900/80 backdrop-blur-md py-4">
<body
class="h-full select-none font-sans min-h-screen bg-opacity-0 bg-cover bg-center bg-fixed transition-opacity duration-300 ease-in-out flex flex-col"
>
<!-- Logo section remains the same -->
<div
class="max-w-7xl mx-auto px-4 flex justify-between items-center text-white sm:flex-row flex-col sm:gap-0 gap-4">
<div class="flex sm:flex-row flex-col sm:gap-8 gap-2">
<a href="https://youtu.be/jvHEvbko3uw?si=znspkP84P76B1w5I"
class="text-white/70 hover:text-white transition-colors duration-300" target="_blank">How to Play</a>
<a href="https://discord.gg/k22YrnAzGp" class="text-white/70 hover:text-white transition-colors duration-300"
target="_blank">Discord</a>
<a href="https://openfront.fandom.com/wiki/Openfront_Wiki" class="text-white/70 hover:text-white transition-colors duration-300"
target="_blank">Wiki</a>
</div>
<div class="text-white/70">
© 2025
<a href="https://github.com/openfrontio/OpenFrontIO" class="hover:text-white transition-colors duration-300"
target="_blank">OpenFront.io</a>.
class="flex justify-center items-center flex-col w-full bg-gray-900/80 backdrop-blur-md pt-8 pb-2 px-8"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1364 259"
width="100%"
height="100%"
fill="currentColor"
class="max-w-[450px] h-full logo-glow"
>
<defs>
<linearGradient id="logo-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color: #2563eb" />
<stop offset="100%" style="stop-color: #3b82f6" />
</linearGradient>
</defs>
<g>
<path
d="M0,174V51h15.24v-17.14h16.81v-16.98h16.96V0h1266v17.23h17.13v16.81h16.98v16.96h14.88v123h-15.13v17.08h-17.08v17.08h-16.9v17.04H324.9v16.86h-16.9v16.95h-102v-17.12h-17.07v-17.05H48.73v-17.05h-16.89v-16.89H14.94v-16.89H0ZM1297.95,17.35H65.9v16.7h-17.08v17.08h-14.5v123.08h14.85v16.9h17.08v17.08h139.9v17.08h17.08v16.36h67.9v-16.72h17.08v-17.07h989.88v-17.07h17.08v-16.9h14.44V50.8h-14.75v-17.08h-16.9v-16.37Z"
/>
<path
d="M189.1,154.78v17.07h-16.9v16.75h-51.07v-16.42h-16.9v-17.07h-16.97v-84.88h16.63v-17.07h16.9v-16.84h51.07v16.5h17.07v17.07h16.7v84.89h-16.54ZM137.87,53.1v17.15h-16.6v84.86h16.97v16.61h16.89v-16.97h16.6v-84.86h-16.97v-16.79h-16.89Z"
/>
<path
d="M273.91,104.06v-16.73h50.92v16.45h16.85v68.05h-16.44v17.06h-50.96v16.88h16.4v16.96h-67.28v-16.61h16.33v-101.86h-16.38v-16.98h33.4v16.63c6.12,0,11.72,0,17.31,0,0,22.56,0,45.13,0,67.75h33.59v-67.61h-33.73Z"
/>
<path
d="M631.12,188.64v-16.36h16.53V53.2h-16.25v-16.86h118.33v33.29h-16.65v-16.36h-50.72v50.44h33.36v-16.35h16.99v50.25h-16.6v-16.33h-33.73v50.65h16.37v16.72h-67.63Z"
/>
<path
d="M596.78,103.8v84.94h-33.54v-84.39h-34.03v84.25h-33.85v-101.29h84.5v16.49h16.93Z"
/>
<path
d="M1107.12,188.71v-84.34h-34.03v84.37h-33.7v-101.41h84.42v16.41h16.86v84.96h-33.54Z"
/>
<path
d="M988.1,171.78v16.87h-67.88v-16.38h-16.87v-68.06h16.38v-16.87h68.06v16.38h16.87v68.06h-16.55ZM970.78,104.35h-33.39v67.38h33.39v-67.38Z"
/>
<path
d="M460.77,155.38v16.49h-16.58v16.83h-68.05v-16.5h-16.83v-68.05h16.49v-16.83h68.05v16.49h16.83v34.06h-67.31v33.82h33.47v-16.31h33.92ZM393.39,104.18v16.56h33.3v-16.56h-33.3Z"
/>
<path
d="M1209.13,172h-16.9v-67.9h-16.96v-16.9h16.68v-17.08h16.9v-16.82h16.9v33.58h50.98v16.91h-50.4v67.96h16.48v-16.43h50.95v16.54h-16.55v16.76h-68.08v-16.6Z"
/>
<path
d="M834.91,120.94v16.96h-16.65v33.88h16.41v16.96h-67.29v-16.63h16.34v-67.87h-16.4v-16.97h50.42v33.81h17.3l-.14-.14Z"
/>
<path
d="M835.05,121.08v-33.75h33.76v16.43h16.85v33.96h-33.43v-16.79c-6.13,0-11.73,0-17.32,0,0,0,.14.14.14.14Z"
/>
</g>
</svg>
<div class="flex justify-center text-sm font-bold mt-[-5px] logo-version">
v0.15.0
</div>
</div>
</div>
<!-- Scripts -->
<script>
// Remove preload class after everything is loaded
window.addEventListener("load", function () {
requestAnimationFrame(() => {
document.documentElement.classList.remove("preload");
<!-- 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="relative items-center max-w-sm sm:max-w-md lg:max-w-lg xl:max-w-xl mx-auto p-2 pb-4"
>
<flag-input></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 p-2">
<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
id="host-lobby-button"
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
id="join-private-lobby-button"
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>
</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"
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>
</div>
</div>
<!-- Game components -->
<div id="customMenu" class="mt-4 sm:mt-6 lg:mt-8">
<ul></ul>
</div>
<div id="app"></div>
<div id="radialMenu" class="radial-menu"></div>
<!-- Game modals and overlays -->
<single-player-modal></single-player-modal>
<host-lobby-modal></host-lobby-modal>
<join-private-lobby-modal></join-private-lobby-modal>
<emoji-table></emoji-table>
<leader-board></leader-board>
<build-menu></build-menu>
<win-modal></win-modal>
<top-bar></top-bar>
<player-panel></player-panel>
<div class="fixed right-0 top-0 z-50 flex flex-col w-32 sm:w-32 lg:w-48">
<options-menu></options-menu>
<player-info-overlay></player-info-overlay>
</div>
<div
class="bottom-0 w-full flex-col-reverse sm:flex-row z-50"
style="position: fixed; pointer-events: none"
>
<div
class="w-full sm:w-2/3 sm:fixed sm:right-0 sm:bottom-0 sm:flex justify-end"
style="pointer-events: auto"
>
<events-display></events-display>
</div>
<div class="w-full sm:w-1/3 md:max-w-72" style="pointer-events: auto">
<control-panel></control-panel>
</div>
</div>
<!-- Footer section -->
<div class="w-full bg-gray-900/80 backdrop-blur-md py-4">
<div
class="max-w-7xl mx-auto px-4 flex justify-between items-center text-white sm:flex-row flex-col sm:gap-0 gap-4"
>
<div class="flex sm:flex-row flex-col sm:gap-8 gap-2">
<a
href="https://youtu.be/jvHEvbko3uw?si=znspkP84P76B1w5I"
class="text-white/70 hover:text-white transition-colors duration-300"
target="_blank"
>How to Play</a
>
<a
href="https://discord.gg/k22YrnAzGp"
class="text-white/70 hover:text-white transition-colors duration-300"
target="_blank"
>Discord</a
>
<a
href="https://openfront.fandom.com/wiki/Openfront_Wiki"
class="text-white/70 hover:text-white transition-colors duration-300"
target="_blank"
>Wiki</a
>
</div>
<div class="text-white/70">
© 2025
<a
href="https://github.com/openfrontio/OpenFrontIO"
class="hover:text-white transition-colors duration-300"
target="_blank"
>OpenFront.io</a
>.
</div>
</div>
</div>
<!-- Scripts -->
<script>
// Remove preload class after everything is loaded
window.addEventListener("load", function () {
requestAnimationFrame(() => {
document.documentElement.classList.remove("preload");
});
});
});
</script>
</script>
<!-- Analytics -->
<script defer src="https://static.cloudflareinsights.com/beacon.min.js"
data-cf-beacon='{"token": "03d93e6fefb349c28ee69b408fa25a13"}'></script>
</body>
</html>
<!-- Analytics -->
<script
defer
src="https://static.cloudflareinsights.com/beacon.min.js"
data-cf-beacon='{"token": "03d93e6fefb349c28ee69b408fa25a13"}'
></script>
</body>
</html>