refactor: particles
This commit is contained in:
parent
b719e3227b
commit
64df3b882f
|
@ -1,13 +1,38 @@
|
||||||
import Component from '@/ecs/component.js';
|
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() {
|
instanceFromSchema() {
|
||||||
const Component = this;
|
const Component = this;
|
||||||
|
const {ecs} = this;
|
||||||
return class EmitterInstance extends super.instanceFromSchema() {
|
return class EmitterInstance extends super.instanceFromSchema() {
|
||||||
emitting = {};
|
emitting = {};
|
||||||
id = 0;
|
id = 0;
|
||||||
emit(specification) {
|
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 {
|
export default class Sprite extends Component {
|
||||||
instanceFromSchema() {
|
instanceFromSchema() {
|
||||||
return class SpriteInstance extends super.instanceFromSchema() {
|
return class SpriteInstance extends super.instanceFromSchema() {
|
||||||
|
$$anchor = {x: 0.5, y: 0.5};
|
||||||
|
$$scale = {x: 1, y: 1};
|
||||||
$$sourceJson = {};
|
$$sourceJson = {};
|
||||||
get anchor() {
|
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() {
|
get animation() {
|
||||||
return super.animation;
|
return super.animation;
|
||||||
|
@ -56,7 +70,28 @@ export default class Sprite extends Component {
|
||||||
return this.$$sourceJson.meta.rotation;
|
return this.$$sourceJson.meta.rotation;
|
||||||
}
|
}
|
||||||
get scale() {
|
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) {
|
toNet(recipient, data) {
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
@ -66,7 +101,9 @@ export default class Sprite extends Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
async load(instance) {
|
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) {
|
markChange(entityId, key, value) {
|
||||||
if ('elapsed' === key) {
|
if ('elapsed' === key) {
|
||||||
|
@ -86,5 +123,6 @@ export default class Sprite extends Component {
|
||||||
scaleY: {defaultValue: 1, type: 'float32'},
|
scaleY: {defaultValue: 1, type: 'float32'},
|
||||||
source: {type: 'string'},
|
source: {type: 'string'},
|
||||||
speed: {type: 'float32'},
|
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() {
|
tick() {
|
||||||
const {AreaSize} = this.ecs.get(1);
|
const {AreaSize} = this.ecs.get(1);
|
||||||
|
if (!AreaSize) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
for (const {Position} of this.ecs.changed(['Position'])) {
|
for (const {Position} of this.ecs.changed(['Position'])) {
|
||||||
if (Position.x < 0) {
|
if (Position.x < 0) {
|
||||||
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) {
|
reindex(entities) {
|
||||||
for (const id of entities) {
|
for (const id of entities) {
|
||||||
if (1 === id) {
|
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);
|
super.reindex(entities);
|
||||||
|
@ -31,7 +34,7 @@ export default class MaintainColliderHash extends System {
|
||||||
}
|
}
|
||||||
|
|
||||||
updateHash(entity) {
|
updateHash(entity) {
|
||||||
if (!entity.Collider) {
|
if (!entity.Collider || !this.hash) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.hash.update(entity.Collider.aabb, entity.id);
|
this.hash.update(entity.Collider.aabb, entity.id);
|
||||||
|
|
|
@ -21,15 +21,18 @@ export default class VisibleAabbs extends System {
|
||||||
reindex(entities) {
|
reindex(entities) {
|
||||||
for (const id of entities) {
|
for (const id of entities) {
|
||||||
if (1 === id) {
|
if (1 === id) {
|
||||||
const {x, y} = this.ecs.get(1).AreaSize;
|
const {AreaSize} = this.ecs.get(1)
|
||||||
if (
|
if (AreaSize) {
|
||||||
!this.hash ||
|
const {x, y} = AreaSize;
|
||||||
(
|
if (
|
||||||
this.hash.area.x !== x
|
!this.hash ||
|
||||||
|| this.hash.area.y !== y
|
(
|
||||||
)
|
this.hash.area.x !== x
|
||||||
) {
|
|| this.hash.area.y !== y
|
||||||
this.hash = new SpatialHash(this.ecs.get(1).AreaSize);
|
)
|
||||||
|
) {
|
||||||
|
this.hash = new SpatialHash(this.ecs.get(1).AreaSize);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,6 +43,9 @@ export default class VisibleAabbs extends System {
|
||||||
}
|
}
|
||||||
|
|
||||||
updateHash(entity) {
|
updateHash(entity) {
|
||||||
|
if (!this.hash) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!entity.VisibleAabb) {
|
if (!entity.VisibleAabb) {
|
||||||
this.hash.remove(entity.id);
|
this.hash.remove(entity.id);
|
||||||
return;
|
return;
|
||||||
|
@ -53,10 +59,7 @@ export default class VisibleAabbs extends System {
|
||||||
if (VisibleAabb) {
|
if (VisibleAabb) {
|
||||||
let size = undefined;
|
let size = undefined;
|
||||||
if (Sprite) {
|
if (Sprite) {
|
||||||
const frame = Sprite.animation
|
size = Sprite.size;
|
||||||
? Sprite.$$sourceJson.animations[Sprite.animation][Sprite.frame]
|
|
||||||
: '';
|
|
||||||
size = Sprite.$$sourceJson.frames[frame].sourceSize;
|
|
||||||
}
|
}
|
||||||
/* v8 ignore next 3 */
|
/* v8 ignore next 3 */
|
||||||
if (!size) {
|
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)) {
|
if (!cache.has(uri)) {
|
||||||
const {promise, resolve, reject} = withResolvers();
|
const {promise, resolve, reject} = withResolvers();
|
||||||
cache.set(uri, promise);
|
cache.set(uri, promise);
|
||||||
fetch(new URL(uri, window.location.origin))
|
fetch(new URL(uri, location.origin))
|
||||||
.then(async (response) => {
|
.then(async (response) => {
|
||||||
resolve(response.ok ? response.arrayBuffer() : new ArrayBuffer(0));
|
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;
|
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 [ecs] = useEcs();
|
||||||
const [filters, setFilters] = useState([]);
|
const [filters, setFilters] = useState([]);
|
||||||
const [mainEntity] = useMainEntity();
|
const [mainEntity] = useMainEntity();
|
||||||
|
@ -154,6 +154,7 @@ export default function Ecs({applyFilters, camera, monopolizers, scale}) {
|
||||||
<Entities
|
<Entities
|
||||||
filters={filters}
|
filters={filters}
|
||||||
monopolizers={monopolizers}
|
monopolizers={monopolizers}
|
||||||
|
particleWorker={particleWorker}
|
||||||
/>
|
/>
|
||||||
{projected?.length > 0 && layers[0] && (
|
{projected?.length > 0 && layers[0] && (
|
||||||
<TargetingGhost
|
<TargetingGhost
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import {AdjustmentFilter} from '@pixi/filter-adjustment';
|
import {AdjustmentFilter} from '@pixi/filter-adjustment';
|
||||||
import {GlowFilter} from '@pixi/filter-glow';
|
import {GlowFilter} from '@pixi/filter-glow';
|
||||||
import {Container} from '@pixi/react';
|
import {Container} from '@pixi/react';
|
||||||
import {useState} from 'react';
|
import {useEffect, useState} from 'react';
|
||||||
|
|
||||||
import {usePacket} from '@/react/context/client.js';
|
import {usePacket} from '@/react/context/client.js';
|
||||||
import {useEcs, useEcsTick} from '@/react/context/ecs.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';
|
import Entity from './entity.jsx';
|
||||||
|
|
||||||
export default function Entities({filters, monopolizers}) {
|
export default function Entities({filters, monopolizers, particleWorker}) {
|
||||||
const [ecs] = useEcs();
|
const [ecs] = useEcs();
|
||||||
const [entities, setEntities] = useState({});
|
const [entities, setEntities] = useState({});
|
||||||
const [mainEntity] = useMainEntity();
|
const [mainEntity] = useMainEntity();
|
||||||
|
@ -20,6 +20,37 @@ export default function Entities({filters, monopolizers}) {
|
||||||
new AdjustmentFilter(),
|
new AdjustmentFilter(),
|
||||||
new GlowFilter({color: 0x0}),
|
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;
|
const pulse = (Math.cos(radians / 4) + 1) * 0.5;
|
||||||
interactionFilters[0].brightness = (pulse * 0.75) + 1;
|
interactionFilters[0].brightness = (pulse * 0.75) + 1;
|
||||||
interactionFilters[1].outerStrength = pulse * 0.5;
|
interactionFilters[1].outerStrength = pulse * 0.5;
|
||||||
|
@ -43,10 +74,9 @@ export default function Entities({filters, monopolizers}) {
|
||||||
}
|
}
|
||||||
updating[id] = ecs.get(id);
|
updating[id] = ecs.get(id);
|
||||||
if (update.Emitter?.emit) {
|
if (update.Emitter?.emit) {
|
||||||
updating[id].Emitter.emitting = {
|
for (const id in update.Emitter.emit) {
|
||||||
...updating[id].Emitter.emitting,
|
particleWorker?.postMessage(update.Emitter.emit[id]);
|
||||||
...update.Emitter.emit,
|
}
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setEntities((entities) => {
|
setEntities((entities) => {
|
||||||
|
@ -58,7 +88,7 @@ export default function Entities({filters, monopolizers}) {
|
||||||
...updating,
|
...updating,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}, [ecs]);
|
}, [ecs, particleWorker]);
|
||||||
useEcsTick(() => {
|
useEcsTick(() => {
|
||||||
if (!ecs) {
|
if (!ecs) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -4,9 +4,8 @@ import {memo, useCallback} from 'react';
|
||||||
import {useDebug} from '@/react/context/debug.js';
|
import {useDebug} from '@/react/context/debug.js';
|
||||||
import {useMainEntity} from '@/react/context/main-entity.js';
|
import {useMainEntity} from '@/react/context/main-entity.js';
|
||||||
|
|
||||||
import Emitter from './emitter.jsx';
|
|
||||||
import Light from './light.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}) {
|
function Aabb({color, width = 0.5, x0, y0, x1, y1, ...rest}) {
|
||||||
const draw = useCallback((g) => {
|
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) => {
|
const draw = useCallback((g) => {
|
||||||
g.clear();
|
g.clear();
|
||||||
g.lineStyle(1, 0x000000);
|
g.lineStyle(1, 0x000000);
|
||||||
|
@ -42,7 +41,7 @@ function Crosshair({x, y}) {
|
||||||
g.drawCircle(0, 0, 3);
|
g.drawCircle(0, 0, 3);
|
||||||
}, []);
|
}, []);
|
||||||
return (
|
return (
|
||||||
<Graphics draw={draw} x={x} y={y} />
|
<Graphics draw={draw} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,31 +51,39 @@ function Entity({entity, ...rest}) {
|
||||||
if (!entity) {
|
if (!entity) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
const {Direction, id, Sprite} = entity;
|
||||||
return (
|
return (
|
||||||
<Container
|
<>
|
||||||
zIndex={entity.Position?.y || 0}
|
<Container
|
||||||
>
|
x={entity.Position.x}
|
||||||
{entity.Sprite && (
|
y={entity.Position.y}
|
||||||
<Sprite
|
zIndex={entity.Position?.y || 0}
|
||||||
entity={entity}
|
>
|
||||||
{...rest}
|
{entity.Sprite && (
|
||||||
/>
|
<SpriteComponent
|
||||||
)}
|
alpha={Sprite.alpha}
|
||||||
{entity.Emitter && (
|
anchor={Sprite.anchor}
|
||||||
<Emitter
|
animation={Sprite.animation}
|
||||||
entity={entity}
|
direction={Direction?.direction}
|
||||||
/>
|
frame={Sprite.frame}
|
||||||
)}
|
id={id}
|
||||||
{entity.Light && (
|
scale={Sprite.scale}
|
||||||
<Light
|
rotates={Sprite.rotates}
|
||||||
brightness={entity.Light.brightness}
|
rotation={Sprite.rotation}
|
||||||
x={entity.Position.x}
|
source={Sprite.source}
|
||||||
y={entity.Position.y}
|
tint={Sprite.tint}
|
||||||
/>
|
{...rest}
|
||||||
)}
|
/>
|
||||||
{debug && entity.Position && (
|
)}
|
||||||
<Crosshair x={entity.Position.x} y={entity.Position.y} />
|
{entity.Light && (
|
||||||
)}
|
<Light
|
||||||
|
brightness={entity.Light.brightness}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{debug && entity.Position && (
|
||||||
|
<Crosshair />
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
{debug && (
|
{debug && (
|
||||||
<Aabb
|
<Aabb
|
||||||
color={0xff00ff}
|
color={0xff00ff}
|
||||||
|
@ -106,7 +113,7 @@ function Entity({entity, ...rest}) {
|
||||||
{...entity.Interacts.aabb()}
|
{...entity.Interacts.aabb()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Container>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,13 +3,11 @@ import {PixiComponent} from '@pixi/react';
|
||||||
import {PointLight} from './lights.js';
|
import {PointLight} from './lights.js';
|
||||||
|
|
||||||
const LightInternal = PixiComponent('Light', {
|
const LightInternal = PixiComponent('Light', {
|
||||||
create({brightness, x, y}) {
|
create({brightness}) {
|
||||||
const light = new PointLight(
|
const light = new PointLight(
|
||||||
0xffffff - 0x2244cc,
|
0xffffff - 0x2244cc,
|
||||||
0//brightness,
|
brightness,
|
||||||
);
|
);
|
||||||
|
|
||||||
light.position.set(x, y);
|
|
||||||
// light.shader.program.fragmentSrc = light.shader.program.fragmentSrc.replace(
|
// light.shader.program.fragmentSrc = light.shader.program.fragmentSrc.replace(
|
||||||
// 'float D = length(lightVector)',
|
// 'float D = length(lightVector)',
|
||||||
// 'float D = length(lightVector) / 1.0',
|
// 'float D = length(lightVector) / 1.0',
|
||||||
|
@ -24,17 +22,12 @@ const LightInternal = PixiComponent('Light', {
|
||||||
// delete light.parentGroup;
|
// delete light.parentGroup;
|
||||||
return light;
|
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 (
|
return (
|
||||||
<LightInternal
|
<LightInternal
|
||||||
brightness={brightness}
|
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 (
|
return (
|
||||||
<Stage
|
<Stage
|
||||||
className={styles.stage}
|
className={styles.stage}
|
||||||
|
@ -71,6 +71,7 @@ export default function Pixi({applyFilters, camera, monopolizers, scale}) {
|
||||||
applyFilters={applyFilters}
|
applyFilters={applyFilters}
|
||||||
camera={camera}
|
camera={camera}
|
||||||
monopolizers={monopolizers}
|
monopolizers={monopolizers}
|
||||||
|
particleWorker={particleWorker}
|
||||||
scale={scale}
|
scale={scale}
|
||||||
/>
|
/>
|
||||||
</Stage>
|
</Stage>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import {Sprite as PixiSprite} from '@pixi/react';
|
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';
|
import {useAsset} from '@/react/context/assets.js';
|
||||||
|
|
||||||
|
@ -22,11 +22,23 @@ function textureFromAsset(asset, animation, frame) {
|
||||||
return texture;
|
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 [mounted, setMounted] = useState();
|
||||||
const [normals, setNormals] = useState();
|
const [normals, setNormals] = useState();
|
||||||
const [normalsMounted, setNormalsMounted] = useState();
|
const [normalsMounted, setNormalsMounted] = useState();
|
||||||
const {alpha, anchor, animation, frame, scale, rotates, rotation, source} = entity.Sprite;
|
|
||||||
const asset = useAsset(source);
|
const asset = useAsset(source);
|
||||||
const normalsAsset = useAsset(normals);
|
const normalsAsset = useAsset(normals);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -65,11 +77,10 @@ export default function Sprite({entity, ...rest}) {
|
||||||
alpha={alpha}
|
alpha={alpha}
|
||||||
anchor={anchor}
|
anchor={anchor}
|
||||||
ref={setMounted}
|
ref={setMounted}
|
||||||
{...(rotates ? {rotation: entity.Direction.direction + rotation} : {})}
|
{...(rotates ? {rotation: direction + rotation} : {})}
|
||||||
scale={scale}
|
scale={scale}
|
||||||
texture={texture}
|
texture={texture}
|
||||||
x={Math.round(entity.Position.x)}
|
{...(0 !== tint ? {tint} : {})}
|
||||||
y={Math.round(entity.Position.y)}
|
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -78,14 +89,14 @@ export default function Sprite({entity, ...rest}) {
|
||||||
alpha={alpha}
|
alpha={alpha}
|
||||||
anchor={anchor}
|
anchor={anchor}
|
||||||
ref={setNormalsMounted}
|
ref={setNormalsMounted}
|
||||||
{...(rotates ? {rotation: entity.Direction.direction + rotation} : {})}
|
{...(rotates ? {rotation: direction + rotation} : {})}
|
||||||
scale={scale}
|
scale={scale}
|
||||||
texture={normalsTexture}
|
texture={normalsTexture}
|
||||||
x={Math.round(entity.Position.x)}
|
|
||||||
y={Math.round(entity.Position.y)}
|
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default memo(Sprite);
|
||||||
|
|
|
@ -10,12 +10,13 @@ const TargetingGhostInternal = PixiComponent('TargetingGhost', {
|
||||||
create: () => {
|
create: () => {
|
||||||
// Solid target square.
|
// Solid target square.
|
||||||
const target = new Graphics();
|
const target = new Graphics();
|
||||||
|
target.alpha = 0.7;
|
||||||
target.lineStyle(1, 0xffffff);
|
target.lineStyle(1, 0xffffff);
|
||||||
target.drawRect(0.5, 0.5, tileSize.x, tileSize.y);
|
target.drawRect(0.5, 0.5, tileSize.x, tileSize.y);
|
||||||
target.pivot = {x: tileSize.x / 2, y: tileSize.y / 2};
|
target.pivot = {x: tileSize.x / 2, y: tileSize.y / 2};
|
||||||
// Inner spinny part.
|
// Inner spinny part.
|
||||||
const targetInner = new Graphics();
|
const targetInner = new Graphics();
|
||||||
targetInner.alpha = 0.6;
|
targetInner.alpha = 0.3;
|
||||||
targetInner.lineStyle(3, 0x333333);
|
targetInner.lineStyle(3, 0x333333);
|
||||||
targetInner.beginFill(0xdddddd);
|
targetInner.beginFill(0xdddddd);
|
||||||
targetInner.pivot = {x: tileSize.x / 2, y: tileSize.y / 2};
|
targetInner.pivot = {x: tileSize.x / 2, y: tileSize.y / 2};
|
||||||
|
|
|
@ -62,6 +62,10 @@ function Ui({disconnected}) {
|
||||||
const [isInventoryOpen, setIsInventoryOpen] = useState(false);
|
const [isInventoryOpen, setIsInventoryOpen] = useState(false);
|
||||||
const [externalInventory, setExternalInventory] = useState();
|
const [externalInventory, setExternalInventory] = useState();
|
||||||
const [externalInventorySlots, setExternalInventorySlots] = useState();
|
const [externalInventorySlots, setExternalInventorySlots] = useState();
|
||||||
|
const [particleWorker, setParticleWorker] = useState();
|
||||||
|
const refreshEcs = useCallback(() => {
|
||||||
|
setEcs(new ClientEcs({Components, Systems}));
|
||||||
|
}, [Components, Systems, setEcs]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function setEcsStuff() {
|
async function setEcsStuff() {
|
||||||
const {default: Components} = await import('@/ecs/components/index.js');
|
const {default: Components} = await import('@/ecs/components/index.js');
|
||||||
|
@ -72,8 +76,8 @@ function Ui({disconnected}) {
|
||||||
setEcsStuff();
|
setEcsStuff();
|
||||||
}, []);
|
}, []);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setEcs(new ClientEcs({Components, Systems}));
|
refreshEcs();
|
||||||
}, [Components, setEcs, Systems]);
|
}, [refreshEcs]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let handle;
|
let handle;
|
||||||
if (disconnected) {
|
if (disconnected) {
|
||||||
|
@ -305,10 +309,10 @@ function Ui({disconnected}) {
|
||||||
setScale,
|
setScale,
|
||||||
]);
|
]);
|
||||||
usePacket('EcsChange', async () => {
|
usePacket('EcsChange', async () => {
|
||||||
setEcs(new ClientEcs({Components, Systems}));
|
refreshEcs();
|
||||||
setMainEntity(undefined);
|
setMainEntity(undefined);
|
||||||
setMonopolizers([]);
|
setMonopolizers([]);
|
||||||
}, [Components, Systems, setEcs, setMainEntity]);
|
}, [refreshEcs, setMainEntity, setMonopolizers]);
|
||||||
usePacket('Tick', async (payload, client) => {
|
usePacket('Tick', async (payload, client) => {
|
||||||
if (0 === Object.keys(payload.ecs).length) {
|
if (0 === Object.keys(payload.ecs).length) {
|
||||||
return;
|
return;
|
||||||
|
@ -316,6 +320,22 @@ function Ui({disconnected}) {
|
||||||
await ecs.apply(payload.ecs);
|
await ecs.apply(payload.ecs);
|
||||||
client.emitter.invoke(':Ecs', payload.ecs);
|
client.emitter.invoke(':Ecs', payload.ecs);
|
||||||
}, [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) => {
|
useEcsTick((payload) => {
|
||||||
let localMainEntity = mainEntity;
|
let localMainEntity = mainEntity;
|
||||||
for (const id in payload) {
|
for (const id in payload) {
|
||||||
|
@ -518,6 +538,7 @@ function Ui({disconnected}) {
|
||||||
applyFilters={applyFilters}
|
applyFilters={applyFilters}
|
||||||
camera={camera}
|
camera={camera}
|
||||||
monopolizers={monopolizers}
|
monopolizers={monopolizers}
|
||||||
|
particleWorker={particleWorker}
|
||||||
scale={scale}
|
scale={scale}
|
||||||
/>
|
/>
|
||||||
<Dom>
|
<Dom>
|
||||||
|
@ -583,13 +604,15 @@ function Ui({disconnected}) {
|
||||||
)}
|
)}
|
||||||
</Dom>
|
</Dom>
|
||||||
</div>
|
</div>
|
||||||
<div className={[styles.devtools, devtoolsIsOpen && styles.devtoolsIsOpen].filter(Boolean).join(' ')}>
|
{devtoolsIsOpen && (
|
||||||
<Devtools
|
<div className={[styles.devtools, devtoolsIsOpen && styles.devtoolsIsOpen].filter(Boolean).join(' ')}>
|
||||||
applyFilters={applyFilters}
|
<Devtools
|
||||||
eventsChannel={devEventsChannel}
|
applyFilters={applyFilters}
|
||||||
setApplyFilters={setApplyFilters}
|
eventsChannel={devEventsChannel}
|
||||||
/>
|
setApplyFilters={setApplyFilters}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import {Texture} from '@pixi/core';
|
||||||
import {Assets} from '@pixi/assets';
|
import {Assets} from '@pixi/assets';
|
||||||
import {createContext, useContext, useEffect} from 'react';
|
import {createContext, useContext, useEffect} from 'react';
|
||||||
|
|
||||||
|
@ -11,7 +12,7 @@ export function useAsset(source) {
|
||||||
const [assets, setAssets] = useContext(context);
|
const [assets, setAssets] = useContext(context);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!source) {
|
if (!source) {
|
||||||
return undefined;
|
return;
|
||||||
}
|
}
|
||||||
if (!assets[source]) {
|
if (!assets[source]) {
|
||||||
if (!loading[source]) {
|
if (!loading[source]) {
|
||||||
|
@ -24,5 +25,5 @@ export function useAsset(source) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [assets, setAssets, 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',
|
'Water',
|
||||||
'Interactions',
|
'Interactions',
|
||||||
'InventoryCloser',
|
'InventoryCloser',
|
||||||
|
'KillPerishable',
|
||||||
];
|
];
|
||||||
defaultSystems.forEach((defaultSystem) => {
|
defaultSystems.forEach((defaultSystem) => {
|
||||||
const System = ecs.system(defaultSystem);
|
const System = ecs.system(defaultSystem);
|
||||||
|
|
|
@ -33,7 +33,7 @@ export default async function createPlayer(id) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Health: {health: 100},
|
Health: {health: 100},
|
||||||
Light: {brightness: 4},
|
Light: {brightness: 0},
|
||||||
Magnet: {strength: 24},
|
Magnet: {strength: 24},
|
||||||
Player: {},
|
Player: {},
|
||||||
Position: {x: 128, y: 448},
|
Position: {x: 128, y: 448},
|
||||||
|
|
6
package-lock.json
generated
6
package-lock.json
generated
|
@ -26,6 +26,7 @@
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"idb-keyval": "^6.2.1",
|
"idb-keyval": "^6.2.1",
|
||||||
"isbot": "^4.1.0",
|
"isbot": "^4.1.0",
|
||||||
|
"kefir": "^3.8.8",
|
||||||
"lru-cache": "^10.2.2",
|
"lru-cache": "^10.2.2",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"pixi.js": "^7.4.2",
|
"pixi.js": "^7.4.2",
|
||||||
|
@ -12697,6 +12698,11 @@
|
||||||
"node": ">=4.0"
|
"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": {
|
"node_modules/keyv": {
|
||||||
"version": "4.5.4",
|
"version": "4.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||||
|
|
|
@ -33,6 +33,7 @@
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"idb-keyval": "^6.2.1",
|
"idb-keyval": "^6.2.1",
|
||||||
"isbot": "^4.1.0",
|
"isbot": "^4.1.0",
|
||||||
|
"kefir": "^3.8.8",
|
||||||
"lru-cache": "^10.2.2",
|
"lru-cache": "^10.2.2",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"pixi.js": "^7.4.2",
|
"pixi.js": "^7.4.2",
|
||||||
|
|
|
@ -9,104 +9,30 @@ if (projected?.length > 0) {
|
||||||
Controlled.locked = 1
|
Controlled.locked = 1
|
||||||
const [, direction] = Sprite.animation.split(':')
|
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) {
|
for (let i = 0; i < 2; ++i) {
|
||||||
Sound.play('/assets/hoe/dig.wav');
|
Sound.play('/assets/hoe/dig.wav');
|
||||||
for (const {x, y} of projected) {
|
for (const {x, y} of projected) {
|
||||||
Emitter.emit({
|
Emitter.emit({
|
||||||
...dirtParticles,
|
count: 25,
|
||||||
behaviors: [
|
frequency: 0.01,
|
||||||
...dirtParticles.behaviors,
|
shape: {
|
||||||
{
|
type: 'filledRect',
|
||||||
type: 'spawnShape',
|
payload: {width: 16, height: 16},
|
||||||
config: {
|
},
|
||||||
type: 'rect',
|
entity: {
|
||||||
data: {
|
Forces: {forceY: -50},
|
||||||
x: x * layer.tileSize.x,
|
Position: {
|
||||||
y: y * layer.tileSize.y,
|
x: x * layer.tileSize.x + (layer.tileSize.x / 2),
|
||||||
w: layer.tileSize.x,
|
y: y * layer.tileSize.y + (layer.tileSize.y / 2),
|
||||||
h: layer.tileSize.y,
|
},
|
||||||
}
|
Sprite: {
|
||||||
}
|
scaleX: 0.2,
|
||||||
}
|
scaleY: 0.2,
|
||||||
]
|
tint: 0x552200,
|
||||||
})
|
},
|
||||||
|
Ttl: {ttl: 0.125},
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
Sprite.animation = ['moving', direction].join(':');
|
Sprite.animation = ['moving', direction].join(':');
|
||||||
await wait(0.3)
|
await wait(0.3)
|
||||||
|
|
|
@ -38,84 +38,27 @@ if (projected?.length > 0) {
|
||||||
VisibleAabb: {},
|
VisibleAabb: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const seedParticles = {
|
Emitter.emit({
|
||||||
behaviors: [
|
count: 25,
|
||||||
{
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
frequency: 0.01,
|
frequency: 0.01,
|
||||||
emitterLifetime: 0.75,
|
shape: {
|
||||||
pos: {
|
type: 'filledRect',
|
||||||
x: Position.x,
|
payload: {width: 16, height: 16},
|
||||||
y: Position.y,
|
|
||||||
},
|
},
|
||||||
};
|
entity: {
|
||||||
|
Forces: {forceY: -100},
|
||||||
Emitter.emit(seedParticles)
|
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');
|
Sound.play('/assets/sow.wav');
|
||||||
Sprite.animation = ['moving', direction].join(':');
|
Sprite.animation = ['moving', direction].join(':');
|
||||||
|
|
|
@ -10,98 +10,32 @@ if (projected?.length > 0) {
|
||||||
const [, direction] = Sprite.animation.split(':')
|
const [, direction] = Sprite.animation.split(':')
|
||||||
Sprite.animation = ['idle', direction].join(':');
|
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');
|
Sound.play('/assets/watering-can/water.wav');
|
||||||
for (let i = 0; i < 2; ++i) {
|
|
||||||
for (const {x, y} of projected) {
|
for (const {x, y} of projected) {
|
||||||
Emitter.emit({
|
Emitter.emit({
|
||||||
...waterParticles,
|
count: 25,
|
||||||
behaviors: [
|
frequency: 0.01,
|
||||||
...waterParticles.behaviors,
|
shape: {
|
||||||
{
|
type: 'filledRect',
|
||||||
type: 'spawnShape',
|
payload: {width: 16, height: 16},
|
||||||
config: {
|
},
|
||||||
type: 'rect',
|
entity: {
|
||||||
data: {
|
Forces: {forceY: 100},
|
||||||
x: x * layer.tileSize.x,
|
Position: {
|
||||||
y: y * layer.tileSize.y - (layer.tileSize.y * 0.5),
|
x: x * layer.tileSize.x + (layer.tileSize.x / 2),
|
||||||
w: layer.tileSize.x,
|
y: y * layer.tileSize.y - layer.tileSize.y,
|
||||||
h: layer.tileSize.y,
|
},
|
||||||
}
|
Sprite: {
|
||||||
}
|
scaleX: 0.2,
|
||||||
}
|
scaleY: 0.2,
|
||||||
]
|
tint: 0x0022aa,
|
||||||
})
|
},
|
||||||
}
|
Ttl: {ttl: 0.25},
|
||||||
await wait(0.25);
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
await wait(0.5);
|
||||||
|
|
||||||
for (const {x, y} of projected) {
|
for (const {x, y} of projected) {
|
||||||
const tileIndex = layer.area.x * y + x
|
const tileIndex = layer.area.x * y + x
|
||||||
|
|
Loading…
Reference in New Issue
Block a user