feat: resource registration

This commit is contained in:
cha0s 2019-09-16 21:52:08 -05:00
parent d5fac99c7b
commit 305f421085
21 changed files with 566 additions and 485 deletions

View File

@ -0,0 +1,368 @@
import D from 'debug';
import without from 'lodash.without';
import {
compose,
EventEmitter,
fastApply,
merge,
mergeDiff,
} from '@avocado/core';
import {Resource} from '@avocado/resource';
import {Synchronized} from '@avocado/state';
import {EntityCreatePacket} from './packets/entity-create.packet';
import {hasTrait, lookupTrait} from './trait/registry';
const debug = D('@avocado:entity:traits');
const blacklistedAccessorKeys = [
'state',
];
// This really seems like a whole lot of complicated nonsense, but it's an
// unfortunate consequence of V8 (and maybe others) not optimizing mutable
// accessors in fast hidden classes.
const traitAccessorForPropertyMap = {};
function traitAccessorForProperty(type, property) {
if (!(type in traitAccessorForPropertyMap)) {
traitAccessorForPropertyMap[type] = {};
}
if (!(property in traitAccessorForPropertyMap[type])) {
traitAccessorForPropertyMap[type][property] = {
get: new Function('', `
return this._traits['${type}']['${property}'];
`),
set: new Function('value', `
this._traits['${type}']['${property}'] = value;
`),
};
}
return traitAccessorForPropertyMap[type][property];
}
function defineTraitAccessors(from, to, instance) {
const type = instance.constructor.type();
do {
Object.getOwnPropertyNames(from).forEach((accessorKey) => {
if (-1 !== blacklistedAccessorKeys.indexOf(accessorKey)) {
return;
}
let descriptor = Object.getOwnPropertyDescriptor(from, accessorKey);
// Make sure it's actually an accessor.
if (!descriptor.get && !descriptor.set) {
return;
}
const accessor = traitAccessorForProperty(type, accessorKey);
if (descriptor.get) {
descriptor.get = accessor.get;
}
if (descriptor.set) {
descriptor.set = accessor.set;
}
Object.defineProperty(to, accessorKey, descriptor);
});
} while (Object.prototype !== (from = Object.getPrototypeOf(from)));
}
function enumerateTraitAccessorKeys(prototype) {
const keys = [];
do {
Object.getOwnPropertyNames(prototype).forEach((accessorKey) => {
if (-1 !== blacklistedAccessorKeys.indexOf(accessorKey)) {
return;
}
let descriptor = Object.getOwnPropertyDescriptor(prototype, accessorKey);
// Make sure it's actually an accessor.
if (!descriptor.get && !descriptor.set) {
return;
}
keys.push(accessorKey);
});
} while (Object.prototype !== (prototype = Object.getPrototypeOf(prototype)));
return keys;
}
const decorate = compose(
EventEmitter,
Synchronized,
);
let numericUid = 1;
export class Entity extends decorate(Resource) {
constructor(json, jsonext) {
super();
this._hooks = {};
this._hydrationPromise = undefined;
this._json = Entity.jsonWithDefaults(json);
this._traits = {};
this._traitsFlat = [];
this._traitTickers = [];
this._traitRenderTickers = [];
this._traitsAcceptingPackets = [];
this.once('destroyed', () => {
this.removeAllTraits();
});
// Bind to prevent lookup overhead.
this.tick = this.tick.bind(this);
// Fast props.
this.numericUid = numericUid++;
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));
}
}
acceptPacket(packet) {
for (let i = 0; i < this._traitsAcceptingPackets.length; i++) {
const instance = this._traitsAcceptingPackets[i];
instance.acceptPacket(packet);
}
}
addTrait(type, json = {}) {
if (this.is(type)) {
debug(`Tried to add trait "${type}" when it already exists!`);
return;
}
if (!hasTrait(type)) {
debug(`Tried to add trait "${type}" which isn't registered!`);
return;
}
const Trait = lookupTrait(type);
// Ensure dependencies.
const dependencies = Trait.dependencies();
const allTypes = this.allTraitTypes();
const lacking = without(dependencies, ...allTypes);
if (lacking.length > 0) {
debug(
`Tried to add trait "${type}" but lack one or more dependents: "${
lacking.join('", "')
}"!`
);
return;
}
// Instantiate.
const {params, state} = json;
const instance = new Trait(this, params, state);
// Proxy properties.
defineTraitAccessors(Trait.prototype, this, instance);
// Attach listeners.
const listeners = instance.memoizedListeners();
for (const eventName in listeners) {
this.on(eventName, listeners[eventName]);
}
// Proxy methods.
const methods = instance.methods();
for (const key in methods) {
this[key] = methods[key];
}
// Register hook listeners.
const hooks = instance.hooks();
for (const key in hooks) {
this._hooks[key] = this._hooks[key] || [];
this._hooks[key].push({
fn: hooks[key],
type: Trait.type(),
});
}
// Track trait.
this._traits[type] = instance;
this._traitsFlat.push(instance);
if ('tick' in instance) {
this._traitTickers.push(instance.tick);
}
if ('renderTick' in instance) {
this._traitRenderTickers.push(instance.renderTick);
}
if ('acceptPacket' in instance) {
this._traitsAcceptingPackets.push(instance);
}
this.emit('traitAdded', type, instance);
}
addTraits(traits) {
for (const type in traits) {
this.addTrait(type, traits[type]);
}
}
allTraitInstances() {
return this._traits;
}
allTraitTypes() {
return Object.keys(this._traits);
}
fromJSON(json) {
super.fromJSON(json);
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);
}
return this._hydrationPromise;
}
invokeHook(hook, ...args) {
const results = {};
if (!(hook in this._hooks)) {
return results;
}
for (const {fn, type} of this._hooks[hook]) {
results[type] = fastApply(null, fn, args);
}
return results;
}
invokeHookFlat(hook, ...args) {
const invokeResults = this.invokeHook(hook, ...args);
const results = [];
for (const type in invokeResults) {
results.push(invokeResults[type]);
}
return results;
}
is(type) {
return type in this._traits;
}
static jsonWithDefaults(json) {
if ('undefined' === typeof json) {
return;
}
const pristine = JSON.parse(JSON.stringify(json));
for (const type in json.traits) {
if (!hasTrait(type)) {
continue;
}
const Trait = lookupTrait(type);
pristine.traits[type] = merge({}, Trait.defaultJSON(), json.traits[type]);
}
return pristine;
}
mergeDiff() {
if (!this.uri) {
return this.toJSON();
}
return mergeDiff(this._json, this.toJSON());
}
packetsForUpdate(force = false) {
const packets = [];
if (force) {
const packet = new EntityCreatePacket(this.mergeDiff(), this);
packets.push(packet);
}
else {
for (let i = 0; i < this._traitsFlat.length; i++) {
const traitPackets = this._traitsFlat[i].packetsForUpdate();
for (let j = 0; j < traitPackets.length; j++) {
packets.push(traitPackets[j]);
}
}
}
return packets;
}
renderTick(elapsed) {
for (let i = 0; i < this._traitRenderTickers.length; i++) {
this._traitRenderTickers[i](elapsed);
}
}
removeAllTraits() {
const types = this.allTraitTypes();
this.removeTraits(types);
}
removeTrait(type) {
if (!this.is(type)) {
debug(`Tried to remove trait "${type}" when it doesn't exist!`);
return;
}
// Destroy instance.
const instance = this._traits[type];
instance.destroy();
// Remove methods, hooks, and properties.
const methods = instance.methods();
for (const key in methods) {
delete this[key];
}
const hooks = instance.hooks();
for (const key in hooks) {
const implementation = this._hooks[key].find(({type: hookType}) => {
return hookType === type;
});
this._hooks[key].splice(this._hooks[key].indexOf(implementation), 1);
}
const Trait = lookupTrait(type);
const properties = enumerateTraitAccessorKeys(Trait.prototype);
for (let i = 0; i < properties.length; ++i) {
const property = properties[i];
delete this[property];
}
// Remove all event listeners.
const listeners = instance.memoizedListeners();
for (const eventName in listeners) {
this.off(eventName, listeners[eventName]);
}
instance._memoizedListeners = {};
// Remove instance.
delete this._traits[type];
this._traitsFlat.splice(this._traitsFlat.indexOf(instance), 1);
if ('tick' in instance) {
this._traitTickers.splice(
this._traitTickers.indexOf(instance.tick),
1
);
}
if ('renderTick' in instance) {
this._traitRenderTickers.splice(
this._traitRenderTickers.indexOf(instance.renderTick),
1
);
}
const acceptPacketIndex = this._traitsAcceptingPackets.indexOf(instance);
if (-1 !== acceptPacketIndex) {
this._traitsAcceptingPackets.splice(acceptPacketIndex, 1);
}
// Unloop.
instance.entity = undefined;
}
removeTraits(types) {
types.forEach((type) => this.removeTrait(type));
}
tick(elapsed) {
for (let i = 0; i < this._traitTickers.length; i++) {
this._traitTickers[i](elapsed);
}
}
toJSON() {
const json = {};
for (const type in this._traits) {
json[type] = this._traits[type].toJSON();
}
return {
...super.toJSON(),
traits: json,
};
}
}

