Compare commits

..

19 Commits

Author SHA1 Message Date
cha0s
324123df14 fix: scripts HMR 2024-11-05 19:24:39 -06:00
cha0s
ceaa7c1287 fix: destruction dependency 2024-11-05 18:09:14 -06:00
cha0s
11457a3c75 refactor: client code 2024-11-05 18:08:25 -06:00
cha0s
e8c9633a4e refactor: move top-level await with glob imports 2024-11-04 22:13:51 -06:00
cha0s
0e8e58b365 chore: from flecks-vite 2024-11-04 22:12:58 -06:00
cha0s
9872cbb423 chore: disable cors for now 2024-11-04 22:12:16 -06:00
cha0s
626df5b912 fix: websocket 2024-11-04 16:27:31 -06:00
cha0s
2e6e339d6d chore: synchronize with flecks-vite 2024-11-04 12:39:06 -06:00
cha0s
9494e0b901 refactor: silphius app structure 2024-11-04 11:37:57 -06:00
cha0s
4eef129c53 refactor: particles 2024-11-04 11:33:14 -06:00
cha0s
4218f1adc7 refactor util -> lib 2024-11-04 11:28:37 -06:00
cha0s
e599a10186 fix: uri 2024-11-04 11:21:00 -06:00
cha0s
b72a0db2be refactor: cwd 2024-10-22 23:41:11 -05:00
cha0s
21fa00da85 refactor: script in schema 2024-10-21 08:58:49 -05:00
cha0s
01e085499a fix: empty script 2024-10-21 08:01:44 -05:00
cha0s
5fa3b72880 fix: lint WeakRef 2024-10-21 08:01:35 -05:00
cha0s
531266f93e refactor: simplify script paths 2024-10-21 08:01:22 -05:00
cha0s
c499afb246 refactor: path only script + weak ref hmr 2024-10-21 07:52:34 -05:00
cha0s
bd5c069090 refactor: exclude scripts from asset payload 2024-10-21 04:14:17 -05:00
240 changed files with 8150 additions and 5481 deletions

View File

@ -20,29 +20,19 @@ module.exports = {
es6: true,
},
globals: {
process: false,
WeakRef: false,
},
rules: {
'no-constant-condition': ['error', {checkLoops: false}],
},
ignorePatterns: ['!**/.server', '!**/.client'],
// Base config
extends: ['eslint:recommended'],
rules: {
'no-constant-condition': ['error', {checkLoops: false}],
'require-yield': 0,
},
overrides: [
// Tests
{
files: ['**/*.test.{js,jsx,ts,tsx}'],
rules: {
'no-empty-pattern': 'off',
},
},
// React
{
files: ['**/*.{jsx,tsx}'],
files: ['**/*.{js,jsx}'],
plugins: ['react', 'jsx-a11y'],
extends: [
'plugin:react/recommended',
@ -56,8 +46,8 @@ module.exports = {
},
formComponents: ['Form'],
linkComponents: [
{ name: 'Link', linkAttribute: 'to' },
{ name: 'NavLink', linkAttribute: 'to' },
{name: 'Link', linkAttribute: 'to'},
{name: 'NavLink', linkAttribute: 'to'},
],
},
rules: {
@ -71,15 +61,28 @@ module.exports = {
// Node
{
files: [
'app/websocket.js',
'.eslintrc.cjs',
'server.js',
'app/websocket/**',
'**/.server/**',
'*.server.{js,jsx}',
'**/{build,node}.js',
'vite.config.js',
'vitest.workspace.js',
],
env: {
node: true,
},
},
// game scripts
{
files: [
'resources/**/*.js',
],
rules: {
'require-yield': 0,
},
},
],
};

4
.gitignore vendored
View File

@ -2,7 +2,7 @@ node_modules
/.cache
/build
/coverage
.env
/data
/dev
.env

View File

@ -1,17 +1,20 @@
process.env.STORYBOOK = 1
process.env.REMIX_DISABLED = 1
/** @type { import('@storybook/react-vite').StorybookConfig } */
const config = {
stories: ['../stories/**/*.mdx', '../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: [
'@chromatic-com/storybook',
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
],
core: {
disableTelemetry: true,
},
framework: {
name: '@storybook/react-vite',
options: {},
},
stories: ['../app/**/*.stories.mdx', '../app/**/*.stories.@(js|jsx|mjs)'],
};
export default config;

9
.vscode/css_custom_data.json vendored Normal file
View File

@ -0,0 +1,9 @@
{
"version": 1.1,
"atDirectives": [
{
"name": "@tailwind",
"description": "Use the @tailwind directive to insert Tailwind's `base`, `components`, `utilities`, and `screens` styles into your CSS."
}
]
}

49
.vscode/launch.json vendored
View File

@ -7,59 +7,38 @@
{
"type": "chrome",
"request": "launch",
"name": "Silphius Chrome",
"url": "https://localhost:3000",
"name": "Client",
"webRoot": "${workspaceFolder}",
"runtimeArgs": ["--auto-open-devtools-for-tabs"]
"outputCapture": "std",
"runtimeArgs": [
"https://localhost:3200/",
"https://localhost:3200/storybook"
]
},
{
"type": "node",
"request": "launch",
"name": "Silphius Dev",
"name": "Server",
"skipFiles": [
"<node_internals>/**"
],
"resolveSourceMapLocations": [],
"cwd": "${workspaceFolder}",
"env": {
"MKCERT": "1",
"PORT": "3200"
},
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev"],
},
{
"type": "chrome",
"request": "launch",
"name": "Storybook Chrome",
"url": "http://localhost:6006",
"webRoot": "${workspaceFolder}",
},
{
"type": "node",
"request": "launch",
"name": "Storybook Dev",
"skipFiles": [
"<node_internals>/**"
],
"resolveSourceMapLocations": [],
"cwd": "${workspaceFolder}",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "storybook", "--", "--no-open"],
},
],
"compounds": [
{
"name": "Silphius",
"name": "App",
"configurations": [
"Silphius Dev",
"Silphius Chrome",
"Server",
"Client",
],
"stopAll": true,
},
{
"name": "Storybook",
"configurations": [
"Storybook Dev",
"Storybook Chrome",
],
"stopAll": true,
}
]
}

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"css.customData": [".vscode/css_custom_data.json"]
}

View File

