This repository has been archived on 2025-03-02. You can view files and clone it, but cannot push or open issues or pull requests.
rpgedit/src/renderer/runtime/controller/gamecontext.ts

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();
}
}
}