Compare commits
No commits in common. "main" and "old/canvas2d" have entirely different histories.
main
...
old/canvas
30
.eslintrc.js
|
@ -1,30 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
parser: '@typescript-eslint/parser',
|
|
||||||
plugins: [
|
|
||||||
'react-hooks',
|
|
||||||
],
|
|
||||||
extends: [
|
|
||||||
'plugin:react/recommended',
|
|
||||||
'plugin:@typescript-eslint/recommended',
|
|
||||||
'prettier/@typescript-eslint',
|
|
||||||
'plugin:prettier/recommended',
|
|
||||||
],
|
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: 2018,
|
|
||||||
sourceType: 'module',
|
|
||||||
ecmaFeatures: {
|
|
||||||
jsx: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
'react/prop-types': [0],
|
|
||||||
'react-hooks/rules-of-hooks': 'error',
|
|
||||||
'react-hooks/exhaustive-deps': 'warn',
|
|
||||||
'prettier/prettier': 'warn',
|
|
||||||
},
|
|
||||||
settings: {
|
|
||||||
react: {
|
|
||||||
version: 'detect',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
3
.gitignore
vendored
|
@ -1,2 +1,3 @@
|
||||||
|
/build
|
||||||
/node_modules
|
/node_modules
|
||||||
/.webpack
|
/typings
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
entry: './src/main/index.ts',
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
test: /\.ts$/,
|
|
||||||
use: 'ts-loader',
|
|
||||||
exclude: /node_modules/,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
resolve: {
|
|
||||||
extensions: ['.ts', '.js'],
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1,30 +0,0 @@
|
||||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
|
||||||
const CopyPlugin = require('copy-webpack-plugin');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
test: /\.tsx?$/,
|
|
||||||
use: 'ts-loader',
|
|
||||||
exclude: /node_modules/,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.css$/,
|
|
||||||
use: [{ loader: 'style-loader' }, { loader: 'css-loader' }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.(woff|woff2|eot|ttf|otf)$/,
|
|
||||||
use: 'file-loader',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.(vs|fs)$/,
|
|
||||||
use: 'raw-loader',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
plugins: [new CopyPlugin([{ from: 'static' }])],
|
|
||||||
resolve: {
|
|
||||||
extensions: ['.ts', '.js', '.tsx', '.jsx'],
|
|
||||||
},
|
|
||||||
};
|
|
69
package.json
|
@ -1,67 +1,16 @@
|
||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
|
||||||
"main": ".webpack/main",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "electron-forge start",
|
"start": "webpack-dev-server -d --content-base static",
|
||||||
"lint": "eslint 'src/**/*.ts' 'src/**/*.tsx'"
|
"build": "webpack"
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"forge": {
|
|
||||||
"plugins": [
|
|
||||||
[
|
|
||||||
"@electron-forge/plugin-webpack",
|
|
||||||
{
|
|
||||||
"mainConfig": "./build/webpack.main.js",
|
|
||||||
"renderer": {
|
|
||||||
"config": "./build/webpack.renderer.js",
|
|
||||||
"entryPoints": [
|
|
||||||
{
|
|
||||||
"html": "./src/renderer/index.html",
|
|
||||||
"js": "./src/renderer/index.tsx",
|
|
||||||
"name": "main_window"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@electron-forge/cli": "^6.0.0-beta.50",
|
|
||||||
"@electron-forge/plugin-webpack": "^6.0.0-beta.50",
|
|
||||||
"@types/color": "^3.0.1",
|
|
||||||
"@types/electron-devtools-installer": "^2.2.0",
|
|
||||||
"@types/react": "^16.9.25",
|
|
||||||
"@types/react-dom": "^16.9.5",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^2.24.0",
|
|
||||||
"@typescript-eslint/parser": "^2.24.0",
|
|
||||||
"copy-webpack-plugin": "^5.1.1",
|
|
||||||
"css-loader": "^3.4.2",
|
|
||||||
"electron": "^8.1.1",
|
|
||||||
"electron-devtools-installer": "^2.2.4",
|
|
||||||
"eslint": "^6.8.0",
|
|
||||||
"eslint-config-prettier": "^6.10.0",
|
|
||||||
"eslint-plugin-prettier": "^3.1.2",
|
|
||||||
"eslint-plugin-react": "^7.19.0",
|
|
||||||
"eslint-plugin-react-hooks": "^2.5.1",
|
|
||||||
"file-loader": "^6.0.0",
|
|
||||||
"prettier": "^2.0.1",
|
|
||||||
"raw-loader": "^4.0.0",
|
|
||||||
"style-loader": "^1.1.3",
|
|
||||||
"ts-loader": "^6.2.1",
|
|
||||||
"typescript": "^3.8.3",
|
|
||||||
"webpack": "^4.42.0"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@material-ui/core": "^4.9.7",
|
"css-loader": "^0.23.1",
|
||||||
"color": "^3.1.2",
|
"lodash": "^3.10.1",
|
||||||
"fast-glob": "^3.2.2",
|
"style-loader": "^0.13.1",
|
||||||
"gl-matrix": "^3.2.1",
|
"ts-loader": "^0.7.2",
|
||||||
"immutable": "^4.0.0-rc.12",
|
"typescript": "^1.8.10",
|
||||||
"react": "^16.13.1",
|
"webpack": "^1.13.1",
|
||||||
"react-dom": "^16.13.1",
|
"webpack-dev-server": "^1.14.1"
|
||||||
"typeface-roboto": "^0.0.75"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
semi: true,
|
|
||||||
trailingComma: 'all',
|
|
||||||
arrowParens: 'always',
|
|
||||||
singleQuote: true,
|
|
||||||
jsxSingleQuote: true,
|
|
||||||
printWidth: 120,
|
|
||||||
useTabs: true,
|
|
||||||
tabWidth: 8,
|
|
||||||
};
|
|
Before Width: | Height: | Size: 1 KiB |
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"name": "Dirt",
|
|
||||||
"width": 1,
|
|
||||||
"height": 1
|
|
||||||
}
|
|
Before Width: | Height: | Size: 1.3 KiB |
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"name": "Grass",
|
|
||||||
"width": 1,
|
|
||||||
"height": 1
|
|
||||||
}
|
|
5
require.d.ts
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
declare var require: {
|
||||||
|
<T>(path: string): T;
|
||||||
|
(paths: string[], callback: (...modules: any[]) => void): void;
|
||||||
|
ensure: (paths: string[], callback: (require: <T>(path: string) => T) => void) => void;
|
||||||
|
};
|
38
src/app.ts
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
|
||||||
|
require('./style.css');
|
||||||
|
|
||||||
|
|
||||||
|
import * as lodash from 'lodash';
|
||||||
|
_ = lodash;
|
||||||
|
|
||||||
|
import MapContext from './control/MapContext';
|
||||||
|
import MapData from './model/MapData';
|
||||||
|
import InputHandler from './view/InputHandler';
|
||||||
|
|
||||||
|
import * as util from './util';
|
||||||
|
|
||||||
|
|
||||||
|
var relevantKeys = [
|
||||||
|
InputHandler.Up,
|
||||||
|
InputHandler.Right,
|
||||||
|
InputHandler.Down,
|
||||||
|
InputHandler.Left,
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
var mapContext: MapContext;
|
||||||
|
|
||||||
|
window.onload = () => {
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
|
||||||
|
xhr.onload = function() {
|
||||||
|
var mapDef = new MapData(JSON.parse(this.responseText));
|
||||||
|
var inputHandler = new InputHandler(util.arrayToObject(relevantKeys));
|
||||||
|
mapContext = new MapContext(mapDef, inputHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
xhr.open('GET', 'resources/map/test.json', true);
|
||||||
|
xhr.send();
|
||||||
|
};
|
138
src/control/MapContext.ts
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
|
||||||
|
import Direction from '../model/Direction';
|
||||||
|
import Entity from '../model/Entity';
|
||||||
|
import EntityPosition from '../model/EntityPosition';
|
||||||
|
import MapData from '../model/MapData';
|
||||||
|
import Position from '../model/Position';
|
||||||
|
import Transition from '../model/Transition';
|
||||||
|
|
||||||
|
import InputHandler from '../view/InputHandler';
|
||||||
|
import MapView from '../view/MapView';
|
||||||
|
|
||||||
|
|
||||||
|
export default class MapContext {
|
||||||
|
private view: MapView;
|
||||||
|
|
||||||
|
private entities: EntityPosition[] = [];
|
||||||
|
private playerEntity: EntityPosition;
|
||||||
|
|
||||||
|
private collision: number[][];
|
||||||
|
|
||||||
|
|
||||||
|
constructor(private map: MapData, private inputHandler: InputHandler) {
|
||||||
|
this.collision = new Array(map.width);
|
||||||
|
for (let i = 0; i < map.width; i++) {
|
||||||
|
this.collision[i] = new Array(map.height);
|
||||||
|
|
||||||
|
for (let j = 0; j < map.height; j++)
|
||||||
|
this.collision[i][j] = map.collision[j][i] == '0' ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.playerEntity = new EntityPosition(
|
||||||
|
new Entity('square'),
|
||||||
|
new Position(8, 8),
|
||||||
|
Direction.East
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addEntity(this.playerEntity);
|
||||||
|
|
||||||
|
this.addEntity(new EntityPosition(
|
||||||
|
new Entity('square'),
|
||||||
|
new Position(10, 10),
|
||||||
|
Direction.East
|
||||||
|
));
|
||||||
|
|
||||||
|
this.view = new MapView(
|
||||||
|
map,
|
||||||
|
this.entities,
|
||||||
|
this.playerEntity,
|
||||||
|
(time: number) => this.updateState(time)
|
||||||
|
);
|
||||||
|
|
||||||
|
inputHandler.addListener(() => {
|
||||||
|
if (this.updateState(performance.now()))
|
||||||
|
this.view.redraw();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private inMap(p: Position): boolean {
|
||||||
|
return p.x >= 0 && p.x < this.map.width && p.y >= 0 && p.y < this.map.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
private incCollision(p: Position): void {
|
||||||
|
if (this.inMap(p))
|
||||||
|
this.collision[p.x][p.y]++;
|
||||||
|
}
|
||||||
|
|
||||||
|
private decCollision(p: Position): void {
|
||||||
|
if (this.inMap(p))
|
||||||
|
this.collision[p.x][p.y]--;
|
||||||
|
}
|
||||||
|
|
||||||
|
private collides(p: Position): boolean {
|
||||||
|
return (!this.inMap(p)) || (this.collision[p.x][p.y] > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private addEntity(entity: EntityPosition): void {
|
||||||
|
this.entities.push(entity);
|
||||||
|
|
||||||
|
this.incCollision(entity.position);
|
||||||
|
}
|
||||||
|
|
||||||
|
private addTransition(entity: EntityPosition, dest: Position, start: number, dur: number): void {
|
||||||
|
entity.transition = new Transition(start, start+dur, entity.position, dest);
|
||||||
|
|
||||||
|
this.incCollision(dest);
|
||||||
|
}
|
||||||
|
|
||||||
|
private finishTransition(entity: EntityPosition): void {
|
||||||
|
this.decCollision(entity.position);
|
||||||
|
|
||||||
|
entity.position = entity.transition.dest;
|
||||||
|
entity.transition = null;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateState(time: number): boolean {
|
||||||
|
var ret = false;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
let origTime = time;
|
||||||
|
|
||||||
|
if (this.playerEntity.transition && this.playerEntity.transition.end <= time) {
|
||||||
|
origTime = this.playerEntity.transition.end;
|
||||||
|
this.finishTransition(this.playerEntity);
|
||||||
|
ret = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.playerEntity.transition)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
let dir: Direction = null;
|
||||||
|
|
||||||
|
if (this.inputHandler.keys[InputHandler.Up])
|
||||||
|
dir = Direction.North;
|
||||||
|
else if (this.inputHandler.keys[InputHandler.Right])
|
||||||
|
dir = Direction.East;
|
||||||
|
else if (this.inputHandler.keys[InputHandler.Down])
|
||||||
|
dir = Direction.South;
|
||||||
|
else if (this.inputHandler.keys[InputHandler.Left])
|
||||||
|
dir = Direction.West;
|
||||||
|
|
||||||
|
if (dir === null)
|
||||||
|
return ret;
|
||||||
|
|
||||||
|
this.playerEntity.direction = dir;
|
||||||
|
ret = true;
|
||||||
|
|
||||||
|
let dest = this.playerEntity.position.translate(dir, 1);
|
||||||
|
if (this.collides(dest))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
this.addTransition(this.playerEntity, dest, origTime, 250);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,55 +0,0 @@
|
||||||
import { app, BrowserWindow } from 'electron';
|
|
||||||
|
|
||||||
declare const MAIN_WINDOW_WEBPACK_ENTRY: string;
|
|
||||||
|
|
||||||
const isDevelopment = process.env.NODE_ENV !== 'production';
|
|
||||||
|
|
||||||
app.allowRendererProcessReuse = true;
|
|
||||||
|
|
||||||
async function installReactDevTools(): Promise<void> {
|
|
||||||
const { default: installExtension, REACT_DEVELOPER_TOOLS } = await import('electron-devtools-installer');
|
|
||||||
const name = await installExtension(REACT_DEVELOPER_TOOLS);
|
|
||||||
console.log(`Added Extension: ${name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createWindow(): void {
|
|
||||||
const window = new BrowserWindow({ webPreferences: { nodeIntegration: true } });
|
|
||||||
|
|
||||||
window.loadURL(MAIN_WINDOW_WEBPACK_ENTRY);
|
|
||||||
|
|
||||||
if (isDevelopment) {
|
|
||||||
window.webContents.openDevTools();
|
|
||||||
}
|
|
||||||
|
|
||||||
window.webContents.on('devtools-opened', () => {
|
|
||||||
window.webContents.focus();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// quit application when all windows are closed
|
|
||||||
app.on('window-all-closed', () => {
|
|
||||||
// On macOS it is common for applications and their menu bar
|
|
||||||
// to stay active until the user quits explicitly with Cmd + Q
|
|
||||||
if (process.platform !== 'darwin') {
|
|
||||||
app.quit();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.on('activate', () => {
|
|
||||||
// On macOS it's common to re-create a window in the app when the
|
|
||||||
// dock icon is clicked and there are no other windows open.
|
|
||||||
if (BrowserWindow.getAllWindows().length === 0) {
|
|
||||||
createWindow();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
async function initialize(): Promise<void> {
|
|
||||||
if (isDevelopment) {
|
|
||||||
await installReactDevTools();
|
|
||||||
}
|
|
||||||
|
|
||||||
createWindow();
|
|
||||||
}
|
|
||||||
|
|
||||||
// create main BrowserWindow when electron is ready
|
|
||||||
app.on('ready', initialize);
|
|
15
src/model/Direction.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
|
||||||
|
export enum Direction {
|
||||||
|
North,
|
||||||
|
East,
|
||||||
|
South,
|
||||||
|
West
|
||||||
|
};
|
||||||
|
|
||||||
|
export module Direction {
|
||||||
|
export function reverse(r: Direction): Direction { return (r+2) % 4; }
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Direction;
|
6
src/model/Entity.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
|
||||||
|
export default class Entity {
|
||||||
|
constructor(public name: string) {}
|
||||||
|
}
|
14
src/model/EntityPosition.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
|
||||||
|
import Direction from './Direction';
|
||||||
|
import Entity from './Entity';
|
||||||
|
import Position from './Position';
|
||||||
|
import Transition from './Transition';
|
||||||
|
|
||||||
|
|
||||||
|
export default class EntityPosition {
|
||||||
|
transition: Transition = null;
|
||||||
|
|
||||||
|
constructor(public entity: Entity, public position: Position, public direction: Direction) {}
|
||||||
|
}
|
29
src/model/MapData.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
|
||||||
|
import TileData from './TileData';
|
||||||
|
|
||||||
|
|
||||||
|
interface Input {
|
||||||
|
tiles: {[key: string]: TileData};
|
||||||
|
collision: string[];
|
||||||
|
layers: string[][][];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class MapData {
|
||||||
|
tiles: {[key: string]: TileData};
|
||||||
|
collision: string[];
|
||||||
|
layers: string[][][];
|
||||||
|
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
|
||||||
|
constructor(data: Input) {
|
||||||
|
this.tiles = data.tiles;
|
||||||
|
this.collision = data.collision;
|
||||||
|
this.layers = data.layers;
|
||||||
|
|
||||||
|
this.height = this.collision.length;
|
||||||
|
this.width = this.collision[0].length;
|
||||||
|
}
|
||||||
|
}
|
32
src/model/Position.ts
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
|
||||||
|
import Direction from './Direction';
|
||||||
|
|
||||||
|
|
||||||
|
export default class Position {
|
||||||
|
constructor(public x: number, public y: number) {}
|
||||||
|
|
||||||
|
translate(dir: Direction, amount: number): Position {
|
||||||
|
var p = new Position(this.x, this.y);
|
||||||
|
|
||||||
|
switch (dir) {
|
||||||
|
case Direction.North:
|
||||||
|
p.y -= amount;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Direction.East:
|
||||||
|
p.x += amount;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Direction.South:
|
||||||
|
p.y += amount;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Direction.West:
|
||||||
|
p.x -= amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
}
|
7
src/model/TileData.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
|
||||||
|
export default class TileData {
|
||||||
|
file: string;
|
||||||
|
subtile: number;
|
||||||
|
}
|
8
src/model/Transition.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
|
||||||
|
import Position from './Position';
|
||||||
|
|
||||||
|
export default class Transition {
|
||||||
|
constructor(public start: number, public end: number, public orig: Position, public dest: Position) {}
|
||||||
|
};
|
16
src/renderer/custom.d.ts
vendored
|
@ -1,16 +0,0 @@
|
||||||
declare namespace NodeJS {
|
|
||||||
interface Module {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
hot: any;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module '*.vs' {
|
|
||||||
const content: string;
|
|
||||||
export default content;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module '*.fs' {
|
|
||||||
const content: string;
|
|
||||||
export default content;
|
|
||||||
}
|
|
|
@ -1,64 +0,0 @@
|
||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
import 'typeface-roboto';
|
|
||||||
|
|
||||||
import { createMuiTheme, makeStyles, Theme, ThemeProvider } from '@material-ui/core/styles';
|
|
||||||
|
|
||||||
import CssBaseline from '@material-ui/core/CssBaseline';
|
|
||||||
import Drawer from '@material-ui/core/Drawer';
|
|
||||||
|
|
||||||
import { Library } from './library';
|
|
||||||
|
|
||||||
const drawerWidth = 240;
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) => ({
|
|
||||||
root: {
|
|
||||||
display: 'flex',
|
|
||||||
},
|
|
||||||
drawer: {
|
|
||||||
width: drawerWidth,
|
|
||||||
flexShrink: 0,
|
|
||||||
},
|
|
||||||
drawerPaper: {
|
|
||||||
width: drawerWidth,
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
flexGrow: 1,
|
|
||||||
padding: theme.spacing(3),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
function EditorLayout(): JSX.Element {
|
|
||||||
const classes = useStyles();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classes.root}>
|
|
||||||
<CssBaseline />
|
|
||||||
<Drawer
|
|
||||||
className={classes.drawer}
|
|
||||||
variant='permanent'
|
|
||||||
classes={{
|
|
||||||
paper: classes.drawerPaper,
|
|
||||||
}}
|
|
||||||
anchor='left'
|
|
||||||
></Drawer>
|
|
||||||
<main className={classes.content}>
|
|
||||||
<Library />
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const theme = createMuiTheme({
|
|
||||||
palette: {
|
|
||||||
type: 'dark',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export function Editor(): JSX.Element {
|
|
||||||
return (
|
|
||||||
<ThemeProvider theme={theme}>
|
|
||||||
<EditorLayout />
|
|
||||||
</ThemeProvider>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,118 +0,0 @@
|
||||||
import * as React from 'react';
|
|
||||||
const { useCallback, useMemo } = React;
|
|
||||||
|
|
||||||
import { makeStyles, Theme } from '@material-ui/core/styles';
|
|
||||||
|
|
||||||
import Box from '@material-ui/core/Box';
|
|
||||||
import Button from '@material-ui/core/Button';
|
|
||||||
import Container from '@material-ui/core/Container';
|
|
||||||
|
|
||||||
import * as Color from 'color';
|
|
||||||
import * as glob from 'fast-glob';
|
|
||||||
|
|
||||||
import { Tiling, TilingMeta } from './types';
|
|
||||||
import { usePromise, useReadFile, readJSON } from './util';
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) => ({
|
|
||||||
grid: {
|
|
||||||
display: 'flex',
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
margin: theme.spacing(-0.5),
|
|
||||||
},
|
|
||||||
tile: {
|
|
||||||
imageRendering: 'pixelated',
|
|
||||||
borderStyle: 'solid',
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: theme.palette.divider,
|
|
||||||
background: theme.palette.background.paper,
|
|
||||||
position: 'relative',
|
|
||||||
width: 130,
|
|
||||||
height: 130,
|
|
||||||
margin: theme.spacing(0.5),
|
|
||||||
// cursor: 'pointer',
|
|
||||||
// '&:hover': {
|
|
||||||
// borderColor: theme.palette.text.secondary,
|
|
||||||
// boxShadow: `0 0 2px 1px ${theme.palette.text.secondary}`,
|
|
||||||
// },
|
|
||||||
},
|
|
||||||
img: {
|
|
||||||
position: 'absolute',
|
|
||||||
zIndex: 1,
|
|
||||||
width: 128,
|
|
||||||
height: 128,
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
position: 'absolute',
|
|
||||||
zIndex: 2,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: 0,
|
|
||||||
height: '37.5%',
|
|
||||||
padding: theme.spacing(1),
|
|
||||||
background: Color(theme.palette.background.default).fade(0.3).string(),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
function tilingSprite(tiling: Tiling): string {
|
|
||||||
const x = (tiling.meta.width - 1) / 2;
|
|
||||||
const y = (tiling.meta.height - 1) / 2;
|
|
||||||
return `project/tiling/${tiling.id}/${x}_${y}.png`;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TilingDisplayProps {
|
|
||||||
tiling: Tiling;
|
|
||||||
}
|
|
||||||
|
|
||||||
function TilingDisplay({ tiling }: TilingDisplayProps): JSX.Element | null {
|
|
||||||
const classes = useStyles();
|
|
||||||
|
|
||||||
const path = tilingSprite(tiling);
|
|
||||||
const image = useReadFile(path);
|
|
||||||
const src = useMemo(() => (image ? `data:image/png;base64,${image.toString('base64')}` : undefined), [image]);
|
|
||||||
return (
|
|
||||||
<div className={classes.tile}>
|
|
||||||
<img className={classes.img} src={src} />
|
|
||||||
<div className={classes.label}>{tiling.meta.name}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function listTilings(): Promise<string[]> {
|
|
||||||
const matches = await glob('project/tiling/*/meta.json');
|
|
||||||
return matches.map((m) => m.split('/')[2]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadTilingMeta(id: string): Promise<TilingMeta> {
|
|
||||||
const path = `project/tiling/${id}/meta.json`;
|
|
||||||
const meta = await readJSON(path);
|
|
||||||
return meta as TilingMeta;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadTilings(): Promise<Tiling[]> {
|
|
||||||
const tilings = await listTilings();
|
|
||||||
return Promise.all(
|
|
||||||
tilings.map((id) => [id, loadTilingMeta(id)] as const).map(async ([id, p]) => ({ id, meta: await p })),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Library(): JSX.Element {
|
|
||||||
const classes = useStyles();
|
|
||||||
|
|
||||||
const mkLoadTilings = useCallback(() => loadTilings(), []);
|
|
||||||
const tilings = usePromise(mkLoadTilings) ?? [];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container>
|
|
||||||
<Box mb={2}>
|
|
||||||
<div className={classes.grid}>
|
|
||||||
{tilings.map((tiling, i) => (
|
|
||||||
<TilingDisplay key={i} tiling={tiling} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Box>
|
|
||||||
<Button variant='contained' color='primary'>
|
|
||||||
Add
|
|
||||||
</Button>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
export interface TilingMeta {
|
|
||||||
name: string;
|
|
||||||
desc?: string;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Tiling {
|
|
||||||
id: string;
|
|
||||||
meta: TilingMeta;
|
|
||||||
}
|
|
|
@ -1,36 +0,0 @@
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import * as fs from 'fs';
|
|
||||||
|
|
||||||
export function usePromise<T>(f: () => Promise<T>): T | null {
|
|
||||||
const [value, setValue] = useState<T | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setValue(null);
|
|
||||||
|
|
||||||
let cancelled = false;
|
|
||||||
|
|
||||||
(async (): Promise<void> => {
|
|
||||||
const v = await f();
|
|
||||||
if (!cancelled) {
|
|
||||||
setValue(v);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
return (): void => {
|
|
||||||
cancelled = true;
|
|
||||||
};
|
|
||||||
}, [f]);
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useReadFile(path: string): Buffer | null {
|
|
||||||
const readFile = useCallback(() => fs.promises.readFile(path), [path]);
|
|
||||||
return usePromise(readFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function readJSON(path: string): Promise<unknown> {
|
|
||||||
const content = await fs.promises.readFile(path, 'utf8');
|
|
||||||
return JSON.parse(content);
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />
|
|
||||||
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width" />
|
|
||||||
<title>RPGedit</title>
|
|
||||||
<style>
|
|
||||||
html, body, #app {
|
|
||||||
height: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="app"></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,8 +0,0 @@
|
||||||
import * as React from 'react';
|
|
||||||
import * as ReactDOM from 'react-dom';
|
|
||||||
|
|
||||||
import { Editor } from './editor/editor';
|
|
||||||
|
|
||||||
ReactDOM.render(<Editor />, document.querySelector('#app'));
|
|
||||||
|
|
||||||
module.hot?.accept();
|
|
|
@ -1,60 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
|
@ -1,43 +0,0 @@
|
||||||
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): void {
|
|
||||||
this.renderer.setTranslation(this.pos);
|
|
||||||
this.view.renderByTime(time);
|
|
||||||
}
|
|
||||||
|
|
||||||
public getTranslation(): vec2 {
|
|
||||||
return this.pos;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getCollidables(): Collidable[] {
|
|
||||||
return this.collision;
|
|
||||||
}
|
|
||||||
|
|
||||||
public interact(): void {
|
|
||||||
alert(`You've interacted with ${this.name}!`);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,163 +0,0 @@
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
html, body, div {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
margin: 0px;
|
|
||||||
padding: 0px;
|
|
||||||
border: 0px;
|
|
||||||
background: #223;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
import { Collidable } from './collision';
|
|
||||||
import { Movement } from './line';
|
|
||||||
import { Point } from './point';
|
|
||||||
|
|
||||||
import { vec2 } from 'gl-matrix';
|
|
||||||
|
|
||||||
export class Circle implements Collidable, Point {
|
|
||||||
private pointCollide = Point.prototype.collide;
|
|
||||||
|
|
||||||
constructor(public readonly p: vec2, public readonly r: number) {}
|
|
||||||
|
|
||||||
public collide(out: vec2, move: Movement, r: number): boolean {
|
|
||||||
return this.pointCollide(out, move, r + this.r);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
import { Movement } from './line';
|
|
||||||
|
|
||||||
import { vec2 } from 'gl-matrix';
|
|
||||||
|
|
||||||
export interface Collidable {
|
|
||||||
collide(out: vec2, move: Movement, r: number): boolean;
|
|
||||||
}
|
|
|
@ -1,124 +0,0 @@
|
||||||
import { mat2, vec2 } from 'gl-matrix';
|
|
||||||
import { Collidable } from './collision';
|
|
||||||
|
|
||||||
const rot90 = mat2.fromValues(0, 1, -1, 0);
|
|
||||||
|
|
||||||
export function normal(out: vec2, a: vec2): vec2 {
|
|
||||||
return vec2.transformMat2(out, a, rot90);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function crossz(a: vec2, b: vec2): number {
|
|
||||||
return a[0] * b[1] - a[1] * b[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Line {
|
|
||||||
constructor(public readonly p: vec2, public readonly v: vec2) {}
|
|
||||||
|
|
||||||
public getNormal(out: vec2): vec2 {
|
|
||||||
return normal(out, this.v);
|
|
||||||
}
|
|
||||||
|
|
||||||
public projectPointDistance(p2: vec2): number {
|
|
||||||
const v2 = vec2.sub(vec2.create(), p2, this.p);
|
|
||||||
return vec2.dot(this.v, v2);
|
|
||||||
}
|
|
||||||
|
|
||||||
public projectPoint(out: vec2, p2: vec2): vec2 {
|
|
||||||
const d = this.projectPointDistance(p2);
|
|
||||||
return vec2.scaleAndAdd(out, this.p, this.v, d);
|
|
||||||
}
|
|
||||||
|
|
||||||
public distancePoint(p2: vec2): number {
|
|
||||||
const v2 = vec2.sub(vec2.create(), p2, this.p);
|
|
||||||
return crossz(this.v, v2);
|
|
||||||
}
|
|
||||||
|
|
||||||
public intersectLine(out: vec2, l2: Line): vec2 {
|
|
||||||
const vp = vec2.sub(vec2.create(), l2.p, this.p);
|
|
||||||
const d = crossz(vp, this.v);
|
|
||||||
const d2 = d / crossz(this.v, l2.v);
|
|
||||||
return vec2.scaleAndAdd(out, l2.p, l2.v, d2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Movement {
|
|
||||||
public readonly v: vec2;
|
|
||||||
|
|
||||||
constructor(public readonly src: vec2, public readonly dest: vec2) {
|
|
||||||
this.v = vec2.sub(vec2.create(), dest, src);
|
|
||||||
}
|
|
||||||
|
|
||||||
public intersectLine(out: vec2, l: Line): vec2 {
|
|
||||||
const vp = vec2.sub(vec2.create(), l.p, this.src);
|
|
||||||
const d = crossz(vp, this.v);
|
|
||||||
const d2 = d / crossz(this.v, l.v);
|
|
||||||
return vec2.scaleAndAdd(out, l.p, l.v, d2);
|
|
||||||
}
|
|
||||||
|
|
||||||
public passes(p: vec2): boolean {
|
|
||||||
const vp = vec2.sub(vec2.create(), p, this.src);
|
|
||||||
const d = vec2.dot(this.v, vp);
|
|
||||||
return d >= 0 && d <= vec2.sqrLen(this.v);
|
|
||||||
}
|
|
||||||
|
|
||||||
public toLineSegment(): LineSegment {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
|
||||||
return LineSegment.fromPoints(this.src, this.dest);
|
|
||||||
}
|
|
||||||
|
|
||||||
public translate(t: vec2): Movement {
|
|
||||||
const src = vec2.add(vec2.create(), this.src, t);
|
|
||||||
const dest = vec2.add(vec2.create(), this.dest, t);
|
|
||||||
return new Movement(src, dest);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class LineSegment extends Line implements Collidable {
|
|
||||||
public static fromPoints(p1: vec2, p2: vec2): LineSegment {
|
|
||||||
const d = vec2.dist(p1, p2);
|
|
||||||
const v = vec2.sub(vec2.create(), p2, p1);
|
|
||||||
vec2.scale(v, v, 1 / d);
|
|
||||||
|
|
||||||
return new LineSegment(p1, v, d);
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(p: vec2, v: vec2, public readonly l: number) {
|
|
||||||
super(p, v);
|
|
||||||
}
|
|
||||||
|
|
||||||
public getP2(out: vec2): vec2 {
|
|
||||||
return vec2.scaleAndAdd(out, this.p, this.v, this.l);
|
|
||||||
}
|
|
||||||
|
|
||||||
public containsPoint(p2: vec2): boolean {
|
|
||||||
const d = this.projectPointDistance(p2);
|
|
||||||
return d >= 0 && d <= this.l;
|
|
||||||
}
|
|
||||||
|
|
||||||
public collide(out: vec2, move: Movement, r: number): boolean {
|
|
||||||
if (this.distancePoint(move.src) < 0) return false;
|
|
||||||
|
|
||||||
if (crossz(move.v, this.v) < 0) return false;
|
|
||||||
|
|
||||||
const t = this.getNormal(vec2.create());
|
|
||||||
vec2.scale(t, t, -r);
|
|
||||||
|
|
||||||
const refMove = move.translate(t);
|
|
||||||
|
|
||||||
if (!this.collideRef(out, refMove)) return false;
|
|
||||||
|
|
||||||
vec2.sub(out, out, t);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private collideRef(out: vec2, move: Movement): boolean {
|
|
||||||
if (this.distancePoint(move.dest) >= 0) return false;
|
|
||||||
|
|
||||||
const x = move.intersectLine(vec2.create(), this);
|
|
||||||
if (!this.containsPoint(x)) return false;
|
|
||||||
|
|
||||||
this.projectPoint(out, move.dest);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
import { Collidable } from './collision';
|
|
||||||
import { Line, Movement, normal } from './line';
|
|
||||||
|
|
||||||
import { vec2 } from 'gl-matrix';
|
|
||||||
|
|
||||||
export class Point implements Collidable {
|
|
||||||
constructor(public readonly p: vec2) {}
|
|
||||||
|
|
||||||
public collide(out: vec2, move: Movement, r: number): boolean {
|
|
||||||
const moveLine = move.toLineSegment();
|
|
||||||
|
|
||||||
if (moveLine.projectPointDistance(this.p) < 0) return false;
|
|
||||||
|
|
||||||
const d = moveLine.distancePoint(this.p) / r;
|
|
||||||
if (Math.abs(d) >= 1) return false;
|
|
||||||
|
|
||||||
const e = Math.sqrt(1 - d * d);
|
|
||||||
|
|
||||||
const t = moveLine.getNormal(vec2.create());
|
|
||||||
vec2.scale(t, t, d);
|
|
||||||
vec2.scaleAndAdd(t, t, moveLine.v, e);
|
|
||||||
|
|
||||||
const tr = vec2.scale(vec2.create(), t, r);
|
|
||||||
|
|
||||||
const refMove = move.translate(tr);
|
|
||||||
|
|
||||||
if (vec2.sqrDist(this.p, move.src) > r * r && !refMove.passes(this.p)) return false;
|
|
||||||
|
|
||||||
normal(t, t);
|
|
||||||
|
|
||||||
const tang = new Line(this.p, t);
|
|
||||||
tang.projectPoint(out, refMove.dest);
|
|
||||||
vec2.sub(out, out, tr);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
export interface PolygonCollision {
|
|
||||||
readonly type: 'polygon';
|
|
||||||
readonly vertices: Array<[number, number]>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CircleCollision {
|
|
||||||
readonly type: 'circle';
|
|
||||||
readonly center: [number, number];
|
|
||||||
readonly radius: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Collision = PolygonCollision | CircleCollision;
|
|
|
@ -1,29 +0,0 @@
|
||||||
import { Collision } from './collision';
|
|
||||||
|
|
||||||
export interface EntityAnimation {
|
|
||||||
readonly sequence: ReadonlyArray<[number, number]>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EntityDataInput {
|
|
||||||
readonly sprite: string;
|
|
||||||
readonly anchor?: [number, number];
|
|
||||||
readonly collision?: Collision[];
|
|
||||||
readonly frames?: number;
|
|
||||||
readonly animation?: EntityAnimation;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class EntityData {
|
|
||||||
public readonly sprite: string;
|
|
||||||
public readonly anchor: [number, number];
|
|
||||||
public readonly collision: Collision[];
|
|
||||||
public readonly frames: number;
|
|
||||||
public readonly animation?: EntityAnimation;
|
|
||||||
|
|
||||||
constructor(input: EntityDataInput) {
|
|
||||||
this.sprite = input.sprite;
|
|
||||||
this.anchor = input.anchor || [0.5, 0.5];
|
|
||||||
this.collision = input.collision || [];
|
|
||||||
this.frames = input.frames || 1;
|
|
||||||
this.animation = input.animation;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
import { Collision } from './collision';
|
|
||||||
|
|
||||||
export interface MapLayer {
|
|
||||||
readonly tiles: number[][];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MapDataInput {
|
|
||||||
readonly tiles: string[];
|
|
||||||
readonly layers: MapLayer[];
|
|
||||||
readonly collision: Collision[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export class MapData {
|
|
||||||
public readonly tiles: string[];
|
|
||||||
public readonly layers: MapLayer[];
|
|
||||||
public readonly collision: Collision[];
|
|
||||||
|
|
||||||
public readonly width: number;
|
|
||||||
public readonly height: number;
|
|
||||||
|
|
||||||
constructor(data: MapDataInput) {
|
|
||||||
this.tiles = data.tiles;
|
|
||||||
this.layers = data.layers;
|
|
||||||
this.collision = data.collision;
|
|
||||||
|
|
||||||
this.height = this.layers[0].tiles.length;
|
|
||||||
this.width = this.layers[0].tiles[0].length;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,55 +0,0 @@
|
||||||
export function recordToMap<T>(r: Record<string, T>): Map<string, T> {
|
|
||||||
const ret = new Map();
|
|
||||||
|
|
||||||
for (const k of Object.keys(r)) ret.set(k, r[k]);
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mapValues<K, V1, V2>(f: (v: V1) => V2, map: Map<K, V1>): Map<K, V2> {
|
|
||||||
const ret: Map<K, V2> = new Map();
|
|
||||||
|
|
||||||
for (const [k, v] of map) ret.set(k, f(v));
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
for (const [k, v] of mapValues(f, map)) ret.set(k, await v);
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function nextPowerOf2(n: number): number {
|
|
||||||
let i = 1;
|
|
||||||
|
|
||||||
while (i < n) i *= 2;
|
|
||||||
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
export class Listenable<T extends any[]> {
|
|
||||||
private readonly listeners: Array<(...args: T) => void> = [];
|
|
||||||
|
|
||||||
public addListener(listener: (...args: T) => void): void {
|
|
||||||
this.listeners.push(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected runListeners(...args: T): void {
|
|
||||||
this.listeners.forEach((l) => l(...args));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getJSON(url: string): Promise<unknown> {
|
|
||||||
const res = await window.fetch(url);
|
|
||||||
if (res.status < 200 || res.status >= 300) throw new Error(res.statusText);
|
|
||||||
|
|
||||||
return await res.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function nextAnimationFrame(): Promise<DOMHighResTimeStamp> {
|
|
||||||
return new Promise((resolve) => window.requestAnimationFrame(resolve));
|
|
||||||
}
|
|
|
@ -1,62 +0,0 @@
|
||||||
import { EntityData, EntityDataInput } from '../model/data/entity';
|
|
||||||
import { Renderer } from './renderer/renderer';
|
|
||||||
import { SpriteCoords, SpriteView, SpriteViewBuilder } from './sprite';
|
|
||||||
import { loadImage, mkTexture } from './util/image';
|
|
||||||
|
|
||||||
import { getJSON } from '../util';
|
|
||||||
|
|
||||||
import { vec2 } from 'gl-matrix';
|
|
||||||
|
|
||||||
export class EntityView {
|
|
||||||
public static async load(r: Renderer, name: string): Promise<EntityView> {
|
|
||||||
const data = new EntityData((await getJSON(`resources/entity/${name}.json`)) as EntityDataInput);
|
|
||||||
const tile = await loadImage(`resources/sprite/entity/${data.sprite}.png`);
|
|
||||||
|
|
||||||
const [texture, size] = mkTexture(r, tile);
|
|
||||||
const frameSize: [number, number] = [size[0], size[1] / data.frames];
|
|
||||||
|
|
||||||
const offset = vec2.mul(vec2.create(), frameSize, data.anchor);
|
|
||||||
r.snapToGrid(offset, offset);
|
|
||||||
|
|
||||||
const coords: SpriteCoords = [
|
|
||||||
-offset[0],
|
|
||||||
-offset[1],
|
|
||||||
-offset[0] + frameSize[0],
|
|
||||||
-offset[1] + frameSize[1],
|
|
||||||
];
|
|
||||||
|
|
||||||
const sprites: SpriteView[] = [];
|
|
||||||
|
|
||||||
for (let frame = 0; frame < data.frames; frame++) {
|
|
||||||
const builder = new SpriteViewBuilder(r, texture);
|
|
||||||
builder.addSprite(coords, [0, frame / data.frames, 1, (frame + 1) / data.frames]);
|
|
||||||
sprites.push(builder.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
return new EntityView(data, sprites);
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly totalTime: number;
|
|
||||||
|
|
||||||
private constructor(public readonly data: EntityData, public readonly sprites: SpriteView[]) {
|
|
||||||
if (data.animation) this.totalTime = data.animation.sequence.reduce((a, s) => a + s[0], 0);
|
|
||||||
else this.totalTime = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getSpriteByTime(time: number): SpriteView {
|
|
||||||
time %= this.totalTime;
|
|
||||||
|
|
||||||
if (this.data.animation) {
|
|
||||||
for (const [len, sprite] of this.data.animation.sequence) {
|
|
||||||
time -= len;
|
|
||||||
if (time < 0) return this.sprites[sprite];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.sprites[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
public renderByTime(time: number): void {
|
|
||||||
this.getSpriteByTime(time).render();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,68 +0,0 @@
|
||||||
import { InputHandler } from './inputhandler';
|
|
||||||
|
|
||||||
import { Listenable } from '../../util';
|
|
||||||
|
|
||||||
import { vec2 } from 'gl-matrix';
|
|
||||||
|
|
||||||
export enum ButtonCode {
|
|
||||||
Action,
|
|
||||||
Back,
|
|
||||||
Menu,
|
|
||||||
}
|
|
||||||
|
|
||||||
const buttonMapping: Record<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,40 +0,0 @@
|
||||||
import { Listenable } from '../../util';
|
|
||||||
|
|
||||||
export class InputHandler extends Listenable<[string, boolean]> {
|
|
||||||
private readonly keys: Set<string> = new Set();
|
|
||||||
|
|
||||||
constructor(relevantKeys: Set<string>) {
|
|
||||||
super();
|
|
||||||
|
|
||||||
window.addEventListener('keydown', (ev) => {
|
|
||||||
if (!relevantKeys.has(ev.code)) return;
|
|
||||||
|
|
||||||
ev.preventDefault();
|
|
||||||
|
|
||||||
if (ev.repeat) return;
|
|
||||||
|
|
||||||
this.keys.add(ev.code);
|
|
||||||
this.runListeners(ev.code, true);
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener('keyup', (ev) => {
|
|
||||||
if (!relevantKeys.has(ev.code)) return;
|
|
||||||
|
|
||||||
ev.preventDefault();
|
|
||||||
|
|
||||||
if (!this.keys.has(ev.code)) return;
|
|
||||||
|
|
||||||
this.keys.delete(ev.code);
|
|
||||||
this.runListeners(ev.code, false);
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener('blur', () => {
|
|
||||||
this.keys.clear();
|
|
||||||
this.runListeners('', false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public has(key: string): boolean {
|
|
||||||
return this.keys.has(key);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,172 +0,0 @@
|
||||||
import { EntityView } from './entity';
|
|
||||||
import { Renderer } from './renderer/renderer';
|
|
||||||
import { SpriteCoords, SpriteView, SpriteViewBuilder } from './sprite';
|
|
||||||
import { loadImage, mkTexture } from './util/image';
|
|
||||||
|
|
||||||
import { MapData } from '../model/data/map';
|
|
||||||
|
|
||||||
import { nextPowerOf2 } from '../util';
|
|
||||||
|
|
||||||
interface StaticMapTile {
|
|
||||||
type: 'static';
|
|
||||||
image: HTMLImageElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EntityTile {
|
|
||||||
type: 'entity';
|
|
||||||
entity: EntityView;
|
|
||||||
}
|
|
||||||
|
|
||||||
type MapTile = StaticMapTile | EntityTile;
|
|
||||||
|
|
||||||
interface StaticTilesetTile {
|
|
||||||
type: 'static';
|
|
||||||
coords: SpriteCoords;
|
|
||||||
}
|
|
||||||
|
|
||||||
type TilesetTile = StaticTilesetTile | EntityTile;
|
|
||||||
|
|
||||||
interface Tileset {
|
|
||||||
texture: WebGLTexture;
|
|
||||||
tiles: TilesetTile[];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadTile(r: Renderer, tile: string): Promise<MapTile> {
|
|
||||||
const name = tile.substr(1);
|
|
||||||
switch (tile[0]) {
|
|
||||||
case '-':
|
|
||||||
return {
|
|
||||||
type: 'static',
|
|
||||||
image: await loadImage(`resources/sprite/tile/${name}.png`),
|
|
||||||
};
|
|
||||||
|
|
||||||
case '@':
|
|
||||||
return {
|
|
||||||
type: 'entity',
|
|
||||||
entity: await EntityView.load(r, name),
|
|
||||||
};
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new Error('invalid tile specifier');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadTiles(r: Renderer, tiles: string[]): Promise<MapTile[]> {
|
|
||||||
return Promise.all(tiles.map((tile) => loadTile(r, tile)));
|
|
||||||
}
|
|
||||||
|
|
||||||
function mkTileset(r: Renderer, mapTiles: MapTile[]): Tileset {
|
|
||||||
const tileSize = 32;
|
|
||||||
|
|
||||||
const canvasDim = nextPowerOf2(Math.sqrt(mapTiles.length));
|
|
||||||
const canvasSize = canvasDim * tileSize;
|
|
||||||
|
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
canvas.width = canvas.height = canvasSize;
|
|
||||||
|
|
||||||
let x = 0;
|
|
||||||
let y = 0;
|
|
||||||
const tiles: TilesetTile[] = [];
|
|
||||||
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
|
|
||||||
|
|
||||||
for (const tile of mapTiles) {
|
|
||||||
switch (tile.type) {
|
|
||||||
case 'static':
|
|
||||||
ctx.drawImage(tile.image, x * tileSize, y * tileSize);
|
|
||||||
tiles.push({
|
|
||||||
type: 'static',
|
|
||||||
coords: [
|
|
||||||
x / canvasDim,
|
|
||||||
y / canvasDim,
|
|
||||||
(x + 1) / canvasDim,
|
|
||||||
(y + 1) / canvasDim,
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
x++;
|
|
||||||
if (x === canvasDim) {
|
|
||||||
x = 0;
|
|
||||||
y++;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'entity':
|
|
||||||
tiles.push(tile);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const [texture] = mkTexture(r, canvas);
|
|
||||||
|
|
||||||
return {
|
|
||||||
texture,
|
|
||||||
tiles,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function addSprite(
|
|
||||||
builder: SpriteViewBuilder,
|
|
||||||
entityTiles: Array<[[number, number], EntityView]>,
|
|
||||||
tileset: Tileset,
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
tile: number,
|
|
||||||
): void {
|
|
||||||
if (!tile) return;
|
|
||||||
|
|
||||||
const tilesetTile = tileset.tiles[tile - 1];
|
|
||||||
|
|
||||||
switch (tilesetTile.type) {
|
|
||||||
case 'static':
|
|
||||||
builder.addSprite([x, y, x + 1, y + 1], tilesetTile.coords);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'entity':
|
|
||||||
entityTiles.push([[x + 0.5, y + 0.5], tilesetTile.entity]);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MapLayerView {
|
|
||||||
public constructor(
|
|
||||||
private r: Renderer,
|
|
||||||
private staticTiles: SpriteView,
|
|
||||||
private entityTiles: Array<[[number, number], EntityView]>,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public render(time: number): void {
|
|
||||||
this.r.setTranslation([0, 0]);
|
|
||||||
this.staticTiles.render();
|
|
||||||
|
|
||||||
for (const [coords, entity] of this.entityTiles) {
|
|
||||||
this.r.setTranslation(coords);
|
|
||||||
entity.renderByTime(time);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
public static async load(r: Renderer, map: MapData): Promise<MapView> {
|
|
||||||
const tiles = await loadTiles(r, map.tiles);
|
|
||||||
const tileset = mkTileset(r, tiles);
|
|
||||||
|
|
||||||
const layers = map.layers.map((layer) => buildMapLayer(r, tileset, layer.tiles));
|
|
||||||
return new MapView(layers);
|
|
||||||
}
|
|
||||||
|
|
||||||
private constructor(private layers: MapLayerView[]) {}
|
|
||||||
|
|
||||||
public render(time: number): void {
|
|
||||||
for (const layer of this.layers) layer.render(time);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,96 +0,0 @@
|
||||||
import { nextPowerOf2 } from '../../util';
|
|
||||||
import { Shaders } from './shaders';
|
|
||||||
|
|
||||||
import { mat4, vec2 } from 'gl-matrix';
|
|
||||||
|
|
||||||
export class Renderer {
|
|
||||||
public readonly coordScale = 32;
|
|
||||||
private readonly viewScale = 2;
|
|
||||||
|
|
||||||
private readonly gl: WebGLRenderingContext;
|
|
||||||
private readonly shaders: Shaders;
|
|
||||||
|
|
||||||
private readonly center: vec2 = vec2.create();
|
|
||||||
private readonly translation: vec2 = vec2.create();
|
|
||||||
private readonly viewport: mat4 = mat4.create();
|
|
||||||
|
|
||||||
constructor(private readonly canvas: HTMLCanvasElement) {
|
|
||||||
this.gl = this.mkContext();
|
|
||||||
|
|
||||||
this.shaders = new Shaders(this.gl);
|
|
||||||
|
|
||||||
this.gl.clearColor(0.0, 0.0, 0.0, 1.0);
|
|
||||||
|
|
||||||
this.gl.enable(this.gl.BLEND);
|
|
||||||
this.gl.blendFunc(this.gl.ONE, this.gl.ONE_MINUS_SRC_ALPHA);
|
|
||||||
this.gl.pixelStorei(this.gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public createBuffer(): WebGLBuffer {
|
|
||||||
const ret = this.gl.createBuffer();
|
|
||||||
if (!ret) throw new Error('unable to create buffer');
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getContext(): WebGLRenderingContext {
|
|
||||||
return this.gl;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getVertexPosLoc(): number {
|
|
||||||
return this.shaders.vertexPosLoc;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getTextureCoordLoc(): number {
|
|
||||||
return this.shaders.textureCoordLoc;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getSamplerLoc(): WebGLUniformLocation {
|
|
||||||
return this.shaders.samplerLoc;
|
|
||||||
}
|
|
||||||
|
|
||||||
public setCenter(v: vec2): void {
|
|
||||||
this.snapToGrid(this.center, v);
|
|
||||||
}
|
|
||||||
|
|
||||||
public setTranslation(v: vec2): void {
|
|
||||||
vec2.sub(this.translation, v, this.center);
|
|
||||||
this.snapToGrid(this.translation, this.translation);
|
|
||||||
this.gl.uniform2fv(this.shaders.translateLoc, this.translation);
|
|
||||||
}
|
|
||||||
|
|
||||||
public clear(): void {
|
|
||||||
this.gl.clear(this.gl.COLOR_BUFFER_BIT);
|
|
||||||
|
|
||||||
this.setTranslation([0, 0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public snapToGrid(out: vec2, a: vec2): void {
|
|
||||||
vec2.scale(out, a, this.coordScale);
|
|
||||||
vec2.round(out, out);
|
|
||||||
vec2.scale(out, out, 1 / this.coordScale);
|
|
||||||
}
|
|
||||||
|
|
||||||
public resize(): void {
|
|
||||||
const w = this.canvas.width;
|
|
||||||
const h = this.canvas.height;
|
|
||||||
const ws = nextPowerOf2(w);
|
|
||||||
const hs = nextPowerOf2(h);
|
|
||||||
|
|
||||||
this.gl.viewport((w - ws) / 2, (h - hs) / 2, ws, hs);
|
|
||||||
this.clear();
|
|
||||||
|
|
||||||
const scale = this.viewScale * this.coordScale;
|
|
||||||
|
|
||||||
mat4.identity(this.viewport);
|
|
||||||
mat4.scale(this.viewport, this.viewport, [(2 * scale) / ws, (-2 * scale) / hs, 1.0]);
|
|
||||||
this.gl.uniformMatrix4fv(this.shaders.viewportLoc, false, this.viewport);
|
|
||||||
}
|
|
||||||
|
|
||||||
private mkContext(): WebGLRenderingContext {
|
|
||||||
const gl = this.canvas.getContext('webgl');
|
|
||||||
if (!gl) throw new Error('unable to initialize WebGL context');
|
|
||||||
|
|
||||||
return gl;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,75 +0,0 @@
|
||||||
import fragmentShaderSrc from './shaders/default.fs';
|
|
||||||
import vertexShaderSrc from './shaders/default.vs';
|
|
||||||
|
|
||||||
export class Shaders {
|
|
||||||
public readonly viewportLoc: WebGLUniformLocation;
|
|
||||||
public readonly translateLoc: WebGLUniformLocation;
|
|
||||||
|
|
||||||
public readonly vertexPosLoc: number;
|
|
||||||
public readonly textureCoordLoc: number;
|
|
||||||
public readonly samplerLoc: WebGLUniformLocation;
|
|
||||||
|
|
||||||
constructor(private readonly gl: WebGLRenderingContext) {
|
|
||||||
const shaderProgram = this.gl.createProgram();
|
|
||||||
if (!shaderProgram) throw new Error('Unable to create shader program');
|
|
||||||
|
|
||||||
const vertexShader = this.compileShader(this.gl.VERTEX_SHADER, vertexShaderSrc);
|
|
||||||
const fragmentShader = this.compileShader(this.gl.FRAGMENT_SHADER, fragmentShaderSrc);
|
|
||||||
|
|
||||||
this.gl.attachShader(shaderProgram, vertexShader);
|
|
||||||
this.gl.attachShader(shaderProgram, fragmentShader);
|
|
||||||
|
|
||||||
this.gl.linkProgram(shaderProgram);
|
|
||||||
if (!this.gl.getProgramParameter(shaderProgram, this.gl.LINK_STATUS)) {
|
|
||||||
const err = this.gl.getProgramInfoLog(shaderProgram);
|
|
||||||
|
|
||||||
this.gl.deleteShader(vertexShader);
|
|
||||||
this.gl.deleteShader(fragmentShader);
|
|
||||||
this.gl.deleteProgram(shaderProgram);
|
|
||||||
|
|
||||||
throw new Error('Unable to link shader: ' + err);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.gl.useProgram(shaderProgram);
|
|
||||||
|
|
||||||
this.vertexPosLoc = this.getAttribLocation(shaderProgram, 'aVertexPos');
|
|
||||||
this.gl.enableVertexAttribArray(this.vertexPosLoc);
|
|
||||||
|
|
||||||
this.textureCoordLoc = this.getAttribLocation(shaderProgram, 'aTextureCoord');
|
|
||||||
this.gl.enableVertexAttribArray(this.textureCoordLoc);
|
|
||||||
|
|
||||||
this.viewportLoc = this.getUniformLocation(shaderProgram, 'uViewport');
|
|
||||||
this.translateLoc = this.getUniformLocation(shaderProgram, 'uTranslate');
|
|
||||||
this.samplerLoc = this.getUniformLocation(shaderProgram, 'uSampler');
|
|
||||||
}
|
|
||||||
|
|
||||||
private compileShader(type: number, src: string): WebGLShader {
|
|
||||||
const shader = this.gl.createShader(type);
|
|
||||||
if (!shader) throw new Error('Unable to create shader');
|
|
||||||
|
|
||||||
this.gl.shaderSource(shader, src);
|
|
||||||
this.gl.compileShader(shader);
|
|
||||||
|
|
||||||
if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
|
|
||||||
const err = this.gl.getShaderInfoLog(shader);
|
|
||||||
this.gl.deleteShader(shader);
|
|
||||||
throw new Error('Unable to compile shader: ' + err);
|
|
||||||
}
|
|
||||||
|
|
||||||
return shader;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getAttribLocation(program: WebGLProgram, name: string): number {
|
|
||||||
const ret = this.gl.getAttribLocation(program, name);
|
|
||||||
if (ret < 0) throw new Error(`unable to get location of attribute '${name}'`);
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getUniformLocation(program: WebGLProgram, name: string): WebGLUniformLocation {
|
|
||||||
const ret = this.gl.getUniformLocation(program, name);
|
|
||||||
if (!ret) throw new Error(`unable to get location of uniform '${name}'`);
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
precision highp float;
|
|
||||||
|
|
||||||
varying vec2 vTextureCoord;
|
|
||||||
|
|
||||||
uniform sampler2D uSampler;
|
|
||||||
|
|
||||||
|
|
||||||
void main(void) {
|
|
||||||
gl_FragColor = texture2D(uSampler, vTextureCoord);
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
attribute vec2 aVertexPos;
|
|
||||||
attribute vec2 aTextureCoord;
|
|
||||||
|
|
||||||
uniform mat4 uViewport;
|
|
||||||
uniform vec2 uTranslate;
|
|
||||||
|
|
||||||
varying highp vec2 vTextureCoord;
|
|
||||||
|
|
||||||
|
|
||||||
void main(void) {
|
|
||||||
gl_Position = uViewport * vec4(aVertexPos + uTranslate, 0.0, 1.0);
|
|
||||||
vTextureCoord = aTextureCoord;
|
|
||||||
}
|
|
|
@ -1,78 +0,0 @@
|
||||||
import { Renderer } from './renderer/renderer';
|
|
||||||
|
|
||||||
export type SpriteCoords = [number, number, number, number];
|
|
||||||
|
|
||||||
export class SpriteView {
|
|
||||||
private readonly vertexCount: number;
|
|
||||||
private readonly vertexBuffer: WebGLBuffer;
|
|
||||||
private readonly textureBuffer: WebGLBuffer;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly r: Renderer,
|
|
||||||
private readonly texture: WebGLTexture,
|
|
||||||
vertexData: number[],
|
|
||||||
textureData: number[],
|
|
||||||
) {
|
|
||||||
const gl = r.getContext();
|
|
||||||
|
|
||||||
this.vertexBuffer = r.createBuffer();
|
|
||||||
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
|
|
||||||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertexData), gl.STATIC_DRAW);
|
|
||||||
|
|
||||||
this.textureBuffer = r.createBuffer();
|
|
||||||
gl.bindBuffer(gl.ARRAY_BUFFER, this.textureBuffer);
|
|
||||||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(textureData), gl.STATIC_DRAW);
|
|
||||||
|
|
||||||
this.vertexCount = vertexData.length / 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
public render(): void {
|
|
||||||
const gl = this.r.getContext();
|
|
||||||
|
|
||||||
gl.activeTexture(gl.TEXTURE0);
|
|
||||||
gl.bindTexture(gl.TEXTURE_2D, this.texture);
|
|
||||||
gl.uniform1i(this.r.getSamplerLoc(), 0);
|
|
||||||
|
|
||||||
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
|
|
||||||
gl.vertexAttribPointer(this.r.getVertexPosLoc(), 2, gl.FLOAT, false, 0, 0);
|
|
||||||
|
|
||||||
gl.bindBuffer(gl.ARRAY_BUFFER, this.textureBuffer);
|
|
||||||
gl.vertexAttribPointer(this.r.getTextureCoordLoc(), 2, gl.FLOAT, false, 0, 0);
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
import { Renderer } from '../renderer/renderer';
|
|
||||||
|
|
||||||
export function loadImage(url: string): Promise<HTMLImageElement> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const img = new Image();
|
|
||||||
img.addEventListener('load', () => {
|
|
||||||
resolve(img);
|
|
||||||
});
|
|
||||||
img.addEventListener('error', () => {
|
|
||||||
reject(new Error('failed to load ' + url));
|
|
||||||
});
|
|
||||||
img.src = url;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mkTexture(r: Renderer, src: HTMLCanvasElement | HTMLImageElement): [WebGLTexture, [number, number]] {
|
|
||||||
const gl = r.getContext();
|
|
||||||
const texture = gl.createTexture();
|
|
||||||
if (!texture) throw new Error('unable to create texture');
|
|
||||||
|
|
||||||
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
||||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, src);
|
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
||||||
|
|
||||||
const size: [number, number] = [src.width / r.coordScale, src.height / r.coordScale];
|
|
||||||
|
|
||||||
return [texture, size];
|
|
||||||
}
|
|
12
src/style.css
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
html, body {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
background: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0px;
|
||||||
|
padding: 0px;
|
||||||
|
border: 0px;
|
||||||
|
}
|
22
src/util.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
|
||||||
|
export function mapPromises<T>(promises: {[key: string]: Promise<T>}): Promise<{[key: string]: T}> {
|
||||||
|
var p: Promise<void>[] = [];
|
||||||
|
var ret: {[key: string]: T} = {};
|
||||||
|
|
||||||
|
_.forOwn(promises, (v, k) => {
|
||||||
|
p.push(v.then(r => {ret[k] = r;}));
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(p).then(() => ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function arrayToObject(arr: (number|string)[]): {[key: string]: boolean} {
|
||||||
|
var ret: {[key: string]: boolean} = {};
|
||||||
|
|
||||||
|
for (let v of arr)
|
||||||
|
ret[v] = true;
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
54
src/view/InputHandler.ts
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
|
||||||
|
import Direction from '../model/Direction';
|
||||||
|
|
||||||
|
|
||||||
|
class InputHandler {
|
||||||
|
keys: {[key: number]: boolean} = {};
|
||||||
|
|
||||||
|
private listeners: (() => void)[] = [];
|
||||||
|
|
||||||
|
private callListeners(): void {
|
||||||
|
this.listeners.forEach(l => l());
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(private relevantKeys: {[key: number]: boolean}) {
|
||||||
|
window.addEventListener('keydown', (ev) => {
|
||||||
|
if (!relevantKeys[ev.keyCode])
|
||||||
|
return;
|
||||||
|
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
if (!this.keys[ev.keyCode]) {
|
||||||
|
this.keys[ev.keyCode] = true;
|
||||||
|
this.callListeners();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('keyup', (ev) => {
|
||||||
|
if (!relevantKeys[ev.keyCode])
|
||||||
|
return;
|
||||||
|
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
if (this.keys[ev.keyCode]) {
|
||||||
|
delete this.keys[ev.keyCode];
|
||||||
|
this.callListeners();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addListener(l: () => void) {
|
||||||
|
this.listeners.push(l);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module InputHandler {
|
||||||
|
export const Left = 37;
|
||||||
|
export const Up = 38;
|
||||||
|
export const Right = 39;
|
||||||
|
export const Down = 40;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InputHandler;
|
230
src/view/MapView.ts
Normal file
|
@ -0,0 +1,230 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
|
||||||
|
import * as util from '../util';
|
||||||
|
import EntityPosition from '../model/EntityPosition';
|
||||||
|
import MapData from '../model/MapData';
|
||||||
|
import Position from '../model/Position';
|
||||||
|
import TileData from '../model/TileData';
|
||||||
|
|
||||||
|
|
||||||
|
const tileSize = 32;
|
||||||
|
|
||||||
|
const body = document.getElementsByTagName('body')[0];
|
||||||
|
|
||||||
|
|
||||||
|
function loadImage(url: string): Promise<HTMLImageElement> {
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
var img = new Image();
|
||||||
|
img.addEventListener('load', () => { resolve(img); });
|
||||||
|
img.addEventListener('error', () => { reject(Error('Failed to load ' + url)); });
|
||||||
|
img.src = url;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadImages(imgs: {[key: string]: string}): Promise<{[key: string]: HTMLImageElement}> {
|
||||||
|
return util.mapPromises(_.mapValues(imgs, loadImage));
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadTiles(tiles: {[key: string]: TileData}): Promise<{[key: string]: HTMLImageElement}> {
|
||||||
|
var imgs: {[key: string]: string} = {}
|
||||||
|
|
||||||
|
_.forOwn(tiles, t => {
|
||||||
|
imgs[t.file] = `resources/sprite/tile/${t.file}.png`
|
||||||
|
});
|
||||||
|
|
||||||
|
return loadImages(imgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadEntities(entities: EntityPosition[]): Promise<{[key: string]: HTMLImageElement}> {
|
||||||
|
var p: {[key: string]: Promise<HTMLImageElement>} = {};
|
||||||
|
|
||||||
|
entities.forEach(e => {
|
||||||
|
p[e.entity.name] = loadImage(`resources/sprite/entity/${e.entity.name}.png`);
|
||||||
|
});
|
||||||
|
|
||||||
|
return util.mapPromises(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
function entityPosition(e: EntityPosition, time: number): Position {
|
||||||
|
if (e.transition) {
|
||||||
|
var t = e.transition;
|
||||||
|
var d = (time - t.start) / (t.end-t.start);
|
||||||
|
|
||||||
|
return new Position(
|
||||||
|
(1-d) * t.orig.x + d * t.dest.x,
|
||||||
|
(1-d) * t.orig.y + d * t.dest.y
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return e.position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function tiles(n: number) {
|
||||||
|
return Math.round(tileSize*n);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Rect {
|
||||||
|
constructor(public x1: number, public y1: number, public x2: number, public y2: number) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default class MapView {
|
||||||
|
private redrawPending: boolean = false;
|
||||||
|
|
||||||
|
private canvas: HTMLCanvasElement;
|
||||||
|
private ctx: CanvasRenderingContext2D;
|
||||||
|
private scale: number;
|
||||||
|
|
||||||
|
private tiles: {[key: string]: HTMLImageElement};
|
||||||
|
private entitySprites: {[key: string]: HTMLImageElement};
|
||||||
|
|
||||||
|
constructor(private map: MapData,
|
||||||
|
private entities: EntityPosition[],
|
||||||
|
private origin: EntityPosition,
|
||||||
|
private updateState: (time: number) => void) {
|
||||||
|
this.canvas = document.createElement('canvas');
|
||||||
|
this.canvas.style.position = 'absolute';
|
||||||
|
body.appendChild(this.canvas);
|
||||||
|
|
||||||
|
this.ctx = this.canvas.getContext('2d');
|
||||||
|
|
||||||
|
window.addEventListener('resize', () => this.setSize());
|
||||||
|
this.setSize();
|
||||||
|
|
||||||
|
var tilesReady = loadTiles(map.tiles).then((tiles) => {
|
||||||
|
this.tiles = tiles;
|
||||||
|
});
|
||||||
|
|
||||||
|
var entitiesReady = loadEntities(this.entities).then((entities) => {
|
||||||
|
this.entitySprites = entities;
|
||||||
|
});
|
||||||
|
|
||||||
|
Promise.all([tilesReady, entitiesReady]).then(() => {
|
||||||
|
this.redraw();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private setSize(): void {
|
||||||
|
var e = document.documentElement;
|
||||||
|
var w = window.innerWidth || e.clientWidth || body.clientWidth;
|
||||||
|
var h = window.innerHeight || e.clientHeight || body.clientHeight;
|
||||||
|
|
||||||
|
this.canvas.width = w;
|
||||||
|
this.canvas.height = h;
|
||||||
|
|
||||||
|
var pixels = Math.max(w/20, h/15.0);
|
||||||
|
this.scale = Math.ceil(pixels / tileSize);
|
||||||
|
|
||||||
|
this.redraw()
|
||||||
|
}
|
||||||
|
|
||||||
|
private drawSprite(img: HTMLImageElement, srcX: number, srcY: number, destX: number, destY: number): void {
|
||||||
|
this.ctx.drawImage(
|
||||||
|
img,
|
||||||
|
tiles(srcX), tiles(srcY),
|
||||||
|
tileSize, tileSize,
|
||||||
|
tiles(destX)*this.scale,
|
||||||
|
tiles(destY)*this.scale,
|
||||||
|
tileSize*this.scale,
|
||||||
|
tileSize*this.scale
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private drawTile(x: number, y: number, tile: TileData): void {
|
||||||
|
if (!tile)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var img = this.tiles[tile.file];
|
||||||
|
if (!img)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.drawSprite(img, tile.subtile || 0, 0, x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
private drawEntity(e: EntityPosition, time: number): boolean {
|
||||||
|
var sprite = this.entitySprites[e.entity.name];
|
||||||
|
if (!sprite)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var p = entityPosition(e, time);
|
||||||
|
this.drawSprite(sprite, e.direction, 0, p.x, p.y);
|
||||||
|
|
||||||
|
return !!e.transition;
|
||||||
|
}
|
||||||
|
|
||||||
|
private setOrigin(time: number): Rect {
|
||||||
|
var origin = entityPosition(this.origin, time);
|
||||||
|
|
||||||
|
var w = this.canvas.width;
|
||||||
|
var h = this.canvas.height;
|
||||||
|
|
||||||
|
this.ctx.translate(Math.round(w/2), Math.round(h/2));
|
||||||
|
this.ctx.translate(-tiles(origin.x + 0.5)*this.scale, -tiles(origin.y + 0.5)*this.scale);
|
||||||
|
|
||||||
|
var sw = w / (this.scale*tileSize), sh = h / (this.scale*tileSize);
|
||||||
|
|
||||||
|
return new Rect(
|
||||||
|
Math.floor(origin.x-sw/2+0.5), Math.floor(origin.y-sh/2+0.5),
|
||||||
|
Math.ceil(origin.x+sw/2-0.5), Math.ceil(origin.y+sh/2-0.5)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private draw(time: number): void {
|
||||||
|
this.updateState(time);
|
||||||
|
|
||||||
|
this.redrawPending = false;
|
||||||
|
|
||||||
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
|
||||||
|
if (!this.tiles || !this.entitySprites)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.ctx.save();
|
||||||
|
|
||||||
|
var rect = this.setOrigin(time);
|
||||||
|
|
||||||
|
(<any>this.ctx).mozImageSmoothingEnabled = false;
|
||||||
|
(<any>this.ctx).webkitImageSmoothingEnabled = false;
|
||||||
|
(<any>this.ctx).msImageSmoothingEnabled = false;
|
||||||
|
(<any>this.ctx).imageSmoothingEnabled = false;
|
||||||
|
|
||||||
|
this.map.layers.forEach((layer) => {
|
||||||
|
for (let y = rect.y1; y <= rect.y2; y++) {
|
||||||
|
let row = layer[y];
|
||||||
|
if (!row)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
for (let x = rect.x1; x <= rect.x2; x++) {
|
||||||
|
let tile = row[x];
|
||||||
|
if (!tile)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
this.drawTile(x, y, this.map.tiles[tile]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var animate = false;
|
||||||
|
|
||||||
|
this.entities.forEach(e => {
|
||||||
|
if (this.drawEntity(e, time))
|
||||||
|
animate = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ctx.restore();
|
||||||
|
|
||||||
|
if (animate)
|
||||||
|
this.redraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
redraw(): void {
|
||||||
|
if (this.redrawPending)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.redrawPending = true;
|
||||||
|
window.requestAnimationFrame((time: number) => this.draw(time));
|
||||||
|
}
|
||||||
|
}
|
10
static/index.html
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>RPGedit</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script src="bundle.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"sprite": "green_circle"
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
{
|
|
||||||
"sprite": "red_circle",
|
|
||||||
"collision": [
|
|
||||||
{
|
|
||||||
"type": "circle",
|
|
||||||
"center": [0, 0],
|
|
||||||
"radius": 0.46875
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"sprite": "red_ellipse"
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
{
|
|
||||||
"sprite": "square",
|
|
||||||
"collision": [
|
|
||||||
{
|
|
||||||
"type": "polygon",
|
|
||||||
"vertices": [
|
|
||||||
[-0.46875, -0.46875],
|
|
||||||
[-0.46875, 0.46875],
|
|
||||||
[0.46875, 0.46875],
|
|
||||||
[0.46875, -0.46875]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"frames": 4,
|
|
||||||
"animation": {
|
|
||||||
"sequence": [
|
|
||||||
[500, 0],
|
|
||||||
[500, 1],
|
|
||||||
[500, 2],
|
|
||||||
[500, 3]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
{
|
|
||||||
"sprite": "water",
|
|
||||||
"frames": 4,
|
|
||||||
"animation": {
|
|
||||||
"sequence": [
|
|
||||||
[500, 0],
|
|
||||||
[500, 1],
|
|
||||||
[500, 2],
|
|
||||||
[500, 3]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,84 +1,111 @@
|
||||||
{
|
{
|
||||||
"tiles": [
|
"tiles": {
|
||||||
"-stone/floor",
|
"G": {"file": "grass"},
|
||||||
"-stone/plate",
|
"<": {"file": "road", "subtile": 0},
|
||||||
"-stone/wall/top",
|
">": {"file": "road", "subtile": 1}
|
||||||
"-stone/wall/right",
|
},
|
||||||
"-stone/wall/bottom",
|
"collision": [
|
||||||
"-stone/wall/left",
|
"00000000000110000000000000000000",
|
||||||
"-stone/wall/top_left",
|
"00000000000110000000000000000000",
|
||||||
"-stone/wall/top_right",
|
"00000000000110000000000000000000",
|
||||||
"-stone/wall/bottom_right",
|
"00000000000110000000000000000000",
|
||||||
"-stone/wall/bottom_left",
|
"00000000000110000000000000000000",
|
||||||
"-stone/wall/top_left_inner",
|
"00000000000110000000000000000000",
|
||||||
"-stone/wall/top_right_inner",
|
"00000000000110000000000000000000",
|
||||||
"-stone/wall/bottom_right_inner",
|
"00000000000110000000000000000000",
|
||||||
"-stone/wall/bottom_left_inner",
|
"00000000000110000000000000000000",
|
||||||
"@water",
|
"00000000000110000000000000000000",
|
||||||
"-stone/border/bottom_right",
|
"00000000000110000000000000000000",
|
||||||
"-stone/border/bottom_left"
|
"00000000000110000000000000000000",
|
||||||
|
"00000000000110000000000000000000",
|
||||||
|
"00000000000110000000000000000000",
|
||||||
|
"00000000000110000000000000000000",
|
||||||
|
"00000000000110000000000000000000",
|
||||||
|
"00000000000110000000000000000000",
|
||||||
|
"00000000000110000000000000000000",
|
||||||
|
"00000000000110000000000000000000",
|
||||||
|
"00000000000110000000000000000000",
|
||||||
|
"00000000000110000000000000000000",
|
||||||
|
"00000000000110000000000000000000",
|
||||||
|
"00000000000110000000000000000000",
|
||||||
|
"00000000000110000000000000000000",
|
||||||
|
"00000000000110000000000000000000",
|
||||||
|
"00000000000110000000000000000000",
|
||||||
|
"00000000000110000000000000000000",
|
||||||
|
"00000000000110000000000000000000",
|
||||||
|
"00000000000110000000000000000000",
|
||||||
|
"00000000000110000000000000000000",
|
||||||
|
"00000000000110000000000000000000",
|
||||||
|
"00000000000000000000000000000000"
|
||||||
],
|
],
|
||||||
"layers": [
|
"layers": [
|
||||||
{
|
[
|
||||||
"tiles": [
|
"GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG",
|
||||||
[0, 7, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 8, 0],
|
"GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG",
|
||||||
[0, 6, 1, 1, 1, 1, 15, 15, 1, 1, 1, 1, 4, 0],
|
"GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG",
|
||||||
[0, 6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4, 0],
|
"GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG",
|
||||||
[0, 6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4, 0],
|
"GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG",
|
||||||
[3, 11, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 12, 3],
|
"GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG",
|
||||||
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
"GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG",
|
||||||
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
"GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG",
|
||||||
[5, 14, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 13, 5],
|
"GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG",
|
||||||
[0, 6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4, 0],
|
"GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG",
|
||||||
[0, 6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4, 0],
|
"GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG",
|
||||||
[0, 6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4, 0],
|
"GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG",
|
||||||
[0, 10, 5, 5, 5, 14, 1, 1, 13, 5, 5, 5, 9, 0],
|
"GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG",
|
||||||
[0, 0, 0, 0, 0, 6, 1, 1, 4, 0, 0, 0, 0, 0]
|
"GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG",
|
||||||
]
|
"GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG",
|
||||||
},
|
"GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG",
|
||||||
{
|
"GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG",
|
||||||
"tiles": [
|
"GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG",
|
||||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
"GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG",
|
||||||
[0, 0, 0, 0, 0, 0, 17, 16, 0, 0, 0, 0, 0, 0],
|
"GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG",
|
||||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
"GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG",
|
||||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
"GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG",
|
||||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
"GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG",
|
||||||
[2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
|
"GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG",
|
||||||
[2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
|
"GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG",
|
||||||
[0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0],
|
"GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG",
|
||||||
[0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0],
|
"GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG",
|
||||||
[0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0],
|
"GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG",
|
||||||
[0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0],
|
"GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG",
|
||||||
[0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0],
|
"GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG",
|
||||||
[0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0]
|
"GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG",
|
||||||
]
|
"GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG"
|
||||||
}
|
|
||||||
],
|
],
|
||||||
"collision": [
|
[
|
||||||
{
|
"................................",
|
||||||
"type": "polygon",
|
"................................",
|
||||||
"vertices": [
|
"................................",
|
||||||
[ 2, 1],
|
"................................",
|
||||||
[ 6, 1],
|
"................................",
|
||||||
[ 6, 2],
|
"................................",
|
||||||
[ 8, 2],
|
"................................",
|
||||||
[ 8, 1],
|
"................................",
|
||||||
[12, 1],
|
"................................",
|
||||||
[12, 5],
|
"................................",
|
||||||
[14, 5],
|
"................................",
|
||||||
[14, 7],
|
"................................",
|
||||||
[12, 7],
|
"................................",
|
||||||
[12, 11],
|
"................................",
|
||||||
[ 8, 11],
|
"................................",
|
||||||
[ 8, 13],
|
"................................",
|
||||||
[ 6, 13],
|
"................................",
|
||||||
[ 6, 11],
|
"................................",
|
||||||
[ 2, 11],
|
"................................",
|
||||||
[ 2, 7],
|
"................................",
|
||||||
[ 0, 7],
|
"................................",
|
||||||
[ 0, 5],
|
"................................",
|
||||||
[ 2, 5]
|
"................................",
|
||||||
]
|
"................................",
|
||||||
}
|
"................................",
|
||||||
|
"................................",
|
||||||
|
"................................",
|
||||||
|
"................................",
|
||||||
|
"................................",
|
||||||
|
"................................",
|
||||||
|
"................................",
|
||||||
|
"................................"
|
||||||
|
]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 155 B |
Before Width: | Height: | Size: 528 B After Width: | Height: | Size: 808 B |
Before Width: | Height: | Size: 1.7 KiB |
BIN
static/resources/sprite/tile/horizontal_bar.png
Normal file
After Width: | Height: | Size: 118 B |
BIN
static/resources/sprite/tile/road.png
Normal file
After Width: | Height: | Size: 329 B |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 245 B |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 242 B |
Before Width: | Height: | Size: 930 B |
Before Width: | Height: | Size: 927 B |
Before Width: | Height: | Size: 765 B |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 1.4 KiB |
|
@ -1,13 +1,34 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2017",
|
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"strict": true,
|
"target": "es5",
|
||||||
"jsx": "react",
|
"noImplicitAny": true,
|
||||||
"outDir": "dist",
|
"sourceMap": true,
|
||||||
"sourceMap": true
|
"noEmit": true
|
||||||
},
|
},
|
||||||
"include": [
|
"files": [
|
||||||
"src/**/*"
|
"require.d.ts",
|
||||||
]
|
"typings/tsd.d.ts",
|
||||||
|
"src/app.ts",
|
||||||
|
"src/control/MapContext.ts",
|
||||||
|
"src/model/Direction.ts",
|
||||||
|
"src/model/Entity.ts",
|
||||||
|
"src/model/EntityPosition.ts",
|
||||||
|
"src/model/MapData.ts",
|
||||||
|
"src/model/Position.ts",
|
||||||
|
"src/model/TileData.ts",
|
||||||
|
"src/model/Transition.ts",
|
||||||
|
"src/util.ts",
|
||||||
|
"src/view/InputHandler.ts",
|
||||||
|
"src/view/MapView.ts"
|
||||||
|
],
|
||||||
|
"filesGlob": [
|
||||||
|
"require.d.ts",
|
||||||
|
"typings/tsd.d.ts",
|
||||||
|
"src/**/*.ts"
|
||||||
|
],
|
||||||
|
"compileOnSave": false,
|
||||||
|
"atom": {
|
||||||
|
"rewriteTsconfig": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
15
tsd.json
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"version": "v4",
|
||||||
|
"repo": "borisyankov/DefinitelyTyped",
|
||||||
|
"ref": "master",
|
||||||
|
"path": "typings",
|
||||||
|
"bundle": "typings/tsd.d.ts",
|
||||||
|
"installed": {
|
||||||
|
"lodash/lodash.d.ts": {
|
||||||
|
"commit": "f11e49cfb77f99678657ddfe5d43017849675c64"
|
||||||
|
},
|
||||||
|
"es6-promise/es6-promise.d.ts": {
|
||||||
|
"commit": "f11e49cfb77f99678657ddfe5d43017849675c64"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
21
webpack.config.js
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
var HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
entry: './src/app.ts',
|
||||||
|
output: {
|
||||||
|
path: './build',
|
||||||
|
filename: 'bundle.js'
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
loaders: [
|
||||||
|
{ test: /\.css$/, loader: 'style-loader!css-loader' },
|
||||||
|
{ test: /\.ts$/, loader: 'ts-loader' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
extensions: ['', '.ts']
|
||||||
|
},
|
||||||
|
ts: {
|
||||||
|
compilerOptions: { noEmit: false }
|
||||||
|
}
|
||||||
|
};
|