From 7c76b16d13e2202f53f3581492baba27437ecb0e Mon Sep 17 00:00:00 2001 From: schmelczerandras Date: Tue, 20 Oct 2020 08:56:38 +0200 Subject: [PATCH] Improve gameplay --- .do/apps.yml | 2 +- Dockerfile | 4 +- backend/src/game-server.ts | 4 +- backend/src/map/create-world.ts | 21 +- backend/src/objects/circle-physical.ts | 4 - backend/src/objects/planet-physical.ts | 52 ++++- .../src/objects/player-character-physical.ts | 61 +++++- backend/src/objects/projectile-physical.ts | 28 ++- .../src/physics/physicals/dynamic-physical.ts | 3 - backend/src/players/player-color-service.ts | 15 -- backend/src/players/player-team-service.ts | 22 +++ backend/src/players/player.ts | 186 +++++++++--------- frontend/package.json | 2 +- frontend/src/index.html | 31 ++- frontend/src/index.ts | 16 +- frontend/src/scripts/game.ts | 131 +++++++----- frontend/src/scripts/handle-full-screen.ts | 27 +++ .../scripts/helper/delta-time-calculator.ts | 25 --- .../src/scripts/landing-page-background.ts | 87 ++++---- frontend/src/scripts/objects/camera.ts | 6 +- .../src/scripts/objects/character-view.ts | 14 +- .../scripts/objects/game-object-container.ts | 13 +- frontend/src/scripts/objects/lamp-view.ts | 8 +- frontend/src/scripts/objects/planet-view.ts | 50 ++++- .../scripts/objects/player-character-view.ts | 61 +++++- .../src/scripts/objects/projectile-view.ts | 30 +-- frontend/src/scripts/objects/view-object.ts | 4 +- frontend/src/scripts/shapes/circle.ts | 3 - frontend/src/scripts/shapes/planet-shape.ts | 163 +++++++++++++++ frontend/src/scripts/shapes/polygon.ts | 4 - frontend/src/styles/form.scss | 55 +++--- frontend/src/styles/main.scss | 109 +++++++++- frontend/src/styles/mixins.scss | 11 ++ frontend/static/declared.png | Bin 26027 -> 0 bytes frontend/static/declared.psd | 3 - frontend/static/maximize.svg | 7 + frontend/static/minimize.svg | 7 + frontend/static/settings.svg | 4 + frontend/tsconfig.json | 5 +- frontend/webpack.config.js | 10 + shared/src/commands/types/player-died.ts | 14 ++ .../commands/types/update-planet-ownership.ts | 17 ++ shared/src/helper/hsl.ts | 34 ++++ shared/src/main.ts | 4 + shared/src/objects/game-object.ts | 9 + shared/src/objects/types/character-base.ts | 7 +- shared/src/objects/types/character-team.ts | 4 + shared/src/objects/types/planet-base.ts | 12 +- .../objects/types/player-character-base.ts | 9 +- shared/src/objects/types/projectile-base.ts | 10 +- shared/src/settings.ts | 64 +++--- shared/tsconfig.json | 3 +- tsconfig.json | 13 ++ 53 files changed, 1084 insertions(+), 404 deletions(-) delete mode 100644 backend/src/players/player-color-service.ts create mode 100644 backend/src/players/player-team-service.ts create mode 100644 frontend/src/scripts/handle-full-screen.ts delete mode 100644 frontend/src/scripts/helper/delta-time-calculator.ts delete mode 100644 frontend/src/scripts/shapes/circle.ts create mode 100644 frontend/src/scripts/shapes/planet-shape.ts delete mode 100644 frontend/src/scripts/shapes/polygon.ts create mode 100644 frontend/src/styles/mixins.scss delete mode 100644 frontend/static/declared.png delete mode 100644 frontend/static/declared.psd create mode 100644 frontend/static/maximize.svg create mode 100644 frontend/static/minimize.svg create mode 100644 frontend/static/settings.svg create mode 100644 shared/src/commands/types/player-died.ts create mode 100644 shared/src/commands/types/update-planet-ownership.ts create mode 100644 shared/src/helper/hsl.ts create mode 100644 shared/src/objects/types/character-team.ts create mode 100644 tsconfig.json diff --git a/.do/apps.yml b/.do/apps.yml index 0a41826..3727b4b 100644 --- a/.do/apps.yml +++ b/.do/apps.yml @@ -9,7 +9,7 @@ services: http_port: 3000 instance_count: 1 instance_size_slug: basic-xxs - run_command: node main.js --name=Frankfurt --seed=102 + run_command: ' node main.js --name=Frankfurt --seed=102' name: decla-red-server routes: - path: / diff --git a/Dockerfile b/Dockerfile index 79c3b51..0a1d9b6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,4 +13,6 @@ RUN npm install --production COPY --from=build backend/dist/main.js main.js EXPOSE 3000 -CMD [ "node", "main.js" ] + +CMD ["--port=3000", "--name=Docker server", "--seed=500"] +ENTRYPOINT [ "node", "main.js" ] diff --git a/backend/src/game-server.ts b/backend/src/game-server.ts index 2c8403d..93446a0 100644 --- a/backend/src/game-server.ts +++ b/backend/src/game-server.ts @@ -29,7 +29,7 @@ export class GameServer { io.on('connection', (socket: SocketIO.Socket) => { socket.on(TransportEvents.PlayerJoining, (playerInfo: PlayerInformation) => { - const player = new Player(playerInfo, this.objects, socket); + const player = new Player(playerInfo, this.players, this.objects, socket); this.players.push(player); socket.on(TransportEvents.PlayerToServer, (json: string) => { const command = deserialize(json); @@ -61,7 +61,7 @@ export class GameServer { this.deltaTimes.sort((a, b) => a - b); console.log( `Median physics time: ${this.deltaTimes[ - framesBetweenDeltaTimeCalculation + Math.floor(framesBetweenDeltaTimeCalculation / 2) ].toFixed(2)} ms`, ); console.log( diff --git a/backend/src/map/create-world.ts b/backend/src/map/create-world.ts index d6e9e71..f6c7001 100644 --- a/backend/src/map/create-world.ts +++ b/backend/src/map/create-world.ts @@ -1,5 +1,5 @@ import { vec2, vec3 } from 'gl-matrix'; -import { Random, settings, PlanetBase } from 'shared'; +import { Random, settings, PlanetBase, hsl } from 'shared'; import { LampPhysical } from '../objects/lamp-physical'; import { PlanetPhysical } from '../objects/planet-physical'; import { PhysicalContainer } from '../physics/containers/physical-container'; @@ -17,7 +17,7 @@ export const createWorld = (objectContainer: PhysicalContainer) => { ), ); - for (let i = 0; i < worldSize / 400; i++) { + for (let i = 0; i < 15; i++) { console.log('planet', i); let position: vec2; @@ -44,7 +44,7 @@ export const createWorld = (objectContainer: PhysicalContainer) => { ); } - for (let i = 0; i < worldSize / 350; i++) { + for (let i = 0; i < 15; i++) { console.log('light', i); let position: vec2; do { @@ -54,20 +54,17 @@ export const createWorld = (objectContainer: PhysicalContainer) => { ); } while ( evaluateSdf(position, objects) < 200 || - lights.find((l) => l.distance(position) < 1800) + lights.find((l) => l.distance(position) < 1500) ); lights.push( new LampPhysical( position, - vec3.normalize( - vec3.create(), - vec3.fromValues( - Random.getRandomInRange(0.5, 1), - 0, - Random.getRandomInRange(0.5, 1), - ), + hsl( + Random.getRandomInRange(0, 360), + Random.getRandomInRange(50, 100), + Random.getRandomInRange(25, 50), ), - Random.getRandomInRange(0.25, 1), + Random.getRandomInRange(0.5, 2), ), ); } diff --git a/backend/src/objects/circle-physical.ts b/backend/src/objects/circle-physical.ts index 753167f..6e1afec 100644 --- a/backend/src/objects/circle-physical.ts +++ b/backend/src/objects/circle-physical.ts @@ -30,10 +30,6 @@ export class CirclePhysical implements Circle, DynamicPhysical, ReactsToCollisio this.recalculateBoundingBox(); } - public calculateUpdates(): UpdateObjectMessage | null { - return null; - } - public get boundingBox(): BoundingBoxBase { return this._boundingBox; } diff --git a/backend/src/objects/planet-physical.ts b/backend/src/objects/planet-physical.ts index a53b053..561a4c4 100644 --- a/backend/src/objects/planet-physical.ts +++ b/backend/src/objects/planet-physical.ts @@ -9,23 +9,28 @@ import { serializesTo, settings, PlanetBase, + CharacterTeam, + UpdateObjectMessage, } from 'shared'; import { ImmutableBoundingBox } from '../physics/bounding-boxes/immutable-bounding-box'; import { StaticPhysical } from '../physics/physicals/static-physical'; +import { UpdateGameObjectMessage } from '../update-game-object-message'; @serializesTo(PlanetBase) export class PlanetPhysical extends PlanetBase implements StaticPhysical { public readonly canCollide = true; public readonly canMove = false; - private center: vec2; + + public static neutralPlanetCount = 0; + public static declaPlanetCount = 0; + public static redPlanetCount = 0; private _boundingBox?: ImmutableBoundingBox; constructor(vertices: Array) { super(id(), vertices); - this.center = vertices.reduce((sum, v) => vec2.add(sum, sum, v), vec2.create()); - vec2.scale(this.center, this.center, 1 / vertices.length); + PlanetPhysical.neutralPlanetCount++; } public distance(target: vec2): number { @@ -62,6 +67,47 @@ export class PlanetPhysical extends PlanetBase implements StaticPhysical { return sign * d; } + public calculateUpdates(): UpdateObjectMessage { + return new UpdateGameObjectMessage(this, ['ownership']); + } + + public takeControl(team: CharacterTeam, deltaTime: number) { + const previousOwnership = this.ownership; + if (team === CharacterTeam.decla) { + this.ownership -= (0.5 / settings.takeControlTimeInSeconds) * deltaTime; + if ( + previousOwnership >= 0.5 - settings.planetControlThreshold && + this.ownership < 0.5 - settings.planetControlThreshold + ) { + PlanetPhysical.declaPlanetCount++; + PlanetPhysical.neutralPlanetCount--; + } else if ( + previousOwnership > 0.5 + settings.planetControlThreshold && + previousOwnership <= 0.5 + settings.planetControlThreshold + ) { + PlanetPhysical.redPlanetCount--; + PlanetPhysical.neutralPlanetCount++; + } + } else { + this.ownership += (0.5 / settings.takeControlTimeInSeconds) * deltaTime; + if ( + previousOwnership < 0.5 + settings.planetControlThreshold && + this.ownership >= 0.5 + settings.planetControlThreshold + ) { + PlanetPhysical.redPlanetCount++; + PlanetPhysical.neutralPlanetCount--; + } else if ( + previousOwnership <= 0.5 - settings.planetControlThreshold && + previousOwnership > 0.5 + settings.planetControlThreshold + ) { + PlanetPhysical.declaPlanetCount--; + PlanetPhysical.neutralPlanetCount++; + } + } + + this.ownership = clamp01(this.ownership); + } + public get boundingBox(): ImmutableBoundingBox { if (!this._boundingBox) { const { xMin, xMax, yMin, yMax } = this.vertices.reduce( diff --git a/backend/src/objects/player-character-physical.ts b/backend/src/objects/player-character-physical.ts index 9e50b30..43955cd 100644 --- a/backend/src/objects/player-character-physical.ts +++ b/backend/src/objects/player-character-physical.ts @@ -9,6 +9,7 @@ import { GameObject, Circle, PlayerCharacterBase, + CharacterTeam, } from 'shared'; import { DynamicPhysical } from '../physics/physicals/dynamic-physical'; import { CirclePhysical } from './circle-physical'; @@ -32,6 +33,8 @@ export class PlayerCharacterPhysical private static readonly headRadius = 50; private static readonly feetRadius = 20; + private projectileStrength = settings.playerMaxStrength; + // offsets are meassured from (0, 0) private static readonly desiredHeadOffset = vec2.fromValues(0, 65); private static readonly desiredLeftFootOffset = vec2.fromValues(-20, 0); @@ -93,10 +96,11 @@ export class PlayerCharacterPhysical constructor( name: string, public readonly colorIndex: number, + team: CharacterTeam, private readonly container: PhysicalContainer, startPosition: vec2, ) { - super(id(), name, colorIndex); + super(id(), name, colorIndex, team, settings.playerMaxHealth); this.head = new CirclePhysical( vec2.add(vec2.create(), startPosition, PlayerCharacterPhysical.headOffset), @@ -129,7 +133,7 @@ export class PlayerCharacterPhysical } public calculateUpdates(): UpdateObjectMessage { - return new UpdateGameObjectMessage(this, ['head', 'leftFoot', 'rightFoot']); + return new UpdateGameObjectMessage(this, ['head', 'leftFoot', 'rightFoot', 'health']); } public handleMovementAction(c: MoveActionCommand) { @@ -137,12 +141,44 @@ export class PlayerCharacterPhysical } public onCollision(other: GameObject) { - if (other instanceof ProjectilePhysical) { + if (other instanceof ProjectilePhysical && other.team !== this.team) { other.destroy(); - this.destroy(); + this.health -= other.strength; + if (this.health <= 0) { + this.destroy(); + } } } + public shootTowards(position: vec2) { + if (!this.isAlive) { + return; + } + + const start = vec2.clone(this.center); + const direction = vec2.subtract(vec2.create(), position, start); + vec2.normalize(direction, direction); + vec2.add( + start, + start, + vec2.scale(vec2.create(), direction, settings.projectileStartOffset), + ); + const velocity = vec2.scale(direction, direction, settings.projectileSpeed); + vec2.add(velocity, velocity, this.velocity); + const strength = this.projectileStrength / 2; + this.projectileStrength -= strength; + const projectile = new ProjectilePhysical( + start, + 20, + this.colorIndex, + strength, + this.team, + velocity, + this.container, + ); + this.container.addObject(projectile); + } + public get boundingBox(): BoundingBoxBase { this.bound.center = this.head.center; return this.bound.boundingBox; @@ -197,6 +233,13 @@ export class PlayerCharacterPhysical this.currentPlanet = undefined; } + this.projectileStrength = Math.min( + settings.playerMaxStrength, + this.projectileStrength + settings.playerStrengthRegenerationPerSeconds * deltaTime, + ); + + this.currentPlanet?.takeControl(this.team, deltaTime); + const intersectingWithForcefield = this.container.findIntersecting( getBoundingBoxOfCircle( new Circle( @@ -205,7 +248,13 @@ export class PlayerCharacterPhysical ), ), ); - const actualGravity = forceAtPosition(this.center, intersectingWithForcefield); + const feetCenter = vec2.add( + vec2.create(), + this.leftFoot.center, + this.rightFoot.center, + ); + vec2.scale(feetCenter, feetCenter, 0.5); + const actualGravity = forceAtPosition(feetCenter, intersectingWithForcefield); const direction = this.averageAndResetMovementActions(); const movementForce = vec2.scale(direction, direction, settings.maxAcceleration); @@ -269,7 +318,7 @@ export class PlayerCharacterPhysical private springMove(object: CirclePhysical, center: vec2, offset: vec2) { // todo: make time-independent - const springConstant = 0.35; + const springConstant = 0.55; const desiredPosition = vec2.add(vec2.create(), center, offset); vec2.rotate(desiredPosition, desiredPosition, center, this.direction); diff --git a/backend/src/objects/projectile-physical.ts b/backend/src/objects/projectile-physical.ts index 336cade..e7d6886 100644 --- a/backend/src/objects/projectile-physical.ts +++ b/backend/src/objects/projectile-physical.ts @@ -1,5 +1,12 @@ import { vec2 } from 'gl-matrix'; -import { id, settings, serializesTo, ProjectileBase, GameObject } from 'shared'; +import { + id, + settings, + serializesTo, + ProjectileBase, + GameObject, + CharacterTeam, +} from 'shared'; import { ImmutableBoundingBox } from '../physics/bounding-boxes/immutable-bounding-box'; import { CirclePhysical } from './circle-physical'; import { DynamicPhysical } from '../physics/physicals/dynamic-physical'; @@ -8,6 +15,7 @@ import { PlanetPhysical } from './planet-physical'; import { ReactsToCollision } from '../physics/physicals/reacts-to-collision'; import { UpdateObjectMessage } from 'shared/lib/src/objects/update-object-message'; import { UpdateGameObjectMessage } from '../update-game-object-message'; +import { PlayerCharacterPhysical } from './player-character-physical'; @serializesTo(ProjectileBase) export class ProjectilePhysical @@ -15,6 +23,7 @@ export class ProjectilePhysical implements DynamicPhysical, ReactsToCollision { public readonly canCollide = true; public readonly canMove = true; + private isDestroyed = false; private bounceCount = 0; private _boundingBox?: ImmutableBoundingBox; @@ -24,15 +33,18 @@ export class ProjectilePhysical constructor( center: vec2, radius: number, + colorIndex: number, + public strength: number, + public team: CharacterTeam, private velocity: vec2, readonly container: PhysicalContainer, ) { - super(id(), center, radius); + super(id(), center, radius, colorIndex, strength); this.object = new CirclePhysical(center, radius, this, container, 0.9); } public calculateUpdates(): UpdateObjectMessage { - return new UpdateGameObjectMessage(this, ['center']); + return new UpdateGameObjectMessage(this, ['center', 'strength']); } public get boundingBox(): ImmutableBoundingBox { @@ -59,12 +71,20 @@ export class ProjectilePhysical } public onCollision(other: GameObject) { - if (this.bounceCount++ === settings.projectileMaxBounceCount) { + if ( + !(other instanceof PlayerCharacterPhysical && other.team === this.team) && + this.bounceCount++ === settings.projectileMaxBounceCount + ) { this.destroy(); } } public step(deltaTime: number) { + if ((this.strength -= settings.projectileFadeSpeed * deltaTime) < 0) { + this.destroy(); + return; + } + const gravity = vec2.create(); const intersecting = this.container.findIntersecting(this.boundingBox); intersecting.forEach((i) => { diff --git a/backend/src/physics/physicals/dynamic-physical.ts b/backend/src/physics/physicals/dynamic-physical.ts index c5b1cc5..a5e03c1 100644 --- a/backend/src/physics/physicals/dynamic-physical.ts +++ b/backend/src/physics/physicals/dynamic-physical.ts @@ -1,9 +1,6 @@ -import { UpdateObjectMessage } from 'shared/lib/src/objects/update-object-message'; import { PhysicalBase } from './physical-base'; export interface DynamicPhysical extends PhysicalBase { readonly canMove: true; step(deltaTimeInMilliseconds: number): void; - - calculateUpdates(): UpdateObjectMessage | null; } diff --git a/backend/src/players/player-color-service.ts b/backend/src/players/player-color-service.ts deleted file mode 100644 index e23d49d..0000000 --- a/backend/src/players/player-color-service.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { settings } from 'shared'; - -const colorUsage: Array = new Array(settings.playerColors.length).fill(false); - -export const requestColor = (): number => { - const index = colorUsage.findIndex((a) => !a); - if (index >= 0) { - colorUsage[index] = true; - } - return index + settings.playerColorIndexOffset; -}; - -export const freeColor = (index: number) => { - colorUsage[index - settings.playerColorIndexOffset] = false; -}; diff --git a/backend/src/players/player-team-service.ts b/backend/src/players/player-team-service.ts new file mode 100644 index 0000000..39d1ce3 --- /dev/null +++ b/backend/src/players/player-team-service.ts @@ -0,0 +1,22 @@ +import { CharacterTeam, Random } from 'shared'; + +let declaCount = 0; +let redCount = 0; + +export const requestTeam = (): { team: CharacterTeam; colorIndex: number } => { + if ((declaCount === redCount && Random.getRandom() > 0.5) || declaCount < redCount) { + declaCount++; + return { team: CharacterTeam.decla, colorIndex: 0 }; + } else { + redCount++; + return { team: CharacterTeam.red, colorIndex: 1 }; + } +}; + +export const freeTeam = (team: CharacterTeam) => { + if (team === CharacterTeam.decla) { + declaCount--; + } else { + redCount--; + } +}; diff --git a/backend/src/players/player.ts b/backend/src/players/player.ts index f245cce..efafb49 100644 --- a/backend/src/players/player.ts +++ b/backend/src/players/player.ts @@ -12,30 +12,32 @@ import { SetAspectRatioActionCommand, calculateViewArea, SecondaryActionCommand, + PlayerDiedCommand, settings, Circle, PlayerInformation, + CharacterTeam, + UpdatePlanetOwnershipCommand, + GameObject, + Command, + UpdateObjectMessage, } from 'shared'; import { getTimeInMilliseconds } from '../helper/get-time-in-milliseconds'; -import { ProjectilePhysical } from '../objects/projectile-physical'; import { BoundingBox } from '../physics/bounding-boxes/bounding-box'; import { PhysicalContainer } from '../physics/containers/physical-container'; import { getBoundingBoxOfCircle } from '../physics/functions/get-bounding-box-of-circle'; import { isCircleIntersecting } from '../physics/functions/is-circle-intersecting'; -import { requestColor, freeColor } from './player-color-service'; import { PlayerCharacterPhysical } from '../objects/player-character-physical'; -import { DynamicPhysical } from '../physics/physicals/dynamic-physical'; import { Physical } from '../physics/physicals/physical'; +import { freeTeam, requestTeam } from './player-team-service'; +import { PlanetPhysical } from '../objects/planet-physical'; export class Player extends CommandReceiver { - private character: PlayerCharacterPhysical; + private character?: PlayerCharacterPhysical | null; private aspectRatio: number = 16 / 9; private isActive = true; - private timeSinceLastProjectile = 0; - - private objectsPreviouslyInViewArea: Array = []; - private objectsInViewArea: Array = []; + private objectsPreviouslyInViewArea: Array = []; private pingTime?: number; private _latency?: number; @@ -55,35 +57,20 @@ export class Player extends CommandReceiver { [SetAspectRatioActionCommand.type]: (v: SetAspectRatioActionCommand) => (this.aspectRatio = v.aspectRatio), [MoveActionCommand.type]: (c: MoveActionCommand) => - this.character.handleMovementAction(c), + this.character?.handleMovementAction(c), [SecondaryActionCommand.type]: (c: SecondaryActionCommand) => { - if ( - !this.character.isAlive || - this.timeSinceLastProjectile < settings.projectileCreationInterval - ) { - return; - } - - const start = vec2.clone(this.character.center); - const direction = vec2.subtract(vec2.create(), c.position, start); - vec2.normalize(direction, direction); - vec2.add( - start, - start, - vec2.scale(vec2.create(), direction, settings.projectileStartOffset), - ); - const velocity = vec2.scale(direction, direction, settings.projectileSpeed); - vec2.add(velocity, velocity, this.character.velocity); - const projectile = new ProjectilePhysical(start, 20, velocity, this.objects); - this.objects.addObject(projectile); - - this.timeSinceLastProjectile = 0; + this.character?.shootTowards(c.position); }, }; private findEmptyPositionForPlayer(): vec2 { - let rotation = 0; - let radius = 0; + let possibleCenter = this.players.find((p) => p.team === this.team)?.center; + if (!possibleCenter) { + possibleCenter = vec2.create(); + } + + let rotation = Math.atan2(possibleCenter.y, possibleCenter.x); + let radius = vec2.length(possibleCenter); for (;;) { const playerPosition = vec2.fromValues( radius * Math.cos(rotation), @@ -106,27 +93,21 @@ export class Player extends CommandReceiver { } } + public readonly team: CharacterTeam; + private colorIndex: number; + constructor( - playerInfo: PlayerInformation, + private readonly playerInfo: PlayerInformation, + private readonly players: Array, private readonly objects: PhysicalContainer, private readonly socket: SocketIO.Socket, ) { super(); - const colorIndex = requestColor(); + const { team, colorIndex } = requestTeam(); + this.team = team; + this.colorIndex = colorIndex; - this.character = new PlayerCharacterPhysical( - playerInfo.name, - colorIndex, - objects, - this.findEmptyPositionForPlayer(), - ); - - this.objects.addObject(this.character); - - socket.emit( - TransportEvents.ServerToPlayer, - serialize(new CreatePlayerCommand(this.character)), - ); + this.createCharacter(); socket.on( TransportEvents.Pong, @@ -134,78 +115,99 @@ export class Player extends CommandReceiver { ); this.measureLatency(); - this.sendObjects(); + this.step(0); } + private createCharacter() { + this.character = new PlayerCharacterPhysical( + this.playerInfo.name.slice(0, 20), + this.colorIndex, + this.team, + this.objects, + this.findEmptyPositionForPlayer(), + ); + + this.objects.addObject(this.character); + this.objectsPreviouslyInViewArea.push(this.character); + + this.socket.emit( + TransportEvents.ServerToPlayer, + serialize(new CreatePlayerCommand(this.character)), + ); + } + + private center: vec2 = vec2.create(); + private timeUntilRespawn = 0; public step(deltaTime: number) { - this.sendObjects(); - this.timeSinceLastProjectile += deltaTime; - } + if (this.character) { + this.center = this.character?.center; - public sendObjects() { - const viewArea = calculateViewArea(this.character.center, this.aspectRatio, 1.5); + if (!this.character.isAlive) { + this.socket.emit( + TransportEvents.ServerToPlayer, + serialize(new PlayerDiedCommand(settings.playerDiedTimeout)), + ); + this.character = null; + this.timeUntilRespawn = settings.playerDiedTimeout; + } + } else if ((this.timeUntilRespawn -= deltaTime) < 0) { + this.createCharacter(); + } + + const viewArea = calculateViewArea(this.center, this.aspectRatio, 1.5); const bb = new BoundingBox(); bb.topLeft = viewArea.topLeft; bb.size = viewArea.size; - this.objectsInViewArea = this.objects.findIntersecting(bb); + const objectsInViewArea = Array.from( + new Set(this.objects.findIntersecting(bb).map((o) => o.gameObject)), + ); - const newlyIntersecting = this.objectsInViewArea.filter( + const newlyIntersecting = objectsInViewArea.filter( (o) => !this.objectsPreviouslyInViewArea.includes(o), ); const noLongerIntersecting = this.objectsPreviouslyInViewArea.filter( - (o) => !this.objectsInViewArea.includes(o), + (o) => !objectsInViewArea.includes(o), ); - this.objectsPreviouslyInViewArea = this.objectsInViewArea; + this.objectsPreviouslyInViewArea = objectsInViewArea; if (noLongerIntersecting.length > 0) { - this.socket.emit( - TransportEvents.ServerToPlayer, - serialize( - new DeleteObjectsCommand([ - ...new Set( - noLongerIntersecting - .filter((p) => p.gameObject !== this.character) - .map((p) => p.gameObject.id), - ), - ]), - ), - ); + this.sendToPlayer(new DeleteObjectsCommand(noLongerIntersecting.map((g) => g.id))); } if (newlyIntersecting.length > 0) { - this.socket.emit( - TransportEvents.ServerToPlayer, - serialize( - new CreateObjectsCommand([ - ...new Set( - newlyIntersecting - .map((p) => p.gameObject) - .filter((g) => g !== this.character), - ), - ]), - ), - ); + this.sendToPlayer(new CreateObjectsCommand(newlyIntersecting)); } - this.socket.emit( - TransportEvents.ServerToPlayer, - serialize( - new UpdateObjectsCommand( - Array.from(new Set(this.objectsInViewArea)) - .filter((p) => p.canMove) - .map((p) => (p as DynamicPhysical).calculateUpdates()) - .filter((p) => p !== null) as any, - ), + this.sendToPlayer( + new UpdateObjectsCommand( + this.objectsPreviouslyInViewArea + .map((g) => g.calculateUpdates()) + .filter((u) => u) as Array, + ), + ); + + this.sendToPlayer( + new UpdatePlanetOwnershipCommand( + PlanetPhysical.declaPlanetCount, + PlanetPhysical.redPlanetCount, + PlanetPhysical.neutralPlanetCount, ), ); } + private sendToPlayer(command: Command) { + this.socket.emit(TransportEvents.ServerToPlayer, serialize(command)); + } + public destroy() { this.isActive = false; - freeColor(this.character.colorIndex); - this.character.destroy(); + freeTeam(this.team); + + if (this.character) { + this.character.destroy(); + } } } diff --git a/frontend/package.json b/frontend/package.json index a951f71..54c1224 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -45,7 +45,7 @@ "ts-loader": "^8.0.3", "sass": "^1.26.3", "sass-loader": "^9.0.2", - "sdf-2d": "^0.5.3", + "sdf-2d": "^0.6.0", "shared": "0.0.0", "socket.io-client": "^2.3.1", "source-map-loader": "^1.1.0", diff --git a/frontend/src/index.html b/frontend/src/index.html index 36fdd8c..db4f744 100644 --- a/frontend/src/index.html +++ b/frontend/src/index.html @@ -4,7 +4,8 @@ @@ -15,10 +16,12 @@ + +
-
+

decla.red

@@ -41,8 +44,28 @@
-
+ - + open-settings + +
+

Settings

+ + + +
+ + minimize-application + maximize-application diff --git a/frontend/src/index.ts b/frontend/src/index.ts index 945d38a..6961c9b 100644 --- a/frontend/src/index.ts +++ b/frontend/src/index.ts @@ -14,6 +14,7 @@ import { PlanetView } from './scripts/objects/planet-view'; import './styles/main.scss'; import { LandingPageBackground } from './scripts/landing-page-background'; import { JoinFormHandler } from './scripts/join-form-handler'; +import { handleFullScreen } from './scripts/handle-full-screen'; import { Game } from './scripts/game'; import { PlayerCharacterView } from './scripts/objects/player-character-view'; @@ -32,15 +33,28 @@ const main = async () => { const serverContainer = document.querySelector('#server-container') as HTMLElement; const canvas = document.querySelector('canvas') as HTMLCanvasElement; const overlay = document.querySelector('#overlay') as HTMLElement; + const settings = document.querySelector('#settings') as HTMLElement; + const openSettings = document.querySelector('#open-settings') as HTMLElement; + const closeSettings = document.querySelector('#close-settings') as HTMLElement; + const minimize = document.querySelector('#minimize') as HTMLElement; + const maximize = document.querySelector('#maximize') as HTMLElement; + const enableRelativeMovementCheckbox = document.querySelector( + '#enable-relative-movement', + ) as HTMLElement; + + openSettings.addEventListener('click', () => (settings.style.visibility = 'visible')); + closeSettings.addEventListener('click', () => (settings.style.visibility = 'hidden')); const background = new LandingPageBackground(canvas); const joinHandler = new JoinFormHandler(joinGameForm, serverContainer); + handleFullScreen(minimize, maximize); const playerDecision = await joinHandler.getPlayerDecision(); landingUI.style.display = 'none'; background.destroy(); - await new Game(playerDecision, canvas, overlay).start(); + + new Game(playerDecision, canvas, overlay); } catch (e) { console.error(e); alert(e); diff --git a/frontend/src/scripts/game.ts b/frontend/src/scripts/game.ts index a6b7b52..00ae80c 100644 --- a/frontend/src/scripts/game.ts +++ b/frontend/src/scripts/game.ts @@ -1,11 +1,13 @@ import { vec2 } from 'gl-matrix'; import { CircleLight, + ColorfulCircle, compile, FilteringOptions, Flashlight, Renderer, renderNoise, + runAnimation, WrapOptions, } from 'sdf-2d'; import { @@ -17,36 +19,41 @@ import { SetAspectRatioActionCommand, rgb, PlayerInformation, + PlayerDiedCommand, + UpdatePlanetOwnershipCommand, } from 'shared'; import io from 'socket.io-client'; import { KeyboardListener } from './commands/generators/keyboard-listener'; import { MouseListener } from './commands/generators/mouse-listener'; import { TouchListener } from './commands/generators/touch-listener'; import { CommandReceiverSocket } from './commands/receivers/command-receiver-socket'; - -import { DeltaTimeCalculator } from './helper/delta-time-calculator'; import { PlayerDecision } from './join-form-handler'; import { GameObjectContainer } from './objects/game-object-container'; import { BlobShape } from './shapes/blob-shape'; -import { Circle } from './shapes/circle'; -import { Polygon } from './shapes/polygon'; +import { PlanetShape } from './shapes/planet-shape'; export class Game { public readonly gameObjects = new GameObjectContainer(this); - private renderer!: Renderer; + private renderer?: Renderer; private socket!: SocketIOClient.Socket; - private promises: Promise<[void, void]>; - private deltaTimeCalculator = new DeltaTimeCalculator(); + private deadTimeout = 0; + + private declaPlanetCountElement = document.createElement('div'); + private redPlanetCountElement = document.createElement('div'); + private neutralPlanetCountElement = document.createElement('div'); constructor( private readonly playerDecision: PlayerDecision, private readonly canvas: HTMLCanvasElement, private readonly overlay: HTMLElement, ) { - this.promises = Promise.all([ - this.setupCommunication(playerDecision.server), - this.setupRenderer(), - ]); + this.start(); + const progressBar = document.createElement('div'); + progressBar.className = 'planet-progress'; + overlay.appendChild(progressBar); + progressBar.appendChild(this.declaPlanetCountElement); + progressBar.appendChild(this.neutralPlanetCountElement); + progressBar.appendChild(this.redPlanetCountElement); } private async setupCommunication(serverUrl: string): Promise { @@ -61,7 +68,26 @@ export class Game { this.socket.on(TransportEvents.ServerToPlayer, (serialized: string) => { const command = deserialize(serialized); - this.gameObjects.sendCommand(command); + if (command instanceof PlayerDiedCommand) { + this.deadTimeout = command.timeout; + this.overlay.appendChild(this.announcmentText); + } else if (command instanceof UpdatePlanetOwnershipCommand) { + const all = command.declaCount + command.redCount + command.neutralCount; + this.declaPlanetCountElement.style.width = (command.declaCount / all) * 100 + '%'; + this.neutralPlanetCountElement.style.width = + (command.neutralCount / all) * 100 + '%'; + this.redPlanetCountElement.style.width = (command.redCount / all) * 100 + '%'; + + if (command.declaCount > all * 0.5) { + this.overlay.appendChild(this.announcmentText); + this.announcmentText.innerText = 'Decla team won 🎉'; + } + + if (command.redCount > all * 0.5) { + this.overlay.appendChild(this.announcmentText); + this.announcmentText.innerText = 'Red team won 🎉'; + } + } else this.gameObjects.sendCommand(command); }); this.socket.on(TransportEvents.Ping, () => { @@ -82,14 +108,14 @@ export class Game { ); } - private async setupRenderer(): Promise { + private async start(): Promise { const noiseTexture = await renderNoise([256, 256], 2, 1); - - this.renderer = await compile( + this.setupCommunication(this.playerDecision.server); + runAnimation( this.canvas, [ { - ...Polygon.descriptor, + ...PlanetShape.descriptor, shaderCombinationSteps: [0, 1, 2, 3], }, { @@ -97,55 +123,49 @@ export class Game { shaderCombinationSteps: [0, 1, 2, 8], }, { - ...Circle.descriptor, + ...ColorfulCircle.descriptor, shaderCombinationSteps: [0, 2, 16], }, { ...CircleLight.descriptor, - shaderCombinationSteps: [0, 1, 2, 4, 8], + shaderCombinationSteps: [0, 1, 2, 4, 8, 16], }, { ...Flashlight.descriptor, shaderCombinationSteps: [0], }, ], + this.gameLoop.bind(this), { shadowTraceCount: 16, - paletteSize: 10, + paletteSize: settings.palette.length, //enableStopwatch: true, }, - ); - - this.renderer.setRuntimeSettings({ - ambientLight: rgb(0.45, 0.4, 0.45), - colorPalette: [ - rgb(1, 1, 1), - rgb(0.4, 0.4, 0.4), - rgb(1, 1, 1), - ...settings.playerColors, - ], - enableHighDpiRendering: true, - lightCutoffDistance: settings.lightCutoffDistance, - textures: { - noiseTexture: { - source: noiseTexture, - overrides: { - maxFilter: FilteringOptions.LINEAR, - wrapS: WrapOptions.MIRRORED_REPEAT, - wrapT: WrapOptions.MIRRORED_REPEAT, + { + ambientLight: rgb(0.45, 0.4, 0.45), + colorPalette: settings.palette, + enableHighDpiRendering: true, + lightCutoffDistance: settings.lightCutoffDistance, + textures: { + noiseTexture: { + source: noiseTexture, + overrides: { + maxFilter: FilteringOptions.LINEAR, + wrapS: WrapOptions.MIRRORED_REPEAT, + wrapT: WrapOptions.MIRRORED_REPEAT, + }, }, }, }, - }); - } - - public async start(): Promise { - await this.promises; - requestAnimationFrame(this.gameLoop.bind(this)); + ); } public displayToWorldCoordinates(p: vec2): vec2 { - return this.renderer?.displayToWorldCoordinates(p); + const result = this.renderer?.displayToWorldCoordinates(p); + if (!result) { + return vec2.create(); + } + return result; } public aspectRatioChanged(aspectRatio: number) { @@ -155,14 +175,23 @@ export class Game { ); } - private gameLoop(time: DOMHighResTimeStamp) { - const deltaTime = this.deltaTimeCalculator.getNextDeltaTimeInMilliseconds(time); + private announcmentText = document.createElement('h2'); + private gameLoop( + renderer: Renderer, + currentTime: DOMHighResTimeStamp, + deltaTime: DOMHighResTimeStamp, + ): boolean { + this.renderer = renderer; + + if ((this.deadTimeout -= deltaTime / 1000) > 0) { + this.announcmentText.innerText = `Respawning in ${Math.floor(this.deadTimeout)} …`; + } else { + this.announcmentText.parentElement?.removeChild(this.announcmentText); + } this.gameObjects.stepObjects(deltaTime); - this.gameObjects.drawObjects(this.renderer); - this.renderer.renderDrawables(); + this.gameObjects.drawObjects(this.renderer, this.overlay); - // this.overlay.innerText = prettyPrint(this.renderer.insights); - requestAnimationFrame(this.gameLoop.bind(this)); + return true; } } diff --git a/frontend/src/scripts/handle-full-screen.ts b/frontend/src/scripts/handle-full-screen.ts new file mode 100644 index 0000000..66a921c --- /dev/null +++ b/frontend/src/scripts/handle-full-screen.ts @@ -0,0 +1,27 @@ +export const handleFullScreen = ( + minimizeButton: HTMLElement, + maximizeButton: HTMLElement, +) => { + if (!document.fullscreenEnabled) { + minimizeButton.style.visibility = 'hidden'; + maximizeButton.style.visibility = 'hidden'; + return; + } + + let isInFullScreen = document.fullscreenElement !== null; + + const showButtons = () => { + minimizeButton.style.visibility = isInFullScreen ? 'visible' : 'hidden'; + maximizeButton.style.visibility = isInFullScreen ? 'hidden' : 'visible'; + }; + + showButtons(); + + maximizeButton.addEventListener('click', () => document.body.requestFullscreen()); + minimizeButton.addEventListener('click', () => document.exitFullscreen()); + + document.addEventListener('fullscreenchange', () => { + isInFullScreen = !isInFullScreen; + showButtons(); + }); +}; diff --git a/frontend/src/scripts/helper/delta-time-calculator.ts b/frontend/src/scripts/helper/delta-time-calculator.ts deleted file mode 100644 index 7f93741..0000000 --- a/frontend/src/scripts/helper/delta-time-calculator.ts +++ /dev/null @@ -1,25 +0,0 @@ -export class DeltaTimeCalculator { - private previousTime: DOMHighResTimeStamp | null = null; - - constructor() { - document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this)); - } - - public getNextDeltaTimeInMilliseconds( - currentTime: DOMHighResTimeStamp, - ): DOMHighResTimeStamp { - if (this.previousTime === null) { - this.previousTime = currentTime; - } - - const delta = currentTime - this.previousTime; - this.previousTime = currentTime; - return delta; - } - - private handleVisibilityChange() { - if (!document.hidden) { - this.previousTime = null; - } - } -} diff --git a/frontend/src/scripts/landing-page-background.ts b/frontend/src/scripts/landing-page-background.ts index fda6efc..ef9b3c2 100644 --- a/frontend/src/scripts/landing-page-background.ts +++ b/frontend/src/scripts/landing-page-background.ts @@ -7,6 +7,7 @@ import { NoisyPolygonFactory, Renderer, renderNoise, + runAnimation, WrapOptions, } from 'sdf-2d'; import { settings, rgb, PlanetBase, Random } from 'shared'; @@ -18,17 +19,17 @@ const LangindPagePolygon = NoisyPolygonFactory( ); export class LandingPageBackground { - private renderer!: Renderer; + private isActive = true; - constructor(private readonly canvas: HTMLCanvasElement) { - this.start(); + constructor(canvas: HTMLCanvasElement) { + this.start(canvas); } - private async start(): Promise { + private async start(canvas: HTMLCanvasElement): Promise { const noiseTexture = await renderNoise([256, 256], 1.2, 2); - this.renderer = await compile( - this.canvas, + runAnimation( + canvas, [ { ...LangindPagePolygon.descriptor, @@ -39,45 +40,36 @@ export class LandingPageBackground { shaderCombinationSteps: [0, 2], }, ], + this.gameLoop.bind(this), { shadowTraceCount: 16, paletteSize: 1, }, - ); - - this.renderer.setRuntimeSettings({ - ambientLight: rgb(0, 0, 0), - lightCutoffDistance: settings.lightCutoffDistance, - textures: { - noiseTexture: { - source: noiseTexture, - overrides: { - maxFilter: FilteringOptions.LINEAR, - wrapS: WrapOptions.MIRRORED_REPEAT, - wrapT: WrapOptions.MIRRORED_REPEAT, + { + ambientLight: rgb(0, 0, 0), + lightCutoffDistance: settings.lightCutoffDistance, + textures: { + noiseTexture: { + source: noiseTexture, + overrides: { + maxFilter: FilteringOptions.LINEAR, + wrapS: WrapOptions.MIRRORED_REPEAT, + wrapT: WrapOptions.MIRRORED_REPEAT, + }, }, }, }, - }); - - requestAnimationFrame(this.gameLoop.bind(this)); + ); } - public destroy() { - this.renderer.destroy(); - } - - private gameLoop(time: DOMHighResTimeStamp) { + private gameLoop(renderer: Renderer, time: DOMHighResTimeStamp, _: number): boolean { Random.seed = 42; - this.renderer.setViewArea( - vec2.fromValues(0, this.renderer.canvasSize.y), - this.renderer.canvasSize, - ); + renderer.setViewArea(vec2.fromValues(0, renderer.canvasSize.y), renderer.canvasSize); const topPlanetPosition = vec2.fromValues( - 0.7 * this.renderer.canvasSize.x, - 0.7 * this.renderer.canvasSize.y, + 0.7 * renderer.canvasSize.x, + 0.7 * renderer.canvasSize.y, ); const topPlanet = new LangindPagePolygon( @@ -93,8 +85,8 @@ export class LandingPageBackground { (topPlanet as any).randomOffset = 0.5 + time / 3500; const bottomPlanetPosition = vec2.fromValues( - 0.3 * this.renderer.canvasSize.x, - 0.3 * this.renderer.canvasSize.y, + 0.3 * renderer.canvasSize.x, + 0.3 * renderer.canvasSize.y, ); const bottomPlanet = new LangindPagePolygon( @@ -118,11 +110,12 @@ export class LandingPageBackground { const planetDirection = vec2.normalize(planetDistance, planetDistance); const planetAngle = Math.atan2(planetDirection.y, planetDirection.x); - this.renderer.addDrawable(topPlanet); - this.renderer.addDrawable(bottomPlanet); - this.renderer.addDrawable( + renderer.addDrawable(topPlanet); + renderer.addDrawable(bottomPlanet); + renderer.addDrawable( new CircleLight( this.calculateLightPosition( + renderer, planetAngle, planetDistanceLength * 1.2, -time / 3000, @@ -132,9 +125,10 @@ export class LandingPageBackground { ), ); - this.renderer.addDrawable( + renderer.addDrawable( new CircleLight( this.calculateLightPosition( + renderer, planetAngle, planetDistanceLength * 1.2, time / 2000 + Math.PI, @@ -144,21 +138,28 @@ export class LandingPageBackground { ), ); - this.renderer.renderDrawables(); - - requestAnimationFrame(this.gameLoop.bind(this)); + return this.isActive; } - private calculateLightPosition(angle: number, length: number, t: number): vec2 { + private calculateLightPosition( + renderer: Renderer, + angle: number, + length: number, + t: number, + ): vec2 { const lightPosition = vec2.fromValues( length * Math.sin(t), length * Math.sin(t) * Math.cos(t), ); - const canvasCenter = vec2.scale(vec2.create(), this.renderer.canvasSize, 0.5); + const canvasCenter = vec2.scale(vec2.create(), renderer.canvasSize, 0.5); vec2.add(lightPosition, lightPosition, canvasCenter); vec2.rotate(lightPosition, lightPosition, canvasCenter, angle); return lightPosition; } + + public destroy() { + this.isActive = false; + } } diff --git a/frontend/src/scripts/objects/camera.ts b/frontend/src/scripts/objects/camera.ts index 1116fc8..2e28612 100644 --- a/frontend/src/scripts/objects/camera.ts +++ b/frontend/src/scripts/objects/camera.ts @@ -14,13 +14,11 @@ export class Camera extends GameObject implements ViewObject { super(null); } - public update(updates: Array) { - throw new Error(); - } + public beforeDestroy(): void {} public step(deltaTimeInMilliseconds: number): void {} - public draw(renderer: Renderer) { + public draw(renderer: Renderer, overlay: HTMLElement) { const canvasAspectRatio = renderer.canvasSize.x / renderer.canvasSize.y; if (canvasAspectRatio !== this.aspectRatio) { this.aspectRatio = canvasAspectRatio; diff --git a/frontend/src/scripts/objects/character-view.ts b/frontend/src/scripts/objects/character-view.ts index 6f617c7..3e88c5c 100644 --- a/frontend/src/scripts/objects/character-view.ts +++ b/frontend/src/scripts/objects/character-view.ts @@ -1,6 +1,6 @@ import { vec2 } from 'gl-matrix'; import { Renderer } from 'sdf-2d'; -import { CharacterBase, Circle, Id, UpdateMessage } from 'shared'; +import { CharacterBase, CharacterTeam, Circle, Id, UpdateMessage } from 'shared'; import { BlobShape } from '../shapes/blob-shape'; import { ViewObject } from './view-object'; @@ -11,25 +11,25 @@ export class CharacterView extends CharacterBase implements ViewObject { constructor( id: Id, colorIndex: number, + team: CharacterTeam, + health: number, head?: Circle, leftFoot?: Circle, rightFoot?: Circle, ) { - super(id, colorIndex, head, leftFoot, rightFoot); + super(id, colorIndex, team, health, head, leftFoot, rightFoot); this.shape = new BlobShape(colorIndex); } - public update(updates: Array) { - updates.forEach((u) => ((this as any)[u.key] = u.value)); - } - public get position(): vec2 { return this.head!.center; } + public beforeDestroy(): void {} + public step(deltaTimeInMilliseconds: number): void {} - public draw(renderer: Renderer): void { + public draw(renderer: Renderer, overlay: HTMLElement): void { this.shape.setCircles([this.head!, this.leftFoot!, this.rightFoot!]); renderer.addDrawable(this.shape); } diff --git a/frontend/src/scripts/objects/game-object-container.ts b/frontend/src/scripts/objects/game-object-container.ts index 2a03745..adf92b5 100644 --- a/frontend/src/scripts/objects/game-object-container.ts +++ b/frontend/src/scripts/objects/game-object-container.ts @@ -22,6 +22,7 @@ export class GameObjectContainer extends CommandReceiver { protected commandExecutors: CommandExecutors = { [CreatePlayerCommand.type]: (c: CreatePlayerCommand) => { this.player = c.character as PlayerCharacterView; + console.log(c.character); this.camera = new Camera(this.game); this.addObject(this.player); this.addObject(this.camera); @@ -31,7 +32,7 @@ export class GameObjectContainer extends CommandReceiver { c.objects.forEach((o) => this.addObject(o as ViewObject)), [DeleteObjectsCommand.type]: (c: DeleteObjectsCommand) => - c.ids.forEach((id: Id) => this.objects.delete(id)), + c.ids.forEach((id: Id) => this.deleteObject(id)), [UpdateObjectsCommand.type]: (c: UpdateObjectsCommand) => { c.updates.forEach((u) => this.objects.get(u.id)?.update(u.updates)); @@ -50,11 +51,17 @@ export class GameObjectContainer extends CommandReceiver { this.objects.forEach((o) => o.step(delta)); } - public drawObjects(renderer: Renderer) { - this.objects.forEach((o) => o.draw(renderer)); + public drawObjects(renderer: Renderer, overlay: HTMLElement) { + this.objects.forEach((o) => o.draw(renderer, overlay)); } private addObject(object: ViewObject) { this.objects.set(object.id, object); } + + private deleteObject(id: Id) { + const object = this.objects.get(id); + object?.beforeDestroy(); + this.objects.delete(id); + } } diff --git a/frontend/src/scripts/objects/lamp-view.ts b/frontend/src/scripts/objects/lamp-view.ts index c4590a7..2da4521 100644 --- a/frontend/src/scripts/objects/lamp-view.ts +++ b/frontend/src/scripts/objects/lamp-view.ts @@ -16,13 +16,11 @@ export class LampView extends LampBase implements ViewObject { this.light = new CircleLight(center, color, lightness); } - public update(message: Array): void { - throw new Error('Method not implemented.'); - } - public step(deltaTimeInMilliseconds: number): void {} - public draw(renderer: Renderer): void { + public beforeDestroy(): void {} + + public draw(renderer: Renderer, overlay: HTMLElement): void { renderer.addDrawable(this.light); } } diff --git a/frontend/src/scripts/objects/planet-view.ts b/frontend/src/scripts/objects/planet-view.ts index 4f45f88..316fc9f 100644 --- a/frontend/src/scripts/objects/planet-view.ts +++ b/frontend/src/scripts/objects/planet-view.ts @@ -1,32 +1,62 @@ import { vec2 } from 'gl-matrix'; import { Drawable, Renderer } from 'sdf-2d'; -import { CommandExecutors, Id, Random, PlanetBase, UpdateMessage } from 'shared'; +import { + CommandExecutors, + Id, + Random, + PlanetBase, + UpdateMessage, + settings, +} from 'shared'; import { RenderCommand } from '../commands/types/render'; -import { Polygon } from '../shapes/polygon'; +import { PlanetShape } from '../shapes/planet-shape'; import { ViewObject } from './view-object'; export class PlanetView extends PlanetBase implements ViewObject { - private shape: Drawable; + private shape: PlanetShape; + private ownershipProgess: HTMLElement; protected commandExecutors: CommandExecutors = { [RenderCommand.type]: (c: RenderCommand) => c.renderer.addDrawable(this.shape), }; - constructor(id: Id, vertices: Array) { + constructor(id: Id, vertices: Array, ownership: number) { super(id, vertices); - this.shape = new Polygon(vertices); + this.shape = new PlanetShape(vertices, ownership); (this.shape as any).randomOffset = Random.getRandom(); - } - public update(message: Array): void { - throw new Error('Method not implemented.'); + this.ownershipProgess = document.createElement('div'); + this.ownershipProgess.className = 'ownership'; } public step(deltaTimeInMilliseconds: number): void { - (this.shape as any).randomOffset += deltaTimeInMilliseconds / 4000; + this.shape.randomOffset += deltaTimeInMilliseconds / 4000; + this.shape.colorMixQ = this.ownership; + let teamName = 'Neutral'; + if (this.ownership < 0.5 - settings.planetControlThreshold) { + teamName = 'Decla'; + } else if (this.ownership > 0.5 + settings.planetControlThreshold) { + teamName = 'Red'; + } + + this.ownershipProgess.innerText = `${teamName} ${Math.round( + (Math.abs(this.ownership - 0.5) / 0.5) * 100, + )}%`; } - public draw(renderer: Renderer): void { + public beforeDestroy(): void { + this.ownershipProgess.parentElement?.removeChild(this.ownershipProgess); + } + + public draw(renderer: Renderer, overlay: HTMLElement): void { + if (!this.ownershipProgess.parentElement) { + overlay.appendChild(this.ownershipProgess); + } + + const screenPosition = renderer.worldToDisplayCoordinates(this.center); + this.ownershipProgess.style.left = screenPosition.x + 'px'; + this.ownershipProgess.style.top = screenPosition.y + 'px'; + renderer.addDrawable(this.shape); } } diff --git a/frontend/src/scripts/objects/player-character-view.ts b/frontend/src/scripts/objects/player-character-view.ts index 8d94cc9..26f9b42 100644 --- a/frontend/src/scripts/objects/player-character-view.ts +++ b/frontend/src/scripts/objects/player-character-view.ts @@ -1,37 +1,84 @@ import { vec2 } from 'gl-matrix'; import { Renderer } from 'sdf-2d'; -import { Circle, Id, PlayerCharacterBase, UpdateMessage } from 'shared'; +import { Circle, Id, PlayerCharacterBase, UpdateMessage, CharacterTeam } from 'shared'; import { BlobShape } from '../shapes/blob-shape'; import { ViewObject } from './view-object'; export class PlayerCharacterView extends PlayerCharacterBase implements ViewObject { private shape: BlobShape; + private nameElement: HTMLElement; + private healthElement: HTMLElement; + private timeSinceLastNameElementUpdate = 0; constructor( id: Id, name: string, colorIndex: number, + team: CharacterTeam, + health: number, head?: Circle, leftFoot?: Circle, rightFoot?: Circle, ) { - super(id, name, colorIndex, head, leftFoot, rightFoot); + super(id, name, colorIndex, team, health, head, leftFoot, rightFoot); this.shape = new BlobShape(colorIndex); - } - public update(updates: Array) { - updates.forEach((u) => ((this as any)[u.key] = u.value)); + console.log(this.id, 'created'); + + this.nameElement = document.createElement('div'); + this.nameElement.className = 'player-tag'; + this.nameElement.innerText = this.name; + this.healthElement = document.createElement('div'); + this.nameElement.appendChild(this.healthElement); } public get position(): vec2 { return this.head!.center; } - public step(deltaTimeInMilliseconds: number): void {} + public step(deltaTimeInMilliseconds: number): void { + this.timeSinceLastNameElementUpdate += deltaTimeInMilliseconds; + this.healthElement.style.width = this.health + '%'; + } + + public beforeDestroy(): void { + console.log(this.id, 'destroyes'); + this.nameElement.parentElement?.removeChild(this.nameElement); + } + + private elementAdded = false; + public draw(renderer: Renderer, overlay: HTMLElement): void { + if (!this.elementAdded) { + this.elementAdded = true; + console.log(this.id, 'add', this.nameElement, this.nameElement.parentElement); + overlay.appendChild(this.nameElement); + } + + if (this.timeSinceLastNameElementUpdate > 0.15) { + const screenPosition = renderer.worldToDisplayCoordinates( + this.calculateTextPosition(), + ); + this.nameElement.style.left = screenPosition.x + 'px'; + this.nameElement.style.top = screenPosition.y + 'px'; + this.timeSinceLastNameElementUpdate = 0; + } - public draw(renderer: Renderer): void { this.shape.setCircles([this.head!, this.leftFoot!, this.rightFoot!]); renderer.addDrawable(this.shape); } + + private calculateTextPosition(): vec2 { + const footAverage = vec2.add( + vec2.create(), + this.leftFoot!.center, + this.rightFoot!.center, + ); + vec2.scale(footAverage, footAverage, 0.5); + + const headFeetDelta = vec2.subtract(footAverage, this.head!.center, footAverage); + vec2.normalize(headFeetDelta, headFeetDelta); + const textOffset = vec2.scale(headFeetDelta, headFeetDelta, this.head!.radius + 50); + return vec2.add(textOffset, this.head!.center, textOffset); + } } diff --git a/frontend/src/scripts/objects/projectile-view.ts b/frontend/src/scripts/objects/projectile-view.ts index 86c2047..9717e36 100644 --- a/frontend/src/scripts/objects/projectile-view.ts +++ b/frontend/src/scripts/objects/projectile-view.ts @@ -1,29 +1,33 @@ import { vec2 } from 'gl-matrix'; -import { CircleLight, Renderer } from 'sdf-2d'; -import { Id, ProjectileBase, rgb, UpdateMessage } from 'shared'; +import { CircleLight, ColorfulCircle, Renderer } from 'sdf-2d'; +import { Id, ProjectileBase, settings, UpdateMessage } from 'shared'; import { ViewObject } from './view-object'; -import { Circle } from '../shapes/circle'; export class ProjectileView extends ProjectileBase implements ViewObject { - private circle: InstanceType; + private circle: ColorfulCircle; private light: CircleLight; - constructor(id: Id, center: vec2, radius: number) { - super(id, center, radius); - this.circle = new Circle(center, radius / 2); - this.light = new CircleLight(center, rgb(1, 0.5, 0), 0.15); - } - - update(updates: Array): void { - updates.forEach((u) => ((this as any)[u.key] = u.value)); + constructor( + id: Id, + center: vec2, + radius: number, + colorIndex: number, + strength: number, + ) { + super(id, center, radius, colorIndex, strength); + this.circle = new ColorfulCircle(center, radius / 2, colorIndex); + this.light = new CircleLight(center, settings.palette[colorIndex], 0); } public step(deltaTimeInMilliseconds: number): void { this.circle.center = this.center; this.light.center = this.center; + this.light.intensity = (0.15 * this.strength) / settings.projectileMaxStrength; } - public draw(renderer: Renderer): void { + public beforeDestroy(): void {} + + public draw(renderer: Renderer, overlay: HTMLElement): void { renderer.addDrawable(this.circle); renderer.addDrawable(this.light); } diff --git a/frontend/src/scripts/objects/view-object.ts b/frontend/src/scripts/objects/view-object.ts index a395011..18c8a37 100644 --- a/frontend/src/scripts/objects/view-object.ts +++ b/frontend/src/scripts/objects/view-object.ts @@ -2,7 +2,7 @@ import { Renderer } from 'sdf-2d'; import { GameObject, UpdateMessage } from 'shared'; export interface ViewObject extends GameObject { - update(updates: Array): void; step(deltaTimeInMilliseconds: number): void; - draw(renderer: Renderer): void; + draw(renderer: Renderer, overlay: HTMLElement): void; + beforeDestroy(): void; } diff --git a/frontend/src/scripts/shapes/circle.ts b/frontend/src/scripts/shapes/circle.ts deleted file mode 100644 index e4f8374..0000000 --- a/frontend/src/scripts/shapes/circle.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { CircleFactory } from 'sdf-2d'; - -export const Circle = CircleFactory(2); diff --git a/frontend/src/scripts/shapes/planet-shape.ts b/frontend/src/scripts/shapes/planet-shape.ts new file mode 100644 index 0000000..e637e29 --- /dev/null +++ b/frontend/src/scripts/shapes/planet-shape.ts @@ -0,0 +1,163 @@ +import { mat2d, vec2, vec3, vec4 } from 'gl-matrix'; +import { PolygonFactory, DrawableDescriptor, Drawable } from 'sdf-2d'; +import { settings } from 'shared'; + +export const colorToString = (v: vec3 | vec4): string => + `vec4(${v[0]}, ${v[1]}, ${v[2]}, ${v.length > 3 ? v[3] : 1})`; + +export class PlanetShape extends PolygonFactory(settings.planetEdgeCount, 0) { + public static descriptor: DrawableDescriptor = { + sdf: { + shader: ` + uniform vec2 planetVertices[PLANET_COUNT * ${settings.planetEdgeCount}]; + uniform vec2 planetCenters[PLANET_COUNT]; + uniform float planetLengths[PLANET_COUNT]; + uniform float planetRandoms[PLANET_COUNT]; + uniform float planetColorMixQ[PLANET_COUNT]; + + uniform sampler2D noiseTexture; + + #ifdef WEBGL2_IS_AVAILABLE + float planetTerrain(vec2 h) { + return texture(noiseTexture, h)[0] - 0.5; + } + #else + float planetTerrain(vec2 h) { + return texture2D(noiseTexture, h)[0] - 0.5; + } + #endif + + vec2 planetLineDistance(vec2 target, vec2 from, vec2 to) { + vec2 targetFromDelta = target - from; + vec2 toFromDelta = to - from; + float h = clamp( + dot(targetFromDelta, toFromDelta) / dot(toFromDelta, toFromDelta), + 0.0, 1.0 + ); + + vec2 diff = targetFromDelta - toFromDelta * h; + return vec2( + dot(diff, diff), + toFromDelta.x * targetFromDelta.y - toFromDelta.y * targetFromDelta.x + ); + } + + float planetMinDistance(vec2 target, out vec4 color) { + float minDistance = 100.0; + + for (int j = 0; j < PLANET_COUNT; j++) { + vec2 startEnd = planetVertices[j * ${settings.planetEdgeCount}]; + vec2 vb = startEnd; + + vec2 center = planetCenters[j]; + float l = planetLengths[j]; + float randomOffset = planetRandoms[j]; + vec2 targetTangent = normalize(target - center); + vec2 noisyTarget = target - ( + targetTangent * planetTerrain(vec2( + l * abs(atan(targetTangent.y, targetTangent.x)), + randomOffset + )) / 12.0 + ); + + float d = 10000.0; + float s = 1.0; + for (int k = 1; k < ${settings.planetEdgeCount}; k++) { + vec2 va = vb; + vb = planetVertices[j * ${settings.planetEdgeCount} + k]; + vec2 ds = planetLineDistance(noisyTarget, va, vb); + + bvec3 cond = bvec3(noisyTarget.y >= va.y, noisyTarget.y < vb.y, ds.y > 0.0); + if (all(cond) || all(not(cond))) { + s *= -1.0; + } + + d = min(d, ds.x); + } + + vec2 ds = planetLineDistance(noisyTarget, vb, startEnd); + bvec3 cond = bvec3(noisyTarget.y >= vb.y, noisyTarget.y < startEnd.y, ds.y > 0.0); + if (all(cond) || all(not(cond))) { + s *= -1.0; + } + + d = min(d, ds.x); + float dist = s * sqrt(d); + + if (dist < minDistance) { + minDistance = dist; + color = mix(${colorToString(settings.declaPlanetColor)}, ${colorToString( + settings.redPlanetColor, + )}, planetColorMixQ[j]); + } + } + + return minDistance; + } + `, + distanceFunctionName: 'planetMinDistance', + }, + propertyUniformMapping: { + length: 'planetLengths', + random: 'planetRandoms', + center: 'planetCenters', + vertices: 'planetVertices', + colorMixQ: 'planetColorMixQ', + }, + uniformCountMacroName: `PLANET_COUNT`, + shaderCombinationSteps: [0, 1, 2, 3, 8, 16], + empty: new PlanetShape(new Array(settings.planetEdgeCount).fill(vec2.create()), 0), + }; + + public randomOffset = 0; + + constructor(public vertices: Array, public colorMixQ: number) { + super(vertices); + } + + protected getObjectToSerialize(transform2d: mat2d, transform1d: number): any { + const transformedVertices = (this as any).actualVertices.map((v: vec2) => + vec2.transformMat2d(vec2.create(), v, transform2d), + ); + + const center = transformedVertices.reduce( + (sum: vec2, v: vec2) => vec2.add(sum, sum, v), + vec2.create(), + ); + vec2.scale(center, center, 1 / transformedVertices.length); + + let length = 0; + for (let i = 1; i < this.vertices.length; i++) { + length += vec2.distance(transformedVertices[i - 1], transformedVertices[i]); + } + + return { + vertices: transformedVertices, + center, + length, + random: this.randomOffset, + colorMixQ: this.colorMixQ, + }; + } + + public serializeToUniforms( + uniforms: any, + transform2d: mat2d, + transform1d: number, + ): void { + const { propertyUniformMapping } = (this.constructor as typeof Drawable).descriptor; + + const serialized = this.getObjectToSerialize(transform2d, transform1d); + Object.entries(propertyUniformMapping).forEach(([k, v]) => { + if (!Object.prototype.hasOwnProperty.call(uniforms, v)) { + uniforms[v] = []; + } + + if (k === 'vertices') { + uniforms[v].push(...serialized[k]); + } else { + uniforms[v].push(serialized[k]); + } + }); + } +} diff --git a/frontend/src/scripts/shapes/polygon.ts b/frontend/src/scripts/shapes/polygon.ts deleted file mode 100644 index df62695..0000000 --- a/frontend/src/scripts/shapes/polygon.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { NoisyPolygonFactory } from 'sdf-2d'; -import { settings } from 'shared'; - -export const Polygon = NoisyPolygonFactory(settings.polygonEdgeCount, 1); diff --git a/frontend/src/styles/form.scss b/frontend/src/styles/form.scss index 3aa02d7..154d393 100644 --- a/frontend/src/styles/form.scss +++ b/frontend/src/styles/form.scss @@ -1,9 +1,18 @@ @import './vars.scss'; +@import './mixins.scss'; form { - backdrop-filter: blur(24px); - background-color: rgba(0, 0, 0, 0.15); + @include background; + padding: $small-padding / 2 $small-padding $small-padding $small-padding; + border-radius: $border-radius; + + input:-webkit-autofill, + input:-webkit-autofill:hover, + input:-webkit-autofill:focus { + -webkit-text-fill-color: white; + transition: background-color 5000s ease-in-out 0s; + } input, label { @@ -57,6 +66,7 @@ form { display: block; transition: $animation-time; color: $gray; + font-size: 1.1rem; } input:focus, @@ -78,6 +88,7 @@ form { input[type='radio'] { width: 0; height: 0; + appearance: none; + label { display: block; @@ -100,25 +111,23 @@ form { } } } - - button { - border: none; - background: none; - font-size: 2rem; - font-family: 'Comfortaa', sans-serif; - cursor: pointer; - display: block; - margin: auto; - padding-top: $medium-padding; - transition: transform $animation-time; - - &:hover, - &:focus { - outline: none; - color: $accent; - transform: translateY(-8px); - } - } - - border-radius: $border-radius; +} + +button { + border: none; + background: none; + font-size: 2rem; + font-family: 'Comfortaa', sans-serif; + cursor: pointer; + display: block; + margin: auto; + padding-top: $medium-padding; + transition: transform $animation-time; + + &:hover, + &:focus { + outline: none; + color: $accent; + transform: translateY(-8px); + } } diff --git a/frontend/src/styles/main.scss b/frontend/src/styles/main.scss index e0d97fd..d0a3f2b 100644 --- a/frontend/src/styles/main.scss +++ b/frontend/src/styles/main.scss @@ -1,5 +1,6 @@ @import './vars.scss'; @import './form.scss'; +@import './mixins.scss'; @import url('https://fonts.googleapis.com/css2?family=Comfortaa:wght@300&family=Open+Sans&display=swap'); * { @@ -21,7 +22,8 @@ html { } } -h1 { +h1, +h2 { &, * { font-family: 'Comfortaa', sans-serif; @@ -31,6 +33,10 @@ h1 { padding-bottom: $medium-padding; } +h2 { + font-size: 4rem; +} + .red { color: $red; &::selection { @@ -48,6 +54,8 @@ canvas { } body { + overflow: hidden; + #landing-ui { width: 100%; height: 100%; @@ -67,7 +75,104 @@ body { top: 0; pointer-events: none; + user-select: none; - font-size: 0.75rem; + overflow: hidden; + + h2 { + margin: $large-padding 0; + } + + .player-tag { + font-size: 1.3rem; + position: absolute; + transform: translateX(-50%) translateY(-50%) rotate(-15deg); + transition: left 200ms, top 200ms; + + div { + height: 3px; + background-color: rebeccapurple; + } + } + + .ownership { + font-size: 1.3rem; + position: absolute; + transform: translateX(-50%) translateY(-50%); + } + + .planet-progress { + position: absolute; + top: $small-padding; + left: 50%; + transform: translateX(-50%); + width: 50%; + display: flex; + $height: 8px; + height: $height; + + border-radius: 4px; + + div { + height: $height; + } + + div:nth-child(1) { + background: rebeccapurple; + } + + div:nth-child(2) { + background: gray; + } + + div:nth-child(3) { + background: red; + } + } + } + + #open-settings { + position: absolute; + top: 0; + right: 0; + + animation: spin 32s linear infinite; + @keyframes spin { + 100% { + transform: rotate(360deg); + } + } + } + + .icon { + box-sizing: content-box; + + user-select: none; + cursor: pointer; + + padding: $medium-padding; + @include square(48px); + @media (max-width: $breakpoint) { + @include square(32px); + padding: $small-padding; + } + } + + .full-screen-controllers { + position: absolute; + bottom: 0; + left: 0; + } + + #settings { + @include background; + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + + padding: $large-padding; + visibility: hidden; } } diff --git a/frontend/src/styles/mixins.scss b/frontend/src/styles/mixins.scss new file mode 100644 index 0000000..1afa563 --- /dev/null +++ b/frontend/src/styles/mixins.scss @@ -0,0 +1,11 @@ +@mixin square($size) { + width: $size; + height: $size; +} + +@mixin background { + backdrop-filter: blur(24px); + @supports not (backdrop-filter: blur(24px)) { + background-color: rgba(0, 0, 0, 0.15); + } +} diff --git a/frontend/static/declared.png b/frontend/static/declared.png deleted file mode 100644 index 897278d92a5e349f0a101b77ac6804fdeca52a22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 26027 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 diff --git a/frontend/static/declared.psd b/frontend/static/declared.psd deleted file mode 100644 index e2f814d..0000000 --- a/frontend/static/declared.psd +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:03ed783e268bc05d40997239f6118767c5d5d8b15d23fa7b329321c0a1529c46 -size 437314 diff --git a/frontend/static/maximize.svg b/frontend/static/maximize.svg new file mode 100644 index 0000000..7118488 --- /dev/null +++ b/frontend/static/maximize.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/frontend/static/minimize.svg b/frontend/static/minimize.svg new file mode 100644 index 0000000..93a212a --- /dev/null +++ b/frontend/static/minimize.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/frontend/static/settings.svg b/frontend/static/settings.svg new file mode 100644 index 0000000..8e99bbe --- /dev/null +++ b/frontend/static/settings.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index c883490..235fc22 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -1,9 +1,6 @@ { "compilerOptions": { "outDir": "dist", - "sourceMap": true, - "declaration": true, - "declarationMap": true, "target": "es6", "esModuleInterop": true, "strict": true, @@ -14,5 +11,5 @@ "composite": true, "lib": ["dom", "es2017"] }, - "include": ["src/**/*.ts"] + "include": ["src/**/*.ts", "src/*.ts"] } diff --git a/frontend/webpack.config.js b/frontend/webpack.config.js index f219f3c..ed9e7b2 100644 --- a/frontend/webpack.config.js +++ b/frontend/webpack.config.js @@ -80,6 +80,16 @@ module.exports = (env, argv) => ({ }, }, }, + { + test: /\.svg$/, + use: { + loader: 'file-loader', + query: { + outputPath: '/static', + name: '[name].[ext]', + }, + }, + }, { test: /\.scss$/, use: [ diff --git a/shared/src/commands/types/player-died.ts b/shared/src/commands/types/player-died.ts new file mode 100644 index 0000000..bf43e0d --- /dev/null +++ b/shared/src/commands/types/player-died.ts @@ -0,0 +1,14 @@ +import { vec2 } from 'gl-matrix'; +import { serializable } from '../../transport/serialization/serializable'; +import { Command } from '../command'; + +@serializable +export class PlayerDiedCommand extends Command { + public constructor(public readonly timeout: number) { + super(); + } + + public toArray(): Array { + return [this.timeout]; + } +} diff --git a/shared/src/commands/types/update-planet-ownership.ts b/shared/src/commands/types/update-planet-ownership.ts new file mode 100644 index 0000000..e2b3c1c --- /dev/null +++ b/shared/src/commands/types/update-planet-ownership.ts @@ -0,0 +1,17 @@ +import { serializable } from '../../transport/serialization/serializable'; +import { Command } from '../command'; + +@serializable +export class UpdatePlanetOwnershipCommand extends Command { + public constructor( + public readonly declaCount: number, + public readonly redCount: number, + public readonly neutralCount: number, + ) { + super(); + } + + public toArray(): Array { + return [this.declaCount, this.redCount, this.neutralCount]; + } +} diff --git a/shared/src/helper/hsl.ts b/shared/src/helper/hsl.ts new file mode 100644 index 0000000..6bece56 --- /dev/null +++ b/shared/src/helper/hsl.ts @@ -0,0 +1,34 @@ +import { vec3 } from 'gl-matrix'; +import { rgb } from './rgb'; + +export const hsl = (hue: number, saturation: number, lightness: number): vec3 => { + hue /= 360; + saturation /= 100; + lightness /= 100; + let r, g, b; + + if (saturation == 0) { + r = g = b = lightness; + } else { + const hue2rgb = (p: number, q: number, t: number) => { + if (t < 0) t += 1; + if (t > 1) t -= 1; + if (t < 1 / 6) return p + (q - p) * 6 * t; + if (t < 1 / 2) return q; + if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; + return p; + }; + + const q = + lightness < 0.5 + ? lightness * (1 + saturation) + : lightness + saturation - lightness * saturation; + const p = 2 * lightness - q; + + r = hue2rgb(p, q, hue + 1 / 3); + g = hue2rgb(p, q, hue); + b = hue2rgb(p, q, hue - 1 / 3); + } + + return rgb(r, g, b); +}; diff --git a/shared/src/main.ts b/shared/src/main.ts index 6154297..81235b1 100644 --- a/shared/src/main.ts +++ b/shared/src/main.ts @@ -5,7 +5,9 @@ export * from './commands/types/delete-objects'; export * from './commands/types/ternary-action'; export * from './commands/types/move-action'; export * from './commands/types/primary-action'; +export * from './commands/types/player-died'; export * from './commands/types/update-objects'; +export * from './commands/types/update-planet-ownership'; export * from './commands/types/secondary-action'; export * from './commands/command-receiver'; export * from './commands/command-executors'; @@ -15,6 +17,7 @@ export * from './commands/types/set-aspect-ratio-action'; export * from './helper/array'; export * from './helper/last'; export * from './helper/rgb'; +export * from './helper/hsl'; export * from './helper/rgb255'; export * from './helper/mix-rgb'; export * from './helper/clamp'; @@ -36,6 +39,7 @@ export * from './transport/serialization/serializes-to'; export * from './transport/serialization/serializable'; export * from './transport/serialization/override-deserialization'; export * from './objects/types/character-base'; +export * from './objects/types/character-team'; export * from './objects/update-message'; export * from './objects/types/player-character-base'; export * from './objects/types/lamp-base'; diff --git a/shared/src/objects/game-object.ts b/shared/src/objects/game-object.ts index 22d9e6f..76f080d 100644 --- a/shared/src/objects/game-object.ts +++ b/shared/src/objects/game-object.ts @@ -1,5 +1,14 @@ +import { UpdateMessage, UpdateObjectMessage } from '../main'; import { Id } from '../transport/identity'; export abstract class GameObject { constructor(public readonly id: Id) {} + + public calculateUpdates(): UpdateObjectMessage | undefined { + return; + } + + update(updates: Array): void { + updates.forEach((u) => ((this as any)[u.key] = u.value)); + } } diff --git a/shared/src/objects/types/character-base.ts b/shared/src/objects/types/character-base.ts index 57c9e99..7e8b981 100644 --- a/shared/src/objects/types/character-base.ts +++ b/shared/src/objects/types/character-base.ts @@ -2,12 +2,15 @@ import { Circle } from '../../helper/circle'; import { Id } from '../../transport/identity'; import { serializable } from '../../transport/serialization/serializable'; import { GameObject } from '../game-object'; +import { CharacterTeam } from './character-team'; @serializable export class CharacterBase extends GameObject { constructor( id: Id, public colorIndex: number, + public team: CharacterTeam, + public health: number, public head?: Circle, public leftFoot?: Circle, public rightFoot?: Circle, @@ -16,7 +19,7 @@ export class CharacterBase extends GameObject { } public toArray(): Array { - const { id, colorIndex, head, leftFoot, rightFoot } = this; - return [id, colorIndex, head, leftFoot, rightFoot]; + const { id, colorIndex, team, health, head, leftFoot, rightFoot } = this; + return [id, colorIndex, team, health, head, leftFoot, rightFoot]; } } diff --git a/shared/src/objects/types/character-team.ts b/shared/src/objects/types/character-team.ts new file mode 100644 index 0000000..000a78b --- /dev/null +++ b/shared/src/objects/types/character-team.ts @@ -0,0 +1,4 @@ +export enum CharacterTeam { + decla = 'decla', + red = 'red', +} diff --git a/shared/src/objects/types/planet-base.ts b/shared/src/objects/types/planet-base.ts index 205f6c4..2f2b1e0 100644 --- a/shared/src/objects/types/planet-base.ts +++ b/shared/src/objects/types/planet-base.ts @@ -7,8 +7,16 @@ import { GameObject } from '../game-object'; @serializable export class PlanetBase extends GameObject { - constructor(id: Id, public readonly vertices: Array) { + public readonly center: vec2; + + constructor( + id: Id, + public readonly vertices: Array, + public ownership: number = 0.5, + ) { super(id); + this.center = vertices.reduce((sum, v) => vec2.add(sum, sum, v), vec2.create()); + vec2.scale(this.center, this.center, 1 / vertices.length); } public static createPlanetVertices( @@ -16,7 +24,7 @@ export class PlanetBase extends GameObject { width: number, height: number, randomness: number, - vertexCount = settings.polygonEdgeCount, + vertexCount = settings.planetEdgeCount, ): Array { const vertices = []; diff --git a/shared/src/objects/types/player-character-base.ts b/shared/src/objects/types/player-character-base.ts index 61d9e62..ec65fac 100644 --- a/shared/src/objects/types/player-character-base.ts +++ b/shared/src/objects/types/player-character-base.ts @@ -2,6 +2,7 @@ import { CharacterBase } from './character-base'; import { Circle } from '../../helper/circle'; import { Id } from '../../transport/identity'; import { serializable } from '../../transport/serialization/serializable'; +import { CharacterTeam } from './character-team'; @serializable export class PlayerCharacterBase extends CharacterBase { @@ -9,15 +10,17 @@ export class PlayerCharacterBase extends CharacterBase { id: Id, public name: string, colorIndex: number, + team: CharacterTeam, + health: number, head?: Circle, leftFoot?: Circle, rightFoot?: Circle, ) { - super(id, colorIndex, head, leftFoot, rightFoot); + super(id, colorIndex, team, health, head, leftFoot, rightFoot); } public toArray(): Array { - const { id, name, colorIndex, head, leftFoot, rightFoot } = this; - return [id, name, colorIndex, head, leftFoot, rightFoot]; + const { id, name, colorIndex, team, health, head, leftFoot, rightFoot } = this; + return [id, name, colorIndex, team, health, head, leftFoot, rightFoot]; } } diff --git a/shared/src/objects/types/projectile-base.ts b/shared/src/objects/types/projectile-base.ts index 90ef4e2..ab51e75 100644 --- a/shared/src/objects/types/projectile-base.ts +++ b/shared/src/objects/types/projectile-base.ts @@ -5,11 +5,17 @@ import { GameObject } from '../game-object'; @serializable export class ProjectileBase extends GameObject { - constructor(id: Id, public center: vec2, public radius: number) { + constructor( + id: Id, + public center: vec2, + public radius: number, + public colorIndex: number, + public strength: number, + ) { super(id); } public toArray(): Array { - return [this.id, this.center, this.radius]; + return [this.id, this.center, this.radius, this.colorIndex, this.strength]; } } diff --git a/shared/src/settings.ts b/shared/src/settings.ts index 777265c..98aec09 100644 --- a/shared/src/settings.ts +++ b/shared/src/settings.ts @@ -1,55 +1,47 @@ +import { rgb } from './helper/rgb'; import { rgb255 } from './helper/rgb255'; +const declaColor = rgb255(181, 138, 255); +const redColor = rgb255(255, 138, 138); +const declaPlanetColor = rgb(0, 0, 3); +const redPlanetColor = rgb(3, 0, 0); + export const settings = { lightCutoffDistance: 600, physicsMaxStep: 2, maxVelocityX: 1000, maxVelocityY: 1000, - polygonEdgeCount: 7, + planetEdgeCount: 7, + takeControlTimeInSeconds: 10, maxGravityDistance: 700, maxGravityQ: 180, + planetControlThreshold: 0.2, + playerMaxHealth: 100, maxGravityStrength: 10000, maxAcceleration: 10000, + playerMaxStrength: 80, + playerDiedTimeout: 5, + playerStrengthRegenerationPerSeconds: 40, + projectileMaxStrength: 40, projectileSpeed: 4000, projectileMaxBounceCount: 1, - projectileStartOffset: 250, + projectileTimeout: 3, + projectileFadeSpeed: 20, + projectileStartOffset: 150, projectileCreationInterval: 0.1, playerColorIndexOffset: 3, - worldTopEdge: 10000, - worldRightEdge: 10000, - worldLeftEdge: -10000, - worldBottomEdge: -10000, + worldTopEdge: 3000, + worldRightEdge: 3000, + worldLeftEdge: -3000, + worldBottomEdge: -3000, backgroundGradient: [rgb255(90, 38, 43), rgb255(43, 39, 73)], - playerColors: [ - rgb255(107, 48, 188), - rgb255(56, 254, 220), - rgb255(197, 17, 17), - rgb255(24, 24, 24), - rgb255(245, 245, 88), - rgb255(80, 239, 58), - rgb255(18, 127, 45), - rgb255(240, 125, 13), - rgb255(214, 227, 241), - rgb255(0, 161, 161), - rgb255(250, 161, 151), - rgb255(235, 84, 185), - rgb255(114, 73, 30), - rgb255(75, 75, 75), - rgb255(107, 48, 188), - rgb255(56, 254, 220), - rgb255(197, 17, 17), - rgb255(24, 24, 24), - rgb255(245, 245, 88), - rgb255(80, 239, 58), - rgb255(18, 127, 45), - rgb255(240, 125, 13), - rgb255(214, 227, 241), - rgb255(0, 161, 161), - rgb255(250, 161, 151), - rgb255(235, 84, 185), - rgb255(114, 73, 30), - rgb255(75, 75, 75), - ], + declaColor, + declaPlanetColor, + redColor, + redPlanetColor, + declaIndex: 0, + redIndex: 1, + palette: [declaColor, redColor], targetPhysicsDeltaTimeInMilliseconds: 20, minPhysicsSleepTime: 4, velocityAttenuation: 0.25, diff --git a/shared/tsconfig.json b/shared/tsconfig.json index f85c232..46fe043 100644 --- a/shared/tsconfig.json +++ b/shared/tsconfig.json @@ -13,5 +13,6 @@ "module": "commonjs", "composite": true, "lib": ["dom", "es2017"] - } + }, + "include": ["src/**/*.ts", "src/*.ts"] } diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..4b59984 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "es6", + "esModuleInterop": true, + "strict": true, + "experimentalDecorators": true, + "downlevelIteration": true, + "moduleResolution": "node", + "module": "commonjs", + "composite": true, + "lib": ["dom", "es2017"] + } +}