mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-08-08 03:29:44 +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" />
|
||||
|
||||
+27
-27
@@ -1,39 +1,39 @@
|
||||
import { EventBus, GameEvent } from "./EventBus"
|
||||
import { LogSeverity } from "./Schemas"
|
||||
import { EventBus, GameEvent } from "./EventBus";
|
||||
import { LogSeverity } from "./Schemas";
|
||||
|
||||
export const consolex = {
|
||||
log: console.log,
|
||||
warn: console.warn,
|
||||
error: console.error
|
||||
}
|
||||
log: console.log,
|
||||
warn: console.warn,
|
||||
error: console.error,
|
||||
};
|
||||
|
||||
let inited = false
|
||||
let inited = false;
|
||||
|
||||
// Only call this in client/browser!
|
||||
export function initRemoteSender(eventBus: EventBus) {
|
||||
if (inited) {
|
||||
return
|
||||
}
|
||||
inited = true
|
||||
if (inited) {
|
||||
return;
|
||||
}
|
||||
inited = true;
|
||||
|
||||
consolex.log = (...args: any[]): void => {
|
||||
console.log(...args);
|
||||
// eventBus.emit(new SendLogEvent(LogSeverity.Info, args.join(' ')))
|
||||
}
|
||||
consolex.log = (...args: any[]): void => {
|
||||
console.log(...args);
|
||||
// eventBus.emit(new SendLogEvent(LogSeverity.Info, args.join(' ')))
|
||||
};
|
||||
|
||||
consolex.warn = (...args: any[]): void => {
|
||||
console.warn(...args);
|
||||
// eventBus.emit(new SendLogEvent(LogSeverity.Warn, args.join(' ')))
|
||||
}
|
||||
consolex.warn = (...args: any[]): void => {
|
||||
console.warn(...args);
|
||||
// eventBus.emit(new SendLogEvent(LogSeverity.Warn, args.join(' ')))
|
||||
};
|
||||
|
||||
consolex.error = (...args: any[]): void => {
|
||||
console.error(...args);
|
||||
// eventBus.emit(new SendLogEvent(LogSeverity.Error, args.join(' ')))
|
||||
}
|
||||
consolex.error = (...args: any[]): void => {
|
||||
console.error(...args);
|
||||
// eventBus.emit(new SendLogEvent(LogSeverity.Error, args.join(' ')))
|
||||
};
|
||||
}
|
||||
export class SendLogEvent implements GameEvent {
|
||||
constructor(
|
||||
public readonly severity: LogSeverity,
|
||||
public readonly log: string
|
||||
) { }
|
||||
constructor(
|
||||
public readonly severity: LogSeverity,
|
||||
public readonly log: string,
|
||||
) {}
|
||||
}
|
||||
|
||||
+10
-6
@@ -1,11 +1,12 @@
|
||||
export interface GameEvent { }
|
||||
export interface GameEvent {}
|
||||
|
||||
export interface EventConstructor<T extends GameEvent = GameEvent> {
|
||||
new(...args: any[]): T;
|
||||
new (...args: any[]): T;
|
||||
}
|
||||
|
||||
export class EventBus {
|
||||
private listeners: Map<EventConstructor, Array<(event: GameEvent) => void>> = new Map();
|
||||
private listeners: Map<EventConstructor, Array<(event: GameEvent) => void>> =
|
||||
new Map();
|
||||
|
||||
emit<T extends GameEvent>(event: T): void {
|
||||
const eventConstructor = event.constructor as EventConstructor<T>;
|
||||
@@ -19,7 +20,7 @@ export class EventBus {
|
||||
|
||||
on<T extends GameEvent>(
|
||||
eventType: EventConstructor<T>,
|
||||
callback: (event: T) => void
|
||||
callback: (event: T) => void,
|
||||
): void {
|
||||
if (!this.listeners.has(eventType)) {
|
||||
this.listeners.set(eventType, []);
|
||||
@@ -28,7 +29,10 @@ export class EventBus {
|
||||
callbacks.push(callback as (event: GameEvent) => void);
|
||||
}
|
||||
|
||||
off<T extends GameEvent>(eventType: EventConstructor<T>, callback: (event: T) => void): void {
|
||||
off<T extends GameEvent>(
|
||||
eventType: EventConstructor<T>,
|
||||
callback: (event: T) => void,
|
||||
): void {
|
||||
const callbacks = this.listeners.get(eventType);
|
||||
if (callbacks) {
|
||||
const index = callbacks.indexOf(callback as (event: GameEvent) => void);
|
||||
@@ -37,4 +41,4 @@ export class EventBus {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+144
-108
@@ -4,137 +4,173 @@ import { getConfig } from "./configuration/Config";
|
||||
import { EventBus } from "./EventBus";
|
||||
import { Executor } from "./execution/ExecutionManager";
|
||||
import { WinCheckExecution } from "./execution/WinCheckExecution";
|
||||
import { AllPlayers, Cell, Game, GameUpdates, MessageType, Player, PlayerActions, PlayerID, PlayerProfile, PlayerType, UnitType } from "./game/Game";
|
||||
import {
|
||||
AllPlayers,
|
||||
Cell,
|
||||
Game,
|
||||
GameUpdates,
|
||||
MessageType,
|
||||
Player,
|
||||
PlayerActions,
|
||||
PlayerID,
|
||||
PlayerProfile,
|
||||
PlayerType,
|
||||
UnitType,
|
||||
} from "./game/Game";
|
||||
import { DisplayMessageUpdate, ErrorUpdate } from "./game/GameUpdates";
|
||||
import { NameViewData } from './game/Game';
|
||||
import { NameViewData } from "./game/Game";
|
||||
import { GameUpdateType } from "./game/GameUpdates";
|
||||
import { createGame } from "./game/GameImpl";
|
||||
import { loadTerrainMap as loadGameMap } from "./game/TerrainMapLoader";
|
||||
import { GameConfig, Turn } from "./Schemas";
|
||||
import { GameUpdateViewData } from './game/GameUpdates';
|
||||
import { GameUpdateViewData } from "./game/GameUpdates";
|
||||
import { andFN, manhattanDistFN, TileRef } from "./game/GameMap";
|
||||
import { targetTransportTile } from "./Util";
|
||||
|
||||
export async function createGameRunner(gameID: string, gameConfig: GameConfig, callBack: (gu: GameUpdateViewData) => void): Promise<GameRunner> {
|
||||
const config = getConfig(gameConfig)
|
||||
const gameMap = await loadGameMap(gameConfig.gameMap);
|
||||
const game = createGame(gameMap.gameMap, gameMap.miniGameMap, gameMap.nationMap, config)
|
||||
const gr = new GameRunner(game as Game, new Executor(game, gameID), callBack)
|
||||
gr.init()
|
||||
return gr
|
||||
export async function createGameRunner(
|
||||
gameID: string,
|
||||
gameConfig: GameConfig,
|
||||
callBack: (gu: GameUpdateViewData) => void,
|
||||
): Promise<GameRunner> {
|
||||
const config = getConfig(gameConfig);
|
||||
const gameMap = await loadGameMap(gameConfig.gameMap);
|
||||
const game = createGame(
|
||||
gameMap.gameMap,
|
||||
gameMap.miniGameMap,
|
||||
gameMap.nationMap,
|
||||
config,
|
||||
);
|
||||
const gr = new GameRunner(game as Game, new Executor(game, gameID), callBack);
|
||||
gr.init();
|
||||
return gr;
|
||||
}
|
||||
|
||||
export class GameRunner {
|
||||
private tickInterval = null
|
||||
private turns: Turn[] = []
|
||||
private currTurn = 0
|
||||
private isExecuting = false
|
||||
private tickInterval = null;
|
||||
private turns: Turn[] = [];
|
||||
private currTurn = 0;
|
||||
private isExecuting = false;
|
||||
|
||||
private playerViewData: Record<PlayerID, NameViewData> = {}
|
||||
private playerViewData: Record<PlayerID, NameViewData> = {};
|
||||
|
||||
constructor(
|
||||
public game: Game,
|
||||
private execManager: Executor,
|
||||
private callBack: (gu: GameUpdateViewData | ErrorUpdate) => void
|
||||
) {
|
||||
constructor(
|
||||
public game: Game,
|
||||
private execManager: Executor,
|
||||
private callBack: (gu: GameUpdateViewData | ErrorUpdate) => void,
|
||||
) {}
|
||||
|
||||
init() {
|
||||
this.game.addExecution(
|
||||
...this.execManager.spawnBots(this.game.config().numBots()),
|
||||
);
|
||||
if (this.game.config().spawnNPCs()) {
|
||||
this.game.addExecution(...this.execManager.fakeHumanExecutions());
|
||||
}
|
||||
this.game.addExecution(new WinCheckExecution());
|
||||
this.tickInterval = setInterval(() => this.executeNextTick(), 10);
|
||||
}
|
||||
|
||||
init() {
|
||||
this.game.addExecution(...this.execManager.spawnBots(this.game.config().numBots()))
|
||||
if (this.game.config().spawnNPCs()) {
|
||||
this.game.addExecution(...this.execManager.fakeHumanExecutions())
|
||||
}
|
||||
this.game.addExecution(new WinCheckExecution())
|
||||
this.tickInterval = setInterval(() => this.executeNextTick(), 10)
|
||||
public addTurn(turn: Turn): void {
|
||||
this.turns.push(turn);
|
||||
}
|
||||
|
||||
public executeNextTick() {
|
||||
if (this.isExecuting) {
|
||||
return;
|
||||
}
|
||||
|
||||
public addTurn(turn: Turn): void {
|
||||
this.turns.push(turn)
|
||||
if (this.currTurn >= this.turns.length) {
|
||||
return;
|
||||
}
|
||||
this.isExecuting = true;
|
||||
|
||||
public executeNextTick() {
|
||||
if (this.isExecuting) {
|
||||
return
|
||||
}
|
||||
if (this.currTurn >= this.turns.length) {
|
||||
return
|
||||
}
|
||||
this.isExecuting = true
|
||||
this.game.addExecution(
|
||||
...this.execManager.createExecs(this.turns[this.currTurn]),
|
||||
);
|
||||
this.currTurn++;
|
||||
|
||||
let updates: GameUpdates;
|
||||
|
||||
this.game.addExecution(...this.execManager.createExecs(this.turns[this.currTurn]))
|
||||
this.currTurn++
|
||||
|
||||
let updates: GameUpdates;
|
||||
|
||||
try {
|
||||
updates = this.game.executeNextTick();
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
console.error('Game tick error:', error.message);
|
||||
this.callBack({
|
||||
errMsg: error.message,
|
||||
stack: error.stack
|
||||
} as ErrorUpdate)
|
||||
clearInterval(this.tickInterval)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (this.game.inSpawnPhase() && this.game.ticks() % 2 == 0) {
|
||||
this.game.players()
|
||||
.filter(p => p.type() == PlayerType.Human || p.type() == PlayerType.FakeHuman)
|
||||
.forEach(p => this.playerViewData[p.id()] = placeName(this.game, p))
|
||||
}
|
||||
|
||||
if (this.game.ticks() < 3 || this.game.ticks() % 30 == 0) {
|
||||
this.game.players().forEach(p => {
|
||||
this.playerViewData[p.id()] = placeName(this.game, p)
|
||||
})
|
||||
}
|
||||
|
||||
// Many tiles are updated to pack it into an array
|
||||
const packedTileUpdates = updates[GameUpdateType.Tile].map(u => u.update)
|
||||
updates[GameUpdateType.Tile] = []
|
||||
|
||||
try {
|
||||
updates = this.game.executeNextTick();
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
console.error("Game tick error:", error.message);
|
||||
this.callBack({
|
||||
tick: this.game.ticks(),
|
||||
packedTileUpdates: new BigUint64Array(packedTileUpdates),
|
||||
updates: updates,
|
||||
playerNameViewData: this.playerViewData
|
||||
})
|
||||
this.isExecuting = false
|
||||
errMsg: error.message,
|
||||
stack: error.stack,
|
||||
} as ErrorUpdate);
|
||||
clearInterval(this.tickInterval);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public playerActions(playerID: PlayerID, x: number, y: number): PlayerActions {
|
||||
const player = this.game.player(playerID)
|
||||
const tile = this.game.ref(x, y)
|
||||
const actions = {
|
||||
canBoat: player.canBoat(tile),
|
||||
canAttack: player.canAttack(tile),
|
||||
buildableUnits: Object.values(UnitType).filter(ut => player.canBuild(ut, tile) != false),
|
||||
canSendEmojiAllPlayers: player.canSendEmoji(AllPlayers)
|
||||
} as PlayerActions
|
||||
|
||||
if (this.game.hasOwner(tile)) {
|
||||
const other = this.game.owner(tile) as Player
|
||||
actions.interaction = {
|
||||
sharedBorder: player.sharesBorderWith(other),
|
||||
canSendEmoji: player.canSendEmoji(other),
|
||||
canTarget: player.canTarget(other),
|
||||
canSendAllianceRequest: !player.recentOrPendingAllianceRequestWith(other),
|
||||
canBreakAlliance: player.isAlliedWith(other),
|
||||
canDonate: player.canDonate(other)
|
||||
}
|
||||
}
|
||||
|
||||
return actions
|
||||
if (this.game.inSpawnPhase() && this.game.ticks() % 2 == 0) {
|
||||
this.game
|
||||
.players()
|
||||
.filter(
|
||||
(p) =>
|
||||
p.type() == PlayerType.Human || p.type() == PlayerType.FakeHuman,
|
||||
)
|
||||
.forEach(
|
||||
(p) => (this.playerViewData[p.id()] = placeName(this.game, p)),
|
||||
);
|
||||
}
|
||||
public playerProfile(playerID: number): PlayerProfile {
|
||||
const player = this.game.playerBySmallID(playerID)
|
||||
if (!player.isPlayer()) {
|
||||
throw new Error(`player with id ${playerID} not found`)
|
||||
}
|
||||
return player.playerProfile()
|
||||
|
||||
if (this.game.ticks() < 3 || this.game.ticks() % 30 == 0) {
|
||||
this.game.players().forEach((p) => {
|
||||
this.playerViewData[p.id()] = placeName(this.game, p);
|
||||
});
|
||||
}
|
||||
|
||||
// Many tiles are updated to pack it into an array
|
||||
const packedTileUpdates = updates[GameUpdateType.Tile].map((u) => u.update);
|
||||
updates[GameUpdateType.Tile] = [];
|
||||
|
||||
this.callBack({
|
||||
tick: this.game.ticks(),
|
||||
packedTileUpdates: new BigUint64Array(packedTileUpdates),
|
||||
updates: updates,
|
||||
playerNameViewData: this.playerViewData,
|
||||
});
|
||||
this.isExecuting = false;
|
||||
}
|
||||
|
||||
public playerActions(
|
||||
playerID: PlayerID,
|
||||
x: number,
|
||||
y: number,
|
||||
): PlayerActions {
|
||||
const player = this.game.player(playerID);
|
||||
const tile = this.game.ref(x, y);
|
||||
const actions = {
|
||||
canBoat: player.canBoat(tile),
|
||||
canAttack: player.canAttack(tile),
|
||||
buildableUnits: Object.values(UnitType).filter(
|
||||
(ut) => player.canBuild(ut, tile) != false,
|
||||
),
|
||||
canSendEmojiAllPlayers: player.canSendEmoji(AllPlayers),
|
||||
} as PlayerActions;
|
||||
|
||||
if (this.game.hasOwner(tile)) {
|
||||
const other = this.game.owner(tile) as Player;
|
||||
actions.interaction = {
|
||||
sharedBorder: player.sharesBorderWith(other),
|
||||
canSendEmoji: player.canSendEmoji(other),
|
||||
canTarget: player.canTarget(other),
|
||||
canSendAllianceRequest:
|
||||
!player.recentOrPendingAllianceRequestWith(other),
|
||||
canBreakAlliance: player.isAlliedWith(other),
|
||||
canDonate: player.canDonate(other),
|
||||
};
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
public playerProfile(playerID: number): PlayerProfile {
|
||||
const player = this.game.playerBySmallID(playerID);
|
||||
if (!player.isPlayer()) {
|
||||
throw new Error(`player with id ${playerID} not found`);
|
||||
}
|
||||
return player.playerProfile();
|
||||
}
|
||||
}
|
||||
|
||||
+46
-47
@@ -1,51 +1,50 @@
|
||||
export class PseudoRandom {
|
||||
private m: number = 0x80000000; // 2**31
|
||||
private a: number = 1103515245;
|
||||
private c: number = 12345;
|
||||
private state: number;
|
||||
private m: number = 0x80000000; // 2**31
|
||||
private a: number = 1103515245;
|
||||
private c: number = 12345;
|
||||
private state: number;
|
||||
|
||||
constructor(seed: number) {
|
||||
this.state = seed % this.m;
|
||||
constructor(seed: number) {
|
||||
this.state = seed % this.m;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the next pseudorandom number.
|
||||
* @returns A number between 0 (inclusive) and 1 (exclusive).
|
||||
*/
|
||||
next(): number {
|
||||
this.state = (this.a * this.state + this.c) % this.m;
|
||||
return this.state / this.m;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random integer between min (inclusive) and max (exclusive).
|
||||
*/
|
||||
nextInt(min: number, max: number): number {
|
||||
return Math.floor(this.next() * (max - min) + min);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random float between min (inclusive) and max (exclusive).
|
||||
*/
|
||||
nextFloat(min: number, max: number): number {
|
||||
return this.next() * (max - min) + min;
|
||||
}
|
||||
|
||||
nextID(): string {
|
||||
return this.nextInt(0, Math.pow(36, 8)) // 36^8 possibilities
|
||||
.toString(36) // Convert to base36 (0-9 and a-z)
|
||||
.padStart(8, "0"); // Ensure 8 chars by padding with zeros
|
||||
}
|
||||
|
||||
randElement<T>(arr: T[]): T {
|
||||
if (arr.length == 0) {
|
||||
throw new Error("array must not be empty");
|
||||
}
|
||||
return arr[this.nextInt(0, arr.length)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the next pseudorandom number.
|
||||
* @returns A number between 0 (inclusive) and 1 (exclusive).
|
||||
*/
|
||||
next(): number {
|
||||
this.state = (this.a * this.state + this.c) % this.m;
|
||||
return this.state / this.m;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random integer between min (inclusive) and max (exclusive).
|
||||
*/
|
||||
nextInt(min: number, max: number): number {
|
||||
return Math.floor(this.next() * (max - min) + min);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random float between min (inclusive) and max (exclusive).
|
||||
*/
|
||||
nextFloat(min: number, max: number): number {
|
||||
return this.next() * (max - min) + min;
|
||||
}
|
||||
|
||||
nextID(): string {
|
||||
return this.nextInt(0, Math.pow(36, 8)) // 36^8 possibilities
|
||||
.toString(36) // Convert to base36 (0-9 and a-z)
|
||||
.padStart(8, '0'); // Ensure 8 chars by padding with zeros
|
||||
}
|
||||
|
||||
randElement<T>(arr: T[]): T {
|
||||
if (arr.length == 0) {
|
||||
throw new Error('array must not be empty')
|
||||
}
|
||||
return arr[this.nextInt(0, arr.length)];
|
||||
}
|
||||
|
||||
chance(odds: number): boolean {
|
||||
return this.nextInt(0, odds) == 0
|
||||
}
|
||||
|
||||
}
|
||||
chance(odds: number): boolean {
|
||||
return this.nextInt(0, odds) == 0;
|
||||
}
|
||||
}
|
||||
|
||||
+229
-197
@@ -1,288 +1,320 @@
|
||||
import { z } from 'zod';
|
||||
import { AllPlayers, Difficulty, GameMapType, GameType, PlayerType, UnitType } from './game/Game';
|
||||
import { z } from "zod";
|
||||
import {
|
||||
AllPlayers,
|
||||
Difficulty,
|
||||
GameMapType,
|
||||
GameType,
|
||||
PlayerType,
|
||||
UnitType,
|
||||
} from "./game/Game";
|
||||
|
||||
export type GameID = string
|
||||
export type ClientID = string
|
||||
export type GameID = string;
|
||||
export type ClientID = string;
|
||||
|
||||
export type Intent = SpawnIntent
|
||||
| AttackIntent
|
||||
| BoatAttackIntent
|
||||
| AllianceRequestIntent
|
||||
| AllianceRequestReplyIntent
|
||||
| BreakAllianceIntent
|
||||
| TargetPlayerIntent
|
||||
| EmojiIntent
|
||||
| DonateIntent
|
||||
| TargetTroopRatioIntent
|
||||
| BuildUnitIntent
|
||||
export type Intent =
|
||||
| SpawnIntent
|
||||
| AttackIntent
|
||||
| BoatAttackIntent
|
||||
| AllianceRequestIntent
|
||||
| AllianceRequestReplyIntent
|
||||
| BreakAllianceIntent
|
||||
| TargetPlayerIntent
|
||||
| EmojiIntent
|
||||
| DonateIntent
|
||||
| TargetTroopRatioIntent
|
||||
| BuildUnitIntent;
|
||||
|
||||
export type AttackIntent = z.infer<typeof AttackIntentSchema>
|
||||
export type SpawnIntent = z.infer<typeof SpawnIntentSchema>
|
||||
export type BoatAttackIntent = z.infer<typeof BoatAttackIntentSchema>
|
||||
export type AllianceRequestIntent = z.infer<typeof AllianceRequestIntentSchema>
|
||||
export type AllianceRequestReplyIntent = z.infer<typeof AllianceRequestReplyIntentSchema>
|
||||
export type BreakAllianceIntent = z.infer<typeof BreakAllianceIntentSchema>
|
||||
export type TargetPlayerIntent = z.infer<typeof TargetPlayerIntentSchema>
|
||||
export type EmojiIntent = z.infer<typeof EmojiIntentSchema>
|
||||
export type DonateIntent = z.infer<typeof DonateIntentSchema>
|
||||
export type TargetTroopRatioIntent = z.infer<typeof TargetTroopRatioIntentSchema>
|
||||
export type BuildUnitIntent = z.infer<typeof BuildUnitIntentSchema>
|
||||
export type AttackIntent = z.infer<typeof AttackIntentSchema>;
|
||||
export type SpawnIntent = z.infer<typeof SpawnIntentSchema>;
|
||||
export type BoatAttackIntent = z.infer<typeof BoatAttackIntentSchema>;
|
||||
export type AllianceRequestIntent = z.infer<typeof AllianceRequestIntentSchema>;
|
||||
export type AllianceRequestReplyIntent = z.infer<
|
||||
typeof AllianceRequestReplyIntentSchema
|
||||
>;
|
||||
export type BreakAllianceIntent = z.infer<typeof BreakAllianceIntentSchema>;
|
||||
export type TargetPlayerIntent = z.infer<typeof TargetPlayerIntentSchema>;
|
||||
export type EmojiIntent = z.infer<typeof EmojiIntentSchema>;
|
||||
export type DonateIntent = z.infer<typeof DonateIntentSchema>;
|
||||
export type TargetTroopRatioIntent = z.infer<
|
||||
typeof TargetTroopRatioIntentSchema
|
||||
>;
|
||||
export type BuildUnitIntent = z.infer<typeof BuildUnitIntentSchema>;
|
||||
|
||||
export type Turn = z.infer<typeof TurnSchema>
|
||||
export type GameConfig = z.infer<typeof GameConfigSchema>
|
||||
export type Turn = z.infer<typeof TurnSchema>;
|
||||
export type GameConfig = z.infer<typeof GameConfigSchema>;
|
||||
|
||||
export type ClientMessage = ClientSendWinnerMessage | ClientPingMessage | ClientIntentMessage | ClientJoinMessage | ClientLogMessage
|
||||
export type ServerMessage = ServerSyncMessage | ServerStartGameMessage | ServerPingMessage
|
||||
export type ClientMessage =
|
||||
| ClientSendWinnerMessage
|
||||
| ClientPingMessage
|
||||
| ClientIntentMessage
|
||||
| ClientJoinMessage
|
||||
| ClientLogMessage;
|
||||
export type ServerMessage =
|
||||
| ServerSyncMessage
|
||||
| ServerStartGameMessage
|
||||
| ServerPingMessage;
|
||||
|
||||
export type ServerSyncMessage = z.infer<typeof ServerTurnMessageSchema>
|
||||
export type ServerStartGameMessage = z.infer<typeof ServerStartGameMessageSchema>
|
||||
export type ServerPingMessage = z.infer<typeof ServerPingMessageSchema>
|
||||
export type ServerSyncMessage = z.infer<typeof ServerTurnMessageSchema>;
|
||||
export type ServerStartGameMessage = z.infer<
|
||||
typeof ServerStartGameMessageSchema
|
||||
>;
|
||||
export type ServerPingMessage = z.infer<typeof ServerPingMessageSchema>;
|
||||
|
||||
export type ClientSendWinnerMessage = z.infer<typeof ClientSendWinnerSchema>
|
||||
export type ClientPingMessage = z.infer<typeof ClientPingMessageSchema>
|
||||
export type ClientIntentMessage = z.infer<typeof ClientIntentMessageSchema>
|
||||
export type ClientJoinMessage = z.infer<typeof ClientJoinMessageSchema>
|
||||
export type ClientLogMessage = z.infer<typeof ClientLogMessageSchema>
|
||||
export type ClientSendWinnerMessage = z.infer<typeof ClientSendWinnerSchema>;
|
||||
export type ClientPingMessage = z.infer<typeof ClientPingMessageSchema>;
|
||||
export type ClientIntentMessage = z.infer<typeof ClientIntentMessageSchema>;
|
||||
export type ClientJoinMessage = z.infer<typeof ClientJoinMessageSchema>;
|
||||
export type ClientLogMessage = z.infer<typeof ClientLogMessageSchema>;
|
||||
|
||||
export type PlayerRecord = z.infer<typeof PlayerRecordSchema>
|
||||
export type GameRecord = z.infer<typeof GameRecordSchema>
|
||||
export type PlayerRecord = z.infer<typeof PlayerRecordSchema>;
|
||||
export type GameRecord = z.infer<typeof GameRecordSchema>;
|
||||
|
||||
const PlayerTypeSchema = z.nativeEnum(PlayerType);
|
||||
|
||||
export enum LogSeverity {
|
||||
Debug = 'DEBUG',
|
||||
Info = 'INFO',
|
||||
Warn = 'WARN',
|
||||
Error = 'ERROR',
|
||||
Fatal = 'FATAL'
|
||||
Debug = "DEBUG",
|
||||
Info = "INFO",
|
||||
Warn = "WARN",
|
||||
Error = "ERROR",
|
||||
Fatal = "FATAL",
|
||||
}
|
||||
|
||||
// TODO: create Cell schema
|
||||
|
||||
export interface Lobby {
|
||||
id: string;
|
||||
msUntilStart?: number;
|
||||
numClients?: number;
|
||||
id: string;
|
||||
msUntilStart?: number;
|
||||
numClients?: number;
|
||||
}
|
||||
|
||||
const GameConfigSchema = z.object({
|
||||
gameMap: z.nativeEnum(GameMapType),
|
||||
difficulty: z.nativeEnum(Difficulty),
|
||||
gameType: z.nativeEnum(GameType)
|
||||
})
|
||||
gameMap: z.nativeEnum(GameMapType),
|
||||
difficulty: z.nativeEnum(Difficulty),
|
||||
gameType: z.nativeEnum(GameType),
|
||||
});
|
||||
|
||||
const SafeString = z.string()
|
||||
// Remove common dangerous characters and patterns
|
||||
.regex(/^[a-zA-Z0-9\s.,!?@#$%&*()-_+=[\]{}|;:"'\/]+$/)
|
||||
// Reasonable max length to prevent DOS
|
||||
.max(1000)
|
||||
const SafeString = z
|
||||
.string()
|
||||
// Remove common dangerous characters and patterns
|
||||
.regex(/^[a-zA-Z0-9\s.,!?@#$%&*()-_+=[\]{}|;:"'\/]+$/)
|
||||
// Reasonable max length to prevent DOS
|
||||
.max(1000);
|
||||
|
||||
const EmojiSchema = z.string().refine(
|
||||
(val) => {
|
||||
return /\p{Emoji}/u.test(val);
|
||||
},
|
||||
{
|
||||
message: "Must contain at least one emoji character"
|
||||
}
|
||||
(val) => {
|
||||
return /\p{Emoji}/u.test(val);
|
||||
},
|
||||
{
|
||||
message: "Must contain at least one emoji character",
|
||||
},
|
||||
);
|
||||
const ID = z.string()
|
||||
.regex(/^[a-zA-Z0-9]+$/)
|
||||
.length(8);
|
||||
|
||||
const ID = z
|
||||
.string()
|
||||
.regex(/^[a-zA-Z0-9]+$/)
|
||||
.length(8);
|
||||
|
||||
// Zod schemas
|
||||
const BaseIntentSchema = z.object({
|
||||
type: z.enum(['attack', 'spawn', 'boat', 'name', 'targetPlayer', 'emoji', 'troop_ratio', 'build_unit']),
|
||||
clientID: ID,
|
||||
type: z.enum([
|
||||
"attack",
|
||||
"spawn",
|
||||
"boat",
|
||||
"name",
|
||||
"targetPlayer",
|
||||
"emoji",
|
||||
"troop_ratio",
|
||||
"build_unit",
|
||||
]),
|
||||
clientID: ID,
|
||||
});
|
||||
|
||||
export const AttackIntentSchema = BaseIntentSchema.extend({
|
||||
type: z.literal('attack'),
|
||||
attackerID: ID,
|
||||
targetID: ID.nullable(),
|
||||
troops: z.number().nullable(),
|
||||
type: z.literal("attack"),
|
||||
attackerID: ID,
|
||||
targetID: ID.nullable(),
|
||||
troops: z.number().nullable(),
|
||||
});
|
||||
|
||||
export const SpawnIntentSchema = BaseIntentSchema.extend({
|
||||
type: z.literal('spawn'),
|
||||
playerID: ID,
|
||||
name: SafeString,
|
||||
playerType: PlayerTypeSchema,
|
||||
x: z.number(),
|
||||
y: z.number(),
|
||||
})
|
||||
type: z.literal("spawn"),
|
||||
playerID: ID,
|
||||
name: SafeString,
|
||||
playerType: PlayerTypeSchema,
|
||||
x: z.number(),
|
||||
y: z.number(),
|
||||
});
|
||||
|
||||
export const BoatAttackIntentSchema = BaseIntentSchema.extend({
|
||||
type: z.literal('boat'),
|
||||
attackerID: ID,
|
||||
targetID: ID.nullable(),
|
||||
troops: z.number().nullable(),
|
||||
x: z.number(),
|
||||
y: z.number(),
|
||||
})
|
||||
type: z.literal("boat"),
|
||||
attackerID: ID,
|
||||
targetID: ID.nullable(),
|
||||
troops: z.number().nullable(),
|
||||
x: z.number(),
|
||||
y: z.number(),
|
||||
});
|
||||
|
||||
export const AllianceRequestIntentSchema = BaseIntentSchema.extend({
|
||||
type: z.literal('allianceRequest'),
|
||||
requestor: ID,
|
||||
recipient: ID,
|
||||
})
|
||||
type: z.literal("allianceRequest"),
|
||||
requestor: ID,
|
||||
recipient: ID,
|
||||
});
|
||||
|
||||
export const AllianceRequestReplyIntentSchema = BaseIntentSchema.extend({
|
||||
type: z.literal('allianceRequestReply'),
|
||||
requestor: ID, // The one who made the original alliance request
|
||||
recipient: ID,
|
||||
accept: z.boolean(),
|
||||
})
|
||||
type: z.literal("allianceRequestReply"),
|
||||
requestor: ID, // The one who made the original alliance request
|
||||
recipient: ID,
|
||||
accept: z.boolean(),
|
||||
});
|
||||
|
||||
export const BreakAllianceIntentSchema = BaseIntentSchema.extend({
|
||||
type: z.literal('breakAlliance'),
|
||||
requestor: ID, // The one who made the original alliance request
|
||||
recipient: ID,
|
||||
})
|
||||
type: z.literal("breakAlliance"),
|
||||
requestor: ID, // The one who made the original alliance request
|
||||
recipient: ID,
|
||||
});
|
||||
|
||||
export const TargetPlayerIntentSchema = BaseIntentSchema.extend({
|
||||
type: z.literal('targetPlayer'),
|
||||
requestor: ID,
|
||||
target: ID,
|
||||
})
|
||||
type: z.literal("targetPlayer"),
|
||||
requestor: ID,
|
||||
target: ID,
|
||||
});
|
||||
|
||||
export const EmojiIntentSchema = BaseIntentSchema.extend({
|
||||
type: z.literal('emoji'),
|
||||
sender: ID,
|
||||
recipient: z.union([ID, z.literal(AllPlayers)]),
|
||||
emoji: EmojiSchema,
|
||||
})
|
||||
type: z.literal("emoji"),
|
||||
sender: ID,
|
||||
recipient: z.union([ID, z.literal(AllPlayers)]),
|
||||
emoji: EmojiSchema,
|
||||
});
|
||||
|
||||
export const DonateIntentSchema = BaseIntentSchema.extend({
|
||||
type: z.literal('donate'),
|
||||
sender: ID,
|
||||
recipient: ID,
|
||||
troops: z.number().nullable(),
|
||||
})
|
||||
type: z.literal("donate"),
|
||||
sender: ID,
|
||||
recipient: ID,
|
||||
troops: z.number().nullable(),
|
||||
});
|
||||
|
||||
export const TargetTroopRatioIntentSchema = BaseIntentSchema.extend({
|
||||
type: z.literal('troop_ratio'),
|
||||
player: ID,
|
||||
ratio: z.number().min(0).max(1),
|
||||
})
|
||||
type: z.literal("troop_ratio"),
|
||||
player: ID,
|
||||
ratio: z.number().min(0).max(1),
|
||||
});
|
||||
|
||||
export const BuildUnitIntentSchema = BaseIntentSchema.extend({
|
||||
type: z.literal('build_unit'),
|
||||
player: ID,
|
||||
unit: z.nativeEnum(UnitType),
|
||||
x: z.number(),
|
||||
y: z.number(),
|
||||
})
|
||||
type: z.literal("build_unit"),
|
||||
player: ID,
|
||||
unit: z.nativeEnum(UnitType),
|
||||
x: z.number(),
|
||||
y: z.number(),
|
||||
});
|
||||
|
||||
const IntentSchema = z.union([
|
||||
AttackIntentSchema,
|
||||
SpawnIntentSchema,
|
||||
BoatAttackIntentSchema,
|
||||
AllianceRequestIntentSchema,
|
||||
AllianceRequestReplyIntentSchema,
|
||||
BreakAllianceIntentSchema,
|
||||
TargetPlayerIntentSchema,
|
||||
EmojiIntentSchema,
|
||||
DonateIntentSchema,
|
||||
TargetTroopRatioIntentSchema,
|
||||
BuildUnitIntentSchema,
|
||||
AttackIntentSchema,
|
||||
SpawnIntentSchema,
|
||||
BoatAttackIntentSchema,
|
||||
AllianceRequestIntentSchema,
|
||||
AllianceRequestReplyIntentSchema,
|
||||
BreakAllianceIntentSchema,
|
||||
TargetPlayerIntentSchema,
|
||||
EmojiIntentSchema,
|
||||
DonateIntentSchema,
|
||||
TargetTroopRatioIntentSchema,
|
||||
BuildUnitIntentSchema,
|
||||
]);
|
||||
|
||||
export const TurnSchema = z.object({
|
||||
turnNumber: z.number(),
|
||||
gameID: ID,
|
||||
intents: z.array(IntentSchema)
|
||||
})
|
||||
turnNumber: z.number(),
|
||||
gameID: ID,
|
||||
intents: z.array(IntentSchema),
|
||||
});
|
||||
|
||||
// Server
|
||||
|
||||
const ServerBaseMessageSchema = z.object({
|
||||
type: SafeString
|
||||
})
|
||||
type: SafeString,
|
||||
});
|
||||
|
||||
export const ServerTurnMessageSchema = ServerBaseMessageSchema.extend({
|
||||
type: z.literal('turn'),
|
||||
turn: TurnSchema,
|
||||
})
|
||||
type: z.literal("turn"),
|
||||
turn: TurnSchema,
|
||||
});
|
||||
|
||||
export const ServerPingMessageSchema = ServerBaseMessageSchema.extend({
|
||||
type: z.literal('ping')
|
||||
})
|
||||
type: z.literal("ping"),
|
||||
});
|
||||
|
||||
export const ServerStartGameMessageSchema = ServerBaseMessageSchema.extend({
|
||||
type: z.literal('start'),
|
||||
// Turns the client missed if they are late to the game.
|
||||
turns: z.array(TurnSchema),
|
||||
config: GameConfigSchema
|
||||
})
|
||||
type: z.literal("start"),
|
||||
// Turns the client missed if they are late to the game.
|
||||
turns: z.array(TurnSchema),
|
||||
config: GameConfigSchema,
|
||||
});
|
||||
|
||||
export const ServerMessageSchema = z.union([
|
||||
ServerTurnMessageSchema,
|
||||
ServerStartGameMessageSchema,
|
||||
ServerPingMessageSchema,
|
||||
ServerTurnMessageSchema,
|
||||
ServerStartGameMessageSchema,
|
||||
ServerPingMessageSchema,
|
||||
]);
|
||||
|
||||
// Client
|
||||
|
||||
const ClientBaseMessageSchema = z.object({
|
||||
type: z.enum(['winner', 'join', 'intent', 'ping', 'log']),
|
||||
clientID: ID,
|
||||
gameID: ID,
|
||||
})
|
||||
type: z.enum(["winner", "join", "intent", "ping", "log"]),
|
||||
clientID: ID,
|
||||
gameID: ID,
|
||||
});
|
||||
|
||||
export const ClientSendWinnerSchema = ClientBaseMessageSchema.extend({
|
||||
type: z.literal('winner'),
|
||||
winner: ID.nullable(),
|
||||
})
|
||||
type: z.literal("winner"),
|
||||
winner: ID.nullable(),
|
||||
});
|
||||
|
||||
export const ClientLogMessageSchema = ClientBaseMessageSchema.extend({
|
||||
type: z.literal('log'),
|
||||
severity: z.nativeEnum(LogSeverity),
|
||||
log: ID,
|
||||
persistentID: SafeString,
|
||||
})
|
||||
type: z.literal("log"),
|
||||
severity: z.nativeEnum(LogSeverity),
|
||||
log: ID,
|
||||
persistentID: SafeString,
|
||||
});
|
||||
|
||||
export const ClientPingMessageSchema = ClientBaseMessageSchema.extend({
|
||||
type: z.literal('ping'),
|
||||
})
|
||||
type: z.literal("ping"),
|
||||
});
|
||||
|
||||
export const ClientIntentMessageSchema = ClientBaseMessageSchema.extend({
|
||||
type: z.literal('intent'),
|
||||
intent: IntentSchema
|
||||
})
|
||||
type: z.literal("intent"),
|
||||
intent: IntentSchema,
|
||||
});
|
||||
|
||||
// WARNING: never send this message to clients.
|
||||
export const ClientJoinMessageSchema = ClientBaseMessageSchema.extend({
|
||||
type: z.literal('join'),
|
||||
persistentID: SafeString, // WARNING: persistent id is private.
|
||||
lastTurn: z.number(), // The last turn the client saw.
|
||||
username: SafeString,
|
||||
})
|
||||
type: z.literal("join"),
|
||||
persistentID: SafeString, // WARNING: persistent id is private.
|
||||
lastTurn: z.number(), // The last turn the client saw.
|
||||
username: SafeString,
|
||||
});
|
||||
|
||||
export const ClientMessageSchema = z.union([
|
||||
ClientSendWinnerSchema,
|
||||
ClientPingMessageSchema,
|
||||
ClientIntentMessageSchema,
|
||||
ClientJoinMessageSchema,
|
||||
ClientLogMessageSchema,
|
||||
ClientSendWinnerSchema,
|
||||
ClientPingMessageSchema,
|
||||
ClientIntentMessageSchema,
|
||||
ClientJoinMessageSchema,
|
||||
ClientLogMessageSchema,
|
||||
]);
|
||||
|
||||
export const PlayerRecordSchema = z.object({
|
||||
clientID: ID,
|
||||
username: SafeString,
|
||||
ip: SafeString.nullable(), // WARNING: PII
|
||||
persistentID: SafeString, // WARNING: PII
|
||||
})
|
||||
clientID: ID,
|
||||
username: SafeString,
|
||||
ip: SafeString.nullable(), // WARNING: PII
|
||||
persistentID: SafeString, // WARNING: PII
|
||||
});
|
||||
|
||||
export const GameRecordSchema = z.object({
|
||||
id: ID,
|
||||
gameConfig: GameConfigSchema,
|
||||
players: z.array(PlayerRecordSchema),
|
||||
startTimestampMS: z.number(),
|
||||
endTimestampMS: z.number(),
|
||||
durationSeconds: z.number(),
|
||||
date: SafeString,
|
||||
num_turns: z.number(),
|
||||
turns: z.array(TurnSchema),
|
||||
winner: ID.nullable()
|
||||
})
|
||||
id: ID,
|
||||
gameConfig: GameConfigSchema,
|
||||
players: z.array(PlayerRecordSchema),
|
||||
startTimestampMS: z.number(),
|
||||
endTimestampMS: z.number(),
|
||||
durationSeconds: z.number(),
|
||||
date: SafeString,
|
||||
num_turns: z.number(),
|
||||
turns: z.array(TurnSchema),
|
||||
winner: ID.nullable(),
|
||||
});
|
||||
|
||||
+232
-173
@@ -1,174 +1,229 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import twemoji from 'twemoji';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import twemoji from "twemoji";
|
||||
import DOMPurify from "dompurify";
|
||||
import { Cell, Game, Player, Unit } from "./game/Game";
|
||||
import { ClientID, GameConfig, GameID, GameRecord, PlayerRecord, Turn } from './Schemas';
|
||||
import { customAlphabet, nanoid } from 'nanoid';
|
||||
import { andFN, GameMap, manhattanDistFN, TileRef } from './game/GameMap';
|
||||
import {
|
||||
ClientID,
|
||||
GameConfig,
|
||||
GameID,
|
||||
GameRecord,
|
||||
PlayerRecord,
|
||||
Turn,
|
||||
} from "./Schemas";
|
||||
import { customAlphabet, nanoid } from "nanoid";
|
||||
import { andFN, GameMap, manhattanDistFN, TileRef } from "./game/GameMap";
|
||||
|
||||
export function manhattanDistWrapped(
|
||||
c1: Cell,
|
||||
c2: Cell,
|
||||
width: number,
|
||||
): number {
|
||||
// Calculate x distance
|
||||
let dx = Math.abs(c1.x - c2.x);
|
||||
// Check if wrapping around the x-axis is shorter
|
||||
dx = Math.min(dx, width - dx);
|
||||
|
||||
// Calculate y distance (no wrapping for y-axis)
|
||||
let dy = Math.abs(c1.y - c2.y);
|
||||
|
||||
|
||||
|
||||
export function manhattanDistWrapped(c1: Cell, c2: Cell, width: number): number {
|
||||
// Calculate x distance
|
||||
let dx = Math.abs(c1.x - c2.x);
|
||||
// Check if wrapping around the x-axis is shorter
|
||||
dx = Math.min(dx, width - dx);
|
||||
|
||||
// Calculate y distance (no wrapping for y-axis)
|
||||
let dy = Math.abs(c1.y - c2.y);
|
||||
|
||||
// Return the sum of x and y distances
|
||||
return dx + dy;
|
||||
// Return the sum of x and y distances
|
||||
return dx + dy;
|
||||
}
|
||||
|
||||
export function within(value: number, min: number, max: number): number {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
|
||||
export function distSort(gm: GameMap, target: TileRef): (a: TileRef, b: TileRef) => number {
|
||||
return (a: TileRef, b: TileRef) => {
|
||||
return gm.manhattanDist(a, target) - gm.manhattanDist(b, target);
|
||||
}
|
||||
export function distSort(
|
||||
gm: GameMap,
|
||||
target: TileRef,
|
||||
): (a: TileRef, b: TileRef) => number {
|
||||
return (a: TileRef, b: TileRef) => {
|
||||
return gm.manhattanDist(a, target) - gm.manhattanDist(b, target);
|
||||
};
|
||||
}
|
||||
|
||||
export function distSortUnit(gm: GameMap, target: Unit | TileRef): (a: Unit, b: Unit) => number {
|
||||
const targetRef = (typeof target === 'number') ? target : target.tile()
|
||||
export function distSortUnit(
|
||||
gm: GameMap,
|
||||
target: Unit | TileRef,
|
||||
): (a: Unit, b: Unit) => number {
|
||||
const targetRef = typeof target === "number" ? target : target.tile();
|
||||
|
||||
return (a: Unit, b: Unit) => {
|
||||
return gm.manhattanDist(a.tile(), targetRef) - gm.manhattanDist(b.tile(), targetRef);
|
||||
}
|
||||
return (a: Unit, b: Unit) => {
|
||||
return (
|
||||
gm.manhattanDist(a.tile(), targetRef) -
|
||||
gm.manhattanDist(b.tile(), targetRef)
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// TODO: refactor to new file
|
||||
export function sourceDstOceanShore(gm: Game, src: Player, tile: TileRef): [TileRef | null, TileRef | null] {
|
||||
const dst = gm.owner(tile)
|
||||
let srcTile = closestOceanShoreFromPlayer(gm, src, tile)
|
||||
let dstTile: TileRef | null = null
|
||||
if (dst.isPlayer()) {
|
||||
dstTile = closestOceanShoreFromPlayer(gm, dst as Player, tile)
|
||||
} else {
|
||||
dstTile = closestOceanShoreTN(gm, tile, 300)
|
||||
}
|
||||
return [srcTile, dstTile]
|
||||
export function sourceDstOceanShore(
|
||||
gm: Game,
|
||||
src: Player,
|
||||
tile: TileRef,
|
||||
): [TileRef | null, TileRef | null] {
|
||||
const dst = gm.owner(tile);
|
||||
let srcTile = closestOceanShoreFromPlayer(gm, src, tile);
|
||||
let dstTile: TileRef | null = null;
|
||||
if (dst.isPlayer()) {
|
||||
dstTile = closestOceanShoreFromPlayer(gm, dst as Player, tile);
|
||||
} else {
|
||||
dstTile = closestOceanShoreTN(gm, tile, 300);
|
||||
}
|
||||
return [srcTile, dstTile];
|
||||
}
|
||||
|
||||
export function targetTransportTile(gm: Game, tile: TileRef): TileRef | null {
|
||||
const dst = gm.playerBySmallID(gm.ownerID(tile))
|
||||
let dstTile: TileRef | null = null
|
||||
if (dst.isPlayer()) {
|
||||
dstTile = closestOceanShoreFromPlayer(gm, dst as Player, tile)
|
||||
} else {
|
||||
dstTile = closestOceanShoreTN(gm, tile, 300)
|
||||
}
|
||||
return dstTile
|
||||
const dst = gm.playerBySmallID(gm.ownerID(tile));
|
||||
let dstTile: TileRef | null = null;
|
||||
if (dst.isPlayer()) {
|
||||
dstTile = closestOceanShoreFromPlayer(gm, dst as Player, tile);
|
||||
} else {
|
||||
dstTile = closestOceanShoreTN(gm, tile, 300);
|
||||
}
|
||||
return dstTile;
|
||||
}
|
||||
|
||||
export function closestOceanShoreFromPlayer(gm: GameMap, player: Player, target: TileRef): TileRef | null {
|
||||
const shoreTiles = Array.from(player.borderTiles()).filter(t => gm.isOceanShore(t))
|
||||
if (shoreTiles.length == 0) {
|
||||
return null
|
||||
}
|
||||
export function closestOceanShoreFromPlayer(
|
||||
gm: GameMap,
|
||||
player: Player,
|
||||
target: TileRef,
|
||||
): TileRef | null {
|
||||
const shoreTiles = Array.from(player.borderTiles()).filter((t) =>
|
||||
gm.isOceanShore(t),
|
||||
);
|
||||
if (shoreTiles.length == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return shoreTiles.reduce((closest, current) => {
|
||||
const closestDistance = manhattanDistWrapped(gm.cell(target), gm.cell(closest), gm.width());
|
||||
const currentDistance = manhattanDistWrapped(gm.cell(target), gm.cell(current), gm.width());
|
||||
return currentDistance < closestDistance ? current : closest;
|
||||
});
|
||||
return shoreTiles.reduce((closest, current) => {
|
||||
const closestDistance = manhattanDistWrapped(
|
||||
gm.cell(target),
|
||||
gm.cell(closest),
|
||||
gm.width(),
|
||||
);
|
||||
const currentDistance = manhattanDistWrapped(
|
||||
gm.cell(target),
|
||||
gm.cell(current),
|
||||
gm.width(),
|
||||
);
|
||||
return currentDistance < closestDistance ? current : closest;
|
||||
});
|
||||
}
|
||||
|
||||
function closestOceanShoreTN(gm: GameMap, tile: TileRef, searchDist: number): TileRef {
|
||||
const tn = Array.from(gm.bfs(tile, andFN((_, t) => !gm.hasOwner(t), manhattanDistFN(tile, searchDist))))
|
||||
.filter(t => gm.isOceanShore(t))
|
||||
.sort((a, b) => gm.manhattanDist(tile, a) - gm.manhattanDist(tile, b))
|
||||
if (tn.length == 0) {
|
||||
return null
|
||||
}
|
||||
return tn[0]
|
||||
function closestOceanShoreTN(
|
||||
gm: GameMap,
|
||||
tile: TileRef,
|
||||
searchDist: number,
|
||||
): TileRef {
|
||||
const tn = Array.from(
|
||||
gm.bfs(
|
||||
tile,
|
||||
andFN((_, t) => !gm.hasOwner(t), manhattanDistFN(tile, searchDist)),
|
||||
),
|
||||
)
|
||||
.filter((t) => gm.isOceanShore(t))
|
||||
.sort((a, b) => gm.manhattanDist(tile, a) - gm.manhattanDist(tile, b));
|
||||
if (tn.length == 0) {
|
||||
return null;
|
||||
}
|
||||
return tn[0];
|
||||
}
|
||||
|
||||
export function simpleHash(str: string): number {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + char;
|
||||
hash = hash & hash; // Convert to 32-bit integer
|
||||
}
|
||||
return Math.abs(hash);
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str.charCodeAt(i);
|
||||
hash = (hash << 5) - hash + char;
|
||||
hash = hash & hash; // Convert to 32-bit integer
|
||||
}
|
||||
return Math.abs(hash);
|
||||
}
|
||||
|
||||
export function calculateBoundingBox(gm: GameMap, borderTiles: ReadonlySet<TileRef>): { min: Cell; max: Cell } {
|
||||
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
||||
export function calculateBoundingBox(
|
||||
gm: GameMap,
|
||||
borderTiles: ReadonlySet<TileRef>,
|
||||
): { min: Cell; max: Cell } {
|
||||
let minX = Infinity,
|
||||
minY = Infinity,
|
||||
maxX = -Infinity,
|
||||
maxY = -Infinity;
|
||||
|
||||
borderTiles.forEach((tile: TileRef) => {
|
||||
const cell = gm.cell(tile);
|
||||
minX = Math.min(minX, cell.x);
|
||||
minY = Math.min(minY, cell.y);
|
||||
maxX = Math.max(maxX, cell.x);
|
||||
maxY = Math.max(maxY, cell.y);
|
||||
});
|
||||
borderTiles.forEach((tile: TileRef) => {
|
||||
const cell = gm.cell(tile);
|
||||
minX = Math.min(minX, cell.x);
|
||||
minY = Math.min(minY, cell.y);
|
||||
maxX = Math.max(maxX, cell.x);
|
||||
maxY = Math.max(maxY, cell.y);
|
||||
});
|
||||
|
||||
return { min: new Cell(minX, minY), max: new Cell(maxX, maxY) }
|
||||
return { min: new Cell(minX, minY), max: new Cell(maxX, maxY) };
|
||||
}
|
||||
|
||||
export function calculateBoundingBoxCenter(gm: GameMap, borderTiles: ReadonlySet<TileRef>): Cell {
|
||||
const { min, max } = calculateBoundingBox(gm, borderTiles)
|
||||
return new Cell(
|
||||
min.x + Math.floor((max.x - min.x) / 2),
|
||||
min.y + Math.floor((max.y - min.y) / 2)
|
||||
)
|
||||
export function calculateBoundingBoxCenter(
|
||||
gm: GameMap,
|
||||
borderTiles: ReadonlySet<TileRef>,
|
||||
): Cell {
|
||||
const { min, max } = calculateBoundingBox(gm, borderTiles);
|
||||
return new Cell(
|
||||
min.x + Math.floor((max.x - min.x) / 2),
|
||||
min.y + Math.floor((max.y - min.y) / 2),
|
||||
);
|
||||
}
|
||||
|
||||
export function inscribed(outer: { min: Cell; max: Cell }, inner: { min: Cell; max: Cell }): boolean {
|
||||
return (
|
||||
outer.min.x <= inner.min.x &&
|
||||
outer.min.y <= inner.min.y &&
|
||||
outer.max.x >= inner.max.x &&
|
||||
outer.max.y >= inner.max.y
|
||||
);
|
||||
export function inscribed(
|
||||
outer: { min: Cell; max: Cell },
|
||||
inner: { min: Cell; max: Cell },
|
||||
): boolean {
|
||||
return (
|
||||
outer.min.x <= inner.min.x &&
|
||||
outer.min.y <= inner.min.y &&
|
||||
outer.max.x >= inner.max.x &&
|
||||
outer.max.y >= inner.max.y
|
||||
);
|
||||
}
|
||||
|
||||
export function getMode(list: Set<number>): number {
|
||||
// Count occurrences
|
||||
const counts = new Map<number, number>()
|
||||
for (const item of list) {
|
||||
counts.set(item, (counts.get(item) || 0) + 1);
|
||||
// Count occurrences
|
||||
const counts = new Map<number, number>();
|
||||
for (const item of list) {
|
||||
counts.set(item, (counts.get(item) || 0) + 1);
|
||||
}
|
||||
|
||||
// Find the item with the highest count
|
||||
let mode = 0;
|
||||
let maxCount = 0;
|
||||
|
||||
for (const [item, count] of counts) {
|
||||
if (count > maxCount) {
|
||||
maxCount = count;
|
||||
mode = item;
|
||||
}
|
||||
}
|
||||
|
||||
// Find the item with the highest count
|
||||
let mode = 0;
|
||||
let maxCount = 0;
|
||||
|
||||
for (const [item, count] of counts) {
|
||||
if (count > maxCount) {
|
||||
maxCount = count
|
||||
mode = item;
|
||||
}
|
||||
}
|
||||
|
||||
return mode;
|
||||
return mode;
|
||||
}
|
||||
|
||||
export function sanitize(name: string): string {
|
||||
return Array.from(name).join('').replace(/[^\p{L}\p{N}\s\p{Emoji}\p{Emoji_Component}]/gu, '');
|
||||
return Array.from(name)
|
||||
.join("")
|
||||
.replace(/[^\p{L}\p{N}\s\p{Emoji}\p{Emoji_Component}]/gu, "");
|
||||
}
|
||||
|
||||
export function processName(name: string): string {
|
||||
// First sanitize the raw input - strip everything except text and emojis
|
||||
const sanitizedName = sanitize(name);
|
||||
// First sanitize the raw input - strip everything except text and emojis
|
||||
const sanitizedName = sanitize(name);
|
||||
|
||||
// Process emojis with twemoji
|
||||
const withEmojis = twemoji.parse(sanitizedName, {
|
||||
base: 'https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/',
|
||||
folder: 'svg',
|
||||
ext: '.svg'
|
||||
});
|
||||
// Process emojis with twemoji
|
||||
const withEmojis = twemoji.parse(sanitizedName, {
|
||||
base: "https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/",
|
||||
folder: "svg",
|
||||
ext: ".svg",
|
||||
});
|
||||
|
||||
// Add CSS styles inline to the wrapper span
|
||||
const styledHTML = `
|
||||
// Add CSS styles inline to the wrapper span
|
||||
const styledHTML = `
|
||||
<span class="player-name" style="
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -180,71 +235,75 @@ export function processName(name: string): string {
|
||||
</span>
|
||||
`;
|
||||
|
||||
// Add CSS for the emoji images
|
||||
const withEmojiStyles = styledHTML.replace(
|
||||
/<img/g,
|
||||
'<img style="height: 1.2em; width: 1.2em; vertical-align: -0.2em; margin: 0 0.05em 0 0.1em;"'
|
||||
);
|
||||
// Add CSS for the emoji images
|
||||
const withEmojiStyles = styledHTML.replace(
|
||||
/<img/g,
|
||||
'<img style="height: 1.2em; width: 1.2em; vertical-align: -0.2em; margin: 0 0.05em 0 0.1em;"',
|
||||
);
|
||||
|
||||
// Sanitize the final HTML, allowing styles and specific attributes
|
||||
return onlyImages(withEmojiStyles)
|
||||
// Sanitize the final HTML, allowing styles and specific attributes
|
||||
return onlyImages(withEmojiStyles);
|
||||
}
|
||||
|
||||
export function onlyImages(html: string) {
|
||||
|
||||
return DOMPurify.sanitize(html, {
|
||||
ALLOWED_TAGS: ['span', 'img'],
|
||||
ALLOWED_ATTR: ['src', 'alt', 'class', 'style'],
|
||||
ALLOWED_URI_REGEXP: /^https:\/\/cdn\.jsdelivr\.net\/gh\/twitter\/twemoji/,
|
||||
ADD_ATTR: ['style']
|
||||
});
|
||||
return DOMPurify.sanitize(html, {
|
||||
ALLOWED_TAGS: ["span", "img"],
|
||||
ALLOWED_ATTR: ["src", "alt", "class", "style"],
|
||||
ALLOWED_URI_REGEXP: /^https:\/\/cdn\.jsdelivr\.net\/gh\/twitter\/twemoji/,
|
||||
ADD_ATTR: ["style"],
|
||||
});
|
||||
}
|
||||
|
||||
export function CreateGameRecord(
|
||||
id: GameID,
|
||||
gameConfig: GameConfig,
|
||||
// username does not need to be set.
|
||||
players: PlayerRecord[],
|
||||
turns: Turn[],
|
||||
start: number,
|
||||
end: number,
|
||||
winner: ClientID | null
|
||||
id: GameID,
|
||||
gameConfig: GameConfig,
|
||||
// username does not need to be set.
|
||||
players: PlayerRecord[],
|
||||
turns: Turn[],
|
||||
start: number,
|
||||
end: number,
|
||||
winner: ClientID | null,
|
||||
): GameRecord {
|
||||
const record: GameRecord = {
|
||||
id: id,
|
||||
gameConfig: gameConfig,
|
||||
startTimestampMS: start,
|
||||
endTimestampMS: end,
|
||||
date: new Date().toISOString().split('T')[0],
|
||||
turns: []
|
||||
}
|
||||
const record: GameRecord = {
|
||||
id: id,
|
||||
gameConfig: gameConfig,
|
||||
startTimestampMS: start,
|
||||
endTimestampMS: end,
|
||||
date: new Date().toISOString().split("T")[0],
|
||||
turns: [],
|
||||
};
|
||||
|
||||
for (const turn of turns) {
|
||||
if (turn.intents.length != 0) {
|
||||
record.turns.push(turn)
|
||||
for (const intent of turn.intents) {
|
||||
if (intent.type == "spawn") {
|
||||
for (const playerRecord of players) {
|
||||
if (playerRecord.clientID == intent.clientID) {
|
||||
playerRecord.username = intent.name
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const turn of turns) {
|
||||
if (turn.intents.length != 0) {
|
||||
record.turns.push(turn);
|
||||
for (const intent of turn.intents) {
|
||||
if (intent.type == "spawn") {
|
||||
for (const playerRecord of players) {
|
||||
if (playerRecord.clientID == intent.clientID) {
|
||||
playerRecord.username = intent.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
record.players = players
|
||||
record.durationSeconds = Math.floor((record.endTimestampMS - record.startTimestampMS) / 1000)
|
||||
record.num_turns = turns.length
|
||||
record.winner = winner
|
||||
return record;
|
||||
}
|
||||
record.players = players;
|
||||
record.durationSeconds = Math.floor(
|
||||
(record.endTimestampMS - record.startTimestampMS) / 1000,
|
||||
);
|
||||
record.num_turns = turns.length;
|
||||
record.winner = winner;
|
||||
return record;
|
||||
}
|
||||
|
||||
export function assertNever(x: never): never {
|
||||
throw new Error('Unexpected value: ' + x);
|
||||
throw new Error("Unexpected value: " + x);
|
||||
}
|
||||
|
||||
export function generateID(): GameID {
|
||||
const nanoid = customAlphabet('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', 8)
|
||||
return nanoid()
|
||||
const nanoid = customAlphabet(
|
||||
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||
8,
|
||||
);
|
||||
return nanoid();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
import { Difficulty, GameType, Gold, Player, PlayerID, PlayerInfo, TerraNullius, Tick, UnitInfo, UnitType } from "../game/Game";
|
||||
import {
|
||||
Difficulty,
|
||||
GameType,
|
||||
Gold,
|
||||
Player,
|
||||
PlayerID,
|
||||
PlayerInfo,
|
||||
TerraNullius,
|
||||
Tick,
|
||||
UnitInfo,
|
||||
UnitType,
|
||||
} from "../game/Game";
|
||||
import { Colord, colord } from "colord";
|
||||
import { preprodConfig } from "./PreprodConfig";
|
||||
import { prodConfig } from "./ProdConfig";
|
||||
@@ -10,101 +21,111 @@ import { GameMap, TileRef } from "../game/GameMap";
|
||||
import { PlayerView } from "../game/GameView";
|
||||
|
||||
export enum GameEnv {
|
||||
Dev,
|
||||
Prod
|
||||
Dev,
|
||||
Prod,
|
||||
}
|
||||
export function getConfig(gameConfig: GameConfig): Config {
|
||||
const sc = getServerConfig()
|
||||
switch (process.env.GAME_ENV) {
|
||||
case 'dev':
|
||||
return new DevConfig(sc, gameConfig)
|
||||
case 'preprod':
|
||||
case 'prod':
|
||||
consolex.log('using prod config')
|
||||
return new DefaultConfig(sc, gameConfig)
|
||||
default:
|
||||
throw Error(`unsupported server configuration: ${process.env.GAME_ENV}`)
|
||||
}
|
||||
const sc = getServerConfig();
|
||||
switch (process.env.GAME_ENV) {
|
||||
case "dev":
|
||||
return new DevConfig(sc, gameConfig);
|
||||
case "preprod":
|
||||
case "prod":
|
||||
consolex.log("using prod config");
|
||||
return new DefaultConfig(sc, gameConfig);
|
||||
default:
|
||||
throw Error(`unsupported server configuration: ${process.env.GAME_ENV}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function getServerConfig(): ServerConfig {
|
||||
switch (process.env.GAME_ENV) {
|
||||
case 'dev':
|
||||
consolex.log('using dev config')
|
||||
return new DevServerConfig()
|
||||
case 'preprod':
|
||||
consolex.log('using preprod config')
|
||||
return preprodConfig
|
||||
case 'prod':
|
||||
default:
|
||||
consolex.log('using prod config')
|
||||
return prodConfig
|
||||
// default:
|
||||
// throw Error(`unsupported server configuration: ${process.env.GAME_ENV}`)
|
||||
}
|
||||
switch (process.env.GAME_ENV) {
|
||||
case "dev":
|
||||
consolex.log("using dev config");
|
||||
return new DevServerConfig();
|
||||
case "preprod":
|
||||
consolex.log("using preprod config");
|
||||
return preprodConfig;
|
||||
case "prod":
|
||||
default:
|
||||
consolex.log("using prod config");
|
||||
return prodConfig;
|
||||
// default:
|
||||
// throw Error(`unsupported server configuration: ${process.env.GAME_ENV}`)
|
||||
}
|
||||
}
|
||||
|
||||
export interface ServerConfig {
|
||||
turnIntervalMs(): number
|
||||
gameCreationRate(): number
|
||||
lobbyLifetime(): number
|
||||
turnIntervalMs(): number;
|
||||
gameCreationRate(): number;
|
||||
lobbyLifetime(): number;
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
serverConfig(): ServerConfig
|
||||
gameConfig(): GameConfig
|
||||
theme(): Theme;
|
||||
percentageTilesOwnedToWin(): number
|
||||
numBots(): number
|
||||
spawnNPCs(): boolean
|
||||
numSpawnPhaseTurns(): number
|
||||
serverConfig(): ServerConfig;
|
||||
gameConfig(): GameConfig;
|
||||
theme(): Theme;
|
||||
percentageTilesOwnedToWin(): number;
|
||||
numBots(): number;
|
||||
spawnNPCs(): boolean;
|
||||
numSpawnPhaseTurns(): number;
|
||||
|
||||
startManpower(playerInfo: PlayerInfo): number
|
||||
populationIncreaseRate(player: Player | PlayerView): number
|
||||
goldAdditionRate(player: Player | PlayerView): number
|
||||
troopAdjustmentRate(player: Player): number
|
||||
attackTilesPerTick(attckTroops: number, attacker: Player, defender: Player | TerraNullius, numAdjacentTilesWithEnemy: number): number
|
||||
attackLogic(gm: GameMap, attackTroops: number, attacker: Player, defender: Player | TerraNullius, tileToConquer: TileRef): {
|
||||
attackerTroopLoss: number,
|
||||
defenderTroopLoss: number,
|
||||
tilesPerTickUsed: number
|
||||
}
|
||||
attackAmount(attacker: Player, defender: Player | TerraNullius): number
|
||||
maxPopulation(player: Player | PlayerView): number
|
||||
cityPopulationIncrease(): number
|
||||
boatAttackAmount(attacker: Player, defender: Player | TerraNullius): number
|
||||
boatMaxDistance(): number
|
||||
boatMaxNumber(): number
|
||||
allianceDuration(): Tick
|
||||
allianceRequestCooldown(): Tick
|
||||
targetDuration(): Tick
|
||||
targetCooldown(): Tick
|
||||
emojiMessageCooldown(): Tick
|
||||
emojiMessageDuration(): Tick
|
||||
donateCooldown(): Tick
|
||||
defaultDonationAmount(sender: Player): number
|
||||
unitInfo(type: UnitType): UnitInfo
|
||||
tradeShipGold(dist: number): Gold
|
||||
tradeShipSpawnRate(): number
|
||||
defensePostRange(): number
|
||||
defensePostDefenseBonus(): number
|
||||
falloutDefenseModifier(): number
|
||||
difficultyModifier(difficulty: Difficulty): number
|
||||
startManpower(playerInfo: PlayerInfo): number;
|
||||
populationIncreaseRate(player: Player | PlayerView): number;
|
||||
goldAdditionRate(player: Player | PlayerView): number;
|
||||
troopAdjustmentRate(player: Player): number;
|
||||
attackTilesPerTick(
|
||||
attckTroops: number,
|
||||
attacker: Player,
|
||||
defender: Player | TerraNullius,
|
||||
numAdjacentTilesWithEnemy: number,
|
||||
): number;
|
||||
attackLogic(
|
||||
gm: GameMap,
|
||||
attackTroops: number,
|
||||
attacker: Player,
|
||||
defender: Player | TerraNullius,
|
||||
tileToConquer: TileRef,
|
||||
): {
|
||||
attackerTroopLoss: number;
|
||||
defenderTroopLoss: number;
|
||||
tilesPerTickUsed: number;
|
||||
};
|
||||
attackAmount(attacker: Player, defender: Player | TerraNullius): number;
|
||||
maxPopulation(player: Player | PlayerView): number;
|
||||
cityPopulationIncrease(): number;
|
||||
boatAttackAmount(attacker: Player, defender: Player | TerraNullius): number;
|
||||
boatMaxDistance(): number;
|
||||
boatMaxNumber(): number;
|
||||
allianceDuration(): Tick;
|
||||
allianceRequestCooldown(): Tick;
|
||||
targetDuration(): Tick;
|
||||
targetCooldown(): Tick;
|
||||
emojiMessageCooldown(): Tick;
|
||||
emojiMessageDuration(): Tick;
|
||||
donateCooldown(): Tick;
|
||||
defaultDonationAmount(sender: Player): number;
|
||||
unitInfo(type: UnitType): UnitInfo;
|
||||
tradeShipGold(dist: number): Gold;
|
||||
tradeShipSpawnRate(): number;
|
||||
defensePostRange(): number;
|
||||
defensePostDefenseBonus(): number;
|
||||
falloutDefenseModifier(): number;
|
||||
difficultyModifier(difficulty: Difficulty): number;
|
||||
}
|
||||
|
||||
export interface Theme {
|
||||
playerInfoColor(id: PlayerID): Colord;
|
||||
territoryColor(playerInfo: PlayerInfo): Colord;
|
||||
borderColor(playerInfo: PlayerInfo): Colord;
|
||||
defendedBorderColor(playerInfo: PlayerInfo): Colord;
|
||||
terrainColor(gm: GameMap, tile: TileRef): Colord;
|
||||
backgroundColor(): Colord;
|
||||
falloutColor(): Colord
|
||||
font(): string;
|
||||
// unit color for alternate view
|
||||
selfColor(): Colord
|
||||
allyColor(): Colord
|
||||
enemyColor(): Colord
|
||||
spawnHighlightColor(): Colord
|
||||
playerInfoColor(id: PlayerID): Colord;
|
||||
territoryColor(playerInfo: PlayerInfo): Colord;
|
||||
borderColor(playerInfo: PlayerInfo): Colord;
|
||||
defendedBorderColor(playerInfo: PlayerInfo): Colord;
|
||||
terrainColor(gm: GameMap, tile: TileRef): Colord;
|
||||
backgroundColor(): Colord;
|
||||
falloutColor(): Colord;
|
||||
font(): string;
|
||||
// unit color for alternate view
|
||||
selfColor(): Colord;
|
||||
allyColor(): Colord;
|
||||
enemyColor(): Colord;
|
||||
spawnHighlightColor(): Colord;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,16 @@
|
||||
import { Difficulty, GameType, Gold, Player, PlayerInfo, PlayerType, TerrainType, TerraNullius, Tick, UnitInfo, UnitType } from "../game/Game";
|
||||
import {
|
||||
Difficulty,
|
||||
GameType,
|
||||
Gold,
|
||||
Player,
|
||||
PlayerInfo,
|
||||
PlayerType,
|
||||
TerrainType,
|
||||
TerraNullius,
|
||||
Tick,
|
||||
UnitInfo,
|
||||
UnitType,
|
||||
} from "../game/Game";
|
||||
import { GameMap, TileRef } from "../game/GameMap";
|
||||
import { PlayerView } from "../game/GameView";
|
||||
import { GameConfig } from "../Schemas";
|
||||
@@ -7,338 +19,376 @@ import { Config, ServerConfig, Theme } from "./Config";
|
||||
import { pastelTheme } from "./PastelTheme";
|
||||
|
||||
export abstract class DefaultServerConfig implements ServerConfig {
|
||||
turnIntervalMs(): number {
|
||||
return 100
|
||||
}
|
||||
gameCreationRate(): number {
|
||||
return 1 * 60 * 1000
|
||||
}
|
||||
lobbyLifetime(): number {
|
||||
return 2 * 60 * 1000
|
||||
}
|
||||
turnIntervalMs(): number {
|
||||
return 100;
|
||||
}
|
||||
gameCreationRate(): number {
|
||||
return 1 * 60 * 1000;
|
||||
}
|
||||
lobbyLifetime(): number {
|
||||
return 2 * 60 * 1000;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class DefaultConfig implements Config {
|
||||
constructor(
|
||||
private _serverConfig: ServerConfig,
|
||||
private _gameConfig: GameConfig,
|
||||
) {}
|
||||
|
||||
constructor(private _serverConfig: ServerConfig, private _gameConfig: GameConfig) {
|
||||
gameConfig(): GameConfig {
|
||||
return this._gameConfig;
|
||||
}
|
||||
|
||||
serverConfig(): ServerConfig {
|
||||
return this._serverConfig;
|
||||
}
|
||||
|
||||
difficultyModifier(difficulty: Difficulty): number {
|
||||
switch (difficulty) {
|
||||
case Difficulty.Easy:
|
||||
return 1;
|
||||
case Difficulty.Medium:
|
||||
return 3;
|
||||
case Difficulty.Hard:
|
||||
return 9;
|
||||
case Difficulty.Impossible:
|
||||
return 18;
|
||||
}
|
||||
}
|
||||
|
||||
cityPopulationIncrease(): number {
|
||||
return 250_000;
|
||||
}
|
||||
|
||||
falloutDefenseModifier(): number {
|
||||
return 5;
|
||||
}
|
||||
|
||||
defensePostRange(): number {
|
||||
return 40;
|
||||
}
|
||||
defensePostDefenseBonus(): number {
|
||||
return 5;
|
||||
}
|
||||
spawnNPCs(): boolean {
|
||||
return true;
|
||||
}
|
||||
tradeShipGold(dist: number): Gold {
|
||||
return 10000 + 100 * Math.pow(dist, 1.1);
|
||||
}
|
||||
tradeShipSpawnRate(): number {
|
||||
return 500;
|
||||
}
|
||||
|
||||
unitInfo(type: UnitType): UnitInfo {
|
||||
switch (type) {
|
||||
case UnitType.TransportShip:
|
||||
return {
|
||||
cost: () => 0,
|
||||
territoryBound: false,
|
||||
};
|
||||
case UnitType.Destroyer:
|
||||
return {
|
||||
cost: (p: Player) =>
|
||||
(p.units(UnitType.Destroyer).length + 1) * 250_000,
|
||||
territoryBound: false,
|
||||
maxHealth: 1000,
|
||||
};
|
||||
case UnitType.Battleship:
|
||||
return {
|
||||
cost: (p: Player) =>
|
||||
(p.units(UnitType.Battleship).length + 1) * 500_000,
|
||||
territoryBound: false,
|
||||
maxHealth: 5000,
|
||||
};
|
||||
case UnitType.Shell:
|
||||
return {
|
||||
cost: () => 0,
|
||||
territoryBound: false,
|
||||
damage: 250,
|
||||
};
|
||||
case UnitType.Port:
|
||||
return {
|
||||
cost: (p: Player) =>
|
||||
Math.min(
|
||||
1_000_000,
|
||||
Math.pow(2, p.units(UnitType.Port).length) * 250_000,
|
||||
),
|
||||
territoryBound: true,
|
||||
};
|
||||
case UnitType.AtomBomb:
|
||||
return {
|
||||
cost: () => 750_000,
|
||||
territoryBound: false,
|
||||
};
|
||||
case UnitType.HydrogenBomb:
|
||||
return {
|
||||
cost: () => 5_000_000,
|
||||
territoryBound: false,
|
||||
};
|
||||
case UnitType.TradeShip:
|
||||
return {
|
||||
cost: () => 0,
|
||||
territoryBound: false,
|
||||
};
|
||||
case UnitType.MissileSilo:
|
||||
return {
|
||||
cost: () => 1_000_000,
|
||||
territoryBound: true,
|
||||
};
|
||||
case UnitType.DefensePost:
|
||||
return {
|
||||
cost: (p: Player) =>
|
||||
Math.min(
|
||||
250_000,
|
||||
(p.units(UnitType.DefensePost).length + 1) * 50_000,
|
||||
),
|
||||
territoryBound: true,
|
||||
};
|
||||
case UnitType.City:
|
||||
return {
|
||||
cost: (p: Player) =>
|
||||
Math.min(
|
||||
1_000_000,
|
||||
Math.pow(2, p.units(UnitType.City).length) * 125_000,
|
||||
),
|
||||
territoryBound: true,
|
||||
};
|
||||
default:
|
||||
assertNever(type);
|
||||
}
|
||||
}
|
||||
defaultDonationAmount(sender: Player): number {
|
||||
return Math.floor(sender.troops() / 3);
|
||||
}
|
||||
donateCooldown(): Tick {
|
||||
return 10 * 10;
|
||||
}
|
||||
emojiMessageDuration(): Tick {
|
||||
return 5 * 10;
|
||||
}
|
||||
emojiMessageCooldown(): Tick {
|
||||
return 15 * 10;
|
||||
}
|
||||
targetDuration(): Tick {
|
||||
return 10 * 10;
|
||||
}
|
||||
targetCooldown(): Tick {
|
||||
return 15 * 10;
|
||||
}
|
||||
allianceRequestCooldown(): Tick {
|
||||
return 30 * 10;
|
||||
}
|
||||
allianceDuration(): Tick {
|
||||
return 600 * 10;
|
||||
}
|
||||
percentageTilesOwnedToWin(): number {
|
||||
return 80;
|
||||
}
|
||||
boatMaxNumber(): number {
|
||||
return 3;
|
||||
}
|
||||
boatMaxDistance(): number {
|
||||
return 500;
|
||||
}
|
||||
numSpawnPhaseTurns(): number {
|
||||
return this._gameConfig.gameType == GameType.Singleplayer ? 100 : 300;
|
||||
}
|
||||
numBots(): number {
|
||||
return 400;
|
||||
}
|
||||
theme(): Theme {
|
||||
return pastelTheme;
|
||||
}
|
||||
|
||||
attackLogic(
|
||||
gm: GameMap,
|
||||
attackTroops: number,
|
||||
attacker: Player,
|
||||
defender: Player | TerraNullius,
|
||||
tileToConquer: TileRef,
|
||||
): {
|
||||
attackerTroopLoss: number;
|
||||
defenderTroopLoss: number;
|
||||
tilesPerTickUsed: number;
|
||||
} {
|
||||
let mag = 0;
|
||||
let speed = 0;
|
||||
const type = gm.terrainType(tileToConquer);
|
||||
switch (type) {
|
||||
case TerrainType.Plains:
|
||||
mag = 80;
|
||||
speed = 15;
|
||||
break;
|
||||
case TerrainType.Highland:
|
||||
mag = 100;
|
||||
speed = 20;
|
||||
break;
|
||||
case TerrainType.Mountain:
|
||||
mag = 120;
|
||||
speed = 25;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`terrain type ${type} not supported`);
|
||||
}
|
||||
// TODO
|
||||
// mag *= tileToConquer.defenseBonus(attacker)
|
||||
// speed *= tileToConquer.defenseBonus(attacker)
|
||||
if (gm.hasFallout(tileToConquer)) {
|
||||
mag *= this.falloutDefenseModifier();
|
||||
speed *= this.falloutDefenseModifier();
|
||||
}
|
||||
|
||||
gameConfig(): GameConfig {
|
||||
return this._gameConfig
|
||||
if (attacker.isPlayer() && defender.isPlayer()) {
|
||||
if (
|
||||
attacker.type() == PlayerType.Human &&
|
||||
defender.type() == PlayerType.Bot
|
||||
) {
|
||||
mag *= 0.8;
|
||||
}
|
||||
if (
|
||||
attacker.type() == PlayerType.FakeHuman &&
|
||||
defender.type() == PlayerType.Bot
|
||||
) {
|
||||
mag *= 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
serverConfig(): ServerConfig {
|
||||
return this._serverConfig
|
||||
if (defender.isPlayer()) {
|
||||
return {
|
||||
attackerTroopLoss:
|
||||
within(defender.troops() / (2.5 * attackTroops), 0.1, 10) * mag,
|
||||
defenderTroopLoss: defender.troops() / defender.numTilesOwned(),
|
||||
tilesPerTickUsed:
|
||||
within(defender.troops() / (5 * attackTroops), 0.2, 1.5) * speed,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
attackerTroopLoss:
|
||||
attacker.type() == PlayerType.Bot ? mag / 10 : mag / 5,
|
||||
defenderTroopLoss: 0,
|
||||
tilesPerTickUsed: within(
|
||||
(2000 * Math.max(10, speed)) / attackTroops,
|
||||
5,
|
||||
100,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
attackTilesPerTick(
|
||||
attackTroops: number,
|
||||
attacker: Player,
|
||||
defender: Player | TerraNullius,
|
||||
numAdjacentTilesWithEnemy: number,
|
||||
): number {
|
||||
if (defender.isPlayer()) {
|
||||
return (
|
||||
within(((5 * attackTroops) / defender.troops()) * 2, 0.01, 0.5) *
|
||||
numAdjacentTilesWithEnemy *
|
||||
3
|
||||
);
|
||||
} else {
|
||||
return numAdjacentTilesWithEnemy * 2;
|
||||
}
|
||||
}
|
||||
|
||||
boatAttackAmount(attacker: Player, defender: Player | TerraNullius): number {
|
||||
return Math.floor(attacker.troops() / 5);
|
||||
}
|
||||
|
||||
attackAmount(attacker: Player, defender: Player | TerraNullius) {
|
||||
if (attacker.type() == PlayerType.Bot) {
|
||||
return attacker.troops() / 20;
|
||||
} else {
|
||||
return attacker.troops() / 5;
|
||||
}
|
||||
}
|
||||
|
||||
startManpower(playerInfo: PlayerInfo): number {
|
||||
if (playerInfo.playerType == PlayerType.Bot) {
|
||||
return 10000;
|
||||
}
|
||||
if (playerInfo.playerType == PlayerType.FakeHuman) {
|
||||
// start troops * strength * difficulty
|
||||
switch (this._gameConfig.difficulty) {
|
||||
case Difficulty.Easy:
|
||||
case Difficulty.Medium:
|
||||
return 1000;
|
||||
case Difficulty.Hard:
|
||||
case Difficulty.Impossible:
|
||||
return 2000;
|
||||
}
|
||||
}
|
||||
return 25000;
|
||||
}
|
||||
|
||||
maxPopulation(player: Player | PlayerView): number {
|
||||
let maxPop = Math.pow(player.numTilesOwned(), 0.6) * 1000 + 50000;
|
||||
if (player.type() == PlayerType.Bot) {
|
||||
return maxPop;
|
||||
}
|
||||
return (
|
||||
maxPop * 2 +
|
||||
player.units(UnitType.City).length * this.cityPopulationIncrease()
|
||||
);
|
||||
}
|
||||
|
||||
populationIncreaseRate(player: Player): number {
|
||||
let max = this.maxPopulation(player);
|
||||
|
||||
// const thing = Math.sqrt(player.population() + player.population() * player.workers())
|
||||
|
||||
let toAdd = 10 + Math.pow(player.population(), 0.73) / 4;
|
||||
|
||||
const ratio = 1 - player.population() / max;
|
||||
toAdd *= ratio;
|
||||
|
||||
if (player.type() == PlayerType.FakeHuman) {
|
||||
toAdd *= 1.0;
|
||||
}
|
||||
if (player.type() == PlayerType.Bot) {
|
||||
toAdd *= 0.7;
|
||||
}
|
||||
let difficultyMultiplier = 1;
|
||||
switch (this._gameConfig.difficulty) {
|
||||
case Difficulty.Easy:
|
||||
difficultyMultiplier = 1;
|
||||
break;
|
||||
case Difficulty.Medium:
|
||||
difficultyMultiplier = 1.2;
|
||||
break;
|
||||
case Difficulty.Hard:
|
||||
difficultyMultiplier = 1.5;
|
||||
break;
|
||||
case Difficulty.Impossible:
|
||||
difficultyMultiplier = 1.7;
|
||||
break;
|
||||
}
|
||||
if (player.type() == PlayerType.FakeHuman) {
|
||||
toAdd *= difficultyMultiplier;
|
||||
}
|
||||
|
||||
difficultyModifier(difficulty: Difficulty): number {
|
||||
switch (difficulty) {
|
||||
case Difficulty.Easy:
|
||||
return 1
|
||||
case Difficulty.Medium:
|
||||
return 3
|
||||
case Difficulty.Hard:
|
||||
return 9
|
||||
case Difficulty.Impossible:
|
||||
return 18
|
||||
}
|
||||
}
|
||||
return Math.min(player.population() + toAdd, max) - player.population();
|
||||
}
|
||||
|
||||
goldAdditionRate(player: Player): number {
|
||||
return Math.sqrt(player.workers() * player.numTilesOwned()) / 200;
|
||||
}
|
||||
|
||||
cityPopulationIncrease(): number {
|
||||
return 250_000
|
||||
troopAdjustmentRate(player: Player): number {
|
||||
const maxDiff = this.maxPopulation(player) / 1000;
|
||||
const target = player.population() * player.targetTroopRatio();
|
||||
const diff = target - player.troops();
|
||||
if (Math.abs(diff) < maxDiff) {
|
||||
return diff;
|
||||
}
|
||||
|
||||
falloutDefenseModifier(): number {
|
||||
return 5
|
||||
}
|
||||
|
||||
defensePostRange(): number {
|
||||
return 40
|
||||
}
|
||||
defensePostDefenseBonus(): number {
|
||||
return 5
|
||||
}
|
||||
spawnNPCs(): boolean {
|
||||
return true
|
||||
}
|
||||
tradeShipGold(dist: number): Gold {
|
||||
return 10000 + 100 * Math.pow(dist, 1.1)
|
||||
}
|
||||
tradeShipSpawnRate(): number {
|
||||
return 500
|
||||
}
|
||||
|
||||
unitInfo(type: UnitType): UnitInfo {
|
||||
switch (type) {
|
||||
case UnitType.TransportShip:
|
||||
return {
|
||||
cost: () => 0,
|
||||
territoryBound: false,
|
||||
}
|
||||
case UnitType.Destroyer:
|
||||
return {
|
||||
cost: (p: Player) => (p.units(UnitType.Destroyer).length + 1) * 250_000,
|
||||
territoryBound: false,
|
||||
maxHealth: 1000,
|
||||
}
|
||||
case UnitType.Battleship:
|
||||
return {
|
||||
cost: (p: Player) => (p.units(UnitType.Battleship).length + 1) * 500_000,
|
||||
territoryBound: false,
|
||||
maxHealth: 5000
|
||||
}
|
||||
case UnitType.Shell:
|
||||
return {
|
||||
cost: () => 0,
|
||||
territoryBound: false,
|
||||
damage: 250
|
||||
}
|
||||
case UnitType.Port:
|
||||
return {
|
||||
cost: (p: Player) =>
|
||||
Math.min(
|
||||
1_000_000,
|
||||
Math.pow(2, p.units(UnitType.Port).length) * 250_000
|
||||
),
|
||||
territoryBound: true
|
||||
}
|
||||
case UnitType.AtomBomb:
|
||||
return {
|
||||
cost: () => 750_000,
|
||||
territoryBound: false
|
||||
}
|
||||
case UnitType.HydrogenBomb:
|
||||
return {
|
||||
cost: () => 5_000_000,
|
||||
territoryBound: false
|
||||
}
|
||||
case UnitType.TradeShip:
|
||||
return {
|
||||
cost: () => 0,
|
||||
territoryBound: false
|
||||
}
|
||||
case UnitType.MissileSilo:
|
||||
return {
|
||||
cost: () => 1_000_000,
|
||||
territoryBound: true
|
||||
}
|
||||
case UnitType.DefensePost:
|
||||
return {
|
||||
cost: (p: Player) =>
|
||||
Math.min(
|
||||
250_000,
|
||||
(p.units(UnitType.DefensePost).length + 1) * 50_000
|
||||
),
|
||||
territoryBound: true
|
||||
}
|
||||
case UnitType.City:
|
||||
return {
|
||||
cost: (p: Player) => Math.min(
|
||||
1_000_000,
|
||||
Math.pow(2, p.units(UnitType.City).length) * 125_000,
|
||||
),
|
||||
territoryBound: true
|
||||
}
|
||||
default:
|
||||
assertNever(type)
|
||||
}
|
||||
}
|
||||
defaultDonationAmount(sender: Player): number {
|
||||
return Math.floor(sender.troops() / 3)
|
||||
}
|
||||
donateCooldown(): Tick {
|
||||
return 10 * 10
|
||||
}
|
||||
emojiMessageDuration(): Tick {
|
||||
return 5 * 10
|
||||
}
|
||||
emojiMessageCooldown(): Tick {
|
||||
return 15 * 10
|
||||
}
|
||||
targetDuration(): Tick {
|
||||
return 10 * 10
|
||||
}
|
||||
targetCooldown(): Tick {
|
||||
return 15 * 10
|
||||
}
|
||||
allianceRequestCooldown(): Tick {
|
||||
return 30 * 10
|
||||
}
|
||||
allianceDuration(): Tick {
|
||||
return 600 * 10
|
||||
}
|
||||
percentageTilesOwnedToWin(): number {
|
||||
return 80
|
||||
}
|
||||
boatMaxNumber(): number {
|
||||
return 3
|
||||
}
|
||||
boatMaxDistance(): number {
|
||||
return 500
|
||||
}
|
||||
numSpawnPhaseTurns(): number {
|
||||
return this._gameConfig.gameType == GameType.Singleplayer ? 100 : 300
|
||||
}
|
||||
numBots(): number {
|
||||
return 400
|
||||
}
|
||||
theme(): Theme { return pastelTheme; }
|
||||
|
||||
attackLogic(gm: GameMap, attackTroops: number, attacker: Player, defender: Player | TerraNullius, tileToConquer: TileRef): { attackerTroopLoss: number; defenderTroopLoss: number; tilesPerTickUsed: number } {
|
||||
let mag = 0
|
||||
let speed = 0
|
||||
const type = gm.terrainType(tileToConquer)
|
||||
switch (type) {
|
||||
case TerrainType.Plains:
|
||||
mag = 80
|
||||
speed = 15
|
||||
break
|
||||
case TerrainType.Highland:
|
||||
mag = 100
|
||||
speed = 20
|
||||
break
|
||||
case TerrainType.Mountain:
|
||||
mag = 120
|
||||
speed = 25
|
||||
break
|
||||
default:
|
||||
throw new Error(`terrain type ${type} not supported`)
|
||||
}
|
||||
// TODO
|
||||
// mag *= tileToConquer.defenseBonus(attacker)
|
||||
// speed *= tileToConquer.defenseBonus(attacker)
|
||||
if (gm.hasFallout(tileToConquer)) {
|
||||
mag *= this.falloutDefenseModifier()
|
||||
speed *= this.falloutDefenseModifier()
|
||||
}
|
||||
|
||||
if (attacker.isPlayer() && defender.isPlayer()) {
|
||||
if (attacker.type() == PlayerType.Human && defender.type() == PlayerType.Bot) {
|
||||
mag *= .8
|
||||
}
|
||||
if (attacker.type() == PlayerType.FakeHuman && defender.type() == PlayerType.Bot) {
|
||||
mag *= .8
|
||||
}
|
||||
}
|
||||
|
||||
if (defender.isPlayer()) {
|
||||
return {
|
||||
attackerTroopLoss: within(defender.troops() / (2.5 * attackTroops), .1, 10) * mag,
|
||||
defenderTroopLoss: defender.troops() / defender.numTilesOwned(),
|
||||
tilesPerTickUsed: within(defender.troops() / (5 * attackTroops), .2, 1.5) * speed
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
attackerTroopLoss: attacker.type() == PlayerType.Bot ? mag / 10 : mag / 5,
|
||||
defenderTroopLoss: 0,
|
||||
tilesPerTickUsed: within(2000 * Math.max(10, speed) / attackTroops, 5, 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
attackTilesPerTick(attackTroops: number, attacker: Player, defender: Player | TerraNullius, numAdjacentTilesWithEnemy: number): number {
|
||||
if (defender.isPlayer()) {
|
||||
return within((5 * attackTroops) / defender.troops() * 2, .01, .5) * numAdjacentTilesWithEnemy * 3
|
||||
} else {
|
||||
return numAdjacentTilesWithEnemy * 2
|
||||
}
|
||||
}
|
||||
|
||||
boatAttackAmount(attacker: Player, defender: Player | TerraNullius): number {
|
||||
return Math.floor(attacker.troops() / 5)
|
||||
}
|
||||
|
||||
attackAmount(attacker: Player, defender: Player | TerraNullius) {
|
||||
if (attacker.type() == PlayerType.Bot) {
|
||||
return attacker.troops() / 20
|
||||
} else {
|
||||
return attacker.troops() / 5
|
||||
}
|
||||
}
|
||||
|
||||
startManpower(playerInfo: PlayerInfo): number {
|
||||
if (playerInfo.playerType == PlayerType.Bot) {
|
||||
return 10000
|
||||
}
|
||||
if (playerInfo.playerType == PlayerType.FakeHuman) {
|
||||
// start troops * strength * difficulty
|
||||
switch (this._gameConfig.difficulty) {
|
||||
case Difficulty.Easy:
|
||||
case Difficulty.Medium:
|
||||
return 1000
|
||||
case Difficulty.Hard:
|
||||
case Difficulty.Impossible:
|
||||
return 2000
|
||||
}
|
||||
}
|
||||
return 25000
|
||||
}
|
||||
|
||||
maxPopulation(player: Player | PlayerView): number {
|
||||
let maxPop = Math.pow(player.numTilesOwned(), .6) * 1000 + 50000
|
||||
if (player.type() == PlayerType.Bot) {
|
||||
return maxPop
|
||||
}
|
||||
return maxPop * 2 + player.units(UnitType.City).length * this.cityPopulationIncrease()
|
||||
}
|
||||
|
||||
populationIncreaseRate(player: Player): number {
|
||||
let max = this.maxPopulation(player)
|
||||
|
||||
// const thing = Math.sqrt(player.population() + player.population() * player.workers())
|
||||
|
||||
let toAdd = 10 + Math.pow(player.population(), .73) / 4
|
||||
|
||||
const ratio = 1 - (player.population() / max)
|
||||
toAdd *= ratio
|
||||
|
||||
if (player.type() == PlayerType.FakeHuman) {
|
||||
toAdd *= 1.0
|
||||
}
|
||||
if (player.type() == PlayerType.Bot) {
|
||||
toAdd *= .7
|
||||
}
|
||||
let difficultyMultiplier = 1
|
||||
switch (this._gameConfig.difficulty) {
|
||||
case Difficulty.Easy:
|
||||
difficultyMultiplier = 1
|
||||
break
|
||||
case Difficulty.Medium:
|
||||
difficultyMultiplier = 1.2
|
||||
break
|
||||
case Difficulty.Hard:
|
||||
difficultyMultiplier = 1.5
|
||||
break
|
||||
case Difficulty.Impossible:
|
||||
difficultyMultiplier = 1.7
|
||||
break
|
||||
}
|
||||
if (player.type() == PlayerType.FakeHuman) {
|
||||
toAdd *= difficultyMultiplier
|
||||
}
|
||||
|
||||
return Math.min(player.population() + toAdd, max) - player.population()
|
||||
}
|
||||
|
||||
goldAdditionRate(player: Player): number {
|
||||
return Math.sqrt(player.workers() * player.numTilesOwned()) / 200
|
||||
}
|
||||
|
||||
troopAdjustmentRate(player: Player): number {
|
||||
const maxDiff = this.maxPopulation(player) / 1000
|
||||
const target = player.population() * player.targetTroopRatio()
|
||||
const diff = target - player.troops()
|
||||
if (Math.abs(diff) < maxDiff) {
|
||||
return diff
|
||||
}
|
||||
const adjustment = maxDiff * Math.sign(diff)
|
||||
// Can ramp down troops much faster
|
||||
if (adjustment < 0) {
|
||||
return adjustment * 5
|
||||
}
|
||||
return adjustment
|
||||
const adjustment = maxDiff * Math.sign(diff);
|
||||
// Can ramp down troops much faster
|
||||
if (adjustment < 0) {
|
||||
return adjustment * 5;
|
||||
}
|
||||
return adjustment;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,219 +6,219 @@ import { PseudoRandom } from "../PseudoRandom";
|
||||
import { simpleHash } from "../Util";
|
||||
import { GameMap, TileRef } from "../game/GameMap";
|
||||
|
||||
export const pastelTheme = new class implements Theme {
|
||||
export const pastelTheme = new (class implements Theme {
|
||||
private rand = new PseudoRandom(123);
|
||||
|
||||
private rand = new PseudoRandom(123)
|
||||
private background = colord({ r: 60, g: 60, b: 60 });
|
||||
private land = colord({ r: 194, g: 193, b: 148 });
|
||||
private shore = colord({ r: 204, g: 203, b: 158 });
|
||||
private falloutColors = [
|
||||
colord({ r: 120, g: 255, b: 71 }), // Original color
|
||||
colord({ r: 130, g: 255, b: 85 }), // Slightly lighter
|
||||
colord({ r: 110, g: 245, b: 65 }), // Slightly darker
|
||||
colord({ r: 125, g: 255, b: 75 }), // Warmer tint
|
||||
colord({ r: 115, g: 250, b: 68 }), // Cooler tint
|
||||
];
|
||||
private water = colord({ r: 75, g: 142, b: 190 });
|
||||
private shorelineWater = colord({ r: 100, g: 143, b: 255 });
|
||||
|
||||
private background = colord({ r: 60, g: 60, b: 60 });
|
||||
private land = colord({ r: 194, g: 193, b: 148 });
|
||||
private shore = colord({ r: 204, g: 203, b: 158 });
|
||||
private falloutColors = [
|
||||
colord({ r: 120, g: 255, b: 71 }), // Original color
|
||||
colord({ r: 130, g: 255, b: 85 }), // Slightly lighter
|
||||
colord({ r: 110, g: 245, b: 65 }), // Slightly darker
|
||||
colord({ r: 125, g: 255, b: 75 }), // Warmer tint
|
||||
colord({ r: 115, g: 250, b: 68 }) // Cooler tint
|
||||
private territoryColors: Colord[] = [
|
||||
colord({ r: 230, g: 100, b: 100 }), // Bright Red
|
||||
colord({ r: 100, g: 180, b: 230 }), // Sky Blue
|
||||
colord({ r: 230, g: 180, b: 80 }), // Golden Yellow
|
||||
colord({ r: 180, g: 100, b: 230 }), // Purple
|
||||
colord({ r: 80, g: 200, b: 120 }), // Emerald Green
|
||||
colord({ r: 230, g: 130, b: 180 }), // Pink
|
||||
colord({ r: 100, g: 160, b: 80 }), // Olive Green
|
||||
colord({ r: 230, g: 150, b: 100 }), // Peach
|
||||
colord({ r: 80, g: 130, b: 190 }), // Navy Blue
|
||||
colord({ r: 210, g: 210, b: 100 }), // Lime Yellow
|
||||
colord({ r: 190, g: 100, b: 130 }), // Maroon
|
||||
colord({ r: 100, g: 210, b: 210 }), // Turquoise
|
||||
colord({ r: 210, g: 140, b: 80 }), // Light Orange
|
||||
colord({ r: 150, g: 110, b: 190 }), // Lavender
|
||||
colord({ r: 180, g: 210, b: 120 }), // Light Green
|
||||
colord({ r: 210, g: 100, b: 160 }), // Hot Pink
|
||||
colord({ r: 100, g: 140, b: 110 }), // Sea Green
|
||||
colord({ r: 230, g: 180, b: 180 }), // Light Pink
|
||||
colord({ r: 120, g: 120, b: 190 }), // Periwinkle
|
||||
colord({ r: 190, g: 170, b: 100 }), // Sand
|
||||
colord({ r: 100, g: 180, b: 160 }), // Aquamarine
|
||||
colord({ r: 210, g: 160, b: 200 }), // Orchid
|
||||
colord({ r: 170, g: 190, b: 100 }), // Yellow Green
|
||||
colord({ r: 100, g: 130, b: 150 }), // Steel Blue
|
||||
colord({ r: 230, g: 140, b: 140 }), // Salmon
|
||||
colord({ r: 140, g: 180, b: 220 }), // Light Blue
|
||||
colord({ r: 200, g: 160, b: 110 }), // Tan
|
||||
colord({ r: 180, g: 130, b: 180 }), // Plum
|
||||
colord({ r: 130, g: 200, b: 130 }), // Light Sea Green
|
||||
colord({ r: 220, g: 120, b: 120 }), // Coral
|
||||
colord({ r: 120, g: 160, b: 200 }), // Cornflower Blue
|
||||
colord({ r: 200, g: 200, b: 140 }), // Khaki
|
||||
colord({ r: 160, g: 120, b: 160 }), // Purple Gray
|
||||
colord({ r: 140, g: 180, b: 140 }), // Dark Sea Green
|
||||
colord({ r: 200, g: 130, b: 110 }), // Dark Salmon
|
||||
colord({ r: 130, g: 170, b: 190 }), // Cadet Blue
|
||||
colord({ r: 190, g: 180, b: 160 }), // Tan Gray
|
||||
colord({ r: 170, g: 140, b: 190 }), // Medium Purple
|
||||
colord({ r: 160, g: 190, b: 160 }), // Pale Green
|
||||
colord({ r: 190, g: 150, b: 130 }), // Rosy Brown
|
||||
colord({ r: 140, g: 150, b: 180 }), // Light Slate Gray
|
||||
colord({ r: 180, g: 170, b: 140 }), // Dark Khaki
|
||||
colord({ r: 150, g: 130, b: 150 }), // Thistle
|
||||
colord({ r: 170, g: 190, b: 180 }), // Pale Blue Green
|
||||
colord({ r: 190, g: 140, b: 150 }), // Puce
|
||||
colord({ r: 130, g: 180, b: 170 }), // Medium Aquamarine
|
||||
colord({ r: 180, g: 160, b: 180 }), // Mauve
|
||||
colord({ r: 160, g: 180, b: 140 }), // Dark Olive Green
|
||||
colord({ r: 170, g: 150, b: 170 }), // Dusty Rose
|
||||
colord({ r: 100, g: 180, b: 230 }), // Sky Blue
|
||||
colord({ r: 230, g: 180, b: 80 }), // Golden Yellow
|
||||
colord({ r: 180, g: 100, b: 230 }), // Purple
|
||||
colord({ r: 80, g: 200, b: 120 }), // Emerald Green
|
||||
colord({ r: 230, g: 130, b: 180 }), // Pink
|
||||
colord({ r: 100, g: 160, b: 80 }), // Olive Green
|
||||
colord({ r: 230, g: 150, b: 100 }), // Peach
|
||||
colord({ r: 80, g: 130, b: 190 }), // Navy Blue
|
||||
colord({ r: 210, g: 210, b: 100 }), // Lime Yellow
|
||||
colord({ r: 190, g: 100, b: 130 }), // Maroon
|
||||
colord({ r: 100, g: 210, b: 210 }), // Turquoise
|
||||
colord({ r: 210, g: 140, b: 80 }), // Light Orange
|
||||
colord({ r: 150, g: 110, b: 190 }), // Lavender
|
||||
colord({ r: 180, g: 210, b: 120 }), // Light Green
|
||||
colord({ r: 210, g: 100, b: 160 }), // Hot Pink
|
||||
colord({ r: 100, g: 140, b: 110 }), // Sea Green
|
||||
colord({ r: 230, g: 180, b: 180 }), // Light Pink
|
||||
colord({ r: 120, g: 120, b: 190 }), // Periwinkle
|
||||
colord({ r: 190, g: 170, b: 100 }), // Sand
|
||||
colord({ r: 100, g: 180, b: 160 }), // Aquamarine
|
||||
colord({ r: 210, g: 160, b: 200 }), // Orchid
|
||||
colord({ r: 170, g: 190, b: 100 }), // Yellow Green
|
||||
colord({ r: 100, g: 130, b: 150 }), // Steel Blue
|
||||
colord({ r: 230, g: 140, b: 140 }), // Salmon
|
||||
colord({ r: 140, g: 180, b: 220 }), // Light Blue
|
||||
colord({ r: 200, g: 160, b: 110 }), // Tan
|
||||
colord({ r: 180, g: 130, b: 180 }), // Plum
|
||||
colord({ r: 130, g: 200, b: 130 }), // Light Sea Green
|
||||
colord({ r: 220, g: 120, b: 120 }), // Coral
|
||||
colord({ r: 120, g: 160, b: 200 }), // Cornflower Blue
|
||||
colord({ r: 200, g: 200, b: 140 }), // Khaki
|
||||
colord({ r: 160, g: 120, b: 160 }), // Purple Gray
|
||||
colord({ r: 140, g: 180, b: 140 }), // Dark Sea Green
|
||||
colord({ r: 200, g: 130, b: 110 }), // Dark Salmon
|
||||
colord({ r: 130, g: 170, b: 190 }), // Cadet Blue
|
||||
colord({ r: 190, g: 180, b: 160 }), // Tan Gray
|
||||
colord({ r: 170, g: 140, b: 190 }), // Medium Purple
|
||||
colord({ r: 160, g: 190, b: 160 }), // Pale Green
|
||||
colord({ r: 190, g: 150, b: 130 }), // Rosy Brown
|
||||
colord({ r: 140, g: 150, b: 180 }), // Light Slate Gray
|
||||
colord({ r: 180, g: 170, b: 140 }), // Dark Khaki
|
||||
colord({ r: 150, g: 130, b: 150 }), // Thistle
|
||||
colord({ r: 170, g: 190, b: 180 }), // Pale Blue Green
|
||||
colord({ r: 190, g: 140, b: 150 }), // Puce
|
||||
colord({ r: 130, g: 180, b: 170 }), // Medium Aquamarine
|
||||
colord({ r: 180, g: 160, b: 180 }), // Mauve
|
||||
colord({ r: 160, g: 180, b: 140 }), // Dark Olive Green
|
||||
colord({ r: 170, g: 150, b: 170 }), // Dusty Rose
|
||||
];
|
||||
|
||||
private _selfColor = colord({ r: 0, g: 255, b: 0 });
|
||||
private _allyColor = colord({ r: 255, g: 255, b: 0 });
|
||||
private _enemyColor = colord({ r: 255, g: 0, b: 0 });
|
||||
|
||||
private _spawnHighlightColor = colord({ r: 255, g: 213, b: 79 });
|
||||
|
||||
playerInfoColor(id: PlayerID): Colord {
|
||||
return colord({ r: 50, g: 50, b: 50 });
|
||||
}
|
||||
|
||||
territoryColor(playerInfo: PlayerInfo): Colord {
|
||||
return this.territoryColors[
|
||||
simpleHash(playerInfo.name) % this.territoryColors.length
|
||||
];
|
||||
private water = colord({ r: 75, g: 142, b: 190 });
|
||||
private shorelineWater = colord({ r: 100, g: 143, b: 255 });
|
||||
}
|
||||
|
||||
private territoryColors: Colord[] = [
|
||||
colord({ r: 230, g: 100, b: 100 }), // Bright Red
|
||||
colord({ r: 100, g: 180, b: 230 }), // Sky Blue
|
||||
colord({ r: 230, g: 180, b: 80 }), // Golden Yellow
|
||||
colord({ r: 180, g: 100, b: 230 }), // Purple
|
||||
colord({ r: 80, g: 200, b: 120 }), // Emerald Green
|
||||
colord({ r: 230, g: 130, b: 180 }), // Pink
|
||||
colord({ r: 100, g: 160, b: 80 }), // Olive Green
|
||||
colord({ r: 230, g: 150, b: 100 }), // Peach
|
||||
colord({ r: 80, g: 130, b: 190 }), // Navy Blue
|
||||
colord({ r: 210, g: 210, b: 100 }), // Lime Yellow
|
||||
colord({ r: 190, g: 100, b: 130 }), // Maroon
|
||||
colord({ r: 100, g: 210, b: 210 }), // Turquoise
|
||||
colord({ r: 210, g: 140, b: 80 }), // Light Orange
|
||||
colord({ r: 150, g: 110, b: 190 }), // Lavender
|
||||
colord({ r: 180, g: 210, b: 120 }), // Light Green
|
||||
colord({ r: 210, g: 100, b: 160 }), // Hot Pink
|
||||
colord({ r: 100, g: 140, b: 110 }), // Sea Green
|
||||
colord({ r: 230, g: 180, b: 180 }), // Light Pink
|
||||
colord({ r: 120, g: 120, b: 190 }), // Periwinkle
|
||||
colord({ r: 190, g: 170, b: 100 }), // Sand
|
||||
colord({ r: 100, g: 180, b: 160 }), // Aquamarine
|
||||
colord({ r: 210, g: 160, b: 200 }), // Orchid
|
||||
colord({ r: 170, g: 190, b: 100 }), // Yellow Green
|
||||
colord({ r: 100, g: 130, b: 150 }), // Steel Blue
|
||||
colord({ r: 230, g: 140, b: 140 }), // Salmon
|
||||
colord({ r: 140, g: 180, b: 220 }), // Light Blue
|
||||
colord({ r: 200, g: 160, b: 110 }), // Tan
|
||||
colord({ r: 180, g: 130, b: 180 }), // Plum
|
||||
colord({ r: 130, g: 200, b: 130 }), // Light Sea Green
|
||||
colord({ r: 220, g: 120, b: 120 }), // Coral
|
||||
colord({ r: 120, g: 160, b: 200 }), // Cornflower Blue
|
||||
colord({ r: 200, g: 200, b: 140 }), // Khaki
|
||||
colord({ r: 160, g: 120, b: 160 }), // Purple Gray
|
||||
colord({ r: 140, g: 180, b: 140 }), // Dark Sea Green
|
||||
colord({ r: 200, g: 130, b: 110 }), // Dark Salmon
|
||||
colord({ r: 130, g: 170, b: 190 }), // Cadet Blue
|
||||
colord({ r: 190, g: 180, b: 160 }), // Tan Gray
|
||||
colord({ r: 170, g: 140, b: 190 }), // Medium Purple
|
||||
colord({ r: 160, g: 190, b: 160 }), // Pale Green
|
||||
colord({ r: 190, g: 150, b: 130 }), // Rosy Brown
|
||||
colord({ r: 140, g: 150, b: 180 }), // Light Slate Gray
|
||||
colord({ r: 180, g: 170, b: 140 }), // Dark Khaki
|
||||
colord({ r: 150, g: 130, b: 150 }), // Thistle
|
||||
colord({ r: 170, g: 190, b: 180 }), // Pale Blue Green
|
||||
colord({ r: 190, g: 140, b: 150 }), // Puce
|
||||
colord({ r: 130, g: 180, b: 170 }), // Medium Aquamarine
|
||||
colord({ r: 180, g: 160, b: 180 }), // Mauve
|
||||
colord({ r: 160, g: 180, b: 140 }), // Dark Olive Green
|
||||
colord({ r: 170, g: 150, b: 170 }), // Dusty Rose
|
||||
colord({ r: 100, g: 180, b: 230 }), // Sky Blue
|
||||
colord({ r: 230, g: 180, b: 80 }), // Golden Yellow
|
||||
colord({ r: 180, g: 100, b: 230 }), // Purple
|
||||
colord({ r: 80, g: 200, b: 120 }), // Emerald Green
|
||||
colord({ r: 230, g: 130, b: 180 }), // Pink
|
||||
colord({ r: 100, g: 160, b: 80 }), // Olive Green
|
||||
colord({ r: 230, g: 150, b: 100 }), // Peach
|
||||
colord({ r: 80, g: 130, b: 190 }), // Navy Blue
|
||||
colord({ r: 210, g: 210, b: 100 }), // Lime Yellow
|
||||
colord({ r: 190, g: 100, b: 130 }), // Maroon
|
||||
colord({ r: 100, g: 210, b: 210 }), // Turquoise
|
||||
colord({ r: 210, g: 140, b: 80 }), // Light Orange
|
||||
colord({ r: 150, g: 110, b: 190 }), // Lavender
|
||||
colord({ r: 180, g: 210, b: 120 }), // Light Green
|
||||
colord({ r: 210, g: 100, b: 160 }), // Hot Pink
|
||||
colord({ r: 100, g: 140, b: 110 }), // Sea Green
|
||||
colord({ r: 230, g: 180, b: 180 }), // Light Pink
|
||||
colord({ r: 120, g: 120, b: 190 }), // Periwinkle
|
||||
colord({ r: 190, g: 170, b: 100 }), // Sand
|
||||
colord({ r: 100, g: 180, b: 160 }), // Aquamarine
|
||||
colord({ r: 210, g: 160, b: 200 }), // Orchid
|
||||
colord({ r: 170, g: 190, b: 100 }), // Yellow Green
|
||||
colord({ r: 100, g: 130, b: 150 }), // Steel Blue
|
||||
colord({ r: 230, g: 140, b: 140 }), // Salmon
|
||||
colord({ r: 140, g: 180, b: 220 }), // Light Blue
|
||||
colord({ r: 200, g: 160, b: 110 }), // Tan
|
||||
colord({ r: 180, g: 130, b: 180 }), // Plum
|
||||
colord({ r: 130, g: 200, b: 130 }), // Light Sea Green
|
||||
colord({ r: 220, g: 120, b: 120 }), // Coral
|
||||
colord({ r: 120, g: 160, b: 200 }), // Cornflower Blue
|
||||
colord({ r: 200, g: 200, b: 140 }), // Khaki
|
||||
colord({ r: 160, g: 120, b: 160 }), // Purple Gray
|
||||
colord({ r: 140, g: 180, b: 140 }), // Dark Sea Green
|
||||
colord({ r: 200, g: 130, b: 110 }), // Dark Salmon
|
||||
colord({ r: 130, g: 170, b: 190 }), // Cadet Blue
|
||||
colord({ r: 190, g: 180, b: 160 }), // Tan Gray
|
||||
colord({ r: 170, g: 140, b: 190 }), // Medium Purple
|
||||
colord({ r: 160, g: 190, b: 160 }), // Pale Green
|
||||
colord({ r: 190, g: 150, b: 130 }), // Rosy Brown
|
||||
colord({ r: 140, g: 150, b: 180 }), // Light Slate Gray
|
||||
colord({ r: 180, g: 170, b: 140 }), // Dark Khaki
|
||||
colord({ r: 150, g: 130, b: 150 }), // Thistle
|
||||
colord({ r: 170, g: 190, b: 180 }), // Pale Blue Green
|
||||
colord({ r: 190, g: 140, b: 150 }), // Puce
|
||||
colord({ r: 130, g: 180, b: 170 }), // Medium Aquamarine
|
||||
colord({ r: 180, g: 160, b: 180 }), // Mauve
|
||||
colord({ r: 160, g: 180, b: 140 }), // Dark Olive Green
|
||||
colord({ r: 170, g: 150, b: 170 }) // Dusty Rose
|
||||
];
|
||||
borderColor(playerInfo: PlayerInfo): Colord {
|
||||
const tc = this.territoryColor(playerInfo).rgba;
|
||||
return colord({
|
||||
r: Math.max(tc.r - 40, 0),
|
||||
g: Math.max(tc.g - 40, 0),
|
||||
b: Math.max(tc.b - 40, 0),
|
||||
});
|
||||
}
|
||||
defendedBorderColor(playerInfo: PlayerInfo): Colord {
|
||||
const bc = this.borderColor(playerInfo).rgba;
|
||||
return colord({
|
||||
r: Math.max(bc.r - 40, 0),
|
||||
g: Math.max(bc.g - 40, 0),
|
||||
b: Math.max(bc.b - 40, 0),
|
||||
});
|
||||
}
|
||||
|
||||
private _selfColor = colord({ r: 0, g: 255, b: 0 })
|
||||
private _allyColor = colord({ r: 255, g: 255, b: 0 })
|
||||
private _enemyColor = colord({ r: 255, g: 0, b: 0 })
|
||||
|
||||
private _spawnHighlightColor = colord({ r: 255, g: 213, b: 79 })
|
||||
|
||||
|
||||
playerInfoColor(id: PlayerID): Colord {
|
||||
return colord({ r: 50, g: 50, b: 50 })
|
||||
terrainColor(gm: GameMap, tile: TileRef): Colord {
|
||||
let mag = gm.magnitude(tile);
|
||||
if (gm.isShore(tile)) {
|
||||
return this.shore;
|
||||
}
|
||||
|
||||
territoryColor(playerInfo: PlayerInfo): Colord {
|
||||
return this.territoryColors[simpleHash(playerInfo.name) % this.territoryColors.length]
|
||||
}
|
||||
|
||||
borderColor(playerInfo: PlayerInfo): Colord {
|
||||
const tc = this.territoryColor(playerInfo).rgba;
|
||||
return colord({
|
||||
r: Math.max(tc.r - 40, 0),
|
||||
g: Math.max(tc.g - 40, 0),
|
||||
b: Math.max(tc.b - 40, 0)
|
||||
})
|
||||
}
|
||||
defendedBorderColor(playerInfo: PlayerInfo): Colord {
|
||||
const bc = this.borderColor(playerInfo).rgba;
|
||||
return colord({
|
||||
r: Math.max(bc.r - 40, 0),
|
||||
g: Math.max(bc.g - 40, 0),
|
||||
b: Math.max(bc.b - 40, 0)
|
||||
})
|
||||
}
|
||||
|
||||
terrainColor(gm: GameMap, tile: TileRef): Colord {
|
||||
let mag = gm.magnitude(tile)
|
||||
if (gm.isShore(tile)) {
|
||||
return this.shore
|
||||
switch (gm.terrainType(tile)) {
|
||||
case TerrainType.Ocean:
|
||||
case TerrainType.Lake:
|
||||
const w = this.water.rgba;
|
||||
if (gm.isShoreline(tile) && gm.isWater(tile)) {
|
||||
return this.shorelineWater;
|
||||
}
|
||||
switch (gm.terrainType(tile)) {
|
||||
case TerrainType.Ocean:
|
||||
case TerrainType.Lake:
|
||||
const w = this.water.rgba
|
||||
if (gm.isShoreline(tile) && gm.isWater(tile)) {
|
||||
return this.shorelineWater
|
||||
}
|
||||
if (gm.magnitude(tile) < 7) {
|
||||
return colord({
|
||||
r: Math.max(w.r - 7 + mag, 0),
|
||||
g: Math.max(w.g - 7 + mag, 0),
|
||||
b: Math.max(w.b - 7 + mag, 0)
|
||||
})
|
||||
}
|
||||
return this.water
|
||||
case TerrainType.Plains:
|
||||
return colord({
|
||||
r: 190,
|
||||
g: 220 - 2 * mag,
|
||||
b: 138
|
||||
})
|
||||
case TerrainType.Highland:
|
||||
return colord({
|
||||
r: 200 + 2 * mag,
|
||||
g: 183 + 2 * mag,
|
||||
b: 138 + 2 * mag
|
||||
})
|
||||
case TerrainType.Mountain:
|
||||
return colord({
|
||||
r: 230 + mag / 2,
|
||||
g: 230 + mag / 2,
|
||||
b: 230 + mag / 2
|
||||
})
|
||||
if (gm.magnitude(tile) < 7) {
|
||||
return colord({
|
||||
r: Math.max(w.r - 7 + mag, 0),
|
||||
g: Math.max(w.g - 7 + mag, 0),
|
||||
b: Math.max(w.b - 7 + mag, 0),
|
||||
});
|
||||
}
|
||||
return this.water;
|
||||
case TerrainType.Plains:
|
||||
return colord({
|
||||
r: 190,
|
||||
g: 220 - 2 * mag,
|
||||
b: 138,
|
||||
});
|
||||
case TerrainType.Highland:
|
||||
return colord({
|
||||
r: 200 + 2 * mag,
|
||||
g: 183 + 2 * mag,
|
||||
b: 138 + 2 * mag,
|
||||
});
|
||||
case TerrainType.Mountain:
|
||||
return colord({
|
||||
r: 230 + mag / 2,
|
||||
g: 230 + mag / 2,
|
||||
b: 230 + mag / 2,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
backgroundColor(): Colord {
|
||||
return this.background;
|
||||
}
|
||||
backgroundColor(): Colord {
|
||||
return this.background;
|
||||
}
|
||||
|
||||
falloutColor(): Colord {
|
||||
return this.rand.randElement(this.falloutColors)
|
||||
}
|
||||
falloutColor(): Colord {
|
||||
return this.rand.randElement(this.falloutColors);
|
||||
}
|
||||
|
||||
font(): string {
|
||||
return "Overpass, sans-serif";
|
||||
}
|
||||
font(): string {
|
||||
return "Overpass, sans-serif";
|
||||
}
|
||||
|
||||
selfColor(): Colord {
|
||||
return this._selfColor
|
||||
}
|
||||
allyColor(): Colord {
|
||||
return this._allyColor
|
||||
}
|
||||
enemyColor(): Colord {
|
||||
return this._enemyColor
|
||||
}
|
||||
selfColor(): Colord {
|
||||
return this._selfColor;
|
||||
}
|
||||
allyColor(): Colord {
|
||||
return this._allyColor;
|
||||
}
|
||||
enemyColor(): Colord {
|
||||
return this._enemyColor;
|
||||
}
|
||||
|
||||
spawnHighlightColor(): Colord {
|
||||
return this._spawnHighlightColor
|
||||
}
|
||||
}
|
||||
spawnHighlightColor(): Colord {
|
||||
return this._spawnHighlightColor;
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { DefaultConfig, DefaultServerConfig } from "./DefaultConfig";
|
||||
|
||||
export const preprodConfig = new class extends DefaultServerConfig {
|
||||
|
||||
}
|
||||
export const preprodConfig = new (class extends DefaultServerConfig {})();
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { DefaultConfig, DefaultServerConfig } from "./DefaultConfig";
|
||||
|
||||
export const prodConfig = new class extends DefaultServerConfig {
|
||||
|
||||
}
|
||||
export const prodConfig = new (class extends DefaultServerConfig {})();
|
||||
|
||||
@@ -1,243 +1,298 @@
|
||||
import { PriorityQueue } from "@datastructures-js/priority-queue";
|
||||
import { Cell, Execution, Game, Player, PlayerID, PlayerType, TerrainType, TerraNullius } from "../game/Game";
|
||||
import {
|
||||
Cell,
|
||||
Execution,
|
||||
Game,
|
||||
Player,
|
||||
PlayerID,
|
||||
PlayerType,
|
||||
TerrainType,
|
||||
TerraNullius,
|
||||
} from "../game/Game";
|
||||
import { PseudoRandom } from "../PseudoRandom";
|
||||
import { MessageType } from '../game/Game';
|
||||
import { MessageType } from "../game/Game";
|
||||
import { renderNumber } from "../../client/Utils";
|
||||
import { TileRef } from "../game/GameMap";
|
||||
|
||||
export class AttackExecution implements Execution {
|
||||
private breakAlliance = false
|
||||
private active: boolean = true;
|
||||
private toConquer: PriorityQueue<TileContainer> = new PriorityQueue<TileContainer>((a: TileContainer, b: TileContainer) => {
|
||||
if (a.priority == b.priority) {
|
||||
if (a.tick == b.tick) {
|
||||
return 0
|
||||
// return this.random.nextInt(-1, 1)
|
||||
}
|
||||
return a.tick - b.tick
|
||||
private breakAlliance = false;
|
||||
private active: boolean = true;
|
||||
private toConquer: PriorityQueue<TileContainer> =
|
||||
new PriorityQueue<TileContainer>((a: TileContainer, b: TileContainer) => {
|
||||
if (a.priority == b.priority) {
|
||||
if (a.tick == b.tick) {
|
||||
return 0;
|
||||
// return this.random.nextInt(-1, 1)
|
||||
}
|
||||
return a.priority - b.priority
|
||||
return a.tick - b.tick;
|
||||
}
|
||||
return a.priority - b.priority;
|
||||
});
|
||||
private random = new PseudoRandom(123)
|
||||
private random = new PseudoRandom(123);
|
||||
|
||||
private _owner: Player
|
||||
private target: Player | TerraNullius
|
||||
private _owner: Player;
|
||||
private target: Player | TerraNullius;
|
||||
|
||||
private mg: Game
|
||||
private mg: Game;
|
||||
|
||||
private border = new Set<TileRef>()
|
||||
private border = new Set<TileRef>();
|
||||
|
||||
constructor(
|
||||
private troops: number | null,
|
||||
private _ownerID: PlayerID,
|
||||
private _targetID: PlayerID | null,
|
||||
private sourceTile: TileRef | null,
|
||||
private removeTroops: boolean = true,
|
||||
) { }
|
||||
constructor(
|
||||
private troops: number | null,
|
||||
private _ownerID: PlayerID,
|
||||
private _targetID: PlayerID | null,
|
||||
private sourceTile: TileRef | null,
|
||||
private removeTroops: boolean = true,
|
||||
) {}
|
||||
|
||||
public targetID(): PlayerID {
|
||||
return this._targetID
|
||||
public targetID(): PlayerID {
|
||||
return this._targetID;
|
||||
}
|
||||
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
init(mg: Game, ticks: number) {
|
||||
if (!this.active) {
|
||||
return;
|
||||
}
|
||||
this.mg = mg;
|
||||
|
||||
this._owner = mg.player(this._ownerID);
|
||||
this.target =
|
||||
this._targetID == this.mg.terraNullius().id()
|
||||
? mg.terraNullius()
|
||||
: mg.player(this._targetID);
|
||||
|
||||
if (this._owner == this.target) {
|
||||
throw new Error(`Player ${this._owner} cannot attack itself`);
|
||||
}
|
||||
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false
|
||||
if (this.troops == null) {
|
||||
this.troops = this.mg.config().attackAmount(this._owner, this.target);
|
||||
}
|
||||
this.troops = Math.min(this._owner.troops(), this.troops);
|
||||
if (this.removeTroops) {
|
||||
this._owner.removeTroops(this.troops);
|
||||
}
|
||||
|
||||
init(mg: Game, ticks: number) {
|
||||
if (!this.active) {
|
||||
return
|
||||
for (const exec of mg.executions()) {
|
||||
if (exec.isActive() && exec instanceof AttackExecution && exec != this) {
|
||||
const otherAttack = exec as AttackExecution;
|
||||
// Target has opposing attack, cancel them out
|
||||
if (
|
||||
this.target.isPlayer() &&
|
||||
otherAttack._targetID == this._ownerID &&
|
||||
this._targetID == otherAttack._ownerID
|
||||
) {
|
||||
if (otherAttack.troops > this.troops) {
|
||||
otherAttack.troops -= this.troops;
|
||||
// otherAttack.calculateToConquer()
|
||||
this.active = false;
|
||||
return;
|
||||
} else {
|
||||
this.troops -= otherAttack.troops;
|
||||
otherAttack.active = false;
|
||||
}
|
||||
}
|
||||
this.mg = mg
|
||||
|
||||
this._owner = mg.player(this._ownerID)
|
||||
this.target = this._targetID == this.mg.terraNullius().id() ? mg.terraNullius() : mg.player(this._targetID)
|
||||
|
||||
if (this._owner == this.target) {
|
||||
throw new Error(`Player ${this._owner} cannot attack itself`)
|
||||
// Existing attack on same target, add troops
|
||||
if (
|
||||
otherAttack._owner == this._owner &&
|
||||
otherAttack._targetID == this._targetID &&
|
||||
this.sourceTile == otherAttack.sourceTile
|
||||
) {
|
||||
otherAttack.troops += this.troops;
|
||||
otherAttack.refreshToConquer();
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.troops == null) {
|
||||
this.troops = this.mg.config().attackAmount(this._owner, this.target)
|
||||
}
|
||||
this.troops = Math.min(this._owner.troops(), this.troops)
|
||||
if (this.removeTroops) {
|
||||
this._owner.removeTroops(this.troops)
|
||||
}
|
||||
|
||||
for (const exec of mg.executions()) {
|
||||
if (exec.isActive() && exec instanceof AttackExecution && exec != this) {
|
||||
const otherAttack = exec as AttackExecution
|
||||
// Target has opposing attack, cancel them out
|
||||
if (this.target.isPlayer() && otherAttack._targetID == this._ownerID && this._targetID == otherAttack._ownerID) {
|
||||
if (otherAttack.troops > this.troops) {
|
||||
otherAttack.troops -= this.troops
|
||||
// otherAttack.calculateToConquer()
|
||||
this.active = false
|
||||
return
|
||||
} else {
|
||||
this.troops -= otherAttack.troops
|
||||
otherAttack.active = false
|
||||
}
|
||||
}
|
||||
// Existing attack on same target, add troops
|
||||
if (otherAttack._owner == this._owner && otherAttack._targetID == this._targetID && this.sourceTile == otherAttack.sourceTile) {
|
||||
otherAttack.troops += this.troops
|
||||
otherAttack.refreshToConquer()
|
||||
this.active = false
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this._owner.type() != PlayerType.Bot && this.target.isPlayer() && this.target.type() == PlayerType.Human) {
|
||||
mg.displayMessage(`You are being attacked by ${this._owner.displayName()}`, MessageType.ERROR, this._targetID)
|
||||
}
|
||||
if (this.sourceTile != null) {
|
||||
this.addNeighbors(this.sourceTile)
|
||||
} else {
|
||||
this.refreshToConquer()
|
||||
}
|
||||
|
||||
if (this.target.isPlayer()) {
|
||||
if (this._owner.isAlliedWith(this.target)) {
|
||||
// No updates should happen in init.
|
||||
this.breakAlliance = true
|
||||
}
|
||||
this.target.updateRelation(this._owner, -80)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
if (
|
||||
this._owner.type() != PlayerType.Bot &&
|
||||
this.target.isPlayer() &&
|
||||
this.target.type() == PlayerType.Human
|
||||
) {
|
||||
mg.displayMessage(
|
||||
`You are being attacked by ${this._owner.displayName()}`,
|
||||
MessageType.ERROR,
|
||||
this._targetID,
|
||||
);
|
||||
}
|
||||
if (this.sourceTile != null) {
|
||||
this.addNeighbors(this.sourceTile);
|
||||
} else {
|
||||
this.refreshToConquer();
|
||||
}
|
||||
|
||||
private refreshToConquer() {
|
||||
this.toConquer.clear()
|
||||
this.border.clear()
|
||||
for (const tile of this._owner.borderTiles()) {
|
||||
this.addNeighbors(tile)
|
||||
}
|
||||
if (this.target.isPlayer()) {
|
||||
if (this._owner.isAlliedWith(this.target)) {
|
||||
// No updates should happen in init.
|
||||
this.breakAlliance = true;
|
||||
}
|
||||
this.target.updateRelation(this._owner, -80);
|
||||
}
|
||||
}
|
||||
|
||||
private refreshToConquer() {
|
||||
this.toConquer.clear();
|
||||
this.border.clear();
|
||||
for (const tile of this._owner.borderTiles()) {
|
||||
this.addNeighbors(tile);
|
||||
}
|
||||
}
|
||||
|
||||
tick(ticks: number) {
|
||||
if (!this.active) {
|
||||
return;
|
||||
}
|
||||
const alliance = this._owner.allianceWith(this.target as Player);
|
||||
if (this.breakAlliance && alliance != null) {
|
||||
this.breakAlliance = false;
|
||||
this._owner.breakAlliance(alliance);
|
||||
}
|
||||
if (this.target.isPlayer() && this._owner.isAlliedWith(this.target)) {
|
||||
// In this case a new alliance was created AFTER the attack started.
|
||||
this._owner.addTroops(this.troops);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
tick(ticks: number) {
|
||||
if (!this.active) {
|
||||
return
|
||||
}
|
||||
const alliance = this._owner.allianceWith(this.target as Player)
|
||||
if (this.breakAlliance && alliance != null) {
|
||||
this.breakAlliance = false
|
||||
this._owner.breakAlliance(alliance)
|
||||
}
|
||||
if (this.target.isPlayer() && this._owner.isAlliedWith(this.target)) {
|
||||
// In this case a new alliance was created AFTER the attack started.
|
||||
this._owner.addTroops(this.troops)
|
||||
this.active = false
|
||||
return
|
||||
}
|
||||
let numTilesPerTick = this.mg
|
||||
.config()
|
||||
.attackTilesPerTick(
|
||||
this.troops,
|
||||
this._owner,
|
||||
this.target,
|
||||
this.border.size + this.random.nextInt(0, 5),
|
||||
);
|
||||
// consolex.log(`num tiles per tick: ${numTilesPerTick}`)
|
||||
// consolex.log(`num execs: ${this.mg.executions().length}`)
|
||||
|
||||
let numTilesPerTick = this.mg.config().attackTilesPerTick(this.troops, this._owner, this.target, this.border.size + this.random.nextInt(0, 5))
|
||||
// consolex.log(`num tiles per tick: ${numTilesPerTick}`)
|
||||
// consolex.log(`num execs: ${this.mg.executions().length}`)
|
||||
while (numTilesPerTick > 0) {
|
||||
if (this.troops < 1) {
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.toConquer.size() == 0) {
|
||||
this.refreshToConquer();
|
||||
this.active = false;
|
||||
this._owner.addTroops(this.troops);
|
||||
return;
|
||||
}
|
||||
|
||||
while (numTilesPerTick > 0) {
|
||||
if (this.troops < 1) {
|
||||
this.active = false
|
||||
return
|
||||
}
|
||||
const tileToConquer = this.toConquer.dequeue().tile;
|
||||
this.border.delete(tileToConquer);
|
||||
|
||||
if (this.toConquer.size() == 0) {
|
||||
this.refreshToConquer()
|
||||
this.active = false
|
||||
this._owner.addTroops(this.troops)
|
||||
return
|
||||
}
|
||||
|
||||
const tileToConquer = this.toConquer.dequeue().tile
|
||||
this.border.delete(tileToConquer)
|
||||
|
||||
const onBorder = this.mg.neighbors(tileToConquer).filter(t => this.mg.owner(t) == this._owner).length > 0
|
||||
if (this.mg.owner(tileToConquer) != this.target || !onBorder) {
|
||||
continue
|
||||
}
|
||||
this.addNeighbors(tileToConquer)
|
||||
const { attackerTroopLoss, defenderTroopLoss, tilesPerTickUsed } = this.mg.config().attackLogic(this.mg, this.troops, this._owner, this.target, tileToConquer)
|
||||
numTilesPerTick -= tilesPerTickUsed
|
||||
this.troops -= attackerTroopLoss
|
||||
if (this.target.isPlayer()) {
|
||||
this.target.removeTroops(defenderTroopLoss)
|
||||
}
|
||||
this._owner.conquer(tileToConquer)
|
||||
this.handleDeadDefender()
|
||||
}
|
||||
const onBorder =
|
||||
this.mg
|
||||
.neighbors(tileToConquer)
|
||||
.filter((t) => this.mg.owner(t) == this._owner).length > 0;
|
||||
if (this.mg.owner(tileToConquer) != this.target || !onBorder) {
|
||||
continue;
|
||||
}
|
||||
this.addNeighbors(tileToConquer);
|
||||
const { attackerTroopLoss, defenderTroopLoss, tilesPerTickUsed } = this.mg
|
||||
.config()
|
||||
.attackLogic(
|
||||
this.mg,
|
||||
this.troops,
|
||||
this._owner,
|
||||
this.target,
|
||||
tileToConquer,
|
||||
);
|
||||
numTilesPerTick -= tilesPerTickUsed;
|
||||
this.troops -= attackerTroopLoss;
|
||||
if (this.target.isPlayer()) {
|
||||
this.target.removeTroops(defenderTroopLoss);
|
||||
}
|
||||
this._owner.conquer(tileToConquer);
|
||||
this.handleDeadDefender();
|
||||
}
|
||||
}
|
||||
|
||||
private addNeighbors(tile: TileRef) {
|
||||
for (const neighbor of this.mg.neighbors(tile)) {
|
||||
if (this.mg.isWater(neighbor) || this.mg.owner(neighbor) != this.target) {
|
||||
continue
|
||||
private addNeighbors(tile: TileRef) {
|
||||
for (const neighbor of this.mg.neighbors(tile)) {
|
||||
if (this.mg.isWater(neighbor) || this.mg.owner(neighbor) != this.target) {
|
||||
continue;
|
||||
}
|
||||
this.border.add(neighbor);
|
||||
let numOwnedByMe = this.mg
|
||||
.neighbors(neighbor)
|
||||
.filter((t) => this.mg.owner(t) == this._owner).length;
|
||||
let dist = 0;
|
||||
if (numOwnedByMe > 2) {
|
||||
numOwnedByMe = 10;
|
||||
}
|
||||
let mag = 0;
|
||||
switch (this.mg.terrainType(tile)) {
|
||||
case TerrainType.Plains:
|
||||
mag = 1;
|
||||
break;
|
||||
case TerrainType.Highland:
|
||||
mag = 1.5;
|
||||
break;
|
||||
case TerrainType.Mountain:
|
||||
mag = 2;
|
||||
break;
|
||||
}
|
||||
this.toConquer.enqueue(
|
||||
new TileContainer(
|
||||
neighbor,
|
||||
dist / 100 + this.random.nextInt(0, 2) - numOwnedByMe + mag,
|
||||
this.mg.ticks(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private handleDeadDefender() {
|
||||
if (this.target.isPlayer() && this.target.numTilesOwned() < 100) {
|
||||
const gold = this.target.gold();
|
||||
this.mg.displayMessage(
|
||||
`Conquered ${this.target.displayName()} received ${renderNumber(gold)} gold`,
|
||||
MessageType.SUCCESS,
|
||||
this._owner.id(),
|
||||
);
|
||||
this.target.removeGold(gold);
|
||||
this._owner.addGold(gold);
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
for (const tile of this.target.tiles()) {
|
||||
const borders = this.mg
|
||||
.neighbors(tile)
|
||||
.some((t) => this.mg.owner(t) == this._owner);
|
||||
if (borders) {
|
||||
this._owner.conquer(tile);
|
||||
} else {
|
||||
for (const neighbor of this.mg.neighbors(tile)) {
|
||||
const no = this.mg.owner(neighbor);
|
||||
if (no.isPlayer() && no != this.target) {
|
||||
this.mg.player(no.id()).conquer(tile);
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.border.add(neighbor)
|
||||
let numOwnedByMe = this.mg.neighbors(neighbor)
|
||||
.filter(t => this.mg.owner(t) == this._owner)
|
||||
.length
|
||||
let dist = 0
|
||||
if (numOwnedByMe > 2) {
|
||||
numOwnedByMe = 10
|
||||
}
|
||||
let mag = 0
|
||||
switch (this.mg.terrainType(tile)) {
|
||||
case TerrainType.Plains:
|
||||
mag = 1
|
||||
break
|
||||
case TerrainType.Highland:
|
||||
mag = 1.5
|
||||
break
|
||||
case TerrainType.Mountain:
|
||||
mag = 2
|
||||
break
|
||||
}
|
||||
this.toConquer.enqueue(new TileContainer(
|
||||
neighbor,
|
||||
dist / 100 + this.random.nextInt(0, 2) - numOwnedByMe + mag,
|
||||
this.mg.ticks()
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private handleDeadDefender() {
|
||||
if (this.target.isPlayer() && this.target.numTilesOwned() < 100) {
|
||||
const gold = this.target.gold()
|
||||
this.mg.displayMessage(`Conquered ${this.target.displayName()} received ${renderNumber(gold)} gold`, MessageType.SUCCESS, this._owner.id())
|
||||
this.target.removeGold(gold)
|
||||
this._owner.addGold(gold)
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
for (const tile of this.target.tiles()) {
|
||||
const borders = this.mg.neighbors(tile).some(t => this.mg.owner(t) == this._owner)
|
||||
if (borders) {
|
||||
this._owner.conquer(tile)
|
||||
} else {
|
||||
for (const neighbor of this.mg.neighbors(tile)) {
|
||||
const no = this.mg.owner(neighbor)
|
||||
if (no.isPlayer() && no != this.target) {
|
||||
this.mg.player(no.id()).conquer(tile)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
owner(): Player {
|
||||
return this._owner
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active
|
||||
}
|
||||
owner(): Player {
|
||||
return this._owner;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TileContainer {
|
||||
constructor(public readonly tile: TileRef, public readonly priority: number, public readonly tick: number) { }
|
||||
}
|
||||
constructor(
|
||||
public readonly tile: TileRef,
|
||||
public readonly priority: number,
|
||||
public readonly tick: number,
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
import { Cell, Execution, Game, Player, Unit, PlayerID, TerrainType, UnitType } from "../game/Game";
|
||||
import {
|
||||
Cell,
|
||||
Execution,
|
||||
Game,
|
||||
Player,
|
||||
Unit,
|
||||
PlayerID,
|
||||
TerrainType,
|
||||
UnitType,
|
||||
} from "../game/Game";
|
||||
import { PathFinder } from "../pathfinding/PathFinding";
|
||||
import { PathFindResultType } from "../pathfinding/AStar";
|
||||
import { PseudoRandom } from "../PseudoRandom";
|
||||
@@ -8,133 +17,157 @@ import { consolex } from "../Consolex";
|
||||
import { TileRef } from "../game/GameMap";
|
||||
|
||||
export class BattleshipExecution implements Execution {
|
||||
private random: PseudoRandom
|
||||
private random: PseudoRandom;
|
||||
|
||||
private _owner: Player
|
||||
private active = true
|
||||
private battleship: Unit = null
|
||||
private mg: Game = null
|
||||
private _owner: Player;
|
||||
private active = true;
|
||||
private battleship: Unit = null;
|
||||
private mg: Game = null;
|
||||
|
||||
private pathfinder: PathFinder
|
||||
private pathfinder: PathFinder;
|
||||
|
||||
private patrolTile: TileRef;
|
||||
private patrolTile: TileRef;
|
||||
|
||||
// TODO: put in config
|
||||
private searchRange = 100
|
||||
private attackRate = 5
|
||||
private lastAttack = 0
|
||||
// TODO: put in config
|
||||
private searchRange = 100;
|
||||
private attackRate = 5;
|
||||
private lastAttack = 0;
|
||||
|
||||
private alreadyTargeted = new Set<Unit>()
|
||||
private alreadyTargeted = new Set<Unit>();
|
||||
|
||||
constructor(
|
||||
private playerID: PlayerID,
|
||||
private patrolCenterTile: TileRef,
|
||||
) { }
|
||||
constructor(
|
||||
private playerID: PlayerID,
|
||||
private patrolCenterTile: TileRef,
|
||||
) {}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.pathfinder = PathFinder.Mini(mg, 5000, false);
|
||||
this._owner = mg.player(this.playerID);
|
||||
this.mg = mg;
|
||||
this.patrolTile = this.patrolCenterTile;
|
||||
this.random = new PseudoRandom(mg.ticks());
|
||||
}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.pathfinder = PathFinder.Mini(mg, 5000, false)
|
||||
this._owner = mg.player(this.playerID)
|
||||
this.mg = mg
|
||||
this.patrolTile = this.patrolCenterTile
|
||||
this.random = new PseudoRandom(mg.ticks())
|
||||
tick(ticks: number): void {
|
||||
this.alreadyTargeted.forEach((u) => {
|
||||
if (!u.isActive()) {
|
||||
this.alreadyTargeted.delete(u);
|
||||
}
|
||||
});
|
||||
if (this.battleship == null) {
|
||||
const spawn = this._owner.canBuild(UnitType.Battleship, this.patrolTile);
|
||||
if (spawn == false) {
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
this.battleship = this._owner.buildUnit(UnitType.Battleship, 0, spawn);
|
||||
return;
|
||||
}
|
||||
if (!this.battleship.isActive()) {
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
this.alreadyTargeted.forEach(u => {
|
||||
if (!u.isActive()) {
|
||||
this.alreadyTargeted.delete(u)
|
||||
}
|
||||
})
|
||||
if (this.battleship == null) {
|
||||
const spawn = this._owner.canBuild(UnitType.Battleship, this.patrolTile)
|
||||
if (spawn == false) {
|
||||
this.active = false
|
||||
return
|
||||
}
|
||||
this.battleship = this._owner.buildUnit(UnitType.Battleship, 0, spawn)
|
||||
return
|
||||
}
|
||||
if (!this.battleship.isActive()) {
|
||||
this.active = false
|
||||
return
|
||||
}
|
||||
|
||||
if (this.mg.ticks() % 2 == 0) {
|
||||
const result = this.pathfinder.nextTile(this.battleship.tile(), this.patrolTile)
|
||||
switch (result.type) {
|
||||
case PathFindResultType.Completed:
|
||||
this.patrolTile = this.randomTile()
|
||||
break
|
||||
case PathFindResultType.NextTile:
|
||||
this.battleship.move(result.tile)
|
||||
break
|
||||
case PathFindResultType.Pending:
|
||||
return
|
||||
case PathFindResultType.PathNotFound:
|
||||
consolex.log(`path not found to patrol tile`)
|
||||
this.patrolTile = this.randomTile()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (this.mg.ticks() - this.lastAttack < this.attackRate) {
|
||||
return
|
||||
}
|
||||
|
||||
let ships = this.mg.units(UnitType.TransportShip, UnitType.Destroyer, UnitType.TradeShip, UnitType.Battleship)
|
||||
.filter(u => this.mg.manhattanDist(u.tile(), this.battleship.tile()) < 100)
|
||||
.filter(u => u.owner() != this.battleship.owner())
|
||||
.filter(u => u != this.battleship)
|
||||
.filter(u => !u.owner().isAlliedWith(this.battleship.owner()))
|
||||
.filter(u => !this.alreadyTargeted.has(u))
|
||||
.sort(distSortUnit(this.mg, this.battleship));
|
||||
|
||||
const friendlyDestroyerNearby = this.battleship.owner().units(UnitType.Destroyer)
|
||||
.filter(d => this.mg.manhattanDist(d.tile(), this.battleship.tile()) < 120)
|
||||
.length > 0
|
||||
|
||||
if (friendlyDestroyerNearby) {
|
||||
// Don't attack trade ships to allow friendly destroyer to capture them
|
||||
ships = ships.filter(s => s.type() != UnitType.TradeShip)
|
||||
}
|
||||
|
||||
if (ships.length > 0) {
|
||||
const toAttack = ships[0]
|
||||
if (!toAttack.hasHealth()) {
|
||||
// Don't send multiple shells to target if it can be one-shotted.
|
||||
this.alreadyTargeted.add(toAttack)
|
||||
}
|
||||
this.lastAttack = this.mg.ticks()
|
||||
this.mg.addExecution(new ShellExecution(this.battleship.tile(), this.battleship.owner(), this.battleship, toAttack))
|
||||
}
|
||||
if (this.mg.ticks() % 2 == 0) {
|
||||
const result = this.pathfinder.nextTile(
|
||||
this.battleship.tile(),
|
||||
this.patrolTile,
|
||||
);
|
||||
switch (result.type) {
|
||||
case PathFindResultType.Completed:
|
||||
this.patrolTile = this.randomTile();
|
||||
break;
|
||||
case PathFindResultType.NextTile:
|
||||
this.battleship.move(result.tile);
|
||||
break;
|
||||
case PathFindResultType.Pending:
|
||||
return;
|
||||
case PathFindResultType.PathNotFound:
|
||||
consolex.log(`path not found to patrol tile`);
|
||||
this.patrolTile = this.randomTile();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
owner(): Player {
|
||||
return this._owner
|
||||
if (this.mg.ticks() - this.lastAttack < this.attackRate) {
|
||||
return;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active
|
||||
let ships = this.mg
|
||||
.units(
|
||||
UnitType.TransportShip,
|
||||
UnitType.Destroyer,
|
||||
UnitType.TradeShip,
|
||||
UnitType.Battleship,
|
||||
)
|
||||
.filter(
|
||||
(u) => this.mg.manhattanDist(u.tile(), this.battleship.tile()) < 100,
|
||||
)
|
||||
.filter((u) => u.owner() != this.battleship.owner())
|
||||
.filter((u) => u != this.battleship)
|
||||
.filter((u) => !u.owner().isAlliedWith(this.battleship.owner()))
|
||||
.filter((u) => !this.alreadyTargeted.has(u))
|
||||
.sort(distSortUnit(this.mg, this.battleship));
|
||||
|
||||
const friendlyDestroyerNearby =
|
||||
this.battleship
|
||||
.owner()
|
||||
.units(UnitType.Destroyer)
|
||||
.filter(
|
||||
(d) => this.mg.manhattanDist(d.tile(), this.battleship.tile()) < 120,
|
||||
).length > 0;
|
||||
|
||||
if (friendlyDestroyerNearby) {
|
||||
// Don't attack trade ships to allow friendly destroyer to capture them
|
||||
ships = ships.filter((s) => s.type() != UnitType.TradeShip);
|
||||
}
|
||||
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false
|
||||
if (ships.length > 0) {
|
||||
const toAttack = ships[0];
|
||||
if (!toAttack.hasHealth()) {
|
||||
// Don't send multiple shells to target if it can be one-shotted.
|
||||
this.alreadyTargeted.add(toAttack);
|
||||
}
|
||||
this.lastAttack = this.mg.ticks();
|
||||
this.mg.addExecution(
|
||||
new ShellExecution(
|
||||
this.battleship.tile(),
|
||||
this.battleship.owner(),
|
||||
this.battleship,
|
||||
toAttack,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
randomTile(): TileRef {
|
||||
while (true) {
|
||||
const x = this.mg.x(this.patrolCenterTile) + this.random.nextInt(-this.searchRange / 2, this.searchRange / 2)
|
||||
const y = this.mg.y(this.patrolCenterTile) + this.random.nextInt(-this.searchRange / 2, this.searchRange / 2)
|
||||
if (!this.mg.isValidCoord(x, y)) {
|
||||
continue
|
||||
}
|
||||
const tile = this.mg.ref(x, y)
|
||||
if (!this.mg.isOcean(tile)) {
|
||||
continue
|
||||
}
|
||||
return tile
|
||||
}
|
||||
owner(): Player {
|
||||
return this._owner;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
randomTile(): TileRef {
|
||||
while (true) {
|
||||
const x =
|
||||
this.mg.x(this.patrolCenterTile) +
|
||||
this.random.nextInt(-this.searchRange / 2, this.searchRange / 2);
|
||||
const y =
|
||||
this.mg.y(this.patrolCenterTile) +
|
||||
this.random.nextInt(-this.searchRange / 2, this.searchRange / 2);
|
||||
if (!this.mg.isValidCoord(x, y)) {
|
||||
continue;
|
||||
}
|
||||
const tile = this.mg.ref(x, y);
|
||||
if (!this.mg.isOcean(tile)) {
|
||||
continue;
|
||||
}
|
||||
return tile;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,109 +1,118 @@
|
||||
import { Cell, Execution, Game, Player, PlayerType, TerraNullius } from "../game/Game"
|
||||
import { PseudoRandom } from "../PseudoRandom"
|
||||
import {
|
||||
Cell,
|
||||
Execution,
|
||||
Game,
|
||||
Player,
|
||||
PlayerType,
|
||||
TerraNullius,
|
||||
} from "../game/Game";
|
||||
import { PseudoRandom } from "../PseudoRandom";
|
||||
import { simpleHash } from "../Util";
|
||||
import { AttackExecution } from "./AttackExecution";
|
||||
|
||||
export class BotExecution implements Execution {
|
||||
private active = true;
|
||||
private random: PseudoRandom;
|
||||
private attackRate: number;
|
||||
private mg: Game;
|
||||
private neighborsTerraNullius = true;
|
||||
|
||||
private active = true
|
||||
private random: PseudoRandom;
|
||||
private attackRate: number
|
||||
private mg: Game
|
||||
private neighborsTerraNullius = true
|
||||
constructor(private bot: Player) {
|
||||
this.random = new PseudoRandom(simpleHash(bot.id()));
|
||||
this.attackRate = this.random.nextInt(10, 50);
|
||||
}
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
init(mg: Game, ticks: number) {
|
||||
this.mg = mg;
|
||||
// this.neighborsTerra = this.bot.neighbors().filter(n => n == this.gs.terraNullius()).length > 0
|
||||
}
|
||||
|
||||
constructor(private bot: Player) {
|
||||
this.random = new PseudoRandom(simpleHash(bot.id()))
|
||||
this.attackRate = this.random.nextInt(10, 50)
|
||||
}
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false
|
||||
tick(ticks: number) {
|
||||
if (!this.bot.isAlive()) {
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
init(mg: Game, ticks: number) {
|
||||
this.mg = mg
|
||||
// this.neighborsTerra = this.bot.neighbors().filter(n => n == this.gs.terraNullius()).length > 0
|
||||
if (ticks % this.attackRate != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
tick(ticks: number) {
|
||||
if (!this.bot.isAlive()) {
|
||||
this.active = false
|
||||
return
|
||||
this.bot.incomingAllianceRequests().forEach((ar) => {
|
||||
if (ar.requestor().isTraitor()) {
|
||||
ar.reject();
|
||||
} else {
|
||||
ar.accept();
|
||||
}
|
||||
});
|
||||
|
||||
const traitors = this.bot
|
||||
.neighbors()
|
||||
.filter((n) => n.isPlayer() && n.isTraitor()) as Player[];
|
||||
if (traitors.length > 0) {
|
||||
const toAttack = this.random.randElement(traitors);
|
||||
const odds = this.bot.isAlliedWith(toAttack) ? 6 : 3;
|
||||
if (this.random.chance(odds)) {
|
||||
this.sendAttack(toAttack);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.neighborsTerraNullius) {
|
||||
for (const b of this.bot.borderTiles()) {
|
||||
for (const n of this.mg.neighbors(b)) {
|
||||
if (!this.mg.hasOwner(n) && this.mg.isLand(n)) {
|
||||
this.sendAttack(this.mg.terraNullius());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (ticks % this.attackRate != 0) {
|
||||
return
|
||||
}
|
||||
|
||||
this.bot.incomingAllianceRequests().forEach(ar => {
|
||||
if (ar.requestor().isTraitor()) {
|
||||
ar.reject()
|
||||
} else {
|
||||
ar.accept()
|
||||
}
|
||||
})
|
||||
|
||||
const traitors = this.bot.neighbors().filter(n => n.isPlayer() && n.isTraitor()) as Player[]
|
||||
if (traitors.length > 0) {
|
||||
const toAttack = this.random.randElement(traitors)
|
||||
const odds = this.bot.isAlliedWith(toAttack) ? 6 : 3
|
||||
if (this.random.chance(odds)) {
|
||||
this.sendAttack(toAttack)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (this.neighborsTerraNullius) {
|
||||
for (const b of this.bot.borderTiles()) {
|
||||
for (const n of this.mg.neighbors(b)) {
|
||||
if (!this.mg.hasOwner(n) && this.mg.isLand(n)) {
|
||||
this.sendAttack(this.mg.terraNullius())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
this.neighborsTerraNullius = false
|
||||
}
|
||||
|
||||
const border = Array.from(this.bot.borderTiles())
|
||||
.flatMap(t => this.mg.neighbors(t))
|
||||
.filter(t => this.mg.hasOwner(t) && this.mg.owner(t) != this.bot)
|
||||
|
||||
if (border.length == 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const toAttack = border[this.random.nextInt(0, border.length)]
|
||||
const owner = this.mg.owner(toAttack)
|
||||
|
||||
if (owner.isPlayer()) {
|
||||
if (this.bot.isAlliedWith(owner)) {
|
||||
return
|
||||
}
|
||||
if (owner.type() == PlayerType.FakeHuman) {
|
||||
if (!this.random.chance(2)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
this.sendAttack(owner)
|
||||
}
|
||||
this.neighborsTerraNullius = false;
|
||||
}
|
||||
|
||||
sendAttack(toAttack: Player | TerraNullius) {
|
||||
this.mg.addExecution(new AttackExecution(
|
||||
this.bot.troops() / 20,
|
||||
this.bot.id(),
|
||||
toAttack.isPlayer() ? toAttack.id() : null,
|
||||
null,
|
||||
null
|
||||
))
|
||||
const border = Array.from(this.bot.borderTiles())
|
||||
.flatMap((t) => this.mg.neighbors(t))
|
||||
.filter((t) => this.mg.hasOwner(t) && this.mg.owner(t) != this.bot);
|
||||
|
||||
if (border.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
owner(): Player {
|
||||
return this.bot
|
||||
}
|
||||
const toAttack = border[this.random.nextInt(0, border.length)];
|
||||
const owner = this.mg.owner(toAttack);
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active
|
||||
if (owner.isPlayer()) {
|
||||
if (this.bot.isAlliedWith(owner)) {
|
||||
return;
|
||||
}
|
||||
if (owner.type() == PlayerType.FakeHuman) {
|
||||
if (!this.random.chance(2)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.sendAttack(owner);
|
||||
}
|
||||
|
||||
sendAttack(toAttack: Player | TerraNullius) {
|
||||
this.mg.addExecution(
|
||||
new AttackExecution(
|
||||
this.bot.troops() / 20,
|
||||
this.bot.id(),
|
||||
toAttack.isPlayer() ? toAttack.id() : null,
|
||||
null,
|
||||
null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
owner(): Player {
|
||||
return this.bot;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,63 +6,65 @@ import { GameID, SpawnIntent } from "../Schemas";
|
||||
import { simpleHash } from "../Util";
|
||||
import { BOT_NAME_PREFIXES, BOT_NAME_SUFFIXES } from "./utils/BotNames";
|
||||
|
||||
|
||||
export class BotSpawner {
|
||||
private random: PseudoRandom
|
||||
private bots: SpawnIntent[] = [];
|
||||
private random: PseudoRandom;
|
||||
private bots: SpawnIntent[] = [];
|
||||
|
||||
constructor(private gs: Game, gameID: GameID) {
|
||||
this.random = new PseudoRandom(simpleHash(gameID))
|
||||
}
|
||||
constructor(
|
||||
private gs: Game,
|
||||
gameID: GameID,
|
||||
) {
|
||||
this.random = new PseudoRandom(simpleHash(gameID));
|
||||
}
|
||||
|
||||
spawnBots(numBots: number): SpawnIntent[] {
|
||||
let tries = 0
|
||||
while (this.bots.length < numBots) {
|
||||
if (tries > 10000) {
|
||||
consolex.log('too many retries while spawning bots, giving up')
|
||||
return this.bots
|
||||
}
|
||||
const botName = this.randomBotName();
|
||||
const spawn = this.spawnBot(botName);
|
||||
if (spawn != null) {
|
||||
this.bots.push(spawn);
|
||||
} else {
|
||||
tries++
|
||||
}
|
||||
}
|
||||
spawnBots(numBots: number): SpawnIntent[] {
|
||||
let tries = 0;
|
||||
while (this.bots.length < numBots) {
|
||||
if (tries > 10000) {
|
||||
consolex.log("too many retries while spawning bots, giving up");
|
||||
return this.bots;
|
||||
}
|
||||
const botName = this.randomBotName();
|
||||
const spawn = this.spawnBot(botName);
|
||||
if (spawn != null) {
|
||||
this.bots.push(spawn);
|
||||
} else {
|
||||
tries++;
|
||||
}
|
||||
}
|
||||
return this.bots;
|
||||
}
|
||||
|
||||
spawnBot(botName: string): SpawnIntent | null {
|
||||
const tile = this.randTile()
|
||||
if (!this.gs.isLand(tile)) {
|
||||
return null
|
||||
}
|
||||
for (const spawn of this.bots) {
|
||||
if (this.gs.manhattanDist(this.gs.ref(spawn.x, spawn.y), tile) < 30) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
return {
|
||||
type: 'spawn',
|
||||
playerID: this.random.nextID(),
|
||||
name: botName,
|
||||
playerType: PlayerType.Bot,
|
||||
x: this.gs.x(tile),
|
||||
y: this.gs.y(tile)
|
||||
};
|
||||
spawnBot(botName: string): SpawnIntent | null {
|
||||
const tile = this.randTile();
|
||||
if (!this.gs.isLand(tile)) {
|
||||
return null;
|
||||
}
|
||||
for (const spawn of this.bots) {
|
||||
if (this.gs.manhattanDist(this.gs.ref(spawn.x, spawn.y), tile) < 30) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return {
|
||||
type: "spawn",
|
||||
playerID: this.random.nextID(),
|
||||
name: botName,
|
||||
playerType: PlayerType.Bot,
|
||||
x: this.gs.x(tile),
|
||||
y: this.gs.y(tile),
|
||||
};
|
||||
}
|
||||
|
||||
private randomBotName(): string {
|
||||
const prefixIndex = this.random.nextInt(0, BOT_NAME_PREFIXES.length);
|
||||
const suffixIndex = this.random.nextInt(0, BOT_NAME_SUFFIXES.length);
|
||||
return `${BOT_NAME_PREFIXES[prefixIndex]} ${BOT_NAME_SUFFIXES[suffixIndex]}`;
|
||||
}
|
||||
private randomBotName(): string {
|
||||
const prefixIndex = this.random.nextInt(0, BOT_NAME_PREFIXES.length);
|
||||
const suffixIndex = this.random.nextInt(0, BOT_NAME_SUFFIXES.length);
|
||||
return `${BOT_NAME_PREFIXES[prefixIndex]} ${BOT_NAME_SUFFIXES[suffixIndex]}`;
|
||||
}
|
||||
|
||||
private randTile(): TileRef {
|
||||
return this.gs.ref(
|
||||
this.random.nextInt(0, this.gs.width()),
|
||||
this.random.nextInt(0, this.gs.height())
|
||||
)
|
||||
}
|
||||
private randTile(): TileRef {
|
||||
return this.gs.ref(
|
||||
this.random.nextInt(0, this.gs.width()),
|
||||
this.random.nextInt(0, this.gs.height()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,47 +1,55 @@
|
||||
import { consolex } from "../Consolex";
|
||||
import { Execution, Game, Player, Unit, PlayerID, UnitType } from "../game/Game";
|
||||
import {
|
||||
Execution,
|
||||
Game,
|
||||
Player,
|
||||
Unit,
|
||||
PlayerID,
|
||||
UnitType,
|
||||
} from "../game/Game";
|
||||
import { TileRef } from "../game/GameMap";
|
||||
|
||||
export class CityExecution implements Execution {
|
||||
private player: Player;
|
||||
private mg: Game;
|
||||
private city: Unit;
|
||||
private active: boolean = true;
|
||||
|
||||
private player: Player
|
||||
private mg: Game
|
||||
private city: Unit
|
||||
private active: boolean = true
|
||||
constructor(
|
||||
private ownerId: PlayerID,
|
||||
private tile: TileRef,
|
||||
) {}
|
||||
|
||||
constructor(private ownerId: PlayerID, private tile: TileRef) { }
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.mg = mg;
|
||||
this.player = mg.player(this.ownerId);
|
||||
}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.mg = mg
|
||||
this.player = mg.player(this.ownerId)
|
||||
tick(ticks: number): void {
|
||||
if (this.city == null) {
|
||||
const spawnTile = this.player.canBuild(UnitType.City, this.tile);
|
||||
if (spawnTile == false) {
|
||||
consolex.warn("cannot build city");
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
this.city = this.player.buildUnit(UnitType.City, 0, spawnTile);
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
if (this.city == null) {
|
||||
const spawnTile = this.player.canBuild(UnitType.City, this.tile)
|
||||
if (spawnTile == false) {
|
||||
consolex.warn('cannot build city')
|
||||
this.active = false
|
||||
return
|
||||
}
|
||||
this.city = this.player.buildUnit(UnitType.City, 0, spawnTile)
|
||||
}
|
||||
if (!this.city.isActive()) {
|
||||
this.active = false
|
||||
return
|
||||
}
|
||||
if (!this.city.isActive()) {
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
owner(): Player {
|
||||
return null
|
||||
}
|
||||
owner(): Player {
|
||||
return null;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active
|
||||
}
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,57 +1,76 @@
|
||||
import { consolex } from "../Consolex";
|
||||
import { Cell, DefenseBonus, Execution, Game, Player, Unit, PlayerID, UnitType } from "../game/Game";
|
||||
import {
|
||||
Cell,
|
||||
DefenseBonus,
|
||||
Execution,
|
||||
Game,
|
||||
Player,
|
||||
Unit,
|
||||
PlayerID,
|
||||
UnitType,
|
||||
} from "../game/Game";
|
||||
import { manhattanDistFN, TileRef } from "../game/GameMap";
|
||||
|
||||
export class DefensePostExecution implements Execution {
|
||||
private player: Player;
|
||||
private mg: Game;
|
||||
private post: Unit;
|
||||
private active: boolean = true;
|
||||
|
||||
private player: Player
|
||||
private mg: Game
|
||||
private post: Unit
|
||||
private active: boolean = true
|
||||
private defenseBonuses: DefenseBonus[] = [];
|
||||
|
||||
private defenseBonuses: DefenseBonus[] = []
|
||||
constructor(
|
||||
private ownerId: PlayerID,
|
||||
private tile: TileRef,
|
||||
) {}
|
||||
|
||||
constructor(private ownerId: PlayerID, private tile: TileRef) { }
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.mg = mg;
|
||||
this.player = mg.player(this.ownerId);
|
||||
}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.mg = mg
|
||||
this.player = mg.player(this.ownerId)
|
||||
tick(ticks: number): void {
|
||||
if (this.post == null) {
|
||||
const spawnTile = this.player.canBuild(UnitType.DefensePost, this.tile);
|
||||
if (spawnTile == false) {
|
||||
consolex.warn("cannot build Defense Post");
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
this.post = this.player.buildUnit(UnitType.DefensePost, 0, spawnTile);
|
||||
this.mg
|
||||
.bfs(
|
||||
spawnTile,
|
||||
manhattanDistFN(spawnTile, this.mg.config().defensePostRange()),
|
||||
)
|
||||
.forEach((t) => {
|
||||
if (this.mg.isLake(t)) {
|
||||
this.defenseBonuses.push(
|
||||
this.mg.addTileDefenseBonus(
|
||||
t,
|
||||
this.post,
|
||||
this.mg.config().defensePostDefenseBonus(),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
if (this.post == null) {
|
||||
const spawnTile = this.player.canBuild(UnitType.DefensePost, this.tile)
|
||||
if (spawnTile == false) {
|
||||
consolex.warn('cannot build Defense Post')
|
||||
this.active = false
|
||||
return
|
||||
}
|
||||
this.post = this.player.buildUnit(UnitType.DefensePost, 0, spawnTile)
|
||||
this.mg.bfs(spawnTile, manhattanDistFN(spawnTile, this.mg.config().defensePostRange())).forEach(t => {
|
||||
if (this.mg.isLake(t)) {
|
||||
this.defenseBonuses.push(
|
||||
this.mg.addTileDefenseBonus(t, this.post, this.mg.config().defensePostDefenseBonus())
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
if (!this.post.isActive()) {
|
||||
this.defenseBonuses.forEach(df => this.mg.removeTileDefenseBonus(df))
|
||||
this.active = false
|
||||
return
|
||||
}
|
||||
if (!this.post.isActive()) {
|
||||
this.defenseBonuses.forEach((df) => this.mg.removeTileDefenseBonus(df));
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
owner(): Player {
|
||||
return null
|
||||
}
|
||||
owner(): Player {
|
||||
return null;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active
|
||||
}
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
import { Cell, Execution, Game, Player, Unit, PlayerID, TerrainType, UnitType } from "../game/Game";
|
||||
import {
|
||||
Cell,
|
||||
Execution,
|
||||
Game,
|
||||
Player,
|
||||
Unit,
|
||||
PlayerID,
|
||||
TerrainType,
|
||||
UnitType,
|
||||
} from "../game/Game";
|
||||
import { PathFinder } from "../pathfinding/PathFinding";
|
||||
import { PathFindResultType } from "../pathfinding/AStar";
|
||||
import { PseudoRandom } from "../PseudoRandom";
|
||||
@@ -7,142 +16,163 @@ import { consolex } from "../Consolex";
|
||||
import { TileRef } from "../game/GameMap";
|
||||
|
||||
export class DestroyerExecution implements Execution {
|
||||
private random: PseudoRandom
|
||||
private random: PseudoRandom;
|
||||
|
||||
private _owner: Player
|
||||
private active = true
|
||||
private destroyer: Unit = null
|
||||
private mg: Game = null
|
||||
private _owner: Player;
|
||||
private active = true;
|
||||
private destroyer: Unit = null;
|
||||
private mg: Game = null;
|
||||
|
||||
private target: Unit = null
|
||||
private pathfinder: PathFinder
|
||||
private target: Unit = null;
|
||||
private pathfinder: PathFinder;
|
||||
|
||||
private patrolTile: TileRef;
|
||||
private patrolTile: TileRef;
|
||||
|
||||
// TODO: put in config
|
||||
private searchRange = 100
|
||||
// TODO: put in config
|
||||
private searchRange = 100;
|
||||
|
||||
constructor(
|
||||
private playerID: PlayerID,
|
||||
private patrolCenterTile: TileRef,
|
||||
) { }
|
||||
constructor(
|
||||
private playerID: PlayerID,
|
||||
private patrolCenterTile: TileRef,
|
||||
) {}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.pathfinder = PathFinder.Mini(mg, 5000, false);
|
||||
this._owner = mg.player(this.playerID);
|
||||
this.mg = mg;
|
||||
this.patrolTile = this.patrolCenterTile;
|
||||
this.random = new PseudoRandom(mg.ticks());
|
||||
}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.pathfinder = PathFinder.Mini(mg, 5000, false)
|
||||
this._owner = mg.player(this.playerID)
|
||||
this.mg = mg
|
||||
this.patrolTile = this.patrolCenterTile
|
||||
this.random = new PseudoRandom(mg.ticks())
|
||||
tick(ticks: number): void {
|
||||
if (this.destroyer == null) {
|
||||
const spawn = this._owner.canBuild(UnitType.Destroyer, this.patrolTile);
|
||||
if (spawn == false) {
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
this.destroyer = this._owner.buildUnit(UnitType.Destroyer, 0, spawn);
|
||||
return;
|
||||
}
|
||||
if (!this.destroyer.isActive()) {
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
if (this.target != null && !this.target.isActive()) {
|
||||
this.target = null;
|
||||
}
|
||||
if (this.target == null) {
|
||||
const ships = this.mg
|
||||
.units(
|
||||
UnitType.TransportShip,
|
||||
UnitType.Destroyer,
|
||||
UnitType.TradeShip,
|
||||
UnitType.Battleship,
|
||||
)
|
||||
.filter(
|
||||
(u) => this.mg.manhattanDist(u.tile(), this.destroyer.tile()) < 100,
|
||||
)
|
||||
.filter(
|
||||
(u) =>
|
||||
u.type() != UnitType.Destroyer ||
|
||||
u.health() < this.destroyer.health(),
|
||||
) // only attack Destroyers weaker than it.
|
||||
.filter((u) => u.owner() != this.destroyer.owner())
|
||||
.filter((u) => u != this.destroyer)
|
||||
.filter((u) => !u.owner().isAlliedWith(this.destroyer.owner()));
|
||||
if (ships.length == 0) {
|
||||
const result = this.pathfinder.nextTile(
|
||||
this.destroyer.tile(),
|
||||
this.patrolTile,
|
||||
);
|
||||
switch (result.type) {
|
||||
case PathFindResultType.Completed:
|
||||
this.patrolTile = this.randomTile();
|
||||
break;
|
||||
case PathFindResultType.NextTile:
|
||||
this.destroyer.move(result.tile);
|
||||
break;
|
||||
case PathFindResultType.Pending:
|
||||
return;
|
||||
case PathFindResultType.PathNotFound:
|
||||
consolex.log(`path not found to patrol tile`);
|
||||
this.patrolTile = this.randomTile();
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.target = ships.sort(distSortUnit(this.mg, this.destroyer))[0];
|
||||
}
|
||||
if (!this.target.isActive() || this.target.owner() == this._owner) {
|
||||
// Incase another destroyer captured or destroyed target
|
||||
this.target = null;
|
||||
return;
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
if (this.destroyer == null) {
|
||||
const spawn = this._owner.canBuild(UnitType.Destroyer, this.patrolTile)
|
||||
if (spawn == false) {
|
||||
this.active = false
|
||||
return
|
||||
}
|
||||
this.destroyer = this._owner.buildUnit(UnitType.Destroyer, 0, spawn)
|
||||
return
|
||||
}
|
||||
if (!this.destroyer.isActive()) {
|
||||
this.active = false
|
||||
return
|
||||
}
|
||||
if (this.target != null && !this.target.isActive()) {
|
||||
this.target = null
|
||||
}
|
||||
if (this.target == null) {
|
||||
const ships = this.mg.units(UnitType.TransportShip, UnitType.Destroyer, UnitType.TradeShip, UnitType.Battleship)
|
||||
.filter(u => this.mg.manhattanDist(u.tile(), this.destroyer.tile()) < 100)
|
||||
.filter(u => u.type() != UnitType.Destroyer || u.health() < this.destroyer.health()) // only attack Destroyers weaker than it.
|
||||
.filter(u => u.owner() != this.destroyer.owner())
|
||||
.filter(u => u != this.destroyer)
|
||||
.filter(u => !u.owner().isAlliedWith(this.destroyer.owner()))
|
||||
if (ships.length == 0) {
|
||||
const result = this.pathfinder.nextTile(this.destroyer.tile(), this.patrolTile)
|
||||
switch (result.type) {
|
||||
case PathFindResultType.Completed:
|
||||
this.patrolTile = this.randomTile()
|
||||
break
|
||||
case PathFindResultType.NextTile:
|
||||
this.destroyer.move(result.tile)
|
||||
break
|
||||
case PathFindResultType.Pending:
|
||||
return
|
||||
case PathFindResultType.PathNotFound:
|
||||
consolex.log(`path not found to patrol tile`)
|
||||
this.patrolTile = this.randomTile()
|
||||
break
|
||||
}
|
||||
return
|
||||
}
|
||||
this.target = ships.sort(distSortUnit(this.mg, this.destroyer))[0]
|
||||
}
|
||||
if (!this.target.isActive() || this.target.owner() == this._owner) {
|
||||
// Incase another destroyer captured or destroyed target
|
||||
this.target = null
|
||||
return
|
||||
}
|
||||
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const result = this.pathfinder.nextTile(this.destroyer.tile(), this.target.tile(), 5)
|
||||
switch (result.type) {
|
||||
case PathFindResultType.Completed:
|
||||
switch (this.target.type()) {
|
||||
case UnitType.TransportShip:
|
||||
case UnitType.Battleship:
|
||||
this.target.delete()
|
||||
break
|
||||
case UnitType.TradeShip:
|
||||
this.owner().captureUnit(this.target)
|
||||
break
|
||||
case UnitType.Destroyer:
|
||||
const health = this.target.health()
|
||||
this.target.modifyHealth(-this.destroyer.health())
|
||||
this.destroyer.modifyHealth(-health)
|
||||
break
|
||||
}
|
||||
this.target = null
|
||||
return
|
||||
case PathFindResultType.NextTile:
|
||||
this.destroyer.move(result.tile)
|
||||
break
|
||||
case PathFindResultType.Pending:
|
||||
break
|
||||
case PathFindResultType.PathNotFound:
|
||||
consolex.log(`path not found to target`)
|
||||
break
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const result = this.pathfinder.nextTile(
|
||||
this.destroyer.tile(),
|
||||
this.target.tile(),
|
||||
5,
|
||||
);
|
||||
switch (result.type) {
|
||||
case PathFindResultType.Completed:
|
||||
switch (this.target.type()) {
|
||||
case UnitType.TransportShip:
|
||||
case UnitType.Battleship:
|
||||
this.target.delete();
|
||||
break;
|
||||
case UnitType.TradeShip:
|
||||
this.owner().captureUnit(this.target);
|
||||
break;
|
||||
case UnitType.Destroyer:
|
||||
const health = this.target.health();
|
||||
this.target.modifyHealth(-this.destroyer.health());
|
||||
this.destroyer.modifyHealth(-health);
|
||||
break;
|
||||
}
|
||||
this.target = null;
|
||||
return;
|
||||
case PathFindResultType.NextTile:
|
||||
this.destroyer.move(result.tile);
|
||||
break;
|
||||
case PathFindResultType.Pending:
|
||||
break;
|
||||
case PathFindResultType.PathNotFound:
|
||||
consolex.log(`path not found to target`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
owner(): Player {
|
||||
return this._owner
|
||||
owner(): Player {
|
||||
return this._owner;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
randomTile(): TileRef {
|
||||
while (true) {
|
||||
const x =
|
||||
this.mg.x(this.patrolCenterTile) +
|
||||
this.random.nextInt(-this.searchRange / 2, this.searchRange / 2);
|
||||
const y =
|
||||
this.mg.y(this.patrolCenterTile) +
|
||||
this.random.nextInt(-this.searchRange / 2, this.searchRange / 2);
|
||||
if (!this.mg.isValidCoord(x, y)) {
|
||||
continue;
|
||||
}
|
||||
const tile = this.mg.ref(x, y);
|
||||
if (!this.mg.isOcean(tile)) {
|
||||
continue;
|
||||
}
|
||||
return tile;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active
|
||||
}
|
||||
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
randomTile(): TileRef {
|
||||
while (true) {
|
||||
const x = this.mg.x(this.patrolCenterTile) + this.random.nextInt(-this.searchRange / 2, this.searchRange / 2)
|
||||
const y = this.mg.y(this.patrolCenterTile) + this.random.nextInt(-this.searchRange / 2, this.searchRange / 2)
|
||||
if (!this.mg.isValidCoord(x, y)) {
|
||||
continue
|
||||
}
|
||||
const tile = this.mg.ref(x, y)
|
||||
if (!this.mg.isOcean(tile)) {
|
||||
continue
|
||||
}
|
||||
return tile
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,47 +2,46 @@ import { consolex } from "../Consolex";
|
||||
import { Execution, Game, Player, PlayerID } from "../game/Game";
|
||||
|
||||
export class DonateExecution implements Execution {
|
||||
private sender: Player;
|
||||
private recipient: Player;
|
||||
|
||||
private sender: Player
|
||||
private recipient: Player
|
||||
private active = true;
|
||||
|
||||
private active = true
|
||||
constructor(
|
||||
private senderID: PlayerID,
|
||||
private recipientID: PlayerID,
|
||||
private troops: number | null,
|
||||
) {}
|
||||
|
||||
constructor(
|
||||
private senderID: PlayerID,
|
||||
private recipientID: PlayerID,
|
||||
private troops: number | null
|
||||
) { }
|
||||
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.sender = mg.player(this.senderID)
|
||||
this.recipient = mg.player(this.recipientID)
|
||||
if (this.troops == null) {
|
||||
this.troops = mg.config().defaultDonationAmount(this.sender)
|
||||
}
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.sender = mg.player(this.senderID);
|
||||
this.recipient = mg.player(this.recipientID);
|
||||
if (this.troops == null) {
|
||||
this.troops = mg.config().defaultDonationAmount(this.sender);
|
||||
}
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
if (this.sender.canDonate(this.recipient)) {
|
||||
this.sender.donate(this.recipient, this.troops)
|
||||
this.recipient.updateRelation(this.sender, 50)
|
||||
} else {
|
||||
consolex.warn(`cannot send tropps from ${this.sender} to ${this.recipient}`)
|
||||
}
|
||||
this.active = false
|
||||
tick(ticks: number): void {
|
||||
if (this.sender.canDonate(this.recipient)) {
|
||||
this.sender.donate(this.recipient, this.troops);
|
||||
this.recipient.updateRelation(this.sender, 50);
|
||||
} else {
|
||||
consolex.warn(
|
||||
`cannot send tropps from ${this.sender} to ${this.recipient}`,
|
||||
);
|
||||
}
|
||||
this.active = false;
|
||||
}
|
||||
|
||||
owner(): Player {
|
||||
return null
|
||||
}
|
||||
owner(): Player {
|
||||
return null;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active
|
||||
}
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,47 +1,59 @@
|
||||
import { consolex } from "../Consolex";
|
||||
import { AllPlayers, Execution, Game, Player, PlayerID, PlayerType, UnitType } from "../game/Game";
|
||||
import {
|
||||
AllPlayers,
|
||||
Execution,
|
||||
Game,
|
||||
Player,
|
||||
PlayerID,
|
||||
PlayerType,
|
||||
UnitType,
|
||||
} from "../game/Game";
|
||||
|
||||
export class EmojiExecution implements Execution {
|
||||
private requestor: Player;
|
||||
private recipient: Player | typeof AllPlayers;
|
||||
|
||||
private requestor: Player
|
||||
private recipient: Player | typeof AllPlayers
|
||||
private active = true;
|
||||
|
||||
private active = true
|
||||
constructor(
|
||||
private senderID: PlayerID,
|
||||
private recipientID: PlayerID | typeof AllPlayers,
|
||||
private emoji: string,
|
||||
) {}
|
||||
|
||||
constructor(
|
||||
private senderID: PlayerID,
|
||||
private recipientID: PlayerID | typeof AllPlayers,
|
||||
private emoji: string
|
||||
) { }
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.requestor = mg.player(this.senderID);
|
||||
this.recipient =
|
||||
this.recipientID == AllPlayers ? AllPlayers : mg.player(this.recipientID);
|
||||
}
|
||||
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.requestor = mg.player(this.senderID)
|
||||
this.recipient = this.recipientID == AllPlayers ? AllPlayers : mg.player(this.recipientID)
|
||||
tick(ticks: number): void {
|
||||
if (this.requestor.canSendEmoji(this.recipient)) {
|
||||
this.requestor.sendEmoji(this.recipient, this.emoji);
|
||||
if (
|
||||
this.emoji == "🖕" &&
|
||||
this.recipient != AllPlayers &&
|
||||
this.recipient.type() == PlayerType.FakeHuman
|
||||
) {
|
||||
this.recipient.updateRelation(this.requestor, -100);
|
||||
}
|
||||
} else {
|
||||
consolex.warn(
|
||||
`cannot send emoji from ${this.requestor} to ${this.recipient}`,
|
||||
);
|
||||
}
|
||||
this.active = false;
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
if (this.requestor.canSendEmoji(this.recipient)) {
|
||||
this.requestor.sendEmoji(this.recipient, this.emoji)
|
||||
if (this.emoji == "🖕" && this.recipient != AllPlayers && this.recipient.type() == PlayerType.FakeHuman) {
|
||||
this.recipient.updateRelation(this.requestor, -100)
|
||||
}
|
||||
} else {
|
||||
consolex.warn(`cannot send emoji from ${this.requestor} to ${this.recipient}`)
|
||||
}
|
||||
this.active = false
|
||||
}
|
||||
owner(): Player {
|
||||
return null;
|
||||
}
|
||||
|
||||
owner(): Player {
|
||||
return null
|
||||
}
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active
|
||||
}
|
||||
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
import { Cell, Execution, Game, Player, PlayerInfo, TerraNullius, PlayerType, Alliance, UnitType } from "../game/Game";
|
||||
import { AttackIntent, BoatAttackIntentSchema, GameID, Intent, Turn } from "../Schemas";
|
||||
import {
|
||||
Cell,
|
||||
Execution,
|
||||
Game,
|
||||
Player,
|
||||
PlayerInfo,
|
||||
TerraNullius,
|
||||
PlayerType,
|
||||
Alliance,
|
||||
UnitType,
|
||||
} from "../game/Game";
|
||||
import {
|
||||
AttackIntent,
|
||||
BoatAttackIntentSchema,
|
||||
GameID,
|
||||
Intent,
|
||||
Turn,
|
||||
} from "../Schemas";
|
||||
import { AttackExecution } from "./AttackExecution";
|
||||
import { SpawnExecution } from "./SpawnExecution";
|
||||
import { BotSpawner } from "./BotSpawner";
|
||||
@@ -23,104 +39,148 @@ import { DefensePostExecution } from "./DefensePostExecution";
|
||||
import { CityExecution } from "./CityExecution";
|
||||
import { TileRef } from "../game/GameMap";
|
||||
|
||||
|
||||
|
||||
export class Executor {
|
||||
// private random = new PseudoRandom(999)
|
||||
private random: PseudoRandom = null;
|
||||
|
||||
constructor(
|
||||
private mg: Game,
|
||||
private gameID: GameID,
|
||||
) {
|
||||
// Add one to avoid id collisions with bots.
|
||||
this.random = new PseudoRandom(simpleHash(gameID) + 1);
|
||||
}
|
||||
|
||||
// private random = new PseudoRandom(999)
|
||||
private random: PseudoRandom = null
|
||||
createExecs(turn: Turn): Execution[] {
|
||||
return turn.intents.map((i) => this.createExec(i));
|
||||
}
|
||||
|
||||
constructor(private mg: Game, private gameID: GameID) {
|
||||
// Add one to avoid id collisions with bots.
|
||||
this.random = new PseudoRandom(simpleHash(gameID) + 1)
|
||||
}
|
||||
|
||||
createExecs(turn: Turn): Execution[] {
|
||||
return turn.intents.map(i => this.createExec(i))
|
||||
}
|
||||
|
||||
createExec(intent: Intent): Execution {
|
||||
switch (intent.type) {
|
||||
case "attack": {
|
||||
return new AttackExecution(
|
||||
intent.troops,
|
||||
intent.attackerID,
|
||||
intent.targetID,
|
||||
null
|
||||
);
|
||||
}
|
||||
case "spawn":
|
||||
return new SpawnExecution(
|
||||
new PlayerInfo(sanitize(intent.name), intent.playerType, intent.clientID, intent.playerID),
|
||||
this.mg.ref(intent.x, intent.y)
|
||||
);
|
||||
case "boat":
|
||||
return new TransportShipExecution(
|
||||
intent.attackerID,
|
||||
intent.targetID,
|
||||
this.mg.ref(intent.x, intent.y),
|
||||
intent.troops
|
||||
);
|
||||
case "allianceRequest":
|
||||
return new AllianceRequestExecution(intent.requestor, intent.recipient);
|
||||
case "allianceRequestReply":
|
||||
return new AllianceRequestReplyExecution(intent.requestor, intent.recipient, intent.accept);
|
||||
case "breakAlliance":
|
||||
return new BreakAllianceExecution(intent.requestor, intent.recipient);
|
||||
case "targetPlayer":
|
||||
return new TargetPlayerExecution(intent.requestor, intent.target);
|
||||
case "emoji":
|
||||
return new EmojiExecution(intent.sender, intent.recipient, intent.emoji);
|
||||
case "donate":
|
||||
return new DonateExecution(intent.sender, intent.recipient, intent.troops);
|
||||
case "troop_ratio":
|
||||
return new SetTargetTroopRatioExecution(intent.player, intent.ratio);
|
||||
case "build_unit":
|
||||
switch (intent.unit) {
|
||||
case UnitType.AtomBomb:
|
||||
case UnitType.HydrogenBomb:
|
||||
return new NukeExecution(intent.unit, intent.player, this.mg.ref(intent.x, intent.y))
|
||||
case UnitType.Destroyer:
|
||||
return new DestroyerExecution(intent.player, this.mg.ref(intent.x, intent.y))
|
||||
case UnitType.Battleship:
|
||||
return new BattleshipExecution(intent.player, this.mg.ref(intent.x, intent.y))
|
||||
case UnitType.Port:
|
||||
return new PortExecution(intent.player, this.mg.ref(intent.x, intent.y))
|
||||
case UnitType.MissileSilo:
|
||||
return new MissileSiloExecution(intent.player, this.mg.ref(intent.x, intent.y))
|
||||
case UnitType.DefensePost:
|
||||
return new DefensePostExecution(intent.player, this.mg.ref(intent.x, intent.y))
|
||||
case UnitType.City:
|
||||
return new CityExecution(intent.player, this.mg.ref(intent.x, intent.y))
|
||||
default:
|
||||
throw Error(`unit type ${intent.unit} not supported`)
|
||||
}
|
||||
default:
|
||||
throw new Error(`intent type ${intent} not found`);
|
||||
createExec(intent: Intent): Execution {
|
||||
switch (intent.type) {
|
||||
case "attack": {
|
||||
return new AttackExecution(
|
||||
intent.troops,
|
||||
intent.attackerID,
|
||||
intent.targetID,
|
||||
null,
|
||||
);
|
||||
}
|
||||
case "spawn":
|
||||
return new SpawnExecution(
|
||||
new PlayerInfo(
|
||||
sanitize(intent.name),
|
||||
intent.playerType,
|
||||
intent.clientID,
|
||||
intent.playerID,
|
||||
),
|
||||
this.mg.ref(intent.x, intent.y),
|
||||
);
|
||||
case "boat":
|
||||
return new TransportShipExecution(
|
||||
intent.attackerID,
|
||||
intent.targetID,
|
||||
this.mg.ref(intent.x, intent.y),
|
||||
intent.troops,
|
||||
);
|
||||
case "allianceRequest":
|
||||
return new AllianceRequestExecution(intent.requestor, intent.recipient);
|
||||
case "allianceRequestReply":
|
||||
return new AllianceRequestReplyExecution(
|
||||
intent.requestor,
|
||||
intent.recipient,
|
||||
intent.accept,
|
||||
);
|
||||
case "breakAlliance":
|
||||
return new BreakAllianceExecution(intent.requestor, intent.recipient);
|
||||
case "targetPlayer":
|
||||
return new TargetPlayerExecution(intent.requestor, intent.target);
|
||||
case "emoji":
|
||||
return new EmojiExecution(
|
||||
intent.sender,
|
||||
intent.recipient,
|
||||
intent.emoji,
|
||||
);
|
||||
case "donate":
|
||||
return new DonateExecution(
|
||||
intent.sender,
|
||||
intent.recipient,
|
||||
intent.troops,
|
||||
);
|
||||
case "troop_ratio":
|
||||
return new SetTargetTroopRatioExecution(intent.player, intent.ratio);
|
||||
case "build_unit":
|
||||
switch (intent.unit) {
|
||||
case UnitType.AtomBomb:
|
||||
case UnitType.HydrogenBomb:
|
||||
return new NukeExecution(
|
||||
intent.unit,
|
||||
intent.player,
|
||||
this.mg.ref(intent.x, intent.y),
|
||||
);
|
||||
case UnitType.Destroyer:
|
||||
return new DestroyerExecution(
|
||||
intent.player,
|
||||
this.mg.ref(intent.x, intent.y),
|
||||
);
|
||||
case UnitType.Battleship:
|
||||
return new BattleshipExecution(
|
||||
intent.player,
|
||||
this.mg.ref(intent.x, intent.y),
|
||||
);
|
||||
case UnitType.Port:
|
||||
return new PortExecution(
|
||||
intent.player,
|
||||
this.mg.ref(intent.x, intent.y),
|
||||
);
|
||||
case UnitType.MissileSilo:
|
||||
return new MissileSiloExecution(
|
||||
intent.player,
|
||||
this.mg.ref(intent.x, intent.y),
|
||||
);
|
||||
case UnitType.DefensePost:
|
||||
return new DefensePostExecution(
|
||||
intent.player,
|
||||
this.mg.ref(intent.x, intent.y),
|
||||
);
|
||||
case UnitType.City:
|
||||
return new CityExecution(
|
||||
intent.player,
|
||||
this.mg.ref(intent.x, intent.y),
|
||||
);
|
||||
default:
|
||||
throw Error(`unit type ${intent.unit} not supported`);
|
||||
}
|
||||
default:
|
||||
throw new Error(`intent type ${intent} not found`);
|
||||
}
|
||||
}
|
||||
|
||||
spawnBots(numBots: number): Execution[] {
|
||||
return new BotSpawner(this.mg, this.gameID).spawnBots(numBots).map(i => this.createExec(i))
|
||||
spawnBots(numBots: number): Execution[] {
|
||||
return new BotSpawner(this.mg, this.gameID)
|
||||
.spawnBots(numBots)
|
||||
.map((i) => this.createExec(i));
|
||||
}
|
||||
|
||||
fakeHumanExecutions(): Execution[] {
|
||||
const execs = [];
|
||||
for (const nation of this.mg.nations()) {
|
||||
execs.push(
|
||||
new FakeHumanExecution(
|
||||
this.gameID,
|
||||
new PlayerInfo(
|
||||
nation.name,
|
||||
PlayerType.FakeHuman,
|
||||
null,
|
||||
this.random.nextID(),
|
||||
),
|
||||
nation.cell,
|
||||
nation.strength *
|
||||
this.mg
|
||||
.config()
|
||||
.difficultyModifier(this.mg.config().gameConfig().difficulty),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
fakeHumanExecutions(): Execution[] {
|
||||
const execs = []
|
||||
for (const nation of this.mg.nations()) {
|
||||
execs.push(new FakeHumanExecution(
|
||||
this.gameID,
|
||||
new PlayerInfo(
|
||||
nation.name,
|
||||
PlayerType.FakeHuman,
|
||||
null,
|
||||
this.random.nextID()
|
||||
),
|
||||
nation.cell,
|
||||
nation.strength * this.mg.config().difficultyModifier(this.mg.config().gameConfig().difficulty)
|
||||
))
|
||||
}
|
||||
return execs
|
||||
}
|
||||
|
||||
}
|
||||
return execs;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,46 +1,53 @@
|
||||
import { consolex } from "../Consolex";
|
||||
import { Cell, Execution, Game, Player, Unit, PlayerID, UnitType } from "../game/Game";
|
||||
import {
|
||||
Cell,
|
||||
Execution,
|
||||
Game,
|
||||
Player,
|
||||
Unit,
|
||||
PlayerID,
|
||||
UnitType,
|
||||
} from "../game/Game";
|
||||
import { TileRef } from "../game/GameMap";
|
||||
|
||||
export class MissileSiloExecution implements Execution {
|
||||
private active = true;
|
||||
private mg: Game;
|
||||
private player: Player;
|
||||
private silo: Unit;
|
||||
|
||||
private active = true
|
||||
private mg: Game
|
||||
private player: Player
|
||||
private silo: Unit
|
||||
constructor(
|
||||
private _owner: PlayerID,
|
||||
private tile: TileRef,
|
||||
) {}
|
||||
|
||||
constructor(
|
||||
private _owner: PlayerID,
|
||||
private tile: TileRef
|
||||
) { }
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.mg = mg;
|
||||
this.player = mg.player(this._owner);
|
||||
}
|
||||
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.mg = mg
|
||||
this.player = mg.player(this._owner)
|
||||
tick(ticks: number): void {
|
||||
if (this.silo == null) {
|
||||
if (!this.player.canBuild(UnitType.MissileSilo, this.tile)) {
|
||||
consolex.warn(
|
||||
`player ${this.player} cannot build port at ${this.tile}`,
|
||||
);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
this.silo = this.player.buildUnit(UnitType.MissileSilo, 0, this.tile);
|
||||
}
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
if (this.silo == null) {
|
||||
if (!this.player.canBuild(UnitType.MissileSilo, this.tile)) {
|
||||
consolex.warn(`player ${this.player} cannot build port at ${this.tile}`)
|
||||
this.active = false
|
||||
return
|
||||
}
|
||||
this.silo = this.player.buildUnit(UnitType.MissileSilo, 0, this.tile)
|
||||
}
|
||||
}
|
||||
owner(): Player {
|
||||
return null;
|
||||
}
|
||||
|
||||
owner(): Player {
|
||||
return null
|
||||
}
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active
|
||||
}
|
||||
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
+120
-104
@@ -1,5 +1,14 @@
|
||||
import { nextTick } from "process";
|
||||
import { Cell, Execution, Game, Player, PlayerID, Unit, UnitType, TerraNullius } from "../game/Game";
|
||||
import {
|
||||
Cell,
|
||||
Execution,
|
||||
Game,
|
||||
Player,
|
||||
PlayerID,
|
||||
Unit,
|
||||
UnitType,
|
||||
TerraNullius,
|
||||
} from "../game/Game";
|
||||
import { PathFinder } from "../pathfinding/PathFinding";
|
||||
import { PathFindResultType } from "../pathfinding/AStar";
|
||||
import { PseudoRandom } from "../PseudoRandom";
|
||||
@@ -7,125 +16,132 @@ import { consolex } from "../Consolex";
|
||||
import { TileRef } from "../game/GameMap";
|
||||
|
||||
export class NukeExecution implements Execution {
|
||||
private player: Player;
|
||||
|
||||
private player: Player
|
||||
private active = true;
|
||||
|
||||
private active = true
|
||||
private mg: Game;
|
||||
|
||||
private mg: Game
|
||||
private nuke: Unit;
|
||||
|
||||
private nuke: Unit
|
||||
private pathFinder: PathFinder;
|
||||
constructor(
|
||||
private type: UnitType.AtomBomb | UnitType.HydrogenBomb,
|
||||
private senderID: PlayerID,
|
||||
private dst: TileRef,
|
||||
) {}
|
||||
|
||||
private pathFinder: PathFinder
|
||||
constructor(
|
||||
private type: UnitType.AtomBomb | UnitType.HydrogenBomb,
|
||||
private senderID: PlayerID,
|
||||
private dst: TileRef,
|
||||
) { }
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.mg = mg;
|
||||
this.pathFinder = PathFinder.Mini(mg, 10_000, true);
|
||||
this.player = mg.player(this.senderID);
|
||||
}
|
||||
|
||||
public target(): Player | TerraNullius {
|
||||
return this.mg.owner(this.dst);
|
||||
}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.mg = mg
|
||||
this.pathFinder = PathFinder.Mini(mg, 10_000, true)
|
||||
this.player = mg.player(this.senderID)
|
||||
tick(ticks: number): void {
|
||||
if (this.nuke == null) {
|
||||
const spawn = this.player.canBuild(this.type, this.dst);
|
||||
if (spawn == false) {
|
||||
consolex.warn(`cannot build Nuke`);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
this.nuke = this.player.buildUnit(this.type, 0, spawn);
|
||||
}
|
||||
|
||||
public target(): Player | TerraNullius {
|
||||
return this.mg.owner(this.dst)
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const result = this.pathFinder.nextTile(this.nuke.tile(), this.dst);
|
||||
switch (result.type) {
|
||||
case PathFindResultType.Completed:
|
||||
this.nuke.move(result.tile);
|
||||
this.detonate();
|
||||
return;
|
||||
case PathFindResultType.NextTile:
|
||||
this.nuke.move(result.tile);
|
||||
break;
|
||||
case PathFindResultType.Pending:
|
||||
break;
|
||||
case PathFindResultType.PathNotFound:
|
||||
consolex.warn(
|
||||
`nuke cannot find path from ${this.nuke.tile()} to ${this.dst}`,
|
||||
);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
if (this.nuke == null) {
|
||||
const spawn = this.player.canBuild(this.type, this.dst)
|
||||
if (spawn == false) {
|
||||
consolex.warn(`cannot build Nuke`)
|
||||
this.active = false
|
||||
return
|
||||
}
|
||||
this.nuke = this.player.buildUnit(this.type, 0, spawn)
|
||||
private detonate() {
|
||||
const magnitude =
|
||||
this.type == UnitType.AtomBomb
|
||||
? { inner: 15, outer: 40 }
|
||||
: { inner: 140, outer: 160 };
|
||||
const rand = new PseudoRandom(this.mg.ticks());
|
||||
const toDestroy = this.mg.bfs(this.dst, (_, n: TileRef) => {
|
||||
const d = this.mg.euclideanDist(this.dst, n);
|
||||
return (d <= magnitude.inner || rand.chance(2)) && d <= magnitude.outer;
|
||||
});
|
||||
|
||||
const ratio = Object.fromEntries(
|
||||
this.mg
|
||||
.players()
|
||||
.map((p) => [p.id(), (p.troops() + p.workers()) / p.numTilesOwned()]),
|
||||
);
|
||||
const attacked = new Map<Player, number>();
|
||||
for (const tile of toDestroy) {
|
||||
const owner = this.mg.owner(tile);
|
||||
if (owner.isPlayer()) {
|
||||
const mp = this.mg.player(owner.id());
|
||||
mp.relinquish(tile);
|
||||
mp.removeTroops(2 * ratio[mp.id()]);
|
||||
if (!attacked.has(mp)) {
|
||||
attacked.set(mp, 0);
|
||||
}
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const result = this.pathFinder.nextTile(this.nuke.tile(), this.dst)
|
||||
switch (result.type) {
|
||||
case PathFindResultType.Completed:
|
||||
this.nuke.move(result.tile)
|
||||
this.detonate()
|
||||
return
|
||||
case PathFindResultType.NextTile:
|
||||
this.nuke.move(result.tile)
|
||||
break
|
||||
case PathFindResultType.Pending:
|
||||
break
|
||||
case PathFindResultType.PathNotFound:
|
||||
consolex.warn(`nuke cannot find path from ${this.nuke.tile()} to ${this.dst}`)
|
||||
this.active = false
|
||||
return
|
||||
}
|
||||
const prev = attacked.get(mp);
|
||||
attacked.set(mp, prev + 1);
|
||||
}
|
||||
if (this.mg.isLand(tile)) {
|
||||
this.mg.setFallout(tile, true);
|
||||
}
|
||||
}
|
||||
for (const [other, tilesDestroyed] of attacked) {
|
||||
if (tilesDestroyed > 100) {
|
||||
const alliance = this.player.allianceWith(other);
|
||||
if (alliance != null) {
|
||||
this.player.breakAlliance(alliance);
|
||||
}
|
||||
}
|
||||
|
||||
private detonate() {
|
||||
const magnitude = this.type == UnitType.AtomBomb ? { inner: 15, outer: 40 } : { inner: 140, outer: 160 }
|
||||
const rand = new PseudoRandom(this.mg.ticks())
|
||||
const toDestroy = this.mg.bfs(this.dst, (_, n: TileRef) => {
|
||||
const d = this.mg.euclideanDist(this.dst, n)
|
||||
return (d <= magnitude.inner || rand.chance(2)) && d <= magnitude.outer
|
||||
})
|
||||
|
||||
const ratio = Object.fromEntries(
|
||||
this.mg.players().map(p => [p.id(), (p.troops() + p.workers()) / p.numTilesOwned()])
|
||||
)
|
||||
const attacked = new Map<Player, number>()
|
||||
for (const tile of toDestroy) {
|
||||
const owner = this.mg.owner(tile)
|
||||
if (owner.isPlayer()) {
|
||||
const mp = this.mg.player(owner.id())
|
||||
mp.relinquish(tile)
|
||||
mp.removeTroops(2 * ratio[mp.id()])
|
||||
if (!attacked.has(mp)) {
|
||||
attacked.set(mp, 0)
|
||||
}
|
||||
const prev = attacked.get(mp)
|
||||
attacked.set(mp, prev + 1)
|
||||
}
|
||||
if (this.mg.isLand(tile)) {
|
||||
this.mg.setFallout(tile, true)
|
||||
}
|
||||
if (other != this.player) {
|
||||
other.updateRelation(this.player, -100);
|
||||
}
|
||||
for (const [other, tilesDestroyed] of attacked) {
|
||||
if (tilesDestroyed > 100) {
|
||||
const alliance = this.player.allianceWith(other)
|
||||
if (alliance != null) {
|
||||
this.player.breakAlliance(alliance)
|
||||
}
|
||||
if (other != this.player) {
|
||||
other.updateRelation(this.player, -100)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const unit of this.mg.units()) {
|
||||
if (
|
||||
unit.type() != UnitType.AtomBomb &&
|
||||
unit.type() != UnitType.HydrogenBomb
|
||||
) {
|
||||
if (this.mg.euclideanDist(this.dst, unit.tile()) < magnitude.outer) {
|
||||
unit.delete();
|
||||
}
|
||||
|
||||
for (const unit of this.mg.units()) {
|
||||
if (unit.type() != UnitType.AtomBomb && unit.type() != UnitType.HydrogenBomb) {
|
||||
if (this.mg.euclideanDist(this.dst, unit.tile()) < magnitude.outer) {
|
||||
unit.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
this.active = false
|
||||
this.nuke.delete(false)
|
||||
}
|
||||
}
|
||||
this.active = false;
|
||||
this.nuke.delete(false);
|
||||
}
|
||||
|
||||
owner(): Player {
|
||||
return this.player
|
||||
}
|
||||
owner(): Player {
|
||||
return this.player;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active
|
||||
}
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,211 +1,226 @@
|
||||
import { Config } from "../configuration/Config"
|
||||
import { Execution, Game, Player, PlayerID, TerraNullius, UnitType } from "../game/Game"
|
||||
import { calculateBoundingBox, getMode, inscribed, simpleHash } from "../Util"
|
||||
import { GameImpl } from "../game/GameImpl"
|
||||
import { consolex } from "../Consolex"
|
||||
import { GameMap, TileRef } from "../game/GameMap"
|
||||
import { Config } from "../configuration/Config";
|
||||
import {
|
||||
Execution,
|
||||
Game,
|
||||
Player,
|
||||
PlayerID,
|
||||
TerraNullius,
|
||||
UnitType,
|
||||
} from "../game/Game";
|
||||
import { calculateBoundingBox, getMode, inscribed, simpleHash } from "../Util";
|
||||
import { GameImpl } from "../game/GameImpl";
|
||||
import { consolex } from "../Consolex";
|
||||
import { GameMap, TileRef } from "../game/GameMap";
|
||||
|
||||
export class PlayerExecution implements Execution {
|
||||
private readonly ticksPerClusterCalc = 20;
|
||||
|
||||
private readonly ticksPerClusterCalc = 20
|
||||
private player: Player;
|
||||
private config: Config;
|
||||
private lastCalc = 0;
|
||||
private mg: Game;
|
||||
private active = true;
|
||||
|
||||
private player: Player
|
||||
private config: Config
|
||||
private lastCalc = 0
|
||||
private mg: Game
|
||||
private active = true
|
||||
constructor(private playerID: PlayerID) {}
|
||||
|
||||
constructor(private playerID: PlayerID) {
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
init(mg: Game, ticks: number) {
|
||||
this.mg = mg;
|
||||
this.config = mg.config();
|
||||
this.player = mg.player(this.playerID);
|
||||
this.lastCalc =
|
||||
ticks + (simpleHash(this.player.name()) % this.ticksPerClusterCalc);
|
||||
}
|
||||
|
||||
tick(ticks: number) {
|
||||
this.player.decayRelations();
|
||||
this.player.units().forEach((u) => {
|
||||
if (u.health() <= 0) {
|
||||
u.delete();
|
||||
return;
|
||||
}
|
||||
u.modifyHealth(1);
|
||||
const tileOwner = this.mg.owner(u.tile());
|
||||
if (u.info().territoryBound) {
|
||||
if (tileOwner.isPlayer()) {
|
||||
if (tileOwner != this.player) {
|
||||
this.mg.player(tileOwner.id()).captureUnit(u);
|
||||
}
|
||||
} else {
|
||||
u.delete();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!this.player.isAlive()) {
|
||||
this.player.units().forEach((u) => {
|
||||
if (
|
||||
u.type() != UnitType.AtomBomb &&
|
||||
u.type() != UnitType.HydrogenBomb
|
||||
) {
|
||||
u.delete();
|
||||
}
|
||||
});
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false
|
||||
const popInc = this.config.populationIncreaseRate(this.player);
|
||||
this.player.addWorkers(popInc * (1 - this.player.targetTroopRatio())); // (1 - this.player.targetTroopRatio()))
|
||||
this.player.addTroops(popInc * this.player.targetTroopRatio());
|
||||
this.player.addGold(this.config.goldAdditionRate(this.player));
|
||||
const adjustRate = this.config.troopAdjustmentRate(this.player);
|
||||
this.player.addTroops(adjustRate);
|
||||
this.player.removeWorkers(adjustRate);
|
||||
|
||||
const alliances = Array.from(this.player.alliances());
|
||||
for (const alliance of alliances) {
|
||||
if (
|
||||
this.mg.ticks() - alliance.createdAt() >
|
||||
this.mg.config().allianceDuration()
|
||||
) {
|
||||
alliance.expire();
|
||||
}
|
||||
}
|
||||
|
||||
init(mg: Game, ticks: number) {
|
||||
this.mg = mg
|
||||
this.config = mg.config()
|
||||
this.player = mg.player(this.playerID)
|
||||
this.lastCalc = ticks + (simpleHash(this.player.name()) % this.ticksPerClusterCalc)
|
||||
if (ticks - this.lastCalc > this.ticksPerClusterCalc) {
|
||||
if (this.player.lastTileChange() > this.lastCalc) {
|
||||
this.lastCalc = ticks;
|
||||
const start = performance.now();
|
||||
this.removeClusters();
|
||||
const end = performance.now();
|
||||
if (end - start > 1000) {
|
||||
consolex.log(`player ${this.player.name()}, took ${end - start}ms`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private removeClusters() {
|
||||
const clusters = this.calculateClusters();
|
||||
clusters.sort((a, b) => b.size - a.size);
|
||||
|
||||
const main = clusters.shift();
|
||||
this.player.largestClusterBoundingBox = calculateBoundingBox(this.mg, main);
|
||||
const surroundedBy = this.surroundedBySamePlayer(main);
|
||||
if (surroundedBy && !this.player.isAlliedWith(surroundedBy)) {
|
||||
this.removeCluster(main);
|
||||
}
|
||||
|
||||
tick(ticks: number) {
|
||||
this.player.decayRelations()
|
||||
this.player.units().forEach(u => {
|
||||
if (u.health() <= 0) {
|
||||
u.delete()
|
||||
return
|
||||
}
|
||||
u.modifyHealth(1)
|
||||
const tileOwner = this.mg.owner(u.tile())
|
||||
if (u.info().territoryBound) {
|
||||
if (tileOwner.isPlayer()) {
|
||||
if (tileOwner != this.player) {
|
||||
this.mg.player(tileOwner.id()).captureUnit(u)
|
||||
}
|
||||
} else {
|
||||
u.delete()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (!this.player.isAlive()) {
|
||||
this.player.units().forEach(u => {
|
||||
if (u.type() != UnitType.AtomBomb && u.type() != UnitType.HydrogenBomb) {
|
||||
u.delete()
|
||||
}
|
||||
})
|
||||
this.active = false
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
const popInc = this.config.populationIncreaseRate(this.player)
|
||||
this.player.addWorkers(popInc * (1 - this.player.targetTroopRatio()))// (1 - this.player.targetTroopRatio()))
|
||||
this.player.addTroops(popInc * this.player.targetTroopRatio())
|
||||
this.player.addGold(this.config.goldAdditionRate(this.player))
|
||||
const adjustRate = this.config.troopAdjustmentRate(this.player)
|
||||
this.player.addTroops(adjustRate)
|
||||
this.player.removeWorkers(adjustRate)
|
||||
|
||||
const alliances = Array.from(this.player.alliances())
|
||||
for (const alliance of alliances) {
|
||||
if (this.mg.ticks() - alliance.createdAt() > this.mg.config().allianceDuration()) {
|
||||
alliance.expire()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (ticks - this.lastCalc > this.ticksPerClusterCalc) {
|
||||
if (this.player.lastTileChange() > this.lastCalc) {
|
||||
this.lastCalc = ticks
|
||||
const start = performance.now()
|
||||
this.removeClusters()
|
||||
const end = performance.now()
|
||||
if (end - start > 1000) {
|
||||
consolex.log(`player ${this.player.name()}, took ${end - start}ms`)
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const cluster of clusters) {
|
||||
if (this.isSurrounded(cluster)) {
|
||||
this.removeCluster(cluster);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private removeClusters() {
|
||||
const clusters = this.calculateClusters()
|
||||
clusters.sort((a, b) => b.size - a.size);
|
||||
|
||||
const main = clusters.shift()
|
||||
this.player.largestClusterBoundingBox = calculateBoundingBox(this.mg, main)
|
||||
const surroundedBy = this.surroundedBySamePlayer(main)
|
||||
if (surroundedBy && !this.player.isAlliedWith(surroundedBy)) {
|
||||
this.removeCluster(main)
|
||||
}
|
||||
|
||||
for (const cluster of clusters) {
|
||||
if (this.isSurrounded(cluster)) {
|
||||
this.removeCluster(cluster)
|
||||
}
|
||||
}
|
||||
private surroundedBySamePlayer(cluster: Set<TileRef>): false | Player {
|
||||
const enemies = new Set<number>();
|
||||
for (const ref of cluster) {
|
||||
if (
|
||||
this.mg.isOceanShore(ref) ||
|
||||
this.mg.neighbors(ref).some((n) => !this.mg.hasOwner(n))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
this.mg
|
||||
.neighbors(ref)
|
||||
.filter((n) => this.mg.ownerID(n) != this.player.smallID())
|
||||
.forEach((p) => enemies.add(this.mg.ownerID(p)));
|
||||
if (enemies.size != 1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private surroundedBySamePlayer(cluster: Set<TileRef>): false | Player {
|
||||
const enemies = new Set<number>()
|
||||
for (const ref of cluster) {
|
||||
if (this.mg.isOceanShore(ref) || this.mg.neighbors(ref).some(n => !this.mg.hasOwner(n))) {
|
||||
return false
|
||||
}
|
||||
this.mg.neighbors(ref)
|
||||
.filter(n => this.mg.ownerID(n) != this.player.smallID())
|
||||
.forEach(p => enemies.add(this.mg.ownerID(p)))
|
||||
if (enemies.size != 1) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if (enemies.size != 1) {
|
||||
return false
|
||||
}
|
||||
return this.mg.playerBySmallID(Array.from(enemies)[0]) as Player
|
||||
if (enemies.size != 1) {
|
||||
return false;
|
||||
}
|
||||
return this.mg.playerBySmallID(Array.from(enemies)[0]) as Player;
|
||||
}
|
||||
|
||||
private isSurrounded(cluster: Set<TileRef>): boolean {
|
||||
let enemyTiles = new Set<TileRef>()
|
||||
for (const tr of cluster) {
|
||||
if (this.mg.isOceanShore(tr)) {
|
||||
return false
|
||||
}
|
||||
this.mg.neighbors(tr)
|
||||
.filter(n => this.mg.ownerID(n) != this.player.smallID())
|
||||
.forEach(n => enemyTiles.add(n))
|
||||
}
|
||||
if (enemyTiles.size == 0) {
|
||||
return false
|
||||
}
|
||||
const enemyBox = calculateBoundingBox(this.mg, enemyTiles)
|
||||
const clusterBox = calculateBoundingBox(this.mg, cluster)
|
||||
return inscribed(enemyBox, clusterBox)
|
||||
private isSurrounded(cluster: Set<TileRef>): boolean {
|
||||
let enemyTiles = new Set<TileRef>();
|
||||
for (const tr of cluster) {
|
||||
if (this.mg.isOceanShore(tr)) {
|
||||
return false;
|
||||
}
|
||||
this.mg
|
||||
.neighbors(tr)
|
||||
.filter((n) => this.mg.ownerID(n) != this.player.smallID())
|
||||
.forEach((n) => enemyTiles.add(n));
|
||||
}
|
||||
|
||||
private removeCluster(cluster: Set<TileRef>) {
|
||||
const result = new Set<number>(); // Use Set to automatically deduplicate ownerIDs
|
||||
for (const t of cluster) {
|
||||
for (const neighbor of this.mg.neighbors(t)) {
|
||||
if (this.mg.ownerID(neighbor) != this.player.smallID()) {
|
||||
result.add(this.mg.ownerID(neighbor));
|
||||
}
|
||||
}
|
||||
}
|
||||
const mode = getMode(result)
|
||||
if (!this.mg.playerBySmallID(mode).isPlayer()) {
|
||||
return
|
||||
}
|
||||
const firstTile = cluster.values().next().value
|
||||
const filter = (_, t: TileRef): boolean => this.mg.ownerID(t) == this.mg.ownerID(firstTile)
|
||||
const tiles = this.mg.bfs(firstTile, filter)
|
||||
|
||||
const modePlayer = this.mg.playerBySmallID(mode)
|
||||
if (!modePlayer.isPlayer()) {
|
||||
consolex.warn('mode player is null')
|
||||
return
|
||||
}
|
||||
for (const tile of tiles) {
|
||||
(modePlayer as Player).conquer(tile)
|
||||
}
|
||||
if (enemyTiles.size == 0) {
|
||||
return false;
|
||||
}
|
||||
const enemyBox = calculateBoundingBox(this.mg, enemyTiles);
|
||||
const clusterBox = calculateBoundingBox(this.mg, cluster);
|
||||
return inscribed(enemyBox, clusterBox);
|
||||
}
|
||||
|
||||
private calculateClusters(): Set<TileRef>[] {
|
||||
const seen = new Set<TileRef>()
|
||||
const border = this.player.borderTiles()
|
||||
const clusters: Set<TileRef>[] = []
|
||||
for (const tile of border) {
|
||||
if (seen.has(tile)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const cluster = new Set<TileRef>()
|
||||
const queue: TileRef[] = [tile]
|
||||
seen.add(tile)
|
||||
while (queue.length > 0) {
|
||||
const curr = queue.shift()
|
||||
cluster.add(curr)
|
||||
|
||||
const neighbors = (this.mg as GameImpl).neighborsWithDiag(curr)
|
||||
for (const neighbor of neighbors) {
|
||||
if (border.has(neighbor) && !seen.has(neighbor)) {
|
||||
queue.push(neighbor)
|
||||
seen.add(neighbor)
|
||||
}
|
||||
}
|
||||
}
|
||||
clusters.push(cluster)
|
||||
private removeCluster(cluster: Set<TileRef>) {
|
||||
const result = new Set<number>(); // Use Set to automatically deduplicate ownerIDs
|
||||
for (const t of cluster) {
|
||||
for (const neighbor of this.mg.neighbors(t)) {
|
||||
if (this.mg.ownerID(neighbor) != this.player.smallID()) {
|
||||
result.add(this.mg.ownerID(neighbor));
|
||||
}
|
||||
return clusters
|
||||
}
|
||||
}
|
||||
const mode = getMode(result);
|
||||
if (!this.mg.playerBySmallID(mode).isPlayer()) {
|
||||
return;
|
||||
}
|
||||
const firstTile = cluster.values().next().value;
|
||||
const filter = (_, t: TileRef): boolean =>
|
||||
this.mg.ownerID(t) == this.mg.ownerID(firstTile);
|
||||
const tiles = this.mg.bfs(firstTile, filter);
|
||||
|
||||
owner(): Player {
|
||||
return this.player
|
||||
const modePlayer = this.mg.playerBySmallID(mode);
|
||||
if (!modePlayer.isPlayer()) {
|
||||
consolex.warn("mode player is null");
|
||||
return;
|
||||
}
|
||||
for (const tile of tiles) {
|
||||
(modePlayer as Player).conquer(tile);
|
||||
}
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active
|
||||
private calculateClusters(): Set<TileRef>[] {
|
||||
const seen = new Set<TileRef>();
|
||||
const border = this.player.borderTiles();
|
||||
const clusters: Set<TileRef>[] = [];
|
||||
for (const tile of border) {
|
||||
if (seen.has(tile)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const cluster = new Set<TileRef>();
|
||||
const queue: TileRef[] = [tile];
|
||||
seen.add(tile);
|
||||
while (queue.length > 0) {
|
||||
const curr = queue.shift();
|
||||
cluster.add(curr);
|
||||
|
||||
const neighbors = (this.mg as GameImpl).neighborsWithDiag(curr);
|
||||
for (const neighbor of neighbors) {
|
||||
if (border.has(neighbor) && !seen.has(neighbor)) {
|
||||
queue.push(neighbor);
|
||||
seen.add(neighbor);
|
||||
}
|
||||
}
|
||||
}
|
||||
clusters.push(cluster);
|
||||
}
|
||||
}
|
||||
return clusters;
|
||||
}
|
||||
|
||||
owner(): Player {
|
||||
return this.player;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
}
|
||||
|
||||
+123
-106
@@ -1,4 +1,14 @@
|
||||
import { AllPlayers, Cell, Execution, Game, Player, Unit, PlayerID, TerrainType, UnitType } from "../game/Game";
|
||||
import {
|
||||
AllPlayers,
|
||||
Cell,
|
||||
Execution,
|
||||
Game,
|
||||
Player,
|
||||
Unit,
|
||||
PlayerID,
|
||||
TerrainType,
|
||||
UnitType,
|
||||
} from "../game/Game";
|
||||
import { PathFinder } from "../pathfinding/PathFinding";
|
||||
import { PathFindResultType } from "../pathfinding/AStar";
|
||||
import { PseudoRandom } from "../PseudoRandom";
|
||||
@@ -8,126 +18,133 @@ import { MiniAStar } from "../pathfinding/MiniAStar";
|
||||
import { manhattanDistFN, TileRef } from "../game/GameMap";
|
||||
|
||||
export class PortExecution implements Execution {
|
||||
private active = true;
|
||||
private mg: Game;
|
||||
private port: Unit;
|
||||
private random: PseudoRandom;
|
||||
private portPaths = new Map<Unit, TileRef[]>();
|
||||
private computingPaths = new Map<Unit, MiniAStar>();
|
||||
|
||||
private active = true
|
||||
private mg: Game
|
||||
private port: Unit
|
||||
private random: PseudoRandom
|
||||
private portPaths = new Map<Unit, TileRef[]>()
|
||||
private computingPaths = new Map<Unit, MiniAStar>()
|
||||
constructor(
|
||||
private _owner: PlayerID,
|
||||
private tile: TileRef,
|
||||
) {}
|
||||
|
||||
constructor(
|
||||
private _owner: PlayerID,
|
||||
private tile: TileRef,
|
||||
) { }
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.mg = mg;
|
||||
this.random = new PseudoRandom(mg.ticks());
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
if (this.port == null) {
|
||||
// TODO: use canBuild
|
||||
const tile = this.tile;
|
||||
const player = this.mg.player(this._owner);
|
||||
if (!player.canBuild(UnitType.Port, tile)) {
|
||||
consolex.warn(`player ${player} cannot build port at ${this.tile}`);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
const spawns = Array.from(this.mg.bfs(tile, manhattanDistFN(tile, 20)))
|
||||
.filter((t) => this.mg.isOceanShore(t) && this.mg.owner(t) == player)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
this.mg.manhattanDist(a, tile) - this.mg.manhattanDist(b, tile),
|
||||
);
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.mg = mg
|
||||
this.random = new PseudoRandom(mg.ticks())
|
||||
if (spawns.length == 0) {
|
||||
consolex.warn(`cannot find spawn for port`);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
this.port = player.buildUnit(UnitType.Port, 0, spawns[0]);
|
||||
}
|
||||
if (!this.port.isActive()) {
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
const alliedPorts = this.player()
|
||||
.alliances()
|
||||
.map((a) => a.other(this.player()))
|
||||
.flatMap((p) => p.units(UnitType.Port));
|
||||
const alliedPortsSet = new Set(alliedPorts);
|
||||
|
||||
if (this.port == null) {
|
||||
// TODO: use canBuild
|
||||
const tile = this.tile
|
||||
const player = this.mg.player(this._owner)
|
||||
if (!player.canBuild(UnitType.Port, tile)) {
|
||||
consolex.warn(`player ${player} cannot build port at ${this.tile}`)
|
||||
this.active = false
|
||||
return
|
||||
}
|
||||
const spawns = Array.from(this.mg.bfs(tile, manhattanDistFN(tile, 20)))
|
||||
.filter(t => this.mg.isOceanShore(t) && this.mg.owner(t) == player)
|
||||
.sort((a, b) => this.mg.manhattanDist(a, tile) - this.mg.manhattanDist(b, tile))
|
||||
const allyConnections = new Set(
|
||||
Array.from(this.portPaths.keys()).map((p) => p.owner()),
|
||||
);
|
||||
allyConnections;
|
||||
|
||||
if (spawns.length == 0) {
|
||||
consolex.warn(`cannot find spawn for port`)
|
||||
this.active = false
|
||||
return
|
||||
}
|
||||
this.port = player.buildUnit(UnitType.Port, 0, spawns[0])
|
||||
}
|
||||
if (!this.port.isActive()) {
|
||||
this.active = false
|
||||
return
|
||||
for (const port of alliedPorts) {
|
||||
if (allyConnections.has(port.owner())) {
|
||||
continue;
|
||||
}
|
||||
allyConnections.add(port.owner());
|
||||
if (this.computingPaths.has(port)) {
|
||||
const aStar = this.computingPaths.get(port);
|
||||
switch (aStar.compute()) {
|
||||
case PathFindResultType.Completed:
|
||||
this.portPaths.set(port, aStar.reconstructPath());
|
||||
this.computingPaths.delete(port);
|
||||
break;
|
||||
case PathFindResultType.Pending:
|
||||
break;
|
||||
case PathFindResultType.PathNotFound:
|
||||
consolex.warn(`path not found to port`);
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
const alliedPorts = this.player().alliances().map(a => a.other(this.player())).flatMap(p => p.units(UnitType.Port))
|
||||
const alliedPortsSet = new Set(alliedPorts)
|
||||
|
||||
const allyConnections = new Set(Array.from(this.portPaths.keys()).map(p => p.owner()))
|
||||
allyConnections
|
||||
|
||||
|
||||
for (const port of alliedPorts) {
|
||||
if (allyConnections.has(port.owner())) {
|
||||
continue
|
||||
}
|
||||
allyConnections.add(port.owner())
|
||||
if (this.computingPaths.has(port)) {
|
||||
const aStar = this.computingPaths.get(port)
|
||||
switch (aStar.compute()) {
|
||||
case PathFindResultType.Completed:
|
||||
this.portPaths.set(port, aStar.reconstructPath())
|
||||
this.computingPaths.delete(port)
|
||||
break
|
||||
case PathFindResultType.Pending:
|
||||
break
|
||||
case PathFindResultType.PathNotFound:
|
||||
consolex.warn(`path not found to port`)
|
||||
break
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
const pf = new MiniAStar(
|
||||
this.mg.map(),
|
||||
this.mg.miniMap(),
|
||||
this.port.tile(),
|
||||
port.tile(),
|
||||
(tr: TileRef) => this.mg.miniMap().isOcean(tr),
|
||||
10_000,
|
||||
25
|
||||
)
|
||||
this.computingPaths.set(port, pf)
|
||||
}
|
||||
|
||||
for (const port of this.portPaths.keys()) {
|
||||
if (!port.isActive() || !alliedPortsSet.has(port)) {
|
||||
this.portPaths.delete(port)
|
||||
this.computingPaths.delete(port)
|
||||
}
|
||||
}
|
||||
|
||||
const portConnections = Array.from(this.portPaths.keys())
|
||||
|
||||
if (portConnections.length > 0 && this.random.chance(this.mg.config().tradeShipSpawnRate())) {
|
||||
const port = this.random.randElement(portConnections)
|
||||
const path = this.portPaths.get(port)
|
||||
if (path != null) {
|
||||
const pf = PathFinder.Mini(this.mg, 10, false)
|
||||
this.mg.addExecution(new TradeShipExecution(this.player().id(), this.port, port, pf, path))
|
||||
}
|
||||
}
|
||||
const pf = new MiniAStar(
|
||||
this.mg.map(),
|
||||
this.mg.miniMap(),
|
||||
this.port.tile(),
|
||||
port.tile(),
|
||||
(tr: TileRef) => this.mg.miniMap().isOcean(tr),
|
||||
10_000,
|
||||
25,
|
||||
);
|
||||
this.computingPaths.set(port, pf);
|
||||
}
|
||||
|
||||
owner(): Player {
|
||||
return null
|
||||
for (const port of this.portPaths.keys()) {
|
||||
if (!port.isActive() || !alliedPortsSet.has(port)) {
|
||||
this.portPaths.delete(port);
|
||||
this.computingPaths.delete(port);
|
||||
}
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active
|
||||
}
|
||||
const portConnections = Array.from(this.portPaths.keys());
|
||||
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false
|
||||
if (
|
||||
portConnections.length > 0 &&
|
||||
this.random.chance(this.mg.config().tradeShipSpawnRate())
|
||||
) {
|
||||
const port = this.random.randElement(portConnections);
|
||||
const path = this.portPaths.get(port);
|
||||
if (path != null) {
|
||||
const pf = PathFinder.Mini(this.mg, 10, false);
|
||||
this.mg.addExecution(
|
||||
new TradeShipExecution(this.player().id(), this.port, port, pf, path),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
player(): Player {
|
||||
return this.port.owner()
|
||||
}
|
||||
owner(): Player {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
player(): Player {
|
||||
return this.port.owner();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,37 +2,39 @@ import { consolex } from "../Consolex";
|
||||
import { Execution, Game, Player, PlayerID } from "../game/Game";
|
||||
|
||||
export class SetTargetTroopRatioExecution implements Execution {
|
||||
private player: Player;
|
||||
|
||||
private player: Player
|
||||
private active = true;
|
||||
|
||||
private active = true
|
||||
constructor(
|
||||
private playerID: PlayerID,
|
||||
private targetTroopsRatio: number,
|
||||
) {}
|
||||
|
||||
constructor(private playerID: PlayerID, private targetTroopsRatio: number) { }
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.player = mg.player(this.playerID);
|
||||
}
|
||||
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.player = mg.player(this.playerID)
|
||||
tick(ticks: number): void {
|
||||
if (this.targetTroopsRatio < 0 || this.targetTroopsRatio > 1) {
|
||||
consolex.warn(
|
||||
`target troop ratio of ${this.targetTroopsRatio} for player ${this.player} invalid`,
|
||||
);
|
||||
} else {
|
||||
this.player.setTargetTroopRatio(this.targetTroopsRatio);
|
||||
}
|
||||
this.active = false;
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
if (this.targetTroopsRatio < 0 || this.targetTroopsRatio > 1) {
|
||||
consolex.warn(`target troop ratio of ${this.targetTroopsRatio} for player ${this.player} invalid`)
|
||||
} else {
|
||||
this.player.setTargetTroopRatio(this.targetTroopsRatio)
|
||||
}
|
||||
this.active = false
|
||||
}
|
||||
owner(): Player {
|
||||
return null;
|
||||
}
|
||||
|
||||
owner(): Player {
|
||||
return null
|
||||
}
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active
|
||||
}
|
||||
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,62 +5,71 @@ import { consolex } from "../Consolex";
|
||||
import { TileRef } from "../game/GameMap";
|
||||
|
||||
export class ShellExecution implements Execution {
|
||||
private active = true;
|
||||
private pathFinder: PathFinder;
|
||||
private shell: Unit;
|
||||
|
||||
private active = true
|
||||
private pathFinder: PathFinder
|
||||
private shell: Unit
|
||||
constructor(
|
||||
private spawn: TileRef,
|
||||
private _owner: Player,
|
||||
private ownerUnit: Unit,
|
||||
private target: Unit,
|
||||
) {}
|
||||
|
||||
constructor(private spawn: TileRef, private _owner: Player, private ownerUnit: Unit, private target: Unit) {
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.pathFinder = PathFinder.Mini(mg, 2000, true, 10);
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
if (this.shell == null) {
|
||||
this.shell = this._owner.buildUnit(UnitType.Shell, 0, this.spawn);
|
||||
}
|
||||
if (!this.shell.isActive()) {
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!this.target.isActive() ||
|
||||
!this.ownerUnit.isActive() ||
|
||||
this.target.owner() == this.shell.owner()
|
||||
) {
|
||||
this.shell.delete(false);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const result = this.pathFinder.nextTile(
|
||||
this.shell.tile(),
|
||||
this.target.tile(),
|
||||
3,
|
||||
);
|
||||
switch (result.type) {
|
||||
case PathFindResultType.Completed:
|
||||
this.active = false;
|
||||
this.target.modifyHealth(-this.shell.info().damage);
|
||||
this.shell.delete(false);
|
||||
return;
|
||||
case PathFindResultType.NextTile:
|
||||
this.shell.move(result.tile);
|
||||
break;
|
||||
case PathFindResultType.Pending:
|
||||
return;
|
||||
case PathFindResultType.PathNotFound:
|
||||
consolex.log(`Shell ${this.shell} could not find target`);
|
||||
this.active = false;
|
||||
this.shell.delete(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.pathFinder = PathFinder.Mini(mg, 2000, true, 10)
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
if (this.shell == null) {
|
||||
this.shell = this._owner.buildUnit(UnitType.Shell, 0, this.spawn)
|
||||
}
|
||||
if (!this.shell.isActive()) {
|
||||
this.active = false
|
||||
return
|
||||
}
|
||||
if (!this.target.isActive() || !this.ownerUnit.isActive() || this.target.owner() == this.shell.owner()) {
|
||||
this.shell.delete(false)
|
||||
this.active = false
|
||||
return
|
||||
}
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const result = this.pathFinder.nextTile(this.shell.tile(), this.target.tile(), 3)
|
||||
switch (result.type) {
|
||||
case PathFindResultType.Completed:
|
||||
this.active = false
|
||||
this.target.modifyHealth(-this.shell.info().damage)
|
||||
this.shell.delete(false)
|
||||
return
|
||||
case PathFindResultType.NextTile:
|
||||
this.shell.move(result.tile)
|
||||
break
|
||||
case PathFindResultType.Pending:
|
||||
return
|
||||
case PathFindResultType.PathNotFound:
|
||||
consolex.log(`Shell ${this.shell} could not find target`)
|
||||
this.active = false
|
||||
this.shell.delete(false)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
owner(): Player {
|
||||
return null
|
||||
}
|
||||
isActive(): boolean {
|
||||
return this.active
|
||||
}
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
owner(): Player {
|
||||
return null;
|
||||
}
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,58 +1,68 @@
|
||||
import { Cell, Execution, Game, Player, PlayerInfo, PlayerType } from "../game/Game"
|
||||
import { TileRef } from "../game/GameMap"
|
||||
import { BotExecution } from "./BotExecution"
|
||||
import { PlayerExecution } from "./PlayerExecution"
|
||||
import { getSpawnTiles } from "./Util"
|
||||
import {
|
||||
Cell,
|
||||
Execution,
|
||||
Game,
|
||||
Player,
|
||||
PlayerInfo,
|
||||
PlayerType,
|
||||
} from "../game/Game";
|
||||
import { TileRef } from "../game/GameMap";
|
||||
import { BotExecution } from "./BotExecution";
|
||||
import { PlayerExecution } from "./PlayerExecution";
|
||||
import { getSpawnTiles } from "./Util";
|
||||
|
||||
export class SpawnExecution implements Execution {
|
||||
active: boolean = true;
|
||||
private mg: Game;
|
||||
|
||||
active: boolean = true
|
||||
private mg: Game
|
||||
constructor(
|
||||
private playerInfo: PlayerInfo,
|
||||
private tile: TileRef,
|
||||
) {}
|
||||
|
||||
constructor(
|
||||
private playerInfo: PlayerInfo,
|
||||
private tile: TileRef
|
||||
) { }
|
||||
init(mg: Game, ticks: number) {
|
||||
this.mg = mg;
|
||||
}
|
||||
|
||||
init(mg: Game, ticks: number) {
|
||||
this.mg = mg
|
||||
tick(ticks: number) {
|
||||
this.active = false;
|
||||
|
||||
if (!this.mg.inSpawnPhase()) {
|
||||
return;
|
||||
}
|
||||
|
||||
tick(ticks: number) {
|
||||
this.active = false
|
||||
|
||||
if (!this.mg.inSpawnPhase()) {
|
||||
return
|
||||
}
|
||||
|
||||
const existing = this.mg.players().find(p => p.id() == this.playerInfo.id)
|
||||
if (existing) {
|
||||
existing.tiles().forEach(t => existing.relinquish(t))
|
||||
getSpawnTiles(this.mg, this.tile).forEach(t => {
|
||||
existing.conquer(t)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const player = this.mg.addPlayer(this.playerInfo, this.mg.config().startManpower(this.playerInfo))
|
||||
getSpawnTiles(this.mg, this.tile).forEach(t => {
|
||||
player.conquer(t)
|
||||
})
|
||||
this.mg.addExecution(new PlayerExecution(player.id()))
|
||||
if (player.type() == PlayerType.Bot) {
|
||||
this.mg.addExecution(new BotExecution(player))
|
||||
}
|
||||
const existing = this.mg
|
||||
.players()
|
||||
.find((p) => p.id() == this.playerInfo.id);
|
||||
if (existing) {
|
||||
existing.tiles().forEach((t) => existing.relinquish(t));
|
||||
getSpawnTiles(this.mg, this.tile).forEach((t) => {
|
||||
existing.conquer(t);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
owner(): Player {
|
||||
return null
|
||||
}
|
||||
isActive(): boolean {
|
||||
return this.active
|
||||
const player = this.mg.addPlayer(
|
||||
this.playerInfo,
|
||||
this.mg.config().startManpower(this.playerInfo),
|
||||
);
|
||||
getSpawnTiles(this.mg, this.tile).forEach((t) => {
|
||||
player.conquer(t);
|
||||
});
|
||||
this.mg.addExecution(new PlayerExecution(player.id()));
|
||||
if (player.type() == PlayerType.Bot) {
|
||||
this.mg.addExecution(new BotExecution(player));
|
||||
}
|
||||
}
|
||||
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return true
|
||||
}
|
||||
owner(): Player {
|
||||
return null;
|
||||
}
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
}
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
import { Execution, Game, Player, PlayerID } from "../game/Game";
|
||||
|
||||
export class TargetPlayerExecution implements Execution {
|
||||
private requestor: Player;
|
||||
private target: Player;
|
||||
|
||||
private requestor: Player
|
||||
private target: Player
|
||||
private active = true;
|
||||
|
||||
private active = true
|
||||
constructor(
|
||||
private requestorID: PlayerID,
|
||||
private targetID: PlayerID,
|
||||
) {}
|
||||
|
||||
constructor(private requestorID: PlayerID, private targetID: PlayerID) { }
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.requestor = mg.player(this.requestorID);
|
||||
this.target = mg.player(this.targetID);
|
||||
}
|
||||
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.requestor = mg.player(this.requestorID)
|
||||
this.target = mg.player(this.targetID)
|
||||
tick(ticks: number): void {
|
||||
if (this.requestor.canTarget(this.target)) {
|
||||
this.requestor.target(this.target);
|
||||
this.target.updateRelation(this.requestor, -40);
|
||||
}
|
||||
this.active = false;
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
if (this.requestor.canTarget(this.target)) {
|
||||
this.requestor.target(this.target)
|
||||
this.target.updateRelation(this.requestor, -40)
|
||||
}
|
||||
this.active = false
|
||||
}
|
||||
owner(): Player {
|
||||
return null;
|
||||
}
|
||||
|
||||
owner(): Player {
|
||||
return null
|
||||
}
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active
|
||||
}
|
||||
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,131 +1,157 @@
|
||||
import { MessageType } from '../game/Game';
|
||||
import { MessageType } from "../game/Game";
|
||||
import { renderNumber } from "../../client/Utils";
|
||||
import { AllPlayers, Cell, Execution, Game, Unit, Player, PlayerID, UnitType } from "../game/Game";
|
||||
import {
|
||||
AllPlayers,
|
||||
Cell,
|
||||
Execution,
|
||||
Game,
|
||||
Unit,
|
||||
Player,
|
||||
PlayerID,
|
||||
UnitType,
|
||||
} from "../game/Game";
|
||||
import { PathFinder } from "../pathfinding/PathFinding";
|
||||
import { PathFindResultType } from "../pathfinding/AStar";
|
||||
import { distSortUnit } from "../Util";
|
||||
import { consolex } from "../Consolex";
|
||||
import { TileRef } from '../game/GameMap';
|
||||
import { TileRef } from "../game/GameMap";
|
||||
|
||||
export class TradeShipExecution implements Execution {
|
||||
private active = true;
|
||||
private mg: Game;
|
||||
private origOwner: Player;
|
||||
private tradeShip: Unit;
|
||||
private index = 0;
|
||||
private wasCaptured = false;
|
||||
|
||||
private active = true
|
||||
private mg: Game
|
||||
private origOwner: Player
|
||||
private tradeShip: Unit
|
||||
private index = 0
|
||||
private wasCaptured = false
|
||||
constructor(
|
||||
private _owner: PlayerID,
|
||||
private srcPort: Unit,
|
||||
private dstPort: Unit,
|
||||
private pathFinder: PathFinder,
|
||||
// don't modify
|
||||
private path: TileRef[],
|
||||
) {}
|
||||
|
||||
constructor(
|
||||
private _owner: PlayerID,
|
||||
private srcPort: Unit,
|
||||
private dstPort: Unit,
|
||||
private pathFinder: PathFinder,
|
||||
// don't modify
|
||||
private path: TileRef[]
|
||||
) { }
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.mg = mg;
|
||||
this.origOwner = mg.player(this._owner);
|
||||
}
|
||||
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.mg = mg
|
||||
this.origOwner = mg.player(this._owner)
|
||||
tick(ticks: number): void {
|
||||
if (this.tradeShip == null) {
|
||||
const spawn = this.origOwner.canBuild(
|
||||
UnitType.TradeShip,
|
||||
this.srcPort.tile(),
|
||||
);
|
||||
if (spawn == false) {
|
||||
consolex.warn(`cannot build trade ship`);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
this.tradeShip = this.origOwner.buildUnit(UnitType.TradeShip, 0, spawn);
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
if (this.tradeShip == null) {
|
||||
const spawn = this.origOwner.canBuild(UnitType.TradeShip, this.srcPort.tile())
|
||||
if (spawn == false) {
|
||||
consolex.warn(`cannot build trade ship`)
|
||||
this.active = false
|
||||
return
|
||||
}
|
||||
this.tradeShip = this.origOwner.buildUnit(UnitType.TradeShip, 0, spawn)
|
||||
}
|
||||
|
||||
if (!this.tradeShip.isActive()) {
|
||||
this.active = false
|
||||
return
|
||||
}
|
||||
|
||||
if (this.origOwner != this.tradeShip.owner()) {
|
||||
// Store as vairable in case ship is recaptured by previous owner
|
||||
this.wasCaptured = true
|
||||
}
|
||||
|
||||
if (!this.wasCaptured && (!this.dstPort.isActive() || !this.tradeShip.owner().isAlliedWith(this.dstPort.owner()))) {
|
||||
this.tradeShip.delete(false)
|
||||
this.active = false
|
||||
return
|
||||
}
|
||||
|
||||
if (this.wasCaptured) {
|
||||
const ports = this.tradeShip.owner().units(UnitType.Port).sort(distSortUnit(this.mg, this.tradeShip))
|
||||
if (ports.length == 0) {
|
||||
this.tradeShip.delete(false)
|
||||
this.active = false
|
||||
return
|
||||
}
|
||||
const dstPort = ports[0]
|
||||
const result = this.pathFinder.nextTile(this.tradeShip.tile(), dstPort.tile())
|
||||
switch (result.type) {
|
||||
case PathFindResultType.Completed:
|
||||
const gold = this.mg.config().tradeShipGold(this.mg.manhattanDist(this.srcPort.tile(), dstPort.tile()))
|
||||
this.tradeShip.owner().addGold(gold)
|
||||
this.mg.displayMessage(
|
||||
`Received ${renderNumber(gold)} gold from ship captured from ${this.origOwner.displayName()}`,
|
||||
MessageType.SUCCESS,
|
||||
this.tradeShip.owner().id()
|
||||
)
|
||||
this.tradeShip.delete(false)
|
||||
break
|
||||
case PathFindResultType.Pending:
|
||||
// Fire unit event to rerender.
|
||||
this.tradeShip.move(this.tradeShip.tile())
|
||||
break
|
||||
case PathFindResultType.NextTile:
|
||||
this.tradeShip.move(result.tile)
|
||||
break
|
||||
case PathFindResultType.PathNotFound:
|
||||
consolex.warn('captured trade ship cannot find route')
|
||||
this.active = false
|
||||
break
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (this.index >= this.path.length) {
|
||||
this.active = false
|
||||
const gold = this.mg.config().tradeShipGold(this.mg.manhattanDist(this.srcPort.tile(), this.dstPort.tile()))
|
||||
this.srcPort.owner().addGold(gold)
|
||||
this.dstPort.owner().addGold(gold)
|
||||
this.mg.displayMessage(
|
||||
`Received ${renderNumber(gold)} gold from trade with ${this.srcPort.owner().displayName()}`,
|
||||
MessageType.SUCCESS,
|
||||
this.dstPort.owner().id()
|
||||
)
|
||||
this.mg.displayMessage(
|
||||
`Received ${renderNumber(gold)} gold from trade with ${this.dstPort.owner().displayName()}`,
|
||||
MessageType.SUCCESS,
|
||||
this.srcPort.owner().id()
|
||||
)
|
||||
this.tradeShip.delete(false)
|
||||
return
|
||||
}
|
||||
this.tradeShip.move(this.path[this.index])
|
||||
this.index++
|
||||
if (!this.tradeShip.isActive()) {
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
owner(): Player {
|
||||
return null
|
||||
if (this.origOwner != this.tradeShip.owner()) {
|
||||
// Store as vairable in case ship is recaptured by previous owner
|
||||
this.wasCaptured = true;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active
|
||||
if (
|
||||
!this.wasCaptured &&
|
||||
(!this.dstPort.isActive() ||
|
||||
!this.tradeShip.owner().isAlliedWith(this.dstPort.owner()))
|
||||
) {
|
||||
this.tradeShip.delete(false);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false
|
||||
if (this.wasCaptured) {
|
||||
const ports = this.tradeShip
|
||||
.owner()
|
||||
.units(UnitType.Port)
|
||||
.sort(distSortUnit(this.mg, this.tradeShip));
|
||||
if (ports.length == 0) {
|
||||
this.tradeShip.delete(false);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
const dstPort = ports[0];
|
||||
const result = this.pathFinder.nextTile(
|
||||
this.tradeShip.tile(),
|
||||
dstPort.tile(),
|
||||
);
|
||||
switch (result.type) {
|
||||
case PathFindResultType.Completed:
|
||||
const gold = this.mg
|
||||
.config()
|
||||
.tradeShipGold(
|
||||
this.mg.manhattanDist(this.srcPort.tile(), dstPort.tile()),
|
||||
);
|
||||
this.tradeShip.owner().addGold(gold);
|
||||
this.mg.displayMessage(
|
||||
`Received ${renderNumber(gold)} gold from ship captured from ${this.origOwner.displayName()}`,
|
||||
MessageType.SUCCESS,
|
||||
this.tradeShip.owner().id(),
|
||||
);
|
||||
this.tradeShip.delete(false);
|
||||
break;
|
||||
case PathFindResultType.Pending:
|
||||
// Fire unit event to rerender.
|
||||
this.tradeShip.move(this.tradeShip.tile());
|
||||
break;
|
||||
case PathFindResultType.NextTile:
|
||||
this.tradeShip.move(result.tile);
|
||||
break;
|
||||
case PathFindResultType.PathNotFound:
|
||||
consolex.warn("captured trade ship cannot find route");
|
||||
this.active = false;
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
if (this.index >= this.path.length) {
|
||||
this.active = false;
|
||||
const gold = this.mg
|
||||
.config()
|
||||
.tradeShipGold(
|
||||
this.mg.manhattanDist(this.srcPort.tile(), this.dstPort.tile()),
|
||||
);
|
||||
this.srcPort.owner().addGold(gold);
|
||||
this.dstPort.owner().addGold(gold);
|
||||
this.mg.displayMessage(
|
||||
`Received ${renderNumber(gold)} gold from trade with ${this.srcPort.owner().displayName()}`,
|
||||
MessageType.SUCCESS,
|
||||
this.dstPort.owner().id(),
|
||||
);
|
||||
this.mg.displayMessage(
|
||||
`Received ${renderNumber(gold)} gold from trade with ${this.dstPort.owner().displayName()}`,
|
||||
MessageType.SUCCESS,
|
||||
this.srcPort.owner().id(),
|
||||
);
|
||||
this.tradeShip.delete(false);
|
||||
return;
|
||||
}
|
||||
this.tradeShip.move(this.path[this.index]);
|
||||
this.index++;
|
||||
}
|
||||
|
||||
owner(): Player {
|
||||
return null;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import { Unit, Cell, Execution, Game, Player, PlayerID, TerraNullius, UnitType, TerrainType } from "../game/Game";
|
||||
import {
|
||||
Unit,
|
||||
Cell,
|
||||
Execution,
|
||||
Game,
|
||||
Player,
|
||||
PlayerID,
|
||||
TerraNullius,
|
||||
UnitType,
|
||||
TerrainType,
|
||||
} from "../game/Game";
|
||||
import { AttackExecution } from "./AttackExecution";
|
||||
import { MessageType } from '../game/Game';
|
||||
import { MessageType } from "../game/Game";
|
||||
import { PathFinder } from "../pathfinding/PathFinding";
|
||||
import { PathFindResultType } from "../pathfinding/AStar";
|
||||
import { consolex } from "../Consolex";
|
||||
@@ -8,138 +18,154 @@ import { TileRef } from "../game/GameMap";
|
||||
import { targetTransportTile } from "../Util";
|
||||
|
||||
export class TransportShipExecution implements Execution {
|
||||
private lastMove: number;
|
||||
|
||||
private lastMove: number
|
||||
// TODO: make this configurable
|
||||
private ticksPerMove = 1;
|
||||
|
||||
// TODO: make this configurable
|
||||
private ticksPerMove = 1
|
||||
private active = true;
|
||||
|
||||
private active = true
|
||||
private mg: Game;
|
||||
private attacker: Player;
|
||||
private target: Player | TerraNullius;
|
||||
|
||||
private mg: Game
|
||||
private attacker: Player
|
||||
private target: Player | TerraNullius
|
||||
// TODO make private
|
||||
public path: TileRef[];
|
||||
private src: TileRef | null;
|
||||
private dst: TileRef | null;
|
||||
|
||||
// TODO make private
|
||||
public path: TileRef[]
|
||||
private src: TileRef | null
|
||||
private dst: TileRef | null
|
||||
private boat: Unit;
|
||||
|
||||
private pathFinder: PathFinder;
|
||||
|
||||
private boat: Unit
|
||||
constructor(
|
||||
private attackerID: PlayerID,
|
||||
private targetID: PlayerID | null,
|
||||
private ref: TileRef,
|
||||
private troops: number | null,
|
||||
) {}
|
||||
|
||||
private pathFinder: PathFinder
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private attackerID: PlayerID,
|
||||
private targetID: PlayerID | null,
|
||||
private ref: TileRef,
|
||||
private troops: number | null,
|
||||
) { }
|
||||
init(mg: Game, ticks: number) {
|
||||
this.lastMove = ticks;
|
||||
this.mg = mg;
|
||||
this.pathFinder = PathFinder.Mini(mg, 10_000, false, 2);
|
||||
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false
|
||||
this.attacker = mg.player(this.attackerID);
|
||||
|
||||
if (
|
||||
this.attacker.units(UnitType.TransportShip).length >=
|
||||
mg.config().boatMaxNumber()
|
||||
) {
|
||||
mg.displayMessage(
|
||||
`No boats available, max ${mg.config().boatMaxNumber()}`,
|
||||
MessageType.WARN,
|
||||
this.attackerID,
|
||||
);
|
||||
this.active = false;
|
||||
this.attacker.addTroops(this.troops);
|
||||
return;
|
||||
}
|
||||
|
||||
init(mg: Game, ticks: number) {
|
||||
this.lastMove = ticks
|
||||
this.mg = mg
|
||||
this.pathFinder = PathFinder.Mini(mg, 10_000, false, 2)
|
||||
if (this.targetID == null || this.targetID == this.mg.terraNullius().id()) {
|
||||
this.target = mg.terraNullius();
|
||||
} else {
|
||||
this.target = mg.player(this.targetID);
|
||||
}
|
||||
|
||||
this.attacker = mg.player(this.attackerID)
|
||||
if (this.troops == null) {
|
||||
this.troops = this.mg
|
||||
.config()
|
||||
.boatAttackAmount(this.attacker, this.target);
|
||||
}
|
||||
|
||||
if (this.attacker.units(UnitType.TransportShip).length >= mg.config().boatMaxNumber()) {
|
||||
mg.displayMessage(`No boats available, max ${mg.config().boatMaxNumber()}`, MessageType.WARN, this.attackerID)
|
||||
this.active = false
|
||||
this.attacker.addTroops(this.troops)
|
||||
return
|
||||
this.troops = Math.min(this.troops, this.attacker.troops());
|
||||
|
||||
this.dst = targetTransportTile(this.mg, this.ref);
|
||||
if (this.dst == null) {
|
||||
consolex.warn(
|
||||
`${this.attacker} cannot send ship to ${this.target}, cannot find attack tile`,
|
||||
);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
const src = this.attacker.canBuild(UnitType.TransportShip, this.dst);
|
||||
if (src == false) {
|
||||
consolex.warn(`can't build transport ship`);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.src = src;
|
||||
|
||||
this.boat = this.attacker.buildUnit(
|
||||
UnitType.TransportShip,
|
||||
this.troops,
|
||||
this.src,
|
||||
);
|
||||
}
|
||||
|
||||
tick(ticks: number) {
|
||||
if (!this.active) {
|
||||
return;
|
||||
}
|
||||
if (!this.boat.isActive()) {
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
if (ticks - this.lastMove < this.ticksPerMove) {
|
||||
return;
|
||||
}
|
||||
this.lastMove = ticks;
|
||||
|
||||
const result = this.pathFinder.nextTile(this.boat.tile(), this.dst);
|
||||
switch (result.type) {
|
||||
case PathFindResultType.Completed:
|
||||
if (this.mg.owner(this.dst) == this.attacker) {
|
||||
this.attacker.addTroops(this.troops);
|
||||
this.boat.delete(false);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.targetID == null || this.targetID == this.mg.terraNullius().id()) {
|
||||
this.target = mg.terraNullius()
|
||||
if (this.target.isPlayer() && this.attacker.isAlliedWith(this.target)) {
|
||||
this.target.addTroops(this.troops);
|
||||
} else {
|
||||
this.target = mg.player(this.targetID)
|
||||
this.attacker.conquer(this.dst);
|
||||
this.mg.addExecution(
|
||||
new AttackExecution(
|
||||
this.troops,
|
||||
this.attacker.id(),
|
||||
this.targetID,
|
||||
this.dst,
|
||||
false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (this.troops == null) {
|
||||
this.troops = this.mg.config().boatAttackAmount(this.attacker, this.target)
|
||||
}
|
||||
|
||||
this.troops = Math.min(this.troops, this.attacker.troops())
|
||||
|
||||
this.dst = targetTransportTile(this.mg, this.ref)
|
||||
if (this.dst == null) {
|
||||
consolex.warn(`${this.attacker} cannot send ship to ${this.target}, cannot find attack tile`)
|
||||
this.active = false
|
||||
return
|
||||
}
|
||||
const src = this.attacker.canBuild(UnitType.TransportShip, this.dst)
|
||||
if (src == false) {
|
||||
consolex.warn(`can't build transport ship`)
|
||||
this.active = false
|
||||
return
|
||||
}
|
||||
|
||||
this.src = src
|
||||
|
||||
this.boat = this.attacker.buildUnit(UnitType.TransportShip, this.troops, this.src)
|
||||
this.boat.delete(false);
|
||||
this.active = false;
|
||||
return;
|
||||
case PathFindResultType.NextTile:
|
||||
this.boat.move(result.tile);
|
||||
break;
|
||||
case PathFindResultType.Pending:
|
||||
break;
|
||||
case PathFindResultType.PathNotFound:
|
||||
// TODO: add to poisoned port list
|
||||
consolex.warn(`path not found tot dst`);
|
||||
this.boat.delete(false);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
tick(ticks: number) {
|
||||
if (!this.active) {
|
||||
return
|
||||
}
|
||||
if (!this.boat.isActive()) {
|
||||
this.active = false
|
||||
return
|
||||
}
|
||||
if (ticks - this.lastMove < this.ticksPerMove) {
|
||||
return
|
||||
}
|
||||
this.lastMove = ticks
|
||||
|
||||
const result = this.pathFinder.nextTile(this.boat.tile(), this.dst)
|
||||
switch (result.type) {
|
||||
case PathFindResultType.Completed:
|
||||
if (this.mg.owner(this.dst) == this.attacker) {
|
||||
this.attacker.addTroops(this.troops)
|
||||
this.boat.delete(false)
|
||||
this.active = false
|
||||
return
|
||||
}
|
||||
if (this.target.isPlayer() && this.attacker.isAlliedWith(this.target)) {
|
||||
this.target.addTroops(this.troops)
|
||||
} else {
|
||||
this.attacker.conquer(this.dst)
|
||||
this.mg.addExecution(
|
||||
new AttackExecution(this.troops, this.attacker.id(), this.targetID, this.dst, false)
|
||||
)
|
||||
}
|
||||
this.boat.delete(false)
|
||||
this.active = false
|
||||
return
|
||||
case PathFindResultType.NextTile:
|
||||
this.boat.move(result.tile)
|
||||
break
|
||||
case PathFindResultType.Pending:
|
||||
break
|
||||
case PathFindResultType.PathNotFound:
|
||||
// TODO: add to poisoned port list
|
||||
consolex.warn(`path not found tot dst`)
|
||||
this.boat.delete(false)
|
||||
this.active = false
|
||||
return
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
owner(): Player {
|
||||
return this.attacker
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active
|
||||
}
|
||||
owner(): Player {
|
||||
return this.attacker;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+46
-42
@@ -1,52 +1,56 @@
|
||||
import { euclDistFN, GameMap, TileRef } from "../game/GameMap";
|
||||
|
||||
|
||||
export function getSpawnTiles(gm: GameMap, tile: TileRef): TileRef[] {
|
||||
return Array.from(gm.bfs(tile, euclDistFN(tile, 4)))
|
||||
.filter(t => !gm.hasOwner(t) && gm.isLand(t))
|
||||
return Array.from(gm.bfs(tile, euclDistFN(tile, 4))).filter(
|
||||
(t) => !gm.hasOwner(t) && gm.isLand(t),
|
||||
);
|
||||
}
|
||||
|
||||
export function closestTwoTiles(gm: GameMap, x: Iterable<TileRef>, y: Iterable<TileRef>): { x: TileRef, y: TileRef } {
|
||||
const xSorted = Array.from(x).sort((a, b) => gm.x(a) - gm.x(b));
|
||||
const ySorted = Array.from(y).sort((a, b) => gm.x(a) - gm.x(b));
|
||||
export function closestTwoTiles(
|
||||
gm: GameMap,
|
||||
x: Iterable<TileRef>,
|
||||
y: Iterable<TileRef>,
|
||||
): { x: TileRef; y: TileRef } {
|
||||
const xSorted = Array.from(x).sort((a, b) => gm.x(a) - gm.x(b));
|
||||
const ySorted = Array.from(y).sort((a, b) => gm.x(a) - gm.x(b));
|
||||
|
||||
if (xSorted.length == 0 || ySorted.length == 0) {
|
||||
return null;
|
||||
if (xSorted.length == 0 || ySorted.length == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let i = 0;
|
||||
let j = 0;
|
||||
let minDistance = Infinity;
|
||||
let result = { x: xSorted[0], y: ySorted[0] };
|
||||
|
||||
while (i < xSorted.length && j < ySorted.length) {
|
||||
const currentX = xSorted[i];
|
||||
const currentY = ySorted[j];
|
||||
|
||||
const distance =
|
||||
Math.abs(gm.x(currentX) - gm.x(currentY)) +
|
||||
Math.abs(gm.y(currentX) - gm.y(currentY));
|
||||
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
result = { x: currentX, y: currentY };
|
||||
}
|
||||
|
||||
let i = 0;
|
||||
let j = 0;
|
||||
let minDistance = Infinity;
|
||||
let result = { x: xSorted[0], y: ySorted[0] };
|
||||
|
||||
while (i < xSorted.length && j < ySorted.length) {
|
||||
const currentX = xSorted[i];
|
||||
const currentY = ySorted[j];
|
||||
|
||||
const distance =
|
||||
Math.abs(gm.x(currentX) - gm.x(currentY)) +
|
||||
Math.abs(gm.y(currentX) - gm.y(currentY));
|
||||
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
result = { x: currentX, y: currentY };
|
||||
}
|
||||
|
||||
// If we're at the end of X, must move Y forward
|
||||
if (i === xSorted.length - 1) {
|
||||
j++;
|
||||
}
|
||||
// If we're at the end of Y, must move X forward
|
||||
else if (j === ySorted.length - 1) {
|
||||
i++;
|
||||
}
|
||||
// Otherwise, move whichever pointer has smaller x value
|
||||
else if (gm.x(currentX) < gm.x(currentY)) {
|
||||
i++;
|
||||
} else {
|
||||
j++;
|
||||
}
|
||||
// If we're at the end of X, must move Y forward
|
||||
if (i === xSorted.length - 1) {
|
||||
j++;
|
||||
}
|
||||
// If we're at the end of Y, must move X forward
|
||||
else if (j === ySorted.length - 1) {
|
||||
i++;
|
||||
}
|
||||
// Otherwise, move whichever pointer has smaller x value
|
||||
else if (gm.x(currentX) < gm.x(currentY)) {
|
||||
i++;
|
||||
} else {
|
||||
j++;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1,49 +1,53 @@
|
||||
import { EventBus, GameEvent } from "../EventBus"
|
||||
import { Execution, Game, Player, PlayerID } from "../game/Game"
|
||||
import { EventBus, GameEvent } from "../EventBus";
|
||||
import { Execution, Game, Player, PlayerID } from "../game/Game";
|
||||
|
||||
export class WinEvent implements GameEvent {
|
||||
constructor(public readonly winner: Player) { }
|
||||
constructor(public readonly winner: Player) {}
|
||||
}
|
||||
|
||||
export class WinCheckExecution implements Execution {
|
||||
private active = true;
|
||||
|
||||
private active = true
|
||||
private mg: Game;
|
||||
|
||||
private mg: Game
|
||||
constructor() {}
|
||||
|
||||
constructor() {
|
||||
init(mg: Game, ticks: number) {
|
||||
this.mg = mg;
|
||||
}
|
||||
|
||||
tick(ticks: number) {
|
||||
if (ticks % 10 != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
init(mg: Game, ticks: number) {
|
||||
this.mg = mg
|
||||
const sorted = this.mg
|
||||
.players()
|
||||
.sort((a, b) => b.numTilesOwned() - a.numTilesOwned());
|
||||
if (sorted.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
tick(ticks: number) {
|
||||
if (ticks % 10 != 0) {
|
||||
return
|
||||
}
|
||||
const sorted = this.mg.players().sort((a, b) => b.numTilesOwned() - a.numTilesOwned())
|
||||
if (sorted.length == 0) {
|
||||
return
|
||||
}
|
||||
const max = sorted[0]
|
||||
const numTilesWithoutFallout = this.mg.numLandTiles() - this.mg.numTilesWithFallout()
|
||||
if (max.numTilesOwned() / numTilesWithoutFallout * 100 > this.mg.config().percentageTilesOwnedToWin()) {
|
||||
this.mg.setWinner(max)
|
||||
console.log(`${max.name()} has won the game`)
|
||||
this.active = false
|
||||
}
|
||||
const max = sorted[0];
|
||||
const numTilesWithoutFallout =
|
||||
this.mg.numLandTiles() - this.mg.numTilesWithFallout();
|
||||
if (
|
||||
(max.numTilesOwned() / numTilesWithoutFallout) * 100 >
|
||||
this.mg.config().percentageTilesOwnedToWin()
|
||||
) {
|
||||
this.mg.setWinner(max);
|
||||
console.log(`${max.name()} has won the game`);
|
||||
this.active = false;
|
||||
}
|
||||
}
|
||||
|
||||
owner(): Player {
|
||||
return null
|
||||
}
|
||||
owner(): Player {
|
||||
return null;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active
|
||||
}
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,40 +1,51 @@
|
||||
import { consolex } from "../../Consolex";
|
||||
import {AllianceRequest, Execution, Game, Player, PlayerID} from "../../game/Game";
|
||||
import {
|
||||
AllianceRequest,
|
||||
Execution,
|
||||
Game,
|
||||
Player,
|
||||
PlayerID,
|
||||
} from "../../game/Game";
|
||||
|
||||
export class AllianceRequestExecution implements Execution {
|
||||
private active = true
|
||||
private mg: Game = null
|
||||
private requestor: Player;
|
||||
private recipient: Player
|
||||
private active = true;
|
||||
private mg: Game = null;
|
||||
private requestor: Player;
|
||||
private recipient: Player;
|
||||
|
||||
constructor(private requestorID: PlayerID, private recipientID: PlayerID) { }
|
||||
constructor(
|
||||
private requestorID: PlayerID,
|
||||
private recipientID: PlayerID,
|
||||
) {}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.mg = mg
|
||||
this.requestor = mg.player(this.requestorID)
|
||||
this.recipient = mg.player(this.recipientID)
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.mg = mg;
|
||||
this.requestor = mg.player(this.requestorID);
|
||||
this.recipient = mg.player(this.recipientID);
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
if (this.requestor.isAlliedWith(this.recipient)) {
|
||||
consolex.warn("already allied");
|
||||
} else if (
|
||||
this.requestor.recentOrPendingAllianceRequestWith(this.recipient)
|
||||
) {
|
||||
consolex.warn("recent or pending alliance request");
|
||||
} else {
|
||||
this.requestor.createAllianceRequest(this.recipient);
|
||||
}
|
||||
this.active = false;
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
if (this.requestor.isAlliedWith(this.recipient)) {
|
||||
consolex.warn('already allied')
|
||||
} else if (this.requestor.recentOrPendingAllianceRequestWith(this.recipient)) {
|
||||
consolex.warn('recent or pending alliance request')
|
||||
} else {
|
||||
this.requestor.createAllianceRequest(this.recipient)
|
||||
}
|
||||
this.active = false
|
||||
}
|
||||
owner(): Player {
|
||||
return null;
|
||||
}
|
||||
|
||||
owner(): Player {
|
||||
return null
|
||||
}
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active
|
||||
}
|
||||
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,49 +1,61 @@
|
||||
import { consolex } from "../../Consolex";
|
||||
import { AllianceRequest, Execution, Game, Player, PlayerID } from "../../game/Game";
|
||||
import {
|
||||
AllianceRequest,
|
||||
Execution,
|
||||
Game,
|
||||
Player,
|
||||
PlayerID,
|
||||
} from "../../game/Game";
|
||||
|
||||
export class AllianceRequestReplyExecution implements Execution {
|
||||
private active = true
|
||||
private mg: Game = null
|
||||
private requestor: Player;
|
||||
private recipient: Player
|
||||
private active = true;
|
||||
private mg: Game = null;
|
||||
private requestor: Player;
|
||||
private recipient: Player;
|
||||
|
||||
constructor(private requestorID: PlayerID, private recipientID: PlayerID, private accept: boolean) { }
|
||||
constructor(
|
||||
private requestorID: PlayerID,
|
||||
private recipientID: PlayerID,
|
||||
private accept: boolean,
|
||||
) {}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.mg = mg
|
||||
this.requestor = mg.player(this.requestorID)
|
||||
this.recipient = mg.player(this.recipientID)
|
||||
}
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.mg = mg;
|
||||
this.requestor = mg.player(this.requestorID);
|
||||
this.recipient = mg.player(this.recipientID);
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
if (this.requestor.isAlliedWith(this.recipient)) {
|
||||
consolex.warn('already allied')
|
||||
tick(ticks: number): void {
|
||||
if (this.requestor.isAlliedWith(this.recipient)) {
|
||||
consolex.warn("already allied");
|
||||
} else {
|
||||
const request = this.requestor
|
||||
.outgoingAllianceRequests()
|
||||
.find((ar) => ar.recipient() == this.recipient);
|
||||
if (request == null) {
|
||||
consolex.warn("no alliance request found");
|
||||
} else {
|
||||
if (this.accept) {
|
||||
request.accept();
|
||||
this.requestor.updateRelation(this.recipient, 100);
|
||||
this.recipient.updateRelation(this.requestor, 100);
|
||||
} else {
|
||||
const request = this.requestor.outgoingAllianceRequests().find(ar => ar.recipient() == this.recipient)
|
||||
if (request == null) {
|
||||
consolex.warn('no alliance request found')
|
||||
} else {
|
||||
if (this.accept) {
|
||||
request.accept()
|
||||
this.requestor.updateRelation(this.recipient, 100)
|
||||
this.recipient.updateRelation(this.requestor, 100)
|
||||
} else {
|
||||
request.reject()
|
||||
}
|
||||
}
|
||||
request.reject();
|
||||
}
|
||||
this.active = false
|
||||
}
|
||||
}
|
||||
this.active = false;
|
||||
}
|
||||
|
||||
owner(): Player {
|
||||
return null
|
||||
}
|
||||
owner(): Player {
|
||||
return null;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active
|
||||
}
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +1,54 @@
|
||||
import { consolex } from "../../Consolex";
|
||||
import { AllianceRequest, Execution, Game, Player, PlayerID } from "../../game/Game";
|
||||
import {
|
||||
AllianceRequest,
|
||||
Execution,
|
||||
Game,
|
||||
Player,
|
||||
PlayerID,
|
||||
} from "../../game/Game";
|
||||
|
||||
export class BreakAllianceExecution implements Execution {
|
||||
private active = true
|
||||
private requestor: Player;
|
||||
private recipient: Player
|
||||
private mg: Game
|
||||
private active = true;
|
||||
private requestor: Player;
|
||||
private recipient: Player;
|
||||
private mg: Game;
|
||||
|
||||
constructor(private requestorID: PlayerID, private recipientID: PlayerID) { }
|
||||
constructor(
|
||||
private requestorID: PlayerID,
|
||||
private recipientID: PlayerID,
|
||||
) {}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.requestor = mg.player(this.requestorID)
|
||||
this.recipient = mg.player(this.recipientID)
|
||||
this.mg = mg
|
||||
}
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.requestor = mg.player(this.requestorID);
|
||||
this.recipient = mg.player(this.recipientID);
|
||||
this.mg = mg;
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
const alliance = this.requestor.allianceWith(this.recipient)
|
||||
if (alliance == null) {
|
||||
consolex.warn('cant break alliance, not allied')
|
||||
} else {
|
||||
this.requestor.breakAlliance(alliance)
|
||||
this.recipient.updateRelation(this.requestor, -200)
|
||||
for (const player of this.mg.players()) {
|
||||
if (player != this.requestor) {
|
||||
player.updateRelation(this.requestor, -40)
|
||||
}
|
||||
}
|
||||
tick(ticks: number): void {
|
||||
const alliance = this.requestor.allianceWith(this.recipient);
|
||||
if (alliance == null) {
|
||||
consolex.warn("cant break alliance, not allied");
|
||||
} else {
|
||||
this.requestor.breakAlliance(alliance);
|
||||
this.recipient.updateRelation(this.requestor, -200);
|
||||
for (const player of this.mg.players()) {
|
||||
if (player != this.requestor) {
|
||||
player.updateRelation(this.requestor, -40);
|
||||
}
|
||||
this.active = false
|
||||
}
|
||||
}
|
||||
this.active = false;
|
||||
}
|
||||
|
||||
owner(): Player {
|
||||
return null
|
||||
}
|
||||
owner(): Player {
|
||||
return null;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active
|
||||
}
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +1,233 @@
|
||||
export const BOT_NAME_PREFIXES = [
|
||||
"Akkadian", "Babylonian", "Assyrian", "Sumerian", "Hittite", "Phoenician",
|
||||
"Canaanite", "Minoan", "Mycenaean", "Etruscan", "Scythian", "Thracian",
|
||||
"Dacian", "Illyrian", "Median", "Chaldean",
|
||||
"Roman", "Greek", "Byzantine", "Persian", "Parthian", "Seleucid",
|
||||
"Ptolemaic", "Palmyrene", "Macedonian", "Carthaginian",
|
||||
"Ming", "Tang", "Song", "Yuan",
|
||||
"Mauryan", "Kushan", "Rajput", "Mughal", "Satavahana", "Vijayanagara",
|
||||
"Egyptian", "Nubian", "Kushite", "Aksumite", "Ethiopian", "Songhai",
|
||||
"Malian", "Ghanaian", "Benin", "Ashanti", "Zulu", "Tuareg", "Berber",
|
||||
"Kanem-Bornu", "Buganda", "Mossi", "Swahili", "Somali", "Wolof",
|
||||
"Umayyad", "Abbasid", "Ayyubid", "Fatimid", "Mamluk", "Seljuk",
|
||||
"Safavid", "Ottoman", "Almoravid", "Almohad", "Rashidun", "Ziyarid",
|
||||
"Frankish", "Visigothic", "Ostrogothic", "Viking", "Norman", "Saxon",
|
||||
"Anglo-Saxon", "Celtic", "Gaulish", "Carolingian", "Merovingian",
|
||||
"Capetian", "Plantagenet", "Tudor", "Stuart", "Habsburg", "Romanov",
|
||||
"Lancaster", "York", "Bourbon", "Napoleonic",
|
||||
"British", "French", "Spanish", "Portuguese", "Dutch", "Russian",
|
||||
"German", "Italian", "Swedish", "Norwegian", "Danish", "Polish",
|
||||
"Hungarian", "Austrian", "Swiss", "Czech", "Slovak", "Serbian",
|
||||
"Croatian", "Bosnian", "Montenegrin", "Bulgarian", "Romanian",
|
||||
"Apache", "Sioux", "Cherokee", "Navajo", "Iroquois", "Inuit", "Arawak",
|
||||
"Carib", "Taino", "Aztec", "Mayan", "Incan", "Mapuche", "Guarani",
|
||||
"Tupi", "Yanomami", "Zuni", "Hopi", "Kiowa", "Comanche", "Shoshone",
|
||||
"Japanese", "Ryukyu", "Ainu", "Cham", "Khmer", "Thai", "Vietnamese",
|
||||
"Burmese", "Balinese", "Malay", "Filipino", "Mongolian",
|
||||
"Korean", "Tibetan", "Manchu", "Uyghur", "Hmong", "Karen", "Pyu",
|
||||
"Hawaiian", "Fijian", "Tongan", "Samoan", "Maori", "Micronesian",
|
||||
"Hebrew", "Armenian", "Georgian", "Phoenician", "Assyrian", "Chaldean",
|
||||
"Kurdish", "Turkic", "Kazakh", "Uzbek", "Kyrgyz", "Tajik", "Uighur",
|
||||
"Pashtun", "Baloch", "Afghan", "Persian",
|
||||
]
|
||||
"Akkadian",
|
||||
"Babylonian",
|
||||
"Assyrian",
|
||||
"Sumerian",
|
||||
"Hittite",
|
||||
"Phoenician",
|
||||
"Canaanite",
|
||||
"Minoan",
|
||||
"Mycenaean",
|
||||
"Etruscan",
|
||||
"Scythian",
|
||||
"Thracian",
|
||||
"Dacian",
|
||||
"Illyrian",
|
||||
"Median",
|
||||
"Chaldean",
|
||||
"Roman",
|
||||
"Greek",
|
||||
"Byzantine",
|
||||
"Persian",
|
||||
"Parthian",
|
||||
"Seleucid",
|
||||
"Ptolemaic",
|
||||
"Palmyrene",
|
||||
"Macedonian",
|
||||
"Carthaginian",
|
||||
"Ming",
|
||||
"Tang",
|
||||
"Song",
|
||||
"Yuan",
|
||||
"Mauryan",
|
||||
"Kushan",
|
||||
"Rajput",
|
||||
"Mughal",
|
||||
"Satavahana",
|
||||
"Vijayanagara",
|
||||
"Egyptian",
|
||||
"Nubian",
|
||||
"Kushite",
|
||||
"Aksumite",
|
||||
"Ethiopian",
|
||||
"Songhai",
|
||||
"Malian",
|
||||
"Ghanaian",
|
||||
"Benin",
|
||||
"Ashanti",
|
||||
"Zulu",
|
||||
"Tuareg",
|
||||
"Berber",
|
||||
"Kanem-Bornu",
|
||||
"Buganda",
|
||||
"Mossi",
|
||||
"Swahili",
|
||||
"Somali",
|
||||
"Wolof",
|
||||
"Umayyad",
|
||||
"Abbasid",
|
||||
"Ayyubid",
|
||||
"Fatimid",
|
||||
"Mamluk",
|
||||
"Seljuk",
|
||||
"Safavid",
|
||||
"Ottoman",
|
||||
"Almoravid",
|
||||
"Almohad",
|
||||
"Rashidun",
|
||||
"Ziyarid",
|
||||
"Frankish",
|
||||
"Visigothic",
|
||||
"Ostrogothic",
|
||||
"Viking",
|
||||
"Norman",
|
||||
"Saxon",
|
||||
"Anglo-Saxon",
|
||||
"Celtic",
|
||||
"Gaulish",
|
||||
"Carolingian",
|
||||
"Merovingian",
|
||||
"Capetian",
|
||||
"Plantagenet",
|
||||
"Tudor",
|
||||
"Stuart",
|
||||
"Habsburg",
|
||||
"Romanov",
|
||||
"Lancaster",
|
||||
"York",
|
||||
"Bourbon",
|
||||
"Napoleonic",
|
||||
"British",
|
||||
"French",
|
||||
"Spanish",
|
||||
"Portuguese",
|
||||
"Dutch",
|
||||
"Russian",
|
||||
"German",
|
||||
"Italian",
|
||||
"Swedish",
|
||||
"Norwegian",
|
||||
"Danish",
|
||||
"Polish",
|
||||
"Hungarian",
|
||||
"Austrian",
|
||||
"Swiss",
|
||||
"Czech",
|
||||
"Slovak",
|
||||
"Serbian",
|
||||
"Croatian",
|
||||
"Bosnian",
|
||||
"Montenegrin",
|
||||
"Bulgarian",
|
||||
"Romanian",
|
||||
"Apache",
|
||||
"Sioux",
|
||||
"Cherokee",
|
||||
"Navajo",
|
||||
"Iroquois",
|
||||
"Inuit",
|
||||
"Arawak",
|
||||
"Carib",
|
||||
"Taino",
|
||||
"Aztec",
|
||||
"Mayan",
|
||||
"Incan",
|
||||
"Mapuche",
|
||||
"Guarani",
|
||||
"Tupi",
|
||||
"Yanomami",
|
||||
"Zuni",
|
||||
"Hopi",
|
||||
"Kiowa",
|
||||
"Comanche",
|
||||
"Shoshone",
|
||||
"Japanese",
|
||||
"Ryukyu",
|
||||
"Ainu",
|
||||
"Cham",
|
||||
"Khmer",
|
||||
"Thai",
|
||||
"Vietnamese",
|
||||
"Burmese",
|
||||
"Balinese",
|
||||
"Malay",
|
||||
"Filipino",
|
||||
"Mongolian",
|
||||
"Korean",
|
||||
"Tibetan",
|
||||
"Manchu",
|
||||
"Uyghur",
|
||||
"Hmong",
|
||||
"Karen",
|
||||
"Pyu",
|
||||
"Hawaiian",
|
||||
"Fijian",
|
||||
"Tongan",
|
||||
"Samoan",
|
||||
"Maori",
|
||||
"Micronesian",
|
||||
"Hebrew",
|
||||
"Armenian",
|
||||
"Georgian",
|
||||
"Phoenician",
|
||||
"Assyrian",
|
||||
"Chaldean",
|
||||
"Kurdish",
|
||||
"Turkic",
|
||||
"Kazakh",
|
||||
"Uzbek",
|
||||
"Kyrgyz",
|
||||
"Tajik",
|
||||
"Uighur",
|
||||
"Pashtun",
|
||||
"Baloch",
|
||||
"Afghan",
|
||||
"Persian",
|
||||
];
|
||||
export const BOT_NAME_SUFFIXES = [
|
||||
"Empire", "Dynasty", "Kingdom", "Sultanate", "Confederation", "Union",
|
||||
"Republic", "Caliphate", "Dominion", "Realm", "State",
|
||||
"Federation", "Territory", "Commonwealth", "League", "Duchy", "Province",
|
||||
"Protectorate", "Colony", "Mandate", "Free State","Canton", "Region", "Nation",
|
||||
"Assembly", "Hierarchy", "Archduchy", "Grand Duchy","Metropolis", "Cluster",
|
||||
"Alliance", "Tribunal", "Council", "Confederacy", "Order", "Regime",
|
||||
"Dominion", "Syndicate","Guild", "Corporation", "Patriarchy",
|
||||
"Matriarchy","Legion", "Horde", "Clan", "Brotherhood", "Sisterhood","Ascendancy", "Supremacy",
|
||||
"Province","Kingdoms", "Tribes", "Dominion", "Assembly", "Republics"
|
||||
"Empire",
|
||||
"Dynasty",
|
||||
"Kingdom",
|
||||
"Sultanate",
|
||||
"Confederation",
|
||||
"Union",
|
||||
"Republic",
|
||||
"Caliphate",
|
||||
"Dominion",
|
||||
"Realm",
|
||||
"State",
|
||||
"Federation",
|
||||
"Territory",
|
||||
"Commonwealth",
|
||||
"League",
|
||||
"Duchy",
|
||||
"Province",
|
||||
"Protectorate",
|
||||
"Colony",
|
||||
"Mandate",
|
||||
"Free State",
|
||||
"Canton",
|
||||
"Region",
|
||||
"Nation",
|
||||
"Assembly",
|
||||
"Hierarchy",
|
||||
"Archduchy",
|
||||
"Grand Duchy",
|
||||
"Metropolis",
|
||||
"Cluster",
|
||||
"Alliance",
|
||||
"Tribunal",
|
||||
"Council",
|
||||
"Confederacy",
|
||||
"Order",
|
||||
"Regime",
|
||||
"Dominion",
|
||||
"Syndicate",
|
||||
"Guild",
|
||||
"Corporation",
|
||||
"Patriarchy",
|
||||
"Matriarchy",
|
||||
"Legion",
|
||||
"Horde",
|
||||
"Clan",
|
||||
"Brotherhood",
|
||||
"Sisterhood",
|
||||
"Ascendancy",
|
||||
"Supremacy",
|
||||
"Province",
|
||||
"Kingdoms",
|
||||
"Tribes",
|
||||
"Dominion",
|
||||
"Assembly",
|
||||
"Republics",
|
||||
];
|
||||
|
||||
@@ -1,36 +1,35 @@
|
||||
import {MutableAlliance, Game, Player, Tick} from "./Game";
|
||||
import {GameImpl} from "./GameImpl";
|
||||
import {PlayerImpl} from "./PlayerImpl";
|
||||
import { MutableAlliance, Game, Player, Tick } from "./Game";
|
||||
import { GameImpl } from "./GameImpl";
|
||||
import { PlayerImpl } from "./PlayerImpl";
|
||||
|
||||
export class AllianceImpl implements MutableAlliance {
|
||||
constructor(
|
||||
private readonly mg: GameImpl,
|
||||
readonly requestor_: PlayerImpl,
|
||||
readonly recipient_: PlayerImpl,
|
||||
readonly createdAtTick_: Tick,
|
||||
) { }
|
||||
constructor(
|
||||
private readonly mg: GameImpl,
|
||||
readonly requestor_: PlayerImpl,
|
||||
readonly recipient_: PlayerImpl,
|
||||
readonly createdAtTick_: Tick,
|
||||
) {}
|
||||
|
||||
other(player: Player): PlayerImpl {
|
||||
if (this.requestor_ == player) {
|
||||
return this.recipient_
|
||||
}
|
||||
return this.requestor_
|
||||
other(player: Player): PlayerImpl {
|
||||
if (this.requestor_ == player) {
|
||||
return this.recipient_;
|
||||
}
|
||||
return this.requestor_;
|
||||
}
|
||||
|
||||
requestor(): Player {
|
||||
return this.requestor_
|
||||
}
|
||||
requestor(): Player {
|
||||
return this.requestor_;
|
||||
}
|
||||
|
||||
recipient(): Player {
|
||||
return this.recipient_
|
||||
}
|
||||
recipient(): Player {
|
||||
return this.recipient_;
|
||||
}
|
||||
|
||||
createdAt(): Tick {
|
||||
return this.createdAtTick_
|
||||
}
|
||||
createdAt(): Tick {
|
||||
return this.createdAtTick_;
|
||||
}
|
||||
|
||||
expire(): void {
|
||||
this.mg.expireAlliance(this)
|
||||
}
|
||||
|
||||
}
|
||||
expire(): void {
|
||||
this.mg.expireAlliance(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,37 +3,39 @@ import { AllianceRequestUpdate } from "./GameUpdates";
|
||||
import { GameUpdateType } from "./GameUpdates";
|
||||
import { GameImpl } from "./GameImpl";
|
||||
|
||||
|
||||
export class AllianceRequestImpl implements AllianceRequest {
|
||||
constructor(
|
||||
private requestor_: Player,
|
||||
private recipient_: Player,
|
||||
private tickCreated: number,
|
||||
private game: GameImpl,
|
||||
) {}
|
||||
|
||||
constructor(private requestor_: Player, private recipient_: Player, private tickCreated: number, private game: GameImpl) { }
|
||||
requestor(): Player {
|
||||
return this.requestor_;
|
||||
}
|
||||
|
||||
requestor(): Player {
|
||||
return this.requestor_;
|
||||
}
|
||||
recipient(): Player {
|
||||
return this.recipient_;
|
||||
}
|
||||
|
||||
recipient(): Player {
|
||||
return this.recipient_;
|
||||
}
|
||||
createdAt(): Tick {
|
||||
return this.tickCreated;
|
||||
}
|
||||
|
||||
createdAt(): Tick {
|
||||
return this.tickCreated
|
||||
}
|
||||
|
||||
accept(): void {
|
||||
this.game.acceptAllianceRequest(this)
|
||||
}
|
||||
reject(): void {
|
||||
this.game.rejectAllianceRequest(this)
|
||||
}
|
||||
|
||||
toUpdate(): AllianceRequestUpdate {
|
||||
return {
|
||||
type: GameUpdateType.AllianceRequest,
|
||||
requestorID: this.requestor_.smallID(),
|
||||
recipientID: this.recipient_.smallID(),
|
||||
createdAt: this.tickCreated,
|
||||
}
|
||||
}
|
||||
accept(): void {
|
||||
this.game.acceptAllianceRequest(this);
|
||||
}
|
||||
reject(): void {
|
||||
this.game.rejectAllianceRequest(this);
|
||||
}
|
||||
|
||||
toUpdate(): AllianceRequestUpdate {
|
||||
return {
|
||||
type: GameUpdateType.AllianceRequest,
|
||||
requestorID: this.requestor_.smallID(),
|
||||
recipientID: this.recipient_.smallID(),
|
||||
createdAt: this.tickCreated,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+270
-261
@@ -1,13 +1,18 @@
|
||||
import { Config } from "../configuration/Config"
|
||||
import { GameEvent } from "../EventBus"
|
||||
import { PlayerView } from "./GameView"
|
||||
import { ClientID, GameConfig, GameID } from "../Schemas"
|
||||
import { GameMap, GameMapImpl, TileRef } from "./GameMap"
|
||||
import { GameUpdate, GameUpdateType, PlayerUpdate, UnitUpdate } from "./GameUpdates"
|
||||
import { Config } from "../configuration/Config";
|
||||
import { GameEvent } from "../EventBus";
|
||||
import { PlayerView } from "./GameView";
|
||||
import { ClientID, GameConfig, GameID } from "../Schemas";
|
||||
import { GameMap, GameMapImpl, TileRef } from "./GameMap";
|
||||
import {
|
||||
GameUpdate,
|
||||
GameUpdateType,
|
||||
PlayerUpdate,
|
||||
UnitUpdate,
|
||||
} from "./GameUpdates";
|
||||
|
||||
export type PlayerID = string
|
||||
export type Tick = number
|
||||
export type Gold = number
|
||||
export type PlayerID = string;
|
||||
export type Tick = number;
|
||||
export type Gold = number;
|
||||
|
||||
export const AllPlayers = "AllPlayers" as const;
|
||||
|
||||
@@ -17,360 +22,364 @@ type UpdateTypeMap<T extends GameUpdateType> = Extract<GameUpdate, { type: T }>;
|
||||
|
||||
// Then use it to create the record type
|
||||
export type GameUpdates = {
|
||||
[K in GameUpdateType]: UpdateTypeMap<K>[]
|
||||
}
|
||||
[K in GameUpdateType]: UpdateTypeMap<K>[];
|
||||
};
|
||||
|
||||
export interface MapPos {
|
||||
x: number
|
||||
y: number
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export enum Difficulty {
|
||||
Easy = "Easy",
|
||||
Medium = "Medium",
|
||||
Hard = "Hard",
|
||||
Impossible = "Impossible",
|
||||
Easy = "Easy",
|
||||
Medium = "Medium",
|
||||
Hard = "Hard",
|
||||
Impossible = "Impossible",
|
||||
}
|
||||
|
||||
export enum GameMapType {
|
||||
World = "World",
|
||||
Europe = "Europe",
|
||||
Mena = "Mena",
|
||||
NorthAmerica = "North America",
|
||||
Oceania = "Oceania",
|
||||
BlackSea = "Black Sea"
|
||||
World = "World",
|
||||
Europe = "Europe",
|
||||
Mena = "Mena",
|
||||
NorthAmerica = "North America",
|
||||
Oceania = "Oceania",
|
||||
BlackSea = "Black Sea",
|
||||
}
|
||||
|
||||
export enum GameType {
|
||||
Singleplayer = "Singleplayer",
|
||||
Public = "Public",
|
||||
Private = "Private",
|
||||
Singleplayer = "Singleplayer",
|
||||
Public = "Public",
|
||||
Private = "Private",
|
||||
}
|
||||
|
||||
export interface UnitInfo {
|
||||
cost: (player: Player | PlayerView) => Gold
|
||||
// Determines if its owner changes when its tile is conquered.
|
||||
territoryBound: boolean
|
||||
maxHealth?: number,
|
||||
damage?: number
|
||||
cost: (player: Player | PlayerView) => Gold;
|
||||
// Determines if its owner changes when its tile is conquered.
|
||||
territoryBound: boolean;
|
||||
maxHealth?: number;
|
||||
damage?: number;
|
||||
}
|
||||
|
||||
export enum UnitType {
|
||||
TransportShip = "Transport",
|
||||
Destroyer = "Destroyer",
|
||||
Battleship = "Battleship",
|
||||
Shell = "Shell",
|
||||
Port = "Port",
|
||||
AtomBomb = "Atom Bomb",
|
||||
HydrogenBomb = "Hydrogen Bomb",
|
||||
TradeShip = "Trade Ship",
|
||||
MissileSilo = "Missile Silo",
|
||||
DefensePost = "Defense Post",
|
||||
City = "City"
|
||||
TransportShip = "Transport",
|
||||
Destroyer = "Destroyer",
|
||||
Battleship = "Battleship",
|
||||
Shell = "Shell",
|
||||
Port = "Port",
|
||||
AtomBomb = "Atom Bomb",
|
||||
HydrogenBomb = "Hydrogen Bomb",
|
||||
TradeShip = "Trade Ship",
|
||||
MissileSilo = "Missile Silo",
|
||||
DefensePost = "Defense Post",
|
||||
City = "City",
|
||||
}
|
||||
|
||||
export enum Relation {
|
||||
Hostile = 0,
|
||||
Distrustful = 1,
|
||||
Neutral = 2,
|
||||
Friendly = 3
|
||||
Hostile = 0,
|
||||
Distrustful = 1,
|
||||
Neutral = 2,
|
||||
Friendly = 3,
|
||||
}
|
||||
|
||||
export class Nation {
|
||||
constructor(
|
||||
public readonly name: string,
|
||||
public readonly cell: Cell,
|
||||
public readonly strength: number,
|
||||
) { }
|
||||
constructor(
|
||||
public readonly name: string,
|
||||
public readonly cell: Cell,
|
||||
public readonly strength: number,
|
||||
) {}
|
||||
}
|
||||
|
||||
|
||||
export class Cell {
|
||||
public index: number
|
||||
public index: number;
|
||||
|
||||
private strRepr: string
|
||||
private strRepr: string;
|
||||
|
||||
constructor(
|
||||
public readonly x,
|
||||
public readonly y
|
||||
) {
|
||||
this.strRepr = `Cell[${this.x},${this.y}]`
|
||||
}
|
||||
constructor(
|
||||
public readonly x,
|
||||
public readonly y,
|
||||
) {
|
||||
this.strRepr = `Cell[${this.x},${this.y}]`;
|
||||
}
|
||||
|
||||
pos(): MapPos {
|
||||
return {
|
||||
x: this.x,
|
||||
y: this.y
|
||||
}
|
||||
}
|
||||
pos(): MapPos {
|
||||
return {
|
||||
x: this.x,
|
||||
y: this.y,
|
||||
};
|
||||
}
|
||||
|
||||
toString(): string { return this.strRepr }
|
||||
toString(): string {
|
||||
return this.strRepr;
|
||||
}
|
||||
}
|
||||
|
||||
export enum TerrainType {
|
||||
Plains,
|
||||
Highland,
|
||||
Mountain,
|
||||
Lake,
|
||||
Ocean
|
||||
Plains,
|
||||
Highland,
|
||||
Mountain,
|
||||
Lake,
|
||||
Ocean,
|
||||
}
|
||||
|
||||
export enum PlayerType {
|
||||
Bot = "BOT",
|
||||
Human = "HUMAN",
|
||||
FakeHuman = "FAKEHUMAN",
|
||||
Bot = "BOT",
|
||||
Human = "HUMAN",
|
||||
FakeHuman = "FAKEHUMAN",
|
||||
}
|
||||
|
||||
export interface Execution {
|
||||
isActive(): boolean
|
||||
activeDuringSpawnPhase(): boolean
|
||||
init(mg: Game, ticks: number): void
|
||||
tick(ticks: number): void
|
||||
owner(): Player
|
||||
isActive(): boolean;
|
||||
activeDuringSpawnPhase(): boolean;
|
||||
init(mg: Game, ticks: number): void;
|
||||
tick(ticks: number): void;
|
||||
owner(): Player;
|
||||
}
|
||||
|
||||
export interface AllianceRequest {
|
||||
accept(): void
|
||||
reject(): void
|
||||
requestor(): Player
|
||||
recipient(): Player
|
||||
createdAt(): Tick
|
||||
accept(): void;
|
||||
reject(): void;
|
||||
requestor(): Player;
|
||||
recipient(): Player;
|
||||
createdAt(): Tick;
|
||||
}
|
||||
|
||||
export interface Alliance {
|
||||
requestor(): Player
|
||||
recipient(): Player
|
||||
createdAt(): Tick
|
||||
other(player: Player): Player
|
||||
requestor(): Player;
|
||||
recipient(): Player;
|
||||
createdAt(): Tick;
|
||||
other(player: Player): Player;
|
||||
}
|
||||
|
||||
export interface MutableAlliance extends Alliance {
|
||||
expire(): void
|
||||
other(player: Player): Player
|
||||
expire(): void;
|
||||
other(player: Player): Player;
|
||||
}
|
||||
|
||||
export class PlayerInfo {
|
||||
constructor(
|
||||
public readonly name: string,
|
||||
public readonly playerType: PlayerType,
|
||||
// null if bot.
|
||||
public readonly clientID: ClientID | null,
|
||||
// TODO: make player id the small id
|
||||
public readonly id: PlayerID
|
||||
) { }
|
||||
constructor(
|
||||
public readonly name: string,
|
||||
public readonly playerType: PlayerType,
|
||||
// null if bot.
|
||||
public readonly clientID: ClientID | null,
|
||||
// TODO: make player id the small id
|
||||
public readonly id: PlayerID,
|
||||
) {}
|
||||
}
|
||||
|
||||
export interface DefenseBonus {
|
||||
// Unit providing the defense bonus
|
||||
unit: Unit
|
||||
amount: number
|
||||
tile: TileRef
|
||||
// Unit providing the defense bonus
|
||||
unit: Unit;
|
||||
amount: number;
|
||||
tile: TileRef;
|
||||
}
|
||||
|
||||
export interface Unit {
|
||||
// Properties
|
||||
type(): UnitType
|
||||
troops(): number
|
||||
owner(): Player
|
||||
info(): UnitInfo
|
||||
// Properties
|
||||
type(): UnitType;
|
||||
troops(): number;
|
||||
owner(): Player;
|
||||
info(): UnitInfo;
|
||||
|
||||
// Location
|
||||
tile(): TileRef
|
||||
lastTile(): TileRef
|
||||
move(tile: TileRef): void
|
||||
// Location
|
||||
tile(): TileRef;
|
||||
lastTile(): TileRef;
|
||||
move(tile: TileRef): void;
|
||||
|
||||
// State
|
||||
isActive(): boolean
|
||||
hasHealth(): boolean
|
||||
health(): number
|
||||
modifyHealth(delta: number): void
|
||||
// State
|
||||
isActive(): boolean;
|
||||
hasHealth(): boolean;
|
||||
health(): number;
|
||||
modifyHealth(delta: number): void;
|
||||
|
||||
// Mutations
|
||||
setTroops(troops: number): void
|
||||
delete(displayerMessage?: boolean): void
|
||||
// Mutations
|
||||
setTroops(troops: number): void;
|
||||
delete(displayerMessage?: boolean): void;
|
||||
|
||||
// Updates
|
||||
toUpdate(): UnitUpdate
|
||||
// Updates
|
||||
toUpdate(): UnitUpdate;
|
||||
}
|
||||
|
||||
export interface TerraNullius {
|
||||
isPlayer(): false
|
||||
id(): PlayerID // always zero, maybe make it TerraNulliusID?
|
||||
clientID(): ClientID
|
||||
smallID(): number
|
||||
isPlayer(): false;
|
||||
id(): PlayerID; // always zero, maybe make it TerraNulliusID?
|
||||
clientID(): ClientID;
|
||||
smallID(): number;
|
||||
}
|
||||
|
||||
export interface Player {
|
||||
// Basic Info
|
||||
smallID(): number
|
||||
info(): PlayerInfo
|
||||
name(): string
|
||||
displayName(): string
|
||||
clientID(): ClientID
|
||||
id(): PlayerID
|
||||
type(): PlayerType
|
||||
isPlayer(): this is Player
|
||||
toString(): string
|
||||
// Basic Info
|
||||
smallID(): number;
|
||||
info(): PlayerInfo;
|
||||
name(): string;
|
||||
displayName(): string;
|
||||
clientID(): ClientID;
|
||||
id(): PlayerID;
|
||||
type(): PlayerType;
|
||||
isPlayer(): this is Player;
|
||||
toString(): string;
|
||||
|
||||
// State & Properties
|
||||
isAlive(): boolean
|
||||
isTraitor(): boolean
|
||||
largestClusterBoundingBox: { min: Cell, max: Cell } | null
|
||||
lastTileChange(): Tick
|
||||
// State & Properties
|
||||
isAlive(): boolean;
|
||||
isTraitor(): boolean;
|
||||
largestClusterBoundingBox: { min: Cell; max: Cell } | null;
|
||||
lastTileChange(): Tick;
|
||||
|
||||
// Territory
|
||||
tiles(): ReadonlySet<TileRef>
|
||||
borderTiles(): ReadonlySet<TileRef>
|
||||
numTilesOwned(): number
|
||||
conquer(tile: TileRef): void
|
||||
relinquish(tile: TileRef): void
|
||||
// Territory
|
||||
tiles(): ReadonlySet<TileRef>;
|
||||
borderTiles(): ReadonlySet<TileRef>;
|
||||
numTilesOwned(): number;
|
||||
conquer(tile: TileRef): void;
|
||||
relinquish(tile: TileRef): void;
|
||||
|
||||
// Resources & Population
|
||||
gold(): Gold
|
||||
population(): number
|
||||
workers(): number
|
||||
troops(): number
|
||||
targetTroopRatio(): number
|
||||
addGold(toAdd: Gold): void
|
||||
removeGold(toRemove: Gold): void
|
||||
addWorkers(toAdd: number): void
|
||||
removeWorkers(toRemove: number): void
|
||||
setTargetTroopRatio(target: number): void
|
||||
setTroops(troops: number): void
|
||||
addTroops(troops: number): void
|
||||
removeTroops(troops: number): number
|
||||
// Resources & Population
|
||||
gold(): Gold;
|
||||
population(): number;
|
||||
workers(): number;
|
||||
troops(): number;
|
||||
targetTroopRatio(): number;
|
||||
addGold(toAdd: Gold): void;
|
||||
removeGold(toRemove: Gold): void;
|
||||
addWorkers(toAdd: number): void;
|
||||
removeWorkers(toRemove: number): void;
|
||||
setTargetTroopRatio(target: number): void;
|
||||
setTroops(troops: number): void;
|
||||
addTroops(troops: number): void;
|
||||
removeTroops(troops: number): number;
|
||||
|
||||
// Units
|
||||
units(...types: UnitType[]): Unit[]
|
||||
canBuild(type: UnitType, targetTile: TileRef): TileRef | false
|
||||
buildUnit(type: UnitType, troops: number, tile: TileRef): Unit
|
||||
captureUnit(unit: Unit): void
|
||||
// Units
|
||||
units(...types: UnitType[]): Unit[];
|
||||
canBuild(type: UnitType, targetTile: TileRef): TileRef | false;
|
||||
buildUnit(type: UnitType, troops: number, tile: TileRef): Unit;
|
||||
captureUnit(unit: Unit): void;
|
||||
|
||||
// Relations & Diplomacy
|
||||
neighbors(): (Player | TerraNullius)[]
|
||||
sharesBorderWith(other: Player | TerraNullius): boolean
|
||||
relation(other: Player): Relation
|
||||
allRelationsSorted(): { player: Player, relation: Relation }[]
|
||||
updateRelation(other: Player, delta: number): void
|
||||
decayRelations(): void
|
||||
// Relations & Diplomacy
|
||||
neighbors(): (Player | TerraNullius)[];
|
||||
sharesBorderWith(other: Player | TerraNullius): boolean;
|
||||
relation(other: Player): Relation;
|
||||
allRelationsSorted(): { player: Player; relation: Relation }[];
|
||||
updateRelation(other: Player, delta: number): void;
|
||||
decayRelations(): void;
|
||||
|
||||
// Alliances
|
||||
incomingAllianceRequests(): AllianceRequest[]
|
||||
outgoingAllianceRequests(): AllianceRequest[]
|
||||
alliances(): MutableAlliance[]
|
||||
allies(): Player[]
|
||||
isAlliedWith(other: Player): boolean
|
||||
allianceWith(other: Player): MutableAlliance | null
|
||||
recentOrPendingAllianceRequestWith(other: Player): boolean
|
||||
breakAlliance(alliance: Alliance): void
|
||||
createAllianceRequest(recipient: Player): AllianceRequest
|
||||
// Alliances
|
||||
incomingAllianceRequests(): AllianceRequest[];
|
||||
outgoingAllianceRequests(): AllianceRequest[];
|
||||
alliances(): MutableAlliance[];
|
||||
allies(): Player[];
|
||||
isAlliedWith(other: Player): boolean;
|
||||
allianceWith(other: Player): MutableAlliance | null;
|
||||
recentOrPendingAllianceRequestWith(other: Player): boolean;
|
||||
breakAlliance(alliance: Alliance): void;
|
||||
createAllianceRequest(recipient: Player): AllianceRequest;
|
||||
|
||||
// Targeting
|
||||
canTarget(other: Player): boolean
|
||||
target(other: Player): void
|
||||
targets(): Player[]
|
||||
transitiveTargets(): Player[]
|
||||
// Targeting
|
||||
canTarget(other: Player): boolean;
|
||||
target(other: Player): void;
|
||||
targets(): Player[];
|
||||
transitiveTargets(): Player[];
|
||||
|
||||
// Communication
|
||||
canSendEmoji(recipient: Player | typeof AllPlayers): boolean
|
||||
outgoingEmojis(): EmojiMessage[]
|
||||
sendEmoji(recipient: Player | typeof AllPlayers, emoji: string): void
|
||||
// Communication
|
||||
canSendEmoji(recipient: Player | typeof AllPlayers): boolean;
|
||||
outgoingEmojis(): EmojiMessage[];
|
||||
sendEmoji(recipient: Player | typeof AllPlayers, emoji: string): void;
|
||||
|
||||
// Trading
|
||||
canDonate(recipient: Player): boolean
|
||||
donate(recipient: Player, troops: number): void
|
||||
// Trading
|
||||
canDonate(recipient: Player): boolean;
|
||||
donate(recipient: Player, troops: number): void;
|
||||
|
||||
// Misc
|
||||
executions(): Execution[]
|
||||
toUpdate(): PlayerUpdate
|
||||
playerProfile(): PlayerProfile
|
||||
canBoat(tile: TileRef): boolean
|
||||
canAttack(tile: TileRef)
|
||||
// Misc
|
||||
executions(): Execution[];
|
||||
toUpdate(): PlayerUpdate;
|
||||
playerProfile(): PlayerProfile;
|
||||
canBoat(tile: TileRef): boolean;
|
||||
canAttack(tile: TileRef);
|
||||
}
|
||||
|
||||
export interface Game extends GameMap {
|
||||
// Map & Dimensions
|
||||
isOnMap(cell: Cell): boolean
|
||||
width(): number
|
||||
height(): number
|
||||
map(): GameMap
|
||||
miniMap(): GameMap
|
||||
forEachTile(fn: (tile: TileRef) => void): void
|
||||
// Map & Dimensions
|
||||
isOnMap(cell: Cell): boolean;
|
||||
width(): number;
|
||||
height(): number;
|
||||
map(): GameMap;
|
||||
miniMap(): GameMap;
|
||||
forEachTile(fn: (tile: TileRef) => void): void;
|
||||
|
||||
// Player Management
|
||||
player(id: PlayerID): Player
|
||||
players(): Player[]
|
||||
allPlayers(): Player[]
|
||||
playerByClientID(id: ClientID): Player | null
|
||||
playerBySmallID(id: number): Player | TerraNullius
|
||||
hasPlayer(id: PlayerID): boolean
|
||||
addPlayer(playerInfo: PlayerInfo, manpower: number): Player
|
||||
terraNullius(): TerraNullius
|
||||
owner(ref: TileRef): Player | TerraNullius
|
||||
// Player Management
|
||||
player(id: PlayerID): Player;
|
||||
players(): Player[];
|
||||
allPlayers(): Player[];
|
||||
playerByClientID(id: ClientID): Player | null;
|
||||
playerBySmallID(id: number): Player | TerraNullius;
|
||||
hasPlayer(id: PlayerID): boolean;
|
||||
addPlayer(playerInfo: PlayerInfo, manpower: number): Player;
|
||||
terraNullius(): TerraNullius;
|
||||
owner(ref: TileRef): Player | TerraNullius;
|
||||
|
||||
// Game State
|
||||
ticks(): Tick
|
||||
inSpawnPhase(): boolean
|
||||
executeNextTick(): GameUpdates
|
||||
setWinner(winner: Player): void
|
||||
config(): Config
|
||||
// Game State
|
||||
ticks(): Tick;
|
||||
inSpawnPhase(): boolean;
|
||||
executeNextTick(): GameUpdates;
|
||||
setWinner(winner: Player): void;
|
||||
config(): Config;
|
||||
|
||||
// Units
|
||||
units(...types: UnitType[]): Unit[]
|
||||
unitInfo(type: UnitType): UnitInfo
|
||||
addTileDefenseBonus(tile: TileRef, unit: Unit, amount: number): DefenseBonus
|
||||
removeTileDefenseBonus(bonus: DefenseBonus): void
|
||||
// Units
|
||||
units(...types: UnitType[]): Unit[];
|
||||
unitInfo(type: UnitType): UnitInfo;
|
||||
addTileDefenseBonus(tile: TileRef, unit: Unit, amount: number): DefenseBonus;
|
||||
removeTileDefenseBonus(bonus: DefenseBonus): void;
|
||||
|
||||
// Events & Messages
|
||||
executions(): Execution[]
|
||||
addExecution(...exec: Execution[]): void
|
||||
displayMessage(message: string, type: MessageType, playerID: PlayerID | null): void
|
||||
// Events & Messages
|
||||
executions(): Execution[];
|
||||
addExecution(...exec: Execution[]): void;
|
||||
displayMessage(
|
||||
message: string,
|
||||
type: MessageType,
|
||||
playerID: PlayerID | null,
|
||||
): void;
|
||||
|
||||
// Nations
|
||||
nations(): Nation[]
|
||||
// Nations
|
||||
nations(): Nation[];
|
||||
|
||||
numTilesWithFallout(): number
|
||||
numTilesWithFallout(): number;
|
||||
}
|
||||
|
||||
export interface PlayerActions {
|
||||
canBoat: boolean
|
||||
canAttack: boolean
|
||||
buildableUnits: UnitType[]
|
||||
canSendEmojiAllPlayers: boolean
|
||||
interaction?: PlayerInteraction
|
||||
canBoat: boolean;
|
||||
canAttack: boolean;
|
||||
buildableUnits: UnitType[];
|
||||
canSendEmojiAllPlayers: boolean;
|
||||
interaction?: PlayerInteraction;
|
||||
}
|
||||
|
||||
export interface PlayerProfile {
|
||||
relations: Record<number, Relation>
|
||||
alliances: number[]
|
||||
relations: Record<number, Relation>;
|
||||
alliances: number[];
|
||||
}
|
||||
|
||||
export interface PlayerInteraction {
|
||||
sharedBorder: boolean
|
||||
canSendEmoji: boolean
|
||||
canSendAllianceRequest: boolean
|
||||
canBreakAlliance: boolean
|
||||
canTarget: boolean
|
||||
canDonate: boolean
|
||||
sharedBorder: boolean;
|
||||
canSendEmoji: boolean;
|
||||
canSendAllianceRequest: boolean;
|
||||
canBreakAlliance: boolean;
|
||||
canTarget: boolean;
|
||||
canDonate: boolean;
|
||||
}
|
||||
|
||||
export interface EmojiMessage {
|
||||
message: string
|
||||
senderID: number
|
||||
recipientID: number | typeof AllPlayers
|
||||
createdAt: Tick
|
||||
message: string;
|
||||
senderID: number;
|
||||
recipientID: number | typeof AllPlayers;
|
||||
createdAt: Tick;
|
||||
}
|
||||
|
||||
export enum MessageType {
|
||||
SUCCESS,
|
||||
INFO,
|
||||
WARN,
|
||||
ERROR
|
||||
SUCCESS,
|
||||
INFO,
|
||||
WARN,
|
||||
ERROR,
|
||||
}
|
||||
|
||||
export interface NameViewData {
|
||||
x: number;
|
||||
y: number;
|
||||
size: number;
|
||||
x: number;
|
||||
y: number;
|
||||
size: number;
|
||||
}
|
||||
|
||||
|
||||
+589
-465
File diff suppressed because it is too large
Load Diff
+304
-266
@@ -1,294 +1,332 @@
|
||||
import { Cell, TerrainType } from "./Game";
|
||||
|
||||
export type TileRef = number;
|
||||
export type TileUpdate = bigint
|
||||
export type TileUpdate = bigint;
|
||||
|
||||
export interface GameMap {
|
||||
ref(x: number, y: number): TileRef
|
||||
ref(x: number, y: number): TileRef;
|
||||
|
||||
x(ref: TileRef): number
|
||||
y(ref: TileRef): number
|
||||
cell(ref: TileRef): Cell
|
||||
width(): number
|
||||
height(): number
|
||||
numLandTiles(): number
|
||||
x(ref: TileRef): number;
|
||||
y(ref: TileRef): number;
|
||||
cell(ref: TileRef): Cell;
|
||||
width(): number;
|
||||
height(): number;
|
||||
numLandTiles(): number;
|
||||
|
||||
isValidCoord(x: number, y: number): boolean
|
||||
// Terrain getters (immutable)
|
||||
isLand(ref: TileRef): boolean
|
||||
isOceanShore(ref: TileRef): boolean
|
||||
isOcean(ref: TileRef): boolean
|
||||
isShoreline(ref: TileRef): boolean
|
||||
magnitude(ref: TileRef): number
|
||||
// State getters and setters (mutable)
|
||||
ownerID(ref: TileRef): number
|
||||
hasOwner(ref: TileRef): boolean
|
||||
isValidCoord(x: number, y: number): boolean;
|
||||
// Terrain getters (immutable)
|
||||
isLand(ref: TileRef): boolean;
|
||||
isOceanShore(ref: TileRef): boolean;
|
||||
isOcean(ref: TileRef): boolean;
|
||||
isShoreline(ref: TileRef): boolean;
|
||||
magnitude(ref: TileRef): number;
|
||||
// State getters and setters (mutable)
|
||||
ownerID(ref: TileRef): number;
|
||||
hasOwner(ref: TileRef): boolean;
|
||||
|
||||
setOwnerID(ref: TileRef, playerId: number): void
|
||||
hasFallout(ref: TileRef): boolean
|
||||
setFallout(ref: TileRef, value: boolean): void
|
||||
isBorder(ref: TileRef): boolean
|
||||
neighbors(ref: TileRef): TileRef[]
|
||||
isWater(ref: TileRef): boolean
|
||||
isLake(ref: TileRef): boolean
|
||||
isShore(ref: TileRef): boolean
|
||||
cost(ref: TileRef): number
|
||||
terrainType(ref: TileRef): TerrainType
|
||||
forEachTile(fn: (tile: TileRef) => void): void
|
||||
setOwnerID(ref: TileRef, playerId: number): void;
|
||||
hasFallout(ref: TileRef): boolean;
|
||||
setFallout(ref: TileRef, value: boolean): void;
|
||||
isBorder(ref: TileRef): boolean;
|
||||
neighbors(ref: TileRef): TileRef[];
|
||||
isWater(ref: TileRef): boolean;
|
||||
isLake(ref: TileRef): boolean;
|
||||
isShore(ref: TileRef): boolean;
|
||||
cost(ref: TileRef): number;
|
||||
terrainType(ref: TileRef): TerrainType;
|
||||
forEachTile(fn: (tile: TileRef) => void): void;
|
||||
|
||||
manhattanDist(c1: TileRef, c2: TileRef): number;
|
||||
euclideanDist(c1: TileRef, c2: TileRef): number;
|
||||
bfs(
|
||||
tile: TileRef,
|
||||
filter: (gm: GameMap, tile: TileRef) => boolean,
|
||||
): Set<TileRef>;
|
||||
|
||||
manhattanDist(c1: TileRef, c2: TileRef): number
|
||||
euclideanDist(c1: TileRef, c2: TileRef): number
|
||||
bfs(tile: TileRef, filter: (gm: GameMap, tile: TileRef) => boolean): Set<TileRef>
|
||||
toTileUpdate(tile: TileRef): bigint;
|
||||
updateTile(tu: TileUpdate): TileRef;
|
||||
|
||||
toTileUpdate(tile: TileRef): bigint
|
||||
updateTile(tu: TileUpdate): TileRef
|
||||
|
||||
numTilesWithFallout(): number
|
||||
numTilesWithFallout(): number;
|
||||
}
|
||||
|
||||
export class GameMapImpl implements GameMap {
|
||||
private _numTilesWithFallout = 0
|
||||
private _numTilesWithFallout = 0;
|
||||
|
||||
private readonly terrain: Uint8Array; // Immutable terrain data
|
||||
private readonly state: Uint16Array; // Mutable game state
|
||||
private readonly width_: number;
|
||||
private readonly height_: number;
|
||||
private readonly terrain: Uint8Array; // Immutable terrain data
|
||||
private readonly state: Uint16Array; // Mutable game state
|
||||
private readonly width_: number;
|
||||
private readonly height_: number;
|
||||
|
||||
// Terrain bits (Uint8Array)
|
||||
private static readonly IS_LAND_BIT = 7;
|
||||
private static readonly SHORELINE_BIT = 6;
|
||||
private static readonly OCEAN_BIT = 5;
|
||||
private static readonly MAGNITUDE_OFFSET = 4; // Uses bits 3-7 (5 bits)
|
||||
private static readonly MAGNITUDE_MASK = 0x1F; // 11111 in binary
|
||||
// Terrain bits (Uint8Array)
|
||||
private static readonly IS_LAND_BIT = 7;
|
||||
private static readonly SHORELINE_BIT = 6;
|
||||
private static readonly OCEAN_BIT = 5;
|
||||
private static readonly MAGNITUDE_OFFSET = 4; // Uses bits 3-7 (5 bits)
|
||||
private static readonly MAGNITUDE_MASK = 0x1f; // 11111 in binary
|
||||
|
||||
// State bits (Uint16Array)
|
||||
private static readonly PLAYER_ID_OFFSET = 0; // Uses bits 0-11 (12 bits)
|
||||
private static readonly PLAYER_ID_MASK = 0xFFF;
|
||||
private static readonly FALLOUT_BIT = 13;
|
||||
private static readonly DEFENSE_BONUS_BIT = 14;
|
||||
// Bit 15 still reserved
|
||||
// State bits (Uint16Array)
|
||||
private static readonly PLAYER_ID_OFFSET = 0; // Uses bits 0-11 (12 bits)
|
||||
private static readonly PLAYER_ID_MASK = 0xfff;
|
||||
private static readonly FALLOUT_BIT = 13;
|
||||
private static readonly DEFENSE_BONUS_BIT = 14;
|
||||
// Bit 15 still reserved
|
||||
|
||||
constructor(width: number, height: number, terrainData: Uint8Array, private numLandTiles_: number) {
|
||||
if (terrainData.length !== width * height) {
|
||||
throw new Error(`Terrain data length ${terrainData.length} doesn't match dimensions ${width}x${height}`);
|
||||
constructor(
|
||||
width: number,
|
||||
height: number,
|
||||
terrainData: Uint8Array,
|
||||
private numLandTiles_: number,
|
||||
) {
|
||||
if (terrainData.length !== width * height) {
|
||||
throw new Error(
|
||||
`Terrain data length ${terrainData.length} doesn't match dimensions ${width}x${height}`,
|
||||
);
|
||||
}
|
||||
this.width_ = width;
|
||||
this.height_ = height;
|
||||
this.terrain = terrainData;
|
||||
this.state = new Uint16Array(width * height);
|
||||
}
|
||||
numTilesWithFallout(): number {
|
||||
return this._numTilesWithFallout;
|
||||
}
|
||||
|
||||
ref(x: number, y: number): TileRef {
|
||||
if (!this.isValidCoord(x, y)) {
|
||||
throw new Error(`Invalid coordinates: ${x},${y}`);
|
||||
}
|
||||
return y * this.width_ + x;
|
||||
}
|
||||
|
||||
x(ref: TileRef): number {
|
||||
return ref % this.width_;
|
||||
}
|
||||
|
||||
y(ref: TileRef): number {
|
||||
return Math.floor(ref / this.width_);
|
||||
}
|
||||
|
||||
cell(ref: TileRef): Cell {
|
||||
return new Cell(this.x(ref), this.y(ref));
|
||||
}
|
||||
|
||||
width(): number {
|
||||
return this.width_;
|
||||
}
|
||||
height(): number {
|
||||
return this.height_;
|
||||
}
|
||||
numLandTiles(): number {
|
||||
return this.numLandTiles_;
|
||||
}
|
||||
|
||||
isValidCoord(x: number, y: number): boolean {
|
||||
return x >= 0 && x < this.width_ && y >= 0 && y < this.height_;
|
||||
}
|
||||
|
||||
// Terrain getters (immutable)
|
||||
isLand(ref: TileRef): boolean {
|
||||
return Boolean(this.terrain[ref] & (1 << GameMapImpl.IS_LAND_BIT));
|
||||
}
|
||||
|
||||
isOceanShore(ref: TileRef): boolean {
|
||||
return (
|
||||
this.isLand(ref) && this.neighbors(ref).some((tr) => this.isOcean(tr))
|
||||
);
|
||||
}
|
||||
|
||||
isOcean(ref: TileRef): boolean {
|
||||
return Boolean(this.terrain[ref] & (1 << GameMapImpl.OCEAN_BIT));
|
||||
}
|
||||
|
||||
isShoreline(ref: TileRef): boolean {
|
||||
return Boolean(this.terrain[ref] & (1 << GameMapImpl.SHORELINE_BIT));
|
||||
}
|
||||
|
||||
magnitude(ref: TileRef): number {
|
||||
return this.terrain[ref] & GameMapImpl.MAGNITUDE_MASK;
|
||||
}
|
||||
|
||||
// State getters and setters (mutable)
|
||||
ownerID(ref: TileRef): number {
|
||||
return this.state[ref] & GameMapImpl.PLAYER_ID_MASK;
|
||||
}
|
||||
|
||||
hasOwner(ref: TileRef): boolean {
|
||||
return this.ownerID(ref) != 0;
|
||||
}
|
||||
|
||||
setOwnerID(ref: TileRef, playerId: number): void {
|
||||
if (playerId > GameMapImpl.PLAYER_ID_MASK) {
|
||||
throw new Error(
|
||||
`Player ID ${playerId} exceeds maximum value ${GameMapImpl.PLAYER_ID_MASK}`,
|
||||
);
|
||||
}
|
||||
this.state[ref] =
|
||||
(this.state[ref] & ~GameMapImpl.PLAYER_ID_MASK) | playerId;
|
||||
}
|
||||
|
||||
hasFallout(ref: TileRef): boolean {
|
||||
return Boolean(this.state[ref] & (1 << GameMapImpl.FALLOUT_BIT));
|
||||
}
|
||||
|
||||
setFallout(ref: TileRef, value: boolean): void {
|
||||
const existingFallout = this.hasFallout(ref);
|
||||
if (value) {
|
||||
if (!existingFallout) {
|
||||
this._numTilesWithFallout++;
|
||||
this.state[ref] |= 1 << GameMapImpl.FALLOUT_BIT;
|
||||
}
|
||||
} else {
|
||||
if (existingFallout) {
|
||||
this._numTilesWithFallout--;
|
||||
this.state[ref] &= ~(1 << GameMapImpl.FALLOUT_BIT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isBorder(ref: TileRef): boolean {
|
||||
return this.neighbors(ref).some(
|
||||
(tr) => this.ownerID(tr) != this.ownerID(ref),
|
||||
);
|
||||
}
|
||||
|
||||
hasDefenseBonus(ref: TileRef): boolean {
|
||||
return Boolean(this.state[ref] & (1 << GameMapImpl.DEFENSE_BONUS_BIT));
|
||||
}
|
||||
|
||||
setDefenseBonus(ref: TileRef, value: boolean): void {
|
||||
if (value) {
|
||||
this.state[ref] |= 1 << GameMapImpl.DEFENSE_BONUS_BIT;
|
||||
} else {
|
||||
this.state[ref] &= ~(1 << GameMapImpl.DEFENSE_BONUS_BIT);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
isWater(ref: TileRef): boolean {
|
||||
return !this.isLand(ref);
|
||||
}
|
||||
|
||||
isLake(ref: TileRef): boolean {
|
||||
return !this.isLand(ref) && !this.isOcean(ref);
|
||||
}
|
||||
|
||||
isShore(ref: TileRef): boolean {
|
||||
return this.isLand(ref) && this.isShoreline(ref);
|
||||
}
|
||||
|
||||
cost(ref: TileRef): number {
|
||||
return this.magnitude(ref) < 10 ? 2 : 1;
|
||||
}
|
||||
|
||||
terrainType(ref: TileRef): TerrainType {
|
||||
if (this.isLand(ref)) {
|
||||
const magnitude = this.magnitude(ref);
|
||||
if (magnitude < 10) return TerrainType.Plains;
|
||||
if (magnitude < 20) return TerrainType.Highland;
|
||||
return TerrainType.Mountain;
|
||||
}
|
||||
return this.isOcean(ref) ? TerrainType.Ocean : TerrainType.Lake;
|
||||
}
|
||||
|
||||
neighbors(ref: TileRef): TileRef[] {
|
||||
const neighbors: TileRef[] = [];
|
||||
const w = this.width_;
|
||||
|
||||
if (ref >= w) neighbors.push(ref - w);
|
||||
if (ref < (this.height_ - 1) * w) neighbors.push(ref + w);
|
||||
if (ref % w !== 0) neighbors.push(ref - 1);
|
||||
if (ref % w !== w - 1) neighbors.push(ref + 1);
|
||||
|
||||
for (const n of neighbors) {
|
||||
this.ref(this.x(n), this.y(n));
|
||||
}
|
||||
|
||||
return neighbors;
|
||||
}
|
||||
|
||||
forEachTile(fn: (tile: TileRef) => void): void {
|
||||
for (let x = 0; x < this.width_; x++) {
|
||||
for (let y = 0; y < this.height_; y++) {
|
||||
fn(this.ref(x, y));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
manhattanDist(c1: TileRef, c2: TileRef): number {
|
||||
return (
|
||||
Math.abs(this.x(c1) - this.x(c2)) + Math.abs(this.y(c1) - this.y(c2))
|
||||
);
|
||||
}
|
||||
euclideanDist(c1: TileRef, c2: TileRef): number {
|
||||
return Math.sqrt(
|
||||
Math.pow(this.x(c1) - this.x(c2), 2) +
|
||||
Math.pow(this.y(c1) - this.y(c2), 2),
|
||||
);
|
||||
}
|
||||
bfs(
|
||||
tile: TileRef,
|
||||
filter: (gm: GameMap, tile: TileRef) => boolean,
|
||||
): Set<TileRef> {
|
||||
const seen = new Set<TileRef>();
|
||||
const q: TileRef[] = [];
|
||||
q.push(tile);
|
||||
while (q.length > 0) {
|
||||
const curr = q.pop();
|
||||
seen.add(curr);
|
||||
for (const n of this.neighbors(curr)) {
|
||||
if (!seen.has(n) && filter(this, n)) {
|
||||
q.push(n);
|
||||
}
|
||||
this.width_ = width;
|
||||
this.height_ = height;
|
||||
this.terrain = terrainData;
|
||||
this.state = new Uint16Array(width * height);
|
||||
}
|
||||
}
|
||||
numTilesWithFallout(): number {
|
||||
return this._numTilesWithFallout
|
||||
return seen;
|
||||
}
|
||||
|
||||
toTileUpdate(tile: TileRef): bigint {
|
||||
// Pack the tile reference and state into a bigint
|
||||
// Format: [32 bits for tile reference][16 bits for state]
|
||||
return (BigInt(tile) << 16n) | BigInt(this.state[tile]);
|
||||
}
|
||||
|
||||
updateTile(tu: TileUpdate): TileRef {
|
||||
// Extract tile reference and state from the TileUpdate
|
||||
// Last 16 bits are state, rest is tile reference
|
||||
const tileRef = Number(tu >> 16n);
|
||||
const state = Number(tu & 0xffffn);
|
||||
|
||||
const existingFallout = this.hasFallout(tileRef);
|
||||
this.state[tileRef] = state;
|
||||
const newFallout = this.hasFallout(tileRef);
|
||||
if (existingFallout && !newFallout) {
|
||||
this._numTilesWithFallout--;
|
||||
}
|
||||
if (!existingFallout && newFallout) {
|
||||
this._numTilesWithFallout++;
|
||||
}
|
||||
|
||||
ref(x: number, y: number): TileRef {
|
||||
if (!this.isValidCoord(x, y)) {
|
||||
throw new Error(`Invalid coordinates: ${x},${y}`);
|
||||
}
|
||||
return y * this.width_ + x;
|
||||
}
|
||||
|
||||
x(ref: TileRef): number {
|
||||
return ref % this.width_;
|
||||
}
|
||||
|
||||
y(ref: TileRef): number {
|
||||
return Math.floor(ref / this.width_);
|
||||
}
|
||||
|
||||
cell(ref: TileRef): Cell {
|
||||
return new Cell(this.x(ref), this.y(ref))
|
||||
}
|
||||
|
||||
width(): number { return this.width_; }
|
||||
height(): number { return this.height_; }
|
||||
numLandTiles(): number { return this.numLandTiles_; }
|
||||
|
||||
isValidCoord(x: number, y: number): boolean {
|
||||
return x >= 0 && x < this.width_ && y >= 0 && y < this.height_;
|
||||
}
|
||||
|
||||
// Terrain getters (immutable)
|
||||
isLand(ref: TileRef): boolean {
|
||||
return Boolean(this.terrain[ref] & (1 << GameMapImpl.IS_LAND_BIT));
|
||||
}
|
||||
|
||||
isOceanShore(ref: TileRef): boolean {
|
||||
return this.isLand(ref) && this.neighbors(ref).some(tr => this.isOcean(tr))
|
||||
}
|
||||
|
||||
isOcean(ref: TileRef): boolean {
|
||||
return Boolean(this.terrain[ref] & (1 << GameMapImpl.OCEAN_BIT));
|
||||
}
|
||||
|
||||
isShoreline(ref: TileRef): boolean {
|
||||
return Boolean(this.terrain[ref] & (1 << GameMapImpl.SHORELINE_BIT));
|
||||
}
|
||||
|
||||
magnitude(ref: TileRef): number {
|
||||
return this.terrain[ref] & GameMapImpl.MAGNITUDE_MASK;
|
||||
}
|
||||
|
||||
// State getters and setters (mutable)
|
||||
ownerID(ref: TileRef): number {
|
||||
return this.state[ref] & GameMapImpl.PLAYER_ID_MASK;
|
||||
}
|
||||
|
||||
hasOwner(ref: TileRef): boolean {
|
||||
return this.ownerID(ref) != 0
|
||||
}
|
||||
|
||||
|
||||
setOwnerID(ref: TileRef, playerId: number): void {
|
||||
if (playerId > GameMapImpl.PLAYER_ID_MASK) {
|
||||
throw new Error(`Player ID ${playerId} exceeds maximum value ${GameMapImpl.PLAYER_ID_MASK}`);
|
||||
}
|
||||
this.state[ref] = (this.state[ref] & ~GameMapImpl.PLAYER_ID_MASK) | playerId;
|
||||
}
|
||||
|
||||
hasFallout(ref: TileRef): boolean {
|
||||
return Boolean(this.state[ref] & (1 << GameMapImpl.FALLOUT_BIT));
|
||||
}
|
||||
|
||||
setFallout(ref: TileRef, value: boolean): void {
|
||||
const existingFallout = this.hasFallout(ref)
|
||||
if (value) {
|
||||
if (!existingFallout) {
|
||||
this._numTilesWithFallout++
|
||||
this.state[ref] |= 1 << GameMapImpl.FALLOUT_BIT;
|
||||
}
|
||||
} else {
|
||||
if (existingFallout) {
|
||||
this._numTilesWithFallout--
|
||||
this.state[ref] &= ~(1 << GameMapImpl.FALLOUT_BIT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isBorder(ref: TileRef): boolean {
|
||||
return this.neighbors(ref).some(tr => this.ownerID(tr) != this.ownerID(ref))
|
||||
}
|
||||
|
||||
hasDefenseBonus(ref: TileRef): boolean {
|
||||
return Boolean(this.state[ref] & (1 << GameMapImpl.DEFENSE_BONUS_BIT));
|
||||
}
|
||||
|
||||
setDefenseBonus(ref: TileRef, value: boolean): void {
|
||||
if (value) {
|
||||
this.state[ref] |= 1 << GameMapImpl.DEFENSE_BONUS_BIT;
|
||||
} else {
|
||||
this.state[ref] &= ~(1 << GameMapImpl.DEFENSE_BONUS_BIT);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
isWater(ref: TileRef): boolean {
|
||||
return !this.isLand(ref);
|
||||
}
|
||||
|
||||
isLake(ref: TileRef): boolean {
|
||||
return !this.isLand(ref) && !this.isOcean(ref);
|
||||
}
|
||||
|
||||
isShore(ref: TileRef): boolean {
|
||||
return this.isLand(ref) && this.isShoreline(ref);
|
||||
}
|
||||
|
||||
cost(ref: TileRef): number {
|
||||
return this.magnitude(ref) < 10 ? 2 : 1;
|
||||
}
|
||||
|
||||
terrainType(ref: TileRef): TerrainType {
|
||||
if (this.isLand(ref)) {
|
||||
const magnitude = this.magnitude(ref);
|
||||
if (magnitude < 10) return TerrainType.Plains;
|
||||
if (magnitude < 20) return TerrainType.Highland;
|
||||
return TerrainType.Mountain;
|
||||
}
|
||||
return this.isOcean(ref) ? TerrainType.Ocean : TerrainType.Lake;
|
||||
}
|
||||
|
||||
neighbors(ref: TileRef): TileRef[] {
|
||||
const neighbors: TileRef[] = [];
|
||||
const w = this.width_;
|
||||
|
||||
if (ref >= w) neighbors.push(ref - w);
|
||||
if (ref < (this.height_ - 1) * w) neighbors.push(ref + w);
|
||||
if (ref % w !== 0) neighbors.push(ref - 1);
|
||||
if (ref % w !== w - 1) neighbors.push(ref + 1);
|
||||
|
||||
for (const n of neighbors) {
|
||||
(this.ref(this.x(n), this.y(n)))
|
||||
}
|
||||
|
||||
return neighbors;
|
||||
}
|
||||
|
||||
forEachTile(fn: (tile: TileRef) => void): void {
|
||||
for (let x = 0; x < this.width_; x++) {
|
||||
for (let y = 0; y < this.height_; y++) {
|
||||
fn(this.ref(x, y))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
manhattanDist(c1: TileRef, c2: TileRef): number {
|
||||
return Math.abs(this.x(c1) - this.x(c2)) + Math.abs(this.y(c1) - this.y(c2));
|
||||
}
|
||||
euclideanDist(c1: TileRef, c2: TileRef): number {
|
||||
return Math.sqrt(Math.pow(this.x(c1) - this.x(c2), 2) + Math.pow(this.y(c1) - this.y(c2), 2));
|
||||
}
|
||||
bfs(tile: TileRef, filter: (gm: GameMap, tile: TileRef) => boolean): Set<TileRef> {
|
||||
const seen = new Set<TileRef>()
|
||||
const q: TileRef[] = []
|
||||
q.push(tile)
|
||||
while (q.length > 0) {
|
||||
const curr = q.pop()
|
||||
seen.add(curr)
|
||||
for (const n of this.neighbors(curr)) {
|
||||
if (!seen.has(n) && filter(this, n)) {
|
||||
q.push(n)
|
||||
}
|
||||
}
|
||||
}
|
||||
return seen
|
||||
}
|
||||
|
||||
toTileUpdate(tile: TileRef): bigint {
|
||||
// Pack the tile reference and state into a bigint
|
||||
// Format: [32 bits for tile reference][16 bits for state]
|
||||
return (BigInt(tile) << 16n) | BigInt(this.state[tile]);
|
||||
}
|
||||
|
||||
updateTile(tu: TileUpdate): TileRef {
|
||||
// Extract tile reference and state from the TileUpdate
|
||||
// Last 16 bits are state, rest is tile reference
|
||||
const tileRef = Number(tu >> 16n);
|
||||
const state = Number(tu & 0xFFFFn);
|
||||
|
||||
const existingFallout = this.hasFallout(tileRef)
|
||||
this.state[tileRef] = state;
|
||||
const newFallout = this.hasFallout(tileRef)
|
||||
if (existingFallout && !newFallout) {
|
||||
this._numTilesWithFallout--
|
||||
}
|
||||
if (!existingFallout && newFallout) {
|
||||
this._numTilesWithFallout++
|
||||
}
|
||||
|
||||
return tileRef;
|
||||
}
|
||||
return tileRef;
|
||||
}
|
||||
}
|
||||
|
||||
export function euclDistFN(root: TileRef, dist: number): (gm: GameMap, tile: TileRef) => boolean {
|
||||
return (gm: GameMap, n: TileRef) => gm.euclideanDist(root, n) <= dist;
|
||||
export function euclDistFN(
|
||||
root: TileRef,
|
||||
dist: number,
|
||||
): (gm: GameMap, tile: TileRef) => boolean {
|
||||
return (gm: GameMap, n: TileRef) => gm.euclideanDist(root, n) <= dist;
|
||||
}
|
||||
|
||||
export function manhattanDistFN(root: TileRef, dist: number): (gm: GameMap, tile: TileRef) => boolean {
|
||||
return (gm: GameMap, n: TileRef) => gm.manhattanDist(root, n) <= dist;
|
||||
export function manhattanDistFN(
|
||||
root: TileRef,
|
||||
dist: number,
|
||||
): (gm: GameMap, tile: TileRef) => boolean {
|
||||
return (gm: GameMap, n: TileRef) => gm.manhattanDist(root, n) <= dist;
|
||||
}
|
||||
|
||||
export function andFN(x: (gm: GameMap, tile: TileRef) => boolean, y: (gm: GameMap, tile: TileRef) => boolean): (gm: GameMap, tile: TileRef) => boolean {
|
||||
return (gm: GameMap, tile: TileRef) => x(gm, tile) && y(gm, tile)
|
||||
}
|
||||
export function andFN(
|
||||
x: (gm: GameMap, tile: TileRef) => boolean,
|
||||
y: (gm: GameMap, tile: TileRef) => boolean,
|
||||
): (gm: GameMap, tile: TileRef) => boolean {
|
||||
return (gm: GameMap, tile: TileRef) => x(gm, tile) && y(gm, tile);
|
||||
}
|
||||
|
||||
@@ -1,130 +1,139 @@
|
||||
import { ClientID } from '../Schemas';
|
||||
import { EmojiMessage, GameUpdates, MapPos, MessageType, NameViewData, PlayerID, PlayerType, Tick, UnitType } from './Game';
|
||||
import { TileUpdate } from './GameMap';
|
||||
|
||||
import { ClientID } from "../Schemas";
|
||||
import {
|
||||
EmojiMessage,
|
||||
GameUpdates,
|
||||
MapPos,
|
||||
MessageType,
|
||||
NameViewData,
|
||||
PlayerID,
|
||||
PlayerType,
|
||||
Tick,
|
||||
UnitType,
|
||||
} from "./Game";
|
||||
import { TileUpdate } from "./GameMap";
|
||||
|
||||
export interface GameUpdateViewData {
|
||||
tick: number;
|
||||
updates: GameUpdates;
|
||||
packedTileUpdates: BigUint64Array;
|
||||
playerNameViewData: Record<number, NameViewData>;
|
||||
tick: number;
|
||||
updates: GameUpdates;
|
||||
packedTileUpdates: BigUint64Array;
|
||||
playerNameViewData: Record<number, NameViewData>;
|
||||
}
|
||||
|
||||
export interface ErrorUpdate {
|
||||
errMsg: string
|
||||
stack?: string
|
||||
errMsg: string;
|
||||
stack?: string;
|
||||
}
|
||||
|
||||
export enum GameUpdateType {
|
||||
Tile,
|
||||
Unit,
|
||||
Player,
|
||||
DisplayEvent,
|
||||
AllianceRequest,
|
||||
AllianceRequestReply,
|
||||
BrokeAlliance,
|
||||
AllianceExpired,
|
||||
TargetPlayer,
|
||||
EmojiUpdate,
|
||||
WinUpdate
|
||||
Tile,
|
||||
Unit,
|
||||
Player,
|
||||
DisplayEvent,
|
||||
AllianceRequest,
|
||||
AllianceRequestReply,
|
||||
BrokeAlliance,
|
||||
AllianceExpired,
|
||||
TargetPlayer,
|
||||
EmojiUpdate,
|
||||
WinUpdate,
|
||||
}
|
||||
|
||||
export type GameUpdate = TileUpdateWrapper |
|
||||
UnitUpdate |
|
||||
PlayerUpdate |
|
||||
AllianceRequestUpdate |
|
||||
AllianceRequestReplyUpdate |
|
||||
BrokeAllianceUpdate |
|
||||
AllianceExpiredUpdate |
|
||||
DisplayMessageUpdate |
|
||||
TargetPlayerUpdate |
|
||||
EmojiUpdate |
|
||||
WinUpdate
|
||||
export type GameUpdate =
|
||||
| TileUpdateWrapper
|
||||
| UnitUpdate
|
||||
| PlayerUpdate
|
||||
| AllianceRequestUpdate
|
||||
| AllianceRequestReplyUpdate
|
||||
| BrokeAllianceUpdate
|
||||
| AllianceExpiredUpdate
|
||||
| DisplayMessageUpdate
|
||||
| TargetPlayerUpdate
|
||||
| EmojiUpdate
|
||||
| WinUpdate;
|
||||
|
||||
export interface TileUpdateWrapper {
|
||||
type: GameUpdateType.Tile
|
||||
update: TileUpdate
|
||||
type: GameUpdateType.Tile;
|
||||
update: TileUpdate;
|
||||
}
|
||||
|
||||
export interface UnitUpdate {
|
||||
type: GameUpdateType.Unit
|
||||
unitType: UnitType
|
||||
troops: number
|
||||
id: number
|
||||
ownerID: number
|
||||
pos: MapPos
|
||||
lastPos: MapPos
|
||||
isActive: boolean
|
||||
health?: number
|
||||
type: GameUpdateType.Unit;
|
||||
unitType: UnitType;
|
||||
troops: number;
|
||||
id: number;
|
||||
ownerID: number;
|
||||
pos: MapPos;
|
||||
lastPos: MapPos;
|
||||
isActive: boolean;
|
||||
health?: number;
|
||||
}
|
||||
|
||||
export interface PlayerUpdate {
|
||||
type: GameUpdateType.Player
|
||||
nameViewData?: NameViewData
|
||||
clientID: ClientID
|
||||
name: string
|
||||
displayName: string
|
||||
id: PlayerID
|
||||
smallID: number
|
||||
playerType: PlayerType
|
||||
isAlive: boolean
|
||||
tilesOwned: number
|
||||
gold: number
|
||||
population: number
|
||||
workers: number
|
||||
troops: number
|
||||
targetTroopRatio: number
|
||||
allies: number[]
|
||||
isTraitor: boolean
|
||||
targets: number[]
|
||||
outgoingEmojis: EmojiMessage[]
|
||||
type: GameUpdateType.Player;
|
||||
nameViewData?: NameViewData;
|
||||
clientID: ClientID;
|
||||
name: string;
|
||||
displayName: string;
|
||||
id: PlayerID;
|
||||
smallID: number;
|
||||
playerType: PlayerType;
|
||||
isAlive: boolean;
|
||||
tilesOwned: number;
|
||||
gold: number;
|
||||
population: number;
|
||||
workers: number;
|
||||
troops: number;
|
||||
targetTroopRatio: number;
|
||||
allies: number[];
|
||||
isTraitor: boolean;
|
||||
targets: number[];
|
||||
outgoingEmojis: EmojiMessage[];
|
||||
}
|
||||
|
||||
export interface AllianceRequestUpdate {
|
||||
type: GameUpdateType.AllianceRequest
|
||||
requestorID: number
|
||||
recipientID: number
|
||||
createdAt: Tick
|
||||
type: GameUpdateType.AllianceRequest;
|
||||
requestorID: number;
|
||||
recipientID: number;
|
||||
createdAt: Tick;
|
||||
}
|
||||
|
||||
export interface AllianceRequestReplyUpdate {
|
||||
type: GameUpdateType.AllianceRequestReply
|
||||
request: AllianceRequestUpdate
|
||||
accepted: boolean
|
||||
type: GameUpdateType.AllianceRequestReply;
|
||||
request: AllianceRequestUpdate;
|
||||
accepted: boolean;
|
||||
}
|
||||
|
||||
export interface BrokeAllianceUpdate {
|
||||
type: GameUpdateType.BrokeAlliance
|
||||
traitorID: number
|
||||
betrayedID: number
|
||||
type: GameUpdateType.BrokeAlliance;
|
||||
traitorID: number;
|
||||
betrayedID: number;
|
||||
}
|
||||
|
||||
export interface AllianceExpiredUpdate {
|
||||
type: GameUpdateType.AllianceExpired
|
||||
player1ID: number
|
||||
player2ID: number
|
||||
type: GameUpdateType.AllianceExpired;
|
||||
player1ID: number;
|
||||
player2ID: number;
|
||||
}
|
||||
|
||||
export interface TargetPlayerUpdate {
|
||||
type: GameUpdateType.TargetPlayer
|
||||
playerID: number
|
||||
targetID: number
|
||||
type: GameUpdateType.TargetPlayer;
|
||||
playerID: number;
|
||||
targetID: number;
|
||||
}
|
||||
|
||||
export interface EmojiUpdate {
|
||||
type: GameUpdateType.EmojiUpdate
|
||||
emoji: EmojiMessage
|
||||
type: GameUpdateType.EmojiUpdate;
|
||||
emoji: EmojiMessage;
|
||||
}
|
||||
|
||||
export interface DisplayMessageUpdate {
|
||||
type: GameUpdateType.DisplayEvent
|
||||
message: string
|
||||
messageType: MessageType
|
||||
playerID: number | null
|
||||
type: GameUpdateType.DisplayEvent;
|
||||
message: string;
|
||||
messageType: MessageType;
|
||||
playerID: number | null;
|
||||
}
|
||||
|
||||
export interface WinUpdate {
|
||||
type: GameUpdateType.WinUpdate
|
||||
winnerID: number
|
||||
type: GameUpdateType.WinUpdate;
|
||||
winnerID: number;
|
||||
}
|
||||
|
||||
|
||||
+383
-269
@@ -1,318 +1,432 @@
|
||||
import { GameUpdates, MapPos, MessageType, Player, PlayerActions, PlayerProfile, Unit } from './Game';
|
||||
import {
|
||||
GameUpdates,
|
||||
MapPos,
|
||||
MessageType,
|
||||
Player,
|
||||
PlayerActions,
|
||||
PlayerProfile,
|
||||
Unit,
|
||||
} from "./Game";
|
||||
import { PlayerUpdate } from "./GameUpdates";
|
||||
import { UnitUpdate } from "./GameUpdates";
|
||||
import { NameViewData } from './Game';
|
||||
import { NameViewData } from "./Game";
|
||||
import { GameUpdateType } from "./GameUpdates";
|
||||
import { Config } from "../configuration/Config";
|
||||
import { Alliance, AllianceRequest, AllPlayers, Cell, DefenseBonus, EmojiMessage, Game, Gold, Nation, PlayerID, PlayerInfo, PlayerType, Relation, TerrainType, TerraNullius, Tick, UnitInfo, UnitType } from "./Game";
|
||||
import {
|
||||
Alliance,
|
||||
AllianceRequest,
|
||||
AllPlayers,
|
||||
Cell,
|
||||
DefenseBonus,
|
||||
EmojiMessage,
|
||||
Game,
|
||||
Gold,
|
||||
Nation,
|
||||
PlayerID,
|
||||
PlayerInfo,
|
||||
PlayerType,
|
||||
Relation,
|
||||
TerrainType,
|
||||
TerraNullius,
|
||||
Tick,
|
||||
UnitInfo,
|
||||
UnitType,
|
||||
} from "./Game";
|
||||
import { ClientID } from "../Schemas";
|
||||
import { TerraNulliusImpl } from './TerraNulliusImpl';
|
||||
import { WorkerClient } from '../worker/WorkerClient';
|
||||
import { GameMap, GameMapImpl, TileRef, TileUpdate } from './GameMap';
|
||||
import { GameUpdateViewData } from './GameUpdates';
|
||||
import { TerraNulliusImpl } from "./TerraNulliusImpl";
|
||||
import { WorkerClient } from "../worker/WorkerClient";
|
||||
import { GameMap, GameMapImpl, TileRef, TileUpdate } from "./GameMap";
|
||||
import { GameUpdateViewData } from "./GameUpdates";
|
||||
|
||||
export class UnitView {
|
||||
public _wasUpdated = true
|
||||
public lastPos: MapPos[] = []
|
||||
public _wasUpdated = true;
|
||||
public lastPos: MapPos[] = [];
|
||||
|
||||
constructor(private gameView: GameView, private data: UnitUpdate) {
|
||||
this.lastPos.push(data.pos)
|
||||
}
|
||||
constructor(
|
||||
private gameView: GameView,
|
||||
private data: UnitUpdate,
|
||||
) {
|
||||
this.lastPos.push(data.pos);
|
||||
}
|
||||
|
||||
wasUpdated(): boolean {
|
||||
return this._wasUpdated
|
||||
}
|
||||
wasUpdated(): boolean {
|
||||
return this._wasUpdated;
|
||||
}
|
||||
|
||||
lastTiles(): TileRef[] {
|
||||
return this.lastPos.map(pos => this.gameView.ref(pos.x, pos.y))
|
||||
}
|
||||
lastTiles(): TileRef[] {
|
||||
return this.lastPos.map((pos) => this.gameView.ref(pos.x, pos.y));
|
||||
}
|
||||
|
||||
lastTile(): TileRef {
|
||||
if (this.lastPos.length == 0) {
|
||||
return this.gameView.ref(this.data.pos.x, this.data.pos.y)
|
||||
}
|
||||
return this.gameView.ref(this.lastPos[0].x, this.lastPos[0].y)
|
||||
lastTile(): TileRef {
|
||||
if (this.lastPos.length == 0) {
|
||||
return this.gameView.ref(this.data.pos.x, this.data.pos.y);
|
||||
}
|
||||
return this.gameView.ref(this.lastPos[0].x, this.lastPos[0].y);
|
||||
}
|
||||
|
||||
update(data: UnitUpdate) {
|
||||
this.lastPos.push(data.pos)
|
||||
this._wasUpdated = true
|
||||
this.data = data
|
||||
}
|
||||
update(data: UnitUpdate) {
|
||||
this.lastPos.push(data.pos);
|
||||
this._wasUpdated = true;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
id(): number {
|
||||
return this.data.id
|
||||
}
|
||||
id(): number {
|
||||
return this.data.id;
|
||||
}
|
||||
|
||||
type(): UnitType {
|
||||
return this.data.unitType
|
||||
}
|
||||
troops(): number {
|
||||
return this.data.troops
|
||||
}
|
||||
tile(): TileRef {
|
||||
return this.gameView.ref(this.data.pos.x, this.data.pos.y)
|
||||
}
|
||||
owner(): PlayerView {
|
||||
return this.gameView.playerBySmallID(this.data.ownerID) as PlayerView
|
||||
}
|
||||
isActive(): boolean {
|
||||
return this.data.isActive
|
||||
}
|
||||
hasHealth(): boolean {
|
||||
return this.data.health != undefined
|
||||
}
|
||||
health(): number {
|
||||
return this.data.health ?? 0
|
||||
}
|
||||
type(): UnitType {
|
||||
return this.data.unitType;
|
||||
}
|
||||
troops(): number {
|
||||
return this.data.troops;
|
||||
}
|
||||
tile(): TileRef {
|
||||
return this.gameView.ref(this.data.pos.x, this.data.pos.y);
|
||||
}
|
||||
owner(): PlayerView {
|
||||
return this.gameView.playerBySmallID(this.data.ownerID) as PlayerView;
|
||||
}
|
||||
isActive(): boolean {
|
||||
return this.data.isActive;
|
||||
}
|
||||
hasHealth(): boolean {
|
||||
return this.data.health != undefined;
|
||||
}
|
||||
health(): number {
|
||||
return this.data.health ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
export class PlayerView {
|
||||
constructor(
|
||||
private game: GameView,
|
||||
public data: PlayerUpdate,
|
||||
public nameData: NameViewData,
|
||||
) {}
|
||||
|
||||
constructor(private game: GameView, public data: PlayerUpdate, public nameData: NameViewData) { }
|
||||
async actions(tile: TileRef): Promise<PlayerActions> {
|
||||
return this.game.worker.playerInteraction(
|
||||
this.id(),
|
||||
this.game.x(tile),
|
||||
this.game.y(tile),
|
||||
);
|
||||
}
|
||||
|
||||
async actions(tile: TileRef): Promise<PlayerActions> {
|
||||
return this.game.worker.playerInteraction(this.id(), this.game.x(tile), this.game.y(tile))
|
||||
}
|
||||
units(): UnitView[] {
|
||||
return this.game
|
||||
.units()
|
||||
.filter((u) => u.owner().smallID() == this.smallID());
|
||||
}
|
||||
|
||||
units(): UnitView[] {
|
||||
return this.game.units().filter(u => u.owner().smallID() == this.smallID())
|
||||
}
|
||||
nameLocation(): NameViewData {
|
||||
return this.nameData;
|
||||
}
|
||||
|
||||
nameLocation(): NameViewData {
|
||||
return this.nameData
|
||||
}
|
||||
smallID(): number {
|
||||
return this.data.smallID;
|
||||
}
|
||||
name(): string {
|
||||
return this.data.name;
|
||||
}
|
||||
displayName(): string {
|
||||
return this.data.displayName;
|
||||
}
|
||||
clientID(): ClientID {
|
||||
return this.data.clientID;
|
||||
}
|
||||
id(): PlayerID {
|
||||
return this.data.id;
|
||||
}
|
||||
type(): PlayerType {
|
||||
return this.data.playerType;
|
||||
}
|
||||
isAlive(): boolean {
|
||||
return this.data.isAlive;
|
||||
}
|
||||
isPlayer(): this is Player {
|
||||
return true;
|
||||
}
|
||||
numTilesOwned(): number {
|
||||
return this.data.tilesOwned;
|
||||
}
|
||||
allies(): PlayerView[] {
|
||||
return this.data.allies.map(
|
||||
(a) => this.game.playerBySmallID(a) as PlayerView,
|
||||
);
|
||||
}
|
||||
targets(): PlayerView[] {
|
||||
return this.data.targets.map(
|
||||
(id) => this.game.playerBySmallID(id) as PlayerView,
|
||||
);
|
||||
}
|
||||
gold(): Gold {
|
||||
return this.data.gold;
|
||||
}
|
||||
population(): number {
|
||||
return this.data.population;
|
||||
}
|
||||
workers(): number {
|
||||
return this.data.workers;
|
||||
}
|
||||
targetTroopRatio(): number {
|
||||
return this.data.targetTroopRatio;
|
||||
}
|
||||
troops(): number {
|
||||
return this.data.troops;
|
||||
}
|
||||
|
||||
smallID(): number {
|
||||
return this.data.smallID
|
||||
}
|
||||
name(): string {
|
||||
return this.data.name
|
||||
}
|
||||
displayName(): string {
|
||||
return this.data.displayName
|
||||
}
|
||||
clientID(): ClientID {
|
||||
return this.data.clientID
|
||||
}
|
||||
id(): PlayerID {
|
||||
return this.data.id
|
||||
}
|
||||
type(): PlayerType {
|
||||
return this.data.playerType
|
||||
}
|
||||
isAlive(): boolean {
|
||||
return this.data.isAlive
|
||||
}
|
||||
isPlayer(): this is Player {
|
||||
return true
|
||||
}
|
||||
numTilesOwned(): number {
|
||||
return this.data.tilesOwned
|
||||
}
|
||||
allies(): PlayerView[] {
|
||||
return this.data.allies.map(a => this.game.playerBySmallID(a) as PlayerView)
|
||||
}
|
||||
targets(): PlayerView[] {
|
||||
return this.data.targets.map(id => this.game.playerBySmallID(id) as PlayerView)
|
||||
}
|
||||
gold(): Gold {
|
||||
return this.data.gold
|
||||
}
|
||||
population(): number {
|
||||
return this.data.population
|
||||
}
|
||||
workers(): number {
|
||||
return this.data.workers
|
||||
}
|
||||
targetTroopRatio(): number {
|
||||
return this.data.targetTroopRatio
|
||||
}
|
||||
troops(): number {
|
||||
return this.data.troops
|
||||
}
|
||||
isAlliedWith(other: PlayerView): boolean {
|
||||
return this.data.allies.some((n) => other.smallID() == n);
|
||||
}
|
||||
|
||||
isAlliedWith(other: PlayerView): boolean {
|
||||
return this.data.allies.some(n => other.smallID() == n)
|
||||
}
|
||||
profile(): Promise<PlayerProfile> {
|
||||
return this.game.worker.playerProfile(this.smallID());
|
||||
}
|
||||
|
||||
profile(): Promise<PlayerProfile> {
|
||||
return this.game.worker.playerProfile(this.smallID())
|
||||
}
|
||||
transitiveTargets(): PlayerView[] {
|
||||
return [...this.targets(), ...this.allies().flatMap((p) => p.targets())];
|
||||
}
|
||||
|
||||
transitiveTargets(): PlayerView[] {
|
||||
return [...this.targets(), ...this.allies().flatMap(p => p.targets())]
|
||||
}
|
||||
|
||||
isTraitor(): boolean {
|
||||
return this.data.isTraitor
|
||||
}
|
||||
outgoingEmojis(): EmojiMessage[] {
|
||||
return this.data.outgoingEmojis
|
||||
}
|
||||
info(): PlayerInfo {
|
||||
return new PlayerInfo(this.name(), this.type(), this.clientID(), this.id())
|
||||
}
|
||||
isTraitor(): boolean {
|
||||
return this.data.isTraitor;
|
||||
}
|
||||
outgoingEmojis(): EmojiMessage[] {
|
||||
return this.data.outgoingEmojis;
|
||||
}
|
||||
info(): PlayerInfo {
|
||||
return new PlayerInfo(this.name(), this.type(), this.clientID(), this.id());
|
||||
}
|
||||
}
|
||||
|
||||
export class GameView implements GameMap {
|
||||
private lastUpdate: GameUpdateViewData
|
||||
private smallIDToID = new Map<number, PlayerID>()
|
||||
private _players = new Map<PlayerID, PlayerView>()
|
||||
private _units = new Map<number, UnitView>()
|
||||
private updatedTiles: TileRef[] = []
|
||||
private lastUpdate: GameUpdateViewData;
|
||||
private smallIDToID = new Map<number, PlayerID>();
|
||||
private _players = new Map<PlayerID, PlayerView>();
|
||||
private _units = new Map<number, UnitView>();
|
||||
private updatedTiles: TileRef[] = [];
|
||||
|
||||
private _myPlayer: PlayerView | null = null;
|
||||
|
||||
private _myPlayer: PlayerView | null = null
|
||||
constructor(
|
||||
public worker: WorkerClient,
|
||||
private _config: Config,
|
||||
private _map: GameMap,
|
||||
private _myClientID: ClientID,
|
||||
) {
|
||||
this.lastUpdate = {
|
||||
tick: 0,
|
||||
packedTileUpdates: new BigUint64Array([]),
|
||||
// TODO: make this empty map instead of null?
|
||||
updates: null,
|
||||
playerNameViewData: {},
|
||||
};
|
||||
}
|
||||
|
||||
constructor(
|
||||
public worker: WorkerClient,
|
||||
private _config: Config,
|
||||
private _map: GameMap,
|
||||
private _myClientID: ClientID
|
||||
) {
|
||||
this.lastUpdate = {
|
||||
tick: 0,
|
||||
packedTileUpdates: new BigUint64Array([]),
|
||||
// TODO: make this empty map instead of null?
|
||||
updates: null,
|
||||
playerNameViewData: {},
|
||||
}
|
||||
}
|
||||
public updatesSinceLastTick(): GameUpdates {
|
||||
return this.lastUpdate.updates;
|
||||
}
|
||||
|
||||
public updatesSinceLastTick(): GameUpdates {
|
||||
return this.lastUpdate.updates
|
||||
}
|
||||
public update(gu: GameUpdateViewData) {
|
||||
this.lastUpdate = gu;
|
||||
|
||||
public update(gu: GameUpdateViewData) {
|
||||
this.lastUpdate = gu
|
||||
this.updatedTiles = [];
|
||||
this.lastUpdate.packedTileUpdates.forEach((tu) => {
|
||||
this.updatedTiles.push(this.updateTile(tu));
|
||||
});
|
||||
|
||||
this.updatedTiles = []
|
||||
this.lastUpdate.packedTileUpdates.forEach(tu => {
|
||||
this.updatedTiles.push(this.updateTile(tu))
|
||||
})
|
||||
gu.updates[GameUpdateType.Player].forEach((pu) => {
|
||||
this.smallIDToID.set(pu.smallID, pu.id);
|
||||
if (this._players.has(pu.id)) {
|
||||
this._players.get(pu.id).data = pu;
|
||||
this._players.get(pu.id).nameData = gu.playerNameViewData[pu.id];
|
||||
} else {
|
||||
this._players.set(
|
||||
pu.id,
|
||||
new PlayerView(this, pu, gu.playerNameViewData[pu.id]),
|
||||
);
|
||||
}
|
||||
});
|
||||
for (const unit of this._units.values()) {
|
||||
unit._wasUpdated = false;
|
||||
unit.lastPos = unit.lastPos.slice(-1);
|
||||
}
|
||||
gu.updates[GameUpdateType.Unit].forEach((unit) => {
|
||||
if (this._units.has(unit.id)) {
|
||||
this._units.get(unit.id).update(unit);
|
||||
} else {
|
||||
this._units.set(unit.id, new UnitView(this, unit));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
gu.updates[GameUpdateType.Player].forEach((pu) => {
|
||||
this.smallIDToID.set(pu.smallID, pu.id);
|
||||
if (this._players.has(pu.id)) {
|
||||
this._players.get(pu.id).data = pu
|
||||
this._players.get(pu.id).nameData = gu.playerNameViewData[pu.id]
|
||||
} else {
|
||||
this._players.set(pu.id, new PlayerView(this, pu, gu.playerNameViewData[pu.id]))
|
||||
}
|
||||
});
|
||||
for (const unit of this._units.values()) {
|
||||
unit._wasUpdated = false
|
||||
unit.lastPos = unit.lastPos.slice(-1)
|
||||
}
|
||||
gu.updates[GameUpdateType.Unit].forEach(unit => {
|
||||
if (this._units.has(unit.id)) {
|
||||
this._units.get(unit.id).update(unit)
|
||||
} else {
|
||||
this._units.set(unit.id, new UnitView(this, unit))
|
||||
}
|
||||
})
|
||||
}
|
||||
recentlyUpdatedTiles(): TileRef[] {
|
||||
return this.updatedTiles;
|
||||
}
|
||||
|
||||
recentlyUpdatedTiles(): TileRef[] {
|
||||
return this.updatedTiles
|
||||
}
|
||||
myClientID(): ClientID {
|
||||
return this._myClientID;
|
||||
}
|
||||
|
||||
myClientID(): ClientID {
|
||||
return this._myClientID
|
||||
myPlayer(): PlayerView | null {
|
||||
if (this._myPlayer == null) {
|
||||
this._myPlayer = this.playerByClientID(this._myClientID);
|
||||
}
|
||||
return this._myPlayer;
|
||||
}
|
||||
|
||||
myPlayer(): PlayerView | null {
|
||||
if (this._myPlayer == null) {
|
||||
this._myPlayer = this.playerByClientID(this._myClientID)
|
||||
}
|
||||
return this._myPlayer
|
||||
player(id: PlayerID): PlayerView {
|
||||
if (this._players.has(id)) {
|
||||
return this._players.get(id);
|
||||
}
|
||||
throw Error(`player id ${id} not found`);
|
||||
}
|
||||
|
||||
player(id: PlayerID): PlayerView {
|
||||
if (this._players.has(id)) {
|
||||
return this._players.get(id)
|
||||
}
|
||||
throw Error(`player id ${id} not found`)
|
||||
playerBySmallID(id: number): PlayerView | TerraNullius {
|
||||
if (id == 0) {
|
||||
return new TerraNulliusImpl();
|
||||
}
|
||||
if (!this.smallIDToID.has(id)) {
|
||||
throw new Error(`small id ${id} not found`);
|
||||
}
|
||||
return this.player(this.smallIDToID.get(id));
|
||||
}
|
||||
|
||||
playerBySmallID(id: number): PlayerView | TerraNullius {
|
||||
if (id == 0) {
|
||||
return new TerraNulliusImpl()
|
||||
}
|
||||
if (!this.smallIDToID.has(id)) {
|
||||
throw new Error(`small id ${id} not found`)
|
||||
}
|
||||
return this.player(this.smallIDToID.get(id))
|
||||
playerByClientID(id: ClientID): PlayerView | null {
|
||||
const player =
|
||||
Array.from(this._players.values()).filter((p) => p.clientID() == id)[0] ??
|
||||
null;
|
||||
if (player == null) {
|
||||
return null;
|
||||
}
|
||||
return player;
|
||||
}
|
||||
hasPlayer(id: PlayerID): boolean {
|
||||
return false;
|
||||
}
|
||||
playerViews(): PlayerView[] {
|
||||
return Array.from(this._players.values());
|
||||
}
|
||||
|
||||
playerByClientID(id: ClientID): PlayerView | null {
|
||||
const player = Array.from(this._players.values()).filter(p => p.clientID() == id)[0] ?? null
|
||||
if (player == null) {
|
||||
return null
|
||||
}
|
||||
return player
|
||||
}
|
||||
hasPlayer(id: PlayerID): boolean {
|
||||
return false
|
||||
}
|
||||
playerViews(): PlayerView[] {
|
||||
return Array.from(this._players.values())
|
||||
}
|
||||
owner(tile: TileRef): PlayerView | TerraNullius {
|
||||
return this.playerBySmallID(this.ownerID(tile));
|
||||
}
|
||||
|
||||
owner(tile: TileRef): PlayerView | TerraNullius {
|
||||
return this.playerBySmallID(this.ownerID(tile))
|
||||
}
|
||||
ticks(): Tick {
|
||||
return this.lastUpdate.tick;
|
||||
}
|
||||
inSpawnPhase(): boolean {
|
||||
return this.lastUpdate.tick <= this._config.numSpawnPhaseTurns();
|
||||
}
|
||||
config(): Config {
|
||||
return this._config;
|
||||
}
|
||||
units(...types: UnitType[]): UnitView[] {
|
||||
return Array.from(this._units.values());
|
||||
}
|
||||
unit(id: number): UnitView {
|
||||
return this._units.get(id);
|
||||
}
|
||||
unitInfo(type: UnitType): UnitInfo {
|
||||
return this._config.unitInfo(type);
|
||||
}
|
||||
|
||||
ticks(): Tick {
|
||||
return this.lastUpdate.tick
|
||||
}
|
||||
inSpawnPhase(): boolean {
|
||||
return this.lastUpdate.tick <= this._config.numSpawnPhaseTurns()
|
||||
}
|
||||
config(): Config {
|
||||
return this._config
|
||||
}
|
||||
units(...types: UnitType[]): UnitView[] {
|
||||
return Array.from(this._units.values())
|
||||
}
|
||||
unit(id: number): UnitView {
|
||||
return this._units.get(id)
|
||||
}
|
||||
unitInfo(type: UnitType): UnitInfo {
|
||||
return this._config.unitInfo(type)
|
||||
}
|
||||
|
||||
ref(x: number, y: number): TileRef { return this._map.ref(x, y) }
|
||||
x(ref: TileRef): number { return this._map.x(ref) }
|
||||
y(ref: TileRef): number { return this._map.y(ref) }
|
||||
cell(ref: TileRef): Cell { return this._map.cell(ref) }
|
||||
width(): number { return this._map.width() }
|
||||
height(): number { return this._map.height() }
|
||||
numLandTiles(): number { return this._map.numLandTiles() }
|
||||
isValidCoord(x: number, y: number): boolean { return this._map.isValidCoord(x, y) }
|
||||
isLand(ref: TileRef): boolean { return this._map.isLand(ref) }
|
||||
isOceanShore(ref: TileRef): boolean { return this._map.isOceanShore(ref) }
|
||||
isOcean(ref: TileRef): boolean { return this._map.isOcean(ref) }
|
||||
isShoreline(ref: TileRef): boolean { return this._map.isShoreline(ref) }
|
||||
magnitude(ref: TileRef): number { return this._map.magnitude(ref) }
|
||||
ownerID(ref: TileRef): number { return this._map.ownerID(ref) }
|
||||
hasOwner(ref: TileRef): boolean { return this._map.hasOwner(ref) }
|
||||
setOwnerID(ref: TileRef, playerId: number): void { return this._map.setOwnerID(ref, playerId) }
|
||||
hasFallout(ref: TileRef): boolean { return this._map.hasFallout(ref) }
|
||||
setFallout(ref: TileRef, value: boolean): void { return this._map.setFallout(ref, value) }
|
||||
isBorder(ref: TileRef): boolean { return this._map.isBorder(ref) }
|
||||
neighbors(ref: TileRef): TileRef[] { return this._map.neighbors(ref) }
|
||||
isWater(ref: TileRef): boolean { return this._map.isWater(ref) }
|
||||
isLake(ref: TileRef): boolean { return this._map.isLake(ref) }
|
||||
isShore(ref: TileRef): boolean { return this._map.isShore(ref) }
|
||||
cost(ref: TileRef): number { return this._map.cost(ref) }
|
||||
terrainType(ref: TileRef): TerrainType { return this._map.terrainType(ref) }
|
||||
forEachTile(fn: (tile: TileRef) => void): void { return this._map.forEachTile(fn) }
|
||||
manhattanDist(c1: TileRef, c2: TileRef): number { return this._map.manhattanDist(c1, c2) }
|
||||
euclideanDist(c1: TileRef, c2: TileRef): number { return this._map.euclideanDist(c1, c2) }
|
||||
bfs(tile: TileRef, filter: (gm: GameMap, tile: TileRef) => boolean): Set<TileRef> { return this._map.bfs(tile, filter) }
|
||||
toTileUpdate(tile: TileRef): bigint { return this._map.toTileUpdate(tile) }
|
||||
updateTile(tu: TileUpdate): TileRef { return this._map.updateTile(tu) }
|
||||
numTilesWithFallout(): number { return this._map.numTilesWithFallout() }
|
||||
ref(x: number, y: number): TileRef {
|
||||
return this._map.ref(x, y);
|
||||
}
|
||||
x(ref: TileRef): number {
|
||||
return this._map.x(ref);
|
||||
}
|
||||
y(ref: TileRef): number {
|
||||
return this._map.y(ref);
|
||||
}
|
||||
cell(ref: TileRef): Cell {
|
||||
return this._map.cell(ref);
|
||||
}
|
||||
width(): number {
|
||||
return this._map.width();
|
||||
}
|
||||
height(): number {
|
||||
return this._map.height();
|
||||
}
|
||||
numLandTiles(): number {
|
||||
return this._map.numLandTiles();
|
||||
}
|
||||
isValidCoord(x: number, y: number): boolean {
|
||||
return this._map.isValidCoord(x, y);
|
||||
}
|
||||
isLand(ref: TileRef): boolean {
|
||||
return this._map.isLand(ref);
|
||||
}
|
||||
isOceanShore(ref: TileRef): boolean {
|
||||
return this._map.isOceanShore(ref);
|
||||
}
|
||||
isOcean(ref: TileRef): boolean {
|
||||
return this._map.isOcean(ref);
|
||||
}
|
||||
isShoreline(ref: TileRef): boolean {
|
||||
return this._map.isShoreline(ref);
|
||||
}
|
||||
magnitude(ref: TileRef): number {
|
||||
return this._map.magnitude(ref);
|
||||
}
|
||||
ownerID(ref: TileRef): number {
|
||||
return this._map.ownerID(ref);
|
||||
}
|
||||
hasOwner(ref: TileRef): boolean {
|
||||
return this._map.hasOwner(ref);
|
||||
}
|
||||
setOwnerID(ref: TileRef, playerId: number): void {
|
||||
return this._map.setOwnerID(ref, playerId);
|
||||
}
|
||||
hasFallout(ref: TileRef): boolean {
|
||||
return this._map.hasFallout(ref);
|
||||
}
|
||||
setFallout(ref: TileRef, value: boolean): void {
|
||||
return this._map.setFallout(ref, value);
|
||||
}
|
||||
isBorder(ref: TileRef): boolean {
|
||||
return this._map.isBorder(ref);
|
||||
}
|
||||
neighbors(ref: TileRef): TileRef[] {
|
||||
return this._map.neighbors(ref);
|
||||
}
|
||||
isWater(ref: TileRef): boolean {
|
||||
return this._map.isWater(ref);
|
||||
}
|
||||
isLake(ref: TileRef): boolean {
|
||||
return this._map.isLake(ref);
|
||||
}
|
||||
isShore(ref: TileRef): boolean {
|
||||
return this._map.isShore(ref);
|
||||
}
|
||||
cost(ref: TileRef): number {
|
||||
return this._map.cost(ref);
|
||||
}
|
||||
terrainType(ref: TileRef): TerrainType {
|
||||
return this._map.terrainType(ref);
|
||||
}
|
||||
forEachTile(fn: (tile: TileRef) => void): void {
|
||||
return this._map.forEachTile(fn);
|
||||
}
|
||||
manhattanDist(c1: TileRef, c2: TileRef): number {
|
||||
return this._map.manhattanDist(c1, c2);
|
||||
}
|
||||
euclideanDist(c1: TileRef, c2: TileRef): number {
|
||||
return this._map.euclideanDist(c1, c2);
|
||||
}
|
||||
bfs(
|
||||
tile: TileRef,
|
||||
filter: (gm: GameMap, tile: TileRef) => boolean,
|
||||
): Set<TileRef> {
|
||||
return this._map.bfs(tile, filter);
|
||||
}
|
||||
toTileUpdate(tile: TileRef): bigint {
|
||||
return this._map.toTileUpdate(tile);
|
||||
}
|
||||
updateTile(tu: TileUpdate): TileRef {
|
||||
return this._map.updateTile(tu);
|
||||
}
|
||||
numTilesWithFallout(): number {
|
||||
return this._map.numTilesWithFallout();
|
||||
}
|
||||
}
|
||||
|
||||
+711
-573
File diff suppressed because it is too large
Load Diff
@@ -3,22 +3,20 @@ import { TerraNullius, Cell, PlayerID } from "./Game";
|
||||
import { GameImpl } from "./GameImpl";
|
||||
import { TileRef } from "./GameMap";
|
||||
|
||||
|
||||
export class TerraNulliusImpl implements TerraNullius {
|
||||
constructor() {}
|
||||
smallID(): number {
|
||||
return 0;
|
||||
}
|
||||
clientID(): ClientID {
|
||||
return "TERRA_NULLIUS_CLIENT_ID";
|
||||
}
|
||||
|
||||
id(): PlayerID {
|
||||
return null;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
}
|
||||
smallID(): number {
|
||||
return 0
|
||||
}
|
||||
clientID(): ClientID {
|
||||
return "TERRA_NULLIUS_CLIENT_ID"
|
||||
}
|
||||
|
||||
id(): PlayerID {
|
||||
return null
|
||||
}
|
||||
|
||||
isPlayer(): false { return false as const; }
|
||||
isPlayer(): false {
|
||||
return false as const;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,88 +1,94 @@
|
||||
import { Cell, GameMapType, TerrainType } from './Game';
|
||||
import { consolex } from '../Consolex';
|
||||
import { NationMap } from './TerrainMapLoader';
|
||||
import { Cell, GameMapType, TerrainType } from "./Game";
|
||||
import { consolex } from "../Consolex";
|
||||
import { NationMap } from "./TerrainMapLoader";
|
||||
|
||||
interface MapData {
|
||||
mapBin: string;
|
||||
miniMapBin: string;
|
||||
nationMap: NationMap;
|
||||
mapBin: string;
|
||||
miniMapBin: string;
|
||||
nationMap: NationMap;
|
||||
}
|
||||
|
||||
interface MapCache {
|
||||
bin?: string;
|
||||
miniMapBin?: string
|
||||
nationMap?: NationMap;
|
||||
bin?: string;
|
||||
miniMapBin?: string;
|
||||
nationMap?: NationMap;
|
||||
}
|
||||
|
||||
interface BinModule {
|
||||
default: string;
|
||||
default: string;
|
||||
}
|
||||
|
||||
interface NationMapModule {
|
||||
default: NationMap;
|
||||
default: NationMap;
|
||||
}
|
||||
|
||||
// Mapping from GameMap enum values to file names
|
||||
const MAP_FILE_NAMES: Record<GameMapType, string> = {
|
||||
[GameMapType.World]: 'WorldMap',
|
||||
[GameMapType.Europe]: 'Europe',
|
||||
[GameMapType.Mena]: 'Mena',
|
||||
[GameMapType.NorthAmerica]: 'NorthAmerica',
|
||||
[GameMapType.Oceania]: 'Oceania',
|
||||
[GameMapType.BlackSea]: 'BlackSea',
|
||||
[GameMapType.World]: "WorldMap",
|
||||
[GameMapType.Europe]: "Europe",
|
||||
[GameMapType.Mena]: "Mena",
|
||||
[GameMapType.NorthAmerica]: "NorthAmerica",
|
||||
[GameMapType.Oceania]: "Oceania",
|
||||
[GameMapType.BlackSea]: "BlackSea",
|
||||
};
|
||||
|
||||
class GameMapLoader {
|
||||
private maps: Map<GameMapType, MapCache>;
|
||||
private loadingPromises: Map<GameMapType, Promise<MapData>>;
|
||||
private maps: Map<GameMapType, MapCache>;
|
||||
private loadingPromises: Map<GameMapType, Promise<MapData>>;
|
||||
|
||||
constructor() {
|
||||
this.maps = new Map<GameMapType, MapCache>();
|
||||
this.loadingPromises = new Map<GameMapType, Promise<MapData>>();
|
||||
constructor() {
|
||||
this.maps = new Map<GameMapType, MapCache>();
|
||||
this.loadingPromises = new Map<GameMapType, Promise<MapData>>();
|
||||
}
|
||||
|
||||
public async getMapData(map: GameMapType): Promise<MapData> {
|
||||
const cachedMap = this.maps.get(map);
|
||||
if (cachedMap?.bin && cachedMap?.nationMap) {
|
||||
return cachedMap as MapData;
|
||||
}
|
||||
|
||||
public async getMapData(map: GameMapType): Promise<MapData> {
|
||||
const cachedMap = this.maps.get(map);
|
||||
if (cachedMap?.bin && cachedMap?.nationMap) {
|
||||
return cachedMap as MapData;
|
||||
}
|
||||
|
||||
if (!this.loadingPromises.has(map)) {
|
||||
this.loadingPromises.set(map, this.loadMapData(map));
|
||||
}
|
||||
|
||||
const data = await this.loadingPromises.get(map)!;
|
||||
this.maps.set(map, data);
|
||||
return data;
|
||||
if (!this.loadingPromises.has(map)) {
|
||||
this.loadingPromises.set(map, this.loadMapData(map));
|
||||
}
|
||||
|
||||
private async loadMapData(map: GameMapType): Promise<MapData> {
|
||||
const fileName = MAP_FILE_NAMES[map];
|
||||
if (!fileName) {
|
||||
throw new Error(`No file name mapping found for map: ${map}`);
|
||||
}
|
||||
const data = await this.loadingPromises.get(map)!;
|
||||
this.maps.set(map, data);
|
||||
return data;
|
||||
}
|
||||
|
||||
const [binModule, miniBinModule, infoModule] = await Promise.all([
|
||||
import(`!!binary-loader!../../../resources/maps/${fileName}.bin`) as Promise<BinModule>,
|
||||
import(`!!binary-loader!../../../resources/maps/${fileName}Mini.bin`) as Promise<BinModule>,
|
||||
import(`../../../resources/maps/${fileName}.json`) as Promise<NationMapModule>
|
||||
]);
|
||||
|
||||
return {
|
||||
mapBin: binModule.default,
|
||||
miniMapBin: miniBinModule.default,
|
||||
nationMap: infoModule.default
|
||||
};
|
||||
private async loadMapData(map: GameMapType): Promise<MapData> {
|
||||
const fileName = MAP_FILE_NAMES[map];
|
||||
if (!fileName) {
|
||||
throw new Error(`No file name mapping found for map: ${map}`);
|
||||
}
|
||||
|
||||
public isMapLoaded(map: GameMapType): boolean {
|
||||
const mapData = this.maps.get(map);
|
||||
return !!mapData?.bin && !!mapData?.nationMap;
|
||||
}
|
||||
const [binModule, miniBinModule, infoModule] = await Promise.all([
|
||||
import(
|
||||
`!!binary-loader!../../../resources/maps/${fileName}.bin`
|
||||
) as Promise<BinModule>,
|
||||
import(
|
||||
`!!binary-loader!../../../resources/maps/${fileName}Mini.bin`
|
||||
) as Promise<BinModule>,
|
||||
import(
|
||||
`../../../resources/maps/${fileName}.json`
|
||||
) as Promise<NationMapModule>,
|
||||
]);
|
||||
|
||||
public getLoadedMaps(): GameMapType[] {
|
||||
return Array.from(this.maps.keys()).filter(map => this.isMapLoaded(map));
|
||||
}
|
||||
return {
|
||||
mapBin: binModule.default,
|
||||
miniMapBin: miniBinModule.default,
|
||||
nationMap: infoModule.default,
|
||||
};
|
||||
}
|
||||
|
||||
public isMapLoaded(map: GameMapType): boolean {
|
||||
const mapData = this.maps.get(map);
|
||||
return !!mapData?.bin && !!mapData?.nationMap;
|
||||
}
|
||||
|
||||
public getLoadedMaps(): GameMapType[] {
|
||||
return Array.from(this.maps.keys()).filter((map) => this.isMapLoaded(map));
|
||||
}
|
||||
}
|
||||
|
||||
export const terrainMapFileLoader = new GameMapLoader();
|
||||
export const terrainMapFileLoader = new GameMapLoader();
|
||||
|
||||
@@ -1,69 +1,77 @@
|
||||
import { consolex } from '../Consolex';
|
||||
import { Cell, GameMapType, TerrainType } from './Game';
|
||||
import { GameMap, GameMapImpl } from './GameMap';
|
||||
import { terrainMapFileLoader } from './TerrainMapFileLoader';
|
||||
import { consolex } from "../Consolex";
|
||||
import { Cell, GameMapType, TerrainType } from "./Game";
|
||||
import { GameMap, GameMapImpl } from "./GameMap";
|
||||
import { terrainMapFileLoader } from "./TerrainMapFileLoader";
|
||||
|
||||
const loadedMaps = new Map<GameMapType, { nationMap: NationMap, gameMap: GameMap, miniGameMap: GameMap }>()
|
||||
const loadedMaps = new Map<
|
||||
GameMapType,
|
||||
{ nationMap: NationMap; gameMap: GameMap; miniGameMap: GameMap }
|
||||
>();
|
||||
|
||||
export interface NationMap {
|
||||
name: string;
|
||||
width: number;
|
||||
height: number;
|
||||
nations: Nation[];
|
||||
name: string;
|
||||
width: number;
|
||||
height: number;
|
||||
nations: Nation[];
|
||||
}
|
||||
|
||||
export interface Nation {
|
||||
coordinates: [number, number];
|
||||
name: string;
|
||||
strength: number;
|
||||
coordinates: [number, number];
|
||||
name: string;
|
||||
strength: number;
|
||||
}
|
||||
|
||||
export async function loadTerrainMap(map: GameMapType): Promise<{ nationMap: NationMap, gameMap: GameMap, miniGameMap: GameMap }> {
|
||||
if (loadedMaps.has(map)) {
|
||||
return loadedMaps.get(map)
|
||||
}
|
||||
const mapFiles = await terrainMapFileLoader.getMapData(map)
|
||||
export async function loadTerrainMap(
|
||||
map: GameMapType,
|
||||
): Promise<{ nationMap: NationMap; gameMap: GameMap; miniGameMap: GameMap }> {
|
||||
if (loadedMaps.has(map)) {
|
||||
return loadedMaps.get(map);
|
||||
}
|
||||
const mapFiles = await terrainMapFileLoader.getMapData(map);
|
||||
|
||||
const gameMap = await loadTerrainFromFile(mapFiles.mapBin)
|
||||
const miniGameMap = await loadTerrainFromFile(mapFiles.miniMapBin)
|
||||
const result = { nationMap: mapFiles.nationMap, gameMap: gameMap, miniGameMap: miniGameMap }
|
||||
loadedMaps.set(map, result)
|
||||
return result
|
||||
const gameMap = await loadTerrainFromFile(mapFiles.mapBin);
|
||||
const miniGameMap = await loadTerrainFromFile(mapFiles.miniMapBin);
|
||||
const result = {
|
||||
nationMap: mapFiles.nationMap,
|
||||
gameMap: gameMap,
|
||||
miniGameMap: miniGameMap,
|
||||
};
|
||||
loadedMaps.set(map, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function loadTerrainFromFile(fileData: string): Promise<GameMap> {
|
||||
const width = (fileData.charCodeAt(1) << 8) | fileData.charCodeAt(0);
|
||||
const height = (fileData.charCodeAt(3) << 8) | fileData.charCodeAt(2);
|
||||
const width = (fileData.charCodeAt(1) << 8) | fileData.charCodeAt(0);
|
||||
const height = (fileData.charCodeAt(3) << 8) | fileData.charCodeAt(2);
|
||||
|
||||
if (fileData.length != width * height + 4) {
|
||||
throw new Error(`Invalid data: buffer size ${fileData.length} incorrect for ${width}x${height} terrain plus 4 bytes for dimensions.`);
|
||||
}
|
||||
if (fileData.length != width * height + 4) {
|
||||
throw new Error(
|
||||
`Invalid data: buffer size ${fileData.length} incorrect for ${width}x${height} terrain plus 4 bytes for dimensions.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Store raw data in Uint8Array
|
||||
const rawData = new Uint8Array(width * height);
|
||||
let numLand = 0;
|
||||
|
||||
// Store raw data in Uint8Array
|
||||
const rawData = new Uint8Array(width * height);
|
||||
let numLand = 0;
|
||||
|
||||
// Copy data starting after the header
|
||||
for (let i = 0; i < width * height; i++) {
|
||||
const packedByte = fileData.charCodeAt(i + 4);
|
||||
rawData[i] = packedByte;
|
||||
if (packedByte & 0b10000000) numLand++;
|
||||
}
|
||||
|
||||
return new GameMapImpl(width, height, rawData, numLand)
|
||||
// Copy data starting after the header
|
||||
for (let i = 0; i < width * height; i++) {
|
||||
const packedByte = fileData.charCodeAt(i + 4);
|
||||
rawData[i] = packedByte;
|
||||
if (packedByte & 0b10000000) numLand++;
|
||||
}
|
||||
|
||||
return new GameMapImpl(width, height, rawData, numLand);
|
||||
}
|
||||
|
||||
|
||||
function logBinaryAsAscii(data: string, length: number = 8) {
|
||||
consolex.log('Binary data (1 = set bit, 0 = unset bit):');
|
||||
for (let i = 0; i < Math.min(length, data.length); i++) {
|
||||
let byte = data.charCodeAt(i);
|
||||
let byteString = '';
|
||||
for (let j = 7; j >= 0; j--) {
|
||||
byteString += (byte & (1 << j)) ? '1' : '0';
|
||||
}
|
||||
consolex.log(`Byte ${i}: ${byteString}`);
|
||||
consolex.log("Binary data (1 = set bit, 0 = unset bit):");
|
||||
for (let i = 0; i < Math.min(length, data.length); i++) {
|
||||
let byte = data.charCodeAt(i);
|
||||
let byteString = "";
|
||||
for (let j = 7; j >= 0; j--) {
|
||||
byteString += byte & (1 << j) ? "1" : "0";
|
||||
}
|
||||
}
|
||||
consolex.log(`Byte ${i}: ${byteString}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,63 +1,67 @@
|
||||
export enum SearchMapTileType {
|
||||
Land,
|
||||
Shore,
|
||||
Water,
|
||||
Land,
|
||||
Shore,
|
||||
Water,
|
||||
}
|
||||
|
||||
export class TerrainSearchMap {
|
||||
private width: number;
|
||||
private height: number;
|
||||
private mapData: Uint8Array;
|
||||
private width: number;
|
||||
private height: number;
|
||||
private mapData: Uint8Array;
|
||||
|
||||
constructor(buffer: SharedArrayBuffer) {
|
||||
this.mapData = new Uint8Array(buffer);
|
||||
this.width = (this.mapData[1] << 8) | this.mapData[0];
|
||||
this.height = (this.mapData[3] << 8) | this.mapData[2];
|
||||
constructor(buffer: SharedArrayBuffer) {
|
||||
this.mapData = new Uint8Array(buffer);
|
||||
this.width = (this.mapData[1] << 8) | this.mapData[0];
|
||||
this.height = (this.mapData[3] << 8) | this.mapData[2];
|
||||
}
|
||||
|
||||
node(x: number, y: number): SearchMapTileType {
|
||||
const packedByte = this.mapData[4 + y * this.width + x];
|
||||
const isLand = packedByte & 0b10000000;
|
||||
const shoreline = !!(packedByte & 0b01000000);
|
||||
const ocean = !!(packedByte & 0b00100000);
|
||||
const magnitude = packedByte & 0b00011111;
|
||||
if (isLand) {
|
||||
return SearchMapTileType.Land;
|
||||
}
|
||||
|
||||
node(x: number, y: number): SearchMapTileType {
|
||||
const packedByte = this.mapData[4 + y * this.width + x];
|
||||
const isLand = (packedByte & 0b10000000)
|
||||
const shoreline = !!(packedByte & 0b01000000);
|
||||
const ocean = !!(packedByte & 0b00100000);
|
||||
const magnitude = packedByte & 0b00011111;
|
||||
if (isLand) {
|
||||
return SearchMapTileType.Land
|
||||
}
|
||||
if (magnitude < 10) {
|
||||
return SearchMapTileType.Shore
|
||||
}
|
||||
return SearchMapTileType.Water
|
||||
if (magnitude < 10) {
|
||||
return SearchMapTileType.Shore;
|
||||
}
|
||||
return SearchMapTileType.Water;
|
||||
}
|
||||
|
||||
neighbors(x: number, y: number): Array<{ x: number; y: number }> {
|
||||
const result: Array<{ x: number; y: number }> = [];
|
||||
neighbors(x: number, y: number): Array<{ x: number; y: number }> {
|
||||
const result: Array<{ x: number; y: number }> = [];
|
||||
|
||||
// Check all 8 adjacent tiles
|
||||
const dirs = [
|
||||
[-1, -1], [0, -1], [1, -1],
|
||||
[-1, 0], [1, 0],
|
||||
[-1, 1], [0, 1], [1, 1]
|
||||
];
|
||||
// Check all 8 adjacent tiles
|
||||
const dirs = [
|
||||
[-1, -1],
|
||||
[0, -1],
|
||||
[1, -1],
|
||||
[-1, 0],
|
||||
[1, 0],
|
||||
[-1, 1],
|
||||
[0, 1],
|
||||
[1, 1],
|
||||
];
|
||||
|
||||
for (const [dx, dy] of dirs) {
|
||||
const newX = x + dx;
|
||||
const newY = y + dy;
|
||||
for (const [dx, dy] of dirs) {
|
||||
const newX = x + dx;
|
||||
const newY = y + dy;
|
||||
|
||||
// Check bounds
|
||||
if (newX >= 0 && newX < this.width &&
|
||||
newY >= 0 && newY < this.height) {
|
||||
result.push({ x: newX, y: newY });
|
||||
}
|
||||
}
|
||||
return result;
|
||||
// Check bounds
|
||||
if (newX >= 0 && newX < this.width && newY >= 0 && newY < this.height) {
|
||||
result.push({ x: newX, y: newY });
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
getWidth(): number {
|
||||
return this.width;
|
||||
}
|
||||
getWidth(): number {
|
||||
return this.width;
|
||||
}
|
||||
|
||||
getHeight(): number {
|
||||
return this.height;
|
||||
}
|
||||
}
|
||||
getHeight(): number {
|
||||
return this.height;
|
||||
}
|
||||
}
|
||||
|
||||
+100
-104
@@ -1,126 +1,122 @@
|
||||
import { MessageType } from './Game';
|
||||
import { MessageType } from "./Game";
|
||||
import { UnitUpdate } from "./GameUpdates";
|
||||
import { GameUpdateType } from "./GameUpdates";
|
||||
import { simpleHash, within } from "../Util";
|
||||
import { Unit, TerraNullius, UnitType, Player, UnitInfo } from "./Game";
|
||||
import { GameImpl } from "./GameImpl";
|
||||
import { PlayerImpl } from "./PlayerImpl";
|
||||
import { TileRef } from './GameMap';
|
||||
|
||||
import { TileRef } from "./GameMap";
|
||||
|
||||
export class UnitImpl implements Unit {
|
||||
private _active = true;
|
||||
private _health: number
|
||||
private _lastTile: TileRef = null
|
||||
private _active = true;
|
||||
private _health: number;
|
||||
private _lastTile: TileRef = null;
|
||||
|
||||
constructor(
|
||||
private _type: UnitType,
|
||||
private mg: GameImpl,
|
||||
private _tile: TileRef,
|
||||
private _troops: number,
|
||||
private _id: number,
|
||||
public _owner: PlayerImpl,
|
||||
) {
|
||||
// default to half health (or 1 is no health specified)
|
||||
this._health = (this.mg.unitInfo(_type).maxHealth ?? 2) / 2
|
||||
this._lastTile = _tile
|
||||
}
|
||||
constructor(
|
||||
private _type: UnitType,
|
||||
private mg: GameImpl,
|
||||
private _tile: TileRef,
|
||||
private _troops: number,
|
||||
private _id: number,
|
||||
public _owner: PlayerImpl,
|
||||
) {
|
||||
// default to half health (or 1 is no health specified)
|
||||
this._health = (this.mg.unitInfo(_type).maxHealth ?? 2) / 2;
|
||||
this._lastTile = _tile;
|
||||
}
|
||||
|
||||
toUpdate(): UnitUpdate {
|
||||
return {
|
||||
type: GameUpdateType.Unit,
|
||||
unitType: this._type,
|
||||
id: this._id,
|
||||
troops: this._troops,
|
||||
ownerID: this._owner.smallID(),
|
||||
isActive: this._active,
|
||||
pos: { x: this.mg.x(this._tile), y: this.mg.y(this._tile) },
|
||||
lastPos: { x: this.mg.x(this._lastTile), y: this.mg.y(this._lastTile) },
|
||||
};
|
||||
}
|
||||
|
||||
toUpdate(): UnitUpdate {
|
||||
return {
|
||||
type: GameUpdateType.Unit,
|
||||
unitType: this._type,
|
||||
id: this._id,
|
||||
troops: this._troops,
|
||||
ownerID: this._owner.smallID(),
|
||||
isActive: this._active,
|
||||
pos: { x: this.mg.x(this._tile), y: this.mg.y(this._tile) },
|
||||
lastPos: { x: this.mg.x(this._lastTile), y: this.mg.y(this._lastTile) }
|
||||
type(): UnitType {
|
||||
return this._type;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
lastTile(): TileRef {
|
||||
return this._lastTile;
|
||||
}
|
||||
|
||||
type(): UnitType {
|
||||
return this._type
|
||||
move(tile: TileRef): void {
|
||||
if (tile == null) {
|
||||
throw new Error("tile cannot be null");
|
||||
}
|
||||
this._lastTile = this._tile;
|
||||
this._tile = tile;
|
||||
this.mg.addUpdate(this.toUpdate());
|
||||
}
|
||||
setTroops(troops: number): void {
|
||||
this._troops = troops;
|
||||
}
|
||||
troops(): number {
|
||||
return this._troops;
|
||||
}
|
||||
health(): number {
|
||||
return this._health;
|
||||
}
|
||||
hasHealth(): boolean {
|
||||
return this.info().maxHealth != undefined;
|
||||
}
|
||||
tile(): TileRef {
|
||||
return this._tile;
|
||||
}
|
||||
owner(): PlayerImpl {
|
||||
return this._owner;
|
||||
}
|
||||
|
||||
lastTile(): TileRef {
|
||||
return this._lastTile
|
||||
}
|
||||
info(): UnitInfo {
|
||||
return this.mg.unitInfo(this._type);
|
||||
}
|
||||
|
||||
move(tile: TileRef): void {
|
||||
if (tile == null) {
|
||||
throw new Error("tile cannot be null")
|
||||
}
|
||||
this._lastTile = this._tile
|
||||
this._tile = tile;
|
||||
this.mg.addUpdate(this.toUpdate());
|
||||
}
|
||||
setTroops(troops: number): void {
|
||||
this._troops = troops;
|
||||
}
|
||||
troops(): number {
|
||||
return this._troops;
|
||||
}
|
||||
health(): number {
|
||||
return this._health
|
||||
}
|
||||
hasHealth(): boolean {
|
||||
return this.info().maxHealth != undefined
|
||||
}
|
||||
tile(): TileRef {
|
||||
return this._tile
|
||||
}
|
||||
owner(): PlayerImpl {
|
||||
return this._owner;
|
||||
}
|
||||
setOwner(newOwner: Player): void {
|
||||
const oldOwner = this._owner;
|
||||
oldOwner._units = oldOwner._units.filter((u) => u != this);
|
||||
this._owner = newOwner as PlayerImpl;
|
||||
this.mg.addUpdate(this.toUpdate());
|
||||
this.mg.displayMessage(
|
||||
`Your ${this.type()} was captured by ${newOwner.displayName()}`,
|
||||
MessageType.ERROR,
|
||||
oldOwner.id(),
|
||||
);
|
||||
}
|
||||
|
||||
info(): UnitInfo {
|
||||
return this.mg.unitInfo(this._type)
|
||||
}
|
||||
modifyHealth(delta: number): void {
|
||||
this._health = within(this._health + delta, 0, this.info().maxHealth ?? 1);
|
||||
}
|
||||
|
||||
setOwner(newOwner: Player): void {
|
||||
const oldOwner = this._owner
|
||||
oldOwner._units = oldOwner._units.filter(u => u != this)
|
||||
this._owner = newOwner as PlayerImpl
|
||||
this.mg.addUpdate(this.toUpdate())
|
||||
this.mg.displayMessage(
|
||||
`Your ${this.type()} was captured by ${newOwner.displayName()}`,
|
||||
MessageType.ERROR,
|
||||
oldOwner.id()
|
||||
)
|
||||
delete(displayMessage: boolean = true): void {
|
||||
if (!this.isActive()) {
|
||||
throw new Error(`cannot delete ${this} not active`);
|
||||
}
|
||||
this._owner._units = this._owner._units.filter((b) => b != this);
|
||||
this._active = false;
|
||||
this.mg.addUpdate(this.toUpdate());
|
||||
if (displayMessage) {
|
||||
this.mg.displayMessage(
|
||||
`Your ${this.type()} was destroyed`,
|
||||
MessageType.ERROR,
|
||||
this.owner().id(),
|
||||
);
|
||||
}
|
||||
}
|
||||
isActive(): boolean {
|
||||
return this._active;
|
||||
}
|
||||
|
||||
modifyHealth(delta: number): void {
|
||||
this._health = within(
|
||||
this._health + delta,
|
||||
0,
|
||||
this.info().maxHealth ?? 1
|
||||
)
|
||||
}
|
||||
hash(): number {
|
||||
return this.tile() + simpleHash(this.type());
|
||||
}
|
||||
|
||||
|
||||
delete(displayMessage: boolean = true): void {
|
||||
if (!this.isActive()) {
|
||||
throw new Error(`cannot delete ${this} not active`)
|
||||
}
|
||||
this._owner._units = this._owner._units.filter(b => b != this);
|
||||
this._active = false;
|
||||
this.mg.addUpdate(this.toUpdate());
|
||||
if (displayMessage) {
|
||||
this.mg.displayMessage(`Your ${this.type()} was destroyed`, MessageType.ERROR, this.owner().id())
|
||||
}
|
||||
}
|
||||
isActive(): boolean {
|
||||
return this._active;
|
||||
}
|
||||
|
||||
hash(): number {
|
||||
return this.tile() + simpleHash(this.type())
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `Unit:${this._type},owner:${this.owner().name()}`
|
||||
}
|
||||
toString(): string {
|
||||
return `Unit:${this._type},owner:${this.owner().name()}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,33 @@
|
||||
import { TileRef } from "../game/GameMap";
|
||||
|
||||
export interface AStar {
|
||||
compute(): PathFindResultType
|
||||
reconstructPath(): TileRef[]
|
||||
compute(): PathFindResultType;
|
||||
reconstructPath(): TileRef[];
|
||||
}
|
||||
|
||||
export enum PathFindResultType {
|
||||
NextTile,
|
||||
Pending,
|
||||
Completed,
|
||||
PathNotFound
|
||||
} export type TileResult = {
|
||||
type: PathFindResultType.NextTile;
|
||||
tile: TileRef;
|
||||
} | {
|
||||
type: PathFindResultType.Pending;
|
||||
} | {
|
||||
type: PathFindResultType.Completed;
|
||||
tile: TileRef;
|
||||
} | {
|
||||
type: PathFindResultType.PathNotFound;
|
||||
};
|
||||
NextTile,
|
||||
Pending,
|
||||
Completed,
|
||||
PathNotFound,
|
||||
}
|
||||
export type TileResult =
|
||||
| {
|
||||
type: PathFindResultType.NextTile;
|
||||
tile: TileRef;
|
||||
}
|
||||
| {
|
||||
type: PathFindResultType.Pending;
|
||||
}
|
||||
| {
|
||||
type: PathFindResultType.Completed;
|
||||
tile: TileRef;
|
||||
}
|
||||
| {
|
||||
type: PathFindResultType.PathNotFound;
|
||||
};
|
||||
|
||||
export interface Point {
|
||||
x: number;
|
||||
y: number;
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,89 +1,92 @@
|
||||
import { Cell, } from "../game/Game";
|
||||
import { Cell } from "../game/Game";
|
||||
import { GameMap, GameMapImpl, TileRef } from "../game/GameMap";
|
||||
import { AStar, PathFindResultType, } from "./AStar";
|
||||
import { AStar, PathFindResultType } from "./AStar";
|
||||
import { SerialAStar } from "./SerialAStar";
|
||||
|
||||
// TODO: test this, get it work
|
||||
export class MiniAStar implements AStar {
|
||||
private aStar: SerialAStar;
|
||||
|
||||
private aStar: SerialAStar
|
||||
constructor(
|
||||
private gameMap: GameMap,
|
||||
private miniMap: GameMap,
|
||||
private src: TileRef,
|
||||
private dst: TileRef,
|
||||
private canMove: (t: TileRef) => boolean,
|
||||
private iterations: number,
|
||||
private maxTries: number,
|
||||
) {
|
||||
const miniSrc = this.miniMap.ref(
|
||||
Math.floor(gameMap.x(src) / 2),
|
||||
Math.floor(gameMap.y(src) / 2),
|
||||
);
|
||||
const miniDst = this.miniMap.ref(
|
||||
Math.floor(gameMap.x(dst) / 2),
|
||||
Math.floor(gameMap.y(dst) / 2),
|
||||
);
|
||||
this.aStar = new SerialAStar(
|
||||
miniSrc,
|
||||
miniDst,
|
||||
canMove,
|
||||
iterations,
|
||||
maxTries,
|
||||
this.miniMap,
|
||||
);
|
||||
}
|
||||
|
||||
constructor(
|
||||
private gameMap: GameMap,
|
||||
private miniMap: GameMap,
|
||||
private src: TileRef,
|
||||
private dst: TileRef,
|
||||
private canMove: (t: TileRef) => boolean,
|
||||
private iterations: number,
|
||||
private maxTries: number
|
||||
) {
|
||||
const miniSrc = this.miniMap.ref(
|
||||
Math.floor(gameMap.x(src) / 2),
|
||||
Math.floor(gameMap.y(src) / 2)
|
||||
)
|
||||
const miniDst = this.miniMap.ref(
|
||||
Math.floor(gameMap.x(dst) / 2),
|
||||
Math.floor(gameMap.y(dst) / 2)
|
||||
)
|
||||
this.aStar = new SerialAStar(
|
||||
miniSrc,
|
||||
miniDst,
|
||||
canMove,
|
||||
iterations,
|
||||
maxTries,
|
||||
this.miniMap
|
||||
)
|
||||
}
|
||||
|
||||
compute(): PathFindResultType {
|
||||
return this.aStar.compute()
|
||||
}
|
||||
|
||||
reconstructPath(): TileRef[] {
|
||||
const upscaled = upscalePath(this.aStar.reconstructPath().map(tr => new Cell(this.miniMap.x(tr), this.miniMap.y(tr))))
|
||||
upscaled.push(new Cell(this.gameMap.x(this.dst), this.gameMap.y(this.dst)))
|
||||
return upscaled.map(c => this.gameMap.ref(c.x, c.y))
|
||||
}
|
||||
compute(): PathFindResultType {
|
||||
return this.aStar.compute();
|
||||
}
|
||||
|
||||
reconstructPath(): TileRef[] {
|
||||
const upscaled = upscalePath(
|
||||
this.aStar
|
||||
.reconstructPath()
|
||||
.map((tr) => new Cell(this.miniMap.x(tr), this.miniMap.y(tr))),
|
||||
);
|
||||
upscaled.push(new Cell(this.gameMap.x(this.dst), this.gameMap.y(this.dst)));
|
||||
return upscaled.map((c) => this.gameMap.ref(c.x, c.y));
|
||||
}
|
||||
}
|
||||
|
||||
function upscalePath(path: Cell[], scaleFactor: number = 2): Cell[] {
|
||||
// Scale up each point
|
||||
const scaledPath = path.map(point => (new Cell(
|
||||
point.x * scaleFactor,
|
||||
point.y * scaleFactor
|
||||
)));
|
||||
// Scale up each point
|
||||
const scaledPath = path.map(
|
||||
(point) => new Cell(point.x * scaleFactor, point.y * scaleFactor),
|
||||
);
|
||||
|
||||
const smoothPath: Cell[] = [];
|
||||
const smoothPath: Cell[] = [];
|
||||
|
||||
for (let i = 0; i < scaledPath.length - 1; i++) {
|
||||
const current = scaledPath[i];
|
||||
const next = scaledPath[i + 1];
|
||||
for (let i = 0; i < scaledPath.length - 1; i++) {
|
||||
const current = scaledPath[i];
|
||||
const next = scaledPath[i + 1];
|
||||
|
||||
// Add the current point
|
||||
smoothPath.push(current);
|
||||
// Add the current point
|
||||
smoothPath.push(current);
|
||||
|
||||
// Always interpolate between scaled points
|
||||
const dx = next.x - current.x;
|
||||
const dy = next.y - current.y;
|
||||
// Always interpolate between scaled points
|
||||
const dx = next.x - current.x;
|
||||
const dy = next.y - current.y;
|
||||
|
||||
// Calculate number of steps needed
|
||||
const distance = Math.max(Math.abs(dx), Math.abs(dy));
|
||||
const steps = distance;
|
||||
// Calculate number of steps needed
|
||||
const distance = Math.max(Math.abs(dx), Math.abs(dy));
|
||||
const steps = distance;
|
||||
|
||||
// Add intermediate points
|
||||
for (let step = 1; step < steps; step++) {
|
||||
smoothPath.push(new Cell(
|
||||
Math.round(current.x + (dx * step) / steps),
|
||||
Math.round(current.y + (dy * step) / steps)
|
||||
));
|
||||
}
|
||||
// Add intermediate points
|
||||
for (let step = 1; step < steps; step++) {
|
||||
smoothPath.push(
|
||||
new Cell(
|
||||
Math.round(current.x + (dx * step) / steps),
|
||||
Math.round(current.y + (dy * step) / steps),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Add the last point
|
||||
if (scaledPath.length > 0) {
|
||||
smoothPath.push(scaledPath[scaledPath.length - 1]);
|
||||
}
|
||||
// Add the last point
|
||||
if (scaledPath.length > 0) {
|
||||
smoothPath.push(scaledPath[scaledPath.length - 1]);
|
||||
}
|
||||
|
||||
return smoothPath;
|
||||
}
|
||||
return smoothPath;
|
||||
}
|
||||
|
||||
@@ -6,98 +6,98 @@ import { consolex } from "../Consolex";
|
||||
import { TileRef } from "../game/GameMap";
|
||||
|
||||
export class PathFinder {
|
||||
private curr: TileRef = null;
|
||||
private dst: TileRef = null;
|
||||
private path: TileRef[];
|
||||
private aStar: AStar;
|
||||
private computeFinished = true;
|
||||
|
||||
private curr: TileRef = null
|
||||
private dst: TileRef = null
|
||||
private path: TileRef[]
|
||||
private aStar: AStar
|
||||
private computeFinished = true
|
||||
private constructor(
|
||||
private game: Game,
|
||||
private newAStar: (curr: TileRef, dst: TileRef) => AStar,
|
||||
) {}
|
||||
|
||||
private constructor(
|
||||
private game: Game,
|
||||
private newAStar: (curr: TileRef, dst: TileRef) => AStar
|
||||
) { }
|
||||
public static Mini(
|
||||
game: Game,
|
||||
iterations: number,
|
||||
canMoveOnLand: boolean,
|
||||
maxTries: number = 20,
|
||||
) {
|
||||
return new PathFinder(game, (curr: TileRef, dst: TileRef) => {
|
||||
return new MiniAStar(
|
||||
game.map(),
|
||||
game.miniMap(),
|
||||
curr,
|
||||
dst,
|
||||
(tr: TileRef): boolean => {
|
||||
if (canMoveOnLand) {
|
||||
return true;
|
||||
}
|
||||
return game.miniMap().isOcean(tr);
|
||||
},
|
||||
iterations,
|
||||
maxTries,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public static Mini(game: Game, iterations: number, canMoveOnLand: boolean, maxTries: number = 20) {
|
||||
return new PathFinder(
|
||||
game,
|
||||
(curr: TileRef, dst: TileRef) => {
|
||||
return new MiniAStar(
|
||||
game.map(),
|
||||
game.miniMap(),
|
||||
curr,
|
||||
dst,
|
||||
(tr: TileRef): boolean => {
|
||||
if (canMoveOnLand) {
|
||||
return true
|
||||
}
|
||||
return game.miniMap().isOcean(tr)
|
||||
},
|
||||
iterations,
|
||||
maxTries
|
||||
)
|
||||
}
|
||||
)
|
||||
nextTile(curr: TileRef, dst: TileRef, dist: number = 1): TileResult {
|
||||
if (curr == null) {
|
||||
consolex.error("curr is null");
|
||||
}
|
||||
if (dst == null) {
|
||||
consolex.error("dst is null");
|
||||
}
|
||||
|
||||
nextTile(curr: TileRef, dst: TileRef, dist: number = 1): TileResult {
|
||||
if (curr == null) {
|
||||
consolex.error('curr is null')
|
||||
}
|
||||
if (dst == null) {
|
||||
consolex.error('dst is null')
|
||||
}
|
||||
|
||||
if (this.game.manhattanDist(curr, dst) < dist) {
|
||||
return { type: PathFindResultType.Completed, tile: curr }
|
||||
}
|
||||
|
||||
if (this.computeFinished) {
|
||||
if (this.shouldRecompute(curr, dst)) {
|
||||
this.curr = curr
|
||||
this.dst = dst
|
||||
this.path = null
|
||||
this.aStar = this.newAStar(curr, dst)
|
||||
this.computeFinished = false
|
||||
return this.nextTile(curr, dst)
|
||||
} else {
|
||||
return { type: PathFindResultType.NextTile, tile: this.path.shift() }
|
||||
}
|
||||
}
|
||||
|
||||
switch (this.aStar.compute()) {
|
||||
case PathFindResultType.Completed:
|
||||
this.computeFinished = true
|
||||
this.path = this.aStar.reconstructPath()
|
||||
// Remove the start tile
|
||||
this.path.shift()
|
||||
return this.nextTile(curr, dst)
|
||||
case PathFindResultType.Pending:
|
||||
return { type: PathFindResultType.Pending }
|
||||
case PathFindResultType.PathNotFound:
|
||||
return { type: PathFindResultType.PathNotFound }
|
||||
}
|
||||
if (this.game.manhattanDist(curr, dst) < dist) {
|
||||
return { type: PathFindResultType.Completed, tile: curr };
|
||||
}
|
||||
|
||||
private shouldRecompute(curr: TileRef, dst: TileRef) {
|
||||
if (this.path == null || this.curr == null || this.dst == null) {
|
||||
return true
|
||||
}
|
||||
const dist = this.game.manhattanDist(curr, dst)
|
||||
let tolerance = 10
|
||||
if (dist > 50) {
|
||||
tolerance = 10
|
||||
} else if (dist > 25) {
|
||||
tolerance = 5
|
||||
} else if (dist > 10) {
|
||||
tolerance = 3
|
||||
} else {
|
||||
tolerance = 0
|
||||
}
|
||||
if (this.game.manhattanDist(this.dst, dst) > tolerance) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
if (this.computeFinished) {
|
||||
if (this.shouldRecompute(curr, dst)) {
|
||||
this.curr = curr;
|
||||
this.dst = dst;
|
||||
this.path = null;
|
||||
this.aStar = this.newAStar(curr, dst);
|
||||
this.computeFinished = false;
|
||||
return this.nextTile(curr, dst);
|
||||
} else {
|
||||
return { type: PathFindResultType.NextTile, tile: this.path.shift() };
|
||||
}
|
||||
}
|
||||
|
||||
switch (this.aStar.compute()) {
|
||||
case PathFindResultType.Completed:
|
||||
this.computeFinished = true;
|
||||
this.path = this.aStar.reconstructPath();
|
||||
// Remove the start tile
|
||||
this.path.shift();
|
||||
return this.nextTile(curr, dst);
|
||||
case PathFindResultType.Pending:
|
||||
return { type: PathFindResultType.Pending };
|
||||
case PathFindResultType.PathNotFound:
|
||||
return { type: PathFindResultType.PathNotFound };
|
||||
}
|
||||
}
|
||||
|
||||
private shouldRecompute(curr: TileRef, dst: TileRef) {
|
||||
if (this.path == null || this.curr == null || this.dst == null) {
|
||||
return true;
|
||||
}
|
||||
const dist = this.game.manhattanDist(curr, dst);
|
||||
let tolerance = 10;
|
||||
if (dist > 50) {
|
||||
tolerance = 10;
|
||||
} else if (dist > 25) {
|
||||
tolerance = 5;
|
||||
} else if (dist > 10) {
|
||||
tolerance = 3;
|
||||
} else {
|
||||
tolerance = 0;
|
||||
}
|
||||
if (this.game.manhattanDist(this.dst, dst) > tolerance) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
+123
-116
@@ -4,137 +4,144 @@ import { PathFindResultType } from "./AStar";
|
||||
import { consolex } from "../Consolex";
|
||||
import { GameMap, GameMapImpl, TileRef } from "../game/GameMap";
|
||||
|
||||
|
||||
export class SerialAStar implements AStar {
|
||||
private fwdOpenSet: PriorityQueue<{ tile: TileRef; fScore: number; }>;
|
||||
private bwdOpenSet: PriorityQueue<{ tile: TileRef; fScore: number; }>;
|
||||
private fwdCameFrom: Map<TileRef, TileRef>;
|
||||
private bwdCameFrom: Map<TileRef, TileRef>;
|
||||
private fwdGScore: Map<TileRef, number>;
|
||||
private bwdGScore: Map<TileRef, number>;
|
||||
private meetingPoint: TileRef | null;
|
||||
public completed: boolean;
|
||||
private fwdOpenSet: PriorityQueue<{ tile: TileRef; fScore: number }>;
|
||||
private bwdOpenSet: PriorityQueue<{ tile: TileRef; fScore: number }>;
|
||||
private fwdCameFrom: Map<TileRef, TileRef>;
|
||||
private bwdCameFrom: Map<TileRef, TileRef>;
|
||||
private fwdGScore: Map<TileRef, number>;
|
||||
private bwdGScore: Map<TileRef, number>;
|
||||
private meetingPoint: TileRef | null;
|
||||
public completed: boolean;
|
||||
|
||||
constructor(
|
||||
private src: TileRef,
|
||||
private dst: TileRef,
|
||||
private canMove: (t: TileRef) => boolean,
|
||||
private iterations: number,
|
||||
private maxTries: number,
|
||||
private gameMap: GameMap
|
||||
) {
|
||||
this.fwdOpenSet = new PriorityQueue<{ tile: TileRef; fScore: number; }>(
|
||||
(a, b) => a.fScore - b.fScore
|
||||
);
|
||||
this.bwdOpenSet = new PriorityQueue<{ tile: TileRef; fScore: number; }>(
|
||||
(a, b) => a.fScore - b.fScore
|
||||
);
|
||||
this.fwdCameFrom = new Map<TileRef, TileRef>();
|
||||
this.bwdCameFrom = new Map<TileRef, TileRef>();
|
||||
this.fwdGScore = new Map<TileRef, number>();
|
||||
this.bwdGScore = new Map<TileRef, number>();
|
||||
this.meetingPoint = null;
|
||||
this.completed = false;
|
||||
constructor(
|
||||
private src: TileRef,
|
||||
private dst: TileRef,
|
||||
private canMove: (t: TileRef) => boolean,
|
||||
private iterations: number,
|
||||
private maxTries: number,
|
||||
private gameMap: GameMap,
|
||||
) {
|
||||
this.fwdOpenSet = new PriorityQueue<{ tile: TileRef; fScore: number }>(
|
||||
(a, b) => a.fScore - b.fScore,
|
||||
);
|
||||
this.bwdOpenSet = new PriorityQueue<{ tile: TileRef; fScore: number }>(
|
||||
(a, b) => a.fScore - b.fScore,
|
||||
);
|
||||
this.fwdCameFrom = new Map<TileRef, TileRef>();
|
||||
this.bwdCameFrom = new Map<TileRef, TileRef>();
|
||||
this.fwdGScore = new Map<TileRef, number>();
|
||||
this.bwdGScore = new Map<TileRef, number>();
|
||||
this.meetingPoint = null;
|
||||
this.completed = false;
|
||||
|
||||
// Initialize forward search
|
||||
this.fwdGScore.set(src, 0);
|
||||
this.fwdOpenSet.enqueue({ tile: src, fScore: this.heuristic(src, dst) });
|
||||
// Initialize forward search
|
||||
this.fwdGScore.set(src, 0);
|
||||
this.fwdOpenSet.enqueue({ tile: src, fScore: this.heuristic(src, dst) });
|
||||
|
||||
// Initialize backward search
|
||||
this.bwdGScore.set(dst, 0);
|
||||
this.bwdOpenSet.enqueue({ tile: dst, fScore: this.heuristic(dst, src) });
|
||||
// Initialize backward search
|
||||
this.bwdGScore.set(dst, 0);
|
||||
this.bwdOpenSet.enqueue({ tile: dst, fScore: this.heuristic(dst, src) });
|
||||
}
|
||||
|
||||
compute(): PathFindResultType {
|
||||
if (this.completed) return PathFindResultType.Completed;
|
||||
|
||||
this.maxTries -= 1;
|
||||
let iterations = this.iterations;
|
||||
|
||||
while (!this.fwdOpenSet.isEmpty() && !this.bwdOpenSet.isEmpty()) {
|
||||
iterations--;
|
||||
if (iterations <= 0) {
|
||||
if (this.maxTries <= 0) {
|
||||
return PathFindResultType.PathNotFound;
|
||||
}
|
||||
return PathFindResultType.Pending;
|
||||
}
|
||||
|
||||
// Process forward search
|
||||
const fwdCurrent = this.fwdOpenSet.dequeue()!.tile;
|
||||
if (this.bwdGScore.has(fwdCurrent)) {
|
||||
// We found a meeting point!
|
||||
this.meetingPoint = fwdCurrent;
|
||||
this.completed = true;
|
||||
return PathFindResultType.Completed;
|
||||
}
|
||||
|
||||
this.expandTileRef(fwdCurrent, true);
|
||||
|
||||
// Process backward search
|
||||
const bwdCurrent = this.bwdOpenSet.dequeue()!.tile;
|
||||
if (this.fwdGScore.has(bwdCurrent)) {
|
||||
// We found a meeting point!
|
||||
this.meetingPoint = bwdCurrent;
|
||||
this.completed = true;
|
||||
return PathFindResultType.Completed;
|
||||
}
|
||||
|
||||
this.expandTileRef(bwdCurrent, false);
|
||||
}
|
||||
|
||||
compute(): PathFindResultType {
|
||||
if (this.completed) return PathFindResultType.Completed;
|
||||
return this.completed
|
||||
? PathFindResultType.Completed
|
||||
: PathFindResultType.PathNotFound;
|
||||
}
|
||||
|
||||
this.maxTries -= 1;
|
||||
let iterations = this.iterations;
|
||||
private expandTileRef(current: TileRef, isForward: boolean) {
|
||||
for (const neighbor of this.gameMap.neighbors(current)) {
|
||||
if (
|
||||
neighbor != (isForward ? this.dst : this.src) &&
|
||||
!this.canMove(neighbor)
|
||||
)
|
||||
continue;
|
||||
|
||||
while (!this.fwdOpenSet.isEmpty() && !this.bwdOpenSet.isEmpty()) {
|
||||
iterations--;
|
||||
if (iterations <= 0) {
|
||||
if (this.maxTries <= 0) {
|
||||
return PathFindResultType.PathNotFound;
|
||||
}
|
||||
return PathFindResultType.Pending;
|
||||
}
|
||||
const gScore = isForward ? this.fwdGScore : this.bwdGScore;
|
||||
const openSet = isForward ? this.fwdOpenSet : this.bwdOpenSet;
|
||||
const cameFrom = isForward ? this.fwdCameFrom : this.bwdCameFrom;
|
||||
|
||||
// Process forward search
|
||||
const fwdCurrent = this.fwdOpenSet.dequeue()!.tile;
|
||||
if (this.bwdGScore.has(fwdCurrent)) {
|
||||
// We found a meeting point!
|
||||
this.meetingPoint = fwdCurrent;
|
||||
this.completed = true;
|
||||
return PathFindResultType.Completed;
|
||||
}
|
||||
let tentativeGScore = gScore.get(current)! + this.gameMap.cost(neighbor);
|
||||
|
||||
this.expandTileRef(fwdCurrent, true);
|
||||
if (!gScore.has(neighbor) || tentativeGScore < gScore.get(neighbor)!) {
|
||||
cameFrom.set(neighbor, current);
|
||||
gScore.set(neighbor, tentativeGScore);
|
||||
const fScore =
|
||||
tentativeGScore +
|
||||
this.heuristic(neighbor, isForward ? this.dst : this.src);
|
||||
openSet.enqueue({ tile: neighbor, fScore: fScore });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process backward search
|
||||
const bwdCurrent = this.bwdOpenSet.dequeue()!.tile;
|
||||
if (this.fwdGScore.has(bwdCurrent)) {
|
||||
// We found a meeting point!
|
||||
this.meetingPoint = bwdCurrent;
|
||||
this.completed = true;
|
||||
return PathFindResultType.Completed;
|
||||
}
|
||||
private heuristic(a: TileRef, b: TileRef): number {
|
||||
// TODO use wrapped
|
||||
try {
|
||||
return (
|
||||
1.1 * Math.abs(this.gameMap.x(a) - this.gameMap.x(b)) +
|
||||
Math.abs(this.gameMap.y(a) - this.gameMap.y(b))
|
||||
);
|
||||
} catch {
|
||||
consolex.log("uh oh");
|
||||
}
|
||||
}
|
||||
|
||||
this.expandTileRef(bwdCurrent, false);
|
||||
}
|
||||
public reconstructPath(): TileRef[] {
|
||||
if (!this.meetingPoint) return [];
|
||||
|
||||
return this.completed ? PathFindResultType.Completed : PathFindResultType.PathNotFound;
|
||||
// Reconstruct path from start to meeting point
|
||||
const fwdPath: TileRef[] = [this.meetingPoint];
|
||||
let current = this.meetingPoint;
|
||||
while (this.fwdCameFrom.has(current)) {
|
||||
current = this.fwdCameFrom.get(current)!;
|
||||
fwdPath.unshift(current);
|
||||
}
|
||||
|
||||
private expandTileRef(current: TileRef, isForward: boolean) {
|
||||
for (const neighbor of this.gameMap.neighbors(current)) {
|
||||
if (neighbor != (isForward ? this.dst : this.src) && !this.canMove(neighbor)) continue;
|
||||
|
||||
const gScore = isForward ? this.fwdGScore : this.bwdGScore;
|
||||
const openSet = isForward ? this.fwdOpenSet : this.bwdOpenSet;
|
||||
const cameFrom = isForward ? this.fwdCameFrom : this.bwdCameFrom;
|
||||
|
||||
let tentativeGScore = gScore.get(current)! + this.gameMap.cost(neighbor);
|
||||
|
||||
if (!gScore.has(neighbor) || tentativeGScore < gScore.get(neighbor)!) {
|
||||
cameFrom.set(neighbor, current);
|
||||
gScore.set(neighbor, tentativeGScore);
|
||||
const fScore = tentativeGScore + this.heuristic(
|
||||
neighbor,
|
||||
isForward ? this.dst : this.src
|
||||
);
|
||||
openSet.enqueue({ tile: neighbor, fScore: fScore });
|
||||
}
|
||||
}
|
||||
// Reconstruct path from meeting point to goal
|
||||
current = this.meetingPoint;
|
||||
while (this.bwdCameFrom.has(current)) {
|
||||
current = this.bwdCameFrom.get(current)!;
|
||||
fwdPath.push(current);
|
||||
}
|
||||
|
||||
private heuristic(a: TileRef, b: TileRef): number {
|
||||
// TODO use wrapped
|
||||
try {
|
||||
return 1.1 * Math.abs(this.gameMap.x(a) - this.gameMap.x(b)) + Math.abs(this.gameMap.y(a) - this.gameMap.y(b));
|
||||
} catch {
|
||||
consolex.log('uh oh')
|
||||
}
|
||||
}
|
||||
|
||||
public reconstructPath(): TileRef[] {
|
||||
if (!this.meetingPoint) return [];
|
||||
|
||||
// Reconstruct path from start to meeting point
|
||||
const fwdPath: TileRef[] = [this.meetingPoint];
|
||||
let current = this.meetingPoint;
|
||||
while (this.fwdCameFrom.has(current)) {
|
||||
current = this.fwdCameFrom.get(current)!;
|
||||
fwdPath.unshift(current);
|
||||
}
|
||||
|
||||
// Reconstruct path from meeting point to goal
|
||||
current = this.meetingPoint;
|
||||
while (this.bwdCameFrom.has(current)) {
|
||||
current = this.bwdCameFrom.get(current)!;
|
||||
fwdPath.push(current);
|
||||
}
|
||||
|
||||
return fwdPath
|
||||
}
|
||||
return fwdPath;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,38 +3,42 @@ export const MAX_USERNAME_LENGTH = 20;
|
||||
|
||||
const validPattern = /^[a-zA-Z0-9_ ]+$/;
|
||||
|
||||
export function validateUsername(username: string): { isValid: boolean; error?: string } {
|
||||
export function validateUsername(username: string): {
|
||||
isValid: boolean;
|
||||
error?: string;
|
||||
} {
|
||||
if (typeof username !== "string") {
|
||||
return { isValid: false, error: "Username must be a string." };
|
||||
}
|
||||
|
||||
if (typeof username !== 'string') {
|
||||
return { isValid: false, error: "Username must be a string." };
|
||||
}
|
||||
if (username.length < MIN_USERNAME_LENGTH) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: `Username must be at least ${MIN_USERNAME_LENGTH} characters long.`,
|
||||
};
|
||||
}
|
||||
|
||||
if (username.length < MIN_USERNAME_LENGTH) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: `Username must be at least ${MIN_USERNAME_LENGTH} characters long.`,
|
||||
};
|
||||
}
|
||||
if (username.length > MAX_USERNAME_LENGTH) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: `Username must not exceed ${MAX_USERNAME_LENGTH} characters.`,
|
||||
};
|
||||
}
|
||||
|
||||
if (username.length > MAX_USERNAME_LENGTH) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: `Username must not exceed ${MAX_USERNAME_LENGTH} characters.`,
|
||||
};
|
||||
}
|
||||
if (!validPattern.test(username)) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: "Username can only contain letters, numbers, and underscores.",
|
||||
};
|
||||
}
|
||||
|
||||
if (!validPattern.test(username)) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: "Username can only contain letters, numbers, and underscores.",
|
||||
};
|
||||
}
|
||||
|
||||
// All checks passed
|
||||
return { isValid: true };
|
||||
// All checks passed
|
||||
return { isValid: true };
|
||||
}
|
||||
|
||||
export function sanitizeUsername(str: string): string {
|
||||
const sanitized = str.replace(/[^a-zA-Z0-9]/g, '').slice(0, MAX_USERNAME_LENGTH);
|
||||
return sanitized.padEnd(MIN_USERNAME_LENGTH, 'x')
|
||||
};
|
||||
const sanitized = str
|
||||
.replace(/[^a-zA-Z0-9]/g, "")
|
||||
.slice(0, MAX_USERNAME_LENGTH);
|
||||
return sanitized.padEnd(MIN_USERNAME_LENGTH, "x");
|
||||
}
|
||||
|
||||
@@ -1,110 +1,114 @@
|
||||
import { createGameRunner, GameRunner } from "../GameRunner";
|
||||
import { GameUpdateViewData } from '../game/GameUpdates';
|
||||
import { GameUpdateViewData } from "../game/GameUpdates";
|
||||
import {
|
||||
MainThreadMessage,
|
||||
WorkerMessage,
|
||||
InitializedMessage,
|
||||
PlayerActionsResultMessage,
|
||||
PlayerProfileResultMessage,
|
||||
} from './WorkerMessages';
|
||||
MainThreadMessage,
|
||||
WorkerMessage,
|
||||
InitializedMessage,
|
||||
PlayerActionsResultMessage,
|
||||
PlayerProfileResultMessage,
|
||||
} from "./WorkerMessages";
|
||||
|
||||
const ctx: Worker = self as any;
|
||||
let gameRunner: Promise<GameRunner> | null = null;
|
||||
|
||||
function gameUpdate(gu: GameUpdateViewData) {
|
||||
sendMessage({
|
||||
type: "game_update",
|
||||
gameUpdate: gu
|
||||
});
|
||||
sendMessage({
|
||||
type: "game_update",
|
||||
gameUpdate: gu,
|
||||
});
|
||||
}
|
||||
|
||||
function sendMessage(message: WorkerMessage) {
|
||||
ctx.postMessage(message);
|
||||
ctx.postMessage(message);
|
||||
}
|
||||
|
||||
ctx.addEventListener('message', async (e: MessageEvent<MainThreadMessage>) => {
|
||||
const message = e.data;
|
||||
ctx.addEventListener("message", async (e: MessageEvent<MainThreadMessage>) => {
|
||||
const message = e.data;
|
||||
|
||||
switch (message.type) {
|
||||
case 'heartbeat':
|
||||
break
|
||||
case 'init':
|
||||
try {
|
||||
gameRunner = createGameRunner(
|
||||
message.gameID,
|
||||
message.gameConfig,
|
||||
gameUpdate
|
||||
).then(gr => {
|
||||
sendMessage({
|
||||
type: 'initialized',
|
||||
id: message.id
|
||||
} as InitializedMessage);
|
||||
return gr;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize game runner:', error);
|
||||
throw error;
|
||||
}
|
||||
break;
|
||||
switch (message.type) {
|
||||
case "heartbeat":
|
||||
break;
|
||||
case "init":
|
||||
try {
|
||||
gameRunner = createGameRunner(
|
||||
message.gameID,
|
||||
message.gameConfig,
|
||||
gameUpdate,
|
||||
).then((gr) => {
|
||||
sendMessage({
|
||||
type: "initialized",
|
||||
id: message.id,
|
||||
} as InitializedMessage);
|
||||
return gr;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize game runner:", error);
|
||||
throw error;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'turn':
|
||||
if (!gameRunner) {
|
||||
throw new Error('Game runner not initialized');
|
||||
}
|
||||
case "turn":
|
||||
if (!gameRunner) {
|
||||
throw new Error("Game runner not initialized");
|
||||
}
|
||||
|
||||
try {
|
||||
const gr = await gameRunner;
|
||||
await gr.addTurn(message.turn);
|
||||
} catch (error) {
|
||||
console.error('Failed to process turn:', error);
|
||||
throw error;
|
||||
}
|
||||
break;
|
||||
try {
|
||||
const gr = await gameRunner;
|
||||
await gr.addTurn(message.turn);
|
||||
} catch (error) {
|
||||
console.error("Failed to process turn:", error);
|
||||
throw error;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'player_actions':
|
||||
if (!gameRunner) {
|
||||
throw new Error('Game runner not initialized');
|
||||
}
|
||||
case "player_actions":
|
||||
if (!gameRunner) {
|
||||
throw new Error("Game runner not initialized");
|
||||
}
|
||||
|
||||
try {
|
||||
const actions = (await gameRunner).playerActions(message.playerID, message.x, message.y)
|
||||
sendMessage({
|
||||
type: 'player_actions_result',
|
||||
id: message.id,
|
||||
result: actions
|
||||
} as PlayerActionsResultMessage);
|
||||
} catch (error) {
|
||||
console.error('Failed to check borders:', error);
|
||||
throw error;
|
||||
}
|
||||
break;
|
||||
case 'player_profile':
|
||||
if (!gameRunner) {
|
||||
throw new Error('Game runner not initialized');
|
||||
}
|
||||
try {
|
||||
const actions = (await gameRunner).playerActions(
|
||||
message.playerID,
|
||||
message.x,
|
||||
message.y,
|
||||
);
|
||||
sendMessage({
|
||||
type: "player_actions_result",
|
||||
id: message.id,
|
||||
result: actions,
|
||||
} as PlayerActionsResultMessage);
|
||||
} catch (error) {
|
||||
console.error("Failed to check borders:", error);
|
||||
throw error;
|
||||
}
|
||||
break;
|
||||
case "player_profile":
|
||||
if (!gameRunner) {
|
||||
throw new Error("Game runner not initialized");
|
||||
}
|
||||
|
||||
try {
|
||||
const profile = (await gameRunner).playerProfile(message.playerID)
|
||||
sendMessage({
|
||||
type: 'player_profile_result',
|
||||
id: message.id,
|
||||
result: profile
|
||||
} as PlayerProfileResultMessage);
|
||||
} catch (error) {
|
||||
console.error('Failed to check borders:', error);
|
||||
throw error;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
console.warn('Unknown message :', message);
|
||||
}
|
||||
try {
|
||||
const profile = (await gameRunner).playerProfile(message.playerID);
|
||||
sendMessage({
|
||||
type: "player_profile_result",
|
||||
id: message.id,
|
||||
result: profile,
|
||||
} as PlayerProfileResultMessage);
|
||||
} catch (error) {
|
||||
console.error("Failed to check borders:", error);
|
||||
throw error;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
console.warn("Unknown message :", message);
|
||||
}
|
||||
});
|
||||
|
||||
// Error handling
|
||||
ctx.addEventListener('error', (error) => {
|
||||
console.error('Worker error:', error);
|
||||
ctx.addEventListener("error", (error) => {
|
||||
console.error("Worker error:", error);
|
||||
});
|
||||
|
||||
ctx.addEventListener('unhandledrejection', (event) => {
|
||||
console.error('Unhandled promise rejection in worker:', event);
|
||||
});
|
||||
ctx.addEventListener("unhandledrejection", (event) => {
|
||||
console.error("Unhandled promise rejection in worker:", event);
|
||||
});
|
||||
|
||||
+144
-124
@@ -1,150 +1,170 @@
|
||||
import { PlayerActions, PlayerID, PlayerInfo, PlayerProfile } from "../game/Game";
|
||||
import { ErrorUpdate, GameUpdateViewData } from '../game/GameUpdates';
|
||||
import {
|
||||
PlayerActions,
|
||||
PlayerID,
|
||||
PlayerInfo,
|
||||
PlayerProfile,
|
||||
} from "../game/Game";
|
||||
import { ErrorUpdate, GameUpdateViewData } from "../game/GameUpdates";
|
||||
import { GameConfig, GameID, Turn } from "../Schemas";
|
||||
import { generateID } from "../Util";
|
||||
import { WorkerMessage } from "./WorkerMessages";
|
||||
|
||||
export class WorkerClient {
|
||||
private worker: Worker;
|
||||
private isInitialized = false;
|
||||
private messageHandlers: Map<string, (message: WorkerMessage) => void>;
|
||||
private gameUpdateCallback?: (update: GameUpdateViewData | ErrorUpdate) => void;
|
||||
private worker: Worker;
|
||||
private isInitialized = false;
|
||||
private messageHandlers: Map<string, (message: WorkerMessage) => void>;
|
||||
private gameUpdateCallback?: (
|
||||
update: GameUpdateViewData | ErrorUpdate,
|
||||
) => void;
|
||||
|
||||
constructor(private gameID: GameID, private gameConfig: GameConfig) {
|
||||
this.worker = new Worker(new URL('./Worker.worker.ts', import.meta.url));
|
||||
this.messageHandlers = new Map();
|
||||
constructor(
|
||||
private gameID: GameID,
|
||||
private gameConfig: GameConfig,
|
||||
) {
|
||||
this.worker = new Worker(new URL("./Worker.worker.ts", import.meta.url));
|
||||
this.messageHandlers = new Map();
|
||||
|
||||
// Set up global message handler
|
||||
this.worker.addEventListener('message', this.handleWorkerMessage.bind(this));
|
||||
}
|
||||
// Set up global message handler
|
||||
this.worker.addEventListener(
|
||||
"message",
|
||||
this.handleWorkerMessage.bind(this),
|
||||
);
|
||||
}
|
||||
|
||||
private handleWorkerMessage(event: MessageEvent<WorkerMessage>) {
|
||||
const message = event.data;
|
||||
private handleWorkerMessage(event: MessageEvent<WorkerMessage>) {
|
||||
const message = event.data;
|
||||
|
||||
switch (message.type) {
|
||||
case 'game_update':
|
||||
if (this.gameUpdateCallback && message.gameUpdate) {
|
||||
this.gameUpdateCallback(message.gameUpdate);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'initialized':
|
||||
default:
|
||||
if (message.id && this.messageHandlers.has(message.id)) {
|
||||
const handler = this.messageHandlers.get(message.id)!;
|
||||
handler(message);
|
||||
this.messageHandlers.delete(message.id);
|
||||
}
|
||||
break;
|
||||
switch (message.type) {
|
||||
case "game_update":
|
||||
if (this.gameUpdateCallback && message.gameUpdate) {
|
||||
this.gameUpdateCallback(message.gameUpdate);
|
||||
}
|
||||
break;
|
||||
|
||||
case "initialized":
|
||||
default:
|
||||
if (message.id && this.messageHandlers.has(message.id)) {
|
||||
const handler = this.messageHandlers.get(message.id)!;
|
||||
handler(message);
|
||||
this.messageHandlers.delete(message.id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
initialize(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const messageId = generateID()
|
||||
initialize(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const messageId = generateID();
|
||||
|
||||
this.messageHandlers.set(messageId, (message) => {
|
||||
if (message.type === 'initialized') {
|
||||
this.isInitialized = true;
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
this.messageHandlers.set(messageId, (message) => {
|
||||
if (message.type === "initialized") {
|
||||
this.isInitialized = true;
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
||||
this.worker.postMessage({
|
||||
type: 'init',
|
||||
id: messageId,
|
||||
gameID: this.gameID,
|
||||
gameConfig: this.gameConfig
|
||||
});
|
||||
this.worker.postMessage({
|
||||
type: "init",
|
||||
id: messageId,
|
||||
gameID: this.gameID,
|
||||
gameConfig: this.gameConfig,
|
||||
});
|
||||
|
||||
// Add timeout for initialization
|
||||
setTimeout(() => {
|
||||
if (!this.isInitialized) {
|
||||
this.messageHandlers.delete(messageId);
|
||||
reject(new Error('Worker initialization timeout'));
|
||||
}
|
||||
}, 5000); // 5 second timeout
|
||||
});
|
||||
}
|
||||
|
||||
start(gameUpdate: (gu: GameUpdateViewData | ErrorUpdate) => void) {
|
||||
// Add timeout for initialization
|
||||
setTimeout(() => {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error('Failed to initialize pathfinder');
|
||||
this.messageHandlers.delete(messageId);
|
||||
reject(new Error("Worker initialization timeout"));
|
||||
}
|
||||
this.gameUpdateCallback = gameUpdate;
|
||||
}, 5000); // 5 second timeout
|
||||
});
|
||||
}
|
||||
|
||||
start(gameUpdate: (gu: GameUpdateViewData | ErrorUpdate) => void) {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error("Failed to initialize pathfinder");
|
||||
}
|
||||
this.gameUpdateCallback = gameUpdate;
|
||||
}
|
||||
|
||||
sendTurn(turn: Turn) {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error("Worker not initialized");
|
||||
}
|
||||
|
||||
sendTurn(turn: Turn) {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error('Worker not initialized');
|
||||
this.worker.postMessage({
|
||||
type: "turn",
|
||||
turn,
|
||||
});
|
||||
}
|
||||
|
||||
sendHeartbeat() {
|
||||
this.worker.postMessage({
|
||||
type: "heartbeat",
|
||||
});
|
||||
}
|
||||
|
||||
playerProfile(playerID: number): Promise<PlayerProfile> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.isInitialized) {
|
||||
reject(new Error("Worker not initialized"));
|
||||
return;
|
||||
}
|
||||
|
||||
const messageId = generateID();
|
||||
|
||||
this.messageHandlers.set(messageId, (message) => {
|
||||
if (
|
||||
message.type === "player_profile_result" &&
|
||||
message.result !== undefined
|
||||
) {
|
||||
resolve(message.result);
|
||||
}
|
||||
});
|
||||
|
||||
this.worker.postMessage({
|
||||
type: 'turn',
|
||||
turn
|
||||
});
|
||||
}
|
||||
this.worker.postMessage({
|
||||
type: "player_profile",
|
||||
id: messageId,
|
||||
playerID: playerID,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
sendHeartbeat() {
|
||||
this.worker.postMessage({
|
||||
type: 'heartbeat'
|
||||
});
|
||||
}
|
||||
playerInteraction(
|
||||
playerID: PlayerID,
|
||||
x: number,
|
||||
y: number,
|
||||
): Promise<PlayerActions> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.isInitialized) {
|
||||
reject(new Error("Worker not initialized"));
|
||||
return;
|
||||
}
|
||||
|
||||
playerProfile(playerID: number): Promise<PlayerProfile> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.isInitialized) {
|
||||
reject(new Error('Worker not initialized'));
|
||||
return;
|
||||
}
|
||||
const messageId = generateID();
|
||||
|
||||
const messageId = generateID()
|
||||
this.messageHandlers.set(messageId, (message) => {
|
||||
if (
|
||||
message.type === "player_actions_result" &&
|
||||
message.result !== undefined
|
||||
) {
|
||||
resolve(message.result);
|
||||
}
|
||||
});
|
||||
|
||||
this.messageHandlers.set(messageId, (message) => {
|
||||
if (message.type === 'player_profile_result' && message.result !== undefined) {
|
||||
resolve(message.result);
|
||||
}
|
||||
});
|
||||
this.worker.postMessage({
|
||||
type: "player_actions",
|
||||
id: messageId,
|
||||
playerID: playerID,
|
||||
x: x,
|
||||
y: y,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
this.worker.postMessage({
|
||||
type: 'player_profile',
|
||||
id: messageId,
|
||||
playerID: playerID,
|
||||
});
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
playerInteraction(playerID: PlayerID, x: number, y: number): Promise<PlayerActions> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.isInitialized) {
|
||||
reject(new Error('Worker not initialized'));
|
||||
return;
|
||||
}
|
||||
|
||||
const messageId = generateID()
|
||||
|
||||
this.messageHandlers.set(messageId, (message) => {
|
||||
if (message.type === 'player_actions_result' && message.result !== undefined) {
|
||||
resolve(message.result);
|
||||
}
|
||||
});
|
||||
|
||||
this.worker.postMessage({
|
||||
type: 'player_actions',
|
||||
id: messageId,
|
||||
playerID: playerID,
|
||||
x: x,
|
||||
y: y
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
cleanup() {
|
||||
this.worker.terminate();
|
||||
this.messageHandlers.clear();
|
||||
this.gameUpdateCallback = undefined;
|
||||
}
|
||||
}
|
||||
cleanup() {
|
||||
this.worker.terminate();
|
||||
this.messageHandlers.clear();
|
||||
this.gameUpdateCallback = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,74 +1,83 @@
|
||||
import { GameUpdateViewData } from '../game/GameUpdates';
|
||||
import { GameUpdateViewData } from "../game/GameUpdates";
|
||||
import { GameConfig, GameID, Turn } from "../Schemas";
|
||||
import { PlayerActions, PlayerID, PlayerProfile } from "../game/Game";
|
||||
|
||||
export type WorkerMessageType =
|
||||
| 'heartbeat'
|
||||
| 'init'
|
||||
| 'initialized'
|
||||
| 'turn'
|
||||
| 'game_update'
|
||||
| 'player_actions'
|
||||
| 'player_actions_result'
|
||||
| 'player_profile'
|
||||
| 'player_profile_result'
|
||||
| "heartbeat"
|
||||
| "init"
|
||||
| "initialized"
|
||||
| "turn"
|
||||
| "game_update"
|
||||
| "player_actions"
|
||||
| "player_actions_result"
|
||||
| "player_profile"
|
||||
| "player_profile_result";
|
||||
|
||||
// Base interface for all messages
|
||||
interface BaseWorkerMessage {
|
||||
type: WorkerMessageType;
|
||||
id?: string;
|
||||
type: WorkerMessageType;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export interface HeartbeatMessage extends BaseWorkerMessage {
|
||||
type: 'heartbeat'
|
||||
type: "heartbeat";
|
||||
}
|
||||
|
||||
// Messages from main thread to worker
|
||||
export interface InitMessage extends BaseWorkerMessage {
|
||||
type: 'init';
|
||||
gameID: GameID;
|
||||
gameConfig: GameConfig;
|
||||
type: "init";
|
||||
gameID: GameID;
|
||||
gameConfig: GameConfig;
|
||||
}
|
||||
|
||||
export interface TurnMessage extends BaseWorkerMessage {
|
||||
type: 'turn';
|
||||
turn: Turn;
|
||||
type: "turn";
|
||||
turn: Turn;
|
||||
}
|
||||
|
||||
// Messages from worker to main thread
|
||||
export interface InitializedMessage extends BaseWorkerMessage {
|
||||
type: 'initialized';
|
||||
type: "initialized";
|
||||
}
|
||||
|
||||
export interface GameUpdateMessage extends BaseWorkerMessage {
|
||||
type: 'game_update';
|
||||
gameUpdate: GameUpdateViewData;
|
||||
type: "game_update";
|
||||
gameUpdate: GameUpdateViewData;
|
||||
}
|
||||
|
||||
export interface PlayerActionsMessage extends BaseWorkerMessage {
|
||||
type: 'player_actions'
|
||||
playerID: PlayerID
|
||||
x: number,
|
||||
y: number
|
||||
type: "player_actions";
|
||||
playerID: PlayerID;
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface PlayerActionsResultMessage extends BaseWorkerMessage {
|
||||
type: 'player_actions_result';
|
||||
result: PlayerActions;
|
||||
type: "player_actions_result";
|
||||
result: PlayerActions;
|
||||
}
|
||||
|
||||
export interface PlayerProfileMessage extends BaseWorkerMessage {
|
||||
type: 'player_profile'
|
||||
playerID: number
|
||||
type: "player_profile";
|
||||
playerID: number;
|
||||
}
|
||||
|
||||
export interface PlayerProfileResultMessage extends BaseWorkerMessage {
|
||||
type: 'player_profile_result'
|
||||
result: PlayerProfile
|
||||
type: "player_profile_result";
|
||||
result: PlayerProfile;
|
||||
}
|
||||
|
||||
// Union types for type safety
|
||||
export type MainThreadMessage = HeartbeatMessage | InitMessage | TurnMessage | PlayerActionsMessage | PlayerProfileMessage
|
||||
export type MainThreadMessage =
|
||||
| HeartbeatMessage
|
||||
| InitMessage
|
||||
| TurnMessage
|
||||
| PlayerActionsMessage
|
||||
| PlayerProfileMessage;
|
||||
|
||||
// Message send from worker
|
||||
export type WorkerMessage = InitializedMessage | GameUpdateMessage | PlayerActionsResultMessage | PlayerProfileResultMessage
|
||||
export type WorkerMessage =
|
||||
| InitializedMessage
|
||||
| GameUpdateMessage
|
||||
| PlayerActionsResultMessage
|
||||
| PlayerProfileResultMessage;
|
||||
|
||||
Vendored
+25
-25
@@ -1,33 +1,33 @@
|
||||
declare module '*.png' {
|
||||
const content: string;
|
||||
export default content;
|
||||
declare module "*.png" {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
declare module '*.jpg' {
|
||||
const value: string;
|
||||
export default value;
|
||||
declare module "*.jpg" {
|
||||
const value: string;
|
||||
export default value;
|
||||
}
|
||||
|
||||
declare module '*.jpeg' {
|
||||
const value: string;
|
||||
export default value;
|
||||
declare module "*.jpeg" {
|
||||
const value: string;
|
||||
export default value;
|
||||
}
|
||||
declare module '*.svg' {
|
||||
const value: string;
|
||||
export default value;
|
||||
declare module "*.svg" {
|
||||
const value: string;
|
||||
export default value;
|
||||
}
|
||||
declare module '*.bin' {
|
||||
const value: string;
|
||||
export default value;
|
||||
declare module "*.bin" {
|
||||
const value: string;
|
||||
export default value;
|
||||
}
|
||||
declare module '*.txt' {
|
||||
const value: string;
|
||||
export default value;
|
||||
declare module "*.txt" {
|
||||
const value: string;
|
||||
export default value;
|
||||
}
|
||||
declare module '*.html' {
|
||||
const content: string;
|
||||
export default content;
|
||||
declare module "*.html" {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
declare module "*.json" {
|
||||
const value: any;
|
||||
export default value;
|
||||
}
|
||||
declare module '*.json' {
|
||||
const value: any;
|
||||
export default value;
|
||||
}
|
||||
+233
-190
@@ -1,267 +1,310 @@
|
||||
import { decodePNGFromStream } from 'pureimage'; import path from 'path';
|
||||
import fs from 'fs/promises';
|
||||
import { createReadStream } from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
import { decodePNGFromStream } from "pureimage";
|
||||
import path from "path";
|
||||
import fs from "fs/promises";
|
||||
import { createReadStream } from "fs";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const mapName = "Europe"
|
||||
const mapName = "Europe";
|
||||
|
||||
interface Coord {
|
||||
x: number;
|
||||
y: number;
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
enum TerrainType {
|
||||
Land,
|
||||
Water
|
||||
Land,
|
||||
Water,
|
||||
}
|
||||
|
||||
class Terrain {
|
||||
public shoreline: boolean = false
|
||||
public magnitude: number = 0
|
||||
public ocean: boolean
|
||||
constructor(public type: TerrainType) { }
|
||||
public shoreline: boolean = false;
|
||||
public magnitude: number = 0;
|
||||
public ocean: boolean;
|
||||
constructor(public type: TerrainType) {}
|
||||
}
|
||||
|
||||
export async function loadTerrainMap(): Promise<void> {
|
||||
const imagePath = path.resolve(__dirname, '..', '..', 'resources', 'maps', mapName + '.png');
|
||||
const imagePath = path.resolve(
|
||||
__dirname,
|
||||
"..",
|
||||
"..",
|
||||
"resources",
|
||||
"maps",
|
||||
mapName + ".png",
|
||||
);
|
||||
|
||||
const readStream = createReadStream(imagePath);
|
||||
const img = await decodePNGFromStream(readStream);
|
||||
const readStream = createReadStream(imagePath);
|
||||
const img = await decodePNGFromStream(readStream);
|
||||
|
||||
console.log('Image loaded successfully');
|
||||
console.log('Image dimensions:', img.width, 'x', img.height);
|
||||
console.log("Image loaded successfully");
|
||||
console.log("Image dimensions:", img.width, "x", img.height);
|
||||
|
||||
const terrain: Terrain[][] = Array(img.width).fill(null).map(() => Array(img.height).fill(null));
|
||||
const terrain: Terrain[][] = Array(img.width)
|
||||
.fill(null)
|
||||
.map(() => Array(img.height).fill(null));
|
||||
|
||||
// Iterate through each pixel
|
||||
for (let x = 0; x < img.width; x++) {
|
||||
for (let y = 0; y < img.height; y++) {
|
||||
const color = img.getPixelRGBA(x, y);
|
||||
const alpha = color & 0xff;
|
||||
const blue = (color >> 8) & 0xff;
|
||||
// Iterate through each pixel
|
||||
for (let x = 0; x < img.width; x++) {
|
||||
for (let y = 0; y < img.height; y++) {
|
||||
const color = img.getPixelRGBA(x, y);
|
||||
const alpha = color & 0xff;
|
||||
const blue = (color >> 8) & 0xff;
|
||||
|
||||
if (alpha < 20 || blue == 106) { // transparent
|
||||
terrain[x][y] = new Terrain(TerrainType.Water);
|
||||
} else {
|
||||
terrain[x][y] = new Terrain(TerrainType.Land)
|
||||
terrain[x][y].magnitude = 0
|
||||
if (alpha < 20 || blue == 106) {
|
||||
// transparent
|
||||
terrain[x][y] = new Terrain(TerrainType.Water);
|
||||
} else {
|
||||
terrain[x][y] = new Terrain(TerrainType.Land);
|
||||
terrain[x][y].magnitude = 0;
|
||||
|
||||
|
||||
// 140 -> 200 = 60
|
||||
const mag = Math.min(200, Math.max(140, blue)) - 140
|
||||
terrain[x][y].magnitude = mag / 2
|
||||
|
||||
}
|
||||
}
|
||||
// 140 -> 200 = 60
|
||||
const mag = Math.min(200, Math.max(140, blue)) - 140;
|
||||
terrain[x][y].magnitude = mag / 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
removeSmallLakes(terrain)
|
||||
const shorelineWaters = processShore(terrain)
|
||||
processDistToLand(shorelineWaters, terrain)
|
||||
processOcean(terrain)
|
||||
const outputPath = path.join(__dirname, '..', '..', 'resources', 'maps', mapName + '.bin');
|
||||
fs.writeFile(outputPath, packTerrain(terrain));
|
||||
removeSmallLakes(terrain);
|
||||
const shorelineWaters = processShore(terrain);
|
||||
processDistToLand(shorelineWaters, terrain);
|
||||
processOcean(terrain);
|
||||
const outputPath = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"..",
|
||||
"resources",
|
||||
"maps",
|
||||
mapName + ".bin",
|
||||
);
|
||||
fs.writeFile(outputPath, packTerrain(terrain));
|
||||
|
||||
const miniTerrain = await createMiniMap(terrain)
|
||||
const miniOutputPath = path.join(__dirname, '..', '..', 'resources', 'maps', mapName + 'Mini.bin');
|
||||
fs.writeFile(miniOutputPath, packTerrain(miniTerrain))
|
||||
const miniTerrain = await createMiniMap(terrain);
|
||||
const miniOutputPath = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"..",
|
||||
"resources",
|
||||
"maps",
|
||||
mapName + "Mini.bin",
|
||||
);
|
||||
fs.writeFile(miniOutputPath, packTerrain(miniTerrain));
|
||||
}
|
||||
|
||||
export async function createMiniMap(tm: Terrain[][]): Promise<Terrain[][]> {
|
||||
// Create 2D array properly with correct dimensions
|
||||
const miniMap: Terrain[][] = Array(Math.floor(tm.length / 2))
|
||||
.fill(null)
|
||||
.map(() => Array(Math.floor(tm[0].length / 2)).fill(null));
|
||||
// Create 2D array properly with correct dimensions
|
||||
const miniMap: Terrain[][] = Array(Math.floor(tm.length / 2))
|
||||
.fill(null)
|
||||
.map(() => Array(Math.floor(tm[0].length / 2)).fill(null));
|
||||
|
||||
for (let x = 0; x < tm.length; x++) {
|
||||
for (let y = 0; y < tm[0].length; y++) {
|
||||
const miniX = Math.floor(x / 2);
|
||||
const miniY = Math.floor(y / 2);
|
||||
for (let x = 0; x < tm.length; x++) {
|
||||
for (let y = 0; y < tm[0].length; y++) {
|
||||
const miniX = Math.floor(x / 2);
|
||||
const miniY = Math.floor(y / 2);
|
||||
|
||||
if (miniMap[miniX][miniY] == null || miniMap[miniX][miniY].type != TerrainType.Water) {
|
||||
// We shrink 4 tiles into 1 tile. If any of the 4 large tiles
|
||||
// has water, then the mini tile is considered water.
|
||||
miniMap[miniX][miniY] = tm[x][y]
|
||||
}
|
||||
}
|
||||
if (
|
||||
miniMap[miniX][miniY] == null ||
|
||||
miniMap[miniX][miniY].type != TerrainType.Water
|
||||
) {
|
||||
// We shrink 4 tiles into 1 tile. If any of the 4 large tiles
|
||||
// has water, then the mini tile is considered water.
|
||||
miniMap[miniX][miniY] = tm[x][y];
|
||||
}
|
||||
}
|
||||
return miniMap
|
||||
}
|
||||
return miniMap;
|
||||
}
|
||||
|
||||
|
||||
function processShore(map: Terrain[][]): Coord[] {
|
||||
const shorelineWaters: Coord[] = []
|
||||
for (let x = 0; x < map.length; x++) {
|
||||
for (let y = 0; y < map[0].length; y++) {
|
||||
const terrain = map[x][y]
|
||||
const ns = neighbors(x, y, map)
|
||||
if (terrain.type == TerrainType.Land) {
|
||||
if (ns.filter(t => t.type == TerrainType.Water).length > 0) {
|
||||
terrain.shoreline = true
|
||||
}
|
||||
} else {
|
||||
if (ns.filter(t => t.type == TerrainType.Land).length > 0) {
|
||||
terrain.shoreline = true
|
||||
shorelineWaters.push({ x, y })
|
||||
}
|
||||
}
|
||||
const shorelineWaters: Coord[] = [];
|
||||
for (let x = 0; x < map.length; x++) {
|
||||
for (let y = 0; y < map[0].length; y++) {
|
||||
const terrain = map[x][y];
|
||||
const ns = neighbors(x, y, map);
|
||||
if (terrain.type == TerrainType.Land) {
|
||||
if (ns.filter((t) => t.type == TerrainType.Water).length > 0) {
|
||||
terrain.shoreline = true;
|
||||
}
|
||||
} else {
|
||||
if (ns.filter((t) => t.type == TerrainType.Land).length > 0) {
|
||||
terrain.shoreline = true;
|
||||
shorelineWaters.push({ x, y });
|
||||
}
|
||||
}
|
||||
}
|
||||
return shorelineWaters
|
||||
}
|
||||
return shorelineWaters;
|
||||
}
|
||||
|
||||
function processDistToLand(shorelineWaters: Coord[], map: Terrain[][]) {
|
||||
const queue: [Coord, number][] = shorelineWaters.map(coord => [coord, 0]);
|
||||
const visited = new Set<string>();
|
||||
const queue: [Coord, number][] = shorelineWaters.map((coord) => [coord, 0]);
|
||||
const visited = new Set<string>();
|
||||
|
||||
while (queue.length > 0) {
|
||||
const [coord, distance] = queue.shift()!;
|
||||
const key = `${coord.x},${coord.y}`;
|
||||
while (queue.length > 0) {
|
||||
const [coord, distance] = queue.shift()!;
|
||||
const key = `${coord.x},${coord.y}`;
|
||||
|
||||
if (visited.has(key)) continue;
|
||||
visited.add(key);
|
||||
if (visited.has(key)) continue;
|
||||
visited.add(key);
|
||||
|
||||
const terrain = map[coord.x][coord.y];
|
||||
if (terrain.type === TerrainType.Water) {
|
||||
terrain.magnitude = distance;
|
||||
const terrain = map[coord.x][coord.y];
|
||||
if (terrain.type === TerrainType.Water) {
|
||||
terrain.magnitude = distance;
|
||||
|
||||
for (const [dx, dy] of [[-1, 0], [1, 0], [0, -1], [0, 1]]) {
|
||||
const newX = coord.x + dx;
|
||||
const newY = coord.y + dy;
|
||||
for (const [dx, dy] of [
|
||||
[-1, 0],
|
||||
[1, 0],
|
||||
[0, -1],
|
||||
[0, 1],
|
||||
]) {
|
||||
const newX = coord.x + dx;
|
||||
const newY = coord.y + dy;
|
||||
|
||||
if (newX >= 0 && newX < map.length && newY >= 0 && newY < map[0].length) {
|
||||
queue.push([{ x: newX, y: newY }, distance + 1]);
|
||||
}
|
||||
}
|
||||
if (
|
||||
newX >= 0 &&
|
||||
newX < map.length &&
|
||||
newY >= 0 &&
|
||||
newY < map[0].length
|
||||
) {
|
||||
queue.push([{ x: newX, y: newY }, distance + 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function neighbors(x: number, y: number, map: Terrain[][]): Terrain[] {
|
||||
const ns: Terrain[] = []
|
||||
if (x > 0) {
|
||||
ns.push(map[x - 1][y])
|
||||
}
|
||||
if (x < map.length - 1) {
|
||||
ns.push(map[x + 1][y])
|
||||
}
|
||||
if (y > 0) {
|
||||
ns.push(map[x][y - 1])
|
||||
}
|
||||
if (y < map[0].length - 1) {
|
||||
ns.push(map[x][y + 1])
|
||||
}
|
||||
return ns
|
||||
const ns: Terrain[] = [];
|
||||
if (x > 0) {
|
||||
ns.push(map[x - 1][y]);
|
||||
}
|
||||
if (x < map.length - 1) {
|
||||
ns.push(map[x + 1][y]);
|
||||
}
|
||||
if (y > 0) {
|
||||
ns.push(map[x][y - 1]);
|
||||
}
|
||||
if (y < map[0].length - 1) {
|
||||
ns.push(map[x][y + 1]);
|
||||
}
|
||||
return ns;
|
||||
}
|
||||
|
||||
function packTerrain(map: Terrain[][]): Uint8Array {
|
||||
const width = map.length;
|
||||
const height = map[0].length;
|
||||
const packedData = new Uint8Array(4 + width * height);
|
||||
const width = map.length;
|
||||
const height = map[0].length;
|
||||
const packedData = new Uint8Array(4 + width * height);
|
||||
|
||||
// Add width and height to the first 4 bytes
|
||||
packedData[0] = width & 0xFF;
|
||||
packedData[1] = (width >> 8) & 0xFF;
|
||||
packedData[2] = height & 0xFF;
|
||||
packedData[3] = (height >> 8) & 0xFF;
|
||||
// Add width and height to the first 4 bytes
|
||||
packedData[0] = width & 0xff;
|
||||
packedData[1] = (width >> 8) & 0xff;
|
||||
packedData[2] = height & 0xff;
|
||||
packedData[3] = (height >> 8) & 0xff;
|
||||
|
||||
for (let x = 0; x < width; x++) {
|
||||
for (let y = 0; y < height; y++) {
|
||||
const terrain = map[x][y];
|
||||
let packedByte = 0;
|
||||
if (terrain == null) {
|
||||
throw new Error(`terrain null at ${x}:${y}`);
|
||||
}
|
||||
|
||||
for (let x = 0; x < width; x++) {
|
||||
for (let y = 0; y < height; y++) {
|
||||
const terrain = map[x][y];
|
||||
let packedByte = 0;
|
||||
if (terrain == null) {
|
||||
throw new Error(`terrain null at ${x}:${y}`)
|
||||
}
|
||||
if (terrain.type === TerrainType.Land) {
|
||||
packedByte |= 0b10000000;
|
||||
}
|
||||
if (terrain.shoreline) {
|
||||
packedByte |= 0b01000000;
|
||||
}
|
||||
if (terrain.ocean) {
|
||||
packedByte |= 0b00100000;
|
||||
}
|
||||
if (terrain.type == TerrainType.Land) {
|
||||
packedByte |= Math.min(Math.ceil(terrain.magnitude), 31);
|
||||
} else {
|
||||
packedByte |= Math.min(Math.ceil(terrain.magnitude / 2), 31);
|
||||
}
|
||||
|
||||
if (terrain.type === TerrainType.Land) {
|
||||
packedByte |= 0b10000000;
|
||||
}
|
||||
if (terrain.shoreline) {
|
||||
packedByte |= 0b01000000;
|
||||
}
|
||||
if (terrain.ocean) {
|
||||
packedByte |= 0b00100000;
|
||||
}
|
||||
if (terrain.type == TerrainType.Land) {
|
||||
packedByte |= Math.min(Math.ceil(terrain.magnitude), 31);
|
||||
} else {
|
||||
packedByte |= Math.min(Math.ceil(terrain.magnitude / 2), 31);
|
||||
}
|
||||
|
||||
packedData[4 + y * width + x] = packedByte;
|
||||
}
|
||||
packedData[4 + y * width + x] = packedByte;
|
||||
}
|
||||
logBinaryAsBits(packedData)
|
||||
return packedData;
|
||||
}
|
||||
logBinaryAsBits(packedData);
|
||||
return packedData;
|
||||
}
|
||||
|
||||
function processOcean(map: Terrain[][]) {
|
||||
const queue: Coord[] = [];
|
||||
if (map[0][0].type == TerrainType.Water) {
|
||||
queue.push({ x: 0, y: 0 })
|
||||
} else if (map[map.length - 1][map[0].length - 1].type == TerrainType.Water) {
|
||||
queue.push({ x: map.length - 1, y: map[0].length - 1 })
|
||||
} else {
|
||||
queue.push({ x: 0, y: map[0].length - 1 })
|
||||
}
|
||||
const visited = new Set<string>();
|
||||
const queue: Coord[] = [];
|
||||
if (map[0][0].type == TerrainType.Water) {
|
||||
queue.push({ x: 0, y: 0 });
|
||||
} else if (map[map.length - 1][map[0].length - 1].type == TerrainType.Water) {
|
||||
queue.push({ x: map.length - 1, y: map[0].length - 1 });
|
||||
} else {
|
||||
queue.push({ x: 0, y: map[0].length - 1 });
|
||||
}
|
||||
const visited = new Set<string>();
|
||||
|
||||
while (queue.length > 0) {
|
||||
const coord = queue.shift()!;
|
||||
const key = `${coord.x},${coord.y}`;
|
||||
while (queue.length > 0) {
|
||||
const coord = queue.shift()!;
|
||||
const key = `${coord.x},${coord.y}`;
|
||||
|
||||
if (visited.has(key)) continue;
|
||||
visited.add(key);
|
||||
if (visited.has(key)) continue;
|
||||
visited.add(key);
|
||||
|
||||
const terrain = map[coord.x][coord.y];
|
||||
if (terrain.type === TerrainType.Water) {
|
||||
terrain.ocean = true;
|
||||
const terrain = map[coord.x][coord.y];
|
||||
if (terrain.type === TerrainType.Water) {
|
||||
terrain.ocean = true;
|
||||
|
||||
// Check neighbors
|
||||
for (const [dx, dy] of [[-1, 0], [1, 0], [0, -1], [0, 1]]) {
|
||||
const newX = coord.x + dx;
|
||||
const newY = coord.y + dy;
|
||||
// Check neighbors
|
||||
for (const [dx, dy] of [
|
||||
[-1, 0],
|
||||
[1, 0],
|
||||
[0, -1],
|
||||
[0, 1],
|
||||
]) {
|
||||
const newX = coord.x + dx;
|
||||
const newY = coord.y + dy;
|
||||
|
||||
if (newX >= 0 && newX < map.length && newY >= 0 && newY < map[0].length) {
|
||||
queue.push({ x: newX, y: newY });
|
||||
}
|
||||
}
|
||||
if (
|
||||
newX >= 0 &&
|
||||
newX < map.length &&
|
||||
newY >= 0 &&
|
||||
newY < map[0].length
|
||||
) {
|
||||
queue.push({ x: newX, y: newY });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removeSmallLakes(map: Terrain[][]) {
|
||||
console.log(`removing lakes ${map.length}, ${map[0].length}`)
|
||||
console.log(`removing lakes ${map.length}, ${map[0].length}`);
|
||||
|
||||
for (let x = 0; x < map.length; x++) {
|
||||
for (let y = 0; y < map[0].length; y++) {
|
||||
if (map[x][y].type != TerrainType.Water) {
|
||||
continue
|
||||
}
|
||||
let allLand = true
|
||||
for (const neighbor of neighbors(x, y, map)) {
|
||||
if (neighbor.type != TerrainType.Land) {
|
||||
allLand = false
|
||||
}
|
||||
}
|
||||
if (allLand) {
|
||||
map[x][y].type = TerrainType.Land
|
||||
map[x][y].magnitude = 0
|
||||
}
|
||||
for (let x = 0; x < map.length; x++) {
|
||||
for (let y = 0; y < map[0].length; y++) {
|
||||
if (map[x][y].type != TerrainType.Water) {
|
||||
continue;
|
||||
}
|
||||
let allLand = true;
|
||||
for (const neighbor of neighbors(x, y, map)) {
|
||||
if (neighbor.type != TerrainType.Land) {
|
||||
allLand = false;
|
||||
}
|
||||
}
|
||||
if (allLand) {
|
||||
map[x][y].type = TerrainType.Land;
|
||||
map[x][y].magnitude = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function logBinaryAsBits(data: Uint8Array, length: number = 8) {
|
||||
const bits = Array.from(data.slice(0, length))
|
||||
.map(b => b.toString(2).padStart(8, '0'))
|
||||
.join(' ');
|
||||
console.log('Binary data (bits):', bits);
|
||||
const bits = Array.from(data.slice(0, length))
|
||||
.map((b) => b.toString(2).padStart(8, "0"))
|
||||
.join(" ");
|
||||
console.log("Binary data (bits):", bits);
|
||||
}
|
||||
|
||||
await loadTerrainMap()
|
||||
await loadTerrainMap();
|
||||
|
||||
+136
-120
@@ -1,7 +1,12 @@
|
||||
import { GameConfig, GameID, GameRecord, GameRecordSchema, Turn } from "../core/Schemas";
|
||||
import { Storage } from '@google-cloud/storage';
|
||||
import { BigQuery } from '@google-cloud/bigquery';
|
||||
|
||||
import {
|
||||
GameConfig,
|
||||
GameID,
|
||||
GameRecord,
|
||||
GameRecordSchema,
|
||||
Turn,
|
||||
} from "../core/Schemas";
|
||||
import { Storage } from "@google-cloud/storage";
|
||||
import { BigQuery } from "@google-cloud/bigquery";
|
||||
|
||||
const storage = new Storage();
|
||||
const bucket = storage.bucket("openfront-games");
|
||||
@@ -11,166 +16,177 @@ const MAX_RETRIES = 5;
|
||||
const INITIAL_RETRY_DELAY_MS = 1000; // Start with 1 second delay
|
||||
|
||||
export async function archive(gameRecord: GameRecord) {
|
||||
try {
|
||||
// First archive to BigQuery with retries
|
||||
await withRetry(
|
||||
() => archiveToBigQuery(gameRecord),
|
||||
'BigQuery archive',
|
||||
gameRecord.id
|
||||
);
|
||||
try {
|
||||
// First archive to BigQuery with retries
|
||||
await withRetry(
|
||||
() => archiveToBigQuery(gameRecord),
|
||||
"BigQuery archive",
|
||||
gameRecord.id,
|
||||
);
|
||||
|
||||
// Then archive to GCS with retries if there are turns
|
||||
if (gameRecord.turns.length > 0) {
|
||||
console.log(`${gameRecord.id}: game has more than zero turns, attempting to write to GCS`);
|
||||
await withRetry(
|
||||
() => archiveToGCS(gameRecord),
|
||||
'GCS archive',
|
||||
gameRecord.id
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`${gameRecord.id}: Final archive error: ${error}`, {
|
||||
message: error?.message || error,
|
||||
stack: error?.stack,
|
||||
name: error?.name,
|
||||
...(error && typeof error === 'object' ? error : {})
|
||||
});
|
||||
// Then archive to GCS with retries if there are turns
|
||||
if (gameRecord.turns.length > 0) {
|
||||
console.log(
|
||||
`${gameRecord.id}: game has more than zero turns, attempting to write to GCS`,
|
||||
);
|
||||
await withRetry(
|
||||
() => archiveToGCS(gameRecord),
|
||||
"GCS archive",
|
||||
gameRecord.id,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`${gameRecord.id}: Final archive error: ${error}`, {
|
||||
message: error?.message || error,
|
||||
stack: error?.stack,
|
||||
name: error?.name,
|
||||
...(error && typeof error === "object" ? error : {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function delay(ms: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function withRetry<T>(
|
||||
operation: () => Promise<T>,
|
||||
operationName: string,
|
||||
gameId: string,
|
||||
operation: () => Promise<T>,
|
||||
operationName: string,
|
||||
gameId: string,
|
||||
): Promise<T> {
|
||||
let lastError: Error | null = null;
|
||||
let lastError: Error | null = null;
|
||||
|
||||
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
||||
try {
|
||||
return await operation();
|
||||
} catch (error) {
|
||||
lastError = error as Error;
|
||||
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
||||
try {
|
||||
return await operation();
|
||||
} catch (error) {
|
||||
lastError = error as Error;
|
||||
|
||||
if (attempt < MAX_RETRIES) {
|
||||
const backoffDelay = INITIAL_RETRY_DELAY_MS * Math.pow(2, attempt);
|
||||
const jitter = Math.random() * 1000;
|
||||
const totalDelay = backoffDelay + jitter;
|
||||
if (attempt < MAX_RETRIES) {
|
||||
const backoffDelay = INITIAL_RETRY_DELAY_MS * Math.pow(2, attempt);
|
||||
const jitter = Math.random() * 1000;
|
||||
const totalDelay = backoffDelay + jitter;
|
||||
|
||||
console.log(`${gameId}: ${operationName} attempt ${attempt + 1} failed with ${error}. Retrying in ${Math.round(totalDelay)}ms...`);
|
||||
await delay(totalDelay);
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
`${gameId}: ${operationName} attempt ${attempt + 1} failed with ${error}. Retrying in ${Math.round(totalDelay)}ms...`,
|
||||
);
|
||||
await delay(totalDelay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.error(`${gameId}: All ${MAX_RETRIES + 1} ${operationName} attempts failed. Last error:`, lastError);
|
||||
throw lastError;
|
||||
console.error(
|
||||
`${gameId}: All ${MAX_RETRIES + 1} ${operationName} attempts failed. Last error:`,
|
||||
lastError,
|
||||
);
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
async function archiveToBigQuery(gameRecord: GameRecord) {
|
||||
const row = {
|
||||
id: gameRecord.id,
|
||||
start: new Date(gameRecord.startTimestampMS),
|
||||
end: new Date(gameRecord.endTimestampMS),
|
||||
duration_seconds: gameRecord.durationSeconds,
|
||||
number_turns: gameRecord.num_turns,
|
||||
game_mode: gameRecord.gameConfig.gameType,
|
||||
winner: gameRecord.winner,
|
||||
difficulty: gameRecord.gameConfig.difficulty,
|
||||
map: gameRecord.gameConfig.gameMap,
|
||||
players: gameRecord.players.map(p => ({
|
||||
username: p.username,
|
||||
ip: anonymizeIP(p.ip),
|
||||
persistentID: p.persistentID,
|
||||
clientID: p.clientID,
|
||||
})),
|
||||
};
|
||||
const row = {
|
||||
id: gameRecord.id,
|
||||
start: new Date(gameRecord.startTimestampMS),
|
||||
end: new Date(gameRecord.endTimestampMS),
|
||||
duration_seconds: gameRecord.durationSeconds,
|
||||
number_turns: gameRecord.num_turns,
|
||||
game_mode: gameRecord.gameConfig.gameType,
|
||||
winner: gameRecord.winner,
|
||||
difficulty: gameRecord.gameConfig.difficulty,
|
||||
map: gameRecord.gameConfig.gameMap,
|
||||
players: gameRecord.players.map((p) => ({
|
||||
username: p.username,
|
||||
ip: anonymizeIP(p.ip),
|
||||
persistentID: p.persistentID,
|
||||
clientID: p.clientID,
|
||||
})),
|
||||
};
|
||||
|
||||
const [apiResponse] = await bigquery
|
||||
.dataset('game_archive')
|
||||
.table('game_results')
|
||||
.insert([row]);
|
||||
const [apiResponse] = await bigquery
|
||||
.dataset("game_archive")
|
||||
.table("game_results")
|
||||
.insert([row]);
|
||||
|
||||
console.log(`${gameRecord.id}: wrote game metadata to BigQuery`);
|
||||
return apiResponse;
|
||||
console.log(`${gameRecord.id}: wrote game metadata to BigQuery`);
|
||||
return apiResponse;
|
||||
}
|
||||
|
||||
async function archiveToGCS(gameRecord: GameRecord) {
|
||||
// Create a deep copy to avoid modifying the original
|
||||
const recordCopy = JSON.parse(JSON.stringify(gameRecord));
|
||||
// Create a deep copy to avoid modifying the original
|
||||
const recordCopy = JSON.parse(JSON.stringify(gameRecord));
|
||||
|
||||
// Players may see this so make sure to clear PII
|
||||
recordCopy.players.forEach(p => {
|
||||
p.ip = "REDACTED";
|
||||
p.persistentID = "REDACTED";
|
||||
});
|
||||
// Players may see this so make sure to clear PII
|
||||
recordCopy.players.forEach((p) => {
|
||||
p.ip = "REDACTED";
|
||||
p.persistentID = "REDACTED";
|
||||
});
|
||||
|
||||
const file = bucket.file(recordCopy.id);
|
||||
await file.save(JSON.stringify(GameRecordSchema.parse(recordCopy)), {
|
||||
contentType: 'application/json'
|
||||
});
|
||||
const file = bucket.file(recordCopy.id);
|
||||
await file.save(JSON.stringify(GameRecordSchema.parse(recordCopy)), {
|
||||
contentType: "application/json",
|
||||
});
|
||||
|
||||
console.log(`${gameRecord.id}: game record successfully written to GCS`);
|
||||
console.log(`${gameRecord.id}: game record successfully written to GCS`);
|
||||
}
|
||||
|
||||
function anonymizeIPv4(ipv4: string): string | null {
|
||||
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
|
||||
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
|
||||
|
||||
if (!ipv4Regex.test(ipv4)) {
|
||||
return null;
|
||||
}
|
||||
if (!ipv4Regex.test(ipv4)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const octets = ipv4.split('.');
|
||||
const octets = ipv4.split(".");
|
||||
|
||||
if (!octets.every(octet => {
|
||||
const num = parseInt(octet);
|
||||
return num >= 0 && num <= 255;
|
||||
})) {
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
!octets.every((octet) => {
|
||||
const num = parseInt(octet);
|
||||
return num >= 0 && num <= 255;
|
||||
})
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
octets[3] = 'xxx';
|
||||
octets[3] = "xxx";
|
||||
|
||||
return octets.join('.');
|
||||
return octets.join(".");
|
||||
}
|
||||
|
||||
function anonymizeIPv6(ipv6: string): string | null {
|
||||
const ipv6Regex = /^(?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4}$/i;
|
||||
const ipv6Regex = /^(?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4}$/i;
|
||||
|
||||
const normalizedIPv6 = ipv6.toUpperCase()
|
||||
.replace(/([^:]):([^:])/g, '$1:0$2')
|
||||
.replace(/::/, ':0000:');
|
||||
const normalizedIPv6 = ipv6
|
||||
.toUpperCase()
|
||||
.replace(/([^:]):([^:])/g, "$1:0$2")
|
||||
.replace(/::/, ":0000:");
|
||||
|
||||
if (!ipv6Regex.test(normalizedIPv6)) {
|
||||
return null;
|
||||
}
|
||||
if (!ipv6Regex.test(normalizedIPv6)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const segments = normalizedIPv6.split(':');
|
||||
const segments = normalizedIPv6.split(":");
|
||||
|
||||
if (!segments.every(segment => {
|
||||
const hex = parseInt(segment, 16);
|
||||
return hex >= 0 && hex <= 65535;
|
||||
})) {
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
!segments.every((segment) => {
|
||||
const hex = parseInt(segment, 16);
|
||||
return hex >= 0 && hex <= 65535;
|
||||
})
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (let i = 4; i < 8; i++) {
|
||||
segments[i] = 'xxxx';
|
||||
}
|
||||
for (let i = 4; i < 8; i++) {
|
||||
segments[i] = "xxxx";
|
||||
}
|
||||
|
||||
return segments.join(':');
|
||||
return segments.join(":");
|
||||
}
|
||||
|
||||
function anonymizeIP(ip: string): string | null {
|
||||
const ipv4Result = anonymizeIPv4(ip);
|
||||
if (ipv4Result) {
|
||||
return ipv4Result;
|
||||
}
|
||||
const ipv4Result = anonymizeIPv4(ip);
|
||||
if (ipv4Result) {
|
||||
return ipv4Result;
|
||||
}
|
||||
|
||||
const ipv6 = anonymizeIPv6(ip);
|
||||
return ipv6
|
||||
const ipv6 = anonymizeIPv6(ip);
|
||||
return ipv6;
|
||||
}
|
||||
|
||||
|
||||
+11
-13
@@ -1,16 +1,14 @@
|
||||
import WebSocket from 'ws';
|
||||
import { ClientID } from '../core/Schemas';
|
||||
|
||||
import WebSocket from "ws";
|
||||
import { ClientID } from "../core/Schemas";
|
||||
|
||||
export class Client {
|
||||
public lastPing: number;
|
||||
|
||||
public lastPing: number
|
||||
|
||||
constructor(
|
||||
public readonly clientID: ClientID,
|
||||
public readonly persistentID: string,
|
||||
public readonly ip: string | null,
|
||||
public readonly username: string,
|
||||
public readonly ws: WebSocket,
|
||||
) { }
|
||||
}
|
||||
constructor(
|
||||
public readonly clientID: ClientID,
|
||||
public readonly persistentID: string,
|
||||
public readonly ip: string | null,
|
||||
public readonly username: string,
|
||||
public readonly ws: WebSocket,
|
||||
) {}
|
||||
}
|
||||
|
||||
+56
-55
@@ -1,60 +1,61 @@
|
||||
import { SecretManagerServiceClient } from '@google-cloud/secret-manager';
|
||||
import { Client, Events, GatewayIntentBits } from 'discord.js';
|
||||
import { SecretManagerServiceClient } from "@google-cloud/secret-manager";
|
||||
import { Client, Events, GatewayIntentBits } from "discord.js";
|
||||
|
||||
export class DiscordBot {
|
||||
private client: Client;
|
||||
private secretManager: SecretManagerServiceClient;
|
||||
private client: Client;
|
||||
private secretManager: SecretManagerServiceClient;
|
||||
|
||||
constructor() {
|
||||
this.client = new Client({
|
||||
intents: [
|
||||
GatewayIntentBits.Guilds,
|
||||
GatewayIntentBits.GuildMessages,
|
||||
GatewayIntentBits.MessageContent,
|
||||
],
|
||||
});
|
||||
this.secretManager = new SecretManagerServiceClient();
|
||||
this.setupEventHandlers();
|
||||
constructor() {
|
||||
this.client = new Client({
|
||||
intents: [
|
||||
GatewayIntentBits.Guilds,
|
||||
GatewayIntentBits.GuildMessages,
|
||||
GatewayIntentBits.MessageContent,
|
||||
],
|
||||
});
|
||||
this.secretManager = new SecretManagerServiceClient();
|
||||
this.setupEventHandlers();
|
||||
}
|
||||
|
||||
private setupEventHandlers(): void {
|
||||
this.client.once(Events.ClientReady, (c) => {
|
||||
console.log(`Ready! Logged in as ${c.user.tag}`);
|
||||
});
|
||||
|
||||
this.client.on(Events.MessageCreate, async (message) => {
|
||||
if (message.author.bot) return;
|
||||
|
||||
if (message.content === "!ping") {
|
||||
await message.reply("Pong! 🏓");
|
||||
}
|
||||
|
||||
if (message.content === "!hello") {
|
||||
await message.reply(`Hello ${message.author.username}! 👋`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async getToken(): Promise<string | undefined> {
|
||||
const name =
|
||||
"projects/openfrontio/secrets/discord-bot-token/versions/latest";
|
||||
const [version] = await this.secretManager.accessSecretVersion({ name });
|
||||
return version.payload?.data?.toString().trim();
|
||||
}
|
||||
|
||||
public async start(): Promise<void> {
|
||||
try {
|
||||
const token = await this.getToken();
|
||||
if (!token) {
|
||||
throw new Error("Failed to retrieve Discord token");
|
||||
}
|
||||
await this.client.login(token);
|
||||
} catch (error) {
|
||||
console.error("Failed to start bot:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private setupEventHandlers(): void {
|
||||
this.client.once(Events.ClientReady, (c) => {
|
||||
console.log(`Ready! Logged in as ${c.user.tag}`);
|
||||
});
|
||||
|
||||
this.client.on(Events.MessageCreate, async (message) => {
|
||||
if (message.author.bot) return;
|
||||
|
||||
if (message.content === '!ping') {
|
||||
await message.reply('Pong! 🏓');
|
||||
}
|
||||
|
||||
if (message.content === '!hello') {
|
||||
await message.reply(`Hello ${message.author.username}! 👋`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async getToken(): Promise<string | undefined> {
|
||||
const name = 'projects/openfrontio/secrets/discord-bot-token/versions/latest';
|
||||
const [version] = await this.secretManager.accessSecretVersion({ name });
|
||||
return version.payload?.data?.toString().trim();
|
||||
}
|
||||
|
||||
public async start(): Promise<void> {
|
||||
try {
|
||||
const token = await this.getToken();
|
||||
if (!token) {
|
||||
throw new Error('Failed to retrieve Discord token');
|
||||
}
|
||||
await this.client.login(token);
|
||||
} catch (error) {
|
||||
console.error('Failed to start bot:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public stop(): void {
|
||||
this.client.destroy();
|
||||
}
|
||||
}
|
||||
public stop(): void {
|
||||
this.client.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
+85
-90
@@ -1,104 +1,99 @@
|
||||
import { Config, ServerConfig } from "../core/configuration/Config";
|
||||
import { ClientID, GameConfig, GameID } from "../core/Schemas";
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { Client } from "./Client";
|
||||
import { GamePhase, GameServer } from "./GameServer";
|
||||
import { Difficulty, GameMapType, GameType } from "../core/game/Game";
|
||||
import { generateID } from "../core/Util";
|
||||
|
||||
|
||||
|
||||
export class GameManager {
|
||||
private lastNewLobby: number = 0;
|
||||
|
||||
private lastNewLobby: number = 0
|
||||
private games: GameServer[] = [];
|
||||
|
||||
private games: GameServer[] = []
|
||||
constructor(private config: ServerConfig) {}
|
||||
|
||||
constructor(private config: ServerConfig) { }
|
||||
public game(id: GameID): GameServer | null {
|
||||
return this.games.find((g) => g.id == id);
|
||||
}
|
||||
|
||||
public game(id: GameID): GameServer | null {
|
||||
return this.games.find(g => g.id == id)
|
||||
gamesByPhase(phase: GamePhase): GameServer[] {
|
||||
return this.games.filter((g) => g.phase() == phase);
|
||||
}
|
||||
|
||||
addClient(client: Client, gameID: GameID, lastTurn: number) {
|
||||
const game = this.games.find((g) => g.id == gameID);
|
||||
if (!game) {
|
||||
console.log(`game id ${gameID} not found`);
|
||||
return;
|
||||
}
|
||||
game.addClient(client, lastTurn);
|
||||
}
|
||||
|
||||
updateGameConfig(gameID: GameID, gameConfig: GameConfig) {
|
||||
const game = this.games.find((g) => g.id == gameID);
|
||||
if (game == null) {
|
||||
console.warn(`game ${gameID} not found`);
|
||||
return;
|
||||
}
|
||||
game.updateGameConfig(gameConfig);
|
||||
}
|
||||
|
||||
createPrivateGame(): string {
|
||||
const id = generateID();
|
||||
this.games.push(
|
||||
new GameServer(id, Date.now(), false, this.config, {
|
||||
gameMap: GameMapType.World,
|
||||
gameType: GameType.Private,
|
||||
difficulty: Difficulty.Medium,
|
||||
}),
|
||||
);
|
||||
return id;
|
||||
}
|
||||
|
||||
hasActiveGame(gameID: GameID): boolean {
|
||||
const game = this.games
|
||||
.filter(
|
||||
(g) => g.phase() == GamePhase.Lobby || g.phase() == GamePhase.Active,
|
||||
)
|
||||
.find((g) => g.id == gameID);
|
||||
return game != null;
|
||||
}
|
||||
|
||||
// TODO: stop private games to prevent memory leak.
|
||||
startPrivateGame(gameID: GameID) {
|
||||
const game = this.games.find((g) => g.id == gameID);
|
||||
console.log(`found game ${game}`);
|
||||
if (game) {
|
||||
game.start();
|
||||
} else {
|
||||
throw new Error(`cannot start private game, game ${gameID} not found`);
|
||||
}
|
||||
}
|
||||
|
||||
tick() {
|
||||
const lobbies = this.gamesByPhase(GamePhase.Lobby);
|
||||
const active = this.gamesByPhase(GamePhase.Active);
|
||||
const finished = this.gamesByPhase(GamePhase.Finished);
|
||||
|
||||
const now = Date.now();
|
||||
if (now > this.lastNewLobby + this.config.gameCreationRate()) {
|
||||
this.lastNewLobby = now;
|
||||
lobbies.push(
|
||||
new GameServer(generateID(), now, true, this.config, {
|
||||
gameMap: GameMapType.World,
|
||||
gameType: GameType.Public,
|
||||
difficulty: Difficulty.Medium,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
gamesByPhase(phase: GamePhase): GameServer[] {
|
||||
return this.games.filter(g => g.phase() == phase)
|
||||
}
|
||||
|
||||
addClient(client: Client, gameID: GameID, lastTurn: number) {
|
||||
const game = this.games.find(g => g.id == gameID)
|
||||
if (!game) {
|
||||
console.log(`game id ${gameID} not found`)
|
||||
return
|
||||
}
|
||||
game.addClient(client, lastTurn)
|
||||
}
|
||||
|
||||
updateGameConfig(gameID: GameID, gameConfig: GameConfig) {
|
||||
const game = this.games.find(g => g.id == gameID)
|
||||
if (game == null) {
|
||||
console.warn(`game ${gameID} not found`)
|
||||
return
|
||||
}
|
||||
game.updateGameConfig(gameConfig)
|
||||
}
|
||||
|
||||
createPrivateGame(): string {
|
||||
const id = generateID()
|
||||
this.games.push(new GameServer(
|
||||
id,
|
||||
Date.now(),
|
||||
false,
|
||||
this.config,
|
||||
{
|
||||
gameMap: GameMapType.World,
|
||||
gameType: GameType.Private,
|
||||
difficulty: Difficulty.Medium
|
||||
}
|
||||
))
|
||||
return id
|
||||
}
|
||||
|
||||
hasActiveGame(gameID: GameID): boolean {
|
||||
const game = this.games.filter(g => g.phase() == GamePhase.Lobby || g.phase() == GamePhase.Active).find(g => g.id == gameID)
|
||||
return game != null
|
||||
}
|
||||
|
||||
// TODO: stop private games to prevent memory leak.
|
||||
startPrivateGame(gameID: GameID) {
|
||||
const game = this.games.find(g => g.id == gameID)
|
||||
console.log(`found game ${game}`)
|
||||
if (game) {
|
||||
game.start()
|
||||
} else {
|
||||
throw new Error(`cannot start private game, game ${gameID} not found`)
|
||||
}
|
||||
}
|
||||
|
||||
tick() {
|
||||
const lobbies = this.gamesByPhase(GamePhase.Lobby)
|
||||
const active = this.gamesByPhase(GamePhase.Active)
|
||||
const finished = this.gamesByPhase(GamePhase.Finished)
|
||||
|
||||
const now = Date.now()
|
||||
if (now > this.lastNewLobby + this.config.gameCreationRate()) {
|
||||
this.lastNewLobby = now
|
||||
lobbies.push(new GameServer(
|
||||
generateID(),
|
||||
now,
|
||||
true,
|
||||
this.config,
|
||||
{
|
||||
gameMap: GameMapType.World,
|
||||
gameType: GameType.Public,
|
||||
difficulty: Difficulty.Medium
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
active.filter(g => !g.hasStarted() && g.isPublic).forEach(g => {
|
||||
g.start()
|
||||
})
|
||||
finished.map(g => g.endGame()); // Fire and forget
|
||||
this.games = [...lobbies, ...active]
|
||||
}
|
||||
}
|
||||
active
|
||||
.filter((g) => !g.hasStarted() && g.isPublic)
|
||||
.forEach((g) => {
|
||||
g.start();
|
||||
});
|
||||
finished.map((g) => g.endGame()); // Fire and forget
|
||||
this.games = [...lobbies, ...active];
|
||||
}
|
||||
}
|
||||
|
||||
+270
-236
@@ -1,280 +1,314 @@
|
||||
import { ClientID, ClientMessage, ClientMessageSchema, GameConfig, GameRecordSchema, Intent, PlayerRecord, ServerPingMessageSchema, ServerStartGameMessage, ServerStartGameMessageSchema, ServerTurnMessageSchema, Turn } from "../core/Schemas";
|
||||
import {
|
||||
ClientID,
|
||||
ClientMessage,
|
||||
ClientMessageSchema,
|
||||
GameConfig,
|
||||
GameRecordSchema,
|
||||
Intent,
|
||||
PlayerRecord,
|
||||
ServerPingMessageSchema,
|
||||
ServerStartGameMessage,
|
||||
ServerStartGameMessageSchema,
|
||||
ServerTurnMessageSchema,
|
||||
Turn,
|
||||
} from "../core/Schemas";
|
||||
import { Config, ServerConfig } from "../core/configuration/Config";
|
||||
import { Client } from "./Client";
|
||||
import WebSocket from 'ws';
|
||||
import WebSocket from "ws";
|
||||
import { slog } from "./StructuredLog";
|
||||
import { CreateGameRecord } from "../core/Util";
|
||||
import { archive } from "./Archive";
|
||||
|
||||
|
||||
export enum GamePhase {
|
||||
Lobby = 'LOBBY',
|
||||
Active = 'ACTIVE',
|
||||
Finished = 'FINISHED'
|
||||
Lobby = "LOBBY",
|
||||
Active = "ACTIVE",
|
||||
Finished = "FINISHED",
|
||||
}
|
||||
|
||||
export class GameServer {
|
||||
private maxGameDuration = 5 * 60 * 60 * 1000; // 5 hours
|
||||
|
||||
private turns: Turn[] = [];
|
||||
private intents: Intent[] = [];
|
||||
public activeClients: Client[] = [];
|
||||
// Used for record record keeping
|
||||
private allClients: Map<ClientID, Client> = new Map();
|
||||
private _hasStarted = false;
|
||||
private _startTime: number = null;
|
||||
|
||||
private maxGameDuration = 5 * 60 * 60 * 1000 // 5 hours
|
||||
private endTurnIntervalID;
|
||||
|
||||
private turns: Turn[] = []
|
||||
private intents: Intent[] = []
|
||||
public activeClients: Client[] = []
|
||||
// Used for record record keeping
|
||||
private allClients: Map<ClientID, Client> = new Map()
|
||||
private _hasStarted = false
|
||||
private _startTime: number = null
|
||||
private lastPingUpdate = 0;
|
||||
|
||||
private endTurnIntervalID
|
||||
private winner: ClientID | null = null;
|
||||
|
||||
private lastPingUpdate = 0
|
||||
constructor(
|
||||
public readonly id: string,
|
||||
public readonly createdAt: number,
|
||||
public readonly isPublic: boolean,
|
||||
private config: ServerConfig,
|
||||
private gameConfig: GameConfig,
|
||||
) {}
|
||||
|
||||
private winner: ClientID | null = null
|
||||
public updateGameConfig(gameConfig: GameConfig): void {
|
||||
if (gameConfig.gameMap != null) {
|
||||
this.gameConfig.gameMap = gameConfig.gameMap;
|
||||
}
|
||||
if (gameConfig.difficulty != null) {
|
||||
this.gameConfig.difficulty = gameConfig.difficulty;
|
||||
}
|
||||
}
|
||||
|
||||
constructor(
|
||||
public readonly id: string,
|
||||
public readonly createdAt: number,
|
||||
public readonly isPublic: boolean,
|
||||
private config: ServerConfig,
|
||||
private gameConfig: GameConfig,
|
||||
public addClient(client: Client, lastTurn: number) {
|
||||
console.log(`${this.id}: adding client ${client.clientID}`);
|
||||
slog({
|
||||
logKey: "client_joined_game",
|
||||
msg: `client ${client.clientID} (re)joining game ${this.id}`,
|
||||
data: {
|
||||
clientID: client.clientID,
|
||||
clientIP: client.ip,
|
||||
gameID: this.id,
|
||||
isRejoin: lastTurn > 0,
|
||||
},
|
||||
clientID: client.clientID,
|
||||
persistentID: client.persistentID,
|
||||
gameID: this.id,
|
||||
});
|
||||
// Remove stale client if this is a reconnect
|
||||
const existing = this.activeClients.find(
|
||||
(c) => c.clientID == client.clientID,
|
||||
);
|
||||
if (existing != null) {
|
||||
existing.ws.removeAllListeners("message");
|
||||
}
|
||||
this.activeClients = this.activeClients.filter(
|
||||
(c) => c.clientID != client.clientID,
|
||||
);
|
||||
this.activeClients.push(client);
|
||||
client.lastPing = Date.now();
|
||||
|
||||
) { }
|
||||
this.allClients.set(client.clientID, client);
|
||||
|
||||
public updateGameConfig(gameConfig: GameConfig): void {
|
||||
if (gameConfig.gameMap != null) {
|
||||
this.gameConfig.gameMap = gameConfig.gameMap
|
||||
client.ws.on("message", (message: string) => {
|
||||
try {
|
||||
const clientMsg: ClientMessage = ClientMessageSchema.parse(
|
||||
JSON.parse(message),
|
||||
);
|
||||
if (clientMsg.type == "intent") {
|
||||
if (clientMsg.gameID == this.id) {
|
||||
this.addIntent(clientMsg.intent);
|
||||
} else {
|
||||
console.warn(
|
||||
`${this.id}: client ${clientMsg.clientID} sent to wrong game`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (gameConfig.difficulty != null) {
|
||||
this.gameConfig.difficulty = gameConfig.difficulty
|
||||
if (clientMsg.type == "ping") {
|
||||
this.lastPingUpdate = Date.now();
|
||||
client.lastPing = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
public addClient(client: Client, lastTurn: number) {
|
||||
console.log(`${this.id}: adding client ${client.clientID}`)
|
||||
slog({
|
||||
logKey: 'client_joined_game',
|
||||
msg: `client ${client.clientID} (re)joining game ${this.id}`,
|
||||
data: {
|
||||
clientID: client.clientID,
|
||||
clientIP: client.ip,
|
||||
gameID: this.id,
|
||||
isRejoin: lastTurn > 0
|
||||
},
|
||||
clientID: client.clientID,
|
||||
persistentID: client.persistentID,
|
||||
gameID: this.id,
|
||||
})
|
||||
// Remove stale client if this is a reconnect
|
||||
const existing = this.activeClients.find(c => c.clientID == client.clientID)
|
||||
if (existing != null) {
|
||||
existing.ws.removeAllListeners('message')
|
||||
if (clientMsg.type == "winner") {
|
||||
this.winner = clientMsg.winner;
|
||||
}
|
||||
this.activeClients = this.activeClients.filter(c => c.clientID != client.clientID)
|
||||
this.activeClients.push(client)
|
||||
client.lastPing = Date.now()
|
||||
} catch (error) {
|
||||
console.log(
|
||||
`error handline websocket request in game server: ${error}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
client.ws.on("close", () => {
|
||||
console.log(`${this.id}: client ${client.clientID} disconnected`);
|
||||
this.activeClients = this.activeClients.filter(
|
||||
(c) => c.clientID != client.clientID,
|
||||
);
|
||||
});
|
||||
|
||||
this.allClients.set(client.clientID, client)
|
||||
|
||||
client.ws.on('message', (message: string) => {
|
||||
try {
|
||||
const clientMsg: ClientMessage = ClientMessageSchema.parse(JSON.parse(message))
|
||||
if (clientMsg.type == "intent") {
|
||||
if (clientMsg.gameID == this.id) {
|
||||
this.addIntent(clientMsg.intent)
|
||||
} else {
|
||||
console.warn(`${this.id}: client ${clientMsg.clientID} sent to wrong game`)
|
||||
}
|
||||
}
|
||||
if (clientMsg.type == "ping") {
|
||||
this.lastPingUpdate = Date.now()
|
||||
client.lastPing = Date.now()
|
||||
}
|
||||
if (clientMsg.type == "winner") {
|
||||
this.winner = clientMsg.winner
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`error handline websocket request in game server: ${error}`)
|
||||
}
|
||||
})
|
||||
client.ws.on('close', () => {
|
||||
console.log(`${this.id}: client ${client.clientID} disconnected`)
|
||||
this.activeClients = this.activeClients.filter(c => c.clientID != client.clientID)
|
||||
})
|
||||
|
||||
// In case a client joined the game late and missed the start message.
|
||||
if (this._hasStarted) {
|
||||
this.sendStartGameMsg(client.ws, lastTurn)
|
||||
}
|
||||
// In case a client joined the game late and missed the start message.
|
||||
if (this._hasStarted) {
|
||||
this.sendStartGameMsg(client.ws, lastTurn);
|
||||
}
|
||||
}
|
||||
|
||||
public numClients(): number {
|
||||
return this.activeClients.length
|
||||
public numClients(): number {
|
||||
return this.activeClients.length;
|
||||
}
|
||||
|
||||
public startTime(): number {
|
||||
if (this._startTime > 0) {
|
||||
return this._startTime;
|
||||
} else {
|
||||
//game hasn't started yet, only works for public games
|
||||
return this.createdAt + this.config.lobbyLifetime();
|
||||
}
|
||||
}
|
||||
|
||||
public startTime(): number {
|
||||
if (this._startTime > 0) {
|
||||
return this._startTime
|
||||
} else {
|
||||
//game hasn't started yet, only works for public games
|
||||
return this.createdAt + this.config.lobbyLifetime()
|
||||
}
|
||||
}
|
||||
public start() {
|
||||
this._hasStarted = true;
|
||||
this._startTime = Date.now();
|
||||
// Set last ping to start so we don't immediately stop the game
|
||||
// if no client connects/pings.
|
||||
this.lastPingUpdate = Date.now();
|
||||
|
||||
public start() {
|
||||
this._hasStarted = true
|
||||
this._startTime = Date.now()
|
||||
// Set last ping to start so we don't immediately stop the game
|
||||
// if no client connects/pings.
|
||||
this.lastPingUpdate = Date.now()
|
||||
this.endTurnIntervalID = setInterval(
|
||||
() => this.endTurn(),
|
||||
this.config.turnIntervalMs(),
|
||||
);
|
||||
this.activeClients.forEach((c) => {
|
||||
console.log(`${this.id}: sending start message to ${c.clientID}`);
|
||||
this.sendStartGameMsg(c.ws, 0);
|
||||
});
|
||||
}
|
||||
|
||||
this.endTurnIntervalID = setInterval(() => this.endTurn(), this.config.turnIntervalMs());
|
||||
this.activeClients.forEach(c => {
|
||||
console.log(`${this.id}: sending start message to ${c.clientID}`)
|
||||
this.sendStartGameMsg(c.ws, 0)
|
||||
})
|
||||
}
|
||||
private addIntent(intent: Intent) {
|
||||
this.intents.push(intent);
|
||||
}
|
||||
|
||||
private addIntent(intent: Intent) {
|
||||
this.intents.push(intent)
|
||||
}
|
||||
private sendStartGameMsg(ws: WebSocket, lastTurn: number) {
|
||||
ws.send(
|
||||
JSON.stringify(
|
||||
ServerStartGameMessageSchema.parse({
|
||||
type: "start",
|
||||
turns: this.turns.slice(lastTurn),
|
||||
config: this.gameConfig,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private sendStartGameMsg(ws: WebSocket, lastTurn: number) {
|
||||
ws.send(JSON.stringify(ServerStartGameMessageSchema.parse(
|
||||
{
|
||||
type: "start",
|
||||
turns: this.turns.slice(lastTurn),
|
||||
config: this.gameConfig
|
||||
}
|
||||
)))
|
||||
}
|
||||
private endTurn() {
|
||||
const pastTurn: Turn = {
|
||||
turnNumber: this.turns.length,
|
||||
gameID: this.id,
|
||||
intents: this.intents,
|
||||
};
|
||||
this.turns.push(pastTurn);
|
||||
this.intents = [];
|
||||
|
||||
private endTurn() {
|
||||
const pastTurn: Turn = {
|
||||
turnNumber: this.turns.length,
|
||||
gameID: this.id,
|
||||
intents: this.intents
|
||||
}
|
||||
this.turns.push(pastTurn)
|
||||
this.intents = []
|
||||
const msg = JSON.stringify(
|
||||
ServerTurnMessageSchema.parse({
|
||||
type: "turn",
|
||||
turn: pastTurn,
|
||||
}),
|
||||
);
|
||||
this.activeClients.forEach((c) => {
|
||||
c.ws.send(msg);
|
||||
});
|
||||
}
|
||||
|
||||
const msg = JSON.stringify(ServerTurnMessageSchema.parse(
|
||||
{
|
||||
type: "turn",
|
||||
turn: pastTurn
|
||||
}
|
||||
))
|
||||
this.activeClients.forEach(c => {
|
||||
c.ws.send(msg)
|
||||
})
|
||||
}
|
||||
|
||||
async endGame() {
|
||||
// Close all WebSocket connections
|
||||
clearInterval(this.endTurnIntervalID);
|
||||
this.activeClients.forEach(client => {
|
||||
client.ws.removeAllListeners('message'); // TODO: remove this?
|
||||
if (client.ws.readyState === WebSocket.OPEN) {
|
||||
client.ws.close(1000, "game has ended");
|
||||
}
|
||||
});
|
||||
console.log(`${this.id}: ending game ${this.id} with ${this.turns.length} turns`)
|
||||
async endGame() {
|
||||
// Close all WebSocket connections
|
||||
clearInterval(this.endTurnIntervalID);
|
||||
this.activeClients.forEach((client) => {
|
||||
client.ws.removeAllListeners("message"); // TODO: remove this?
|
||||
if (client.ws.readyState === WebSocket.OPEN) {
|
||||
client.ws.close(1000, "game has ended");
|
||||
}
|
||||
});
|
||||
console.log(
|
||||
`${this.id}: ending game ${this.id} with ${this.turns.length} turns`,
|
||||
);
|
||||
try {
|
||||
if (this.allClients.size > 0) {
|
||||
const playerRecords: PlayerRecord[] = Array.from(
|
||||
this.allClients.values(),
|
||||
).map((client) => ({
|
||||
ip: client.ip,
|
||||
clientID: client.clientID,
|
||||
username: client.username,
|
||||
persistentID: client.persistentID,
|
||||
}));
|
||||
archive(
|
||||
CreateGameRecord(
|
||||
this.id,
|
||||
this.gameConfig,
|
||||
playerRecords,
|
||||
this.turns,
|
||||
this._startTime,
|
||||
Date.now(),
|
||||
this.winner,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
console.log(`${this.id}: no clients joined, not archiving game`);
|
||||
}
|
||||
} catch (error) {
|
||||
let errorDetails;
|
||||
if (error instanceof Error) {
|
||||
errorDetails = {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
};
|
||||
} else if (Array.isArray(error)) {
|
||||
errorDetails = error; // Now we'll actually see the array contents
|
||||
} else {
|
||||
try {
|
||||
if (this.allClients.size > 0) {
|
||||
const playerRecords: PlayerRecord[] = Array.from(this.allClients.values()).map(client => ({
|
||||
ip: client.ip,
|
||||
clientID: client.clientID,
|
||||
username: client.username,
|
||||
persistentID: client.persistentID,
|
||||
}));
|
||||
archive(
|
||||
CreateGameRecord(
|
||||
this.id,
|
||||
this.gameConfig,
|
||||
playerRecords,
|
||||
this.turns,
|
||||
this._startTime,
|
||||
Date.now(),
|
||||
this.winner
|
||||
)
|
||||
)
|
||||
} else {
|
||||
console.log(`${this.id}: no clients joined, not archiving game`)
|
||||
}
|
||||
} catch (error) {
|
||||
let errorDetails;
|
||||
if (error instanceof Error) {
|
||||
errorDetails = {
|
||||
message: error.message,
|
||||
stack: error.stack
|
||||
};
|
||||
} else if (Array.isArray(error)) {
|
||||
errorDetails = error; // Now we'll actually see the array contents
|
||||
} else {
|
||||
try {
|
||||
errorDetails = JSON.stringify(error, null, 2);
|
||||
} catch (e) {
|
||||
errorDetails = String(error);
|
||||
}
|
||||
}
|
||||
|
||||
console.error("Error archiving game record details:", {
|
||||
gameId: this.id,
|
||||
errorType: typeof error,
|
||||
error: errorDetails
|
||||
});
|
||||
errorDetails = JSON.stringify(error, null, 2);
|
||||
} catch (e) {
|
||||
errorDetails = String(error);
|
||||
}
|
||||
}
|
||||
|
||||
console.error("Error archiving game record details:", {
|
||||
gameId: this.id,
|
||||
errorType: typeof error,
|
||||
error: errorDetails,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
phase(): GamePhase {
|
||||
const now = Date.now();
|
||||
const alive = [];
|
||||
for (const client of this.activeClients) {
|
||||
if (now - client.lastPing > 60_000) {
|
||||
console.log(
|
||||
`${this.id}: no pings from ${client.clientID}, terminating connection`,
|
||||
);
|
||||
if (client.ws.readyState === WebSocket.OPEN) {
|
||||
client.ws.close(1000, "no heartbeats received, closing connection");
|
||||
}
|
||||
} else {
|
||||
alive.push(client);
|
||||
}
|
||||
}
|
||||
this.activeClients = alive;
|
||||
if (
|
||||
now >
|
||||
this.createdAt + this.config.lobbyLifetime() + this.maxGameDuration
|
||||
) {
|
||||
console.warn(`${this.id}: game past max duration ${this.id}`);
|
||||
return GamePhase.Finished;
|
||||
}
|
||||
|
||||
phase(): GamePhase {
|
||||
const now = Date.now()
|
||||
const alive = []
|
||||
for (const client of this.activeClients) {
|
||||
if (now - client.lastPing > 60_000) {
|
||||
console.log(`${this.id}: no pings from ${client.clientID}, terminating connection`)
|
||||
if (client.ws.readyState === WebSocket.OPEN) {
|
||||
client.ws.close(1000, "no heartbeats received, closing connection");
|
||||
}
|
||||
} else {
|
||||
alive.push(client)
|
||||
}
|
||||
}
|
||||
this.activeClients = alive
|
||||
if (now > this.createdAt + this.config.lobbyLifetime() + this.maxGameDuration) {
|
||||
console.warn(`${this.id}: game past max duration ${this.id}`)
|
||||
return GamePhase.Finished
|
||||
}
|
||||
const noRecentPings = now > this.lastPingUpdate + 20 * 1000;
|
||||
const noActive = this.activeClients.length == 0;
|
||||
|
||||
const noRecentPings = now > this.lastPingUpdate + 20 * 1000
|
||||
const noActive = this.activeClients.length == 0
|
||||
|
||||
if (!this.isPublic) {
|
||||
if (this._hasStarted) {
|
||||
if (noActive && noRecentPings) {
|
||||
console.log(`${this.id}: private game: ${this.id} complete`)
|
||||
return GamePhase.Finished
|
||||
} else {
|
||||
return GamePhase.Active
|
||||
}
|
||||
} else {
|
||||
return GamePhase.Lobby
|
||||
}
|
||||
if (!this.isPublic) {
|
||||
if (this._hasStarted) {
|
||||
if (noActive && noRecentPings) {
|
||||
console.log(`${this.id}: private game: ${this.id} complete`);
|
||||
return GamePhase.Finished;
|
||||
} else {
|
||||
return GamePhase.Active;
|
||||
}
|
||||
|
||||
if (now - this.createdAt < this.config.lobbyLifetime()) {
|
||||
return GamePhase.Lobby
|
||||
}
|
||||
const warmupOver = now > this.createdAt + this.config.lobbyLifetime() + 30 * 1000
|
||||
if (noActive && warmupOver && noRecentPings) {
|
||||
return GamePhase.Finished
|
||||
}
|
||||
|
||||
return GamePhase.Active
|
||||
} else {
|
||||
return GamePhase.Lobby;
|
||||
}
|
||||
}
|
||||
|
||||
hasStarted(): boolean {
|
||||
return this._hasStarted
|
||||
if (now - this.createdAt < this.config.lobbyLifetime()) {
|
||||
return GamePhase.Lobby;
|
||||
}
|
||||
const warmupOver =
|
||||
now > this.createdAt + this.config.lobbyLifetime() + 30 * 1000;
|
||||
if (noActive && warmupOver && noRecentPings) {
|
||||
return GamePhase.Finished;
|
||||
}
|
||||
|
||||
return GamePhase.Active;
|
||||
}
|
||||
|
||||
}
|
||||
hasStarted(): boolean {
|
||||
return this._hasStarted;
|
||||
}
|
||||
}
|
||||
|
||||
+169
-150
@@ -1,17 +1,26 @@
|
||||
import express, { json } from 'express';
|
||||
import http from 'http';
|
||||
import { WebSocketServer } from 'ws';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { GameManager } from './GameManager';
|
||||
import { ClientMessage, ClientMessageSchema, GameRecord, GameRecordSchema, LogSeverity } from '../core/Schemas';
|
||||
import { getConfig, getServerConfig } from '../core/configuration/Config';
|
||||
import { slog } from './StructuredLog';
|
||||
import { Client } from './Client';
|
||||
import { GamePhase, GameServer } from './GameServer';
|
||||
import { archive } from './Archive';
|
||||
import { DiscordBot } from './DiscordBot';
|
||||
import { sanitizeUsername, validateUsername } from "../core/validations/username";
|
||||
import express, { json } from "express";
|
||||
import http from "http";
|
||||
import { WebSocketServer } from "ws";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { GameManager } from "./GameManager";
|
||||
import {
|
||||
ClientMessage,
|
||||
ClientMessageSchema,
|
||||
GameRecord,
|
||||
GameRecordSchema,
|
||||
LogSeverity,
|
||||
} from "../core/Schemas";
|
||||
import { getConfig, getServerConfig } from "../core/configuration/Config";
|
||||
import { slog } from "./StructuredLog";
|
||||
import { Client } from "./Client";
|
||||
import { GamePhase, GameServer } from "./GameServer";
|
||||
import { archive } from "./Archive";
|
||||
import { DiscordBot } from "./DiscordBot";
|
||||
import {
|
||||
sanitizeUsername,
|
||||
validateUsername,
|
||||
} from "../core/validations/username";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
@@ -21,170 +30,180 @@ const server = http.createServer(app);
|
||||
const wss = new WebSocketServer({ server });
|
||||
|
||||
// Serve static files from the 'out' directory
|
||||
app.use(express.static(path.join(__dirname, '../../out')));
|
||||
app.use(express.json())
|
||||
app.use(express.static(path.join(__dirname, "../../out")));
|
||||
app.use(express.json());
|
||||
|
||||
const gm = new GameManager(getServerConfig())
|
||||
const gm = new GameManager(getServerConfig());
|
||||
|
||||
const bot = new DiscordBot();
|
||||
try {
|
||||
await bot.start();
|
||||
await bot.start();
|
||||
} catch (error) {
|
||||
console.error('Failed to start bot:', error);
|
||||
console.error("Failed to start bot:", error);
|
||||
}
|
||||
|
||||
// New GET endpoint to list lobbies
|
||||
app.get('/lobbies', (req, res) => {
|
||||
const now = Date.now()
|
||||
res.json({
|
||||
lobbies: gm.gamesByPhase(GamePhase.Lobby)
|
||||
.filter(g => g.isPublic)
|
||||
.map(g => ({ id: g.id, msUntilStart: g.startTime() - now, numClients: g.numClients() }))
|
||||
.sort((a, b) => a.msUntilStart - b.msUntilStart),
|
||||
});
|
||||
app.get("/lobbies", (req, res) => {
|
||||
const now = Date.now();
|
||||
res.json({
|
||||
lobbies: gm
|
||||
.gamesByPhase(GamePhase.Lobby)
|
||||
.filter((g) => g.isPublic)
|
||||
.map((g) => ({
|
||||
id: g.id,
|
||||
msUntilStart: g.startTime() - now,
|
||||
numClients: g.numClients(),
|
||||
}))
|
||||
.sort((a, b) => a.msUntilStart - b.msUntilStart),
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/private_lobby', (req, res) => {
|
||||
const id = gm.createPrivateGame()
|
||||
console.log('creating private lobby with id ${id}')
|
||||
res.json({
|
||||
id: id
|
||||
});
|
||||
app.post("/private_lobby", (req, res) => {
|
||||
const id = gm.createPrivateGame();
|
||||
console.log("creating private lobby with id ${id}");
|
||||
res.json({
|
||||
id: id,
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/archive_singleplayer_game', (req, res) => {
|
||||
app.post("/archive_singleplayer_game", (req, res) => {
|
||||
try {
|
||||
const gameRecord: GameRecord = req.body;
|
||||
const clientIP = req.ip || req.socket.remoteAddress || "unknown"; // Added this line
|
||||
|
||||
if (!gameRecord) {
|
||||
console.log("game record not found in request");
|
||||
res.status(404).json({ error: "Game record not found" });
|
||||
return;
|
||||
}
|
||||
gameRecord.players.forEach((p) => (p.ip = clientIP));
|
||||
GameRecordSchema.parse(gameRecord);
|
||||
archive(gameRecord);
|
||||
res.json({
|
||||
success: true,
|
||||
});
|
||||
} catch (error) {
|
||||
slog({
|
||||
logKey: "complete_single_player_game_record",
|
||||
msg: `Failed to complete game record: ${error}`,
|
||||
severity: LogSeverity.Error,
|
||||
});
|
||||
res.status(400).json({ error: "Invalid game record format" });
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/start_private_lobby/:id", (req, res) => {
|
||||
console.log(`starting private lobby with id ${req.params.id}`);
|
||||
gm.startPrivateGame(req.params.id);
|
||||
});
|
||||
|
||||
app.put("/private_lobby/:id", (req, res) => {
|
||||
const lobbyID = req.params.id;
|
||||
gm.updateGameConfig(lobbyID, {
|
||||
gameMap: req.body.gameMap,
|
||||
difficulty: req.body.difficulty,
|
||||
});
|
||||
});
|
||||
|
||||
app.get("/lobby/:id/exists", (req, res) => {
|
||||
const lobbyId = req.params.id;
|
||||
console.log(`checking lobby ${lobbyId} exists`);
|
||||
const lobbyExists = gm.hasActiveGame(lobbyId);
|
||||
|
||||
res.json({
|
||||
exists: lobbyExists,
|
||||
});
|
||||
});
|
||||
|
||||
app.get("/lobby/:id", (req, res) => {
|
||||
const game = gm.game(req.params.id);
|
||||
if (game == null) {
|
||||
console.log(`lobby ${req.params.id} not found`);
|
||||
return res.status(404).json({ error: "Game not found" });
|
||||
}
|
||||
res.json({
|
||||
players: game.activeClients.map((c) => ({
|
||||
username: c.username,
|
||||
clientID: c.clientID,
|
||||
})),
|
||||
});
|
||||
});
|
||||
|
||||
app.get("/private_lobby/:id", (req, res) => {
|
||||
res.json({
|
||||
hi: "5",
|
||||
});
|
||||
});
|
||||
|
||||
wss.on("connection", (ws, req) => {
|
||||
ws.on("message", (message: string) => {
|
||||
try {
|
||||
const gameRecord: GameRecord = req.body
|
||||
const clientIP = req.ip || req.socket.remoteAddress || 'unknown'; // Added this line
|
||||
|
||||
|
||||
if (!gameRecord) {
|
||||
console.log('game record not found in request')
|
||||
res.status(404).json({ error: 'Game record not found' });
|
||||
return;
|
||||
const clientMsg: ClientMessage = ClientMessageSchema.parse(
|
||||
JSON.parse(message),
|
||||
);
|
||||
slog({
|
||||
logKey: "websocket_msg",
|
||||
msg: "server received websocket message",
|
||||
data: clientMsg,
|
||||
severity: LogSeverity.Debug,
|
||||
});
|
||||
if (clientMsg.type == "join") {
|
||||
const forwarded = req.headers["x-forwarded-for"];
|
||||
let ip = Array.isArray(forwarded)
|
||||
? forwarded[0] // Get the first IP if it's an array
|
||||
: forwarded || req.socket.remoteAddress;
|
||||
if (Array.isArray(ip)) {
|
||||
ip = ip[0];
|
||||
}
|
||||
gameRecord.players.forEach(p => p.ip = clientIP)
|
||||
GameRecordSchema.parse(gameRecord);
|
||||
archive(gameRecord)
|
||||
res.json({
|
||||
success: true,
|
||||
});
|
||||
} catch (error) {
|
||||
const { isValid, error } = validateUsername(clientMsg.username);
|
||||
if (!isValid) {
|
||||
console.log(
|
||||
`game ${clientMsg.gameID}, client ${clientMsg.clientID} received invalid username, ${error}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
clientMsg.username = sanitizeUsername(clientMsg.username);
|
||||
gm.addClient(
|
||||
new Client(
|
||||
clientMsg.clientID,
|
||||
clientMsg.persistentID,
|
||||
ip,
|
||||
clientMsg.username,
|
||||
ws,
|
||||
),
|
||||
clientMsg.gameID,
|
||||
clientMsg.lastTurn,
|
||||
);
|
||||
}
|
||||
if (clientMsg.type == "log") {
|
||||
slog({
|
||||
logKey: 'complete_single_player_game_record',
|
||||
msg: `Failed to complete game record: ${error}`,
|
||||
severity: LogSeverity.Error,
|
||||
logKey: "client_console_log",
|
||||
msg: clientMsg.log,
|
||||
severity: clientMsg.severity,
|
||||
clientID: clientMsg.clientID,
|
||||
gameID: clientMsg.gameID,
|
||||
persistentID: clientMsg.persistentID,
|
||||
});
|
||||
res.status(400).json({ error: 'Invalid game record format' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`errror handling websocket message: ${error}`);
|
||||
}
|
||||
})
|
||||
|
||||
app.post('/start_private_lobby/:id', (req, res) => {
|
||||
console.log(`starting private lobby with id ${req.params.id}`)
|
||||
gm.startPrivateGame(req.params.id)
|
||||
});
|
||||
|
||||
app.put('/private_lobby/:id', (req, res) => {
|
||||
const lobbyID = req.params.id
|
||||
gm.updateGameConfig(lobbyID, { gameMap: req.body.gameMap, difficulty: req.body.difficulty })
|
||||
});
|
||||
|
||||
app.get('/lobby/:id/exists', (req, res) => {
|
||||
const lobbyId = req.params.id;
|
||||
console.log(`checking lobby ${lobbyId} exists`)
|
||||
const lobbyExists = gm.hasActiveGame(lobbyId);
|
||||
|
||||
res.json({
|
||||
exists: lobbyExists
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/lobby/:id', (req, res) => {
|
||||
const game = gm.game(req.params.id)
|
||||
if (game == null) {
|
||||
console.log(`lobby ${req.params.id} not found`)
|
||||
return res.status(404).json({ error: 'Game not found' });
|
||||
}
|
||||
res.json({
|
||||
players: game.activeClients.map(c => ({
|
||||
username: c.username,
|
||||
clientID: c.clientID
|
||||
}))
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
app.get('/private_lobby/:id', (req, res) => {
|
||||
res.json({
|
||||
hi: '5'
|
||||
});
|
||||
});
|
||||
|
||||
wss.on('connection', (ws, req) => {
|
||||
ws.on('message', (message: string) => {
|
||||
try {
|
||||
const clientMsg: ClientMessage = ClientMessageSchema.parse(JSON.parse(message))
|
||||
slog({
|
||||
logKey: 'websocket_msg',
|
||||
msg: 'server received websocket message',
|
||||
data: clientMsg,
|
||||
severity: LogSeverity.Debug
|
||||
})
|
||||
if (clientMsg.type == "join") {
|
||||
const forwarded = req.headers['x-forwarded-for']
|
||||
let ip = Array.isArray(forwarded)
|
||||
? forwarded[0] // Get the first IP if it's an array
|
||||
: forwarded || req.socket.remoteAddress;
|
||||
if (Array.isArray(ip)) {
|
||||
ip = ip[0]
|
||||
}
|
||||
const { isValid, error } = validateUsername(clientMsg.username);
|
||||
if (!isValid) {
|
||||
console.log(`game ${clientMsg.gameID}, client ${clientMsg.clientID} received invalid username, ${error}`)
|
||||
return;
|
||||
}
|
||||
clientMsg.username = sanitizeUsername(clientMsg.username)
|
||||
gm.addClient(
|
||||
new Client(
|
||||
clientMsg.clientID,
|
||||
clientMsg.persistentID,
|
||||
ip,
|
||||
clientMsg.username,
|
||||
ws
|
||||
),
|
||||
clientMsg.gameID,
|
||||
clientMsg.lastTurn
|
||||
)
|
||||
}
|
||||
if (clientMsg.type == "log") {
|
||||
slog({
|
||||
logKey: "client_console_log",
|
||||
msg: clientMsg.log,
|
||||
severity: clientMsg.severity,
|
||||
clientID: clientMsg.clientID,
|
||||
gameID: clientMsg.gameID,
|
||||
persistentID: clientMsg.persistentID,
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`errror handling websocket message: ${error}`)
|
||||
}
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
function runGame() {
|
||||
setInterval(() => tick(), 1000);
|
||||
setInterval(() => tick(), 1000);
|
||||
}
|
||||
|
||||
function tick() {
|
||||
gm.tick()
|
||||
gm.tick();
|
||||
}
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
console.log(`Server will try to run on http://localhost:${PORT}`);
|
||||
|
||||
server.listen(PORT, () => {
|
||||
console.log(`Server is running on http://localhost:${PORT}`);
|
||||
console.log(`Server is running on http://localhost:${PORT}`);
|
||||
});
|
||||
|
||||
runGame()
|
||||
runGame();
|
||||
|
||||
+25
-25
@@ -1,33 +1,33 @@
|
||||
import { ClientID, GameID, LogSeverity } from "../core/Schemas";
|
||||
|
||||
export interface slogMsg {
|
||||
logKey: string,
|
||||
msg: string,
|
||||
data?: any
|
||||
severity?: LogSeverity
|
||||
gameID?: GameID
|
||||
clientID?: ClientID
|
||||
persistentID?: string
|
||||
logKey: string;
|
||||
msg: string;
|
||||
data?: any;
|
||||
severity?: LogSeverity;
|
||||
gameID?: GameID;
|
||||
clientID?: ClientID;
|
||||
persistentID?: string;
|
||||
}
|
||||
|
||||
export function slog(msg: slogMsg): void {
|
||||
msg.severity = msg.severity ?? LogSeverity.Info;
|
||||
msg.severity = msg.severity ?? LogSeverity.Info;
|
||||
|
||||
if (process.env.GAME_ENV == 'dev') {
|
||||
// Avoid blowing up the log during development.
|
||||
if (msg.logKey == 'client_console_log') {
|
||||
return
|
||||
}
|
||||
if (msg.severity != LogSeverity.Debug) {
|
||||
console.log(msg.msg)
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
console.log(JSON.stringify(msg));
|
||||
} catch (error) {
|
||||
console.error('Failed to stringify log message:', error);
|
||||
// Fallback to basic logging
|
||||
console.log(`${msg.severity}: ${msg.msg}`);
|
||||
}
|
||||
if (process.env.GAME_ENV == "dev") {
|
||||
// Avoid blowing up the log during development.
|
||||
if (msg.logKey == "client_console_log") {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (msg.severity != LogSeverity.Debug) {
|
||||
console.log(msg.msg);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
console.log(JSON.stringify(msg));
|
||||
} catch (error) {
|
||||
console.error("Failed to stringify log message:", error);
|
||||
// Fallback to basic logging
|
||||
console.log(`${msg.severity}: ${msg.msg}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user