import {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 Devtools from './devtools.jsx'; import Disconnected from './disconnected.jsx'; import Dom from './dom.jsx'; import HotBar from './hotbar.jsx'; import Pixi from './pixi.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); }, }; export default function Ui({disconnected}) { // Key input. const client = useClient(); 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 [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); 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}) => { const KEY_MAP = { keyDown: 1, keyUp: 0, }; let actionPayload; switch (payload) { case '-': if ('keyDown' === type) { setScale((scale) => scale > 1 ? scale - 1 : 0.666); } 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 'e': { 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, }); } }); }, [client, debug, devtoolsIsOpen, setDebug, setScale]); usePacket('EcsChange', async () => { setMainEntity(undefined); setEcs(new ClientEcs({Components, Systems})); }, [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.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); } } } }, [ecs, mainEntity]); 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 */ }
{ 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: client.send({ type: 'Action', payload: {type: 'interact', value: 1}, }); break; } event.preventDefault(); }} onMouseUp={(event) => { 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 (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} > {mainEntity && ( { client.send({ type: 'Action', payload: {type: 'swapSlots', value: [0, i + 1]}, }); }} slots={hotbarSlots} /> {showDisconnected && ( )} )}
); }