import { CollidableGroup, collide, mkCollision } from './collision'; import { EntityContext } from './entitycontext'; import { MapData } from '../model/data/map'; import { ButtonCode, GameInputHandler } from '../view/input/gameinput'; import { MapView } from '../view/map'; import { Renderer } from '../view/renderer/renderer'; import { Collidable } from '../math/collision'; import { Movement } from '../math/line'; import { getJSON, nextAnimationFrame } from '../util'; import { vec2 } from 'gl-matrix'; export class GameContext implements CollidableGroup { public static async load(renderer: Renderer): Promise { const map = this.loadMap(renderer, 'test'); const loadPlayer = EntityContext.load(renderer, 'green_circle'); const loadEntity = EntityContext.load(renderer, 'red_circle'); const [mapView, mapCollision] = await map; const player = await loadPlayer; const entity = await loadEntity; vec2.set(player.pos, 7, 6); vec2.set(entity.pos, 4, 3); return new GameContext( renderer, mapView, player, [entity], mapCollision, ); } private static async loadMap(renderer: Renderer, name: string): Promise<[MapView, Collidable[]]> { const map = new MapData(await getJSON(`resources/map/${name}.json`)); return [await MapView.load(renderer, map), mkCollision(map.collision)]; } private readonly initTime: number = performance.now(); private time = 0; private readonly tick = 10; // ms per tick private readonly maxSpeed = 0.04; // movement per tick private readonly maxSkip = 20; // maximum ticks to process in a single render step private readonly input: GameInputHandler; private readonly playerDir: vec2 = vec2.fromValues(0, 1); private speed: number = 0; private readonly collisionRadius = 15 / 32; private readonly interactLength = 1 / 32; private constructor( private readonly renderer: Renderer, private readonly mapView: MapView, private readonly player: EntityContext, private readonly entities: EntityContext[], private readonly collision: Collidable[], ) { this.input = new GameInputHandler(); this.input.addListener((input) => { switch (input.type) { case 'button': if (input.button === ButtonCode.Action) this.interact(); break; case 'direction': if (vec2.sqrLen(input.direction) > 0) { vec2.copy(this.playerDir, input.direction); this.speed = this.maxSpeed; } else { this.speed = 0; } break; } }); this.renderLoop(); } public getTranslation(): null { return null; } public getCollidables(): Collidable[] { return this.collision; } private updateTime(time: number): number { const diff = Math.round(time / this.tick) - Math.round(this.time / this.tick); this.time = time; return diff; } private canInteract(c: CollidableGroup): boolean { const dest = vec2.scaleAndAdd(vec2.create(), this.player.pos, this.playerDir, this.interactLength); const move = new Movement(this.player.pos, dest); return collide(c, vec2.create(), move, this.collisionRadius); } private interact(): void { for (const e of this.entities) { if (!this.canInteract(e)) continue; e.interact(); break; } } private updateStepCollide(out: vec2, dest: vec2): boolean { const move = new Movement(this.player.pos, dest); for (const c of [this, ...this.entities]) { if (collide(c, out, move, this.collisionRadius)) { if (vec2.squaredDistance(move.src, out) >= vec2.squaredDistance(move.src, move.dest)) continue; return true; } } return false; } private updateStep(): void { const dest = vec2.scaleAndAdd(vec2.create(), this.player.pos, this.playerDir, this.speed); const newDest = vec2.create(); while (this.updateStepCollide(newDest, dest)) { if (vec2.equals(newDest, this.player.pos)) return; vec2.copy(dest, newDest); } vec2.copy(this.player.pos, dest); } private update(time: number): void { const diff = Math.min(this.maxSkip, this.updateTime(time)); if (!this.speed) { this.renderer.snapToGrid(this.player.pos, this.player.pos); return; } for (let i = 0; i < diff; i++) this.updateStep(); } private render(): void { this.renderer.setCenter(this.player.pos); this.renderer.clear(); for (const r of [this.mapView, ...this.entities, this.player]) r.render(this.time); } private async renderLoop(): Promise { while (true) { this.update(await nextAnimationFrame() - this.initTime); this.render(); } } }