import {memo, useEffect, useRef, useState} from 'react'; 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'; import {RESOLUTION} from '@/util/constants.js'; import EventEmitter from '@/util/event-emitter.js'; import addKeyListener from './add-key-listener.js'; import ClientEcs from './client-ecs.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 = new EventEmitter(); 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); client.emitter.invoke(':Ecs', 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 (
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */ }
{ 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} > { client.send({ type: 'Action', payload: {type: 'swapSlots', value: [0, i + 1]}, }); }} slots={hotbarSlots} /> {chatIsOpen && ( { setChatIsOpen(false); }} /> )} {showDisconnected && ( )}
); } export default memo(Ui);