Merge branch 'main' into ui/modal-overlay-improvements

This commit is contained in:
q8gazy
2025-02-12 06:58:36 +03:00
committed by GitHub
318 changed files with 30438 additions and 17781 deletions
+7
View File
@@ -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,
};
}
+18
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}
+34
View File
@@ -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]}`
+1
View File
@@ -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
View File
@@ -3,6 +3,7 @@ export function renderTroops(troops: number): string {
}
export function renderNumber(num: number) {
num = Math.max(num, 0);
let numStr = "";
if (num >= 10_000_000) {
numStr = (num / 1000000).toFixed(1) + "M";
@@ -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);
},
}
);
}
+230
View File
@@ -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>
`;
}
}
+974
View File
@@ -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"
}
]
+3 -1
View File
@@ -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 },
+2 -2
View File
@@ -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;
+104 -16
View File
@@ -1,8 +1,11 @@
import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { EventBus } from "../../../core/EventBus";
import { AllPlayers, MessageType } from "../../../core/game/Game";
import { DisplayMessageUpdate } from "../../../core/game/GameUpdates";
import { AllPlayers, MessageType, PlayerType } from "../../../core/game/Game";
import {
AttackUpdate,
DisplayMessageUpdate,
} from "../../../core/game/GameUpdates";
import { EmojiUpdate } from "../../../core/game/GameUpdates";
import { TargetPlayerUpdate } from "../../../core/game/GameUpdates";
import { AllianceExpiredUpdate } from "../../../core/game/GameUpdates";
@@ -16,6 +19,7 @@ import { SendAllianceReplyIntentEvent } from "../../Transport";
import { unsafeHTML } from "lit/directives/unsafe-html.js";
import { onlyImages, sanitize } from "../../../core/Util";
import { GameView, PlayerView } from "../../../core/game/GameView";
import { renderTroops } from "../../Utils";
interface Event {
description: string;
@@ -38,6 +42,8 @@ export class EventsDisplay extends LitElement implements Layer {
public clientID: ClientID;
private events: Event[] = [];
@state() private incomingAttacks: AttackUpdate[] = [];
@state() private outgoingAttacks: AttackUpdate[] = [];
private updateMap = new Map([
[GameUpdateType.DisplayEvent, (u) => this.onDisplayMessageEvent(u)],
@@ -54,6 +60,8 @@ export class EventsDisplay extends LitElement implements Layer {
constructor() {
super();
this.events = [];
this.incomingAttacks = [];
this.outgoingAttacks = [];
}
init() {}
@@ -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;
}
}
+28 -10
View File
@@ -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();
+4 -2
View File
@@ -26,16 +26,19 @@ export class TopBar extends LitElement implements Layer {
if (!this.isVisible) {
return html``;
}
const myPlayer = this.game?.myPlayer();
if (!myPlayer?.isAlive() || this.game?.inSpawnPhase()) {
return html``;
}
const popRate = this.game.config().populationIncreaseRate(myPlayer) * 10;
const maxPop = this.game.config().maxPopulation(myPlayer);
const goldPerSecond = this.game.config().goldAdditionRate(myPlayer) * 10;
return html`
<div
class="fixed top-0 z-50 bg-black/90 text-white text-sm p-1 rounded grid grid-cols-1 sm:grid-cols-2 w-1/2 sm:w-2/3 md:w-1/2 lg:hidden"
class="fixed top-0 z-50 bg-gray-800/70 text-white text-sm p-1 rounded grid grid-cols-1 sm:grid-cols-2 w-1/2 sm:w-2/3 md:w-1/2 lg:hidden backdrop-blur"
>
<!-- Pop section (takes 2 columns on desktop) -->
<div
@@ -48,7 +51,6 @@ export class TopBar extends LitElement implements Layer {
>
<span>(+${renderTroops(popRate)})</span>
</div>
<!-- Gold section (takes 1 column on desktop) -->
<div
class="flex items-center space-x-2 overflow-x-auto whitespace-nowrap"
+6 -3
View File
@@ -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
+5 -3
View File
@@ -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());
}
+4
View File
@@ -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,
+2
View File
@@ -70,6 +70,8 @@ export interface Config {
percentageTilesOwnedToWin(): number;
numBots(): number;
spawnNPCs(): boolean;
spawnBots(): boolean;
creativeMode(): boolean;
numSpawnPhaseTurns(): number;
startManpower(playerInfo: PlayerInfo): number;
+39 -34
View File
@@ -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();
}
+2 -2
View File
@@ -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;
}
+118 -1
View File
@@ -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
];
+48 -48
View File
@@ -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);
}
+2
View File
@@ -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,
+4 -4
View File
@@ -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(
+2 -2
View File
@@ -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;
}
+49
View File
@@ -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
View File
@@ -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,
+1
View File
@@ -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
+9
View File
@@ -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 {
+13 -2
View File
@@ -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());
}
}
+48 -1
View File
@@ -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) &&
+1
View File
@@ -17,6 +17,7 @@ export interface NationMap {
export interface Nation {
coordinates: [number, number];
flag: string;
name: string;
strength: number;
}
+6
View File
@@ -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,
})
);
}
+9
View File
@@ -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) {
+3
View File
@@ -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,
});
});