silphius/app/react-components/ui.jsx
2024-07-02 20:43:55 -05:00

302 lines
8.0 KiB
JavaScript

import {useEffect, useState} from 'react';
import addKeyListener from '@/add-key-listener.js';
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 './disconnected.jsx';
import Dom from './dom.jsx';
import HotBar from './hotbar.jsx';
import Pixi from './pixi.jsx';
import styles from './ui.module.css';
const ratio = RESOLUTION.x / RESOLUTION.y;
function emptySlots() {
return Array(10).fill(undefined);
}
export default function Ui({disconnected}) {
// Key input.
const client = useClient();
const [mainEntity, setMainEntity] = useMainEntity();
const [debug, setDebug] = useDebug();
const [ecs] = useEcs();
const [showDisconnected, setShowDisconnected] = useState(false);
const [bufferSlot, setBufferSlot] = useState();
const [hotbarSlots, setHotbarSlots] = useState(emptySlots());
const [activeSlot, setActiveSlot] = useState(0);
const [scale, setScale] = useState(2);
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 '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, setDebug, setScale]);
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
<div
className={styles.ui}
onMouseDown={(event) => {
switch (event.button) {
case 0:
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)},
});
}
}}
>
<style>
{`
@media (max-aspect-ratio: ${ratio}) { .${styles.ui} { width: 100%; } }
@media (min-aspect-ratio: ${ratio}) { .${styles.ui} { height: 100%; } }
.${styles.ui} {
cursor: ${
bufferSlot
? `url('${bufferSlot.icon}'), auto !important`
: 'auto'
};
}
`}
</style>
<Pixi scale={scale} />
{mainEntity && (
<Dom>
<HotBar
active={activeSlot}
onActivate={(i) => {
client.send({
type: 'Action',
payload: {type: 'swapSlots', value: [0, i + 1]},
});
}}
slots={hotbarSlots}
/>
{showDisconnected && (
<Disconnected />
)}
</Dom>
)}
</div>
);
}