feat: entity devtools

This commit is contained in:
cha0s 2024-11-10 15:53:42 -06:00
parent 21f8864b1b
commit 3df5266ba3
5 changed files with 206 additions and 1 deletions

View File

@ -0,0 +1,172 @@
import {Map} from 'immutable';
import {useCallback, useEffect, useState} from 'react';
import {useClient} from '@/react/context/client.js';
import {useEcs, useEcsTick} from '@/react/context/ecs.js';
import styles from './entities.module.css';
const entityJsonPaths = Object.keys(import.meta.glob('%/**/*.entity.json'));
function entityLabel(entity) {
let label = `${entity.id}`;
const {Player, Position} = entity;
if (1 === entity.id) {
label = 'Master';
}
if (Player) {
label += `: Player (${Player.id})`;
}
if (Position) {
label += `: [${Position.x.toFixed(2)}, ${Position.y.toFixed(2)}]`
}
return label;
}
function Entities({eventsChannel}) {
const client = useClient();
const ecsRef = useEcs();
const [activeEntity, setActiveEntity] = useState(1);
const [creatingEntityPath, setCreatingEntityPath] = useState(entityJsonPaths[0]);
const [entities, setEntities] = useState(Map());
const onEcsTick = useCallback((payload, ecs) => {
setEntities((entities) => {
return entities.withMutations((entities) => {
if (0 === entities.size) {
for (const id in ecs.$$entities) {
const entity = ecs.get(id);
entities.set(id, entityLabel(entity));
}
return;
}
for (const id in payload) {
const update = payload[id];
if (false === update) {
entities.delete(id);
}
else {
const entity = ecs.get(id);
entities.set(id, entityLabel(entity));
}
}
});
});
}, []);
useEcsTick(onEcsTick);
useEffect(() => {
if (!ecsRef.current) {
return;
}
const master = ecsRef.current.get(1);
if (!master) {
return;
}
const {TileLayers} = master;
const {size, tileSize} = TileLayers.layer(0);
function onClick({x, y}, {shiftKey}) {
const at = {
x: shiftKey ? Math.floor(x / tileSize.x) * tileSize.x + (tileSize.x / 2) : x,
y: shiftKey ? Math.floor(y / tileSize.y) * tileSize.y + (tileSize.y / 2) : y,
};
if (at.x < 0 || at.y < 0 || at.x >= size.x || at.y >= size.y) {
return;
}
const entity = ecsRef.current.get(activeEntity);
if (!entity.Position) {
return;
}
client.send({
type: 'AdminAction',
payload: {
type: 'moveEntity',
value: {
id: activeEntity,
at,
},
},
});
}
eventsChannel.addListener('click', onClick);
return () => {
eventsChannel.removeListener('click', onClick);
};
}, [activeEntity, client, ecsRef, eventsChannel]);
const options = [];
for (const [id, label] of entities.entries()) {
options.push(
<option
key={id}
value={id}
>
{label}
</option>);
}
return (
<div className={styles.entities}>
<form>
<div
className={styles.activeEntity}
>
<button
onClick={(event) => {
client.send({
type: 'AdminAction',
payload: {
type: 'destroyEntity',
value: {
id: activeEntity,
},
},
});
event.preventDefault();
}}
>
-
</button>
<select
onChange={(event) => {
setActiveEntity(event.target.value);
}}
value={activeEntity}
>
{options}
</select>
</div>
<div
className={styles.createEntity}
>
<button
onClick={(event) => {
client.send({
type: 'AdminAction',
payload: {
type: 'createEntity',
value: {
path: creatingEntityPath,
},
},
});
event.preventDefault();
}}
>
+
</button>
<select
onChange={(event) => {
setCreatingEntityPath(event.target.value);
}}
value={creatingEntityPath}
>
{entityJsonPaths.map((path) => (
<option key={path} value={path}>{path}</option>
))}
</select>
</div>
</form>
</div>
);
}
Entities.displayName = 'Entities';
export default Entities;

View File

@ -0,0 +1,7 @@
.activeEntity {
display: flex;
}
.createEntity {
display: flex;
}

View File

@ -195,6 +195,16 @@ export default class Engine {
});
break;
}
case 'createEntity': {
const {path} = payload.value;
ecs.create({$$extends: path});
break;
}
case 'destroyEntity': {
const {id} = payload.value;
ecs.destroy(parseInt(id));
break;
}
case 'paint': {
const {TileLayers} = ecs.get(1);
const {brush, layer: paintLayer, stamp} = payload.value;
@ -213,6 +223,15 @@ export default class Engine {
}
break;
}
case 'moveEntity': {
const {at: {x, y}, id} = payload.value;
const entity = ecs.get(id);
if (entity.Position) {
entity.Position.x = x;
entity.Position.y = y;
}
break;
}
case 'moveUp':
case 'moveRight':
case 'moveDown':

8
package-lock.json generated
View File

@ -26,6 +26,7 @@
"cross-env": "^7.0.3",
"express": "^4.19.2",
"idb-keyval": "^6.2.1",
"immutable": "^5.0.2",
"isbot": "^4.1.0",
"kefir": "^3.8.8",
"morgan": "^1.10.0",
@ -36,7 +37,7 @@
"remark-mdx": "^3.0.1",
"remark-parse": "^11.0.0",
"simplex-noise": "^4.0.1",
"sylvite": "^1.0.5",
"sylvite": "^1.0.6",
"unified": "^11.0.5",
"unist-util-visit-parents": "^6.0.1",
"ws": "^8.18.0"
@ -11571,6 +11572,11 @@
"node": ">=16.x"
}
},
"node_modules/immutable": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.2.tgz",
"integrity": "sha512-1NU7hWZDkV7hJ4PJ9dur9gTNQ4ePNPN4k9/0YhwjzykTi/+3Q5pF93YU5QoVj8BuOnhLgaY8gs0U2pj4kSYVcw=="
},
"node_modules/import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",

View File

@ -32,6 +32,7 @@
"cross-env": "^7.0.3",
"express": "^4.19.2",
"idb-keyval": "^6.2.1",
"immutable": "^5.0.2",
"isbot": "^4.1.0",
"kefir": "^3.8.8",
"morgan": "^1.10.0",