silphius/app/react/components/ui.jsx

487 lines
14 KiB
React
Raw Normal View History

2024-07-14 21:07:46 -05:00
import {memo, useEffect, useRef, useState} from 'react';
2024-06-10 22:42:30 -05:00
2024-07-20 04:32:33 -05:00
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';
2024-07-20 04:41:00 -05:00
import {RESOLUTION} from '@/util/constants.js';
2024-06-10 22:42:30 -05:00
2024-07-20 03:46:38 -05:00
import addKeyListener from './add-key-listener.js';
import ClientEcs from './client-ecs.js';
2024-07-12 01:52:43 -05:00
import Disconnected from './dom/disconnected.jsx';
2024-07-14 07:24:15 -05:00
import Chat from './dom/chat/chat.jsx';
2024-07-12 01:52:43 -05:00
import Dom from './dom/dom.jsx';
2024-07-12 02:10:22 -05:00
import Entities from './dom/entities.jsx';
2024-07-12 01:52:43 -05:00
import HotBar from './dom/hotbar.jsx';
2024-07-12 01:29:54 -05:00
import Pixi from './pixi/pixi.jsx';
2024-07-12 01:52:43 -05:00
import Devtools from './devtools.jsx';
2024-06-10 22:42:30 -05:00
import styles from './ui.module.css';
2024-06-24 04:43:11 -05:00
function emptySlots() {
return Array(10).fill(undefined);
}
2024-07-09 16:00:05 -05:00
const devEventsChannel = {
$$listeners: {},
addListener(type, listener) {
if (!this.$$listeners[type]) {
this.$$listeners[type] = new Set();
}
this.$$listeners[type].add(listener);
},
invoke(type, payload) {
const listeners = this.$$listeners[type];
if (!listeners) {
return;
}
for (const listener of listeners) {
listener(payload);
}
},
removeListener(type, listener) {
const listeners = this.$$listeners[type];
if (!listeners) {
return;
}
listeners.delete(listener);
},
};
2024-07-14 21:07:46 -05:00
function Ui({disconnected}) {
2024-06-10 22:42:30 -05:00
// Key input.
2024-06-27 04:08:52 -05:00
const client = useClient();
2024-07-14 16:33:58 -05:00
const chatInputRef = useRef();
2024-07-07 23:30:48 -05:00
const gameRef = useRef();
const [mainEntity, setMainEntity] = useMainEntity();
2024-06-25 08:54:19 -05:00
const [debug, setDebug] = useDebug();
2024-07-03 11:17:36 -05:00
const [ecs, setEcs] = useEcs();
2024-06-13 12:24:32 -05:00
const [showDisconnected, setShowDisconnected] = useState(false);
2024-06-24 08:14:32 -05:00
const [bufferSlot, setBufferSlot] = useState();
2024-07-07 23:30:48 -05:00
const [devtoolsIsOpen, setDevtoolsIsOpen] = useState(false);
const ratio = (RESOLUTION.x * (devtoolsIsOpen ? 2 : 1)) / RESOLUTION.y;
2024-07-12 00:00:03 -05:00
const [camera, setCamera] = useState({x: 0, y: 0});
2024-06-24 04:43:11 -05:00
const [hotbarSlots, setHotbarSlots] = useState(emptySlots());
const [activeSlot, setActiveSlot] = useState(0);
2024-07-01 13:44:52 -05:00
const [scale, setScale] = useState(2);
2024-07-03 11:17:36 -05:00
const [Components, setComponents] = useState();
const [Systems, setSystems] = useState();
2024-07-08 14:07:26 -05:00
const [applyFilters, setApplyFilters] = useState(true);
2024-07-13 17:08:23 -05:00
const [monopolizers, setMonopolizers] = useState([]);
2024-07-14 02:26:43 -05:00
const [message, setMessage] = useState('');
const [chatIsOpen, setChatIsOpen] = useState(false);
const [chatHistory, setChatHistory] = useState([]);
2024-07-14 16:33:58 -05:00
const [chatHistoryCaret, setChatHistoryCaret] = useState(-1);
2024-07-14 21:44:33 -05:00
const [chatMessages, setChatMessages] = useState({});
2024-07-14 16:33:58 -05:00
const [pendingMessage, setPendingMessage] = useState('');
2024-07-03 11:17:36 -05:00
useEffect(() => {
async function setEcsStuff() {
2024-07-20 05:07:39 -05:00
const {default: Components} = await import('@/ecs/components/index.js');
const {default: Systems} = await import('@/ecs/systems/index.js');
2024-07-03 11:17:36 -05:00
setComponents(Components);
setSystems(Systems);
}
setEcsStuff();
}, []);
useEffect(() => {
setEcs(new ClientEcs({Components, Systems}));
}, [Components, setEcs, Systems]);
2024-06-13 12:24:32 -05:00
useEffect(() => {
let handle;
if (disconnected) {
handle = setTimeout(() => {
setShowDisconnected(true);
}, 1000);
2024-06-13 12:24:32 -05:00
}
else {
setShowDisconnected(false)
}
return () => {
clearTimeout(handle);
};
}, [disconnected]);
2024-06-10 22:42:30 -05:00
useEffect(() => {
2024-06-25 08:54:19 -05:00
return addKeyListener(document.body, ({event, type, payload}) => {
2024-07-14 07:24:15 -05:00
if ('Escape' === payload && 'keyDown' === type && chatIsOpen) {
setChatIsOpen(false);
return;
}
2024-07-14 16:33:58 -05:00
if (chatInputRef.current) {
chatInputRef.current.focus();
}
2024-07-14 02:26:43 -05:00
if (chatIsOpen) {
return;
}
const KEY_MAP = {
keyDown: 1,
keyUp: 0,
};
let actionPayload;
switch (payload) {
2024-07-01 13:44:52 -05:00
case '-':
if ('keyDown' === type) {
2024-07-10 16:08:15 -05:00
setScale((scale) => scale > 1 ? scale - 1 : 1);
2024-07-01 13:44:52 -05:00
}
break;
case '=':
case '+':
if ('keyDown' === type) {
setScale((scale) => scale < 4 ? Math.floor(scale + 1) : 4);
}
break;
2024-06-25 08:54:19 -05:00
case 'F3': {
if (event) {
event.preventDefault();
}
if ('keyDown' === type) {
setDebug(!debug);
}
break;
}
2024-07-07 23:30:48 -05:00
case 'F4': {
if (event) {
event.preventDefault();
}
if ('keyDown' === type) {
setDevtoolsIsOpen(!devtoolsIsOpen);
}
break;
}
case 'w': {
actionPayload = {type: 'moveUp', value: KEY_MAP[type]};
break;
}
case 'a': {
actionPayload = {type: 'moveLeft', value: KEY_MAP[type]};
break;
}
case 's': {
actionPayload = {type: 'moveDown', value: KEY_MAP[type]};
break;
}
case 'd': {
actionPayload = {type: 'moveRight', value: KEY_MAP[type]};
break;
}
case ' ': {
actionPayload = {type: 'use', value: KEY_MAP[type]};
break;
}
2024-07-14 02:26:43 -05:00
case 'Enter': {
if ('keyDown' === type) {
setChatIsOpen(true);
}
break
}
2024-07-01 18:12:53 -05:00
case 'e': {
2024-07-13 17:08:23 -05:00
if (KEY_MAP[type]) {
if (monopolizers.length > 0) {
monopolizers[0].trigger();
break;
}
}
2024-07-01 18:12:53 -05:00
actionPayload = {type: 'interact', value: KEY_MAP[type]};
break;
}
case '1': {
if ('keyDown' === type) {
actionPayload = {type: 'changeSlot', value: 1};
}
break;
}
case '2': {
if ('keyDown' === type) {
actionPayload = {type: 'changeSlot', value: 2};
}
break;
}
case '3': {
if ('keyDown' === type) {
actionPayload = {type: 'changeSlot', value: 3};
}
break;
}
case '4': {
if ('keyDown' === type) {
actionPayload = {type: 'changeSlot', value: 4};
}
break;
}
case '5': {
if ('keyDown' === type) {
actionPayload = {type: 'changeSlot', value: 5};
}
break;
}
case '6': {
if ('keyDown' === type) {
actionPayload = {type: 'changeSlot', value: 6};
}
break;
}
case '7': {
if ('keyDown' === type) {
actionPayload = {type: 'changeSlot', value: 7};
}
break;
}
case '8': {
if ('keyDown' === type) {
actionPayload = {type: 'changeSlot', value: 8};
}
break;
}
case '9': {
if ('keyDown' === type) {
actionPayload = {type: 'changeSlot', value: 9};
}
break;
}
case '0': {
if ('keyDown' === type) {
actionPayload = {type: 'changeSlot', value: 10};
}
break;
}
}
if (actionPayload) {
2024-06-10 22:42:30 -05:00
client.send({
type: 'Action',
payload: actionPayload,
2024-06-10 22:42:30 -05:00
});
}
});
2024-07-14 02:26:43 -05:00
}, [chatIsOpen, client, debug, devtoolsIsOpen, monopolizers, setDebug, setScale]);
2024-07-03 11:17:36 -05:00
usePacket('EcsChange', async () => {
setEcs(new ClientEcs({Components, Systems}));
2024-07-14 02:28:32 -05:00
setMainEntity(undefined);
setMonopolizers([]);
2024-07-03 11:17:36 -05:00
}, [Components, Systems, setEcs, setMainEntity]);
usePacket('Tick', async (payload, client) => {
if (0 === Object.keys(payload.ecs).length) {
return;
}
await ecs.apply(payload.ecs);
for (const listener of client.listeners[':Ecs'] ?? []) {
listener(payload.ecs);
}
2024-07-02 20:43:55 -05:00
}, [ecs]);
useEcsTick((payload) => {
let localMainEntity = mainEntity;
for (const id in payload) {
2024-06-24 04:43:11 -05:00
const entity = ecs.get(id);
const update = payload[id];
2024-07-12 01:53:15 -05:00
if (!update) {
continue;
}
2024-06-25 12:29:09 -05:00
if (update.Sound?.play) {
for (const sound of update.Sound.play) {
(new Audio(sound)).play();
}
}
2024-07-12 01:53:15 -05:00
if (update.MainEntity) {
setMainEntity(localMainEntity = id);
}
if (localMainEntity === id) {
2024-06-22 23:32:57 -05:00
if (update.Inventory) {
2024-06-28 14:10:44 -05:00
setBufferSlot(entity.Inventory.item(0));
2024-06-24 04:43:11 -05:00
const newHotbarSlots = emptySlots();
for (let i = 1; i < 11; ++i) {
2024-06-28 12:12:38 -05:00
newHotbarSlots[i - 1] = entity.Inventory.item(i);
2024-06-22 22:04:24 -05:00
}
2024-06-22 11:44:49 -05:00
setHotbarSlots(newHotbarSlots);
}
2024-06-22 23:32:57 -05:00
if (update.Wielder && 'activeSlot' in update.Wielder) {
setActiveSlot(update.Wielder.activeSlot);
}
}
}
2024-07-12 00:00:03 -05:00
if (localMainEntity) {
const mainEntityEntity = ecs.get(localMainEntity);
2024-07-13 00:34:26 -05:00
const x = Math.round((mainEntityEntity.Camera.x * scale) - RESOLUTION.x / 2);
const y = Math.round((mainEntityEntity.Camera.y * scale) - RESOLUTION.y / 2);
if (x !== camera.x || y !== camera.y) {
setCamera({x, y});
}
2024-07-12 00:00:03 -05:00
}
2024-07-13 00:34:26 -05:00
}, [camera, ecs, mainEntity, scale]);
2024-07-02 18:20:53 -05:00
useEffect(() => {
function onContextMenu(event) {
event.preventDefault();
}
document.body.addEventListener('contextmenu', onContextMenu);
return () => {
document.body.removeEventListener('contextmenu', onContextMenu);
};
2024-07-12 01:53:15 -05:00
}, []);
2024-06-10 22:42:30 -05:00
return (
2024-06-24 08:14:32 -05:00
<div
className={styles.ui}
>
2024-06-10 22:42:30 -05:00
<style>
{`
2024-07-07 23:30:48 -05:00
@media (max-aspect-ratio: ${ratio}) { .${styles.game} { width: 100%; } }
@media (min-aspect-ratio: ${ratio}) { .${styles.game} { height: 100%; } }
.${styles.game} {
2024-06-24 08:14:32 -05:00
cursor: ${
bufferSlot
2024-06-28 14:10:44 -05:00
? `url('${bufferSlot.icon}'), auto !important`
2024-06-24 08:14:32 -05:00
: 'auto'
};
}
2024-06-10 22:42:30 -05:00
`}
</style>
2024-07-07 23:30:48 -05:00
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */ }
<div
className={[styles.game, devtoolsIsOpen && styles.devtoolsIsOpen].filter(Boolean).join(' ')}
onMouseDown={(event) => {
2024-07-14 07:24:15 -05:00
if (chatIsOpen) {
event.preventDefault();
return;
}
2024-07-07 23:30:48 -05:00
switch (event.button) {
case 0:
if (devtoolsIsOpen) {
if (!gameRef.current || !mainEntity) {
return;
}
const {top, left, width} = gameRef.current.getBoundingClientRect();
const master = ecs.get(1);
if (!master) {
return;
}
const {Camera} = ecs.get(mainEntity);
const size = width / RESOLUTION.x;
2024-07-09 16:00:05 -05:00
const client = {
2024-07-07 23:30:48 -05:00
x: (event.clientX - left) / size,
y: (event.clientY - top) / size,
};
2024-07-09 16:00:05 -05:00
const camera = {
2024-07-07 23:30:48 -05:00
x: ((Camera.x * scale) - (RESOLUTION.x / 2)),
y: ((Camera.y * scale) - (RESOLUTION.y / 2)),
}
2024-07-09 16:00:05 -05:00
devEventsChannel.invoke(
'click',
{
x: (client.x + camera.x) / scale,
y: (client.y + camera.y) / scale,
2024-07-07 23:30:48 -05:00
},
2024-07-09 16:00:05 -05:00
);
2024-07-07 23:30:48 -05:00
}
else {
client.send({
type: 'Action',
payload: {type: 'use', value: 1},
});
}
break;
case 2:
2024-07-13 17:08:23 -05:00
if (monopolizers.length > 0) {
monopolizers[0].trigger();
break;
}
2024-07-07 23:30:48 -05:00
client.send({
type: 'Action',
payload: {type: 'interact', value: 1},
});
break;
}
}}
onMouseUp={(event) => {
2024-07-14 07:24:15 -05:00
if (chatIsOpen) {
event.preventDefault();
return;
}
2024-07-07 23:30:48 -05:00
switch (event.button) {
case 0:
client.send({
type: 'Action',
payload: {type: 'use', value: 0},
});
break;
case 2:
client.send({
type: 'Action',
2024-07-07 23:30:48 -05:00
payload: {type: 'interact', value: 0},
});
2024-07-07 23:30:48 -05:00
break;
}
event.preventDefault();
}}
onWheel={(event) => {
2024-07-14 07:24:15 -05:00
if (chatIsOpen) {
event.preventDefault();
return;
}
2024-07-07 23:30:48 -05:00
if (event.deltaY > 0) {
client.send({
type: 'Action',
payload: {type: 'changeSlot', value: 1 + ((activeSlot + 1) % 10)},
});
}
else {
client.send({
type: 'Action',
payload: {type: 'changeSlot', value: 1 + ((activeSlot + 9) % 10)},
});
}
}}
ref={gameRef}
>
2024-07-12 00:00:03 -05:00
<Pixi
applyFilters={applyFilters}
camera={camera}
2024-07-13 17:08:23 -05:00
monopolizers={monopolizers}
2024-07-12 00:00:03 -05:00
scale={scale}
/>
2024-07-12 01:52:43 -05:00
<Dom>
2024-07-11 16:44:41 -05:00
<HotBar
active={activeSlot}
onActivate={(i) => {
client.send({
type: 'Action',
payload: {type: 'swapSlots', value: [0, i + 1]},
});
}}
slots={hotbarSlots}
/>
2024-07-12 02:10:22 -05:00
<Entities
camera={camera}
scale={scale}
2024-07-14 07:24:15 -05:00
setChatMessages={setChatMessages}
2024-07-13 17:08:23 -05:00
setMonopolizers={setMonopolizers}
2024-07-12 02:10:22 -05:00
/>
2024-07-14 02:26:43 -05:00
{chatIsOpen && (
<Chat
chatHistory={chatHistory}
2024-07-14 16:33:58 -05:00
chatHistoryCaret={chatHistoryCaret}
chatInputRef={chatInputRef}
2024-07-14 07:24:15 -05:00
chatMessages={chatMessages}
2024-07-14 02:26:43 -05:00
message={message}
2024-07-14 16:33:58 -05:00
pendingMessage={pendingMessage}
2024-07-14 02:26:43 -05:00
setChatHistory={setChatHistory}
2024-07-14 16:33:58 -05:00
setChatHistoryCaret={setChatHistoryCaret}
2024-07-14 02:26:43 -05:00
setMessage={setMessage}
2024-07-14 16:33:58 -05:00
setPendingMessage={setPendingMessage}
2024-07-14 02:26:43 -05:00
onClose={() => {
setChatIsOpen(false);
}}
/>
)}
2024-07-11 16:44:41 -05:00
{showDisconnected && (
<Disconnected />
)}
</Dom>
2024-07-07 23:30:48 -05:00
</div>
<div className={[styles.devtools, devtoolsIsOpen && styles.devtoolsIsOpen].filter(Boolean).join(' ')}>
<Devtools
2024-07-08 14:07:26 -05:00
applyFilters={applyFilters}
2024-07-09 16:00:05 -05:00
eventsChannel={devEventsChannel}
2024-07-08 14:07:26 -05:00
setApplyFilters={setApplyFilters}
2024-07-07 23:30:48 -05:00
/>
</div>
2024-06-10 22:42:30 -05:00
</div>
);
}
2024-07-14 21:07:46 -05:00
export default memo(Ui);