mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-02 02:03:38 +00:00
Merge branch 'main' into randmap
This commit is contained in:
@@ -6,11 +6,16 @@ import { ClientID, GameConfig, GameID, ServerMessage } from "../core/Schemas";
|
||||
import { loadTerrainMap } from "../core/game/TerrainMapLoader";
|
||||
import {
|
||||
SendAttackIntentEvent,
|
||||
SendHashEvent,
|
||||
SendSpawnIntentEvent,
|
||||
Transport,
|
||||
} from "./Transport";
|
||||
import { createCanvas } from "./Utils";
|
||||
import { ErrorUpdate } from "../core/game/GameUpdates";
|
||||
import {
|
||||
ErrorUpdate,
|
||||
GameUpdateType,
|
||||
HashUpdate,
|
||||
} from "../core/game/GameUpdates";
|
||||
import { WorkerClient } from "../core/worker/WorkerClient";
|
||||
import { consolex, initRemoteSender } from "../core/Consolex";
|
||||
import { getConfig, getServerConfig } from "../core/configuration/Config";
|
||||
@@ -171,6 +176,9 @@ export class ClientGameRunner {
|
||||
showErrorModal(gu.errMsg, gu.stack, this.clientID);
|
||||
return;
|
||||
}
|
||||
gu.updates[GameUpdateType.Hash].forEach((hu: HashUpdate) => {
|
||||
this.eventBus.emit(new SendHashEvent(hu.tick, hu.hash));
|
||||
});
|
||||
this.gameView.update(gu);
|
||||
this.renderer.tick();
|
||||
});
|
||||
@@ -205,6 +213,13 @@ export class ClientGameRunner {
|
||||
this.turnsSeen++;
|
||||
}
|
||||
}
|
||||
if (message.type == "desync") {
|
||||
showErrorModal(
|
||||
`desync from server: ${JSON.stringify(message)}`,
|
||||
"",
|
||||
this.clientID,
|
||||
);
|
||||
}
|
||||
if (message.type == "turn") {
|
||||
if (!this.hasJoined) {
|
||||
this.transport.joinGame(0);
|
||||
|
||||
+10
-7
@@ -1,8 +1,7 @@
|
||||
import { LitElement, html, css } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import Countries from "./data/countries.json";
|
||||
import { UserSettings } from "../core/game/UserSettings";
|
||||
|
||||
import { ModalOverlay } from "./components/ModalOverlay";
|
||||
const flagKey: string = "flag";
|
||||
|
||||
@customElement("flag-input")
|
||||
@@ -10,7 +9,6 @@ export class FlagInput extends LitElement {
|
||||
@state() private flag: string = "";
|
||||
@state() private search: string = "";
|
||||
@state() private showModal: boolean = false;
|
||||
private userSettings: UserSettings = new UserSettings();
|
||||
|
||||
static styles = css`
|
||||
@media (max-width: 768px) {
|
||||
@@ -29,11 +27,10 @@ export class FlagInput extends LitElement {
|
||||
}
|
||||
|
||||
private setFlag(flag: string) {
|
||||
if (flag == "") {
|
||||
this.flag = "";
|
||||
} else {
|
||||
this.flag = flag;
|
||||
if (flag == "xx") {
|
||||
flag = "";
|
||||
}
|
||||
this.flag = flag;
|
||||
this.showModal = false;
|
||||
this.storeFlag(flag);
|
||||
}
|
||||
@@ -80,6 +77,12 @@ export class FlagInput extends LitElement {
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div
|
||||
class="absolute left-0 top-0 w-full h-full ${this.showModal
|
||||
? ""
|
||||
: "hidden"}"
|
||||
@click=${() => (this.showModal = false)}
|
||||
></div>
|
||||
<div class="flex relative">
|
||||
<button
|
||||
@click=${() => (this.showModal = !this.showModal)}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { LitElement, html, css } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
|
||||
@customElement("google-ad")
|
||||
export class GoogleAdElement extends LitElement {
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.google-ad-container {
|
||||
margin-top: 1rem;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dark .google-ad-container {
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
`;
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="google-ad-container">
|
||||
<ins
|
||||
class="adsbygoogle"
|
||||
style="display:block"
|
||||
data-ad-client="ca-pub-7035513310742290"
|
||||
data-ad-slot="rightsidebar"
|
||||
data-ad-format="auto"
|
||||
data-full-width-responsive="true"
|
||||
></ins>
|
||||
<script>
|
||||
(adsbygoogle = window.adsbygoogle || []).push({});
|
||||
</script>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
+24
-4
@@ -1,5 +1,5 @@
|
||||
import { LitElement, html, css } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import "./components/Difficulties";
|
||||
import "./components/Maps";
|
||||
|
||||
@@ -207,11 +207,17 @@ export class HelpModal extends LitElement {
|
||||
class="modal-overlay"
|
||||
style="display: ${this.isModalOpen ? "flex" : "none"}"
|
||||
>
|
||||
<div
|
||||
class="absolute left-0 top-0 w-full h-full ${
|
||||
this.isModalOpen ? "" : "hidden"
|
||||
}"
|
||||
@click=${this.close}
|
||||
></div>
|
||||
<div class="modal-content">
|
||||
<span class="close" @click=${this.close}>×</span>
|
||||
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="text-center text-2xl font-bold mb-4">Keybinds</div>
|
||||
<div class="text-center text-2xl font-bold mb-4">Hotkeys</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -220,10 +226,18 @@ export class HelpModal extends LitElement {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="text-left">
|
||||
<tr>
|
||||
<td>CTRL + Left Click</td>
|
||||
<td>Open build menu</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Space</td>
|
||||
<td>Alternate view</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>C</td>
|
||||
<td>Center camera on player</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Q / E</td>
|
||||
<td>Zoom out/in</td>
|
||||
@@ -236,6 +250,10 @@ export class HelpModal extends LitElement {
|
||||
<td>1 / 2</td>
|
||||
<td>Decrease/Increase attack ratio</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Shift + scroll down / scroll up</td>
|
||||
<td>Decrease/Increase attack ratio</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ALT + R</td>
|
||||
<td>Reset graphics</td>
|
||||
@@ -303,7 +321,8 @@ export class HelpModal extends LitElement {
|
||||
<p class="mb-4">Right clicking (or touch on mobile) opens the radial menu. From there you can:</p>
|
||||
<ul>
|
||||
<li class="mb-4"><div class="inline-block icon build-icon"></div> - Open the build menu.</li>
|
||||
<li class="mb-4"><div class="inline-block icon info-icon"></div> - Open the Info menu.</li>
|
||||
<li class="mb-4">
|
||||
<img src="/images/InfoIcon.svg" class="inline-block icon" style="fill: white; background: transparent;"/> - Open the Info menu.</li>
|
||||
<li class="mb-4"><div class="inline-block icon boat-icon"></div> - Send a boat to attack at the selected location (only available if you have access to water).</li>
|
||||
<li class="mb-4"><div class="inline-block icon cancel-icon"></div> - Close the menu.</li>
|
||||
</ul>
|
||||
@@ -455,5 +474,6 @@ export class HelpModal extends LitElement {
|
||||
|
||||
public close() {
|
||||
this.isModalOpen = false;
|
||||
console.log("closing modal");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { LitElement, html, css } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { Difficulty, GameMapType, GameType } from "../core/game/Game";
|
||||
import { Lobby } from "../core/Schemas";
|
||||
import { GameConfig, GameInfo } from "../core/Schemas";
|
||||
import { consolex } from "../core/Consolex";
|
||||
import "./components/Difficulties";
|
||||
import { DifficultyDescription } from "./components/Difficulties";
|
||||
import "./components/Maps";
|
||||
import { generateID } from "../core/Util";
|
||||
import { getConfig, getServerConfig } from "../core/configuration/Config";
|
||||
import randomMap from "../../resources/images/RandomMap.png";
|
||||
|
||||
|
||||
@customElement("host-lobby-modal")
|
||||
export class HostLobbyModal extends LitElement {
|
||||
@state() private isModalOpen = false;
|
||||
@@ -323,6 +326,11 @@ export class HostLobbyModal extends LitElement {
|
||||
class="modal-overlay"
|
||||
style="display: ${this.isModalOpen ? "flex" : "none"}"
|
||||
>
|
||||
<div
|
||||
style="position: absolute; left: 0px; top: 0px; width: 100%; height: 100%;"
|
||||
class="${this.isModalOpen ? "" : "hidden"}"
|
||||
@click=${this.close}
|
||||
></div>
|
||||
<div class="modal-content">
|
||||
<span class="close" @click=${this.close}>×</span>
|
||||
|
||||
@@ -430,7 +438,7 @@ export class HostLobbyModal extends LitElement {
|
||||
step="1"
|
||||
@input=${this.handleBotsChange}
|
||||
@change=${this.handleBotsChange}
|
||||
.value="${this.bots}"
|
||||
.value="${String(this.bots)}"
|
||||
/>
|
||||
<div class="option-card-title">
|
||||
Bots: ${this.bots == 0 ? "Disabled" : this.bots}
|
||||
@@ -526,7 +534,7 @@ export class HostLobbyModal extends LitElement {
|
||||
public open() {
|
||||
createLobby()
|
||||
.then((lobby) => {
|
||||
this.lobbyId = lobby.id;
|
||||
this.lobbyId = lobby.gameID;
|
||||
// join lobby
|
||||
})
|
||||
.then(() => {
|
||||
@@ -535,7 +543,7 @@ export class HostLobbyModal extends LitElement {
|
||||
detail: {
|
||||
gameType: GameType.Private,
|
||||
lobby: {
|
||||
id: this.lobbyId,
|
||||
gameID: this.lobbyId,
|
||||
},
|
||||
map: this.selectedMap,
|
||||
difficulty: this.selectedDifficulty,
|
||||
@@ -604,21 +612,24 @@ export class HostLobbyModal extends LitElement {
|
||||
}
|
||||
|
||||
private async putGameConfig() {
|
||||
const response = await fetch(`/private_lobby/${this.lobbyId}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
const response = await fetch(
|
||||
`${window.location.origin}/${getServerConfig().workerPath(this.lobbyId)}/game/${this.lobbyId}`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
gameMap: this.selectedMap,
|
||||
difficulty: this.selectedDifficulty,
|
||||
disableNPCs: this.disableNPCs,
|
||||
bots: this.bots,
|
||||
infiniteGold: this.infiniteGold,
|
||||
infiniteTroops: this.infiniteTroops,
|
||||
instantBuild: this.instantBuild,
|
||||
} as GameConfig),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
gameMap: this.selectedMap,
|
||||
difficulty: this.selectedDifficulty,
|
||||
disableNPCs: this.disableNPCs,
|
||||
bots: this.bots,
|
||||
infiniteGold: this.infiniteGold,
|
||||
infiniteTroops: this.infiniteTroops,
|
||||
instantBuild: this.instantBuild,
|
||||
}),
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
private getRandomMap(): GameMapType {
|
||||
@@ -637,12 +648,15 @@ export class HostLobbyModal extends LitElement {
|
||||
`Starting private game with map: ${GameMapType[this.selectedMap]} ${this.useRandomMap ? " (Randomly selected)" : ""}`,
|
||||
);
|
||||
this.close();
|
||||
const response = await fetch(`/start_private_lobby/${this.lobbyId}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
const response = await fetch(
|
||||
`${window.location.origin}/${getServerConfig().workerPath(this.lobbyId)}/start_game/${this.lobbyId}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
private async copyToClipboard() {
|
||||
@@ -656,34 +670,42 @@ export class HostLobbyModal extends LitElement {
|
||||
this.copySuccess = false;
|
||||
}, 2000);
|
||||
} catch (err) {
|
||||
consolex.error("Failed to copy text: ", err);
|
||||
consolex.error(`Failed to copy text: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async pollPlayers() {
|
||||
fetch(`/lobby/${this.lobbyId}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
fetch(
|
||||
`/${getServerConfig().workerPath(this.lobbyId)}/game/${this.lobbyId}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
.then((data: GameInfo) => {
|
||||
console.log(`got response: ${data}`);
|
||||
this.players = data.players.map((p) => p.username);
|
||||
this.players = data.clients.map((p) => p.username);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function createLobby(): Promise<Lobby> {
|
||||
async function createLobby(): Promise<GameInfo> {
|
||||
const serverConfig = getServerConfig();
|
||||
try {
|
||||
const response = await fetch("/private_lobby", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
const id = generateID();
|
||||
const response = await fetch(
|
||||
`/${serverConfig.workerPath(id)}/create_game/${id}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
// body: JSON.stringify(data), // Include this if you need to send data
|
||||
},
|
||||
// body: JSON.stringify(data), // Include this if you need to send data
|
||||
});
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
@@ -692,13 +714,7 @@ async function createLobby(): Promise<Lobby> {
|
||||
const data = await response.json();
|
||||
consolex.log("Success:", data);
|
||||
|
||||
// Assuming the server returns an object with an 'id' property
|
||||
const lobby: Lobby = {
|
||||
id: data.id,
|
||||
// Add other properties as needed
|
||||
};
|
||||
|
||||
return lobby;
|
||||
return data as GameInfo;
|
||||
} catch (error) {
|
||||
consolex.error("Error creating lobby:", error);
|
||||
throw error; // Re-throw the error so the caller can handle it
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { EventBus, GameEvent } from "../core/EventBus";
|
||||
import { Game } from "../core/game/Game";
|
||||
|
||||
export class MouseUpEvent implements GameEvent {
|
||||
constructor(
|
||||
@@ -63,6 +62,10 @@ export class AttackRatioEvent implements GameEvent {
|
||||
constructor(public readonly attackRatio: number) {}
|
||||
}
|
||||
|
||||
export class CenterCameraEvent implements GameEvent {
|
||||
constructor() {}
|
||||
}
|
||||
|
||||
export class InputHandler {
|
||||
private lastPointerX: number = 0;
|
||||
private lastPointerY: number = 0;
|
||||
@@ -95,6 +98,9 @@ export class InputHandler {
|
||||
this.canvas.addEventListener("wheel", (e) => this.onScroll(e), {
|
||||
passive: false,
|
||||
});
|
||||
this.canvas.addEventListener("wheel", (e) => this.onShiftScroll(e), {
|
||||
passive: false,
|
||||
});
|
||||
window.addEventListener("pointermove", this.onPointerMove.bind(this));
|
||||
this.canvas.addEventListener("contextmenu", (e: MouseEvent) => {
|
||||
this.onContextMenu(e);
|
||||
@@ -173,6 +179,7 @@ export class InputHandler {
|
||||
"KeyQ",
|
||||
"Digit1",
|
||||
"Digit2",
|
||||
"KeyC",
|
||||
].includes(e.code)
|
||||
) {
|
||||
this.activeKeys.add(e.code);
|
||||
@@ -200,6 +207,11 @@ export class InputHandler {
|
||||
this.eventBus.emit(new AttackRatioEvent(10));
|
||||
}
|
||||
|
||||
if (e.code === "KeyC") {
|
||||
e.preventDefault();
|
||||
this.eventBus.emit(new CenterCameraEvent());
|
||||
}
|
||||
|
||||
// Remove all movement keys from activeKeys
|
||||
if (
|
||||
[
|
||||
@@ -217,6 +229,7 @@ export class InputHandler {
|
||||
"KeyQ",
|
||||
"Digit1",
|
||||
"Digit2",
|
||||
"KeyC",
|
||||
].includes(e.code)
|
||||
) {
|
||||
this.activeKeys.delete(e.code);
|
||||
@@ -272,7 +285,16 @@ export class InputHandler {
|
||||
}
|
||||
|
||||
private onScroll(event: WheelEvent) {
|
||||
this.eventBus.emit(new ZoomEvent(event.x, event.y, event.deltaY));
|
||||
if (!event.shiftKey) {
|
||||
this.eventBus.emit(new ZoomEvent(event.x, event.y, event.deltaY));
|
||||
}
|
||||
}
|
||||
|
||||
private onShiftScroll(event: WheelEvent) {
|
||||
if (event.shiftKey) {
|
||||
const ratio = event.deltaY > 0 ? -10 : 10;
|
||||
this.eventBus.emit(new AttackRatioEvent(ratio));
|
||||
}
|
||||
}
|
||||
|
||||
private onPointerMove(event: PointerEvent) {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { LitElement, html, css } from "lit";
|
||||
import { customElement, property, state, query } from "lit/decorators.js";
|
||||
import { GameMapType, GameType } from "../core/game/Game";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, query, state } from "lit/decorators.js";
|
||||
import { getServerConfig } from "../core/configuration/Config";
|
||||
import { consolex } from "../core/Consolex";
|
||||
import { GameMapType, GameType } from "../core/game/Game";
|
||||
import { GameInfo } from "../core/Schemas";
|
||||
|
||||
@customElement("join-private-lobby-modal")
|
||||
export class JoinPrivateLobbyModal extends LitElement {
|
||||
@@ -231,6 +233,11 @@ export class JoinPrivateLobbyModal extends LitElement {
|
||||
class="modal-overlay"
|
||||
style="display: ${this.isModalOpen ? "flex" : "none"}"
|
||||
>
|
||||
<div
|
||||
style="position: absolute; left: 0px; top: 0px; width: 100%; height: 100%;"
|
||||
class="${this.isModalOpen ? "" : "hidden"}"
|
||||
@click=${this.close}
|
||||
></div>
|
||||
<div class="modal-content">
|
||||
<span class="close" @click=${this.closeAndLeave}>×</span>
|
||||
<div class="title">Join Private Lobby</div>
|
||||
@@ -358,13 +365,16 @@ export class JoinPrivateLobbyModal extends LitElement {
|
||||
consolex.log(`Joining lobby with ID: ${lobbyId}`);
|
||||
this.message = "Checking lobby..."; // Set initial message
|
||||
|
||||
fetch(`/lobby/${lobbyId}/exists`, {
|
||||
const url = `/${getServerConfig().workerPath(lobbyId)}/game/${lobbyId}/exists`;
|
||||
fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((response) => {
|
||||
return response.json();
|
||||
})
|
||||
.then((data) => {
|
||||
if (data.exists) {
|
||||
this.message = "Joined successfully! Waiting for game to start...";
|
||||
@@ -372,7 +382,7 @@ export class JoinPrivateLobbyModal extends LitElement {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("join-lobby", {
|
||||
detail: {
|
||||
lobby: { id: lobbyId },
|
||||
lobby: { gameID: lobbyId },
|
||||
gameType: GameType.Private,
|
||||
map: GameMapType.World,
|
||||
},
|
||||
@@ -394,15 +404,18 @@ export class JoinPrivateLobbyModal extends LitElement {
|
||||
private async pollPlayers() {
|
||||
if (!this.lobbyIdInput?.value) return;
|
||||
|
||||
fetch(`/lobby/${this.lobbyIdInput.value}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
fetch(
|
||||
`/${getServerConfig().workerPath(this.lobbyIdInput.value)}/game/${this.lobbyIdInput.value}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
this.players = data.players.map((p) => p.username);
|
||||
.then((data: GameInfo) => {
|
||||
this.players = data.clients.map((p) => p.username);
|
||||
})
|
||||
.catch((error) => {
|
||||
consolex.error("Error polling players:", error);
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { Config, GameEnv, ServerConfig } from "../core/configuration/Config";
|
||||
import {
|
||||
Config,
|
||||
GameEnv,
|
||||
getServerConfig,
|
||||
ServerConfig,
|
||||
} from "../core/configuration/Config";
|
||||
import { consolex } from "../core/Consolex";
|
||||
import { GameEvent } from "../core/EventBus";
|
||||
import {
|
||||
@@ -125,6 +130,7 @@ export class LocalServer {
|
||||
const blob = new Blob([JSON.stringify(GameRecordSchema.parse(record))], {
|
||||
type: "application/json",
|
||||
});
|
||||
navigator.sendBeacon("/archive_singleplayer_game", blob);
|
||||
const workerPath = getServerConfig().workerPath(this.lobbyConfig.gameID);
|
||||
navigator.sendBeacon(`/${workerPath}/archive_singleplayer_game`, blob);
|
||||
}
|
||||
}
|
||||
|
||||
+9
-13
@@ -17,6 +17,7 @@ import { PublicLobby } from "./PublicLobby";
|
||||
import { UserSettings } from "../core/game/UserSettings";
|
||||
import "./DarkModeButton";
|
||||
import { DarkModeButton } from "./DarkModeButton";
|
||||
import "./GoogleAdElement";
|
||||
import { HelpModal } from "./HelpModal";
|
||||
import { GameType } from "../core/game/Game";
|
||||
|
||||
@@ -65,10 +66,6 @@ class Client {
|
||||
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",
|
||||
@@ -112,9 +109,9 @@ class Client {
|
||||
});
|
||||
|
||||
if (this.userSettings.darkMode()) {
|
||||
document.body.classList.add("dark");
|
||||
document.documentElement.classList.add("dark");
|
||||
} else {
|
||||
document.body.classList.remove("dark");
|
||||
document.documentElement.classList.remove("dark");
|
||||
}
|
||||
page("/join/:lobbyId", (ctx) => {
|
||||
if (ctx.init && sessionStorage.getItem("inLobby")) {
|
||||
@@ -143,9 +140,12 @@ class Client {
|
||||
this.gameStop = joinLobby(
|
||||
{
|
||||
gameType: gameType,
|
||||
flag: (): string => this.flagInput.getCurrentFlag(),
|
||||
flag: (): string =>
|
||||
this.flagInput.getCurrentFlag() == "xx"
|
||||
? ""
|
||||
: this.flagInput.getCurrentFlag(),
|
||||
playerName: (): string => this.usernameInput.getCurrentUsername(),
|
||||
gameID: lobby.id,
|
||||
gameID: lobby.gameID,
|
||||
persistentID: getPersistentIDFromCookie(),
|
||||
playerID: generateID(),
|
||||
clientID: generateID(),
|
||||
@@ -161,7 +161,7 @@ class Client {
|
||||
this.joinModal.close();
|
||||
this.publicLobby.stop();
|
||||
if (gameType != GameType.Singleplayer) {
|
||||
window.history.pushState({}, "", `/join/${lobby.id}`);
|
||||
window.history.pushState({}, "", `/join/${lobby.gameID}`);
|
||||
sessionStorage.setItem("inLobby", "true");
|
||||
}
|
||||
},
|
||||
@@ -177,10 +177,6 @@ class Client {
|
||||
this.gameStop = null;
|
||||
this.publicLobby.leaveLobby();
|
||||
}
|
||||
|
||||
private async handleSinglePlayer(event: CustomEvent) {
|
||||
alert("coming soon");
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the client when the DOM is loaded
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { Lobby } from "../core/Schemas";
|
||||
import { Difficulty, GameMapType, GameType } from "../core/game/Game";
|
||||
import { consolex } from "../core/Consolex";
|
||||
import { getMapsImage } from "./utilities/Maps";
|
||||
import { GameInfo } from "../core/Schemas";
|
||||
|
||||
@customElement("public-lobby")
|
||||
export class PublicLobby extends LitElement {
|
||||
@state() private lobbies: Lobby[] = [];
|
||||
@state() private lobbies: GameInfo[] = [];
|
||||
@state() public isLobbyHighlighted: boolean = false;
|
||||
private lobbiesInterval: number | null = null;
|
||||
private currLobby: Lobby = null;
|
||||
private currLobby: GameInfo = null;
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
@@ -42,9 +42,9 @@ export class PublicLobby extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
async fetchLobbies(): Promise<Lobby[]> {
|
||||
async fetchLobbies(): Promise<GameInfo[]> {
|
||||
try {
|
||||
const response = await fetch("/lobbies");
|
||||
const response = await fetch(`/public_lobbies`);
|
||||
if (!response.ok)
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
const data = await response.json();
|
||||
@@ -67,6 +67,9 @@ export class PublicLobby extends LitElement {
|
||||
if (this.lobbies.length === 0) return html``;
|
||||
|
||||
const lobby = this.lobbies[0];
|
||||
if (!lobby?.gameConfig) {
|
||||
return;
|
||||
}
|
||||
const timeRemaining = Math.max(0, Math.floor(lobby.msUntilStart / 1000));
|
||||
|
||||
// Format time to show minutes and seconds
|
||||
@@ -81,7 +84,7 @@ export class PublicLobby extends LitElement {
|
||||
? "bg-gradient-to-r from-green-600 to-green-500"
|
||||
: "bg-gradient-to-r from-blue-600 to-blue-500"} text-white font-medium rounded-xl transition-opacity duration-200 hover:opacity-90"
|
||||
>
|
||||
<div class="text-lg md:text-2xl font-semibold mb-2">Next Game</div>
|
||||
<div class="text-lg md:text-2xl font-semibold mb-2">Join next Game</div>
|
||||
<div class="flex">
|
||||
<img
|
||||
src="${getMapsImage(lobby.gameConfig.gameMap)}"
|
||||
@@ -121,7 +124,7 @@ export class PublicLobby extends LitElement {
|
||||
this.currLobby = null;
|
||||
}
|
||||
|
||||
private lobbyClicked(lobby: Lobby) {
|
||||
private lobbyClicked(lobby: GameInfo) {
|
||||
if (this.currLobby == null) {
|
||||
this.isLobbyHighlighted = true;
|
||||
this.currLobby = lobby;
|
||||
|
||||
@@ -256,6 +256,11 @@ export class SinglePlayerModal extends LitElement {
|
||||
class="modal-overlay"
|
||||
style="display: ${this.isModalOpen ? "flex" : "none"}"
|
||||
>
|
||||
<div
|
||||
style="position: absolute; left: 0px; top: 0px; width: 100%; height: 100%;"
|
||||
class="${this.isModalOpen ? "" : "hidden"}"
|
||||
@click=${this.close}
|
||||
></div>
|
||||
<div class="modal-content">
|
||||
<span class="close" @click=${this.close}>×</span>
|
||||
|
||||
@@ -476,7 +481,7 @@ export class SinglePlayerModal extends LitElement {
|
||||
detail: {
|
||||
gameType: GameType.Singleplayer,
|
||||
lobby: {
|
||||
id: generateID(),
|
||||
gameID: generateID(),
|
||||
},
|
||||
map: this.selectedMap,
|
||||
difficulty: this.selectedDifficulty,
|
||||
|
||||
+56
-3
@@ -9,6 +9,7 @@ import {
|
||||
Player,
|
||||
PlayerID,
|
||||
PlayerType,
|
||||
Tick,
|
||||
UnitType,
|
||||
} from "../core/game/Game";
|
||||
import {
|
||||
@@ -23,6 +24,7 @@ import {
|
||||
GameConfig,
|
||||
ClientLogMessageSchema,
|
||||
ClientSendWinnerSchema,
|
||||
ClientMessageSchema,
|
||||
} from "../core/Schemas";
|
||||
import { LobbyConfig } from "./ClientGameRunner";
|
||||
import { LocalServer } from "./LocalServer";
|
||||
@@ -100,6 +102,14 @@ export class SendDonateIntentEvent implements GameEvent {
|
||||
) {}
|
||||
}
|
||||
|
||||
export class SendEmbargoIntentEvent implements GameEvent {
|
||||
constructor(
|
||||
public readonly sender: PlayerView,
|
||||
public readonly target: PlayerView,
|
||||
public readonly action: "start" | "stop",
|
||||
) {}
|
||||
}
|
||||
|
||||
export class SendSetTargetTroopRatioEvent implements GameEvent {
|
||||
constructor(public readonly ratio: number) {}
|
||||
}
|
||||
@@ -107,6 +117,12 @@ export class SendSetTargetTroopRatioEvent implements GameEvent {
|
||||
export class SendWinnerEvent implements GameEvent {
|
||||
constructor(public readonly winner: ClientID) {}
|
||||
}
|
||||
export class SendHashEvent implements GameEvent {
|
||||
constructor(
|
||||
public readonly tick: Tick,
|
||||
public readonly hash: number,
|
||||
) {}
|
||||
}
|
||||
|
||||
export class Transport {
|
||||
private socket: WebSocket;
|
||||
@@ -151,6 +167,9 @@ export class Transport {
|
||||
);
|
||||
this.eventBus.on(SendEmojiIntentEvent, (e) => this.onSendEmojiIntent(e));
|
||||
this.eventBus.on(SendDonateIntentEvent, (e) => this.onSendDonateIntent(e));
|
||||
this.eventBus.on(SendEmbargoIntentEvent, (e) =>
|
||||
this.onSendEmbargoIntent(e),
|
||||
);
|
||||
this.eventBus.on(SendSetTargetTroopRatioEvent, (e) =>
|
||||
this.onSendSetTargetTroopRatioEvent(e),
|
||||
);
|
||||
@@ -159,6 +178,7 @@ export class Transport {
|
||||
this.eventBus.on(SendLogEvent, (e) => this.onSendLogEvent(e));
|
||||
this.eventBus.on(PauseGameEvent, (e) => this.onPauseGameEvent(e));
|
||||
this.eventBus.on(SendWinnerEvent, (e) => this.onSendWinnerEvent(e));
|
||||
this.eventBus.on(SendHashEvent, (e) => this.onSendHashEvent(e));
|
||||
}
|
||||
|
||||
private startPing() {
|
||||
@@ -219,9 +239,10 @@ export class Transport {
|
||||
) {
|
||||
this.startPing();
|
||||
this.maybeKillSocket();
|
||||
const wsHost = process.env.WEBSOCKET_URL || window.location.host;
|
||||
const wsHost = window.location.host;
|
||||
const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
this.socket = new WebSocket(`${wsProtocol}//${wsHost}`);
|
||||
const workerPath = this.serverConfig.workerPath(this.lobbyConfig.gameID);
|
||||
this.socket = new WebSocket(`${wsProtocol}//${wsHost}/${workerPath}`);
|
||||
this.onconnect = onconnect;
|
||||
this.onmessage = onmessage;
|
||||
this.socket.onopen = () => {
|
||||
@@ -237,7 +258,9 @@ export class Transport {
|
||||
const serverMsg = ServerMessageSchema.parse(JSON.parse(event.data));
|
||||
this.onmessage(serverMsg);
|
||||
} catch (error) {
|
||||
console.error("Failed to process server message:", error);
|
||||
console.error(
|
||||
`Failed to process server message ${event.data}: ${error}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
this.socket.onerror = (err) => {
|
||||
@@ -397,6 +420,16 @@ export class Transport {
|
||||
});
|
||||
}
|
||||
|
||||
private onSendEmbargoIntent(event: SendEmbargoIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "embargo",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
playerID: this.lobbyConfig.playerID,
|
||||
targetID: event.target.id(),
|
||||
action: event.action,
|
||||
});
|
||||
}
|
||||
|
||||
private onSendSetTargetTroopRatioEvent(event: SendSetTargetTroopRatioEvent) {
|
||||
this.sendIntent({
|
||||
type: "troop_ratio",
|
||||
@@ -448,6 +481,26 @@ export class Transport {
|
||||
}
|
||||
}
|
||||
|
||||
private onSendHashEvent(event: SendHashEvent) {
|
||||
if (this.isLocal || this.socket.readyState === WebSocket.OPEN) {
|
||||
const msg = ClientMessageSchema.parse({
|
||||
type: "hash",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
persistentID: this.lobbyConfig.persistentID,
|
||||
gameID: this.lobbyConfig.gameID,
|
||||
tick: event.tick,
|
||||
hash: event.hash,
|
||||
});
|
||||
this.sendMsg(JSON.stringify(msg));
|
||||
} else {
|
||||
console.log(
|
||||
"WebSocket is not open. Current state:",
|
||||
this.socket.readyState,
|
||||
);
|
||||
console.log("attempting reconnect");
|
||||
}
|
||||
}
|
||||
|
||||
private sendIntent(intent: Intent) {
|
||||
if (this.isLocal || this.socket.readyState === WebSocket.OPEN) {
|
||||
const msg = ClientIntentMessageSchema.parse({
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { LitElement, html, css } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
@customElement("modal-overlay")
|
||||
export class ModalOverlay extends LitElement {
|
||||
@property({ reflect: true }) public visible: boolean = false;
|
||||
|
||||
static styles = css`
|
||||
.overlay {
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div
|
||||
class="overlay ${this.visible ? "" : "hidden"}"
|
||||
@click=${() => (this.visible = false)}
|
||||
></div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
calculateBoundingBox,
|
||||
calculateBoundingBoxCenter,
|
||||
} from "../../core/Util";
|
||||
import { ZoomEvent, DragEvent } from "../InputHandler";
|
||||
import { ZoomEvent, DragEvent, CenterCameraEvent } from "../InputHandler";
|
||||
import { GoToPlayerEvent } from "./layers/Leaderboard";
|
||||
import { placeName } from "./NameBoxCalculator";
|
||||
import { GameView } from "../../core/game/GameView";
|
||||
@@ -27,6 +27,7 @@ export class TransformHandler {
|
||||
this.eventBus.on(ZoomEvent, (e) => this.onZoom(e));
|
||||
this.eventBus.on(DragEvent, (e) => this.onMove(e));
|
||||
this.eventBus.on(GoToPlayerEvent, (e) => this.onGoToPlayer(e));
|
||||
this.eventBus.on(CenterCameraEvent, () => this.centerCamera());
|
||||
}
|
||||
|
||||
boundingRect(): DOMRect {
|
||||
@@ -148,6 +149,14 @@ export class TransformHandler {
|
||||
this.intervalID = setInterval(() => this.goTo(), 1);
|
||||
}
|
||||
|
||||
centerCamera() {
|
||||
this.clearTarget();
|
||||
const player = this.game.myPlayer();
|
||||
if (!player || !player.nameLocation()) return;
|
||||
this.target = new Cell(player.nameLocation().x, player.nameLocation().y);
|
||||
this.intervalID = setInterval(() => this.goTo(), 1);
|
||||
}
|
||||
|
||||
private goTo() {
|
||||
const { screenX, screenY } = this.screenCenter();
|
||||
const screenMapCenter = new Cell(screenX, screenY);
|
||||
|
||||
@@ -190,7 +190,7 @@ export class ControlPanel extends LitElement implements Layer {
|
||||
</style>
|
||||
<div
|
||||
class="${this._isVisible
|
||||
? "w-full text-sm lg:text-m lg:w-72 bg-gray-800/70 p-2 pr-3 lg:p-4 shadow-lg rounded-lg backdrop-blur"
|
||||
? "w-full text-sm lg:text-m lg:w-72 bg-gray-800/70 p-2 pr-3 lg:p-4 shadow-lg lg:rounded-lg backdrop-blur"
|
||||
: "hidden"}"
|
||||
@contextmenu=${(e) => e.preventDefault()}
|
||||
>
|
||||
|
||||
@@ -25,6 +25,7 @@ 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";
|
||||
import { GoToPlayerEvent } from "./Leaderboard";
|
||||
|
||||
interface Event {
|
||||
description: string;
|
||||
@@ -33,6 +34,7 @@ interface Event {
|
||||
text: string;
|
||||
className: string;
|
||||
action: () => void;
|
||||
preventClose?: boolean;
|
||||
}[];
|
||||
type: MessageType;
|
||||
highlight?: boolean;
|
||||
@@ -53,6 +55,15 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
@state() private incomingAttacks: AttackUpdate[] = [];
|
||||
@state() private outgoingAttacks: AttackUpdate[] = [];
|
||||
@state() private _hidden: boolean = false;
|
||||
@state() private newEvents: number = 0;
|
||||
|
||||
private toggleHidden() {
|
||||
this._hidden = !this._hidden;
|
||||
if (this._hidden) {
|
||||
this.newEvents = 0;
|
||||
}
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private updateMap = new Map([
|
||||
[GameUpdateType.DisplayEvent, (u) => this.onDisplayMessageEvent(u)],
|
||||
@@ -63,7 +74,7 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
],
|
||||
[GameUpdateType.BrokeAlliance, (u) => this.onBrokeAllianceEvent(u)],
|
||||
[GameUpdateType.TargetPlayer, (u) => this.onTargetPlayerEvent(u)],
|
||||
[GameUpdateType.EmojiUpdate, (u) => this.onEmojiMessageEvent(u)],
|
||||
[GameUpdateType.Emoji, (u) => this.onEmojiMessageEvent(u)],
|
||||
]);
|
||||
|
||||
constructor() {
|
||||
@@ -119,6 +130,9 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
|
||||
private addEvent(event: Event) {
|
||||
this.events = [...this.events, event];
|
||||
if (this._hidden == true) {
|
||||
this.newEvents++;
|
||||
}
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
@@ -169,6 +183,12 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
this.addEvent({
|
||||
description: `${requestor.name()} requests an alliance!`,
|
||||
buttons: [
|
||||
{
|
||||
text: "Focus",
|
||||
className: "btn-gray",
|
||||
action: () => this.eventBus.emit(new GoToPlayerEvent(requestor)),
|
||||
preventClose: true,
|
||||
},
|
||||
{
|
||||
text: "Accept",
|
||||
className: "btn",
|
||||
@@ -397,7 +417,7 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
<div
|
||||
class="${this._hidden
|
||||
? "w-fit px-[10px] py-[5px]"
|
||||
: ""} rounded-md bg-black bg-opacity-60 relative max-h-[30vh] flex flex-col-reverse overflow-y-auto w-full lg:bottom-2.5 lg:right-2.5 z-50 lg:max-w-3xl lg:w-full lg:w-auto"
|
||||
: ""} rounded-md bg-black bg-opacity-60 relative max-h-[30vh] flex flex-col-reverse overflow-y-auto w-full lg:bottom-2.5 lg:right-2.5 z-50 lg:max-w-[30vw] lg:w-full lg:w-auto"
|
||||
>
|
||||
<div>
|
||||
<div class="w-full bg-black/80 sticky top-0 px-[10px]">
|
||||
@@ -406,7 +426,7 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
._hidden
|
||||
? "hidden"
|
||||
: ""}"
|
||||
@click=${() => (this._hidden = true)}
|
||||
@click=${this.toggleHidden}
|
||||
>
|
||||
Hide
|
||||
</button>
|
||||
@@ -415,9 +435,15 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
class="text-white cursor-pointer pointer-events-auto ${this._hidden
|
||||
? ""
|
||||
: "hidden"}"
|
||||
@click=${() => (this._hidden = false)}
|
||||
@click=${this.toggleHidden}
|
||||
>
|
||||
Events
|
||||
<span
|
||||
class="${this.newEvents
|
||||
? ""
|
||||
: "hidden"} inline-block px-2 bg-red-500 rounded-sm"
|
||||
>${this.newEvents}</span
|
||||
>
|
||||
</button>
|
||||
<table
|
||||
class="w-full border-collapse text-white shadow-lg lg:text-xl text-xs ${this
|
||||
@@ -447,10 +473,14 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
class="inline-block px-3 py-1 text-white rounded text-sm cursor-pointer transition-colors duration-300
|
||||
${btn.className.includes("btn-info")
|
||||
? "bg-blue-500 hover:bg-blue-600"
|
||||
: "bg-green-600 hover:bg-green-700"}"
|
||||
: btn.className.includes("btn-gray")
|
||||
? "bg-gray-500 hover:bg-gray-600"
|
||||
: "bg-green-600 hover:bg-green-700"}"
|
||||
@click=${() => {
|
||||
btn.action();
|
||||
this.removeEvent(index);
|
||||
if (!btn.preventClose) {
|
||||
this.removeEvent(index);
|
||||
}
|
||||
this.requestUpdate();
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import { LitElement, html, css } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { Layer } from "./Layer";
|
||||
import { ClientID } from "../../../core/Schemas";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
||||
import { EventBus, GameEvent } from "../../../core/EventBus";
|
||||
import { renderNumber } from "../../Utils";
|
||||
import { GameView, PlayerView } from "../../../core/game/GameView";
|
||||
import { ClientID } from "../../../core/Schemas";
|
||||
import { renderNumber } from "../../Utils";
|
||||
import { Layer } from "./Layer";
|
||||
|
||||
interface Entry {
|
||||
name: string;
|
||||
position: number;
|
||||
score: string;
|
||||
gold: string;
|
||||
troops: string;
|
||||
isMyPlayer: boolean;
|
||||
player: PlayerView;
|
||||
}
|
||||
@@ -26,6 +27,13 @@ export class Leaderboard extends LitElement implements Layer {
|
||||
public clientID: ClientID;
|
||||
public eventBus: EventBus;
|
||||
|
||||
players: Entry[] = [];
|
||||
|
||||
@state()
|
||||
private _leaderboardHidden = true;
|
||||
private _shownOnInit = false;
|
||||
private showTopFive = true;
|
||||
|
||||
init() {}
|
||||
|
||||
tick() {
|
||||
@@ -58,14 +66,25 @@ export class Leaderboard extends LitElement implements Layer {
|
||||
const numTilesWithoutFallout =
|
||||
this.game.numLandTiles() - this.game.numTilesWithFallout();
|
||||
|
||||
this.players = sorted.slice(0, 5).map((player, index) => ({
|
||||
name: player.displayName(),
|
||||
position: index + 1,
|
||||
score: formatPercentage(player.numTilesOwned() / numTilesWithoutFallout),
|
||||
gold: renderNumber(player.gold()),
|
||||
isMyPlayer: player == myPlayer,
|
||||
player: player,
|
||||
}));
|
||||
const playersToShow = this.showTopFive ? sorted.slice(0, 5) : sorted;
|
||||
|
||||
this.players = playersToShow.map((player, index) => {
|
||||
let troops = player.troops() / 10;
|
||||
if (!player.isAlive()) {
|
||||
troops = 0;
|
||||
}
|
||||
return {
|
||||
name: player.displayName(),
|
||||
position: index + 1,
|
||||
score: formatPercentage(
|
||||
player.numTilesOwned() / numTilesWithoutFallout,
|
||||
),
|
||||
gold: renderNumber(player.gold()),
|
||||
troops: renderNumber(troops),
|
||||
isMyPlayer: player == myPlayer,
|
||||
player: player,
|
||||
};
|
||||
});
|
||||
|
||||
if (myPlayer != null && this.players.find((p) => p.isMyPlayer) == null) {
|
||||
let place = 0;
|
||||
@@ -76,6 +95,10 @@ export class Leaderboard extends LitElement implements Layer {
|
||||
}
|
||||
}
|
||||
|
||||
let myPlayerTroops = myPlayer.troops() / 10;
|
||||
if (!myPlayer.isAlive()) {
|
||||
myPlayerTroops = 0;
|
||||
}
|
||||
this.players.pop();
|
||||
this.players.push({
|
||||
name: myPlayer.displayName(),
|
||||
@@ -84,6 +107,7 @@ export class Leaderboard extends LitElement implements Layer {
|
||||
myPlayer.numTilesOwned() / this.game.numLandTiles(),
|
||||
),
|
||||
gold: renderNumber(myPlayer.gold()),
|
||||
troops: renderNumber(myPlayerTroops),
|
||||
isMyPlayer: true,
|
||||
player: myPlayer,
|
||||
});
|
||||
@@ -119,10 +143,10 @@ export class Leaderboard extends LitElement implements Layer {
|
||||
padding-top: 0px;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
|
||||
border-radius: 10px;
|
||||
max-width: 300px;
|
||||
max-height: 80vh;
|
||||
max-width: 500px;
|
||||
max-height: 30vh;
|
||||
overflow-y: auto;
|
||||
width: 300px;
|
||||
width: 400px;
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
table {
|
||||
@@ -180,6 +204,13 @@ export class Leaderboard extends LitElement implements Layer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.leaderboard-top-five-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.player-name {
|
||||
max-width: 10ch;
|
||||
overflow: hidden;
|
||||
@@ -188,7 +219,8 @@ export class Leaderboard extends LitElement implements Layer {
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
.leaderboard {
|
||||
top: 60px;
|
||||
top: 70px;
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
.leaderboard-button {
|
||||
@@ -198,13 +230,6 @@ export class Leaderboard extends LitElement implements Layer {
|
||||
}
|
||||
`;
|
||||
|
||||
players: Entry[] = [];
|
||||
|
||||
@state()
|
||||
private _leaderboardHidden = true;
|
||||
|
||||
private _shownOnInit = false;
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<button
|
||||
@@ -225,6 +250,15 @@ export class Leaderboard extends LitElement implements Layer {
|
||||
>
|
||||
Hide
|
||||
</button>
|
||||
<button
|
||||
class="leaderboard-top-five-button"
|
||||
@click=${() => {
|
||||
this.showTopFive = !this.showTopFive;
|
||||
this.updateLeaderboard();
|
||||
}}
|
||||
>
|
||||
${this.showTopFive ? "Show All" : "Show Top 5"}
|
||||
</button>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -232,6 +266,7 @@ export class Leaderboard extends LitElement implements Layer {
|
||||
<th>Player</th>
|
||||
<th>Owned</th>
|
||||
<th>Gold</th>
|
||||
<th>Troops</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -245,6 +280,7 @@ export class Leaderboard extends LitElement implements Layer {
|
||||
<td class="player-name">${unsafeHTML(player.name)}</td>
|
||||
<td>${player.score}</td>
|
||||
<td>${player.gold}</td>
|
||||
<td>${player.troops}</td>
|
||||
</tr>
|
||||
`,
|
||||
)}
|
||||
|
||||
@@ -11,8 +11,10 @@ import { Layer } from "./Layer";
|
||||
import { TransformHandler } from "../TransformHandler";
|
||||
import traitorIcon from "../../../../resources/images/TraitorIcon.svg";
|
||||
import allianceIcon from "../../../../resources/images/AllianceIcon.svg";
|
||||
import allianceRequestIcon from "../../../../resources/images/AllianceRequestIcon.svg";
|
||||
import crownIcon from "../../../../resources/images/CrownIcon.svg";
|
||||
import targetIcon from "../../../../resources/images/TargetIcon.svg";
|
||||
import embargoIcon from "../../../../resources/images/EmbargoIcon.svg";
|
||||
import { ClientID } from "../../../core/Schemas";
|
||||
import { GameView, PlayerView } from "../../../core/game/GameView";
|
||||
import { createCanvas, renderTroops } from "../../Utils";
|
||||
@@ -40,9 +42,11 @@ export class NameLayer implements Layer {
|
||||
private renders: RenderInfo[] = [];
|
||||
private seenPlayers: Set<PlayerView> = new Set();
|
||||
private traitorIconImage: HTMLImageElement;
|
||||
private allianceRequestIconImage: HTMLImageElement;
|
||||
private allianceIconImage: HTMLImageElement;
|
||||
private targetIconImage: HTMLImageElement;
|
||||
private crownIconImage: HTMLImageElement;
|
||||
private embargoIconImage: HTMLImageElement;
|
||||
private container: HTMLDivElement;
|
||||
private myPlayer: PlayerView | null = null;
|
||||
private firstPlace: PlayerView | null = null;
|
||||
@@ -57,10 +61,14 @@ export class NameLayer implements Layer {
|
||||
this.traitorIconImage.src = traitorIcon;
|
||||
this.allianceIconImage = new Image();
|
||||
this.allianceIconImage.src = allianceIcon;
|
||||
this.allianceRequestIconImage = new Image();
|
||||
this.allianceRequestIconImage.src = allianceRequestIcon;
|
||||
this.crownIconImage = new Image();
|
||||
this.crownIconImage.src = crownIcon;
|
||||
this.targetIconImage = new Image();
|
||||
this.targetIconImage.src = targetIcon;
|
||||
this.embargoIconImage = new Image();
|
||||
this.embargoIconImage.src = embargoIcon;
|
||||
}
|
||||
|
||||
resizeCanvas() {
|
||||
@@ -162,6 +170,7 @@ export class NameLayer implements Layer {
|
||||
iconsDiv.style.justifyContent = "center";
|
||||
iconsDiv.style.alignItems = "center";
|
||||
iconsDiv.style.zIndex = "2";
|
||||
iconsDiv.style.opacity = "0.8";
|
||||
element.appendChild(iconsDiv);
|
||||
|
||||
const nameDiv = document.createElement("div");
|
||||
@@ -314,6 +323,23 @@ export class NameLayer implements Layer {
|
||||
existingAlliance.remove();
|
||||
}
|
||||
|
||||
// Alliance request icon
|
||||
const data = '[data-icon="alliance-request"]';
|
||||
const existingRequestAlliance = iconsDiv.querySelector(data);
|
||||
if (myPlayer != null && render.player.isRequestingAllianceWith(myPlayer)) {
|
||||
if (!existingRequestAlliance) {
|
||||
iconsDiv.appendChild(
|
||||
this.createIconElement(
|
||||
this.allianceRequestIconImage.src,
|
||||
iconSize,
|
||||
"alliance-request",
|
||||
),
|
||||
);
|
||||
}
|
||||
} else if (existingRequestAlliance) {
|
||||
existingRequestAlliance.remove();
|
||||
}
|
||||
|
||||
// Target icon
|
||||
const existingTarget = iconsDiv.querySelector('[data-icon="target"]');
|
||||
if (
|
||||
@@ -359,6 +385,24 @@ export class NameLayer implements Layer {
|
||||
existingEmoji.remove();
|
||||
}
|
||||
|
||||
const existingEmbargo = iconsDiv.querySelector('[data-icon="embargo"]');
|
||||
const hasEmbargo =
|
||||
render.player.hasEmbargoAgainst(myPlayer) ||
|
||||
myPlayer.hasEmbargoAgainst(render.player);
|
||||
if (myPlayer && hasEmbargo) {
|
||||
if (!existingEmbargo) {
|
||||
iconsDiv.appendChild(
|
||||
this.createIconElement(
|
||||
this.embargoIconImage.src,
|
||||
iconSize,
|
||||
"embargo",
|
||||
),
|
||||
);
|
||||
}
|
||||
} else if (existingEmbargo) {
|
||||
existingEmbargo.remove();
|
||||
}
|
||||
|
||||
// Update all icon sizes
|
||||
const icons = iconsDiv.getElementsByTagName("img");
|
||||
for (const icon of icons) {
|
||||
|
||||
@@ -107,7 +107,7 @@ export class OptionsMenu extends LitElement implements Layer {
|
||||
tick() {
|
||||
this.hasWinner =
|
||||
this.hasWinner ||
|
||||
this.game.updatesSinceLastTick()[GameUpdateType.WinUpdate].length > 0;
|
||||
this.game.updatesSinceLastTick()[GameUpdateType.Win].length > 0;
|
||||
if (this.game.inSpawnPhase()) {
|
||||
this.timer = 0;
|
||||
} else if (!this.hasWinner && this.game.ticks() % 10 == 0) {
|
||||
@@ -127,7 +127,7 @@ export class OptionsMenu extends LitElement implements Layer {
|
||||
@contextmenu=${(e) => e.preventDefault()}
|
||||
>
|
||||
<div
|
||||
class="bg-opacity-60 bg-gray-900 p-1 lg:p-2 rounded-lg backdrop-blur-md"
|
||||
class="bg-opacity-60 bg-gray-900 p-1 lg:p-2 rounded-es-sm lg:rounded-lg backdrop-blur-md"
|
||||
>
|
||||
<div class="flex items-stretch gap-1 lg:gap-2">
|
||||
${button({
|
||||
|
||||
@@ -4,7 +4,12 @@ import { EventBus } from "../../../core/EventBus";
|
||||
import { GameView, PlayerView } from "../../../core/game/GameView";
|
||||
import { Layer } from "./Layer";
|
||||
import { MouseUpEvent } from "../../InputHandler";
|
||||
import { AllPlayers, Player, PlayerActions } from "../../../core/game/Game";
|
||||
import {
|
||||
AllPlayers,
|
||||
Player,
|
||||
PlayerActions,
|
||||
UnitType,
|
||||
} from "../../../core/game/Game";
|
||||
import { TileRef } from "../../../core/game/GameMap";
|
||||
import { renderNumber, renderTroops } from "../../Utils";
|
||||
import targetIcon from "../../../../resources/images/TargetIconWhite.svg";
|
||||
@@ -18,6 +23,7 @@ import {
|
||||
SendDonateIntentEvent,
|
||||
SendEmojiIntentEvent,
|
||||
SendTargetPlayerIntentEvent,
|
||||
SendEmbargoIntentEvent,
|
||||
} from "../../Transport";
|
||||
import { EmojiTable } from "./EmojiTable";
|
||||
|
||||
@@ -76,6 +82,26 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
this.hide();
|
||||
}
|
||||
|
||||
private handleEmbargoClick(
|
||||
e: Event,
|
||||
myPlayer: PlayerView,
|
||||
other: PlayerView,
|
||||
) {
|
||||
e.stopPropagation();
|
||||
this.eventBus.emit(new SendEmbargoIntentEvent(myPlayer, other, "start"));
|
||||
this.hide();
|
||||
}
|
||||
|
||||
private handleStopEmbargoClick(
|
||||
e: Event,
|
||||
myPlayer: PlayerView,
|
||||
other: PlayerView,
|
||||
) {
|
||||
e.stopPropagation();
|
||||
this.eventBus.emit(new SendEmbargoIntentEvent(myPlayer, other, "stop"));
|
||||
this.hide();
|
||||
}
|
||||
|
||||
private handleEmojiClick(e: Event, myPlayer: PlayerView, other: PlayerView) {
|
||||
e.stopPropagation();
|
||||
this.emojiTable.showTable((emoji: string) => {
|
||||
@@ -107,6 +133,24 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
getTotalNukesSent(): number {
|
||||
const stats = this.actions.interaction?.stats;
|
||||
if (!stats) {
|
||||
return 0;
|
||||
}
|
||||
let sum = 0;
|
||||
const nukes = stats.sentNukes[this.g.myPlayer().id()];
|
||||
if (!nukes) {
|
||||
return 0;
|
||||
}
|
||||
for (const nukeType in nukes) {
|
||||
if (nukeType != UnitType.MIRVWarhead) {
|
||||
sum += nukes[nukeType];
|
||||
}
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.isVisible) {
|
||||
return html``;
|
||||
@@ -131,6 +175,7 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
: this.actions.interaction?.canSendEmoji;
|
||||
const canBreakAlliance = this.actions.interaction?.canBreakAlliance;
|
||||
const canTarget = this.actions.interaction?.canTarget;
|
||||
const canEmbargo = this.actions.interaction?.canEmbargo;
|
||||
|
||||
return html`
|
||||
<div
|
||||
@@ -143,7 +188,7 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
<!-- Close button -->
|
||||
<button
|
||||
@click=${this.handleClose}
|
||||
class="absolute -top-2 -right-2 w-6 h-6 flex items-center justify-center
|
||||
class="absolute -top-2 -right-2 w-6 h-6 flex items-center justify-center
|
||||
bg-red-500 hover:bg-red-600 text-white rounded-full
|
||||
text-sm font-bold transition-colors"
|
||||
>
|
||||
@@ -155,7 +200,7 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
<div class="flex items-center gap-1 lg:gap-2">
|
||||
<div
|
||||
class="px-4 h-8 lg:h-10 flex items-center justify-center
|
||||
bg-opacity-50 bg-gray-700 text-opacity-90 text-white
|
||||
bg-opacity-50 bg-gray-700 text-opacity-90 text-white
|
||||
rounded text-sm lg:text-xl w-full"
|
||||
>
|
||||
${other?.name()}
|
||||
@@ -190,12 +235,32 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Embargo -->
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="text-white text-opacity-80 text-sm px-2">
|
||||
Embargo against you
|
||||
</div>
|
||||
<div class="bg-opacity-50 bg-gray-700 rounded p-2 text-white">
|
||||
${other.hasEmbargoAgainst(myPlayer) ? "Yes" : "No"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="text-white text-opacity-80 text-sm px-2">
|
||||
Nukes sent by them to you
|
||||
</div>
|
||||
<div class="bg-opacity-50 bg-gray-700 rounded p-2 text-white">
|
||||
${this.getTotalNukesSent()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action buttons -->
|
||||
<div class="flex justify-center gap-2">
|
||||
${canTarget
|
||||
? html`<button
|
||||
@click=${(e) => this.handleTargetClick(e, other)}
|
||||
class="w-10 h-10 flex items-center justify-center
|
||||
class="w-10 h-10 flex items-center justify-center
|
||||
bg-opacity-50 bg-gray-700 hover:bg-opacity-70
|
||||
text-white rounded-lg transition-colors"
|
||||
>
|
||||
@@ -206,8 +271,8 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
? html`<button
|
||||
@click=${(e) =>
|
||||
this.handleBreakAllianceClick(e, myPlayer, other)}
|
||||
class="w-10 h-10 flex items-center justify-center
|
||||
bg-opacity-50 bg-gray-700 hover:bg-opacity-70
|
||||
class="w-10 h-10 flex items-center justify-center
|
||||
bg-opacity-50 bg-gray-700 hover:bg-opacity-70
|
||||
text-white rounded-lg transition-colors"
|
||||
>
|
||||
<img
|
||||
@@ -221,8 +286,8 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
? html`<button
|
||||
@click=${(e) =>
|
||||
this.handleAllianceClick(e, myPlayer, other)}
|
||||
class="w-10 h-10 flex items-center justify-center
|
||||
bg-opacity-50 bg-gray-700 hover:bg-opacity-70
|
||||
class="w-10 h-10 flex items-center justify-center
|
||||
bg-opacity-50 bg-gray-700 hover:bg-opacity-70
|
||||
text-white rounded-lg transition-colors"
|
||||
>
|
||||
<img src=${allianceIcon} alt="Alliance" class="w-6 h-6" />
|
||||
@@ -231,8 +296,8 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
${canDonate
|
||||
? html`<button
|
||||
@click=${(e) => this.handleDonateClick(e, myPlayer, other)}
|
||||
class="w-10 h-10 flex items-center justify-center
|
||||
bg-opacity-50 bg-gray-700 hover:bg-opacity-70
|
||||
class="w-10 h-10 flex items-center justify-center
|
||||
bg-opacity-50 bg-gray-700 hover:bg-opacity-70
|
||||
text-white rounded-lg transition-colors"
|
||||
>
|
||||
<img src=${donateIcon} alt="Donate" class="w-6 h-6" />
|
||||
@@ -241,14 +306,35 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
${canSendEmoji
|
||||
? html`<button
|
||||
@click=${(e) => this.handleEmojiClick(e, myPlayer, other)}
|
||||
class="w-10 h-10 flex items-center justify-center
|
||||
bg-opacity-50 bg-gray-700 hover:bg-opacity-70
|
||||
class="w-10 h-10 flex items-center justify-center
|
||||
bg-opacity-50 bg-gray-700 hover:bg-opacity-70
|
||||
text-white rounded-lg transition-colors"
|
||||
>
|
||||
<img src=${emojiIcon} alt="Emoji" class="w-6 h-6" />
|
||||
</button>`
|
||||
: ""}
|
||||
</div>
|
||||
${canEmbargo && other != myPlayer
|
||||
? html`<button
|
||||
@click=${(e) => this.handleEmbargoClick(e, myPlayer, other)}
|
||||
class="w-100 h-10 flex items-center justify-center
|
||||
bg-opacity-50 bg-gray-700 hover:bg-opacity-70
|
||||
text-white rounded-lg transition-colors"
|
||||
>
|
||||
Start embargo
|
||||
</button>`
|
||||
: ""}
|
||||
${!canEmbargo && other != myPlayer
|
||||
? html`<button
|
||||
@click=${(e) =>
|
||||
this.handleStopEmbargoClick(e, myPlayer, other)}
|
||||
class="w-100 h-10 flex items-center justify-center
|
||||
bg-opacity-50 bg-gray-700 hover:bg-opacity-70
|
||||
text-white rounded-lg transition-colors"
|
||||
>
|
||||
Stop embargo
|
||||
</button>`
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,9 @@ import { renderNumber, renderTroops } from "../../Utils";
|
||||
export class TopBar extends LitElement implements Layer {
|
||||
public game: GameView;
|
||||
private isVisible = false;
|
||||
private _population = 0;
|
||||
private _lastPopulationIncreaseRate = 0;
|
||||
private _popRateIsIncreasing = false;
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
@@ -19,6 +22,15 @@ export class TopBar extends LitElement implements Layer {
|
||||
}
|
||||
|
||||
tick() {
|
||||
if (this.game?.myPlayer() !== null) {
|
||||
const popIncreaseRate =
|
||||
this.game.myPlayer().population() - this._population;
|
||||
if (this.game.ticks() % 5 == 0) {
|
||||
this._popRateIsIncreasing =
|
||||
popIncreaseRate >= this._lastPopulationIncreaseRate;
|
||||
this._lastPopulationIncreaseRate = popIncreaseRate;
|
||||
}
|
||||
}
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
@@ -38,7 +50,7 @@ export class TopBar extends LitElement implements Layer {
|
||||
|
||||
return html`
|
||||
<div
|
||||
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"
|
||||
class="fixed top-0 z-50 bg-gray-800/70 text-white text-sm p-1 rounded-ee-sm lg:rounded grid grid-cols-1 sm:grid-cols-2 w-1/2 sm:w-2/3 md:w-1/2 lg:hidden backdrop-blur"
|
||||
>
|
||||
<!-- Pop section (takes 2 columns on desktop) -->
|
||||
<div
|
||||
@@ -49,7 +61,12 @@ export class TopBar extends LitElement implements Layer {
|
||||
>${renderTroops(myPlayer.population())} /
|
||||
${renderTroops(maxPop)}</span
|
||||
>
|
||||
<span>(+${renderTroops(popRate)})</span>
|
||||
<span
|
||||
class="${this._popRateIsIncreasing
|
||||
? "text-green-500"
|
||||
: "text-yellow-500"}"
|
||||
>(+${renderTroops(popRate)})</span
|
||||
>
|
||||
</div>
|
||||
<!-- Gold section (takes 1 column on desktop) -->
|
||||
<div
|
||||
|
||||
@@ -34,12 +34,13 @@ export class WinModal extends LitElement implements Layer {
|
||||
private _title: string;
|
||||
private won: boolean;
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
// Override to prevent shadow DOM creation
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
.modal {
|
||||
static styles = css`
|
||||
.win-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
@@ -58,7 +59,7 @@ export class WinModal extends LitElement implements Layer {
|
||||
visibility 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.modal.visible {
|
||||
.win-modal.visible {
|
||||
display: block;
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
@@ -74,14 +75,14 @@ export class WinModal extends LitElement implements Layer {
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
.win-modal h2 {
|
||||
margin: 0 0 15px 0;
|
||||
font-size: 24px;
|
||||
text-align: center;
|
||||
color: white;
|
||||
}
|
||||
|
||||
p {
|
||||
.win-modal p {
|
||||
margin: 0 0 20px 0;
|
||||
text-align: center;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
@@ -95,7 +96,7 @@ export class WinModal extends LitElement implements Layer {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
button {
|
||||
.win-modal button {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
font-size: 16px;
|
||||
@@ -109,38 +110,46 @@ export class WinModal extends LitElement implements Layer {
|
||||
transform 0.1s ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
.win-modal button:hover {
|
||||
background: rgba(0, 150, 255, 0.8);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
button:active {
|
||||
.win-modal button:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.modal {
|
||||
.win-modal {
|
||||
width: 90%;
|
||||
max-width: 300px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
.win-modal h2 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
button {
|
||||
.win-modal button {
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
// Add styles to document
|
||||
const styleEl = document.createElement("style");
|
||||
styleEl.textContent = WinModal.styles.toString();
|
||||
document.head.appendChild(styleEl);
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="modal ${this.isVisible ? "visible" : ""}">
|
||||
<h2>${this._title}</h2>
|
||||
<div>${this.won ? this.supportHTML() : this.adsHTML()}</div>
|
||||
<div class="win-modal ${this.isVisible ? "visible" : ""}">
|
||||
<h2>${this._title || ""}</h2>
|
||||
${this.won ? this.supportHTML() : this.adsHTML()}
|
||||
<div class="button-container">
|
||||
<button @click=${this._handleExit}>Exit Game</button>
|
||||
<button @click=${this.hide}>Keep Playing</button>
|
||||
@@ -149,24 +158,40 @@ export class WinModal extends LitElement implements Layer {
|
||||
`;
|
||||
}
|
||||
|
||||
adsHTML(): ReturnType<typeof html> {
|
||||
updated(changedProperties) {
|
||||
super.updated(changedProperties);
|
||||
// Initialize ads if modal is visible and showing ads
|
||||
if (changedProperties.has("isVisible") && this.isVisible && !this.won) {
|
||||
try {
|
||||
setTimeout(() => {
|
||||
(adsbygoogle = window.adsbygoogle || []).push({});
|
||||
}, 0);
|
||||
} catch (error) {
|
||||
console.error("Error initializing ad:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
adsHTML() {
|
||||
return html`<ins
|
||||
class="adsbygoogle"
|
||||
style="display:block"
|
||||
data-ad-client="ca-pub-7035513310742290"
|
||||
data-ad-slot="3772893937"
|
||||
data-ad-slot="winmodalad"
|
||||
data-ad-format="auto"
|
||||
data-full-width-responsive="true"
|
||||
></ins>`;
|
||||
}
|
||||
|
||||
supportHTML(): ReturnType<typeof html> {
|
||||
supportHTML() {
|
||||
return html`
|
||||
<div style="text-align: center; margin: 15px 0;">
|
||||
<p>
|
||||
Like the game? Help make this my full-time project!
|
||||
<a
|
||||
href="https://discord.com/channels/1284581928254701718/shop/1330243291366559744"
|
||||
href="https://discord.gg/k22YrnAzGp"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style="color: #0096ff; text-decoration: underline; display: block; margin-top: 5px;"
|
||||
>
|
||||
Support the game!
|
||||
@@ -201,14 +226,10 @@ export class WinModal extends LitElement implements Layer {
|
||||
this.hasShownDeathModal = true;
|
||||
this._title = "You died";
|
||||
this.won = false;
|
||||
try {
|
||||
(adsbygoogle = window.adsbygoogle || []).push({});
|
||||
} catch (error) {
|
||||
console.error("Error initializing ad:", error);
|
||||
}
|
||||
this.show();
|
||||
}
|
||||
this.game.updatesSinceLastTick()[GameUpdateType.WinUpdate].forEach((wu) => {
|
||||
|
||||
this.game.updatesSinceLastTick()[GameUpdateType.Win].forEach((wu) => {
|
||||
const winner = this.game.playerBySmallID(wu.winnerID) as PlayerView;
|
||||
this.eventBus.emit(new SendWinnerEvent(winner.clientID()));
|
||||
if (winner == this.game.myPlayer()) {
|
||||
|
||||
@@ -153,19 +153,20 @@
|
||||
</g>
|
||||
</svg>
|
||||
<div class="flex justify-center text-sm font-bold mt-[-5px] logo-version">
|
||||
v0.16.0
|
||||
v0.17.1
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-image"></div>
|
||||
|
||||
<dark-mode-button></dark-mode-button>
|
||||
<google-ad></google-ad>
|
||||
|
||||
<!-- 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 flex gap-1 items-center max-w-sm sm:max-w-md lg:max-w-lg xl:max-w-xl mx-auto p-2 pb-4"
|
||||
class="flex gap-1 items-center max-w-sm sm:max-w-md lg:max-w-lg xl:max-w-xl mx-auto p-2 pb-4"
|
||||
>
|
||||
<flag-input class="w-[20%] md:w-[15%]"></flag-input>
|
||||
<username-input class="w-full"></username-input>
|
||||
@@ -174,8 +175,13 @@
|
||||
<div class="max-w-sm sm:max-w-md lg:max-w-lg xl:max-w-xl mx-auto mt-4">
|
||||
<a
|
||||
href="https://discord.gg/k22YrnAzGp"
|
||||
class="w-full bg-[#5865F2] hover:bg-[#4752C4] text-white p-3 sm:p-4 lg:p-5 font-medium text-lg sm:text-xl lg:text-2xl rounded-lg border-none cursor-pointer transition-colors duration-300 flex justify-center"
|
||||
class="w-full bg-[#5865F2] hover:bg-[#4752C4] text-white p-3 sm:p-4 lg:p-5 font-medium text-lg sm:text-xl lg:text-2xl rounded-lg border-none cursor-pointer transition-colors duration-300 flex justify-center items-center gap-5"
|
||||
>
|
||||
<img
|
||||
style="height: 50px; width: 50px"
|
||||
alt="Discord"
|
||||
src="../../resources/icons/discord.svg"
|
||||
/>
|
||||
Join the Discord!
|
||||
</a>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user