Compare commits
52 Commits
c9e7b33d25
...
219ee71c2c
Author | SHA1 | Date | |
---|---|---|---|
|
219ee71c2c | ||
|
8e4bfaf6b7 | ||
|
b3f162b323 | ||
|
591c1201f4 | ||
|
65f1fe6270 | ||
|
06066e5c43 | ||
|
d8323ed9f3 | ||
|
02a124cb3e | ||
|
84ee8ee997 | ||
|
d9f5869a37 | ||
|
9afb5bba81 | ||
|
d6af0199c9 | ||
|
917465a35f | ||
|
14add7e8bb | ||
|
51bdda5eb9 | ||
|
b5698cd392 | ||
|
ecd26f0ddb | ||
|
c1a090688e | ||
|
4378ef2a12 | ||
|
030bf436c4 | ||
|
e99ae1136b | ||
|
91af56c9ff | ||
|
16a094806e | ||
|
eaec1d0022 | ||
|
623aabf525 | ||
|
172b457a8c | ||
|
097bf9505f | ||
|
4cb29e56cd | ||
|
2f5032762a | ||
|
29f6987a48 | ||
|
bf299e718e | ||
|
c4f2c4e7d4 | ||
|
b467874608 | ||
|
2e183559ad | ||
|
72d114ac74 | ||
|
770a63897d | ||
|
f5d092efaf | ||
|
38d76791f7 | ||
|
a1873c5295 | ||
|
c1eb862af2 | ||
|
05350e6ccf | ||
|
5d352bb367 | ||
|
0e5fcc3ea3 | ||
|
8583772652 | ||
|
93cb69e99a | ||
|
30caab6c9e | ||
|
a4949bd7a0 | ||
|
b53d7e3d35 | ||
|
cb3d9ad8c6 | ||
|
2997370e3b | ||
|
170cd4e0d1 | ||
|
e4cd769ee2 |
|
@ -704,7 +704,10 @@ export default class Sandbox {
|
||||||
switch (result.yield) {
|
switch (result.yield) {
|
||||||
case YIELD_PROMISE: {
|
case YIELD_PROMISE: {
|
||||||
stepResult.async = true;
|
stepResult.async = true;
|
||||||
stepResult.value = Promise.resolve(result.value)
|
const promise = result.value instanceof Promise
|
||||||
|
? result.value
|
||||||
|
: Promise.resolve(result.value);
|
||||||
|
promise
|
||||||
.then((value) => {
|
.then((value) => {
|
||||||
const top = this.$$execution.stack[this.$$execution.stack.length - 1];
|
const top = this.$$execution.stack[this.$$execution.stack.length - 1];
|
||||||
this.$$execution.deferred.set(top, {
|
this.$$execution.deferred.set(top, {
|
||||||
|
@ -712,6 +715,7 @@ export default class Sandbox {
|
||||||
yield: YIELD_NONE,
|
yield: YIELD_NONE,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
stepResult.value = promise;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case YIELD_LOOP_UPDATE: {
|
case YIELD_LOOP_UPDATE: {
|
||||||
|
|
|
@ -2,10 +2,9 @@ import Schema from './schema.js';
|
||||||
|
|
||||||
export default class Component {
|
export default class Component {
|
||||||
|
|
||||||
data = [];
|
|
||||||
ecs;
|
ecs;
|
||||||
Instance;
|
Instance;
|
||||||
map = {};
|
instances = {};
|
||||||
pool = [];
|
pool = [];
|
||||||
static properties = {};
|
static properties = {};
|
||||||
static $$schema;
|
static $$schema;
|
||||||
|
@ -21,7 +20,7 @@ export default class Component {
|
||||||
results.push(
|
results.push(
|
||||||
this.pool.length > 0
|
this.pool.length > 0
|
||||||
? this.pool.pop()
|
? this.pool.pop()
|
||||||
: this.data.push(new this.Instance()) - 1,
|
: new this.Instance(),
|
||||||
)
|
)
|
||||||
count -= 1;
|
count -= 1;
|
||||||
}
|
}
|
||||||
|
@ -38,35 +37,51 @@ export default class Component {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const allocated = this.allocateMany(entries.length);
|
const allocated = this.allocateMany(entries.length);
|
||||||
const {properties} = this.constructor.schema.specification.concrete;
|
const {
|
||||||
const Schema = this.constructor.schema.constructor;
|
constructor: Schema,
|
||||||
const keys = Object.keys(properties);
|
specification: {concrete: {properties}},
|
||||||
|
} = this.constructor.schema;
|
||||||
|
const defaults = {};
|
||||||
|
for (const key in properties) {
|
||||||
|
defaults[key] = ((key) => () => Schema.defaultValue(properties[key]))(key);
|
||||||
|
switch (properties[key].type) {
|
||||||
|
case 'float32':
|
||||||
|
case 'float64':
|
||||||
|
case 'int8':
|
||||||
|
case 'int16':
|
||||||
|
case 'int32':
|
||||||
|
case 'int64':
|
||||||
|
case 'string':
|
||||||
|
case 'uint8':
|
||||||
|
case 'uint16':
|
||||||
|
case 'uint32':
|
||||||
|
case 'uint64': {
|
||||||
|
defaults[key] = defaults[key]();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const keys = new Set(Object.keys(defaults));
|
||||||
const promises = [];
|
const promises = [];
|
||||||
for (let i = 0; i < entries.length; ++i) {
|
for (let i = 0; i < entries.length; ++i) {
|
||||||
const [entityId, values = {}] = entries[i];
|
const [entityId, values] = entries[i];
|
||||||
this.map[entityId] = allocated[i];
|
const instance = allocated[i];
|
||||||
this.data[allocated[i]].entity = entityId;
|
instance.entity = entityId;
|
||||||
for (let k = 0; k < keys.length; ++k) {
|
this.instances[entityId] = instance;
|
||||||
const j = keys[k];
|
for (const key in values) {
|
||||||
const instance = this.data[allocated[i]];
|
keys.delete(key);
|
||||||
if (j in values) {
|
|
||||||
instance[j] = values[j];
|
|
||||||
}
|
}
|
||||||
else {
|
const defaultValues = {};
|
||||||
const defaultValue = Schema.defaultValue(properties[j]);
|
for (const key of keys) {
|
||||||
if ('undefined' !== typeof defaultValue) {
|
defaultValues[key] = 'function' === typeof defaults[key]
|
||||||
instance[j] = defaultValue;
|
? defaults[key]()
|
||||||
|
: defaults[key];
|
||||||
}
|
}
|
||||||
}
|
instance.initialize(values, defaultValues);
|
||||||
}
|
promises.push(this.load(instance));
|
||||||
promises.push(this.load(this.data[allocated[i]]));
|
|
||||||
}
|
}
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
const created = [];
|
return allocated;
|
||||||
for (let i = 0; i < allocated.length; ++i) {
|
|
||||||
created.push(this.data[allocated[i]]);
|
|
||||||
}
|
|
||||||
return created;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deserialize(entityId, view, offset) {
|
deserialize(entityId, view, offset) {
|
||||||
|
@ -79,22 +94,18 @@ export default class Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy(entityId) {
|
destroy(entityId) {
|
||||||
this.destroyMany([entityId]);
|
this.destroyMany(new Set([entityId]));
|
||||||
}
|
}
|
||||||
|
|
||||||
destroyMany(entities) {
|
destroyMany(entityIds) {
|
||||||
this.freeMany(
|
for (const entityId of entityIds) {
|
||||||
entities
|
const instance = this.instances[entityId];
|
||||||
.map((entityId) => {
|
if ('undefined' === typeof instance) {
|
||||||
if ('undefined' !== typeof this.map[entityId]) {
|
|
||||||
return this.map[entityId];
|
|
||||||
}
|
|
||||||
throw new Error(`can't free for non-existent id ${entityId}`);
|
throw new Error(`can't free for non-existent id ${entityId}`);
|
||||||
}),
|
}
|
||||||
);
|
instance.destroy();
|
||||||
for (let i = 0; i < entities.length; i++) {
|
this.pool.push(instance);
|
||||||
this.data[this.map[entities[i]]].destroy();
|
delete this.instances[entityId];
|
||||||
this.map[entities[i]] = undefined;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,14 +121,8 @@ export default class Component {
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
freeMany(indices) {
|
|
||||||
for (let i = 0; i < indices.length; ++i) {
|
|
||||||
this.pool.push(indices[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get(entityId) {
|
get(entityId) {
|
||||||
return this.data[this.map[entityId]];
|
return this.instances[entityId];
|
||||||
}
|
}
|
||||||
|
|
||||||
async insertMany(entities) {
|
async insertMany(entities) {
|
||||||
|
@ -127,25 +132,25 @@ export default class Component {
|
||||||
instanceFromSchema() {
|
instanceFromSchema() {
|
||||||
const Component = this;
|
const Component = this;
|
||||||
const {concrete} = Component.constructor.schema.specification;
|
const {concrete} = Component.constructor.schema.specification;
|
||||||
const Schema = Component.constructor.schema.constructor;
|
|
||||||
const Instance = class {
|
const Instance = class {
|
||||||
$$entity = 0;
|
$$entity = 0;
|
||||||
constructor() {
|
|
||||||
this.$$reset();
|
|
||||||
}
|
|
||||||
$$reset() {
|
|
||||||
for (const key in concrete.properties) {
|
|
||||||
this[`$$${key}`] = Schema.defaultValue(concrete.properties[key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
destroy() {}
|
destroy() {}
|
||||||
|
initialize(values, defaults) {
|
||||||
|
for (const key in values) {
|
||||||
|
this[`$$${key}`] = values[key];
|
||||||
|
}
|
||||||
|
for (const key in defaults) {
|
||||||
|
this[`$$${key}`] = defaults[key];
|
||||||
|
}
|
||||||
|
Component.ecs.markChange(this.entity, {[Component.constructor.componentName]: values})
|
||||||
|
}
|
||||||
toNet(recipient, data) {
|
toNet(recipient, data) {
|
||||||
return data || Component.constructor.filterDefaults(this);
|
return data || Component.constructor.filterDefaults(this);
|
||||||
}
|
}
|
||||||
toJSON() {
|
toJSON() {
|
||||||
return Component.constructor.filterDefaults(this);
|
return Component.constructor.filterDefaults(this);
|
||||||
}
|
}
|
||||||
update(values) {
|
async update(values) {
|
||||||
for (const key in values) {
|
for (const key in values) {
|
||||||
if (concrete.properties[key]) {
|
if (concrete.properties[key]) {
|
||||||
this[`$$${key}`] = values[key];
|
this[`$$${key}`] = values[key];
|
||||||
|
@ -163,7 +168,6 @@ export default class Component {
|
||||||
},
|
},
|
||||||
set: function set(v) {
|
set: function set(v) {
|
||||||
this.$$entity = v;
|
this.$$entity = v;
|
||||||
this.$$reset();
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
for (const key in concrete.properties) {
|
for (const key in concrete.properties) {
|
||||||
|
@ -214,10 +218,12 @@ export default class Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateMany(entities) {
|
async updateMany(entities) {
|
||||||
|
const promises = [];
|
||||||
for (let i = 0; i < entities.length; i++) {
|
for (let i = 0; i < entities.length; i++) {
|
||||||
const [entityId, values] = entities[i];
|
const [entityId, values] = entities[i];
|
||||||
this.get(entityId).update(values);
|
promises.push(this.get(entityId).update(values));
|
||||||
}
|
}
|
||||||
|
return Promise.all(promises);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ export default class Alive extends Component {
|
||||||
this.$$dead = true;
|
this.$$dead = true;
|
||||||
const {Ticking} = ecs.get(this.entity);
|
const {Ticking} = ecs.get(this.entity);
|
||||||
if (Ticking) {
|
if (Ticking) {
|
||||||
|
this.$$death.context.entity = ecs.get(this.entity);
|
||||||
const ticker = this.$$death.ticker();
|
const ticker = this.$$death.ticker();
|
||||||
ecs.addDestructionDependency(this.entity.id, ticker);
|
ecs.addDestructionDependency(this.entity.id, ticker);
|
||||||
Ticking.add(ticker);
|
Ticking.add(ticker);
|
||||||
|
@ -35,7 +36,6 @@ export default class Alive extends Component {
|
||||||
instance.deathScript,
|
instance.deathScript,
|
||||||
{
|
{
|
||||||
ecs: this.ecs,
|
ecs: this.ecs,
|
||||||
entity: this.ecs.get(instance.entity),
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (0 === instance.maxHealth) {
|
if (0 === instance.maxHealth) {
|
||||||
|
|
|
@ -2,11 +2,13 @@ import Component from '@/ecs/component.js';
|
||||||
|
|
||||||
export default class Behaving extends Component {
|
export default class Behaving extends Component {
|
||||||
instanceFromSchema() {
|
instanceFromSchema() {
|
||||||
|
const {ecs} = this;
|
||||||
return class BehavingInstance extends super.instanceFromSchema() {
|
return class BehavingInstance extends super.instanceFromSchema() {
|
||||||
$$routineInstances = {};
|
$$routineInstances = {};
|
||||||
tick(elapsed) {
|
tick(elapsed) {
|
||||||
const routine = this.$$routineInstances[this.currentRoutine];
|
const routine = this.$$routineInstances[this.currentRoutine];
|
||||||
if (routine) {
|
if (routine) {
|
||||||
|
routine.context.entity = ecs.get(this.entity);
|
||||||
routine.tick(elapsed);
|
routine.tick(elapsed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,12 +22,7 @@ export default class Behaving extends Component {
|
||||||
const promises = [];
|
const promises = [];
|
||||||
for (const key in instance.routines) {
|
for (const key in instance.routines) {
|
||||||
promises.push(
|
promises.push(
|
||||||
this.ecs.readScript(
|
this.ecs.readScript(instance.routines[key])
|
||||||
instance.routines[key],
|
|
||||||
{
|
|
||||||
entity: this.ecs.get(instance.entity),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.then((script) => {
|
.then((script) => {
|
||||||
instance.$$routineInstances[key] = script;
|
instance.$$routineInstances[key] = script;
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -106,6 +106,7 @@ export default class Collider extends Component {
|
||||||
if (!hasMatchingIntersection) {
|
if (!hasMatchingIntersection) {
|
||||||
if (this.$$collisionStart) {
|
if (this.$$collisionStart) {
|
||||||
const script = this.$$collisionStart.clone();
|
const script = this.$$collisionStart.clone();
|
||||||
|
script.context.entity = thisEntity;
|
||||||
script.context.other = otherEntity;
|
script.context.other = otherEntity;
|
||||||
script.context.pair = [body, otherBody];
|
script.context.pair = [body, otherBody];
|
||||||
const ticker = script.ticker();
|
const ticker = script.ticker();
|
||||||
|
@ -115,6 +116,7 @@ export default class Collider extends Component {
|
||||||
}
|
}
|
||||||
if (other.$$collisionStart) {
|
if (other.$$collisionStart) {
|
||||||
const script = other.$$collisionStart.clone();
|
const script = other.$$collisionStart.clone();
|
||||||
|
script.context.entity = otherEntity;
|
||||||
script.context.other = thisEntity;
|
script.context.other = thisEntity;
|
||||||
script.context.pair = [otherBody, body];
|
script.context.pair = [otherBody, body];
|
||||||
const ticker = script.ticker();
|
const ticker = script.ticker();
|
||||||
|
@ -229,7 +231,7 @@ export default class Collider extends Component {
|
||||||
this.$$aabb = {x0: Infinity, x1: -Infinity, y0: Infinity, y1: -Infinity};
|
this.$$aabb = {x0: Infinity, x1: -Infinity, y0: Infinity, y1: -Infinity};
|
||||||
this.$$aabbs = [];
|
this.$$aabbs = [];
|
||||||
const {bodies} = this;
|
const {bodies} = this;
|
||||||
const {Direction: {direction = 0} = {}} = ecs.get(this.entity);
|
const {Direction: {direction = 0} = {}} = ecs.get(this.entity) || {};
|
||||||
for (const body of bodies) {
|
for (const body of bodies) {
|
||||||
let x0 = Infinity, x1 = -Infinity, y0 = Infinity, y1 = -Infinity;
|
let x0 = Infinity, x1 = -Infinity, y0 = Infinity, y1 = -Infinity;
|
||||||
for (const point of transform(body.points, {rotation: direction})) {
|
for (const point of transform(body.points, {rotation: direction})) {
|
||||||
|
@ -271,14 +273,12 @@ export default class Collider extends Component {
|
||||||
instance.collisionEndScript,
|
instance.collisionEndScript,
|
||||||
{
|
{
|
||||||
ecs: this.ecs,
|
ecs: this.ecs,
|
||||||
entity: this.ecs.get(instance.entity),
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
instance.$$collisionStart = await this.ecs.readScript(
|
instance.$$collisionStart = await this.ecs.readScript(
|
||||||
instance.collisionStartScript,
|
instance.collisionStartScript,
|
||||||
{
|
{
|
||||||
ecs: this.ecs,
|
ecs: this.ecs,
|
||||||
entity: this.ecs.get(instance.entity),
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,9 +6,13 @@ const Gathered = gather(
|
||||||
|
|
||||||
const Components = {};
|
const Components = {};
|
||||||
for (const componentName in Gathered) {
|
for (const componentName in Gathered) {
|
||||||
Components[componentName] = class Named extends Gathered[componentName] {
|
Components[componentName] = eval(`
|
||||||
static componentName = componentName;
|
((Gathered) => (
|
||||||
};
|
class ${componentName} extends Gathered['${componentName}'] {
|
||||||
|
static componentName = '${componentName}';
|
||||||
|
}
|
||||||
|
))
|
||||||
|
`)(Gathered);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Components;
|
export default Components;
|
||||||
|
|
|
@ -8,7 +8,8 @@ export default class Interactive extends Component {
|
||||||
interact(initiator) {
|
interact(initiator) {
|
||||||
const script = this.$$interact.clone();
|
const script = this.$$interact.clone();
|
||||||
script.context.initiator = initiator;
|
script.context.initiator = initiator;
|
||||||
const {Ticking} = ecs.get(this.entity);
|
script.context.subject = ecs.get(this.entity);
|
||||||
|
const {Ticking} = script.context.subject;
|
||||||
Ticking.add(script.ticker());
|
Ticking.add(script.ticker());
|
||||||
}
|
}
|
||||||
get interacting() {
|
get interacting() {
|
||||||
|
@ -28,7 +29,6 @@ export default class Interactive extends Component {
|
||||||
instance.interactScript,
|
instance.interactScript,
|
||||||
{
|
{
|
||||||
ecs: this.ecs,
|
ecs: this.ecs,
|
||||||
subject: this.ecs.get(instance.entity),
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
import Component from '@/ecs/component.js';
|
import Component from '@/ecs/component.js';
|
||||||
|
import {hexToHsl, hslToHex} from '@/util/color.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};
|
$$anchor = {x: 0.5, y: 0.5};
|
||||||
|
$$hue = 0;
|
||||||
|
$$saturation = 0;
|
||||||
|
$$lightness = 1;
|
||||||
$$scale = {x: 1, y: 1};
|
$$scale = {x: 1, y: 1};
|
||||||
$$sourceJson = {};
|
$$sourceJson = {};
|
||||||
get anchor() {
|
get anchor() {
|
||||||
|
@ -14,12 +18,14 @@ export default class Sprite extends Component {
|
||||||
}
|
}
|
||||||
set anchorX(anchorX) {
|
set anchorX(anchorX) {
|
||||||
this.$$anchor = {x: anchorX, y: this.anchorY};
|
this.$$anchor = {x: anchorX, y: this.anchorY};
|
||||||
|
super.anchorX = anchorX;
|
||||||
}
|
}
|
||||||
get anchorY() {
|
get anchorY() {
|
||||||
return this.$$anchor.y;
|
return this.$$anchor.y;
|
||||||
}
|
}
|
||||||
set anchorY(anchorY) {
|
set anchorY(anchorY) {
|
||||||
this.$$anchor = {x: this.anchorX, y: anchorY};
|
this.$$anchor = {x: this.anchorX, y: anchorY};
|
||||||
|
super.anchorY = anchorY;
|
||||||
}
|
}
|
||||||
get animation() {
|
get animation() {
|
||||||
return super.animation;
|
return super.animation;
|
||||||
|
@ -57,6 +63,37 @@ export default class Sprite extends Component {
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
get hue() {
|
||||||
|
return this.$$hue;
|
||||||
|
}
|
||||||
|
set hue(hue) {
|
||||||
|
this.$$hue = hue;
|
||||||
|
const [, s, l] = hexToHsl(this.$$tint);
|
||||||
|
super.tint = hslToHex(hue, s, l);
|
||||||
|
}
|
||||||
|
initialize(values, defaults) {
|
||||||
|
let {
|
||||||
|
animation = defaults.animation,
|
||||||
|
anchorX = defaults.anchorX,
|
||||||
|
anchorY = defaults.anchorY,
|
||||||
|
frame = defaults.frame,
|
||||||
|
scaleX = defaults.scaleX,
|
||||||
|
scaleY = defaults.scaleY,
|
||||||
|
} = values;
|
||||||
|
this.$$anchor = {x: anchorX, y: anchorY};
|
||||||
|
this.$$scale = {x: scaleX, y: scaleY};
|
||||||
|
super.initialize(values, defaults);
|
||||||
|
this.frame = frame;
|
||||||
|
this.animation = animation;
|
||||||
|
}
|
||||||
|
get lightness() {
|
||||||
|
return this.$$lightness;
|
||||||
|
}
|
||||||
|
set lightness(lightness) {
|
||||||
|
this.$$lightness = lightness;
|
||||||
|
const [h, s] = hexToHsl(this.$$tint);
|
||||||
|
super.tint = hslToHex(h, s, lightness);
|
||||||
|
}
|
||||||
get rotates() {
|
get rotates() {
|
||||||
if (!this.$$sourceJson.meta) {
|
if (!this.$$sourceJson.meta) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -69,20 +106,37 @@ export default class Sprite extends Component {
|
||||||
}
|
}
|
||||||
return this.$$sourceJson.meta.rotation;
|
return this.$$sourceJson.meta.rotation;
|
||||||
}
|
}
|
||||||
|
get saturation() {
|
||||||
|
return this.$$saturation;
|
||||||
|
}
|
||||||
|
set saturation(saturation) {
|
||||||
|
this.$$saturation = saturation;
|
||||||
|
const [h, , l] = hexToHsl(this.$$tint);
|
||||||
|
super.tint = hslToHex(h, saturation, l);
|
||||||
|
}
|
||||||
get scale() {
|
get scale() {
|
||||||
return this.$$scale;
|
return this.$$scale;
|
||||||
}
|
}
|
||||||
|
set scale(scale) {
|
||||||
|
if ('number' === typeof scale) {
|
||||||
|
scale = {x: scale, y: scale};
|
||||||
|
}
|
||||||
|
this.scaleX = scale.x;
|
||||||
|
this.scaleY = scale.y;
|
||||||
|
}
|
||||||
get scaleX() {
|
get scaleX() {
|
||||||
return this.$$scale.x;
|
return this.$$scale.x;
|
||||||
}
|
}
|
||||||
set scaleX(scaleX) {
|
set scaleX(scaleX) {
|
||||||
this.$$scale = {x: scaleX, y: this.scaleY};
|
this.$$scale = {x: scaleX, y: this.scaleY};
|
||||||
|
super.scaleX = scaleX;
|
||||||
}
|
}
|
||||||
get scaleY() {
|
get scaleY() {
|
||||||
return this.$$scale.y;
|
return this.$$scale.y;
|
||||||
}
|
}
|
||||||
set scaleY(scaleY) {
|
set scaleY(scaleY) {
|
||||||
this.$$scale = {x: this.scaleX, y: scaleY};
|
this.$$scale = {x: this.scaleX, y: scaleY};
|
||||||
|
super.scaleY = scaleY;
|
||||||
}
|
}
|
||||||
get size() {
|
get size() {
|
||||||
if (!this.$$sourceJson.frames) {
|
if (!this.$$sourceJson.frames) {
|
||||||
|
@ -93,11 +147,48 @@ export default class Sprite extends Component {
|
||||||
: '';
|
: '';
|
||||||
return this.$$sourceJson.frames[frame].sourceSize;
|
return this.$$sourceJson.frames[frame].sourceSize;
|
||||||
}
|
}
|
||||||
|
get tint() {
|
||||||
|
return super.tint;
|
||||||
|
}
|
||||||
|
set tint(tint) {
|
||||||
|
[
|
||||||
|
this.$$hue,
|
||||||
|
this.$$saturation,
|
||||||
|
this.$$lightness,
|
||||||
|
] = hexToHsl(tint)
|
||||||
|
super.tint = tint;
|
||||||
|
}
|
||||||
toNet(recipient, data) {
|
toNet(recipient, data) {
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
const {elapsed, ...rest} = super.toNet(recipient, data);
|
const {elapsed, ...rest} = super.toNet(recipient, data);
|
||||||
return rest;
|
return rest;
|
||||||
}
|
}
|
||||||
|
update(values) {
|
||||||
|
for (const key in values) {
|
||||||
|
switch (key) {
|
||||||
|
case 'anchorX': {
|
||||||
|
this.$$anchor.x = values[key];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'anchorY': {
|
||||||
|
this.$$anchor.y = values[key];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'scaleX': {
|
||||||
|
this.$$scale.x = values[key];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'scaleY': {
|
||||||
|
this.$$scale.y = values[key];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.update(values);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
async load(instance) {
|
async load(instance) {
|
||||||
|
@ -123,6 +214,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'},
|
tint: {defaultValue: 0xffffff, type: 'uint32'},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,9 +5,15 @@ export default class Ttl extends Component {
|
||||||
const {ecs} = this;
|
const {ecs} = this;
|
||||||
return class TtlInstance extends super.instanceFromSchema() {
|
return class TtlInstance extends super.instanceFromSchema() {
|
||||||
$$elapsed = 0;
|
$$elapsed = 0;
|
||||||
$$reset() {
|
destroy() {
|
||||||
this.$$elapsed = 0;
|
this.$$elapsed = 0;
|
||||||
}
|
}
|
||||||
|
get elapsed() {
|
||||||
|
return this.$$elapsed;
|
||||||
|
}
|
||||||
|
set elapsed(elapsed) {
|
||||||
|
this.$$elapsed = elapsed;
|
||||||
|
}
|
||||||
tick(elapsed) {
|
tick(elapsed) {
|
||||||
this.$$elapsed += elapsed;
|
this.$$elapsed += elapsed;
|
||||||
if (this.$$elapsed >= this.ttl) {
|
if (this.$$elapsed >= this.ttl) {
|
||||||
|
|
160
app/ecs/ecs.js
|
@ -1,7 +1,7 @@
|
||||||
import {Encoder, Decoder} from '@msgpack/msgpack';
|
import {Encoder, Decoder} from '@msgpack/msgpack';
|
||||||
import {LRUCache} from 'lru-cache';
|
import {LRUCache} from 'lru-cache';
|
||||||
|
|
||||||
import {Ticker, withResolvers} from '@/util/promise.js';
|
import {withResolvers} from '@/util/promise.js';
|
||||||
import Script from '@/util/script.js';
|
import Script from '@/util/script.js';
|
||||||
|
|
||||||
import EntityFactory from './entity-factory.js';
|
import EntityFactory from './entity-factory.js';
|
||||||
|
@ -23,16 +23,22 @@ export default class Ecs {
|
||||||
|
|
||||||
deferredChanges = {}
|
deferredChanges = {}
|
||||||
|
|
||||||
|
$$deindexing = new Set();
|
||||||
|
|
||||||
$$destructionDependencies = new Map();
|
$$destructionDependencies = new Map();
|
||||||
|
|
||||||
diff = {};
|
$$detached = new Set();
|
||||||
|
|
||||||
Systems = {};
|
diff = {};
|
||||||
|
|
||||||
$$entities = {};
|
$$entities = {};
|
||||||
|
|
||||||
$$entityFactory = new EntityFactory();
|
$$entityFactory = new EntityFactory();
|
||||||
|
|
||||||
|
$$reindexing = new Set();
|
||||||
|
|
||||||
|
Systems = {};
|
||||||
|
|
||||||
constructor({Systems, Components} = {}) {
|
constructor({Systems, Components} = {}) {
|
||||||
for (const componentName in Components) {
|
for (const componentName in Components) {
|
||||||
this.Components[componentName] = new Components[componentName](this);
|
this.Components[componentName] = new Components[componentName](this);
|
||||||
|
@ -58,7 +64,7 @@ export default class Ecs {
|
||||||
|
|
||||||
async apply(patch) {
|
async apply(patch) {
|
||||||
const creating = [];
|
const creating = [];
|
||||||
const destroying = [];
|
const destroying = new Set();
|
||||||
const inserting = [];
|
const inserting = [];
|
||||||
const removing = [];
|
const removing = [];
|
||||||
const updating = [];
|
const updating = [];
|
||||||
|
@ -66,7 +72,7 @@ export default class Ecs {
|
||||||
const entityId = parseInt(entityIdString);
|
const entityId = parseInt(entityIdString);
|
||||||
const components = patch[entityId];
|
const components = patch[entityId];
|
||||||
if (false === components) {
|
if (false === components) {
|
||||||
destroying.push(entityId);
|
destroying.add(entityId);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const componentsToRemove = [];
|
const componentsToRemove = [];
|
||||||
|
@ -82,22 +88,26 @@ export default class Ecs {
|
||||||
if (componentsToRemove.length > 0) {
|
if (componentsToRemove.length > 0) {
|
||||||
removing.push([entityId, componentsToRemove]);
|
removing.push([entityId, componentsToRemove]);
|
||||||
}
|
}
|
||||||
if (this.$$entities[entityId]) {
|
if (this.$$entities[entityIdString]) {
|
||||||
const entity = this.$$entities[entityId];
|
const entity = this.$$entities[entityIdString];
|
||||||
|
let isInserting = false;
|
||||||
const entityInserts = {};
|
const entityInserts = {};
|
||||||
|
let isUpdating = false;
|
||||||
const entityUpdates = {};
|
const entityUpdates = {};
|
||||||
for (const componentName in componentsToUpdate) {
|
for (const componentName in componentsToUpdate) {
|
||||||
if (entity[componentName]) {
|
if (entity[componentName]) {
|
||||||
entityUpdates[componentName] = componentsToUpdate[componentName];
|
entityUpdates[componentName] = componentsToUpdate[componentName];
|
||||||
|
isUpdating = true;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
entityInserts[componentName] = componentsToUpdate[componentName];
|
entityInserts[componentName] = componentsToUpdate[componentName];
|
||||||
|
isInserting = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (Object.keys(entityInserts).length > 0) {
|
if (isInserting) {
|
||||||
inserting.push([entityId, entityInserts]);
|
inserting.push([entityId, entityInserts]);
|
||||||
}
|
}
|
||||||
if (Object.keys(entityUpdates).length > 0) {
|
if (isUpdating) {
|
||||||
updating.push([entityId, entityUpdates]);
|
updating.push([entityId, entityUpdates]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -105,9 +115,6 @@ export default class Ecs {
|
||||||
creating.push([entityId, componentsToUpdate]);
|
creating.push([entityId, componentsToUpdate]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (destroying.length > 0) {
|
|
||||||
this.destroyMany(destroying);
|
|
||||||
}
|
|
||||||
const promises = [];
|
const promises = [];
|
||||||
if (inserting.length > 0) {
|
if (inserting.length > 0) {
|
||||||
promises.push(this.insertMany(inserting));
|
promises.push(this.insertMany(inserting));
|
||||||
|
@ -119,11 +126,30 @@ export default class Ecs {
|
||||||
promises.push(this.createManySpecific(creating));
|
promises.push(this.createManySpecific(creating));
|
||||||
}
|
}
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
|
if (destroying.size > 0) {
|
||||||
|
this.destroyMany(destroying);
|
||||||
|
}
|
||||||
if (removing.length > 0) {
|
if (removing.length > 0) {
|
||||||
this.removeMany(removing);
|
this.removeMany(removing);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applyDeferredChanges(entityId) {
|
||||||
|
const changes = this.deferredChanges[entityId];
|
||||||
|
delete this.deferredChanges[entityId];
|
||||||
|
for (const components of changes) {
|
||||||
|
this.markChange(entityId, components);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
attach(entityIds) {
|
||||||
|
for (const entityId of entityIds) {
|
||||||
|
this.$$detached.delete(entityId);
|
||||||
|
this.$$reindexing.add(entityId);
|
||||||
|
this.applyDeferredChanges(entityId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
changed(criteria) {
|
changed(criteria) {
|
||||||
const it = Object.entries(this.diff).values();
|
const it = Object.entries(this.diff).values();
|
||||||
return {
|
return {
|
||||||
|
@ -158,10 +184,27 @@ export default class Ecs {
|
||||||
return entityId;
|
return entityId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createDetached(components = {}) {
|
||||||
|
const [entityId] = await this.createManyDetached([components]);
|
||||||
|
return entityId;
|
||||||
|
}
|
||||||
|
|
||||||
async createMany(componentsList) {
|
async createMany(componentsList) {
|
||||||
const specificsList = [];
|
const specificsList = [];
|
||||||
for (const components of componentsList) {
|
for (const components of componentsList) {
|
||||||
specificsList.push([this.$$caret++, components]);
|
specificsList.push([this.$$caret, components]);
|
||||||
|
this.$$caret += 1;
|
||||||
|
}
|
||||||
|
return this.createManySpecific(specificsList);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createManyDetached(componentsList) {
|
||||||
|
const specificsList = [];
|
||||||
|
for (const components of componentsList) {
|
||||||
|
specificsList.push([this.$$caret, components]);
|
||||||
|
this.$$detached.add(this.$$caret);
|
||||||
|
this.deferredChanges[this.$$caret] = [];
|
||||||
|
this.$$caret += 1;
|
||||||
}
|
}
|
||||||
return this.createManySpecific(specificsList);
|
return this.createManySpecific(specificsList);
|
||||||
}
|
}
|
||||||
|
@ -170,19 +213,20 @@ export default class Ecs {
|
||||||
if (0 === specificsList.length) {
|
if (0 === specificsList.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const entityIds = [];
|
const entityIds = new Set();
|
||||||
const creating = {};
|
const creating = {};
|
||||||
for (let i = 0; i < specificsList.length; i++) {
|
for (let i = 0; i < specificsList.length; i++) {
|
||||||
const [entityId, components] = specificsList[i];
|
const [entityId, components] = specificsList[i];
|
||||||
|
if (!this.$$detached.has(entityId)) {
|
||||||
this.deferredChanges[entityId] = [];
|
this.deferredChanges[entityId] = [];
|
||||||
|
}
|
||||||
const componentNames = [];
|
const componentNames = [];
|
||||||
for (const componentName in components) {
|
for (const componentName in components) {
|
||||||
if (this.Components[componentName]) {
|
if (this.Components[componentName]) {
|
||||||
componentNames.push(componentName);
|
componentNames.push(componentName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
entityIds.push(entityId);
|
entityIds.add(entityId);
|
||||||
this.rebuild(entityId, () => componentNames);
|
|
||||||
for (const componentName of componentNames) {
|
for (const componentName of componentNames) {
|
||||||
if (!creating[componentName]) {
|
if (!creating[componentName]) {
|
||||||
creating[componentName] = [];
|
creating[componentName] = [];
|
||||||
|
@ -197,28 +241,38 @@ export default class Ecs {
|
||||||
}
|
}
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
for (let i = 0; i < specificsList.length; i++) {
|
for (let i = 0; i < specificsList.length; i++) {
|
||||||
const [entityId] = specificsList[i];
|
const [entityId, components] = specificsList[i];
|
||||||
const changes = this.deferredChanges[entityId];
|
this.$$reindexing.add(entityId);
|
||||||
delete this.deferredChanges[entityId];
|
this.rebuild(entityId, () => Object.keys(components));
|
||||||
for (const components of changes) {
|
if (this.$$detached.has(entityId)) {
|
||||||
this.markChange(entityId, components);
|
continue;
|
||||||
}
|
}
|
||||||
|
this.applyDeferredChanges(entityId);
|
||||||
}
|
}
|
||||||
this.reindex(entityIds);
|
|
||||||
return entityIds;
|
return entityIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
async createSpecific(entityId, components) {
|
async createSpecific(entityId, components) {
|
||||||
return this.createManySpecific([[entityId, components]]);
|
const [created] = await this.createManySpecific([[entityId, components]]);
|
||||||
|
return created;
|
||||||
}
|
}
|
||||||
|
|
||||||
deindex(entityIds) {
|
deindex(entityIds) {
|
||||||
|
// Stage 4 Draft / July 6, 2024
|
||||||
|
// const attached = entityIds.difference(this.$$detached);
|
||||||
|
const attached = new Set(entityIds);
|
||||||
|
for (const detached of this.$$detached) {
|
||||||
|
attached.delete(detached);
|
||||||
|
}
|
||||||
|
if (0 === attached.size) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
for (const systemName in this.Systems) {
|
for (const systemName in this.Systems) {
|
||||||
const System = this.Systems[systemName];
|
const System = this.Systems[systemName];
|
||||||
if (!System.active) {
|
if (!System.active) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
System.deindex(entityIds);
|
System.deindex(attached);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -259,14 +313,11 @@ export default class Ecs {
|
||||||
return dependencies.resolvers.promise;
|
return dependencies.resolvers.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
destroyAll() {
|
|
||||||
this.destroyMany(this.entities);
|
|
||||||
}
|
|
||||||
|
|
||||||
destroyMany(entityIds) {
|
destroyMany(entityIds) {
|
||||||
const destroying = {};
|
const destroying = {};
|
||||||
this.deindex(entityIds);
|
// this.deindex(entityIds);
|
||||||
for (const entityId of entityIds) {
|
for (const entityId of entityIds) {
|
||||||
|
this.$$deindexing.add(entityId);
|
||||||
if (!this.$$entities[entityId]) {
|
if (!this.$$entities[entityId]) {
|
||||||
throw new Error(`can't destroy non-existent entity ${entityId}`);
|
throw new Error(`can't destroy non-existent entity ${entityId}`);
|
||||||
}
|
}
|
||||||
|
@ -286,6 +337,13 @@ export default class Ecs {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
detach(entityIds) {
|
||||||
|
for (const entityId of entityIds) {
|
||||||
|
this.$$deindexing.add(entityId);
|
||||||
|
this.$$detached.add(entityId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
get entities() {
|
get entities() {
|
||||||
const ids = [];
|
const ids = [];
|
||||||
for (const entity of Object.values(this.$$entities)) {
|
for (const entity of Object.values(this.$$entities)) {
|
||||||
|
@ -306,7 +364,6 @@ export default class Ecs {
|
||||||
const inserting = {};
|
const inserting = {};
|
||||||
const unique = new Set();
|
const unique = new Set();
|
||||||
for (const [entityId, components] of entities) {
|
for (const [entityId, components] of entities) {
|
||||||
this.rebuild(entityId, (componentNames) => [...new Set(componentNames.concat(Object.keys(components)))]);
|
|
||||||
const diff = {};
|
const diff = {};
|
||||||
for (const componentName in components) {
|
for (const componentName in components) {
|
||||||
if (!inserting[componentName]) {
|
if (!inserting[componentName]) {
|
||||||
|
@ -323,7 +380,10 @@ export default class Ecs {
|
||||||
promises.push(this.Components[componentName].insertMany(inserting[componentName]));
|
promises.push(this.Components[componentName].insertMany(inserting[componentName]));
|
||||||
}
|
}
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
this.reindex(unique.values());
|
for (const [entityId, components] of entities) {
|
||||||
|
this.$$reindexing.add(entityId);
|
||||||
|
this.rebuild(entityId, (componentNames) => [...new Set(componentNames.concat(Object.keys(components)))]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
markChange(entityId, components) {
|
markChange(entityId, components) {
|
||||||
|
@ -337,13 +397,7 @@ export default class Ecs {
|
||||||
}
|
}
|
||||||
// Created?
|
// Created?
|
||||||
else if (!this.diff[entityId]) {
|
else if (!this.diff[entityId]) {
|
||||||
const filtered = {};
|
this.diff[entityId] = components;
|
||||||
for (const componentName in components) {
|
|
||||||
filtered[componentName] = false === components[componentName]
|
|
||||||
? false
|
|
||||||
: components[componentName];
|
|
||||||
}
|
|
||||||
this.diff[entityId] = filtered;
|
|
||||||
}
|
}
|
||||||
// Otherwise, merge.
|
// Otherwise, merge.
|
||||||
else {
|
else {
|
||||||
|
@ -415,12 +469,21 @@ export default class Ecs {
|
||||||
}
|
}
|
||||||
|
|
||||||
reindex(entityIds) {
|
reindex(entityIds) {
|
||||||
|
// Stage 4 Draft / July 6, 2024
|
||||||
|
// const attached = entityIds.difference(this.$$detached);
|
||||||
|
const attached = new Set(entityIds);
|
||||||
|
for (const detached of this.$$detached) {
|
||||||
|
attached.delete(detached);
|
||||||
|
}
|
||||||
|
if (0 === attached.size) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
for (const systemName in this.Systems) {
|
for (const systemName in this.Systems) {
|
||||||
const System = this.Systems[systemName];
|
const System = this.Systems[systemName];
|
||||||
if (!System.active) {
|
if (!System.active) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
System.reindex(entityIds);
|
System.reindex(attached);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -442,12 +505,14 @@ export default class Ecs {
|
||||||
removing[componentName].push(entityId);
|
removing[componentName].push(entityId);
|
||||||
}
|
}
|
||||||
this.markChange(entityId, diff);
|
this.markChange(entityId, diff);
|
||||||
this.rebuild(entityId, (componentNames) => componentNames.filter((type) => !components.includes(type)));
|
|
||||||
}
|
}
|
||||||
for (const componentName in removing) {
|
for (const componentName in removing) {
|
||||||
this.Components[componentName].destroyMany(removing[componentName]);
|
this.Components[componentName].destroyMany(removing[componentName]);
|
||||||
}
|
}
|
||||||
this.reindex(unique);
|
for (const [entityId, components] of entities) {
|
||||||
|
this.$$reindexing.add(entityId);
|
||||||
|
this.rebuild(entityId, (componentNames) => componentNames.filter((type) => !components.includes(type)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static serialize(ecs, view) {
|
static serialize(ecs, view) {
|
||||||
|
@ -468,6 +533,7 @@ export default class Ecs {
|
||||||
}
|
}
|
||||||
|
|
||||||
tick(elapsed) {
|
tick(elapsed) {
|
||||||
|
// tick systems
|
||||||
for (const systemName in this.Systems) {
|
for (const systemName in this.Systems) {
|
||||||
const System = this.Systems[systemName];
|
const System = this.Systems[systemName];
|
||||||
if (!System.active) {
|
if (!System.active) {
|
||||||
|
@ -483,6 +549,7 @@ export default class Ecs {
|
||||||
System.elapsed -= System.frequency;
|
System.elapsed -= System.frequency;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// destroy entities
|
||||||
const destroying = new Set();
|
const destroying = new Set();
|
||||||
for (const [entityId, {promises}] of this.$$destructionDependencies) {
|
for (const [entityId, {promises}] of this.$$destructionDependencies) {
|
||||||
if (0 === promises.size) {
|
if (0 === promises.size) {
|
||||||
|
@ -496,6 +563,15 @@ export default class Ecs {
|
||||||
this.$$destructionDependencies.delete(entityId);
|
this.$$destructionDependencies.delete(entityId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// update indices
|
||||||
|
if (this.$$deindexing.size > 0) {
|
||||||
|
this.deindex(this.$$deindexing);
|
||||||
|
this.$$deindexing.clear();
|
||||||
|
}
|
||||||
|
if (this.$$reindexing.size > 0) {
|
||||||
|
this.reindex(this.$$reindexing);
|
||||||
|
this.$$reindexing.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON() {
|
toJSON() {
|
||||||
|
@ -528,6 +604,7 @@ export default class Ecs {
|
||||||
}
|
}
|
||||||
updating[componentName].push([entityId, components[componentName]]);
|
updating[componentName].push([entityId, components[componentName]]);
|
||||||
}
|
}
|
||||||
|
this.$$reindexing.add(entityId);
|
||||||
unique.add(entityId);
|
unique.add(entityId);
|
||||||
}
|
}
|
||||||
const promises = [];
|
const promises = [];
|
||||||
|
@ -535,7 +612,6 @@ export default class Ecs {
|
||||||
promises.push(this.Components[componentName].updateMany(updating[componentName]));
|
promises.push(this.Components[componentName].updateMany(updating[componentName]));
|
||||||
}
|
}
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
this.reindex(unique.values());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -110,11 +110,11 @@ test('destroys entities', async () => {
|
||||||
.to.deep.equal(JSON.stringify({Empty: {}, Position: {y: 128}}));
|
.to.deep.equal(JSON.stringify({Empty: {}, Position: {y: 128}}));
|
||||||
expect(ecs.get(entity))
|
expect(ecs.get(entity))
|
||||||
.to.not.be.undefined;
|
.to.not.be.undefined;
|
||||||
ecs.destroyAll();
|
ecs.destroyMany(new Set([entity]));
|
||||||
expect(ecs.get(entity))
|
expect(ecs.get(entity))
|
||||||
.to.be.undefined;
|
.to.be.undefined;
|
||||||
expect(() => {
|
expect(() => {
|
||||||
ecs.destroyMany([entity]);
|
ecs.destroyMany(new Set([entity]));
|
||||||
})
|
})
|
||||||
.to.throw();
|
.to.throw();
|
||||||
});
|
});
|
||||||
|
@ -122,10 +122,10 @@ test('destroys entities', async () => {
|
||||||
test('inserts components into entities', async () => {
|
test('inserts components into entities', async () => {
|
||||||
const ecs = new Ecs({Components: {Empty, Position}});
|
const ecs = new Ecs({Components: {Empty, Position}});
|
||||||
const entity = await ecs.create({Empty: {}});
|
const entity = await ecs.create({Empty: {}});
|
||||||
ecs.insert(entity, {Position: {y: 128}});
|
await ecs.insert(entity, {Position: {y: 128}});
|
||||||
expect(JSON.stringify(ecs.get(entity)))
|
expect(JSON.stringify(ecs.get(entity)))
|
||||||
.to.deep.equal(JSON.stringify({Empty: {}, Position: {y: 128}}));
|
.to.deep.equal(JSON.stringify({Empty: {}, Position: {y: 128}}));
|
||||||
ecs.insert(entity, {Position: {y: 64}});
|
await ecs.insert(entity, {Position: {y: 64}});
|
||||||
expect(JSON.stringify(ecs.get(entity)))
|
expect(JSON.stringify(ecs.get(entity)))
|
||||||
.to.deep.equal(JSON.stringify({Empty: {}, Position: {y: 64}}));
|
.to.deep.equal(JSON.stringify({Empty: {}, Position: {y: 64}}));
|
||||||
});
|
});
|
||||||
|
@ -197,6 +197,39 @@ test('schedules entities to be deleted when ticking systems', async () => {
|
||||||
.to.be.undefined;
|
.to.be.undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('skips indexing detached entities', async () => {
|
||||||
|
const ecs = new Ecs({
|
||||||
|
Components: {Empty},
|
||||||
|
Systems: {
|
||||||
|
Indexer: class extends System {
|
||||||
|
static queries() {
|
||||||
|
return {
|
||||||
|
default: ['Empty'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const {$$map: map} = ecs.system('Indexer').queries.default;
|
||||||
|
ecs.system('Indexer').active = true;
|
||||||
|
const attached = await ecs.create({Empty: {}});
|
||||||
|
ecs.tick(0);
|
||||||
|
expect(Array.from(map.keys()))
|
||||||
|
.to.deep.equal([attached]);
|
||||||
|
ecs.destroyMany(new Set([attached]));
|
||||||
|
ecs.tick(0);
|
||||||
|
expect(Array.from(map.keys()))
|
||||||
|
.to.deep.equal([]);
|
||||||
|
const detached = await ecs.createDetached({Empty: {}});
|
||||||
|
ecs.tick(0);
|
||||||
|
expect(Array.from(map.keys()))
|
||||||
|
.to.deep.equal([]);
|
||||||
|
ecs.destroyMany(new Set([detached]));
|
||||||
|
ecs.tick(0);
|
||||||
|
expect(Array.from(map.keys()))
|
||||||
|
.to.deep.equal([]);
|
||||||
|
});
|
||||||
|
|
||||||
test('generates diffs for entity creation', async () => {
|
test('generates diffs for entity creation', async () => {
|
||||||
const ecs = new Ecs();
|
const ecs = new Ecs();
|
||||||
let entity;
|
let entity;
|
||||||
|
@ -210,7 +243,7 @@ test('generates diffs for adding and removing components', async () => {
|
||||||
let entity;
|
let entity;
|
||||||
entity = await ecs.create();
|
entity = await ecs.create();
|
||||||
ecs.setClean();
|
ecs.setClean();
|
||||||
ecs.insert(entity, {Position: {x: 64}});
|
await ecs.insert(entity, {Position: {x: 64}});
|
||||||
expect(ecs.diff)
|
expect(ecs.diff)
|
||||||
.to.deep.equal({[entity]: {Position: {x: 64}}});
|
.to.deep.equal({[entity]: {Position: {x: 64}}});
|
||||||
ecs.setClean();
|
ecs.setClean();
|
||||||
|
@ -246,6 +279,23 @@ test('generates diffs for entity mutations', async () => {
|
||||||
.to.deep.equal({});
|
.to.deep.equal({});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('generates no diffs for detached entities', async () => {
|
||||||
|
const ecs = new Ecs({Components: {Position}});
|
||||||
|
let entity;
|
||||||
|
entity = await ecs.createDetached();
|
||||||
|
expect(ecs.diff)
|
||||||
|
.to.deep.equal({});
|
||||||
|
await ecs.insert(entity, {Position: {x: 64}});
|
||||||
|
expect(ecs.diff)
|
||||||
|
.to.deep.equal({});
|
||||||
|
ecs.get(entity).Position.x = 128;
|
||||||
|
expect(ecs.diff)
|
||||||
|
.to.deep.equal({});
|
||||||
|
ecs.remove(entity, ['Position']);
|
||||||
|
expect(ecs.diff)
|
||||||
|
.to.deep.equal({});
|
||||||
|
});
|
||||||
|
|
||||||
test('generates coalesced diffs for components', async () => {
|
test('generates coalesced diffs for components', async () => {
|
||||||
const ecs = new Ecs({Components: {Position}});
|
const ecs = new Ecs({Components: {Position}});
|
||||||
let entity;
|
let entity;
|
||||||
|
@ -253,7 +303,7 @@ test('generates coalesced diffs for components', async () => {
|
||||||
ecs.remove(entity, ['Position']);
|
ecs.remove(entity, ['Position']);
|
||||||
expect(ecs.diff)
|
expect(ecs.diff)
|
||||||
.to.deep.equal({[entity]: {Position: false}});
|
.to.deep.equal({[entity]: {Position: false}});
|
||||||
ecs.insert(entity, {Position: {}});
|
await ecs.insert(entity, {Position: {}});
|
||||||
expect(ecs.diff)
|
expect(ecs.diff)
|
||||||
.to.deep.equal({[entity]: {Position: {}}});
|
.to.deep.equal({[entity]: {Position: {}}});
|
||||||
});
|
});
|
||||||
|
@ -290,11 +340,11 @@ test('applies creation patches', async () => {
|
||||||
.to.equal(64);
|
.to.equal(64);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('applies update patches', () => {
|
test('applies update patches', async () => {
|
||||||
const ecs = new Ecs({Components: {Position}});
|
const ecs = new Ecs({Components: {Position}});
|
||||||
ecs.createSpecific(16, {Position: {x: 64}});
|
await ecs.createSpecific(16, {Position: {x: 64}});
|
||||||
ecs.apply({16: {Position: {x: 128}}});
|
await ecs.apply({16: {Position: {x: 128}}});
|
||||||
expect(Array.from(ecs.entities).length)
|
expect(Object.keys(ecs.$$entities).length)
|
||||||
.to.equal(1);
|
.to.equal(1);
|
||||||
expect(ecs.get(16).Position.x)
|
expect(ecs.get(16).Position.x)
|
||||||
.to.equal(128);
|
.to.equal(128);
|
||||||
|
@ -318,9 +368,9 @@ test('applies component deletion patches', async () => {
|
||||||
.to.deep.equal(['Position']);
|
.to.deep.equal(['Position']);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('calculates entity size', () => {
|
test('calculates entity size', async () => {
|
||||||
const ecs = new Ecs({Components: {Empty, Position}});
|
const ecs = new Ecs({Components: {Empty, Position}});
|
||||||
ecs.createSpecific(1, {Empty: {}, Position: {}});
|
await ecs.createSpecific(1, {Empty: {}, Position: {}});
|
||||||
// ID + # of components + Empty + Position + x + y + z
|
// ID + # of components + Empty + Position + x + y + z
|
||||||
// 4 + 2 + 2 + 4 + 2 + 4 + 4 + 4 + 4 = 30
|
// 4 + 2 + 2 + 4 + 2 + 4 + 4 + 4 + 4 = 30
|
||||||
expect(ecs.get(1).size())
|
expect(ecs.get(1).size())
|
||||||
|
@ -329,8 +379,8 @@ test('calculates entity size', () => {
|
||||||
|
|
||||||
test('serializes and deserializes', async () => {
|
test('serializes and deserializes', async () => {
|
||||||
const ecs = new Ecs({Components: {Empty, Name, Position}});
|
const ecs = new Ecs({Components: {Empty, Name, Position}});
|
||||||
ecs.createSpecific(1, {Empty: {}, Position: {x: 64}});
|
await ecs.createSpecific(1, {Empty: {}, Position: {x: 64}});
|
||||||
ecs.createSpecific(16, {Name: {name: 'foobar'}, Position: {x: 128}});
|
await ecs.createSpecific(16, {Name: {name: 'foobar'}, Position: {x: 128}});
|
||||||
expect(ecs.toJSON())
|
expect(ecs.toJSON())
|
||||||
.to.deep.equal({
|
.to.deep.equal({
|
||||||
entities: {
|
entities: {
|
||||||
|
@ -362,8 +412,8 @@ test('serializes and deserializes', async () => {
|
||||||
|
|
||||||
test('deserializes from compatible ECS', async () => {
|
test('deserializes from compatible ECS', async () => {
|
||||||
const ecs = new Ecs({Components: {Empty, Name, Position}});
|
const ecs = new Ecs({Components: {Empty, Name, Position}});
|
||||||
ecs.createSpecific(1, {Empty: {}, Position: {x: 64}});
|
await ecs.createSpecific(1, {Empty: {}, Position: {x: 64}});
|
||||||
ecs.createSpecific(16, {Name: {name: 'foobar'}, Position: {x: 128}});
|
await ecs.createSpecific(16, {Name: {name: 'foobar'}, Position: {x: 128}});
|
||||||
const view = Ecs.serialize(ecs);
|
const view = Ecs.serialize(ecs);
|
||||||
const deserialized = await Ecs.deserialize(
|
const deserialized = await Ecs.deserialize(
|
||||||
new Ecs({Components: {Empty, Name}}),
|
new Ecs({Components: {Empty, Name}}),
|
||||||
|
|
|
@ -23,6 +23,9 @@ export default class EntityFactory {
|
||||||
static componentNames = sorted;
|
static componentNames = sorted;
|
||||||
constructor(id) {
|
constructor(id) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
|
for (const type of sorted) {
|
||||||
|
this[type] = Components[type].get(id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
size() {
|
size() {
|
||||||
let size = 0;
|
let size = 0;
|
||||||
|
@ -34,15 +37,6 @@ export default class EntityFactory {
|
||||||
return size + 4 + 2;
|
return size + 4 + 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const properties = {};
|
|
||||||
for (const type of sorted) {
|
|
||||||
properties[type] = {};
|
|
||||||
const get = Components[type].get.bind(Components[type]);
|
|
||||||
properties[type].get = function() {
|
|
||||||
return get(this.id);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Object.defineProperties(Entity.prototype, properties);
|
|
||||||
Entity.prototype.updateAttachments = new Function('update', `
|
Entity.prototype.updateAttachments = new Function('update', `
|
||||||
${
|
${
|
||||||
sorted
|
sorted
|
||||||
|
|
|
@ -2,7 +2,7 @@ export default class Query {
|
||||||
|
|
||||||
$$criteria = {with: [], without: []};
|
$$criteria = {with: [], without: []};
|
||||||
$$ecs;
|
$$ecs;
|
||||||
$$index = new Set();
|
$$map = new Map();
|
||||||
|
|
||||||
constructor(parameters, ecs) {
|
constructor(parameters, ecs) {
|
||||||
this.$$ecs = ecs;
|
this.$$ecs = ecs;
|
||||||
|
@ -20,19 +20,19 @@ export default class Query {
|
||||||
}
|
}
|
||||||
|
|
||||||
get count() {
|
get count() {
|
||||||
return this.$$index.size;
|
return this.$$map.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
deindex(entityIds) {
|
deindex(entityIds) {
|
||||||
for (const entityId of entityIds) {
|
for (const entityId of entityIds) {
|
||||||
this.$$index.delete(entityId);
|
this.$$map.delete(entityId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reindex(entityIds) {
|
reindex(entityIds) {
|
||||||
if (0 === this.$$criteria.with.length && 0 === this.$$criteria.without.length) {
|
if (0 === this.$$criteria.with.length && 0 === this.$$criteria.without.length) {
|
||||||
for (const entityId of entityIds) {
|
for (const entityId of entityIds) {
|
||||||
this.$$index.add(entityId);
|
this.$$map.set(entityId, this.$$ecs.get(entityId));
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -53,28 +53,16 @@ export default class Query {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (should) {
|
if (should) {
|
||||||
this.$$index.add(entityId);
|
this.$$map.set(entityId, this.$$ecs.get(entityId));
|
||||||
}
|
}
|
||||||
else if (!should) {
|
else if (!should) {
|
||||||
this.$$index.delete(entityId);
|
this.$$map.delete(entityId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
select() {
|
select() {
|
||||||
const it = this.$$index.values();
|
return this.$$map.values();
|
||||||
return {
|
|
||||||
[Symbol.iterator]() {
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
next: () => {
|
|
||||||
const result = it.next();
|
|
||||||
if (result.done) {
|
|
||||||
return {done: true, value: undefined};
|
|
||||||
}
|
|
||||||
return {done: false, value: this.$$ecs.get(result.value)};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,37 +1,39 @@
|
||||||
import {expect, test} from 'vitest';
|
import {expect, test} from 'vitest';
|
||||||
|
|
||||||
|
import Ecs from './ecs';
|
||||||
import Component from './component.js';
|
import Component from './component.js';
|
||||||
import Query from './query.js';
|
import Query from './query.js';
|
||||||
|
|
||||||
function wrapProperties(name, properties) {
|
const Components = [
|
||||||
return class WrappedComponent extends Component {
|
['A', {a: {type: 'int32', defaultValue: 64}}],
|
||||||
static name = name;
|
['B', {b: {type: 'int32', defaultValue: 32}}],
|
||||||
|
['C', {c: {type: 'int32'}}],
|
||||||
|
]
|
||||||
|
.reduce((Components, [componentName, properties]) => {
|
||||||
|
return {
|
||||||
|
...Components,
|
||||||
|
[componentName]: class extends Component {
|
||||||
|
static componentName = componentName;
|
||||||
static properties = properties;
|
static properties = properties;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}, {})
|
||||||
|
|
||||||
const A = new (wrapProperties('A', {a: {type: 'int32', defaultValue: 420}}));
|
const ecsTest = test.extend({
|
||||||
const B = new (wrapProperties('B', {b: {type: 'int32', defaultValue: 69}}));
|
ecs: async ({}, use) => {
|
||||||
const C = new (wrapProperties('C', {c: {type: 'int32'}}));
|
const ecs = new Ecs({Components});
|
||||||
|
await ecs.createManySpecific([
|
||||||
const Components = {A, B, C};
|
[1, {B: {}}],
|
||||||
Components.A.createMany([[2], [3]]);
|
[2, {A: {}, B: {}, C: {}}],
|
||||||
Components.B.createMany([[1], [2]]);
|
[3, {A: {}}],
|
||||||
Components.C.createMany([[2], [4]]);
|
[4, {C: {}}],
|
||||||
|
]);
|
||||||
const fakeEcs = (Components) => ({
|
await use(ecs);
|
||||||
Components,
|
|
||||||
get(id) {
|
|
||||||
return Object.fromEntries(
|
|
||||||
Object.entries(Components)
|
|
||||||
.map(([componentName, Component]) => [componentName, Component.get(id)])
|
|
||||||
.concat([['id', id]])
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function testQuery(parameters, expected) {
|
async function testQuery(ecs, parameters, expected) {
|
||||||
const query = new Query(parameters, fakeEcs(Components));
|
const query = new Query(parameters, ecs);
|
||||||
query.reindex([1, 2, 3]);
|
query.reindex([1, 2, 3]);
|
||||||
expect(query.count)
|
expect(query.count)
|
||||||
.to.equal(expected.length);
|
.to.equal(expected.length);
|
||||||
|
@ -41,22 +43,22 @@ function testQuery(parameters, expected) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test('can query all', () => {
|
ecsTest('can query all', async ({ecs}) => {
|
||||||
testQuery([], [1, 2, 3]);
|
await testQuery(ecs, [], [1, 2, 3]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('can query some', () => {
|
ecsTest('can query some', async ({ecs}) => {
|
||||||
testQuery(['A'], [2, 3]);
|
await testQuery(ecs, ['A'], [2, 3]);
|
||||||
testQuery(['A', 'B'], [2]);
|
await testQuery(ecs, ['A', 'B'], [2]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('can query excluding', () => {
|
ecsTest('can query excluding', async ({ecs}) => {
|
||||||
testQuery(['!A'], [1]);
|
await testQuery(ecs, ['!A'], [1]);
|
||||||
testQuery(['A', '!B'], [3]);
|
await testQuery(ecs, ['A', '!B'], [3]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('can deindex', () => {
|
ecsTest('can deindex', async ({ecs}) => {
|
||||||
const query = new Query(['A'], fakeEcs(Components));
|
const query = new Query(['A'], ecs);
|
||||||
query.reindex([1, 2, 3]);
|
query.reindex([1, 2, 3]);
|
||||||
expect(query.count)
|
expect(query.count)
|
||||||
.to.equal(2);
|
.to.equal(2);
|
||||||
|
@ -65,24 +67,22 @@ test('can deindex', () => {
|
||||||
.to.equal(1);
|
.to.equal(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('can reindex', () => {
|
ecsTest('can reindex', async ({ecs}) => {
|
||||||
const Test = new (wrapProperties('Test', {a: {type: 'int32', defaultValue: 420}}));
|
const query = new Query(['B'], ecs);
|
||||||
Test.createMany([[2], [3]]);
|
query.reindex([1, 2]);
|
||||||
const query = new Query(['Test'], fakeEcs({Test}));
|
|
||||||
query.reindex([2, 3]);
|
|
||||||
expect(query.count)
|
expect(query.count)
|
||||||
.to.equal(2);
|
.to.equal(2);
|
||||||
Test.destroy(2);
|
ecs.destroyMany(new Set([2]));
|
||||||
query.reindex([2, 3]);
|
query.reindex([1, 2]);
|
||||||
expect(query.count)
|
expect(query.count)
|
||||||
.to.equal(1);
|
.to.equal(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('can select', () => {
|
ecsTest('can select', async ({ecs}) => {
|
||||||
const query = new Query(['A'], fakeEcs(Components));
|
const query = new Query(['A'], ecs);
|
||||||
query.reindex([1, 2, 3]);
|
query.reindex([1, 2, 3]);
|
||||||
const it = query.select();
|
const it = query.select();
|
||||||
const {value: {A}} = it.next();
|
const {value: {A}} = it.next();
|
||||||
expect(A.a)
|
expect(A.a)
|
||||||
.to.equal(420);
|
.to.equal(64);
|
||||||
});
|
});
|
||||||
|
|
|
@ -35,6 +35,7 @@ export default class Schema {
|
||||||
return {
|
return {
|
||||||
$: this.$$types[type],
|
$: this.$$types[type],
|
||||||
concrete: $$type.normalize ? $$type.normalize(rest) : rest,
|
concrete: $$type.normalize ? $$type.normalize(rest) : rest,
|
||||||
|
type,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,6 @@ export default class System {
|
||||||
for (const i in queries) {
|
for (const i in queries) {
|
||||||
this.queries[i] = new Query(queries[i], ecs);
|
this.queries[i] = new Query(queries[i], ecs);
|
||||||
}
|
}
|
||||||
this.reindex(ecs.entities);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deindex(entityIds) {
|
deindex(entityIds) {
|
||||||
|
@ -45,6 +44,10 @@ export default class System {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
schedule() {
|
||||||
|
this.elapsed = this.frequency;
|
||||||
|
}
|
||||||
|
|
||||||
select(query) {
|
select(query) {
|
||||||
return this.queries[query].select();
|
return this.queries[query].select();
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,8 +10,8 @@ export default class IntegratePhysics extends System {
|
||||||
|
|
||||||
tick(elapsed) {
|
tick(elapsed) {
|
||||||
for (const {Position, Forces} of this.select('default')) {
|
for (const {Position, Forces} of this.select('default')) {
|
||||||
Position.x += elapsed * (Forces.impulseX + Forces.forceX);
|
Position.x = Position.$$x + elapsed * (Forces.$$impulseX + Forces.$$forceX);
|
||||||
Position.y += elapsed * (Forces.impulseY + Forces.forceY);
|
Position.y = Position.$$y + elapsed * (Forces.$$impulseY + Forces.$$forceY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -54,9 +54,11 @@ export default class MaintainColliderHash extends System {
|
||||||
|
|
||||||
within(query) {
|
within(query) {
|
||||||
const within = new Set();
|
const within = new Set();
|
||||||
|
if (this.hash) {
|
||||||
for (const id of this.hash.within(query)) {
|
for (const id of this.hash.within(query)) {
|
||||||
within.add(this.ecs.get(id));
|
within.add(this.ecs.get(id));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return within;
|
return within;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,22 +1,50 @@
|
||||||
import K from 'kefir';
|
import K from 'kefir';
|
||||||
|
|
||||||
|
import * as easings from '@/util/easing.js';
|
||||||
|
import {TAU} from '@/util/math.js';
|
||||||
|
|
||||||
export default class Emitter {
|
export default class Emitter {
|
||||||
constructor(ecs) {
|
constructor(ecs) {
|
||||||
this.ecs = ecs;
|
this.ecs = ecs;
|
||||||
this.scheduled = [];
|
this.scheduled = [];
|
||||||
}
|
}
|
||||||
async allocate({entity, shape}) {
|
async configure(entityId, {fields, shape}) {
|
||||||
const allocated = this.ecs.get(await this.ecs.create(entity));
|
const entity = this.ecs.get(entityId);
|
||||||
if (shape) {
|
if (shape) {
|
||||||
switch (shape.type) {
|
switch (shape.type) {
|
||||||
|
case 'circle': {
|
||||||
|
const r = Math.random() * TAU;
|
||||||
|
entity.Position.x += Math.cos(r) * shape.payload.radius;
|
||||||
|
entity.Position.y += Math.sin(r) * shape.payload.radius;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'filledCircle': {
|
||||||
|
const r = Math.random() * TAU;
|
||||||
|
entity.Position.x += Math.cos(r) * Math.random() * shape.payload.radius;
|
||||||
|
entity.Position.y += Math.sin(r) * Math.random() * shape.payload.radius;
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 'filledRect': {
|
case 'filledRect': {
|
||||||
allocated.Position.x += Math.random() * shape.payload.width - (shape.payload.width / 2);
|
entity.Position.x += Math.random() * shape.payload.width - (shape.payload.width / 2);
|
||||||
allocated.Position.y += Math.random() * shape.payload.height - (shape.payload.height / 2);
|
entity.Position.y += Math.random() * shape.payload.height - (shape.payload.height / 2);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return allocated;
|
if (fields) {
|
||||||
|
for (const {easing = 'linear', path, value} of fields) {
|
||||||
|
let walk = entity;
|
||||||
|
const pathCopy = path.slice(0);
|
||||||
|
const final = pathCopy.pop();
|
||||||
|
for (const key of pathCopy) {
|
||||||
|
walk = walk[key];
|
||||||
|
}
|
||||||
|
const c = Math.random() * (value.length - 1);
|
||||||
|
const i = Math.floor(c);
|
||||||
|
walk[final] = easings[easing](c - i, value[i], value[i + 1] - value[i], 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entity;
|
||||||
}
|
}
|
||||||
emit(particle) {
|
emit(particle) {
|
||||||
particle = {
|
particle = {
|
||||||
|
@ -24,59 +52,60 @@ export default class Emitter {
|
||||||
entity: {
|
entity: {
|
||||||
Position: {},
|
Position: {},
|
||||||
Sprite: {},
|
Sprite: {},
|
||||||
VisibleAabb: {},
|
|
||||||
...particle.entity,
|
...particle.entity,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
let {count = 1} = particle;
|
let {count = 1} = particle;
|
||||||
const {frequency = 0} = particle;
|
const {entity, frequency = 0, spurt = 1, ttl = 0} = particle;
|
||||||
const stream = K.stream((emitter) => {
|
if (ttl > 0) {
|
||||||
if (0 === frequency) {
|
count = Math.floor(ttl / frequency);
|
||||||
const promises = [];
|
}
|
||||||
for (let i = 0; i < count; ++i) {
|
const specifications = Array(count);
|
||||||
promises.push(
|
for (let i = 0; i < count * spurt; ++i) {
|
||||||
this.allocate(particle)
|
specifications[i] = entity;
|
||||||
.then((entity) => {
|
}
|
||||||
emitter.emit(entity);
|
const stream = K.stream(async (emitter) => {
|
||||||
}),
|
const entityIds = await this.ecs.createManyDetached(specifications);
|
||||||
);
|
if (0 === frequency) {
|
||||||
|
this.ecs.attach(entityIds);
|
||||||
|
for (const entityId of entityIds) {
|
||||||
|
emitter.emit(this.configure(entityId, particle));
|
||||||
}
|
}
|
||||||
Promise.all(promises)
|
|
||||||
.then(() => {
|
|
||||||
emitter.end();
|
emitter.end();
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const promise = this.allocate(particle)
|
const it = entityIds.values();
|
||||||
.then((entity) => {
|
const batch = new Set();
|
||||||
emitter.emit(entity);
|
for (let i = 0; i < spurt; ++i) {
|
||||||
});
|
batch.add(it.next().value);
|
||||||
|
}
|
||||||
|
this.ecs.attach(batch);
|
||||||
|
for (const entityId of batch) {
|
||||||
|
emitter.emit(this.configure(entityId, particle));
|
||||||
|
}
|
||||||
count -= 1;
|
count -= 1;
|
||||||
if (0 === count) {
|
if (0 === count) {
|
||||||
promise.then(() => {
|
|
||||||
emitter.end();
|
emitter.end();
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const promises = [promise];
|
|
||||||
let accumulated = 0;
|
let accumulated = 0;
|
||||||
const scheduled = (elapsed) => {
|
const scheduled = (elapsed) => {
|
||||||
accumulated += elapsed;
|
accumulated += elapsed;
|
||||||
while (accumulated > frequency && count > 0) {
|
while (accumulated > frequency && count > 0) {
|
||||||
promises.push(
|
const batch = new Set();
|
||||||
this.allocate(particle)
|
for (let i = 0; i < spurt; ++i) {
|
||||||
.then((entity) => {
|
batch.add(it.next().value);
|
||||||
emitter.emit(entity);
|
}
|
||||||
}),
|
this.ecs.attach(batch);
|
||||||
);
|
for (const entityId of batch) {
|
||||||
|
emitter.emit(this.configure(entityId, particle));
|
||||||
|
}
|
||||||
accumulated -= frequency;
|
accumulated -= frequency;
|
||||||
count -= 1;
|
count -= 1;
|
||||||
}
|
}
|
||||||
if (0 === count) {
|
if (0 === count) {
|
||||||
this.scheduled.splice(this.scheduled.indexOf(scheduled), 1);
|
this.scheduled.splice(this.scheduled.indexOf(scheduled), 1);
|
||||||
Promise.all(promises).then(() => {
|
|
||||||
emitter.end();
|
emitter.end();
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
this.scheduled.push(scheduled);
|
this.scheduled.push(scheduled);
|
||||||
|
|
|
@ -32,10 +32,8 @@ test('emits particles over time', async () => {
|
||||||
current.onValue(resolve);
|
current.onValue(resolve);
|
||||||
}))
|
}))
|
||||||
.to.deep.include({id: 1});
|
.to.deep.include({id: 1});
|
||||||
expect(ecs.get(1))
|
expect(Array.from(ecs.$$detached))
|
||||||
.to.not.be.undefined;
|
.to.deep.equal([2]);
|
||||||
expect(ecs.get(2))
|
|
||||||
.to.be.undefined;
|
|
||||||
emitter.tick(0.06);
|
emitter.tick(0.06);
|
||||||
expect(await new Promise((resolve) => {
|
expect(await new Promise((resolve) => {
|
||||||
current.onValue(resolve);
|
current.onValue(resolve);
|
||||||
|
@ -47,6 +45,6 @@ test('emits particles over time', async () => {
|
||||||
current.onValue(resolve);
|
current.onValue(resolve);
|
||||||
}))
|
}))
|
||||||
.to.deep.include({id: 2});
|
.to.deep.include({id: 2});
|
||||||
expect(ecs.get(2))
|
expect(Array.from(ecs.$$detached))
|
||||||
.to.not.be.undefined;
|
.to.deep.equal([]);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import {useState} from 'react';
|
import {useCallback, useState} from 'react';
|
||||||
import {Tab, Tabs, TabList, TabPanel} from 'react-tabs';
|
import {Tab, Tabs, TabList, TabPanel} from 'react-tabs';
|
||||||
import 'react-tabs/style/react-tabs.css';
|
import 'react-tabs/style/react-tabs.css';
|
||||||
|
|
||||||
import {useEcs, useEcsTick} from '@/react/context/ecs.js';
|
import {useEcsTick} from '@/react/context/ecs.js';
|
||||||
import {useMainEntity} from '@/react/context/main-entity.js';
|
import {useMainEntity} from '@/react/context/main-entity.js';
|
||||||
|
|
||||||
import styles from './devtools.module.css';
|
import styles from './devtools.module.css';
|
||||||
|
@ -12,15 +12,15 @@ import Tiles from './devtools/tiles.jsx';
|
||||||
export default function Devtools({
|
export default function Devtools({
|
||||||
eventsChannel,
|
eventsChannel,
|
||||||
}) {
|
}) {
|
||||||
const [ecs] = useEcs();
|
|
||||||
const [mainEntity] = useMainEntity();
|
const [mainEntity] = useMainEntity();
|
||||||
const [mainEntityJson, setMainEntityJson] = useState('');
|
const [mainEntityJson, setMainEntityJson] = useState('');
|
||||||
useEcsTick(() => {
|
const onEcsTick = useCallback((payload, ecs) => {
|
||||||
if (!ecs || !mainEntity) {
|
if (!mainEntity) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setMainEntityJson(JSON.stringify(ecs.get(mainEntity), null, 2));
|
setMainEntityJson(JSON.stringify(ecs.get(mainEntity), null, 2));
|
||||||
}, [ecs, mainEntity]);
|
}, [mainEntity]);
|
||||||
|
useEcsTick(onEcsTick);
|
||||||
return (
|
return (
|
||||||
<div className={styles.devtools}>
|
<div className={styles.devtools}>
|
||||||
<Tabs>
|
<Tabs>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import {memo, useCallback, useRef} from 'react';
|
import {memo, useCallback, useRef} from 'react';
|
||||||
|
|
||||||
import {DamageTypes} from '@/ecs/components/vulnerable.js';
|
import {DamageTypes} from '@/ecs/components/vulnerable.js';
|
||||||
|
import {useEcsTick} from '@/react/context/ecs.js';
|
||||||
import useAnimationFrame from '@/react/hooks/use-animation-frame.js';
|
import useAnimationFrame from '@/react/hooks/use-animation-frame.js';
|
||||||
import {easeInOutExpo, easeInQuint, easeOutQuad, linear} from '@/util/easing.js';
|
import {easeInOutExpo, easeInQuint, easeOutQuad, linear} from '@/util/easing.js';
|
||||||
|
|
||||||
|
@ -25,142 +26,143 @@ function damageHue(type) {
|
||||||
return hue;
|
return hue;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createDamageNode() {
|
class Damage {
|
||||||
const damage = document.createElement('div');
|
elapsed = 0;
|
||||||
damage.classList.add(styles.damage);
|
hue = [0, 30];
|
||||||
damage.appendChild(document.createElement('p'));
|
offsetX = 0;
|
||||||
return damage;
|
offsetY = 0;
|
||||||
|
step = 0;
|
||||||
|
constructor() {
|
||||||
|
const element = document.createElement('div');
|
||||||
|
element.classList.add(styles.damage);
|
||||||
|
element.appendChild(document.createElement('p'));
|
||||||
|
this.element = element;
|
||||||
|
}
|
||||||
|
reset(key, scale, {amount, position, type}) {
|
||||||
|
this.elapsed = 0;
|
||||||
|
[this.hueStart, this.hueEnd] = damageHue(type);
|
||||||
|
this.offsetX = 20 * scale * 1 * (Math.random() - 0.5);
|
||||||
|
this.offsetY = -1 * (10 + Math.random() * 45 * scale);
|
||||||
|
this.step = 0;
|
||||||
|
const {element} = this;
|
||||||
|
element.style.setProperty('rotate', `${Math.random() * (Math.PI / 16) - (Math.PI / 32)}rad`);
|
||||||
|
element.style.setProperty('--magnitude', Math.max(1, Math.floor(Math.log10(Math.abs(amount)))));
|
||||||
|
element.style.setProperty('--positionX', `${position.x * scale}px`);
|
||||||
|
element.style.setProperty('--positionY', `${position.y * scale}px`);
|
||||||
|
element.style.setProperty('zIndex', key);
|
||||||
|
const p = element.querySelector('p');
|
||||||
|
p.style.scale = scale / 2;
|
||||||
|
p.innerText = Math.abs(amount);
|
||||||
|
}
|
||||||
|
tick(elapsed, stepSize) {
|
||||||
|
this.elapsed += elapsed;
|
||||||
|
this.step += elapsed;
|
||||||
|
// offset
|
||||||
|
let offsetX = 0, offsetY = 0;
|
||||||
|
if (this.elapsed <= 0.5) {
|
||||||
|
offsetX = easeOutQuad(this.elapsed, 0, this.offsetX, 0.5);
|
||||||
|
offsetY = easeOutQuad(this.elapsed, 0, this.offsetY, 0.5);
|
||||||
|
}
|
||||||
|
else if (this.elapsed > 1.375) {
|
||||||
|
offsetX = easeOutQuad(this.elapsed - 1.375, this.offsetX, -this.offsetX, 0.125);
|
||||||
|
offsetY = easeOutQuad(this.elapsed - 1.375, this.offsetY, -this.offsetY, 0.125);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
offsetX = this.offsetX;
|
||||||
|
offsetY = this.offsetY;
|
||||||
|
}
|
||||||
|
this.element.style.setProperty('--offsetX', offsetX);
|
||||||
|
this.element.style.setProperty('--offsetY', offsetY);
|
||||||
|
if (this.step > stepSize) {
|
||||||
|
this.step = this.step % stepSize;
|
||||||
|
// scale
|
||||||
|
let scale = 0.35;
|
||||||
|
if (this.elapsed <= 0.5) {
|
||||||
|
scale = easeOutQuad(this.elapsed, 0, 1, 0.5);
|
||||||
|
}
|
||||||
|
else if (this.elapsed > 0.5 && this.elapsed < 0.6) {
|
||||||
|
scale = linear(this.elapsed - 0.5, 1, 0.5, 0.1);
|
||||||
|
}
|
||||||
|
else if (this.elapsed > 0.6 && this.elapsed < 0.675) {
|
||||||
|
scale = easeInQuint(this.elapsed - 0.6, 1, 0.25, 0.075);
|
||||||
|
}
|
||||||
|
else if (this.elapsed > 0.675 && this.elapsed < 1.375) {
|
||||||
|
scale = 1;
|
||||||
|
}
|
||||||
|
else if (this.elapsed > 1.375) {
|
||||||
|
scale = easeOutQuad(this.elapsed - 1.375, 1, -1, 0.125);
|
||||||
|
}
|
||||||
|
this.element.style.setProperty('--scale', scale);
|
||||||
|
// opacity
|
||||||
|
let opacity = 0.75;
|
||||||
|
if (this.elapsed <= 0.375) {
|
||||||
|
opacity = linear(this.elapsed, 0.75, 0.25, 0.375);
|
||||||
|
}
|
||||||
|
else if (this.elapsed > 0.375 && this.elapsed < 1.375) {
|
||||||
|
opacity = 1;
|
||||||
|
}
|
||||||
|
else if (this.elapsed > 1.375) {
|
||||||
|
opacity = linear(this.elapsed - 1.375, 1, -1, 0.125);
|
||||||
|
}
|
||||||
|
this.element.style.setProperty('--opacity', opacity);
|
||||||
|
// hue
|
||||||
|
this.element.style.setProperty(
|
||||||
|
'--hue',
|
||||||
|
easeInOutExpo(
|
||||||
|
Math.abs((this.elapsed % 0.3) - 0.15) / 0.15,
|
||||||
|
this.hueStart,
|
||||||
|
this.hueEnd,
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function Damages({damages, scale}) {
|
function Damages({scale}) {
|
||||||
const animations = useRef({});
|
const damages = useRef({});
|
||||||
const pool = useRef([]);
|
|
||||||
const damagesRef = useRef();
|
const damagesRef = useRef();
|
||||||
|
const pool = useRef([]);
|
||||||
|
const onEcsTick = useCallback((payload) => {
|
||||||
|
for (const id in payload) {
|
||||||
|
const update = payload[id];
|
||||||
|
if (false === update || !update.Vulnerable?.damage) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const {damage: damageUpdate} = update.Vulnerable;
|
||||||
|
for (const key in damageUpdate) {
|
||||||
|
const damage = pool.current.length > 0 ? pool.current.pop() : new Damage();
|
||||||
|
damage.reset(key, scale, damageUpdate[key]);
|
||||||
|
damages.current[key] = damage;
|
||||||
|
damagesRef.current.appendChild(damage.element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [scale]);
|
||||||
|
useEcsTick(onEcsTick);
|
||||||
const frame = useCallback((elapsed) => {
|
const frame = useCallback((elapsed) => {
|
||||||
if (!damagesRef.current) {
|
if (!damagesRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (0 === pool.current.length) {
|
const keys = Object.keys(damages.current);
|
||||||
for (let i = 0; i < 512; ++i) {
|
if (0 === keys.length && 0 === pool.current.length) {
|
||||||
const damage = createDamageNode();
|
for (let i = 0; i < 500; ++i) {
|
||||||
damagesRef.current.appendChild(damage);
|
pool.current.push(new Damage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const stepSize = keys.length > 150 ? (keys.length / 500) * 0.5 : elapsed;
|
||||||
|
for (const key of keys) {
|
||||||
|
const damage = damages.current[key];
|
||||||
|
damage.tick(elapsed, stepSize);
|
||||||
|
if (damage.elapsed > 1.5) {
|
||||||
|
damagesRef.current.removeChild(damage.element);
|
||||||
|
delete damages.current[key];
|
||||||
|
if (pool.current.length < 1000) {
|
||||||
pool.current.push(damage);
|
pool.current.push(damage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const keys = Object.keys(animations.current);
|
|
||||||
for (const key of keys) {
|
|
||||||
const animation = animations.current[key];
|
|
||||||
if (!damages[key]) {
|
|
||||||
if (animation.element) {
|
|
||||||
if (pool.current.length < 512) {
|
|
||||||
pool.current.push(animation.element);
|
|
||||||
}
|
}
|
||||||
animation.element = undefined;
|
}, []);
|
||||||
}
|
|
||||||
delete animations.current[key];
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!animation.element) {
|
|
||||||
const {amount, position} = damages[key];
|
|
||||||
let damage;
|
|
||||||
if (pool.current.length > 0) {
|
|
||||||
damage = pool.current.pop();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
damage = createDamageNode();
|
|
||||||
damagesRef.current.appendChild(damage);
|
|
||||||
}
|
|
||||||
const p = damage.querySelector('p');
|
|
||||||
p.style.scale = scale / 2;
|
|
||||||
p.innerText = Math.abs(amount);
|
|
||||||
damage.style.setProperty('--randomnessX', animation['--randomnessX']);
|
|
||||||
damage.style.setProperty('--randomnessY', animation['--randomnessY']);
|
|
||||||
damage.style.setProperty('rotate', animation['rotate']);
|
|
||||||
damage.style.setProperty('rotate', animation['rotate']);
|
|
||||||
damage.style.setProperty('--magnitude', Math.max(1, Math.floor(Math.log10(Math.abs(amount)))));
|
|
||||||
damage.style.setProperty('--positionX', `${position.x * scale}px`);
|
|
||||||
damage.style.setProperty('--positionY', `${position.y * scale}px`);
|
|
||||||
damage.style.setProperty('zIndex', key);
|
|
||||||
animation.element = damage;
|
|
||||||
}
|
|
||||||
animation.elapsed += elapsed;
|
|
||||||
animation.step += elapsed;
|
|
||||||
const offsetX = 20 * scale * animation['--randomnessX'];
|
|
||||||
const offsetY = -1 * (10 + animation['--randomnessY'] * 45 * scale);
|
|
||||||
// offset
|
|
||||||
if (animation.elapsed <= 0.5) {
|
|
||||||
animation['--offsetX'] = easeOutQuad(animation.elapsed, 0, offsetX, 0.5);
|
|
||||||
animation['--offsetY'] = easeOutQuad(animation.elapsed, 0, offsetY, 0.5);
|
|
||||||
}
|
|
||||||
else if (animation.elapsed > 1.375) {
|
|
||||||
animation['--offsetX'] = offsetX - easeOutQuad(animation.elapsed - 1.375, 0, offsetX, 0.125);
|
|
||||||
animation['--offsetY'] = offsetY - easeOutQuad(animation.elapsed - 1.375, 0, offsetY, 0.125);
|
|
||||||
}
|
|
||||||
// scale
|
|
||||||
if (animation.elapsed <= 0.5) {
|
|
||||||
animation['--scale'] = easeOutQuad(animation.elapsed, 0, 1, 0.5);
|
|
||||||
}
|
|
||||||
else if (animation.elapsed > 0.5 && animation.elapsed < 0.6) {
|
|
||||||
animation['--scale'] = linear(animation.elapsed - 0.5, 1, 0.5, 0.1);
|
|
||||||
}
|
|
||||||
else if (animation.elapsed > 0.6 && animation.elapsed < 0.675) {
|
|
||||||
animation['--scale'] = 1.5 - easeInQuint(animation.elapsed - 0.6, 1, 0.5, 0.075);
|
|
||||||
}
|
|
||||||
else if (animation.elapsed > 0.675 && animation.elapsed < 1.375) {
|
|
||||||
animation['--scale'] = 1;
|
|
||||||
}
|
|
||||||
else if (animation.elapsed > 1.375) {
|
|
||||||
animation['--scale'] = 1 - easeOutQuad(animation.elapsed - 1.375, 0, 1, 0.125);
|
|
||||||
}
|
|
||||||
// fade
|
|
||||||
if (animation.elapsed <= 0.375) {
|
|
||||||
animation['--opacity'] = linear(animation.elapsed, 0.75, 0.25, 0.375);
|
|
||||||
}
|
|
||||||
else if (animation.elapsed > 0.375 && animation.elapsed < 1.375) {
|
|
||||||
animation['--opacity'] = 1;
|
|
||||||
}
|
|
||||||
else if (animation.elapsed > 1.375) {
|
|
||||||
animation['--opacity'] = 1 - linear(animation.elapsed - 1.375, 0, 1, 0.125);
|
|
||||||
}
|
|
||||||
// hue
|
|
||||||
const h = Math.abs((animation.elapsed % 0.3) - 0.15) / 0.15;
|
|
||||||
animation['--hue'] = easeInOutExpo(h, animation.hue[0], animation.hue[1], 1);
|
|
||||||
const step = keys.length > 150 ? (keys.length / 500) * 0.25 : elapsed;
|
|
||||||
if (animation.step > step) {
|
|
||||||
animation.step = animation.step % step;
|
|
||||||
animation.element.style.setProperty('--hue', animation['--hue']);
|
|
||||||
animation.element.style.setProperty('--opacity', animation['--opacity']);
|
|
||||||
animation.element.style.setProperty('--offsetX', animation['--offsetX']);
|
|
||||||
animation.element.style.setProperty('--offsetY', animation['--offsetY']);
|
|
||||||
animation.element.style.setProperty('--scale', animation['--scale']);
|
|
||||||
}
|
|
||||||
if (animation.elapsed > 1.5) {
|
|
||||||
if (pool.current.length < 512) {
|
|
||||||
pool.current.push(animation.element);
|
|
||||||
}
|
|
||||||
animation.element.style.setProperty('--opacity', 0);
|
|
||||||
animation.element = undefined;
|
|
||||||
damages[key].onClose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [damages, scale]);
|
|
||||||
useAnimationFrame(frame);
|
useAnimationFrame(frame);
|
||||||
for (const key in damages) {
|
|
||||||
if (!animations.current[key]) {
|
|
||||||
animations.current[key] = {
|
|
||||||
elapsed: 0,
|
|
||||||
hue: damageHue(damages[key].type),
|
|
||||||
rotate: `${Math.random() * (Math.PI / 16) - (Math.PI / 32)}rad`,
|
|
||||||
step: 0,
|
|
||||||
'--scale': 0.35,
|
|
||||||
'--opacity': 0.75,
|
|
||||||
'--offsetX': 0,
|
|
||||||
'--offsetY': 0,
|
|
||||||
'--randomnessX': 1 * (Math.random() - 0.5),
|
|
||||||
'--randomnessY': Math.random(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={styles.damages}
|
className={styles.damages}
|
||||||
|
|
|
@ -23,22 +23,10 @@
|
||||||
inherits: false;
|
inherits: false;
|
||||||
syntax: '<number>';
|
syntax: '<number>';
|
||||||
}
|
}
|
||||||
@property --randomnessX {
|
|
||||||
initial-value: 0;
|
|
||||||
inherits: false;
|
|
||||||
syntax: '<number>';
|
|
||||||
}
|
|
||||||
@property --randomnessY {
|
|
||||||
initial-value: 0;
|
|
||||||
inherits: false;
|
|
||||||
syntax: '<number>';
|
|
||||||
}
|
|
||||||
|
|
||||||
.damage {
|
.damage {
|
||||||
--hue: 0;
|
--hue: 0;
|
||||||
--opacity: 0.75;
|
--opacity: 0.75;
|
||||||
--randomnessX: 0;
|
|
||||||
--randomnessY: 0;
|
|
||||||
--scale: 0.35;
|
--scale: 0.35;
|
||||||
--background: hsl(var(--hue) 100% 12.5%);
|
--background: hsl(var(--hue) 100% 12.5%);
|
||||||
--foreground: hsl(var(--hue) 100% 50%);
|
--foreground: hsl(var(--hue) 100% 50%);
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import {useEffect, useMemo, useRef, useState} from 'react';
|
import {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
||||||
|
|
||||||
import {useDomScale} from '@/react/context/dom-scale.js';
|
import {useDomScale} from '@/react/context/dom-scale.js';
|
||||||
import {useRadians} from '@/react/context/radians.js';
|
import {useRadians} from '@/react/context/radians.js';
|
||||||
|
import useAnimationFrame from '@/react/hooks/use-animation-frame.js';
|
||||||
import {RESOLUTION} from '@/util/constants.js';
|
import {RESOLUTION} from '@/util/constants.js';
|
||||||
import {render} from '@/util/dialogue.js';
|
import {render} from '@/util/dialogue.js';
|
||||||
|
|
||||||
|
@ -56,20 +57,13 @@ export default function Dialogue({
|
||||||
clearTimeout(handle);
|
clearTimeout(handle);
|
||||||
}
|
}
|
||||||
}, [caret, dialogue]);
|
}, [caret, dialogue]);
|
||||||
useEffect(() => {
|
const updateDimensions = useCallback(() => {
|
||||||
let handle;
|
|
||||||
function track() {
|
|
||||||
if (ref.current) {
|
if (ref.current) {
|
||||||
const {height, width} = ref.current.getBoundingClientRect();
|
const {height, width} = ref.current.getBoundingClientRect();
|
||||||
setDimensions({h: height / domScale, w: width / domScale});
|
setDimensions({h: height / domScale, w: width / domScale});
|
||||||
}
|
}
|
||||||
handle = requestAnimationFrame(track);
|
}, [domScale]);
|
||||||
}
|
useAnimationFrame(updateDimensions);
|
||||||
handle = requestAnimationFrame(track);
|
|
||||||
return () => {
|
|
||||||
cancelAnimationFrame(handle);
|
|
||||||
};
|
|
||||||
}, [dialogue, domScale, ref]);
|
|
||||||
const localRender = useMemo(
|
const localRender = useMemo(
|
||||||
() => render(dialogue.letters, styles.letter),
|
() => render(dialogue.letters, styles.letter),
|
||||||
[dialogue.letters],
|
[dialogue.letters],
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
;
|
;
|
||||||
top: 0;
|
top: 0;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
|
transition: translate 100ms;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
max-width: 66%;
|
max-width: 66%;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import {useState} from 'react';
|
import {useCallback, 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 {useEcsTick} from '@/react/context/ecs.js';
|
||||||
import {parseLetters} from '@/util/dialogue.js';
|
import {parseLetters} from '@/util/dialogue.js';
|
||||||
|
|
||||||
import Damages from './damages.jsx';
|
import Damages from './damages.jsx';
|
||||||
|
@ -13,17 +13,11 @@ export default function Entities({
|
||||||
setChatMessages,
|
setChatMessages,
|
||||||
setMonopolizers,
|
setMonopolizers,
|
||||||
}) {
|
}) {
|
||||||
const [ecs] = useEcs();
|
|
||||||
const [entities, setEntities] = useState({});
|
const [entities, setEntities] = useState({});
|
||||||
const [damages, setDamages] = useState({});
|
|
||||||
const [pendingDamage] = useState({accumulated: [], handle: undefined});
|
|
||||||
usePacket('EcsChange', async () => {
|
usePacket('EcsChange', async () => {
|
||||||
setEntities({});
|
setEntities({});
|
||||||
}, [setEntities]);
|
});
|
||||||
useEcsTick((payload) => {
|
const onEcsTick = useCallback((payload, ecs) => {
|
||||||
if (!ecs) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const deleting = {};
|
const deleting = {};
|
||||||
const updating = {};
|
const updating = {};
|
||||||
for (const id in payload) {
|
for (const id in payload) {
|
||||||
|
@ -91,36 +85,6 @@ export default function Entities({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const {damage} = update.Vulnerable || {};
|
|
||||||
if (damage) {
|
|
||||||
for (const key in damage) {
|
|
||||||
damage[key].onClose = () => {
|
|
||||||
setDamages((damages) => {
|
|
||||||
const {[key]: _, ...rest} = damages; // eslint-disable-line no-unused-vars
|
|
||||||
return rest;
|
|
||||||
})
|
|
||||||
};
|
|
||||||
}
|
|
||||||
pendingDamage.accumulated.push(damage);
|
|
||||||
if (!pendingDamage.handle) {
|
|
||||||
pendingDamage.handle = setTimeout(() => {
|
|
||||||
const update = {};
|
|
||||||
for (const damage of pendingDamage.accumulated) {
|
|
||||||
for (const key in damage) {
|
|
||||||
update[key] = damage[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pendingDamage.accumulated.length = 0;
|
|
||||||
setDamages((damages) => {
|
|
||||||
return {
|
|
||||||
...damages,
|
|
||||||
...update,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
pendingDamage.handle = undefined;
|
|
||||||
}, 50);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
setEntities((entities) => {
|
setEntities((entities) => {
|
||||||
for (const id in deleting) {
|
for (const id in deleting) {
|
||||||
|
@ -131,7 +95,8 @@ export default function Entities({
|
||||||
...updating,
|
...updating,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}, [ecs, setMonopolizers]);
|
}, [setChatMessages, setMonopolizers]);
|
||||||
|
useEcsTick(onEcsTick);
|
||||||
const renderables = [];
|
const renderables = [];
|
||||||
for (const id in entities) {
|
for (const id in entities) {
|
||||||
renderables.push(
|
renderables.push(
|
||||||
|
@ -154,7 +119,6 @@ export default function Entities({
|
||||||
>
|
>
|
||||||
{renderables}
|
{renderables}
|
||||||
<Damages
|
<Damages
|
||||||
damages={damages}
|
|
||||||
scale={scale}
|
scale={scale}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,6 +4,17 @@ import createEcs from '@/server/create/ecs.js';
|
||||||
import ClientEcs from './client-ecs.js';
|
import ClientEcs from './client-ecs.js';
|
||||||
|
|
||||||
const ecs = createEcs(ClientEcs);
|
const ecs = createEcs(ClientEcs);
|
||||||
|
|
||||||
|
[
|
||||||
|
'ClampPositions',
|
||||||
|
'Colliders',
|
||||||
|
'MaintainColliderHash',
|
||||||
|
'VisibleAabbs',
|
||||||
|
]
|
||||||
|
.forEach((system) => {
|
||||||
|
ecs.system(system).active = false;
|
||||||
|
})
|
||||||
|
|
||||||
ecs.$$caret = Math.pow(2, 31);
|
ecs.$$caret = Math.pow(2, 31);
|
||||||
|
|
||||||
const emitter = new Emitter(ecs);
|
const emitter = new Emitter(ecs);
|
||||||
|
@ -17,20 +28,22 @@ addEventListener('message', (particle) => {
|
||||||
.onEnd(() => {});
|
.onEnd(() => {});
|
||||||
});
|
});
|
||||||
|
|
||||||
let last = Date.now();
|
let last = performance.now();
|
||||||
function tick() {
|
function tick(now) {
|
||||||
const now = Date.now();
|
|
||||||
const elapsed = (now - last) / 1000;
|
const elapsed = (now - last) / 1000;
|
||||||
last = now;
|
last = now;
|
||||||
if (ecs.get(1)) {
|
requestAnimationFrame(tick);
|
||||||
|
if (!ecs.get(1)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
ecs.tick(elapsed);
|
ecs.tick(elapsed);
|
||||||
emitter.tick(elapsed);
|
emitter.tick(elapsed);
|
||||||
if ('1' in ecs.diff) {
|
if ('1' in ecs.diff) {
|
||||||
delete ecs.diff['1'];
|
delete ecs.diff['1'];
|
||||||
}
|
}
|
||||||
|
if (Object.keys(ecs.diff).length > 0) {
|
||||||
postMessage(ecs.diff);
|
postMessage(ecs.diff);
|
||||||
ecs.setClean();
|
|
||||||
}
|
}
|
||||||
requestAnimationFrame(tick);
|
ecs.setClean();
|
||||||
}
|
}
|
||||||
requestAnimationFrame(tick);
|
requestAnimationFrame(tick);
|
||||||
|
|
18
app/react/components/pixi/aabb.js
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import {Graphics} from '@pixi/graphics';
|
||||||
|
|
||||||
|
export default class Aabb extends Graphics {
|
||||||
|
constructor({color, width = 0.5}) {
|
||||||
|
super();
|
||||||
|
this.$$color = color;
|
||||||
|
this.$$width = width;
|
||||||
|
}
|
||||||
|
redraw({x0, y0, x1, y1}) {
|
||||||
|
this.clear();
|
||||||
|
this.lineStyle(this.$$width, this.$$color);
|
||||||
|
this.moveTo(x0, y0);
|
||||||
|
this.lineTo(x1, y0);
|
||||||
|
this.lineTo(x1, y1);
|
||||||
|
this.lineTo(x0, y1);
|
||||||
|
this.lineTo(x0, y0);
|
||||||
|
}
|
||||||
|
}
|
21
app/react/components/pixi/crosshair.js
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import {Graphics} from '@pixi/graphics';
|
||||||
|
|
||||||
|
export default function crosshair() {
|
||||||
|
const g = new Graphics();
|
||||||
|
g.clear();
|
||||||
|
g.lineStyle(1, 0x000000);
|
||||||
|
g.moveTo(-5, 0);
|
||||||
|
g.lineTo(5, 0);
|
||||||
|
g.moveTo(0, -5);
|
||||||
|
g.lineTo(0, 5);
|
||||||
|
g.lineStyle(0.5, 0xffffff);
|
||||||
|
g.moveTo(-5, 0);
|
||||||
|
g.lineTo(5, 0);
|
||||||
|
g.moveTo(0, -5);
|
||||||
|
g.lineTo(0, 5);
|
||||||
|
g.lineStyle(1, 0x000000);
|
||||||
|
g.drawCircle(0, 0, 3);
|
||||||
|
g.lineStyle(0.5, 0xffffff);
|
||||||
|
g.drawCircle(0, 0, 3);
|
||||||
|
return g;
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
import {Container} from '@pixi/react';
|
import {Container} from '@pixi/react';
|
||||||
import {useState} from 'react';
|
import {useCallback, useState} from 'react';
|
||||||
|
|
||||||
import {useEcs, useEcsTick} from '@/react/context/ecs.js';
|
import {useEcsTick} from '@/react/context/ecs.js';
|
||||||
import {useMainEntity} from '@/react/context/main-entity.js';
|
import {useMainEntity} from '@/react/context/main-entity.js';
|
||||||
|
|
||||||
import Entities from './entities.jsx';
|
import Entities from './entities.jsx';
|
||||||
|
@ -11,14 +11,13 @@ import TileLayer from './tile-layer.jsx';
|
||||||
import Water from './water.jsx';
|
import Water from './water.jsx';
|
||||||
|
|
||||||
export default function Ecs({camera, monopolizers, particleWorker, scale}) {
|
export default function Ecs({camera, monopolizers, particleWorker, scale}) {
|
||||||
const [ecs] = useEcs();
|
|
||||||
const [mainEntity] = useMainEntity();
|
const [mainEntity] = useMainEntity();
|
||||||
const [layers, setLayers] = useState([]);
|
const [layers, setLayers] = useState([]);
|
||||||
const [hour, setHour] = useState(10);
|
const [hour, setHour] = useState(10);
|
||||||
const [projected, setProjected] = useState([]);
|
const [projected, setProjected] = useState([]);
|
||||||
const [position, setPosition] = useState({x: 0, y: 0});
|
const [position, setPosition] = useState({x: 0, y: 0});
|
||||||
const [water, setWater] = useState();
|
const [water, setWater] = useState();
|
||||||
useEcsTick((payload) => {
|
const onEcsTick = useCallback((payload, ecs) => {
|
||||||
const entity = ecs.get(mainEntity);
|
const entity = ecs.get(mainEntity);
|
||||||
for (const id in payload) {
|
for (const id in payload) {
|
||||||
const update = payload[id];
|
const update = payload[id];
|
||||||
|
@ -43,7 +42,8 @@ export default function Ecs({camera, monopolizers, particleWorker, scale}) {
|
||||||
setPosition(Position.toJSON());
|
setPosition(Position.toJSON());
|
||||||
setProjected(Wielder.activeItem()?.project(Position.tile, Direction.quantize(4)));
|
setProjected(Wielder.activeItem()?.project(Position.tile, Direction.quantize(4)));
|
||||||
}
|
}
|
||||||
}, [ecs, mainEntity, scale]);
|
}, [mainEntity]);
|
||||||
|
useEcsTick(onEcsTick);
|
||||||
return (
|
return (
|
||||||
<Container
|
<Container
|
||||||
scale={scale}
|
scale={scale}
|
||||||
|
@ -71,16 +71,16 @@ export default function Ecs({camera, monopolizers, particleWorker, scale}) {
|
||||||
y={position.y}
|
y={position.y}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Entities
|
|
||||||
monopolizers={monopolizers}
|
|
||||||
particleWorker={particleWorker}
|
|
||||||
/>
|
|
||||||
{projected?.length > 0 && layers[0] && (
|
{projected?.length > 0 && layers[0] && (
|
||||||
<TargetingGhost
|
<TargetingGhost
|
||||||
projected={projected}
|
projected={projected}
|
||||||
tileLayer={layers[0]}
|
tileLayer={layers[0]}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<Entities
|
||||||
|
monopolizers={monopolizers}
|
||||||
|
particleWorker={particleWorker}
|
||||||
|
/>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,52 +0,0 @@
|
||||||
import {Container} from '@pixi/display';
|
|
||||||
import {PixiComponent} from '@pixi/react';
|
|
||||||
import * as particles from '@pixi/particle-emitter';
|
|
||||||
|
|
||||||
const EmitterInternal = PixiComponent('Emitter', {
|
|
||||||
$$emitter: undefined,
|
|
||||||
$$raf: undefined,
|
|
||||||
create() {
|
|
||||||
return new Container();
|
|
||||||
},
|
|
||||||
applyProps(container, oldProps, newProps) {
|
|
||||||
if (!this.$$emitter) {
|
|
||||||
const {onComplete, particle} = newProps;
|
|
||||||
this.$$emitter = new particles.Emitter(container, particle);
|
|
||||||
this.$$emitter._completeCallback = onComplete;
|
|
||||||
let last = Date.now();
|
|
||||||
const render = () => {
|
|
||||||
this.$$raf = requestAnimationFrame(render);
|
|
||||||
const now = Date.now();
|
|
||||||
this.$$emitter.update((now - last) / 1000);
|
|
||||||
last = now;
|
|
||||||
};
|
|
||||||
this.$$emitter.emit = true;
|
|
||||||
render();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
willUnmount() {
|
|
||||||
if (this.$$emitter) {
|
|
||||||
this.$$emitter.emit = false;
|
|
||||||
cancelAnimationFrame(this.$$raf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function Emitter({entity}) {
|
|
||||||
const {Emitter} = entity;
|
|
||||||
const emitters = [];
|
|
||||||
for (const id in Emitter.emitting) {
|
|
||||||
const particle = Emitter.emitting[id];
|
|
||||||
emitters.push(
|
|
||||||
<EmitterInternal
|
|
||||||
key={id}
|
|
||||||
onComplete={() => {
|
|
||||||
delete Emitter.emitting[id];
|
|
||||||
}}
|
|
||||||
particle={particle}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return <>{emitters}</>;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,119 +1,126 @@
|
||||||
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 {useEffect, useState} from 'react';
|
import {useCallback, useEffect, useRef, useState} from 'react';
|
||||||
|
|
||||||
import {usePacket} from '@/react/context/client.js';
|
import {usePacket} from '@/react/context/client.js';
|
||||||
|
import {useDebug} from '@/react/context/debug.js';
|
||||||
import {useEcs, useEcsTick} from '@/react/context/ecs.js';
|
import {useEcs, useEcsTick} from '@/react/context/ecs.js';
|
||||||
import {useMainEntity} from '@/react/context/main-entity.js';
|
import {useMainEntity} from '@/react/context/main-entity.js';
|
||||||
import {useRadians} from '@/react/context/radians.js';
|
import {useRadians} from '@/react/context/radians.js';
|
||||||
|
|
||||||
import Entity from './entity.jsx';
|
import Entity from './entity.js';
|
||||||
|
|
||||||
export default function Entities({monopolizers, particleWorker}) {
|
export default function Entities({monopolizers, particleWorker}) {
|
||||||
|
const [debug] = useDebug();
|
||||||
const [ecs] = useEcs();
|
const [ecs] = useEcs();
|
||||||
const [entities, setEntities] = useState({});
|
const containerRef = useRef();
|
||||||
|
const latestTick = useRef();
|
||||||
|
const entities = useRef({});
|
||||||
|
const pool = useRef([]);
|
||||||
const [mainEntity] = useMainEntity();
|
const [mainEntity] = useMainEntity();
|
||||||
const radians = useRadians();
|
const radians = useRadians();
|
||||||
const [willInteractWith, setWillInteractWith] = useState(0);
|
const willInteractWith = useRef(0);
|
||||||
const [interactionFilters] = useState([
|
const [interactionFilters] = useState([
|
||||||
new AdjustmentFilter(),
|
new AdjustmentFilter(),
|
||||||
new GlowFilter({color: 0x0}),
|
new GlowFilter({color: 0x0}),
|
||||||
]);
|
]);
|
||||||
|
const pulse = (Math.cos(radians / 4) + 1) * 0.5;
|
||||||
|
interactionFilters[0].brightness = (pulse * 0.75) + 1;
|
||||||
|
interactionFilters[1].outerStrength = pulse * 0.5;
|
||||||
|
const updateEntities = useCallback((payload) => {
|
||||||
|
for (const id in payload) {
|
||||||
|
if ('1' === id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!payload[id]) {
|
||||||
|
entities.current[id].removeFromContainer();
|
||||||
|
pool.current.push(entities.current[id]);
|
||||||
|
delete entities.current[id];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!entities.current[id]) {
|
||||||
|
entities.current[id] = pool.current.length > 0
|
||||||
|
? pool.current.pop()
|
||||||
|
: new Entity();
|
||||||
|
entities.current[id].reset(ecs.get(id), debug);
|
||||||
|
if (mainEntity === id) {
|
||||||
|
entities.current[id].setMainEntity();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entities.current[id].update(payload[id], containerRef.current);
|
||||||
|
}
|
||||||
|
}, [debug, ecs, mainEntity])
|
||||||
|
useEffect(() => {
|
||||||
|
if (0 === pool.current.length) {
|
||||||
|
for (let i = 0; i < 1000; ++i) {
|
||||||
|
pool.current.push(new Entity());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
useEffect(() => {
|
||||||
|
for (const key in entities.current) {
|
||||||
|
entities.current[key].setDebug(debug);
|
||||||
|
}
|
||||||
|
}, [debug]);
|
||||||
|
usePacket('EcsChange', async () => {
|
||||||
|
for (const id in entities.current) {
|
||||||
|
entities.current[id].removeFromContainer();
|
||||||
|
}
|
||||||
|
entities.current = {};
|
||||||
|
});
|
||||||
|
const onEcsTickEntities = useCallback((payload) => {
|
||||||
|
updateEntities(payload);
|
||||||
|
}, [updateEntities]);
|
||||||
|
useEcsTick(onEcsTickEntities);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ecs || !particleWorker) {
|
if (!ecs || !particleWorker) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
async function onMessage(diff) {
|
async function onMessage(diff) {
|
||||||
|
latestTick.current = Promise.resolve(latestTick.current).then(async () => {
|
||||||
await ecs.apply(diff.data);
|
await ecs.apply(diff.data);
|
||||||
const deleted = {};
|
updateEntities(diff.data);
|
||||||
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);
|
particleWorker.addEventListener('message', onMessage);
|
||||||
return () => {
|
return () => {
|
||||||
particleWorker.removeEventListener('message', onMessage);
|
particleWorker.removeEventListener('message', onMessage);
|
||||||
};
|
};
|
||||||
}, [ecs, particleWorker]);
|
}, [ecs, particleWorker, updateEntities]);
|
||||||
const pulse = (Math.cos(radians / 4) + 1) * 0.5;
|
const onEcsTickParticles = useCallback((payload) => {
|
||||||
interactionFilters[0].brightness = (pulse * 0.75) + 1;
|
|
||||||
interactionFilters[1].outerStrength = pulse * 0.5;
|
|
||||||
usePacket('EcsChange', async () => {
|
|
||||||
setEntities({});
|
|
||||||
}, [setEntities]);
|
|
||||||
useEcsTick((payload) => {
|
|
||||||
if (!ecs) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const deleting = {};
|
|
||||||
const updating = {};
|
|
||||||
for (const id in payload) {
|
for (const id in payload) {
|
||||||
if ('1' === id) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const update = payload[id];
|
const update = payload[id];
|
||||||
if (false === update) {
|
|
||||||
deleting[id] = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
updating[id] = ecs.get(id);
|
|
||||||
if (update.Emitter?.emit) {
|
if (update.Emitter?.emit) {
|
||||||
for (const id in update.Emitter.emit) {
|
for (const id in update.Emitter.emit) {
|
||||||
particleWorker?.postMessage(update.Emitter.emit[id]);
|
particleWorker?.postMessage(update.Emitter.emit[id]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setEntities((entities) => {
|
}, [particleWorker]);
|
||||||
for (const id in deleting) {
|
useEcsTick(onEcsTickParticles);
|
||||||
delete entities[id];
|
const onEcsTickInteractions = useCallback((payload, ecs) => {
|
||||||
}
|
|
||||||
return {
|
|
||||||
...entities,
|
|
||||||
...updating,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}, [ecs, particleWorker]);
|
|
||||||
useEcsTick(() => {
|
|
||||||
if (!ecs) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const main = ecs.get(mainEntity);
|
const main = ecs.get(mainEntity);
|
||||||
if (main) {
|
if (main) {
|
||||||
setWillInteractWith(main.Interacts.willInteractWith);
|
if (willInteractWith.current !== main.Interacts.willInteractWith) {
|
||||||
|
if (entities.current[willInteractWith.current]) {
|
||||||
|
entities.current[willInteractWith.current].diffuse.filters = [];
|
||||||
}
|
}
|
||||||
}, [ecs, mainEntity]);
|
willInteractWith.current = main.Interacts.willInteractWith;
|
||||||
const renderables = [];
|
|
||||||
for (const id in entities) {
|
|
||||||
const isHighlightedInteraction = 0 === monopolizers.length && id == willInteractWith;
|
|
||||||
renderables.push(
|
|
||||||
<Entity
|
|
||||||
filters={isHighlightedInteraction ? interactionFilters : null}
|
|
||||||
entity={entities[id]}
|
|
||||||
key={id}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
const interacting = entities.current[main.Interacts.willInteractWith];
|
||||||
|
if (interacting) {
|
||||||
|
interacting.diffuse.filters = 0 === monopolizers.length
|
||||||
|
? interactionFilters
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [interactionFilters, mainEntity, monopolizers]);
|
||||||
|
useEcsTick(onEcsTickInteractions);
|
||||||
return (
|
return (
|
||||||
<Container
|
<Container
|
||||||
|
ref={containerRef}
|
||||||
sortableChildren
|
sortableChildren
|
||||||
>
|
/>
|
||||||
{renderables}
|
|
||||||
</Container>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
197
app/react/components/pixi/entity.js
Normal file
|
@ -0,0 +1,197 @@
|
||||||
|
import {Assets} from '@pixi/assets';
|
||||||
|
import {Container as PixiContainer} from '@pixi/display';
|
||||||
|
import {Sprite} from '@pixi/sprite';
|
||||||
|
|
||||||
|
import Aabb from './aabb.js';
|
||||||
|
import createCrosshair from './crosshair.js';
|
||||||
|
import {deferredLighting, PointLight} from './lights.js';
|
||||||
|
|
||||||
|
export default class Entity {
|
||||||
|
static assets = {};
|
||||||
|
attached = false;
|
||||||
|
colliderAabb = new Aabb({color: 0xffffff});
|
||||||
|
container = new PixiContainer();
|
||||||
|
crosshair = createCrosshair();
|
||||||
|
debug = new PixiContainer();
|
||||||
|
diffuse = new Sprite();
|
||||||
|
isMainEntity = false;
|
||||||
|
normals = new Sprite();
|
||||||
|
point;
|
||||||
|
sprite = new PixiContainer();
|
||||||
|
visibleAabb = new Aabb({color: 0xff00ff});
|
||||||
|
constructor() {
|
||||||
|
this.diffuse.parentGroup = deferredLighting.diffuseGroup;
|
||||||
|
this.normals.parentGroup = deferredLighting.normalGroup;
|
||||||
|
this.container.addChild(this.diffuse);
|
||||||
|
this.container.addChild(this.normals);
|
||||||
|
this.container.addChild(this.crosshair);
|
||||||
|
this.debug.addChild(this.colliderAabb);
|
||||||
|
this.debug.addChild(this.visibleAabb);
|
||||||
|
}
|
||||||
|
static async loadAsset(source = '') {
|
||||||
|
if (!this.assets['']) {
|
||||||
|
const {Texture} = await import('@pixi/core');
|
||||||
|
this.assets[''] = {data: {meta: {}}, textures: {'': Texture.WHITE}};
|
||||||
|
}
|
||||||
|
if (!this.assets[source]) {
|
||||||
|
this.assets[source] = Assets.load(source);
|
||||||
|
}
|
||||||
|
return this.assets[source];
|
||||||
|
}
|
||||||
|
removeFromContainer() {
|
||||||
|
this.container.parent.removeChild(this.container);
|
||||||
|
this.debug.parent.removeChild(this.debug);
|
||||||
|
this.attached = false;
|
||||||
|
}
|
||||||
|
reset(entity, debug) {
|
||||||
|
this.entity = entity;
|
||||||
|
if (this.light) {
|
||||||
|
this.container.removeChild(this.light);
|
||||||
|
this.light = undefined;
|
||||||
|
}
|
||||||
|
this.setDebug(debug);
|
||||||
|
this.isMainEntity = false;
|
||||||
|
if (this.interactionAabb) {
|
||||||
|
this.debug.removeChild(this.interactionAabb);
|
||||||
|
}
|
||||||
|
this.interactionAabb = undefined;
|
||||||
|
}
|
||||||
|
setDebug(isDebugging) {
|
||||||
|
if (isDebugging) {
|
||||||
|
this.crosshair.alpha = 1;
|
||||||
|
this.debug.alpha = 1;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.crosshair.alpha = 0;
|
||||||
|
this.debug.alpha = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setMainEntity() {
|
||||||
|
if (this.isMainEntity) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.isMainEntity = true;
|
||||||
|
this.interactionAabb = new Aabb({color: 0x00ff00});
|
||||||
|
this.debug.addChild(this.interactionAabb);
|
||||||
|
}
|
||||||
|
static textureFromAsset(asset, animation, frame) {
|
||||||
|
if (!asset) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
let texture;
|
||||||
|
if (asset.data.animations) {
|
||||||
|
if (!animation) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
texture = asset.animations[animation][frame];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
texture = asset.textures[''];
|
||||||
|
}
|
||||||
|
return texture;
|
||||||
|
}
|
||||||
|
update({Direction, Light, Position, Sprite, VisibleAabb}, container) {
|
||||||
|
if (Light) {
|
||||||
|
if (!this.light) {
|
||||||
|
this.light = new PointLight(
|
||||||
|
0xffffff - 0x2244cc,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.light.brightness = Light.brightness;
|
||||||
|
}
|
||||||
|
if (Position) {
|
||||||
|
const {x, y} = this.entity.Position;
|
||||||
|
this.container.x = x;
|
||||||
|
this.container.y = y;
|
||||||
|
this.container.zIndex = y;
|
||||||
|
}
|
||||||
|
if (Sprite) {
|
||||||
|
const {diffuse, normals} = this;
|
||||||
|
if (!this.attached || 'alpha' in Sprite) {
|
||||||
|
diffuse.alpha = normals.alpha = this.entity.Sprite.alpha;
|
||||||
|
}
|
||||||
|
if (!this.attached || 'anchorX' in Sprite) {
|
||||||
|
diffuse.anchor.x = normals.anchor.x = this.entity.Sprite.anchorX;
|
||||||
|
}
|
||||||
|
if (!this.attached || 'anchorY' in Sprite) {
|
||||||
|
diffuse.anchor.y = normals.anchor.y = this.entity.Sprite.anchorY;
|
||||||
|
}
|
||||||
|
if (!this.attached || 'scaleX' in Sprite) {
|
||||||
|
diffuse.scale.x = normals.scale.x = this.entity.Sprite.scaleX;
|
||||||
|
}
|
||||||
|
if (!this.attached || 'scaleY' in Sprite) {
|
||||||
|
diffuse.scale.y = normals.scale.y = this.entity.Sprite.scaleY;
|
||||||
|
}
|
||||||
|
if (!this.attached || 'tint' in Sprite) {
|
||||||
|
diffuse.tint = normals.tint = this.entity.Sprite.tint;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!this.attached
|
||||||
|
|| ('source' in Sprite)
|
||||||
|
|| ('animation' in Sprite)
|
||||||
|
|| ('frame' in Sprite)
|
||||||
|
) {
|
||||||
|
this.constructor.loadAsset(this.entity.Sprite.source).then(async (asset) => {
|
||||||
|
if (!this.entity?.Sprite) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const texture = await this.constructor.textureFromAsset(
|
||||||
|
asset,
|
||||||
|
this.entity.Sprite.animation,
|
||||||
|
this.entity.Sprite.frame,
|
||||||
|
);
|
||||||
|
diffuse.texture = texture;
|
||||||
|
if (asset.data.meta.normals) {
|
||||||
|
const {pathname} = new URL(
|
||||||
|
Sprite.source
|
||||||
|
.split('/')
|
||||||
|
.slice(0, -1)
|
||||||
|
.concat(asset.data.meta.normals)
|
||||||
|
.join('/'),
|
||||||
|
'http://example.org',
|
||||||
|
);
|
||||||
|
this.constructor.loadAsset(pathname).then(async (asset) => {
|
||||||
|
const texture = await this.constructor.textureFromAsset(
|
||||||
|
asset,
|
||||||
|
this.entity.Sprite.animation,
|
||||||
|
this.entity.Sprite.frame,
|
||||||
|
);
|
||||||
|
normals.texture = texture;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!this.attached) {
|
||||||
|
const {diffuse, normals} = this;
|
||||||
|
diffuse.rotation = 0;
|
||||||
|
normals.rotation = 0;
|
||||||
|
}
|
||||||
|
if (Direction) {
|
||||||
|
const {diffuse, normals} = this;
|
||||||
|
if (!this.attached || 'direction' in Direction) {
|
||||||
|
if (this.entity.Sprite.rotates) {
|
||||||
|
const rotation = this.entity.Direction.direction + this.entity.Sprite.rotation;
|
||||||
|
diffuse.rotation = rotation;
|
||||||
|
normals.rotation = rotation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.entity.Collider) {
|
||||||
|
this.colliderAabb.redraw(this.entity.Collider.aabb);
|
||||||
|
}
|
||||||
|
if (VisibleAabb) {
|
||||||
|
this.visibleAabb.redraw(this.entity.VisibleAabb);
|
||||||
|
}
|
||||||
|
if (this.isMainEntity) {
|
||||||
|
this.interactionAabb.redraw(this.entity.Interacts.aabb());
|
||||||
|
}
|
||||||
|
if (this.attached || !container) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
container.addChild(this.container);
|
||||||
|
container.addChild(this.debug);
|
||||||
|
this.attached = true;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,120 +0,0 @@
|
||||||
import {Container, Graphics} from '@pixi/react';
|
|
||||||
import {memo, useCallback} from 'react';
|
|
||||||
|
|
||||||
import {useDebug} from '@/react/context/debug.js';
|
|
||||||
import {useMainEntity} from '@/react/context/main-entity.js';
|
|
||||||
|
|
||||||
import Light from './light.jsx';
|
|
||||||
import SpriteComponent from './sprite.jsx';
|
|
||||||
|
|
||||||
function Aabb({color, width = 0.5, x0, y0, x1, y1, ...rest}) {
|
|
||||||
const draw = useCallback((g) => {
|
|
||||||
g.clear();
|
|
||||||
g.lineStyle(width, color);
|
|
||||||
g.moveTo(x0, y0);
|
|
||||||
g.lineTo(x1, y0);
|
|
||||||
g.lineTo(x1, y1);
|
|
||||||
g.lineTo(x0, y1);
|
|
||||||
g.lineTo(x0, y0);
|
|
||||||
}, [color, width, x0, x1, y0, y1]);
|
|
||||||
return (
|
|
||||||
<Graphics draw={draw} x={0} y={0} {...rest} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Crosshair() {
|
|
||||||
const draw = useCallback((g) => {
|
|
||||||
g.clear();
|
|
||||||
g.lineStyle(1, 0x000000);
|
|
||||||
g.moveTo(-5, 0);
|
|
||||||
g.lineTo(5, 0);
|
|
||||||
g.moveTo(0, -5);
|
|
||||||
g.lineTo(0, 5);
|
|
||||||
g.lineStyle(0.5, 0xffffff);
|
|
||||||
g.moveTo(-5, 0);
|
|
||||||
g.lineTo(5, 0);
|
|
||||||
g.moveTo(0, -5);
|
|
||||||
g.lineTo(0, 5);
|
|
||||||
g.lineStyle(1, 0x000000);
|
|
||||||
g.drawCircle(0, 0, 3);
|
|
||||||
g.lineStyle(0.5, 0xffffff);
|
|
||||||
g.drawCircle(0, 0, 3);
|
|
||||||
}, []);
|
|
||||||
return (
|
|
||||||
<Graphics draw={draw} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Entity({entity, ...rest}) {
|
|
||||||
const [debug] = useDebug();
|
|
||||||
const [mainEntity] = useMainEntity();
|
|
||||||
if (!entity) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const {Direction, id, Sprite} = entity;
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<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}
|
|
||||||
{...entity.VisibleAabb}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{debug && entity.Collider && (
|
|
||||||
<>
|
|
||||||
<Aabb
|
|
||||||
color={0xffffff}
|
|
||||||
width={0.5}
|
|
||||||
{...entity.Collider.aabb}
|
|
||||||
/>
|
|
||||||
{entity.Collider.aabbs.map((aabb, i) => (
|
|
||||||
<Aabb
|
|
||||||
color={0xffff00}
|
|
||||||
width={0.25}
|
|
||||||
key={i}
|
|
||||||
{...aabb}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{debug && mainEntity == entity.id && (
|
|
||||||
<Aabb
|
|
||||||
color={0x00ff00}
|
|
||||||
{...entity.Interacts.aabb()}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default memo(Entity);
|
|
|
@ -1,34 +0,0 @@
|
||||||
import {PixiComponent} from '@pixi/react';
|
|
||||||
|
|
||||||
import {PointLight} from './lights.js';
|
|
||||||
|
|
||||||
const LightInternal = PixiComponent('Light', {
|
|
||||||
create({brightness}) {
|
|
||||||
const light = new PointLight(
|
|
||||||
0xffffff - 0x2244cc,
|
|
||||||
brightness,
|
|
||||||
);
|
|
||||||
// light.shader.program.fragmentSrc = light.shader.program.fragmentSrc.replace(
|
|
||||||
// 'float D = length(lightVector)',
|
|
||||||
// 'float D = length(lightVector) / 1.0',
|
|
||||||
// );
|
|
||||||
// light.shader.program.fragmentSrc = light.shader.program.fragmentSrc.replace(
|
|
||||||
// 'intensity = diffuse * attenuation',
|
|
||||||
// 'intensity = diffuse * (attenuation * 2.0)',
|
|
||||||
// );
|
|
||||||
// light.falloff = [0.5, 5, 50];
|
|
||||||
// light.falloff = light.falloff.map((n, i) => n / (2 + i));
|
|
||||||
// light.parentGroup = deferredLighting.lightGroup;
|
|
||||||
// delete light.parentGroup;
|
|
||||||
return light;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function Light({brightness}) {
|
|
||||||
return (
|
|
||||||
<LightInternal
|
|
||||||
brightness={brightness}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,102 +0,0 @@
|
||||||
import {Sprite as PixiSprite} from '@pixi/react';
|
|
||||||
import {memo, useEffect, useState} from 'react';
|
|
||||||
|
|
||||||
import {useAsset} from '@/react/context/assets.js';
|
|
||||||
|
|
||||||
import {deferredLighting} from './lights.js';
|
|
||||||
|
|
||||||
function textureFromAsset(asset, animation, frame) {
|
|
||||||
if (!asset) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
let texture;
|
|
||||||
if (asset.data.animations) {
|
|
||||||
if (!animation) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
texture = asset.animations[animation][frame];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
texture = asset.textures[''];
|
|
||||||
}
|
|
||||||
return texture;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 asset = useAsset(source);
|
|
||||||
const normalsAsset = useAsset(normals);
|
|
||||||
useEffect(() => {
|
|
||||||
if (!asset) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const {normals} = asset.data.meta;
|
|
||||||
if (normals) {
|
|
||||||
const {pathname} = new URL(
|
|
||||||
source.split('/').slice(0, -1).concat(normals).join('/'),
|
|
||||||
'http://example.org',
|
|
||||||
);
|
|
||||||
setNormals(pathname);
|
|
||||||
}
|
|
||||||
}, [asset, source]);
|
|
||||||
const texture = textureFromAsset(
|
|
||||||
asset,
|
|
||||||
animation,
|
|
||||||
frame,
|
|
||||||
);
|
|
||||||
const normalsTexture = textureFromAsset(
|
|
||||||
normalsAsset,
|
|
||||||
animation,
|
|
||||||
frame,
|
|
||||||
);
|
|
||||||
if (mounted) {
|
|
||||||
mounted.parentGroup = deferredLighting.diffuseGroup;
|
|
||||||
}
|
|
||||||
if (normalsMounted) {
|
|
||||||
normalsMounted.parentGroup = deferredLighting.normalGroup;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{texture && (
|
|
||||||
<PixiSprite
|
|
||||||
alpha={alpha}
|
|
||||||
anchor={anchor}
|
|
||||||
ref={setMounted}
|
|
||||||
{...(rotates ? {rotation: direction + rotation} : {})}
|
|
||||||
scale={scale}
|
|
||||||
texture={texture}
|
|
||||||
{...(0 !== tint ? {tint} : {})}
|
|
||||||
{...rest}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{normalsTexture && (
|
|
||||||
<PixiSprite
|
|
||||||
alpha={alpha}
|
|
||||||
anchor={anchor}
|
|
||||||
ref={setNormalsMounted}
|
|
||||||
{...(rotates ? {rotation: direction + rotation} : {})}
|
|
||||||
scale={scale}
|
|
||||||
texture={normalsTexture}
|
|
||||||
{...rest}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default memo(Sprite);
|
|
|
@ -4,19 +4,19 @@ import {PixiComponent} from '@pixi/react';
|
||||||
|
|
||||||
import {useRadians} from '@/react/context/radians.js';
|
import {useRadians} from '@/react/context/radians.js';
|
||||||
|
|
||||||
|
import {deferredLighting} from './lights.js';
|
||||||
|
|
||||||
const tileSize = {x: 16, y: 16};
|
const tileSize = {x: 16, y: 16};
|
||||||
|
|
||||||
const TargetingGhostInternal = PixiComponent('TargetingGhost', {
|
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.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};
|
||||||
|
@ -25,7 +25,9 @@ const TargetingGhostInternal = PixiComponent('TargetingGhost', {
|
||||||
targetInner.position.y = 0.5;
|
targetInner.position.y = 0.5;
|
||||||
// ...
|
// ...
|
||||||
const container = new Container();
|
const container = new Container();
|
||||||
|
container.alpha = 0.4;
|
||||||
container.addChild(target, targetInner);
|
container.addChild(target, targetInner);
|
||||||
|
container.parentGroup = deferredLighting.diffuseGroup;
|
||||||
return container;
|
return container;
|
||||||
},
|
},
|
||||||
applyProps: (container, oldProps, {x, y, radians}) => {
|
applyProps: (container, oldProps, {x, y, radians}) => {
|
||||||
|
|
|
@ -35,6 +35,7 @@ function Ui({disconnected}) {
|
||||||
// Key input.
|
// Key input.
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
const chatInputRef = useRef();
|
const chatInputRef = useRef();
|
||||||
|
const latestTick = useRef();
|
||||||
const gameRef = useRef();
|
const gameRef = useRef();
|
||||||
const [mainEntity, setMainEntity] = useMainEntity();
|
const [mainEntity, setMainEntity] = useMainEntity();
|
||||||
const [debug, setDebug] = useDebug();
|
const [debug, setDebug] = useDebug();
|
||||||
|
@ -310,35 +311,23 @@ function Ui({disconnected}) {
|
||||||
setDebug,
|
setDebug,
|
||||||
setScale,
|
setScale,
|
||||||
]);
|
]);
|
||||||
usePacket('EcsChange', async () => {
|
const onEcsChangePacket = useCallback(() => {
|
||||||
refreshEcs();
|
refreshEcs();
|
||||||
setMainEntity(undefined);
|
setMainEntity(undefined);
|
||||||
setMonopolizers([]);
|
setMonopolizers([]);
|
||||||
}, [refreshEcs, setMainEntity, setMonopolizers]);
|
}, [refreshEcs, setMainEntity]);
|
||||||
usePacket('Tick', async (payload, client) => {
|
usePacket('EcsChange', onEcsChangePacket);
|
||||||
|
const onTickPacket = useCallback(async (payload, client) => {
|
||||||
if (0 === Object.keys(payload.ecs).length) {
|
if (0 === Object.keys(payload.ecs).length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
latestTick.current = Promise.resolve(latestTick.current).then(async () => {
|
||||||
await ecs.apply(payload.ecs);
|
await ecs.apply(payload.ecs);
|
||||||
client.emitter.invoke(':Ecs', 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]);
|
}, [ecs]);
|
||||||
useEcsTick((payload) => {
|
usePacket('Tick', onTickPacket);
|
||||||
|
const onEcsTick = useCallback((payload, ecs) => {
|
||||||
let localMainEntity = mainEntity;
|
let localMainEntity = mainEntity;
|
||||||
for (const id in payload) {
|
for (const id in payload) {
|
||||||
const entity = ecs.get(id);
|
const entity = ecs.get(id);
|
||||||
|
@ -346,14 +335,6 @@ function Ui({disconnected}) {
|
||||||
if (!update) {
|
if (!update) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (update.Direction && entity.Collider) {
|
|
||||||
entity.Collider.updateAabbs();
|
|
||||||
}
|
|
||||||
if (update.Sound?.play) {
|
|
||||||
for (const sound of update.Sound.play) {
|
|
||||||
(new Audio(sound)).play();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (update.MainEntity) {
|
if (update.MainEntity) {
|
||||||
setMainEntity(localMainEntity = id);
|
setMainEntity(localMainEntity = id);
|
||||||
}
|
}
|
||||||
|
@ -390,15 +371,60 @@ function Ui({disconnected}) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (localMainEntity) {
|
}, [hotbarHideHandle, mainEntity, setMainEntity]);
|
||||||
const mainEntityEntity = ecs.get(localMainEntity);
|
useEcsTick(onEcsTick);
|
||||||
|
const onEcsTickParticles = useCallback((payload, ecs) => {
|
||||||
|
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(onEcsTickParticles);
|
||||||
|
const onEcsTickSound = useCallback((payload) => {
|
||||||
|
for (const id in payload) {
|
||||||
|
const update = payload[id];
|
||||||
|
if (update.Sound?.play) {
|
||||||
|
for (const sound of update.Sound.play) {
|
||||||
|
(new Audio(sound)).play();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
useEcsTick(onEcsTickSound);
|
||||||
|
const onEcsTickAabbs = useCallback((payload, ecs) => {
|
||||||
|
for (const id in payload) {
|
||||||
|
const entity = ecs.get(id);
|
||||||
|
const update = payload[id];
|
||||||
|
if (!update) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (update.Direction && entity.Collider) {
|
||||||
|
entity.Collider.updateAabbs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
useEcsTick(onEcsTickAabbs);
|
||||||
|
const onEcsTickCamera = useCallback((payload, ecs) => {
|
||||||
|
if (mainEntity) {
|
||||||
|
const mainEntityEntity = ecs.get(mainEntity);
|
||||||
const x = Math.round((mainEntityEntity.Camera.x * scale) - RESOLUTION.x / 2);
|
const x = Math.round((mainEntityEntity.Camera.x * scale) - RESOLUTION.x / 2);
|
||||||
const y = Math.round((mainEntityEntity.Camera.y * scale) - RESOLUTION.y / 2);
|
const y = Math.round((mainEntityEntity.Camera.y * scale) - RESOLUTION.y / 2);
|
||||||
if (x !== camera.x || y !== camera.y) {
|
if (x !== camera.x || y !== camera.y) {
|
||||||
setCamera({x, y});
|
setCamera({x, y});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [camera, ecs, hotbarHideHandle, mainEntity, scale]);
|
}, [camera, mainEntity, scale]);
|
||||||
|
useEcsTick(onEcsTickCamera);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function onContextMenu(event) {
|
function onContextMenu(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
|
@ -8,7 +8,7 @@ export function useClient() {
|
||||||
return useContext(context);
|
return useContext(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePacket(type, fn, dependencies) {
|
export function usePacket(type, fn) {
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!client) {
|
if (!client) {
|
||||||
|
@ -21,6 +21,5 @@ export function usePacket(type, fn, dependencies) {
|
||||||
return () => {
|
return () => {
|
||||||
client.removePacketListener(type, listener);
|
client.removePacketListener(type, listener);
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
}, [client, fn]);
|
||||||
}, [client, ...dependencies]);
|
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import {createContext, useContext} from 'react';
|
import {createContext, useCallback, useContext} from 'react';
|
||||||
|
|
||||||
import {usePacket} from './client.js';
|
import {usePacket} from './client.js';
|
||||||
|
|
||||||
|
@ -10,7 +10,13 @@ export function useEcs() {
|
||||||
return useContext(context);
|
return useContext(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useEcsTick(fn, dependencies) {
|
export function useEcsTick(fn) {
|
||||||
const [ecs] = useEcs();
|
const [ecs] = useEcs();
|
||||||
usePacket(':Ecs', fn, [ecs, ...dependencies]);
|
const memo = useCallback((payload) => {
|
||||||
|
if (!ecs) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fn(payload, ecs);
|
||||||
|
}, [ecs, fn]);
|
||||||
|
usePacket(':Ecs', memo);
|
||||||
}
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import {json} from "@remix-run/node";
|
import {json} from "@remix-run/node";
|
||||||
import {useEffect, useState} from 'react';
|
import {useCallback, useEffect, useState} from 'react';
|
||||||
import {useOutletContext, useParams} from 'react-router-dom';
|
import {useOutletContext, useParams} from 'react-router-dom';
|
||||||
|
|
||||||
import Ui from '@/react/components/ui.jsx';
|
import Ui from '@/react/components/ui.jsx';
|
||||||
|
@ -9,6 +9,7 @@ import DebugContext from '@/react/context/debug.js';
|
||||||
import EcsContext from '@/react/context/ecs.js';
|
import EcsContext from '@/react/context/ecs.js';
|
||||||
import MainEntityContext from '@/react/context/main-entity.js';
|
import MainEntityContext from '@/react/context/main-entity.js';
|
||||||
import RadiansContext from '@/react/context/radians.js';
|
import RadiansContext from '@/react/context/radians.js';
|
||||||
|
import useAnimationFrame from '@/react/hooks/use-animation-frame.js';
|
||||||
import {juggleSession} from '@/server/session.server.js';
|
import {juggleSession} from '@/server/session.server.js';
|
||||||
import {TAU} from '@/util/math.js';
|
import {TAU} from '@/util/math.js';
|
||||||
|
|
||||||
|
@ -29,23 +30,10 @@ export default function PlaySpecific() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const [type, url] = params['*'].split('/');
|
const [type, url] = params['*'].split('/');
|
||||||
const [radians, setRadians] = useState(0);
|
const [radians, setRadians] = useState(0);
|
||||||
useEffect(() => {
|
const spin = useCallback((elapsed) => {
|
||||||
let handle;
|
|
||||||
let last;
|
|
||||||
const spin = (ts) => {
|
|
||||||
if ('undefined' === typeof last) {
|
|
||||||
last = ts;
|
|
||||||
}
|
|
||||||
const elapsed = (ts - last) / 1000;
|
|
||||||
last = ts;
|
|
||||||
setRadians((radians) => radians + (elapsed * TAU));
|
setRadians((radians) => radians + (elapsed * TAU));
|
||||||
handle = requestAnimationFrame(spin);
|
|
||||||
};
|
|
||||||
handle = requestAnimationFrame(spin);
|
|
||||||
return () => {
|
|
||||||
cancelAnimationFrame(handle);
|
|
||||||
};
|
|
||||||
}, []);
|
}, []);
|
||||||
|
useAnimationFrame(spin);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!Client) {
|
if (!Client) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -1,5 +1,27 @@
|
||||||
import data from '../../../public/assets/dev/homestead.json';
|
import data from '../../../public/assets/dev/homestead.json';
|
||||||
|
|
||||||
|
function animal() {
|
||||||
|
return {
|
||||||
|
Alive: {health: 100},
|
||||||
|
Behaving: {},
|
||||||
|
Collider: {},
|
||||||
|
Controlled: {},
|
||||||
|
Direction: {},
|
||||||
|
Emitter: {},
|
||||||
|
Forces: {},
|
||||||
|
Interlocutor: {},
|
||||||
|
Position: {},
|
||||||
|
Speed: {speed: 20},
|
||||||
|
Sprite: {
|
||||||
|
animation: 'moving:down',
|
||||||
|
},
|
||||||
|
Tags: {},
|
||||||
|
Ticking: {},
|
||||||
|
VisibleAabb: {},
|
||||||
|
Vulnerable: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export default async function createHomestead(id) {
|
export default async function createHomestead(id) {
|
||||||
const area = {x: 100, y: 60};
|
const area = {x: 100, y: 60};
|
||||||
const entities = [];
|
const entities = [];
|
||||||
|
@ -138,8 +160,10 @@ export default async function createHomestead(id) {
|
||||||
Ticking: {},
|
Ticking: {},
|
||||||
VisibleAabb: {},
|
VisibleAabb: {},
|
||||||
});
|
});
|
||||||
const kitty = {
|
const animalJson = animal();
|
||||||
Alive: {health: 100},
|
for (let i = 0; i < 50; ++i) {
|
||||||
|
entities.push({
|
||||||
|
...animalJson,
|
||||||
Behaving: {
|
Behaving: {
|
||||||
routines: {
|
routines: {
|
||||||
initial: '/assets/kitty/initial.js',
|
initial: '/assets/kitty/initial.js',
|
||||||
|
@ -157,10 +181,6 @@ export default async function createHomestead(id) {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
Controlled: {},
|
|
||||||
Direction: {},
|
|
||||||
Emitter: {},
|
|
||||||
Forces: {},
|
|
||||||
Interactive: {
|
Interactive: {
|
||||||
interacting: 1,
|
interacting: 1,
|
||||||
interactScript: `
|
interactScript: `
|
||||||
|
@ -181,22 +201,122 @@ export default async function createHomestead(id) {
|
||||||
})
|
})
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
Interlocutor: {},
|
Position: {
|
||||||
Position: {x: 250, y: 250},
|
x: 250 + (Math.random() - 0.5) * 300,
|
||||||
Speed: {speed: 20},
|
y: 250 + (Math.random() - 0.5) * 300,
|
||||||
|
},
|
||||||
Sprite: {
|
Sprite: {
|
||||||
|
...animalJson.Sprite,
|
||||||
anchorX: 0.5,
|
anchorX: 0.5,
|
||||||
anchorY: 0.7,
|
anchorY: 0.7,
|
||||||
source: '/assets/kitty/kitty.json',
|
source: '/assets/kitty/kitty.json',
|
||||||
speed: 0.115,
|
speed: 0.115,
|
||||||
},
|
},
|
||||||
Tags: {tags: ['kittan']},
|
Tags: {tags: ['kittan']},
|
||||||
Ticking: {},
|
});
|
||||||
VisibleAabb: {},
|
}
|
||||||
Vulnerable: {},
|
|
||||||
};
|
|
||||||
for (let i = 0; i < 50; ++i) {
|
for (let i = 0; i < 50; ++i) {
|
||||||
entities.push(kitty);
|
entities.push({
|
||||||
|
...animalJson,
|
||||||
|
Behaving: {
|
||||||
|
routines: {
|
||||||
|
initial: '/assets/farm/animals/cow-adult/initial.js',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Collider: {
|
||||||
|
bodies: [
|
||||||
|
{
|
||||||
|
points: [
|
||||||
|
{x: -3.5, y: -3.5},
|
||||||
|
{x: 3.5, y: -3.5},
|
||||||
|
{x: 3.5, y: 3.5},
|
||||||
|
{x: -3.5, y: 3.5},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Interactive: {
|
||||||
|
interacting: 1,
|
||||||
|
interactScript: `
|
||||||
|
const lines = [
|
||||||
|
'sno<shake>rr</shake>t',
|
||||||
|
'm<wave>ooooooooooo</wave>',
|
||||||
|
];
|
||||||
|
const line = lines[Math.floor(Math.random() * lines.length)];
|
||||||
|
subject.Interlocutor.dialogue({
|
||||||
|
body: line,
|
||||||
|
linger: 2,
|
||||||
|
offset: {x: 0, y: -16},
|
||||||
|
origin: 'track',
|
||||||
|
position: 'track',
|
||||||
|
})
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
Position: {
|
||||||
|
x: 350 + (Math.random() - 0.5) * 300,
|
||||||
|
y: 350 + (Math.random() - 0.5) * 300,
|
||||||
|
},
|
||||||
|
Sprite: {
|
||||||
|
...animalJson.Sprite,
|
||||||
|
anchorX: 0.5,
|
||||||
|
anchorY: 0.8,
|
||||||
|
source: '/assets/farm/animals/cow-adult/cow-adult.json',
|
||||||
|
speed: 0.25,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (let i = 0; i < 50; ++i) {
|
||||||
|
entities.push({
|
||||||
|
...animalJson,
|
||||||
|
Behaving: {
|
||||||
|
routines: {
|
||||||
|
initial: '/assets/farm/animals/goat-white/initial.js',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Collider: {
|
||||||
|
bodies: [
|
||||||
|
{
|
||||||
|
points: [
|
||||||
|
{x: -7, y: -3.5},
|
||||||
|
{x: 7, y: -3.5},
|
||||||
|
{x: 7, y: 3.5},
|
||||||
|
{x: -7, y: 3.5},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Interactive: {
|
||||||
|
interacting: 1,
|
||||||
|
interactScript: `
|
||||||
|
const lines = [
|
||||||
|
'mrowwr',
|
||||||
|
'p<shake>rrr</shake>o<wave>wwwww</wave>',
|
||||||
|
'mew<rate frequency={0.5}> </rate>mew!',
|
||||||
|
'me<wave>wwwww</wave>',
|
||||||
|
'\\\\*pu<shake>rrrrr</shake>\\\\*',
|
||||||
|
];
|
||||||
|
const line = lines[Math.floor(Math.random() * lines.length)];
|
||||||
|
subject.Interlocutor.dialogue({
|
||||||
|
body: line,
|
||||||
|
linger: 2,
|
||||||
|
offset: {x: 0, y: -16},
|
||||||
|
origin: 'track',
|
||||||
|
position: 'track',
|
||||||
|
})
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
Position: {
|
||||||
|
x: 350 + (Math.random() - 0.5) * 300,
|
||||||
|
y: 150 + (Math.random() - 0.5) * 300,
|
||||||
|
},
|
||||||
|
Sprite: {
|
||||||
|
...animalJson.Sprite,
|
||||||
|
anchorX: 0.5,
|
||||||
|
anchorY: 0.8,
|
||||||
|
source: '/assets/farm/animals/goat-white/goat-white.json',
|
||||||
|
speed: 0.25,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
entities.push({
|
entities.push({
|
||||||
Collider: {
|
Collider: {
|
||||||
|
|
|
@ -37,7 +37,7 @@ export default async function createPlayer(id) {
|
||||||
Magnet: {strength: 24},
|
Magnet: {strength: 24},
|
||||||
Player: {},
|
Player: {},
|
||||||
Position: {x: 128, y: 448},
|
Position: {x: 128, y: 448},
|
||||||
Speed: {speed: 100},
|
Speed: {speed: 300},
|
||||||
Sound: {},
|
Sound: {},
|
||||||
Sprite: {
|
Sprite: {
|
||||||
anchorX: 0.5,
|
anchorX: 0.5,
|
||||||
|
@ -55,4 +55,3 @@ export default async function createPlayer(id) {
|
||||||
};
|
};
|
||||||
return player;
|
return player;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
61
app/util/color.js
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
const {min, max, round} = Math;
|
||||||
|
|
||||||
|
export function hslToHex(h, s, l) {
|
||||||
|
const [r, g, b] = hslToRgb(h, s, l);
|
||||||
|
return (r << 16) | (g << 8) | b;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hslToRgb(h, s, l) {
|
||||||
|
let r, g, b;
|
||||||
|
if (s === 0) {
|
||||||
|
r = g = b = l;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
||||||
|
const p = 2 * l - q;
|
||||||
|
r = hueToRgb(p, q, h + 1/3);
|
||||||
|
g = hueToRgb(p, q, h);
|
||||||
|
b = hueToRgb(p, q, h - 1/3);
|
||||||
|
}
|
||||||
|
return [round(r * 255), round(g * 255), round(b * 255)];
|
||||||
|
}
|
||||||
|
|
||||||
|
function hueToRgb(p, q, t) {
|
||||||
|
if (t < 0) t += 1;
|
||||||
|
if (t > 1) t -= 1;
|
||||||
|
if (t < 1/6) return p + (q - p) * 6 * t;
|
||||||
|
if (t < 1/2) return q;
|
||||||
|
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hexToHsl(i) {
|
||||||
|
const [r, g, b] = hexToRgb(i);
|
||||||
|
return rgbToHsl(r, g, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hexToRgb(i) {
|
||||||
|
const o = i > 16777215 ? 8 : 0;
|
||||||
|
return [
|
||||||
|
(i >> o + 16) & 255,
|
||||||
|
(i >> o + 8 ) & 255,
|
||||||
|
(i >> o + 0 ) & 255,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function rgbToHsl(r, g, b) {
|
||||||
|
(r /= 255), (g /= 255), (b /= 255);
|
||||||
|
const vmax = max(r, g, b), vmin = min(r, g, b);
|
||||||
|
let h, s, l = (vmax + vmin) / 2;
|
||||||
|
if (vmax === vmin) {
|
||||||
|
return [0, 0, l];
|
||||||
|
}
|
||||||
|
const d = vmax - vmin;
|
||||||
|
s = l > 0.5 ? d / (2 - vmax - vmin) : d / (vmax + vmin);
|
||||||
|
if (vmax === r) h = (g - b) / d + (g < b ? 6 : 0);
|
||||||
|
if (vmax === g) h = (b - r) / d + 2;
|
||||||
|
if (vmax === b) h = (r - g) / d + 4;
|
||||||
|
h /= 6;
|
||||||
|
return [h, s, l];
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ import {parse as acornParse} from 'acorn';
|
||||||
import {LRUCache} from 'lru-cache';
|
import {LRUCache} from 'lru-cache';
|
||||||
|
|
||||||
import Sandbox from '@/astride/sandbox.js';
|
import Sandbox from '@/astride/sandbox.js';
|
||||||
|
import * as color from '@/util/color.js';
|
||||||
import delta from '@/util/delta.js';
|
import delta from '@/util/delta.js';
|
||||||
import lfo from '@/util/lfo.js';
|
import lfo from '@/util/lfo.js';
|
||||||
import * as MathUtil from '@/util/math.js';
|
import * as MathUtil from '@/util/math.js';
|
||||||
|
@ -40,6 +41,7 @@ export default class Script {
|
||||||
|
|
||||||
static contextDefaults() {
|
static contextDefaults() {
|
||||||
return {
|
return {
|
||||||
|
color,
|
||||||
console,
|
console,
|
||||||
delta,
|
delta,
|
||||||
lfo,
|
lfo,
|
||||||
|
@ -117,7 +119,7 @@ export default class Script {
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (async) {
|
if (async || value instanceof Promise) {
|
||||||
this.promise = value;
|
this.promise = value;
|
||||||
value
|
value
|
||||||
.catch(reject ? reject : () => {})
|
.catch(reject ? reject : () => {})
|
||||||
|
|
BIN
public/assets/farm/animals/bull-adult/bull-adult.png
Normal file
After Width: | Height: | Size: 5.4 KiB |
BIN
public/assets/farm/animals/bull-baby/bull-baby.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
263
public/assets/farm/animals/cow-adult/cow-adult.json
Normal file
|
@ -0,0 +1,263 @@
|
||||||
|
{
|
||||||
|
"animations": {
|
||||||
|
"idle:up": [
|
||||||
|
"farm/animals/cow-adult/cow-adult/10"
|
||||||
|
],
|
||||||
|
"idle:right": [
|
||||||
|
"farm/animals/cow-adult/cow-adult/7"
|
||||||
|
],
|
||||||
|
"idle:down": [
|
||||||
|
"farm/animals/cow-adult/cow-adult/1"
|
||||||
|
],
|
||||||
|
"idle:left": [
|
||||||
|
"farm/animals/cow-adult/cow-adult/4"
|
||||||
|
],
|
||||||
|
"moving:down": [
|
||||||
|
"farm/animals/cow-adult/cow-adult/0",
|
||||||
|
"farm/animals/cow-adult/cow-adult/1",
|
||||||
|
"farm/animals/cow-adult/cow-adult/2"
|
||||||
|
],
|
||||||
|
"moving:left": [
|
||||||
|
"farm/animals/cow-adult/cow-adult/3",
|
||||||
|
"farm/animals/cow-adult/cow-adult/4",
|
||||||
|
"farm/animals/cow-adult/cow-adult/5"
|
||||||
|
],
|
||||||
|
"moving:right": [
|
||||||
|
"farm/animals/cow-adult/cow-adult/6",
|
||||||
|
"farm/animals/cow-adult/cow-adult/7",
|
||||||
|
"farm/animals/cow-adult/cow-adult/8"
|
||||||
|
],
|
||||||
|
"moving:up": [
|
||||||
|
"farm/animals/cow-adult/cow-adult/9",
|
||||||
|
"farm/animals/cow-adult/cow-adult/10",
|
||||||
|
"farm/animals/cow-adult/cow-adult/11"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"frames": {
|
||||||
|
"farm/animals/cow-adult/cow-adult/0": {
|
||||||
|
"frame": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
},
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"farm/animals/cow-adult/cow-adult/1": {
|
||||||
|
"frame": {
|
||||||
|
"x": 42,
|
||||||
|
"y": 0,
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
},
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"farm/animals/cow-adult/cow-adult/2": {
|
||||||
|
"frame": {
|
||||||
|
"x": 84,
|
||||||
|
"y": 0,
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
},
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"farm/animals/cow-adult/cow-adult/3": {
|
||||||
|
"frame": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 36,
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
},
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"farm/animals/cow-adult/cow-adult/4": {
|
||||||
|
"frame": {
|
||||||
|
"x": 42,
|
||||||
|
"y": 36,
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
},
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"farm/animals/cow-adult/cow-adult/5": {
|
||||||
|
"frame": {
|
||||||
|
"x": 84,
|
||||||
|
"y": 36,
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
},
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"farm/animals/cow-adult/cow-adult/6": {
|
||||||
|
"frame": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 72,
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
},
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"farm/animals/cow-adult/cow-adult/7": {
|
||||||
|
"frame": {
|
||||||
|
"x": 42,
|
||||||
|
"y": 72,
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
},
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"farm/animals/cow-adult/cow-adult/8": {
|
||||||
|
"frame": {
|
||||||
|
"x": 84,
|
||||||
|
"y": 72,
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
},
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"farm/animals/cow-adult/cow-adult/9": {
|
||||||
|
"frame": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 108,
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
},
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"farm/animals/cow-adult/cow-adult/10": {
|
||||||
|
"frame": {
|
||||||
|
"x": 42,
|
||||||
|
"y": 108,
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
},
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"farm/animals/cow-adult/cow-adult/11": {
|
||||||
|
"frame": {
|
||||||
|
"x": 84,
|
||||||
|
"y": 108,
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
},
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"meta": {
|
||||||
|
"format": "RGBA8888",
|
||||||
|
"image": "./cow-adult.png",
|
||||||
|
"scale": 1,
|
||||||
|
"size": {
|
||||||
|
"w": 126,
|
||||||
|
"h": 144
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
BIN
public/assets/farm/animals/cow-adult/cow-adult.png
Normal file
After Width: | Height: | Size: 5.2 KiB |
17
public/assets/farm/animals/cow-adult/initial.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
entity.Direction.direction = Math.random() * Math.TAU;
|
||||||
|
|
||||||
|
entity.Controlled.directionMove(entity.Direction.direction);
|
||||||
|
|
||||||
|
await wait(0.25 + Math.random() * 2.25);
|
||||||
|
|
||||||
|
entity.Controlled.stop();
|
||||||
|
|
||||||
|
entity.Sprite.isAnimating = 0;
|
||||||
|
|
||||||
|
await wait(1 + Math.random() * 3);
|
||||||
|
|
||||||
|
entity.Direction.direction = Math.random() * Math.TAU;
|
||||||
|
|
||||||
|
await wait(0.5 + Math.random() * 2.5);
|
||||||
|
|
||||||
|
entity.Sprite.isAnimating = 1;
|
BIN
public/assets/farm/animals/cow-baby/cow-baby.png
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
public/assets/farm/animals/goat-black/goat-black.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
263
public/assets/farm/animals/goat-white/goat-white.json
Normal file
|
@ -0,0 +1,263 @@
|
||||||
|
{
|
||||||
|
"animations": {
|
||||||
|
"idle:up": [
|
||||||
|
"farm/animals/goat-white/goat-white/10"
|
||||||
|
],
|
||||||
|
"idle:right": [
|
||||||
|
"farm/animals/goat-white/goat-white/7"
|
||||||
|
],
|
||||||
|
"idle:down": [
|
||||||
|
"farm/animals/goat-white/goat-white/1"
|
||||||
|
],
|
||||||
|
"idle:left": [
|
||||||
|
"farm/animals/goat-white/goat-white/4"
|
||||||
|
],
|
||||||
|
"moving:down": [
|
||||||
|
"farm/animals/goat-white/goat-white/0",
|
||||||
|
"farm/animals/goat-white/goat-white/1",
|
||||||
|
"farm/animals/goat-white/goat-white/2"
|
||||||
|
],
|
||||||
|
"moving:left": [
|
||||||
|
"farm/animals/goat-white/goat-white/3",
|
||||||
|
"farm/animals/goat-white/goat-white/4",
|
||||||
|
"farm/animals/goat-white/goat-white/5"
|
||||||
|
],
|
||||||
|
"moving:right": [
|
||||||
|
"farm/animals/goat-white/goat-white/6",
|
||||||
|
"farm/animals/goat-white/goat-white/7",
|
||||||
|
"farm/animals/goat-white/goat-white/8"
|
||||||
|
],
|
||||||
|
"moving:up": [
|
||||||
|
"farm/animals/goat-white/goat-white/9",
|
||||||
|
"farm/animals/goat-white/goat-white/10",
|
||||||
|
"farm/animals/goat-white/goat-white/11"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"frames": {
|
||||||
|
"farm/animals/goat-white/goat-white/0": {
|
||||||
|
"frame": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
},
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"farm/animals/goat-white/goat-white/1": {
|
||||||
|
"frame": {
|
||||||
|
"x": 42,
|
||||||
|
"y": 0,
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
},
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"farm/animals/goat-white/goat-white/2": {
|
||||||
|
"frame": {
|
||||||
|
"x": 84,
|
||||||
|
"y": 0,
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
},
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"farm/animals/goat-white/goat-white/3": {
|
||||||
|
"frame": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 36,
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
},
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"farm/animals/goat-white/goat-white/4": {
|
||||||
|
"frame": {
|
||||||
|
"x": 42,
|
||||||
|
"y": 36,
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
},
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"farm/animals/goat-white/goat-white/5": {
|
||||||
|
"frame": {
|
||||||
|
"x": 84,
|
||||||
|
"y": 36,
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
},
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"farm/animals/goat-white/goat-white/6": {
|
||||||
|
"frame": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 72,
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
},
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"farm/animals/goat-white/goat-white/7": {
|
||||||
|
"frame": {
|
||||||
|
"x": 42,
|
||||||
|
"y": 72,
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
},
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"farm/animals/goat-white/goat-white/8": {
|
||||||
|
"frame": {
|
||||||
|
"x": 84,
|
||||||
|
"y": 72,
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
},
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"farm/animals/goat-white/goat-white/9": {
|
||||||
|
"frame": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 108,
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
},
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"farm/animals/goat-white/goat-white/10": {
|
||||||
|
"frame": {
|
||||||
|
"x": 42,
|
||||||
|
"y": 108,
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
},
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"farm/animals/goat-white/goat-white/11": {
|
||||||
|
"frame": {
|
||||||
|
"x": 84,
|
||||||
|
"y": 108,
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
},
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 42,
|
||||||
|
"h": 36
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"meta": {
|
||||||
|
"format": "RGBA8888",
|
||||||
|
"image": "./goat-white.png",
|
||||||
|
"scale": 1,
|
||||||
|
"size": {
|
||||||
|
"w": 126,
|
||||||
|
"h": 144
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
BIN
public/assets/farm/animals/goat-white/goat-white.png
Normal file
After Width: | Height: | Size: 4.1 KiB |
17
public/assets/farm/animals/goat-white/initial.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
entity.Direction.direction = Math.random() * Math.TAU;
|
||||||
|
|
||||||
|
entity.Controlled.directionMove(entity.Direction.direction);
|
||||||
|
|
||||||
|
await wait(0.25 + Math.random() * 2.25);
|
||||||
|
|
||||||
|
entity.Controlled.stop();
|
||||||
|
|
||||||
|
entity.Sprite.isAnimating = 0;
|
||||||
|
|
||||||
|
await wait(1 + Math.random() * 3);
|
||||||
|
|
||||||
|
entity.Direction.direction = Math.random() * Math.TAU;
|
||||||
|
|
||||||
|
await wait(0.5 + Math.random() * 2.5);
|
||||||
|
|
||||||
|
entity.Sprite.isAnimating = 1;
|
BIN
public/assets/farm/animals/pig-adult/pig-adult.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
public/assets/farm/animals/pig-baby/pig-baby.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
|
@ -13,25 +13,43 @@ if (projected?.length > 0) {
|
||||||
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({
|
||||||
count: 25,
|
|
||||||
frequency: 0.01,
|
|
||||||
shape: {
|
|
||||||
type: 'filledRect',
|
|
||||||
payload: {width: 16, height: 16},
|
|
||||||
},
|
|
||||||
entity: {
|
entity: {
|
||||||
Forces: {forceY: -50},
|
Behaving: {
|
||||||
|
routines: {
|
||||||
|
initial: 'await delta(entity.Forces, {forceY: {delta: 640, duration: 0.125}}).promise',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Forces: {forceY: -80},
|
||||||
Position: {
|
Position: {
|
||||||
x: x * layer.tileSize.x + (layer.tileSize.x / 2),
|
x: x * layer.tileSize.x + (layer.tileSize.x / 2),
|
||||||
y: y * layer.tileSize.y + (layer.tileSize.y / 2),
|
y: y * layer.tileSize.y + (layer.tileSize.y / 2),
|
||||||
},
|
},
|
||||||
Sprite: {
|
Sprite: {
|
||||||
scaleX: 0.2,
|
|
||||||
scaleY: 0.2,
|
|
||||||
tint: 0x552200,
|
tint: 0x552200,
|
||||||
},
|
},
|
||||||
Ttl: {ttl: 0.125},
|
Ttl: {ttl: 0.35},
|
||||||
}
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
path: ['Sprite', 'lightness'],
|
||||||
|
value: [0.05, 0.25],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ['Sprite', 'alpha'],
|
||||||
|
value: [0.5, 1],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ['Sprite', 'scale'],
|
||||||
|
value: [0.05, 0.1],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
frequency: 0.05,
|
||||||
|
shape: {
|
||||||
|
type: 'filledRect',
|
||||||
|
payload: {width: 12, height: 12},
|
||||||
|
},
|
||||||
|
spurt: 5,
|
||||||
|
ttl: 0.4,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Sprite.animation = ['moving', direction].join(':');
|
Sprite.animation = ['moving', direction].join(':');
|
||||||
|
|
|
@ -75,7 +75,7 @@ while (shots.length > 0) {
|
||||||
)
|
)
|
||||||
shot.Speed.speed = 400;
|
shot.Speed.speed = 400;
|
||||||
shot.Direction.direction = (Math.TAU + toward) % Math.TAU;
|
shot.Direction.direction = (Math.TAU + toward) % Math.TAU;
|
||||||
if (Math.distance(where, shot.Position) < 4) {
|
if (accumulated[shot.id] > 1.5 || Math.distance(where, shot.Position) < 4) {
|
||||||
delete accumulated[shot.id];
|
delete accumulated[shot.id];
|
||||||
shot.Sprite.alpha = 0;
|
shot.Sprite.alpha = 0;
|
||||||
ecs.destroy(shot.id);
|
ecs.destroy(shot.id);
|
||||||
|
|
|
@ -14,25 +14,31 @@ if (projected?.length > 0) {
|
||||||
|
|
||||||
for (const {x, y} of projected) {
|
for (const {x, y} of projected) {
|
||||||
Emitter.emit({
|
Emitter.emit({
|
||||||
count: 25,
|
|
||||||
frequency: 0.01,
|
|
||||||
shape: {
|
|
||||||
type: 'filledRect',
|
|
||||||
payload: {width: 16, height: 16},
|
|
||||||
},
|
|
||||||
entity: {
|
entity: {
|
||||||
Forces: {forceY: 100},
|
Forces: {forceY: 100},
|
||||||
Position: {
|
Position: {
|
||||||
x: x * layer.tileSize.x + (layer.tileSize.x / 2),
|
x: x * layer.tileSize.x + (layer.tileSize.x / 2),
|
||||||
y: y * layer.tileSize.y - layer.tileSize.y,
|
y: y * layer.tileSize.y - (layer.tileSize.y / 4),
|
||||||
},
|
},
|
||||||
Sprite: {
|
Sprite: {
|
||||||
scaleX: 0.2,
|
scaleX: 0.05,
|
||||||
scaleY: 0.2,
|
scaleY: 0.15,
|
||||||
tint: 0x0022aa,
|
tint: 0x0022aa,
|
||||||
},
|
},
|
||||||
Ttl: {ttl: 0.25},
|
Ttl: {ttl: 0.1},
|
||||||
}
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
path: ['Sprite', 'lightness'],
|
||||||
|
value: [0.111, 0.666],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
frequency: 0.01,
|
||||||
|
shape: {
|
||||||
|
type: 'circle',
|
||||||
|
payload: {radius: 4},
|
||||||
|
},
|
||||||
|
ttl: 0.5,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
await wait(0.5);
|
await wait(0.5);
|
||||||
|
@ -49,6 +55,8 @@ if (projected?.length > 0) {
|
||||||
Water.water[tileIndex] = Math.min(255, 64 + w);
|
Water.water[tileIndex] = Math.min(255, 64 + w);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ecs.system('Water').schedule();
|
||||||
|
|
||||||
Controlled.locked = 0;
|
Controlled.locked = 0;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|