From 3df5266ba32f1e6f72a83a489b7cf6b487df8606 Mon Sep 17 00:00:00 2001 From: cha0s Date: Sun, 10 Nov 2024 15:53:42 -0600 Subject: [PATCH] feat: entity devtools --- app/react/components/devtools/entities.jsx | 172 ++++++++++++++++++ .../components/devtools/entities.module.css | 7 + app/silphius/server/engine.js | 19 ++ package-lock.json | 8 +- package.json | 1 + 5 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 app/react/components/devtools/entities.jsx create mode 100644 app/react/components/devtools/entities.module.css diff --git a/app/react/components/devtools/entities.jsx b/app/react/components/devtools/entities.jsx new file mode 100644 index 0000000..8c88248 --- /dev/null +++ b/app/react/components/devtools/entities.jsx @@ -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( + ); + } + return ( +
+
+
+ + +
+
+ + +
+
+
+ ); +} + +Entities.displayName = 'Entities'; + +export default Entities; diff --git a/app/react/components/devtools/entities.module.css b/app/react/components/devtools/entities.module.css new file mode 100644 index 0000000..c62fbed --- /dev/null +++ b/app/react/components/devtools/entities.module.css @@ -0,0 +1,7 @@ +.activeEntity { + display: flex; +} + +.createEntity { + display: flex; +} diff --git a/app/silphius/server/engine.js b/app/silphius/server/engine.js index afaaf7e..6b107c9 100644 --- a/app/silphius/server/engine.js +++ b/app/silphius/server/engine.js @@ -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': diff --git a/package-lock.json b/package-lock.json index 6c8bcd8..cbc1097 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 0736bee..5b6abbb 100644 --- a/package.json +++ b/package.json @@ -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",