Compare commits
No commits in common. "962f867ed97a9bfb60589fd41201259120efa884" and "772308a2995e49469ff27d220ff56ff7ef9551b6" have entirely different histories.
962f867ed9
...
772308a299
|
@ -1,70 +0,0 @@
|
||||||
/**
|
|
||||||
* This is intended to be a basic starting point for linting in your app.
|
|
||||||
* It relies on recommended configs out of the box for simplicity, but you can
|
|
||||||
* and should modify this configuration to best suit your team's needs.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** @type {import('eslint').Linter.Config} */
|
|
||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: 'latest',
|
|
||||||
sourceType: 'module',
|
|
||||||
ecmaFeatures: {
|
|
||||||
jsx: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
env: {
|
|
||||||
browser: true,
|
|
||||||
commonjs: true,
|
|
||||||
es6: true,
|
|
||||||
},
|
|
||||||
globals: {
|
|
||||||
process: false,
|
|
||||||
},
|
|
||||||
ignorePatterns: ['!**/.server', '!**/.client'],
|
|
||||||
|
|
||||||
// Base config
|
|
||||||
extends: ['eslint:recommended'],
|
|
||||||
|
|
||||||
overrides: [
|
|
||||||
// React
|
|
||||||
{
|
|
||||||
files: ['**/*.{js,jsx,ts,tsx}'],
|
|
||||||
plugins: ['react', 'jsx-a11y'],
|
|
||||||
extends: [
|
|
||||||
'plugin:react/recommended',
|
|
||||||
'plugin:react/jsx-runtime',
|
|
||||||
'plugin:react-hooks/recommended',
|
|
||||||
'plugin:jsx-a11y/recommended',
|
|
||||||
],
|
|
||||||
settings: {
|
|
||||||
react: {
|
|
||||||
version: 'detect',
|
|
||||||
},
|
|
||||||
formComponents: ['Form'],
|
|
||||||
linkComponents: [
|
|
||||||
{ name: 'Link', linkAttribute: 'to' },
|
|
||||||
{ name: 'NavLink', linkAttribute: 'to' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
'react/prop-types': 'off',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// Node
|
|
||||||
{
|
|
||||||
files: [
|
|
||||||
'app/websocket.js',
|
|
||||||
'.eslintrc.cjs',
|
|
||||||
'server.js',
|
|
||||||
'vite.config.js',
|
|
||||||
'public/assets/tileset.js',
|
|
||||||
],
|
|
||||||
env: {
|
|
||||||
node: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
7
.gitignore
vendored
7
.gitignore
vendored
|
@ -1,5 +1,2 @@
|
||||||
node_modules
|
/indev
|
||||||
|
/node_modules
|
||||||
/.cache
|
|
||||||
/build
|
|
||||||
.env
|
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
process.env.STORYBOOK = 1
|
|
||||||
|
|
||||||
/** @type { import('@storybook/react-vite').StorybookConfig } */
|
/** @type { import('@storybook/react-vite').StorybookConfig } */
|
||||||
const config = {
|
const config = {
|
||||||
stories: ['../stories/**/*.mdx', '../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
|
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
|
||||||
addons: [
|
addons: [
|
||||||
'@chromatic-com/storybook',
|
'@chromatic-com/storybook',
|
||||||
'@storybook/addon-links',
|
'@storybook/addon-links',
|
||||||
|
|
36
.vscode/launch.json
vendored
36
.vscode/launch.json
vendored
|
@ -4,13 +4,6 @@
|
||||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
|
||||||
"type": "chrome",
|
|
||||||
"request": "launch",
|
|
||||||
"name": "Silphius Chrome",
|
|
||||||
"url": "https://localhost:3000",
|
|
||||||
"webRoot": "${workspaceFolder}",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"type": "node",
|
"type": "node",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
|
@ -21,14 +14,7 @@
|
||||||
"resolveSourceMapLocations": [],
|
"resolveSourceMapLocations": [],
|
||||||
"cwd": "${workspaceFolder}",
|
"cwd": "${workspaceFolder}",
|
||||||
"runtimeExecutable": "npm",
|
"runtimeExecutable": "npm",
|
||||||
"runtimeArgs": ["run", "dev"],
|
"runtimeArgs": ["run", "dev", "--", "--host", "0.0.0.0"],
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "chrome",
|
|
||||||
"request": "launch",
|
|
||||||
"name": "Storybook Chrome",
|
|
||||||
"url": "http://localhost:6006",
|
|
||||||
"webRoot": "${workspaceFolder}",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "node",
|
"type": "node",
|
||||||
|
@ -42,21 +28,25 @@
|
||||||
"runtimeExecutable": "npm",
|
"runtimeExecutable": "npm",
|
||||||
"runtimeArgs": ["run", "storybook", "--", "--no-open"],
|
"runtimeArgs": ["run", "storybook", "--", "--no-open"],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "chrome",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Chrome",
|
||||||
|
"url": "",
|
||||||
|
"webRoot": "${workspaceFolder}",
|
||||||
|
"runtimeArgs": [
|
||||||
|
"http://localhost:5173",
|
||||||
|
"http://localhost:6006",
|
||||||
|
]
|
||||||
|
},
|
||||||
],
|
],
|
||||||
"compounds": [
|
"compounds": [
|
||||||
{
|
{
|
||||||
"name": "Silphius",
|
"name": "Silphius",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
"Silphius Dev",
|
"Silphius Dev",
|
||||||
"Silphius Chrome",
|
|
||||||
],
|
|
||||||
"stopAll": true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Storybook",
|
|
||||||
"configurations": [
|
|
||||||
"Storybook Dev",
|
"Storybook Dev",
|
||||||
"Storybook Chrome",
|
"Chrome",
|
||||||
],
|
],
|
||||||
"stopAll": true,
|
"stopAll": true,
|
||||||
}
|
}
|
||||||
|
|
27
README.md
27
README.md
|
@ -1,27 +0,0 @@
|
||||||
# Silphius
|
|
||||||
|
|
||||||
All the world's a game!
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
Run the dev server:
|
|
||||||
|
|
||||||
```shellscript
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
## Deployment
|
|
||||||
|
|
||||||
First, build your app for production:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
Then run the app in production mode:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm start
|
|
||||||
```
|
|
||||||
|
|
||||||
Now you'll need to pick a host to deploy it to.
|
|
|
@ -1,3 +0,0 @@
|
||||||
export default {
|
|
||||||
frame: {type: 'uint16'},
|
|
||||||
};
|
|
|
@ -1,4 +0,0 @@
|
||||||
export default {
|
|
||||||
x: {type: 'uint16'},
|
|
||||||
y: {type: 'uint16'},
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
export default {
|
|
||||||
x: {type: 'uint16'},
|
|
||||||
y: {type: 'uint16'},
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
export default {
|
|
||||||
up: {type: 'float32'},
|
|
||||||
right: {type: 'float32'},
|
|
||||||
down: {type: 'float32'},
|
|
||||||
left: {type: 'float32'},
|
|
||||||
};
|
|
|
@ -1,3 +0,0 @@
|
||||||
export default {
|
|
||||||
direction: {type: 'uint8'},
|
|
||||||
};
|
|
|
@ -1,3 +0,0 @@
|
||||||
import gather from '@/engine/gather.js';
|
|
||||||
|
|
||||||
export default gather(import.meta.glob('./*.js', {eager: true, import: 'default'}));
|
|
|
@ -1 +0,0 @@
|
||||||
export default {};
|
|
|
@ -1,4 +0,0 @@
|
||||||
export default {
|
|
||||||
x: {type: 'float32'},
|
|
||||||
y: {type: 'float32'},
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
export default {
|
|
||||||
x: {type: 'float32'},
|
|
||||||
y: {type: 'float32'},
|
|
||||||
};
|
|
|
@ -1 +0,0 @@
|
||||||
export default {};
|
|
|
@ -1,8 +0,0 @@
|
||||||
export default {
|
|
||||||
animation: {type: 'string'},
|
|
||||||
elapsed: {type: 'float32'},
|
|
||||||
frame: {type: 'uint16'},
|
|
||||||
frames: {type: 'uint16'},
|
|
||||||
source: {type: 'string'},
|
|
||||||
speed: {type: 'float32'},
|
|
||||||
};
|
|
|
@ -1,16 +0,0 @@
|
||||||
export default {
|
|
||||||
layers: {
|
|
||||||
type: 'array',
|
|
||||||
subtype: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
data: {
|
|
||||||
type: 'array',
|
|
||||||
subtype: {
|
|
||||||
type: 'uint16',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1,6 +0,0 @@
|
||||||
export default {
|
|
||||||
x0: {type: 'float32'},
|
|
||||||
x1: {type: 'float32'},
|
|
||||||
y0: {type: 'float32'},
|
|
||||||
y1: {type: 'float32'},
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
export default {};
|
|
|
@ -1,3 +0,0 @@
|
||||||
export default {
|
|
||||||
world: {type: 'uint16'},
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
import {System} from '@/ecs/index.js';
|
|
||||||
|
|
||||||
export default class ApplyMomentum extends System {
|
|
||||||
|
|
||||||
static queries() {
|
|
||||||
return {
|
|
||||||
default: ['Position', 'Momentum'],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
tick(elapsed) {
|
|
||||||
for (const [position, momentum] of this.select('default')) {
|
|
||||||
position.x += elapsed * momentum.x;
|
|
||||||
position.y += elapsed * momentum.y;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
import {System} from '@/ecs/index.js';
|
|
||||||
|
|
||||||
export default class CalculateAabbs extends System {
|
|
||||||
|
|
||||||
tick() {
|
|
||||||
const {diff} = this.ecs;
|
|
||||||
for (const id in diff) {
|
|
||||||
if (diff[id].Position) {
|
|
||||||
const {Position: {x, y}, VisibleAabb} = this.ecs.get(id);
|
|
||||||
if (VisibleAabb) {
|
|
||||||
VisibleAabb.x0 = x - 32;
|
|
||||||
VisibleAabb.x1 = x + 32;
|
|
||||||
VisibleAabb.y0 = y - 32;
|
|
||||||
VisibleAabb.y1 = y + 32;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
import {System} from '@/ecs/index.js';
|
|
||||||
|
|
||||||
export default class ClampPositions extends System {
|
|
||||||
|
|
||||||
tick() {
|
|
||||||
const {diff} = this.ecs;
|
|
||||||
const {AreaSize} = this.ecs.get(1);
|
|
||||||
for (const id in diff) {
|
|
||||||
if (diff[id].Position) {
|
|
||||||
const {Position} = this.ecs.get(id);
|
|
||||||
if (Position.x < 0) {
|
|
||||||
Position.x = 0;
|
|
||||||
}
|
|
||||||
if (Position.y < 0) {
|
|
||||||
Position.y = 0;
|
|
||||||
}
|
|
||||||
if (Position.x >= AreaSize.x) {
|
|
||||||
Position.x = AreaSize.x - 0.0001;
|
|
||||||
}
|
|
||||||
if (Position.y >= AreaSize.y) {
|
|
||||||
Position.y = AreaSize.y - 0.0001;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
import {System} from '@/ecs/index.js';
|
|
||||||
|
|
||||||
export default class ControlDirection extends System {
|
|
||||||
|
|
||||||
tick() {
|
|
||||||
const {diff} = this.ecs;
|
|
||||||
for (const id in diff) {
|
|
||||||
const {Controlled} = diff[id];
|
|
||||||
if (Controlled) {
|
|
||||||
const {Controlled: {up, right, down, left}, Direction} = this.ecs.get(id);
|
|
||||||
if (up > 0) {
|
|
||||||
Direction.direction = 0;
|
|
||||||
}
|
|
||||||
if (down > 0) {
|
|
||||||
Direction.direction = 2;
|
|
||||||
}
|
|
||||||
if (left > 0) {
|
|
||||||
Direction.direction = 3;
|
|
||||||
}
|
|
||||||
if (right > 0) {
|
|
||||||
Direction.direction = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
import {System} from '@/ecs/index.js';
|
|
||||||
|
|
||||||
const SPEED = 100;
|
|
||||||
|
|
||||||
export default class ControlMovement extends System {
|
|
||||||
|
|
||||||
static queries() {
|
|
||||||
return {
|
|
||||||
default: ['Controlled', 'Momentum'],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
tick() {
|
|
||||||
for (const [controlled, momentum] of this.select('default')) {
|
|
||||||
momentum.x = SPEED * (controlled.right - controlled.left);
|
|
||||||
momentum.y = SPEED * (controlled.down - controlled.up);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
import {RESOLUTION} from '@/constants.js'
|
|
||||||
import {System} from '@/ecs/index.js';
|
|
||||||
|
|
||||||
export default class FollowCamera extends System {
|
|
||||||
|
|
||||||
reindex(entities) {
|
|
||||||
super.reindex(entities);
|
|
||||||
for (const id of entities) {
|
|
||||||
this.updateCamera(this.ecs.get(id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tick() {
|
|
||||||
const {diff} = this.ecs;
|
|
||||||
for (const id in diff) {
|
|
||||||
if (diff[id].Position) {
|
|
||||||
this.updateCamera(this.ecs.get(id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateCamera(entity) {
|
|
||||||
const {Camera, Position} = entity;
|
|
||||||
if (Camera && Position) {
|
|
||||||
const {AreaSize: {x, y}} = this.ecs.get(1);
|
|
||||||
const [hx, hy] = [RESOLUTION.x / 2, RESOLUTION.y / 2];
|
|
||||||
Camera.x = Math.max(hx, Math.min(Position.x, x - hx));
|
|
||||||
Camera.y = Math.max(hy, Math.min(Position.y, y - hy));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
import {System} from '@/ecs/index.js';
|
|
||||||
|
|
||||||
export default class ControlMovement extends System {
|
|
||||||
|
|
||||||
static queries() {
|
|
||||||
return {
|
|
||||||
default: ['Sprite'],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
tick(elapsed) {
|
|
||||||
for (const [Sprite] of this.select('default')) {
|
|
||||||
Sprite.elapsed += elapsed / Sprite.speed;
|
|
||||||
while (Sprite.elapsed > 1) {
|
|
||||||
Sprite.elapsed -= 1;
|
|
||||||
Sprite.frame = (Sprite.frame + 1) % Sprite.frames;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
import {System} from '@/ecs/index.js';
|
|
||||||
|
|
||||||
export default class SpriteDirection extends System {
|
|
||||||
|
|
||||||
static queries() {
|
|
||||||
return {
|
|
||||||
default: ['Sprite'],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
tick() {
|
|
||||||
for (const [Sprite, entityId] of this.select('default')) {
|
|
||||||
const entity = this.ecs.get(entityId);
|
|
||||||
const parts = [];
|
|
||||||
if (entity.Controlled) {
|
|
||||||
const {up, right, down, left} = entity.Controlled;
|
|
||||||
if (up > 0 || right > 0 || down > 0 || left > 0) {
|
|
||||||
parts.push('moving');
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
parts.push('idle');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (entity.Direction) {
|
|
||||||
const name = {
|
|
||||||
0: 'up',
|
|
||||||
1: 'right',
|
|
||||||
2: 'down',
|
|
||||||
3: 'left',
|
|
||||||
};
|
|
||||||
parts.push(name[entity.Direction.direction]);
|
|
||||||
}
|
|
||||||
if (parts.length > 0) {
|
|
||||||
Sprite.animation = parts.join(':');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,113 +0,0 @@
|
||||||
import {RESOLUTION} from '@/constants.js'
|
|
||||||
import {System} from '@/ecs/index.js';
|
|
||||||
|
|
||||||
class SpatialHash {
|
|
||||||
|
|
||||||
constructor({x, y}) {
|
|
||||||
this.area = {x, y};
|
|
||||||
this.chunkSize = {x: RESOLUTION.x / 2, y: RESOLUTION.y / 2};
|
|
||||||
this.chunks = Array(Math.ceil(this.area.x / this.chunkSize.x))
|
|
||||||
.fill(0)
|
|
||||||
.map(() => (
|
|
||||||
Array(Math.ceil(this.area.y / this.chunkSize.y))
|
|
||||||
.fill(0)
|
|
||||||
.map(() => [])
|
|
||||||
));
|
|
||||||
this.data = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
clamp(x, y) {
|
|
||||||
return [
|
|
||||||
Math.max(0, Math.min(x, this.area.x - 1)),
|
|
||||||
Math.max(0, Math.min(y, this.area.y - 1))
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
chunkIndex(x, y) {
|
|
||||||
const [cx, cy] = this.clamp(x, y);
|
|
||||||
return [
|
|
||||||
Math.floor(cx / this.chunkSize.x),
|
|
||||||
Math.floor(cy / this.chunkSize.y),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
remove(datum) {
|
|
||||||
if (datum in this.data) {
|
|
||||||
for (const [cx, cy] of this.data[datum]) {
|
|
||||||
const chunk = this.chunks[cx][cy];
|
|
||||||
chunk.splice(chunk.indexOf(datum), 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.data[datum] = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
update({x0, x1, y0, y1}, datum) {
|
|
||||||
this.remove(datum);
|
|
||||||
for (const [x, y] of [[x0, y0], [x0, y1], [x1, y0], [x1, y1]]) {
|
|
||||||
const [cx, cy] = this.chunkIndex(x, y);
|
|
||||||
this.data[datum].push([cx, cy]);
|
|
||||||
this.chunks[cx][cy].push(datum);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class UpdateSpatialHash extends System {
|
|
||||||
|
|
||||||
constructor(ecs) {
|
|
||||||
super(ecs);
|
|
||||||
const master = ecs.get(1);
|
|
||||||
this.hash = new SpatialHash(master.AreaSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
deindex(entities) {
|
|
||||||
super.deindex(entities);
|
|
||||||
for (const id of entities) {
|
|
||||||
this.hash.remove(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
reindex(entities) {
|
|
||||||
super.reindex(entities);
|
|
||||||
for (const id of entities) {
|
|
||||||
this.updateHash(this.ecs.get(id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateHash(entity) {
|
|
||||||
if (!entity.VisibleAabb) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.hash.update(entity.VisibleAabb, entity.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
tick() {
|
|
||||||
const {diff} = this.ecs;
|
|
||||||
for (const id in diff) {
|
|
||||||
if (diff[id].VisibleAabb) {
|
|
||||||
this.updateHash(this.ecs.get(id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
nearby(entity) {
|
|
||||||
const [cx0, cy0] = this.hash.chunkIndex(
|
|
||||||
entity.Position.x - RESOLUTION.x * 0.75,
|
|
||||||
entity.Position.y - RESOLUTION.x * 0.75,
|
|
||||||
);
|
|
||||||
const [cx1, cy1] = this.hash.chunkIndex(
|
|
||||||
entity.Position.x + RESOLUTION.x * 0.75,
|
|
||||||
entity.Position.y + RESOLUTION.x * 0.75,
|
|
||||||
);
|
|
||||||
const nearby = new Set();
|
|
||||||
for (let cy = cy0; cy <= cy1; ++cy) {
|
|
||||||
for (let cx = cx0; cx <= cx1; ++cx) {
|
|
||||||
this.hash.chunks[cx][cy].forEach((id) => {
|
|
||||||
nearby.add(this.ecs.get(id));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nearby;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,109 +0,0 @@
|
||||||
import Base from './base.js';
|
|
||||||
|
|
||||||
export default class Arbitrary extends Base {
|
|
||||||
|
|
||||||
data = [];
|
|
||||||
|
|
||||||
serializer;
|
|
||||||
|
|
||||||
Instance;
|
|
||||||
|
|
||||||
allocateMany(count) {
|
|
||||||
if (!this.Instance) {
|
|
||||||
this.Instance = this.instanceFromSchema();
|
|
||||||
}
|
|
||||||
const results = super.allocateMany(count);
|
|
||||||
count -= results.length; while (count--) {
|
|
||||||
results.push(this.data.push(new this.Instance()) - 1);
|
|
||||||
}
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
createMany(entries) {
|
|
||||||
if (entries.length > 0) {
|
|
||||||
const allocated = this.allocateMany(entries.length);
|
|
||||||
const keys = Object.keys(this.constructor.properties);
|
|
||||||
for (let i = 0; i < entries.length; ++i) {
|
|
||||||
const [entityId, values = {}] = entries[i];
|
|
||||||
this.map[entityId] = allocated[i];
|
|
||||||
this.data[allocated[i]].entity = entityId;
|
|
||||||
if (false === values) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (let k = 0; k < keys.length; ++k) {
|
|
||||||
const j = keys[k];
|
|
||||||
const {defaultValue} = this.constructor.properties[j];
|
|
||||||
if (j in values) {
|
|
||||||
this.data[allocated[i]][j] = values[j];
|
|
||||||
}
|
|
||||||
else if ('undefined' !== typeof defaultValue) {
|
|
||||||
this.data[allocated[i]][j] = defaultValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deserialize(entityId, view, offset) {
|
|
||||||
const {properties} = this.constructor;
|
|
||||||
const instance = this.get(entityId);
|
|
||||||
const deserialized = this.constructor.schema.deserialize(view, offset);
|
|
||||||
for (const key in properties) {
|
|
||||||
instance[key] = deserialized[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
serialize(entityId, view, offset) {
|
|
||||||
this.constructor.schema.serialize(this.get(entityId), view, offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
get(entityId) {
|
|
||||||
return this.data[this.map[entityId]];
|
|
||||||
}
|
|
||||||
|
|
||||||
instanceFromSchema() {
|
|
||||||
const Component = this;
|
|
||||||
const Instance = class {
|
|
||||||
$$entity = 0;
|
|
||||||
constructor() {
|
|
||||||
this.$$reset();
|
|
||||||
}
|
|
||||||
$$reset() {
|
|
||||||
const {properties} = Component.constructor;
|
|
||||||
for (const key in properties) {
|
|
||||||
const {defaultValue} = properties[key];
|
|
||||||
this[`$$${key}`] = defaultValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
toJSON() {
|
|
||||||
return Component.constructor.filterDefaults(this);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const properties = {};
|
|
||||||
properties.entity = {
|
|
||||||
get: function get() {
|
|
||||||
return this.$$entity;
|
|
||||||
},
|
|
||||||
set: function set(v) {
|
|
||||||
this.$$entity = v;
|
|
||||||
this.$$reset();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
for (const key in Component.constructor.properties) {
|
|
||||||
properties[key] = {
|
|
||||||
get: function get() {
|
|
||||||
return this[`$$${key}`];
|
|
||||||
},
|
|
||||||
set: function set(value) {
|
|
||||||
if (this[`$$${key}`] !== value) {
|
|
||||||
this[`$$${key}`] = value;
|
|
||||||
Component.markChange(this.entity, key, value);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Object.defineProperties(Instance.prototype, properties);
|
|
||||||
return Instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,55 +0,0 @@
|
||||||
import {expect, test} from 'vitest';
|
|
||||||
|
|
||||||
import Schema from './schema.js';
|
|
||||||
import Arbitrary from './arbitrary.js';
|
|
||||||
|
|
||||||
test('creates instances', () => {
|
|
||||||
class CreatingArbitrary extends Arbitrary {
|
|
||||||
static schema = new Schema({
|
|
||||||
type: 'object',
|
|
||||||
properties: {foo: {defaultValue: 'bar', type: 'string'}},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const Component = new CreatingArbitrary();
|
|
||||||
Component.create(1);
|
|
||||||
expect(Component.get(1).entity)
|
|
||||||
.to.equal(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('does not serialize default values', () => {
|
|
||||||
class CreatingArbitrary extends Arbitrary {
|
|
||||||
static schema = new Schema({
|
|
||||||
type: 'object',
|
|
||||||
properties: {foo: {defaultValue: 'bar', type: 'string'}, bar: {type: 'uint8'}},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const Component = new CreatingArbitrary();
|
|
||||||
Component.create(1)
|
|
||||||
expect(Component.get(1).toJSON())
|
|
||||||
.to.deep.equal({});
|
|
||||||
Component.get(1).bar = 1;
|
|
||||||
expect(Component.get(1).toJSON())
|
|
||||||
.to.deep.equal({bar: 1});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('reuses instances', () => {
|
|
||||||
class ReusingArbitrary extends Arbitrary {
|
|
||||||
static schema = new Schema({
|
|
||||||
type: 'object',
|
|
||||||
properties: {foo: {type: 'string'}},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const Component = new ReusingArbitrary();
|
|
||||||
Component.create(1);
|
|
||||||
const instance = Component.get(1);
|
|
||||||
Component.destroy(1);
|
|
||||||
expect(Component.get(1))
|
|
||||||
.to.be.undefined;
|
|
||||||
expect(() => {
|
|
||||||
Component.destroy(1);
|
|
||||||
})
|
|
||||||
.to.throw();
|
|
||||||
Component.create(1);
|
|
||||||
expect(Component.get(1))
|
|
||||||
.to.equal(instance);
|
|
||||||
});
|
|
|
@ -1,98 +0,0 @@
|
||||||
export default class Base {
|
|
||||||
|
|
||||||
map = [];
|
|
||||||
|
|
||||||
pool = [];
|
|
||||||
|
|
||||||
static schema;
|
|
||||||
|
|
||||||
allocateMany(count) {
|
|
||||||
const results = [];
|
|
||||||
while (count-- > 0 && this.pool.length > 0) {
|
|
||||||
results.push(this.pool.pop());
|
|
||||||
}
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
create(entityId, values) {
|
|
||||||
this.createMany([[entityId, values]]);
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy(entityId) {
|
|
||||||
this.destroyMany([entityId]);
|
|
||||||
}
|
|
||||||
|
|
||||||
destroyMany(entities) {
|
|
||||||
this.freeMany(
|
|
||||||
entities
|
|
||||||
.map((entityId) => {
|
|
||||||
if ('undefined' !== typeof this.map[entityId]) {
|
|
||||||
return this.map[entityId];
|
|
||||||
}
|
|
||||||
throw new Error(`can't free for non-existent id ${entityId}`);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
for (let i = 0; i < entities.length; i++) {
|
|
||||||
this.map[entities[i]] = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static filterDefaults(instance) {
|
|
||||||
const json = {};
|
|
||||||
for (const key in this.properties) {
|
|
||||||
const {defaultValue} = this.properties[key];
|
|
||||||
if (key in instance && instance[key] !== defaultValue) {
|
|
||||||
json[key] = instance[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
freeMany(indices) {
|
|
||||||
for (let i = 0; i < indices.length; ++i) {
|
|
||||||
this.pool.push(indices[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
insertMany(entities) {
|
|
||||||
const creating = [];
|
|
||||||
for (let i = 0; i < entities.length; i++) {
|
|
||||||
const [entityId, values] = entities[i];
|
|
||||||
if (!this.get(entityId)) {
|
|
||||||
creating.push([entityId, values]);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const instance = this.get(entityId);
|
|
||||||
for (const i in values) {
|
|
||||||
instance[i] = values[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.createMany(creating);
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
markChange(entityId, components) {}
|
|
||||||
|
|
||||||
mergeDiff(original, update) {
|
|
||||||
return {...original, ...update};
|
|
||||||
}
|
|
||||||
|
|
||||||
static get properties() {
|
|
||||||
return this.schema.specification.properties;
|
|
||||||
}
|
|
||||||
|
|
||||||
sizeOf(entityId) {
|
|
||||||
return this.constructor.schema.sizeOf(this.get(entityId));
|
|
||||||
}
|
|
||||||
|
|
||||||
static wrap(name, ecs) {
|
|
||||||
class WrappedComponent extends this {
|
|
||||||
markChange(entityId, key, value) {
|
|
||||||
ecs.markChange(entityId, {[name]: {[key]: value}})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new WrappedComponent();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
import Arbitrary from './arbitrary.js';
|
|
||||||
import Base from './base.js';
|
|
||||||
import Schema from './schema.js';
|
|
||||||
|
|
||||||
export default function Component(specificationOrClass) {
|
|
||||||
if (specificationOrClass instanceof Base) {
|
|
||||||
return specificationOrClass;
|
|
||||||
}
|
|
||||||
// Why the rigamarole? Maybe we'll implement a flat component for direct binary storage
|
|
||||||
// eventually.
|
|
||||||
return class AdhocComponent extends Arbitrary {
|
|
||||||
static schema = new Schema({type: 'object', properties: specificationOrClass});
|
|
||||||
};
|
|
||||||
}
|
|
401
app/ecs/ecs.js
401
app/ecs/ecs.js
|
@ -1,401 +0,0 @@
|
||||||
import Component from './component.js';
|
|
||||||
import EntityFactory from './entity-factory.js';
|
|
||||||
import System from './system.js';
|
|
||||||
|
|
||||||
export default class Ecs {
|
|
||||||
|
|
||||||
$$caret = 1;
|
|
||||||
|
|
||||||
diff = {};
|
|
||||||
|
|
||||||
static Types = {};
|
|
||||||
|
|
||||||
Types = {};
|
|
||||||
|
|
||||||
$$entities = {};
|
|
||||||
|
|
||||||
$$entityFactory = new EntityFactory();
|
|
||||||
|
|
||||||
$$systems = [];
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
const {Types} = this.constructor;
|
|
||||||
for (const i in Types) {
|
|
||||||
this.Types[i] = Component(Types[i]).wrap(i, this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addSystem(source) {
|
|
||||||
const system = System.wrap(source, this);
|
|
||||||
this.$$systems.push(system);
|
|
||||||
system.reindex(this.entities);
|
|
||||||
}
|
|
||||||
|
|
||||||
apply(patch) {
|
|
||||||
const creating = [];
|
|
||||||
const destroying = [];
|
|
||||||
const removing = [];
|
|
||||||
const updating = [];
|
|
||||||
for (const entityId in patch) {
|
|
||||||
const components = patch[entityId];
|
|
||||||
if (false === components) {
|
|
||||||
destroying.push(entityId);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const componentsToRemove = [];
|
|
||||||
const componentsToUpdate = {};
|
|
||||||
for (const i in components) {
|
|
||||||
if (false === components[i]) {
|
|
||||||
componentsToRemove.push(i);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
componentsToUpdate[i] = components[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (componentsToRemove.length > 0) {
|
|
||||||
removing.push([entityId, componentsToRemove]);
|
|
||||||
}
|
|
||||||
if (this.$$entities[entityId]) {
|
|
||||||
updating.push([entityId, componentsToUpdate]);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
creating.push([entityId, componentsToUpdate]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.destroyMany(destroying);
|
|
||||||
this.insertMany(updating);
|
|
||||||
this.removeMany(removing);
|
|
||||||
this.createManySpecific(creating);
|
|
||||||
}
|
|
||||||
|
|
||||||
create(components = {}) {
|
|
||||||
const [entityId] = this.createMany([components]);
|
|
||||||
return entityId;
|
|
||||||
}
|
|
||||||
|
|
||||||
createMany(componentsList) {
|
|
||||||
const specificsList = [];
|
|
||||||
for (const components of componentsList) {
|
|
||||||
specificsList.push([this.$$caret++, components]);
|
|
||||||
}
|
|
||||||
return this.createManySpecific(specificsList);
|
|
||||||
}
|
|
||||||
|
|
||||||
createManySpecific(specificsList) {
|
|
||||||
const entityIds = [];
|
|
||||||
const creating = {};
|
|
||||||
for (let i = 0; i < specificsList.length; i++) {
|
|
||||||
const [entityId, components] = specificsList[i];
|
|
||||||
const componentKeys = [];
|
|
||||||
for (const key of Object.keys(components)) {
|
|
||||||
if (this.Types[key]) {
|
|
||||||
componentKeys.push(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
entityIds.push(entityId);
|
|
||||||
this.rebuild(entityId, () => componentKeys);
|
|
||||||
for (const component of componentKeys) {
|
|
||||||
if (!creating[component]) {
|
|
||||||
creating[component] = [];
|
|
||||||
}
|
|
||||||
creating[component].push([entityId, components[component]]);
|
|
||||||
}
|
|
||||||
this.markChange(entityId, components);
|
|
||||||
}
|
|
||||||
for (const i in creating) {
|
|
||||||
this.Types[i].createMany(creating[i]);
|
|
||||||
}
|
|
||||||
this.reindex(entityIds);
|
|
||||||
return entityIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
createSpecific(entityId, components) {
|
|
||||||
return this.createManySpecific([[entityId, components]]);
|
|
||||||
}
|
|
||||||
|
|
||||||
deindex(entityIds) {
|
|
||||||
for (let i = 0; i < this.$$systems.length; i++) {
|
|
||||||
this.$$systems[i].deindex(entityIds);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static deserialize(view) {
|
|
||||||
const ecs = new this();
|
|
||||||
let cursor = 0;
|
|
||||||
const count = view.getUint32(cursor, true);
|
|
||||||
const keys = Object.keys(ecs.Types);
|
|
||||||
cursor += 4;
|
|
||||||
const creating = new Map();
|
|
||||||
const updating = new Map();
|
|
||||||
const cursors = new Map();
|
|
||||||
for (let i = 0; i < count; ++i) {
|
|
||||||
const entityId = view.getUint32(cursor, true);
|
|
||||||
if (!ecs.$$entities[entityId]) {
|
|
||||||
creating.set(entityId, {});
|
|
||||||
}
|
|
||||||
cursor += 4;
|
|
||||||
const componentCount = view.getUint16(cursor, true);
|
|
||||||
cursor += 2;
|
|
||||||
cursors.set(entityId, {});
|
|
||||||
const addedComponents = [];
|
|
||||||
for (let j = 0; j < componentCount; ++j) {
|
|
||||||
const componentId = view.getUint16(cursor, true);
|
|
||||||
cursor += 2;
|
|
||||||
const component = keys[componentId];
|
|
||||||
if (!component) {
|
|
||||||
throw new Error(`can't decode component ${componentId}`);
|
|
||||||
}
|
|
||||||
if (!ecs.$$entities[entityId]) {
|
|
||||||
creating.get(entityId)[component] = false;
|
|
||||||
}
|
|
||||||
else if (!ecs.$$entities[entityId].constructor.types.includes(component)) {
|
|
||||||
addedComponents.push(component);
|
|
||||||
if (!updating.has(component)) {
|
|
||||||
updating.set(component, []);
|
|
||||||
}
|
|
||||||
updating.get(component).push([entityId, false]);
|
|
||||||
}
|
|
||||||
cursors.get(entityId)[component] = cursor;
|
|
||||||
cursor += ecs.Types[component].constructor.schema.readSize(view, cursor);
|
|
||||||
}
|
|
||||||
if (addedComponents.length > 0 && ecs.$$entities[entityId]) {
|
|
||||||
ecs.rebuild(entityId, (types) => types.concat(addedComponents));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ecs.createManySpecific(Array.from(creating.entries()));
|
|
||||||
for (const [component, entityIds] of updating) {
|
|
||||||
ecs.Types[component].createMany(entityIds);
|
|
||||||
}
|
|
||||||
for (const [entityId, components] of cursors) {
|
|
||||||
for (const component in components) {
|
|
||||||
ecs.Types[component].deserialize(entityId, view, components[component]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ecs;
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy(entityId) {
|
|
||||||
this.destroyMany([entityId]);
|
|
||||||
}
|
|
||||||
|
|
||||||
destroyAll() {
|
|
||||||
this.destroyMany(this.entities);
|
|
||||||
}
|
|
||||||
|
|
||||||
destroyMany(entityIds) {
|
|
||||||
const destroying = {};
|
|
||||||
this.deindex(entityIds);
|
|
||||||
for (const entityId of entityIds) {
|
|
||||||
if (!this.$$entities[entityId]) {
|
|
||||||
throw new Error(`can't destroy non-existent entity ${entityId}`);
|
|
||||||
}
|
|
||||||
for (const component of this.$$entities[entityId].constructor.types) {
|
|
||||||
if (!destroying[component]) {
|
|
||||||
destroying[component] = [];
|
|
||||||
}
|
|
||||||
destroying[component].push(entityId);
|
|
||||||
}
|
|
||||||
this.$$entities[entityId] = undefined;
|
|
||||||
this.diff[entityId] = false;
|
|
||||||
}
|
|
||||||
for (const i in destroying) {
|
|
||||||
this.Types[i].destroyMany(destroying[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get entities() {
|
|
||||||
const it = Object.values(this.$$entities).values();
|
|
||||||
return {
|
|
||||||
[Symbol.iterator]() {
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
next: () => {
|
|
||||||
let result = it.next();
|
|
||||||
while (!result.done && !result.value) {
|
|
||||||
result = it.next();
|
|
||||||
}
|
|
||||||
if (result.done) {
|
|
||||||
return {done: true, value: undefined};
|
|
||||||
}
|
|
||||||
return {done: false, value: result.value.id};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
get(entityId) {
|
|
||||||
return this.$$entities[entityId];
|
|
||||||
}
|
|
||||||
|
|
||||||
insert(entityId, components) {
|
|
||||||
this.insertMany([[entityId, components]]);
|
|
||||||
}
|
|
||||||
|
|
||||||
insertMany(entities) {
|
|
||||||
const inserting = {};
|
|
||||||
const unique = new Set();
|
|
||||||
for (const [entityId, components] of entities) {
|
|
||||||
this.rebuild(entityId, (types) => [...new Set(types.concat(Object.keys(components)))]);
|
|
||||||
const diff = {};
|
|
||||||
for (const component in components) {
|
|
||||||
if (!inserting[component]) {
|
|
||||||
inserting[component] = [];
|
|
||||||
}
|
|
||||||
diff[component] = {};
|
|
||||||
inserting[component].push([entityId, components[component]]);
|
|
||||||
}
|
|
||||||
unique.add(entityId);
|
|
||||||
this.markChange(entityId, diff);
|
|
||||||
}
|
|
||||||
for (const component in inserting) {
|
|
||||||
this.Types[component].insertMany(inserting[component]);
|
|
||||||
}
|
|
||||||
this.reindex(unique.values());
|
|
||||||
}
|
|
||||||
|
|
||||||
markChange(entityId, components) {
|
|
||||||
// Deleted?
|
|
||||||
if (false === components) {
|
|
||||||
this.diff[entityId] = false;
|
|
||||||
}
|
|
||||||
// Created?
|
|
||||||
else if (!this.diff[entityId]) {
|
|
||||||
const filtered = {};
|
|
||||||
for (const type in components) {
|
|
||||||
filtered[type] = false === components[type]
|
|
||||||
? false
|
|
||||||
: this.Types[type].constructor.filterDefaults(components[type]);
|
|
||||||
}
|
|
||||||
this.diff[entityId] = filtered;
|
|
||||||
}
|
|
||||||
// Otherwise, merge.
|
|
||||||
else {
|
|
||||||
for (const type in components) {
|
|
||||||
this.diff[entityId][type] = false === components[type]
|
|
||||||
? false
|
|
||||||
: this.Types[type].mergeDiff(
|
|
||||||
this.diff[entityId][type] || {},
|
|
||||||
components[type],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rebuild(entityId, types) {
|
|
||||||
let existing = [];
|
|
||||||
if (this.$$entities[entityId]) {
|
|
||||||
existing.push(...this.$$entities[entityId].constructor.types);
|
|
||||||
}
|
|
||||||
const Class = this.$$entityFactory.makeClass(types(existing), this.Types);
|
|
||||||
this.$$entities[entityId] = new Class(entityId);
|
|
||||||
}
|
|
||||||
|
|
||||||
reindex(entityIds) {
|
|
||||||
for (let i = 0; i < this.$$systems.length; i++) {
|
|
||||||
this.$$systems[i].reindex(entityIds);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
remove(entityId, components) {
|
|
||||||
this.removeMany([[entityId, components]]);
|
|
||||||
}
|
|
||||||
|
|
||||||
removeMany(entities) {
|
|
||||||
const removing = {};
|
|
||||||
const unique = new Set();
|
|
||||||
for (const [entityId, components] of entities) {
|
|
||||||
unique.add(entityId);
|
|
||||||
const diff = {};
|
|
||||||
for (const component of components) {
|
|
||||||
diff[component] = false;
|
|
||||||
if (!removing[component]) {
|
|
||||||
removing[component] = [];
|
|
||||||
}
|
|
||||||
removing[component].push(entityId);
|
|
||||||
}
|
|
||||||
this.markChange(entityId, diff);
|
|
||||||
this.rebuild(entityId, (types) => types.filter((type) => !components.includes(type)));
|
|
||||||
}
|
|
||||||
for (const component in removing) {
|
|
||||||
this.Types[component].destroyMany(removing[component]);
|
|
||||||
}
|
|
||||||
this.reindex(unique.values());
|
|
||||||
}
|
|
||||||
|
|
||||||
removeSystem(SystemLike) {
|
|
||||||
const index = this.$$systems.findIndex((system) => SystemLike === system.source);
|
|
||||||
if (-1 !== index) {
|
|
||||||
this.$$systems.splice(index, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static serialize(ecs, view) {
|
|
||||||
if (!view) {
|
|
||||||
view = new DataView(new ArrayBuffer(ecs.size()));
|
|
||||||
}
|
|
||||||
let cursor = 0;
|
|
||||||
let entitiesWritten = 0;
|
|
||||||
cursor += 4;
|
|
||||||
const keys = Object.keys(ecs.Types);
|
|
||||||
for (const entityId of ecs.entities) {
|
|
||||||
const entity = ecs.get(entityId);
|
|
||||||
entitiesWritten += 1;
|
|
||||||
view.setUint32(cursor, entityId, true);
|
|
||||||
cursor += 4;
|
|
||||||
const entityComponents = entity.constructor.types;
|
|
||||||
view.setUint16(cursor, entityComponents.length, true);
|
|
||||||
const componentsWrittenIndex = cursor;
|
|
||||||
cursor += 2;
|
|
||||||
for (const component of entityComponents) {
|
|
||||||
const instance = ecs.Types[component];
|
|
||||||
view.setUint16(cursor, keys.indexOf(component), true);
|
|
||||||
cursor += 2;
|
|
||||||
instance.serialize(entityId, view, cursor);
|
|
||||||
cursor += instance.sizeOf(entityId);
|
|
||||||
}
|
|
||||||
view.setUint16(componentsWrittenIndex, entityComponents.length, true);
|
|
||||||
}
|
|
||||||
view.setUint32(0, entitiesWritten, true);
|
|
||||||
return view;
|
|
||||||
}
|
|
||||||
|
|
||||||
setClean() {
|
|
||||||
this.diff = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
size() {
|
|
||||||
// # of entities.
|
|
||||||
let size = 4;
|
|
||||||
for (const entityId of this.entities) {
|
|
||||||
size += this.get(entityId).size();
|
|
||||||
}
|
|
||||||
return size;
|
|
||||||
}
|
|
||||||
|
|
||||||
system(SystemLike) {
|
|
||||||
return this.$$systems.find((system) => SystemLike === system.source)
|
|
||||||
}
|
|
||||||
|
|
||||||
tick(elapsed) {
|
|
||||||
for (let i = 0; i < this.$$systems.length; i++) {
|
|
||||||
this.$$systems[i].tick(elapsed);
|
|
||||||
}
|
|
||||||
for (let i = 0; i < this.$$systems.length; i++) {
|
|
||||||
this.$$systems[i].finalize(elapsed);
|
|
||||||
}
|
|
||||||
this.tickDestruction();
|
|
||||||
}
|
|
||||||
|
|
||||||
tickDestruction() {
|
|
||||||
const unique = new Set();
|
|
||||||
for (let i = 0; i < this.$$systems.length; i++) {
|
|
||||||
for (let j = 0; j < this.$$systems[i].destroying.length; j++) {
|
|
||||||
unique.add(this.$$systems[i].destroying[j]);
|
|
||||||
}
|
|
||||||
this.$$systems[i].tickDestruction();
|
|
||||||
}
|
|
||||||
if (unique.size > 0) {
|
|
||||||
this.destroyMany(unique.values());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,459 +0,0 @@
|
||||||
import {expect, test} from 'vitest';
|
|
||||||
|
|
||||||
import Ecs from './ecs.js';
|
|
||||||
import System from './system.js';
|
|
||||||
|
|
||||||
const Empty = {};
|
|
||||||
|
|
||||||
const Name = {
|
|
||||||
name: {type: 'string'},
|
|
||||||
};
|
|
||||||
|
|
||||||
const Position = {
|
|
||||||
x: {type: 'int32', defaultValue: 32},
|
|
||||||
y: {type: 'int32'},
|
|
||||||
z: {type: 'int32'},
|
|
||||||
};
|
|
||||||
|
|
||||||
test('adds and remove systems at runtime', () => {
|
|
||||||
const ecs = new Ecs();
|
|
||||||
let oneCount = 0;
|
|
||||||
let twoCount = 0;
|
|
||||||
const oneSystem = () => {
|
|
||||||
oneCount++;
|
|
||||||
};
|
|
||||||
ecs.addSystem(oneSystem);
|
|
||||||
ecs.tick();
|
|
||||||
expect(oneCount)
|
|
||||||
.to.equal(1);
|
|
||||||
const twoSystem = () => {
|
|
||||||
twoCount++;
|
|
||||||
};
|
|
||||||
ecs.addSystem(twoSystem);
|
|
||||||
ecs.tick();
|
|
||||||
expect(oneCount)
|
|
||||||
.to.equal(2);
|
|
||||||
expect(twoCount)
|
|
||||||
.to.equal(1);
|
|
||||||
ecs.removeSystem(oneSystem);
|
|
||||||
ecs.tick();
|
|
||||||
expect(oneCount)
|
|
||||||
.to.equal(2);
|
|
||||||
expect(twoCount)
|
|
||||||
.to.equal(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('creates entities with components', () => {
|
|
||||||
class CreateEcs extends Ecs {
|
|
||||||
static Types = {Empty, Position};
|
|
||||||
}
|
|
||||||
const ecs = new CreateEcs();
|
|
||||||
const entity = ecs.create({Empty: {}, Position: {y: 128}});
|
|
||||||
expect(JSON.stringify(ecs.get(entity)))
|
|
||||||
.to.deep.equal(JSON.stringify({Empty: {}, Position: {y: 128}}));
|
|
||||||
});
|
|
||||||
|
|
||||||
test("removes entities' components", () => {
|
|
||||||
class RemoveEcs extends Ecs {
|
|
||||||
static Types = {Empty, Position};
|
|
||||||
}
|
|
||||||
const ecs = new RemoveEcs();
|
|
||||||
const entity = ecs.create({Empty: {}, Position: {y: 128}});
|
|
||||||
ecs.remove(entity, ['Position']);
|
|
||||||
expect(JSON.stringify(ecs.get(entity)))
|
|
||||||
.to.deep.equal(JSON.stringify({Empty: {}}));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('gets entities', () => {
|
|
||||||
class GetEcs extends Ecs {
|
|
||||||
static Types = {Empty, Position};
|
|
||||||
}
|
|
||||||
const ecs = new GetEcs();
|
|
||||||
const entity = ecs.create({Empty: {}, Position: {y: 128}});
|
|
||||||
expect(JSON.stringify(ecs.get(entity)))
|
|
||||||
.to.deep.equal(JSON.stringify({Empty: {}, Position: {y: 128}}));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('destroys entities', () => {
|
|
||||||
class DestroyEcs extends Ecs {
|
|
||||||
static Types = {Empty, Position};
|
|
||||||
}
|
|
||||||
const ecs = new DestroyEcs();
|
|
||||||
const entity = ecs.create({Empty: {}, Position: {y: 128}});
|
|
||||||
expect(JSON.stringify(ecs.get(entity)))
|
|
||||||
.to.deep.equal(JSON.stringify({Empty: {}, Position: {y: 128}}));
|
|
||||||
expect(ecs.get(entity))
|
|
||||||
.to.not.be.undefined;
|
|
||||||
ecs.destroyAll();
|
|
||||||
expect(ecs.get(entity))
|
|
||||||
.to.be.undefined;
|
|
||||||
expect(() => {
|
|
||||||
ecs.destroy(entity);
|
|
||||||
})
|
|
||||||
.to.throw();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('inserts components into entities', () => {
|
|
||||||
class InsertEcs extends Ecs {
|
|
||||||
static Types = {Empty, Position};
|
|
||||||
}
|
|
||||||
const ecs = new InsertEcs();
|
|
||||||
const entity = ecs.create({Empty: {}});
|
|
||||||
ecs.insert(entity, {Position: {y: 128}});
|
|
||||||
expect(JSON.stringify(ecs.get(entity)))
|
|
||||||
.to.deep.equal(JSON.stringify({Empty: {}, Position: {y: 128}}));
|
|
||||||
ecs.insert(entity, {Position: {y: 64}});
|
|
||||||
expect(JSON.stringify(ecs.get(entity)))
|
|
||||||
.to.deep.equal(JSON.stringify({Empty: {}, Position: {y: 64}}));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('ticks systems', () => {
|
|
||||||
const Momentum = {
|
|
||||||
x: {type: 'int32'},
|
|
||||||
y: {type: 'int32'},
|
|
||||||
z: {type: 'int32'},
|
|
||||||
};
|
|
||||||
class TickEcs extends Ecs {
|
|
||||||
static Types = {Momentum, Position};
|
|
||||||
}
|
|
||||||
const ecs = new TickEcs();
|
|
||||||
class Physics extends System {
|
|
||||||
|
|
||||||
static queries() {
|
|
||||||
return {
|
|
||||||
default: ['Position', 'Momentum'],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
tick(elapsed) {
|
|
||||||
for (const [position, momentum] of this.select('default')) {
|
|
||||||
position.x += momentum.x * elapsed;
|
|
||||||
position.y += momentum.y * elapsed;
|
|
||||||
position.z += momentum.z * elapsed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
ecs.addSystem(Physics);
|
|
||||||
const entity = ecs.create({Momentum: {}, Position: {y: 128}});
|
|
||||||
const position = JSON.stringify(ecs.get(entity).Position);
|
|
||||||
ecs.tick(1);
|
|
||||||
expect(JSON.stringify(ecs.get(entity).Position))
|
|
||||||
.to.deep.equal(position);
|
|
||||||
ecs.get(1).Momentum.y = 30;
|
|
||||||
ecs.tick(1);
|
|
||||||
expect(JSON.stringify(ecs.get(entity).Position))
|
|
||||||
.to.deep.equal(JSON.stringify({y: 128 + 30}));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('creates many entities when ticking systems', () => {
|
|
||||||
const ecs = new Ecs();
|
|
||||||
class Spawn extends System {
|
|
||||||
tick() {
|
|
||||||
this.createManyEntities(Array.from({length: 5}).map(() => []));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ecs.addSystem(Spawn);
|
|
||||||
ecs.create();
|
|
||||||
expect(ecs.get(5))
|
|
||||||
.to.be.undefined;
|
|
||||||
ecs.tick(1);
|
|
||||||
expect(ecs.get(5))
|
|
||||||
.to.not.be.undefined;
|
|
||||||
});
|
|
||||||
|
|
||||||
test('creates entities when ticking systems', () => {
|
|
||||||
const ecs = new Ecs();
|
|
||||||
class Spawn extends System {
|
|
||||||
tick() {
|
|
||||||
this.createEntity();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ecs.addSystem(Spawn);
|
|
||||||
ecs.create();
|
|
||||||
expect(ecs.get(2))
|
|
||||||
.to.be.undefined;
|
|
||||||
ecs.tick(1);
|
|
||||||
expect(ecs.get(2))
|
|
||||||
.to.not.be.undefined;
|
|
||||||
});
|
|
||||||
|
|
||||||
test('schedules entities to be deleted when ticking systems', () => {
|
|
||||||
const ecs = new Ecs();
|
|
||||||
let entity;
|
|
||||||
class Despawn extends System {
|
|
||||||
|
|
||||||
finalize() {
|
|
||||||
entity = ecs.get(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
tick() {
|
|
||||||
this.destroyEntity(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
ecs.addSystem(Despawn);
|
|
||||||
ecs.create();
|
|
||||||
ecs.tick(1);
|
|
||||||
expect(entity)
|
|
||||||
.to.not.be.undefined;
|
|
||||||
expect(ecs.get(1))
|
|
||||||
.to.be.undefined;
|
|
||||||
});
|
|
||||||
|
|
||||||
test('adds components to and remove components from entities when ticking systems', () => {
|
|
||||||
class TickingEcs extends Ecs {
|
|
||||||
static Types = {Foo: {bar: {type: 'uint8'}}};
|
|
||||||
}
|
|
||||||
const ecs = new TickingEcs();
|
|
||||||
let addLength, removeLength;
|
|
||||||
class AddComponent extends System {
|
|
||||||
static queries() {
|
|
||||||
return {
|
|
||||||
default: ['Foo'],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
tick() {
|
|
||||||
this.insertComponents(1, {Foo: {}});
|
|
||||||
}
|
|
||||||
finalize() {
|
|
||||||
addLength = Array.from(this.select('default')).length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
class RemoveComponent extends System {
|
|
||||||
static queries() {
|
|
||||||
return {
|
|
||||||
default: ['Foo'],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
tick() {
|
|
||||||
this.removeComponents(1, ['Foo']);
|
|
||||||
}
|
|
||||||
finalize() {
|
|
||||||
removeLength = Array.from(this.select('default')).length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ecs.addSystem(AddComponent);
|
|
||||||
ecs.create();
|
|
||||||
ecs.tick(1);
|
|
||||||
expect(addLength)
|
|
||||||
.to.equal(1);
|
|
||||||
expect(ecs.get(1).Foo)
|
|
||||||
.to.not.be.undefined;
|
|
||||||
ecs.removeSystem(AddComponent);
|
|
||||||
ecs.addSystem(RemoveComponent);
|
|
||||||
ecs.tick(1);
|
|
||||||
expect(removeLength)
|
|
||||||
.to.equal(0);
|
|
||||||
expect(ecs.get(1).Foo)
|
|
||||||
.to.be.undefined;
|
|
||||||
});
|
|
||||||
|
|
||||||
test('generates coalesced diffs for entity creation', () => {
|
|
||||||
const ecs = new Ecs();
|
|
||||||
let entity;
|
|
||||||
entity = ecs.create();
|
|
||||||
expect(ecs.diff)
|
|
||||||
.to.deep.equal({[entity]: {}});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('generates diffs for adding and removing components', () => {
|
|
||||||
class DiffedEcs extends Ecs {
|
|
||||||
static Types = {Position};
|
|
||||||
}
|
|
||||||
const ecs = new DiffedEcs();
|
|
||||||
let entity;
|
|
||||||
entity = ecs.create();
|
|
||||||
ecs.setClean();
|
|
||||||
ecs.insert(entity, {Position: {x: 64}});
|
|
||||||
expect(ecs.diff)
|
|
||||||
.to.deep.equal({[entity]: {Position: {x: 64}}});
|
|
||||||
ecs.setClean();
|
|
||||||
expect(ecs.diff)
|
|
||||||
.to.deep.equal({});
|
|
||||||
ecs.remove(entity, ['Position']);
|
|
||||||
expect(ecs.diff)
|
|
||||||
.to.deep.equal({[entity]: {Position: false}});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('generates diffs for empty components', () => {
|
|
||||||
class DiffedEcs extends Ecs {
|
|
||||||
static Types = {Empty};
|
|
||||||
}
|
|
||||||
const ecs = new DiffedEcs();
|
|
||||||
let entity;
|
|
||||||
entity = ecs.create({Empty});
|
|
||||||
expect(ecs.diff)
|
|
||||||
.to.deep.equal({[entity]: {Empty: {}}});
|
|
||||||
ecs.setClean();
|
|
||||||
ecs.remove(entity, ['Empty']);
|
|
||||||
expect(ecs.diff)
|
|
||||||
.to.deep.equal({[entity]: {Empty: false}});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('generates diffs for entity mutations', () => {
|
|
||||||
class DiffedEcs extends Ecs {
|
|
||||||
static Types = {Position};
|
|
||||||
}
|
|
||||||
const ecs = new DiffedEcs();
|
|
||||||
let entity;
|
|
||||||
entity = ecs.create({Position: {}});
|
|
||||||
ecs.setClean();
|
|
||||||
ecs.get(entity).Position.x = 128;
|
|
||||||
expect(ecs.diff)
|
|
||||||
.to.deep.equal({[entity]: {Position: {x: 128}}});
|
|
||||||
ecs.setClean();
|
|
||||||
expect(ecs.diff)
|
|
||||||
.to.deep.equal({});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('generates coalesced diffs for components', () => {
|
|
||||||
class DiffedEcs extends Ecs {
|
|
||||||
static Types = {Position};
|
|
||||||
}
|
|
||||||
const ecs = new DiffedEcs();
|
|
||||||
let entity;
|
|
||||||
entity = ecs.create({Position});
|
|
||||||
ecs.remove(entity, ['Position']);
|
|
||||||
expect(ecs.diff)
|
|
||||||
.to.deep.equal({[entity]: {Position: false}});
|
|
||||||
ecs.insert(entity, {Position: {}});
|
|
||||||
expect(ecs.diff)
|
|
||||||
.to.deep.equal({[entity]: {Position: {}}});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('generates coalesced diffs for mutations', () => {
|
|
||||||
class DiffedEcs extends Ecs {
|
|
||||||
static Types = {Position};
|
|
||||||
}
|
|
||||||
const ecs = new DiffedEcs();
|
|
||||||
let entity;
|
|
||||||
entity = ecs.create({Position});
|
|
||||||
ecs.setClean();
|
|
||||||
ecs.get(entity).Position.x = 128;
|
|
||||||
ecs.get(entity).Position.x = 256;
|
|
||||||
ecs.get(entity).Position.x = 512;
|
|
||||||
expect(ecs.diff)
|
|
||||||
.to.deep.equal({[entity]: {Position: {x: 512}}});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('generates diffs for deletions', () => {
|
|
||||||
const ecs = new Ecs();
|
|
||||||
let entity;
|
|
||||||
entity = ecs.create();
|
|
||||||
ecs.setClean();
|
|
||||||
ecs.destroy(entity);
|
|
||||||
expect(ecs.diff)
|
|
||||||
.to.deep.equal({[entity]: false});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('applies creation patches', () => {
|
|
||||||
class PatchedEcs extends Ecs {
|
|
||||||
static Types = {Position};
|
|
||||||
}
|
|
||||||
const ecs = new PatchedEcs();
|
|
||||||
ecs.apply({16: {Position: {x: 64}}});
|
|
||||||
expect(Array.from(ecs.entities).length)
|
|
||||||
.to.equal(1);
|
|
||||||
expect(ecs.get(16).Position.x)
|
|
||||||
.to.equal(64);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('applies update patches', () => {
|
|
||||||
class PatchedEcs extends Ecs {
|
|
||||||
static Types = {Position};
|
|
||||||
}
|
|
||||||
const ecs = new PatchedEcs();
|
|
||||||
ecs.createSpecific(16, {Position: {x: 64}});
|
|
||||||
ecs.apply({16: {Position: {x: 128}}});
|
|
||||||
expect(Array.from(ecs.entities).length)
|
|
||||||
.to.equal(1);
|
|
||||||
expect(ecs.get(16).Position.x)
|
|
||||||
.to.equal(128);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('applies entity deletion patches', () => {
|
|
||||||
class PatchedEcs extends Ecs {
|
|
||||||
static Types = {Position};
|
|
||||||
}
|
|
||||||
const ecs = new PatchedEcs();
|
|
||||||
ecs.createSpecific(16, {Position: {x: 64}});
|
|
||||||
ecs.apply({16: false});
|
|
||||||
expect(Array.from(ecs.entities).length)
|
|
||||||
.to.equal(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('applies component deletion patches', () => {
|
|
||||||
class PatchedEcs extends Ecs {
|
|
||||||
static Types = {Empty, Position};
|
|
||||||
}
|
|
||||||
const ecs = new PatchedEcs();
|
|
||||||
ecs.createSpecific(16, {Empty: {}, Position: {x: 64}});
|
|
||||||
expect(ecs.get(16).constructor.types)
|
|
||||||
.to.deep.equal(['Empty', 'Position']);
|
|
||||||
ecs.apply({16: {Empty: false}});
|
|
||||||
expect(ecs.get(16).constructor.types)
|
|
||||||
.to.deep.equal(['Position']);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('calculates entity size', () => {
|
|
||||||
class SizingEcs extends Ecs {
|
|
||||||
static Types = {Empty, Position};
|
|
||||||
}
|
|
||||||
const ecs = new SizingEcs();
|
|
||||||
ecs.createSpecific(1, {Empty: {}, Position: {}});
|
|
||||||
// ID + # of components + Empty + Position + x + y + z
|
|
||||||
// 4 + 2 + 2 + 2 + 4 + 4 + 4 = 22
|
|
||||||
expect(ecs.get(1).size())
|
|
||||||
.to.equal(22);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('serializes and deserializes', () => {
|
|
||||||
class SerializingEcs extends Ecs {
|
|
||||||
static Types = {Empty, Name, Position};
|
|
||||||
}
|
|
||||||
const ecs = new SerializingEcs();
|
|
||||||
// ID + # of components + Empty + Position + x + y + z
|
|
||||||
// 4 + 2 + 2 + 2 + 4 + 4 + 4 = 22
|
|
||||||
ecs.createSpecific(1, {Empty: {}, Position: {x: 64}});
|
|
||||||
// ID + # of components + Name + 'foobar' + Position + x + y + z
|
|
||||||
// 4 + 2 + 2 + 4 + 6 + 2 + 4 + 4 + 4 = 32
|
|
||||||
ecs.createSpecific(16, {Name: {name: 'foobar'}, Position: {x: 128}});
|
|
||||||
const view = SerializingEcs.serialize(ecs);
|
|
||||||
// # of entities + Entity(1) + Entity(16)
|
|
||||||
// 4 + 22 + 32 = 58
|
|
||||||
expect(view.byteLength)
|
|
||||||
.to.equal(58);
|
|
||||||
// Entity values.
|
|
||||||
expect(view.getUint32(4 + 22 - 12, true))
|
|
||||||
.to.equal(64);
|
|
||||||
expect(view.getUint32(4 + 22 + 32 - 12, true))
|
|
||||||
.to.equal(128);
|
|
||||||
const deserialized = SerializingEcs.deserialize(view);
|
|
||||||
// # of entities.
|
|
||||||
expect(Array.from(deserialized.entities).length)
|
|
||||||
.to.equal(2);
|
|
||||||
// Composition of entities.
|
|
||||||
expect(deserialized.get(1).constructor.types)
|
|
||||||
.to.deep.equal(['Empty', 'Position']);
|
|
||||||
expect(deserialized.get(16).constructor.types)
|
|
||||||
.to.deep.equal(['Name', 'Position']);
|
|
||||||
// Entity values.
|
|
||||||
expect(JSON.stringify(deserialized.get(1)))
|
|
||||||
.to.equal(JSON.stringify({Empty: {}, Position: {x: 64}}))
|
|
||||||
expect(JSON.stringify(deserialized.get(1).Position))
|
|
||||||
.to.equal(JSON.stringify({x: 64}));
|
|
||||||
expect(JSON.stringify(deserialized.get(16).Position))
|
|
||||||
.to.equal(JSON.stringify({x: 128}));
|
|
||||||
expect(deserialized.get(16).Name.name)
|
|
||||||
.to.equal('foobar');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('deserializes empty', () => {
|
|
||||||
class SerializingEcs extends Ecs {
|
|
||||||
static Types = {Empty, Name, Position};
|
|
||||||
}
|
|
||||||
const ecs = SerializingEcs.deserialize(new DataView(new Uint32Array([0]).buffer));
|
|
||||||
expect(ecs)
|
|
||||||
.to.not.be.undefined;
|
|
||||||
});
|
|
|
@ -1,84 +0,0 @@
|
||||||
export default class Query {
|
|
||||||
|
|
||||||
$$criteria = {with: [], without: []};
|
|
||||||
|
|
||||||
$$index = new Set();
|
|
||||||
|
|
||||||
constructor(parameters, Types) {
|
|
||||||
for (let i = 0; i < parameters.length; ++i) {
|
|
||||||
const parameter = parameters[i];
|
|
||||||
switch (parameter.charCodeAt(0)) {
|
|
||||||
case '!'.charCodeAt(0):
|
|
||||||
this.$$criteria.without.push(Types[parameter.slice(1)]);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
this.$$criteria.with.push(Types[parameter]);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get count() {
|
|
||||||
return this.$$index.size;
|
|
||||||
}
|
|
||||||
|
|
||||||
deindex(entityIds) {
|
|
||||||
for (let i = 0; i < entityIds.length; ++i) {
|
|
||||||
this.$$index.delete(entityIds[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
reindex(entityIds) {
|
|
||||||
if (0 === this.$$criteria.with.length && 0 === this.$$criteria.without.length) {
|
|
||||||
for (const entityId of entityIds) {
|
|
||||||
this.$$index.add(entityId);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const entityId of entityIds) {
|
|
||||||
let should = true;
|
|
||||||
for (let j = 0; j < this.$$criteria.with.length; ++j) {
|
|
||||||
if ('undefined' === typeof this.$$criteria.with[j].get(entityId)) {
|
|
||||||
should = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (should) {
|
|
||||||
for (let j = 0; j < this.$$criteria.without.length; ++j) {
|
|
||||||
if ('undefined' !== typeof this.$$criteria.without[j].get(entityId)) {
|
|
||||||
should = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (should) {
|
|
||||||
this.$$index.add(entityId);
|
|
||||||
}
|
|
||||||
else if (!should) {
|
|
||||||
this.$$index.delete(entityId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
select() {
|
|
||||||
const it = this.$$index.values();
|
|
||||||
const value = [];
|
|
||||||
return {
|
|
||||||
[Symbol.iterator]() {
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
next: () => {
|
|
||||||
const result = it.next();
|
|
||||||
if (result.done) {
|
|
||||||
return {done: true, value: undefined};
|
|
||||||
}
|
|
||||||
for (let i = 0; i < this.$$criteria.with.length; ++i) {
|
|
||||||
value[i] = this.$$criteria.with[i].get(result.value);
|
|
||||||
}
|
|
||||||
value[this.$$criteria.with.length] = result.value;
|
|
||||||
return {done: false, value};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,72 +0,0 @@
|
||||||
import {expect, test} from 'vitest';
|
|
||||||
|
|
||||||
import Component from './component.js';
|
|
||||||
import Query from './query.js';
|
|
||||||
|
|
||||||
const A = new (Component({a: {type: 'int32', defaultValue: 420}}));
|
|
||||||
const B = new (Component({b: {type: 'int32', defaultValue: 69}}));
|
|
||||||
const C = new (Component({c: {type: 'int32'}}));
|
|
||||||
|
|
||||||
const Types = {A, B, C};
|
|
||||||
Types.A.createMany([[2], [3]]);
|
|
||||||
Types.B.createMany([[1], [2]]);
|
|
||||||
Types.C.createMany([[2], [4]]);
|
|
||||||
|
|
||||||
function testQuery(parameters, expected) {
|
|
||||||
const query = new Query(parameters, Types);
|
|
||||||
query.reindex([1, 2, 3]);
|
|
||||||
expect(query.count)
|
|
||||||
.to.equal(expected.length);
|
|
||||||
for (const _ of query.select()) {
|
|
||||||
expect(_.length)
|
|
||||||
.to.equal(parameters.filter((spec) => '!'.charCodeAt(0) !== spec.charCodeAt(0)).length + 1);
|
|
||||||
expect(expected.includes(_.pop()))
|
|
||||||
.to.equal(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
test('can query all', () => {
|
|
||||||
testQuery([], [1, 2, 3]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('can query some', () => {
|
|
||||||
testQuery(['A'], [2, 3]);
|
|
||||||
testQuery(['A', 'B'], [2]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('can query excluding', () => {
|
|
||||||
testQuery(['!A'], [1]);
|
|
||||||
testQuery(['A', '!B'], [3]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('can deindex', () => {
|
|
||||||
const query = new Query(['A'], Types);
|
|
||||||
query.reindex([1, 2, 3]);
|
|
||||||
expect(query.count)
|
|
||||||
.to.equal(2);
|
|
||||||
query.deindex([2]);
|
|
||||||
expect(query.count)
|
|
||||||
.to.equal(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('can reindex', () => {
|
|
||||||
const Test = new (Component({a: {type: 'int32', defaultValue: 420}}));
|
|
||||||
Test.createMany([[2], [3]]);
|
|
||||||
const query = new Query(['Test'], {Test});
|
|
||||||
query.reindex([2, 3]);
|
|
||||||
expect(query.count)
|
|
||||||
.to.equal(2);
|
|
||||||
Test.destroy(2);
|
|
||||||
query.reindex([2, 3]);
|
|
||||||
expect(query.count)
|
|
||||||
.to.equal(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('can select', () => {
|
|
||||||
const query = new Query(['A'], Types);
|
|
||||||
query.reindex([1, 2, 3]);
|
|
||||||
const it = query.select();
|
|
||||||
const result = it.next();
|
|
||||||
expect(result.value[0].a)
|
|
||||||
.to.equal(420);
|
|
||||||
});
|
|
|
@ -1,300 +0,0 @@
|
||||||
const encoder = new TextEncoder();
|
|
||||||
|
|
||||||
export default class Schema {
|
|
||||||
|
|
||||||
$$size = 0;
|
|
||||||
|
|
||||||
specification;
|
|
||||||
|
|
||||||
static viewGetMethods = {
|
|
||||||
uint8: 'getUint8',
|
|
||||||
int8: 'getInt8',
|
|
||||||
uint16: 'getUint16',
|
|
||||||
int16: 'getInt16',
|
|
||||||
uint32: 'getUint32',
|
|
||||||
int32: 'getInt32',
|
|
||||||
float32: 'getFloat32',
|
|
||||||
float64: 'getFloat64',
|
|
||||||
int64: 'getBigInt64',
|
|
||||||
uint64: 'getBigUint64',
|
|
||||||
};
|
|
||||||
|
|
||||||
static viewSetMethods = {
|
|
||||||
uint8: 'setUint8',
|
|
||||||
int8: 'setInt8',
|
|
||||||
uint16: 'setUint16',
|
|
||||||
int16: 'setInt16',
|
|
||||||
uint32: 'setUint32',
|
|
||||||
int32: 'setInt32',
|
|
||||||
float32: 'setFloat32',
|
|
||||||
float64: 'setFloat64',
|
|
||||||
int64: 'setBigInt64',
|
|
||||||
uint64: 'setBigUint64',
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(specification) {
|
|
||||||
this.specification = this.constructor.normalize(specification);
|
|
||||||
}
|
|
||||||
|
|
||||||
static deserialize(view, offset, specification) {
|
|
||||||
const viewGetMethod = this.viewGetMethods[specification.type];
|
|
||||||
if (viewGetMethod) {
|
|
||||||
const value = view[viewGetMethod](offset.value, true);
|
|
||||||
offset.value += this.size(specification);
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
switch (specification.type) {
|
|
||||||
case 'array': {
|
|
||||||
const length = view.getUint32(offset.value, true);
|
|
||||||
offset.value += 4;
|
|
||||||
const value = [];
|
|
||||||
for (let i = 0; i < length; ++i) {
|
|
||||||
value.push(this.deserialize(view, offset, specification.subtype));
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
case 'object': {
|
|
||||||
const value = {};
|
|
||||||
for (const key in specification.properties) {
|
|
||||||
value[key] = this.deserialize(view, offset, specification.properties[key]);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
case 'string': {
|
|
||||||
const length = view.getUint32(offset.value, true);
|
|
||||||
offset.value += 4;
|
|
||||||
const {buffer, byteOffset} = view;
|
|
||||||
const decoder = new TextDecoder();
|
|
||||||
const value = decoder.decode(new DataView(buffer, byteOffset + offset.value, length));
|
|
||||||
offset.value += length;
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deserialize(view, offset = 0) {
|
|
||||||
const wrapped = {value: offset};
|
|
||||||
return this.constructor.deserialize(view, wrapped, this.specification);
|
|
||||||
}
|
|
||||||
|
|
||||||
static defaultValue(specification) {
|
|
||||||
if (specification.defaultValue) {
|
|
||||||
return specification.defaultValue;
|
|
||||||
}
|
|
||||||
switch (specification.type) {
|
|
||||||
case 'uint8': case 'int8':
|
|
||||||
case 'uint16': case 'int16':
|
|
||||||
case 'uint32': case 'int32':
|
|
||||||
case 'uint64': case 'int64':
|
|
||||||
case 'float32': case 'float64': {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
case 'array': {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
case 'object': {
|
|
||||||
const object = {};
|
|
||||||
for (const key in specification.properties) {
|
|
||||||
object[key] = this.defaultValue(specification.properties[key]);
|
|
||||||
}
|
|
||||||
return object;
|
|
||||||
}
|
|
||||||
case 'string': {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultValue() {
|
|
||||||
return this.constructor.defaultValue(this.specification);
|
|
||||||
}
|
|
||||||
|
|
||||||
has(key) {
|
|
||||||
return key in this.specification;
|
|
||||||
}
|
|
||||||
|
|
||||||
static normalize(specification) {
|
|
||||||
let normalized = specification;
|
|
||||||
switch (specification.type) {
|
|
||||||
case 'array': {
|
|
||||||
normalized.subtype = this.normalize(specification.subtype);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'object': {
|
|
||||||
for (const key in specification.properties) {
|
|
||||||
normalized.properties[key] = this.normalize(specification.properties[key])
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'uint8':
|
|
||||||
case 'int8':
|
|
||||||
case 'uint16':
|
|
||||||
case 'int16':
|
|
||||||
case 'uint32':
|
|
||||||
case 'int32':
|
|
||||||
case 'uint64':
|
|
||||||
case 'int64':
|
|
||||||
case 'float32':
|
|
||||||
case 'float64':
|
|
||||||
case 'string': {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
throw new TypeError(`invalid specification: ${JSON.stringify(specification)}`);
|
|
||||||
}
|
|
||||||
return {...normalized, defaultValue: this.defaultValue(normalized)};
|
|
||||||
}
|
|
||||||
|
|
||||||
static readSize(view, offset, specification) {
|
|
||||||
const size = this.size(specification);
|
|
||||||
if (size > 0) {
|
|
||||||
return size;
|
|
||||||
}
|
|
||||||
switch (specification.type) {
|
|
||||||
case 'array': {
|
|
||||||
const length = view.getUint32(offset.value, true);
|
|
||||||
offset.value += 4;
|
|
||||||
let arraySize = 0;
|
|
||||||
for (let i = 0; i < length; ++i) {
|
|
||||||
arraySize += this.readSize(view, offset, specification.subtype);
|
|
||||||
}
|
|
||||||
return 4 + arraySize;
|
|
||||||
}
|
|
||||||
case 'object': {
|
|
||||||
let objectSize = 0;
|
|
||||||
for (const key in specification.properties) {
|
|
||||||
objectSize += this.readSize(view, offset, specification.properties[key]);
|
|
||||||
}
|
|
||||||
return objectSize;
|
|
||||||
}
|
|
||||||
case 'string': {
|
|
||||||
const length = view.getUint32(offset.value, true);
|
|
||||||
offset.value += 4 + length;
|
|
||||||
return 4 + length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
readSize(view, offset) {
|
|
||||||
const wrapped = {value: offset};
|
|
||||||
return this.constructor.readSize(view, wrapped, this.specification);
|
|
||||||
}
|
|
||||||
|
|
||||||
static serialize(source, view, offset, specification) {
|
|
||||||
const viewSetMethod = this.viewSetMethods[specification.type];
|
|
||||||
if (viewSetMethod) {
|
|
||||||
view[viewSetMethod](offset, source, true);
|
|
||||||
return this.size(specification);
|
|
||||||
}
|
|
||||||
switch (specification.type) {
|
|
||||||
case 'array': {
|
|
||||||
view.setUint32(offset, source.length, true);
|
|
||||||
offset += 4;
|
|
||||||
let arraySize = 0;
|
|
||||||
for (const element of source) {
|
|
||||||
arraySize += this.serialize(
|
|
||||||
element,
|
|
||||||
view,
|
|
||||||
offset + arraySize,
|
|
||||||
specification.subtype,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return 4 + arraySize;
|
|
||||||
}
|
|
||||||
case 'object': {
|
|
||||||
let objectSize = 0;
|
|
||||||
for (const key in specification.properties) {
|
|
||||||
objectSize += this.serialize(
|
|
||||||
source[key],
|
|
||||||
view,
|
|
||||||
offset + objectSize,
|
|
||||||
specification.properties[key],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return objectSize;
|
|
||||||
}
|
|
||||||
case 'string': {
|
|
||||||
const encoder = new TextEncoder();
|
|
||||||
const bytes = encoder.encode(source);
|
|
||||||
view.setUint32(offset, bytes.length, true);
|
|
||||||
offset += 4;
|
|
||||||
for (let i = 0; i < bytes.length; ++i) {
|
|
||||||
view.setUint8(offset++, bytes[i]);
|
|
||||||
}
|
|
||||||
return 4 + bytes.length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
serialize(source, view, offset = 0) {
|
|
||||||
this.constructor.serialize(source, view, offset, this.specification);
|
|
||||||
}
|
|
||||||
|
|
||||||
static sizeOf(concrete, specification) {
|
|
||||||
const size = this.size(specification);
|
|
||||||
if (size > 0) {
|
|
||||||
return size;
|
|
||||||
}
|
|
||||||
let fullSize = 0;
|
|
||||||
const {type} = specification;
|
|
||||||
switch (type) {
|
|
||||||
case 'array': {
|
|
||||||
fullSize += 4;
|
|
||||||
for (const element of concrete) {
|
|
||||||
fullSize += this.sizeOf(element, specification.subtype);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'object': {
|
|
||||||
for (const key in specification.properties) {
|
|
||||||
fullSize += this.sizeOf(concrete[key], specification.properties[key]);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'string':
|
|
||||||
fullSize += 4;
|
|
||||||
fullSize += (encoder.encode(concrete)).length;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return fullSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
sizeOf(concrete) {
|
|
||||||
return this.constructor.sizeOf(concrete, this.specification);
|
|
||||||
}
|
|
||||||
|
|
||||||
static size(specification) {
|
|
||||||
switch (specification.type) {
|
|
||||||
case 'array': return 0;
|
|
||||||
case 'object': {
|
|
||||||
let size = 0;
|
|
||||||
for (const key in specification.properties) {
|
|
||||||
const propertySize = this.size(specification.properties[key]);
|
|
||||||
if (0 === propertySize) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
size += propertySize;
|
|
||||||
}
|
|
||||||
return size;
|
|
||||||
}
|
|
||||||
case 'uint8': case 'int8': {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
case 'uint16': case 'int16': {
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
case 'uint32': case 'int32': case 'float32': {
|
|
||||||
return 4;
|
|
||||||
}
|
|
||||||
case 'uint64': case 'int64': case 'float64': {
|
|
||||||
return 8;
|
|
||||||
}
|
|
||||||
default: return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
size() {
|
|
||||||
return this.constructor.size(this.specification);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,136 +0,0 @@
|
||||||
import {expect, test} from 'vitest';
|
|
||||||
|
|
||||||
import Schema from './schema.js';
|
|
||||||
|
|
||||||
test('defaults values', () => {
|
|
||||||
[
|
|
||||||
'uint8',
|
|
||||||
'int8',
|
|
||||||
'uint16',
|
|
||||||
'int16',
|
|
||||||
'uint32',
|
|
||||||
'int32',
|
|
||||||
'uint64',
|
|
||||||
'int64',
|
|
||||||
'float32',
|
|
||||||
'float64',
|
|
||||||
].forEach((type) => {
|
|
||||||
expect(Schema.defaultValue({type}))
|
|
||||||
.to.equal(0);
|
|
||||||
expect(new Schema({type}).specification.defaultValue)
|
|
||||||
.to.equal(0);
|
|
||||||
});
|
|
||||||
expect(Schema.defaultValue({type: 'string'}))
|
|
||||||
.to.equal('');
|
|
||||||
expect(new Schema({type: 'string'}).specification.defaultValue)
|
|
||||||
.to.equal('');
|
|
||||||
expect(Schema.defaultValue({type: 'array', subtype: {type: 'string'}}))
|
|
||||||
.to.deep.equal([]);
|
|
||||||
expect(new Schema({type: 'array', subtype: {type: 'string'}}).specification.defaultValue)
|
|
||||||
.to.deep.equal([]);
|
|
||||||
expect(Schema.defaultValue({
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
foo: {type: 'uint8'},
|
|
||||||
bar: {type: 'string'},
|
|
||||||
baz: {type: 'object', properties: {blah: {type: 'array', subtype: {type: 'string'}}}},
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
.to.deep.equal({foo: 0, bar: '', baz: {blah: []}});
|
|
||||||
expect(new Schema({
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
foo: {type: 'uint8'},
|
|
||||||
bar: {type: 'string'},
|
|
||||||
baz: {type: 'object', properties: {blah: {type: 'array', subtype: {type: 'string'}}}},
|
|
||||||
},
|
|
||||||
}).specification.defaultValue)
|
|
||||||
.to.deep.equal({foo: 0, bar: '', baz: {blah: []}});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('validates a schema', () => {
|
|
||||||
[
|
|
||||||
'uint8',
|
|
||||||
'int8',
|
|
||||||
'uint16',
|
|
||||||
'int16',
|
|
||||||
'uint32',
|
|
||||||
'int32',
|
|
||||||
'uint64',
|
|
||||||
'int64',
|
|
||||||
'float32',
|
|
||||||
'float64',
|
|
||||||
'string',
|
|
||||||
].forEach((type) => {
|
|
||||||
expect(() => {
|
|
||||||
new Schema({type});
|
|
||||||
new Schema({type: 'array', subtype: {type}});
|
|
||||||
new Schema({type: 'object', properties: {foo: {type}}});
|
|
||||||
})
|
|
||||||
.to.not.throw();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('calculates the size of concrete instances', () => {
|
|
||||||
expect(
|
|
||||||
(new Schema({type: 'string'}))
|
|
||||||
.sizeOf('hi')
|
|
||||||
)
|
|
||||||
.to.equal(4 + (new TextEncoder().encode('hi')).length);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
(new Schema(
|
|
||||||
{type: 'object', properties: {
|
|
||||||
foo: {type: 'uint8'},
|
|
||||||
bar: {type: 'uint32'},
|
|
||||||
baz: {type: 'string'},
|
|
||||||
}}
|
|
||||||
))
|
|
||||||
.sizeOf({foo: 69, bar: 420, baz: 'aα'})
|
|
||||||
)
|
|
||||||
.to.equal(
|
|
||||||
1
|
|
||||||
+ 4
|
|
||||||
+ 4 + (new TextEncoder().encode('aα')).length
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
(new Schema({type: 'array', subtype: {type: 'string'}}))
|
|
||||||
.sizeOf(['hallo', 'hαllo'])
|
|
||||||
)
|
|
||||||
.to.equal(
|
|
||||||
4
|
|
||||||
+ 4 + (new TextEncoder().encode('hallo')).length
|
|
||||||
+ 4 + (new TextEncoder().encode('hαllo')).length
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('encodes and decodes', () => {
|
|
||||||
const entries = [
|
|
||||||
[{type: 'uint8'}, 255],
|
|
||||||
[{type: 'int8'}, -128],
|
|
||||||
[{type: 'int8'}, 127],
|
|
||||||
[{type: 'uint16'}, 65535],
|
|
||||||
[{type: 'int16'}, -32768],
|
|
||||||
[{type: 'int16'}, 32767],
|
|
||||||
[{type: 'uint32'}, 4294967295],
|
|
||||||
[{type: 'int32'}, -2147483648],
|
|
||||||
[{type: 'int32'}, 2147483647],
|
|
||||||
[{type: 'uint64'}, 18446744073709551615n],
|
|
||||||
[{type: 'int64'}, -9223372036854775808n],
|
|
||||||
[{type: 'int64'}, 9223372036854775807n],
|
|
||||||
[{type: 'float32'}, 0.5],
|
|
||||||
[{type: 'float64'}, 1.234],
|
|
||||||
[{type: 'string'}, 'hello world'],
|
|
||||||
[{type: 'string'}, 'α'],
|
|
||||||
[{type: 'array', subtype: {type: 'uint8'}}, [1, 2, 3, 4]],
|
|
||||||
[{type: 'array', subtype: {type: 'string'}}, ['one', 'two', 'three', 'four']],
|
|
||||||
[{type: 'object', properties: {foo: {type: 'uint8'}, bar: {type: 'string'}}}, {foo: 64, bar: 'baz'}],
|
|
||||||
];
|
|
||||||
entries.forEach(([specification, concrete]) => {
|
|
||||||
const schema = new Schema(specification);
|
|
||||||
const view = new DataView(new ArrayBuffer(schema.sizeOf(concrete)));
|
|
||||||
schema.serialize(concrete, view);
|
|
||||||
expect(concrete)
|
|
||||||
.to.deep.equal(schema.deserialize(view));
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,112 +0,0 @@
|
||||||
|
|
||||||
import Query from './query.js';
|
|
||||||
|
|
||||||
export default class System {
|
|
||||||
|
|
||||||
destroying = [];
|
|
||||||
|
|
||||||
ecs;
|
|
||||||
|
|
||||||
queries = {};
|
|
||||||
|
|
||||||
constructor(ecs) {
|
|
||||||
this.ecs = ecs;
|
|
||||||
const queries = this.constructor.queries();
|
|
||||||
for (const i in queries) {
|
|
||||||
this.queries[i] = new Query(queries[i], ecs.Types);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deindex(entityIds) {
|
|
||||||
for (const i in this.queries) {
|
|
||||||
this.queries[i].deindex(entityIds);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
destroyEntity(entityId) {
|
|
||||||
this.destroyManyEntities([entityId]);
|
|
||||||
}
|
|
||||||
|
|
||||||
destroyManyEntities(entityIds) {
|
|
||||||
for (let i = 0; i < entityIds.length; i++) {
|
|
||||||
this.destroying.push(entityIds[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
finalize() {}
|
|
||||||
|
|
||||||
static normalize(SystemLike) {
|
|
||||||
if (SystemLike.prototype instanceof System) {
|
|
||||||
return SystemLike;
|
|
||||||
}
|
|
||||||
if ('function' === typeof SystemLike) {
|
|
||||||
class TickingSystem extends System {}
|
|
||||||
TickingSystem.prototype.tick = SystemLike;
|
|
||||||
return TickingSystem;
|
|
||||||
}
|
|
||||||
/* v8 ignore next */
|
|
||||||
throw new TypeError(`Couldn't normalize '${SystemLike}' as a system`);
|
|
||||||
}
|
|
||||||
|
|
||||||
static queries() {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
reindex(entityIds) {
|
|
||||||
for (const i in this.queries) {
|
|
||||||
this.queries[i].reindex(entityIds);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
select(query) {
|
|
||||||
return this.queries[query].select();
|
|
||||||
}
|
|
||||||
|
|
||||||
tickDestruction() {
|
|
||||||
this.deindex(this.destroying);
|
|
||||||
this.destroying = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
tick() {}
|
|
||||||
|
|
||||||
static wrap(source, ecs) {
|
|
||||||
class WrappedSystem extends System.normalize(source) {
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super(ecs);
|
|
||||||
this.reindex(ecs.entities);
|
|
||||||
}
|
|
||||||
|
|
||||||
createEntity(components) {
|
|
||||||
return this.ecs.create(components);
|
|
||||||
}
|
|
||||||
|
|
||||||
createManyEntities(componentsList) {
|
|
||||||
return this.ecs.createMany(componentsList);
|
|
||||||
}
|
|
||||||
|
|
||||||
get source() {
|
|
||||||
return source;
|
|
||||||
}
|
|
||||||
|
|
||||||
insertComponents(entityId, components) {
|
|
||||||
this.ecs.insert(entityId, components);
|
|
||||||
}
|
|
||||||
|
|
||||||
insertManyComponents(components) {
|
|
||||||
this.ecs.insertMany(components);
|
|
||||||
}
|
|
||||||
|
|
||||||
removeComponents(entityId, components) {
|
|
||||||
this.ecs.remove(entityId, components);
|
|
||||||
}
|
|
||||||
|
|
||||||
removeManyComponents(entityIds) {
|
|
||||||
this.ecs.removeMany(entityIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
return new WrappedSystem();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
import Ecs from '@/ecs/ecs.js';
|
|
||||||
import Types from '@/ecs-components/index.js';
|
|
||||||
|
|
||||||
class EngineEcs extends Ecs {
|
|
||||||
static Types = Types;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default EngineEcs;
|
|
|
@ -1,200 +0,0 @@
|
||||||
import {
|
|
||||||
MOVE_MAP,
|
|
||||||
RESOLUTION,
|
|
||||||
TPS,
|
|
||||||
} from '@/constants.js';
|
|
||||||
import ControlMovement from '@/ecs-systems/control-movement.js';
|
|
||||||
import ApplyMomentum from '@/ecs-systems/apply-momentum.js';
|
|
||||||
import CalculateAabbs from '@/ecs-systems/calculate-aabbs.js';
|
|
||||||
import ClampPositions from '@/ecs-systems/clamp-positions.js';
|
|
||||||
import FollowCamera from '@/ecs-systems/follow-camera.js';
|
|
||||||
import UpdateSpatialHash from '@/ecs-systems/update-spatial-hash.js';
|
|
||||||
import RunAnimations from '@/ecs-systems/run-animations.js';
|
|
||||||
import ControlDirection from '@/ecs-systems/control-direction.js';
|
|
||||||
import SpriteDirection from '@/ecs-systems/sprite-direction.js';
|
|
||||||
import Ecs from '@/engine/ecs.js';
|
|
||||||
import {decode, encode} from '@/packets/index.js';
|
|
||||||
|
|
||||||
const players = {
|
|
||||||
0: {
|
|
||||||
Camera: {},
|
|
||||||
Controlled: {up: 0, right: 0, down: 0, left: 0},
|
|
||||||
Direction: {direction: 2},
|
|
||||||
Momentum: {},
|
|
||||||
Position: {x: 368, y: 368},
|
|
||||||
VisibleAabb: {},
|
|
||||||
World: {world: 1},
|
|
||||||
Sprite: {
|
|
||||||
animation: 'moving:down',
|
|
||||||
frame: 0,
|
|
||||||
frames: 8,
|
|
||||||
source: '/assets/dude.json',
|
|
||||||
speed: 0.115,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default class Engine {
|
|
||||||
|
|
||||||
static Ecs = Ecs;
|
|
||||||
|
|
||||||
incomingActions = [];
|
|
||||||
|
|
||||||
connections = [];
|
|
||||||
|
|
||||||
connectedPlayers = new Map();
|
|
||||||
|
|
||||||
ecses = {};
|
|
||||||
|
|
||||||
frame = 0;
|
|
||||||
|
|
||||||
last = Date.now();
|
|
||||||
|
|
||||||
server;
|
|
||||||
|
|
||||||
constructor(Server) {
|
|
||||||
const ecs = new this.constructor.Ecs();
|
|
||||||
const layerSize = {x: Math.ceil(RESOLUTION.x / 4), y: Math.ceil(RESOLUTION.y / 4)};
|
|
||||||
ecs.create({
|
|
||||||
AreaSize: {x: RESOLUTION.x * 4, y: RESOLUTION.y * 4},
|
|
||||||
TileLayers: {
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
data: (
|
|
||||||
Array(layerSize.x * layerSize.y)
|
|
||||||
.fill(0)
|
|
||||||
.map(() => 1 + Math.floor(Math.random() * 4))
|
|
||||||
),
|
|
||||||
size: layerSize,
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
ecs.addSystem(ControlMovement);
|
|
||||||
ecs.addSystem(ApplyMomentum);
|
|
||||||
ecs.addSystem(ClampPositions);
|
|
||||||
ecs.addSystem(FollowCamera);
|
|
||||||
ecs.addSystem(CalculateAabbs);
|
|
||||||
ecs.addSystem(UpdateSpatialHash);
|
|
||||||
ecs.addSystem(ControlDirection);
|
|
||||||
ecs.addSystem(SpriteDirection);
|
|
||||||
ecs.addSystem(RunAnimations);
|
|
||||||
this.ecses = {
|
|
||||||
1: ecs,
|
|
||||||
};
|
|
||||||
class SilphiusServer extends Server {
|
|
||||||
accept(connection, packed) {
|
|
||||||
super.accept(connection, decode(packed));
|
|
||||||
}
|
|
||||||
transmit(connection, packet) {
|
|
||||||
super.transmit(connection, encode(packet));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.server = new SilphiusServer();
|
|
||||||
this.server.addPacketListener('Action', (connection, payload) => {
|
|
||||||
this.incomingActions.push([this.connectedPlayers.get(connection).entity, payload]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async connectPlayer(connection) {
|
|
||||||
this.connections.push(connection);
|
|
||||||
const entityJson = await this.loadPlayer(connection);
|
|
||||||
const ecs = this.ecses[entityJson.World.world];
|
|
||||||
const entity = ecs.create(entityJson);
|
|
||||||
this.connectedPlayers.set(
|
|
||||||
connection,
|
|
||||||
{
|
|
||||||
entity: ecs.get(entity),
|
|
||||||
memory: new Set(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnectPlayer(connection) {
|
|
||||||
const {entity} = this.connectedPlayers.get(connection);
|
|
||||||
const ecs = this.ecses[entity.World.world];
|
|
||||||
players[0] = JSON.parse(JSON.stringify(entity.toJSON()));
|
|
||||||
ecs.destroy(entity.id);
|
|
||||||
this.connectedPlayers.delete(connection);
|
|
||||||
this.connections.splice(this.connections.indexOf(connection), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
async load() {
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadPlayer() {
|
|
||||||
return players[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
start() {
|
|
||||||
return setInterval(() => {
|
|
||||||
const elapsed = (Date.now() - this.last) / 1000;
|
|
||||||
this.last = Date.now();
|
|
||||||
this.tick(elapsed);
|
|
||||||
this.update(elapsed);
|
|
||||||
this.frame += 1;
|
|
||||||
}, 1000 / TPS);
|
|
||||||
}
|
|
||||||
|
|
||||||
tick(elapsed) {
|
|
||||||
for (const i in this.ecses) {
|
|
||||||
this.ecses[i].setClean();
|
|
||||||
}
|
|
||||||
for (const [{Controlled}, payload] of this.incomingActions) {
|
|
||||||
if (payload.type in MOVE_MAP) {
|
|
||||||
Controlled[MOVE_MAP[payload.type]] = payload.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.incomingActions = [];
|
|
||||||
for (const i in this.ecses) {
|
|
||||||
this.ecses[i].tick(elapsed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
update(elapsed) {
|
|
||||||
for (const connection of this.connections) {
|
|
||||||
this.server.send(
|
|
||||||
connection,
|
|
||||||
{
|
|
||||||
type: 'Tick',
|
|
||||||
payload: {
|
|
||||||
ecs: this.updateFor(connection),
|
|
||||||
elapsed,
|
|
||||||
frame: this.frame,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateFor(connection) {
|
|
||||||
const update = {};
|
|
||||||
const {entity, memory} = this.connectedPlayers.get(connection);
|
|
||||||
const mainEntityId = entity.id;
|
|
||||||
const ecs = this.ecses[entity.World.world];
|
|
||||||
const nearby = ecs.system(UpdateSpatialHash).nearby(entity);
|
|
||||||
// Master entity.
|
|
||||||
nearby.add(ecs.get(1));
|
|
||||||
const lastMemory = new Set(memory.values());
|
|
||||||
for (const entity of nearby) {
|
|
||||||
const {id} = entity;
|
|
||||||
lastMemory.delete(id);
|
|
||||||
if (!memory.has(id)) {
|
|
||||||
update[id] = entity.toJSON();
|
|
||||||
if (mainEntityId === id) {
|
|
||||||
update[id].MainEntity = {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (ecs.diff[id]) {
|
|
||||||
update[id] = ecs.diff[id];
|
|
||||||
}
|
|
||||||
memory.add(id);
|
|
||||||
}
|
|
||||||
for (const id of lastMemory) {
|
|
||||||
memory.delete(id);
|
|
||||||
update[id] = false;
|
|
||||||
}
|
|
||||||
return update;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,43 +0,0 @@
|
||||||
import {expect, test} from 'vitest';
|
|
||||||
|
|
||||||
import {RESOLUTION} from '@/constants.js'
|
|
||||||
import Server from '@/net/server/server.js';
|
|
||||||
import Engine from './engine.js';
|
|
||||||
|
|
||||||
test('visibility-based updates', async () => {
|
|
||||||
const engine = new Engine(Server);
|
|
||||||
const ecs = engine.ecses[1];
|
|
||||||
// Create an entity.
|
|
||||||
const entity = ecs.get(ecs.create({
|
|
||||||
Momentum: {x: 1, y: 0},
|
|
||||||
Position: {x: (RESOLUTION.x * 1.5) + 32 - 3, y: 20},
|
|
||||||
VisibleAabb: {},
|
|
||||||
}));
|
|
||||||
// Connect an entity.
|
|
||||||
await engine.connectPlayer(undefined);
|
|
||||||
// Tick and get update. Should be a full update.
|
|
||||||
engine.tick(1);
|
|
||||||
expect(engine.updateFor(undefined))
|
|
||||||
.to.deep.include({2: ecs.get(2).toJSON(), 3: {MainEntity: {}, ...ecs.get(3).toJSON()}});
|
|
||||||
// Tick and get update. Should be a partial update.
|
|
||||||
engine.tick(1);
|
|
||||||
expect(engine.updateFor(undefined))
|
|
||||||
.to.deep.equal({
|
|
||||||
2: {
|
|
||||||
Position: {x: (RESOLUTION.x * 1.5) + 32 - 1},
|
|
||||||
VisibleAabb: {
|
|
||||||
x0: 1199,
|
|
||||||
x1: 1263,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
// Tick and get update. Should remove the entity.
|
|
||||||
engine.tick(1);
|
|
||||||
expect(engine.updateFor(undefined))
|
|
||||||
.to.deep.equal({2: false});
|
|
||||||
// Aim back toward visible area and tick. Should be a full update for that entity.
|
|
||||||
entity.Momentum.x = -1;
|
|
||||||
engine.tick(1);
|
|
||||||
expect(engine.updateFor(undefined))
|
|
||||||
.to.deep.equal({2: ecs.get(2).toJSON()});
|
|
||||||
});
|
|
|
@ -1,23 +0,0 @@
|
||||||
function capitalize(string) {
|
|
||||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function gather(imports, options = {}) {
|
|
||||||
const {
|
|
||||||
mapperForPath = (path) => path.replace(/\.\/(.*)\.js/, '$1'),
|
|
||||||
} = options;
|
|
||||||
const Gathered = {};
|
|
||||||
let id = 1;
|
|
||||||
for (const [path, Component] of Object.entries(imports).sort(([l], [r]) => l < r ? -1 : 1)) {
|
|
||||||
const key = mapperForPath(path)
|
|
||||||
.split('-')
|
|
||||||
.map(capitalize)
|
|
||||||
.join('');
|
|
||||||
if (Component.gathered) {
|
|
||||||
Component.gathered(id, key);
|
|
||||||
}
|
|
||||||
Gathered[key] = Gathered[id] = Component;
|
|
||||||
id += 1;
|
|
||||||
}
|
|
||||||
return Gathered;
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
import {expect, test} from 'vitest';
|
|
||||||
|
|
||||||
import gather from './gather.js';
|
|
||||||
|
|
||||||
test('gathers', async () => {
|
|
||||||
const Gathered = gather(
|
|
||||||
import.meta.glob('./test/*.js', {eager: true, import: 'default'}),
|
|
||||||
{mapperForPath: (path) => path.replace(/\.\/test\/(.*)\.js/, '$1')},
|
|
||||||
);
|
|
||||||
expect(Gathered.First.key)
|
|
||||||
.to.equal('First');
|
|
||||||
expect(Gathered[1].id)
|
|
||||||
.to.equal(1);
|
|
||||||
expect(Gathered.Second.key)
|
|
||||||
.to.equal('Second');
|
|
||||||
expect(Gathered[2].id)
|
|
||||||
.to.equal(2);
|
|
||||||
});
|
|
|
@ -1,6 +0,0 @@
|
||||||
export default class First {
|
|
||||||
static gathered(id, key) {
|
|
||||||
this.id = id;
|
|
||||||
this.key = key;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
export default class Second {
|
|
||||||
static gathered(id, key) {
|
|
||||||
this.id = id;
|
|
||||||
this.key = key;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
/**
|
|
||||||
* By default, Remix will handle hydrating your app on the client for you.
|
|
||||||
* You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
|
|
||||||
* For more information, see https://remix.run/file-conventions/entry.client
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { RemixBrowser } from "@remix-run/react";
|
|
||||||
import { startTransition, StrictMode } from "react";
|
|
||||||
import { hydrateRoot } from "react-dom/client";
|
|
||||||
|
|
||||||
startTransition(() => {
|
|
||||||
hydrateRoot(
|
|
||||||
document,
|
|
||||||
<StrictMode>
|
|
||||||
<RemixBrowser />
|
|
||||||
</StrictMode>
|
|
||||||
);
|
|
||||||
});
|
|
|
@ -1,150 +0,0 @@
|
||||||
/**
|
|
||||||
* By default, Remix will handle generating the HTTP Response for you.
|
|
||||||
* You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
|
|
||||||
* For more information, see https://remix.run/file-conventions/entry.server
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { PassThrough } from "node:stream";
|
|
||||||
|
|
||||||
import { createReadableStreamFromReadable } from "@remix-run/node";
|
|
||||||
import { RemixServer } from "@remix-run/react";
|
|
||||||
import { isbot } from "isbot";
|
|
||||||
import { renderToPipeableStream } from "react-dom/server";
|
|
||||||
|
|
||||||
const ABORT_DELAY = 5_000;
|
|
||||||
|
|
||||||
export async function websocket(server, viteDevServer) {
|
|
||||||
if (viteDevServer) {
|
|
||||||
const {createViteRuntime} = await import('vite');
|
|
||||||
const runtime = await createViteRuntime(viteDevServer);
|
|
||||||
(await runtime.executeEntrypoint('/app/websocket.js')).default(server);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
(await import('./websocket.js')).default(server);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function handleRequest(
|
|
||||||
request,
|
|
||||||
responseStatusCode,
|
|
||||||
responseHeaders,
|
|
||||||
remixContext,
|
|
||||||
// This is ignored so we can keep it in the template for visibility. Feel
|
|
||||||
// free to delete this parameter in your app if you're not using it!
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
loadContext
|
|
||||||
) {
|
|
||||||
return isbot(request.headers.get("user-agent") || "")
|
|
||||||
? handleBotRequest(
|
|
||||||
request,
|
|
||||||
responseStatusCode,
|
|
||||||
responseHeaders,
|
|
||||||
remixContext
|
|
||||||
)
|
|
||||||
: handleBrowserRequest(
|
|
||||||
request,
|
|
||||||
responseStatusCode,
|
|
||||||
responseHeaders,
|
|
||||||
remixContext
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleBotRequest(
|
|
||||||
request,
|
|
||||||
responseStatusCode,
|
|
||||||
responseHeaders,
|
|
||||||
remixContext
|
|
||||||
) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
let shellRendered = false;
|
|
||||||
const { pipe, abort } = renderToPipeableStream(
|
|
||||||
<RemixServer
|
|
||||||
context={remixContext}
|
|
||||||
url={request.url}
|
|
||||||
abortDelay={ABORT_DELAY}
|
|
||||||
/>,
|
|
||||||
{
|
|
||||||
onAllReady() {
|
|
||||||
shellRendered = true;
|
|
||||||
const body = new PassThrough();
|
|
||||||
const stream = createReadableStreamFromReadable(body);
|
|
||||||
|
|
||||||
responseHeaders.set("Content-Type", "text/html");
|
|
||||||
|
|
||||||
resolve(
|
|
||||||
new Response(stream, {
|
|
||||||
headers: responseHeaders,
|
|
||||||
status: responseStatusCode,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
pipe(body);
|
|
||||||
},
|
|
||||||
onShellError(error) {
|
|
||||||
reject(error);
|
|
||||||
},
|
|
||||||
onError(error) {
|
|
||||||
responseStatusCode = 500;
|
|
||||||
// Log streaming rendering errors from inside the shell. Don't log
|
|
||||||
// errors encountered during initial shell rendering since they'll
|
|
||||||
// reject and get logged in handleDocumentRequest.
|
|
||||||
if (shellRendered) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
setTimeout(abort, ABORT_DELAY);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleBrowserRequest(
|
|
||||||
request,
|
|
||||||
responseStatusCode,
|
|
||||||
responseHeaders,
|
|
||||||
remixContext
|
|
||||||
) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
let shellRendered = false;
|
|
||||||
const { pipe, abort } = renderToPipeableStream(
|
|
||||||
<RemixServer
|
|
||||||
context={remixContext}
|
|
||||||
url={request.url}
|
|
||||||
abortDelay={ABORT_DELAY}
|
|
||||||
/>,
|
|
||||||
{
|
|
||||||
onShellReady() {
|
|
||||||
shellRendered = true;
|
|
||||||
const body = new PassThrough();
|
|
||||||
const stream = createReadableStreamFromReadable(body);
|
|
||||||
|
|
||||||
responseHeaders.set("Content-Type", "text/html");
|
|
||||||
|
|
||||||
resolve(
|
|
||||||
new Response(stream, {
|
|
||||||
headers: responseHeaders,
|
|
||||||
status: responseStatusCode,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
pipe(body);
|
|
||||||
},
|
|
||||||
onShellError(error) {
|
|
||||||
reject(error);
|
|
||||||
},
|
|
||||||
onError(error) {
|
|
||||||
responseStatusCode = 500;
|
|
||||||
// Log streaming rendering errors from inside the shell. Don't log
|
|
||||||
// errors encountered during initial shell rendering since they'll
|
|
||||||
// reject and get logged in handleDocumentRequest.
|
|
||||||
if (shellRendered) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
setTimeout(abort, ABORT_DELAY);
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
import {useContext, useEffect} from 'react';
|
|
||||||
|
|
||||||
import ClientContext from '@/context/client.js';
|
|
||||||
|
|
||||||
export default function usePacket(type, fn, dependencies) {
|
|
||||||
const client = useContext(ClientContext);
|
|
||||||
useEffect(() => {
|
|
||||||
if (!client) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
client.addPacketListener(type, fn);
|
|
||||||
return () => {
|
|
||||||
client.removePacketListener(type, fn);
|
|
||||||
};
|
|
||||||
}, [client, ...dependencies]);
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
import {CLIENT_LATENCY} from '@/constants.js';
|
|
||||||
|
|
||||||
export default class Client {
|
|
||||||
constructor() {
|
|
||||||
this.listeners = {};
|
|
||||||
}
|
|
||||||
accept(packet) {
|
|
||||||
const listeners = this.listeners[packet.type];
|
|
||||||
if (!listeners) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const i in listeners) {
|
|
||||||
listeners[i](packet.payload);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
addPacketListener(type, listener) {
|
|
||||||
if (!this.listeners[type]) {
|
|
||||||
this.listeners[type] = [];
|
|
||||||
}
|
|
||||||
this.listeners[type].push(listener);
|
|
||||||
}
|
|
||||||
removePacketListener(type, listener) {
|
|
||||||
const listeners = this.listeners[type];
|
|
||||||
if (!listeners) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const index = listeners.indexOf(listener);
|
|
||||||
if (-1 !== index) {
|
|
||||||
listeners.splice(index, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
send(packet) {
|
|
||||||
if (CLIENT_LATENCY > 0) {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.transmit(packet);
|
|
||||||
}, CLIENT_LATENCY);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.transmit(packet);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,36 +0,0 @@
|
||||||
import {encode} from '@/packets/index.js';
|
|
||||||
|
|
||||||
let connected = false;
|
|
||||||
let socket;
|
|
||||||
|
|
||||||
function onMessage(event) {
|
|
||||||
postMessage(event.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
onmessage = async (event) => {
|
|
||||||
if (!connected) {
|
|
||||||
const url = new URL(`wss://${event.data.host}/ws`)
|
|
||||||
if ('production' === process.env.NODE_ENV) {
|
|
||||||
url.protocol = 'ws:';
|
|
||||||
}
|
|
||||||
socket = new WebSocket(url.href);
|
|
||||||
socket.binaryType = 'arraybuffer';
|
|
||||||
const {promise, resolve} = Promise.withResolvers();
|
|
||||||
socket.addEventListener('open', resolve);
|
|
||||||
socket.addEventListener('error', () => {
|
|
||||||
postMessage(encode({type: 'ConnectionStatus', payload: 'aborted'}));
|
|
||||||
close();
|
|
||||||
});
|
|
||||||
await promise;
|
|
||||||
socket.removeEventListener('open', resolve);
|
|
||||||
socket.addEventListener('message', onMessage);
|
|
||||||
socket.addEventListener('close', () => {
|
|
||||||
postMessage(encode({type: 'ConnectionStatus', payload: 'aborted'}));
|
|
||||||
close();
|
|
||||||
});
|
|
||||||
postMessage(encode({type: 'ConnectionStatus', payload: 'connected'}));
|
|
||||||
connected = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
socket.send(event.data);
|
|
||||||
};
|
|
|
@ -1,67 +0,0 @@
|
||||||
import {CLIENT_PREDICTION} from '@/constants.js';
|
|
||||||
import {encode} from '@/packets/index.js';
|
|
||||||
|
|
||||||
import Client from './client.js';
|
|
||||||
|
|
||||||
export default class RemoteClient extends Client {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
if (CLIENT_PREDICTION) {
|
|
||||||
this.worker = undefined;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.socket = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async connect(host) {
|
|
||||||
if (CLIENT_PREDICTION) {
|
|
||||||
this.worker = new Worker(
|
|
||||||
new URL('../client/prediction.js', import.meta.url),
|
|
||||||
{type: 'module'},
|
|
||||||
);
|
|
||||||
this.worker.postMessage({host});
|
|
||||||
this.worker.onmessage = (event) => {
|
|
||||||
this.accept(event.data);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const url = new URL(`wss://${host}/ws`)
|
|
||||||
if ('production' === process.env.NODE_ENV) {
|
|
||||||
url.protocol = 'ws:';
|
|
||||||
}
|
|
||||||
this.socket = new WebSocket(url.href);
|
|
||||||
this.socket.binaryType = 'arraybuffer';
|
|
||||||
const onMessage = (event) => {
|
|
||||||
this.accept(event.data);
|
|
||||||
}
|
|
||||||
const {promise, resolve} = Promise.withResolvers();
|
|
||||||
this.socket.addEventListener('open', resolve);
|
|
||||||
this.socket.addEventListener('error', () => {
|
|
||||||
this.accept(encode({type: 'ConnectionStatus', payload: 'aborted'}));
|
|
||||||
});
|
|
||||||
await promise;
|
|
||||||
this.socket.removeEventListener('open', resolve);
|
|
||||||
this.socket.addEventListener('message', onMessage);
|
|
||||||
this.socket.addEventListener('close', () => {
|
|
||||||
this.accept(encode({type: 'ConnectionStatus', payload: 'aborted'}));
|
|
||||||
});
|
|
||||||
this.accept(encode({type: 'ConnectionStatus', payload: 'connected'}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
disconnect() {
|
|
||||||
if (CLIENT_PREDICTION) {
|
|
||||||
this.worker.terminate();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.socket.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
transmit(packed) {
|
|
||||||
if (CLIENT_PREDICTION) {
|
|
||||||
this.worker.postMessage(packed);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.socket.send(packed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,34 +0,0 @@
|
||||||
import {Encoder, Decoder} from '@msgpack/msgpack';
|
|
||||||
|
|
||||||
const decoder = new Decoder();
|
|
||||||
const encoder = new Encoder();
|
|
||||||
|
|
||||||
export default class Packet {
|
|
||||||
|
|
||||||
static id;
|
|
||||||
static type;
|
|
||||||
|
|
||||||
static decode(view) {
|
|
||||||
return decoder.decode(new DataView(view.buffer, view.byteOffset + 2, view.byteLength - 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
static encode(payload) {
|
|
||||||
encoder.pos = 2;
|
|
||||||
encoder.doEncode(payload)
|
|
||||||
return new DataView(encoder.bytes.buffer, 0, encoder.pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
static gathered(id, type) {
|
|
||||||
this.id = id;
|
|
||||||
this.type = type;
|
|
||||||
}
|
|
||||||
|
|
||||||
static pack(payload) {
|
|
||||||
return payload;
|
|
||||||
}
|
|
||||||
|
|
||||||
static unpack(packed) {
|
|
||||||
return packed;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
import {expect, test} from 'vitest';
|
|
||||||
|
|
||||||
import Packet from './packet.js';
|
|
||||||
|
|
||||||
class PackedPacket extends Packet {
|
|
||||||
static map = {
|
|
||||||
1: 'one',
|
|
||||||
2: 'two',
|
|
||||||
one: 1,
|
|
||||||
two: 2,
|
|
||||||
};
|
|
||||||
static pack(payload) {
|
|
||||||
return Object.fromEntries(Object.entries(payload).map(([k, v]) => [k, this.map[v]]));
|
|
||||||
}
|
|
||||||
static unpack(payload) {
|
|
||||||
return Object.fromEntries(Object.entries(payload).map(([k, v]) => [k, this.map[v]]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
test('packs and unpacks', async () => {
|
|
||||||
const payload = {foo: 'one', bar: 'two'};
|
|
||||||
const encoded = PackedPacket.encode(payload);
|
|
||||||
expect(Packet.decode(encoded))
|
|
||||||
.to.deep.equal(payload);
|
|
||||||
const packed = PackedPacket.pack(payload)
|
|
||||||
expect(packed)
|
|
||||||
.to.deep.equal({foo: 1, bar: 2});
|
|
||||||
expect(PackedPacket.unpack(packed))
|
|
||||||
.to.deep.equal({foo: 'one', bar: 'two'});
|
|
||||||
});
|
|
|
@ -1,42 +0,0 @@
|
||||||
import {SERVER_LATENCY} from '@/constants.js';
|
|
||||||
|
|
||||||
export default class Server {
|
|
||||||
constructor() {
|
|
||||||
this.listeners = {};
|
|
||||||
}
|
|
||||||
accept(connection, packet) {
|
|
||||||
const listeners = this.listeners[packet.type];
|
|
||||||
if (!listeners) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const i in listeners) {
|
|
||||||
listeners[i](connection, packet.payload);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
addPacketListener(type, listener) {
|
|
||||||
if (!this.listeners[type]) {
|
|
||||||
this.listeners[type] = [];
|
|
||||||
}
|
|
||||||
this.listeners[type].push(listener);
|
|
||||||
}
|
|
||||||
removePacketListener(type, listener) {
|
|
||||||
const listeners = this.listeners[type];
|
|
||||||
if (!listeners) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const index = listeners.indexOf(listener);
|
|
||||||
if (-1 !== index) {
|
|
||||||
listeners.splice(index, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
send(connection, packet) {
|
|
||||||
if (SERVER_LATENCY > 0) {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.transmit(connection, packet);
|
|
||||||
}, SERVER_LATENCY);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.transmit(connection, packet);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
import Engine from '../../engine/engine.js';
|
|
||||||
import {encode} from '@/packets/index.js';
|
|
||||||
|
|
||||||
import Server from './server.js';
|
|
||||||
|
|
||||||
class WorkerServer extends Server {
|
|
||||||
transmit(connection, packed) { postMessage(packed); }
|
|
||||||
}
|
|
||||||
|
|
||||||
const engine = new Engine(WorkerServer);
|
|
||||||
|
|
||||||
onmessage = (event) => {
|
|
||||||
engine.server.accept(undefined, event.data);
|
|
||||||
};
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
await engine.load();
|
|
||||||
engine.start();
|
|
||||||
await engine.connectPlayer(undefined);
|
|
||||||
postMessage(encode({type: 'ConnectionStatus', payload: 'connected'}));
|
|
||||||
})();
|
|
||||||
|
|
||||||
import.meta.hot.accept('../../engine/engine.js', () => {
|
|
||||||
postMessage(encode({type: 'ConnectionStatus', payload: 'aborted'}));
|
|
||||||
close();
|
|
||||||
});
|
|
|
@ -1,3 +0,0 @@
|
||||||
import Packet from '@/net/packet.js';
|
|
||||||
|
|
||||||
export default class ConnectionStatus extends Packet {}
|
|
|
@ -1,20 +0,0 @@
|
||||||
import gather from '@/engine/gather.js';
|
|
||||||
|
|
||||||
const Gathered = gather(import.meta.glob('./*.js', {eager: true, import: 'default'}));
|
|
||||||
|
|
||||||
export function decode(packed) {
|
|
||||||
const view = ArrayBuffer.isView(packed) ? packed : new DataView(packed);
|
|
||||||
const id = view.getUint16(0, true);
|
|
||||||
const Packet = Gathered[id];
|
|
||||||
return {
|
|
||||||
type: Packet.type,
|
|
||||||
payload: Packet.decode(view),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function encode(packet) {
|
|
||||||
const Packet = Gathered[packet.type];
|
|
||||||
const encoded = Packet.encode(packet.payload);
|
|
||||||
encoded.setUint16(0, Packet.id, true);
|
|
||||||
return encoded;
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
import Packet from '@/net/packet.js';
|
|
||||||
|
|
||||||
export default class Tick extends Packet {}
|
|
|
@ -1,27 +0,0 @@
|
||||||
import {useEffect, useState} from 'react';
|
|
||||||
|
|
||||||
import styles from './disconnected.module.css';
|
|
||||||
|
|
||||||
export default function Disconnected() {
|
|
||||||
const [dots, setDots] = useState(3);
|
|
||||||
const [delta, setDelta] = useState(1);
|
|
||||||
useEffect(() => {
|
|
||||||
const handle = setTimeout(() => {
|
|
||||||
const updated = dots + delta;
|
|
||||||
setDots(updated);
|
|
||||||
if (updated < 1 || updated > 5) {
|
|
||||||
setDelta(-delta);
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
return () => {
|
|
||||||
clearTimeout(handle);
|
|
||||||
};
|
|
||||||
}, [dots, delta]);
|
|
||||||
const rendered = Array(dots).fill('.').join('');
|
|
||||||
return (
|
|
||||||
<div className={styles.disconnected}>
|
|
||||||
<p>There's a problem with the connection.</p>
|
|
||||||
<p>{rendered}Reconnection attempt in progress{rendered}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
.disconnected {
|
|
||||||
align-items: center;
|
|
||||||
background-color: rgba(0, 0, 0, 0.4);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
justify-content: center;
|
|
||||||
text-align: center;
|
|
||||||
width: 100%;
|
|
||||||
p {
|
|
||||||
color: #cccccc;
|
|
||||||
font-size: 2em;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,60 +0,0 @@
|
||||||
import {Container} from '@pixi/react';
|
|
||||||
import {useState} from 'react';
|
|
||||||
|
|
||||||
import {RESOLUTION} from '@/constants.js';
|
|
||||||
import Ecs from '@/engine/ecs.js';
|
|
||||||
import usePacket from '@/hooks/use-packet.js';
|
|
||||||
|
|
||||||
import Entities from './entities.jsx';
|
|
||||||
import TileLayer from './tile-layer.jsx';
|
|
||||||
|
|
||||||
export default function EcsComponent() {
|
|
||||||
const [ecs] = useState(new Ecs());
|
|
||||||
const [entities, setEntities] = useState({});
|
|
||||||
const [mainEntity, setMainEntity] = useState();
|
|
||||||
usePacket('Tick', (payload) => {
|
|
||||||
if (0 === Object.keys(payload.ecs).length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ecs.apply(payload.ecs);
|
|
||||||
const updatedEntities = {...entities};
|
|
||||||
for (const id in payload.ecs) {
|
|
||||||
if (false === payload.ecs[id]) {
|
|
||||||
delete updatedEntities[id];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
updatedEntities[id] = ecs.get(id);
|
|
||||||
if (updatedEntities[id].MainEntity) {
|
|
||||||
setMainEntity(ecs.get(id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setEntities(updatedEntities);
|
|
||||||
}, [entities, mainEntity]);
|
|
||||||
if (!mainEntity) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const {Camera} = mainEntity;
|
|
||||||
const {TileLayers} = ecs.get(1);
|
|
||||||
const [cx, cy] = [
|
|
||||||
Math.round(Camera.x - RESOLUTION.x / 2),
|
|
||||||
Math.round(Camera.y - RESOLUTION.y / 2),
|
|
||||||
];
|
|
||||||
return (
|
|
||||||
<Container>
|
|
||||||
<TileLayer
|
|
||||||
size={TileLayers.layers[0].size}
|
|
||||||
tiles={TileLayers.layers[0].data}
|
|
||||||
tileset="/assets/tileset.json"
|
|
||||||
tileSize={{x: 16, y: 16}}
|
|
||||||
x={-cx}
|
|
||||||
y={-cy}
|
|
||||||
/>
|
|
||||||
<Entities
|
|
||||||
entities={entities}
|
|
||||||
x={-cx}
|
|
||||||
y={-cy}
|
|
||||||
/>
|
|
||||||
</Container>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
import {Container} from '@pixi/react';
|
|
||||||
|
|
||||||
import Sprite from './sprite.jsx';
|
|
||||||
|
|
||||||
export default function Entities({entities, x, y}) {
|
|
||||||
const sprites = [];
|
|
||||||
for (const id in entities) {
|
|
||||||
const entity = entities[id];
|
|
||||||
if (!entity.Position || !entity.Sprite) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
sprites.push(
|
|
||||||
<Sprite
|
|
||||||
entity={entity}
|
|
||||||
key={id}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Container x={x} y={y}>
|
|
||||||
{sprites}
|
|
||||||
</Container>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,50 +0,0 @@
|
||||||
import {
|
|
||||||
Stage as PixiStage,
|
|
||||||
} from '@pixi/react';
|
|
||||||
import {SCALE_MODES} from '@pixi/constants';
|
|
||||||
import {settings} from '@pixi/settings';
|
|
||||||
|
|
||||||
import {RESOLUTION} from '@/constants.js';
|
|
||||||
import ClientContext from '@/context/client.js';
|
|
||||||
|
|
||||||
import Ecs from './ecs.jsx';
|
|
||||||
import styles from './pixi.module.css';
|
|
||||||
|
|
||||||
settings.SCALE_MODE = SCALE_MODES.NEAREST;
|
|
||||||
|
|
||||||
const ContextBridge = ({ children, Context, render }) => {
|
|
||||||
return (
|
|
||||||
<Context.Consumer>
|
|
||||||
{(value) =>
|
|
||||||
render(<Context.Provider value={value}>{children}</Context.Provider>)
|
|
||||||
}
|
|
||||||
</Context.Consumer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Stage = ({children, ...props}) => {
|
|
||||||
return (
|
|
||||||
<ContextBridge
|
|
||||||
Context={ClientContext}
|
|
||||||
render={(children) => <PixiStage {...props}>{children}</PixiStage>}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</ContextBridge>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function Pixi() {
|
|
||||||
return (
|
|
||||||
<Stage
|
|
||||||
className={styles.stage}
|
|
||||||
width={RESOLUTION.x}
|
|
||||||
height={RESOLUTION.y}
|
|
||||||
options={{
|
|
||||||
background: 0x1099bb,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Ecs />
|
|
||||||
</Stage>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,35 +0,0 @@
|
||||||
import {Assets} from '@pixi/assets';
|
|
||||||
import {Sprite as PixiSprite} from '@pixi/react';
|
|
||||||
import {useEffect, useState} from 'react';
|
|
||||||
|
|
||||||
export default function Sprite({entity}) {
|
|
||||||
const [asset, setAsset] = useState();
|
|
||||||
useEffect(() => {
|
|
||||||
const asset = Assets.get(entity.Sprite.source);
|
|
||||||
if (asset) {
|
|
||||||
setAsset(asset);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Assets.load(entity.Sprite.source).then(setAsset);
|
|
||||||
}
|
|
||||||
}, [setAsset, entity.Sprite.source]);
|
|
||||||
if (!asset) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
let texture;
|
|
||||||
if (asset.textures) {
|
|
||||||
const animation = asset.animations[entity.Sprite.animation]
|
|
||||||
texture = animation[entity.Sprite.frame];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
texture = asset;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<PixiSprite
|
|
||||||
anchor={0.5}
|
|
||||||
texture={texture}
|
|
||||||
x={Math.round(entity.Position.x)}
|
|
||||||
y={Math.round(entity.Position.y)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,50 +0,0 @@
|
||||||
import {useEffect, useState} from 'react';
|
|
||||||
import {Assets} from '@pixi/assets';
|
|
||||||
import {PixiComponent} from '@pixi/react';
|
|
||||||
import '@pixi/spritesheet'; // NECESSARY!
|
|
||||||
import {CompositeTilemap} from '@pixi/tilemap';
|
|
||||||
|
|
||||||
const TileLayerInternal = PixiComponent('TileLayer', {
|
|
||||||
create: () => new CompositeTilemap(),
|
|
||||||
applyProps: (tilemap, {tiles: oldTiles}, props) => {
|
|
||||||
const {asset, tiles, tileset, tileSize, size, x, y} = props;
|
|
||||||
const extless = tileset.slice('/assets/'.length, -'.json'.length);
|
|
||||||
const {textures} = asset;
|
|
||||||
tilemap.position.x = x;
|
|
||||||
tilemap.position.y = y;
|
|
||||||
if (tiles === oldTiles) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
tilemap.clear();
|
|
||||||
let i = 0;
|
|
||||||
for (let y = 0; y < size.y; ++y) {
|
|
||||||
for (let x = 0; x < size.x; ++x) {
|
|
||||||
tilemap.tile(textures[`${extless}/${tiles[i++]}`], tileSize.x * x, tileSize.y * y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
export default function TileLayer(props) {
|
|
||||||
const {tileset} = props;
|
|
||||||
const [asset, setAsset] = useState();
|
|
||||||
useEffect(() => {
|
|
||||||
const asset = Assets.get(tileset);
|
|
||||||
if (asset) {
|
|
||||||
setAsset(asset);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Assets.load(tileset).then(setAsset);
|
|
||||||
}
|
|
||||||
}, [setAsset, tileset]);
|
|
||||||
if (!asset) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<TileLayerInternal
|
|
||||||
{...props}
|
|
||||||
asset={asset}
|
|
||||||
tileset={tileset}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
31
app/root.jsx
31
app/root.jsx
|
@ -1,31 +0,0 @@
|
||||||
import {
|
|
||||||
Links,
|
|
||||||
Meta,
|
|
||||||
Outlet,
|
|
||||||
Scripts,
|
|
||||||
ScrollRestoration,
|
|
||||||
} from "@remix-run/react";
|
|
||||||
|
|
||||||
import './root.css';
|
|
||||||
|
|
||||||
export function Layout({ children }) {
|
|
||||||
return (
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charSet="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<Meta />
|
|
||||||
<Links />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
{children}
|
|
||||||
<ScrollRestoration />
|
|
||||||
<Scripts />
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function App() {
|
|
||||||
return <Outlet />;
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
.title {
|
|
||||||
font-size: 10em;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
font-size: 3em;
|
|
||||||
list-style: none;
|
|
||||||
text-align: center;
|
|
||||||
li {
|
|
||||||
margin-bottom: 1em;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
import styles from './index.module.css';
|
|
||||||
|
|
||||||
export const meta = () => {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
title: 'Silphius',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'description',
|
|
||||||
content: 'Silphius is an action RPG and homestead simulator',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function Index() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1 className={styles.title}>Silphius</h1>
|
|
||||||
<ul className={styles.actions}>
|
|
||||||
<li><a href="/play/local">Single-player</a></li>
|
|
||||||
<li><a href="/play/remote/localhost:3000">Multi-player</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
.play {
|
|
||||||
display: flex;
|
|
||||||
height: 100%;
|
|
||||||
justify-content: space-around;
|
|
||||||
line-height: 0;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
|
@ -1,86 +0,0 @@
|
||||||
import {useEffect, useState} from 'react';
|
|
||||||
import {useParams} from 'react-router-dom';
|
|
||||||
|
|
||||||
import ClientContext from '@/context/client.js';
|
|
||||||
import LocalClient from '@/net/client/local.js';
|
|
||||||
import RemoteClient from '@/net/client/remote.js';
|
|
||||||
import {decode, encode} from '@/packets/index.js';
|
|
||||||
import Ui from '@/react-components/ui.jsx';
|
|
||||||
|
|
||||||
import styles from './play.module.css';
|
|
||||||
|
|
||||||
export default function Index() {
|
|
||||||
const [client, setClient] = useState();
|
|
||||||
const [disconnected, setDisconnected] = useState(false);
|
|
||||||
const params = useParams();
|
|
||||||
const [type, url] = params['*'].split('/');
|
|
||||||
useEffect(() => {
|
|
||||||
let Client;
|
|
||||||
switch (type) {
|
|
||||||
case 'local':
|
|
||||||
Client = LocalClient;
|
|
||||||
break;
|
|
||||||
case 'remote':
|
|
||||||
Client = RemoteClient;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
class SilphiusClient extends Client {
|
|
||||||
accept(packed) {
|
|
||||||
super.accept(decode(packed));
|
|
||||||
}
|
|
||||||
transmit(packet) {
|
|
||||||
super.transmit(encode(packet));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const client = new SilphiusClient();
|
|
||||||
async function connect() {
|
|
||||||
await client.connect(url);
|
|
||||||
setClient(client);
|
|
||||||
}
|
|
||||||
connect();
|
|
||||||
return () => {
|
|
||||||
client.disconnect();
|
|
||||||
};
|
|
||||||
}, [type, url]);
|
|
||||||
useEffect(() => {
|
|
||||||
if (!client) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
function onConnectionStatus(status) {
|
|
||||||
switch (status) {
|
|
||||||
case 'aborted': {
|
|
||||||
setDisconnected(true);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'connected': {
|
|
||||||
setDisconnected(false);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
client.addPacketListener('ConnectionStatus', onConnectionStatus);
|
|
||||||
return () => {
|
|
||||||
client.removePacketListener('ConnectionStatus', onConnectionStatus);
|
|
||||||
};
|
|
||||||
}, [client]);
|
|
||||||
useEffect(() => {
|
|
||||||
if (!disconnected) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
async function reconnect() {
|
|
||||||
await client.connect(url);
|
|
||||||
}
|
|
||||||
reconnect();
|
|
||||||
const handle = setInterval(reconnect, 1000);
|
|
||||||
return () => {
|
|
||||||
clearInterval(handle);
|
|
||||||
};
|
|
||||||
}, [client, disconnected, url]);
|
|
||||||
return (
|
|
||||||
<div className={styles.play}>
|
|
||||||
<ClientContext.Provider value={client}>
|
|
||||||
<Ui disconnected={disconnected} />
|
|
||||||
</ClientContext.Provider>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
.main-menu {
|
|
||||||
height: 100%;
|
|
||||||
line-height: 1;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
import {Outlet} from 'react-router-dom';
|
|
||||||
|
|
||||||
import styles from './main-menu.module.css';
|
|
||||||
|
|
||||||
export default function MainMenu() {
|
|
||||||
return (
|
|
||||||
<div className={styles['main-menu']}>
|
|
||||||
<Outlet />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,63 +0,0 @@
|
||||||
import {WebSocketServer} from 'ws';
|
|
||||||
|
|
||||||
import Engine from './engine/engine.js';
|
|
||||||
import Server from './net/server/server.js';
|
|
||||||
|
|
||||||
const wss = new WebSocketServer({
|
|
||||||
noServer: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
function onUpgrade(request, socket, head) {
|
|
||||||
const {pathname} = new URL(request.url, 'wss://base.url');
|
|
||||||
if (pathname === '/ws') {
|
|
||||||
wss.handleUpgrade(request, socket, head, function done(ws) {
|
|
||||||
wss.emit('connection', ws, request);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
socket.destroy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function listen(server) {
|
|
||||||
server.on('upgrade', onUpgrade);
|
|
||||||
|
|
||||||
class SocketServer extends Server {
|
|
||||||
transmit(ws, packed) { ws.send(packed); }
|
|
||||||
}
|
|
||||||
|
|
||||||
let onConnect;
|
|
||||||
function makeOnConnect(engine) {
|
|
||||||
return async (ws) => {
|
|
||||||
ws.on('close', () => {
|
|
||||||
engine.disconnectPlayer(ws);
|
|
||||||
})
|
|
||||||
ws.on('message', (packed) => {
|
|
||||||
engine.server.accept(ws, new DataView(packed.buffer, packed.byteOffset, packed.length));
|
|
||||||
});
|
|
||||||
await engine.connectPlayer(ws);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let engine;
|
|
||||||
async function makeEngine(Engine) {
|
|
||||||
const engine = new Engine(SocketServer);
|
|
||||||
await engine.load();
|
|
||||||
engine.start();
|
|
||||||
return engine;
|
|
||||||
}
|
|
||||||
|
|
||||||
engine = await makeEngine(Engine);
|
|
||||||
wss.on('connection', onConnect = makeOnConnect(engine));
|
|
||||||
|
|
||||||
if (import.meta.hot) {
|
|
||||||
import.meta.hot.accept('./engine/engine.js', async ({default: Engine}) => {
|
|
||||||
wss.off('connection', onConnect);
|
|
||||||
for (const [connection] of engine.connectedPlayers) {
|
|
||||||
connection.close();
|
|
||||||
}
|
|
||||||
engine = await makeEngine(Engine);
|
|
||||||
wss.on('connection', onConnect = makeOnConnect(engine));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
25
eslint.config.js
Normal file
25
eslint.config.js
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
// eslint.config.js
|
||||||
|
import globals from 'globals';
|
||||||
|
import js from '@eslint/js';
|
||||||
|
|
||||||
|
export default [
|
||||||
|
js.configs.recommended,
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
process: false,
|
||||||
|
},
|
||||||
|
parserOptions: {
|
||||||
|
ecmaFeatures: {
|
||||||
|
impliedStrict: true,
|
||||||
|
jsx: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
// 'no-unused-vars': 'warn',
|
||||||
|
// 'no-undef': 'warn',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
10110
package-lock.json
generated
10110
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
77
package.json
77
package.json
|
@ -1,57 +1,44 @@
|
||||||
{
|
{
|
||||||
"name": "silphius-next",
|
"name": "silphius",
|
||||||
"private": true,
|
"private": true,
|
||||||
"sideEffects": false,
|
"version": "1.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "remix vite:build",
|
"dev": "npm run vite",
|
||||||
"dev": "node ./server.js",
|
"build": "npm run vite build",
|
||||||
"lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
|
"preview": "npm run vite preview",
|
||||||
"start": "cross-env NODE_ENV=production node ./server.js",
|
"server": "esbuild src/net/server/socket.js --bundle --format=esm --alias:@=`pwd`/src --platform=node --external:./node_modules/* | node --input-type=module -",
|
||||||
|
"start": "npm run dev",
|
||||||
"storybook": "storybook dev -p 6006",
|
"storybook": "storybook dev -p 6006",
|
||||||
"storybook:build": "storybook build"
|
"storybook:build": "storybook build",
|
||||||
|
"vite": "vite --config vite.config.js src"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@chromatic-com/storybook": "^1.4.0",
|
||||||
|
"@eslint/js": "^9.3.0",
|
||||||
|
"@storybook/addon-essentials": "^8.1.3",
|
||||||
|
"@storybook/addon-interactions": "^8.1.3",
|
||||||
|
"@storybook/addon-links": "^8.1.3",
|
||||||
|
"@storybook/addon-onboarding": "^8.1.3",
|
||||||
|
"@storybook/blocks": "^8.1.3",
|
||||||
|
"@storybook/react": "^8.1.3",
|
||||||
|
"@storybook/react-vite": "^8.1.3",
|
||||||
|
"@storybook/test": "^8.1.3",
|
||||||
|
"@vitejs/plugin-react": "^4.3.0",
|
||||||
|
"@vitest/coverage-v8": "^1.6.0",
|
||||||
|
"eslint": "^9.3.0",
|
||||||
|
"globals": "^15.3.0",
|
||||||
|
"prop-types": "^15.8.1",
|
||||||
|
"storybook": "^8.1.3",
|
||||||
|
"vite": "^5.2.11",
|
||||||
|
"vitest": "^1.6.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@msgpack/msgpack": "^3.0.0-beta2",
|
"@msgpack/msgpack": "^3.0.0-beta2",
|
||||||
"@pixi/react": "^7.1.2",
|
"@pixi/react": "^7.1.2",
|
||||||
"@pixi/spritesheet": "^7.4.2",
|
"pixi.js": "^8.1.5",
|
||||||
"@pixi/tilemap": "^4.1.0",
|
"react": "^18.3.1",
|
||||||
"@remix-run/express": "^2.9.2",
|
"react-dom": "^18.3.1",
|
||||||
"@remix-run/node": "^2.9.2",
|
|
||||||
"@remix-run/react": "^2.9.2",
|
|
||||||
"compression": "^1.7.4",
|
|
||||||
"express": "^4.18.2",
|
|
||||||
"isbot": "^4.1.0",
|
|
||||||
"morgan": "^1.10.0",
|
|
||||||
"react": "^18.2.0",
|
|
||||||
"react-dom": "^18.2.0",
|
|
||||||
"ws": "^8.17.0"
|
"ws": "^8.17.0"
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@chromatic-com/storybook": "^1.5.0",
|
|
||||||
"@remix-run/dev": "^2.9.2",
|
|
||||||
"@storybook/addon-essentials": "^8.1.6",
|
|
||||||
"@storybook/addon-interactions": "^8.1.6",
|
|
||||||
"@storybook/addon-links": "^8.1.6",
|
|
||||||
"@storybook/addon-onboarding": "^8.1.6",
|
|
||||||
"@storybook/blocks": "^8.1.6",
|
|
||||||
"@storybook/react": "^8.1.6",
|
|
||||||
"@storybook/react-vite": "^8.1.6",
|
|
||||||
"@storybook/test": "^8.1.6",
|
|
||||||
"@vitejs/plugin-react": "^4.3.1",
|
|
||||||
"@vitest/coverage-v8": "^1.6.0",
|
|
||||||
"cross-env": "^7.0.3",
|
|
||||||
"eslint": "^8.38.0",
|
|
||||||
"eslint-plugin-import": "^2.28.1",
|
|
||||||
"eslint-plugin-jsx-a11y": "^6.7.1",
|
|
||||||
"eslint-plugin-react": "^7.33.2",
|
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
|
||||||
"image-size": "^1.1.1",
|
|
||||||
"storybook": "^8.1.6",
|
|
||||||
"vite": "^5.1.0",
|
|
||||||
"vitest": "^1.6.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because one or more lines are too long
Binary file not shown.
Before Width: | Height: | Size: 9.2 KiB |
|
@ -1,40 +0,0 @@
|
||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
import {writeFileSync} from 'node:fs';
|
|
||||||
import {basename, dirname, extname, join} from 'node:path';
|
|
||||||
|
|
||||||
import imageSize from 'image-size';
|
|
||||||
|
|
||||||
const tileset = process.argv[2];
|
|
||||||
const w = parseInt(process.argv[3]);
|
|
||||||
const h = parseInt(process.argv[4]);
|
|
||||||
|
|
||||||
const {width, height} = imageSize(tileset);
|
|
||||||
|
|
||||||
const json = {
|
|
||||||
frames: {},
|
|
||||||
meta: {
|
|
||||||
format: 'RGBA8888',
|
|
||||||
image: tileset,
|
|
||||||
scale: 1,
|
|
||||||
size: {w: width, h: height},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const extlessPath = join(dirname(tileset), basename(tileset, extname(tileset)));
|
|
||||||
|
|
||||||
let i = 0;
|
|
||||||
for (let y = 0; y < height; y += h) {
|
|
||||||
for (let x = 0; x < width; x += w) {
|
|
||||||
json.frames[join(extlessPath, `${i++}`)] = {
|
|
||||||
frame: {x, y, w, h},
|
|
||||||
spriteSourceSize: {x: 0, y: 0, w, h},
|
|
||||||
sourceSize: {w, h},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
writeFileSync(
|
|
||||||
`${extlessPath}.json`,
|
|
||||||
JSON.stringify(json),
|
|
||||||
);
|
|
File diff suppressed because one or more lines are too long
Binary file not shown.
Before Width: | Height: | Size: 195 KiB |
Binary file not shown.
Before Width: | Height: | Size: 11 KiB |
83
server.js
83
server.js
|
@ -1,83 +0,0 @@
|
||||||
import {createRequestHandler} from '@remix-run/express';
|
|
||||||
import compression from 'compression';
|
|
||||||
import express from 'express';
|
|
||||||
import morgan from 'morgan';
|
|
||||||
|
|
||||||
const isProduction = process.env.NODE_ENV === 'production';
|
|
||||||
|
|
||||||
const app = express();
|
|
||||||
|
|
||||||
let server;
|
|
||||||
if (isProduction) {
|
|
||||||
const {createServer} = await import('node:http');
|
|
||||||
server = createServer(app);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const {execSync} = await import('node:child_process');
|
|
||||||
const {mkdirSync, readFileSync, statSync} = await import('node:fs');
|
|
||||||
const cacheDirectory = `${import.meta.dirname}/node_modules/.cache`;
|
|
||||||
mkdirSync(cacheDirectory, {recursive: true});
|
|
||||||
try {
|
|
||||||
statSync(`${cacheDirectory}/localhost-key.pem`);
|
|
||||||
}
|
|
||||||
catch (error) { // eslint-disable-line no-unused-vars
|
|
||||||
execSync(`mkcert -cert-file ${cacheDirectory}/localhost.pem -key-file ${cacheDirectory}/localhost-key.pem localhost`)
|
|
||||||
}
|
|
||||||
const serverOptions = {
|
|
||||||
key: readFileSync(`${cacheDirectory}/localhost-key.pem`),
|
|
||||||
cert: readFileSync(`${cacheDirectory}/localhost.pem`),
|
|
||||||
};
|
|
||||||
const {createServer} = await import('node:https');
|
|
||||||
server = createServer(serverOptions, app);
|
|
||||||
}
|
|
||||||
|
|
||||||
const viteDevServer = isProduction
|
|
||||||
? undefined
|
|
||||||
: await import('vite').then((vite) =>
|
|
||||||
vite.createServer({
|
|
||||||
server: {middlewareMode: {server}},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const ssr = await (
|
|
||||||
viteDevServer
|
|
||||||
? viteDevServer.ssrLoadModule('virtual:remix/server-build')
|
|
||||||
: import('./build/server/index.js')
|
|
||||||
);
|
|
||||||
|
|
||||||
const remixHandler = createRequestHandler({
|
|
||||||
build: () => ssr,
|
|
||||||
});
|
|
||||||
|
|
||||||
await ssr.entry.module.websocket(server, viteDevServer);
|
|
||||||
|
|
||||||
app.use(compression());
|
|
||||||
|
|
||||||
// http://expressjs.com/en/advanced/best-practice-security.html#at-a-minimum-disable-x-powered-by-header
|
|
||||||
app.disable('x-powered-by');
|
|
||||||
|
|
||||||
// handle asset requests
|
|
||||||
if (viteDevServer) {
|
|
||||||
app.use(viteDevServer.middlewares);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Vite fingerprints its assets so we can cache forever.
|
|
||||||
app.use(
|
|
||||||
'/assets',
|
|
||||||
express.static('build/client/assets', { immutable: true, maxAge: '1y' })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Everything else (like favicon.ico) is cached for an hour. You may want to be
|
|
||||||
// more aggressive with this caching.
|
|
||||||
app.use(express.static('build/client', { maxAge: '1h' }));
|
|
||||||
|
|
||||||
app.use(morgan('tiny'));
|
|
||||||
|
|
||||||
// handle SSR requests
|
|
||||||
app.all('*', remixHandler);
|
|
||||||
|
|
||||||
const port = process.env.PORT || 3000;
|
|
||||||
server.listen(port, () =>
|
|
||||||
console.log(`Express server listening at http${isProduction ? '' : 's'}://localhost:${port}`)
|
|
||||||
);
|
|
BIN
src/assets/bunny.png
Normal file
BIN
src/assets/bunny.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 449 B |
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
10
src/cell.js
Normal file
10
src/cell.js
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import {Ecs} from '@/ecs/index.js';
|
||||||
|
|
||||||
|
export default class Cell {
|
||||||
|
constructor() {
|
||||||
|
this.ecs = new Ecs();
|
||||||
|
}
|
||||||
|
tick(elapsed) {
|
||||||
|
this.ecs.tick(elapsed);
|
||||||
|
}
|
||||||
|
}
|
8
src/components/configuration.jsx
Normal file
8
src/components/configuration.jsx
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
export default function Configuration() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Configuration</h1>
|
||||||
|
<p>This is the configuration page.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
0
src/components/configuration.module.css
Normal file
0
src/components/configuration.module.css
Normal file
|
@ -17,7 +17,7 @@ export default function Dom({children}) {
|
||||||
function onResize() {
|
function onResize() {
|
||||||
const {parentNode} = ref.current;
|
const {parentNode} = ref.current;
|
||||||
const {width} = parentNode.getBoundingClientRect();
|
const {width} = parentNode.getBoundingClientRect();
|
||||||
setScale(width / RESOLUTION.x);
|
setScale(width / RESOLUTION[0]);
|
||||||
}
|
}
|
||||||
window.addEventListener('resize', onResize);
|
window.addEventListener('resize', onResize);
|
||||||
onResize();
|
onResize();
|
||||||
|
@ -28,12 +28,7 @@ export default function Dom({children}) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.dom} ref={ref}>
|
<div className={styles.dom} ref={ref}>
|
||||||
{scale > 0 && (
|
{scale > 0 && (
|
||||||
<style>{`
|
<style>{`.${styles.dom}{--scale:${scale}}`}</style>
|
||||||
.${styles.dom}{
|
|
||||||
--scale: ${scale};
|
|
||||||
--unit: calc(${RESOLUTION.x} / 1000);
|
|
||||||
}
|
|
||||||
`}</style>
|
|
||||||
)}
|
)}
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
19
src/components/entities.jsx
Normal file
19
src/components/entities.jsx
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import {Sprite} from '@pixi/react';
|
||||||
|
|
||||||
|
export default function Entities({entities}) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{
|
||||||
|
entities
|
||||||
|
.map(({image, position: [x, y]}, i) => (
|
||||||
|
<Sprite
|
||||||
|
image={image}
|
||||||
|
key={i}
|
||||||
|
x={x}
|
||||||
|
y={y}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,17 +1,15 @@
|
||||||
.hotbar {
|
.hotbar {
|
||||||
--border: calc(var(--unit) * 3px);
|
border: 2px solid #999999;
|
||||||
background-color: rgba(0, 0, 0, 0.2);
|
|
||||||
border: var(--border) solid #444444;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
left: calc(var(--unit) * 225px);
|
left: 135px;
|
||||||
line-height: 0;
|
line-height: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: calc(var(--unit) * 25px);
|
top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.slotWrapper {
|
.slotWrapper {
|
||||||
border: var(--border) solid #999999;
|
border: 2px solid #999999;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
line-height: 0;
|
line-height: 0;
|
||||||
|
@ -21,7 +19,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgba(0, 0, 0, 0.2);
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
62
src/components/pixi.jsx
Normal file
62
src/components/pixi.jsx
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
import {
|
||||||
|
Stage,
|
||||||
|
Container,
|
||||||
|
} from '@pixi/react';
|
||||||
|
import {useContext, useEffect, useState} from 'react';
|
||||||
|
|
||||||
|
import {RESOLUTION} from '@/constants.js';
|
||||||
|
import ClientContext from '@/context/client.js';
|
||||||
|
import {Ecs} from '@/ecs/index.js';
|
||||||
|
|
||||||
|
import Entities from './entities.jsx';
|
||||||
|
import styles from './pixi.module.css';
|
||||||
|
|
||||||
|
export default function Pixi() {
|
||||||
|
const client = useContext(ClientContext);
|
||||||
|
const [entities, setEntities] = useState([]);
|
||||||
|
useEffect(() => {
|
||||||
|
function onMessage(message) {
|
||||||
|
const {type, payload} = message;
|
||||||
|
switch (type) {
|
||||||
|
case 'connected': {
|
||||||
|
setEntities(payload);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'tick': {
|
||||||
|
const {buffer, byteLength, byteOffset} = payload.entities;
|
||||||
|
const view = new DataView(buffer, byteOffset, byteLength);
|
||||||
|
const ecs = new Ecs();
|
||||||
|
ecs.decode(view);
|
||||||
|
const entities = [];
|
||||||
|
for (const entity of ecs.entities) {
|
||||||
|
const {Position, Visible} = ecs.get(entity);
|
||||||
|
entities.push({
|
||||||
|
image: Visible.image,
|
||||||
|
position: [Position.x, Position.y],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
setEntities(entities);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
client.addMessageListener(onMessage);
|
||||||
|
return () => {
|
||||||
|
client.removeMessageListener(onMessage);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<Stage
|
||||||
|
className={styles.stage}
|
||||||
|
width={RESOLUTION[0]}
|
||||||
|
height={RESOLUTION[1]}
|
||||||
|
options={{
|
||||||
|
background: 0x1099bb,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Entities entities={entities} />
|
||||||
|
</Stage>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
46
src/components/silphius.jsx
Normal file
46
src/components/silphius.jsx
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import {useEffect, useState} from 'react';
|
||||||
|
|
||||||
|
import ClientContext from '@/context/client.js';
|
||||||
|
|
||||||
|
import Title from './title.jsx';
|
||||||
|
import Ui from './ui.jsx';
|
||||||
|
|
||||||
|
export default function Silphius() {
|
||||||
|
const connectionTuple = useState();
|
||||||
|
const [client, setClient] = useState();
|
||||||
|
useEffect(() => {
|
||||||
|
if (!connectionTuple[0]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
async function connect() {
|
||||||
|
let Client;
|
||||||
|
switch (connectionTuple[0]) {
|
||||||
|
case 'local':
|
||||||
|
({default: Client} = await import('@/net/client/local.js'));
|
||||||
|
break;
|
||||||
|
case 'remote':
|
||||||
|
({default: Client} = await import('@/net/client/remote.js'));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const client = new Client();
|
||||||
|
await client.connect();
|
||||||
|
client.send({type: 'connect'});
|
||||||
|
setClient(client);
|
||||||
|
}
|
||||||
|
connect();
|
||||||
|
}, [connectionTuple[0]]);
|
||||||
|
return (
|
||||||
|
connectionTuple[0]
|
||||||
|
? (
|
||||||
|
client
|
||||||
|
? (
|
||||||
|
<ClientContext.Provider value={client}>
|
||||||
|
<Ui />
|
||||||
|
</ClientContext.Provider>
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
)
|
||||||
|
: <Title connectionTuple={connectionTuple} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
0
src/components/silphius.module.css
Normal file
0
src/components/silphius.module.css
Normal file
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user