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) {
|
||||
case YIELD_PROMISE: {
|
||||
stepResult.async = true;
|
||||
stepResult.value = Promise.resolve(result.value)
|
||||
const promise = result.value instanceof Promise
|
||||
? result.value
|
||||
: Promise.resolve(result.value);
|
||||
promise
|
||||
.then((value) => {
|
||||
const top = this.$$execution.stack[this.$$execution.stack.length - 1];
|
||||
this.$$execution.deferred.set(top, {
|
||||
|
@ -712,6 +715,7 @@ export default class Sandbox {
|
|||
yield: YIELD_NONE,
|
||||
});
|
||||
});
|
||||
stepResult.value = promise;
|
||||
break;
|
||||
}
|
||||
case YIELD_LOOP_UPDATE: {
|
||||
|
|
|
@ -2,10 +2,9 @@ import Schema from './schema.js';
|
|||
|
||||
export default class Component {
|
||||
|
||||
data = [];
|
||||
ecs;
|
||||
Instance;
|
||||
map = {};
|
||||
instances = {};
|
||||
pool = [];
|
||||
static properties = {};
|
||||
static $$schema;
|
||||
|
@ -21,7 +20,7 @@ export default class Component {
|
|||
results.push(
|
||||
this.pool.length > 0
|
||||
? this.pool.pop()
|
||||
: this.data.push(new this.Instance()) - 1,
|
||||
: new this.Instance(),
|
||||
)
|
||||
count -= 1;
|
||||
}
|
||||
|
@ -38,35 +37,51 @@ export default class Component {
|
|||
return [];
|
||||
}
|
||||
const allocated = this.allocateMany(entries.length);
|
||||
const {properties} = this.constructor.schema.specification.concrete;
|
||||
const Schema = this.constructor.schema.constructor;
|
||||
const keys = Object.keys(properties);
|
||||
const {
|
||||
constructor: Schema,
|
||||
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 = [];
|
||||
for (let i = 0; i < entries.length; ++i) {
|
||||
const [entityId, values = {}] = entries[i];
|
||||
this.map[entityId] = allocated[i];
|
||||
this.data[allocated[i]].entity = entityId;
|
||||
for (let k = 0; k < keys.length; ++k) {
|
||||
const j = keys[k];
|
||||
const instance = this.data[allocated[i]];
|
||||
if (j in values) {
|
||||
instance[j] = values[j];
|
||||
const [entityId, values] = entries[i];
|
||||
const instance = allocated[i];
|
||||
instance.entity = entityId;
|
||||
this.instances[entityId] = instance;
|
||||
for (const key in values) {
|
||||
keys.delete(key);
|
||||
}
|
||||
else {
|
||||
const defaultValue = Schema.defaultValue(properties[j]);
|
||||
if ('undefined' !== typeof defaultValue) {
|
||||
instance[j] = defaultValue;
|
||||
const defaultValues = {};
|
||||
for (const key of keys) {
|
||||
defaultValues[key] = 'function' === typeof defaults[key]
|
||||
? defaults[key]()
|
||||
: defaults[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
promises.push(this.load(this.data[allocated[i]]));
|
||||
instance.initialize(values, defaultValues);
|
||||
promises.push(this.load(instance));
|
||||
}
|
||||
await Promise.all(promises);
|
||||
const created = [];
|
||||
for (let i = 0; i < allocated.length; ++i) {
|
||||
created.push(this.data[allocated[i]]);
|
||||
}
|
||||
return created;
|
||||
return allocated;
|
||||
}
|
||||
|
||||
deserialize(entityId, view, offset) {
|
||||
|
@ -79,22 +94,18 @@ export default class Component {
|
|||
}
|
||||
|
||||
destroy(entityId) {
|
||||
this.destroyMany([entityId]);
|
||||
this.destroyMany(new Set([entityId]));
|
||||
}
|
||||
|
||||
destroyMany(entities) {
|
||||
this.freeMany(
|
||||
entities
|
||||
.map((entityId) => {
|
||||
if ('undefined' !== typeof this.map[entityId]) {
|
||||
return this.map[entityId];
|
||||
}
|
||||
destroyMany(entityIds) {
|
||||
for (const entityId of entityIds) {
|
||||
const instance = this.instances[entityId];
|
||||
if ('undefined' === typeof instance) {
|
||||
throw new Error(`can't free for non-existent id ${entityId}`);
|
||||
}),
|
||||
);
|
||||
for (let i = 0; i < entities.length; i++) {
|
||||
this.data[this.map[entities[i]]].destroy();
|
||||
this.map[entities[i]] = undefined;
|
||||
}
|
||||
instance.destroy();
|
||||
this.pool.push(instance);
|
||||
delete this.instances[entityId];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -110,14 +121,8 @@ export default class Component {
|
|||
return json;
|
||||
}
|
||||
|
||||
freeMany(indices) {
|
||||
for (let i = 0; i < indices.length; ++i) {
|
||||
this.pool.push(indices[i]);
|
||||
}
|
||||
}
|
||||
|
||||
get(entityId) {
|
||||
return this.data[this.map[entityId]];
|
||||
return this.instances[entityId];
|
||||
}
|
||||
|
||||
async insertMany(entities) {
|
||||
|
@ -127,25 +132,25 @@ export default class Component {
|
|||
instanceFromSchema() {
|
||||
const Component = this;
|
||||
const {concrete} = Component.constructor.schema.specification;
|
||||
const Schema = Component.constructor.schema.constructor;
|
||||
const Instance = class {
|
||||
$$entity = 0;
|
||||
constructor() {
|
||||
this.$$reset();
|
||||
}
|
||||
$$reset() {
|
||||
for (const key in concrete.properties) {
|
||||
this[`$$${key}`] = Schema.defaultValue(concrete.properties[key]);
|
||||
}
|
||||
}
|
||||
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) {
|
||||
return data || Component.constructor.filterDefaults(this);
|
||||
}
|
||||
toJSON() {
|
||||
return Component.constructor.filterDefaults(this);
|
||||
}
|
||||
update(values) {
|
||||
async update(values) {
|
||||
for (const key in values) {
|
||||
if (concrete.properties[key]) {
|
||||
this[`$$${key}`] = values[key];
|
||||
|
@ -163,7 +168,6 @@ export default class Component {
|
|||
},
|
||||
set: function set(v) {
|
||||
this.$$entity = v;
|
||||
this.$$reset();
|
||||
},
|
||||
};
|
||||
for (const key in concrete.properties) {
|
||||
|
@ -214,10 +218,12 @@ export default class Component {
|
|||
}
|
||||
|
||||
async updateMany(entities) {
|
||||
const promises = [];
|
||||
for (let i = 0; i < entities.length; 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;
|
||||
const {Ticking} = ecs.get(this.entity);
|
||||
if (Ticking) {
|
||||
this.$$death.context.entity = ecs.get(this.entity);
|
||||
const ticker = this.$$death.ticker();
|
||||
ecs.addDestructionDependency(this.entity.id, ticker);
|
||||
Ticking.add(ticker);
|
||||
|
@ -35,7 +36,6 @@ export default class Alive extends Component {
|
|||
instance.deathScript,
|
||||
{
|
||||
ecs: this.ecs,
|
||||
entity: this.ecs.get(instance.entity),
|
||||
},
|
||||
);
|
||||
if (0 === instance.maxHealth) {
|
||||
|
|
|
@ -2,11 +2,13 @@ import Component from '@/ecs/component.js';
|
|||
|
||||
export default class Behaving extends Component {
|
||||
instanceFromSchema() {
|
||||
const {ecs} = this;
|
||||
return class BehavingInstance extends super.instanceFromSchema() {
|
||||
$$routineInstances = {};
|
||||
tick(elapsed) {
|
||||
const routine = this.$$routineInstances[this.currentRoutine];
|
||||
if (routine) {
|
||||
routine.context.entity = ecs.get(this.entity);
|
||||
routine.tick(elapsed);
|
||||
}
|
||||
}
|
||||
|
@ -20,12 +22,7 @@ export default class Behaving extends Component {
|
|||
const promises = [];
|
||||
for (const key in instance.routines) {
|
||||
promises.push(
|
||||
this.ecs.readScript(
|
||||
instance.routines[key],
|
||||
{
|
||||
entity: this.ecs.get(instance.entity),
|
||||
},
|
||||
)
|
||||
this.ecs.readScript(instance.routines[key])
|
||||
.then((script) => {
|
||||
instance.$$routineInstances[key] = script;
|
||||
}),
|
||||
|
|
|
@ -106,6 +106,7 @@ export default class Collider extends Component {
|
|||
if (!hasMatchingIntersection) {
|
||||
if (this.$$collisionStart) {
|
||||
const script = this.$$collisionStart.clone();
|
||||
script.context.entity = thisEntity;
|
||||
script.context.other = otherEntity;
|
||||
script.context.pair = [body, otherBody];
|
||||
const ticker = script.ticker();
|
||||
|
@ -115,6 +116,7 @@ export default class Collider extends Component {
|
|||
}
|
||||
if (other.$$collisionStart) {
|
||||
const script = other.$$collisionStart.clone();
|
||||
script.context.entity = otherEntity;
|
||||
script.context.other = thisEntity;
|
||||
script.context.pair = [otherBody, body];
|
||||
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.$$aabbs = [];
|
||||
const {bodies} = this;
|
||||
const {Direction: {direction = 0} = {}} = ecs.get(this.entity);
|
||||
const {Direction: {direction = 0} = {}} = ecs.get(this.entity) || {};
|
||||
for (const body of bodies) {
|
||||
let x0 = Infinity, x1 = -Infinity, y0 = Infinity, y1 = -Infinity;
|
||||
for (const point of transform(body.points, {rotation: direction})) {
|
||||
|
@ -271,14 +273,12 @@ export default class Collider extends Component {
|
|||
instance.collisionEndScript,
|
||||
{
|
||||
ecs: this.ecs,
|
||||
entity: this.ecs.get(instance.entity),
|
||||
},
|
||||
);
|
||||
instance.$$collisionStart = await this.ecs.readScript(
|
||||
instance.collisionStartScript,
|
||||
{
|
||||
ecs: this.ecs,
|
||||
entity: this.ecs.get(instance.entity),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
@ -6,9 +6,13 @@ const Gathered = gather(
|
|||
|
||||
const Components = {};
|
||||
for (const componentName in Gathered) {
|
||||
Components[componentName] = class Named extends Gathered[componentName] {
|
||||
static componentName = componentName;
|
||||
};
|
||||
Components[componentName] = eval(`
|
||||
((Gathered) => (
|
||||
class ${componentName} extends Gathered['${componentName}'] {
|
||||
static componentName = '${componentName}';
|
||||
}
|
||||
))
|
||||
`)(Gathered);
|
||||
}
|
||||
|
||||
export default Components;
|
||||
|
|
|
@ -8,7 +8,8 @@ export default class Interactive extends Component {
|
|||
interact(initiator) {
|
||||
const script = this.$$interact.clone();
|
||||
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());
|
||||
}
|
||||
get interacting() {
|
||||
|
@ -28,7 +29,6 @@ export default class Interactive extends Component {
|
|||
instance.interactScript,
|
||||
{
|
||||
ecs: this.ecs,
|
||||
subject: this.ecs.get(instance.entity),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
import Component from '@/ecs/component.js';
|
||||
import {hexToHsl, hslToHex} from '@/util/color.js';
|
||||
|
||||
export default class Sprite extends Component {
|
||||
instanceFromSchema() {
|
||||
return class SpriteInstance extends super.instanceFromSchema() {
|
||||
$$anchor = {x: 0.5, y: 0.5};
|
||||
$$hue = 0;
|
||||
$$saturation = 0;
|
||||
$$lightness = 1;
|
||||
$$scale = {x: 1, y: 1};
|
||||
$$sourceJson = {};
|
||||
get anchor() {
|
||||
|
@ -14,12 +18,14 @@ export default class Sprite extends Component {
|
|||
}
|
||||
set anchorX(anchorX) {
|
||||
this.$$anchor = {x: anchorX, y: this.anchorY};
|
||||
super.anchorX = anchorX;
|
||||
}
|
||||
get anchorY() {
|
||||
return this.$$anchor.y;
|
||||
}
|
||||
set anchorY(anchorY) {
|
||||
this.$$anchor = {x: this.anchorX, y: anchorY};
|
||||
super.anchorY = anchorY;
|
||||
}
|
||||
get animation() {
|
||||
return super.animation;
|
||||
|
@ -57,6 +63,37 @@ export default class Sprite extends Component {
|
|||
}
|
||||
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() {
|
||||
if (!this.$$sourceJson.meta) {
|
||||
return false;
|
||||
|
@ -69,20 +106,37 @@ export default class Sprite extends Component {
|
|||
}
|
||||
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() {
|
||||
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() {
|
||||
return this.$$scale.x;
|
||||
}
|
||||
set scaleX(scaleX) {
|
||||
this.$$scale = {x: scaleX, y: this.scaleY};
|
||||
super.scaleX = scaleX;
|
||||
}
|
||||
get scaleY() {
|
||||
return this.$$scale.y;
|
||||
}
|
||||
set scaleY(scaleY) {
|
||||
this.$$scale = {x: this.scaleX, y: scaleY};
|
||||
super.scaleY = scaleY;
|
||||
}
|
||||
get size() {
|
||||
if (!this.$$sourceJson.frames) {
|
||||
|
@ -93,11 +147,48 @@ export default class Sprite extends Component {
|
|||
: '';
|
||||
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) {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const {elapsed, ...rest} = super.toNet(recipient, data);
|
||||
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) {
|
||||
|
@ -123,6 +214,6 @@ export default class Sprite extends Component {
|
|||
scaleY: {defaultValue: 1, type: 'float32'},
|
||||
source: {type: 'string'},
|
||||
speed: {type: 'float32'},
|
||||
tint: {type: 'uint32'},
|
||||
tint: {defaultValue: 0xffffff, type: 'uint32'},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -5,9 +5,15 @@ export default class Ttl extends Component {
|
|||
const {ecs} = this;
|
||||
return class TtlInstance extends super.instanceFromSchema() {
|
||||
$$elapsed = 0;
|
||||
$$reset() {
|
||||
destroy() {
|
||||
this.$$elapsed = 0;
|
||||
}
|
||||
get elapsed() {
|
||||
return this.$$elapsed;
|
||||
}
|
||||
set elapsed(elapsed) {
|
||||
this.$$elapsed = elapsed;
|
||||
}
|
||||
tick(elapsed) {
|
||||
this.$$elapsed += elapsed;
|
||||
if (this.$$elapsed >= this.ttl) {
|
||||
|
|
160
app/ecs/ecs.js
|
@ -1,7 +1,7 @@
|
|||
import {Encoder, Decoder} from '@msgpack/msgpack';
|
||||
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 EntityFactory from './entity-factory.js';
|
||||
|
@ -23,16 +23,22 @@ export default class Ecs {
|
|||
|
||||
deferredChanges = {}
|
||||
|
||||
$$deindexing = new Set();
|
||||
|
||||
$$destructionDependencies = new Map();
|
||||
|
||||
diff = {};
|
||||
$$detached = new Set();
|
||||
|
||||
Systems = {};
|
||||
diff = {};
|
||||
|
||||
$$entities = {};
|
||||
|
||||
$$entityFactory = new EntityFactory();
|
||||
|
||||
$$reindexing = new Set();
|
||||
|
||||
Systems = {};
|
||||
|
||||
constructor({Systems, Components} = {}) {
|
||||
for (const componentName in Components) {
|
||||
this.Components[componentName] = new Components[componentName](this);
|
||||
|
@ -58,7 +64,7 @@ export default class Ecs {
|
|||
|
||||
async apply(patch) {
|
||||
const creating = [];
|
||||
const destroying = [];
|
||||
const destroying = new Set();
|
||||
const inserting = [];
|
||||
const removing = [];
|
||||
const updating = [];
|
||||
|
@ -66,7 +72,7 @@ export default class Ecs {
|
|||
const entityId = parseInt(entityIdString);
|
||||
const components = patch[entityId];
|
||||
if (false === components) {
|
||||
destroying.push(entityId);
|
||||
destroying.add(entityId);
|
||||
continue;
|
||||
}
|
||||
const componentsToRemove = [];
|
||||
|
@ -82,22 +88,26 @@ export default class Ecs {
|
|||
if (componentsToRemove.length > 0) {
|
||||
removing.push([entityId, componentsToRemove]);
|
||||
}
|
||||
if (this.$$entities[entityId]) {
|
||||
const entity = this.$$entities[entityId];
|
||||
if (this.$$entities[entityIdString]) {
|
||||
const entity = this.$$entities[entityIdString];
|
||||
let isInserting = false;
|
||||
const entityInserts = {};
|
||||
let isUpdating = false;
|
||||
const entityUpdates = {};
|
||||
for (const componentName in componentsToUpdate) {
|
||||
if (entity[componentName]) {
|
||||
entityUpdates[componentName] = componentsToUpdate[componentName];
|
||||
isUpdating = true;
|
||||
}
|
||||
else {
|
||||
entityInserts[componentName] = componentsToUpdate[componentName];
|
||||
isInserting = true;
|
||||
}
|
||||
}
|
||||
if (Object.keys(entityInserts).length > 0) {
|
||||
if (isInserting) {
|
||||
inserting.push([entityId, entityInserts]);
|
||||
}
|
||||
if (Object.keys(entityUpdates).length > 0) {
|
||||
if (isUpdating) {
|
||||
updating.push([entityId, entityUpdates]);
|
||||
}
|
||||
}
|
||||
|
@ -105,9 +115,6 @@ export default class Ecs {
|
|||
creating.push([entityId, componentsToUpdate]);
|
||||
}
|
||||
}
|
||||
if (destroying.length > 0) {
|
||||
this.destroyMany(destroying);
|
||||
}
|
||||
const promises = [];
|
||||
if (inserting.length > 0) {
|
||||
promises.push(this.insertMany(inserting));
|
||||
|
@ -119,11 +126,30 @@ export default class Ecs {
|
|||
promises.push(this.createManySpecific(creating));
|
||||
}
|
||||
await Promise.all(promises);
|
||||
if (destroying.size > 0) {
|
||||
this.destroyMany(destroying);
|
||||
}
|
||||
if (removing.length > 0) {
|
||||
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) {
|
||||
const it = Object.entries(this.diff).values();
|
||||
return {
|
||||
|
@ -158,10 +184,27 @@ export default class Ecs {
|
|||
return entityId;
|
||||
}
|
||||
|
||||
async createDetached(components = {}) {
|
||||
const [entityId] = await this.createManyDetached([components]);
|
||||
return entityId;
|
||||
}
|
||||
|
||||
async createMany(componentsList) {
|
||||
const specificsList = [];
|
||||
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);
|
||||
}
|
||||
|
@ -170,19 +213,20 @@ export default class Ecs {
|
|||
if (0 === specificsList.length) {
|
||||
return;
|
||||
}
|
||||
const entityIds = [];
|
||||
const entityIds = new Set();
|
||||
const creating = {};
|
||||
for (let i = 0; i < specificsList.length; i++) {
|
||||
const [entityId, components] = specificsList[i];
|
||||
if (!this.$$detached.has(entityId)) {
|
||||
this.deferredChanges[entityId] = [];
|
||||
}
|
||||
const componentNames = [];
|
||||
for (const componentName in components) {
|
||||
if (this.Components[componentName]) {
|
||||
componentNames.push(componentName);
|
||||
}
|
||||
}
|
||||
entityIds.push(entityId);
|
||||
this.rebuild(entityId, () => componentNames);
|
||||
entityIds.add(entityId);
|
||||
for (const componentName of componentNames) {
|
||||
if (!creating[componentName]) {
|
||||
creating[componentName] = [];
|
||||
|
@ -197,28 +241,38 @@ export default class Ecs {
|
|||
}
|
||||
await Promise.all(promises);
|
||||
for (let i = 0; i < specificsList.length; i++) {
|
||||
const [entityId] = specificsList[i];
|
||||
const changes = this.deferredChanges[entityId];
|
||||
delete this.deferredChanges[entityId];
|
||||
for (const components of changes) {
|
||||
this.markChange(entityId, components);
|
||||
const [entityId, components] = specificsList[i];
|
||||
this.$$reindexing.add(entityId);
|
||||
this.rebuild(entityId, () => Object.keys(components));
|
||||
if (this.$$detached.has(entityId)) {
|
||||
continue;
|
||||
}
|
||||
this.applyDeferredChanges(entityId);
|
||||
}
|
||||
this.reindex(entityIds);
|
||||
return entityIds;
|
||||
}
|
||||
|
||||
async createSpecific(entityId, components) {
|
||||
return this.createManySpecific([[entityId, components]]);
|
||||
const [created] = await this.createManySpecific([[entityId, components]]);
|
||||
return created;
|
||||
}
|
||||
|
||||
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) {
|
||||
const System = this.Systems[systemName];
|
||||
if (!System.active) {
|
||||
continue;
|
||||
}
|
||||
System.deindex(entityIds);
|
||||
System.deindex(attached);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -259,14 +313,11 @@ export default class Ecs {
|
|||
return dependencies.resolvers.promise;
|
||||
}
|
||||
|
||||
destroyAll() {
|
||||
this.destroyMany(this.entities);
|
||||
}
|
||||
|
||||
destroyMany(entityIds) {
|
||||
const destroying = {};
|
||||
this.deindex(entityIds);
|
||||
// this.deindex(entityIds);
|
||||
for (const entityId of entityIds) {
|
||||
this.$$deindexing.add(entityId);
|
||||
if (!this.$$entities[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() {
|
||||
const ids = [];
|
||||
for (const entity of Object.values(this.$$entities)) {
|
||||
|
@ -306,7 +364,6 @@ export default class Ecs {
|
|||
const inserting = {};
|
||||
const unique = new Set();
|
||||
for (const [entityId, components] of entities) {
|
||||
this.rebuild(entityId, (componentNames) => [...new Set(componentNames.concat(Object.keys(components)))]);
|
||||
const diff = {};
|
||||
for (const componentName in components) {
|
||||
if (!inserting[componentName]) {
|
||||
|
@ -323,7 +380,10 @@ export default class Ecs {
|
|||
promises.push(this.Components[componentName].insertMany(inserting[componentName]));
|
||||
}
|
||||
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) {
|
||||
|
@ -337,13 +397,7 @@ export default class Ecs {
|
|||
}
|
||||
// Created?
|
||||
else if (!this.diff[entityId]) {
|
||||
const filtered = {};
|
||||
for (const componentName in components) {
|
||||
filtered[componentName] = false === components[componentName]
|
||||
? false
|
||||
: components[componentName];
|
||||
}
|
||||
this.diff[entityId] = filtered;
|
||||
this.diff[entityId] = components;
|
||||
}
|
||||
// Otherwise, merge.
|
||||
else {
|
||||
|
@ -415,12 +469,21 @@ export default class Ecs {
|
|||
}
|
||||
|
||||
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) {
|
||||
const System = this.Systems[systemName];
|
||||
if (!System.active) {
|
||||
continue;
|
||||
}
|
||||
System.reindex(entityIds);
|
||||
System.reindex(attached);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -442,12 +505,14 @@ export default class Ecs {
|
|||
removing[componentName].push(entityId);
|
||||
}
|
||||
this.markChange(entityId, diff);
|
||||
this.rebuild(entityId, (componentNames) => componentNames.filter((type) => !components.includes(type)));
|
||||
}
|
||||
for (const componentName in removing) {
|
||||
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) {
|
||||
|
@ -468,6 +533,7 @@ export default class Ecs {
|
|||
}
|
||||
|
||||
tick(elapsed) {
|
||||
// tick systems
|
||||
for (const systemName in this.Systems) {
|
||||
const System = this.Systems[systemName];
|
||||
if (!System.active) {
|
||||
|
@ -483,6 +549,7 @@ export default class Ecs {
|
|||
System.elapsed -= System.frequency;
|
||||
}
|
||||
}
|
||||
// destroy entities
|
||||
const destroying = new Set();
|
||||
for (const [entityId, {promises}] of this.$$destructionDependencies) {
|
||||
if (0 === promises.size) {
|
||||
|
@ -496,6 +563,15 @@ export default class Ecs {
|
|||
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() {
|
||||
|
@ -528,6 +604,7 @@ export default class Ecs {
|
|||
}
|
||||
updating[componentName].push([entityId, components[componentName]]);
|
||||
}
|
||||
this.$$reindexing.add(entityId);
|
||||
unique.add(entityId);
|
||||
}
|
||||
const promises = [];
|
||||
|
@ -535,7 +612,6 @@ export default class Ecs {
|
|||
promises.push(this.Components[componentName].updateMany(updating[componentName]));
|
||||
}
|
||||
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}}));
|
||||
expect(ecs.get(entity))
|
||||
.to.not.be.undefined;
|
||||
ecs.destroyAll();
|
||||
ecs.destroyMany(new Set([entity]));
|
||||
expect(ecs.get(entity))
|
||||
.to.be.undefined;
|
||||
expect(() => {
|
||||
ecs.destroyMany([entity]);
|
||||
ecs.destroyMany(new Set([entity]));
|
||||
})
|
||||
.to.throw();
|
||||
});
|
||||
|
@ -122,10 +122,10 @@ test('destroys entities', async () => {
|
|||
test('inserts components into entities', async () => {
|
||||
const ecs = new Ecs({Components: {Empty, Position}});
|
||||
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)))
|
||||
.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)))
|
||||
.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;
|
||||
});
|
||||
|
||||
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 () => {
|
||||
const ecs = new Ecs();
|
||||
let entity;
|
||||
|
@ -210,7 +243,7 @@ test('generates diffs for adding and removing components', async () => {
|
|||
let entity;
|
||||
entity = await ecs.create();
|
||||
ecs.setClean();
|
||||
ecs.insert(entity, {Position: {x: 64}});
|
||||
await ecs.insert(entity, {Position: {x: 64}});
|
||||
expect(ecs.diff)
|
||||
.to.deep.equal({[entity]: {Position: {x: 64}}});
|
||||
ecs.setClean();
|
||||
|
@ -246,6 +279,23 @@ test('generates diffs for entity mutations', async () => {
|
|||
.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 () => {
|
||||
const ecs = new Ecs({Components: {Position}});
|
||||
let entity;
|
||||
|
@ -253,7 +303,7 @@ test('generates coalesced diffs for components', async () => {
|
|||
ecs.remove(entity, ['Position']);
|
||||
expect(ecs.diff)
|
||||
.to.deep.equal({[entity]: {Position: false}});
|
||||
ecs.insert(entity, {Position: {}});
|
||||
await ecs.insert(entity, {Position: {}});
|
||||
expect(ecs.diff)
|
||||
.to.deep.equal({[entity]: {Position: {}}});
|
||||
});
|
||||
|
@ -290,11 +340,11 @@ test('applies creation patches', async () => {
|
|||
.to.equal(64);
|
||||
});
|
||||
|
||||
test('applies update patches', () => {
|
||||
test('applies update patches', async () => {
|
||||
const ecs = new Ecs({Components: {Position}});
|
||||
ecs.createSpecific(16, {Position: {x: 64}});
|
||||
ecs.apply({16: {Position: {x: 128}}});
|
||||
expect(Array.from(ecs.entities).length)
|
||||
await ecs.createSpecific(16, {Position: {x: 64}});
|
||||
await ecs.apply({16: {Position: {x: 128}}});
|
||||
expect(Object.keys(ecs.$$entities).length)
|
||||
.to.equal(1);
|
||||
expect(ecs.get(16).Position.x)
|
||||
.to.equal(128);
|
||||
|
@ -318,9 +368,9 @@ test('applies component deletion patches', async () => {
|
|||
.to.deep.equal(['Position']);
|
||||
});
|
||||
|
||||
test('calculates entity size', () => {
|
||||
test('calculates entity size', async () => {
|
||||
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
|
||||
// 4 + 2 + 2 + 4 + 2 + 4 + 4 + 4 + 4 = 30
|
||||
expect(ecs.get(1).size())
|
||||
|
@ -329,8 +379,8 @@ test('calculates entity size', () => {
|
|||
|
||||
test('serializes and deserializes', async () => {
|
||||
const ecs = new Ecs({Components: {Empty, Name, Position}});
|
||||
ecs.createSpecific(1, {Empty: {}, Position: {x: 64}});
|
||||
ecs.createSpecific(16, {Name: {name: 'foobar'}, Position: {x: 128}});
|
||||
await ecs.createSpecific(1, {Empty: {}, Position: {x: 64}});
|
||||
await ecs.createSpecific(16, {Name: {name: 'foobar'}, Position: {x: 128}});
|
||||
expect(ecs.toJSON())
|
||||
.to.deep.equal({
|
||||
entities: {
|
||||
|
@ -362,8 +412,8 @@ test('serializes and deserializes', async () => {
|
|||
|
||||
test('deserializes from compatible ECS', async () => {
|
||||
const ecs = new Ecs({Components: {Empty, Name, Position}});
|
||||
ecs.createSpecific(1, {Empty: {}, Position: {x: 64}});
|
||||
ecs.createSpecific(16, {Name: {name: 'foobar'}, Position: {x: 128}});
|
||||
await ecs.createSpecific(1, {Empty: {}, Position: {x: 64}});
|
||||
await ecs.createSpecific(16, {Name: {name: 'foobar'}, Position: {x: 128}});
|
||||
const view = Ecs.serialize(ecs);
|
||||
const deserialized = await Ecs.deserialize(
|
||||
new Ecs({Components: {Empty, Name}}),
|
||||
|
|
|
@ -23,6 +23,9 @@ export default class EntityFactory {
|
|||
static componentNames = sorted;
|
||||
constructor(id) {
|
||||
this.id = id;
|
||||
for (const type of sorted) {
|
||||
this[type] = Components[type].get(id);
|
||||
}
|
||||
}
|
||||
size() {
|
||||
let size = 0;
|
||||
|
@ -34,15 +37,6 @@ export default class EntityFactory {
|
|||
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', `
|
||||
${
|
||||
sorted
|
||||
|
|
|
@ -2,7 +2,7 @@ export default class Query {
|
|||
|
||||
$$criteria = {with: [], without: []};
|
||||
$$ecs;
|
||||
$$index = new Set();
|
||||
$$map = new Map();
|
||||
|
||||
constructor(parameters, ecs) {
|
||||
this.$$ecs = ecs;
|
||||
|
@ -20,19 +20,19 @@ export default class Query {
|
|||
}
|
||||
|
||||
get count() {
|
||||
return this.$$index.size;
|
||||
return this.$$map.size;
|
||||
}
|
||||
|
||||
deindex(entityIds) {
|
||||
for (const entityId of entityIds) {
|
||||
this.$$index.delete(entityId);
|
||||
this.$$map.delete(entityId);
|
||||
}
|
||||
}
|
||||
|
||||
reindex(entityIds) {
|
||||
if (0 === this.$$criteria.with.length && 0 === this.$$criteria.without.length) {
|
||||
for (const entityId of entityIds) {
|
||||
this.$$index.add(entityId);
|
||||
this.$$map.set(entityId, this.$$ecs.get(entityId));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
@ -53,28 +53,16 @@ export default class Query {
|
|||
}
|
||||
}
|
||||
if (should) {
|
||||
this.$$index.add(entityId);
|
||||
this.$$map.set(entityId, this.$$ecs.get(entityId));
|
||||
}
|
||||
else if (!should) {
|
||||
this.$$index.delete(entityId);
|
||||
this.$$map.delete(entityId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
select() {
|
||||
const it = this.$$index.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)};
|
||||
},
|
||||
};
|
||||
return this.$$map.values();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,37 +1,39 @@
|
|||
import {expect, test} from 'vitest';
|
||||
|
||||
import Ecs from './ecs';
|
||||
import Component from './component.js';
|
||||
import Query from './query.js';
|
||||
|
||||
function wrapProperties(name, properties) {
|
||||
return class WrappedComponent extends Component {
|
||||
static name = name;
|
||||
const Components = [
|
||||
['A', {a: {type: 'int32', defaultValue: 64}}],
|
||||
['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;
|
||||
},
|
||||
};
|
||||
}
|
||||
}, {})
|
||||
|
||||
const A = new (wrapProperties('A', {a: {type: 'int32', defaultValue: 420}}));
|
||||
const B = new (wrapProperties('B', {b: {type: 'int32', defaultValue: 69}}));
|
||||
const C = new (wrapProperties('C', {c: {type: 'int32'}}));
|
||||
|
||||
const Components = {A, B, C};
|
||||
Components.A.createMany([[2], [3]]);
|
||||
Components.B.createMany([[1], [2]]);
|
||||
Components.C.createMany([[2], [4]]);
|
||||
|
||||
const fakeEcs = (Components) => ({
|
||||
Components,
|
||||
get(id) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(Components)
|
||||
.map(([componentName, Component]) => [componentName, Component.get(id)])
|
||||
.concat([['id', id]])
|
||||
);
|
||||
const ecsTest = test.extend({
|
||||
ecs: async ({}, use) => {
|
||||
const ecs = new Ecs({Components});
|
||||
await ecs.createManySpecific([
|
||||
[1, {B: {}}],
|
||||
[2, {A: {}, B: {}, C: {}}],
|
||||
[3, {A: {}}],
|
||||
[4, {C: {}}],
|
||||
]);
|
||||
await use(ecs);
|
||||
},
|
||||
});
|
||||
|
||||
function testQuery(parameters, expected) {
|
||||
const query = new Query(parameters, fakeEcs(Components));
|
||||
async function testQuery(ecs, parameters, expected) {
|
||||
const query = new Query(parameters, ecs);
|
||||
query.reindex([1, 2, 3]);
|
||||
expect(query.count)
|
||||
.to.equal(expected.length);
|
||||
|
@ -41,22 +43,22 @@ function testQuery(parameters, expected) {
|
|||
}
|
||||
}
|
||||
|
||||
test('can query all', () => {
|
||||
testQuery([], [1, 2, 3]);
|
||||
ecsTest('can query all', async ({ecs}) => {
|
||||
await testQuery(ecs, [], [1, 2, 3]);
|
||||
});
|
||||
|
||||
test('can query some', () => {
|
||||
testQuery(['A'], [2, 3]);
|
||||
testQuery(['A', 'B'], [2]);
|
||||
ecsTest('can query some', async ({ecs}) => {
|
||||
await testQuery(ecs, ['A'], [2, 3]);
|
||||
await testQuery(ecs, ['A', 'B'], [2]);
|
||||
});
|
||||
|
||||
test('can query excluding', () => {
|
||||
testQuery(['!A'], [1]);
|
||||
testQuery(['A', '!B'], [3]);
|
||||
ecsTest('can query excluding', async ({ecs}) => {
|
||||
await testQuery(ecs, ['!A'], [1]);
|
||||
await testQuery(ecs, ['A', '!B'], [3]);
|
||||
});
|
||||
|
||||
test('can deindex', () => {
|
||||
const query = new Query(['A'], fakeEcs(Components));
|
||||
ecsTest('can deindex', async ({ecs}) => {
|
||||
const query = new Query(['A'], ecs);
|
||||
query.reindex([1, 2, 3]);
|
||||
expect(query.count)
|
||||
.to.equal(2);
|
||||
|
@ -65,24 +67,22 @@ test('can deindex', () => {
|
|||
.to.equal(1);
|
||||
});
|
||||
|
||||
test('can reindex', () => {
|
||||
const Test = new (wrapProperties('Test', {a: {type: 'int32', defaultValue: 420}}));
|
||||
Test.createMany([[2], [3]]);
|
||||
const query = new Query(['Test'], fakeEcs({Test}));
|
||||
query.reindex([2, 3]);
|
||||
ecsTest('can reindex', async ({ecs}) => {
|
||||
const query = new Query(['B'], ecs);
|
||||
query.reindex([1, 2]);
|
||||
expect(query.count)
|
||||
.to.equal(2);
|
||||
Test.destroy(2);
|
||||
query.reindex([2, 3]);
|
||||
ecs.destroyMany(new Set([2]));
|
||||
query.reindex([1, 2]);
|
||||
expect(query.count)
|
||||
.to.equal(1);
|
||||
});
|
||||
|
||||
test('can select', () => {
|
||||
const query = new Query(['A'], fakeEcs(Components));
|
||||
ecsTest('can select', async ({ecs}) => {
|
||||
const query = new Query(['A'], ecs);
|
||||
query.reindex([1, 2, 3]);
|
||||
const it = query.select();
|
||||
const {value: {A}} = it.next();
|
||||
expect(A.a)
|
||||
.to.equal(420);
|
||||
.to.equal(64);
|
||||
});
|
||||
|
|
|
@ -35,6 +35,7 @@ export default class Schema {
|
|||
return {
|
||||
$: this.$$types[type],
|
||||
concrete: $$type.normalize ? $$type.normalize(rest) : rest,
|
||||
type,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -20,7 +20,6 @@ export default class System {
|
|||
for (const i in queries) {
|
||||
this.queries[i] = new Query(queries[i], ecs);
|
||||
}
|
||||
this.reindex(ecs.entities);
|
||||
}
|
||||
|
||||
deindex(entityIds) {
|
||||
|
@ -45,6 +44,10 @@ export default class System {
|
|||
}
|
||||
}
|
||||
|
||||
schedule() {
|
||||
this.elapsed = this.frequency;
|
||||
}
|
||||
|
||||
select(query) {
|
||||
return this.queries[query].select();
|
||||
}
|
||||
|
|
|
@ -10,8 +10,8 @@ export default class IntegratePhysics extends System {
|
|||
|
||||
tick(elapsed) {
|
||||
for (const {Position, Forces} of this.select('default')) {
|
||||
Position.x += elapsed * (Forces.impulseX + Forces.forceX);
|
||||
Position.y += elapsed * (Forces.impulseY + Forces.forceY);
|
||||
Position.x = Position.$$x + elapsed * (Forces.$$impulseX + Forces.$$forceX);
|
||||
Position.y = Position.$$y + elapsed * (Forces.$$impulseY + Forces.$$forceY);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -54,9 +54,11 @@ export default class MaintainColliderHash extends System {
|
|||
|
||||
within(query) {
|
||||
const within = new Set();
|
||||
if (this.hash) {
|
||||
for (const id of this.hash.within(query)) {
|
||||
within.add(this.ecs.get(id));
|
||||
}
|
||||
}
|
||||
return within;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,22 +1,50 @@
|
|||
import K from 'kefir';
|
||||
|
||||
import * as easings from '@/util/easing.js';
|
||||
import {TAU} from '@/util/math.js';
|
||||
|
||||
export default class Emitter {
|
||||
constructor(ecs) {
|
||||
this.ecs = ecs;
|
||||
this.scheduled = [];
|
||||
}
|
||||
async allocate({entity, shape}) {
|
||||
const allocated = this.ecs.get(await this.ecs.create(entity));
|
||||
async configure(entityId, {fields, shape}) {
|
||||
const entity = this.ecs.get(entityId);
|
||||
if (shape) {
|
||||
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': {
|
||||
allocated.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.x += Math.random() * shape.payload.width - (shape.payload.width / 2);
|
||||
entity.Position.y += Math.random() * shape.payload.height - (shape.payload.height / 2);
|
||||
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) {
|
||||
particle = {
|
||||
|
@ -24,59 +52,60 @@ export default class Emitter {
|
|||
entity: {
|
||||
Position: {},
|
||||
Sprite: {},
|
||||
VisibleAabb: {},
|
||||
...particle.entity,
|
||||
},
|
||||
}
|
||||
let {count = 1} = particle;
|
||||
const {frequency = 0} = particle;
|
||||
const stream = K.stream((emitter) => {
|
||||
if (0 === frequency) {
|
||||
const promises = [];
|
||||
for (let i = 0; i < count; ++i) {
|
||||
promises.push(
|
||||
this.allocate(particle)
|
||||
.then((entity) => {
|
||||
emitter.emit(entity);
|
||||
}),
|
||||
);
|
||||
const {entity, frequency = 0, spurt = 1, ttl = 0} = particle;
|
||||
if (ttl > 0) {
|
||||
count = Math.floor(ttl / frequency);
|
||||
}
|
||||
const specifications = Array(count);
|
||||
for (let i = 0; i < count * spurt; ++i) {
|
||||
specifications[i] = 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();
|
||||
});
|
||||
return;
|
||||
}
|
||||
const promise = this.allocate(particle)
|
||||
.then((entity) => {
|
||||
emitter.emit(entity);
|
||||
});
|
||||
const it = entityIds.values();
|
||||
const batch = new Set();
|
||||
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;
|
||||
if (0 === count) {
|
||||
promise.then(() => {
|
||||
emitter.end();
|
||||
});
|
||||
return;
|
||||
}
|
||||
const promises = [promise];
|
||||
let accumulated = 0;
|
||||
const scheduled = (elapsed) => {
|
||||
accumulated += elapsed;
|
||||
while (accumulated > frequency && count > 0) {
|
||||
promises.push(
|
||||
this.allocate(particle)
|
||||
.then((entity) => {
|
||||
emitter.emit(entity);
|
||||
}),
|
||||
);
|
||||
const batch = new Set();
|
||||
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));
|
||||
}
|
||||
accumulated -= frequency;
|
||||
count -= 1;
|
||||
}
|
||||
if (0 === count) {
|
||||
this.scheduled.splice(this.scheduled.indexOf(scheduled), 1);
|
||||
Promise.all(promises).then(() => {
|
||||
emitter.end();
|
||||
});
|
||||
}
|
||||
};
|
||||
this.scheduled.push(scheduled);
|
||||
|
|
|
@ -32,10 +32,8 @@ test('emits particles over time', async () => {
|
|||
current.onValue(resolve);
|
||||
}))
|
||||
.to.deep.include({id: 1});
|
||||
expect(ecs.get(1))
|
||||
.to.not.be.undefined;
|
||||
expect(ecs.get(2))
|
||||
.to.be.undefined;
|
||||
expect(Array.from(ecs.$$detached))
|
||||
.to.deep.equal([2]);
|
||||
emitter.tick(0.06);
|
||||
expect(await new Promise((resolve) => {
|
||||
current.onValue(resolve);
|
||||
|
@ -47,6 +45,6 @@ test('emits particles over time', async () => {
|
|||
current.onValue(resolve);
|
||||
}))
|
||||
.to.deep.include({id: 2});
|
||||
expect(ecs.get(2))
|
||||
.to.not.be.undefined;
|
||||
expect(Array.from(ecs.$$detached))
|
||||
.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 '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 styles from './devtools.module.css';
|
||||
|
@ -12,15 +12,15 @@ import Tiles from './devtools/tiles.jsx';
|
|||
export default function Devtools({
|
||||
eventsChannel,
|
||||
}) {
|
||||
const [ecs] = useEcs();
|
||||
const [mainEntity] = useMainEntity();
|
||||
const [mainEntityJson, setMainEntityJson] = useState('');
|
||||
useEcsTick(() => {
|
||||
if (!ecs || !mainEntity) {
|
||||
const onEcsTick = useCallback((payload, ecs) => {
|
||||
if (!mainEntity) {
|
||||
return;
|
||||
}
|
||||
setMainEntityJson(JSON.stringify(ecs.get(mainEntity), null, 2));
|
||||
}, [ecs, mainEntity]);
|
||||
}, [mainEntity]);
|
||||
useEcsTick(onEcsTick);
|
||||
return (
|
||||
<div className={styles.devtools}>
|
||||
<Tabs>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {memo, useCallback, useRef} from 'react';
|
||||
|
||||
import {DamageTypes} from '@/ecs/components/vulnerable.js';
|
||||
import {useEcsTick} from '@/react/context/ecs.js';
|
||||
import useAnimationFrame from '@/react/hooks/use-animation-frame.js';
|
||||
import {easeInOutExpo, easeInQuint, easeOutQuad, linear} from '@/util/easing.js';
|
||||
|
||||
|
@ -25,142 +26,143 @@ function damageHue(type) {
|
|||
return hue;
|
||||
}
|
||||
|
||||
function createDamageNode() {
|
||||
const damage = document.createElement('div');
|
||||
damage.classList.add(styles.damage);
|
||||
damage.appendChild(document.createElement('p'));
|
||||
return damage;
|
||||
class Damage {
|
||||
elapsed = 0;
|
||||
hue = [0, 30];
|
||||
offsetX = 0;
|
||||
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}) {
|
||||
const animations = useRef({});
|
||||
const pool = useRef([]);
|
||||
function Damages({scale}) {
|
||||
const damages = 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) => {
|
||||
if (!damagesRef.current) {
|
||||
return;
|
||||
}
|
||||
if (0 === pool.current.length) {
|
||||
for (let i = 0; i < 512; ++i) {
|
||||
const damage = createDamageNode();
|
||||
damagesRef.current.appendChild(damage);
|
||||
const keys = Object.keys(damages.current);
|
||||
if (0 === keys.length && 0 === pool.current.length) {
|
||||
for (let i = 0; i < 500; ++i) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
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);
|
||||
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 (
|
||||
<div
|
||||
className={styles.damages}
|
||||
|
|
|
@ -23,22 +23,10 @@
|
|||
inherits: false;
|
||||
syntax: '<number>';
|
||||
}
|
||||
@property --randomnessX {
|
||||
initial-value: 0;
|
||||
inherits: false;
|
||||
syntax: '<number>';
|
||||
}
|
||||
@property --randomnessY {
|
||||
initial-value: 0;
|
||||
inherits: false;
|
||||
syntax: '<number>';
|
||||
}
|
||||
|
||||
.damage {
|
||||
--hue: 0;
|
||||
--opacity: 0.75;
|
||||
--randomnessX: 0;
|
||||
--randomnessY: 0;
|
||||
--scale: 0.35;
|
||||
--background: hsl(var(--hue) 100% 12.5%);
|
||||
--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 {useRadians} from '@/react/context/radians.js';
|
||||
import useAnimationFrame from '@/react/hooks/use-animation-frame.js';
|
||||
import {RESOLUTION} from '@/util/constants.js';
|
||||
import {render} from '@/util/dialogue.js';
|
||||
|
||||
|
@ -56,20 +57,13 @@ export default function Dialogue({
|
|||
clearTimeout(handle);
|
||||
}
|
||||
}, [caret, dialogue]);
|
||||
useEffect(() => {
|
||||
let handle;
|
||||
function track() {
|
||||
const updateDimensions = useCallback(() => {
|
||||
if (ref.current) {
|
||||
const {height, width} = ref.current.getBoundingClientRect();
|
||||
setDimensions({h: height / domScale, w: width / domScale});
|
||||
}
|
||||
handle = requestAnimationFrame(track);
|
||||
}
|
||||
handle = requestAnimationFrame(track);
|
||||
return () => {
|
||||
cancelAnimationFrame(handle);
|
||||
};
|
||||
}, [dialogue, domScale, ref]);
|
||||
}, [domScale]);
|
||||
useAnimationFrame(updateDimensions);
|
||||
const localRender = useMemo(
|
||||
() => render(dialogue.letters, styles.letter),
|
||||
[dialogue.letters],
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
;
|
||||
top: 0;
|
||||
transform: translate(-50%, -50%);
|
||||
transition: translate 100ms;
|
||||
user-select: none;
|
||||
max-width: 66%;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import {useState} from 'react';
|
||||
import {useCallback, useState} from 'react';
|
||||
|
||||
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 Damages from './damages.jsx';
|
||||
|
@ -13,17 +13,11 @@ export default function Entities({
|
|||
setChatMessages,
|
||||
setMonopolizers,
|
||||
}) {
|
||||
const [ecs] = useEcs();
|
||||
const [entities, setEntities] = useState({});
|
||||
const [damages, setDamages] = useState({});
|
||||
const [pendingDamage] = useState({accumulated: [], handle: undefined});
|
||||
usePacket('EcsChange', async () => {
|
||||
setEntities({});
|
||||
}, [setEntities]);
|
||||
useEcsTick((payload) => {
|
||||
if (!ecs) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
const onEcsTick = useCallback((payload, ecs) => {
|
||||
const deleting = {};
|
||||
const updating = {};
|
||||
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) => {
|
||||
for (const id in deleting) {
|
||||
|
@ -131,7 +95,8 @@ export default function Entities({
|
|||
...updating,
|
||||
};
|
||||
});
|
||||
}, [ecs, setMonopolizers]);
|
||||
}, [setChatMessages, setMonopolizers]);
|
||||
useEcsTick(onEcsTick);
|
||||
const renderables = [];
|
||||
for (const id in entities) {
|
||||
renderables.push(
|
||||
|
@ -154,7 +119,6 @@ export default function Entities({
|
|||
>
|
||||
{renderables}
|
||||
<Damages
|
||||
damages={damages}
|
||||
scale={scale}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -4,6 +4,17 @@ import createEcs from '@/server/create/ecs.js';
|
|||
import ClientEcs from './client-ecs.js';
|
||||
|
||||
const ecs = createEcs(ClientEcs);
|
||||
|
||||
[
|
||||
'ClampPositions',
|
||||
'Colliders',
|
||||
'MaintainColliderHash',
|
||||
'VisibleAabbs',
|
||||
]
|
||||
.forEach((system) => {
|
||||
ecs.system(system).active = false;
|
||||
})
|
||||
|
||||
ecs.$$caret = Math.pow(2, 31);
|
||||
|
||||
const emitter = new Emitter(ecs);
|
||||
|
@ -17,20 +28,22 @@ addEventListener('message', (particle) => {
|
|||
.onEnd(() => {});
|
||||
});
|
||||
|
||||
let last = Date.now();
|
||||
function tick() {
|
||||
const now = Date.now();
|
||||
let last = performance.now();
|
||||
function tick(now) {
|
||||
const elapsed = (now - last) / 1000;
|
||||
last = now;
|
||||
if (ecs.get(1)) {
|
||||
requestAnimationFrame(tick);
|
||||
if (!ecs.get(1)) {
|
||||
return;
|
||||
}
|
||||
ecs.tick(elapsed);
|
||||
emitter.tick(elapsed);
|
||||
if ('1' in ecs.diff) {
|
||||
delete ecs.diff['1'];
|
||||
}
|
||||
if (Object.keys(ecs.diff).length > 0) {
|
||||
postMessage(ecs.diff);
|
||||
}
|
||||
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 {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 Entities from './entities.jsx';
|
||||
|
@ -11,14 +11,13 @@ import TileLayer from './tile-layer.jsx';
|
|||
import Water from './water.jsx';
|
||||
|
||||
export default function Ecs({camera, monopolizers, particleWorker, scale}) {
|
||||
const [ecs] = useEcs();
|
||||
const [mainEntity] = useMainEntity();
|
||||
const [layers, setLayers] = useState([]);
|
||||
const [hour, setHour] = useState(10);
|
||||
const [projected, setProjected] = useState([]);
|
||||
const [position, setPosition] = useState({x: 0, y: 0});
|
||||
const [water, setWater] = useState();
|
||||
useEcsTick((payload) => {
|
||||
const onEcsTick = useCallback((payload, ecs) => {
|
||||
const entity = ecs.get(mainEntity);
|
||||
for (const id in payload) {
|
||||
const update = payload[id];
|
||||
|
@ -43,7 +42,8 @@ export default function Ecs({camera, monopolizers, particleWorker, scale}) {
|
|||
setPosition(Position.toJSON());
|
||||
setProjected(Wielder.activeItem()?.project(Position.tile, Direction.quantize(4)));
|
||||
}
|
||||
}, [ecs, mainEntity, scale]);
|
||||
}, [mainEntity]);
|
||||
useEcsTick(onEcsTick);
|
||||
return (
|
||||
<Container
|
||||
scale={scale}
|
||||
|
@ -71,16 +71,16 @@ export default function Ecs({camera, monopolizers, particleWorker, scale}) {
|
|||
y={position.y}
|
||||
/>
|
||||
)}
|
||||
<Entities
|
||||
monopolizers={monopolizers}
|
||||
particleWorker={particleWorker}
|
||||
/>
|
||||
{projected?.length > 0 && layers[0] && (
|
||||
<TargetingGhost
|
||||
projected={projected}
|
||||
tileLayer={layers[0]}
|
||||
/>
|
||||
)}
|
||||
<Entities
|
||||
monopolizers={monopolizers}
|
||||
particleWorker={particleWorker}
|
||||
/>
|
||||
</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 {GlowFilter} from '@pixi/filter-glow';
|
||||
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 {useDebug} from '@/react/context/debug.js';
|
||||
import {useEcs, useEcsTick} from '@/react/context/ecs.js';
|
||||
import {useMainEntity} from '@/react/context/main-entity.js';
|
||||
import {useRadians} from '@/react/context/radians.js';
|
||||
|
||||
import Entity from './entity.jsx';
|
||||
import Entity from './entity.js';
|
||||
|
||||
export default function Entities({monopolizers, particleWorker}) {
|
||||
const [debug] = useDebug();
|
||||
const [ecs] = useEcs();
|
||||
const [entities, setEntities] = useState({});
|
||||
const containerRef = useRef();
|
||||
const latestTick = useRef();
|
||||
const entities = useRef({});
|
||||
const pool = useRef([]);
|
||||
const [mainEntity] = useMainEntity();
|
||||
const radians = useRadians();
|
||||
const [willInteractWith, setWillInteractWith] = useState(0);
|
||||
const willInteractWith = useRef(0);
|
||||
const [interactionFilters] = useState([
|
||||
new AdjustmentFilter(),
|
||||
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(() => {
|
||||
if (!ecs || !particleWorker) {
|
||||
return;
|
||||
}
|
||||
async function onMessage(diff) {
|
||||
latestTick.current = Promise.resolve(latestTick.current).then(async () => {
|
||||
await ecs.apply(diff.data);
|
||||
const deleted = {};
|
||||
const updated = {};
|
||||
for (const id in diff.data) {
|
||||
if (!diff.data[id]) {
|
||||
deleted[id] = true;
|
||||
}
|
||||
else {
|
||||
updated[id] = ecs.get(id);
|
||||
}
|
||||
}
|
||||
setEntities((entities) => {
|
||||
for (const id in deleted) {
|
||||
delete entities[id];
|
||||
}
|
||||
return {
|
||||
...entities,
|
||||
...updated,
|
||||
};
|
||||
updateEntities(diff.data);
|
||||
});
|
||||
}
|
||||
particleWorker.addEventListener('message', onMessage);
|
||||
return () => {
|
||||
particleWorker.removeEventListener('message', onMessage);
|
||||
};
|
||||
}, [ecs, particleWorker]);
|
||||
const pulse = (Math.cos(radians / 4) + 1) * 0.5;
|
||||
interactionFilters[0].brightness = (pulse * 0.75) + 1;
|
||||
interactionFilters[1].outerStrength = pulse * 0.5;
|
||||
usePacket('EcsChange', async () => {
|
||||
setEntities({});
|
||||
}, [setEntities]);
|
||||
useEcsTick((payload) => {
|
||||
if (!ecs) {
|
||||
return;
|
||||
}
|
||||
const deleting = {};
|
||||
const updating = {};
|
||||
}, [ecs, particleWorker, updateEntities]);
|
||||
const onEcsTickParticles = useCallback((payload) => {
|
||||
for (const id in payload) {
|
||||
if ('1' === id) {
|
||||
continue;
|
||||
}
|
||||
const update = payload[id];
|
||||
if (false === update) {
|
||||
deleting[id] = true;
|
||||
continue;
|
||||
}
|
||||
updating[id] = ecs.get(id);
|
||||
if (update.Emitter?.emit) {
|
||||
for (const id in update.Emitter.emit) {
|
||||
particleWorker?.postMessage(update.Emitter.emit[id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
setEntities((entities) => {
|
||||
for (const id in deleting) {
|
||||
delete entities[id];
|
||||
}
|
||||
return {
|
||||
...entities,
|
||||
...updating,
|
||||
};
|
||||
});
|
||||
}, [ecs, particleWorker]);
|
||||
useEcsTick(() => {
|
||||
if (!ecs) {
|
||||
return;
|
||||
}
|
||||
}, [particleWorker]);
|
||||
useEcsTick(onEcsTickParticles);
|
||||
const onEcsTickInteractions = useCallback((payload, ecs) => {
|
||||
const main = ecs.get(mainEntity);
|
||||
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]);
|
||||
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}
|
||||
willInteractWith.current = main.Interacts.willInteractWith;
|
||||
}
|
||||
const interacting = entities.current[main.Interacts.willInteractWith];
|
||||
if (interacting) {
|
||||
interacting.diffuse.filters = 0 === monopolizers.length
|
||||
? interactionFilters
|
||||
: [];
|
||||
}
|
||||
}
|
||||
}, [interactionFilters, mainEntity, monopolizers]);
|
||||
useEcsTick(onEcsTickInteractions);
|
||||
return (
|
||||
<Container
|
||||
ref={containerRef}
|
||||
sortableChildren
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Container
|
||||
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 {deferredLighting} from './lights.js';
|
||||
|
||||
const tileSize = {x: 16, y: 16};
|
||||
|
||||
const TargetingGhostInternal = PixiComponent('TargetingGhost', {
|
||||
create: () => {
|
||||
// Solid target square.
|
||||
const target = new Graphics();
|
||||
target.alpha = 0.7;
|
||||
target.lineStyle(1, 0xffffff);
|
||||
target.drawRect(0.5, 0.5, tileSize.x, tileSize.y);
|
||||
target.pivot = {x: tileSize.x / 2, y: tileSize.y / 2};
|
||||
// Inner spinny part.
|
||||
const targetInner = new Graphics();
|
||||
targetInner.alpha = 0.3;
|
||||
targetInner.lineStyle(3, 0x333333);
|
||||
targetInner.beginFill(0xdddddd);
|
||||
targetInner.pivot = {x: tileSize.x / 2, y: tileSize.y / 2};
|
||||
|
@ -25,7 +25,9 @@ const TargetingGhostInternal = PixiComponent('TargetingGhost', {
|
|||
targetInner.position.y = 0.5;
|
||||
// ...
|
||||
const container = new Container();
|
||||
container.alpha = 0.4;
|
||||
container.addChild(target, targetInner);
|
||||
container.parentGroup = deferredLighting.diffuseGroup;
|
||||
return container;
|
||||
},
|
||||
applyProps: (container, oldProps, {x, y, radians}) => {
|
||||
|
|
|
@ -35,6 +35,7 @@ function Ui({disconnected}) {
|
|||
// Key input.
|
||||
const client = useClient();
|
||||
const chatInputRef = useRef();
|
||||
const latestTick = useRef();
|
||||
const gameRef = useRef();
|
||||
const [mainEntity, setMainEntity] = useMainEntity();
|
||||
const [debug, setDebug] = useDebug();
|
||||
|
@ -310,35 +311,23 @@ function Ui({disconnected}) {
|
|||
setDebug,
|
||||
setScale,
|
||||
]);
|
||||
usePacket('EcsChange', async () => {
|
||||
const onEcsChangePacket = useCallback(() => {
|
||||
refreshEcs();
|
||||
setMainEntity(undefined);
|
||||
setMonopolizers([]);
|
||||
}, [refreshEcs, setMainEntity, setMonopolizers]);
|
||||
usePacket('Tick', async (payload, client) => {
|
||||
}, [refreshEcs, setMainEntity]);
|
||||
usePacket('EcsChange', onEcsChangePacket);
|
||||
const onTickPacket = useCallback(async (payload, client) => {
|
||||
if (0 === Object.keys(payload.ecs).length) {
|
||||
return;
|
||||
}
|
||||
latestTick.current = Promise.resolve(latestTick.current).then(async () => {
|
||||
await ecs.apply(payload.ecs);
|
||||
client.emitter.invoke(':Ecs', payload.ecs);
|
||||
}, [ecs]);
|
||||
useEcsTick((payload) => {
|
||||
if (!('1' in payload) || particleWorker) {
|
||||
return
|
||||
}
|
||||
const localParticleWorker = new Worker(
|
||||
new URL('./particle-worker.js', import.meta.url),
|
||||
{type: 'module'},
|
||||
);
|
||||
localParticleWorker.postMessage(ecs.get(1).toJSON());
|
||||
setParticleWorker((particleWorker) => {
|
||||
if (particleWorker) {
|
||||
particleWorker.terminate();
|
||||
}
|
||||
return localParticleWorker;
|
||||
});
|
||||
}, [particleWorker]);
|
||||
useEcsTick((payload) => {
|
||||
}, [ecs]);
|
||||
usePacket('Tick', onTickPacket);
|
||||
const onEcsTick = useCallback((payload, ecs) => {
|
||||
let localMainEntity = mainEntity;
|
||||
for (const id in payload) {
|
||||
const entity = ecs.get(id);
|
||||
|
@ -346,14 +335,6 @@ function Ui({disconnected}) {
|
|||
if (!update) {
|
||||
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) {
|
||||
setMainEntity(localMainEntity = id);
|
||||
}
|
||||
|
@ -390,15 +371,60 @@ function Ui({disconnected}) {
|
|||
}
|
||||
}
|
||||
}
|
||||
if (localMainEntity) {
|
||||
const mainEntityEntity = ecs.get(localMainEntity);
|
||||
}, [hotbarHideHandle, mainEntity, setMainEntity]);
|
||||
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 y = Math.round((mainEntityEntity.Camera.y * scale) - RESOLUTION.y / 2);
|
||||
if (x !== camera.x || y !== camera.y) {
|
||||
setCamera({x, y});
|
||||
}
|
||||
}
|
||||
}, [camera, ecs, hotbarHideHandle, mainEntity, scale]);
|
||||
}, [camera, mainEntity, scale]);
|
||||
useEcsTick(onEcsTickCamera);
|
||||
useEffect(() => {
|
||||
function onContextMenu(event) {
|
||||
event.preventDefault();
|
||||
|
|
|
@ -8,7 +8,7 @@ export function useClient() {
|
|||
return useContext(context);
|
||||
}
|
||||
|
||||
export function usePacket(type, fn, dependencies) {
|
||||
export function usePacket(type, fn) {
|
||||
const client = useClient();
|
||||
useEffect(() => {
|
||||
if (!client) {
|
||||
|
@ -21,6 +21,5 @@ export function usePacket(type, fn, dependencies) {
|
|||
return () => {
|
||||
client.removePacketListener(type, listener);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [client, ...dependencies]);
|
||||
}, [client, fn]);
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import {createContext, useContext} from 'react';
|
||||
import {createContext, useCallback, useContext} from 'react';
|
||||
|
||||
import {usePacket} from './client.js';
|
||||
|
||||
|
@ -10,7 +10,13 @@ export function useEcs() {
|
|||
return useContext(context);
|
||||
}
|
||||
|
||||
export function useEcsTick(fn, dependencies) {
|
||||
export function useEcsTick(fn) {
|
||||
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 {useEffect, useState} from 'react';
|
||||
import {useCallback, useEffect, useState} from 'react';
|
||||
import {useOutletContext, useParams} from 'react-router-dom';
|
||||
|
||||
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 MainEntityContext from '@/react/context/main-entity.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 {TAU} from '@/util/math.js';
|
||||
|
||||
|
@ -29,23 +30,10 @@ export default function PlaySpecific() {
|
|||
const params = useParams();
|
||||
const [type, url] = params['*'].split('/');
|
||||
const [radians, setRadians] = useState(0);
|
||||
useEffect(() => {
|
||||
let handle;
|
||||
let last;
|
||||
const spin = (ts) => {
|
||||
if ('undefined' === typeof last) {
|
||||
last = ts;
|
||||
}
|
||||
const elapsed = (ts - last) / 1000;
|
||||
last = ts;
|
||||
const spin = useCallback((elapsed) => {
|
||||
setRadians((radians) => radians + (elapsed * TAU));
|
||||
handle = requestAnimationFrame(spin);
|
||||
};
|
||||
handle = requestAnimationFrame(spin);
|
||||
return () => {
|
||||
cancelAnimationFrame(handle);
|
||||
};
|
||||
}, []);
|
||||
useAnimationFrame(spin);
|
||||
useEffect(() => {
|
||||
if (!Client) {
|
||||
return;
|
||||
|
|
|
@ -1,5 +1,27 @@
|
|||
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) {
|
||||
const area = {x: 100, y: 60};
|
||||
const entities = [];
|
||||
|
@ -138,8 +160,10 @@ export default async function createHomestead(id) {
|
|||
Ticking: {},
|
||||
VisibleAabb: {},
|
||||
});
|
||||
const kitty = {
|
||||
Alive: {health: 100},
|
||||
const animalJson = animal();
|
||||
for (let i = 0; i < 50; ++i) {
|
||||
entities.push({
|
||||
...animalJson,
|
||||
Behaving: {
|
||||
routines: {
|
||||
initial: '/assets/kitty/initial.js',
|
||||
|
@ -157,10 +181,6 @@ export default async function createHomestead(id) {
|
|||
},
|
||||
],
|
||||
},
|
||||
Controlled: {},
|
||||
Direction: {},
|
||||
Emitter: {},
|
||||
Forces: {},
|
||||
Interactive: {
|
||||
interacting: 1,
|
||||
interactScript: `
|
||||
|
@ -181,22 +201,122 @@ export default async function createHomestead(id) {
|
|||
})
|
||||
`,
|
||||
},
|
||||
Interlocutor: {},
|
||||
Position: {x: 250, y: 250},
|
||||
Speed: {speed: 20},
|
||||
Position: {
|
||||
x: 250 + (Math.random() - 0.5) * 300,
|
||||
y: 250 + (Math.random() - 0.5) * 300,
|
||||
},
|
||||
Sprite: {
|
||||
...animalJson.Sprite,
|
||||
anchorX: 0.5,
|
||||
anchorY: 0.7,
|
||||
source: '/assets/kitty/kitty.json',
|
||||
speed: 0.115,
|
||||
},
|
||||
Tags: {tags: ['kittan']},
|
||||
Ticking: {},
|
||||
VisibleAabb: {},
|
||||
Vulnerable: {},
|
||||
};
|
||||
});
|
||||
}
|
||||
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({
|
||||
Collider: {
|
||||
|
|
|
@ -37,7 +37,7 @@ export default async function createPlayer(id) {
|
|||
Magnet: {strength: 24},
|
||||
Player: {},
|
||||
Position: {x: 128, y: 448},
|
||||
Speed: {speed: 100},
|
||||
Speed: {speed: 300},
|
||||
Sound: {},
|
||||
Sprite: {
|
||||
anchorX: 0.5,
|
||||
|
@ -55,4 +55,3 @@ export default async function createPlayer(id) {
|
|||
};
|
||||
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 Sandbox from '@/astride/sandbox.js';
|
||||
import * as color from '@/util/color.js';
|
||||
import delta from '@/util/delta.js';
|
||||
import lfo from '@/util/lfo.js';
|
||||
import * as MathUtil from '@/util/math.js';
|
||||
|
@ -40,6 +41,7 @@ export default class Script {
|
|||
|
||||
static contextDefaults() {
|
||||
return {
|
||||
color,
|
||||
console,
|
||||
delta,
|
||||
lfo,
|
||||
|
@ -117,7 +119,7 @@ export default class Script {
|
|||
}
|
||||
return;
|
||||
}
|
||||
if (async) {
|
||||
if (async || value instanceof Promise) {
|
||||
this.promise = value;
|
||||
value
|
||||
.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');
|
||||
for (const {x, y} of projected) {
|
||||
Emitter.emit({
|
||||
count: 25,
|
||||
frequency: 0.01,
|
||||
shape: {
|
||||
type: 'filledRect',
|
||||
payload: {width: 16, height: 16},
|
||||
},
|
||||
entity: {
|
||||
Forces: {forceY: -50},
|
||||
Behaving: {
|
||||
routines: {
|
||||
initial: 'await delta(entity.Forces, {forceY: {delta: 640, duration: 0.125}}).promise',
|
||||
},
|
||||
},
|
||||
Forces: {forceY: -80},
|
||||
Position: {
|
||||
x: x * layer.tileSize.x + (layer.tileSize.x / 2),
|
||||
y: y * layer.tileSize.y + (layer.tileSize.y / 2),
|
||||
},
|
||||
Sprite: {
|
||||
scaleX: 0.2,
|
||||
scaleY: 0.2,
|
||||
tint: 0x552200,
|
||||
},
|
||||
Ttl: {ttl: 0.125},
|
||||
}
|
||||
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(':');
|
||||
|
|
|
@ -75,7 +75,7 @@ while (shots.length > 0) {
|
|||
)
|
||||
shot.Speed.speed = 400;
|
||||
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];
|
||||
shot.Sprite.alpha = 0;
|
||||
ecs.destroy(shot.id);
|
||||
|
|
|
@ -14,25 +14,31 @@ if (projected?.length > 0) {
|
|||
|
||||
for (const {x, y} of projected) {
|
||||
Emitter.emit({
|
||||
count: 25,
|
||||
frequency: 0.01,
|
||||
shape: {
|
||||
type: 'filledRect',
|
||||
payload: {width: 16, height: 16},
|
||||
},
|
||||
entity: {
|
||||
Forces: {forceY: 100},
|
||||
Position: {
|
||||
x: x * layer.tileSize.x + (layer.tileSize.x / 2),
|
||||
y: y * layer.tileSize.y - layer.tileSize.y,
|
||||
y: y * layer.tileSize.y - (layer.tileSize.y / 4),
|
||||
},
|
||||
Sprite: {
|
||||
scaleX: 0.2,
|
||||
scaleY: 0.2,
|
||||
scaleX: 0.05,
|
||||
scaleY: 0.15,
|
||||
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);
|
||||
|
@ -49,6 +55,8 @@ if (projected?.length > 0) {
|
|||
Water.water[tileIndex] = Math.min(255, 64 + w);
|
||||
}
|
||||
|
||||
ecs.system('Water').schedule();
|
||||
|
||||
Controlled.locked = 0;
|
||||
|
||||
}
|
||||
|
|