flow: request/response, entity editing and labels

This commit is contained in:
cha0s 2024-11-10 18:19:17 -06:00
parent 9262e7b1a8
commit 04fd533811
9 changed files with 141 additions and 42 deletions

View File

@ -1,12 +1,16 @@
.devtools {
background-color: #444444;
color: white;
font-size: 24px;
height: 100%;
overflow: hidden;
width: 100%;
}
.devtools form {
&, & * {
font-size: 100%;
}
label {
font-weight: bold;
:first-child {
@ -29,6 +33,24 @@
> :global(.react-tabs__tab-panel) {
overflow-y: auto;
}
:global(.react-tabs__tab) {
button {
background: transparent;
border: none;
border-radius: 1em;
cursor: pointer;
left: 0.25em;
padding: 0 0.125em;
position: relative;
visibility: hidden;
&:hover {
background-color: #ffffff33;
}
}
&:hover button {
visibility: visible;
}
}
:global(.react-tabs__tab--selected) {
background-color: #00000022;
color: #ffffff;

View File

@ -1,5 +1,6 @@
import {Map} from 'immutable';
import {useCallback, useEffect, useState} from 'react';
import {Tab, Tabs, TabList, TabPanel} from 'react-tabs';
import {useClient} from '@/react/context/client.js';
import {useEcs, useEcsTick} from '@/react/context/ecs.js';
@ -10,15 +11,15 @@ const entityJsonPaths = Object.keys(import.meta.glob('%/**/*.entity.json'));
function entityLabel(entity) {
let label = `${entity.id}`;
const {Player, Position} = entity;
const {Label, Player} = entity;
if (1 === entity.id) {
label = 'Master';
}
if (Player) {
label += `: Player (${Player.id})`;
label = `Player (${Player.id})`;
}
if (Position) {
label += `: [${Position.x.toFixed(2)}, ${Position.y.toFixed(2)}]`
if (Label) {
label = Label.label;
}
return label;
}
@ -26,7 +27,7 @@ function entityLabel(entity) {
function Entities({eventsChannel}) {
const client = useClient();
const ecsRef = useEcs();
const [activeEntity, setActiveEntity] = useState(1);
const [activeEntity, setActiveEntity] = useState('1');
const [creatingEntityPath, setCreatingEntityPath] = useState(entityJsonPaths[0]);
const [entities, setEntities] = useState(Map());
const onEcsTick = useCallback((payload, ecs) => {
@ -53,6 +54,8 @@ function Entities({eventsChannel}) {
});
}, []);
useEcsTick(onEcsTick);
const list = Array.from(entities.keys())
.toSorted(new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'}).compare);
useEffect(() => {
if (!ecsRef.current) {
return;
@ -92,7 +95,36 @@ function Entities({eventsChannel}) {
};
}, [activeEntity, client, ecsRef, eventsChannel]);
const options = [];
for (const [id, label] of entities.entries()) {
const tabLabels = [];
const tabPanels = [];
for (const id of list) {
const label = entities.get(id);
tabLabels.push(
<Tab key={id}>
{label}
<button
onClick={(event) => {
client.send({
type: 'AdminAction',
payload: {
type: 'destroyEntity',
value: {
id,
},
},
});
event.preventDefault();
}}
>
×
</button>
</Tab>
);
tabPanels.push(
<TabPanel key={id}>
{id}
</TabPanel>
);
options.push(
<option
key={id}
@ -104,40 +136,13 @@ function Entities({eventsChannel}) {
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({
onClick={async (event) => {
event.preventDefault();
const {payload} = await client.request({
type: 'AdminAction',
payload: {
type: 'createEntity',
@ -146,7 +151,7 @@ function Entities({eventsChannel}) {
},
},
});
event.preventDefault();
setActiveEntity(`${payload.id}`);
}}
>
+
@ -162,6 +167,21 @@ function Entities({eventsChannel}) {
))}
</select>
</div>
<div
className={styles.activeEntity}
>
<Tabs
onSelect={(index) => {
setActiveEntity(list[index]);
}}
selectedIndex={list.indexOf(activeEntity)}
>
<TabList>
{tabLabels}
</TabList>
{tabPanels}
</Tabs>
</div>
</form>
</div>
);

View File

@ -0,0 +1,7 @@
import Component from '@/silphius/ecs/component.js';
export default class Label extends Component {
static properties = {
label: {type: 'string'},
};
}

View File

@ -1,5 +1,6 @@
import {CLIENT_LATENCY, CLIENT_INTERPOLATION, CLIENT_PREDICTION} from '@/lib/constants.js';
import EventEmitter from '@/lib/event-emitter.js';
import {withResolvers} from '@/lib/promise.js';
import {decode, encode} from '@/silphius/net/packets/index.js';
import {Flow} from './constants.js';
@ -97,6 +98,25 @@ export default class Client {
removePacketListener(type, listener) {
this.emitter.removeListener(type, listener);
}
async request(packet) {
// needs crypto strength
const id = Math.random();
const request = {
type: 'Request',
payload: {id, request: packet},
}
this.send(request);
const {promise, resolve} = withResolvers();
const listenForId = (payload) => {
if (id !== payload.id) {
return;
}
this.emitter.removeListener(listenForId);
resolve(payload.response);
}
this.emitter.addListener('Response', listenForId);
return promise;
}
send(packet) {
const transmitOrPredict = () => {
if (CLIENT_PREDICTION) {

View File

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

View File

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

View File

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

View File

@ -106,14 +106,21 @@ export default class Engine {
if (!this.incomingActions.has(connection)) {
this.incomingActions.set(connection, []);
}
this.incomingActions.get(connection).push(payload);
this.incomingActions.get(connection).push({payload});
});
this.server.addPacketListener('AdminAction', (connection, payload) => {
// check...
if (!this.incomingActions.has(connection)) {
this.incomingActions.set(connection, []);
}
this.incomingActions.get(connection).push(payload);
this.incomingActions.get(connection).push({payload});
});
this.server.addPacketListener('Request', (connection, payload) => {
if (!this.incomingActions.has(connection)) {
this.incomingActions.set(connection, []);
}
const {request} = payload;
this.incomingActions.get(connection).push({payload: request.payload, respondTo: payload.id});
});
this.server.addPacketListener('Heartbeat', (connection) => {
const playerData = this.connectedPlayers.get(connection);
@ -158,7 +165,7 @@ export default class Engine {
Wielder,
} = entity;
const ecs = this.ecses[Ecs.path];
for (const payload of payloads) {
for (const {payload, respondTo} of payloads) {
switch (payload.type) {
case 'chat': {
if (payload.value.startsWith('/')) {
@ -197,7 +204,20 @@ export default class Engine {
}
case 'createEntity': {
const {path} = payload.value;
ecs.create({$$extends: path});
const id = ecs.create({$$extends: path});
if (respondTo) {
this.server.send(
connection,
{
type: 'Response',
payload: {
id: respondTo,
response: {type: 'AdminActionResponse', payload: {id}},
},
},
);
}
break;
}
case 'destroyEntity': {

View File

@ -1,4 +1,5 @@
{
"Label": {"label": "Shrub"},
"Position": {},
"Sprite": {
"anchorY": 0.7,