Allow interacting with entities
This commit is contained in:
parent
e0eddb8741
commit
453a9391cc
6 changed files with 140 additions and 62 deletions
|
@ -17,6 +17,7 @@ export class EntityContext implements CollidableGroup {
|
||||||
|
|
||||||
return new EntityContext(
|
return new EntityContext(
|
||||||
renderer,
|
renderer,
|
||||||
|
name,
|
||||||
await loadEntity(renderer, entity),
|
await loadEntity(renderer, entity),
|
||||||
mkCollision(entity.collision),
|
mkCollision(entity.collision),
|
||||||
);
|
);
|
||||||
|
@ -26,8 +27,9 @@ export class EntityContext implements CollidableGroup {
|
||||||
|
|
||||||
private constructor(
|
private constructor(
|
||||||
private readonly renderer: Renderer,
|
private readonly renderer: Renderer,
|
||||||
|
private readonly name: string,
|
||||||
private readonly view: SpriteView,
|
private readonly view: SpriteView,
|
||||||
public readonly collision: Collidable[],
|
private readonly collision: Collidable[],
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
|
@ -42,4 +44,8 @@ export class EntityContext implements CollidableGroup {
|
||||||
public getCollidables(): Collidable[] {
|
public getCollidables(): Collidable[] {
|
||||||
return this.collision;
|
return this.collision;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public interact() {
|
||||||
|
alert(`You've interacted with ${this.name}!`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { EntityContext } from './entitycontext';
|
||||||
|
|
||||||
import { MapData } from '../model/data/map';
|
import { MapData } from '../model/data/map';
|
||||||
|
|
||||||
import { DirectionHandler } from '../view/input/directionhandler';
|
import { ButtonCode, GameInputHandler } from '../view/input/gameinput';
|
||||||
import { loadMap } from '../view/map';
|
import { loadMap } from '../view/map';
|
||||||
import { Renderer } from '../view/renderer/renderer';
|
import { Renderer } from '../view/renderer/renderer';
|
||||||
import { SpriteView } from '../view/sprite';
|
import { SpriteView } from '../view/sprite';
|
||||||
|
@ -44,14 +44,16 @@ export class GameContext implements CollidableGroup {
|
||||||
private time: number|null = null;
|
private time: number|null = null;
|
||||||
|
|
||||||
private readonly tick = 10; // ms per tick
|
private readonly tick = 10; // ms per tick
|
||||||
private readonly speed = 0.04; // movement 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 maxSkip = 20; // maximum ticks to process in a single render step
|
||||||
|
|
||||||
private readonly input: DirectionHandler;
|
private readonly input: GameInputHandler;
|
||||||
|
|
||||||
private readonly playerMovement: vec2 = vec2.create();
|
private readonly playerDir: vec2 = vec2.fromValues(0, 1);
|
||||||
|
private speed: number = 0;
|
||||||
|
|
||||||
private readonly collisionRadius = 7 / 16;
|
private readonly collisionRadius = 7 / 16;
|
||||||
|
private readonly interactLength = 1 / 32;
|
||||||
|
|
||||||
private constructor(
|
private constructor(
|
||||||
private readonly renderer: Renderer,
|
private readonly renderer: Renderer,
|
||||||
|
@ -60,12 +62,23 @@ export class GameContext implements CollidableGroup {
|
||||||
private readonly entities: EntityContext[],
|
private readonly entities: EntityContext[],
|
||||||
private readonly collision: Collidable[],
|
private readonly collision: Collidable[],
|
||||||
) {
|
) {
|
||||||
this.input = new DirectionHandler();
|
this.input = new GameInputHandler();
|
||||||
this.input.addListener((v) => {
|
this.input.addListener((input) => {
|
||||||
if (vec2.sqrLen(v) > 0)
|
switch (input.type) {
|
||||||
vec2.normalize(this.playerMovement, v);
|
case 'button':
|
||||||
else
|
if (input.button === ButtonCode.Action)
|
||||||
vec2.copy(this.playerMovement, [0, 0]);
|
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;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
window.requestAnimationFrame(this.render);
|
window.requestAnimationFrame(this.render);
|
||||||
|
@ -86,6 +99,23 @@ export class GameContext implements CollidableGroup {
|
||||||
return diff;
|
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 {
|
private updateStepCollide(out: vec2, dest: vec2): boolean {
|
||||||
const move = new Movement(this.player.pos, dest);
|
const move = new Movement(this.player.pos, dest);
|
||||||
|
|
||||||
|
@ -102,7 +132,7 @@ export class GameContext implements CollidableGroup {
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateStep(): void {
|
private updateStep(): void {
|
||||||
const dest = vec2.scaleAndAdd(vec2.create(), this.player.pos, this.playerMovement, this.speed);
|
const dest = vec2.scaleAndAdd(vec2.create(), this.player.pos, this.playerDir, this.speed);
|
||||||
const newDest = vec2.create();
|
const newDest = vec2.create();
|
||||||
|
|
||||||
while (this.updateStepCollide(newDest, dest)) {
|
while (this.updateStepCollide(newDest, dest)) {
|
||||||
|
@ -118,7 +148,7 @@ export class GameContext implements CollidableGroup {
|
||||||
private update(time: number): void {
|
private update(time: number): void {
|
||||||
const diff = Math.min(this.maxSkip, this.updateTime(time));
|
const diff = Math.min(this.maxSkip, this.updateTime(time));
|
||||||
|
|
||||||
if (vec2.sqrLen(this.playerMovement) === 0) {
|
if (!this.speed) {
|
||||||
this.renderer.snapToGrid(this.player.pos, this.player.pos);
|
this.renderer.snapToGrid(this.player.pos, this.player.pos);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
import { InputHandler } from './inputhandler';
|
|
||||||
|
|
||||||
import { Listenable } from '../../util';
|
|
||||||
|
|
||||||
import { vec2 } from 'gl-matrix';
|
|
||||||
|
|
||||||
export const enum Keycode {
|
|
||||||
Left = 37,
|
|
||||||
Up = 38,
|
|
||||||
Right = 39,
|
|
||||||
Down = 40,
|
|
||||||
}
|
|
||||||
|
|
||||||
export class DirectionHandler extends Listenable<[vec2]> {
|
|
||||||
private readonly input: InputHandler;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.input = new InputHandler(new Set([Keycode.Left, Keycode.Up, Keycode.Right, Keycode.Down]));
|
|
||||||
|
|
||||||
this.input.addListener(() => {
|
|
||||||
const dir = vec2.create();
|
|
||||||
|
|
||||||
if (this.input.has(Keycode.Left))
|
|
||||||
vec2.add(dir, dir, [-1, 0]);
|
|
||||||
if (this.input.has(Keycode.Up))
|
|
||||||
vec2.add(dir, dir, [0, -1]);
|
|
||||||
if (this.input.has(Keycode.Right))
|
|
||||||
vec2.add(dir, dir, [1, 0]);
|
|
||||||
if (this.input.has(Keycode.Down))
|
|
||||||
vec2.add(dir, dir, [0, 1]);
|
|
||||||
|
|
||||||
this.runListeners(dir);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
78
src/view/input/gameinput.ts
Normal file
78
src/view/input/gameinput.ts
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
import { InputHandler } from './inputhandler';
|
||||||
|
|
||||||
|
import { Listenable } from '../../util';
|
||||||
|
|
||||||
|
import { vec2 } from 'gl-matrix';
|
||||||
|
|
||||||
|
export enum ButtonCode {
|
||||||
|
Action,
|
||||||
|
Back,
|
||||||
|
Menu,
|
||||||
|
}
|
||||||
|
|
||||||
|
const buttonMapping: {[key: string]: ButtonCode} = {
|
||||||
|
KeyZ: ButtonCode.Action,
|
||||||
|
KeyX: ButtonCode.Back,
|
||||||
|
KeyC: ButtonCode.Menu,
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface DirectionInput {
|
||||||
|
type: 'direction';
|
||||||
|
direction: vec2;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ButtonInput {
|
||||||
|
type: 'button';
|
||||||
|
button: ButtonCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GameInput = DirectionInput | ButtonInput;
|
||||||
|
|
||||||
|
export class GameInputHandler extends Listenable<[GameInput]> {
|
||||||
|
private readonly input: InputHandler;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.input = new InputHandler(
|
||||||
|
new Set([
|
||||||
|
'ArrowLeft',
|
||||||
|
'ArrowUp',
|
||||||
|
'ArrowRight',
|
||||||
|
'ArrowDown',
|
||||||
|
...Object.keys(buttonMapping),
|
||||||
|
]));
|
||||||
|
|
||||||
|
this.input.addListener((key: string, pressed: boolean) => {
|
||||||
|
const button = buttonMapping[key];
|
||||||
|
if (button !== undefined) {
|
||||||
|
if (pressed)
|
||||||
|
this.runListeners({
|
||||||
|
type: 'button',
|
||||||
|
button,
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dir = vec2.create();
|
||||||
|
|
||||||
|
if (this.input.has('ArrowLeft'))
|
||||||
|
vec2.add(dir, dir, [-1, 0]);
|
||||||
|
if (this.input.has('ArrowUp'))
|
||||||
|
vec2.add(dir, dir, [0, -1]);
|
||||||
|
if (this.input.has('ArrowRight'))
|
||||||
|
vec2.add(dir, dir, [1, 0]);
|
||||||
|
if (this.input.has('ArrowDown'))
|
||||||
|
vec2.add(dir, dir, [0, 1]);
|
||||||
|
|
||||||
|
if (vec2.sqrLen(dir) > 0)
|
||||||
|
vec2.normalize(dir, dir);
|
||||||
|
|
||||||
|
this.runListeners({
|
||||||
|
type: 'direction',
|
||||||
|
direction: dir,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,39 +1,39 @@
|
||||||
import { Listenable } from '../../util';
|
import { Listenable } from '../../util';
|
||||||
|
|
||||||
export class InputHandler extends Listenable<[]> {
|
export class InputHandler extends Listenable<[string, boolean]> {
|
||||||
private readonly keys: Set<number> = new Set();
|
private readonly keys: Set<string> = new Set();
|
||||||
|
|
||||||
constructor(relevantKeys: Set<number>) {
|
constructor(relevantKeys: Set<string>) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
window.addEventListener('keydown', (ev) => {
|
window.addEventListener('keydown', (ev) => {
|
||||||
if (!relevantKeys.has(ev.keyCode))
|
if (!relevantKeys.has(ev.code))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
||||||
if (this.keys.has(ev.keyCode))
|
if (ev.repeat)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this.keys.add(ev.keyCode);
|
this.keys.add(ev.code);
|
||||||
this.runListeners();
|
this.runListeners(ev.code, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('keyup', (ev) => {
|
window.addEventListener('keyup', (ev) => {
|
||||||
if (!relevantKeys.has(ev.keyCode))
|
if (!relevantKeys.has(ev.code))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
||||||
if (!this.keys.has(ev.keyCode))
|
if (!this.keys.has(ev.code))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this.keys.delete(ev.keyCode);
|
this.keys.delete(ev.code);
|
||||||
this.runListeners();
|
this.runListeners(ev.code, false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public has(key: number): boolean {
|
public has(key: string): boolean {
|
||||||
return this.keys.has(key);
|
return this.keys.has(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
"interface-name": false,
|
"interface-name": false,
|
||||||
"max-classes-per-file": false,
|
"max-classes-per-file": false,
|
||||||
"no-bitwise": false,
|
"no-bitwise": false,
|
||||||
|
"object-literal-sort-keys": false,
|
||||||
"one-variable-per-declaration": false,
|
"one-variable-per-declaration": false,
|
||||||
"quotemark": [true, "single", "avoid-escape", "avoid-template"]
|
"quotemark": [true, "single", "avoid-escape", "avoid-template"]
|
||||||
},
|
},
|
||||||
|
|
Reference in a new issue