summaryrefslogtreecommitdiffstats
path: root/src/renderer/runtime/controller
diff options
context:
space:
mode:
Diffstat (limited to 'src/renderer/runtime/controller')
-rw-r--r--src/renderer/runtime/controller/collision.ts65
-rw-r--r--src/renderer/runtime/controller/entitycontext.ts47
-rw-r--r--src/renderer/runtime/controller/gamecontext.ts174
3 files changed, 286 insertions, 0 deletions
diff --git a/src/renderer/runtime/controller/collision.ts b/src/renderer/runtime/controller/collision.ts
new file mode 100644
index 0000000..a44ffa4
--- /dev/null
+++ b/src/renderer/runtime/controller/collision.ts
@@ -0,0 +1,65 @@
+import { Collision } from '../model/data/collision';
+
+import { Circle } from '../math/circle';
+import { Collidable } from '../math/collision';
+import { LineSegment, Movement } from '../math/line';
+import { Point } from '../math/point';
+
+import { vec2 } from 'gl-matrix';
+
+export function mkCollision(collision: Collision[]): Collidable[] {
+ const ret: Collidable[] = [];
+
+ for (const c of collision) {
+ switch (c.type) {
+ case 'polygon':
+ if (!c.vertices.length)
+ continue;
+
+ let prev = c.vertices[c.vertices.length - 1];
+
+ for (const v of c.vertices) {
+ ret.push(LineSegment.fromPoints(vec2.clone(prev), vec2.clone(v)));
+ prev = v;
+ }
+
+ for (const v of c.vertices) {
+ ret.push(new Point(vec2.clone(v)));
+ prev = v;
+ }
+ break;
+
+ case 'circle':
+ ret.push(new Circle(vec2.clone(c.center), c.radius));
+ break;
+ }
+ }
+
+ return ret;
+}
+
+export interface CollidableGroup {
+ getTranslation(): vec2|null;
+ getCollidables(): Collidable[];
+}
+
+export function collide(collision: CollidableGroup, out: vec2, move: Movement, radius: number): boolean {
+ const t = collision.getTranslation();
+ if (t)
+ move = move.translate(vec2.negate(vec2.create(), t));
+
+ for (const c of collision.getCollidables()) {
+ if (!c.collide(out, move, radius))
+ continue;
+
+ if (vec2.squaredDistance(move.src, out) >= vec2.squaredDistance(move.src, move.dest))
+ continue;
+
+ if (t)
+ vec2.add(out, out, t);
+
+ return true;
+ }
+
+ return false;
+}
diff --git a/src/renderer/runtime/controller/entitycontext.ts b/src/renderer/runtime/controller/entitycontext.ts
new file mode 100644
index 0000000..434bf9b
--- /dev/null
+++ b/src/renderer/runtime/controller/entitycontext.ts
@@ -0,0 +1,47 @@
+import { EntityView } from '../view/entity';
+import { Renderer } from '../view/renderer/renderer';
+
+import { Collidable } from '../math/collision';
+
+import { CollidableGroup, mkCollision } from './collision';
+
+import { vec2 } from 'gl-matrix';
+
+export class EntityContext implements CollidableGroup {
+ public static async load(renderer: Renderer, name: string): Promise<EntityContext> {
+ return new EntityContext(
+ renderer,
+ name,
+ await EntityView.load(renderer, name),
+ );
+ }
+
+ public readonly pos: vec2 = vec2.create();
+
+ private readonly collision: Collidable[];
+
+ private constructor(
+ private readonly renderer: Renderer,
+ private readonly name: string,
+ private readonly view: EntityView,
+ ) {
+ this.collision = mkCollision(view.data.collision);
+ }
+
+ public render(time: number) {
+ this.renderer.setTranslation(this.pos);
+ this.view.renderByTime(time);
+ }
+
+ public getTranslation(): vec2 {
+ return this.pos;
+ }
+
+ public getCollidables(): Collidable[] {
+ return this.collision;
+ }
+
+ public interact() {
+ alert(`You've interacted with ${this.name}!`);
+ }
+}
diff --git a/src/renderer/runtime/controller/gamecontext.ts b/src/renderer/runtime/controller/gamecontext.ts
new file mode 100644
index 0000000..9a68e25
--- /dev/null
+++ b/src/renderer/runtime/controller/gamecontext.ts
@@ -0,0 +1,174 @@
+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();
+ }
+ }
+}