This commit is contained in:
cha0s 2021-01-03 00:57:51 -06:00
parent ebdf85c8a1
commit 1ba411a690
13 changed files with 408 additions and 177 deletions

View File

@ -1,4 +1,4 @@
import compile from './expression';
import compile from './compile';
export default (latus) => ({expressions}) => () => (
expressions.map((expression) => compile(expression, latus))

View File

@ -55,10 +55,6 @@ export default (latus) => class Entity extends decorate(JsonResource) {
this.position = [0, 0];
this.room = null;
this.visibleAabb = [0, 0, 0, 0];
// Fast path for instance.
// if ('undefined' !== typeof json) {
// this.fromJSON(merge(json, jsonext));
// }
this.instanceUuid = instanceUuid || this.numericId;
this.addTraits(traits);
}
@ -87,12 +83,10 @@ export default (latus) => class Entity extends decorate(JsonResource) {
}
addTrait([Trait, json]) {
// const Trait = trait.constructor;
if (this.is(Trait.type)) {
debug(`Tried to add trait "${Trait.type}" when it already exists!`);
return;
}
// const {fromType: {[type]: Trait}} = traits(latus);
if (!Trait) {
debug(`Tried to add trait "${Trait.type}" which isn't registered!`);
return;
@ -112,7 +106,6 @@ export default (latus) => class Entity extends decorate(JsonResource) {
// Instantiate.
// eslint-disable-next-line no-param-reassign
json.entity = this;
// const instance = new Trait(json);
const trait = new Trait(json);
// Proxy properties.
defineTraitAccessors(Trait.prototype, this, trait);
@ -135,7 +128,7 @@ export default (latus) => class Entity extends decorate(JsonResource) {
this._hooks[key] = this._hooks[key] || [];
this._hooks[key].push({
fn,
type: Trait.type(),
type: Trait.type,
});
}
// Track trait.
@ -157,11 +150,6 @@ export default (latus) => class Entity extends decorate(JsonResource) {
for (let i = 0; i < traits.length; i++) {
this.addTrait(traits[i]);
}
// const entries = Object.entries(traits);
// for (let i = 0; i < entries.length; i++) {
// const [type, trait] = entries[i];
// this.addTrait(type, trait);
// }
}
allTraitInstances() {
@ -182,35 +170,10 @@ export default (latus) => class Entity extends decorate(JsonResource) {
this._fastDirtyCheck = false;
}
// fromJSON(json) {
// super.fromJSON(json);
// if (json.instanceUuid) {
// this.instanceUuid = json.instanceUuid;
// }
// this.addTraits(json.traits);
// return this;
// }
// hydrate() {
// if (!this._hydrationPromise) {
// const promises = [];
// for (let i = 0; i < this._traitsFlat.length; i++) {
// promises.push(this._traitsFlat[i].hydrate());
// }
// this._hydrationPromise = Promise.all(promises);
// this._hydrationPromise.then(() => {
// this.tick(0);
// });
// }
// return this._hydrationPromise;
// }
static async extendJson(json) {
const extended = await super.extendJson(json);
// const {fromResourceType: {EntityList, Tileset}} = resource(latus);
if (extended.traits) {
const entries = Object.entries(extended.traits);
// extended.traits = [];
const promises = entries.map(async ([type, json]) => {
const {fromType: {[type]: Trait}} = traits(latus);
if (!Trait) {
@ -220,20 +183,7 @@ export default (latus) => class Entity extends decorate(JsonResource) {
return [Trait, await Trait.extendJson(json)];
});
extended.traits = (await Promise.all(promises)).filter((entry) => !!entry);
// for (let i = 0; i < entries.length; i++) {
// const [type, json] = entries[i];
// const {fromType: {[type]: Trait}} = traits(latus);
// if (!Trait) {
// debug(`Tried to add trait "${type}" which isn't registered!`);
// }
// else {
// extended.traits.push(await Trait.load(json));
// }
// }
}
// if (extended.tilesetUri) {
// extended.tileset = await Tileset.load(extended.tilesetUri);
// }
return extended;
}

View File

@ -9,6 +9,10 @@ import {
import {StateProperty, Trait} from '@avocado/traits';
import {compose} from '@latus/core';
const {
SIDE,
} = process.env;
const decorate = compose(
StateProperty('isDying', {
track: true,
@ -23,6 +27,31 @@ const decorate = compose(
export default (latus) => class Alive extends decorate(Trait) {
constructor(...args) {
super(...args);
this._context = new Context(
{
entity: [this.entity, 'entity'],
},
latus,
);
this._deathActions = new Actions(compile(this.params.deathActions, latus));
this._deathCondition = compile(this.params.deathCondition, latus);
}
acceptPacket(packet) {
switch (packet.constructor.name) {
case 'DiedPacket':
this.entity.forceDeath();
break;
case 'TraitUpdateAlivePacket':
this.entity.life = packet.data.life;
this.entity.maxLife = packet.data.maxLife;
break;
default:
}
}
static behaviorTypes() {
return {
deathSound: {
@ -36,6 +65,10 @@ export default (latus) => class Alive extends decorate(Trait) {
};
}
get deathSound() {
return this.params.deathSound;
}
static defaultParams() {
const playDeathSound = buildInvoke(['entity', 'playSound'], [
buildExpression(['entity', 'deathSound']),
@ -107,77 +140,38 @@ export default (latus) => class Alive extends decorate(Trait) {
};
}
constructor(...args) {
super(...args);
this._context = new Context(
{
entity: [this.entity, 'entity'],
},
latus,
);
this._deathActions = new Actions(compile(this.params.deathActions, latus));
this._deathCondition = compile(this.params.deathCondition, latus);
}
destroy() {
this._context.destroy();
}
acceptPacket(packet) {
switch (packet.constructor.name) {
case 'DiedPacket':
this.entity.forceDeath();
break;
case 'TraitUpdateAlivePacket':
this.entity.life = packet.data.life;
this.entity.maxLife = packet.data.maxLife;
break;
default:
}
get life() {
return super.life;
}
get deathSound() {
return this.params.deathSound;
}
packets() {
const packets = [];
const {isDying, life, maxLife} = this.stateDifferences();
if (life || maxLife) {
packets.push([
'TraitUpdateAlivePacket',
{
life: this.state.life,
maxLife: this.state.maxLife,
},
]);
}
if (isDying) {
packets.push(['DiedPacket']);
}
return packets;
set life(life) {
// Clamp health between 0 and max.
super.life = Math.min(Math.max(0, life), this.entity.maxLife);
}
listeners() {
return {
tookHarm: (harm) => {
if (harm.isHarm) {
this.entity.life -= harm.amount;
}
else {
this.entity.life += harm.amount;
}
// Clamp health between 0 and max.
this.entity.life = Math.min(
Math.max(0, this.entity.life),
this.entity.maxLife,
);
this.entity.life += (harm.isHarm ? -1 : 1) * harm.amount;
},
};
}
get maxLife() {
return super.maxLife;
}
set maxLife(maxLife) {
super.maxLife = maxLife;
this.entity.life = super.life;
}
methods() {
return {
@ -194,8 +188,26 @@ export default (latus) => class Alive extends decorate(Trait) {
};
}
packets() {
const packets = [];
const {isDying, life, maxLife} = this.stateDifferences();
if (life || maxLife) {
packets.push([
'TraitUpdateAlive',
{
life: this.state.life,
maxLife: this.state.maxLife,
},
]);
}
if (isDying) {
packets.push(['Died']);
}
return packets;
}
tick() {
if (AVOCADO_SERVER) {
if ('client' !== SIDE) {
if (!this.entity.isDying && this._deathCondition(this._context)) {
this.entity.forceDeath();
}

View File

@ -0,0 +1,118 @@
import {
buildCondition,
buildExpression,
buildInvoke,
} from '@avocado/behavior';
import {resource} from '@avocado/resource';
import {Latus} from '@latus/core';
import {expect} from 'chai';
const {name} = require('../package.json');
describe(name, () => {
let latus;
let Entity;
beforeEach(async () => {
latus = Latus.mock([
'@avocado/behavior',
['@avocado/entity', `${__dirname}/../src`],
'@avocado/resource',
'@avocado/traits',
'@latus/socket',
]);
await Promise.all(latus.invokeFlat('@latus/core/starting'));
({fromResourceType: {Entity}} = resource(latus));
});
describe('Traits', () => {
describe('Alive', () => {
let entity;
beforeEach(async () => {
entity = await Entity.load({
traits: {
Alive: {},
},
});
});
it('exists', async () => {
expect(entity.is('Alive')).to.equal(true);
});
it('is alive', async () => {
expect(entity.isDying).to.equal(false);
});
it('can die', async () => {
entity.life = 0;
entity.tick(0);
expect(entity.isDying).to.equal(true);
});
it('clamps life', async () => {
entity.life = 120;
expect(entity.life).to.equal(100);
entity.maxLife = 50;
expect(entity.life).to.equal(50);
});
it('can have a custom death condition', async () => {
const entity = await Entity.load({
traits: {
Alive: {
params: {
deathCondition: buildCondition('<=', [
buildExpression(['entity', 'life']),
10,
]),
},
},
},
});
entity.tick(0);
expect(entity.isDying).to.equal(false);
entity.life = 10;
entity.tick(0);
expect(entity.isDying).to.equal(true);
});
it('runs actions on death', async () => {
let didActions;
const entity = await Entity.load({
traits: {
Alive: {
params: {
deathActions: {
type: 'expressions',
expressions: [
buildInvoke(['entity', 'ded']),
],
},
},
},
},
});
entity.ded = () => {
didActions = true;
};
const handle = setInterval(() => {
entity.tick();
}, 16.66);
await entity.forceDeath();
clearInterval(handle);
expect(didActions).to.equal(true);
});
describe('Packets', () => {
it('generates life packets', async () => {
entity.life = 80;
entity.maxLife = 90;
const packets = entity.traitInstance('Alive').packets();
expect(packets).to.have.lengthOf(1);
expect(packets[0][0]).to.equal('TraitUpdateAlive');
expect(packets[0][1]).to.deep.equal({life: 80, maxLife: 90});
});
it('generates death packets', async () => {
entity.life = 0;
entity.tick();
const packets = entity.traitInstance('Alive').packets();
expect(packets).to.have.lengthOf(2);
expect(packets[0][0]).to.equal('TraitUpdateAlive');
expect(packets[1][0]).to.equal('Died');
});
});
});
});
});

View File

@ -0,0 +1,31 @@
import {resource} from '@avocado/resource';
import {Latus} from '@latus/core';
import {expect} from 'chai';
const {name} = require('../package.json');
describe(name, () => {
let latus;
let Entity;
beforeEach(async () => {
latus = Latus.mock([
['@avocado/entity', `${__dirname}/../src`],
'@avocado/resource',
'@avocado/traits',
]);
await Promise.all(latus.invokeFlat('@latus/core/starting'));
({fromResourceType: {Entity}} = resource(latus));
});
describe('Traits', () => {
describe('Directional', () => {
it('is Directional', async () => {
const entity = await Entity.load({
traits: {
Directional: {},
},
});
expect(entity.is('Directional')).to.equal(true);
});
});
});
});

View File

@ -57,70 +57,4 @@ describe(name, () => {
});
expect(Date.now() - start).to.be.at.least(DELAY);
});
describe('Traits', () => {
it('is Alive', async () => {
const entity = await Entity.load({
traits: {
Alive: {},
},
});
expect(entity.is('Alive')).to.equal(true);
});
it('is Directional', async () => {
const entity = await Entity.load({
traits: {
Directional: {},
},
});
expect(entity.is('Directional')).to.equal(true);
});
it('is Existent', async () => {
const entity = await Entity.load({
traits: {
Existent: {},
},
});
expect(entity.is('Existent')).to.equal(true);
});
it('is Listed', async () => {
const entity = await Entity.load({
traits: {
Listed: {},
},
});
expect(entity.is('Listed')).to.equal(true);
});
it('is Mobile', async () => {
const entity = await Entity.load({
traits: {
Mobile: {},
},
});
expect(entity.is('Mobile')).to.equal(true);
});
it('is Perishable', async () => {
const entity = await Entity.load({
traits: {
Perishable: {},
},
});
expect(entity.is('Perishable')).to.equal(true);
});
it('is Positioned', async () => {
const entity = await Entity.load({
traits: {
Positioned: {},
},
});
expect(entity.is('Positioned')).to.equal(true);
});
it('is Spawner', async () => {
const entity = await Entity.load({
traits: {
Spawner: {},
},
});
expect(entity.is('Spawner')).to.equal(true);
});
});
});

View File

@ -0,0 +1,31 @@
import {resource} from '@avocado/resource';
import {Latus} from '@latus/core';
import {expect} from 'chai';
const {name} = require('../package.json');
describe(name, () => {
let latus;
let Entity;
beforeEach(async () => {
latus = Latus.mock([
['@avocado/entity', `${__dirname}/../src`],
'@avocado/resource',
'@avocado/traits',
]);
await Promise.all(latus.invokeFlat('@latus/core/starting'));
({fromResourceType: {Entity}} = resource(latus));
});
describe('Traits', () => {
describe('Existent', () => {
it('is Existent', async () => {
const entity = await Entity.load({
traits: {
Existent: {},
},
});
expect(entity.is('Existent')).to.equal(true);
});
});
});
});

View File

@ -0,0 +1,31 @@
import {resource} from '@avocado/resource';
import {Latus} from '@latus/core';
import {expect} from 'chai';
const {name} = require('../package.json');
describe(name, () => {
let latus;
let Entity;
beforeEach(async () => {
latus = Latus.mock([
['@avocado/entity', `${__dirname}/../src`],
'@avocado/resource',
'@avocado/traits',
]);
await Promise.all(latus.invokeFlat('@latus/core/starting'));
({fromResourceType: {Entity}} = resource(latus));
});
describe('Traits', () => {
describe('Listed', () => {
it('is Listed', async () => {
const entity = await Entity.load({
traits: {
Listed: {},
},
});
expect(entity.is('Listed')).to.equal(true);
});
});
});
});

View File

@ -0,0 +1,31 @@
import {resource} from '@avocado/resource';
import {Latus} from '@latus/core';
import {expect} from 'chai';
const {name} = require('../package.json');
describe(name, () => {
let latus;
let Entity;
beforeEach(async () => {
latus = Latus.mock([
['@avocado/entity', `${__dirname}/../src`],
'@avocado/resource',
'@avocado/traits',
]);
await Promise.all(latus.invokeFlat('@latus/core/starting'));
({fromResourceType: {Entity}} = resource(latus));
});
describe('Traits', () => {
describe('Mobile', () => {
it('is Mobile', async () => {
const entity = await Entity.load({
traits: {
Mobile: {},
},
});
expect(entity.is('Mobile')).to.equal(true);
});
});
});
});

View File

@ -0,0 +1,31 @@
import {resource} from '@avocado/resource';
import {Latus} from '@latus/core';
import {expect} from 'chai';
const {name} = require('../package.json');
describe(name, () => {
let latus;
let Entity;
beforeEach(async () => {
latus = Latus.mock([
['@avocado/entity', `${__dirname}/../src`],
'@avocado/resource',
'@avocado/traits',
]);
await Promise.all(latus.invokeFlat('@latus/core/starting'));
({fromResourceType: {Entity}} = resource(latus));
});
describe('Traits', () => {
describe('Perishable', () => {
it('is Perishable', async () => {
const entity = await Entity.load({
traits: {
Perishable: {},
},
});
expect(entity.is('Perishable')).to.equal(true);
});
});
});
});

View File

@ -0,0 +1,31 @@
import {resource} from '@avocado/resource';
import {Latus} from '@latus/core';
import {expect} from 'chai';
const {name} = require('../package.json');
describe(name, () => {
let latus;
let Entity;
beforeEach(async () => {
latus = Latus.mock([
['@avocado/entity', `${__dirname}/../src`],
'@avocado/resource',
'@avocado/traits',
]);
await Promise.all(latus.invokeFlat('@latus/core/starting'));
({fromResourceType: {Entity}} = resource(latus));
});
describe('Traits', () => {
describe('Positioned', () => {
it('is Positioned', async () => {
const entity = await Entity.load({
traits: {
Positioned: {},
},
});
expect(entity.is('Positioned')).to.equal(true);
});
});
});
});

View File

@ -0,0 +1,31 @@
import {resource} from '@avocado/resource';
import {Latus} from '@latus/core';
import {expect} from 'chai';
const {name} = require('../package.json');
describe(name, () => {
let latus;
let Entity;
beforeEach(async () => {
latus = Latus.mock([
['@avocado/entity', `${__dirname}/../src`],
'@avocado/resource',
'@avocado/traits',
]);
await Promise.all(latus.invokeFlat('@latus/core/starting'));
({fromResourceType: {Entity}} = resource(latus));
});
describe('Traits', () => {
describe('Spawner', () => {
it('is Spawner', async () => {
const entity = await Entity.load({
traits: {
Spawner: {},
},
});
expect(entity.is('Spawner')).to.equal(true);
});
});
});
});

View File

@ -4,8 +4,8 @@
"@avocado/behavior@2.0.0":
version "2.0.0"
resolved "https://npm.i12e.cha0s.io/@avocado%2fbehavior/-/behavior-2.0.0.tgz#24d29c0cff73aea30ad323b5c5cb474ee9fe8c8a"
integrity sha512-BQVlDW80DeRj6ZoPDjq7EuAezCYGixsLhNemrDaDnmPfk+Y3nPIMR5sbq7acIw2QM6NkW6aud5sLxboxgwIMbA==
resolved "https://npm.i12e.cha0s.io/@avocado%2fbehavior/-/behavior-2.0.0.tgz#6012a83233beb48ab85d965a1834097961cd7fda"
integrity sha512-f2uTKGXFY0aLxS7VCh7+6q8S7HhkRLnl8tEdNpmqUIlXR7fglm/CCIk5ueZJ2sYCnnBfloptdU31ir8PTCGOBw==
dependencies:
"@avocado/core" "2.0.0"
"@avocado/traits" "^2.0.0"
@ -1029,8 +1029,8 @@
"@latus/socket@2.0.0":
version "2.0.0"
resolved "https://npm.i12e.cha0s.io/@latus%2fsocket/-/socket-2.0.0.tgz#f8843d8733c3f84b7ac9f72afb62a1c783077a7d"
integrity sha512-+hpxoIo0lILIZ2U/xkvD3R6Mv5/SJKaMjKTBbyefMPxwK1TDnhO3peZb5SEizITNw8xmsM/Y06h7s2TrlYN64Q==
resolved "https://npm.i12e.cha0s.io/@latus%2fsocket/-/socket-2.0.0.tgz#b0590eabc39928803ede05dada3b31603ede8e20"
integrity sha512-i6Ed+2tWenNq3S157Vo0BXgMXentWfwCafyD+/3xqKt9tNI2+e2Fp8lQfAYLQoj1jrnvdL+cmJh3bAOLALWDtw==
dependencies:
"@latus/core" "2.0.0"
"@latus/http" "2.0.0"