create basic win popup

This commit is contained in:
evanpelle
2024-09-16 11:51:24 -07:00
parent 2f626bcc39
commit 534d97abb3
15 changed files with 287 additions and 27 deletions
+3 -3
View File
@@ -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
+93
View File
@@ -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
View File
@@ -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"
}
}
+3 -3
View File
@@ -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);
}
+5 -4
View File
@@ -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
}
+109 -14
View File
@@ -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();
}
}
+1
View File
@@ -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
+5
View File
@@ -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)
}
+4 -2
View File
@@ -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) {
+1
View File
@@ -26,6 +26,7 @@ export function getGameEnv(): GameEnv {
export interface Config {
theme(): Theme;
percentageTilesOwnedToWin(): number
turnIntervalMs(): number
gameCreationRate(): number
lobbyLifetime(): number
+3
View File
@@ -7,6 +7,9 @@ import {pastelTheme} from "./PastelTheme";
export class DefaultConfig implements Config {
percentageTilesOwnedToWin(): number {
return 80
}
boatMaxNumber(): number {
return 3
}
+3
View File
@@ -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
}
+47
View File
@@ -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
}
}
+4
View File
@@ -23,3 +23,7 @@ declare module '*.txt' {
const value: string;
export default value;
}
declare module '*.html' {
const content: string;
export default content;
}
+4
View File
@@ -41,6 +41,10 @@ export default (env, argv) => {
filename: 'images/[hash][ext][query]'
}
},
{
test: /\.html$/,
use: ['html-loader']
},
{
test: /\.svg$/,
type: 'asset/inline',