@ -1,85 +0,0 @@
import Client from '@/net/client.js';
import {decode, encode} from '@/net/packets/index.js';
import {CLIENT_INTERPOLATION, CLIENT_PREDICTION} from '@/util/constants.js';
export default class LocalClient extends Client {
server = null;
interpolator = null;
predictor = null;
async connect() {
this.server = new Worker(
new URL('../server/worker.js', import.meta.url),
{type: 'module'},
);
if (CLIENT_INTERPOLATION) {
this.interpolator = new Worker(
new URL('./interpolator.js', import.meta.url),
{type: 'module'},
);
this.interpolator.addEventListener('message', (event) => {
const packet = event.data;
if (CLIENT_PREDICTION) {
this.predictor.postMessage([1, packet]);
}
else {
this.accept(packet);
}
});
}
if (CLIENT_PREDICTION) {
this.predictor = new Worker(
new URL('./predictor.js', import.meta.url),
{type: 'module'},
);
this.predictor.addEventListener('message', (event) => {
const [flow, packet] = event.data;
switch (flow) {
case 0: {
const packed = encode(packet);
this.throughput.$$up += packed.byteLength;
this.server.postMessage(packed);
break;
}
case 1: {
this.accept(packet);
break;
}
}
});
}
this.server.addEventListener('message', (event) => {
if (0 === event.data) {
this.server.terminate();
this.server = null;
return;
}
this.throughput.$$down += event.data.byteLength;
const packet = decode(event.data);
if (CLIENT_INTERPOLATION) {
this.interpolator.postMessage(packet);
}
else if (CLIENT_PREDICTION) {
this.predictor.postMessage([1, packet]);
}
else {
this.accept(packet);
}
});
}
disconnect() {
this.server.postMessage(0);
if (CLIENT_INTERPOLATION) {
this.interpolator.terminate();
}
}
transmit(packet) {
if (CLIENT_PREDICTION) {
this.predictor.postMessage([0, packet]);
}
else {
const packed = encode(packet);
this.throughput.$$up += packed.byteLength;
this.server.postMessage(packed);
}
}
}

View File

@ -1,88 +0,0 @@
import Client from '@/net/client.js';
import {decode, encode} from '@/net/packets/index.js';
import {CLIENT_INTERPOLATION, CLIENT_PREDICTION} from '@/util/constants.js';
export default class RemoteClient extends Client {
socket = null;
interpolator = null;
predictor = null;
async connect(host) {
this.interpolator = new Worker(
new URL('./interpolator.js', import.meta.url),
{type: 'module'},
);
if (CLIENT_INTERPOLATION) {
this.interpolator = new Worker(
new URL('./interpolator.js', import.meta.url),
{type: 'module'},
);
this.interpolator.addEventListener('message', (event) => {
this.accept(event.data);
});
}
if (CLIENT_PREDICTION) {
this.predictor = new Worker(
new URL('./predictor.js', import.meta.url),
{type: 'module'},
);
this.predictor.addEventListener('message', (event) => {
const [flow, packet] = event.data;
switch (flow) {
case 0: {
const packed = encode(packet);
this.throughput.$$up += packed.byteLength;
this.socket.send(packed);
break;
}
case 1: {
if (CLIENT_INTERPOLATION) {
this.interpolator.postMessage(packet);
}
else {
this.accept(packet);
}
break;
}
}
});
}
const url = new URL(`wss://${host}/ws`)
this.socket = new WebSocket(url.href);
this.socket.binaryType = 'arraybuffer';
this.socket.addEventListener('message', (event) => {
this.throughput.$$down += event.data.byteLength;
const packet = decode(event.data);
if (CLIENT_PREDICTION) {
this.predictor.postMessage([1, packet]);
}
else if (CLIENT_INTERPOLATION) {
this.interpolator.postMessage(packet);
}
else {
this.accept(packet);
}
});
this.socket.addEventListener('close', () => {
this.accept({type: 'ConnectionStatus', payload: 'aborted'});
});
this.socket.addEventListener('error', () => {
this.accept({type: 'ConnectionStatus', payload: 'aborted'});
});
this.accept({type: 'ConnectionStatus', payload: 'connected'});
}
disconnect() {
if (CLIENT_INTERPOLATION) {
this.interpolator.terminate();
}
}
transmit(packet) {
if (CLIENT_PREDICTION) {
this.predictor.postMessage([0, packet]);
}
else {
const packed = encode(packet);
this.throughput.$$up += packed.byteLength;
this.socket.send(packed);
}
}
}

View File

@ -1,3 +0,0 @@
import Component from '@/ecs/component.js';
export default class Grabber extends Component {}

View File

@ -1,31 +0,0 @@
import Component from '@/ecs/component.js';
export default class Harmful extends Component {
instanceFromSchema() {
const {ecs} = this;
return class HarmfulInstance extends super.instanceFromSchema() {
harm(other) {
const entity = ecs.get(this.entity);
const script = this.$$harm.clone();
script.locals.other = other;
script.locals.entity = entity;
entity.Ticking.add(script.ticker());
}
}
}
load(instance) {
// heavy handed...
if ('undefined' !== typeof window) {
return;
}
instance.$$harm = this.ecs.readScript(
instance.harmScript,
{
ecs: this.ecs,
},
);
}
static properties = {
harmScript: {type: 'string'},
};
}

View File

@ -1,52 +0,0 @@
import Component from '@/ecs/component.js';
export default class Plant extends Component {
instanceFromSchema() {
const {ecs} = this;
const Instance = super.instanceFromSchema();
return class PlantInstance extends Instance {
$$grow;
$$mayGrow;
grow() {
const {Ticking} = ecs.get(this.entity);
Ticking.add(this.$$grow.ticker());
}
mayGrow() {
return this.$$mayGrow.evaluate();
}
};
}
load(instance) {
// heavy handed...
if ('undefined' !== typeof window) {
return;
}
instance.$$grow = this.ecs.readScript(
instance.growScript,
{
ecs: this.ecs,
plant: instance,
},
);
instance.$$mayGrow = this.ecs.readScript(
instance.mayGrowScript,
{
ecs: this.ecs,
plant: instance,
},
);
}
// heavy handed...
markChange() {}
static properties = {
growScript: {type: 'string'},
growth: {type: 'uint16'},
growthFactor: {defaultValue: 127, type: 'uint8'},
mayGrowScript: {type: 'string'},
stage: {type: 'uint8'},
stages: {
type: 'array',
subtype: {type: 'uint16'},
},
};
}

View File

@ -1,3 +0,0 @@
import Component from '@/ecs/component.js';
export default class Shop extends Component {}

View File

