summaryrefslogtreecommitdiffstats
path: root/src/renderer/runtime/controller/gamecontext.ts
blob: 1b17314edc478894eca3b4367ad1e62415a99185 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
import { CollidableGroup, collide, mkCollision } from './collision';
import { EntityContext } from './entitycontext';

import { MapData, MapDataInput } 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`)) as MapDataInput);
		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 = 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();
		}
	}
}