flow: main entity, actions, active slot

This commit is contained in:
cha0s 2024-06-21 04:50:17 -05:00
parent 5f139f8960
commit 36607d3f12
12 changed files with 196 additions and 44 deletions

View File

@ -10,10 +10,3 @@ export const RESOLUTION = {
export const SERVER_LATENCY = 0;
export const TPS = 60;
export const ACTION_MAP = {
w: 'moveUp',
d: 'moveRight',
s: 'moveDown',
a: 'moveLeft',
};

View File

@ -0,0 +1,10 @@
import {createContext, useContext} from 'react';
const context = createContext();
export default context;
export function useMainEntity() {
return useContext(context);
}

View File

@ -3,4 +3,5 @@ export default {
moveRight: {type: 'float32'},
moveDown: {type: 'float32'},
moveLeft: {type: 'float32'},
changeSlot: {type: 'int8'},
};

View File

@ -0,0 +1,3 @@
export default {
activeSlot: {type: 'uint16'},
};

View File

@ -0,0 +1,17 @@
import {System} from '@/ecs/index.js';
export default class ApplyControlMovement extends System {
tick() {
const {diff} = this.ecs;
for (const id in diff) {
if ('changeSlot' in (diff[id].Controlled ?? {})) {
if (diff[id].Controlled.changeSlot > 0) {
this.ecs.get(id).Wielder.activeSlot = diff[id].Controlled.changeSlot - 1;
}
}
}
}
}

View File

@ -81,6 +81,7 @@ export default class Engine {
},
});
const defaultSystems = [
'ApplyControlItem',
'ApplyControlMovement',
'ApplyMomentum',
'ClampPositions',
@ -104,7 +105,7 @@ export default class Engine {
async createPlayer(id) {
const player = {
Camera: {},
Controlled: {moveUp: 0, moveRight: 0, moveDown: 0, moveLeft: 0},
Controlled: {},
Direction: {direction: 2},
Ecs: {path: join('homesteads', `${id}`)},
Momentum: {},
@ -118,6 +119,9 @@ export default class Engine {
source: '/assets/dude.json',
speed: 0.115,
},
Wielder: {
activeSlot: 0,
},
};
const buffer = (new TextEncoder()).encode(JSON.stringify(player));
await this.server.writeData(

View File

@ -5,6 +5,8 @@ const WIRE_MAP = {
'moveRight': 1,
'moveDown': 2,
'moveLeft': 3,
'use': 4,
'changeSlot': 5,
};
Object.entries(WIRE_MAP)
.forEach(([k, v]) => {

View File

@ -2,6 +2,7 @@ import {Container} from '@pixi/react';
import {useState} from 'react';
import {RESOLUTION} from '@/constants.js';
import {useMainEntity} from '@/context/main-entity.js';
import Ecs from '@/ecs/ecs.js';
import Components from '@/ecs-components/index.js';
import Systems from '@/ecs-systems/index.js';
@ -14,7 +15,7 @@ import TileLayer from './tile-layer.jsx';
export default function EcsComponent() {
const [ecs] = useState(new Ecs({Components, Systems}));
const [entities, setEntities] = useState({});
const [mainEntity, setMainEntity] = useState();
const [mainEntity] = useMainEntity();
usePacket('Tick', (payload) => {
if (0 === Object.keys(payload.ecs).length) {
return;
@ -27,9 +28,6 @@ export default function EcsComponent() {
}
else {
updatedEntities[id] = ecs.get(id);
if (updatedEntities[id].MainEntity) {
setMainEntity(ecs.get(id));
}
}
}
setEntities(updatedEntities);
@ -37,7 +35,7 @@ export default function EcsComponent() {
if (!mainEntity) {
return false;
}
const {Camera} = mainEntity;
const {Camera, Position} = ecs.get(mainEntity);
const {TileLayers} = ecs.get(1);
const [cx, cy] = [
Math.round(Camera.x - RESOLUTION.x / 2),
@ -56,8 +54,8 @@ export default function EcsComponent() {
y={-cy}
/>
<TargetingGhost
px={mainEntity.Position.x}
py={mainEntity.Position.y}
px={Position.x}
py={Position.y}
cx={cx}
cy={cy}
/>

View File

@ -27,13 +27,14 @@ export default function Entities({entities, x, y}) {
continue;
}
renderables.push(
<>
<Container
key={id}
>
<Sprite
entity={entity}
key={id}
/>
<Crosshair x={entity.Position.x} y={entity.Position.y} />
</>
</Container>
);
}
return (

View File

@ -3,29 +3,37 @@ import {
} from '@pixi/react';
import {SCALE_MODES} from '@pixi/constants';
import {BaseTexture} from '@pixi/core';
import {createElement, useContext} from 'react';
import {RESOLUTION} from '@/constants.js';
import ClientContext from '@/context/client.js';
import MainEntityContext from '@/context/main-entity.js';
import Ecs from './ecs.jsx';
import styles from './pixi.module.css';
BaseTexture.defaultOptions.scaleMode = SCALE_MODES.NEAREST;
const ContextBridge = ({ children, Context, render }) => {
return (
<Context.Consumer>
{(value) =>
render(<Context.Provider value={value}>{children}</Context.Provider>)
const ContextBridge = ({children, Contexts, render}) => {
const contexts = Contexts.map(useContext);
return render(
<>
{
Contexts.reduce(
(children, Context, i) => ([
createElement(Context.Provider, {value: contexts[i], key: Context}, children)
]),
children,
)
}
</Context.Consumer>
</>
);
};
export const Stage = ({children, ...props}) => {
return (
<ContextBridge
Context={ClientContext}
Contexts={[ClientContext, MainEntityContext]}
render={(children) => <PixiStage {...props}>{children}</PixiStage>}
>
{children}

View File

@ -1,8 +1,10 @@
import {useContext, useEffect, useState} from 'react';
import addKeyListener from '@/add-key-listener.js';
import {ACTION_MAP, RESOLUTION} from '@/constants.js';
import {RESOLUTION} from '@/constants.js';
import ClientContext from '@/context/client.js';
import {useMainEntity} from '@/context/main-entity.js';
import usePacket from '@/hooks/use-packet.js';
import Disconnected from './disconnected.jsx';
import Dom from './dom.jsx';
@ -12,21 +14,18 @@ import styles from './ui.module.css';
const ratio = RESOLUTION.x / RESOLUTION.y;
const KEY_MAP = {
keyDown: 1,
keyUp: 0,
};
export default function Ui({disconnected}) {
// Key input.
const client = useContext(ClientContext);
const [mainEntity, setMainEntity] = useMainEntity();
const [showDisconnected, setShowDisconnected] = useState(false);
const [activeSlot, setActiveSlot] = useState(0);
useEffect(() => {
let handle;
if (disconnected) {
handle = setTimeout(() => {
setShowDisconnected(true);
}, 400);
}, 1000);
}
else {
setShowDisconnected(false)
@ -37,17 +36,117 @@ export default function Ui({disconnected}) {
}, [disconnected]);
useEffect(() => {
return addKeyListener(document.body, ({type, payload}) => {
if (type in KEY_MAP && payload in ACTION_MAP) {
const KEY_MAP = {
keyDown: 1,
keyUp: 0,
};
let actionPayload;
switch (payload) {
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 '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: {
type: ACTION_MAP[payload],
value: KEY_MAP[type],
},
payload: actionPayload,
});
}
});
});
usePacket('Tick', (payload) => {
if (0 === Object.keys(payload.ecs).length) {
return;
}
let localMainEntity = mainEntity;
for (const id in payload.ecs) {
if (payload.ecs[id]?.MainEntity) {
setMainEntity(localMainEntity = id);
}
if (localMainEntity === id) {
if (payload.ecs[id].Wielder && 'activeSlot' in payload.ecs[id].Wielder) {
setActiveSlot(payload.ecs[id].Wielder.activeSlot);
}
}
}
}, [mainEntity, setMainEntity]);
return (
<div className={styles.ui}>
<style>
@ -57,12 +156,23 @@ export default function Ui({disconnected}) {
`}
</style>
<Pixi />
<Dom>
<HotBar active={1} slots={Array(10).fill(0).map(() => {})} />
{showDisconnected && (
<Disconnected />
)}
</Dom>
{mainEntity && (
<Dom>
<HotBar
active={activeSlot}
onActivate={(i) => {
client.send({
type: 'Action',
payload: {type: 'changeSlot', value: i + 1},
});
}}
slots={Array(10).fill(0).map(() => {})}
/>
{showDisconnected && (
<Disconnected />
)}
</Dom>
)}
</div>
);
}

View File

@ -3,6 +3,8 @@ import {useEffect, useState} from 'react';
import {useOutletContext, useParams} from 'react-router-dom';
import ClientContext from '@/context/client.js';
import MainEntityContext from '@/context/main-entity.js';
import Ui from '@/react-components/ui.jsx';
import {juggleSession} from '@/session.server';
@ -14,6 +16,7 @@ export async function loader({request}) {
export default function PlaySpecific() {
const Client = useOutletContext();
const [client, setClient] = useState();
const mainEntityTuple = useState();
const [disconnected, setDisconnected] = useState(false);
const params = useParams();
const [type, url] = params['*'].split('/');
@ -84,7 +87,9 @@ export default function PlaySpecific() {
}, [client, disconnected, url]);
return (
<ClientContext.Provider value={client}>
<Ui disconnected={disconnected} />
<MainEntityContext.Provider value={mainEntityTuple}>
<Ui disconnected={disconnected} />
</MainEntityContext.Provider>
</ClientContext.Provider>
);
}