diff --git a/.prettierignore b/.prettierignore
index 8ae3f9b70..584bf56da 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -1,4 +1,5 @@
*.bin
+*.svg
*.png
*.jpg
*.jpeg
@@ -6,4 +7,4 @@
*.webp
*.txt
.prettierignore
-.gitignore
\ No newline at end of file
+.gitignore
diff --git a/README.md b/README.md
index 7b73c8687..e3765ca1f 100644
--- a/README.md
+++ b/README.md
@@ -1,29 +1,104 @@
-# OpenFront.io
+# OpenFrontIO
+
+
+
+
+
+
+
+

-OpenFront is an online rts.
+OpenFront is an online real-time strategy game focused on territorial control and alliance building. Players compete to expand their territory, build structures, and form strategic alliances in various maps based on real-world geography.
This is a fork/rewrite of WarFront.io. Credit to https://github.com/WarFrontIO.
-## Building
+## 🌟 Features
-To build the project, you will need to have Node.js and npm installed.
+- **Real-time Strategy Gameplay**: Expand your territory and engage in strategic battles
+- **Alliance System**: Form alliances with other players for mutual defense
+- **Multiple Maps**: Play across various geographical regions including Europe, Asia, Africa, and more
+- **Resource Management**: Balance your expansion with defensive capabilities
+- **Cross-platform**: Play in any modern web browser
-Before building the project, install the dependencies:
+## 📋 Prerequisites
-```bash
-npm install
-```
+- [Node.js](https://nodejs.org/) (v16.x or higher)
+- [npm](https://www.npmjs.com/) (v8.x or higher)
+- A modern web browser (Chrome, Firefox, Edge, etc.)
-To run dev build:
+## 🚀 Installation
+
+1. **Clone the repository**
+
+ ```bash
+ git clone https://github.com/openfrontio/OpenFrontIO.git
+ cd OpenFrontIO
+ ```
+
+2. **Install dependencies**
+
+ ```bash
+ npm install
+ ```
+
+## 🎮 Running the Game
+
+### Development Mode
+
+Run both the client and server in development mode with live reloading:
```bash
npm run dev
```
-Make sure to format code using prettier extension or by running:
+This will:
+
+- Start the webpack dev server for the client
+- Launch the game server with development settings
+- Open the game in your default browser
+
+### Client Only
+
+To run just the client with hot reloading:
```bash
-npm run format
+npm run start:client
```
+
+### Server Only
+
+To run just the server with development settings:
+
+```bash
+npm run start:server-dev
+```
+
+## 🛠️ Development Tools
+
+- **Format code**:
+ ```bash
+ npm run format
+ ```
+
+## 🏗️ Project Structure
+
+- `/src/client` - Frontend game client
+- `/src/core` - Shared game logic
+- `/src/server` - Backend game server
+- `/resources` - Static assets (images, maps, etc.)
+
+## 📝 License
+
+This project is licensed under the terms found in the [LICENSE](LICENSE) file.
+
+## 🤝 Contributing
+
+Contributions are welcome! Please feel free to submit a Pull Request.
+
+1. Fork the repository
+2. Create your feature branch (`git checkout -b amazing-feature`)
+3. Commit your changes (`git commit -m 'Add some amazing feature'`)
+4. Push to the branch (`git push origin amazing-feature`)
+5. Open a Pull Request
diff --git a/resources/images/AllianceRequestIcon.svg b/resources/images/AllianceRequestIcon.svg
new file mode 100755
index 000000000..f5981e241
--- /dev/null
+++ b/resources/images/AllianceRequestIcon.svg
@@ -0,0 +1,14 @@
+
+
+
\ No newline at end of file
diff --git a/resources/images/OpenFrontLogoDark.svg b/resources/images/OpenFrontLogoDark.svg
new file mode 100644
index 000000000..2e2600480
--- /dev/null
+++ b/resources/images/OpenFrontLogoDark.svg
@@ -0,0 +1,15 @@
+
\ No newline at end of file
diff --git a/src/client/Main.ts b/src/client/Main.ts
index ed24a6ff8..8804a0d65 100644
--- a/src/client/Main.ts
+++ b/src/client/Main.ts
@@ -143,7 +143,10 @@ class Client {
this.gameStop = joinLobby(
{
gameType: gameType,
- flag: (): string => this.flagInput.getCurrentFlag(),
+ flag: (): string =>
+ this.flagInput.getCurrentFlag() == "xx"
+ ? ""
+ : this.flagInput.getCurrentFlag(),
playerName: (): string => this.usernameInput.getCurrentUsername(),
gameID: lobby.id,
persistentID: getPersistentIDFromCookie(),
diff --git a/src/client/graphics/layers/Leaderboard.ts b/src/client/graphics/layers/Leaderboard.ts
index d2a146f29..95c6b9789 100644
--- a/src/client/graphics/layers/Leaderboard.ts
+++ b/src/client/graphics/layers/Leaderboard.ts
@@ -1,17 +1,18 @@
-import { LitElement, html, css } from "lit";
-import { customElement, property, state } from "lit/decorators.js";
-import { Layer } from "./Layer";
-import { ClientID } from "../../../core/Schemas";
+import { LitElement, css, html } from "lit";
+import { customElement, state } from "lit/decorators.js";
import { unsafeHTML } from "lit/directives/unsafe-html.js";
import { EventBus, GameEvent } from "../../../core/EventBus";
-import { renderNumber } from "../../Utils";
import { GameView, PlayerView } from "../../../core/game/GameView";
+import { ClientID } from "../../../core/Schemas";
+import { renderNumber } from "../../Utils";
+import { Layer } from "./Layer";
interface Entry {
name: string;
position: number;
score: string;
gold: string;
+ troops: string;
isMyPlayer: boolean;
player: PlayerView;
}
@@ -26,6 +27,13 @@ export class Leaderboard extends LitElement implements Layer {
public clientID: ClientID;
public eventBus: EventBus;
+ players: Entry[] = [];
+
+ @state()
+ private _leaderboardHidden = true;
+ private _shownOnInit = false;
+ private showTopFive = true;
+
init() {}
tick() {
@@ -58,14 +66,25 @@ export class Leaderboard extends LitElement implements Layer {
const numTilesWithoutFallout =
this.game.numLandTiles() - this.game.numTilesWithFallout();
- this.players = sorted.slice(0, 5).map((player, index) => ({
- name: player.displayName(),
- position: index + 1,
- score: formatPercentage(player.numTilesOwned() / numTilesWithoutFallout),
- gold: renderNumber(player.gold()),
- isMyPlayer: player == myPlayer,
- player: player,
- }));
+ const playersToShow = this.showTopFive ? sorted.slice(0, 5) : sorted;
+
+ this.players = playersToShow.map((player, index) => {
+ let troops = player.troops() / 10;
+ if (!player.isAlive()) {
+ troops = 0;
+ }
+ return {
+ name: player.displayName(),
+ position: index + 1,
+ score: formatPercentage(
+ player.numTilesOwned() / numTilesWithoutFallout,
+ ),
+ gold: renderNumber(player.gold()),
+ troops: renderNumber(troops),
+ isMyPlayer: player == myPlayer,
+ player: player,
+ };
+ });
if (myPlayer != null && this.players.find((p) => p.isMyPlayer) == null) {
let place = 0;
@@ -76,6 +95,10 @@ export class Leaderboard extends LitElement implements Layer {
}
}
+ let myPlayerTroops = myPlayer.troops() / 10;
+ if (!myPlayer.isAlive()) {
+ myPlayerTroops = 0;
+ }
this.players.pop();
this.players.push({
name: myPlayer.displayName(),
@@ -84,6 +107,7 @@ export class Leaderboard extends LitElement implements Layer {
myPlayer.numTilesOwned() / this.game.numLandTiles(),
),
gold: renderNumber(myPlayer.gold()),
+ troops: renderNumber(myPlayerTroops),
isMyPlayer: true,
player: myPlayer,
});
@@ -119,10 +143,10 @@ export class Leaderboard extends LitElement implements Layer {
padding-top: 0px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
border-radius: 10px;
- max-width: 300px;
- max-height: 80vh;
+ max-width: 500px;
+ max-height: 30vh;
overflow-y: auto;
- width: 300px;
+ width: 400px;
backdrop-filter: blur(5px);
}
table {
@@ -180,6 +204,13 @@ export class Leaderboard extends LitElement implements Layer {
cursor: pointer;
}
+ .leaderboard-top-five-button {
+ background: none;
+ border: none;
+ color: white;
+ cursor: pointer;
+ }
+
.player-name {
max-width: 10ch;
overflow: hidden;
@@ -198,13 +229,6 @@ export class Leaderboard extends LitElement implements Layer {
}
`;
- players: Entry[] = [];
-
- @state()
- private _leaderboardHidden = true;
-
- private _shownOnInit = false;
-
render() {
return html`
+
@@ -232,6 +265,7 @@ export class Leaderboard extends LitElement implements Layer {
| Player |
Owned |
Gold |
+ Troops |
@@ -245,6 +279,7 @@ export class Leaderboard extends LitElement implements Layer {
${unsafeHTML(player.name)} |
${player.score} |
${player.gold} |
+ ${player.troops} |
`,
)}
diff --git a/src/client/graphics/layers/NameLayer.ts b/src/client/graphics/layers/NameLayer.ts
index 555627039..734507917 100644
--- a/src/client/graphics/layers/NameLayer.ts
+++ b/src/client/graphics/layers/NameLayer.ts
@@ -11,6 +11,7 @@ import { Layer } from "./Layer";
import { TransformHandler } from "../TransformHandler";
import traitorIcon from "../../../../resources/images/TraitorIcon.svg";
import allianceIcon from "../../../../resources/images/AllianceIcon.svg";
+import allianceRequestIcon from "../../../../resources/images/AllianceRequestIcon.svg";
import crownIcon from "../../../../resources/images/CrownIcon.svg";
import targetIcon from "../../../../resources/images/TargetIcon.svg";
import { ClientID } from "../../../core/Schemas";
@@ -40,6 +41,7 @@ export class NameLayer implements Layer {
private renders: RenderInfo[] = [];
private seenPlayers: Set = new Set();
private traitorIconImage: HTMLImageElement;
+ private allianceRequestIconImage: HTMLImageElement;
private allianceIconImage: HTMLImageElement;
private targetIconImage: HTMLImageElement;
private crownIconImage: HTMLImageElement;
@@ -57,6 +59,8 @@ export class NameLayer implements Layer {
this.traitorIconImage.src = traitorIcon;
this.allianceIconImage = new Image();
this.allianceIconImage.src = allianceIcon;
+ this.allianceRequestIconImage = new Image();
+ this.allianceRequestIconImage.src = allianceRequestIcon;
this.crownIconImage = new Image();
this.crownIconImage.src = crownIcon;
this.targetIconImage = new Image();
@@ -314,6 +318,23 @@ export class NameLayer implements Layer {
existingAlliance.remove();
}
+ // Alliance request icon
+ const data = '[data-icon="alliance-request"]';
+ const existingRequestAlliance = iconsDiv.querySelector(data);
+ if (myPlayer != null && render.player.isRequestingAllianceWith(myPlayer)) {
+ if (!existingRequestAlliance) {
+ iconsDiv.appendChild(
+ this.createIconElement(
+ this.allianceRequestIconImage.src,
+ iconSize,
+ "alliance-request",
+ ),
+ );
+ }
+ } else if (existingRequestAlliance) {
+ existingRequestAlliance.remove();
+ }
+
// Target icon
const existingTarget = iconsDiv.querySelector('[data-icon="target"]');
if (
diff --git a/src/core/game/GameUpdates.ts b/src/core/game/GameUpdates.ts
index 815136884..d3896eee0 100644
--- a/src/core/game/GameUpdates.ts
+++ b/src/core/game/GameUpdates.ts
@@ -1,5 +1,6 @@
import { ClientID } from "../Schemas";
import {
+ AllianceRequest,
EmojiMessage,
GameUpdates,
MapPos,
@@ -99,6 +100,7 @@ export interface PlayerUpdate {
outgoingEmojis: EmojiMessage[];
outgoingAttacks: AttackUpdate[];
incomingAttacks: AttackUpdate[];
+ outgoingAllianceRequests: PlayerID[];
}
export interface AllianceRequestUpdate {
diff --git a/src/core/game/GameView.ts b/src/core/game/GameView.ts
index 9370c8895..ad1e9622a 100644
--- a/src/core/game/GameView.ts
+++ b/src/core/game/GameView.ts
@@ -187,6 +187,10 @@ export class PlayerView {
return this.data.allies.some((n) => other.smallID() == n);
}
+ isRequestingAllianceWith(other: PlayerView) {
+ return this.data.outgoingAllianceRequests.some((id) => other.id() == id);
+ }
+
profile(): Promise {
return this.game.worker.playerProfile(this.smallID());
}
diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts
index c1019c59a..0ca8e55ee 100644
--- a/src/core/game/PlayerImpl.ts
+++ b/src/core/game/PlayerImpl.ts
@@ -102,6 +102,10 @@ export class PlayerImpl implements Player {
largestClusterBoundingBox: { min: Cell; max: Cell } | null;
toUpdate(): PlayerUpdate {
+ const outgoingAllianceRequests = this.outgoingAllianceRequests().map((ar) =>
+ ar.recipient().id(),
+ );
+
return {
type: GameUpdateType.Player,
clientID: this.clientID(),
@@ -138,6 +142,7 @@ export class PlayerImpl implements Player {
troops: a.troops(),
}) as AttackUpdate,
),
+ outgoingAllianceRequests: outgoingAllianceRequests,
};
}