Fix lint issues

This commit is contained in:
Matthias Schiffer 2019-12-24 20:13:47 +01:00
parent 2f77cf7b74
commit 764cd8344d
Signed by: neocturne
GPG key ID: 16EF3F64CB201D9C
17 changed files with 203 additions and 271 deletions

View file

@ -5,11 +5,12 @@ import { format as formatUrl } from 'url';
const isDevelopment = process.env.NODE_ENV !== 'production'; const isDevelopment = process.env.NODE_ENV !== 'production';
// global reference to mainWindow (necessary to prevent window from being garbage collected) // global reference to mainWindow (necessary to prevent window from being garbage collected)
let mainWindow: BrowserWindow|null = null; let mainWindow: BrowserWindow | null = null;
function getIndexURL(): string { function getIndexURL(): string {
if (isDevelopment) if (isDevelopment) {
return `http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}`; return `http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}`;
}
return formatUrl({ return formatUrl({
pathname: path.join(__dirname, 'index.html'), pathname: path.join(__dirname, 'index.html'),
@ -19,10 +20,11 @@ function getIndexURL(): string {
} }
function createMainWindow(): BrowserWindow { function createMainWindow(): BrowserWindow {
const window = new BrowserWindow({webPreferences: {nodeIntegration: true}}); const window = new BrowserWindow({ webPreferences: { nodeIntegration: true } });
if (isDevelopment) if (isDevelopment) {
window.webContents.openDevTools(); window.webContents.openDevTools();
}
window.loadURL(getIndexURL()); window.loadURL(getIndexURL());
@ -32,7 +34,7 @@ function createMainWindow(): BrowserWindow {
window.webContents.on('devtools-opened', () => { window.webContents.on('devtools-opened', () => {
window.webContents.focus(); window.webContents.focus();
}) });
return window; return window;
} }
@ -40,14 +42,16 @@ function createMainWindow(): BrowserWindow {
// quit application when all windows are closed // quit application when all windows are closed
app.on('window-all-closed', () => { app.on('window-all-closed', () => {
// on macOS it is common for applications to stay open until the user explicitly quits // on macOS it is common for applications to stay open until the user explicitly quits
if (process.platform !== 'darwin') if (process.platform !== 'darwin') {
app.quit(); app.quit();
}
}); });
app.on('activate', () => { app.on('activate', () => {
// on macOS it is common to re-create a window even after all windows have been closed // on macOS it is common to re-create a window even after all windows have been closed
if (!mainWindow) if (!mainWindow) {
mainWindow = createMainWindow(); mainWindow = createMainWindow();
}
}); });
// create main BrowserWindow when electron is ready // create main BrowserWindow when electron is ready

View file

@ -1,9 +1,9 @@
declare module "*.vs" { declare module '*.vs' {
const content: string; const content: string;
export default content; export default content;
} }
declare module "*.fs" { declare module '*.fs' {
const content: string; const content: string;
export default content; export default content;
} }

View file

@ -4,15 +4,14 @@ import { GameContext } from './runtime/controller/gamecontext';
import { Renderer } from './runtime/view/renderer/renderer'; import { Renderer } from './runtime/view/renderer/renderer';
window.onload = async () => { window.onload = async (): Promise<void> => {
const app = document.getElementById('app'); const app = document.getElementById('app');
if (!app) if (!app) return;
return;
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
const renderer = new Renderer(canvas); const renderer = new Renderer(canvas);
const resize = () => { const resize = (): void => {
canvas.width = window.innerWidth; canvas.width = window.innerWidth;
canvas.height = window.innerHeight; canvas.height = window.innerHeight;
renderer.resize(); renderer.resize();

View file

@ -13,8 +13,7 @@ export function mkCollision(collision: Collision[]): Collidable[] {
for (const c of collision) { for (const c of collision) {
switch (c.type) { switch (c.type) {
case 'polygon': case 'polygon':
if (!c.vertices.length) if (!c.vertices.length) continue;
continue;
let prev = c.vertices[c.vertices.length - 1]; let prev = c.vertices[c.vertices.length - 1];
@ -39,24 +38,20 @@ export function mkCollision(collision: Collision[]): Collidable[] {
} }
export interface CollidableGroup { export interface CollidableGroup {
getTranslation(): vec2|null; getTranslation(): vec2 | null;
getCollidables(): Collidable[]; getCollidables(): Collidable[];
} }
export function collide(collision: CollidableGroup, out: vec2, move: Movement, radius: number): boolean { export function collide(collision: CollidableGroup, out: vec2, move: Movement, radius: number): boolean {
const t = collision.getTranslation(); const t = collision.getTranslation();
if (t) if (t) move = move.translate(vec2.negate(vec2.create(), t));
move = move.translate(vec2.negate(vec2.create(), t));
for (const c of collision.getCollidables()) { for (const c of collision.getCollidables()) {
if (!c.collide(out, move, radius)) if (!c.collide(out, move, radius)) continue;
continue;
if (vec2.squaredDistance(move.src, out) >= vec2.squaredDistance(move.src, move.dest)) if (vec2.squaredDistance(move.src, out) >= vec2.squaredDistance(move.src, move.dest)) continue;
continue;
if (t) if (t) vec2.add(out, out, t);
vec2.add(out, out, t);
return true; return true;
} }

View file

@ -9,11 +9,7 @@ import { vec2 } from 'gl-matrix';
export class EntityContext implements CollidableGroup { export class EntityContext implements CollidableGroup {
public static async load(renderer: Renderer, name: string): Promise<EntityContext> { public static async load(renderer: Renderer, name: string): Promise<EntityContext> {
return new EntityContext( return new EntityContext(renderer, name, await EntityView.load(renderer, name));
renderer,
name,
await EntityView.load(renderer, name),
);
} }
public readonly pos: vec2 = vec2.create(); public readonly pos: vec2 = vec2.create();
@ -28,7 +24,7 @@ export class EntityContext implements CollidableGroup {
this.collision = mkCollision(view.data.collision); this.collision = mkCollision(view.data.collision);
} }
public render(time: number) { public render(time: number): void {
this.renderer.setTranslation(this.pos); this.renderer.setTranslation(this.pos);
this.view.renderByTime(time); this.view.renderByTime(time);
} }
@ -41,7 +37,7 @@ export class EntityContext implements CollidableGroup {
return this.collision; return this.collision;
} }
public interact() { public interact(): void {
alert(`You've interacted with ${this.name}!`); alert(`You've interacted with ${this.name}!`);
} }
} }

View file

@ -1,7 +1,7 @@
import { CollidableGroup, collide, mkCollision } from './collision'; import { CollidableGroup, collide, mkCollision } from './collision';
import { EntityContext } from './entitycontext'; import { EntityContext } from './entitycontext';
import { MapData } from '../model/data/map'; import { MapData, MapDataInput } from '../model/data/map';
import { ButtonCode, GameInputHandler } from '../view/input/gameinput'; import { ButtonCode, GameInputHandler } from '../view/input/gameinput';
import { MapView } from '../view/map'; import { MapView } from '../view/map';
@ -26,17 +26,11 @@ export class GameContext implements CollidableGroup {
vec2.set(player.pos, 7, 6); vec2.set(player.pos, 7, 6);
vec2.set(entity.pos, 4, 3); vec2.set(entity.pos, 4, 3);
return new GameContext( return new GameContext(renderer, mapView, player, [entity], mapCollision);
renderer,
mapView,
player,
[entity],
mapCollision,
);
} }
private static async loadMap(renderer: Renderer, name: string): Promise<[MapView, Collidable[]]> { private static async loadMap(renderer: Renderer, name: string): Promise<[MapView, Collidable[]]> {
const map = new MapData(await getJSON(`resources/map/${name}.json`)); const map = new MapData((await getJSON(`resources/map/${name}.json`)) as MapDataInput);
return [await MapView.load(renderer, map), mkCollision(map.collision)]; return [await MapView.load(renderer, map), mkCollision(map.collision)];
} }
@ -50,7 +44,7 @@ export class GameContext implements CollidableGroup {
private readonly input: GameInputHandler; private readonly input: GameInputHandler;
private readonly playerDir: vec2 = vec2.fromValues(0, 1); private readonly playerDir: vec2 = vec2.fromValues(0, 1);
private speed: number = 0; private speed = 0;
private readonly collisionRadius = 15 / 32; private readonly collisionRadius = 15 / 32;
private readonly interactLength = 1 / 32; private readonly interactLength = 1 / 32;
@ -66,8 +60,7 @@ export class GameContext implements CollidableGroup {
this.input.addListener((input) => { this.input.addListener((input) => {
switch (input.type) { switch (input.type) {
case 'button': case 'button':
if (input.button === ButtonCode.Action) if (input.button === ButtonCode.Action) this.interact();
this.interact();
break; break;
case 'direction': case 'direction':
@ -108,8 +101,7 @@ export class GameContext implements CollidableGroup {
private interact(): void { private interact(): void {
for (const e of this.entities) { for (const e of this.entities) {
if (!this.canInteract(e)) if (!this.canInteract(e)) continue;
continue;
e.interact(); e.interact();
break; break;
@ -136,8 +128,7 @@ export class GameContext implements CollidableGroup {
const newDest = vec2.create(); const newDest = vec2.create();
while (this.updateStepCollide(newDest, dest)) { while (this.updateStepCollide(newDest, dest)) {
if (vec2.equals(newDest, this.player.pos)) if (vec2.equals(newDest, this.player.pos)) return;
return;
vec2.copy(dest, newDest); vec2.copy(dest, newDest);
} }
@ -153,21 +144,19 @@ export class GameContext implements CollidableGroup {
return; return;
} }
for (let i = 0; i < diff; i++) for (let i = 0; i < diff; i++) this.updateStep();
this.updateStep();
} }
private render(): void { private render(): void {
this.renderer.setCenter(this.player.pos); this.renderer.setCenter(this.player.pos);
this.renderer.clear(); this.renderer.clear();
for (const r of [this.mapView, ...this.entities, this.player]) for (const r of [this.mapView, ...this.entities, this.player]) r.render(this.time);
r.render(this.time);
} }
private async renderLoop(): Promise<void> { private async renderLoop(): Promise<void> {
while (true) { while (true) {
this.update(await nextAnimationFrame() - this.initTime); this.update((await nextAnimationFrame()) - this.initTime);
this.render(); this.render();
} }
} }

View file

@ -1,10 +1,7 @@
import { mat2, vec2 } from 'gl-matrix'; import { mat2, vec2 } from 'gl-matrix';
import { Collidable } from './collision'; import { Collidable } from './collision';
const rot90 = mat2.fromValues( const rot90 = mat2.fromValues(0, 1, -1, 0);
0, 1,
-1, 0,
);
export function normal(out: vec2, a: vec2): vec2 { export function normal(out: vec2, a: vec2): vec2 {
return vec2.transformMat2(out, a, rot90); return vec2.transformMat2(out, a, rot90);
@ -15,10 +12,7 @@ export function crossz(a: vec2, b: vec2): number {
} }
export class Line { export class Line {
constructor( constructor(public readonly p: vec2, public readonly v: vec2) {}
public readonly p: vec2,
public readonly v: vec2,
) {}
public getNormal(out: vec2): vec2 { public getNormal(out: vec2): vec2 {
return normal(out, this.v); return normal(out, this.v);
@ -50,10 +44,7 @@ export class Line {
export class Movement { export class Movement {
public readonly v: vec2; public readonly v: vec2;
constructor( constructor(public readonly src: vec2, public readonly dest: vec2) {
public readonly src: vec2,
public readonly dest: vec2,
) {
this.v = vec2.sub(vec2.create(), dest, src); this.v = vec2.sub(vec2.create(), dest, src);
} }
@ -71,6 +62,7 @@ export class Movement {
} }
public toLineSegment(): LineSegment { public toLineSegment(): LineSegment {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
return LineSegment.fromPoints(this.src, this.dest); return LineSegment.fromPoints(this.src, this.dest);
} }
@ -90,11 +82,7 @@ export class LineSegment extends Line implements Collidable {
return new LineSegment(p1, v, d); return new LineSegment(p1, v, d);
} }
constructor( constructor(p: vec2, v: vec2, public readonly l: number) {
p: vec2,
v: vec2,
public readonly l: number,
) {
super(p, v); super(p, v);
} }
@ -104,35 +92,30 @@ export class LineSegment extends Line implements Collidable {
public containsPoint(p2: vec2): boolean { public containsPoint(p2: vec2): boolean {
const d = this.projectPointDistance(p2); const d = this.projectPointDistance(p2);
return (d >= 0 && d <= this.l); return d >= 0 && d <= this.l;
} }
public collide(out: vec2, move: Movement, r: number): boolean { public collide(out: vec2, move: Movement, r: number): boolean {
if (this.distancePoint(move.src) < 0) if (this.distancePoint(move.src) < 0) return false;
return false;
if (crossz(move.v, this.v) < 0) if (crossz(move.v, this.v) < 0) return false;
return false;
const t = this.getNormal(vec2.create()); const t = this.getNormal(vec2.create());
vec2.scale(t, t, -r); vec2.scale(t, t, -r);
const refMove = move.translate(t); const refMove = move.translate(t);
if (!this.collideRef(out, refMove)) if (!this.collideRef(out, refMove)) return false;
return false;
vec2.sub(out, out, t); vec2.sub(out, out, t);
return true; return true;
} }
private collideRef(out: vec2, move: Movement): boolean { private collideRef(out: vec2, move: Movement): boolean {
if (this.distancePoint(move.dest) >= 0) if (this.distancePoint(move.dest) >= 0) return false;
return false;
const x = move.intersectLine(vec2.create(), this); const x = move.intersectLine(vec2.create(), this);
if (!this.containsPoint(x)) if (!this.containsPoint(x)) return false;
return false;
this.projectPoint(out, move.dest); this.projectPoint(out, move.dest);

View file

@ -9,12 +9,10 @@ export class Point implements Collidable {
public collide(out: vec2, move: Movement, r: number): boolean { public collide(out: vec2, move: Movement, r: number): boolean {
const moveLine = move.toLineSegment(); const moveLine = move.toLineSegment();
if (moveLine.projectPointDistance(this.p) < 0) if (moveLine.projectPointDistance(this.p) < 0) return false;
return false;
const d = moveLine.distancePoint(this.p) / r; const d = moveLine.distancePoint(this.p) / r;
if (Math.abs(d) >= 1) if (Math.abs(d) >= 1) return false;
return false;
const e = Math.sqrt(1 - d * d); const e = Math.sqrt(1 - d * d);
@ -26,8 +24,7 @@ export class Point implements Collidable {
const refMove = move.translate(tr); const refMove = move.translate(tr);
if (vec2.sqrDist(this.p, move.src) > r * r && !refMove.passes(this.p)) if (vec2.sqrDist(this.p, move.src) > r * r && !refMove.passes(this.p)) return false;
return false;
normal(t, t); normal(t, t);

View file

@ -1,8 +1,7 @@
export function recordToMap<T>(r: Record<string, T>): Map<string, T> { export function recordToMap<T>(r: Record<string, T>): Map<string, T> {
const ret = new Map(); const ret = new Map();
for (const k of Object.keys(r)) for (const k of Object.keys(r)) ret.set(k, r[k]);
ret.set(k, r[k]);
return ret; return ret;
} }
@ -10,8 +9,7 @@ export function recordToMap<T>(r: Record<string, T>): Map<string, T> {
export function mapValues<K, V1, V2>(f: (v: V1) => V2, map: Map<K, V1>): Map<K, V2> { export function mapValues<K, V1, V2>(f: (v: V1) => V2, map: Map<K, V1>): Map<K, V2> {
const ret: Map<K, V2> = new Map(); const ret: Map<K, V2> = new Map();
for (const [k, v] of map) for (const [k, v] of map) ret.set(k, f(v));
ret.set(k, f(v));
return ret; return ret;
} }
@ -19,8 +17,7 @@ export function mapValues<K, V1, V2>(f: (v: V1) => V2, map: Map<K, V1>): Map<K,
export async function mapValuesAsync<K, V1, V2>(f: (v: V1) => Promise<V2>, map: Map<K, V1>): Promise<Map<K, V2>> { export async function mapValuesAsync<K, V1, V2>(f: (v: V1) => Promise<V2>, map: Map<K, V1>): Promise<Map<K, V2>> {
const ret: Map<K, V2> = new Map(); const ret: Map<K, V2> = new Map();
for (const [k, v] of mapValues(f, map)) for (const [k, v] of mapValues(f, map)) ret.set(k, await v);
ret.set(k, await v);
return ret; return ret;
} }
@ -28,12 +25,12 @@ export async function mapValuesAsync<K, V1, V2>(f: (v: V1) => Promise<V2>, map:
export function nextPowerOf2(n: number): number { export function nextPowerOf2(n: number): number {
let i = 1; let i = 1;
while (i < n) while (i < n) i *= 2;
i *= 2;
return i; return i;
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export class Listenable<T extends any[]> { export class Listenable<T extends any[]> {
private readonly listeners: Array<(...args: T) => void> = []; private readonly listeners: Array<(...args: T) => void> = [];
@ -46,10 +43,9 @@ export class Listenable<T extends any[]> {
} }
} }
export async function getJSON(url: string): Promise<any> { export async function getJSON(url: string): Promise<unknown> {
const res = await window.fetch(url); const res = await window.fetch(url);
if (res.status < 200 || res.status >= 300) if (res.status < 200 || res.status >= 300) throw new Error(res.statusText);
throw new Error(res.statusText);
return await res.json(); return await res.json();
} }

View file

@ -1,4 +1,4 @@
import { EntityData } from '../model/data/entity'; import { EntityData, EntityDataInput } from '../model/data/entity';
import { Renderer } from './renderer/renderer'; import { Renderer } from './renderer/renderer';
import { SpriteCoords, SpriteView, SpriteViewBuilder } from './sprite'; import { SpriteCoords, SpriteView, SpriteViewBuilder } from './sprite';
import { loadImage, mkTexture } from './util/image'; import { loadImage, mkTexture } from './util/image';
@ -9,7 +9,7 @@ import { vec2 } from 'gl-matrix';
export class EntityView { export class EntityView {
public static async load(r: Renderer, name: string): Promise<EntityView> { public static async load(r: Renderer, name: string): Promise<EntityView> {
const data = new EntityData(await getJSON(`resources/entity/${name}.json`)); const data = new EntityData((await getJSON(`resources/entity/${name}.json`)) as EntityDataInput);
const tile = await loadImage(`resources/sprite/entity/${data.sprite}.png`); const tile = await loadImage(`resources/sprite/entity/${data.sprite}.png`);
const [texture, size] = mkTexture(r, tile); const [texture, size] = mkTexture(r, tile);
@ -33,22 +33,14 @@ export class EntityView {
sprites.push(builder.build()); sprites.push(builder.build());
} }
return new EntityView( return new EntityView(data, sprites);
data,
sprites,
);
} }
private readonly totalTime: number; private readonly totalTime: number;
private constructor( private constructor(public readonly data: EntityData, public readonly sprites: SpriteView[]) {
public readonly data: EntityData, if (data.animation) this.totalTime = data.animation.sequence.reduce((a, s) => a + s[0], 0);
public readonly sprites: SpriteView[], else this.totalTime = 0;
) {
if (data.animation)
this.totalTime = data.animation.sequence.reduce((a, s) => a + s[0], 0);
else
this.totalTime = 0;
} }
public getSpriteByTime(time: number): SpriteView { public getSpriteByTime(time: number): SpriteView {
@ -57,16 +49,14 @@ export class EntityView {
if (this.data.animation) { if (this.data.animation) {
for (const [len, sprite] of this.data.animation.sequence) { for (const [len, sprite] of this.data.animation.sequence) {
time -= len; time -= len;
if (time < 0) if (time < 0) return this.sprites[sprite];
return this.sprites[sprite];
} }
} }
return this.sprites[0]; return this.sprites[0];
} }
public renderByTime(time: number) { public renderByTime(time: number): void {
this.getSpriteByTime(time).render(); this.getSpriteByTime(time).render();
} }
} }

View file

@ -35,13 +35,8 @@ export class GameInputHandler extends Listenable<[GameInput]> {
super(); super();
this.input = new InputHandler( this.input = new InputHandler(
new Set([ new Set(['ArrowLeft', 'ArrowUp', 'ArrowRight', 'ArrowDown', ...Object.keys(buttonMapping)]),
'ArrowLeft', );
'ArrowUp',
'ArrowRight',
'ArrowDown',
...Object.keys(buttonMapping),
]));
this.input.addListener((key: string, pressed: boolean) => { this.input.addListener((key: string, pressed: boolean) => {
const button = buttonMapping[key]; const button = buttonMapping[key];
@ -57,17 +52,12 @@ export class GameInputHandler extends Listenable<[GameInput]> {
const dir = vec2.create(); const dir = vec2.create();
if (this.input.has('ArrowLeft')) if (this.input.has('ArrowLeft')) vec2.add(dir, dir, [-1, 0]);
vec2.add(dir, dir, [-1, 0]); if (this.input.has('ArrowUp')) vec2.add(dir, dir, [0, -1]);
if (this.input.has('ArrowUp')) if (this.input.has('ArrowRight')) vec2.add(dir, dir, [1, 0]);
vec2.add(dir, dir, [0, -1]); if (this.input.has('ArrowDown')) 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) if (vec2.sqrLen(dir) > 0) vec2.normalize(dir, dir);
vec2.normalize(dir, dir);
this.runListeners({ this.runListeners({
type: 'direction', type: 'direction',

View file

@ -7,26 +7,22 @@ export class InputHandler extends Listenable<[string, boolean]> {
super(); super();
window.addEventListener('keydown', (ev) => { window.addEventListener('keydown', (ev) => {
if (!relevantKeys.has(ev.code)) if (!relevantKeys.has(ev.code)) return;
return;
ev.preventDefault(); ev.preventDefault();
if (ev.repeat) if (ev.repeat) return;
return;
this.keys.add(ev.code); this.keys.add(ev.code);
this.runListeners(ev.code, true); this.runListeners(ev.code, true);
}); });
window.addEventListener('keyup', (ev) => { window.addEventListener('keyup', (ev) => {
if (!relevantKeys.has(ev.code)) if (!relevantKeys.has(ev.code)) return;
return;
ev.preventDefault(); ev.preventDefault();
if (!this.keys.has(ev.code)) if (!this.keys.has(ev.code)) return;
return;
this.keys.delete(ev.code); this.keys.delete(ev.code);
this.runListeners(ev.code, false); this.runListeners(ev.code, false);

View file

@ -55,10 +55,7 @@ function loadTiles(r: Renderer, tiles: string[]): Promise<MapTile[]> {
return Promise.all(tiles.map((tile) => loadTile(r, tile))); return Promise.all(tiles.map((tile) => loadTile(r, tile)));
} }
function mkTileset( function mkTileset(r: Renderer, mapTiles: MapTile[]): Tileset {
r: Renderer,
mapTiles: MapTile[],
): Tileset {
const tileSize = 32; const tileSize = 32;
const canvasDim = nextPowerOf2(Math.sqrt(mapTiles.length)); const canvasDim = nextPowerOf2(Math.sqrt(mapTiles.length));
@ -67,7 +64,8 @@ function mkTileset(
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
canvas.width = canvas.height = canvasSize; canvas.width = canvas.height = canvasSize;
let x = 0, y = 0; let x = 0;
let y = 0;
const tiles: TilesetTile[] = []; const tiles: TilesetTile[] = [];
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D; const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
@ -77,7 +75,12 @@ function mkTileset(
ctx.drawImage(tile.image, x * tileSize, y * tileSize); ctx.drawImage(tile.image, x * tileSize, y * tileSize);
tiles.push({ tiles.push({
type: 'static', type: 'static',
coords: [x / canvasDim, y / canvasDim, (x + 1) / canvasDim, (y + 1) / canvasDim], coords: [
x / canvasDim,
y / canvasDim,
(x + 1) / canvasDim,
(y + 1) / canvasDim,
],
}); });
x++; x++;
@ -107,9 +110,9 @@ function addSprite(
tileset: Tileset, tileset: Tileset,
x: number, x: number,
y: number, y: number,
tile: number) { tile: number,
if (!tile) ): void {
return; if (!tile) return;
const tilesetTile = tileset.tiles[tile - 1]; const tilesetTile = tileset.tiles[tile - 1];
@ -124,24 +127,12 @@ function addSprite(
} }
} }
function buildMapLayer(r: Renderer, tileset: Tileset, layer: number[][]): MapLayerView {
const builder = new SpriteViewBuilder(r, tileset.texture);
const entityTiles: Array<[[number, number], EntityView]> = [];
for (let x = 0; x < layer[0].length; x++)
for (let y = 0; y < layer.length; y++)
addSprite(builder, entityTiles, tileset, x, y, layer[y][x]);
return new MapLayerView(r, builder.build(), entityTiles);
}
class MapLayerView { class MapLayerView {
public constructor( public constructor(
private r: Renderer, private r: Renderer,
private staticTiles: SpriteView, private staticTiles: SpriteView,
private entityTiles: Array<[[number, number], EntityView]>, private entityTiles: Array<[[number, number], EntityView]>,
) { ) {}
}
public render(time: number): void { public render(time: number): void {
this.r.setTranslation([0, 0]); this.r.setTranslation([0, 0]);
@ -154,6 +145,16 @@ class MapLayerView {
} }
} }
function buildMapLayer(r: Renderer, tileset: Tileset, layer: number[][]): MapLayerView {
const builder = new SpriteViewBuilder(r, tileset.texture);
const entityTiles: Array<[[number, number], EntityView]> = [];
for (let x = 0; x < layer[0].length; x++)
for (let y = 0; y < layer.length; y++) addSprite(builder, entityTiles, tileset, x, y, layer[y][x]);
return new MapLayerView(r, builder.build(), entityTiles);
}
export class MapView { export class MapView {
public static async load(r: Renderer, map: MapData): Promise<MapView> { public static async load(r: Renderer, map: MapData): Promise<MapView> {
const tiles = await loadTiles(r, map.tiles); const tiles = await loadTiles(r, map.tiles);
@ -163,11 +164,9 @@ export class MapView {
return new MapView(layers); return new MapView(layers);
} }
private constructor(private layers: MapLayerView[]) { private constructor(private layers: MapLayerView[]) {}
}
public render(time: number): void { public render(time: number): void {
for (const layer of this.layers) for (const layer of this.layers) layer.render(time);
layer.render(time);
} }
} }

View file

@ -28,8 +28,7 @@ export class Renderer {
public createBuffer(): WebGLBuffer { public createBuffer(): WebGLBuffer {
const ret = this.gl.createBuffer(); const ret = this.gl.createBuffer();
if (!ret) if (!ret) throw new Error('unable to create buffer');
throw new Error('unable to create buffer');
return ret; return ret;
} }
@ -50,11 +49,11 @@ export class Renderer {
return this.shaders.samplerLoc; return this.shaders.samplerLoc;
} }
public setCenter(v: vec2|number[]) { public setCenter(v: vec2 | number[]): void {
this.snapToGrid(this.center, v); this.snapToGrid(this.center, v);
} }
public setTranslation(v: vec2|number[]) { public setTranslation(v: vec2 | number[]): void {
vec2.sub(this.translation, v, this.center); vec2.sub(this.translation, v, this.center);
this.snapToGrid(this.translation, this.translation); this.snapToGrid(this.translation, this.translation);
this.gl.uniform2fv(this.shaders.translateLoc, this.translation); this.gl.uniform2fv(this.shaders.translateLoc, this.translation);
@ -66,7 +65,7 @@ export class Renderer {
this.setTranslation([0, 0]); this.setTranslation([0, 0]);
} }
public snapToGrid(out: vec2, a: vec2|number[]): void { public snapToGrid(out: vec2, a: vec2 | number[]): void {
vec2.scale(out, a, this.coordScale); vec2.scale(out, a, this.coordScale);
vec2.round(out, out); vec2.round(out, out);
vec2.scale(out, out, 1 / this.coordScale); vec2.scale(out, out, 1 / this.coordScale);
@ -84,14 +83,13 @@ export class Renderer {
const scale = this.viewScale * this.coordScale; const scale = this.viewScale * this.coordScale;
mat4.identity(this.viewport); mat4.identity(this.viewport);
mat4.scale(this.viewport, this.viewport, [2 * scale / ws, -2 * scale / hs, 1.0]); mat4.scale(this.viewport, this.viewport, [(2 * scale) / ws, (-2 * scale) / hs, 1.0]);
this.gl.uniformMatrix4fv(this.shaders.viewportLoc, false, this.viewport); this.gl.uniformMatrix4fv(this.shaders.viewportLoc, false, this.viewport);
} }
private mkContext(): WebGLRenderingContext { private mkContext(): WebGLRenderingContext {
const gl = this.canvas.getContext('webgl'); const gl = this.canvas.getContext('webgl');
if (!gl) if (!gl) throw new Error('unable to initialize WebGL context');
throw new Error('unable to initialize WebGL context');
return gl; return gl;
} }

View file

@ -11,8 +11,7 @@ export class Shaders {
constructor(private readonly gl: WebGLRenderingContext) { constructor(private readonly gl: WebGLRenderingContext) {
const shaderProgram = this.gl.createProgram(); const shaderProgram = this.gl.createProgram();
if (!shaderProgram) if (!shaderProgram) throw new Error('Unable to create shader program');
throw new Error('Unable to create shader program');
const vertexShader = this.compileShader(this.gl.VERTEX_SHADER, vertexShaderSrc); const vertexShader = this.compileShader(this.gl.VERTEX_SHADER, vertexShaderSrc);
const fragmentShader = this.compileShader(this.gl.FRAGMENT_SHADER, fragmentShaderSrc); const fragmentShader = this.compileShader(this.gl.FRAGMENT_SHADER, fragmentShaderSrc);
@ -46,8 +45,7 @@ export class Shaders {
private compileShader(type: number, src: string): WebGLShader { private compileShader(type: number, src: string): WebGLShader {
const shader = this.gl.createShader(type); const shader = this.gl.createShader(type);
if (!shader) if (!shader) throw new Error('Unable to create shader');
throw new Error('Unable to create shader');
this.gl.shaderSource(shader, src); this.gl.shaderSource(shader, src);
this.gl.compileShader(shader); this.gl.compileShader(shader);
@ -63,16 +61,14 @@ export class Shaders {
private getAttribLocation(program: WebGLProgram, name: string): number { private getAttribLocation(program: WebGLProgram, name: string): number {
const ret = this.gl.getAttribLocation(program, name); const ret = this.gl.getAttribLocation(program, name);
if (ret < 0) if (ret < 0) throw new Error("unable to get location of attribute '" + name + "'");
throw new Error("unable to get location of attribute '" + name + "'");
return ret; return ret;
} }
private getUniformLocation(program: WebGLProgram, name: string): WebGLUniformLocation { private getUniformLocation(program: WebGLProgram, name: string): WebGLUniformLocation {
const ret = this.gl.getUniformLocation(program, name); const ret = this.gl.getUniformLocation(program, name);
if (!ret) if (!ret) throw new Error("unable to get location of uniform '" + name + "'");
throw new Error("unable to get location of uniform '" + name + "'");
return ret; return ret;
} }

View file

@ -2,34 +2,6 @@ import { Renderer } from './renderer/renderer';
export type SpriteCoords = [number, number, number, number]; export type SpriteCoords = [number, number, number, number];
export class SpriteViewBuilder {
private static pushSprite(buf: number[], coords: SpriteCoords): void {
const [x1, y1, x2, y2] = coords;
buf.push(x1); buf.push(y1);
buf.push(x2); buf.push(y1);
buf.push(x1); buf.push(y2);
buf.push(x1); buf.push(y2);
buf.push(x2); buf.push(y1);
buf.push(x2); buf.push(y2);
}
private readonly vertexData: number[] = [];
private readonly textureData: number[] = [];
constructor(private readonly r: Renderer, private readonly texture: WebGLTexture) {}
public addSprite(vertexCoords: SpriteCoords, texCoords: SpriteCoords): void {
SpriteViewBuilder.pushSprite(this.vertexData, vertexCoords);
SpriteViewBuilder.pushSprite(this.textureData, texCoords);
}
public build(): SpriteView {
return new SpriteView(this.r, this.texture, this.vertexData, this.textureData);
}
}
export class SpriteView { export class SpriteView {
private readonly vertexCount: number; private readonly vertexCount: number;
private readonly vertexBuffer: WebGLBuffer; private readonly vertexBuffer: WebGLBuffer;
@ -70,3 +42,37 @@ export class SpriteView {
gl.drawArrays(gl.TRIANGLES, 0, this.vertexCount); gl.drawArrays(gl.TRIANGLES, 0, this.vertexCount);
} }
} }
export class SpriteViewBuilder {
private static pushSprite(buf: number[], coords: SpriteCoords): void {
const [x1, y1, x2, y2] = coords;
buf.push(x1);
buf.push(y1);
buf.push(x2);
buf.push(y1);
buf.push(x1);
buf.push(y2);
buf.push(x1);
buf.push(y2);
buf.push(x2);
buf.push(y1);
buf.push(x2);
buf.push(y2);
}
private readonly vertexData: number[] = [];
private readonly textureData: number[] = [];
constructor(private readonly r: Renderer, private readonly texture: WebGLTexture) {}
public addSprite(vertexCoords: SpriteCoords, texCoords: SpriteCoords): void {
SpriteViewBuilder.pushSprite(this.vertexData, vertexCoords);
SpriteViewBuilder.pushSprite(this.textureData, texCoords);
}
public build(): SpriteView {
return new SpriteView(this.r, this.texture, this.vertexData, this.textureData);
}
}

View file

@ -3,20 +3,20 @@ import { Renderer } from '../renderer/renderer';
export function loadImage(url: string): Promise<HTMLImageElement> { export function loadImage(url: string): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const img = new Image(); const img = new Image();
img.addEventListener('load', () => { resolve(img); }); img.addEventListener('load', () => {
img.addEventListener('error', () => { reject(new Error('failed to load ' + url)); }); resolve(img);
});
img.addEventListener('error', () => {
reject(new Error('failed to load ' + url));
});
img.src = url; img.src = url;
}); });
} }
export function mkTexture( export function mkTexture(r: Renderer, src: HTMLCanvasElement | HTMLImageElement): [WebGLTexture, [number, number]] {
r: Renderer,
src: HTMLCanvasElement|HTMLImageElement,
): [WebGLTexture, [number, number]] {
const gl = r.getContext(); const gl = r.getContext();
const texture = gl.createTexture(); const texture = gl.createTexture();
if (!texture) if (!texture) throw new Error('unable to create texture');
throw new Error('unable to create texture');
gl.bindTexture(gl.TEXTURE_2D, texture); gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, src); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, src);
@ -25,9 +25,7 @@ export function mkTexture(
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
const size: [number, number] = [ const size: [number, number] = [src.width / r.coordScale, src.height / r.coordScale];
src.width / r.coordScale, src.height / r.coordScale,
];
return [texture, size]; return [texture, size];
} }