174 lines
4.4 KiB
TypeScript
174 lines
4.4 KiB
TypeScript
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<GameContext> {
|
|
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<void> {
|
|
while (true) {
|
|
this.update(await nextAnimationFrame() - this.initTime);
|
|
this.render();
|
|
}
|
|
}
|
|
}
|