mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-19 13:55:08 +00:00
Merge branch 'main' into ui/modal-overlay-improvements
This commit is contained in:
@@ -48,6 +48,7 @@ import { GameView, PlayerView } from "../core/game/GameView";
|
||||
import { GameUpdateViewData } from "../core/game/GameUpdates";
|
||||
|
||||
export interface LobbyConfig {
|
||||
flag: () => string;
|
||||
playerName: () => string;
|
||||
clientID: ClientID;
|
||||
playerID: PlayerID;
|
||||
@@ -56,6 +57,9 @@ export interface LobbyConfig {
|
||||
gameID: GameID;
|
||||
map: GameMapType | null;
|
||||
difficulty: Difficulty | null;
|
||||
disableBots: boolean | null;
|
||||
disableNPCs: boolean | null;
|
||||
creativeMode: boolean | null;
|
||||
}
|
||||
|
||||
export function joinLobby(
|
||||
@@ -77,6 +81,9 @@ export function joinLobby(
|
||||
gameType: GameType.Singleplayer,
|
||||
gameMap: lobbyConfig.map,
|
||||
difficulty: lobbyConfig.difficulty,
|
||||
disableBots: lobbyConfig.disableBots,
|
||||
disableNPCs: lobbyConfig.disableNPCs,
|
||||
creativeMode: lobbyConfig.creativeMode,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -475,6 +475,24 @@ export class HostLobbyModal extends LitElement {
|
||||
this.putGameConfig();
|
||||
}
|
||||
|
||||
private async handleDisableBotsChange(e: Event) {
|
||||
this.disableBots = Boolean((e.target as HTMLInputElement).checked);
|
||||
consolex.log(`updating disable bots to ${this.disableBots}`);
|
||||
this.putGameConfig();
|
||||
}
|
||||
|
||||
private async handleDisableNPCsChange(e: Event) {
|
||||
this.disableNPCs = Boolean((e.target as HTMLInputElement).checked);
|
||||
consolex.log(`updating disable npcs to ${this.disableNPCs}`);
|
||||
this.putGameConfig();
|
||||
}
|
||||
|
||||
private async handleCreativeModeChange(e: Event) {
|
||||
this.creativeMode = Boolean((e.target as HTMLInputElement).checked);
|
||||
consolex.log(`updating creative mode to ${this.creativeMode}`);
|
||||
this.putGameConfig();
|
||||
}
|
||||
|
||||
private async putGameConfig() {
|
||||
const response = await fetch(`/private_lobby/${this.lobbyId}`, {
|
||||
method: "PUT",
|
||||
|
||||
+92
-32
@@ -2,46 +2,31 @@ 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 {
|
||||
@@ -51,10 +36,7 @@ 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 {
|
||||
@@ -72,10 +54,13 @@ export class InputHandler {
|
||||
|
||||
private alternateView = false;
|
||||
|
||||
constructor(
|
||||
private canvas: HTMLCanvasElement,
|
||||
private eventBus: EventBus,
|
||||
) {}
|
||||
private moveInterval: any = null;
|
||||
private activeKeys = new Set<string>();
|
||||
|
||||
private readonly PAN_SPEED = 5;
|
||||
private readonly ZOOM_SPEED = 10;
|
||||
|
||||
constructor(private canvas: HTMLCanvasElement, private eventBus: EventBus) {}
|
||||
|
||||
initialize() {
|
||||
this.canvas.addEventListener("pointerdown", (e) => this.onPointerDown(e));
|
||||
@@ -95,14 +80,67 @@ export class InputHandler {
|
||||
});
|
||||
this.pointers.clear();
|
||||
|
||||
// Initialize the combined movement interval
|
||||
this.moveInterval = setInterval(() => {
|
||||
let deltaX = 0;
|
||||
let deltaY = 0;
|
||||
|
||||
// Handle both WASD and arrow keys
|
||||
if (this.activeKeys.has("KeyW") || this.activeKeys.has("ArrowUp"))
|
||||
deltaY += this.PAN_SPEED;
|
||||
if (this.activeKeys.has("KeyS") || this.activeKeys.has("ArrowDown"))
|
||||
deltaY -= this.PAN_SPEED;
|
||||
if (this.activeKeys.has("KeyA") || this.activeKeys.has("ArrowLeft"))
|
||||
deltaX += this.PAN_SPEED;
|
||||
if (this.activeKeys.has("KeyD") || this.activeKeys.has("ArrowRight"))
|
||||
deltaX -= this.PAN_SPEED;
|
||||
|
||||
if (deltaX !== 0 || deltaY !== 0) {
|
||||
this.eventBus.emit(new DragEvent(deltaX, deltaY));
|
||||
}
|
||||
|
||||
// Handle zooming
|
||||
const screenCenterX = window.innerWidth / 2;
|
||||
const screenCenterY = window.innerHeight / 2;
|
||||
|
||||
if (this.activeKeys.has("Minus")) {
|
||||
this.eventBus.emit(
|
||||
new ZoomEvent(screenCenterX, screenCenterY, this.ZOOM_SPEED)
|
||||
);
|
||||
}
|
||||
if (this.activeKeys.has("Equal")) {
|
||||
this.eventBus.emit(
|
||||
new ZoomEvent(screenCenterX, screenCenterY, -this.ZOOM_SPEED)
|
||||
);
|
||||
}
|
||||
}, 1);
|
||||
|
||||
window.addEventListener("keydown", (e) => {
|
||||
if (e.code === "Space") {
|
||||
e.preventDefault(); // Prevent page scrolling
|
||||
e.preventDefault();
|
||||
if (!this.alternateView) {
|
||||
this.alternateView = true;
|
||||
this.eventBus.emit(new AlternateViewEvent(true));
|
||||
}
|
||||
}
|
||||
|
||||
// Add all movement keys to activeKeys
|
||||
if (
|
||||
[
|
||||
"KeyW",
|
||||
"KeyA",
|
||||
"KeyS",
|
||||
"KeyD",
|
||||
"ArrowUp",
|
||||
"ArrowLeft",
|
||||
"ArrowDown",
|
||||
"ArrowRight",
|
||||
"Minus",
|
||||
"Equal",
|
||||
].includes(e.code)
|
||||
) {
|
||||
this.activeKeys.add(e.code);
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener("keyup", (e) => {
|
||||
@@ -115,6 +153,24 @@ export class InputHandler {
|
||||
e.preventDefault();
|
||||
this.eventBus.emit(new RefreshGraphicsEvent());
|
||||
}
|
||||
|
||||
// Remove all movement keys from activeKeys
|
||||
if (
|
||||
[
|
||||
"KeyW",
|
||||
"KeyA",
|
||||
"KeyS",
|
||||
"KeyD",
|
||||
"ArrowUp",
|
||||
"ArrowLeft",
|
||||
"ArrowDown",
|
||||
"ArrowRight",
|
||||
"Minus",
|
||||
"Equal",
|
||||
].includes(e.code)
|
||||
) {
|
||||
this.activeKeys.delete(e.code);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -193,10 +249,9 @@ export class InputHandler {
|
||||
const pinchDelta = currentPinchDistance - this.lastPinchDistance;
|
||||
|
||||
if (Math.abs(pinchDelta) > 1) {
|
||||
// Threshold to avoid tiny zoom adjustments
|
||||
const zoomCenter = this.getPinchCenter();
|
||||
this.eventBus.emit(
|
||||
new ZoomEvent(zoomCenter.x, zoomCenter.y, -pinchDelta * 2),
|
||||
new ZoomEvent(zoomCenter.x, zoomCenter.y, -pinchDelta * 2)
|
||||
);
|
||||
this.lastPinchDistance = currentPinchDistance;
|
||||
}
|
||||
@@ -222,4 +277,9 @@ export class InputHandler {
|
||||
y: (pointerEvents[0].clientY + pointerEvents[1].clientY) / 2,
|
||||
};
|
||||
}
|
||||
|
||||
destroy() {
|
||||
clearInterval(this.moveInterval);
|
||||
this.activeKeys.clear();
|
||||
}
|
||||
}
|
||||
|
||||
+150
-127
@@ -1,156 +1,179 @@
|
||||
import { ClientGameRunner, joinLobby } from "./ClientGameRunner";
|
||||
import backgroundImage from "../../resources/images/EuropeBackground.svg";
|
||||
import favicon from "../../resources/images/Favicon.svg";
|
||||
import { ClientGameRunner, joinLobby } from './ClientGameRunner';
|
||||
import backgroundImage from '../../resources/images/EuropeBackground.svg';
|
||||
import favicon from '../../resources/images/Favicon.svg';
|
||||
|
||||
import "./PublicLobby";
|
||||
import "./UsernameInput";
|
||||
import "./styles.css";
|
||||
import { UsernameInput } from "./UsernameInput";
|
||||
import { SinglePlayerModal } from "./SinglePlayerModal";
|
||||
import { HostLobbyModal as HostPrivateLobbyModal } from "./HostLobbyModal";
|
||||
import { JoinPrivateLobbyModal } from "./JoinPrivateLobbyModal";
|
||||
import { generateID } from "../core/Util";
|
||||
import { generateCryptoRandomUUID } from "./Utils";
|
||||
import { consolex } from "../core/Consolex";
|
||||
import './PublicLobby';
|
||||
import './UsernameInput';
|
||||
import './styles.css';
|
||||
import { UsernameInput } from './UsernameInput';
|
||||
import { SinglePlayerModal } from './SinglePlayerModal';
|
||||
import { HostLobbyModal as HostPrivateLobbyModal } from './HostLobbyModal';
|
||||
import { JoinPrivateLobbyModal } from './JoinPrivateLobbyModal';
|
||||
import { generateID } from '../core/Util';
|
||||
import { generateCryptoRandomUUID } from './Utils';
|
||||
import { consolex } from '../core/Consolex';
|
||||
import './components/FlagInput';
|
||||
import { FlagInput } from './components/FlagInput';
|
||||
|
||||
class Client {
|
||||
private gameStop: () => void;
|
||||
private gameStop: () => void;
|
||||
|
||||
private usernameInput: UsernameInput | null = null;
|
||||
private usernameInput: UsernameInput | null = null;
|
||||
private flagInput: FlagInput | 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.flagInput = document.querySelector('flag-input') as FlagInput;
|
||||
if (!this.flagInput) {
|
||||
consolex.warn('Flag 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)
|
||||
);
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
const spModal = document.querySelector(
|
||||
"single-player-modal"
|
||||
) as SinglePlayerModal;
|
||||
spModal instanceof SinglePlayerModal;
|
||||
document.getElementById("single-player").addEventListener("click", () => {
|
||||
if (this.usernameInput.isValid()) {
|
||||
spModal.open();
|
||||
}
|
||||
});
|
||||
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 hostModal = document.querySelector(
|
||||
"host-lobby-modal"
|
||||
) as HostPrivateLobbyModal;
|
||||
hostModal instanceof HostPrivateLobbyModal;
|
||||
document
|
||||
.getElementById("host-lobby-button")
|
||||
.addEventListener("click", () => {
|
||||
if (this.usernameInput.isValid()) {
|
||||
hostModal.open();
|
||||
}
|
||||
});
|
||||
const spModal = document.querySelector(
|
||||
'single-player-modal'
|
||||
) as SinglePlayerModal;
|
||||
spModal instanceof SinglePlayerModal;
|
||||
document
|
||||
.getElementById('single-player')
|
||||
.addEventListener('click', () => {
|
||||
if (this.usernameInput.isValid()) {
|
||||
spModal.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();
|
||||
}
|
||||
});
|
||||
}
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
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()
|
||||
);
|
||||
}
|
||||
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 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,
|
||||
flag: (): string => this.flagInput.getCurrentFlag(),
|
||||
playerName: (): string =>
|
||||
this.usernameInput.getCurrentUsername(),
|
||||
gameID: lobby.id,
|
||||
persistentID: getPersistentIDFromCookie(),
|
||||
playerID: generateID(),
|
||||
clientID: generateID(),
|
||||
map: event.detail.map,
|
||||
difficulty: event.detail.difficulty,
|
||||
disableBots: event.detail.disableBots,
|
||||
disableNPCs: event.detail.disableNPCs,
|
||||
creativeMode: event.detail.creativeMode,
|
||||
},
|
||||
() => this.joinModal.close()
|
||||
);
|
||||
}
|
||||
|
||||
private async 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;
|
||||
}
|
||||
|
||||
@@ -268,6 +268,31 @@ export class SinglePlayerModal extends LitElement {
|
||||
/>
|
||||
<label for="disable-npcs">Disable NPCs</label>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="disable-bots"
|
||||
@change=${this.handleDisableBotsChange}
|
||||
/>
|
||||
<label for="disable-bots">Disable Bots</label>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="disable-npcs"
|
||||
@change=${this.handleDisableNPCsChange}
|
||||
/>
|
||||
<label for="disable-npcs">Disable NPCs</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="creative-mode"
|
||||
@change=${this.handleCreativeModeChange}
|
||||
/>
|
||||
<label for="creative-mode">Creative mode</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input
|
||||
@@ -308,6 +333,15 @@ export class SinglePlayerModal extends LitElement {
|
||||
private handleCreativeModeChange(e: Event) {
|
||||
this.creativeMode = Boolean((e.target as HTMLInputElement).checked);
|
||||
}
|
||||
private handleDisableBotsChange(e: Event) {
|
||||
this.disableBots = Boolean((e.target as HTMLInputElement).checked);
|
||||
}
|
||||
private handleDisableNPCsChange(e: Event) {
|
||||
this.disableNPCs = Boolean((e.target as HTMLInputElement).checked);
|
||||
}
|
||||
private handleCreativeModeChange(e: Event) {
|
||||
this.creativeMode = Boolean((e.target as HTMLInputElement).checked);
|
||||
}
|
||||
private startGame() {
|
||||
consolex.log(
|
||||
`Starting single player game with map: ${GameMapType[this.selectedMap]}`
|
||||
|
||||
@@ -340,6 +340,7 @@ export class Transport {
|
||||
type: "spawn",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
playerID: this.lobbyConfig.playerID,
|
||||
flag: this.lobbyConfig.flag(),
|
||||
name: this.lobbyConfig.playerName(),
|
||||
playerType: PlayerType.Human,
|
||||
x: event.cell.x,
|
||||
|
||||
+3
-2
@@ -3,6 +3,7 @@ export function renderTroops(troops: number): string {
|
||||
}
|
||||
|
||||
export function renderNumber(num: number) {
|
||||
num = Math.max(num, 0);
|
||||
let numStr = "";
|
||||
if (num >= 10_000_000) {
|
||||
numStr = (num / 1000000).toFixed(1) + "M";
|
||||
@@ -51,7 +52,7 @@ export function generateCryptoRandomUUID(): string {
|
||||
(
|
||||
c ^
|
||||
(crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))
|
||||
).toString(16),
|
||||
).toString(16)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -63,6 +64,6 @@ export function generateCryptoRandomUUID(): string {
|
||||
const r: number = (Math.random() * 16) | 0;
|
||||
const v: number = c === "x" ? r : (r & 0x3) | 0x8;
|
||||
return v.toString(16);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
import { LitElement, html, css } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import Countries from "../data/countries.json";
|
||||
|
||||
const flagKey: string = "flag";
|
||||
|
||||
@customElement("flag-input")
|
||||
export class FlagInput extends LitElement {
|
||||
@state() private flag: string = "";
|
||||
@state() private search: string = "";
|
||||
@state() private showModal: boolean = false;
|
||||
|
||||
static styles = css`
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.flag-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.no-selected-flag {
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
top: 8px;
|
||||
height: 50px;
|
||||
border-radius: 0.75rem;
|
||||
border: none;
|
||||
background: none;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.selected-flag {
|
||||
width: 48px;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
left: 24px;
|
||||
top: 14px;
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
.flag-modal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
position: absolute;
|
||||
top: 60px;
|
||||
left: 0;
|
||||
width: 560px;
|
||||
height: 500px;
|
||||
background-color: rgb(35 35 35 / 0.8);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
backdrop-filter: blur(12px);
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.flag-search {
|
||||
height: 2rem;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
text-align: center;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.flag-dropdown {
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
opacity: 0.7;
|
||||
width: calc(100% / 4 - 15px);
|
||||
text-align: center;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.dropdown-item:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.country-flag {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.flag-modal {
|
||||
left: 0px;
|
||||
width: calc(100% - 16px);
|
||||
height: 50vh;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
width: calc(100% / 3 - 15px);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
private handleSearch(e: Event) {
|
||||
this.search = String((e.target as HTMLInputElement).value);
|
||||
}
|
||||
|
||||
private setFlag(flag: string) {
|
||||
this.flag = flag;
|
||||
this.showModal = false;
|
||||
this.storeFlag(flag);
|
||||
}
|
||||
|
||||
public getCurrentFlag(): string {
|
||||
return this.flag;
|
||||
}
|
||||
|
||||
private getStoredFlag(): string {
|
||||
const storedFlag = localStorage.getItem(flagKey);
|
||||
if (storedFlag) {
|
||||
return storedFlag;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private storeFlag(flag: string) {
|
||||
if (flag) {
|
||||
localStorage.setItem(flagKey, flag);
|
||||
} else if (flag === "") {
|
||||
localStorage.removeItem(flagKey);
|
||||
}
|
||||
}
|
||||
|
||||
private dispatchFlagEvent() {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("flag-change", {
|
||||
detail: { flag: this.flag },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.flag = this.getStoredFlag();
|
||||
this.dispatchFlagEvent();
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="flag-container">
|
||||
${this.flag === ""
|
||||
? html` <button
|
||||
class="no-selected-flag"
|
||||
@click=${() => (this.showModal = true)}
|
||||
>
|
||||
Flags
|
||||
</button>`
|
||||
: html`<img
|
||||
class="selected-flag"
|
||||
src="flags/${this.flag}.svg"
|
||||
@click=${() => (this.showModal = true)}
|
||||
/>`}
|
||||
${this.showModal
|
||||
? html`
|
||||
<div
|
||||
class="flag-modal ${this.showModal
|
||||
? ""
|
||||
: "hidden"}"
|
||||
>
|
||||
<input
|
||||
class="flag-search"
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
@change=${this.handleSearch}
|
||||
@keyup=${this.handleSearch}
|
||||
/>
|
||||
<div class="flag-dropdown">
|
||||
<!-- Show each flag as button -->
|
||||
<button
|
||||
@click=${() => this.setFlag("")}
|
||||
class="dropdown-item"
|
||||
>
|
||||
<img
|
||||
class="country-flag"
|
||||
src="flags/none.svg"
|
||||
/>
|
||||
<span class="country-name">None</span>
|
||||
</button>
|
||||
${Countries.filter(
|
||||
(country) =>
|
||||
country.name
|
||||
.toLowerCase()
|
||||
.includes(
|
||||
this.search.toLowerCase()
|
||||
) ||
|
||||
country.code
|
||||
.toLowerCase()
|
||||
.includes(
|
||||
this.search.toLowerCase()
|
||||
)
|
||||
).map(
|
||||
(country) => html`
|
||||
<button
|
||||
@click=${() =>
|
||||
this.setFlag(country.code)}
|
||||
class="dropdown-item"
|
||||
>
|
||||
<img
|
||||
class="country-flag"
|
||||
src="flags/${country.code}.svg"
|
||||
/>
|
||||
<span class="country-name"
|
||||
>${country.name}</span
|
||||
>
|
||||
</button>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,974 @@
|
||||
[
|
||||
{
|
||||
"name": "Afghanistan",
|
||||
"code": "af"
|
||||
},
|
||||
{
|
||||
"name": "Åland Islands",
|
||||
"code": "ax"
|
||||
},
|
||||
{
|
||||
"name": "Albania",
|
||||
"code": "al"
|
||||
},
|
||||
{
|
||||
"name": "Algeria",
|
||||
"code": "dz"
|
||||
},
|
||||
{
|
||||
"name": "American Samoa",
|
||||
"code": "as"
|
||||
},
|
||||
{
|
||||
"name": "AndorrA",
|
||||
"code": "ad"
|
||||
},
|
||||
{
|
||||
"name": "Angola",
|
||||
"code": "ao"
|
||||
},
|
||||
{
|
||||
"name": "Anguilla",
|
||||
"code": "ai"
|
||||
},
|
||||
{
|
||||
"name": "Antarctica",
|
||||
"code": "aq"
|
||||
},
|
||||
{
|
||||
"name": "Antigua and Barbuda",
|
||||
"code": "ag"
|
||||
},
|
||||
{
|
||||
"name": "Argentina",
|
||||
"code": "ar"
|
||||
},
|
||||
{
|
||||
"name": "Armenia",
|
||||
"code": "am"
|
||||
},
|
||||
{
|
||||
"name": "Aruba",
|
||||
"code": "aw"
|
||||
},
|
||||
{
|
||||
"name": "Australia",
|
||||
"code": "au"
|
||||
},
|
||||
{
|
||||
"name": "Austria",
|
||||
"code": "at"
|
||||
},
|
||||
{
|
||||
"name": "Azerbaijan",
|
||||
"code": "az"
|
||||
},
|
||||
{
|
||||
"name": "Bahamas",
|
||||
"code": "bs"
|
||||
},
|
||||
{
|
||||
"name": "Bahrain",
|
||||
"code": "bh"
|
||||
},
|
||||
{
|
||||
"name": "Bangladesh",
|
||||
"code": "bd"
|
||||
},
|
||||
{
|
||||
"name": "Barbados",
|
||||
"code": "bb"
|
||||
},
|
||||
{
|
||||
"name": "Belarus",
|
||||
"code": "by"
|
||||
},
|
||||
{
|
||||
"name": "Belgium",
|
||||
"code": "be"
|
||||
},
|
||||
{
|
||||
"name": "Belize",
|
||||
"code": "bz"
|
||||
},
|
||||
{
|
||||
"name": "Benin",
|
||||
"code": "bj"
|
||||
},
|
||||
{
|
||||
"name": "Bermuda",
|
||||
"code": "bm"
|
||||
},
|
||||
{
|
||||
"name": "Bhutan",
|
||||
"code": "bt"
|
||||
},
|
||||
{
|
||||
"name": "Bolivia",
|
||||
"code": "bo"
|
||||
},
|
||||
{
|
||||
"name": "Bosnia and Herzegovina",
|
||||
"code": "ba"
|
||||
},
|
||||
{
|
||||
"name": "Botswana",
|
||||
"code": "bw"
|
||||
},
|
||||
{
|
||||
"name": "Bouvet Island",
|
||||
"code": "bv"
|
||||
},
|
||||
{
|
||||
"name": "Brazil",
|
||||
"code": "br"
|
||||
},
|
||||
{
|
||||
"name": "British Indian Ocean Territory",
|
||||
"code": "io"
|
||||
},
|
||||
{
|
||||
"name": "Brunei Darussalam",
|
||||
"code": "bn"
|
||||
},
|
||||
{
|
||||
"name": "Bulgaria",
|
||||
"code": "bg"
|
||||
},
|
||||
{
|
||||
"name": "Burkina Faso",
|
||||
"code": "bf"
|
||||
},
|
||||
{
|
||||
"name": "Burundi",
|
||||
"code": "bi"
|
||||
},
|
||||
{
|
||||
"name": "Cambodia",
|
||||
"code": "kh"
|
||||
},
|
||||
{
|
||||
"name": "Cameroon",
|
||||
"code": "cm"
|
||||
},
|
||||
{
|
||||
"name": "Canada",
|
||||
"code": "ca"
|
||||
},
|
||||
{
|
||||
"name": "Cape Verde",
|
||||
"code": "cv"
|
||||
},
|
||||
{
|
||||
"name": "Cayman Islands",
|
||||
"code": "ky"
|
||||
},
|
||||
{
|
||||
"name": "Central African Republic",
|
||||
"code": "cf"
|
||||
},
|
||||
{
|
||||
"name": "Chad",
|
||||
"code": "td"
|
||||
},
|
||||
{
|
||||
"name": "Chile",
|
||||
"code": "cl"
|
||||
},
|
||||
{
|
||||
"name": "China",
|
||||
"code": "cn"
|
||||
},
|
||||
{
|
||||
"name": "Christmas Island",
|
||||
"code": "cx"
|
||||
},
|
||||
{
|
||||
"name": "Cocos (Keeling) Islands",
|
||||
"code": "cc"
|
||||
},
|
||||
{
|
||||
"name": "Colombia",
|
||||
"code": "co"
|
||||
},
|
||||
{
|
||||
"name": "Comoros",
|
||||
"code": "km"
|
||||
},
|
||||
{
|
||||
"name": "Congo",
|
||||
"code": "cg"
|
||||
},
|
||||
{
|
||||
"name": "Congo, The Democratic Republic of the",
|
||||
"code": "cd"
|
||||
},
|
||||
{
|
||||
"name": "Cook Islands",
|
||||
"code": "ck"
|
||||
},
|
||||
{
|
||||
"name": "Costa Rica",
|
||||
"code": "cr"
|
||||
},
|
||||
{
|
||||
"name": "Cote D'Ivoire",
|
||||
"code": "ci"
|
||||
},
|
||||
{
|
||||
"name": "Croatia",
|
||||
"code": "hr"
|
||||
},
|
||||
{
|
||||
"name": "Cuba",
|
||||
"code": "cu"
|
||||
},
|
||||
{
|
||||
"name": "Cyprus",
|
||||
"code": "cy"
|
||||
},
|
||||
{
|
||||
"name": "Czech Republic",
|
||||
"code": "cz"
|
||||
},
|
||||
{
|
||||
"name": "Denmark",
|
||||
"code": "dk"
|
||||
},
|
||||
{
|
||||
"name": "Djibouti",
|
||||
"code": "dj"
|
||||
},
|
||||
{
|
||||
"name": "Dominica",
|
||||
"code": "dm"
|
||||
},
|
||||
{
|
||||
"name": "Dominican Republic",
|
||||
"code": "do"
|
||||
},
|
||||
{
|
||||
"name": "Ecuador",
|
||||
"code": "ec"
|
||||
},
|
||||
{
|
||||
"name": "Egypt",
|
||||
"code": "eg"
|
||||
},
|
||||
{
|
||||
"name": "El Salvador",
|
||||
"code": "sv"
|
||||
},
|
||||
{
|
||||
"name": "Equatorial Guinea",
|
||||
"code": "gq"
|
||||
},
|
||||
{
|
||||
"name": "Eritrea",
|
||||
"code": "er"
|
||||
},
|
||||
{
|
||||
"name": "Estonia",
|
||||
"code": "ee"
|
||||
},
|
||||
{
|
||||
"name": "Ethiopia",
|
||||
"code": "et"
|
||||
},
|
||||
{
|
||||
"name": "Falkland Islands (Malvinas)",
|
||||
"code": "fk"
|
||||
},
|
||||
{
|
||||
"name": "Faroe Islands",
|
||||
"code": "fo"
|
||||
},
|
||||
{
|
||||
"name": "Fiji",
|
||||
"code": "fj"
|
||||
},
|
||||
{
|
||||
"name": "Finland",
|
||||
"code": "fi"
|
||||
},
|
||||
{
|
||||
"name": "France",
|
||||
"code": "fr"
|
||||
},
|
||||
{
|
||||
"name": "French Guiana",
|
||||
"code": "gf"
|
||||
},
|
||||
{
|
||||
"name": "French Polynesia",
|
||||
"code": "pf"
|
||||
},
|
||||
{
|
||||
"name": "French Southern Territories",
|
||||
"code": "tf"
|
||||
},
|
||||
{
|
||||
"name": "Gabon",
|
||||
"code": "ga"
|
||||
},
|
||||
{
|
||||
"name": "Gambia",
|
||||
"code": "gm"
|
||||
},
|
||||
{
|
||||
"name": "Georgia",
|
||||
"code": "ge"
|
||||
},
|
||||
{
|
||||
"name": "Germany",
|
||||
"code": "de"
|
||||
},
|
||||
{
|
||||
"name": "Ghana",
|
||||
"code": "gh"
|
||||
},
|
||||
{
|
||||
"name": "Gibraltar",
|
||||
"code": "gi"
|
||||
},
|
||||
{
|
||||
"name": "Greece",
|
||||
"code": "gr"
|
||||
},
|
||||
{
|
||||
"name": "Greenland",
|
||||
"code": "gl"
|
||||
},
|
||||
{
|
||||
"name": "Grenada",
|
||||
"code": "gd"
|
||||
},
|
||||
{
|
||||
"name": "Guadeloupe",
|
||||
"code": "gp"
|
||||
},
|
||||
{
|
||||
"name": "Guam",
|
||||
"code": "gu"
|
||||
},
|
||||
{
|
||||
"name": "Guatemala",
|
||||
"code": "gt"
|
||||
},
|
||||
{
|
||||
"name": "Guernsey",
|
||||
"code": "gg"
|
||||
},
|
||||
{
|
||||
"name": "Guinea",
|
||||
"code": "gn"
|
||||
},
|
||||
{
|
||||
"name": "Guinea-Bissau",
|
||||
"code": "gw"
|
||||
},
|
||||
{
|
||||
"name": "Guyana",
|
||||
"code": "gy"
|
||||
},
|
||||
{
|
||||
"name": "Haiti",
|
||||
"code": "ht"
|
||||
},
|
||||
{
|
||||
"name": "Heard Island and Mcdonald Islands",
|
||||
"code": "hm"
|
||||
},
|
||||
{
|
||||
"name": "Holy See (Vatican City State)",
|
||||
"code": "va"
|
||||
},
|
||||
{
|
||||
"name": "Honduras",
|
||||
"code": "hn"
|
||||
},
|
||||
{
|
||||
"name": "Hong Kong",
|
||||
"code": "hk"
|
||||
},
|
||||
{
|
||||
"name": "Hungary",
|
||||
"code": "hu"
|
||||
},
|
||||
{
|
||||
"name": "Iceland",
|
||||
"code": "is"
|
||||
},
|
||||
{
|
||||
"name": "India",
|
||||
"code": "in"
|
||||
},
|
||||
{
|
||||
"name": "Indonesia",
|
||||
"code": "id"
|
||||
},
|
||||
{
|
||||
"name": "Iran, Islamic Republic Of",
|
||||
"code": "ir"
|
||||
},
|
||||
{
|
||||
"name": "Iraq",
|
||||
"code": "iq"
|
||||
},
|
||||
{
|
||||
"name": "Ireland",
|
||||
"code": "ie"
|
||||
},
|
||||
{
|
||||
"name": "Isle of Man",
|
||||
"code": "im"
|
||||
},
|
||||
{
|
||||
"name": "Israel",
|
||||
"code": "il"
|
||||
},
|
||||
{
|
||||
"name": "Italy",
|
||||
"code": "it"
|
||||
},
|
||||
{
|
||||
"name": "Jamaica",
|
||||
"code": "jm"
|
||||
},
|
||||
{
|
||||
"name": "Japan",
|
||||
"code": "jp"
|
||||
},
|
||||
{
|
||||
"name": "Jersey",
|
||||
"code": "je"
|
||||
},
|
||||
{
|
||||
"name": "Jordan",
|
||||
"code": "jo"
|
||||
},
|
||||
{
|
||||
"name": "Kazakhstan",
|
||||
"code": "kz"
|
||||
},
|
||||
{
|
||||
"name": "Kenya",
|
||||
"code": "ke"
|
||||
},
|
||||
{
|
||||
"name": "Kiribati",
|
||||
"code": "ki"
|
||||
},
|
||||
{
|
||||
"name": "Korea, Democratic People'S Republic of",
|
||||
"code": "kp"
|
||||
},
|
||||
{
|
||||
"name": "Korea, Republic of",
|
||||
"code": "kr"
|
||||
},
|
||||
{
|
||||
"name": "Kuwait",
|
||||
"code": "kw"
|
||||
},
|
||||
{
|
||||
"name": "Kyrgyzstan",
|
||||
"code": "kg"
|
||||
},
|
||||
{
|
||||
"name": "Lao People'S Democratic Republic",
|
||||
"code": "la"
|
||||
},
|
||||
{
|
||||
"name": "Latvia",
|
||||
"code": "lv"
|
||||
},
|
||||
{
|
||||
"name": "Lebanon",
|
||||
"code": "lb"
|
||||
},
|
||||
{
|
||||
"name": "Lesotho",
|
||||
"code": "ls"
|
||||
},
|
||||
{
|
||||
"name": "Liberia",
|
||||
"code": "lr"
|
||||
},
|
||||
{
|
||||
"name": "Libyan Arab Jamahiriya",
|
||||
"code": "ly"
|
||||
},
|
||||
{
|
||||
"name": "Liechtenstein",
|
||||
"code": "li"
|
||||
},
|
||||
{
|
||||
"name": "Lithuania",
|
||||
"code": "lt"
|
||||
},
|
||||
{
|
||||
"name": "Luxembourg",
|
||||
"code": "lu"
|
||||
},
|
||||
{
|
||||
"name": "Macao",
|
||||
"code": "mo"
|
||||
},
|
||||
{
|
||||
"name": "Macedonia, The Former Yugoslav Republic of",
|
||||
"code": "mk"
|
||||
},
|
||||
{
|
||||
"name": "Madagascar",
|
||||
"code": "mg"
|
||||
},
|
||||
{
|
||||
"name": "Malawi",
|
||||
"code": "mw"
|
||||
},
|
||||
{
|
||||
"name": "Malaysia",
|
||||
"code": "my"
|
||||
},
|
||||
{
|
||||
"name": "Maldives",
|
||||
"code": "mv"
|
||||
},
|
||||
{
|
||||
"name": "Mali",
|
||||
"code": "ml"
|
||||
},
|
||||
{
|
||||
"name": "Malta",
|
||||
"code": "mt"
|
||||
},
|
||||
{
|
||||
"name": "Marshall Islands",
|
||||
"code": "mh"
|
||||
},
|
||||
{
|
||||
"name": "Martinique",
|
||||
"code": "mq"
|
||||
},
|
||||
{
|
||||
"name": "Mauritania",
|
||||
"code": "mr"
|
||||
},
|
||||
{
|
||||
"name": "Mauritius",
|
||||
"code": "mu"
|
||||
},
|
||||
{
|
||||
"name": "Mayotte",
|
||||
"code": "yt"
|
||||
},
|
||||
{
|
||||
"name": "Mexico",
|
||||
"code": "mx"
|
||||
},
|
||||
{
|
||||
"name": "Micronesia, Federated States of",
|
||||
"code": "fm"
|
||||
},
|
||||
{
|
||||
"name": "Moldova, Republic of",
|
||||
"code": "md"
|
||||
},
|
||||
{
|
||||
"name": "Monaco",
|
||||
"code": "mc"
|
||||
},
|
||||
{
|
||||
"name": "Mongolia",
|
||||
"code": "mn"
|
||||
},
|
||||
{
|
||||
"name": "Montserrat",
|
||||
"code": "ms"
|
||||
},
|
||||
{
|
||||
"name": "Morocco",
|
||||
"code": "ma"
|
||||
},
|
||||
{
|
||||
"name": "Mozambique",
|
||||
"code": "mz"
|
||||
},
|
||||
{
|
||||
"name": "Myanmar",
|
||||
"code": "mm"
|
||||
},
|
||||
{
|
||||
"name": "Namibia",
|
||||
"code": "na"
|
||||
},
|
||||
{
|
||||
"name": "Nauru",
|
||||
"code": "nr"
|
||||
},
|
||||
{
|
||||
"name": "Nepal",
|
||||
"code": "np"
|
||||
},
|
||||
{
|
||||
"name": "Netherlands",
|
||||
"code": "nl"
|
||||
},
|
||||
{
|
||||
"name": "Netherlands Antilles",
|
||||
"code": "an"
|
||||
},
|
||||
{
|
||||
"name": "New Caledonia",
|
||||
"code": "nc"
|
||||
},
|
||||
{
|
||||
"name": "New Zealand",
|
||||
"code": "nz"
|
||||
},
|
||||
{
|
||||
"name": "Nicaragua",
|
||||
"code": "ni"
|
||||
},
|
||||
{
|
||||
"name": "Niger",
|
||||
"code": "ne"
|
||||
},
|
||||
{
|
||||
"name": "Nigeria",
|
||||
"code": "ng"
|
||||
},
|
||||
{
|
||||
"name": "Niue",
|
||||
"code": "nu"
|
||||
},
|
||||
{
|
||||
"name": "Norfolk Island",
|
||||
"code": "nf"
|
||||
},
|
||||
{
|
||||
"name": "Northern Mariana Islands",
|
||||
"code": "mp"
|
||||
},
|
||||
{
|
||||
"name": "Norway",
|
||||
"code": "no"
|
||||
},
|
||||
{
|
||||
"name": "Oman",
|
||||
"code": "om"
|
||||
},
|
||||
{
|
||||
"name": "Pakistan",
|
||||
"code": "pk"
|
||||
},
|
||||
{
|
||||
"name": "Palau",
|
||||
"code": "pw"
|
||||
},
|
||||
{
|
||||
"name": "Palestinian Territory, Occupied",
|
||||
"code": "ps"
|
||||
},
|
||||
{
|
||||
"name": "Panama",
|
||||
"code": "pa"
|
||||
},
|
||||
{
|
||||
"name": "Papua New Guinea",
|
||||
"code": "pg"
|
||||
},
|
||||
{
|
||||
"name": "Paraguay",
|
||||
"code": "py"
|
||||
},
|
||||
{
|
||||
"name": "Peru",
|
||||
"code": "pe"
|
||||
},
|
||||
{
|
||||
"name": "Philippines",
|
||||
"code": "ph"
|
||||
},
|
||||
{
|
||||
"name": "Pitcairn",
|
||||
"code": "pn"
|
||||
},
|
||||
{
|
||||
"name": "Poland",
|
||||
"code": "pl"
|
||||
},
|
||||
{
|
||||
"name": "Portugal",
|
||||
"code": "pt"
|
||||
},
|
||||
{
|
||||
"name": "Puerto Rico",
|
||||
"code": "pr"
|
||||
},
|
||||
{
|
||||
"name": "Qatar",
|
||||
"code": "qa"
|
||||
},
|
||||
{
|
||||
"name": "Reunion",
|
||||
"code": "re"
|
||||
},
|
||||
{
|
||||
"name": "Romania",
|
||||
"code": "ro"
|
||||
},
|
||||
{
|
||||
"name": "Russian Federation",
|
||||
"code": "ru"
|
||||
},
|
||||
{
|
||||
"name": "RWANDA",
|
||||
"code": "rw"
|
||||
},
|
||||
{
|
||||
"name": "Saint Helena",
|
||||
"code": "sh"
|
||||
},
|
||||
{
|
||||
"name": "Saint Kitts and Nevis",
|
||||
"code": "kn"
|
||||
},
|
||||
{
|
||||
"name": "Saint Lucia",
|
||||
"code": "lc"
|
||||
},
|
||||
{
|
||||
"name": "Saint Pierre and Miquelon",
|
||||
"code": "pm"
|
||||
},
|
||||
{
|
||||
"name": "Saint Vincent and the Grenadines",
|
||||
"code": "vc"
|
||||
},
|
||||
{
|
||||
"name": "Samoa",
|
||||
"code": "ws"
|
||||
},
|
||||
{
|
||||
"name": "San Marino",
|
||||
"code": "sm"
|
||||
},
|
||||
{
|
||||
"name": "Sao Tome and Principe",
|
||||
"code": "st"
|
||||
},
|
||||
{
|
||||
"name": "Saudi Arabia",
|
||||
"code": "sa"
|
||||
},
|
||||
{
|
||||
"name": "Senegal",
|
||||
"code": "sn"
|
||||
},
|
||||
{
|
||||
"name": "Serbia and Montenegro",
|
||||
"code": "cs"
|
||||
},
|
||||
{
|
||||
"name": "Seychelles",
|
||||
"code": "sc"
|
||||
},
|
||||
{
|
||||
"name": "Sierra Leone",
|
||||
"code": "sl"
|
||||
},
|
||||
{
|
||||
"name": "Singapore",
|
||||
"code": "sg"
|
||||
},
|
||||
{
|
||||
"name": "Slovakia",
|
||||
"code": "sk"
|
||||
},
|
||||
{
|
||||
"name": "Slovenia",
|
||||
"code": "si"
|
||||
},
|
||||
{
|
||||
"name": "Solomon Islands",
|
||||
"code": "sb"
|
||||
},
|
||||
{
|
||||
"name": "Somalia",
|
||||
"code": "so"
|
||||
},
|
||||
{
|
||||
"name": "South Africa",
|
||||
"code": "za"
|
||||
},
|
||||
{
|
||||
"name": "South Georgia and the South Sandwich Islands",
|
||||
"code": "gs"
|
||||
},
|
||||
{
|
||||
"name": "Spain",
|
||||
"code": "es"
|
||||
},
|
||||
{
|
||||
"name": "Sri Lanka",
|
||||
"code": "lk"
|
||||
},
|
||||
{
|
||||
"name": "Sudan",
|
||||
"code": "sd"
|
||||
},
|
||||
{
|
||||
"name": "Suriname",
|
||||
"code": "sr"
|
||||
},
|
||||
{
|
||||
"name": "Svalbard and Jan Mayen",
|
||||
"code": "sj"
|
||||
},
|
||||
{
|
||||
"name": "Swaziland",
|
||||
"code": "sz"
|
||||
},
|
||||
{
|
||||
"name": "Sweden",
|
||||
"code": "se"
|
||||
},
|
||||
{
|
||||
"name": "Switzerland",
|
||||
"code": "ch"
|
||||
},
|
||||
{
|
||||
"name": "Syrian Arab Republic",
|
||||
"code": "sy"
|
||||
},
|
||||
{
|
||||
"name": "Taiwan, Province of China",
|
||||
"code": "tw"
|
||||
},
|
||||
{
|
||||
"name": "Tajikistan",
|
||||
"code": "tj"
|
||||
},
|
||||
{
|
||||
"name": "Tanzania, United Republic of",
|
||||
"code": "tz"
|
||||
},
|
||||
{
|
||||
"name": "Thailand",
|
||||
"code": "th"
|
||||
},
|
||||
{
|
||||
"name": "Timor-Leste",
|
||||
"code": "tl"
|
||||
},
|
||||
{
|
||||
"name": "Togo",
|
||||
"code": "tg"
|
||||
},
|
||||
{
|
||||
"name": "Tokelau",
|
||||
"code": "tk"
|
||||
},
|
||||
{
|
||||
"name": "Tonga",
|
||||
"code": "to"
|
||||
},
|
||||
{
|
||||
"name": "Trinidad and Tobago",
|
||||
"code": "tt"
|
||||
},
|
||||
{
|
||||
"name": "Tunisia",
|
||||
"code": "tn"
|
||||
},
|
||||
{
|
||||
"name": "Turkey",
|
||||
"code": "tr"
|
||||
},
|
||||
{
|
||||
"name": "Turkmenistan",
|
||||
"code": "tm"
|
||||
},
|
||||
{
|
||||
"name": "Turks and Caicos Islands",
|
||||
"code": "tc"
|
||||
},
|
||||
{
|
||||
"name": "Tuvalu",
|
||||
"code": "tv"
|
||||
},
|
||||
{
|
||||
"name": "Uganda",
|
||||
"code": "ug"
|
||||
},
|
||||
{
|
||||
"name": "Ukraine",
|
||||
"code": "ua"
|
||||
},
|
||||
{
|
||||
"name": "United Arab Emirates",
|
||||
"code": "ae"
|
||||
},
|
||||
{
|
||||
"name": "United Kingdom",
|
||||
"code": "gb"
|
||||
},
|
||||
{
|
||||
"name": "United States",
|
||||
"code": "us"
|
||||
},
|
||||
{
|
||||
"name": "United States Minor Outlying Islands",
|
||||
"code": "um"
|
||||
},
|
||||
{
|
||||
"name": "Uruguay",
|
||||
"code": "uy"
|
||||
},
|
||||
{
|
||||
"name": "Uzbekistan",
|
||||
"code": "uz"
|
||||
},
|
||||
{
|
||||
"name": "Vanuatu",
|
||||
"code": "vu"
|
||||
},
|
||||
{
|
||||
"name": "Venezuela",
|
||||
"code": "ve"
|
||||
},
|
||||
{
|
||||
"name": "Viet Nam",
|
||||
"code": "vn"
|
||||
},
|
||||
{
|
||||
"name": "Virgin Islands, British",
|
||||
"code": "vg"
|
||||
},
|
||||
{
|
||||
"name": "Virgin Islands, U.S.",
|
||||
"code": "vi"
|
||||
},
|
||||
{
|
||||
"name": "Wallis and Futuna",
|
||||
"code": "wf"
|
||||
},
|
||||
{
|
||||
"name": "Western Sahara",
|
||||
"code": "eh"
|
||||
},
|
||||
{
|
||||
"name": "Yemen",
|
||||
"code": "ye"
|
||||
},
|
||||
{
|
||||
"name": "Zambia",
|
||||
"code": "zm"
|
||||
},
|
||||
{
|
||||
"name": "Zimbabwe",
|
||||
"code": "zw"
|
||||
}
|
||||
]
|
||||
@@ -15,6 +15,7 @@ import warshipIcon 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 mirvIcon from "../../../../resources/images/MIRVIcon.svg";
|
||||
import cityIcon from "../../../../resources/images/CityIconWhite.svg";
|
||||
import shieldIcon from "../../../../resources/images/ShieldIconWhite.svg";
|
||||
import { renderNumber } from "../../Utils";
|
||||
@@ -28,7 +29,8 @@ interface BuildItemDisplay {
|
||||
const buildTable: BuildItemDisplay[][] = [
|
||||
[
|
||||
{ unitType: UnitType.AtomBomb, icon: atomBombIcon },
|
||||
{ unitType: UnitType.MIRV, icon: hydrogenBombIcon },
|
||||
{ unitType: UnitType.MIRV, icon: mirvIcon },
|
||||
{ unitType: UnitType.HydrogenBomb, icon: hydrogenBombIcon },
|
||||
{ unitType: UnitType.Warship, icon: warshipIcon },
|
||||
{ unitType: UnitType.Port, icon: portIcon },
|
||||
{ unitType: UnitType.MissileSilo, icon: missileSiloIcon },
|
||||
|
||||
@@ -195,7 +195,7 @@ export class ControlPanel extends LitElement implements Layer {
|
||||
type="range"
|
||||
min="1"
|
||||
max="100"
|
||||
.value=${this.targetTroopRatio * 100}
|
||||
.value=${(this.targetTroopRatio * 100).toString()}
|
||||
@input=${(e: Event) => {
|
||||
this.targetTroopRatio =
|
||||
parseInt((e.target as HTMLInputElement).value) / 100;
|
||||
@@ -225,7 +225,7 @@ export class ControlPanel extends LitElement implements Layer {
|
||||
type="range"
|
||||
min="1"
|
||||
max="100"
|
||||
.value=${this.attackRatio * 100}
|
||||
.value=${(this.attackRatio * 100).toString()}
|
||||
@input=${(e: Event) => {
|
||||
this.attackRatio =
|
||||
parseInt((e.target as HTMLInputElement).value) / 100;
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
import { AllPlayers, MessageType } from "../../../core/game/Game";
|
||||
import { DisplayMessageUpdate } from "../../../core/game/GameUpdates";
|
||||
import { AllPlayers, MessageType, PlayerType } from "../../../core/game/Game";
|
||||
import {
|
||||
AttackUpdate,
|
||||
DisplayMessageUpdate,
|
||||
} from "../../../core/game/GameUpdates";
|
||||
import { EmojiUpdate } from "../../../core/game/GameUpdates";
|
||||
import { TargetPlayerUpdate } from "../../../core/game/GameUpdates";
|
||||
import { AllianceExpiredUpdate } from "../../../core/game/GameUpdates";
|
||||
@@ -16,6 +19,7 @@ import { SendAllianceReplyIntentEvent } from "../../Transport";
|
||||
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
||||
import { onlyImages, sanitize } from "../../../core/Util";
|
||||
import { GameView, PlayerView } from "../../../core/game/GameView";
|
||||
import { renderTroops } from "../../Utils";
|
||||
|
||||
interface Event {
|
||||
description: string;
|
||||
@@ -38,6 +42,8 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
public clientID: ClientID;
|
||||
|
||||
private events: Event[] = [];
|
||||
@state() private incomingAttacks: AttackUpdate[] = [];
|
||||
@state() private outgoingAttacks: AttackUpdate[] = [];
|
||||
|
||||
private updateMap = new Map([
|
||||
[GameUpdateType.DisplayEvent, (u) => this.onDisplayMessageEvent(u)],
|
||||
@@ -54,6 +60,8 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
constructor() {
|
||||
super();
|
||||
this.events = [];
|
||||
this.incomingAttacks = [];
|
||||
this.outgoingAttacks = [];
|
||||
}
|
||||
|
||||
init() {}
|
||||
@@ -80,6 +88,31 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
this.events = remainingEvents;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
const myPlayer = this.game.myPlayer();
|
||||
if (!myPlayer) {
|
||||
return;
|
||||
}
|
||||
|
||||
myPlayer.incomingAttacks().forEach((a) => {
|
||||
console.log(
|
||||
`got type: ${(
|
||||
this.game.playerBySmallID(a.attackerID) as PlayerView
|
||||
).type()}`
|
||||
);
|
||||
});
|
||||
|
||||
// Update attacks
|
||||
this.incomingAttacks = myPlayer.incomingAttacks().filter((a) => {
|
||||
const t = (this.game.playerBySmallID(a.attackerID) as PlayerView).type();
|
||||
return t != PlayerType.Bot;
|
||||
});
|
||||
|
||||
this.outgoingAttacks = myPlayer
|
||||
.outgoingAttacks()
|
||||
.filter((a) => a.targetID != 0);
|
||||
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private addEvent(event: Event) {
|
||||
@@ -125,10 +158,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 +172,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 +180,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 +189,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 +201,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 +246,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 +283,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) {
|
||||
@@ -289,8 +322,62 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
}
|
||||
}
|
||||
|
||||
private renderAttacks() {
|
||||
if (
|
||||
this.incomingAttacks.length === 0 &&
|
||||
this.outgoingAttacks.length === 0
|
||||
) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
${this.incomingAttacks.length > 0
|
||||
? html`
|
||||
<tr class="border-t border-gray-700">
|
||||
<td class="lg:p-3 p-1 text-left text-red-400">
|
||||
${this.incomingAttacks.map(
|
||||
(attack) => html`
|
||||
<div class="ml-2">
|
||||
${renderTroops(attack.troops)}
|
||||
${(
|
||||
this.game.playerBySmallID(
|
||||
attack.attackerID
|
||||
) as PlayerView
|
||||
)?.name()}
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
`
|
||||
: ""}
|
||||
${this.outgoingAttacks.length > 0
|
||||
? html`
|
||||
<tr class="border-t border-gray-700">
|
||||
<td class="lg:p-3 p-1 text-left text-blue-400">
|
||||
${this.outgoingAttacks.map(
|
||||
(attack) => html`
|
||||
<div class="ml-2">
|
||||
${renderTroops(attack.troops)}
|
||||
${(
|
||||
this.game.playerBySmallID(attack.targetID) as PlayerView
|
||||
)?.name()}
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
`
|
||||
: ""}
|
||||
`;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.events.length === 0) {
|
||||
if (
|
||||
this.events.length === 0 &&
|
||||
this.incomingAttacks.length === 0 &&
|
||||
this.outgoingAttacks.length === 0
|
||||
) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
@@ -306,7 +393,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,15 +418,16 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
>
|
||||
${btn.text}
|
||||
</button>
|
||||
`,
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</td>
|
||||
</tr>
|
||||
`,
|
||||
`
|
||||
)}
|
||||
${this.renderAttacks()}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -347,6 +435,6 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
}
|
||||
|
||||
createRenderRoot() {
|
||||
return this; // Required for Tailwind classes to work with Lit
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import targetIcon from "../../../../resources/images/TargetIcon.svg";
|
||||
import { ClientID } from "../../../core/Schemas";
|
||||
import { GameView, PlayerView } from "../../../core/game/GameView";
|
||||
import { createCanvas, renderTroops } from "../../Utils";
|
||||
import { sanitize } from "../../../core/Util";
|
||||
|
||||
class RenderInfo {
|
||||
public icons: Map<string, HTMLImageElement> = new Map(); // Track icon elements
|
||||
@@ -142,33 +143,50 @@ export class NameLayer implements Layer {
|
||||
element.style.alignItems = "center";
|
||||
element.style.gap = "0px";
|
||||
|
||||
|
||||
if (player.flag()) {
|
||||
const flagImg = document.createElement("img");
|
||||
flagImg.classList.add('player-flag');
|
||||
flagImg.style.marginBottom = "-5%";
|
||||
flagImg.style.opacity = '0.8';
|
||||
flagImg.src = 'flags/' + sanitize(player.flag()) + '.svg';
|
||||
flagImg.style.zIndex = "1";
|
||||
flagImg.style.width = "40%";
|
||||
flagImg.style.aspectRatio = "3/4";
|
||||
element.appendChild(flagImg);
|
||||
}
|
||||
|
||||
const nameDiv = document.createElement("div");
|
||||
nameDiv.innerHTML = player.name();
|
||||
nameDiv.classList.add('player-name');
|
||||
nameDiv.innerHTML = (player.type() !== PlayerType.Human ? "🤖 " : '') + player.name();
|
||||
nameDiv.style.color = this.theme.playerInfoColor(player.id()).toHex();
|
||||
nameDiv.style.fontFamily = this.theme.font();
|
||||
nameDiv.style.whiteSpace = "nowrap";
|
||||
nameDiv.style.overflow = "hidden";
|
||||
nameDiv.style.textOverflow = "ellipsis";
|
||||
nameDiv.style.zIndex = "2";
|
||||
nameDiv.style.zIndex = "3";
|
||||
element.appendChild(nameDiv);
|
||||
|
||||
|
||||
const troopsDiv = document.createElement("div");
|
||||
troopsDiv.classList.add('player-troops');
|
||||
troopsDiv.textContent = renderTroops(player.troops());
|
||||
troopsDiv.style.color = this.theme.playerInfoColor(player.id()).toHex();
|
||||
troopsDiv.style.fontFamily = this.theme.font();
|
||||
troopsDiv.style.fontWeight = "bold";
|
||||
troopsDiv.style.zIndex = "2";
|
||||
troopsDiv.style.zIndex = "3";
|
||||
element.appendChild(troopsDiv);
|
||||
|
||||
const iconsDiv = document.createElement("div");
|
||||
iconsDiv.classList.add('player-icons');
|
||||
iconsDiv.style.display = "flex";
|
||||
iconsDiv.style.gap = "4px";
|
||||
iconsDiv.style.justifyContent = "center";
|
||||
iconsDiv.style.alignItems = "center";
|
||||
iconsDiv.style.position = "absolute"; // Add this
|
||||
iconsDiv.style.zIndex = "1"; // Add this
|
||||
iconsDiv.style.width = "100%"; // Add this
|
||||
iconsDiv.style.height = "100%"; // Add this
|
||||
iconsDiv.style.position = "absolute";
|
||||
iconsDiv.style.zIndex = "2";
|
||||
iconsDiv.style.width = "100%";
|
||||
iconsDiv.style.height = "100%";
|
||||
element.appendChild(iconsDiv);
|
||||
|
||||
this.container.appendChild(element);
|
||||
@@ -208,14 +226,14 @@ export class NameLayer implements Layer {
|
||||
render.lastRenderCalc = now + this.rand.nextInt(0, 100);
|
||||
|
||||
// Update text sizes
|
||||
const nameDiv = render.element.children[0] as HTMLDivElement;
|
||||
const troopsDiv = render.element.children[1] as HTMLDivElement;
|
||||
const nameDiv = render.element.querySelector(".player-name") as HTMLDivElement;
|
||||
const troopsDiv = render.element.querySelector(".player-troops") as HTMLDivElement;
|
||||
nameDiv.style.fontSize = `${render.fontSize}px`;
|
||||
troopsDiv.style.fontSize = `${render.fontSize}px`;
|
||||
troopsDiv.textContent = renderTroops(render.player.troops());
|
||||
|
||||
// Handle icons
|
||||
const iconsDiv = render.element.children[2] as HTMLDivElement;
|
||||
const iconsDiv = render.element.querySelector(".player-icons") as HTMLDivElement;
|
||||
const iconSize = Math.min(render.fontSize * 1.5, 48);
|
||||
const myPlayer = this.getPlayer();
|
||||
|
||||
|
||||
@@ -26,16 +26,19 @@ export class TopBar extends LitElement implements Layer {
|
||||
if (!this.isVisible) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const myPlayer = this.game?.myPlayer();
|
||||
if (!myPlayer?.isAlive() || this.game?.inSpawnPhase()) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const popRate = this.game.config().populationIncreaseRate(myPlayer) * 10;
|
||||
const maxPop = this.game.config().maxPopulation(myPlayer);
|
||||
const goldPerSecond = this.game.config().goldAdditionRate(myPlayer) * 10;
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="fixed top-0 z-50 bg-black/90 text-white text-sm p-1 rounded grid grid-cols-1 sm:grid-cols-2 w-1/2 sm:w-2/3 md:w-1/2 lg:hidden"
|
||||
class="fixed top-0 z-50 bg-gray-800/70 text-white text-sm p-1 rounded grid grid-cols-1 sm:grid-cols-2 w-1/2 sm:w-2/3 md:w-1/2 lg:hidden backdrop-blur"
|
||||
>
|
||||
<!-- Pop section (takes 2 columns on desktop) -->
|
||||
<div
|
||||
@@ -48,7 +51,6 @@ export class TopBar extends LitElement implements Layer {
|
||||
>
|
||||
<span>(+${renderTroops(popRate)})</span>
|
||||
</div>
|
||||
|
||||
<!-- Gold section (takes 1 column on desktop) -->
|
||||
<div
|
||||
class="flex items-center space-x-2 overflow-x-auto whitespace-nowrap"
|
||||
|
||||
@@ -80,7 +80,8 @@
|
||||
v0.15.0
|
||||
</div>
|
||||
|
||||
<div class="max-w-sm sm:max-w-md lg:max-w-lg xl:max-w-xl mx-auto p-2 pb-4">
|
||||
<div class="relative items-center max-w-sm sm:max-w-md lg:max-w-lg xl:max-w-xl mx-auto p-2 pb-4">
|
||||
<flag-input></flag-input>
|
||||
<username-input class="w-full"></username-input>
|
||||
</div>
|
||||
|
||||
@@ -146,8 +147,10 @@
|
||||
<div class="flex sm:flex-row flex-col sm:gap-8 gap-2">
|
||||
<a href="https://youtu.be/jvHEvbko3uw?si=znspkP84P76B1w5I"
|
||||
class="text-white/70 hover:text-white transition-colors duration-300" target="_blank">How to Play</a>
|
||||
<a href="https://discord.gg/k22YrnAzGp" class="text-white/70 hover:text-white transition-colors duration-300"
|
||||
target="_blank">Discord</a>
|
||||
<a href="https://discord.gg/k22YrnAzGp" class="text-white/70 hover:text-white transition-colors duration-300"
|
||||
target="_blank">Discord</a>
|
||||
<a href="https://openfront.fandom.com/wiki/Openfront_Wiki" class="text-white/70 hover:text-white transition-colors duration-300"
|
||||
target="_blank">Wiki</a>
|
||||
</div>
|
||||
<div class="text-white/70">
|
||||
© 2025
|
||||
|
||||
@@ -58,9 +58,11 @@ export class GameRunner {
|
||||
) {}
|
||||
|
||||
init() {
|
||||
this.game.addExecution(
|
||||
...this.execManager.spawnBots(this.game.config().numBots())
|
||||
);
|
||||
if (this.game.config().spawnBots()) {
|
||||
this.game.addExecution(
|
||||
...this.execManager.spawnBots(this.game.config().numBots())
|
||||
);
|
||||
}
|
||||
if (this.game.config().spawnNPCs()) {
|
||||
this.game.addExecution(...this.execManager.fakeHumanExecutions());
|
||||
}
|
||||
|
||||
@@ -92,6 +92,9 @@ const GameConfigSchema = z.object({
|
||||
gameMap: z.nativeEnum(GameMapType),
|
||||
difficulty: z.nativeEnum(Difficulty),
|
||||
gameType: z.nativeEnum(GameType),
|
||||
disableBots: z.boolean(),
|
||||
disableNPCs: z.boolean(),
|
||||
creativeMode: z.boolean(),
|
||||
});
|
||||
|
||||
const SafeString = z
|
||||
@@ -137,6 +140,7 @@ export const AttackIntentSchema = BaseIntentSchema.extend({
|
||||
});
|
||||
|
||||
export const SpawnIntentSchema = BaseIntentSchema.extend({
|
||||
flag: z.string().nullable(),
|
||||
type: z.literal("spawn"),
|
||||
playerID: ID,
|
||||
name: SafeString,
|
||||
|
||||
@@ -70,6 +70,8 @@ export interface Config {
|
||||
percentageTilesOwnedToWin(): number;
|
||||
numBots(): number;
|
||||
spawnNPCs(): boolean;
|
||||
spawnBots(): boolean;
|
||||
creativeMode(): boolean;
|
||||
numSpawnPhaseTurns(): number;
|
||||
|
||||
startManpower(playerInfo: PlayerInfo): number;
|
||||
|
||||
@@ -76,7 +76,13 @@ export class DefaultConfig implements Config {
|
||||
return 5;
|
||||
}
|
||||
spawnNPCs(): boolean {
|
||||
return true;
|
||||
return !this._gameConfig.disableNPCs;
|
||||
}
|
||||
spawnBots(): boolean {
|
||||
return !this._gameConfig.disableBots;
|
||||
}
|
||||
creativeMode(): boolean {
|
||||
return this._gameConfig.creativeMode;
|
||||
}
|
||||
tradeShipGold(dist: number): Gold {
|
||||
return 10000 + 100 * Math.pow(dist, 1.1);
|
||||
@@ -94,7 +100,7 @@ export class DefaultConfig implements Config {
|
||||
};
|
||||
case UnitType.Warship:
|
||||
return {
|
||||
cost: (p: Player) => (p.units(UnitType.Warship).length + 1) * 250_000,
|
||||
cost: (p: Player) => this.creativeMode() ? 0 : (p.units(UnitType.Warship).length + 1) * 250_000,
|
||||
territoryBound: false,
|
||||
maxHealth: 1000,
|
||||
};
|
||||
@@ -107,26 +113,27 @@ export class DefaultConfig implements Config {
|
||||
case UnitType.Port:
|
||||
return {
|
||||
cost: (p: Player) =>
|
||||
this.creativeMode() ? 0 :
|
||||
Math.min(
|
||||
1_000_000,
|
||||
Math.pow(2, p.units(UnitType.Port).length) * 250_000
|
||||
),
|
||||
territoryBound: true,
|
||||
constructionDuration: 2 * 10,
|
||||
constructionDuration: this.creativeMode() ? 0 : 2 * 10,
|
||||
};
|
||||
case UnitType.AtomBomb:
|
||||
return {
|
||||
cost: () => 750_000,
|
||||
cost: () => this.creativeMode() ? 0 : 750_000,
|
||||
territoryBound: false,
|
||||
};
|
||||
case UnitType.HydrogenBomb:
|
||||
return {
|
||||
cost: () => 5_000_000,
|
||||
cost: () => this.creativeMode() ? 0 : 5_000_000,
|
||||
territoryBound: false,
|
||||
};
|
||||
case UnitType.MIRV:
|
||||
return {
|
||||
cost: () => 5_000_000,
|
||||
cost: () => this.creativeMode() ? 0 : 10_000_000,
|
||||
territoryBound: false,
|
||||
};
|
||||
case UnitType.MIRVWarhead:
|
||||
@@ -141,29 +148,31 @@ export class DefaultConfig implements Config {
|
||||
};
|
||||
case UnitType.MissileSilo:
|
||||
return {
|
||||
cost: () => 1_000_000,
|
||||
cost: () => this.creativeMode() ? 0 : 1_000_000,
|
||||
territoryBound: true,
|
||||
constructionDuration: 10 * 10,
|
||||
constructionDuration: this.creativeMode() ? 0 : 10 * 10,
|
||||
};
|
||||
case UnitType.DefensePost:
|
||||
return {
|
||||
cost: (p: Player) =>
|
||||
this.creativeMode() ? 0 :
|
||||
Math.min(
|
||||
250_000,
|
||||
(p.units(UnitType.DefensePost).length + 1) * 50_000
|
||||
),
|
||||
territoryBound: true,
|
||||
constructionDuration: 5 * 10,
|
||||
constructionDuration: this.creativeMode() ? 0 : 5 * 10,
|
||||
};
|
||||
case UnitType.City:
|
||||
return {
|
||||
cost: (p: Player) =>
|
||||
this.creativeMode() ? 0 :
|
||||
Math.min(
|
||||
1_000_000,
|
||||
Math.pow(2, p.units(UnitType.City).length) * 125_000
|
||||
),
|
||||
territoryBound: true,
|
||||
constructionDuration: 2 * 10,
|
||||
constructionDuration: this.creativeMode() ? 0 : 2 * 10,
|
||||
};
|
||||
case UnitType.Construction:
|
||||
return {
|
||||
@@ -344,18 +353,32 @@ export class DefaultConfig implements Config {
|
||||
return 20_000 * (playerInfo?.nation?.strength ?? 1);
|
||||
}
|
||||
}
|
||||
return 25_000;
|
||||
return this.creativeMode() ? 1_000_000 : 25_000;
|
||||
}
|
||||
|
||||
maxPopulation(player: Player | PlayerView): number {
|
||||
let maxPop = Math.pow(player.numTilesOwned(), 0.6) * 1000 + 50000;
|
||||
let maxPop = this.creativeMode() ? 999_999_999_999 :
|
||||
2 * (Math.pow(player.numTilesOwned(), 0.6) * 1000 + 50000) +
|
||||
player.units(UnitType.City).length * this.cityPopulationIncrease();
|
||||
|
||||
if (player.type() == PlayerType.Bot) {
|
||||
return maxPop / 2;
|
||||
}
|
||||
|
||||
if (player.type() == PlayerType.Human) {
|
||||
return maxPop;
|
||||
}
|
||||
return (
|
||||
maxPop * 2 +
|
||||
player.units(UnitType.City).length * this.cityPopulationIncrease()
|
||||
);
|
||||
|
||||
switch (this._gameConfig.difficulty) {
|
||||
case Difficulty.Easy:
|
||||
return maxPop * 0.5;
|
||||
case Difficulty.Medium:
|
||||
return maxPop * 0.7;
|
||||
case Difficulty.Hard:
|
||||
return maxPop * 1;
|
||||
case Difficulty.Impossible:
|
||||
return maxPop * 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
populationIncreaseRate(player: Player): number {
|
||||
@@ -369,24 +392,6 @@ export class DefaultConfig implements Config {
|
||||
if (player.type() == PlayerType.Bot) {
|
||||
toAdd *= 0.7;
|
||||
}
|
||||
let difficultyMultiplier = 1;
|
||||
switch (this._gameConfig.difficulty) {
|
||||
case Difficulty.Easy:
|
||||
difficultyMultiplier = 0.3;
|
||||
break;
|
||||
case Difficulty.Medium:
|
||||
difficultyMultiplier = 0.5;
|
||||
break;
|
||||
case Difficulty.Hard:
|
||||
difficultyMultiplier = 1;
|
||||
break;
|
||||
case Difficulty.Impossible:
|
||||
difficultyMultiplier = 1.2;
|
||||
break;
|
||||
}
|
||||
if (player.type() == PlayerType.FakeHuman) {
|
||||
toAdd *= difficultyMultiplier;
|
||||
}
|
||||
|
||||
return Math.min(player.population() + toAdd, max) - player.population();
|
||||
}
|
||||
|
||||
@@ -18,14 +18,14 @@ export class DevConfig extends DefaultConfig {
|
||||
}
|
||||
|
||||
numSpawnPhaseTurns(): number {
|
||||
return this.gameConfig().gameType == GameType.Singleplayer ? 20 : 100;
|
||||
return this.gameConfig().gameType == GameType.Singleplayer ? 40 : 100;
|
||||
// return 100
|
||||
}
|
||||
|
||||
unitInfo(type: UnitType): UnitInfo {
|
||||
const info = super.unitInfo(type);
|
||||
const oldCost = info.cost;
|
||||
info.cost = (p: Player) => oldCost(p) / 1000000000;
|
||||
// info.cost = (p: Player) => oldCost(p) / 1000000000;
|
||||
return info;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { Colord, colord, random } from "colord";
|
||||
import { Game, PlayerID, PlayerInfo, TerrainType } from "../game/Game";
|
||||
import {
|
||||
Game,
|
||||
PlayerID,
|
||||
PlayerInfo,
|
||||
PlayerType,
|
||||
TerrainType,
|
||||
} from "../game/Game";
|
||||
import { Theme } from "./Config";
|
||||
import { time } from "console";
|
||||
import { PseudoRandom } from "../PseudoRandom";
|
||||
@@ -122,6 +128,112 @@ export const pastelTheme = new (class implements Theme {
|
||||
colord({ r: 170, g: 150, b: 170 }), // Dusty Rose
|
||||
];
|
||||
|
||||
private humanColors: Colord[] = [
|
||||
// Original set
|
||||
colord({ r: 235, g: 75, b: 75 }), // Bright Red
|
||||
colord({ r: 67, g: 190, b: 84 }), // Fresh Green
|
||||
colord({ r: 59, g: 130, b: 246 }), // Royal Blue
|
||||
colord({ r: 245, g: 158, b: 11 }), // Amber
|
||||
colord({ r: 236, g: 72, b: 153 }), // Deep Pink
|
||||
colord({ r: 48, g: 178, b: 180 }), // Teal
|
||||
colord({ r: 168, g: 85, b: 247 }), // Vibrant Purple
|
||||
colord({ r: 251, g: 191, b: 36 }), // Marigold
|
||||
colord({ r: 74, g: 222, b: 128 }), // Mint
|
||||
colord({ r: 239, g: 68, b: 68 }), // Crimson
|
||||
colord({ r: 34, g: 197, b: 94 }), // Emerald
|
||||
colord({ r: 96, g: 165, b: 250 }), // Sky Blue
|
||||
colord({ r: 249, g: 115, b: 22 }), // Tangerine
|
||||
colord({ r: 192, g: 132, b: 252 }), // Lavender
|
||||
colord({ r: 45, g: 212, b: 191 }), // Turquoise
|
||||
colord({ r: 244, g: 114, b: 182 }), // Rose
|
||||
colord({ r: 132, g: 204, b: 22 }), // Lime
|
||||
colord({ r: 56, g: 189, b: 248 }), // Light Blue
|
||||
colord({ r: 234, g: 179, b: 8 }), // Sunflower
|
||||
colord({ r: 217, g: 70, b: 239 }), // Fuchsia
|
||||
colord({ r: 16, g: 185, b: 129 }), // Sea Green
|
||||
colord({ r: 251, g: 146, b: 60 }), // Light Orange
|
||||
colord({ r: 147, g: 51, b: 234 }), // Bright Purple
|
||||
colord({ r: 79, g: 70, b: 229 }), // Indigo
|
||||
colord({ r: 245, g: 101, b: 101 }), // Coral
|
||||
colord({ r: 134, g: 239, b: 172 }), // Light Green
|
||||
colord({ r: 59, g: 130, b: 246 }), // Cerulean
|
||||
colord({ r: 253, g: 164, b: 175 }), // Salmon Pink
|
||||
colord({ r: 147, g: 197, b: 253 }), // Powder Blue
|
||||
colord({ r: 252, g: 211, b: 77 }), // Golden
|
||||
colord({ r: 190, g: 92, b: 251 }), // Amethyst
|
||||
colord({ r: 82, g: 183, b: 136 }), // Jade
|
||||
colord({ r: 248, g: 113, b: 113 }), // Warm Red
|
||||
colord({ r: 99, g: 202, b: 253 }), // Azure
|
||||
colord({ r: 240, g: 171, b: 252 }), // Orchid
|
||||
colord({ r: 163, g: 230, b: 53 }), // Yellow Green
|
||||
colord({ r: 234, g: 88, b: 12 }), // Burnt Orange
|
||||
colord({ r: 125, g: 211, b: 252 }), // Crystal Blue
|
||||
colord({ r: 251, g: 113, b: 133 }), // Watermelon
|
||||
colord({ r: 52, g: 211, b: 153 }), // Spearmint
|
||||
colord({ r: 167, g: 139, b: 250 }), // Periwinkle
|
||||
colord({ r: 245, g: 158, b: 11 }), // Honey
|
||||
colord({ r: 110, g: 231, b: 183 }), // Seafoam
|
||||
colord({ r: 233, g: 213, b: 255 }), // Light Lilac
|
||||
colord({ r: 202, g: 138, b: 4 }), // Rich Gold
|
||||
colord({ r: 151, g: 255, b: 187 }), // Fresh Mint
|
||||
colord({ r: 220, g: 38, b: 38 }), // Ruby
|
||||
colord({ r: 124, g: 58, b: 237 }), // Royal Purple
|
||||
colord({ r: 45, g: 212, b: 191 }), // Ocean
|
||||
colord({ r: 252, g: 165, b: 165 }), // Peach
|
||||
|
||||
// Additional 50 colors
|
||||
colord({ r: 179, g: 136, b: 255 }), // Light Purple
|
||||
colord({ r: 133, g: 77, b: 14 }), // Chocolate
|
||||
colord({ r: 52, g: 211, b: 153 }), // Aquamarine
|
||||
colord({ r: 234, g: 179, b: 8 }), // Mustard
|
||||
colord({ r: 236, g: 72, b: 153 }), // Hot Pink
|
||||
colord({ r: 147, g: 197, b: 253 }), // Sky
|
||||
colord({ r: 249, g: 115, b: 22 }), // Pumpkin
|
||||
colord({ r: 167, g: 139, b: 250 }), // Iris
|
||||
colord({ r: 16, g: 185, b: 129 }), // Pine
|
||||
colord({ r: 251, g: 146, b: 60 }), // Mango
|
||||
colord({ r: 192, g: 132, b: 252 }), // Wisteria
|
||||
colord({ r: 79, g: 70, b: 229 }), // Sapphire
|
||||
colord({ r: 245, g: 101, b: 101 }), // Salmon
|
||||
colord({ r: 134, g: 239, b: 172 }), // Spring Green
|
||||
colord({ r: 59, g: 130, b: 246 }), // Ocean Blue
|
||||
colord({ r: 253, g: 164, b: 175 }), // Rose Gold
|
||||
colord({ r: 16, g: 185, b: 129 }), // Forest
|
||||
colord({ r: 252, g: 211, b: 77 }), // Sunshine
|
||||
colord({ r: 190, g: 92, b: 251 }), // Grape
|
||||
colord({ r: 82, g: 183, b: 136 }), // Eucalyptus
|
||||
colord({ r: 248, g: 113, b: 113 }), // Cherry
|
||||
colord({ r: 99, g: 202, b: 253 }), // Arctic
|
||||
colord({ r: 240, g: 171, b: 252 }), // Lilac
|
||||
colord({ r: 163, g: 230, b: 53 }), // Chartreuse
|
||||
colord({ r: 234, g: 88, b: 12 }), // Rust
|
||||
colord({ r: 125, g: 211, b: 252 }), // Ice Blue
|
||||
colord({ r: 251, g: 113, b: 133 }), // Strawberry
|
||||
colord({ r: 52, g: 211, b: 153 }), // Sage
|
||||
colord({ r: 167, g: 139, b: 250 }), // Violet
|
||||
colord({ r: 245, g: 158, b: 11 }), // Apricot
|
||||
colord({ r: 110, g: 231, b: 183 }), // Mint Green
|
||||
colord({ r: 233, g: 213, b: 255 }), // Thistle
|
||||
colord({ r: 202, g: 138, b: 4 }), // Bronze
|
||||
colord({ r: 151, g: 255, b: 187 }), // Pistachio
|
||||
colord({ r: 220, g: 38, b: 38 }), // Fire Engine
|
||||
colord({ r: 124, g: 58, b: 237 }), // Electric Purple
|
||||
colord({ r: 45, g: 212, b: 191 }), // Caribbean
|
||||
colord({ r: 252, g: 165, b: 165 }), // Melon
|
||||
colord({ r: 168, g: 85, b: 247 }), // Byzantium
|
||||
colord({ r: 74, g: 222, b: 128 }), // Kelly Green
|
||||
colord({ r: 239, g: 68, b: 68 }), // Cardinal
|
||||
colord({ r: 34, g: 197, b: 94 }), // Shamrock
|
||||
colord({ r: 96, g: 165, b: 250 }), // Marina
|
||||
colord({ r: 249, g: 115, b: 22 }), // Carrot
|
||||
colord({ r: 192, g: 132, b: 252 }), // Heliotrope
|
||||
colord({ r: 45, g: 212, b: 191 }), // Lagoon
|
||||
colord({ r: 244, g: 114, b: 182 }), // Bubble Gum
|
||||
colord({ r: 132, g: 204, b: 22 }), // Apple
|
||||
colord({ r: 56, g: 189, b: 248 }), // Electric Blue
|
||||
colord({ r: 234, g: 179, b: 8 }), // Daffodil
|
||||
];
|
||||
|
||||
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 });
|
||||
@@ -133,6 +245,11 @@ export const pastelTheme = new (class implements Theme {
|
||||
}
|
||||
|
||||
territoryColor(playerInfo: PlayerInfo): Colord {
|
||||
if (playerInfo.playerType == PlayerType.Human) {
|
||||
return this.humanColors[
|
||||
simpleHash(playerInfo.name) % this.humanColors.length
|
||||
];
|
||||
}
|
||||
return this.territoryColors[
|
||||
simpleHash(playerInfo.name) % this.territoryColors.length
|
||||
];
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { PriorityQueue } from "@datastructures-js/priority-queue";
|
||||
import {
|
||||
Attack,
|
||||
Cell,
|
||||
Execution,
|
||||
Game,
|
||||
@@ -37,8 +38,10 @@ export class AttackExecution implements Execution {
|
||||
|
||||
private border = new Set<TileRef>();
|
||||
|
||||
private attack: Attack = null;
|
||||
|
||||
constructor(
|
||||
private troops: number | null,
|
||||
private startTroops: number | null = null,
|
||||
private _ownerID: PlayerID,
|
||||
private _targetID: PlayerID | null,
|
||||
private sourceTile: TileRef | null,
|
||||
@@ -80,57 +83,49 @@ export class AttackExecution implements Execution {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.troops == null) {
|
||||
this.troops = this.mg.config().attackAmount(this._owner, this.target);
|
||||
if (this.startTroops == null) {
|
||||
this.startTroops = this.mg
|
||||
.config()
|
||||
.attackAmount(this._owner, this.target);
|
||||
}
|
||||
this.troops = Math.min(this._owner.troops(), this.troops);
|
||||
this.startTroops = Math.min(this._owner.troops(), this.startTroops);
|
||||
if (this.removeTroops) {
|
||||
this._owner.removeTroops(this.troops);
|
||||
this._owner.removeTroops(this.startTroops);
|
||||
}
|
||||
this.attack = this._owner.createAttack(
|
||||
this.target,
|
||||
this.startTroops,
|
||||
this.sourceTile
|
||||
);
|
||||
|
||||
for (const exec of mg.executions()) {
|
||||
if (exec.isActive() && exec instanceof AttackExecution && exec != this) {
|
||||
const otherAttack = exec as AttackExecution;
|
||||
for (const incoming of this._owner.incomingAttacks()) {
|
||||
if (incoming.attacker() == this.target) {
|
||||
// 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();
|
||||
if (incoming.troops() > this.attack.troops()) {
|
||||
incoming.setTroops(incoming.troops() - this.attack.troops());
|
||||
this.attack.delete();
|
||||
this.active = false;
|
||||
return;
|
||||
} else {
|
||||
this.attack.setTroops(this.attack.troops() - incoming.troops());
|
||||
incoming.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
);
|
||||
for (const outgoing of this._owner.outgoingAttacks()) {
|
||||
if (
|
||||
outgoing != this.attack &&
|
||||
outgoing.target() == this.attack.target() &&
|
||||
outgoing.sourceTile() == this.attack.sourceTile()
|
||||
) {
|
||||
// Existing attack on same target, add troops
|
||||
outgoing.setTroops(outgoing.troops() + this.attack.troops());
|
||||
this.active = false;
|
||||
this.attack.delete();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.sourceTile != null) {
|
||||
this.addNeighbors(this.sourceTile);
|
||||
} else {
|
||||
@@ -155,9 +150,11 @@ export class AttackExecution implements Execution {
|
||||
}
|
||||
|
||||
tick(ticks: number) {
|
||||
if (!this.active) {
|
||||
if (!this.attack.isActive()) {
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const alliance = this._owner.allianceWith(this.target as Player);
|
||||
if (this.breakAlliance && alliance != null) {
|
||||
this.breakAlliance = false;
|
||||
@@ -165,7 +162,8 @@ export class AttackExecution implements Execution {
|
||||
}
|
||||
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._owner.addTroops(this.attack.troops());
|
||||
this.attack.delete();
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
@@ -173,7 +171,7 @@ export class AttackExecution implements Execution {
|
||||
let numTilesPerTick = this.mg
|
||||
.config()
|
||||
.attackTilesPerTick(
|
||||
this.troops,
|
||||
this.attack.troops(),
|
||||
this._owner,
|
||||
this.target,
|
||||
this.border.size + this.random.nextInt(0, 5)
|
||||
@@ -182,7 +180,8 @@ export class AttackExecution implements Execution {
|
||||
// consolex.log(`num execs: ${this.mg.executions().length}`)
|
||||
|
||||
while (numTilesPerTick > 0) {
|
||||
if (this.troops < 1) {
|
||||
if (this.attack.troops() < 1) {
|
||||
this.attack.delete();
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
@@ -190,7 +189,8 @@ export class AttackExecution implements Execution {
|
||||
if (this.toConquer.size() == 0) {
|
||||
this.refreshToConquer();
|
||||
this.active = false;
|
||||
this._owner.addTroops(this.troops);
|
||||
this._owner.addTroops(this.attack.troops());
|
||||
this.attack.delete();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -209,13 +209,13 @@ export class AttackExecution implements Execution {
|
||||
.config()
|
||||
.attackLogic(
|
||||
this.mg,
|
||||
this.troops,
|
||||
this.attack.troops(),
|
||||
this._owner,
|
||||
this.target,
|
||||
tileToConquer
|
||||
);
|
||||
numTilesPerTick -= tilesPerTickUsed;
|
||||
this.troops -= attackerTroopLoss;
|
||||
this.attack.setTroops(this.attack.troops() - attackerTroopLoss);
|
||||
if (this.target.isPlayer()) {
|
||||
this.target.removeTroops(defenderTroopLoss);
|
||||
}
|
||||
|
||||
@@ -64,6 +64,7 @@ export class Executor {
|
||||
case "spawn":
|
||||
return new SpawnExecution(
|
||||
new PlayerInfo(
|
||||
intent.flag,
|
||||
// Players see their original name, others see a sanitized version
|
||||
intent.clientID == this.clientID
|
||||
? sanitize(intent.name)
|
||||
@@ -131,6 +132,7 @@ export class Executor {
|
||||
new FakeHumanExecution(
|
||||
this.gameID,
|
||||
new PlayerInfo(
|
||||
nation.flag || "",
|
||||
nation.name,
|
||||
PlayerType.FakeHuman,
|
||||
null,
|
||||
|
||||
@@ -26,9 +26,8 @@ export class MirvExecution implements Execution {
|
||||
|
||||
private nuke: Unit;
|
||||
|
||||
private mirvRange = 500;
|
||||
private warheadCount = 1000;
|
||||
// private warheadRange = 5;
|
||||
private mirvRange = 1500;
|
||||
private warheadCount = 500;
|
||||
|
||||
private random: PseudoRandom;
|
||||
|
||||
@@ -92,7 +91,7 @@ export class MirvExecution implements Execution {
|
||||
|
||||
private separate() {
|
||||
const dsts: TileRef[] = [this.dst];
|
||||
let attempts = 1000;
|
||||
let attempts = 10000;
|
||||
while (attempts > 0 && dsts.length < this.warheadCount) {
|
||||
attempts--;
|
||||
const potential = this.randomLand(this.dst);
|
||||
@@ -106,6 +105,7 @@ export class MirvExecution implements Execution {
|
||||
(a, b) =>
|
||||
this.mg.manhattanDist(b, this.dst) - this.mg.manhattanDist(a, this.dst)
|
||||
);
|
||||
console.log(`got ${dsts.length} dsts!!`);
|
||||
|
||||
for (const [i, dst] of dsts.entries()) {
|
||||
this.mg.addExecution(
|
||||
|
||||
@@ -104,13 +104,13 @@ export class NukeExecution implements Execution {
|
||||
let magnitude;
|
||||
switch (this.type) {
|
||||
case UnitType.MIRVWarhead:
|
||||
magnitude = { inner: 10, outer: 14 };
|
||||
magnitude = { inner: 20, outer: 25 };
|
||||
break;
|
||||
case UnitType.AtomBomb:
|
||||
magnitude = { inner: 15, outer: 40 };
|
||||
break;
|
||||
case UnitType.HydrogenBomb:
|
||||
magnitude = { inner: 140, outer: 160 };
|
||||
magnitude = { inner: 120, outer: 140 };
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Attack, Player, TerraNullius } from "./Game";
|
||||
import { TileRef } from "./GameMap";
|
||||
import { PlayerImpl } from "./PlayerImpl";
|
||||
|
||||
export class AttackImpl implements Attack {
|
||||
private _isActive = true;
|
||||
|
||||
constructor(
|
||||
private _target: Player | TerraNullius,
|
||||
private _attacker: Player,
|
||||
private _troops: number,
|
||||
private _sourceTile: TileRef | null
|
||||
) {}
|
||||
|
||||
sourceTile(): TileRef | null {
|
||||
return this._sourceTile;
|
||||
}
|
||||
|
||||
target(): Player | TerraNullius {
|
||||
return this._target;
|
||||
}
|
||||
attacker(): Player {
|
||||
return this._attacker;
|
||||
}
|
||||
troops(): number {
|
||||
return this._troops;
|
||||
}
|
||||
setTroops(troops: number) {
|
||||
this._troops = troops;
|
||||
}
|
||||
|
||||
isActive() {
|
||||
return this._isActive;
|
||||
}
|
||||
|
||||
delete() {
|
||||
if (this._target.isPlayer()) {
|
||||
(this._target as PlayerImpl)._incomingAttacks = (
|
||||
this._target as PlayerImpl
|
||||
)._incomingAttacks.filter((a) => a != this);
|
||||
}
|
||||
|
||||
(this._attacker as PlayerImpl)._outgoingAttacks = (
|
||||
this._attacker as PlayerImpl
|
||||
)._outgoingAttacks.filter((a) => a != this);
|
||||
|
||||
this._isActive = false;
|
||||
}
|
||||
}
|
||||
+23
-3
@@ -86,6 +86,7 @@ export enum Relation {
|
||||
|
||||
export class Nation {
|
||||
constructor(
|
||||
public readonly flag: string,
|
||||
public readonly name: string,
|
||||
public readonly cell: Cell,
|
||||
public readonly strength: number
|
||||
@@ -135,6 +136,17 @@ export interface Execution {
|
||||
owner(): Player;
|
||||
}
|
||||
|
||||
export interface Attack {
|
||||
target(): Player | TerraNullius;
|
||||
attacker(): Player;
|
||||
troops(): number;
|
||||
setTroops(troops: number): void;
|
||||
isActive(): boolean;
|
||||
delete(): void;
|
||||
// The tile the attack originated from, mostly used for boat attacks.
|
||||
sourceTile(): TileRef | null;
|
||||
}
|
||||
|
||||
export interface AllianceRequest {
|
||||
accept(): void;
|
||||
reject(): void;
|
||||
@@ -157,6 +169,7 @@ export interface MutableAlliance extends Alliance {
|
||||
|
||||
export class PlayerInfo {
|
||||
constructor(
|
||||
public readonly flag: string,
|
||||
public readonly name: string,
|
||||
public readonly playerType: PlayerType,
|
||||
// null if bot.
|
||||
@@ -284,12 +297,21 @@ export interface Player {
|
||||
canDonate(recipient: Player): boolean;
|
||||
donate(recipient: Player, troops: number): void;
|
||||
|
||||
// Attacking.
|
||||
canAttack(tile: TileRef): boolean;
|
||||
createAttack(
|
||||
target: Player | TerraNullius,
|
||||
troops: number,
|
||||
sourceTile: TileRef
|
||||
): Attack;
|
||||
outgoingAttacks(): Attack[];
|
||||
incomingAttacks(): Attack[];
|
||||
|
||||
// Misc
|
||||
executions(): Execution[];
|
||||
toUpdate(): PlayerUpdate;
|
||||
playerProfile(): PlayerProfile;
|
||||
canBoat(tile: TileRef): boolean;
|
||||
canAttack(tile: TileRef);
|
||||
}
|
||||
|
||||
export interface Game extends GameMap {
|
||||
@@ -324,8 +346,6 @@ export interface Game extends GameMap {
|
||||
unitInfo(type: UnitType): UnitInfo;
|
||||
nearbyDefensePosts(tile: TileRef): Unit[];
|
||||
|
||||
// Events & Messages
|
||||
executions(): Execution[];
|
||||
addExecution(...exec: Execution[]): void;
|
||||
displayMessage(
|
||||
message: string,
|
||||
|
||||
@@ -78,6 +78,7 @@ export class GameImpl implements Game {
|
||||
this.nations_ = nationMap.nations.map(
|
||||
(n) =>
|
||||
new Nation(
|
||||
n.flag || "",
|
||||
n.name,
|
||||
new Cell(n.coordinates[0], n.coordinates[1]),
|
||||
n.strength
|
||||
|
||||
@@ -70,10 +70,17 @@ export interface UnitUpdate {
|
||||
constructionType?: UnitType;
|
||||
}
|
||||
|
||||
export interface AttackUpdate {
|
||||
attackerID: number;
|
||||
targetID: number;
|
||||
troops: number;
|
||||
}
|
||||
|
||||
export interface PlayerUpdate {
|
||||
type: GameUpdateType.Player;
|
||||
nameViewData?: NameViewData;
|
||||
clientID: ClientID;
|
||||
flag: string,
|
||||
name: string;
|
||||
displayName: string;
|
||||
id: PlayerID;
|
||||
@@ -90,6 +97,8 @@ export interface PlayerUpdate {
|
||||
isTraitor: boolean;
|
||||
targets: number[];
|
||||
outgoingEmojis: EmojiMessage[];
|
||||
outgoingAttacks: AttackUpdate[];
|
||||
incomingAttacks: AttackUpdate[];
|
||||
}
|
||||
|
||||
export interface AllianceRequestUpdate {
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
PlayerProfile,
|
||||
Unit,
|
||||
} from "./Game";
|
||||
import { PlayerUpdate } from "./GameUpdates";
|
||||
import { AttackUpdate, PlayerUpdate } from "./GameUpdates";
|
||||
import { UnitUpdate } from "./GameUpdates";
|
||||
import { NameViewData } from "./Game";
|
||||
import { GameUpdateType } from "./GameUpdates";
|
||||
@@ -106,6 +106,14 @@ export class PlayerView {
|
||||
);
|
||||
}
|
||||
|
||||
outgoingAttacks(): AttackUpdate[] {
|
||||
return this.data.outgoingAttacks;
|
||||
}
|
||||
|
||||
incomingAttacks(): AttackUpdate[] {
|
||||
return this.data.incomingAttacks;
|
||||
}
|
||||
|
||||
units(...types: UnitType[]): UnitView[] {
|
||||
return this.game
|
||||
.units(...types)
|
||||
@@ -119,6 +127,9 @@ export class PlayerView {
|
||||
smallID(): number {
|
||||
return this.data.smallID;
|
||||
}
|
||||
flag(): string {
|
||||
return this.data.flag;
|
||||
}
|
||||
name(): string {
|
||||
return this.data.name;
|
||||
}
|
||||
@@ -188,7 +199,7 @@ export class PlayerView {
|
||||
return this.data.outgoingEmojis;
|
||||
}
|
||||
info(): PlayerInfo {
|
||||
return new PlayerInfo(this.name(), this.type(), this.clientID(), this.id());
|
||||
return new PlayerInfo(this.flag(), this.name(), this.type(), this.clientID(), this.id());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,8 +17,9 @@ import {
|
||||
Relation,
|
||||
EmojiMessage,
|
||||
PlayerProfile,
|
||||
Attack,
|
||||
} from "./Game";
|
||||
import { PlayerUpdate } from "./GameUpdates";
|
||||
import { AttackUpdate, PlayerUpdate } from "./GameUpdates";
|
||||
import { GameUpdateType } from "./GameUpdates";
|
||||
import { ClientID } from "../Schemas";
|
||||
import {
|
||||
@@ -37,6 +38,7 @@ import { renderTroops } from "../../client/Utils";
|
||||
import { TerraNulliusImpl } from "./TerraNulliusImpl";
|
||||
import { andFN, manhattanDistFN, TileRef } from "./GameMap";
|
||||
import { Emoji } from "discord.js";
|
||||
import { AttackImpl } from "./AttackImpl";
|
||||
|
||||
interface Target {
|
||||
tick: Tick;
|
||||
@@ -62,6 +64,7 @@ export class PlayerImpl implements Player {
|
||||
public _units: UnitImpl[] = [];
|
||||
public _tiles: Set<TileRef> = new Set();
|
||||
|
||||
private _flag: string;
|
||||
private _name: string;
|
||||
private _displayName: string;
|
||||
|
||||
@@ -75,12 +78,16 @@ export class PlayerImpl implements Player {
|
||||
|
||||
private relations = new Map<Player, number>();
|
||||
|
||||
public _incomingAttacks: Attack[] = [];
|
||||
public _outgoingAttacks: Attack[] = [];
|
||||
|
||||
constructor(
|
||||
private mg: GameImpl,
|
||||
private _smallID: number,
|
||||
private readonly playerInfo: PlayerInfo,
|
||||
startPopulation: number
|
||||
) {
|
||||
this._flag = playerInfo.flag;
|
||||
this._name = playerInfo.name;
|
||||
this._targetTroopRatio = 1;
|
||||
this._troops = startPopulation * this._targetTroopRatio;
|
||||
@@ -95,6 +102,7 @@ export class PlayerImpl implements Player {
|
||||
return {
|
||||
type: GameUpdateType.Player,
|
||||
clientID: this.clientID(),
|
||||
flag: this.flag(),
|
||||
name: this.name(),
|
||||
displayName: this.displayName(),
|
||||
id: this.id(),
|
||||
@@ -111,6 +119,22 @@ export class PlayerImpl implements Player {
|
||||
isTraitor: this.isTraitor(),
|
||||
targets: this.targets().map((p) => p.smallID()),
|
||||
outgoingEmojis: this.outgoingEmojis(),
|
||||
outgoingAttacks: this._outgoingAttacks.map(
|
||||
(a) =>
|
||||
({
|
||||
attackerID: a.attacker().smallID(),
|
||||
targetID: a.target().smallID(),
|
||||
troops: a.troops(),
|
||||
} as AttackUpdate)
|
||||
),
|
||||
incomingAttacks: this._incomingAttacks.map(
|
||||
(a) =>
|
||||
({
|
||||
attackerID: a.attacker().smallID(),
|
||||
targetID: a.target().smallID(),
|
||||
troops: a.troops(),
|
||||
} as AttackUpdate)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -118,6 +142,10 @@ export class PlayerImpl implements Player {
|
||||
return this._smallID;
|
||||
}
|
||||
|
||||
flag(): string {
|
||||
return this._flag;
|
||||
}
|
||||
|
||||
name(): string {
|
||||
return this._name;
|
||||
}
|
||||
@@ -759,6 +787,25 @@ export class PlayerImpl implements Player {
|
||||
}
|
||||
}
|
||||
|
||||
createAttack(
|
||||
target: Player | TerraNullius,
|
||||
troops: number,
|
||||
sourceTile: TileRef
|
||||
): Attack {
|
||||
const attack = new AttackImpl(target, this, troops, sourceTile);
|
||||
this._outgoingAttacks.push(attack);
|
||||
if (target.isPlayer()) {
|
||||
(target as PlayerImpl)._incomingAttacks.push(attack);
|
||||
}
|
||||
return attack;
|
||||
}
|
||||
outgoingAttacks(): Attack[] {
|
||||
return this._outgoingAttacks;
|
||||
}
|
||||
incomingAttacks(): Attack[] {
|
||||
return this._incomingAttacks;
|
||||
}
|
||||
|
||||
public canAttack(tile: TileRef): boolean {
|
||||
if (
|
||||
this.mg.hasOwner(tile) &&
|
||||
|
||||
@@ -17,6 +17,7 @@ export interface NationMap {
|
||||
|
||||
export interface Nation {
|
||||
coordinates: [number, number];
|
||||
flag: string;
|
||||
name: string;
|
||||
strength: number;
|
||||
}
|
||||
|
||||
@@ -49,6 +49,9 @@ export class GameManager {
|
||||
gameMap: GameMapType.World,
|
||||
gameType: GameType.Private,
|
||||
difficulty: Difficulty.Medium,
|
||||
disableBots: false,
|
||||
disableNPCs: false,
|
||||
creativeMode: false,
|
||||
})
|
||||
);
|
||||
return id;
|
||||
@@ -87,6 +90,9 @@ export class GameManager {
|
||||
gameMap: this.random.randElement(Object.values(GameMapType)),
|
||||
gameType: GameType.Public,
|
||||
difficulty: Difficulty.Medium,
|
||||
disableBots: false,
|
||||
disableNPCs: false,
|
||||
creativeMode: false,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -57,6 +57,15 @@ export class GameServer {
|
||||
if (gameConfig.difficulty != null) {
|
||||
this.gameConfig.difficulty = gameConfig.difficulty;
|
||||
}
|
||||
if (gameConfig.disableBots != null) {
|
||||
this.gameConfig.disableBots = gameConfig.disableBots;
|
||||
}
|
||||
if (gameConfig.disableNPCs != null) {
|
||||
this.gameConfig.disableNPCs = gameConfig.disableNPCs;
|
||||
}
|
||||
if (gameConfig.creativeMode != null) {
|
||||
this.gameConfig.creativeMode = gameConfig.creativeMode;
|
||||
}
|
||||
}
|
||||
|
||||
public addClient(client: Client, lastTurn: number) {
|
||||
|
||||
@@ -94,6 +94,9 @@ app.put("/private_lobby/:id", (req, res) => {
|
||||
gm.updateGameConfig(lobbyID, {
|
||||
gameMap: req.body.gameMap,
|
||||
difficulty: req.body.difficulty,
|
||||
disableBots: req.body.disableBots,
|
||||
disableNPCs: req.body.disableNPCs,
|
||||
creativeMode: req.body.creativeMode,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user