Compare commits

..

18 Commits

Author SHA1 Message Date
cha0s
92a77d8046 fun: basic dialogue 2024-07-12 02:10:22 -05:00
cha0s
78060ded37 chore: tidy 2024-07-12 01:53:15 -05:00
cha0s
7141d20a94 refactor: dom 2024-07-12 01:52:43 -05:00
cha0s
bb0c5ab1e9 refactor: pixi 2024-07-12 01:29:54 -05:00
cha0s
d086f802f8 feat: DOM scale context 2024-07-12 00:00:48 -05:00
cha0s
375b83f366 fix: hide DOM overflow 2024-07-12 00:00:27 -05:00
cha0s
34cd695a80 refactor: camera propagation 2024-07-12 00:00:03 -05:00
cha0s
71c4f1f959 ui: user selection 2024-07-11 17:20:33 -05:00
cha0s
80a2833842 dev: fix implicit hmr synchronization 2024-07-11 17:05:54 -05:00
cha0s
dfc7665856 refactor: tidy 2024-07-11 16:44:53 -05:00
cha0s
ece20dfcd7 fun: chest 2024-07-11 16:04:12 -05:00
cha0s
dcf6fff66d refactor: better gen 2024-07-11 16:01:16 -05:00
cha0s
a8d345eebe refactor: script name and defaults 2024-07-11 15:43:31 -05:00
cha0s
ef744591d2 chore: tidy 2024-07-11 15:43:17 -05:00
cha0s
e2c62a6522 refactor: script API 2024-07-11 15:43:08 -05:00
cha0s
a208863823 perf: entity rendering 2024-07-11 03:09:28 -05:00
cha0s
354b013e70 fix: water 2024-07-11 03:09:15 -05:00
cha0s
14c15bbf4a refactor: no mask 2024-07-11 03:08:39 -05:00
45 changed files with 525 additions and 184 deletions

View File

@ -41,10 +41,22 @@ export default class ClientEcs extends Ecs {
} }
return cache.get(key); return cache.get(key);
} }
async readScript(uri, context = {}) { async readScript(uriOrCode, context = {}) {
const code = await this.readAsset(uri); if (!uriOrCode) {
if (code.byteLength > 0) { return undefined;
return Script.fromCode((new TextDecoder()).decode(code), context); }
let code = '';
if (!uriOrCode.startsWith('/')) {
code = uriOrCode;
}
else {
const buffer = await this.readAsset(uriOrCode);
if (buffer.byteLength > 0) {
code = (new TextDecoder()).decode(buffer);
}
}
if (code) {
return Script.fromCode(code, context);
} }
} }
} }

9
app/context/dom-scale.js Normal file
View File

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

View File

