diff --git a/.vscode/settings.json b/.vscode/settings.json index 18453b6..2561c01 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,6 +2,7 @@ "cSpell.words": [ "Deserializable", "Deserialization", + "Respawn", "decla", "overridable", "serializable" diff --git a/backend/package.json b/backend/package.json index efaf4d4..e2bf197 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "declared-server", - "version": "0.0.12", + "version": "0.0.13", "description": "Game server for decla.red", "keywords": [], "author": "András Schmelczer (https://schmelczer.dev/)", diff --git a/backend/src/default-options.ts b/backend/src/default-options.ts index 1e34d21..a43f5f0 100644 --- a/backend/src/default-options.ts +++ b/backend/src/default-options.ts @@ -4,8 +4,8 @@ export const defaultOptions: Options = { port: 3000, name: 'Test server', playerLimit: 16, - npcCount: 16, + npcCount: 12, seed: Math.random(), - scoreLimit: 500, + scoreLimit: 1000, worldSize: 8000, }; diff --git a/backend/src/game-server.ts b/backend/src/game-server.ts index 6c48f4f..6c67dc2 100644 --- a/backend/src/game-server.ts +++ b/backend/src/game-server.ts @@ -115,11 +115,12 @@ export class GameServer { private endGame(winningTeam: CharacterTeam) { this.isInEndGame = true; - const endTitleLength = 6000; + const endTitleLength = 6; + this.players.endGame(winningTeam); this.players.queueCommandForEachClient( - new GameEndCommand(winningTeam, endTitleLength / 1000, true), + new GameEndCommand(winningTeam, endTitleLength, true), ); - setTimeout(() => this.destroy(), endTitleLength * 1.1); + setTimeout(() => this.destroy(), endTitleLength * 1000 * 1.1); } private destroy() { @@ -147,15 +148,17 @@ export class GameServer { ); } + let scaledDelta = delta; if (this.isInEndGame) { this.timeScaling *= Math.pow(settings.endGameDeltaScaling, delta); - delta /= this.timeScaling; + scaledDelta /= this.timeScaling; } else { this.updatePoints(); } - this.objects.stepObjects(delta); - this.players.step(delta); + this.objects.stepObjects(scaledDelta); + this.players.step(scaledDelta); + this.players.stepCommunication(delta); this.objects.resetRemoteCalls(); this.deltaTimes.push(this.deltaTimeCalculator.getNextDeltaTimeInSeconds()); @@ -165,7 +168,7 @@ export class GameServer { } private handleStats() { - const framesBetweenDeltaTimeCalculation = 1000; + const framesBetweenDeltaTimeCalculation = 10000; if (this.deltaTimes.length > framesBetweenDeltaTimeCalculation) { this.deltaTimes.sort((a, b) => a - b); diff --git a/backend/src/objects/planet-physical.ts b/backend/src/objects/planet-physical.ts index 8c4c589..11e5ea3 100644 --- a/backend/src/objects/planet-physical.ts +++ b/backend/src/objects/planet-physical.ts @@ -8,6 +8,8 @@ import { settings, PlanetBase, CharacterTeam, + PropertyUpdatesForObject, + UpdateProperty, } from 'shared'; import { ImmutableBoundingBox } from '../physics/bounding-boxes/immutable-bounding-box'; @@ -102,12 +104,16 @@ export class PlanetPhysical public step(deltaTime: number): void { this.timeSinceLastPointGeneration += deltaTime; - // In reverse order, so that teams can achieve a 100% control. - this.remoteCall('setOwnership', this.ownership); this.takeControl(CharacterTeam.neutral, deltaTime); } + public getPropertyUpdates(): PropertyUpdatesForObject { + return new PropertyUpdatesForObject(this.id, [ + new UpdateProperty('ownership', this.ownership, 0), + ]); + } + public takeControl(team: CharacterTeam, deltaTime: number) { if (team === CharacterTeam.decla) { this.ownership -= (0.5 / settings.takeControlTimeInSeconds) * deltaTime; diff --git a/backend/src/players/player-container.ts b/backend/src/players/player-container.ts index ca75831..c8900a8 100644 --- a/backend/src/players/player-container.ts +++ b/backend/src/players/player-container.ts @@ -22,7 +22,7 @@ export class PlayerContainer { this.npcMaxCount - this._npcs.length, ); for (let i = 0; i < newNpcCount; i++) { - const name = `BOT ${Random.choose(settings.npcNames)}`; + const name = `🤖 ${Random.choose(settings.npcNames)}`; this._npcs.push( new NPC({ name }, this, this.objects, this.getTeamOfNextPlayer(true)), ); @@ -60,6 +60,14 @@ export class PlayerContainer { this.players.forEach((p) => p.step(deltaTimeInSeconds)); } + public stepCommunication(deltaTimeInSeconds: number) { + this._players.forEach((p) => p.stepCommunications(deltaTimeInSeconds)); + } + + public endGame(winner: CharacterTeam) { + this._players.forEach((p) => p.onGameEnded(winner)); + } + public queueCommandForEachClient(command: Command) { this._players.forEach((p) => p.queueCommandSend(command)); } diff --git a/backend/src/players/player.ts b/backend/src/players/player.ts index 06123b8..c58b36f 100644 --- a/backend/src/players/player.ts +++ b/backend/src/players/player.ts @@ -32,7 +32,8 @@ import { PlayerBase } from './player-base'; export class Player extends PlayerBase { // default, until the clients sends its real value private aspectRatio: number = 16 / 9; - + private timeUntilRespawn = 0; + private timeSinceLastMessage = 0; private objectsPreviouslyInViewArea: Array = []; protected commandExecutors: CommandExecutors = { @@ -68,12 +69,12 @@ export class Player extends PlayerBase { this.queueCommandSend(new CreatePlayerCommand(this.character!)); } - private timeSinceLastMessage = 0; - private messageInterval = 1 / 30; - private timeUntilRespawn = 0; - public step(deltaTimeInSeconds: number) { - this.timeSinceLastMessage += deltaTimeInSeconds; + private winnerTeam?: CharacterTeam; + public onGameEnded(winnerTeam: CharacterTeam) { + this.winnerTeam = winnerTeam; + } + public step(deltaTimeInSeconds: number) { if (this.character) { this.center = this.character?.center; @@ -85,72 +86,60 @@ export class Player extends PlayerBase { this.timeUntilRespawn = settings.playerDiedTimeout; } } else { - if (this.timeSinceLastMessage > this.messageInterval) { - this.queueCommandSend( - new ServerAnnouncement(`Reviving in ${Math.round(this.timeUntilRespawn)}…`), - ); - } - if ((this.timeUntilRespawn -= deltaTimeInSeconds) < 0) { this.createCharacter(); this.center = this.character!.center; - this.queueCommandSend(new ServerAnnouncement('')); } } - if (this.timeSinceLastMessage > this.messageInterval) { - const viewArea = calculateViewArea(this.center, this.aspectRatio, 1.2); - const bb = new BoundingBox(); - bb.topLeft = viewArea.topLeft; - bb.size = viewArea.size; + const remoteCalls = this.objectsPreviouslyInViewArea + .map((g) => new RemoteCallsForObject(g.id, g.getRemoteCalls())) + .filter((c) => c.calls.length > 0); - const objectsInViewArea = Array.from( - new Set(this.objectContainer.findIntersecting(bb).map((o) => o.gameObject)), - ); - - const newlyIntersecting = objectsInViewArea.filter( - (o) => !this.objectsPreviouslyInViewArea.includes(o), - ); - - const noLongerIntersecting = this.objectsPreviouslyInViewArea.filter( - (o) => !objectsInViewArea.includes(o), - ); - - this.objectsPreviouslyInViewArea = objectsInViewArea; - - if (noLongerIntersecting.length > 0) { - this.queueCommandSend( - new DeleteObjectsCommand(noLongerIntersecting.map((g) => g.id)), - ); - } - - if (newlyIntersecting.length > 0) { - this.queueCommandSend(new CreateObjectsCommand(newlyIntersecting)); - } - - this.queueCommandSend(new UpdateOtherPlayerDirections(this.getOtherPlayers())); - - this.queueCommandSend( - new PropertyUpdatesForObjects( - this.objectsPreviouslyInViewArea - .map((o) => o.getPropertyUpdates()) - .filter((u) => u) as Array, - ), - ); + if (remoteCalls.length > 0) { + this.queueCommandSend(new RemoteCallsForObjects(remoteCalls)); } + } - this.queueCommandSend( - new RemoteCallsForObjects( - this.objectsPreviouslyInViewArea.map( - (g) => new RemoteCallsForObject(g.id, g.getRemoteCalls()), - ), - ), + private handleViewAreaUpdate() { + const viewArea = calculateViewArea(this.center, this.aspectRatio, 1.2); + const bb = new BoundingBox(); + bb.topLeft = viewArea.topLeft; + bb.size = viewArea.size; + + const objectsInViewArea = Array.from( + new Set(this.objectContainer.findIntersecting(bb).map((o) => o.gameObject)), ); - if (this.timeSinceLastMessage > this.messageInterval) { - this.sendQueuedCommandsToClient(); - this.timeSinceLastMessage = 0; + const newlyIntersecting = objectsInViewArea.filter( + (o) => !this.objectsPreviouslyInViewArea.includes(o), + ); + + const noLongerIntersecting = this.objectsPreviouslyInViewArea.filter( + (o) => !objectsInViewArea.includes(o), + ); + + this.objectsPreviouslyInViewArea = objectsInViewArea; + + if (noLongerIntersecting.length > 0) { + this.queueCommandSend( + new DeleteObjectsCommand(noLongerIntersecting.map((g) => g.id)), + ); } + + if (newlyIntersecting.length > 0) { + this.queueCommandSend(new CreateObjectsCommand(newlyIntersecting)); + } + + this.queueCommandSend(new UpdateOtherPlayerDirections(this.getOtherPlayers())); + + this.queueCommandSend( + new PropertyUpdatesForObjects( + this.objectsPreviouslyInViewArea + .map((o) => o.getPropertyUpdates()) + .filter((u) => u) as Array, + ), + ); } private getOtherPlayers(): Array { @@ -190,11 +179,33 @@ export class Player extends PlayerBase { this.commandsToBeSent.push(command); } + public stepCommunications(deltaTime: number) { + if ((this.timeSinceLastMessage += deltaTime) > settings.updateMessageInterval) { + this.handleAnnouncements(); + this.handleViewAreaUpdate(); + this.sendQueuedCommandsToClient(); + this.timeSinceLastMessage = 0; + } + } + public sendQueuedCommandsToClient() { this.socket.emit(TransportEvents.ServerToPlayer, serialize(this.commandsToBeSent)); this.commandsToBeSent = []; } + private handleAnnouncements() { + let announcement = ''; + if (this.winnerTeam) { + announcement = `Team ${this.winnerTeam} won 🎉`; + } else if (!this.character) { + announcement = `Reviving in ${Math.round(this.timeUntilRespawn)}…`; + } + + if (announcement) { + this.queueCommandSend(new ServerAnnouncement(announcement)); + } + } + public destroy() { super.destroy(); } diff --git a/frontend/src/scripts/game.ts b/frontend/src/scripts/game.ts index 3d80d94..b86d0e7 100644 --- a/frontend/src/scripts/game.ts +++ b/frontend/src/scripts/game.ts @@ -81,6 +81,7 @@ export class Game extends CommandReceiver { this.lastAnnouncementText = ''; this.overlay.appendChild(this.progressBar); this.announcementText.innerText = ''; + this.timeScaling = 1; this.overlay.appendChild(this.announcementText); this.socket = io(this.playerDecision.server, { @@ -128,16 +129,16 @@ export class Game extends CommandReceiver { private lastGameState?: UpdateGameState; private isEnding = false; + private timeScaling = 1; + private lastAnnouncementText = ''; protected commandExecutors: CommandExecutors = { - [ServerAnnouncement.type]: (c: ServerAnnouncement) => - (this.lastAnnouncementText = c.text), - [UpdateGameState.type]: (c: UpdateGameState) => (this.lastGameState = c), - [GameEndCommand.type]: (c: GameEndCommand) => { - const team = `${c.winningTeam}`; - this.lastAnnouncementText = `Team ${team} won 🎉`; - this.isEnding = true; + [ServerAnnouncement.type]: (c: ServerAnnouncement) => { + this.lastAnnouncementText = c.text; + this.timeSinceLastAnnouncement = 0; }, + [UpdateGameState.type]: (c: UpdateGameState) => (this.lastGameState = c), + [GameEndCommand.type]: () => (this.isEnding = true), [UpdateOtherPlayerDirections.type]: (c: UpdateOtherPlayerDirections) => (this.lastOtherPlayerDirections = c), [GameStartCommand.type]: this.initialize.bind(this), @@ -255,6 +256,7 @@ export class Game extends CommandReceiver { this.isActive = false; } + private timeSinceLastAnnouncement = 0; private framesSinceLastLayoutUpdate = 0; private gameLoop( renderer: Renderer, @@ -271,10 +273,19 @@ export class Game extends CommandReceiver { this.draw(); } + if ((this.timeSinceLastAnnouncement += deltaTime) > 0.5) { + this.lastAnnouncementText = ''; + } + + if (this.isEnding) { + this.timeScaling *= Math.pow(settings.endGameDeltaScaling, deltaTime); + deltaTime /= this.timeScaling; + } + this.renderer = renderer; this.socketReceiver.sendQueuedCommands(); - this.gameObjects.stepObjects(this.isEnding ? 0 : deltaTime); + this.gameObjects.stepObjects(deltaTime); this.gameObjects.drawObjects(this.renderer, this.overlay, shouldChangeLayout); return this.isActive; diff --git a/frontend/src/scripts/join-form-handler.ts b/frontend/src/scripts/join-form-handler.ts index 70874b9..93a34a0 100644 --- a/frontend/src/scripts/join-form-handler.ts +++ b/frontend/src/scripts/join-form-handler.ts @@ -15,19 +15,20 @@ export class JoinFormHandler { private waitingForDecision: Promise; private resolvePlayerDecision!: (d: PlayerDecision) => void; private pollServersTimer: any; + private keyUpListener = (e: KeyboardEvent) => { + if (e.key === 'enter') { + this.form.submit(); + } + }; - constructor(form: HTMLFormElement, private readonly container: HTMLElement) { + constructor(private form: HTMLFormElement, private readonly container: HTMLElement) { this.joinButton = form.querySelector('button[type="submit"]') as HTMLButtonElement; this.joinButton.disabled = true; this.waitingForDecision = new Promise((r) => (this.resolvePlayerDecision = r)); new FormData(form); - document.addEventListener('keyup', (e) => { - if (e.key === 'enter') { - form.submit(); - } - }); + addEventListener('keyup', this.keyUpListener); form.onsubmit = (e) => { SoundHandler.play(Sounds.click); @@ -54,6 +55,7 @@ export class JoinFormHandler { } private destroy() { + removeEventListener('keyup', this.keyUpListener); clearInterval(this.pollServersTimer); this.servers.forEach((s) => s.destroy()); } diff --git a/frontend/src/scripts/objects/camera.ts b/frontend/src/scripts/objects/camera.ts index cc44a0a..979ba03 100644 --- a/frontend/src/scripts/objects/camera.ts +++ b/frontend/src/scripts/objects/camera.ts @@ -7,7 +7,6 @@ import { ViewObject } from './view-object'; export class Camera extends GameObject implements ViewObject { public center: vec2 = vec2.create(); - private aspectRatio?: number; constructor(private game: Game) { @@ -15,12 +14,10 @@ export class Camera extends GameObject implements ViewObject { } public updateProperties(update: UpdateProperty[]): void {} - + public step(deltaTimeInSeconds: number): void {} public beforeDestroy(): void {} - public step(deltaTimeInSeconds: number): void {} - - public draw(renderer: Renderer, overlay: HTMLElement, shouldChangeLayout: boolean) { + public draw(renderer: Renderer) { const canvasAspectRatio = renderer.canvasSize.x / renderer.canvasSize.y; if (canvasAspectRatio !== this.aspectRatio) { this.aspectRatio = canvasAspectRatio; diff --git a/frontend/src/scripts/objects/planet-view.ts b/frontend/src/scripts/objects/planet-view.ts index 85fe69a..7f0d58a 100644 --- a/frontend/src/scripts/objects/planet-view.ts +++ b/frontend/src/scripts/objects/planet-view.ts @@ -14,19 +14,17 @@ type FallingPoint = { export class PlanetView extends PlanetBase implements ViewObject { private shape: PlanetShape; - private ownershipProgess: HTMLElement; + private ownershipProgress: HTMLElement; constructor(id: Id, vertices: Array, ownership: number) { super(id, vertices); this.shape = new PlanetShape(vertices, ownership); (this.shape as any).randomOffset = Random.getRandom(); - this.ownershipProgess = document.createElement('div'); - this.ownershipProgess.className = 'ownership'; + this.ownershipProgress = document.createElement('div'); + this.ownershipProgress.className = 'ownership'; } - public updateProperties(update: UpdateProperty[]): void {} - public step(deltaTimeInSeconds: number): void { this.shape.randomOffset += deltaTimeInSeconds / 4; this.shape.colorMixQ = this.ownership; @@ -56,20 +54,26 @@ export class PlanetView extends PlanetBase implements ViewObject { } public beforeDestroy(): void { - this.ownershipProgess.parentElement?.removeChild(this.ownershipProgess); + this.ownershipProgress.parentElement?.removeChild(this.ownershipProgress); this.generatedPointElements.forEach((p) => p.element.parentElement?.removeChild(p.element), ); } + public updateProperties(update: UpdateProperty[]): void { + update.forEach((u) => { + this.ownership = u.propertyValue; + }); + } + public draw( renderer: Renderer, overlay: HTMLElement, shouldChangeLayout: boolean, ): void { if (shouldChangeLayout) { - if (!this.ownershipProgess.parentElement) { - overlay.appendChild(this.ownershipProgess); + if (!this.ownershipProgress.parentElement) { + overlay.appendChild(this.ownershipProgress); } const screenPosition = renderer.worldToDisplayCoordinates(this.center); @@ -94,8 +98,8 @@ export class PlanetView extends PlanetBase implements ViewObject { (p) => p.timeToLive > 0, ); - this.ownershipProgess.style.transform = `translateX(${screenPosition.x}px) translateY(${screenPosition.y}px) translateX(-50%) translateY(-50%)`; - this.ownershipProgess.style.background = this.getGradient(); + this.ownershipProgress.style.transform = `translateX(${screenPosition.x}px) translateY(${screenPosition.y}px) translateX(-50%) translateY(-50%)`; + this.ownershipProgress.style.background = this.getGradient(); if (this.lastGeneratedPoint !== undefined) { const element = document.createElement('div'); diff --git a/frontend/webpack.config.js b/frontend/webpack.config.js index c4bcfa1..33ecee1 100644 --- a/frontend/webpack.config.js +++ b/frontend/webpack.config.js @@ -4,10 +4,11 @@ const TsConfigWebpackPlugin = require('ts-config-webpack-plugin'); const HtmlWebpackInlineSVGPlugin = require('html-webpack-inline-svg-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const TerserJSPlugin = require('terser-webpack-plugin'); +// const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; const HtmlWebpackInlineSourcePlugin = require('html-webpack-inline-source-plugin'); const Sass = require('sass'); -module.exports = { +module.exports = (env, argv) => ({ devServer: { host: '0.0.0.0', disableHostCheck: true, @@ -18,9 +19,10 @@ module.exports = { plugins: [ new CleanWebpackPlugin(), new MiniCssExtractPlugin(), + //new BundleAnalyzerPlugin(), new HtmlWebpackPlugin({ template: './src/index.html', - //inlineSource: '.(css)$', + inlineSource: argv.mode === 'development' ? '' : '.(css)$', }), new HtmlWebpackInlineSourcePlugin(HtmlWebpackPlugin), new HtmlWebpackInlineSVGPlugin({ @@ -31,7 +33,6 @@ module.exports = { }, ], }), - new TsConfigWebpackPlugin(), ], optimization: { @@ -106,4 +107,4 @@ module.exports = { }, ], }, -}; +}); diff --git a/shared/src/objects/game-object.ts b/shared/src/objects/game-object.ts index c3cb937..05fba25 100644 --- a/shared/src/objects/game-object.ts +++ b/shared/src/objects/game-object.ts @@ -1,4 +1,3 @@ -import { Command } from '../commands/command'; import { Id } from '../communication/id'; import { serializable } from '../serialization/serializable'; diff --git a/shared/src/objects/types/planet-base.ts b/shared/src/objects/types/planet-base.ts index 57aafd6..9d1d526 100644 --- a/shared/src/objects/types/planet-base.ts +++ b/shared/src/objects/types/planet-base.ts @@ -19,10 +19,6 @@ export class PlanetBase extends GameObject { vec2.scale(this.center, this.center, 1 / vertices.length); } - public setOwnership(value: number) { - this.ownership = value; - } - public generatedPoints(value: number) {} public static createPlanetVertices( diff --git a/shared/src/settings.ts b/shared/src/settings.ts index e451ed3..75c08a6 100644 --- a/shared/src/settings.ts +++ b/shared/src/settings.ts @@ -12,12 +12,11 @@ const redPlanetColor = redColorDim; export const settings = { lightCutoffDistance: 600, - maxVelocityX: 1000, - maxVelocityY: 1000, radiusSteps: 500, spawnDespawnTime: 0.7, worldRadius: 10000, objectsOnCircleLength: 0.002, + updateMessageInterval: 1 / 25, planetEdgeCount: 7, playerKillPoint: 10, takeControlTimeInSeconds: 4, @@ -29,9 +28,9 @@ export const settings = { planetControlThreshold: 0.2, playerMaxHealth: 100, maxGravityStrength: 10000, - maxAcceleration: 60000, + maxAcceleration: 120000, playerMaxStrength: 80, - endGameDeltaScaling: 4, + endGameDeltaScaling: 2, playerDiedTimeout: 5, playerStrengthRegenerationPerSeconds: 80, projectileMaxStrength: 40, @@ -47,58 +46,50 @@ export const settings = { npcNames: [ 'Adam', 'Andrew', + 'Blaise', 'Clarence', + 'Dean', + 'Dustin', 'Elliot', - 'Elmer', 'Ernie', - 'Eugene', - 'Fergus', - 'Ferris', + 'Ethan', 'Frank', - 'Frasier', 'Fred', 'George', 'Graham', + 'Harold', 'Harvey', + 'Henry', + 'Mingan', + 'Irving', 'Irwin', + 'Jason', + 'Jenssen', + 'Josh', + 'Ladislaus', 'Larry', 'Lester', + 'Martin', 'Marvin', 'Neil', + 'Nick', 'Niles', + 'Norm', 'Oliver', - 'Blaise', - 'Opie', + 'Orin', + 'Pat', + 'Perry', + 'Ron', 'Ryan', + 'Sisyphus', + 'Tim', 'Toby', - 'Ulric', 'Ulysses', 'Uri', 'Waldo', 'Wally', 'Walt', 'Wesley', - 'Yanni', - 'Yogi', - 'Yuri', - 'Dean', - 'Dustin', - 'Ethan', - 'Harold', - 'Henry', - 'Irving', - 'Jason', - 'Jenssen', - 'Josh', - 'Martin', - 'Nick', - 'Norm', - 'Orin', - 'Pat', - 'Perry', - 'Ron', - 'Shawn', - 'Tim', 'Will', 'Wyatt', ], @@ -111,6 +102,6 @@ export const settings = { }, palette: [declaColor, neutralColor, redColor], paletteDim: [declaColorDim, neutralColor, redColorDim], - targetPhysicsDeltaTimeInSeconds: 1 / 100, + targetPhysicsDeltaTimeInSeconds: 1 / 200, inViewAreaSize: 1920 * 1080 * 4, };