diff --git a/app/ecs-components/tile-layers.js b/app/ecs-components/tile-layers.js
index 9235d92..77d1167 100644
--- a/app/ecs-components/tile-layers.js
+++ b/app/ecs-components/tile-layers.js
@@ -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',
diff --git a/app/engine.js b/app/engine.js
index 5c5ed85..5f176a2 100644
--- a/app/engine.js
+++ b/app/engine.js
@@ -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;
diff --git a/app/packets/admin-action.js b/app/packets/admin-action.js
new file mode 100644
index 0000000..2ff065f
--- /dev/null
+++ b/app/packets/admin-action.js
@@ -0,0 +1,3 @@
+import Packet from '@/net/packet.js';
+
+export default class AdminAction extends Packet {}
diff --git a/app/react-components/devtools.jsx b/app/react-components/devtools.jsx
new file mode 100644
index 0000000..01e9ccf
--- /dev/null
+++ b/app/react-components/devtools.jsx
@@ -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 (
+
+
+ {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
+
{
+ 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}
+ >
+
+
.source.replace('.json',)
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/react-components/devtools.module.css b/app/react-components/devtools.module.css
new file mode 100644
index 0000000..a362cbe
--- /dev/null
+++ b/app/react-components/devtools.module.css
@@ -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;
+}
diff --git a/app/react-components/dom.module.css b/app/react-components/dom.module.css
index ce71e48..8fd45a4 100644
--- a/app/react-components/dom.module.css
+++ b/app/react-components/dom.module.css
@@ -2,6 +2,7 @@
display: flex;
flex-direction: column;
height: calc(100% / var(--scale));
+ left: 0;
position: absolute;
top: 0;
transform: scale(var(--scale));
diff --git a/app/react-components/ui.jsx b/app/react-components/ui.jsx
index 2c57712..516d8f9 100644
--- a/app/react-components/ui.jsx
+++ b/app/react-components/ui.jsx
@@ -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
{
- 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)},
- });
- }
- }}
>
-
- {mainEntity && (
-
- {
+ {/* 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 {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 && (
-
- )}
-
- )}
+ 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 && (
+
+ )}
+
+ )}
+
+
+
+
);
}
diff --git a/app/react-components/ui.module.css b/app/react-components/ui.module.css
index 5aadf30..3994edd 100644
--- a/app/react-components/ui.module.css
+++ b/app/react-components/ui.module.css
@@ -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;
}