Compare commits
18 Commits
3d4d29625d
...
92a77d8046
Author | SHA1 | Date | |
---|---|---|---|
|
92a77d8046 | ||
|
78060ded37 | ||
|
7141d20a94 | ||
|
bb0c5ab1e9 | ||
|
d086f802f8 | ||
|
375b83f366 | ||
|
34cd695a80 | ||
|
71c4f1f959 | ||
|
80a2833842 | ||
|
dfc7665856 | ||
|
ece20dfcd7 | ||
|
dcf6fff66d | ||
|
a8d345eebe | ||
|
ef744591d2 | ||
|
e2c62a6522 | ||
|
a208863823 | ||
|
354b013e70 | ||
|
14c15bbf4a |
|
@ -41,10 +41,22 @@ export default class ClientEcs extends Ecs {
|
|||
}
|
||||
return cache.get(key);
|
||||
}
|
||||
async readScript(uri, context = {}) {
|
||||
const code = await this.readAsset(uri);
|
||||
if (code.byteLength > 0) {
|
||||
return Script.fromCode((new TextDecoder()).decode(code), context);
|
||||
async readScript(uriOrCode, context = {}) {
|
||||
if (!uriOrCode) {
|
||||
return undefined;
|
||||
}
|
||||
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
9
app/context/dom-scale.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
import {createContext, useContext} from 'react';
|
||||
|
||||
const context = createContext();
|
||||
|
||||
export default context;
|
||||
|
||||
export function useDomScale() {
|
||||
return useContext(context);
|
||||
}
|
|
@ -3,8 +3,6 @@ import {createNoise2D} from 'simplex-noise';
|
|||
|
||||
import {createRandom, Generator} from '@/util/math.js';
|
||||
|
||||
import createEcs from './create-ecs.js';
|
||||
|
||||
const seed = 42069;
|
||||
const prng = alea(seed);
|
||||
const rawNoise = createNoise2D(prng);
|
||||
|
@ -54,10 +52,10 @@ const Forest = new Generator({
|
|||
],
|
||||
});
|
||||
|
||||
export default async function createForest(Ecs) {
|
||||
const ecs = createEcs(Ecs);
|
||||
export default async function createForest() {
|
||||
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},
|
||||
Ticking: {},
|
||||
TileLayers: {
|
||||
|
@ -77,8 +75,10 @@ export default async function createForest(Ecs) {
|
|||
],
|
||||
},
|
||||
Time: {},
|
||||
}));
|
||||
Water: {},
|
||||
});
|
||||
Forest.generate();
|
||||
const master = entities[0];
|
||||
const layer0 = master.TileLayers.layers[0];
|
||||
layer0.data = Forest.matrix;
|
||||
for (let y = 0; y < h; ++y) {
|
||||
|
@ -109,7 +109,7 @@ export default async function createForest(Ecs) {
|
|||
if (v > 0.2) {
|
||||
v = noise(x * (1 / 7), y * (1 / 7));
|
||||
if (v < 0.15) {
|
||||
await ecs.create({
|
||||
entities.push({
|
||||
Position: entityPosition(x, y),
|
||||
Sprite: {
|
||||
anchorY: 0.7,
|
||||
|
@ -119,7 +119,7 @@ export default async function createForest(Ecs) {
|
|||
});
|
||||
}
|
||||
else if (v < 0.17) {
|
||||
await ecs.create({
|
||||
entities.push({
|
||||
Position: entityPosition(x, y),
|
||||
Sprite: {
|
||||
anchorY: 0.875,
|
||||
|
@ -130,7 +130,7 @@ export default async function createForest(Ecs) {
|
|||
}
|
||||
v = noise(x * (1 / 12), y * (1 / 12));
|
||||
if (v < 0.08) {
|
||||
await ecs.create({
|
||||
entities.push({
|
||||
Position: entityPosition(x, y),
|
||||
Sprite: {
|
||||
anchorY: 0.7,
|
||||
|
@ -142,6 +142,6 @@ export default async function createForest(Ecs) {
|
|||
}
|
||||
}
|
||||
}
|
||||
return ecs;
|
||||
return entities;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import createEcs from './create-ecs.js';
|
||||
|
||||
export default async function createHomestead(Ecs, id) {
|
||||
const ecs = createEcs(Ecs);
|
||||
export default async function createHomestead(id) {
|
||||
const area = {x: 100, y: 60};
|
||||
await ecs.create({
|
||||
const entities = [];
|
||||
entities.push({
|
||||
AreaSize: {x: area.x * 16, y: area.y * 16},
|
||||
Ticking: {},
|
||||
TileLayers: {
|
||||
|
@ -25,7 +23,7 @@ export default async function createHomestead(Ecs, id) {
|
|||
Time: {},
|
||||
Water: {water: {}},
|
||||
});
|
||||
await ecs.create({
|
||||
entities.push({
|
||||
Collider: {
|
||||
bodies: [
|
||||
{
|
||||
|
@ -61,5 +59,39 @@ export default async function createHomestead(Ecs, id) {
|
|||
Ticking: {},
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ export default async function createPlayer(id) {
|
|||
},
|
||||
Controlled: {},
|
||||
Direction: {direction: 2},
|
||||
Ecs: {path: ['forests', `${id}`].join('/')},
|
||||
Ecs: {path: ['homesteads', `${id}`].join('/')},
|
||||
Emitter: {},
|
||||
Forces: {},
|
||||
Interacts: {},
|
||||
|
|
|
@ -13,9 +13,8 @@ export default class Emitter extends Component {
|
|||
}
|
||||
instanceFromSchema() {
|
||||
const Component = this;
|
||||
const Instance = super.instanceFromSchema();
|
||||
return class EmitterInstance extends Instance {
|
||||
emitting = [];
|
||||
return class EmitterInstance extends super.instanceFromSchema() {
|
||||
emitting = {};
|
||||
id = 0;
|
||||
emit(specification) {
|
||||
Component.markChange(this.entity, 'emit', {[this.id++]: specification});
|
||||
|
|
24
app/ecs-components/interlocutor.js
Normal file
24
app/ecs-components/interlocutor.js
Normal 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});
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -82,7 +82,7 @@ class ItemProxy {
|
|||
if (this.scripts.projectionCheckInstance) {
|
||||
this.scripts.projectionCheckInstance.context.ecs = this.Component.ecs;
|
||||
this.scripts.projectionCheckInstance.context.projected = projected;
|
||||
return this.scripts.projectionCheckInstance.evaluateSync();
|
||||
return this.scripts.projectionCheckInstance.evaluate();
|
||||
}
|
||||
else {
|
||||
return projected;
|
||||
|
|
|
@ -7,7 +7,7 @@ export default class Plant extends Component {
|
|||
const Instance = super.instanceFromSchema();
|
||||
return class PlantInstance extends Instance {
|
||||
mayGrow() {
|
||||
return this.mayGrowScriptInstance.evaluateSync();
|
||||
return this.mayGrowScriptInstance.evaluate();
|
||||
}
|
||||
grow() {
|
||||
const {Ticking} = ecs.get(this.entity);
|
||||
|
|
|
@ -76,13 +76,22 @@ export default class Engine {
|
|||
}
|
||||
return cache.get(key);
|
||||
}
|
||||
async readScript(uri, context) {
|
||||
if (!uri) {
|
||||
async readScript(uriOrCode, context) {
|
||||
if (!uriOrCode) {
|
||||
return undefined;
|
||||
}
|
||||
const code = await this.readAsset(uri);
|
||||
if (code.byteLength > 0) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
async switchEcs(entity, path, updates) {
|
||||
|
@ -258,7 +267,10 @@ export default class Engine {
|
|||
if ('ENOENT' !== error.code) {
|
||||
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(
|
||||
['homesteads', `${id}`].join('/'),
|
||||
homestead,
|
||||
|
@ -268,7 +280,10 @@ export default class Engine {
|
|||
['houses', `${id}`].join('/'),
|
||||
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(
|
||||
['forests', `${id}`].join('/'),
|
||||
forest,
|
||||
|
|
|
@ -2,6 +2,7 @@ import {del, get, set} from 'idb-keyval';
|
|||
|
||||
import {encode} from '@/packets/index.js';
|
||||
|
||||
import createEcs from '../../create-ecs.js';
|
||||
import '../../create-forest.js';
|
||||
import '../../create-homestead.js';
|
||||
import '../../create-player.js';
|
||||
|
@ -70,7 +71,7 @@ if (import.meta.hot) {
|
|||
return promise;
|
||||
};
|
||||
const beforeResolver = createResolver();
|
||||
const resolvers = [];
|
||||
const resolvers = [beforeResolver];
|
||||
import.meta.hot.on('vite:beforeUpdate', async () => {
|
||||
engine.stop();
|
||||
await engine.disconnectPlayer(0);
|
||||
|
@ -82,15 +83,15 @@ 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-forest.js', async ({default: createForest}) => {
|
||||
|
@ -99,9 +100,10 @@ if (import.meta.hot) {
|
|||
await beforeResolver;
|
||||
delete engine.ecses['forests/0'];
|
||||
await engine.server.removeData('forests/0');
|
||||
const last = performance.now();
|
||||
const forest = await createForest(engine.Ecs, '0');
|
||||
console.log((performance.now() - last) / 1000);
|
||||
const forest = createEcs(engine.Ecs);
|
||||
for (const entity of await createForest()) {
|
||||
await forest.create(entity);
|
||||
}
|
||||
await engine.saveEcs('forests/0', forest);
|
||||
resolver.resolve();
|
||||
});
|
||||
|
@ -109,10 +111,13 @@ if (import.meta.hot) {
|
|||
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, '0');
|
||||
// await engine.saveEcs('homesteads/0', homestead);
|
||||
delete engine.ecses['homesteads/0'];
|
||||
await engine.server.removeData('homesteads/0');
|
||||
const homestead = createEcs(engine.Ecs);
|
||||
for (const entity of await createHomestead('0')) {
|
||||
await homestead.create(entity);
|
||||
}
|
||||
await engine.saveEcs('homesteads/0', homestead);
|
||||
resolver.resolve();
|
||||
});
|
||||
import.meta.hot.on('vite:afterUpdate', async () => {
|
||||
|
|
|
@ -46,4 +46,7 @@
|
|||
|
||||
.dashboard {
|
||||
margin: 16px;
|
||||
pre {
|
||||
user-select: text;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,6 @@
|
|||
}
|
||||
|
||||
.selectionWrapper img {
|
||||
user-select: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
@ -36,6 +35,7 @@
|
|||
gap: 32px;
|
||||
justify-content: space-between;
|
||||
margin: 8px;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.selectionStatus {
|
||||
|
|
140
app/react-components/dom/dialogue.jsx
Normal file
140
app/react-components/dom/dialogue.jsx
Normal 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>
|
||||
);
|
||||
}
|
7
app/react-components/dom/dialogue.module.css
Normal file
7
app/react-components/dom/dialogue.module.css
Normal file
|
@ -0,0 +1,7 @@
|
|||
.caret {
|
||||
position: absolute;
|
||||
fill: #00000044;
|
||||
stroke: white;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
}
|
22
app/react-components/dom/dialogues.jsx
Normal file
22
app/react-components/dom/dialogues.jsx
Normal 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>;
|
||||
}
|
||||
|
4
app/react-components/dom/dialogues.module.css
Normal file
4
app/react-components/dom/dialogues.module.css
Normal file
|
@ -0,0 +1,4 @@
|
|||
.dialogues {
|
||||
font-family: 'Times New Roman', Times, serif;
|
||||
font-size: 16px;
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import {useEffect, useRef, useState} from 'react';
|
||||
|
||||
import {RESOLUTION} from '@/constants.js';
|
||||
import DomContext from '@/context/dom-scale.js';
|
||||
|
||||
import styles from './dom.module.css';
|
||||
|
||||
|
@ -35,7 +36,9 @@ export default function Dom({children}) {
|
|||
}
|
||||
`}</style>
|
||||
)}
|
||||
{children}
|
||||
<DomContext.Provider value={scale}>
|
||||
{children}
|
||||
</DomContext.Provider>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -3,6 +3,7 @@
|
|||
flex-direction: column;
|
||||
height: calc(100% / var(--scale));
|
||||
left: 0;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
transform: scale(var(--scale));
|
49
app/react-components/dom/entities.jsx
Normal file
49
app/react-components/dom/entities.jsx
Normal 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}
|
||||
</>
|
||||
);
|
||||
}
|
15
app/react-components/dom/entity.jsx
Normal file
15
app/react-components/dom/entity.jsx
Normal 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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -10,7 +10,6 @@
|
|||
display: inline-block;
|
||||
height: var(--size);
|
||||
image-rendering: pixelated;
|
||||
user-select: none;
|
||||
width: var(--size);
|
||||
&:focus-visible {
|
||||
outline: none;
|
|
@ -1,8 +1,6 @@
|
|||
import {Container} from '@pixi/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 {useMainEntity} from '@/context/main-entity.js';
|
||||
|
||||
|
@ -28,13 +26,16 @@ function calculateDarkness(hour) {
|
|||
return Math.floor(darkness * 1000) / 1000;
|
||||
}
|
||||
|
||||
export default function Ecs({applyFilters, scale}) {
|
||||
export default function Ecs({applyFilters, camera, scale}) {
|
||||
const [ecs] = useEcs();
|
||||
const [entities, setEntities] = useState({});
|
||||
const [filters, setFilters] = useState([]);
|
||||
const [mainEntity] = useMainEntity();
|
||||
const [layers, setLayers] = useState([]);
|
||||
const [hour, setHour] = useState(10);
|
||||
const [night, setNight] = useState();
|
||||
const [projected, setProjected] = useState([]);
|
||||
const [position, setPosition] = useState({x: 0, y: 0});
|
||||
const [water, setWater] = useState();
|
||||
useEffect(() => {
|
||||
async function buildNightFilter() {
|
||||
const {ColorMatrixFilter} = await import('@pixi/filter-color-matrix');
|
||||
|
@ -62,101 +63,78 @@ export default function Ecs({applyFilters, scale}) {
|
|||
night.setIntensity(calculateDarkness(hour));
|
||||
}
|
||||
}, [hour, night]);
|
||||
usePacket('EcsChange', async () => {
|
||||
setEntities({});
|
||||
}, [setEntities]);
|
||||
useEcsTick((payload) => {
|
||||
if (!ecs) {
|
||||
return;
|
||||
}
|
||||
const updatedEntities = {...entities};
|
||||
const entity = ecs.get(mainEntity);
|
||||
for (const id in payload) {
|
||||
const update = payload[id];
|
||||
if (false === update) {
|
||||
delete updatedEntities[id];
|
||||
}
|
||||
else {
|
||||
if ('1' === id) {
|
||||
switch (id) {
|
||||
case '1': {
|
||||
const master = ecs.get(1);
|
||||
if (update.TileLayers) {
|
||||
setLayers(Object.values(master.TileLayers.$$layersProxies));
|
||||
}
|
||||
if (update.Time) {
|
||||
setHour(Math.round(ecs.get(1).Time.hour * 60) / 60);
|
||||
}
|
||||
}
|
||||
updatedEntities[id] = ecs.get(id);
|
||||
if (update.Emitter?.emit) {
|
||||
updatedEntities[id].Emitter.emitting = {
|
||||
...updatedEntities[id].Emitter.emitting,
|
||||
...update.Emitter.emit,
|
||||
};
|
||||
if (update.Water) {
|
||||
setWater(master.Water.water);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
setEntities(updatedEntities);
|
||||
}, [ecs, entities, mainEntity]);
|
||||
if (entity) {
|
||||
const {Direction, Position, Wielder} = entity;
|
||||
setPosition(Position.toJSON());
|
||||
setProjected(Wielder.activeItem()?.project(Position.tile, Direction.direction));
|
||||
}
|
||||
}, [ecs, mainEntity, scale]);
|
||||
useEffect(() => {
|
||||
setFilters(
|
||||
applyFilters
|
||||
? [night]
|
||||
? [
|
||||
...(night ? [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 (
|
||||
<Container
|
||||
scale={scale}
|
||||
x={-cx}
|
||||
y={-cy}
|
||||
x={-camera.x}
|
||||
y={-camera.y}
|
||||
>
|
||||
<Container
|
||||
filters={filters}
|
||||
>
|
||||
<TileLayer
|
||||
filters={filters}
|
||||
tileLayer={layer0}
|
||||
/>
|
||||
{layer1 && (
|
||||
{layers.map((layer, i) => (
|
||||
<TileLayer
|
||||
filters={filters}
|
||||
tileLayer={layer1}
|
||||
key={i}
|
||||
tileLayer={layer}
|
||||
/>
|
||||
)}
|
||||
))}
|
||||
</Container>
|
||||
{WaterEcs && (
|
||||
{water && layers[0] && (
|
||||
<Water
|
||||
tileLayer={layer0}
|
||||
water={WaterEcs.water}
|
||||
tileLayer={layers[0]}
|
||||
water={water}
|
||||
/>
|
||||
)}
|
||||
{projected && (
|
||||
{projected && layers[0] && (
|
||||
<TargetingGrid
|
||||
tileLayer={layer0}
|
||||
x={Position.x}
|
||||
y={Position.y}
|
||||
tileLayer={layers[0]}
|
||||
x={position.x}
|
||||
y={position.y}
|
||||
/>
|
||||
)}
|
||||
<Entities
|
||||
entities={entities}
|
||||
filters={filters}
|
||||
/>
|
||||
{projected?.length > 0 && (
|
||||
{projected?.length > 0 && layers[0] && (
|
||||
<TargetingGhost
|
||||
projected={projected}
|
||||
tileLayer={layer0}
|
||||
tileLayer={layers[0]}
|
||||
/>
|
||||
)}
|
||||
</Container>
|
|
@ -3,13 +3,15 @@ import {GlowFilter} from '@pixi/filter-glow';
|
|||
import {Container} from '@pixi/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 Entity from './entity.jsx';
|
||||
|
||||
export default function Entities({entities, filters}) {
|
||||
export default function Entities({filters}) {
|
||||
const [ecs] = useEcs();
|
||||
const [entities, setEntities] = useState({});
|
||||
const [mainEntity] = useMainEntity();
|
||||
const [radians, setRadians] = 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;
|
||||
interactionFilters[0].brightness = (pulse * 0.75) + 1;
|
||||
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(() => {
|
||||
setRadians(0);
|
||||
const handle = setInterval(() => {
|
||||
|
@ -26,17 +60,8 @@ export default function Entities({entities, filters}) {
|
|||
clearInterval(handle);
|
||||
};
|
||||
}, [willInteractWith]);
|
||||
useEffect(() => {
|
||||
if (!mainEntity) {
|
||||
return;
|
||||
}
|
||||
setWillInteractWith(ecs.get(mainEntity).Interacts.willInteractWith);
|
||||
}, [entities, ecs, mainEntity]);
|
||||
const renderables = [];
|
||||
for (const id in entities) {
|
||||
if ('1' === id) {
|
||||
continue;
|
||||
}
|
||||
const isHighlightedInteraction = id == willInteractWith;
|
||||
renderables.push(
|
||||
<Entity
|
|
@ -45,7 +45,7 @@ export const Stage = ({children, ...props}) => {
|
|||
);
|
||||
};
|
||||
|
||||
export default function Pixi({applyFilters, scale}) {
|
||||
export default function Pixi({applyFilters, camera, scale}) {
|
||||
return (
|
||||
<Stage
|
||||
className={styles.stage}
|
||||
|
@ -55,7 +55,11 @@ export default function Pixi({applyFilters, scale}) {
|
|||
background: 0x1099bb,
|
||||
}}
|
||||
>
|
||||
<Ecs applyFilters={applyFilters} scale={scale} />
|
||||
<Ecs
|
||||
applyFilters={applyFilters}
|
||||
camera={camera}
|
||||
scale={scale}
|
||||
/>
|
||||
</Stage>
|
||||
);
|
||||
}
|
|
@ -15,7 +15,7 @@ const WaterTile = forwardRef(function WaterTile({height, width}, 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);
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
|
@ -39,7 +39,7 @@ export default function Water({mask, tileLayer, water}) {
|
|||
}
|
||||
}
|
||||
return (
|
||||
<Container mask={mask}>
|
||||
<Container>
|
||||
<WaterTile
|
||||
height={tileLayer.tileSize.y}
|
||||
ref={waterTile}
|
|
@ -8,11 +8,12 @@ import {useDebug} from '@/context/debug.js';
|
|||
import {useEcs, useEcsTick} from '@/context/ecs.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 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';
|
||||
|
||||
function emptySlots() {
|
||||
|
@ -56,6 +57,7 @@ export default function Ui({disconnected}) {
|
|||
const [bufferSlot, setBufferSlot] = useState();
|
||||
const [devtoolsIsOpen, setDevtoolsIsOpen] = useState(false);
|
||||
const ratio = (RESOLUTION.x * (devtoolsIsOpen ? 2 : 1)) / RESOLUTION.y;
|
||||
const [camera, setCamera] = useState({x: 0, y: 0});
|
||||
const [hotbarSlots, setHotbarSlots] = useState(emptySlots());
|
||||
const [activeSlot, setActiveSlot] = useState(0);
|
||||
const [scale, setScale] = useState(2);
|
||||
|
@ -236,12 +238,15 @@ export default function Ui({disconnected}) {
|
|||
for (const id in payload) {
|
||||
const entity = ecs.get(id);
|
||||
const update = payload[id];
|
||||
if (!update) {
|
||||
continue;
|
||||
}
|
||||
if (update.Sound?.play) {
|
||||
for (const sound of update.Sound.play) {
|
||||
(new Audio(sound)).play();
|
||||
}
|
||||
}
|
||||
if (update?.MainEntity) {
|
||||
if (update.MainEntity) {
|
||||
setMainEntity(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(() => {
|
||||
function onContextMenu(event) {
|
||||
event.preventDefault();
|
||||
|
@ -267,7 +279,7 @@ export default function Ui({disconnected}) {
|
|||
return () => {
|
||||
document.body.removeEventListener('contextmenu', onContextMenu);
|
||||
};
|
||||
}, [])
|
||||
}, []);
|
||||
return (
|
||||
<div
|
||||
className={styles.ui}
|
||||
|
@ -367,24 +379,30 @@ export default function Ui({disconnected}) {
|
|||
}}
|
||||
ref={gameRef}
|
||||
>
|
||||
<Pixi applyFilters={applyFilters} 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>
|
||||
)}
|
||||
<Pixi
|
||||
applyFilters={applyFilters}
|
||||
camera={camera}
|
||||
scale={scale}
|
||||
/>
|
||||
<Dom>
|
||||
<HotBar
|
||||
active={activeSlot}
|
||||
onActivate={(i) => {
|
||||
client.send({
|
||||
type: 'Action',
|
||||
payload: {type: 'swapSlots', value: [0, i + 1]},
|
||||
});
|
||||
}}
|
||||
slots={hotbarSlots}
|
||||
/>
|
||||
<Entities
|
||||
camera={camera}
|
||||
scale={scale}
|
||||
/>
|
||||
{showDisconnected && (
|
||||
<Disconnected />
|
||||
)}
|
||||
</Dom>
|
||||
</div>
|
||||
<div className={[styles.devtools, devtoolsIsOpen && styles.devtoolsIsOpen].filter(Boolean).join(' ')}>
|
||||
<Devtools
|
||||
|
|
|
@ -20,4 +20,5 @@
|
|||
display: flex;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
}
|
||||
|
|
|
@ -65,28 +65,7 @@ export default class Script {
|
|||
};
|
||||
}
|
||||
|
||||
// async evaluate(callback) {
|
||||
// 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() {
|
||||
evaluate() {
|
||||
this.sandbox.reset();
|
||||
const {value} = this.sandbox.run();
|
||||
return value;
|
||||
|
|
1
public/assets/chest.json
Normal file
1
public/assets/chest.json
Normal 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
BIN
public/assets/chest.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
|
@ -6,8 +6,8 @@ import {basename, dirname, extname, join} from 'node:path';
|
|||
import imageSize from 'image-size';
|
||||
|
||||
const tileset = process.argv[2];
|
||||
let w = parseInt(process.argv[3]);
|
||||
let h = parseInt(process.argv[4]);
|
||||
let w = parseInt(process.argv[3] || '0');
|
||||
let h = parseInt(process.argv[4] || '0');
|
||||
|
||||
const {width, height} = imageSize(tileset);
|
||||
|
|
@ -30,13 +30,9 @@ if (projected?.length > 0) {
|
|||
stages: Array(5).fill(0.5),
|
||||
},
|
||||
Sprite: {
|
||||
anchorX: 0.5,
|
||||
anchorY: 0.75,
|
||||
animation: 'stage/0',
|
||||
frame: 0,
|
||||
frames: 1,
|
||||
source: '/assets/tomato-plant/tomato-plant.json',
|
||||
speed: 0,
|
||||
},
|
||||
Ticking: {},
|
||||
VisibleAabb: {},
|
||||
|
|
Loading…
Reference in New Issue
Block a user