@ -3,8 +3,6 @@ import {createNoise2D} from 'simplex-noise';
import {createRandom, Generator} from '@/util/math.js'; import {createRandom, Generator} from '@/util/math.js';
import createEcs from './create-ecs.js';
const seed = 42069; const seed = 42069;
const prng = alea(seed); const prng = alea(seed);
const rawNoise = createNoise2D(prng); const rawNoise = createNoise2D(prng);
@ -54,10 +52,10 @@ const Forest = new Generator({
], ],
}); });
export default async function createForest(Ecs) { export default async function createForest() {
const ecs = createEcs(Ecs);
const area = {x: w, y: h}; const area = {x: w, y: h};
const master = ecs.get(await ecs.create({ const entities = [];
entities.push({
AreaSize: {x: area.x * 16, y: area.y * 16}, AreaSize: {x: area.x * 16, y: area.y * 16},
Ticking: {}, Ticking: {},
TileLayers: { TileLayers: {
@ -77,8 +75,10 @@ export default async function createForest(Ecs) {
], ],
}, },
Time: {}, Time: {},
})); Water: {},
});
Forest.generate(); Forest.generate();
const master = entities[0];
const layer0 = master.TileLayers.layers[0]; const layer0 = master.TileLayers.layers[0];
layer0.data = Forest.matrix; layer0.data = Forest.matrix;
for (let y = 0; y < h; ++y) { for (let y = 0; y < h; ++y) {
@ -109,7 +109,7 @@ export default async function createForest(Ecs) {
if (v > 0.2) { if (v > 0.2) {
v = noise(x * (1 / 7), y * (1 / 7)); v = noise(x * (1 / 7), y * (1 / 7));
if (v < 0.15) { if (v < 0.15) {
await ecs.create({ entities.push({
Position: entityPosition(x, y), Position: entityPosition(x, y),
Sprite: { Sprite: {
anchorY: 0.7, anchorY: 0.7,
@ -119,7 +119,7 @@ export default async function createForest(Ecs) {
}); });
} }
else if (v < 0.17) { else if (v < 0.17) {
await ecs.create({ entities.push({
Position: entityPosition(x, y), Position: entityPosition(x, y),
Sprite: { Sprite: {
anchorY: 0.875, anchorY: 0.875,
@ -130,7 +130,7 @@ export default async function createForest(Ecs) {
} }
v = noise(x * (1 / 12), y * (1 / 12)); v = noise(x * (1 / 12), y * (1 / 12));
if (v < 0.08) { if (v < 0.08) {
await ecs.create({ entities.push({
Position: entityPosition(x, y), Position: entityPosition(x, y),
Sprite: { Sprite: {
anchorY: 0.7, anchorY: 0.7,
@ -142,6 +142,6 @@ export default async function createForest(Ecs) {
} }
} }
} }
return ecs; return entities;
} }

View File

@ -1,9 +1,7 @@
import createEcs from './create-ecs.js'; export default async function createHomestead(id) {
export default async function createHomestead(Ecs, id) {
const ecs = createEcs(Ecs);
const area = {x: 100, y: 60}; const area = {x: 100, y: 60};
await ecs.create({ const entities = [];
entities.push({
AreaSize: {x: area.x * 16, y: area.y * 16}, AreaSize: {x: area.x * 16, y: area.y * 16},
Ticking: {}, Ticking: {},
TileLayers: { TileLayers: {
@ -25,7 +23,7 @@ export default async function createHomestead(Ecs, id) {
Time: {}, Time: {},
Water: {water: {}}, Water: {water: {}},
}); });
await ecs.create({ entities.push({
Collider: { Collider: {
bodies: [ bodies: [
{ {
@ -61,5 +59,39 @@ export default async function createHomestead(Ecs, id) {
Ticking: {}, Ticking: {},
VisibleAabb: {}, VisibleAabb: {},
}); });
return ecs; entities.push({
Collider: {
bodies: [
{
impassable: 1,
points: [
{x: -11, y: -7},
{x: 10, y: -7},
{x: 10, y: 5},
{x: -11, y: 5},
],
},
],
},
Interactive: {
interacting: 1,
interactScript: `
subject.Interlocutor.dialogue({
body: 'Hey what is up :)',
origin: subject.Position.toJSON(),
position: {x: subject.Position.x, y: subject.Position.y - 32},
})
`,
},
Interlocutor: {},
Position: {x: 200, y: 200},
Sprite: {
anchorX: 0.5,
anchorY: 0.7,
source: '/assets/chest.json',
},
Ticking: {},
VisibleAabb: {},
});
return entities;
} }

View File

@ -15,7 +15,7 @@ export default async function createPlayer(id) {
}, },
Controlled: {}, Controlled: {},
Direction: {direction: 2}, Direction: {direction: 2},
Ecs: {path: ['forests', `${id}`].join('/')}, Ecs: {path: ['homesteads', `${id}`].join('/')},
Emitter: {}, Emitter: {},
Forces: {}, Forces: {},
Interacts: {}, Interacts: {},

View File

@ -13,9 +13,8 @@ export default class Emitter extends Component {
} }
instanceFromSchema() { instanceFromSchema() {
const Component = this; const Component = this;
const Instance = super.instanceFromSchema(); return class EmitterInstance extends super.instanceFromSchema() {
return class EmitterInstance extends Instance { emitting = {};
emitting = [];
id = 0; id = 0;
emit(specification) { emit(specification) {
Component.markChange(this.entity, 'emit', {[this.id++]: specification}); Component.markChange(this.entity, 'emit', {[this.id++]: specification});

View File

@ -0,0 +1,24 @@
import Component from '@/ecs/component.js';
export default class Interlocutor extends Component {
mergeDiff(original, update) {
const merged = {};
if (update.dialogue) {
merged.dialogue = {
...original.dialogue,
...update.dialogue,
}
}
return merged;
}
instanceFromSchema() {
const Component = this;
return class InterlocutorInstance extends super.instanceFromSchema() {
dialogues = {};
id = 0;
dialogue(specification) {
Component.markChange(this.entity, 'dialogue', {[this.id++]: specification});
}
};
}
}

View File

@ -82,7 +82,7 @@ class ItemProxy {
if (this.scripts.projectionCheckInstance) { if (this.scripts.projectionCheckInstance) {
this.scripts.projectionCheckInstance.context.ecs = this.Component.ecs; this.scripts.projectionCheckInstance.context.ecs = this.Component.ecs;
this.scripts.projectionCheckInstance.context.projected = projected; this.scripts.projectionCheckInstance.context.projected = projected;
return this.scripts.projectionCheckInstance.evaluateSync(); return this.scripts.projectionCheckInstance.evaluate();
} }
else { else {
return projected; return projected;

View File

@ -7,7 +7,7 @@ export default class Plant extends Component {
const Instance = super.instanceFromSchema(); const Instance = super.instanceFromSchema();
return class PlantInstance extends Instance { return class PlantInstance extends Instance {
mayGrow() { mayGrow() {
return this.mayGrowScriptInstance.evaluateSync(); return this.mayGrowScriptInstance.evaluate();
} }
grow() { grow() {
const {Ticking} = ecs.get(this.entity); const {Ticking} = ecs.get(this.entity);

View File

@ -76,13 +76,22 @@ export default class Engine {
} }
return cache.get(key); return cache.get(key);
} }
async readScript(uri, context) { async readScript(uriOrCode, context) {
if (!uri) { if (!uriOrCode) {
return undefined; return undefined;
} }
const code = await this.readAsset(uri); let code = '';
if (code.byteLength > 0) { if (!uriOrCode.startsWith('/')) {
return Script.fromCode((new TextDecoder()).decode(code), context); code = uriOrCode;
}
else {
const buffer = await this.readAsset(uriOrCode);
if (buffer.byteLength > 0) {
code = (new TextDecoder()).decode(buffer);
}
}
if (code) {
return Script.fromCode(code, context);
} }
} }
async switchEcs(entity, path, updates) { async switchEcs(entity, path, updates) {
@ -258,7 +267,10 @@ export default class Engine {
if ('ENOENT' !== error.code) { if ('ENOENT' !== error.code) {
throw error; throw error;
} }
const homestead = await createHomestead(this.Ecs, id); const homestead = createEcs(this.Ecs);
for (const entity of await createHomestead(id)) {
await homestead.create(entity);
}
await this.saveEcs( await this.saveEcs(
['homesteads', `${id}`].join('/'), ['homesteads', `${id}`].join('/'),
homestead, homestead,
@ -268,7 +280,10 @@ export default class Engine {
['houses', `${id}`].join('/'), ['houses', `${id}`].join('/'),
house, house,
); );
const forest = await createForest(this.Ecs, id); const forest = createEcs(this.Ecs);
for (const entity of await createForest()) {
await forest.create(entity);
}
await this.saveEcs( await this.saveEcs(
['forests', `${id}`].join('/'), ['forests', `${id}`].join('/'),
forest, forest,

View File

@ -2,6 +2,7 @@ import {del, get, set} from 'idb-keyval';
import {encode} from '@/packets/index.js'; import {encode} from '@/packets/index.js';
import createEcs from '../../create-ecs.js';
import '../../create-forest.js'; import '../../create-forest.js';
import '../../create-homestead.js'; import '../../create-homestead.js';
import '../../create-player.js'; import '../../create-player.js';
@ -70,7 +71,7 @@ if (import.meta.hot) {
return promise; return promise;
}; };
const beforeResolver = createResolver(); const beforeResolver = createResolver();
const resolvers = []; const resolvers = [beforeResolver];
import.meta.hot.on('vite:beforeUpdate', async () => { import.meta.hot.on('vite:beforeUpdate', async () => {
engine.stop(); engine.stop();
await engine.disconnectPlayer(0); await engine.disconnectPlayer(0);
@ -82,15 +83,15 @@ if (import.meta.hot) {
const resolver = createResolver(); const resolver = createResolver();
resolvers.push(resolver); resolvers.push(resolver);
await beforeResolver; await beforeResolver;
// const oldBuffer = await engine.server.readData('players/0'); const oldBuffer = await engine.server.readData('players/0');
// const oldPlayer = JSON.parse((new TextDecoder()).decode(oldBuffer)); const oldPlayer = JSON.parse((new TextDecoder()).decode(oldBuffer));
// const buffer = await createPlayer(0); const buffer = await createPlayer(0);
// const player = JSON.parse((new TextDecoder()).decode(buffer)); const player = JSON.parse((new TextDecoder()).decode(buffer));
// // Less jarring // Less jarring
// player.Ecs = oldPlayer.Ecs; player.Ecs = oldPlayer.Ecs;
// player.Direction = oldPlayer.Direction; player.Direction = oldPlayer.Direction;
// player.Position = oldPlayer.Position; player.Position = oldPlayer.Position;
// await engine.server.writeData('players/0', (new TextEncoder()).encode(JSON.stringify(player))); await engine.server.writeData('players/0', (new TextEncoder()).encode(JSON.stringify(player)));
resolver.resolve(); resolver.resolve();
}); });
import.meta.hot.accept('../../create-forest.js', async ({default: createForest}) => { import.meta.hot.accept('../../create-forest.js', async ({default: createForest}) => {
@ -99,9 +100,10 @@ if (import.meta.hot) {
await beforeResolver; await beforeResolver;
delete engine.ecses['forests/0']; delete engine.ecses['forests/0'];
await engine.server.removeData('forests/0'); await engine.server.removeData('forests/0');
const last = performance.now(); const forest = createEcs(engine.Ecs);
const forest = await createForest(engine.Ecs, '0'); for (const entity of await createForest()) {
console.log((performance.now() - last) / 1000); await forest.create(entity);
}
await engine.saveEcs('forests/0', forest); await engine.saveEcs('forests/0', forest);
resolver.resolve(); resolver.resolve();
}); });
@ -109,10 +111,13 @@ if (import.meta.hot) {
const resolver = createResolver(); const resolver = createResolver();
resolvers.push(resolver); resolvers.push(resolver);
await beforeResolver; await beforeResolver;
// delete engine.ecses['homesteads/0']; delete engine.ecses['homesteads/0'];
// await engine.server.removeData('homesteads/0'); await engine.server.removeData('homesteads/0');
// const homestead = await createHomestead(engine.Ecs, '0'); const homestead = createEcs(engine.Ecs);
// await engine.saveEcs('homesteads/0', homestead); for (const entity of await createHomestead('0')) {
await homestead.create(entity);
}
await engine.saveEcs('homesteads/0', homestead);
resolver.resolve(); resolver.resolve();
}); });
import.meta.hot.on('vite:afterUpdate', async () => { import.meta.hot.on('vite:afterUpdate', async () => {

View File

@ -46,4 +46,7 @@
.dashboard { .dashboard {
margin: 16px; margin: 16px;
pre {
user-select: text;
}
} }

View File

@ -20,7 +20,6 @@
} }
.selectionWrapper img { .selectionWrapper img {
user-select: none;
width: 100%; width: 100%;
} }
@ -36,6 +35,7 @@
gap: 32px; gap: 32px;
justify-content: space-between; justify-content: space-between;
margin: 8px; margin: 8px;
user-select: text;
} }
.selectionStatus { .selectionStatus {

View File

@ -0,0 +1,140 @@
import {useEffect, useRef, useState} from 'react';
import {RESOLUTION} from '@/constants.js';
import {useDomScale} from '@/context/dom-scale.js';
import styles from './dialogue.module.css';
const CARET_SIZE = 12;
export default function Dialogue({
camera,
dialogue,
onClose,
scale,
}) {
const domScale = useDomScale();
const ref = useRef();
const [dimensions, setDimensions] = useState({h: 0, w: 0});
useEffect(() => {
const {ttl = 5} = dialogue;
if (!ttl) {
return;
}
setTimeout(() => {
onClose();
}, ttl * 1000);
}, [dialogue, onClose]);
useEffect(() => {
let handle;
function track() {
if (ref.current) {
const {height, width} = ref.current.getBoundingClientRect();
setDimensions({h: height / domScale, w: width / domScale});
}
handle = requestAnimationFrame(track);
}
handle = requestAnimationFrame(track);
return () => {
cancelAnimationFrame(handle);
};
}, [dialogue, domScale, ref]);
const origin = 'function' === typeof dialogue.origin
? dialogue.origin()
: dialogue.origin || {x: 0, y: 0};
const bounds = {
x: dimensions.w / (2 * scale),
y: dimensions.h / (2 * scale),
};
const left = Math.max(
Math.min(
dialogue.position.x * scale - camera.x,
RESOLUTION.x - bounds.x * scale - 16,
),
bounds.x * scale + 16,
);
const top = Math.max(
Math.min(
dialogue.position.y * scale - camera.y,
RESOLUTION.y - bounds.y * scale - 16,
),
bounds.y * scale + 88,
);
const offsetPosition = {
x: ((dialogue.position.x * scale - camera.x) - left) / scale,
y: ((dialogue.position.y * scale - camera.y) - top) / scale,
};
const difference = {
x: origin.x - dialogue.position.x + offsetPosition.x,
y: origin.y - dialogue.position.y + offsetPosition.y,
};
const within = {
x: Math.abs(difference.x) < bounds.x,
y: Math.abs(difference.y) < bounds.y,
};
const caretPosition = {
x: Math.max(-bounds.x, Math.min(origin.x - dialogue.position.x + offsetPosition.x, bounds.x)),
y: Math.max(-bounds.y, Math.min(origin.y - dialogue.position.y + offsetPosition.y, bounds.y)),
};
let caretRotation = Math.atan2(
difference.y - caretPosition.y,
difference.x - caretPosition.x,
);
caretRotation += Math.PI * 1.5;
if (within.x) {
caretPosition.y = bounds.y * Math.sign(difference.y);
if (within.y) {
if (Math.sign(difference.y) > 0) {
caretRotation = Math.PI;
}
else {
caretRotation = 0;
}
}
}
else if (within.y) {
caretPosition.x = bounds.x * Math.sign(difference.x);
}
caretPosition.x *= scale;
caretPosition.y *= scale;
caretPosition.x += -Math.sin(caretRotation) * (CARET_SIZE / 2);
caretPosition.y += Math.cos(caretRotation) * (CARET_SIZE / 2);
return (
<div
ref={ref}
style={{
backgroundColor: '#00000044',
border: 'solid 1px white',
borderRadius: '8px',
color: 'white',
padding: '1em',
position: 'absolute',
left: `${left}px`,
margin: '0',
top: `${top}px`,
transform: 'translate(-50%, -50%)',
userSelect: 'none',
whiteSpace: 'nowrap',
}}
>
<svg
className={styles.caret}
viewBox="0 0 24 24"
width={CARET_SIZE}
height={CARET_SIZE}
style={{
transform: `
translate(
calc(-50% + ${caretPosition.x}px),
calc(-50% + ${caretPosition.y}px)
)
rotate(${caretRotation}rad)
`,
}}
>
<polygon points="0 0, 24 0, 12 24" />
</svg>
{dialogue.body}
</div>
);
}

View File

@ -0,0 +1,7 @@
.caret {
position: absolute;
fill: #00000044;
stroke: white;
left: 50%;
top: 50%;
}

View File

@ -0,0 +1,22 @@
import styles from './dialogues.module.css';
import Dialogue from './dialogue.jsx';
export default function Dialogues({camera, dialogues, scale}) {
const elements = [];
for (const key in dialogues) {
elements.push(
<Dialogue
camera={camera}
dialogue={dialogues[key]}
key={key}
onClose={() => {
delete dialogues[key];
}}
scale={scale}
/>
);
}
return <div className={styles.dialogues}>{elements}</div>;
}

View File

@ -0,0 +1,4 @@
.dialogues {
font-family: 'Times New Roman', Times, serif;
font-size: 16px;
}

View File

@ -1,6 +1,7 @@
import {useEffect, useRef, useState} from 'react'; import {useEffect, useRef, useState} from 'react';
import {RESOLUTION} from '@/constants.js'; import {RESOLUTION} from '@/constants.js';
import DomContext from '@/context/dom-scale.js';
import styles from './dom.module.css'; import styles from './dom.module.css';
@ -35,7 +36,9 @@ export default function Dom({children}) {
} }
`}</style> `}</style>
)} )}
<DomContext.Provider value={scale}>
{children} {children}
</DomContext.Provider>
</div> </div>
); );
} }

View File

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

View File

@ -0,0 +1,49 @@
import {useState} from 'react';
import {useEcs, useEcsTick} from '@/context/ecs.js';
import Entity from './entity.jsx';
export default function Entities({camera, scale}) {
const [ecs] = useEcs();
const [entities, setEntities] = useState({});
useEcsTick((payload) => {
if (!ecs) {
return;
}
const updatedEntities = {...entities};
for (const id in payload) {
if ('1' === id) {
continue;
}
const update = payload[id];
if (false === update) {
delete updatedEntities[id];
continue;
}
updatedEntities[id] = ecs.get(id);
const {dialogue} = update.Interlocutor || {};
if (dialogue) {
for (const key in dialogue) {
updatedEntities[id].Interlocutor.dialogues[key] = dialogue[key];
}
}
}
setEntities(updatedEntities);
}, [ecs, entities]);
const renderables = [];
for (const id in entities) {
renderables.push(
<Entity
camera={camera}
entity={entities[id]}
key={id}
scale={scale}
/>
);
}
return (
<>
{renderables}
</>
);
}

View File

@ -0,0 +1,15 @@
import Dialogues from './dialogues.jsx';
export default function Entity({camera, entity, scale}) {
return (
<>
{entity.Interlocutor && (
<Dialogues
camera={camera}
dialogues={entity.Interlocutor.dialogues}
scale={scale}
/>
)}
</>
)
}

View File

@ -10,7 +10,6 @@
display: inline-block; display: inline-block;
height: var(--size); height: var(--size);
image-rendering: pixelated; image-rendering: pixelated;
user-select: none;
width: var(--size); width: var(--size);
&:focus-visible { &:focus-visible {
outline: none; outline: none;

View File

@ -1,8 +1,6 @@
import {Container} from '@pixi/react'; import {Container} from '@pixi/react';
import {useEffect, useState} from 'react'; import {useEffect, useState} from 'react';
import {RESOLUTION} from '@/constants.js';
import {usePacket} from '@/context/client.js';
import {useEcs, useEcsTick} from '@/context/ecs.js'; import {useEcs, useEcsTick} from '@/context/ecs.js';
import {useMainEntity} from '@/context/main-entity.js'; import {useMainEntity} from '@/context/main-entity.js';
@ -28,13 +26,16 @@ function calculateDarkness(hour) {
return Math.floor(darkness * 1000) / 1000; return Math.floor(darkness * 1000) / 1000;
} }
export default function Ecs({applyFilters, scale}) { export default function Ecs({applyFilters, camera, scale}) {
const [ecs] = useEcs(); const [ecs] = useEcs();
const [entities, setEntities] = useState({});
const [filters, setFilters] = useState([]); const [filters, setFilters] = useState([]);
const [mainEntity] = useMainEntity(); const [mainEntity] = useMainEntity();
const [layers, setLayers] = useState([]);
const [hour, setHour] = useState(10); const [hour, setHour] = useState(10);
const [night, setNight] = useState(); const [night, setNight] = useState();
const [projected, setProjected] = useState([]);
const [position, setPosition] = useState({x: 0, y: 0});
const [water, setWater] = useState();
useEffect(() => { useEffect(() => {
async function buildNightFilter() { async function buildNightFilter() {
const {ColorMatrixFilter} = await import('@pixi/filter-color-matrix'); const {ColorMatrixFilter} = await import('@pixi/filter-color-matrix');
@ -62,101 +63,78 @@ export default function Ecs({applyFilters, scale}) {
night.setIntensity(calculateDarkness(hour)); night.setIntensity(calculateDarkness(hour));
} }
}, [hour, night]); }, [hour, night]);
usePacket('EcsChange', async () => {
setEntities({});
}, [setEntities]);
useEcsTick((payload) => { useEcsTick((payload) => {
if (!ecs) { const entity = ecs.get(mainEntity);
return;
}
const updatedEntities = {...entities};
for (const id in payload) { for (const id in payload) {
const update = payload[id]; const update = payload[id];
if (false === update) { switch (id) {
delete updatedEntities[id]; case '1': {
const master = ecs.get(1);
if (update.TileLayers) {
setLayers(Object.values(master.TileLayers.$$layersProxies));
} }
else {
if ('1' === id) {
if (update.Time) { if (update.Time) {
setHour(Math.round(ecs.get(1).Time.hour * 60) / 60); setHour(Math.round(ecs.get(1).Time.hour * 60) / 60);
} }
if (update.Water) {
setWater(master.Water.water);
} }
updatedEntities[id] = ecs.get(id); break;
if (update.Emitter?.emit) {
updatedEntities[id].Emitter.emitting = {
...updatedEntities[id].Emitter.emitting,
...update.Emitter.emit,
};
} }
} }
} }
setEntities(updatedEntities); if (entity) {
}, [ecs, entities, mainEntity]); const {Direction, Position, Wielder} = entity;
setPosition(Position.toJSON());
setProjected(Wielder.activeItem()?.project(Position.tile, Direction.direction));
}
}, [ecs, mainEntity, scale]);
useEffect(() => { useEffect(() => {
setFilters( setFilters(
applyFilters applyFilters
? [night] ? [
...(night ? [night] : [])
]
: [], : [],
); );
}, [applyFilters, night]) }, [applyFilters, night])
if (!ecs || !mainEntity) {
return false;
}
const entity = ecs.get(mainEntity);
if (!entity) {
return false;
}
const {Direction, Position, Wielder} = entity;
const projected = Wielder.activeItem()?.project(Position.tile, Direction.direction)
const {Camera} = entity;
const {TileLayers, Water: WaterEcs} = ecs.get(1);
const layer0 = TileLayers.layer(0);
const layer1 = TileLayers.layer(1);
const [cx, cy] = [
Math.round((Camera.x * scale) - RESOLUTION.x / 2),
Math.round((Camera.y * scale) - RESOLUTION.y / 2),
];
return ( return (
<Container <Container
scale={scale} scale={scale}
x={-cx} x={-camera.x}
y={-cy} y={-camera.y}
> >
<Container <Container
filters={filters} filters={filters}
> >
{layers.map((layer, i) => (
<TileLayer <TileLayer
filters={filters} filters={filters}
tileLayer={layer0} key={i}
tileLayer={layer}
/> />
{layer1 && ( ))}
<TileLayer
filters={filters}
tileLayer={layer1}
/>
)}
</Container> </Container>
{WaterEcs && ( {water && layers[0] && (
<Water <Water
tileLayer={layer0} tileLayer={layers[0]}
water={WaterEcs.water} water={water}
/> />
)} )}
{projected && ( {projected && layers[0] && (
<TargetingGrid <TargetingGrid
tileLayer={layer0} tileLayer={layers[0]}
x={Position.x} x={position.x}
y={Position.y} y={position.y}
/> />
)} )}
<Entities <Entities
entities={entities}
filters={filters} filters={filters}
/> />
{projected?.length > 0 && ( {projected?.length > 0 && layers[0] && (
<TargetingGhost <TargetingGhost
projected={projected} projected={projected}
tileLayer={layer0} tileLayer={layers[0]}
/> />
)} )}
</Container> </Container>

View File

@ -3,13 +3,15 @@ import {GlowFilter} from '@pixi/filter-glow';
import {Container} from '@pixi/react'; import {Container} from '@pixi/react';
import {useEffect, useState} from 'react'; import {useEffect, useState} from 'react';
import {useEcs} from '@/context/ecs.js'; import {usePacket} from '@/context/client.js';
import {useEcs, useEcsTick} from '@/context/ecs.js';
import {useMainEntity} from '@/context/main-entity.js'; import {useMainEntity} from '@/context/main-entity.js';
import Entity from './entity.jsx'; import Entity from './entity.jsx';
export default function Entities({entities, filters}) { export default function Entities({filters}) {
const [ecs] = useEcs(); const [ecs] = useEcs();
const [entities, setEntities] = useState({});
const [mainEntity] = useMainEntity(); const [mainEntity] = useMainEntity();
const [radians, setRadians] = useState(0); const [radians, setRadians] = useState(0);
const [willInteractWith, setWillInteractWith] = useState(0); const [willInteractWith, setWillInteractWith] = useState(0);
@ -17,6 +19,38 @@ export default function Entities({entities, filters}) {
const pulse = (Math.cos(radians) + 1) * 0.5; const pulse = (Math.cos(radians) + 1) * 0.5;
interactionFilters[0].brightness = (pulse * 0.75) + 1; interactionFilters[0].brightness = (pulse * 0.75) + 1;
interactionFilters[1].outerStrength = pulse * 0.5; interactionFilters[1].outerStrength = pulse * 0.5;
usePacket('EcsChange', async () => {
setEntities({});
}, [setEntities]);
useEcsTick((payload) => {
if (!ecs) {
return;
}
const updatedEntities = {...entities};
for (const id in payload) {
if ('1' === id) {
continue;
}
const update = payload[id];
if (false === update) {
delete updatedEntities[id];
}
else {
updatedEntities[id] = ecs.get(id);
if (update.Emitter?.emit) {
updatedEntities[id].Emitter.emitting = {
...updatedEntities[id].Emitter.emitting,
...update.Emitter.emit,
};
}
}
}
setEntities(updatedEntities);
const main = ecs.get(mainEntity);
if (main) {
setWillInteractWith(main.Interacts.willInteractWith);
}
}, [ecs, entities, mainEntity]);
useEffect(() => { useEffect(() => {
setRadians(0); setRadians(0);
const handle = setInterval(() => { const handle = setInterval(() => {
@ -26,17 +60,8 @@ export default function Entities({entities, filters}) {
clearInterval(handle); clearInterval(handle);
}; };
}, [willInteractWith]); }, [willInteractWith]);
useEffect(() => {
if (!mainEntity) {
return;
}
setWillInteractWith(ecs.get(mainEntity).Interacts.willInteractWith);
}, [entities, ecs, mainEntity]);
const renderables = []; const renderables = [];
for (const id in entities) { for (const id in entities) {
if ('1' === id) {
continue;
}
const isHighlightedInteraction = id == willInteractWith; const isHighlightedInteraction = id == willInteractWith;
renderables.push( renderables.push(
<Entity <Entity

View File

@ -45,7 +45,7 @@ export const Stage = ({children, ...props}) => {
); );
}; };
export default function Pixi({applyFilters, scale}) { export default function Pixi({applyFilters, camera, scale}) {
return ( return (
<Stage <Stage
className={styles.stage} className={styles.stage}
@ -55,7 +55,11 @@ export default function Pixi({applyFilters, scale}) {
background: 0x1099bb, background: 0x1099bb,
}} }}
> >
<Ecs applyFilters={applyFilters} scale={scale} /> <Ecs
applyFilters={applyFilters}
camera={camera}
scale={scale}
/>
</Stage> </Stage>
); );
} }

View File

@ -15,7 +15,7 @@ const WaterTile = forwardRef(function WaterTile({height, width}, ref) {
return <Graphics alpha={0} draw={draw} ref={ref} /> return <Graphics alpha={0} draw={draw} ref={ref} />
}); });
export default function Water({mask, tileLayer, water}) { export default function Water({tileLayer, water}) {
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
useEffect(() => { useEffect(() => {
setMounted(true); setMounted(true);
@ -39,7 +39,7 @@ export default function Water({mask, tileLayer, water}) {
} }
} }
return ( return (
<Container mask={mask}> <Container>
<WaterTile <WaterTile
height={tileLayer.tileSize.y} height={tileLayer.tileSize.y}
ref={waterTile} ref={waterTile}

View File

@ -8,11 +8,12 @@ import {useDebug} from '@/context/debug.js';
import {useEcs, useEcsTick} from '@/context/ecs.js'; import {useEcs, useEcsTick} from '@/context/ecs.js';
import {useMainEntity} from '@/context/main-entity.js'; import {useMainEntity} from '@/context/main-entity.js';
import Disconnected from './dom/disconnected.jsx';
import Dom from './dom/dom.jsx';
import Entities from './dom/entities.jsx';
import HotBar from './dom/hotbar.jsx';
import Pixi from './pixi/pixi.jsx';
import Devtools from './devtools.jsx'; 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'; import styles from './ui.module.css';
function emptySlots() { function emptySlots() {
@ -56,6 +57,7 @@ export default function Ui({disconnected}) {
const [bufferSlot, setBufferSlot] = useState(); const [bufferSlot, setBufferSlot] = useState();
const [devtoolsIsOpen, setDevtoolsIsOpen] = useState(false); const [devtoolsIsOpen, setDevtoolsIsOpen] = useState(false);
const ratio = (RESOLUTION.x * (devtoolsIsOpen ? 2 : 1)) / RESOLUTION.y; const ratio = (RESOLUTION.x * (devtoolsIsOpen ? 2 : 1)) / RESOLUTION.y;
const [camera, setCamera] = useState({x: 0, y: 0});
const [hotbarSlots, setHotbarSlots] = useState(emptySlots()); const [hotbarSlots, setHotbarSlots] = useState(emptySlots());
const [activeSlot, setActiveSlot] = useState(0); const [activeSlot, setActiveSlot] = useState(0);
const [scale, setScale] = useState(2); const [scale, setScale] = useState(2);
@ -236,12 +238,15 @@ export default function Ui({disconnected}) {
for (const id in payload) { for (const id in payload) {
const entity = ecs.get(id); const entity = ecs.get(id);
const update = payload[id]; const update = payload[id];
if (!update) {
continue;
}
if (update.Sound?.play) { if (update.Sound?.play) {
for (const sound of update.Sound.play) { for (const sound of update.Sound.play) {
(new Audio(sound)).play(); (new Audio(sound)).play();
} }
} }
if (update?.MainEntity) { if (update.MainEntity) {
setMainEntity(localMainEntity = id); setMainEntity(localMainEntity = id);
} }
if (localMainEntity === id) { if (localMainEntity === id) {
@ -258,7 +263,14 @@ export default function Ui({disconnected}) {
} }
} }
} }
}, [ecs, mainEntity]); if (localMainEntity) {
const mainEntityEntity = ecs.get(localMainEntity);
setCamera({
x: Math.round((mainEntityEntity.Camera.x * scale) - RESOLUTION.x / 2),
y: Math.round((mainEntityEntity.Camera.y * scale) - RESOLUTION.y / 2),
});
}
}, [ecs, mainEntity, scale]);
useEffect(() => { useEffect(() => {
function onContextMenu(event) { function onContextMenu(event) {
event.preventDefault(); event.preventDefault();
@ -267,7 +279,7 @@ export default function Ui({disconnected}) {
return () => { return () => {
document.body.removeEventListener('contextmenu', onContextMenu); document.body.removeEventListener('contextmenu', onContextMenu);
}; };
}, []) }, []);
return ( return (
<div <div
className={styles.ui} className={styles.ui}
@ -367,9 +379,12 @@ export default function Ui({disconnected}) {
}} }}
ref={gameRef} ref={gameRef}
> >
<Pixi applyFilters={applyFilters} scale={scale} /> <Pixi
{mainEntity && ( applyFilters={applyFilters}
<Dom devtoolsIsOpen={devtoolsIsOpen}> camera={camera}
scale={scale}
/>
<Dom>
<HotBar <HotBar
active={activeSlot} active={activeSlot}
onActivate={(i) => { onActivate={(i) => {
@ -380,11 +395,14 @@ export default function Ui({disconnected}) {
}} }}
slots={hotbarSlots} slots={hotbarSlots}
/> />
<Entities
camera={camera}
scale={scale}
/>
{showDisconnected && ( {showDisconnected && (
<Disconnected /> <Disconnected />
)} )}
</Dom> </Dom>
)}
</div> </div>
<div className={[styles.devtools, devtoolsIsOpen && styles.devtoolsIsOpen].filter(Boolean).join(' ')}> <div className={[styles.devtools, devtoolsIsOpen && styles.devtoolsIsOpen].filter(Boolean).join(' ')}>
<Devtools <Devtools

View File

@ -20,4 +20,5 @@
display: flex; display: flex;
line-height: 0; line-height: 0;
position: relative; position: relative;
user-select: none;
} }

View File

@ -65,28 +65,7 @@ export default class Script {
}; };
} }
// async evaluate(callback) { evaluate() {
// this.sandbox.reset();
// let {done, value} = this.sandbox.next();
// if (value instanceof Promise) {
// await value;
// }
// while (!done) {
// ({done, value} = this.sandbox.next());
// if (value instanceof Promise) {
// // eslint-disable-next-line no-await-in-loop
// await value;
// }
// }
// if (value instanceof Promise) {
// value.then(callback);
// }
// else {
// callback(value);
// }
// }
evaluateSync() {
this.sandbox.reset(); this.sandbox.reset();
const {value} = this.sandbox.run(); const {value} = this.sandbox.run();
return value; return value;

1
public/assets/chest.json Normal file
View File

@ -0,0 +1 @@
{"frames":{"":{"frame":{"x":0,"y":0,"w":22,"h":22},"spriteSourceSize":{"x":0,"y":0,"w":22,"h":22},"sourceSize":{"w":22,"h":22}}},"meta":{"format":"RGBA8888","image":"./chest.png","scale":1,"size":{"w":22,"h":22}}}

BIN
public/assets/chest.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -6,8 +6,8 @@ import {basename, dirname, extname, join} from 'node:path';
import imageSize from 'image-size'; import imageSize from 'image-size';
const tileset = process.argv[2]; const tileset = process.argv[2];
let w = parseInt(process.argv[3]); let w = parseInt(process.argv[3] || '0');
let h = parseInt(process.argv[4]); let h = parseInt(process.argv[4] || '0');
const {width, height} = imageSize(tileset); const {width, height} = imageSize(tileset);

View File

@ -30,13 +30,9 @@ if (projected?.length > 0) {
stages: Array(5).fill(0.5), stages: Array(5).fill(0.5),
}, },
Sprite: { Sprite: {
anchorX: 0.5,
anchorY: 0.75, anchorY: 0.75,
animation: 'stage/0', animation: 'stage/0',
frame: 0,
frames: 1,
source: '/assets/tomato-plant/tomato-plant.json', source: '/assets/tomato-plant/tomato-plant.json',
speed: 0,
}, },
Ticking: {}, Ticking: {},
VisibleAabb: {}, VisibleAabb: {},