487 lines
14 KiB
JavaScript
487 lines
14 KiB
JavaScript
import {memo, useEffect, useRef, useState} from 'react';
|
|
|
|
import addKeyListener from '@/add-key-listener.js';
|
|
import ClientEcs from '@/client-ecs';
|
|
import {RESOLUTION} from '@/constants.js';
|
|
import {useClient, usePacket} from '@/context/client.js';
|
|
import {useDebug} from '@/context/debug.js';
|
|
import {useEcs, useEcsTick} from '@/context/ecs.js';
|
|
import {useMainEntity} from '@/context/main-entity.js';
|
|
|
|
import Disconnected from './dom/disconnected.jsx';
|
|
import Chat from './dom/chat/chat.jsx';
|
|
import Dom from './dom/dom.jsx';
|
|
import Entities from './dom/entities.jsx';
|
|
import HotBar from './dom/hotbar.jsx';
|
|
import Pixi from './pixi/pixi.jsx';
|
|
import Devtools from './devtools.jsx';
|
|
import styles from './ui.module.css';
|
|
|
|
function emptySlots() {
|
|
return Array(10).fill(undefined);
|
|
}
|
|
|
|
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);
|
|
},
|
|
};
|
|
|
|
function Ui({disconnected}) {
|
|
// Key input.
|
|
const client = useClient();
|
|
const chatInputRef = useRef();
|
|
const gameRef = useRef();
|
|
const [mainEntity, setMainEntity] = useMainEntity();
|
|
const [debug, setDebug] = useDebug();
|
|
const [ecs, setEcs] = useEcs();
|
|
const [showDisconnected, setShowDisconnected] = useState(false);
|
|
const [bufferSlot, setBufferSlot] = useState();
|
|
const [devtoolsIsOpen, setDevtoolsIsOpen] = useState(false);
|
|
const ratio = (RESOLUTION.x * (devtoolsIsOpen ? 2 : 1)) / RESOLUTION.y;
|
|
const [camera, setCamera] = useState({x: 0, y: 0});
|
|
const [hotbarSlots, setHotbarSlots] = useState(emptySlots());
|
|
const [activeSlot, setActiveSlot] = useState(0);
|
|
const [scale, setScale] = useState(2);
|
|
const [Components, setComponents] = useState();
|
|
const [Systems, setSystems] = useState();
|
|
const [applyFilters, setApplyFilters] = useState(true);
|
|
const [monopolizers, setMonopolizers] = useState([]);
|
|
const [message, setMessage] = useState('');
|
|
const [chatIsOpen, setChatIsOpen] = useState(false);
|
|
const [chatHistory, setChatHistory] = useState([]);
|
|
const [chatHistoryCaret, setChatHistoryCaret] = useState(-1);
|
|
const [chatMessages, setChatMessages] = useState({});
|
|
const [pendingMessage, setPendingMessage] = useState('');
|
|
useEffect(() => {
|
|
async function setEcsStuff() {
|
|
const {default: Components} = await import('@/ecs-components/index.js');
|
|
const {default: Systems} = await import('@/ecs-systems/index.js');
|
|
setComponents(Components);
|
|
setSystems(Systems);
|
|
}
|
|
setEcsStuff();
|
|
}, []);
|
|
useEffect(() => {
|
|
setEcs(new ClientEcs({Components, Systems}));
|
|
}, [Components, setEcs, Systems]);
|
|
useEffect(() => {
|
|
let handle;
|
|
if (disconnected) {
|
|
handle = setTimeout(() => {
|
|
setShowDisconnected(true);
|
|
}, 1000);
|
|
}
|
|
else {
|
|
setShowDisconnected(false)
|
|
}
|
|
return () => {
|
|
clearTimeout(handle);
|
|
};
|
|
}, [disconnected]);
|
|
useEffect(() => {
|
|
return addKeyListener(document.body, ({event, type, payload}) => {
|
|
if ('Escape' === payload && 'keyDown' === type && chatIsOpen) {
|
|
setChatIsOpen(false);
|
|
return;
|
|
}
|
|
if (chatInputRef.current) {
|
|
chatInputRef.current.focus();
|
|
}
|
|
if (chatIsOpen) {
|
|
return;
|
|
}
|
|
const KEY_MAP = {
|
|
keyDown: 1,
|
|
keyUp: 0,
|
|
};
|
|
let actionPayload;
|
|
switch (payload) {
|
|
case '-':
|
|
if ('keyDown' === type) {
|
|
setScale((scale) => scale > 1 ? scale - 1 : 1);
|
|
}
|
|
break;
|
|
case '=':
|
|
case '+':
|
|
if ('keyDown' === type) {
|
|
setScale((scale) => scale < 4 ? Math.floor(scale + 1) : 4);
|
|
}
|
|
break;
|
|
case 'F3': {
|
|
if (event) {
|
|
event.preventDefault();
|
|
}
|
|
if ('keyDown' === type) {
|
|
setDebug(!debug);
|
|
}
|
|
break;
|
|
}
|
|
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;
|
|
}
|
|
case 'Enter': {
|
|
if ('keyDown' === type) {
|
|
setChatIsOpen(true);
|
|
}
|
|
break
|
|
}
|
|
case 'e': {
|
|
if (KEY_MAP[type]) {
|
|
if (monopolizers.length > 0) {
|
|
monopolizers[0].trigger();
|
|
break;
|
|
}
|
|
}
|
|
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) {
|
|
client.send({
|
|
type: 'Action',
|
|
payload: actionPayload,
|
|
});
|
|
}
|
|
});
|
|
}, [chatIsOpen, client, debug, devtoolsIsOpen, monopolizers, setDebug, setScale]);
|
|
usePacket('EcsChange', async () => {
|
|
setEcs(new ClientEcs({Components, Systems}));
|
|
setMainEntity(undefined);
|
|
setMonopolizers([]);
|
|
}, [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);
|
|
}
|
|
}, [ecs]);
|
|
useEcsTick((payload) => {
|
|
let localMainEntity = mainEntity;
|
|
for (const id in payload) {
|
|
const entity = ecs.get(id);
|
|
const update = payload[id];
|
|
if (!update) {
|
|
continue;
|
|
}
|
|
if (update.Sound?.play) {
|
|
for (const sound of update.Sound.play) {
|
|
(new Audio(sound)).play();
|
|
}
|
|
}
|
|
if (update.MainEntity) {
|
|
setMainEntity(localMainEntity = id);
|
|
}
|
|
if (localMainEntity === id) {
|
|
if (update.Inventory) {
|
|
setBufferSlot(entity.Inventory.item(0));
|
|
const newHotbarSlots = emptySlots();
|
|
for (let i = 1; i < 11; ++i) {
|
|
newHotbarSlots[i - 1] = entity.Inventory.item(i);
|
|
}
|
|
setHotbarSlots(newHotbarSlots);
|
|
}
|
|
if (update.Wielder && 'activeSlot' in update.Wielder) {
|
|
setActiveSlot(update.Wielder.activeSlot);
|
|
}
|
|
}
|
|
}
|
|
if (localMainEntity) {
|
|
const mainEntityEntity = ecs.get(localMainEntity);
|
|
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});
|
|
}
|
|
}
|
|
}, [camera, ecs, mainEntity, scale]);
|
|
useEffect(() => {
|
|
function onContextMenu(event) {
|
|
event.preventDefault();
|
|
}
|
|
document.body.addEventListener('contextmenu', onContextMenu);
|
|
return () => {
|
|
document.body.removeEventListener('contextmenu', onContextMenu);
|
|
};
|
|
}, []);
|
|
return (
|
|
<div
|
|
className={styles.ui}
|
|
>
|
|
<style>
|
|
{`
|
|
@media (max-aspect-ratio: ${ratio}) { .${styles.game} { width: 100%; } }
|
|
@media (min-aspect-ratio: ${ratio}) { .${styles.game} { height: 100%; } }
|
|
.${styles.game} {
|
|
cursor: ${
|
|
bufferSlot
|
|
? `url('${bufferSlot.icon}'), auto !important`
|
|
: 'auto'
|
|
};
|
|
}
|
|
`}
|
|
</style>
|
|
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */ }
|
|
<div
|
|
className={[styles.game, devtoolsIsOpen && styles.devtoolsIsOpen].filter(Boolean).join(' ')}
|
|
onMouseDown={(event) => {
|
|
if (chatIsOpen) {
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
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;
|
|
const client = {
|
|
x: (event.clientX - left) / size,
|
|
y: (event.clientY - top) / size,
|
|
};
|
|
const camera = {
|
|
x: ((Camera.x * scale) - (RESOLUTION.x / 2)),
|
|
y: ((Camera.y * scale) - (RESOLUTION.y / 2)),
|
|
}
|
|
devEventsChannel.invoke(
|
|
'click',
|
|
{
|
|
x: (client.x + camera.x) / scale,
|
|
y: (client.y + camera.y) / scale,
|
|
},
|
|
);
|
|
}
|
|
else {
|
|
client.send({
|
|
type: 'Action',
|
|
payload: {type: 'use', value: 1},
|
|
});
|
|
}
|
|
break;
|
|
case 2:
|
|
if (monopolizers.length > 0) {
|
|
monopolizers[0].trigger();
|
|
break;
|
|
}
|
|
client.send({
|
|
type: 'Action',
|
|
payload: {type: 'interact', value: 1},
|
|
});
|
|
break;
|
|
}
|
|
}}
|
|
onMouseUp={(event) => {
|
|
if (chatIsOpen) {
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
switch (event.button) {
|
|
case 0:
|
|
client.send({
|
|
type: 'Action',
|
|
payload: {type: 'use', value: 0},
|
|
});
|
|
break;
|
|
case 2:
|
|
client.send({
|
|
type: 'Action',
|
|
payload: {type: 'interact', value: 0},
|
|
});
|
|
break;
|
|
}
|
|
event.preventDefault();
|
|
}}
|
|
onWheel={(event) => {
|
|
if (chatIsOpen) {
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
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}
|
|
>
|
|
<Pixi
|
|
applyFilters={applyFilters}
|
|
camera={camera}
|
|
monopolizers={monopolizers}
|
|
scale={scale}
|
|
/>
|
|
<Dom>
|
|
<HotBar
|
|
active={activeSlot}
|
|
onActivate={(i) => {
|
|
client.send({
|
|
type: 'Action',
|
|
payload: {type: 'swapSlots', value: [0, i + 1]},
|
|
});
|
|
}}
|
|
slots={hotbarSlots}
|
|
/>
|
|
<Entities
|
|
camera={camera}
|
|
scale={scale}
|
|
setChatMessages={setChatMessages}
|
|
setMonopolizers={setMonopolizers}
|
|
/>
|
|
{chatIsOpen && (
|
|
<Chat
|
|
chatHistory={chatHistory}
|
|
chatHistoryCaret={chatHistoryCaret}
|
|
chatInputRef={chatInputRef}
|
|
chatMessages={chatMessages}
|
|
message={message}
|
|
pendingMessage={pendingMessage}
|
|
setChatHistory={setChatHistory}
|
|
setChatHistoryCaret={setChatHistoryCaret}
|
|
setMessage={setMessage}
|
|
setPendingMessage={setPendingMessage}
|
|
onClose={() => {
|
|
setChatIsOpen(false);
|
|
}}
|
|
/>
|
|
)}
|
|
{showDisconnected && (
|
|
<Disconnected />
|
|
)}
|
|
</Dom>
|
|
</div>
|
|
<div className={[styles.devtools, devtoolsIsOpen && styles.devtoolsIsOpen].filter(Boolean).join(' ')}>
|
|
<Devtools
|
|
applyFilters={applyFilters}
|
|
eventsChannel={devEventsChannel}
|
|
setApplyFilters={setApplyFilters}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default memo(Ui);
|