import { vec2 } from 'gl-matrix'; import { CircleLight, compile, FilteringOptions, Flashlight, Renderer, renderNoise, WrapOptions, } from 'sdf-2d'; import { broadcastCommands, deserialize, serialize, settings, TransportEvents, SetAspectRatioActionCommand, rgb, PlayerInformation, } 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'; export class Game { public readonly gameObjects = new GameObjectContainer(this); private renderer!: Renderer; private socket!: SocketIOClient.Socket; private promises: Promise<[void, void]>; private deltaTimeCalculator = new DeltaTimeCalculator(); constructor( private readonly playerDecision: PlayerDecision, private readonly canvas: HTMLCanvasElement, private readonly overlay: HTMLElement, ) { this.promises = Promise.all([ this.setupCommunication(playerDecision.server), this.setupRenderer(), ]); } private async setupCommunication(serverUrl: string): Promise { this.socket = io(serverUrl, { reconnectionDelayMax: 10000, transports: ['websocket'], }); this.socket.on('reconnect_attempt', () => { this.socket.io.opts.transports = ['polling', 'websocket']; }); this.socket.on(TransportEvents.ServerToPlayer, (serialized: string) => { const command = deserialize(serialized); this.gameObjects.sendCommand(command); }); this.socket.on(TransportEvents.Ping, () => { this.socket.emit(TransportEvents.Pong); }); this.socket.emit(TransportEvents.PlayerJoining, { name: this.playerDecision.playerName, } as PlayerInformation); broadcastCommands( [ new KeyboardListener(document.body), new MouseListener(this.canvas, this), new TouchListener(this.canvas, this), ], [this.gameObjects, new CommandReceiverSocket(this.socket)], ); } private async setupRenderer(): Promise { const noiseTexture = await renderNoise([256, 256], 2, 1); this.renderer = await compile( this.canvas, [ { ...Polygon.descriptor, shaderCombinationSteps: [0, 1, 2, 3], }, { ...BlobShape.descriptor, shaderCombinationSteps: [0, 1, 2, 8], }, { ...Circle.descriptor, shaderCombinationSteps: [0, 2, 16], }, { ...CircleLight.descriptor, shaderCombinationSteps: [0, 1, 2, 4, 8], }, { ...Flashlight.descriptor, shaderCombinationSteps: [0], }, ], { shadowTraceCount: 16, paletteSize: 10, //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, }, }, }, }); } public async start(): Promise { await this.promises; requestAnimationFrame(this.gameLoop.bind(this)); } public displayToWorldCoordinates(p: vec2): vec2 { return this.renderer?.displayToWorldCoordinates(p); } public aspectRatioChanged(aspectRatio: number) { this.socket.emit( TransportEvents.PlayerToServer, serialize(new SetAspectRatioActionCommand(aspectRatio)), ); } private gameLoop(time: DOMHighResTimeStamp) { const deltaTime = this.deltaTimeCalculator.getNextDeltaTimeInMilliseconds(time); this.gameObjects.stepObjects(deltaTime); this.gameObjects.drawObjects(this.renderer); this.renderer.renderDrawables(); // this.overlay.innerText = prettyPrint(this.renderer.insights); requestAnimationFrame(this.gameLoop.bind(this)); } }