refactor: immutablen't

This commit is contained in:
cha0s 2019-05-13 21:07:51 -05:00
parent 2ccd527884
commit 3631a7f669
27 changed files with 467 additions and 528 deletions

11
TODO.md
View File

@ -20,13 +20,20 @@
- ❌ entityList.fromJSON() - ❌ entityList.fromJSON()
- ❌ Socket WebWorker can't connect in Firefox - ❌ Socket WebWorker can't connect in Firefox
- ✔ Don't run emitter destruction tickers on server - ✔ Don't run emitter destruction tickers on server
- Investigate unrolling equalsClose - Investigate unrolling equalsClose
- ✔ Bitshifts for on_positionChanged x/y boxing - ✔ Bitshifts for on_positionChanged x/y boxing
- ✔ Memoize Object.getOwnPropertyNames results per trait constructor - ✔ Memoize Object.getOwnPropertyNames results per trait constructor
- ✔ EE optimizations (lookupEmitListeners) - ✔ EE optimizations (lookupEmitListeners)
- ✔ Core.fastApply, search for /\(.../ - ✔ Core.fastApply, search for /\(\.\.\./
- ✔ Rename visibleBoundingBox(es)? to visibleAabb(s)? - ✔ Rename visibleBoundingBox(es)? to visibleAabb(s)?
- ❌ Property.fastAccess to skip getter, this.entity.currentAnimation - ❌ Property.fastAccess to skip getter, this.entity.currentAnimation
- ✔ Trait::isDirty should be flat - ✔ Trait::isDirty should be flat
- ✔ Trait params fromJS is super slow - ✔ Trait params fromJS is super slow
- ✔ Entity::is is slow? - ✔ Entity::is is slow?
- ❌ Manual state sync
- ✔ Synchronized provides packet updates
- ✔ Phase out EntityPacketSynchronizer
- ❌ Implement ALL trait state update packets
- ❌ Implement Entity remember packets
- ✔ Implement Entity remove packets
- ❌ Save state/param extensions separately on create instead of only merging

View File

@ -35,7 +35,7 @@ export class Behaved extends decorate(Trait) {
const routinesJSON = this.params.routines; const routinesJSON = this.params.routines;
this._routines = (new Routines()).fromJSON(routinesJSON); this._routines = (new Routines()).fromJSON(routinesJSON);
this._routines.context = this._context; this._routines.context = this._context;
this.updateCurrentRoutine(this.state.get('currentRoutine')); this.updateCurrentRoutine(this.state.currentRoutine);
} }
destroy() { destroy() {

View File

@ -1,25 +0,0 @@
import {Packet} from '@avocado/net';
export class EntityPacket extends Packet {
static get schema() {
return {
...super.schema,
data: [{
uuid: 'string',
}],
};
}
bundleWith(other) {
this.data.push(other.data[0]);
}
forEachData(fn) {
for (let i = 0; i < this.data.length; i++) {
const data = this.data[i];
fn(data);
}
}
}

View File

@ -6,6 +6,7 @@ import {compose, EventEmitter, fastApply} from '@avocado/core';
import {Resource} from '@avocado/resource'; import {Resource} from '@avocado/resource';
import {Synchronized} from '@avocado/state'; import {Synchronized} from '@avocado/state';
import {EntityCreatePacket} from './packets/entity-create.packet';
import {hasTrait, lookupTrait} from './trait/registry'; import {hasTrait, lookupTrait} from './trait/registry';
const debug = D('@avocado:entity:traits'); const debug = D('@avocado:entity:traits');
@ -86,7 +87,6 @@ export class Entity extends decorate(Resource) {
super(); super();
this._hooks = {}; this._hooks = {};
this._traits = {}; this._traits = {};
this._traitsTypesDirty = [];
this._traitsFlat = []; this._traitsFlat = [];
this._traitTickers = []; this._traitTickers = [];
this._traitRenderTickers = []; this._traitRenderTickers = [];
@ -158,11 +158,6 @@ export class Entity extends decorate(Resource) {
type: Trait.type(), type: Trait.type(),
}); });
} }
// Add state.
this.state = this.state.set(type, I.Map({
params: instance.params,
state: instance.state,
}))
// Track trait. // Track trait.
this._traits[type] = instance; this._traits[type] = instance;
this._traitsFlat.push(instance); this._traitsFlat.push(instance);
@ -201,8 +196,7 @@ export class Entity extends decorate(Resource) {
hydrate() { hydrate() {
const promises = []; const promises = [];
for (let i = 0; i < this._traitsFlat.length; i++) { for (let i = 0; i < this._traitsFlat.length; i++) {
const instance = this._traitsFlat[i]; promises.push(this._traitsFlat[i].hydrate());
promises.push(instance.hydrate());
} }
return Promise.all(promises); return Promise.all(promises);
} }
@ -231,26 +225,21 @@ export class Entity extends decorate(Resource) {
return type in this._traits; return type in this._traits;
} }
patchStateStep(type, step) { packetsForUpdate(force = false) {
let instance = this._traits[type]; const packets = [];
// New trait requested? if (force) {
if (!this._traits[type]) { const packet = new EntityCreatePacket(this.toJSON(), this);
// Doesn't exist? packets.push(packet);
if (!hasTrait(type)) {
return;
}
if ('params' in step.value) {
step.value.params = step.value.params.toJS();
}
this.addTrait(type, step.value);
instance = this._traits[type];
this.state = this.state.setIn([type, 'params'], instance.params);
} }
else { else {
// Accept state. for (let i = 0; i < this._traitsFlat.length; i++) {
instance.patchState([step]); const traitPackets = this._traitsFlat[i].packetsForUpdate();
for (let j = 0; j < traitPackets.length; j++) {
packets.push(traitPackets[j]);
}
}
} }
this.state = this.state.setIn([type, 'state'], instance.state); return packets;
} }
renderTick(elapsed) { renderTick(elapsed) {
@ -296,8 +285,6 @@ export class Entity extends decorate(Resource) {
this.off(eventName, listeners[eventName]); this.off(eventName, listeners[eventName]);
} }
instance._memoizedListeners = {}; instance._memoizedListeners = {};
// Remove state.
this.state = this.state.delete(type);
// Remove instance. // Remove instance.
delete this._traits[type]; delete this._traits[type];
this._traitsFlat.splice(this._traitsFlat.indexOf(instance), 1); this._traitsFlat.splice(this._traitsFlat.indexOf(instance), 1);
@ -325,34 +312,10 @@ export class Entity extends decorate(Resource) {
types.forEach((type) => this.removeTrait(type)); types.forEach((type) => this.removeTrait(type));
} }
setTraitDirty(type) {
this._traitsTypesDirty.push(type);
if (this.is('listed')) {
this.list && this.list.markEntityDirty(this);
}
}
tick(elapsed) { tick(elapsed) {
for (let i = 0; i < this._traitTickers.length; i++) { for (let i = 0; i < this._traitTickers.length; i++) {
this._traitTickers[i](elapsed); this._traitTickers[i](elapsed);
} }
if (AVOCADO_SERVER) {
this.tickMutateState();
}
}
tickMutateState() {
if (0 === this._traitsTypesDirty.length) {
return;
}
this.state = this.state.withMutations((state) => {
for (let i = 0; i < this._traitsTypesDirty.length; i++) {
const type = this._traitsTypesDirty[i];
const instance = this._traits[type];
state.setIn([type, 'state'], instance.state);
}
});
this._traitsTypesDirty = [];
} }
toJSON() { toJSON() {
@ -368,12 +331,12 @@ export class Entity extends decorate(Resource) {
} }
export {EntityPacket} from './entity.packet'; export {EntityCreatePacket} from './packets/entity-create.packet';
export {EntityRemovePacket} from './packets/entity-remove.packet';
export {EntityPacket} from './packets/entity.packet';
export {EntityList, EntityListView} from './list'; export {EntityList, EntityListView} from './list';
export {EntityPacketSynchronizer} from './packet-synchronizer';
export { export {
hasTrait, hasTrait,
lookupTrait, lookupTrait,

View File

@ -1,10 +1,9 @@
import * as I from 'immutable';
import mapValues from 'lodash.mapvalues';
import {compose, EventEmitter} from '@avocado/core'; import {compose, EventEmitter} from '@avocado/core';
import {QuadTree, Rectangle, Vector} from '@avocado/math'; import {QuadTree, Rectangle, Vector} from '@avocado/math';
import {Synchronized} from '@avocado/state'; import {Synchronized} from '@avocado/state';
import {EntityCreatePacket} from '../packets/entity-create.packet';
import {EntityRemovePacket} from '../packets/entity-remove.packet';
import {Entity} from '../index'; import {Entity} from '../index';
const decorate = compose( const decorate = compose(
@ -17,8 +16,9 @@ export class EntityList extends decorate(class {}) {
constructor() { constructor() {
super(); super();
this._afterDestructionTickers = []; this._afterDestructionTickers = [];
this._dirtyEntities = [];
this._entities = {}; this._entities = {};
this._entitiesJustAdded = [];
this._entitiesJustRemoved = [];
this._entityTickers = [] this._entityTickers = []
this._flatEntities = []; this._flatEntities = [];
this._quadTree = new QuadTree(); this._quadTree = new QuadTree();
@ -31,12 +31,22 @@ export class EntityList extends decorate(class {}) {
} }
} }
acceptPacket(packet) {
if (packet instanceof EntityCreatePacket) {
const entity = new Entity(packet.data);
entity.instanceUuid = packet.data.uuid;
this.addEntity(entity);
}
}
addEntity(entity) { addEntity(entity) {
const uuid = entity.instanceUuid; const uuid = entity.instanceUuid;
this._entities[uuid] = entity; this._entities[uuid] = entity;
this._flatEntities.push(entity); this._flatEntities.push(entity);
this._entityTickers.push(entity.tick); this._entityTickers.push(entity.tick);
this.state = this.state.set(uuid, entity.state); if (AVOCADO_SERVER) {
this._entitiesJustAdded.push(entity);
}
entity.setIntoList(this); entity.setIntoList(this);
entity.once('destroy', () => { entity.once('destroy', () => {
this.removeEntity(entity); this.removeEntity(entity);
@ -52,8 +62,8 @@ export class EntityList extends decorate(class {}) {
} }
destroy() { destroy() {
for (const entity of this) { for (let i = 0; i < this._flatEntities.length; i++) {
entity.destroy(); this._flatEntities[i].destroy();
} }
} }
@ -63,40 +73,27 @@ export class EntityList extends decorate(class {}) {
} }
} }
markEntityDirty(entity) { packetsForUpdate(force = false) {
if (AVOCADO_CLIENT) { const packets = [];
return; if (!force) {
} for (let i = 0; i < this._entitiesJustAdded.length; i++) {
if (-1 === this._dirtyEntities.indexOf(entity)) { const entity = this._entitiesJustAdded[i];
this._dirtyEntities.push(entity); packets.push(new EntityCreatePacket(entity.toJSON(), entity));
}
}
patchStateStep(uuid, step) {
const entity = this._entities[uuid];
if ('/' === step.path) {
switch (step.op) {
case 'add':
// New entity. Create with patch as traits.
const newEntity = new Entity({
traits: step.value,
});
newEntity.instanceUuid = uuid;
this.addEntity(newEntity);
break;
case 'remove':
// Maybe already destroyed on the client?
if (entity && entity.is('existent')) {
entity.destroy();
}
break;
} }
return; this._entitiesJustAdded = [];
for (let i = 0; i < this._entitiesJustRemoved.length; i++) {
const entity = this._entitiesJustRemoved[i];
packets.push(new EntityRemovePacket({}, entity));
}
this._entitiesJustRemoved = [];
} }
if ('replace' === step.op && entity) { for (let i = 0; i < this._flatEntities.length; i++) {
// Exists; patch. const entityPackets = this._flatEntities[i].packetsForUpdate(force);
entity.patchState([step]); for (let j = 0; j < entityPackets.length; j++) {
packets.push(entityPackets[j]);
}
} }
return packets;
} }
get quadTree() { get quadTree() {
@ -108,10 +105,12 @@ export class EntityList extends decorate(class {}) {
if (!(uuid in this._entities)) { if (!(uuid in this._entities)) {
return; return;
} }
if (AVOCADO_SERVER) {
this._entitiesJustRemoved.push(entity);
}
delete this._entities[uuid]; delete this._entities[uuid];
this._flatEntities.splice(this._flatEntities.indexOf(entity), 1); this._flatEntities.splice(this._flatEntities.indexOf(entity), 1);
this._entityTickers.splice(this._entityTickers.indexOf(entity.tick), 1); this._entityTickers.splice(this._entityTickers.indexOf(entity.tick), 1);
this.state = this.state.delete(uuid);
this.emit('entityRemoved', entity); this.emit('entityRemoved', entity);
} }
@ -122,10 +121,6 @@ export class EntityList extends decorate(class {}) {
} }
// Run normal tickers. // Run normal tickers.
this.tickEntities(elapsed) this.tickEntities(elapsed)
// Update state.
if (AVOCADO_SERVER) {
this.tickMutateState();
}
} }
tickAfterDestructionTickers(elapsed) { tickAfterDestructionTickers(elapsed) {
@ -149,19 +144,6 @@ export class EntityList extends decorate(class {}) {
} }
} }
tickMutateState(state) {
if (0 === this._dirtyEntities.length) {
return;
}
this.state = this.state.withMutations((state) => {
for (let i = 0; i < this._dirtyEntities.length; i++) {
const entity = this._dirtyEntities[i];
state.set(entity.$$avocado_property_instanceUuid, entity.state);
}
});
this._dirtyEntities = [];
}
visibleEntities(query) { visibleEntities(query) {
const entities = []; const entities = [];
const entitiesChecked = []; const entitiesChecked = [];

View File

@ -1,79 +0,0 @@
import {EntityPacket} from '@avocado/entity';
export class EntityPacketSynchronizer {
constructor() {
this.packetsToSynchronize = new Map();
this.trackedEntities = new Map();
}
acceptPacket(packet) {
if (!(packet instanceof EntityPacket)) {
return;
}
for (let i = 0; i < packet.data.length; i++) {
const data = packet.data[i];
const packetEntity = this.trackedEntities.get(data.uuid);
if (!packetEntity) {
continue;
}
const Packet = packet.constructor;
packetEntity.acceptPacket(new Packet([data]));
}
}
flushPackets(flusher) {
const flushed = new Map();
const it = this.packetsToSynchronize.entries();
for (let value = it.next(); !value.done; value = it.next()) {
const entity = value.value[0];
const packets = value.value[1];
const mergedPackets = this._mergePackets(packets);
flushed.set(entity, mergedPackets);
}
this.packetsToSynchronize.clear();
return flushed;
}
_mergePackets(packets) {
const mergedPackets = new Map();
for (let i = 0; i < packets.length; i++) {
const packet = packets[i];
const Packet = packet.constructor;
if (!mergedPackets.has(Packet)) {
mergedPackets.set(Packet, packet);
}
else {
mergedPackets.get(Packet).mergeWith(packet);
}
}
return mergedPackets;
}
_queuePacketFor(entity, packet) {
if (!this.packetsToSynchronize.has(entity)) {
this.packetsToSynchronize.set(entity, []);
}
this.packetsToSynchronize.get(entity).push(packet);
}
trackEntity(entity) {
this.trackedEntities.set(entity.instanceUuid, entity);
entity.once('destroy', () => {
this.trackedEntities.delete(entity.instanceUuid);
});
this._watchEntityPackets(entity);
}
_watchEntityPackets(entity) {
const onSendPacket = (packet) => {
this._queuePacketFor(entity, packet);
};
entity.on('sendPacket', onSendPacket);
entity.once('destroy', () => {
this.packetsToSynchronize.delete(entity);
entity.off('sendPacket', onSendPacket);
});
}
}

View File

@ -0,0 +1,35 @@
import msgpack from 'msgpack-lite';
import {EntityPacket} from './entity.packet';
export class EntityCreatePacket extends EntityPacket {
constructor(data, entity) {
if ('undefined' !== typeof entity) {
data.uuid = entity.instanceUuid;
data.layer = entity.layer.index;
}
super(data);
this.entity = entity;
}
static pack(packet) {
return this.builder.encode({
_id: packet.data[0],
data: msgpack.encode(packet.data[1]),
})
}
static get schema() {
return {
...super.schema,
data: 'buffer',
};
}
static unpack(packet) {
const {data} = this.builder.decode(packet);
return msgpack.decode(data);
}
}

View File

@ -0,0 +1,3 @@
import {EntityPacket} from './entity.packet';
export class EntityRemovePacket extends EntityPacket {}

View File

@ -0,0 +1,22 @@
import {Packet} from '@avocado/net';
export class EntityPacket extends Packet {
constructor(data, entity) {
if ('undefined' !== typeof entity) {
data.uuid = entity.instanceUuid;
}
super(data);
this.entity = entity;
}
static get schema() {
return {
...super.schema,
data: {
uuid: 'string',
},
};
}
}

View File

@ -0,0 +1,11 @@
import {EntityPacket} from './entity.packet';
export class TraitDirectionalPacket extends EntityPacket {
static get schema() {
const schema = super.schema;
schema.data.direction = 'uint8';
return schema;
}
}

View File

@ -0,0 +1,11 @@
import {EntityPacket} from './entity.packet';
export class TraitPositionedPacket extends EntityPacket {
static get schema() {
const schema = super.schema;
schema.data.position = 'uint32';
return schema;
}
}

View File

@ -1,4 +1,3 @@
import * as I from 'immutable';
import merge from 'lodash.merge'; import merge from 'lodash.merge';
import {compose, Property} from '@avocado/core'; import {compose, Property} from '@avocado/core';
@ -18,7 +17,8 @@ export class Trait extends decorate(class {}) {
const ctor = this.constructor; const ctor = this.constructor;
this._memoizedListeners = undefined; this._memoizedListeners = undefined;
this.params = Object.assign({}, ctor.defaultParams(), params); this.params = Object.assign({}, ctor.defaultParams(), params);
this.state = I.fromJS(ctor.defaultState()).merge(I.fromJS(state)); this.state = Object.assign({}, ctor.defaultState(), state);
this.previousState = JSON.parse(JSON.stringify(this.state));
if (this.tick) { if (this.tick) {
this.tick = this.tick.bind(this); this.tick = this.tick.bind(this);
} }
@ -56,24 +56,10 @@ export class Trait extends decorate(class {}) {
return {}; return {};
} }
patchStateStep(key, step) {
if ('state' !== key) {
return;
}
const stateKey = step.path.substr(1);
const value = this.transformPatchValue(stateKey, step.value);
if (stateKey in this.entity) {
this.entity[stateKey] = value;
}
else {
this.state = this.state.set(stateKey, value);
}
}
toJSON() { toJSON() {
return { return {
params: this.params, params: this.params,
state: this.state.toJS(), state: this.state,
}; };
} }
@ -112,15 +98,14 @@ export function StateProperty(key, meta = {}) {
this.entity.emit(...args); this.entity.emit(...args);
}; };
meta.initialize = meta.initialize || function() { meta.initialize = meta.initialize || function() {
this[transformedProperty] = this.state.get(key); this[transformedProperty] = this.state[key];
} }
meta.get = meta.get || new Function(` meta.get = meta.get || new Function(`
return this.${transformedProperty}; return this.${transformedProperty};
`); `);
meta.set = meta.set || new Function('value', ` meta.set = meta.set || new Function('value', `
this.entity.setTraitDirty(this.constructor.type());
this.${transformedProperty} = value; this.${transformedProperty} = value;
this.state = this.state.set('${key}', value); this.state['${key}'] = value;
`); `);
return Property(key, meta)(Superclass); return Property(key, meta)(Superclass);
} }

View File

@ -2,6 +2,7 @@ import {compose} from '@avocado/core';
import {Vector} from '@avocado/math'; import {Vector} from '@avocado/math';
import {StateProperty, Trait} from '../trait'; import {StateProperty, Trait} from '../trait';
import {TraitDirectionalPacket} from '../packets/trait-directional.packet';
const decorate = compose( const decorate = compose(
StateProperty('direction', { StateProperty('direction', {
@ -33,6 +34,23 @@ export class Directional extends decorate(Trait) {
this.directionCount = this.params.directionCount; this.directionCount = this.params.directionCount;
} }
acceptPacket(packet) {
if (packet instanceof TraitDirectionalPacket) {
this.entity.direction = packet.data.direction;
}
}
packetsForUpdate() {
const packets = [];
if (this.state.direction !== this.previousState.direction) {
packets.push(new TraitDirectionalPacket({
direction: this.state.direction,
}, this.entity));
this.previousState.direction = this.state.direction;
}
return packets;
}
listeners() { listeners() {
const listeners = {}; const listeners = {};
if (this.params.trackMovement) { if (this.params.trackMovement) {

View File

@ -2,6 +2,7 @@ import {compose, EventEmitter} from '@avocado/core';
import {Vector} from '@avocado/math'; import {Vector} from '@avocado/math';
import {Trait} from '../trait'; import {Trait} from '../trait';
import {TraitPositionedPacket} from '../packets/trait-positioned.packet';
const decorate = compose( const decorate = compose(
EventEmitter, EventEmitter,
@ -30,8 +31,8 @@ export class Positioned extends decorate(Trait) {
constructor(entity, params, state) { constructor(entity, params, state) {
super(entity, params, state); super(entity, params, state);
this.on('_positionChanged', this.on_positionChanged, this); this.on('_positionChanged', this.on_positionChanged, this);
const x = this.state.get('x') >> 2; const x = this.state.x >> 2;
const y = this.state.get('y') >> 2; const y = this.state.y >> 2;
this._position = [x, y]; this._position = [x, y];
this.entity.position[0] = x; this.entity.position[0] = x;
this.entity.position[1] = y; this.entity.position[1] = y;
@ -50,16 +51,19 @@ export class Positioned extends decorate(Trait) {
} }
} }
acceptPacket(packet) {
if (packet instanceof TraitPositionedPacket) {
this.serverX = (packet.data.position & 0xFFFF) >> 2;
this.serverY = (packet.data.position >> 16) >> 2;
}
}
on_positionChanged(oldPosition, newPosition) { on_positionChanged(oldPosition, newPosition) {
this.entity.position[0] = newPosition[0]; this.entity.position[0] = newPosition[0];
this.entity.position[1] = newPosition[1]; this.entity.position[1] = newPosition[1];
if (AVOCADO_SERVER) { if (AVOCADO_SERVER) {
const x = newPosition[0] << 2; this.state.x = newPosition[0] << 2;
const y = newPosition[1] << 2; this.state.y = newPosition[1] << 2;
this.state = this.state.withMutations((state) => {
state.set('x', x).set('y', y);
});
this.entity.setTraitDirty('positioned');
} }
this.entity.emit('positionChanged', oldPosition, newPosition); this.entity.emit('positionChanged', oldPosition, newPosition);
} }
@ -68,36 +72,26 @@ export class Positioned extends decorate(Trait) {
this.serverPositionDirty = true; this.serverPositionDirty = true;
} }
patchStateStep(key, step) { packetsForUpdate() {
if ('state' !== key) { const packets = [];
return; if (
} this.state.x !== this.previousState.x
const stateKey = step.path.substr(1); || this.state.y !== this.previousState.y
const value = this.transformPatchValue(stateKey, step.value); ) {
switch (stateKey) { const packed = ((this.state.y) << 16) | (this.state.x << 0);
case 'x': packets.push(new TraitPositionedPacket({
this.serverX = value; position: packed,
break; }, this.entity));
case 'y': this.previousState.x = this.state.x;
this.serverY = value; this.previousState.y = this.state.y;
break;
default:
super.patchStateStep(key, step);
break;
} }
return packets;
} }
set relaxServerPositionConstraintIfNearerThan(nearerThan) { set relaxServerPositionConstraintIfNearerThan(nearerThan) {
this._relaxServerPositionConstraintIfNearerThan = nearerThan; this._relaxServerPositionConstraintIfNearerThan = nearerThan;
} }
transformPatchValue(key, value) {
if (-1 !== ['x', 'y'].indexOf(key)) {
return value / 4;
}
super.transformPatchValue(key, value);
}
listeners() { listeners() {
return { return {

View File

@ -54,14 +54,14 @@ export class Visible extends decorate(Trait) {
if (filter) { if (filter) {
this._container.setFilter(filter); this._container.setFilter(filter);
} }
this._container.isVisible = this.state.get('isVisible'); this._container.isVisible = this.state.isVisible;
} }
this._rawVisibleAabb = [0, 0, 0, 0]; this._rawVisibleAabb = [0, 0, 0, 0];
this.scheduledBoundingBoxUpdate = true; this.scheduledBoundingBoxUpdate = true;
this.trackPosition = this.params.trackPosition; this.trackPosition = this.params.trackPosition;
const scale = this.state.get('visibleScale'); const scale = this.state.visibleScale;
this._visibleScale = [scale.get(0), scale.get(1)]; this._visibleScale = [scale[0], scale[1]];
this.onZIndexChanged(this.state.get('zIndex')); this.onZIndexChanged(this.state.zIndex);
} }
destroy() { destroy() {
@ -88,7 +88,7 @@ export class Visible extends decorate(Trait) {
} }
set rawVisibleScale(scale) { set rawVisibleScale(scale) {
this.entity.visibleScale = I.List(scale); this.entity.visibleScale = scale;
} }
synchronizePosition() { synchronizePosition() {
@ -107,7 +107,7 @@ export class Visible extends decorate(Trait) {
} }
get visibleScaleX() { get visibleScaleX() {
return this.entity.visibleScale.get(0); return this.state.visibleScale[0];
} }
set visibleScaleX(x) { set visibleScaleX(x) {
@ -115,7 +115,7 @@ export class Visible extends decorate(Trait) {
} }
get visibleScaleY() { get visibleScaleY() {
return this.entity.visibleScale.get(1); return this.state.visibleScale[1];
} }
set visibleScaleY(y) { set visibleScaleY(y) {
@ -155,7 +155,7 @@ export class Visible extends decorate(Trait) {
visibleScaleChanged: () => { visibleScaleChanged: () => {
const scale = this.entity.visibleScale; const scale = this.entity.visibleScale;
this._visibleScale = [scale.get(0), scale.get(1)]; this._visibleScale = [scale[0], scale[1]];
if (this._container) { if (this._container) {
this._container.scale = this._visibleScale; this._container.scale = this._visibleScale;
} }

View File

@ -11,105 +11,11 @@ const decorate = compose(
export function Synchronized(Superclass) { export function Synchronized(Superclass) {
return class Synchronized extends decorate(Superclass) { return class Synchronized extends decorate(Superclass) {
constructor(...args) { acceptPacket(packet) {}
super(...args);
this.state = I.Map();
this._childrenNeedInitialization = true;
this._childrenWithState = []
this._childrenWithoutState = [];
this._childrenTickers = [];
}
ensureSynchronizedChildren() { packetsForUpdate(force = false) {
if (!this._childrenNeedInitialization) {
return;
}
this._childrenNeedInitialization = false;
const synchronizedChildren = this.synchronizedChildren();
for (let i = 0; i < synchronizedChildren.length; ++i) {
const key = synchronizedChildren[i];
if (
'undefined' !== typeof this[key]
&& 'undefined' !== typeof this[key].tick
) {
this._childrenTickers.push(this[key].tick.bind(this[key]));
}
if (
'undefined' !== typeof this[key]
&& 'undefined' !== typeof this[key].state
) {
this._childrenWithState.push(key);
}
else {
this._childrenWithoutState.push(key);
}
}
}
patchState(patch) {
for (const step of patch) {
const {op, path, value} = step;
if ('/' === path) {
for (const key in value) {
this.patchStateStep(key, {
op,
path,
value: value[key],
});
}
}
else {
const [key, substep] = nextStep(step);
this.patchStateStep(key, {
op: substep.op,
path: substep.path,
value: substep.value,
});
}
}
}
patchStateStep(key, step) {
if (!(key in this)) {
return;
}
if (
'undefined' !== typeof this[key]
&& 'undefined' !== typeof this[key].patchState
) {
this[key].patchState([step]);
}
else {
this[key] = step.value;
}
}
synchronizedChildren() {
return []; return [];
} }
tickSynchronized(elapsed) {
this.ensureSynchronizedChildren();
for (let i = 0; i < this._childrenTickers.length; ++i) {
this._childrenTickers[i](elapsed);
}
if (AVOCADO_SERVER) {
this.tickMutateState();
}
}
tickMutateState() {
this.state = this.state.withMutations((state) => {
for (let i = 0; i < this._childrenWithState.length; ++i) {
const key = this._childrenWithState[i];
state.set(key, this[key].state);
}
for (let i = 0; i < this._childrenWithoutState.length; ++i) {
const key = this._childrenWithoutState[i];
state.set(key, this[key]);
}
});
}
} }
} }

View File

@ -4,55 +4,25 @@ import {nextStep} from './next-step';
export class Synchronizer { export class Synchronizer {
constructor(statefuls) { constructor(children) {
this._state = I.Map(); this.children = children;
this._statefuls = statefuls;
this.updateState();
} }
patchState(patch) { acceptPacket(packet) {
const stepMap = {}; for (let i = 0; i < this.children.length; i++) {
for (const step of patch) { this.children[i].acceptPacket(packet);
const [key, substep] = nextStep(step) }
if (!stepMap[key]) { }
stepMap[key] = [];
packetsForUpdate(force = false) {
const packetsForUpdate = [];
for (let i = 0; i < this.children.length; i++) {
const childPacketsForUpdate = this.children[i].packetsForUpdate(force);
for (let j = 0; j < childPacketsForUpdate.length; j++) {
packetsForUpdate.push(childPacketsForUpdate[j]);
} }
stepMap[key].push(substep);
}
for (const key in stepMap) {
const stateful = this._statefuls[key];
if (!stateful) {
continue;
}
stateful.patchState(stepMap[key]);
}
}
setStateful(key, stateful) {
this._statefuls[key] = stateful;
}
get state() {
return this._state;
}
tick(elapsed) {
for (const key in this._statefuls) {
const stateful = this._statefuls[key];
stateful.tick(elapsed);
}
this.updateState();
}
updateState() {
if (AVOCADO_SERVER) {
this._state = this._state.withMutations((state) => {
for (const key in this._statefuls) {
const stateful = this._statefuls[key];
state.set(key, stateful.state);
}
});
} }
return packetsForUpdate;
} }
} }

View File

@ -0,0 +1,12 @@
import {EntityPacket} from '@avocado/entity';
export class TraitAnimatedPacket extends EntityPacket {
static get schema() {
const schema = super.schema;
schema.data.currentAnimation = 'string';
schema.data.isAnimating = 'bool';
return schema;
}
}

View File

@ -4,6 +4,7 @@ import {Rectangle, Vector} from '@avocado/math';
import {Animation} from '../animation'; import {Animation} from '../animation';
import {AnimationView} from '../animation-view'; import {AnimationView} from '../animation-view';
import {TraitAnimatedPacket} from '../packets/trait-animated.packet';
const decorate = compose( const decorate = compose(
StateProperty('currentAnimation', { StateProperty('currentAnimation', {
@ -40,7 +41,7 @@ export class Animated extends decorate(Trait) {
this.animationViews = undefined; this.animationViews = undefined;
this.animationsPromise = undefined; this.animationsPromise = undefined;
this._cachedAabbs = {}; this._cachedAabbs = {};
this._currentAnimation = this.state.get('currentAnimation'); this._currentAnimation = this.state.currentAnimation;
} }
destroy() { destroy() {
@ -61,6 +62,13 @@ export class Animated extends decorate(Trait) {
this._cachedAabbs = {}; this._cachedAabbs = {};
} }
acceptPacket(packet) {
if (packet instanceof TraitAnimatedPacket) {
this.entity.currentAnimation = packet.data.currentAnimation;
this.entity.isAnimating = packet.data.isAnimating;
}
}
hideAnimation(key) { hideAnimation(key) {
if (!this.animationViews) { if (!this.animationViews) {
return; return;
@ -142,6 +150,22 @@ export class Animated extends decorate(Trait) {
return this._animations[key].offset; return this._animations[key].offset;
} }
packetsForUpdate() {
const packets = [];
if (
this.state.currentAnimation !== this.previousState.currentAnimation
|| this.state.isAnimating !== this.previousState.isAnimating
) {
packets.push(new TraitAnimatedPacket({
currentAnimation: this.state.currentAnimation,
isAnimating: this.state.isAnimating,
}, this.entity));
this.previousState.currentAnimation = this.state.currentAnimation;
this.previousState.isAnimating = this.state.isAnimating;
}
return packets;
}
setSpriteScale() { setSpriteScale() {
if (!this.animationViews) { if (!this.animationViews) {
return; return;

View File

@ -1,13 +1,13 @@
import * as I from 'immutable';
import {compose, EventEmitter, Property} from '@avocado/core'; import {compose, EventEmitter, Property} from '@avocado/core';
import {Entity, EntityList} from '@avocado/entity'; import {Entity, EntityCreatePacket, EntityList} from '@avocado/entity';
import {Vector} from '@avocado/math'; import {Vector} from '@avocado/math';
import {ShapeList} from '@avocado/physics'; 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';
import {LayerCreatePacket} from './packets/layer-create.packet';
import {TileUpdatePacket} from './packets/tile-update.packet';
const decorate = compose( const decorate = compose(
EventEmitter, EventEmitter,
@ -25,9 +25,10 @@ const decorate = compose(
export class Layer extends decorate(class {}) { export class Layer extends decorate(class {}) {
constructor() { constructor(json) {
super(); super();
this.entityList = new EntityList(); this.entityList = new EntityList();
this.index = -1;
this.tileGeometry = []; this.tileGeometry = [];
this.tiles = new Tiles(); this.tiles = new Tiles();
// Listeners. // Listeners.
@ -36,8 +37,19 @@ export class Layer extends decorate(class {}) {
this.tiles.on('dataChanged', this.onTileDataChanged, this); this.tiles.on('dataChanged', this.onTileDataChanged, this);
this.on('tilesetChanged', this.onTilesetChanged, this); this.on('tilesetChanged', this.onTilesetChanged, this);
this.on('tilesetUriChanged', this.onTilesetUriChanged, this); this.on('tilesetUriChanged', this.onTilesetUriChanged, this);
this.onTilesetUriChanged();
this.on('worldChanged', this.onWorldChanged, this); this.on('worldChanged', this.onWorldChanged, this);
if ('undefined' !== typeof json) {
this.fromJSON(json);
}
}
acceptPacket(packet) {
if (packet instanceof TileUpdatePacket) {
this.tiles.acceptPacket(packet);
}
if (packet instanceof EntityCreatePacket) {
this.entityList.acceptPacket(packet);
}
} }
addEntity(entity) { addEntity(entity) {
@ -92,10 +104,9 @@ export class Layer extends decorate(class {}) {
fromJSON(json) { fromJSON(json) {
if (json.entities) { if (json.entities) {
json.entities.forEach((entityJSON) => { for (let i = 0; i < json.entities.length; i++) {
const entity = new Entity(entityJSON); this.entityList.addEntity(new Entity(json.entities[i]));
this.entityList.addEntity(entity); }
});
} }
if (json.tiles) { if (json.tiles) {
this.tiles.fromJSON(json.tiles) this.tiles.fromJSON(json.tiles)
@ -156,6 +167,19 @@ export class Layer extends decorate(class {}) {
} }
} }
packetsForUpdate(force = false) {
const packets = [];
// Create layer during a force.
if (force) {
packets.push(new LayerCreatePacket(this.toJSON()));
}
const entityListPackets = this.entityList.packetsForUpdate(force);
for (let i = 0; i < entityListPackets.length; i++) {
packets.push(entityListPackets[i]);
}
return packets;
}
removeEntity(entity) { removeEntity(entity) {
this.entityList.removeEntity(entity); this.entityList.removeEntity(entity);
} }
@ -177,7 +201,14 @@ export class Layer extends decorate(class {}) {
} }
tick(elapsed) { tick(elapsed) {
this.tickSynchronized(elapsed); this.entityList.tick(elapsed);
}
toJSON() {
return {
tilesetUri: this.tilesetUri,
tiles: this.tiles.toJSON(),
};
} }
visibleEntities(query) { visibleEntities(query) {

View File

@ -1,9 +1,12 @@
import * as I from 'immutable'; import * as I from 'immutable';
import {arrayUnique, compose, EventEmitter, flatten} from '@avocado/core'; import {arrayUnique, compose, EventEmitter, flatten} from '@avocado/core';
import {EntityCreatePacket} from '@avocado/entity';
import {Synchronized} from '@avocado/state'; import {Synchronized} from '@avocado/state';
import {Layer} from './layer'; import {Layer} from './layer';
import {LayerCreatePacket} from './packets/layer-create.packet';
import {TileUpdatePacket} from './packets/tile-update.packet';
const decorate = compose( const decorate = compose(
EventEmitter, EventEmitter,
@ -12,15 +15,29 @@ const decorate = compose(
export class Layers extends decorate(class {}) { export class Layers extends decorate(class {}) {
constructor() { constructor(json) {
super(); super();
this.layers = {}; this.layers = [];
if ('undefined' !== typeof json) {
this.fromJSON(json);
}
} }
*[Symbol.iterator]() { *[Symbol.iterator]() {
for (const index in this.layers) { for (let i = 0; i < this.layers.length; i++) {
const layer = this.layers[index]; yield this.layers[i];
yield {index, layer}; }
}
acceptPacket(packet) {
if (packet instanceof LayerCreatePacket) {
this.addLayer(new Layer(packet.data));
}
if (packet instanceof TileUpdatePacket) {
this.layers[packet.data.layer].acceptPacket(packet);
}
if (packet instanceof EntityCreatePacket) {
this.layers[packet.data.layer].acceptPacket(packet);
} }
} }
@ -32,34 +49,36 @@ export class Layers extends decorate(class {}) {
layer.addEntity(entity); layer.addEntity(entity);
} }
addLayer(index, layer) { addLayer(layer) {
layer.on('entityAdded', this.onEntityAddedToLayers, this); layer.on('entityAdded', this.onEntityAddedToLayers, this);
layer.on('entityRemoved', this.onEntityRemovedFromLayers, this); layer.on('entityRemoved', this.onEntityRemovedFromLayers, this);
this.layers[index] = layer; this.layers.push(layer);
this.emit('layerAdded', layer, index); layer.index = this.layers.length - 1;
this.emit('layerAdded', layer);
} }
allEntities() { allEntities() {
let allEntities = []; let allEntities = [];
for (const index in this.layers) { for (let i = 0; i < this.layers.length; i++) {
const layer = this.layers[index]; const layerEntities = this.layers[i].allEntities();
allEntities = allEntities.concat(layer.allEntities()); for (let j = 0; j < layerEntities.length; j++) {
allEntities.push(layerEntities[j]);
}
} }
return allEntities; return allEntities;
} }
destroy() { destroy() {
for (const index in this.layers) { for (let i = 0; i < this.layers.length; i++) {
const layer = this.layers[index]; const layer = this.layers[i];
this.removeLayer(index); this.removeLayer(layer);
layer.destroy(); layer.destroy();
} }
} }
findEntity(uuid) { findEntity(uuid) {
for (const index in this.layers) { for (let i = 0; i < this.layers.length; i++) {
const layer = this.layers[index]; const foundEntity = this.layers[i].findEntity(uuid);
const foundEntity = layer.findEntity(uuid);
if (foundEntity) { if (foundEntity) {
return foundEntity; return foundEntity;
} }
@ -67,13 +86,8 @@ export class Layers extends decorate(class {}) {
} }
fromJSON(json) { fromJSON(json) {
if (json) { for (let i = 0; i < json.length; i++) {
for (const index in json) { this.addLayer(new Layer(json[i]));
const layerJSON = json[index];
const layer = new Layer()
this.addLayer(index, layer);
layer.fromJSON(layerJSON);
}
} }
return this; return this;
} }
@ -90,12 +104,15 @@ export class Layers extends decorate(class {}) {
this.emit('entityRemoved', entity); this.emit('entityRemoved', entity);
} }
patchStateStep(index, step) { packetsForUpdate(force = false) {
const layer = this.layers[index] ? this.layers[index] : new Layer(); const packets = [];
if (!this.layers[index]) { for (let i = 0; i < this.layers.length; i++) {
this.addLayer(index, layer); const layerPacketsForUpdate = this.layers[i].packetsForUpdate(force);
for (let j = 0; j < layerPacketsForUpdate.length; j++) {
packets.push(layerPacketsForUpdate[j]);
}
} }
layer.patchState([step]); return packets;
} }
removeEntityFromLayer(entity, layerIndex) { removeEntityFromLayer(entity, layerIndex) {
@ -106,41 +123,30 @@ export class Layers extends decorate(class {}) {
layer.removeEntity(entity); layer.removeEntity(entity);
} }
removeLayer(index) { // TODO: fix index for remaining layers
const layer = this.layers[index]; removeLayer(layer) {
if (!layer) { const index = this.layers.indexOf(layer);
if (-1 === index) {
return; return;
} }
layer.off('entityAdded', this.onEntityAddedToLayers); layer.off('entityAdded', this.onEntityAddedToLayers);
layer.off('entityRemoved', this.onEntityRemovedFromLayers); layer.off('entityRemoved', this.onEntityRemovedFromLayers);
delete this.layers[index]; this.layers.splice(index, 1);
this.emit('layerRemoved', layer, index); this.emit('layerRemoved', layer);
} }
tick(elapsed) { tick(elapsed) {
if (this.layers) { for (let i = 0; i < this.layers.length; i++) {
for (const index in this.layers) { this.layers[i].tick(elapsed);
const layer = this.layers[index];
layer.tick(elapsed);
}
if (AVOCADO_SERVER) {
this.state = this.state.withMutations((state) => {
for (const index in this.layers) {
const layer = this.layers[index];
state.set(index, layer.state);
}
});
}
} }
} }
visibleEntities(query) { visibleEntities(query) {
const entities = []; const entities = [];
for (const index in this.layers) { for (let i = 0; i < this.layers.length; i++) {
const layer = this.layers[index]; const layerVisibleEntities = this.layers[i].visibleEntities(query);
const layerVisibleEntities = layer.visibleEntities(query); for (let j = 0; j < layerVisibleEntities.length; j++) {
for (let i = 0; i < layerVisibleEntities.length; i++) { const layerVisibleEntity = layerVisibleEntities[j];
const layerVisibleEntity = layerVisibleEntities[i];
entities.push(layerVisibleEntity); entities.push(layerVisibleEntity);
} }
} }

View File

@ -0,0 +1,18 @@
import {Packet} from '@avocado/net';
export class LayerCreatePacket extends Packet {
static get schema() {
return {
...super.schema,
data: {
tilesetUri: 'string',
tiles: {
size: ['uint16'],
data: ['uint16'],
},
},
};
}
}

View File

@ -0,0 +1,12 @@
import {Packet} from '@avocado/net';
export class RoomSizeUpdatePacket extends Packet {
static get schema() {
return {
...super.schema,
data: 'uint32',
};
}
}

View File

@ -0,0 +1,12 @@
import {Packet} from '@avocado/net';
export class TileUpdatePacket extends Packet {
static get schema() {
return {
...super.schema,
data: ['uint16'],
};
}
}

View File

@ -1,11 +1,15 @@
import * as I from 'immutable'; import * as I from 'immutable';
import {compose, EventEmitter, Property} from '@avocado/core'; import {compose, EventEmitter, Property} from '@avocado/core';
import {EntityCreatePacket, EntityPacket, EntityRemovePacket} from '@avocado/entity';
import {Vector} from '@avocado/math'; import {Vector} from '@avocado/math';
import {RectangleShape} from '@avocado/physics'; import {RectangleShape} from '@avocado/physics';
import {Synchronized} from '@avocado/state'; import {Synchronized} from '@avocado/state';
import {Layers} from './layers'; import {Layers} from './layers';
import {LayerCreatePacket} from './packets/layer-create.packet';
import {RoomSizeUpdatePacket} from './packets/room-size-update.packet';
import {TileUpdatePacket} from './packets/tile-update.packet';
const ROOM_BOUND_SIZE = 64; const ROOM_BOUND_SIZE = 64;
const HALF_ROOM_BOUND_SIZE = ROOM_BOUND_SIZE / 2; const HALF_ROOM_BOUND_SIZE = ROOM_BOUND_SIZE / 2;
@ -36,6 +40,34 @@ export class Room extends decorate(class {}) {
this.on('worldChanged', this.onWorldChanged, this); this.on('worldChanged', this.onWorldChanged, this);
} }
acceptPacket(packet) {
if (
packet instanceof LayerCreatePacket
|| packet instanceof TileUpdatePacket
|| packet instanceof EntityCreatePacket
) {
this.layers.acceptPacket(packet);
}
if (packet instanceof EntityRemovePacket) {
const entity = this.findEntity(packet.data.uuid);
if (entity) {
entity.destroy();
}
return;
}
if (packet instanceof EntityPacket) {
const entity = this.findEntity(packet.data.uuid);
if (entity) {
entity.acceptPacket(packet);
}
}
if (packet instanceof RoomSizeUpdatePacket) {
const x = packet.data & 0xFFFF;
const y = packet.data >> 16;
this.size = [x, y];
}
}
addEntityToLayer(entity, layerIndex = 0) { addEntityToLayer(entity, layerIndex = 0) {
this.layers.addEntityToLayer(entity, layerIndex); this.layers.addEntityToLayer(entity, layerIndex);
} }
@ -78,7 +110,7 @@ export class Room extends decorate(class {}) {
this.emit('entityRemoved', entity); this.emit('entityRemoved', entity);
} }
onLayerAdded(layer, index) { onLayerAdded(layer) {
layer.world = this.world; layer.world = this.world;
} }
@ -88,7 +120,7 @@ export class Room extends decorate(class {}) {
onWorldChanged() { onWorldChanged() {
const world = this.world; const world = this.world;
for (const {layer} of this.layers) { for (const layer of this.layers) {
layer.world = world; layer.world = world;
for (const entity of layer.entityList) { for (const entity of layer.entityList) {
if (entity.is('physical')) { if (entity.is('physical')) {
@ -100,16 +132,22 @@ export class Room extends decorate(class {}) {
this.updateBounds(); this.updateBounds();
} }
removeEntityFromLayer(entity, layerIndex = 0) { packetsForUpdate(force = false) {
this.layers.removeEntityFromLayer(entity, layerIndex); const packets = [];
if (force) {
const packed = (this.height << 16) | (this.width);
packets.push(new RoomSizeUpdatePacket(packed));
}
// Layers packets.
const layersPacketsForUpdate = this.layers.packetsForUpdate(force);
for (let i = 0; i < layersPacketsForUpdate.length; i++) {
packets.push(layersPacketsForUpdate[i]);
}
return packets;
} }
synchronizedChildren() { removeEntityFromLayer(entity, layerIndex) {
return [ this.layers.removeEntityFromLayer(entity, layerIndex);
'width',
'height',
'layers',
];
} }
updateBounds() { updateBounds() {
@ -152,7 +190,7 @@ export class Room extends decorate(class {}) {
} }
tick(elapsed) { tick(elapsed) {
this.tickSynchronized(elapsed); this.layers.tick(elapsed);
if (this.world) { if (this.world) {
this.world.tick(elapsed); this.world.tick(elapsed);
} }

View File

@ -4,6 +4,8 @@ import {compose, EventEmitter} from '@avocado/core';
import {Rectangle, Vector} from '@avocado/math'; import {Rectangle, Vector} from '@avocado/math';
import {Synchronized} from '@avocado/state'; import {Synchronized} from '@avocado/state';
import {TileUpdatePacket} from './packets/tile-update.packet';
const decorate = compose( const decorate = compose(
EventEmitter, EventEmitter,
Vector.Mixin('size', 'width', 'height', { Vector.Mixin('size', 'width', 'height', {
@ -14,9 +16,17 @@ const decorate = compose(
export class Tiles extends decorate(class {}) { export class Tiles extends decorate(class {}) {
constructor() { constructor(json) {
super(); super();
this.data = I.List(); this.data = [];
if ('undefined' !== typeof json) {
this.fromJSON(json);
}
}
acceptPacket(packet) {
if (packet instanceof TileUpdatePacket) {
}
} }
forEachTile(fn) { forEachTile(fn) {
@ -25,7 +35,7 @@ export class Tiles extends decorate(class {}) {
let i = 0; let i = 0;
for (let k = 0; k < height; ++k) { for (let k = 0; k < height; ++k) {
for (let j = 0; j < width; ++j) { for (let j = 0; j < width; ++j) {
fn(this.data.get(i), x, y, i); fn(this.data[i], x, y, i);
++i; ++i;
++x; ++x;
} }
@ -39,26 +49,11 @@ export class Tiles extends decorate(class {}) {
this.size = json.size; this.size = json.size;
} }
if (json.data) { if (json.data) {
this.data = I.fromJS(json.data); this.data = json.data.slice(0);
} }
return this; return this;
} }
patchStateStep(key, step) {
if ('data' === key) {
const oldData = this.data;
for (const i in step.value) {
const index = parseInt(i);
this.data = this.data.set(index, step.value[i]);
}
if (oldData !== this.data) {
this.emit('dataChanged');
}
return;
}
super.patchStateStep(key, step);
}
get rectangle() { get rectangle() {
return Rectangle.compose([0, 0], this.size); return Rectangle.compose([0, 0], this.size);
} }
@ -69,10 +64,10 @@ export class Tiles extends decorate(class {}) {
return; return;
} }
const index = y * this.width + x; const index = y * this.width + x;
if (index < 0 || index >= this.data.size) { if (index < 0 || index >= this.data.length) {
return; return;
} }
this.data = this.data.set(index, tile); this.data[index] = tile;
this.emit('dataChanged'); this.emit('dataChanged');
} }
@ -94,7 +89,7 @@ export class Tiles extends decorate(class {}) {
const slice = new Array(sliceWidth * sliceHeight); const slice = new Array(sliceWidth * sliceHeight);
for (let j = 0; j < sliceHeight; ++j) { for (let j = 0; j < sliceHeight; ++j) {
for (let i = 0; i < sliceWidth; ++i) { for (let i = 0; i < sliceWidth; ++i) {
slice[sliceRow + x] = this.data.get(dataRow + x); slice[sliceRow + x] = this.data[dataRow + x];
x++; x++;
} }
sliceRow += sliceWidth; sliceRow += sliceWidth;
@ -104,26 +99,14 @@ export class Tiles extends decorate(class {}) {
return slice; return slice;
} }
synchronizedChildren() {
return [
'width',
'height',
'data',
];
}
tick(elapsed) {
this.tickSynchronized(elapsed);
}
tileAt(x, y) { tileAt(x, y) {
return this.data.get(y * this.width + x); return this.data[y * this.width + x];
} }
toJSON() { toJSON() {
return { return {
size: [...this.size], size: Vector.copy(this.size),
data: [...this.data.toJS()], data: this.data.slice(0),
}; };
} }