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} + > +
+ tileset +
+
+ ); +} \ 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; }