Compare commits

...

5 Commits

Author SHA1 Message Date
cha0s
f9f3abf4ef feat: admin painting 2024-07-07 23:30:48 -05:00
cha0s
7cd05faddf refactor: all hulls 2024-07-07 23:28:17 -05:00
cha0s
65c23b50d9 refactor: box-sizing 2024-07-07 23:25:07 -05:00
cha0s
520b255d5d dev: less choppy for now 2024-07-07 17:40:40 -05:00
cha0s
2d0a3a9794 dev: light please 2024-07-07 17:40:30 -05:00
13 changed files with 382 additions and 103 deletions

View File

@ -4,6 +4,7 @@ import {floodwalk2D, ortho, removeCollinear} from '@/util/math.js';
import vector2d from './helpers/vector-2d';
class LayerProxy {
$$sourceJson;
constructor(instance, Component, index) {
this.instance = instance;
this.Component = Component;
@ -79,9 +80,15 @@ class LayerProxy {
get layer() {
return this.instance.layers[this.index];
}
async load() {
this.$$sourceJson = await this.Component.ecs.readJson(this.layer.source);
}
get source() {
return this.layer.source;
}
get sourceJson() {
return this.$$sourceJson;
}
stamp(at, data) {
const changes = {};
for (const row in data) {
@ -112,7 +119,7 @@ class LayerProxy {
}
export default class TileLayers extends Component {
insertMany(entities) {
async insertMany(entities) {
for (const [id, {layerChange}] of entities) {
if (layerChange) {
const component = this.get(id);
@ -124,14 +131,24 @@ export default class TileLayers extends Component {
}
layers[layerIndex] = {...layers[layerIndex]};
component.$$layersProxies[layerIndex] = new LayerProxy(component, this, layerIndex);
await component.$$layersProxies[layerIndex].load();
}
}
}
return super.insertMany(entities);
}
load(instance) {
instanceFromSchema() {
return class TileLayersInstance extends super.instanceFromSchema() {
$$layersProxies = {};
layer(index) {
return this.$$layersProxies[index];
}
}
}
async load(instance) {
for (const index in instance.layers) {
instance.$$layersProxies[index] = new LayerProxy(instance, this, index);
await instance.$$layersProxies[index].load();
}
}
mergeDiff(original, update) {
@ -149,14 +166,6 @@ export default class TileLayers extends Component {
}
return {layerChange};
}
instanceFromSchema() {
return class TileLayersInstance extends super.instanceFromSchema() {
$$layersProxies = {};
layer(index) {
return this.$$layersProxies[index];
}
}
}
static properties = {
layers: {
type: 'array',

View File

@ -18,6 +18,6 @@ export default class Time extends Component {
};
}
static properties = {
irlSeconds: {defaultValue: 18 * realSecondsPerGameHour, type: 'uint16'},
irlSeconds: {defaultValue: 10 * realSecondsPerGameHour, type: 'uint16'},
};
}

View File

@ -94,6 +94,13 @@ export default class Engine {
}
this.incomingActions.get(connection).push(payload);
});
this.server.addPacketListener('AdminAction', (connection, payload) => {
// check...
if (!this.incomingActions.has(connection)) {
this.incomingActions.set(connection, []);
}
this.incomingActions.get(connection).push(payload);
});
}
acceptActions() {
@ -108,6 +115,14 @@ export default class Engine {
const {Controlled, Ecs, Interacts, Inventory, Wielder} = entity;
for (const payload of payloads) {
switch (payload.type) {
case 'paint': {
const ecs = this.ecses[Ecs.path];
const {TileLayers} = ecs.get(1);
const {brush, layer: paintLayer, stamp} = payload.value;
const layer = TileLayers.layer(paintLayer);
layer.stamp(stamp.at, stamp.data)
break;
}
case 'changeSlot': {
if (!Controlled.locked) {
Wielder.activeSlot = payload.value - 1;

View File

@ -80,26 +80,26 @@ if (import.meta.hot) {
const resolver = createResolver();
resolvers.push(resolver);
await beforeResolver;
const oldBuffer = await engine.server.readData('players/0');
const oldPlayer = JSON.parse((new TextDecoder()).decode(oldBuffer));
const buffer = await createPlayer(0);
const player = JSON.parse((new TextDecoder()).decode(buffer));
// Less jarring
player.Ecs = oldPlayer.Ecs;
player.Direction = oldPlayer.Direction;
player.Position = oldPlayer.Position;
await engine.server.writeData('players/0', (new TextEncoder()).encode(JSON.stringify(player)));
// const oldBuffer = await engine.server.readData('players/0');
// const oldPlayer = JSON.parse((new TextDecoder()).decode(oldBuffer));
// const buffer = await createPlayer(0);
// const player = JSON.parse((new TextDecoder()).decode(buffer));
// // Less jarring
// player.Ecs = oldPlayer.Ecs;
// player.Direction = oldPlayer.Direction;
// player.Position = oldPlayer.Position;
// await engine.server.writeData('players/0', (new TextEncoder()).encode(JSON.stringify(player)));
resolver.resolve();
});
import.meta.hot.accept('../../create-homestead.js', async ({default: createHomestead}) => {
const resolver = createResolver();
resolvers.push(resolver);
await beforeResolver;
delete engine.ecses['homesteads/0'];
await engine.server.removeData('homesteads/0');
const homestead = await createHomestead(engine.Ecs);
homestead.get(2).Ecs.path = 'houses/0';
await engine.saveEcs('homesteads/0', homestead);
// delete engine.ecses['homesteads/0'];
// await engine.server.removeData('homesteads/0');
// const homestead = await createHomestead(engine.Ecs);
// homestead.get(2).Ecs.path = 'houses/0';
// await engine.saveEcs('homesteads/0', homestead);
resolver.resolve();
});
import.meta.hot.on('vite:afterUpdate', async () => {

View File

@ -0,0 +1,3 @@
import Packet from '@/net/packet.js';
export default class AdminAction extends Packet {}

View File

@ -0,0 +1,134 @@
import {useRef, useState} from 'react';
import {useEcs} from '@/context/ecs.js';
import styles from './devtools.module.css';
export default function Devtools({
brush,
layer,
setBrush,
setLayer,
setStamp,
}) {
const offsetRef = useRef();
const [selection, setSelection] = useState({x: 0, y: 0, w: 2, h: 2});
const [moveStart, setMoveStart] = useState();
const [ecs] = useEcs();
if (!ecs) {
return false;
}
const master = ecs.get(1);
if (!master) {
return false;
}
const {TileLayers} = master;
const {sourceJson, tileSize} = TileLayers.layer(0);
const {w, h} = sourceJson.meta.size;
return (
<div className={styles.devtools}>
<form>
<div className={styles.topBar}>
<div className={styles.layer}>
<label>
Layer:
<select
onChange={(event) => {
setLayer(event.target.value)
}}
value={layer}
>
{
Object.keys(TileLayers.layers)
.map((layer, i) => (
<option
key={i}
value={i}
>
{i}
</option>
))
}
</select>
</label>
</div>
<div className={styles.brush}>
<label>
Brush:
<select
onChange={(event) => {
setLayer(event.target.value)
}}
value={brush}
>
<option>Paint</option>
</select>
</label>
</div>
</div>
</form>
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
<div
onMouseDown={(event) => {
if (!offsetRef.current) {
return;
}
const {left, top} = offsetRef.current.getBoundingClientRect();
const x = Math.floor((event.clientX - left) / tileSize.x);
const y = Math.floor((event.clientY - top) / tileSize.y);
setMoveStart({x, y});
setSelection({x, y, w: 1, h: 1});
}}
onMouseMove={(event) => {
if (!offsetRef.current) {
return;
}
if (!moveStart) {
return;
}
const {x: sx, y: sy} = moveStart;
const {left, top} = offsetRef.current.getBoundingClientRect();
const x = Math.floor(
Math.max(0, Math.min(w - 1, (event.clientX - left)) / tileSize.x),
);
const y = Math.floor(
Math.max(0, Math.min(h - 1, (event.clientY - top)) / tileSize.y),
);
const mx = Math.min(sx, x);
const my = Math.min(sy, y);
setSelection({x: mx, y: my, w: Math.abs(sx - x) + 1, h: Math.abs(sy - y) + 1});
}}
onMouseUp={() => {
setMoveStart();
const stamp = [];
const {x, y, w: sw, h: sh} = selection;
const tw = w / tileSize.x;
for (let iy = 0; iy < sh; ++iy) {
const row = [];
for (let ix = 0; ix < sw; ++ix) {
row.push((y + iy) * tw + x + ix);
}
stamp.push(row);
}
setStamp(stamp);
}}
className={styles.selectionWrapper}
ref={offsetRef}
>
<div
className={styles.selection}
style={{
top: selection.y * tileSize.y,
left: selection.x * tileSize.x,
height: selection.h * tileSize.x,
width: selection.w * tileSize.y,
}}
/>
<img
alt="tileset"
src={TileLayers.layer(0).source.replace('.json', '.png')}
/>
</div>
</div>
);
}

View File

@ -0,0 +1,30 @@
.topBar {
display: flex;
gap: 16px;
margin-bottom: 16px;
}
.devtools {
background-color: #444444;
color: white;
max-height: 100%;
overflow-y: auto;
padding: 16px;
}
.devtools p {
text-align: center;
}
.selectionWrapper {
position: relative;
}
.selectionWrapper img {
user-select: none;
}
.selection {
background-color: #ffffff44;
position: absolute;
}

View File

@ -2,6 +2,7 @@
display: flex;
flex-direction: column;
height: calc(100% / var(--scale));
left: 0;
position: absolute;
top: 0;
transform: scale(var(--scale));

View File

@ -44,10 +44,6 @@ function createLayerMask(layer) {
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'rgba(255, 255, 255, 1)';
for (let i = 0; i < hulls.length; ++i) {
const {x, y} = hulls[i][0];
if (7 !== layer.tile({x: Math.floor(x / tileSize.x), y: Math.floor(y / tileSize.y)})) {
continue;
}
const hull = [...hulls[i]];
hull.push(hull[0]);
ctx.beginPath();

View File

@ -21,10 +21,10 @@
background-position: center;
background-repeat: no-repeat;
background-size: 75%;
height: calc(100% - var(--space) * 2);
height: 100%;
padding: var(--space);
position: relative;
width: calc(100% - var(--space) * 2);
width: 100%;
}
.qty {

View File

@ -1,4 +1,4 @@
import {useEffect, useState} from 'react';
import {useEffect, useRef, useState} from 'react';
import addKeyListener from '@/add-key-listener.js';
import ClientEcs from '@/client-ecs';
@ -8,14 +8,13 @@ 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';
const ratio = RESOLUTION.x / RESOLUTION.y;
function emptySlots() {
return Array(10).fill(undefined);
}
@ -23,16 +22,22 @@ function emptySlots() {
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 [layer, setLayer] = useState(0);
const [brush, setBrush] = useState(0);
const [stamp, setStamp] = useState([]);
useEffect(() => {
async function setEcsStuff() {
const {default: Components} = await import('@/ecs-components/index.js');
@ -87,6 +92,15 @@ export default function Ui({disconnected}) {
}
break;
}
case 'F4': {
if (event) {
event.preventDefault();
}
if ('keyDown' === type) {
setDevtoolsIsOpen(!devtoolsIsOpen);
}
break;
}
case 'w': {
actionPayload = {type: 'moveUp', value: KEY_MAP[type]};
break;
@ -179,7 +193,7 @@ export default function Ui({disconnected}) {
});
}
});
}, [client, debug, setDebug, setScale]);
}, [client, debug, devtoolsIsOpen, setDebug, setScale]);
usePacket('EcsChange', async () => {
setMainEntity(undefined);
setEcs(new ClientEcs({Components, Systems}));
@ -231,63 +245,14 @@ export default function Ui({disconnected}) {
};
}, [])
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} {
@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`
@ -296,24 +261,131 @@ export default function Ui({disconnected}) {
}
`}
</style>
<Pixi scale={scale} />
{mainEntity && (
<Dom>
<HotBar
active={activeSlot}
onActivate={(i) => {
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */ }
<div
className={[styles.game, devtoolsIsOpen && styles.devtoolsIsOpen].filter(Boolean).join(' ')}
onMouseDown={(event) => {
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 {TileLayers} = master;
const {area, tileSize} = TileLayers.layer(0);
const size = width / RESOLUTION.x;
const cr = {
x: (event.clientX - left) / size,
y: (event.clientY - top) / size,
};
const cm = {
x: ((Camera.x * scale) - (RESOLUTION.x / 2)),
y: ((Camera.y * scale) - (RESOLUTION.y / 2)),
}
const at = {
x: Math.floor((cr.x + cm.x) / (tileSize.x * scale)),
y: Math.floor((cr.y + cm.y) / (tileSize.y * scale)),
};
if (at.x < 0 || at.y < 0 || at.x >= area.x || at.y >= area.y) {
return;
}
const payload = {
brush,
layer,
stamp: {
at,
data: stamp,
},
}
client.send({
type: 'AdminAction',
payload: {type: 'paint', value: payload},
});
}
else {
client.send({
type: 'Action',
payload: {type: 'use', value: 1},
});
}
break;
case 2:
client.send({
type: 'Action',
payload: {type: 'swapSlots', value: [0, i + 1]},
payload: {type: 'interact', value: 1},
});
}}
slots={hotbarSlots}
/>
{showDisconnected && (
<Disconnected />
)}
</Dom>
)}
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}
>
<Pixi scale={scale} />
{mainEntity && (
<Dom devtoolsIsOpen={devtoolsIsOpen}>
<HotBar
active={activeSlot}
onActivate={(i) => {
client.send({
type: 'Action',
payload: {type: 'swapSlots', value: [0, i + 1]},
});
}}
slots={hotbarSlots}
/>
{showDisconnected && (
<Disconnected />
)}
</Dom>
)}
</div>
<div className={[styles.devtools, devtoolsIsOpen && styles.devtoolsIsOpen].filter(Boolean).join(' ')}>
<Devtools
brush={brush}
layer={layer}
stamp={stamp}
setBrush={setBrush}
setLayer={setLayer}
setStamp={setStamp}
/>
</div>
</div>
);
}

View File

@ -1,5 +1,23 @@
.ui {
.devtools {
display: none;
height: 100%;
&.devtoolsIsOpen {
display: block;
width: 50%;
}
}
.game {
align-self: center;
line-height: 0;
position: relative;
&.devtoolsIsOpen {
width: 50%;
}
}
.ui {
display: flex;
line-height: 0;
position: relative;
}

View File

@ -6,6 +6,7 @@ html, body {
margin: 0;
width: 100%;
* {
box-sizing: border-box;
line-height: 1;
}
}