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);
}
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
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 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;
}

View File

@ -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;
}

View File

@ -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: {},

View File

@ -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});

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) {
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;

View File

@ -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);

View File

@ -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,

View File

@ -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 () => {

View File

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

View File

@ -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 {

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 {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>
);
}

View File

@ -3,6 +3,7 @@
flex-direction: column;
height: calc(100% / var(--scale));
left: 0;
overflow: hidden;
position: absolute;
top: 0;
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;
height: var(--size);
image-rendering: pixelated;
user-select: none;
width: var(--size);
&:focus-visible {
outline: none;

View File

@ -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>

View File

@ -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

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 (
<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>
);
}

View File

@ -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}

View File

@ -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

View File

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

View File

@ -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
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';
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);

View File

@ -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: {},