mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-30 20:43:26 +00:00
@@ -162,21 +162,24 @@
|
||||
* disable double tap on mobile DONE 10/6/2024
|
||||
* donate troops button DONE 10/7/2024
|
||||
* Make fake humans spawn by their country DONE 10/9/2024
|
||||
* UI: leader board DONE 10/12/2024
|
||||
* single player mode DONE 10/12/2024
|
||||
* single player select map DONE 10/12/2024
|
||||
* implement private game
|
||||
* private game can select map
|
||||
* optimize sendBoat function
|
||||
* Test on android
|
||||
* Increase disk size
|
||||
* NPC more likely to accept alliance fewer alliance player has
|
||||
* fake humans target enemies
|
||||
* create private lobby menu
|
||||
* better NPC relation logic
|
||||
* surface NPC relations
|
||||
* block user inputs if too far behind server
|
||||
* BUG: FakeHuman don't be enemy if don't share border (or send boat)
|
||||
* store cookies
|
||||
* UI: leader board
|
||||
* Load terrain dataImage in background
|
||||
* BUG: half encircle Antartica causes capture
|
||||
|
||||
* improve front page (make map larger?)
|
||||
* Add additional maps
|
||||
* add offline mode
|
||||
* REFACTOR: give terranullius an ID, game.player() returns terranullius
|
||||
* REFACTOR: ocean is considered TerraNullius ?
|
||||
* Make icons svgs
|
||||
|
||||
Generated
+72
-19
@@ -22,6 +22,7 @@
|
||||
"googleapis": "^143.0.0",
|
||||
"hammerjs": "^2.0.8",
|
||||
"jimp": "^0.22.12",
|
||||
"lit": "^3.2.1",
|
||||
"msgpack5": "^6.0.2",
|
||||
"node-addon-api": "^8.1.0",
|
||||
"node-gyp": "^10.2.0",
|
||||
@@ -43,7 +44,7 @@
|
||||
"@types/d3": "^7.4.3",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/mocha": "^10.0.7",
|
||||
"@types/node": "^22.5.2",
|
||||
"@types/node": "^22.7.5",
|
||||
"@types/sinon": "^17.0.3",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@types/ws": "^8.5.11",
|
||||
@@ -69,7 +70,7 @@
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"tsx": "^4.17.0",
|
||||
"typescript": "^5.5.4",
|
||||
"typescript": "^5.6.3",
|
||||
"webpack": "^5.91.0",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"webpack-dev-server": "^5.0.4"
|
||||
@@ -3655,6 +3656,21 @@
|
||||
"integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@lit-labs/ssr-dom-shim": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.2.1.tgz",
|
||||
"integrity": "sha512-wx4aBmgeGvFmOKucFKY+8VFJSYZxs9poN3SDNQFF6lT6NrQUnHiPB2PWz2sc4ieEcAaYYzN+1uWahEeTq2aRIQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@lit/reactive-element": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.0.4.tgz",
|
||||
"integrity": "sha512-GFn91inaUa2oHLak8awSIigYz0cU0Payr1rcFsrkf5OJ5eSPxElyZfKh0f2p9FsTiZWXQdWGJeXZICEfXXYSXQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@lit-labs/ssr-dom-shim": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@npmcli/agent": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-2.2.2.tgz",
|
||||
@@ -4428,9 +4444,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.2.tgz",
|
||||
"integrity": "sha512-acJsPTEqYqulZS/Yp/S3GgeE6GZ0qYODUR8aVr/DkhHQ8l9nd4j5x1/ZJy9/gHrRlFMqkO6i0I3E27Alu4jjPg==",
|
||||
"version": "22.7.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz",
|
||||
"integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.19.2"
|
||||
@@ -4542,6 +4558,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/uuid": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
|
||||
@@ -9990,6 +10012,37 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lit": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/lit/-/lit-3.2.1.tgz",
|
||||
"integrity": "sha512-1BBa1E/z0O9ye5fZprPtdqnc0BFzxIxTTOO/tQFmyC/hj1O3jL4TfmLBw0WEwjAokdLwpclkvGgDJwTIh0/22w==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@lit/reactive-element": "^2.0.4",
|
||||
"lit-element": "^4.1.0",
|
||||
"lit-html": "^3.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lit-element": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.1.1.tgz",
|
||||
"integrity": "sha512-HO9Tkkh34QkTeUmEdNYhMT8hzLid7YlMlATSi1q4q17HE5d9mrrEHJ/o8O2D0cMi182zK1F3v7x0PWFjrhXFew==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@lit-labs/ssr-dom-shim": "^1.2.0",
|
||||
"@lit/reactive-element": "^2.0.4",
|
||||
"lit-html": "^3.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lit-html": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.2.1.tgz",
|
||||
"integrity": "sha512-qI/3lziaPMSKsrwlxH/xMgikhQ0EGOX2ICU73Bi/YHFvz2j/yMCIrw4+puF2IpQ4+upd3EWbvnHM9+PnJn48YA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@types/trusted-types": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/load-bmfont": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/load-bmfont/-/load-bmfont-1.4.2.tgz",
|
||||
@@ -11355,9 +11408,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
|
||||
"integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
|
||||
"integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
@@ -11423,9 +11476,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.41",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz",
|
||||
"integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==",
|
||||
"version": "8.4.47",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
|
||||
"integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -11444,8 +11497,8 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.7",
|
||||
"picocolors": "^1.0.1",
|
||||
"source-map-js": "^1.2.0"
|
||||
"picocolors": "^1.1.0",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
@@ -12573,9 +12626,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
|
||||
"integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
@@ -13470,9 +13523,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.5.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
|
||||
"integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
|
||||
"version": "5.6.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
|
||||
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
|
||||
+5
-3
@@ -9,7 +9,8 @@
|
||||
"start:server-dev": "GAME_ENV=dev node --loader ts-node/esm --experimental-specifier-resolution=node src/server/Server.ts",
|
||||
"dev": "GAME_ENV=dev concurrently \"npm run start:client\" \"npm run start:server-dev\"",
|
||||
"tunnel": "npm run build-prod && npm run start:server",
|
||||
"test": "jest"
|
||||
"test": "jest",
|
||||
"tailwind": "tailwindcss build -i ./src/client/tailwind.css -o public/tailwind.css"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.25.2",
|
||||
@@ -19,7 +20,7 @@
|
||||
"@types/d3": "^7.4.3",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/mocha": "^10.0.7",
|
||||
"@types/node": "^22.5.2",
|
||||
"@types/node": "^22.7.5",
|
||||
"@types/sinon": "^17.0.3",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@types/ws": "^8.5.11",
|
||||
@@ -45,7 +46,7 @@
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"tsx": "^4.17.0",
|
||||
"typescript": "^5.5.4",
|
||||
"typescript": "^5.6.3",
|
||||
"webpack": "^5.91.0",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"webpack-dev-server": "^5.0.4"
|
||||
@@ -67,6 +68,7 @@
|
||||
"googleapis": "^143.0.0",
|
||||
"hammerjs": "^2.0.8",
|
||||
"jimp": "^0.22.12",
|
||||
"lit": "^3.2.1",
|
||||
"msgpack5": "^6.0.2",
|
||||
"node-addon-api": "^8.1.0",
|
||||
"node-gyp": "^10.2.0",
|
||||
|
||||
@@ -1,242 +0,0 @@
|
||||
import {Config, getConfig} from "../core/configuration/Config";
|
||||
import {GameID, Lobby, ServerMessage, ServerMessageSchema} from "../core/Schemas";
|
||||
import {loadTerrainMap, TerrainMap} from "../core/game/TerrainMapLoader";
|
||||
import {ClientGame, createClientGame} from "./ClientGame";
|
||||
import backgroundImage from '../../resources/images/TerrainMapFrontPage.png';
|
||||
import favicon from '../../resources/images/Favicon.png';
|
||||
import {v4 as uuidv4} from 'uuid';
|
||||
|
||||
|
||||
import './styles.css';
|
||||
import {simpleHash} from "../core/Util";
|
||||
import {PseudoRandom} from "../core/PseudoRandom";
|
||||
|
||||
const usernameKey: string = 'username';
|
||||
|
||||
|
||||
class Client {
|
||||
|
||||
|
||||
private terrainMap: Promise<TerrainMap>
|
||||
private game: ClientGame
|
||||
private lobbiesInterval: NodeJS.Timeout | null = null;
|
||||
private isLobbyHighlighted: boolean = false;
|
||||
|
||||
private ip: Promise<string | null> = null
|
||||
|
||||
private config: Config
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
initialize(): void {
|
||||
const storedUsername = localStorage.getItem(usernameKey)
|
||||
if (storedUsername) {
|
||||
const usernameInput = document.getElementById('username') as HTMLInputElement | null;
|
||||
if (usernameInput) {
|
||||
usernameInput.value = storedUsername
|
||||
}
|
||||
}
|
||||
this.config = getConfig()
|
||||
setFavicon()
|
||||
this.terrainMap = loadTerrainMap()
|
||||
this.startLobbyPolling()
|
||||
this.ip = getClientIP()
|
||||
}
|
||||
|
||||
private startLobbyPolling(): void {
|
||||
this.fetchAndUpdateLobbies(); // Fetch immediately on start
|
||||
this.lobbiesInterval = setInterval(() => this.fetchAndUpdateLobbies(), 1000);
|
||||
}
|
||||
|
||||
private async fetchAndUpdateLobbies(): Promise<void> {
|
||||
try {
|
||||
const lobbies = await this.fetchLobbies();
|
||||
this.updateLobbiesDisplay(lobbies);
|
||||
} catch (error) {
|
||||
console.error('Error fetching and updating lobbies:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private updateLobbiesDisplay(lobbies: Lobby[]): void {
|
||||
if (lobbies.length === 0) {
|
||||
document.getElementById('lobby-button').style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
const lobby = lobbies[0];
|
||||
const lobbyButton = document.getElementById('lobby-button');
|
||||
const nameElement = document.getElementById('lobby-name');
|
||||
const timerElement = document.getElementById('lobby-timer');
|
||||
const playerCountElement = document.getElementById('player-count');
|
||||
|
||||
if (lobbyButton) {
|
||||
lobbyButton.style.display = 'flex';
|
||||
lobbyButton.onclick = () => this.joinLobby(lobby);
|
||||
|
||||
// Preserve the highlighted state
|
||||
lobbyButton.classList.toggle('highlighted', this.isLobbyHighlighted);
|
||||
}
|
||||
|
||||
if (nameElement) nameElement.textContent = `Game ${lobby.id.substring(0, 3)}`;
|
||||
if (timerElement) {
|
||||
const timeRemaining = Math.max(0, Math.floor((lobby.msUntilStart) / 1000));
|
||||
timerElement.textContent = `Starts in: ${timeRemaining}s`;
|
||||
}
|
||||
|
||||
if (playerCountElement) playerCountElement.textContent = `Players: ${lobby.numClients}`;
|
||||
|
||||
if (lobbies.length > 1) {
|
||||
const nextLobby = lobbies[1]
|
||||
const nextGame = document.getElementById('next-game');
|
||||
nextGame.textContent = `Next Game: ${nextLobby.id.substring(0, 3)}`
|
||||
}
|
||||
}
|
||||
|
||||
async fetchLobbies(): Promise<Lobby[]> {
|
||||
const url = '/lobbies';
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}, statusText: ${response.statusText}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
return data.lobbies;
|
||||
} catch (error) {
|
||||
console.error('Error fetching lobbies:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async joinLobby(lobby: Lobby) {
|
||||
console.log(`joining lobby ${lobby.id}`)
|
||||
const lobbyButton = document.getElementById('lobby-button');
|
||||
if (lobbyButton) {
|
||||
this.isLobbyHighlighted = !this.isLobbyHighlighted;
|
||||
lobbyButton.classList.toggle('highlighted', this.isLobbyHighlighted);
|
||||
}
|
||||
if (!this.isLobbyHighlighted) {
|
||||
this.game.stop()
|
||||
this.game = null
|
||||
return
|
||||
}
|
||||
|
||||
if (this.game != null) {
|
||||
return;
|
||||
}
|
||||
const [terrainMap, clientIP] = await Promise.all([
|
||||
this.terrainMap,
|
||||
this.ip
|
||||
]);
|
||||
console.log(`got ip ${clientIP}`)
|
||||
this.game = createClientGame(
|
||||
refreshUsername,
|
||||
uuidv4(),
|
||||
uuidv4(),
|
||||
clientIP,
|
||||
lobby.id,
|
||||
this.config,
|
||||
terrainMap
|
||||
);
|
||||
this.game.join();
|
||||
const g = this.game;
|
||||
window.addEventListener('beforeunload', function (event) {
|
||||
console.log('Browser is closing');
|
||||
g.stop();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function refreshUsername(): string {
|
||||
const usernameInput = document.getElementById('username') as HTMLInputElement | null;
|
||||
if (usernameInput == null) {
|
||||
console.warn('username element not found')
|
||||
return "Anon"
|
||||
}
|
||||
if (usernameInput && usernameInput.value.trim()) {
|
||||
const trimmedValue = usernameInput.value.trim();
|
||||
localStorage.setItem(usernameKey, trimmedValue)
|
||||
return trimmedValue
|
||||
} else {
|
||||
const storedUsername = localStorage.getItem(usernameKey);
|
||||
if (storedUsername) {
|
||||
return storedUsername
|
||||
}
|
||||
const newUsername = "Anon" + uuidToThreeDigits()
|
||||
localStorage.setItem(usernameKey, newUsername)
|
||||
return newUsername
|
||||
}
|
||||
}
|
||||
|
||||
function setupUsernameCallback(callback: (username: string) => void): void {
|
||||
const usernameInput = document.getElementById('username') as HTMLInputElement | null;
|
||||
if (usernameInput) {
|
||||
usernameInput.addEventListener('input', () => {
|
||||
const username = refreshUsername();
|
||||
callback(username);
|
||||
});
|
||||
} else {
|
||||
console.error('Username input element not found');
|
||||
}
|
||||
}
|
||||
|
||||
async function getClientIP(timeoutMs: number = 1000): Promise<string | null> {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
||||
|
||||
try {
|
||||
const response: Response = await fetch('https://api.ipify.org?format=json', {
|
||||
signal: controller.signal
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data: {ip: string} = await response.json();
|
||||
return data.ip;
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
if (error.name === 'AbortError') {
|
||||
console.error('Request timed out');
|
||||
} else {
|
||||
console.error('Error fetching IP:', error.message);
|
||||
}
|
||||
} else {
|
||||
console.error('An unknown error occurred');
|
||||
}
|
||||
return null;
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the client when the DOM is loaded
|
||||
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);
|
||||
}
|
||||
|
||||
function uuidToThreeDigits(): string {
|
||||
const uuid = uuidv4()
|
||||
// Remove hyphens and convert to lowercase
|
||||
const cleanUuid = uuid.replace(/-/g, '').toLowerCase();
|
||||
|
||||
// Convert hex string to decimal
|
||||
const decimal = BigInt(`0x${cleanUuid}`);
|
||||
|
||||
// Get last 3 digits
|
||||
const threeDigits = decimal % 1000n;
|
||||
|
||||
// Pad with leading zeros if necessary
|
||||
return threeDigits.toString().padStart(3, '0');
|
||||
}
|
||||
+21
-13
@@ -1,43 +1,52 @@
|
||||
import {Executor} from "../core/execution/ExecutionManager";
|
||||
import {Cell, MutableGame, PlayerEvent, PlayerID, MutablePlayer, TileEvent, Player, Game, BoatEvent, Tile, PlayerType} from "../core/game/Game";
|
||||
import {Cell, MutableGame, PlayerEvent, PlayerID, MutablePlayer, TileEvent, Player, Game, BoatEvent, Tile, PlayerType, GameMap} from "../core/game/Game";
|
||||
import {createGame} from "../core/game/GameImpl";
|
||||
import {EventBus} from "../core/EventBus";
|
||||
import {Config} from "../core/configuration/Config";
|
||||
import {Config, getConfig} from "../core/configuration/Config";
|
||||
import {createRenderer, GameRenderer} from "./graphics/GameRenderer";
|
||||
import {InputHandler, MouseUpEvent, ZoomEvent, DragEvent, MouseDownEvent} from "./InputHandler"
|
||||
import {ClientID, ClientIntentMessageSchema, ClientJoinMessageSchema, ClientLeaveMessageSchema, ClientMessageSchema, GameID, Intent, ServerMessage, ServerMessageSchema, ServerSyncMessage, Turn} from "../core/Schemas";
|
||||
import {TerrainMap} from "../core/game/TerrainMapLoader";
|
||||
import {loadTerrainMap, TerrainMap} from "../core/game/TerrainMapLoader";
|
||||
import {and, bfs, dist, manhattanDist} from "../core/Util";
|
||||
import {TerrainLayer} from "./graphics/layers/TerrainLayer";
|
||||
import {WinCheckExecution} from "../core/execution/WinCheckExecution";
|
||||
import {SendAttackIntentEvent, SendSpawnIntentEvent, Transport} from "./Transport";
|
||||
import {createCanvas} from "./graphics/Utils";
|
||||
import {DisplayMessageEvent, MessageType} from "./graphics/layers/EventsDisplay";
|
||||
import {placeName} from "./graphics/NameBoxCalculator";
|
||||
import {v4 as uuidv4} from 'uuid';
|
||||
|
||||
|
||||
export interface GameConfig {
|
||||
isLocal: boolean
|
||||
playerName: () => string
|
||||
gameID: GameID
|
||||
ip: string | null
|
||||
map: GameMap
|
||||
}
|
||||
|
||||
export function createClientGame(playerName: () => string, clientID: ClientID, playerID: PlayerID, ip: string | null, gameID: GameID, config: Config, terrainMap: TerrainMap): ClientGame {
|
||||
export async function createClientGame(gameConfig: GameConfig): Promise<ClientGame> {
|
||||
let eventBus = new EventBus()
|
||||
const config = getConfig()
|
||||
|
||||
const clientID = uuidv4()
|
||||
const playerID = uuidv4()
|
||||
|
||||
const terrainMap = await loadTerrainMap(gameConfig.map)
|
||||
|
||||
let game = createGame(terrainMap, eventBus, config)
|
||||
const canvas = createCanvas()
|
||||
let gameRenderer = createRenderer(canvas, game, eventBus, clientID)
|
||||
|
||||
|
||||
|
||||
const transport = new Transport(null, eventBus, gameID, clientID, playerID, playerName)
|
||||
const transport = new Transport(gameConfig.isLocal, eventBus, gameConfig.gameID, clientID, playerID, config, gameConfig.playerName)
|
||||
|
||||
|
||||
return new ClientGame(
|
||||
clientID,
|
||||
ip,
|
||||
gameID,
|
||||
gameConfig.ip,
|
||||
eventBus,
|
||||
game,
|
||||
gameRenderer,
|
||||
new InputHandler(canvas, eventBus),
|
||||
new Executor(game, gameID),
|
||||
new Executor(game, gameConfig.gameID),
|
||||
transport,
|
||||
)
|
||||
}
|
||||
@@ -56,7 +65,6 @@ export class ClientGame {
|
||||
constructor(
|
||||
private id: ClientID,
|
||||
private clientIP: string | null,
|
||||
private gameID: GameID,
|
||||
private eventBus: EventBus,
|
||||
private gs: Game,
|
||||
private renderer: GameRenderer,
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import {Config} from "../core/configuration/Config";
|
||||
import {ClientMessage, ClientMessageSchema, Intent, ServerMessage, ServerTurnMessageSchema, Turn} from "../core/Schemas";
|
||||
|
||||
export class LocalServer {
|
||||
|
||||
private gameID = "LOCAL"
|
||||
|
||||
|
||||
private turns: Turn[] = []
|
||||
private intents: Intent[] = []
|
||||
|
||||
private endTurnIntervalID
|
||||
|
||||
constructor(private config: Config, private clientConnect: () => void, private clientMessage: (message: ServerMessage) => void) {
|
||||
}
|
||||
|
||||
start() {
|
||||
this.endTurnIntervalID = setInterval(() => this.endTurn(), this.config.turnIntervalMs());
|
||||
this.clientConnect()
|
||||
this.clientMessage({
|
||||
type: "start",
|
||||
turns: [],
|
||||
})
|
||||
}
|
||||
|
||||
onMessage(message: string) {
|
||||
const clientMsg: ClientMessage = ClientMessageSchema.parse(JSON.parse(message))
|
||||
if (clientMsg.type == "intent") {
|
||||
this.intents.push(clientMsg.intent)
|
||||
}
|
||||
}
|
||||
|
||||
private endTurn() {
|
||||
const pastTurn: Turn = {
|
||||
turnNumber: this.turns.length,
|
||||
gameID: this.gameID,
|
||||
intents: this.intents
|
||||
}
|
||||
this.turns.push(pastTurn)
|
||||
this.intents = []
|
||||
this.clientMessage({
|
||||
type: "turn",
|
||||
turn: pastTurn
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import {ClientGame, createClientGame} from "./ClientGame";
|
||||
import backgroundImage from '../../resources/images/TerrainMapFrontPage.png';
|
||||
import favicon from '../../resources/images/Favicon.png';
|
||||
|
||||
import './PublicLobby';
|
||||
import './UsernameInput';
|
||||
|
||||
|
||||
import './styles.css';
|
||||
import {UsernameInput} from "./UsernameInput";
|
||||
import {SinglePlayerModal} from "./SinglePlayerModal";
|
||||
import {GameMap} from "../core/game/Game";
|
||||
|
||||
|
||||
const usernameKey: string = 'username';
|
||||
|
||||
|
||||
class Client {
|
||||
private game: ClientGame
|
||||
|
||||
private ip: Promise<string | null> = null
|
||||
|
||||
private usernameInput: UsernameInput | null = null;
|
||||
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
initialize(): void {
|
||||
this.usernameInput = document.querySelector('username-input') as UsernameInput;
|
||||
if (!this.usernameInput) {
|
||||
console.warn('Username input element not found');
|
||||
}
|
||||
|
||||
setFavicon()
|
||||
this.ip = getClientIP()
|
||||
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 singlePlayerButton = document.getElementById('single-player');
|
||||
const modal = document.querySelector('single-player-modal') as SinglePlayerModal;
|
||||
|
||||
if (singlePlayerButton && modal instanceof SinglePlayerModal) {
|
||||
singlePlayerButton.addEventListener('click', () => {
|
||||
modal.open();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private async handleJoinLobby(event: CustomEvent) {
|
||||
const lobby = event.detail.lobby
|
||||
console.log(`joining lobby ${lobby.id}`)
|
||||
const clientIP = await this.ip
|
||||
console.log(`got ip ${clientIP}`)
|
||||
if (this.game != null) {
|
||||
this.game.stop()
|
||||
}
|
||||
this.game = await createClientGame(
|
||||
{
|
||||
isLocal: event.detail.singlePlayer,
|
||||
playerName: (): string => this.usernameInput.getCurrentUsername(),
|
||||
gameID: lobby.id,
|
||||
ip: clientIP,
|
||||
map: event.detail.map,
|
||||
}
|
||||
);
|
||||
this.game.join();
|
||||
const g = this.game;
|
||||
window.addEventListener('beforeunload', function (event) {
|
||||
console.log('Browser is closing');
|
||||
g.stop();
|
||||
});
|
||||
}
|
||||
|
||||
private async handleLeaveLobby(event: CustomEvent) {
|
||||
this.game.stop()
|
||||
this.game = null
|
||||
}
|
||||
|
||||
private async handleSinglePlayer(event: CustomEvent) {
|
||||
alert('coming soon')
|
||||
}
|
||||
}
|
||||
|
||||
async function getClientIP(timeoutMs: number = 1000): Promise<string | null> {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
||||
|
||||
try {
|
||||
const response: Response = await fetch('https://api.ipify.org?format=json', {
|
||||
signal: controller.signal
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data: {ip: string} = await response.json();
|
||||
return data.ip;
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
if (error.name === 'AbortError') {
|
||||
console.error('Request timed out');
|
||||
} else {
|
||||
console.error('Error fetching IP:', error.message);
|
||||
}
|
||||
} else {
|
||||
console.error('An unknown error occurred');
|
||||
}
|
||||
return null;
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the client when the DOM is loaded
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import {LitElement, html, css} from 'lit';
|
||||
import {customElement, state} from 'lit/decorators.js';
|
||||
import {Lobby} from "../core/Schemas";
|
||||
import {GameMap} from '../core/game/Game';
|
||||
|
||||
@customElement('public-lobby')
|
||||
export class PublicLobby extends LitElement {
|
||||
@state() private lobbies: Lobby[] = [];
|
||||
@state() private isLobbyHighlighted: boolean = false;
|
||||
private lobbiesInterval: number | null = null;
|
||||
|
||||
private currLobby: Lobby = null
|
||||
|
||||
static styles = css`
|
||||
/* Add your styles here, based on your existing CSS */
|
||||
.lobby-button {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
max-width: 20rem;
|
||||
margin: 0 auto;
|
||||
padding: 1.5rem 2rem;
|
||||
background-color: #2563eb;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
border-radius: 0.5rem;
|
||||
transition: background-color 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.lobby-button:hover {
|
||||
background-color: #1d4ed8;
|
||||
}
|
||||
|
||||
.lobby-button.highlighted {
|
||||
background-color: #16a34a;
|
||||
}
|
||||
|
||||
.lobby-button.highlighted:hover {
|
||||
background-color: #15803d;
|
||||
}
|
||||
|
||||
.lobby-name { font-size: 1.5rem; }
|
||||
.lobby-timer { font-size: 1.25rem; }
|
||||
.player-count { font-size: 1rem; }
|
||||
`;
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.fetchAndUpdateLobbies(); // Fetch immediately on start
|
||||
this.lobbiesInterval = window.setInterval(() => this.fetchAndUpdateLobbies(), 1000);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
if (this.lobbiesInterval !== null) {
|
||||
clearInterval(this.lobbiesInterval);
|
||||
this.lobbiesInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchAndUpdateLobbies(): Promise<void> {
|
||||
try {
|
||||
const lobbies = await this.fetchLobbies();
|
||||
this.lobbies = lobbies;
|
||||
} catch (error) {
|
||||
console.error('Error fetching and updating lobbies:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async fetchLobbies(): Promise<Lobby[]> {
|
||||
const url = '/lobbies';
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
return data.lobbies;
|
||||
} catch (error) {
|
||||
console.error('Error fetching lobbies:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.lobbies.length === 0) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const lobby = this.lobbies[0];
|
||||
const timeRemaining = Math.max(0, Math.floor(lobby.msUntilStart / 1000));
|
||||
|
||||
return html`
|
||||
<button
|
||||
@click=${() => this.lobbyClicked(lobby)}
|
||||
class="lobby-button ${this.isLobbyHighlighted ? 'highlighted' : ''}"
|
||||
>
|
||||
<div class="lobby-name">Game ${lobby.id.substring(0, 3)}</div>
|
||||
<div class="lobby-timer">Starts in: ${timeRemaining}s</div>
|
||||
<div class="player-count">Players: ${lobby.numClients}</div>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
private lobbyClicked(lobby: Lobby) {
|
||||
this.isLobbyHighlighted = !this.isLobbyHighlighted;
|
||||
if (this.currLobby == null) {
|
||||
this.currLobby = lobby
|
||||
this.dispatchEvent(new CustomEvent('join-lobby', {
|
||||
detail: {
|
||||
lobby: lobby,
|
||||
singlePlayer: false,
|
||||
map: GameMap.World,
|
||||
},
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
} else {
|
||||
this.dispatchEvent(new CustomEvent('leave-lobby', {
|
||||
detail: {lobby: this.currLobby},
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
this.currLobby = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import {LitElement, html, css} from 'lit';
|
||||
import {customElement, property, state} from 'lit/decorators.js';
|
||||
import {GameMap} from '../core/game/Game';
|
||||
|
||||
@customElement('single-player-modal')
|
||||
export class SinglePlayerModal extends LitElement {
|
||||
@state() private isModalOpen = false;
|
||||
@state() private selectedMap: GameMap = GameMap.World;
|
||||
|
||||
static styles = css`
|
||||
.modal-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: white;
|
||||
margin: 15% auto;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
width: 80%;
|
||||
max-width: 500px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.close {
|
||||
color: #aaa;
|
||||
float: right;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.close:hover,
|
||||
.close:focus {
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 10px 20px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.3s;
|
||||
display: inline-block;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
select {
|
||||
padding: 8px;
|
||||
font-size: 16px;
|
||||
margin-top: 10px;
|
||||
width: 200px;
|
||||
}
|
||||
`;
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="modal-overlay" style="display: ${this.isModalOpen ? 'block' : 'none'}">
|
||||
<div class="modal-content">
|
||||
<span class="close" @click=${this.close}>×</span>
|
||||
<h2>Start Single Player Game</h2>
|
||||
<div>
|
||||
<label for="map-select">Map: </label>
|
||||
<select id="map-select" @change=${this.handleMapChange}>
|
||||
${Object.entries(GameMap)
|
||||
.filter(([key]) => isNaN(Number(key)))
|
||||
.map(([key, value]) => html`
|
||||
<option value=${value} ?selected=${this.selectedMap === value}>
|
||||
${key}
|
||||
</option>
|
||||
`)}
|
||||
</select>
|
||||
</div>
|
||||
<button @click=${this.startGame}>Start Game</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
public open() {
|
||||
this.isModalOpen = true;
|
||||
}
|
||||
|
||||
public close() {
|
||||
this.isModalOpen = false;
|
||||
}
|
||||
|
||||
private handleMapChange(e: Event) {
|
||||
this.selectedMap = Number((e.target as HTMLSelectElement).value) as GameMap;
|
||||
}
|
||||
|
||||
private startGame() {
|
||||
console.log(`Starting single player game with map: ${GameMap[this.selectedMap]}`);
|
||||
this.dispatchEvent(new CustomEvent('join-lobby', {
|
||||
detail: {
|
||||
singlePlayer: true,
|
||||
lobby: {
|
||||
id: "LOCAL",
|
||||
},
|
||||
map: this.selectedMap,
|
||||
},
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
+33
-7
@@ -1,6 +1,8 @@
|
||||
import {Config} from "../core/configuration/Config"
|
||||
import {EventBus, GameEvent} from "../core/EventBus"
|
||||
import {AllianceRequest, AllPlayers, Cell, Player, PlayerID, PlayerType} from "../core/game/Game"
|
||||
import {ClientID, ClientIntentMessageSchema, ClientJoinMessageSchema, ClientLeaveMessageSchema, GameID, Intent, ServerMessage, ServerMessageSchema} from "../core/Schemas"
|
||||
import {LocalServer} from "./LocalServer"
|
||||
|
||||
|
||||
export class SendAllianceRequestIntentEvent implements GameEvent {
|
||||
@@ -67,14 +69,17 @@ export class SendDonateIntentEvent implements GameEvent {
|
||||
|
||||
export class Transport {
|
||||
|
||||
public onconnect: () => {}
|
||||
private socket: WebSocket
|
||||
|
||||
private localServer: LocalServer
|
||||
|
||||
constructor(
|
||||
public socket: WebSocket,
|
||||
private isLocal: boolean,
|
||||
private eventBus: EventBus,
|
||||
private gameID: GameID,
|
||||
private clientID: ClientID,
|
||||
private playerID: PlayerID,
|
||||
private config: Config,
|
||||
private playerName: () => string,
|
||||
) {
|
||||
this.eventBus.on(SendAllianceRequestIntentEvent, (e) => this.onSendAllianceRequest(e))
|
||||
@@ -89,6 +94,19 @@ export class Transport {
|
||||
}
|
||||
|
||||
connect(onconnect: () => void, onmessage: (message: ServerMessage) => void, isActive: () => boolean) {
|
||||
if (this.isLocal) {
|
||||
this.connectLocal(onconnect, onmessage, isActive)
|
||||
} else {
|
||||
this.connectRemote(onconnect, onmessage, isActive)
|
||||
}
|
||||
}
|
||||
|
||||
private connectLocal(onconnect: () => void, onmessage: (message: ServerMessage) => void, isActive: () => boolean) {
|
||||
this.localServer = new LocalServer(this.config, onconnect, onmessage)
|
||||
this.localServer.start()
|
||||
}
|
||||
|
||||
private connectRemote(onconnect: () => void, onmessage: (message: ServerMessage) => void, isActive: () => boolean) {
|
||||
const wsHost = process.env.WEBSOCKET_URL || window.location.host;
|
||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
this.socket = new WebSocket(`${wsProtocol}//${wsHost}`)
|
||||
@@ -115,7 +133,7 @@ export class Transport {
|
||||
}
|
||||
|
||||
joinGame(clientIP: string | null, numTurns: number) {
|
||||
this.socket.send(
|
||||
this.sendMsg(
|
||||
JSON.stringify(
|
||||
ClientJoinMessageSchema.parse({
|
||||
type: "join",
|
||||
@@ -136,7 +154,7 @@ export class Transport {
|
||||
clientID: this.clientID,
|
||||
gameID: this.gameID,
|
||||
})
|
||||
this.socket.send(JSON.stringify(msg))
|
||||
this.sendMsg(JSON.stringify(msg))
|
||||
} else {
|
||||
console.log('WebSocket is not open. Current state:', this.socket.readyState);
|
||||
console.log('attempting reconnect')
|
||||
@@ -239,17 +257,25 @@ export class Transport {
|
||||
}
|
||||
|
||||
private sendIntent(intent: Intent) {
|
||||
if (this.socket.readyState === WebSocket.OPEN) {
|
||||
if (this.isLocal || this.socket.readyState === WebSocket.OPEN) {
|
||||
const msg = ClientIntentMessageSchema.parse({
|
||||
type: "intent",
|
||||
clientID: this.clientID,
|
||||
gameID: this.gameID,
|
||||
intent: intent
|
||||
})
|
||||
this.socket.send(JSON.stringify(msg))
|
||||
this.sendMsg(JSON.stringify(msg))
|
||||
} else {
|
||||
console.log('WebSocket is not open. Current state:', this.socket.readyState);
|
||||
console.log('attempting reconnect')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sendMsg(msg: string) {
|
||||
if (this.isLocal) {
|
||||
this.localServer.onMessage(msg)
|
||||
} else {
|
||||
this.socket.send(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import {LitElement, html, css} from 'lit';
|
||||
import {customElement, property, state} from 'lit/decorators.js';
|
||||
import {v4 as uuidv4} from 'uuid';
|
||||
|
||||
const usernameKey: string = 'username';
|
||||
|
||||
@customElement('username-input')
|
||||
export class UsernameInput extends LitElement {
|
||||
@state() private username: string = '';
|
||||
|
||||
static styles = css`
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
background-color: white;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.375rem;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
ring: 2px;
|
||||
ring-color: #3b82f6;
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
`;
|
||||
|
||||
public getCurrentUsername(): string {
|
||||
return this.username;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.username = this.getStoredUsername();
|
||||
this.dispatchUsernameEvent()
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<input
|
||||
type="text"
|
||||
.value=${this.username}
|
||||
@input=${this.handleInput}
|
||||
placeholder="Enter your username"
|
||||
maxlength="18"
|
||||
>
|
||||
`;
|
||||
}
|
||||
|
||||
private handleInput(e: Event) {
|
||||
const input = e.target as HTMLInputElement;
|
||||
this.username = input.value.trim();
|
||||
this.storeUsername(this.username);
|
||||
this.dispatchUsernameEvent()
|
||||
}
|
||||
|
||||
private getStoredUsername(): string {
|
||||
const storedUsername = localStorage.getItem(usernameKey);
|
||||
if (storedUsername) {
|
||||
return storedUsername;
|
||||
}
|
||||
return this.generateNewUsername();
|
||||
}
|
||||
|
||||
private storeUsername(username: string) {
|
||||
if (username) {
|
||||
localStorage.setItem(usernameKey, username);
|
||||
}
|
||||
}
|
||||
|
||||
private dispatchUsernameEvent() {
|
||||
this.dispatchEvent(new CustomEvent('username-change', {
|
||||
detail: {username: this.username},
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
private generateNewUsername(): string {
|
||||
const newUsername = "Anon" + this.uuidToThreeDigits();
|
||||
this.storeUsername(newUsername);
|
||||
return newUsername;
|
||||
}
|
||||
|
||||
private uuidToThreeDigits(): string {
|
||||
const uuid = uuidv4();
|
||||
const cleanUuid = uuid.replace(/-/g, '').toLowerCase();
|
||||
const decimal = BigInt(`0x${cleanUuid}`);
|
||||
const threeDigits = decimal % 1000n;
|
||||
return threeDigits.toString().padStart(3, '0');
|
||||
}
|
||||
}
|
||||
@@ -8,19 +8,33 @@ import {EventBus} from "../../core/EventBus";
|
||||
import {TransformHandler} from "./TransformHandler";
|
||||
import {Layer} from "./layers/Layer";
|
||||
import {EventsDisplay} from "./layers/EventsDisplay";
|
||||
import {RadialMenu} from "./layers/RadialMenu";
|
||||
import {RadialMenu} from "./layers/radial/RadialMenu";
|
||||
import {EmojiTable} from "./layers/radial/EmojiTable";
|
||||
import {Leaderboard} from "./layers/Leaderboard";
|
||||
|
||||
|
||||
export function createRenderer(canvas: HTMLCanvasElement, game: Game, eventBus: EventBus, clientID: ClientID): GameRenderer {
|
||||
const transformHandler = new TransformHandler(game, eventBus, canvas)
|
||||
|
||||
const emojiTable = document.querySelector('emoji-table') as EmojiTable;
|
||||
if (!emojiTable || !(emojiTable instanceof EmojiTable)) {
|
||||
console.error('EmojiTable element not found in the DOM');
|
||||
}
|
||||
|
||||
const leaderboard = document.querySelector('leader-board') as Leaderboard;
|
||||
if (!emojiTable || !(leaderboard instanceof Leaderboard)) {
|
||||
console.error('EmojiTable element not found in the DOM');
|
||||
}
|
||||
leaderboard.clientID = clientID
|
||||
|
||||
const layers: Layer[] = [
|
||||
new TerrainLayer(game),
|
||||
new TerritoryLayer(game, eventBus),
|
||||
new NameLayer(game, game.config().theme(), transformHandler, clientID),
|
||||
new UILayer(eventBus, game, clientID, transformHandler),
|
||||
new EventsDisplay(eventBus, game, clientID),
|
||||
new RadialMenu(eventBus, game, transformHandler, clientID),
|
||||
new RadialMenu(eventBus, game, transformHandler, clientID, emojiTable as EmojiTable),
|
||||
leaderboard,
|
||||
]
|
||||
|
||||
return new GameRenderer(game, eventBus, canvas, transformHandler, layers)
|
||||
@@ -36,7 +50,7 @@ export class GameRenderer {
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.layers.forEach(l => l.init())
|
||||
this.layers.forEach(l => l.init(this.game))
|
||||
|
||||
document.body.appendChild(this.canvas);
|
||||
window.addEventListener('resize', () => this.resizeCanvas());
|
||||
@@ -65,7 +79,7 @@ export class GameRenderer {
|
||||
|
||||
this.layers.forEach(l => {
|
||||
if (l.shouldTransform()) {
|
||||
l.render(this.context)
|
||||
l.renderLayer(this.context)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -73,7 +87,7 @@ export class GameRenderer {
|
||||
|
||||
this.layers.forEach(l => {
|
||||
if (!l.shouldTransform()) {
|
||||
l.render(this.context)
|
||||
l.renderLayer(this.context)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -263,7 +263,7 @@ export class EventsDisplay implements Layer {
|
||||
this.events[index] = event;
|
||||
}
|
||||
|
||||
render(): void { }
|
||||
renderLayer(): void { }
|
||||
|
||||
renderTable(): void {
|
||||
if (this.events.length === 0) {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import {Game} from "../../../core/game/Game"
|
||||
import {TransformHandler} from "../TransformHandler"
|
||||
|
||||
export interface Layer {
|
||||
init()
|
||||
init(game: Game)
|
||||
tick()
|
||||
render(context: CanvasRenderingContext2D)
|
||||
renderLayer(context: CanvasRenderingContext2D)
|
||||
shouldTransform(): boolean
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
import {LitElement, html, css} from 'lit';
|
||||
import {customElement, property, state} from 'lit/decorators.js';
|
||||
import {Layer} from './Layer';
|
||||
import {Game, Player} from '../../../core/game/Game';
|
||||
import {ClientID} from '../../../core/Schemas';
|
||||
|
||||
interface Entry {
|
||||
name: string
|
||||
position: number
|
||||
score: number
|
||||
isMyPlayer: boolean
|
||||
}
|
||||
|
||||
@customElement('leader-board')
|
||||
export class Leaderboard extends LitElement implements Layer {
|
||||
|
||||
private game: Game
|
||||
public clientID: ClientID
|
||||
|
||||
init(game: Game) {
|
||||
this.game = game
|
||||
}
|
||||
|
||||
tick() {
|
||||
if (this._hidden && !this.game.inSpawnPhase()) {
|
||||
this.showLeaderboard()
|
||||
this.updateLeaderboard()
|
||||
}
|
||||
if (this._hidden) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.game.ticks() % 10 == 0) {
|
||||
this.updateLeaderboard()
|
||||
}
|
||||
}
|
||||
|
||||
private updateLeaderboard() {
|
||||
if (this.clientID == null) {
|
||||
return
|
||||
}
|
||||
const myPlayer = this.game.players().find(p => p.clientID() == this.clientID)
|
||||
if (myPlayer == null) {
|
||||
return
|
||||
}
|
||||
|
||||
const sorted = this.game.players()
|
||||
.sort((a, b) => b.numTilesOwned() - a.numTilesOwned())
|
||||
|
||||
this.players = sorted
|
||||
.slice(0, 5)
|
||||
.map((player, index) => ({
|
||||
name: player.name(),
|
||||
position: index + 1,
|
||||
score: player.numTilesOwned(),
|
||||
isMyPlayer: player == myPlayer
|
||||
}));
|
||||
|
||||
if (this.players.find(p => p.isMyPlayer) == null) {
|
||||
let place = 0
|
||||
for (const p of sorted) {
|
||||
place++
|
||||
if (p == myPlayer) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
this.players.pop()
|
||||
this.players.push({
|
||||
name: myPlayer.name(),
|
||||
position: place,
|
||||
score: myPlayer.numTilesOwned(),
|
||||
isMyPlayer: true,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
this.requestUpdate()
|
||||
}
|
||||
|
||||
renderLayer(context: CanvasRenderingContext2D) {
|
||||
}
|
||||
shouldTransform(): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
.leaderboard {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
z-index: 9999;
|
||||
background-color: #1E1E1E;
|
||||
padding: 15px;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
|
||||
border-radius: 10px;
|
||||
max-width: 300px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
width: 300px;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
th, td {
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #333;
|
||||
color: white;
|
||||
}
|
||||
th {
|
||||
background-color: #2C2C2C;
|
||||
color: white;
|
||||
}
|
||||
.myPlayer {
|
||||
font-weight: bold;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
tr:nth-child(even) {
|
||||
background-color: #2C2C2C;
|
||||
}
|
||||
tr:hover {
|
||||
background-color: #3A3A3A;
|
||||
}
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
`;
|
||||
|
||||
@property({type: Array})
|
||||
players: Entry[] = [];
|
||||
|
||||
@state()
|
||||
private _hidden = true;
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="leaderboard ${this._hidden ? 'hidden' : ''}">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Rank</th>
|
||||
<th>Player</th>
|
||||
<th>Score</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${this.players
|
||||
.map((player, index) => html`
|
||||
<tr class="${player.isMyPlayer ? 'myPlayer' : 'none'}">
|
||||
<td>${player.position}</td>
|
||||
<td>${player.name.slice(0, 12)}</td>
|
||||
<td>${player.score}</td>
|
||||
</tr>
|
||||
`)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
hideLeaderboard() {
|
||||
this._hidden = true;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
showLeaderboard() {
|
||||
this._hidden = false;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
get isVisible() {
|
||||
return !this._hidden;
|
||||
}
|
||||
}
|
||||
@@ -60,7 +60,7 @@ export class NameLayer implements Layer {
|
||||
return true
|
||||
}
|
||||
|
||||
public init() {
|
||||
public init(game: Game) {
|
||||
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ export class NameLayer implements Layer {
|
||||
}
|
||||
}
|
||||
|
||||
public render(mainContex: CanvasRenderingContext2D) {
|
||||
public renderLayer(mainContex: CanvasRenderingContext2D) {
|
||||
const [upperLeft, bottomRight] = this.transformHandler.screenBoundingRect()
|
||||
for (const render of this.renders) {
|
||||
render.isVisible = this.isVisible(render, upperLeft, bottomRight)
|
||||
|
||||
@@ -17,7 +17,7 @@ export class TerrainLayer implements Layer {
|
||||
tick() {
|
||||
}
|
||||
|
||||
init() {
|
||||
init(game: Game) {
|
||||
this.canvas = document.createElement('canvas');
|
||||
this.context = this.canvas.getContext("2d")
|
||||
|
||||
@@ -41,7 +41,7 @@ export class TerrainLayer implements Layer {
|
||||
})
|
||||
}
|
||||
|
||||
render(context: CanvasRenderingContext2D) {
|
||||
renderLayer(context: CanvasRenderingContext2D) {
|
||||
context.drawImage(
|
||||
this.canvas,
|
||||
-this.game.width() / 2,
|
||||
|
||||
@@ -31,7 +31,7 @@ export class TerritoryLayer implements Layer {
|
||||
tick() {
|
||||
}
|
||||
|
||||
init() {
|
||||
init(game: Game) {
|
||||
this.canvas = document.createElement('canvas');
|
||||
this.context = this.canvas.getContext("2d")
|
||||
|
||||
@@ -50,7 +50,7 @@ export class TerritoryLayer implements Layer {
|
||||
})
|
||||
}
|
||||
|
||||
render(context: CanvasRenderingContext2D) {
|
||||
renderLayer(context: CanvasRenderingContext2D) {
|
||||
this.renderTerritory()
|
||||
this.context.putImageData(this.imageData, 0, 0);
|
||||
context.drawImage(
|
||||
|
||||
@@ -30,7 +30,7 @@ export class UILayer implements Layer {
|
||||
|
||||
}
|
||||
|
||||
render(context: CanvasRenderingContext2D) {
|
||||
renderLayer(context: CanvasRenderingContext2D) {
|
||||
if (!this.game.inSpawnPhase()) {
|
||||
return
|
||||
}
|
||||
@@ -55,7 +55,7 @@ export class UILayer implements Layer {
|
||||
tick() {
|
||||
}
|
||||
|
||||
init() {
|
||||
init(game: Game) {
|
||||
this.createExitButton()
|
||||
this.createWinModal()
|
||||
this.initRightClickMenu()
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import {LitElement, html, css} from 'lit';
|
||||
import {customElement, state} from 'lit/decorators.js';
|
||||
|
||||
const emojiTable: string[][] = [
|
||||
["😀", "😱", "🤩", "🎯", "🥺"],
|
||||
["🪦", "👏", "🥉", "🥈", "🥇"],
|
||||
["🤙", "🥰", "😇", "😊", "🔥"],
|
||||
["💪", "🥳", "💀", "😭", "🤦♂️"],
|
||||
["😎", "👎", "👍", "🥱", "💔"],
|
||||
["❤️", "💰", "🤝", "🛡️", "💥"],
|
||||
["🆘", "🕊️", "➡️", "⬅️", "↙️"],
|
||||
["↖️", "↗️", "⬆️", "↘️", "⬇️"]
|
||||
];
|
||||
|
||||
@customElement('emoji-table')
|
||||
export class EmojiTable extends LitElement {
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
.emoji-table {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 9999;
|
||||
background-color: #1E1E1E;
|
||||
padding: 15px;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
max-width: 95vw;
|
||||
max-height: 95vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.emoji-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
.emoji-button {
|
||||
font-size: 60px;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border: 1px solid #333;
|
||||
background-color: #2C2C2C;
|
||||
color: white;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 8px;
|
||||
}
|
||||
.emoji-button:hover {
|
||||
background-color: #3A3A3A;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
.emoji-button:active {
|
||||
background-color: #4A4A4A;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
`;
|
||||
|
||||
@state()
|
||||
private _hidden = true;
|
||||
|
||||
public onEmojiClicked: (emoji: string) => void = () => { }
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="emoji-table ${this._hidden ? 'hidden' : ''}">
|
||||
${emojiTable.map(row => html`
|
||||
<div class="emoji-row">
|
||||
${row.map(emoji => html`
|
||||
<button class="emoji-button" @click=${() => this.onEmojiClicked(emoji)}>
|
||||
${emoji}
|
||||
</button>
|
||||
`)}
|
||||
</div>
|
||||
`)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
hideTable() {
|
||||
this._hidden = true;
|
||||
this.requestUpdate();
|
||||
|
||||
}
|
||||
|
||||
showTable() {
|
||||
this._hidden = false;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
get isVisible() {
|
||||
return !this._hidden;
|
||||
}
|
||||
}
|
||||
+26
-65
@@ -1,21 +1,21 @@
|
||||
import {EventBus} from "../../../core/EventBus";
|
||||
import {AllPlayers, Cell, Game, Player} from "../../../core/game/Game";
|
||||
import {ClientID} from "../../../core/Schemas";
|
||||
import {and, bfs, dist, manhattanDist, manhattanDistWrapped, sourceDstOceanShore} from "../../../core/Util";
|
||||
import {ContextMenuEvent, MouseUpEvent} from "../../InputHandler";
|
||||
import {SendAllianceRequestIntentEvent, SendAttackIntentEvent, SendBoatAttackIntentEvent, SendBreakAllianceIntentEvent, SendDonateIntentEvent, SendEmojiIntentEvent, SendSpawnIntentEvent, SendTargetPlayerIntentEvent} from "../../Transport";
|
||||
import {TransformHandler} from "../TransformHandler";
|
||||
import {Layer} from "./Layer";
|
||||
import {EventBus} from "../../../../core/EventBus";
|
||||
import {AllPlayers, Cell, Game, Player} from "../../../../core/game/Game";
|
||||
import {ClientID} from "../../../../core/Schemas";
|
||||
import {and, bfs, dist, manhattanDist, manhattanDistWrapped, sourceDstOceanShore} from "../../../../core/Util";
|
||||
import {ContextMenuEvent, MouseUpEvent} from "../../../InputHandler";
|
||||
import {SendAllianceRequestIntentEvent, SendAttackIntentEvent, SendBoatAttackIntentEvent, SendBreakAllianceIntentEvent, SendDonateIntentEvent, SendEmojiIntentEvent, SendSpawnIntentEvent, SendTargetPlayerIntentEvent} from "../../../Transport";
|
||||
import {TransformHandler} from "../../TransformHandler";
|
||||
import {Layer} from "../Layer";
|
||||
import * as d3 from 'd3';
|
||||
import traitorIcon from '../../../../resources/images/TraitorIconWhite.png';
|
||||
import allianceIcon from '../../../../resources/images/AllianceIconWhite.png';
|
||||
import boatIcon from '../../../../resources/images/BoatIconWhite.png';
|
||||
import swordIcon from '../../../../resources/images/SwordIconWhite.png';
|
||||
import targetIcon from '../../../../resources/images/TargetIconWhite.png';
|
||||
import emojiIcon from '../../../../resources/images/EmojiIconWhite.png';
|
||||
import disabledIcon from '../../../../resources/images/DisabledIcon.png';
|
||||
import donateIcon from '../../../../resources/images/DonateIconWhite.png';
|
||||
import {MessageType} from "./EventsDisplay";
|
||||
import traitorIcon from '../../../../../resources/images/TraitorIconWhite.png';
|
||||
import allianceIcon from '../../../../../resources/images/AllianceIconWhite.png';
|
||||
import boatIcon from '../../../../../resources/images/BoatIconWhite.png';
|
||||
import swordIcon from '../../../../../resources/images/SwordIconWhite.png';
|
||||
import targetIcon from '../../../../../resources/images/TargetIconWhite.png';
|
||||
import emojiIcon from '../../../../../resources/images/EmojiIconWhite.png';
|
||||
import disabledIcon from '../../../../../resources/images/DisabledIcon.png';
|
||||
import donateIcon from '../../../../../resources/images/DonateIconWhite.png';
|
||||
import {EmojiTable} from "./EmojiTable";
|
||||
|
||||
|
||||
enum Slot {
|
||||
@@ -45,12 +45,12 @@ export class RadialMenu implements Layer {
|
||||
|
||||
private isCenterButtonEnabled = false
|
||||
|
||||
|
||||
constructor(
|
||||
private eventBus: EventBus,
|
||||
private game: Game,
|
||||
private transformHandler: TransformHandler,
|
||||
private clientID: ClientID,
|
||||
private emojiTable: EmojiTable
|
||||
) { }
|
||||
|
||||
init() {
|
||||
@@ -59,49 +59,6 @@ export class RadialMenu implements Layer {
|
||||
this.createMenuElement();
|
||||
}
|
||||
|
||||
private hideEmojiTable(): void {
|
||||
const emojiTable: HTMLTableElement | null = document.getElementById('uniqueEmojiTable') as HTMLTableElement | null;
|
||||
|
||||
if (emojiTable instanceof HTMLTableElement) {
|
||||
if (!emojiTable.classList.contains('hidden')) {
|
||||
emojiTable.classList.add('hidden');
|
||||
}
|
||||
} else {
|
||||
console.error('Emoji table not found');
|
||||
}
|
||||
}
|
||||
|
||||
private showEmojiTable(recipient: Player | typeof AllPlayers): void {
|
||||
const emojiTable: HTMLTableElement | null = document.getElementById('uniqueEmojiTable') as HTMLTableElement | null;
|
||||
|
||||
if (emojiTable instanceof HTMLTableElement) {
|
||||
emojiTable.classList.remove('hidden');
|
||||
} else {
|
||||
console.error('Emoji table not found');
|
||||
}
|
||||
this.setupEmojiButtons(recipient)
|
||||
}
|
||||
|
||||
private setupEmojiButtons(recipient: Player | typeof AllPlayers) {
|
||||
let emojiTable = document.getElementById('uniqueEmojiTable');
|
||||
|
||||
if (emojiTable) {
|
||||
// Remove existing listeners
|
||||
emojiTable.replaceWith(emojiTable.cloneNode(true));
|
||||
emojiTable = document.getElementById('uniqueEmojiTable');
|
||||
emojiTable.addEventListener('click', (event) => {
|
||||
const emojiElement = event.target as HTMLElement;
|
||||
if (emojiElement.classList.contains('emoji-button')) {
|
||||
const emoji = emojiElement.textContent;
|
||||
this.hideEmojiTable()
|
||||
this.eventBus.emit(new SendEmojiIntentEvent(recipient, emoji))
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.error('Emoji table not found');
|
||||
}
|
||||
}
|
||||
|
||||
private createMenuElement() {
|
||||
this.menuElement = d3.select(document.body)
|
||||
.append('div')
|
||||
@@ -227,7 +184,7 @@ export class RadialMenu implements Layer {
|
||||
// Update logic if needed
|
||||
}
|
||||
|
||||
render(context: CanvasRenderingContext2D) {
|
||||
renderLayer(context: CanvasRenderingContext2D) {
|
||||
// No need to render anything on the canvas
|
||||
}
|
||||
|
||||
@@ -272,7 +229,11 @@ export class RadialMenu implements Layer {
|
||||
const target = tile.owner() == myPlayer ? AllPlayers : (tile.owner() as Player)
|
||||
if (myPlayer.canSendEmoji(target)) {
|
||||
this.activateMenuElement(Slot.Emoji, "#ebe250", emojiIcon, () => {
|
||||
this.showEmojiTable(target)
|
||||
this.emojiTable.onEmojiClicked = (emoji: string) => {
|
||||
this.emojiTable.hideTable()
|
||||
this.eventBus.emit(new SendEmojiIntentEvent(target, emoji))
|
||||
}
|
||||
this.emojiTable.showTable()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -387,7 +348,7 @@ export class RadialMenu implements Layer {
|
||||
|
||||
private onPointerUp(event: MouseUpEvent) {
|
||||
this.hideRadialMenu()
|
||||
this.hideEmojiTable()
|
||||
this.emojiTable.hideTable()
|
||||
}
|
||||
|
||||
private showRadialMenu(x: number, y: number) {
|
||||
@@ -467,4 +428,4 @@ export class RadialMenu implements Layer {
|
||||
.attr('fill', enabled ? 'white' : '#cccccc');
|
||||
}, 25);
|
||||
}
|
||||
}
|
||||
}
|
||||
+33
-76
@@ -5,6 +5,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>OpenFront (ALPHA)</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=AW-16702609763">
|
||||
</script>
|
||||
@@ -26,19 +27,36 @@
|
||||
fill="#ffffff" />
|
||||
</svg>
|
||||
</a>
|
||||
<h1>OpenFront.io</h1>
|
||||
<h2>(v0.6.5)</h2>
|
||||
<div id="username-container">
|
||||
<input type="text" id="username" placeholder="Enter your username">
|
||||
<h1 class="text-9xl">OpenFront.io</h1>
|
||||
<h2 class="text-6xl mb-2">(v0.6.5)</h2>
|
||||
<div class="flex justify-center items-start">
|
||||
<div class="w-full max-w-3xl p-4 space-y-4">
|
||||
<username-input></username-input>
|
||||
<!-- Button layout -->
|
||||
<div class="flex space-x-4 max-w-xs mx-auto">
|
||||
<!-- Single Player button -->
|
||||
<button id="single-player"
|
||||
class="flex-1 h-31 px-6 py-8 text-xl font-bold text-white bg-blue-600 rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 transition duration-300 ease-in-out">
|
||||
Single Player
|
||||
</button>
|
||||
|
||||
<!-- Create and Join Lobby buttons stacked -->
|
||||
<div class="flex-1 space-y-4">
|
||||
<button id="create-lobby"
|
||||
class="w-full h-12 px-4 py-4 text-sm font-medium text-blue-700 bg-blue-100 rounded-md hover:bg-blue-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition duration-300 ease-in-out">
|
||||
Create Lobby
|
||||
</button>
|
||||
<button id="join-lobby"
|
||||
class="w-full h-12 px-4 py-4 text-sm font-medium text-blue-700 bg-blue-100 rounded-md hover:bg-blue-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition duration-300 ease-in-out">
|
||||
Join Lobby
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="text-6xl mt-8 tracking-wide pt-8">Public Games</h3>
|
||||
<public-lobby></public-lobby>
|
||||
</div>
|
||||
</div>
|
||||
<div id="lobbies-container">
|
||||
<button id="lobby-button" class="lobby-button">
|
||||
<div id="lobby-name" class="lobby-name"></div>
|
||||
<div id="lobby-timer" class="lobby-timer"></div>
|
||||
<div id="player-count" class="player-count"></div>
|
||||
</button>
|
||||
</div>
|
||||
<h3 id="next-game"> </h3>
|
||||
</div>
|
||||
|
||||
<div id="customMenu">
|
||||
@@ -49,71 +67,10 @@
|
||||
<div id="app"></div>
|
||||
<div id="radialMenu" class="radial-menu"></div>
|
||||
|
||||
<table id="uniqueEmojiTable" class="emoji-table hidden">
|
||||
<tr>
|
||||
<td><button class="emoji-button">😀</button></td>
|
||||
<td><button class="emoji-button">😱</button></td>
|
||||
<td><button class="emoji-button">🤩</button></td>
|
||||
<td><button class="emoji-button">🎯</button></td>
|
||||
<td><button class="emoji-button">🥺</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><button class="emoji-button">🪦</button></td>
|
||||
<td><button class="emoji-button">👏</button></td>
|
||||
<td><button class="emoji-button">🥉</button></td>
|
||||
<td><button class="emoji-button">🥈</button></td>
|
||||
<td><button class="emoji-button">🥇</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><button class="emoji-button">🤙</button></td>
|
||||
<td><button class="emoji-button">🥰</button></td>
|
||||
<td><button class="emoji-button">😇</button></td>
|
||||
<td><button class="emoji-button">😊</button></td>
|
||||
<td><button class="emoji-button">🔥</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><button class="emoji-button">💪</button></td>
|
||||
<td><button class="emoji-button">🥳</button></td>
|
||||
<td><button class="emoji-button">💀</button></td>
|
||||
<td><button class="emoji-button">😭</button></td>
|
||||
<td><button class="emoji-button">🤦♂️</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><button class="emoji-button">😎</button></td>
|
||||
<td><button class="emoji-button">👎</button></td>
|
||||
<td><button class="emoji-button">👍</button></td>
|
||||
<td><button class="emoji-button">🥱</button></td>
|
||||
<td><button class="emoji-button">💔</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><button class="emoji-button">❤️</button></td>
|
||||
<td><button class="emoji-button">💰</button></td>
|
||||
<td><button class="emoji-button">🤝</button></td>
|
||||
<td><button class="emoji-button">🛡️</button></td>
|
||||
<td><button class="emoji-button">💥</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><button class="emoji-button">🆘</button></td>
|
||||
<td><button class="emoji-button">🕊️</button></td>
|
||||
<td><button class="emoji-button">➡️</button></td>
|
||||
<td><button class="emoji-button">⬅️</button></td>
|
||||
<td><button class="emoji-button">↙️</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><button class="emoji-button">↖️</button></td>
|
||||
<td><button class="emoji-button">↗️</button></td>
|
||||
<td><button class="emoji-button">⬆️</button></td>
|
||||
<td><button class="emoji-button">↘️</button></td>
|
||||
<td><button class="emoji-button">⬇️</button></td>
|
||||
</tr>
|
||||
</table>
|
||||
<single-player-modal></single-player-modal>
|
||||
<emoji-table></emoji-table>
|
||||
<leader-board></leader-board>
|
||||
|
||||
<style>
|
||||
body {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
window.addEventListener('DOMContentLoaded', (event) => {
|
||||
document.body.style.visibility = 'visible';
|
||||
|
||||
+4
-117
@@ -1,3 +1,7 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
html,
|
||||
body {
|
||||
touch-action: manipulation;
|
||||
@@ -90,66 +94,6 @@ h3 {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#lobbies-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.lobby-button {
|
||||
width: 50%;
|
||||
max-width: 300px;
|
||||
max-height: 300px;
|
||||
height: auto;
|
||||
aspect-ratio: 1 / 1;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
border: 3px solid #007bff;
|
||||
background-color: rgba(0, 123, 255, 0.2);
|
||||
color: #ffffff;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.lobby-button:hover {
|
||||
background-color: rgba(0, 123, 255, 0.4);
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.lobby-button.highlighted {
|
||||
background-color: rgba(0, 123, 255, 0.6);
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 6px 20px rgba(0, 123, 255, 0.6);
|
||||
border-color: #ffffff;
|
||||
}
|
||||
|
||||
.lobby-name {
|
||||
font-size: 24px;
|
||||
margin-bottom: 10px;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.lobby-timer {
|
||||
font-size: 20px;
|
||||
margin-bottom: 10px;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.player-count {
|
||||
font-size: 20px;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.joining-message {
|
||||
font-size: 24px;
|
||||
color: rgb(0, 0, 0);
|
||||
@@ -402,64 +346,7 @@ h3 {
|
||||
}
|
||||
|
||||
/* EMOJI Table */
|
||||
.emoji-table {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 9999;
|
||||
background-color: #1E1E1E;
|
||||
padding: 15px;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
max-width: 95vw;
|
||||
max-height: 95vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.emoji-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.emoji-button {
|
||||
font-size: 60px;
|
||||
/* Increased font size for larger emojis */
|
||||
width: 80px;
|
||||
/* Increased width */
|
||||
height: 80px;
|
||||
/* Increased height */
|
||||
border: 1px solid #333;
|
||||
background-color: #2C2C2C;
|
||||
color: white;
|
||||
border-radius: 12px;
|
||||
/* Slightly increased border radius */
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 8px;
|
||||
/* Increased margin */
|
||||
}
|
||||
|
||||
.emoji-button:hover {
|
||||
background-color: #3A3A3A;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.emoji-button:active {
|
||||
background-color: #4A4A4A;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.emoji-table.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.emoji-button {
|
||||
|
||||
@@ -9,10 +9,10 @@ export const devConfig = new class extends DefaultConfig {
|
||||
return 40
|
||||
}
|
||||
gameCreationRate(): number {
|
||||
return 2 * 1000
|
||||
return 20 * 1000
|
||||
}
|
||||
lobbyLifetime(): number {
|
||||
return 2 * 1000
|
||||
return 20 * 1000
|
||||
}
|
||||
turnIntervalMs(): number {
|
||||
return 100
|
||||
|
||||
@@ -47,7 +47,7 @@ export class Executor {
|
||||
)
|
||||
} else if (intent.type == "spawn") {
|
||||
return new SpawnExecution(
|
||||
new PlayerInfo(intent.name, intent.playerType, intent.clientID, intent.playerID),
|
||||
new PlayerInfo(intent.name.slice(0, 18), intent.playerType, intent.clientID, intent.playerID),
|
||||
new Cell(intent.x, intent.y)
|
||||
)
|
||||
} else if (intent.type == "boat") {
|
||||
|
||||
@@ -11,6 +11,11 @@ export type Tick = number
|
||||
|
||||
export const AllPlayers = "AllPlayers" as const;
|
||||
|
||||
export enum GameMap {
|
||||
World,
|
||||
Europe
|
||||
}
|
||||
|
||||
export class Nation {
|
||||
constructor(
|
||||
public readonly name: string,
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import {Cell, TerrainType} from './Game';
|
||||
import binAsString from "!!binary-loader!../../../resources/maps/Europe.bin";
|
||||
import worldMapInfo from "../../../resources/maps/Europe.json"
|
||||
import {Cell, GameMap, TerrainType} from './Game';
|
||||
import europeBin from "!!binary-loader!../../../resources/maps/Europe.bin";
|
||||
import europeInfo from "../../../resources/maps/Europe.json"
|
||||
|
||||
import worldBin from "!!binary-loader!../../../resources/maps/WorldMap.bin";
|
||||
import worldInfo from "../../../resources/maps/WorldMap.json"
|
||||
|
||||
const maps = new Map()
|
||||
.set(GameMap.World, {bin: worldBin, info: worldInfo})
|
||||
.set(GameMap.Europe, {bin: europeBin, info: europeInfo});
|
||||
|
||||
export interface NationMap {
|
||||
name: string;
|
||||
@@ -44,10 +51,13 @@ export class Terrain {
|
||||
constructor(public type: TerrainType) { }
|
||||
}
|
||||
|
||||
export async function loadTerrainMap(): Promise<TerrainMap> {
|
||||
export async function loadTerrainMap(map: GameMap): Promise<TerrainMap> {
|
||||
|
||||
const mapData = maps.get(map)
|
||||
|
||||
// Simulate an asynchronous file load
|
||||
const fileData = await new Promise<string>((resolve) => {
|
||||
setTimeout(() => resolve(binAsString), 100);
|
||||
setTimeout(() => resolve(mapData.bin), 100);
|
||||
});
|
||||
|
||||
console.log(`Loaded data length: ${fileData.length} bytes`);
|
||||
@@ -103,7 +113,7 @@ export async function loadTerrainMap(): Promise<TerrainMap> {
|
||||
}
|
||||
}
|
||||
|
||||
return new TerrainMap(terrain, numLand, worldMapInfo);
|
||||
return new TerrainMap(terrain, numLand, mapData.info);
|
||||
}
|
||||
|
||||
function logBinaryAsAscii(data: string, length: number = 8) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import PImage from 'pureimage';
|
||||
import path from 'path';
|
||||
import {decodePNGFromStream} from 'pureimage'; import path from 'path';
|
||||
import fs from 'fs/promises';
|
||||
import {createReadStream} from 'fs';
|
||||
import {fileURLToPath} from 'url';
|
||||
@@ -47,7 +46,7 @@ export async function loadTerrainMap(): Promise<void> {
|
||||
const imagePath = path.resolve(__dirname, '..', '..', 'resources', 'maps', mapName + '.png');
|
||||
|
||||
const readStream = createReadStream(imagePath);
|
||||
const img = await PImage.decodePNGFromStream(readStream);
|
||||
const img = await decodePNGFromStream(readStream);
|
||||
|
||||
console.log('Image loaded successfully');
|
||||
console.log('Image dimensions:', img.width, 'x', img.height);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import {GamePhase, GameServer} from "./GameServer";
|
||||
import {Config} from "../core/configuration/Config";
|
||||
import {PseudoRandom} from "../core/PseudoRandom";
|
||||
import WebSocket from 'ws';
|
||||
import {ClientID, GameID} from "../core/Schemas";
|
||||
import {Client} from "./Client";
|
||||
import {v4 as uuidv4} from 'uuid';
|
||||
import {Client} from "./Client";
|
||||
import {GamePhase, GameServer} from "./GameServer";
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import {ClientID} from "../core/Schemas";
|
||||
import {Client} from "./Client";
|
||||
|
||||
export class Lobby {
|
||||
|
||||
public clients: Map<ClientID, Client> = new Map()
|
||||
private startGameTs: number
|
||||
|
||||
|
||||
constructor(public readonly id: string, durationMs: number) {
|
||||
this.startGameTs = Date.now() + durationMs
|
||||
}
|
||||
|
||||
public addClient(client: Client) {
|
||||
this.clients.set(client.id, client)
|
||||
}
|
||||
|
||||
public isExpired(now: number): boolean {
|
||||
return now > this.startGameTs
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,11 @@ import {WebSocketServer} from 'ws';
|
||||
import path from 'path';
|
||||
import {fileURLToPath} from 'url';
|
||||
import {GameManager} from './GameManager';
|
||||
import {Client} from './Client';
|
||||
import {ClientMessage, ClientMessageSchema} from '../core/Schemas';
|
||||
import {GamePhase} from './GameServer';
|
||||
import {getConfig} from '../core/configuration/Config';
|
||||
import {LogSeverity, slog} from './StructuredLog';
|
||||
import {Client} from './Client';
|
||||
import {GamePhase} from './GameServer';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
+3
-1
@@ -6,7 +6,9 @@
|
||||
"moduleResolution": "node",
|
||||
"sourceMap": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true
|
||||
"esModuleInterop": true,
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ export default (env, argv) => {
|
||||
const isProduction = argv.mode === 'production';
|
||||
|
||||
return {
|
||||
entry: './src/client/Client.ts',
|
||||
entry: './src/client/Main.ts',
|
||||
output: {
|
||||
filename: 'bundle.js',
|
||||
path: path.resolve(__dirname, 'out'),
|
||||
|
||||
Reference in New Issue
Block a user