mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 09:20:47 +00:00
create basic win popup
This commit is contained in:
@@ -112,8 +112,9 @@
|
||||
--- v3 Release DONE
|
||||
|
||||
* Add discord link DONE 9/14/2024
|
||||
* front page mobile friendly
|
||||
* game mobile friendly
|
||||
* front page mobile friendly DONE 9/15/2024
|
||||
* game mobile friendly DONE 9/16/2024
|
||||
* UI: basic win condition & popup DONE 9/16/2024
|
||||
* right click popup alliance option
|
||||
* click alliance sends alliance request
|
||||
* notification for alliance request
|
||||
@@ -125,7 +126,6 @@
|
||||
* BUG: when send boat only captures one pixel
|
||||
* store cookies
|
||||
* names dissapear on bottom of screen
|
||||
* UI: win condition & popup
|
||||
* UI: boats
|
||||
* UI: current attacks
|
||||
* UI: leader board
|
||||
|
||||
Generated
+93
@@ -47,6 +47,7 @@
|
||||
"css-loader": "^7.1.2",
|
||||
"file-loader": "^6.2.0",
|
||||
"html-inline-script-webpack-plugin": "^3.2.1",
|
||||
"html-loader": "^5.1.0",
|
||||
"html-webpack-plugin": "^5.6.0",
|
||||
"jest": "^29.7.0",
|
||||
"mocha": "^10.7.0",
|
||||
@@ -7394,6 +7395,72 @@
|
||||
"webpack": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/html-loader": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/html-loader/-/html-loader-5.1.0.tgz",
|
||||
"integrity": "sha512-Jb3xwDbsm0W3qlXrCZwcYqYGnYz55hb6aoKQTlzyZPXsPpi6tHXzAfqalecglMQgNvtEfxrCQPaKT90Irt5XDA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"html-minifier-terser": "^7.2.0",
|
||||
"parse5": "^7.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18.12.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/webpack"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"webpack": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/html-loader/node_modules/commander": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
|
||||
"integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/html-loader/node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/html-loader/node_modules/html-minifier-terser": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-7.2.0.tgz",
|
||||
"integrity": "sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"camel-case": "^4.1.2",
|
||||
"clean-css": "~5.3.2",
|
||||
"commander": "^10.0.0",
|
||||
"entities": "^4.4.0",
|
||||
"param-case": "^3.0.4",
|
||||
"relateurl": "^0.2.7",
|
||||
"terser": "^5.15.1"
|
||||
},
|
||||
"bin": {
|
||||
"html-minifier-terser": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.13.1 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/html-minifier-terser": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz",
|
||||
@@ -10387,6 +10454,32 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/parse5": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz",
|
||||
"integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"entities": "^4.4.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/parse5/node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
|
||||
+2
-1
@@ -29,6 +29,7 @@
|
||||
"css-loader": "^7.1.2",
|
||||
"file-loader": "^6.2.0",
|
||||
"html-inline-script-webpack-plugin": "^3.2.1",
|
||||
"html-loader": "^5.1.0",
|
||||
"html-webpack-plugin": "^5.6.0",
|
||||
"jest": "^29.7.0",
|
||||
"mocha": "^10.7.0",
|
||||
@@ -73,4 +74,4 @@
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"type": "module"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {ClientID, ClientIntentMessageSchema, ClientJoinMessageSchema, ClientLeav
|
||||
import {TerrainMap} from "../core/TerrainMapLoader";
|
||||
import {and, bfs, dist, manhattanDist} from "../core/Util";
|
||||
import {TerrainRenderer} from "./graphics/TerrainRenderer";
|
||||
import {WinCheckExecution} from "../core/execution/WinCheckExecution";
|
||||
|
||||
|
||||
|
||||
@@ -16,7 +17,7 @@ export function createClientGame(name: string, clientID: ClientID, playerID: Pla
|
||||
let eventBus = new EventBus()
|
||||
let game = createGame(terrainMap, eventBus, config)
|
||||
let terrainRenderer = new TerrainRenderer(game)
|
||||
let gameRenderer = new GameRenderer(game, clientID, terrainRenderer)
|
||||
let gameRenderer = new GameRenderer(eventBus, game, clientID, terrainRenderer)
|
||||
|
||||
return new ClientGame(
|
||||
name,
|
||||
@@ -40,7 +41,6 @@ export class ClientGame {
|
||||
|
||||
private currTurn = 0
|
||||
|
||||
|
||||
private intervalID: NodeJS.Timeout
|
||||
|
||||
private isProcessingTurn = false
|
||||
@@ -131,8 +131,8 @@ export class ClientGame {
|
||||
this.renderer.initialize()
|
||||
this.input.initialize()
|
||||
this.gs.addExecution(...this.executor.spawnBots(this.gs.config().numBots()))
|
||||
console.log('!!! number fake humans ')
|
||||
this.gs.addExecution(...this.executor.fakeHumanExecutions(this.gs.config().numFakeHumans(this.gameID)))
|
||||
this.gs.addExecution(new WinCheckExecution(this.eventBus))
|
||||
|
||||
this.intervalID = setInterval(() => this.tick(), 10);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {TerritoryRenderer} from "./TerritoryRenderer";
|
||||
import {ClientID} from "../../core/Schemas";
|
||||
import {renderTroops} from "./Utils";
|
||||
import {UIRenderer} from "./UIRenderer";
|
||||
import {EventBus} from "../../core/EventBus";
|
||||
|
||||
export class GameRenderer {
|
||||
private territoryCanvas: HTMLCanvasElement
|
||||
@@ -28,11 +29,11 @@ export class GameRenderer {
|
||||
private theme: Theme
|
||||
|
||||
|
||||
constructor(private gs: Game, private clientID: ClientID, private terrainRenderer: TerrainRenderer) {
|
||||
constructor(private eventBus: EventBus, private gs: Game, private clientID: ClientID, private terrainRenderer: TerrainRenderer) {
|
||||
this.theme = gs.config().theme()
|
||||
this.nameRenderer = new NameRenderer(gs, this.theme)
|
||||
this.territoryRenderer = new TerritoryRenderer(gs)
|
||||
this.uiRenderer = new UIRenderer(gs, this.theme, clientID)
|
||||
this.uiRenderer = new UIRenderer(eventBus, gs, this.theme, clientID)
|
||||
}
|
||||
|
||||
initialize() {
|
||||
@@ -107,7 +108,7 @@ export class GameRenderer {
|
||||
|
||||
this.context.restore()
|
||||
|
||||
this.renderUIBar()
|
||||
this.renderSpawnBar()
|
||||
this.uiRenderer.render(this.context)
|
||||
|
||||
requestAnimationFrame(() => this.renderGame());
|
||||
@@ -115,7 +116,7 @@ export class GameRenderer {
|
||||
|
||||
|
||||
// TODO: move to UIRenderer
|
||||
renderUIBar() {
|
||||
renderSpawnBar() {
|
||||
if (!this.gs.inSpawnPhase()) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,17 +1,94 @@
|
||||
import {Theme} from "../../core/configuration/Config";
|
||||
import {Game} from "../../core/Game";
|
||||
import {EventBus} from "../../core/EventBus";
|
||||
import {WinEvent} from "../../core/execution/WinCheckExecution";
|
||||
import {Game, Player} from "../../core/Game";
|
||||
import {ClientID} from "../../core/Schemas";
|
||||
import {renderTroops} from "./Utils";
|
||||
import winModalHtml from '../WinModal.html';
|
||||
|
||||
export class UIRenderer {
|
||||
private exitButton: HTMLButtonElement;
|
||||
private winModal: HTMLElement | null = null;
|
||||
|
||||
constructor(private game: Game, private theme: Theme, private clientID: ClientID) {
|
||||
|
||||
constructor(private eventBus: EventBus, private game: Game, private theme: Theme, private clientID: ClientID) {
|
||||
|
||||
}
|
||||
|
||||
init() {
|
||||
this.createExitButton()
|
||||
this.createWinModal()
|
||||
this.eventBus.on(WinEvent, (e) => this.onWinEvent(e))
|
||||
}
|
||||
|
||||
createWinModal() {
|
||||
console.log("Creating win modal");
|
||||
this.winModal = document.createElement('div');
|
||||
this.winModal.style.cssText = `
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background-color: white;
|
||||
padding: 20px;
|
||||
border: 2px solid black;
|
||||
border-radius: 10px;
|
||||
z-index: 2000;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
`;
|
||||
|
||||
const content = document.createElement('div');
|
||||
|
||||
const title = document.createElement('h2');
|
||||
title.textContent = 'Game Over';
|
||||
title.id = 'winTitle';
|
||||
title.style.marginTop = '0';
|
||||
|
||||
const message = document.createElement('p');
|
||||
message.id = 'winMessage';
|
||||
|
||||
const buttonContainer = document.createElement('div');
|
||||
buttonContainer.style.display = 'flex';
|
||||
buttonContainer.style.justifyContent = 'space-between';
|
||||
buttonContainer.style.marginTop = '20px';
|
||||
|
||||
const exitButton = document.createElement('button');
|
||||
exitButton.textContent = 'Exit Game';
|
||||
exitButton.onclick = () => this.exitGame();
|
||||
this.styleButton(exitButton);
|
||||
|
||||
const continueButton = document.createElement('button');
|
||||
continueButton.textContent = 'Keep Playing';
|
||||
continueButton.onclick = () => this.closeWinModal();
|
||||
this.styleButton(continueButton);
|
||||
|
||||
buttonContainer.appendChild(exitButton);
|
||||
buttonContainer.appendChild(continueButton);
|
||||
|
||||
content.appendChild(title);
|
||||
content.appendChild(message);
|
||||
content.appendChild(buttonContainer);
|
||||
|
||||
this.winModal.appendChild(content);
|
||||
document.body.appendChild(this.winModal);
|
||||
|
||||
console.log("Win modal appended to body");
|
||||
}
|
||||
|
||||
styleButton(button: HTMLButtonElement) {
|
||||
button.style.cssText = `
|
||||
padding: 10px 20px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
background-color: #4A90E2;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
transition: background-color 0.3s;
|
||||
`;
|
||||
button.onmouseover = () => button.style.backgroundColor = '#3A7BCE';
|
||||
button.onmouseout = () => button.style.backgroundColor = '#4A90E2';
|
||||
}
|
||||
|
||||
createExitButton() {
|
||||
@@ -48,26 +125,44 @@ export class UIRenderer {
|
||||
}
|
||||
|
||||
render(context: CanvasRenderingContext2D) {
|
||||
// const p = this.game.players().find(p => p.clientID() == this.clientID);
|
||||
// let troopCount = p ? `${renderTroops(p.troops())}` : '';
|
||||
}
|
||||
|
||||
// context.save();
|
||||
// context.fillStyle = 'rgba(0, 0, 0, 0.7)'; // Black with 70% opacity
|
||||
// context.textAlign = 'center';
|
||||
// context.textBaseline = 'top';
|
||||
|
||||
// const x = context.canvas.width / 2; // Center horizontally
|
||||
// const y = 40; // Distance from the top
|
||||
onWinEvent(event: WinEvent) {
|
||||
console.log(`${event.winner.name()} won the game!!}`)
|
||||
this.showWinModal(event.winner)
|
||||
}
|
||||
|
||||
// context.font = `bold ${60}px ${this.theme.font()}`;
|
||||
// context.fillText(troopCount, x, y);
|
||||
// context.restore();
|
||||
showWinModal(winner: Player) {
|
||||
if (this.winModal) {
|
||||
const message = this.winModal.querySelector('#winMessage');
|
||||
if (message) {
|
||||
message.textContent = `${winner.name()} won the game!`;
|
||||
}
|
||||
const title = this.winModal.querySelector('#winTitle')
|
||||
if (winner.clientID() == this.clientID) {
|
||||
title.textContent = 'You Won!!!'
|
||||
} else {
|
||||
title.textContent = 'You Lost!!!'
|
||||
}
|
||||
this.winModal.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
onExitButtonClick() {
|
||||
console.log('Button clicked!');
|
||||
window.location.reload();
|
||||
// Add your button action here
|
||||
}
|
||||
|
||||
closeWinModal() {
|
||||
if (this.winModal) {
|
||||
this.winModal.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
exitGame() {
|
||||
this.closeWinModal();
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -142,6 +142,7 @@ export interface Game {
|
||||
neighbors(cell: Cell | Tile): Tile[]
|
||||
width(): number
|
||||
height(): number
|
||||
numLandTiles(): number
|
||||
forEachTile(fn: (tile: Tile) => void): void
|
||||
executions(): ExecutionView[]
|
||||
terraNullius(): TerraNullius
|
||||
|
||||
@@ -294,12 +294,14 @@ export class GameImpl implements MutableGame {
|
||||
private execs: Execution[] = []
|
||||
private _width: number
|
||||
private _height: number
|
||||
private _numLandTiles: number
|
||||
_terraNullius: TerraNulliusImpl
|
||||
|
||||
constructor(terrainMap: TerrainMap, private eventBus: EventBus, private _config: Config) {
|
||||
this._terraNullius = new TerraNulliusImpl(this)
|
||||
this._width = terrainMap.width();
|
||||
this._height = terrainMap.height();
|
||||
this._numLandTiles = terrainMap.numLandTiles
|
||||
this.map = new Array(this._width);
|
||||
for (let x = 0; x < this._width; x++) {
|
||||
this.map[x] = new Array(this._height);
|
||||
@@ -309,6 +311,9 @@ export class GameImpl implements MutableGame {
|
||||
}
|
||||
}
|
||||
}
|
||||
numLandTiles(): number {
|
||||
return this._numLandTiles
|
||||
}
|
||||
hasPlayer(id: PlayerID): boolean {
|
||||
return this._players.has(id)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import {Cell, TerrainType} from './Game';
|
||||
import binAsString from "!!binary-loader!../../resources/TopoWorldMap.bin";
|
||||
|
||||
export class TerrainMap {
|
||||
constructor(public readonly tiles: Terrain[][]) { }
|
||||
constructor(public readonly tiles: Terrain[][], public readonly numLandTiles: number) { }
|
||||
|
||||
terrain(cell: Cell): Terrain {
|
||||
return this.tiles[cell.x][cell.y]
|
||||
@@ -45,6 +45,7 @@ export async function loadTerrainMap(): Promise<TerrainMap> {
|
||||
}
|
||||
|
||||
const terrain: Terrain[][] = Array(width).fill(null).map(() => Array(height).fill(null));
|
||||
let numLand = 0
|
||||
|
||||
// Start from the 5th byte (index 4) when processing terrain data
|
||||
for (let x = 0; x < width; x++) {
|
||||
@@ -58,6 +59,7 @@ export async function loadTerrainMap(): Promise<TerrainMap> {
|
||||
let type: TerrainType = null
|
||||
let land = false
|
||||
if (isLand) {
|
||||
numLand++
|
||||
land = true
|
||||
if (magnitude < 10) {
|
||||
type = TerrainType.Plains
|
||||
@@ -82,7 +84,7 @@ export async function loadTerrainMap(): Promise<TerrainMap> {
|
||||
}
|
||||
}
|
||||
|
||||
return new TerrainMap(terrain);
|
||||
return new TerrainMap(terrain, numLand);
|
||||
}
|
||||
|
||||
function logBinaryAsAscii(data: string, length: number = 8) {
|
||||
|
||||
@@ -26,6 +26,7 @@ export function getGameEnv(): GameEnv {
|
||||
|
||||
export interface Config {
|
||||
theme(): Theme;
|
||||
percentageTilesOwnedToWin(): number
|
||||
turnIntervalMs(): number
|
||||
gameCreationRate(): number
|
||||
lobbyLifetime(): number
|
||||
|
||||
@@ -7,6 +7,9 @@ import {pastelTheme} from "./PastelTheme";
|
||||
|
||||
|
||||
export class DefaultConfig implements Config {
|
||||
percentageTilesOwnedToWin(): number {
|
||||
return 80
|
||||
}
|
||||
boatMaxNumber(): number {
|
||||
return 3
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@ import {GameID} from "../Schemas";
|
||||
import {DefaultConfig} from "./DefaultConfig";
|
||||
|
||||
export const devConfig = new class extends DefaultConfig {
|
||||
percentageTilesOwnedToWin(): number {
|
||||
return 80
|
||||
}
|
||||
numSpawnPhaseTurns(): number {
|
||||
return 40
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import {EventBus, GameEvent} from "../EventBus"
|
||||
import {Execution, MutableGame, MutablePlayer, Player, PlayerID} from "../Game"
|
||||
|
||||
export class WinEvent implements GameEvent {
|
||||
constructor(public readonly winner: Player) { }
|
||||
}
|
||||
|
||||
export class WinCheckExecution implements Execution {
|
||||
|
||||
private active = true
|
||||
|
||||
private mg: MutableGame
|
||||
|
||||
constructor(private eventBus: EventBus) {
|
||||
}
|
||||
|
||||
init(mg: MutableGame, ticks: number) {
|
||||
this.mg = mg
|
||||
}
|
||||
|
||||
tick(ticks: number) {
|
||||
if (ticks % 10 != 0) {
|
||||
return
|
||||
}
|
||||
const sorted = this.mg.players().sort((a, b) => b.numTilesOwned() - a.numTilesOwned())
|
||||
if (sorted.length == 0) {
|
||||
return
|
||||
}
|
||||
const max = sorted[0]
|
||||
if (max.numTilesOwned() / this.mg.numLandTiles() * 100 > this.mg.config().percentageTilesOwnedToWin()) {
|
||||
this.eventBus.emit(new WinEvent(max))
|
||||
this.active = false
|
||||
}
|
||||
}
|
||||
|
||||
owner(): MutablePlayer {
|
||||
return null
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active
|
||||
}
|
||||
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
Vendored
+4
@@ -23,3 +23,7 @@ declare module '*.txt' {
|
||||
const value: string;
|
||||
export default value;
|
||||
}
|
||||
declare module '*.html' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
@@ -41,6 +41,10 @@ export default (env, argv) => {
|
||||
filename: 'images/[hash][ext][query]'
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.html$/,
|
||||
use: ['html-loader']
|
||||
},
|
||||
{
|
||||
test: /\.svg$/,
|
||||
type: 'asset/inline',
|
||||
|
||||
Reference in New Issue
Block a user