View File

@ -1,371 +1,4 @@
import D from 'debug'; export {Entity} from './entity.resource';
import without from 'lodash.without';
import {
compose,
EventEmitter,
fastApply,
merge,
mergeDiff,
} from '@avocado/core';
import {Resource} from '@avocado/resource';
import {Synchronized} from '@avocado/state';
import {EntityCreatePacket} from './packets/entity-create.packet';
import {hasTrait, lookupTrait} from './trait/registry';
const debug = D('@avocado:entity:traits');
const blacklistedAccessorKeys = [
'state',
];
// This really seems like a whole lot of complicated nonsense, but it's an
// unfortunate consequence of V8 (and maybe others) not optimizing mutable
// accessors in fast hidden classes.
const traitAccessorForPropertyMap = {};
function traitAccessorForProperty(type, property) {
if (!(type in traitAccessorForPropertyMap)) {
traitAccessorForPropertyMap[type] = {};
}
if (!(property in traitAccessorForPropertyMap[type])) {
traitAccessorForPropertyMap[type][property] = {
get: new Function('', `
return this._traits['${type}']['${property}'];
`),
set: new Function('value', `
this._traits['${type}']['${property}'] = value;
`),
};
}
return traitAccessorForPropertyMap[type][property];
}
function defineTraitAccessors(from, to, instance) {
const type = instance.constructor.type();
do {
Object.getOwnPropertyNames(from).forEach((accessorKey) => {
if (-1 !== blacklistedAccessorKeys.indexOf(accessorKey)) {
return;
}
let descriptor = Object.getOwnPropertyDescriptor(from, accessorKey);
// Make sure it's actually an accessor.
if (!descriptor.get && !descriptor.set) {
return;
}
const accessor = traitAccessorForProperty(type, accessorKey);
if (descriptor.get) {
descriptor.get = accessor.get;
}
if (descriptor.set) {
descriptor.set = accessor.set;
}
Object.defineProperty(to, accessorKey, descriptor);
});
} while (Object.prototype !== (from = Object.getPrototypeOf(from)));
}
function enumerateTraitAccessorKeys(prototype) {
const keys = [];
do {
Object.getOwnPropertyNames(prototype).forEach((accessorKey) => {
if (-1 !== blacklistedAccessorKeys.indexOf(accessorKey)) {
return;
}
let descriptor = Object.getOwnPropertyDescriptor(prototype, accessorKey);
// Make sure it's actually an accessor.
if (!descriptor.get && !descriptor.set) {
return;
}
keys.push(accessorKey);
});
} while (Object.prototype !== (prototype = Object.getPrototypeOf(prototype)));
return keys;
}
const decorate = compose(
EventEmitter,
Synchronized,
);
let numericUid = 1;
export class Entity extends decorate(Resource) {
constructor(json, jsonext) {
super();
this._hooks = {};
this._hydrationPromise = undefined;
this._json = Entity.jsonWithDefaults(json);
this._traits = {};
this._traitsFlat = [];
this._traitTickers = [];
this._traitRenderTickers = [];
this._traitsAcceptingPackets = [];
this.once('destroyed', () => {
this.removeAllTraits();
});
// Bind to prevent lookup overhead.
this.tick = this.tick.bind(this);
// Fast props.
this.numericUid = numericUid++;
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));
}
}
acceptPacket(packet) {
for (let i = 0; i < this._traitsAcceptingPackets.length; i++) {
const instance = this._traitsAcceptingPackets[i];
instance.acceptPacket(packet);
}
}
addTrait(type, json = {}) {
if (this.is(type)) {
debug(`Tried to add trait "${type}" when it already exists!`);
return;
}
if (!hasTrait(type)) {
debug(`Tried to add trait "${type}" which isn't registered!`);
return;
}
const Trait = lookupTrait(type);
// Ensure dependencies.
const dependencies = Trait.dependencies();
const allTypes = this.allTraitTypes();
const lacking = without(dependencies, ...allTypes);
if (lacking.length > 0) {
debug(
`Tried to add trait "${type}" but lack one or more dependents: "${
lacking.join('", "')
}"!`
);
return;
}
// Instantiate.
const {params, state} = json;
const instance = new Trait(this, params, state);
// Proxy properties.
defineTraitAccessors(Trait.prototype, this, instance);
// Attach listeners.
const listeners = instance.memoizedListeners();
for (const eventName in listeners) {
this.on(eventName, listeners[eventName]);
}
// Proxy methods.
const methods = instance.methods();
for (const key in methods) {
this[key] = methods[key];
}
// Register hook listeners.
const hooks = instance.hooks();
for (const key in hooks) {
this._hooks[key] = this._hooks[key] || [];
this._hooks[key].push({
fn: hooks[key],
type: Trait.type(),
});
}
// Track trait.
this._traits[type] = instance;
this._traitsFlat.push(instance);
if ('tick' in instance) {
this._traitTickers.push(instance.tick);
}
if ('renderTick' in instance) {
this._traitRenderTickers.push(instance.renderTick);
}
if ('acceptPacket' in instance) {
this._traitsAcceptingPackets.push(instance);
}
this.emit('traitAdded', type, instance);
}
addTraits(traits) {
for (const type in traits) {
this.addTrait(type, traits[type]);
}
}
allTraitInstances() {
return this._traits;
}
allTraitTypes() {
return Object.keys(this._traits);
}
fromJSON(json) {
super.fromJSON(json);
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);
}
return this._hydrationPromise;
}
invokeHook(hook, ...args) {
const results = {};
if (!(hook in this._hooks)) {
return results;
}
for (const {fn, type} of this._hooks[hook]) {
results[type] = fastApply(null, fn, args);
}
return results;
}
invokeHookFlat(hook, ...args) {
const invokeResults = this.invokeHook(hook, ...args);
const results = [];
for (const type in invokeResults) {
results.push(invokeResults[type]);
}
return results;
}
is(type) {
return type in this._traits;
}
static jsonWithDefaults(json) {
if ('undefined' === typeof json) {
return;
}
const pristine = JSON.parse(JSON.stringify(json));
for (const type in json.traits) {
if (!hasTrait(type)) {
continue;
}
const Trait = lookupTrait(type);
pristine.traits[type] = merge({}, Trait.defaultJSON(), json.traits[type]);
}
return pristine;
}
mergeDiff() {
if (!this.uri) {
return this.toJSON();
}
return mergeDiff(this._json, this.toJSON());
}
packetsForUpdate(force = false) {
const packets = [];
if (force) {
const packet = new EntityCreatePacket(this.mergeDiff(), this);
packets.push(packet);
}
else {
for (let i = 0; i < this._traitsFlat.length; i++) {
const traitPackets = this._traitsFlat[i].packetsForUpdate();
for (let j = 0; j < traitPackets.length; j++) {
packets.push(traitPackets[j]);
}
}
}
return packets;
}
renderTick(elapsed) {
for (let i = 0; i < this._traitRenderTickers.length; i++) {
this._traitRenderTickers[i](elapsed);
}
}
removeAllTraits() {
const types = this.allTraitTypes();
this.removeTraits(types);
}
removeTrait(type) {
if (!this.is(type)) {
debug(`Tried to remove trait "${type}" when it doesn't exist!`);
return;
}
// Destroy instance.
const instance = this._traits[type];
instance.destroy();
// Remove methods, hooks, and properties.
const methods = instance.methods();
for (const key in methods) {
delete this[key];
}
const hooks = instance.hooks();
for (const key in hooks) {
const implementation = this._hooks[key].find(({type: hookType}) => {
return hookType === type;
});
this._hooks[key].splice(this._hooks[key].indexOf(implementation), 1);
}
const Trait = lookupTrait(type);
const properties = enumerateTraitAccessorKeys(Trait.prototype);
for (let i = 0; i < properties.length; ++i) {
const property = properties[i];
delete this[property];
}
// Remove all event listeners.
const listeners = instance.memoizedListeners();
for (const eventName in listeners) {
this.off(eventName, listeners[eventName]);
}
instance._memoizedListeners = {};
// Remove instance.
delete this._traits[type];
this._traitsFlat.splice(this._traitsFlat.indexOf(instance), 1);
if ('tick' in instance) {
this._traitTickers.splice(
this._traitTickers.indexOf(instance.tick),
1
);
}
if ('renderTick' in instance) {
this._traitRenderTickers.splice(
this._traitRenderTickers.indexOf(instance.renderTick),
1
);
}
const acceptPacketIndex = this._traitsAcceptingPackets.indexOf(instance);
if (-1 !== acceptPacketIndex) {
this._traitsAcceptingPackets.splice(acceptPacketIndex, 1);
}
// Unloop.
instance.entity = undefined;
}
removeTraits(types) {
types.forEach((type) => this.removeTrait(type));
}
tick(elapsed) {
for (let i = 0; i < this._traitTickers.length; i++) {
this._traitTickers[i](elapsed);
}
}
toJSON() {
const json = {};
for (const type in this._traits) {
json[type] = this._traits[type].toJSON();
}
return {
...super.toJSON(),
traits: json,
};
}
}
export {EntityCreatePacket} from './packets/entity-create.packet'; export {EntityCreatePacket} from './packets/entity-create.packet';
export {EntityRemovePacket} from './packets/entity-remove.packet'; export {EntityRemovePacket} from './packets/entity-remove.packet';