@ -4,9 +4,9 @@
* 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";
import {RemixBrowser} from '@remix-run/react';
import {startTransition, StrictMode} from 'react';
import {hydrateRoot} from 'react-dom/client';
startTransition(() => {
hydrateRoot(

View File

@ -4,17 +4,18 @@
* For more information, see https://remix.run/file-conventions/entry.server
*/
import { PassThrough } from "node:stream";
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";
import {createReadableStreamFromReadable} from '@remix-run/node';
import {RemixServer} from '@remix-run/react';
import {isbot} from 'isbot';
import {renderToPipeableStream} from 'react-dom/server';
import {hooks} from 'virtual:sylvite/server';
const ABORT_DELAY = 5_000;
export async function handleUpgrade(request, socket, head) {
const {handleUpgrade} = await import('./server/websocket.js');
const {handleUpgrade} = await import('./websocket.js');
handleUpgrade(request, socket, head);
}
@ -23,23 +24,21 @@ export default function handleRequest(
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") || "")
return isbot(request.headers.get('user-agent') || '')
? handleBotRequest(
request,
responseStatusCode,
responseHeaders,
remixContext
remixContext,
)
: handleBrowserRequest(
request,
responseStatusCode,
responseHeaders,
remixContext
remixContext,
loadContext,
);
}
@ -51,7 +50,7 @@ function handleBotRequest(
) {
return new Promise((resolve, reject) => {
let shellRendered = false;
const { pipe, abort } = renderToPipeableStream(
const {pipe, abort} = renderToPipeableStream(
<RemixServer
context={remixContext}
url={request.url}
@ -63,7 +62,7 @@ function handleBotRequest(
const body = new PassThrough();
const stream = createReadableStreamFromReadable(body);
responseHeaders.set("Content-Type", "text/html");
responseHeaders.set('Content-Type', 'text/html');
resolve(
new Response(stream, {
@ -97,11 +96,12 @@ function handleBrowserRequest(
request,
responseStatusCode,
responseHeaders,
remixContext
remixContext,
loadContext,
) {
return new Promise((resolve, reject) => {
let shellRendered = false;
const { pipe, abort } = renderToPipeableStream(
const {pipe, abort} = renderToPipeableStream(
<RemixServer
context={remixContext}
url={request.url}
@ -110,15 +110,11 @@ function handleBrowserRequest(
{
onShellReady() {
shellRendered = true;
const body = new PassThrough();
const body = hooks.call('@/sylvite/cha0s-remix:requestBody', new PassThrough(), request, loadContext);
const stream = createReadableStreamFromReadable(body);
responseHeaders.set("Content-Type", "text/html");
// security
// responseHeaders.set("Cross-Origin-Embedder-Policy", "require-corp");
// responseHeaders.set("Cross-Origin-Opener-Policy", "same-origin");
// responseHeaders.set("Cross-Origin-Resource-Policy", "same-site");
// responseHeaders.set("Content-Security-Policy", "default-src 'self'");
responseHeaders.set('Content-Type', 'text/html');
hooks.call('@/sylvite/cha0s-remix:responseHeaders', responseHeaders, request, loadContext);
resolve(
new Response(stream, {

View File

@ -1,4 +1,4 @@
import Ticker from '@/util/ticker.js';
import Ticker from '@/lib/ticker.js';
export default function delta(object, properties) {
const deltas = {};

View File

@ -5,7 +5,7 @@ import {createNoise2D} from 'simplex-noise';
import {unified} from 'unified';
import {visitParents as visit} from 'unist-util-visit-parents';
import {TAU} from '@/util/math.js';
import {TAU} from '@/lib/math.js';
const rawNoise = createNoise2D();
const noise = (x, y) => (1 + rawNoise(x, y)) / 2;

14
app/lib/event-source.js Normal file
View File

@ -0,0 +1,14 @@
import streamResponse from './stream-response';
export function eventSource(request, init) {
const headers = new Headers();
headers.set('Content-Type', 'text/event-stream');
return streamResponse(request, headers, (send, close) => {
const encoder = new TextEncoder();
function sendEvent(data, event = 'message') {
send(encoder.encode(`event: ${event}\n`));
send(encoder.encode(`data: ${data}\n\n`));
}
return init(sendEvent, close);
});
}

View File

@ -1,4 +1,4 @@
import Ticker from '@/util/ticker.js';
import Ticker from '@/lib/ticker.js';
const Modulators = {
flat: () => 0.5,

7
app/lib/long-poll.js Normal file
View File

@ -0,0 +1,7 @@
import streamResponse from './stream-response';
export function longPoll(request, init) {
const headers = new Headers();
headers.set('Content-Type', 'text/html');
return streamResponse(request, headers, init);
}

View File

@ -1,7 +1,7 @@
// import {Texture} from '@pixi/core';
// import {Sprite} from '@pixi/sprite';
import {bresenham, createNoise2D, TAU} from '@/util/math.js';
import {bresenham, createNoise2D, TAU} from '@/lib/math.js';
const simplex2D = createNoise2D();

View File

@ -1,9 +1,9 @@
import K from 'kefir';
import * as easings from '@/util/easing.js';
import {TAU} from '@/util/math.js';
import * as easings from '@/lib/easing.js';
import {TAU} from '@/lib/math.js';
export default class Emitter {
export class Emitter {
constructor(ecs) {
this.ecs = ecs;
this.scheduled = [];

View File

@ -1,9 +1,9 @@
import {expect, test} from 'vitest';
import Components from '@/ecs/components/index.js';
import Ecs from '@/ecs/ecs.js';
import Components from '@/silphius/ecs/components/index.js';
import Ecs from '@/silphius/ecs/ecs.js';
import Emitter from './emitter.js';
import {Emitter} from './particles.js';
test('emits particles at once', async () => {
const ecs = new Ecs({

View File

@ -1,3 +1,5 @@
import Script from '@/lib/script.js';
export async function computeMissing(current, manifest) {
const missing = [];
for (const path in manifest) {
@ -30,7 +32,7 @@ export async function fetchMissingResources(manifest, signal) {
}
}
}
loadResources(current);
await loadResources(current);
}
export async function fetchResources(paths, {signal} = {}) {
@ -53,7 +55,8 @@ export async function get() {
const cache = new Map();
export function loadResources(resources) {
export async function loadResources(resources) {
await Script.registerScriptsModule();
for (const path in resources) {
cache.set(path, resources[path].asset);
}

View File

@ -25,7 +25,7 @@ async function computeAsset(path) {
}
async function assetPaths() {
return glob(RESOURCES_GLOB, {nodir: true});
return glob(RESOURCES_GLOB, {ignore: '**/*.js', nodir: true});
}
export async function createManifest() {
@ -48,7 +48,7 @@ export async function loadManifest() {
for (const path in created) {
manifest[path] = created[path];
}
const watcher = chokidar.watch(RESOURCES_GLOB);
const watcher = chokidar.watch(RESOURCES_GLOB, {ignored: '**/*.js'});
watcher.on('add', async (path) => {
const asset = await computeAsset(path);
manifest[asset.path] = asset.hash.toString('hex');

View File

@ -1,48 +1,41 @@
import Ticker from '@/util/ticker.js';
import Ticker from '@/lib/ticker.js';
export default class Script {
static registered = {};
constructor(fn, locals) {
constructor(path, fn, locals) {
if (!fn) {
throw new TypeError('Script needs a function');
}
this.fn = fn;
this.iterator = null;
this.locals = locals;
this.path = path;
this.$$ticker = null;
}
clone() {
return new this.constructor(this.fn, this.locals);
return new this.constructor(this.path, this.fn, this.locals);
}
evaluate() {
return this.fn(this.locals).next().value;
}
static load(pathOrFunction, locals) {
let fn;
if (this.registered[pathOrFunction]) {
fn = this.registered[pathOrFunction];
}
else if (pathOrFunction) {
try {
fn = eval(pathOrFunction);
}
catch (error) {
console.error("Couldn't eval script", pathOrFunction);
console.error(error);
}
}
static load(path, locals) {
const fn = this.registered[path];
if (!fn) {
return undefined;
throw new Error(`no such script: ${path}`);
}
const script = new this(fn, locals);
const script = new this(path, fn, locals);
if (import.meta.hot) {
const hotRef = new WeakRef(script);
import.meta.hot.accept('./scripts.js', ({default: scripts}) => {
script.fn = scripts[`../..${pathOrFunction}`];
const ref = hotRef.deref();
if (ref) {
ref.fn = scripts[path];
}
});
}
return script;
@ -52,6 +45,13 @@ export default class Script {
this.registered[path] = fn;
}
static async registerScriptsModule() {
const {default: scripts} = await import('./scripts.js');
for (const path in scripts) {
Script.register(path, scripts[path]);
}
}
reset() {
this.iterator = null;
this.$$ticker = null;
@ -107,16 +107,8 @@ export default class Script {
}
function register({default: scripts}) {
for (const path in scripts) {
Script.register(path.slice('../..'.length), scripts[path]);
}
}
register(await import('./scripts.js'));
if (import.meta.hot) {
import.meta.hot.accept('./scripts.js', (M) => {
register(M);
import.meta.hot.accept('./scripts.js', () => {
Script.registerScriptsModule();
});
}

View File

@ -1,5 +1,4 @@
export default import.meta.glob(
'../../resources/**/*.js',
'%/**/*.js',
{eager: true, import: 'default'},
);

View File

@ -6,4 +6,8 @@ export function singleton(key, value) {
singleton.reset = function (key) {
delete global.__singletons[key];
}
};
singleton.set = function (key, value) {
global.__singletons[key] = value;
};

View File

@ -1,4 +1,4 @@
import {clamp, intersects} from '@/util/math.js';
import {clamp, intersects} from '@/lib/math.js';
export default class SpatialHash {

View File

@ -0,0 +1,30 @@
export default function streamResponse(request, headers, init) {
headers.set('Connection', 'keep-alive');
headers.set('Cache-Control', 'no-store, no-transform');
const stream = new ReadableStream({
start(controller) {
function send(chunk) {
controller.enqueue(chunk);
}
const cleanup = init(send, close);
let closed = false;
function close() {
if (closed) {
return;
}
cleanup();
closed = true;
request.signal.removeEventListener('abort', close);
controller.close();
}
request.signal.addEventListener('abort', close);
if (request.signal.aborted) {
close();
}
},
});
return new Response(stream, {
headers,
status: 200,
});
}

View File

@ -0,0 +1,33 @@
import {Buffer} from 'node:buffer';
import {Transform} from 'node:stream';
const TERMINATION = Buffer.from('</body></html>');
export class TopLevelLongPoll extends Transform {
constructor(pump) {
super();
this.pump = pump;
}
async _flush(done) {
await this.pump((chunk) => {
this.push(chunk);
});
this.push(TERMINATION);
done();
}
_transform(chunk, encoding, done) {
if (0 === Buffer.compare(TERMINATION, chunk.slice(-TERMINATION.length))) {
this.push(chunk.slice(0, -TERMINATION.length));
done();
}
else {
this.push(chunk);
done();
}
}
}

View File

@ -1,4 +1,4 @@
import Ticker from '@/util/ticker.js';
import Ticker from '@/lib/ticker.js';
import * as Easing from './easing';

View File

@ -1,40 +0,0 @@
import {CLIENT_LATENCY, CLIENT_PREDICTION} from '@/util/constants.js';
import EventEmitter from '@/util/event-emitter.js';
export default class Client {
emitter = new EventEmitter();
rtt = 0;
throughput = {$$down: 0, down: 0, $$up: 0, up: 0};
constructor() {
setInterval(() => {
const {throughput} = this;
throughput.down = throughput.$$down * 4;
throughput.up = throughput.$$up * 4;
throughput.$$down = throughput.$$up = 0;
}, 250);
}
accept(packet) {
if ('Heartbeat' === packet.type) {
this.rtt = packet.payload.rtt;
this.send(packet);
return;
}
this.emitter.invoke(packet.type, packet.payload);
}
addPacketListener(type, listener) {
this.emitter.addListener(type, listener);
}
removePacketListener(type, listener) {
this.emitter.removeListener(type, listener);
}
send(packet) {
if (CLIENT_LATENCY > 0 && !CLIENT_PREDICTION) {
setTimeout(() => {
this.transmit(packet);
}, CLIENT_LATENCY);
}
else {
this.transmit(packet);
}
}
}

View File

@ -1,3 +0,0 @@
import Packet from '@/net/packet.js';
export default class Tick extends Packet {}

View File

@ -1,5 +1,5 @@
import Ecs from '@/ecs/ecs.js';
import {readAsset} from '@/util/resources.js';
import Ecs from '@/silphius/ecs/ecs.js';
import {readAsset} from '@/lib/resources.js';
export default class ClientEcs extends Ecs {
constructor(specification) {

View File

@ -204,7 +204,7 @@ export default function Tiles({eventsChannel}) {
<img
alt="tileset"
ref={imageRef}
src={TileLayers.layer(0).source.replace('.json', '.png')}
src={TileLayers.layer(0).source.replace('.sprite.json', '.png')}
/>
</div>
<div

View File

@ -1,7 +1,7 @@
import {memo, useEffect, useState} from 'react';
import {useRadians} from '@/react/context/radians.js';
import {render} from '@/util/dialogue.js';
import {render} from '@/lib/dialogue.js';
import styles from './message.module.css';

View File

@ -1,9 +1,9 @@
import {memo, useCallback, useRef} from 'react';
import {DamageTypes} from '@/ecs/components/vulnerable.js';
import {DamageTypes} from '@/silphius/ecs/components/vulnerable.js';
import {useEcsTick} from '@/react/context/ecs.js';
import useAnimationFrame from '@/react/hooks/use-animation-frame.js';
import {easeInOutExpo, easeInQuint, easeOutQuad, linear} from '@/util/easing.js';
import {easeInOutExpo, easeInQuint, easeOutQuad, linear} from '@/lib/easing.js';
import styles from './damages.module.css';

View File

@ -1,4 +1,4 @@
import {RESOLUTION} from '@/util/constants.js';
import {RESOLUTION} from '@/lib/constants.js';
import styles from './dialogue-caret.module.css';

View File

@ -3,8 +3,8 @@ import {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {useDomScale} from '@/react/context/dom-scale.js';
import {useRadians} from '@/react/context/radians.js';
import useAnimationFrame from '@/react/hooks/use-animation-frame.js';
import {RESOLUTION} from '@/util/constants.js';
import {render} from '@/util/dialogue.js';
import {RESOLUTION} from '@/lib/constants.js';
import {render} from '@/lib/dialogue.js';
import DialogueCaret from './dialogue-caret.jsx';
import styles from './dialogue.module.css';

View File

@ -1,7 +1,7 @@
import {useEffect, useRef, useState} from 'react';
import DomContext from '@/react/context/dom-scale.js';
import {RESOLUTION} from '@/util/constants.js';
import {RESOLUTION} from '@/lib/constants.js';
import styles from './dom.module.css';

View File

@ -2,8 +2,8 @@ import {useCallback, useState} from 'react';
import {usePacket} from '@/react/context/client.js';
import {useEcsTick} from '@/react/context/ecs.js';
import {RESOLUTION} from '@/util/constants.js';
import {parseLetters} from '@/util/dialogue.js';
import {RESOLUTION} from '@/lib/constants.js';
import {parseLetters} from '@/lib/dialogue.js';
import Damages from './damages.jsx';
import Entity from './entity.jsx';

View File

@ -1,5 +1,5 @@
import Emitter from '@/particles/emitter.js';
import createEcs from '@/server/create/ecs.js';
import {Emitter} from '@/lib/particles.js';
import createEcs from '@/silphius/server/create/ecs.js';
import ClientEcs from './client-ecs.js';

View File

@ -8,7 +8,7 @@ import ClientContext from '@/react/context/client.js';
import DebugContext from '@/react/context/debug.js';
import EcsContext from '@/react/context/ecs.js';
import MainEntityContext from '@/react/context/main-entity.js';
import {RESOLUTION} from '@/util/constants.js';
import {RESOLUTION} from '@/lib/constants.js';
import Ecs from './ecs.jsx';
import {ApplicationStageLayers, ApplicationStageLights} from './extensions.js';

View File

@ -7,7 +7,7 @@ import '@pixi/spritesheet'; // NECESSARY!
import {CompositeTilemap} from '@pixi/tilemap';
import {useAsset} from '@/react/context/assets.js';
import {CHUNK_SIZE, RESOLUTION} from '@/util/constants.js';
import {CHUNK_SIZE, RESOLUTION} from '@/lib/constants.js';
import {deferredLighting} from './lights.js';

View File

@ -4,9 +4,9 @@ import {useClient, usePacket} from '@/react/context/client.js';
import {useDebug} from '@/react/context/debug.js';
import {useEcs, useEcsTick} from '@/react/context/ecs.js';
import {useMainEntity} from '@/react/context/main-entity.js';
import {RESOLUTION} from '@/util/constants.js';
import EventEmitter from '@/util/event-emitter.js';
import {distribute} from '@/util/inventory.js';
import {RESOLUTION} from '@/lib/constants.js';
import EventEmitter from '@/lib/event-emitter.js';
import {distribute} from '@/lib/inventory.js';
import addKeyListener from './add-key-listener.js';
import Disconnected from './dom/disconnected.jsx';
@ -233,20 +233,6 @@ function Ui({disconnected}) {
mainEntityRef,
]);
usePacket('EcsChange', onEcsChangePacket);
const onTickPacket = useCallback((payload, client) => {
if (!ecsRef.current || 0 === Object.keys(payload.ecs).length) {
return;
}
try {
ecsRef.current.apply(payload.ecs);
client.emitter.invoke(':Ecs', payload.ecs);
}
catch (error) {
ecsRef.current = undefined;
console.error('tick crash', error);
}
}, [ecsRef]);
usePacket('Tick', onTickPacket);
const onEcsTick = useCallback((payload, ecs) => {
for (const id in payload) {
const entity = ecs.get(id);

View File

@ -1,7 +1,7 @@
import {useCallback, useState} from 'react';
import useAnimationFrame from '@/react/hooks/use-animation-frame.js';
import {TAU} from '@/util/math.js';
import {TAU} from '@/lib/math.js';
export function useRadians() {
const [radians, setRadians] = useState(0);

View File

@ -0,0 +1,29 @@
import {useEffect, useState} from 'react';
export function useWebSocket(path, {host, schema} = {}) {
const [socket, setSocket] = useState();
useEffect(() => {
async function connect() {
const socket = new WebSocket(`${schema || ''}//${host || location.host}${path || '/'}`);
socket.addEventListener('open', () => {
setSocket(socket);
});
socket.addEventListener('close', ({reason}) => {
if ('hmr' === reason) {
connect();
}
setSocket(undefined);
});
}
connect();
return () => {
setSocket((socket) => {
if (socket) {
socket.close();
}
return undefined;
});
}
}, [path, host, schema]);
return socket;
}

View File

@ -4,11 +4,11 @@ import {
Outlet,
Scripts,
ScrollRestoration,
} from "@remix-run/react";
} from '@remix-run/react';
import './root.css';
export function Layout({ children }) {
export function Layout({children}) {
return (
<html lang="en">
<head>

View File

@ -18,12 +18,13 @@ export default function PlaySpecific() {
const debugTuple = useState(false);
const [Components, setComponents] = useState();
const [Systems, setSystems] = useState();
const reconnectionBackoff = useRef(0);
const ecsRef = useRef();
const [disconnected, setDisconnected] = useState(false);
const params = useParams();
const [type, url] = params['*'].split('/');
useEffect(() => {
if (!Client) {
if (!Client || !Components || !Systems) {
return;
}
const client = new Client();
@ -35,7 +36,7 @@ export default function PlaySpecific() {
return () => {
client.disconnect();
};
}, [Client, url]);
}, [Client, Components, Systems, url]);
// Sneakily use beforeunload to snag some time to save.
useEffect(() => {
if ('local' !== type) {
@ -62,8 +63,8 @@ export default function PlaySpecific() {
}, [ecsRef, Components, Systems]);
useEffect(() => {
async function setEcsStuff() {
const {default: Components} = await import('@/ecs/components/index.js');
const {default: Systems} = await import('@/ecs/systems/index.js');
const {default: Components} = await import('@/silphius/ecs/components/index.js');
const {default: Systems} = await import('@/silphius/ecs/systems/index.js');
setComponents(Components);
setSystems(Systems);
}
@ -84,19 +85,52 @@ export default function PlaySpecific() {
client.removePacketListener('EcsChange', onEcsChangePacket);
};
}, [client, refreshEcs]);
const onTickPacket = useCallback((payload) => {
if (!ecsRef.current || 0 === Object.keys(payload.ecs).length) {
return;
}
reconnectionBackoff.current = 0;
setDisconnected(false);
try {
ecsRef.current.apply(payload.ecs);
client.emitter.invoke(':Ecs', payload.ecs);
}
catch (error) {
ecsRef.current = undefined;
console.error('tick crash', error);
}
}, [client, ecsRef]);
useEffect(() => {
if (!client) {
return;
}
client.addPacketListener('Tick', onTickPacket);
return () => {
client.removePacketListener('Tick', onTickPacket);
};
}, [client, onTickPacket]);
useEffect(() => {
if (!client) {
return;
}
let handle;
function onConnectionStatus(status) {
switch (status) {
case 'aborted': {
client.disconnect();
setDisconnected(true);
break;
}
case 'connected': {
setDisconnected(false);
if (!handle) {
client.disconnect();
setDisconnected(true);
reconnectionBackoff.current = (
Math.max(100, Math.min(1000, reconnectionBackoff.current * 2))
);
handle = setTimeout(
() => {
client.connect(url);
handle = null;
},
reconnectionBackoff.current,
);
}
break;
}
}
@ -104,8 +138,9 @@ export default function PlaySpecific() {
client.addPacketListener('ConnectionStatus', onConnectionStatus);
return () => {
client.removePacketListener('ConnectionStatus', onConnectionStatus);
clearTimeout(handle);
};
}, [client]);
}, [client, url]);
useEffect(() => {
if (!client) {
return;
@ -125,15 +160,6 @@ export default function PlaySpecific() {
client.removePacketListener('Download', onDownload);
};
}, [client]);
useEffect(() => {
if (!client || !disconnected) {
return;
}
async function reconnect() {
await client.connect(url);
}
reconnect();
}, [client, disconnected, mainEntityRef, url]);
// useEffect(() => {
// let source = true;
// async function play() {

View File

@ -3,7 +3,7 @@ import {json, useLoaderData} from "@remix-run/react";
import {useEffect, useState} from 'react';
import {Outlet, useParams} from 'react-router-dom';
import {fetchMissingResources, readAsset} from '@/util/resources.js';
import {fetchMissingResources, readAsset} from '@/lib/resources.js';
import styles from './play.module.css';
@ -13,8 +13,8 @@ settings.ADAPTER.fetch = async (path) => {
};
export async function loader({request}) {
const {juggleSession} = await import('@/server/session.server.js');
const {loadManifest} = await import('@/util/resources.server.js');
const {juggleSession} = await import('@/silphius/server/session.server.js');
const {loadManifest} = await import('@/lib/resources.server.js');
await juggleSession(request);
return json({
manifest: await loadManifest(),
@ -43,10 +43,10 @@ export default function Play() {
let Client;
switch (type) {
case 'local':
({default: Client} = await import('@/client/local.js'));
({default: Client} = await import('@/silphius/client/local.js'));
break;
case 'remote':
({default: Client} = await import('@/client/remote.js'));
({default: Client} = await import('@/silphius/client/remote.js'));
break;
}
setClient(() => Client);

View File

@ -2,7 +2,7 @@
import {Form, json, redirect, useLoaderData} from '@remix-run/react';
import {useState} from 'react';
import {commitSession, getSession, juggleSession} from '@/server/session.server.js';
import {commitSession, getSession, juggleSession} from '@/silphius/server/session.server.js';
import NoiseField from './noise-field.jsx';

View File

@ -4,7 +4,7 @@ import {useState} from 'react';
import SliderText from '@/react/components/dev/slider-text.jsx';
import TileLayer from '@/react/components/pixi/tile-layer.jsx';
import AssetsContext from '@/react/context/assets.js';
import {CHUNK_SIZE} from '@/util/constants.js';
import {CHUNK_SIZE} from '@/lib/constants.js';
import alea from 'alea';
import {createNoise2D} from 'simplex-noise';

View File

@ -1,4 +1,4 @@
import {encodeResources, loadResources} from '@/util/resources.server.js';
import {encodeResources, loadResources} from '@/lib/resources.server.js';
export async function action({request}) {
const paths = await request.json();

View File

@ -1,95 +0,0 @@
import {mkdir, readFile, unlink, writeFile} from 'node:fs/promises';
import {dirname, join} from 'node:path';
import {WebSocketServer} from 'ws';
import Server from '@/net/server.js';
import {getSession} from '@/server/session.server.js';
import {loadResources, readAsset} from '@/util/resources.js';
import {loadResources as loadServerResources} from '@/util/resources.server.js';
import Engine from './engine.js';
global.__silphiusWebsocket = null;
class SocketServer extends Server {
async ensurePath(path) {
await mkdir(path, {recursive: true});
}
async load() {
await loadResources(await loadServerResources());
}
static qualify(path) {
return join(import.meta.dirname, '..', '..', 'data', 'remote', 'UNIVERSE', path);
}
readAsset(path) {
return readAsset(path) ?? new ArrayBuffer(0);
}
async readData(path) {
const qualified = this.constructor.qualify(path);
await this.ensurePath(dirname(qualified));
return readFile(qualified);
}
async removeData(path) {
await unlink(path);
}
async writeData(path, view) {
const qualified = this.constructor.qualify(path);
await this.ensurePath(dirname(qualified));
await writeFile(qualified, view);
}
transmit(ws, packed) { ws.send(packed); }
}
export async function handleUpgrade(request, socket, head) {
if (!global.__silphiusWebsocket) {
const engine = new Engine(SocketServer);
await engine.load();
engine.start();
const handleConnection = async (ws, request) => {
ws.on('close', async () => {
await engine.disconnectPlayer(ws);
})
ws.on('message', (packed) => {
engine.server.accept(ws, new DataView(packed.buffer, packed.byteOffset, packed.length));
});
const session = await getSession(request.headers['cookie']);
await engine.connectPlayer(ws, session.get('id'));
};
const wss = new WebSocketServer({
noServer: true,
});
wss.on('connection', handleConnection);
global.__silphiusWebsocket = {engine, handleConnection, wss};
}
const {pathname} = new URL(request.url, 'wss://base.url');
if (pathname === '/ws') {
const {wss} = global.__silphiusWebsocket;
wss.handleUpgrade(request, socket, head, function done(ws) {
wss.emit('connection', ws, request);
});
}
else {
socket.destroy();
}
}
if (import.meta.hot) {
import.meta.hot.on('vite:beforeUpdate', async () => {
if (global.__silphiusWebsocket) {
const {engine, handleConnection, wss} = global.__silphiusWebsocket;
wss.off('connection', handleConnection);
const connections = [];
for (const [connection] of engine.connectedPlayers) {
engine.server.send(connection, {type: 'EcsChange'});
connections.push(connection);
}
await engine.stop();
for (const connection of connections) {
connection.close();
}
global.__silphiusWebsocket = null;
}
});
import.meta.hot.accept();
}

17
app/silphius/build.js Normal file
View File

@ -0,0 +1,17 @@
import {fileURLToPath} from 'node:url';
export async function implement({hooks}) {
hooks.tap('sylvite:viteConfig', (config) => ({
...config,
resolve: {
...config.resolve,
alias: [
...config.resolve.alias,
{
find: '%',
replacement: fileURLToPath(new URL('../../resources', import.meta.url))
},
],
},
}));
}

View File

@ -0,0 +1,27 @@
import Client from '@/silphius/net/client.js';
export default class LocalClient extends Client {
server = null;
async connect() {
await super.connect();
this.server = new Worker(
new URL('../server/worker.js', import.meta.url),
{type: 'module'},
);
this.server.addEventListener('message', (event) => {
if (0 === event.data) {
this.server.terminate();
this.server = null;
return;
}
this.receive(event.data);
});
}
disconnect() {
super.disconnect();
this.server.postMessage(0);
}
transmit(packed) {
this.server.postMessage(packed);
}
}

View File

@ -0,0 +1,25 @@
import Client from '@/silphius/net/client.js';
export default class RemoteClient extends Client {
socket = null;
async connect(host) {
await super.connect();
this.socket = new WebSocket(`//${host}/silphius`);
this.socket.binaryType = 'arraybuffer';
this.socket.addEventListener('message', (event) => {
this.receive(event.data);
});
this.socket.addEventListener('close', () => {
this.accept({type: 'ConnectionStatus', payload: 'aborted'});
});
this.socket.addEventListener('error', () => {
this.accept({type: 'ConnectionStatus', payload: 'aborted'});
});
this.socket.addEventListener('open', () => {
this.accept({type: 'ConnectionStatus', payload: 'connected'});
});
}
transmit(packed) {
this.socket.send(packed);
}
}

View File

@ -134,11 +134,22 @@ export default class Component {
$$entity = 0;
destroy() {}
initialize(values, defaults) {
const {properties} = concrete;
for (const key in values) {
this[`$$${key}`] = values[key];
if (properties[key]?.$.set) {
properties[key].$.set(Component, this, `$$${key}`, values[key]);
}
else {
this[`$$${key}`] = values[key];
}
}
for (const key in defaults) {
this[`$$${key}`] = defaults[key];
if (properties[key]?.$.set) {
properties[key].$.set(Component, this, `$$${key}`, defaults[key]);
}
else {
this[`$$${key}`] = defaults[key];
}
}
Component.ecs.markChange(this.entity, {[Component.constructor.componentName]: values})
}
@ -146,7 +157,12 @@ export default class Component {
const {properties} = concrete;
const json = {};
for (const key in properties) {
json[key] = this[key];
if (properties[key]?.$.json) {
json[key] = properties[key].$.json(this[key]);
}
else {
json[key] = this[key];
}
}
return json;
}
@ -160,9 +176,15 @@ export default class Component {
return this.toFullJSON();
}
update(values) {
const {properties} = concrete;
for (const key in values) {
if (concrete.properties[key]) {
this[`$$${key}`] = values[key];
if (properties[key]) {
if (properties[key]?.$.set) {
properties[key].$.set(Component, this, `$$${key}`, values[key]);
}
else {
this[`$$${key}`] = values[key];
}
}
else {
this[key] = values[key];
@ -186,7 +208,12 @@ export default class Component {
},
set: function set(value) {
if (this[`$$${key}`] !== value) {
this[`$$${key}`] = value;
if (concrete.properties[key]?.$.set) {
concrete.properties[key].$.set(Component, this, `$$${key}`, value);
}
else {
this[`$$${key}`] = value;
}
Component.markChange(this.entity, key, value);
}
},

View File

@ -1,4 +1,4 @@
import Component from '@/ecs/component.js';
import Component from '@/silphius/ecs/component.js';
export default class Alive extends Component {
instanceFromSchema() {
@ -19,8 +19,8 @@ export default class Alive extends Component {
this.$$dead = true;
const {Ticking} = ecs.get(this.entity);
if (Ticking) {
this.$$death.locals.entity = ecs.get(this.entity);
const ticker = this.$$death.ticker();
this.deathScript.locals.entity = ecs.get(this.entity);
const ticker = this.deathScript.ticker();
ecs.addDestructionDependency(this.entity.id, Ticking.add(ticker));
}
}
@ -30,21 +30,11 @@ export default class Alive extends Component {
if (0 === instance.maxHealth) {
instance.maxHealth = instance.health;
}
// heavy handed...
if ('undefined' !== typeof window) {
return;
}
instance.$$death = this.ecs.readScript(
instance.deathScript,
{
ecs: this.ecs,
},
);
}
static properties = {
deathScript: {
defaultValue: '/resources/misc/death-default.js',
type: 'string',
type: 'script',
},
health: {type: 'uint32'},
maxHealth: {type: 'uint32'},

View File

@ -1,4 +1,4 @@
import Component from '@/ecs/component.js';
import Component from '@/silphius/ecs/component.js';
export default class Animation extends Component {
static properties = {

View File

@ -1,4 +1,4 @@
import Component from '@/ecs/component.js';
import Component from '@/silphius/ecs/component.js';
export default class Arbitrary extends Component {
instanceFromSchema() {

View File

@ -1,4 +1,4 @@
import Component from '@/ecs/component.js';
import Component from '@/silphius/ecs/component.js';
export default class AreaSize extends Component {
static properties = {

View File

@ -1,4 +1,4 @@
import Component from '@/ecs/component.js';
import Component from '@/silphius/ecs/component.js';
export default class Behaving extends Component {
instanceFromSchema() {
@ -16,10 +16,6 @@ export default class Behaving extends Component {
};
}
load(instance) {
// heavy handed...
if ('undefined' !== typeof window) {
return;
}
for (const key in instance.routines) {
instance.$$routineInstances[key] = this.ecs.readScript(instance.routines[key]);
}

View File

@ -1,4 +1,4 @@
import Component from '@/ecs/component.js';
import Component from '@/silphius/ecs/component.js';
export default class Camera extends Component {
static properties = {

View File

@ -1,5 +1,5 @@
import Component from '@/ecs/component.js';
import {distance, intersects, transform} from '@/util/math.js';
import Component from '@/silphius/ecs/component.js';
import {distance, intersects, transform} from '@/lib/math.js';
import vector2d from './helpers/vector-2d';
@ -9,8 +9,6 @@ export default class Collider extends Component {
return class ColliderInstance extends super.instanceFromSchema() {
$$aabb = {x0: Infinity, x1: -Infinity, y0: Infinity, y1: -Infinity};
$$aabbs = [];
$$collisionStart;
$$collisionEnd;
$$intersections = new Map();
get aabb() {
const {Position: {x: px, y: py}} = ecs.get(this.entity);
@ -77,8 +75,8 @@ export default class Collider extends Component {
}
}
if (!hasMatchingIntersection) {
if (this.$$collisionStart) {
const script = this.$$collisionStart.clone();
if (this.collisionStartScript) {
const script = this.collisionStartScript.clone();
script.locals.entity = thisEntity;
script.locals.other = otherEntity;
script.locals.pair = [body, otherBody];
@ -87,8 +85,8 @@ export default class Collider extends Component {
ecs.addDestructionDependency(otherEntity.id, promise);
ecs.addDestructionDependency(thisEntity.id, promise);
}
if (other.$$collisionStart) {
const script = other.$$collisionStart.clone();
if (other.collisionStartScript) {
const script = other.collisionStartScript.clone();
script.locals.entity = otherEntity;
script.locals.other = thisEntity;
script.locals.pair = [otherBody, body];
@ -162,8 +160,8 @@ export default class Collider extends Component {
intersection.entity.bodies[intersection.i],
intersection.other.bodies[intersection.j],
];
if (this.$$collisionEnd) {
const script = this.$$collisionEnd.clone();
if (this.collisionEndScript) {
const script = this.collisionEndScript.clone();
script.locals.other = otherEntity;
script.locals.pair = [body, otherBody];
const ticker = script.ticker();
@ -171,8 +169,8 @@ export default class Collider extends Component {
ecs.addDestructionDependency(thisEntity.id, promise);
ecs.addDestructionDependency(otherEntity.id, promise);
}
if (other.$$collisionEnd) {
const script = other.$$collisionEnd.clone();
if (other.collisionEndScript) {
const script = other.collisionEndScript.clone();
script.locals.other = thisEntity;
script.locals.pair = [otherBody, body];
const ticker = script.ticker();
@ -268,22 +266,6 @@ export default class Collider extends Component {
};
}
instance.updateAabbs();
// heavy handed...
if ('undefined' !== typeof window) {
return;
}
instance.$$collisionEnd = this.ecs.readScript(
instance.collisionEndScript,
{
ecs: this.ecs,
},
);
instance.$$collisionStart = this.ecs.readScript(
instance.collisionStartScript,
{
ecs: this.ecs,
},
);
}
static properties = {
bodies: {
@ -307,8 +289,8 @@ export default class Collider extends Component {
},
},
},
collisionEndScript: {type: 'string'},
collisionStartScript: {type: 'string'},
collisionEndScript: {type: 'script'},
collisionStartScript: {type: 'script'},
isColliding: {defaultValue: 1, type: 'uint8'},
};
}

View File

@ -1,4 +1,4 @@
import Component from '@/ecs/component.js';
import Component from '@/silphius/ecs/component.js';
export default class Controlled extends Component {
instanceFromSchema() {

View File

@ -1,5 +1,5 @@
import Component from '@/ecs/component.js';
import {HALF_PI, TAU} from '@/util/math.js';
import Component from '@/silphius/ecs/component.js';
import {HALF_PI, TAU} from '@/lib/math.js';
export default class Direction extends Component {
instanceFromSchema() {

View File

@ -1,4 +1,4 @@
import Component from '@/ecs/component.js';
import Component from '@/silphius/ecs/component.js';
export default class Ecs extends Component {
static properties = {

View File

@ -1,7 +1,7 @@
import Component from '@/ecs/component.js';
import Component from '@/silphius/ecs/component.js';
import Emitter from '@/particles/emitter.js';
import Ticker from '@/util/ticker.js';
import {Emitter} from '@/lib/particles.js';
import Ticker from '@/lib/ticker.js';
export default class EmitterComponent extends Component {
instanceFromSchema() {

View File

@ -1,4 +1,4 @@
import Component from '@/ecs/component.js';
import Component from '@/silphius/ecs/component.js';
export default class Forces extends Component {
instanceFromSchema() {

View File

@ -0,0 +1,3 @@
import Component from '@/silphius/ecs/component.js';
export default class Grabber extends Component {}

View File

@ -0,0 +1,21 @@
import Component from '@/silphius/ecs/component.js';
export default class Harmful extends Component {
instanceFromSchema() {
const {ecs} = this;
return class HarmfulInstance extends super.instanceFromSchema() {
harm(other) {
const entity = ecs.get(this.entity);
const script = this.harmScript.clone();
script.locals.other = other;
script.locals.entity = entity;
const promise = entity.Ticking.add(script.ticker());
ecs.addDestructionDependency(entity.id, promise);
ecs.addDestructionDependency(other.id, promise);
}
}
}
static properties = {
harmScript: {type: 'script'},
};
}

View File

@ -1,4 +1,4 @@
import Component from '@/ecs/component.js';
import Component from '@/silphius/ecs/component.js';
export default class Health extends Component {
static properties = {

View File

@ -1,4 +1,4 @@
import gather from '@/util/gather.js';
import gather from '@/lib/gather.js';
const Gathered = gather(
import.meta.glob(['./*.js', '!./*.test.js'], {eager: true, import: 'default'}),
@ -13,7 +13,7 @@ if (import.meta.env.PROD) {
);
}
else {
const {default: ieval} = await import('@/util/eval.js');
const {default: ieval} = await import('@/lib/eval.js');
wrapComponent = (componentName, Component) => (
ieval(`
((Component) => (

View File

@ -1,12 +1,11 @@
import Component from '@/ecs/component.js';
import Component from '@/silphius/ecs/component.js';
export default class Interactive extends Component {
instanceFromSchema() {
const {ecs} = this;
return class ControlledInstance extends super.instanceFromSchema() {
$$interact;
interact(initiator) {
const script = this.$$interact.clone();
const script = this.interactScript.clone();
script.locals.initiator = initiator;
script.locals.subject = ecs.get(this.entity);
const {Ticking} = script.locals.subject;
@ -20,20 +19,8 @@ export default class Interactive extends Component {
}
}
}
load(instance) {
// heavy handed...
if ('undefined' !== typeof window) {
return;
}
instance.$$interact = this.ecs.readScript(
instance.interactScript,
{
ecs: this.ecs,
},
);
}
static properties = {
interacting: {type: 'uint8'},
interactScript: {type: 'string'},
interactScript: {type: 'script'},
};
}

View File

@ -1,4 +1,4 @@
import Component from '@/ecs/component.js';
import Component from '@/silphius/ecs/component.js';
export default class Interacts extends Component {
instanceFromSchema() {

View File

@ -1,4 +1,4 @@
import Component from '@/ecs/component.js';
import Component from '@/silphius/ecs/component.js';
export default class Interlocutor extends Component {
mergeDiff(original, update) {

View File

@ -1,6 +1,6 @@
import Component from '@/ecs/component.js';
import Component from '@/silphius/ecs/component.js';
import {distribute} from '@/util/inventory.js';
import {distribute} from '@/lib/inventory.js';
class ItemProxy {
scripts = {};

Some files were not shown because too many files have changed in this diff Show More