refactor: particles
This commit is contained in:
parent
b719e3227b
commit
64df3b882f
|
@ -1,13 +1,38 @@
|
|||
import Component from '@/ecs/component.js';
|
||||
|
||||
export default class Emitter extends Component {
|
||||
import Emitter from '@/particles/emitter.js';
|
||||
import {Ticker as TickerPromise} from '@/util/promise.js';
|
||||
|
||||
export default class EmitterComponent extends Component {
|
||||
instanceFromSchema() {
|
||||
const Component = this;
|
||||
const {ecs} = this;
|
||||
return class EmitterInstance extends super.instanceFromSchema() {
|
||||
emitting = {};
|
||||
id = 0;
|
||||
emit(specification) {
|
||||
Component.markChange(this.entity, 'emit', {[this.id++]: specification});
|
||||
if (specification.server) {
|
||||
const {Ticker} = ecs.get(1);
|
||||
if (Ticker) {
|
||||
const emitter = new Emitter(ecs);
|
||||
const promise = new Promise((resolve) => {
|
||||
emitter.emit().onEnd(resolve);
|
||||
});
|
||||
Ticker.add(
|
||||
new TickerPromise(
|
||||
(resolve) => {
|
||||
promise.then(resolve);
|
||||
},
|
||||
(elapsed) => {
|
||||
this.emitter.tick(elapsed);
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
else {
|
||||
Component.markChange(this.entity, 'emit', {[this.id++]: specification});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -3,9 +3,23 @@ import Component from '@/ecs/component.js';
|
|||
export default class Sprite extends Component {
|
||||
instanceFromSchema() {
|
||||
return class SpriteInstance extends super.instanceFromSchema() {
|
||||
$$anchor = {x: 0.5, y: 0.5};
|
||||
$$scale = {x: 1, y: 1};
|
||||
$$sourceJson = {};
|
||||
get anchor() {
|
||||
return {x: this.anchorX, y: this.anchorY};
|
||||
return this.$$anchor;
|
||||
}
|
||||
get anchorX() {
|
||||
return this.$$anchor.x;
|
||||
}
|
||||
set anchorX(anchorX) {
|
||||
this.$$anchor = {x: anchorX, y: this.anchorY};
|
||||
}
|
||||
get anchorY() {
|
||||
return this.$$anchor.y;
|
||||
}
|
||||
set anchorY(anchorY) {
|
||||
this.$$anchor = {x: this.anchorX, y: anchorY};
|
||||
}
|
||||
get animation() {
|
||||
return super.animation;
|
||||
|
@ -56,7 +70,28 @@ export default class Sprite extends Component {
|
|||
return this.$$sourceJson.meta.rotation;
|
||||
}
|
||||
get scale() {
|
||||
return {x: this.scaleX, y: this.scaleY};
|
||||
return this.$$scale;
|
||||
}
|
||||
get scaleX() {
|
||||
return this.$$scale.x;
|
||||
}
|
||||
set scaleX(scaleX) {
|
||||
this.$$scale = {x: scaleX, y: this.scaleY};
|
||||
}
|
||||
get scaleY() {
|
||||
return this.$$scale.y;
|
||||
}
|
||||
set scaleY(scaleY) {
|
||||
this.$$scale = {x: this.scaleX, y: scaleY};
|
||||
}
|
||||
get size() {
|
||||
if (!this.$$sourceJson.frames) {
|
||||
return {x: 16, y: 16};
|
||||
}
|
||||
const frame = this.animation
|
||||
? this.$$sourceJson.animations[this.animation][this.frame]
|
||||
: '';
|
||||
return this.$$sourceJson.frames[frame].sourceSize;
|
||||
}
|
||||
toNet(recipient, data) {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
|
@ -66,7 +101,9 @@ export default class Sprite extends Component {
|
|||
};
|
||||
}
|
||||
async load(instance) {
|
||||
instance.$$sourceJson = await this.ecs.readJson(instance.source);
|
||||
if (instance.source) {
|
||||
instance.$$sourceJson = await this.ecs.readJson(instance.source);
|
||||
}
|
||||
}
|
||||
markChange(entityId, key, value) {
|
||||
if ('elapsed' === key) {
|
||||
|
@ -86,5 +123,6 @@ export default class Sprite extends Component {
|
|||
scaleY: {defaultValue: 1, type: 'float32'},
|
||||
source: {type: 'string'},
|
||||
speed: {type: 'float32'},
|
||||
tint: {type: 'uint32'},
|
||||
};
|
||||
}
|
||||
|
|
22
app/ecs/components/ttl.js
Normal file
22
app/ecs/components/ttl.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
import Component from '@/ecs/component.js';
|
||||
|
||||
export default class Ttl extends Component {
|
||||
instanceFromSchema() {
|
||||
const {ecs} = this;
|
||||
return class TtlInstance extends super.instanceFromSchema() {
|
||||
$$elapsed = 0;
|
||||
$$reset() {
|
||||
this.$$elapsed = 0;
|
||||
}
|
||||
tick(elapsed) {
|
||||
this.$$elapsed += elapsed;
|
||||
if (this.$$elapsed >= this.ttl) {
|
||||
ecs.destroy(this.entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
static properties = {
|
||||
ttl: {type: 'float32'},
|
||||
};
|
||||
}
|
|
@ -10,6 +10,9 @@ export default class ClampPositions extends System {
|
|||
|
||||
tick() {
|
||||
const {AreaSize} = this.ecs.get(1);
|
||||
if (!AreaSize) {
|
||||
return;
|
||||
}
|
||||
for (const {Position} of this.ecs.changed(['Position'])) {
|
||||
if (Position.x < 0) {
|
||||
Position.x = 0;
|
||||
|
|
17
app/ecs/systems/kill-perishable.js
Normal file
17
app/ecs/systems/kill-perishable.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
import {System} from '@/ecs/index.js';
|
||||
|
||||
export default class KillPerishable extends System {
|
||||
|
||||
static queries() {
|
||||
return {
|
||||
default: ['Ttl'],
|
||||
};
|
||||
}
|
||||
|
||||
tick(elapsed) {
|
||||
for (const {Ttl} of this.select('default')) {
|
||||
Ttl.tick(elapsed);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -21,7 +21,10 @@ export default class MaintainColliderHash extends System {
|
|||
reindex(entities) {
|
||||
for (const id of entities) {
|
||||
if (1 === id) {
|
||||
this.hash = new SpatialHash(this.ecs.get(1).AreaSize);
|
||||
const {AreaSize} = this.ecs.get(1);
|
||||
if (AreaSize) {
|
||||
this.hash = new SpatialHash(AreaSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
super.reindex(entities);
|
||||
|
@ -31,7 +34,7 @@ export default class MaintainColliderHash extends System {
|
|||
}
|
||||
|
||||
updateHash(entity) {
|
||||
if (!entity.Collider) {
|
||||
if (!entity.Collider || !this.hash) {
|
||||
return;
|
||||
}
|
||||
this.hash.update(entity.Collider.aabb, entity.id);
|
||||
|
|
|
@ -21,15 +21,18 @@ export default class VisibleAabbs extends System {
|
|||
reindex(entities) {
|
||||
for (const id of entities) {
|
||||
if (1 === id) {
|
||||
const {x, y} = this.ecs.get(1).AreaSize;
|
||||
if (
|
||||
!this.hash ||
|
||||
(
|
||||
this.hash.area.x !== x
|
||||
|| this.hash.area.y !== y
|
||||
)
|
||||
) {
|
||||
this.hash = new SpatialHash(this.ecs.get(1).AreaSize);
|
||||
const {AreaSize} = this.ecs.get(1)
|
||||
if (AreaSize) {
|
||||
const {x, y} = AreaSize;
|
||||
if (
|
||||
!this.hash ||
|
||||
(
|
||||
this.hash.area.x !== x
|
||||
|| this.hash.area.y !== y
|
||||
)
|
||||
) {
|
||||
this.hash = new SpatialHash(this.ecs.get(1).AreaSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -40,6 +43,9 @@ export default class VisibleAabbs extends System {
|
|||
}
|
||||
|
||||
updateHash(entity) {
|
||||
if (!this.hash) {
|
||||
return;
|
||||
}
|
||||
if (!entity.VisibleAabb) {
|
||||
this.hash.remove(entity.id);
|
||||
return;
|
||||
|
@ -53,10 +59,7 @@ export default class VisibleAabbs extends System {
|
|||
if (VisibleAabb) {
|
||||
let size = undefined;
|
||||
if (Sprite) {
|
||||
const frame = Sprite.animation
|
||||
? Sprite.$$sourceJson.animations[Sprite.animation][Sprite.frame]
|
||||
: '';
|
||||
size = Sprite.$$sourceJson.frames[frame].sourceSize;
|
||||
size = Sprite.size;
|
||||
}
|
||||
/* v8 ignore next 3 */
|
||||
if (!size) {
|
||||
|
|
93
app/particles/emitter.js
Normal file
93
app/particles/emitter.js
Normal file
|
@ -0,0 +1,93 @@
|
|||
import K from 'kefir';
|
||||
|
||||
export default class Emitter {
|
||||
constructor(ecs) {
|
||||
this.ecs = ecs;
|
||||
this.scheduled = [];
|
||||
}
|
||||
async allocate({entity, shape}) {
|
||||
const allocated = this.ecs.get(await this.ecs.create(entity));
|
||||
if (shape) {
|
||||
switch (shape.type) {
|
||||
case 'filledRect': {
|
||||
allocated.Position.x += Math.random() * shape.payload.width - (shape.payload.width / 2);
|
||||
allocated.Position.y += Math.random() * shape.payload.height - (shape.payload.height / 2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return allocated;
|
||||
}
|
||||
emit(particle) {
|
||||
particle = {
|
||||
...particle,
|
||||
entity: {
|
||||
Position: {},
|
||||
Sprite: {},
|
||||
VisibleAabb: {},
|
||||
...particle.entity,
|
||||
},
|
||||
}
|
||||
let {count = 1} = particle;
|
||||
const {frequency = 0} = particle;
|
||||
const stream = K.stream((emitter) => {
|
||||
if (0 === frequency) {
|
||||
const promises = [];
|
||||
for (let i = 0; i < count; ++i) {
|
||||
promises.push(
|
||||
this.allocate(particle)
|
||||
.then((entity) => {
|
||||
emitter.emit(entity);
|
||||
}),
|
||||
);
|
||||
}
|
||||
Promise.all(promises)
|
||||
.then(() => {
|
||||
emitter.end();
|
||||
});
|
||||
return;
|
||||
}
|
||||
const promise = this.allocate(particle)
|
||||
.then((entity) => {
|
||||
emitter.emit(entity);
|
||||
});
|
||||
count -= 1;
|
||||
if (0 === count) {
|
||||
promise.then(() => {
|
||||
emitter.end();
|
||||
});
|
||||
return;
|
||||
}
|
||||
const promises = [promise];
|
||||
let accumulated = 0;
|
||||
const scheduled = (elapsed) => {
|
||||
accumulated += elapsed;
|
||||
while (accumulated > frequency && count > 0) {
|
||||
promises.push(
|
||||
this.allocate(particle)
|
||||
.then((entity) => {
|
||||
emitter.emit(entity);
|
||||
}),
|
||||
);
|
||||
accumulated -= frequency;
|
||||
count -= 1;
|
||||
}
|
||||
if (0 === count) {
|
||||
this.scheduled.splice(this.scheduled.indexOf(scheduled), 1);
|
||||
Promise.all(promises).then(() => {
|
||||
emitter.end();
|
||||
});
|
||||
}
|
||||
};
|
||||
this.scheduled.push(scheduled);
|
||||
});
|
||||
return stream;
|
||||
}
|
||||
|
||||
tick(elapsed) {
|
||||
for (const ticker of this.scheduled) {
|
||||
ticker(elapsed);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
52
app/particles/emitter.test.js
Normal file
52
app/particles/emitter.test.js
Normal file
|
@ -0,0 +1,52 @@
|
|||
import {expect, test} from 'vitest';
|
||||
|
||||
import Components from '@/ecs/components/index.js';
|
||||
import Ecs from '@/ecs/ecs.js';
|
||||
|
||||
import Emitter from './emitter.js';
|
||||
|
||||
test('emits particles at once', async () => {
|
||||
const ecs = new Ecs({
|
||||
Components,
|
||||
});
|
||||
const emitter = new Emitter(ecs);
|
||||
const stream = emitter.emit({
|
||||
count: 5,
|
||||
frequency: 0,
|
||||
entity: {},
|
||||
});
|
||||
expect(await stream.scan((r) => r + 1, 0).toPromise()).to.equal(5);
|
||||
});
|
||||
|
||||
test('emits particles over time', async () => {
|
||||
const ecs = new Ecs({
|
||||
Components,
|
||||
});
|
||||
const emitter = new Emitter(ecs);
|
||||
const stream = emitter.emit({
|
||||
count: 2,
|
||||
frequency: 0.1,
|
||||
});
|
||||
const current = stream.toProperty();
|
||||
expect(await new Promise((resolve) => {
|
||||
current.onValue(resolve);
|
||||
}))
|
||||
.to.deep.include({id: 1});
|
||||
expect(ecs.get(1))
|
||||
.to.not.be.undefined;
|
||||
expect(ecs.get(2))
|
||||
.to.be.undefined;
|
||||
emitter.tick(0.06);
|
||||
expect(await new Promise((resolve) => {
|
||||
current.onValue(resolve);
|
||||
}))
|
||||
.to.deep.include({id: 1});
|
||||
emitter.tick(0.06);
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
expect(await new Promise((resolve) => {
|
||||
current.onValue(resolve);
|
||||
}))
|
||||
.to.deep.include({id: 2});
|
||||
expect(ecs.get(2))
|
||||
.to.not.be.undefined;
|
||||
});
|
|
@ -23,7 +23,7 @@ export default class ClientEcs extends Ecs {
|
|||
if (!cache.has(uri)) {
|
||||
const {promise, resolve, reject} = withResolvers();
|
||||
cache.set(uri, promise);
|
||||
fetch(new URL(uri, window.location.origin))
|
||||
fetch(new URL(uri, location.origin))
|
||||
.then(async (response) => {
|
||||
resolve(response.ok ? response.arrayBuffer() : new ArrayBuffer(0));
|
||||
})
|
||||
|
|
36
app/react/components/particle-worker.js
Normal file
36
app/react/components/particle-worker.js
Normal file
|
@ -0,0 +1,36 @@
|
|||
import Emitter from '@/particles/emitter.js';
|
||||
import createEcs from '@/server/create/ecs.js';
|
||||
|
||||
import ClientEcs from './client-ecs.js';
|
||||
|
||||
const ecs = createEcs(ClientEcs);
|
||||
ecs.$$caret = Math.pow(2, 31);
|
||||
|
||||
const emitter = new Emitter(ecs);
|
||||
|
||||
addEventListener('message', (particle) => {
|
||||
if (!ecs.get(1)) {
|
||||
ecs.createManySpecific([[1, particle.data]]);
|
||||
return;
|
||||
}
|
||||
emitter.emit(particle.data)
|
||||
.onEnd(() => {});
|
||||
});
|
||||
|
||||
let last = Date.now();
|
||||
function tick() {
|
||||
const now = Date.now();
|
||||
const elapsed = (now - last) / 1000;
|
||||
last = now;
|
||||
if (ecs.get(1)) {
|
||||
ecs.tick(elapsed);
|
||||
emitter.tick(elapsed);
|
||||
if ('1' in ecs.diff) {
|
||||
delete ecs.diff['1'];
|
||||
}
|
||||
postMessage(ecs.diff);
|
||||
ecs.setClean();
|
||||
}
|
||||
requestAnimationFrame(tick);
|
||||
}
|
||||
requestAnimationFrame(tick);
|
|
@ -28,7 +28,7 @@ function calculateDarkness(hour) {
|
|||
return Math.floor(darkness * 1000) / 1000;
|
||||
}
|
||||
|
||||
export default function Ecs({applyFilters, camera, monopolizers, scale}) {
|
||||
export default function Ecs({applyFilters, camera, monopolizers, particleWorker, scale}) {
|
||||
const [ecs] = useEcs();
|
||||
const [filters, setFilters] = useState([]);
|
||||
const [mainEntity] = useMainEntity();
|
||||
|
@ -154,6 +154,7 @@ export default function Ecs({applyFilters, camera, monopolizers, scale}) {
|
|||
<Entities
|
||||
filters={filters}
|
||||
monopolizers={monopolizers}
|
||||
particleWorker={particleWorker}
|
||||
/>
|
||||
{projected?.length > 0 && layers[0] && (
|
||||
<TargetingGhost
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import {AdjustmentFilter} from '@pixi/filter-adjustment';
|
||||
import {GlowFilter} from '@pixi/filter-glow';
|
||||
import {Container} from '@pixi/react';
|
||||
import {useState} from 'react';
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
import {usePacket} from '@/react/context/client.js';
|
||||
import {useEcs, useEcsTick} from '@/react/context/ecs.js';
|
||||
|
@ -10,7 +10,7 @@ import {useRadians} from '@/react/context/radians.js';
|
|||
|
||||
import Entity from './entity.jsx';
|
||||
|
||||
export default function Entities({filters, monopolizers}) {
|
||||
export default function Entities({filters, monopolizers, particleWorker}) {
|
||||
const [ecs] = useEcs();
|
||||
const [entities, setEntities] = useState({});
|
||||
const [mainEntity] = useMainEntity();
|
||||
|
@ -20,6 +20,37 @@ export default function Entities({filters, monopolizers}) {
|
|||
new AdjustmentFilter(),
|
||||
new GlowFilter({color: 0x0}),
|
||||
]);
|
||||
useEffect(() => {
|
||||
if (!ecs || !particleWorker) {
|
||||
return;
|
||||
}
|
||||
async function onMessage(diff) {
|
||||
await ecs.apply(diff.data);
|
||||
const deleted = {};
|
||||
const updated = {};
|
||||
for (const id in diff.data) {
|
||||
if (!diff.data[id]) {
|
||||
deleted[id] = true;
|
||||
}
|
||||
else {
|
||||
updated[id] = ecs.get(id);
|
||||
}
|
||||
}
|
||||
setEntities((entities) => {
|
||||
for (const id in deleted) {
|
||||
delete entities[id];
|
||||
}
|
||||
return {
|
||||
...entities,
|
||||
...updated,
|
||||
};
|
||||
});
|
||||
}
|
||||
particleWorker.addEventListener('message', onMessage);
|
||||
return () => {
|
||||
particleWorker.removeEventListener('message', onMessage);
|
||||
};
|
||||
}, [ecs, particleWorker]);
|
||||
const pulse = (Math.cos(radians / 4) + 1) * 0.5;
|
||||
interactionFilters[0].brightness = (pulse * 0.75) + 1;
|
||||
interactionFilters[1].outerStrength = pulse * 0.5;
|
||||
|
@ -43,10 +74,9 @@ export default function Entities({filters, monopolizers}) {
|
|||
}
|
||||
updating[id] = ecs.get(id);
|
||||
if (update.Emitter?.emit) {
|
||||
updating[id].Emitter.emitting = {
|
||||
...updating[id].Emitter.emitting,
|
||||
...update.Emitter.emit,
|
||||
};
|
||||
for (const id in update.Emitter.emit) {
|
||||
particleWorker?.postMessage(update.Emitter.emit[id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
setEntities((entities) => {
|
||||
|
@ -58,7 +88,7 @@ export default function Entities({filters, monopolizers}) {
|
|||
...updating,
|
||||
};
|
||||
});
|
||||
}, [ecs]);
|
||||
}, [ecs, particleWorker]);
|
||||
useEcsTick(() => {
|
||||
if (!ecs) {
|
||||
return;
|
||||
|
|
|
@ -4,9 +4,8 @@ import {memo, useCallback} from 'react';
|
|||
import {useDebug} from '@/react/context/debug.js';
|
||||
import {useMainEntity} from '@/react/context/main-entity.js';
|
||||
|
||||
import Emitter from './emitter.jsx';
|
||||
import Light from './light.jsx';
|
||||
import Sprite from './sprite.jsx';
|
||||
import SpriteComponent from './sprite.jsx';
|
||||
|
||||
function Aabb({color, width = 0.5, x0, y0, x1, y1, ...rest}) {
|
||||
const draw = useCallback((g) => {
|
||||
|
@ -23,7 +22,7 @@ function Aabb({color, width = 0.5, x0, y0, x1, y1, ...rest}) {
|
|||
);
|
||||
}
|
||||
|
||||
function Crosshair({x, y}) {
|
||||
function Crosshair() {
|
||||
const draw = useCallback((g) => {
|
||||
g.clear();
|
||||
g.lineStyle(1, 0x000000);
|
||||
|
@ -42,7 +41,7 @@ function Crosshair({x, y}) {
|
|||
g.drawCircle(0, 0, 3);
|
||||
}, []);
|
||||
return (
|
||||
<Graphics draw={draw} x={x} y={y} />
|
||||
<Graphics draw={draw} />
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -52,31 +51,39 @@ function Entity({entity, ...rest}) {
|
|||
if (!entity) {
|
||||
return false;
|
||||
}
|
||||
const {Direction, id, Sprite} = entity;
|
||||
return (
|
||||
<Container
|
||||
zIndex={entity.Position?.y || 0}
|
||||
>
|
||||
{entity.Sprite && (
|
||||
<Sprite
|
||||
entity={entity}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
{entity.Emitter && (
|
||||
<Emitter
|
||||
entity={entity}
|
||||
/>
|
||||
)}
|
||||
{entity.Light && (
|
||||
<Light
|
||||
brightness={entity.Light.brightness}
|
||||
x={entity.Position.x}
|
||||
y={entity.Position.y}
|
||||
/>
|
||||
)}
|
||||
{debug && entity.Position && (
|
||||
<Crosshair x={entity.Position.x} y={entity.Position.y} />
|
||||
)}
|
||||
<>
|
||||
<Container
|
||||
x={entity.Position.x}
|
||||
y={entity.Position.y}
|
||||
zIndex={entity.Position?.y || 0}
|
||||
>
|
||||
{entity.Sprite && (
|
||||
<SpriteComponent
|
||||
alpha={Sprite.alpha}
|
||||
anchor={Sprite.anchor}
|
||||
animation={Sprite.animation}
|
||||
direction={Direction?.direction}
|
||||
frame={Sprite.frame}
|
||||
id={id}
|
||||
scale={Sprite.scale}
|
||||
rotates={Sprite.rotates}
|
||||
rotation={Sprite.rotation}
|
||||
source={Sprite.source}
|
||||
tint={Sprite.tint}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
{entity.Light && (
|
||||
<Light
|
||||
brightness={entity.Light.brightness}
|
||||
/>
|
||||
)}
|
||||
{debug && entity.Position && (
|
||||
<Crosshair />
|
||||
)}
|
||||
</Container>
|
||||
{debug && (
|
||||
<Aabb
|
||||
color={0xff00ff}
|
||||
|
@ -106,7 +113,7 @@ function Entity({entity, ...rest}) {
|
|||
{...entity.Interacts.aabb()}
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -3,13 +3,11 @@ import {PixiComponent} from '@pixi/react';
|
|||
import {PointLight} from './lights.js';
|
||||
|
||||
const LightInternal = PixiComponent('Light', {
|
||||
create({brightness, x, y}) {
|
||||
create({brightness}) {
|
||||
const light = new PointLight(
|
||||
0xffffff - 0x2244cc,
|
||||
0//brightness,
|
||||
brightness,
|
||||
);
|
||||
|
||||
light.position.set(x, y);
|
||||
// light.shader.program.fragmentSrc = light.shader.program.fragmentSrc.replace(
|
||||
// 'float D = length(lightVector)',
|
||||
// 'float D = length(lightVector) / 1.0',
|
||||
|
@ -24,17 +22,12 @@ const LightInternal = PixiComponent('Light', {
|
|||
// delete light.parentGroup;
|
||||
return light;
|
||||
},
|
||||
applyProps(light, oldProps, {x, y}) {
|
||||
light.position.set(x, y);
|
||||
},
|
||||
});
|
||||
|
||||
export default function Light({brightness, x, y}) {
|
||||
export default function Light({brightness}) {
|
||||
return (
|
||||
<LightInternal
|
||||
brightness={brightness}
|
||||
x={x}
|
||||
y={y}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -57,7 +57,7 @@ export const Stage = ({children, ...props}) => {
|
|||
);
|
||||
};
|
||||
|
||||
export default function Pixi({applyFilters, camera, monopolizers, scale}) {
|
||||
export default function Pixi({applyFilters, camera, monopolizers, particleWorker, scale}) {
|
||||
return (
|
||||
<Stage
|
||||
className={styles.stage}
|
||||
|
@ -71,6 +71,7 @@ export default function Pixi({applyFilters, camera, monopolizers, scale}) {
|
|||
applyFilters={applyFilters}
|
||||
camera={camera}
|
||||
monopolizers={monopolizers}
|
||||
particleWorker={particleWorker}
|
||||
scale={scale}
|
||||
/>
|
||||
</Stage>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {Sprite as PixiSprite} from '@pixi/react';
|
||||
import {useEffect, useState} from 'react';
|
||||
import {memo, useEffect, useState} from 'react';
|
||||
|
||||
import {useAsset} from '@/react/context/assets.js';
|
||||
|
||||
|
@ -22,11 +22,23 @@ function textureFromAsset(asset, animation, frame) {
|
|||
return texture;
|
||||
}
|
||||
|
||||
export default function Sprite({entity, ...rest}) {
|
||||
function Sprite(props) {
|
||||
const {
|
||||
alpha,
|
||||
anchor,
|
||||
animation,
|
||||
direction,
|
||||
frame,
|
||||
scale,
|
||||
rotates,
|
||||
rotation,
|
||||
source,
|
||||
tint,
|
||||
...rest
|
||||
} = props;
|
||||
const [mounted, setMounted] = useState();
|
||||
const [normals, setNormals] = useState();
|
||||
const [normalsMounted, setNormalsMounted] = useState();
|
||||
const {alpha, anchor, animation, frame, scale, rotates, rotation, source} = entity.Sprite;
|
||||
const asset = useAsset(source);
|
||||
const normalsAsset = useAsset(normals);
|
||||
useEffect(() => {
|
||||
|
@ -65,11 +77,10 @@ export default function Sprite({entity, ...rest}) {
|
|||
alpha={alpha}
|
||||
anchor={anchor}
|
||||
ref={setMounted}
|
||||
{...(rotates ? {rotation: entity.Direction.direction + rotation} : {})}
|
||||
{...(rotates ? {rotation: direction + rotation} : {})}
|
||||
scale={scale}
|
||||
texture={texture}
|
||||
x={Math.round(entity.Position.x)}
|
||||
y={Math.round(entity.Position.y)}
|
||||
{...(0 !== tint ? {tint} : {})}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
|
@ -78,14 +89,14 @@ export default function Sprite({entity, ...rest}) {
|
|||
alpha={alpha}
|
||||
anchor={anchor}
|
||||
ref={setNormalsMounted}
|
||||
{...(rotates ? {rotation: entity.Direction.direction + rotation} : {})}
|
||||
{...(rotates ? {rotation: direction + rotation} : {})}
|
||||
scale={scale}
|
||||
texture={normalsTexture}
|
||||
x={Math.round(entity.Position.x)}
|
||||
y={Math.round(entity.Position.y)}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default memo(Sprite);
|
||||
|
|
|
@ -10,12 +10,13 @@ const TargetingGhostInternal = PixiComponent('TargetingGhost', {
|
|||
create: () => {
|
||||
// Solid target square.
|
||||
const target = new Graphics();
|
||||
target.alpha = 0.7;
|
||||
target.lineStyle(1, 0xffffff);
|
||||
target.drawRect(0.5, 0.5, tileSize.x, tileSize.y);
|
||||
target.pivot = {x: tileSize.x / 2, y: tileSize.y / 2};
|
||||
// Inner spinny part.
|
||||
const targetInner = new Graphics();
|
||||
targetInner.alpha = 0.6;
|
||||
targetInner.alpha = 0.3;
|
||||
targetInner.lineStyle(3, 0x333333);
|
||||
targetInner.beginFill(0xdddddd);
|
||||
targetInner.pivot = {x: tileSize.x / 2, y: tileSize.y / 2};
|
||||
|
|
|
@ -62,6 +62,10 @@ function Ui({disconnected}) {
|
|||
const [isInventoryOpen, setIsInventoryOpen] = useState(false);
|
||||
const [externalInventory, setExternalInventory] = useState();
|
||||
const [externalInventorySlots, setExternalInventorySlots] = useState();
|
||||
const [particleWorker, setParticleWorker] = useState();
|
||||
const refreshEcs = useCallback(() => {
|
||||
setEcs(new ClientEcs({Components, Systems}));
|
||||
}, [Components, Systems, setEcs]);
|
||||
useEffect(() => {
|
||||
async function setEcsStuff() {
|
||||
const {default: Components} = await import('@/ecs/components/index.js');
|
||||
|
@ -72,8 +76,8 @@ function Ui({disconnected}) {
|
|||
setEcsStuff();
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
setEcs(new ClientEcs({Components, Systems}));
|
||||
}, [Components, setEcs, Systems]);
|
||||
refreshEcs();
|
||||
}, [refreshEcs]);
|
||||
useEffect(() => {
|
||||
let handle;
|
||||
if (disconnected) {
|
||||
|
@ -305,10 +309,10 @@ function Ui({disconnected}) {
|
|||
setScale,
|
||||
]);
|
||||
usePacket('EcsChange', async () => {
|
||||
setEcs(new ClientEcs({Components, Systems}));
|
||||
refreshEcs();
|
||||
setMainEntity(undefined);
|
||||
setMonopolizers([]);
|
||||
}, [Components, Systems, setEcs, setMainEntity]);
|
||||
}, [refreshEcs, setMainEntity, setMonopolizers]);
|
||||
usePacket('Tick', async (payload, client) => {
|
||||
if (0 === Object.keys(payload.ecs).length) {
|
||||
return;
|
||||
|
@ -316,6 +320,22 @@ function Ui({disconnected}) {
|
|||
await ecs.apply(payload.ecs);
|
||||
client.emitter.invoke(':Ecs', payload.ecs);
|
||||
}, [ecs]);
|
||||
useEcsTick((payload) => {
|
||||
if (!('1' in payload) || particleWorker) {
|
||||
return
|
||||
}
|
||||
const localParticleWorker = new Worker(
|
||||
new URL('./particle-worker.js', import.meta.url),
|
||||
{type: 'module'},
|
||||
);
|
||||
localParticleWorker.postMessage(ecs.get(1).toJSON());
|
||||
setParticleWorker((particleWorker) => {
|
||||
if (particleWorker) {
|
||||
particleWorker.terminate();
|
||||
}
|
||||
return localParticleWorker;
|
||||
});
|
||||
}, [particleWorker]);
|
||||
useEcsTick((payload) => {
|
||||
let localMainEntity = mainEntity;
|
||||
for (const id in payload) {
|
||||
|
@ -518,6 +538,7 @@ function Ui({disconnected}) {
|
|||
applyFilters={applyFilters}
|
||||
camera={camera}
|
||||
monopolizers={monopolizers}
|
||||
particleWorker={particleWorker}
|
||||
scale={scale}
|
||||
/>
|
||||
<Dom>
|
||||
|
@ -583,13 +604,15 @@ function Ui({disconnected}) {
|
|||
)}
|
||||
</Dom>
|
||||
</div>
|
||||
<div className={[styles.devtools, devtoolsIsOpen && styles.devtoolsIsOpen].filter(Boolean).join(' ')}>
|
||||
<Devtools
|
||||
applyFilters={applyFilters}
|
||||
eventsChannel={devEventsChannel}
|
||||
setApplyFilters={setApplyFilters}
|
||||
/>
|
||||
</div>
|
||||
{devtoolsIsOpen && (
|
||||
<div className={[styles.devtools, devtoolsIsOpen && styles.devtoolsIsOpen].filter(Boolean).join(' ')}>
|
||||
<Devtools
|
||||
applyFilters={applyFilters}
|
||||
eventsChannel={devEventsChannel}
|
||||
setApplyFilters={setApplyFilters}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import {Texture} from '@pixi/core';
|
||||
import {Assets} from '@pixi/assets';
|
||||
import {createContext, useContext, useEffect} from 'react';
|
||||
|
||||
|
@ -11,7 +12,7 @@ export function useAsset(source) {
|
|||
const [assets, setAssets] = useContext(context);
|
||||
useEffect(() => {
|
||||
if (!source) {
|
||||
return undefined;
|
||||
return;
|
||||
}
|
||||
if (!assets[source]) {
|
||||
if (!loading[source]) {
|
||||
|
@ -24,5 +25,5 @@ export function useAsset(source) {
|
|||
}
|
||||
}
|
||||
}, [assets, setAssets, source]);
|
||||
return source ? assets[source] : undefined;
|
||||
return source ? assets[source] : {data: {meta: {}}, textures: {'': Texture.WHITE}};
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ export default function createEcs(Ecs) {
|
|||
'Water',
|
||||
'Interactions',
|
||||
'InventoryCloser',
|
||||
'KillPerishable',
|
||||
];
|
||||
defaultSystems.forEach((defaultSystem) => {
|
||||
const System = ecs.system(defaultSystem);
|
||||
|
|
|
@ -33,7 +33,7 @@ export default async function createPlayer(id) {
|
|||
},
|
||||
},
|
||||
Health: {health: 100},
|
||||
Light: {brightness: 4},
|
||||
Light: {brightness: 0},
|
||||
Magnet: {strength: 24},
|
||||
Player: {},
|
||||
Position: {x: 128, y: 448},
|
||||
|
|
6
package-lock.json
generated
6
package-lock.json
generated
|
@ -26,6 +26,7 @@
|
|||
"express": "^4.18.2",
|
||||
"idb-keyval": "^6.2.1",
|
||||
"isbot": "^4.1.0",
|
||||
"kefir": "^3.8.8",
|
||||
"lru-cache": "^10.2.2",
|
||||
"morgan": "^1.10.0",
|
||||
"pixi.js": "^7.4.2",
|
||||
|
@ -12697,6 +12698,11 @@
|
|||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/kefir": {
|
||||
"version": "3.8.8",
|
||||
"resolved": "https://registry.npmjs.org/kefir/-/kefir-3.8.8.tgz",
|
||||
"integrity": "sha512-xWga7QCZsR2Wjy2vNL3Kq/irT+IwxwItEWycRRlT5yhqHZK2fmEhziP+LzcJBWSTAMranGKtGTQ6lFpyJS3+jA=="
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
"express": "^4.18.2",
|
||||
"idb-keyval": "^6.2.1",
|
||||
"isbot": "^4.1.0",
|
||||
"kefir": "^3.8.8",
|
||||
"lru-cache": "^10.2.2",
|
||||
"morgan": "^1.10.0",
|
||||
"pixi.js": "^7.4.2",
|
||||
|
|
|
@ -9,104 +9,30 @@ if (projected?.length > 0) {
|
|||
Controlled.locked = 1
|
||||
const [, direction] = Sprite.animation.split(':')
|
||||
|
||||
const dirtParticles = {
|
||||
behaviors: [
|
||||
{
|
||||
type: 'moveAcceleration',
|
||||
config: {
|
||||
accel: {
|
||||
x: 0,
|
||||
y: 200,
|
||||
},
|
||||
minStart: 0,
|
||||
maxStart: 0,
|
||||
rotate: false,
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'moveSpeed',
|
||||
config: {
|
||||
speed: {
|
||||
list: [
|
||||
{
|
||||
time: 0,
|
||||
value: 60
|
||||
},
|
||||
{
|
||||
time: 1,
|
||||
value: 10
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'rotation',
|
||||
config: {
|
||||
accel: 0,
|
||||
minSpeed: 0,
|
||||
maxSpeed: 0,
|
||||
minStart: 225,
|
||||
maxStart: 320
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'scale',
|
||||
config: {
|
||||
scale: {
|
||||
list: [
|
||||
{
|
||||
value: 0.25,
|
||||
time: 0,
|
||||
},
|
||||
{
|
||||
value: 0.125,
|
||||
time: 1,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'textureSingle',
|
||||
config: {
|
||||
texture: 'tileset/7',
|
||||
}
|
||||
},
|
||||
],
|
||||
lifetime: {
|
||||
min: 0.25,
|
||||
max: 0.25,
|
||||
},
|
||||
frequency: 0.01,
|
||||
emitterLifetime: 0.25,
|
||||
pos: {
|
||||
x: 0,
|
||||
y: 0
|
||||
},
|
||||
};
|
||||
|
||||
for (let i = 0; i < 2; ++i) {
|
||||
Sound.play('/assets/hoe/dig.wav');
|
||||
for (const {x, y} of projected) {
|
||||
Emitter.emit({
|
||||
...dirtParticles,
|
||||
behaviors: [
|
||||
...dirtParticles.behaviors,
|
||||
{
|
||||
type: 'spawnShape',
|
||||
config: {
|
||||
type: 'rect',
|
||||
data: {
|
||||
x: x * layer.tileSize.x,
|
||||
y: y * layer.tileSize.y,
|
||||
w: layer.tileSize.x,
|
||||
h: layer.tileSize.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
count: 25,
|
||||
frequency: 0.01,
|
||||
shape: {
|
||||
type: 'filledRect',
|
||||
payload: {width: 16, height: 16},
|
||||
},
|
||||
entity: {
|
||||
Forces: {forceY: -50},
|
||||
Position: {
|
||||
x: x * layer.tileSize.x + (layer.tileSize.x / 2),
|
||||
y: y * layer.tileSize.y + (layer.tileSize.y / 2),
|
||||
},
|
||||
Sprite: {
|
||||
scaleX: 0.2,
|
||||
scaleY: 0.2,
|
||||
tint: 0x552200,
|
||||
},
|
||||
Ttl: {ttl: 0.125},
|
||||
}
|
||||
});
|
||||
}
|
||||
Sprite.animation = ['moving', direction].join(':');
|
||||
await wait(0.3)
|
||||
|
|
|
@ -38,84 +38,27 @@ if (projected?.length > 0) {
|
|||
VisibleAabb: {},
|
||||
};
|
||||
|
||||
const seedParticles = {
|
||||
behaviors: [
|
||||
{
|
||||
type: 'moveAcceleration',
|
||||
config: {
|
||||
accel: {
|
||||
x: 0,
|
||||
y: 400,
|
||||
},
|
||||
minStart: 0,
|
||||
maxStart: 0,
|
||||
rotate: false,
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'moveSpeed',
|
||||
config: {
|
||||
speed: {
|
||||
list: [
|
||||
{
|
||||
time: 0,
|
||||
value: 100
|
||||
},
|
||||
{
|
||||
time: 1,
|
||||
value: 10
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'rotation',
|
||||
config: {
|
||||
accel: 0,
|
||||
minSpeed: 0,
|
||||
maxSpeed: 0,
|
||||
minStart: 225,
|
||||
maxStart: 320
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'scale',
|
||||
config: {
|
||||
scale: {
|
||||
list: [
|
||||
{
|
||||
value: 0.75,
|
||||
time: 0,
|
||||
},
|
||||
{
|
||||
value: 0.5,
|
||||
time: 1,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'textureSingle',
|
||||
config: {
|
||||
texture: 'tomato-plant/tomato-plant/0',
|
||||
}
|
||||
},
|
||||
],
|
||||
lifetime: {
|
||||
min: 0.25,
|
||||
max: 0.25,
|
||||
},
|
||||
Emitter.emit({
|
||||
count: 25,
|
||||
frequency: 0.01,
|
||||
emitterLifetime: 0.75,
|
||||
pos: {
|
||||
x: Position.x,
|
||||
y: Position.y,
|
||||
shape: {
|
||||
type: 'filledRect',
|
||||
payload: {width: 16, height: 16},
|
||||
},
|
||||
};
|
||||
|
||||
Emitter.emit(seedParticles)
|
||||
entity: {
|
||||
Forces: {forceY: -100},
|
||||
Position: {
|
||||
x: Position.x,
|
||||
y: Position.y,
|
||||
},
|
||||
Sprite: {
|
||||
scaleX: 0.125,
|
||||
scaleY: 0.125,
|
||||
tint: 0x221100,
|
||||
},
|
||||
Ttl: {ttl: 0.25},
|
||||
}
|
||||
});
|
||||
|
||||
Sound.play('/assets/sow.wav');
|
||||
Sprite.animation = ['moving', direction].join(':');
|
||||
|
|
|
@ -10,98 +10,32 @@ if (projected?.length > 0) {
|
|||
const [, direction] = Sprite.animation.split(':')
|
||||
Sprite.animation = ['idle', direction].join(':');
|
||||
|
||||
const waterParticles = {
|
||||
behaviors: [
|
||||
{
|
||||
type: 'moveAcceleration',
|
||||
config: {
|
||||
accel: {
|
||||
x: 0,
|
||||
y: 1500,
|
||||
},
|
||||
minStart: 0,
|
||||
maxStart: 0,
|
||||
rotate: false,
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'moveSpeed',
|
||||
config: {
|
||||
speed: {
|
||||
list: [
|
||||
{
|
||||
time: 0,
|
||||
value: 30
|
||||
},
|
||||
{
|
||||
time: 1,
|
||||
value: 0
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'scale',
|
||||
config: {
|
||||
scale: {
|
||||
list: [
|
||||
{
|
||||
value: 0.25,
|
||||
time: 0,
|
||||
},
|
||||
{
|
||||
value: 0.125,
|
||||
time: 1,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'textureSingle',
|
||||
config: {
|
||||
texture: 'tileset/38',
|
||||
}
|
||||
},
|
||||
],
|
||||
lifetime: {
|
||||
min: 0.25,
|
||||
max: 0.25,
|
||||
},
|
||||
frequency: 0.01,
|
||||
emitterLifetime: 0.25,
|
||||
pos: {
|
||||
x: 0,
|
||||
y: 0
|
||||
},
|
||||
rotation: 0,
|
||||
};
|
||||
|
||||
Sound.play('/assets/watering-can/water.wav');
|
||||
for (let i = 0; i < 2; ++i) {
|
||||
for (const {x, y} of projected) {
|
||||
Emitter.emit({
|
||||
...waterParticles,
|
||||
behaviors: [
|
||||
...waterParticles.behaviors,
|
||||
{
|
||||
type: 'spawnShape',
|
||||
config: {
|
||||
type: 'rect',
|
||||
data: {
|
||||
x: x * layer.tileSize.x,
|
||||
y: y * layer.tileSize.y - (layer.tileSize.y * 0.5),
|
||||
w: layer.tileSize.x,
|
||||
h: layer.tileSize.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
await wait(0.25);
|
||||
|
||||
for (const {x, y} of projected) {
|
||||
Emitter.emit({
|
||||
count: 25,
|
||||
frequency: 0.01,
|
||||
shape: {
|
||||
type: 'filledRect',
|
||||
payload: {width: 16, height: 16},
|
||||
},
|
||||
entity: {
|
||||
Forces: {forceY: 100},
|
||||
Position: {
|
||||
x: x * layer.tileSize.x + (layer.tileSize.x / 2),
|
||||
y: y * layer.tileSize.y - layer.tileSize.y,
|
||||
},
|
||||
Sprite: {
|
||||
scaleX: 0.2,
|
||||
scaleY: 0.2,
|
||||
tint: 0x0022aa,
|
||||
},
|
||||
Ttl: {ttl: 0.25},
|
||||
}
|
||||
});
|
||||
}
|
||||
await wait(0.5);
|
||||
|
||||
for (const {x, y} of projected) {
|
||||
const tileIndex = layer.area.x * y + x
|
||||
|
|
Loading…
Reference in New Issue
Block a user