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 897278d..0000000 Binary files a/frontend/static/declared.png and /dev/null differ 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"] + } +}