View File

@ -4,7 +4,7 @@ import {Synchronized} from '@avocado/state';
import {EntityCreatePacket} from '../packets/entity-create.packet'; import {EntityCreatePacket} from '../packets/entity-create.packet';
import {EntityRemovePacket} from '../packets/entity-remove.packet'; import {EntityRemovePacket} from '../packets/entity-remove.packet';
import {Entity} from '../index'; import {Entity} from '../entity.resource';
const decorate = compose( const decorate = compose(
EventEmitter, EventEmitter,

View File

@ -3,7 +3,7 @@ const PIXI = 'undefined' !== typeof window ? require('pixi.js') : undefined;
import {compose} from '@avocado/core'; import {compose} from '@avocado/core';
import {Vector} from '@avocado/math'; import {Vector} from '@avocado/math';
import {Image} from './image'; import {Image} from './image.resource';
const decorate = compose( const decorate = compose(
Vector.Mixin('size', 'width', 'height', { Vector.Mixin('size', 'width', 'height', {

View File

@ -4,7 +4,7 @@ export {Canvas} from './canvas';
export {Color} from './color'; export {Color} from './color';
export {Container} from './container'; export {Container} from './container';
export {hasGraphics} from './has-graphics'; export {hasGraphics} from './has-graphics';
export {Image} from './image'; export {Image} from './image.resource';
export {Primitives} from './primitives'; export {Primitives} from './primitives';
export {Renderable} from './renderable'; export {Renderable} from './renderable';
export {Renderer} from './renderer'; export {Renderer} from './renderer';

View File

@ -5,7 +5,7 @@ import * as THREE from 'three';
import {compose, Property} from '@avocado/core'; import {compose, Property} from '@avocado/core';
import {hasGraphics} from '../has-graphics'; import {hasGraphics} from '../has-graphics';
import {Image} from '../image'; import {Image} from '../image.resource';
import {Renderable} from '../renderable'; import {Renderable} from '../renderable';
import {Sprite} from '../sprite'; import {Sprite} from '../sprite';

View File

@ -2,7 +2,7 @@ import {compose} from '@avocado/core';
import {StateProperty, Trait} from '@avocado/entity'; import {StateProperty, Trait} from '@avocado/entity';
import {Rectangle, Vector} from '@avocado/math'; import {Rectangle, Vector} from '@avocado/math';
import {Image} from '../image'; import {Image} from '../image.resource';
import {Sprite} from '../sprite'; import {Sprite} from '../sprite';
const decorate = compose( const decorate = compose(

View File

@ -0,0 +1,37 @@
export function SynchronizedMixin(Superclass) {
return class Synchronized extends Superclass {
constructor() {
super();
this._idempotentPackets = [];
}
cleanPackets() {
this._idempotentPackets = [];
}
packetsAreIdempotent() {
return true;
}
packets(informed) {
if (this._idempotentPackets.length > 0) {
return this._idempotentPackets;
}
let packets = this.packetsForTick(informed);
if (!packets) {
return [];
}
packets = Array.isArray(packets) ? packets : [packets];
if (this.packetsAreIdempotent()) {
this._idempotentPackets = packets;
}
return packets;
}
packetsForTick(informed) {}
}
}

View File

@ -1,2 +1,10 @@
export {ResourceRegistry, globalRegistry} from './registry'; export {
allResources,
idFromResource,
resourceFromId,
registerResource,
} from './registry';
export {Resource} from './resource'; export {Resource} from './resource';
export {ResourceRegistry, globalRegistry} from './store';

View File

@ -0,0 +1,13 @@
import {Packet} from '@avocado/net';
export class ResourcePacket extends Packet {
static get schema() {
return {
...super.schema,
resourceType: 'string',
uuid: 'string',
};
}
}

View File

@ -0,0 +1,21 @@
let resourceId = 0;
const resourceToIdMap = new Map();
const idToResourceMap = new Map();
export function allResources() {
return Array.from(idToResourceMap.values());
}
export function idFromResource(Resource) {
return resourceToIdMap.get(Resource);
}
export function resourceFromId(id) {
return idToResourceMap.get(id);
}
export function registerResource(Resource) {
if (resourceToIdMap.has(Resource)) {
return;
}
const id = resourceId++;
resourceToIdMap.set(Resource, id);
idToResourceMap.set(id, Resource);
return id;
}

View File

@ -1,106 +1 @@
import {Howl, Howler} from 'howler'; export {Sound} from './sound.resource';
import {Resource} from '@avocado/resource';
export class Sound extends Resource {
static load(uri) {
if (!this.loadCache) {
this.loadCache = {};
}
if (!this.refCache) {
this.refCache = {};
}
if (!(uri in this.refCache)) {
this.refCache[uri] = 0;
}
this.refCache[uri] += 1;
if (this.loadCache[uri]) {
return this.loadCache[uri];
}
return this.loadCache[uri] = this.read(uri).then((json) => {
const instance = new Sound(json);
let lastTime = performance.now();
instance.tickHandle = setInterval(() => {
const now = performance.now();
const elapsed = (now - lastTime) / 1000;
lastTime = now;
instance.tick(elapsed);
}, 20);
return new Promise((resolve) => {
instance.sound.once('load', () => {
resolve(instance);
});
});
});
}
constructor(json) {
super();
this.interval = 0;
this.lastPlayHandle = undefined;
this.locked = false;
this.originalVolume = 1;
this.remaining = 0;
this.sound = undefined;
this.tickHandle = undefined;
if ('undefined' !== typeof json) {
this.fromJSON(json);
}
}
destroy() {
clearInterval(instance.tickHandle);
if (this.sound) {
this.sound.unload();
}
}
fromJSON(json) {
super.fromJSON(json);
if (json.interval) {
this.interval = json.interval;
}
if (json.volume) {
this.originalVolume = json.volume;
}
this.sound = new Howl(json);
return this;
}
play() {
if (this.locked) {
const volume = this.sound.volume(this.lastPlayHandle);
this.sound.volume(
Math.min(1, Math.min(volume, this.originalVolume * 20)),
this.lastPlayHandle
);
return;
}
this.lastPlayHandle = this.sound.play();
if (this.interval > 0) {
this.locked = true;
this.remaining = this.interval;
}
}
release() {
const ctor = this.constructor;
const uri = this.uri;
ctor.refCache[uri] -= 1;
if (0 === ctor.refCache[uri]) {
delete ctor.loadCache[uri];
this.destroy();
}
}
tick(elapsed) {
if (this.locked && this.interval > 0) {
this.remaining -= elapsed;
if (this.remaining <= 0) {
this.locked = false;
}
}
}
}

View File

@ -0,0 +1,106 @@
import {Howl, Howler} from 'howler';
import {Resource} from '@avocado/resource';
export class Sound extends Resource {
static load(uri) {
if (!this.loadCache) {
this.loadCache = {};
}
if (!this.refCache) {
this.refCache = {};
}
if (!(uri in this.refCache)) {
this.refCache[uri] = 0;
}
this.refCache[uri] += 1;
if (this.loadCache[uri]) {
return this.loadCache[uri];
}
return this.loadCache[uri] = this.read(uri).then((json) => {
const instance = new Sound(json);
let lastTime = performance.now();
instance.tickHandle = setInterval(() => {
const now = performance.now();
const elapsed = (now - lastTime) / 1000;
lastTime = now;
instance.tick(elapsed);
}, 20);
return new Promise((resolve) => {
instance.sound.once('load', () => {
resolve(instance);
});
});
});
}
constructor(json) {
super();
this.interval = 0;
this.lastPlayHandle = undefined;
this.locked = false;
this.originalVolume = 1;
this.remaining = 0;
this.sound = undefined;
this.tickHandle = undefined;
if ('undefined' !== typeof json) {
this.fromJSON(json);
}
}
destroy() {
clearInterval(instance.tickHandle);
if (this.sound) {
this.sound.unload();
}
}
fromJSON(json) {
super.fromJSON(json);
if (json.interval) {
this.interval = json.interval;
}
if (json.volume) {
this.originalVolume = json.volume;
}
this.sound = new Howl(json);
return this;
}
play() {
if (this.locked) {
const volume = this.sound.volume(this.lastPlayHandle);
this.sound.volume(
Math.min(1, Math.min(volume, this.originalVolume * 20)),
this.lastPlayHandle
);
return;
}
this.lastPlayHandle = this.sound.play();
if (this.interval > 0) {
this.locked = true;
this.remaining = this.interval;
}
}
release() {
const ctor = this.constructor;
const uri = this.uri;
ctor.refCache[uri] -= 1;
if (0 === ctor.refCache[uri]) {
delete ctor.loadCache[uri];
this.destroy();
}
}
tick(elapsed) {
if (this.locked && this.interval > 0) {
this.remaining -= elapsed;
if (this.remaining <= 0) {
this.locked = false;
}
}
}
}

View File

@ -1,4 +1,4 @@
export {Animation} from './animation'; export {Animation} from './animation.resource';
export {AnimationView} from './animation-view'; export {AnimationView} from './animation-view';
export { export {
cancelAnimationFrame, cancelAnimationFrame,

View File

@ -2,7 +2,7 @@ import {compose} from '@avocado/core';
import {StateProperty, Trait} from '@avocado/entity'; import {StateProperty, Trait} from '@avocado/entity';
import {Rectangle, Vector} from '@avocado/math'; import {Rectangle, Vector} from '@avocado/math';
import {Animation} from '../animation'; import {Animation} from '../animation.resource';
import {AnimationView} from '../animation-view'; import {AnimationView} from '../animation-view';
import {TraitAnimatedPacket} from '../packets/trait-animated.packet'; import {TraitAnimatedPacket} from '../packets/trait-animated.packet';

View File

@ -1,8 +1,8 @@
export {Camera} from './camera'; export {Camera} from './camera';
export {Layer} from './layer'; export {Layer} from './layer';
export {LayerView} from './layer-view'; export {LayerView} from './layer-view';
export {Room} from './room'; export {Room} from './room.resource';
export {RoomView} from './room-view'; export {RoomView} from './room-view';
export {TilesRenderer} from './tiles-renderer'; export {TilesRenderer} from './tiles-renderer';
export {Tiles} from './tiles'; export {Tiles} from './tiles';
export {Tileset} from './tileset'; export {Tileset} from './tileset.resource';

View File

@ -5,7 +5,7 @@ import {ShapeList} from '@avocado/physics';
import {Synchronized} from '@avocado/state'; import {Synchronized} from '@avocado/state';
import {Tiles} from './tiles'; import {Tiles} from './tiles';
import {Tileset} from './tileset'; import {Tileset} from './tileset.resource';
import {LayerCreatePacket} from './packets/layer-create.packet'; import {LayerCreatePacket} from './packets/layer-create.packet';
import {TileUpdatePacket} from './packets/tile-update.packet'; import {TileUpdatePacket} from './packets/tile-update.packet';