mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-08 16:12:10 +00:00
format codebase with prettier
This commit is contained in:
+261
-196
@@ -1,240 +1,305 @@
|
||||
import { Executor } from "../core/execution/ExecutionManager";
|
||||
import { Cell, Game, PlayerID, GameMapType, Difficulty, GameType } from "../core/game/Game";
|
||||
import {
|
||||
Cell,
|
||||
Game,
|
||||
PlayerID,
|
||||
GameMapType,
|
||||
Difficulty,
|
||||
GameType,
|
||||
} from "../core/game/Game";
|
||||
import { EventBus } from "../core/EventBus";
|
||||
import { createRenderer, GameRenderer } from "./graphics/GameRenderer";
|
||||
import { InputHandler, MouseUpEvent, ZoomEvent, DragEvent, MouseDownEvent } from "./InputHandler"
|
||||
import { ClientID, ClientIntentMessageSchema, ClientJoinMessageSchema, ClientMessageSchema, GameConfig, GameID, Intent, ServerMessage, ServerMessageSchema, ServerSyncMessage, Turn } from "../core/Schemas";
|
||||
import { loadTerrainFromFile, loadTerrainMap } from "../core/game/TerrainMapLoader";
|
||||
import { SendAttackIntentEvent, SendSpawnIntentEvent, Transport } from "./Transport";
|
||||
import {
|
||||
InputHandler,
|
||||
MouseUpEvent,
|
||||
ZoomEvent,
|
||||
DragEvent,
|
||||
MouseDownEvent,
|
||||
} from "./InputHandler";
|
||||
import {
|
||||
ClientID,
|
||||
ClientIntentMessageSchema,
|
||||
ClientJoinMessageSchema,
|
||||
ClientMessageSchema,
|
||||
GameConfig,
|
||||
GameID,
|
||||
Intent,
|
||||
ServerMessage,
|
||||
ServerMessageSchema,
|
||||
ServerSyncMessage,
|
||||
Turn,
|
||||
} from "../core/Schemas";
|
||||
import {
|
||||
loadTerrainFromFile,
|
||||
loadTerrainMap,
|
||||
} from "../core/game/TerrainMapLoader";
|
||||
import {
|
||||
SendAttackIntentEvent,
|
||||
SendSpawnIntentEvent,
|
||||
Transport,
|
||||
} from "./Transport";
|
||||
import { createCanvas } from "./Utils";
|
||||
import { MessageType } from '../core/game/Game';
|
||||
import { MessageType } from "../core/game/Game";
|
||||
import { DisplayMessageUpdate, ErrorUpdate } from "../core/game/GameUpdates";
|
||||
import { WorkerClient } from "../core/worker/WorkerClient";
|
||||
import { consolex, initRemoteSender } from "../core/Consolex";
|
||||
import { getConfig, getServerConfig } from "../core/configuration/Config";
|
||||
import { GameView, PlayerView } from "../core/game/GameView";
|
||||
import { GameUpdateViewData } from '../core/game/GameUpdates';
|
||||
import { GameUpdateViewData } from "../core/game/GameUpdates";
|
||||
|
||||
export interface LobbyConfig {
|
||||
playerName: () => string
|
||||
clientID: ClientID,
|
||||
playerID: PlayerID,
|
||||
persistentID: string,
|
||||
gameType: GameType
|
||||
gameID: GameID,
|
||||
map: GameMapType | null
|
||||
difficulty: Difficulty | null
|
||||
playerName: () => string;
|
||||
clientID: ClientID;
|
||||
playerID: PlayerID;
|
||||
persistentID: string;
|
||||
gameType: GameType;
|
||||
gameID: GameID;
|
||||
map: GameMapType | null;
|
||||
difficulty: Difficulty | null;
|
||||
}
|
||||
|
||||
export function joinLobby(lobbyConfig: LobbyConfig, onjoin: () => void): () => void {
|
||||
const eventBus = new EventBus()
|
||||
initRemoteSender(eventBus)
|
||||
export function joinLobby(
|
||||
lobbyConfig: LobbyConfig,
|
||||
onjoin: () => void,
|
||||
): () => void {
|
||||
const eventBus = new EventBus();
|
||||
initRemoteSender(eventBus);
|
||||
|
||||
consolex.log(`joinging lobby: gameID: ${lobbyConfig.gameID}, clientID: ${lobbyConfig.clientID}, persistentID: ${lobbyConfig.persistentID}`)
|
||||
consolex.log(
|
||||
`joinging lobby: gameID: ${lobbyConfig.gameID}, clientID: ${lobbyConfig.clientID}, persistentID: ${lobbyConfig.persistentID}`,
|
||||
);
|
||||
|
||||
const serverConfig = getServerConfig()
|
||||
const serverConfig = getServerConfig();
|
||||
|
||||
let gameConfig: GameConfig = null
|
||||
if (lobbyConfig.gameType == GameType.Singleplayer) {
|
||||
gameConfig = {
|
||||
gameType: GameType.Singleplayer,
|
||||
gameMap: lobbyConfig.map,
|
||||
difficulty: lobbyConfig.difficulty,
|
||||
}
|
||||
}
|
||||
|
||||
const transport = new Transport(
|
||||
lobbyConfig,
|
||||
gameConfig,
|
||||
eventBus,
|
||||
serverConfig,
|
||||
)
|
||||
|
||||
const onconnect = () => {
|
||||
consolex.log(`Joined game lobby ${lobbyConfig.gameID}`);
|
||||
transport.joinGame(0)
|
||||
let gameConfig: GameConfig = null;
|
||||
if (lobbyConfig.gameType == GameType.Singleplayer) {
|
||||
gameConfig = {
|
||||
gameType: GameType.Singleplayer,
|
||||
gameMap: lobbyConfig.map,
|
||||
difficulty: lobbyConfig.difficulty,
|
||||
};
|
||||
const onmessage = (message: ServerMessage) => {
|
||||
if (message.type == "start") {
|
||||
consolex.log('lobby: game started')
|
||||
onjoin()
|
||||
createClientGame(lobbyConfig, message.config, eventBus, transport).then(r => r.start())
|
||||
};
|
||||
}
|
||||
transport.connect(onconnect, onmessage)
|
||||
return () => {
|
||||
consolex.log('leaving game')
|
||||
transport.leaveGame()
|
||||
}
|
||||
|
||||
const transport = new Transport(
|
||||
lobbyConfig,
|
||||
gameConfig,
|
||||
eventBus,
|
||||
serverConfig,
|
||||
);
|
||||
|
||||
const onconnect = () => {
|
||||
consolex.log(`Joined game lobby ${lobbyConfig.gameID}`);
|
||||
transport.joinGame(0);
|
||||
};
|
||||
const onmessage = (message: ServerMessage) => {
|
||||
if (message.type == "start") {
|
||||
consolex.log("lobby: game started");
|
||||
onjoin();
|
||||
createClientGame(lobbyConfig, message.config, eventBus, transport).then(
|
||||
(r) => r.start(),
|
||||
);
|
||||
}
|
||||
};
|
||||
transport.connect(onconnect, onmessage);
|
||||
return () => {
|
||||
consolex.log("leaving game");
|
||||
transport.leaveGame();
|
||||
};
|
||||
}
|
||||
|
||||
export async function createClientGame(
|
||||
lobbyConfig: LobbyConfig,
|
||||
gameConfig: GameConfig,
|
||||
eventBus: EventBus,
|
||||
transport: Transport,
|
||||
): Promise<ClientGameRunner> {
|
||||
const config = getConfig(gameConfig);
|
||||
|
||||
export async function createClientGame(lobbyConfig: LobbyConfig, gameConfig: GameConfig, eventBus: EventBus, transport: Transport): Promise<ClientGameRunner> {
|
||||
const config = getConfig(gameConfig)
|
||||
const gameMap = await loadTerrainMap(gameConfig.gameMap);
|
||||
const worker = new WorkerClient(lobbyConfig.gameID, gameConfig);
|
||||
await worker.initialize();
|
||||
const gameView = new GameView(
|
||||
worker,
|
||||
config,
|
||||
gameMap.gameMap,
|
||||
lobbyConfig.clientID,
|
||||
);
|
||||
|
||||
const gameMap = await loadTerrainMap(gameConfig.gameMap);
|
||||
const worker = new WorkerClient(lobbyConfig.gameID, gameConfig)
|
||||
await worker.initialize()
|
||||
const gameView = new GameView(worker, config, gameMap.gameMap, lobbyConfig.clientID)
|
||||
consolex.log("going to init path finder");
|
||||
consolex.log("inited path finder");
|
||||
const canvas = createCanvas();
|
||||
let gameRenderer = createRenderer(
|
||||
canvas,
|
||||
gameView,
|
||||
eventBus,
|
||||
lobbyConfig.clientID,
|
||||
);
|
||||
|
||||
consolex.log(
|
||||
`creating private game got difficulty: ${gameConfig.difficulty}`,
|
||||
);
|
||||
|
||||
consolex.log('going to init path finder')
|
||||
consolex.log('inited path finder')
|
||||
const canvas = createCanvas()
|
||||
let gameRenderer = createRenderer(canvas, gameView, eventBus, lobbyConfig.clientID)
|
||||
|
||||
|
||||
consolex.log(`creating private game got difficulty: ${gameConfig.difficulty}`)
|
||||
|
||||
return new ClientGameRunner(
|
||||
lobbyConfig.clientID,
|
||||
eventBus,
|
||||
gameRenderer,
|
||||
new InputHandler(canvas, eventBus),
|
||||
transport,
|
||||
worker,
|
||||
gameView
|
||||
)
|
||||
return new ClientGameRunner(
|
||||
lobbyConfig.clientID,
|
||||
eventBus,
|
||||
gameRenderer,
|
||||
new InputHandler(canvas, eventBus),
|
||||
transport,
|
||||
worker,
|
||||
gameView,
|
||||
);
|
||||
}
|
||||
|
||||
export class ClientGameRunner {
|
||||
private myPlayer: PlayerView
|
||||
private isActive = false
|
||||
private myPlayer: PlayerView;
|
||||
private isActive = false;
|
||||
|
||||
private turnsSeen = 0
|
||||
private hasJoined = false
|
||||
private turnsSeen = 0;
|
||||
private hasJoined = false;
|
||||
|
||||
constructor(
|
||||
private clientID: ClientID,
|
||||
private eventBus: EventBus,
|
||||
private renderer: GameRenderer,
|
||||
private input: InputHandler,
|
||||
private transport: Transport,
|
||||
private worker: WorkerClient,
|
||||
private gameView: GameView
|
||||
) { }
|
||||
constructor(
|
||||
private clientID: ClientID,
|
||||
private eventBus: EventBus,
|
||||
private renderer: GameRenderer,
|
||||
private input: InputHandler,
|
||||
private transport: Transport,
|
||||
private worker: WorkerClient,
|
||||
private gameView: GameView,
|
||||
) {}
|
||||
|
||||
public start() {
|
||||
consolex.log('starting client game')
|
||||
this.isActive = true
|
||||
this.eventBus.on(MouseUpEvent, (e) => this.inputEvent(e))
|
||||
public start() {
|
||||
consolex.log("starting client game");
|
||||
this.isActive = true;
|
||||
this.eventBus.on(MouseUpEvent, (e) => this.inputEvent(e));
|
||||
|
||||
this.renderer.initialize()
|
||||
this.input.initialize()
|
||||
this.worker.start((gu: GameUpdateViewData | ErrorUpdate) => {
|
||||
if ('errMsg' in gu) {
|
||||
showErrorModal(gu.errMsg, gu.stack, this.clientID)
|
||||
return
|
||||
}
|
||||
this.gameView.update(gu)
|
||||
this.renderer.tick()
|
||||
})
|
||||
const worker = this.worker
|
||||
const keepWorkerAlive = () => {
|
||||
worker.sendHeartbeat
|
||||
requestAnimationFrame(keepWorkerAlive)
|
||||
this.renderer.initialize();
|
||||
this.input.initialize();
|
||||
this.worker.start((gu: GameUpdateViewData | ErrorUpdate) => {
|
||||
if ("errMsg" in gu) {
|
||||
showErrorModal(gu.errMsg, gu.stack, this.clientID);
|
||||
return;
|
||||
}
|
||||
this.gameView.update(gu);
|
||||
this.renderer.tick();
|
||||
});
|
||||
const worker = this.worker;
|
||||
const keepWorkerAlive = () => {
|
||||
worker.sendHeartbeat;
|
||||
requestAnimationFrame(keepWorkerAlive);
|
||||
};
|
||||
requestAnimationFrame(keepWorkerAlive);
|
||||
|
||||
const onconnect = () => {
|
||||
consolex.log("Connected to game server!");
|
||||
this.transport.joinGame(this.turnsSeen);
|
||||
};
|
||||
const onmessage = (message: ServerMessage) => {
|
||||
if (message.type == "start") {
|
||||
this.hasJoined = true;
|
||||
consolex.log("starting game!");
|
||||
for (const turn of message.turns) {
|
||||
if (turn.turnNumber < this.turnsSeen) {
|
||||
continue;
|
||||
}
|
||||
this.worker.sendTurn(turn);
|
||||
this.turnsSeen++;
|
||||
}
|
||||
requestAnimationFrame(keepWorkerAlive)
|
||||
}
|
||||
if (message.type == "turn") {
|
||||
if (!this.hasJoined) {
|
||||
this.transport.joinGame(0);
|
||||
return;
|
||||
}
|
||||
if (this.turnsSeen != message.turn.turnNumber) {
|
||||
consolex.error(
|
||||
`got wrong turn have turns ${this.turnsSeen}, received turn ${message.turn.turnNumber}`,
|
||||
);
|
||||
} else {
|
||||
this.worker.sendTurn(message.turn);
|
||||
this.turnsSeen++;
|
||||
}
|
||||
}
|
||||
};
|
||||
this.transport.connect(onconnect, onmessage);
|
||||
}
|
||||
|
||||
const onconnect = () => {
|
||||
consolex.log('Connected to game server!');
|
||||
this.transport.joinGame(this.turnsSeen)
|
||||
};
|
||||
const onmessage = (message: ServerMessage) => {
|
||||
if (message.type == "start") {
|
||||
this.hasJoined = true
|
||||
consolex.log("starting game!")
|
||||
for (const turn of message.turns) {
|
||||
if (turn.turnNumber < this.turnsSeen) {
|
||||
continue
|
||||
}
|
||||
this.worker.sendTurn(turn)
|
||||
this.turnsSeen++
|
||||
}
|
||||
}
|
||||
if (message.type == "turn") {
|
||||
if (!this.hasJoined) {
|
||||
this.transport.joinGame(0)
|
||||
return
|
||||
}
|
||||
if (this.turnsSeen != message.turn.turnNumber) {
|
||||
consolex.error(`got wrong turn have turns ${this.turnsSeen}, received turn ${message.turn.turnNumber}`)
|
||||
} else {
|
||||
this.worker.sendTurn(message.turn)
|
||||
this.turnsSeen++
|
||||
}
|
||||
}
|
||||
};
|
||||
this.transport.connect(onconnect, onmessage)
|
||||
public stop() {
|
||||
this.worker.cleanup();
|
||||
this.isActive = false;
|
||||
this.transport.leaveGame();
|
||||
}
|
||||
|
||||
private inputEvent(event: MouseUpEvent) {
|
||||
if (!this.isActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
public stop() {
|
||||
this.worker.cleanup()
|
||||
this.isActive = false
|
||||
this.transport.leaveGame()
|
||||
const cell = this.renderer.transformHandler.screenToWorldCoordinates(
|
||||
event.x,
|
||||
event.y,
|
||||
);
|
||||
if (!this.gameView.isValidCoord(cell.x, cell.y)) {
|
||||
return;
|
||||
}
|
||||
|
||||
private inputEvent(event: MouseUpEvent) {
|
||||
if (!this.isActive) {
|
||||
return
|
||||
}
|
||||
const cell = this.renderer.transformHandler.screenToWorldCoordinates(event.x, event.y)
|
||||
if (!this.gameView.isValidCoord(cell.x, cell.y)) {
|
||||
return
|
||||
}
|
||||
consolex.log(`clicked cell ${cell}`)
|
||||
const tile = this.gameView.ref(cell.x, cell.y)
|
||||
if (this.gameView.isLand(tile) && !this.gameView.hasOwner(tile) && this.gameView.inSpawnPhase()) {
|
||||
this.eventBus.emit(new SendSpawnIntentEvent(cell))
|
||||
return
|
||||
}
|
||||
if (this.gameView.inSpawnPhase()) {
|
||||
return
|
||||
}
|
||||
if (this.myPlayer == null) {
|
||||
this.myPlayer = this.gameView.playerByClientID(this.clientID)
|
||||
if (this.myPlayer == null) {
|
||||
return
|
||||
}
|
||||
}
|
||||
this.myPlayer.actions(tile).then(actions => {
|
||||
console.log(`got actions: ${JSON.stringify(actions)}`)
|
||||
if (actions.canAttack) {
|
||||
this.eventBus.emit(
|
||||
new SendAttackIntentEvent(
|
||||
this.gameView.owner(tile).id(),
|
||||
this.myPlayer.troops() * this.renderer.uiState.attackRatio
|
||||
)
|
||||
)
|
||||
}
|
||||
})
|
||||
consolex.log(`clicked cell ${cell}`);
|
||||
const tile = this.gameView.ref(cell.x, cell.y);
|
||||
if (
|
||||
this.gameView.isLand(tile) &&
|
||||
!this.gameView.hasOwner(tile) &&
|
||||
this.gameView.inSpawnPhase()
|
||||
) {
|
||||
this.eventBus.emit(new SendSpawnIntentEvent(cell));
|
||||
return;
|
||||
}
|
||||
if (this.gameView.inSpawnPhase()) {
|
||||
return;
|
||||
}
|
||||
if (this.myPlayer == null) {
|
||||
this.myPlayer = this.gameView.playerByClientID(this.clientID);
|
||||
if (this.myPlayer == null) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.myPlayer.actions(tile).then((actions) => {
|
||||
console.log(`got actions: ${JSON.stringify(actions)}`);
|
||||
if (actions.canAttack) {
|
||||
this.eventBus.emit(
|
||||
new SendAttackIntentEvent(
|
||||
this.gameView.owner(tile).id(),
|
||||
this.myPlayer.troops() * this.renderer.uiState.attackRatio,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function showErrorModal(errMsg: string, stack: string, clientID: ClientID) {
|
||||
const errorText = `Error: ${errMsg}\nStack: ${stack}`;
|
||||
consolex.error(errorText);
|
||||
const errorText = `Error: ${errMsg}\nStack: ${stack}`;
|
||||
consolex.error(errorText);
|
||||
|
||||
const modal = document.createElement('div');
|
||||
const content = `Game crashed! client id: ${clientID}\nPlease paste the following in your bug report in Discord:\n${errorText}`;
|
||||
const modal = document.createElement("div");
|
||||
const content = `Game crashed! client id: ${clientID}\nPlease paste the following in your bug report in Discord:\n${errorText}`;
|
||||
|
||||
// Create elements
|
||||
const pre = document.createElement('pre');
|
||||
pre.textContent = content;
|
||||
// Create elements
|
||||
const pre = document.createElement("pre");
|
||||
pre.textContent = content;
|
||||
|
||||
const button = document.createElement('button');
|
||||
button.textContent = 'Copy to clipboard';
|
||||
button.style.cssText = 'padding: 8px 16px; margin-top: 10px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer;';
|
||||
button.addEventListener('click', () => {
|
||||
navigator.clipboard.writeText(content)
|
||||
.then(() => button.textContent = 'Copied!')
|
||||
.catch(() => button.textContent = 'Failed to copy');
|
||||
});
|
||||
const button = document.createElement("button");
|
||||
button.textContent = "Copy to clipboard";
|
||||
button.style.cssText =
|
||||
"padding: 8px 16px; margin-top: 10px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer;";
|
||||
button.addEventListener("click", () => {
|
||||
navigator.clipboard
|
||||
.writeText(content)
|
||||
.then(() => (button.textContent = "Copied!"))
|
||||
.catch(() => (button.textContent = "Failed to copy"));
|
||||
});
|
||||
|
||||
// Add to modal
|
||||
modal.style.cssText = 'position:fixed; padding:20px; background:white; border:1px solid black; top:50%; left:50%; transform:translate(-50%,-50%); z-index:9999;';
|
||||
modal.appendChild(pre);
|
||||
modal.appendChild(button);
|
||||
// Add to modal
|
||||
modal.style.cssText =
|
||||
"position:fixed; padding:20px; background:white; border:1px solid black; top:50%; left:50%; transform:translate(-50%,-50%); z-index:9999;";
|
||||
modal.appendChild(pre);
|
||||
modal.appendChild(button);
|
||||
|
||||
document.body.appendChild(modal);
|
||||
}
|
||||
document.body.appendChild(modal);
|
||||
}
|
||||
|
||||
+118
-78
@@ -1,19 +1,19 @@
|
||||
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 { consolex } from '../core/Consolex';
|
||||
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 { consolex } from "../core/Consolex";
|
||||
|
||||
@customElement('host-lobby-modal')
|
||||
@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 lobbyId = '';
|
||||
@state() private lobbyId = "";
|
||||
@state() private copySuccess = false;
|
||||
@state() private players: string[] = []
|
||||
@state() private players: string[] = [];
|
||||
|
||||
private playersInterval = null
|
||||
private playersInterval = null;
|
||||
|
||||
static styles = css`
|
||||
.modal-overlay {
|
||||
@@ -101,113 +101,154 @@ export class HostLobbyModal extends LitElement {
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="modal-overlay" style="display: ${this.isModalOpen ? 'block' : 'none'}">
|
||||
<div
|
||||
class="modal-overlay"
|
||||
style="display: ${this.isModalOpen ? "block" : "none"}"
|
||||
>
|
||||
<div class="modal-content">
|
||||
<span class="close" @click=${this.close}>×</span>
|
||||
<h2>Private Lobby</h2>
|
||||
<div class="lobby-id-container">
|
||||
<h3>Lobby ID: ${this.lobbyId}</h3>
|
||||
<svg @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">
|
||||
<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>
|
||||
<svg
|
||||
@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"
|
||||
>
|
||||
<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>` : ''}
|
||||
${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}>
|
||||
${key}
|
||||
</option>
|
||||
`)}
|
||||
.filter(([key]) => isNaN(Number(key)))
|
||||
.map(
|
||||
([key, value]) => html`
|
||||
<option
|
||||
value=${value}
|
||||
?selected=${this.selectedMap === value}
|
||||
>
|
||||
${key}
|
||||
</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.selectedDiffculty === value}>
|
||||
${key}
|
||||
</option>
|
||||
`)}
|
||||
.filter(([key]) => isNaN(Number(key)))
|
||||
.map(
|
||||
([key, value]) => html`
|
||||
<option
|
||||
value=${value}
|
||||
?selected=${this.selectedDiffculty === value}
|
||||
>
|
||||
${key}
|
||||
</option>
|
||||
`,
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
<button @click=${this.startGame}>Start Game</button>
|
||||
<div>
|
||||
<p>Players: ${this.players.join(", ")}<p>
|
||||
</div>
|
||||
<div>
|
||||
<p>Players: ${this.players.join(", ")}</p>
|
||||
<p></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
public open() {
|
||||
createLobby().then((lobby) => {
|
||||
this.lobbyId = lobby.id
|
||||
// join lobby
|
||||
}).then(() => {
|
||||
this.dispatchEvent(new CustomEvent('join-lobby', {
|
||||
detail: {
|
||||
gameType: GameType.Private,
|
||||
lobby: {
|
||||
id: this.lobbyId,
|
||||
},
|
||||
map: this.selectedMap,
|
||||
difficulty: this.selectedDiffculty,
|
||||
},
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
|
||||
})
|
||||
createLobby()
|
||||
.then((lobby) => {
|
||||
this.lobbyId = lobby.id;
|
||||
// join lobby
|
||||
})
|
||||
.then(() => {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("join-lobby", {
|
||||
detail: {
|
||||
gameType: GameType.Private,
|
||||
lobby: {
|
||||
id: this.lobbyId,
|
||||
},
|
||||
map: this.selectedMap,
|
||||
difficulty: this.selectedDiffculty,
|
||||
},
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
this.isModalOpen = true;
|
||||
this.playersInterval = setInterval(() => this.pollPlayers(), 1000)
|
||||
this.playersInterval = setInterval(() => this.pollPlayers(), 1000);
|
||||
}
|
||||
|
||||
public close() {
|
||||
this.isModalOpen = false;
|
||||
this.copySuccess = false;
|
||||
if (this.playersInterval) {
|
||||
clearInterval(this.playersInterval)
|
||||
this.playersInterval = null
|
||||
clearInterval(this.playersInterval);
|
||||
this.playersInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async handleMapChange(e: Event) {
|
||||
this.selectedMap = String((e.target as HTMLSelectElement).value) as GameMapType;
|
||||
consolex.log(`updating map to ${this.selectedMap}`)
|
||||
this.putGameConfig()
|
||||
this.selectedMap = String(
|
||||
(e.target as HTMLSelectElement).value,
|
||||
) as GameMapType;
|
||||
consolex.log(`updating map to ${this.selectedMap}`);
|
||||
this.putGameConfig();
|
||||
}
|
||||
|
||||
private async handleDifficultyChange(e: Event) {
|
||||
this.selectedDiffculty = String((e.target as HTMLSelectElement).value) as Difficulty;
|
||||
consolex.log(`updating difficulty to ${this.selectedDiffculty}`)
|
||||
this.putGameConfig()
|
||||
this.selectedDiffculty = String(
|
||||
(e.target as HTMLSelectElement).value,
|
||||
) as Difficulty;
|
||||
consolex.log(`updating difficulty to ${this.selectedDiffculty}`);
|
||||
this.putGameConfig();
|
||||
}
|
||||
|
||||
private async putGameConfig() {
|
||||
const response = await fetch(`/private_lobby/${this.lobbyId}`, {
|
||||
method: 'PUT',
|
||||
method: "PUT",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ gameMap: this.selectedMap, difficulty: this.selectedDiffculty })
|
||||
body: JSON.stringify({
|
||||
gameMap: this.selectedMap,
|
||||
difficulty: this.selectedDiffculty,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
private async startGame() {
|
||||
consolex.log(`Starting private game with map: ${GameMapType[this.selectedMap]}`);
|
||||
consolex.log(
|
||||
`Starting private game with map: ${GameMapType[this.selectedMap]}`,
|
||||
);
|
||||
this.close();
|
||||
const response = await fetch(`/start_private_lobby/${this.lobbyId}`, {
|
||||
method: 'POST',
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -219,31 +260,31 @@ 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',
|
||||
method: "GET",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
}).then(response => response.json())
|
||||
.then(data => {
|
||||
console.log(`got response: ${data}`)
|
||||
this.players = data.players.map(p => p.username)
|
||||
})
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
console.log(`got response: ${data}`);
|
||||
this.players = data.players.map((p) => p.username);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function createLobby(): Promise<Lobby> {
|
||||
try {
|
||||
const response = await fetch('/private_lobby', {
|
||||
method: 'POST',
|
||||
const response = await fetch("/private_lobby", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
// body: JSON.stringify(data), // Include this if you need to send data
|
||||
});
|
||||
@@ -253,7 +294,7 @@ async function createLobby(): Promise<Lobby> {
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
consolex.log('Success:', data);
|
||||
consolex.log("Success:", data);
|
||||
|
||||
// Assuming the server returns an object with an 'id' property
|
||||
const lobby: Lobby = {
|
||||
@@ -263,8 +304,7 @@ async function createLobby(): Promise<Lobby> {
|
||||
|
||||
return lobby;
|
||||
} catch (error) {
|
||||
consolex.error('Error creating lobby:', error);
|
||||
consolex.error("Error creating lobby:", error);
|
||||
throw error; // Re-throw the error so the caller can handle it
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,31 +2,46 @@ import { EventBus, GameEvent } from "../core/EventBus";
|
||||
import { Game } from "../core/game/Game";
|
||||
|
||||
export class MouseUpEvent implements GameEvent {
|
||||
constructor(public readonly x: number, public readonly y: number) {}
|
||||
constructor(
|
||||
public readonly x: number,
|
||||
public readonly y: number,
|
||||
) {}
|
||||
}
|
||||
|
||||
export class MouseDownEvent implements GameEvent {
|
||||
constructor(public readonly x: number, public readonly y: number) {}
|
||||
constructor(
|
||||
public readonly x: number,
|
||||
public readonly y: number,
|
||||
) {}
|
||||
}
|
||||
|
||||
export class MouseMoveEvent implements GameEvent {
|
||||
constructor(public readonly x: number, public readonly y: number) {}
|
||||
constructor(
|
||||
public readonly x: number,
|
||||
public readonly y: number,
|
||||
) {}
|
||||
}
|
||||
|
||||
export class ContextMenuEvent implements GameEvent {
|
||||
constructor(public readonly x: number, public readonly y: number) {}
|
||||
constructor(
|
||||
public readonly x: number,
|
||||
public readonly y: number,
|
||||
) {}
|
||||
}
|
||||
|
||||
export class ZoomEvent implements GameEvent {
|
||||
constructor(
|
||||
public readonly x: number,
|
||||
public readonly y: number,
|
||||
public readonly delta: number
|
||||
public readonly delta: number,
|
||||
) {}
|
||||
}
|
||||
|
||||
export class DragEvent implements GameEvent {
|
||||
constructor(public readonly deltaX: number, public readonly deltaY: number) {}
|
||||
constructor(
|
||||
public readonly deltaX: number,
|
||||
public readonly deltaY: number,
|
||||
) {}
|
||||
}
|
||||
|
||||
export class AlternateViewEvent implements GameEvent {
|
||||
@@ -36,7 +51,10 @@ export class AlternateViewEvent implements GameEvent {
|
||||
export class RefreshGraphicsEvent implements GameEvent {}
|
||||
|
||||
export class ShowBuildMenuEvent implements GameEvent {
|
||||
constructor(public readonly x: number, public readonly y: number) {}
|
||||
constructor(
|
||||
public readonly x: number,
|
||||
public readonly y: number,
|
||||
) {}
|
||||
}
|
||||
|
||||
export class InputHandler {
|
||||
@@ -54,7 +72,10 @@ export class InputHandler {
|
||||
|
||||
private alternateView = false;
|
||||
|
||||
constructor(private canvas: HTMLCanvasElement, private eventBus: EventBus) {}
|
||||
constructor(
|
||||
private canvas: HTMLCanvasElement,
|
||||
private eventBus: EventBus,
|
||||
) {}
|
||||
|
||||
initialize() {
|
||||
this.canvas.addEventListener("pointerdown", (e) => this.onPointerDown(e));
|
||||
@@ -175,7 +196,7 @@ export class InputHandler {
|
||||
// Threshold to avoid tiny zoom adjustments
|
||||
const zoomCenter = this.getPinchCenter();
|
||||
this.eventBus.emit(
|
||||
new ZoomEvent(zoomCenter.x, zoomCenter.y, -pinchDelta * 2)
|
||||
new ZoomEvent(zoomCenter.x, zoomCenter.y, -pinchDelta * 2),
|
||||
);
|
||||
this.lastPinchDistance = currentPinchDistance;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { LitElement, html, css } from 'lit';
|
||||
import { customElement, property, state, query } from 'lit/decorators.js';
|
||||
import { GameMapType, GameType } from '../core/game/Game';
|
||||
import { consolex } from '../core/Consolex';
|
||||
import { LitElement, html, css } from "lit";
|
||||
import { customElement, property, state, query } from "lit/decorators.js";
|
||||
import { GameMapType, GameType } from "../core/game/Game";
|
||||
import { consolex } from "../core/Consolex";
|
||||
|
||||
@customElement('join-private-lobby-modal')
|
||||
@customElement("join-private-lobby-modal")
|
||||
export class JoinPrivateLobbyModal extends LitElement {
|
||||
@state() private isModalOpen = false;
|
||||
@state() private message: string = '';
|
||||
@query('#lobbyIdInput') private lobbyIdInput!: HTMLInputElement;
|
||||
@state() private hasJoined = false
|
||||
@state() private message: string = "";
|
||||
@query("#lobbyIdInput") private lobbyIdInput!: HTMLInputElement;
|
||||
@state() private hasJoined = false;
|
||||
|
||||
static styles = css`
|
||||
.modal-overlay {
|
||||
@@ -78,51 +78,58 @@ export class JoinPrivateLobbyModal extends LitElement {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.message-area {
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
transition: opacity 0.3s ease;
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.message-area {
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
transition: opacity 0.3s ease;
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.message-area.show {
|
||||
opacity: 1;
|
||||
height: auto;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.message-area.show {
|
||||
opacity: 1;
|
||||
height: auto;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.message-area.error {
|
||||
background-color: #ffebee;
|
||||
color: #c62828;
|
||||
}
|
||||
.message-area.error {
|
||||
background-color: #ffebee;
|
||||
color: #c62828;
|
||||
}
|
||||
|
||||
.message-area.success {
|
||||
background-color: #e8f5e9;
|
||||
color: #2e7d32;
|
||||
}
|
||||
.message-area.success {
|
||||
background-color: #e8f5e9;
|
||||
color: #2e7d32;
|
||||
}
|
||||
`;
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="modal-overlay" style="display: ${this.isModalOpen ? 'block' : 'none'}">
|
||||
<div class="modal-content">
|
||||
<span class="close" @click=${this.closeAndLeave}>×</span>
|
||||
<h2>Join Private Lobby</h2>
|
||||
<div class="lobby-id-container">
|
||||
<input type="text" id="lobbyIdInput" placeholder="Enter Lobby ID">
|
||||
<button @click=${this.pasteFromClipboard}>Paste</button>
|
||||
<div
|
||||
class="modal-overlay"
|
||||
style="display: ${this.isModalOpen ? "block" : "none"}"
|
||||
>
|
||||
<div class="modal-content">
|
||||
<span class="close" @click=${this.closeAndLeave}>×</span>
|
||||
<h2>Join Private Lobby</h2>
|
||||
<div class="lobby-id-container">
|
||||
<input type="text" id="lobbyIdInput" placeholder="Enter Lobby ID" />
|
||||
<button @click=${this.pasteFromClipboard}>Paste</button>
|
||||
</div>
|
||||
<div class="message-area ${this.message ? "show" : ""}">
|
||||
${this.message}
|
||||
</div>
|
||||
${!this.hasJoined
|
||||
? html`<button class="join-button" @click=${this.joinLobby}>
|
||||
Join Lobby
|
||||
</button>`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="message-area ${this.message ? 'show' : ''}">
|
||||
${this.message}
|
||||
</div>
|
||||
${!this.hasJoined ? html`<button class="join-button" @click=${this.joinLobby}>Join Lobby</button>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
`;
|
||||
}
|
||||
|
||||
public open() {
|
||||
@@ -131,18 +138,20 @@ export class JoinPrivateLobbyModal extends LitElement {
|
||||
|
||||
public close() {
|
||||
this.isModalOpen = false;
|
||||
this.lobbyIdInput.value = null
|
||||
this.lobbyIdInput.value = null;
|
||||
}
|
||||
|
||||
public closeAndLeave() {
|
||||
this.close()
|
||||
this.hasJoined = false
|
||||
this.message = ""
|
||||
this.dispatchEvent(new CustomEvent('leave-lobby', {
|
||||
detail: { lobby: this.lobbyIdInput.value },
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
this.close();
|
||||
this.hasJoined = false;
|
||||
this.message = "";
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("leave-lobby", {
|
||||
detail: { lobby: this.lobbyIdInput.value },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private async pasteFromClipboard() {
|
||||
@@ -150,43 +159,44 @@ export class JoinPrivateLobbyModal extends LitElement {
|
||||
const clipText = await navigator.clipboard.readText();
|
||||
this.lobbyIdInput.value = clipText;
|
||||
} catch (err) {
|
||||
consolex.error('Failed to read clipboard contents: ', err);
|
||||
consolex.error("Failed to read clipboard contents: ", err);
|
||||
}
|
||||
}
|
||||
|
||||
private joinLobby() {
|
||||
const lobbyId = this.lobbyIdInput.value;
|
||||
consolex.log(`Joining lobby with ID: ${lobbyId}`);
|
||||
this.message = 'Checking lobby...'; // Set initial message
|
||||
this.message = "Checking lobby..."; // Set initial message
|
||||
|
||||
fetch(`/lobby/${lobbyId}/exists`, {
|
||||
method: 'GET',
|
||||
method: "GET",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
if (data.exists) {
|
||||
this.message = 'Joined successfully! Waiting for game to start...';
|
||||
this.hasJoined = true
|
||||
this.dispatchEvent(new CustomEvent('join-lobby', {
|
||||
detail: {
|
||||
lobby: { id: lobbyId },
|
||||
gameType: GameType.Private,
|
||||
map: GameMapType.World,
|
||||
},
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
this.message = "Joined successfully! Waiting for game to start...";
|
||||
this.hasJoined = true;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("join-lobby", {
|
||||
detail: {
|
||||
lobby: { id: lobbyId },
|
||||
gameType: GameType.Private,
|
||||
map: GameMapType.World,
|
||||
},
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
this.message = 'Lobby not found. Please check the ID and try again.';
|
||||
this.message = "Lobby not found. Please check the ID and try again.";
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
consolex.error('Error checking lobby existence:', error);
|
||||
this.message = 'An error occurred. Please try again.';
|
||||
.catch((error) => {
|
||||
consolex.error("Error checking lobby existence:", error);
|
||||
this.message = "An error occurred. Please try again.";
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
+109
-90
@@ -1,111 +1,130 @@
|
||||
import { Config, GameEnv, ServerConfig } from "../core/configuration/Config";
|
||||
import { consolex } from "../core/Consolex";
|
||||
import { GameEvent } from "../core/EventBus";
|
||||
import { ClientID, ClientMessage, ClientMessageSchema, GameConfig, GameID, GameRecordSchema, Intent, PlayerRecord, ServerMessage, ServerStartGameMessageSchema, ServerTurnMessageSchema, Turn } from "../core/Schemas";
|
||||
import {
|
||||
ClientID,
|
||||
ClientMessage,
|
||||
ClientMessageSchema,
|
||||
GameConfig,
|
||||
GameID,
|
||||
GameRecordSchema,
|
||||
Intent,
|
||||
PlayerRecord,
|
||||
ServerMessage,
|
||||
ServerStartGameMessageSchema,
|
||||
ServerTurnMessageSchema,
|
||||
Turn,
|
||||
} from "../core/Schemas";
|
||||
import { CreateGameRecord, generateID } from "../core/Util";
|
||||
import { LobbyConfig } from "./ClientGameRunner";
|
||||
import { getPersistentIDFromCookie } from "./Main";
|
||||
|
||||
|
||||
export class LocalServer {
|
||||
private turns: Turn[] = []
|
||||
private intents: Intent[] = []
|
||||
private startedAt: number
|
||||
private turns: Turn[] = [];
|
||||
private intents: Intent[] = [];
|
||||
private startedAt: number;
|
||||
|
||||
private endTurnIntervalID
|
||||
private endTurnIntervalID;
|
||||
|
||||
private paused = false
|
||||
private paused = false;
|
||||
|
||||
private winner: ClientID | null = null
|
||||
private winner: ClientID | null = null;
|
||||
|
||||
constructor(
|
||||
private serverConfig: ServerConfig,
|
||||
private gameConfig: GameConfig,
|
||||
private lobbyConfig: LobbyConfig,
|
||||
private clientConnect: () => void,
|
||||
private clientMessage: (message: ServerMessage) => void,
|
||||
) {}
|
||||
|
||||
constructor(
|
||||
private serverConfig: ServerConfig,
|
||||
private gameConfig: GameConfig,
|
||||
private lobbyConfig: LobbyConfig,
|
||||
private clientConnect: () => void,
|
||||
private clientMessage: (message: ServerMessage) => void
|
||||
) {
|
||||
}
|
||||
start() {
|
||||
this.startedAt = Date.now();
|
||||
this.endTurnIntervalID = setInterval(
|
||||
() => this.endTurn(),
|
||||
this.serverConfig.turnIntervalMs(),
|
||||
);
|
||||
this.clientConnect();
|
||||
this.clientMessage(
|
||||
ServerStartGameMessageSchema.parse({
|
||||
type: "start",
|
||||
config: this.gameConfig,
|
||||
turns: [],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
start() {
|
||||
this.startedAt = Date.now()
|
||||
this.endTurnIntervalID = setInterval(() => this.endTurn(), this.serverConfig.turnIntervalMs());
|
||||
this.clientConnect()
|
||||
this.clientMessage(ServerStartGameMessageSchema.parse({
|
||||
type: "start",
|
||||
config: this.gameConfig,
|
||||
turns: [],
|
||||
}))
|
||||
}
|
||||
pause() {
|
||||
this.paused = true;
|
||||
}
|
||||
|
||||
pause() {
|
||||
this.paused = true
|
||||
}
|
||||
resume() {
|
||||
this.paused = false;
|
||||
}
|
||||
|
||||
resume() {
|
||||
this.paused = false
|
||||
}
|
||||
|
||||
onMessage(message: string) {
|
||||
const clientMsg: ClientMessage = ClientMessageSchema.parse(JSON.parse(message))
|
||||
if (clientMsg.type == "intent") {
|
||||
if (this.paused) {
|
||||
if (clientMsg.intent.type == "troop_ratio") {
|
||||
// Store troop change events because otherwise they are
|
||||
// not registered when game is paused.
|
||||
this.intents.push(clientMsg.intent)
|
||||
}
|
||||
return
|
||||
}
|
||||
this.intents.push(clientMsg.intent)
|
||||
}
|
||||
if (clientMsg.type == "winner") {
|
||||
this.winner = clientMsg.winner
|
||||
onMessage(message: string) {
|
||||
const clientMsg: ClientMessage = ClientMessageSchema.parse(
|
||||
JSON.parse(message),
|
||||
);
|
||||
if (clientMsg.type == "intent") {
|
||||
if (this.paused) {
|
||||
if (clientMsg.intent.type == "troop_ratio") {
|
||||
// Store troop change events because otherwise they are
|
||||
// not registered when game is paused.
|
||||
this.intents.push(clientMsg.intent);
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.intents.push(clientMsg.intent);
|
||||
}
|
||||
if (clientMsg.type == "winner") {
|
||||
this.winner = clientMsg.winner;
|
||||
}
|
||||
}
|
||||
|
||||
private endTurn() {
|
||||
if (this.paused) {
|
||||
return
|
||||
}
|
||||
const pastTurn: Turn = {
|
||||
turnNumber: this.turns.length,
|
||||
gameID: this.lobbyConfig.gameID,
|
||||
intents: this.intents
|
||||
}
|
||||
this.turns.push(pastTurn)
|
||||
this.intents = []
|
||||
this.clientMessage({
|
||||
type: "turn",
|
||||
turn: pastTurn
|
||||
})
|
||||
private endTurn() {
|
||||
if (this.paused) {
|
||||
return;
|
||||
}
|
||||
const pastTurn: Turn = {
|
||||
turnNumber: this.turns.length,
|
||||
gameID: this.lobbyConfig.gameID,
|
||||
intents: this.intents,
|
||||
};
|
||||
this.turns.push(pastTurn);
|
||||
this.intents = [];
|
||||
this.clientMessage({
|
||||
type: "turn",
|
||||
turn: pastTurn,
|
||||
});
|
||||
}
|
||||
|
||||
public endGame() {
|
||||
consolex.log('local server ending game')
|
||||
clearInterval(this.endTurnIntervalID)
|
||||
const players: PlayerRecord[] = [{
|
||||
ip: null,
|
||||
persistentID: getPersistentIDFromCookie(),
|
||||
username: this.lobbyConfig.playerName(),
|
||||
clientID: this.lobbyConfig.clientID
|
||||
}]
|
||||
const record = CreateGameRecord(
|
||||
this.lobbyConfig.gameID,
|
||||
this.gameConfig,
|
||||
players,
|
||||
this.turns,
|
||||
this.startedAt,
|
||||
Date.now(),
|
||||
this.winner
|
||||
)
|
||||
// Clear turns because beacon only supports up to 64kb
|
||||
record.turns = []
|
||||
// For unload events, sendBeacon is the only reliable method
|
||||
const blob = new Blob([JSON.stringify(GameRecordSchema.parse(record))], {
|
||||
type: 'application/json'
|
||||
});
|
||||
navigator.sendBeacon('/archive_singleplayer_game', blob);
|
||||
}
|
||||
}
|
||||
public endGame() {
|
||||
consolex.log("local server ending game");
|
||||
clearInterval(this.endTurnIntervalID);
|
||||
const players: PlayerRecord[] = [
|
||||
{
|
||||
ip: null,
|
||||
persistentID: getPersistentIDFromCookie(),
|
||||
username: this.lobbyConfig.playerName(),
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
},
|
||||
];
|
||||
const record = CreateGameRecord(
|
||||
this.lobbyConfig.gameID,
|
||||
this.gameConfig,
|
||||
players,
|
||||
this.turns,
|
||||
this.startedAt,
|
||||
Date.now(),
|
||||
this.winner,
|
||||
);
|
||||
// Clear turns because beacon only supports up to 64kb
|
||||
record.turns = [];
|
||||
// For unload events, sendBeacon is the only reliable method
|
||||
const blob = new Blob([JSON.stringify(GameRecordSchema.parse(record))], {
|
||||
type: "application/json",
|
||||
});
|
||||
navigator.sendBeacon("/archive_singleplayer_game", blob);
|
||||
}
|
||||
}
|
||||
|
||||
+118
-105
@@ -1,10 +1,10 @@
|
||||
import { ClientGameRunner, joinLobby } from "./ClientGameRunner";
|
||||
import backgroundImage from '../../resources/images/EuropeBackground.png';
|
||||
import favicon from '../../resources/images/Favicon.svg';
|
||||
import backgroundImage from "../../resources/images/EuropeBackground.png";
|
||||
import favicon from "../../resources/images/Favicon.svg";
|
||||
|
||||
import './PublicLobby';
|
||||
import './UsernameInput';
|
||||
import './styles.css';
|
||||
import "./PublicLobby";
|
||||
import "./UsernameInput";
|
||||
import "./styles.css";
|
||||
import { UsernameInput } from "./UsernameInput";
|
||||
import { SinglePlayerModal } from "./SinglePlayerModal";
|
||||
import { HostLobbyModal as HostPrivateLobbyModal } from "./HostLobbyModal";
|
||||
@@ -14,130 +14,143 @@ import { generateCryptoRandomUUID } from "./Utils";
|
||||
import { consolex } from "../core/Consolex";
|
||||
|
||||
class Client {
|
||||
private gameStop: () => void
|
||||
private gameStop: () => void;
|
||||
|
||||
private usernameInput: UsernameInput | null = null;
|
||||
private usernameInput: UsernameInput | null = null;
|
||||
|
||||
private joinModal: JoinPrivateLobbyModal
|
||||
constructor() {
|
||||
private joinModal: JoinPrivateLobbyModal;
|
||||
constructor() {}
|
||||
|
||||
initialize(): void {
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
initialize(): void {
|
||||
this.usernameInput = document.querySelector('username-input') as UsernameInput;
|
||||
if (!this.usernameInput) {
|
||||
consolex.warn('Username input element not found');
|
||||
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 hostModal = document.querySelector(
|
||||
"host-lobby-modal",
|
||||
) as HostPrivateLobbyModal;
|
||||
hostModal instanceof HostPrivateLobbyModal;
|
||||
document
|
||||
.getElementById("host-lobby-button")
|
||||
.addEventListener("click", () => {
|
||||
if (this.usernameInput.isValid()) {
|
||||
hostModal.open();
|
||||
}
|
||||
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));
|
||||
|
||||
|
||||
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();
|
||||
}
|
||||
})
|
||||
|
||||
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();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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.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.gameStop = joinLobby(
|
||||
{
|
||||
gameType: event.detail.gameType,
|
||||
playerName: (): string => this.usernameInput.getCurrentUsername(),
|
||||
gameID: lobby.id,
|
||||
persistentID: getPersistentIDFromCookie(),
|
||||
playerID: generateID(),
|
||||
clientID: generateID(),
|
||||
map: event.detail.map,
|
||||
difficulty: event.detail.difficulty,
|
||||
},
|
||||
() => 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 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,
|
||||
playerName: (): string => this.usernameInput.getCurrentUsername(),
|
||||
gameID: lobby.id,
|
||||
persistentID: getPersistentIDFromCookie(),
|
||||
playerID: generateID(),
|
||||
clientID: generateID(),
|
||||
map: event.detail.map,
|
||||
difficulty: event.detail.difficulty,
|
||||
},
|
||||
() => this.joinModal.close(),
|
||||
);
|
||||
}
|
||||
|
||||
private async handleSinglePlayer(event: CustomEvent) {
|
||||
alert('coming soon')
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
+98
-89
@@ -1,100 +1,109 @@
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement, state } from 'lit/decorators.js';
|
||||
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 { Difficulty, GameMapType, GameType } from "../core/game/Game";
|
||||
import { consolex } from "../core/Consolex";
|
||||
|
||||
@customElement('public-lobby')
|
||||
@customElement("public-lobby")
|
||||
export class PublicLobby extends LitElement {
|
||||
@state() private lobbies: Lobby[] = [];
|
||||
@state() private isLobbyHighlighted: boolean = false;
|
||||
private lobbiesInterval: number | null = null;
|
||||
private currLobby: Lobby = null;
|
||||
@state() private lobbies: Lobby[] = [];
|
||||
@state() private isLobbyHighlighted: boolean = false;
|
||||
private lobbiesInterval: number | null = null;
|
||||
private currLobby: Lobby = null;
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.fetchAndUpdateLobbies();
|
||||
this.lobbiesInterval = window.setInterval(
|
||||
() => this.fetchAndUpdateLobbies(),
|
||||
1000,
|
||||
);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
if (this.lobbiesInterval !== null) {
|
||||
clearInterval(this.lobbiesInterval);
|
||||
this.lobbiesInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.fetchAndUpdateLobbies();
|
||||
this.lobbiesInterval = window.setInterval(() => this.fetchAndUpdateLobbies(), 1000);
|
||||
private async fetchAndUpdateLobbies(): Promise<void> {
|
||||
try {
|
||||
const lobbies = await this.fetchLobbies();
|
||||
this.lobbies = lobbies;
|
||||
} catch (error) {
|
||||
consolex.error("Error fetching lobbies:", error);
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
if (this.lobbiesInterval !== null) {
|
||||
clearInterval(this.lobbiesInterval);
|
||||
this.lobbiesInterval = null;
|
||||
}
|
||||
async fetchLobbies(): Promise<Lobby[]> {
|
||||
try {
|
||||
const response = await fetch("/lobbies");
|
||||
if (!response.ok)
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
const data = await response.json();
|
||||
return data.lobbies;
|
||||
} catch (error) {
|
||||
consolex.error("Error fetching lobbies:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchAndUpdateLobbies(): Promise<void> {
|
||||
try {
|
||||
const lobbies = await this.fetchLobbies();
|
||||
this.lobbies = lobbies;
|
||||
} catch (error) {
|
||||
consolex.error('Error fetching lobbies:', error);
|
||||
}
|
||||
render() {
|
||||
if (this.lobbies.length === 0) return html``;
|
||||
|
||||
const lobby = this.lobbies[0];
|
||||
const timeRemaining = Math.max(0, Math.floor(lobby.msUntilStart / 1000));
|
||||
|
||||
return html`
|
||||
<button
|
||||
@click=${() => this.lobbyClicked(lobby)}
|
||||
class="w-full mx-auto p-4 md:p-6 ${this.isLobbyHighlighted
|
||||
? "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="flex flex-col gap-1 md:gap-2 text-blue-100 text-s md:text-lg"
|
||||
>
|
||||
<div>Starts in: ${timeRemaining}s</div>
|
||||
<div>Players: ${lobby.numClients}</div>
|
||||
<div>ID: ${lobby.id}</div>
|
||||
</div>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
private lobbyClicked(lobby: Lobby) {
|
||||
this.isLobbyHighlighted = !this.isLobbyHighlighted;
|
||||
if (this.currLobby == null) {
|
||||
this.currLobby = lobby;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("join-lobby", {
|
||||
detail: {
|
||||
lobby,
|
||||
gameType: GameType.Public,
|
||||
map: GameMapType.World,
|
||||
difficulty: Difficulty.Medium,
|
||||
},
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("leave-lobby", {
|
||||
detail: { lobby: this.currLobby },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
this.currLobby = null;
|
||||
}
|
||||
|
||||
async fetchLobbies(): Promise<Lobby[]> {
|
||||
try {
|
||||
const response = await fetch('/lobbies');
|
||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||
const data = await response.json();
|
||||
return data.lobbies;
|
||||
} catch (error) {
|
||||
consolex.error('Error fetching lobbies:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.lobbies.length === 0) return html``;
|
||||
|
||||
const lobby = this.lobbies[0];
|
||||
const timeRemaining = Math.max(0, Math.floor(lobby.msUntilStart / 1000));
|
||||
|
||||
return html`
|
||||
<button
|
||||
@click=${() => this.lobbyClicked(lobby)}
|
||||
class="w-full mx-auto p-4 md:p-6 ${this.isLobbyHighlighted
|
||||
? '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="flex flex-col gap-1 md:gap-2 text-blue-100 text-s md:text-lg">
|
||||
<div>Starts in: ${timeRemaining}s</div>
|
||||
<div>Players: ${lobby.numClients}</div>
|
||||
<div>ID: ${lobby.id}</div>
|
||||
</div>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
private lobbyClicked(lobby: Lobby) {
|
||||
this.isLobbyHighlighted = !this.isLobbyHighlighted;
|
||||
if (this.currLobby == null) {
|
||||
this.currLobby = lobby;
|
||||
this.dispatchEvent(new CustomEvent('join-lobby', {
|
||||
detail: {
|
||||
lobby,
|
||||
gameType: GameType.Public,
|
||||
map: GameMapType.World,
|
||||
difficulty: Difficulty.Medium,
|
||||
},
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
} else {
|
||||
this.dispatchEvent(new CustomEvent('leave-lobby', {
|
||||
detail: { lobby: this.currLobby },
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
this.currLobby = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { LitElement, html, css } from 'lit';
|
||||
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 { LitElement, html, css } from "lit";
|
||||
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";
|
||||
|
||||
@customElement('single-player-modal')
|
||||
@customElement("single-player-modal")
|
||||
export class SinglePlayerModal extends LitElement {
|
||||
@state() private isModalOpen = false;
|
||||
@state() private selectedMap: GameMapType = GameMapType.World;
|
||||
@@ -74,7 +74,10 @@ export class SinglePlayerModal extends LitElement {
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="modal-overlay" style="display: ${this.isModalOpen ? 'block' : 'none'}">
|
||||
<div
|
||||
class="modal-overlay"
|
||||
style="display: ${this.isModalOpen ? "block" : "none"}"
|
||||
>
|
||||
<div class="modal-content">
|
||||
<span class="close" @click=${this.close}>×</span>
|
||||
<h2>Start Single Player Game</h2>
|
||||
@@ -82,24 +85,34 @@ export class SinglePlayerModal extends LitElement {
|
||||
<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>
|
||||
`)}
|
||||
.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>
|
||||
`)}
|
||||
.filter(([key]) => isNaN(Number(key)))
|
||||
.map(
|
||||
([key, value]) => html`
|
||||
<option
|
||||
value=${value}
|
||||
?selected=${this.selectedDifficulty === value}
|
||||
>
|
||||
${value}
|
||||
</option>
|
||||
`,
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -118,25 +131,33 @@ export class SinglePlayerModal extends LitElement {
|
||||
}
|
||||
|
||||
private handleMapChange(e: Event) {
|
||||
this.selectedMap = String((e.target as HTMLSelectElement).value) as GameMapType;
|
||||
this.selectedMap = String(
|
||||
(e.target as HTMLSelectElement).value,
|
||||
) as GameMapType;
|
||||
}
|
||||
private handleDifficultyChange(e: Event) {
|
||||
this.selectedDifficulty = String((e.target as HTMLSelectElement).value) as Difficulty;
|
||||
this.selectedDifficulty = String(
|
||||
(e.target as HTMLSelectElement).value,
|
||||
) as Difficulty;
|
||||
}
|
||||
private startGame() {
|
||||
consolex.log(`Starting single player game with map: ${GameMapType[this.selectedMap]}`);
|
||||
this.dispatchEvent(new CustomEvent('join-lobby', {
|
||||
detail: {
|
||||
gameType: GameType.Singleplayer,
|
||||
lobby: {
|
||||
id: generateID(),
|
||||
consolex.log(
|
||||
`Starting single player game with map: ${GameMapType[this.selectedMap]}`,
|
||||
);
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("join-lobby", {
|
||||
detail: {
|
||||
gameType: GameType.Singleplayer,
|
||||
lobby: {
|
||||
id: generateID(),
|
||||
},
|
||||
map: this.selectedMap,
|
||||
difficulty: this.selectedDifficulty,
|
||||
},
|
||||
map: this.selectedMap,
|
||||
difficulty: this.selectedDifficulty
|
||||
},
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+438
-381
@@ -1,448 +1,505 @@
|
||||
import { Config, ServerConfig } from "../core/configuration/Config"
|
||||
import { SendLogEvent } from "../core/Consolex"
|
||||
import { EventBus, GameEvent } from "../core/EventBus"
|
||||
import { AllianceRequest, AllPlayers, Cell, GameType, Player, PlayerID, PlayerType, UnitType } from "../core/game/Game"
|
||||
import { ClientID, ClientIntentMessageSchema, ClientJoinMessageSchema, GameID, Intent, ServerMessage, ServerMessageSchema, ClientPingMessageSchema, GameConfig, ClientLogMessageSchema, ClientSendWinnerSchema } from "../core/Schemas"
|
||||
import { LobbyConfig } from "./ClientGameRunner"
|
||||
import { LocalServer } from "./LocalServer"
|
||||
import { Config, ServerConfig } from "../core/configuration/Config";
|
||||
import { SendLogEvent } from "../core/Consolex";
|
||||
import { EventBus, GameEvent } from "../core/EventBus";
|
||||
import {
|
||||
AllianceRequest,
|
||||
AllPlayers,
|
||||
Cell,
|
||||
GameType,
|
||||
Player,
|
||||
PlayerID,
|
||||
PlayerType,
|
||||
UnitType,
|
||||
} from "../core/game/Game";
|
||||
import {
|
||||
ClientID,
|
||||
ClientIntentMessageSchema,
|
||||
ClientJoinMessageSchema,
|
||||
GameID,
|
||||
Intent,
|
||||
ServerMessage,
|
||||
ServerMessageSchema,
|
||||
ClientPingMessageSchema,
|
||||
GameConfig,
|
||||
ClientLogMessageSchema,
|
||||
ClientSendWinnerSchema,
|
||||
} from "../core/Schemas";
|
||||
import { LobbyConfig } from "./ClientGameRunner";
|
||||
import { LocalServer } from "./LocalServer";
|
||||
import { UsernameInput } from "./UsernameInput";
|
||||
import { HostLobbyModal as HostPrivateLobbyModal } from "./HostLobbyModal";
|
||||
import { JoinPrivateLobbyModal } from "./JoinPrivateLobbyModal";
|
||||
import { SinglePlayerModal } from "./SinglePlayerModal";
|
||||
import { PlayerView } from "../core/game/GameView"
|
||||
import { PlayerView } from "../core/game/GameView";
|
||||
|
||||
export class PauseGameEvent implements GameEvent {
|
||||
constructor(public readonly paused: boolean) { }
|
||||
constructor(public readonly paused: boolean) {}
|
||||
}
|
||||
|
||||
export class SendAllianceRequestIntentEvent implements GameEvent {
|
||||
constructor(
|
||||
public readonly requestor: PlayerView,
|
||||
public readonly recipient: PlayerView
|
||||
) { }
|
||||
constructor(
|
||||
public readonly requestor: PlayerView,
|
||||
public readonly recipient: PlayerView,
|
||||
) {}
|
||||
}
|
||||
|
||||
export class SendBreakAllianceIntentEvent implements GameEvent {
|
||||
constructor(
|
||||
public readonly requestor: PlayerView,
|
||||
public readonly recipient: PlayerView
|
||||
) { }
|
||||
constructor(
|
||||
public readonly requestor: PlayerView,
|
||||
public readonly recipient: PlayerView,
|
||||
) {}
|
||||
}
|
||||
|
||||
export class SendAllianceReplyIntentEvent implements GameEvent {
|
||||
constructor(
|
||||
// The original alliance requestor
|
||||
public readonly requestor: PlayerView,
|
||||
public readonly recipient: PlayerView,
|
||||
public readonly accepted: boolean
|
||||
) { }
|
||||
constructor(
|
||||
// The original alliance requestor
|
||||
public readonly requestor: PlayerView,
|
||||
public readonly recipient: PlayerView,
|
||||
public readonly accepted: boolean,
|
||||
) {}
|
||||
}
|
||||
|
||||
export class SendSpawnIntentEvent implements GameEvent {
|
||||
constructor(
|
||||
public readonly cell: Cell,
|
||||
) { }
|
||||
constructor(public readonly cell: Cell) {}
|
||||
}
|
||||
|
||||
export class SendAttackIntentEvent implements GameEvent {
|
||||
constructor(
|
||||
public readonly targetID: PlayerID,
|
||||
public readonly troops: number,
|
||||
) { }
|
||||
constructor(
|
||||
public readonly targetID: PlayerID,
|
||||
public readonly troops: number,
|
||||
) {}
|
||||
}
|
||||
|
||||
export class SendBoatAttackIntentEvent implements GameEvent {
|
||||
constructor(
|
||||
public readonly targetID: PlayerID,
|
||||
public readonly cell: Cell,
|
||||
public readonly troops: number
|
||||
) { }
|
||||
constructor(
|
||||
public readonly targetID: PlayerID,
|
||||
public readonly cell: Cell,
|
||||
public readonly troops: number,
|
||||
) {}
|
||||
}
|
||||
|
||||
export class BuildUnitIntentEvent implements GameEvent {
|
||||
constructor(
|
||||
public readonly unit: UnitType,
|
||||
public readonly cell: Cell,
|
||||
) { }
|
||||
constructor(
|
||||
public readonly unit: UnitType,
|
||||
public readonly cell: Cell,
|
||||
) {}
|
||||
}
|
||||
|
||||
export class SendTargetPlayerIntentEvent implements GameEvent {
|
||||
constructor(
|
||||
public readonly targetID: PlayerID,
|
||||
) { }
|
||||
constructor(public readonly targetID: PlayerID) {}
|
||||
}
|
||||
|
||||
export class SendEmojiIntentEvent implements GameEvent {
|
||||
constructor(
|
||||
public readonly recipient: PlayerView | typeof AllPlayers,
|
||||
public readonly emoji: string
|
||||
) { }
|
||||
constructor(
|
||||
public readonly recipient: PlayerView | typeof AllPlayers,
|
||||
public readonly emoji: string,
|
||||
) {}
|
||||
}
|
||||
|
||||
export class SendDonateIntentEvent implements GameEvent {
|
||||
constructor(
|
||||
public readonly sender: PlayerView,
|
||||
public readonly recipient: PlayerView,
|
||||
public readonly troops: number | null,
|
||||
) { }
|
||||
constructor(
|
||||
public readonly sender: PlayerView,
|
||||
public readonly recipient: PlayerView,
|
||||
public readonly troops: number | null,
|
||||
) {}
|
||||
}
|
||||
|
||||
export class SendSetTargetTroopRatioEvent implements GameEvent {
|
||||
constructor(
|
||||
public readonly ratio: number,
|
||||
) { }
|
||||
constructor(public readonly ratio: number) {}
|
||||
}
|
||||
|
||||
export class SendWinnerEvent implements GameEvent {
|
||||
constructor(
|
||||
public readonly winner: ClientID
|
||||
) { }
|
||||
constructor(public readonly winner: ClientID) {}
|
||||
}
|
||||
|
||||
export class Transport {
|
||||
private socket: WebSocket;
|
||||
|
||||
private socket: WebSocket
|
||||
private localServer: LocalServer;
|
||||
|
||||
private localServer: LocalServer
|
||||
private buffer: string[] = [];
|
||||
|
||||
private buffer: string[] = []
|
||||
private onconnect: () => void;
|
||||
private onmessage: (msg: ServerMessage) => void;
|
||||
|
||||
private pingInterval: number | null = null;
|
||||
private isLocal: boolean;
|
||||
|
||||
private onconnect: () => void
|
||||
private onmessage: (msg: ServerMessage) => void
|
||||
constructor(
|
||||
private lobbyConfig: LobbyConfig,
|
||||
// gameConfig only set on private games
|
||||
private gameConfig: GameConfig | null,
|
||||
private eventBus: EventBus,
|
||||
private serverConfig: ServerConfig,
|
||||
) {
|
||||
this.isLocal = lobbyConfig.gameType == GameType.Singleplayer;
|
||||
|
||||
this.eventBus.on(SendAllianceRequestIntentEvent, (e) =>
|
||||
this.onSendAllianceRequest(e),
|
||||
);
|
||||
this.eventBus.on(SendAllianceReplyIntentEvent, (e) =>
|
||||
this.onAllianceRequestReplyUIEvent(e),
|
||||
);
|
||||
this.eventBus.on(SendBreakAllianceIntentEvent, (e) =>
|
||||
this.onBreakAllianceRequestUIEvent(e),
|
||||
);
|
||||
this.eventBus.on(SendSpawnIntentEvent, (e) =>
|
||||
this.onSendSpawnIntentEvent(e),
|
||||
);
|
||||
this.eventBus.on(SendAttackIntentEvent, (e) => this.onSendAttackIntent(e));
|
||||
this.eventBus.on(SendBoatAttackIntentEvent, (e) =>
|
||||
this.onSendBoatAttackIntent(e),
|
||||
);
|
||||
this.eventBus.on(SendTargetPlayerIntentEvent, (e) =>
|
||||
this.onSendTargetPlayerIntent(e),
|
||||
);
|
||||
this.eventBus.on(SendEmojiIntentEvent, (e) => this.onSendEmojiIntent(e));
|
||||
this.eventBus.on(SendDonateIntentEvent, (e) => this.onSendDonateIntent(e));
|
||||
this.eventBus.on(SendSetTargetTroopRatioEvent, (e) =>
|
||||
this.onSendSetTargetTroopRatioEvent(e),
|
||||
);
|
||||
this.eventBus.on(BuildUnitIntentEvent, (e) => this.onBuildUnitIntent(e));
|
||||
|
||||
private pingInterval: number | null = null
|
||||
private isLocal: boolean
|
||||
this.eventBus.on(SendLogEvent, (e) => this.onSendLogEvent(e));
|
||||
this.eventBus.on(PauseGameEvent, (e) => this.onPauseGameEvent(e));
|
||||
this.eventBus.on(SendWinnerEvent, (e) => this.onSendWinnerEvent(e));
|
||||
}
|
||||
|
||||
constructor(
|
||||
private lobbyConfig: LobbyConfig,
|
||||
// gameConfig only set on private games
|
||||
private gameConfig: GameConfig | null,
|
||||
private eventBus: EventBus,
|
||||
private serverConfig: ServerConfig,
|
||||
) {
|
||||
this.isLocal = lobbyConfig.gameType == GameType.Singleplayer
|
||||
|
||||
this.eventBus.on(SendAllianceRequestIntentEvent, (e) => this.onSendAllianceRequest(e))
|
||||
this.eventBus.on(SendAllianceReplyIntentEvent, (e) => this.onAllianceRequestReplyUIEvent(e))
|
||||
this.eventBus.on(SendBreakAllianceIntentEvent, (e) => this.onBreakAllianceRequestUIEvent(e))
|
||||
this.eventBus.on(SendSpawnIntentEvent, (e) => this.onSendSpawnIntentEvent(e))
|
||||
this.eventBus.on(SendAttackIntentEvent, (e) => this.onSendAttackIntent(e))
|
||||
this.eventBus.on(SendBoatAttackIntentEvent, (e) => this.onSendBoatAttackIntent(e))
|
||||
this.eventBus.on(SendTargetPlayerIntentEvent, (e) => this.onSendTargetPlayerIntent(e))
|
||||
this.eventBus.on(SendEmojiIntentEvent, (e) => this.onSendEmojiIntent(e))
|
||||
this.eventBus.on(SendDonateIntentEvent, (e) => this.onSendDonateIntent(e))
|
||||
this.eventBus.on(SendSetTargetTroopRatioEvent, (e) => this.onSendSetTargetTroopRatioEvent(e))
|
||||
this.eventBus.on(BuildUnitIntentEvent, (e) => this.onBuildUnitIntent(e))
|
||||
|
||||
this.eventBus.on(SendLogEvent, (e) => this.onSendLogEvent(e))
|
||||
this.eventBus.on(PauseGameEvent, (e) => this.onPauseGameEvent(e))
|
||||
this.eventBus.on(SendWinnerEvent, (e) => this.onSendWinnerEvent(e))
|
||||
}
|
||||
|
||||
private startPing() {
|
||||
if (this.isLocal || this.pingInterval) return;
|
||||
if (this.pingInterval == null) {
|
||||
this.pingInterval = window.setInterval(() => {
|
||||
if (this.socket != null && this.socket.readyState === WebSocket.OPEN) {
|
||||
this.sendMsg(JSON.stringify(ClientPingMessageSchema.parse({
|
||||
type: 'ping',
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
gameID: this.lobbyConfig.gameID,
|
||||
})))
|
||||
}
|
||||
}, 5 * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
private stopPing() {
|
||||
if (this.pingInterval) {
|
||||
window.clearInterval(this.pingInterval);
|
||||
this.pingInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
public connect(onconnect: () => void, onmessage: (message: ServerMessage) => void) {
|
||||
if (this.isLocal) {
|
||||
this.connectLocal(onconnect, onmessage)
|
||||
} else {
|
||||
this.connectRemote(onconnect, onmessage)
|
||||
}
|
||||
}
|
||||
|
||||
private connectLocal(onconnect: () => void, onmessage: (message: ServerMessage) => void) {
|
||||
this.localServer = new LocalServer(this.serverConfig, this.gameConfig, this.lobbyConfig, onconnect, onmessage)
|
||||
this.localServer.start()
|
||||
}
|
||||
|
||||
private connectRemote(onconnect: () => void, onmessage: (message: ServerMessage) => void) {
|
||||
this.startPing()
|
||||
this.maybeKillSocket()
|
||||
const wsHost = process.env.WEBSOCKET_URL || window.location.host;
|
||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
this.socket = new WebSocket(`${wsProtocol}//${wsHost}`)
|
||||
this.onconnect = onconnect
|
||||
this.onmessage = onmessage
|
||||
this.socket.onopen = () => {
|
||||
console.log('Connected to game server!');
|
||||
while (this.buffer.length > 0) {
|
||||
console.log('sending dropped message')
|
||||
this.sendMsg(this.buffer.pop())
|
||||
}
|
||||
onconnect()
|
||||
};
|
||||
this.socket.onmessage = (event: MessageEvent) => {
|
||||
try {
|
||||
const serverMsg = ServerMessageSchema.parse(JSON.parse(event.data));
|
||||
this.onmessage(serverMsg);
|
||||
} catch (error) {
|
||||
console.error('Failed to process server message:', error);
|
||||
}
|
||||
};
|
||||
this.socket.onerror = (err) => {
|
||||
console.error('Socket encountered error: ', err, 'Closing socket');
|
||||
this.socket.close();
|
||||
};
|
||||
this.socket.onclose = (event: CloseEvent) => {
|
||||
console.log(`WebSocket closed. Code: ${event.code}, Reason: ${event.reason}`);
|
||||
if (event.code != 1000) {
|
||||
console.log(`reconnecting`)
|
||||
this.connect(onconnect, onmessage)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private onSendLogEvent(event: SendLogEvent) {
|
||||
this.sendMsg(
|
||||
private startPing() {
|
||||
if (this.isLocal || this.pingInterval) return;
|
||||
if (this.pingInterval == null) {
|
||||
this.pingInterval = window.setInterval(() => {
|
||||
if (this.socket != null && this.socket.readyState === WebSocket.OPEN) {
|
||||
this.sendMsg(
|
||||
JSON.stringify(
|
||||
ClientLogMessageSchema.parse({
|
||||
type: "log",
|
||||
gameID: this.lobbyConfig.gameID,
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
persistentID: this.lobbyConfig.persistentID,
|
||||
log: event.log,
|
||||
severity: event.severity,
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
joinGame(numTurns: number) {
|
||||
this.sendMsg(
|
||||
JSON.stringify(
|
||||
ClientJoinMessageSchema.parse({
|
||||
type: "join",
|
||||
gameID: this.lobbyConfig.gameID,
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
lastTurn: numTurns,
|
||||
persistentID: this.lobbyConfig.persistentID,
|
||||
username: this.lobbyConfig.playerName()
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
leaveGame() {
|
||||
if (this.isLocal) {
|
||||
this.localServer.endGame()
|
||||
return
|
||||
}
|
||||
this.stopPing()
|
||||
if (this.socket.readyState === WebSocket.OPEN) {
|
||||
console.log('on stop: leaving game')
|
||||
this.socket.close()
|
||||
} else {
|
||||
console.log('WebSocket is not open. Current state:', this.socket.readyState);
|
||||
console.error('attempting reconnect')
|
||||
}
|
||||
this.socket.onclose = (event: CloseEvent) => { }
|
||||
}
|
||||
|
||||
private onSendAllianceRequest(event: SendAllianceRequestIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "allianceRequest",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
requestor: event.requestor.id(),
|
||||
recipient: event.recipient.id(),
|
||||
})
|
||||
}
|
||||
|
||||
private onAllianceRequestReplyUIEvent(event: SendAllianceReplyIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "allianceRequestReply",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
requestor: event.requestor.id(),
|
||||
recipient: event.recipient.id(),
|
||||
accept: event.accepted,
|
||||
})
|
||||
}
|
||||
|
||||
private onBreakAllianceRequestUIEvent(event: SendBreakAllianceIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "breakAlliance",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
requestor: event.requestor.id(),
|
||||
recipient: event.recipient.id(),
|
||||
})
|
||||
}
|
||||
|
||||
private onSendSpawnIntentEvent(event: SendSpawnIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "spawn",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
playerID: this.lobbyConfig.playerID,
|
||||
name: this.lobbyConfig.playerName(),
|
||||
playerType: PlayerType.Human,
|
||||
x: event.cell.x,
|
||||
y: event.cell.y
|
||||
})
|
||||
}
|
||||
|
||||
private onSendAttackIntent(event: SendAttackIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "attack",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
attackerID: this.lobbyConfig.playerID,
|
||||
targetID: event.targetID,
|
||||
troops: event.troops,
|
||||
})
|
||||
}
|
||||
|
||||
private onSendBoatAttackIntent(event: SendBoatAttackIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "boat",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
attackerID: this.lobbyConfig.playerID,
|
||||
targetID: event.targetID,
|
||||
troops: event.troops,
|
||||
x: event.cell.x,
|
||||
y: event.cell.y,
|
||||
})
|
||||
}
|
||||
|
||||
private onSendTargetPlayerIntent(event: SendTargetPlayerIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "targetPlayer",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
requestor: this.lobbyConfig.playerID,
|
||||
target: event.targetID,
|
||||
})
|
||||
}
|
||||
|
||||
private onSendEmojiIntent(event: SendEmojiIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "emoji",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
sender: this.lobbyConfig.playerID,
|
||||
recipient: event.recipient == AllPlayers ? AllPlayers : event.recipient.id(),
|
||||
emoji: event.emoji
|
||||
})
|
||||
}
|
||||
|
||||
private onSendDonateIntent(event: SendDonateIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "donate",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
sender: event.sender.id(),
|
||||
recipient: event.recipient.id(),
|
||||
troops: event.troops,
|
||||
})
|
||||
}
|
||||
|
||||
private onSendSetTargetTroopRatioEvent(event: SendSetTargetTroopRatioEvent) {
|
||||
this.sendIntent({
|
||||
type: "troop_ratio",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
player: this.lobbyConfig.playerID,
|
||||
ratio: event.ratio,
|
||||
})
|
||||
}
|
||||
|
||||
private onBuildUnitIntent(event: BuildUnitIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "build_unit",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
player: this.lobbyConfig.playerID,
|
||||
unit: event.unit,
|
||||
x: event.cell.x,
|
||||
y: event.cell.y,
|
||||
})
|
||||
}
|
||||
|
||||
private onPauseGameEvent(event: PauseGameEvent) {
|
||||
if (!this.isLocal) {
|
||||
console.log(`cannot pause multiplayer games`)
|
||||
return
|
||||
}
|
||||
if (event.paused) {
|
||||
this.localServer.pause()
|
||||
} else {
|
||||
this.localServer.resume()
|
||||
}
|
||||
}
|
||||
|
||||
private onSendWinnerEvent(event: SendWinnerEvent) {
|
||||
if (this.isLocal || this.socket.readyState === WebSocket.OPEN) {
|
||||
const msg = ClientSendWinnerSchema.parse({
|
||||
type: "winner",
|
||||
ClientPingMessageSchema.parse({
|
||||
type: "ping",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
gameID: this.lobbyConfig.gameID,
|
||||
winner: event.winner,
|
||||
})
|
||||
this.sendMsg(JSON.stringify(msg))
|
||||
} else {
|
||||
console.log('WebSocket is not open. Current state:', this.socket.readyState);
|
||||
console.log('attempting reconnect')
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}, 5 * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
private sendIntent(intent: Intent) {
|
||||
if (this.isLocal || this.socket.readyState === WebSocket.OPEN) {
|
||||
const msg = ClientIntentMessageSchema.parse({
|
||||
type: "intent",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
gameID: this.lobbyConfig.gameID,
|
||||
intent: intent
|
||||
})
|
||||
this.sendMsg(JSON.stringify(msg))
|
||||
} else {
|
||||
console.log('WebSocket is not open. Current state:', this.socket.readyState);
|
||||
console.log('attempting reconnect')
|
||||
}
|
||||
private stopPing() {
|
||||
if (this.pingInterval) {
|
||||
window.clearInterval(this.pingInterval);
|
||||
this.pingInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
private sendMsg(msg: string) {
|
||||
if (this.isLocal) {
|
||||
this.localServer.onMessage(msg)
|
||||
} else {
|
||||
if (this.socket.readyState == WebSocket.CLOSED || this.socket.readyState == WebSocket.CLOSED) {
|
||||
console.warn('socket not ready, closing and trying later')
|
||||
this.socket.close()
|
||||
this.socket = null
|
||||
this.connectRemote(this.onconnect, this.onmessage)
|
||||
this.buffer.push(msg)
|
||||
} else {
|
||||
this.socket.send(msg)
|
||||
}
|
||||
}
|
||||
public connect(
|
||||
onconnect: () => void,
|
||||
onmessage: (message: ServerMessage) => void,
|
||||
) {
|
||||
if (this.isLocal) {
|
||||
this.connectLocal(onconnect, onmessage);
|
||||
} else {
|
||||
this.connectRemote(onconnect, onmessage);
|
||||
}
|
||||
}
|
||||
|
||||
private maybeKillSocket(): void {
|
||||
if (this.socket == null) {
|
||||
return
|
||||
}
|
||||
// Remove all event listeners
|
||||
this.socket.onmessage = null;
|
||||
this.socket.onopen = null;
|
||||
this.socket.onclose = null;
|
||||
this.socket.onerror = null;
|
||||
private connectLocal(
|
||||
onconnect: () => void,
|
||||
onmessage: (message: ServerMessage) => void,
|
||||
) {
|
||||
this.localServer = new LocalServer(
|
||||
this.serverConfig,
|
||||
this.gameConfig,
|
||||
this.lobbyConfig,
|
||||
onconnect,
|
||||
onmessage,
|
||||
);
|
||||
this.localServer.start();
|
||||
}
|
||||
|
||||
// Close the connection if it's still open
|
||||
if (this.socket.readyState === WebSocket.OPEN) {
|
||||
this.socket.close();
|
||||
}
|
||||
this.socket = null
|
||||
private connectRemote(
|
||||
onconnect: () => void,
|
||||
onmessage: (message: ServerMessage) => void,
|
||||
) {
|
||||
this.startPing();
|
||||
this.maybeKillSocket();
|
||||
const wsHost = process.env.WEBSOCKET_URL || window.location.host;
|
||||
const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
this.socket = new WebSocket(`${wsProtocol}//${wsHost}`);
|
||||
this.onconnect = onconnect;
|
||||
this.onmessage = onmessage;
|
||||
this.socket.onopen = () => {
|
||||
console.log("Connected to game server!");
|
||||
while (this.buffer.length > 0) {
|
||||
console.log("sending dropped message");
|
||||
this.sendMsg(this.buffer.pop());
|
||||
}
|
||||
onconnect();
|
||||
};
|
||||
this.socket.onmessage = (event: MessageEvent) => {
|
||||
try {
|
||||
const serverMsg = ServerMessageSchema.parse(JSON.parse(event.data));
|
||||
this.onmessage(serverMsg);
|
||||
} catch (error) {
|
||||
console.error("Failed to process server message:", error);
|
||||
}
|
||||
};
|
||||
this.socket.onerror = (err) => {
|
||||
console.error("Socket encountered error: ", err, "Closing socket");
|
||||
this.socket.close();
|
||||
};
|
||||
this.socket.onclose = (event: CloseEvent) => {
|
||||
console.log(
|
||||
`WebSocket closed. Code: ${event.code}, Reason: ${event.reason}`,
|
||||
);
|
||||
if (event.code != 1000) {
|
||||
console.log(`reconnecting`);
|
||||
this.connect(onconnect, onmessage);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private onSendLogEvent(event: SendLogEvent) {
|
||||
this.sendMsg(
|
||||
JSON.stringify(
|
||||
ClientLogMessageSchema.parse({
|
||||
type: "log",
|
||||
gameID: this.lobbyConfig.gameID,
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
persistentID: this.lobbyConfig.persistentID,
|
||||
log: event.log,
|
||||
severity: event.severity,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
joinGame(numTurns: number) {
|
||||
this.sendMsg(
|
||||
JSON.stringify(
|
||||
ClientJoinMessageSchema.parse({
|
||||
type: "join",
|
||||
gameID: this.lobbyConfig.gameID,
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
lastTurn: numTurns,
|
||||
persistentID: this.lobbyConfig.persistentID,
|
||||
username: this.lobbyConfig.playerName(),
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
leaveGame() {
|
||||
if (this.isLocal) {
|
||||
this.localServer.endGame();
|
||||
return;
|
||||
}
|
||||
this.stopPing();
|
||||
if (this.socket.readyState === WebSocket.OPEN) {
|
||||
console.log("on stop: leaving game");
|
||||
this.socket.close();
|
||||
} else {
|
||||
console.log(
|
||||
"WebSocket is not open. Current state:",
|
||||
this.socket.readyState,
|
||||
);
|
||||
console.error("attempting reconnect");
|
||||
}
|
||||
this.socket.onclose = (event: CloseEvent) => {};
|
||||
}
|
||||
|
||||
private onSendAllianceRequest(event: SendAllianceRequestIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "allianceRequest",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
requestor: event.requestor.id(),
|
||||
recipient: event.recipient.id(),
|
||||
});
|
||||
}
|
||||
|
||||
private onAllianceRequestReplyUIEvent(event: SendAllianceReplyIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "allianceRequestReply",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
requestor: event.requestor.id(),
|
||||
recipient: event.recipient.id(),
|
||||
accept: event.accepted,
|
||||
});
|
||||
}
|
||||
|
||||
private onBreakAllianceRequestUIEvent(event: SendBreakAllianceIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "breakAlliance",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
requestor: event.requestor.id(),
|
||||
recipient: event.recipient.id(),
|
||||
});
|
||||
}
|
||||
|
||||
private onSendSpawnIntentEvent(event: SendSpawnIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "spawn",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
playerID: this.lobbyConfig.playerID,
|
||||
name: this.lobbyConfig.playerName(),
|
||||
playerType: PlayerType.Human,
|
||||
x: event.cell.x,
|
||||
y: event.cell.y,
|
||||
});
|
||||
}
|
||||
|
||||
private onSendAttackIntent(event: SendAttackIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "attack",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
attackerID: this.lobbyConfig.playerID,
|
||||
targetID: event.targetID,
|
||||
troops: event.troops,
|
||||
});
|
||||
}
|
||||
|
||||
private onSendBoatAttackIntent(event: SendBoatAttackIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "boat",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
attackerID: this.lobbyConfig.playerID,
|
||||
targetID: event.targetID,
|
||||
troops: event.troops,
|
||||
x: event.cell.x,
|
||||
y: event.cell.y,
|
||||
});
|
||||
}
|
||||
|
||||
private onSendTargetPlayerIntent(event: SendTargetPlayerIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "targetPlayer",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
requestor: this.lobbyConfig.playerID,
|
||||
target: event.targetID,
|
||||
});
|
||||
}
|
||||
|
||||
private onSendEmojiIntent(event: SendEmojiIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "emoji",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
sender: this.lobbyConfig.playerID,
|
||||
recipient:
|
||||
event.recipient == AllPlayers ? AllPlayers : event.recipient.id(),
|
||||
emoji: event.emoji,
|
||||
});
|
||||
}
|
||||
|
||||
private onSendDonateIntent(event: SendDonateIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "donate",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
sender: event.sender.id(),
|
||||
recipient: event.recipient.id(),
|
||||
troops: event.troops,
|
||||
});
|
||||
}
|
||||
|
||||
private onSendSetTargetTroopRatioEvent(event: SendSetTargetTroopRatioEvent) {
|
||||
this.sendIntent({
|
||||
type: "troop_ratio",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
player: this.lobbyConfig.playerID,
|
||||
ratio: event.ratio,
|
||||
});
|
||||
}
|
||||
|
||||
private onBuildUnitIntent(event: BuildUnitIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "build_unit",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
player: this.lobbyConfig.playerID,
|
||||
unit: event.unit,
|
||||
x: event.cell.x,
|
||||
y: event.cell.y,
|
||||
});
|
||||
}
|
||||
|
||||
private onPauseGameEvent(event: PauseGameEvent) {
|
||||
if (!this.isLocal) {
|
||||
console.log(`cannot pause multiplayer games`);
|
||||
return;
|
||||
}
|
||||
if (event.paused) {
|
||||
this.localServer.pause();
|
||||
} else {
|
||||
this.localServer.resume();
|
||||
}
|
||||
}
|
||||
|
||||
private onSendWinnerEvent(event: SendWinnerEvent) {
|
||||
if (this.isLocal || this.socket.readyState === WebSocket.OPEN) {
|
||||
const msg = ClientSendWinnerSchema.parse({
|
||||
type: "winner",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
gameID: this.lobbyConfig.gameID,
|
||||
winner: event.winner,
|
||||
});
|
||||
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({
|
||||
type: "intent",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
gameID: this.lobbyConfig.gameID,
|
||||
intent: intent,
|
||||
});
|
||||
this.sendMsg(JSON.stringify(msg));
|
||||
} else {
|
||||
console.log(
|
||||
"WebSocket is not open. Current state:",
|
||||
this.socket.readyState,
|
||||
);
|
||||
console.log("attempting reconnect");
|
||||
}
|
||||
}
|
||||
|
||||
private sendMsg(msg: string) {
|
||||
if (this.isLocal) {
|
||||
this.localServer.onMessage(msg);
|
||||
} else {
|
||||
if (
|
||||
this.socket.readyState == WebSocket.CLOSED ||
|
||||
this.socket.readyState == WebSocket.CLOSED
|
||||
) {
|
||||
console.warn("socket not ready, closing and trying later");
|
||||
this.socket.close();
|
||||
this.socket = null;
|
||||
this.connectRemote(this.onconnect, this.onmessage);
|
||||
this.buffer.push(msg);
|
||||
} else {
|
||||
this.socket.send(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private maybeKillSocket(): void {
|
||||
if (this.socket == null) {
|
||||
return;
|
||||
}
|
||||
// Remove all event listeners
|
||||
this.socket.onmessage = null;
|
||||
this.socket.onopen = null;
|
||||
this.socket.onclose = null;
|
||||
this.socket.onerror = null;
|
||||
|
||||
// Close the connection if it's still open
|
||||
if (this.socket.readyState === WebSocket.OPEN) {
|
||||
this.socket.close();
|
||||
}
|
||||
this.socket = null;
|
||||
}
|
||||
}
|
||||
|
||||
+95
-86
@@ -1,100 +1,109 @@
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { MAX_USERNAME_LENGTH, validateUsername } from '../core/validations/username';
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import {
|
||||
MAX_USERNAME_LENGTH,
|
||||
validateUsername,
|
||||
} from "../core/validations/username";
|
||||
|
||||
const usernameKey: string = 'username';
|
||||
const usernameKey: string = "username";
|
||||
|
||||
@customElement('username-input')
|
||||
@customElement("username-input")
|
||||
export class UsernameInput extends LitElement {
|
||||
@state() private username: string = '';
|
||||
@property({ type: String }) validationError: string = '';
|
||||
private _isValid: boolean = true;
|
||||
@state() private username: string = "";
|
||||
@property({ type: String }) validationError: string = "";
|
||||
private _isValid: boolean = true;
|
||||
|
||||
// Remove static styles since we're using Tailwind
|
||||
// Remove static styles since we're using Tailwind
|
||||
|
||||
createRenderRoot() {
|
||||
// Disable shadow DOM to allow Tailwind classes to work
|
||||
return this;
|
||||
createRenderRoot() {
|
||||
// Disable shadow DOM to allow Tailwind classes to work
|
||||
return this;
|
||||
}
|
||||
|
||||
public getCurrentUsername(): string {
|
||||
return this.username;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.username = this.getStoredUsername();
|
||||
this.dispatchUsernameEvent();
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<input
|
||||
type="text"
|
||||
.value=${this.username}
|
||||
@input=${this.handleChange}
|
||||
@change=${this.handleChange}
|
||||
placeholder="Enter your username"
|
||||
maxlength="${MAX_USERNAME_LENGTH}"
|
||||
class="w-full px-4 py-2 bg-white border border-gray-300 rounded-xl shadow-sm text-2xl text-gray-900 text-center focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
${this.validationError
|
||||
? html`<div
|
||||
class="mt-2 px-3 py-1 text-lg text-red-600 bg-white border border-red-600 rounded"
|
||||
>
|
||||
${this.validationError}
|
||||
</div>`
|
||||
: null}
|
||||
`;
|
||||
}
|
||||
|
||||
private handleChange(e: Event) {
|
||||
const input = e.target as HTMLInputElement;
|
||||
this.username = input.value.trim();
|
||||
const result = validateUsername(this.username);
|
||||
this._isValid = result.isValid;
|
||||
if (result.isValid) {
|
||||
this.storeUsername(this.username);
|
||||
this.validationError = "";
|
||||
} else {
|
||||
this.validationError = result.error;
|
||||
}
|
||||
}
|
||||
|
||||
public getCurrentUsername(): string {
|
||||
return this.username;
|
||||
private getStoredUsername(): string {
|
||||
const storedUsername = localStorage.getItem(usernameKey);
|
||||
if (storedUsername) {
|
||||
return storedUsername;
|
||||
}
|
||||
return this.generateNewUsername();
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.username = this.getStoredUsername();
|
||||
this.dispatchUsernameEvent();
|
||||
private storeUsername(username: string) {
|
||||
if (username) {
|
||||
localStorage.setItem(usernameKey, username);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<input
|
||||
type="text"
|
||||
.value=${this.username}
|
||||
@input=${this.handleChange}
|
||||
@change=${this.handleChange}
|
||||
placeholder="Enter your username"
|
||||
maxlength="${MAX_USERNAME_LENGTH}"
|
||||
class="w-full px-4 py-2 bg-white border border-gray-300 rounded-xl shadow-sm text-2xl text-gray-900 text-center focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
${this.validationError
|
||||
? html`<div class="mt-2 px-3 py-1 text-lg text-red-600 bg-white border border-red-600 rounded">${this.validationError}</div>`
|
||||
: null}
|
||||
`;
|
||||
}
|
||||
private dispatchUsernameEvent() {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("username-change", {
|
||||
detail: { username: this.username },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private handleChange(e: Event) {
|
||||
const input = e.target as HTMLInputElement;
|
||||
this.username = input.value.trim();
|
||||
const result = validateUsername(this.username)
|
||||
this._isValid = result.isValid
|
||||
if (result.isValid) {
|
||||
this.storeUsername(this.username);
|
||||
this.validationError = ''
|
||||
} else {
|
||||
this.validationError = result.error
|
||||
}
|
||||
}
|
||||
private generateNewUsername(): string {
|
||||
const newUsername = "Anon" + this.uuidToThreeDigits();
|
||||
this.storeUsername(newUsername);
|
||||
return newUsername;
|
||||
}
|
||||
|
||||
private getStoredUsername(): string {
|
||||
const storedUsername = localStorage.getItem(usernameKey);
|
||||
if (storedUsername) {
|
||||
return storedUsername;
|
||||
}
|
||||
return this.generateNewUsername();
|
||||
}
|
||||
private uuidToThreeDigits(): string {
|
||||
const uuid = uuidv4();
|
||||
const cleanUuid = uuid.replace(/-/g, "").toLowerCase();
|
||||
const decimal = BigInt(`0x${cleanUuid}`);
|
||||
const threeDigits = decimal % 1000n;
|
||||
return threeDigits.toString().padStart(3, "0");
|
||||
}
|
||||
|
||||
private storeUsername(username: string) {
|
||||
if (username) {
|
||||
localStorage.setItem(usernameKey, username);
|
||||
}
|
||||
}
|
||||
|
||||
private dispatchUsernameEvent() {
|
||||
this.dispatchEvent(new CustomEvent('username-change', {
|
||||
detail: { username: this.username },
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
private generateNewUsername(): string {
|
||||
const newUsername = "Anon" + this.uuidToThreeDigits();
|
||||
this.storeUsername(newUsername);
|
||||
return newUsername;
|
||||
}
|
||||
|
||||
private uuidToThreeDigits(): string {
|
||||
const uuid = uuidv4();
|
||||
const cleanUuid = uuid.replace(/-/g, '').toLowerCase();
|
||||
const decimal = BigInt(`0x${cleanUuid}`);
|
||||
const threeDigits = decimal % 1000n;
|
||||
return threeDigits.toString().padStart(3, '0');
|
||||
}
|
||||
|
||||
public isValid(): boolean {
|
||||
return this._isValid;
|
||||
}
|
||||
}
|
||||
public isValid(): boolean {
|
||||
return this._isValid;
|
||||
}
|
||||
}
|
||||
|
||||
+51
-49
@@ -1,66 +1,68 @@
|
||||
|
||||
export function renderTroops(troops: number): string {
|
||||
return renderNumber(troops / 10)
|
||||
return renderNumber(troops / 10);
|
||||
}
|
||||
|
||||
export function renderNumber(num: number) {
|
||||
let numStr = ''
|
||||
if (num >= 10_000_000) {
|
||||
numStr = (num / 1000000).toFixed(1) + "M"
|
||||
} else if (num >= 1_000_000) {
|
||||
numStr = (num / 1000000).toFixed(2) + "M"
|
||||
} else if (num >= 100000) {
|
||||
numStr = Math.floor(num / 1000) + "K"
|
||||
} else if (num >= 10000) {
|
||||
numStr = (num / 1000).toFixed(1) + "K"
|
||||
} else if (num >= 1000) {
|
||||
numStr = (num / 1000).toFixed(2) + "K"
|
||||
} else {
|
||||
numStr = Math.floor(num).toString()
|
||||
}
|
||||
return numStr
|
||||
let numStr = "";
|
||||
if (num >= 10_000_000) {
|
||||
numStr = (num / 1000000).toFixed(1) + "M";
|
||||
} else if (num >= 1_000_000) {
|
||||
numStr = (num / 1000000).toFixed(2) + "M";
|
||||
} else if (num >= 100000) {
|
||||
numStr = Math.floor(num / 1000) + "K";
|
||||
} else if (num >= 10000) {
|
||||
numStr = (num / 1000).toFixed(1) + "K";
|
||||
} else if (num >= 1000) {
|
||||
numStr = (num / 1000).toFixed(2) + "K";
|
||||
} else {
|
||||
numStr = Math.floor(num).toString();
|
||||
}
|
||||
return numStr;
|
||||
}
|
||||
|
||||
export function createCanvas(): HTMLCanvasElement {
|
||||
const canvas = document.createElement('canvas');
|
||||
const canvas = document.createElement("canvas");
|
||||
|
||||
// Set canvas style to fill the screen
|
||||
canvas.style.position = 'fixed';
|
||||
canvas.style.left = '0';
|
||||
canvas.style.top = '0';
|
||||
canvas.style.width = '100%';
|
||||
canvas.style.height = '100%';
|
||||
canvas.style.touchAction = 'none';
|
||||
// Set canvas style to fill the screen
|
||||
canvas.style.position = "fixed";
|
||||
canvas.style.left = "0";
|
||||
canvas.style.top = "0";
|
||||
canvas.style.width = "100%";
|
||||
canvas.style.height = "100%";
|
||||
canvas.style.touchAction = "none";
|
||||
|
||||
return canvas
|
||||
return canvas;
|
||||
}
|
||||
/**
|
||||
* A polyfill for crypto.randomUUID that provides fallback implementations
|
||||
* for older browsers, particularly Safari versions < 15.4
|
||||
*/
|
||||
export function generateCryptoRandomUUID(): string {
|
||||
// Type guard to check if randomUUID is available
|
||||
if (typeof crypto !== 'undefined' && 'randomUUID' in crypto) {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
// Type guard to check if randomUUID is available
|
||||
if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
|
||||
// Fallback using crypto.getRandomValues
|
||||
if (typeof crypto !== 'undefined' && 'getRandomValues' in crypto) {
|
||||
return ([1e7] as any + -1e3 + -4e3 + -8e3 + -1e11).replace(
|
||||
/[018]/g,
|
||||
(c: number): string =>
|
||||
(c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4)).toString(16)
|
||||
);
|
||||
}
|
||||
|
||||
// Last resort fallback using Math.random
|
||||
// Note: This is less cryptographically secure but ensures functionality
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(
|
||||
/[xy]/g,
|
||||
(c: string): string => {
|
||||
const r: number = Math.random() * 16 | 0;
|
||||
const v: number = c === 'x' ? r : (r & 0x3 | 0x8);
|
||||
return v.toString(16);
|
||||
}
|
||||
// Fallback using crypto.getRandomValues
|
||||
if (typeof crypto !== "undefined" && "getRandomValues" in crypto) {
|
||||
return (([1e7] as any) + -1e3 + -4e3 + -8e3 + -1e11).replace(
|
||||
/[018]/g,
|
||||
(c: number): string =>
|
||||
(
|
||||
c ^
|
||||
(crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))
|
||||
).toString(16),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Last resort fallback using Math.random
|
||||
// Note: This is less cryptographically secure but ensures functionality
|
||||
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(
|
||||
/[xy]/g,
|
||||
(c: string): string => {
|
||||
const r: number = (Math.random() * 16) | 0;
|
||||
const v: number = c === "x" ? r : (r & 0x3) | 0x8;
|
||||
return v.toString(16);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ export function createRenderer(
|
||||
canvas: HTMLCanvasElement,
|
||||
game: GameView,
|
||||
eventBus: EventBus,
|
||||
clientID: ClientID
|
||||
clientID: ClientID,
|
||||
): GameRenderer {
|
||||
const transformHandler = new TransformHandler(game, eventBus, canvas);
|
||||
|
||||
@@ -64,7 +64,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");
|
||||
@@ -74,7 +74,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");
|
||||
@@ -119,7 +119,7 @@ export function createRenderer(
|
||||
emojiTable as EmojiTable,
|
||||
buildMenu,
|
||||
uiState,
|
||||
playerInfo
|
||||
playerInfo,
|
||||
),
|
||||
new SpawnTimer(game, transformHandler),
|
||||
leaderboard,
|
||||
@@ -136,7 +136,7 @@ export function createRenderer(
|
||||
canvas,
|
||||
transformHandler,
|
||||
uiState,
|
||||
layers
|
||||
layers,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -149,7 +149,7 @@ export class GameRenderer {
|
||||
private canvas: HTMLCanvasElement,
|
||||
public transformHandler: TransformHandler,
|
||||
public uiState: UIState,
|
||||
private layers: Layer[]
|
||||
private layers: Layer[],
|
||||
) {
|
||||
this.context = canvas.getContext("2d");
|
||||
}
|
||||
@@ -172,7 +172,7 @@ export class GameRenderer {
|
||||
this.transformHandler = new TransformHandler(
|
||||
this.game,
|
||||
this.eventBus,
|
||||
this.canvas
|
||||
this.canvas,
|
||||
);
|
||||
|
||||
requestAnimationFrame(() => this.renderGame());
|
||||
@@ -218,7 +218,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`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,158 +1,170 @@
|
||||
import { Game, Player, Cell } from '../../core/game/Game';
|
||||
import { NameViewData } from '../../core/game/Game';
|
||||
import { calculateBoundingBox, within } from '../../core/Util';
|
||||
import { Game, Player, Cell } from "../../core/game/Game";
|
||||
import { NameViewData } from "../../core/game/Game";
|
||||
import { calculateBoundingBox, within } from "../../core/Util";
|
||||
|
||||
export interface Point {
|
||||
x: number;
|
||||
y: number;
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface Rectangle {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function placeName(game: Game, player: Player): NameViewData {
|
||||
const boundingBox =
|
||||
player.largestClusterBoundingBox ??
|
||||
calculateBoundingBox(game, player.borderTiles());
|
||||
|
||||
const boundingBox = player.largestClusterBoundingBox ?? calculateBoundingBox(game, player.borderTiles());
|
||||
let scalingFactor = 1;
|
||||
const width = boundingBox.max.x - boundingBox.min.x;
|
||||
const height = boundingBox.max.y - boundingBox.min.y;
|
||||
const size = Math.min(width, height);
|
||||
if (size < 25) {
|
||||
scalingFactor = 1;
|
||||
} else if (size < 50) {
|
||||
scalingFactor = 2;
|
||||
} else if (size < 100) {
|
||||
scalingFactor = 4;
|
||||
} else if (size < 250) {
|
||||
scalingFactor = 8;
|
||||
} else if (size < 500) {
|
||||
scalingFactor = 16;
|
||||
} else {
|
||||
scalingFactor = 32;
|
||||
}
|
||||
|
||||
let scalingFactor = 1
|
||||
const width = boundingBox.max.x - boundingBox.min.x
|
||||
const height = boundingBox.max.y - boundingBox.min.y
|
||||
const size = Math.min(width, height)
|
||||
if (size < 25) {
|
||||
scalingFactor = 1
|
||||
} else if (size < 50) {
|
||||
scalingFactor = 2
|
||||
} else if (size < 100) {
|
||||
scalingFactor = 4
|
||||
} else if (size < 250) {
|
||||
scalingFactor = 8
|
||||
} else if (size < 500) {
|
||||
scalingFactor = 16
|
||||
} else {
|
||||
scalingFactor = 32
|
||||
}
|
||||
const grid = createGrid(game, player, boundingBox, scalingFactor);
|
||||
const largestRectangle = findLargestInscribedRectangle(grid);
|
||||
largestRectangle.x = largestRectangle.x * scalingFactor;
|
||||
largestRectangle.y = largestRectangle.y * scalingFactor;
|
||||
largestRectangle.width = largestRectangle.width * scalingFactor;
|
||||
largestRectangle.height = largestRectangle.height * scalingFactor;
|
||||
|
||||
const grid = createGrid(game, player, boundingBox, scalingFactor);
|
||||
const largestRectangle = findLargestInscribedRectangle(grid);
|
||||
largestRectangle.x = largestRectangle.x * scalingFactor
|
||||
largestRectangle.y = largestRectangle.y * scalingFactor
|
||||
largestRectangle.width = largestRectangle.width * scalingFactor
|
||||
largestRectangle.height = largestRectangle.height * scalingFactor
|
||||
let center = new Cell(
|
||||
Math.floor(
|
||||
largestRectangle.x + largestRectangle.width / 2 + boundingBox.min.x,
|
||||
),
|
||||
Math.floor(
|
||||
largestRectangle.y + largestRectangle.height / 2 + boundingBox.min.y,
|
||||
),
|
||||
);
|
||||
|
||||
let center = new Cell(
|
||||
Math.floor(largestRectangle.x + largestRectangle.width / 2 + boundingBox.min.x),
|
||||
Math.floor(largestRectangle.y + largestRectangle.height / 2 + boundingBox.min.y),
|
||||
)
|
||||
const fontSize = calculateFontSize(largestRectangle, player.name());
|
||||
center = new Cell(center.x, center.y - fontSize / 3);
|
||||
|
||||
const fontSize = calculateFontSize(largestRectangle, player.name());
|
||||
center = new Cell(center.x, center.y - fontSize / 3)
|
||||
|
||||
return {
|
||||
x: Math.ceil(center.x),
|
||||
y: Math.ceil(center.y),
|
||||
size: fontSize,
|
||||
}
|
||||
return {
|
||||
x: Math.ceil(center.x),
|
||||
y: Math.ceil(center.y),
|
||||
size: fontSize,
|
||||
};
|
||||
}
|
||||
|
||||
export function createGrid(game: Game, player: Player, boundingBox: { min: Point; max: Point }, scalingFactor: number): boolean[][] {
|
||||
const scaledBoundingBox: { min: Point; max: Point } = {
|
||||
min: {
|
||||
x: Math.floor(boundingBox.min.x / scalingFactor),
|
||||
y: Math.floor(boundingBox.min.y / scalingFactor)
|
||||
},
|
||||
max: {
|
||||
x: Math.floor(boundingBox.max.x / scalingFactor),
|
||||
y: Math.floor(boundingBox.max.y / scalingFactor)
|
||||
}
|
||||
export function createGrid(
|
||||
game: Game,
|
||||
player: Player,
|
||||
boundingBox: { min: Point; max: Point },
|
||||
scalingFactor: number,
|
||||
): boolean[][] {
|
||||
const scaledBoundingBox: { min: Point; max: Point } = {
|
||||
min: {
|
||||
x: Math.floor(boundingBox.min.x / scalingFactor),
|
||||
y: Math.floor(boundingBox.min.y / scalingFactor),
|
||||
},
|
||||
max: {
|
||||
x: Math.floor(boundingBox.max.x / scalingFactor),
|
||||
y: Math.floor(boundingBox.max.y / scalingFactor),
|
||||
},
|
||||
};
|
||||
|
||||
const width = scaledBoundingBox.max.x - scaledBoundingBox.min.x + 1;
|
||||
const height = scaledBoundingBox.max.y - scaledBoundingBox.min.y + 1;
|
||||
const grid: boolean[][] = Array(width)
|
||||
.fill(null)
|
||||
.map(() => Array(height).fill(false));
|
||||
|
||||
for (let x = scaledBoundingBox.min.x; x <= scaledBoundingBox.max.x; x++) {
|
||||
for (let y = scaledBoundingBox.min.y; y <= scaledBoundingBox.max.y; y++) {
|
||||
const cell = new Cell(x * scalingFactor, y * scalingFactor);
|
||||
if (game.isOnMap(cell)) {
|
||||
const tile = game.ref(cell.x, cell.y);
|
||||
grid[x - scaledBoundingBox.min.x][y - scaledBoundingBox.min.y] =
|
||||
game.isLake(tile) || game.owner(tile) === player; // TODO: okay if lake
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const width = scaledBoundingBox.max.x - scaledBoundingBox.min.x + 1;
|
||||
const height = scaledBoundingBox.max.y - scaledBoundingBox.min.y + 1;
|
||||
const grid: boolean[][] = Array(width).fill(null).map(() => Array(height).fill(false));
|
||||
|
||||
|
||||
for (let x = scaledBoundingBox.min.x; x <= scaledBoundingBox.max.x; x++) {
|
||||
for (let y = scaledBoundingBox.min.y; y <= scaledBoundingBox.max.y; y++) {
|
||||
const cell = new Cell(x * scalingFactor, y * scalingFactor);
|
||||
if (game.isOnMap(cell)) {
|
||||
const tile = game.ref(cell.x, cell.y);
|
||||
grid[x - scaledBoundingBox.min.x][y - scaledBoundingBox.min.y] = game.isLake(tile) || game.owner(tile) === player; // TODO: okay if lake
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return grid;
|
||||
return grid;
|
||||
}
|
||||
|
||||
export function findLargestInscribedRectangle(grid: boolean[][]): Rectangle {
|
||||
const rows = grid[0].length;
|
||||
const cols = grid.length;
|
||||
const heights: number[] = new Array(cols).fill(0);
|
||||
let largestRect: Rectangle = { x: 0, y: 0, width: 0, height: 0 };
|
||||
const rows = grid[0].length;
|
||||
const cols = grid.length;
|
||||
const heights: number[] = new Array(cols).fill(0);
|
||||
let largestRect: Rectangle = { x: 0, y: 0, width: 0, height: 0 };
|
||||
|
||||
for (let row = 0; row < rows; row++) {
|
||||
for (let col = 0; col < cols; col++) {
|
||||
if (grid[col][row]) {
|
||||
heights[col]++;
|
||||
} else {
|
||||
heights[col] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
const rectForRow = largestRectangleInHistogram(heights);
|
||||
|
||||
if (rectForRow.width * rectForRow.height > largestRect.width * largestRect.height) {
|
||||
largestRect = {
|
||||
x: rectForRow.x,
|
||||
y: row - rectForRow.height + 1,
|
||||
width: rectForRow.width,
|
||||
height: rectForRow.height
|
||||
};
|
||||
}
|
||||
for (let row = 0; row < rows; row++) {
|
||||
for (let col = 0; col < cols; col++) {
|
||||
if (grid[col][row]) {
|
||||
heights[col]++;
|
||||
} else {
|
||||
heights[col] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return largestRect;
|
||||
const rectForRow = largestRectangleInHistogram(heights);
|
||||
|
||||
if (
|
||||
rectForRow.width * rectForRow.height >
|
||||
largestRect.width * largestRect.height
|
||||
) {
|
||||
largestRect = {
|
||||
x: rectForRow.x,
|
||||
y: row - rectForRow.height + 1,
|
||||
width: rectForRow.width,
|
||||
height: rectForRow.height,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return largestRect;
|
||||
}
|
||||
|
||||
export function largestRectangleInHistogram(widths: number[]): Rectangle {
|
||||
const stack: number[] = [];
|
||||
let maxArea = 0;
|
||||
let largestRect: Rectangle = { x: 0, y: 0, width: 0, height: 0 };
|
||||
const stack: number[] = [];
|
||||
let maxArea = 0;
|
||||
let largestRect: Rectangle = { x: 0, y: 0, width: 0, height: 0 };
|
||||
|
||||
for (let i = 0; i <= widths.length; i++) {
|
||||
const h = i === widths.length ? 0 : widths[i];
|
||||
for (let i = 0; i <= widths.length; i++) {
|
||||
const h = i === widths.length ? 0 : widths[i];
|
||||
|
||||
while (stack.length > 0 && h < widths[stack[stack.length - 1]]) {
|
||||
const height = widths[stack.pop()!];
|
||||
const width = stack.length === 0 ? i : i - stack[stack.length - 1] - 1;
|
||||
while (stack.length > 0 && h < widths[stack[stack.length - 1]]) {
|
||||
const height = widths[stack.pop()!];
|
||||
const width = stack.length === 0 ? i : i - stack[stack.length - 1] - 1;
|
||||
|
||||
if (height * width > maxArea) {
|
||||
maxArea = height * width;
|
||||
largestRect = {
|
||||
x: stack.length === 0 ? 0 : stack[stack.length - 1] + 1,
|
||||
y: 0,
|
||||
width: width,
|
||||
height: height
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
stack.push(i);
|
||||
if (height * width > maxArea) {
|
||||
maxArea = height * width;
|
||||
largestRect = {
|
||||
x: stack.length === 0 ? 0 : stack[stack.length - 1] + 1,
|
||||
y: 0,
|
||||
width: width,
|
||||
height: height,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return largestRect;
|
||||
stack.push(i);
|
||||
}
|
||||
|
||||
return largestRect;
|
||||
}
|
||||
|
||||
export function calculateFontSize(rectangle: Rectangle, name: string): number {
|
||||
// This is a simplified calculation. You might want to adjust it based on your specific font and rendering system.
|
||||
const widthConstrained = rectangle.width / name.length * 2;
|
||||
const heightConstrained = rectangle.height / 3;
|
||||
return Math.min(widthConstrained, heightConstrained);
|
||||
// This is a simplified calculation. You might want to adjust it based on your specific font and rendering system.
|
||||
const widthConstrained = (rectangle.width / name.length) * 2;
|
||||
const heightConstrained = rectangle.height / 3;
|
||||
return Math.min(widthConstrained, heightConstrained);
|
||||
}
|
||||
|
||||
@@ -1,198 +1,225 @@
|
||||
import { colord } from "colord";
|
||||
import { EventBus } from "../../core/EventBus"
|
||||
import { EventBus } from "../../core/EventBus";
|
||||
import { Cell, Game, Player } from "../../core/game/Game";
|
||||
import { calculateBoundingBox, calculateBoundingBoxCenter } from "../../core/Util";
|
||||
import {
|
||||
calculateBoundingBox,
|
||||
calculateBoundingBoxCenter,
|
||||
} from "../../core/Util";
|
||||
import { ZoomEvent, DragEvent } from "../InputHandler";
|
||||
import { GoToPlayerEvent } from "./layers/Leaderboard";
|
||||
import { placeName } from "./NameBoxCalculator";
|
||||
import { GameView } from "../../core/game/GameView";
|
||||
|
||||
export class TransformHandler {
|
||||
public scale: number = 1.8
|
||||
private offsetX: number = -350
|
||||
private offsetY: number = -200
|
||||
public scale: number = 1.8;
|
||||
private offsetX: number = -350;
|
||||
private offsetY: number = -200;
|
||||
|
||||
private target: Cell
|
||||
private intervalID = null
|
||||
private changed = false
|
||||
private target: Cell;
|
||||
private intervalID = null;
|
||||
private changed = false;
|
||||
|
||||
constructor(private game: GameView, private eventBus: EventBus, private canvas: HTMLCanvasElement) {
|
||||
this.eventBus.on(ZoomEvent, (e) => this.onZoom(e))
|
||||
this.eventBus.on(DragEvent, (e) => this.onMove(e))
|
||||
this.eventBus.on(GoToPlayerEvent, (e) => this.onGoToPlayer(e))
|
||||
constructor(
|
||||
private game: GameView,
|
||||
private eventBus: EventBus,
|
||||
private canvas: HTMLCanvasElement,
|
||||
) {
|
||||
this.eventBus.on(ZoomEvent, (e) => this.onZoom(e));
|
||||
this.eventBus.on(DragEvent, (e) => this.onMove(e));
|
||||
this.eventBus.on(GoToPlayerEvent, (e) => this.onGoToPlayer(e));
|
||||
}
|
||||
|
||||
boundingRect(): DOMRect {
|
||||
return this.canvas.getBoundingClientRect();
|
||||
}
|
||||
|
||||
width(): number {
|
||||
return this.boundingRect().width;
|
||||
}
|
||||
hasChanged(): boolean {
|
||||
return this.changed;
|
||||
}
|
||||
|
||||
handleTransform(context: CanvasRenderingContext2D) {
|
||||
// Disable image smoothing for pixelated effect
|
||||
context.imageSmoothingEnabled = false;
|
||||
|
||||
// Apply zoom and pan
|
||||
context.setTransform(
|
||||
this.scale,
|
||||
0,
|
||||
0,
|
||||
this.scale,
|
||||
this.game.width() / 2 - this.offsetX * this.scale,
|
||||
this.game.height() / 2 - this.offsetY * this.scale,
|
||||
);
|
||||
this.changed = false;
|
||||
}
|
||||
|
||||
worldToScreenCoordinates(cell: Cell): { x: number; y: number } {
|
||||
// Step 1: Convert from Cell coordinates to game coordinates
|
||||
// (reverse of Math.floor operation - we'll use the exact values)
|
||||
const gameX = cell.x;
|
||||
const gameY = cell.y;
|
||||
|
||||
// Step 2: Reverse the game center offset calculation
|
||||
// Original: gameX = centerX + this.game.width() / 2
|
||||
// Therefore: centerX = gameX - this.game.width() / 2
|
||||
const centerX = gameX - this.game.width() / 2;
|
||||
const centerY = gameY - this.game.height() / 2;
|
||||
|
||||
// Step 3: Reverse the world point calculation
|
||||
// Original: centerX = (canvasX - this.game.width() / 2) / this.scale + this.offsetX
|
||||
// Therefore: canvasX = (centerX - this.offsetX) * this.scale + this.game.width() / 2
|
||||
const canvasX =
|
||||
(centerX - this.offsetX) * this.scale + this.game.width() / 2;
|
||||
const canvasY =
|
||||
(centerY - this.offsetY) * this.scale + this.game.height() / 2;
|
||||
|
||||
// Step 4: Convert canvas coordinates back to screen coordinates
|
||||
const canvasRect = this.boundingRect();
|
||||
const screenX = canvasX + canvasRect.left;
|
||||
const screenY = canvasY + canvasRect.top;
|
||||
return { x: screenX, y: screenY };
|
||||
}
|
||||
|
||||
screenToWorldCoordinates(screenX: number, screenY: number): Cell {
|
||||
const canvasRect = this.boundingRect();
|
||||
const canvasX = screenX - canvasRect.left;
|
||||
const canvasY = screenY - canvasRect.top;
|
||||
|
||||
// Calculate the world point we want to zoom towards
|
||||
const centerX =
|
||||
(canvasX - this.game.width() / 2) / this.scale + this.offsetX;
|
||||
const centerY =
|
||||
(canvasY - this.game.height() / 2) / this.scale + this.offsetY;
|
||||
|
||||
const gameX = centerX + this.game.width() / 2;
|
||||
const gameY = centerY + this.game.height() / 2;
|
||||
|
||||
return new Cell(Math.floor(gameX), Math.floor(gameY));
|
||||
}
|
||||
|
||||
screenBoundingRect(): [Cell, Cell] {
|
||||
const LeftX = -this.game.width() / 2 / this.scale + this.offsetX;
|
||||
const TopY = -this.game.height() / 2 / this.scale + this.offsetY;
|
||||
|
||||
const gameLeftX = LeftX + this.game.width() / 2;
|
||||
const gameTopY = TopY + this.game.height() / 2;
|
||||
|
||||
const rightX =
|
||||
(screen.width - this.game.width() / 2) / this.scale + this.offsetX;
|
||||
const rightY =
|
||||
(screen.height - this.game.height() / 2) / this.scale + this.offsetY;
|
||||
|
||||
const gameRightX = rightX + this.game.width() / 2;
|
||||
const gameBottomY = rightY + this.game.height() / 2;
|
||||
|
||||
return [
|
||||
new Cell(Math.floor(gameLeftX), Math.floor(gameTopY)),
|
||||
new Cell(Math.floor(gameRightX), Math.floor(gameBottomY)),
|
||||
];
|
||||
}
|
||||
|
||||
isOnScreen(cell: Cell): boolean {
|
||||
const [topLeft, bottomRight] = this.screenBoundingRect();
|
||||
return (
|
||||
cell.x > topLeft.x &&
|
||||
cell.x < bottomRight.x &&
|
||||
cell.y > topLeft.y &&
|
||||
cell.y < bottomRight.y
|
||||
);
|
||||
}
|
||||
|
||||
screenCenter(): { screenX: number; screenY: number } {
|
||||
const [upperLeft, bottomRight] = this.screenBoundingRect();
|
||||
return {
|
||||
screenX: upperLeft.x + Math.floor((bottomRight.x - upperLeft.x) / 2),
|
||||
screenY: upperLeft.y + Math.floor((bottomRight.y - upperLeft.y) / 2),
|
||||
};
|
||||
}
|
||||
|
||||
onGoToPlayer(event: GoToPlayerEvent) {
|
||||
this.clearTarget();
|
||||
this.target = new Cell(
|
||||
event.player.nameLocation().x,
|
||||
event.player.nameLocation().y,
|
||||
);
|
||||
this.intervalID = setInterval(() => this.goTo(), 1);
|
||||
}
|
||||
|
||||
private goTo() {
|
||||
const { screenX, screenY } = this.screenCenter();
|
||||
const screenMapCenter = new Cell(screenX, screenY);
|
||||
|
||||
if (
|
||||
this.game.manhattanDist(
|
||||
this.game.ref(screenX, screenY),
|
||||
this.game.ref(this.target.x, this.target.y),
|
||||
) < 2
|
||||
) {
|
||||
this.clearTarget();
|
||||
return;
|
||||
}
|
||||
|
||||
boundingRect(): DOMRect {
|
||||
return this.canvas.getBoundingClientRect()
|
||||
const dX = Math.abs(screenMapCenter.x - this.target.x);
|
||||
if (dX > 2) {
|
||||
const offsetDx = Math.max(1, Math.floor(dX / 25));
|
||||
if (screenMapCenter.x > this.target.x) {
|
||||
this.offsetX -= offsetDx;
|
||||
} else {
|
||||
this.offsetX += offsetDx;
|
||||
}
|
||||
}
|
||||
|
||||
width(): number {
|
||||
return this.boundingRect().width
|
||||
const dY = Math.abs(screenMapCenter.y - this.target.y);
|
||||
if (dY > 2) {
|
||||
const offsetDy = Math.max(1, Math.floor(dY / 25));
|
||||
if (screenMapCenter.y > this.target.y) {
|
||||
this.offsetY -= offsetDy;
|
||||
} else {
|
||||
this.offsetY += offsetDy;
|
||||
}
|
||||
}
|
||||
hasChanged(): boolean {
|
||||
return this.changed
|
||||
this.changed = true;
|
||||
}
|
||||
|
||||
onZoom(event: ZoomEvent) {
|
||||
this.clearTarget();
|
||||
const oldScale = this.scale;
|
||||
const zoomFactor = 1 + event.delta / 600;
|
||||
this.scale /= zoomFactor;
|
||||
|
||||
// Clamp the scale to prevent extreme zooming
|
||||
this.scale = Math.max(0.5, Math.min(20, this.scale));
|
||||
|
||||
const canvasRect = this.boundingRect();
|
||||
const canvasX = event.x - canvasRect.left;
|
||||
const canvasY = event.y - canvasRect.top;
|
||||
|
||||
// Calculate the world point we want to zoom towards
|
||||
const zoomPointX =
|
||||
(canvasX - this.game.width() / 2) / oldScale + this.offsetX;
|
||||
const zoomPointY =
|
||||
(canvasY - this.game.height() / 2) / oldScale + this.offsetY;
|
||||
|
||||
// Adjust the offset
|
||||
this.offsetX = zoomPointX - (canvasX - this.game.width() / 2) / this.scale;
|
||||
this.offsetY = zoomPointY - (canvasY - this.game.height() / 2) / this.scale;
|
||||
this.changed = true;
|
||||
}
|
||||
|
||||
onMove(event: DragEvent) {
|
||||
this.clearTarget();
|
||||
this.offsetX -= event.deltaX / this.scale;
|
||||
this.offsetY -= event.deltaY / this.scale;
|
||||
this.changed = true;
|
||||
}
|
||||
|
||||
private clearTarget() {
|
||||
if (this.intervalID != null) {
|
||||
clearInterval(this.intervalID);
|
||||
this.intervalID = null;
|
||||
}
|
||||
|
||||
handleTransform(context: CanvasRenderingContext2D) {
|
||||
// Disable image smoothing for pixelated effect
|
||||
context.imageSmoothingEnabled = false;
|
||||
|
||||
|
||||
// Apply zoom and pan
|
||||
context.setTransform(
|
||||
this.scale,
|
||||
0,
|
||||
0,
|
||||
this.scale,
|
||||
this.game.width() / 2 - this.offsetX * this.scale,
|
||||
this.game.height() / 2 - this.offsetY * this.scale
|
||||
);
|
||||
this.changed = false
|
||||
}
|
||||
|
||||
worldToScreenCoordinates(cell: Cell): { x: number, y: number } {
|
||||
// Step 1: Convert from Cell coordinates to game coordinates
|
||||
// (reverse of Math.floor operation - we'll use the exact values)
|
||||
const gameX = cell.x;
|
||||
const gameY = cell.y;
|
||||
|
||||
// Step 2: Reverse the game center offset calculation
|
||||
// Original: gameX = centerX + this.game.width() / 2
|
||||
// Therefore: centerX = gameX - this.game.width() / 2
|
||||
const centerX = gameX - this.game.width() / 2;
|
||||
const centerY = gameY - this.game.height() / 2;
|
||||
|
||||
// Step 3: Reverse the world point calculation
|
||||
// Original: centerX = (canvasX - this.game.width() / 2) / this.scale + this.offsetX
|
||||
// Therefore: canvasX = (centerX - this.offsetX) * this.scale + this.game.width() / 2
|
||||
const canvasX = (centerX - this.offsetX) * this.scale + this.game.width() / 2;
|
||||
const canvasY = (centerY - this.offsetY) * this.scale + this.game.height() / 2;
|
||||
|
||||
// Step 4: Convert canvas coordinates back to screen coordinates
|
||||
const canvasRect = this.boundingRect();
|
||||
const screenX = canvasX + canvasRect.left;
|
||||
const screenY = canvasY + canvasRect.top;
|
||||
return { x: screenX, y: screenY }
|
||||
}
|
||||
|
||||
screenToWorldCoordinates(screenX: number, screenY: number): Cell {
|
||||
const canvasRect = this.boundingRect();
|
||||
const canvasX = screenX - canvasRect.left;
|
||||
const canvasY = screenY - canvasRect.top;
|
||||
|
||||
// Calculate the world point we want to zoom towards
|
||||
const centerX = (canvasX - this.game.width() / 2) / this.scale + this.offsetX;
|
||||
const centerY = (canvasY - this.game.height() / 2) / this.scale + this.offsetY;
|
||||
|
||||
const gameX = centerX + this.game.width() / 2
|
||||
const gameY = centerY + this.game.height() / 2
|
||||
|
||||
return new Cell(Math.floor(gameX), Math.floor(gameY));
|
||||
}
|
||||
|
||||
screenBoundingRect(): [Cell, Cell] {
|
||||
|
||||
const LeftX = (- this.game.width() / 2) / this.scale + this.offsetX;
|
||||
const TopY = (- this.game.height() / 2) / this.scale + this.offsetY;
|
||||
|
||||
const gameLeftX = LeftX + this.game.width() / 2
|
||||
const gameTopY = TopY + this.game.height() / 2
|
||||
|
||||
|
||||
const rightX = (screen.width - this.game.width() / 2) / this.scale + this.offsetX;
|
||||
const rightY = (screen.height - this.game.height() / 2) / this.scale + this.offsetY;
|
||||
|
||||
const gameRightX = rightX + this.game.width() / 2
|
||||
const gameBottomY = rightY + this.game.height() / 2
|
||||
|
||||
return [new Cell(Math.floor(gameLeftX), Math.floor(gameTopY)), new Cell(Math.floor(gameRightX), Math.floor(gameBottomY))]
|
||||
}
|
||||
|
||||
isOnScreen(cell: Cell): boolean {
|
||||
const [topLeft, bottomRight] = this.screenBoundingRect()
|
||||
return cell.x > topLeft.x && cell.x < bottomRight.x && cell.y > topLeft.y && cell.y < bottomRight.y
|
||||
}
|
||||
|
||||
screenCenter(): { screenX: number, screenY: number } {
|
||||
const [upperLeft, bottomRight] = this.screenBoundingRect()
|
||||
return {
|
||||
screenX: upperLeft.x + Math.floor((bottomRight.x - upperLeft.x) / 2),
|
||||
screenY: upperLeft.y + Math.floor((bottomRight.y - upperLeft.y) / 2)
|
||||
}
|
||||
}
|
||||
|
||||
onGoToPlayer(event: GoToPlayerEvent) {
|
||||
this.clearTarget();
|
||||
this.target = new Cell(event.player.nameLocation().x, event.player.nameLocation().y)
|
||||
this.intervalID = setInterval(() => this.goTo(), 1)
|
||||
}
|
||||
|
||||
private goTo() {
|
||||
const { screenX, screenY } = this.screenCenter()
|
||||
const screenMapCenter = new Cell(screenX, screenY)
|
||||
|
||||
if (this.game.manhattanDist(this.game.ref(screenX, screenY), this.game.ref(this.target.x, this.target.y)) < 2) {
|
||||
this.clearTarget()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
const dX = Math.abs(screenMapCenter.x - this.target.x)
|
||||
if (dX > 2) {
|
||||
const offsetDx = Math.max(1, Math.floor(dX / 25))
|
||||
if (screenMapCenter.x > this.target.x) {
|
||||
this.offsetX -= offsetDx
|
||||
} else {
|
||||
this.offsetX += offsetDx
|
||||
}
|
||||
}
|
||||
const dY = Math.abs(screenMapCenter.y - this.target.y)
|
||||
if (dY > 2) {
|
||||
const offsetDy = Math.max(1, Math.floor(dY / 25))
|
||||
if (screenMapCenter.y > this.target.y) {
|
||||
this.offsetY -= offsetDy
|
||||
} else {
|
||||
this.offsetY += offsetDy
|
||||
}
|
||||
}
|
||||
this.changed = true
|
||||
}
|
||||
|
||||
onZoom(event: ZoomEvent) {
|
||||
this.clearTarget()
|
||||
const oldScale = this.scale;
|
||||
const zoomFactor = 1 + event.delta / 600;
|
||||
this.scale /= zoomFactor;
|
||||
|
||||
// Clamp the scale to prevent extreme zooming
|
||||
this.scale = Math.max(0.5, Math.min(20, this.scale));
|
||||
|
||||
const canvasRect = this.boundingRect()
|
||||
const canvasX = event.x - canvasRect.left;
|
||||
const canvasY = event.y - canvasRect.top;
|
||||
|
||||
// Calculate the world point we want to zoom towards
|
||||
const zoomPointX = (canvasX - this.game.width() / 2) / oldScale + this.offsetX;
|
||||
const zoomPointY = (canvasY - this.game.height() / 2) / oldScale + this.offsetY;
|
||||
|
||||
// Adjust the offset
|
||||
this.offsetX = zoomPointX - (canvasX - this.game.width() / 2) / this.scale;
|
||||
this.offsetY = zoomPointY - (canvasY - this.game.height() / 2) / this.scale;
|
||||
this.changed = true
|
||||
}
|
||||
|
||||
onMove(event: DragEvent) {
|
||||
this.clearTarget()
|
||||
this.offsetX -= event.deltaX / this.scale;
|
||||
this.offsetY -= event.deltaY / this.scale;
|
||||
this.changed = true
|
||||
}
|
||||
|
||||
private clearTarget() {
|
||||
if (this.intervalID != null) {
|
||||
clearInterval(this.intervalID)
|
||||
this.intervalID = null
|
||||
}
|
||||
this.target = null
|
||||
}
|
||||
}
|
||||
this.target = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export interface UIState {
|
||||
attackRatio: number
|
||||
}
|
||||
attackRatio: number;
|
||||
}
|
||||
|
||||
@@ -125,10 +125,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({
|
||||
@@ -139,7 +139,7 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
className: "btn",
|
||||
action: () =>
|
||||
this.eventBus.emit(
|
||||
new SendAllianceReplyIntentEvent(requestor, recipient, true)
|
||||
new SendAllianceReplyIntentEvent(requestor, recipient, true),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -147,7 +147,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),
|
||||
),
|
||||
},
|
||||
],
|
||||
@@ -156,7 +156,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),
|
||||
),
|
||||
});
|
||||
}
|
||||
@@ -168,7 +168,7 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
}
|
||||
|
||||
const recipient = this.game.playerBySmallID(
|
||||
update.request.recipientID
|
||||
update.request.recipientID,
|
||||
) as PlayerView;
|
||||
|
||||
this.addEvent({
|
||||
@@ -213,8 +213,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;
|
||||
|
||||
@@ -250,7 +250,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) {
|
||||
@@ -306,7 +306,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">
|
||||
@@ -331,14 +331,14 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
>
|
||||
${btn.text}
|
||||
</button>
|
||||
`
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</td>
|
||||
</tr>
|
||||
`
|
||||
`,
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Game } from "../../../core/game/Game"
|
||||
import { Game } from "../../../core/game/Game";
|
||||
|
||||
export interface Layer {
|
||||
init?()
|
||||
tick?()
|
||||
renderLayer?(context: CanvasRenderingContext2D)
|
||||
shouldTransform?(): boolean
|
||||
redraw?(): void
|
||||
}
|
||||
init?();
|
||||
tick?();
|
||||
renderLayer?(context: CanvasRenderingContext2D);
|
||||
shouldTransform?(): boolean;
|
||||
redraw?(): void;
|
||||
}
|
||||
|
||||
@@ -1,167 +1,169 @@
|
||||
import { LitElement, html, css } from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { Layer } from './Layer';
|
||||
import { ClientID } from '../../../core/Schemas';
|
||||
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 { LitElement, html, css } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { Layer } from "./Layer";
|
||||
import { ClientID } from "../../../core/Schemas";
|
||||
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";
|
||||
|
||||
interface Entry {
|
||||
name: string
|
||||
position: number
|
||||
score: string
|
||||
gold: string
|
||||
isMyPlayer: boolean
|
||||
player: PlayerView
|
||||
name: string;
|
||||
position: number;
|
||||
score: string;
|
||||
gold: string;
|
||||
isMyPlayer: boolean;
|
||||
player: PlayerView;
|
||||
}
|
||||
|
||||
export class GoToPlayerEvent implements GameEvent {
|
||||
constructor(public player: PlayerView) { }
|
||||
constructor(public player: PlayerView) {}
|
||||
}
|
||||
|
||||
@customElement('leader-board')
|
||||
@customElement("leader-board")
|
||||
export class Leaderboard extends LitElement implements Layer {
|
||||
public game: GameView;
|
||||
public clientID: ClientID;
|
||||
public eventBus: EventBus;
|
||||
|
||||
public game: GameView
|
||||
public clientID: ClientID
|
||||
public eventBus: EventBus
|
||||
|
||||
init() {
|
||||
}
|
||||
init() {}
|
||||
|
||||
tick() {
|
||||
if (this._hidden && !this.game.inSpawnPhase()) {
|
||||
this.showLeaderboard()
|
||||
this.updateLeaderboard()
|
||||
this.showLeaderboard();
|
||||
this.updateLeaderboard();
|
||||
}
|
||||
if (this._hidden) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.game.ticks() % 10 == 0) {
|
||||
this.updateLeaderboard()
|
||||
this.updateLeaderboard();
|
||||
}
|
||||
}
|
||||
|
||||
private updateLeaderboard() {
|
||||
if (this.clientID == null) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
const myPlayer = this.game.playerViews().find(p => p.clientID() == this.clientID)
|
||||
const myPlayer = this.game
|
||||
.playerViews()
|
||||
.find((p) => p.clientID() == this.clientID);
|
||||
|
||||
const sorted = this.game.playerViews()
|
||||
.sort((a, b) => b.numTilesOwned() - a.numTilesOwned())
|
||||
const sorted = this.game
|
||||
.playerViews()
|
||||
.sort((a, b) => b.numTilesOwned() - a.numTilesOwned());
|
||||
|
||||
const numTilesWithoutFallout = this.game.numLandTiles() - this.game.numTilesWithFallout()
|
||||
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
|
||||
}));
|
||||
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,
|
||||
}));
|
||||
|
||||
if (myPlayer != null && this.players.find(p => p.isMyPlayer) == null) {
|
||||
let place = 0
|
||||
if (myPlayer != null && this.players.find((p) => p.isMyPlayer) == null) {
|
||||
let place = 0;
|
||||
for (const p of sorted) {
|
||||
place++
|
||||
place++;
|
||||
if (p == myPlayer) {
|
||||
break
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.players.pop()
|
||||
this.players.pop();
|
||||
this.players.push({
|
||||
name: myPlayer.displayName(),
|
||||
position: place,
|
||||
score: formatPercentage(myPlayer.numTilesOwned() / this.game.numLandTiles()),
|
||||
score: formatPercentage(
|
||||
myPlayer.numTilesOwned() / this.game.numLandTiles(),
|
||||
),
|
||||
gold: renderNumber(myPlayer.gold()),
|
||||
isMyPlayer: true,
|
||||
player: myPlayer
|
||||
})
|
||||
player: myPlayer,
|
||||
});
|
||||
}
|
||||
|
||||
this.requestUpdate()
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private handleRowClick(player: PlayerView) {
|
||||
this.eventBus.emit(new GoToPlayerEvent(player))
|
||||
this.eventBus.emit(new GoToPlayerEvent(player));
|
||||
}
|
||||
|
||||
renderLayer(context: CanvasRenderingContext2D) {
|
||||
}
|
||||
renderLayer(context: CanvasRenderingContext2D) {}
|
||||
shouldTransform(): boolean {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
img.emoji {
|
||||
height: 1em;
|
||||
width: auto;
|
||||
}
|
||||
.leaderboard {
|
||||
position: fixed;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
z-index: 9999;
|
||||
background-color: rgba(30, 30, 30, 0.7);
|
||||
padding: 10px;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
|
||||
border-radius: 10px;
|
||||
max-width: 300px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
width: 300px;
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
th, td {
|
||||
padding: 5px;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid rgba(51, 51, 51, 0.2);
|
||||
color: white;
|
||||
}
|
||||
th {
|
||||
background-color: rgba(44, 44, 44, 0.5);
|
||||
color: white;
|
||||
}
|
||||
.myPlayer {
|
||||
font-weight: bold;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
.otherPlayer {
|
||||
font-size: 1.0em;
|
||||
}
|
||||
tr:nth-child(even) {
|
||||
background-color: rgba(44, 44, 44, 0.5);
|
||||
}
|
||||
tbody tr {
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
tbody tr:hover {
|
||||
background-color: rgba(78, 78, 78, 0.8);
|
||||
}
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
@media (max-width: 1000px) {
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
img.emoji {
|
||||
height: 1em;
|
||||
width: auto;
|
||||
}
|
||||
.leaderboard {
|
||||
position: fixed;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
z-index: 9999;
|
||||
background-color: rgba(30, 30, 30, 0.7);
|
||||
padding: 10px;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
|
||||
border-radius: 10px;
|
||||
max-width: 300px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
width: 300px;
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
th,
|
||||
td {
|
||||
padding: 5px;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid rgba(51, 51, 51, 0.2);
|
||||
color: white;
|
||||
}
|
||||
th {
|
||||
background-color: rgba(44, 44, 44, 0.5);
|
||||
color: white;
|
||||
}
|
||||
.myPlayer {
|
||||
font-weight: bold;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
.otherPlayer {
|
||||
font-size: 1em;
|
||||
}
|
||||
tr:nth-child(even) {
|
||||
background-color: rgba(44, 44, 44, 0.5);
|
||||
}
|
||||
tbody tr {
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
tbody tr:hover {
|
||||
background-color: rgba(78, 78, 78, 0.8);
|
||||
}
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@media (max-width: 1000px) {
|
||||
.leaderboard {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
players: Entry[] = [];
|
||||
|
||||
@@ -178,7 +180,7 @@ export class Leaderboard extends LitElement implements Layer {
|
||||
// </div>
|
||||
// `;
|
||||
return html`
|
||||
<div class="leaderboard ${this._hidden ? 'hidden' : ''}">
|
||||
<div class="leaderboard ${this._hidden ? "hidden" : ""}">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -189,10 +191,10 @@ export class Leaderboard extends LitElement implements Layer {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${this.players
|
||||
.map((player) => html`
|
||||
<tr
|
||||
class="${player.isMyPlayer ? 'myPlayer' : 'otherPlayer'}"
|
||||
${this.players.map(
|
||||
(player) => html`
|
||||
<tr
|
||||
class="${player.isMyPlayer ? "myPlayer" : "otherPlayer"}"
|
||||
@click=${() => this.handleRowClick(player.player)}
|
||||
>
|
||||
<td>${player.position}</td>
|
||||
@@ -200,7 +202,8 @@ export class Leaderboard extends LitElement implements Layer {
|
||||
<td>${player.score}</td>
|
||||
<td>${player.gold}</td>
|
||||
</tr>
|
||||
`)}
|
||||
`,
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -223,15 +226,15 @@ export class Leaderboard extends LitElement implements Layer {
|
||||
}
|
||||
|
||||
function formatPercentage(value: number): string {
|
||||
const perc = value * 100
|
||||
const perc = value * 100;
|
||||
if (perc > 99.5) {
|
||||
return "100%"
|
||||
return "100%";
|
||||
}
|
||||
if (perc < .01) {
|
||||
return "0%"
|
||||
if (perc < 0.01) {
|
||||
return "0%";
|
||||
}
|
||||
if (perc < .1) {
|
||||
return (perc).toPrecision(1) + '%'
|
||||
if (perc < 0.1) {
|
||||
return perc.toPrecision(1) + "%";
|
||||
}
|
||||
return perc.toPrecision(2) + '%';
|
||||
}
|
||||
return perc.toPrecision(2) + "%";
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ class RenderInfo {
|
||||
public lastRenderCalc: number,
|
||||
public location: Cell,
|
||||
public fontSize: number,
|
||||
public element: HTMLElement
|
||||
public element: HTMLElement,
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,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;
|
||||
@@ -100,7 +100,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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -109,11 +115,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})`;
|
||||
|
||||
@@ -130,7 +136,7 @@ export class NameLayer implements Layer {
|
||||
0,
|
||||
0,
|
||||
mainContex.canvas.width,
|
||||
mainContex.canvas.height
|
||||
mainContex.canvas.height,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -185,7 +191,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
|
||||
@@ -224,7 +230,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) {
|
||||
@@ -236,7 +242,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) {
|
||||
@@ -251,8 +261,8 @@ export class NameLayer implements Layer {
|
||||
this.createIconElement(
|
||||
this.allianceIconImage.src,
|
||||
iconSize,
|
||||
"alliance"
|
||||
)
|
||||
"alliance",
|
||||
),
|
||||
);
|
||||
}
|
||||
} else if (existingAlliance) {
|
||||
@@ -267,7 +277,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) {
|
||||
@@ -281,7 +291,7 @@ export class NameLayer implements Layer {
|
||||
.filter(
|
||||
(emoji) =>
|
||||
emoji.recipientID == AllPlayers ||
|
||||
emoji.recipientID == myPlayer?.smallID()
|
||||
emoji.recipientID == myPlayer?.smallID(),
|
||||
);
|
||||
|
||||
if (emojis.length > 0) {
|
||||
@@ -314,7 +324,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;
|
||||
}
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
import { GameView } from '../../../core/game/GameView';
|
||||
import { TransformHandler } from '../TransformHandler';
|
||||
import { Layer } from './Layer';
|
||||
import { GameView } from "../../../core/game/GameView";
|
||||
import { TransformHandler } from "../TransformHandler";
|
||||
import { Layer } from "./Layer";
|
||||
|
||||
export class SpawnTimer implements Layer {
|
||||
constructor(
|
||||
private game: GameView,
|
||||
private transformHandler: TransformHandler,
|
||||
) {}
|
||||
|
||||
constructor(private game: GameView, private transformHandler: TransformHandler) { }
|
||||
init() {}
|
||||
tick() {}
|
||||
shouldTransform(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
init() {
|
||||
}
|
||||
tick() {
|
||||
}
|
||||
shouldTransform(): boolean {
|
||||
return false
|
||||
renderLayer(context: CanvasRenderingContext2D) {
|
||||
if (!this.game.inSpawnPhase()) {
|
||||
return;
|
||||
}
|
||||
|
||||
renderLayer(context: CanvasRenderingContext2D) {
|
||||
if (!this.game.inSpawnPhase()) {
|
||||
return
|
||||
}
|
||||
const barHeight = 15;
|
||||
const barBackgroundWidth = this.transformHandler.width();
|
||||
|
||||
const barHeight = 15;
|
||||
const barBackgroundWidth = this.transformHandler.width();
|
||||
const ratio = this.game.ticks() / this.game.config().numSpawnPhaseTurns();
|
||||
|
||||
const ratio = this.game.ticks() / this.game.config().numSpawnPhaseTurns()
|
||||
// Draw bar background
|
||||
context.fillStyle = "rgba(0, 0, 0, 0.5)";
|
||||
context.fillRect(0, 0, barBackgroundWidth, barHeight);
|
||||
|
||||
// Draw bar background
|
||||
context.fillStyle = 'rgba(0, 0, 0, 0.5)';
|
||||
context.fillRect(0, 0, barBackgroundWidth, barHeight);
|
||||
|
||||
context.fillStyle = 'rgba(0, 128, 255, 0.7)';
|
||||
context.fillRect(0, 0, barBackgroundWidth * ratio, barHeight);
|
||||
}
|
||||
context.fillStyle = "rgba(0, 128, 255, 0.7)";
|
||||
context.fillRect(0, 0, barBackgroundWidth * ratio, barHeight);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,192 +3,216 @@ import { Theme } from "../../../core/configuration/Config";
|
||||
import { Layer } from "./Layer";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
|
||||
import anchorIcon from '../../../../resources/images/AnchorIcon.png';
|
||||
import missileSiloIcon from '../../../../resources/images/MissileSiloUnit.png';
|
||||
import shieldIcon from '../../../../resources/images/ShieldIcon.png';
|
||||
import cityIcon from '../../../../resources/images/CityIcon.png';
|
||||
import anchorIcon from "../../../../resources/images/AnchorIcon.png";
|
||||
import missileSiloIcon from "../../../../resources/images/MissileSiloUnit.png";
|
||||
import shieldIcon from "../../../../resources/images/ShieldIcon.png";
|
||||
import cityIcon from "../../../../resources/images/CityIcon.png";
|
||||
import { GameView, UnitView } from "../../../core/game/GameView";
|
||||
import { Cell, Unit, UnitType } from "../../../core/game/Game";
|
||||
import { GameUpdateType } from "../../../core/game/GameUpdates";
|
||||
import { euclDistFN } from "../../../core/game/GameMap";
|
||||
|
||||
interface UnitRenderConfig {
|
||||
icon: string;
|
||||
borderRadius: number;
|
||||
territoryRadius: number;
|
||||
icon: string;
|
||||
borderRadius: number;
|
||||
territoryRadius: number;
|
||||
}
|
||||
|
||||
export class StructureLayer implements Layer {
|
||||
private canvas: HTMLCanvasElement;
|
||||
private context: CanvasRenderingContext2D;
|
||||
private unitIcons: Map<string, ImageData> = new Map();
|
||||
private theme: Theme = null;
|
||||
private canvas: HTMLCanvasElement;
|
||||
private context: CanvasRenderingContext2D;
|
||||
private unitIcons: Map<string, ImageData> = new Map();
|
||||
private theme: Theme = null;
|
||||
|
||||
// Configuration for supported unit types only
|
||||
private readonly unitConfigs: Partial<Record<UnitType, UnitRenderConfig>> = {
|
||||
[UnitType.Port]: {
|
||||
icon: anchorIcon,
|
||||
borderRadius: 8,
|
||||
territoryRadius: 6,
|
||||
},
|
||||
[UnitType.MissileSilo]: {
|
||||
icon: missileSiloIcon,
|
||||
borderRadius: 8,
|
||||
territoryRadius: 6,
|
||||
},
|
||||
[UnitType.DefensePost]: {
|
||||
icon: shieldIcon,
|
||||
borderRadius: 8,
|
||||
territoryRadius: 6,
|
||||
},
|
||||
[UnitType.City]: {
|
||||
icon: cityIcon,
|
||||
borderRadius: 8,
|
||||
territoryRadius: 6,
|
||||
},
|
||||
};
|
||||
|
||||
// Configuration for supported unit types only
|
||||
private readonly unitConfigs: Partial<Record<UnitType, UnitRenderConfig>> = {
|
||||
[UnitType.Port]: {
|
||||
icon: anchorIcon,
|
||||
borderRadius: 8,
|
||||
territoryRadius: 6
|
||||
},
|
||||
[UnitType.MissileSilo]: {
|
||||
icon: missileSiloIcon,
|
||||
borderRadius: 8,
|
||||
territoryRadius: 6
|
||||
},
|
||||
[UnitType.DefensePost]: {
|
||||
icon: shieldIcon,
|
||||
borderRadius: 8,
|
||||
territoryRadius: 6
|
||||
},
|
||||
[UnitType.City]: {
|
||||
icon: cityIcon,
|
||||
borderRadius: 8,
|
||||
territoryRadius: 6
|
||||
}
|
||||
};
|
||||
constructor(
|
||||
private game: GameView,
|
||||
private eventBus: EventBus,
|
||||
) {
|
||||
this.theme = game.config().theme();
|
||||
this.loadIconData();
|
||||
}
|
||||
|
||||
constructor(private game: GameView, private eventBus: EventBus) {
|
||||
this.theme = game.config().theme();
|
||||
this.loadIconData();
|
||||
}
|
||||
private loadIconData() {
|
||||
Object.entries(this.unitConfigs).forEach(([unitType, config]) => {
|
||||
const image = new Image();
|
||||
image.src = config.icon;
|
||||
image.onload = () => {
|
||||
// Create temporary canvas for icon processing
|
||||
const tempCanvas = document.createElement("canvas");
|
||||
const tempContext = tempCanvas.getContext("2d");
|
||||
tempCanvas.width = image.width;
|
||||
tempCanvas.height = image.height;
|
||||
|
||||
private loadIconData() {
|
||||
Object.entries(this.unitConfigs).forEach(([unitType, config]) => {
|
||||
const image = new Image();
|
||||
image.src = config.icon;
|
||||
image.onload = () => {
|
||||
// Create temporary canvas for icon processing
|
||||
const tempCanvas = document.createElement('canvas');
|
||||
const tempContext = tempCanvas.getContext('2d');
|
||||
tempCanvas.width = image.width;
|
||||
tempCanvas.height = image.height;
|
||||
|
||||
// Draw the unit icon
|
||||
tempContext.drawImage(image, 0, 0);
|
||||
const iconData = tempContext.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
|
||||
this.unitIcons.set(unitType, iconData)
|
||||
console.log(`icond data width height: ${iconData.width}, ${iconData.height}`)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
shouldTransform(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
tick() {
|
||||
this.game.updatesSinceLastTick()[GameUpdateType.Unit]
|
||||
.forEach(u => this.handleUnitRendering(this.game.unit(u.id)));
|
||||
}
|
||||
|
||||
init() {
|
||||
this.redraw();
|
||||
}
|
||||
|
||||
redraw() {
|
||||
console.log('structure layer redrawing');
|
||||
this.canvas = document.createElement('canvas');
|
||||
this.context = this.canvas.getContext("2d", { alpha: true });
|
||||
this.canvas.width = this.game.width();
|
||||
this.canvas.height = this.game.height();
|
||||
this.game.units().forEach(u => this.handleUnitRendering(u));
|
||||
}
|
||||
|
||||
renderLayer(context: CanvasRenderingContext2D) {
|
||||
context.drawImage(
|
||||
this.canvas,
|
||||
-this.game.width() / 2,
|
||||
-this.game.height() / 2,
|
||||
this.game.width(),
|
||||
this.game.height()
|
||||
// Draw the unit icon
|
||||
tempContext.drawImage(image, 0, 0);
|
||||
const iconData = tempContext.getImageData(
|
||||
0,
|
||||
0,
|
||||
tempCanvas.width,
|
||||
tempCanvas.height,
|
||||
);
|
||||
this.unitIcons.set(unitType, iconData);
|
||||
console.log(
|
||||
`icond data width height: ${iconData.width}, ${iconData.height}`,
|
||||
);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
shouldTransform(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
tick() {
|
||||
this.game
|
||||
.updatesSinceLastTick()
|
||||
[
|
||||
GameUpdateType.Unit
|
||||
].forEach((u) => this.handleUnitRendering(this.game.unit(u.id)));
|
||||
}
|
||||
|
||||
init() {
|
||||
this.redraw();
|
||||
}
|
||||
|
||||
redraw() {
|
||||
console.log("structure layer redrawing");
|
||||
this.canvas = document.createElement("canvas");
|
||||
this.context = this.canvas.getContext("2d", { alpha: true });
|
||||
this.canvas.width = this.game.width();
|
||||
this.canvas.height = this.game.height();
|
||||
this.game.units().forEach((u) => this.handleUnitRendering(u));
|
||||
}
|
||||
|
||||
renderLayer(context: CanvasRenderingContext2D) {
|
||||
context.drawImage(
|
||||
this.canvas,
|
||||
-this.game.width() / 2,
|
||||
-this.game.height() / 2,
|
||||
this.game.width(),
|
||||
this.game.height(),
|
||||
);
|
||||
}
|
||||
|
||||
private isUnitTypeSupported(unitType: UnitType): boolean {
|
||||
return unitType in this.unitConfigs;
|
||||
}
|
||||
|
||||
private handleUnitRendering(unit: UnitView) {
|
||||
const unitType = unit.type();
|
||||
if (!this.isUnitTypeSupported(unitType)) return;
|
||||
|
||||
const config = this.unitConfigs[unitType];
|
||||
const icon = this.unitIcons.get(unitType);
|
||||
|
||||
if (!config || !icon) return;
|
||||
|
||||
// Clear previous rendering
|
||||
for (const tile of this.game.bfs(
|
||||
unit.tile(),
|
||||
euclDistFN(unit.tile(), config.borderRadius),
|
||||
)) {
|
||||
this.clearCell(new Cell(this.game.x(tile), this.game.y(tile)));
|
||||
}
|
||||
|
||||
private isUnitTypeSupported(unitType: UnitType): boolean {
|
||||
return unitType in this.unitConfigs;
|
||||
if (!unit.isActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
private handleUnitRendering(unit: UnitView) {
|
||||
const unitType = unit.type();
|
||||
if (!this.isUnitTypeSupported(unitType)) return;
|
||||
// Draw border and territory
|
||||
for (const tile of this.game.bfs(
|
||||
unit.tile(),
|
||||
euclDistFN(unit.tile(), config.borderRadius),
|
||||
)) {
|
||||
this.paintCell(
|
||||
new Cell(this.game.x(tile), this.game.y(tile)),
|
||||
this.theme.borderColor(unit.owner().info()),
|
||||
255,
|
||||
);
|
||||
}
|
||||
|
||||
for (const tile of this.game.bfs(
|
||||
unit.tile(),
|
||||
euclDistFN(unit.tile(), config.territoryRadius),
|
||||
)) {
|
||||
this.paintCell(
|
||||
new Cell(this.game.x(tile), this.game.y(tile)),
|
||||
this.theme.territoryColor(unit.owner().info()),
|
||||
130,
|
||||
);
|
||||
}
|
||||
|
||||
const config = this.unitConfigs[unitType];
|
||||
const icon = this.unitIcons.get(unitType);
|
||||
const startX = this.game.x(unit.tile()) - Math.floor(icon.width / 2);
|
||||
const startY = this.game.y(unit.tile()) - Math.floor(icon.height / 2);
|
||||
// Draw the icon
|
||||
this.renderIcon(icon, startX, startY, icon.width, icon.height, unit);
|
||||
}
|
||||
|
||||
if (!config || !icon) return;
|
||||
private renderIcon(
|
||||
iconData: ImageData,
|
||||
startX: number,
|
||||
startY: number,
|
||||
width: number,
|
||||
height: number,
|
||||
unit: UnitView,
|
||||
) {
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
const iconIndex = (y * width + x) * 4;
|
||||
const alpha = iconData.data[iconIndex + 3];
|
||||
|
||||
// Clear previous rendering
|
||||
for (const tile of this.game.bfs(unit.tile(), euclDistFN(unit.tile(), config.borderRadius))) {
|
||||
this.clearCell(new Cell(this.game.x(tile), this.game.y(tile)));
|
||||
}
|
||||
if (alpha > 0) {
|
||||
const targetX = startX + x;
|
||||
const targetY = startY + y;
|
||||
|
||||
if (!unit.isActive()) {
|
||||
return
|
||||
}
|
||||
|
||||
// Draw border and territory
|
||||
for (const tile of this.game.bfs(unit.tile(), euclDistFN(unit.tile(), config.borderRadius))) {
|
||||
if (
|
||||
targetX >= 0 &&
|
||||
targetX < this.game.width() &&
|
||||
targetY >= 0 &&
|
||||
targetY < this.game.height()
|
||||
) {
|
||||
this.paintCell(
|
||||
new Cell(this.game.x(tile), this.game.y(tile)),
|
||||
this.theme.borderColor(unit.owner().info()),
|
||||
255
|
||||
new Cell(targetX, targetY),
|
||||
this.theme.borderColor(unit.owner().info()),
|
||||
alpha,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const tile of this.game.bfs(unit.tile(), euclDistFN(unit.tile(), config.territoryRadius))) {
|
||||
this.paintCell(
|
||||
new Cell(this.game.x(tile), this.game.y(tile)),
|
||||
this.theme.territoryColor(unit.owner().info()),
|
||||
130
|
||||
);
|
||||
}
|
||||
|
||||
const startX = this.game.x(unit.tile()) - Math.floor(icon.width / 2);
|
||||
const startY = this.game.y(unit.tile()) - Math.floor(icon.height / 2);
|
||||
// Draw the icon
|
||||
this.renderIcon(icon, startX, startY, icon.width, icon.height, unit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private renderIcon(
|
||||
iconData: ImageData,
|
||||
startX: number,
|
||||
startY: number,
|
||||
width: number,
|
||||
height: number,
|
||||
unit: UnitView
|
||||
) {
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
const iconIndex = (y * width + x) * 4;
|
||||
const alpha = iconData.data[iconIndex + 3];
|
||||
paintCell(cell: Cell, color: Colord, alpha: number) {
|
||||
this.clearCell(cell);
|
||||
this.context.fillStyle = color.alpha(alpha / 255).toRgbString();
|
||||
this.context.fillRect(cell.x, cell.y, 1, 1);
|
||||
}
|
||||
|
||||
if (alpha > 0) {
|
||||
const targetX = startX + x;
|
||||
const targetY = startY + y;
|
||||
|
||||
if (targetX >= 0 && targetX < this.game.width() &&
|
||||
targetY >= 0 && targetY < this.game.height()) {
|
||||
this.paintCell(
|
||||
new Cell(targetX, targetY),
|
||||
this.theme.borderColor(unit.owner().info()),
|
||||
alpha
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
paintCell(cell: Cell, color: Colord, alpha: number) {
|
||||
this.clearCell(cell);
|
||||
this.context.fillStyle = color.alpha(alpha / 255).toRgbString();
|
||||
this.context.fillRect(cell.x, cell.y, 1, 1);
|
||||
}
|
||||
|
||||
clearCell(cell: Cell) {
|
||||
this.context.clearRect(cell.x, cell.y, 1, 1);
|
||||
}
|
||||
}
|
||||
clearCell(cell: Cell) {
|
||||
this.context.clearRect(cell.x, cell.y, 1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,55 +2,58 @@ import { Layer } from "./Layer";
|
||||
import { GameView } from "../../../core/game/GameView";
|
||||
|
||||
export class TerrainLayer implements Layer {
|
||||
private canvas: HTMLCanvasElement
|
||||
private context: CanvasRenderingContext2D
|
||||
private imageData: ImageData
|
||||
private canvas: HTMLCanvasElement;
|
||||
private context: CanvasRenderingContext2D;
|
||||
private imageData: ImageData;
|
||||
|
||||
constructor(private game: GameView) {}
|
||||
shouldTransform(): boolean {
|
||||
return true;
|
||||
}
|
||||
tick() {}
|
||||
|
||||
constructor(private game: GameView) { }
|
||||
shouldTransform(): boolean {
|
||||
return true
|
||||
}
|
||||
tick() {
|
||||
}
|
||||
init() {
|
||||
console.log("redrew terrain layer");
|
||||
this.redraw();
|
||||
}
|
||||
|
||||
init() {
|
||||
console.log('redrew terrain layer')
|
||||
this.redraw()
|
||||
}
|
||||
redraw(): void {
|
||||
this.canvas = document.createElement("canvas");
|
||||
this.context = this.canvas.getContext("2d");
|
||||
|
||||
redraw(): void {
|
||||
this.canvas = document.createElement('canvas');
|
||||
this.context = this.canvas.getContext("2d")
|
||||
this.imageData = this.context.getImageData(
|
||||
0,
|
||||
0,
|
||||
this.game.width(),
|
||||
this.game.height(),
|
||||
);
|
||||
this.initImageData();
|
||||
this.canvas.width = this.game.width();
|
||||
this.canvas.height = this.game.height();
|
||||
this.context.putImageData(this.imageData, 0, 0);
|
||||
}
|
||||
|
||||
this.imageData = this.context.getImageData(0, 0, this.game.width(), this.game.height())
|
||||
this.initImageData()
|
||||
this.canvas.width = this.game.width();
|
||||
this.canvas.height = this.game.height();
|
||||
this.context.putImageData(this.imageData, 0, 0);
|
||||
}
|
||||
initImageData() {
|
||||
const theme = this.game.config().theme();
|
||||
this.game.forEachTile((tile) => {
|
||||
let terrainColor = theme.terrainColor(this.game, tile);
|
||||
// TODO: isn'te tileref and index the same?
|
||||
const index = this.game.y(tile) * this.game.width() + this.game.x(tile);
|
||||
const offset = index * 4;
|
||||
this.imageData.data[offset] = terrainColor.rgba.r;
|
||||
this.imageData.data[offset + 1] = terrainColor.rgba.g;
|
||||
this.imageData.data[offset + 2] = terrainColor.rgba.b;
|
||||
this.imageData.data[offset + 3] = (terrainColor.rgba.a * 255) | 0;
|
||||
});
|
||||
}
|
||||
|
||||
initImageData() {
|
||||
const theme = this.game.config().theme()
|
||||
this.game.forEachTile((tile) => {
|
||||
let terrainColor = theme.terrainColor(this.game, tile)
|
||||
// TODO: isn'te tileref and index the same?
|
||||
const index = (this.game.y(tile) * this.game.width()) + this.game.x(tile)
|
||||
const offset = index * 4
|
||||
this.imageData.data[offset] = terrainColor.rgba.r;
|
||||
this.imageData.data[offset + 1] = terrainColor.rgba.g;
|
||||
this.imageData.data[offset + 2] = terrainColor.rgba.b;
|
||||
this.imageData.data[offset + 3] = terrainColor.rgba.a * 255 | 0
|
||||
})
|
||||
}
|
||||
|
||||
renderLayer(context: CanvasRenderingContext2D) {
|
||||
context.drawImage(
|
||||
this.canvas,
|
||||
-this.game.width() / 2,
|
||||
-this.game.height() / 2,
|
||||
this.game.width(),
|
||||
this.game.height()
|
||||
)
|
||||
}
|
||||
}
|
||||
renderLayer(context: CanvasRenderingContext2D) {
|
||||
context.drawImage(
|
||||
this.canvas,
|
||||
-this.game.width() / 2,
|
||||
-this.game.height() / 2,
|
||||
this.game.width(),
|
||||
this.game.height(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,222 +1,262 @@
|
||||
import { PriorityQueue } from "@datastructures-js/priority-queue";
|
||||
import { Cell, Game, Player, PlayerType, Unit, UnitType } from "../../../core/game/Game";
|
||||
import {
|
||||
Cell,
|
||||
Game,
|
||||
Player,
|
||||
PlayerType,
|
||||
Unit,
|
||||
UnitType,
|
||||
} from "../../../core/game/Game";
|
||||
import { UnitUpdate } from "../../../core/game/GameUpdates";
|
||||
import { PseudoRandom } from "../../../core/PseudoRandom";
|
||||
import { colord, Colord } from "colord";
|
||||
import { Theme } from "../../../core/configuration/Config";
|
||||
import { Layer } from "./Layer";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
import { AlternateViewEvent, DragEvent, MouseDownEvent } from "../../InputHandler";
|
||||
import {
|
||||
AlternateViewEvent,
|
||||
DragEvent,
|
||||
MouseDownEvent,
|
||||
} from "../../InputHandler";
|
||||
import { GameView, PlayerView } from "../../../core/game/GameView";
|
||||
import { euclDistFN, TileRef } from "../../../core/game/GameMap";
|
||||
|
||||
export class TerritoryLayer implements Layer {
|
||||
private canvas: HTMLCanvasElement;
|
||||
private context: CanvasRenderingContext2D;
|
||||
private imageData: ImageData;
|
||||
private canvas: HTMLCanvasElement;
|
||||
private context: CanvasRenderingContext2D;
|
||||
private imageData: ImageData;
|
||||
|
||||
private tileToRenderQueue: PriorityQueue<{ tile: TileRef, lastUpdate: number }> = new PriorityQueue((a, b) => { return a.lastUpdate - b.lastUpdate });
|
||||
private random = new PseudoRandom(123);
|
||||
private theme: Theme = null;
|
||||
private tileToRenderQueue: PriorityQueue<{
|
||||
tile: TileRef;
|
||||
lastUpdate: number;
|
||||
}> = new PriorityQueue((a, b) => {
|
||||
return a.lastUpdate - b.lastUpdate;
|
||||
});
|
||||
private random = new PseudoRandom(123);
|
||||
private theme: Theme = null;
|
||||
|
||||
// Used for spawn highlighting
|
||||
private highlightCanvas: HTMLCanvasElement;
|
||||
private highlightContext: CanvasRenderingContext2D;
|
||||
// Used for spawn highlighting
|
||||
private highlightCanvas: HTMLCanvasElement;
|
||||
private highlightContext: CanvasRenderingContext2D;
|
||||
|
||||
private alternativeView = false;
|
||||
private lastDragTime = 0;
|
||||
private nodrawDragDuration = 200;
|
||||
private alternativeView = false;
|
||||
private lastDragTime = 0;
|
||||
private nodrawDragDuration = 200;
|
||||
|
||||
private refreshRate = 50;
|
||||
private lastRefresh = 0;
|
||||
private refreshRate = 50;
|
||||
private lastRefresh = 0;
|
||||
|
||||
constructor(private game: GameView, private eventBus: EventBus) {
|
||||
this.theme = game.config().theme();
|
||||
constructor(
|
||||
private game: GameView,
|
||||
private eventBus: EventBus,
|
||||
) {
|
||||
this.theme = game.config().theme();
|
||||
}
|
||||
|
||||
shouldTransform(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
tick() {
|
||||
this.game.recentlyUpdatedTiles().forEach((t) => this.enqueueTile(t));
|
||||
|
||||
if (!this.game.inSpawnPhase()) {
|
||||
return;
|
||||
}
|
||||
if (this.game.ticks() % 5 == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
shouldTransform(): boolean {
|
||||
return true;
|
||||
}
|
||||
this.highlightContext.clearRect(
|
||||
0,
|
||||
0,
|
||||
this.game.width(),
|
||||
this.game.height(),
|
||||
);
|
||||
const humans = this.game
|
||||
.playerViews()
|
||||
.filter((p) => p.type() == PlayerType.Human);
|
||||
|
||||
tick() {
|
||||
this.game.recentlyUpdatedTiles()
|
||||
.forEach(t => this.enqueueTile(t));
|
||||
|
||||
if (!this.game.inSpawnPhase()) {
|
||||
return;
|
||||
}
|
||||
if (this.game.ticks() % 5 == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.highlightContext.clearRect(0, 0, this.game.width(), this.game.height());
|
||||
const humans = this.game.playerViews()
|
||||
.filter(p => p.type() == PlayerType.Human);
|
||||
|
||||
for (const human of humans) {
|
||||
const center = human.nameLocation();
|
||||
if (!center) {
|
||||
continue;
|
||||
}
|
||||
const centerTile = this.game.ref(center.x, center.y)
|
||||
if (!centerTile) {
|
||||
continue;
|
||||
}
|
||||
for (const tile of this.game.bfs(centerTile, euclDistFN(centerTile, 9))) {
|
||||
if (!this.game.hasOwner(tile)) {
|
||||
this.paintHighlightCell(
|
||||
new Cell(this.game.x(tile), this.game.y(tile)),
|
||||
this.theme.spawnHighlightColor(),
|
||||
255
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
this.eventBus.on(AlternateViewEvent, e => { this.alternativeView = e.alternateView; });
|
||||
this.eventBus.on(DragEvent, e => { this.lastDragTime = Date.now(); });
|
||||
this.redraw();
|
||||
}
|
||||
|
||||
redraw() {
|
||||
console.log('redrew territory layer');
|
||||
this.canvas = document.createElement('canvas');
|
||||
this.context = this.canvas.getContext("2d");
|
||||
|
||||
this.imageData = this.context.getImageData(0, 0, this.game.width(), this.game.height());
|
||||
this.initImageData();
|
||||
this.canvas.width = this.game.width();
|
||||
this.canvas.height = this.game.height();
|
||||
this.context.putImageData(this.imageData, 0, 0);
|
||||
|
||||
// Add a second canvas for highlights
|
||||
this.highlightCanvas = document.createElement('canvas');
|
||||
this.highlightContext = this.highlightCanvas.getContext("2d", { alpha: true });
|
||||
this.highlightCanvas.width = this.game.width();
|
||||
this.highlightCanvas.height = this.game.height();
|
||||
|
||||
this.game.forEachTile(t => {
|
||||
this.paintTerritory(t);
|
||||
});
|
||||
}
|
||||
|
||||
initImageData() {
|
||||
this.game.forEachTile((tile) => {
|
||||
const cell = new Cell(this.game.x(tile), this.game.y(tile));
|
||||
const index = (cell.y * this.game.width()) + cell.x;
|
||||
const offset = index * 4;
|
||||
this.imageData.data[offset + 3] = 0;
|
||||
});
|
||||
}
|
||||
|
||||
renderLayer(context: CanvasRenderingContext2D) {
|
||||
if (Date.now() > this.lastDragTime + this.nodrawDragDuration && Date.now() > this.lastRefresh + this.refreshRate) {
|
||||
this.lastRefresh = Date.now();
|
||||
this.renderTerritory();
|
||||
this.context.putImageData(this.imageData, 0, 0);
|
||||
}
|
||||
if (this.alternativeView) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.drawImage(
|
||||
this.canvas,
|
||||
-this.game.width() / 2,
|
||||
-this.game.height() / 2,
|
||||
this.game.width(),
|
||||
this.game.height()
|
||||
);
|
||||
if (this.game.inSpawnPhase()) {
|
||||
context.drawImage(
|
||||
this.highlightCanvas,
|
||||
-this.game.width() / 2,
|
||||
-this.game.height() / 2,
|
||||
this.game.width(),
|
||||
this.game.height()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
renderTerritory() {
|
||||
let numToRender = Math.floor(this.tileToRenderQueue.size() / 5);
|
||||
if (numToRender == 0 || this.game.inSpawnPhase()) {
|
||||
numToRender = this.tileToRenderQueue.size();
|
||||
}
|
||||
|
||||
while (numToRender > 0) {
|
||||
numToRender--;
|
||||
const tile = this.tileToRenderQueue.pop().tile;
|
||||
this.paintTerritory(tile);
|
||||
for (const neighbor of this.game.neighbors(tile)) {
|
||||
this.paintTerritory(neighbor, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
paintTerritory(tile: TileRef, isBorder: boolean = false) {
|
||||
if (isBorder && !this.game.hasOwner(tile)) {
|
||||
return;
|
||||
}
|
||||
for (const human of humans) {
|
||||
const center = human.nameLocation();
|
||||
if (!center) {
|
||||
continue;
|
||||
}
|
||||
const centerTile = this.game.ref(center.x, center.y);
|
||||
if (!centerTile) {
|
||||
continue;
|
||||
}
|
||||
for (const tile of this.game.bfs(centerTile, euclDistFN(centerTile, 9))) {
|
||||
if (!this.game.hasOwner(tile)) {
|
||||
if (this.game.hasFallout(tile)) {
|
||||
this.paintCell(
|
||||
this.game.x(tile),
|
||||
this.game.y(tile),
|
||||
this.theme.falloutColor(),
|
||||
150
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.clearCell(new Cell(this.game.x(tile), this.game.y(tile)));
|
||||
return;
|
||||
}
|
||||
const owner = this.game.owner(tile) as Player;
|
||||
if (this.game.isBorder(tile)) {
|
||||
this.paintCell(
|
||||
this.game.x(tile), this.game.y(tile),
|
||||
this.theme.borderColor(owner.info()),
|
||||
255
|
||||
);
|
||||
} else {
|
||||
this.paintCell(
|
||||
this.game.x(tile), this.game.y(tile),
|
||||
this.theme.territoryColor(owner.info()),
|
||||
150
|
||||
);
|
||||
this.paintHighlightCell(
|
||||
new Cell(this.game.x(tile), this.game.y(tile)),
|
||||
this.theme.spawnHighlightColor(),
|
||||
255,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
this.eventBus.on(AlternateViewEvent, (e) => {
|
||||
this.alternativeView = e.alternateView;
|
||||
});
|
||||
this.eventBus.on(DragEvent, (e) => {
|
||||
this.lastDragTime = Date.now();
|
||||
});
|
||||
this.redraw();
|
||||
}
|
||||
|
||||
redraw() {
|
||||
console.log("redrew territory layer");
|
||||
this.canvas = document.createElement("canvas");
|
||||
this.context = this.canvas.getContext("2d");
|
||||
|
||||
this.imageData = this.context.getImageData(
|
||||
0,
|
||||
0,
|
||||
this.game.width(),
|
||||
this.game.height(),
|
||||
);
|
||||
this.initImageData();
|
||||
this.canvas.width = this.game.width();
|
||||
this.canvas.height = this.game.height();
|
||||
this.context.putImageData(this.imageData, 0, 0);
|
||||
|
||||
// Add a second canvas for highlights
|
||||
this.highlightCanvas = document.createElement("canvas");
|
||||
this.highlightContext = this.highlightCanvas.getContext("2d", {
|
||||
alpha: true,
|
||||
});
|
||||
this.highlightCanvas.width = this.game.width();
|
||||
this.highlightCanvas.height = this.game.height();
|
||||
|
||||
this.game.forEachTile((t) => {
|
||||
this.paintTerritory(t);
|
||||
});
|
||||
}
|
||||
|
||||
initImageData() {
|
||||
this.game.forEachTile((tile) => {
|
||||
const cell = new Cell(this.game.x(tile), this.game.y(tile));
|
||||
const index = cell.y * this.game.width() + cell.x;
|
||||
const offset = index * 4;
|
||||
this.imageData.data[offset + 3] = 0;
|
||||
});
|
||||
}
|
||||
|
||||
renderLayer(context: CanvasRenderingContext2D) {
|
||||
if (
|
||||
Date.now() > this.lastDragTime + this.nodrawDragDuration &&
|
||||
Date.now() > this.lastRefresh + this.refreshRate
|
||||
) {
|
||||
this.lastRefresh = Date.now();
|
||||
this.renderTerritory();
|
||||
this.context.putImageData(this.imageData, 0, 0);
|
||||
}
|
||||
if (this.alternativeView) {
|
||||
return;
|
||||
}
|
||||
|
||||
paintCell(x: number, y: number, color: Colord, alpha: number) {
|
||||
const index = (y * this.game.width()) + x;
|
||||
const offset = index * 4;
|
||||
this.imageData.data[offset] = color.rgba.r;
|
||||
this.imageData.data[offset + 1] = color.rgba.g;
|
||||
this.imageData.data[offset + 2] = color.rgba.b;
|
||||
this.imageData.data[offset + 3] = alpha;
|
||||
context.drawImage(
|
||||
this.canvas,
|
||||
-this.game.width() / 2,
|
||||
-this.game.height() / 2,
|
||||
this.game.width(),
|
||||
this.game.height(),
|
||||
);
|
||||
if (this.game.inSpawnPhase()) {
|
||||
context.drawImage(
|
||||
this.highlightCanvas,
|
||||
-this.game.width() / 2,
|
||||
-this.game.height() / 2,
|
||||
this.game.width(),
|
||||
this.game.height(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
renderTerritory() {
|
||||
let numToRender = Math.floor(this.tileToRenderQueue.size() / 5);
|
||||
if (numToRender == 0 || this.game.inSpawnPhase()) {
|
||||
numToRender = this.tileToRenderQueue.size();
|
||||
}
|
||||
|
||||
clearCell(cell: Cell) {
|
||||
const index = (cell.y * this.game.width()) + cell.x;
|
||||
const offset = index * 4;
|
||||
this.imageData.data[offset + 3] = 0; // Set alpha to 0 (fully transparent)
|
||||
while (numToRender > 0) {
|
||||
numToRender--;
|
||||
const tile = this.tileToRenderQueue.pop().tile;
|
||||
this.paintTerritory(tile);
|
||||
for (const neighbor of this.game.neighbors(tile)) {
|
||||
this.paintTerritory(neighbor, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enqueueTile(tile: TileRef) {
|
||||
this.tileToRenderQueue.push({
|
||||
tile: tile,
|
||||
lastUpdate: this.game.ticks() + this.random.nextFloat(0, .5)
|
||||
});
|
||||
paintTerritory(tile: TileRef, isBorder: boolean = false) {
|
||||
if (isBorder && !this.game.hasOwner(tile)) {
|
||||
return;
|
||||
}
|
||||
if (!this.game.hasOwner(tile)) {
|
||||
if (this.game.hasFallout(tile)) {
|
||||
this.paintCell(
|
||||
this.game.x(tile),
|
||||
this.game.y(tile),
|
||||
this.theme.falloutColor(),
|
||||
150,
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.clearCell(new Cell(this.game.x(tile), this.game.y(tile)));
|
||||
return;
|
||||
}
|
||||
const owner = this.game.owner(tile) as Player;
|
||||
if (this.game.isBorder(tile)) {
|
||||
this.paintCell(
|
||||
this.game.x(tile),
|
||||
this.game.y(tile),
|
||||
this.theme.borderColor(owner.info()),
|
||||
255,
|
||||
);
|
||||
} else {
|
||||
this.paintCell(
|
||||
this.game.x(tile),
|
||||
this.game.y(tile),
|
||||
this.theme.territoryColor(owner.info()),
|
||||
150,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
paintHighlightCell(cell: Cell, color: Colord, alpha: number) {
|
||||
this.clearCell(cell);
|
||||
this.highlightContext.fillStyle = color.alpha(alpha / 255).toRgbString();
|
||||
this.highlightContext.fillRect(cell.x, cell.y, 1, 1);
|
||||
}
|
||||
paintCell(x: number, y: number, color: Colord, alpha: number) {
|
||||
const index = y * this.game.width() + x;
|
||||
const offset = index * 4;
|
||||
this.imageData.data[offset] = color.rgba.r;
|
||||
this.imageData.data[offset + 1] = color.rgba.g;
|
||||
this.imageData.data[offset + 2] = color.rgba.b;
|
||||
this.imageData.data[offset + 3] = alpha;
|
||||
}
|
||||
|
||||
clearHighlightCell(cell: Cell) {
|
||||
this.highlightContext.clearRect(cell.x, cell.y, 1, 1);
|
||||
}
|
||||
}
|
||||
clearCell(cell: Cell) {
|
||||
const index = cell.y * this.game.width() + cell.x;
|
||||
const offset = index * 4;
|
||||
this.imageData.data[offset + 3] = 0; // Set alpha to 0 (fully transparent)
|
||||
}
|
||||
|
||||
enqueueTile(tile: TileRef) {
|
||||
this.tileToRenderQueue.push({
|
||||
tile: tile,
|
||||
lastUpdate: this.game.ticks() + this.random.nextFloat(0, 0.5),
|
||||
});
|
||||
}
|
||||
|
||||
paintHighlightCell(cell: Cell, color: Colord, alpha: number) {
|
||||
this.clearCell(cell);
|
||||
this.highlightContext.fillStyle = color.alpha(alpha / 255).toRgbString();
|
||||
this.highlightContext.fillRect(cell.x, cell.y, 1, 1);
|
||||
}
|
||||
|
||||
clearHighlightCell(cell: Cell) {
|
||||
this.highlightContext.clearRect(cell.x, cell.y, 1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,358 +7,404 @@ import { EventBus } from "../../../core/EventBus";
|
||||
import { AlternateViewEvent } from "../../InputHandler";
|
||||
import { ClientID } from "../../../core/Schemas";
|
||||
import { GameView, PlayerView, UnitView } from "../../../core/game/GameView";
|
||||
import { euclDistFN, manhattanDistFN, TileRef } from "../../../core/game/GameMap";
|
||||
import {
|
||||
euclDistFN,
|
||||
manhattanDistFN,
|
||||
TileRef,
|
||||
} from "../../../core/game/GameMap";
|
||||
|
||||
enum Relationship {
|
||||
Self,
|
||||
Ally,
|
||||
Enemy
|
||||
Self,
|
||||
Ally,
|
||||
Enemy,
|
||||
}
|
||||
|
||||
export class UnitLayer implements Layer {
|
||||
private canvas: HTMLCanvasElement;
|
||||
private context: CanvasRenderingContext2D;
|
||||
private canvas: HTMLCanvasElement;
|
||||
private context: CanvasRenderingContext2D;
|
||||
|
||||
private boatToTrail = new Map<UnitView, Set<TileRef>>();
|
||||
private boatToTrail = new Map<UnitView, Set<TileRef>>();
|
||||
|
||||
private theme: Theme = null;
|
||||
private theme: Theme = null;
|
||||
|
||||
private alternateView = false;
|
||||
private alternateView = false;
|
||||
|
||||
private myPlayer: PlayerView | null = null;
|
||||
private myPlayer: PlayerView | null = null;
|
||||
|
||||
private oldShellTile = new Map<UnitView, TileRef>();
|
||||
private oldShellTile = new Map<UnitView, TileRef>();
|
||||
|
||||
constructor(private game: GameView, private eventBus: EventBus, private clientID: ClientID) {
|
||||
this.theme = game.config().theme();
|
||||
constructor(
|
||||
private game: GameView,
|
||||
private eventBus: EventBus,
|
||||
private clientID: ClientID,
|
||||
) {
|
||||
this.theme = game.config().theme();
|
||||
}
|
||||
|
||||
shouldTransform(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
tick() {
|
||||
if (this.myPlayer == null) {
|
||||
this.myPlayer = this.game.playerByClientID(this.clientID);
|
||||
}
|
||||
for (const unit of this.game.units()) {
|
||||
if (unit.wasUpdated()) this.onUnitEvent(unit);
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
this.eventBus.on(AlternateViewEvent, (e) => this.onAlternativeViewEvent(e));
|
||||
this.redraw();
|
||||
}
|
||||
|
||||
renderLayer(context: CanvasRenderingContext2D) {
|
||||
context.drawImage(
|
||||
this.canvas,
|
||||
-this.game.width() / 2,
|
||||
-this.game.height() / 2,
|
||||
this.game.width(),
|
||||
this.game.height(),
|
||||
);
|
||||
}
|
||||
|
||||
onAlternativeViewEvent(event: AlternateViewEvent) {
|
||||
this.alternateView = event.alternateView;
|
||||
this.redraw();
|
||||
}
|
||||
|
||||
redraw() {
|
||||
this.canvas = document.createElement("canvas");
|
||||
this.context = this.canvas.getContext("2d");
|
||||
|
||||
this.canvas.width = this.game.width();
|
||||
this.canvas.height = this.game.height();
|
||||
for (const unit of this.game.units()) {
|
||||
// this.onUnitEvent(new UnitEvent(unit, unit.tile()))
|
||||
}
|
||||
}
|
||||
|
||||
private relationship(unit: UnitView): Relationship {
|
||||
if (this.myPlayer == null) {
|
||||
return Relationship.Enemy;
|
||||
}
|
||||
if (this.myPlayer == unit.owner()) {
|
||||
return Relationship.Self;
|
||||
}
|
||||
if (this.myPlayer.isAlliedWith(unit.owner())) {
|
||||
return Relationship.Ally;
|
||||
}
|
||||
return Relationship.Enemy;
|
||||
}
|
||||
|
||||
onUnitEvent(unit: UnitView) {
|
||||
switch (unit.type()) {
|
||||
case UnitType.TransportShip:
|
||||
this.handleBoatEvent(unit);
|
||||
break;
|
||||
case UnitType.Destroyer:
|
||||
this.handleDestroyerEvent(unit);
|
||||
break;
|
||||
case UnitType.Battleship:
|
||||
this.handleBattleshipEvent(unit);
|
||||
break;
|
||||
case UnitType.Shell:
|
||||
this.handleShellEvent(unit);
|
||||
break;
|
||||
case UnitType.TradeShip:
|
||||
this.handleTradeShipEvent(unit);
|
||||
break;
|
||||
case UnitType.AtomBomb:
|
||||
case UnitType.HydrogenBomb:
|
||||
this.handleNuke(unit);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private handleDestroyerEvent(unit: UnitView) {
|
||||
const rel = this.relationship(unit);
|
||||
|
||||
// Clear previous area
|
||||
for (const t of this.game.bfs(
|
||||
unit.lastTile(),
|
||||
euclDistFN(unit.lastTile(), 4),
|
||||
)) {
|
||||
this.clearCell(this.game.x(t), this.game.y(t));
|
||||
}
|
||||
|
||||
shouldTransform(): boolean {
|
||||
return true;
|
||||
if (!unit.isActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
tick() {
|
||||
if (this.myPlayer == null) {
|
||||
this.myPlayer = this.game.playerByClientID(this.clientID);
|
||||
}
|
||||
for (const unit of this.game.units()) {
|
||||
if (unit.wasUpdated())
|
||||
this.onUnitEvent(unit);
|
||||
}
|
||||
// Paint border
|
||||
for (const t of this.game.bfs(unit.tile(), euclDistFN(unit.tile(), 4))) {
|
||||
this.paintCell(
|
||||
this.game.x(t),
|
||||
this.game.y(t),
|
||||
rel,
|
||||
this.theme.borderColor(unit.owner().info()),
|
||||
255,
|
||||
);
|
||||
}
|
||||
|
||||
init() {
|
||||
this.eventBus.on(AlternateViewEvent, e => this.onAlternativeViewEvent(e));
|
||||
this.redraw();
|
||||
// Paint territory
|
||||
for (const t of this.game.bfs(
|
||||
unit.tile(),
|
||||
manhattanDistFN(unit.tile(), 3),
|
||||
)) {
|
||||
this.paintCell(
|
||||
this.game.x(t),
|
||||
this.game.y(t),
|
||||
rel,
|
||||
this.theme.territoryColor(unit.owner().info()),
|
||||
255,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private handleBattleshipEvent(unit: UnitView) {
|
||||
const rel = this.relationship(unit);
|
||||
|
||||
// Clear previous area
|
||||
for (const t of this.game.bfs(
|
||||
unit.lastTile(),
|
||||
euclDistFN(unit.lastTile(), 6),
|
||||
)) {
|
||||
this.clearCell(this.game.x(t), this.game.y(t));
|
||||
}
|
||||
|
||||
renderLayer(context: CanvasRenderingContext2D) {
|
||||
context.drawImage(
|
||||
this.canvas,
|
||||
-this.game.width() / 2,
|
||||
-this.game.height() / 2,
|
||||
this.game.width(),
|
||||
this.game.height()
|
||||
);
|
||||
if (!unit.isActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
onAlternativeViewEvent(event: AlternateViewEvent) {
|
||||
this.alternateView = event.alternateView;
|
||||
this.redraw();
|
||||
// Paint outer territory
|
||||
for (const t of this.game.bfs(unit.tile(), euclDistFN(unit.tile(), 5))) {
|
||||
this.paintCell(
|
||||
this.game.x(t),
|
||||
this.game.y(t),
|
||||
rel,
|
||||
this.theme.territoryColor(unit.owner().info()),
|
||||
255,
|
||||
);
|
||||
}
|
||||
|
||||
redraw() {
|
||||
this.canvas = document.createElement('canvas');
|
||||
this.context = this.canvas.getContext("2d");
|
||||
|
||||
this.canvas.width = this.game.width();
|
||||
this.canvas.height = this.game.height();
|
||||
for (const unit of this.game.units()) {
|
||||
// this.onUnitEvent(new UnitEvent(unit, unit.tile()))
|
||||
}
|
||||
// Paint border
|
||||
for (const t of this.game.bfs(
|
||||
unit.tile(),
|
||||
manhattanDistFN(unit.tile(), 4),
|
||||
)) {
|
||||
this.paintCell(
|
||||
this.game.x(t),
|
||||
this.game.y(t),
|
||||
rel,
|
||||
this.theme.borderColor(unit.owner().info()),
|
||||
255,
|
||||
);
|
||||
}
|
||||
|
||||
private relationship(unit: UnitView): Relationship {
|
||||
if (this.myPlayer == null) {
|
||||
return Relationship.Enemy;
|
||||
}
|
||||
if (this.myPlayer == unit.owner()) {
|
||||
return Relationship.Self;
|
||||
}
|
||||
if (this.myPlayer.isAlliedWith(unit.owner())) {
|
||||
return Relationship.Ally;
|
||||
}
|
||||
return Relationship.Enemy;
|
||||
// Paint inner territory
|
||||
for (const t of this.game.bfs(unit.tile(), euclDistFN(unit.tile(), 1))) {
|
||||
this.paintCell(
|
||||
this.game.x(t),
|
||||
this.game.y(t),
|
||||
rel,
|
||||
this.theme.territoryColor(unit.owner().info()),
|
||||
255,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private handleShellEvent(unit: UnitView) {
|
||||
const rel = this.relationship(unit);
|
||||
|
||||
// Clear current and previous positions
|
||||
this.clearCell(this.game.x(unit.lastTile()), this.game.y(unit.lastTile()));
|
||||
if (this.oldShellTile.has(unit)) {
|
||||
const oldTile = this.oldShellTile.get(unit);
|
||||
this.clearCell(this.game.x(oldTile), this.game.y(oldTile));
|
||||
}
|
||||
|
||||
onUnitEvent(unit: UnitView) {
|
||||
switch (unit.type()) {
|
||||
case UnitType.TransportShip:
|
||||
this.handleBoatEvent(unit);
|
||||
break;
|
||||
case UnitType.Destroyer:
|
||||
this.handleDestroyerEvent(unit);
|
||||
break;
|
||||
case UnitType.Battleship:
|
||||
this.handleBattleshipEvent(unit);
|
||||
break;
|
||||
case UnitType.Shell:
|
||||
this.handleShellEvent(unit);
|
||||
break;
|
||||
case UnitType.TradeShip:
|
||||
this.handleTradeShipEvent(unit);
|
||||
break;
|
||||
case UnitType.AtomBomb:
|
||||
case UnitType.HydrogenBomb:
|
||||
this.handleNuke(unit);
|
||||
break;
|
||||
}
|
||||
this.oldShellTile.set(unit, unit.lastTile());
|
||||
if (!unit.isActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
private handleDestroyerEvent(unit: UnitView) {
|
||||
const rel = this.relationship(unit);
|
||||
// Paint current and previous positions
|
||||
this.paintCell(
|
||||
this.game.x(unit.tile()),
|
||||
this.game.y(unit.tile()),
|
||||
rel,
|
||||
this.theme.borderColor(unit.owner().info()),
|
||||
255,
|
||||
);
|
||||
this.paintCell(
|
||||
this.game.x(unit.lastTile()),
|
||||
this.game.y(unit.lastTile()),
|
||||
rel,
|
||||
this.theme.borderColor(unit.owner().info()),
|
||||
255,
|
||||
);
|
||||
}
|
||||
|
||||
// Clear previous area
|
||||
for (const t of this.game.bfs(unit.lastTile(), euclDistFN(unit.lastTile(), 4))) {
|
||||
this.clearCell(this.game.x(t), this.game.y(t));
|
||||
}
|
||||
private handleNuke(unit: UnitView) {
|
||||
const rel = this.relationship(unit);
|
||||
|
||||
if (!unit.isActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Paint border
|
||||
for (const t of this.game.bfs(unit.tile(), euclDistFN(unit.tile(), 4))) {
|
||||
this.paintCell(
|
||||
this.game.x(t),
|
||||
this.game.y(t),
|
||||
rel,
|
||||
this.theme.borderColor(unit.owner().info()),
|
||||
255
|
||||
);
|
||||
}
|
||||
|
||||
// Paint territory
|
||||
for (const t of this.game.bfs(unit.tile(), manhattanDistFN(unit.tile(), 3))) {
|
||||
this.paintCell(
|
||||
this.game.x(t),
|
||||
this.game.y(t),
|
||||
rel,
|
||||
this.theme.territoryColor(unit.owner().info()),
|
||||
255
|
||||
);
|
||||
}
|
||||
// Clear previous area
|
||||
for (const t of this.game.bfs(
|
||||
unit.lastTile(),
|
||||
euclDistFN(unit.lastTile(), 2),
|
||||
)) {
|
||||
this.clearCell(this.game.x(t), this.game.y(t));
|
||||
}
|
||||
|
||||
private handleBattleshipEvent(unit: UnitView) {
|
||||
const rel = this.relationship(unit);
|
||||
|
||||
// Clear previous area
|
||||
for (const t of this.game.bfs(unit.lastTile(), euclDistFN(unit.lastTile(), 6))) {
|
||||
this.clearCell(this.game.x(t), this.game.y(t));
|
||||
}
|
||||
|
||||
if (!unit.isActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Paint outer territory
|
||||
for (const t of this.game.bfs(unit.tile(), euclDistFN(unit.tile(), 5))) {
|
||||
this.paintCell(
|
||||
this.game.x(t),
|
||||
this.game.y(t),
|
||||
rel,
|
||||
this.theme.territoryColor(unit.owner().info()),
|
||||
255
|
||||
);
|
||||
}
|
||||
|
||||
// Paint border
|
||||
for (const t of this.game.bfs(unit.tile(), manhattanDistFN(unit.tile(), 4))) {
|
||||
this.paintCell(
|
||||
this.game.x(t),
|
||||
this.game.y(t),
|
||||
rel,
|
||||
this.theme.borderColor(unit.owner().info()),
|
||||
255
|
||||
);
|
||||
}
|
||||
|
||||
// Paint inner territory
|
||||
for (const t of this.game.bfs(unit.tile(), euclDistFN(unit.tile(), 1))) {
|
||||
this.paintCell(
|
||||
this.game.x(t),
|
||||
this.game.y(t),
|
||||
rel,
|
||||
this.theme.territoryColor(unit.owner().info()),
|
||||
255
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private handleShellEvent(unit: UnitView) {
|
||||
const rel = this.relationship(unit);
|
||||
|
||||
// Clear current and previous positions
|
||||
this.clearCell(this.game.x(unit.lastTile()), this.game.y(unit.lastTile()));
|
||||
if (this.oldShellTile.has(unit)) {
|
||||
const oldTile = this.oldShellTile.get(unit);
|
||||
this.clearCell(this.game.x(oldTile), this.game.y(oldTile));
|
||||
}
|
||||
|
||||
this.oldShellTile.set(unit, unit.lastTile());
|
||||
if (!unit.isActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Paint current and previous positions
|
||||
if (unit.isActive()) {
|
||||
// Paint area
|
||||
for (const t of this.game.bfs(unit.tile(), euclDistFN(unit.tile(), 2))) {
|
||||
this.paintCell(
|
||||
this.game.x(unit.tile()),
|
||||
this.game.y(unit.tile()),
|
||||
rel,
|
||||
this.theme.borderColor(unit.owner().info()),
|
||||
255
|
||||
this.game.x(t),
|
||||
this.game.y(t),
|
||||
rel,
|
||||
this.theme.borderColor(unit.owner().info()),
|
||||
255,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private handleTradeShipEvent(unit: UnitView) {
|
||||
const rel = this.relationship(unit);
|
||||
|
||||
// Clear previous area
|
||||
for (const t of this.game.bfs(
|
||||
unit.lastTile(),
|
||||
euclDistFN(unit.lastTile(), 3),
|
||||
)) {
|
||||
this.clearCell(this.game.x(t), this.game.y(t));
|
||||
}
|
||||
|
||||
if (unit.isActive()) {
|
||||
// Paint territory
|
||||
for (const t of this.game.bfs(
|
||||
unit.tile(),
|
||||
manhattanDistFN(unit.tile(), 2),
|
||||
)) {
|
||||
this.paintCell(
|
||||
this.game.x(unit.lastTile()),
|
||||
this.game.y(unit.lastTile()),
|
||||
rel,
|
||||
this.theme.borderColor(unit.owner().info()),
|
||||
255
|
||||
this.game.x(t),
|
||||
this.game.y(t),
|
||||
rel,
|
||||
this.theme.territoryColor(unit.owner().info()),
|
||||
255,
|
||||
);
|
||||
}
|
||||
|
||||
// Paint border
|
||||
for (const t of this.game.bfs(
|
||||
unit.tile(),
|
||||
manhattanDistFN(unit.tile(), 1),
|
||||
)) {
|
||||
this.paintCell(
|
||||
this.game.x(t),
|
||||
this.game.y(t),
|
||||
rel,
|
||||
this.theme.borderColor(unit.owner().info()),
|
||||
255,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private handleBoatEvent(unit: UnitView) {
|
||||
const rel = this.relationship(unit);
|
||||
|
||||
if (!this.boatToTrail.has(unit)) {
|
||||
this.boatToTrail.set(unit, new Set<TileRef>());
|
||||
}
|
||||
const trail = this.boatToTrail.get(unit);
|
||||
trail.add(unit.lastTile());
|
||||
|
||||
// Clear previous area
|
||||
for (const t of this.game.bfs(
|
||||
unit.lastTile(),
|
||||
manhattanDistFN(unit.lastTile(), 3),
|
||||
)) {
|
||||
this.clearCell(this.game.x(t), this.game.y(t));
|
||||
}
|
||||
|
||||
private handleNuke(unit: UnitView) {
|
||||
const rel = this.relationship(unit);
|
||||
if (unit.isActive()) {
|
||||
// Paint trail
|
||||
for (const t of trail) {
|
||||
this.paintCell(
|
||||
this.game.x(t),
|
||||
this.game.y(t),
|
||||
rel,
|
||||
this.theme.territoryColor(unit.owner().info()),
|
||||
150,
|
||||
);
|
||||
}
|
||||
|
||||
// Clear previous area
|
||||
for (const t of this.game.bfs(unit.lastTile(), euclDistFN(unit.lastTile(), 2))) {
|
||||
this.clearCell(this.game.x(t), this.game.y(t));
|
||||
}
|
||||
// Paint border
|
||||
for (const t of this.game.bfs(
|
||||
unit.tile(),
|
||||
manhattanDistFN(unit.tile(), 2),
|
||||
)) {
|
||||
this.paintCell(
|
||||
this.game.x(t),
|
||||
this.game.y(t),
|
||||
rel,
|
||||
this.theme.borderColor(unit.owner().info()),
|
||||
255,
|
||||
);
|
||||
}
|
||||
|
||||
if (unit.isActive()) {
|
||||
// Paint area
|
||||
for (const t of this.game.bfs(unit.tile(), euclDistFN(unit.tile(), 2))) {
|
||||
this.paintCell(
|
||||
this.game.x(t),
|
||||
this.game.y(t),
|
||||
rel,
|
||||
this.theme.borderColor(unit.owner().info()),
|
||||
255
|
||||
);
|
||||
}
|
||||
}
|
||||
// Paint territory
|
||||
for (const t of this.game.bfs(
|
||||
unit.tile(),
|
||||
manhattanDistFN(unit.tile(), 1),
|
||||
)) {
|
||||
this.paintCell(
|
||||
this.game.x(t),
|
||||
this.game.y(t),
|
||||
rel,
|
||||
this.theme.territoryColor(unit.owner().info()),
|
||||
255,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
for (const t of trail) {
|
||||
this.clearCell(this.game.x(t), this.game.y(t));
|
||||
}
|
||||
this.boatToTrail.delete(unit);
|
||||
}
|
||||
}
|
||||
|
||||
private handleTradeShipEvent(unit: UnitView) {
|
||||
const rel = this.relationship(unit);
|
||||
|
||||
// Clear previous area
|
||||
for (const t of this.game.bfs(unit.lastTile(), euclDistFN(unit.lastTile(), 3))) {
|
||||
this.clearCell(this.game.x(t), this.game.y(t));
|
||||
}
|
||||
|
||||
if (unit.isActive()) {
|
||||
// Paint territory
|
||||
for (const t of this.game.bfs(unit.tile(), manhattanDistFN(unit.tile(), 2))) {
|
||||
this.paintCell(
|
||||
this.game.x(t),
|
||||
this.game.y(t),
|
||||
rel,
|
||||
this.theme.territoryColor(unit.owner().info()),
|
||||
255
|
||||
);
|
||||
}
|
||||
|
||||
// Paint border
|
||||
for (const t of this.game.bfs(unit.tile(), manhattanDistFN(unit.tile(), 1))) {
|
||||
this.paintCell(
|
||||
this.game.x(t),
|
||||
this.game.y(t),
|
||||
rel,
|
||||
this.theme.borderColor(unit.owner().info()),
|
||||
255
|
||||
);
|
||||
}
|
||||
}
|
||||
paintCell(
|
||||
x: number,
|
||||
y: number,
|
||||
relationship: Relationship,
|
||||
color: Colord,
|
||||
alpha: number,
|
||||
) {
|
||||
this.clearCell(x, y);
|
||||
if (this.alternateView) {
|
||||
switch (relationship) {
|
||||
case Relationship.Self:
|
||||
this.context.fillStyle = this.theme.selfColor().toRgbString();
|
||||
break;
|
||||
case Relationship.Ally:
|
||||
this.context.fillStyle = this.theme.allyColor().toRgbString();
|
||||
break;
|
||||
case Relationship.Enemy:
|
||||
this.context.fillStyle = this.theme.enemyColor().toRgbString();
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
this.context.fillStyle = color.alpha(alpha / 255).toRgbString();
|
||||
}
|
||||
this.context.fillRect(x, y, 1, 1);
|
||||
}
|
||||
|
||||
private handleBoatEvent(unit: UnitView) {
|
||||
const rel = this.relationship(unit);
|
||||
|
||||
if (!this.boatToTrail.has(unit)) {
|
||||
this.boatToTrail.set(unit, new Set<TileRef>());
|
||||
}
|
||||
const trail = this.boatToTrail.get(unit);
|
||||
trail.add(unit.lastTile());
|
||||
|
||||
// Clear previous area
|
||||
for (const t of this.game.bfs(unit.lastTile(), manhattanDistFN(unit.lastTile(), 3))) {
|
||||
this.clearCell(this.game.x(t), this.game.y(t));
|
||||
}
|
||||
|
||||
if (unit.isActive()) {
|
||||
// Paint trail
|
||||
for (const t of trail) {
|
||||
this.paintCell(
|
||||
this.game.x(t),
|
||||
this.game.y(t),
|
||||
rel,
|
||||
this.theme.territoryColor(unit.owner().info()),
|
||||
150
|
||||
);
|
||||
}
|
||||
|
||||
// Paint border
|
||||
for (const t of this.game.bfs(unit.tile(), manhattanDistFN(unit.tile(), 2))) {
|
||||
this.paintCell(
|
||||
this.game.x(t),
|
||||
this.game.y(t),
|
||||
rel,
|
||||
this.theme.borderColor(unit.owner().info()),
|
||||
255
|
||||
);
|
||||
}
|
||||
|
||||
// Paint territory
|
||||
for (const t of this.game.bfs(unit.tile(), manhattanDistFN(unit.tile(), 1))) {
|
||||
this.paintCell(
|
||||
this.game.x(t),
|
||||
this.game.y(t),
|
||||
rel,
|
||||
this.theme.territoryColor(unit.owner().info()),
|
||||
255
|
||||
);
|
||||
}
|
||||
} else {
|
||||
for (const t of trail) {
|
||||
this.clearCell(this.game.x(t), this.game.y(t));
|
||||
}
|
||||
this.boatToTrail.delete(unit);
|
||||
}
|
||||
}
|
||||
|
||||
paintCell(x: number, y: number, relationship: Relationship, color: Colord, alpha: number) {
|
||||
this.clearCell(x, y);
|
||||
if (this.alternateView) {
|
||||
switch (relationship) {
|
||||
case Relationship.Self:
|
||||
this.context.fillStyle = this.theme.selfColor().toRgbString();
|
||||
break;
|
||||
case Relationship.Ally:
|
||||
this.context.fillStyle = this.theme.allyColor().toRgbString();
|
||||
break;
|
||||
case Relationship.Enemy:
|
||||
this.context.fillStyle = this.theme.enemyColor().toRgbString();
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
this.context.fillStyle = color.alpha(alpha / 255).toRgbString();
|
||||
}
|
||||
this.context.fillRect(x, y, 1, 1);
|
||||
}
|
||||
|
||||
clearCell(x: number, y: number) {
|
||||
this.context.clearRect(x, y, 1, 1);
|
||||
}
|
||||
}
|
||||
clearCell(x: number, y: number) {
|
||||
this.context.clearRect(x, y, 1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,328 +1,328 @@
|
||||
import { LitElement, html, css } from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { Player } from '../../../core/game/Game';
|
||||
import { ClientID } from '../../../core/Schemas';
|
||||
import { GameView, PlayerView } from '../../../core/game/GameView';
|
||||
import { Layer } from './Layer';
|
||||
import { GameUpdateType } from '../../../core/game/GameUpdates';
|
||||
import { PseudoRandom } from '../../../core/PseudoRandom';
|
||||
import { simpleHash } from '../../../core/Util';
|
||||
import { EventBus } from '../../../core/EventBus';
|
||||
import { SendWinnerEvent } from '../../Transport';
|
||||
|
||||
import { LitElement, html, css } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { Player } from "../../../core/game/Game";
|
||||
import { ClientID } from "../../../core/Schemas";
|
||||
import { GameView, PlayerView } from "../../../core/game/GameView";
|
||||
import { Layer } from "./Layer";
|
||||
import { GameUpdateType } from "../../../core/game/GameUpdates";
|
||||
import { PseudoRandom } from "../../../core/PseudoRandom";
|
||||
import { simpleHash } from "../../../core/Util";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
import { SendWinnerEvent } from "../../Transport";
|
||||
|
||||
const lowRadiationVictoryQuotes = [
|
||||
"Victory is mine. The world endures - under new management.",
|
||||
"They thought they could stop me. Now they serve me.",
|
||||
"The old order has fallen. My reign begins.",
|
||||
"Not every victory requires destruction.",
|
||||
"From this day forward, all will know who rules.",
|
||||
"The war is over. Long live the victor.",
|
||||
"A new empire rises - with me at its helm.",
|
||||
"The throne is claimed. The crown is mine.",
|
||||
"Today marks the beginning of my dynasty.",
|
||||
"They feared my wrath. Now they'll know my rule.",
|
||||
"Victory was inevitable. Surrender was optional.",
|
||||
"A new era dawns - under my command.",
|
||||
"The pieces are in place. The victory is complete.",
|
||||
"Let history remember who conquered all.",
|
||||
"Their resistance only delayed the inevitable.",
|
||||
"Power shifts. Empires fall. I remain.",
|
||||
"The world has a new master now.",
|
||||
"Their armies fell. Their nations surrendered. I prevailed.",
|
||||
"All paths led to this victory.",
|
||||
"The world bows to its new ruler.",
|
||||
"From the ashes of their defeat, my victory rises.",
|
||||
"Destiny called. I answered. The world followed.",
|
||||
"They called me tyrant. Now they call me emperor.",
|
||||
"The old powers have fallen. Mine endures.",
|
||||
"Every empire needs a beginning. This is mine.",
|
||||
"Their defiance crumbled before my ambition.",
|
||||
"No more rebels. No more resistance. Only order.",
|
||||
"The final piece falls into place.",
|
||||
"Let them write of this day in their histories.",
|
||||
"My vision becomes reality.",
|
||||
"Their surrender was wise. My victory was certain.",
|
||||
"The wheels of fate turn in my favor.",
|
||||
"A new chapter begins - written by the victor.",
|
||||
"They fought the inevitable. The inevitable won.",
|
||||
"All that was theirs is now mine.",
|
||||
"The gods themselves bow before my triumph.",
|
||||
"Their kingdoms shatter. My empire rises.",
|
||||
"Victory tastes sweeter than wine.",
|
||||
"The crown suits me well, don't you think?",
|
||||
"Behold the dawn of my eternal reign."
|
||||
"Victory is mine. The world endures - under new management.",
|
||||
"They thought they could stop me. Now they serve me.",
|
||||
"The old order has fallen. My reign begins.",
|
||||
"Not every victory requires destruction.",
|
||||
"From this day forward, all will know who rules.",
|
||||
"The war is over. Long live the victor.",
|
||||
"A new empire rises - with me at its helm.",
|
||||
"The throne is claimed. The crown is mine.",
|
||||
"Today marks the beginning of my dynasty.",
|
||||
"They feared my wrath. Now they'll know my rule.",
|
||||
"Victory was inevitable. Surrender was optional.",
|
||||
"A new era dawns - under my command.",
|
||||
"The pieces are in place. The victory is complete.",
|
||||
"Let history remember who conquered all.",
|
||||
"Their resistance only delayed the inevitable.",
|
||||
"Power shifts. Empires fall. I remain.",
|
||||
"The world has a new master now.",
|
||||
"Their armies fell. Their nations surrendered. I prevailed.",
|
||||
"All paths led to this victory.",
|
||||
"The world bows to its new ruler.",
|
||||
"From the ashes of their defeat, my victory rises.",
|
||||
"Destiny called. I answered. The world followed.",
|
||||
"They called me tyrant. Now they call me emperor.",
|
||||
"The old powers have fallen. Mine endures.",
|
||||
"Every empire needs a beginning. This is mine.",
|
||||
"Their defiance crumbled before my ambition.",
|
||||
"No more rebels. No more resistance. Only order.",
|
||||
"The final piece falls into place.",
|
||||
"Let them write of this day in their histories.",
|
||||
"My vision becomes reality.",
|
||||
"Their surrender was wise. My victory was certain.",
|
||||
"The wheels of fate turn in my favor.",
|
||||
"A new chapter begins - written by the victor.",
|
||||
"They fought the inevitable. The inevitable won.",
|
||||
"All that was theirs is now mine.",
|
||||
"The gods themselves bow before my triumph.",
|
||||
"Their kingdoms shatter. My empire rises.",
|
||||
"Victory tastes sweeter than wine.",
|
||||
"The crown suits me well, don't you think?",
|
||||
"Behold the dawn of my eternal reign.",
|
||||
];
|
||||
|
||||
const highRadiationVictoryQuotes = [
|
||||
"Let the world burn. I just want to rule the ashes.",
|
||||
"The old world died screaming. My new one rises from its bones.",
|
||||
"They could have surrendered. Now they glow.",
|
||||
"Everything burns. The throne of ashes awaits.",
|
||||
"A wasteland needs a king. I have answered the call.",
|
||||
"Their cities turned to glass. My victory endures.",
|
||||
"Who needs a pristine world when you can rule its ruins?",
|
||||
"They feared the fire. I embraced it.",
|
||||
"The radiation clears the way for my reign.",
|
||||
"A dead world makes for quiet subjects.",
|
||||
"From atomic fire, my kingdom rises.",
|
||||
"Nothing left but ashes and victory.",
|
||||
"They wanted war. I gave them annihilation.",
|
||||
"The world burns green. My empire glows eternal.",
|
||||
"All thrones are built on ashes. Mine just glows.",
|
||||
"Look upon my works and despair - if you can still see.",
|
||||
"The mushroom clouds herald my coronation.",
|
||||
"A crown of thorns for a radioactive realm.",
|
||||
"They chose extinction. I chose supremacy.",
|
||||
"The wasteland's throne is mine to claim.",
|
||||
"The Geiger counter clicks. The masses bow.",
|
||||
"Atoms split. Nations fall. I reign supreme.",
|
||||
"My kingdom radiates with possibility.",
|
||||
"The isotopes of victory decay slowly.",
|
||||
"Critical mass achieved. Dominion secured.",
|
||||
"Chain reaction complete: world falls, empire rises.",
|
||||
"In nuclear fire, I forge my legacy."
|
||||
"Let the world burn. I just want to rule the ashes.",
|
||||
"The old world died screaming. My new one rises from its bones.",
|
||||
"They could have surrendered. Now they glow.",
|
||||
"Everything burns. The throne of ashes awaits.",
|
||||
"A wasteland needs a king. I have answered the call.",
|
||||
"Their cities turned to glass. My victory endures.",
|
||||
"Who needs a pristine world when you can rule its ruins?",
|
||||
"They feared the fire. I embraced it.",
|
||||
"The radiation clears the way for my reign.",
|
||||
"A dead world makes for quiet subjects.",
|
||||
"From atomic fire, my kingdom rises.",
|
||||
"Nothing left but ashes and victory.",
|
||||
"They wanted war. I gave them annihilation.",
|
||||
"The world burns green. My empire glows eternal.",
|
||||
"All thrones are built on ashes. Mine just glows.",
|
||||
"Look upon my works and despair - if you can still see.",
|
||||
"The mushroom clouds herald my coronation.",
|
||||
"A crown of thorns for a radioactive realm.",
|
||||
"They chose extinction. I chose supremacy.",
|
||||
"The wasteland's throne is mine to claim.",
|
||||
"The Geiger counter clicks. The masses bow.",
|
||||
"Atoms split. Nations fall. I reign supreme.",
|
||||
"My kingdom radiates with possibility.",
|
||||
"The isotopes of victory decay slowly.",
|
||||
"Critical mass achieved. Dominion secured.",
|
||||
"Chain reaction complete: world falls, empire rises.",
|
||||
"In nuclear fire, I forge my legacy.",
|
||||
];
|
||||
|
||||
export const defeatQuotes = [
|
||||
// Last words and final thoughts
|
||||
"The flame of our nation flickers out...",
|
||||
"History will remember we fought to the last.",
|
||||
"Our glory fades into darkness.",
|
||||
"The end comes for all nations. Today, it comes for us.",
|
||||
"We fought. We failed. We fade.",
|
||||
"Our time in the sun is done.",
|
||||
"The pages of history close on our chapter.",
|
||||
"So falls the dream of empire.",
|
||||
"We built in stone, but even stone crumbles.",
|
||||
"The stars themselves will remember our defiance.",
|
||||
// Last words and final thoughts
|
||||
"The flame of our nation flickers out...",
|
||||
"History will remember we fought to the last.",
|
||||
"Our glory fades into darkness.",
|
||||
"The end comes for all nations. Today, it comes for us.",
|
||||
"We fought. We failed. We fade.",
|
||||
"Our time in the sun is done.",
|
||||
"The pages of history close on our chapter.",
|
||||
"So falls the dream of empire.",
|
||||
"We built in stone, but even stone crumbles.",
|
||||
"The stars themselves will remember our defiance.",
|
||||
|
||||
// Bitter defeats
|
||||
"Treachery and fate conspired against us.",
|
||||
"Our enemies dance on the graves of heroes.",
|
||||
"The vultures circle what remains.",
|
||||
"Let them celebrate. Dead men need no vengeance.",
|
||||
"The light dies. The darkness wins.",
|
||||
"Our walls fall. Our spirit breaks.",
|
||||
"Victory goes to the ruthless.",
|
||||
"Time claims another empire.",
|
||||
"The crown shatters. The throne burns.",
|
||||
"Our banners fall. Our story ends.",
|
||||
// Bitter defeats
|
||||
"Treachery and fate conspired against us.",
|
||||
"Our enemies dance on the graves of heroes.",
|
||||
"The vultures circle what remains.",
|
||||
"Let them celebrate. Dead men need no vengeance.",
|
||||
"The light dies. The darkness wins.",
|
||||
"Our walls fall. Our spirit breaks.",
|
||||
"Victory goes to the ruthless.",
|
||||
"Time claims another empire.",
|
||||
"The crown shatters. The throne burns.",
|
||||
"Our banners fall. Our story ends.",
|
||||
|
||||
// Philosophical acceptance
|
||||
"All great nations must face their sunset.",
|
||||
"Time is the ultimate conqueror.",
|
||||
"Today we join the ghosts of fallen empires.",
|
||||
"What rises must also fall.",
|
||||
"Our legacy scatters like dust in the wind.",
|
||||
"The wheel turns. We descend.",
|
||||
"From glory to ashes, as all things must.",
|
||||
"The tides of fate show no mercy.",
|
||||
"Let history judge if we were worthy.",
|
||||
"We join the eternal silence.",
|
||||
// Philosophical acceptance
|
||||
"All great nations must face their sunset.",
|
||||
"Time is the ultimate conqueror.",
|
||||
"Today we join the ghosts of fallen empires.",
|
||||
"What rises must also fall.",
|
||||
"Our legacy scatters like dust in the wind.",
|
||||
"The wheel turns. We descend.",
|
||||
"From glory to ashes, as all things must.",
|
||||
"The tides of fate show no mercy.",
|
||||
"Let history judge if we were worthy.",
|
||||
"We join the eternal silence.",
|
||||
|
||||
// Defiant last stands
|
||||
"Our spirit remains unbroken.",
|
||||
"They may take our lands, but not our pride.",
|
||||
"Remember us as we were, not as we fell.",
|
||||
"We chose death before dishonor.",
|
||||
"Our courage lives beyond our defeat.",
|
||||
"Let them write of how we stood fast.",
|
||||
"We fall, but we fall fighting.",
|
||||
"Honor guides us to our end.",
|
||||
"Death before surrender.",
|
||||
"The echoes of our defiance will ring eternal.",
|
||||
// Defiant last stands
|
||||
"Our spirit remains unbroken.",
|
||||
"They may take our lands, but not our pride.",
|
||||
"Remember us as we were, not as we fell.",
|
||||
"We chose death before dishonor.",
|
||||
"Our courage lives beyond our defeat.",
|
||||
"Let them write of how we stood fast.",
|
||||
"We fall, but we fall fighting.",
|
||||
"Honor guides us to our end.",
|
||||
"Death before surrender.",
|
||||
"The echoes of our defiance will ring eternal.",
|
||||
|
||||
// Prophetic/Cursed
|
||||
"Our shadow will haunt their victory.",
|
||||
"They'll learn the price of empire.",
|
||||
"Time will prove our cause was just.",
|
||||
"The seeds of their downfall are sown in our ashes.",
|
||||
"Our fall heralds their doom.",
|
||||
"Victory today. Nemesis tomorrow.",
|
||||
"The wheel turns for all.",
|
||||
"They'll remember us in their nightmares.",
|
||||
"Our curse follows them to their graves.",
|
||||
"What rises in our place will shake the world."
|
||||
// Prophetic/Cursed
|
||||
"Our shadow will haunt their victory.",
|
||||
"They'll learn the price of empire.",
|
||||
"Time will prove our cause was just.",
|
||||
"The seeds of their downfall are sown in our ashes.",
|
||||
"Our fall heralds their doom.",
|
||||
"Victory today. Nemesis tomorrow.",
|
||||
"The wheel turns for all.",
|
||||
"They'll remember us in their nightmares.",
|
||||
"Our curse follows them to their graves.",
|
||||
"What rises in our place will shake the world.",
|
||||
];
|
||||
|
||||
@customElement('win-modal')
|
||||
@customElement("win-modal")
|
||||
export class WinModal extends LitElement implements Layer {
|
||||
public game: GameView
|
||||
public eventBus: EventBus
|
||||
public game: GameView;
|
||||
public eventBus: EventBus;
|
||||
|
||||
private rand: PseudoRandom;
|
||||
|
||||
private rand: PseudoRandom;
|
||||
private hasShownDeathModal = false;
|
||||
|
||||
private hasShownDeathModal = false
|
||||
@state()
|
||||
isVisible = false;
|
||||
|
||||
@state()
|
||||
isVisible = false
|
||||
private _title: string;
|
||||
private message: string;
|
||||
|
||||
private _title: string
|
||||
private message: string
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background-color: rgba(30, 30, 30, 0.7);
|
||||
padding: 25px;
|
||||
border-radius: 10px;
|
||||
z-index: 9999;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(5px);
|
||||
color: white;
|
||||
width: 300px;
|
||||
transition:
|
||||
opacity 0.3s ease-in-out,
|
||||
visibility 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background-color: rgba(30, 30, 30, 0.7);
|
||||
padding: 25px;
|
||||
border-radius: 10px;
|
||||
z-index: 9999;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(5px);
|
||||
color: white;
|
||||
width: 300px;
|
||||
transition: opacity 0.3s ease-in-out, visibility 0.3s ease-in-out;
|
||||
}
|
||||
.modal.visible {
|
||||
display: block;
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
.modal.visible {
|
||||
display: block;
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -48%);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -48%);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
}
|
||||
h2 {
|
||||
margin: 0 0 15px 0;
|
||||
font-size: 24px;
|
||||
text-align: center;
|
||||
color: white;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0 0 15px 0;
|
||||
font-size: 24px;
|
||||
text-align: center;
|
||||
color: white;
|
||||
}
|
||||
p {
|
||||
margin: 0 0 20px 0;
|
||||
text-align: center;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 20px 0;
|
||||
text-align: center;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.button-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
button {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
background: rgba(0, 150, 255, 0.6);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
transition:
|
||||
background-color 0.2s ease,
|
||||
transform 0.1s ease;
|
||||
}
|
||||
|
||||
button {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
background: rgba(0, 150, 255, 0.6);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
transition: background-color 0.2s ease, transform 0.1s ease;
|
||||
}
|
||||
button:hover {
|
||||
background: rgba(0, 150, 255, 0.8);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: rgba(0, 150, 255, 0.8);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
button:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.modal {
|
||||
width: 90%;
|
||||
max-width: 300px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.modal {
|
||||
width: 90%;
|
||||
max-width: 300px;
|
||||
padding: 20px;
|
||||
}
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
}
|
||||
button {
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
button {
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
render() {
|
||||
return html`
|
||||
<div class="modal ${this.isVisible ? "visible" : ""}">
|
||||
<h2>${this._title}</h2>
|
||||
<p>${this.message}</p>
|
||||
<div class="button-container">
|
||||
<button @click=${this._handleExit}>Exit Game</button>
|
||||
<button @click=${this.hide}>Keep Playing</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="modal ${this.isVisible ? 'visible' : ''}">
|
||||
<h2>${this._title}</h2>
|
||||
<p>${this.message}</p>
|
||||
<div class="button-container">
|
||||
<button @click=${this._handleExit}>Exit Game</button>
|
||||
<button @click=${this.hide}>Keep Playing</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
show() {
|
||||
this.isVisible = true;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.isVisible = false;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private _handleExit() {
|
||||
this.hide();
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.rand = new PseudoRandom(simpleHash(this.game.myClientID()));
|
||||
}
|
||||
|
||||
tick() {
|
||||
const myPlayer = this.game.myPlayer();
|
||||
if (!this.hasShownDeathModal && myPlayer && !myPlayer.isAlive()) {
|
||||
this.hasShownDeathModal = true;
|
||||
this._title = "You died";
|
||||
this.message = this.rand.randElement(defeatQuotes);
|
||||
this.show();
|
||||
}
|
||||
|
||||
show() {
|
||||
this.isVisible = true;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.isVisible = false;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private _handleExit() {
|
||||
this.hide();
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.rand = new PseudoRandom(simpleHash(this.game.myClientID()))
|
||||
}
|
||||
|
||||
tick() {
|
||||
const myPlayer = this.game.myPlayer()
|
||||
if (!this.hasShownDeathModal && myPlayer && !myPlayer.isAlive()) {
|
||||
this.hasShownDeathModal = true
|
||||
this._title = 'You died'
|
||||
this.message = this.rand.randElement(defeatQuotes)
|
||||
this.show()
|
||||
this.game.updatesSinceLastTick()[GameUpdateType.WinUpdate].forEach((wu) => {
|
||||
const winner = this.game.playerBySmallID(wu.winnerID) as PlayerView;
|
||||
this.eventBus.emit(new SendWinnerEvent(winner.clientID()));
|
||||
if (winner == this.game.myPlayer()) {
|
||||
this._title = "You Won!";
|
||||
if (this.game.numTilesWithFallout() / this.game.numLandTiles() > 0.6) {
|
||||
this.message = this.rand.randElement(highRadiationVictoryQuotes);
|
||||
} else {
|
||||
this.message = this.rand.randElement(lowRadiationVictoryQuotes);
|
||||
}
|
||||
this.game.updatesSinceLastTick()[GameUpdateType.WinUpdate]
|
||||
.forEach(wu => {
|
||||
const winner = this.game.playerBySmallID(wu.winnerID) as PlayerView
|
||||
this.eventBus.emit(new SendWinnerEvent(winner.clientID()))
|
||||
if (winner == this.game.myPlayer()) {
|
||||
this._title = 'You Won!'
|
||||
if (this.game.numTilesWithFallout() / this.game.numLandTiles() > .6) {
|
||||
this.message = this.rand.randElement(highRadiationVictoryQuotes)
|
||||
} else {
|
||||
this.message = this.rand.randElement(lowRadiationVictoryQuotes)
|
||||
}
|
||||
} else {
|
||||
this._title = `${winner.name()} has won!`
|
||||
this.message = this.rand.randElement(defeatQuotes)
|
||||
}
|
||||
this.show()
|
||||
})
|
||||
}
|
||||
} else {
|
||||
this._title = `${winner.name()} has won!`;
|
||||
this.message = this.rand.randElement(defeatQuotes);
|
||||
}
|
||||
this.show();
|
||||
});
|
||||
}
|
||||
|
||||
renderLayer(context: CanvasRenderingContext2D) {
|
||||
}
|
||||
renderLayer(context: CanvasRenderingContext2D) {}
|
||||
|
||||
shouldTransform(): boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
shouldTransform(): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,246 +1,277 @@
|
||||
import { LitElement, html, css } from 'lit';
|
||||
import { customElement, state } from 'lit/decorators.js';
|
||||
import { EventBus } from '../../../../core/EventBus';
|
||||
import { Cell, Game, Player, PlayerActions, UnitType } from '../../../../core/game/Game';
|
||||
import { BuildUnitIntentEvent } from '../../../Transport';
|
||||
import atomBombIcon from '../../../../../resources/images/NukeIconWhite.svg';
|
||||
import hydrogenBombIcon from '../../../../../resources/images/MushroomCloudIconWhite.svg';
|
||||
import destroyerIcon from '../../../../../resources/images/DestroyerIconWhite.svg';
|
||||
import battleshipIcon from '../../../../../resources/images/BattleshipIconWhite.svg';
|
||||
import missileSiloIcon from '../../../../../resources/images/MissileSiloIconWhite.svg';
|
||||
import goldCoinIcon from '../../../../../resources/images/GoldCoinIcon.svg';
|
||||
import portIcon from '../../../../../resources/images/PortIcon.svg';
|
||||
import shieldIcon from '../../../../../resources/images/ShieldIconWhite.svg';
|
||||
import cityIcon from '../../../../../resources/images/CityIconWhite.svg';
|
||||
import { renderNumber } from '../../../Utils';
|
||||
import { ContextMenuEvent } from '../../../InputHandler';
|
||||
import { GameView, PlayerView } from '../../../../core/game/GameView';
|
||||
import { LitElement, html, css } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { EventBus } from "../../../../core/EventBus";
|
||||
import {
|
||||
Cell,
|
||||
Game,
|
||||
Player,
|
||||
PlayerActions,
|
||||
UnitType,
|
||||
} from "../../../../core/game/Game";
|
||||
import { BuildUnitIntentEvent } from "../../../Transport";
|
||||
import atomBombIcon from "../../../../../resources/images/NukeIconWhite.svg";
|
||||
import hydrogenBombIcon from "../../../../../resources/images/MushroomCloudIconWhite.svg";
|
||||
import destroyerIcon from "../../../../../resources/images/DestroyerIconWhite.svg";
|
||||
import battleshipIcon from "../../../../../resources/images/BattleshipIconWhite.svg";
|
||||
import missileSiloIcon from "../../../../../resources/images/MissileSiloIconWhite.svg";
|
||||
import goldCoinIcon from "../../../../../resources/images/GoldCoinIcon.svg";
|
||||
import portIcon from "../../../../../resources/images/PortIcon.svg";
|
||||
import shieldIcon from "../../../../../resources/images/ShieldIconWhite.svg";
|
||||
import cityIcon from "../../../../../resources/images/CityIconWhite.svg";
|
||||
import { renderNumber } from "../../../Utils";
|
||||
import { ContextMenuEvent } from "../../../InputHandler";
|
||||
import { GameView, PlayerView } from "../../../../core/game/GameView";
|
||||
|
||||
interface BuildItemDisplay {
|
||||
unitType: UnitType
|
||||
icon: string;
|
||||
unitType: UnitType;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
const buildTable: BuildItemDisplay[][] = [
|
||||
[
|
||||
{ unitType: UnitType.AtomBomb, icon: atomBombIcon },
|
||||
{ unitType: UnitType.HydrogenBomb, icon: hydrogenBombIcon },
|
||||
{ unitType: UnitType.Destroyer, icon: destroyerIcon },
|
||||
{ unitType: UnitType.Battleship, icon: battleshipIcon },
|
||||
{ unitType: UnitType.Port, icon: portIcon },
|
||||
{ unitType: UnitType.MissileSilo, icon: missileSiloIcon },
|
||||
// { unitType: UnitType.DefensePost, icon: shieldIcon },
|
||||
{ unitType: UnitType.City, icon: cityIcon }
|
||||
]
|
||||
[
|
||||
{ unitType: UnitType.AtomBomb, icon: atomBombIcon },
|
||||
{ unitType: UnitType.HydrogenBomb, icon: hydrogenBombIcon },
|
||||
{ unitType: UnitType.Destroyer, icon: destroyerIcon },
|
||||
{ unitType: UnitType.Battleship, icon: battleshipIcon },
|
||||
{ unitType: UnitType.Port, icon: portIcon },
|
||||
{ unitType: UnitType.MissileSilo, icon: missileSiloIcon },
|
||||
// { unitType: UnitType.DefensePost, icon: shieldIcon },
|
||||
{ unitType: UnitType.City, icon: cityIcon },
|
||||
],
|
||||
];
|
||||
|
||||
@customElement('build-menu')
|
||||
@customElement("build-menu")
|
||||
export class BuildMenu extends LitElement {
|
||||
public game: GameView;
|
||||
public eventBus: EventBus;
|
||||
private myPlayer: PlayerView;
|
||||
private clickedCell: Cell;
|
||||
private playerActions: PlayerActions | null
|
||||
public game: GameView;
|
||||
public eventBus: EventBus;
|
||||
private myPlayer: PlayerView;
|
||||
private clickedCell: Cell;
|
||||
private playerActions: PlayerActions | null;
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
.build-menu {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 9999;
|
||||
background-color: #1E1E1E;
|
||||
padding: 15px;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
max-width: 95vw;
|
||||
max-height: 95vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.build-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
}
|
||||
.build-button {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border: 2px solid #444;
|
||||
background-color: #2C2C2C;
|
||||
color: white;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 8px;
|
||||
padding: 10px;
|
||||
}
|
||||
.build-button:not(:disabled):hover {
|
||||
background-color: #3A3A3A;
|
||||
transform: scale(1.05);
|
||||
border-color: #666;
|
||||
}
|
||||
.build-button:not(:disabled):active {
|
||||
background-color: #4A4A4A;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
.build-button:disabled {
|
||||
background-color: #1A1A1A;
|
||||
border-color: #333;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.build-button:disabled img {
|
||||
opacity: 0.5;
|
||||
}
|
||||
.build-button:disabled .build-cost {
|
||||
color: #FF4444;
|
||||
}
|
||||
.build-icon {
|
||||
font-size: 40px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.build-name {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
.build-cost {
|
||||
font-size: 14px;
|
||||
}
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.build-menu {
|
||||
padding: 10px;
|
||||
max-height: 80vh;
|
||||
}
|
||||
.build-button {
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
margin: 4px;
|
||||
padding: 6px;
|
||||
}
|
||||
.build-icon {
|
||||
font-size: 28px;
|
||||
}
|
||||
.build-name {
|
||||
font-size: 12px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.build-cost {
|
||||
font-size: 11px;
|
||||
}
|
||||
.build-button img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.build-menu {
|
||||
padding: 8px;
|
||||
max-height: 70vh;
|
||||
}
|
||||
.build-button {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
margin: 3px;
|
||||
padding: 4px;
|
||||
border-width: 1px;
|
||||
}
|
||||
.build-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
.build-name {
|
||||
font-size: 10px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.build-cost {
|
||||
font-size: 9px;
|
||||
}
|
||||
.build-button img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
.build-cost img {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@state()
|
||||
private _hidden = true;
|
||||
|
||||
private canBuild(item: BuildItemDisplay): boolean {
|
||||
if (this.myPlayer == null || this.playerActions == null) {
|
||||
return false
|
||||
}
|
||||
return this.playerActions.buildableUnits.some(u => u == item.unitType)
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
.build-menu {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 9999;
|
||||
background-color: #1e1e1e;
|
||||
padding: 15px;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
max-width: 95vw;
|
||||
max-height: 95vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.build-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
}
|
||||
.build-button {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border: 2px solid #444;
|
||||
background-color: #2c2c2c;
|
||||
color: white;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 8px;
|
||||
padding: 10px;
|
||||
}
|
||||
.build-button:not(:disabled):hover {
|
||||
background-color: #3a3a3a;
|
||||
transform: scale(1.05);
|
||||
border-color: #666;
|
||||
}
|
||||
.build-button:not(:disabled):active {
|
||||
background-color: #4a4a4a;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
.build-button:disabled {
|
||||
background-color: #1a1a1a;
|
||||
border-color: #333;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.build-button:disabled img {
|
||||
opacity: 0.5;
|
||||
}
|
||||
.build-button:disabled .build-cost {
|
||||
color: #ff4444;
|
||||
}
|
||||
.build-icon {
|
||||
font-size: 40px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.build-name {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
.build-cost {
|
||||
font-size: 14px;
|
||||
}
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
public onBuildSelected = (item: BuildItemDisplay) => {
|
||||
this.eventBus.emit(new BuildUnitIntentEvent(item.unitType, this.clickedCell))
|
||||
this.hideMenu()
|
||||
};
|
||||
@media (max-width: 768px) {
|
||||
.build-menu {
|
||||
padding: 10px;
|
||||
max-height: 80vh;
|
||||
}
|
||||
.build-button {
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
margin: 4px;
|
||||
padding: 6px;
|
||||
}
|
||||
.build-icon {
|
||||
font-size: 28px;
|
||||
}
|
||||
.build-name {
|
||||
font-size: 12px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.build-cost {
|
||||
font-size: 11px;
|
||||
}
|
||||
.build-button img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="build-menu ${this._hidden ? 'hidden' : ''}">
|
||||
${buildTable.map(row => html`
|
||||
<div class="build-row">
|
||||
${row.map(item => html`
|
||||
<button
|
||||
class="build-button"
|
||||
@click=${() => this.onBuildSelected(item)}
|
||||
?disabled=${!this.canBuild(item)}
|
||||
title=${!this.canBuild(item) ? 'Not enough money' : ''}
|
||||
>
|
||||
<img src=${item.icon} alt="${item.unitType}" width="40" height="40">
|
||||
<span class="build-name">${item.unitType}</span>
|
||||
<span class="build-cost">
|
||||
${renderNumber(this.game && this.myPlayer ? this.game.unitInfo(item.unitType).cost(this.myPlayer) : 0)}
|
||||
<img src=${goldCoinIcon} alt="gold" width="12" height="12" style="vertical-align: middle;">
|
||||
</span>
|
||||
</button>
|
||||
`)}
|
||||
</div>
|
||||
`)}
|
||||
@media (max-width: 480px) {
|
||||
.build-menu {
|
||||
padding: 8px;
|
||||
max-height: 70vh;
|
||||
}
|
||||
.build-button {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
margin: 3px;
|
||||
padding: 4px;
|
||||
border-width: 1px;
|
||||
}
|
||||
.build-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
.build-name {
|
||||
font-size: 10px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.build-cost {
|
||||
font-size: 9px;
|
||||
}
|
||||
.build-button img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
.build-cost img {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@state()
|
||||
private _hidden = true;
|
||||
|
||||
private canBuild(item: BuildItemDisplay): boolean {
|
||||
if (this.myPlayer == null || this.playerActions == null) {
|
||||
return false;
|
||||
}
|
||||
return this.playerActions.buildableUnits.some((u) => u == item.unitType);
|
||||
}
|
||||
|
||||
public onBuildSelected = (item: BuildItemDisplay) => {
|
||||
this.eventBus.emit(
|
||||
new BuildUnitIntentEvent(item.unitType, this.clickedCell),
|
||||
);
|
||||
this.hideMenu();
|
||||
};
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="build-menu ${this._hidden ? "hidden" : ""}">
|
||||
${buildTable.map(
|
||||
(row) => html`
|
||||
<div class="build-row">
|
||||
${row.map(
|
||||
(item) => html`
|
||||
<button
|
||||
class="build-button"
|
||||
@click=${() => this.onBuildSelected(item)}
|
||||
?disabled=${!this.canBuild(item)}
|
||||
title=${!this.canBuild(item) ? "Not enough money" : ""}
|
||||
>
|
||||
<img
|
||||
src=${item.icon}
|
||||
alt="${item.unitType}"
|
||||
width="40"
|
||||
height="40"
|
||||
/>
|
||||
<span class="build-name">${item.unitType}</span>
|
||||
<span class="build-cost">
|
||||
${renderNumber(
|
||||
this.game && this.myPlayer
|
||||
? this.game
|
||||
.unitInfo(item.unitType)
|
||||
.cost(this.myPlayer)
|
||||
: 0,
|
||||
)}
|
||||
<img
|
||||
src=${goldCoinIcon}
|
||||
alt="gold"
|
||||
width="12"
|
||||
height="12"
|
||||
style="vertical-align: middle;"
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
hideMenu() {
|
||||
this._hidden = true;
|
||||
hideMenu() {
|
||||
this._hidden = true;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
showMenu(player: PlayerView, clickedCell: Cell) {
|
||||
player
|
||||
.actions(this.game.ref(clickedCell.x, clickedCell.y))
|
||||
.then((actions) => {
|
||||
console.log(`got actions: ${JSON.stringify(actions)}`);
|
||||
this.playerActions = actions;
|
||||
this.myPlayer = player;
|
||||
this.clickedCell = clickedCell;
|
||||
this._hidden = false;
|
||||
this.requestUpdate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
showMenu(player: PlayerView, clickedCell: Cell) {
|
||||
player.actions(this.game.ref(clickedCell.x, clickedCell.y)).then(actions => {
|
||||
console.log(`got actions: ${JSON.stringify(actions)}`)
|
||||
this.playerActions = actions
|
||||
this.myPlayer = player;
|
||||
this.clickedCell = clickedCell;
|
||||
this._hidden = false;
|
||||
this.requestUpdate();
|
||||
})
|
||||
}
|
||||
|
||||
get isVisible() {
|
||||
return !this._hidden;
|
||||
}
|
||||
}
|
||||
get isVisible() {
|
||||
return !this._hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,129 +1,134 @@
|
||||
import { LitElement, html, css } from 'lit';
|
||||
import { customElement, state } from 'lit/decorators.js';
|
||||
import { LitElement, html, css } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
|
||||
const emojiTable: string[][] = [
|
||||
["😀", "😱", "🤡", "😡", "🥺"],
|
||||
["😈", "👏", "🥉", "🥈", "🥇"],
|
||||
["🤙", "🥰", "😇", "😊", "🔥"],
|
||||
["💪", "🏳️", "💀", "😭", "🤦♂️"],
|
||||
["😎", "👎", "👍", "🥱", "💔"],
|
||||
["❤️", "💰", "🤝", "🖕", "💥"],
|
||||
["🆘", "🕊️", "➡️", "⬅️", "↙️"],
|
||||
["↖️", "↗️", "⬆️", "↘️", "⬇️"]
|
||||
["😀", "😱", "🤡", "😡", "🥺"],
|
||||
["😈", "👏", "🥉", "🥈", "🥇"],
|
||||
["🤙", "🥰", "😇", "😊", "🔥"],
|
||||
["💪", "🏳️", "💀", "😭", "🤦♂️"],
|
||||
["😎", "👎", "👍", "🥱", "💔"],
|
||||
["❤️", "💰", "🤝", "🖕", "💥"],
|
||||
["🆘", "🕊️", "➡️", "⬅️", "↙️"],
|
||||
["↖️", "↗️", "⬆️", "↘️", "⬇️"],
|
||||
];
|
||||
|
||||
@customElement('emoji-table')
|
||||
@customElement("emoji-table")
|
||||
export class EmojiTable extends LitElement {
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
.emoji-table {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 9999;
|
||||
background-color: #1E1E1E;
|
||||
padding: 15px;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
max-width: 95vw;
|
||||
max-height: 95vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.emoji-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
.emoji-button {
|
||||
font-size: 60px;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border: 1px solid #333;
|
||||
background-color: #2C2C2C;
|
||||
color: white;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 8px;
|
||||
}
|
||||
.emoji-button:hover {
|
||||
background-color: #3A3A3A;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
.emoji-button:active {
|
||||
background-color: #4A4A4A;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.emoji-button {
|
||||
font-size: 32px;
|
||||
/* Slightly smaller font size for mobile */
|
||||
width: 60px;
|
||||
/* Smaller width for mobile */
|
||||
height: 60px;
|
||||
/* Smaller height for mobile */
|
||||
margin: 5px;
|
||||
/* Smaller margin for mobile */
|
||||
}
|
||||
}
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
.emoji-table {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 9999;
|
||||
background-color: #1e1e1e;
|
||||
padding: 15px;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
max-width: 95vw;
|
||||
max-height: 95vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.emoji-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
.emoji-button {
|
||||
font-size: 60px;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border: 1px solid #333;
|
||||
background-color: #2c2c2c;
|
||||
color: white;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 8px;
|
||||
}
|
||||
.emoji-button:hover {
|
||||
background-color: #3a3a3a;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
.emoji-button:active {
|
||||
background-color: #4a4a4a;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@media (max-width: 400px) {
|
||||
.emoji-button {
|
||||
font-size: 28px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
margin: 3px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@media (max-width: 600px) {
|
||||
.emoji-button {
|
||||
font-size: 32px;
|
||||
/* Slightly smaller font size for mobile */
|
||||
width: 60px;
|
||||
/* Smaller width for mobile */
|
||||
height: 60px;
|
||||
/* Smaller height for mobile */
|
||||
margin: 5px;
|
||||
/* Smaller margin for mobile */
|
||||
}
|
||||
}
|
||||
|
||||
@state()
|
||||
private _hidden = true;
|
||||
@media (max-width: 400px) {
|
||||
.emoji-button {
|
||||
font-size: 28px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
margin: 3px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
public onEmojiClicked: (emoji: string) => void = () => { }
|
||||
@state()
|
||||
private _hidden = true;
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="emoji-table ${this._hidden ? 'hidden' : ''}">
|
||||
${emojiTable.map(row => html`
|
||||
<div class="emoji-row">
|
||||
${row.map(emoji => html`
|
||||
<button class="emoji-button" @click=${() => this.onEmojiClicked(emoji)}>
|
||||
${emoji}
|
||||
</button>
|
||||
`)}
|
||||
</div>
|
||||
`)}
|
||||
public onEmojiClicked: (emoji: string) => void = () => {};
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="emoji-table ${this._hidden ? "hidden" : ""}">
|
||||
${emojiTable.map(
|
||||
(row) => html`
|
||||
<div class="emoji-row">
|
||||
${row.map(
|
||||
(emoji) => html`
|
||||
<button
|
||||
class="emoji-button"
|
||||
@click=${() => this.onEmojiClicked(emoji)}
|
||||
>
|
||||
${emoji}
|
||||
</button>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
hideTable() {
|
||||
this._hidden = true;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
hideTable() {
|
||||
this._hidden = true;
|
||||
this.requestUpdate();
|
||||
showTable() {
|
||||
this._hidden = false;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
showTable() {
|
||||
this._hidden = false;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
get isVisible() {
|
||||
return !this._hidden;
|
||||
}
|
||||
}
|
||||
get isVisible() {
|
||||
return !this._hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ export class RadialMenu implements Layer {
|
||||
private emojiTable: EmojiTable,
|
||||
private buildMenu: BuildMenu,
|
||||
private uiState: UIState,
|
||||
private playerInfoOverlay: PlayerInfoOverlay
|
||||
private playerInfoOverlay: PlayerInfoOverlay,
|
||||
) {}
|
||||
|
||||
init() {
|
||||
@@ -106,7 +106,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;
|
||||
@@ -139,7 +139,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
|
||||
@@ -162,7 +162,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")
|
||||
@@ -287,7 +287,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;
|
||||
@@ -316,7 +316,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);
|
||||
@@ -348,8 +348,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(),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -426,8 +426,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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -438,7 +438,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;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en" class="h-full">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
|
||||
Reference in New Issue
Block a user