commit
8a4ab8d79b
4
.prettierrc
Normal file
4
.prettierrc
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "none"
|
||||||
|
}
|
@ -1,11 +1,11 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
extends: [
|
extends: [
|
||||||
// add more generic rule sets here, such as:
|
// add more generic rule sets here, such as:
|
||||||
"eslint:recommended",
|
'eslint:recommended',
|
||||||
"plugin:svelte/recommended",
|
'plugin:svelte/recommended'
|
||||||
],
|
],
|
||||||
rules: {
|
rules: {
|
||||||
// override/add rules settings here, such as:
|
// override/add rules settings here, such as:
|
||||||
// 'svelte/rule-name': 'error'
|
// 'svelte/rule-name': 'error'
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
--orange: #af3a03;
|
--orange: #af3a03;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="dark"] {
|
[data-theme='dark'] {
|
||||||
--bg: #282828;
|
--bg: #282828;
|
||||||
--text: #f9f5d7;
|
--text: #f9f5d7;
|
||||||
--red: #fb4934;
|
--red: #fb4934;
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
@import url("./theme.css");
|
@import url('./theme.css');
|
||||||
@import url("./tf.css");
|
@import url('./tf.css');
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "scientifica";
|
font-family: 'scientifica';
|
||||||
src: url("/fonts/scientifica.ttf");
|
src: url('/fonts/scientifica.ttf');
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: "scientifica", monospace;
|
font-family: 'scientifica', monospace;
|
||||||
transition: background 0.2s ease-in-out;
|
transition: background 0.2s ease-in-out;
|
||||||
font-smooth: never;
|
font-smooth: never;
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
rgba(162, 254, 254, 1) 100%
|
rgba(162, 254, 254, 1) 100%
|
||||||
);
|
);
|
||||||
|
|
||||||
content: "";
|
content: '';
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
@import url("./colors.css");
|
@import url('./colors.css');
|
||||||
|
|
||||||
.primary {
|
.primary {
|
||||||
color: var(--aqua);
|
color: var(--aqua);
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Floor, Player } from "@engine/entities";
|
import { Game } from '@engine/Game';
|
||||||
import { Game } from "@engine/Game";
|
import { Entity } from '@engine/entities';
|
||||||
|
import { Grid } from '@engine/structures';
|
||||||
import {
|
import {
|
||||||
WallBounds,
|
WallBounds,
|
||||||
FacingDirection,
|
FacingDirection,
|
||||||
@ -7,28 +8,141 @@ import {
|
|||||||
Physics,
|
Physics,
|
||||||
Input,
|
Input,
|
||||||
Collision,
|
Collision,
|
||||||
} from "@engine/systems";
|
NetworkUpdate
|
||||||
|
} from '@engine/systems';
|
||||||
|
import {
|
||||||
|
type MessageQueueProvider,
|
||||||
|
type MessagePublisher,
|
||||||
|
type MessageProcessor,
|
||||||
|
type Message,
|
||||||
|
type EntityAddBody,
|
||||||
|
MessageType,
|
||||||
|
type EntityUpdateBody
|
||||||
|
} from '@engine/network';
|
||||||
|
import { stringify, parse } from '@engine/utils';
|
||||||
|
|
||||||
|
class ClientMessageProcessor implements MessageProcessor {
|
||||||
|
private game: Game;
|
||||||
|
|
||||||
|
constructor(game: Game) {
|
||||||
|
this.game = game;
|
||||||
|
}
|
||||||
|
|
||||||
|
public process(message: Message) {
|
||||||
|
switch (message.type) {
|
||||||
|
case MessageType.NEW_ENTITIES:
|
||||||
|
const entityAdditions = message.body as unknown as EntityAddBody[];
|
||||||
|
entityAdditions.forEach((addBody) =>
|
||||||
|
this.game.addEntity(
|
||||||
|
Entity.from(addBody.entityName, addBody.id, addBody.args)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case MessageType.REMOVE_ENTITIES:
|
||||||
|
const ids = message.body as unknown as string[];
|
||||||
|
ids.forEach((id) => this.game.removeEntity(id));
|
||||||
|
break;
|
||||||
|
case MessageType.UPDATE_ENTITIES:
|
||||||
|
const entityUpdates = message.body as unknown as EntityUpdateBody[];
|
||||||
|
entityUpdates.forEach(
|
||||||
|
({ id, args }) => this.game.getEntity(id)?.setFrom(args)
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ClientSocketMessageQueueProvider implements MessageQueueProvider {
|
||||||
|
private socket: WebSocket;
|
||||||
|
private messages: Message[];
|
||||||
|
|
||||||
|
constructor(socket: WebSocket) {
|
||||||
|
this.socket = socket;
|
||||||
|
this.messages = [];
|
||||||
|
|
||||||
|
this.socket.addEventListener('message', (e) => {
|
||||||
|
const messages = parse<Message[]>(e.data);
|
||||||
|
this.messages = this.messages.concat(messages);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public getNewMessages() {
|
||||||
|
return this.messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
public clearMessages() {
|
||||||
|
this.messages = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ClientSocketMessagePublisher implements MessagePublisher {
|
||||||
|
private socket: WebSocket;
|
||||||
|
private messages: Message[];
|
||||||
|
|
||||||
|
constructor(socket: WebSocket) {
|
||||||
|
this.socket = socket;
|
||||||
|
this.messages = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public addMessage(message: Message) {
|
||||||
|
this.messages.push(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public publish() {
|
||||||
|
if (this.socket.readyState == WebSocket.OPEN) {
|
||||||
|
this.messages.forEach((message: Message) =>
|
||||||
|
this.socket.send(stringify(message))
|
||||||
|
);
|
||||||
|
this.messages = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class JumpStorm {
|
export class JumpStorm {
|
||||||
private game: Game;
|
private game: Game;
|
||||||
private socket: WebSocket;
|
private clientId: string;
|
||||||
|
|
||||||
constructor(ctx: CanvasRenderingContext2D) {
|
constructor(game: Game) {
|
||||||
this.game = new Game();
|
this.game = game;
|
||||||
this.socket = new WebSocket("ws://localhost:8080");
|
}
|
||||||
|
|
||||||
|
public async init(
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
httpMethod: string,
|
||||||
|
wsMethod: string,
|
||||||
|
host: string
|
||||||
|
) {
|
||||||
|
this.clientId = await this.getAssignedCookie(
|
||||||
|
`${httpMethod}://${host}/assign`
|
||||||
|
);
|
||||||
|
const socket = new WebSocket(`${wsMethod}://${host}/game`);
|
||||||
|
const clientSocketMessageQueueProvider =
|
||||||
|
new ClientSocketMessageQueueProvider(socket);
|
||||||
|
const clientSocketMessagePublisher = new ClientSocketMessagePublisher(
|
||||||
|
socket
|
||||||
|
);
|
||||||
|
const clientMessageProcessor = new ClientMessageProcessor(this.game);
|
||||||
|
|
||||||
|
const inputSystem = new Input(this.clientId, clientSocketMessagePublisher);
|
||||||
|
this.addWindowEventListenersToInputSystem(inputSystem);
|
||||||
|
|
||||||
|
const grid = new Grid();
|
||||||
|
|
||||||
[
|
[
|
||||||
this.createInputSystem(),
|
new NetworkUpdate(
|
||||||
|
clientSocketMessageQueueProvider,
|
||||||
|
clientSocketMessagePublisher,
|
||||||
|
clientMessageProcessor
|
||||||
|
),
|
||||||
|
inputSystem,
|
||||||
new FacingDirection(),
|
new FacingDirection(),
|
||||||
new Physics(),
|
new Physics(),
|
||||||
new Collision(),
|
new Collision(grid),
|
||||||
new WallBounds(ctx.canvas.width),
|
new WallBounds(),
|
||||||
new Render(ctx),
|
new Render(ctx)
|
||||||
].forEach((system) => this.game.addSystem(system));
|
].forEach((system) => this.game.addSystem(system));
|
||||||
|
|
||||||
[new Floor(160), new Player()].forEach((entity) =>
|
|
||||||
this.game.addEntity(entity),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public play() {
|
public play() {
|
||||||
@ -41,16 +155,26 @@ export class JumpStorm {
|
|||||||
requestAnimationFrame(loop);
|
requestAnimationFrame(loop);
|
||||||
}
|
}
|
||||||
|
|
||||||
private createInputSystem(): Input {
|
private addWindowEventListenersToInputSystem(input: Input) {
|
||||||
const inputSystem = new Input();
|
window.addEventListener('keydown', (e) => {
|
||||||
|
|
||||||
window.addEventListener("keydown", (e) => {
|
|
||||||
if (!e.repeat) {
|
if (!e.repeat) {
|
||||||
inputSystem.keyPressed(e.key);
|
input.keyPressed(e.key.toLowerCase());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
window.addEventListener("keyup", (e) => inputSystem.keyReleased(e.key));
|
|
||||||
|
|
||||||
return inputSystem;
|
window.addEventListener('keyup', (e) =>
|
||||||
|
input.keyReleased(e.key.toLowerCase())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getAssignedCookie(endpoint: string): Promise<string> {
|
||||||
|
return fetch(endpoint)
|
||||||
|
.then((resp) => {
|
||||||
|
if (resp.ok) {
|
||||||
|
return resp.text();
|
||||||
|
}
|
||||||
|
throw resp;
|
||||||
|
})
|
||||||
|
.then((cookie) => cookie);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,24 +1,26 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { loadAssets } from "@engine/config";
|
import { loadAssets } from "@engine/config";
|
||||||
|
import { Game } from "@engine/Game";
|
||||||
import { JumpStorm } from "../JumpStorm";
|
import { JumpStorm } from "../JumpStorm";
|
||||||
|
|
||||||
let canvas: HTMLCanvasElement;
|
let canvas: HTMLCanvasElement;
|
||||||
let ctx: CanvasRenderingContext2D;
|
let ctx: CanvasRenderingContext2D;
|
||||||
|
|
||||||
export let width: number;
|
export let width: number;
|
||||||
export let height: number;
|
export let height: number;
|
||||||
|
|
||||||
let jumpStorm: JumpStorm;
|
onMount(async () => {
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
ctx = canvas.getContext("2d");
|
ctx = canvas.getContext("2d");
|
||||||
ctx.imageSmoothingEnabled = false;
|
ctx.imageSmoothingEnabled = false;
|
||||||
|
|
||||||
loadAssets().then(() => {
|
await loadAssets();
|
||||||
jumpStorm = new JumpStorm(ctx);
|
|
||||||
jumpStorm.play();
|
const game = new Game();
|
||||||
});
|
const jumpStorm = new JumpStorm(game);
|
||||||
|
|
||||||
|
await jumpStorm.init(ctx, "http", "ws", document.location.host + "/api");
|
||||||
|
jumpStorm.play();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
const MAX_ENTRIES = 8;
|
const MAX_ENTRIES = 8;
|
||||||
|
|
||||||
export let entries: { name: string, score: number }[] = [];
|
export let entries: { name: string; score: number }[] = [];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="leaderboard">
|
<div class="leaderboard">
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import App from "./App.svelte";
|
import App from './App.svelte';
|
||||||
|
|
||||||
const app = new App({
|
const app = new App({
|
||||||
target: document.getElementById("app"),
|
target: document.getElementById('app')
|
||||||
});
|
});
|
||||||
|
|
||||||
export default app;
|
export default app;
|
||||||
|
@ -3,10 +3,9 @@
|
|||||||
import LeaderBoard from "../components/LeaderBoard.svelte";
|
import LeaderBoard from "../components/LeaderBoard.svelte";
|
||||||
|
|
||||||
import { Miscellaneous } from "@engine/config";
|
import { Miscellaneous } from "@engine/config";
|
||||||
|
|
||||||
let width: number = Miscellaneous.WIDTH;
|
let width: number = Miscellaneous.WIDTH;
|
||||||
let height: number = Miscellaneous.HEIGHT;
|
let height: number = Miscellaneous.HEIGHT;
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="centered-game">
|
<div class="centered-game">
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
|
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
|
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
|
||||||
// for more information about preprocessors
|
// for more information about preprocessors
|
||||||
preprocess: vitePreprocess(),
|
preprocess: vitePreprocess()
|
||||||
}
|
};
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"extends": "@tsconfig/svelte/tsconfig.json",
|
"extends": ["@tsconfig/svelte/tsconfig.json", "../tsconfig.engine.json"],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
@ -24,8 +24,5 @@
|
|||||||
"src/**/*.js",
|
"src/**/*.js",
|
||||||
"src/**/*.svelte"
|
"src/**/*.svelte"
|
||||||
],
|
],
|
||||||
"paths": {
|
|
||||||
"@engine/*": ["../engine/*"]
|
|
||||||
},
|
|
||||||
"references": [{ "path": "./tsconfig.node.json" }]
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,24 @@
|
|||||||
import { defineConfig } from "vite";
|
import { defineConfig } from 'vite';
|
||||||
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
||||||
import { fileURLToPath, URL } from "node:url";
|
import { fileURLToPath, URL } from 'node:url';
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
server: {
|
||||||
|
host: '0.0.0.0',
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://10.0.0.237:8080',
|
||||||
|
ws: true,
|
||||||
|
rewrite: (path) => path.replace(/^\/api/, '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cors: true,
|
||||||
plugins: [svelte()],
|
plugins: [svelte()],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
"@engine": fileURLToPath(new URL("../engine", import.meta.url)),
|
'@engine': fileURLToPath(new URL('../engine', import.meta.url))
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Entity } from "./entities";
|
import { Entity } from './entities';
|
||||||
import { System } from "./systems";
|
import { System } from './systems';
|
||||||
|
|
||||||
export class Game {
|
export class Game {
|
||||||
private systemOrder: string[];
|
private systemOrder: string[];
|
||||||
@ -7,9 +7,9 @@ export class Game {
|
|||||||
private running: boolean;
|
private running: boolean;
|
||||||
private lastTimeStamp: number;
|
private lastTimeStamp: number;
|
||||||
|
|
||||||
public entities: Map<number, Entity>;
|
public entities: Map<string, Entity>;
|
||||||
public systems: Map<string, System>;
|
public systems: Map<string, System>;
|
||||||
public componentEntities: Map<string, Set<number>>;
|
public componentEntities: Map<string, Set<string>>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.lastTimeStamp = performance.now();
|
this.lastTimeStamp = performance.now();
|
||||||
@ -29,17 +29,17 @@ export class Game {
|
|||||||
this.entities.set(entity.id, entity);
|
this.entities.set(entity.id, entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getEntity(id: number): Entity | undefined {
|
public getEntity(id: string): Entity | undefined {
|
||||||
return this.entities.get(id);
|
return this.entities.get(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public removeEntity(id: number) {
|
public removeEntity(id: string) {
|
||||||
this.entities.delete(id);
|
this.entities.delete(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public forEachEntityWithComponent(
|
public forEachEntityWithComponent(
|
||||||
componentName: string,
|
componentName: string,
|
||||||
callback: (entity: Entity) => void,
|
callback: (entity: Entity) => void
|
||||||
) {
|
) {
|
||||||
this.componentEntities.get(componentName)?.forEach((entityId) => {
|
this.componentEntities.get(componentName)?.forEach((entityId) => {
|
||||||
const entity = this.getEntity(entityId);
|
const entity = this.getEntity(entityId);
|
||||||
@ -60,7 +60,7 @@ export class Game {
|
|||||||
return this.systems.get(name);
|
return this.systems.get(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
public doGameLoop = (timeStamp: number) => {
|
public doGameLoop(timeStamp: number) {
|
||||||
if (!this.running) {
|
if (!this.running) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -75,16 +75,16 @@ export class Game {
|
|||||||
if (!this.componentEntities.has(component.name)) {
|
if (!this.componentEntities.has(component.name)) {
|
||||||
this.componentEntities.set(
|
this.componentEntities.set(
|
||||||
component.name,
|
component.name,
|
||||||
new Set<number>([entity.id]),
|
new Set<string>([entity.id])
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.componentEntities.get(component.name)?.add(entity.id);
|
this.componentEntities.get(component.name)?.add(entity.id);
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
this.systemOrder.forEach((systemName) => {
|
this.systemOrder.forEach((systemName) => {
|
||||||
this.systems.get(systemName)?.update(dt, this);
|
this.systems.get(systemName)?.update(dt, this);
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Component, ComponentNames } from ".";
|
import { Component, ComponentNames } from '.';
|
||||||
import type { Coord2D, Dimension2D } from "../interfaces";
|
import type { Coord2D, Dimension2D } from '../interfaces';
|
||||||
import { dotProduct, rotateVector } from "../utils";
|
import { dotProduct, rotateVector } from '../utils';
|
||||||
|
|
||||||
export class BoundingBox extends Component {
|
export class BoundingBox extends Component {
|
||||||
public center: Coord2D;
|
public center: Coord2D;
|
||||||
@ -15,8 +15,27 @@ export class BoundingBox extends Component {
|
|||||||
this.rotation = rotation ?? 0;
|
this.rotation = rotation ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://en.wikipedia.org/wiki/Hyperplane_separation_theorem
|
|
||||||
public isCollidingWith(box: BoundingBox): boolean {
|
public isCollidingWith(box: BoundingBox): boolean {
|
||||||
|
if (this.rotation == 0 && box.rotation == 0) {
|
||||||
|
const thisTopLeft = this.getTopLeft();
|
||||||
|
const thisBottomRight = this.getBottomRight();
|
||||||
|
|
||||||
|
const thatTopLeft = box.getTopLeft();
|
||||||
|
const thatBottomRight = box.getBottomRight();
|
||||||
|
|
||||||
|
if (
|
||||||
|
thisBottomRight.x <= thatTopLeft.x ||
|
||||||
|
thisTopLeft.x >= thatBottomRight.x ||
|
||||||
|
thisBottomRight.y <= thatTopLeft.y ||
|
||||||
|
thisTopLeft.y >= thatBottomRight.y
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://en.wikipedia.org/wiki/Hyperplane_separation_theorem
|
||||||
const boxes = [this.getVertices(), box.getVertices()];
|
const boxes = [this.getVertices(), box.getVertices()];
|
||||||
for (const poly of boxes) {
|
for (const poly of boxes) {
|
||||||
for (let i = 0; i < poly.length; i++) {
|
for (let i = 0; i < poly.length; i++) {
|
||||||
@ -29,8 +48,8 @@ export class BoundingBox extends Component {
|
|||||||
const projection = dotProduct(normal, vertex);
|
const projection = dotProduct(normal, vertex);
|
||||||
return [Math.min(min, projection), Math.max(max, projection)];
|
return [Math.min(min, projection), Math.max(max, projection)];
|
||||||
},
|
},
|
||||||
[Infinity, -Infinity],
|
[Infinity, -Infinity]
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (maxThis < minBox || maxBox < minThis) return false;
|
if (maxThis < minBox || maxBox < minThis) return false;
|
||||||
@ -45,20 +64,22 @@ export class BoundingBox extends Component {
|
|||||||
{ x: -this.dimension.width / 2, y: -this.dimension.height / 2 },
|
{ x: -this.dimension.width / 2, y: -this.dimension.height / 2 },
|
||||||
{ x: -this.dimension.width / 2, y: this.dimension.height / 2 },
|
{ x: -this.dimension.width / 2, y: this.dimension.height / 2 },
|
||||||
{ x: this.dimension.width / 2, y: this.dimension.height / 2 },
|
{ x: this.dimension.width / 2, y: this.dimension.height / 2 },
|
||||||
{ x: this.dimension.width / 2, y: -this.dimension.height / 2 },
|
{ x: this.dimension.width / 2, y: -this.dimension.height / 2 }
|
||||||
]
|
]
|
||||||
.map((vertex) => rotateVector(vertex, this.rotation))
|
.map((vertex) => rotateVector(vertex, this.rotation)) // rotate
|
||||||
.map((vertex) => {
|
.map((vertex) => {
|
||||||
|
// translate
|
||||||
return {
|
return {
|
||||||
x: vertex.x + this.center.x,
|
x: vertex.x + this.center.x,
|
||||||
y: vertex.y + this.center.y,
|
y: vertex.y + this.center.y
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public getRotationInPiOfUnitCircle() {
|
public getRotationInPiOfUnitCircle(): number {
|
||||||
let rads = this.rotation * (Math.PI / 180);
|
let rads = this.rotation * (Math.PI / 180);
|
||||||
if (rads >= Math.PI) {
|
if (rads >= Math.PI) {
|
||||||
|
// Physics system guarantees rotation \in [0, 360)
|
||||||
rads -= Math.PI;
|
rads -= Math.PI;
|
||||||
}
|
}
|
||||||
return rads;
|
return rads;
|
||||||
@ -68,17 +89,33 @@ export class BoundingBox extends Component {
|
|||||||
let rads = this.getRotationInPiOfUnitCircle();
|
let rads = this.getRotationInPiOfUnitCircle();
|
||||||
const { width, height } = this.dimension;
|
const { width, height } = this.dimension;
|
||||||
|
|
||||||
|
if (rads == 0) return this.dimension;
|
||||||
|
|
||||||
if (rads <= Math.PI / 2) {
|
if (rads <= Math.PI / 2) {
|
||||||
return {
|
return {
|
||||||
width: Math.abs(height * Math.sin(rads) + width * Math.cos(rads)),
|
width: Math.abs(height * Math.sin(rads) + width * Math.cos(rads)),
|
||||||
height: Math.abs(width * Math.sin(rads) + height * Math.cos(rads)),
|
height: Math.abs(width * Math.sin(rads) + height * Math.cos(rads))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
rads -= Math.PI / 2;
|
rads -= Math.PI / 2;
|
||||||
return {
|
return {
|
||||||
width: Math.abs(height * Math.cos(rads) + width * Math.sin(rads)),
|
width: Math.abs(height * Math.cos(rads) + width * Math.sin(rads)),
|
||||||
height: Math.abs(width * Math.cos(rads) + height * Math.sin(rads)),
|
height: Math.abs(width * Math.cos(rads) + height * Math.sin(rads))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public getTopLeft(): Coord2D {
|
||||||
|
return {
|
||||||
|
x: this.center.x - this.dimension.width / 2,
|
||||||
|
y: this.center.y - this.dimension.height / 2
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public getBottomRight(): Coord2D {
|
||||||
|
return {
|
||||||
|
x: this.center.x + this.dimension.width / 2,
|
||||||
|
y: this.center.y + this.dimension.height / 2
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Component, ComponentNames } from ".";
|
import { Component, ComponentNames } from '.';
|
||||||
|
|
||||||
export class Collide extends Component {
|
export class Collide extends Component {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -1,11 +1,18 @@
|
|||||||
import { Component, ComponentNames, Velocity } from ".";
|
import { Component, ComponentNames, Velocity } from '.';
|
||||||
|
|
||||||
export class Control extends Component {
|
export class Control extends Component {
|
||||||
public controlVelocity: Velocity;
|
public controlVelocityComponent: Velocity;
|
||||||
|
public controllableBy: string;
|
||||||
|
public isControllable: boolean; // computed each update in the input system
|
||||||
|
|
||||||
constructor(controlVelocity: Velocity = new Velocity()) {
|
constructor(
|
||||||
|
controllableBy: string,
|
||||||
|
controlVelocityComponent: Velocity = new Velocity()
|
||||||
|
) {
|
||||||
super(ComponentNames.Control);
|
super(ComponentNames.Control);
|
||||||
|
|
||||||
this.controlVelocity = controlVelocity;
|
this.controllableBy = controllableBy;
|
||||||
|
this.controlVelocityComponent = controlVelocityComponent;
|
||||||
|
this.isControllable = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Component, ComponentNames, Sprite } from ".";
|
import { Component, ComponentNames, Sprite } from '.';
|
||||||
|
|
||||||
export class FacingDirection extends Component {
|
export class FacingDirection extends Component {
|
||||||
public readonly facingLeftSprite: Sprite;
|
public readonly facingLeftSprite: Sprite;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import type { Force2D } from "../interfaces";
|
import type { Force2D } from '../interfaces';
|
||||||
import { Component } from "./Component";
|
import { Component } from './Component';
|
||||||
import { ComponentNames } from ".";
|
import { ComponentNames } from '.';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A list of forces and torque, (in newtons, and newton-meters respectively)
|
* A list of forces and torque, (in newtons, and newton-meters respectively)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { ComponentNames, Component } from ".";
|
import { ComponentNames, Component } from '.';
|
||||||
|
|
||||||
export class Gravity extends Component {
|
export class Gravity extends Component {
|
||||||
private static DEFAULT_TERMINAL_VELOCITY = 5;
|
private static DEFAULT_TERMINAL_VELOCITY = 4.5;
|
||||||
|
|
||||||
public terminalVelocity: number;
|
public terminalVelocity: number;
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Component, ComponentNames } from ".";
|
import { Component, ComponentNames } from '.';
|
||||||
|
|
||||||
export class Jump extends Component {
|
export class Jump extends Component {
|
||||||
public canJump: boolean;
|
public canJump: boolean;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Component, ComponentNames } from ".";
|
import { Component, ComponentNames } from '.';
|
||||||
|
|
||||||
export class Mass extends Component {
|
export class Mass extends Component {
|
||||||
public mass: number;
|
public mass: number;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Component, ComponentNames } from ".";
|
import { Component, ComponentNames } from '.';
|
||||||
|
|
||||||
export class Moment extends Component {
|
export class Moment extends Component {
|
||||||
public inertia: number;
|
public inertia: number;
|
||||||
|
7
engine/components/NetworkUpdateable.ts
Normal file
7
engine/components/NetworkUpdateable.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { Component, ComponentNames } from '.';
|
||||||
|
|
||||||
|
export class NetworkUpdateable extends Component {
|
||||||
|
constructor() {
|
||||||
|
super(ComponentNames.NetworkUpdateable);
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
import { Component, ComponentNames } from ".";
|
import { Component, ComponentNames } from '.';
|
||||||
import type { Dimension2D, DrawArgs, Coord2D } from "../interfaces";
|
import type { Dimension2D, DrawArgs, Coord2D } from '../interfaces';
|
||||||
|
|
||||||
export class Sprite extends Component {
|
export class Sprite extends Component {
|
||||||
private sheet: HTMLImageElement;
|
private sheet: HTMLImageElement;
|
||||||
@ -17,7 +17,7 @@ export class Sprite extends Component {
|
|||||||
spriteImgPos: Coord2D,
|
spriteImgPos: Coord2D,
|
||||||
spriteImgDimensions: Dimension2D,
|
spriteImgDimensions: Dimension2D,
|
||||||
msPerFrame: number,
|
msPerFrame: number,
|
||||||
numFrames: number,
|
numFrames: number
|
||||||
) {
|
) {
|
||||||
super(ComponentNames.Sprite);
|
super(ComponentNames.Sprite);
|
||||||
|
|
||||||
@ -56,12 +56,12 @@ export class Sprite extends Component {
|
|||||||
ctx.drawImage(
|
ctx.drawImage(
|
||||||
this.sheet,
|
this.sheet,
|
||||||
...this.getSpriteArgs(),
|
...this.getSpriteArgs(),
|
||||||
...this.getDrawArgs(drawArgs),
|
...this.getDrawArgs(drawArgs)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (tint) {
|
if (tint) {
|
||||||
ctx.globalAlpha = 0.5;
|
ctx.globalAlpha = 0.5;
|
||||||
ctx.globalCompositeOperation = "source-atop";
|
ctx.globalCompositeOperation = 'source-atop';
|
||||||
ctx.fillStyle = tint;
|
ctx.fillStyle = tint;
|
||||||
ctx.fillRect(...this.getDrawArgs(drawArgs));
|
ctx.fillRect(...this.getDrawArgs(drawArgs));
|
||||||
}
|
}
|
||||||
@ -74,19 +74,23 @@ export class Sprite extends Component {
|
|||||||
this.spriteImgPos.x + this.currentFrame * this.spriteImgDimensions.width,
|
this.spriteImgPos.x + this.currentFrame * this.spriteImgDimensions.width,
|
||||||
this.spriteImgPos.y,
|
this.spriteImgPos.y,
|
||||||
this.spriteImgDimensions.width,
|
this.spriteImgDimensions.width,
|
||||||
this.spriteImgDimensions.height,
|
this.spriteImgDimensions.height
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private getDrawArgs({
|
private getDrawArgs({
|
||||||
center,
|
center,
|
||||||
dimension,
|
dimension
|
||||||
}: DrawArgs): [dx: number, dy: number, dw: number, dh: number] {
|
}: DrawArgs): [dx: number, dy: number, dw: number, dh: number] {
|
||||||
return [
|
return [
|
||||||
center.x - dimension.width / 2,
|
center.x - dimension.width / 2,
|
||||||
center.y - dimension.height / 2,
|
center.y - dimension.height / 2,
|
||||||
dimension.width,
|
dimension.width,
|
||||||
dimension.height,
|
dimension.height
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getSpriteDimensions() {
|
||||||
|
return this.spriteImgDimensions;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Component, ComponentNames } from ".";
|
import { Component, ComponentNames } from '.';
|
||||||
|
|
||||||
export class TopCollidable extends Component {
|
export class TopCollidable extends Component {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -1,23 +1,23 @@
|
|||||||
import type { Velocity2D } from "../interfaces";
|
import type { Velocity2D } from '../interfaces';
|
||||||
import { Component } from "./Component";
|
import { Component } from './Component';
|
||||||
import { ComponentNames } from ".";
|
import { ComponentNames } from '.';
|
||||||
|
|
||||||
export class Velocity extends Component {
|
export class Velocity extends Component {
|
||||||
public dCartesian: Velocity2D;
|
public velocity: Velocity2D;
|
||||||
public dTheta: number;
|
|
||||||
|
|
||||||
constructor(dCartesian: Velocity2D = { dx: 0, dy: 0 }, dTheta: number = 0) {
|
constructor(
|
||||||
|
velocity: Velocity2D = { dCartesian: { dx: 0, dy: 0 }, dTheta: 0 }
|
||||||
|
) {
|
||||||
super(ComponentNames.Velocity);
|
super(ComponentNames.Velocity);
|
||||||
|
|
||||||
this.dCartesian = dCartesian;
|
this.velocity = velocity;
|
||||||
this.dTheta = dTheta;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public add(velocity?: Velocity) {
|
public add(velocity?: Velocity2D) {
|
||||||
if (velocity) {
|
if (velocity) {
|
||||||
this.dCartesian.dx += velocity.dCartesian.dx;
|
this.velocity.dCartesian.dx += velocity.dCartesian.dx;
|
||||||
this.dCartesian.dy += velocity.dCartesian.dy;
|
this.velocity.dCartesian.dy += velocity.dCartesian.dy;
|
||||||
this.dTheta += velocity.dTheta;
|
this.velocity.dTheta += velocity.dTheta;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Component, ComponentNames } from ".";
|
import { Component, ComponentNames } from '.';
|
||||||
|
|
||||||
export class WallBounded extends Component {
|
export class WallBounded extends Component {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
export * from "./Component";
|
export * from './Component';
|
||||||
export * from "./BoundingBox";
|
export * from './BoundingBox';
|
||||||
export * from "./Velocity";
|
export * from './Velocity';
|
||||||
export * from "./Forces";
|
export * from './Forces';
|
||||||
export * from "./Sprite";
|
export * from './Sprite';
|
||||||
export * from "./FacingDirection";
|
export * from './FacingDirection';
|
||||||
export * from "./Jump";
|
export * from './Jump';
|
||||||
export * from "./TopCollidable";
|
export * from './TopCollidable';
|
||||||
export * from "./Collide";
|
export * from './Collide';
|
||||||
export * from "./Control";
|
export * from './Control';
|
||||||
export * from "./WallBounded";
|
export * from './WallBounded';
|
||||||
export * from "./Gravity";
|
export * from './Gravity';
|
||||||
export * from "./Mass";
|
export * from './Mass';
|
||||||
export * from "./Moment";
|
export * from './Moment';
|
||||||
export * from "./names";
|
export * from './NetworkUpdateable';
|
||||||
|
export * from './names';
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
export namespace ComponentNames {
|
export namespace ComponentNames {
|
||||||
export const Sprite = "Sprite";
|
export const Sprite = 'Sprite';
|
||||||
export const BoundingBox = "BoundingBox";
|
export const BoundingBox = 'BoundingBox';
|
||||||
export const Velocity = "Velocity";
|
export const Velocity = 'Velocity';
|
||||||
export const FacingDirection = "FacingDirection";
|
export const FacingDirection = 'FacingDirection';
|
||||||
export const Control = "Control";
|
export const Control = 'Control';
|
||||||
export const Jump = "Jump";
|
export const Jump = 'Jump';
|
||||||
export const TopCollidable = "TopCollidable";
|
export const TopCollidable = 'TopCollidable';
|
||||||
export const Collide = "Collide";
|
export const Collide = 'Collide';
|
||||||
export const WallBounded = "WallBounded";
|
export const WallBounded = 'WallBounded';
|
||||||
export const Gravity = "Gravity";
|
export const Gravity = 'Gravity';
|
||||||
export const Forces = "Forces";
|
export const Forces = 'Forces';
|
||||||
export const Mass = "Mass";
|
export const Mass = 'Mass';
|
||||||
export const Moment = "Moment";
|
export const Moment = 'Moment';
|
||||||
|
export const NetworkUpdateable = 'NetworkUpdateable';
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import type { SpriteSpec } from "./sprites";
|
import type { SpriteSpec } from './sprites';
|
||||||
import { SPRITE_SPECS } from "./sprites";
|
import { SPRITE_SPECS } from './sprites';
|
||||||
|
|
||||||
export const IMAGES = new Map<string, HTMLImageElement>();
|
export const IMAGES = new Map<string, HTMLImageElement>();
|
||||||
|
|
||||||
export const loadSpritesIntoImageElements = (
|
export const loadSpritesIntoImageElements = (
|
||||||
spriteSpecs: Partial<SpriteSpec>[],
|
spriteSpecs: Partial<SpriteSpec>[]
|
||||||
): Promise<void>[] => {
|
): Promise<void>[] => {
|
||||||
const spritePromises: Promise<void>[] = [];
|
const spritePromises: Promise<void>[] = [];
|
||||||
|
|
||||||
@ -17,13 +17,13 @@ export const loadSpritesIntoImageElements = (
|
|||||||
spritePromises.push(
|
spritePromises.push(
|
||||||
new Promise((resolve) => {
|
new Promise((resolve) => {
|
||||||
img.onload = () => resolve();
|
img.onload = () => resolve();
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (spriteSpec.states) {
|
if (spriteSpec.states) {
|
||||||
spritePromises.push(
|
spritePromises.push(
|
||||||
...loadSpritesIntoImageElements(Array.from(spriteSpec.states.values())),
|
...loadSpritesIntoImageElements(Array.from(spriteSpec.states.values()))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -35,8 +35,8 @@ export const loadAssets = () =>
|
|||||||
Promise.all([
|
Promise.all([
|
||||||
...loadSpritesIntoImageElements(
|
...loadSpritesIntoImageElements(
|
||||||
Array.from(SPRITE_SPECS.keys()).map(
|
Array.from(SPRITE_SPECS.keys()).map(
|
||||||
(key) => SPRITE_SPECS.get(key) as SpriteSpec,
|
(key) => SPRITE_SPECS.get(key) as SpriteSpec
|
||||||
),
|
)
|
||||||
),
|
)
|
||||||
// TODO: Sound
|
// TODO: Sound
|
||||||
]);
|
]);
|
||||||
|
@ -1,34 +1,39 @@
|
|||||||
import { Action } from "../interfaces";
|
import { Action } from '../interfaces';
|
||||||
|
|
||||||
export namespace KeyConstants {
|
export namespace KeyConstants {
|
||||||
export const KeyActions: Record<string, Action> = {
|
export const KeyActions: Record<string, Action> = {
|
||||||
a: Action.MOVE_LEFT,
|
a: Action.MOVE_LEFT,
|
||||||
ArrowLeft: Action.MOVE_LEFT,
|
arrowleft: Action.MOVE_LEFT,
|
||||||
|
|
||||||
d: Action.MOVE_RIGHT,
|
d: Action.MOVE_RIGHT,
|
||||||
ArrowRight: Action.MOVE_RIGHT,
|
arrowright: Action.MOVE_RIGHT,
|
||||||
|
|
||||||
w: Action.JUMP,
|
w: Action.JUMP,
|
||||||
ArrowUp: Action.JUMP,
|
arrowup: Action.JUMP,
|
||||||
|
|
||||||
|
' ': Action.JUMP
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// value -> [key] from KeyActions
|
||||||
export const ActionKeys: Map<Action, string[]> = Object.keys(
|
export const ActionKeys: Map<Action, string[]> = Object.keys(
|
||||||
KeyActions,
|
KeyActions
|
||||||
).reduce((acc: Map<Action, string[]>, key) => {
|
).reduce((acc: Map<Action, string[]>, key) => {
|
||||||
const action = KeyActions[key];
|
const action = KeyActions[key.toLowerCase()];
|
||||||
|
|
||||||
if (acc.has(action)) {
|
if (acc.has(action)) {
|
||||||
acc.get(action)?.push(key);
|
acc.get(action)!.push(key);
|
||||||
return acc;
|
return acc;
|
||||||
}
|
}
|
||||||
|
|
||||||
acc.set(action, [key]);
|
acc.set(action, [key]);
|
||||||
return acc;
|
return acc;
|
||||||
}, new Map<Action, string[]>());
|
}, new Map());
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace PhysicsConstants {
|
export namespace PhysicsConstants {
|
||||||
export const MAX_JUMP_TIME_MS = 150;
|
export const MAX_JUMP_TIME_MS = 150;
|
||||||
export const GRAVITY = 0.0075;
|
export const GRAVITY = 0.0075;
|
||||||
export const PLAYER_MOVE_VEL = 1;
|
export const PLAYER_MOVE_VEL = 0.8;
|
||||||
export const PLAYER_JUMP_ACC = -0.008;
|
export const PLAYER_JUMP_ACC = -0.008;
|
||||||
export const PLAYER_JUMP_INITIAL_VEL = -1;
|
export const PLAYER_JUMP_INITIAL_VEL = -1;
|
||||||
}
|
}
|
||||||
@ -36,4 +41,7 @@ export namespace PhysicsConstants {
|
|||||||
export namespace Miscellaneous {
|
export namespace Miscellaneous {
|
||||||
export const WIDTH = 600;
|
export const WIDTH = 600;
|
||||||
export const HEIGHT = 800;
|
export const HEIGHT = 800;
|
||||||
|
|
||||||
|
export const DEFAULT_GRID_WIDTH = 30;
|
||||||
|
export const DEFAULT_GRID_HEIGHT = 30;
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
export * from "./constants";
|
export * from './constants';
|
||||||
export * from "./assets.ts";
|
export * from './assets.ts';
|
||||||
export * from "./sprites.ts";
|
export * from './sprites.ts';
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
export enum Sprites {
|
export enum Sprites {
|
||||||
FLOOR,
|
FLOOR,
|
||||||
TRAMPOLINE,
|
TRAMPOLINE,
|
||||||
COFFEE,
|
COFFEE
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SpriteSpec {
|
export interface SpriteSpec {
|
||||||
@ -22,12 +22,12 @@ const floorSpriteSpec = {
|
|||||||
height: 40,
|
height: 40,
|
||||||
frames: 3,
|
frames: 3,
|
||||||
msPerFrame: 125,
|
msPerFrame: 125,
|
||||||
states: new Map<number, Partial<SpriteSpec>>(),
|
states: new Map<number, Partial<SpriteSpec>>()
|
||||||
};
|
};
|
||||||
[40, 80, 120, 160].forEach((width) => {
|
[40, 80, 120, 160].forEach((width) => {
|
||||||
floorSpriteSpec.states.set(width, {
|
floorSpriteSpec.states.set(width, {
|
||||||
width,
|
width,
|
||||||
sheet: `/assets/floor_tile_${width}.png`,
|
sheet: `/assets/floor_tile_${width}.png`
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
SPRITE_SPECS.set(Sprites.FLOOR, floorSpriteSpec);
|
SPRITE_SPECS.set(Sprites.FLOOR, floorSpriteSpec);
|
||||||
@ -37,12 +37,12 @@ const coffeeSpriteSpec = {
|
|||||||
width: 60,
|
width: 60,
|
||||||
height: 45,
|
height: 45,
|
||||||
frames: 3,
|
frames: 3,
|
||||||
states: new Map<string, Partial<SpriteSpec>>(),
|
states: new Map<string, Partial<SpriteSpec>>()
|
||||||
};
|
};
|
||||||
coffeeSpriteSpec.states.set("LEFT", {
|
coffeeSpriteSpec.states.set('LEFT', {
|
||||||
sheet: "/assets/coffee_left.png",
|
sheet: '/assets/coffee_left.png'
|
||||||
});
|
});
|
||||||
coffeeSpriteSpec.states.set("RIGHT", {
|
coffeeSpriteSpec.states.set('RIGHT', {
|
||||||
sheet: "/assets/coffee_right.png",
|
sheet: '/assets/coffee_right.png'
|
||||||
});
|
});
|
||||||
SPRITE_SPECS.set(Sprites.COFFEE, coffeeSpriteSpec);
|
SPRITE_SPECS.set(Sprites.COFFEE, coffeeSpriteSpec);
|
||||||
|
@ -1,13 +1,17 @@
|
|||||||
import type { Component } from "../components";
|
import { EntityNames, Floor, Player } from '.';
|
||||||
|
import { type Component } from '../components';
|
||||||
|
|
||||||
|
const randomId = () =>
|
||||||
|
(performance.now() + Math.random() * 10_000_000).toString();
|
||||||
|
|
||||||
export abstract class Entity {
|
export abstract class Entity {
|
||||||
private static ID = 0;
|
public id: string;
|
||||||
|
public components: Map<string, Component>;
|
||||||
|
public name: string;
|
||||||
|
|
||||||
public readonly id: number;
|
constructor(name: string, id: string = randomId()) {
|
||||||
public readonly components: Map<string, Component>;
|
this.name = name;
|
||||||
|
this.id = id;
|
||||||
constructor() {
|
|
||||||
this.id = Entity.ID++;
|
|
||||||
this.components = new Map();
|
this.components = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -17,7 +21,7 @@ export abstract class Entity {
|
|||||||
|
|
||||||
public getComponent<T extends Component>(name: string): T {
|
public getComponent<T extends Component>(name: string): T {
|
||||||
if (!this.hasComponent(name)) {
|
if (!this.hasComponent(name)) {
|
||||||
throw new Error("Entity does not have component " + name);
|
throw new Error('Entity does not have component ' + name);
|
||||||
}
|
}
|
||||||
return this.components.get(name) as T;
|
return this.components.get(name) as T;
|
||||||
}
|
}
|
||||||
@ -29,4 +33,30 @@ export abstract class Entity {
|
|||||||
public hasComponent(name: string): boolean {
|
public hasComponent(name: string): boolean {
|
||||||
return this.components.has(name);
|
return this.components.has(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static from(entityName: string, id: string, args: any): Entity {
|
||||||
|
let entity: Entity;
|
||||||
|
|
||||||
|
switch (entityName) {
|
||||||
|
case EntityNames.Player:
|
||||||
|
const player = new Player();
|
||||||
|
player.setFrom(args);
|
||||||
|
entity = player;
|
||||||
|
break;
|
||||||
|
case EntityNames.Floor:
|
||||||
|
const floor = new Floor(args.floorWidth);
|
||||||
|
floor.setFrom(args);
|
||||||
|
entity = floor;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error('.from() Entity type not implemented: ' + entityName);
|
||||||
|
}
|
||||||
|
|
||||||
|
entity.id = id;
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract setFrom(args: Record<string, any>): void;
|
||||||
|
|
||||||
|
public abstract serialize(): Record<string, any>;
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,19 @@
|
|||||||
import { IMAGES, SPRITE_SPECS, Sprites, type SpriteSpec } from "../config";
|
import { IMAGES, SPRITE_SPECS, Sprites, type SpriteSpec } from '../config';
|
||||||
import { BoundingBox, Sprite } from "../components";
|
import { BoundingBox, ComponentNames, Sprite } from '../components';
|
||||||
import { TopCollidable } from "../components/TopCollidable";
|
import { TopCollidable } from '../components/TopCollidable';
|
||||||
import { Entity } from "../entities";
|
import { Entity, EntityNames } from '../entities';
|
||||||
|
|
||||||
export class Floor extends Entity {
|
export class Floor extends Entity {
|
||||||
private static spriteSpec: SpriteSpec = SPRITE_SPECS.get(
|
private static spriteSpec: SpriteSpec = SPRITE_SPECS.get(
|
||||||
Sprites.FLOOR,
|
Sprites.FLOOR
|
||||||
) as SpriteSpec;
|
) as SpriteSpec;
|
||||||
|
|
||||||
|
private width: number;
|
||||||
|
|
||||||
constructor(width: number) {
|
constructor(width: number) {
|
||||||
super();
|
super(EntityNames.Floor);
|
||||||
|
|
||||||
|
this.width = width;
|
||||||
|
|
||||||
this.addComponent(
|
this.addComponent(
|
||||||
new Sprite(
|
new Sprite(
|
||||||
@ -17,17 +21,28 @@ export class Floor extends Entity {
|
|||||||
{ x: 0, y: 0 },
|
{ x: 0, y: 0 },
|
||||||
{ width, height: Floor.spriteSpec.height },
|
{ width, height: Floor.spriteSpec.height },
|
||||||
Floor.spriteSpec.msPerFrame,
|
Floor.spriteSpec.msPerFrame,
|
||||||
Floor.spriteSpec.frames,
|
Floor.spriteSpec.frames
|
||||||
),
|
)
|
||||||
);
|
|
||||||
|
|
||||||
this.addComponent(
|
|
||||||
new BoundingBox(
|
|
||||||
{ x: 300, y: 300 },
|
|
||||||
{ width, height: Floor.spriteSpec.height },
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
this.addComponent(new TopCollidable());
|
this.addComponent(new TopCollidable());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public serialize() {
|
||||||
|
return {
|
||||||
|
floorWidth: this.width,
|
||||||
|
boundingBox: this.getComponent<BoundingBox>(ComponentNames.BoundingBox)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public setFrom(args: any) {
|
||||||
|
const { boundingBox } = args;
|
||||||
|
this.addComponent(
|
||||||
|
new BoundingBox(
|
||||||
|
boundingBox.center,
|
||||||
|
boundingBox.dimension,
|
||||||
|
boundingBox.rotation
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Entity } from ".";
|
import { Entity, EntityNames } from '.';
|
||||||
import { IMAGES, SPRITE_SPECS, Sprites, type SpriteSpec } from "../config";
|
import { IMAGES, SPRITE_SPECS, Sprites, type SpriteSpec } from '../config';
|
||||||
import {
|
import {
|
||||||
Jump,
|
Jump,
|
||||||
FacingDirection,
|
FacingDirection,
|
||||||
@ -10,32 +10,38 @@ import {
|
|||||||
WallBounded,
|
WallBounded,
|
||||||
Forces,
|
Forces,
|
||||||
Collide,
|
Collide,
|
||||||
Control,
|
|
||||||
Mass,
|
Mass,
|
||||||
Moment,
|
Moment,
|
||||||
} from "../components";
|
ComponentNames,
|
||||||
import { Direction } from "../interfaces";
|
Control
|
||||||
|
} from '../components';
|
||||||
|
import { Direction } from '../interfaces';
|
||||||
|
|
||||||
export class Player extends Entity {
|
export class Player extends Entity {
|
||||||
private static MASS: number = 10;
|
private static MASS: number = 10;
|
||||||
private static MOI: number = 1000;
|
private static MOI: number = 100;
|
||||||
|
|
||||||
private static spriteSpec: SpriteSpec = SPRITE_SPECS.get(
|
private static spriteSpec: SpriteSpec = SPRITE_SPECS.get(
|
||||||
Sprites.COFFEE,
|
Sprites.COFFEE
|
||||||
) as SpriteSpec;
|
) as SpriteSpec;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super(EntityNames.Player);
|
||||||
|
|
||||||
this.addComponent(
|
this.addComponent(
|
||||||
new BoundingBox(
|
new BoundingBox(
|
||||||
{ x: 300, y: 100 },
|
{
|
||||||
|
x: 0,
|
||||||
|
y: 0
|
||||||
|
},
|
||||||
{ width: Player.spriteSpec.width, height: Player.spriteSpec.height },
|
{ width: Player.spriteSpec.width, height: Player.spriteSpec.height },
|
||||||
0,
|
0
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
this.addComponent(new Velocity({ dx: 0, dy: 0 }, 0));
|
this.addComponent(
|
||||||
|
new Velocity({ dCartesian: { dx: 0, dy: 0 }, dTheta: 0 })
|
||||||
|
);
|
||||||
|
|
||||||
this.addComponent(new Mass(Player.MASS));
|
this.addComponent(new Mass(Player.MASS));
|
||||||
this.addComponent(new Moment(Player.MOI));
|
this.addComponent(new Moment(Player.MOI));
|
||||||
@ -43,7 +49,6 @@ export class Player extends Entity {
|
|||||||
this.addComponent(new Gravity());
|
this.addComponent(new Gravity());
|
||||||
|
|
||||||
this.addComponent(new Jump());
|
this.addComponent(new Jump());
|
||||||
this.addComponent(new Control());
|
|
||||||
|
|
||||||
this.addComponent(new Collide());
|
this.addComponent(new Collide());
|
||||||
this.addComponent(new WallBounded());
|
this.addComponent(new WallBounded());
|
||||||
@ -59,11 +64,41 @@ export class Player extends Entity {
|
|||||||
{ x: 0, y: 0 },
|
{ x: 0, y: 0 },
|
||||||
{ width: Player.spriteSpec.width, height: Player.spriteSpec.height },
|
{ width: Player.spriteSpec.width, height: Player.spriteSpec.height },
|
||||||
Player.spriteSpec.msPerFrame,
|
Player.spriteSpec.msPerFrame,
|
||||||
Player.spriteSpec.frames,
|
Player.spriteSpec.frames
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
this.addComponent(new FacingDirection(leftSprite, rightSprite));
|
this.addComponent(new FacingDirection(leftSprite, rightSprite));
|
||||||
this.addComponent(leftSprite); // face Left by default
|
this.addComponent(leftSprite); // face left by default
|
||||||
|
}
|
||||||
|
|
||||||
|
public serialize(): Record<string, any> {
|
||||||
|
return {
|
||||||
|
control: this.getComponent<Control>(ComponentNames.Control),
|
||||||
|
boundingBox: this.getComponent<BoundingBox>(ComponentNames.BoundingBox),
|
||||||
|
velocity: this.getComponent<Velocity>(ComponentNames.Velocity),
|
||||||
|
forces: this.getComponent<Forces>(ComponentNames.Forces)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public setFrom(args: Record<string, any>) {
|
||||||
|
const { control, velocity, forces, boundingBox } = args;
|
||||||
|
|
||||||
|
let center = boundingBox.center;
|
||||||
|
|
||||||
|
const myCenter = this.getComponent<BoundingBox>(
|
||||||
|
ComponentNames.BoundingBox
|
||||||
|
).center;
|
||||||
|
const distance = Math.sqrt(
|
||||||
|
Math.pow(center.y - myCenter.y, 2) + Math.pow(center.x - myCenter.x, 2)
|
||||||
|
);
|
||||||
|
if (distance < 30) center = myCenter;
|
||||||
|
|
||||||
|
[
|
||||||
|
Object.assign(new Control(control.controllableBy), control),
|
||||||
|
new Velocity(velocity.velocity),
|
||||||
|
new Forces(forces.forces),
|
||||||
|
new BoundingBox(center, boundingBox.dimension, boundingBox.rotation)
|
||||||
|
].forEach((component) => this.addComponent(component));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
export * from "./Entity";
|
export * from './Entity';
|
||||||
export * from "./Floor";
|
export * from './Floor';
|
||||||
export * from "./Player";
|
export * from './Player';
|
||||||
|
export * from './names';
|
||||||
|
4
engine/entities/names.ts
Normal file
4
engine/entities/names.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export namespace EntityNames {
|
||||||
|
export const Player = 'Player';
|
||||||
|
export const Floor = 'Floor';
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
export enum Action {
|
export enum Action {
|
||||||
MOVE_LEFT,
|
MOVE_LEFT,
|
||||||
MOVE_RIGHT,
|
MOVE_RIGHT,
|
||||||
JUMP,
|
JUMP
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
export enum Direction {
|
export enum Direction {
|
||||||
UP = "UP",
|
UP = 'UP',
|
||||||
DOWN = "DOWN",
|
DOWN = 'DOWN',
|
||||||
LEFT = "LEFT",
|
LEFT = 'LEFT',
|
||||||
RIGHT = "RIGHT",
|
RIGHT = 'RIGHT'
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { Coord2D, Dimension2D } from "./";
|
import type { Coord2D, Dimension2D } from './';
|
||||||
|
|
||||||
export interface DrawArgs {
|
export interface DrawArgs {
|
||||||
center: Coord2D;
|
center: Coord2D;
|
||||||
|
@ -9,8 +9,11 @@ export interface Dimension2D {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Velocity2D {
|
export interface Velocity2D {
|
||||||
dx: number;
|
dCartesian: {
|
||||||
dy: number;
|
dx: number;
|
||||||
|
dy: number;
|
||||||
|
};
|
||||||
|
dTheta: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Force2D {
|
export interface Force2D {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
export * from "./Vec2";
|
export * from './Vec2';
|
||||||
export * from "./Draw";
|
export * from './Draw';
|
||||||
export * from "./Direction";
|
export * from './Direction';
|
||||||
export * from "./Action";
|
export * from './Action';
|
||||||
|
37
engine/network/index.ts
Normal file
37
engine/network/index.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
export enum MessageType {
|
||||||
|
NEW_ENTITIES = 'NEW_ENTITIES',
|
||||||
|
REMOVE_ENTITIES = 'REMOVE_ENTITIES',
|
||||||
|
UPDATE_ENTITIES = 'UPDATE_ENTITIES',
|
||||||
|
NEW_INPUT = 'NEW_INPUT',
|
||||||
|
REMOVE_INPUT = 'REMOVE_INPUT'
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EntityAddBody = {
|
||||||
|
entityName: string;
|
||||||
|
id: string;
|
||||||
|
args: Record<string, any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EntityUpdateBody = {
|
||||||
|
id: string;
|
||||||
|
args: Record<string, any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Message = {
|
||||||
|
type: MessageType;
|
||||||
|
body: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface MessageQueueProvider {
|
||||||
|
getNewMessages(): Message[];
|
||||||
|
clearMessages(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MessagePublisher {
|
||||||
|
addMessage(message: Message): void;
|
||||||
|
publish(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MessageProcessor {
|
||||||
|
process(message: Message): void;
|
||||||
|
}
|
104
engine/structures/Grid.ts
Normal file
104
engine/structures/Grid.ts
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import type { Coord2D, Dimension2D } from '../interfaces';
|
||||||
|
import type { BoxedEntry, RefreshingCollisionFinderBehavior } from '.';
|
||||||
|
import { Miscellaneous } from '../config/constants';
|
||||||
|
|
||||||
|
export class Grid implements RefreshingCollisionFinderBehavior {
|
||||||
|
private cellEntities: Map<number, string[]>;
|
||||||
|
|
||||||
|
private gridDimension: Dimension2D;
|
||||||
|
private cellDimension: Dimension2D;
|
||||||
|
private topLeft: Coord2D;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
gridDimension: Dimension2D = {
|
||||||
|
width: Miscellaneous.WIDTH,
|
||||||
|
height: Miscellaneous.HEIGHT
|
||||||
|
},
|
||||||
|
cellDimension: Dimension2D = {
|
||||||
|
width: Miscellaneous.DEFAULT_GRID_WIDTH,
|
||||||
|
height: Miscellaneous.DEFAULT_GRID_HEIGHT
|
||||||
|
},
|
||||||
|
topLeft = { x: 0, y: 0 }
|
||||||
|
) {
|
||||||
|
this.gridDimension = gridDimension;
|
||||||
|
this.cellDimension = cellDimension;
|
||||||
|
this.topLeft = topLeft;
|
||||||
|
|
||||||
|
this.cellEntities = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
public insert(boxedEntry: BoxedEntry) {
|
||||||
|
this.getOverlappingCells(boxedEntry).forEach((gridIdx) => {
|
||||||
|
if (!this.cellEntities.has(gridIdx)) {
|
||||||
|
this.cellEntities.set(gridIdx, []);
|
||||||
|
}
|
||||||
|
this.cellEntities.get(gridIdx)!.push(boxedEntry.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public getNeighborIds(boxedEntry: BoxedEntry): Set<string> {
|
||||||
|
const neighborIds: Set<string> = new Set();
|
||||||
|
this.getOverlappingCells(boxedEntry).forEach((gridIdx) => {
|
||||||
|
if (this.cellEntities.has(gridIdx)) {
|
||||||
|
this.cellEntities.get(gridIdx)!.forEach((id) => neighborIds.add(id));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return neighborIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public clear() {
|
||||||
|
this.cellEntities.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public setTopLeft(topLeft: Coord2D) {
|
||||||
|
this.topLeft = topLeft;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setDimension(dimension: Dimension2D) {
|
||||||
|
this.gridDimension = dimension;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setCellDimension(cellDimension: Dimension2D) {
|
||||||
|
this.cellDimension = cellDimension;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getOverlappingCells(boxedEntry: BoxedEntry): number[] {
|
||||||
|
const { center, dimension } = boxedEntry;
|
||||||
|
const yBoxes = Math.ceil(
|
||||||
|
this.gridDimension.height / this.cellDimension.height
|
||||||
|
);
|
||||||
|
const xBoxes = Math.ceil(
|
||||||
|
this.gridDimension.width / this.cellDimension.width
|
||||||
|
);
|
||||||
|
|
||||||
|
const translated: Coord2D = {
|
||||||
|
y: center.y - this.topLeft.y,
|
||||||
|
x: center.x - this.topLeft.x
|
||||||
|
};
|
||||||
|
|
||||||
|
const topLeftBox = {
|
||||||
|
x: Math.floor(
|
||||||
|
(translated.x - dimension.width / 2) / this.cellDimension.width
|
||||||
|
),
|
||||||
|
y: Math.floor(
|
||||||
|
(translated.y - dimension.height / 2) / this.cellDimension.height
|
||||||
|
)
|
||||||
|
};
|
||||||
|
const bottomRightBox = {
|
||||||
|
x: Math.floor(
|
||||||
|
(translated.x + dimension.width / 2) / this.cellDimension.width
|
||||||
|
),
|
||||||
|
y: Math.floor(
|
||||||
|
(translated.y + dimension.height / 2) / this.cellDimension.height
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
const cells: number[] = [];
|
||||||
|
|
||||||
|
for (let y = topLeftBox.y; y <= bottomRightBox.y; ++y)
|
||||||
|
for (let x = topLeftBox.x; x <= bottomRightBox.x; ++x)
|
||||||
|
cells.push(yBoxes * y + x);
|
||||||
|
|
||||||
|
return cells;
|
||||||
|
}
|
||||||
|
}
|
@ -1,19 +1,21 @@
|
|||||||
import type { Coord2D, Dimension2D } from "../interfaces";
|
import type { Coord2D, Dimension2D } from '../interfaces';
|
||||||
|
import type { BoxedEntry, RefreshingCollisionFinderBehavior } from '.';
|
||||||
interface BoxedEntry {
|
|
||||||
id: number;
|
|
||||||
dimension: Dimension2D;
|
|
||||||
center: Coord2D;
|
|
||||||
}
|
|
||||||
|
|
||||||
enum Quadrant {
|
enum Quadrant {
|
||||||
I,
|
I,
|
||||||
II,
|
II,
|
||||||
III,
|
III,
|
||||||
IV,
|
IV
|
||||||
}
|
}
|
||||||
|
|
||||||
export class QuadTree {
|
/*
|
||||||
|
unused due to performance problems. here anyways, in case it _really_ is necessary at some point
|
||||||
|
(and to justify the amount of time i spent here).
|
||||||
|
*/
|
||||||
|
export class QuadTree implements RefreshingCollisionFinderBehavior {
|
||||||
|
private static readonly QUADTREE_MAX_LEVELS = 3;
|
||||||
|
private static readonly QUADTREE_SPLIT_THRESHOLD = 2000;
|
||||||
|
|
||||||
private maxLevels: number;
|
private maxLevels: number;
|
||||||
private splitThreshold: number;
|
private splitThreshold: number;
|
||||||
private level: number;
|
private level: number;
|
||||||
@ -24,34 +26,33 @@ export class QuadTree {
|
|||||||
private objects: BoxedEntry[];
|
private objects: BoxedEntry[];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
topLeft: Coord2D,
|
topLeft: Coord2D = { x: 0, y: 0 },
|
||||||
dimension: Dimension2D,
|
dimension: Dimension2D,
|
||||||
maxLevels: number,
|
maxLevels: number = QuadTree.QUADTREE_MAX_LEVELS,
|
||||||
splitThreshold: number,
|
splitThreshold: number = QuadTree.QUADTREE_SPLIT_THRESHOLD,
|
||||||
level?: number,
|
level: number = 0
|
||||||
) {
|
) {
|
||||||
this.children = new Map<Quadrant, QuadTree>();
|
this.children = new Map<Quadrant, QuadTree>();
|
||||||
this.objects = [];
|
this.objects = [];
|
||||||
|
|
||||||
this.maxLevels = maxLevels;
|
this.maxLevels = maxLevels;
|
||||||
this.splitThreshold = splitThreshold;
|
this.splitThreshold = splitThreshold;
|
||||||
this.level = level ?? 0;
|
this.level = level;
|
||||||
|
|
||||||
this.topLeft = topLeft;
|
this.topLeft = topLeft;
|
||||||
this.dimension = dimension;
|
this.dimension = dimension;
|
||||||
}
|
}
|
||||||
|
|
||||||
public insert(id: number, dimension: Dimension2D, center: Coord2D): void {
|
public insert(boxedEntry: BoxedEntry): void {
|
||||||
const box: BoxedEntry = { id, center, dimension };
|
|
||||||
if (this.hasChildren()) {
|
if (this.hasChildren()) {
|
||||||
this.getQuadrants(box).forEach((quadrant) => {
|
this.getQuadrants(boxedEntry).forEach((quadrant) => {
|
||||||
const quadrantBox = this.children.get(quadrant);
|
const quadrantBox = this.children.get(quadrant);
|
||||||
quadrantBox?.insert(id, dimension, center);
|
quadrantBox!.insert(boxedEntry);
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.objects.push({ id, dimension, center });
|
this.objects.push(boxedEntry);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.objects.length > this.splitThreshold &&
|
this.objects.length > this.splitThreshold &&
|
||||||
@ -66,22 +67,24 @@ export class QuadTree {
|
|||||||
|
|
||||||
public clear(): void {
|
public clear(): void {
|
||||||
this.objects = [];
|
this.objects = [];
|
||||||
|
|
||||||
if (this.hasChildren()) {
|
if (this.hasChildren()) {
|
||||||
this.children.forEach((child) => child.clear());
|
this.children.forEach((child) => child.clear());
|
||||||
this.children.clear();
|
this.children.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public getNeighborIds(boxedEntry: BoxedEntry): number[] {
|
public getNeighborIds(boxedEntry: BoxedEntry): Set<string> {
|
||||||
const neighbors: number[] = this.objects.map(({ id }) => id);
|
const neighbors = new Set<string>(
|
||||||
|
this.objects.map(({ id }) => id).filter((id) => id != boxedEntry.id)
|
||||||
|
);
|
||||||
|
|
||||||
if (this.hasChildren()) {
|
if (this.hasChildren()) {
|
||||||
this.getQuadrants(boxedEntry).forEach((quadrant) => {
|
this.getQuadrants(boxedEntry).forEach((quadrant) => {
|
||||||
const quadrantBox = this.children.get(quadrant);
|
const quadrantBox = this.children.get(quadrant);
|
||||||
|
|
||||||
quadrantBox
|
quadrantBox
|
||||||
?.getNeighborIds(boxedEntry)
|
?.getNeighborIds(boxedEntry)
|
||||||
.forEach((id) => neighbors.push(id));
|
.forEach((id) => neighbors.add(id));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,9 +102,9 @@ export class QuadTree {
|
|||||||
[Quadrant.III, { x: this.topLeft.x, y: this.topLeft.y + halfHeight }],
|
[Quadrant.III, { x: this.topLeft.x, y: this.topLeft.y + halfHeight }],
|
||||||
[
|
[
|
||||||
Quadrant.IV,
|
Quadrant.IV,
|
||||||
{ x: this.topLeft.x + halfWidth, y: this.topLeft.y + halfHeight },
|
{ x: this.topLeft.x + halfWidth, y: this.topLeft.y + halfHeight }
|
||||||
],
|
]
|
||||||
] as [[Quadrant, Coord2D]]
|
] as [Quadrant, Coord2D][]
|
||||||
).forEach(([quadrant, pos]) => {
|
).forEach(([quadrant, pos]) => {
|
||||||
this.children.set(
|
this.children.set(
|
||||||
quadrant,
|
quadrant,
|
||||||
@ -110,8 +113,8 @@ export class QuadTree {
|
|||||||
{ width: halfWidth, height: halfHeight },
|
{ width: halfWidth, height: halfHeight },
|
||||||
this.maxLevels,
|
this.maxLevels,
|
||||||
this.splitThreshold,
|
this.splitThreshold,
|
||||||
this.level + 1,
|
this.level + 1
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -119,52 +122,48 @@ export class QuadTree {
|
|||||||
private getQuadrants(boxedEntry: BoxedEntry): Quadrant[] {
|
private getQuadrants(boxedEntry: BoxedEntry): Quadrant[] {
|
||||||
const treeCenter: Coord2D = {
|
const treeCenter: Coord2D = {
|
||||||
x: this.topLeft.x + this.dimension.width / 2,
|
x: this.topLeft.x + this.dimension.width / 2,
|
||||||
y: this.topLeft.y + this.dimension.height / 2,
|
y: this.topLeft.y + this.dimension.height / 2
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
Quadrant.I,
|
Quadrant.I,
|
||||||
(x: number, y: number) => x >= treeCenter.x && y < treeCenter.y,
|
(x: number, y: number) => x >= treeCenter.x && y < treeCenter.y
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
Quadrant.II,
|
Quadrant.II,
|
||||||
(x: number, y: number) => x < treeCenter.x && y < treeCenter.y,
|
(x: number, y: number) => x < treeCenter.x && y < treeCenter.y
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
Quadrant.III,
|
Quadrant.III,
|
||||||
(x: number, y: number) => x < treeCenter.x && y >= treeCenter.y,
|
(x: number, y: number) => x < treeCenter.x && y >= treeCenter.y
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
Quadrant.IV,
|
Quadrant.IV,
|
||||||
(x: number, y: number) => x >= treeCenter.x && y >= treeCenter.y,
|
(x: number, y: number) => x >= treeCenter.x && y >= treeCenter.y
|
||||||
],
|
]
|
||||||
] as [[Quadrant, (x: number, y: number) => boolean]]
|
] as [Quadrant, (x: number, y: number) => boolean][]
|
||||||
)
|
)
|
||||||
.filter(
|
.filter(
|
||||||
([_quadrant, condition]) =>
|
([_quadrant, condition]) =>
|
||||||
condition(
|
condition(
|
||||||
boxedEntry.center.x + boxedEntry.dimension.width / 2,
|
boxedEntry.center.x + boxedEntry.dimension.width / 2,
|
||||||
boxedEntry.center.y + boxedEntry.dimension.height / 2,
|
boxedEntry.center.y + boxedEntry.dimension.height / 2
|
||||||
) ||
|
) ||
|
||||||
condition(
|
condition(
|
||||||
boxedEntry.center.x - boxedEntry.dimension.width / 2,
|
boxedEntry.center.x - boxedEntry.dimension.width / 2,
|
||||||
boxedEntry.center.y - boxedEntry.dimension.height / 2,
|
boxedEntry.center.y - boxedEntry.dimension.height / 2
|
||||||
),
|
)
|
||||||
)
|
)
|
||||||
.map(([quadrant]) => quadrant);
|
.map(([quadrant]) => quadrant);
|
||||||
}
|
}
|
||||||
|
|
||||||
private realignObjects(): void {
|
private realignObjects(): void {
|
||||||
this.objects.forEach((boxedEntry) => {
|
this.objects.forEach((boxedEntry) => {
|
||||||
this.getQuadrants(boxedEntry).forEach((direction) => {
|
this.getQuadrants(boxedEntry).forEach((quadrant) => {
|
||||||
const quadrant = this.children.get(direction);
|
const quadrantBox = this.children.get(quadrant);
|
||||||
quadrant?.insert(
|
quadrantBox!.insert(boxedEntry);
|
||||||
boxedEntry.id,
|
|
||||||
boxedEntry.dimension,
|
|
||||||
boxedEntry.center,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -174,4 +173,12 @@ export class QuadTree {
|
|||||||
private hasChildren() {
|
private hasChildren() {
|
||||||
return this.children && this.children.size > 0;
|
return this.children && this.children.size > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setTopLeft(topLeft: Coord2D) {
|
||||||
|
this.topLeft = topLeft;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setDimension(dimension: Dimension2D) {
|
||||||
|
this.dimension = dimension;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
14
engine/structures/RefreshingCollisionFinderBehavior.ts
Normal file
14
engine/structures/RefreshingCollisionFinderBehavior.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import type { Coord2D, Dimension2D } from '../interfaces';
|
||||||
|
|
||||||
|
export interface BoxedEntry {
|
||||||
|
id: string;
|
||||||
|
dimension: Dimension2D;
|
||||||
|
center: Coord2D;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RefreshingCollisionFinderBehavior {
|
||||||
|
clear(): void;
|
||||||
|
insert(boxedEntry: BoxedEntry): void;
|
||||||
|
getNeighborIds(boxedEntry: BoxedEntry): Set<string>;
|
||||||
|
setTopLeft(topLeft: Coord2D): void;
|
||||||
|
}
|
@ -1 +1,3 @@
|
|||||||
export * from "./QuadTree";
|
export * from './RefreshingCollisionFinderBehavior';
|
||||||
|
export * from './QuadTree';
|
||||||
|
export * from './Grid';
|
||||||
|
@ -1,61 +1,59 @@
|
|||||||
import { SystemNames, System } from ".";
|
import { SystemNames, System } from '.';
|
||||||
import {
|
import {
|
||||||
Mass,
|
Mass,
|
||||||
BoundingBox,
|
BoundingBox,
|
||||||
ComponentNames,
|
ComponentNames,
|
||||||
Jump,
|
Jump,
|
||||||
Velocity,
|
Velocity,
|
||||||
Forces,
|
Forces
|
||||||
} from "../components";
|
} from '../components';
|
||||||
import { Game } from "../Game";
|
import { Game } from '../Game';
|
||||||
import { PhysicsConstants } from "../config";
|
import { Miscellaneous, PhysicsConstants } from '../config';
|
||||||
import { Entity } from "../entities";
|
import { Entity } from '../entities';
|
||||||
import type { Dimension2D } from "../interfaces";
|
import type { Coord2D, Dimension2D, Velocity2D } from '../interfaces';
|
||||||
import { QuadTree } from "../structures";
|
import { BoxedEntry, RefreshingCollisionFinderBehavior } from '../structures';
|
||||||
|
|
||||||
export class Collision extends System {
|
export class Collision extends System {
|
||||||
private static readonly COLLIDABLE_COMPONENT_NAMES = [
|
private static readonly COLLIDABLE_COMPONENT_NAMES = [
|
||||||
ComponentNames.Collide,
|
ComponentNames.Collide,
|
||||||
ComponentNames.TopCollidable,
|
ComponentNames.TopCollidable
|
||||||
];
|
];
|
||||||
private static readonly QUADTREE_MAX_LEVELS = 10;
|
|
||||||
private static readonly QUADTREE_SPLIT_THRESHOLD = 10;
|
|
||||||
|
|
||||||
private quadTree: QuadTree;
|
private collisionFinder: RefreshingCollisionFinderBehavior;
|
||||||
|
|
||||||
constructor(screenDimensions: Dimension2D) {
|
constructor(refreshingCollisionFinder: RefreshingCollisionFinderBehavior) {
|
||||||
super(SystemNames.Collision);
|
super(SystemNames.Collision);
|
||||||
|
|
||||||
this.quadTree = new QuadTree(
|
this.collisionFinder = refreshingCollisionFinder;
|
||||||
{ x: 0, y: 0 },
|
|
||||||
screenDimensions,
|
|
||||||
Collision.QUADTREE_MAX_LEVELS,
|
|
||||||
Collision.QUADTREE_SPLIT_THRESHOLD,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public update(_dt: number, game: Game) {
|
public update(_dt: number, game: Game) {
|
||||||
// rebuild the quadtree
|
this.collisionFinder.clear();
|
||||||
this.quadTree.clear();
|
|
||||||
|
|
||||||
const entitiesToAddToQuadtree: Entity[] = [];
|
const entitiesToAddToCollisionFinder: Entity[] = [];
|
||||||
|
|
||||||
Collision.COLLIDABLE_COMPONENT_NAMES.map((componentName) =>
|
Collision.COLLIDABLE_COMPONENT_NAMES.map((componentName) =>
|
||||||
game.componentEntities.get(componentName),
|
game.forEachEntityWithComponent(componentName, (entity) => {
|
||||||
).forEach(
|
if (!entity.hasComponent(ComponentNames.BoundingBox)) {
|
||||||
(entityIds?: Set<number>) =>
|
return;
|
||||||
entityIds?.forEach((id) => {
|
}
|
||||||
const entity = game.entities.get(id);
|
entitiesToAddToCollisionFinder.push(entity);
|
||||||
if (!entity || !entity.hasComponent(ComponentNames.BoundingBox)) {
|
})
|
||||||
return;
|
|
||||||
}
|
|
||||||
entitiesToAddToQuadtree.push(entity);
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
entitiesToAddToQuadtree.forEach((entity) => {
|
this.insertEntitiesAndUpdateBounds(entitiesToAddToCollisionFinder);
|
||||||
|
this.findCollidingEntitiesAndCollide(entitiesToAddToCollisionFinder, game);
|
||||||
|
}
|
||||||
|
|
||||||
|
private insertEntitiesAndUpdateBounds(entities: Entity[]) {
|
||||||
|
const collisionFinderInsertions: BoxedEntry[] = [];
|
||||||
|
|
||||||
|
const topLeft: Coord2D = { x: Infinity, y: Infinity };
|
||||||
|
const bottomRight: Coord2D = { x: -Infinity, y: -Infinity };
|
||||||
|
|
||||||
|
entities.forEach((entity) => {
|
||||||
const boundingBox = entity.getComponent<BoundingBox>(
|
const boundingBox = entity.getComponent<BoundingBox>(
|
||||||
ComponentNames.BoundingBox,
|
ComponentNames.BoundingBox
|
||||||
);
|
);
|
||||||
|
|
||||||
let dimension = { ...boundingBox.dimension };
|
let dimension = { ...boundingBox.dimension };
|
||||||
@ -63,18 +61,43 @@ export class Collision extends System {
|
|||||||
dimension = boundingBox.getOutscribedBoxDims();
|
dimension = boundingBox.getOutscribedBoxDims();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.quadTree.insert(entity.id, dimension, boundingBox.center);
|
const { center } = boundingBox;
|
||||||
|
const topLeftBoundingBox = boundingBox.getTopLeft();
|
||||||
|
const bottomRightBoundingBox = boundingBox.getBottomRight();
|
||||||
|
|
||||||
|
topLeft.x = Math.min(topLeftBoundingBox.x, topLeft.x);
|
||||||
|
topLeft.y = Math.min(topLeftBoundingBox.y, topLeft.y);
|
||||||
|
bottomRight.x = Math.max(bottomRightBoundingBox.x, bottomRight.x);
|
||||||
|
bottomRight.y = Math.max(bottomRightBoundingBox.y, bottomRight.y);
|
||||||
|
|
||||||
|
collisionFinderInsertions.push({
|
||||||
|
id: entity.id,
|
||||||
|
dimension,
|
||||||
|
center
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// find colliding entities and perform collisions
|
// set bounds first
|
||||||
const collidingEntities = this.getCollidingEntities(
|
if (entities.length > 0) {
|
||||||
entitiesToAddToQuadtree,
|
this.collisionFinder.setTopLeft(topLeft);
|
||||||
game,
|
this.collisionFinder.setDimension({
|
||||||
|
width: bottomRight.x - topLeft.x,
|
||||||
|
height: bottomRight.y - topLeft.y
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// then, begin insertions
|
||||||
|
collisionFinderInsertions.forEach((boxedEntry: BoxedEntry) =>
|
||||||
|
this.collisionFinder.insert(boxedEntry)
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private findCollidingEntitiesAndCollide(entities: Entity[], game: Game) {
|
||||||
|
const collidingEntities = this.getCollidingEntities(entities, game);
|
||||||
|
|
||||||
collidingEntities.forEach(([entityAId, entityBId]) => {
|
collidingEntities.forEach(([entityAId, entityBId]) => {
|
||||||
const [entityA, entityB] = [entityAId, entityBId].map((id) =>
|
const [entityA, entityB] = [entityAId, entityBId].map((id) =>
|
||||||
game.entities.get(id),
|
game.entities.get(id)
|
||||||
);
|
);
|
||||||
if (entityA && entityB) {
|
if (entityA && entityB) {
|
||||||
this.performCollision(entityA, entityB);
|
this.performCollision(entityA, entityB);
|
||||||
@ -84,12 +107,14 @@ export class Collision extends System {
|
|||||||
|
|
||||||
private performCollision(entityA: Entity, entityB: Entity) {
|
private performCollision(entityA: Entity, entityB: Entity) {
|
||||||
const [entityABoundingBox, entityBBoundingBox] = [entityA, entityB].map(
|
const [entityABoundingBox, entityBBoundingBox] = [entityA, entityB].map(
|
||||||
(entity) => entity.getComponent<BoundingBox>(ComponentNames.BoundingBox),
|
(entity) => entity.getComponent<BoundingBox>(ComponentNames.BoundingBox)
|
||||||
);
|
);
|
||||||
|
|
||||||
let velocity = new Velocity();
|
let velocity: Velocity2D = { dCartesian: { dx: 0, dy: 0 }, dTheta: 0 };
|
||||||
if (entityA.hasComponent(ComponentNames.Velocity)) {
|
if (entityA.hasComponent(ComponentNames.Velocity)) {
|
||||||
velocity = entityA.getComponent<Velocity>(ComponentNames.Velocity);
|
velocity = entityA.getComponent<Velocity>(
|
||||||
|
ComponentNames.Velocity
|
||||||
|
).velocity;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -100,7 +125,7 @@ export class Collision extends System {
|
|||||||
) {
|
) {
|
||||||
if (entityBBoundingBox.rotation != 0) {
|
if (entityBBoundingBox.rotation != 0) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`entity with id ${entityB.id} has TopCollidable component and a non-zero rotation. that is not (yet) supported.`,
|
`entity with id ${entityB.id} has TopCollidable component and a non-zero rotation. that is not (yet) supported.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,7 +139,7 @@ export class Collision extends System {
|
|||||||
|
|
||||||
entityA.getComponent<Forces>(ComponentNames.Forces).forces.push({
|
entityA.getComponent<Forces>(ComponentNames.Forces).forces.push({
|
||||||
fCartesian: { fy: F_n, fx: 0 },
|
fCartesian: { fy: F_n, fx: 0 },
|
||||||
torque: 0,
|
torque: 0
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,35 +157,33 @@ export class Collision extends System {
|
|||||||
|
|
||||||
private getCollidingEntities(
|
private getCollidingEntities(
|
||||||
collidableEntities: Entity[],
|
collidableEntities: Entity[],
|
||||||
game: Game,
|
game: Game
|
||||||
): [number, number][] {
|
): [string, string][] {
|
||||||
const collidingEntityIds: [number, number][] = [];
|
const collidingEntityIds: [string, string][] = [];
|
||||||
|
|
||||||
for (const entity of collidableEntities) {
|
for (const entity of collidableEntities) {
|
||||||
const boundingBox = entity.getComponent<BoundingBox>(
|
const boundingBox = entity.getComponent<BoundingBox>(
|
||||||
ComponentNames.BoundingBox,
|
ComponentNames.BoundingBox
|
||||||
);
|
);
|
||||||
|
|
||||||
const neighborIds = this.quadTree
|
const neighborIds = this.collisionFinder.getNeighborIds({
|
||||||
.getNeighborIds({
|
id: entity.id,
|
||||||
id: entity.id,
|
dimension: boundingBox.dimension,
|
||||||
dimension: boundingBox.dimension,
|
center: boundingBox.center
|
||||||
center: boundingBox.center,
|
});
|
||||||
})
|
|
||||||
.filter((neighborId) => neighborId != entity.id);
|
|
||||||
|
|
||||||
neighborIds.forEach((neighborId) => {
|
for (const neighborId of neighborIds) {
|
||||||
const neighbor = game.getEntity(neighborId);
|
const neighbor = game.getEntity(neighborId);
|
||||||
if (!neighbor) return;
|
if (!neighbor) return;
|
||||||
|
|
||||||
const neighborBoundingBox = neighbor.getComponent<BoundingBox>(
|
const neighborBoundingBox = neighbor.getComponent<BoundingBox>(
|
||||||
ComponentNames.BoundingBox,
|
ComponentNames.BoundingBox
|
||||||
);
|
);
|
||||||
|
|
||||||
if (boundingBox.isCollidingWith(neighborBoundingBox)) {
|
if (boundingBox.isCollidingWith(neighborBoundingBox)) {
|
||||||
collidingEntityIds.push([entity.id, neighborId]);
|
collidingEntityIds.push([entity.id, neighborId]);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return collidingEntityIds;
|
return collidingEntityIds;
|
||||||
@ -169,11 +192,11 @@ export class Collision extends System {
|
|||||||
// ramblings: https://excalidraw.com/#json=z-xD86Za4a3duZuV2Oky0,KaGe-5iHJu1Si8inEo4GLQ
|
// ramblings: https://excalidraw.com/#json=z-xD86Za4a3duZuV2Oky0,KaGe-5iHJu1Si8inEo4GLQ
|
||||||
private getDyToPushOutOfFloor(
|
private getDyToPushOutOfFloor(
|
||||||
entityBoundingBox: BoundingBox,
|
entityBoundingBox: BoundingBox,
|
||||||
floorBoundingBox: BoundingBox,
|
floorBoundingBox: BoundingBox
|
||||||
): number {
|
): number {
|
||||||
const {
|
const {
|
||||||
dimension: { width, height },
|
dimension: { width, height },
|
||||||
center: { x },
|
center: { x }
|
||||||
} = entityBoundingBox;
|
} = entityBoundingBox;
|
||||||
|
|
||||||
const outScribedRectangle = entityBoundingBox.getOutscribedBoxDims();
|
const outScribedRectangle = entityBoundingBox.getOutscribedBoxDims();
|
||||||
@ -192,7 +215,7 @@ export class Collision extends System {
|
|||||||
if (x >= floorBoundingBox.center.x) {
|
if (x >= floorBoundingBox.center.x) {
|
||||||
boundedCollisionX = Math.min(
|
boundedCollisionX = Math.min(
|
||||||
floorBoundingBox.center.x + floorBoundingBox.dimension.width / 2,
|
floorBoundingBox.center.x + floorBoundingBox.dimension.width / 2,
|
||||||
clippedX,
|
clippedX
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
outScribedRectangle.height / 2 -
|
outScribedRectangle.height / 2 -
|
||||||
@ -202,7 +225,7 @@ export class Collision extends System {
|
|||||||
|
|
||||||
boundedCollisionX = Math.max(
|
boundedCollisionX = Math.max(
|
||||||
floorBoundingBox.center.x - floorBoundingBox.dimension.width / 2,
|
floorBoundingBox.center.x - floorBoundingBox.dimension.width / 2,
|
||||||
clippedX,
|
clippedX
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -2,10 +2,10 @@ import {
|
|||||||
ComponentNames,
|
ComponentNames,
|
||||||
Velocity,
|
Velocity,
|
||||||
FacingDirection as FacingDirectionComponent,
|
FacingDirection as FacingDirectionComponent,
|
||||||
Control,
|
Control
|
||||||
} from "../components";
|
} from '../components';
|
||||||
import { Game } from "../Game";
|
import { Game } from '../Game';
|
||||||
import { System, SystemNames } from "./";
|
import { System, SystemNames } from './';
|
||||||
|
|
||||||
export class FacingDirection extends System {
|
export class FacingDirection extends System {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -20,24 +20,27 @@ export class FacingDirection extends System {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalVelocity: Velocity = new Velocity();
|
const totalVelocityComponent = new Velocity();
|
||||||
const control = entity.getComponent<Control>(ComponentNames.Control);
|
const control = entity.getComponent<Control>(ComponentNames.Control);
|
||||||
const velocity = entity.getComponent<Velocity>(ComponentNames.Velocity);
|
const velocity = entity.getComponent<Velocity>(
|
||||||
totalVelocity.add(velocity);
|
ComponentNames.Velocity
|
||||||
|
).velocity;
|
||||||
|
|
||||||
|
totalVelocityComponent.add(velocity);
|
||||||
if (control) {
|
if (control) {
|
||||||
totalVelocity.add(control.controlVelocity);
|
totalVelocityComponent.add(control.controlVelocityComponent.velocity);
|
||||||
}
|
}
|
||||||
|
|
||||||
const facingDirection = entity.getComponent<FacingDirectionComponent>(
|
const facingDirection = entity.getComponent<FacingDirectionComponent>(
|
||||||
ComponentNames.FacingDirection,
|
ComponentNames.FacingDirection
|
||||||
);
|
);
|
||||||
|
|
||||||
if (totalVelocity.dCartesian.dx > 0) {
|
if (totalVelocityComponent.velocity.dCartesian.dx > 0) {
|
||||||
entity.addComponent(facingDirection.facingRightSprite);
|
entity.addComponent(facingDirection.facingRightSprite);
|
||||||
} else if (totalVelocity.dCartesian.dx < 0) {
|
} else if (totalVelocityComponent.velocity.dCartesian.dx < 0) {
|
||||||
entity.addComponent(facingDirection.facingLeftSprite);
|
entity.addComponent(facingDirection.facingLeftSprite);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,30 +4,117 @@ import {
|
|||||||
ComponentNames,
|
ComponentNames,
|
||||||
Velocity,
|
Velocity,
|
||||||
Mass,
|
Mass,
|
||||||
Control,
|
Control
|
||||||
} from "../components";
|
} from '../components';
|
||||||
import { Game } from "../Game";
|
import { Game } from '../Game';
|
||||||
import { KeyConstants, PhysicsConstants } from "../config";
|
import { KeyConstants, PhysicsConstants } from '../config';
|
||||||
import { Action } from "../interfaces";
|
import { Action } from '../interfaces';
|
||||||
import { System, SystemNames } from "./";
|
import { System, SystemNames } from '.';
|
||||||
|
import { MessagePublisher, MessageType } from '../network';
|
||||||
|
import { Entity } from '../entities';
|
||||||
|
|
||||||
export class Input extends System {
|
export class Input extends System {
|
||||||
|
public clientId: string;
|
||||||
|
|
||||||
private keys: Set<string>;
|
private keys: Set<string>;
|
||||||
private actionTimeStamps: Map<Action, number>;
|
private actionTimeStamps: Map<Action, number>;
|
||||||
|
private messagePublisher?: MessagePublisher;
|
||||||
|
|
||||||
constructor() {
|
constructor(clientId: string, messagePublisher?: MessagePublisher) {
|
||||||
super(SystemNames.Input);
|
super(SystemNames.Input);
|
||||||
|
|
||||||
this.keys = new Set<string>();
|
this.clientId = clientId;
|
||||||
this.actionTimeStamps = new Map<Action, number>();
|
this.keys = new Set();
|
||||||
|
this.actionTimeStamps = new Map();
|
||||||
|
|
||||||
|
this.messagePublisher = messagePublisher;
|
||||||
}
|
}
|
||||||
|
|
||||||
public keyPressed(key: string) {
|
public keyPressed(key: string) {
|
||||||
this.keys.add(key);
|
this.keys.add(key);
|
||||||
|
|
||||||
|
if (this.messagePublisher) {
|
||||||
|
this.messagePublisher.addMessage({
|
||||||
|
type: MessageType.NEW_INPUT,
|
||||||
|
body: key
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public keyReleased(key: string) {
|
public keyReleased(key: string) {
|
||||||
this.keys.delete(key);
|
this.keys.delete(key);
|
||||||
|
|
||||||
|
if (this.messagePublisher) {
|
||||||
|
this.messagePublisher.addMessage({
|
||||||
|
type: MessageType.REMOVE_INPUT,
|
||||||
|
body: key
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public update(_dt: number, game: Game) {
|
||||||
|
game.forEachEntityWithComponent(ComponentNames.Control, (entity) =>
|
||||||
|
this.handleInput(entity)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public handleInput(entity: Entity) {
|
||||||
|
const controlComponent = entity.getComponent<Control>(
|
||||||
|
ComponentNames.Control
|
||||||
|
);
|
||||||
|
controlComponent.isControllable =
|
||||||
|
controlComponent.controllableBy === this.clientId;
|
||||||
|
|
||||||
|
if (!controlComponent.isControllable) return;
|
||||||
|
|
||||||
|
if (this.hasSomeKey(KeyConstants.ActionKeys.get(Action.MOVE_RIGHT))) {
|
||||||
|
controlComponent.controlVelocityComponent.velocity.dCartesian.dx +=
|
||||||
|
PhysicsConstants.PLAYER_MOVE_VEL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.hasSomeKey(KeyConstants.ActionKeys.get(Action.MOVE_LEFT))) {
|
||||||
|
controlComponent.controlVelocityComponent.velocity.dCartesian.dx +=
|
||||||
|
-PhysicsConstants.PLAYER_MOVE_VEL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
entity.hasComponent(ComponentNames.Jump) &&
|
||||||
|
this.hasSomeKey(KeyConstants.ActionKeys.get(Action.JUMP))
|
||||||
|
) {
|
||||||
|
this.performJump(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private performJump(entity: Entity) {
|
||||||
|
const velocity = entity.getComponent<Velocity>(
|
||||||
|
ComponentNames.Velocity
|
||||||
|
).velocity;
|
||||||
|
const jump = entity.getComponent<Jump>(ComponentNames.Jump);
|
||||||
|
|
||||||
|
if (jump.canJump) {
|
||||||
|
this.actionTimeStamps.set(Action.JUMP, performance.now());
|
||||||
|
|
||||||
|
velocity.dCartesian.dy += PhysicsConstants.PLAYER_JUMP_INITIAL_VEL;
|
||||||
|
jump.canJump = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
performance.now() - (this.actionTimeStamps.get(Action.JUMP) || 0) <
|
||||||
|
PhysicsConstants.MAX_JUMP_TIME_MS
|
||||||
|
) {
|
||||||
|
const mass = entity.getComponent<Mass>(ComponentNames.Mass).mass;
|
||||||
|
|
||||||
|
const jumpForce = {
|
||||||
|
fCartesian: {
|
||||||
|
fy: mass * PhysicsConstants.PLAYER_JUMP_ACC,
|
||||||
|
fx: 0
|
||||||
|
},
|
||||||
|
torque: 0
|
||||||
|
};
|
||||||
|
entity
|
||||||
|
.getComponent<Forces>(ComponentNames.Forces)
|
||||||
|
?.forces.push(jumpForce);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private hasSomeKey(keys?: string[]): boolean {
|
private hasSomeKey(keys?: string[]): boolean {
|
||||||
@ -36,48 +123,4 @@ export class Input extends System {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public update(_dt: number, game: Game) {
|
|
||||||
game.forEachEntityWithComponent(ComponentNames.Control, (entity) => {
|
|
||||||
const control = entity.getComponent<Control>(ComponentNames.Control);
|
|
||||||
|
|
||||||
if (this.hasSomeKey(KeyConstants.ActionKeys.get(Action.MOVE_RIGHT))) {
|
|
||||||
control.controlVelocity.dCartesian.dx +=
|
|
||||||
PhysicsConstants.PLAYER_MOVE_VEL;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.hasSomeKey(KeyConstants.ActionKeys.get(Action.MOVE_LEFT))) {
|
|
||||||
control.controlVelocity.dCartesian.dx +=
|
|
||||||
-PhysicsConstants.PLAYER_MOVE_VEL;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entity.hasComponent(ComponentNames.Jump)) {
|
|
||||||
const velocity = entity.getComponent<Velocity>(ComponentNames.Velocity);
|
|
||||||
const jump = entity.getComponent<Jump>(ComponentNames.Jump);
|
|
||||||
|
|
||||||
if (this.hasSomeKey(KeyConstants.ActionKeys.get(Action.JUMP))) {
|
|
||||||
if (jump.canJump) {
|
|
||||||
this.actionTimeStamps.set(Action.JUMP, performance.now());
|
|
||||||
|
|
||||||
velocity.dCartesian.dy += PhysicsConstants.PLAYER_JUMP_INITIAL_VEL;
|
|
||||||
jump.canJump = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
performance.now() - (this.actionTimeStamps.get(Action.JUMP) || 0) <
|
|
||||||
PhysicsConstants.MAX_JUMP_TIME_MS
|
|
||||||
) {
|
|
||||||
const mass = entity.getComponent<Mass>(ComponentNames.Mass).mass;
|
|
||||||
entity.getComponent<Forces>(ComponentNames.Forces)?.forces.push({
|
|
||||||
fCartesian: {
|
|
||||||
fy: mass * PhysicsConstants.PLAYER_JUMP_ACC,
|
|
||||||
fx: 0,
|
|
||||||
},
|
|
||||||
torque: 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
72
engine/systems/NetworkUpdate.ts
Normal file
72
engine/systems/NetworkUpdate.ts
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import { System, SystemNames } from '.';
|
||||||
|
import { Game } from '../Game';
|
||||||
|
import { ComponentNames } from '../components';
|
||||||
|
import {
|
||||||
|
type MessageQueueProvider,
|
||||||
|
type MessagePublisher,
|
||||||
|
type MessageProcessor,
|
||||||
|
MessageType,
|
||||||
|
EntityUpdateBody
|
||||||
|
} from '../network';
|
||||||
|
|
||||||
|
export class NetworkUpdate extends System {
|
||||||
|
private queueProvider: MessageQueueProvider;
|
||||||
|
private publisher: MessagePublisher;
|
||||||
|
private messageProcessor: MessageProcessor;
|
||||||
|
|
||||||
|
private entityUpdateTimers: Map<string, number>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
queueProvider: MessageQueueProvider,
|
||||||
|
publisher: MessagePublisher,
|
||||||
|
messageProcessor: MessageProcessor
|
||||||
|
) {
|
||||||
|
super(SystemNames.NetworkUpdate);
|
||||||
|
|
||||||
|
this.queueProvider = queueProvider;
|
||||||
|
this.publisher = publisher;
|
||||||
|
this.messageProcessor = messageProcessor;
|
||||||
|
|
||||||
|
this.entityUpdateTimers = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
public update(dt: number, game: Game) {
|
||||||
|
// 1. process new messages
|
||||||
|
this.queueProvider
|
||||||
|
.getNewMessages()
|
||||||
|
.forEach((message) => this.messageProcessor.process(message));
|
||||||
|
this.queueProvider.clearMessages();
|
||||||
|
|
||||||
|
// 2. send entity updates
|
||||||
|
const updateMessages: EntityUpdateBody[] = [];
|
||||||
|
game.forEachEntityWithComponent(
|
||||||
|
ComponentNames.NetworkUpdateable,
|
||||||
|
(entity) => {
|
||||||
|
let timer = this.entityUpdateTimers.get(entity.id) ?? dt;
|
||||||
|
timer -= dt;
|
||||||
|
this.entityUpdateTimers.set(entity.id, timer);
|
||||||
|
|
||||||
|
if (timer > 0) return;
|
||||||
|
this.entityUpdateTimers.set(entity.id, this.getNextUpdateTimeMs());
|
||||||
|
|
||||||
|
if (entity.hasComponent(ComponentNames.NetworkUpdateable)) {
|
||||||
|
updateMessages.push({
|
||||||
|
id: entity.id,
|
||||||
|
args: entity.serialize()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this.publisher.addMessage({
|
||||||
|
type: MessageType.UPDATE_ENTITIES,
|
||||||
|
body: updateMessages
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. publish changes
|
||||||
|
this.publisher.publish();
|
||||||
|
}
|
||||||
|
|
||||||
|
private getNextUpdateTimeMs() {
|
||||||
|
return Math.random() * 70 + 50;
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import { System, SystemNames } from ".";
|
import { System, SystemNames } from '.';
|
||||||
import {
|
import {
|
||||||
BoundingBox,
|
BoundingBox,
|
||||||
ComponentNames,
|
ComponentNames,
|
||||||
@ -8,11 +8,11 @@ import {
|
|||||||
Mass,
|
Mass,
|
||||||
Jump,
|
Jump,
|
||||||
Moment,
|
Moment,
|
||||||
Control,
|
Control
|
||||||
} from "../components";
|
} from '../components';
|
||||||
import { PhysicsConstants } from "../config";
|
import { PhysicsConstants } from '../config';
|
||||||
import type { Force2D } from "../interfaces";
|
import type { Force2D, Velocity2D } from '../interfaces';
|
||||||
import { Game } from "../Game";
|
import { Game } from '../Game';
|
||||||
|
|
||||||
export class Physics extends System {
|
export class Physics extends System {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -23,9 +23,11 @@ export class Physics extends System {
|
|||||||
game.forEachEntityWithComponent(ComponentNames.Forces, (entity) => {
|
game.forEachEntityWithComponent(ComponentNames.Forces, (entity) => {
|
||||||
const mass = entity.getComponent<Mass>(ComponentNames.Mass).mass;
|
const mass = entity.getComponent<Mass>(ComponentNames.Mass).mass;
|
||||||
const forces = entity.getComponent<Forces>(ComponentNames.Forces).forces;
|
const forces = entity.getComponent<Forces>(ComponentNames.Forces).forces;
|
||||||
const velocity = entity.getComponent<Velocity>(ComponentNames.Velocity);
|
const velocity = entity.getComponent<Velocity>(
|
||||||
|
ComponentNames.Velocity
|
||||||
|
).velocity;
|
||||||
const inertia = entity.getComponent<Moment>(
|
const inertia = entity.getComponent<Moment>(
|
||||||
ComponentNames.Moment,
|
ComponentNames.Moment
|
||||||
).inertia;
|
).inertia;
|
||||||
|
|
||||||
// F_g = mg, applied only until terminal velocity is reached
|
// F_g = mg, applied only until terminal velocity is reached
|
||||||
@ -35,9 +37,9 @@ export class Physics extends System {
|
|||||||
forces.push({
|
forces.push({
|
||||||
fCartesian: {
|
fCartesian: {
|
||||||
fy: mass * PhysicsConstants.GRAVITY,
|
fy: mass * PhysicsConstants.GRAVITY,
|
||||||
fx: 0,
|
fx: 0
|
||||||
},
|
},
|
||||||
torque: 0,
|
torque: 0
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -47,17 +49,17 @@ export class Physics extends System {
|
|||||||
(accum: Force2D, { fCartesian, torque }: Force2D) => ({
|
(accum: Force2D, { fCartesian, torque }: Force2D) => ({
|
||||||
fCartesian: {
|
fCartesian: {
|
||||||
fx: accum.fCartesian.fx + (fCartesian?.fx ?? 0),
|
fx: accum.fCartesian.fx + (fCartesian?.fx ?? 0),
|
||||||
fy: accum.fCartesian.fy + (fCartesian?.fy ?? 0),
|
fy: accum.fCartesian.fy + (fCartesian?.fy ?? 0)
|
||||||
},
|
},
|
||||||
torque: accum.torque + (torque ?? 0),
|
torque: accum.torque + (torque ?? 0)
|
||||||
}),
|
}),
|
||||||
{ fCartesian: { fx: 0, fy: 0 }, torque: 0 },
|
{ fCartesian: { fx: 0, fy: 0 }, torque: 0 }
|
||||||
);
|
);
|
||||||
|
|
||||||
// integrate accelerations
|
// integrate accelerations
|
||||||
const [ddy, ddx] = [
|
const [ddy, ddx] = [
|
||||||
sumOfForces.fCartesian.fy,
|
sumOfForces.fCartesian.fy,
|
||||||
sumOfForces.fCartesian.fx,
|
sumOfForces.fCartesian.fx
|
||||||
].map((x) => x / mass);
|
].map((x) => x / mass);
|
||||||
velocity.dCartesian.dx += ddx * dt;
|
velocity.dCartesian.dx += ddx * dt;
|
||||||
velocity.dCartesian.dy += ddy * dt;
|
velocity.dCartesian.dy += ddy * dt;
|
||||||
@ -73,30 +75,32 @@ export class Physics extends System {
|
|||||||
});
|
});
|
||||||
|
|
||||||
game.forEachEntityWithComponent(ComponentNames.Velocity, (entity) => {
|
game.forEachEntityWithComponent(ComponentNames.Velocity, (entity) => {
|
||||||
const velocity: Velocity = new Velocity();
|
const velocityComponent: Velocity = new Velocity();
|
||||||
const control = entity.getComponent<Control>(ComponentNames.Control);
|
const control = entity.getComponent<Control>(ComponentNames.Control);
|
||||||
|
|
||||||
velocity.add(entity.getComponent<Velocity>(ComponentNames.Velocity));
|
velocityComponent.add(
|
||||||
|
entity.getComponent<Velocity>(ComponentNames.Velocity).velocity
|
||||||
|
);
|
||||||
if (control) {
|
if (control) {
|
||||||
velocity.add(control.controlVelocity);
|
velocityComponent.add(control.controlVelocityComponent.velocity);
|
||||||
}
|
}
|
||||||
|
|
||||||
const boundingBox = entity.getComponent<BoundingBox>(
|
const boundingBox = entity.getComponent<BoundingBox>(
|
||||||
ComponentNames.BoundingBox,
|
ComponentNames.BoundingBox
|
||||||
);
|
);
|
||||||
|
|
||||||
// integrate velocity
|
// integrate velocity
|
||||||
boundingBox.center.x += velocity.dCartesian.dx * dt;
|
boundingBox.center.x += velocityComponent.velocity.dCartesian.dx * dt;
|
||||||
boundingBox.center.y += velocity.dCartesian.dy * dt;
|
boundingBox.center.y += velocityComponent.velocity.dCartesian.dy * dt;
|
||||||
boundingBox.rotation += velocity.dTheta * dt;
|
boundingBox.rotation += velocityComponent.velocity.dTheta * dt;
|
||||||
boundingBox.rotation =
|
boundingBox.rotation =
|
||||||
(boundingBox.rotation < 0
|
(boundingBox.rotation < 0
|
||||||
? 360 + boundingBox.rotation
|
? 360 + boundingBox.rotation
|
||||||
: boundingBox.rotation) % 360;
|
: boundingBox.rotation) % 360;
|
||||||
|
|
||||||
// clear the control velocity
|
// clear the control velocity
|
||||||
if (control) {
|
if (control && control.isControllable) {
|
||||||
control.controlVelocity = new Velocity();
|
control.controlVelocityComponent = new Velocity();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { System, SystemNames } from ".";
|
import { System, SystemNames } from '.';
|
||||||
import { BoundingBox, ComponentNames, Sprite } from "../components";
|
import { BoundingBox, ComponentNames, Sprite } from '../components';
|
||||||
import { Game } from "../Game";
|
import { Game } from '../Game';
|
||||||
import { clamp } from "../utils";
|
import { clamp } from '../utils';
|
||||||
|
|
||||||
export class Render extends System {
|
export class Render extends System {
|
||||||
private ctx: CanvasRenderingContext2D;
|
private ctx: CanvasRenderingContext2D;
|
||||||
@ -19,7 +19,7 @@ export class Render extends System {
|
|||||||
sprite.update(dt);
|
sprite.update(dt);
|
||||||
|
|
||||||
const boundingBox = entity.getComponent<BoundingBox>(
|
const boundingBox = entity.getComponent<BoundingBox>(
|
||||||
ComponentNames.BoundingBox,
|
ComponentNames.BoundingBox
|
||||||
);
|
);
|
||||||
|
|
||||||
// don't render if we're outside the screen
|
// don't render if we're outside the screen
|
||||||
@ -27,12 +27,12 @@ export class Render extends System {
|
|||||||
clamp(
|
clamp(
|
||||||
boundingBox.center.y,
|
boundingBox.center.y,
|
||||||
-boundingBox.dimension.height / 2,
|
-boundingBox.dimension.height / 2,
|
||||||
this.ctx.canvas.height + boundingBox.dimension.height / 2,
|
this.ctx.canvas.height + boundingBox.dimension.height / 2
|
||||||
) != boundingBox.center.y ||
|
) != boundingBox.center.y ||
|
||||||
clamp(
|
clamp(
|
||||||
boundingBox.center.x,
|
boundingBox.center.x,
|
||||||
-boundingBox.dimension.width / 2,
|
-boundingBox.dimension.width / 2,
|
||||||
this.ctx.canvas.width + boundingBox.dimension.width / 2,
|
this.ctx.canvas.width + boundingBox.dimension.width / 2
|
||||||
) != boundingBox.center.x
|
) != boundingBox.center.x
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
@ -41,7 +41,7 @@ export class Render extends System {
|
|||||||
const drawArgs = {
|
const drawArgs = {
|
||||||
center: boundingBox.center,
|
center: boundingBox.center,
|
||||||
dimension: boundingBox.dimension,
|
dimension: boundingBox.dimension,
|
||||||
rotation: boundingBox.rotation,
|
rotation: boundingBox.rotation
|
||||||
};
|
};
|
||||||
|
|
||||||
sprite.draw(this.ctx, drawArgs);
|
sprite.draw(this.ctx, drawArgs);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Game } from "../Game";
|
import { Game } from '../Game';
|
||||||
|
|
||||||
export abstract class System {
|
export abstract class System {
|
||||||
public readonly name: string;
|
public readonly name: string;
|
||||||
|
@ -1,28 +1,24 @@
|
|||||||
import { System, SystemNames } from ".";
|
import { System, SystemNames } from '.';
|
||||||
import { BoundingBox, ComponentNames } from "../components";
|
import { BoundingBox, ComponentNames } from '../components';
|
||||||
import { Game } from "../Game";
|
import { Game } from '../Game';
|
||||||
import type { Entity } from "../entities";
|
import { clamp } from '../utils';
|
||||||
import { clamp } from "../utils";
|
import { Miscellaneous } from '../config';
|
||||||
|
|
||||||
export class WallBounds extends System {
|
export class WallBounds extends System {
|
||||||
private screenWidth: number;
|
constructor() {
|
||||||
|
|
||||||
constructor(screenWidth: number) {
|
|
||||||
super(SystemNames.WallBounds);
|
super(SystemNames.WallBounds);
|
||||||
|
|
||||||
this.screenWidth = screenWidth;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public update(_dt: number, game: Game) {
|
public update(_dt: number, game: Game) {
|
||||||
game.forEachEntityWithComponent(ComponentNames.WallBounded, (entity) => {
|
game.forEachEntityWithComponent(ComponentNames.WallBounded, (entity) => {
|
||||||
const boundingBox = entity.getComponent<BoundingBox>(
|
const boundingBox = entity.getComponent<BoundingBox>(
|
||||||
ComponentNames.BoundingBox,
|
ComponentNames.BoundingBox
|
||||||
);
|
);
|
||||||
|
|
||||||
boundingBox.center.x = clamp(
|
boundingBox.center.x = clamp(
|
||||||
boundingBox.center.x,
|
boundingBox.center.x,
|
||||||
boundingBox.dimension.width / 2,
|
boundingBox.dimension.width / 2,
|
||||||
this.screenWidth - boundingBox.dimension.width / 2,
|
Miscellaneous.WIDTH - boundingBox.dimension.width / 2
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
export * from "./names";
|
export * from './names';
|
||||||
export * from "./System";
|
export * from './System';
|
||||||
export * from "./Render";
|
export * from './Render';
|
||||||
export * from "./Physics";
|
export * from './Physics';
|
||||||
export * from "./Input";
|
export * from './Input';
|
||||||
export * from "./FacingDirection";
|
export * from './FacingDirection';
|
||||||
export * from "./Collision";
|
export * from './Collision';
|
||||||
export * from "./WallBounds";
|
export * from './WallBounds';
|
||||||
|
export * from './NetworkUpdate';
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
export namespace SystemNames {
|
export namespace SystemNames {
|
||||||
export const Render = "Render";
|
export const Render = 'Render';
|
||||||
export const Physics = "Physics";
|
export const Physics = 'Physics';
|
||||||
export const FacingDirection = "FacingDirection";
|
export const FacingDirection = 'FacingDirection';
|
||||||
export const Input = "Input";
|
export const Input = 'Input';
|
||||||
export const Collision = "Collision";
|
export const Collision = 'Collision';
|
||||||
export const WallBounds = "WallBounds";
|
export const WallBounds = 'WallBounds';
|
||||||
|
export const NetworkUpdate = 'NetworkUpdate';
|
||||||
}
|
}
|
||||||
|
27
engine/utils/coding.ts
Normal file
27
engine/utils/coding.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
const replacer = (_key: any, value: any) => {
|
||||||
|
if (value instanceof Map) {
|
||||||
|
return {
|
||||||
|
dataType: 'Map',
|
||||||
|
value: Array.from(value.entries())
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const reviver = (_key: any, value: any) => {
|
||||||
|
if (typeof value === 'object' && value !== null) {
|
||||||
|
if (value.dataType === 'Map') {
|
||||||
|
return new Map(value.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const stringify = (obj: any) => {
|
||||||
|
return JSON.stringify(obj, replacer);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parse = <T>(str: string) => {
|
||||||
|
return JSON.parse(str, reviver) as unknown as T;
|
||||||
|
};
|
@ -1,4 +1,4 @@
|
|||||||
import type { Coord2D } from "../interfaces";
|
import type { Coord2D } from '../interfaces';
|
||||||
|
|
||||||
export const dotProduct = (vector1: Coord2D, vector2: Coord2D): number =>
|
export const dotProduct = (vector1: Coord2D, vector2: Coord2D): number =>
|
||||||
vector1.x * vector2.x + vector1.y * vector2.y;
|
vector1.x * vector2.x + vector1.y * vector2.y;
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
export * from "./rotateVector";
|
export * from './rotateVector';
|
||||||
export * from "./dotProduct";
|
export * from './dotProduct';
|
||||||
export * from "./clamp";
|
export * from './clamp';
|
||||||
|
export * from './coding';
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { Coord2D } from "../interfaces";
|
import type { Coord2D } from '../interfaces';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ([[cos(θ), -sin(θ),]) ([x,)
|
* ([[cos(θ), -sin(θ),]) ([x,)
|
||||||
@ -10,6 +10,6 @@ export const rotateVector = (vector: Coord2D, theta: number): Coord2D => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
x: vector.x * cos - vector.y * sin,
|
x: vector.x * cos - vector.y * sin,
|
||||||
y: vector.x * sin + vector.y * cos,
|
y: vector.x * sin + vector.y * cos
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
BIN
server/bun.lockb
BIN
server/bun.lockb
Binary file not shown.
@ -8,6 +8,5 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": "^5.0.0"
|
"typescript": "^5.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
6
server/src/constants.ts
Normal file
6
server/src/constants.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export namespace Constants {
|
||||||
|
export const SERVER_PORT = 8080;
|
||||||
|
export const SERVER_TICK_RATE = (1 / 60) * 1000;
|
||||||
|
export const GAME_TOPIC = 'game';
|
||||||
|
export const MAX_PLAYERS = 8;
|
||||||
|
}
|
59
server/src/main.ts
Normal file
59
server/src/main.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { Grid } from '@engine/structures';
|
||||||
|
import {
|
||||||
|
ServerMessageProcessor,
|
||||||
|
ServerSocketMessagePublisher,
|
||||||
|
ServerSocketMessageReceiver,
|
||||||
|
MemorySessionManager,
|
||||||
|
SessionInputSystem
|
||||||
|
} from './network';
|
||||||
|
import { Collision, NetworkUpdate, Physics, WallBounds } from '@engine/systems';
|
||||||
|
import { Game } from '@engine/Game';
|
||||||
|
import { Constants } from './constants';
|
||||||
|
import { GameServer } from './server';
|
||||||
|
import { Floor } from '@engine/entities';
|
||||||
|
import { BoundingBox } from '@engine/components';
|
||||||
|
import { Miscellaneous } from '@engine/config';
|
||||||
|
|
||||||
|
const game = new Game();
|
||||||
|
|
||||||
|
const sessionManager = new MemorySessionManager();
|
||||||
|
|
||||||
|
const messageReceiver = new ServerSocketMessageReceiver();
|
||||||
|
const messagePublisher = new ServerSocketMessagePublisher();
|
||||||
|
const messageProcessor = new ServerMessageProcessor(game, sessionManager);
|
||||||
|
|
||||||
|
const server = new GameServer(
|
||||||
|
game,
|
||||||
|
messageReceiver,
|
||||||
|
messagePublisher,
|
||||||
|
sessionManager
|
||||||
|
);
|
||||||
|
|
||||||
|
[
|
||||||
|
new SessionInputSystem(sessionManager),
|
||||||
|
new NetworkUpdate(messageReceiver, messagePublisher, messageProcessor),
|
||||||
|
new Physics(),
|
||||||
|
new Collision(new Grid()),
|
||||||
|
new WallBounds()
|
||||||
|
].forEach((system) => game.addSystem(system));
|
||||||
|
|
||||||
|
const floor = new Floor(160);
|
||||||
|
const floorHeight = 200;
|
||||||
|
|
||||||
|
floor.addComponent(
|
||||||
|
new BoundingBox(
|
||||||
|
{
|
||||||
|
x: Miscellaneous.WIDTH / 2,
|
||||||
|
y: Miscellaneous.HEIGHT + floorHeight / 2
|
||||||
|
},
|
||||||
|
{ width: Miscellaneous.WIDTH, height: floorHeight }
|
||||||
|
)
|
||||||
|
);
|
||||||
|
game.addEntity(floor);
|
||||||
|
|
||||||
|
game.start();
|
||||||
|
setInterval(() => {
|
||||||
|
game.doGameLoop(performance.now());
|
||||||
|
}, Constants.SERVER_TICK_RATE);
|
||||||
|
|
||||||
|
server.serve();
|
36
server/src/network/MessageProcessor.ts
Normal file
36
server/src/network/MessageProcessor.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import {
|
||||||
|
EntityUpdateBody,
|
||||||
|
MessageProcessor,
|
||||||
|
MessageType
|
||||||
|
} from '@engine/network';
|
||||||
|
import { ServerMessage, SessionManager } from '.';
|
||||||
|
import { Game } from '@engine/Game';
|
||||||
|
|
||||||
|
export class ServerMessageProcessor implements MessageProcessor {
|
||||||
|
private game: Game;
|
||||||
|
private sessionManager: SessionManager;
|
||||||
|
|
||||||
|
constructor(game: Game, sessionManager: SessionManager) {
|
||||||
|
this.game = game;
|
||||||
|
this.sessionManager = sessionManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public process(message: ServerMessage) {
|
||||||
|
switch (message.type) {
|
||||||
|
case MessageType.NEW_INPUT: {
|
||||||
|
const { sessionId } = message.sessionData;
|
||||||
|
const session = this.sessionManager.getSession(sessionId);
|
||||||
|
session?.inputSystem.keyPressed(message.body as string);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case MessageType.REMOVE_INPUT: {
|
||||||
|
const { sessionId } = message.sessionData;
|
||||||
|
const session = this.sessionManager.getSession(sessionId);
|
||||||
|
session?.inputSystem.keyReleased(message.body as string);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
31
server/src/network/MessagePublisher.ts
Normal file
31
server/src/network/MessagePublisher.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { Message, MessagePublisher } from '@engine/network';
|
||||||
|
import { Server } from 'bun';
|
||||||
|
import { Constants } from '../constants';
|
||||||
|
import { stringify } from '@engine/utils';
|
||||||
|
|
||||||
|
export class ServerSocketMessagePublisher implements MessagePublisher {
|
||||||
|
private server?: Server;
|
||||||
|
private messages: Message[];
|
||||||
|
|
||||||
|
constructor(server?: Server) {
|
||||||
|
this.messages = [];
|
||||||
|
|
||||||
|
if (server) this.setServer(server);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setServer(server: Server) {
|
||||||
|
this.server = server;
|
||||||
|
}
|
||||||
|
|
||||||
|
public addMessage(message: Message) {
|
||||||
|
this.messages.push(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public publish() {
|
||||||
|
if (this.messages.length) {
|
||||||
|
this.server?.publish(Constants.GAME_TOPIC, stringify(this.messages));
|
||||||
|
|
||||||
|
this.messages = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
22
server/src/network/MessageReceiver.ts
Normal file
22
server/src/network/MessageReceiver.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { MessageQueueProvider } from '@engine/network';
|
||||||
|
import type { ServerMessage } from '.';
|
||||||
|
|
||||||
|
export class ServerSocketMessageReceiver implements MessageQueueProvider {
|
||||||
|
private messages: ServerMessage[];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.messages = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public addMessage(message: ServerMessage) {
|
||||||
|
this.messages.push(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getNewMessages() {
|
||||||
|
return this.messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
public clearMessages() {
|
||||||
|
this.messages = [];
|
||||||
|
}
|
||||||
|
}
|
32
server/src/network/SessionInputSystem.ts
Normal file
32
server/src/network/SessionInputSystem.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { Game } from '@engine/Game';
|
||||||
|
import { SessionManager } from '.';
|
||||||
|
import { System } from '@engine/systems';
|
||||||
|
import { BoundingBox, ComponentNames, Control } from '@engine/components';
|
||||||
|
|
||||||
|
export class SessionInputSystem extends System {
|
||||||
|
private sessionManager: SessionManager;
|
||||||
|
|
||||||
|
constructor(sessionManager: SessionManager) {
|
||||||
|
super('SessionInputSystem');
|
||||||
|
|
||||||
|
this.sessionManager = sessionManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public update(_dt: number, game: Game) {
|
||||||
|
this.sessionManager.getSessions().forEach((sessionId) => {
|
||||||
|
const session = this.sessionManager.getSession(sessionId);
|
||||||
|
|
||||||
|
if (!session) return;
|
||||||
|
|
||||||
|
const { inputSystem } = session;
|
||||||
|
session.controllableEntities.forEach((entityId) => {
|
||||||
|
const entity = game.getEntity(entityId);
|
||||||
|
if (!entity) return;
|
||||||
|
|
||||||
|
if (entity.hasComponent(ComponentNames.Control)) {
|
||||||
|
inputSystem.handleInput(entity);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
33
server/src/network/SessionManager.ts
Normal file
33
server/src/network/SessionManager.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { Session, SessionManager } from '.';
|
||||||
|
|
||||||
|
export class MemorySessionManager implements SessionManager {
|
||||||
|
private sessions: Map<string, Session>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.sessions = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
public getSessions() {
|
||||||
|
return Array.from(this.sessions.keys());
|
||||||
|
}
|
||||||
|
|
||||||
|
public uniqueSessionId() {
|
||||||
|
return crypto.randomUUID();
|
||||||
|
}
|
||||||
|
|
||||||
|
public getSession(id: string) {
|
||||||
|
return this.sessions.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public putSession(id: string, session: Session) {
|
||||||
|
return this.sessions.set(id, session);
|
||||||
|
}
|
||||||
|
|
||||||
|
public numSessions() {
|
||||||
|
return this.sessions.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeSession(id: string) {
|
||||||
|
this.sessions.delete(id);
|
||||||
|
}
|
||||||
|
}
|
29
server/src/network/index.ts
Normal file
29
server/src/network/index.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { Message } from '@engine/network';
|
||||||
|
import { Input } from '@engine/systems';
|
||||||
|
|
||||||
|
export * from './MessageProcessor';
|
||||||
|
export * from './MessagePublisher';
|
||||||
|
export * from './MessageReceiver';
|
||||||
|
export * from './SessionManager';
|
||||||
|
export * from './SessionInputSystem';
|
||||||
|
|
||||||
|
export type SessionData = { sessionId: string };
|
||||||
|
|
||||||
|
export type Session = {
|
||||||
|
sessionId: string;
|
||||||
|
controllableEntities: Set<string>;
|
||||||
|
inputSystem: Input;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ServerMessage extends Message {
|
||||||
|
sessionData: SessionData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SessionManager {
|
||||||
|
uniqueSessionId(): string;
|
||||||
|
getSession(id: string): Session | undefined;
|
||||||
|
getSessions(): string[];
|
||||||
|
putSession(id: string, session: Session): void;
|
||||||
|
removeSession(id: string): void;
|
||||||
|
numSessions(): number;
|
||||||
|
}
|
@ -1,37 +1,174 @@
|
|||||||
import { Game } from "../../engine/Game";
|
import { Game } from '@engine/Game';
|
||||||
import { Floor, Player } from "../../engine/entities";
|
import { Player } from '@engine/entities';
|
||||||
import { WallBounds, Physics, Collision } from "../../engine/systems";
|
import { Message, MessageType } from '@engine/network';
|
||||||
import { Miscellaneous } from "../../engine/config";
|
import { Constants } from './constants';
|
||||||
|
import {
|
||||||
|
ServerSocketMessageReceiver,
|
||||||
|
ServerSocketMessagePublisher,
|
||||||
|
SessionData,
|
||||||
|
ServerMessage,
|
||||||
|
Session,
|
||||||
|
SessionManager
|
||||||
|
} from './network';
|
||||||
|
import { parse } from '@engine/utils';
|
||||||
|
import { Server, ServerWebSocket } from 'bun';
|
||||||
|
import { Input } from '@engine/systems';
|
||||||
|
import { Control, NetworkUpdateable } from '@engine/components';
|
||||||
|
import { stringify } from '@engine/utils';
|
||||||
|
|
||||||
const TICK_RATE = 60 / 1000;
|
export class GameServer {
|
||||||
|
private server?: Server;
|
||||||
|
private game: Game;
|
||||||
|
private messageReceiver: ServerSocketMessageReceiver;
|
||||||
|
private messagePublisher: ServerSocketMessagePublisher;
|
||||||
|
private sessionManager: SessionManager;
|
||||||
|
|
||||||
const game = new Game();
|
constructor(
|
||||||
|
game: Game,
|
||||||
|
messageReceiver: ServerSocketMessageReceiver,
|
||||||
|
messagePublisher: ServerSocketMessagePublisher,
|
||||||
|
sessionManager: SessionManager
|
||||||
|
) {
|
||||||
|
this.game = game;
|
||||||
|
this.messageReceiver = messageReceiver;
|
||||||
|
this.messagePublisher = messagePublisher;
|
||||||
|
this.sessionManager = sessionManager;
|
||||||
|
}
|
||||||
|
|
||||||
[
|
public serve() {
|
||||||
new Physics(),
|
if (!this.server)
|
||||||
new Collision({ width: Miscellaneous.WIDTH, height: Miscellaneous.HEIGHT }),
|
this.server = Bun.serve<SessionData>({
|
||||||
new WallBounds(Miscellaneous.WIDTH),
|
port: Constants.SERVER_PORT,
|
||||||
].forEach((system) => game.addSystem(system));
|
fetch: (req, srv) => this.fetchHandler(req, srv),
|
||||||
|
websocket: {
|
||||||
|
open: (ws) => this.openWebsocket(ws),
|
||||||
|
message: (ws, msg) => this.websocketMessage(ws, msg),
|
||||||
|
close: (ws) => this.closeWebsocket(ws)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
[new Floor(160), new Player()].forEach((entity) => game.addEntity(entity));
|
this.messagePublisher.setServer(this.server);
|
||||||
|
|
||||||
game.start();
|
console.log(`Listening on ${this.server.hostname}:${this.server.port}`);
|
||||||
setInterval(() => {
|
}
|
||||||
game.doGameLoop(performance.now());
|
|
||||||
}, TICK_RATE);
|
|
||||||
|
|
||||||
const server = Bun.serve<>({
|
private websocketMessage(
|
||||||
port: 8080,
|
websocket: ServerWebSocket<SessionData>,
|
||||||
fetch(req, server) {
|
message: string | Uint8Array
|
||||||
server.upgrade(req, {
|
) {
|
||||||
data: {},
|
if (typeof message == 'string') {
|
||||||
|
const receivedMessage = parse<ServerMessage>(message);
|
||||||
|
receivedMessage.sessionData = websocket.data;
|
||||||
|
|
||||||
|
this.messageReceiver.addMessage(receivedMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private closeWebsocket(websocket: ServerWebSocket<SessionData>) {
|
||||||
|
const { sessionId } = websocket.data;
|
||||||
|
|
||||||
|
const sessionEntities =
|
||||||
|
this.sessionManager.getSession(sessionId)!.controllableEntities;
|
||||||
|
this.sessionManager.removeSession(sessionId);
|
||||||
|
|
||||||
|
if (!sessionEntities) return;
|
||||||
|
sessionEntities.forEach((id) => this.game.removeEntity(id));
|
||||||
|
|
||||||
|
this.messagePublisher.addMessage({
|
||||||
|
type: MessageType.REMOVE_ENTITIES,
|
||||||
|
body: Array.from(sessionEntities)
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
websocket: {
|
|
||||||
// handler called when a message is received
|
private openWebsocket(websocket: ServerWebSocket<SessionData>) {
|
||||||
async message(ws, message) {
|
websocket.subscribe(Constants.GAME_TOPIC);
|
||||||
console.log(`Received ${message}`);
|
|
||||||
},
|
const { sessionId } = websocket.data;
|
||||||
},
|
if (this.sessionManager.getSession(sessionId)) {
|
||||||
});
|
return;
|
||||||
console.log(`Listening on localhost:${server.port}`);
|
}
|
||||||
|
|
||||||
|
const newSession: Session = {
|
||||||
|
sessionId,
|
||||||
|
controllableEntities: new Set(),
|
||||||
|
inputSystem: new Input(sessionId)
|
||||||
|
};
|
||||||
|
|
||||||
|
const player = new Player();
|
||||||
|
player.addComponent(new Control(sessionId));
|
||||||
|
player.addComponent(new NetworkUpdateable());
|
||||||
|
this.game.addEntity(player);
|
||||||
|
|
||||||
|
newSession.controllableEntities.add(player.id);
|
||||||
|
this.sessionManager.putSession(sessionId, newSession);
|
||||||
|
|
||||||
|
const addCurrentEntities: Message[] = [
|
||||||
|
{
|
||||||
|
type: MessageType.NEW_ENTITIES,
|
||||||
|
body: Array.from(this.game.entities.values())
|
||||||
|
.filter((entity) => entity.id != player.id)
|
||||||
|
.map((entity) => {
|
||||||
|
return {
|
||||||
|
id: entity.id,
|
||||||
|
entityName: entity.name,
|
||||||
|
args: entity.serialize()
|
||||||
|
};
|
||||||
|
})
|
||||||
|
}
|
||||||
|
];
|
||||||
|
websocket.sendText(stringify(addCurrentEntities));
|
||||||
|
|
||||||
|
const addNewPlayer: Message = {
|
||||||
|
type: MessageType.NEW_ENTITIES,
|
||||||
|
body: [
|
||||||
|
{
|
||||||
|
id: player.id,
|
||||||
|
entityName: player.name,
|
||||||
|
args: player.serialize()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
this.messagePublisher.addMessage(addNewPlayer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private fetchHandler(req: Request, server: Server): Response {
|
||||||
|
const url = new URL(req.url);
|
||||||
|
|
||||||
|
const headers = new Headers();
|
||||||
|
headers.set('Access-Control-Allow-Origin', '*');
|
||||||
|
|
||||||
|
if (url.pathname == '/assign') {
|
||||||
|
if (this.sessionManager.numSessions() > Constants.MAX_PLAYERS)
|
||||||
|
return new Response('too many players', { headers, status: 400 });
|
||||||
|
|
||||||
|
const sessionId = crypto.randomUUID();
|
||||||
|
headers.set('Set-Cookie', `SessionId=${sessionId};`);
|
||||||
|
|
||||||
|
return new Response(sessionId, { headers });
|
||||||
|
}
|
||||||
|
|
||||||
|
const cookie = req.headers.get('cookie');
|
||||||
|
if (!cookie) {
|
||||||
|
return new Response('No session', { headers, status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionId = cookie.split(';').at(0)!.split('SessionId=').at(1);
|
||||||
|
|
||||||
|
if (url.pathname == '/game') {
|
||||||
|
server.upgrade(req, {
|
||||||
|
headers,
|
||||||
|
data: {
|
||||||
|
sessionId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Response('upgraded to ws', { headers });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.pathname == '/me') {
|
||||||
|
return new Response(sessionId, { headers });
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response('Not found', { headers, status: 404 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,21 +1,27 @@
|
|||||||
{
|
{
|
||||||
|
"extends": "../tsconfig.engine.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"lib": ["ESNext"],
|
// add Bun type definitions
|
||||||
|
"types": ["bun-types"],
|
||||||
|
|
||||||
|
// enable latest features
|
||||||
|
"lib": ["esnext"],
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"target": "esnext",
|
"target": "esnext",
|
||||||
|
|
||||||
|
// if TS 5.x+
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"moduleDetection": "force",
|
|
||||||
"allowImportingTsExtensions": true,
|
|
||||||
"strict": true,
|
|
||||||
"downlevelIteration": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"jsx": "preserve",
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"allowJs": true,
|
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"types": [
|
"allowImportingTsExtensions": true,
|
||||||
"bun-types" // add Bun global
|
"moduleDetection": "force",
|
||||||
]
|
|
||||||
|
"jsx": "react-jsx", // support JSX
|
||||||
|
"allowJs": true, // allow importing `.js` from `.ts`
|
||||||
|
"esModuleInterop": true, // allow default imports for CommonJS modules
|
||||||
|
|
||||||
|
// best practices
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"skipLibCheck": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
15
tsconfig.engine.json
Normal file
15
tsconfig.engine.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"paths": {
|
||||||
|
"@engine/*": ["./engine/*"],
|
||||||
|
"@engine/components": ["./engine/components"],
|
||||||
|
"@engine/config": ["./engine/config"],
|
||||||
|
"@engine/entities": ["./engine/entities"],
|
||||||
|
"@engine/interfaces": ["./engine/interfaces"],
|
||||||
|
"@engine/structures": ["./engine/structures"],
|
||||||
|
"@engine/systems": ["./engine/systems"],
|
||||||
|
"@engine/utils": ["./engine/utils"],
|
||||||
|
"@engine/network": ["./engine/network"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user